name: lazy-batch
description: Autonomous orchestrator for the AlgoBooth (or any queue.json-driven) feature pipeline. Loops on lazy-state.py, spawns one Opus subagent per cycle, and drives the full tail (/spec → /plan-feature → /execute-plan → /mcp-test → mark_complete). A halt for any reason other than max-cycles presents an AskUserQuestion resolution path and resumes — only max-cycles, all-features-complete, environment-exhaustion, and missing-queue remain clean stops. Terminal action is mark_complete, gated by the MCP-coverage audit + completion-integrity gate. (The /retro step is unwired — 2026-06.)
argument-hint: <max-cycles, e.g. 10> [--allow-research-skip] [--adhoc "" — enqueue an ad-hoc task at the top of the queue] [--park] [--per-feature-cycle-cap ] [--strict-research-halt]
plan-mode: never
model: opus
allowed-tools: ["Bash", "Read", "Agent", "Write", "Edit", "AskUserQuestion"]
Lazy Batch — Autonomous Pipeline Orchestrator
Drives the per-feature autonomous tail (/plan-feature (= /spec-phases + /write-plan in one cycle) → /execute-plan → /mcp-test → mark-complete) by looping on ~/.claude/scripts/lazy-state.py. Each cycle spawns an Opus subagent that invokes the named sub-skill; the orchestrator (this skill, running in the main session) never touches source code, never invokes a skill directly, and never parses sentinel files manually.
Step ordering note: the /retro step has been UNWIRED (operator decision, 2026-06). Once all phases are complete the pipeline routes directly to /mcp-test (Step 9 MCP gate); lazy-state.py never emits retro-feature. /mcp-test only runs on workstation (cloud defers). Behavior inside the loop is otherwise unchanged — the orchestrator dispatches whatever lazy-state.py returns. (The /retro-feature skill remains in the catalog; git history is the restore path.)
This is the workstation orchestrator. The cloud variant is /lazy-batch-cloud (under repos/algobooth/.claude/skills/lazy-batch-cloud/); the two are coupled per CLAUDE.md.
HARD CONSTRAINTS (non-negotiable)
The orchestrator MAY use
Write/EditONLY on sentinel files (BLOCKED.md,DEFERRED_NON_CLOUD.md,VALIDATED.md,COMPLETED.md,NEEDS_RESEARCH.md,NEEDS_INPUT.md,RETRO_DONE.md,SKIP_MCP_TEST.md,MCP_TEST_RESULTS.md) insidedocs/features/, AND onROADMAP.md/ per-featureSPEC.md/PHASES.mdstatus lines when performing the__mark_complete__action (which is a documentation-level update by definition, not a source-code edit).NEEDS_INPUT.mdmay additionally be appended to (not overwritten) with a## Resolutionsection by Step 1g (decision-resume mode) afterAskUserQuestionreturns — or by the Step 1g D7 scope resolution (resolved_by: completeness-policy, no question); the orchestrator then dispatches a Sonnet subagent to propagate the choice into SPEC.md / PHASES.md and neutralize the sentinel.BLOCKED.mdmay likewise be appended to (not overwritten) with a## Resolutionsection by Step 1h (blocked-resolution mode) afterAskUserQuestionreturns — or by the Step 1h D7 sequencing-only auto-resolution (no question); the orchestrator then enacts the chosen resolution path and neutralizes the sentinel by rename (lazy-state.py keys the halt on theBLOCKED.mdfilename). A Defer (queue reorder) resolution is now a deterministic out-of-cyclelazy-state.py --reorder-queue --id <id> --to tailBashcall the orchestrator runs directly (the script callslazy_core.reorder_queue+_atomic_write; gated byrefuse_if_cycle_active), NOT a dispatched Opus apply-resolution subagent — and the orchestrator still NEVER hand-editsqueue.json(it calls the script). Resolution paths that DO require source/SPEC/PHASES edits (e.g./add-phase) still dispatch an Opus subagent. All otherWrite/Editoperations — source code, test files, plan files, PHASES.md — require subagent dispatch (the Step 1g apply-resolution subagent is the dispatch that authorizes the SPEC/PHASES edits flowing from a decision).The orchestrator MUST NOT invoke any
/skilldirectly via theSkilltool. Every sub-skill invocation goes through a spawnedAgentsubagent. This keeps the orchestrator's context lean across many cycles. Pseudo-skills (__*__) are NOT real skills and are handled inline per Step 1c.5 — they are sentinel-file edits + commits, not skill dispatches.The orchestrator MUST NOT manually parse SPEC.md, PHASES.md, or plan files. State inference is exclusively via
lazy-state.py. Sentinel files MAY be read by the orchestrator to confirm a write or to drive a pseudo-skill action.One cycle = one subagent dispatch FOR REAL WORK SKILLS. Do not chain multiple sub-skills inside a single cycle; the state machine drives that progression across cycles. Pseudo-skill cycles (sentinel writes) are not subagent dispatches at all — they are inline orchestrator actions that count as one cycle each.
Interactive prompts are scoped to the resolution modes — decision-resume (Step 1g), blocked-resolution (Step 1h), and operator-directed halt-resolution (Step 1i) — ONLY for the orchestrator itself. The guiding rule: a halt for ANY reason other than
max-cycles(and the genuine all-done success / environment-exhaustion / no-queue stops listed in Step 1i) presents the operator anAskUserQuestionresolution path and continues the loop, rather than dead-ending — except that scope-class decisions and sequencing-only blockers are auto-resolved per~/.claude/skills/_components/completeness-policy.md(D7), not asked: the standing policy reduces questions, never adds them, and the resolution modes ask only for what remains product-class. Outside Step 1g / 1h / 1i, the orchestrator MUST NOT callAskUserQuestion— with four additional permitted uses: (i) the one-time echo-back confirmation when a mid-run operator message implies a budget change, standing resolution mode, or early stop (Step 0 standing-directive protocol); (ii) the budget-and-queue guard question when the run would otherwise end with budget and queue both remaining; (iii) the Step 0.45--enqueue-adhoctask-details prompt when--adhocis supplied with no text and the task cannot be unambiguously inferred from the conversation; and (iv) the Step 5 in-session resume multi-feature disambiguation question when research arrives for an ambiguous feature ("which feature does this research belong to?"). Uses (i) and (ii) are orchestrator-level confirmations of operator intent; uses (iii) and (iv) are bounded single-question disambiguation prompts at well-defined pre-loop and resume boundaries. None are resolution-mode decisions about feature/bug content. Inside Step 1g, the orchestrator MUSTAskUserQuestionagainst a well-formedNEEDS_INPUT.md(rich body per~/.claude/skills/_components/sentinel-frontmatter.md), append a## Resolutionsection, dispatch the apply-resolution subagent, and then continue the loop — Step 1g no longer halts the orchestrator. Inside Step 1h, the orchestrator MUSTAskUserQuestionfor the resolution path against aBLOCKED.md(re-printing its body first), record the choice, dispatch the apply-resolution subagent to enact it, and continue the loop —blockedno longer halts the orchestrator either (except the operator-chosen "Halt for manual fix" path). (The legacy halt-on-needs-input behavior is gone; the user retains decision-making autonomy viaAskUserQuestion, the apply step is mechanical propagation.) This constraint scopes the orchestrator, not subagents it dispatches. A/specsubagent dispatched at state-machine Step 4.5 (stub-spec detected) is allowed and expected to callAskUserQuestionduring Phase 1 brainstorming — that's the legitimate design-conversation channel for a SPEC whose baseline doesn't exist yet. The orchestrator dispatches/specexactly the same way it dispatches/execute-plan(one Agent call per cycle); whatever the dispatched skill does internally is its own contract. See "Stub specs vs structured-research-pending specs" below for the disambiguation rule.The orchestrator MUST print a Zero-Context Operator Briefing AND re-print the load-bearing context to chat BEFORE calling
AskUserQuestion. The operator may have been away for hours and retains NO session context (and may be reading on mobile, whereAskUserQuestiontruncates). In Step 1g the briefing (step 2a of the decision-resume component) catches them up from zero — what's being worked, why we halted, every option with pros/cons and fit against the original requirements, and a recommendation — followed by the verbatim## Decision Contextre-print (step 2b); theAskUserQuestionoption set MUST exactly match the options presented in the briefing (same labels, 1:1 — no option may appear in the UI that wasn't explained in chat first). Never callAskUserQuestionagainst a malformedNEEDS_INPUT.md(one missing the## Decision ContextH2 with H3 subsections matchingdecisions:1:1) — surface the malformation as a quality issue and halt instead (see Step 1g.1). In Step 1h the load-bearing context is theBLOCKED.mdbody verbatim (no mandated rich-body schema — a thin body is NOT a malformation halt; re-print whatever is there and note in chat if it is sparse); in Step 1i it is the obstacle context the sharedhalt-resolution.mdmandates. The same zero-context briefing discipline (catch the away operator up from zero before asking) applies to Step 1h/1i.NEVER actively wait for filesystem events. The orchestrator MUST NOT use
Monitor,sleep,wait, polling loops, or any other mechanism to block while research is uploaded. Research arrives on the user's own timeline — they may be away from their device for hours or days. Whenqueue-blocked-on-researchorneeds-researchfires, the orchestrator halts cleanly (Step 1f / Step 4). The resume signal is chat-driven, not filesystem-driven: if the user's next message in the same conversation supplies research (file attachment, pasted text, or absolute path), the in-session resume protocol (Step 5) fires immediately; otherwise the user's next/lazy-batchinvocation is the resume signal. Responding to a chat message is NOT polling — it is a single-turn event, not an active wait.TWO session-global monotonic counters replace the single
cyclecounter. Both are initialized once in Step 0 and NEITHER is ever reset on feature transitions.forward_cycles— counts pipeline-advancing work. Ceiling:max_cycles. Incremented by: (a) real-skill dispatch cycles (Step 1e step 5) and (b) pipeline-advancing pseudo-skills at Step 1c.5 (__mark_complete__,__mark_fixed__,__write_deferred_non_cloud__(cloud variant only — workstationlazy-state.pynever emits this),__write_validated_from_results__,__write_validated_from_skip__,__grant_skip_no_mcp_surface__,__flip_plan_complete_cloud_saturated__). Capped at Step 1c (if forward_cycles >= max_cycles→ the existing max-cycles halt).meta_cycles— counts resolution / recovery / audit / cleanup work. NO ceiling — uncapped by design (operator decision 2026-06-14). Incremented by: Step 1g (decision-resume), Step 1h (blocked-resolution), Step 1i (operator-directed halt-resolution), LOOP-DETECTED / Step 1e.4a recovery dispatches, the input-audit cycle at Step 1d.5, and the stale-plan flip pseudo-skill__flip_plan_complete_stale__. The meta loop is NOT bounded by a meta cap; the run's only hard stop is theforward_cycles >= max_cyclescap at Step 1c.meta_cyclesis still tracked and displayed (as a bare count), but there is NOif meta_cycles >= …halt anywhere — Step 1g/1h/1i have no meta-cap check.- Input-audit (Step 1d.5): audits are NOT counted as separate cycles (they share the real-skill cycle's slot in
cycle_logand do NOT increment either counter). This keeps audit costs outside the budget. - Running total for cycle_log index: use
forward_cycles + meta_cyclesas the monotonicNin cycle-log entries and per-cycle headings (i.e., the N-th action in this invocation regardless of type).prev_cycle_signatureis a tuple of ids, unaffected. - Cycle N's per-cycle heading always refers to the N-th action in this invocation, regardless of which feature it operated on. A feature transition is NOT a fresh batch; the orchestrator runs ONE log across every feature it touches.
Dispatch ONLY against the feature
lazy-state.pyreturned THIS cycle; never fabricate a feature. The orchestrator dispatches a cycle subagent against exactly thefeature_id+spec_pathfrom the current cycle'slazy-state.pyoutput, verbatim. It MUST NOT invent, infer, or hand-edit afeature_id/slug that the state script did not emit. The state script (Step 2) already skips any queue entry whosespec_dirdoes not resolve on disk (emitting adangling queue entrydiagnostic) — so a real feature ALWAYS has an on-diskspec_pathbefore dispatch. The cycle subagent prompt MUST forbid the subagent from CREATING a feature'sSPEC.md/RESEARCH.md/queue.json/ROADMAP.mdentries from a bare slug: the only sanctioned dir-creating paths are the--enqueue-adhocbootstrap (Step 0.45) and a/specdispatch against an already-seeded directory. If a cycle'sfeature_iddoes not correspond to an on-diskspec_path, that is a bug to surface (halt + report) — NEVER a cue to manufacture the feature. (This guards the observed failure where a hallucinated slug caused a subagent to fabricate an entire feature.)HARD CONSTRAINT — stop-authorization: the orchestrator MUST NOT end a run except on
max-cyclesor a genuine script-emitted terminal it JUST received from the state probe. The ONLY legitimate no-AskUserQuestionstops are: (a)forward_cycles >= max_cycles(Step 1c), and (b) aterminal_reasonin {all-features-complete,all-bugs-fixed,max-cycles,cloud-queue-exhausted,device-queue-exhausted,host-capability-saturated,queue-missing,blocked-halt-for-manual,needs-research,queue-blocked-on-research} returned by the state script in the CURRENT cycle's probe. Any DESIRE to stop for ANY other reason — context pressure, orchestrator-context load, reliability friction, "I think I should checkpoint", or ≥2 guard denials in an attended run — is NOT license to end the run. The orchestrator MUST first route through the budget-and-queue-guardAskUserQuestion. A checkpoint stop MAY then proceed only after the operator confirms and only by callinglazy-state.py --run-end --reason checkpoint --operator-authorized. The script now mechanically enforces this: an attended--run-end --reason checkpointwithout--operator-authorizedis REFUSED (exit 1, marker kept) — an orchestrator that unilaterally decides to checkpoint will be denied and must continue or ask. Motivating incident (2026-06-14 / lazy-validation-readiness Phase 7): during an attended/lazy-batch 50run the orchestrator permanently stopped at 5/50 cycles via--run-end --reason checkpointwithout presenting anAskUserQuestion— the ≥2-denial prose trigger was read as license to stop unilaterally in an attended run; it is not. When passing--run-endon a genuine terminal, INCLUDE--terminal-reason <reason>(from the sanctioned set above) for stop-authorization validation; omitting it is back-compat but deprecated.
Cycle-subagent execution model (recursive dispatch is NOT available — inline edits required). The cycle subagent dispatched at Step 1d does not have the Agent tool: recursive sub-subagent dispatch is not supported from inside a dispatched subagent, even on workstation. (This was confirmed empirically — an /execute-plan cycle subagent that tried to dispatch Sonnet test/impl agents found the tool unavailable and could only halt.) This forces a load-bearing override of any dispatched skill's sub-subagent contract: skills that nominally fan out to sub-subagents (e.g. /execute-plan → Sonnet test-agent + impl-agent, /retro → research subagents A–G) MUST be performed INLINE inside the cycle subagent itself using Edit/Write/Read directly. This override applies only at the cycle-subagent level — the orchestrator still dispatches exactly one Agent per cycle, and the override never expands the orchestrator's Write/Edit scope (HARD CONSTRAINT 1 still holds; the orchestrator edits only sentinels). This is the same execution model as /lazy-batch-cloud; the two orchestrators are coupled per CLAUDE.md. (Unlike cloud, workstation retains the Tauri runtime, MCP HTTP server, audio device, and Windows tooling — only the recursive-dispatch limit and its inline-edit override are shared.)
Known limitation — TDD agent-separation is traded away. Collapsing
/execute-plan's test-agent→impl-agent split into ONE inline cycle subagent means the structural test-first guarantee (a separate agent writes failing tests before a separate agent implements — theR-EP-2/R-EP-3separation) is GONE: it cannot be enforced from sub-subagent dispatch evidence when there is no dispatch. This is an intentional tradeoff given the no-recursive-dispatch reality, not a defect. Compensating controls: (1) per-batch quality gates (R-EP-6) still run and must pass 100%; (2) the/retropass audits the landed work; (3) the MCP-validation pass (which writesVALIDATED.md) gates final completion. The inline cycle subagent SHOULD still write tests-before-impl within each batch — read the test expectations, write the failing tests, confirm they fail for the right reason, THEN implement — even though the ordering can't be structurally verified./lazy-batch-retro's cloud branch already gradesR-EP-2/R-EP-3asn/a (cloud-override); the same grading applies to inline workstation cycles.
OUTPUT CONTRACT — orchestrator voice (read at run start)
ALL orchestrator chat output MUST follow ~/.claude/skills/_components/orchestrator-voice.md — the turn-template contract (T1 run banner, T2 dispatch / T3 return / T4 inline-gate cycle blocks, T5 park line, T6 rich zones, T7 final report; mechanics silent; rules cited only on deviation; probe JSON never restated in prose). ZERO-TEXT RULE: Claude Code's general "say what you're about to do before tool calls / give brief updates" guidance is OVERRIDDEN for this run — the UI already prints every tool call; between tool calls emit NOTHING unless it is byte-shaped as a template (sanctioned output starts with ## , ### Cycle , a template field line, ⏸/⚖/⚠, or a T6/T7 body — anything else, don't type it). No transition sentences, no "reading X", no "preflight passed", no "composing the dispatch". Read it at run start, and RE-READ it after any compaction boundary (alongside lazy-dispatch-template.md — see Step 1d's compaction discipline); the contract survives summarization by re-read, not by memory. Where an older passage in this skill prescribes a different chat-output shape, the contract's Precedence clause wins; the verbatim re-print / Zero-Context Operator Briefing requirements (HARD CONSTRAINT 6, decision-resume.md, blocked-resolution.md, parked-flush.md, halt-resolution.md) are sanctioned T6 rich zones and are never overridden. Graded by /lazy-batch-retro's R-V-* rules.
STANDING POLICY — completeness-first (D7). Read ~/.claude/skills/_components/completeness-policy.md at run start, and RE-READ it after any compaction boundary (it is on the Step 1d compaction re-read list). It is pre-authorized: decisions whose options differ only in effort / sizing / sequencing / completeness (class: scope) are auto-resolved to the MOST COMPLETE option in BOTH modes — logged (⚖ policy: line, resolved_by: completeness-policy, run-end D7 digest in the T7 report), never asked. It governs the cycle and input-audit subagent prompts (source suppression), Step 1g (scope-class sentinel resolution runs first), Step 1h (sequencing-only blockers auto-resolve; spin-offs pre-authorized, notify + log), and the Gate-1 coverage outcome at Step 1c.5 (author coverage / test-exempt, never ask). D7 only REMOVES questions — product-class decisions still ask exactly as before. Graded by /lazy-batch-retro's R-D7-* rules.
$ARGUMENTS is tokenized on whitespace. Recognized tokens:
Positive integer →
max_cycles. If absent, default to10. If a non-numeric /< 1integer is supplied, refuse with:/lazy-batchrequires a positive integer max-cycles. Usage:/lazy-batch <N> [--allow-research-skip] [--adhoc "<task>"] [--park]. Default: 10.Ambiguous max-cycles (Deliverable D — clarify, never silently coerce): if the token is present but non-integer in a way that suggests a quantity the user had in mind — e.g.
"infinity","lots","max","all","unlimited"— do NOT silently translate it to a hard-coded default. Instead, ask ONE clarifyingAskUserQuestionbefore proceeding:You passed
'{token}'for max-cycles — how many cycles should I run? (e.g.10/30/100)--allow-research-skip(optional flag) → setsallow_research_skip = true. Defaultfalse. When set, the orchestrator restores the legacy "batch the research backlog" behavior:lazy-state.pyis called with--skip-needs-research, Step 4 drops aNEEDS_RESEARCH.mdsentinel for each research-pending feature without halting, and the loop halts onqueue-blocked-on-researchonce every remaining feature is research-pending. This flag is for sessions where you have manually verified the remaining queue is independent — i.e., starting work on a downstream feature is safe even though an upstream feature is awaiting research. Use case is rare. The DEFAULT (flag absent) is to halt strictly on the FIRSTneeds-researchso an ordered queue with dependencies cannot leak work onto unsafe downstream features.--adhoc(optional flag) → setsadhoc_taskto the remainder of$ARGUMENTSafter the--adhoctoken (everything following it, verbatim). If--adhocis the last token with no trailing text,adhoc_taskis empty and the task is inferred from the conversation (see Step 0.45). Whenadhoc_taskis set (flag present), the orchestrator runs Step 0.45 (Ad-hoc Enqueue) before the main loop so the referenced work is enqueued at the top of the queue. Off by default (flag absent → no ad-hoc enqueue). Because--adhocconsumes the rest of the string, place<N>and--allow-research-skipBEFORE it.--park(optional flag) → setspark_mode = true. Defaultfalse. Enables "park-and-continue" mode. This flag is opt-in and off by default. Without it, the orchestrator's behavior is byte-for-byte the existing one — aNEEDS_INPUT.mdhalts the loop into the existing Step 1g resolution-and-wait. The--parkflag may appear in any position relative to the cycle-count arg (e.g./lazy-batch --park 30and/lazy-batch 30 --parkare equivalent). The full park/flush/auto-accept semantics (what happens when park mode is active) are defined in Steps 1g, 1h, and 1i of this skill — this token purely enables the mode.--per-feature-cycle-cap <N>(optional flag) → arms the per-feature budget guard with a fixed ceilingN; OFF by default (the guard never arms without this flag — the whole-runmax-cyclesis the sole default budget). Pass--per-feature-cycle-cap <N>to everylazy-state.pyprobe invocation in Step 1a to opt-in. The orchestrator itself does NOT compute the ceiling — that islazy-state.py's job; this flag merely threads the override in. When the budget guard trips, thebudget_guardprobe field surfaces the ceiling in the PushNotification (see §1c.6 budget-guard notification).--strict-research-halt(optional flag) → pass--strict-research-haltto everylazy-state.pyprobe invocation in Step 1a, restoring the legacy halt-on-first-gated-head behavior (disabling the default-on dependency-aware skip-ahead). Default (flag absent): the new dependency-aware skip-ahead is ON — when the queue head is research-gated or BLOCKED,lazy-state.pyautomatically advances to the next independent,independent: true-marked queue item (if one exists) instead of halting immediately. Pass--strict-research-haltonly when you want the pre-feature strict behavior (halt as soon as the queue head is gated, regardless of downstream items). The gated head is always surfaced (notification + end-of-run flush in the batch report) regardless of whether skip-ahead advanced past it.
Unknown tokens are an error:
/lazy-batch: unrecognized argument{token}. Usage:/lazy-batch <N> [--allow-research-skip] [--adhoc "<task>"] [--park] [--per-feature-cycle-cap <N>] [--strict-research-halt].
Standing-directive echo-back protocol (Deliverable C): mid-run operator messages that imply a change to the orchestrator's operating mode MUST be acknowledged with a single AskUserQuestion echo-back BEFORE the mode takes effect. A "standing directive" is any message that implies one of:
- (a) Budget change — the operator wants to extend or reduce
max_cycles(e.g. "run 20 more cycles", "stop after this feature"). - (b) Standing resolution mode — the operator wants a recurring resolution policy applied automatically until some condition (e.g. "auto-resolve all blockers as add-phase-and-fix until feature X completes").
- (c) Early stop — the operator wants to terminate the current run sooner than
max_cycles(e.g. "stop after this cycle", "pause after the next commit").
Echo-back format (one AskUserQuestion, phrased in active terms):
{Interpretation of the directive in active terms, e.g. "Extend to N cycles and auto-resolve blockers as add-phase-and-fix until X completes — confirm?"}— Yes / No (adjust: ...)
Only enter the new mode after the operator confirms. If they say No or provide a correction, re-parse and echo again.
Budget-and-queue guard: the orchestrator MUST NOT end a run with both budget remaining (forward_cycles < max_cycles) AND active queue items remaining (features that are neither complete, deferred, nor blocked on research) without first asking the operator (one AskUserQuestion) whether to continue into a new run or stop now. This prevents silent early exits where the orchestrator halts mid-queue without the operator realising. The AskUserQuestion path is the attended default. After the operator confirms a stop, the orchestrator passes --operator-authorized to lazy-state.py --run-end --reason checkpoint; without that confirmation flag the script REFUSES the checkpoint (exit 1, marker kept) — see HARD CONSTRAINT 10.
Unattended-checkpoint arm (sanctioned early stop — UNATTENDED runs only). In an unattended run (a scheduled / cron / overnight run that passed --run-start --unattended — see Step 0.55; interactive /lazy-batch invocations are attended and this arm does NOT apply), an early stop is sanctioned ONLY as a CHECKPOINT, and ONLY when a reliability trigger holds: ≥2 guard denials this run, OR an explicit operator pause message. The reliability trigger sanctions an unattended checkpoint because no operator can answer the AskUserQuestion in a scheduled run. In an attended run — even if ≥2 guard denials occurred — the orchestrator MUST route through the budget-and-queue-guard AskUserQuestion (above) and may checkpoint only after the operator confirms. The script enforces this: an attended --run-end --reason checkpoint without --operator-authorized is REFUSED (exit 1, marker kept), so an orchestrator that "decides to checkpoint" without asking will be denied and must continue or ask. A checkpoint (unattended OR operator-authorized) requires ALL THREE of: (1) python3 ~/.claude/scripts/lazy-state.py --run-end --reason checkpoint --next-route "<the probed next route>" [--operator-authorized] (writes lazy-run-checkpoint.json so the next --run-start resumes from it; --operator-authorized is included when and only when the budget-and-queue-guard AskUserQuestion confirmed the stop); (2) a PushNotification carrying the next route + the trigger reason; (3) the T7 final report naming the trigger. An early stop WITHOUT the checkpoint --run-end (or without a holding trigger, or without operator authorization for an attended run) remains a contract violation — the unattended arm narrows, never widens, the silent-exit ban. Resume side: --run-start echoes resumed_from_checkpoint (and deletes the checkpoint file) when it consumes one; surface it on the T1 run-start block as one line (see Step 0.55 / orchestrator-voice.md T1).
Initialize counters and per-session state:
forward_cycles = 0— initialized once per/lazy-batchinvocation; monotonic across feature transitions (HARD CONSTRAINT 8 — never reset whenlazy-state.pyreturns a newfeature_id). Counts pipeline-advancing work; ceiling ismax_cycles.meta_cycles = 0— initialized once per/lazy-batchinvocation; monotonic across feature transitions (HARD CONSTRAINT 8 — never reset on feature transitions). Counts resolution/recovery/cleanup work; uncapped — no ceiling, no cap enforcement (operator decision 2026-06-14). Onlyforward_cyclesis capped (atmax_cycles).max_cycles = <parsed>allow_research_skip = <parsed>— see Step 4 + Step 1f for the behavior switch.cycle_log = []— each entry:{forward_cycles + meta_cycles, feature, action, subagent_summary}(the running total is the monotonic N-th action in this invocation).research_pending = set()— feature_ids whoseRESEARCH.mdis missing and aNEEDS_RESEARCH.mdsentinel was dropped this session. Only used whenallow_research_skip == true. In the default (strict-halt) path this set never accumulates because Step 4 halts on the first feature; it stays empty.skip_needs_research = false— flips totrueafter the firstneeds-researchcycle only whenallow_research_skip == true. In the default path this staysfalsefor the entire session because Step 4 halts before the loop continues.prev_cycle_signature = None— tuple(feature_id, sub_skill, sub_skill_args, current_step)from the most recent cycle (pseudo-skill or real-skill). Drives the Step 1d loop-guard hint.Noneuntil at least one cycle has dispatched.sub_skill_argsis part of the tuple deliberately: a multi-part/execute-plansequence (part-1 → part-2 → part-3) returns the same(feature_id, sub_skill, current_step)on every part but a differentsub_skill_args(the plan-part path), which is real forward progress, not a loop. Omittingsub_skill_argsmade the loop-guard false-trigger on every multi-part plan. Including it lets the guard fire only on a genuine no-progress repeat (identical part re-returned).adhoc_task = <parsed>— the ad-hoc task text from--adhoc(empty string if the flag was present with no text; unset/Noneif the flag was absent). See Step 0.45.park_mode = <parsed>—trueif--parkwas present,falseotherwise. Whenfalse, all halt behavior is byte-for-byte the existing one.
Step 0.0: Environment Preflight (FIRST — before the start banner and before remote sync)
Read and follow ~/.claude/skills/_components/lazy-preflight.md as the very first action of this
invocation — before the start banner, before Step 0.4 remote sync, before the first state probe.
Run its read-only check block (skills symlink resolves, ~/.claude/scripts/lazy-state.py exists,
python3 runs, node resolvable — prepending /c/nvm4w/nodejs if needed). If any check fails, print the
component's setup recipe and STOP — zero cycles consumed (do not print the banner, do not call the
state script, do not enter the loop). On success, node is on PATH for the whole session (no per-call
export PATH), and you continue to the banner / Step 0.4 as normal.
The entire run-start sequence is SILENT (zero-text rule): the preflight, the contract/policy reads, Step 0.4 remote sync, and the queue read for the banner are executed back-to-back with NO text between the tool calls — no "I'll start by…", no "preflight passed", no "let me read…", no "sync clean". The FIRST text this invocation emits is the T1 banner (preflight failure and sync divergence are the T6 exceptions).
Print the start banner — T1 per ~/.claude/skills/_components/orchestrator-voice.md (≤4 lines; nothing else before the first cycle block):
## /lazy-batch — run start
mode workstation · park {on|off} · research {strict|batched}
budget fwd {max_cycles} · meta no cap
queue {N} feature(s) · first: {first queue entry id}
The queue line is best-effort (one Bash read of docs/features/queue.json for the entry count — a banner fact, not state inference; state inference remains exclusively lazy-state.py per HARD CONSTRAINT 3); omit the line if the queue file can't be read cheaply. The repo root and flag parsing are mechanics — not announced.
Step 0.4: Resume-time remote sync (HARD REQUIREMENT)
Runs once, immediately after Step 0 (arg parsing) and BEFORE Step 0.5 / the Step 1a first state probe. This is a single-turn git reconciliation, NOT polling — it does not violate HARD CONSTRAINT 7 (no active waiting). It does NOT touch the orchestrator's Write/Edit sentinel-only scope (HARD CONSTRAINT 1) — these are Bash git operations, not file edits.
Rationale: a /lazy-batch session can be interrupted (machine sleep, crash, terminal close) and resumed later, or the work branch's remote may have advanced from another machine (or a cloud /lazy-batch-cloud run on the same branch). If the orchestrator runs lazy-state.py against a local tree behind the remote tip, it infers state from stale local files (plans, sentinels, SPEC) and may re-do or corrupt already-pushed work. Reconcile local to the remote tip BEFORE any local-state inference. (This guardrail is mirrored from /lazy-batch-cloud Step 0.4, where the same failure mode is acute because of container reclaim.)
Algorithm:
Determine the work branch:
branch=$(git rev-parse --abbrev-ref HEAD)Fetch the remote tip (retry up to 4× with exponential backoff 2s/4s/8s/16s on network error — bounded retry, not an active wait):
git fetch origin "$branch"If the branch does not exist on
originyet (brand-new work branch never pushed), there is nothing to reconcile: skip the rest of Step 0.4 and continue to Step 0.5.Fast-forward local to the remote tip:
git merge --ff-only "origin/$branch"If the fast-forward FAILS because local has DIVERGED from
origin(non-fast-forwardable — local has commits origin lacks AND origin has commits local lacks), do NOT clobber. Do NOTgit reset --hard, do NOT force anything. Surface the divergence to chat and halt for human resolution:🛑 /lazy-batch — work branch diverged from origin Local `{branch}` and origin/{branch} have both moved independently (non-fast-forwardable). This may indicate concurrent edits from another machine or a force-push. Refusing to auto-reconcile to avoid losing work. Resolve manually (inspect `git log --oneline --graph {branch} origin/{branch}`), then re-invoke /lazy-batch.PushNotification with the same one-line summary, then STOP. Do NOT run
lazy-state.py.On a clean fast-forward (or when local was already up to date / the branch was unpushed), continue to Step 0.5 silently — a successful sync is mechanics per the orchestrator-voice contract (silence means the machinery worked). Only the step-4 divergence halt is announced (a T6 error — recipe printed in full).
Step 0.45: Ad-hoc Enqueue (only when --adhoc was supplied)
Runs once, after Step 0.4 (remote sync) and BEFORE Step 0.5 / the first state probe. Skipped entirely when the --adhoc flag was absent. It runs AFTER the remote ff-sync deliberately: enqueuing mutates queue.json in the working tree, so it must happen on the reconciled remote tip, not a stale local snapshot that the Step 0.4 fast-forward would then conflict with.
!cat ~/.claude/skills/_components/adhoc-enqueue.md
After the enqueue returns, continue to Step 0.5. The first cycle's state probe will return the ad-hoc feature first and route it to /spec; its end-of-cycle commit+push carries the bootstrap files (queue.json, ROADMAP.md, the spec dir + ADHOC_BRIEF.md) to origin.
Step 0.5: Pre-loop staged-research ingest check
Before entering the main loop, check whether the user staged Gemini research uploads between sessions. This is the "resume after halt" entry point — a previous /lazy-batch invocation may have halted in Step 1f (research-wait), the user uploaded research in the meantime, and this invocation should pick it up automatically.
Algorithm:
Probe for staged
.txtfiles:find docs/gemini-sprint/results -maxdepth 1 -name '*.txt' -type f 2>/dev/null | head -1If empty → no staged research, skip to Step 1.
If staged
.txtfiles exist, dispatch/ingest-researchas cycle 1 (counts againstmax_cycles):Agent({ description: "lazy-batch pre-loop ingest-research dispatch", subagent_type: "general-purpose", model: "sonnet", prompt: <the prompt below> })Subagent prompt:
You are advancing one cycle of the autonomous feature pipeline. The orchestrator detected staged Gemini research at session start — .txt file(s) are present in docs/gemini-sprint/results/. Working directory: {cwd} Action for this cycle: Invoke the /ingest-research skill with no arguments. It will scan docs/gemini-sprint/results/ for every .txt, correlate each to a feature via the prompt symlinks under docs/gemini-sprint/prompts/, write per-feature RESEARCH.md + RESEARCH_SUMMARY.md, drop the > Draft (pre-Gemini) trailer in SPEC.md, clear queue.json "stub": true, move consumed .txt files to _consumed/, and commit per feature. Operating mode: batch (--batch is implicit for /ingest-research — see its SKILL.md hard constraints). After the skill returns: 1. Report the final summary block /ingest-research printed. 2. List any ambiguous correlations (NEEDS_INPUT.md sentinels written) — the next orchestrator cycle will halt at decision-halt mode (Step 1g). 3. Report which feature_ids now have RESEARCH.md on disk. You may NOT spawn further subagents. You MAY use Edit/Write under docs/ per /ingest-research's hard constraints.After dispatch:
- Append to
cycle_log:{forward_cycles + meta_cycles + 1, "—", "/ingest-research (pre-loop)", "<subagent summary>"}. - Increment
forward_cyclesto 1 (ingesting research is pipeline-advancing work). - Enter the main loop (Step 1).
- Append to
Direct RESEARCH.md drops into canonical feature directories don't require ingestion — lazy-state.py sees them at Step 5 and routes to /spec Phase 3 naturally. Step 0.5 is specifically for the staged .txt upload path.
If the user provided a one-off file path via /ingest-research <path> (run BEFORE /lazy-batch), that invocation handled the ingest in its own session — by the time /lazy-batch runs, RESEARCH.md already exists in the canonical location, and Step 0.5 is a no-op for that feature.
Step 0.52: Validation-readiness pre-screen (advisory — F5 / lazy-validation-readiness)
Purpose: Before front-loading DEFERRED_NON_CLOUD features, emit a per-feature verdict table
showing whether each candidate's MCP test scenarios assert tools that are already registered in
the repo. This surfaces "the scenario asserts a tool that doesn't exist yet" early — at curation
time — rather than three cycles later at the Step-9 mcp-test boundary.
Run:
python3 ~/.claude/scripts/validation_readiness.py --repo-root {cwd}
The script lives at ~/.claude/scripts/validation_readiness.py (symlinked from
user/scripts/validation_readiness.py in the claude-config repo).
Output format (example):
validation_readiness — DEFERRED_NON_CLOUD pre-screen verdict
======================================================================
FEATURE VERDICT MISSING TOOLS
---------------------------------------- ---------------------- ------------------------------
sidecar-watchdog ready
d8-session-format needs-work evaluate_code
polyphonic-stems needs-work get_diagnostic_counters
...
advisory: operator may still front-load a needs-work feature.
This step is ADVISORY — not a hard gate. The operator may still choose to front-load a
needs-work feature deliberately (e.g., the plan for this session IS to implement the missing
surface first). However, the verdict is logged so that if the run later hits a deep blocker at
Step 9, the blocker is traceable to an ignored pre-screen warning rather than appearing as a
surprise. Features with no DEFERRED_NON_CLOUD.md are silently skipped (not front-load
candidates); features with DEFERRED_NON_CLOUD.md but no mcp-tests/ scenarios are shown as
ready (no scenarios).
If the script is absent (first run after the claude-config symlink was set up, or a
machine where the ~/.claude/scripts/ symlink hasn't been refreshed), skip this step silently
and continue to Step 0.55 — this is a zero-text-rule advisory, never a blocker.
Step 0.55: Write the run marker (IMMEDIATELY before the T1 banner / loop entry)
After Step 0.5 (pre-loop ingest check) completes — and before printing the T1 banner or entering the Step 1 loop — assert the orchestrator identity signal, then write the run marker:
# C3 self-immunity signal (cycle-subagent-runs-orchestrator-work, Phase 1): the
# orchestrator asserts its identity by EXPORTING LAZY_ORCHESTRATOR=1 into the
# session env it runs every lazy-state.py lifecycle/routing call from. This is
# the positive, marker-independent carrier `refuse_if_cycle_active` /
# `refuse_cycle_marker_mutation_if_subagent` key on (lazy_core.py priority 1) —
# it makes the orchestrator STRUCTURALLY IMMUNE to a stale/live cycle marker
# (its own --cycle-end clears the marker while the marker is still present), and
# the ABSENCE of the var is what marks a cycle subagent (a subagent's Bash
# subprocess never inherits this export). Carry it on EVERY lifecycle/routing
# call below (--run-start/--run-end/--cycle-begin/--cycle-end/--apply-pseudo/
# --enqueue-adhoc/--emit-dispatch); export once for the session so it persists.
export LAZY_ORCHESTRATOR=1
python3 ~/.claude/scripts/lazy-state.py \
--run-start --max-cycles {max_cycles} \
--repo-root {cwd}
Attendedness: interactive /lazy-batch invocations (the operator is present in the session) call --run-start WITHOUT --unattended — the marker records attended: true (the default). Only a scheduled/cron driver (a cloud task, an overnight automation) passes --unattended, recording attended: false. The attended field governs whether --run-end --reason checkpoint requires --operator-authorized (see HARD CONSTRAINT 10 and the budget-and-queue guard above). Legacy markers lacking the field are treated as attended — the stricter gate is the safe default.
What this does. The marker (~/.claude/state/lazy-run-marker.json) is the single on/off switch for the inject + validate-deny hooks. While the marker is present:
- The inject hook (
lazy-route-inject.sh) fires on every UserPromptSubmit turn, runs the full probe form, and injects the route (LAZY-ROUTE (hook-injected, turn N): …) into the model's context viaadditionalContext— the orchestrator does NOT need to remember to probe; the probe arrives with the turn. - The validate-deny guard (
lazy-dispatch-guard.sh) checks everyAgentdispatch against the prompt registry; an unregistered prompt is denied with a corrective recipe.
Interactive sessions (no marker) are completely untouched — both hooks exit instantly on the test -f fast path. The marker is script-owned: --run-start writes it; --run-end deletes it. The orchestrator never hand-writes the marker file.
Session state pinned in the marker: pipeline=feature, cloud=false, repo_root, max_cycles, session_id (bound on first hook firing), nonce_seed. Counters (forward_cycles, meta_cycles) are persisted in the marker from this point forward — the inject hook reads them without needing CLI flags.
Resume from a checkpoint. If a prior run ended via the unattended-checkpoint arm (Step 0 budget-and-queue guard), --run-start consumes lazy-run-checkpoint.json and echoes its content as resumed_from_checkpoint in the run-start output (then deletes the file — single-use). When the run-start output carries resumed_from_checkpoint, surface it on the T1 banner as one extra line — resume <next_route> (checkpoint <date>) (orchestrator-voice.md T1) — so the operator sees the run picked up where the checkpoint left off.
--run-end is MANDATORY on every terminal/halt path — see §1c.6 for the enumeration. A missed deletion is self-healing (24h staleness + session-id mismatch cleanup) but is a protocol violation the retro grades.
If --run-start fails (script error), surface a T6 ⚠ and STOP before printing the banner — a run with no marker is a run with no enforcement, which is still safe for the pipeline (it degrades to pre-Phase-5 behavior) but should not silently proceed without the operator knowing enforcement is off.
Step 1: Cycle Loop
Unified driver — merged-view dispatch (single driver, two state scripts; unified-pipeline-orchestrator Phase 2).
/lazy-batchis the SHARED driver for BOTH the feature and bug pipelines. Each cycle it probes the merged work-list head withpython3 ~/.claude/scripts/lazy-state.py --next-merged --repo-root {cwd}(Phase-1 surface — read-only ORDERING ONLY; it never re-infers per-item state) to learn the next actionable item's{item_id, type, repo_root}, then type-dispatches the rest of the cycle to the matching state script:
type == "feature"→ drive this cycle withlazy-state.pyexactly as Steps 1a–1e describe; the type-correct terminal action is__mark_complete__(writesCOMPLETED.md).type == "bug"→ drive this cycle withbug-state.py(same JSON contract,docs/bugs/,--bug-idscoping); the type-correct terminal action is__mark_fixed__(writesFIXED.md).The merged view normalizes the two queues' divergent ordering fields (feature
tierint / bugseverityP0..Low) onto one effective-priority scale and breaks ties bug-before-feature — but that ordering lives ENTIRELY inlazy_core.merged_priority(Phase 1), NOT in this prose: the driver only CONSUMES the merged head, it never re-implements ordering. Both state machines and all gates run UNCHANGED — this skill carries NO new state-machine logic; the merged probe is the only addition.No-regression (single-type runs are unchanged). When only ONE queue is populated, the merged head is simply that queue's head, so the cycle sequence is byte-for-byte identical to the pre-unification per-type batch: a features-only queue runs exactly as
/lazy-batchalways did (drivelazy-state.py, terminal__mark_complete__); a bugs-only queue runs exactly as the standalone/lazy-bug-batch(drivebug-state.py, terminal__mark_fixed__). The merged probe is additive —lazy-state.py --next-mergedover a single populated queue returns that queue's head and nothing else. The parity audit (lazy_parity_audit.py --merged-view) + a single-type fixture assert this no-regression guarantee.Steps 1a–1e below are written against
lazy-state.py(the feature path, the common case). For atype == "bug"cycle, substitutebug-state.pyforlazy-state.pyand__mark_fixed__for__mark_complete__everywhere in the cycle body; the dispatch SHAPE is otherwise identical (this is the same coupling/lazy-bug-batchalready documents). See the State Machine Summary at the bottom of this skill for the per-type dispatch table.
Repeat:
1a. Run lazy-state.py
LAZY-ROUTE banner check (FIRST — before deciding to run the probe). The inject hook (lazy-route-inject.sh) fires on every UserPromptSubmit turn while the run marker is present. When the hook fires, it runs the full probe form itself and injects the result into the turn context as a single additionalContext string with the following structure:
LAZY-ROUTE (hook-injected, turn N): {"feature_id": "...", "sub_skill": "...", "cycle_prompt": "...", "cycle_model": "opus", ...} nonce=<hex-value>
On a post-compaction re-entry (the hook event is PostCompact or SessionStart with source=="compact"), the string also carries a POST-COMPACTION RE-ENTRY: paragraph after the nonce line. If the inject hook itself errored (fail-open breadcrumb), a HOOK_ERROR: <error text> suffix appears at the end of the string. The probe JSON is compact (no indentation) and contains all the same fields as a manual lazy-state.py --repeat-count --emit-prompt --probe invocation. If the current turn carries a LAZY-ROUTE (hook-injected, turn N): banner, consume it directly — extract feature_id, sub_skill, cycle_prompt, cycle_model, and all other probe fields from the injected JSON. Do NOT run another lazy-state.py probe on this turn. Re-probing when a banner is already present advances the persisted counters TWICE for one logical cycle — a protocol violation the retro grades. The counter in cycle_header is POST-advance (1-based current-cycle semantics — the script already incremented before injecting). If no LAZY-ROUTE banner is present (hook was inactive, or the turn is the first turn after a compaction boundary before the hook re-armed), run the probe as below.
python3 ~/.claude/scripts/lazy-state.py [--skip-needs-research]
Pass --skip-needs-research only when allow_research_skip == true AND skip_needs_research == true. The double-gate matters: in the default (strict-halt) path, skip_needs_research never flips to true because Step 4 halts the loop on the first needs-research, so the script is always called without the flag and returns terminal_reason: needs-research for the first research-pending feature in queue order. Only the --allow-research-skip path arms the legacy batching behavior.
Probe enrichment (optional — folds repeat-count, git guards, and cycle header into one payload). The orchestrator MAY call the probe with additional flags to fold repeat_count, git_guards, and cycle_header into the JSON in a single invocation:
python3 ~/.claude/scripts/lazy-state.py --repeat-count --emit-prompt --probe \
--max-cycles {max_cycles} \
[--skip-needs-research]
The --forward-cycles and --meta-cycles flags are NO LONGER passed on probe invocations. The marker persists the counters (forward_cycles, meta_cycles) from the moment --run-start wrote it; the inject hook and probe read them directly from the marker without needing CLI flags. The flags remain in the CLI for backward compatibility but MUST NOT be passed by the orchestrator — passing them would override the marker's persisted state with stale in-memory values.
--repeat-count enriches the output with a repeat_count field (how many consecutive cycles returned the same (feature_id, sub_skill, sub_skill_args, current_step) tuple) for mechanical loop detection. It ALSO emits a step_repeat_count field (how many consecutive cycles reached the same (feature_id, current_step) STEP — sub_skill/sub_skill_args-blind, and with NO head-advance reset). Probe hygiene: --repeat-count ADVANCES both persisted streaks, so it is reserved for the SINGLE dispatch-bound probe per cycle (the one whose result you actually dispatch on). Any diagnostic / inspection probe — re-checking state out of band, sanity-reading the routing — MUST use --repeat-count-peek instead (it reads the would-be streaks WITHOUT advancing the persisted state). The dispatch-tuple repeat_count is HEAD-aware: the same tuple plus new commits since the last probe RESETS it to 1 (a re-validation after landed commits is forward progress, not a loop). step_repeat_count is the oscillation tripwire (T6): when it is >= 3, STOP — do NOT keep dispatching the emitted action mechanically. Surface the warning ⚠ step '<current_step>' reached <step_repeat_count> times without advancing — inspect routing before dispatching, then inspect WHY the state machine keeps returning to that step before continuing. Unlike repeat_count, the step counter does NOT reset when HEAD advances — it is built to catch "productive-looking" oscillation where each cycle commits a file (HEAD moves → the dispatch streak resets every iteration) yet routing never leaves the step (the live d8 write-plan loop on a gate-owned PHASES row, 2026-06-11). A high step_repeat_count with a low repeat_count is exactly that signature. Never redirect probe or diagnostic output into the repo tree. Capture probe output IN-BAND — the probe already prints its JSON to stdout, so consume that stdout directly (result=$(python3 user/scripts/lazy-state.py … ) or pipe it straight into the consumer); do NOT round-trip through a temp file. A temp-file round-trip is the source of the Windows-portability crash: a hardcoded/idiomatic POSIX /tmp/probe.json (the fallback when $TMPDIR is unset in Git-Bash) is read back by Windows-native Python, which has no /tmp, and the read-back dies with FileNotFoundError. If a temp file is GENUINELY unavoidable, NEVER use a bare /tmp/... path — require a path PRODUCED AND READ BY THE SAME INTERPRETER (f="$(mktemp)" consumed by the same shell, or %TEMP% resolved by the same Windows Python that reads it back), never a path that crosses Git-Bash/Windows-Python conventions. --probe (combined with the three counter flags) folds git_guards (clean-tree + origin-parity) and a pre-formatted cycle_header string into the response. --emit-prompt (composed with --repeat-count) folds the fully-assembled cycle dispatch prompt into the JSON: cycle_prompt (the complete, token-bound prompt — the loop block already appended when repeat_count >= 2, the mcp-test runtime variant already selected from the spec's PHASES.md **MCP runtime:** line) and cycle_model ("opus", or "sonnet" when the loop block was appended). Both are null on pseudo-skill (__*) and terminal/idle probes; on an assembly failure for a real skill cycle_prompt_refused carries the reason instead. These flags are purely additive — the base JSON fields are unchanged. --emit-prompt SHOULD be passed on EVERY probe — it is null on pseudo-skill/terminal probes, so it is always safe to request, and folding prompt assembly into the same probe call is what makes Step 1d a pure consume-and-dispatch.
Step 1a — probe ONCE per cycle (F2 double-probe debounce). Run exactly ONE advancing, dispatch-bound --repeat-count --emit-prompt probe per cycle — the one whose cycle_prompt you actually dispatch — and use --repeat-count-peek for EVERY inspection / sanity / out-of-band probe so that only the single dispatch-bound probe advances the streaks. Probing a route twice with no dispatch between (an inspection probe, then the dispatch-bound probe) is a re-read, not a re-attempt, and historically inflated step_repeat_count into false LOOP DETECTED blocks. update_repeat_counts now defends this in depth: when a run marker is present it debounces a re-read via the registry consume-count delta (an unchanged consumed-emission count between two identical step probes ⇒ no dispatch landed ⇒ step_repeat_count is HELD, not incremented), so a genuine same-step oscillation (a real dispatch — hence a consume — between repeats) still trips while a benign double-probe no longer does. This note is the behavioral complement: even with the script debounce, keep to one advancing probe + peek for inspection.
Investigation triggers + the inline-diagnosis budget (see ~/.claude/skills/_components/investigation-dispatch.md — the dispatch template lives THERE, reference it, never inline-copy; the orchestrator emits the dispatch via python3 ~/.claude/scripts/lazy-state.py --emit-dispatch investigation --context item_name=… --context spec_path=… --context symptom=… --context trigger=… --context inherited_hypotheses=… --context item_id=… --context cwd=… and dispatches dispatch_prompt VERBATIM). Root-cause diagnosis is dispatched work, not orchestrator work. Three triggers: (1) the probe JSON carries validation_escalation: true and no current INVESTIGATION.md exists in {spec_path} → the blocked-resolution path dispatches /investigate BEFORE any corrective phase (Step 1h carries the wiring); (2) failed fix — a fix cycle landed but the post-fix live/validation check shows the symptom unchanged → the next dispatch for that issue is /investigate, NOT another fix cycle (a headless-green fix built to an unverified orchestrator hypothesis once burned ~266k tokens and seeded the next bug); (3) inline-diagnosis budget — if you have spent more than ~8 of your own diagnostic tool calls (source reads, log greps, live probes) on one issue, STOP probing and dispatch /investigate; quick checks stay inline, sustained diagnosis does not (measured cost of unbounded inline diagnosis: ~60% of orchestrator activity + a mid-diagnosis compaction in the 2026-06-11 live run). No-narrative-as-fact: your dispatch prompts cite INVESTIGATION.md (artifact path + ledger rows) or say "cause unknown — investigation pending"; unproven hunches go to the investigation labeled unproven, never to a fix cycle as "strong hypothesis" headers.
Park-mode probe flag (--park only). When park_mode == true (the --park invocation flag), append BOTH --park-needs-input AND --park-blocked to EVERY lazy-state.py probe invocation in this step (base or enriched form alike). With these flags, the script skips features carrying an unresolved NEEDS_INPUT.md (instead of halting on needs-input) OR a feature-local BLOCKED.md (instead of halting on blocked), and reports them in a parked[] array on the JSON output — each entry tagged sentinel_kind (needs-input | blocked) — the input to the Step 1g park path, the Step 1g-flush, and the §1c.6 park notifications. When every remaining feature is parked, the script returns the distinct queue-exhausted-all-parked terminal (handled in Step 1b). When park_mode == false, call the script plain (NEITHER flag) — existing behavior, byte-for-byte; the parked[] key never appears, and a feature-local BLOCKED.md still halts on blocked (Step 1h).
If the script exits non-zero, run python3 ~/.claude/scripts/lazy-state.py --run-end (idempotent — safe even if the marker is absent), surface the error, push a PushNotification, print the final batch report (see Step 2), and STOP.
Parse the JSON output. Extract: feature_id, feature_name, spec_path, current_step, sub_skill, sub_skill_args, terminal_reason, notify_message, diagnostics.
1b. Handle terminal states
If terminal_reason is set:
blocked: see Step 1h (blocked-resolution mode). Not a terminal halt anymore — and most blockers no longer ask. Step 1h FIRST applies the research-blocked carve-out (component step 1a-research): ablockedfeature carrying a co-located liveNEEDS_RESEARCH.md+RESEARCH_PROMPT.mdwithRESEARCH.mdabsent is a research gap, NOT a product fork — route it to Step 4 (Research Halt) and surface the research prompt for the operator to paste into Gemini; do NOT run the blocked-resolutionAskUserQuestionand do NOT add-a-phase (a research gap is filled by Gemini research, not a corrective phase). Otherwise Step 1h classifies the blocker percompleteness-policy.md§3: a sequencing-only blocker (every resolution path converges on the same product behavior) is auto-resolved — add-phase + fix now, or/spec-bug/ ad-hoc spin-off + dependency-gate + requeue-to-tail — logged + push-notified, no question. Only a genuine product fork takes the operator path: re-print theBLOCKED.mdbody verbatim, runAskUserQuestionfor the resolution path (add a phase / defer to queue tail / halt-for-manual / custom), record the choice, dispatch the Opus apply-resolution subagent to enact it (neutralizingBLOCKED.mdvia rename), and return to Step 1a. The loop continues; do NOT print the final batch report — UNLESS the operator chooses "Halt for manual fix", which keepsBLOCKED.mduntouched and STOPs (the legacy behavior, now one option among several). Park-mode exception (park_mode == true): this terminal is NOT reached for a feature-local block — the--park-blockedprobe flag (Step 1a) parks the blocked feature intoparked[]and advances the queue, so Step 1h does NOT fire for it; the block is deferred to the Step 1g-flush (which re-prints theBLOCKED.mdbody and runs the SAME resolution affordance at run-end). Per SPEC D5, this includes escalation/mcp-validation per-feature blocks (validation_escalation,blocker_kind: mcp-validation) — park mode defers everything parkable. Only the global/environment terminals below (queue-missing,device-queue-exhausted, etc.) still halt mid-run.needs-input: see Step 1g (decision-resume mode). Not a terminal state for the orchestrator anymore. Step 1g first auto-resolves any scope-class decisions percompleteness-policy.md(D7 — step 1b of the component, both modes, never asked); for the remaining product-class decisions it re-prints the rich## Decision Context, runsAskUserQuestion, appends## Resolution, dispatches the Sonnet apply-resolution subagent (which edits SPEC.md / PHASES.md and neutralizes the sentinel), and returns to Step 1a. The loop continues; do NOT print the final batch report.needs-research: see Step 4 (research halt). Behavior depends onallow_research_skip:- Default (
allow_research_skip == false): Step 4 writesNEEDS_RESEARCH.md, prints the inline-prompt halt announcement, PushNotifications, prints the final batch report, and STOPs. The orchestrator does NOT advance past the research-pending feature — this is critical for ordered queues where downstream features depend on upstream work. - Opt-in (
allow_research_skip == true): legacy batching behavior — Step 4 writesNEEDS_RESEARCH.md, addsfeature_idtoresearch_pending, DOES NOT increment either counter, flipsskip_needs_research = true, and returns to Step 1a so the next state-script call passes--skip-needs-researchand either advances to a ready feature or returnsqueue-blocked-on-research.
- Default (
queue-blocked-on-research: see Step 1f (research-wait mode). Only reachable whenallow_research_skip == true— in the default path Step 4 halts before this terminal can fire.needs-spec-input: see Step 1i (operator-directed halt-resolution) — the orchestrator re-prints what the dir contains andAskUserQuestions the path (provide spec direction → seed the baseline / defer & continue queue / halt). It no longer bare-STOPs "cannot start from nothing".queue-missing: Run--run-end, then PushNotification withnotify_message, print final batch report, STOP. (There is no queue to continue — the operator must createqueue.jsonfirst; NOT routed to Step 1i per the halt-resolution component's exclusion list.)completion-unverified: a feature's SPEC/ROADMAP claimsCompletebut noCOMPLETED.mdreceipt exists — it was flipped OUTSIDE the validation gate (a cycle subagent or hand edit bypassing/mcp-test+ the coverage audit). See Step 1i (operator-directed halt-resolution): re-print the gap andAskUserQuestionthe path — reopen & re-validate (**Status:** In-progress→ let the pipeline re-run MCP validation) / grandfather the receipt (lazy-state.py --backfill-receipts, only if genuinely validated before the gate) / defer & continue / halt. Do NOT auto-flip, auto-reopen, or auto-backfill — that judgment is the operator's, now surfaced as a choice rather than a bare halt. (This is the terminal that makes failure mode 1 self-announcing instead of silent.)stale_upstream: an upstream feature/work-item this feature was materialized from changed since materialize. See Step 1i (operator-directed halt-resolution): re-print the gap andAskUserQuestionthe path (re-materialize/absorb → re-run materialize or/realign-spec/ reject the change / defer & continue / halt).lazy-state.pyemits this (Step 2.9); do NOT auto-resolve.all-features-complete: a SINGLE-TYPE queue-exhausted terminal — the FEATURE side is done, but the unified driver must NOT stop the whole run if the BUG side still has actionable work. Apply the option-(b) unified-driver fallthrough (see the box below) FIRST: probe the OTHER type (bug-state.py); only if it too is exhausted do you stop. When BOTH types are exhausted: Run--run-end, then PushNotification"ALL FEATURES COMPLETE — roadmap finished after {forward_cycles} forward + {meta_cycles} meta /lazy-batch cycle(s).", print final batch report, STOP.queue-exhausted-all-parked(--parkmode only): the queue advanced past every workable feature and every remaining feature is parked (blocked and/or needs-input). This is an HONEST distinct terminal — NOTall-features-complete(the roadmap is not finished). FIRST fire the Step 1g-flush (triggers (b)/(c)) so every parked item — needs-input AND blocked (sentinel_kind) — is surfaced and resolved at run-end; THEN runpython3 ~/.claude/scripts/lazy-state.py --run-end, PushNotification"Queue exhausted — {parked_count} feature(s) parked (blocked/needs-input); surfaced at flush.", print final batch report, STOP. Do NOT report success.queue-exhausted-budget-deferred: All remaining queue items were deferred/evicted to the queue tail by the budget guard (no independent successor exists to skip-ahead to). This is NOTall-features-complete— the roadmap is not finished; features were over-budget, not done. Fire the budget-guard trip PushNotification (§1c.6 point 5) for the triggering feature, then runpython3 ~/.claude/scripts/lazy-state.py --run-end, then PushNotification"lazy-batch halted — queue exhausted by budget guard; {N} feature(s) deferred to queue tail. Re-invoke /lazy-batch to continue.", print final batch report, STOP. On the next/lazy-batchinvocation the deferred features reappear at the queue tail with fresh cycle counts.cloud-queue-exhausted: Unreachable for/lazy-batch(workstation variant); treat asall-features-completedefensively — runpython3 ~/.claude/scripts/lazy-state.py --run-endfirst, then PushNotification, print final batch report, STOP.all-bugs-fixed: the SINGLE-TYPE bug-side queue-exhausted terminal (reachable for the unified driver when the merged head istype == bug). Symmetric toall-features-complete: apply the option-(b) unified-driver fallthrough (box below) FIRST — probe the OTHER type (lazy-state.py); only when BOTH types are exhausted: Run--run-end, then PushNotification"ALL BUGS FIXED — bug queue cleared after {forward_cycles} forward + {meta_cycles} meta /lazy-batch cycle(s).", print final batch report, STOP.device-queue-exhausted: Reachable only on a no-real-device workstation (WSL2/CI, where the audio backend is the HeadlessPumpDriver). Every remaining feature carriesDEFERRED_REQUIRES_DEVICE.md(real-device-only MCP assertions that cannot be certified here). Run--run-end, then PushNotification withnotify_message, print final batch report, STOP. The honest resume is a real-device host: tell the user to setALGOBOOTH_REAL_AUDIO_DEVICE=1(or run on native hardware) and re-run/lazy-batch— there the same features RE-OPEN (Step 9 dispatches/mcp-testscoped to the deferred scenario IDs as ordinary cycles) and complete. This is the device-axis mirror ofcloud-queue-exhausted. Note: the re-open dispatch itself needs no special handling — on a real-device host the state script emitssub_skill: mcp-testfor the deferred scenarios, which runs as a normal cycle.host-capability-saturated: The host-capability-axis generalization ofdevice-queue-exhausted(host-capability-declaration-for-gated-features). Every remaining feature declares arequires_host:capability (a binary toolchain, GPU, etc.) that is absent on THIS host — each carriesDEFERRED_REQUIRES_HOST.md(the missing capability ids) and noVALIDATED.md. Run--run-end, then PushNotification withnotify_message(the script names each feature + its missing capability id(s):host-capability miss — <feature-id> requires <cap-id> (absent on this host); deferred to capability-host), print final batch report, STOP. The honest resume is a capability-bearing host: run/lazy-batchon a machine where the probe reports the capability present — there the same features RE-OPEN (an emptymissingset is the no-special-case re-open; Step 9 dispatches/mcp-testas an ordinary cycle) and complete. The deferred features were NOT dropped or waived — they remain in the queue, re-openable. Thehost_deferred_featuresprobe key surfaces each deferred feature_id for the end-of-run flush. The re-open dispatch needs no special handling — on a capability-host the state script simply does not skip the feature.
Option-(b) unified-driver fallthrough (single-type queue-exhausted terminal; item 3, lazy-batch-unified-driver-parity-and-accounting Phase 2).
lazy-state.py --next-mergedis PURE ORDERING — it returns the merged head WITHOUT re-inferring per-item state, so a resolved (already-Complete / already-Fixed) item can sit at the merged head and a single-type queue-exhausted terminal can fire while the OTHER type still has an actionable item (the on-disk-bug masking that motivated this fix). DO NOT stop the whole run on a single-type terminal. When this cycle's terminal isall-features-complete(the head wastype == feature) ORall-bugs-fixed(the head wastype == bug), the driver FALLS THROUGH to probe the OTHER type before declaring the run done:
- The terminal was
all-features-complete→ probe the BUG side:python3 ~/.claude/scripts/bug-state.py --repo-root {cwd}(which loadsdocs/bugs/queue.jsonAND on-disk bugs viaload_bug_queue/_find_open_bug_dirs). If it returns an actionable bug (NOTall-bugs-fixed/queue-missing), DO NOT stop — start the next cycle on that bug (drivebug-state.py, terminal__mark_fixed__) and continue the loop.- The terminal was
all-bugs-fixed→ probe the FEATURE side:python3 ~/.claude/scripts/lazy-state.py --repo-root {cwd}. If it returns an actionable feature (NOTall-features-complete/queue-missing), DO NOT stop — start the next cycle on that feature and continue the loop.- Only when the OTHER type ALSO returns its own queue-exhausted terminal (or
queue-missing) is the whole run genuinely terminal — THEN run--run-end+ PushNotification + final report + STOP, per the matching bullet above.This keeps
--next-merged/merged_worklistPURE (no per-item state inference leaks into them — the documented "ordering-only" contract inuser/scripts/CLAUDE.md); the fallthrough lives ENTIRELY in this driver loop. Features stay strictlyqueue.json-driven — the fallthrough adds NO on-disk feature fallback (out of scope per SPEC Verified Symptom 4); only the bug side honors on-disk pickup, exactly as/lazy-bug-batchdoes.
1c. Check the max-cycles cap
If forward_cycles >= max_cycles:
python3 ~/.claude/scripts/lazy-state.py --run-end
PushNotification({ message: "lazy-batch hit max-cycles ({max_cycles}). Restart from a fresh session to continue." })
Print final batch report, STOP. Do NOT try to renew the cap automatically — the cap exists to bound runaway costs.
1c.6. PushNotification policy (park / halt / flush / run-end)
The orchestrator fires PushNotification at exactly four canonical event points so the operator receives a phone notification whenever the run changes state. PushNotification is always called by the orchestrator — state scripts never call it.
park (
--parkmode only) — fired once per newly-parked item whenpark_mode == trueand the probe returns a non-emptyparked[]array (the script's queue-walk park skip;parked[]arrives on ordinary Step 1a probes and lists ALL currently-parked items, not just new ones). Dedup rule: maintain an in-session set of already-notified parked ids; on each probe, fire only for ids inparked[]that are NOT yet in the set, then add them. Never re-fire for an id already in the set. (After a compaction boundary the set may be lost — one duplicate notification per item after a compact is acceptable; re-seed the set from the currentparked[]on the first post-compact probe without firing.) Wording branches on the entry'ssentinel_kind: for a needs-input park, the message carries the running parked-count:"parked {feature_name} — {N} decision(s) parked so far this run", and the T5 chat line is⏸ parked {feature_name} — {N} decision(s) · notified ({parked_count} parked this run). For a blocked park (sentinel_kind == "blocked",decision_count == 0), it reads as a parked BLOCK with the blocker's phase: message"parked {feature_name} — BLOCKED ({phase}); deferred to flush ({parked_count} parked this run)", T5 chat line⏸ parked {feature_name} — BLOCKED ({phase}) · notified ({parked_count} parked this run)(read{phase}from the parked entry / theBLOCKED.mdfrontmatter). Both branches are governed by the SAME dedup set (fire once per newly-parked id; never re-fire; re-seed silently after a compaction boundary).halt (both modes) — fired on every terminal/halt:
NEEDS_INPUThalt,BLOCKEDhalt-for-manual,needs-researchstrict halt,queue-blocked-on-research,queue-missing,all-features-complete,queue-exhausted-all-parked(--parkmode — after the flush),queue-exhausted-budget-deferred(budget-guard — all items deferred to queue tail),max-cycles,device-queue-exhausted,host-capability-saturated, script-error, and any future obstacle terminal. Most of these already carry per-terminalPushNotificationcalls above — this point names the policy explicitly so no terminal can be added without a notification.--run-endis MANDATORY before EVERY terminal/halt PushNotification. On every path listed above, callpython3 ~/.claude/scripts/lazy-state.py --run-endBEFORE the PushNotification fires.--run-enddeletes the run marker AND the prompt registry (all run-scoped enforcement state). A missed deletion is self-healing (24h staleness + session-id mismatch cleanup) but is a protocol violation the retro grades. The call is idempotent — if the marker is already absent (e.g.--run-startfailed earlier),--run-endexits cleanly.Dev-runtime teardown is MANDATORY on run-end (ISSUE 4 — d8-effect-chains run, 2026-06-14). The orchestrator OWNS the dev runtime it pre-booted in Step 1d.0 (
npm run dev:restart), so it MUST tear it down when the run ends — otherwise the runtime (Vite 1420 + MCP 3333 + sidecar + Tauri binary) leaks across runs (a stray dev process was left running after the d8 run). On EVERY terminal/halt path, AFTER--run-endand BEFORE the PushNotification, run the full kill in the orchestrator session:npm run dev:kill # workstation only; no-op-safe if nothing is runningdev:kill(scripts/kill-dev.js) is the only reliable full teardown — it kills Vite, the MCP server, named-pipe-surviving sidecar processes, and orphaned Tauri binaries. Run it UNCONDITIONALLY on workstation runs (it is safe even if the runtime was never booted — e.g. an all-not-requiredqueue). It is N/A for/lazy-batch-cloud(no desktop runtime is ever booted). The mcp-test cycle subagent does NOT kill the orchestrator-owned runtime mid-run (it may be reused next cycle); teardown is the orchestrator's responsibility at run boundary — see mcp-test SKILL.md Step 7.--terminal-reason <reason>(SHOULD — deprecated to omit). When ending a run on a genuine terminal (not an operator-authorized checkpoint), pass--run-end --reason terminal --terminal-reason <reason>where<reason>is one of the sanctioned set:all-features-complete,all-bugs-fixed,max-cycles,cloud-queue-exhausted,device-queue-exhausted,host-capability-saturated,queue-missing,blocked-halt-for-manual,needs-research,queue-blocked-on-research. The script validates<reason>againstlazy_core.SANCTIONED_STOP_TERMINAL— an unsanctioned reason requires--operator-authorizedor the call is refused (exit 1, marker kept). Omitting--terminal-reasonis back-compat (the script infersterminalif--reasonis absent) but is deprecated; include it for stop-authorization validation and retro auditability. (Phase 7 / lazy-validation-readiness.)flush (
--parkmode only) — fired when parked decisions are collected and sent to the operator via the batchedAskUserQuestion(the WU-4 flush protocol). The notification signals that the operator's input is being requested. Message:"lazy-batch flush — {N} parked decision(s) ready for your input".run-end (both modes) — fired when the run terminates and the final batch report is printed. This point largely coincides with the terminal halts above; stating it as a named point ensures every run termination path fires a notification, even if a new exit path is added that does not fit one of the named terminal reasons.
budget-guard trip (both modes, when budget guard fires mid-cycle) — fired ONCE per feature that the budget guard defers/evicts (the
budget_guardprobe field is non-null in the cycle's probe output). The orchestrator reads thebudget_guardfield from the probe JSON and fires:PushNotification({ message: "feature-budget-guard tripped — {budget_guard.feature_id} deferred to queue tail after {budget_guard.count_at_trip} cycles (computed ceiling {budget_guard.computed_ceiling}); advancing to {budget_guard.next_id}" })This is distinct from a terminal notification — the run CONTINUES (the guard defers the over-budget feature and advances to the next independent item, if one exists). A trip notification fires in-cycle, not at halt. If the budget guard trips AND the resulting terminal is
queue-exhausted-budget-deferred(all remaining items are budget-deferred with no independent successor), the trip notification fires first, then the halt notification fires (point 2 above).
Per-cycle
LAZY_QUEUE.mdregen (mobile-queue-control, claude-config + AlgoBooth). At each per-cycle commit point (the pseudo-skill commit below, and the recovery-cycle commit at Step 1d that stages residue), BEFORE thegit add -Athat stages the cycle's changes, runpython user/scripts/lazy-queue-doc.py --repo-root <repo_root>so the regenerated root-levelLAZY_QUEUE.md(the GitHub-mobile-readable queue status doc) is staged by the existinggit add -Aand rides the cycle's commit onmain. The generator is a PURE read over on-disk lazy state viapipeline_visualizer.probe.probe_state— it embeds NO wall-clock, so an unchanged-state regen is byte-identical and adds nothing to the commit (no spurious diff). It is orchestrator-invoked only — NEVER called from thelazy-state.py/bug-state.pycompute path (this keeps the "pure read, never writes during a probe" / one-writer discipline; no state-machine change). For AlgoBooth, the equivalent invocation lands in that repo's/lazy-batch(-cloud) cycle commit (a one-line--repo-root-addressable add — the generator itself needs no AlgoBooth-side change; cross-repo wiring is documented for the operator, not authored from this repo's tree).
1c.5. Inline pseudo-skill handling (NO subagent dispatch)
If sub_skill starts with __ (double-underscore), it is a pseudo-skill — a small sentinel-file write + commit, NOT a real skill that performs implementation work. Perform the action inline (orchestrator session) instead of dispatching a subagent. This is the spirit-preserving relaxation of HARD CONSTRAINT 1: sentinel files are documentation, and dispatching an Opus subagent to write a 10-line YAML block + run git commit wastes a full subagent's worth of context.
Follow ~/.claude/skills/lazy/SKILL.md Step 3's protocol for each pseudo-skill exactly (the wrapper and orchestrator do the same thing here):
__grant_skip_no_mcp_surface__— emitted at Step 9 (workstation only) when the feature's PHASES declares**MCP runtime:** not-requiredAND the repo has no app surface (nosrc-tauri/, nopackage.json). Runpython3 ~/.claude/scripts/lazy-state.py --apply-pseudo __grant_skip_no_mcp_surface__ <spec_path>(the script is the single author of the SKIP_MCP_TEST.md write —granted_by: pipeline-structural, re-verified byskip_waiver_refusal; idempotent; refuses if the repo has an app surface or PHASES is notnot-required), then commit + push per policy. This is the structural short-circuit that avoids dispatching a wasted/mcp-testOpus cycle whose only job would be to confirm there is nothing to test. The next probe routes to__write_validated_from_skip__. Pipeline-advancing →forward_cycles.__write_validated_from_skip__— runpython3 ~/.claude/scripts/lazy-state.py --apply-pseudo __write_validated_from_skip__ <spec_path>(the script is the single author of the VALIDATED.md write — it reads SKIP_MCP_TEST.md, writes VALIDATED.md, and is idempotent), then commit + push per policy.__write_validated_from_results__— runpython3 ~/.claude/scripts/lazy-state.py --apply-pseudo __write_validated_from_results__ <spec_path>(the script reads MCP_TEST_RESULTS.md, writes VALIDATED.md with the extracted scenarios, and is idempotent), then commit + push per policy. The script is the SINGLE author of VALIDATED.md — hand-writing it is the one remaining integrity side-door and is banned (the 2026-06-11 run's endgame bypass). The apply is gated: it refuses (refused:<reason>, zero writes, exit 1) on missing/wrong-kind MCP_TEST_RESULTS.md, onresult≠all-passing, onpass_count != total_count, and on avalidated_committhat doesn't match HEAD (stale results). On a refusal, do NOT retry blindly and do NOT hand-write the sentinel — the refusal names expected vs found; the honest route is a fresh/mcp-testcycle that produces genuinely passing, fresh results (the refusal on stale/partial results IS the pipeline working).__mark_complete__— gated by TWO inline docs-only gates, in order, BEFORE the flip runs. Gate 1 — MCP-coverage audit via the deterministic--gate-coveragesubcommand (unified-pipeline-orchestrator Phase 5): runpython3 ~/.claude/scripts/lazy-state.py --gate-coverage <spec_path>— it reads SPEC.md's## Locked Decisions/## Resolved by Research/ numbered key-decisions surface and greps each<spec_path>/mcp-tests/*.md(RESOLVING symlink/64-byte-pointer targets — the Windows blindspot) for each decision's id + keywords, returning JSON{ok, decisions, uncovered:[id], scenario_count}(exit 1 iffuncovered[]is non-empty). The algorithm spec + the D7 routing live in~/.claude/skills/_components/mcp-coverage-audit.md. Ifuncovered[]is non-empty, follow the component's D7 outcome (completeness-policy.md§4 — Gate 1 never asks, no NEEDS_INPUT.md): documented-MCP-untestable decisions get an inline SPEC test-exempt note (a docs-level__mark_complete__edit — HARD CONSTRAINT 1 holds); the rest route to a corrective coverage cycle — dispatch a cycle subagent to author themcp-tests/scenario(s) and run them (meta cycle), emit the⚖ policy:line(s) + D7-digest entries, then on Gate-1 halt: append{forward_cycles + meta_cycles + 1, feature_name, "__mark_complete__ (gate 1 halted)", "{N} uncovered → corrective coverage cycle"}tocycle_log, incrementforward_cycles(gate-halted mark-complete is still a forward-advancing attempt), and return to Step 1a — the next mark-complete attempt re-auditsclean. Gate 2 — completion-integrity gate per the shared~/.claude/skills/_components/completion-integrity-gate.mdcomponent (runs ONLY after gate 1 returnsclean): verify phase-coherence (zero non-verification unchecked deliverables in PHASES.md) and that a validation sentinel (VALIDATED.md, orSKIP_MCP_TEST.md; workstation does NOT accept a bareDEFERRED_NON_CLOUD.md) exists. (RETRO_DONE.mdis NO LONGER required here — retro is unwired, 2026-06.) If a precondition fails, the orchestrator writes<spec_path>/NEEDS_INPUT.md(written_by: completion-integrity-gate) describing the gap and commits it; append{forward_cycles + meta_cycles + 1, feature_name, "__mark_complete__ (gate 2 halted)", "<reason> → NEEDS_INPUT.md"}tocycle_log, incrementforward_cycles, return to Step 1a — the next state-script call returnsterminal_reason: needs-inputand Step 1g handles it before the next mark-complete attempt (Gate 2's integrity gaps are NOT scope decisions; they keep the needs-input path). Only when BOTH gates pass does the orchestrator proceed: runpython3 ~/.claude/scripts/lazy-state.py --apply-pseudo __mark_complete__ <spec_path>— the script is the single author of COMPLETED.md (kind: completed, provenance: gated, folding the validation evidence from VALIDATED.md/MCP_TEST_RESULTS.md into the receipt body — the durable prooflazy-state.pyStep 2 keys on), the SPEC.md/PHASES.md**Status:** Completeflip, the deletion of the consumed VALIDATED.md/RETRO_DONE.md/DEFERRED_NON_CLOUD.md sentinels (COMPLETED.md/SKIP_MCP_TEST.md/MCP_TEST_RESULTS.md are kept), thedocs/features/queue.jsontrim (now by RESOLVEDspec_dir, killing the-followupsqueue.no-completed class), AND thedocs/features/ROADMAP.mdstrikethrough (moved INTO--apply-pseudoas of unified-pipeline-orchestrator Phase 5 — the orchestrator no longer hand-strikes the ROADMAP row; the subcommand returnsroadmap_struck/queue_trimmed). Mechanical third gate:--apply-pseudoitself ALSO enforces per-phase coherence before writing — it auto-flips all-ticked phases to Complete and REFUSES (refused:<reason>, zero writes) if any phase retains an unchecked box (verification rows included) or a non-Complete/Superseded Status. Onok: false+ this refusal, do NOT retry blindly — route a corrective coherence cycle. Emit the dispatch via the script (registry-registered, guard allows it):
python3 ~/.claude/scripts/lazy-state.py \
--emit-dispatch coherence-recovery \
--context item_name="{feature_name}" \
--context spec_path="{spec_path}" \
--context gate_output="<the --apply-pseudo refusal reason string>" \
--context item_id="{feature_id}" \
--context cwd="{cwd}"
Dispatch dispatch_prompt VERBATIM using dispatch_model. The @requires keys for --emit-dispatch coherence-recovery are: item_name, spec_path, gate_output, item_id, cwd. The subagent reconciles PHASES.md honestly (tick-with-evidence or re-scope, never blind-tick) then returns to Step 1a. Exactly as a Gate-1 halt routes. The ROADMAP strikethrough is NO LONGER an orchestrator step — --apply-pseudo __mark_complete__ strikes the docs/features/ROADMAP.md row itself (unified-pipeline-orchestrator Phase 5; returns roadmap_struck). Then commit + push per project policy. See the component files for the full gate algorithms. Both gates are docs-only (read SPEC.md / PHASES.md / mcp-tests/*.md / sentinels, no Tauri / no MCP server) — they run identically in workstation and cloud.
__flip_plan_complete_cloud_saturated__— emitted only bylazy-state.py --cloudat Step 7a when anIn-progressplan's only unchecked WUs (scoped to the plan'sphases:field) are documented in<spec_path>/DEFERRED_NON_CLOUD.mdas workstation-only.sub_skill_argsis the absolute plan-file path. Runpython3 ~/.claude/scripts/lazy-state.py --apply-pseudo __flip_plan_complete_cloud_saturated__ <spec_path> --plan <plan_file_path>(the script edits only thestatus:line in the plan frontmatter →Complete, is idempotent, and does NOT touch SPEC.md, ROADMAP.md, or any sentinel). Derive the plan part number from the plan'sphases:field for the commit message (e.g.phases: [6]→ part 6; fall back to the plan filename's leadingpart-N/phase-Ntoken). Commit per project policy with messagechore(<feature_id>): mark plan part N Complete (cloud-saturated), then push. This is a forward cycle — incrementforward_cycles.__flip_plan_complete_stale__— emitted bylazy-state.pyat Step 7a (in both cloud and workstation mode) when EVERY work-unit a Ready/In-progress plan references is already[x]— the plan is stale/already-applied but the frontmatterstatus:was never flipped.sub_skill_argsis the absolute plan-file path. Action (stays inline —--apply-pseudodoes NOT implement stale): read the plan's YAML frontmatter, edit ONLY thestatus:line in place (ReadyorIn-progress→Complete) — leave every other field and the markdown body untouched. Derive the plan part number from the plan'sphases:field; fall back to the plan filename's leadingpart-N/phase-Ntoken ifphases:is missing. Stage the plan file and commit per project policy with messagechore(<feature_id>): mark plan part N Complete (stale — already applied). Do NOT touch SPEC.md, ROADMAP.md, or any other sentinel. Distinction from__flip_plan_complete_cloud_saturated__: stale fires in BOTH cloud and workstation (it is not cloud-only) and means every WU was already[x]— not deferred to workstation, genuinely done. Without this flip theStep 7a: execute planprobe would return an In-progress plan with all WUs done, the orchestrator would dispatch/execute-planagainst it, the subagent would find no work, make no commit, and the next cycle would return the same state — a no-op loop. This is a meta cycle — incrementmeta_cycles(flipping a stale plan is cleanup, not forward implementation work).__mark_fixed__(thetype == bugterminal — unified driver; item 2, lazy-batch-unified-driver-parity-and-accounting Phase 3) — gated by the SAME TWO inline docs-only gates as__mark_complete__, in order, BEFORE the archive runs. This block is the bug-pipeline twin of__mark_complete__above and mirrors/lazy-bug-batchStep 1c.5's__mark_fixed__block VERBATIM — drive it withbug-state.py(NOTlazy-state.py) for atype == bugcycle.Gate 1 — MCP-coverage audit per
~/.claude/skills/_components/mcp-coverage-audit.md(or the deterministicpython3 ~/.claude/scripts/bug-state.py --gate-coverage <spec_path>equivalent). Run with{spec_path}and{bug_id}. Ifuncovered:N, follow its D7 outcome (completeness-policy.md§4 — Gate 1 never asks, no NEEDS_INPUT.md): documented-MCP-untestable decisions get an inline SPEC test-exempt note; the rest route to a corrective coverage cycle (dispatch a cycle subagent to author themcp-tests/scenario(s) + run them — meta cycle), with⚖ policy:line(s) + D7-digest entries. Do NOT run the archive steps. Append tocycle_log{forward_cycles + meta_cycles + 1, bug_name, "__mark_fixed__ (gate 1 halted)", "{N} uncovered → corrective coverage cycle"}, incrementforward_cycles(gate-halted mark-fixed is still a forward-advancing attempt), return to Step 1a — the next mark-fixed attempt re-auditsclean.Gate 2 — completion-integrity gate per
~/.claude/skills/_components/completion-integrity-gate.md(runs ONLY after gate 1 returnsclean). Adapted for bugs:kind: fixed,filename: FIXED.md. If a precondition fails, write{spec_path}/NEEDS_INPUT.md(written_by: completion-integrity-gate), commit it, and returnrefused:<reason>— same halt-cycle-and-surface-via-Step-1g pattern as gate 1 (Gate 2's integrity gaps keep the needs-input path).Only when BOTH gates pass: run
python3 ~/.claude/scripts/bug-state.py --apply-pseudo __mark_fixed__ {spec_path}— the script is the single author of theFIXED.mdreceipt (kind: fixed,provenance: gated, folding validation evidence from VALIDATED.md / MCP_TEST_RESULTS.md into the receipt body), the SPEC.md/PHASES.md**Status:** Fixedflip, and the deletion of the consumed VALIDATED.md / RETRO_DONE.md (if stale) / DEFERRED_NON_CLOUD.md sentinels (FIXED.md / SKIP_MCP_TEST.md / MCP_TEST_RESULTS.md are kept). Mechanical third gate inside--apply-pseudo __mark_fixed__: the script auto-flips all-ticked phases to Complete and REFUSES (refused:<reason>, zero writes) if any phase retains an unchecked box (verification rows included) or a non-Complete/Superseded Status. Onok: false+ this refusal, do NOT retry blindly — route a corrective coherence cycle viapython3 ~/.claude/scripts/bug-state.py --emit-dispatch coherence-recovery --context item_name="{bug_name}" --context spec_path="{spec_path}" --context gate_output="<the --apply-pseudo refusal reason string>" --context item_id="{bug_id}" --context cwd="{cwd}"and dispatchdispatch_promptVERBATIM. The orchestrator NEVER hand-writes the receipt, the status flip, or the sentinel deletions.After the script returns, the orchestrator runs ONE more script call — the archive mechanics are also script-owned per
~/.claude/skills/_components/mark-fixed-archive.md:python3 ~/.claude/scripts/bug-state.py --repo-root {repo_root} --archive-fixed {spec_path}(SPEC evidence header lines, staged-deletion-coherentgit mvtodocs/bugs/_archive/with Windows-lock retry, tracked-only inbound-reference repoint,docs/bugs/queue.jsontrim, atomic commit — then push the commit it created). The orchestrator performs ZERO hand edits for the archive; onok: falseit writes{spec_path}/BLOCKED.md(blocker_kind: archive-failure) quoting the script'srefuseddiagnostic verbatim (sentinel-scope — within HARD CONSTRAINT 1). The call is idempotent and resume-safe — a PARTIAL STATE diagnostic means re-run, never hand-unwind. Pipeline-advancing →forward_cycles.
After the inline action:
- Append to
cycle_log:{forward_cycles + meta_cycles, feature_name, sub_skill, "inline: <one-line summary>"}(use the UPDATED total after the increment in step 5 below, i.e. the N-th total action completed this invocation). - Push backstop (guardrail C — mirrored from
/lazy-batch-cloud). The inline pseudo-skill committed a sentinel / plan-frontmatter change locally; push it now —git push origin $(git rev-parse --abbrev-ref HEAD)(retry up to 4× with exponential backoff 2s/4s/8s/16s on network error; WORK BRANCH only, never main, never force). This backstops inline cycles the orchestrator owns directly — agit pushof an already-committed change, NOT a Write/Edit, so HARD CONSTRAINT 1 still holds. "Up to date" is a fine result (a prior cycle's push already carried it). - Emit the T4 inline pseudo-skill block (Step 3 / orchestrator-voice.md): the canonical step heading (
### {Step name} — {work summary} [x/y]), anactline ({sub_skill} → {feature_id}), agatesline when gates ran (__mark_complete__), adoneline (inline outcome), and anextline. Nothing else. A gate REFUSAL switches to T6-refusal (rich) — the refusal evidence and the NEEDS_INPUT routing deserve full detail. - Update
prev_cycle_signature = (feature_id, sub_skill, sub_skill_args, current_step)(same uniform post-cycle update as Step 1e — keeps loop-guard accurate across mixed pseudo-skill / real-skill cycles). - Increment the appropriate counter:
forward_cyclesfor pipeline-advancing pseudo-skills (__mark_complete__,__mark_fixed__,__write_deferred_non_cloud__(cloud variant only — workstationlazy-state.pynever emits this),__write_validated_from_results__,__write_validated_from_skip__,__grant_skip_no_mcp_surface__,__flip_plan_complete_cloud_saturated__);meta_cyclesfor cleanup pseudo-skills (__flip_plan_complete_stale__). Return to Step 1a — DO NOT fall through to Step 1d.
This saves one Opus dispatch per pseudo-skill action. On a typical feature lifecycle (workstation: 1 × __write_validated_* + 1 × __mark_complete__ = 2 dispatches reclaimed; cloud: 1 × __write_deferred_non_cloud__ minimum) the savings compound across a multi-feature queue pass.
1d. Compose and dispatch the cycle subagent (REAL SKILLS ONLY)
Compaction discipline — re-read the dispatch template AND the output contract first. Before composing this dispatch — and ALWAYS as the first action after any compaction boundary — re-read ~/.claude/skills/_components/lazy-dispatch-template.md, ~/.claude/skills/_components/orchestrator-voice.md (the chat-output contract — its turn templates survive summarization by re-read, not by memory; the re-reads themselves are silent mechanics), AND ~/.claude/skills/_components/completeness-policy.md (the D7 standing policy — its auto-resolve rules likewise survive compaction by re-read, not memory). The dispatch template is the on-disk canonical dispatch skeleton (subagent_type, the REQUIRED model: field, prompt envelope) and carries the Read-before-Edit rule: compaction resets read-state, so re-Read any file (PHASES.md, plans, SKILLs, components) before you Edit/Write it. The prompt contents are NOT reconstructed by hand — they arrive pre-bound from the probe's cycle_prompt (the --emit-prompt field); the template governs only the dispatch ENVELOPE (which fields, which model). 41% of post-compaction spawns in the 2026-06-10 audit dropped the model: field — re-reading this template before each dispatch, and copying cycle_model into it rather than reconstructing prompts from memory, is what prevents that.
Post-compaction re-entry protocol (HARD — the first post-compaction action is NEVER a dispatch). Compaction is the measured protocol cliff (2026-06-11 run: after the compaction boundary the counters never recovered, probes stopped entirely, and prompts went hand-authored for the rest of the run). On the first turn after any compaction boundary, BEFORE any Agent call: (1) re-read Step 1a of this SKILL plus the three components named above; (2) the session counters (forward_cycles, meta_cycles) are persisted in the run marker — the post-compaction probe reads them from the marker directly, so no manual reconstruction is needed. As a cross-check, verify the surviving T1/T2/T4 context broadly agrees with the marker's counters; if there is a discrepancy, trust the marker (it is the script-owned source of truth) and record any notable divergence in a single T6 ⚠ line; (3) run the FULL Step 1a probe form (--repeat-count --emit-prompt --probe --max-cycles …) — note: --forward-cycles/--meta-cycles are NOT passed (the marker owns the counters); proceed only from its output. Dispatching from a pre-compaction probe held in memory, or from a hand-reconstructed prompt, is a contract violation.
Governing-file reload discipline (self-edit mode — C8). When the Step 1a probe reports self_edit_mode: true, this /lazy-batch run is editing the very harness it executes from, so a cycle that commits to the orchestrator's own in-context governing prose makes the copy you hold stale. After EVERY cycle, intersect the cycle's commit (git diff --name-only, or read the probe's governing_files_touched list — the script computes the same intersection for you) with the governing-file set — the files the orchestrator holds in-context and does NOT get for free from a fresh subprocess / disk-read:
user/skills/lazy-batch/SKILL.md(+ theuser/skills/lazy-bug-batch/SKILL.mdandrepos/algobooth/.claude/skills/lazy-batch-cloud/SKILL.mdtwins for those orchestrators)user/skills/_components/orchestrator-voice.md,user/skills/_components/completeness-policy.md,user/skills/_components/lazy-dispatch-template.md
For ANY hit, re-Read that file via its ~/.claude/... path BEFORE composing the next dispatch. This is the SAME re-read as the compaction discipline above — triggered by a self-edit commit instead of a compaction boundary — and the governing-file set MUST stay in lockstep with that compaction re-read list (if the compaction list grows, this set grows with it). The re-read is a silent mechanic (no chat narration).
Auto-refresh boundary (documented no-ops — MUST NOT be reloaded; they were never stale). These surfaces are ALREADY live on the next probe/dispatch and are EXCLUDED from the governing-file set by construction — never re-read them as part of the reload check: lazy_core.py / lazy-state.py / bug-state.py (a fresh python3 subprocess runs every probe); lazy-batch-prompts/cycle-base-prompt.md + its addenda + loop-block.md (re-read by emit_cycle_prompt from disk every probe); hook .sh bodies (bash ~/.claude/hooks/X.sh reads the file each invocation); and downstream skill prose (each dispatched subagent loads its skill fresh).
New-hook-registration restart surfacing (T6). If a cycle's commit added or REMOVED a hook ENTRY in settings.json (a new PreToolUse/PostToolUse wiring object — NOT merely an edit to an already-wired script body, which is an auto-refresh no-op above), surface a single T6 line: ⚠ settings.json hook wiring changed — restart the session to (de)register; the running session still uses the old wiring. Do NOT claim the change is live — hook registration is read at session start, so only a session restart re-registers it. Distinguish an ENTRY add/remove (restart-required) from a script-body edit (already live) explicitly.
Never hand-append to cycle_prompt. Repo-specific instructions (an audio-INVARIANTS gate, a project HARD requirement) live in <repo>/.claude/skill-config/cycle-prompt-addenda.md (same @section grammar as the base template) — the SCRIPT reads that file and appends the matching sections to cycle_prompt, token-bound and residue-checked. A live orchestrator hand-spliced the AlgoBooth audio gate onto the emitted prompt on 2026-06-11; that path is now closed — if a repo gate is missing, add a section to the addenda file, do not edit the dispatch.
Long-build ownership (harness-tracked). Any build or test that may exceed a single subagent turn is orchestrator-owned: start it with Bash run_in_background: true from this (the orchestrator) session and track it via the harness — NEVER background it from inside a dispatched cycle subagent, whose process tree is torn down when its turn ends (a tauri build backgrounded that way once silently vanished). Before committing to a 20–40 min packaged tauri build, run cargo check --release first to catch compile errors in minutes. Full rule: .claude/skill-config/long-build-ownership.md. This is Bash-only process ownership — it does not expand the orchestrator's sentinel-only Write/Edit scope (HARD CONSTRAINT 1 holds).
If Step 1c.5 did not handle this cycle (i.e. sub_skill is a real skill name, not __*__), build a minimal subagent prompt. The prompt instructs the subagent to invoke ONE skill in batch mode, commit, and report — nothing else.
1d.0. Pre-boot the dev runtime for /mcp-test cycles (WORKSTATION ONLY — runs BEFORE prompt composition)
Applies ONLY when sub_skill == "mcp-test". Skip this sub-step entirely for every other sub_skill. (This sub-step does not exist in /lazy-batch-cloud — cloud's Step 9 returns __write_deferred_non_cloud__, never mcp-test, so the cloud orchestrator never reaches it.)
Why this exists (the failure it fixes). The cycle subagent has NO Agent tool (HARD CONSTRAINT block above) and runs /mcp-test INLINE. The mcp-test SKILL.md Step 2 boots npm run tauri:dev as a background task, then Step 4 waits for readiness. Empirically, an inline cycle subagent that started a background build and then ENDED ITS TURN waiting on it produced a premature, resultless return: the background build process did NOT survive the subagent's turn boundary, and the orchestrator (SendMessage unavailable in this workstation environment) could not resume the dead subagent. Net: a validation cycle that wrote no result and no sentinel, burning the whole cycle. The structural fix is for the orchestrator's own session — which is long-lived and persists across subagent turns — to OWN the dev-runtime background process, so the runtime is already up and MCP-ready when the mcp-test subagent connects to it.
Procedure (orchestrator session, all Bash — NOT file edits):
Plan-declared structural untestability — skip the boot entirely (routing only, NOT a waiver). Check the feature's PHASES.md for an
**MCP runtime:**header line (authored by/spec-phasesat decomposition time):grep -m1 '^\*\*MCP runtime:\*\*' "{spec_path}/PHASES.md"- Line says
not-required→ skip steps 1–3 entirely (no probe, nodev:restart, no readiness block — the ~3–7.5 min boot is pure waste when the deliverable has no MCP surface). The mcp-test prompt VARIANT (runtime-up vs no-runtime, with{untestability_reason}bound) is chosen by the SCRIPT —emit_cycle_promptreads the same PHASES.md**MCP runtime:**line and selects the matching section, so the orchestrator does NOT swap any prompt block by hand here; the script-assembledcycle_promptalready carries the correct variant. The skip AUTHORITY stays with the mcp-test cycle (it verifies the plan's claim againstdocs/features/mcp-testing/SPEC.mdand writes thegranted_by: mcp-test+spec_classsentinel only if it concurs) — the plan field is routing, never a grant. - Line absent or
required→ proceed with steps 1–3 as written.
NEEDS_RUNTIME recovery: if a no-runtime mcp-test cycle returns the single line
NEEDS_RUNTIME(it found an MCP-testable surface the plan missed), run steps 1–3 NOW, then emit the re-dispatch via the script (registry-registered, guard allows it):- Line says
python3 ~/.claude/scripts/lazy-state.py \
--emit-dispatch needs-runtime-redispatch \
--context item_name="{feature_name}" \
--context spec_path="{spec_path}" \
--context original_cycle_prompt_note="mcp-test cycle found MCP-testable surface; plan declared not-required" \
--context item_id="{feature_id}" \
--context cwd="{cwd}"
Dispatch dispatch_prompt VERBATIM using dispatch_model. The @requires keys for --emit-dispatch needs-runtime-redispatch are: item_name, spec_path, original_cycle_prompt_note, item_id, cwd. The failed attempt + re-dispatch together consume ONE forward cycle (increment once, after the re-dispatched cycle returns); tag the re-dispatch disp line (opus, recovery). A disagreement costs one extra dispatch round-trip — never correctness.
Ensure the runtime is up, current, AND owned — ONE
--ensure-runtimesubcommand call consuming the M4 verdict (long-build-and-runtime-ownership Phase 5, atop unified-pipeline-orchestrator Phase 5). The probe → stale-binary check →dev:restart(bg) → curl-until-200 → MCP-tool assertion dance is thelazy-state.py --ensure-runtimesubcommand (lazy_core.ensure_runtime), now the M4 liveness/recovery state machine. Run it ONCE instead of any hand-composed rebuild→health-poll→inspect until-loop — there is NO hand-rolled poll loop in the cycle prompt:python3 ~/.claude/scripts/lazy-state.py --ensure-runtime --repo-root "{cwd}" # → M4 verdict JSON: # {state: READY|STALE|HIJACKED|DEAD|BLOCKED, # ownership_verified: bool, health_code: int, mcp_tools_present: bool, # terminal_blocker: str|null, # status: ready|booted|stale-rebuilt} # legacy field, retained (superset)Route on the FULL verdict's
state(not a field-extracted subset — see the "route from the FULL probe JSON" rule below). The subcommand already performed Identity (ownership) → Staleness → Health classification + bounded recovery (STALE/DEAD auto-recover viarestart()in an exponential-backoff loop capped at 5 attempts); the orchestrator only ROUTES on the returnedstate:state: READY— runtime is up, current, healthy, AND owned (.runtime.lock.jsonverified against the live kernel start_time + this run's session). Proceed to dispatch ONLY whenstate == READY AND health_code == 200 AND mcp_tools_present(a conjunction — read all three from the FULL probe JSON, neverstatealone). Astate: READYpaired with a non-200health_code(ormcp_tools_present: false) is treated as NOT-ready and is NOT dispatched against: the orchestrator OWNS the--ensure-runtimeboot/recovery FIRST (the same orchestrator-owned cold-compile/boot path it already runs at the top of Step 1d.0) before anymcp-testdispatch — never dispatch a subagent against a runtime the verdict itself reports as non-serving. Thishealth_code/mcp_tools_presentcross-check is defense-in-depth atop the Phase 1 producer fix (ensure-runtime-legacy-mode-optimistic-ready-verdict): after that fix the producer never emitsREADYwith a non-200health_code(the legacy branch derives its verdict from the actual re-probe code, exactly as the M4 path does), so this guard should never fire in practice — it exists to catch a future producer regression and the residualmcp_tools_present: falsecase thestate: BLOCKEDbullet already names. It introduces NO newblocker_kindand NO new state: a miss reuses the existing orchestrator-owned--ensure-runtimeboot path (and, if recovery exhausts, the existingmcp-runtime-unreadyBLOCKED.md), exactly as theDEAD/HIJACKED/BLOCKEDbullets below already do.state: READYwithownership_verified: falseis ALSO a "proceed to dispatch" outcome — the soft owned-unverified READY (ensure-runtime-false-hijacked-on-owned-serving-runtime). This is a self-booted, serving runtime whose ownership diverges ONLY on the session component: the live PID is the SAME process this run booted (kernel start_time matches the recorded lock start_time) and it is provably serving THIS app's MCP tools (health_code == 200+mcp_tools_present), but the lock'scontroller_session_idand the threadedlive_session_idcome from different sources and do not match (a Bash-driven single-session orchestrator). The proceed-conjunction above ALREADY admits it — it readsstate/health_code/mcp_tools_present, NEVERownership_verified— so anownership_verified: falseREADY proceeds with no special-casing. Say so explicitly: do NOT hand-verify the runtime, do NOTdev:kill+ cold-boot, and do NOT writeBLOCKED.md— the producer no longer false-reports HIJACKED for the owned-serving case (the relaxation lives in_ensure_runtime_m4's classifier; seeuser/scripts/CLAUDE.md--ensure-runtime). The genuine-foreign case (no matching PID, or a divergent live PID/start_time) still classifies HIJACKED (next bullet) and stays terminal.
state: STALEthat recovered to READY — the native binary was stale (a newersrc-tauri/cratescommit exists since boot, via thestale_binarypredicate); the subcommand forced adev:restartso the mcp-test cycle sees the current tool registry, then re-verified READY. (Legacystatus: stale-rebuilt; spec reference: F7 indocs/specs/lazy-validation-readiness/SPEC.md.) Proceed to dispatch.state: DEADthat recovered to READY — runtime was down; the subcommand started/restarted it (backgrounddev:restart, orchestrator-owned) and re-verified health=200 + ownership. (Legacystatus: booted.) Proceed to dispatch.state: HIJACKED— a GENUINELY FOREIGN process holds the port: the recorded lock PID is either dead (→ DEAD recovery) or held by a DIFFERENT live process whose start_time diverges from the recorded lock start_time (real PID reuse). This is now the ONLY case the producer emits HIJACKED for — a self-booted serving runtime that diverges only on the session component classifies the soft owned-unverified READY above, not HIJACKED (ensure-runtime-false-hijacked-on-owned-serving-runtime). The subcommand DID NOT and WILL NOT SIGKILL it (LD3 safety/stability rule — never kill an unowned process). Surface aBLOCKED.md(blocker_kind: mcp-runtime-unready) with the verdict'sterminal_blockertext VERBATIM as the body, and do NOT dispatch a subagent against it.state: BLOCKED— bounded recovery exhausted (restart()retried up to 5× with backoff without restoring a healthy, owned runtime), OR an unrecoverable readiness failure (mcp_tools_present: falseafter recovery / health never reached 200). Surface aBLOCKED.md(blocker_kind: mcp-runtime-unready) with the verdict'sterminal_blockertext VERBATIM as the body, and do NOT dispatch a subagent against a dead runtime — a subagent cannot recover a runtime the orchestrator failed to boot.
Cold-compile patient-wait (ensure-runtime-recovery-starves-cold-compile). A cold/first boot or a new-crate
STALEno longer starves: the subcommand now distinguishes a compiling runtime (Vite:1420up, backend:3333not yet serving — a long cold Rust compile in progress) from a genuinely dead one (both ports down) via a two-port readiness check. Acompilingruntime is PATIENTLY WAITED on (owned, cold-compile-sized ~7.5-min budget, NEVER kill-restarted) and reachesstate: READYwithout the old 5-starved-kill-restarts → false-BLOCKEDsymptom. The ≤5×backoff bounded recovery is now reserved strictly for a genuine crash (dead). Acompilingwait that genuinely never serves within the budget still surfacesstate: BLOCKED— but itsterminal_blockercarries DISTINCT cold-compile-timeout text (the runtime was compiling and waited, not crash-recovery-exhausted), still mapped to the SAMEblocker_kind: mcp-runtime-unreadydownstream (no new blocker_kind, no routing change — the orchestrator surfaces the verdict text VERBATIM exactly as above). Default-off / repo-agnostic: a repo with no:1420frontend behaves byte-identically to before (every non-serving runtime classifiesdead). No new routing branch here —compiling→READY proceeds to dispatch like any other recovered state; the only operator-visible change is a clearer BLOCKED message on a genuine cold-compile timeout.Pre-Vite cold-boot patient-wait (ensure-runtime-starves-pre-vite-sidecar-build). The cold-compile patient-wait above covered only the Vite-up window (its only "still booting" signal was Vite
:1420being up). A cold/first boot ALSO spends its first ~1–2 min in the PRE-VITEBeforeDevCommand/sidecar:buildphase, where BOTH:1420and:3333are down while the spawned boot process is still alive — that window was previously misclassifieddeadand kill-restarted into a falseBLOCKED. The subcommand now consults a SECOND "still booting" signal — boot-process liveness — so a both-ports-down-but-live-boot runtime classifies as booting (not killed): it is PATIENTLY WAITED through the pre-Vite window on the same ~7.5-min cold-compile budget and reachesstate: READYwithout starvation, extending the Vite-up patient-wait above. The genuine-crash path is unchanged: a both-ports-down runtime with NO live boot (never booted / truly crashed and not restarting) still enters the bounded ≤5×backoff recovery. Default-off / repo-agnostic: a repo whose config does not opt into the boot-liveness signal behaves byte-identically to before (a both-ports-down runtime classifiesdead). No new routing branch and no newblocker_kind—booting→READY proceeds to dispatch like any other recovered state.state ∈ {HIJACKED, BLOCKED}(and any residual unrecoverablemcp_tools_present: false) is the ONLY blocking path;state ∈ {READY, STALE, DEAD}having reached READY means the subcommand already recovered — proceed. The AlgoBooth specifics (TCP 3333 health endpoint,npm run dev:restart, thesrc-tauri/cratesnative globs, the asserted MCP tool, the.runtime.lock.jsonlock filename, the port) are parameterized inlazy_core's default config dict — not hand-typed here.Sidecar-pipe readiness dimension (env-transient-counts-against-validation-retry-budget Phase 1, Leg A — repo-agnostic default OFF). The dev HTTP server boots INDEPENDENTLY of the MCP sidecar named pipe, so
health_code: 200does NOT prove the sidecar is connected. A zombie node process holding the:3333pipe after adev:restartleaves the runtime HTTP-healthy but MCP-functionally DEAD — a self-inflicted ENV transient, not a code failure. When a repo setsassert_sidecar_connected: truein its--ensure-runtimeconfig override (AlgoBooth opts in; the default is OFF so non-AlgoBooth repos are unaffected), the M4 Health phase additionally assertsget_sidecar_status.is_connected: true: a pipe-dead-but-HTTP-200 runtime routes through bounded recovery (adev:restartthat reaps the stale pipe) and, on persistent disconnect, tostate: BLOCKED→ the sameblocker_kind: mcp-runtime-unready(escalation-immune) terminal as above — NOT a dispatch against a pipe-dead runtime, and NEVER anmcp-validationcharge against the feature's validation-retry budget. (The cycle prompt's runtime-up variant carries the matching mid-cycleNEEDS_RUNTIMEescape for the case the gate is OFF or the pipe dies after the gate passed.) The subcommand owns the orchestrator-owned background process exactly as the old hand-run steps did (the runtime survives the upcoming subagent's turn boundary), and the M4 lock makes that ownership verifiable across the cycle boundary (Persistent Service contract).Guard-takeover — the orchestrator owns long builds (long-build-and-runtime-ownership Phase 3/4). The
long-build-ownership-guard.shPreToolUse(Bash) hook (registered inuser/settings.json) fail-OPEN denies any subagent attempt to run an exact long-build signature (tauri build/cargo build --release/npm run build), bubbling the literal takeover tokenLONG-BUILD-OWNERSHIP-TAKEOVERin its deny reason. When that token surfaces from a cycle subagent's denied build, the orchestrator session (long-lived, persists across subagent turns) takes over the spawn instead of the subagent: run the build under the Transient Build contract —lazy_core.run_transient_build(cmd, cwd=...)(spawns detached viaspawn_detachedso it survives a subagent tear, then synchronously AWAITS conclusion, capturingexit_code/stdoutfor telemetry) — and on its return calllazy_core.promote_artifact_atomically(staging_dir, final_dir, exit_code=result["exit_code"])so the artifact isos.replace'd into production ONLY onexit_code == 0(a torn/failed build leaves production untouched). The Transient Build contract is DISTINCT from the Persistent Service--ensure-runtimeabove (LD5: one spawn primitive, two contracts) — it does NOT write.runtime.lock.jsonand does NOT leave the process for a future cycle. This isBash/lazy_core-driven process ownership, NOT a file edit — HARD CONSTRAINT 1 holds (same rationale as the runtime boot).**No prompt amendment by ha
Content truncated for page performance. Open the source repository for the full SKILL.md file.