devlyn-cli 3.1.0 → 3.1.2

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.
Files changed (32) hide show
  1. package/README.md +6 -1
  2. package/bin/devlyn.js +69 -23
  3. package/config/skills/_shared/adapters/claude.md +3 -2
  4. package/config/skills/_shared/adapters/codex.md +1 -1
  5. package/config/skills/_shared/archive_run.py +5 -3
  6. package/config/skills/_shared/codex-config.md +13 -8
  7. package/config/skills/_shared/codex-monitored.sh +12 -19
  8. package/config/skills/_shared/collect-codex-findings.py +1 -0
  9. package/config/skills/_shared/engine-preflight.md +2 -0
  10. package/config/skills/_shared/finish-gate.py +4 -1
  11. package/config/skills/_shared/invocation-receipt.py +194 -5
  12. package/config/skills/_shared/judge-role-evidence.py +33 -6
  13. package/config/skills/_shared/phase-prompt-render.py +2 -0
  14. package/config/skills/_shared/platform-support.py +370 -0
  15. package/config/skills/_shared/process-evidence.py +20 -3
  16. package/config/skills/_shared/resolve-bootstrap.py +131 -63
  17. package/config/skills/_shared/resolve-stop-hook.py +3 -1
  18. package/config/skills/_shared/role-config.py +7 -5
  19. package/config/skills/_shared/run-bounded.py +35 -21
  20. package/config/skills/_shared/spec-verify-check.py +393 -167
  21. package/config/skills/_shared/state-phase-write.py +26 -9
  22. package/config/skills/_shared/task-complete.py +337 -97
  23. package/config/skills/_shared/verify-merge-findings.py +9 -8
  24. package/config/skills/devlyn:ideate/references/spec-template.md +1 -1
  25. package/config/skills/devlyn:resolve/SKILL.md +10 -7
  26. package/config/skills/devlyn:resolve/references/free-form-mode.md +1 -1
  27. package/config/skills/devlyn:resolve/references/phases/build-gate.md +1 -1
  28. package/config/skills/devlyn:resolve/references/phases/verify.md +14 -10
  29. package/config/skills/devlyn:resolve/references/state-schema.md +1 -1
  30. package/config/skills/devlyn:resolve/references/task-completion.md +28 -1
  31. package/package.json +1 -1
  32. package/scripts/lint-skills.sh +6 -5
@@ -474,7 +474,7 @@ def read_findings(devlyn: pathlib.Path) -> tuple[list[dict[str, Any]], dict[str,
474
474
  )
475
475
  if (devlyn / "pipeline.state.json").is_file():
476
476
  try:
477
- state = loads_strict_json((devlyn / "pipeline.state.json").read_text())
477
+ state = loads_strict_json((devlyn / "pipeline.state.json").read_text(encoding="utf-8"))
478
478
  required = JUDGE_ROLE_EVIDENCE["required_roles"](state)
479
479
  if rank(source_verdicts.get("mechanical")) >= 2:
480
480
  required = []
@@ -1663,21 +1663,21 @@ def self_test() -> int:
1663
1663
  for malformed in (None, {}, {"roles": {}}):
1664
1664
  bad_state = loads_strict_json(original_state)
1665
1665
  bad_state["role_resolution"] = malformed
1666
- (devlyn / "pipeline.state.json").write_text(json.dumps(bad_state))
1666
+ (devlyn / "pipeline.state.json").write_text(json.dumps(bad_state), encoding="utf-8")
1667
1667
  result = subprocess.run([sys.executable, str(pathlib.Path(__file__).resolve()),
1668
- "--devlyn-dir", str(devlyn), "--write-state"], capture_output=True, text=True)
1668
+ "--devlyn-dir", str(devlyn), "--write-state"], capture_output=True, text=True, encoding="utf-8")
1669
1669
  assert result.returncode == 0 and "Traceback" not in result.stderr, result.stderr
1670
- persisted = loads_strict_json((devlyn / "pipeline.state.json").read_text())
1670
+ persisted = loads_strict_json((devlyn / "pipeline.state.json").read_text(encoding="utf-8"))
1671
1671
  assert persisted["phases"]["verify"]["verdict"] == "BLOCKED"
1672
- assert "verify-role-resolution-invalid" in (devlyn / "verify-merged.findings.jsonl").read_text()
1672
+ assert "verify-role-resolution-invalid" in (devlyn / "verify-merged.findings.jsonl").read_text(encoding="utf-8")
1673
1673
  (devlyn / "pipeline.state.json").write_bytes(original_state)
1674
1674
  different_primary = {"engine": "codex", "phases": {"verify": {"engine": "claude"}}}
1675
- (devlyn / "pipeline.state.json").write_text(json.dumps(different_primary))
1676
- (devlyn / "verify.primary.timeout.json").write_text(json.dumps({"engine": "claude", "budget_seconds": 600}))
1675
+ (devlyn / "pipeline.state.json").write_text(json.dumps(different_primary), encoding="utf-8")
1676
+ (devlyn / "verify.primary.timeout.json").write_text(json.dumps({"engine": "claude", "budget_seconds": 600}), encoding="utf-8")
1677
1677
  marker, violation = read_primary_timeout_marker(devlyn)
1678
1678
  assert marker is not None and violation is None
1679
1679
  different_primary["phases"]["verify"]["engine"] = None
1680
- (devlyn / "pipeline.state.json").write_text(json.dumps(different_primary))
1680
+ (devlyn / "pipeline.state.json").write_text(json.dumps(different_primary), encoding="utf-8")
1681
1681
  assert read_primary_timeout_marker(devlyn)[1] is not None
