Swarm Decompose
Swarm Decompose — split one instruction into N independent subtasks
Section titled “Swarm Decompose — split one instruction into N independent subtasks”The swarm decompose engine is the lead/decompose step of the agent swarm (#577, part of #419): it turns one free-text instruction into a validated, machine-readable plan of N independent, non-overlapping subtasks — the foundation every other swarm step builds on.
It is the deliberate division of labour the parallel-orchestration research
(docs/research/parallel-agent-orchestration.md) named as the single “unsolved
piece”: the semantic split — which subtasks, what each one owns — is the lead
agent’s judgement, exactly as the article frames it (“Let the lead
decompose”). This engine is the deterministic safety + emission backbone
around that judgement. It validates the plan, analyses cross-subtask file
ownership for overlap, detects pinch points, computes a fan-out suitability
verdict, and emits both machine- and human-readable forms.
It never spawns workers. The decompose step only produces the plan. The dispatch step (the cdfork branch model, or the in-session Workflow lane) consumes it.
When to use
Section titled “When to use”- Before fanning out a broad instruction across parallel agents, to get a clean, conflict-free division of labour you can trust.
- To answer “should this even fan out?” — the verdict recommends a single agent when there are fewer than three genuinely independent pieces.
- To produce a cdfork-compatible branch list from a fresh
instruction (not just pre-existing approved issues, which
cdfork --from-issuesalready covers).
Run it all: /swarm
Section titled “Run it all: /swarm”The whole pipeline — decompose → approve → dispatch → gate → grade → integrate — is wrapped in one command (#582). Use it once the plan is approved; the sections below document each step it composes.
Two ways to run this. The wrapper is the one part of the swarm with both a
terminal entrypoint and a slash command — they forward identical arguments to the
same scripts/cli/swarm engine (bob swarm; bin/swarm is its alias shim, #2060):
Terminal:
bob swarm run [--cap <n>] [--dry-run] [--finalize]bob swarm plan --plan plan.json [--cap <n>]bob swarm summarySlash command:
/swarm run [--cap <n>] [--dry-run] [--finalize]/swarm plan --plan plan.json [--cap <n>]/swarm summary- Composes, never reimplements.
bob swarm rundrives the existing primitives by their CLIs —approve.js verify→ lane routing (#583) →bob fork forkand/or the in-session Workflow lane (fan-out + worktree isolation) → the #579 gate → the #580 grader →integrate.js. It adds only what the primitives lack. - Two lanes, routed by shape (#583). Heavyweight tickets fan out to cdfork; wide mechanical work runs in-session via the native Workflow tool. Both feed the same gate, grader, integrator, and summary — see Two dispatch lanes.
- Approval-gated. It refuses to dispatch unless
.swarm/approved-plan.jsonexists and its hash still matches — the plan cannot drift between approval and spawn. - Concurrency ceiling (
--cap, default 4). Bounds simultaneously-running workers (cdfork itself is uncapped); excess subtasks queue and start as slots free. - Idempotent resume. Run state lives in
.swarm/run.json; re-invoking is a safe resume — completed workers aren’t relaunched and the summary is recomputed. - One summary.
bob swarm summarylists every branch as passed / failed / needs-attention, and after integration as merged / returned / held, each with its blast-radius lane (#4302). AReturned:block names each returned unit with its verdict and reason, and counts the units merged despite a sibling being returned (#4301). The JSON form carries the same asreturnedandmerged_despite_returns.
Preview the exact composition without spawning anything with bob swarm run --dry-run. Full reference: the /swarm command.
The three commands
Section titled “The three commands”Terminal only. Everything below the
/swarmwrapper is an engine primitive invoked asnode scripts/swarm/*.js— there is no slash form for the individual steps (the wrapper above is the only slash surface). Run these from a shell.
Terminal:
node scripts/swarm/decompose.js scaffold "<instruction>"node scripts/swarm/decompose.js analyze <plan.json|-> [--format json|human|both]node scripts/swarm/decompose.js branches <plan.json|-> [--include-serial]1. scaffold — free-text instruction → lead brief
Section titled “1. scaffold — free-text instruction → lead brief”scaffold is the entry point for a raw instruction. It does not (and cannot,
deterministically) invent the split — it emits the brief the lead agent fills to
produce a schema-valid plan, keeping the judgement where it belongs while
guaranteeing the output is machine-consumable.
node scripts/swarm/decompose.js scaffold "Add a product catalog to the storefront" [--cwd DIR]When earlier runs in the project (--cwd, default the current directory)
recorded splitter constraints, the
brief carries a ## Standing constraints section listing every one, newest
first, each with the run it came from:
## Standing constraintsEarlier runs in this project were cut wrong in these ways. Honour each one:- src/a/** and src/b/shared.ts conflicted on src/b/shared.ts — keep them in one unit (run sha256:3f9c0e1a2b4d)With none recorded the section is absent.
2. analyze — validate + annotate
Section titled “2. analyze — validate + annotate”analyze takes a candidate plan (authored against
schemas/swarm-plan.schema.json) and returns it annotated with fan_out,
overlaps, and pinch_points. --format selects the output: json (machine),
human (readable), or both (JSON on stdout, human on stderr — the default).
node scripts/swarm/decompose.js analyze plan.json --format humananalyze also writes every worker’s brief to .swarm/briefs/<id>.md before
approval (#3557), so you can read exactly what each worker will be told:
--cwd <dir> picks the project root, --no-briefs skips the files. A brief
longer than _workflow.swarm_brief_max_lines (default 150) means the piece is
too big for one worker. The verdict names each one in fan_out.over_budget
and its reason, and the human output prints a WARNING: line. Split the
subtask, or trim its brief. The verdict itself is unchanged. See
Worker briefs.
Exit codes let CI / the dispatch step gate on the result:
| Exit | Meaning |
|---|---|
0 |
Valid plan, no file-ownership overlaps — safe to fan out. |
3 |
Valid plan, but overlaps remain — resolve before fan-out. |
1 |
Invalid plan (schema or semantic error). |
2 |
Usage error. |
3. branches — emit a cdfork branch list
Section titled “3. branches — emit a cdfork branch list”branches emits the same tab-separated <branch>\t<id>\t<title> contract that
bob fork fork consumes from from-issues.sh. serial_only subtasks (pinch
points) are excluded by default so they are never fanned out; pass
--include-serial to override.
node scripts/swarm/decompose.js branches plan.json# feat/api-routes-... api-routes Add the JSON:API product endpoints# feat/product-grid-... product-grid Build the Next.js product grid pageThe plan format
Section titled “The plan format”A plan is one instruction plus a list of subtasks, each owning a disjoint
set of globs. Authored fields are required; the engine computes the rest. Full
contract: schemas/swarm-plan.schema.json (a sample lives at
schemas/fixtures/swarm-plan.sample.json).
{ "instruction": "Add a product catalog to the storefront", "subtasks": [ { "id": "api-routes", "title": "Add the JSON:API product endpoints", "owned_globs": ["src/api/products/**"], "deliverable": "a Hono router exporting product list/detail handlers", "verification": "npm test -- products" } ]}| Field | Required | Purpose |
|---|---|---|
id |
✓ | kebab-case, unique; substitutes for an issue number in the TSV. |
title |
✓ | Drives the derived <kind>/<id>-<slug> branch name. |
owned_globs |
✓ | The files this subtask is the sole writer of. |
deliverable |
✓ | The concrete artifact produced. |
verification |
✓ | A command that proves it is done. |
branch |
Explicit branch name (else derived from the title). | |
serial_only |
Force out of the fan-out (engine sets it for detected pinch points). | |
depends_on |
Ids that must finish first; excludes this subtask from the independent count. | |
brief |
Everything else the worker needs: the files to read first, the constraints, the interfaces it must match (#3557). See Worker briefs. |
Worker briefs
Section titled “Worker briefs”A worker sees nothing of the lead’s conversation, so whatever it needs and cannot find in its owned files has to be in its brief. The rule, taken from ai-website-cloner-template’s parallel build (#1239), is to give every worker its whole brief inline, keep a copy as a file so it can be audited, and treat a long brief as a sign the piece is too big.
-
One renderer.
decompose.jsrenders each subtask’s brief: the title, owned files, deliverable, done-check,depends_on, then thebrieffield (or a line saying none was written). -
Written before approval.
analyzeandbob swarm planwrite it to.swarm/briefs/<id>.md, andbob swarm runwrites it again from the approved plan..swarm/is reapable scratch (reap.json), not a tracked file. -
Both lanes get it inline. The Workflow lane puts the brief in each worker’s implement prompt. The cdfork lane passes
--brief <branch>=<file>: the file is copied into the worktree at.swarm/brief.md, kept out of git, and the session starts with it as part of its system prompt (the harness’sbrief_flag,--append-system-prompt-filefor Claude Code), beside/warp-drive. -
A line budget.
_workflow.swarm_brief_max_lines(default 150). Over it, the subtask is a candidate to split:Swarm plan: 3 worker(s), cap 4, 1 wave(s)wave 1: products, cart, checkoutBriefs (3), what each worker is told:products: .swarm/briefs/products.md (9 lines)checkout: .swarm/briefs/checkout.md (40 lines)WARNING: checkout brief is 40 lines (budget 20) — a candidate to split
How the analysis works
Section titled “How the analysis works”- Fan-out verdict (AC-02). A subtask is independent when it is not
serial_only, has nodepends_on, and is in no overlap. With ≥ 3 independent subtasks the verdict isfan-out; fewer and it recommends asingle-agentrun — the article’s rule of thumb. - Overlap analysis (AC-03). Every pair of subtasks is compared with a
recursive
**-aware glob matcher (plus a literal directory-ancestor rule). Any two touching the same path are flagged, never silently emitted — a non-emptyoverlapslist means the decomposition is not cleanly independent. - Pinch-point detection (AC-04). Subtasks touching a single-source-of-truth
path — lockfiles, migrations, build/CI config, deploy artifacts — are detected
and forced
serial_onlyso they are kept out of the fan-out. The matcher is precise: a broadsrc/api/**is not falsely flagged just because it could one day contain amigrations/dir. This reuses the sameserial-onlyconvention cdfork already understands.
Approval checkpoint — before any spawn
Section titled “Approval checkpoint — before any spawn”Decomposition produces a plan; it does not authorise a fan-out. The article’s
single most-emphasised human gate sits between decompose and dispatch: review
the plan before anyone spawns, because a wrong dependency here becomes a
tangled merge later. scripts/swarm/approve.js (#578) makes that gate
mechanical — dispatch can only proceed against a recorded, hash-verified
approval.
Terminal:
node scripts/swarm/approve.js review <plan.json|-> [--format json|human|both]node scripts/swarm/approve.js record <plan.json|-> --decision approve|revise --approver <who> [--out <path>] [--note <text>]node scripts/swarm/approve.js verify <approval.json|->node scripts/swarm/approve.js gate [--root <dir>] [--format json|human]The protocol
Section titled “The protocol”reviewsurfaces the article’s checklist on the analyzed plan: any two subtasks touching the same files (blocking), any non-independently- verifiable deliverable — a placeholder verification, or one command shared verbatim across subtasks (warning), and any suspect dependency ordering — adepends_oncycle (blocking) or a dependency on a serial-only pinch point (warning). Exit0approvable /3blocking /1invalid.gateresolves how approval must be obtained from the project’s automation level and RDB setting. At Level 3 it is a mandatory stop — a human-referee step that must not be auto-approved; with RDB enabled the prompt routes throughask_remote, otherwise the terminal.- Present the review to the human and get an explicit approve / revise.
- Revise →
record --decision reviselogs the rejection and writes no approved plan. Return to decomposition and re-plan. - Approve →
record --decision approvewrites a content-hashed approval artifact (schemas/swarm-approval.schema.json, default.swarm/approved-plan.json).recordrefuses to approve a plan with blocking findings — the gate is not advisory.
- Revise →
verifyis the dispatch-side gate: it authorises a spawn only when the artifact’sdecisionisapproveand the recordedplan_hashstill matches its embedded plan. The plan therefore cannot drift between approval and spawn — the swarm runs against exactly what was approved.
The gate is mechanical, not advisory. Blocking findings can’t be approved; an unapproved or modified plan can’t pass
verify; at Level 3 the checkpoint can’t be auto-skipped. The dispatch step (#582) and the integrator (#581) both operate against the verified artifact.
Two dispatch lanes — cdfork vs Workflow (#583)
Section titled “Two dispatch lanes — cdfork vs Workflow (#583)”A swarm has two dispatch lanes, and they are complementary, not competing.
scripts/swarm/lane.js routes each subtask to the lane that fits its shape, with
an explicit, explained verdict — surfaced in the /swarm dry-run plan:
Terminal:
node scripts/swarm/lane.js route --plan plan.json --format human| Lane | Shape | When | Mechanism |
|---|---|---|---|
| cdfork | N full warp-drive sessions, one per branch, in tmux windows on the machine where the code lives | heavyweight feature/fix tickets that each need a full dev loop | bob fork fork … — worktree isolation per session |
| Workflow | tens-to-hundreds of lightweight subagents in one session | wide, mechanical, independent fan-out (docs per module, an API migration across 30 files, a benchmark matrix) | the native Claude Code Workflow tool — pipeline() with isolation: worktree + the native concurrency cap |
The router’s bias is conservative (AC-04): a subtask routes to the Workflow lane
only when it looks mechanical (title verb: document / update / rename / format
/ migrate …) and narrowly scoped (no broad recursive globs). Heavyweight
verbs, broad globs, depends_on, and serial_only pinch points all keep a
subtask on cdfork — the safe full-dev-loop default. A too-small Workflow set is
folded back to cdfork (the wide-fan-out win needs width), and the demotion is
explained.
The Workflow lane runs a generated script. Because the Workflow tool is
invoked by the orchestrator in-session (not via a CLI), scripts/swarm/workflow-lane.js
emits the program to run:
Terminal:
node scripts/swarm/workflow-lane.js emit --plan plan.json # prints the Workflow scriptThe emitted script is a pipeline() over the Workflow-lane workers: each flows
implement (in an isolation: worktree subagent) → verify, where verify
runs the same #579 gate + #580 grade/revise loop per worker (AC-03), capturing
to .swarm/{gate,grade}-results — the same artifacts the integrator (#581)
and summary (#582) read. So quality enforcement and the downstream merge are
lane-agnostic (AC-05): a branch folds in identically regardless of which lane
produced it.
Selection is explicit, not magic.
bob swarm run --dry-runprints the lane split (lanes: N cdfork, M workflow) and alane-routestep before any spawn, so you see exactly what runs where.
Authoring your own graph? The generalized idioms behind this lane — node/edge vocabulary, the fake-edge test, diamond + checker, streaming vs. barrier, per-node tiering — plus paste-adaptable Workflow templates that reuse this same gate/grade checker, live in the
graph-orchestrationregistry skill. For discovery of unknown size (bugs, edge cases, call sites) before you can write a plan at all, itsdiscovery-sweep.workflow.jstemplate runs rounds of finders until two rounds in a row find nothing new, the budget falls underargs.budgetFloor, orargs.maxRounds(default 5) rounds have run, and returns{findings, rounds, stop_reason}withstop_reasonone ofdry,budgetormax-rounds(#4304). Itsresearch-sweepandmulti-file-reviewtemplates layer their final step when there are more results thanargs.batchSize(default 8) — summarize per batch, then combine — and logexpected N, received M, naming any lost result indropped(#4305).
Result gate — before any result folds back in
Section titled “Result gate — before any result folds back in”Approval gates the plan; the result gate (#579) gates each worker’s output.
When a worker finishes, its branch must clear deterministic non-negotiables
before it is eligible for integration — the article’s “gate every result with a
hook” step. scripts/swarm/result-gate.js enforces five checks:
- tests — the project’s detected test command passes,
- verification — the subtask’s own
verificationcommand passes. It is read from the plan with--plan … --subtask …, or passed as--verify-cmd '<cmd>'(#3556). With neither, the check is skipped, not failed. The Workflow lane names the command in each worker’s implement prompt and passes it to the gate. - lint — the detected lint command passes (absent ⇒ skipped, not failed),
- secrets — no secret patterns in the diff’s added lines (values are masked in output, never echoed),
- scope — every changed file is within the subtask’s #577
owned_globs; an out-of-scope write fails the gate.
Terminal:
node scripts/swarm/result-gate.js check --plan plan.json --subtask <id> --base <ref> --cwd <worktree> --worker <id>node scripts/swarm/result-gate.js summary [--results-dir <dir>] [--format json|human]A failing check makes the worker ineligible and records the reasons (feeding
the grader / revise loop, #580). Each verdict is captured to
.swarm/gate-results/<worker>.json; summary rolls them up for the end-of-run
report (#582).
A failing gate is also a return (#4301). It writes a gate-failure record
to .swarm/returns/<worker>.json, beside the gate results directory
(--returns-dir overrides the location). The record’s reason and evidence
come from the failed checks and the out-of-scope paths, and its scope is the
owned globs; see A returned unit. A later passing gate
closes the record and keeps its attempt count. Corrections are capped at
3, the same ceiling the grade-until-rubric-pass loop uses: the fourth
failure turns the record needs-attention. Such a unit is never merged, even
if a later gate passes: integrate.js reports it under needs_attention. The
SubagentStop hook then allows the worker to stop instead of forcing another
revision, because the unit now waits for a person.
Two substrates
Section titled “Two substrates”- In-session (Workflow lane): the
SubagentStophookswarm-result-gate.shruns the gate when a worker subagent finishes and emits{"decision":"block"}on an ineligible result, forcing a revise pass. It reads the worker’s owned globs + base ref from the environment (SWARM_OWNED_GLOBSorSWARM_PLAN+SWARM_SUBTASK,SWARM_BASE, andSWARM_VERIFY_CMDwhen there is no plan to read the done-check from), fails open for non-swarm subagents, and respectsstop_hook_activeto avoid re-block loops. Register it underSubagentStopin~/.claude/settings.json. - cdfork lane: run
result-gate.js check --cwd <worktree> --base <main> --plan plan.json --subtask <id>per branch — the same engine, applied to each worktree before integration. With the plan it runs the subtask’s verification command too (#3556).
The gate is deterministic and per-worker. One bad agent cannot quietly poison the integrated result: an ineligible branch is recorded ineligible and held out of the merge until it is revised.
Grading — score each branch, revise the weak ones
Section titled “Grading — score each branch, revise the weak ones”The gate is pass/fail; the grader (#580) is what turns a fast swarm into a
good one. It scores each gated-passing branch against a rubric and sends
sub-bar branches back to their worker to revise — the article’s “grade the
results and send back the weak ones” step. scripts/swarm/grade.js does the
scoring:
Terminal:
node scripts/swarm/grade.js check [--rubric <file>] --branch <name> --cwd <worktree> \ [--gate-result <file>] [--review-cmd <cmd> | --review-verdict <file>]node scripts/swarm/grade.js rubric [--rubric <file>] # print the effective rubricnode scripts/swarm/grade.js summary [--results-dir <dir>] [--format json|human]The default rubric covers three dimensions:
| Criterion | Kind | Passes when |
|---|---|---|
change-tests |
deterministic | a change-specific test exists among the changed files and the suite passes (per the #579 gate’s tests check) |
conventions |
review | the change adheres to the project CLAUDE.md conventions |
no-placeholders |
deterministic | no TODO/FIXME/placeholder/stub marker on an added diff line |
The rubric is configurable (AC-01): drop a .swarm/rubric.json (or pass
--rubric <file>) — an array of { id, kind, description } criteria, each
deterministic (scored from the diff + gate result) or review.
Reusing the code-reviewer agent (AC-05). review-kind criteria are scored
by the existing code-reviewer agent —
never a parallel reviewer. grade.js stays deterministic by treating the
reviewer as an injected verdict; scripts/swarm/grade-review.sh is the live hook
that runs code-reviewer over the branch diff via claude -p and prints the
verdict JSON grade.js ingests. Without a verdict (--review-cmd/--review-verdict),
review criteria skip (neutral) — the gate’s hard checks still stand.
The auto-revise loop (AC-03) is the grade-until-rubric-pass template on the
generic loop runner (#424) — not a bespoke loop:
Terminal:
node scripts/loop/run.js ~/.claude/templates/loops/grade-until-rubric-pass.json --cwd <worktree>Each iteration the worker revises only the failed criteria, then grade.js
re-grades. The loop’s max_iterations is the retry ceiling; its telemetry
and budget_exceeded halt are the loop runner’s, reused as-is.
Branches that exhaust the ceiling are flagged needs-attention, not merged (AC-04). A branch graded sub-bar after the last revise pass is held by the integrator (
--grade-results) and shownneeds-attentionin the run summary — only graded-passing branches fold in.
Blast-radius lanes — who may merge a unit (#4302)
Section titled “Blast-radius lanes — who may merge a unit (#4302)”The swarm gates a unit on its blast radius, meaning what its branch actually
changed, not on how confident a worker or grader was. scripts/swarm/blast-radius.js
puts each unit in one of three lanes. The rules are file-path rules: no model
is asked, and the same diff always lands in the same lane.
| Lane | Rule |
|---|---|
hard-to-reverse |
The diff touches a path in HARD_TO_REVERSE_PATTERNS: a migration, a wrangler or deploy config, a Dockerfile, a CI workflow, a settings.json, secrets-inventory.json, or a release script. |
contained |
Every changed file is inside the unit’s owned_globs, and there are at most 10 of them (--limit changes that). |
wide |
Any other unit: a file outside the owned globs, or more files than the limit. |
The hard-to-reverse list is declared once in scripts/swarm/decompose.js, next
to the pinch-point patterns. Where an entry is also one of the decision engine’s
one-way doors (destructive-data for migrations, prod-deploy for deploy
config), the rule names that door.
Terminal:
node scripts/swarm/blast-radius.js classify --worker <id> --branch <branch> [--base <ref>] [--plan .swarm/plan.json] [--limit 10] [--json]It prints the lane and the rule that decided it, for example
hard-to-reverse — migrations/0042_add_col.sql is a database migration (**/migrations/**); one-way door: destructive-data.
--json prints {unit, lane, rule, files}. The diff base defaults to where the
branch left main (or master), and the owned globs are read from the plan.
What each lane lets integrate do
Section titled “What each lane lets integrate do”integrate.js integrate classifies every unit against the integration branch
before it merges, and the lane decides:
- contained: merged once its gate passed.
- wide: merged only when its gate and its grade both passed on the first
attempt. That means a grade record with
attempts: 1(the grader now counts its passes per worker) and no return record ever. Otherwise it is listed underheld_by_lanewith the reason, and not merged. - hard-to-reverse: never merged by the swarm. It gets a return record
with verdict
held-for-human(which never spends the correction cap) and is listed underheld_for_human.
A unit that depends on a held unit waits behind it (held_by_return), and the
result’s lanes map names every unit’s lane.
When a run holds at least one hard-to-reverse unit, integrate files one
todo for the run. It lists every held unit with the command that merges it
after review:
node ~/.claude/scripts/swarm/integrate.js integrate --plan <plan> --target <branch> --approve <unit>--approve <unit,unit> merges the named units whatever their lane. Pass
--issue <N>, the issue the swarm works for, so the todo can link to it.
Without it, nothing is filed and integrate says why on stderr. The todo goes
through the ask admission gate. Its number is kept at .swarm/held-todo.json,
so running integrate again for the same plan updates that todo instead of filing
a second one. The integrate exit code is 3 whenever any unit is held.
bob swarm summary prints every unit’s lane ([lane: contained], and lane in
the JSON rows). The lane is the one integrate recorded in
.swarm/integration.json, or, for a unit integrate never reached, classified
from its branch at summary time. A unit held for a human shows as held with
the rule that held it, not as returned, because it did not fail.
Integration — the lead merges, not the workers
Section titled “Integration — the lead merges, not the workers”A swarm produces N graded branches; someone has to fold them back together. The
article’s rule: the lead is the integration layer — workers never merge their
own work. scripts/swarm/integrate.js (#581) is that single integrator.
Terminal:
node scripts/swarm/integrate.js order --plan plan.json [--results-dir <dir>] [--grade-results <dir>]node scripts/swarm/integrate.js integrate --plan plan.json [--results-dir <dir>] [--grade-results <dir>] [--target <branch>] \ [--test-cmd <cmd>] [--strategy merge-commit|squash] [--dry-run] [--finalize]- Collect the integrable branches — workers the #579 result gate marked
eligible, minus any the #580 grader scored sub-bar (held
needs-attentionvia--grade-results, not merged). Ineligible workers are reported and skipped; ungraded branches pass through on their gate verdict (no-op when grading wasn’t run). - Order them by the #577
depends_ongraph (Kahn topological sort), never arbitrary order. A branch whose dependency did not pass is held; a cycle blocks integration outright. - Merge each branch, in order, into the integration branch (the one set
by
bob warp session start <branch>). After every merge the full test suite runs. A merge conflict or a test failure returns that unit (#4301): the merge is aborted, the integration branch is reset to where it was before the unit, and the run goes on with the next unit that does not depend on it. Nothing is auto-resolved. - Ship with
--finalize(or runbob warp finalizeyourself): the existing ship path rebases the integration branch onto main and merges/PRs it. cdfork output flows into the established mechanism, not a new one.--finalizeonly ships a run where every unit merged; a run with a returned unit waits for the correction.
integrate exits 0 when every unit merged, 3 when it merged around a
returned unit, and 1 when it halted outright (no integration branch, or a
branch that does not resolve).
A returned unit
Section titled “A returned unit”A returned unit gets a record at .swarm/returns/<unit>.json
(schemas/swarm-return.schema.json). That record is the correction sent back
to its producer:
| Field | What it holds |
|---|---|
unit |
The subtask id. |
verdict |
conflict, merge-failure, test-failure, gate-failure, or needs-attention once the correction cap is spent. |
reason |
One line saying what went wrong. |
evidence |
paths (the conflicting files) and/or output (the last 20 lines of the failing output, at most 2000 characters). |
scope |
The unit’s owned_globs: the files its correction may touch. |
attempts, max_attempts, open, returned_at |
How often it came back, the cap (3), whether it is still unresolved, and when. |
The integrate JSON lists the same units under returned, every unit that
depends on one of them, directly or transitively, under held_by_return with
the reason depends on returned unit <X> (never merged), and
merged_despite_returns counts the units that merged while a sibling was
returned. The result is also kept at .swarm/integration.json, where
bob swarm summary reads it. When a corrected unit later merges cleanly, its record is closed
(open: false) and keeps its attempt count.
What a run teaches the next split (#4303)
Section titled “What a run teaches the next split (#4303)”Some returns mean the work was cut wrong, not done wrong. At the end of every
integrate run (not a dry run), each of these becomes a splitter constraint
in the project’s loop-notes store (.claude/loop-notes.json, kind
splitter-constraint):
| Return | The constraint |
|---|---|
| A merge conflict between two units | src/a/** and src/b/shared.ts conflicted on src/b/shared.ts — keep them in one unit |
A scope violation (a result-gate return naming files outside the unit’s owned_globs) |
src/a/** wrote src/c/x.ts outside its owned globs — give those paths to that unit |
| A unit held because a unit it depends on was returned | src/b/** was held because src/a/**, which it depends on, was returned — keep dependent work in one unit |
A test failure is not one: the cut was fine, the work was wrong. A unit is
named by its owned globs, so the same mistake in a later run is the same
constraint: it is de-duplicated by signature and its count goes up, once per
run (the run id is the plan’s hash, kept on the note as run; integrating the
same run again after a correction does not count twice). The integrate JSON
lists what was recorded under constraints. See them with
node ~/.claude/scripts/warp-drive/loop-notes.js list.
The next decompose.js scaffold brief prints them under
## Standing constraints.
A constraint leaves the brief the way any loop note leaves the store: one that
keeps recurring is promoted to a bug by loop-notes.js promote (#2063), which
tags it with that bug, and once the bug is closed loop-notes.js retire drops
it. A constraint you know is wrong can be tagged by hand with
loop-notes.js add --kind splitter-constraint --summary "<the same line>" --defect <owner>/<repo>#<n>
and retires when that issue closes.
Workers never merge to the integration target. The integrator is the sole writer of the integration branch; worker sessions only push their own feature branches. One bad branch can’t pile up into the others: it is held or returned, never silently folded in, and it never stops its independent siblings.
See also
Section titled “See also”- cdfork Guide — the consumer that fans a branch list into parallel worktrees + warp-drive sessions.
docs/research/parallel-agent-orchestration.md— the research this realizes.schemas/swarm-plan.schema.json— the plan contract.schemas/swarm-approval.schema.json— the approval-artifact contract.schemas/swarm-return.schema.json— the returned-unit record (#4301).- Blast-radius lanes — who may merge a unit (#4302).
- Hooks Reference — the
SubagentStopgate hook.