okstra 0.134.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 (29) 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 +8 -3
  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/bin/okstra-antigravity-exec.sh +12 -2
  10. package/runtime/bin/okstra-claude-exec.sh +10 -1
  11. package/runtime/bin/okstra-codex-exec.sh +12 -2
  12. package/runtime/prompts/lead/convergence.md +1 -1
  13. package/runtime/prompts/lead/team-contract.md +2 -2
  14. package/runtime/prompts/profiles/_common-contract.md +0 -1
  15. package/runtime/prompts/profiles/_implementation-executor.md +20 -1
  16. package/runtime/prompts/profiles/final-verification.md +1 -1
  17. package/runtime/prompts/profiles/implementation-planning.md +1 -0
  18. package/runtime/python/okstra_ctl/convergence_engine.py +11 -2
  19. package/runtime/python/okstra_ctl/models.py +2 -0
  20. package/runtime/python/okstra_ctl/render_final_report.py +1 -1
  21. package/runtime/python/okstra_ctl/run.py +12 -1
  22. package/runtime/python/okstra_ctl/worker_artifact_paths.py +5 -1
  23. package/runtime/python/okstra_token_usage/collect.py +25 -15
  24. package/runtime/python/okstra_token_usage/pricing.py +4 -1
  25. package/runtime/skills/okstra-pr-gen/SKILL.md +24 -0
  26. package/runtime/skills/okstra-user-response/SKILL.md +65 -17
  27. package/runtime/validators/forbidden_actions.py +1 -1
  28. package/runtime/validators/validate-run.py +11 -2
  29. package/runtime/validators/validate_session_conformance.py +1 -1
@@ -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
 
@@ -35,8 +37,11 @@ okstra preflight --runtime claude-code --json
35
37
  ## Flow
36
38
 
