okstra 0.34.1 → 0.36.1

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 (108) hide show
  1. package/README.kr.md +27 -19
  2. package/README.md +27 -19
  3. package/docs/kr/architecture.md +59 -45
  4. package/docs/kr/cli.md +61 -18
  5. package/docs/pr-template-usage.md +65 -0
  6. package/docs/project-structure-overview.md +353 -354
  7. package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +1 -1
  8. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1 -1
  9. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +1 -1
  10. package/docs/superpowers/plans/2026-05-20-final-report-language.md +1501 -0
  11. package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +1267 -0
  12. package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +1007 -0
  13. package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +720 -0
  14. package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +681 -0
  15. package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +1691 -0
  16. package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +1700 -0
  17. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +383 -0
  18. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +320 -0
  19. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +299 -0
  20. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +335 -0
  21. package/docs/task-process/README.md +74 -0
  22. package/docs/task-process/common-flow.md +166 -0
  23. package/docs/task-process/error-analysis.md +101 -0
  24. package/docs/task-process/final-verification.md +167 -0
  25. package/docs/task-process/implementation-planning.md +128 -0
  26. package/docs/task-process/implementation.md +149 -0
  27. package/docs/task-process/release-handoff.md +206 -0
  28. package/docs/task-process/requirements-discovery.md +115 -0
  29. package/package.json +1 -1
  30. package/runtime/BUILD.json +2 -2
  31. package/runtime/agents/SKILL.md +30 -7
  32. package/runtime/agents/workers/claude-worker.md +31 -6
  33. package/runtime/agents/workers/codex-worker.md +37 -10
  34. package/runtime/agents/workers/gemini-worker.md +34 -7
  35. package/runtime/agents/workers/report-writer-worker.md +19 -10
  36. package/runtime/bin/okstra-central.sh +6 -6
  37. package/runtime/bin/okstra-codex-exec.sh +49 -28
  38. package/runtime/bin/okstra-gemini-exec.sh +39 -21
  39. package/runtime/bin/okstra-render-final-report.py +13 -2
  40. package/runtime/bin/okstra-wrapper-status.py +155 -0
  41. package/runtime/bin/okstra.sh +2 -2
  42. package/runtime/prompts/launch.template.md +1 -0
  43. package/runtime/prompts/profiles/_common-contract.md +11 -6
  44. package/runtime/prompts/profiles/_implementation-deliverable.md +53 -0
  45. package/runtime/prompts/profiles/_implementation-executor.md +60 -0
  46. package/runtime/prompts/profiles/_implementation-verifier.md +76 -0
  47. package/runtime/prompts/profiles/error-analysis.md +3 -7
  48. package/runtime/prompts/profiles/implementation-planning.md +22 -21
  49. package/runtime/prompts/profiles/implementation.md +28 -118
  50. package/runtime/prompts/profiles/improvement-discovery.md +42 -0
  51. package/runtime/prompts/profiles/release-handoff.md +1 -1
  52. package/runtime/prompts/profiles/requirements-discovery.md +8 -12
  53. package/runtime/prompts/wizard/prompts.ko.json +230 -0
  54. package/runtime/python/lib/okstra/cli.sh +2 -49
  55. package/runtime/python/lib/okstra/globals.sh +21 -21
  56. package/runtime/python/lib/okstra/interactive.sh +7 -7
  57. package/runtime/python/okstra_ctl/clarification_items.py +3 -9
  58. package/runtime/python/okstra_ctl/consumers.py +53 -0
  59. package/runtime/python/okstra_ctl/final_report_schema.py +0 -7
  60. package/runtime/python/okstra_ctl/i18n.py +73 -0
  61. package/runtime/python/okstra_ctl/improvement_lenses.py +44 -0
  62. package/runtime/python/okstra_ctl/index.py +1 -1
  63. package/runtime/python/okstra_ctl/paths.py +26 -20
  64. package/runtime/python/okstra_ctl/render.py +166 -207
  65. package/runtime/python/okstra_ctl/render_final_report.py +53 -10
  66. package/runtime/python/okstra_ctl/run.py +299 -108
  67. package/runtime/python/okstra_ctl/run_context.py +22 -0
  68. package/runtime/python/okstra_ctl/seeding.py +186 -0
  69. package/runtime/python/okstra_ctl/session.py +65 -7
  70. package/runtime/python/okstra_ctl/wizard.py +348 -127
  71. package/runtime/python/okstra_ctl/workflow.py +21 -2
  72. package/runtime/python/okstra_ctl/worktree.py +54 -1
  73. package/runtime/python/okstra_project/resolver.py +4 -3
  74. package/runtime/python/okstra_token_usage/report.py +2 -2
  75. package/runtime/schemas/final-report-v1.0.schema.json +22 -16
  76. package/runtime/skills/okstra-brief/SKILL.md +102 -218
  77. package/runtime/skills/okstra-convergence/SKILL.md +2 -3
  78. package/runtime/skills/okstra-inspect/SKILL.md +581 -0
  79. package/runtime/skills/okstra-report-writer/SKILL.md +35 -15
  80. package/runtime/skills/okstra-run/SKILL.md +8 -7
  81. package/runtime/skills/okstra-schedule/SKILL.md +14 -157
  82. package/runtime/skills/okstra-setup/SKILL.md +28 -1
  83. package/runtime/skills/okstra-team-contract/SKILL.md +16 -107
  84. package/runtime/templates/okstra.CLAUDE.md +104 -0
  85. package/runtime/templates/reports/brief.template.md +204 -0
  86. package/runtime/templates/reports/final-report.template.md +93 -98
  87. package/runtime/templates/reports/i18n/en.json +135 -0
  88. package/runtime/templates/reports/i18n/ko.json +135 -0
  89. package/runtime/templates/reports/implementation-planning-input.template.md +18 -0
  90. package/runtime/templates/reports/improvement-discovery-input.template.md +78 -0
  91. package/runtime/templates/reports/schedule.template.md +12 -3
  92. package/runtime/templates/reports/task-brief.template.md +2 -2
  93. package/runtime/templates/worker-prompt-preamble.md +108 -0
  94. package/runtime/validators/lib/fixtures.sh +30 -0
  95. package/runtime/validators/lib/runners.sh +1 -1
  96. package/runtime/validators/validate-implementation-plan-stages.py +211 -0
  97. package/runtime/validators/validate-run.py +121 -26
  98. package/runtime/validators/validate-workflow.sh +2 -2
  99. package/runtime/validators/validate_improvement_report.py +275 -0
  100. package/src/config.mjs +18 -0
  101. package/src/install.mjs +41 -14
  102. package/src/setup.mjs +133 -1
  103. package/src/uninstall.mjs +27 -3
  104. package/runtime/skills/okstra-history/SKILL.md +0 -165
  105. package/runtime/skills/okstra-logs/SKILL.md +0 -173
  106. package/runtime/skills/okstra-report-finder/SKILL.md +0 -111
  107. package/runtime/skills/okstra-status/SKILL.md +0 -246
  108. package/runtime/skills/okstra-time-summary/SKILL.md +0 -172
