Parallel Fan-out

N engineers. One message. Skeptic-gated join.

When fan-out applies

Fan-out is triggered when orchestration-planner returns 2 or more independent units.

Conditions for fan-out:

  • Planner classified units as independent (no shared state, no interface dependency)
  • Each unit's brief is fully self-contained (files, acceptance criteria, quality gate)
  • A bug in unit A would not be detectable only by examining unit B's code

What does NOT trigger fan-out:

  • N=1: falls through to standard single-engineer Phase 5 path
  • Interdependent units: still run in parallel, but use integration Skeptic strategy
  • Units with shared file writes: planner misclassification - will surface as a merge conflict
The conductor must not derive parallelization itself. The orchestration-planner's classification is the authoritative source.

The fan-out execution model

orchestration-planner returns N units
         │
         ▼
1. Conductor writes N pending entries to .agentic/tasks.jsonl
2. Conductor creates N worktrees from BASE_BRANCH
   git worktree add .agentic/worktrees/feature-unit1 -b feature-unit1 origin/$BASE_BRANCH
   git worktree add .agentic/worktrees/feature-unit2 -b feature-unit2 origin/$BASE_BRANCH

3. Conductor updates entries to in_progress, then spawns N engineers
   in a SINGLE MESSAGE (parallel)
         │
         ▼
   [engineer A]  [engineer B]  [engineer C]
   worktree A    worktree B    worktree C
   P0 loop A     P0 loop B     P0 loop C
         │
         ▼
4. All N return → conductor evaluates join condition
5. Join: merge in merge_order, then post-merge quality check
Workers return their summaries in the normal return path. Conductor handles all task-state file writes.

The SKEPTIC_STRATEGY decision

The planner classifies each parallel group and sets skeptic_strategy:

per-unit
Units fully independent. Each gets its own Skeptic reviewing only that unit's diff. Per-unit Skeptics can be spawned in parallel - non-overlapping diffs, no interference. Runs inside each unit's P0 persistence loop.
integration
Units share interface contracts, data models, or cross-cutting concerns. Still implemented in parallel, but Skeptic review is deferred until all units merge onto a scratch integration branch. One Skeptic reviews the combined diff. This IS Phase 6 - no second Skeptic.
multi-dimensional
High-stakes units (auth, payments, migrations, crypto, secrets). Three reviewers in one message: correctness-Skeptic + security-auditor + perf-analyst. Conductor synthesizes all findings before any fix loop. All three must clear.

Independence heuristic (from subagent-protocol.md Section 6):

"If a bug in unit A is detectable only by examining unit B's implementation" - classify as interdependent.

Stacked Skeptics on interdependent units produce false signal. One integration Skeptic sees the combined diff. The planner's classification is the source of truth.

P0 persistence loop per unit

Each unit runs its own P0 Engineer → Skeptic loop inside its worktree:

Unit A worktree                     Unit B worktree
─────────────────────────           ─────────────────────────
Engineer A implements               Engineer B implements
    │                                   │
Skeptic A reviews ──> sign-off?    Skeptic B reviews ──> sign-off?
    │ findings?                         │ findings?
    └── Engineer A fix ──> loop        └── Engineer B fix ──> loop
    │ cap / convergence? ──> FAIL      │ cap / convergence? ──> FAIL
    ▼ sign-off                         ▼ sign-off
status: done                       status: done
  • "Done" means Skeptic signed off, not first engineer commit
  • Conductor updates the task entry to status: done only after sign-off
  • Loops are fully self-contained per worktree - no cross-unit context sharing
  • If a unit's loop exhausts its cap: status: failed with "persistence loop exhausted"
The join fires only after ALL units have reached a terminal state (done/failed/blocked). There is no per-unit notification event - the conductor waits for all N return values.

Join conditions

After all N engineers return, conductor reads .agentic/tasks.jsonl and evaluates:

All-done
All N units at status: done.
Proceed: sequential --no-ff merge in merge_order, then post-merge quality check.
Partial success
Some done, some failed.
Merge green units. Retry failed unit once with preserved worktree (depth=1). Second failure escalates.
Total failure
All units failed.
Clean up all worktrees. Escalate with failure outputs. Recommend sequential re-implementation.
Blocked
Any unit returned Status: BLOCKED.
Treat as failed for that unit. Cannot be resolved by conductor - escalate immediately.

Join timeout: 30-minute deadline (configurable). Units still in_progress at deadline treated as failed.

