okstra 0.134.0 → 0.136.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 (39) 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/lib/okstra/interactive.sh +24 -67
  10. package/runtime/bin/okstra-antigravity-exec.sh +22 -10
  11. package/runtime/bin/okstra-claude-exec.sh +20 -9
  12. package/runtime/bin/okstra-codex-exec.sh +22 -10
  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/backfill.py +2 -1
  20. package/runtime/python/okstra_ctl/context_cost.py +3 -2
  21. package/runtime/python/okstra_ctl/convergence_engine.py +11 -2
  22. package/runtime/python/okstra_ctl/error_log_core.py +3 -1
  23. package/runtime/python/okstra_ctl/handoff.py +2 -1
  24. package/runtime/python/okstra_ctl/implementation_outcome.py +6 -5
  25. package/runtime/python/okstra_ctl/models.py +2 -0
  26. package/runtime/python/okstra_ctl/path_hints.py +3 -1
  27. package/runtime/python/okstra_ctl/paths.py +261 -2
  28. package/runtime/python/okstra_ctl/plan_run_root.py +4 -4
  29. package/runtime/python/okstra_ctl/render_final_report.py +1 -1
  30. package/runtime/python/okstra_ctl/run.py +12 -1
  31. package/runtime/python/okstra_ctl/wizard.py +5 -3
  32. package/runtime/python/okstra_ctl/worker_artifact_paths.py +5 -1
  33. package/runtime/python/okstra_token_usage/collect.py +25 -15
  34. package/runtime/python/okstra_token_usage/pricing.py +4 -1
  35. package/runtime/skills/okstra-pr-gen/SKILL.md +24 -0
  36. package/runtime/skills/okstra-user-response/SKILL.md +65 -17
  37. package/runtime/validators/forbidden_actions.py +1 -1
  38. package/runtime/validators/validate-run.py +20 -22
  39. 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.136.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.136.0",
3
+ "builtAt": "2026-07-25T16:28:08.933Z",
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.
@@ -11,19 +11,6 @@ is_interactive_session() {
11
11
  [[ -t 0 && -t 1 ]]
12
12
  }
13
13
 