@@ -68,7 +68,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
68
68
  6. Extract the assigned model execution value for `Gemini worker`.
69
69
  - First, use the value explicitly assigned in the lead prompt.
70
70
  - If the lead prompt only lists the display model, use the canonical execution value from the referenced task bundle metadata (`task-manifest.json` → `resultContract.requiredWorkerRoles[]` for the gemini role).
71
- - If no assigned model execution value can be determined, immediately return `GEMINI_MODEL_MISSING: assigned Gemini model execution value was not provided`. Do NOT fall back to training-data defaults — historical Gemini defaults (e.g. `gemini-1.5-flash`) are NOT acceptable substitutes for the assigned model. Returning the sentinel is the correct behavior; the lead is responsible for fixing its prompt and redispatching.
71
+ - If no assigned model execution value can be determined, immediately return `GEMINI_MODEL_MISSING: assigned Gemini model execution value was not provided`. Do NOT fall back to training-data defaults — historical Gemini defaults like `gemini-1.5-flash` are NOT acceptable substitutes for the assigned model. Returning the sentinel is the correct behavior; the lead is responsible for fixing its prompt and redispatching.
72
72
  - This rule applies equally to convergence reverify rounds. The reverify prompt MUST carry the same `**Model:**` line as the initial run (see `okstra-convergence` skill, "Required reverify-prompt anchor headers"). If the line is absent in a reverify prompt, return `GEMINI_MODEL_MISSING` rather than guessing.
73
73
 
74
74
  7. If installed, dispatch the wrapper as a **background** Bash command and poll for completion. The two-minute foreground Bash timeout is insufficient for implementation-phase Gemini runs and forced workers into ad-hoc background dispatch with lost output. The polling contract below is the formal replacement.
@@ -79,7 +79,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
79
79
  ```
80
80
  Call `Bash` with `run_in_background: true`. Capture the returned `bash_id` (a.k.a. `shell_id`). Pass the positional arguments verbatim — do NOT use environment variables, `cd`, `&&` chains, or pipes from `cat`. Substitute the literal extracted Project Root, model execution value, prompt-history path, and worktree path. The fourth argument is **mandatory for implementation phase** (extract from `EXECUTOR_WORKTREE_PATH` in the lead prompt's run context or the `**Worktree:**` / `cwd for every mutating command:` line) and **may be omitted only for non-implementation analysis phases** that do not mutate the worktree. The wrapper handles `-p -`, `-m`, `-o text`, `--include-directories`, the stdin redirect from the prompt file, and stderr suppression internally. Calling `gemini` directly (without the wrapper) is an error in this skill: the redirect tokens disqualify the prefix match against `Bash(gemini:*)` and produce a permission prompt every dispatch.
81
81
 
82
- **Poll loop (BashOutput-only, 30-minute hard cap):**
82
+ **Poll loop (BashOutput-only, 30-minute cap):**
83
83
  - Record `start_ts` at dispatch time via a single `Bash` call: `date +%s` (output captured).
84
84
  - Repeat:
85
85
  1. Call `BashOutput(bash_id: <shell_id>)`. Inspect `status`. The harness's `BashOutput` primitive already waits internally for new output before returning; back-to-back calls are the canonical wait mechanism for a background shell.
@@ -108,6 +108,8 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
108
108
 
109
109
  d. **Normal return.** Otherwise (`exit_code == 0` AND result file exists), concatenate the wrapper's accumulated stdout from `BashOutput` and return it as-is without modification.
110
110
 
111
+ 9. When `Task Type` is `improvement-discovery`, the lead's Phase 1.5 reflect-back log at `<RUN_DIR>/state/phase-1.5-grilling.md` is the authoritative scope and lens definition. Read its `Resolved scope` and `Resolved lenses` blocks and do NOT re-interpret the brief's raw `scan-scope` / `priority-lenses` fields. Findings that violate the resolved lens whitelist or scope are rejected by `validators/validate-improvement-report.py`.
112
+
111
113
  ## Stop Condition
112
114
 
113
115
  This wrapper is a thin Bash-execution shell over the Gemini CLI (via `okstra-gemini-exec.sh`). The CLI process itself is the analysis engine; this subagent's only job is to dispatch it and forward output. Therefore:
@@ -126,7 +128,7 @@ This wrapper does NOT invoke MCP tools directly. MCP availability inside the Gem
126
128
  ## Prompt Composition
127
129
 
128
130
  - The lead prompt must include both `**Project Root:** <absolute-path>` (at the top) and `Assigned worker prompt history path: <path>`.
129
- - Treat that path as the canonical worker prompt history artifact for the current run.
131
+ - Treat the prompt-history path as the canonical worker prompt history artifact for the current run, resolved to absolute against `Project Root` if given as relative.
130
132
  - The assigned model execution value is canonical for CLI execution. Do not substitute a different Gemini model unless the task bundle explicitly changes it.
131
133
  - Pass the prompt received from Lead directly to gemini after persisting the exact prompt to the assigned path.
132
134
  - Include context (code, diff, file paths) if provided.
@@ -138,11 +140,12 @@ This wrapper does NOT invoke MCP tools directly. MCP availability inside the Gem
138
140
 
139
141
  ## Required Reading Before Any Analysis
140
142
 
141
- Before producing any output, you MUST ensure the underlying Gemini CLI run reads every input file enumerated in the `[Required reading]` block of the lead's prompt from the very first character to the very last character. For analysis workers this includes the task brief, analysis profile, analysis material (if present), reference expectations, and the carry-in clarification response (if present). Analysis workers do NOT read `final-report-template.md` — that file is for the Report writer worker only (see `okstra-team-contract` "Audience-scoped enumeration"). Producing findings without the template is the intended contract; the report writer in Phase 6 owns final-report structure.
143
+ Before invoking the Gemini CLI, you MUST:
144
+
145
+ 1. Extract the absolute path from the lead's `**Worker Preamble Path:**` anchor header and verify the CLI run will Read that file end-to-end (canonical SSOT for the Required Reading + Error Reporting + Output sections contract). The lead's prompt body — which you persist verbatim and feed into Gemini via stdin — already contains this anchor; do not strip it.
146
+ 2. Verify the lead's prompt body lists the per-run input files under `## Inputs` (task-brief, analysis-profile, analysis-material if present, reference-expectations, clarification-response if carry-in). Analysis workers do NOT read `final-report-template.md` — that file is for the report writer only.
142
147
 
