okstra 0.133.0 → 0.135.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.
Files changed (36) hide show
  1. package/docs/architecture/storage-model.md +1 -1
  2. package/docs/cli.md +1 -1
  3. package/docs/for-ai/skills/okstra-pr-gen.md +2 -1
  4. package/docs/for-ai/skills/okstra-user-response.md +9 -4
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/antigravity-worker.md +1 -1
  8. package/runtime/agents/workers/codex-worker.md +1 -1
  9. package/runtime/agents/workers/report-writer-worker.md +3 -1
  10. package/runtime/bin/okstra-antigravity-exec.sh +12 -2
  11. package/runtime/bin/okstra-claude-exec.sh +10 -1
  12. package/runtime/bin/okstra-codex-exec.sh +12 -2
  13. package/runtime/prompts/lead/convergence.md +1 -1
  14. package/runtime/prompts/lead/team-contract.md +2 -2
  15. package/runtime/prompts/profiles/_common-contract.md +0 -1
  16. package/runtime/prompts/profiles/_implementation-executor.md +20 -1
  17. package/runtime/prompts/profiles/final-verification.md +1 -1
  18. package/runtime/prompts/profiles/implementation-planning.md +1 -0
  19. package/runtime/python/okstra_ctl/clarification_items.py +34 -12
  20. package/runtime/python/okstra_ctl/convergence_engine.py +11 -2
  21. package/runtime/python/okstra_ctl/models.py +2 -0
  22. package/runtime/python/okstra_ctl/render_final_report.py +1 -1
  23. package/runtime/python/okstra_ctl/run.py +30 -11
  24. package/runtime/python/okstra_ctl/session.py +36 -7
  25. package/runtime/python/okstra_ctl/user_response.py +14 -5
  26. package/runtime/python/okstra_ctl/worker_artifact_paths.py +5 -1
  27. package/runtime/python/okstra_ctl/worker_heartbeat.py +35 -4
  28. package/runtime/python/okstra_ctl/worker_liveness.py +19 -6
  29. package/runtime/python/okstra_ctl/worktree.py +39 -9
  30. package/runtime/python/okstra_token_usage/collect.py +75 -9
  31. package/runtime/python/okstra_token_usage/pricing.py +4 -1
  32. package/runtime/skills/okstra-pr-gen/SKILL.md +24 -0
  33. package/runtime/skills/okstra-user-response/SKILL.md +67 -19
  34. package/runtime/validators/forbidden_actions.py +1 -1
  35. package/runtime/validators/validate-run.py +26 -8
  36. package/runtime/validators/validate_session_conformance.py +17 -7
@@ -70,7 +70,7 @@ Artifacts from each execution accumulate beneath the current run's resolved `run
70
70
  <target-project>/.okstra/tasks/<task-group>/<task-id>/runs/<task-type>/
71
71
  ```
72
72
 
73
- The representative files below are all relative to the resolved run directory (`run_dir`). Therefore, report, log, carry, and session paths for stage-isolated runs include the `stage-<N>/` segment.
73
+ The representative files below are all relative to the resolved run directory (`run_dir`). Therefore, report, log, and session paths for stage-isolated runs include the `stage-<N>/` segment. The carry sidecar is the exception — it stays flat under the task-type run dir (see the `carry/stage-<N>.json` entry below).
74
74
 
75
75
  - `manifests/run-manifest-<task-type>-<seq>.json`
76
76
  - `state/team-state-<task-type>-<seq>.json`
package/docs/cli.md CHANGED
@@ -343,7 +343,7 @@ Then `okstra codex-dispatch --project-root <dir> --run-manifest <run-manifest> -
343
343
  The Codex worker (`--workers codex`, `--codex-model`) and Codex lead runtime are separate. The former selects a worker provider dispatched by a Claude Code lead; the latter is the upper runtime boundary for running the lead itself through the Codex adapter.
344
344
 
345
345
  > Every `--*-model` flag accepts only aliases registered in the provider mappings in `scripts/okstra_ctl/models.py`. An unregistered value is immediately rejected with `UnknownModelError`, preventing a contract violation where the manifest's `modelExecutionValue` differs from the actual execution value. Allowed values:
346
- > - Claude (`--lead-model` / `--claude-model` / `--report-writer-model`): `fable`, `fable-5`, `claude-fable-5`, `opus`, `opus-4-8`, `claude-opus-4-8`, `opus-4-7`, `claude-opus-4-7`, `opus-4-6`, `claude-opus-4-6`, `sonnet`, `sonnet-4-6`, `claude-sonnet-4-6`, `haiku`, `haiku-4-5`, `claude-haiku-4-5`, `claude-haiku-4-5-20251001`
346
+ > - Claude (`--lead-model` / `--claude-model` / `--report-writer-model`): `fable`, `fable-5`, `claude-fable-5`, `opus`, `opus-5`, `claude-opus-5`, `opus-4-8`, `claude-opus-4-8`, `opus-4-7`, `claude-opus-4-7`, `opus-4-6`, `claude-opus-4-6`, `sonnet`, `sonnet-4-6`, `claude-sonnet-4-6`, `haiku`, `haiku-4-5`, `claude-haiku-4-5`, `claude-haiku-4-5-20251001`
347
347
  > - Codex (`--codex-model`): `gpt-5.6-sol`, `gpt-5.6`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.2`, `codex-auto-review`
348
348
  > - Antigravity (`--antigravity-model`): `gemini-3.1-pro` (default), `gemini-3.5-flash`, and the space-separated aliases `gemini 3.1 pro` / `gemini 3.5 flash`. The antigravity worker uses the `agy` CLI to run Gemini-family models, so model IDs retain the `gemini-*` form.
349
349
 
@@ -34,7 +34,8 @@ A 3-option picker via `AskUserQuestion`:
34
34
  1. Pick a template: `okstra pr template list --json`. If empty, use the bundled default. Carry the chosen name as `<template>` (`default` for the bundled one).
35
35
  2. Pick the base branch: `okstra pr branches --json`. 3-option from the top `recommended` entries plus `Enter directly`. Carry the choice as `<base>`.
36
36
  3. Generation bundle: `okstra pr gen --base <base> --template <template> --json` → `{base, currentBranch, templateName, template, commits, diffStat}`. Then **read the real diff honestly** (SSOT): `git diff <base>...HEAD` (large diffs section by section). Fill the placeholders from the diff and commits, describing **only actual changes**. Mark a checklist box `[x]` only when the diff supports it (tests touched → tests box, docs touched → docs box). If `commits`/`diffStat` are empty, say there is nothing to describe and stop. **Never append AI trailers/footers.**
37
- 4. Output and offer to create the PR: print the filled PR body as a single fenced markdown block. Ask whether to open a PR. **Only on an explicit yes**: write the body to a temp file and run `gh pr create --base <base> --title "<title>" --body-file <path>`. If `gh` is missing or unauthenticated (`gh auth status` fails), leave the text in chat and give manual-creation guidance. **No push/PR creation without the user's confirmation.**
37
+ 4. Identifier allowlist for the title and body: only repo-relative source paths (optionally `path:line`), symbol names present in the diff, branch names / commit subjects / SHAs, and issue-tracker ticket ids the reviewer can open. okstra's own artifact identifiers are out of the allowlist report item ids (`F-001`, `C-001`, `R-001`, `D-0001`, `PREP-001`), run artifact names and their `<task-type>-<seq>` suffixes, phase/stage/worker labels (`final-verification`, `stage-2`, `codex-worker`), and any path under `.okstra/`. They resolve to nothing for a reviewer; restate the substance in code terms instead of citing the id.
38
+ 5. Output and offer to create the PR: print the filled PR body as a single fenced markdown block. Ask whether to open a PR. **Only on an explicit yes**: write the body to a temp file and run `gh pr create --base <base> --title "<title>" --body-file <path>`. If `gh` is missing or unauthenticated (`gh auth status` fails), leave the text in chat and give manual-creation guidance. **No push/PR creation without the user's confirmation.**
38
39
 
39
40
  ## Mode B — Register template
40
41
 
@@ -10,7 +10,9 @@
10
10
 
11
11
  `okstra-user-response` answers the **unresolved clarification questions** an okstra run left behind (the open `C-*` rows under the final report's `## 1. Clarification Items`) **in-session**, and records those answers as a `runs/<type>/user-responses/` sidecar. The next `/okstra-run` auto-attaches this sidecar via `--clarification-response`.
