okstra 0.30.2 → 0.31.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.
@@ -70,7 +70,7 @@ Every worker prompt MUST start with the following anchor headers, in this exact
70
70
 
71
71
  1. `**Project Root:** <absolute-path>` — absolute target project root (from `{{PROJECT_ROOT}}` in the lead's prompt). Required so the worker can self-anchor without relying on inherited cwd.
72
72
  2. `**Prompt History Path:** <project-relative-path>`
73
- 3. `**Result Path:** <project-relative-path>`
73
+ 3. `**Result Path:** <project-relative-path>` — canonical destination for the worker's result file. Workers resolve it to absolute against `**Project Root:**` and use it for the post-completion existence check (see codex-worker / gemini-worker step 8c, and Lead's redispatch policy below). The path identifies a single file; do NOT deliver a directory.
74
74
  4. `Assigned worker prompt history path: <absolute-path>` — same as the prompt-history path but resolved against `Project Root`. Codex/Gemini wrapper subagents extract this exact line.
75
75
 
76
76
  The body must include: role name, task type, task key, required bundle paths, assigned model, output contract, evidence handling rules, and any relevant config/deployment expectations from `reference-expectations.md`.
@@ -209,6 +209,29 @@ Terminal statuses that can be recorded for a worker:
209
209
  | `error` | Execution error, reason recorded; prompt history file must exist |
210
210
  | `not-run` | Not executed, reason recorded |
211
211
 
212
+ ## Lead Redispatch Policy on Result-Missing
213
+
214
+ After each worker subagent returns (regardless of role), Lead MUST verify the canonical result file exists at the absolute path resolved from the `**Result Path:**` anchor header (against `**Project Root:**`). The check is identical for in-process workers (claude-worker) and CLI-wrapper workers (codex-worker / gemini-worker).
215
+
216
+ **Triggers (any of):**
217
+
218
+ - The wrapper subagent returned an explicit `*_RESULT_MISSING` sentinel (codex-worker / gemini-worker step 8c — `CODEX_RESULT_MISSING` / `GEMINI_RESULT_MISSING`).
219
+ - The result file is absent at the resolved absolute path even though the worker returned without a `*_RESULT_MISSING` sentinel — for example, claude-worker returned its final assistant message but never persisted the artifact, or the wrapper exited 0 and the codex/gemini sub-agent forwarded raw stdout despite the contract.
220
+ - The result file exists but cannot be parsed (frontmatter unreadable, sections 1–5 entirely missing). A truncated file in the middle of section 5 is NOT covered here — it goes to the validator's regular `error` path, not the retry path.
221
+
222
+ **One-retry policy:**
223
+
224
+ 1. On the FIRST result-missing trigger for a given role within a single run, Lead MUST re-dispatch the same worker with the byte-identical prompt — same `**Result Path:**`, same `**Prompt History Path:**`, same model assignment, same `team_name`. The redispatch counts as a second attempt against the existing role slot; do NOT create a new role-id, do NOT change the result file path, do NOT switch to a different model as a "workaround".
225
+ 2. If the SECOND attempt also fails the same check, Lead records the role's terminal status as `error` with `--message "result-missing after 1 retry"` and proceeds to Phase 5.5 / Phase 6 with the remaining workers' results. Lead MUST NOT retry a third time — convergence and the report writer are designed to operate on reduced-confidence single-or-two-analyser mode when one role is absent (`agents/SKILL.md` "If only one worker result is usable: reduced-confidence synthesis").
226
+ 3. The retry counter is **per-run, per-role** and is NOT preserved across runs. A subsequent okstra run for the same task-key starts each role's counter fresh.
227
+ 4. Convergence reverify rounds (Phase 5.5) inherit the same one-retry budget — a reverify dispatch that triggers result-missing may be re-dispatched once.
228
+
229
+ **Logging.** Lead records the first attempt's `cli-failure` (already emitted by the wrapper sub-agent) as-is. The retry, on success, is logged via the normal worker-completion path; on failure (second `*_RESULT_MISSING`), Lead records a single `contract-violation` entry with `--message "result-missing after 1 retry"` referencing both attempts' bash_ids / prompt-history paths.
230
+
231
+ **Diagnostic sidecar (advisory).** Both codex/gemini wrappers also write a heartbeat sidecar at `<prompt-path>.status.json` recording `started_ts`, `ended_ts`, `exit_code`, `duration_ms`, and the canonical `log_path` (see `scripts/okstra-wrapper-status.py` for the schema). Lead MAY read this sidecar when deciding whether the first attempt actually launched the CLI (stage=`exited`, `exit_code=0`, non-zero `duration_ms`) versus failed before reaching it (sidecar absent, or stage=`started` with no exit fields). The sidecar is best-effort — its absence is NOT by itself a reason to skip the retry; the canonical trigger remains the missing result file.
232
+
233
+ **Rationale.** Observed failure mode: the CLI (codex/gemini) streams its full analysis to stdout but hits its token budget or a sandbox EPERM mid-`Write` of the result file, exiting 0 with no artifact. Forwarding the partial stdout silently degrades synthesis; classifying the role as `error` without retrying gives up a recoverable signal. A single retry catches the transient class of this failure (re-dispatch with the same prompt typically succeeds when the underlying cause was an intermittent sandbox lock or a token-budget spike) while bounding the retry cost to a known upper bound (~2× the original wrapper budget per role).
234
+
212
235
  ## Worker Output Contract