14
- file_mtime() {
15
- # Epoch mtime; portable across GNU (stat -c) and macOS/BSD (stat -f).
16
- # Validate that the output is a bare integer before accepting it: GNU
17
- # `stat -f %m` (a BSD-ism) does NOT fail cleanly — it succeeds with
18
- # non-numeric filesystem output, so a plain `a || b` would return that
19
- # garbage and later break `-gt`/`-eq` arithmetic under `set -u`.
20
- local m=""
21
- m="$(stat -c %Y "$1" 2>/dev/null)"
22
- [[ "$m" =~ ^[0-9]+$ ]] || m="$(stat -f %m "$1" 2>/dev/null)"
23
- [[ "$m" =~ ^[0-9]+$ ]] || m="0"
24
- printf '%s' "$m"
25
- }
26
-
27
14
  split_task_key() {
28
15
  local raw_key=""
29
16
  raw_key="$(trim_whitespace "${TASK_KEY_INPUT-}")"
@@ -293,74 +280,44 @@ find_latest_final_report() {
293
280
  # Args: project_root task_group task_id task_type_filter(optional)
294
281
  # Echoes: <abs path to latest final-report-*.md>\t<task-type>
295
282
  # Returns: 1 if not found.
283
+ #
284
+ # 선택 규칙(mtime 최신, 동률이면 basename)과 산출물 위치 지식은
285
+ # okstra_ctl.paths.RunRef 가 정본이다. 과거 이 함수는 그 규칙을 bash 로
286
+ # 재구현해 python 쪽과 손으로 동기화해야 했다.
296
287
  local project_root="$1"
297
288
  local task_group="$2"
298
289
  local task_id="$3"
299
290
  local task_type_filter="$4"
300
291
 
292
+ # task-key 단축키 해소와 오타 진단(실제 task-key 목록 출력)은 이 resolver 가
293
+ # 소유한다. 최신 보고서 선택만 python 정본에 위임한다.
301
294
  local task_root=""
302
295
  task_root="$(resolve_task_root_for_shortcut "$project_root" "$PROJECT_ID" "$task_group" "$task_id")" || {
303
296
  printf 'resume-clarification: unable to resolve task root for the given task-key. See diagnostics above.\n' >&2
304
297
  return 1
305
298
  }
306
299
 
307
- local runs_root="$task_root/runs"
308
- if [[ ! -d "$runs_root" ]]; then
309
- printf 'resume-clarification: no runs/ directory under %s\n' "$task_root" >&2
310
- return 1
311
- fi
312
-
313
- local search_dirs=()
314
- if [[ -n "$task_type_filter" ]]; then
315
- search_dirs+=("$runs_root/$task_type_filter/reports")
316
- else
317
- [[ -d "$runs_root/error-analysis/reports" ]] && search_dirs+=("$runs_root/error-analysis/reports")
318
- [[ -d "$runs_root/requirements-discovery/reports" ]] && search_dirs+=("$runs_root/requirements-discovery/reports")
319
- fi
320
-
321
- if (( ${#search_dirs[@]} == 0 )); then
322
- printf 'resume-clarification: no requirements-discovery or error-analysis reports/ directory under %s/runs/. Run that phase first before invoking --resume-clarification.\n' "$task_root" >&2
323
- return 1
324
- fi
325
-
326
- # Pick the chronologically newest report (mtime first), matching the Python
327
- # resume path (okstra_ctl.wizard's mtime-max). A pure basename sort conflates
328
- # phases: across error-analysis/ and requirements-discovery/ dirs the filename
329
- # embeds the task-type, so a lexicographic max always favors one phase
330
- # regardless of which run is actually newer. Ties (same-second mtime within a
331
- # phase dir) fall back to the greater basename so the highest seq/timestamp
332
- # wins deterministically.
333
- local best_path=""
334
- local best_mtime=-1
335
- local best_basename=""
336
- local candidate=""
337
- local candidate_mtime=""
338
- local candidate_base=""
339
- local d=""
340
- for d in "${search_dirs[@]}"; do
341
- [[ -d "$d" ]] || continue
342
- while IFS= read -r candidate; do
343
- candidate_mtime="$(file_mtime "$candidate")"
344
- candidate_base="$(basename "$candidate")"
345
- if [[ -z "$best_path" \
346
- || "$candidate_mtime" -gt "$best_mtime" \
347
- || ( "$candidate_mtime" -eq "$best_mtime" && "$candidate_base" > "$best_basename" ) ]]; then
348
- best_path="$candidate"
349
- best_mtime="$candidate_mtime"
350
- best_basename="$candidate_base"
351
- fi
352
- done < <(find "$d" -maxdepth 1 -type f -name 'final-report-*.md' 2>/dev/null)
353
- done
300
+ local found=""
301
+ found="$(PYTHONPATH="$WORKSPACE_ROOT/scripts:${PYTHONPATH-}" python3 - \
302
+ "$task_root" "$task_type_filter" <<'PY'
303
+ import sys
304
+ from okstra_ctl.paths import RunRef
305
+
306
+ task_root, task_type_filter = sys.argv[1:3]
307
+ types = ((task_type_filter,) if task_type_filter
308
+ else ("error-analysis", "requirements-discovery"))
309
+ ref = RunRef.latest_under(task_root, types)
310
+ if ref is not None:
311
+ sys.stdout.write(f"{ref.report}\t{ref.task_type}")
312
+ PY
313
+ )" || return 1
354
314
 
355
- if [[ -z "$best_path" ]]; then
356
- printf 'resume-clarification: no final-report-*.md found for task %s:%s under %s\n' \
357
- "$task_group" "$task_id" "$runs_root" >&2
315
+ if [[ -z "$found" ]]; then
316
+ printf 'resume-clarification: no final-report-*.md found for task %s:%s under %s/runs\n' \
317
+ "$task_group" "$task_id" "$task_root" >&2
358
318
  return 1
359
319
  fi
360
-
361
- local resolved_type=""
362
- resolved_type="$(basename "$(dirname "$(dirname "$best_path")")")"
363
- printf '%s\t%s\n' "$best_path" "$resolved_type"
320
+ printf '%s\n' "$found"
364
321
  }
365
322
 
366
323
  run_resume_clarification() {
@@ -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
@@ -280,15 +290,17 @@ if (( idle_timeout_secs > 0 )); then
280
290
  while kill -0 "$agy_pid" 2>/dev/null; do
281
291
  sleep "$poll_interval"
282
292
  kill -0 "$agy_pid" 2>/dev/null || exit 0
283
- # GNU `stat -c` 우선 + 숫자 검증. BSD-ism `stat -f %m` 은 Linux 에서 깨끗이
284
- # 실패하지 않고 비숫자 파일시스템 출력으로 성공해 아래 산술을 set -u 에서
285
- # 깨뜨린다file_mtime(lib/okstra/interactive.sh) 동일 가드.
293
+ # Portable mtime probe: GNU (stat -c %Y) first, macOS/BSD (stat -f %m) as
294
+ # the fallback, 0 as the floor. Validate each result as a bare integer
295
+ # before accepting it on Linux the BSD-ism `stat -f %m` does NOT fail
296
+ # cleanly; it succeeds and prints non-numeric filesystem output, which
297
+ # would break the arithmetic below under `set -u`.
286
298
  #
287
- # `|| true` 필수다. macOS 에는 GNU `stat -c` 없어 exit 1 이고,
288
- # `2>/dev/null` stderr 막지 종료코드는 막지 못한다. 워치독은 명시적
289
- # `( ) &` 서브셸이라 위쪽 `set -e` 상속하므로, 실패한 할당 하나가 첫 폴링에서
290
- # 워치독을 통째로 죽인다 조용히, 로그 없이. file_mtime 같은 코드로도
291
- # 멀쩡한 것은 항상 `$( )` 안에서 호출돼 errexit 상속하지 않기 때문이다.
299
+ # `|| true` is mandatory. macOS has no GNU `stat -c`, so that call exits 1,
300
+ # and `2>/dev/null` suppresses stderr but not the exit code. This watchdog
301
+ # is an explicit `( ) &` subshell, so it inherits whatever errexit setting
302
+ # is in force; under `set -e` a single failed assignment kills the whole
303
+ # watchdog on the first poll silently, without a log line.
292
304
  last_mtime="$(stat -c %Y "$log_path" 2>/dev/null || true)"
293
305
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime="$(stat -f %m "$log_path" 2>/dev/null || true)"
294
306
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime=0
@@ -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
@@ -123,15 +132,17 @@ if (( idle_timeout_secs > 0 )); then
123
132
  while kill -0 "$claude_pid" 2>/dev/null; do
124
133
  sleep "$poll_interval"
125
134
  kill -0 "$claude_pid" 2>/dev/null || exit 0
126
- # GNU `stat -c` 우선 + 숫자 검증. BSD-ism `stat -f %m` 은 Linux 에서 깨끗이
127
- # 실패하지 않고 비숫자 파일시스템 출력으로 성공해 아래 산술을 set -u 에서
128
- # 깨뜨린다file_mtime(lib/okstra/interactive.sh) 동일 가드.
135
+ # Portable mtime probe: GNU (stat -c %Y) first, macOS/BSD (stat -f %m) as
136
+ # the fallback, 0 as the floor. Validate each result as a bare integer
137
+ # before accepting it on Linux the BSD-ism `stat -f %m` does NOT fail
138
+ # cleanly; it succeeds and prints non-numeric filesystem output, which
139
+ # would break the arithmetic below under `set -u`.
129
140
  #
130
- # `|| true` 필수다. macOS 에는 GNU `stat -c` 없어 exit 1 이고,
131
- # `2>/dev/null` stderr 막지 종료코드는 막지 못한다. 워치독은 명시적
132
- # `( ) &` 서브셸이라 위쪽 `set -e` 상속하므로, 실패한 할당 하나가 첫 폴링에서
133
- # 워치독을 통째로 죽인다 조용히, 로그 없이. file_mtime 같은 코드로도
134
- # 멀쩡한 것은 항상 `$( )` 안에서 호출돼 errexit 상속하지 않기 때문이다.
141
+ # `|| true` is mandatory. macOS has no GNU `stat -c`, so that call exits 1,
142
+ # and `2>/dev/null` suppresses stderr but not the exit code. This watchdog
143
+ # is an explicit `( ) &` subshell, so it inherits whatever errexit setting
144
+ # is in force; under `set -e` a single failed assignment kills the whole
145
+ # watchdog on the first poll silently, without a log line.
135
146
  last_mtime="$(stat -c %Y "$log_path" 2>/dev/null || true)"
136
147
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime="$(stat -f %m "$log_path" 2>/dev/null || true)"
137
148
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime=0
@@ -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
@@ -411,15 +421,17 @@ if (( idle_timeout_secs > 0 )); then
411
421
  while kill -0 "$codex_pid" 2>/dev/null; do
412
422
  sleep "$poll_interval"
413
423
  kill -0 "$codex_pid" 2>/dev/null || exit 0
414
- # GNU `stat -c` 우선 + 숫자 검증. BSD-ism `stat -f %m` 은 Linux 에서 깨끗이
415
- # 실패하지 않고 비숫자 파일시스템 출력으로 성공해 아래 산술을 set -u 에서
416
- # 깨뜨린다file_mtime(lib/okstra/interactive.sh) 동일 가드.
424
+ # Portable mtime probe: GNU (stat -c %Y) first, macOS/BSD (stat -f %m) as
425
+ # the fallback, 0 as the floor. Validate each result as a bare integer
426
+ # before accepting it on Linux the BSD-ism `stat -f %m` does NOT fail
427
+ # cleanly; it succeeds and prints non-numeric filesystem output, which
428
+ # would break the arithmetic below under `set -u`.
417
429
  #
418
- # `|| true` 필수다. macOS 에는 GNU `stat -c` 없어 exit 1 이고,
419
- # `2>/dev/null` stderr 막지 종료코드는 막지 못한다. 워치독은 명시적
420
- # `( ) &` 서브셸이라 위쪽 `set -e` 상속하므로, 실패한 할당 하나가 첫 폴링에서
421
- # 워치독을 통째로 죽인다 조용히, 로그 없이. file_mtime 같은 코드로도
422
- # 멀쩡한 것은 항상 `$( )` 안에서 호출돼 errexit 상속하지 않기 때문이다.
430
+ # `|| true` is mandatory. macOS has no GNU `stat -c`, so that call exits 1,
431
+ # and `2>/dev/null` suppresses stderr but not the exit code. This watchdog
432
+ # is an explicit `( ) &` subshell, so it inherits whatever errexit setting
433
+ # is in force; under `set -e` a single failed assignment kills the whole
434
+ # watchdog on the first poll silently, without a log line.
423
435
  last_mtime="$(stat -c %Y "$log_path" 2>/dev/null || true)"
424
436
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime="$(stat -f %m "$log_path" 2>/dev/null || true)"
425
437
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime=0
@@ -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):
@@ -10,6 +10,7 @@ from typing import List
10
10
  from okstra_project.dirs import tasks_root
11
11
 
12
12
  from .ids import build_run_id
13
+ from .paths import runs_dir_of
13
14
  from .invocation import save_invocation
14
15
  from .jsonl import append_jsonl, read_jsonl, rotate_recent_if_needed
15
16
  from .project_meta import _project_meta_path
@@ -130,7 +131,7 @@ def backfill_project(home: Path, project_id: str, project_root: Path) -> int:
130
131
  manifest_re = _re.compile(r"^run-manifest-(?P<tt>.+)-(?P<seq>\d+)\.json$")
131
132
  for group_dir in sorted(p for p in base.iterdir() if p.is_dir()):
132
133
  for task_dir in sorted(p for p in group_dir.iterdir() if p.is_dir()):
133
- runs = task_dir / "runs"
134
+ runs = runs_dir_of(task_dir)
134
135
  if not runs.is_dir():
135
136
  continue
136
137
  for manifests in _iter_manifest_dirs(runs):
@@ -13,6 +13,7 @@ import sys
13
13
  from pathlib import Path
14
14
  from typing import Iterable
15
15
 
16
+ from okstra_ctl.paths import RunRef, runs_dir_of
16
17
  from okstra_ctl.task_target import resolve_task_root, project_rel
17
18
 
18
19
 
@@ -102,7 +103,7 @@ def _find_current_run_dir(
102
103
  task_type = manifest.get("workflow", {}).get("currentPhase") or manifest.get("taskType")
103
104
  if not isinstance(task_type, str) or not task_type:
104
105
  return None
105
- candidate = task_root / "runs" / task_type
106
+ candidate = RunRef.from_task_root(task_root, task_type).run_dir
106
107
  return candidate if candidate.is_dir() else None
107
108
 
108
109
 
@@ -407,7 +408,7 @@ def analyze_task_bundle(task_root: Path, project_root: Path) -> dict:
407
408
  task_file_count, task_bytes = _count_files(all_task_files)
408
409
  current_run_file_count, current_run_bytes = _count_files(_all_files(run_dir)) if run_dir else (0, 0)
409
410
  legacy_timestamp_files = [
410
- path for path in (task_root / "runs").rglob("*")
411
+ path for path in runs_dir_of(task_root).rglob("*")
411
412
  if path.is_file() and _is_timestamped_legacy_artifact(path)
412
413
  ]
413
414