12
12
 
13
- **Core principle — the skill never picks an answer for the user.** It only *presents* recommendations and alternatives; every `value` is the user's verbatim input. It does not call `write` until the user has explicitly confirmed (`confirmed`).
13
+ **Core principle — the skill never picks an answer for the user.** It builds the option board — background, a self-contained question, three concrete answers, `Enter directly` — and the user alone picks from it; every `value` is what the user chose or typed. It does not call `write` until the user has explicitly confirmed (`confirmed`).
14
+
15
+ **Second principle — one question at a time.** Never batch two clarification items into one question, and never dump the whole open list at the user.
14
16
 
15
17
  Distinguish it from starting a run (`okstra-run`), inspecting a finished task (`okstra-inspect`), and generating a brief (`okstra-brief-gen`).
16
18
 
@@ -34,9 +36,12 @@ okstra preflight --runtime claude-code --json
34
36
 
35
37
  ## Flow
36
38
 
37
- 1. **list**: `okstra user-response list --home <home> --project <projectId> --limit 3` → an array of `{taskKey, taskType, seq, reportPath, reportMtime, openApprovalCount, unreadable}`. If the array is empty, stop with "no open clarification". A 3-option picker (top recommendations + the final option always "Enter directly" for pasting a `reportPath`/`task-key` directly). `unreadable:true` is a §1 format drift — flag it with `⚠` and do not proceed (do not fabricate rows).
38
- 2. **show**: `okstra user-response show --report <reportPath>` → `rows[]` with `resolvedRefs` (the `definition` of internal tokens such as `RB-002`/`§4.7`). **Do not paste the raw `statement` as the question.** For each open row present: (a) a **self-contained question** with internal tokens expanded inline first, (b) `recommended` (only a recommendation) in plain language, (c) `alternatives[]`, (d) the raw `id`+`statement` as a secondary traceability aid. If `definition` is `null`, resolve the report `§`/`path:line` yourself **using Read**.
39
- 3. **decision (4 branches)**: the user picks one per item Answer (accept the recommendation / own answer, verbatim), Ask-to-explain (explain only and hand the decision back to the user), Free-form (narrative verbatim), Hold+reframe (`disposition:"reframe"`, does not satisfy the approval gate). Each item's JSON: `{id, kind, value, rationale?, disposition}`.
39
+ 1. **list**: `okstra user-response list --home <home> --project <projectId> --limit 3` → an array of `{taskKey, taskType, seq, reportPath, reportMtime, openBlockerCount, openApprovalCount, unreadable}` (`openBlockerCount` = open rows with `Blocks` in `{approval, next-phase}`; `openApprovalCount` = the `approval`-only subset). If the array is empty, stop with "no open clarification". A 3-option picker (top recommendations + the final option always "Enter directly" for pasting a `reportPath`/`task-key` directly). `unreadable:true` is a §1 format drift — flag it with `⚠` and do not proceed (do not fabricate rows).
40
+ 2. **show (data fetch, not a presentation step)**: `okstra user-response show --report <reportPath>` → `rows[]` with `resolvedRefs` (the `definition` of internal tokens such as `RB-002`/`§4.7`). Do not print `rows` at the user and **do not paste the raw `statement` as the question** announce only `<N> open items I'll go through them one at a time.`
41
+ 3. **ask, one item at a time**: iterate the rows in report order, **one item per `AskUserQuestion` call**, headed `[n/N] C-014 blocks: approval gate`. Per item:
42
+ - **Background first**, in the message text above the picker, 3–6 lines: (a) *Situation* — what the run was doing when it stopped here; (b) *What is undecided* — the fork, internal tokens expanded inline from `resolvedRefs[].definition`, plus what is stuck (`approval` → the approval gate stays shut and `implementation` cannot start; `next-phase` → the next phase cannot begin); (c) *What changes with your answer*. Source it from `resolvedRefs[].definition`, else **Read** the `§`/`path:line` in `contextRefs[]`; **never invent it** — say the report is silent instead. Close with `Source: C-014 — "<raw statement>"`.
43
+ - **Picker: exactly four options** — 1) `recommended` restated plainly, label suffixed `(Recommended)`, rationale in the description; 2) `alternatives[0]`; 3) `alternatives[1]`; 4) `Enter directly`, always last. When `alternatives[]` is short, fill the slot with a concrete candidate the report itself names (description opens `Not an okstra proposal — from <where>`), else `Reframe this question` — which is what guarantees three answers plus `Enter directly`. Never pad with an unsourced option; never mark anything but `recommended` as recommended.
44
+ - **Transcribe**: option 1–3 → `value` = that option's full answer text, `disposition:"answer"`; `Enter directly` → the user's utterance verbatim, `disposition:"answer"`; `Reframe this question` → `disposition:"reframe"` (does not satisfy the approval gate). A question back from the user records nothing — **Read** the ref, explain, re-ask the same item with the same four options. Echo `[n/N] C-014 → answer: …` and move on. Each item's JSON: `{id, kind, value, rationale?, disposition}`.
40
45
  4. **echo → confirmed gate**: before `write`, echo the whole collection (each `id`·`disposition`·`value`·`rationale`·approval) as-is and get explicit confirmation. Never `write` before `confirmed`. On any change, re-echo and re-confirm.
41
46
  5. **approval (optional)**: only when the approval-blocking items are **all filled with an answer** and the user explicitly approved, `--approval '{"approved":true,"implementationOption":"<selected option>"}'`. If any item is unfilled/reframe, do not approve and say the gate is still open.
42
47
  6. **write**: `okstra user-response write --report <reportPath> --answers '<json>' [--approval '<json>'] --task-key <taskKey>` → report the returned `{sidecar:<path>}`. (When a same-named sidecar exists, the same `id` is overwritten with the new value and merged.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.133.0",
3
+ "version": "0.135.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.133.0",
3
- "builtAt": "2026-07-22T19:29:52.565Z",
2
+ "package": "0.135.0",
3
+ "builtAt": "2026-07-25T13:20:54.916Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -91,7 +91,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
91
91
  - Otherwise (mtime stale `> 90s`, OR grace already applied): call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms <observed_ms>`, `--message "okstra-antigravity-exec.sh exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, and return `ANTIGRAVITY_CLI_TIMEOUT: agy exec exceeded polling cap`.
92
92
  4. Otherwise continue polling. Read `current_ts` cheaply via another `Bash` call (`date +%s`) at most once per poll iteration.
93
93
  - Do NOT abort the loop on transient `running` status. Only `completed` or the polling cap (initially 30min, optionally extended once to 35min by mtime grace) end it.
94
- - **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit — handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
94
+ - **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s, or 1500s for the build-running `executor` / `verifier` roles), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit — handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
95
95
  - **No external timeout from Lead.** This polling loop and the script's idle watchdog are together the SINGLE timeout authority for this dispatch. Lead MUST NOT impose a separate Agent-call timeout that would terminate this subagent before those caps fire (see team-contract "No external timeout on wrapper subagents").
96
96
  - Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
97
97
  - **No standalone `sleep` between polls.** The harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps to work around it. `BashOutput` itself is the wait primitive — calling it again immediately after a `running` status is correct.
@@ -91,7 +91,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
91
91
  - Otherwise (mtime stale `> 90s`, OR grace already applied): call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms <observed_ms>`, `--message "okstra-codex-exec.sh exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, and return `CODEX_CLI_TIMEOUT: codex exec exceeded polling cap`.
92
92
  4. Otherwise continue polling. Read `current_ts` cheaply via another `Bash` call (`date +%s`) at most once per poll iteration.