213
236
 
214
237
  **Authoritative source.** If other documents (SKILL.md, worker agent definitions) disagree with this section, this section wins.
@@ -355,8 +378,9 @@ empty run-level error logs in production.
355
378
  - **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:
356
379
  - Successful completion: return the wrapper's accumulated stdout from the final `BashOutput`. No log entry.
357
380
  - Non-zero `exit_code` reported by `BashOutput`: record a `cli-failure` to the run-level error log with the real `exit_code` and observed `duration-ms`.
358
- - 30-minute polling cap exceeded: call `KillShell(shell_id)` first, then record `cli-failure` with `--exit-code 124 --duration-ms 1800000 --message "<wrapper> exceeded 30m polling cap"`, then return the language-specific `*_CLI_TIMEOUT` sentinel.
381
+ - 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.
359
382
  - 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).
383
+ - **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.
360
384
  - `contract-violation` events (C) are recorded by Lead via `okstra-error-log.py append-observed --error-type contract-violation ...` after inspecting worker outputs.
361
385
  - Lead's responsibility regarding the sidecar is to dump it to the run-level error log via `okstra-error-log.py append-from-worker` after each worker terminates; Lead does not write into the sidecar.
362
386
 
@@ -419,7 +443,7 @@ Examples:
419
443
  **Task:** error-analysis
420
444
  **Target:** server/auth.ts
421
445
  **Date:** 2026-04-06
422
- **Model:** Report writer worker, opus
446
+ **Model:** Report writer worker, opus-4-6
423
447
  ```
424
448
 
425
449
  Use the actual model identifier recorded in team-state (never invent a model ID — read it from `resultContract.requiredWorkerRoles[*].modelExecutionValue` or the tool response metadata).
@@ -432,15 +456,13 @@ Token usage is collected from agent session transcripts after the run, NOT from
432
456
 
433
457
  ### How to Collect
434
458
 
435
- At the **start of Phase 7** (persistence), run the helper script with the path to this run's `team-state.json`:
459
+ At the **start of Phase 7** (persistence), run the helper via the okstra CLI with the path to this run's `team-state.json`. Substitute `<runDirectoryPath>` with a literal absolute path (no shell variables, no `$(...)`) so the `Bash(okstra:*)` permission match holds:
436
460
 
437
461
  ```bash
