Parallel Agents & Worktrees

Why worktrees, not just branches

Several agents implementing the same project at once need two things: a clean checkout each can build and test in, and a shared history they all merge back into. A branch alone gives you the second but not the first — switching branches mutates one working directory, so two agents on two branches in the same checkout collide on every uncommitted file, every node_modules, every test run.

A git worktree solves this: it is an independent working directory backed by the same .git repository. Each agent gets its own files and its own checked-out branch, but commits, refs and history are shared. The layout is one primary checkout with all agent worktrees project-local under .worktrees/:

scaffold/                      # primary checkout (you work here)
└── .worktrees/                # agent worktrees (gitignored)
    ├── alpha/                 # worktree for agent "alpha"
    └── beta/                  # worktree for agent "beta"

So worktrees give you filesystem isolation with a shared object store: agents never overwrite each other's working files, but a PR merged from one worktree is immediately visible (after a fetch/rebase) to all the others.

One worktree, one branch, one agent. A branch that is checked out in a worktree cannot also be checked out in the primary repo — git enforces this. That constraint is a feature here: it keeps each agent's work pinned to its own branch.

Setup — setup-agent-worktree.sh

scripts/setup-agent-worktree.sh <agent-name> creates a permanent worktree for one parallel agent. Given a name like alpha, it:

  1. Normalizes the name to a lowercase, hyphenated, alphanumeric slug (so Agent_1 becomes agent-1), then derives the worktree directory project-local under the primary repo: .worktrees/<slug>. It also ensures .worktrees/ is gitignored so the worktree's checkout is never accidentally committed.
  2. Creates the workspace branch <slug>-workspace if it does not already exist scripts/setup-agent-worktree.sh:62, then adds the worktree on that branch scripts/setup-agent-worktree.sh:65. Re-running for an existing worktree is a safe no-op.
  3. Writes .scaffold/identity.json — the stable identity that build observability stamps onto every event this worktree records. The script creates .scaffold/ scripts/setup-agent-worktree.sh:73 and, only if no identity file exists yet, writes worktree_id (a UUID), worktree_label (the agent slug), and created_at scripts/setup-agent-worktree.sh:92.
  4. Re-syncs Beads with a fail-soft bd doctor --fix when a .beads/ directory is present scripts/setup-agent-worktree.sh:109, reconciling the worktree's Beads git hooks and project config against the installed bd version. (Beads DB sharing is automatic — worktrees discover the main repo's task DB via git's common directory, so there is nothing for bd doctor to register.)

The worktree_id is what later lets the harvester tell one worktree's ledger from another's — see the observability guide for how identity flows into events.

The identity write is idempotent. The script only writes identity.json when one is absent, so re-running setup never clobbers an established worktree id. If you want a fresh id, delete the file first.

Which build-phase entry point?

Six build-phase commands start or resume implementation work. Pick by two questions: is the work already in the plan? and is one agent or several working?

#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#000000;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#my-svg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#my-svg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#my-svg .error-icon{fill:#552222;}#my-svg .error-text{fill:#552222;stroke:#552222;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:#666;stroke:#666;}#my-svg .marker.cross{stroke:#666;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#000000;}#my-svg .cluster-label text{fill:#333;}#my-svg .cluster-label span{color:#333;}#my-svg .cluster-label span p{background-color:transparent;}#my-svg .label text,#my-svg span{fill:#000000;color:#000000;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#eee;stroke:#999;stroke-width:1px;}#my-svg .rough-node .label text,#my-svg .node .label text,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-anchor:middle;}#my-svg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#my-svg .rough-node .label,#my-svg .node .label,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-align:center;}#my-svg .node.clickable{cursor:pointer;}#my-svg .root .anchor path{fill:#666!important;stroke-width:0;stroke:#666;}#my-svg .arrowheadPath{fill:#333333;}#my-svg .edgePath .path{stroke:#666;stroke-width:1px;}#my-svg .flowchart-link{stroke:#666;fill:none;}#my-svg .edgeLabel{background-color:white;text-align:center;}#my-svg .edgeLabel p{background-color:white;}#my-svg .edgeLabel rect{opacity:0.5;background-color:white;fill:white;}#my-svg .labelBkg{background-color:rgba(255, 255, 255, 0.5);}#my-svg .cluster rect{fill:hsl(0, 0%, 98.9215686275%);stroke:#707070;stroke-width:1px;}#my-svg .cluster text{fill:#333;}#my-svg .cluster span{color:#333;}#my-svg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(-160, 0%, 93.3333333333%);border:1px solid #707070;border-radius:2px;pointer-events:none;z-index:100;}#my-svg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#000000;}#my-svg rect.text{fill:none;stroke-width:0;}#my-svg .icon-shape,#my-svg .image-shape{background-color:white;text-align:center;}#my-svg .icon-shape p,#my-svg .image-shape p{background-color:white;padding:2px;}#my-svg .icon-shape .label rect,#my-svg .image-shape .label rect{opacity:0.5;background-color:white;fill:white;}#my-svg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#my-svg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#my-svg .node .neo-node{stroke:#999;}#my-svg [data-look="neo"].node rect,#my-svg [data-look="neo"].cluster rect,#my-svg [data-look="neo"].node polygon{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node path{stroke:url(#my-svg-gradient);stroke-width:1px;}#my-svg [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node .neo-line path{stroke:#999;filter:none;}#my-svg [data-look="neo"].node circle{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node circle .state-start{fill:#000000;}#my-svg [data-look="neo"].icon-shape .icon{fill:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].icon-shape .icon-neo path{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}No one-off bug/refactorNo a whole new featureYes planned tasksOne agentSeveral agentsFreshResumingFreshResumingBuild-phase work to doAlready in theimplementation plan?quick-tasksingle scoped tasknew-enhancementupdate PRD + stories + planOne agent orseveral in parallel?Fresh start orresuming?Fresh start orresuming?single-agent-startsingle-agent-resumemulti-agent-start(needs a worktree)multi-agent-resume(in the worktree)