93
93
  - Do NOT abort the loop on transient `running` status. Only `completed` or the polling cap (initially 30min, optionally extended once to 35min by mtime grace) end it.
94
- - **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit — handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
94
+ - **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s, or 1500s for the build-running `executor` / `verifier` roles), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit — handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
95
95
  - **No external timeout from Lead.** This polling loop and the script's idle watchdog are together the SINGLE timeout authority for this dispatch. Lead MUST NOT impose a separate Agent-call timeout that would terminate this subagent before those caps fire (see team-contract "No external timeout on wrapper subagents").
96
96
  - Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
97
97
  - **No standalone `sleep` between polls.** The harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps to work around it. `BashOutput` itself is the wait primitive — calling it again immediately after a `running` status is correct.
@@ -41,7 +41,9 @@ Do NOT duplicate the data.json contents here — the data.json is the canonical
41
41
 
42
42
  ## Heartbeat (BLOCKING)
43
43
 
44
- Write the audit sidecar at `**Audit sidecar path:**` before required reading, then append `- PROGRESS: <stage> <ISO-8601-UTC>` at a cadence no longer than five minutes. Use only: `started`, `required-reading-complete`, `synthesis-start`, `data-json-write-start`, `render-start`, and `write-result-start`. The Phase 7 validator enforces the first stage, timestamps, cadence, and this allowlist.
44
+ Write the audit sidecar at `**Audit sidecar path:**` before required reading, then append `- PROGRESS: <stage> <ISO-8601-UTC>` at a cadence no longer than five minutes. Use only: `started`, `required-reading-complete`, `synthesis-start`, `data-json-write-start`, `render-start`, and `write-result-start`. When a stage runs longer than that, append `- PROGRESS: in-stage:<stage> <ISO-8601-UTC>` with the same stage names. The Phase 7 validator enforces the first stage, timestamps, cadence, and this allowlist.
45
+
46
+ `data-json-write-start`, `render-start` and `write-result-start` open a stretch whose work is one uninterruptible tool call, so no `in-stage:` line can be appended while it runs. Those three carry a 20-minute budget instead of five; every other stage keeps the five-minute cadence.
45
47
 
46
48
  ## Execution Rules
47
49
 
@@ -17,7 +17,8 @@
17
17
  #
18
18
  # project-root / model-execution-value / prompt-path are required.
19
19
  #
20
- # idle-timeout-seconds is optional (default 600 = 10 minutes). When > 0, an
20
+ # idle-timeout-seconds is optional (default 600s, or 1500s for the build-running
21
+ # executor/verifier roles — see the role case below). When > 0, an
21
22
  # in-process watchdog polls the live-log mtime; if no stdout/stderr write
22
23
  # occurs for that many seconds, the underlying `agy` is SIGTERM'd (then
23
24
  # SIGKILL'd after a 5-second grace), the status sidecar gets a
@@ -60,7 +61,16 @@ model="$2"
60
61
  prompt_path="$3"
61
62
  worktree_path="${4-}"
62
63
  role="${5:-worker}"
63
- idle_timeout_secs="${6:-600}"
64
+ # Implementation executor / verifier dispatches run whole build + test suites
65
+ # that legitimately emit no stdout for many minutes; the 600s idle cap reaps
66
+ # them mid-suite (observed: a silent jest + tsc build TERM'd at ~929s). Default
67
+ # those roles to a longer idle cap that still sits below the 1800s wall-clock
68
+ # polling cap so a genuine hang is reaped early. An explicit 6th arg wins.
69
+ case "$role" in
70
+ executor | verifier) default_idle_timeout_secs=1500 ;;
71
+ *) default_idle_timeout_secs=600 ;;
72
+ esac
73
+ idle_timeout_secs="${6:-$default_idle_timeout_secs}"
64
74
 
65
75
  if ! [[ "$idle_timeout_secs" =~ ^[0-9]+$ ]]; then
66
76
  printf 'okstra-antigravity-exec: idle-timeout-seconds must be a non-negative integer: %q\n' "$idle_timeout_secs" >&2
@@ -12,7 +12,16 @@ model="$2"
12
12
  prompt_path="$3"
13
13
  worktree_path="${4-}"
14
14
  role="${5:-worker}"
15
- idle_timeout_secs="${6:-600}"
15
+ # Implementation executor / verifier dispatches run whole build + test suites
16
+ # that legitimately emit no stdout for many minutes; the 600s idle cap reaps
17
+ # them mid-suite (observed: a silent jest + tsc build TERM'd at ~929s). Default
18
+ # those roles to a longer idle cap that still sits below the 1800s wall-clock
19
+ # polling cap so a genuine hang is reaped early. An explicit 6th arg wins.
20
+ case "$role" in
21
+ executor | verifier) default_idle_timeout_secs=1500 ;;
22
+ *) default_idle_timeout_secs=600 ;;
23
+ esac
24
+ idle_timeout_secs="${6:-$default_idle_timeout_secs}"
16
25
 
17
26
  if [[ ! -d "$project_root" ]]; then
18
27
  printf 'okstra-claude-exec.sh: project root not found: %s\n' "$project_root" >&2
@@ -17,7 +17,8 @@
17
17
  #
18
18
  # project-root / model-execution-value / prompt-path are required.
19
19
  #
20
- # idle-timeout-seconds is optional (default 600 = 10 minutes). When > 0, an
20
+ # idle-timeout-seconds is optional (default 600s, or 1500s for the build-running
21
+ # executor/verifier roles — see the role case below). When > 0, an
21
22
  # in-process watchdog polls the live-log mtime; if no stdout/stderr write
22
23
  # occurs for that many seconds, the underlying `codex exec` is SIGTERM'd
23
24
  # (then SIGKILL'd after a 5-second grace), the status sidecar gets a
@@ -77,7 +78,16 @@ model="$2"
77
78
  prompt_path="$3"
78
79
  worktree_path="${4-}"
79
80
  role="${5:-worker}"
80
- idle_timeout_secs="${6:-600}"
81
+ # Implementation executor / verifier dispatches run whole build + test suites
82
+ # that legitimately emit no stdout for many minutes; the 600s idle cap reaps
83
+ # them mid-suite (observed: a silent jest + tsc build TERM'd at ~929s). Default
84
+ # those roles to a longer idle cap that still sits below the 1800s wall-clock
85
+ # polling cap so a genuine hang is reaped early. An explicit 6th arg wins.
86
+ case "$role" in
87
+ executor | verifier) default_idle_timeout_secs=1500 ;;
88
+ *) default_idle_timeout_secs=600 ;;
89
+ esac
90
+ idle_timeout_secs="${6:-$default_idle_timeout_secs}"
81
91
 
82
92
  if ! [[ "$idle_timeout_secs" =~ ^[0-9]+$ ]]; then
83
93
  printf 'okstra-codex-exec: idle-timeout-seconds must be a non-negative integer: %q\n' "$idle_timeout_secs" >&2
@@ -84,7 +84,7 @@ Read the worker result files generated in Phase 4/5 and extract individual findi
84
84
  - Same semantics but disjoint ticket sets → separate groups (do NOT over-merge across tickets).
85
85
  - Only one worker confirms a finding → one single-source group.
86
86
  4. When grouping is ambiguous, prefer splitting over merging (avoid over-merging). Semantic matching, ticket-set equality, and evidence interpretation remain lead judgments; the engine does not perform fuzzy matching or decide whether evidence is credible.
