"Orchestrate the full plan-review-implement-validate-PR lifecycle with multi-vendor review convergence"
Resources
4Install
npx skillscat add jankneumann/agentic-coding-tools/autopilot Install via the SkillsCat registry.
Autopilot
Durable state artifact authority
Shared holder, writer, authority, fallback, and rehydration semantics live in docs/guides/state-artifacts.md. The procedures below retain this skill's phase-specific commands and gates.
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" --jsonIf 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
Coordinated phase-projection protocol
The queue mirror is enabled only after coordinator detection selected the
coordinated tier. The host must pass the detected coordinator URL explicitly as<coordinator-url>; local-parallel and sequential hosts do not import,
construct, register, submit, reconcile, or label through the projection adapter.
For a coordinated resume, load no queue state into the loop. First repair the
queue solely from the durable file, before pending-gate handling or phase work:
python3 "<skill-base-dir>/scripts/runner.py" project-state \
--change-id <change-id> --mode reconcile \
--coordinator-url "<coordinator-url>"The host must use runner.py init for initialization and runner.py transition --outcome <outcome> for every ordinary phase edge; it must not
hand-edit current_phase. After every successfully persisted runner
mutation — init, transition, apply-outcome,escalate, record-state-only-archetype, and gate-answer — immediately runproject-state --mode submit. A non-zero mutation exit suppresses projection. Runner mutation exit codes are
0 for a successful write or a clean gate-pending stop, 1 for operational
failure, and 2 for invalid caller input. A clean gate-pending transition writes
nothing and remains parked for gate-answer.
An invalid logical phase edge is different from invalid CLI input: transition
records transition_failed, enters ESCALATE with the current phase asprevious_phase, and exits 0 so the required projection publishes the parked
generation. If host-side phase bookkeeping fails before transition can run,
use the explicit durable writer:
python3 "<skill-base-dir>/scripts/runner.py" escalate \
--change-id <change-id> \
--reason "apply-outcome failed; retained handoff <handoff_id>"After its exit-0 write, submit projection and stop the run.
For gate-check, exits 0, 3, and 4 all mean a decision or park was durably
recorded, so submit projection before asking, continuing, or stopping; exits 1
and 2 suppress projection. Projection failure is reported as degraded but never
reverts or rewrites loop-state.json. A project-state command that emits a
structured degraded JSON envelope exits 0 so authoritative phase work continues;
exit 1 is reserved for failures that prevent producing that envelope.
python3 "<skill-base-dir>/scripts/runner.py" project-state \
--change-id <change-id> --mode submit \
--coordinator-url "<coordinator-url>"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
fiIf 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 heregate-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 applies the canonicalresolved edge, persists the resumed phase, and records authorization before the
host submits projection; --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
Create state through the canonical writer. In coordinated mode, follow it with
the submit call from the protocol above; if init fails, do not project.
Append only the flags present in the Autopilot invocation; init persists them once, and an idempotent resume retains the original options.
python3 "<skill-base-dir>/scripts/runner.py" init --change-id <change-id> [--force] [--val-review] [--no-review]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"
fiPass 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 invendor_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 INITFailure here is non-fatal — the helper logs a warning and writesphase_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 returnsarchetype: 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 viaapply-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 avalidation-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 PLANFailure here is non-fatal — the helper logs a warning and writesphase_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.
Canonical phase edges
Whenever a phase section below says transition to a phase, apply the named
outcome through the durable writer, then submit the projection in coordinated
mode:
python3 "<skill-base-dir>/scripts/runner.py" transition \
--change-id <change-id> --outcome <outcome>A failed transition is a stop: do not project and do not advance work.
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 harnessAgent(...) 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_promptintoprompt
with 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 toAgent(...)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
failedapply-outcomemeans the bookkeeping did not land. Retain the
un-applied handoff file (do not delete it), invokerunner.py escalate
with a reason naming the failed phase and retained handoff, then project the
exit-0 ESCALATE write and stop.apply_outcome_or_escalate()remains the
equivalent in-process API. On apply success, invokerunner.py transition
with the recorded outcome; an unsupported logical edge is itself converted
to a durable, projectable ESCALATE write. 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), andapply-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 atmain 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 sophase_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 a single convergence engine. converge()
compacts the gate-time ledger, hunts the last-fix delta after round 1,
parks disagreements, and applies scoped fixes via fix_callback. PLAN_FIX
is recorded as a phase_history sub-step; the outer machine does not
bounce PLAN_REVIEW → PLAN_FIX → PLAN_REVIEW as a second cold review.
Outcome is "converged" if no blocking ledger items remain, "max_iter"
if the inner loop stalled or exhausted rounds. "not_converged" remains
in the transition table only for resume of in-flight PLAN_FIX loop-state.
Dispatch protocol (3 steps) — the dispatched agent executes converge()
as the whole review phase. Do not instruct it to run /parallel-review-plan
as a one-shot cold review; that re-introduces the outer PLAN_FIX bounce
this phase exists to remove.
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>). The prompt already tells the agent
to runconverge()with a real PLAN_FIXfix_callback. Treatpromptas opaque. Parse the agent's last message for(outcome, handoff_id)— outcome is"converged"or"max_iter".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 is parked to reviews/parked-disagreements.json and does
not abort the loop. Transition to ESCALATE.
For inline plan fixes (PLAN_FIX sub-step inside converge(), NOT an
outer-machine phase and NOT a sub-agent dispatch): Read the blocking
ledger items, edit only their cited file_paths (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. Do not add architecture or
expand scope.
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.
Deterministic Preprocessing Before Each Round
Ported from alibaba/open-code-review (Apache-2.0) — see openspec changeadd-deterministic-review-preprocessing. Every packet converge() builds
now goes through a deterministic pass before a single reviewer token is
spent:
- Selection and rules —
review_packet.build_review_packet()runsfile_selection.select_files()(five gates: binary, excludes, includes,
generated paths, deletions, per-file size) andreview_rulespath-glob
grouping before rendering. Excluded files and their reasons land in the
packet metadata (schemav2), never silently. - Fact-check —
converge(fact_check=True)(the default) runs one
economy-tier model call per vendor after that vendor's raw findings are
checkpointed and before consensus synthesis, removing only findings the
diff itself disproves. Protected-subject findings (memory safety,
concurrency, declaration consistency, behavioral/compatibility change,
unused parameter) are never removed. Passfact_check=Falseto disable
it for an A/B comparison run; the per-round manifest recordsfact_check_tokens,fact_check, andfact_check_removedeither way. - Line resolution and coverage — a vendor's
existing_codesnippet is
resolved to aline_rangeat ingest (before synthesis ever sees it), and
an optional per-vendorcoveragereport narrows that vendor's quorum
eligibility to the files it actually reviewed. Both are visible on the
round manifest (unanchored_findings,coverage_rate,coverage_eligibility). - OCR vendor —
ocr-local(declared inagent-coordinator/agents.yaml)
joins the panel automatically when theocrbinary is on PATH; absent,
dispatch is unchanged.
None of this changes converge()'s call signature except the newfact_check keyword (default True, so existing callers are unaffected).
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-writingPLAN_REVIEW, PLAN_FIX, IMPLEMENT, IMPL_ITERATE,
checkpoint-writing IMPL_REVIEW, IMPL_FIX, VALIDATE, artifact-writingVAL_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-mutation4. 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 noopenspec/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). Recordpackage_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 runapply-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". Same
one-engine contract as PLAN_REVIEW: IMPL_FIX is a fix_callback sub-step
inside converge(), not an outer bounce that re-dispatches a cold review.
Outcome is "converged" if no blocking ledger items remain, "max_iter"
otherwise.
Dispatch protocol (3 steps) — same one-engine contract as PLAN_REVIEW:
the dispatched agent executes converge() as the whole review phase.
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>). The prompt already tells the agent
to runconverge()with a real IMPL_FIXfix_callback. Treatpromptas opaque. Parse the agent's last message for(outcome, handoff_id)— outcome is"converged"or"max_iter".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 sub-step, NOT a
sub-agent dispatch): Look up the lead vendor from package_authors, useCliVendorAdapter.dispatch() to send the fix to that specific vendor,
scoped to the intersection of the package's write_allow paths and the
finding file_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 ispassed 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) — the dispatched agent executes converge()
as the whole VAL_REVIEW phase.
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>). The prompt already tells the agent
to runconverge()with a real VAL_FIXfix_callback. 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 runapply-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_PRFailure 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 hereThis 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 hereAutopilot 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 (explicit escape hatch) — bypass YAML archetype/tier
selection and force harness model ids directly for one or more phases via theAUTOPILOT_PHASE_MODEL_OVERRIDE env var. This is non-default operator
policy, not the authored selection path: normal runs resolve phases througharchetypes.yaml / phase_mapping. Format:<PHASE>=<harness-model-id>[,<PHASE>=<harness-model-id>]*. Example (ids are
illustrative harness strings the override injects as-is):
export AUTOPILOT_PHASE_MODEL_OVERRIDE="PLAN=gpt-5.5,IMPL_REVIEW=gpt-5.4,VALIDATE=gpt-5.4-mini"Override sets options["model"] only (harness model ids, not archetype
names); 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 inPOST /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)openspec/changes/<change-id>/.review-ledger/ledger.json— Gate-time finding ledger (open/addressed/retired/parked)openspec/changes/<change-id>/reviews/parked-disagreements.json— Human queue for disposition disagreements- Pull request with evidence trail
- Coordinator memory entries (episodic)
- Coordinator handoff documents
Next Step
After human approval:
/cleanup-feature <change-id>