autopilot
Skill0 starsChanged 58 days agoMITView on GitHub
Use to run a written, double-SHIP'd implementation plan slice-by-slice unattended. Drives 4 phases per slice (plan-slice + test-list review, implement, review-slice, docs-update), each with own 7-round Claude↔Codex budget. Self-continuing: all progress lives in the sidecar, so re-running /autopilot resumes from where the last session left off across session boundaries.
- Deletes or force-pushes
76 other projects use a version of this.
Adopted by
Most recently taken up by
- stericishere/skillstheir own version, first seen Nov 15, 2026
- zahidakhyar/skillstheir own version, first seen Nov 8, 2026
- kimkon1011/orgiast-claude-rulestheir own version, first seen Nov 8, 2026
- lohi-ai/babysittheir own version, first seen Nov 8, 2026
- paramcodes/skillstheir own version, first seen Nov 8, 2026
- jinjan21-lgtm/autopilottheir own version, first seen Nov 8, 2026
Used with
Projects with it also use review-bugbot, rename-chat and 10 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
0mlkrizzz655335vSkillreview-bugbot
缺陷导向代码审查。用 Bugbot 子代理找 bug。
Also in 3 projects that use this
0mlkrizzz655335vSkillrename-chat
重命名当前聊天。按聊天内容改标题。"disable-model-invocation: true
Also in 3 projects that use this
Tsumugii24Skillsplit-to-prs
Split current work into small reviewable PRs. Use when the user asks to split a chat, set of changes, branch, or PR.
Also in 4 projects that use this
PandelisZSkillupdate-cli-config
View and modify Cursor CLI configuration settings in ~/.cursor/cli-config.json. Use when the user wants to change CLI settings, configure permissions, switch approval mode, enable vim mode, toggle display options, configure sandbox, or manage any CLI preferences.
Also in 3 projects that use this
32103210Skillmigrate-to-skills
Convert 'Applied intelligently' Cursor rules (.cursor/rules/*.mdc) and slash commands (.cursor/commands/*.md) to Agent Skills format (.cursor/skills/). Use when you want to migrate rules or commands to skills, convert .mdc rules to SKILL.md format, or consolidate commands into the skills directory.
Also in 3 projects that use this
32103210Skillupdate-cursor-settings
Modify Cursor/VSCode user settings in settings.json. Use when you want to change editor settings, preferences, configuration, themes, font size, tab size, format on save, auto save, keybindings, or any settings.json values.
Also in 3 projects that use this
- +1 more from 32103210
- +1 more from 0mlkrizzz655335v
Other versions76 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 76
- Yeachan-Heo/oh-my-claude-sisyphus★ 41ksame name
- Yeachan-Heo/oh-my-claudecode★ 41ksame name
- Yeachan-Heo/oh-my-codex★ 34ksame name
- SethGammon/Citadel★ 927same name
- H-mmer/pentest-agents★ 815same name
- Harry-kp/vortix★ 757same name
- nick-vels/skills★ 599same name
- yan-labs/yan-skills★ 232same name
- kentcdodds/kcd-skills★ 64same name
- Kanevry/session-orchestrator★ 53same name
66 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository10 other files it gives its agents.
- app-autopilotSkill
- brainstormingSkill
- doctorSkill
- executionSkill
- honest-reportingSkill
- receiving-code-reviewSkill
- subagent-driven-developmentSkill
- systematic-debuggingSkill
- test-driven-developmentSkill
- writing-plansSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
46 rules. 0 are common, 14 are distinctive, and 32 are very specific to this project.
0
Common
Most of the 68 edited versions we read keep these.
14
Distinctive
This author's own choices. Worth a look.
32
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 11
Self-continuing across sessions: All progress is persisted to the sidecar after every step, so autopilot survives session boundaries …
- Very specific: line 18
When autopilot is invoked as part of an app-autopilot run (multi-plan, /goal-driven), three behaviors change.
- Very specific: line 30
Print
<<<PLAN_SHIPPED>>>when the plan finishes: When the last slice ships and current_phase = "all_done", before returning, emit a … - Distinctive: line 68
If any of these are missing, halt with a clear error message.
- Distinctive: line 74
Write the active anchor:
- Very specific: line 79
If the sidecar's autopilot block is null, initialize it:
- Very specific: line 107
Read the current autopilot block.
- Very specific: line 117
After each phase ships (double-SHIP), advance current_phase to the next phase in the sequence and update phase_start_sha = HEAD and …
- Very specific: line 157
If sidecar.autopilot is null entirely: this is the very first tick.
- Very specific: line 159
Dirty-tree check first: Run git status --porcelain.
- Very specific: line 160
HEAD divergence check: If HEAD does NOT descend from phase_start_sha (history rewrite/force-push/branch switch), halt with halt_reason: …
- Very specific: line 162
If all three checks pass: update last_commit_sha = HEAD (atomic) and continue from the current phase.
- Very specific: line 192
Validation tier extraction: Look for validation: critical (or light / standard) in the slice's frontmatter or section header in the plan.
- Distinctive: line 257
Read the autopilot block from sidecar to get the planPath:
- Very specific: line 304
Before the implementer checklist, resolve the slice's domain.
- Very specific: line 329
All other paths → backend if they look like server/data/runtime code (lib/server/, db/, api/, *.sql, etc.), else general.
- Very specific: line 333
If the slice has no Files: block (single-slice serial batch where Files isn't required), inspect the slice's ### Tasks body for path …
- Very specific: line 337
If inference yields multiple plausible domains AND the slice has an Implementer: directive selecting an implementer that is forbidden for …
- Distinctive: line 479
Before any worktree work, read the current slice section directly from the plan markdown.
- Very specific: line 497
Use the resolved domain from Phase B.0 and the candidate preferred implementer from the table above.
- Very specific: line 601
Do NOT call runTurn(expert, {phase}) with two arguments — that's the scheduler's INTERNAL wrapper signature inside B.5.5's drainPeerDMs …
- Very specific: line 603
If all pre-review experts SHIP (no blocking findings), held turn records flow through to B.7 where they are attached to the dispatch …
- Very specific: line 605
If any pre-review expert returns blocking findings (parsed Machine Result.blocking_findings[] non-empty), the slice does NOT proceed to B.4 …
- Very specific: line 648
Empty ready-set is OK: When a session has only in-progress codex-background-bash dispatches and no pending slices have all deps shipped, …
- Very specific: line 684
Tier 1 — sidecar marker: Read the sidecar. Confirm slice_reviews[slice-N].phases.implement.bootstrap.completed_at is a non-empty string.
- Very specific: line 685
Tier 2 — symlink reality check: Call verifyBootstrap(worktreePath, recordedSymlinks, repoRoot).
- Distinctive: line 698
Before issuing the dispatch tool call (either transport), the orchestrator pre-injects any unread messages the recipient already has queued.
- Distinctive: line 728
If dispatch failed before the agent process started (codex binary not found, Task tool refused), SKIP the mark-read — messages stay unread …
- Distinctive: line 743
Commit Conventions (subject-only, slice number = current slice.
- Very specific: line 752
Pick the form from the role (v0.17.0): Run node <plugin>/lib/codex-bridge/cli.js model-role --role implement --format json (or …
- Very specific: line 792
After dispatch, the orchestrator immediately writes an outcome: "in-progress" dispatch record (Phase B.7) with transport: …
- Very specific: line 829
If the in-progress codex task exceeds codex_dispatch.max_runtime_ms, the orchestrator MUST kill it (best-effort SIGTERM via kill <pid>, …
- Distinctive: line 833
Do not begin Phase B.8 integration cherry-pick until ALL slices in the parallel batch are terminal (shipped / blocked / failed).
- Distinctive: line 837
Every dispatch prompt — codex (Bash invocation prompt) and sonnet (Task subagent prompt) — appends this protocol block:
- Very specific: line 1029
If the slice touched any module in the CLI's pinned import closure or any pinned JSON input (docs/public-api.md cli-verbs block: closure, …
- Very specific: line 1075
After reconcile and before routing/fallback, invoke the peer-DM drain scheduler.
- Distinctive: line 1082
If the expert sent DMs to another active expert, schedules the recipient in the same B.5.5 drain loop.
- Distinctive: line 1182
When an expert emits a blocking finding, the orchestrator MUST resolve its disposition before proceeding past the integration gate.
- Very specific: line 1259
When ANY slice in the autopilot run reaches a failed-halted outcome (preferred + fallback both failed, or BLOCKED/NEEDS_CONTEXT halt …
- Very specific: line 1284
Use the slice-3 implement-phase persistence CLI/methods (sidecar.js):
- Very specific: line 1296
Use a single batch id (e.g., parallel-<ISO>-<slice-from>-<slice-to>) for all slices in a parallel batch.
- Distinctive: line 1361
Use the slice-6 module:
- Distinctive: line 1381
Always integrate slices in ascending slice order, regardless of which subagent reconciled first.
- Very specific: line 1384
Read the integration branch HEAD.
- Distinctive: line 1387
Remove each slice's worktree:
- Very specific: line 1406
B.5 reconcile: Call reconcileWorktree for each.
“Common” is measured: kept by at least 6 in 10 of the 68 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Autopilot
What this is
Given a plan that's already double-SHIP'd in writing-plans, run it slice-by-slice with full Claude↔Codex review at every phase, until all slices ship or the loop halts on a real blocker.
Self-continuing across sessions. All progress is persisted to the sidecar after every step, so
autopilot survives session boundaries WITHOUT an external loop wrapper: re-running /autopilot (with
no argument, or the plan path) reads the sidecar and resumes the exact slice/phase it left off at. The
/autopilot command is itself the resume entry point — there is no ralph-loop to spawn.
Outer-mode under app-autopilot (v0.11.0)
When autopilot is invoked as part of an app-autopilot run (multi-plan, /goal-driven), three behaviors change. Otherwise (single-plan, user-initiated autopilot), everything works exactly as it did in v0.10.x.
Outer-mode detection. Autopilot is in outer-mode when EITHER:
- The environment variable
CODEX_PAIRED_OUTER_MODE=app-autopilotis set, OR - The sidecar has a non-null
app_stateblock (node ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js app-state-get --specPath <spec>emits non-empty).
Either trigger flips on outer-mode. The env var lets app-autopilot signal explicitly inside the headless child; the sidecar check is a belt-and-suspenders fallback if the env var didn't survive a process boundary.
Behavior changes when outer-mode is on:
- Single pass per turn —
/goaldrives re-entry. In outer-mode the/goalevaluator is the outer driver, so do the work for ONE slice (or one phase if mid-slice), surface progress + state, and return immediately — do NOT loop over further slices in this turn (that would race the/goaldriver). The next/goalturn re-enters autopilot via app-autopilot's Step 3. (In default mode, by contrast, autopilot may loop over multiple slices within a session before returning; see "Cross-session continuity" below.) Print
<<<PLAN_SHIPPED>>>when the plan finishes. When the last slice ships andcurrent_phase = "all_done", before returning, emit a single line to stdout:<<<PLAN_SHIPPED>>> path=<plan-path> goals_audited=[goal-id-1,goal-id-2,...]<plan-path>is the plan file that just finished (the one autopilot was driving).goals_auditedis a comma-separated list of goal ids this plan delivered, derived from the plan's frontmatter**Goals:**block (a new optional field; if absent, the orchestrator parses from each slice's "delivers goal:<id>" annotations). If the plan didn't declare which goals it ships, emitgoals_audited=[]and app-autopilot will fall back to asking Codex to map the diff against the goals block.
app-autopilot's per-turn loop reads this line, marks the goals shipped in
app_stateviaapp-state-mark-goal-shipped, and proceeds.Print
APP_HALT: <reason>on non-recoverable halt. Replace the bare halt-summary block with a line of this exact shape:APP_HALT: <halt_reason_slug>: <one-line plain-English explanation><halt_reason_slug>matches the existinghalt_reasonvalues (dirty-tree-on-phase-retry,history-divergence,external-commit-detected,subagent-blocked,cli-infrastructure-error,validation-coverage-malformed:*,panel-quorum-lost, etc.).- The one-line explanation is for the user — plain English, no jargon.
The existing halt machinery (anchor-clear, halt-envelope, sidecar
autopilot.halt_reasonwrite) still runs unchanged. The new sentinel line is additional output the/goalevaluator pattern-matches.
Default (non-outer) mode. Outside outer-mode (no env var AND no app_state block), autopilot is self-continuing: it loops over slices within the session until all ship, a real blocker halts it, or the session ends — then resumes on the next /autopilot invocation (see "Cross-session continuity" below). No PLAN_SHIPPED line is printed (that sentinel is outer-mode only); halts surface to the user with a resume hint.
Honest-reporting activation (v0.8.1, do this first)
On entry, write the honest-reporting marker so the Stop/PreToolUse hook keeps claims sourced for the autopilot run:
node "${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js" honest-reporting-mark-active --skill autopilot --spec <spec-path>The marker has an 8-hour TTL; for longer autopilot runs, refresh it on each major phase boundary (or pass --ttl-hours 24). See skills/honest-reporting/SKILL.md for the VERIFIED / ASSUMED / UNTESTED vocabulary.
Required inputs
- A double-SHIP'd plan at
docs/plans/<plan>.md. - The plan's parent spec at
docs/specs/<spec>.mdwith a sidecar (<spec>.codex.json) containing the persistent Codex threadId. - The plan's frontmatter must reference the spec path explicitly (
**Spec:** docs/specs/...).
If any of these are missing, halt with a clear error message. Do NOT try to brainstorm or write a plan from inside autopilot.
Lifecycle
On run start (called once per autopilot session, NOT on every resume)
- Resolve
<repo-root>(the directory containing the plan; usuallygit rev-parse --show-toplevel). - Write the active anchor:
node ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js anchor-write \ --repoRoot <repo-root> --specPath <spec-path> - If the sidecar's
autopilotblock is null, initialize it:
Atomic write via{ "started_at": "<now>", "last_tick_at": "<now>", "current_slice": "<first unfinished slice number>", "current_phase": "plan-slice", "phase_attempt": 1, "phase_started_at": "<now>", "slice_start_sha": "<HEAD>", "phase_start_sha": "<HEAD>", "last_commit_sha": "<HEAD>", "inflight_subagent_id": null, "halt_reason": null }sidecar-set-autopilot. 3.5. Open the execution thread (v0.16.0) ifrole_sessions["execution-reviewer"]is absent from the sidecar (resumes skip this): follow the five steps inskills/execution/SKILL.md"Execution thread" (model-role --role review --format mcp→sidecar-replay-context→composeSeedPrompt(replay, { reason: 'execution-thread', specPath, planPath })→codexMCP call with that model + config →sidecar-rotate-thread-id --role execution-reviewer --reason execution-thread --threadConfig ...). Phase A keeps using thepaired-reviewer(planning) thread; Phases C and D use this one. Never type a model id; ifmodel-rolefails, haltmodel-role-resolution-failed. - Proceed to the main loop.
Main loop (one tick = one phase progression)
Read the current autopilot block. Dispatch on current_phase:
- plan-slice + test-list review → run Phase A.
- implement → run Phase B.
- review-slice → run Phase C.
- docs-update → run Phase D.
- live-verification → run Phase E (see below).
- shipped → mark the slice shipped (
slice_reviews[slice-N].shipped = true), advancecurrent_sliceto the next unfinished slice, setcurrent_phase = "plan-slice", resetphase_start_sha = HEAD. - all_done → write a final autopilot block with
halt_reason: "completed", clear the anchor, clear the honest-reporting marker (honest-reporting-clear— v0.15.0; the hook must not police unrelated work after the run ends), return success. If outer-mode is on (see § "Outer-mode under app-autopilot"), ALSO print the<<<PLAN_SHIPPED>>> path=<plan> goals_audited=[...]sentinel to stdout before returning.
After each phase ships (double-SHIP), advance current_phase to the next phase in the sequence and update phase_start_sha = HEAD and last_commit_sha = HEAD atomically.
On halt (any reason)
- Set
autopilot.halt_reasonin the sidecar (atomic). - Print a summary to the user: which slice, which phase, what blocked. If outer-mode is on (see § "Outer-mode under app-autopilot"), the summary MUST also include a literal
APP_HALT: <halt_reason>: <plain-English one-liner>line so the parent/goalevaluator picks it up. - Clear the active anchor (
anchor-clear --repoRoot <repo>). This is critical: while halted, the user must be able to make manual recovery commits without the provenance hook blocking them. The sidecar'sautopilotblock (withhalt_reasonset) remains and is the source of truth for resumption. 3b. Clear the honest-reporting marker (honest-reporting-clear— v0.15.0). While halted, the user (or an unrelated session in this repo) must not fight the claim scanner. Resuming re-marks it via the entry block. 3c. Mark every worktree the halt leaves in place as preserved (v0.19.0). For each slice, fan-out or review worktree this run leaves behind for inspection, runnode "${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js" checkout-preserve --path "<worktree>" --reason "halt:<halt_reason>" --run "<slice-id>" --repoRoot "$REPO_ROOT", and list the paths in the halt summary.worktree-reapnever removes a preserved checkout, so a later cleanup cannot delete what a halt kept for diagnosis. Acheckout-preservefailure is reported in the summary; it never replaces the original halt reason. Emit the halt envelope (v0.9.0). Before returning, wrap the halt reason through the halt-envelope module so the resume path (and any
/loopdriver) receives the structured shape:import { wrapAsHaltEnvelope } from '<plugin>/lib/codex-bridge/halt-envelope.js'; const envelope = wrapAsHaltEnvelope(haltReason, { sliceId, // current slice at halt time phase, // current phase at halt time (e.g. "B.5") }); // envelope: { halt, terminal: bool, resume_hint, sliceId, phase } return envelope;terminal: truemeans the operator must act before re-running/autopilot. Autopilot MUST NOT auto-continue on a terminal halt — it surfaces theresume_hintto the user and stops.terminal: falsemeans a transient condition; the next/autopilotinvocation may resume after the cause clears. Unknown halt reasons always produceterminal: true(fail-closed).- On the next
/autopilotinvocation (re-run manually, or via/loop), the autopilot reads the sidecar, seeshalt_reasonset, and either re-writes the anchor and resumes (if the halt cause has been addressed) or exits with the same halt reason. Under outer-mode, the "next invocation" is the next/goalturn — app-autopilot's per-turn Step 1 reconciliation reads the sidecar and decides whether to retry (clearinghalt_reasonto null on success) or re-printAPP_HALT(if the blocker persists).
On resume (re-running /autopilot — cross-session or post-halt)
Each /autopilot invocation re-discovers state from the sidecar. When a plan path is given it is the authoritative entrypoint — autopilot uses it to rediscover the spec and sidecar regardless of whether the active anchor is present. With no plan path, autopilot first locates the in-progress run (see the /autopilot command), then proceeds identically.
- Resolve the spec path from the plan's
**Spec:** ...frontmatter line. - Load the sidecar via the spec path.
- Inspect
sidecar.autopilot.halt_reason:null(and anchor present): normal in-session resume — re-write anchor if missing, run cross-session reconciliation (step 5 below), continue current phase."completed": report success and stop. The run is complete.- any other value (a real halt): the user has either resolved the cause and is asking to continue, or the cause persists. Either way, autopilot rewrites the active anchor (so the hook re-engages) and runs cross-session reconciliation. If reconciliation now succeeds (e.g., dirty tree was cleaned), clear
halt_reasonto null and continue. If reconciliation produces a NEW halt reason (e.g., previously halted onsubagent-blocked, now halts ondirty-tree-on-phase-retrybecause the user left edits behind), write the NEW reason — do NOT preserve the stale one. The current halt reason must always reflect the current blocker.
- If
sidecar.autopilotis null entirely: this is the very first tick. Initialize the autopilot block, write the anchor, start at the first unfinished slice's plan-slice phase. - Cross-session reconciliation (used by step 3 paths above):
- Dirty-tree check first. Run
git status --porcelain. If output is non-empty, the working tree has uncommitted changes from a prior crash or external edit. Halt withhalt_reason: "dirty-tree-on-phase-retry", list the affected files, ping user. - HEAD divergence check. If HEAD does NOT descend from
phase_start_sha(history rewrite/force-push/branch switch), halt withhalt_reason: "history-divergence". - Range walk. Walk every commit in
last_commit_sha..HEAD. Each must conform to Commit Conventions (subject prefix matching the slice, plusCo-Authored-By: Claudetrailer). If any commit doesn't conform, halt withhalt_reason: "external-commit-detected"citing the offending SHA. - If all three checks pass: update
last_commit_sha = HEAD(atomic) and continue from the current phase.
- Dirty-tree check first. Run
The active anchor is the HOOK's discovery mechanism, not autopilot's. Autopilot uses the plan path. The anchor exists during active runs so the hook can find the right sidecar. It's cleared on halt/completion to keep the hook out of the way during user-driven recovery.
Per-phase procedures
Mid-phase Codex thread loss (applies to every codex-reply below). If any phase's codex-reply
returns isError: true with Session not found for thread_id: (the MCP server restarted mid-run —
threads are process-local), do NOT halt the slice. Recover: build replay context
(node ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js sidecar-replay-context --specPath "<spec-path>"),
open a NEW thread via the initial codex tool seeded with that replay + the pending phase prompt,
persist the rotation for the role that owns the lost thread, using the model and effort recorded in
that thread's thread_config entry (spec §5; current model-role output is only the legacy fallback):
recoverStaleThread(specPath, { staleResponse: reply, pendingPrompt, phase: '<phase>', round: <n>, role, planPath, repoRoot }, { codexFn })
from lib/codex-bridge/thread-recovery.js — role is 'execution-reviewer' for Phase B.5/C/D turns and
'paired-reviewer' for Phase A turns; codexFn: async ({ prompt, model, config }) => /* mcp codex tool → { threadId, content } */
makes exactly one initial call with the recorded model + config (agy roles need no dep); the helper
detects the stale response, replays the sidecar context, opens the thread and persists the rotation
with thread_config. Read r.recovered, r.newThreadId, r.content. If you must do it by hand,
sidecar-rotate-thread-id --specPath "<spec-path>" --role <role> --oldThreadId <old> --newThreadId <new>
--reason session-not-found --phase <phase> --round <n> --threadConfig '<recorded object>' — the verb defaults
to paired-reviewer, so omitting --role on an execution-thread loss silently overwrites the planning
thread and leaves the execution thread stale. Surface one line to the user, and continue the current phase. This is distinct from cross-session resume (re-running
/autopilot); it handles a thread dying within a live run.
Phase A: plan-slice + test-list review
Three artifacts reviewed in one phase: the task list, the test list, AND the validation rubric coverage for this slice.
- Task list extraction. Parse the plan's slice-N section. Extract the bullet list of tasks. Format as markdown.
- Test list extraction. From the same slice section, extract every
Write the failing testor test-creation step. Format as a numbered list with: invariant pinned, inputs, expected outcome, mock/integration choice. - Validation tier extraction. Look for
validation: critical(orlight/standard) in the slice's frontmatter or section header in the plan. If absent, default tostandard. Pass this tier into the prompt so Codex knows whether to apply Tier 3 (residual-risk question). - Compose the prompt by concatenating:
- Contents of
${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/prompts/system-rubric.md - Contents of
${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/prompts/verdict-format.md - Contents of
${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/prompts/validation-rubric.md - Phase header + slice content:
Phase: plan-slice + test-list review Round: <N> Slice: <slice-N> Validation tier: <light|standard|critical> ## Task list <task list> ## Test list <test list> Critique with L11 rigor. Apply the validation rubric. SHIP only if every Tier-1 subcategory has an explicit entry in your verdict's critique array AND every Tier-2 trigger is stated as fired-or-not. If validation tier is `critical`, also answer Tier 3.
- Contents of
- Send via
codex-replyon the planning thread (sidecar-thread-id --specPath <spec> --role paired-reviewer— GPT-6 Astra atxhighby default; per-slice plan review is planning-grade work). Run the standard 7-round loop. Append rounds to sidecar viasidecar-append-roundwith phaseplan-slice:<slice-N>. 5a. Planning panel (v0.19.0). Before round 1, run the activation check inskills/brainstorming/codex-pairing.md§ "Review panel rounds (v0.19.0)" with--phase planning. Ifconfiguredisfalse, step 5 applies exactly as written. Ifconfiguredistrue, run eachplan-slice:<slice-N>round as a panel round per that section instead of the singlecodex-reply(Codex members on threads keyedpaired-reviewer:<member_id>), and step 6 validates the rubric coverage of every member's final SHIP critique, not only one. On double-SHIP, parse and validate structured rubric coverage via the bridge CLI, then persist. The verdict's
critiquearray contains the rubric coverage bullets. Pipe it as JSON to thevalidation-parsesubcommand and dispatch on the exit code:echo '<JSON array of critique bullets>' | \ node ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js validation-parse \ --tier <slice-tier-from-frontmatter>Three-way exit-code dispatch:
- Exit 0: success. Stdout is
{"tier": ..., "coverage": {...}}JSON. Parse it; mergetierinto the coverage map; write toslice_reviews[slice-N].phases.plan-slice.validation_coverage. Advance to Phase B. - Exit 2: parser defect. Stderr is
{"defect": "<code>", "detail": "..."}JSON. Halt withhalt_reason: "validation-coverage-malformed:<defect>". The next /autopilot tick must re-prompt Codex; this is NOT a retry path autopilot handles automatically — escalate to user. - Exit 1: CLI infrastructure failure (parser crashed, fs error, etc.). Stderr is the raw error message. Halt with
halt_reason: "cli-infrastructure-error". Do NOT retry — escalate to user.
The orchestrator no longer interprets the rubric — it shells out to a deterministic parser whose behavior is unit-tested.
- Exit 0: success. Stdout is
The system rubric only needs to be sent on the first plan-slice round of the feature's lifetime (it persists in Codex's thread context). The verdict-format and validation-rubric should be re-sent whenever phase changes, since the role they play differs per phase.
Phase B: implement
v0.7.3 — dependency-graph batching + mailbox coordination. Phase B builds a DAG from each slice's
**DependsOn:**directive (block form, parsed bylib/codex-bridge/dependency-graph.js), computes the ready-set (pending slices with all deps shipped), and dispatches the deterministic first-fit non-overlap subset of the ready-set. This replaces v0.7.1's consecutive-slice batching — non-consecutive slices can now parallelize when deps + Files allow. Mailbox infrastructure (file-based JSON inboxes per recipient under.codex-paired/mailboxes/) lets in-flight agents send progress/blocker messages to the orchestrator + each other; orchestrator polls between turns.Carry forward from v0.7.1: domain-aware routing per
agents/dispatchers.json. Carry forward from v0.7.2: codex via background Bash + status file; sonnet via Task subagent.
Phase B has the following steps, executed in order. v0.7.3 adds B.PRE (DAG build/verify, runs once per autopilot session AND on every resume) and replaces B.2 entirely.
- PRE: Build + verify DAG (v0.7.3) —
buildDAG(planPath); persist digest to sidecar; on resume haltplan-changed-during-autopilotif digest mismatches. - Resolve domain — Claude reads
**Domain:**directive or infers from**Files:**paths. - Pre-dispatch checklist — Claude inspects the slice section directly; enforces domain policy via the registry.
- Ready-set + first-fit batching (v0.7.3 REPLACES old conflict comparison) —
computeReadySet(dag, sliceStates)thenmaximalFirstFitNonOverlap(readySet, filesIndex). - Worktree setup per batch — create + bootstrap + verify (two-tier gate).
- Dispatch — invoke routed subagent(s); parallel batches dispatch in a SINGLE assistant turn. v0.7.3 dispatch prompts include the agent's mailbox protocol.
- Between turns (v0.7.3) — orchestrator polls in-flight inboxes + own inbox; surfaces unread messages.
- Reconcile — call
reconcileWorktreefromlib/codex-bridge/reconciler.js. - Apply routing rules — preferred → fallback → halt; fallback respects domain policy.
- Failure cascade halt (v0.7.3) — on any slice's
failed-haltedoutcome, halt the run withdependency-cascade-haltlisting descendants. - Persist — sidecar
setImplementMeta/setImplementBootstrap/appendImplementDispatch. - Integration — ordered cherry-pick via
lib/codex-bridge/worktree-integrate.js; clean up worktrees.
The rest of this section spells each step out verbatim.
Phase B.PRE — Build + verify DAG (v0.7.3)
Read the autopilot block from sidecar to get the planPath:
PLAN_PATH=$(node <plugin>/lib/codex-bridge/cli.js sidecar-show --specPath <spec> | jq -r '.autopilot.plan_path')At the start of an autopilot session AND at the start of every Phase B turn (including resume after crash), follow these 4 steps:
Step 1: Build the DAG.
import { buildDAG } from '<plugin>/lib/codex-bridge/dependency-graph.js';
const built = buildDAG(planPath);Step 2: Halt on validation failure. On { ok: false, halt } — halt with the returned reason (dep-block-malformed, dep-self-reference, dep-unknown-slice, dep-cycle, etc.). These are pre-dispatch fatal — no worktree work.
Step 3: Decide write-vs-verify by checking sidecar.
EXISTING=$(node <plugin>/lib/codex-bridge/cli.js sidecar-get-dependency-graph --specPath <spec>)EXISTINGis empty (first call): persist the freshly-built graph:node <plugin>/lib/codex-bridge/cli.js sidecar-set-dependency-graph \ --specPath <spec> \ --graph "$(jq -nc --arg digest "$built.digest" --argjson dag "$built.dag" \ '{digest: $digest, dag: $dag}')"EXISTINGis non-empty (subsequent call / resume): parse it as JSON, compareEXISTING.digestwithbuilt.digest. If mismatch → haltplan-changed-during-autopilotwith both digests in diagnostic. The user has edited the plan mid-run; autopilot must re-validate before continuing.
Step 4: Compute current slice states from sidecar.
For each slice id in built.dag.nodes:
shipped—slice_reviews[sliceId].phases.implement.shipped === truefailed— most recent entry indispatches[]hasoutcome: "failed-halted"in-progress— most recent entry hasoutcome: "in-progress"(codex-background-bash awaiting completion)pending— otherwise (no dispatches yet, or last outcome wasfailed-fallback-pendingand a fallback hasn't been attempted)
The orchestrator carries dag, filesIndex, and sliceStates through the rest of Phase B.
Phase B.0 — Domain resolution (v0.7.1)
Before the implementer checklist, resolve the slice's domain. The domain governs which implementers are forbidden / allowed / preferred per agents/dispatchers.json.
Step 1: Look for a **Domain:** directive in the slice section.
| Slice line content (after trimming) | Action |
**Domain:** ui (exact, lower-case) | domain = ui |
**Domain:** ai-harness | domain = ai-harness |
**Domain:** backend | domain = backend |
**Domain:** general | domain = general |
**Domain:** with empty value | halt domain-directive-malformed |
**Domain:** Backend / mixed case / any other value | halt domain-directive-malformed |
| line absent entirely | proceed to step 2 (inference) |
Step 2: If no directive, infer from **Files:** paths.
Path-based heuristics, applied to every entry in the slice's Files block:
- A path matching any of these signals →
ui:web/,app/,frontend/*.tsx,*.css,*.html,*.scss,*.svelte,*.vue
- A path matching any of these signals →
ai-harness:skills/,agents/,hooks/lib/codex-bridge/*.skill.md
- All other paths →
backendif they look like server/data/runtime code (lib/server/,db/,api/,*.sql, etc.), elsegeneral.
If multiple paths in the same slice yield different domain signals, pick the strongest match in this priority order: ui > ai-harness > backend > general. (Rationale: a slice that touches BOTH a UI file and a backend file is still UI work in spirit — Codex must not write the React component even if it also touches an API endpoint in the same slice.)
If the slice has no **Files:** block (single-slice serial batch where Files isn't required), inspect the slice's ### Tasks body for path mentions and apply the same heuristics. If still ambiguous, default to general.
Step 3: Ambiguity rule.
If inference yields multiple plausible domains AND the slice has an **Implementer:** directive selecting an implementer that is forbidden for any of those plausible domains, halt domain-policy-ambiguous. The user must add an explicit **Domain:** directive to disambiguate.
In all other ambiguous cases (no implementer directive or all plausible domains share the same policy for the chosen implementer), pick the highest-priority plausible domain per the order above and continue.
Smart-parallelization implication. Two consecutive slices with different inferred domains should be inspected for tight coupling before parallelizing them. A ui slice and an ai-harness slice that touch independent files can still parallel-batch (different domains, both safe for Sonnet). A ui slice and a backend slice cross domain boundaries — verify the **Files:** are genuinely disjoint and the slices aren't logically joined (e.g., a UI component slice that imports a type from a backend slice should NOT parallelize even if the Files don't overlap, because the backend slice's exports are the UI slice's contract).
Phase B.0.5 — Expert selection per slice (v0.8.0)
For each slice in the current batch, the orchestrator selects an expert teammate set based on signals (slice frontmatter **Domain:**, file paths in the slice's **Files:** block, content keywords). The selection is recorded in the sidecar's reviewer_teammates.selected[]:
// Pseudo-code: orchestrator selects reviewers via reviewer-runtime.
// IMPORTANT: if the orchestrator believes a broad selection (>5 reviewers) is
// warranted, it MUST pre-compute fanOutRationale and pass it INTO signals
// BEFORE calling selectTeammates. reviewer-composer.js throws
// `role-composer-fan-out-unjustified` if it returns >5 selections without
// a fanOutRationale, so providing it after the fact is too late.
const { selectTeammates } = await import('<plugin>/lib/codex-bridge/reviewer-runtime.js');
// First pass: estimate breadth from signals (or attempt a narrow call first
// without rationale and recover via try/catch if it throws).
//
// Directive precedence: the canonical **Reviewers:** slice directive maps to
// `reviewersDirective`. The deprecated **Experts:** directive is still accepted
// on read and maps to `explicitDirective` — if both are present, Reviewers wins
// and the composer returns a `directiveWarning` (persist it to the sidecar
// audit). Prefer `sliceFrontmatter.reviewers`; fall back to
// `sliceFrontmatter.experts` only for legacy plans.
const signals = {
specHas: [/* spec keywords */],
filePaths: [/* slice Files block */],
domains: [sliceDomain],
reviewersDirective: sliceFrontmatter.reviewers, // canonical **Reviewers:** directive
explicitDirective: sliceFrontmatter.experts, // deprecated **Experts:** alias (accepted-on-read)
};
// If the orchestrator anticipates broad selection (touches UI + UX + arch
// + security + AI), include the rationale up front:
if (anticipatesFanOut) {
signals.fanOutRationale = "<concrete justification of the breadth>";
}
const result = selectTeammates({
phase: 'post-implementation-review',
signals,
repoRoot: <repoRoot>,
});
// result.selected: ReviewerIdentity[]
// result.fanOutRationale: string|null (populated when >5 selected)
// result.selectionReasons: {[reviewerId]: string}
// result.directiveWarning: string|null (set when **Experts:** is used, or
// when both **Reviewers:** and **Experts:** are present)
// Persist:
for (const identity of result.selected) {
appendReviewerSelection(specPath, {
id: identity.id,
role: identity.role,
source: identity.source,
phase: 'post-implementation-review',
selectionReason: result.selectionReasons[identity.id],
});
}
if (result.directiveWarning) {
// Record the deprecation/precedence warning so the sidecar audit captures
// that a legacy **Experts:** directive was read for this slice.
appendReviewerSelection(specPath, {
id: 'reviewer-directive-warning',
role: 'audit',
source: 'composer',
phase: 'post-implementation-review',
selectionReason: result.directiveWarning,
});
}
if (result.fanOutRationale !== null) {
appendFanOutRationale(specPath, {
phase: 'post-implementation-review',
selected_count: result.selected.length,
rationale: result.fanOutRationale,
});
}The role-composer enforces the fan-out contract via role-composer-fan-out-unjustified (thrown from composeReviewers, not from appendFanOutRationale). appendFanOutRationale is purely a persistence call — it validates that selected_count > 5 but cannot detect missing rationale on the composer side.
Composition with existing Phase B.0: B.0 still chooses implementer transport (codex vs sonnet) via dispatchers.json. B.0.5 augments by selecting domain-expert REVIEWERS for the same slice. Experts do not replace implementers in MVP — they are advisory reviewers that emit verdicts and findings, never accepted manuscript writes.
v0.9.0 dispatcher seams (refactor, no behavior change)
The v0.9.0 dispatcher introduces three named seams that B.0.5 / B.1.5 / B.5.5 now go through. The phases' logic is unchanged; only the API names + adapter-resolution layer are new:
composeReviewers(v0.8.0, unchanged) — picks experts from signals.detectAvailableCLIs+resolveAdapter(v0.9.0 new) — for each selected expert, walks the preference ladder and returns{cli, variant, ...}wherecli ∈ {'claude','codex','ollama','gemini','qwen'}. Sidecar adapter value is derived:cli === 'claude'→'claude-task'; otherwise'cli-harness:${cli}'. User overrides in.codex-paired/role-routing.jsontake precedence. The resolver THROWSRoleRoutingErroron failure (codes:no-supported-cli-for-role,override-cli-unavailable,override-variant-unknown); for gate-class reviews this becomes acli-dispatch-failedhalt — never silently degrade.runTurnWithDeps(v0.9.0 — extended for replay-field persistence +suppressPeerMessages) — single-expert dispatch wrapping the adapter-specificagentDispatch.dispatchPanel(v0.9.0 slice 6) — panel-mode dispatch forpaired-reviewerat the release gate AND for anyhigh_stakes: trueslice'sreviewer-security+reviewer-architecturereviews. Builds adispatchFns: Map<member_id, fn>map where each entry wrapsrunTurnWithDepswith an adapter-specific identity.
The orchestrator (Claude) builds dispatchFns per-member; dispatchPanel snapshots the member list, fans out in parallel, aggregates verdicts deterministically (per spec § 4 panel-mode table).
const { detectAvailableCLIs, availableCLISet } =
await import('<plugin>/lib/codex-bridge/availability/detector.js');
const { resolveAdapter } =
await import('<plugin>/lib/codex-bridge/role-routing/resolver.js');
const { runTurnWithDeps } =
await import('<plugin>/lib/codex-bridge/reviewer-turn.js');
const { dispatchPanel } =
await import('<plugin>/lib/codex-bridge/panel/dispatcher.js');
const detectorResult = await detectAvailableCLIs(repoRoot);
const availableCLIs = availableCLISet(detectorResult);
const { RoleRoutingError } =
await import('<plugin>/lib/codex-bridge/role-routing/errors.js');
for (const identity of result.selected) {
let resolved;
try {
// Resolver is keyed by identity.id ("reviewer-architecture"), NOT identity.role.
resolved = resolveAdapter(identity.id, availableCLIs, projectRouting);
} catch (err) {
if (err instanceof RoleRoutingError) {
// Gate-class review: any RoleRoutingError halts with 'cli-dispatch-failed'.
throw new Error(`cli-dispatch-failed: ${identity.id} (${err.code})`);
}
throw err;
}
// resolved.cli ∈ {'claude','codex','ollama','gemini','qwen'}; resolved.variant may be null.
const adapter = resolved.cli === 'claude'
? 'claude-task'
: `cli-harness:${resolved.cli}`;
// The orchestrator then calls runTurnWithDeps({...request, adapter}, {agentDispatch}).
}Phase B.1 — Pre-dispatch checklist (Claude reads the slice section)
0. Lane (v0.18.1). Autopilot always runs the standard lane: both reviewers, unanimous on the
same commit, cap 7, writer dispatched per B.4 — whatever the slice touches. The prose lane's
no-dispatch path exists only in the interactive driver, because B.5 reconciles returned subagents
and B.8 requires a successful reconciliation before integration; see docs/execution-model.md
"Review lanes". Do not skip B.4 for a documentation-only slice here.
Before any worktree work, read the current slice section directly from the plan markdown. Apply these checks literally — paraphrase or guesswork is non-conforming.
Implementer directive (**Implementer:** line in the slice section):
First, parse the directive itself:
| Slice line content (after trimming) | Action |
| line absent entirely | proceed to registry-default selection |
**Implementer:** codex (exact, lower-case) | candidate preferred = codex |
**Implementer:** sonnet (exact, lower-case) | candidate preferred = sonnet |
**Implementer:** auto | halt implementer-directive-malformed |
**Implementer:** with empty value | halt implementer-directive-malformed |
**Implementer:** Codex / **Implementer:** SONNET / any mixed case | halt implementer-directive-malformed |
| any other value | halt implementer-directive-malformed |
Then enforce domain policy via the dispatcher registry (v0.7.1):
Use the resolved domain from Phase B.0 and the candidate preferred implementer from the table above. Look up enforceDomainPolicy(implementer, domain) from lib/codex-bridge/dispatchers.js (returns forbidden | allowed | preferred).
| Directive state | Domain policy result | Action |
| Line absent (registry-default selection) | — | preferred = the implementer in the registry whose domain policy for this domain is preferred; if none is preferred, the first whose policy is allowed. If none is allowed, halt implementer-unavailable. |
Directive selected codex or sonnet | forbidden | halt domain-policy-violation |
Directive selected codex or sonnet | allowed or preferred | preferred = directive value |
For the v0.16.0 registry shipped in agents/dispatchers.json, Codex writes the code: codex is
preferred in every domain (ui, ai-harness, backend, general) and sonnet is allowed in
every domain as the last-resort fallback. Registry-default selection therefore always starts with
Codex.
The single-implementer ladder (v0.16.0). Dispatch climbs three rungs; each rung uses the same failure triggers (B.6):
rung 1 codex @ implement (--model-role implement; GPT-5.6 Sol, high by default)
rung 2 codex @ implement_fallback (--model-role implement_fallback; GPT-6 Astra, medium by default)
rung 3 sonnet subagent (Task tool, slice-implementer-sonnet)
halt implementer-unavailableA **Implementer:** directive means "start the ladder at this rung": codex → rung 1, sonnet →
rung 3. Record the starting rung as preferred_implementer and the next rung as
fallback_implementer in the B.7 implement meta. If the registry ever marks the fallback rung
forbidden for the resolved domain, skip it and halt implementer-unavailable when the ladder is
exhausted (NOT domain-policy-violation — the user didn't pick the forbidden combo).
Files block (only validated when this slice is a parallel candidate):
A slice is a "parallel candidate" iff Claude is considering dispatching it concurrently with one or more consecutive slices in the current candidate window. A single-slice batch is NOT a parallel candidate and the Files block is NOT required.
For every parallel candidate, locate the **Files:** block. The block:
- Starts at a line equal to
**Files:**after trimming. - Continues through consecutive
- <path>bullet lines. - Ends at a blank line, a heading, or another bold directive.
Apply these checks in order. The first failure halts the entire candidate window:
| Condition | Halt reason |
**Files:** block missing on a parallel candidate | parallel-files-missing |
**Files:** block exists but contains zero bullets | parallel-files-malformed |
Inline form like **Files:** lib/foo.js (no bullet list under it) | parallel-files-malformed |
Any path contains a glob character (*, ?, [) | parallel-files-malformed |
Any path contains a . or .. traversal segment | parallel-files-malformed |
Any absolute path (starts with /) | parallel-files-malformed |
Any backslash separator (\) | parallel-files-malformed |
| Duplicate path inside the same slice | parallel-files-malformed |
Directory-only path with trailing / | parallel-files-malformed |
Candidate-window halt rule: if any of the above halts fire on any candidate slice, halt before creating or bootstrapping any worktree in that window. Do not partially set up worktrees and roll back. The halt summary must surface the exact offending slice id, path, and value.
Phase B.1.5 — Optional expert pre-review (v0.8.0)
For each expert tagged with pre-dispatch in its registry phases (currently architecture and test per agents/dispatchers.json), drive the expert turn via the two-step orchestration pattern. Claude Code does not expose the Agent tool to Node modules, so the runtime cannot dispatch the subagent itself — Claude (orchestrator) drives the Agent dispatch, and the runtime drives everything else around it.
Step 1: Assemble the spawn prompt + dispatch the Task subagent yourself.
const { assembleSpawnPrompt } = await import('<plugin>/lib/codex-bridge/reviewer-turn.js');
// adapter is derived from the v0.9.0 resolution step (resolveAdapter →
// resolved.cli) BEFORE this request is built. It MUST be present or
// runTurnWithDeps defaults the sidecar audit field to 'claude-task'
// regardless of the actual transport (slice 5b contract).
const adapter = resolved.cli === 'claude'
? 'claude-task'
: `cli-harness:${resolved.cli}`;
const request = {
identity: expertIdentity, // from selectTeammates result
repoRoot: <repoRoot>,
specPath: <specPath>,
specSnippet: <slice plan section as context>,
phase: 'pre-dispatch',
sliceId: <currentSliceId>,
adapter, // sidecar audit field; must match transport
sidecarParticipantState: <prior turn summaries for this expert, if any>,
task: 'Pre-dispatch review of the planned slice. Surface blocking ' +
'architectural / test-coverage concerns before implementation begins.',
};
// Need unreadMessages array to render the prompt; readUnreadMessages is exported by mailbox.js.
const { readUnreadMessages } = await import('<plugin>/lib/codex-bridge/mailbox.js');
const unreadMessages = await readUnreadMessages(request.repoRoot, request.identity.id);
const prompt = assembleSpawnPrompt({ ...request, unreadMessages });Then YOU (Claude as orchestrator) dispatch the Task tool with prompt as the subagent's instructions. Capture the subagent's final response as taskResponseText.
Step 2: Drive the rest of the pipeline via runTurnWithDeps with an agentDispatch impl that just returns taskResponseText:
const { runTurnWithDeps } = await import('<plugin>/lib/codex-bridge/reviewer-turn.js');
const result = await runTurnWithDeps(request, {
agentDispatch: async () => taskResponseText,
});
// result: { ok: true, result: ParsedMachineResult } | { ok: false, reason: ... }This runs through: re-read unread → re-assemble prompt (idempotent + cheap) → call your agentDispatch stub → parse → repair if needed → mark-read on success → append sidecar turn.
DO NOT call runTurn(request) directly. The default agentDispatch is intentionally unwired (throws with a clear message pointing here) because Claude Code's Agent surface is not callable from a Node module. The two-step pattern above is the executable orchestration contract for v0.8.0.
Do NOT call runTurn(expert, {phase}) with two arguments — that's the scheduler's INTERNAL wrapper signature inside B.5.5's drainPeerDMs deps, not a public API.
If all pre-review experts SHIP (no blocking findings), held turn records flow through to B.7 where they are attached to the dispatch record's reviewer_blockers: [] (empty) + reviewers_selected[] + reviewer_turn_ids[]. Normal flow continues to B.2.
If any pre-review expert returns blocking findings (parsed Machine Result.blocking_findings[] non-empty), the slice does NOT proceed to B.4 dispatch. To preserve the durable-state requirement, the orchestrator immediately appends a sentinel halted dispatch record to anchor the blockers on a durable record:
appendImplementDispatch(specPath, sliceId, {
/* ...required dispatch fields... */
outcome: "failed-halted",
failure_reason: "pre-dispatch-blocker",
dispatched_at: <now-iso>,
reviewers_selected: <expert ids>,
reviewer_turn_ids: <turn ids from pre-review>,
reviewer_blockers: [/* ...findings with disposition:"open"... */],
});The sentinel exists ONLY to anchor blocker state on a durable record — it does NOT represent a real dispatch (no worker spawned, no commits made). The sentinel makes updateDispatchExpertBlocker({sliceId, dispatched_at}, findingId, ...) resolution work uniformly across pre-dispatch and post-dispatch blockers. After the sentinel is appended, halt with expert-blocker-open (preserves expert mailboxes per slice 7 archival policy).
On resume, Claude reads the sentinel's reviewer_blockers[] and either:
- (a) overrides technical false-positives via
updateDispatchExpertBlocker({sliceId, dispatched_at}, findingId, {disposition: "technical-override", rationale, evidence}) - (b) routes product/UX/business findings to the human user (halt code
expert-blocker-needs-user).
Once all blockers reach disposition !== "open", the orchestrator may advance to B.4 (treating the sentinel as a resolved pre-dispatch gate) or supersede with a real dispatch.
Phase B.2 — Ready-set + first-fit batching (v0.7.3 REPLACEMENT)
v0.7.3 replaces the v0.7.0 "consecutive non-overlapping slices" logic with DAG-aware batching. Non-consecutive slices can now parallelize when both deps and Files allow.
import { computeReadySet, maximalFirstFitNonOverlap } from '<plugin>/lib/codex-bridge/dependency-graph.js';
const readySet = computeReadySet(dag, sliceStates);
// pending slices whose every dep has shipped state.
const batch = maximalFirstFitNonOverlap(readySet, filesIndex);
// deterministic first-fit by numeric slice id; greedy-include if Files
// set disjoint from already-included.Algorithm guarantees:
- Deterministic. Same DAG + same Files yields the same batch every time. Sort key is numeric slice id.
- Conservative. Returns the largest non-overlapping subset achievable via first-fit. Doesn't attempt combinatorial-optimal bin-packing (NP-hard for arbitrary inputs; first-fit is good enough for typical N=2-10).
- Mixed-implementer compatible. Conflict detection still runs only on Files sets, not on implementer choice. A Codex backend slice and a Sonnet UI slice can both batch together if their Files don't overlap.
- Empty ready-set is OK. When a session has only in-progress codex-background-bash dispatches and no pending slices have all deps shipped,
readySet === []andbatch === []. Phase B turn ends; orchestrator waits for in-flight completions (Phase B between-turns inbox polling, see B.4.5).
Per-slice sidecar update for the batch:
For each slice in the chosen batch, write setImplementMeta with:
parallel_group: a batch id likeparallel-<ISO>-<slice-from>-<slice-to>ifbatch.length > 1, elsenull.parallel_suppressed_reason: typicallynullunder v0.7.3 (we no longer "suppress" — we just don't include conflicting slices in the batch). For backward compatibility with v0.7.1 sidecars, this field is preserved asnull.
Slices in readySet that did NOT make it into batch (because they overlap with a higher-priority pick) stay pending and become candidates next turn.
Phase B.3 — Worktree setup per batch
For each slice in the batch, build an isolated worktree using the worktree.js primitives from slice 2:
- Resolve the slice's
slice_start_sha(from the autopilot block; for parallel candidates this is the same SHA across the batch — they all branch from the same starting point). - Call:
import { create, bootstrap, verifyBootstrap } from '<plugin>/lib/codex-bridge/worktree.js'; const c = create(repoRoot, sliceId, sliceStartSha);createenforces the.git-worktrees/gitignore, the path-conflict check, and runsgit worktree add -b <slice-id>-impl <repo>/.git-worktrees/<slice-id> <slice_start_sha>. Surface its halt unchanged (e.g.,worktree-gitignore-missing,worktree-path-conflict,worktree-create-failed). - Resolve the configured symlinks via
loadProjectConfig(repoRoot)— theworktree_bootstrap.symlinksarray of{path, required}entries, with v0.7.0 defaults applied (slice 1). - Call:
On halt, surface unchanged (e.g.,const b = bootstrap(repoRoot, c.worktreePath, symlinks);worktree-bootstrap-failed). - Persist the bootstrap record before dispatch:
Recorded symlinks are the paths that were actually linked (skipped optional missing entries are not recorded).cli sidecar-set-implement-bootstrap --specPath <spec> --sliceId <slice-N> \ --bootstrap '{"symlinks":[<recorded paths>],"completed_at":"<ISO now>"}'
Two-tier bootstrap gate (run immediately before dispatching the implementer for that worktree):
- Tier 1 — sidecar marker. Read the sidecar. Confirm
slice_reviews[slice-N].phases.implement.bootstrap.completed_atis a non-empty string. Missing → haltworktree-bootstrap-failed. - Tier 2 — symlink reality check. Call
verifyBootstrap(worktreePath, recordedSymlinks, repoRoot). Each recorded path mustlstatas a symlink andreadlinkto<repoRoot>/<path>. If{ok:false}, haltworktree-bootstrap-stale. Diagnostics include the per-symlink{symlink, expected, actual}triples returned byverifyBootstrap.
Both tiers must pass for every worktree in the batch before any subagent dispatch fires. A single Tier-1/Tier-2 failure in a parallel batch halts the whole batch — do not dispatch the other slice(s) while one worktree is broken.
Phase B.4 — Dispatch (transport-aware, single-turn parallel)
v0.7.2 splits dispatch by transport. Look up the chosen implementer's transport in agents/dispatchers.json:
transport: claude-subagent→ use theTasktool to dispatch the named subagent. The subagent file is atagents/<agent>.md(e.g.,slice-implementer-sonnet.md).transport: codex-background-bash→ use theBashtool withrun_in_background: trueto spawn the codex wrapper script. The contract for codex's invocation lives atdocs/codex-implementer-contract.md(NOT a Claude Code subagent file).
Pre-dispatch mailbox injection (v0.7.3.1)
Before issuing the dispatch tool call (either transport), the orchestrator pre-injects any unread messages the recipient already has queued. This guarantees that messages written between B.4.5 polling cycles and this dispatch are delivered to the agent at start, without races.
For each slice in the parallel batch, in the same turn that will issue dispatches:
- Read the recipient's unread inbox as orchestrator:
node <plugin>/lib/codex-bridge/cli.js mailbox-read \ --for slice-N --actor orchestrator --unread \ --repoRoot "$REPO_ROOT" - If non-empty, prepend a
<codex-paired-pending-messages>block to the dispatch prompt (same wrapper format the hook emits, so codex- and Sonnet-path agents see a single canonical shape). The trailing note differs from the hook's: pre-injected messages are NOT marked read until terminal result, so use the queued-not-marked phrasing:
The hook's mid-run delivery uses "have been marked read" because the hook marks-read immediately after stdout flush; pre-injection's mark-read is deferred to terminal-result handling (step 4 below). Keeping the wrappers visually similar but the trailers semantically accurate prevents agents from assuming a delivery means a mark.<codex-paired-pending-messages recipient="slice-N"> <pending-message id="<id>" from="<sender>" timestamp="<iso>"> <body> </pending-message> ... </codex-paired-pending-messages> (Messages above were queued for you before this dispatch. They have NOT yet been marked read — the orchestrator marks them read when this dispatch reaches a terminal result.) - Include
injected_message_idsin the dispatch record at the time the orchestrator appends/finalizes it. For the Codex path, that's the immediateoutcome: "in-progress"record (Phase B.7) — include the captured ids there. For the Sonnet path, include the ids in whichever dispatch record the existing flow appends (typically at terminal-result time). Empty array is a positive assertion of "nothing was pre-injected"; absent field is back-compat with pre-0.7.3.1 records. - On terminal result (slice reports DONE/BLOCKED, or codex task exits), mark the pre-injected ids as read using the batch CLI:
If dispatch failed before the agent process started (codex binary not found, Task tool refused), SKIP the mark-read — messages stay unread for the next dispatch attempt. Duplicate delivery is preferred over silent loss.node <plugin>/lib/codex-bridge/cli.js mailbox-mark-read-batch \ --for slice-N --actor orchestrator \ --message-ids "<csv>" \ --repoRoot "$REPO_ROOT"
For the Sonnet path, in-flight mailbox messages that arrive AFTER dispatch are auto-delivered by the PostToolUse hook (hooks/mailbox-inject.sh → lib/codex-bridge/hook-mailbox-inject.js). Identity flows through the subagent's cwd (which is the worktree path) — no env-var manipulation required. The hook reads the slice's unread inbox after every Bash/Edit/Write/Read tool call, emits an additionalContext block, and marks the delivered messages read.
For the Codex path, mid-run injection is architecturally impossible (codex is an opaque subprocess; MCP protocol has no spec-level push). Pre-injection here covers the start-of-run case; cooperative checkpoints in the dispatch prompt body cover the rest (see "Mailbox protocol in agent prompts" below).
Sonnet path (transport: claude-subagent)
Dispatch via Task tool with subagent_type: slice-implementer-sonnet. Prompt content:
- Slice id (e.g.,
slice-3). - Full slice section text from the plan, verbatim.
- Worktree absolute path (the subagent's
cwd). slice_start_sha.- Phase A's structured
validation_coveragefor this slice if available. - Commit Conventions (subject-only, slice number = current slice; trailer not required, presence does not break compliance).
- Required test/verification commands.
- Instruction to leave all changes committed in the worktree before reporting
DONE. - Reminder of the final-message JSON contract:
{"status":"DONE"|"BLOCKED"|"NEEDS_CONTEXT","concerns":[]}.
Codex path (transport: codex-background-bash, v0.7.2)
The orchestrator dispatches codex exec directly via Bash with run_in_background: true. No subagent wrapper. This pattern mirrors Claude Code's LocalShellTask (per src/tasks/LocalShellTask/ in the runtime source) and supports unbounded codex runtimes (subject to codex_dispatch.max_runtime_ms configured via .codex-paired/project.json, default 2 hours).
Pick the form from the role (v0.17.0). Run node <plugin>/lib/codex-bridge/cli.js model-role --role implement --format json (or implement_fallback on rung 2) and read cli. codex → the codex form below; agy → the agy form in docs/codex-implementer-contract.md (--cwd <worktree> + agy -p … --sandbox --dangerously-skip-permissions --add-dir <repo>/.git --output-format json --print-timeout 2h). Never hard-code one: the wrapper exits 78 when the command disagrees with the role.
Locked invocation, codex form (orchestrator constructs and runs):
<plugin>/scripts/codex-exec-with-status.sh \
<status-file-path> \
--model-role implement \
-- \
codex exec \
--skip-git-repo-check \
-s workspace-write \
-C <worktree-absolute-path> \
--add-dir <repo-root>/.git \
"<implementation prompt>" \
</dev/nullRung 2 passes --model-role implement_fallback. Never write -m or model_reasoning_effort
yourself — the wrapper resolves the role (project/env overrides included), writes model_role /
model / effort into the status file before spawning, and inserts the flags after codex exec;
a caller-supplied model flag makes the wrapper exit 78 (model-role-conflicting-args). --add-dir
<repo-root>/.git is required so the sandboxed codex can commit inside a git worktree (the worktree's
metadata lives under the main repo's .git/).
The wrapper at scripts/codex-exec-with-status.sh captures exit code + timestamps + signal (and the
model snapshot) in a JSON status file. This is durable evidence — the on-disk status file survives orchestrator session termination, unlike Claude Code's in-memory Bash task registry.
Per-dispatch path generation (orchestrator computes):
<status-file-path>:~/Library/Application Support/Inkling/diagnostics/codex-dispatch/<slice-id>-<ISO-timestamp>.status.json<output-file-path>: same prefix with.logsuffix; passed to Bash via the standard background-task output capture.
<implementation prompt> argument: same content as the Sonnet path's prompt, but composed as a single string and passed as the final positional argument to codex exec. See docs/codex-implementer-contract.md for the full contract.
Bash invocation parameters:
run_in_background: true— required. Returns immediately with a Bash task id.- No
timeoutparameter (background tasks have no synchronous cap; runtime bound iscodex_dispatch.max_runtime_ms).
After dispatch, the orchestrator immediately writes an outcome: "in-progress" dispatch record (Phase B.7) with transport: codex-background-bash, task_id, output_file, status_file. This is durability evidence — if the orchestrator crashes between dispatch and completion, the next session can read the status file to determine codex's terminal state.
Single-turn parallel dispatch (load-bearing for both transports)
When the batch contains more than one slice, dispatch ALL implementers in a SINGLE assistant turn using Claude's parallel-tool-call mechanism — emit multiple tool calls in the same response. This works across transports: a mixed batch may emit one Bash run_in_background:true for a codex slice + one Task for a sonnet slice in the same turn. Both empirically parallelize:
- Codex via
Bash: 18s for 2 × 5s tasks (proven in v0.7.0 release validation). - Sonnet via
Task: 1s gap between two 5s tasks (proven in v0.7.1 validation; Task subagents parallelize natively).
Issuing dispatches across separate turns is non-conforming and breaks the wall-clock assertion in tests/smoke/implementer-routing-parallel.sh.
Subagent return contract (Sonnet path)
The Sonnet subagent's final message ends with a fenced JSON block. Read its status field. The orchestrator only consults this status for BLOCKED and NEEDS_CONTEXT halts; for everything else the reconciler is authoritative (Phase B.5). If the JSON is missing or malformed, treat it as missing-or-malformed-json and route into fallback rules in Phase B.6 — unless the message is an unambiguous blocker phrased as natural language ("blocked: X"), in which case record BLOCKED and halt without fallback.
Codex completion (codex path)
The codex background task completes asynchronously. Claude Code emits a task-notification when the Bash task exits. The orchestrator handles this notification — possibly across multiple assistant turns from when dispatch happened — by:
- Reading the status file at
<status-file-path>(see schema + lifecycle indocs/codex-implementer-contract.md). - Classifying it with
classifyStatusFilefromlib/codex-bridge/dispatch-status.js({ statusFile, taskAlive, runtimeMs, maxRuntimeMs, halts: { timeout: 'codex-background-timeout', lost: 'codex-background-task-lost', failed: null } }), thendecideImplementAction({ classification, rung, reconciled })andapplyImplementDecision(decision, { poll, reconcile, ship, reset, dispatchNextRung, halt }):completed→reconcile(run the Phase B.5 reconciler), then calldecideImplementActionagain withreconciled→shiporfallback.failed, or reconciled zero / non-conforming commits →fallback(Phase B.6: reset, then the next rung).config-error(statusexit_code: 78) →haltwith the file'serror(model-role-resolution-failed/model-role-conflicting-args) — terminal: no reset, no next rung, no Sonnet fallback.blocked/needs-context→halt(codex-blocked/codex-needs-context) — never fallback.- nonterminal status (
exit_code: null) while the Bash task is alive → keep polling; with the task gone → haltcodex-background-task-lost(the pre-launch status write cannot hide task loss). - missing status file → transient while the task is alive, else halt
codex-background-task-lost.
- Calling
finalizeImplementDispatch(specPath, sliceId, taskId, terminal)to promote the in-progress dispatch entry to its terminal outcome (Phase B.7), copyingmodel_role/model/effortfrom the status file — the orchestrator never resolves them itself for this transport.
If the in-progress codex task exceeds codex_dispatch.max_runtime_ms, the orchestrator MUST kill it (best-effort SIGTERM via kill <pid>, then SIGKILL after 5s grace) and halt codex-background-timeout. The wrapper script writes a status file on signal-kill recording signal: "SIGTERM" and exit_code: 143.
Multi-turn completion handling does NOT violate the single-turn parallel dispatch invariant. The invariant is: all parallel batch dispatches are issued in one turn. Completion is inherently async.
Do not begin Phase B.8 integration cherry-pick until ALL slices in the parallel batch are terminal (shipped / blocked / failed). Reconcile each slice as it completes, but defer integration until the batch is done.
Mailbox protocol in agent prompts (v0.7.3)
Every dispatch prompt — codex (Bash invocation prompt) and sonnet (Task subagent prompt) — appends this protocol block:
=== Mailbox protocol ===
Your inbox: <repo>/.codex-paired/mailboxes/<your-slice-id>.json
To send a status / progress / blocker message to the orchestrator:
node <plugin>/lib/codex-bridge/cli.js mailbox-write \
--to orchestrator \
--from <your-slice-id> \
--text-stdin
(then write your message body to stdin)
Tip: prefix BLOCKER messages with "BLOCKER:" so the orchestrator surfaces
them immediately. Progress messages are best-effort logging — failure to
deliver does not block your work. BLOCKER delivery failure DOES block:
if your mailbox-write call for a BLOCKER fails, set your final-message
JSON status to BLOCKED/NEEDS_CONTEXT with the delivery-failure noted in
concerns.
To send a message to another in-flight slice (rare; usually mediated by
orchestrator instead):
node <plugin>/lib/codex-bridge/cli.js mailbox-write \
--to slice-N --from <your-slice-id> --text-stdin
To read your own inbox (orchestrator may have asked you something):
node <plugin>/lib/codex-bridge/cli.js mailbox-read \
--for <your-slice-id> --actor <your-slice-id> --unread
For long-running slices: send a progress update every 5-10 minutes so the
orchestrator has visibility (e.g., "Progress: 30% — finished test list").Mailbox cooperative checkpoints (v0.7.3.1, codex-transport only)
For transport: codex-background-bash dispatches, the orchestrator additionally appends the following checkpoints block AFTER the shared protocol block. Sonnet-transport dispatches MUST NOT receive this block — the PostToolUse mailbox-inject hook delivers in-flight messages to Sonnet automatically, and adding ritual-polling instructions on top would just teach the agent to second-guess auto-delivery.
=== Mailbox checkpoints (codex transport) ===
The orchestrator cannot mid-run-inject messages into codex sessions (codex is
an opaque subprocess from Claude Code's perspective). Cooperative polling at
named semantic checkpoints is the mechanism. Call:
node <plugin>/lib/codex-bridge/cli.js mailbox-read \
--for <your-slice-id> --actor <your-slice-id> --unread \
--repoRoot "$REPO_ROOT"
at exactly these five points:
1. At the START of your work — after reading this prompt, before
implementation planning or any repo-mutating command. (The poll itself
is a command, but it's a read-only one; this checkpoint is about
surfacing inbox context before you commit to a plan.)
2. BEFORE running each test (before `npm test`, `pytest`, `cargo test`,
etc.).
3. BEFORE each `git commit`.
4. AFTER any command that takes longer than ~30 seconds (long build, long
test run, long network call).
5. BEFORE composing your final response JSON.
Do NOT poll before every file edit. That trains ritual polling rather than
thoughtful integration; the checkpoints above are the ones where new context
is most likely to be load-bearing.
If `mailbox-read` returns one or more messages, process them inline before
continuing your current step (each message is a JSON object with `from`,
`text`, `timestamp` — read them and decide whether they change your plan).
Then, to acknowledge that you have consumed them, mark them read:
node <plugin>/lib/codex-bridge/cli.js mailbox-mark-read-batch \
--for <your-slice-id> --actor <your-slice-id> \
--message-ids "<csv>" \
--repoRoot "$REPO_ROOT"
Read failures and mark-read failures are best-effort logging from your
perspective — they do not block your work; the orchestrator's between-turn
polling (Phase B.4.5) is the authoritative halt path for mailbox-corruption.Architectural framing: pre-injection (Phase B.4 pre-injection flow) covers the START-of-run delivery for codex; checkpoints (this block) cover the rest of the run. Combined they bound the worst-case mid-run latency to the gap between two semantic checkpoints (typically seconds-to-minutes, not the whole codex runtime).
The orchestrator reads in-flight inboxes between turns (Phase B.4.5).
Phase B.4.5 — Between-turns inbox polling (v0.7.3)
Between every assistant turn during Phase B (after dispatching parallel batches, before the next turn's actions), the orchestrator:
- Reads
orchestrator.jsoninbox (mailbox-read --for orchestrator --actor orchestrator --unread). Surfaces every unread message as diagnostics. - For every slice with outcome=in-progress: reads its inbox (
--for slice-N --actor orchestrator --unread). Surfaces unread messages. - Marks each surfaced message as read via
mailbox-mark-read. - Special handling for messages whose
textstarts withBLOCKER:: pause forward-progress for that slice's batch and surface the blocker to the user. Halt code:slice-blocker-from-mailbox(slice still in-progress; user investigates whether to abort or wait).
v0.7.3.1 role for Sonnet slices. B.4.5 polling for Sonnet inboxes was the primary delivery path in v0.7.3. Starting v0.7.3.1 the PostToolUse mailbox-inject hook is the primary in-flight delivery mechanism for Sonnet subagents; B.4.5 polling becomes a crash-recovery safety net that catches messages still unread in two scenarios: (a) hook failed (breadcrumb in .codex-paired/diagnostics/hook-failures.jsonl); (b) slice has not yet hit a Bash/Edit/Write/Read tool call that would trigger the hook. Codex slices still rely on B.4.5 polling as primary (no auto-injection is possible for the codex subprocess transport).
v0.7.3.1 race guard for pre-injection (load-bearing). Pre-injected messages (per the pre-injection flow above) sit in the recipient's inbox as unread until terminal result, because mark-read is deferred to step 4 per the queued-not-marked contract. B.4.5 between-turns polling MUST exclude these ids; otherwise it would surface and mark-read messages that the dispatch already delivered, breaking the "terminal-result owns the mark" invariant and causing duplicate diagnostic surfacing. When polling a slice with outcome=in-progress, the orchestrator computes the in-progress dispatch's injected_message_ids set and skips any inbox messages whose id appears in it. Terminal-result handling owns those ids' mark-read via mailbox-mark-read-batch. Messages NOT in injected_message_ids (new arrivals after pre-injection) are surfaced and marked as usual.
Mailbox failures during polling halt the orchestrator (per spec rev5 §6.3 boundary table — orchestrator ops are NOT best-effort):
mailbox-corrupt— inbox JSON unparseable; corrupt file moved to archive (best-effort) before halt.mailbox-lock-timeout— 50-retry budget exhausted; filesystem trouble.mailbox-permission-denied— should never fire here (orchestrator can read any inbox); indicates programming error.
Archive rotation timing: the orchestrator runs the size check + archiveAndReset decision once per Phase B turn, AFTER reading inboxes but BEFORE the next dispatch. Per-poll size checks are too aggressive (write-amplification under heavy contention); once-per-turn is the right granularity. For each in-flight slice + orchestrator inbox:
import { inboxSizeBytes, archiveAndReset } from '<plugin>/lib/codex-bridge/mailbox.js';
import { applyMailboxDefaults } from '<plugin>/lib/codex-bridge/project-config.js';
const cfg = applyMailboxDefaults(projectConfig);
const size = await inboxSizeBytes(repoRoot, recipient);
if (size > cfg.max_bytes) {
await archiveAndReset(repoRoot, recipient, { archive_policy: cfg.archive_policy });
}All-unread overflow during archiveAndReset throws MailboxError(code='mailbox-overflow-unread') — orchestrator halts (no silent loss). Run cleanupArchives periodically (once per autopilot session is enough) to apply retention policy.
Mailbox archival policy (v0.8.0)
At terminal-state halt (autopilot is stopping forward progress —
either completed / abandoned-by-user or any of the preserve-class
halt reasons below), iterate every active expert in
reviewer_teammates.selected[] and call:
import { archive } from '<plugin>/lib/codex-bridge/reviewer-runtime.js';
for (const expert of sidecar.reviewer_teammates.selected) {
if (expert.status === 'archived' || expert.status === 'failed') continue;
const result = await archive(
{ id: expert.id },
haltReason,
{ repoRoot } // required for ARCHIVE reasons; ignored for PRESERVE
);
// result = { expert_id, status, archive_reason, archived_at }
// Persist via updateExpertStatus(specPath, expert.id, result.status === 'archived' ? 'archived' : expert.status)
// (PRESERVE reasons retain the prior status — the mailbox is kept for resume.)
}The policy from lib/codex-bridge/expert-archive.js:
| Halt reason | Action | Rationale |
completed | ARCHIVE | Feature done; queued teammate state no longer needed. |
abandoned-by-user | ARCHIVE | Explicit cleanup intent. |
external-commit-detected | PRESERVE | Operator must inspect why HEAD diverged. |
slice-blocker-from-mailbox | PRESERVE | Slice in-progress; user decides abort vs wait. |
expert-blocker-open | PRESERVE | Blocking finding awaiting override/resolution. |
expert-peer-dm-drain-cap-exceeded | PRESERVE | Pending peer DMs must survive for resume. |
expert-peer-dm-enqueue-failed | PRESERVE | v0.8.1: writeToMailbox failed for one or more outbound peer DMs; targets recorded in peer_messages_failed for triage. |
subagent-dispatch-failed | PRESERVE | Transient dispatch failure; retry needs queued state. |
reconcile-failed | PRESERVE | Worktree unreliable; operator must inspect. |
validation-failed | PRESERVE | Live verification halted; expert state may inform fix. |
user-input-required | PRESERVE | User answers may reshape queued DMs. |
ARCHIVE drains the inbox via archiveAndReset (preserves read messages
under .codex-paired/archives/, resets the inbox to unread-only).
PRESERVE is a no-op on the mailbox — read messages stay, unread stay
unread — so the next autopilot resume picks up where the last run
stopped.
Unknown halt reasons throw ExpertArchiveError code
unknown-halt-reason. If you find yourself reaching for a new halt
reason, extend the set in expert-archive.js deliberately (the
audit-grep target is "every halt reason maps to ARCHIVE or PRESERVE,
no silent skip").
v0.8.0 update. B.4.5 now ALSO polls active expert inboxes (as recorded in reviewer_teammates.selected[]) alongside orchestrator + in-flight slice inboxes. If an active expert has unread messages at B.4.5:
- Record the inbox state (don't immediately spawn — expert review should see reconciled implementation truth at B.5.5, not partial worker output).
- Schedule the expert for B.5.5 post-review drain.
- Exception: if the unread message indicates a dispatch safety issue (stale basis, wrong slice, command failure), halt before B.5 and route through B.6 fallback.
v0.9.0 halt reasons (panel + CLI dispatch)
Slice 7b adds a halt-envelope module (lib/codex-bridge/halt-envelope.js, not yet shipped — referenced here by name only) that normalizes halt records across autopilot phases. Autopilot's existing halt-reason set extends with these v0.9.0 codes:
| Halt reason | Phase | Class | Rationale |
panel-quorum-unavailable | B.0.5 / release-gate | PRESERVE | dispatchPanel had fewer dispatchFns than panel_min_size at config-time. |
panel-disagreement | B.5.5 / release-gate | PRESERVE | Panel returned mixed verdicts after one consensus round (size=2 or size≥3 stuck). |
panel-quorum-lost | B.5.5 / release-gate | PRESERVE | Mid-panel member dropped below quorum after parse-failure. |
panel-config-invalid | B.0.5 / release-gate | PRESERVE | panel_max_size < panel_min_size (or below hard floor of 2). |
cli-dispatch-failed | B.0.5 / B.1.5 / B.5.5 | PRESERVE | resolveAdapter returned no available CLI for a required reviewer role. |
All v0.9.0 halt reasons are PRESERVE-class (per spec § 5 fail-closed defaults). The mailbox archival table above MUST be extended in expert-archive.js when slice 7b ships the halt-envelope module — until then, an unknown halt-reason throws ExpertArchiveError code unknown-halt-reason. The skill prose surfaces the new halt codes to the user verbatim; the halt-envelope module formalizes the per-code surface (sidecar field shapes, resume signaling).
Phase B.5 — Reconcile (reconciler is truth)
Public-API digest refresh (v0.18.0) — first thing in B.5, before verification and before C0.
If the slice touched any module in the CLI's pinned import closure or any pinned JSON input
(docs/public-api.md cli-verbs block: closure, module_digest, input_digest), run
node scripts/cli-surface.mjs --digest --write from the slice worktree root (the script and the
document it rewrites are the worktree's own; the script defaults --root to the repository it lives
in, so invoking the installed plugin's copy would refresh the plugin checkout instead — if you must
call another copy, pass --root <slice-worktree>), and commit the docs/public-api.md diff onto the
slice branch so the reviewed SHA carries it (npm test fails on a stale digest, and Phase C requires
green verification). --write touches only those three keys; Phase D keeps the semantic review of
the documented cases.
For each returned subagent, call:
import { reconcileWorktree } from '<plugin>/lib/codex-bridge/reconciler.js';
const r = reconcileWorktree({
worktreePath: <worktree absolute path>,
sliceStartSha: <slice_start_sha>,
sliceId: <slice-N>,
});The reconciler reads git -C <worktree> log <slice_start_sha>..HEAD and verifies every subject against ^(feat|test|fix|docs|refactor|chore)\(slice:<N>\): <description>. It returns:
{
ok: true,
commits: [{sha, subject}, ...], // oldest → newest
head_sha,
commit_count,
non_conforming_subjects: [{sha, subject, reason}, ...],
}or {ok:false, halt:{reason:"reconciler-failed", detail}} on git failure.
Reconciler is the source of truth. Subagent JSON status is advisory — never use it to populate commit_count, head_sha, or commits. The orchestrator trusts what git says. Specifically:
- Subagent reports
DONEbut reconciler sayscommit_count == 0→ fallback trigger (zero-commits). - Subagent reports
DONEbutnon_conforming_subjectsis non-empty → fallback trigger (non-conforming-commits). Cite the SHA in diagnostics. - Subagent reports
DONEand reconciler returns ≥1 commits with emptynon_conforming_subjects→ success. - Subagent reports
BLOCKED→ halt withcodex-blocked(Codex implementer) orsubagent-blocked(Sonnet implementer). Do not fall back. Reconcile commits the subagent made before bailing for the audit trail, but do not advance. - Subagent reports
NEEDS_CONTEXT→ halt withcodex-needs-contextorsubagent-needs-context. Same no-fallback rule. reconcileWorktreereturns{ok:false}(e.g., bad sha, broken worktree) → fallback trigger (treated as a dispatch failure: the worktree is unreliable).
Phase B.5.5 — Expert post-review and peer-DM drain (v0.8.0)
After reconcile and before routing/fallback, invoke the peer-DM drain scheduler. For each active expert selected for the slice (per reviewer_teammates.selected[]), the scheduler:
- Reads unread messages for that expert.
- Spawns the expert with the reconciled slice output plus unread messages.
- Parses the machine result.
- Marks injected messages read only after parse success.
- Records findings, DMs sent, and status in the sidecar via
appendReviewerTurn+updateExpertStatus. - If the expert sent DMs to another active expert, schedules the recipient in the same B.5.5 drain loop.
const { drainPeerDMs } = await import('<plugin>/lib/codex-bridge/expert-dm-scheduler.js');
// v0.9.0: build the per-expert resolution map BEFORE invoking the scheduler.
// The runTurn wrapper below reads it via drainContext.resolvedByExpertId to
// compute each expert's adapter for sidecar audit fields.
const resolvedByExpertId = {};
for (const expert of activeExperts) {
resolvedByExpertId[expert.id] = resolveAdapter(expert.id, availableCLIs, projectRouting);
}
const drainResult = await drainPeerDMs(
activeExperts,
{
hasUnread: async (expertId) => (await readUnreadMessages(repoRoot, expertId)).length,
// runTurn wrapper for the scheduler: applies the same two-step pattern
// as B.1.5 (Claude dispatches the Task subagent itself; passes the
// response as agentDispatch). The scheduler invokes this once per
// turn it schedules.
runTurn: async (expert, drainContext) => {
// v0.9.0: resolve the expert's adapter from the recommendation ladder
// (same as B.0.5 / B.1.5). The map MUST be passed through drainContext
// by the orchestrator — drainPeerDMs does not call resolveAdapter itself.
const resolved = drainContext.resolvedByExpertId[expert.id];
const adapter = resolved.cli === 'claude'
? 'claude-task'
: `cli-harness:${resolved.cli}`;
const request = {
identity: expert,
repoRoot,
specPath,
specSnippet: <reconciled slice diff snippet>,
phase: drainContext.phase,
sliceId: drainContext.sliceId,
adapter, // sidecar audit field; must match transport
sidecarParticipantState: <prior turn summaries>,
task: 'Post-implementation review of the reconciled slice.',
};
const unreadMessages = await readUnreadMessages(repoRoot, expert.id);
const prompt = assembleSpawnPrompt({ ...request, unreadMessages });
// YOU (Claude) dispatch the appropriate transport: the Task tool for
// 'claude' (with `prompt`), and for EVERY other CLI the production helper
// dispatchReviewerViaHarness (lib/codex-bridge/reviewer-dispatch.js) —
// v0.16.0: it resolves the model role BY PHASE ('post-implementation-review'
// → review), assembles the real prompt, and delegates to the cli-harness
// with an explicit timeout_ms (15min). NEVER a hand-rolled `codex exec` in
// background Bash — a raw background exec with stderr suppressed parks
// invisibly on auth prompts (observed: 25min + 3h24m panelist hangs).
let requestForTurn = request;
if (resolved.cli !== 'claude') {
const { dispatchReviewerViaHarness } =
await import('${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/reviewer-dispatch.js');
const out = await dispatchReviewerViaHarness(request, { cli: resolved.cli, variant: resolved.variant, repoRoot }); // forward the variant: the helper defaults to 'read-only'
taskResponseText = out.responseText;
requestForTurn = out.requestForTurn; // adapter + modelRole (+ warning)
}
return await runTurnWithDeps(requestForTurn, {
agentDispatch: async () => taskResponseText,
});
},
readExpertTurns, // sidecar.readExpertTurns directly
writeBreadcrumb,
},
{
maxRespawnsPerExpert: 2,
maxTotalTurns: 8,
specPath,
drainContext: {
phase: "post-implementation-review",
sliceId: <currentSliceId>,
resolvedByExpertId, // v0.9.0: per-expert resolver result for adapter derivation
},
resumeFromSidecar: <true if recovering>,
}
);drainContext is REQUIRED. The scheduler fails closed (throws) if it's missing or if readExpertTurns errors during resume — refusing to fail open prevents double-spawning experts already capped from prior turns.
Loop bounds:
- Maximum 2 respawns per expert per slice per B.5.5 drain.
- Maximum 8 total expert turns per slice per B.5.5 drain.
Halt handling:
drainResult.halt === "expert-peer-dm-drain-cap-exceeded": caps were exhausted while DMs remain unread. Halt withexpert-peer-dm-drain-cap-exceeded(preserves expert mailboxes per slice 7). Claude may narrow the question and retry, defer non-blocking peer discussion, or ask the user.drainResult.halt === "expert-peer-dm-enqueue-failed"(v0.8.1): an outbound peer-DM write failed for one or more requested recipients. The scheduler observesturnResult.peer_dm_summary.failed > 0immediately after arunTurnreturns and halts. Inspect the most recent turn'speer_messages_failed[]to see which recipients and why (invalid-recipient,self-dm,empty-body,malformed-item, or a MailboxError code). PRESERVE policy keeps mailboxes intact so resume can fix-and-replay.drainResult.halt === null: drain converged normally. Continue to B.6 routing.
Sidecar safety net (v0.8.1). Scheduler-observed halt is the primary detector. As a backstop, before declaring drain converged autopilot also scans reviewer_teammates.turns[] for any turn with non-empty peer_messages_failed belonging to the current slice/phase. If found AND drainResult.halt === null, treat it as expert-peer-dm-enqueue-failed (the scheduler missed the signal — e.g., appendReviewerTurn succeeded while the scheduler crashed mid-loop). This is a backup, not the primary path.
Peer-DM schema (v0.8.1). Expert Machine Result emits peer_messages_requested: array of {to, body, summary?}. The orchestrator's runtime (runTurnWithDeps) enqueues each valid item into the recipient's mailbox via writeToMailbox(repoRoot, to, {from: identity.id, text: body ?? summary, summary}). The expert does NOT call mailbox-write itself. Legacy peer_messages_sent (v0.8.0) is accepted as an alias for one release with a parse warning.
Integration gate. Block integration if any unresolved blocking finding remains — i.e., any entry in reviewer_blockers[] (across all dispatch records for this slice) with disposition: "open". See the Blocking-Finding Override Authority section below for resolution paths.
Blocking-Finding Override Authority (v0.8.0)
When an expert emits a blocking finding, the orchestrator MUST resolve its disposition before proceeding past the integration gate.
Technical overrides (Claude may apply). Claude can override a blocking expert finding ONLY when ALL of these are true:
- The finding is technical, not product/UX/business.
- Claude writes a specific rationale citing concrete evidence (file path, line/function, current slice version, command output, reconciler result).
- The override is recorded via
updateDispatchExpertBlocker(specPath, {sliceId, dispatched_at}, findingId, {disposition: "technical-override", rationale, evidence}).
Examples of valid technical overrides:
- Expert referenced a stale slice version.
- Expert misread a command boundary.
- Expert claimed a missing test that exists at the failure boundary.
- Expert flagged a behavior that reconciler output proves is not present.
Product/UX/business overrides (REQUIRE human authorization). Halt with expert-blocker-needs-user. Surface the finding to the user. On user response, record via updateDispatchExpertBlocker(specPath, {sliceId, dispatched_at}, findingId, {disposition: "needs-user", rationale: <user's rationale>}).
Examples requiring the human:
- Expert says the workflow is confusing but technically works.
- Expert says a visual choice weakens the intended product feel.
- Expert says the user-facing copy changes the promise of the feature.
- Expert says the feature scope no longer matches the requested outcome.
Claude rubber-stamping a product/UX/business override (i.e., applying technical-override to a non-technical finding) is a contract violation; the rationale field is auditable, and a non-technical rationale is grounds for reversal during slice review.
Phase B.6 — Apply routing rules
The routing sequence is:
rung 1: codex @ implement
-> rung 2: codex @ implement_fallback on an implementation-dispatch failure
-> rung 3: sonnet subagent on another failure
-> halt implementer-unavailable only when the ladder is exhaustedThe decision is made by decideImplementAction / applyImplementDecision
(lib/codex-bridge/dispatch-status.js); the rules below are what they encode.
Failure triggers fallback (any one of these from the current rung):
- Codex
codex execexits non-zero other than 78 (read from status fileexit_code) — fortransport: codex-background-bash. - Sonnet subagent dispatch error (the
Tasktool call itself failed) — fortransport: claude-subagent. - Zero commits produced (
commit_count == 0). - Non-conforming commits emitted (
non_conforming_subjectsnon-empty). - Missing or malformed final-message JSON (Sonnet path only), when there is no clear blocker signal.
NOT fallback triggers (these halt without descending the ladder):
- Status file
exit_code: 78—model-role-resolution-failed/model-role-conflicting-args(v0.16.0). Terminal: no worktree reset, no next rung, no Sonnet fallback. Fix.codex-paired/project.jsonmodelsor theCODEX_PAIRED_*env, then re-run. - Codex background task exceeds
codex_dispatch.max_runtime_ms— orchestrator kills + haltscodex-background-timeout. - Codex background task lost — orchestrator crashed and there's no status file evidence; halts
codex-background-task-lost. BLOCKED(real blocker — spec is unclear, missing dependency, etc.).NEEDS_CONTEXT(real blocker — agent needs information the orchestrator does not have).
Fallback procedure (per spec §9):
- Append the failed dispatch to sidecar with
outcome: "failed-fallback-pending"(see B.7). - Reset the worktree to
slice_start_sha:
On halt, surfaceimport { reset } from '<plugin>/lib/codex-bridge/worktree.js'; reset(worktreePath, sliceStartSha);worktree-reset-failed. - Re-run worktree bootstrap (
bootstrap(repoRoot, worktreePath, symlinks)), update sidecar bootstrap marker, re-verify withverifyBootstrap. Halt withworktree-bootstrap-failedorworktree-bootstrap-staleon tier failure. - Dispatch the next rung fresh (
applyImplementDecisionawaits the reset first and haltsworktree-reset-failedif it fails — it never dispatches after a failed reset). Do not salvage partial work. Do not pass any context from the failed attempt other than the slice section, worktree path, and sha. - Reconcile only the new rung's commits (range is still
slice_start_sha..HEADbecause the reset moved HEAD back).
Ladder exhausted (all three rungs failed) → halt implementer-unavailable. Record every dispatch failure in the sidecar (one record per rung, each with its model_role / model / effort / rung). Leave the worktree in place for inspection.
For parallel batches, fallback is per-slice. One slice failing the preferred implementer does not trigger fallback or halt for the other slice in the batch. Each slice's reconcile/fallback/halt decision is independent.
Phase B.6.5 — Failure cascade halt (v0.7.3)
When ANY slice in the autopilot run reaches a failed-halted outcome (preferred + fallback both failed, or BLOCKED/NEEDS_CONTEXT halt without recovery), the orchestrator MUST halt the entire autopilot run with dependency-cascade-halt.
import { enumerateDescendants } from '<plugin>/lib/codex-bridge/dependency-graph.js';
const blockedDescendants = enumerateDescendants(dag, failedSliceId);The halt diagnostic includes:
failed_slice_id— the slice that failedfailure_reason— the underlying halt reason (implementer-unavailable,codex-blocked, etc.)blocked_descendants— array of slice ids transitively depending on the failed sliceshipped_so_far— array of slice ids already shipped (preserved on integration branch)
Why halt the whole run rather than continuing with non-descendants? Because:
- The user needs visibility: silently continuing would land partial work without surfacing the failure.
- DAG correctness assumes deps shipped successfully; a failed slice invalidates that assumption for descendants.
- Resume after user-side investigation is preferable to hidden state divergence.
On resume after the user fixes the failed slice (or instructs autopilot to skip it), the next session's Phase B.PRE re-validates the DAG digest. If the plan changed during investigation → halt plan-changed-during-autopilot (catches user edits during recovery). Otherwise resume normally; the failed slice's state in sidecar is reset to pending by the user/skill before relaunch.
For parallel batches with multiple failures: enumerate descendants once for each failed slice; the union is the blocked set.
Phase B.7 — Persist (sidecar)
Use the slice-3 implement-phase persistence CLI/methods (sidecar.js):
- Before the first dispatch in this slice's Phase B, write routing meta:
Use a single batch id (e.g.,cli sidecar-set-implement-meta --specPath <spec> --sliceId <slice-N> --meta '{ "preferred_implementer":"codex|sonnet", "fallback_implementer":"sonnet|codex", "parallel_group":"<batch id or null>", "parallel_suppressed_reason":"files-overlap|null", "worktree":"<absolute worktree path>" }'parallel-<ISO>-<slice-from>-<slice-to>) for all slices in a parallel batch. For serial single-slice batches,parallel_group: null. - After bootstrap completes (per Phase B.3, before dispatch), write the bootstrap record (already shown above):
cli sidecar-set-implement-bootstrap --specPath <spec> --sliceId <slice-N> \ --bootstrap '{"symlinks":[...],"completed_at":"<ISO now>"}' - At codex dispatch time (transport=codex-background-bash, v0.7.2), append an in-progress entry IMMEDIATELY after issuing the background Bash, BEFORE the task completes:
(cli sidecar-append-implement-dispatch --specPath <spec> --sliceId <slice-N> --dispatch '{ "slice_id":"<slice-N>", "agent":"codex", "transport":"codex-background-bash", "task_id":"<Bash task id from run_in_background>", "output_file":"<absolute path to .log>", "status_file":"<absolute path to .status.json>", "dispatched_at":"<ISO>", "worktree":"<absolute worktree path>", "outcome":"in-progress", "rung":1, "model_role":"implement" }'rung1..3 andmodel_roleare v0.16.0 fields;model/effortare copied from the wrapper's status file once it exists — never resolved by the orchestrator.) This is durable evidence — if the orchestrator crashes between dispatch and completion, the next session readsstatus_fileto determine codex's terminal state. - At codex completion (after the Bash task notification arrives + you've read the status file + run reconciler), promote the in-progress entry to its terminal outcome via
finalizeImplementDispatch:
This mutates the in-progress record in place byimport { finalizeImplementDispatch } from '<plugin>/lib/codex-bridge/sidecar.js'; finalizeImplementDispatch(specPath, sliceId, taskId, { outcome: 'shipped' | 'failed-fallback-pending' | 'failed-halted', head_sha: reconciler.head_sha, commit_count: reconciler.commit_count, completed_at: <ISO now>, model_role: statusFile.model_role, model: statusFile.model, effort: statusFile.effort, // v0.16.0 snapshot from the status file concerns: [...] // optional });task_idmatch. Throws if no matching in-progress entry found. At sonnet reconcile (transport=claude-subagent, synchronous), append a terminal dispatch record directly:
cli sidecar-append-implement-dispatch --specPath <spec> --sliceId <slice-N> --dispatch '{ "slice_id":"<slice-N>", "agent":"sonnet", "transport":"claude-subagent", "thread_id":null, "dispatched_at":"<ISO>", "completed_at":"<ISO>", "worktree":"<absolute worktree path>", "head_sha":"<reconciler.head_sha>", "commit_count":<reconciler.commit_count>, "outcome":"shipped|failed-fallback-pending|failed-halted" }'Dispatch entries are append-only. If fallback ships, both records persist: the failed preferred dispatch and the shipped fallback dispatch.
- After successful integration (Phase B.8), update
last_commit_shaviasidecar-set-autopilotand writeslice_reviews[slice-N].phases.implementshipped/commits viasidecar-set-phase.
Phase B.8 — Integration (ordered cherry-pick)
Once every slice in the batch has a successful reconcile (Phase B.5 returned commits with no non-conforming subjects, no halt, no fallback pending), integrate the worktree branches onto the integration branch (the autopilot's primary working branch).
Use the slice-6 module:
import { integrate } from '<plugin>/lib/codex-bridge/worktree-integrate.js';
const r = integrate({
repoRoot,
integrationBranch: '<current branch>',
slices: [
{ sliceId: 'slice-3', branchName: 'slice-3-impl', sliceStartSha: <sha> },
{ sliceId: 'slice-4', branchName: 'slice-4-impl', sliceStartSha: <sha> },
],
});The module:
- Enumerates each slice's source commits with
git patch-id --stable. - Runs resume detection: if all source commits already appear in order on the integration branch, the slice is
resumed-already-integratedand skipped. Partial / order-broken matches halt withworktree-resume-ambiguous(diagnostics includeslice_id,branch_name,integrated_subjects,missing_subjects,integration_branch_head). - Otherwise cherry-picks each commit in order. Conflict →
git cherry-pick --abort, halt withworktree-merge-conflict(diagnostics includeslice_id,branch_name,conflicting_paths). - Empty source range after a supposedly shipped dispatch halts with
worktree-integration-empty(broken upstream invariant).
Always integrate slices in ascending slice order, regardless of which subagent reconciled first. The plan order is the integration order.
On success:
- Read the integration branch HEAD; this is the new
last_commit_sha. - Update
sidecar.autopilot.last_commit_sha = <new HEAD>atomically. - For each slice, write
phases.implementshipped state viasidecar-set-phasewith the reconciler'scommitsandhead_sha. - Remove each slice's worktree:
Cleanup failures are warnings, not halts (import { remove, removeBranch } from '<plugin>/lib/codex-bridge/worktree.js'; remove(repoRoot, worktreePath); removeBranch(repoRoot, branchName); // only after commits are reachable from integration branchworktree-cleanup-failed/worktree-branch-cleanup-failedrecorded but execution continues). - Advance each slice's
current_phasetoreview-sliceper the main loop (note: only after integration is complete for the whole batch).
On halt (any of the worktree- reasons above):* leave the worktrees and branches in place for inspection. Record the halt summary with absolute worktree paths and branch names. Do not delete diagnostic worktrees.
Phase B.9 — Worked example: slices 3 + 4 in parallel
Slice 3 declares **Implementer:** codex and **Files:** [lib/codex-bridge/foo.js, tests/codex-bridge/foo.test.js]. Slice 4 declares **Implementer:** sonnet and **Files:** [lib/codex-bridge/bar.js, tests/codex-bridge/bar.test.js]. Both are Phase B-ready (Phase A double-SHIP'd). The autopilot has just shipped slice 2's docs-update; HEAD is abc123.
- B.1 checklist. Slice 3 directive valid (
codex). Slice 3 Files block valid (4 paths, no globs/traversal/abs/dups). Slice 4 directive valid (sonnet). Slice 4 Files block valid. - B.2 conflict.
{lib/codex-bridge/foo.js, tests/codex-bridge/foo.test.js}∩{lib/codex-bridge/bar.js, tests/codex-bridge/bar.test.js}= ∅. Form parallel batchparallel-2026-05-08T12:00:00.000Z-3-4. Persistparallel_groupfor both slices viasidecar-set-implement-meta. - B.3 worktree setup.
slice_start_sha = abc123for both. Create.git-worktrees/slice-3(branchslice-3-impl) and.git-worktrees/slice-4(branchslice-4-impl). Bootstrap each with the project's symlinks (node_modulesetc.). Persist bootstrap records. Verify both worktrees withverifyBootstrap. Both Tier 1 and Tier 2 pass. - B.4 dispatch. In a SINGLE assistant turn, emit two
Agenttool calls — one forslice-implementer-codexwith slice 3's prompt, one forslice-implementer-sonnetwith slice 4's prompt. Eachcwdis the slice's worktree path. Wait for both to return. - B.5 reconcile. Call
reconcileWorktreefor each. Slice 3 returnscommit_count: 2, non_conforming_subjects: []. Slice 4 returnscommit_count: 1, non_conforming_subjects: []. No fallback needed. Append dispatch records withoutcome: "shipped"for each. - B.8 integration. Call
integrate({ repoRoot, integrationBranch: <branch>, slices: [{slice-3, slice-3-impl, abc123}, {slice-4, slice-4-impl, abc123}] })in slice-ascending order. Cherry-pick slice 3's two commits onto the integration branch, then slice 4's one commit. Updatelast_commit_sha. Mark each slice'sphases.implementshipped.removeboth worktrees andremoveBranchboth slice-impl branches. Advance both slices toreview-slice(which still runs serially in slice order, per spec §10).
Phase B reference: failure modes (cross-reference spec §17)
| Halt reason | Trigger |
implementer-directive-malformed | Bad/empty/mixed-case/auto Implementer directive |
parallel-files-missing | Parallel candidate has no **Files:** block |
parallel-files-malformed | Parallel candidate has invalid **Files:** block (per B.1 table) |
worktree-gitignore-missing | .git-worktrees/ not in .gitignore |
worktree-path-conflict | .git-worktrees/slice-N exists and isn't a clean same-slice worktree |
worktree-create-failed | git worktree add exited nonzero |
worktree-bootstrap-failed | Bootstrap symlink failure OR sidecar bootstrap marker missing (Tier 1) |
worktree-bootstrap-stale | verifyBootstrap symlink reality check failed (Tier 2) |
worktree-reset-failed | git reset --hard failed before fallback |
codex-blocked / codex-needs-context | Codex implementer reported BLOCKED/NEEDS_CONTEXT |
subagent-blocked / subagent-needs-context | Sonnet implementer reported BLOCKED/NEEDS_CONTEXT |
implementer-unavailable | Both preferred and fallback failed |
worktree-merge-conflict | Cherry-pick conflict during integration |
worktree-resume-ambiguous | Partial / order-broken patch-id match on integration branch |
worktree-integration-empty | Empty source range after supposedly-shipped dispatch (broken invariant) |
Cleanup-only warnings (not halts): worktree-cleanup-failed, worktree-branch-cleanup-failed.
This reconciliation discipline matters because if a Claude session crashes mid-subagent, the subagent may have committed several tasks already. The reconciler-as-truth invariant means the next tick's Phase B.5 call will pick up exactly the commits in the worktree, regardless of what the subagent self-reported. Sidecar dispatch records are append-only so the audit trail survives every retry.
Edit discipline (v0.13.0, Goal 5)
Implementing subagents in Phase B (and any fix-subagent) MUST follow this to avoid wasted retry turns:
- Read before editing when read-state is uncertain (another agent may have touched the file, or your last read predates a failed edit) — re-read in the current turn first.
- On
File has not been read yet: immediately read the file, then recompute the edit. - On
String to replace not found: immediately re-read and inspect the target region; the file changed or theold_stringis stale. - Never retry the same
(file, old_string, new_string)tuple byte-for-byte after it failed — recompute from a fresh read. A byte-identical retry is a procedural error.
Phase C: review-slice
Lane (v0.18.1). Rules: docs/execution-model.md "Review lanes". Autopilot is always standard
lane: both reviewers must approve the same commit, cap 7. There is no lane to recheck here; the
prose lane belongs to the interactive driver.
Deferred findings are batched (v0.18.1). The reviewer classifies every finding as blocking or
deferred (lib/codex-bridge/prompts/verdict-format.md); a deferred item never opens its own round.
Collect them across rounds and clear them in ONE cleanup pass before the work item merges.
- Compute the diff:
git diff <slice_start_sha>..HEAD. Commit-parity preflight (v0.15.0): first rungit status --porcelain— if any slice-touched file has uncommitted changes, commit them BEFORE computing the diff. This applies doubly on re-review after a REVISE fix: two features each burned a full review round because the fix lived in the working tree while the reviewer judged the committed state. - Read Phase A's structured
validation_coveragefrom the sidecar:node ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js sidecar-show --specPath "<spec-path>" \ | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const sc=JSON.parse(d);console.log(JSON.stringify(sc.slice_reviews['slice-<N>'].phases['plan-slice'].validation_coverage,null,2))})"
2.5. Step C0 — Claude's independent review (v0.16.0). Claude reviews first; Codex second; both
SHIP the same commit. Read the diff in full, run the slice's verification yourself and record it as
the claude side's kind: "verification" audit with "reviewed_sha": "<git rev-parse HEAD>".
Write blocking[] / non_blocking[] findings (file:line + one sentence, each verified against
the code). If blocking[] is non-empty, run at most two fix passes with runFixPass
(lib/codex-bridge/fix-pass.js, { specPath, sliceId, repoRoot, pass, execFn }): it persists
fix_start_sha = HEAD on the sidecar before execFn runs; the fix runs in the integration
checkout (the slice worktree is gone after B.8) via the wrapper — --model-role implement on
pass 1, implement_fallback on pass 2 — with fix(slice:<N>): subjects; a failed pass (non-zero
exit, zero commits, non-fix(slice:N): subjects, thrown exec) is reset to fix_start_sha,
never to slice_start_sha (the implementation and other integrated slices stay intact); status
exit_code: 78 is terminal (no reset — halt as in B.6). After each pass: commit, re-run
verification, re-review. Blockers still open after pass 2 go to Codex as listed findings; the
7-round loop arbitrates. Never hold a slice in C0 indefinitely. This is not the B.6 ladder.
3. Compose the prompt: prepend ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/prompts/validation-rubric.md (and verdict-format.md) so Codex applies the rubric to the implementation, not just the plan. Send to Codex via codex-reply on the execution thread (sidecar-thread-id --specPath <spec> --role execution-reviewer — the review role, GPT-6 Astra high by default; or via background subagent if the orchestrator has unrelated prep to do):
Phase: review-slice
Round: <N>
Slice: <slice-N>
Validation tier: <light|standard|critical>
reviewed_sha: <git rev-parse HEAD>
## Claude review findings
blocking: [...]
non_blocking: [...]
## Slice scope
<task list from Phase A>
## Phase A's structured validation coverage (the contract you are verifying)
<JSON from step 2>
## Diff
<diff>
## Test output
<last test run>
Review only what is in this slice's scope. Out-of-slice issues = `## Deferred`. Apply the validation rubric in Phase C mode (rubric.diff-vs-plan / rubric.test-results / rubric.uncovered-paths / rubric.new-triggers). End with verdict.- On Codex REVISE:
a. Apply anti-yes-man discipline: verify each critique against actual code before accepting. If a critique is wrong, push back via the next round; don't act on it.
b. For accepted critiques, dispatch a fix-subagent (foreground) with: the slice's task list, the slice scope, the accepted critiques, and the Commit Conventions. The subagent makes the fixes, runs the tests, and commits using
fix(slice:<N>):subjects (one commit per logical fix). c. After the fix-subagent returns, reconcile sidecar with git per the same rules as Phase B step 3 (walklast_commit_sha..HEAD, verify all conform, updatelast_commit_sha = HEAD). d. Recompute the slice diff (it now includes the fixes) and send to Codex with the next round's prompt. - Log each round with
sidecar-append-round-with-audits --headSha "$(git rev-parse HEAD)"; both sides' verification audits carryreviewed_sha, and the sink refuses a double-SHIP whose SHAs differ or do not equal--headSha(same-commit rule, v0.16.0). Any fix after a Codex verdict starts a new round with fresh verification on both sides. On double-SHIP, write phase state viasidecar-set-phase, advance to Phase D.
Review panel (v0.19.0) for Phases C and D. Before round 1 of a slice review, run the activation
check in skills/brainstorming/codex-pairing.md § "Review panel rounds (v0.19.0)" with --phase review. If configured is false, Phases C and D apply exactly as
written. If configured is true, send step 3's prompt (and Phase D's) to every member as a panel
round (Codex members on threads keyed execution-reviewer:<member_id>), reduce with panel-reduce,
treat the combined blocking findings as step 3's REVISE input, and log with the panel round shape and
one verification audit per member. The same-commit rule applies to every member.
With a configured review panel, Phase D commits before it reviews: step 4's "do NOT commit yet"
and step 6's commit-only-on-double-SHIP apply to the unconfigured path only. Instead, commit each docs
draft as docs(slice:<N>): <summary> before its panel round; every member reviews that commit (V =
git rev-parse HEAD, the Gemini checkout at --sha V), and a REVISE is fixed in a further
docs(slice:<N>): commit that starts the next round. The round that ships is the one whose V is the
final commit, so the approvals attach to exactly what merges.
Phase D: docs-update
- Compute the slice's diff again:
git diff <slice_start_sha>..HEAD. - Ask Codex via
codex-replyon the execution thread (--role execution-reviewer; round 1 prompt):Phase: docs-update Round: 1 Slice: <slice-N> Given this diff, what doc files require updates? - Plan checkbox (always required). - README.md (only if public surface changed: new commands, flags, MCP tools, file structure). - CHANGELOG.md (one-line entry under the in-progress version). - AGENTS.md / CLAUDE.md (only if conventions for agents changed). - Auto-memory in ~/.claude/projects/<project>/memory (only if a non-obvious decision was locked in). ## Diff <diff> List required updates. End with verdict. - Claude drafts the doc changes per Codex's required-updates list AND independently judges whether anything Codex missed should also be updated.
- Apply the doc edits to the working tree but do NOT commit yet. Send the uncommitted draft to Codex for review:
Phase: docs-update Round: <N+1> ## Working-tree diff (uncommitted docs draft) <git diff -- README.md CHANGELOG.md docs/plans/...md AGENTS.md CLAUDE.md (only files touched)> Are these accurate? Complete? Are they referencing files/symbols that don't exist? Anything missing? End with verdict. - 7-round loop. On Codex REVISE, edit the working tree (still no commit) and send the next round.
- Only on double-SHIP: commit the docs as a single commit with
docs(slice:<N>): <summary>subject +Co-Authored-By: Claudetrailer. Then reconcile sidecar (last_commit_sha = HEAD), mark phase shipped viasidecar-set-phase, and advance the autopilot to next-slice or all-done.
This deferred-commit pattern matters: if Codex finds doc errors across 3 rounds, we end up with ONE clean docs commit, not 3 commit-then-fix-it commits cluttering history.
- Refresh the test-impact map (if using
test:affected). Once the slice is fully shipped, re-map the tests it touched so the coverage map reflects this slice's changes for later slices — without a full rebuild:
This keeps drift from accumulating across an unattended multi-slice run.node ${CLAUDE_PLUGIN_ROOT}/scripts/tia.mjs refresh --base <slice_start_sha>
Phase E: live-verification
Phase E runs after docs-update.shipped == true and before shipped. It launches the real app, drives Codex-generated user-visible scenarios through /computer-use, captures logs and screenshots, and fixes verified failures. A slice cannot reach shipped unless Phase E either ships (double-SHIP'd evidence verdict) or is validly skipped.
The phase sequence is now:
plan-slice -> implement -> review-slice -> docs-update -> live-verification -> shippedWhen current_phase is live-verification, execute the following 9 sub-phases in order.
E.1 — Skip-frontmatter check
Pipe the slice's section markdown to the skip-frontmatter CLI:
echo '<slice section markdown>' | \
node ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js parse-skip-frontmatterDispatch on output:
- Exit 0,
{skip: true, reason}→ writephases.live-verification.skipped = trueandphases.live-verification.skip_reason = <reason>to the sidecar viasidecar-set-live-verification, then advancecurrent_phasetoshipped. Do NOT run sub-phases E.2–E.9. - Exit 0,
{skip: false}→ continue to E.2. - Exit 2,
{defect, ...}→ halt withhalt_reason: "live-verification-skip-malformed".
E.2 — Pure-library project check
Load the project config:
# In orchestrator JS context:
const result = loadProjectConfig(repoRoot);Dispatch:
- If
resultis null and the slice is behavior-changing → halt withhalt_reason: "live-verification-config-missing". - If
result.ok === false→ halt withhalt_reason: "live-verification-config-malformed". - If
result.config.app.type === "library"ANDresult.config.live_verification.default === "skip"→ writephases.live-verification.skipped = true,phases.live-verification.skip_reason = <result.config.live_verification.skip_reason>viasidecar-set-live-verification, advance toshipped. Do NOT run sub-phases E.3–E.9. - Otherwise continue to E.3.
E.3 — Safety gate
Call the pure safety-gate evaluator:
const outcome = evaluateSafetyGate(config, new Date());Dispatch on outcome.status:
'ok'→ continue to E.4. (Current time is inside a configured scheduled window.)'requires-confirmation'→ displayoutcome.promptTextdirectly in the active Claude Code session:
Read the user's response from the conversation:Phase E live verification is about to take screen control via /computer-use for ~<estimate>. It may move the mouse, click, type, and switch focus. Continue now?- "continue" (or any affirmative) → proceed to E.4.
- "cancel", "postpone", or any negative → write
halt_reason: "live-verification-user-postponed"to the sidecar viasidecar-set-autopilot, clear the active anchor (anchor-clear), and halt. Do NOT launch the app.
'halt'→ halt withhalt_reason: outcome.haltReason.
A prior confirmation from a previous Claude Code session does NOT carry over. Every /autopilot resume that reaches Phase E must re-evaluate the safety gate in that session.
E.4 — Scenario generation
Build the scenario-generation prompt by reading ${CLAUDE_PLUGIN_ROOT}/skills/autopilot/scenario-gen-prompt.md and substituting:
| Placeholder | Source |
<round> | Current round number (1 on first entry) |
<slice-N> | Current slice id |
<validation-tier> | Tier from Phase A |
<project-app-name> | config.app.name |
<project-app-description> | config.app.description |
<project-app-type> | config.app.type |
<phase-A-task-list> | Phase A task list (from sidecar or re-parsed) |
<phase-A-validation-coverage> | slice_reviews[slice-N].phases.plan-slice.validation_coverage |
<plan-frontmatter-ui-surface> | UI surface from plan frontmatter if declared |
<slice-diff> | git diff <slice_start_sha>..HEAD |
<relevant-ui-files-or-paths> | config.live_verification.evidence.ui_globs expansion |
Send the prompt via mcp__plugin_codex-paired-superpowers_codex__codex-reply on the feature's threadId (Codex role) — or, if model-role --role review --format json reports "cli":"agy", pipe the same prompt to node "${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js" reviewer-thread-reply --role review --specPath "<spec-path>" --repoRoot "$REPO_ROOT" --prompt-stdin instead of the MCP codex-reply tool; its stdout is the same { threadId, content } shape.
Pipe Codex's content to:
echo '<codex content>' | \
node ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js scenario-validate --require-scenariosDispatch on exit code:
- Exit 2 → increment round counter. If round ≤ 7, compose a REVISE prompt (include the defect detail) and send to Codex; retry scenario-validate on the next response. After round 7 without exit-0, halt with
halt_reason: "live-verification-scenario-deadlock". - Exit 0 → persist the scenario list to the sidecar:
Then write each scenario to the evidence dir via the evidence store. Continue to E.5.node ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js sidecar-set-live-verification \ --specPath <spec-path> --sliceId <slice-N> \ --block '{"scenario_generation":{"shipped":true,"scenario_count":<N>}}'
E.5 — App launch
Call the app launcher:
const handle = await launchApp(config.live_verification);On rejection → capture any available logs, run cleanup (kill), halt with halt_reason: "live-verification-launch-failure".
On success, write launch metadata to the sidecar:
node ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js sidecar-set-live-verification \
--specPath <spec-path> --sliceId <slice-N> \
--block '{"launch":{"ready":true,"ready_signal":"<ready_signal_description>"}}'E.6 — For each scenario: precondition + execution + flake-retry
Claude is the /computer-use adapter at this layer. The scenario runner injects an adapter object; in autopilot execution Claude Code provides the real implementation:
| Adapter method | Claude's runtime action |
executeStep(step, ctx) | Drive /computer-use per the step's action and target (see below) |
captureScreenshot(path) | Use Claude Code's native screenshot facility; save to path |
openRoute(url) | Navigate the browser to the given URL via /computer-use |
getHeadSha() | Run git rev-parse HEAD |
now() | Return new Date() |
Scenario driver instruction block — how to execute steps via /computer-use:
Steps use human-readable target descriptions, not DOM selectors. Examples:
{action: "click", target: "Save Display Name button"}→ use/computer-useto find and click the button labeled "Save Display Name" as it appears on screen./computer-usereasons over visible UI (pixel coordinates, rendered text, layout), not over DOM structure or CSS class names.{action: "type", target: "Display name input", value: "Avery"}→ click the field that accepts display name text, then type "Avery".{action: "navigate", target: "http://127.0.0.1:3000/settings"}→ navigate to that URL.{action: "wait_for", target: "Success message is visible"}→ wait until the described element or state appears on screen, up totimeout_ms.{action: "assert", target: "No error toast appears"}→ verify the stated condition using a screenshot.
If an action fails (element not found, click missed, etc.):
- Retry the action up to
config.live_verification.computer_use.max_action_retriestimes. - If still failing after retries, record
{ok: false, error: "<description>"}and mark the scenario step failed.
For each scenario, use createPreconditionEnforcer and createScenarioRunner (via the injected adapter), wrapped in createFlakeChecker:
const enforcer = createPreconditionEnforcer({ adapter, spawn, now });
const runner = createScenarioRunner({
adapter, // Claude is the adapter
evidenceStore,
preconditionEnforcer: enforcer,
logTailer,
projectConfig: config,
});
const checker = createFlakeChecker({
runner,
evidenceStore,
sliceId,
maxFlakes: 2,
});
const result = await checker.runWithFlakeRetry(scenario);Capture evidence for each scenario attempt:
- Record current
git rev-parse HEAD(getHeadSha). - Enforce preconditions (reset → seed → navigate → login → setup_steps in declared order).
- Capture
before.pngviacaptureScreenshot. - Execute each step via
/computer-use. - Capture
after.png. - Capture log excerpt from the log tailer.
- Evaluate assertions (screenshot + logs).
- Write
result.jsonto the evidence path.
Flake-retry logic: if a scenario fails, do NOT modify files. Re-run the same scenario once at the same HEAD (re-enforcing preconditions from scratch). If it passes on retry → mark flaky. If it fails again with materially the same evidence → treat as deterministic failure and enter the fix loop (E.7). Two flake results across separate scenarios → halt with halt_reason: "live-verification-flaky-runner".
Persist per-scenario results via sidecar-set-live-verification as they complete:
node ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js sidecar-set-live-verification \
--specPath <spec-path> --sliceId <slice-N> \
--block '{"scenarios":{"<scenario-id>":{"status":"passed|failed|flaky|blocked-precondition"}}}'E.7 — Fix loop (on deterministic failure)
On any deterministic scenario failure, instantiate the fix loop:
const fixLoop = createLiveFixLoop({
specPath,
sliceId,
config,
evidenceStore,
adapter, // Claude is the adapter
// v0.17.0: when `model-role --role review --format json` says cli:'agy', codexReply must instead shell out to
// node cli.js reviewer-thread-reply --role review --specPath <spec> --repoRoot <repo> --prompt-stdin
codexReply: (prompt) => mcp__plugin_codex-paired-superpowers_codex__codex-reply({ threadId, content: prompt }),
dispatchFixSubagent: (prompt) => /* dispatch foreground subagent with prompt */,
runScenarios: (scenarios) => /* re-run all scenarios per E.6 */,
reconcile: () => /* walk last_commit_sha..HEAD; verify conventions; update last_commit_sha */,
sidecar: { setLiveVerification, getAutopilot, setAutopilot },
now: () => new Date(),
});
const outcome = await fixLoop.runFixLoop(sliceId, scenarios, initialFailures);The fix-loop (implemented in lib/codex-bridge/live-fix-loop.js) handles:
- Package failure context: failing scenario(s), screenshot paths, log excerpts,
git diff <slice_start_sha>..HEAD, git status. - Send to Codex for diagnosis via
codex-reply. - Claude verifies the diagnosis against actual code (anti-yes-man discipline applies here).
- Dispatch a fix-subagent (foreground) with the diagnosis and Commit Conventions. Fix commits use
fix(slice:<N>): live-verification fix - <scenario-id> <short name>. - Git reconciliation: walk
last_commit_sha..HEAD, verify all commits conform tofix(slice:<N>):+Co-Authored-By: Claudetrailer, updatelast_commit_sha = HEAD. - Re-run ALL scenarios for this slice (not just the failing ones). This is intentionally conservative — any Phase E fix commit could affect any scenario.
- Round budget: max 7. After round 7 without
{outcome: 'shipped'}, halt withhalt_reason: "live-verification-deadlock".
Dispatch on fix-loop outcome:
{outcome: 'shipped'}→ continue to E.8 (evidence verdict).{outcome: 'deadlock'}→ halt withhalt_reason: "live-verification-deadlock".
E.8 — Evidence verdict
Compile the evidence bundle:
- Scenario list (from sidecar or evidence dir
scenarios.json). - Pass/fail/flake matrix across all scenarios.
- Screenshots (
before.png,after.pngper attempt). - Log excerpts.
- Phase E fix commits (from
last_commit_sharange walk since phase_start_sha). - Final app readiness metadata from launch.json.
- Deferred scenarios.
- Cleanup status (preliminary).
Send to Codex via codex-reply for evidence review. The prompt should include the evidence bundle summary and request a verdict using the 13 Phase E live.* keys.
Pipe Codex's response to:
echo '<JSON array of critique bullets>' | \
node ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js live-validation-parse \
--tier <slice-tier>Dispatch on exit code:
- Exit 0 → parse the
{tier, coverage}output; write toslice_reviews[slice-N].phases.live-verification.validation_coverage. Both Claude and Codex must SHIP the same round. On double-SHIP:
Then advancenode ${CLAUDE_PLUGIN_ROOT}/lib/codex-bridge/cli.js sidecar-set-live-verification \ --specPath <spec-path> --sliceId <slice-N> \ --block '{"shipped":true,"validation_coverage":<coverage>}'current_phasetoshipped. - Exit 2 → REVISE round. Iterate up to 7 rounds total (shared with the fix-loop budget in Phase E). If round 7 passes without double-SHIP, halt with
halt_reason: "live-verification-deadlock". - Exit 1 → CLI infrastructure failure. Halt with
halt_reason: "cli-infrastructure-error".
E.9 — Cleanup
Call cleanup per project config:
await cleanup(handle, config.live_verification.cleanup);on_success: "kill"(default) → send SIGTERM, waitshutdown.grace_ms, SIGKILL if still running.on_success: "leave_running"→ do NOT kill the process. Write the following metadata to the sidecar's live-verification block so the halt summary surfaces it:
The halt summary MUST include this information. Autopilot must never leave a process running invisibly.{ "leave_running": { "pid": <pid>, "command": "<launch command>", "port_or_url": "<start URL>", "suggested_cleanup": "kill <pid>" } }
On cleanup success, Phase E is complete. The sidecar already has phases.live-verification.shipped = true from E.8. The main loop advances current_phase from live-verification to shipped (which triggers the existing shipped handler: mark slice_reviews[slice-N].shipped = true, advance to next slice or all_done).
Halt-mode cleanup: if any sub-phase E.1–E.8 triggers a halt, cleanup runs according to config.live_verification.cleanup.on_halt (defaults to "kill"). Evidence write failure always uses kill regardless of config, because the run is not inspectable through persisted evidence.
Non-blocking Codex (UI sense, not concurrency sense)
- Background subagent calls let the orchestrator continue prep work while Codex thinks.
- BUT: only ONE codex-reply may be in flight against the feature's threadId at any time. Single-writer mutex enforced by the orchestrator.
- See
skills/autopilot/codex-via-subagent-prompt.mdfor the subagent prompt template.
Stall watchdog for anything Codex-shaped (v0.15.0)
Transcript replay found two hangs (25min, 3h24m) where the orchestrator dispatched codex in the background and never noticed the stall — both were detected by the user. The rules:
- Every wait has a deadline. When awaiting a Codex MCP call, a codex subagent, or a background
codex task, fix a deadline up front: 15 minutes for a review round (MCP p99 is ~7min), or
codex_dispatch.max_runtime_msfor implementer dispatches. While waiting across turns, check elapsed time EVERY turn (ps -o etime= -p <pid>for process-backed work, dispatch timestamps otherwise). - On deadline: treat it as a stall, not patience. Kill the process if there is one (SIGTERM, then SIGKILL after 5s), record what happened (sidecar contention or halt), then retry ONCE with a fresh dispatch. A second stall is a terminal halt — surface to the user, do not silently re-dispatch again.
- Never suppress the evidence. No
2>/dev/nullon codex dispatches, no piping output throughtailso that nothing streams. A silent auth prompt on stderr was the leading suspect for the 3h24m hang. - Preflight before long dispatch chains.
detectAvailableCLIsnow runs an auth probe for codex (codex login status); anunauthenticatedresult means every one-shot exec WILL park on a login prompt — halt with the doctor remedy instead of dispatching.
Empty-reply retry (v0.15.0)
Codex session logs show a swallowed-API-failure mode: codex/codex-reply returns a completed
turn ~1 second after the prompt with an EMPTY message and unchanged token usage (no error event;
rate limits healthy). Detection: the MCP result text is empty/whitespace. Response: treat it as
transient — re-send the SAME prompt once after ~30s; if still empty, wait ~5 minutes and re-send
once more (observed outage windows were ≤10min). Three consecutive empties → halt
codex-empty-replies and surface to the user. Do NOT log a round for an empty reply, and do NOT
interpret it as a verdict.
Failure modes
See Spec § "Failure modes" — implement every row of that table. Each halt sets autopilot.halt_reason to a specific string the user can search for, and prints a human-readable summary.
Anti-yes-man discipline
Same as upstream codex-paired-superpowers:receiving-code-review. Never accept a Codex critique without verifying against actual code. Never accept a SHIP without applying the pre-SHIP checklist (which is now in system-rubric.md and Codex sees it on every prompt).
Cross-session continuity (self-continuing)
Autopilot needs no external loop wrapper. All progress lives in the sidecar's autopilot block, so
re-running /autopilot is the resume mechanism:
/autopilot # resume the in-progress run (locate it from the sidecar)
/autopilot <plan-path> # start or resume a specific planOn every entry, autopilot resolves the spec via the plan's frontmatter and reads the sidecar's
autopilot block to determine state (the active anchor is the HOOK's discovery mechanism, not the
autopilot's). A run is complete when sidecar.autopilot.halt_reason == "completed". Within a session
(default mode) autopilot loops over slices until completion, a real halt, or the session ends; the
next /autopilot invocation — in this session or a brand-new one — picks up where it left off.
Halt-aware resume contract (v0.9.0)
When autopilot halts, it persists autopilot.halt_reason to the sidecar (see § "On halt") and
surfaces the resume hint to the user. On the NEXT /autopilot invocation, BEFORE doing any work,
autopilot reads that persisted halt_reason, derives the envelope from it via
wrapAsHaltEnvelope(halt_reason, { sliceId, phase }) (lib/codex-bridge/halt-envelope.js) — the same
module the halt path uses — and applies this contract with isTerminalHalt(envelope) (the
load-bearing guard):
halt_reason = sidecar.autopilot.halt_reason # the persisted source of truth
if halt_reason == "completed": report success and stop # the run is done
if halt_reason == null: normal in-session resume (no halt to classify)
envelope = wrapAsHaltEnvelope(halt_reason, { sliceId, phase }) # derive, don't store
if envelope?.halt == "completed":
report success and stop # the run is done
# isTerminalHalt fails closed on malformed envelopes: null, undefined,
# non-object, missing/non-boolean terminal, missing/empty resume_hint, etc.
# all return true. Only a well-formed { terminal: false, halt, resume_hint }
# envelope returns false.
if isTerminalHalt(envelope):
# Operator must act first — do NOT auto-continue.
hint = envelope?.resume_hint ?? "Autopilot returned a malformed halt envelope — operator triage required."
surface(hint) # tell the user exactly what to fix
stop # they re-run /autopilot after fixing the cause
# Only here on a well-formed transient envelope — safe to resume:
clear the transient halt and continue the current phaseKey invariants (applied by autopilot itself on resume — and by /loop or the user if driving it):
- Never auto-continue past
terminal: true. Theresume_hinttells the user exactly what to fix; auto-continuing would loop indefinitely and mask the real blocker. The user re-runs/autopilotafter addressing it. - Always surface
resume_hinton a terminal halt. It's the human-readable description used to diagnose and resolve the blocker. - Unknown halt reasons are always terminal (the envelope module maps them to
terminal: truewith an "operator triage" hint). Treat absence from the known set as a hard stop. - Transient halts resume at most once per invocation. Don't implement exponential retry in the resume path — that belongs in the halting code (e.g.,
transient-networkis retry-eligible, then escalates). - Malformed or absent envelopes are terminal (fail-closed). A null/undefined/non-object envelope, or one missing
terminal/resume_hint/halt, is treated as terminal with a generic "operator triage required" hint.isTerminalHaltenforces this (per slice-7b Codex round-1 finding #3): it returnstruefor nullish values, non-objects, non-booleanterminal, and missing/emptyhaltorresume_hint.
Timer-driven repetition (optional). If you want autopilot to fire on a schedule without manually
re-running it, drive it with the built-in /loop skill — e.g. /loop /autopilot. /loop applies the
same resume contract above: it stops on a terminal halt and surfaces the hint, and continues on a
transient one. This is optional polish; the default model is one self-resuming command.
Phase B implementer-experts branch
Canonical split reader. The Phase B decision point reads the canonical **Split:** directive via
normalizeSplit (Plan 1's lib/codex-bridge/execution/split-dispatcher.js). When a **Split:**
directive is absent, the existing legacy inference is unchanged and still authoritative:
- no
**Split:**and no implementer/orchestration block → single (the single-implementer path); - legacy
**Implementers:**block (including N>2 sets under the existing parser's cost and cap rules) → two-disjoint viadispatchImplementers; - legacy
**Orchestration:** hybrid→ hybrid viarunHybridSlice(autopilot mode).
Reading **Split:** does not change any of these routing targets or the N>2 cap behavior; it only
makes the directive canonical when present.
If the slice's plan frontmatter contains an **Implementers:** block (parsed by
slice-1's parseImplementersBlock), dispatch via dispatchImplementers from
lib/codex-bridge/implementer/orchestrator.js instead of the single-implementer
path. The orchestrator handles:
- worktree fan-out per member
- shared abortSignal for cancellation
- sidecar
started/checkpoint/ terminal events - runtime_kind translation (claude → claude-cli)
- (v0.16.0) resolving the
implementmodel role once per attempt forcodex-climembers (modelRole/model/efforton the dispatch input and thestartedpayload; Claude members keep their ownmodel:); passrepoRootso launch and resume find the same attempt evidence.
The production dispatchFn for codex-cli members is dispatchCodexCliImplementer from
lib/codex-bridge/implementer/codex-cli-dispatch.js: it requires the complete snapshot (throws before
spawning otherwise), writes attempt evidence at .codex-paired/attempts/<run>/<member>.json
(launching → running with the child pid → exited), honors the shared abort signal, and returns
the full result (haltEnvelope beats exit 0; modelSnapshot attached). For claude-cli members keep
the existing Task/claude-cli dispatch.
Resume (v0.16.0). On a session resume with an in-progress run, call
dispatchImplementers({ ..., implementerRunId, resumeInFlight: true }): members whose latest
lifecycle event is completed are reused (no launch); members still started/checkpoint are
observed with observeDirectCliAttempt (never relaunched — an observation timeout is recorded as
a checkpoint, not a terminal event, and the attempt stays in flight); only members whose evidence
shows no live attempt get a new attempt at the current configuration. Never launch a second writer
into a member's worktree.
After all implementers complete:
mergeImplementerBranches(slice 7) integrates branches in member-ID order- On
merge-conflict:runMergerAgent(slice 8) resolves with double-SHIP gate runPostMergeReview(slice 9) runs the final post-merge-review paired review
Phase B hybrid branch (v0.14.0)
As noted in the implementer-experts branch, the canonical split reader is **Split:** via
normalizeSplit; absent a **Split:** directive, a legacy **Orchestration:** hybrid block still
routes here unchanged.
Some slices declare **Orchestration:** hybrid with exactly two owners — a claude-ui half and a codex-backend half (see writing-plans for the syntax). These do not go through the symmetric dispatchImplementers path above. That path treats every member the same way; a hybrid slice is asymmetric — one half builds the UI and the other builds the backend and publishes a contract the UI depends on. So when a slice carries **Orchestration:** hybrid, route it to runHybridSlice from lib/codex-bridge/hybrid/runner.js instead, in autopilot mode.
What runHybridSlice does, in order:
- Preflight + worktrees. Validates the two-owner block (ownership and claimed-file partition), confirms the registry offers both transports, and creates a worktree per owner from the slice-start SHA. In autopilot both halves run in their own worktrees (unlike interactive, where the UI half edits the foreground checkout).
- Concurrent dispatch. Starts both owners at the same time: the UI half as a Claude subagent (runtime kind
claude-subagent) and the backend half as a background Codex run (transportcodex-background-bash, launched by YOUR dispatch fn throughscripts/codex-exec-with-status.sh ... --model-role implement -- codex exec ... --add-dir <repo>/.git— v0.16.0). They run in parallel, not one after the other. Your backend dispatch fn must (a) callinput.onLaunched({ model_role, model, effort, status_file })as soon as the wrapper's initial status file exists (the runner records acheckpointlaunchedevent so the effective model is durable even if this session dies), (b) poll the status file withclassifyStatusFile(aconfig-error— exit 78 — halts with the file's reason), and (c) returnmodelSnapshotread from the final status file. - Contract wait. The UI half builds against its local
__hybrid_contracts__stand-in while the backend half writes the real route/types and publishes the contract. The runner waits for the backend to publish; if the backend exits without publishing, it halts (hybrid-contract-not-published). - Contract-change resync. If the backend publishes a newer contract hash after the UI half already consumed an earlier one, the runner marks the UI owner
needs-contract-resync, surfaces the changed-contract state (sidecar + mailbox), and refuses to let the UI half complete or integrate until it consumes the latest hash. This is the in-progresshybrid-contract-changedstate — not terminal while the UI owner is still working. It only becomes terminal if the UI half completes while still stale:hybrid-contract-stale-at-completion(consumed an older hash) orhybrid-contract-not-consumed(never consumed the latest). - Background-Codex recovery. The background run is monitored by its status file. A lost or vanished run surfaces as
hybrid-codex-background-lost(and a stalled one ashybrid-codex-background-timeout); these route through the existing halt-envelope path and the per-phase Codex thread-loss recovery already documented for Phase B (detect the stale-thread response, replay, rotate the thread id). - Claimed-file verification. Before integrating, each owner's diff is checked against its claimed files. A change outside an owner's claim halts (
implementer-claimed-file-violation), unless the plan rationalized the overlap. - Integrate + realize the contract. Both owner branches are integrated through the existing ordered cherry-pick path, the UI stand-in is swapped for the real backend contract, and a final typecheck runs. A real backend export that does not match the consumed stand-in hash halts (
hybrid-contract-realization-mismatch).
Every halt above flows through the same halt-envelope surface as the rest of Phase B, so resume/skip behavior is unchanged.
Troubleshooting setup errors
If autopilot fails to start, or any phase fails with errors mentioning Cannot find module, proper-lockfile, codex: command not found, codex not authenticated, ENOENT, or any module-load / binary-not-found pattern, invoke /codex-paired-superpowers:doctor first. The doctor diagnoses the install and prints the exact commands to fix each issue. Resume autopilot after the doctor reports all checks green.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