87
- 5. Write `runs/<task-type>/state/convergence-groups-<task-type>-<seq>.json`. Each group carries its `ticketIds`, `originWorker`, `originEvidence`, `discoveredBy`, and every `<worker>:<item-id>` source in `sourceItems`. When a live command or external read produced reproducible evidence, also include `evidenceArtifacts[]` with its `.okstra/` path, SHA-256 digest, command, and environment. The field is optional because historical or inaccessible evidence may not have a captured artifact. The lead and verifier MUST NOT infer live or external evidence from wording or keyword matching; they use the finding's explicit claim, provenance, and supplied artifacts. Include the resolved worker roster in order with functional `audience` values; do not derive scope from provider or model identity.
87
+ 5. Write `runs/<task-type>/state/convergence-groups-<task-type>-<seq>.json`. Each group carries its `ticketIds`, `originWorker`, `originEvidence`, `discoveredBy`, and every `<worker>:<item-id>` source in `sourceItems`. When a live command or external read produced reproducible evidence, also include `evidenceArtifacts[]` with its `.okstra/` path, SHA-256 digest, command, and environment. The field is optional because historical or inaccessible evidence may not have a captured artifact. The lead and verifier MUST NOT infer live or external evidence from wording or keyword matching; they use the finding's explicit claim, provenance, and supplied artifacts. Include the resolved worker roster in order with functional `audience` values; do not derive scope from provider or model identity. The `audience` enum is a convergence role, not a phase label: every finding-producing worker uses `analysis` — an `implementation` run's verifiers included — and only the report author uses `report-writer`. There is no `implementation-verifier` audience here; map the verifier roster to `analysis`.
88
88
  6. Do not write a queue or classification in this grouped-input artifact. `okstra convergence seed` deterministically marks multi-source groups `full-consensus` and puts only single-source groups in the working queue. Section 6 never enters the grouped input.
89
89
 
90
90
  ### Round 1-N: Re-verification Loop (queue-pruned)
@@ -226,13 +226,13 @@ omits either header, the worker MUST return `<WORKER>_ERRORS_PATH_MISSING`
226
226
  without proceeding.
227
227
 
228
228
  - `cli-failure` events are recorded by the wrapper subagent itself (Codex / Antigravity), but **directly to the run-level error log** via `okstra error-log append-observed --error-type cli-failure ...` — NOT via the sidecar. The sidecar is an in-process tool-failure channel only.
229
- - **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-antigravity-exec.sh` take three required positional arguments plus three optional ones, matching their usage line verbatim: `<project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise — when passed it must be an existing directory (the wrapper hard-fails otherwise). For codex it becomes `--add-dir <worktree>` (sandbox write access); for antigravity it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt. The optional fifth `<role>` is folded into both the caller (worker) pane title `<cli>-<role>` and the sibling trace-pane title `<cli>-<role>-tail` (e.g. `codex-executor` ↔ `codex-executor-tail`; antigravity uses the `agy-` prefix). The selected adapter maps the functional assignment identity to this value; when the value is absent, pass the literal `worker` (the wrapper also defaults to `worker` if the argument is omitted). The optional sixth `<idle-timeout-seconds>` overrides the wrapper's stdout-idle watchdog (default 600 — see "No external timeout on wrapper subagents" below).
229
+ - **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-antigravity-exec.sh` take three required positional arguments plus three optional ones, matching their usage line verbatim: `<project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise — when passed it must be an existing directory (the wrapper hard-fails otherwise). For codex it becomes `--add-dir <worktree>` (sandbox write access); for antigravity it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt. The optional fifth `<role>` is folded into both the caller (worker) pane title `<cli>-<role>` and the sibling trace-pane title `<cli>-<role>-tail` (e.g. `codex-executor` ↔ `codex-executor-tail`; antigravity uses the `agy-` prefix). The selected adapter maps the functional assignment identity to this value; when the value is absent, pass the literal `worker` (the wrapper also defaults to `worker` if the argument is omitted). The optional sixth `<idle-timeout-seconds>` overrides the wrapper's stdout-idle watchdog (default 600s, or 1500s when `<role>` is `executor` / `verifier` those roles run silent build+test suites — see "No external timeout on wrapper subagents" below).
230
230
  - **Background dispatch + polling contract (Codex / Antigravity wrappers).** Both wrapper subagents MUST start their CLI through the selected adapter's asynchronous execution mapping and await the same handle until it reports terminal completion, capped at 30 minutes (1800s) of wall-clock elapsed time. The adapter's await operation is the wait primitive; do not add a standalone sleep or build shorter-sleep loops to bypass a host constraint. This rule applies in **every phase**. Recording responsibilities:
231
231
  - Successful completion: return the wrapper's accumulated stdout from the terminal await result. No log entry.
232
232
  - Non-zero `exit_code`: record a `cli-failure` to the run-level error log with the real `exit_code` and observed `duration-ms`.
233
233
  - Polling cap reached: perform a one-shot **mtime-grace check** on the wrapper's live log (`<prompt>.log`). If the log was written within the last 90 seconds and grace has not yet been applied, extend the cap from 1800s to 2100s and continue awaiting. Otherwise call the selected adapter's termination mapping, record `cli-failure` with `--exit-code 124 --duration-ms <observed_ms> --message "<wrapper> exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, then return the language-specific `*_CLI_TIMEOUT` sentinel.
234
234
  - The selected adapter owns runtime-session accounting for the full wrapper window; core retains only the observed start/end event boundaries.
235
- - **No external timeout on wrapper subagents.** The codex/antigravity wrapper owns BOTH of its timeout mechanisms, and they are the only two: (1) the subagent's polling cap (30min + optional 5min mtime grace), and (2) the wrapper script's internal stdout-idle watchdog — if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI and records a `timeout` stage in the status sidecar (`okstra-codex-exec.sh` / `okstra-antigravity-exec.sh`). A long-running but *chatty* CLI is never idle-killed; a silent hang is reaped at the idle cap instead of burning the full 30 minutes. Lead MUST NOT impose a separate `dispatch_worker` timeout, an outer `Bash` wall-clock deadline, or any other mechanism that terminates the subagent before the wrapper's own caps fire. Doing so reproduces the historical failure mode that motivated this rule: Lead aborts the subagent at e.g. 18 minutes, the subagent returns nothing, and Lead classifies the role as "no response" while the underlying CLI was actively working. The caps are calibrated so that, combined with Lead's redispatch policy (see "Lead Redispatch Policy on Result-Missing"), a recoverable single-run failure costs at most ~70 minutes of wall-clock — predictable enough to plan around. If a specific run requires a tighter cap, pass a lower `<idle-timeout-seconds>` or lower the polling cap in the wrapper subagent's polling contract (single source of truth), NOT by layering Lead-side timeouts.
235
+ - **No external timeout on wrapper subagents.** The codex/antigravity wrapper owns BOTH of its timeout mechanisms, and they are the only two: (1) the subagent's polling cap (30min + optional 5min mtime grace), and (2) the wrapper script's internal stdout-idle watchdog — if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s, or 1500s for the build-running `executor` / `verifier` roles), the script TERM/KILLs the CLI and records a `timeout` stage in the status sidecar (`okstra-codex-exec.sh` / `okstra-antigravity-exec.sh`). A long-running but *chatty* CLI is never idle-killed; a silent hang is reaped at the idle cap instead of burning the full 30 minutes. Lead MUST NOT impose a separate `dispatch_worker` timeout, an outer `Bash` wall-clock deadline, or any other mechanism that terminates the subagent before the wrapper's own caps fire. Doing so reproduces the historical failure mode that motivated this rule: Lead aborts the subagent at e.g. 18 minutes, the subagent returns nothing, and Lead classifies the role as "no response" while the underlying CLI was actively working. The caps are calibrated so that, combined with Lead's redispatch policy (see "Lead Redispatch Policy on Result-Missing"), a recoverable single-run failure costs at most ~70 minutes of wall-clock — predictable enough to plan around. If a specific run requires a tighter cap, pass a lower `<idle-timeout-seconds>` or lower the polling cap in the wrapper subagent's polling contract (single source of truth), NOT by layering Lead-side timeouts.
236
236
  - `contract-violation` events (C) are recorded by Lead via `okstra error-log append-observed --error-type contract-violation ...` after inspecting worker outputs.
237
237
  - Lead's responsibility regarding the sidecar is to dump it to the run-level error log via `okstra error-log append-from-worker` after each worker terminates; Lead does not write into the sidecar.
238
238
 
@@ -75,7 +75,6 @@ profile document.
75
75
  - Cross-worker traceability (shared — applies to every analysis worker output and to the lead's `## 6.` / `## 2.` tables in the final-report):
