Greetings, traveler!
An AI agent can add an API client, a view model, a SwiftUI screen, and a navigation route in a single session. The resulting pull request may even build successfully. Reviewing it is another matter: a decision in the networking layer can be buried under hundreds of lines of UI code, while a small change to the navigation contract affects several files at once.
If the API contract is buried in that diff, a reviewer may discover a problem only after the screen and navigation already depend on it. Breaking the work into reviewable changes gives the team a chance to examine that contract earlier and makes the effect of later revisions easier to follow.
This article uses a hypothetical iOS feature to walk through that process. The Git commands illustrate one possible setup; branch protection, merge policies, and CI requirements vary between teams.
Define the boundaries of the feature
Imagine an existing app needs a delivery tracking screen. A customer opens an order, sees its current delivery status, and can retry if loading fails. The project already has a networking layer, a navigation coordinator, and a SwiftUI feature module.
Before asking an agent to implement the feature, identify the decisions that need review:
- What does the tracking API return, and how are its errors represented?
- How does the feature turn that response into loading, content, and error states?
- How does the screen display those states and trigger a retry?
- How does an order route into the new screen?
These questions suggest four changes. They do not require four pull requests in every project. A small screen with a stable API may fit comfortably in one. The split becomes useful when each change has an understandable purpose and can be checked without waiting for all the others.
| Change | Scope | Useful verification |
|---|---|---|
| API and models | Decode the response and expose the existing networking layer’s interface | Decoding and error-mapping tests; build affected targets |
| Feature logic | Load tracking details and manage screen state | Tests for success, failure, and retry using a controlled dependency |
| SwiftUI screen | Render the states and send user actions | Build, previews or simulator checks for loading, content, and error |
| Navigation | Open tracking from an order and pass the correct identifier | Route tests where available and an end-to-end simulator check |
The boundaries are a starting point. If the UI exposes a flaw in the state model, revise the earlier change instead of preserving the plan at all costs. Conversely, avoid creating a separate PR for every file: a reviewer should be able to understand why the change exists and what it enables.
Each intermediate change should build and preserve existing behavior where practical. An API that is temporarily unused can still be reviewed and tested. A half-written model that breaks the app should not be merged merely to keep a PR small. If a feature must remain inaccessible until all its pieces are ready, use the team’s existing release controls or leave the entry point for the final change.
Give the agent a contract for each step
The agent needs more than a list of files to create. Give it the intended behavior, the boundaries of the current step, and the checks that would make the result reviewable. For the feature logic, a task might look like this:
Implement the state and loading behavior for delivery tracking.
Use the tracking client introduced in the API change. Follow the
existing feature module's conventions for dependency injection and
Swift concurrency.
The state must represent loading, loaded content, and a recoverable
error. Retrying after an error must start a new request.
Keep the SwiftUI screen and navigation outside this change. Add tests
for a successful response, an error, and a successful retry. Run the
relevant tests and report the command, result, and any checks you
could not run. Show the final diff before creating a commit.
The request leaves room for the agent to adapt to the codebase while making the expected behavior and scope explicit. It also asks for a result that a developer can inspect. The engineer still needs to decide whether the state model fits the rest of the application and whether the tests cover meaningful behavior.
Start by letting the agent inspect the existing modules and propose a small plan. Check its assumptions about the API, actor isolation, and navigation before it implements dependent layers. A mistake in the initial interface can otherwise travel through the entire stack.
Build a stack of reviewable changes
A stacked workflow places each branch on top of the preceding one. Each pull request targets its immediate parent, so its review focuses on the incremental change. For this example:
| Pull request | Source branch | Target branch |
|---|---|---|
| API and models | tracking-api | main |
| Feature logic | tracking-logic | tracking-api |
| SwiftUI screen | tracking-ui | tracking-logic |
| Navigation | tracking-navigation | tracking-ui |
The navigation branch contains the history of the preceding changes, but the PR against tracking-ui shows the navigation delta. Reviewers can start with the lower PRs while implementation continues above them. The stack also makes dependencies visible: the UI PR depends on the feature logic, and the feature logic depends on the API contract.
A minimal local sequence, assuming a clean working tree and an up-to-date origin/main, is:
git fetch origin
git switch -c tracking-api origin/main
# Implement, verify, and commit the API change.
git switch -c tracking-logic
# Implement, verify, and commit the feature logic.
git switch -c tracking-ui
# Implement, verify, and commit the screen.
git switch -c tracking-navigation
# Implement, verify, and commit the navigation integration.
Push each branch and create the PRs with the bases from the table. Before creating the next branch, confirm that the current one contains the intended committed changes. git status, git log --oneline, and a comparison with the parent branch help expose work left behind or included by mistake.
To inspect the UI change locally:
git diff tracking-logic...tracking-ui
git log --oneline tracking-logic..tracking-ui
The three-dot diff compares the UI branch with its merge base with tracking-logic. Check the actual branch relationship if the output is unexpected. A hosting service may display and update PR diffs differently as bases change, so inspect the web review as well.
Keep the stack healthy as reviews arrive
Suppose a reviewer notices that the API maps an expired order to a generic error, while the product requires a specific message. The correction belongs in tracking-api, along with a test for the mapping. After that change, update the dependent branches from the bottom up.
Before changing the API branch, save the current tips of every branch in the stack. These local backup branches preserve both the old boundaries and a recovery point. The commands assume each branch exists, all relevant changes are committed, the working tree is clean, and the backup names are unused:
git branch backup/tracking-api tracking-api
git branch backup/tracking-logic tracking-logic
git branch backup/tracking-ui tracking-ui
git branch backup/tracking-navigation tracking-navigation
git switch tracking-api
# Fix the error mapping, run the relevant tests, and commit the fix.
Now move the commits unique to each child onto its updated parent. Use the saved parent tip as the old boundary, and complete each rebase before starting the next:
git switch tracking-logic
git rebase --onto tracking-api backup/tracking-api
# Check the result and run the affected tests before continuing.
git switch tracking-ui
git rebase --onto tracking-logic backup/tracking-logic
# Check the result and run the affected checks before continuing.
git switch tracking-navigation
git rebase --onto tracking-ui backup/tracking-ui
For example, the first rebase selects the commits on tracking-logic after backup/tracking-api and replays them onto the updated tracking-api. The second uses the old tip of tracking-logic as its boundary, so it selects the UI commits even though the logic branch now has a different history. This example assumes a linear stack with no extra merges. If the history is more complicated, inspect it before choosing the boundaries.
If a rebase stops on a conflict, resolve it, stage the affected files, and run git rebase --continue. Use git rebase --abort to return that branch to its state before the current rebase if you need to reconsider. Inspect git log --graph --oneline --all and compare each updated branch with its parent. Also check the change against the saved version, for example with git range-diff backup/tracking-logic..backup/tracking-ui tracking-logic..tracking-ui. A successful rebase does not prove that the code still behaves correctly; rerun checks affected by the contract and test the integrated feature at the top.
Rebasing published branches rewrites history. Coordinate with anyone using them and follow the team’s push policy. A plain --force-with-lease uses a remote-tracking reference by default; background fetches can update that reference. For a stronger check, record the remote branch tip you have inspected and pass it explicitly as the expected value. For example, after agreeing that the observed origin/tracking-logic is the version to replace:
git fetch origin
git log --oneline tracking-logic..origin/tracking-logic
# Inspect any remote commits and coordinate with their authors before proceeding.
expected_logic=$(git rev-parse refs/remotes/origin/tracking-logic)
git push origin \
--force-with-lease=refs/heads/tracking-logic:$expected_logic \
tracking-logic:refs/heads/tracking-logic
The push is rejected if the remote branch no longer has the recorded tip. The lease does not tell you whether that tip contains work you have already agreed to preserve, so inspect remote changes before capturing it. Repeat the process for each rewritten branch that needs to be published. Once the updated stack is verified and no one needs the saved state, remove the backup branches using the team’s normal cleanup process.
Merge the stack after a squash merge
Merge from the bottom upward. With a squash merge, the API’s changes reach main as a new commit, so the old API tip is not an ancestor of main. Retargeting the next PR alone can leave its history or diff confusing. The following example assumes tracking-api has just been squash-merged, the current stack has no additional merges, and the other PRs are still open.
Before updating any child, save the current branch tips using fresh backup names. If you have already made changes to the branches, these must reflect the state immediately before this operation:
git branch before-merge/api tracking-api
git branch before-merge/logic tracking-logic
git branch before-merge/ui tracking-ui
git fetch origin
git switch tracking-logic
git rebase --onto origin/main before-merge/api
This replays only the commits after the old API tip onto the fetched main. It assumes the squash commit is present in origin/main and contains the intended API changes. Check that before running the rebase. Then update the remaining children using their saved parent tips:
git switch tracking-ui
git rebase --onto tracking-logic before-merge/logic
git switch tracking-navigation
git rebase --onto tracking-ui before-merge/ui
Resolve any conflicts and inspect each updated diff, as in the previous example. Publish the rewritten branches according to the team’s policy, retarget the logic PR to main, and check the PR view on the hosting service. Run the required checks on the new commit. Repeat the process as further PRs merge, accounting for the chosen merge method and any new changes on main. A previously green CI run belongs to its original commit.
Make review comments actionable
A review comment often mixes an observation with a proposed solution. Give the agent both the surrounding code and the reason for the concern. Ask it to verify the claim before changing anything.
For example, a reviewer might point out that tapping Retry repeatedly can start overlapping requests. The agent should inspect the feature’s request ownership, explain whether overlap is possible, and propose behavior consistent with the product requirements. Depending on the design, that may mean disabling retry during loading, cancelling an earlier task, or allowing a newer result to replace an older one. Each choice has different consequences and needs an appropriate test.
A useful handoff for a review pass includes the branch, the relevant comment, the expected behavior, and permission boundaries:
Review the feedback on tracking-logic concerning repeated retries.
Determine whether overlapping requests can occur and explain the
observed behavior. If the concern is valid, implement a fix in this
branch and add a test. Do not change the API or UI branches yet.
Report the diff, the test result, and any follow-up needed above it.
If the fix changes the logic branch, update its UI and navigation descendants using the saved-boundary approach above, then verify the UI and integrated route again. Some comments will be preferences or based on an incomplete reading. An agent should be able to explain why it disagrees, with reference to the code. A developer can then decide how to resolve the discussion. After a fix, check the actual diff and rerun relevant tests; a response saying that feedback was addressed is not evidence by itself.
Use worktrees to separate concurrent work
A Git worktree provides another working directory attached to the same repository. Each linked worktree has its own checked-out files, index, and HEAD; repository history and branch references are shared. This is useful when one agent is implementing a change while a developer investigates a separate issue, or when independent agents work on separate branches.
The following continues the earlier example, where tracking-api already exists. First move the current checkout off that branch. Check for uncommitted work before switching, and use git worktree list to confirm that the branch is not checked out elsewhere:
git status
git worktree list
git switch main
git worktree add ../app-tracking-api tracking-api
git fetch origin
git worktree add -b order-bugfix ../app-order-bugfix origin/main
git worktree list
The first worktree add checks out the existing branch; the second creates a new branch for an independent bug fix. The checkout of main must already exist locally and be available in the current worktree. If it is checked out elsewhere, switch to another suitable branch or use a detached checkout at an appropriate commit instead. Choose unused paths and ensure order-bugfix does not already exist.
The directories can now be opened independently. Normal Git operations still apply, and there is no need to create another remote repository. Git normally prevents the same branch from being checked out in multiple worktrees; use separate branches rather than forcing a shared checkout.
A worktree isolates files on disk, not every shared resource. Agents can still affect shared Git references and remotes, write to a common temporary directory, use the same credentials, or target the same simulator and backend. Assign ownership of branches and shared environments before parallel work begins. Uncommitted or ignored local files from the original checkout do not automatically appear in a new worktree, so project-specific setup may be needed.
For tightly coupled changes, let one agent own the edits while others investigate or review. If tasks have stable interfaces and separate owners, agents can work in different worktrees, then integrate their changes in dependency order. Agree on shared contracts first: separate files can still encode incompatible assumptions. If the coordination and repeated builds cost more than the time saved, keep the work sequential.
When a worktree is no longer needed, check git status inside it, preserve any work you need, and remove it from the main repository:
git worktree remove ../app-order-bugfix
This removes the worktree, not the branch itself. Avoid forcing removal when it contains changes you have not inspected.
Separate Xcode build output
Parallel checkouts can still compete for build artifacts if they use the same DerivedData directory. Give concurrent builds separate paths. For example, from two worktrees of the same application:
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-derivedDataPath "$HOME/Library/Developer/Xcode/DerivedData/App-tracking" \
build
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-derivedDataPath "$HOME/Library/Developer/Xcode/DerivedData/App-bugfix" \
build
Replace the workspace, scheme, and destination with values available in the project and installed Xcode. Distinct paths reduce interference around DerivedData and its build database, but they also duplicate artifacts and can increase disk use and build time. They do not isolate simulator state, shared package caches, a mock server, or test accounts. Concurrent UI tests may need separate simulator instances and test data as well.
For CI, use paths unique to the job or checkout, and retain the logs and test results according to the team’s existing process. If a build fails, report the failure and its context rather than treating another branch’s successful build as a substitute.
Verify the change at the right level
Every step needs checks suited to what it changes. The API branch can prove that sample payloads decode and errors map as expected. The logic branch can check state transitions and retry behavior. The UI branch can be built and inspected in representative states. The final branch needs an integrated route through the app.
For each PR, record the commit or branch state tested, the commands or actions performed, and their results. Mention skipped checks and known gaps. For example, a passing unit test for a route does not prove that the screen is reachable from a running application. A simulator walkthrough can cover that path, although it still does not replace device testing where device-specific behavior matters.
When a base change modifies a public contract, reassess the checks above it. A formatting-only change may need little additional work; a changed error case can affect feature logic, copy, and navigation. CI results tied to an older commit should remain historical evidence, not be presented as proof for a new one.
Review the combined feature too. Small PRs help isolate decisions, but defects can appear at their boundaries: the API may return one identifier while the navigation passes another, or the UI may lack a state that the model can produce. A final end-to-end pass checks those interactions.
Know when to simplify
Stacked branches have costs. Updating a low-level contract can trigger several rebases, repeated CI runs, and another review of dependent changes. A reviewer may also struggle to understand a UI PR without seeing its intended final context. Link the related PRs and describe the end-user outcome in each one.
Use a shorter stack, or a single PR, when the change is small, the parts cannot stand independently, or the team’s review and merge tooling makes a stack expensive to maintain. A single branch with well-structured commits can still make a change easier to inspect, provided the final PR remains understandable. For independent work, separate branches based on main may be simpler than a chain of dependencies.
Choose one PR for a small, cohesive change. Use a short stack when later changes need earlier code and each step is worth reviewing on its own. For unrelated tasks, start independent branches from the shared base and integrate them separately.
It might be interesting:
- Evidence-Driven Development: How to Verify AI Agent Changes
- Agent Skills, Tools, and MCP: Giving AI Agents Useful Capabilities
- From requirements to implementation: designing tasks for AI agents
- Context Engineering: Helping AI Agents Navigate an Existing iOS Project
- Agentic Engineering: How Development With AI Agents Works