438
- python3 "$HOME/.okstra/lib/python/okstra-token-usage.py" \
439
- <runDirectoryPath>/state/team-state-<task-type>-<seq>.json \
440
- --write --summary
462
+ okstra token-usage /abs/path/to/run/state/team-state-<task-type>-<seq>.json --write --summary
441
463
  ```
442
464
 
443
- The script is installed at `$HOME/.okstra/lib/python/okstra-token-usage.py` by `okstra install`. The previous repo-relative path (`scripts/okstra-token-usage.py`) only exists in a working clone of the okstra repo and is not appropriate for end-user-deployed runs.
465
+ `okstra token-usage` is a thin Node-side wrapper around the python helper installed at `~/.okstra/bin/okstra-token-usage.py`. Calling the python script directly with `python3 "$HOME/..."` is forbidden the `$HOME` expansion breaks the literal-token permission match and forces a confirmation prompt every call.
444
466
 
445
467
  The script reads:
446
468
  - `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl` for the lead and every Claude-side worker (Claude worker, Report writer worker, plus the Claude wrappers around Codex/Gemini workers). Sessions are discovered by `teamName: okstra-<task-id>`, lead is identified by `lead.sessionId`, and other workers are identified by `agentName` (e.g. `claude-worker`, `codex-worker`, `gemini-worker`, `report-writer`).
@@ -599,22 +599,28 @@ Empty-state placeholder (copy verbatim when nothing else applies): `- No further
599
599
 
600
600
  ## 7. Follow-up Tasks (후속 작업)
601
601
 
602
- 본 run 구현·검증 범위를 **넘어서는** 작업을 후속 task 이어가기 위한 표.
602
+ 본 run 끝난 사용자가 이어갈 모든 task 표에 정리. 두 종류가 동거합니다:
603
603
 
604
- - `task-type` = `implementation` / `final-verification` / `release-handoff`: **필수**. 채울 항목이 없으면 대신 `- 후속 작업 없음.`
605
- - task-type: 선택. lead 가 필요하다고 판단할 때만.
604
+ 1. **phase-continuation row (필수)** — 본 task 의 다음 phase. `Suggested task-type` `## 2. Final Verdict` 다음 phase 와 byte-identical. `Auto-spawn? = no` 고정 (다음 phase 는 사용자가 `/okstra-run` 으로 직접 진입하며, spawn 스크립트가 task 디렉토리를 자동 생성하지 않음).
605
+ 2. **scope-boundary row (선택)** run 의 구현·검증 범위를 **넘어서는** 작업. lead 가 필요하다고 판단할 때만.
606
606
 
607
- 후속 항목 출처: `out-of-plan` / `verifier-concern` / `scope-boundary` / `open-question` / `manual` 중 하나.
607
+ phase-continuation row 의무 적용 범위:
608
608
 
609
- 규칙: `Auto-spawn? = yes` row 는 Phase 7 의 `scripts/okstra-spawn-followups.py` 가 자동으로 `tasks/<TASK_GROUP>/<New Task ID>/` 디렉터리 + `task-manifest.json` (status: `todo`) + stub task-brief 를 생성. `no` 는 사람이 처리. `New Task ID` task-group 유일한 알파숫자·하이픈 slug. 동일 follow-up 여러 run 등장하면 `New Task ID` 를 동일하게 유지하여 중복 spawn 방지.
609
+ - `task-type` {`requirements-discovery`, `implementation-planning`, `error-analysis`, `implementation`, `final-verification`}: **필수**. 다음 phase 자명하므로 phase-continuation row 개를 반드시 채움.
610
+ - `release-handoff`: 다음 phase 없음 → phase-continuation 생략. scope-boundary row 만 (있다면) 채움.
611
+
612
+ 후속 항목 출처(`Origin` 컬럼): `phase-continuation` / `out-of-plan` / `verifier-concern` / `scope-boundary` / `open-question` / `manual` 중 하나.
613
+
614
+ 규칙: `Auto-spawn? = yes` 인 row 는 Phase 7 의 `scripts/okstra-spawn-followups.py` 가 자동으로 `tasks/<TASK_GROUP>/<New Task ID>/` 디렉터리 + `task-manifest.json` (status: `todo`) + stub task-brief 를 생성. `no` 는 사람이 처리. `phase-continuation` row 는 항상 `no` — 같은 task-key 를 재사용하여 다음 phase 로 진입하므로 새 task 디렉터리를 만들면 안 됨. `New Task ID` 는 task-group 내 유일한 알파숫자·하이픈 slug(phase-continuation row 의 경우 현재 task-id 를 그대로 사용). 동일 follow-up 이 여러 run 에 등장하면 `New Task ID` 를 동일하게 유지하여 중복 spawn 방지.
610
615
 