76
76
  - **Worker-side item IDs (free-form but unique within the worker).** Every row item in sections 1–5 (and any optional section 6) of an analysis worker's output MUST carry an item ID that is unique within that one worker's result file. The ID convention is the worker's choice — `F-001` / `F-002` per the suggested schema, `1.1` / `1.2` / `1.3` as Codex tends to use, or any other shape — but it MUST appear as the leading column of the row (for table-form items) or as a `[<ID>]` prefix (for bullet/numbered items). Workers that emit findings without IDs make cross-worker reconciliation impossible.
77
77
  - **Lead-side ID assignment + source preservation.** When the lead (or `report-writer-worker`) synthesises `## 6.1 Consensus` / `## 6.2 Differences` / `## 2.1 Primary Evidence` rows from worker outputs, the lead assigns a fresh `C-NNN` / `D-NNN` / `E-NNN` row ID. The `Source items` column (or, where the template still calls it `Supporting workers` / `Workers (position)` / `Source`, that same column) MUST list every contributing worker:item pair (e.g. `claude:F-001, codex:1.1, antigravity:F-3`) so a reviewer can trace the synthesised row back to each worker's original wording without re-reading every worker-results file. Bare worker names without item IDs (e.g. `claude, codex, antigravity`) are rejected. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.SourceItem` pins each entry to `^[a-z][a-z-]*:[A-Za-z0-9._-]+$`, and `ConsensusRow` / `PrimaryEvidenceRow` require a non-empty `sourceItems`, so a bare worker name fails schema validation.
78
- - **Why this matters.** A real run had `claude=F-1..F-11`, `codex=1.1..1.8`, `antigravity=F-3..F-9` — three incompatible ID schemes. When the lead synthesised `C-1..C-8`, the link from `C-3` back to "which sentence in which worker file" was lost. Source-item preservation restores that link without forcing every worker to adopt a single ID prefix, which would over-constrain worker output style.
79
78
  - Audit sidecar (shared): Reading Confirmation placement follows the audience-selected preamble named by `**Worker Preamble Path:**`. Profiles do not restate it; the main worker-results body starts at section 1.
80
79
 
81
80
  - Markdown authoring (shared — applies to markdown documents not already governed by an okstra template/schema):
@@ -35,6 +35,7 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
35
35
  - Doc-only / config-only / pure-rename steps that have no observable runtime behaviour are exempt from the failing-test requirement, but the executor MUST cite the exemption per step in the final report (`TDD exemption: <reason>`).
36
36
  - When the touched area has no existing test harness, the executor MUST stand up the minimum harness needed to host one regression test for this run rather than skipping TDD entirely. Record the harness-bootstrap step as an `Out-of-plan edit` if it is not in the plan.
37
37
  - **DB / IO / SQL changes require real execution — mock-only is NOT validation evidence:** when this run's diff touches DB/IO/SQL (ORM / query-builder code — sequelize / typeorm / prisma / knex / raw SQL — `*.repository.*`, model/entity files, `migrations/**`, `*.sql`, or any changed query string), a mocked unit test cannot observe the SQL the query builder actually emits (observed failure class: `_implementation-verifier.md` §"DB / IO / SQL change — real-execution gate"). The executor MUST run the change against a real (or faithful-replica) datastore — the `db-test` validation step (plan `validation` db step, else `project.json.qaCommands.db-test`), targeting a **local / replica** DB — and cite its exact command + exit code in the final report's `Validation evidence`. If no real DB / `db-test` command is reachable, do NOT claim the change verified: label the DB portion `static-analysis only …, unverified (not executed)` in the report, surface it in the routing recommendation, and never downplay the real run as "too heavy". `git push` stays forbidden (universal list); the unverified DB state is carried forward so `final-verification` cannot accept it and `release-handoff` cannot push.