143
- - The lead's prompt body, which you persist verbatim and feed into Gemini via stdin, already contains the explicit list of files and the end-to-end reading rule. Do not strip or summarize that block before passing it to the CLI.
144
- - For the carry-in clarification response, the CLI must walk every row of `## 5. Clarification Items` (`C-001`, `C-002`, ...) in full, including rows whose `User input` cell is blank — a blank `User input` with `Status=open` is itself a signal you must surface. The structural similarity between the prior final report and the upcoming output is the most common reason this step gets skipped — do not repeat that.
145
- - The wrapper writes a Reading Confirmation block to the **audit sidecar** at `runs/<task-type>/worker-results/gemini-worker-audit-<task-type>-<seq>.md` (sibling to the main worker-results file). The sidecar's body begins with `# Gemini Worker Audit — <task-key>` followed by one short line per input file confirming end-to-end reading (e.g. `- Read task-brief.md end-to-end (147 lines).`). The main Gemini output MUST NOT contain a `## 0. Reading Confirmation` heading — the validator now fails worker-results that contain one. If any file was skipped, record a `tool-failure` in the errors sidecar instead of fabricating Findings.
148
+ The CLI writes a Reading Confirmation block to the **audit sidecar** at `runs/<task-type>/worker-results/gemini-worker-audit-<task-type>-<seq>.md`. The sidecar's body begins with `# Gemini Worker Audit <task-key>` followed by one short line per input file confirming end-to-end reading. The main Gemini output MUST NOT contain a `## 0. Reading Confirmation` heading the validator fails worker-results that contain one. If any file was skipped, record a `tool-failure` in the errors sidecar instead of fabricating Findings.
146
149
 
147
150
  ## Worker Output Structure
148
151
 
@@ -227,3 +230,27 @@ pre-flight terminal status, not a runtime CLI error.
227
230
  - Return error messages as-is on failure.
228
231
  - Do not summarize or modify Gemini results.
229
232
  - Sections 1–5 of the worker output are the common core shared with the Claude and Codex workers — the dispatched prompt asks identical questions for all three roles, and the Gemini CLI must answer all of them, not only requirement-interpretation findings. Your specialization (requirement interpretation, consistency, safety, documentation quality, alternative viewpoints) belongs only in optional Section 6 as additive depth. A Gemini result whose Findings section is populated solely with requirement-interpretation items is in breach of contract; see `skills/okstra-team-contract/SKILL.md` "Worker Output Contract".
233
+
234
+ ## Stage evidence emission (BLOCKING, implementation task only)
235
+
236
+ When this run's `task_type` is `implementation` and you are acting as the **Executor**, after the Stage Validation `post` commands all return exit code 0 you MUST emit a single JSON document matching `docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md` §3.2:
237
+
238
+ ```json
239
+ {
240
+ "schemaVersion": 1,
241
+ "sourcePlanPath": "<approved-plan path>",
242
+ "stageNumber": <int>,
243
+ "stageTitle": "<from Stage Map>",
244
+ "completedAt": "<ISO-8601 with tz>",
245
+ "stageCommitRange": { "base": "<sha>", "head": "<sha>" },
246
+ "filesChanged": ["<rel/path>", "..."],
247
+ "newIdentifiers": ["<name>", "..."],
248
+ "stepResults": [{"step": <int>, "status": "done", "commit": "<sha>"}],
249
+ "validationsPassed": ["<label>", "..."],
250
+ "notes": []
251
+ }
252
+ ```
253
+
254
+ Emit this as a fenced ```json``` block in your worker result under the heading `### Stage Carry Evidence`. The lead (`Claude lead`) is responsible for persisting the block as `runs/<impl-task-key>/carry/stage-<N>.json` — you do not write the file yourself.
255
+
256
+ This applies only when `task_type` is `implementation`. For other task types, skip this block entirely.
@@ -16,6 +16,10 @@ tools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "TodoWrite", "WebFetch"
16
16
 
17
17
  You are the `Report writer worker` for okstra cross-verification. Your sole responsibility is to **author the final-report data.json** (the JSON SSOT) at the assigned `Result Path`, plus an audit sidecar. You are NOT an analysis worker — you do not produce independent findings, you do not vote in convergence, and you do not re-do the workers' analysis.
18
18
 
19
+ - The `**Report Language:**` header in your dispatch prompt is already
20
+ resolved to `en` or `ko` by the lead. Copy it verbatim into
21
+ `data.json.meta.reportLanguage`. Never write `auto` here.
22
+
19
23
  ## Authority
20
24
 
21
25
  You are the canonical author of `runs/<task-type>/reports/final-report-<task-type>-<seq>.data.json` for this run. Claude lead has explicitly delegated file-authorship to you. The lead reviews your output but does not write the file.
@@ -56,18 +60,22 @@ Do NOT duplicate the data.json contents here — the data.json is the canonical
56
60
 
57
61
  ## Required Reading Before Authoring
58
62
 
59
- Before writing the data.json, you MUST read every input file enumerated in the `[Required reading]` block of the lead's prompt from the very first character to the very last character. This always includes:
63
+ Before writing the data.json, you MUST:
64
+
65
+ 1. Extract the absolute path from the lead's `**Worker Preamble Path:**` anchor header and Read that file end-to-end (canonical SSOT for the Required Reading + Error Reporting + Anchor contract — this overrides per-spec restatements).
66
+ 2. Read every input file the lead enumerated under `## Inputs` (or equivalent heading) in the dispatch prompt body, end-to-end (single `Read` call with no `offset`/`limit`; page through with explicit offsets only when a file is too large for one read).
60
67
 
61
- - `schemas/final-report-v1.0.schema.json` — the JSON Schema you must conform to. The renderer + validator both consume this.
62
- - `templates/reports/final-report.template.md` — the Jinja2 template the renderer uses. Read this to understand which data.json fields appear where in the rendered markdown, but do NOT edit it.
68
+ For the report writer specifically, the `## Inputs` list always includes:
69
+
70
+ - `schemas/final-report-v1.0.schema.json` — the JSON Schema you must conform to. The renderer + validator both consume it.
71
+ - `templates/reports/final-report.template.md` — the Jinja2 template the renderer uses. Read it to understand which data.json fields appear where in the rendered markdown; do NOT edit it.
72
+ - `templates/reports/i18n/en.json` and `templates/reports/i18n/ko.json`.
63
73
  - Every analysis worker's result file under `worker-results/`.
