Agentic Skills

The Delivery Loop

How build-item selects, builds, reviews, merges, and releases planned work and one-off items.

The Delivery Loop

This page covers the mechanics. For why the loop works this way, see The Idea Behind the Loop.

Start with one item:

/devstride:build-item

build-item selects the next unblocked leaf, marks it In Progress, creates a branch, builds and reviews it, merges it through the configured path, updates the DevStride item, and stops after that iteration. Once you trust one pass, /loop /devstride:build-item repeats the process across the plan.

The delivery profile

Every iteration begins by resolving the plan’s delivery profile and announcing it with its source. The first match wins:

  1. a bare profile word — prototype, standard, extended, or enterprise — anywhere in the invoking skill’s arguments; build-item accepts one, as plan does;
  2. the Delivery profile: marker in the plan root’s description — a one-off has no plan root, so no marker;
  3. profile in .claude/ds-config.json;
  4. standard.

The profile sets how many readers the build engine fans out to understand the item, the widest adversarial review breadth it may choose, which verified findings are fixed in the item and which are deferred with a rationale, how many rounds the local CLI reviewer gets at a merge boundary, the local gate an item must pass before it merges, and how long the loop waits for a cloud reviewer. The normal target of two adversarial cycles is the same under all four. Under extended only the size-shaped settings move — more understand readers (up to four) and a review breadth that may reach high-risk for a large diff — while the fix floor, the local gate, verification and review cycles are exactly standard’s. See Choose a delivery profile for the four profiles side by side.

Explicit configuration wins over the profile. epicIntegrationBranches.autoRelease, epicIntegrationBranches.fastStoryMerges.enabled and review.pollTimeoutMinutes stand whenever they are present in the file; the profile supplies a value only when the key is absent. A contradiction is honored and reported, never silently resolved.

The three review.* CI-ordering flags work differently: they describe what your workflows support — a draft hold, and a ready flip that releases CI — rather than a per-run choice. Review first and CI last is a floor under every profile, prototype included: where the workflows support the hold, every profile uses it. A repository that cannot prove the hold is reported as a setup blocker rather than quietly running CI alongside review.

review.localCommand is different: it names the local CLI reviewer, it does not schedule it. When present, that engine reviews every release pull request and every pull-request-path item under every profile, and never a fast-merged item that has no pull request — under any profile, prototype included. The profile decides only how many rounds it gets at those boundaries: one under prototype, standard and extended, two under enterprise. null removes it everywhere.

review.localAssistCommand is support rather than a review round. When configured, the build engine may consult it once, read-only, for an ambiguous cross-module design, a critical contract, or a stubborn diagnosis. It does not write, does not satisfy the merge review, and does not run on routine work.

First understand the paths

SituationWorking baseItem pull request?Later release
Planned leaf under a release unit; release-unit branches and fast merges enabledThe release unit’s integration branchNo. Local review, local checks, local merge.One full-diff release pull request carries the completed unit to epicIntegrationBranches.releaseTarget.
Planned leaf under a release unit; fast merges disabledThe release unit’s integration branchYes, targeting that integration branch.The completed unit still gets its release pull request.
Release-unit branches disabled, or no release-unit ancestorbaseBranchYes, targeting baseBranch.No separate release-unit branch path for that item.
One-off, support train configured (supportTrain.branch, and release.mergeTrainBeforeCut: true)The support trainNo when supportTrain.fastMerges is true: local review, local checks, local merge onto the train. Otherwise yes, targeting the train.The train's pull request into baseBranch, which release opens and fully reviews before every cut. The item is Done only when the train reaches baseBranch.
One-off with no support train, or one whose change touches release.releaseBranchFixExclusions (deploy configuration, migrations)baseBranchYes, targeting baseBranch.None. The item pull request is its full release gate.

A one-off may be filed under a release unit, so the skill does not infer its path from ancestry. It detects the one-off from the leaf type, title numbering, and dependency relationships, then explicitly bypasses release-unit branch derivation: a one-off never lands on an epic's branch.