.agentic/tasks.jsonl is the coordination surface. Conductor-only writes. Workers return summaries in their normal return path.

Partial-success handling

When some units fail and some succeed:

1. Record which units are green (status: done) and which failed

2. Are green units independently mergeable?
   (True if they are truly independent of the failed unit)
   → YES: merge green units into FEATURE_BRANCH
   → NO: hold all merges until failed unit resolves

3. Re-spawn engineer for failed unit (retry, depth=1):
   - Brief: original task brief from task entry inputs field
   - Context: failure detail from outputs.worker_summary
   - Worktree: preserved in-place (do NOT clean up)
   - Note: "This is a re-run, not a fresh start"

4. If re-run succeeds → merge, proceed to Skeptic phase
   If re-run fails (second time) → ESCALATE with full failure history

Maximum retry depth: 1 automatic retry per unit. No infinite loops.

The failed unit's worktree is preserved until resolution or escalation. Do not clean it up early.

Merge strategy

After all-done join, conductor merges sequentially in merge_order:

# For each unit in merge_order:
git -C $REPO merge --no-ff ${FEATURE_BRANCH}-unit1

# After each merge, check for conflicts:
git -C $REPO diff --name-only --diff-filter=U
# Any output = conflicts present → git merge --abort → conflict recovery

Rules:

  • Sequential --no-ff always (no octopus merge). Preserves conflict attributability.
  • Conflict at any step: abort, stop remaining merges, spawn single engineer for sequential re-implementation
  • After all N merges: run QUALITY_CMD on FEATURE_BRANCH (integration quality check)
  • Integration check failure: spawn engineer on FEATURE_BRANCH for fix, then single Skeptic on incremental diff
  • Cleanup after success: git worktree remove --force + git branch -d + git worktree prune
The integration quality check catches failures invisible to individual worktrees - behavioral interactions between units that per-unit tests could not detect.

Task-state coordination

.agentic/tasks.jsonl is the durable fan-out coordination surface:

{"task_id":"sess1-auth-middleware","unit_slug":"auth-middleware","status":"pending","branch_name":"feature-auth-unit1","worktree_path":".agentic/worktrees/feature-auth-unit1","inputs":{...}}
{"task_id":"sess1-user-profile-api","unit_slug":"user-profile-api","status":"in_progress","branch_name":"feature-auth-unit2","worktree_path":".agentic/worktrees/feature-auth-unit2","inputs":{...}}

Entry lifecycle (conductor writes all):

Event Status written
Fan-out initiated pending - unit_slug, branch_name, worktree_path, inputs
Before engineer spawn in_progress
Skeptic signed off done
Unit failed or blocked failed / blocked
Branch merged done + outputs.commit_sha updated

Crash recovery: On resume, in_progress entries = dead agents = treat as failed, re-spawn with original brief from inputs field.

If .agentic/tasks.jsonl does not exist, conductor creates it at fan-out initiation. Fallback: derive status from each engineer's structured return line (Status: DONE / Status: BLOCKED).

Edge cases

Wrong branch in worktree
Before merging, verify git rev-parse --abbrev-ref HEAD in worktree matches branch_name from task entry. Mismatch = abort that unit's merge and escalate.
Stale worktrees (crash recovery)
Run git worktree prune and check for stale feature-*-unit* branches before creating new worktrees. Delete stale branches before re-creating.
Shared file writes (planner error)
Two "independent" units writing the same file. Surfaces as merge conflict. Recovery: sequential re-implementation. Promote as planner misclassification finding.
Very large N (>4 units)
Conflict risk grows with N even for independent units (shared test fixtures, generated files). Consider chunking: fan out in batches of 2-4, merge each batch, run integration check, then next batch.
Integration check fails after all-green per-unit Skeptics
Units not as independent as classified - behavioral interaction. Spawn engineer on FEATURE_BRANCH for fix. Fix goes through single Skeptic. Does NOT replace Phase 6.
Phase 6 interaction
per-unit: each unit's own Skeptic IS its Phase 6 gate, reviewing only that unit's diff (not a combined diff). integration: the integration Skeptic IS Phase 6 - it reviews the combined diff after all units merge; do not spawn a second Skeptic.

N engineers. One message. Skeptic-gated join.

Fan-out extends the P0 persistence loop to parallel units - without sacrificing adversarial review.

github.com/Space-Dinosaurs/DinoStack