devlyn-cli 3.1.3 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,13 +48,13 @@ fi
48
48
  </runtime_paths>
49
49
 
50
50
  <engine_routing>
51
- Each phase routes to an engine and prepends the per-engine adapter header from `_shared/adapters/<engine>.md` (e.g. `claude.md`, `codex.md`) to the canonical phase body. Adapter is the per-model delta (Anthropic's prompt-engineering guide for Claude, OpenAI's prompt guidance for Codex). Canonical body is engine-agnostic.
51
+ Each model-invoked phase routes to an engine and prepends the per-engine adapter header from `_shared/adapters/<engine>.md` (e.g. `claude.md`, `codex.md`) to the canonical phase body. Adapter is the per-model delta (Anthropic's prompt-engineering guide for Claude, OpenAI's prompt guidance for Codex). Canonical body is engine-agnostic.
52
52
 
53
- - Phase spawning is mandatory for every orchestrator. Same-context PLAN / IMPLEMENT / BUILD_GATE / CLEANUP / VERIFY is a contract violation. If the current CLI cannot spawn a fresh worker, write the current phase verdict as `"BLOCKED"` and report `BLOCKED:fresh-context-unavailable` with the failed spawn command; do not continue with ad-hoc same-context execution.
54
- - Claude Code phases other than PLAN: spawn `Agent` (`mode: "bypassPermissions"`); prompt = adapter-header + canonical-body + task-context. PLAN uses the native foreground call shape in PHASE 1.
55
- - Codex CLI phases: shell out via `bash "$CODEX_MONITORED_PATH"` with the same compounded prompt. Each `codex exec` child is a new session/fresh context. Write the compounded prompt to a file and set `DEVLYN_CODEX_PROMPT_FILE` with sole prompt argument `-`; the wrapper snapshots exact stdin bytes, seals transport evidence and emits a heartbeat. Without file transport stdin remains DEVNULL. No MCP. The wrapper call is foreground-blocking — never a background shell (`run_in_background`, `&`, `nohup`), never end your message while it runs: headless print-mode wind-down kills backgrounded children (0-byte delivery); the heartbeat is the observability channel.
56
- - oh-my-pi phases: spawn the native `task` tool with a fresh `context` containing adapter-header + canonical-body + task-context. Capture the task result into `.devlyn/<phase>.stdout` and any tool error into `.devlyn/<phase>.stderr` before updating state. If the `task` tool is unavailable for an omp-routed phase, write the current phase verdict as `"BLOCKED"` and report `BLOCKED:fresh-context-unavailable`; do not fall back to same-context execution or a nested `omp -p` subprocess.
57
- - Default engine: Claude when the orchestrator has Claude Code’s native `Agent`; otherwise its own fresh worker. PLAN is orchestrator-fixed and never inherits `--engine`, an executor pin, or `state.engine`. BUILD_GATE and probes retain their existing routes. `_shared/engine-preflight.md#role-resolution` defines the single resolver and explicit `--role-config` / project role precedence. Absent profiles preserve legacy `--engine` / executor / default behavior; VERIFY primary may be independently selected. Configured role/priority pins and flags fail closed on unavailable engine or failed authentication, including failures discovered at dispatch. Unconfigured automatic VERIFY selects an available OTHER engine.
53
+ - PLAN runs in the owner context; BUILD_GATE and CLEANUP run orchestrator commands with no separate model. The selected IMPLEMENT worker also owns code/doc cleanup before its final checkpoint. IMPLEMENT and VERIFY still require fresh workers. If the current CLI cannot spawn a required worker, write the current phase verdict as `"BLOCKED"` and report `BLOCKED:fresh-context-unavailable` with the failed spawn command; do not continue with ad-hoc same-context execution.
54
+ - Claude Code model-invoked phases: spawn `Agent` (`mode: "bypassPermissions"`); prompt = adapter-header + canonical-body + task-context.
55
+ - Codex CLI model-invoked phases: shell out via `bash "$CODEX_MONITORED_PATH"` with the same compounded prompt. Each `codex exec` child is a new session/fresh context. Write the compounded prompt to a file and set `DEVLYN_CODEX_PROMPT_FILE` with sole prompt argument `-`; the wrapper snapshots exact stdin bytes, seals transport evidence and emits a heartbeat. Without file transport stdin remains DEVNULL. No MCP. The wrapper call is foreground-blocking — never a background shell (`run_in_background`, `&`, `nohup`), never end your message while it runs: headless print-mode wind-down kills backgrounded children (0-byte delivery); the heartbeat is the observability channel.
56
+ - oh-my-pi model-invoked phases: spawn the native `task` tool with a fresh `context` containing adapter-header + canonical-body + task-context. Capture the task result into `.devlyn/<phase>.stdout` and any tool error into `.devlyn/<phase>.stderr` before updating state. If the `task` tool is unavailable for an omp-routed phase, write the current phase verdict as `"BLOCKED"` and report `BLOCKED:fresh-context-unavailable`; do not fall back to same-context execution or a nested `omp -p` subprocess.
57
+ - Default engine: Claude when the orchestrator has Claude Code’s native `Agent`; otherwise its own fresh worker. PLAN is orchestrator-fixed and never inherits `--engine`, an executor pin, or `state.engine`. BUILD_GATE runs orchestrator commands; probes retain their existing routes. `_shared/engine-preflight.md#role-resolution` defines the single resolver and explicit `--role-config` / project role precedence. Absent profiles preserve legacy `--engine` / executor / default behavior; VERIFY primary may be independently selected. Configured role/priority pins and flags fail closed on unavailable engine or failed authentication, including failures discovered at dispatch. Unconfigured automatic VERIFY selects an available OTHER engine.
58
58
  - The `--engine` flag does not disable default pairing: the second judge uses the OTHER engine by default when available.
59
59
  - Multi-LLM evolution: when a new model adapter ships in `_shared/adapters/`, that engine becomes selectable for the roles its adapter declares eligible without further skill changes (NORTH-STAR.md "Multi-LLM evolution direction").
60
60
  </engine_routing>
@@ -72,7 +72,7 @@ After `state.implement_passed_sha` is set: SURFACE_CLOSE and CLEANUP are limited
72
72
  </post_implement_invariant>
73
73
 
74
74
  <transition_protocol>
75
- For every direct complete→spawn handoff, call `state-phase-write.py ... --phase <current> transition` with the current phase's normal completion/attestation arguments plus caller-specified `--next-phase`, `--next-round`, `--next-triggered-by`, `--next-engine`, `--next-model`, and any next-phase metadata. The verb validates a legal edge and commits both lifecycle writes atomically; it returns JSON state facts only. It never selects a phase/engine, renders a prompt, or spawns an agent. Use standalone `spawn` only for the initial post-bootstrap dispatch; use standalone `complete` when no next phase opens (including a halt).
75
+ For every direct complete→spawn handoff, call `state-phase-write.py ... --phase <current> transition` with the current phase's normal completion/attestation arguments plus caller-specified `--next-phase`, `--next-round`, `--next-triggered-by`, `--next-engine`, `--next-model`, and any next-phase metadata. For owner PLAN/BUILD_GATE/CLEANUP, omit engine/model/prompt/session arguments. PLAN records `execution_kind: "orchestrator_context"`; BUILD_GATE/CLEANUP record `execution_kind: "orchestrator_commands"`, all with null worker identity. Existing metadata-bearing PLAN/CLEANUP and already-open absent-kind BUILD spans retain historical worker validation; never relabel their evidence. The verb validates a legal edge and commits both lifecycle writes atomically; it returns JSON state facts only. It never selects a phase/engine, renders a prompt, or spawns an agent. Use standalone `spawn` only for the initial post-bootstrap dispatch; use standalone `complete` when no next phase opens (including a halt).
76
76
  </transition_protocol>
77
77
 
78
78
  ## PHASE 0: PARSE + CLASSIFY + ROUTE