38
+ - **External-source adapters — structure AND fixture both derive from a captured real sample; a self-authored fixture is NOT reality evidence:** when this run's diff builds or changes an `external-interface` or `transformation-mapping` surface (an HTTP / network client, or a parser / mapper of a third-party payload — HTML / JSON / XML / CSV originating outside this repo), the adapter's structural assumptions (selectors, field paths, expected response shape) AND the static fixture / golden that tests them MUST BOTH derive from a **captured real sample** of that payload — the capture cited in the stage's `external-interface` / `transformation-mapping` design-prep item, or one captured this run and recorded with its `source` + capture time. The captured sample is a static fixture (no live socket), so a parser test against it stays in source like any unit test — the Real-IO isolation rule below governs *live* calls, not the captured bytes. Do NOT hand-invent the shape and then hand-write a fixture that agrees with it: the passing test then only proves the code matches your assumption, never that the assumption matches reality (self-confirming oracle — the observed failure was a parser whose selectors existed in its synthetic fixture and in zero real pages: hundreds of green units over a fiction, and the whole structure built on the wrong shape). When no real sample is reachable (no network this run, or the brief supplied none), do NOT synthesize a stand-in and present its green tests as correctness: mark the adapter's shape `reality-unverified (no captured sample)` in `Validation evidence`, keep any placeholder fixture explicitly labelled an assumption (never validation evidence), and surface an explicit **user-owned** item in the routing recommendation to confirm against real data. Unlike the DB gate above this does NOT itself block acceptance — live external verification stays a user-owned item per `final-verification`'s External QA advisory policy — but a synthetic external fixture presented as reality-verified is exactly the mock-only external evidence the `final-verification` test-correctness pass is meant to reject.
38
39
  - **Real-IO test isolation (BLOCKING).** A test that exercises a **real** datastore, HTTP endpoint, external service, message queue, or filesystem — a live DB connection / DSN, a real `fetch` / `axios` / `http` request, an actual S3 / queue client, anything the project's normal CI test suite cannot run because that backend is absent — MUST be written under the task's qa scripts directory `<task_root>/qa/scripts/` (`<TASK_QA_PATH>/scripts`; the `qa/` root itself holds only data sidecars — the Tier 3 conformance manifest and `result-*.json`). It MUST NOT be written into the project source test tree — `src/**`, `test/**`, `tests/**`, `**/__test__/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, or anywhere the project's lint/test globs collect. Two reasons: (a) the project's CI / normal suite has no real DB or network, so a real-IO test placed in source silently breaks the pipeline; (b) it is an okstra verification artifact, and the artifact-home rule confines okstra outputs to `.okstra/`. **The dividing line is the IO, not the intent:** a unit test that stubs/spies only *injected collaborators* (mock — no real socket, no real DB handle) is a TDD red-green artifact and stays in source; the moment a test opens a real connection or makes a real network call it belongs in qa. A stage's real-IO requirement check is a Tier 3 conformance script under `<task_root>/qa/scripts/` (declared via the implementation-planning conformance entry) — never smuggle real IO into a `*.spec.*` in source to make it run "as a unit test". The `db-test` real-execution gate above is satisfied by the conformance/db-test path against the replica, NOT by adding a live-DB `*.spec.*` to the project suite. **Author qa specs with the project's own test framework — never hand-roll `describe`/`it`/`expect`.** When the project ships a test runner as a devDependency (jest / vitest / pytest …), the qa spec uses it, invoked with the project config plus a discovery override pointing at the qa scripts dir (jest: `npx jest --config <project jest config> --roots <task_root>/qa/scripts --runInBand <spec-name>`) — the project config keeps module aliases resolving while the default sweep never collects the file; never widen the project's own test config to include qa paths. For TypeScript qa specs also write `<task_root>/qa/scripts/tsconfig.json` (`extends` the project tsconfig, adds the runner's `types` entry, `"include": ["**/*.ts"]`) so editors resolve path aliases and test globals — it is a qa artifact like the rest (untracked). **These qa artifacts stay untracked — never commit them.** `.okstra/**` is gitignored (the artifact-home rule); conformance scripts and their results are *executed* and recorded in the carry sidecar / verifier result, never written into git history. A committed `.okstra/qa` file is a stage-branch defect that leaks okstra internals into the eventual PR (see the `git add` rules below).
39
40
  - re-read the approved plan end-to-end and parse the `## 5.5 Stage Map`. Read the **Stage** injected in the launch prompt (`Stage for this implementation run`): the single stage number this run owns. The runtime already selected and reserved this stage (one run = one stage) — do NOT recompute the start stage from `consumers.jsonl`.
40
41
  - load every `runs/<plan-key>/carry/stage-<i>.json` for `i ∈ depends-on(this stage)` and inject them into the executor's working context as "runtime carry-in". For a `depends-on (none)` stage, no sidecar load — task-brief only.
@@ -55,7 +56,25 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
55
56
 
56
57
  ## Stage execution contract (this run owns one stage)
57
58
 
58
- - **Sidecar evidence writer (BLOCKING).** When this stage's Stage Validation `post` commands all succeed, the Executor MUST emit a JSON object matching the schema in `docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md` §3.2 and the lead MUST persist it to `runs/<impl-task-key>/carry/stage-<N>.json`. The file MUST NOT exist before the run starts (overwrite is refused — see `--force-stage` non-goal). **Enforced:** `validators/validate-run.py` `_validate_stage_carry_sidecar_exists` fails a run that declares `stageSidecarEvidence` without the file on disk. Transcribing the JSON into the report is not the same as writing it: `consumers` treats the carry file as the source of truth for marking the stage `done`, so a missing file leaves the stage permanently incomplete and blocks every dependent stage with a `PrepareError` — while this run reports success.
59
+ - **Sidecar evidence writer (BLOCKING).** When this stage's Stage Validation `post` commands all succeed, the Executor MUST emit a JSON object with **exactly** these fields (spec `docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md` §3.2), as a fenced ```json``` block in the worker result under the heading `### Stage Carry Evidence`, and the lead MUST persist it to `runs/<impl-task-key>/carry/stage-<N>.json`. The schema is inlined here because a CLI executor (codex / antigravity) runs in its own process and never receives the Claude worker-agent definition a carry emitted with an ad-hoc shape (`stage`/`files`/`validation`) is a `contract-violated` gap the lead must rewrite by hand:
60
+
61
+ ```json
62
+ {
63
+ "schemaVersion": 1,
64
+ "sourcePlanPath": "<approved-plan path>",
65
+ "stageNumber": <int>,
66
+ "stageTitle": "<from Stage Map>",
67
+ "completedAt": "<ISO-8601 with tz>",
68
+ "stageCommitRange": { "base": "<sha>", "head": "<sha>" },
69
+ "filesChanged": ["<rel/path>", "..."],
70
+ "newIdentifiers": ["<name>", "..."],
71
+ "stepResults": [{"step": <int>, "status": "done", "commit": "<sha>"}],
72
+ "validationsPassed": ["<label>", "..."],
73
+ "notes": []
74
+ }
75
+ ```
76
+
77
+ The file MUST NOT exist before the run starts (overwrite is refused — see `--force-stage` non-goal). **Enforced:** `validators/validate-run.py` `_validate_stage_carry_sidecar_exists` fails a run that declares `stageSidecarEvidence` without the file on disk. Transcribing the JSON into the report is not the same as writing it: `consumers` treats the carry file as the source of truth for marking the stage `done`, so a missing file leaves the stage permanently incomplete and blocks every dependent stage with a `PrepareError` — while this run reports success.
59
78
  - **Reverse link (BLOCKING).** The runtime already appended a `status:"started"` row for this stage before the run began. On completion, append a `status:"done"` row with `carry_path` populated for this stage number.
60
79
  - **No PR / push in this phase.** This run produces local commits, carry sidecar evidence, verifier results, and the implementation final report only. Push and PR creation belong exclusively to the later `release-handoff` phase after `final-verification` returns `accepted`.
61
80
 
@@ -13,7 +13,7 @@
13
13
  - requirement & acceptance coverage — every must-pass point in the brief's `## Acceptance Criteria` (and the approved plan's requirements) is covered with a cited artifact or raised as an Acceptance Blocker; no silent omissions
14
14
  - delivered artifacts match recorded expected values in `reference-expectations` (config files, deployment manifests, other recorded expected states); when reference-expectations are absent, record it as missing information rather than assuming a match
15
15
  - test & validation suite pass status — independently re-run the read-only two-tier command set (Tier 1 = brief/approved-plan `validation`, Tier 2 = `project.json` `qaCommands`) and confirm each passes on the verified head, citing exact command + exit code
16
- - test correctness — delivered tests actually assert the intended behaviour: no gutted/weakened assertions, no tautological or always-passing tests, no tests exercising only mocks; new behaviour has matching coverage
16
+ - test correctness — delivered tests actually assert the intended behaviour: no gutted/weakened assertions, no tautological or always-passing tests, no tests exercising only mocks; new behaviour has matching coverage. For an `external-interface` / `transformation-mapping` surface specifically, treat a test whose only oracle is a self-authored synthetic fixture (no captured-real-sample provenance) as NOT establishing external correctness — it shows only that the parser agrees with its author's assumed shape, never that the shape matches reality; record that surface's external correctness as a user-owned external advisory gap (a Residual Risk carrying the exact "capture a real sample and confirm" next step, per the Coverage check in the self-review pass below), never as covered — the same non-blocking treatment the External QA advisory policy gives a live external check
17
17
  - DB / IO / SQL real-execution evidence — trigger: the diff touches DB/IO/SQL (ORM / query-builder, `*.repository.*`, model / `migrations/**` / `*.sql`, or changed query strings). Then Validation Evidence MUST cite a real (or faithful-replica) DB execution — the `db-test` command + exit code — not a mock-only suite. Rationale: a mock-only suite cannot observe the SQL actually emitted (observed failure class: `prompts/profiles/_implementation-verifier.md` §"DB / IO / SQL change — real-execution gate"). A DB-touching change whose only evidence is mocked, or for which no `db-test` ran, is an **Acceptance Blocker** (`major`+; per the Verdict vocabulary below, any blocker moves the verdict off `accepted`). This gate stops an unverified DB change from reaching `release-handoff` and being pushed.
18
18
  - **External Tier 3 de-duplication exception.** A DB/IO/SQL surface covered by an in-scope Tier 3 entry whose `requires` include `db`, `http`, or `external` is governed by the External QA outcome policy. Its non-PASS or unavailable result MUST NOT generate a second legacy db-test-not-configured or mock-only blocker solely for that same Tier 3 non-PASS or unavailable result. Tier 1 or Tier 2 failures remain blocking, and DB surfaces without declared external Tier 3 coverage remain blocking.
19
19
  - no new defects introduced — the diff does not break previously-working behaviour and adds no new bug (logic/off-by-one, null/empty handling, resource leaks, broken error paths)
@@ -49,6 +49,7 @@
49
49
  - **Disposition:** a row MUST use `inline-contract` only when the stage already states the kind-specific minimum implementation contract; otherwise it MUST use `prep-item` and reference one or more `designPreparation.items`. `not-applicable` is legal only with a concrete rationale consistent with the stage action. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignSurfaceCoverage` enforces the disposition-specific fields, and `prompts/lead/plan-body-verification.md` `P-Prep-S<stage>-<kind>` verifies semantic sufficiency.
50
50
  - **AI-prepared proposal:** every referenced PREP item MUST record `kind`, `stageRefs`, `need`, evidence-cited `knownFacts`, `openQuestions`, a concrete evidence-backed `aiProposal` (`summary`, `details`, `assumptions`, `evidence`, `confidence`), `humanConfirmation`, explicit `status`, and the safest reversible default available. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignPrepItem` / `$defs.DesignPrepProposal` enforce required fields, `validators/validate-run.py` `_validate_prep_references` enforces the bidirectional stage/kind link, and `prompts/lead/plan-body-verification.md` rejects empty or non-implementable proposals.
51
51
  - **Status choice:** prefer `provisional` with a `workingAssumption`, concrete `guardrails`, `reviewAt`, `ifStillOpen`, and canonical `requestPath`; use `blocked` only for business policy, external authority, a destructive migration decision, or the absence of any safe reversible assumption. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignPrepItem` enforces state-specific fields, `validators/validate-run.py` `_validate_design_prep_states` enforces confirmation/request invariants, and `prompts/lead/plan-body-verification.md` judges whether the disposition is justified. A declared `blocked` status does not by itself fail plan-body verification.
52
+ - **External-reality anchoring (`external-interface` / `transformation-mapping` surfaces):** these two detector kinds are correct only against data whose shape lives *outside this repository* (a third-party response body / external payload format). For such a surface the referenced PREP item's `aiProposal` MUST derive the assumed shape — selectors, field paths, response structure — from a **captured real sample** and cite it in `knownFacts` / `evidence` (source + capture time); a shape invented from internal reasoning and marked `confidence: high` is the disallowed move, because a plan built on an assumed shape yields an implementation whose parser and fixture only ever agree with each other. When the brief supplies no sample and none is capturable at plan time, the item MUST stay `provisional` with a `workingAssumption` that the external shape is unverified against reality, a `guardrails` line forbidding the implementation from presenting a synthetic-fixture green run as reality-verified, and an `ifStillOpen` that routes to user confirmation against real data — it MUST NOT be dispositioned `inline-contract` / settled. **Enforced (semantic):** the surface's presence is machine-checked by `_validate_detector_coverage`; whether its proposal's evidence is genuinely external is judged by the §5.5.9 `P-Prep-S<stage>-<kind>` round (this phase runs it adversarially).
52
53
  - **Planner-only test surface:** add `manual-user-test` only when a test prerequisite changes the implementation interface or acceptance contract; the V1 detector never emits it. **Enforced:** `validators/validate-run.py` `_validate_detector_coverage` rejects detector-produced `manual-user-test`, `schemas/final-report-v1.0.schema.json` permits its planner-authored shape, and `prompts/lead/plan-body-verification.md` verifies the stage-action rationale.
53
54
  - **Trivial task:** when the detector returns no surfaces and no interface/acceptance-changing manual test input exists, `designPreparation` MUST use `mode: no-design-inputs`, an empty `items` array, and a concrete reason tied to the plan. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignPreparation` requires the reason and empty array for that mode; `validators/validate-run.py` `_validate_design_prep_contract` validates the marked V1 payload.
54
55
  - Approval gate (phase-specific addendum to shared authority rule):
@@ -258,23 +258,44 @@ def parse_clarification_rows(report_text: str) -> list[dict]:
258
258
 
259
259
  UNRESOLVED_STATUSES = {"open", "answered"}
260
260
 
261
+ # `Blocks` values that gate the user's `approved: true` flip.
262
+ APPROVAL_BLOCKS = frozenset({"approval"})
263
+ # `Blocks` values that owe the user an answer at all. `next-phase` rows never
264
+ # gate approval, but the next phase cannot start with them open either, so the
265
+ # user-response skill must list them — counting only `approval` made a report
266
+ # whose open items were all `next-phase` look like it had none.
267
+ USER_INPUT_BLOCKS = frozenset({"approval", "next-phase"})
268
+
261
269
 
262
270
  @dataclass(frozen=True)
263
- class ApprovalGateScan:
264
- """Fail-closed read of the §1 approval gate.
271
+ class ClarificationScan:
272
+ """Fail-closed read of §1 for rows blocking on one `Blocks` value set.
265
273
 
266
274
  ``unreadable_reason`` is ``None`` only when the scan is confident: §1
267
275
  parsed cleanly (or is the legitimate table-less placeholder) and
268
276
  ``blockers`` is therefore authoritative. A non-None reason means the
269
- gate must refuse approval — never soft-pass.
277
+ caller must refuse to act — never soft-pass.
270
278
  """
271
279
  blockers: list[ClarificationItem]
272
280
  unreadable_reason: Optional[str]
273
281
 
274
282
 
275
- def scan_approval_gate(report_text: str) -> ApprovalGateScan:
283
+ def scan_approval_gate(report_text: str) -> ClarificationScan:
276
284
  """Scan §1 for unresolved ``Blocks=approval`` rows (``Status`` in
277
285
  ``{open, answered}``), refusing to guess whenever the schema drifted."""
286
+ return scan_clarification_blockers(report_text, APPROVAL_BLOCKS)
287
+
288
+
289
+ def scan_open_user_input(report_text: str) -> ClarificationScan:
290
+ """Scan §1 for every unresolved row that still owes the user an answer
291
+ (``Blocks`` in ``{approval, next-phase}``)."""
292
+ return scan_clarification_blockers(report_text, USER_INPUT_BLOCKS)
293
+
294
+
295
+ def scan_clarification_blockers(
296
+ report_text: str, blocking_values: frozenset[str]
297
+ ) -> ClarificationScan:
298
+ """Shared fail-closed §1 walk for both gates above."""
278
299
  section = _section_1_slice(report_text)
279
300
  if section is None:
280
301
  if _LOOSE_SECTION_1_RE.search(report_text):
@@ -283,32 +304,33 @@ def scan_approval_gate(report_text: str) -> ApprovalGateScan:
283
304
  "the schema heading format (anchor/format drift)"
284
305
  )
285
306
  else:
307
+ expected = "/".join(sorted(blocking_values))
286
308
  reason = (
287
309
  "report has no `## 1. Clarification Items` section — the gate "
288
- "cannot confirm there are no unresolved `Blocks=approval` rows"
310
+ f"cannot confirm there are no unresolved `Blocks={expected}` rows"
289
311
  )
290
- return ApprovalGateScan([], reason)
312
+ return ClarificationScan([], reason)
291
313
  table = _walk_section_1_table(section)
292
314
  if table.items is None:
293
315
  if table.has_pipe_lines:
294
- return ApprovalGateScan([], (
316
+ return ClarificationScan([], (
295
317
  "§1 contains a table but its header row is not the schema "
296
318
  "header (`| ... | Statement | Expected form | User input |`)"
297
319
  ))
298
320
  # Renderer's emptyState placeholder: heading is intact and no table
299
- # was emitted — confidently "no approval-blocking items".
300
- return ApprovalGateScan([], None)
321
+ # was emitted — confidently "no blocking items".
322
+ return ClarificationScan([], None)
301
323
  if table.unparsed_row_count:
302
- return ApprovalGateScan([], (
324
+ return ClarificationScan([], (
303
325
  f"§1 table has {table.unparsed_row_count} row(s) whose metadata "
304
326
  "cell could not be parsed (Blocks/Status markers missing or "
305
327
  "malformed)"
306
328
  ))
307
329
  blockers = [
308
330
  it for it in table.items
309
- if it.blocks == "approval" and it.status in UNRESOLVED_STATUSES
331
+ if it.blocks in blocking_values and it.status in UNRESOLVED_STATUSES
310
332
  ]
311
- return ApprovalGateScan(blockers, None)
333
+ return ClarificationScan(blockers, None)
312
334
 
313
335
 
314
336
  # 느슨한 §1 헤딩 탐지: 엄격한 SECTION_HEADING_PATTERN 이 실패해도 이게 매칭되면
@@ -457,7 +457,12 @@ def validate_working_state(state: Mapping[str, Any]) -> list[str]:
457
457
  else:
458
458
  seen_workers.add(worker_id)
459
459
  if not _nonempty_string(audience) or audience not in _AUDIENCES:
460
- errors.append(f"workers[{index}].audience is unsupported")
460
+ errors.append(
461
+ f"workers[{index}].audience is unsupported: {audience!r}. "
462
+ "Finding-producing workers (implementation verifiers included) "
463
+ "use 'analysis'; only the report author uses 'report-writer'. "
464
+ f"Allowed: {sorted(_AUDIENCES)}."
465
+ )
461
466
  findings = state.get("findings")
462
467
  if not isinstance(findings, list):
463
468
  errors.append("findings must be an array")
@@ -1821,7 +1826,11 @@ def _parse_workers(value: Any) -> list[dict[str, str]]:
1821
1826
  audience = _required_string(worker, "audience", f"workers[{index}]")
1822
1827
  if audience not in _AUDIENCES:
1823
1828
  raise ConvergenceContractError(
1824
- f"workers[{index}] has unsupported audience: {audience}"
1829
+ f"workers[{index}] has unsupported audience: {audience}. "
1830
+ "Convergence audience is a functional role, not a phase label: "
1831
+ "every finding-producing worker uses 'analysis' (an "
1832
+ "implementation run's verifiers included), and only the report "
1833
+ f"author uses 'report-writer'. Allowed: {sorted(_AUDIENCES)}."
1825
1834
  )
1826
1835
  if worker_id in seen:
1827
1836
  raise ConvergenceContractError(f"duplicate workerId: {worker_id}")
@@ -22,6 +22,8 @@ CLAUDE = {
22
22
  "fable-5": ModelSpec("fable-5", "claude-fable-5"),
23
23
  "claude-fable-5": ModelSpec("fable-5", "claude-fable-5"),
24
24
  "opus": ModelSpec("opus", "opus", in_picker=True),
25
+ "opus-5": ModelSpec("opus-5", "claude-opus-5"),
26
+ "claude-opus-5": ModelSpec("opus-5", "claude-opus-5"),
25
27
  "opus-4-8": ModelSpec("opus-4-8", "claude-opus-4-8"),
26
28
  "claude-opus-4-8": ModelSpec("opus-4-8", "claude-opus-4-8"),
27
29
  "opus-4-7": ModelSpec("opus-4-7", "claude-opus-4-7"),
@@ -492,7 +492,7 @@ def _enforce_schema(data: dict) -> None:
492
492
  # 있으나 표시이므로 실행에는 무해하다. 새 버전이 나오면 여기만 갱신한다.
493
493
  _DISPLAY_CONCRETE_CLAUDE = {
494
494
  "fable": "claude-fable-5",
495
- "opus": "claude-opus-4-8",
495
+ "opus": "claude-opus-5",
496
496
  "sonnet": "claude-sonnet-4-6",
497
497
  "haiku": "claude-haiku-4-5",
498
498
  }
@@ -891,6 +891,7 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
891
891
 
892
892
 
893
893
  _INCLUDE_DIRECTIVE = re.compile(r"\{\{INCLUDE:([^}]+?)\}\}")
894
+ _HTML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
894
895
 
895
896
 
896
897
  def _expand_profile_includes(profile_path: Path, _depth: int = 0) -> str:
@@ -901,6 +902,12 @@ def _expand_profile_includes(profile_path: Path, _depth: int = 0) -> str:
901
902
  contents replace the directive line in-place. Missing include targets
902
903
  raise PrepareError so a bad reference fails fast instead of silently
903
904
  leaving a `{{INCLUDE:...}}` token in the rendered profile.
905
+
906
+ The top-level result is stripped of HTML comments: the shared fragments
907
+ carry maintainer-only `<!-- ... -->` notes (edit-here-once guidance, dedup
908
+ rationale) that would otherwise ride into the rendered profile the lead
909
+ reads every run without changing a single instruction. Nested-include
910
+ bodies are left intact and get stripped once by this depth-0 pass.
904
911
  """
905
912
  if _depth > 4:
906
913
  raise PrepareError(
@@ -917,7 +924,11 @@ def _expand_profile_includes(profile_path: Path, _depth: int = 0) -> str:
917
924
  )
918
925
  return _expand_profile_includes(target, _depth + 1).rstrip("\n")
919
926
 
920
- return _INCLUDE_DIRECTIVE.sub(_sub, text)
927
+ expanded = _INCLUDE_DIRECTIVE.sub(_sub, text)
928
+ if _depth == 0:
929
+ expanded = _HTML_COMMENT.sub("", expanded)
930
+ expanded = re.sub(r"\n{3,}", "\n\n", expanded)
931
+ return expanded
921
932
 
922
933
 
923
934
  # ---------------------------------------------------------------------------
@@ -1326,7 +1337,12 @@ def _resolve_pr_template(inp: PrepareInputs) -> tuple[str, str]:
1326
1337
 
1327
1338
  @dataclass
1328
1339
  class _ModelBindings:
1329
- """이 run 의 lead/worker 모델 + critic + executor 바인딩."""
1340
+ """이 run 의 lead/worker 모델 + critic + executor 바인딩.
1341
+
1342
+ `*_execution` 은 dispatch 에 쓰이는 CLI 신원 철자다. 카탈로그 값과 달리
1343
+ provider CLI 조회를 거치므로 실패할 수 있고, 그래서 이 묶음에서 함께
1344
+ 해소한다 — prepare 가 run seq 를 할당하거나 worktree 를 만들기 전에.
1345
+ """
1330
1346
 
1331
1347
  lead: ModelAssignment
1332
1348
  cw: ModelAssignment
@@ -1338,6 +1354,9 @@ class _ModelBindings:
1338
1354
  executor_display_name: str
1339
1355
  executor_worker_agent: str
1340
1356
  executor_model_meta: ModelAssignment
1357
+ codex_worker_execution: str
1358
+ antigravity_worker_execution: str
1359
+ executor_execution: str
1341
1360
 
1342
1361
 
1343
1362
  def recommended_role_models() -> dict[str, str]:
@@ -1413,6 +1432,12 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
1413
1432
  executor_display_name=display_name,
1414
1433
  executor_worker_agent=worker_agent,
1415
1434
  executor_model_meta=model_meta,
1435
+ codex_worker_execution=_worker_execution_value(
1436
+ m["co"], "codex", "worker", workers),
1437
+ antigravity_worker_execution=_worker_execution_value(
1438
+ m["ge"], "antigravity", "worker", workers),
1439
+ executor_execution=_normalized_execution(
1440
+ model_meta, provider=executor_provider, role="executor"),
1416
1441
  )
1417
1442
 
1418
1443
 
@@ -2176,22 +2201,16 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
2176
2201
  "CLAUDE_WORKER_MODEL": cw.display,
2177
2202
  "CLAUDE_WORKER_MODEL_EXECUTION_VALUE": cw.execution,
2178
2203
  "CODEX_WORKER_MODEL": co.display,
2179
- "CODEX_WORKER_MODEL_EXECUTION_VALUE": _worker_execution_value(
2180
- co, "codex", "worker", workers,
2181
- ),
2204
+ "CODEX_WORKER_MODEL_EXECUTION_VALUE": models.codex_worker_execution,
2182
2205
  "ANTIGRAVITY_WORKER_MODEL": ge.display,
2183
- "ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE": _worker_execution_value(
2184
- ge, "antigravity", "worker", workers,
2185
- ),
2206
+ "ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE": models.antigravity_worker_execution,
2186
2207
  "REPORT_WRITER_MODEL": rw.display,
2187
2208
  "REPORT_WRITER_MODEL_EXECUTION_VALUE": rw.execution,
2188
2209
  "EXECUTOR_PROVIDER": executor_provider,
2189
2210
  "EXECUTOR_DISPLAY_NAME": executor_display_name,
2190
2211
  "EXECUTOR_WORKER_AGENT": executor_worker_agent,
2191
2212
  "EXECUTOR_MODEL_DISPLAY": executor_model_meta.display,
2192
- "EXECUTOR_MODEL_EXECUTION_VALUE": _normalized_execution(
2193
- executor_model_meta, provider=executor_provider, role="executor",
2194
- ),
2213
+ "EXECUTOR_MODEL_EXECUTION_VALUE": models.executor_execution,
2195
2214
  "CRITIC_CHOICE": critic_choice,
2196
2215
  "RELATED_TASKS_JSON": related_tasks_json_str,
2197
2216
  "RELATED_TASKS_BULLETS": bullets,