okstra 0.68.0 → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/okstra +18 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +3 -3
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +3 -3
- package/runtime/agents/workers/gemini-worker.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +2 -2
- package/runtime/prompts/launch.template.md +2 -2
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -0
- package/runtime/prompts/profiles/_implementation-executor.md +3 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -0
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +4 -4
- package/runtime/python/okstra_ctl/conformance.py +17 -0
- package/runtime/python/okstra_ctl/run.py +87 -17
- package/runtime/python/okstra_ctl/wizard.py +64 -18
- package/runtime/python/okstra_ctl/worktree.py +18 -0
- package/runtime/python/okstra_token_usage/collect.py +27 -0
- package/runtime/skills/okstra-convergence/SKILL.md +2 -2
- package/runtime/skills/okstra-report-writer/SKILL.md +6 -6
- package/runtime/skills/okstra-team-contract/SKILL.md +5 -5
- package/runtime/validators/validate-run.py +2 -2
- package/src/_python-helper.mjs +52 -0
- package/src/error-log.mjs +19 -0
- package/src/inject-report-index.mjs +22 -0
- package/src/render-final-report.mjs +22 -0
- package/src/render-views.mjs +9 -48
- package/src/spawn-followups.mjs +23 -0
- package/src/token-usage.mjs +3 -34
package/bin/okstra
CHANGED
|
@@ -41,6 +41,19 @@ const COMMANDS = new Map([
|
|
|
41
41
|
() => import("../src/render-bundle.mjs").then((m) => m.run),
|
|
42
42
|
],
|
|
43
43
|
["render-views", () => import("../src/render-views.mjs").then((m) => m.run)],
|
|
44
|
+
[
|
|
45
|
+
"render-final-report",
|
|
46
|
+
() => import("../src/render-final-report.mjs").then((m) => m.run),
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"inject-report-index",
|
|
50
|
+
() => import("../src/inject-report-index.mjs").then((m) => m.run),
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
"spawn-followups",
|
|
54
|
+
() => import("../src/spawn-followups.mjs").then((m) => m.run),
|
|
55
|
+
],
|
|
56
|
+
["error-log", () => import("../src/error-log.mjs").then((m) => m.run)],
|
|
44
57
|
["wizard", () => import("../src/wizard.mjs").then((m) => m.run)],
|
|
45
58
|
["token-usage", () => import("../src/token-usage.mjs").then((m) => m.run)],
|
|
46
59
|
["memory", () => import("../src/memory.mjs").then((m) => m.run)],
|
|
@@ -89,6 +102,11 @@ Introspection commands (JSON output, used by skills to avoid python heredocs):
|
|
|
89
102
|
token-usage Collect token usage for a run (wraps the installed
|
|
90
103
|
okstra-token-usage.py so skills avoid emitting
|
|
91
104
|
python3 "$HOME/..." invocations).
|
|
105
|
+
render-views Render slim AI + self-contained HTML views of a final report
|
|
106
|
+
render-final-report Render the markdown sibling of a final-report data.json
|
|
107
|
+
inject-report-index Add the top-of-report Index + scroll anchors to a report
|
|
108
|
+
spawn-followups Create follow-up task bundles from a final report
|
|
109
|
+
error-log Append run error events to the run error log
|
|
92
110
|
memory Store and find user-home conversation memory under
|
|
93
111
|
~/.okstra/memory-book.
|
|
94
112
|
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
package/runtime/agents/SKILL.md
CHANGED
|
@@ -246,13 +246,13 @@ After each worker terminates, BEFORE classifying its terminal status, verify the
|
|
|
246
246
|
After each worker terminates (any terminal status), if its errors sidecar exists, dump it to the run error log using the same resolved paths from the launch prompt:
|
|
247
247
|
|
|
248
248
|
```bash
|
|
249
|
-
|
|
249
|
+
okstra error-log append-from-worker \
|
|
250
250
|
--sidecar <absolute-sidecar-path-from-launch-prompt> \
|
|
251
251
|
--out <absolute-errors-log-path-from-launch-prompt> \
|
|
252
252
|
--task-key <taskKey> --agent <agent> --agent-role <role> --model <model>
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
-
For Codex/Gemini wrappers: if the CLI returns non-zero, times out, or hits a rate limit, immediately call `okstra
|
|
255
|
+
For Codex/Gemini wrappers: if the CLI returns non-zero, times out, or hits a rate limit, immediately call `okstra error-log append-observed --error-type cli-failure ...` with the captured exit code, duration, message, and stderr excerpt. The wrapper subagent does this from inside its own Bash tool — Lead does NOT need to re-record. Token usage is NOT available from Agent tool results in real time; it is collected post-hoc at the start of Phase 7.
|
|
256
256
|
|
|
257
257
|
## Phase 5.5: Convergence loop
|
|
258
258
|
|
|
@@ -271,7 +271,7 @@ When `task-manifest.json` does not set `convergence.maxRounds`, lead MUST resolv
|
|
|
271
271
|
|
|
272
272
|
**Confirmed findings are pruned from the queue.** Findings classified as `full-consensus`, `partial-consensus`, or `worker-unique` MUST NOT appear in any subsequent round's reverify prompt for any worker. `contested` is a final classification assigned only when the last executed round completes and the queue is still non-empty — it is NEVER an intermediate queue label.
|
|
273
273
|
|
|
274
|
-
If any re-verification batch yields a `verification-error` terminal status, or a worker result fails the contract, Lead MUST record one event per violation via `
|
|
274
|
+
If any re-verification batch yields a `verification-error` terminal status, or a worker result fails the contract, Lead MUST record one event per violation via `okstra error-log append-observed --error-type contract-violation --agent <offending-agent> ...`. Use `agent: "claude-lead"` only when the violation is detected internally without a specific worker.
|
|
275
275
|
|
|
276
276
|
If convergence is disabled, proceed directly to Phase 6 with the raw worker results.
|
|
277
277
|
|
|
@@ -104,7 +104,7 @@ If you find yourself thinking "let me double-check section 3" or "I should read
|
|
|
104
104
|
|
|
105
105
|
## Error reporting
|
|
106
106
|
|
|
107
|
-
This agent is responsible for recording its own tool failures via `
|
|
107
|
+
This agent is responsible for recording its own tool failures via `okstra error-log`:
|
|
108
108
|
|
|
109
109
|
**Path extraction (BLOCKING).** Before recording anything, extract the absolute sidecar path from the lead's dispatch prompt body:
|
|
110
110
|
|
|
@@ -103,7 +103,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
|
|
|
103
103
|
|
|
104
104
|
c. **Result-file existence check (exit 0 only).** If `exit_code == 0` BUT no file exists at the extracted Result Path, the Codex CLI returned 0 without producing the analysis artifact. Observed failure mode: the CLI streams analysis prose on stdout, hits its token budget or a sandbox EPERM mid-`Write`, and exits 0 with the artifact never persisted. Forwarding the partial stdout silently degrades lead synthesis (the case that motivated this rule), so this path is required.
|
|
105
105
|
1. Capture the final ~10 lines of the wrapper's live log for diagnostics — single Bash call: `tail -n 10 "${prompt_path%.md}.log"` (substitute the literal absolute prompt-history path; the wrapper writes the log next to it per the §"trace pane" comment in `okstra-codex-exec.sh`). Write the captured lines to a temp file (e.g. `<errors-sidecar-dir>/codex-result-missing-tail.txt`) so `--stderr-excerpt-file` can reference it.
|
|
106
|
-
2. Record a `cli-failure` event directly to the run-level error log via the exact `okstra
|
|
106
|
+
2. Record a `cli-failure` event directly to the run-level error log via the exact `okstra error-log append-observed` template in §"Error reporting" — substitute `--exit-code 0`, `--duration-ms <observed-ms>`, `--message "okstra-codex-exec.sh exited 0 but no result file at <abs-path>"`, and `--stderr-excerpt-file <temp-tail-path>`.
|
|
107
107
|
3. Return `CODEX_RESULT_MISSING: codex exited 0 but result file absent at <abs-path>` instead of the raw stdout. The lead is responsible for deciding redispatch per `okstra-team-contract` "Lead Redispatch Policy on Result-Missing".
|
|
108
108
|
|
|
109
109
|
d. **Normal return.** Otherwise (`exit_code == 0` AND result file exists), return the wrapper's accumulated stdout from `BashOutput`, prefixed by exactly one model-identity line copied verbatim from the `**Model:** Codex worker, <execution-value>` line in the lead prompt (per Worker Preamble → "Return message to the lead"):
|
|
@@ -175,7 +175,7 @@ This contract mirrors the `okstra-team-contract` skill's Worker Output Contract
|
|
|
175
175
|
## Error reporting
|
|
176
176
|
|
|
177
177
|
The wrapper agent (this Codex worker subagent) is responsible for recording
|
|
178
|
-
two kinds of errors via `
|
|
178
|
+
two kinds of errors via `okstra error-log`:
|
|
179
179
|
|
|
180
180
|
**Path extraction (BLOCKING).** Before recording anything, extract the
|
|
181
181
|
following two absolute paths verbatim from the lead's dispatch prompt body:
|
|
@@ -207,7 +207,7 @@ and the run-level error log staying empty.
|
|
|
207
207
|
the dispatched `bash_id`:
|
|
208
208
|
|
|
209
209
|
```bash
|
|
210
|
-
|
|
210
|
+
okstra error-log append-observed \
|
|
211
211
|
--out "<absolute-errors-log-path-from-lead-prompt>" \
|
|
212
212
|
--task-key "<task-key>" \
|
|
213
213
|
--phase "<phase>" \
|
|
@@ -103,7 +103,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
|
|
|
103
103
|
|
|
104
104
|
c. **Result-file existence check (exit 0 only).** If `exit_code == 0` BUT no file exists at the extracted Result Path, the Gemini CLI returned 0 without producing the analysis artifact. Observed failure mode: the CLI streams analysis prose on stdout, hits its token budget or a sandbox EPERM mid-`Write`, and exits 0 with the artifact never persisted. Forwarding the partial stdout silently degrades lead synthesis (the case that motivated this rule), so this path is required.
|
|
105
105
|
1. Capture the final ~10 lines of the wrapper's live log for diagnostics — single Bash call: `tail -n 10 "${prompt_path%.md}.log"` (substitute the literal absolute prompt-history path; the wrapper writes the log next to it per the §"trace pane" comment in `okstra-gemini-exec.sh`). Write the captured lines to a temp file (e.g. `<errors-sidecar-dir>/gemini-result-missing-tail.txt`) so `--stderr-excerpt-file` can reference it.
|
|
106
|
-
2. Record a `cli-failure` event directly to the run-level error log via the exact `okstra
|
|
106
|
+
2. Record a `cli-failure` event directly to the run-level error log via the exact `okstra error-log append-observed` template in §"Error reporting" — substitute `--exit-code 0`, `--duration-ms <observed-ms>`, `--message "okstra-gemini-exec.sh exited 0 but no result file at <abs-path>"`, and `--stderr-excerpt-file <temp-tail-path>`.
|
|
107
107
|
3. Return `GEMINI_RESULT_MISSING: gemini exited 0 but result file absent at <abs-path>` instead of the raw stdout. The lead is responsible for deciding redispatch per `okstra-team-contract` "Lead Redispatch Policy on Result-Missing".
|
|
108
108
|
|
|
109
109
|
d. **Normal return.** Otherwise (`exit_code == 0` AND result file exists), return the wrapper's accumulated stdout from `BashOutput`, prefixed by exactly one model-identity line copied verbatim from the `**Model:** Gemini worker, <execution-value>` line in the lead prompt (per Worker Preamble → "Return message to the lead"):
|
|
@@ -175,7 +175,7 @@ This contract mirrors the `okstra-team-contract` skill's Worker Output Contract
|
|
|
175
175
|
## Error reporting
|
|
176
176
|
|
|
177
177
|
The wrapper agent (this Gemini worker subagent) is responsible for recording
|
|
178
|
-
two kinds of errors via `
|
|
178
|
+
two kinds of errors via `okstra error-log`:
|
|
179
179
|
|
|
180
180
|
**Path extraction (BLOCKING).** Before recording anything, extract the
|
|
181
181
|
following two absolute paths verbatim from the lead's dispatch prompt body:
|
|
@@ -207,7 +207,7 @@ and the run-level error log staying empty.
|
|
|
207
207
|
the dispatched `bash_id`:
|
|
208
208
|
|
|
209
209
|
```bash
|
|
210
|
-
|
|
210
|
+
okstra error-log append-observed \
|
|
211
211
|
--out "<absolute-errors-log-path-from-lead-prompt>" \
|
|
212
212
|
--task-key "<task-key>" \
|
|
213
213
|
--phase "<phase>" \
|
|
@@ -101,9 +101,9 @@ Rules (the schema enforces most of these — they are listed here so you know *w
|
|
|
101
101
|
- Cite file paths and line numbers in every `evidence.primary[].source` / `consensus[].evidence` cell.
|
|
102
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
103
|
- For `implementation-planning`, populate `implementationPlanning.requirementCoverage` with one row per concrete requirement from the brief / packet, using IDs `R-001`, `R-002`, ... in source order. `coveredBy` MUST name the specific Option Candidate plus Stage/Step that satisfies the requirement. Use `status: "covered"` only when the report's plan actually covers it; otherwise use `gap` or `blocked C-NNN` and ensure the corresponding `Clarification Items` row blocks approval. Do not collapse this into `ticketCoverage`; ticket coverage is not requirement coverage.
|
|
104
|
-
- When the `Task Type` is `improvement-discovery`, populate `## 5.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. `improvement-discovery` is NOT in the data.json schema enum, so author its markdown directly (not via `okstra-render-final-report.py`). Immediately after writing the markdown, run (`Bash`): `
|
|
104
|
+
- When the `Task Type` is `improvement-discovery`, populate `## 5.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. `improvement-discovery` is NOT in the data.json schema enum, so author its markdown directly (not via `okstra-render-final-report.py`). Immediately after writing the markdown, run (`Bash`): `okstra inject-report-index <markdown path> --report-language <en|ko>`. That adds the top-of-report Index plus `I-NNN` / `C-NNN` scroll anchors; the run validator fails the report when the Index anchor is absent.
|
|
105
105
|
|
|
106
|
-
Write the data.json with your `Write` tool
|
|
106
|
+
Write the data.json (and the audit sidecar `.md`) with your `Write` tool — that is the canonical authoring path, and okstra ships no hook that blocks `.md` writes (its only settings hook is the `SessionEnd` trace-cleanup; the coding-preflight hook emits reminders but never blocks). A Bash heredoc is acceptable ONLY when a specific `Write` call is genuinely rejected by the host environment, and it MUST produce byte-identical content — do not reach for it pre-emptively. Then invoke the renderer (`Bash`): `okstra render-final-report <data.json path>`. Confirm both files exist and respond with a short status line prefixed by your model identity, copied verbatim from the `**Model:** Report writer worker, <modelExecutionValue>` line in your dispatch prompt (per Worker Preamble → "Return message to the lead"):
|
|
107
107
|
|
|
108
108
|
```
|
|
109
109
|
**Model:** Report writer worker, <modelExecutionValue>
|
|
@@ -67,8 +67,8 @@ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at
|
|
|
67
67
|
- When dispatching any worker you MUST inject **two header lines** into the dispatch prompt body so the worker subagent can record errors without guessing paths:
|
|
68
68
|
- `**Errors log path:** <absolute run-level errors log path>`
|
|
69
69
|
- `**Errors sidecar path:** <absolute per-worker sidecar path matching the dispatched worker>`
|
|
70
|
-
- These lines are the canonical contract — worker subagents extract them verbatim and pass them to `okstra
|
|
71
|
-
- After each worker terminates, dump its sidecar into the run-level errors log via `
|
|
70
|
+
- These lines are the canonical contract — worker subagents extract them verbatim and pass them to `okstra error-log append-observed --out ...` (run-level cli-failure / contract-violation events) and to their internal sidecar writes (worker-reported tool-failure events) respectively.
|
|
71
|
+
- After each worker terminates, dump its sidecar into the run-level errors log via `okstra error-log append-from-worker --sidecar <sidecar-path> --out <run-errors-log-path> --task-key {{TASK_KEY}} --agent <worker-id> --agent-role worker --model <assigned-model-execution-value>` (per `okstra-team-contract` Worker Output Contract).
|
|
72
72
|
|
|
73
73
|
## Executor Worktree
|
|
74
74
|
|
|
@@ -44,6 +44,7 @@ are collected and convergence finished. Phase 1-5 do not need it.
|
|
|
44
44
|
git diff <base>..HEAD | grep -E '^\+[^+].*\b(TBD|TODO|FIXME|XXX|implement later|handle edge cases|similar to|placeholder)\b' || echo 'clean'
|
|
45
45
|
```
|
|
46
46
|
Only newly-added lines (those starting with `+` and not part of the `+++` header) are inspected. If output is anything other than `clean`, the run MUST either remove the placeholders before finalising or record an explicit justification per occurrence in the final report.
|
|
47
|
+
7. **Stage-foreign literal scrub** — when the report-writer modelled this stage's `data.json` on another stage's report (a common shortcut for structural consistency), stage-specific literals get copied verbatim and silently misattribute this run. Confirm every branch name, commit SHA, stageKey, and stage number in the report resolves to **this** run's stage `<N>` — its worktree branch is `<prefix>-<task-id>-s<N>`, its stageKey `<task-id>-stage-<N>`. Sweep the report for any `-s<M>` / `stage-<M>` / `Stage <M>` where `M ≠ N` and for SHAs not in this run's `Commit list`; each hit is a copy-from-other-stage defect to correct before finalising.
|
|
47
48
|
|
|
48
49
|
## Lead post-stage persistence (BLOCKING — runs after the Executor emits `### Stage Carry Evidence`)
|
|
49
50
|
|
|
@@ -24,12 +24,13 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
|
|
|
24
24
|
- **Language-agnostic principles that ALWAYS bind (the TDD loop below MUST satisfy them):** (1) no self-mocking of the SUT — stub/spy only injected collaborators, never the subject's own methods; (2) behavioral assertions on outcomes (return value, state, persisted rows, events, boundary calls) — never `toHaveBeenCalled*` on an internal helper as the only/primary assertion; (3) truthful names — a `get*` / `find*` that writes/inserts, or a name encoding the caller's use-case (`*ForInit`) or hiding a domain rule (`findValid*`), is a defect; (4) single-purpose functions ≤50 effective lines, plain-English readability.
|
|
25
25
|
- **Graceful degradation (codex / gemini executor runtimes, or any runtime where the `~/.claude/skills/okstra-coding-preflight/` files are absent or unreadable):** do NOT skip the gate — apply the agnostic principles above plus the project's own `CLAUDE.md` / `CONTRIBUTING` / formatter+lint config, and record `coding-conventions: skill-unavailable → applied <project rules + agnostic principles>` in the final report. Never claim a skill read that did not happen.
|
|
26
26
|
- **CLI executor transcription (BLOCKING when the executor provider is `codex` or `gemini`):** the executor CLI process does NOT share the lead's context — a gate that stays in lead memory never reaches it. The lead MUST copy this entire "Coding-conventions preflight" bullet tree (file-read instructions, project review rule packs, agnostic principles, graceful degradation) verbatim into the dispatched executor prompt body. Enforcement: the CLI wrapper agents refuse an implementation-Executor dispatch whose persisted prompt lacks the literal heading `Coding-conventions preflight`, returning `<SENTINEL_PREFIX>_PREFLIGHT_MISSING` (see `agents/workers/_cli-wrapper-template.md` → Prompt Composition).
|
|
27
|
+
- **Non-interactive auto-execution (BLOCKING when the executor provider is `codex` or `gemini`).** A CLI executor runs head-less (`codex exec` / gemini equivalent) — there is no human at the keyboard. Skills loaded during the run (tdd, coding-preflight, and others) contain "get user approval", "state your plan to the user and wait", or "ask before proceeding" gates written for interactive sessions; in this run those gates are **already satisfied** by the upstream `implementation-planning` approval (the plan this stage executes was human-approved). The executor MUST NOT stop to request approval, MUST NOT end its turn after only producing a plan, and MUST carry the stage through end-to-end — RED → GREEN → refactor → per-step commits → `### Stage Carry Evidence`. The ONLY skill step to skip is the interactive user-approval prompt itself; every other skill rule (TDD discipline, conventions, real-IO isolation) still binds. The lead MUST transcribe this bullet verbatim into the dispatched CLI executor prompt (same reason as the preflight transcription rule above — the CLI process does not share lead context). Stopping early for approval in a head-less run is the observed empty-exit failure (exit 0, no diff): treat it as `contract-violated`.
|
|
27
28
|
- **Mandatory TDD loop**: BEFORE the first `Edit` or `Write` call, the executor MUST apply a red-green-refactor loop for every code change in this run. This is required; skipping it is a `contract-violated` outcome. This governs HOW each step is executed (failing test first → minimal implementation → refactor); it does not override the approved plan's WHAT/file scope.
|
|
28
29
|
- Order of operations per plan step: (1) write/extend the test that captures the step's acceptance criterion and confirm it fails for the right reason, (2) commit the failing test (`test(<scope>): ...`), (3) implement the minimum change to make it pass, (4) commit the implementation (`feat|fix(<scope>): ...`), (5) refactor without changing behaviour and commit separately if any cleanup is made (`refactor(<scope>): ...`). The failing-then-passing transition between steps (2) and (4) is the `TDD evidence` required by the final report.
|
|
29
30
|
- Doc-only / config-only / pure-rename steps that have no observable runtime behaviour are exempt from the failing-test requirement, but the executor MUST cite the exemption per step in the final report (`TDD exemption: <reason>`).
|
|
30
31
|
- When the touched area has no existing test harness, the executor MUST stand up the minimum harness needed to host one regression test for this run rather than skipping TDD entirely. Record the harness-bootstrap step as an `Out-of-plan edit` if it is not in the plan.
|
|
31
32
|
- **DB / IO / SQL changes require real execution — mock-only is NOT validation evidence:** when this run's diff touches DB/IO/SQL (ORM / query-builder code — sequelize / typeorm / prisma / knex / raw SQL — `*.repository.*`, model/entity files, `migrations/**`, `*.sql`, or any changed query string), a mocked unit test cannot observe the SQL the query builder actually emits — a mocked suite once passed while `count({ col: 'FontFamily.fontFamily' })` threw `Unknown column` on the real DB. The executor MUST run the change against a real (or faithful-replica) datastore — the `db-test` validation step (plan `validation` db step, else `project.json.qaCommands.db-test`), targeting a **local / replica** DB — and cite its exact command + exit code in the final report's `Validation evidence`. If no real DB / `db-test` command is reachable, do NOT claim the change verified: label the DB portion `정적 분석상 …, 미검증(실행 안 함)` in the report, surface it in the routing recommendation, and never downplay the real run as "too heavy". `git push` stays forbidden (universal list); the unverified DB state is carried forward so `final-verification` cannot accept it and `release-handoff` cannot push.
|
|
32
|
-
- **Real-IO test isolation (BLOCKING).** A test that exercises a **real** datastore, HTTP endpoint, external service, message queue, or filesystem — a live DB connection / DSN, a real `fetch` / `axios` / `http` request, an actual S3 / queue client, anything the project's normal CI test suite cannot run because that backend is absent — MUST be written under the task's qa directory `<task_root>/qa/` (the `TASK_QA_PATH` token; same directory that holds the Tier 3 conformance manifest). It MUST NOT be written into the project source test tree — `src/**`, `test/**`, `tests/**`, `**/__test__/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, or anywhere the project's lint/test globs collect. Two reasons: (a) the project's CI / normal suite has no real DB or network, so a real-IO test placed in source silently breaks the pipeline; (b) it is an okstra verification artifact, and the artifact-home rule confines okstra outputs to `.okstra/`. **The dividing line is the IO, not the intent:** a unit test that stubs/spies only *injected collaborators* (mock — no real socket, no real DB handle) is a TDD red-green artifact and stays in source; the moment a test opens a real connection or makes a real network call it belongs in qa. A stage's real-IO requirement check is a Tier 3 conformance script under `<task_root>/qa/` (declared via the implementation-planning conformance entry) — never smuggle real IO into a `*.spec.*` in source to make it run "as a unit test". The `db-test` real-execution gate above is satisfied by the conformance/db-test path against the replica, NOT by adding a live-DB `*.spec.*` to the project suite.
|
|
33
|
+
- **Real-IO test isolation (BLOCKING).** A test that exercises a **real** datastore, HTTP endpoint, external service, message queue, or filesystem — a live DB connection / DSN, a real `fetch` / `axios` / `http` request, an actual S3 / queue client, anything the project's normal CI test suite cannot run because that backend is absent — MUST be written under the task's qa directory `<task_root>/qa/` (the `TASK_QA_PATH` token; same directory that holds the Tier 3 conformance manifest). It MUST NOT be written into the project source test tree — `src/**`, `test/**`, `tests/**`, `**/__test__/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, or anywhere the project's lint/test globs collect. Two reasons: (a) the project's CI / normal suite has no real DB or network, so a real-IO test placed in source silently breaks the pipeline; (b) it is an okstra verification artifact, and the artifact-home rule confines okstra outputs to `.okstra/`. **The dividing line is the IO, not the intent:** a unit test that stubs/spies only *injected collaborators* (mock — no real socket, no real DB handle) is a TDD red-green artifact and stays in source; the moment a test opens a real connection or makes a real network call it belongs in qa. A stage's real-IO requirement check is a Tier 3 conformance script under `<task_root>/qa/` (declared via the implementation-planning conformance entry) — never smuggle real IO into a `*.spec.*` in source to make it run "as a unit test". The `db-test` real-execution gate above is satisfied by the conformance/db-test path against the replica, NOT by adding a live-DB `*.spec.*` to the project suite. **These qa artifacts stay untracked — never commit them.** `.okstra/**` is gitignored (the artifact-home rule); conformance scripts and their results are *executed* and recorded in the carry sidecar / verifier result, never written into git history. A committed `.okstra/qa` file is a stage-branch defect that leaks okstra internals into the eventual PR (see the `git add` rules below).
|
|
33
34
|
- re-read the approved plan end-to-end and parse the `## 5.5 Stage Map`. Read the **Stage** injected in the launch prompt (`Stage for this implementation run`): the single stage number this run owns. The runtime already selected and reserved this stage (one run = one stage) — do NOT recompute the start stage from `consumers.jsonl`.
|
|
34
35
|
- load every `runs/<plan-key>/carry/stage-<i>.json` for `i ∈ depends-on(this stage)` and inject them into the executor's working context as "runtime carry-in". For a `depends-on (none)` stage, no sidecar load — task-brief only.
|
|
35
36
|
- this stage's `depends-on` are all already `status:done`. Its file list, step order, Stage Validation commands, Stage Exit Contract, and rollback path are the authoritative scope.
|
|
@@ -58,6 +59,7 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
|
|
|
58
59
|
- read-only inspection commands: `git status`, `git diff`, `git log`, `grep`, `rg`, `find`, `cat`, `ls`, file Read tools
|
|
59
60
|
- build, lint, type-check, and test commands (`npm test`, `pytest`, `go build`, `cargo test`, `bash -n`, etc.)
|
|
60
61
|
- **local git operations only**: `git add`, `git commit`. Prefer small commits keyed to plan steps.
|
|
62
|
+
- **No okstra artifacts in commits (BLOCKING).** Never use `git add -f`. Before every `git commit`, run `git diff --cached --name-only` and confirm it contains zero `.okstra/` paths (and zero `.project-docs/` paths when the legacy symlink is present). `.okstra/**` is gitignored; force-staging it onto the stage branch is the one way these verification artifacts reach the upstream PR. Conformance/qa evidence belongs in the carry sidecar and verifier result — committing it is never correct, even when a step's instructions seem to ask for it.
|
|
61
63
|
- **Commit message format (mandatory)**: every commit message MUST follow Conventional Commits — `<type>(<scope>): <subject>` for the first line, optional body separated by a blank line, optional footer. Constraints:
|
|
62
64
|
- `<type>` MUST be one of: `feat` / `fix` / `perf` / `revert` / `deps` / `docs` / `refactor` / `build` / `ci` / `chore` / `test`. When the repo is `release-please`-managed, this aligns the commit with a configured changelog section.
|
|
63
65
|
- `<scope>` SHOULD be the plan step identifier or the primary module touched (e.g. `feat(report-writer): ...`). Omit the parentheses only when no meaningful scope applies.
|
|
@@ -96,6 +96,7 @@ Re-running commands proves the diff *builds and passes*; it does NOT prove the d
|
|
|
96
96
|
- **Tautological delegation assertion:** a test asserts the SUT result equals a direct call to the same pure helper/collaborator that the SUT delegates to, instead of asserting an independent literal value or observable state.
|
|
97
97
|
- **Untruthful name:** a read-named function (`get*` / `find*` / `load*`) that writes/inserts/mutates; an adapter or repository name encoding the caller's use-case (`*ForInit`) or hiding a domain rule (`findValid*` / `findActive*`).
|
|
98
98
|
- **Hexagonal (only when the overlay is loaded):** business logic inside a port body; an adapter method that is not pure I/O (post-fetch JS filtering on domain state, domain-rule evaluation); a domain object declared outside the `domain/` boundary.
|
|
99
|
+
- **okstra artifact committed to the branch:** any path in the `git diff --name-only <base>...HEAD` enumeration that lives under `.okstra/` (or `.project-docs/` when the legacy symlink is present). `.okstra/**` is gitignored, so a committed okstra file means the executor force-staged it (`git add -f`) — leaking verification artifacts (qa scripts, conformance results) into the eventual PR. Cite the path; recommend `git rm --cached <path>` to untrack it while keeping the file on disk. Conformance/qa evidence belongs in the carry sidecar / verifier result, never in git history.
|
|
99
100
|
- **Real-IO test in source tree:** a changed/added test under the project source test tree — `src/**`, `test/**`, `tests/**`, `**/__test__/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*` — that opens a **real** DB connection / DSN, makes a real `fetch` / `axios` / `http` request, or otherwise hits real external IO without mocking the injected collaborator (a live handle, not a stub/spy). Real-IO tests MUST live under `<task_root>/qa/` per the executor's *Real-IO test isolation* rule — a live-IO test in source silently breaks the project's CI suite and violates the artifact-home rule. Cite the test file + the real-IO line; recommend moving it to `<task_root>/qa/` (or declaring it as a Tier 3 conformance script). Mock-only unit tests in source are NOT a hit.
|
|
100
101
|
- **Advisory findings (recorded as recommendations; verdict MAY still PASS):** function >50 effective lines, a single body mixing read+write stages, weak readability, a missing-but-non-critical outcome assertion, newly orphaned private/public code that is safe to remove but not on a critical path, or weak-but-not-misleading names. These land in the verifier result as `should-fix` / `nit` recommendations, not as a `FAIL`.
|
|
101
102
|
- **Output.** Every finding — blocking or advisory — is a structured item in the verifier's worker result (`path:line`, rule, severity, suggested fix) so it carries into Phase 5.5 convergence and the final report. A blocking hit sets the verifier verdict to `FAIL` with the rule cited, using the same verdict machinery as the Discrepancy rule above. `Claude lead` MUST NOT silently downgrade a cited blocking finding to advisory during synthesis; an override requires a concrete cited reason, exactly as for the Discrepancy rule.
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
- `Consensus` cells in `## 5.9 Improvement Candidates` use the table enum exactly: `full`, `partial`, `contested`, `worker-unique`. Map convergence's `full-consensus` / `partial-consensus` labels to `full` / `partial` before writing the table.
|
|
34
34
|
- `## 7. Final Verdict` Verdict Token ∈ {`candidates-ready`, `no-candidates`, `blocked`}; Direction `routing`; Next Step "사용자에게 후보 K개 선택 의뢰 (## 5.9 표 참조)"
|
|
35
35
|
- `## 3. Recommended Next Steps` first entry summarises per-candidate routing and proposes new task-key names of the form `<task-group>/imp-<Cand-ID>`
|
|
36
|
-
- this report is authored free-form (improvement-discovery is not in the data.json schema enum); after the markdown is written, the report-writer runs `
|
|
36
|
+
- this report is authored free-form (improvement-discovery is not in the data.json schema enum); after the markdown is written, the report-writer runs `okstra inject-report-index <report.md> --report-language <en|ko>` to add the top-of-report Index + `I-NNN`/`C-NNN` scroll anchors. The run validator fails the report when the Index anchor is missing.
|
|
37
37
|
- Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
|
|
38
38
|
- if scan-scope or priority-lenses cannot be made concrete during Phase 1.5, end the run with Verdict Token `blocked`, populate `## 1. Clarification Items` with `Blocks=next-phase` rows, and do not run worker dispatch
|
|
39
39
|
- every clarification row carries a recommended answer + one-line rationale inside the `Expected form` cell
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"echo_template": "approved-plan: {value}"
|
|
169
169
|
},
|
|
170
170
|
"approve_plan_confirm": {
|
|
171
|
-
"label": "이 플랜으로
|
|
171
|
+
"label": "이 플랜으로 진행할까요?\n {path}\n· 예 — 진행합니다. 플랜이 아직 승인 전이면 지금 data.json(정본) + 리포트를 함께 approved 로 처리한 뒤 진행합니다. (markdown 만 손으로 고치면 일관성 검증에서 거부되므로 이 경로로 승인하세요.)\n· 아니오 — 진행하지 않습니다.",
|
|
172
172
|
"echo_template": "approve-plan: {value}",
|
|
173
173
|
"options": {
|
|
174
174
|
"yes": "예 — 승인하고 진행",
|
|
@@ -179,16 +179,16 @@
|
|
|
179
179
|
"approved": "approved-plan: {path} (승인·진행 확인됨)"
|
|
180
180
|
},
|
|
181
181
|
"errors": {
|
|
182
|
-
"declined": "진행을 선택하지 않으면
|
|
182
|
+
"declined": "진행을 선택하지 않으면 다음 단계로 넘어갈 수 없습니다. 진행(예)하거나 위저드를 종료하세요.",
|
|
183
183
|
"still_unapproved": "approve-plan: 승인 처리 후에도 승인 상태가 아닙니다 (data.json/markdown 불일치): {path}"
|
|
184
184
|
}
|
|
185
185
|
},
|
|
186
186
|
"stage_pick": {
|
|
187
187
|
"label": "stage 범위를 선택하세요. auto 는 전체 task(모든 stage)를, 특정 번호는 해당 stage 만 대상으로 합니다.",
|
|
188
|
+
"label_final_verification": "검증할 implementation stage 를 선택하세요.",
|
|
188
189
|
"echo_template": "stage: {value}",
|
|
189
190
|
"options": {
|
|
190
|
-
"auto": "auto (다음 미완료 stage)"
|
|
191
|
-
"auto_final_verification": "auto (전체 task — 모든 stage 머지 후 한 번)"
|
|
191
|
+
"auto": "auto (다음 미완료 stage)"
|
|
192
192
|
}
|
|
193
193
|
},
|
|
194
194
|
"directive_pick": {
|
|
@@ -258,6 +258,23 @@ def apply_qa_waiver(manifest: object, stage_key: str, reason: str, *, at: str,
|
|
|
258
258
|
return False
|
|
259
259
|
|
|
260
260
|
|
|
261
|
+
def clear_qa_waiver(manifest: object, stage_key: str) -> bool:
|
|
262
|
+
"""stage_key entry 의 `waiver` 를 제거한다(in place). 제거했으면 True.
|
|
263
|
+
|
|
264
|
+
한 stage 의 새 run 이 시작될 때, 그 stage entry 에 남아 있던 이전 run 의
|
|
265
|
+
waiver(예: all-gate run 이 미래 stage 를 미리 waive 한 것)는 stale 다 —
|
|
266
|
+
그대로 두면 verifier 가 conformance 를 skip 해 마스킹된다. 이 run 이 실제로
|
|
267
|
+
검증하도록 제거한다. 사용자가 이번 run 에 같은 stage 를 명시 waive 한
|
|
268
|
+
경우(--qa-waiver)는 호출 측에서 걸러 보존한다."""
|
|
269
|
+
entries = manifest.get("entries") if isinstance(manifest, dict) else None
|
|
270
|
+
if not isinstance(entries, list):
|
|
271
|
+
return False
|
|
272
|
+
for entry in entries:
|
|
273
|
+
if isinstance(entry, dict) and entry.get("stageKey") == stage_key:
|
|
274
|
+
return entry.pop("waiver", None) is not None
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
|
|
261
278
|
def manifest_required_surfaces(manifest: object) -> set[str]:
|
|
262
279
|
"""매니페스트 전 entry 의 `requires` 합집합 — 선언된 surface 집합."""
|
|
263
280
|
entries = manifest.get("entries") if isinstance(manifest, dict) else None
|
|
@@ -81,7 +81,11 @@ from .workers import (
|
|
|
81
81
|
)
|
|
82
82
|
from .workflow import compute_workflow_state
|
|
83
83
|
from .locks import worktree_provision_mutex
|
|
84
|
-
from .worktree import
|
|
84
|
+
from .worktree import (
|
|
85
|
+
WorktreeProvision,
|
|
86
|
+
okstra_clean_gate_excludes,
|
|
87
|
+
provision_task_worktree,
|
|
88
|
+
)
|
|
85
89
|
|
|
86
90
|
# Frontmatter approval-flag matcher.
|
|
87
91
|
#
|
|
@@ -1151,6 +1155,46 @@ def _apply_qa_waiver_if_requested(inp: "PrepareInputs", project_root: Path) -> N
|
|
|
1151
1155
|
manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n")
|
|
1152
1156
|
|
|
1153
1157
|
|
|
1158
|
+
def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: int) -> None:
|
|
1159
|
+
"""A fresh `implementation` run of stage N must not inherit a waiver left on
|
|
1160
|
+
its conformance entry by an earlier run (e.g. an all-gate run that pre-waived
|
|
1161
|
+
future stages, or an abandoned attempt). A stale waiver makes the verifier
|
|
1162
|
+
skip Tier 3 conformance and silently mask this stage, so clear it — UNLESS
|
|
1163
|
+
the user re-waived this exact stage for this run via `--qa-waiver` (already
|
|
1164
|
+
applied upstream in `_apply_qa_waiver_if_requested`)."""
|
|
1165
|
+
from .conformance import clear_qa_waiver, parse_qa_waiver_arg
|
|
1166
|
+
from .paths import task_dir
|
|
1167
|
+
manifest_path = (
|
|
1168
|
+
task_dir(project_root, inp.task_group, inp.task_id)
|
|
1169
|
+
/ "qa" / "conformance-manifest.json"
|
|
1170
|
+
)
|
|
1171
|
+
if not manifest_path.is_file():
|
|
1172
|
+
return
|
|
1173
|
+
manifest = json.loads(manifest_path.read_text())
|
|
1174
|
+
entries = manifest.get("entries") if isinstance(manifest, dict) else None
|
|
1175
|
+
if not isinstance(entries, list):
|
|
1176
|
+
return
|
|
1177
|
+
# The manifest stageKey is `<task-id>-stage-<N>` authored by planning; match
|
|
1178
|
+
# on the `-stage-<N>` suffix so we do not assume the task-id's exact form.
|
|
1179
|
+
suffix = f"-stage-{stage}"
|
|
1180
|
+
stage_key = next(
|
|
1181
|
+
(e["stageKey"] for e in entries
|
|
1182
|
+
if isinstance(e, dict) and isinstance(e.get("stageKey"), str)
|
|
1183
|
+
and e["stageKey"].endswith(suffix)),
|
|
1184
|
+
None,
|
|
1185
|
+
)
|
|
1186
|
+
if stage_key is None:
|
|
1187
|
+
return
|
|
1188
|
+
if inp.qa_waiver:
|
|
1189
|
+
parsed = parse_qa_waiver_arg(inp.qa_waiver)
|
|
1190
|
+
if parsed is not None and parsed[0] == stage_key:
|
|
1191
|
+
return # user intentionally waived this stage for this run
|
|
1192
|
+
if clear_qa_waiver(manifest, stage_key):
|
|
1193
|
+
manifest_path.write_text(
|
|
1194
|
+
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n"
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
|
|
1154
1198
|
def _register_and_check_project(project_root: Path, inp: PrepareInputs) -> None:
|
|
1155
1199
|
"""project.json self-registration + (implementation 한정) qaCommands gate 검증."""
|
|
1156
1200
|
from okstra_project import ResolverError
|
|
@@ -1493,10 +1537,22 @@ def _is_ancestor(cwd, commit, head) -> bool:
|
|
|
1493
1537
|
|
|
1494
1538
|
|
|
1495
1539
|
def _is_dirty_excluding_okstra(cwd) -> bool:
|
|
1496
|
-
|
|
1540
|
+
excludes = [f":(exclude){p}" for p in okstra_clean_gate_excludes(Path(cwd))]
|
|
1541
|
+
out = _git_out(cwd, "status", "--short", "--", ".", *excludes)
|
|
1497
1542
|
return bool(out.strip())
|
|
1498
1543
|
|
|
1499
1544
|
|
|
1545
|
+
def _single_stage_final_verification_worktree(inp: "PrepareInputs") -> WorktreeProvision:
|
|
1546
|
+
"""Placeholder until the selected stage registry row is resolved."""
|
|
1547
|
+
return WorktreeProvision(
|
|
1548
|
+
status="deferred-final-verification",
|
|
1549
|
+
note=(
|
|
1550
|
+
"final-verification single-stage uses the selected implementation "
|
|
1551
|
+
"stage worktree from the registry"
|
|
1552
|
+
),
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
|
|
1500
1556
|
def _reserve_final_verification_target(
|
|
1501
1557
|
inp: "PrepareInputs", ctx: dict, ctx_stage_map: list,
|
|
1502
1558
|
) -> None:
|
|
@@ -1519,6 +1575,7 @@ def _reserve_final_verification_target(
|
|
|
1519
1575
|
row = _reg.get_stage_row(inp.project_id, inp.task_group, inp.task_id, n)
|
|
1520
1576
|
wt_path = (row or {}).get("worktree_path", "")
|
|
1521
1577
|
stage_base = (row or {}).get("base_ref", "")
|
|
1578
|
+
stage_branch = (row or {}).get("branch", "")
|
|
1522
1579
|
head = _git_out(wt_path, "rev-parse", "HEAD") if wt_path else ""
|
|
1523
1580
|
target = _resolve_single_stage_target(
|
|
1524
1581
|
requested_stage=inp.stage, done_rows=done_rows,
|
|
@@ -1527,6 +1584,12 @@ def _reserve_final_verification_target(
|
|
|
1527
1584
|
stage_dirty=_is_dirty_excluding_okstra(wt_path) if wt_path else False,
|
|
1528
1585
|
)
|
|
1529
1586
|
ctx["EXECUTOR_WORKTREE_PATH"] = wt_path
|
|
1587
|
+
ctx["EXECUTOR_WORKTREE_BRANCH"] = stage_branch
|
|
1588
|
+
ctx["EXECUTOR_WORKTREE_BASE_REF"] = stage_base
|
|
1589
|
+
ctx["EXECUTOR_WORKTREE_STATUS"] = "reused-stage"
|
|
1590
|
+
ctx["EXECUTOR_WORKTREE_NOTE"] = (
|
|
1591
|
+
f"final-verification uses implementation stage {n} worktree"
|
|
1592
|
+
)
|
|
1530
1593
|
else:
|
|
1531
1594
|
wt_path = ctx["EXECUTOR_WORKTREE_PATH"]
|
|
1532
1595
|
anchor = _reg.get_implementation_base(
|
|
@@ -1803,21 +1866,24 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1803
1866
|
with worktree_provision_mutex(
|
|
1804
1867
|
okstra_home(), inp.project_id, task_group_segment, task_id_segment,
|
|
1805
1868
|
):
|
|
1806
|
-
|
|
1807
|
-
worktree =
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1869
|
+
if inp.task_type == "final-verification" and inp.stage and inp.stage != "auto":
|
|
1870
|
+
worktree = _single_stage_final_verification_worktree(inp)
|
|
1871
|
+
else:
|
|
1872
|
+
try:
|
|
1873
|
+
worktree = provision_task_worktree(
|
|
1874
|
+
task_type=inp.task_type,
|
|
1875
|
+
project_root=project_root,
|
|
1876
|
+
project_id=inp.project_id,
|
|
1877
|
+
task_group_segment=task_group_segment,
|
|
1878
|
+
task_id_segment=task_id_segment,
|
|
1879
|
+
work_category=inp.work_category,
|
|
1880
|
+
base_ref=inp.base_ref,
|
|
1881
|
+
require_base_ref=True,
|
|
1882
|
+
)
|
|
1883
|
+
except RuntimeError as exc:
|
|
1884
|
+
raise PrepareError(
|
|
1885
|
+
f"task worktree provisioning failed: {exc}"
|
|
1886
|
+
) from exc
|
|
1821
1887
|
|
|
1822
1888
|
# ---- implementation stage selection (path-independent) ----
|
|
1823
1889
|
# Resolve + provision the stage BEFORE run-path compute so RUN_DIR
|
|
@@ -1832,6 +1898,10 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1832
1898
|
task_key, worktree.status,
|
|
1833
1899
|
)
|
|
1834
1900
|
stage_arg = impl_stage_selection.stage
|
|
1901
|
+
# Drop any stale waiver on this stage so the run actually verifies
|
|
1902
|
+
# conformance (kept inside the per-task-key mutex so concurrent
|
|
1903
|
+
# same-task runs don't race the manifest write).
|
|
1904
|
+
_clear_stale_stage_waiver(inp, project_root, impl_stage_selection.stage)
|
|
1835
1905
|
else:
|
|
1836
1906
|
impl_stage_selection = None
|
|
1837
1907
|
stage_arg = None
|
|
@@ -763,6 +763,22 @@ def _resolve_reuse_worktree(state: WizardState) -> bool:
|
|
|
763
763
|
return bool(entry and entry.status == "active")
|
|
764
764
|
|
|
765
765
|
|
|
766
|
+
def _base_ref_required(state: WizardState) -> bool:
|
|
767
|
+
return state.task_type != "final-verification" and state.reuse_worktree is False
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _base_ref_ready(state: WizardState) -> bool:
|
|
771
|
+
return not _base_ref_required(state) or S_BASE_REF_PICK in state.answered
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _branch_confirm_required(state: WizardState) -> bool:
|
|
775
|
+
return state.task_type != "final-verification"
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _stage_auto_allowed(state: WizardState) -> bool:
|
|
779
|
+
return state.task_type == "implementation"
|
|
780
|
+
|
|
781
|
+
|
|
766
782
|
def _existing_task_brief(project_root: Path, task_key: str) -> str:
|
|
767
783
|
"""Read taskBriefPath from manifest for an existing task. Empty if none."""
|
|
768
784
|
root = find_task_root(project_root, task_key)
|
|
@@ -1495,12 +1511,14 @@ def _build_stage_pick(state: WizardState) -> Prompt:
|
|
|
1495
1511
|
stages, _errs = mod._parse_stage_map(plan_text)
|
|
1496
1512
|
finally:
|
|
1497
1513
|
_sys.modules.pop("_ip_stage_v_wizard", None)
|
|
1498
|
-
|
|
1499
|
-
t
|
|
1514
|
+
label = (
|
|
1515
|
+
t.get("label_final_verification", t["label"])
|
|
1500
1516
|
if state.task_type == "final-verification"
|
|
1501
|
-
else t["
|
|
1517
|
+
else t["label"]
|
|
1502
1518
|
)
|
|
1503
|
-
options = [
|
|
1519
|
+
options = []
|
|
1520
|
+
if _stage_auto_allowed(state):
|
|
1521
|
+
options.append(_opt("auto", t["options"]["auto"]))
|
|
1504
1522
|
for s in stages:
|
|
1505
1523
|
depends = ",".join(map(str, s.depends_on)) or "(none)"
|
|
1506
1524
|
options.append(_opt(
|
|
@@ -1509,7 +1527,7 @@ def _build_stage_pick(state: WizardState) -> Prompt:
|
|
|
1509
1527
|
))
|
|
1510
1528
|
return Prompt(
|
|
1511
1529
|
step=S_STAGE_PICK, kind="pick",
|
|
1512
|
-
label=
|
|
1530
|
+
label=label,
|
|
1513
1531
|
options=options,
|
|
1514
1532
|
echo_template=t["echo_template"],
|
|
1515
1533
|
)
|
|
@@ -1518,7 +1536,12 @@ def _build_stage_pick(state: WizardState) -> Prompt:
|
|
|
1518
1536
|
def _submit_stage_pick(state: WizardState, answer: str) -> Optional[str]:
|
|
1519
1537
|
if not answer:
|
|
1520
1538
|
raise WizardError("value required")
|
|
1521
|
-
if answer
|
|
1539
|
+
if answer == "auto":
|
|
1540
|
+
if not _stage_auto_allowed(state):
|
|
1541
|
+
raise WizardError(
|
|
1542
|
+
"final-verification requires an explicit stage number"
|
|
1543
|
+
)
|
|
1544
|
+
else:
|
|
1522
1545
|
try:
|
|
1523
1546
|
int(answer)
|
|
1524
1547
|
except ValueError:
|
|
@@ -2352,7 +2375,7 @@ STEPS: list[Step] = [
|
|
|
2352
2375
|
owns=("keep_existing_brief",)),
|
|
2353
2376
|
Step(S_BASE_REF_PICK,
|
|
2354
2377
|
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
2355
|
-
and s
|
|
2378
|
+
and _base_ref_required(s)
|
|
2356
2379
|
and S_BASE_REF_PICK not in s.answered
|
|
2357
2380
|
and bool(s.brief_path)),
|
|
2358
2381
|
build=_build_base_ref_pick, submit=_submit_base_ref_pick,
|
|
@@ -2367,8 +2390,7 @@ STEPS: list[Step] = [
|
|
|
2367
2390
|
and not s.approved_plan_pending_text
|
|
2368
2391
|
and S_APPROVED_PLAN_PICK not in s.answered
|
|
2369
2392
|
and bool(s.brief_path)
|
|
2370
|
-
and (s
|
|
2371
|
-
or S_BASE_REF_PICK in s.answered)
|
|
2393
|
+
and _base_ref_ready(s)
|
|
2372
2394
|
and not s.base_ref_pending_text
|
|
2373
2395
|
and _latest_implementation_planning_report(s) is not None),
|
|
2374
2396
|
build=_build_approved_plan_pick, submit=_submit_approved_plan_pick,
|
|
@@ -2377,8 +2399,7 @@ STEPS: list[Step] = [
|
|
|
2377
2399
|
applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
|
|
2378
2400
|
and not s.approved_plan_path
|
|
2379
2401
|
and bool(s.brief_path)
|
|
2380
|
-
and (s
|
|
2381
|
-
or S_BASE_REF_PICK in s.answered)
|
|
2402
|
+
and _base_ref_ready(s)
|
|
2382
2403
|
and not s.base_ref_pending_text
|
|
2383
2404
|
and (s.approved_plan_pending_text
|
|
2384
2405
|
or _latest_implementation_planning_report(s) is None)),
|
|
@@ -2548,11 +2569,16 @@ STEPS: list[Step] = [
|
|
|
2548
2569
|
build=_build_pr_template_scope, submit=_submit_pr_template_scope,
|
|
2549
2570
|
owns=("pr_template_scope",)),
|
|
2550
2571
|
Step(S_BRANCH_CONFIRM,
|
|
2551
|
-
applies=lambda s: _ready_for_confirm(s)
|
|
2572
|
+
applies=lambda s: (_ready_for_confirm(s)
|
|
2573
|
+
and _branch_confirm_required(s)
|
|
2574
|
+
and s.branch_confirmed is None),
|
|
2552
2575
|
build=_build_branch_confirm, submit=_submit_branch_confirm,
|
|
2553
2576
|
owns=("branch_confirmed",)),
|
|
2554
2577
|
Step(S_CONFIRM,
|
|
2555
|
-
applies=lambda s: _ready_for_confirm(s)
|
|
2578
|
+
applies=lambda s: (_ready_for_confirm(s)
|
|
2579
|
+
and (not _branch_confirm_required(s)
|
|
2580
|
+
or s.branch_confirmed is True)
|
|
2581
|
+
and s.confirmed is None),
|
|
2556
2582
|
build=_build_confirm, submit=_submit_confirm,
|
|
2557
2583
|
owns=("confirmed", "edit_target")),
|
|
2558
2584
|
Step(S_EDIT_TARGET,
|
|
@@ -2570,7 +2596,7 @@ def _identity_ready(s: WizardState) -> bool:
|
|
|
2570
2596
|
return False
|
|
2571
2597
|
if not s.brief_path:
|
|
2572
2598
|
return False
|
|
2573
|
-
if s
|
|
2599
|
+
if _base_ref_required(s) and S_BASE_REF_PICK not in s.answered:
|
|
2574
2600
|
return False
|
|
2575
2601
|
if s.base_ref_pending_text:
|
|
2576
2602
|
return False
|
|
@@ -2763,7 +2789,21 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
2763
2789
|
workers = state.workers_override.strip()
|
|
2764
2790
|
if state.task_type == "implementation":
|
|
2765
2791
|
workers = "" # profile-default roster is mandatory for impl
|
|
2766
|
-
base_ref =
|
|
2792
|
+
base_ref = (
|
|
2793
|
+
""
|
|
2794
|
+
if state.reuse_worktree or state.task_type == "final-verification"
|
|
2795
|
+
else state.base_ref
|
|
2796
|
+
)
|
|
2797
|
+
if state.task_type == "implementation":
|
|
2798
|
+
stage = state.selected_stage or "auto"
|
|
2799
|
+
elif state.task_type == "final-verification":
|
|
2800
|
+
if not state.selected_stage or state.selected_stage == "auto":
|
|
2801
|
+
raise WizardError(
|
|
2802
|
+
"final-verification requires an explicit stage number"
|
|
2803
|
+
)
|
|
2804
|
+
stage = state.selected_stage
|
|
2805
|
+
else:
|
|
2806
|
+
stage = ""
|
|
2767
2807
|
pr_template = (
|
|
2768
2808
|
state.pr_template_path
|
|
2769
2809
|
if state.task_type == "release-handoff"
|
|
@@ -2779,7 +2819,7 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
2779
2819
|
"executor": state.executor,
|
|
2780
2820
|
"critic": state.critic,
|
|
2781
2821
|
"approved-plan": state.approved_plan_path,
|
|
2782
|
-
"stage":
|
|
2822
|
+
"stage": stage,
|
|
2783
2823
|
"base-ref": base_ref,
|
|
2784
2824
|
"workers": workers,
|
|
2785
2825
|
"directive": state.directive,
|
|
@@ -2801,7 +2841,9 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
2801
2841
|
lines.append(f" task-type : {state.task_type}")
|
|
2802
2842
|
lines.append(f" task-key : {state.task_group}/{state.task_id}")
|
|
2803
2843
|
lines.append(f" brief : {state.brief_path or '(none)'}")
|
|
2804
|
-
if state.
|
|
2844
|
+
if state.task_type == "final-verification":
|
|
2845
|
+
lines.append(" base-ref : (selected stage worktree)")
|
|
2846
|
+
elif state.reuse_worktree:
|
|
2805
2847
|
lines.append(" base-ref : (reusing existing worktree)")
|
|
2806
2848
|
else:
|
|
2807
2849
|
lines.append(f" base-ref : {state.base_ref}")
|
|
@@ -2829,7 +2871,11 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
2829
2871
|
lines.append(f" critic : {state.critic or '(off)'}")
|
|
2830
2872
|
if state.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
2831
2873
|
lines.append(f" approved-plan : {state.approved_plan_path}")
|
|
2832
|
-
|
|
2874
|
+
stage = (
|
|
2875
|
+
state.selected_stage
|
|
2876
|
+
or ("auto" if state.task_type == "implementation" else "(not selected)")
|
|
2877
|
+
)
|
|
2878
|
+
lines.append(f" stage : {stage}")
|
|
2833
2879
|
if state.clarification_response_path:
|
|
2834
2880
|
lines.append(f" clarification : {state.clarification_response_path}")
|
|
2835
2881
|
if state.task_type == "release-handoff" and state.pr_template_path:
|
|
@@ -373,6 +373,24 @@ def _resolve_snapshot_files(project_root: Optional[Path] = None) -> tuple[str, .
|
|
|
373
373
|
)
|
|
374
374
|
|
|
375
375
|
|
|
376
|
+
def okstra_clean_gate_excludes(project_root: Optional[Path] = None) -> tuple[str, ...]:
|
|
377
|
+
"""Project-relative paths okstra owns and source clean gates should ignore."""
|
|
378
|
+
out: list[str] = []
|
|
379
|
+
seen: set[str] = set()
|
|
380
|
+
for rel in (
|
|
381
|
+
".okstra",
|
|
382
|
+
*_resolve_sync_dirs(project_root),
|
|
383
|
+
*_resolve_sync_files(project_root),
|
|
384
|
+
*_resolve_snapshot_files(project_root),
|
|
385
|
+
):
|
|
386
|
+
cleaned = rel.strip().removeprefix("./").rstrip("/")
|
|
387
|
+
if not cleaned or cleaned == "." or cleaned in seen:
|
|
388
|
+
continue
|
|
389
|
+
seen.add(cleaned)
|
|
390
|
+
out.append(cleaned)
|
|
391
|
+
return tuple(out)
|
|
392
|
+
|
|
393
|
+
|
|
376
394
|
def _link_sync_dirs(source_root: Path, worktree_path: Path) -> list[str]:
|
|
377
395
|
"""Symlink each configured dir from `source_root` (the MAIN
|
|
378
396
|
worktree) into the new worktree.
|
|
@@ -183,6 +183,7 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
|
|
|
183
183
|
# silently (observed in dev-9692 error-analysis: claude/codex workers
|
|
184
184
|
# dispatched without `name` → both unavailable, report-writer named → fine).
|
|
185
185
|
unattributed_sessions: list[str] = []
|
|
186
|
+
unattributed_totals: list[dict] = []
|
|
186
187
|
for sid, path in claude_sessions.items():
|
|
187
188
|
if sid == lead_sid:
|
|
188
189
|
lead_path = path
|
|
@@ -194,6 +195,7 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
|
|
|
194
195
|
by_agent.setdefault(agent, []).append((sid, path, totals))
|
|
195
196
|
else:
|
|
196
197
|
unattributed_sessions.append(sid)
|
|
198
|
+
unattributed_totals.append(totals)
|
|
197
199
|
|
|
198
200
|
# Lead.
|
|
199
201
|
if lead_path is not None:
|
|
@@ -273,6 +275,26 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
|
|
|
273
275
|
block["cliNote"] = f"{agent} CLI session found but no usage recorded (likely errored before completion)"
|
|
274
276
|
worker["usage"] = block
|
|
275
277
|
|
|
278
|
+
# Fold team-tagged worker sessions that carry no agentName into the worker
|
|
279
|
+
# pool. They cannot be mapped to a specific workerId (so each named worker
|
|
280
|
+
# row above stays `unavailable`), but the tokens are real team-worker spend —
|
|
281
|
+
# most often an in-process teammate whose work is commingled in a team-tagged
|
|
282
|
+
# session the harness never tagged with `name`. Without this, the run-level
|
|
283
|
+
# Worker total reads 0 and the report validator hard-fails a legitimate run.
|
|
284
|
+
# Attribution is aggregate, not per-worker; usageSummary records it openly.
|
|
285
|
+
unattributed_usage = None
|
|
286
|
+
if unattributed_totals:
|
|
287
|
+
unattributed_usage = usage_block(
|
|
288
|
+
_aggregate_totals(unattributed_totals), source="claude-jsonl"
|
|
289
|
+
)
|
|
290
|
+
unattributed_usage["sessionIds"] = unattributed_sessions
|
|
291
|
+
unattributed_usage["note"] = (
|
|
292
|
+
"Team-tagged worker session(s) with no agentName (dispatched without "
|
|
293
|
+
"the Agent `name` arg, or an in-process teammate commingled with the "
|
|
294
|
+
"lead). Folded into the worker pool as an aggregate because they "
|
|
295
|
+
"cannot be mapped to a specific workerId."
|
|
296
|
+
)
|
|
297
|
+
|
|
276
298
|
# Aggregate summary.
|
|
277
299
|
lead = state.get("leadUsage") or {}
|
|
278
300
|
workers = state.get("workers", [])
|
|
@@ -283,6 +305,10 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
|
|
|
283
305
|
worker_billable = sum((w.get("usage") or {}).get("billableEquivalentTokens", 0) or 0 for w in workers)
|
|
284
306
|
worker_cost = sum((w.get("usage") or {}).get("estimatedCostUsd", 0) or 0 for w in workers)
|
|
285
307
|
cli_cost = sum((w.get("usage") or {}).get("cliEstimatedCostUsd", 0) or 0 for w in workers)
|
|
308
|
+
if unattributed_usage is not None:
|
|
309
|
+
worker_total += unattributed_usage.get("totalTokens", 0) or 0
|
|
310
|
+
worker_billable += unattributed_usage.get("billableEquivalentTokens", 0) or 0
|
|
311
|
+
worker_cost += unattributed_usage.get("estimatedCostUsd", 0) or 0
|
|
286
312
|
|
|
287
313
|
# Surface models whose pricing lookup failed so the silent-zero case is visible.
|
|
288
314
|
unmatched_models: list[str] = []
|
|
@@ -312,6 +338,7 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
|
|
|
312
338
|
"sessionsFound": len(claude_sessions),
|
|
313
339
|
"unmatchedModels": sorted(set(unmatched_models)),
|
|
314
340
|
"unattributedTeamSessions": unattributed_sessions,
|
|
341
|
+
"unattributedWorkerUsage": unattributed_usage,
|
|
315
342
|
"definitions": {
|
|
316
343
|
"totalTokens": "Sum of input + output + cache_creation + cache_read tokens (raw processed volume; matches Anthropic API breakdown). Cache reads are 95%+ in long sessions.",
|
|
317
344
|
"billableEquivalentTokens": "Tokens normalized to base-input-price units (cache_creation_5m x1.25, cache_creation_1h x2.0, cache_read x0.1, output x5). 5m vs 1h is split from usage.cache_creation when the API breakdown is present; otherwise all cache_creation falls into 5m.",
|
|
@@ -169,7 +169,7 @@ A reverify dispatch that returns a **terminal non-result** (`timeout`, `error`,
|
|
|
169
169
|
Rules:
|
|
170
170
|
|
|
171
171
|
1. For each affected finding, append a `votes[W].verdict = "verification-error"` entry instead of `disagree`, plus the wrapper's captured exit reason in `votes[W].explanation`.
|
|
172
|
-
2. Record one event per failed dispatch via `
|
|
172
|
+
2. Record one event per failed dispatch via `okstra error-log append-observed --error-type cli-failure --agent <worker> ...` (the worker wrapper does this for Codex/Gemini; for Claude worker timeouts the lead does it).
|
|
173
173
|
3. Add an entry to the round's `skippedWorkers[]` with `{worker: <W>, reason: "dispatch-non-result", terminalStatus: <timeout|error|not-run>}`.
|
|
174
174
|
4. If at least one dispatch was issued AND all reverify dispatches in a round terminate as non-result (mirroring the pseudocode's `len(dispatches) > 0` guard), the round is treated as gate-closed: write `round2SkippedReason: "all-reverify-non-result"` (even if the round in question is round 1 — i.e. round 2 never runs because round 1 produced no usable votes), record one `contract-violation` event per non-result dispatch, and exit the WHILE loop.
|
|
175
175
|
5. Section 6 (Specialization Lens) of a worker output is OUT of convergence scope per "Convergence scope" above — its absence is NEVER a `verification-error`.
|
|
@@ -293,7 +293,7 @@ Assigned worker prompt history path: <Project Root>/<Prompt History Path>
|
|
|
293
293
|
2. `team-state-<task-type>-<seq>.json` → `workers[].usage.cliModel` for that role (initial run's actual execution value)
|
|
294
294
|
3. The `**Model:**` line of the initial Phase 4 prompt for that role (read from its persisted prompt-history file)
|
|
295
295
|
|
|
296
|
-
If none of the three is available, **abort the reverify dispatch for that role** and record a `contract-violation` event via `okstra
|
|
296
|
+
If none of the three is available, **abort the reverify dispatch for that role** and record a `contract-violation` event via `okstra error-log append-observed`. Do NOT guess, do NOT fall back to training-data defaults — for codex this would silently produce `o4-mini` instead of the assigned `gpt-5.5`-class model, which is a real bug class observed in production.
|
|
297
297
|
|
|
298
298
|
For Codex/Gemini wrapper subagents, the `**Model:** <role>, <modelExecutionValue>` line is what their wrapper extracts to pass into the underlying CLI's `--model` flag. Omitting it forces the wrapper to fall back to its own training-data knowledge of the CLI's historical default.
|
|
299
299
|
|
|
@@ -60,7 +60,7 @@ The prompt MUST include, in this order at the top:
|
|
|
60
60
|
before the dispatch is constructed. The worker copies this verbatim
|
|
61
61
|
into `data.json.meta.reportLanguage`.
|
|
62
62
|
11. For implementation-planning runs: a literal block listing the 8 required English section headings the validator scans for (`Option Candidates`, `Trade-off`, `Recommended Option`, `Stepwise Execution Order`, `Dependency`, `Validation Checklist`, `Rollback`, `User Approval Request`). The writer must use these exact substrings as section headings (Korean translation in parentheses is allowed).
|
|
63
|
-
12. An explicit instruction: `You are the author of TWO files: (a) the final-report data.json at <Result Path>, (b) the worker-results audit file at <Worker Result Path>. After writing the data.json, invoke "
|
|
63
|
+
12. An explicit instruction: `You are the author of TWO files: (a) the final-report data.json at <Result Path>, (b) the worker-results audit file at <Worker Result Path>. After writing the data.json, invoke "okstra render-final-report <Result Path>" via Bash so the markdown sibling is rendered before you return. Do not return the report inline. The validator fails the run when (a)'s schema validation fails, when the rendered markdown is absent, or when (b) is missing.`
|
|
64
64
|
|
|
65
65
|
**Completion detection after dispatch (BLOCKING).** The `Agent(... team_name ...)` call returns `Spawned successfully` immediately; that ack is NOT completion. After dispatching the report-writer (async), Lead MUST detect its completion via the self-scheduled polling protocol in [okstra-team-contract](../okstra-team-contract/SKILL.md) "Worker-completion detection (self-scheduled polling)", polling for the appearance of the data.json (Result Path) and the worker-results file (Worker Result Path) — do NOT restate the algorithm here. Report-writer is a single worker, so the pending set has one entry; the SSOT protocol handles that naturally. Lead MUST NOT treat the `Spawned successfully` ack as completion and MUST NOT end its turn with a prose "waiting for the report" statement; that path stalls the run until the user manually nudges it.
|
|
66
66
|
|
|
@@ -78,7 +78,7 @@ Except for `release-handoff` (which is single-lead by design and never dispatche
|
|
|
78
78
|
|
|
79
79
|
1. A Report writer worker dispatch was actually attempted (Agent call was issued).
|
|
80
80
|
2. The attempt recorded a terminal status of `error`, `timeout`, or `not-run` with a concrete reason (tool error message, timeout duration, or external blocker).
|
|
81
|
-
3. The reason is logged via `okstra
|
|
81
|
+
3. The reason is logged via `okstra error-log append-observed --error-type cli-failure ...` (or `tool-failure` if the failure was internal).
|
|
82
82
|
|
|
83
83
|
Speculative reasons such as "session resume constraint", "team object no longer exists", or "lead can do it faster" are NOT valid.
|
|
84
84
|
|
|
@@ -90,7 +90,7 @@ The four steps below MUST execute in this exact order. Reordering them is the re
|
|
|
90
90
|
2. **Phase 7 step 1 — Token-usage collector with `--substitute-data`** (BLOCKING). One invocation aggregates `leadUsage` / `workers[].usage` / `usageSummary` into team-state AND populates `tokenUsage` + `executionStatus[].totalTokens` etc. in the data.json AND re-invokes the renderer so the sibling markdown carries the real numbers. Skipping the flag ships a markdown full of `--` cells.
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
|
|
93
|
+
okstra token-usage \
|
|
94
94
|
<runDirectoryPath>/state/team-state-<task-type>-<seq>.json \
|
|
95
95
|
--write --summary \
|
|
96
96
|
--substitute-data <runDirectoryPath>/reports/final-report-<task-type>-<seq>.data.json
|
|
@@ -100,7 +100,7 @@ The four steps below MUST execute in this exact order. Reordering them is the re
|
|
|
100
100
|
3. **Phase 7 step 1.5 — Render report views** (BLOCKING, conditional output). Always invoke the renderer; it decides whether an html sibling is warranted:
|
|
101
101
|
|
|
102
102
|
```bash
|
|
103
|
-
|
|
103
|
+
okstra render-views \
|
|
104
104
|
<runDirectoryPath>/reports/final-report-<task-type>-<seq>.md
|
|
105
105
|
```
|
|
106
106
|
|
|
@@ -112,7 +112,7 @@ The four steps below MUST execute in this exact order. Reordering them is the re
|
|
|
112
112
|
4. **Phase 7 step 2 — Follow-up task spawner** (BLOCKING when Section 4 is non-empty). Turns the report's `## 4. Follow-up Tasks (후속 작업)` rows into `tasks/<task-group>/<new-task-id>/` stubs.
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
|
-
|
|
115
|
+
okstra spawn-followups \
|
|
116
116
|
<runDirectoryPath>/reports/final-report-<task-type>-<seq>.data.json \
|
|
117
117
|
--project-root <project_root> \
|
|
118
118
|
--task-group <task-group> \
|
|
@@ -323,7 +323,7 @@ Persistence steps that must be performed in Phase 7:
|
|
|
323
323
|
- [ ] 5. **Update task-index.md**: Refresh human-readable summary
|
|
324
324
|
- [ ] 6. **Generate final status file**: `runs/<task-type>/status/final-<task-type>-<seq>.status` (if necessary)
|
|
325
325
|
- [ ] 7. **Save convergence state**: `runs/<task-type>/state/convergence-<task-type>-<seq>.json` (when convergence is enabled)
|
|
326
|
-
- [ ] 8. **Spawn follow-up task stubs**: run `
|
|
326
|
+
- [ ] 8. **Spawn follow-up task stubs**: run `okstra spawn-followups` against the final-report per the canonical spawn rule defined in "Phase 7 follow-up task spawner" above. Do not restate the trigger condition here — that section is the single source of truth. The script is idempotent across reruns.
|
|
327
327
|
- [ ] 9. **Human HTML report** (conditional): `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — produced by Phase 7 step 1.5 **only when the report has ≥1 §5 `C-*` clarification row** (self-contained, embeds `Export user response` button). Clarification-free reports legitimately have no html sibling; do not treat its absence as a missing artifact.
|
|
328
328
|
|
|
329
329
|
### Response after Persistence
|
|
@@ -303,7 +303,7 @@ Schema:
|
|
|
303
303
|
Workers MUST omit `source` / `recordedAt` / `agent` / `agentRole` / `model` /
|
|
304
304
|
`taskKey`. Claude lead fills those in when dumping the sidecar to the
|
|
305
305
|
run-level errors log (`runs/<task-type>/logs/errors-<task-type>-<seq>.jsonl`)
|
|
306
|
-
via `
|
|
306
|
+
via `okstra error-log append-from-worker`.
|
|
307
307
|
|
|
308
308
|
Workers MUST use only `errorType: "tool-failure"` in the **sidecar file**.
|
|
309
309
|
|
|
@@ -312,7 +312,7 @@ run-level errors log path or their sidecar path from the
|
|
|
312
312
|
`runs/<task-type>/...` template syntax. Both absolute paths are delivered
|
|
313
313
|
by Lead via two dispatch-prompt header lines:
|
|
314
314
|
|
|
315
|
-
- `**Errors log path:** <absolute path>` — run-level JSONL (`okstra
|
|
315
|
+
- `**Errors log path:** <absolute path>` — run-level JSONL (`okstra error-log append-observed --out ...`)
|
|
316
316
|
- `**Errors sidecar path:** <absolute path>` — per-worker JSON (`{ "schemaVersion": 1, "errors": [...] }`)
|
|
317
317
|
|
|
318
318
|
Lead obtains both paths from the launch prompt's `## Run Logs (error-log
|
|
@@ -322,7 +322,7 @@ without proceeding — this is the contractual replacement for the previous
|
|
|
322
322
|
"derive from template placeholders" behavior, which silently produced
|
|
323
323
|
empty run-level error logs in production.
|
|
324
324
|
|
|
325
|
-
- `cli-failure` events are recorded by the wrapper subagent itself (Codex / Gemini), but **directly to the run-level error log** via `okstra
|
|
325
|
+
- `cli-failure` events are recorded by the wrapper subagent itself (Codex / Gemini), but **directly to the run-level error log** via `okstra error-log append-observed --error-type cli-failure ...` — NOT via the sidecar. The sidecar is an in-process tool-failure channel only.
|
|
326
326
|
- **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-gemini-exec.sh` accept four required positional arguments plus an optional fifth `<role>`: `<project-root> <model> <prompt-path> <worktree-path> [<role>]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise. For codex it becomes `--add-dir <worktree>` (sandbox write access); for gemini it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt. The optional fifth `<role>` is folded into both the caller (worker) pane title `<cli>-<role>-<pid>` and the sibling trace-pane title `<cli>-<role>-<pid>-trace[from=<caller-pane-id>]` (e.g. `codex-worker-93421` ↔ `codex-worker-93421-trace[from=%5]`). `<pid>` is the wrapper's own PID and disambiguates concurrent dispatches of the same role; the embedded caller pane id keeps the trace ↔ worker correlation visible even when the worker pane's title is overwritten by the parent process (Claude Code's TUI emits OSC 2 title escape sequences on its own pane). Always pass the literal string `worker` so the dispatch is self-describing (the wrapper defaults to `worker` if omitted).
|
|
327
327
|
- **Background dispatch + polling contract (Codex / Gemini wrappers).** Both wrapper subagents MUST dispatch `okstra-codex-exec.sh` / `okstra-gemini-exec.sh` via `Bash(run_in_background: true)` and poll with `BashOutput(bash_id)` until the shell reports `status == "completed"`, capped at 30 minutes (1800s) of wall-clock elapsed time. `BashOutput` itself is the wait primitive — call it back-to-back; do NOT insert a standalone `sleep` between polls. The Claude Code harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps inside until-loops to work around the block. Workers that hit the contract bug must NOT self-recover with `until ...; do sleep 2; done` wrappers — that path violates the harness anti-circumvention rule, even though it superficially "works". The legacy "single foreground `Bash` with 120000ms timeout" rule, and the subsequent "60-second cadence with `sleep 60` between polls" rule, are both retired. The current rule applies in **every phase** (analysis runs typically complete in 1–2 `BashOutput` calls, so there is no regression for short jobs). Recording responsibilities:
|
|
328
328
|
- Successful completion: return the wrapper's accumulated stdout from the final `BashOutput`. No log entry.
|
|
@@ -330,8 +330,8 @@ empty run-level error logs in production.
|
|
|
330
330
|
- Polling cap reached: before `KillShell`, perform a one-shot **mtime-grace check** on the wrapper's live log (`<prompt>.log`). If the log was written within the last 90 seconds AND grace has not yet been applied this loop, extend the cap from 1800s → 2100s (one-shot +5min) and continue polling. Otherwise (log stale, OR grace already applied), call `KillShell(shell_id)`, record `cli-failure` with `--exit-code 124 --duration-ms <observed_ms> --message "<wrapper> exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, then return the language-specific `*_CLI_TIMEOUT` sentinel. The grace exists to absorb token-budget spikes where the CLI is genuinely still producing output past the 30-minute mark; it is a one-shot soft extension, NOT a loop.
|
|
331
331
|
- Token-usage matching is unaffected: the wrapper subagent stays alive throughout polling, so the wrapper's jsonl timestamp window continues to cover the underlying CLI rollout's full duration (see §"Token-usage accounting" below).
|
|
332
332
|
- **No external timeout on wrapper subagents.** The codex/gemini wrapper subagent's polling loop (with optional mtime grace) is the SINGLE timeout authority for its dispatch. Lead MUST NOT impose a separate `Agent()` call timeout, an outer `Bash` wall-clock deadline, or any other mechanism that terminates the subagent before its own polling cap is reached. Doing so reproduces the historical failure mode that motivated this rule: Lead aborts the subagent at e.g. 18 minutes, the subagent returns nothing, and Lead classifies the role as "no response" while the underlying CLI was actively working. The wrapper's polling cap (30min + optional 5min grace) is calibrated so that, combined with Lead's redispatch policy (see "Lead Redispatch Policy on Result-Missing"), a recoverable single-run failure costs at most ~70 minutes of wall-clock — predictable enough to plan around. If a specific run requires a tighter cap, lower it in the wrapper subagent's polling contract (single source of truth), NOT by layering Lead-side timeouts.
|
|
333
|
-
- `contract-violation` events (C) are recorded by Lead via `okstra
|
|
334
|
-
- Lead's responsibility regarding the sidecar is to dump it to the run-level error log via `okstra
|
|
333
|
+
- `contract-violation` events (C) are recorded by Lead via `okstra error-log append-observed --error-type contract-violation ...` after inspecting worker outputs.
|
|
334
|
+
- Lead's responsibility regarding the sidecar is to dump it to the run-level error log via `okstra error-log append-from-worker` after each worker terminates; Lead does not write into the sidecar.
|
|
335
335
|
|
|
336
336
|
## Convergence Phase Rules
|
|
337
337
|
|
|
@@ -621,7 +621,7 @@ def _scan_token_usage_summary(content: str, failures: list[str]) -> None:
|
|
|
621
621
|
failures.append(
|
|
622
622
|
f"Token Usage Summary row `{label_cell or '<unlabeled>'}` has "
|
|
623
623
|
f"a zero value `{stripped}` — no okstra run consumes zero "
|
|
624
|
-
"tokens. Re-run `
|
|
624
|
+
"tokens. Re-run `okstra token-usage "
|
|
625
625
|
"<team-state> --write --summary --substitute-data "
|
|
626
626
|
"<report-path>` to repopulate from session jsonls. The "
|
|
627
627
|
"Codex/Gemini CLI row is the only place `$0.00` is "
|
|
@@ -1024,7 +1024,7 @@ def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
|
|
|
1024
1024
|
if not summary or not summary.get("collectedAt"):
|
|
1025
1025
|
failures.append(
|
|
1026
1026
|
"team-state.usageSummary is empty — Phase 7 token-usage collection was skipped. "
|
|
1027
|
-
"Run `
|
|
1027
|
+
"Run `okstra token-usage <team-state> --write --summary "
|
|
1028
1028
|
"--substitute-data <final-report>`."
|
|
1029
1029
|
)
|
|
1030
1030
|
return
|
package/src/_python-helper.mjs
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
2
5
|
import { buildPythonpath, resolvePaths } from "./paths.mjs";
|
|
3
6
|
|
|
7
|
+
function resolveInstalledScript(paths, scriptName) {
|
|
8
|
+
// Prefer the installed copy under ~/.okstra/bin (what production users run);
|
|
9
|
+
// fall back to the in-repo source when invoked from a checkout that has not
|
|
10
|
+
// been installed (dev / CI).
|
|
11
|
+
const installed = join(paths.bin, scriptName);
|
|
12
|
+
if (existsSync(installed)) return installed;
|
|
13
|
+
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
14
|
+
const dev = resolvePath(repoRoot, "scripts", scriptName);
|
|
15
|
+
return existsSync(dev) ? dev : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Thin spawn shim shared by every `okstra <cmd>` subcommand that fronts a
|
|
19
|
+
// `scripts/okstra-*.py` entry point. Centralizing it keeps PYTHONPATH wiring
|
|
20
|
+
// and installed/dev resolution in one place so skills call `okstra <cmd>`
|
|
21
|
+
// instead of emitting `python3 "$HOME/..."` (which breaks `Bash(okstra:*)`
|
|
22
|
+
// permission matching and prompts on every call).
|
|
23
|
+
export async function runInstalledScript({ scriptName, args, usage, emptyArgsCode = 2 }) {
|
|
24
|
+
if (args.length === 0) {
|
|
25
|
+
process.stdout.write(usage);
|
|
26
|
+
return emptyArgsCode;
|
|
27
|
+
}
|
|
28
|
+
// Only a bare `--help` / `-h` prints the wrapper usage. A `--help` that
|
|
29
|
+
// follows a subcommand (e.g. `error-log append-observed --help`) must reach
|
|
30
|
+
// the python helper so its own per-subcommand help shows through.
|
|
31
|
+
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
32
|
+
process.stdout.write(usage);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
const paths = await resolvePaths();
|
|
36
|
+
const entry = resolveInstalledScript(paths, scriptName);
|
|
37
|
+
if (!entry) {
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
`error: ${scriptName} not found — run 'okstra install' (or 'okstra ensure-installed') first\n`,
|
|
40
|
+
);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
return await new Promise((resolve) => {
|
|
44
|
+
const child = spawn("python3", [entry, ...args], {
|
|
45
|
+
stdio: "inherit",
|
|
46
|
+
env: { ...process.env, PYTHONPATH: buildPythonpath(paths) },
|
|
47
|
+
});
|
|
48
|
+
child.on("error", (err) => {
|
|
49
|
+
process.stderr.write(`error: failed to spawn python3: ${err.message}\n`);
|
|
50
|
+
resolve(1);
|
|
51
|
+
});
|
|
52
|
+
child.on("close", (code) => resolve(typeof code === "number" ? code : 1));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
4
56
|
export async function runPythonSnippet({ script, args = [], extraEnv = {} }) {
|
|
5
57
|
const paths = await resolvePaths();
|
|
6
58
|
return new Promise((resolve) => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { runInstalledScript } from "./_python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra error-log — append okstra run error events to the run error log
|
|
4
|
+
|
|
5
|
+
Wraps the python helper (\`okstra-error-log.py\`) installed under
|
|
6
|
+
\`~/.okstra/bin/\` so skills and worker wrappers call \`okstra error-log\`
|
|
7
|
+
instead of emitting a \`python3 "$HOME/..."\` invocation (which breaks
|
|
8
|
+
\`Bash(okstra:*)\` permission matching and prompts on every call).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
okstra error-log <subcommand> [...] # e.g. append-observed / append-from-worker
|
|
12
|
+
|
|
13
|
+
All arguments are forwarded verbatim to the python helper. See
|
|
14
|
+
\`okstra error-log append-observed --help\` for the full option list.
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export async function run(args) {
|
|
18
|
+
return runInstalledScript({ scriptName: "okstra-error-log.py", args, usage: USAGE });
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { runInstalledScript } from "./_python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra inject-report-index — add the top-of-report Index + scroll anchors to a report
|
|
4
|
+
|
|
5
|
+
Wraps the python helper (\`okstra-inject-report-index.py\`) installed under
|
|
6
|
+
\`~/.okstra/bin/\` so skills call \`okstra inject-report-index\` instead of
|
|
7
|
+
emitting a \`python3 "$HOME/..."\` invocation (which breaks \`Bash(okstra:*)\`
|
|
8
|
+
permission matching and prompts on every call).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
okstra inject-report-index <markdown-path> [--report-language <en|ko>]
|
|
12
|
+
|
|
13
|
+
All arguments are forwarded verbatim to the python helper.
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export async function run(args) {
|
|
17
|
+
return runInstalledScript({
|
|
18
|
+
scriptName: "okstra-inject-report-index.py",
|
|
19
|
+
args,
|
|
20
|
+
usage: USAGE,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { runInstalledScript } from "./_python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra render-final-report — render the markdown sibling of a final-report data.json
|
|
4
|
+
|
|
5
|
+
Wraps the python helper (\`okstra-render-final-report.py\`) installed under
|
|
6
|
+
\`~/.okstra/bin/\` so skills call \`okstra render-final-report\` instead of
|
|
7
|
+
emitting a \`python3 "$HOME/..."\` invocation (which breaks \`Bash(okstra:*)\`
|
|
8
|
+
permission matching and prompts on every call).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
okstra render-final-report <path-to-final-report.data.json>
|
|
12
|
+
|
|
13
|
+
The argument is forwarded verbatim to the python helper.
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export async function run(args) {
|
|
17
|
+
return runInstalledScript({
|
|
18
|
+
scriptName: "okstra-render-final-report.py",
|
|
19
|
+
args,
|
|
20
|
+
usage: USAGE,
|
|
21
|
+
});
|
|
22
|
+
}
|
package/src/render-views.mjs
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { resolve as resolvePath } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { resolvePaths } from "./paths.mjs";
|
|
1
|
+
import { runInstalledScript } from "./_python-helper.mjs";
|
|
6
2
|
|
|
7
|
-
const USAGE = `okstra render-views — render
|
|
3
|
+
const USAGE = `okstra render-views — render the self-contained HTML view of a final-report
|
|
8
4
|
|
|
9
5
|
A thin spawn shim over \`scripts/okstra-render-report-views.py\` (installed
|
|
10
6
|
at \`$HOME/.okstra/bin/okstra-render-report-views.py\`). Reads the final-
|
|
11
|
-
report MD and writes
|
|
7
|
+
report MD and writes a single sibling:
|
|
12
8
|
|
|
13
|
-
<stem>.slim.md — token-saving AI consumption copy
|
|
14
9
|
<stem>.html — single-file self-contained human view with form
|
|
15
|
-
controls on §5 clarification rows
|
|
10
|
+
controls on §5 clarification rows (skipped when the
|
|
11
|
+
report has no §5 C-* clarification rows)
|
|
16
12
|
|
|
17
13
|
Usage:
|
|
18
14
|
okstra render-views <path-to-final-report.md>
|
|
@@ -23,45 +19,10 @@ When the optional flags are omitted the script infers from the report
|
|
|
23
19
|
path and its '- Task Type:' / '- Task Key:' lines.
|
|
24
20
|
`;
|
|
25
21
|
|
|
26
|
-
function resolveEntrypoint(paths) {
|
|
27
|
-
// Prefer the installed copy under ~/.okstra/bin (what production users
|
|
28
|
-
// see); fall back to the in-repo dev source when running from a
|
|
29
|
-
// checkout that hasn't been installed.
|
|
30
|
-
const installed = resolvePath(paths.home, "bin", "okstra-render-report-views.py");
|
|
31
|
-
if (existsSync(installed)) return installed;
|
|
32
|
-
const here = fileURLToPath(new URL("..", import.meta.url));
|
|
33
|
-
const dev = resolvePath(here, "scripts", "okstra-render-report-views.py");
|
|
34
|
-
if (existsSync(dev)) return dev;
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
22
|
export async function run(args) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (args.length === 0) {
|
|
44
|
-
process.stderr.write("error: missing <path-to-final-report.md>\n");
|
|
45
|
-
process.stderr.write(USAGE);
|
|
46
|
-
return 2;
|
|
47
|
-
}
|
|
48
|
-
const paths = await resolvePaths();
|
|
49
|
-
const entry = resolveEntrypoint(paths);
|
|
50
|
-
if (!entry) {
|
|
51
|
-
process.stderr.write(
|
|
52
|
-
"error: okstra-render-report-views.py not found. " +
|
|
53
|
-
"Run `okstra install` to install the runtime.\n",
|
|
54
|
-
);
|
|
55
|
-
return 1;
|
|
56
|
-
}
|
|
57
|
-
return await new Promise((res) => {
|
|
58
|
-
const child = spawn("python3", [entry, ...args], {
|
|
59
|
-
stdio: ["ignore", "inherit", "inherit"],
|
|
60
|
-
});
|
|
61
|
-
child.on("error", (err) => {
|
|
62
|
-
process.stderr.write(`error: ${err.message}\n`);
|
|
63
|
-
res(1);
|
|
64
|
-
});
|
|
65
|
-
child.on("close", (code) => res(code ?? 0));
|
|
23
|
+
return runInstalledScript({
|
|
24
|
+
scriptName: "okstra-render-report-views.py",
|
|
25
|
+
args,
|
|
26
|
+
usage: USAGE,
|
|
66
27
|
});
|
|
67
28
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { runInstalledScript } from "./_python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra spawn-followups — create follow-up task bundles from a final report
|
|
4
|
+
|
|
5
|
+
Wraps the python helper (\`okstra-spawn-followups.py\`) installed under
|
|
6
|
+
\`~/.okstra/bin/\` so skills call \`okstra spawn-followups\` instead of
|
|
7
|
+
emitting a \`python3 "$HOME/..."\` invocation (which breaks \`Bash(okstra:*)\`
|
|
8
|
+
permission matching and prompts on every call).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
okstra spawn-followups <args...>
|
|
12
|
+
|
|
13
|
+
All arguments are forwarded verbatim to the python helper. See
|
|
14
|
+
\`okstra spawn-followups --help\` for the full option list.
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export async function run(args) {
|
|
18
|
+
return runInstalledScript({
|
|
19
|
+
scriptName: "okstra-spawn-followups.py",
|
|
20
|
+
args,
|
|
21
|
+
usage: USAGE,
|
|
22
|
+
});
|
|
23
|
+
}
|
package/src/token-usage.mjs
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { promises as fs } from "node:fs";
|
|
4
|
-
import { resolvePaths } from "./paths.mjs";
|
|
1
|
+
import { runInstalledScript } from "./_python-helper.mjs";
|
|
5
2
|
|
|
6
3
|
const USAGE = `okstra token-usage — collect token usage for a run
|
|
7
4
|
|
|
@@ -15,37 +12,9 @@ Usage:
|
|
|
15
12
|
okstra token-usage <state-file> [--write] [--summary] [...]
|
|
16
13
|
|
|
17
14
|
Arguments and flags after the state-file path are forwarded verbatim to
|
|
18
|
-
the python helper. See \`
|
|
19
|
-
for the full option list.
|
|
15
|
+
the python helper. See \`okstra token-usage --help\` for the full option list.
|
|
20
16
|
`;
|
|
21
17
|
|
|
22
18
|
export async function run(args) {
|
|
23
|
-
|
|
24
|
-
process.stdout.write(USAGE);
|
|
25
|
-
return args.length === 0 ? 2 : 0;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const paths = await resolvePaths();
|
|
29
|
-
const script = join(paths.bin, "okstra-token-usage.py");
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
await fs.access(script);
|
|
33
|
-
} catch {
|
|
34
|
-
process.stderr.write(
|
|
35
|
-
`error: ${script} not found — run 'okstra install' (or 'okstra ensure-installed') first\n`,
|
|
36
|
-
);
|
|
37
|
-
return 1;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return await new Promise((resolve) => {
|
|
41
|
-
const child = spawn("python3", [script, ...args], {
|
|
42
|
-
stdio: "inherit",
|
|
43
|
-
env: { ...process.env, PYTHONPATH: paths.pythonpath },
|
|
44
|
-
});
|
|
45
|
-
child.on("error", (err) => {
|
|
46
|
-
process.stderr.write(`error: failed to spawn python3: ${err.message}\n`);
|
|
47
|
-
resolve(1);
|
|
48
|
-
});
|
|
49
|
-
child.on("close", (code) => resolve(typeof code === "number" ? code : 1));
|
|
50
|
-
});
|
|
19
|
+
return runInstalledScript({ scriptName: "okstra-token-usage.py", args, usage: USAGE });
|
|
51
20
|
}
|