Autopilot
Orchestrate the full plan-review-implement-validate-PR lifecycle with multi-vendor review convergence. For simple features, runs fully automatically from proposal to PR. Stops at merge for human approval.
Arguments
<change-id or description> - Either an existing OpenSpec change-id or a feature description in quotes.
Optional flags:
--force— Skip the GATEKEEPER judge entirely (operator override of the verifiability/risk gate). Also bypasses the scope-safety floor.--val-review— Force VAL_REVIEW phase even for simple features--no-review— Skip multi-vendor review phases (PLAN_REVIEW, IMPL_REVIEW); iterate phases still run
Prerequisites
- OpenSpec CLI installed (v1.0+)
- At least 2 vendor CLIs available (claude, codex, antigravity, grok, pi) for multi-vendor convergence
- Coordinator recommended (degrades to linear workflow without it)
Coordinator Capability Check
At skill start, run the coordinator detection script:
python3 "<skill-base-dir>/../coordination-bridge/scripts/check_coordinator.py" --json
If coordinator is unavailable, emit a warning and fall back to sequential skill invocation:
/plan-feature(if description provided)/iterate-on-plan(always runs — self-review)/parallel-review-plan(CLI only — single pass, no convergence loop)/implement-feature/iterate-on-implementation(always runs — self-review)/parallel-review-implementation(CLI only — single pass, no convergence loop)/validate-feature- Create PR manually
Steps
0. Parse Arguments and Check for Resume
Parse the argument to determine:
- If it matches an existing change-id in
openspec/changes/: load that change - Otherwise: treat as a feature description for the PLAN phase
Check for existing loop state:
LOOP_STATE="openspec/changes/<change-id>/loop-state.json"
if [ -f "$LOOP_STATE" ]; then
# Resume from saved state — report current phase and offer to continue
fi
If loop-state.json exists and current_phase == "ESCALATE", report the escalation
reason and blocking findings, then run the gate protocol. The resume decision is a
recorded ApprovalDecision, never an inference from the conversation:
python3 "<skill-base-dir>/scripts/runner.py" gate-check <change-id> \
--gate escalate_resume \
--context escalation_reason="<escalation_reason from loop-state.json>" \
--context previous_phase="<previous_phase from loop-state.json>"
# exit 0 → ask the operator the printed `prompt` verbatim (AskUserQuestion), then:
python3 "<skill-base-dir>/scripts/runner.py" gate-answer <change-id> \
--gate escalate_resume --decision <approved|rejected> [--note "<operator note>"]
# exit 3 → nothing to ask; continue
# exit 4 → parked (see stderr for the recorded reason); stop the run here
gate-check --gate evaluates the gate against TRUST_POSTURE.md and records
the decision before returning: exit 3 means the posture authorized the resume and
the run continues from previous_phase; exit 0 means a person has to answer first;
exit 4 means the gate was blocked in a way no console answer resolves (a rejection
already recorded, a timeout that defaulted to block, or an unreachable coordinator)
and the run stays parked in ESCALATE. --decision approved records the resume
authorization; --decision rejected leaves it parked with the note as the reason.
The loop cannot be advanced around this: apply-outcome refuses to record anything
while a gate is pending.
1. INIT Phase
Detect CLI mode — check whether multi-vendor review is available:
# CLI mode: vendor CLIs are available for multi-vendor review dispatch
CLI_REVIEW_ENABLED=true
if [[ "$ARGUMENTS" == *"--no-review"* ]]; then
CLI_REVIEW_ENABLED=false
fi
# Also disable if fewer than 2 vendors are dispatchable (non-interactive/cloud
# environment). --check-vendors exits 0 at quorum, 2 below it.
#
# Run it BARE — do not pipe. A pipeline's $? is the LAST stage's status, so
# `... --check-vendors | tail` would report tail's 0 even when the probe fails,
# silently enabling review with no vendors behind it.
if ! python3 "<skill-base-dir>/../parallel-infrastructure/scripts/review_dispatcher.py" \
--check-vendors --min-vendors 2; then
CLI_REVIEW_ENABLED=false
echo "[autopilot] Fewer than 2 vendor CLIs detected — multi-vendor review disabled"
fi
Pass cli_review_enabled to run_loop(). When False, PLAN_ITERATE and IMPL_ITERATE still run (self-review is always valuable), but PLAN_REVIEW and IMPL_REVIEW are skipped.
After each review round, check the round's review-manifest.json for vendors
with error_class of vendor_unavailable or auth_required. Drop those
vendors from subsequent rounds instead of re-dispatching them — the failure is
account-scoped (billing, credentials), so a stricter-format re-prompt cannot
fix it and only burns a dispatch per round (issue #383). Record the drop in
vendor_availability so convergence accounting stays honest.
Run the entry gate:
from complexity_gate import assess_complexity
result = assess_complexity(work_packages_path, proposal_path, force=<--force flag>)
The gate no longer blocks on size. It does two cheap, deterministic things and
records the result in loop-state.json:
- Gather the signal profile —
result.signalsis a risk + verifiability profile (package count, LOC estimate, external deps, db/security flags, write scope, and verifiability factshas_specs/has_tasks/has_proposal). Persist it tostate.gate_signals; the GATEKEEPER judge consumes it. - Scope-safety floor — the ONLY remaining hard block. A package that can
write the whole repository (
**,*,.) defeats worktree isolation, soresult.force_requiredis set; without--force,result.allowed == False→ report warnings and stop (suggest--force).
Then:
- If
result.val_review_enabled(db-migration / security signals): record it. - If
result.warnings: report them but continue. - If
result.checkpoints: log injected scheduling checkpoints (wave-validation,limit-concurrency,dependency-review,db-migration-review,security-review).
Gate policy:
- LOC, package count, and external-dependency counts are signals, not blocks. Large but well-decomposed work is fine; high counts only emit scheduling checkpoints so the DAG paces itself.
- Database-migration and security signals (
auth,crypto,secret,token) enable validation review without blocking automation. - The only deterministic hard block is a broad repository write scope, which
still requires
--force. - Everything else — can outcomes be verified? is the risk acceptable? — is delegated to the GATEKEEPER judge sub-agent (Step 1.5), which weighs the signals against autopilot's downstream safeguards (review convergence, validation, and the mandatory human merge gate).
Record INIT phase archetype (state-only resolver — D9). After loop-state.json
is written, shell out to record phase_archetype for INIT so observability
covers all 14 non-terminal phases, not just the 8 dispatching phases:
python3 "<skill-base-dir>/scripts/runner.py" record-state-only-archetype \
--change-id <change-id> --phase INIT
Failure here is non-fatal — the helper logs a warning and writes
phase_archetype = null if the coordinator is unreachable.
1.5. GATEKEEPER Phase (Judge)
Replaces deterministic complexity blocking with a model-based judgment of
whether the change can run autonomously. Skipped entirely when --force is
set (operator override → transitions straight to PLAN).
The GATEKEEPER judge sub-agent reads state.gate_signals plus the available
plan artifacts and evaluates two things — NOT raw size:
- Verifiability — can the intended outcomes be objectively checked? WHEN/THEN specs, a task breakdown, and testable acceptance criteria make outcomes verifiable; a bare description does not.
- Risk — blast radius and reversibility if a slice goes wrong (db migrations, security surfaces, external deps, write scope).
The judge explicitly accounts for autopilot's downstream safeguards (multi-vendor PLAN/IMPL review convergence, the VALIDATE phase, and a mandatory human merge gate) and biases toward letting verifiable work proceed — large but well-specified changes are acceptable.
Dispatch protocol (3 steps — same provider-neutral path as other phases; read
-only, so isolation is not worktree):
- Build kwargs:
python3 "<skill-base-dir>/scripts/runner.py" build-dispatch \ --phase GATEKEEPER --change-id <change-id> - Call the dispatch adapter with
prompt/model(treatpromptas opaque). Parse the agent's last message for(outcome, handoff_id). Outcome is one ofproceed,proceed_with_review, orescalate. - Apply the outcome:
python3 "<skill-base-dir>/scripts/runner.py" apply-outcome \ --change-id <change-id> --phase GATEKEEPER \ --outcome <outcome> --handoff-id <handoff_id>
Fallback (permissive, and DEGRADED): If build-dispatch returns
archetype: null OR no dispatch adapter is available (headless CI, coordinator
down), do NOT block — derive a permissive verdict from state.gate_signals:
proceed_with_review when any risk signal (db migration, security, broad
scope) is present, otherwise proceed. Record phase_archetype = null via
apply-outcome.
This path fails open, so it MUST be reported as such. _phase_gatekeeper
appends a DEGRADED entry to state.phase_history (and echoes it to stderr)
naming what was not checked and why — the risk/verifiability judgment did not
happen; a signal-only verdict stood in for it. When the run writes a
validation-report.md, carry that forward as a DEGRADED phase status with the
same one-line reason: a gate that could not run is not a gate that passed.
If proceed: transition to PLAN.
If proceed_with_review: set state.val_review_enabled = true, transition to PLAN.
If escalate: transition to ESCALATE (the change is judged unverifiable or
too risky for autonomous execution; resolve and resume, or re-run with --force).
2. PLAN Phase
Record PLAN phase archetype (state-only resolver — D9 / VAL_REVIEW G-V-001).
PLAN dispatches via the /plan-feature slash command rather than the
provider-neutral dispatch adapter, so it doesn't go through runner.py build-dispatch. To
keep observability uniform across all 14 non-terminal phases, shell out:
python3 "<skill-base-dir>/scripts/runner.py" record-state-only-archetype \
--change-id <change-id> --phase PLAN
Failure here is non-fatal — the helper logs a warning and writes
phase_archetype = null if the coordinator is unreachable.
If argument was a description (no existing change-id):
- Invoke
/plan-feature <description>(tier auto-detected based on coordinator availability) - Before showing/answering the proposal approval gate prompt, best-effort invoke
/review-artifacts <change-id>to open proposal/design/spec/tasks artifacts in a new VS Code review session. The helper is local-only (depends on the VS Code CLI / a desktop environment) and will be unavailable in cloud harnesses — treat its failure as non-fatal: log a short notice ([autopilot] /review-artifacts unavailable — skipping artifact pre-open) and continue to the approval gate.
If argument was an existing change-id:
- Verify proposal artifacts exist (proposal.md, design.md, specs/, tasks.md)
- Skip to PLAN_REVIEW
Either way, run the gate protocol before acting on the proposal. The loop
evaluates the proposal-approval gate on both PLAN outcomes (created and exists),
so a pre-existing change is approved on this run's terms, not on a previous run's:
python3 "<skill-base-dir>/scripts/runner.py" gate-check <change-id> \
--gate proposal_approval \
--context proposal_path="openspec/changes/<change-id>/proposal.md" \
--context approach="<created|exists>"
# exit 0 → ask the operator the printed `prompt` verbatim (AskUserQuestion), then:
python3 "<skill-base-dir>/scripts/runner.py" gate-answer <change-id> \
--gate proposal_approval --decision <approved|rejected> [--note "<operator note>"]
# exit 3 → nothing to ask; continue
# exit 4 → parked (see stderr for the recorded reason); stop the run here
--decision approved records the approval and the loop moves on to PLAN_ITERATE;
--decision rejected enters ESCALATE naming the gate and the note. Whether the gate
parks at all is the trust posture's call (TRUST_POSTURE.md) — under an auto
disposition gate-check records the decision, exits 3, and there is nothing to ask.
An exit of 4 is not a "continue": the decision was blocked in a way no console answer
resolves, the run is in ESCALATE, and this run stops.
Per-Phase Sub-Agent Dispatch Protocol
Authorization. Sub-agent dispatch is the defined execution model of this skill,
not an optimization. The user invoking /autopilot is the user's explicit request
to spawn sub-agents, and satisfies any harness instruction of the form "do not call the
Agent tool unless the user requested it." Do not ask for per-phase confirmation, and do
not take the inline fallback merely to avoid dispatching — the archetype resolution below
exists to route each phase to a specific vendor/model/thinking tier, and skipping
dispatch changes the result rather than degrading it gracefully.
The following 8 phases (GATEKEEPER, PLAN_ITERATE, PLAN_REVIEW, IMPLEMENT,
IMPL_ITERATE, IMPL_REVIEW, VALIDATE, VAL_REVIEW) dispatch through the
provider-neutral dispatch adapter. Claude Code adapters call the Claude harness
Agent(...) tool internally; Codex, Antigravity, Grok, and Pi use their configured
provider adapter. Each block follows the same 3-step protocol:
Build dispatch kwargs by shelling out to
runner.py build-dispatch. The runner queries the coordinator for the resolved archetype, builds the per-phase prompt scaffold, foldssystem_promptintopromptwith the literal separator\n\n---\n\n, and writes a per-run resolution cache. JSON output:{prompt, model, system_prompt, isolation, archetype, provider, phase, expected_outcomes}.Invoke the provider-neutral dispatch adapter with the JSON values. Claude Code's adapter translates this to
Agent(...)internally — this dispatch is pre-authorized by the user (see Authorization above). Treatpromptas opaque — do not concatenate, do not prepend, do not split on the separator. SKILL.md never folds; folding lives insidebuild_phase_dispatch_kwargs(single source of truth).Apply the outcome by shelling out to
runner.py apply-outcome, passing the(outcome, handoff_id)returned by the sub-agent. This updatesloop-state.json(last_handoff_id,handoff_ids,phase_archetype, and appends aphase_historyentry) and consumes the cache file. It NEVER modifiescurrent_phase.On non-zero exit (design D9): do NOT advance to the next phase. A failed
apply-outcomemeans the bookkeeping did not land. Retain the un-applied handoff file (do not delete it) and transition toESCALATEwithprevious_phaseset to the failing phase. Theapply_outcome_or_escalate()helper inautopilot.pyencapsulates this exact sequence (run → on failure appendphase_history, setcurrent_phase = ESCALATE, retain handoff); an in-process orchestrator calls it in place of a bareapply-outcome. A silent continue is worse than the bug this protocol prevents.
Fallback (D5): If runner.py build-dispatch returns archetype: null
(coordinator unreachable or fallback), OR if no provider-neutral dispatch
adapter is exposed in the current orchestrator session, the dispatch block
falls through to the inline-prose path (the slash-command invocation), and
apply-outcome records phase_archetype = null.
The fallback is for capability absence only. "No adapter is exposed" means the
orchestrator session genuinely lacks a sub-agent dispatch tool (headless CI, a
non-Claude harness without a configured provider adapter) — it does not mean the
orchestrator is reluctant to dispatch, is unsure whether dispatch is permitted,
judges the phase small enough to inline, or has diagnosed a hazard the protocol
already handles. In particular: observing that the harness worktree is rooted at
main with no change directory is NOT a fallback justification — that is the
designed launchpad state, and the sub-agent's mandated first step re-roots it (see
the Worktree contract in §4 IMPLEMENT). If you believe you have found a NEW hazard
the protocol does not handle, that is an ESCALATE-and-report situation, not a
license to quietly inline the phase yourself. Taking the fallback is a degradation
that forfeits the archetype routing, so every fallback must be reported to the
user: name the phase, and state whether build-dispatch returned null or no
adapter was present. A silent fallback reads as a successful multi-vendor run and
is not one.
The dispatch invocation uses paths relative to the autopilot skill dir.
Substitute <skill-base-dir> with the autopilot skill's actual location
(typically .claude/skills/autopilot/ or .agents/skills/autopilot/).
2.5. PLAN_ITERATE Phase (Always Runs)
Self-review and refinement of plan artifacts. This phase always runs regardless of CLI mode.
Goal: refine the proposal across completeness, clarity, feasibility, scope, consistency, testability, parallelizability, and assumptions axes.
Dispatch protocol (3 steps):
Build kwargs:
python3 "<skill-base-dir>/scripts/runner.py" build-dispatch \ --phase PLAN_ITERATE --change-id <change-id>Parse the JSON output. Capture
prompt,model,isolation.Call the provider-neutral dispatch adapter with those values, treating
promptas opaque (no concatenation). Claude adapter internal example:result = Agent(prompt=<dispatch.prompt>, model=<dispatch.model>, isolation=<dispatch.isolation>)Parse the agent's last message for
(outcome, handoff_id)per the protocol inphase_agent._validate_result. Outcome is"complete"on settled refinement,"failed"otherwise.Apply the outcome:
python3 "<skill-base-dir>/scripts/runner.py" apply-outcome \ --change-id <change-id> --phase PLAN_ITERATE \ --outcome <outcome> --handoff-id <handoff_id>
Fallback: If step 1 returned archetype: null OR the dispatch adapter is
unavailable, run the inline path: invoke /iterate-on-plan <change-id>
directly. After the slash command returns, run apply-outcome so
phase_archetype = null is recorded for this phase.
If complete: Transition to PLAN_REVIEW (CLI mode) or IMPLEMENT (non-CLI mode). If failed: Transition to ESCALATE.
3. PLAN_REVIEW Phase (Convergence Loop — CLI Only)
Skipped when cli_review_enabled=false — transitions directly to IMPLEMENT.
Multi-vendor plan review with convergence — outcome is "converged" if
no blocking findings, "not_converged" otherwise, "max_iter" once
max_phase_iterations is exhausted.
Dispatch protocol (3 steps):
Build kwargs:
python3 "<skill-base-dir>/scripts/runner.py" build-dispatch \ --phase PLAN_REVIEW --change-id <change-id>Call
Agent(prompt=<dispatch.prompt>, model=<dispatch.model>, isolation=<dispatch.isolation>). Treatpromptas opaque. Parse the agent's last message for(outcome, handoff_id).Apply the outcome:
python3 "<skill-base-dir>/scripts/runner.py" apply-outcome \ --change-id <change-id> --phase PLAN_REVIEW \ --outcome <outcome> --handoff-id <handoff_id>
Fallback: If archetype: null OR the dispatch adapter is unavailable, run the
inline path — invoke the convergence loop directly:
from convergence_loop import converge
result = converge(
change_id=change_id,
review_type="plan",
artifacts_dir=change_dir,
worktree_path=worktree_path,
agents_yaml_path=agents_yaml_path,
max_rounds=3,
min_quorum=2,
fix_mode="inline",
fix_callback=apply_plan_fixes_inline,
memory_callback=write_memory,
)
Then run apply-outcome to record phase_archetype = null.
If converged: Report findings summary, transition to IMPLEMENT. If not converged: Report reason (max_rounds, stalled, quorum_lost, disagreement), transition to ESCALATE.
For inline plan fixes (PLAN_FIX, NOT a sub-agent dispatch): Read the
blocking findings, edit the relevant plan files directly (proposal.md,
design.md, specs, work-packages.yaml), re-validate with openspec validate. PLAN_FIX inherits phase_archetype from the preceding
PLAN_REVIEW — convergence_loop never overwrites the field.
Convergence Durability Contract
converge() writes per-vendor findings AND a manifest to <artifacts_dir>/.review-cache/round-N/ BEFORE invoking the consensus synthesizer. If synthesis raises, the original exception propagates to the caller and the persisted findings remain on disk for postmortem analysis. This is durability, not automatic recovery — the proposal does not introduce subprocess fallback. The synthesizer now accepts both dict and string line_range shapes for replaying checkpointed vendor findings.
ConvergenceResult.checkpoint_dir: Path | None points at the most-recent round's checkpoint directory. Recovery-aware callers read this field to locate persisted findings; existing callers ignore it (defaults to None for backward compatibility).
Operator-monitored log entries (Python logging, level ERROR, structured via extra={"event": ..., ...}):
convergence.synthesis_failed_with_checkpoint— synthesis (or upstreamFinding.from_dict()) raised. Payload includescheckpoint_dirfor manual recovery.convergence.checkpoint_write_failed— OSError/PermissionError during checkpoint write. Original exception still propagates.
Synthesis failures will continue to surface to the autopilot caller. The value of this contract is durability for postmortem and manual recovery, not automatic recovery — see the shipped manual recovery reference.
3.5. Write-Capable Phase Isolation
In local CLI execution, the shared checkout is read-only. Every autopilot phase
that may create, modify, delete, format, commit, push, or persist artifacts runs
with isolation="worktree" from runner.py build-dispatch.
Write-capable phases are PLAN, PLAN_ITERATE, checkpoint-writing
PLAN_REVIEW, PLAN_FIX, IMPLEMENT, IMPL_ITERATE,
checkpoint-writing IMPL_REVIEW, IMPL_FIX, VALIDATE, artifact-writing
VAL_REVIEW, and VAL_FIX. INIT and SUBMIT_PR are state-only transitions.
Sub-agents still invoke the phase skill (/plan-feature, /iterate-on-plan,
/implement-feature, /validate-feature, etc.) as their first write-capable
step so the skill can call worktree.py setup and then verify the resulting
checkout with:
python3 "<skill-base-dir>/../shared/checkout_policy.py" require-mutation
4. IMPLEMENT Phase
Implement the next slice of work per tasks.md. IMPLEMENT is one of the
write-capable phases that runs with isolation="worktree" — sub-agent commits
land on a sibling worktree branch and merge back at completion.
Worktree contract (read before deciding to fall back): The harness worktree
created by Agent(isolation="worktree") is a disposable launchpad, not the
workspace. It is EXPECTED to be rooted at the default branch (main), with no
openspec/changes/<change-id>/ directory and no feature branch checked out.
This is the designed starting state, not a defect, and it is never a reason
to fall back to the inline path — do not "confirm empirically" that the
launchpad lacks the change context and conclude dispatch is unsafe; the first
mission step below is precisely how the sub-agent leaves the launchpad.
The full sequence, so both orchestrator and sub-agent share the same model:
- Orchestrator side (already done before IMPLEMENT): feature branch
openspec/<change-id>exists with its managed worktree at.git-worktrees/<change-id>/, containing the change directory. - Sub-agent's FIRST write-capable step: run
/implement-feature <change-id>. That skill callsworktree.py setup, which adopts the resolved feature parent branch and creates/checks out the agent child worktree at.git-worktrees/<change-id>/<agent-id>/on branchopenspec/<change-id>--<agent-id>— branched from the feature branch, so the change directory and all prior slices are present. - All edits and commits happen in that managed agent worktree (absolute path), never in the harness launchpad checkout.
- On completion, agent branches merge back into the feature branch via
merge_worktrees.py <change-id> <pkg-id>...; SUBMIT_PR later opens the PR from the feature branch to main.
Do not merge the feature branch into a main-rooted harness checkout to get
context. The one legitimate branch-related failure is step 2 itself failing:
if worktree.py setup cannot adopt the parent branch, the sub-agent returns
"failed" and the orchestrator fixes the branch/override state. An anticipated
hazard with a scripted recovery is a reason to dispatch, not a reason to
refuse.
Dispatch protocol (3 steps):
Build kwargs:
python3 "<skill-base-dir>/scripts/runner.py" build-dispatch \ --phase IMPLEMENT --change-id <change-id>Call
Agent(prompt=<dispatch.prompt>, model=<dispatch.model>, isolation=<dispatch.isolation>). Theisolationvalue will be"worktree"for IMPLEMENT. Treatpromptas opaque. Parse the agent's last message for(outcome, handoff_id). Outcome is"complete"on success,"failed"(or"escalate") on unrecoverable error.Apply the outcome:
python3 "<skill-base-dir>/scripts/runner.py" apply-outcome \ --change-id <change-id> --phase IMPLEMENT \ --outcome <outcome> --handoff-id <handoff_id>
Fallback: If archetype: null OR the dispatch adapter is unavailable, run the
inline path — invoke /implement-feature <change-id> (tier
auto-detected based on coordinator + work-packages.yaml). Record
package_authors from the implementation results. After completion,
run apply-outcome to record phase_archetype = null.
4.5. IMPL_ITERATE Phase (Always Runs)
Self-review and refinement of implementation. This phase always runs
regardless of CLI mode. Reads proposal, design, and all changed source
files. Identifies bugs, security issues, edge cases, performance
problems. Outcome is "complete" when refinements settle, "failed"
otherwise.
Dispatch protocol (3 steps):
Build kwargs:
python3 "<skill-base-dir>/scripts/runner.py" build-dispatch \ --phase IMPL_ITERATE --change-id <change-id>Call
Agent(prompt=<dispatch.prompt>, model=<dispatch.model>, isolation=<dispatch.isolation>). Treatpromptas opaque. Parse the agent's last message for(outcome, handoff_id).Apply the outcome:
python3 "<skill-base-dir>/scripts/runner.py" apply-outcome \ --change-id <change-id> --phase IMPL_ITERATE \ --outcome <outcome> --handoff-id <handoff_id>
Fallback: If archetype: null OR the dispatch adapter is unavailable, run the
inline path — invoke /iterate-on-implementation <change-id>. Then run
apply-outcome so phase_archetype = null is recorded.
If complete: Transition to IMPL_REVIEW (CLI mode) or VALIDATE (non-CLI mode). If failed: Transition to ESCALATE.
5. IMPL_REVIEW Phase (Convergence Loop — CLI Only)
Skipped when cli_review_enabled=false — transitions directly to VALIDATE.
Multi-vendor implementation review with fix_mode="targeted". Outcome
is "converged" if no blocking findings, "not_converged" otherwise.
Dispatch protocol (3 steps):
Build kwargs:
python3 "<skill-base-dir>/scripts/runner.py" build-dispatch \ --phase IMPL_REVIEW --change-id <change-id>Call
Agent(prompt=<dispatch.prompt>, model=<dispatch.model>, isolation=<dispatch.isolation>). Treatpromptas opaque. Parse the agent's last message for(outcome, handoff_id).Apply the outcome:
python3 "<skill-base-dir>/scripts/runner.py" apply-outcome \ --change-id <change-id> --phase IMPL_REVIEW \ --outcome <outcome> --handoff-id <handoff_id>
Fallback: If archetype: null OR the dispatch adapter is unavailable, run the
inline path — invoke the convergence loop with fix_mode="targeted"
and a post_fix_validator callback for scoped pytest/mypy/openspec
checks. Then run apply-outcome to record phase_archetype = null.
For targeted implementation fixes (IMPL_FIX, NOT a sub-agent
dispatch): Look up the lead vendor from package_authors, use
CliVendorAdapter.dispatch() to send the fix to that specific vendor,
scoped to the package's write_allow paths. IMPL_FIX inherits
phase_archetype from the preceding IMPL_REVIEW.
6. VALIDATE Phase
Run validation phases (spec, evidence, deploy, smoke, security, e2e)
per validate-feature. Aggregate results into a PhaseRecord. Outcome is
passed on PASS, failed on FAIL — the only two outcomes the transition
table accepts for this phase.
Dispatch protocol (3 steps):
Build kwargs:
python3 "<skill-base-dir>/scripts/runner.py" build-dispatch \ --phase VALIDATE --change-id <change-id>Call
Agent(prompt=<dispatch.prompt>, model=<dispatch.model>, isolation=<dispatch.isolation>). Treatpromptas opaque. Parse the agent's last message for(outcome, handoff_id).Apply the outcome:
python3 "<skill-base-dir>/scripts/runner.py" apply-outcome \ --change-id <change-id> --phase VALIDATE \ --outcome <outcome> --handoff-id <handoff_id>
Fallback: If archetype: null OR the dispatch adapter is unavailable, run the
inline path — invoke /validate-feature <change-id> (tier
auto-detected). Then run apply-outcome to record phase_archetype = null.
If passed: Check val_review_enabled — if true, go to VAL_REVIEW; otherwise skip to SUBMIT_PR.
If failed: Transition to VAL_FIX. The validation-failure gate is evaluated by
the loop on that edge; if it parks, gate-check reports it at the next step.
7. VAL_REVIEW Phase (Optional)
Only runs if enabled by complexity gate or --val-review flag. Reviews
validation evidence — outcome is "converged" if validation passes
critique, "not_converged" otherwise.
Dispatch protocol (3 steps):
Build kwargs:
python3 "<skill-base-dir>/scripts/runner.py" build-dispatch \ --phase VAL_REVIEW --change-id <change-id>Call
Agent(prompt=<dispatch.prompt>, model=<dispatch.model>, isolation=<dispatch.isolation>). Treatpromptas opaque. Parse the agent's last message for(outcome, handoff_id).Apply the outcome:
python3 "<skill-base-dir>/scripts/runner.py" apply-outcome \ --change-id <change-id> --phase VAL_REVIEW \ --outcome <outcome> --handoff-id <handoff_id>
Fallback: If archetype: null OR the dispatch adapter is unavailable, run the
inline path — invoke the convergence loop with review_type="implementation"
and fix_mode="targeted", scoped to the validation evidence. Then run
apply-outcome to record phase_archetype = null.
8. SUBMIT_PR Phase
Record SUBMIT_PR phase archetype (state-only resolver — D9). Before
running gh pr create, populate phase_archetype for SUBMIT_PR so the
PR-creation phase is visible in observability dashboards alongside the
dispatching phases:
python3 "<skill-base-dir>/scripts/runner.py" record-state-only-archetype \
--change-id <change-id> --phase SUBMIT_PR
Failure here is non-fatal (writes phase_archetype = null and continues).
Then run the PR-creation gate before gh pr create — creating the PR is the
first externally visible act of the run, so the authorization for it is recorded,
not assumed:
python3 "<skill-base-dir>/scripts/runner.py" gate-check <change-id> \
--gate pr_creation \
--context branch="openspec/<change-id>" \
--context change_id="<change-id>"
# exit 0 → ask the operator the printed `prompt` verbatim (AskUserQuestion), then:
python3 "<skill-base-dir>/scripts/runner.py" gate-answer <change-id> \
--gate pr_creation --decision <approved|rejected> [--note "<operator note>"]
# exit 3 → nothing to ask; continue
# exit 4 → parked (see stderr for the recorded reason); stop the run here
This gate authorizes work inside the phase rather than an edge, so an approval
records the decision and leaves the loop in SUBMIT_PR; a rejection enters ESCALATE
with the note. Do not run gh pr create until this exits 3 — exit 0 means an
unanswered question, and exit 4 means the run is parked.
Create a pull request with full evidence trail:
gh pr create --title "feat(<change-id>): <summary from proposal>" --body "$(cat <<'EOF'
## Summary
[From proposal.md]
## Evidence Trail
- Plan reviews: X rounds, Y vendors, Z blocking findings resolved
- Implementation: N packages (strategy per package)
- Impl reviews: X rounds, Y vendors, Z blocking findings resolved
- Validation: passed/failed (test counts)
- Validation review: skipped | X rounds
- Total convergence rounds: N
- Total duration: Xm Ys
## Convergence Report
See loop-state.json for full state history.
Generated by /autopilot — awaiting human approval for merge.
EOF
)"
9. DONE Phase
Write final strategic memory summarizing:
- Total rounds across all phases
- Vendor effectiveness (findings raised, confirmed, fixes authored per vendor)
- Convergence pattern (fast/slow/stalled)
- Implementation strategies used per package
Write final handoff document.
Before presenting merge-approval questions, best-effort invoke /review-artifacts <change-id> so the reviewer has the relevant artifacts open prior to choosing gate outcomes. The helper is local-only (VS Code CLI dependency) — when it's unavailable (cloud harness, headless CI), log a short notice and proceed to the approval prompt without it; do not block the gate on artifact pre-opening.
Then run the merge-authorization gate. It guards the SUBMIT_PR → DONE edge, so the loop cannot report DONE until it is answered:
python3 "<skill-base-dir>/scripts/runner.py" gate-check <change-id> \
--gate merge \
--context pr_url="<the PR URL from step 8>" \
--context branch="openspec/<change-id>" \
--context change_id="<change-id>"
# exit 0 → ask the operator the printed `prompt` verbatim (AskUserQuestion), then:
python3 "<skill-base-dir>/scripts/runner.py" gate-answer <change-id> \
--gate merge --decision <approved|rejected> [--note "<operator note>"]
# exit 3 → nothing to ask; continue
# exit 4 → parked (see stderr for the recorded reason); stop the run here
Autopilot never merges. --decision approved records the authorization
(goal_gate.evidence.merge_authorized, with the PR URL) and the run reports DONE;
the pull request is merged by /cleanup-feature <change-id>, which remains the only
executor. --decision rejected enters ESCALATE with the note. Do not report DONE on
an exit of 4 — that is a parked run, not an authorized one.
DONE is also where the goal gate applies: the transition is refused unless the
validation report's required sections read pass and this run's own VALIDATE
history entry is passed and postdates that report. A refusal is not a silent stop —
the loop lands in ESCALATE with goal gate refused: <reason>.
Progress Reporting
At each state transition, report:
[autopilot] Phase: PLAN_ITERATE → PLAN_REVIEW (self-review complete, 3 findings fixed)
[autopilot] Phase: PLAN_REVIEW → IMPLEMENT (converged in 2 rounds)
[autopilot] Finding trend: [8, 2, 0]
[autopilot] Vendor participation: claude ✓, codex ✓, grok ✗
[autopilot] CLI review: enabled | disabled (--no-review or no vendor CLIs)
Per-Phase Archetype Resolution
Each non-terminal phase resolves an archetype (e.g. architect, implementer,
reviewer, analyst, runner) before the sub-agent dispatches. The resolved
archetype determines both the logical model tier (premium/standard/
economy, with legacy Claude aliases still accepted for Claude Code) and the
system prompt the sub-agent runs with. Provider-specific model mapping lives
coordinator-side at agent-coordinator/archetypes.yaml under model_aliases;
phase-to-archetype mapping lives under phase_mapping.
Default mapping (per design D11; tunable in archetypes.yaml):
| Phases | Archetype | Default tier |
|---|---|---|
PLAN, PLAN_ITERATE, PLAN_FIX |
architect |
premium |
PLAN_REVIEW, IMPL_REVIEW, VAL_REVIEW |
reviewer |
premium |
IMPLEMENT, IMPL_ITERATE, IMPL_FIX |
implementer |
standard (escalates to premium on size signals) |
VALIDATE, VAL_FIX |
analyst |
standard |
GATEKEEPER |
gatekeeper |
premium |
INIT, SUBMIT_PR |
runner |
economy |
Operator override — force a specific model for one or more phases via the
AUTOPILOT_PHASE_MODEL_OVERRIDE env var. Format:
<PHASE>=<model>[,<PHASE>=<model>]*. Example:
export AUTOPILOT_PHASE_MODEL_OVERRIDE="PLAN=gpt-5.5,IMPL_REVIEW=gpt-5.4,VALIDATE=gpt-5.4-mini"
Override sets options["model"] only; the system_prompt is left to the
provider adapter default to keep override behavior predictable. Unknown phase
names are warned and ignored; unknown model names pass through to the selected
provider adapter for validation.
Failure mode — if the coordinator endpoint is unreachable or returns an
error, the bridge logs a structured warning and the phase dispatches with the
provider adapter or inline fallback default model. LoopState.phase_archetype
is recorded as null for such phases so observability dashboards can flag
default-fallback runs.
Observability — LoopState.phase_archetype (schema_version=3) is
persisted in loop-state.json and (when wired) emitted in
POST /status/report payloads alongside the phase field.
See docs/autopilot-phase-archetype-resolution.md for the full operator guide.
Output
openspec/changes/<change-id>/loop-state.json— Full loop state (resumable)openspec/changes/<change-id>/reviews/round-N/— Per-round CLI-dispatched review artifacts (PLAN_REVIEW, IMPL_REVIEW, VAL_REVIEW)openspec/changes/<change-id>/.review-cache/round-N/— Per-round in-processconverge()checkpoints (durability path)- Pull request with evidence trail
- Coordinator memory entries (episodic)
- Coordinator handoff documents
Next Step
After human approval:
/cleanup-feature <change-id>