AI orchestrator — coordinates Phase 3 (planning). Consumes the upstream chain from a topic `_thought` (Mode A full upstream / Mode B chain present + invoke `/solution-design` first / Mode C Ninja-Plan no spine) BEFORE `EnterPlanMode` fires. Sequences per-transition coherency dispatches (`factcheck-plan-step 0A/0B2/0C`) and a final coherency check (`factcheck-plan-coherency`) at each structural transition with explicit per-spawn model pinning (Opus = primary thinker, Sonnet = support; early-exit on Round-1 PASS). After plan approval (ExitPlanMode), hard-gates the next move via an AskUserQuestion (a) handoff-prompt / (b) /close+/clear / (c) continue-in-session triple driven by the `post-plan-uxgate.sh` + `check-post-plan-pending.sh` marker pair.
Install
npx skillscat add dmitrydf/q/plan Install via the SkillsCat registry.
/plan — Phase 3 Orchestrator
Coordinate Phase 3 (planning) in a single command. The orchestrator runs strictly BEFORE EnterPlanMode fires — every spine read, every cross-phase fetch, every per-transition verification dispatch, and every spine writeback happens in the orchestrator phase. Plan mode itself is reserved for operational delivery: per-slice Coherent Actions + Design Review + Gate-1/2/3 verification, all written into the plan file (the only path check-plan-readonly.sh allows during plan mode).
The skill is a stateless thin orchestrator in the hexagonal-AI-adapter pattern (~/.claude/rules/code_first_architecture.md, lines 81–128). Code owns the flow (the new CLIs in pre_plan_gates.py: plan-mode-init, link-plan-to-thought, factcheck-plan-step, factcheck-plan-coherency); the AI sits behind ports and only renders judgment when code cannot (Coherent Actions design, Mode B's /solution-design invocation, the per-transition checker prompts). Producer-never-verifies is enforced at each structural transition, not just once — Outcome → Outcome Claims (0B2), Outcome Claims → Gap Analysis (0C), full plan → Diagnosis + Desired Outcome (0G), plus Mode-C fresh-Diagnosis (0A).
Main-session-only. Cannot be invoked from inside a subagent.
Trigger
Activate when the user says:
/plan/plan --help- "plan this"
- "let's plan"
- "phase 3"
- "ready to plan"
- "enter plan mode" (route through the orchestrator, not directly to
EnterPlanMode)
The plan-mode hotkey (Shift+Tab → EnterPlanMode) is gated by check-thought-bound.sh (PreToolUse on EnterPlanMode). If the user presses the hotkey without a plan-mode-init marker, the hook surfaces the Track-properly vs Ninja-Plan choice and the user is routed back through this skill.
When to Use
Activate in three situations, each mapping to one of the three modes:
| Mode | Precondition | Upstream artifacts consumed |
|---|---|---|
| A Full upstream | _thought bound + # Discovery locked (4 fields) + an Agreed Solution present — a <base>_DESIGN.md in the slug-family or (legacy) at least one inline ### Solution Alternative N in the spine |
Verbatim: Diagnosis, Guiding Policy, Desired Outcome, OMTM, Agreed Solution, Architecture/UX decisions, B2 slices — read from the _DESIGN.md when present, else from the spine's inline block |
| B Chain present, Phase 2 absent | _thought bound + # Discovery locked + NO Agreed Solution (neither a _DESIGN.md nor an inline ### Solution Alternative) |
Invoke /solution-design whole BEFORE plan mode opens → writes a fresh <base>_DESIGN.md + spine pointer via B3 → resume Mode A flow |
| C Ninja-Plan | No _thought bound (user explicitly opts out of tracking) |
None — plan is single-slice, lands at <project_root>/Thoughts/<slug>-<ts>_PLAN.md carrying a bookkeeping: mode-c frontmatter line, no spine writeback |
Mode is chosen before EnterPlanMode fires. The check-thought-bound.sh hook reads the plan-mode-init marker; absence blocks plan-mode entry with the Track-properly / Ninja-Plan surface.
Fast-track entry (user-facing). Mode C is the canonical fast-track into Planning — when the topic is already well-framed (Diagnosis + Desired Outcome are clear; there is no real Solution-Design choice) the user invokes /plan --mode C (or picks "Ninja-Plan" at the Track-properly gate) and the orchestrator drops straight into single-slice plan authoring without consuming any spine. Full plan-mode gates still cover the work — Gate 0 (a–f), Gate 1, Gate 2, Gate 3, and the per-transition + final coherency dispatches. The sibling ultra-fast-track is /ninja-fix, which enters at Implementation rather than Planning; see Workflow.md "Cross-Cutting → Speed-track entry points" for the side-by-side use cases.
Do NOT activate from inside a subagent (recursion is bounded by topology — see Edge Case E11).
Invariants
Orchestrator runs strictly BEFORE
EnterPlanMode. Every read of the spine, every/solution-designinvocation, every per-transition coherency dispatch happens in the orchestrator phase. Inside plan mode the only writable surface is the plan file itself (check-plan-readonly.shenforces this). (F9 /${KIT_HOOKS_DIR}/check-plan-readonly.sh.)Plan mode is a consumer, never a producer. Modes A and B do not re-derive Diagnosis, Desired Outcome, Guiding Policy, or OMTM — these are read verbatim from the spine's
# Discoverysection. TheGATE0:CHAIN_SRCmarker carries thediscovery_src_hashanchor proving no re-rendering happened. (F1, F2.)Producer-never-verifies AT EACH structural transition. Per
code_first_architecture.md:121-124grading order ("code → model → human" applied per layer), every transition is independently verified by parent-spawned isolatedreadonly-checkercheckers — not just the final plan. The four transitions are:- 0A:VALIDATED (Mode C only) — fresh Diagnosis grounded in codebase + concept
- 0B2:VALIDATED — Outcome Claims cover the Desired Outcome
- 0C:VALIDATED — Gap Analysis maps to Outcome Claims AND exists in the current codebase
- 0G:COHERENCY — assembled plan delivers Diagnosis + Desired Outcome
(F4, F11.)
Reuse split locked by topology.
- Bare primitives (reusable as stateless adapters):
outcome-framing,lean-analytics-metrics, reflect-back,/double-check. - Whole skills (invoked entire, never decomposed inline):
/solution-design— because B3 owns spine writeback (~/.claude/skills/solution-design/SKILL.md:45); decomposing into bare B2 would violate the stateless contract (~/.claude/skills/solution-slicer/SKILL.md:52,:426).
(F7.)
- Bare primitives (reusable as stateless adapters):
Per-spawn explicit model pinning. Every checker spawned via the Agent tool MUST carry an explicit
model:parameter — never rely on the subagent default model. Re-fc 2026-05-19 lesson: unpinned Explore subagents silently ran on Haiku for ~1 day, scribe recorded false provenance. Post-spawn enforcement viacheck-impl-models.shis the runtime backstop (${KIT_HOOKS_DIR}/_factcheck_engine.py:380-393precedent). (F6.)Plan-detail level: outcomes + gates, not micro-steps. Each Coherent Action describes Goal + Guard rails + Validation gate. Do NOT enumerate sub-steps; that violates Anthropic prompt-engineering guidance ("prefer general instructions over prescriptive steps",
~/.claude/rules/prompt-engineering.md:139) and Cockburn nano-increments. The checker (and the implementer) supplies the steps from the validation gate.F10 v5 hard-gated post-exit UX. After
ExitPlanModeis granted, the next tool the AI invokes MUST beAskUserQuestionwith the (a) handoff-prompt / (b)/close+/clearpair. The marker pairpost-plan-uxgate.sh(PostToolUse onExitPlanMode) +check-post-plan-pending.sh(PreToolUse.*, in-scriptAskUserQuestionallow-list) enforces this. AI clears the marker viapre_plan_gates.py clear-post-plan-choiceafter the user picks.
Parameters
All optional (defaults shown):
| Flag | Default | Description |
|---|---|---|
--mode {A,B,C} |
inferred from spine state | Force a specific mode. Use Mode C explicitly for Ninja-Plan when an _thought exists but you want to bypass it. |
--thought <path> |
from active topic state | Path to _thought file. Required if mode=A/B and no active topic. |
--coherency-rounds N |
2 | Max rounds passed to factcheck-plan-coherency. Plan-kind ceiling is 3 (~/.claude/rules/factcheck-convergence.md §4). |
--step-rounds N |
1 | Max rounds per factcheck-plan-step 0A/0B2/0C/0D/0E dispatch. Same ceiling rules. |
--help |
— | Print this table and stop. |
Execution
The orchestrator runs in two phases: pre-plan-mode (where all spine I/O and per-transition checks happen) and inside-plan-mode (where the plan file is authored and Gate 1/2/3 verification runs). The boundary is EnterPlanMode.
Step 0 — Help
If invoked with --help, print Parameters and stop.
Step 1 — Resolve session + spine state (pre-plan-mode)
- Read
SESSION_IDfrom the most recentUserPromptSubmit hook success: SESSION_ID=...line. - Resolve the topic spine: use
--thought <path>if provided; otherwise read active topic state viapython3 ${KIT_HOOKS_DIR}/pre_plan_gates.py phase-show. If_thought_file_pathresolves, treat as candidate spine. - Read the candidate spine. Locate
# Discoverysection:- All 4 locked fields present (
## Guiding Policy,## Desired Outcome,## Desired Solution,## Metricswith OMTM sub-line) → Mode A or B candidate - Section absent or any field missing/placeholder → cannot use this spine
- All 4 locked fields present (
- If a candidate spine resolves with all 4 locked fields, look for an Agreed Solution by dual-read (bookkeeping-model OQ18): first check for a
<base>_DESIGN.mdin the slug-family (<base>= spine filename minus_THOUGHT.md); if absent, scan the spine's# Solution Designfor inline### Solution Alternative Nheaders (legacy):- A
_DESIGN.mdexists or ≥1 inline### Solution Alternative Npresent → Mode A (consume the latest Alternative by max-N from whichever source holds it —~/.claude/skills/solution-design/SKILL.md:53monotonic-N invariant) - Neither present → Mode B (chain present, Phase 2 absent)
- A
- If no candidate spine or
# Discoveryincomplete: prompt the user viaAskUserQuestion:- (a) Track properly — pause
/plan, run/clarificationto lock# Discovery, then return - (b) Ninja-Plan — proceed inline as Mode C (no spine, single slice; plan lands in
<project_root>/Thoughts/<slug>-<ts>_PLAN.mdwith abookkeeping: mode-cfrontmatter line)
- (a) Track properly — pause
The --mode flag overrides the inference. Honour explicit user opt-out.
Step 2 — Mode B sub-step (only if Mode B)
Invoke /solution-design whole as a Skill call (never invoke bare B1/B2 inline). The skill produces a new ### Solution Alternative N via B3 and writes it to the topic's <base>_DESIGN.md (with a spine pointer under # Solution Design). On ESCALATE, surface verbatim and stop — do not proceed to plan mode. On PASS, refresh the spine + Design snapshot from Step 1 and continue as Mode A.
Step 3 — Initialize plan-mode marker (pre-plan-mode)
Call:
python3 ${KIT_HOOKS_DIR}/pre_plan_gates.py plan-mode-init <SESSION_ID> --mode {A|B|C} [--thought-path <spine path>]For Modes A/B, --thought-path is required (provides discovery_src_hash). For Mode C, omit --thought-path. The CLI writes ~/.claude/state/plan_mode_init/<SESSION_ID>.json which check-thought-bound.sh reads on EnterPlanMode.
Step 4 — Mode C ONLY: fresh-Diagnosis verification (per-transition gate 0A)
In Mode C only, the user authors a fresh Diagnosis (no upstream to consume). After the user states the diagnosis but before drafting Outcome Claims:
- Draft the candidate Diagnosis in chat (not yet in the plan file — plan mode is not open).
- Pre-plan-mode candidate screen (advisory). Dispatch 1 Sonnet + 1 Opus
readonly-checkercheckers on the candidate Diagnosis text (isolated context, explicitmodel:pinning) with the rubric: "reason through whether this Diagnosis names a critical aspect grounded in observable behavior of the named codebase paths, then emit as the final line exactlyVERDICT: PASSorVERDICT: DISCREPANCY." This early screen catches a weak Diagnosis before you invest in plan mode; if it looks DISCREPANCY, refine with the user before entering plan mode. - The gate-authoritative 0A receipt is written at Step 6, once the Diagnosis is authored into the plan file (the engine receipt is keyed to the plan-file path, which the harness assigns only at
EnterPlanMode). At Step 6, re-runfactcheck-plan-step 0Awith the same dispatch and hand the captured checker outputs to the CLI — that engine receipt is whatcheck-plan-gates.shreads forGATE0A:VALIDATED(S2). Carry the candidate outputs forward or re-dispatch; either way the Step-6 CLI call is what authors the verdict.
(Modes A and B skip Step 4 — the Diagnosis is consumed from the spine, already verified upstream by /clarification Step 7.)
Step 5 — Enter plan mode
The orchestrator's pre-plan-mode phase is now complete. Invoke EnterPlanMode. The check-thought-bound.sh hook reads the marker written at Step 3 and allows entry.
Inside plan mode, author the plan file at the harness-designated path named in the plan-mode system message — ~/.claude/plans/<harness-slug>.md. Author the full Gate 0 (a-f) + Slice-I markers (CHAIN_SRC, SLICES, per-transition *:VALIDATED, COHERENCY) into that file.
Critical (2026-07-06): author ONLY at the harness path during plan mode. The harness-designated plan file is the ONLY path check-plan-readonly.sh permits during plan mode, and the ONLY path that does not trigger the harness's silent plan-mode exit: a successful Write to any other file (including a project-side Thoughts/…_PLAN.md) during plan mode drops the session to acceptEdits with NO ExitPlanMode and NO approval — bypassing every plan-gate + the approval prompt (reproduced live via A/B test; see bookkeeping-model.md §5). Do NOT write the plan to Thoughts/ during plan mode.
The project-side bookkeeping home is produced AFTER approval by the relocation CLI (Step 11), NOT during plan mode:
- Modes A/B:
<project_root>/Thoughts/<project_slug>_PLAN.md(or_<slice-id>_PLAN.mdper slice naming) - Mode C:
<project_root>/Thoughts/<slug>-<ts>_PLAN.mdwith abookkeeping: mode-cfrontmatter line (the frontmatter is whatbookkeeping_invariant.py's G2 case keys on to exempt the singleton plan family).
check-plan-gates.sh validates the plan at the harness path (found via the session manifest — find-session-plan.sh + track-plan-file.sh Arm 1). No project-side file exists until Step 11.
In Modes A/B, the plan opens with a <!-- GATE0:CHAIN_SRC --> block carrying mode, thought_file, discovery_src_hash, and alternative_n (Mode A only). The Diagnosis / Guiding Policy / Desired Outcome / OMTM sections quote the spine verbatim — no re-derivation.
In Mode C the block opens the plan too, but Mode C has no upstream to consume, so every upstream-less field carries the n/a sentinel: mode: C, thought_file: n/a, discovery_src_hash: n/a, alternative_n: n/a. Do NOT fabricate a 12-hex hash — check-plan-gates.sh accepts discovery_src_hash: n/a for Mode C, and the fresh Diagnosis is anchored by GATE0A:VALIDATED instead (see plan-gates.md "Mode-C discovery_src_hash sentinel"). The Diagnosis is authored fresh (Step 4), not quoted from a spine.
Coherent Actions Model column is mandatory in every mode. Regardless of the GATE0:CHAIN_SRC/GATE0SR:SLICES opt-in, the Coherent Actions table must use the 7-column schema and declare a Model per action (≥1 real family; em-dash only for mechanical rows). This is enforced unconditionally by check-plan-gates.sh on both ExitPlanMode paths, and the declared model is enforced at runtime by check-impl-models.sh.
Step 5b — Gate 0b2 producer: Deep-identify the Outcome Claims (all modes)
/plan is the 4th consumer of the shared claim-identification engine (siblings: /clarification Step 2, /extract-knowledge, /double-check Step 2c). Its site (/plan Gate 0b2) resolves to Deep (_claim_harvest.resolve_mode("plan:0b2") → MODE_DEEP), so the Outcome Claims (Gate 0b2) are produced by the engine, not hand-written — using the same skill-dispatched read-only-adapter (readonly-checker) pattern the two siblings carry, over the finalized Desired Outcome (Gate 0b). This step runs when you reach the ## Outcome Claims section, in every mode (A/B/C): the Desired Outcome is present in the plan file before 0b2 in all modes (code-enforced ordering GATE0B < GATE0B2), so there is one invocation site with no mode branching.
Deep (primary).
- Identify (engine contract at DEEP, AI-as-adapter). Dispatch the scoped claim-identification adapter — a parent-spawned
readonly-checkersubagent, isolated context, explicitmodel:pin (the fact-check-engine dispatch pattern, NOT the main AI improvising, NOT aclaude --printsubprocess) — to run the engine's two-stage contract at DEEP thoroughness over the Desired-Outcome text: Stage 1 identify candidate claims + the ambiguity-refusal gate (a candidate advances only if it resolves to a single checkable meaning; ambiguous ones are refused, not extracted); Stage 2 decontextualize/structure each survivor to a standalone atomic claim, scored on the six criteria (atomicity · verifiability · decontextuality · minimality · fluency · faithfulness). The claim set must carrythoroughness: "deep"— the persist seam refuses a NORMAL set at this Deep site (CF-4/A2; no silent "flip ships NORMAL"). The engine does not validate truth (A9). - Consolidate (moderate, producer-never-verifies). Run
${KIT_HOOKS_DIR}/_claim_consolidate.pyover the DEEP output —dedup_exactplus a scoped consolidator dispatch that merges redundant DEEP splits while preserving completeness (gated by an independent completeness spot-check — a THIRD actor). Tames DEEP verbosity without dropping a distinct fact. - Emit — one Edit (single list, A2). In a single Edit to the plan file, write the
## Outcome Claimssection carrying ONE fenced```jsonclaim-set (the consolidated engine output — this IS the single source of truth; there is **no** separate re-worded markdown claims table and **no**GATE0B2:CLAIM_SETmarker — both retired 2026-07-18). The json uses the_claim_persist.pypayload schema (source_type,source_path, six-flagclaims,refused,thoroughness: "deep") **plus two /plan-scoped per-claim fields**: a non-empty **id** (e.g."id": "C1"— the token a Gap'sClaimcell references) and a non-empty **addresses_diagnosis** (the aspect of the Diagnosis that claim resolves — **you** author this; the engine does not know the Diagnosis). Both are **code-required** bycheck-plan-gates.sh(via_plan_claim_gate.py); a claim missing either blocks ExitPlanMode. Place the json inside the## Outcome Claimssection (betweenGATE0B:OUTCOMEandGATE0B2:CLAIMS); the coverage sentence (**Coverage:** …) still follows it. Do **not** run a separate in-mode_claim_persistvalidate —check-plan-gates.shschema+DEEP-validates the json at ExitPlanMode (and writes the success run-record for free); a second call would double-run.
Automatic fallback (retained, reason-logged — never a silent skip). If the DEEP adapter genuinely can't run (engine error, zero BACKWARD claims, or the ambiguity gate refuses everything) — or you deliberately opt out — hand-decompose the Outcome Claims as a plain markdown table (the only list on this degraded path) AND emit, inside the ## Outcome Claims section, a non-empty claim_fallback_reason: <why> line instead of the fenced json. The reason must name a concrete engine-failure category (engine-error:/zero-claims:/timeout:/plan-mode-seam-blocked:) or the explicit override user-acknowledged-skip:. check-plan-gates.sh accepts that branch AND records a site-keyed fallback_record receipt to the plan's .claim-runs.md (parity with the siblings; the fact-check BYPASSED pattern). The Claims↔Gaps bidirectional check still runs on the fallback path (ids read from the markdown table), but the per-claim addresses_diagnosis requirement does not apply there (no structured claims).
This step does not change the GATE0B2:CLAIMS / GATE0B2:VALIDATED marker contract or the Step-6 factcheck-plan-step 0B2 coverage dispatch below — the single json list (schema) and the GATE0B2:VALIDATED verdict (coverage) are independent concerns that coexist.
Step 6 — Per-transition verification dispatches (inside plan mode, between structural steps)
After each structural section is drafted in the plan file, run the appropriate factcheck-plan-step dispatch BEFORE drafting downstream sections:
| After section | Dispatch | Required modes | Marker written |
|---|---|---|---|
| Diagnosis (Mode C only) | factcheck-plan-step 0A |
C only | <!-- GATE0A:VALIDATED --> |
| Outcome Claims (0B2) | factcheck-plan-step 0B2 |
A, B, C | <!-- GATE0B2:VALIDATED --> |
| Gap Analysis (0C) | factcheck-plan-step 0C |
A, B, C | <!-- GATE0C:VALIDATED --> |
Each dispatch is a parent-spawned 1 Sonnet + 1 Opus dispatch (Opus = primary thinker, Sonnet = support — independent cross-check), subagent_type: readonly-checker, isolated context, explicit per-spawn model: pinning. Early-exit on Round-1 PASS. Plan-kind max rounds = 3.
Each checker receives:
- The plan file path (so it can read the section under verification)
- The
--againsttext per gate (0A: codebase + concept; 0B2: Desired Outcome; 0C: Outcome Claims + existing codebase) - The rubric: "reason through whether the section holds against the
--againsttext, then emit as the final line exactlyVERDICT: PASSorVERDICT: DISCREPANCY(append a short bulleted discrepancy list if DISCREPANCY). The finalVERDICT:token is authoritative — reasoning that merely mentions the word 'discrepancy' does not decide the verdict."
You do NOT decide the verdict — the shared engine does (plan-validation-engine-consumer). After the checkers return, capture each checker's raw output into a JSON list and hand it to the CLI, which computes the round verdict via the shared engine aggregator (_factcheck_engine.aggregate_round_verdict, plan kind) and writes a code-owned R<N>.md engine receipt keyed to (gate, section):
printf '%s' '[{"model":"sonnet","verdict":"<checker-1 raw output>"},{"model":"opus","verdict":"<checker-2 raw output>"}]' \
| python3 ${KIT_HOOKS_DIR}/pre_plan_gates.py factcheck-plan-step <GATE> <PLAN_FILE> --round <N> --max-rounds 3(or pass --checkers-json <FILE> instead of stdin). Read the returned verdict: DIRTY → retry (round N+1, diff-only after round 2); ESCALATE → surface the discrepancies verbatim and stop; PASS → write the verdict line into the *:VALIDATED marker block via Edit. That plan-text verdict line is informational only — check-plan-gates.sh reads the engine receipt, not the typed line (S2), so a hand-typed verdict cannot unlock approval.
Same mechanism, on demand (0D/0E and any section). The dispatch→CLI→receipt spine above is not limited to the three per-transition gates. factcheck-plan-step now also accepts 0D (Guiding Policy) and 0E (Coherent Actions) — the former blind-spot sections (S3) — so any kernel section can be validated in isolation with the same 1-Sonnet+1-Opus dispatch, the same VERDICT: rubric against that section's --against, and the same engine-computed receipt. This is what delivers "validate one dedicated section" from the Desired Outcome.
Step 6b — On-demand section & whole-plan validation (S3/S4)
The per-transition dispatches (Step 6) and the final coherency dispatch (Step 7) are the two standing uses of one mechanism. That same mechanism is also available on demand — a per-transition check is a per-section validation with a fixed --against, and the final coherency check is whole-plan validation — not a second pathway.
- Validate one section. Run
factcheck-plan-step <GATE>(any of0A/0B2/0C/0D/0E) orfactcheck-plan-coherency(0G) with the dispatch→CLI→receipt spine. The CLI returns the engine-computed verdict scoped to that section and writes its receipt. - Validate the whole plan (strict rollup — never an omnibus judge). Validate each kernel section with its own per-section dispatch, then roll up the per-section engine receipts: the whole plan PASSes only when every section's latest receipt is a converged PASS (or a recorded override); any non-PASS names the offending section. Do not dispatch one checker over the entire plan body — per-section prompts stay under the codebase's measured ~355 KB checker-prompt cap, and Anthropic evals guidance favors decomposed dimension-by-dimension checks over a single compound judge. (The gate at ExitPlanMode is this rollup:
check-plan-gates.shreads each section's receipt independently.) - Codebase anchors for code-touching sections (C6). When a section makes claims about the codebase (Diagnosis, Gap Analysis, Coherent Actions that cite files), ground its checkers against that section's enumerated codebase anchors — the
Source Locationcells of the Gate-1 table for the claims that section relies on — passed in the checker's--against. A code-touching section's PASS requires a non-empty anchor set so "reads coherent but doesn't match real code" is caught against the actual files, not just the plan's internal prose.
Engine-consumer — define-once-propagate (S5, C7). Plan validation is now a consumer of the one shared validation engine, not a fork. The verdict is computed by the engine's own aggregator (_factcheck_engine.aggregate_round_verdict) and recorded by the engine's own marker writer (_write_round_file, schema factcheck-convergence.md §7); when a section needs claim-level rigor, identify its claims through the same in-session claim-identification port that Gate 0b2 uses at Step 5b (_dc_claim_seam.build_scope_addendum / _claim_engine.in_session_bootstrap, DEEP) rather than a plan-specific copy. Because convergence, axes, and the marker live in the one engine, improving the engine once (its convergence rule, an axis, how reviewers are spread) improves plan validation automatically — there is no plan-side tally to keep in sync. Do not reintroduce a prose convergence count in the orchestrator: capture checker outputs, hand them to the CLI, read the engine's verdict.
Step 7 — Final coherency dispatch (Gate 0G)
After the plan is fully assembled (Coherent Actions + Design Review + Gate 1/2/3), dispatch the final coherency check via factcheck-plan-coherency:
Spawn 3 Sonnet + 1 Opus checkers in parallel via Agent tool,
subagent_type: readonly-checker, isolated context, explicit per-spawnmodel:pinning. Default rounds = 2 (--coherency-rounds; plan-kind ceiling 3 perfactcheck-convergence.md§4). Each checker receives the plan file +--against= "the Coherent Actions solve the Diagnosis and deliver the Desired Outcome while respecting the Guiding Policy".Per-axis structured output (mandatory for all checkers). The checker prompt requires each checker to return three field-shaped axis verdicts plus a per-axis citation — NOT a compound PASS/DISCREPANCY string. The three axes:
axis_1 (Coherent Actions solve the Diagnosis): PASS | DISCREPANCY citation: <plan-file location + 1-line quote> axis_2 (Coherent Actions deliver the Desired Outcome): PASS | DISCREPANCY citation: <plan-file location + 1-line quote> axis_3 (Coherent Actions respect the Guiding Policy): PASS | DISCREPANCY citation: <plan-file location + 1-line quote>The Opus checker (and only Opus) additionally returns a cross-axis coherence assessment — the system-level judgment that catches inter-axis tensions per-axis decomposition cannot:
cross_axis_coherence: COHERENT | TENSION | BROKEN cross_axis_notes: [1-3 sentences naming any inter-axis tension or break]Definitions:
- COHERENT — the three axes hold together as one chain; the Coherent Actions deliver the Outcome via the Policy in a way that flows causally from the Diagnosis.
- TENSION — axes pass individually but inter-axis friction exists (e.g., an action that delivers the Outcome but does so via a path the Guiding Policy only loosely supports). Surface for ESCALATE.
- BROKEN — axes pass individually but the chain doesn't actually flow (e.g., the Actions solve the Diagnosis and deliver the Outcome, but the two are causally disconnected — the chain is two parallel checks pretending to be one). Surface for ESCALATE.
Rationale: Anthropic prompt-engineering guidance favors structured output over compound predicates; this schema turns "averaging" into a code-checkable AND-gate. Opus carries the cross-axis role because the existing
/double-checkOpus scope-coverage pattern (~/.claude/skills/double-check/SKILL.md:142-162) already assigns Opus the system-level judgment that Sonnets aren't tasked with. The 3 Sonnets give per-axis breadth (3 independent perspectives per axis); the 1 Opus adds the system-level catch.Fold into a final
VERDICT:token (mandatory). After the per-axis (and, for Opus, cross-axis) block, each checker emits as its final line exactlyVERDICT: PASSorVERDICT: DISCREPANCY. The rule the checker applies: a Sonnet emitsVERDICT: DISCREPANCYiff any of its three axes is DISCREPANCY; the Opus emitsVERDICT: DISCREPANCYiff any of its three axes is DISCREPANCY ORcross_axis_coherence ∈ {TENSION, BROKEN}. This is what lets the shared engine reproduce the 0G AND-gate from one token per checker (the plan-kind_verdict_bucketreads the finalVERDICT:line — CoT-safe).Gate-PASS logic (code-computed by the shared engine, C4). You do not decide the 0G verdict. The AND-gate — all 12 per-axis verdicts PASS and Opus
cross_axis_coherence= COHERENT — is enforced structurally: every failing axis or a cross-axis TENSION/BROKEN makes that checker'sVERDICT:token DISCREPANCY, and the engine aggregates ANY DISCREPANCY → DIRTY (round < 3) / ESCALATE (final round). 0 discrepancies → PASS.Early-exit on Round-1 PASS.
Persist by handing the captured checker outputs to the CLI (engine computes the verdict). Capture each checker's raw output into a JSON list and pipe it in; the CLI computes the round verdict via the shared engine aggregator and writes the code-owned
0Gengine receipt (plus an audit row to_verifications.jsonl):printf '%s' '[{"model":"sonnet","verdict":"<c1 raw>"},{"model":"sonnet","verdict":"<c2 raw>"},{"model":"sonnet","verdict":"<c3 raw>"},{"model":"opus","verdict":"<c4 raw>"}]' \ | python3 ${KIT_HOOKS_DIR}/pre_plan_gates.py factcheck-plan-coherency \ <SESSION_ID> <PLAN_FILE_PATH> --round <N> --max-rounds 3 --claims-checked <N>(or
--checkers-json <FILE>). Read the returnedverdict.Write the verdict block into the plan file inside
<!-- GATE0G:COHERENCY -->via Edit (verdict + rounds + checker_models + cross_axis_coherence). This block is informational —check-plan-gates.shreads the engine receipt (S2), and a non-PASS receipt no longer unlocks approval (fixes the old E4/0G threshold hole where DIRTY/ESCALATE were silently admitted).
On ESCALATE — two-option user choice (do NOT default-stop). Surface the discrepancies verbatim (including the cross-axis disposition if Opus flagged TENSION/BROKEN) and present the user:
- (a) One more round — re-dispatch the same 3 Sonnet + 1 Opus checkers; round counter increments past
--coherency-rounds(ceiling 3); fresh isolated checkers. After the new round, re-evaluate. - (b) Proceed as is — record an explicit operator override so the gate admits the section (a non-PASS receipt alone will NOT unlock approval — S2):
Then write thepython3 ${KIT_HOOKS_DIR}/pre_plan_gates.py plan-override 0G <PLAN_FILE_PATH> \ --reason "<the user's stated reason for proceeding with the unresolved discrepancies>" \ --session <SESSION_ID>verdict: ESCALATE+ discrepancy list + cross-axis disposition into<!-- GATE0G:COHERENCY -->for the human record. The override is the recorded, explicit escape from the fail-closed gate — never a silently-typed verdict line.
Step 8 — Spine cross-link (Modes A/B only)
Before requesting ExitPlanMode, in Modes A/B only, append a wikilink to the spine's # Implementation Details section:
python3 ${KIT_HOOKS_DIR}/pre_plan_gates.py link-plan-to-thought <SESSION_ID> <PLAN_FILE_PATH>The CLI is idempotent (no-op if already linked) and mode-aware (no-op in Mode C). This is the only spine writeback after Step 2's /solution-design invocation — check-plan-readonly.sh permits it because the orchestrator runs OUTSIDE plan mode for the writeback step (the CLI is invoked from chat, not from plan mode's Edit/Write).
Note: alternatively, this step may run immediately after Step 3 (before entering plan mode) — both placements satisfy the invariant that check-plan-readonly.sh does not block the spine write. Pick whichever fits the conversational flow.
Step 9 — Request ExitPlanMode
The plan is complete and all required markers carry verdict=PASS. Request ExitPlanMode. The existing permission-plan-gate.sh + check-plan-gates.sh validator runs and enforces the new Slice-I per-mode required-set (see plan-gates.md "Per-mode required-set (when CHAIN_SRC is present)").
After ExitPlanMode is granted, post-plan-uxgate.sh (PostToolUse) sets the post_plan_choice marker.
Step 10 — F10 v5 AskUserQuestion (mandatory next call)
Immediately after ExitPlanMode is granted — before any other tool runs — invoke AskUserQuestion. The check-post-plan-pending.sh hook blocks every other tool until this happens.
Verbatim AskUserQuestion contract:
- Question: "Plan approved. What's next?"
- Header: "Next step"
- multiSelect: false
- Options:
- (a) Label: "Generate handoff prompt (Recommended)"
Description: "Compose a fresh-Implementation-session prompt via /prompt-for-handoff. Modes A/B pass the bound _thought file (state-aware); Mode C passes the plan file (state-agnostic mode per ~/.claude/skills/prompt-for-handoff/SKILL.md:27). Run /close in this session before starting the new one." - (b) Label: "Run /close and /clear, pick another topic"
Description: "Wrap this session (diary + TODO + git commit via /close) then /clear to start fresh on a different topic. The current plan stays linked in the spine and is ready for a future Implementation session." - (c) Label: "Continue in this session"
Description: "Exit plan mode and implement here. Suited to small/single-action plans where carrying plan-mode context is fine and a fresh session would be overhead. The plan stays linked in the spine."
- (a) Label: "Generate handoff prompt (Recommended)"
Step 11 — Act on user choice + clear marker
After the user picks:
- Relocate the plan to its durable
Thoughts/home (all choices). The plan was authored at the harness path~/.claude/plans/<harness-slug>.md; now that it is approved, relocate it — this is where the project-side_PLAN.mdfirst comes to exist (it is NOT written during plan mode):
Modes A/B derive the destination slug frompython3 ${KIT_HOOKS_DIR}/pre_plan_gates.py relocate-plan-after-approval <SESSION_ID> <HARNESS_PLAN_PATH> [--dest-slug <slug>]topic_state.topic_slugautomatically and add the# Implementation Detailsspine back-link. Mode C: pass--dest-slugif you already have a good bookkeeping slug; otherwise the CLI auto-derives one from the plan's own problem statement (its H1 title). It only errors if that derivation finds nothing usable — then supply--dest-slug(optionally viaAskUserQuestion). The CLI refuses unless a real ExitPlanMode grant was recorded, so it only runs post-approval, outside plan mode (wherecheck-plan-readonly.shno longer applies). - Arm the per-run execution pointer (register-presence-gated — all choices). This is the on-disk state the code-enforced
/execute-planentry gate reads later, so it must be armed here (post-relocation) — the gate then holds whether or not a future session remembers to route through/execute-plan. The guarantee is code's (S1+S3); this arming is what feeds it. First test whether the plan is a multi-slice plan worth gating, reading the relocated durable copy:printf '%s' '{"spine_path":"<relocated_plan_file>"}' \ | python3 ~/.claude/skills/execute-plan/run.py register-presencepresent: true(≥2 non-closing slices — a real walk) → arm the pointer for the approving session:printf '%s' '{"owner_session_id":"<SESSION_ID>","surface_path":"<relocated_plan_file>","total_slices":<plan slice count>,"execution_pending":true,"walking_session_id":null,"current_slice_id":null}' \ | python3 ~/.claude/skills/execute-plan/run.py set-active-runAdd
"worktree_root":"<worktree root>"to the payload when the plan lives in a git worktree (the run_id keys on surface_path + worktree_root).execution_pending:trueis what the entry gate keys on;walking_session_idandcurrent_slice_idstay explicitlynull— no session is walking yet and no slice is checked out (a walker is elected only when/execute-planactually begins, never here).Worktree auto-placement + land-readiness (S7 A14/A13,
/plan --worktree-opt-in). When/plan --worktreeis used on a harness topic, place the session into the topic's worktree (thework-start/SKILL.md:255-259two-stepplace→ guardedcdform, harness binding the configured source path) so the plan is authored/executed inside its isolated workspace, and setworktree_rootabove from the resultingplacepath. Record the topic's advisory land-readiness for a later resume —python3 ${KIT_HOOKS_DIR}/land_readiness.py write <topic-slug> in-progress(a plan just authored isin-progress; it becomesready-to-verify-then-landonly once the slices are implemented + green, recorded by/close//prompt-for-handoff, S7 A13). The record is an advisory routing hint only — the A6 gate re-computes green/red at land time. Mode C caveat: a Ninja-Plan has no up-front topic slug (the durable slug is minted at Step-11 relocation), so--worktreeplacement in Mode C uses the relocation slug and places at/after relocation; without--worktree, no placement (default unchanged). Projects-side binding activates with S9.present: false→ arm NOTHING (non-regression C6). But first check the register-presence output for a malformed multi-step register — a plan that LOOKS multi-step yet did not resolve, which must be surfaced LOUDLY rather than silently downgraded to a one-shot (A2, C2):Fail-loud warning (A2) — if the register-presence output shows
raw_non_closing_count≥ 2 (there are ≥2 non-closing rows under a recognized register, but the tolerant reader could not turn ≥2 of them into valid slices) ORpointer_broken: true(aslice_register_refthat did not resolve), emit a prominent operator warning naming the plan file and pointing at/execute-plan, e.g.:⚠️ This plan looks multi-step (
raw_non_closing_countnon-closing rows detected) but its slice register did not resolve to ≥2 valid slices — so step-by-step execution was not armed. The register is likely malformed (missing aType/Depends oncolumn, or a brokenslice_register_ref). Fix the register in<relocated_plan_file>and re-run, or drive execution manually via/execute-plan. It is not being silently treated as a one-shot.This is a warning, not a block — after surfacing it, still arm nothing and continue (the operator decides; block-with-override, git-policy §8). The
warningfield in the register-presence output (already printed to stderr for the broken-pointer case) carries the loud text for the pointer case; compose the malformed-register text fromraw_non_closing_count.Genuine single-work / trivial plan —
raw_non_closing_count< 2 (0 or 1 non-closing row, e.g. a canonical1 work slice + S-final) and no broken pointer → arm nothing silently (C6: no spurious warning, no gating). Skip to clearing the marker.
- Clear the marker:
python3 ${KIT_HOOKS_DIR}/pre_plan_gates.py clear-post-plan-choice <SESSION_ID> - If (a): invoke
/prompt-for-handoff <thought_file>(Modes A/B) or/prompt-for-handoff <relocated_plan_file>(Mode C — state-agnostic mode). - If (b): remind the user to run
/closethen/clear. Do not auto-invoke/closeor/clear— they are user-issued commands. - If (c): no further action — proceed with implementation in this session. The marker auto-clears when
AskUserQuestioncompletes (clear-post-plan-marker.sh).
Output Contract
The skill produces no JSON output. Its observable effects are:
- During plan mode: the plan authored at the harness path
~/.claude/plans/<harness-slug>.md, carrying the Slice-I markers andverdict: PASSon every required gate. - After approval (Step 11 relocation): a
<project_root>/Thoughts/<project_slug>_PLAN.md(Modes A/B) or<project_root>/Thoughts/<slug>-<ts>_PLAN.mdwithbookkeeping: mode-cfrontmatter (Mode C) durable copy. - A wikilink under
# Implementation Detailsof the spine (Modes A/B), added by the relocation CLI. - An
ExitPlanModegrant. - An
AskUserQuestionsurface for the post-plan UX gate. - An append to
~/.claude/state/plan_validation/_verifications.jsonlper coherency round.
Edge Cases
- E1 — Marker orphan on session crash.
~/.claude/state/post_plan_choice/<SID>.markermay survive a crash./session-startshould sweep stale markers (cross-slice carry to Slice J). - E2 — Two plans per session. Plan mode in one session writes one plan file (
check-plan-readonly.shenforces the allowed paths). Theplan-mode-initmarker is one-per-session. - E3 — Mode C authoring + relocation. Mode C plans are AUTHORED at the harness path
~/.claude/plans/<harness-slug>.mdduring plan mode (DS5 reversed 2026-07-06 — authoring atThoughts/during plan mode triggers the silent plan-mode exit; seebookkeeping-model.md§5), then relocated after approval to<project_root>/Thoughts/<slug>-<ts>_PLAN.mdwithbookkeeping: mode-cfrontmatter byrelocate-plan-after-approval(Step 11;--dest-slugrequired for Mode C; auto-createsThoughts/if missing).link-plan-to-thoughtis a no-op for Mode C (no spine). - E4 — Gate 0G ESCALATE (receipt-based, plan-validation-engine-consumer). Surface discrepancies verbatim.
check-plan-gates.shreads the engine receipt for each Slice-I gate (0A/0B2/0C/0G), not an author-typedverdict:line, and admits ONLY a converged PASS (or a recorded operator override) — a DIRTY/ESCALATE/missing receipt blocksExitPlanMode, naming the section (this fixes the old threshold hole where the 0G grep silently admitted DIRTY/ESCALATE). To proceed with unresolved discrepancies the user must explicitly accept: record it withpre_plan_gates.py plan-override 0G <plan> --reason "…"(Step 7 option (b)). Theverdict: ESCALATEline in the plan text is the human record; the recorded override is what unlocks approval. - E5 — Coherent Action missing validation gate. The 7-column row schema (
check-plan-gates.shA2 validators) blocksExitPlanModeif any row has fewer than 7 columns. - E6 — Unsupported / missing Model value. The Model-column allowlist (
sonnet,opus,haiku, or—for mechanical rows —check-plan-gates.sh) blocks on unknown values. The Model column is mandatory for every plan in every mode (not opt-in): an empty Model cell is rejected, and at least one action must declare a real family. Enforced on both ExitPlanMode paths (permission-plan-gate.sh+stop-plan-gate.shboth callcheck-plan-gates.sh). - E7 — Hotkey-entry edge.
check-thought-bound.shPreToolUse matcher fires forEnterPlanModeregardless of trigger source — hotkey and/plango through the same gate. - E8 — Plan-naming collision. Slice-id appears in the filename (
_I_PLAN.md); cross-session isolation uses--worktreeperplan-gates.mdcross-session paragraph. - E9 — PostToolUse on ExitPlanMode unsupported. Spike-verified 2026-05-22 per Claude Code hooks docs. Fallback if the harness changes: Stop event hook scanning the transcript for the most-recent
ExitPlanModepermission grant. - E10 — Checker uses default model.
factcheck-plan-coherencyandfactcheck-plan-stepalways spawn with explicitmodel:parameter.check-impl-models.shPostToolUse onAgentis the runtime backstop for all plans that declare a model — the implementation-model contract is unconditional (not gated on theGATE0SR:SLICESopt-in). - E11 —
/planinvoked from inside a subagent. Refuse. The skill is main-session-only — recursive/solution-designinvocation is bounded by Phase 2's own main-session-only invariant (solution-design/SKILL.md:14). - E12 — Stale
discovery_src_hashafter Phase 1 edit. Recomputed everyplan-mode-initcall from the current spine via_discovery_locked_fields_hash(pre_plan_gates.py:178-202). - E13 — Multiple
### Solution Alternative Nblocks. Read the LATEST (max N) persolution-design/SKILL.md:53monotonic-N invariant. - E14 —
link-plan-to-thoughtrace when two sessions write the same spine. The CLI is idempotent (if wikilink_line in text: return already_linked). The plan file basenames are slice-distinct.
See also
~/.claude/rules/plan-gates.md— canonical Gate 0/1/2/3 + Slice-I marker spec~/.claude/rules/code_first_architecture.md— hexagonal + producer-never-verifies + grading order~/.claude/rules/factcheck-convergence.md— §4 stopping condition, §7 R.md schema~/.claude/rules/prompt-engineering.md— §139 prefer general instructions over prescriptive steps${KIT_HOOKS_DIR}/pre_plan_gates.py—plan-mode-init,link-plan-to-thought,factcheck-plan-step,factcheck-plan-coherency,clear-post-plan-choice,relocate-plan-after-approval~/.claude/skills/execute-plan/run.py—register-presence(route-gating predicate) +set-active-run(Step 11 arms the per-run execution pointer the/execute-planentry gate reads)${KIT_HOOKS_DIR}/check-thought-bound.sh— PreToolUseEnterPlanMode${KIT_HOOKS_DIR}/check-impl-models.sh— PostToolUseAgent, runtime model enforcement${KIT_HOOKS_DIR}/post-plan-uxgate.sh— PostToolUseExitPlanMode, F10 v5${KIT_HOOKS_DIR}/check-post-plan-pending.sh— PreToolUse.*, F10 v5~/.claude/skills/solution-design/SKILL.md— Phase 2 (Mode B's/solution-designinvocation)~/.claude/skills/prompt-for-handoff/SKILL.md:27— state-agnostic mode for Mode C