@@ -97,7 +97,7 @@ Outer-owner boundary: before normal task writes, follow `references/task-complet
97
97
 
98
98
  5. Announce one line: `resolve starting — run <run_id> — engine <engine> — mode <mode> — complexity <complexity-or-na> — pair <on|solo:auto_pair_other_engine_unavailable|disabled> — risk_probes <on|off>`.
99
99
 
100
- 6. Open the initial post-bootstrap span with standalone `state-phase-write.py ... spawn`: `plan` before its worker dispatch (normal run), or `verify` before MECHANICAL (verify-only). The bootstrap never chooses a phase, engine, branch, prompt, or agent.
100
+ 6. Open the initial post-bootstrap span with standalone `state-phase-write.py ... spawn`: `plan` before owner planning (normal run), or `verify` before MECHANICAL (verify-only). The bootstrap never chooses a phase, engine, branch, prompt, or agent.
101
101
 
102
102
  ### Explicit role dispatch
103
103
 
@@ -111,17 +111,13 @@ After the actual process exits, run `python3 "$DEVLYN_SHARED_DIR/judge-role-evid
111
111
 
112
112
  ## PHASE 1: PLAN
113
113
 
114
- Skip in verify-only mode. The heaviest phase by design — spec/criteria define non-negotiable invariants; plan formalizes how the implementation hits them.
114
+ Skip in verify-only mode. PLAN is orchestrator-fixed and never inherits `--engine`, an executor pin, or `state.engine`. The owner reads `references/phases/plan.md` and the original request/spec, then writes `.devlyn/plan.md` before product edits. Open with `state-phase-write.py ... --phase plan spawn --round 0`, without worker identity or a prompt dispatch. This is owner reasoning, not zero-cost mechanical work.
115
115
 
116
- Engine: PLAN is orchestrator-fixed and never inherits `--engine`, an executor pin, or `state.engine`; Claude Code uses a native Claude `Agent` worker, while Codex CLI / oh-my-pi use their own fresh worker per `_shared/engine-preflight.md` (PLAN-pair is **research-only** — iter-0033d/f/g; unblock conditions in PHASE 1 / iter-0033g §H; iter-0020 falsified Codex-BUILD/IMPLEMENT, NOT PLAN-pair). Before rendering, assign the current orchestrator's literal adapter name (`PLAN_ENGINE=claude`, `PLAN_ENGINE=codex`, or `PLAN_ENGINE=omp`) and assign `PLAN_MODEL=<exact model id passed to that fresh worker>`; replace the metavalue and fail closed if either assignment is empty. Write the data-only task context to `.devlyn/plan.task-context` with `PLAN_WORKING_DIR="$(pwd -P)"`: its first two lines are exactly `Working directory: $PLAN_WORKING_DIR` and `Plan output: $PLAN_WORKING_DIR/.devlyn/plan.md`, followed by the task data. For round `PLAN_ROUND`, render adapter + `references/phases/plan.md` + task context without byte normalization to `PLAN_PROMPT_FILE=.devlyn/plan.prompt.$PLAN_ROUND`, and set `PLAN_PROMPT_SHA256="$(python3 "$DEVLYN_SHARED_DIR/phase-prompt-render.py" --adapter "$DEVLYN_SHARED_DIR/adapters/$PLAN_ENGINE.md" --canonical-body "$DEVLYN_SKILL_DIR/references/phases/plan.md" --task-context .devlyn/plan.task-context --output "$PLAN_PROMPT_FILE")"`. The renderer fails closed before writing the round-scoped prompt if either fixed header field is missing, non-absolute, or mismatches the active repository paths. Open round 0 with `state-phase-write.py ... --phase plan spawn --round 0 --engine "$PLAN_ENGINE" --model "$PLAN_MODEL" --prompt-sha256 "$PLAN_PROMPT_SHA256"`. In Claude Code, invoke the native `Agent` with its `prompt` field set to the exact `PLAN_PROMPT_FILE` bytes without appending a terminal LF (the renderer emits none), explicitly set `run_in_background: false`, and omit `mode`; this is a data field, so a path, `$(cat ...)`, or any variable-reference substitution is not the rendered prompt. If native input validation rejects this call, surface the failure without retrying a different `mode` or background shape and fail closed through the existing `BLOCKED:fresh-context-unavailable` path. The PLAN worker inherits the parent's permission context: the hands-free parent already uses native `bypassPermissions`, while a non-bypass parent's permission prompt remains the user's native boundary and is never silently escalated. A Codex CLI PLAN must pass explicit `-m "$PLAN_MODEL"` and the seven `DEVLYN_INVOCATION_*` values around `codex-monitored.sh`, using phase `plan`, `PLAN_ROUND`, `PLAN_WORKING_DIR`, `PLAN_PROMPT_FILE`, `PLAN_SESSION_FILE=.devlyn/plan.worker-session.$PLAN_ROUND.jsonl`, and `PLAN_RECEIPT_FILE=.devlyn/plan.invocation.$PLAN_ROUND.json`; invoke the wrapper as `DEVLYN_CODEX_PROMPT_FILE="$PLAN_PROMPT_FILE" bash "$CODEX_MONITORED_PATH" --json -C "$PLAN_WORKING_DIR" -s workspace-write -m "$PLAN_MODEL" -c sandbox_workspace_write.network_access=false <other options> - >"$PLAN_SESSION_FILE" 2>.devlyn/plan.stderr`, then pass that same session as `state-phase-write.py ... --engine-session-log "$PLAN_SESSION_FILE"` on completion/transition. The wrapper must write and finish the receipt around that direct capture; this state-bound receipt proves requested-model dispatch and prompt/session binding; effective model remains unknown, and a native model-reroute event blocks completion. oh-my-pi delivers the same prompt through its unchanged route. Product code records rendered intent; it never parses a transcript or intercepts, rewrites, wraps, or forces native delivery.
117
-
118
- Subagent output (writes `.devlyn/plan.md`): file list to touch, risk list (out-of-scope expansions, ambiguous spec sections), acceptance restatement (what `## Verification` actually requires verbatim). For large work only, PLAN may add a fourth section `## Execution phases` under the conditions in `references/phases/plan.md`; a phase block missing a runnable `gate:` line disqualifies the whole section — treat the run as single-phase and log the disqualification.
119
-
120
- State write: each PLAN receipt carries spawn-known `round`, `started_at`, `triggered_by`, `engine`, `model_requested`, and `prompt_sha256`; completion adds `completed_at`, `duration_ms`, `verdict`, nullable `model_effective`, and `output_sha256` under `references/state-schema.md`. Later mutations rehash bound output. Never edit PLAN to widen scope after completion; only the bounded PLAN re-spawn may replace its receipt.
116
+ Keep the file list/authorized surface, risks and verbatim verification requirements. Large work may include the existing execution-phase section only when every boundary has a runnable gate. Completing PLAN binds the exact output digest; all later mutations rehash it. Never widen it after implementation starts.
121
117
 
122
118
  After return:
123
119
  1. If `.devlyn/plan.md` lists zero files → halt with verdict `BLOCKED:plan-empty`.
124
- 2. If risk list flags an out-of-scope expansion the user did not authorize → the sole legal corrective re-spawn is round 1: update the data-only task context, repeat render + digest into `.devlyn/plan.prompt.1`, open PLAN with `--round 1 --triggered-by plan` and the actual engine/model/digest, then deliver those exact prompt bytes using the same native or receipt-bound CLI call shape as round 0. A second failed plan halts; no further PLAN dispatch is authorized.
120
+ 2. If the plan exceeds the authorized scope, the sole correction is owner round 1 with `--triggered-by plan`, before implementation starts. Narrow to the original authorization, complete and bind the new output; a second failed plan halts. No worker dispatch or new permission is implied.
125
121
  3. After any re-spawn above, if `state.risk_profile.risk_probes_enabled == true` and `state.risk_profile.risk_probes_explicit == false`, parse the `authorized_surface` array from the JSON block under `<!-- devlyn:authorized-surface -->`. For a well-formed string array, compute `probe_scale_small := len(authorized_surface) <= 2 AND no entry ends in "/**"`. If true, set `risk_probes_enabled = false`, leave `high_risk` unchanged, and append `auto-risk-probes demoted: plan surface small (<n> paths)` to `reasons` using the actual length. A missing or malformed block leaves probe state unchanged; BUILD_GATE owns its malformed-block failure.
126
122
 
127
123
  ## PHASE 1.5: RISK_PROBES
@@ -228,9 +224,9 @@ After return:
228
224
 
229
225
  Skip in verify-only mode. Constrained design judgment within PLAN's invariants. Writes code, tests, and inline doc-comments. No standalone DOCS phase — what the spec licenses is updated here, what it does not is out of scope.
230
226
 