37
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).
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}`.
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.134.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.134.0",
3
- "builtAt": "2026-07-23T05:31:24.504Z",
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.
@@ -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):
@@ -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
  # ---------------------------------------------------------------------------
@@ -17,7 +17,11 @@ def audit_sidecar_rel(worker_result_rel: str) -> str:
17
17
  prefix, separator, suffix = basename.partition("-worker-")
18
18
  if not separator:
19
19
  raise WorkerArtifactPathError(
20
- f"worker result path has no canonical -worker- token: {worker_result_rel}"
20
+ "worker result path has no canonical -worker- token: "
21
+ f"{worker_result_rel}. The audit sidecar is derived from the "
22
+ "canonical worker-result name `<role>-worker-<task-type>-<seq>.md` "
23
+ "(e.g. `codex-worker-implementation-001.md`); name the result that "
24
+ "way — a role label like `codex-verifier-...` drops the -worker- token."
21
25
  )
22
26
  audit_filename = f"{prefix}-worker-audit-{suffix}"
23
27
  return f"{directory}{slash}{audit_filename}" if slash else audit_filename
@@ -227,7 +227,9 @@ def _previous_run_end(run_dir: Path, suffix: str) -> str | None:
227
227
  return _run_end_estimate(run_dir, prev) or _run_manifest_created_at(run_dir, prev)
228
228
 
229
229
 
230
- def resolve_run_window(team_state_path: Path, state: dict) -> tuple[str | None, str | None]:
230
+ def resolve_run_window(
231
+ team_state_path: Path, state: dict, *, relax_start: bool = True
232
+ ) -> tuple[str | None, str | None]:
231
233
  """이 run 의 [시작, 종료] ISO 윈도우.
232
234
 
233
235
  in-session lead 는 자기 run 을 사용자의 *세션 전체* jsonl 에 기록하므로,
@@ -238,26 +240,34 @@ def resolve_run_window(team_state_path: Path, state: dict) -> tuple[str | None,
238
240
  status mtime → 현재 시각(아직 진행 중) 순으로 해소한다. 접미사를 못 뽑으면
239
241
  (None, None) — 윈도우 없이 전체를 세는 기존 동작으로 안전 폴백.
240
242
 
241
- 시작 완화: run-manifest createdAt 이 이 run 의 실제 lead/worker 세션보다
242
- 늦게 찍히면(관측: dev-9902 — createdAt 11:43Z 가 05:34Z 시작 세션보다 늦음)
243
- since 가 앞선 세션을 잘라낸다. Task 3 이 기록한 `leadSessionIds[]` 세션의
244
- 첫 ts 최소값이 createdAt 보다 앞서면 그 값으로 since 를 앞당긴다.
245
- `leadSessionIds` 부재(legacy)면 createdAt 그대로 — 기존 동작 불변.
246
-
247
- 완화 하한: 직전 run 의 종료 시각 이전으로는 내려가지 않는다
243
+ 시작 완화(`relax_start`, 기본 True — 토큰 수집용): run-manifest createdAt 이
244
+ 이 run 의 실제 lead/worker 세션보다 늦게 찍히면(관측: dev-9902 — createdAt
245
+ 11:43Z 가 05:34Z 시작 세션보다 늦음) since 가 앞선 세션을 잘라낸다. Task 3 이
246
+ 기록한 `leadSessionIds[]` 세션의 첫 ts 최소값이 createdAt 보다 앞서면 그 값으로
247
+ since 를 앞당긴다. `leadSessionIds` 부재(legacy)면 createdAt 그대로 — 기존
248
+ 동작 불변. 완화 하한: 직전 run 의 종료 시각 이전으로는 내려가지 않는다
248
249
  (`_previous_run_end`). 세션 jsonl 은 run 마다 새로 생기지 않으므로 하한이
249
- 없으면 같은 세션의 이전 run 이 이 윈도우에 통째로 들어온다."""
250
+ 없으면 같은 세션의 이전 run 이 이 윈도우에 통째로 들어온다.
251
+
252
+ `relax_start=False` (세션 스코프 검증기 전용 — session-conformance /
253
+ forbidden-actions): 완화를 건너뛰고 since 를 createdAt 에 고정한다. createdAt
254
+ 은 prep 시각이라 이 run 의 lead·worker 활동보다 반드시 앞서므로 건전한 하한이고,
255
+ 재사용 세션에 섞인 같은 세션의 이전 run·다른 task 활동을 창에서 배제한다
256
+ (dev-10172 D-3: 이전 planning run 의 phase-6/7 앵커, 다른 task 의 git push 를
257
+ 이 run 의 위반으로 오탐하던 근본 원인). 완화는 토큰을 놓치지 않으려고 세션
258
+ birth 까지 내려가지만, 검증기에는 그 관대함이 곧 오탐이다."""
250
259
  suffix = run_artifact_suffix(team_state_path)
251
260
  if not suffix:
252
261
  return None, None
253
262
  run_dir = team_state_path.parent.parent
254
263
  since = _run_manifest_created_at(run_dir, suffix)
255
- earliest = _earliest_lead_session_ts(state, team_state_path)
256
- if earliest and (since is None or earliest < since):
257
- since = earliest
258
- floor = _previous_run_end(run_dir, suffix)
259
- if floor and since and since < floor:
260
- since = floor
264
+ if relax_start:
265
+ earliest = _earliest_lead_session_ts(state, team_state_path)
266
+ if earliest and (since is None or earliest < since):
267
+ since = earliest
268
+ floor = _previous_run_end(run_dir, suffix)
269
+ if floor and since and since < floor:
270
+ since = floor
261
271
  until = state.get("runEndedAt") or _run_end_estimate(run_dir, suffix) or utc_now()
262
272
  return since, until
263
273
 
@@ -4,7 +4,7 @@ Pricing is matched by substring against the model id recorded in the session
4
4
  transcript, so keys must reflect the *actual* model id form emitted by each
5
5
  provider:
6
6
 
7
- * Anthropic — `claude-fable-5*`, `claude-opus-4-*`, `claude-sonnet-4-*`,
7
+ * Anthropic — `claude-fable-5*`, `claude-opus-5*`, `claude-opus-4-*`, `claude-sonnet-4-*`,
8
8
  `claude-haiku-4-5-*`, `claude-3-5-sonnet-*`, `claude-3-5-haiku-*`,
9
9
  `claude-3-opus-*`, `claude-3-haiku-*`.
10
10
  * OpenAI / Codex — `gpt-5*`, `gpt-4o*`, `gpt-4*`.
@@ -52,6 +52,9 @@ CLAUDE_PRICING = {
52
52
  # Claude Fable 5 (tier above Opus).
53
53
  "fable-5": (10.0, 12.5, 1.0, 50.0), # Fable 5 (cache prices derived from ratios)
54
54
 
55
+ # Claude Opus 5.
56
+ "opus-5": (5.0, 6.25, 0.50, 25.0), # Opus 5 (cache prices derived from ratios)
57
+
55
58
  # Claude 4 point releases (explicit so future divergence is easy to see).
56
59
  "opus-4-8": (5.0, 6.25, 0.50, 25.0), # Opus 4.8 (cache prices derived from ratios)
57
60
  "opus-4-7": (5.0, 6.25, 0.50, 25.0), # Opus 4.7 (cache prices derived from ratios)
@@ -96,6 +96,30 @@ diff and commits: describe only what actually changed. Mark checklist boxes
96
96
  docs box). If `commits`/`diffStat` are empty, tell the user there is nothing to
97
97
  describe and stop. **Never** append AI trailers/footers.
98
98
 
99
+ ### A3b. Identifier allowlist for the body
100
+
101
+ Reviewers have never seen okstra, so an okstra-internal id reads as a nonsense
102
+ token. Identifiers in the PR title and body come from this list and nothing else:
103
+
104
+ | allowed | example |
105
+ | --- | --- |
106
+ | repo-relative source path, optionally with a line | `src/commands/pr/pr.mjs:42` |
107
+ | symbol name that exists in the diff | `resolveTemplate()` |
108
+ | branch name, commit subject, commit SHA | `feature/dev-9436`, `a1b2c3d` |
109
+ | issue-tracker ticket id the reviewer can open | `dev-9436` |
110
+
111
+ okstra's own artifact identifiers are out of the allowlist — drop them:
112
+
113
+ - report item ids — `F-001`, `C-001`, `R-001`, `D-0001`, `PREP-001`
114
+ - run artifact names and their `<task-type>-<seq>` suffixes —
115
+ `final-report-implementation-3.md`, `run-manifest-implementation-3.json`
116
+ - phase / stage / worker labels — `final-verification`, `stage-2`, `codex-worker`
117
+ - any path under `.okstra/` — reports, decisions, glossary, user-response sidecars
118
+
119
+ When the only source for a claim is such an artifact, restate its substance in
120
+ the reviewer's terms — what changed in the code, and why — instead of citing the
121
+ id.
122
+
99
123
  ### A4. Output and offer to create the PR
100
124
 
101
125
  Print the filled PR body to chat as a single fenced markdown block. Then ask the
@@ -1,14 +1,16 @@
1
1
  ---
2
2
  name: okstra-user-response
3
3
  description: >-
4
- Use this to answer an okstra task's open clarification questions in-session, without hand-editing any file. The tell is a request to respond to an okstra run's clarification items or its approval gate — "answer okstra", "I'll answer the questions", "clarification response", "user response", "approve and move on". This skill lists tasks whose latest report still has open clarification blockers, presents each open C-id (statement + recommended answer + alternatives), collects the user's own verbatim answers (or a reframe/hold), echoes the full sidecar back for an explicit "confirmed" acknowledgement, optionally records approval, and writes the `user-responses/` sidecar via `okstra user-response write`. NOT for starting a run (okstra-run), inspecting a finished task (okstra-inspect), or generating a brief (okstra-brief-gen). The skill never picks an answer for the user — it only relays recommendations and transcribes what the user decides.
4
+ Use this to answer an okstra task's open clarification questions in-session, without hand-editing any file. The tell is a request to respond to an okstra run's clarification items or its approval gate — "answer okstra", "I'll answer the questions", "clarification response", "user response", "approve and move on". This skill lists tasks whose latest report still has open clarification blockers, then walks the open C-ids one at a time each rewritten as a self-contained question with background, three concrete answers (recommendation first) plus "Enter directly" — echoes the collected answers back for an explicit "confirmed" acknowledgement, optionally records approval, and writes the `user-responses/` sidecar via `okstra user-response write`. NOT for starting a run (okstra-run), inspecting a finished task (okstra-inspect), or generating a brief (okstra-brief-gen). The skill never picks an answer — it builds the option board and transcribes what the user decides.
5
5
  ---
6
6
 
7
7
  # OKSTRA User Response
8
8
 
9
9
  Single entry point for answering the clarification questions an okstra run left behind (the open `C-*` rows under the final report's `## 1. Clarification Items`) **in-session**, and recording those answers as a `runs/<type>/user-responses/` sidecar. The next `/okstra-run` auto-attaches this sidecar via `--clarification-response`.