1682
1682
  (devlyn / "verify.primary.timeout.json").unlink()
1683
1683
  (devlyn / "pipeline.state.json").write_bytes(original_state)
@@ -3433,4 +3433,5 @@ def main() -> int:
3433
3433
 
3434
3434
 
3435
3435
  if __name__ == "__main__":
3436
+ runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
3436
3437
  raise SystemExit(main())
@@ -96,7 +96,7 @@ python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check-expected <expected-pat
96
96
  ```
97
97
 
98
98
  Map mechanically decidable rules from both Requirements and Constraints into the existing carriers. Validate each new guard with a violating control that fails and an allowed control that passes, preserving permitted pre-existing occurrences:
99
- - `verification_commands` ← `## Verification` + commands the conversation surfaced. For semantic annotation or exception-handling rules, use a suitable syntax-aware/executable check, preferring existing checks; explicitly retain uncovered semantics as source-review obligations.
99
+ - `verification_commands` ← `## Verification` + commands the conversation surfaced. While drafting both Verification and its carrier, omit a standalone check only when another command in that contract runs the same assertions under the required execution conditions and propagates failure; describe that shared coverage in Verification and keep both carriers aligned. Preserve distinct scenarios and required independent verification; execute approved commands unchanged without reusing results across phases. For semantic annotation or exception-handling rules, use a suitable syntax-aware/executable check, preferring existing checks; explicitly retain uncovered semantics as source-review obligations.
100
100
  - `forbidden_patterns` ← literal syntax constraints only; regex does not prove semantics. It scans the whole file-filtered diff, including context, deletions and headers; additions-only guards must exclude those lines.
101
101
  - `required_files` / `forbidden_files` ← file requirements and exclusions in Requirements, Constraints and Out of Scope.
102
102
  - `max_deps_added` ← dependency limits in Requirements or Constraints (default 0 unless the spec licenses a new dep).
@@ -52,7 +52,7 @@ Each phase routes to an engine and prepends the per-engine adapter header from `
52
52
 
53
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
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. The wrapper closes stdin and emits a heartbeat. 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.
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
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
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.
58
58
  - The `--engine` flag does not disable default pairing: the second judge uses the OTHER engine by default when available.
@@ -105,7 +105,7 @@ Use the frozen role entry and `role-config.py --state .devlyn/pipeline.state.jso
105
105
 
106
106
  For a Codex worker with explicit effort, retain the exact wrapper arguments (excluding `bash` and the wrapper path) as `.devlyn/<phase>.argv.<round>.json` before launch. The wrapper's existing invocation receipt binds that array's canonical JSON hash; completion checks the requested effort against it. This adds no mutation receipt relaxation or new launcher.
107
107
 
108
- For a judge with explicit model/effort, let `JUDGE_STEM=.devlyn/<engine>-judge.r<VERIFY-round>`. Retain the exact inlined prompt as `$JUDGE_STEM.prompt` and complete dispatched argv array as `$JUDGE_STEM.argv.json`. Codex uses the isolated read-only monitored route with timeout600 and redirects stdout/stderr to `$JUDGE_STEM.stdout`/`$JUDGE_STEM.stderr`. Claude uses the existing 600s `run-bounded.py` CLI route, `--tools Read,Grep,Glob --allowedTools Read,Grep,Glob --permission-mode dontAsk --setting-sources project --strict-mcp-config --mcp-config '{"mcpServers":{}}' --output-format json`; redirect raw JSON to `$JUDGE_STEM.output.json` and stderr to `$JUDGE_STEM.stderr`. Never substitute a Claude CLI worker for native Agent.
108
+ For a judge with explicit model/effort, let `JUDGE_STEM=.devlyn/<engine>-judge.r<VERIFY-round>`. Retain the exact UTF-8 prompt as `$JUDGE_STEM.prompt` and complete dispatched argv array (including launcher) as `$JUDGE_STEM.argv.json`. Codex sets `DEVLYN_CODEX_PROMPT_FILE="$JUDGE_STEM.prompt"` and passes the sole prompt `-`; Claude uses `run-bounded.py 600 --stdin-file "$JUDGE_STEM.prompt" --record-transport -- claude -p` with no positional prompt. Preserve the generated `$JUDGE_STEM.prompt.transport.json`; it binds the snapshotted bytes and actual native argv, including wrapper-added isolation. Missing/mismatched transport evidence BLOCKs authentication; never fabricate the carrier. Codex uses the isolated read-only monitored route with timeout600 and redirects stdout/stderr to `$JUDGE_STEM.stdout`/`$JUDGE_STEM.stderr`. Claude uses the existing 600s `run-bounded.py` CLI route, `--tools Read,Grep,Glob --allowedTools Read,Grep,Glob --permission-mode dontAsk --setting-sources project --strict-mcp-config --mcp-config '{"mcpServers":{}}' --output-format json`; redirect raw JSON to `$JUDGE_STEM.output.json` and stderr to `$JUDGE_STEM.stderr`. Never substitute a Claude CLI worker for native Agent.
109
109
 
110
110
  After the actual process exits, run `python3 "$DEVLYN_SHARED_DIR/judge-role-evidence.py" --devlyn-dir .devlyn --role <primary_judge|pair_judge> --exit-code <actual-exit>`. On failure preserve raw artifacts and BLOCK the source; never create successful evidence manually. On success it retains round-scoped evidence and produces canonical `.devlyn/<engine>-judge.stdout` (exact Claude result extraction, exact Codex stdout copy) for the unchanged findings collector. Merge revalidates/binds evidence and archives all bound originals, including prior rounds. Missing explicit judge evidence cannot become PASS. Unconfigured judge routes retain their existing capture format.