611
616
  | ID | Ticket ID | Origin | New Task ID | Title | Suggested task-type | Scope (files/areas) | Reason / Why deferred | Priority (P0/P1/P2) | Auto-spawn? |
612
617
  |----|-----------|--------|-------------|-------|---------------------|---------------------|------------------------|---------------------|-------------|
613
- | FU-001 | `<TICKET-or-fallback>` | `<out-of-plan / verifier-concern / scope-boundary / open-question / manual>` | `<new-task-id-slug>` | <한 제목> | `<task-type>` | `<files / areas>` | <한 문장 사유> | `P1` | `yes` |
618
+ | FU-001 | `<TICKET-or-fallback>` | `phase-continuation` | `<current-task-id>` | <다음 phase 진입 요약> | `<next-task-type>` | `<scope summary>` | <다음 phase 자동으로 추천되는 사유 한 줄> | `P0` | `no` |
619
+ | FU-002 | `<TICKET-or-fallback>` | `<out-of-plan / verifier-concern / scope-boundary / open-question / manual>` | `<new-task-id-slug>` | <한 줄 제목> | `<task-type>` | `<files / areas>` | <한 두 문장 사유> | `P1` | `yes` |
614
620
 
615
- 상태: `- 후속 작업 없음.`
621
+ 상태(phase-continuation 의무가 없는 task-type 이고 scope-boundary 도 없을 때): `- 후속 작업 없음. 본 run 의 다음 phase 는 §6 (Recommended Next Steps) 참고.`
616
622
 
617
- 본 섹션이 채워진 경우 Section 6 의 "Follow-up tasks" 항목에 자동 생성된 task-key 진입 명령을 함께 적어 사용자가 즉시 이어갈 수 있게 합니다.
623
+ 본 섹션이 채워진 경우 Section 6 의 "Follow-up tasks" 항목에 진입 명령(phase-continuation 은 `/okstra-run` 형태, auto-spawn row 는 새 task-key)을 함께 적어 사용자가 즉시 이어갈 수 있게 합니다.
618
624
 
619
625
  ## Writing Rules
620
626
 
