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.
- package/README.kr.md +27 -19
- package/README.md +27 -19
- package/docs/kr/architecture.md +59 -45
- package/docs/kr/cli.md +61 -18
- package/docs/pr-template-usage.md +65 -0
- package/docs/project-structure-overview.md +353 -354
- package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +1 -1
- package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1 -1
- package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +1 -1
- package/docs/superpowers/plans/2026-05-20-final-report-language.md +1501 -0
- package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +1267 -0
- package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +1007 -0
- package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +720 -0
- package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +681 -0
- package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +1691 -0
- package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +1700 -0
- package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +383 -0
- package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +320 -0
- package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +299 -0
- package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +335 -0
- package/docs/task-process/README.md +74 -0
- package/docs/task-process/common-flow.md +166 -0
- package/docs/task-process/error-analysis.md +101 -0
- package/docs/task-process/final-verification.md +167 -0
- package/docs/task-process/implementation-planning.md +128 -0
- package/docs/task-process/implementation.md +149 -0
- package/docs/task-process/release-handoff.md +206 -0
- package/docs/task-process/requirements-discovery.md +115 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +30 -7
- package/runtime/agents/workers/claude-worker.md +31 -6
- package/runtime/agents/workers/codex-worker.md +37 -10
- package/runtime/agents/workers/gemini-worker.md +34 -7
- package/runtime/agents/workers/report-writer-worker.md +19 -10
- package/runtime/bin/okstra-central.sh +6 -6
- package/runtime/bin/okstra-codex-exec.sh +49 -28
- package/runtime/bin/okstra-gemini-exec.sh +39 -21
- package/runtime/bin/okstra-render-final-report.py +13 -2
- package/runtime/bin/okstra-wrapper-status.py +155 -0
- package/runtime/bin/okstra.sh +2 -2
- package/runtime/prompts/launch.template.md +1 -0
- package/runtime/prompts/profiles/_common-contract.md +11 -6
- package/runtime/prompts/profiles/_implementation-deliverable.md +53 -0
- package/runtime/prompts/profiles/_implementation-executor.md +60 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +76 -0
- package/runtime/prompts/profiles/error-analysis.md +3 -7
- package/runtime/prompts/profiles/implementation-planning.md +22 -21
- package/runtime/prompts/profiles/implementation.md +28 -118
- package/runtime/prompts/profiles/improvement-discovery.md +42 -0
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +8 -12
- package/runtime/prompts/wizard/prompts.ko.json +230 -0
- package/runtime/python/lib/okstra/cli.sh +2 -49
- package/runtime/python/lib/okstra/globals.sh +21 -21
- package/runtime/python/lib/okstra/interactive.sh +7 -7
- package/runtime/python/okstra_ctl/clarification_items.py +3 -9
- package/runtime/python/okstra_ctl/consumers.py +53 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +0 -7
- package/runtime/python/okstra_ctl/i18n.py +73 -0
- package/runtime/python/okstra_ctl/improvement_lenses.py +44 -0
- package/runtime/python/okstra_ctl/index.py +1 -1
- package/runtime/python/okstra_ctl/paths.py +26 -20
- package/runtime/python/okstra_ctl/render.py +166 -207
- package/runtime/python/okstra_ctl/render_final_report.py +53 -10
- package/runtime/python/okstra_ctl/run.py +299 -108
- package/runtime/python/okstra_ctl/run_context.py +22 -0
- package/runtime/python/okstra_ctl/seeding.py +186 -0
- package/runtime/python/okstra_ctl/session.py +65 -7
- package/runtime/python/okstra_ctl/wizard.py +348 -127
- package/runtime/python/okstra_ctl/workflow.py +21 -2
- package/runtime/python/okstra_ctl/worktree.py +54 -1
- package/runtime/python/okstra_project/resolver.py +4 -3
- package/runtime/python/okstra_token_usage/report.py +2 -2
- package/runtime/schemas/final-report-v1.0.schema.json +22 -16
- package/runtime/skills/okstra-brief/SKILL.md +102 -218
- package/runtime/skills/okstra-convergence/SKILL.md +2 -3
- package/runtime/skills/okstra-inspect/SKILL.md +581 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +35 -15
- package/runtime/skills/okstra-run/SKILL.md +8 -7
- package/runtime/skills/okstra-schedule/SKILL.md +14 -157
- package/runtime/skills/okstra-setup/SKILL.md +28 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +16 -107
- package/runtime/templates/okstra.CLAUDE.md +104 -0
- package/runtime/templates/reports/brief.template.md +204 -0
- package/runtime/templates/reports/final-report.template.md +93 -98
- package/runtime/templates/reports/i18n/en.json +135 -0
- package/runtime/templates/reports/i18n/ko.json +135 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +18 -0
- package/runtime/templates/reports/improvement-discovery-input.template.md +78 -0
- package/runtime/templates/reports/schedule.template.md +12 -3
- package/runtime/templates/reports/task-brief.template.md +2 -2
- package/runtime/templates/worker-prompt-preamble.md +108 -0
- package/runtime/validators/lib/fixtures.sh +30 -0
- package/runtime/validators/lib/runners.sh +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +211 -0
- package/runtime/validators/validate-run.py +121 -26
- package/runtime/validators/validate-workflow.sh +2 -2
- package/runtime/validators/validate_improvement_report.py +275 -0
- package/src/config.mjs +18 -0
- package/src/install.mjs +41 -14
- package/src/setup.mjs +133 -1
- package/src/uninstall.mjs +27 -3
- package/runtime/skills/okstra-history/SKILL.md +0 -165
- package/runtime/skills/okstra-logs/SKILL.md +0 -173
- package/runtime/skills/okstra-report-finder/SKILL.md +0 -111
- package/runtime/skills/okstra-status/SKILL.md +0 -246
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
+
TASK_TYPE "${TASK_TYPE-}" \
|
|
106
106
|
OKSTRA_RUN_SEQ "$_run_seq" \
|
|
107
107
|
RUN_TIMESTAMP_ISO "${RUN_TIMESTAMP_ISO-}" \
|
|
108
|
-
|
|
109
|
-
|
|
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("
|
|
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("
|
|
141
|
-
lead_model=payload.get("
|
|
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
|
|
192
|
-
# dispatches of the same role
|
|
193
|
-
#
|
|
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 "$
|
|
202
|
-
original_caller_title=$(tmux display-message -p -t "$
|
|
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 "$
|
|
214
|
-
tmux select-pane -t "$
|
|
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 "$
|
|
222
|
-
tmux select-pane -t "$
|
|
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`
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
# `
|
|
237
|
-
#
|
|
238
|
-
# the same
|
|
239
|
-
#
|
|
246
|
+
# new pane carries the title `codex-<role>-<pid>-trace[from=<caller-pane>]`
|
|
247
|
+
# so the operator can map trace ↔ worker 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
|
|
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
|
-
|
|
245
|
-
|
|
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
|
|
253
|
-
#
|
|
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 "$
|
|
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="${
|
|
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
|
|
141
|
-
# dispatches of the same role
|
|
142
|
-
#
|
|
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 "$
|
|
151
|
-
original_caller_title=$(tmux display-message -p -t "$
|
|
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 "$
|
|
163
|
-
tmux select-pane -t "$
|
|
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 "$
|
|
171
|
-
tmux select-pane -t "$
|
|
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`
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
189
|
+
# implementation-specific. Title `gemini-<role>-<pid>-trace[from=<caller-pane>]`
|
|
190
|
+
# so the operator can map trace ↔ worker 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
|
-
|
|
186
|
-
|
|
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 "$
|
|
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="${
|
|
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
|
-
|
|
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
|
|
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))
|
package/runtime/bin/okstra.sh
CHANGED
|
@@ -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: ${
|
|
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 "$
|
|
106
|
+
--task-type "$TASK_TYPE"
|
|
107
107
|
--task-brief "$BRIEF_PATH"
|
|
108
108
|
)
|
|
109
109
|
[[ -n "${DIRECTIVE-}" ]] && PY_ARGS+=(--directive "$DIRECTIVE")
|