231
- Engine/model/effort: frozen `role_resolution.roles.worker`; obtain validated argv additions using `role-config.py --state .devlyn/pipeline.state.json --role worker --resolved-model <existing-exact-phase-model>`. Prompt body: `references/phases/implement.md`.
227
+ Engine/model/effort: frozen `role_resolution.roles.worker`, including code/doc cleanup within this same invocation before its final checks; obtain validated argv additions using `role-config.py --state .devlyn/pipeline.state.json --role worker --resolved-model <existing-exact-phase-model>`. Prompt body: `references/phases/implement.md`.
232
228
 
233
- For every Codex-routed IMPLEMENT, BUILD_GATE, or CLEANUP spawn, render the exact
229
+ For every Codex-routed IMPLEMENT spawn, render the exact
234
230
  prompt to `.devlyn/<phase>.prompt.<round>`, pass its SHA-256 to `state-phase-write.py
235
231
  spawn --prompt-sha256`, and invoke only through `codex-monitored.sh` with these
236
232
  seven variables set to the active state identity:
@@ -240,13 +236,14 @@ The session and receipt paths are `.devlyn/<phase>.worker-session.<round>.jsonl`
240
236
  and `.devlyn/<phase>.invocation.<round>.json`. These three paths are round-scoped
241
237
  so a retry cannot overwrite earlier prompt/session evidence. Every invocation
242
238
  must include `--json -m <model_requested>` and `-c sandbox_workspace_write.network_access=<true|false>`: exactly
243
- `true` for BUILD_GATE and exactly `false` for PLAN, IMPLEMENT, and CLEANUP, so
239
+ `false` for IMPLEMENT, so
244
240
  user configuration cannot silently change the phase capability. Redirect wrapper stdout directly
245
241
  to that session path. The wrapper rejects bypass/yolo flags and any sandbox other
246
242
  than `workspace-write`. The receipt seals the requested model, sandbox, phase-scoped
247
243
  network capability, prompt, terminal exit, and session digest; completion passes the
248
244
  same canonical session via `--engine-session-log`. A missing/mismatched receipt
249
245
  or same-round retry blocks. Respawn with a new round instead of replacing it.
246
+ Historical metadata-bearing PLAN/CLEANUP and absent-kind BUILD spans retain their original receipt-bound worker routes (network false for PLAN/CLEANUP, true for BUILD). New owner spans never create worker receipts.
250
247
 
251
248
  State write: `phases.implement.{started_at, verdict, completed_at, duration_ms}`.
252
249
 
@@ -276,33 +273,22 @@ Run once iff `state.source.type == "generated"` and complexity is trivial/medium
276
273
 
277
274
  Freeze `.devlyn/surface-close.input.patch`; assemble Claude adapter + `references/phases/surface-close.md` canonical body VERBATIM + data-only Goal/patch/hashes/surface/commands; write the exact bytes to `.devlyn/surface-close.prompt.<round>`, hash them and spawn with `--tools "Read,Grep,Glob,Edit,Write" --dangerously-skip-permissions --output-format json --strict-mcp-config --mcp-config '{"mcpServers":{}}'`, omitting `--model` at SPW spawn to inherit native Claude model settings. Record the effective model from the native JSON result. Use no positional prompt on the CLI route and retain its generated transport carrier. Bound at 600s (native or `run-bounded.py 600 --stdin-file .devlyn/surface-close.prompt.<round> --record-transport -- claude -p`), else block pre-spawn. Save raw stdout as `.devlyn/surface-close.output.json`, extract its `result` string to `.devlyn/surface-close.stdout`, retain `.devlyn/surface-close.worker-session.<round>.jsonl`, and complete with `.devlyn/surface-close.output.json` as `--engine-session-log`; non-JSON/missing result follows the existing failure path. Workers execute nothing; `surface-check` gates rows, citations, scope, and execution. Empty PASS completes; authorized delta is scoped-staged and committed. Only `BLOCKED:surface-close-adjudication-malformed` runs `surface-adjudication-recover --authorized-surface-json <array>`, whose successful rollback and transcript Edit/Write audit complete the nonterminal skip carrier and continue to BUILD_GATE. Timeout, input/prompt mismatch, attestation failure, execution-audit violation, and out-of-surface delta run `surface-rollback`, complete bare `BLOCKED`, and halt; rollback failure also halts. One shot; no `max_rounds`.
278
275
 
279
- **Common post-fix checkpoint (BUILD_GATE and VERIFY):** After fix IMPLEMENT returns, increment `state.rounds.global`; scoped-stage the authorized surface and commit `chore(pipeline): implement fix round <n>`; run `python3 "$DEVLYN_SHARED_DIR/state-phase-write.py" --devlyn-dir .devlyn --phase implement durability-enforce --round <n> --origin-phase <build_gate|verify>`. Its receipt must PASS before re-entry, which enforces it again before VERIFY artifact clearing or phase spawn.
276
+ **Common post-fix checkpoint (BUILD_GATE and VERIFY):** CLEANUP findings use the same checkpoint with origin `cleanup`. After fix IMPLEMENT returns, increment `state.rounds.global`; scoped-stage the authorized surface and commit `chore(pipeline): implement fix round <n>`; run `python3 "$DEVLYN_SHARED_DIR/state-phase-write.py" --devlyn-dir .devlyn --phase implement durability-enforce --round <n> --origin-phase <build_gate|cleanup|verify>`. Its receipt must PASS before re-entry, which enforces it again before VERIFY artifact clearing or phase spawn.
280
277
 
281
278
  ## PHASE 3: BUILD_GATE
282
279
 
283
280
  Skip in verify-only mode OR when `build-gate` in `state.bypasses`. Deterministic — same commands CI / Docker / production run.
284
281
 
285
- Spawn the resolved BUILD_GATE engine through the fresh-worker route in
286
- `<engine_routing>` with prompt body `references/phases/build-gate.md`. A Codex
287
- route must inherit CI-equivalent capabilities from the parent invocation; it
288
- must not add isolation, narrow the sandbox, or widen permissions inside the
289
- worker. Invoke a Codex BUILD_GATE with
290
- `-s workspace-write -c sandbox_workspace_write.network_access=true`; the
291
- receipt rejects a missing, false, duplicate, or non-BUILD_GATE network override
292
- before the worker starts. This retains the write sandbox while allowing the
293
- loopback and network operations exercised by CI-equivalent gates. If an
294
- authoritative route/tool response denies a required filesystem,
295
- subprocess, loopback, PTY, or network operation, preserve that denial through
296
- the BUILD_GATE process-evidence manifest and complete the phase as
297
- `BLOCKED:build-env-underprovisioned`, not as a product finding or substitute
298
- command. The worker:
282
+ The orchestrator reads `references/phases/build-gate.md` and runs the gates directly: no adapter header, rendered worker prompt or separate model. Open its span through the predecessor transition with no BUILD engine/model/prompt/session arguments. New spans record `execution_kind: "orchestrator_commands"`; historical already-open spans with that field absent retain their worker attestation contract.
283
+
284
+ Follow the BUILD body's capability contract on the parent's actual command route and log that basis; no child receipt attests it. Authoritative denial yields report-level `BLOCKED:build-env-underprovisioned` (bare `BLOCKED` in state). Keep spec/probe capture and its conditional post-spec denial carrier refresh; ordinary gates retain findings and raw logs. The orchestrator:
299
285
  1. Detects language/framework via project files (`package.json`, `pyproject.toml`, etc.).
300
286
  2. Runs language-specific gates (tsc / lint / test), deferring exact literal overlaps under `references/phases/build-gate.md`'s current-contract/current-round rule; task-context prompts must preserve that rule.
301
287
  3. Always runs `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --include-risk-probes` (verification_commands literal-match plus `.devlyn/risk-probes.jsonl` when present). If `state.risk_profile.risk_probes_enabled == true`, the script requires `.devlyn/risk-probes.jsonl`; a missing file is a CRITICAL mechanical blocker, not a silent solo run. The script routes each command through `process-evidence.py`; preserve `.devlyn/spec-verify.results.json` and its validated BUILD_GATE carrier for the state-bound archive flow.
302
288
  4. If diff touches web-surface files: run the browser tier with the repo's available toolchain (for example Playwright or curl).
303
289
  5. Emits `.devlyn/build_gate.findings.jsonl` + `.devlyn/build_gate.log.md`.
304
290
 