Why a train: every merge into the base branch pays for its own CI run and, in many repositories, a deploy. One-offs that batch on the train get one full review and one CI pass at release time — the same bargain as an epic's release pull request — and the train ships with every release, so nothing waits longer than the next cut. Deploy configuration and migrations never ride it, because the train reaches production soon after it merges; they deserve their own pull request, their own CI and an earlier deploy to a development stage. The exclusion is checked against the spec when the item starts and again on the real diff just before anything merges onto the train, since a review fix can add such a file.

Selection and branch choice

Empty arguments mean “next.” You can also name a leaf, say next under I1234, or pass a parent item number by itself to scope selection under that plan.

The next item is the highest-priority open leaf whose blockers are Done, inside the earliest-dated open parent item. A blocker in the same release unit also counts once it has landed, meaning the unit's integration branch shows its merge in its git history (the merged status alone never counts, since a column of that name often holds an open code review); a blocker in another release unit counts only when Done, so the next unit waits until the previous one ships. Naming a leaf by number skips this selection, never the check that it is not already Done or landed: such a leaf is reported where it stands, not built again. Relationships are fetched explicitly because normal summary projections omit them.

Where work lands

The working-base precedence is:

  1. an explicit branch override or non-null integrationBranch — refused when it names the production branch, the release source, a protected branch or a release branch;
  2. for planned work, the nearest release-unit ancestor’s integration branch when epicIntegrationBranches.enabled is true;
  3. for a one-off, the support train when supportTrain.branch is set and releases merge it (release.mergeTrainBeforeCut), unless its change touches the fix-exclusion paths;
  4. baseBranch.

The release-unit ancestor is found by walking the entire parent chain and fetching each ancestor’s work type. The integration branch is created from a fresh base branch on the unit’s first item and then reused; its date is the creation date, not the current date. When no branch matches the configured pattern but one on origin carries the epic’s number under an older naming, the loop stops and asks rather than cutting a second integration branch for the same epic.

The precedence is code, not prose. From 3.9.0 one dependency-free helper, skills/build-item/scripts/routing.py, decides where an item lands and names its branches — JSON in, JSON out — and build-item follows its answer. Codex, a script or a person can run the same file and get the same answer. routing-fixtures.json beside it holds the canonical examples, so a repository that implements the rule in its own tooling can test that tooling against them and fail when the two drift; the DevStride repository’s ds work command is held to them that way. Why: the rule used to live only in skill prose, and every agent that re-derived it was a chance to land work on the wrong branch.

What one iteration does

  1. Establish ground truth, then select and validate. Fetch from origin first, read what other authors landed on the working base since the last handoff, and say so when another session appears active. Then show the ready set, check for human or infrastructure gates, and compare the item’s full spec with the current code.
  2. Move to In Progress. This is a live DevStride update.
  3. Create the item branch. branch-feature stops on a dirty tree and branches from the resolved working base.
  4. Build and adversarially review. ultracode-build understands the contract, implements in small green commits, and runs a mandatory maximum-effort Claude review whose breadth scales with risk, up to the profile’s ceiling. Security is always forced on a diff touching the authentication boundary, and from 3.4.0 any lens the repository declares in review.mandatoryLenses is forced the same way whenever the diff touches its paths — one focused finder each, in the story risk check and again at the merge-boundary review, which no profile or breadth ceiling removes. See Mandatory review lenses.
  5. Settle the configured delivery path. Either local fast review and merge, or a pull request through pr and review.
  6. Complete the item. Move it to Done — except an item merged onto a release unit's integration branch, which has landed but is not Done: it moves to the merged status (epicIntegrationBranches.mergedStatusName, Review by default) with a comment naming the merge, and becomes Done when the unit's release pull request merges. A one-off merged onto the support train gets a comment, keeps its status, and becomes Done when the release carries the train into the base branch. Then set the branch-to-merge date window, link the pull request or record the fast-merge commit, and reconcile material differences between the planned and shipped spec.
  7. Track discoveries. New scope with no existing home becomes an inserted Story instead of disappearing into pull-request prose, and a review finding worth filing (a P1, a security finding, or one both likely to happen and material) that is deliberately put off becomes a deferred Defect beside the plan. Any other finding is dismissed with a one-line reason in the pull request or merge commit, and nothing is filed.
  8. Sync and count down the release unit. A clean working base and the count of leaves neither Done nor landed become the handoff for the next iteration.
  9. Release the completed unit when allowed. Automatic cut and merge happens only when epicIntegrationBranches.autoRelease is true. When the release pull request merges, the release unit is closed out first (a comment naming the pull request, and Done where it tracks a status), then every landed leaf is marked Done with a comment linking it; their dates stay those of their merge onto the unit's branch. When nobody closes it out (an owner merged the release by hand, or a run stopped right after the merge), the next /devstride:build-item <root> does it before choosing what to build. Setup writes it false for the standard, extended and enterprise profiles, so a newly configured repository stops at release-ready for an owner; for prototype it writes true and says so.