111
111
 
@@ -113,7 +113,7 @@ After the actual process exits, run `python3 "$DEVLYN_SHARED_DIR/judge-role-evid
113
113
 
114
114
  Skip in verify-only mode. The heaviest phase by design — spec/criteria define non-negotiable invariants; plan formalizes how the implementation hits them.
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 `bash "$CODEX_MONITORED_PATH" --json -C "$PLAN_WORKING_DIR" -s workspace-write -m "$PLAN_MODEL" -c sandbox_workspace_write.network_access=false <other options> "<exact PLAN_PROMPT_FILE bytes>" >"$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.
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
117
 
118
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
119
 
@@ -193,7 +193,7 @@ Invocation contract when OTHER engine is Codex:
193
193
 
194
194
  - Invoke Codex only through the monitored wrapper path in `CODEX_MONITORED_PATH`
195
195
  resolved from `DEVLYN_SHARED_DIR`:
196
- `CODEX_MONITORED_ISOLATED=1 bash "$CODEX_MONITORED_PATH" -C "$PWD" -s workspace-write -c sandbox_workspace_write.network_access=false -c model_reasoning_effort=high "<probe prompt>"`.
196
+ `DEVLYN_CODEX_PROMPT_FILE="<probe-prompt-file>" CODEX_MONITORED_ISOLATED=1 bash "$CODEX_MONITORED_PATH" -C "$PWD" -s workspace-write -c sandbox_workspace_write.network_access=false -c model_reasoning_effort=high -`.
197
197
  Append `-c sandbox_workspace_write.network_access=true` only when a probe's visible Verification command requires a localhost service (for example, a DB test harness); never as a default.
198
198
  Isolation keeps user config, AGENTS.md, hooks, and project rules
199
199
  from adding hidden context, tool calls, or transcript side effects.
@@ -235,10 +235,11 @@ prompt to `.devlyn/<phase>.prompt.<round>`, pass its SHA-256 to `state-phase-wri
235
235
  spawn --prompt-sha256`, and invoke only through `codex-monitored.sh` with these
236
236
  seven variables set to the active state identity:
237
237
  `DEVLYN_INVOCATION_{RUN_ID,PHASE,ROUND,WORKDIR,PROMPT_FILE,SESSION_FILE,RECEIPT}`.
238
+ Set `DEVLYN_CODEX_PROMPT_FILE` to that same prompt file and pass sole prompt `-`; retain its generated `.transport.json` carrier.
238
239
  The session and receipt paths are `.devlyn/<phase>.worker-session.<round>.jsonl`
239
240
  and `.devlyn/<phase>.invocation.<round>.json`. These three paths are round-scoped
240
241
  so a retry cannot overwrite earlier prompt/session evidence. Every invocation
241
- must include `--json` and `-c sandbox_workspace_write.network_access=<true|false>`: exactly
242
+ must include `--json -m <model_requested>` and `-c sandbox_workspace_write.network_access=<true|false>`: exactly
242
243
  `true` for BUILD_GATE and exactly `false` for PLAN, IMPLEMENT, and CLEANUP, so
243
244
  user configuration cannot silently change the phase capability. Redirect wrapper stdout directly
244
245
  to that session path. The wrapper rejects bypass/yolo flags and any sandbox other
@@ -273,7 +274,7 @@ After the final phase's gate PASS: `git diff <base_ref.sha>...HEAD --stat` — e
273
274
 
274
275
  Run once iff `state.source.type == "generated"` and complexity is trivial/medium. Engine is Claude always, pair-judge-routed; executor flag/pin ignored. Per `_shared/engine-preflight.md`, unavailability skips via `surface-skip`, reports `auto_surface_close_claude_unavailable`, never BLOCKs/reroutes.
275
276
 