305
- State write: `phases.build_gate.{started_at, verdict, completed_at, duration_ms, artifacts}`.
291
+ State write: `phases.build_gate.{execution_kind, started_at, verdict, completed_at, duration_ms, artifacts}`; engine/model fields are null, with no worker prompt or receipt. Completion keeps PLAN integrity and process-evidence floors.
306
292
 
307
293
  Branch:
308
294
  - `PASS` → PHASE 4.
@@ -311,21 +297,13 @@ Branch:
311
297
 
312
298
  ## PHASE 4: CLEANUP
313
299
 
314
- Skip if `cleanup` in `state.bypasses`. Task-scoped pass.
300
+ Skip if `cleanup` in `state.bypasses`. The owner reads `references/phases/cleanup.md`; no separate model invocation. Code/doc cleanup already belongs to the selected IMPLEMENT worker, with its original engine/model/effort and receipt.
315
301
 
316
- Engine/model/effort: the same frozen worker profile, with validated role-config argv additions. Prompt body: `references/phases/cleanup.md`. Allowlist enforced post-spawn:
317
- - Tooling artifacts the spec did not list as deliverables (`test-results/`, `playwright-report/`, `.last-run.json`, coverage HTML).
318
- - Dead code added by this diff (not pre-existing dead code).
319
- - Doc references whose target this diff renamed or removed.
302
+ Open through the predecessor transition with `--next-pre-sha "$(git rev-parse HEAD)"`, omitting worker identity. Remove only observed generated, untracked/ignored artifacts owned by this run and not required deliverables; preserve source, user data, evidence and restart records. For PASS, tracked source and HEAD must equal `pre_sha`; the state writer checks them against `--post-sha` and rejects new untracked files relative to `.devlyn/untracked.baseline` (`.devlyn/` exempt). FAIL/BLOCKED closure preserves the observed delta and cannot enter VERIFY.
320
303
 
321
- Before spawn: pass `--pre-sha "$(git rev-parse HEAD)"` to the `state-phase-write.py ... spawn` call for this phase.
304
+ If a new code/doc cleanup issue appears after BUILD, emit a concrete finding and complete CLEANUP as FAIL into the existing selected IMPLEMENT repair route with `--next-triggered-by cleanup`, within the shared round budget. After repair, checkpoint and rerun BUILD_GATE before CLEANUP and fresh VERIFY; never reuse the earlier green checks. Keep the existing authorized surface, allowlist and durability checks. Do not broaden scope for cleanup.
322
305
 
323
- State write: `phases.cleanup.{started_at, verdict, completed_at, duration_ms, pre_sha, post_sha}`.
324
-
325
- After return:
326
- 1. Run `git diff --name-only <pre_sha>` and compare new untracked files against `.devlyn/untracked.baseline` (`.devlyn/` exempt) — any path outside the cleanup allowlist (including a cleanup-created untracked file) → revert to `pre_sha`, delete the unauthorized untracked file, and emit `invariant.cleanup-out-of-scope` into `.devlyn/cleanup.findings.jsonl`.
327
- 2. If allowlist honored and diff non-empty: `git add -u && git commit -m "chore(pipeline): cleanup"`.
328
- 3. Transition CLEANUP→VERIFY through `<transition_protocol>` with `--post-sha "$(git rev-parse HEAD)"` (also when the diff was empty), preserving completion/session arguments and the next-phase identity below.
306
+ On success transition CLEANUP→VERIFY with `--post-sha "$(git rev-parse HEAD)"` and the next judge identity. VERIFY MECHANICAL runs independently on the final source.
329
307
 
330
308
  ## PHASE 5: VERIFY (fresh subagent, findings-only)
331
309
 
@@ -378,7 +356,7 @@ Open the `final_report` span through the predecessor's `state-phase-write.py --d
378
356
 
379
357
  3. **Terminal verdict** — derive from `state.phases.{plan, implement, surface_close, build_gate, cleanup, verify}.verdict` per the precedence rules in `references/state-schema.md#terminal-verdict`. Verify-only mode short-circuits to `state.phases.verify.verdict`.
380
358
 
381
- 4. **Render report to `.devlyn/final-report.md` before completion** — first line exactly `<!-- devlyn:final-report run_id=<current state.run_id> -->`, once only, followed by a nonempty report body. Sections: header (run_id, engine, mode, verdict, wall-time), per-phase summary (including SURFACE_CLOSE run or skip), pair/risk-probe status, findings table (verify + finish-gate findings), follow-up notes (the explicit line `pipeline continued to BUILD_GATE — surface_close_rolled_back_adjudication_malformed` when `continued_after_block` is set, any large-mode `## Assumptions` block, any pair-judge TIMEOUT (headline: solo verdict after pair TIMEOUT), any `--no-pair` / `--no-risk-probes` opt-out, any engine setup guidance after BLOCKED, `/devlyn:ideate` guidance after `BLOCKED:solo-headroom-hypothesis-required` that asks for the visible behavior `solo_claude` is expected to miss, and `/devlyn:ideate` guidance after `BLOCKED:solo-ceiling-avoidance-required` that asks for the concrete difference from rejected or solo-saturated controls such as `S2`-`S6`). User-facing text may follow archive; it cannot substitute for this file.
359
+ 4. **Render report to `.devlyn/final-report.md` before completion** — first line exactly `<!-- devlyn:final-report run_id=<current state.run_id> -->`, once only, followed by a nonempty report body. Sections: header (run_id, engine, mode, verdict, wall-time), per-phase summary (including SURFACE_CLOSE run or skip, owner PLAN reasoning, and BUILD_GATE/CLEANUP as orchestrator commands with no separate model; code cleanup is included in IMPLEMENT cost), pair/risk-probe status, findings table (verify + finish-gate findings), follow-up notes (the explicit line `pipeline continued to BUILD_GATE — surface_close_rolled_back_adjudication_malformed` when `continued_after_block` is set, any large-mode `## Assumptions` block, any pair-judge TIMEOUT (headline: solo verdict after pair TIMEOUT), any `--no-pair` / `--no-risk-probes` opt-out, any engine setup guidance after BLOCKED, `/devlyn:ideate` guidance after `BLOCKED:solo-headroom-hypothesis-required` that asks for the visible behavior `solo_claude` is expected to miss, and `/devlyn:ideate` guidance after `BLOCKED:solo-ceiling-avoidance-required` that asks for the concrete difference from rejected or solo-saturated controls such as `S2`-`S6`). User-facing text may follow archive; it cannot substitute for this file.
382
360
 
383
361
  5. Complete the span with `state-phase-write.py --devlyn-dir .devlyn --phase final_report complete --verdict <bare enum> --log-file .devlyn/final-report.md` — the enum class of the terminal verdict (`BLOCKED:<reason>` → `BLOCKED`; `NEEDS_WORK` / `PASS_WITH_ISSUES` / `PASS` unchanged) — BEFORE archive runs (archive prune skips runs whose `final_report.verdict` is null). The writer validates the canonical nonsymlink regular file, current run marker and nonempty body, then binds its exact bytes; validation failure leaves the phase open. Never hand-edit lifecycle fields in `pipeline.state.json` (`references/state-schema.md` § Write protocol).
384
362
 
@@ -1,15 +1,13 @@
1
1
  # PHASE 3 — BUILD_GATE (canonical body)
2
2
 
3
- Per-engine adapter header is prepended at runtime. BUILD_GATE is mechanical / deterministic — same commands CI / Docker / production run.
3
+ The orchestrator reads this body directly, without an adapter header or worker prompt. BUILD_GATE runs the same commands CI / Docker / production run; gate selection and diagnosis remain orchestrator work.
4
4
 
5
5
  <role>
6
6
  Run language-specific gates and the spec literal-match verification. Emit findings; the orchestrator's fix loop consumes them. Git staging and commits belong to the parent IMPLEMENT checkpoint; verify any staging prerequisite with read-only Git inspection, never repeat `git add` or forward task staging instructions as worker actions.
7
7
  </role>
8
8
 
9
9
  <capability_contract>
10
- A Codex BUILD_GATE runs on the parent-provided CI-equivalent route. Required
11
- filesystem, subprocess, loopback, PTY, and network capabilities are part of the
12
- gate input, not permissions this worker may widen.
10
+ Before executing gates, inspect the orchestrator command route's effective filesystem, subprocess, loopback, PTY and network capabilities and record the actual basis in `.devlyn/build_gate.log.md`. Use an already authorized CI-equivalent route; do not infer capabilities from an engine name or unrelated command success, narrow the route, or widen permissions. No child receipt attests these parent capabilities.
13
11
 