@@ -41,11 +41,38 @@ body {
41
41
  .report-header > div { flex: 1; font-weight: 600; }
42
42
 
43
43
  main {
44
- max-width: 80ch;
44
+ max-width: 120ch;
45
45
  margin: 1.5rem auto;
46
46
  padding: 0 1rem 4rem;
47
47
  }
48
48
 
49
+ nav.toc {
50
+ margin: 1rem 0 2rem;
51
+ padding: 0.6em 1em;
52
+ border: 1px solid color-mix(in srgb, GrayText 40%, transparent);
53
+ border-radius: 4px;
54
+ background: color-mix(in srgb, CanvasText 4%, transparent);
55
+ }
56
+ nav.toc .toc-title {
57
+ font-weight: 600;
58
+ margin-bottom: 0.4em;
59
+ font-size: 0.95rem;
60
+ }
61
+ nav.toc ul {
62
+ list-style: none;
63
+ margin: 0;
64
+ padding: 0;
65
+ columns: 2;
66
+ column-gap: 1.5em;
67
+ }
68
+ @media (max-width: 60ch) {
69
+ nav.toc ul { columns: 1; }
70
+ }
71
+ nav.toc li { margin: 0.15em 0; break-inside: avoid; }
72
+ nav.toc li.toc-h3 { padding-left: 1.2em; font-size: 0.92em; }
73
+ nav.toc a { color: inherit; text-decoration: none; }
74
+ nav.toc a:hover { text-decoration: underline; }
75
+
49
76
  h1, h2, h3, h4, h5, h6 { line-height: 1.25; margin-top: 1.6em; margin-bottom: 0.4em; }
50
77
  h1 { font-size: 1.7rem; }
51
78
  h2 { font-size: 1.35rem; border-bottom: 1px solid GrayText; padding-bottom: 0.2em; }
@@ -88,6 +115,16 @@ th, td {
88
115
  padding: 0.45em 0.6em;
89
116
  vertical-align: top;
90
117
  text-align: left;
118
+ word-break: keep-all;
119
+ overflow-wrap: anywhere;
120
+ min-width: 5ch;
121
+ }
122
+ /* Short single-token cells (<= 20 plain chars) — keep on one line so
123
+ * status / ID / kind columns do not get squeezed into narrow stacks
124
+ * by long-form neighbours. Class is applied by report_views._emit_table. */
125
+ td.td-tight, th.td-tight {
126
+ white-space: nowrap;
127
+ width: 1%; /* shrink-to-fit under auto-layout */
91
128
  }
92
129
  thead th {
93
130
  position: sticky;
@@ -105,18 +142,28 @@ tr[data-response-id][data-status="obsolete"] {
105
142
  opacity: 0.65;
106
143
  }
107
144
 
108
- textarea {
145
+ textarea,
146
+ select[data-response-id],
147
+ input[data-response-id],
148
+ input[data-other-for] {
109
149
  width: 100%;
110
- min-height: 2.2em;
111
150
  font: inherit;
112
151
  padding: 0.3em 0.4em;
113
152
  border: 1px solid GrayText;
114
153
  border-radius: 3px;
115
154
  background: Canvas;
116
155
  color: CanvasText;
156
+ }
157
+ textarea {
158
+ min-height: 2.2em;
117
159
  resize: vertical;
118
160
  }
119
- textarea[disabled] { opacity: 0.55; }
161
+ input[data-other-for] {
162
+ margin-top: 0.35em;
163
+ }
164
+ textarea[disabled],
165
+ select[disabled],
166
+ input[disabled] { opacity: 0.55; }
120
167
 
121
168
  button[data-action] {
122
169
  font: inherit;
@@ -1,10 +1,14 @@
1
1
  /* Client-side glue for the okstra final-report HTML view.
2
2
  *
3
3
  * Responsibilities:
4
- * 1. Collect <textarea> values for every <tr data-response-id> whose
5
- * Status is open/answered (disabled rows are skipped automatically).
6
- * 2. Serialise them into markdown whose bytes are IDENTICAL to
7
- * scripts/okstra_ctl/report_views.py serialize_user_response.
4
+ * 1. Collect entry values for every <tr data-response-id> whose
5
+ * Status is open/answered (disabled rows skipped automatically).
6
+ * Widgets: <select> (enum decision), <input data-other-for> (기타
7
+ * input revealed when select == "__other__"), <input
8
+ * data-response-id> (material/data-point single-line), <textarea>
9
+ * (everything else / fallback).
10
+ * 2. Serialise the entries into markdown whose bytes are IDENTICAL
11
+ * to scripts/okstra_ctl/report_views.py serialize_user_response.
8
12
  * 3. Write the result to <pre id="user-response-output"> and offer a
9
13
  * [Copy] button.
10
14
  *
@@ -36,14 +40,46 @@
36
40
  return String(s == null ? "" : s).replace(/^\s+|\s+$/g, "");
37
41
  }
38
42
 
43
+ // Read the user-supplied value out of one clarification row. Returns
44
+ // the empty string when the row has no usable widget, the widget is
45
+ // disabled, the select sits on its blank "(선택)" placeholder, or the
46
+ // "기타" branch has an empty input. Caller decides whether to emit
47
+ // an entry.
48
+ function readRowValue(row) {
49
+ var sel = row.querySelector("select[data-response-id]");
50
+ if (sel) {
51
+ if (sel.disabled) return "";
52
+ var picked = sel.value;
53
+ if (picked === "") return "";
54
+ if (picked === "__other__") {
55
+ var rid = sel.getAttribute("data-response-id") || "";
56
+ var other = row.querySelector('input[data-other-for="' + rid + '"]');
57
+ return other ? trimMultiline(other.value) : "";
58
+ }
59
+ var opt = sel.options[sel.selectedIndex];
60
+ // Use the visible option text ("(a) one-time backfill") so the
61
+ // user-response sidecar stays human-readable, not just "a".
62
+ return opt ? trimMultiline(opt.textContent) : picked;
63
+ }
64
+ var ta = row.querySelector("textarea[data-response-id]");
65
+ if (ta) {
66
+ if (ta.disabled) return "";
67
+ return trimMultiline(ta.value);
68
+ }
69
+ var inp = row.querySelector("input[data-response-id]");
70
+ if (inp) {
71
+ if (inp.disabled) return "";
72
+ return trimMultiline(inp.value);
73
+ }
74
+ return "";
75
+ }
76
+
39
77
  function collectEntries() {
40
78
  var entries = [];
41
79
  var rows = document.querySelectorAll("tr[data-response-id]");
42
80
  for (var i = 0; i < rows.length; i++) {
43
81
  var row = rows[i];
44
- var ta = row.querySelector("textarea[data-response-id]");
45
- if (!ta || ta.disabled) continue;
46
- var value = trimMultiline(ta.value);
82
+ var value = readRowValue(row);
47
83
  if (!value) continue;
48
84
  entries.push({
49
85
  responseId: row.getAttribute("data-response-id") || "",
@@ -55,6 +91,25 @@
55
91
  return entries;
56
92
  }
57
93
 
94
+ // Toggle the visibility of the "기타" companion input next to each
95
+ // select whose current value is "__other__". Wired at bind() time and
96
+ // also called once for the initial state.
97
+ function bindOtherInputToggle() {
98
+ var selects = document.querySelectorAll("select[data-response-id]");
99
+ for (var i = 0; i < selects.length; i++) {
100
+ (function (sel) {
101
+ var rid = sel.getAttribute("data-response-id") || "";
102
+ var other = document.querySelector('input[data-other-for="' + rid + '"]');
103
+ if (!other) return;
104
+ var update = function () {
105
+ other.hidden = sel.value !== "__other__";
106
+ };
107
+ sel.addEventListener("change", update);
108
+ update();
109
+ })(selects[i]);
110
+ }
111
+ }
112
+
58
113
  function buildUserResponseMarkdown(runMeta, entries, createdAt) {
59
114
  var head =
60
115
  "---\n" +
@@ -132,6 +187,7 @@
132
187
  btn.addEventListener("click", copyUserResponse);
133
188
  }
134
189
  }
190
+ bindOtherInputToggle();
135
191
  }
136
192
 
137
193
  if (typeof window !== "undefined") {
@@ -135,6 +135,7 @@
135
135
  "Bash(npx -y okstra@latest:*)",
136
136
  "Bash($HOME/.okstra/bin/:*)",
137
137
  "Bash(STATE_FILE=:*)",
138
+ "Bash(ROOT=:*)",
138
139
 
139
140
  "Bash(gemini)",
140
141
  "Bash(gemini:*)",
package/src/install.mjs CHANGED
@@ -25,6 +25,7 @@ const BIN_ENTRYPOINTS = [
25
25
  "okstra-token-usage.py",
26
26
  "okstra-error-log.py",
27
27
  "okstra-render-report-views.py",
28
+ "okstra-wrapper-status.py",
28
29
  ];
29
30
 
30
31
  const INSTALL_USAGE = `okstra install — install runtime into ~/.okstra
@@ -0,0 +1,51 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { promises as fs } from "node:fs";
4
+ import { resolvePaths } from "./paths.mjs";
5
+
6
+ const USAGE = `okstra token-usage — collect token usage for a run
7
+
8
+ Wraps the python helper (\`okstra-token-usage.py\`) installed under
9
+ \`~/.okstra/bin/\` so skills can call this command without emitting a
10
+ shell-expansion-bearing \`python3 "$HOME/..."\` invocation (which would
11
+ break \`Bash(okstra:*)\` permission matching and force a confirmation
12
+ prompt every call).
13
+
14
+ Usage:
15
+ okstra token-usage <state-file> [--write] [--summary] [...]
16
+
17
+ Arguments and flags after the state-file path are forwarded verbatim to
18
+ the python helper. See \`python3 ~/.okstra/bin/okstra-token-usage.py --help\`
19
+ for the full option list.
20
+ `;
21
+
22
+ export async function run(args) {
23
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
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
+ });
51
+ }