The skill renders its own numbered progress table as it moves. The skill file states a goal, the rules that must hold and its hard floors, and names its steps 0–8 with a 6.5 so the progress table and other skills can refer to them; the list above is a reader-oriented summary, not those internal labels.

Verification names its path

From 1.2.0, a review verdict that rests on having looked — a browser, a running service, a manual run — reports "verified X via path Y", never a bare "verified". Behavioural states are usually reachable by more than one route, and a fix confirmed on one of them proves nothing about the others, so the verdict lists the routes to the same state and says which were and were not exercised. A reviewer later finding an untried path is the process working. The engine also asserts what it measures — scoping checks to the live container rather than a stale one — and rules out an expired session or a service that is not running before reading code.

Ground truth before the loop acts

From 3.3.0, the first act of build-item and of release is git fetch --prune origin, followed by a read of what other people — and other sessions of yours — landed on the branches the run will touch. A branch push by another author, an open pull request that is not yours, or a merge you did not make is evidence that another session is active, and the loop says so explicitly before touching that branch. Every fact carried in from a memory handoff — the integration branch, the last item that shipped, "nothing else has landed" — is treated as a claim to verify against origin or gh before it is acted on; when the two disagree, the remote wins. A checkout can be current while the conversation is not, which is why this is a step at the start of every iteration rather than something the branch step happens to do later. A repository can also opt in to a bounded fetch at every session start with localEnvironment.fetchOnSessionStart; it shortens the window but does not replace the step. Rationale: skills/build-item/references/ground-truth-at-start.md in the plugin repository.

Human-facing output

Two rules apply to everything the loop says to you. A claim about repository or delivery state — merged, published, CI green, "nothing else has landed", another session active — is read from origin, gh, or the live target at the moment it is made, never from the local checkout, a summary, or memory; a claim that cannot be backed that way is labelled unverified wherever it is repeated. And every handoff, plan, or reviewer prompt ends with two explicit lists, steps the loop does and steps you must do yourself — a login, an approval, a dashboard check, a command only you can run — with an empty second list saying nothing required from you.

Fast merges: no item pull request

With epicIntegrationBranches.fastStoryMerges.enabled: true, a planned leaf on an integration branch receives:

  • the Claude adversarial pass from the build engine;
  • the profile's immediate-risk verification whenever the item's own diff touches authentication, a migration, irreversible state, or a deployed contract;
  • the profile’s story gate — the local checks the delivery profile requires before a merge, from type-checks plus the touched test suites under prototype to type-checks, the full test suite, and lint under enterprise;
  • a --no-ff merge onto the release-unit branch after all findings and checks settle.

There is no item pull request, cloud reviewer, CI run, or review-thread bookkeeping. Those gates move to the completed release unit; they are not declared passed on the item.