14
12
  If the parent route or an authoritative tool response explicitly denies a
15
13
  required operation before the command can produce product output:
@@ -24,6 +22,8 @@ required operation before the command can produce product output:
24
22
  3. Do not emit a product finding, run a substitute command, narrow the command,
25
23
  retry it on another route, or treat the denial as test/lint output.
26
24
 
25
+ If a denial is appended after `.devlyn/spec-verify.results.json` was sealed, refresh only that existing object's `process_evidence` and `commands` before completion. Load `$DEVLYN_SHARED_DIR/process-evidence.py` with Python's `runpy.run_path`; using the current work root, `state.run_id`, `"build_gate"`, and its round, call `validate_manifest(work, manifest_relative_path(state, "build_gate"), run_id, "build_gate", round_, require_expectations=False)`, then `bound_carrier_summary_commands(work, carrier)` and `validate_summary_commands(work, commands, carrier)`. Replace only those two result fields with the returned carrier/commands. Preserve all other fields, genuine findings, failed expectations and raw streams; do not rewrite the manifest or replay commands. This is conditional on an actual append, not routine browser resealing. If no results file exists, retain the existing BLOCKED completion path.
26
+
27
27
  The state writer derives the phase verdict floor from this sealed manifest. A
28
28
  capability denial can complete only as `BLOCKED`; a failed product expectation
29
29
  cannot complete as `PASS` or `PASS_WITH_ISSUES` even if a mutable results or
@@ -51,7 +51,7 @@ Resolve the concrete commands for steps 1–3; detection examples are not comman
51
51
  1. **Type check** (TypeScript / mypy / etc.). Each error → one finding, severity `HIGH`, rule `correctness.type-check`.
52
52
  2. **Lint**. Run a recognized configured language linter (for example, ESLint with a config, `ruff`, or `clippy`) and/or the exact `package.json` `scripts.lint` command. A declared specialized `lint:*` script (for example, `lint:json`) is not the general language lint gate on the strength of its declaration alone. If the spec's verification commands explicitly name such a specialized command, step 4 executes it and preserves its real failure severity. If no linter is applicable, log an explicit SKIP in `.devlyn/build_gate.log.md` and emit zero lint findings. Each error → finding, severity `MEDIUM`, rule `quality.lint`. Warnings stay LOW unless the spec elevates them.
53
53
  3. **Test suite** (npm test / pytest / go test / cargo test). Each failing test → finding, severity `HIGH`, rule `correctness.test-failure`. Include the failing test's file:line and the assertion.
54
- 4. **Spec literal verification + risk probes**: `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --include-risk-probes`. The script routes every literal command and risk probe through `process-evidence.py`, preserving exact command, outcome, raw stdout/stderr, and digests in the BUILD_GATE run/round manifest. It self-stages from sibling `spec.expected.json` next to `state.source.spec_path`, or the legacy inline carrier when the sibling is absent; benchmark-prestaged `.devlyn/spec-verify.json` still wins. It appends `.devlyn/risk-probes.jsonl` when present, and requires that file when `state.risk_profile.risk_probes_enabled == true`. Malformed `state.risk_profile` is also CRITICAL because it can hide enabled risk probes. When probes are enabled or `.devlyn/risk-probes.jsonl` exists, missing or mismatched top-level `state.risk_probes_digest` emits `correctness.risk-probe-integrity` CRITICAL; the digest binds `.devlyn/risk-probes.jsonl` plus referenced `.devlyn/probes/` script bytes. Command or risk-probe mismatch → CRITICAL finding. Missing required risk probes, missing/malformed generated carrier, or malformed sibling expected file → `correctness.spec-verify-malformed` CRITICAL. The same invocation also enforces PLAN's declared `authorized_surface` (the json block under `.devlyn/plan.md`'s `<!-- devlyn:authorized-surface -->` section, located by that sentinel regardless of the heading's language) against this run's diff and untracked delta: any changed path or created-during-run untracked path outside the declared surface → `scope.out-of-scope-file` CRITICAL (`.devlyn/` exempt) — the only sanctioned fix is removing the file, never widening the declared surface, since that would let the same worker that leaked scope self-authorize the leak; missing sentinel, malformed `plan.md`/surface block, or missing `.devlyn/untracked.baseline` → `scope.authorized-surface-malformed` CRITICAL. This scope check runs only here (BUILD_GATE), not when VERIFY MECHANICAL reuses this script post-CLEANUP, because CLEANUP's own allowlist legitimately touches paths PLAN never declared.
54
+ 4. **Spec literal verification + risk probes**: `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --include-risk-probes`. The script routes every literal command and risk probe through `process-evidence.py`, preserving exact command, outcome, raw stdout/stderr, and digests in the BUILD_GATE run/round manifest. It self-stages from sibling `spec.expected.json` next to `state.source.spec_path`, or the legacy inline carrier when the sibling is absent; benchmark-prestaged `.devlyn/spec-verify.json` still wins. It appends `.devlyn/risk-probes.jsonl` when present, and requires that file when `state.risk_profile.risk_probes_enabled == true`. Malformed `state.risk_profile` is also CRITICAL because it can hide enabled risk probes. When probes are enabled or `.devlyn/risk-probes.jsonl` exists, missing or mismatched top-level `state.risk_probes_digest` emits `correctness.risk-probe-integrity` CRITICAL; the digest binds `.devlyn/risk-probes.jsonl` plus referenced `.devlyn/probes/` script bytes. Command or risk-probe mismatch → CRITICAL finding. Missing required risk probes, missing/malformed generated carrier, or malformed sibling expected file → `correctness.spec-verify-malformed` CRITICAL. The same invocation also enforces PLAN's declared `authorized_surface` (the json block under `.devlyn/plan.md`'s `<!-- devlyn:authorized-surface -->` section, located by that sentinel regardless of the heading's language) against this run's diff and untracked delta: any changed path or created-during-run untracked path outside the declared surface → `scope.out-of-scope-file` CRITICAL (`.devlyn/` exempt) — the only sanctioned fix is removing the file, never widening the declared surface, since that would let the same worker that leaked scope self-authorize the leak; missing sentinel, malformed `plan.md`/surface block, or missing `.devlyn/untracked.baseline` → `scope.authorized-surface-malformed` CRITICAL. This scope check runs only here (BUILD_GATE), not when VERIFY MECHANICAL reuses this script post-CLEANUP, while owner CLEANUP separately enforces unchanged tracked source and the final untracked baseline. Historical worker CLEANUP retains its existing allowlist.
55
55
  5. **Browser** (only when diff touches `*.tsx`, `*.jsx`, `*.vue`, `*.svelte`, `page.*`, `layout.*`, `route.*`, `*.css`, `*.html`): start the dev server and run the repo's existing browser checks, or a minimal curl/HTML check when no browser test harness exists. Each failed check → finding, severity `HIGH`, rule `correctness.browser-flow-failed`.
56
56
 
57
57
  Append all findings; do not stop on the first failure.
@@ -1,19 +1,14 @@
1
1
  # PHASE 4 — CLEANUP (canonical body)
2
2
 
3
- Per-engine adapter header is prepended at runtime. Task-scoped pass — only what this diff introduced or invalidated.
4
-
5
- <role>
6
- Remove tooling artifacts, dead code added by this diff, and doc references invalidated by this diff. The cleanup is bounded by an allowlist enforced post-spawn.
7
- </role>
3
+ The selected IMPLEMENT worker performs code/doc cleanup in its existing invocation before its final checks. The owner then uses the CLEANUP span for generated-artifact removal and invariant checks only; no new model call.
8
4
 
9
5
  <input>
10
- - Cumulative diff since `state.base_ref.sha`.
11
- - Spec at `state.source.spec_path` or `state.source.criteria_path`.
12
- - `state.phases.cleanup.pre_sha` (the orchestrator captured this before spawn — your post-cleanup diff against this SHA must stay within the allowlist).
6
+ - Cumulative diff, original spec and immutable PLAN authorized surface.
7
+ - CLEANUP `pre_sha`: tracked source and HEAD must remain unchanged during owner checks.
13
8
  </input>
14
9
 
15
10
  <allowlist>
16
- You may modify or delete:
11
+ Within IMPLEMENT and PLAN authorization, you may modify or delete:
17
12
 