single-agent-start — one agent claims the next planned task, runs the red-green-refactor loop, opens a PR, repeats. The default entry point when one agent works the plan sequentially.

multi-agent-start <agent-name> — establishes a named agent inside a git worktree so several agents run the same loop simultaneously without file conflicts. Run setup-agent-worktree.sh <name> first; this command verifies the worktree environment before claiming tasks.

single-agent-resume / multi-agent-resume <agent-name> — pick these after a break (context reset, paused session, next day). They recover context — git state, in-progress work, merged PRs — and continue the loop. The multi-agent variant additionally verifies the worktree and syncs with main before resuming.

quick-task <description> — a single, well-scoped task for a bug fix, refactor, perf tweak, or small refinement that is not in the plan. Produces one task with acceptance criteria and a TDD test plan; a complexity gate redirects to new-enhancement if the scope turns out to be too large.

new-enhancement <description> — the full-weight path for a genuinely new feature: impact analysis, then updates to the PRD and user stories, an innovation pass, and new implementation tasks that integrate with the existing plan. Use this when the work deserves stories and acceptance criteria, not just a task.

Both unplanned entry points feed back into the planned loop: once quick-task or new-enhancement has created tasks, an agent picks them up with one of the *-start / *-resume commands.

Working in parallel

Several agents merging to main at once is fine if every agent keeps its footprint small and its branches short. The conflict-prevention rules from docs/git-workflow.md apply directly:

Each agent otherwise follows the standard PR workflow from its own worktree: branch, commit, push, gh pr create, wait for CI, squash-merge. The shared object store means a merge from agent alpha's worktree is on main for everyone the moment it lands.

Teardown & harvest

When an agent's work is merged, retire its worktree. The single command for this is scripts/teardown-agent-worktree.sh <worktree-path>, and the order of operations is the whole point.

Harvest the ledger BEFORE removing the worktree — or lose the build record. A worktree's .scaffold/activity.jsonl lives inside that worktree. Once git worktree remove deletes the directory, the ledger goes with it — every decision, blocker, and task event that worktree recorded is gone, and they were never on main to begin with. The teardown script harvests the ledger into the primary repo's archive first scripts/teardown-agent-worktree.sh:42 and only then runs git worktree remove scripts/teardown-agent-worktree.sh:50. Never call git worktree remove by hand on an agent worktree before harvesting — use the script, which enforces the ordering.

The script's full sequence:

  1. Resolve the primary repo from the worktree path via git -C <path> rev-parse --git-common-dir scripts/teardown-agent-worktree.sh:27, so it works run from anywhere.
  2. Read the worktree's branch name before anything is removed scripts/teardown-agent-worktree.sh:37.
  3. Harvest the ledger (fail-soft — a harvest failure prints a warning but does not abort the removal) scripts/teardown-agent-worktree.sh:42.
  4. Remove the worktree scripts/teardown-agent-worktree.sh:50.
  5. Delete the workspace branch — with guards.

The branch-deletion guards

The script refuses to delete a branch that would harm the primary repo:

Recovering orphaned ledgers — --recover

scaffold observe harvest --recover only finalizes already-harvested active-archive entries whose worktree is no longer live. Run it from the primary repo: it lists the live worktrees, then rotates any active-archive entry whose worktree has gone away into the monthly archive. It does not recover a ledger that was never harvested — if a worktree's .scaffold/activity.jsonl was deleted with the worktree before any harvest ran (git worktree remove by hand, a deleted directory, a crashed machine), that ledger is gone and cannot be recovered. This is why teardown must always harvest first. See the observability guide for the active-vs-monthly archive mechanics.

scaffold observe harvest must run from the primary repo, not from inside a worktree — the CLI rejects a worktree primary root src/cli/commands/observe.ts:191 and warns when the target worktree has no identity.json to key the archive on src/cli/commands/observe.ts:194.

Empty ledgers are fine. A worktree that never recorded an event has no activity.jsonl; harvest is a clean no-op and teardown proceeds normally.

Resuming after a break

Coming back to parallel work — a context reset, a paused session, the next morning — does not mean re-running setup. The worktree and its identity.json persist on disk. Instead:

  1. Return to the agent's worktree directory (.worktrees/<agent>).
  2. Run multi-agent-resume <agent-name> (or single-agent-resume for the non-worktree case). It verifies the worktree environment, syncs with main, reconciles task status against any PRs merged while you were away, and resumes the TDD loop from wherever the previous session stopped.

Only run setup-agent-worktree.sh again if the worktree itself was torn down. Re-running it on a live worktree is harmless — the identity write is skipped when identity.json already exists — but it does no useful work.

See also