Fast mode requires at least one local review engine. If no build-time Claude pass covered the diff and no local CLI is available, the item falls back to the pull-request path. Under prototype, fast mode applies whenever fastStoryMerges.enabled is absent or true; a present false wins and is reported. Red local checks always stop the merge.

When the unit reaches zero open leaves and automatic release is enabled, the loop:

  1. merges the current base branch into the shared integration branch without rebasing it;
  2. runs the configured local verification;
  3. opens the release-unit pull request;
  4. reviews the full accumulated diff, because this is the first cloud and CI pass over fast-merged code;
  5. merges only after review and applicable gates settle;
  6. deletes the integration branch only when deleteBranchAfterRelease allows it;
  7. links the release pull request back to the release unit and its constituent leaves.

With autoRelease: false, none of those release mutations starts automatically; the unit is reported release-ready. With autoRelease: "ask", the loop asks for approval for each completed unit before cutting and merging its release pull request.

Pull-request path

pr authors the configured body, requests every configured cloud reviewer when it opens the pull request, and delegates findings and CI settlement to review.

In a draft-held repository:

  1. The pull request opens as a draft.
  2. Claude, the configured local CLI, and configured cloud reviewers run concurrently. A cloud reviewer with baseBranches is requested only on pull requests into one of those branches, and one with requestPolicy: "final-head" is asked once, when every other finding is settled, on the head about to release CI — and again only after a fix that answered one of its own findings. A reviewer left out by its scope is announced as not requested, never waited on and never counted as failed; if the local engine then fails on a pull request no cloud reviewer was asked to review, the loop stops for a human review. Each cloud reviewer request must be proven registered — a review_requested event on the pull-request timeline — within two minutes; otherwise that reviewer is dropped for the run and reported, and nothing waits for it. review.pollTimeoutMinutes bounds only the wait for a registered reviewer’s review — and that wait is adaptive: the loop polls on a lengthening interval and stops at the reviewer’s own historical 95th-percentile latency plus a safety margin, learned per machine from GitHub’s own timestamps, never beyond the timeout, and the full timeout while fewer than five samples exist. Stopping at a learned bound is reported exactly as a timeout would be.
  3. Findings are verified — refuted unless reproducible — and the profile’s fix floor decides which verified findings are fixed in the item; the rest are dismissed or deferred with a rationale, and cloud threads are replied to and resolved. Two adversarial cycles is the normal target across every reviewer. Past it, only a verified P1 or a serious P2 — likely to occur and material if it does — opens another cycle, and that repeats until a cycle finds none; lower findings cannot extend the target. Each follow-up receives the cumulative ledger of prior findings and dispositions and inspects only the changes since the previous cycle — unless a computed rule sends it back over the whole diff: a file the earlier cycle never touched, more than half of its lines rewritten, or a rebase. A settled finding is therefore not rediscovered or reversed. An unchanged patch, no progress, or an unavailable required reviewer stops for a human rather than spinning or merging.
  4. Qualifying repeatable lessons may be written to lessonsDoc before CI.
  5. The head is refreshed against its base when safe.
  6. Matching local preShipChecks run against the final reviewed diff while the pull request remains held.
  7. Marking the pull request ready releases CI.
  8. The final head commit must show every applicable check successful; absent, skipped, pending, or stale results are not green.

The draft hold is conditional on review.openPullRequestsAsDraft, review.readyForReviewReleasesCi, and review.ciHeldUntilReviewSettled. When all three are false, the pull request opens ready and CI runs alongside review. Mixed values take the strictest behavior and are reported.

One-off work

/devstride:create-story add a one-time export
/devstride:create-defect retry creates duplicate records

The create skills ask for a parent, board, and assignee, create the item without plan numbering or dependency edges, and invoke build-item <new-item> once, which terminates instead of selecting another item. Where the one-off lands follows Where work lands: the support train when one is configured — in which case the item gets a comment saying it ships with the next release, and stays open until the release carries the train into the base branch — otherwise its own pull request into the base branch, which is also the path for a one-off touching deploy configuration or migrations.