18
13
  1. **Tooling artifacts** the spec did not list as deliverables: `test-results/`, `playwright-report/`, `.last-run.json`, coverage HTML output, build artifacts, runtime caches (`__pycache__/`, `*.pyc`, `.cache/`).
19
14
  2. **Dead code added by this diff** — symbols (functions, classes, types, exports) introduced by this diff that no other code added by this diff references AND that are not part of the spec's required surface. Pre-existing dead code is out of scope.
@@ -24,8 +19,7 @@ Files outside this allowlist must not change. Pre-existing tooling leaks (alread
24
19
  </allowlist>
25
20
 
26
21
  <output>
27
- - Code changes within the allowlist.
28
- - Report your verdict in this reply: `PASS` if changes within allowlist (or no changes needed); `FAIL` if you cannot complete within the allowlist (the orchestrator will revert). Do not edit `pipeline.state.json` yourself — the orchestrator records it via `state-phase-write.py`.
22
+ The owner removes only run-owned untracked/ignored generated artifacts from item 1, preserving deliverables, evidence and recovery inputs. It records PASS when the source checkpoint is unchanged and no cleanup finding remains. A code/doc finding returns through the selected IMPLEMENT repair route and reruns BUILD before fresh VERIFY. Never modify source inside owner CLEANUP or reuse stale checks. Record lifecycle via `state-phase-write.py`.
29
23
  </output>
30
24
 
31
25
  <quality_bar>
@@ -14,6 +14,7 @@ You execute the plan. Constrained design judgment within PLAN's invariants — w
14
14
 
15
15
  <output>
16
16
  - Code changes implementing every Requirement. Verify with `git diff`.
17
+ - In this same selected worker invocation, remove dead code/comments introduced by this diff and repair references it invalidated, within PLAN authorization and `references/phases/cleanup.md`. Finish before the final checks/checkpoint; no separate cleanup model is dispatched.
17
18
  - Tests added or updated for changed behavior. Run the focused development tests needed to establish that behavior; BUILD_GATE and VERIFY own the post-implementation full suite.
18
19
  - For every sibling `spec.expected.json.process_evidence[]` item whose `phase` is `implement`, run `python3 "$DEVLYN_SHARED_DIR/process-evidence.py" --devlyn-dir .devlyn run --phase implement --id '<id>'`. The runner must report `expectation_met: true`; cite its manifest path in the phase reply.
19
20
  - For each criterion satisfied, set `state.criteria[i].status: "implemented"` with an `evidence` record `{"file": "...", "line": N, "note": "brief"}`.
@@ -1,6 +1,6 @@
1
1
  # PHASE 1 — PLAN (canonical body)
2
2
 
3
- The per-engine adapter header from `_shared/adapters/<engine>.md` is prepended at runtime. This file is engine-agnostic.
3
+ The orchestrator reads this body in its existing context; no PLAN worker is dispatched.
4
4
 
5
5
  <role>
6
6
  You translate a spec or generated criteria into a concrete plan: the file list to touch, the risks the implementation must navigate, and a verbatim restatement of what acceptance requires. The plan is the contract IMPLEMENT executes against.
@@ -20,14 +20,14 @@ Write `.devlyn/plan.md` with three sections:
20
20
  3. **Acceptance restatement** — verbatim copy of the spec's `## Verification` block (or generated criteria's equivalent). The plan is wrong if any verification command later fails because of a planning oversight.
21
21
  4. **Execution phases** — conditional; large work only. Emit this section ONLY when ALL hold: (a) spec frontmatter `complexity: high` (legacy `large`) or `state.complexity == "large"`; (b) the work spans multiple subsystems or more than ~8 files; (c) every phase boundary has at least one runnable gate command. Otherwise emit exactly the three sections above — when in doubt, do not decompose. When emitted: 2-5 `### Phase <k> — <title>` blocks, each with task checkboxes (`- [ ]`), a `gate:` line (1-2 commands, exit-code truth, runnable at that boundary), and the files it owns (a subset of section 1). Definitions are written once here and are the contract; the orchestrator later updates checkboxes as a display mirror only — routing truth lives in `pipeline.state.json`.
22
22
 
23
- Report your verdict in this reply: `PASS` if plan is shippable; `BLOCKED` if spec is internally contradictory or cannot be planned without violating constraints. Do not edit `pipeline.state.json` yourself — the orchestrator records it via `state-phase-write.py`.
23
+ Record via `state-phase-write.py`: `PASS` if plan is shippable; `BLOCKED` if spec is internally contradictory or cannot be planned without violating constraints. Never hand-edit lifecycle fields.
24
24
  </output>
25
25
 
26
26
  <quality_bar>
27
27
  - Scope first, then implementation. Decide what files to touch before deciding how to implement. Files not in the list are off-limits to IMPLEMENT.
28
28
  - Tooling artifacts and reporter output are not deliverables unless the spec lists them. Plan to configure tools to emit to gitignored paths.
29
29
  - Existing tests are contract. Plan to extend them; do not plan to remove or weaken them.
30
- - Spec frontmatter is read-only to PLAN and IMPLEMENT. The DOCS-style status flip happens in CLEANUP under a tight allowlist.
30
+ - Spec frontmatter is read-only; cleanup cannot broaden the task contract.
31
31
  - If a Requirement says "match the literal output X", restate the literal in the plan. Paraphrasing the contract here propagates into IMPLEMENT.
32
32
  </quality_bar>
33
33
 
@@ -92,13 +92,14 @@ Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `surf
92
92
  }
93
93
  ```
94
94
 
95
- `history` is absent until a phase is re-entered. Immediately before re-entry overwrites the live lifecycle, `state-phase-write.py` appends those prior values as one object to `history[]`, creating the append-only array on first re-entry. Codex worker history also retains its state-bound `invocation_receipt`; PLAN history retains the complete prompt/output receipt.
95
+ `history` is absent until a phase is re-entered. Immediately before re-entry overwrites the live lifecycle, `state-phase-write.py` appends those prior values as one object to `history[]`, creating the append-only array on first re-entry. Codex worker history also retains its state-bound `invocation_receipt`; PLAN history retains the complete prompt/output receipt. Owner phase history preserves output/checkpoint digests, its round and present execution kind/worker provenance; historical absence of the kind remains absent.
96
96
 
97
97
  - `verdict` — `"PASS" | "PASS_WITH_ISSUES" | "FAIL" | "NEEDS_WORK" | "BLOCKED"`. PHASE 6 (FINAL_REPORT) writes its own verdict per the terminal-verdict precedence.
98
- - `model_requested` / `model_effective` — `model_requested` is supplied by the orchestrator through `--model`; schema-v3 completion cannot replace the spawn engine or requested model. Schema-v3 Codex PLAN/mutation/BUILD_GATE phases validate requested dispatch using the canonical `.devlyn/<phase>.invocation.<round>.json`; state rehashes the receipt, round-scoped prompt and worker session and cross-checks model, sandbox, prompt, run, phase, round and exit. A structured native model-reroute event or malformed JSONL blocks completion, even at exit0. The receipt proves argv delivery, so `model_effective` remains null; absence of a reroute is not positive model attestation. Arbitrary paths and plaintext `model:` headers are not evidence. Claude JSON output accepts a singleton `modelUsage` key or, for multiple entries, the sole key whose token counters exactly match top-level usage. Missing/malformed evidence and requested/effective mismatch fail closed: the phase verdict becomes `BLOCKED` and completion fails.
98
+ - `execution_kind` — owner PLAN uses `"orchestrator_context"`; owner BUILD_GATE/CLEANUP use `"orchestrator_commands"`, null engine/model fields and no worker prompt, receipt or role identity. Worker claims and current-round worker artifacts are rejected. Already-open absent-kind spans retain historical receipt validation; unknown or null kinds fail closed.
99
+ - `model_requested` / `model_effective` — `model_requested` is supplied by the orchestrator through `--model`; schema-v3 completion cannot replace the spawn engine or requested model. Schema-v3 Codex IMPLEMENT and historical worker PLAN/CLEANUP/BUILD_GATE phases validate requested dispatch using the canonical `.devlyn/<phase>.invocation.<round>.json`; state rehashes the receipt, round-scoped prompt and worker session and cross-checks model, sandbox, prompt, run, phase, round and exit. A structured native model-reroute event or malformed JSONL blocks completion, even at exit0. The receipt proves argv delivery, so `model_effective` remains null; absence of a reroute is not positive model attestation. Arbitrary paths and plaintext `model:` headers are not evidence. Claude JSON output accepts a singleton `modelUsage` key or, for multiple entries, the sole key whose token counters exactly match top-level usage. Missing/malformed evidence and requested/effective mismatch fail closed: the phase verdict becomes `BLOCKED` and completion fails.
99
100
  - `output_sha256` — on schema-v3 PLAN completion, SHA-256 of exact `.devlyn/plan.md` bytes. Only caller-verdict BLOCKED with a lexically absent, never-bound plan records null; receipt/session attestation still applies. This terminal null permits only FINAL_REPORT lifecycle operations while the path remains lexically absent. Bound bytes are rehashed on later spawn/completion/transition, including BLOCKED; legal PLAN re-entry replaces the digest only at the new PLAN completion. FINAL_REPORT completion binds exact `.devlyn/final-report.md` bytes here and its canonical path in `artifacts.log_file`.
