Skip to content

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.

  • 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-issues already covers).

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:

Terminal window
bob swarm run [--cap <n>] [--dry-run] [--finalize]
bob swarm plan --plan plan.json [--cap <n>]
bob swarm summary

Slash command:

/swarm run [--cap <n>] [--dry-run] [--finalize]
/swarm plan --plan plan.json [--cap <n>]
/swarm summary
  • Composes, never reimplements. bob swarm run drives the existing primitives by their CLIs — approve.js verify → lane routing (#583) → bob fork fork and/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.json exists 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 summary lists every branch as passed / failed / needs-attention, and after integration as merged / returned / held, each with its blast-radius lane (#4302). A Returned: 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 as returned and merged_despite_returns.

Preview the exact composition without spawning anything with bob swarm run --dry-run. Full reference: the /swarm command.

Terminal only. Everything below the /swarm wrapper is an engine primitive invoked as node 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:

Terminal window
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.

Terminal window
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 constraints
Earlier 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.

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).

Terminal window
node scripts/swarm/decompose.js analyze plan.json --format human

analyze 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.

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.

Terminal window
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 page

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.

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.js renders each subtask’s brief: the title, owned files, deliverable, done-check, depends_on, then the brief field (or a line saying none was written).

  • Written before approval. analyze and bob swarm plan write it to .swarm/briefs/<id>.md, and bob swarm run writes 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’s brief_flag, --append-system-prompt-file for 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, checkout
    Briefs (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
  • Fan-out verdict (AC-02). A subtask is independent when it is not serial_only, has no depends_on, and is in no overlap. With ≥ 3 independent subtasks the verdict is fan-out; fewer and it recommends a single-agent run — 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-empty overlaps list 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_only so they are kept out of the fan-out. The matcher is precise: a broad src/api/** is not falsely flagged just because it could one day contain a migrations/ dir. This reuses the same serial-only convention cdfork already understands.

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:

Terminal window
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]
  1. review surfaces 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 — a depends_on cycle (blocking) or a dependency on a serial-only pinch point (warning). Exit 0 approvable / 3 blocking / 1 invalid.
  2. gate resolves 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 through ask_remote, otherwise the terminal.
  3. Present the review to the human and get an explicit approve / revise.
    • Revise → record --decision revise logs the rejection and writes no approved plan. Return to decomposition and re-plan.
    • Approve → record --decision approve writes a content-hashed approval artifact (schemas/swarm-approval.schema.json, default .swarm/approved-plan.json). record refuses to approve a plan with blocking findings — the gate is not advisory.
  4. verify is the dispatch-side gate: it authorises a spawn only when the artifact’s decision is approve and the recorded plan_hash still 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:

Terminal window
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:

Terminal window
node scripts/swarm/workflow-lane.js emit --plan plan.json # prints the Workflow script

The 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-run prints the lane split (lanes: N cdfork, M workflow) and a lane-route step 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-orchestration registry skill. For discovery of unknown size (bugs, edge cases, call sites) before you can write a plan at all, its discovery-sweep.workflow.js template runs rounds of finders until two rounds in a row find nothing new, the budget falls under args.budgetFloor, or args.maxRounds (default 5) rounds have run, and returns {findings, rounds, stop_reason} with stop_reason one of dry, budget or max-rounds (#4304). Its research-sweep and multi-file-review templates layer their final step when there are more results than args.batchSize (default 8) — summarize per batch, then combine — and log expected N, received M, naming any lost result in dropped (#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 verification command 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:

Terminal window
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.

  • In-session (Workflow lane): the SubagentStop hook swarm-result-gate.sh runs 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_GLOBS or SWARM_PLAN+SWARM_SUBTASK, SWARM_BASE, and SWARM_VERIFY_CMD when there is no plan to read the done-check from), fails open for non-swarm subagents, and respects stop_hook_active to avoid re-block loops. Register it under SubagentStop in ~/.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:

Terminal window
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 rubric
node 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:

Terminal window
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 shown needs-attention in 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:

Terminal window
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.

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 under held_by_lane with 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 under held_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:

Terminal window
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:

Terminal window
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]
  1. Collect the integrable branches — workers the #579 result gate marked eligible, minus any the #580 grader scored sub-bar (held needs-attention via --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).
  2. Order them by the #577 depends_on graph (Kahn topological sort), never arbitrary order. A branch whose dependency did not pass is held; a cycle blocks integration outright.
  3. 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.
  4. Ship with --finalize (or run bob warp finalize yourself): 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. --finalize only 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 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.

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.

  • 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 SubagentStop gate hook.