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.
- package/bin/okstra +4 -0
- package/docs/kr/architecture.md +2 -2
- package/docs/kr/cli.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +4 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +23 -6
- package/runtime/agents/workers/gemini-worker.md +23 -6
- package/runtime/agents/workers/report-writer-worker.md +2 -1
- package/runtime/bin/okstra-codex-exec.sh +31 -0
- package/runtime/bin/okstra-gemini-exec.sh +26 -0
- package/runtime/python/lib/okstra/globals.sh +1 -1
- package/runtime/python/lib/okstra/usage.sh +2 -2
- package/runtime/python/okstra_ctl/models.py +2 -0
- package/runtime/python/okstra_ctl/report_views.py +186 -10
- package/runtime/python/okstra_ctl/run.py +1 -1
- package/runtime/python/okstra_ctl/wizard.py +53 -14
- package/runtime/python/okstra_ctl/workers.py +45 -11
- package/runtime/python/okstra_token_usage/pricing.py +1 -0
- package/runtime/skills/okstra-logs/SKILL.md +2 -6
- package/runtime/skills/okstra-report-writer/SKILL.md +2 -2
- package/runtime/skills/okstra-run/SKILL.md +6 -4
- package/runtime/skills/okstra-team-contract/SKILL.md +30 -8
- package/runtime/templates/reports/final-report.template.md +14 -8
- package/runtime/templates/reports/report.css +51 -4
- package/runtime/templates/reports/report.js +63 -7
- package/runtime/templates/reports/settings.template.json +1 -0
- package/src/install.mjs +1 -0
- package/src/token-usage.mjs +51 -0
|
@@ -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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
602
|
+
본 run 이 끝난 뒤 사용자가 이어갈 모든 task 를 한 표에 정리. 두 종류가 동거합니다:
|
|
603
603
|
|
|
604
|
-
- `task-type`
|
|
605
|
-
-
|
|
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
|
-
|
|
607
|
+
phase-continuation row 의무 적용 범위:
|
|
608
608
|
|
|
609
|
-
|
|
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>` |
|
|
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" 항목에
|
|
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:
|
|
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
|
-
|
|
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
|
|
5
|
-
* Status is open/answered (disabled rows
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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") {
|
package/src/install.mjs
CHANGED
|
@@ -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
|
+
}
|