Use insert-story or insert-defect when the new work belongs in an existing plan and should be reached by the dependency walk.

Hotfixes

/devstride:branch-hotfix fix-login-crash

This creates a branch from a fresh hotfixBaseBranch, so the fix does not carry unreleased development work. It does not open a pull request; build the fix, then use /devstride:pr targeting the production branch.

Production release

/devstride:release

This is separate from a release-unit pull request. It promotes release.releaseSource to release.productionBranch:

  1. fetch, read what other authors landed on both branches recently, and say so when another session appears active;
  2. merge the support train first, when release.mergeTrainBeforeCut is true: a snapshot of the train (the live train keeps receiving one-offs) goes into the release source through its own fully reviewed pull request, CI runs once, it merges, and every one-off it carried is marked Done;
  3. compute the release delta, then — when release.releaseBranchPattern is set — cut the release branch from the release source, named by the pattern for today (for example release/26-10-03, then -2, -3 on a re-cut), and open the production pull request from it; otherwise the release source itself is the head;
  4. run the configured review roster;
  5. run release-only local pre-ship checks, then settle CI;
  6. present the exact release, verification, the docs and release-notes plan, and the release.autoDeployOnMerge consequence;
  7. wait for explicit owner approval before merging production;
  8. run your local post-deploy health skill (release.postDeployCheckSkill) — only after the deploy is confirmed (by release.deployVerification or by you), never before, so it measures this release rather than the previous one or an in-flight rollout; a configured check keeps that confirmation step even when no documentation or release notes run. It runs before anything public is written: PASS continues, FAIL stops for a rollback decision, NOT RUN asks you whether to proceed, and an unset key reports not configured;
  9. after the deploy is verified live and healthy, hand the delta to your local documentation skill (docs.updateSkill) so core docs are updated by default, unless suppressed, and write a release note through your local release-notes skill — only if you passed --release-notes;
  10. close out: with a release branch, sync the production branch back into the release source by pull request (otherwise the release branch’s fix commits would never reach it), then delete the release branch once both contain it; with a support train, bring the train up to date.

There is no release freeze any more (from 3.8.0). Pull requests into the release source are not settled, parked or waited for, and ci.freezeBaseWhileReleasePrReady is ignored. With a release branch, a merge into the source meanwhile simply ships in the next release; without one, it advances the release pull request’s head, and the review round runs again before the owner is asked.

A release branch is protected. Fixes found during review are new commits on it — never a rebase, amend or force-push. A fix that touches release.releaseBranchFixExclusions (deploy configuration, migrations) is refused: merge it to the release source by a normal pull request, abandon the release branch and re-cut. A hotfix that reaches production meanwhile is merged into the release branch and re-reviewed. Why a branch at all: the release source keeps moving while a release is reviewed, so cutting a branch freezes what ships without freezing everyone else’s work.

Release notes are opt-in: --release-notes (or --release-notes draft to read the note before it is published). Without the flag no note is written, and nothing in the loop decides that a release deserves one. Passing no docs or skip docs suppresses the docs phase. docs only updates documentation for an already-shipped delta; release-notes only writes the note for one.

Switching from planning to execution

Run /clear before moving from setup, planning or Doctor into building, reviewing or releasing. The session gate introduced in 3.5.0 keeps those job classes separate; a build loop may still deliver multiple stories in one session. See Session boundaries and deferred defects for intentional overrides.

A verified finding worth filing (a P1, a security finding, or one both likely to happen and material) that is deliberately put off is filed beside the plan in its Deferred defects container and related to the reviewed item; any other unfixed finding is dismissed with its one-line reason, and nothing is filed. They do not enter the active dependency chain, are not auto-selected, and do not hold the completed release unit open. Newly discovered implementation scope still enters the plan through insert-story.