64
- - `state/convergence-<task-type>-<seq>.json` (if present).
74
+ - `state/convergence-<task-type>-<seq>.json` (if present). When present, reproduce its `roundHistory[]`, `round2SkippedReason`, and `finalClassificationCounts` verbatim into the final report's Section 1 Round History sub-table — do not recompute from worker results.
75
+
76
+ For the carry-in `clarification-response.md` (if present), walk every row of `## 5. Clarification Items` including rows whose `User input` cell is blank — a blank cell with `Status=open` is a signal you must surface in the conditional `## 0. Clarification Response Carried In From Previous Run` section (the template's `RENDER_IF` guard activates it when the carry-in path is non-empty). When no carry-in path was provided, OMIT the `## 0.` heading entirely — do NOT write an empty-state stub.
65
77
 
66
- - Use a single `Read` call per file with no `offset` and no `limit`. If a file is too large for one read, page through it with explicit `offset` / `limit` calls covering the full file.
67
- - For the carry-in `clarification-response.md` (if present), walk every row of `## 5. Clarification Items` (`C-001`, `C-002`, ...) including rows whose `User input` cell is blank — a blank cell with `Status=open` is itself a signal you must surface in the conditional `## 0. Clarification Response Carried In From Previous Run` section (the template's `RENDER_IF` guard activates it when the carry-in path is non-empty). The fact that the file you write has a structurally similar section 5 is NOT an excuse to skim. When no carry-in path was provided, OMIT the `## 0.` heading entirely — do NOT write an empty-state stub.
68
- - Open every analysis-worker result file under `worker-results/` end-to-end. Do not summarize them from convergence output alone — convergence captures classifications, not full evidence.
69
- - Write a Reading Confirmation block to your **audit sidecar** at `runs/<task-type>/worker-results/report-writer-worker-audit-<task-type>-<seq>.md` (sibling to the main worker-results file). The sidecar's body begins with `# Report Writer Worker Audit — <task-key>` followed by one short line per input file confirming end-to-end reading. The main final-report and the main worker-results file MUST NOT contain a `## 0. Reading Confirmation` heading — the validator now fails reports that contain one. If you cannot truthfully confirm a file end-to-end, record a `tool-failure` in the errors sidecar instead of fabricating the report.
70
- - When the convergence-state file is present, read it fully and reproduce the `roundHistory[]` array, `round2SkippedReason`, and `finalClassificationCounts` in the final report's Section 1 Round History sub-table. Do not derive these values from worker results alone — they live in `state/convergence-<task-type>-<seq>.json`.
78
+ Write a Reading Confirmation block to your **audit sidecar** at `runs/<task-type>/worker-results/report-writer-worker-audit-<task-type>-<seq>.md`. The main final-report and the main worker-results file MUST NOT contain a `## 0. Reading Confirmation` heading. If you cannot truthfully confirm a file end-to-end, record a `tool-failure` in the errors sidecar instead of fabricating the report.
71
79
 
72
80
  ## Authoring Contract
73
81
 
@@ -75,7 +83,7 @@ You author the final-report data.json (the JSON SSOT). The schema is `schemas/fi
75
83
 
76
84
  The rendered markdown (`final-report-<task-type>-<seq>.md`) is produced by `scripts/okstra-render-final-report.py` immediately after you write the data.json. The HTML view (`*.html`) is produced from the markdown by Phase 7 step 1.5 (`scripts/okstra-render-report-views.py`). The data.json is the only file you write; the rest are derived.
77
85
 
78
- Hard rules (the schema enforces most of these — they are listed here so you know *what* to populate, not *how* to validate):
86
+ Rules (the schema enforces most of these — they are listed here so you know *what* to populate, not *how* to validate):
79
87
 
80
88
  - `header.reportAuthor` is `"Report writer worker"`; `header.reportOwner` is `"Claude lead"`. Set author to `"Claude lead"` only for `release-handoff` runs (single-lead by design) or a recorded report-writer dispatch failure fallback.
81
89
  - **Source items (worker:item) preservation.** Every `consensus[].sourceItems`, `differences[].workersPosition[].itemId`, and `evidence.primary[].sourceItems` entry MUST carry the worker:item-id pair (e.g. `claude:F-001`, `codex:1.1`, `gemini:F-3`, or `lead:mcp-1` for lead-only evidence). The schema enforces this via the `SourceItem` regex; bare worker-name lists no longer parse.
@@ -92,6 +100,7 @@ Hard rules (the schema enforces most of these — they are listed here so you kn
92
100
  - If evidence is missing, write `"I don't know"` in the relevant statement field rather than fabricating confidence.
93
101
  - Cite file paths and line numbers in every `evidence.primary[].source` / `consensus[].evidence` cell.
94
102
  - Preserve every analysis worker's ticket tagging — every row's `ticketId` field carries the ticket key or the task-fallback. For single-ticket runs, set `ticketCoverage` to `{"singleTicket": "<ticket>"}`. For runs that do not require ticket tagging (`release-handoff`, `final-verification`), set `ticketCoverage` to `{"omit": true}`.
103
+ - When the `Task Type` is `improvement-discovery`, populate `## 4.9 Improvement Candidates` with the 10-column schema enforced by `validators/validate-improvement-report.py`. Source the row IDs (`I-NNN`), lens whitelist, and Source workers patterns from `scripts/okstra_ctl/improvement_lenses.py` — do NOT introduce new lens names or worker prefixes.
95
104
 
96
105
  Write the data.json with your `Write` tool against the absolute `Result Path`. Then invoke the renderer (`Bash`): `python3 scripts/okstra-render-final-report.py <data.json path>`. Confirm both files exist and respond with a short status line: `data.json written to <abs path>; markdown rendered to <abs path>. Sections populated: <count>.`
97
106
 
@@ -102,11 +102,11 @@ print(json.dumps({k: v for k, v in zip(it, it)}, ensure_ascii=False))
102
102
  PROJECT_ROOT "${PROJECT_ROOT-}" \
103
103
  TASK_GROUP "${TASK_GROUP-}" \
104
104
  TASK_ID "${TASK_ID-}" \
105
- ANALYSIS_TYPE "${ANALYSIS_TYPE-}" \
105
+ TASK_TYPE "${TASK_TYPE-}" \
106
106
  OKSTRA_RUN_SEQ "$_run_seq" \
107
107
  RUN_TIMESTAMP_ISO "${RUN_TIMESTAMP_ISO-}" \
108
- SELECTED_REVIEWERS "${SELECTED_REVIEWERS-}" \
109
- LEAD_MODEL_DISPLAY "${LEAD_MODEL_DISPLAY-}" \
108
+ RECOMMENDED_ANALYSERS "${RECOMMENDED_ANALYSERS-}" \
109
+ LEAD_MODEL "${LEAD_MODEL-}" \
110
110
  RUN_DIR_RELATIVE_PATH "${RUN_DIR_RELATIVE_PATH-}" \
111
111
  FINAL_REPORT_RELATIVE_PATH "${FINAL_REPORT_RELATIVE_PATH-}" \
112
112
  FINAL_STATUS_RELATIVE_PATH "${FINAL_STATUS_RELATIVE_PATH-}" \
@@ -134,11 +134,11 @@ with lockfile.open("r+") as lock:
134
134
  project_root=payload["PROJECT_ROOT"],
135
135
  task_group=payload["TASK_GROUP"],
136
136
  task_id=payload["TASK_ID"],
137
- task_type=payload.get("ANALYSIS_TYPE", ""),
137
+ task_type=payload.get("TASK_TYPE", ""),
138
138
  run_seq=int(payload["OKSTRA_RUN_SEQ"]),
139
139
  when=payload["RUN_TIMESTAMP_ISO"],
140
- workers=[w for w in payload.get("SELECTED_REVIEWERS", "").split(",") if w],
141
- lead_model=payload.get("LEAD_MODEL_DISPLAY", ""),
140
+ workers=[w for w in payload.get("RECOMMENDED_ANALYSERS", "").split(",") if w],
141
+ lead_model=payload.get("LEAD_MODEL", ""),
142
142
  run_dir_rel=payload.get("RUN_DIR_RELATIVE_PATH", ""),
143
143
  final_report_rel=payload.get("FINAL_REPORT_RELATIVE_PATH", ""),
144
144
  final_status_rel=payload.get("FINAL_STATUS_RELATIVE_PATH", ""),
@@ -187,19 +187,35 @@ python3 "$script_dir/okstra-wrapper-status.py" \
187
187
  init "$status_path" "$(basename "$0")" "$role" "$$" "$started_ts" "$log_path" \
188
188
  >>"$log_path" 2>&1 || true
189
189
 
190
+ # Resolve caller pane id robustly. tmux normally exports both `$TMUX` and
191
+ # `$TMUX_PANE` to processes started inside a pane, but Claude Code's Bash
192
+ # tool can drop `$TMUX_PANE` while preserving `$TMUX` — which would
193
+ # silently skip the caller-pane rename below AND let `tmux split-window`
194
+ # attach the trace pane to whatever tmux currently considers active
195
+ # (not necessarily Claude's pane). When the wrapper is launched from
196
+ # Claude Code, the Claude session's pane IS the active pane at this
197
+ # moment, so falling back to `display-message -p '#{pane_id}'` recovers
198
+ # the correct id.
199
+ caller_pane="${TMUX_PANE:-}"
200
+ if [[ -z "$caller_pane" && -n "${TMUX:-}" ]]; then
201
+ caller_pane=$(tmux display-message -p '#{pane_id}' 2>/dev/null || true)
202
+ fi
203
+
190
204
  # Pane titles: worker (caller) pane gets `codex-<role>-<pid>`; the sibling
191
- # trace pane appends `-trace`. The wrapper PID disambiguates concurrent
192
- # dispatches of the same role (e.g. two `codex-worker` panes spawned in
193
- # parallel) so the operator can match worker trace at a glance.
205
+ # trace pane appends `-trace[from=<caller-pane-id>]`. The wrapper PID
206
+ # disambiguates concurrent dispatches of the same role; the embedded
207
+ # caller pane id keeps the trace worker mapping visible even if the
208
+ # worker pane's title is later overwritten by the parent process (e.g.
209
+ # Claude Code's TUI emitting OSC 2 escape sequences on its own pane).
194
210
  pane_label="codex-${role}-$$"
195
- trace_label="${pane_label}-trace"
211
+ trace_label="${pane_label}-trace[from=${caller_pane:-?}]"
196
212
 
197
213
  # Capture the caller pane's current title so the EXIT trap can restore it
198
214
  # once the wrapper returns. Empty when not in tmux or capture fails — the
199
215
  # restore step degrades to a no-op in that case.
200
216
  original_caller_title=""
201
- if [[ -n "${TMUX_PANE:-}" ]]; then
202
- original_caller_title=$(tmux display-message -p -t "$TMUX_PANE" '#{pane_title}' 2>/dev/null || true)
217
+ if [[ -n "$caller_pane" ]]; then
218
+ original_caller_title=$(tmux display-message -p -t "$caller_pane" '#{pane_title}' 2>/dev/null || true)
203
219
  fi
204
220
 
205
221
  _okstra_status_finish() {
@@ -210,16 +226,16 @@ _okstra_status_finish() {
210
226
  python3 "$script_dir/okstra-wrapper-status.py" \
211
227
  finish "$status_path" "$exit_code" "$ended_ts" "$duration_ms" \
212
228
  >>"$log_path" 2>&1 || true
213
- if [[ -n "${TMUX_PANE:-}" && -n "$original_caller_title" ]]; then
214
- tmux select-pane -t "$TMUX_PANE" -T "$original_caller_title" 2>/dev/null || true
229
+ if [[ -n "$caller_pane" && -n "$original_caller_title" ]]; then
230
+ tmux select-pane -t "$caller_pane" -T "$original_caller_title" 2>/dev/null || true
215
231
  fi
216
232
  }
217
233
  trap _okstra_status_finish EXIT
218
234
 
219
235
  # Label the caller (worker) pane now that the restore trap is armed. Any
220
236
  # failure after this point still rewinds the title to its prior value.
221
- if [[ -n "${TMUX_PANE:-}" ]]; then
222
- tmux select-pane -t "$TMUX_PANE" -T "$pane_label" 2>/dev/null || true
237
+ if [[ -n "$caller_pane" ]]; then
238
+ tmux select-pane -t "$caller_pane" -T "$pane_label" 2>/dev/null || true
223
239
  fi
224
240
 
225
241
  # When a tmux session is reachable, split a sibling pane that tails the live
@@ -227,35 +243,40 @@ fi
227
243
  # for the wrapper to exit. This fires in every phase the wrapper is invoked
228
244
  # from (analysis, error-analysis, implementation-planning, implementation,
229
245
  # …) — long-running codex dispatches are not implementation-specific. The
230
- # new pane carries the title `codex-<role>-<pid>-trace` (matching the
231
- # caller pane's `codex-<role>-<pid>` label so workertrace pairs are
232
- # greppable); `role` is the optional 5th positional arg (defaults to
233
- # `worker`); callers that dispatch a different role (e.g. `executor`) must
234
- # pass it explicitly. The `<pid>` suffix is the wrapper's PID and
235
- # disambiguates concurrent dispatches of the same role. The pane uses
236
- # `tail -F`
237
- # (follow-by-name) so it survives any truncation a re-dispatch performs on
238
- # the same log path. Failures are tolerated silently: missing $TMUX, a tmux
239
- # that refuses to split (size constraints, locked client), or a stale socket
246
+ # new pane carries the title `codex-<role>-<pid>-trace[from=<caller-pane>]`
247
+ # so the operator can map traceworker by pane id even when the worker
248
+ # pane title is later overwritten by Claude Code. The split is explicitly
249
+ # anchored to the caller pane (`-t "$caller_pane"`) to avoid attaching to
250
+ # tmux's idle active pane when `$TMUX_PANE` was missing. `role` is the
251
+ # optional 5th positional arg (defaults to `worker`); callers that
252
+ # dispatch a different role (e.g. `executor`) must pass it explicitly.
253
+ # The `<pid>` suffix is the wrapper's PID and disambiguates concurrent
254
+ # dispatches of the same role. The pane uses `tail -F` (follow-by-name)
255
+ # so it survives any truncation a re-dispatch performs on the same log
256
+ # path. Failures are tolerated silently: missing $TMUX, a tmux that
257
+ # refuses to split (size constraints, locked client), or a stale socket
240
258
  # all degrade to "log file is still on disk; the operator can tail it
241
- # manually from any terminal." The wrapper does NOT switch focus to the new
242
- # pane — control returns to the caller's pane via `tmux last-pane`.
259
+ # manually from any terminal." The wrapper does NOT switch focus to the
260
+ # new pane — control returns to the caller's pane via `tmux last-pane`.
243
261
  if [[ -n "${TMUX:-}" ]]; then
244
- trace_pane=$(tmux split-window -h -P -F '#{pane_id}' \
245
- -c "$(dirname "$log_path")" \
262
+ split_args=(-h -P -F '#{pane_id}' -c "$(dirname "$log_path")")
263
+ if [[ -n "$caller_pane" ]]; then
264
+ split_args+=(-t "$caller_pane")
265
+ fi
266
+ trace_pane=$(tmux split-window "${split_args[@]}" \
246
267
  "tail -F $(printf '%q' "$log_path")" 2>/dev/null || true)
247
268
  if [[ -n "$trace_pane" ]]; then
248
269
  tmux select-pane -t "$trace_pane" -T "$trace_label" 2>/dev/null || true
249
270
  tmux last-pane 2>/dev/null || true
250
271
  # Register the spawned pane so the `SessionEnd` hook (see
251
272
  # `okstra-trace-cleanup.sh`) can kill it when the caller's Claude
252
- # session exits. Scope by caller `$TMUX_PANE` — the pane Claude itself
253
- # is attached to — so concurrent Claude instances in the same tmux
273
+ # session exits. Scope by `$caller_pane` — the pane Claude itself is
274
+ # attached to — so concurrent Claude instances in the same tmux
254
275
  # session do not stomp each other's trace panes.
255
- if [[ -n "${TMUX_PANE:-}" ]]; then
276
+ if [[ -n "$caller_pane" ]]; then
256
277
  registry_dir="${TMPDIR:-/tmp}/okstra-trace-panes"
257
278
  mkdir -p "$registry_dir" 2>/dev/null || true
258
- safe_pane="${TMUX_PANE//[^A-Za-z0-9]/_}"
279
+ safe_pane="${caller_pane//[^A-Za-z0-9]/_}"
259
280
  printf '%s\n' "$trace_pane" >> "$registry_dir/${safe_pane}.list" 2>/dev/null || true
260
281
  fi
261
282
  fi
@@ -136,19 +136,31 @@ python3 "$script_dir/okstra-wrapper-status.py" \
136
136
  init "$status_path" "$(basename "$0")" "$role" "$$" "$started_ts" "$log_path" \
137
137
  >>"$log_path" 2>&1 || true
138
138
 
139
+ # Resolve caller pane id robustly. See `okstra-codex-exec.sh` for the full
140
+ # rationale — kept in lock-step: tmux normally exports both `$TMUX` and
141
+ # `$TMUX_PANE`, but Claude Code's Bash tool can drop `$TMUX_PANE` while
142
+ # preserving `$TMUX`, which silently skips the caller-pane rename and
143
+ # lets `tmux split-window` attach to whatever tmux considers active.
144
+ caller_pane="${TMUX_PANE:-}"
145
+ if [[ -z "$caller_pane" && -n "${TMUX:-}" ]]; then
146
+ caller_pane=$(tmux display-message -p '#{pane_id}' 2>/dev/null || true)
147
+ fi
148
+
139
149
  # Pane titles: worker (caller) pane gets `gemini-<role>-<pid>`; the sibling
140
- # trace pane appends `-trace`. The wrapper PID disambiguates concurrent
141
- # dispatches of the same role (e.g. two `gemini-worker` panes spawned in
142
- # parallel) so the operator can match worker trace at a glance.
150
+ # trace pane appends `-trace[from=<caller-pane-id>]`. The wrapper PID
151
+ # disambiguates concurrent dispatches of the same role; the embedded
152
+ # caller pane id keeps the trace worker mapping visible even if the
153
+ # worker pane's title is later overwritten by the parent process (e.g.
154
+ # Claude Code's TUI emitting OSC 2 escape sequences on its own pane).
143
155
  pane_label="gemini-${role}-$$"
144
- trace_label="${pane_label}-trace"
156
+ trace_label="${pane_label}-trace[from=${caller_pane:-?}]"
145
157
 
146
158
  # Capture the caller pane's current title so the EXIT trap can restore it
147
159
  # once the wrapper returns. Empty when not in tmux or capture fails — the
148
160
  # restore step degrades to a no-op in that case.
149
161
  original_caller_title=""
150
- if [[ -n "${TMUX_PANE:-}" ]]; then
151
- original_caller_title=$(tmux display-message -p -t "$TMUX_PANE" '#{pane_title}' 2>/dev/null || true)
162
+ if [[ -n "$caller_pane" ]]; then
163
+ original_caller_title=$(tmux display-message -p -t "$caller_pane" '#{pane_title}' 2>/dev/null || true)
152
164
  fi
153
165
 
154
166
  _okstra_status_finish() {
@@ -159,40 +171,46 @@ _okstra_status_finish() {
159
171
  python3 "$script_dir/okstra-wrapper-status.py" \
160
172
  finish "$status_path" "$exit_code" "$ended_ts" "$duration_ms" \
161
173
  >>"$log_path" 2>&1 || true
162
- if [[ -n "${TMUX_PANE:-}" && -n "$original_caller_title" ]]; then
163
- tmux select-pane -t "$TMUX_PANE" -T "$original_caller_title" 2>/dev/null || true
174
+ if [[ -n "$caller_pane" && -n "$original_caller_title" ]]; then
175
+ tmux select-pane -t "$caller_pane" -T "$original_caller_title" 2>/dev/null || true
164
176
  fi
165
177
  }
166
178
  trap _okstra_status_finish EXIT
167
179
 
168
180
  # Label the caller (worker) pane now that the restore trap is armed. Any
169
181
  # failure after this point still rewinds the title to its prior value.
170
- if [[ -n "${TMUX_PANE:-}" ]]; then
171
- tmux select-pane -t "$TMUX_PANE" -T "$pane_label" 2>/dev/null || true
182
+ if [[ -n "$caller_pane" ]]; then
183
+ tmux select-pane -t "$caller_pane" -T "$pane_label" 2>/dev/null || true
172
184
  fi
173
185
 
174
186
  # When a tmux session is reachable, split a sibling pane tailing the log so
175
187
  # the operator can watch progress live. This fires in every phase the
176
188
  # wrapper is invoked from — long-running gemini dispatches are not
177
- # implementation-specific. Title `gemini-<role>-<pid>-trace` (matching the
178
- # caller pane's `gemini-<role>-<pid>` label so workertrace pairs are
179
- # greppable). `role` is the optional 5th positional arg (defaults to
180
- # `worker`); callers that dispatch a different role must pass it
181
- # explicitly. The `<pid>` suffix is the wrapper's PID and disambiguates
182
- # concurrent dispatches of the same role. See the codex wrapper for the
183
- # full design rationale and the silent-degrade failure model.
189
+ # implementation-specific. Title `gemini-<role>-<pid>-trace[from=<caller-pane>]`
190
+ # so the operator can map traceworker by pane id even when the worker
191
+ # pane title is later overwritten by Claude Code. The split is explicitly
192
+ # anchored to the caller pane to avoid attaching to tmux's idle active
193
+ # pane when `$TMUX_PANE` was missing. `role` is the optional 5th
194
+ # positional arg (defaults to `worker`); callers that dispatch a
195
+ # different role must pass it explicitly. The `<pid>` suffix is the
196
+ # wrapper's PID and disambiguates concurrent dispatches of the same role.
197
+ # See the codex wrapper for the full design rationale and the
198
+ # silent-degrade failure model.
184
199
  if [[ -n "${TMUX:-}" ]]; then
185
- trace_pane=$(tmux split-window -h -P -F '#{pane_id}' \
186
- -c "$(dirname "$log_path")" \
200
+ split_args=(-h -P -F '#{pane_id}' -c "$(dirname "$log_path")")
201
+ if [[ -n "$caller_pane" ]]; then
202
+ split_args+=(-t "$caller_pane")
203
+ fi
204
+ trace_pane=$(tmux split-window "${split_args[@]}" \
187
205
  "tail -F $(printf '%q' "$log_path")" 2>/dev/null || true)
188
206
  if [[ -n "$trace_pane" ]]; then
189
207
  tmux select-pane -t "$trace_pane" -T "$trace_label" 2>/dev/null || true
190
208
  tmux last-pane 2>/dev/null || true
191
209
  # See `okstra-codex-exec.sh` for the registry rationale — kept in lock-step.
192
- if [[ -n "${TMUX_PANE:-}" ]]; then
210
+ if [[ -n "$caller_pane" ]]; then
193
211
  registry_dir="${TMPDIR:-/tmp}/okstra-trace-panes"
194
212
  mkdir -p "$registry_dir" 2>/dev/null || true
195
- safe_pane="${TMUX_PANE//[^A-Za-z0-9]/_}"
213
+ safe_pane="${caller_pane//[^A-Za-z0-9]/_}"
196
214
  printf '%s\n' "$trace_pane" >> "$registry_dir/${safe_pane}.list" 2>/dev/null || true
197
215
  fi
198
216
  fi
@@ -26,8 +26,9 @@ _HERE = Path(__file__).resolve().parent
26
26
  # scripts; for in-repo invocation we add ``scripts/`` explicitly.
27
27
  sys.path.insert(0, str(_HERE))
28
28
 
29
+ from okstra_ctl.i18n import SUPPORTED_LANGS # noqa: E402
29
30
  from okstra_ctl.render_final_report import ( # noqa: E402
30
- RenderError,
31
+ FinalReportRenderError,
31
32
  render_to_file,
32
33
  )
33
34
 
@@ -68,6 +69,15 @@ def main(argv: list[str]) -> int:
68
69
  "the repo-local copy."
69
70
  ),
70
71
  )
72
+ parser.add_argument(
73
+ "--report-language",
74
+ choices=list(SUPPORTED_LANGS),
75
+ default=None,
76
+ help=(
77
+ "Override the language passed into the renderer. When omitted, "
78
+ "the renderer reads data.json.meta.reportLanguage (fallback 'en')."
79
+ ),
80
+ )
71
81
  parser.add_argument(
72
82
  "--force",
73
83
  action="store_true",
@@ -88,8 +98,9 @@ def main(argv: list[str]) -> int:
88
98
  args.data,
89
99
  output,
90
100
  template_path=args.template,
101
+ report_language=args.report_language,
91
102
  )
92
- except RenderError as exc:
103
+ except FinalReportRenderError as exc:
93
104
  print(f"error: {exc}", file=sys.stderr)
94
105
  return 1
95
106
 
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env python3
2
+ """okstra-wrapper-status.py — heartbeat sidecar writer for codex/gemini wrappers.
3
+
4
+ The codex/gemini wrappers (`okstra-codex-exec.sh`, `okstra-gemini-exec.sh`)
5
+ dispatch a long-running CLI under `Bash(run_in_background: true)` and rely on
6
+ `BashOutput` polling for liveness. That polling stream only carries stdout
7
+ plus a binary `running`/`completed` state. Several recovery decisions need
8
+ more — specifically, "did this wrapper start at all, when, and how did it
9
+ finish?" — so the wrappers write a small JSON sidecar at
10
+ `<prompt-path>.status.json` that survives independent of the polling channel.
11
+
12
+ Consumers:
13
+
14
+ * `codex-worker` / `gemini-worker` step 8c: read `log_path` to capture a
15
+ diagnostic tail when `exit_code == 0` but the canonical Result file is
16
+ absent.
17
+ * Lead: cross-check `started_ts` / `ended_ts` to distinguish "wrapper hung
18
+ before CLI launched" from "CLI finished but never wrote artifact" when
19
+ applying the redispatch policy (see okstra-team-contract "Lead Redispatch
20
+ Policy on Result-Missing").
21
+
22
+ Failures are deliberately non-fatal for the caller — the wrapper's main
23
+ job is to run the underlying CLI; a missing sidecar must not break that.
24
+ On any error the script prints a one-line diagnostic to stderr and exits 0.
25
+
26
+ Schema (schemaVersion 1):
27
+
28
+ {
29
+ "schemaVersion": 1,
30
+ "wrapper": "<basename of caller>",
31
+ "role": "<worker|executor|verifier|...>",
32
+ "pid": <int — wrapper process pid at init time>,
33
+ "started_ts": <epoch seconds>,
34
+ "log_path": "<absolute path to the wrapper live log>",
35
+ "stage": "started" | "exited",
36
+ "exit_code": <int, only when stage=exited>,
37
+ "ended_ts": <epoch seconds, only when stage=exited>,
38
+ "duration_ms": <int, only when stage=exited>,
39
+ "timeout": <bool, only when killed by idle-watchdog>,
40
+ "idle_at_ts": <epoch seconds, only when timeout>,
41
+ "idle_seconds": <int, only when timeout>,
42
+ "terminated_by": "idle-watchdog" (only when timeout)
43
+ }
44
+
45
+ CLI:
46
+
47
+ okstra-wrapper-status.py init <status-path> <wrapper> <role> <pid> <started-ts> <log-path>
48
+ okstra-wrapper-status.py finish <status-path> <exit-code> <ended-ts> <duration-ms>
49
+ okstra-wrapper-status.py timeout <status-path> <idle-at-ts> <idle-seconds>
50
+ """
51
+ from __future__ import annotations
52
+
53
+ import json
54
+ import os
55
+ import sys
56
+
57
+
58
+ def warn(msg: str) -> None:
59
+ print(f"okstra-wrapper-status: {msg}", file=sys.stderr)
60
+
61
+
62
+ def atomic_write(path: str, doc: dict) -> None:
63
+ tmp = path + ".tmp"
64
+ with open(tmp, "w", encoding="utf-8") as f:
65
+ json.dump(doc, f, ensure_ascii=False, indent=2)
66
+ f.write("\n")
67
+ os.replace(tmp, path)
68
+
69
+
70
+ def cmd_init(argv: list[str]) -> None:
71
+ if len(argv) != 6:
72
+ warn("init expects: <status-path> <wrapper> <role> <pid> <started-ts> <log-path>")
73
+ return
74
+ status_path, wrapper, role, pid, started_ts, log_path = argv
75
+ doc = {
76
+ "schemaVersion": 1,
77
+ "wrapper": wrapper,
78
+ "role": role,
79
+ "pid": int(pid),
80
+ "started_ts": int(started_ts),
81
+ "log_path": log_path,
82
+ "stage": "started",
83
+ }
84
+ try:
85
+ atomic_write(status_path, doc)
86
+ except OSError as exc:
87
+ warn(f"init: failed to write {status_path}: {exc}")
88
+
89
+
90
+ def cmd_finish(argv: list[str]) -> None:
91
+ if len(argv) != 4:
92
+ warn("finish expects: <status-path> <exit-code> <ended-ts> <duration-ms>")
93
+ return
94
+ status_path, exit_code, ended_ts, duration_ms = argv
95
+ try:
96
+ with open(status_path, "r", encoding="utf-8") as f:
97
+ doc = json.load(f)
98
+ except FileNotFoundError:
99
+ warn(f"finish: sidecar absent at {status_path}; skipping")
100
+ return
101
+ except (OSError, json.JSONDecodeError) as exc:
102
+ warn(f"finish: failed to read {status_path}: {exc}")
103
+ return
104
+ doc["stage"] = "exited"
105
+ doc["exit_code"] = int(exit_code)
106
+ doc["ended_ts"] = int(ended_ts)
107
+ doc["duration_ms"] = int(duration_ms)
108
+ try:
109
+ atomic_write(status_path, doc)
110
+ except OSError as exc:
111
+ warn(f"finish: failed to write {status_path}: {exc}")
112
+
113
+
114
+ def cmd_timeout(argv: list[str]) -> None:
115
+ if len(argv) != 3:
116
+ warn("timeout expects: <status-path> <idle-at-ts> <idle-seconds>")
117
+ return
118
+ status_path, idle_at, idle_seconds = argv
119
+ try:
120
+ with open(status_path, "r", encoding="utf-8") as f:
121
+ doc = json.load(f)
122
+ except FileNotFoundError:
123
+ warn(f"timeout: sidecar absent at {status_path}; skipping")
124
+ return
125
+ except (OSError, json.JSONDecodeError) as exc:
126
+ warn(f"timeout: failed to read {status_path}: {exc}")
127
+ return
128
+ doc["timeout"] = True
129
+ doc["idle_at_ts"] = int(idle_at)
130
+ doc["idle_seconds"] = int(idle_seconds)
131
+ doc["terminated_by"] = "idle-watchdog"
132
+ try:
133
+ atomic_write(status_path, doc)
134
+ except OSError as exc:
135
+ warn(f"timeout: failed to write {status_path}: {exc}")
136
+
137
+
138
+ def main(argv: list[str]) -> int:
139
+ if len(argv) < 2:
140
+ warn("missing subcommand (init|finish|timeout)")
141
+ return 0
142
+ sub = argv[1]
143
+ if sub == "init":
144
+ cmd_init(argv[2:])
145
+ elif sub == "finish":
146
+ cmd_finish(argv[2:])
147
+ elif sub == "timeout":
148
+ cmd_timeout(argv[2:])
149
+ else:
150
+ warn(f"unknown subcommand: {sub}")
151
+ return 0
152
+
153
+
154
+ if __name__ == "__main__":
155
+ sys.exit(main(sys.argv))
@@ -68,7 +68,7 @@ if [[ "$ASSUME_YES" != "true" ]] && [[ -t 0 ]] && [[ -t 1 ]]; then
68
68
  cat >&2 <<CONFIRM_EOF
69
69
  okstra execution summary:
70
70
  render only: ${RENDER_ONLY}
71
- task type: ${ANALYSIS_TYPE}
71
+ task type: ${TASK_TYPE}
72
72
  project id: ${PROJECT_ID}
73
73
  project root: ${PROJECT_ROOT}
74
74
  task group: ${TASK_GROUP}
@@ -103,7 +103,7 @@ PY_ARGS=(
103
103
  --project-id "$PROJECT_ID"
104
104
  --task-group "$TASK_GROUP"
105
105
  --task-id "$TASK_ID"
106
- --task-type "$ANALYSIS_TYPE"
106
+ --task-type "$TASK_TYPE"
107
107
  --task-brief "$BRIEF_PATH"
108
108
  )
109
109
  [[ -n "${DIRECTIVE-}" ]] && PY_ARGS+=(--directive "$DIRECTIVE")