10
10
 
11
- **Core principle — the skill never picks an answer for the user.** This skill only *presents* recommendations and alternatives; every `value` is the user's own verbatim input. It never calls `write` until the user has explicitly confirmed.
11
+ **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 never calls `write` until the user has explicitly confirmed.
12
+
13
+ **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. Ask item 1, transcribe the answer, then ask item 2.
12
14
 
13
15
  | Sub-command | What it does |
14
16
  |---|---|
@@ -63,7 +65,7 @@ Returns a JSON array (latest report mtime first); each entry:
63
65
 
64
66
  Carry the chosen entry's `reportPath` and `taskKey` forward.
65
67
 
66
- ## Step 2: Show open rows
68
+ ## Step 2: Fetch the open rows (data only — ask nothing yet)
67
69
 
68
70
  ```bash
69
71
  okstra user-response show --report <reportPath>
@@ -71,33 +73,79 @@ okstra user-response show --report <reportPath>
71
73
 
72
74
  Returns `{reportPath, rows: [{id, kind, blocks, status, statement, recommended, alternatives, contextRefs, resolvedRefs}]}`. `resolvedRefs` is `[{ref, definition}]` — the CLI has already looked up what each internal token (`RB-002`, `FU-001`, `§4.7`, …) means in the report body; `definition` is `null` only when the report text alone could not resolve it (e.g. a `path:line` pointer).
73
75
 
74
- **Do NOT paste the raw `statement` as the question.** A `statement` like `Rewrite RB-002 rollback (see §4.7)` is meaningless to the user on its own that is the whole reason this skill exists. For each open `C-*` row, you MUST present:
76
+ This call is a **data fetch, not a presentation step**. Do not print `rows` at the user, and do not paste a raw `statement` as a question a `statement` like `Rewrite RB-002 rollback (see §4.7)` is meaningless on its own, which is the whole reason this skill exists. Announce only the count and the plan:
77
+
78
+ > `<N>` open items — I'll go through them one at a time.
79
+
80
+ Step 3 then turns exactly one row at a time into a question.
81
+
82
+ ## Step 3: Ask one item at a time (loop)
83
+
84
+ Walk the open rows in report order. **One item per `AskUserQuestion` call** — never batch two items into one call, and never pre-announce the items still to come. Ask, transcribe, move on. Head each item with its position and what it is holding up:
85
+
86
+ > **[2/5] C-014** — blocks: approval gate
87
+
88
+ ### 3a. Background first (plain text, above the picker)
89
+
90
+ `AskUserQuestion` carries one sentence of question, so the background goes in the message text right before the call. Write these three blocks, **3–6 lines total** — enough that someone who never read the report can answer:
91
+
92
+ 1. **Situation** — what the run was doing when it stopped at this item, in the user's own domain terms.
93
+ 2. **What is undecided** — the actual fork, with every internal token expanded inline from `resolvedRefs[].definition`; the user must never need to know what `RB-002` is to answer. Say what is stuck right now in plain words: `blocks: approval` → the approval gate stays shut and `implementation` cannot start; `blocks: next-phase` → the next phase cannot begin.
94
+ 3. **What changes with your answer** — what each direction actually causes downstream.
95
+
96
+ Where the background comes from — in this order:
97
+
98
+ - `resolvedRefs[].definition`, which is already resolved and needs no file read;
99
+ - when a `definition` is `null`, or the three blocks would otherwise be empty, **Read** the report `§`/`path:line` that this item's `contextRefs[]` points at and take it from there;
100
+ - **never invent it.** If the report genuinely does not say why, write `The report states nothing beyond the statement itself.` and go straight to the question. Fabricated background corrupts the answer it collects.
101
+
102
+ Close the background with the raw source on one line, so the mapping back to the report stays visible:
103
+
104
+ > Source: `C-014` — "<raw statement>"
105
+
106
+ ### 3b. The picker — three answers plus `Enter directly`
107
+
108
+ One `AskUserQuestion` (single-select), with exactly four options in this order:
109
+
110
+ 1. **The recommendation** — the answer from `recommended` restated in plain language, label suffixed `(Recommended)`; its description carries the rationale (the part of the `recommended` cell after `—`).
111
+ 2. **Alternative** — `alternatives[0]`.
112
+ 3. **Alternative** — `alternatives[1]`.
113
+ 4. **`Enter directly`** — always last, always present.
114
+
115
+ Slots 2–3 fill from `alternatives[]` first. When `alternatives[]` is short, fill each remaining slot with the first of these that applies:
116
+
117
+ - **a concrete candidate the report itself names** — another option row, an approach §2 rejected, a value already in use elsewhere. Its description must open with `Not an okstra proposal — from <where>` and cite that source.
118
+ - **`Reframe this question`** — the item is held rather than answered, and the next run re-asks it.
119
+
120
+ That last entry is what guarantees the picker always reaches three answers plus `Enter directly`. Never pad a slot with an option no source supports, and never mark anything but `recommended` as recommended.
121
+
122
+ Keep each `label` to the answer itself (1–5 words); the description carries the consequence.
123
+
124
+ ### 3c. Transcribe the decision, then move on
75
125
 
76
- - **A rewritten, self-contained question** (lead with this): rewrite `statement` in plain language with **every internal token expanded inline** from its `resolvedRefs` `definition` — the user must never have to know what `RB-002` is to answer. If a token's `definition` is `null`, resolve it yourself **using Read** on the report `§`/`path:line` before asking; only if it is genuinely unresolvable do you say so plainly.
77
- - **`recommended`** — the answer okstra proposed (+ rationale), also stated in plain language. It is only a recommendation.
78
- - **`alternatives[]`** — list them if present, each in plain language.
79
- - **Raw source** (secondary, for traceability): the raw `id` + `statement` so the mapping back to the report stays visible.
126
+ Record one entry `{id, kind, value, rationale?, disposition}`, `kind` copied from the row `show` returned:
80
127
 
81
- ## Step 3: Per-item collect the user's decision (4 branches)
128
+ | The user picks | `value` | `disposition` |
129
+ |---|---|---|
130
+ | Option 1–3 | that option's full answer text, not its short label | `answer` |
131
+ | `Enter directly` → their own answer | the user's utterance verbatim (rationale into `rationale`) | `answer` |
132
+ | `Reframe this question`, or free text asking for it to be re-asked | what the user wants re-asked, verbatim (empty → the raw statement) | `reframe` |
82
133
 
83
- For each open item, the user picks one of four dispositions. You relay and transcribe; you never choose:
134
+ A reframe is not an answer, so it does not satisfy the approval gate.
84
135
 
85
- 1. **Answer** — the user accepts the recommendation or gives their own answer. `disposition: "answer"`, `value` = the user's utterance verbatim.
86
- 2. **Ask-to-explain (fallback)** — Step 2 already expanded every internal token, so the question should already be self-contained. If the user still asks "what does this mean?" or a `resolvedRefs` `definition` was `null`, open the report `§`/`path:line` that this item's `contextRefs[]` points at **using Read** and explain it in plain language. Only explain — **do not pick the answer for the user**; after explaining, hand the decision back to the user.
87
- 3. **Free-form** — the user gives a narrative answer that matches neither a recommendation nor an alternative. `value` = that narrative verbatim, with the rationale in `rationale` if needed. `disposition: "answer"`.
88
- 4. **Hold + reframe** — if the user asks to "re-frame this question itself", record it as `disposition: "reframe"`. Put what the user wants re-asked into `value` verbatim. A reframe is not an answer, so it does not satisfy the approval gate — the next run re-frames that question.
136
+ If the user replies with a question instead of an answer ("what does this mean?"), record nothing: **Read** the `§`/`path:line` from `contextRefs[]`, explain it in plain language, and re-ask the same item with the same four options. Explain only — **do not resolve it for them**; the decision goes back to the user.
89
137
 
90
- Each answer item's JSON shape: `{id, kind, value, rationale?, disposition}`. `kind` uses the same `kind` that `show` gave for that row.
138
+ Echo one line per finished item (`[2/5] C-014 → answer: 60s`), then ask the next one. Do not summarize the whole set until Step 4.
91
139
 
92
140
  ## Step 4: Echo the full sidecar back → explicit "confirmed" gate
93
141
 
94
- Once every open item is handled, **before** calling `write`, **echo back** everything collected (each `id`, `disposition`, `value`, `rationale` if present, and the approval decision from Step 5 below) to the user as-is. Then ask for explicit confirmation:
142
+ Once the loop has covered every open item, **before** calling `write`, **echo back** everything collected (each `id`, `disposition`, `value`, `rationale` if present, and the approval decision from Step 5 below) to the user as-is. Then ask for explicit confirmation:
95
143
 
96
144
  > Record it as shown above? (Reply `confirmed` to write the sidecar.)
97
145
 
98
146
  - **Never call `write`** until the user says `confirmed` (or gives clear approval).
99
147
  - If the user changes any item, show the echo-back again with the changed value and re-confirm.
100
- - Every `value` must be the user's utterance verbatim — this gate guarantees that.
148
+ - Every `value` must be what the user picked or typed, never a wording you settled on for them — this gate guarantees that.
101
149
 
102
150
  ## Step 5: (Optional) Record approval
103
151
 
@@ -166,7 +166,7 @@ def scan_forbidden_actions(
166
166
  resolve_team_needles_with_source,
167
167
  )
168
168
 
169
- since, until = resolve_run_window(team_state_path, team_state)
169
+ since, until = resolve_run_window(team_state_path, team_state, relax_start=False)
170
170
  lead_sid = (team_state.get("lead") or {}).get("sessionId") or ""
171
171
  team_needles, _needle_source = resolve_team_needles_with_source(
172
172
  team_state, project_root, since, until, projects_root=claude_projects_dir
@@ -3497,8 +3497,17 @@ def _validate_stage_carry_sidecar_exists(
3497
3497
  stage = evidence.get("stageNumber")
3498
3498
  if not isinstance(stage, int):
3499
3499
  return
3500
- # `report_path` is `runs/implementation/reports/final-report-...md`.
3501
- carry_path = report_path.parent.parent / "carry" / f"stage-{stage}.json"
3500
+ # Carry sidecars are stage-SHARED: they live flat at
3501
+ # `runs/implementation/carry/stage-<N>.json`, NOT under the stage-<N>/ run
3502
+ # dir, because the next stage's carry-in and `consumers.backfill_done_from_carry`
3503
+ # glob them without knowing the producing run's layout (storage-model.md
3504
+ # §"representative files"). `report_path` is
3505
+ # `runs/implementation/stage-<N>/reports/final-report-...md`, so drop the
3506
+ # stage-<N>/ segment to reach the flat carry dir that `consumers` reads —
3507
+ # resolving it under the stage run dir forced the lead to write the file twice.
3508
+ run_dir = report_path.parent.parent
3509
+ carry_root = run_dir.parent if _stage_isolated_name(run_dir) else run_dir
3510
+ carry_path = carry_root / "carry" / f"stage-{stage}.json"
3502
3511
  if not carry_path.exists():
3503
3512
  failures.append(
3504
3513
  f"implementation run declares stage-{stage} sidecar evidence but "
@@ -194,7 +194,7 @@ def _collect_lead_evidence(
194
194
  )
195
195
  from okstra_token_usage.paths import claude_project_dir
196
196
 
197
- since, until = resolve_run_window(team_state_path, team_state)
197
+ since, until = resolve_run_window(team_state_path, team_state, relax_start=False)
198
198
  lead_sid = (team_state.get("lead") or {}).get("sessionId") or ""
199
199
  team_needles, _needle_source = resolve_team_needles_with_source(
200
200
  team_state, project_root, since, until, projects_root=projects_root