276
- Freeze `.devlyn/surface-close.input.patch`; assemble Claude adapter + `references/phases/surface-close.md` canonical body VERBATIM + data-only Goal/patch/hashes/surface/commands; hash the bytes 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. Bound at 600s (native or `run-bounded.py 600 -- 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`.
277
+ 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`.
277
278
 
278
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.
279
280
 
@@ -358,7 +359,9 @@ check, and prints setup guidance: install/configure the missing CLI, complete
358
359
  its auth/login flow, verify `<engine> --version`, then rerun. `--no-pair` is only
359
360
  for an intentional solo run.
360
361
 
361
- Findings are written to `.devlyn/verify.findings.jsonl`. **VERIFY agents may inspect authorized source, diff and sealed evidence using native read/search tools or non-mutating shell commands. They must not mutate files or execute verification, lint, test, build, probe or invented-scenario commands.** Capture the primary reply as `.devlyn/<primary-engine>-judge.stdout` and its stderr sibling; never use the generic `.devlyn/verify-judge.stdout`. A Codex primary-JUDGE uses the distinct route `CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600 bash "$CODEX_MONITORED_PATH" -C "$PWD" -s read-only -c model_reasoning_effort=high "<primary prompt>" >.devlyn/codex-judge.stdout 2>.devlyn/codex-judge.stderr`: omit `-m` only without an explicit model; an explicit profile supplies the validated model/effort additions below. Omit bypass flags and capture both streams directly without pipes. On primary exit 124, write `.devlyn/verify.primary.timeout.json` with exactly `{"engine": "<resolved-primary-engine>", "budget_seconds": 600}` before merge. A malformed marker BLOCKs; a valid marker preserves canonical primary findings and floors `judge` at `BLOCKED` even when output is empty, never `PASS`, a solo verdict, or pair-style `TIMEOUT`. Without the marker, existing required-primary failure behavior is unchanged. For Codex pair-JUDGE, keep the read-only `codex-monitored.sh` route with `CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600` and `-c model_reasoning_effort=medium`, no `tail`/`head`/`grep` pipes, and direct stdout/stderr capture; every other resolved OTHER engine follows `_shared/adapters/<name>.md` `## Invocation`. Capture the pair reply as `.devlyn/<other-engine>-judge.stdout`, then run `python3 "$DEVLYN_SHARED_DIR/collect-codex-findings.py" --devlyn-dir "<abs repo>/.devlyn" --stdout-file <other-engine>-judge.stdout` before merge. Raw stdout is diagnostic-only. A non-zero collector exit must write no canonical `.devlyn/verify.pair.findings.jsonl`; set the pair source to `BLOCKED` for `verify.pair.emission-contract` and never merge as though unparsed stdout were only a diagnostic. VERIFY prompts remain bounded, read-only, and must not ask the judge to edit `.devlyn`. On pair-JUDGE subprocess exit 124, write `.devlyn/verify.pair.timeout.json` with `{"engine": "<resolved name>", "budget_seconds": 600}` before merge. With that marker, a capture the parser rejects solely because its message stream was truncated before the terminal result stays `TIMEOUT`; every other parser rejection stays `BLOCKED`. The orchestrator records `phases.verify.judge_durations_ms: {judge: <int>, pair_judge: <int|null>}` as it collects each judge result; durations never nest inside `sub_verdicts`. After primary and pair findings are written, run `python3 "$DEVLYN_SHARED_DIR/verify-merge-findings.py" --write-state`; it rehashes and binds the VERIFY process-evidence carrier before merging. Branch only on the merged `state.phases.verify.verdict`; a HIGH/CRITICAL finding from either judge must mechanically become `NEEDS_WORK`. Never write `.devlyn/verify-merged.findings.jsonl` or `.devlyn/verify-merge.summary.json` by hand; `verify-merge-findings.py` is their only writer. State write: `phases.verify.{started_at, verdict, completed_at, duration_ms, judge_durations_ms: {judge, pair_judge}, sub_verdicts: {mechanical, judge, pair_judge?}, artifacts}`.
362
+ For policy-denied Windows Codex judge reads, prepare the complete inline evidence packet from `_shared/codex-config.md#constrained-windows-judge-reads` for each fresh judge, explicitly instructing no tools. Missing inputs produce a verdict-binding BLOCKED finding; sandbox, isolation, model/effort and 600s bounds remain unchanged.
363
+
364
+ Findings are written to `.devlyn/verify.findings.jsonl`. **Except on the supplied no-tools route, VERIFY agents may inspect authorized source, diff and sealed evidence using native read/search tools or non-mutating shell commands. They must not mutate files or execute verification, lint, test, build, probe or invented-scenario commands.** Capture the primary reply as `.devlyn/<primary-engine>-judge.stdout` and its stderr sibling; never use the generic `.devlyn/verify-judge.stdout`. A Codex primary-JUDGE uses the distinct route `DEVLYN_CODEX_PROMPT_FILE="<primary-prompt-file>" CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600 bash "$CODEX_MONITORED_PATH" -C "$PWD" -s read-only -c model_reasoning_effort=high - >.devlyn/codex-judge.stdout 2>.devlyn/codex-judge.stderr`: omit `-m` only without an explicit model; an explicit profile supplies the validated model/effort additions below. Omit bypass flags and capture both streams directly without pipes. On primary exit 124, write `.devlyn/verify.primary.timeout.json` with exactly `{"engine": "<resolved-primary-engine>", "budget_seconds": 600}` before merge. A malformed marker BLOCKs; a valid marker preserves canonical primary findings and floors `judge` at `BLOCKED` even when output is empty, never `PASS`, a solo verdict, or pair-style `TIMEOUT`. Without the marker, existing required-primary failure behavior is unchanged. For Codex pair-JUDGE, set `DEVLYN_CODEX_PROMPT_FILE="<pair-prompt-file>"` with sole prompt `-` and keep the read-only `codex-monitored.sh` route with `CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600` and `-c model_reasoning_effort=medium`, no `tail`/`head`/`grep` pipes, and direct stdout/stderr capture; every other resolved OTHER engine follows `_shared/adapters/<name>.md` `## Invocation`. Capture the pair reply as `.devlyn/<other-engine>-judge.stdout`, then run `python3 "$DEVLYN_SHARED_DIR/collect-codex-findings.py" --devlyn-dir "<abs repo>/.devlyn" --stdout-file <other-engine>-judge.stdout` before merge. Raw stdout is diagnostic-only. A non-zero collector exit must write no canonical `.devlyn/verify.pair.findings.jsonl`; set the pair source to `BLOCKED` for `verify.pair.emission-contract` and never merge as though unparsed stdout were only a diagnostic. VERIFY prompts remain bounded, read-only, and must not ask the judge to edit `.devlyn`. On pair-JUDGE subprocess exit 124, write `.devlyn/verify.pair.timeout.json` with `{"engine": "<resolved name>", "budget_seconds": 600}` before merge. With that marker, a capture the parser rejects solely because its message stream was truncated before the terminal result stays `TIMEOUT`; every other parser rejection stays `BLOCKED`. The orchestrator records `phases.verify.judge_durations_ms: {judge: <int>, pair_judge: <int|null>}` as it collects each judge result; durations never nest inside `sub_verdicts`. After primary and pair findings are written, run `python3 "$DEVLYN_SHARED_DIR/verify-merge-findings.py" --write-state`; it rehashes and binds the VERIFY process-evidence carrier before merging. Branch only on the merged `state.phases.verify.verdict`; a HIGH/CRITICAL finding from either judge must mechanically become `NEEDS_WORK`. Never write `.devlyn/verify-merged.findings.jsonl` or `.devlyn/verify-merge.summary.json` by hand; `verify-merge-findings.py` is their only writer. State write: `phases.verify.{started_at, verdict, completed_at, duration_ms, judge_durations_ms: {judge, pair_judge}, sub_verdicts: {mechanical, judge, pair_judge?}, artifacts}`.
362
365
 
363
366
  Branch:
364
367
  - `PASS` → PHASE 6.
@@ -75,7 +75,7 @@ When the rules are silent (rare — pathological goal text), default to `medium`
75
75
  The internal mini-spec written for trivial / medium / large-assumptions paths must satisfy:
76
76
 
77
77
  - `## Requirements` non-empty, each bullet testable (CLI command, test command, observable file change).
78
- - For generated verification carriers, follow `../../devlyn:ideate/references/spec-template.md` § "Sibling file: `spec.expected.json`" for constraint coverage, diff scope and guard controls.
78
+ - For executable generated verification, put one fenced `json` object with only `verification_commands` inside the sentinel-marked `## Verification` section. Generated mode reads this inline carrier, not a sibling file. Follow `../../devlyn:ideate/references/spec-template.md` § "Sibling file: `spec.expected.json`" for constraint coverage, diff scope and violating/allowed controls, but express those checks as executable `verification_commands`; sibling-only fields such as `required_files` and `forbidden_patterns` are rejected inline. Preserve uncovered semantics as source-review obligations.
79
79
  - `## Verification` is preceded by a `<!-- devlyn:verification -->` sentinel on its own line directly above the heading — the machine locator `spec-verify-check.py` uses; the heading text itself is decorative and may be any language. `## Verification` non-empty if the goal implies any runnable acceptance check. Empty Verification is allowed only when all Requirements are pure-design (e.g. "follow existing pattern X").
80
80
  - If a free-form goal includes pair-evidence intent and already includes an actionable solo-headroom hypothesis, preserve that literal hypothesis in `.devlyn/criteria.generated.md` unchanged enough for VERIFY to detect `solo-headroom hypothesis`, `solo_claude`, `miss`, and the backticked observable command line that itself contains `miss`, emit the canonical `spec.solo_headroom_hypothesis` pair trigger reason, and satisfy regenerated-evidence checks such as `benchmark audit --require-hypothesis-trigger`.
81
81
  - If a free-form goal includes unmeasured pair-candidate intent and already includes solo ceiling avoidance, preserve that literal note in `.devlyn/criteria.generated.md` unchanged enough for reviewers to see `solo ceiling avoidance`, `solo_claude`, and the concrete difference from rejected or solo-saturated controls such as `S2`-`S6`.
@@ -59,7 +59,7 @@ Append all findings; do not stop on the first failure.
59
59
 
60
60
  <output>
61
61
  - `.devlyn/build_gate.findings.jsonl` — JSONL stream, one finding per line. Schema: `{id, rule_id, severity, file, line, message, fix_hint, criterion_ref}`.
62
- - `.devlyn/build_gate.log.md` — human-readable summary of which gates ran and their raw output.
62
+ - `.devlyn/build_gate.log.md` — human-readable gate results with raw output or links to complete raw-output files, using sealed artifacts where available. Retain complete inventories/hash maps in `.devlyn/` files; report their paths, digests and relevant deltas instead of dumping them into the conversation.
63
63
  - Report `PASS` if zero CRITICAL/HIGH findings, `FAIL` for genuine product findings, or `BLOCKED:build-env-underprovisioned` only for the evidence-backed capability path above. A capability blocker leaves no product finding. Do not edit `pipeline.state.json` yourself — the orchestrator records it via `state-phase-write.py` from these artifacts.
64
64
  </output>
65
65
 
@@ -10,11 +10,13 @@ Independent quality layer. You answer one question: did the diff deliver what th
10
10
  - `spec.md` (or `.devlyn/criteria.generated.md` for free-form mode) — the contract.
11
11
  - `spec.expected.json` — the mechanical acceptance contract per `_shared/expected.schema.json`.
12
12
  - The cumulative diff against `state.base_ref.sha`.
13
- - The source hash (`state.source.spec_sha256` for spec mode, `state.source.criteria_sha256` for generated free-form mode) — re-read the source contract from disk and confirm the hash matches; if it does not, write `state.phases.verify.verdict: "BLOCKED"` with reason `source_sha256_mismatch` and stop.
14
- - `.devlyn/spec-verify.results.json` plus the validated VERIFY
13
+ - The source hash (`state.source.spec_sha256` for spec mode, `state.source.criteria_sha256` for generated free-form mode) — re-read the source contract from disk and confirm the hash matches; if it does not, emit a verdict-binding finding with reason `source_sha256_mismatch` and terminal verdict `BLOCKED`, then stop reviewing without editing state.
14
+ - `.devlyn/spec-verify.results.json` plus any required validated VERIFY
15
15
  process-evidence manifest and raw stdout/stderr streams named by its
16
16
  `process_evidence` carrier. These are immutable MECHANICAL results; rehash
17
- every named byte before JUDGE spawn and again during merge.
17
+ every named byte before JUDGE spawn and again during merge. Valid pure-design
18
+ contracts with no executable obligations have `commands: []` and
19
+ `process_evidence: null`.
18
20
 
19
21
  You do NOT receive: PLAN, IMPLEMENT's reasoning, BUILD_GATE's findings, CLEANUP's allowlist negotiations. Reading those would compromise independence. Inspect authorized source, diff and sealed evidence using native read/search tools or non-mutating shell commands; executable verification belongs exclusively to MECHANICAL.
20
22
  </input>
@@ -36,16 +38,18 @@ Emit findings to `.devlyn/verify-mechanical.findings.jsonl`. Each match = one fi
36
38
 
37
39
  ### JUDGE (fresh-context grading)
38
40
 
41
+ On the constrained Windows read route, the orchestrator supplies the complete spec/expected contract, accepted source SHA/diff, relevant source/tests with file:line locators, and validated sealed MECHANICAL results/manifests/required raw streams inline. Judge that supplied evidence without running tools. Missing, truncated or unbound inputs produce a verdict-binding BLOCKED finding. Retain fresh context, read-only sandbox, isolation, model/effort and bounded output; never widen permissions. This route addresses observed policy-denied reads, not a universal Windows sandbox limitation.
42
+
39
43
  Grade the diff against the spec on rubric axes:
40
44
 
41
- - **Spec compliance** — did every Requirement get an `evidence` record pointing at code that satisfies it?
45
+ - **Spec compliance** — does cited evidence show how every applicable Requirement and Constraint is satisfied?
42
46
  - **Scope** — does the diff touch only files PLAN listed (or the cleanup allowlist)? Out-of-scope file = HIGH finding `scope.out-of-scope-violation`.
43
47
  - **Quality** — does the implementation follow the framework's idiomatic patterns, or are there hand-rolled helpers replacing standard primitives? `design.unidiomatic-pattern` MEDIUM if so.
44
48
  - **Consistency** — internal style (naming, error shape, module boundaries) consistent with the surrounding code.
45
49
 
46
50
  **Bounded primary review**: the primary JUDGE makes one broad pass over the
47
51
  source contract, sealed MECHANICAL carrier, and cumulative diff, covering all
48
- four rubric axes and every binding Requirement clause. It then makes one
52
+ four rubric axes and every binding Requirement and Constraint clause. It then makes one
49
53
  targeted interaction pass over the clauses the broad pass left unresolved.
50
54
  Before any third pass, emit the required terminal result. If R1–R8 or another
51
55
  spec axis remains uncovered, emit a verdict-binding BLOCKED coverage finding
@@ -53,7 +57,7 @@ instead of continuing or assuming PASS.
53
57
 
54
58
  For each finding, write file:line evidence. Do not paraphrase code; quote it.
55
59
 
56
- **Clause-level check**: split each Requirement into its binding clauses before
60
+ **Clause-level check**: split each Requirement and Constraint into its binding clauses before
57
61
  you pass it. Words like `before`, `after`, `once`, `always`, `never`,
58
62
  `regardless`, `irrelevant`, `permanent`, `idempotent`, `duplicate`, `raw`, and
59
63
  `signature` usually encode a separate invariant. A passing verification command
@@ -88,7 +92,7 @@ risk probes and present in the sealed MECHANICAL evidence. Missing coverage is
88
92
  a verdict-binding finding; review the implementation's clause and code order
89
93
  without inventing a replacement command.
90
94
 
91
- **Coverage check**: before declaring done, confirm you have sealed evidence and code-order support for every spec axis. If an axis lacks declared MECHANICAL coverage, or the diff does not touch the code that produces it, set `state.verify.coverage_failed: true` and surface the missing-evidence finding rather than passing on assumption.
95
+ **Coverage check**: account for every applicable Requirement and Constraint using sealed MECHANICAL evidence for required executable checks and cited source/design evidence for pure-design clauses or explicitly retained source-review obligations. Inspect applicable unchanged code and documentation; absence of a code change alone is not missing coverage. Source review never substitutes for required executable coverage. For any unsupported clause, emit a verdict-binding coverage finding identifying the missing evidence. JUDGE does not run checks or edit state; the orchestrator records `state.verify.coverage_failed: true` from coverage findings before merge.
92
96
 
93
97
  **Verdict-binding check**: a demonstrated violation of an applicable mandatory
94
98
  task, public, or existing test contract is binding, including unmet new
@@ -219,10 +223,10 @@ generic `.devlyn/verify-judge.stdout`. When the primary engine is Codex, invoke
219
223
  it only through this distinct monitored route:
220
224
 
221
225
  ```bash
222
- CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600 bash "$CODEX_MONITORED_PATH" -C "$PWD" -s read-only -c model_reasoning_effort=high "<primary prompt>" >.devlyn/codex-judge.stdout 2>.devlyn/codex-judge.stderr
226
+ DEVLYN_CODEX_PROMPT_FILE="<primary-prompt-file>" CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600 bash "$CODEX_MONITORED_PATH" -C "$PWD" -s read-only -c model_reasoning_effort=high - >.devlyn/codex-judge.stdout 2>.devlyn/codex-judge.stderr
223
227
  ```
224
228
 
225
- This command supplies unconfigured defaults. Explicit judge profiles use
229
+ Write the complete prompt to the named file as exact UTF-8 bytes before launch and retain its generated `.transport.json` carrier. This command supplies unconfigured defaults. Explicit judge profiles use
226
230
  `SKILL.md#explicit-role-dispatch` for validated model/effort options and
227
231
  round-scoped evidence. Omit bypass flags; do not pipe either stream. On primary exit
228
232
  124, write `.devlyn/verify.primary.timeout.json` with exactly
@@ -233,7 +237,7 @@ floors `judge` at `BLOCKED`, including when findings/stdout are empty. It never
233
237
  produces `PASS`, a solo verdict, or pair-style `TIMEOUT`; without the marker,
234
238
  existing missing/invalid primary-output behavior remains fail-closed.
235
239
 
236
- Codex pair-JUDGE keeps the monitored
240
+ Codex pair-JUDGE sets `DEVLYN_CODEX_PROMPT_FILE="<pair-prompt-file>"` with sole prompt `-` and keeps the monitored
237
241
  `codex-monitored.sh` route with
238
242
  `CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600` and
239
243
  unconfigured default `-c model_reasoning_effort=medium`; isolation blocks user config, AGENTS.md,
@@ -67,7 +67,7 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
67
67
  - **implement_passed_sha** — captured at end of PHASE 2; null until then. Activates the post-implement invariant for SURFACE_CLOSE, CLEANUP, and VERIFY.
68
68
  - **criteria** — generated from spec's `## Requirements` checklist (one per `- [ ]`). `status: pending → implemented` is the legal transition. `failed_by_finding_ids` populates when VERIFY surfaces a finding tied to a criterion.
69
69
  - **`.devlyn/untracked.baseline` artifact** — written at PHASE 0 (`spec-verify-check.py --write-untracked-baseline`); BUILD_GATE flags created-during-run unauthorized untracked files against it (`.devlyn/` exempt; semantics in `phases/build-gate.md` step 4).
70
- - **verify.coverage_failed** — outcome-dependent telemetry set when JUDGE cannot exercise a spec axis. It no longer gates pair dispatch: after MECHANICAL passes, `pair.default` dispatches both judges concurrently whenever `pair_default_enabled != false` and the OTHER engine is available. An orchestrator without foreground parallel dispatch runs the same two required judges sequentially; a primary blocker never skips the pair. A verdict-binding MECHANICAL blocker still skips both. Legacy complexity values remain accepted only for archived compatibility.
70
+ - **verify.coverage_failed** — outcome-dependent telemetry set by the orchestrator when JUDGE reports missing clause evidence. It no longer gates pair dispatch: after MECHANICAL passes, `pair.default` dispatches both judges concurrently whenever `pair_default_enabled != false` and the OTHER engine is available. An orchestrator without foreground parallel dispatch runs the same two required judges sequentially; a primary blocker never skips the pair. A verdict-binding MECHANICAL blocker still skips both. Legacy complexity values remain accepted only for archived compatibility.
71
71
  - **verify.pair_trigger** — strict decision state: `{ "eligible": boolean, "reasons": string[], "skipped_reason": string|null }`. Schema v3.0 eligible state requires `pair.default` plus every applicable telemetry reason and `skipped_reason: null`; outcome-dependent reasons are appended after both judges return. Canonical reasons are `pair.default`, `mode.verify-only`, `mode.pair-verify`, `complexity.high`, `complexity.large`, `spec.complexity.high`, `spec.complexity.large`, `spec.solo_headroom_hypothesis`, `risk.high`, `risk_probes.enabled`, `risk_probes.present`, `coverage.failed`, `mechanical.warning`, and `judge.warning`. Ineligible new-run state has empty reasons and only `user_no_pair`, `mechanical_blocker`, `auto_pair_other_engine_unavailable`, or null; `primary_judge_blocker` remains parser-recognized only for archived v2.0 replay and retains its existing pre-known-reason rejection. Explicit `mode.pair-verify` cannot use the unavailable-engine skip. Missing, contradictory, incomplete, or unknown trigger state BLOCKs VERIFY.
72
72
 
73
73
  ## Per-phase shape
@@ -24,6 +24,33 @@ names look generated. An interrupted allocation stays blocked for inspection;
24
24
  do not delete its receipt and enroll the resulting branch. Pre-existing tasks
25
25
  remain owner-managed and cannot use this helper retroactively.
26
26
 
27
+ Allocation also returns a receipt-owned `scratch` directory. Put disposable
28
+ build intermediates there (for example, set
29
+ `CARGO_TARGET_DIR` to `<scratch>/target`). Keep source checkouts, Git data,
30
+ lockfiles, reports and restart evidence outside scratch. Do not create unowned
31
+ temporary build trees or preserve them by copying them into evidence custody.
32
+ Prefer `TemporaryDirectory`/`finally` for short-lived test fixtures.
33
+
34
+ After waiting for task writers, `complete --writers-stopped` empties this
35
+ scratch on delivery returns, including local-only and pending-PR routes. To
36
+ release build caches when parking or after a failed run, use:
37
+
38
+ ```sh
39
+ python3 "$DEVLYN_SHARED_DIR/task-complete.py" clean-scratch \
40
+ --receipt '<returned receipt.json>' --writers-stopped
41
+ ```
42
+
43
+ This empties only the prospectively owned scratch, preserves its directory
44
+ identity for interrupted cleanup and later builds, checks live users and reports
45
+ the logical byte count separately from actual free disk space. It does not depend on a product PASS or rewrite that verdict.
46
+ Unknown historical roots, source/recovery data, shared caches and terminal
47
+ history are never adopted or swept. A refused cleanup does not block source
48
+ delivery; it stays visible in the handoff with its receipt and retry command
49
+ (`CLEANUP_PENDING` after delivered source). No task is described as cleaned
50
+ while owned disposable files remain. On resumption, finish any pending scratch
51
+ cleanup before allocating more temporary storage. Rebuild from retained inputs
52
+ when needed; cleanup is not an instruction to restart parked work.
53
+
27
54
  ## Accept a scoped commit
28
55
 
29
56
  Direct work: finish actual decisive checks and diff review, stage only the
@@ -93,7 +120,7 @@ outside that checkout. An OS process observation supplements the owner assertion
93
120
  neither the receipt nor a scan guarantees exclusion of future writers. Unknown
94
121
  writers, inaccessible process state, caller cwd inside the tree, dirty/untracked
95
122
  files, changed refs/Gitdir/registration or locks retain the affected resources.
96
- Do not kill unknown processes or force worktree removal to make completion pass.
123
+ On native Windows, locking and file durability are supported, but the helper cannot prove writer cessation and reports `writer observation unsupported on this platform; retain workspace`. Even `--writers-stopped` cannot authorize deletion without that observation: retain the workspace, refs and external receipt/custody, report delivery separately, and preserve the receipt-based resume command. Do not kill unknown processes or force worktree removal to make completion pass.
97
124
 
98
125
  The receipt directory holds byte-verified `custody/`, `manifest.json` and a guarded
99
126
  `refs/devlyn/completed/<id>` recovery ref. Archive/check evidence is copied there
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
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": {
@@ -58,6 +58,7 @@ fi
58
58
  critical_path_files=$(cat <<'EOF'
59
59
  _shared/.ruff.toml
60
60
  _shared/process-evidence.py
61
+ _shared/platform-support.py
61
62
  _shared/invocation-receipt.py
62
63
  _shared/role-config.py
63
64
  _shared/judge-role-evidence.py
@@ -405,7 +406,7 @@ check_skill_mirror_parity \
405
406
  # ---------------------------------------------------------------------------
406
407
  # 6b. VERIFY merge verdict binding self-test.
407
408
  for helper in role-config judge-role-evidence task-complete; do
408
- if python3 "config/skills/_shared/$helper.py" --self-test >/dev/null 2>&1; then
409
+ if python3 "config/skills/_shared/$helper.py" --self-test; then
409
410
  ok "$helper.py self-test passed"
410
411
  else
411
412
  bad "$helper.py self-test failed"
@@ -514,7 +515,7 @@ if ! grep -Fq 'PHASES = {"implement", "build_gate", "verify"}' config/skills/_sh
514
515
  fi
515
516
 
516
517
  section "Check 6b2: Codex invocation receipts are phase-owned"
517
- if python3 config/skills/_shared/invocation-receipt.py --self-test >/dev/null 2>&1; then
518
+ if python3 config/skills/_shared/invocation-receipt.py --self-test; then
518
519
  ok "invocation-receipt.py self-test passed"
519
520
  else
520
521
  bad "invocation-receipt.py self-test failed"
@@ -627,7 +628,7 @@ for tree in config/skills .agents/skills; do
627
628
  || ! grep -Fq 'executor flag/pin' "$skill" \
628
629
  || ! grep -Fq 'auto_surface_close_claude_unavailable' "$skill" \
629
630
  || ! grep -Fq 'canonical body VERBATIM' "$skill" \
630
- || ! grep -Fq 'run-bounded.py 600 -- claude -p' "$skill" \
631
+ || ! grep -Fq 'run-bounded.py 600 --stdin-file .devlyn/surface-close.prompt.<round> --record-transport -- claude -p' "$skill" \
631
632
  || ! grep -Fq -- '--tools "Read,Grep,Glob,Edit,Write" --dangerously-skip-permissions --output-format json --strict-mcp-config --mcp-config '\''{"mcpServers":{}}'\''' "$skill" \
632
633
  || ! grep -Fq '.devlyn/surface-close.output.json' "$skill" \
633
634
  || ! grep -Fq '**Common post-fix checkpoint (BUILD_GATE and VERIFY):**' "$skill" \
@@ -824,7 +825,7 @@ if ! grep -Fq 'spec.expected.json top-level array produced a traceback' config/s
824
825
  || ! grep -Fq 'NaN risk-probes JSONL did not report invalid numeric constant' config/skills/_shared/spec-verify-check.py \
825
826
  || ! grep -Fq 'def reject_json_constant' config/skills/_shared/spec-verify-check.py \
826
827
  || ! grep -Fq 'loads_strict_json(line)' config/skills/_shared/spec-verify-check.py \
827
- || ! grep -Fq 'loads_strict_json(expected_path.read_text())' config/skills/_shared/spec-verify-check.py \
828
+ || ! grep -Fq 'loads_strict_json(expected_path.read_text(encoding="utf-8"))' config/skills/_shared/spec-verify-check.py \
828
829
  || ! grep -Fq 'top-level must be a JSON object' config/skills/_shared/spec-verify-check.py \
829
830
  || ! grep -Fq 'has invalid JSON' config/skills/_shared/spec-verify-check.py; then
830
831
  bad "spec-verify-check.py self-test must fail malformed spec.expected.json cleanly without traceback"
@@ -1564,7 +1565,7 @@ fi
1564
1565
  # ---------------------------------------------------------------------------
1565
1566
  section "Check 10a0: Codex primary VERIFY budget is bounded and mirrored"
1566
1567
  primary_verify_missing=0
1567
- primary_verify_route='CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600 bash "$CODEX_MONITORED_PATH" -C "$PWD" -s read-only -c model_reasoning_effort=high "<primary prompt>" >.devlyn/codex-judge.stdout 2>.devlyn/codex-judge.stderr'
1568
+ primary_verify_route='DEVLYN_CODEX_PROMPT_FILE="<primary-prompt-file>" CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600 bash "$CODEX_MONITORED_PATH" -C "$PWD" -s read-only -c model_reasoning_effort=high - >.devlyn/codex-judge.stdout 2>.devlyn/codex-judge.stderr'
1568
1569
  for file in \
1569
1570
  config/skills/devlyn:resolve/SKILL.md \
1570
1571
  .agents/skills/devlyn:resolve/SKILL.md \