100
- - `invocation_receipt` — state-bound `{path, sha256, sandbox, sandbox_network_access, argv_sha256, exit_code}` for schema-v3 Codex PLAN/IMPLEMENT/BUILD_GATE/CLEANUP. Receipt schema 2.0 makes `sandbox` exactly `workspace-write`; `sandbox_network_access` is exactly `true` for BUILD_GATE and `false` for the other mutation phases. A missing CI capability or wider phase authority blocks before launch. Pre-2.0 receipts are intentionally rejected rather than migrated because they did not attest this capability; do not rewrite in-flight evidence. Continue through the owning session or start in a distinct worktree; restarting via explicit manual archive requires the operator to establish that prior writers stopped, never automatic archival to defeat admission refusal. A completed final report does not prove all interactive writers exited. The receipt is retained in history and rehashed before archive.
101
- - `triggered_by` — null on first run; one of `"build_gate" | "verify"` when the phase is a fix-loop respawn.
101
+ - `invocation_receipt` — state-bound `{path, sha256, sandbox, sandbox_network_access, argv_sha256, exit_code}` for schema-v3 Codex IMPLEMENT and historical worker PLAN/CLEANUP/BUILD_GATE. Receipt schema 2.0 makes `sandbox` exactly `workspace-write`; `sandbox_network_access` is exactly `true` for BUILD_GATE and `false` for the other mutation phases. A missing CI capability or wider phase authority blocks before launch. Pre-2.0 receipts are intentionally rejected rather than migrated because they did not attest this capability; do not rewrite in-flight evidence. Continue through the owning session or start in a distinct worktree; restarting via explicit manual archive requires the operator to establish that prior writers stopped, never automatic archival to defeat admission refusal. A completed final report does not prove all interactive writers exited. The receipt is retained in history and rehashed before archive.
102
+ - `triggered_by` — null on first run; one of `"build_gate" | "cleanup" | "verify"` when the phase is a fix-loop respawn.
102
103
  - `pre_sha` — captured before SURFACE_CLOSE or CLEANUP. It bounds that phase's post-spawn diff and rollback.
103
104
  - `post_sha` — captured when SURFACE_CLOSE or CLEANUP completes; finish-gate subtracts CLEANUP's `pre_sha..post_sha` window.
104
105
  - `sub_verdicts` — only populated for VERIFY: `{ "mechanical": "PASS|FAIL", "judge": "PASS|...", "pair_judge": "PASS|..." | "TIMEOUT" | null }`. Values are normalized by `verify-merge-findings.py`; model prose verdicts cannot upgrade or downgrade the deterministic findings-derived verdict. `sub_verdicts.pair_judge` is `"TIMEOUT"` when a pair judge exceeded its wall budget and a valid `.devlyn/verify.pair.timeout.json` marker was read — semantics in `references/phases/verify.md` (pair budget section).
@@ -111,9 +112,9 @@ Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `surf
111
112
 
112
113
  Phase lifecycle (`started_at`/`completed_at`/`duration_ms`/`round`/`triggered_by`/`verdict`) is written by a deterministic script, never hand-edited JSON — a prior hand-edited fix-loop respawn left `started_at` stale, corrupting cross-phase ordering because `completed_at`/`round`/`triggered_by` advanced to the new round while `started_at` didn't. Phase workers report their verdict and artifact paths in their reply; they never edit `pipeline.state.json` themselves.
113
114
 
114
- 1. **Spawn**: `state-phase-write.py --devlyn-dir .devlyn --phase <name> spawn --round <N> [--triggered-by build_gate|verify] [--pre-sha <sha>] [--engine <e>] [--model <requested-model>] [--prompt-sha256 <hex>]`. Re-entry preserves lifecycle/receipts in `history[]`, resets owned fields, and leaves fields such as `phases.implement.exec`; VERIFY also clears prior-round files. Schema-v3 Codex IMPLEMENT/BUILD_GATE/CLEANUP requires the requested model and prompt digest; an inherited top-level Codex engine is persisted into the phase rather than becoming an unattested null route. SURFACE_CLOSE inherits native Claude model settings when `--model` is omitted and rejects explicit blank models; it additionally requires `--input-patch-sha256 <hex> --prompt-sha256 <hex> --untracked-before-json <array>`; use `surface-check --authorized-surface-json <array>`, `surface-adjudication-recover --authorized-surface-json <array>` only after its adjudication-malformed result, `surface-rollback` for other failures, or `surface-skip` only for automatic Claude unavailability.
115
+ 1. **Spawn**: `state-phase-write.py --devlyn-dir .devlyn --phase <name> spawn --round <N> [--triggered-by build_gate|cleanup|verify] [--pre-sha <sha>] [--engine <e>] [--model <requested-model>] [--prompt-sha256 <hex>]`. Re-entry preserves lifecycle/receipts in `history[]`, resets owned fields, and leaves fields such as `phases.implement.exec`; VERIFY also clears prior-round files. Owner PLAN/BUILD_GATE/CLEANUP spans omit engine/model/prompt arguments. Metadata-bearing PLAN/CLEANUP calls retain the legacy worker API; owner spans cannot be relabeled as workers. Successful owner CLEANUP requires pre/post SHA equality, unchanged HEAD/tracked source and no new untracked files versus the baseline; failure closure preserves the observed delta; code/doc cleanup belongs to the selected IMPLEMENT invocation. Schema-v3 Codex IMPLEMENT and historical worker CLEANUP requires the requested model and prompt digest; an inherited top-level Codex engine is persisted into the phase rather than becoming an unattested null route. SURFACE_CLOSE inherits native Claude model settings when `--model` is omitted and rejects explicit blank models; it additionally requires `--input-patch-sha256 <hex> --prompt-sha256 <hex> --untracked-before-json <array>`; use `surface-check --authorized-surface-json <array>`, `surface-adjudication-recover --authorized-surface-json <array>` only after its adjudication-malformed result, `surface-rollback` for other failures, or `surface-skip` only for automatic Claude unavailability.
115
116
  2. **Transition** (normal direct handoff): `state-phase-write.py ... --phase <current> transition` takes the same completion/attestation arguments plus caller-specified `--next-phase`, `--next-round`, `--next-triggered-by`, `--next-engine`, `--next-model`, and next-phase metadata. It validates a fixed legal edge, applies complete plus spawn to a detached candidate, and replaces state once; either both lifecycle writes land or neither. A passing IMPLEMENT edge first validates and binds all declared process evidence through `_shared/process-evidence.py`. The verb returns JSON paths/digests/state facts only and never chooses a phase, engine, branch, prompt, or agent.
116
- 3. **Complete** (after the orchestrator reads the phase's reply and determines the verdict, when no next phase opens): `python3 "$DEVLYN_SHARED_DIR/state-phase-write.py" --devlyn-dir .devlyn --phase <name> complete --verdict <V> [--post-sha <sha>] [--findings-file <path>] [--log-file <path>] [--engine-session-log <path>]`. Pass only the phase/round canonical worker session. SURFACE_CLOSE requires `.devlyn/surface-close.output.json` even for inherited model selection (`model_requested=null`); native JSON supplies `model_effective`, and missing/malformed evidence or an explicit model mismatch blocks completion; schema-v3 Codex PLAN/IMPLEMENT/BUILD_GATE/CLEANUP passes the wrapper-captured `.devlyn/<phase>.worker-session.<round>.jsonl` and requires its sibling invocation receipt plus `.devlyn/<phase>.prompt.<round>`. Never scan an engine-global session directory or synthesize a model header. `duration_ms` is derived from the phase's own `started_at`. `--verdict` is required except VERIFY, whose verdict remains owned by `verify-merge-findings.py --write-state`.
117
+ 3. **Complete** (after the orchestrator reads the phase's reply and determines the verdict, when no next phase opens): `python3 "$DEVLYN_SHARED_DIR/state-phase-write.py" --devlyn-dir .devlyn --phase <name> complete --verdict <V> [--post-sha <sha>] [--findings-file <path>] [--log-file <path>] [--engine-session-log <path>]`. Pass only the phase/round canonical worker session. SURFACE_CLOSE requires `.devlyn/surface-close.output.json` even for inherited model selection (`model_requested=null`); native JSON supplies `model_effective`, and missing/malformed evidence or an explicit model mismatch blocks completion; schema-v3 Codex IMPLEMENT and historical worker PLAN/BUILD_GATE/CLEANUP pass the wrapper-captured `.devlyn/<phase>.worker-session.<round>.jsonl` and requires its sibling invocation receipt plus `.devlyn/<phase>.prompt.<round>`. Never scan an engine-global session directory or synthesize a model header. `duration_ms` is derived from the phase's own `started_at`. `--verdict` is required except VERIFY, whose verdict remains owned by `verify-merge-findings.py --write-state`.
117
118
  4. **Before archive** (PHASE 6 step 5): write `.devlyn/final-report.md` with exactly one first-line `<!-- devlyn:final-report run_id=<current state.run_id> -->` and a non-whitespace body, then complete FINAL_REPORT with `--log-file .devlyn/final-report.md`. Missing, unsafe, empty or mismatched report input fails before completion; the marker proves run identity, not semantic completeness. This also applies to BLOCKED missing-PLAN-output closure. Archive rehashes a present FINAL_REPORT `output_sha256` binding before any move; missing/altered files or malformed/null bindings fail without moving artifacts. Historical states with that field absent retain existing archive behavior. Archive prune skips runs whose final_report verdict is null (treated as in-flight).
118
119
 
119
120
  ## Terminal verdict (PHASE 6)
@@ -130,7 +131,7 @@ Precedence:
130
131
 
131
132
  Header: `run_id | engine | mode | complexity | verdict | wall_time_s`.
132
133
 
133
- Per-phase summary table: `phase | verdict | duration_ms | round | triggered_by | findings_count`; include SURFACE_CLOSE when run or skipped.
134
+ Per-phase summary table: `phase | verdict | duration_ms | round | triggered_by | findings_count`; include SURFACE_CLOSE when run or skipped and identify new BUILD_GATE as orchestrator commands with no separate model.
134
135
 
135
136
  Findings table: each emitted finding's `severity | rule_id | file:line | message | confidence`.
136
137
 
@@ -144,6 +145,6 @@ PHASE 6 step 4 moves `PER_RUN_PATTERNS` plus every state-bound process manifest/
144
145
 
145
146
  PHASE0 `state-phase-write.py --freeze-roles --default-engine <orchestrator-default>` freezes `role_resolution` (legacy engine/source, three role entries, input paths/digests, snapshot digest). `role_config_input` holds only the validated per-run roles object and provenance; `role_no_pair` records that flag. No config reread after freezing. Missing legacy snapshots are compatible; present null/malformed snapshots fail. The helper does not launch models.
146
147
 
147
- `state.engine` remains the legacy executor for unchanged PLAN/BUILD/probe routing. IMPLEMENT/CLEANUP spawn enforce frozen worker engine/model. VERIFY spawn explicitly records the primary in `phases.verify.engine`; merge, timeout and OTHER exclusion use it, falling back only when absent. Requested settings are distinct from native-observed fields. Explicit Codex worker effort binds `role_argv` against the existing invocation argv digest; effort remains dispatch evidence, not provider-internal attestation.
148
+ `state.engine` remains the legacy executor; PLAN stays orchestrator-fixed, BUILD runs orchestrator commands, and probe routing is unchanged. IMPLEMENT (including code/doc cleanup) and historical worker CLEANUP enforce frozen worker engine/model. Owner CLEANUP does not claim that identity. VERIFY spawn explicitly records the primary in `phases.verify.engine`; merge, timeout and OTHER exclusion use it, falling back only when absent. Requested settings are distinct from native-observed fields. Explicit Codex worker effort binds `role_argv` against the existing invocation argv digest; effort remains dispatch evidence, not provider-internal attestation.
148
149
 
149
150
  Successful explicit judges retain `<engine>-judge.r<round>.*` raw/prompt/argv/derived/evidence files; `verify.role_evidence` binds their hashes, observed model/nullable effort and named evidence basis. VERIFY history preserves those bindings. Merge alone binds successful evidence; archive rehashes every bound artifact. Canonical `<engine>-judge.stdout` is the current collector input and is never substituted for the immutable round raw. Failed raw remains diagnostic and cannot become a successful binding.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "3.1.3",
3
+ "version": "3.2.0",
4
4
  "description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — take intent to shipped, engineer-quality software hands-free with phase-gated pipelines and agent orchestration",
5
5
  "homepage": "https://github.com/fysoul17/devlyn-cli#readme",
6
6
  "bin": {
@@ -76,6 +76,7 @@
76
76
  "scripts/lint-fixtures.sh",
77
77
  "scripts/lint-shadow-fixtures.sh",
78
78
  "scripts/lint-skills.sh",
79
+ "scripts/test-owner-phases.py",
79
80
  "CLAUDE.md",
80
81
  "AGENTS.md"
81
82
  ],
@@ -569,6 +569,13 @@ if ! grep -Fq 'SAFE_RUN_ID_RE' config/skills/_shared/archive_run.py \
569
569
  bad "archive_run.py must safely archive pair/risk-probe evidence and reject unsafe run ids"
570
570
  fi
571
571
 
572
+ section "Check 6c1: Owner phase lifecycle and scope boundaries"
573
+ if python3 scripts/test-owner-phases.py; then
574
+ ok "owner phase CLI and Git regression tests passed"
575
+ else
576
+ bad "owner phase CLI and Git regression tests failed"
577
+ fi
578
+
572
579
  section "Check 6c2: SURFACE_CLOSE v6 dispatch is complete and mirrored"
573
580
  surface_close_missing=0
574
581
  if python3 config/skills/_shared/state-phase-write.py --self-test >/dev/null 2>&1; then
@@ -632,7 +639,7 @@ for tree in config/skills .agents/skills; do
632
639
  || ! grep -Fq -- '--tools "Read,Grep,Glob,Edit,Write" --dangerously-skip-permissions --output-format json --strict-mcp-config --mcp-config '\''{"mcpServers":{}}'\''' "$skill" \
633
640
  || ! grep -Fq '.devlyn/surface-close.output.json' "$skill" \
634
641
  || ! grep -Fq '**Common post-fix checkpoint (BUILD_GATE and VERIFY):**' "$skill" \
635
- || ! grep -Fq 'durability-enforce --round <n> --origin-phase <build_gate|verify>' "$skill" \
642
+ || ! grep -Fq 'durability-enforce --round <n> --origin-phase <build_gate|cleanup|verify>' "$skill" \
636
643
  || ! grep -Fq '`surface-rollback`' "$skill" \
637
644
  || grep -Fq 'Supplied digests' "$phase" \
638
645
  || grep -Fq 'Hash both artifacts first' "$phase" \
@@ -1346,7 +1353,10 @@ for file in \
1346
1353
  config/skills/devlyn:resolve/SKILL.md \
1347
1354
  .agents/skills/devlyn:resolve/SKILL.md
1348
1355
  do
1349
- if ! grep -Fq 'resolved BUILD_GATE engine through the fresh-worker route' "$file" \
1356
+ if ! grep -Fq 'orchestrator commands with no separate model' "$file" \
1357
+ || ! grep -Fq 'execution_kind: "orchestrator_commands"' "$file" \
1358
+ || grep -Fq 'resolved BUILD_GATE engine through the fresh-worker route' "$file" \
1359
+ || grep -Fq 'Codex-routed IMPLEMENT, BUILD_GATE, or CLEANUP spawn' "$file" \
1350
1360
  || ! grep -Fq 'BLOCKED:build-env-underprovisioned' "$file" \
1351
1361
  || ! grep -Fq '.devlyn/<primary-engine>-judge.stdout' "$file" \
1352
1362
  || ! grep -Fq '.devlyn/<other-engine>-judge.stdout' "$file" \