okstra 0.23.0 → 0.24.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 +3 -0
- package/docs/kr/architecture.md +2 -2
- package/docs/kr/cli.md +1 -0
- package/docs/project-structure-overview.md +4 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +1 -1
- package/runtime/python/okstra_ctl/wizard.py +1249 -0
- package/runtime/python/okstra_token_usage/collect.py +12 -1
- package/runtime/skills/okstra-report-writer/SKILL.md +1 -0
- package/runtime/skills/okstra-run/SKILL.md +111 -247
- package/runtime/skills/okstra-team-contract/SKILL.md +1 -0
- package/src/wizard.mjs +105 -0
|
@@ -90,8 +90,19 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
|
|
|
90
90
|
# when TeamCreate succeeded); only fall back to the `okstra-<task-id>`
|
|
91
91
|
# convention if team-state did not record one. Matching downstream is
|
|
92
92
|
# case-insensitive so either casing works.
|
|
93
|
+
# Lead-written teamName lives at one of two paths depending on which
|
|
94
|
+
# version of the contract the run was authored under:
|
|
95
|
+
# - nested: state.team.teamName (current documented schema)
|
|
96
|
+
# - root: state.teamName (older convention; still common in
|
|
97
|
+
# actual runs because the team
|
|
98
|
+
# contract docs did not pin the
|
|
99
|
+
# location until v0.24)
|
|
100
|
+
# Read both; whichever is non-empty wins. The fallback derives a short
|
|
101
|
+
# team name from task-id only and routinely mis-matches multi-segment
|
|
102
|
+
# task keys (e.g. `okstra-fontsninja-classifier-v2:DEV-9389:DEV-9389`),
|
|
103
|
+
# so it is a last resort.
|
|
93
104
|
state_team = (state.get("team") or {})
|
|
94
|
-
team_name = state_team.get("teamName") or ""
|
|
105
|
+
team_name = state_team.get("teamName") or state.get("teamName") or ""
|
|
95
106
|
if not team_name:
|
|
96
107
|
task_id = task_key.rsplit(":", 1)[-1] if task_key else ""
|
|
97
108
|
team_name = f"okstra-{task_id}" if task_id else ""
|
|
@@ -169,6 +169,7 @@ Place this section immediately after the execution status table.
|
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
Token Summary Generation Rules:
|
|
172
|
+
- **You author this section in Phase 6, BEFORE Phase 7 runs the collector.** Therefore you MUST leave the 10 placeholders (`{{LEAD_TOTAL_TOKENS}}`, `{{LEAD_BILLABLE_TOKENS}}`, `{{LEAD_COST_USD}}`, `{{WORKER_TOTAL_TOKENS}}`, `{{WORKER_BILLABLE_TOKENS}}`, `{{WORKER_COST_USD}}`, `{{GRAND_TOTAL_TOKENS}}`, `{{GRAND_BILLABLE_TOKENS}}`, `{{GRAND_COST_USD}}`, `{{CLI_COST_USD}}`) verbatim in the table cells — `okstra-token-usage.py --substitute-final-report` will fill them in Phase 7. Never replace any of these cells with a literal number, `not-collected`, `N/A`, `--`, `0`, or any other sentinel: that erases the substitution target, and the report ships with no token numbers. Also do not insert a note like "Phase 7 has not run yet" — the report is read AFTER Phase 7, so that statement is wrong on arrival.
|
|
172
173
|
- All values come from `usageSummary` (populated by `scripts/okstra-token-usage.py` at the start of Phase 7). Do not estimate or invent.
|
|
173
174
|
- **Lead** row: `usageSummary.leadTotalTokens` / `usageSummary.leadBillableEquivalentTokens` / `usageSummary.estimatedCostUsd.lead`.
|
|
174
175
|
- **Worker 합계** row: `usageSummary.workerTotalTokens` / `usageSummary.workerBillableEquivalentTokens` / `usageSummary.estimatedCostUsd.claudeWorkers`.
|
|
@@ -5,13 +5,13 @@ description: Use when the user wants to start an okstra task (cross-verification
|
|
|
5
5
|
|
|
6
6
|
# OKSTRA Run (in-session)
|
|
7
7
|
|
|
8
|
-
Launch an okstra task — gather inputs interactively
|
|
8
|
+
Launch an okstra task — gather inputs interactively via the **wizard state machine** (`okstra wizard ...`), then take over as `Claude lead` in the current session.
|
|
9
9
|
|
|
10
|
-
**Single authority**: this skill
|
|
10
|
+
**Single authority**: this skill drives `okstra wizard`, which owns every step (ordering, branching, validation). The skill is just a thin prompt-relay loop — it never decides "what to ask next" itself. If the flow needs to change, edit `scripts/okstra_ctl/wizard.py`, not this file.
|
|
11
11
|
|
|
12
12
|
## When to Use
|
|
13
13
|
|
|
14
|
-
- The user is
|
|
14
|
+
- The user is inside a Claude Code session and asks to start an okstra task ("run okstra here", "start an error-analysis on this branch", "okstra implementation-planning for INV-1234").
|
|
15
15
|
- Continue an existing task (next phase) without leaving the current claude session.
|
|
16
16
|
|
|
17
17
|
## When NOT to Use
|
|
@@ -20,51 +20,47 @@ Launch an okstra task — gather inputs interactively, render the full task bund
|
|
|
20
20
|
- User wants status only — use `okstra-status`.
|
|
21
21
|
- User wants past runs — use `okstra-history`.
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## How the wizard talks to you
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Every wizard call returns JSON. The two shapes you'll see:
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
```json
|
|
28
|
+
{ "ok": true, "echo": "task-group: backend-api",
|
|
29
|
+
"next": { "step": "task_id", "kind": "text", "label": "...", "options": [], "echoTemplate": "..." } }
|
|
30
|
+
```
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
```json
|
|
33
|
+
{ "ok": false, "error": "approved plan has no APPROVED marker: ...",
|
|
34
|
+
"current": { "step": "approved_plan", "kind": "text", "label": "..." } }
|
|
35
|
+
```
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
On `ok: false`, re-prompt with the same `current.step` using the error message. The wizard never advances on validation failure; the user retries the same step.
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
**runtime installation** (stable across runs) — they are NOT per-task identity.
|
|
39
|
+
The wizard tells you *which UI to use* via `kind`:
|
|
37
40
|
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
- `<task-root>/task-manifest.json`
|
|
41
|
+
- `kind: "pick"` → render `AskUserQuestion` with `label` and `options[].label` (use `options[].value` to call `--answer`).
|
|
42
|
+
- `kind: "text"` → write `label` as a plain text message and consume the user's NEXT message as the answer.
|
|
43
|
+
- `kind: "done"` → input collection finished; move to Step 5.
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
Never invent additional questions. Never reorder. Never use `AskUserQuestion` for `text` prompts — the wizard explicitly chose `text` to avoid the picker-Other re-render lag.
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
the single authority — `okstra`:
|
|
47
|
+
## Step 1: Verify okstra runtime + project setup
|
|
47
48
|
|
|
48
49
|
```bash
|
|
49
|
-
# 0) Resolve runner: prefer PATH (npm-installed) over npx (avoids per-call registry lookup).
|
|
50
|
-
# If the user installed okstra via npm, they control upgrade timing — do not force @latest.
|
|
51
50
|
if command -v okstra >/dev/null 2>&1; then
|
|
52
51
|
OKSTRA_CMD="okstra"
|
|
53
52
|
else
|
|
54
53
|
OKSTRA_CMD="npx -y okstra@latest"
|
|
55
54
|
fi
|
|
56
55
|
|
|
57
|
-
# 1) Ensure runtime is fresh (idempotent, cached when up-to-date)
|
|
58
56
|
$OKSTRA_CMD ensure-installed >/dev/null 2>&1 || {
|
|
59
57
|
echo "FAIL: okstra not installed; tell the user to run: npx okstra@latest install" >&2
|
|
60
58
|
exit 1
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
# 2) Load all runtime paths into the shell as OKSTRA_* exports
|
|
64
61
|
eval "$($OKSTRA_CMD paths --shell)"
|
|
65
62
|
export PYTHONPATH="$OKSTRA_PYTHONPATH"
|
|
66
63
|
|
|
67
|
-
# 3) Verify the current project has okstra metadata (project.json + projectId)
|
|
68
64
|
OKSTRA_PROJECT_INFO="$($OKSTRA_CMD check-project --json)" || {
|
|
69
65
|
echo "FAIL: this project has no okstra setup. Tell the user to run /okstra-setup first." >&2
|
|
70
66
|
echo "$OKSTRA_PROJECT_INFO" >&2
|
|
@@ -72,250 +68,101 @@ OKSTRA_PROJECT_INFO="$($OKSTRA_CMD check-project --json)" || {
|
|
|
72
68
|
}
|
|
73
69
|
```
|
|
74
70
|
|
|
75
|
-
|
|
71
|
+
If `OKSTRA_PROJECT_INFO.ok` is `false`, ask the user with a **plain text prompt** for an absolute project-root path; rerun `okstra check-project --cwd <path>`. Re-prompt with plain text on failure.
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|---|---|
|
|
79
|
-
| `$OKSTRA_WORKSPACE` | passed to python as `workspace_root` (prompts/, templates/, validators/, agents/ root) |
|
|
80
|
-
| `$OKSTRA_AGENTS_DIR` | source dir of worker `*.md` (subagent definitions) |
|
|
81
|
-
| `$OKSTRA_PYTHONPATH` | already exported as `PYTHONPATH` |
|
|
82
|
-
| `$OKSTRA_BIN` | bash entrypoints (`okstra.sh`, codex/gemini exec wrappers) |
|
|
83
|
-
| `$OKSTRA_HOME` | `~/.okstra` (recent.jsonl, locks, projects/, archive/) |
|
|
84
|
-
| `$OKSTRA_PROJECT_INFO` | JSON: `{ok, projectRoot, projectJsonPath, projectId}` — parse and reuse instead of re-resolving in Step 1 |
|
|
73
|
+
Parse `projectRoot` and `projectId` from `OKSTRA_PROJECT_INFO`.
|
|
85
74
|
|
|
86
|
-
## Step
|
|
87
|
-
|
|
88
|
-
Prefer `$OKSTRA_PROJECT_INFO` from Step 0 — it already carries `{ok, projectRoot, projectJsonPath, projectId}`. Only re-resolve when that JSON's `ok` is false (cwd outside an okstra project):
|
|
75
|
+
## Step 2: Initialize the wizard
|
|
89
76
|
|
|
90
77
|
```bash
|
|
91
|
-
|
|
92
|
-
```
|
|
78
|
+
STATE_FILE="$(mktemp -t okstra-wizard.XXXX.json)"
|
|
93
79
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
okstra task-list --project "$PROJECT_ROOT"
|
|
80
|
+
okstra wizard init \
|
|
81
|
+
--state-file "$STATE_FILE" \
|
|
82
|
+
--project-root "$projectRoot" \
|
|
83
|
+
--project-id "$projectId"
|
|
101
84
|
```
|
|
102
85
|
|
|
103
|
-
Output
|
|
104
|
-
|
|
105
|
-
Use `AskUserQuestion`:
|
|
106
|
-
|
|
107
|
-
- **Label**: "Which task?"
|
|
108
|
-
- **Options**: each existing task with label `"<taskKey> · <currentPhase or taskType> · next: <nextRecommendedPhase>"`; mark the `latest` entry with `(latest)`. Final option: `"Start a brand-new task"`. Limit to 8 candidates per page; add `"More..."` if more exist.
|
|
109
|
-
|
|
110
|
-
For an existing pick, read its `task-manifest.json` to capture `taskType` and `workflow.nextRecommendedPhase`.
|
|
111
|
-
|
|
112
|
-
## Step 3: For new tasks — collect identity
|
|
113
|
-
|
|
114
|
-
Skip if continuing existing.
|
|
115
|
-
|
|
116
|
-
Use **plain text prompts** (one at a time — write the message and consume the user's next reply; do NOT use `AskUserQuestion` for these per the convention above):
|
|
117
|
-
|
|
118
|
-
1. `"Task group 을 알려주세요 (예: backend-api, INV-1234, refactor)"` → `task_group`
|
|
119
|
-
2. `"Task id 를 알려주세요 (예: login-error-analysis, dev-9043)"` → `task_id`
|
|
120
|
-
|
|
121
|
-
Validate that slugified `task_group` and `task_id` each contain at least one alphanumeric character. On failure, re-prompt with another plain text message stating the validation failure.
|
|
86
|
+
Output: the same `{ok, next}` JSON described above. The first `next` is always `step: "task_pick"`.
|
|
122
87
|
|
|
123
|
-
## Step
|
|
88
|
+
## Step 3: Run the prompt loop
|
|
124
89
|
|
|
125
|
-
|
|
90
|
+
Repeat until `next.kind == "done"`:
|
|
126
91
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
92
|
+
1. **Render** the prompt according to `kind`:
|
|
93
|
+
- `pick` → `AskUserQuestion` with `label` and `options`. The user's chosen option's `value` is the answer string.
|
|
94
|
+
- `text` → plain text message containing `label`. Consume the user's next reply verbatim as the answer string (empty reply = empty string).
|
|
95
|
+
2. **Submit** the answer:
|
|
96
|
+
```bash
|
|
97
|
+
okstra wizard step --state-file "$STATE_FILE" --answer "$ANSWER"
|
|
98
|
+
```
|
|
99
|
+
3. **Handle result**:
|
|
100
|
+
- `ok: true` → echo `result.echo` to the user on one short line, then loop with `result.next`.
|
|
101
|
+
- `ok: false` → show `result.error` to the user verbatim, then loop with `result.current` (re-prompt the same step).
|
|
135
102
|
|
|
136
|
-
|
|
103
|
+
That is the entire interactive flow. The wizard handles:
|
|
137
104
|
|
|
138
|
-
|
|
139
|
-
-
|
|
140
|
-
-
|
|
105
|
+
- new-vs-existing task split, task-group / task-id slug validation,
|
|
106
|
+
- task-type pick (with `nextRecommendedPhase` surfaced as recommended for existing tasks),
|
|
107
|
+
- brief path (with `유지 / 변경` for existing tasks),
|
|
108
|
+
- base-ref pick + git rev-parse validation (skipped when reusing an active worktree),
|
|
109
|
+
- `implementation`-only sub-flow: approved-plan path (APPROVED marker check) + executor pick,
|
|
110
|
+
- `Use defaults / Customize` branch with profile-aware worker/model questions,
|
|
111
|
+
- `release-handoff` PR template override + persist scope,
|
|
112
|
+
- final `Proceed / Edit` confirmation; on `Edit` the wizard asks which step to rewind to and clears every later answer.
|
|
141
113
|
|
|
142
|
-
|
|
114
|
+
Do not second-guess the wizard. If the next prompt seems out of place, the bug is in `wizard.py`, not in your interpretation of the user's input.
|
|
143
115
|
|
|
144
|
-
|
|
145
|
-
and reuses it on every subsequent phase. The base ref of that worktree is the
|
|
146
|
-
**user's choice**, not the caller's current `HEAD`, so the worktree never
|
|
147
|
-
silently inherits an unrelated branch you happen to be checked out on.
|
|
116
|
+
## Step 4: Show the confirmation block before the final Proceed
|
|
148
117
|
|
|
149
|
-
|
|
118
|
+
When `next.step == "confirm"`, before relaying the picker, fetch the human-readable selection summary:
|
|
150
119
|
|
|
151
120
|
```bash
|
|
152
|
-
okstra
|
|
121
|
+
okstra wizard confirmation --state-file "$STATE_FILE"
|
|
153
122
|
```
|
|
154
123
|
|
|
155
|
-
Output
|
|
156
|
-
non-null `entry` with `status: "active"` → **REUSE**.
|
|
124
|
+
Output: `{ok: true, text: "선택 확인:\n task-type : ...\n ..."}`. Print `text` to the user, then render the `confirm` picker (Proceed / Edit).
|
|
157
125
|
|
|
158
|
-
|
|
159
|
-
question (the registered base is authoritative).
|
|
160
|
-
- `ASK` → this is the first phase for this task-key. Continue.
|
|
126
|
+
## Step 5: Render the task bundle
|
|
161
127
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
1. `AskUserQuestion` with label `"이 task worktree 의 base branch?"` and exactly these single-select options (NO auto-Other typing — the literal `직접 입력` option is the typed-input escape hatch):
|
|
165
|
-
1. `main` (recommended)
|
|
166
|
-
2. `dev`
|
|
167
|
-
3. `staging`
|
|
168
|
-
4. `preprod`
|
|
169
|
-
5. `prod`
|
|
170
|
-
6. `직접 입력`
|
|
171
|
-
2. If the user picks `직접 입력`, follow up with a **plain text prompt**: `"base ref 를 입력해주세요 (branch, tag, 또는 short/full SHA)"`. Consume the user's next message as the chosen ref.
|
|
172
|
-
3. Otherwise the picked option label is the chosen ref directly.
|
|
173
|
-
|
|
174
|
-
Validate the chosen ref exists in the MAIN worktree before continuing:
|
|
128
|
+
When `next.kind == "done"`, fetch the final args:
|
|
175
129
|
|
|
176
130
|
```bash
|
|
177
|
-
|
|
178
|
-
rev-parse --verify --quiet "<chosen-ref>^{commit}" >/dev/null \
|
|
179
|
-
|| { echo "ref not found locally: <chosen-ref>"; exit 1; }
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
On failure, re-prompt with a plain text message (or return to step 1's
|
|
183
|
-
menu if the user wants to pick a different canonical branch). Echo the
|
|
184
|
-
resolved short SHA back to the user (`base 확정: <ref> (<short-sha>)`)
|
|
185
|
-
and capture `base_ref=<chosen-ref>` for Step 7.
|
|
186
|
-
|
|
187
|
-
## Step 5: Brief path
|
|
188
|
-
|
|
189
|
-
- New task: **plain text prompt** (file path is pure free text per the convention) `"task brief markdown 의 경로를 알려주세요 (project root 기준 상대경로 또는 절대경로)"`. Consume the user's next message; verify the file exists; on failure, re-prompt with another plain text message.
|
|
190
|
-
- Existing task: default to the manifest's `taskBriefPath`. Show it; ask `AskUserQuestion` `"기존 경로를 유지할까요?"` with options `유지` / `변경`. On `변경`, follow up with a plain text prompt for the new path.
|
|
191
|
-
|
|
192
|
-
## Step 6 (optional): Directive / workers / models / related / clarification
|
|
193
|
-
|
|
194
|
-
Single `AskUserQuestion` first: `"기본 워커/모델로 진행할까요, 아니면 커스터마이즈할까요?"` (options: `Use defaults`, `Customize`).
|
|
195
|
-
|
|
196
|
-
- `Use defaults` → all overrides remain empty.
|
|
197
|
-
- `Customize` → the prompts you ask depend on the `task_type` chosen in Step 4. Blank answer always means "use default". Never call the prompt label "worker CSV" — use plain Korean labels as shown below.
|
|
198
|
-
|
|
199
|
-
### Model selection options (used by 6a and 6b)
|
|
200
|
-
|
|
201
|
-
All model prompts MUST use `AskUserQuestion` with a fixed option list — never free text. This prevents typos like `gpt-5.5-high` (a non-existent model) reaching the manifest. The options below are derived from `scripts/okstra_ctl/models.py` `*_MAPPING` and show "default + 3 latest". Blank/`default` means "use phase default".
|
|
202
|
-
|
|
203
|
-
- **Claude (lead / claude-worker / report-writer)** options: `default`, `opus`, `sonnet`, `haiku`
|
|
204
|
-
- **Codex (codex-worker)** options: `default`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`
|
|
205
|
-
- **Gemini (gemini-worker)** options: `default`, `gemini-3-pro-preview`, `gemini-3-flash-preview`, `auto`
|
|
206
|
-
|
|
207
|
-
When the user picks `default`, pass an empty string to the corresponding `--*-model` flag. Pick any other option ⇒ pass it verbatim. If the user truly needs a value outside the list (e.g. a pinned long-form id), they can use the question's built-in `Other` to type it — but the four canonical options cover the supported set, so `Other` should be rare.
|
|
208
|
-
|
|
209
|
-
### 6a. `implementation` phase (executor-driven)
|
|
210
|
-
|
|
211
|
-
In this phase the roster is fixed by the profile (executor + two verifiers + report-writer). The Step 4 `executor` answer already determines who mutates code; verifier models use phase-specific defaults (`Claude verifier`=sonnet, `Codex verifier`=gpt-5.5, `Gemini verifier`=auto). So ask **only three model prompts** (each via `AskUserQuestion` with options from the table above), plus directive/related/clarification:
|
|
212
|
-
|
|
213
|
-
1. `AskUserQuestion` `"리더(Claude lead) 모델?"` (Claude options) → `lead_model`
|
|
214
|
-
2. `AskUserQuestion` `"실행자({executor-provider}) 모델?"` with options matching the executor's provider (Claude / Codex / Gemini list above) → maps to `claude_model` / `codex_model` / `gemini_model`. The other two provider model fields stay empty (verifiers use defaults).
|
|
215
|
-
3. `AskUserQuestion` `"리포트 작성자(report-writer) 모델?"` (Claude options) → `report_writer_model`
|
|
216
|
-
4. **Plain text prompt** (free text) `"추가 directive 가 있으면 적어주세요 (없으면 빈 줄)"` → `directive`. Consume the user's next message verbatim; an empty line means "no directive".
|
|
217
|
-
5. **Plain text prompt** (free text) `"관련 task id 목록을 쉼표로 구분해서 적어주세요 (없으면 빈 줄)"` → `related_tasks_raw`.
|
|
218
|
-
|
|
219
|
-
Do NOT ask for `workers_override` in implementation — the profile's required roster must be preserved (verifier slots are mandatory). Leave `workers_override=""`.
|
|
220
|
-
|
|
221
|
-
### 6b. Other phases (`requirements-discovery`, `error-analysis`, `implementation-planning`, `final-verification`, `release-handoff`)
|
|
222
|
-
|
|
223
|
-
**Before asking any worker/model question, resolve the profile's allowed roster:**
|
|
224
|
-
|
|
225
|
-
```python
|
|
226
|
-
from okstra_ctl.workers import resolve_profile_workers
|
|
227
|
-
profile_workers = resolve_profile_workers(Path("<OKSTRA_PROMPTS_PROFILES_DIR>/<task-type>.md"))
|
|
131
|
+
okstra wizard render-args --state-file "$STATE_FILE"
|
|
228
132
|
```
|
|
229
133
|
|
|
230
|
-
|
|
231
|
-
workers outside this list. Special cases:
|
|
232
|
-
|
|
233
|
-
- If `profile_workers` is empty (e.g., `release-handoff` is lead-only with no
|
|
234
|
-
`- Required workers:` block), **skip the worker question and all
|
|
235
|
-
worker-model questions entirely** — only ask lead model, directive, related,
|
|
236
|
-
clarification. The backend forces `workers=[]` for these profiles.
|
|
237
|
-
- Otherwise, the worker question must enumerate **only** `profile_workers` —
|
|
238
|
-
do NOT show `claude, codex, gemini, report-writer` blindly.
|
|
239
|
-
|
|
240
|
-
Ask each in turn. **Model prompts use `AskUserQuestion`** with the fixed option lists above. **All other prompts use plain text messages** (do NOT wrap free-text inputs in `AskUserQuestion` — the auto-Other re-render lag is what we're avoiding). Skip any worker-model prompt whose worker is not in `profile_workers`.
|
|
241
|
-
|
|
242
|
-
1. (only when `profile_workers` is non-empty) **Plain text prompt** `"참여 워커 목록을 쉼표로 구분해서 적어주세요. 빈 줄이면 프로필 기본값 <profile_workers_csv> 을 그대로 씁니다. 사용 가능한 워커: <profile_workers_csv>"` → `workers_override`. Validate the answer is a subset of `profile_workers`; on failure, re-prompt with another plain text message. (Backend also rejects violations with `WorkersError`.)
|
|
243
|
-
2. `AskUserQuestion` `"리더(Claude lead) 모델?"` (Claude options) → `lead_model`
|
|
244
|
-
3. (only if `claude` ∈ resolved workers) `AskUserQuestion` `"claude 워커 모델?"` (Claude options) → `claude_model`
|
|
245
|
-
4. (only if `codex` ∈ resolved workers) `AskUserQuestion` `"codex 워커 모델?"` (Codex options) → `codex_model`
|
|
246
|
-
5. (only if `gemini` ∈ resolved workers) `AskUserQuestion` `"gemini 워커 모델?"` (Gemini options) → `gemini_model`
|
|
247
|
-
6. (only if `report-writer` ∈ resolved workers) `AskUserQuestion` `"리포트 작성자 모델?"` (Claude options) → `report_writer_model`
|
|
248
|
-
7. `AskUserQuestion` `"추가 directive (선택, 빈 칸 가능)"` (free text) → `directive`
|
|
249
|
-
8. `AskUserQuestion` `"관련 task id 목록, 쉼표 구분 (선택, 빈 칸 가능)"` (free text) → `related_tasks_raw`
|
|
250
|
-
9. `AskUserQuestion` `"clarification-response 파일 경로 (follow-up 시에만, 빈 칸 가능)"` (free text) → `clarification_response_path`
|
|
251
|
-
10. (only when `task_type == "release-handoff"`) **Plain text prompt** `"PR 본문 템플릿 경로 1회성 override (빈 줄이면 project.json → ~/.okstra/config.json → 스킬 디폴트 순으로 자동 해석)"` → `pr_template_path`. The backend (`okstra_ctl.pr_template.resolve_pr_template_path`) validates the file exists and surfaces `PrTemplateError` on failure.
|
|
252
|
-
- **Persist follow-up** (only when the user typed a non-empty path AND it differs from any currently-registered project/global value): ask `AskUserQuestion` `"방금 입력한 경로를 영구 저장할까요?"` with three options:
|
|
253
|
-
1. `이번 run 만 (1회성)` — proceed with the override; do NOT touch project.json or global config.
|
|
254
|
-
2. `프로젝트에 저장 (project scope)` — run `okstra config set pr-template-path "<path>" --scope project` and use the override for this run too.
|
|
255
|
-
3. `전역에 저장 (global scope)` — run `okstra config set pr-template-path "<path>" --scope global` (must be absolute or `~/`-prefixed; if not, re-ask with a plain text prompt for an absolute version) and use the override for this run too.
|
|
256
|
-
- Skip the persist follow-up entirely when the user left the override blank, or when the typed value matches the value already stored at the scope it would land in (avoid no-op confirmations).
|
|
257
|
-
|
|
258
|
-
For prompts whose target worker is NOT in the resolved workers list (after override), present a single confirmation line such as `gemini-model 생략 (workers에 gemini 없음)` so the user can see why the question was skipped.
|
|
259
|
-
|
|
260
|
-
## Step 6.5: Confirm selections before rendering
|
|
261
|
-
|
|
262
|
-
Before invoking `okstra render-bundle`, echo the resolved selections back to the user in a compact block so they can verify what will be passed. Show the **effective** values, not the raw input — i.e. when the user left a field blank, display `default` (and where known, the actual default such as `opus` / `sonnet`). Example for an `implementation` run:
|
|
263
|
-
|
|
264
|
-
```
|
|
265
|
-
선택 확인:
|
|
266
|
-
task-type : implementation
|
|
267
|
-
task-key : <group>/<id>
|
|
268
|
-
base-ref : main (resolved <short-sha>) ← worktree base, first phase only
|
|
269
|
-
executor : codex
|
|
270
|
-
workers : (프로필 기본 — executor + verifier 2 + report-writer)
|
|
271
|
-
lead-model : default (opus)
|
|
272
|
-
codex-model : gpt-5.5 ← executor model
|
|
273
|
-
claude-model : default (sonnet) ← verifier
|
|
274
|
-
gemini-model : default (auto) ← verifier
|
|
275
|
-
report-writer : default (opus)
|
|
276
|
-
directive : (none)
|
|
277
|
-
approved-plan : <abs path>
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
Then `AskUserQuestion`: `"이대로 진행할까요?"` with options `Proceed` / `Edit`. On `Edit`, return to the relevant Step 6 sub-prompt.
|
|
281
|
-
|
|
282
|
-
## Step 7: Call `okstra render-bundle`
|
|
283
|
-
|
|
284
|
-
This is the single command that materializes the entire task bundle. The
|
|
285
|
-
subcommand auto-supplies `--workspace-root` (from `okstra paths --field
|
|
286
|
-
workspace`) and forces `--render-only`, so the current claude session itself
|
|
287
|
-
takes over as lead — no new claude is spawned.
|
|
134
|
+
Output: `{ok: true, args: {"project-root": "...", "task-type": "...", ...}}`. Build the `okstra render-bundle` invocation from `args`, passing each key as `--<key>` and the value verbatim (including empty strings — they are intentional `use phase default` markers).
|
|
288
135
|
|
|
289
136
|
```bash
|
|
290
137
|
okstra render-bundle \
|
|
291
|
-
--project-root "<project-root>" \
|
|
292
|
-
--project-id
|
|
293
|
-
--task-group
|
|
294
|
-
--task-id
|
|
295
|
-
--task-type
|
|
296
|
-
--task-brief
|
|
297
|
-
--executor
|
|
298
|
-
--approved-plan "<approved-plan
|
|
299
|
-
--base-ref
|
|
300
|
-
--workers
|
|
301
|
-
--directive
|
|
302
|
-
--lead-model
|
|
303
|
-
--
|
|
304
|
-
--
|
|
305
|
-
--
|
|
306
|
-
--
|
|
138
|
+
--project-root "<args.project-root>" \
|
|
139
|
+
--project-id "<args.project-id>" \
|
|
140
|
+
--task-group "<args.task-group>" \
|
|
141
|
+
--task-id "<args.task-id>" \
|
|
142
|
+
--task-type "<args.task-type>" \
|
|
143
|
+
--task-brief "<args.task-brief>" \
|
|
144
|
+
--executor "<args.executor>" \
|
|
145
|
+
--approved-plan "<args.approved-plan>" \
|
|
146
|
+
--base-ref "<args.base-ref>" \
|
|
147
|
+
--workers "<args.workers>" \
|
|
148
|
+
--directive "<args.directive>" \
|
|
149
|
+
--lead-model "<args.lead-model>" \
|
|
150
|
+
--claude-model "<args.claude-model>" \
|
|
151
|
+
--codex-model "<args.codex-model>" \
|
|
152
|
+
--gemini-model "<args.gemini-model>" \
|
|
153
|
+
--report-writer-model "<args.report-writer-model>" \
|
|
154
|
+
--related-tasks "<args.related-tasks>" \
|
|
155
|
+
--clarification-response "<args.clarification-response>" \
|
|
156
|
+
--pr-template-path "<args.pr-template-path>"
|
|
307
157
|
```
|
|
308
158
|
|
|
309
|
-
Stdout prints `okstra task root:`, `okstra instruction-set:`, and the full
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
`claude-execution-prompt.md` path used by Step 8.
|
|
159
|
+
`render-bundle` auto-supplies `--workspace-root` and forces `--render-only`. Stdout prints `okstra task root:`, `okstra instruction-set:`, and the full rendered lead prompt. Parse the labelled lines for `TASK_ROOT` and `INSTRUCTION_SET_DIR`.
|
|
160
|
+
|
|
161
|
+
The python function underneath is mutex-protected (`~/.okstra/.locks/<task-key>.lock`), writes `run-context-*.json` + `run-inputs-*.json` + all manifests + discovery files, and registers the run in `~/.okstra/recent.jsonl` with status `prepared`.
|
|
313
162
|
|
|
314
|
-
|
|
315
|
-
writes `run-context-*.json` + `run-inputs-*.json` + all manifests + discovery
|
|
316
|
-
files, and registers the run in `~/.okstra/recent.jsonl` with status `prepared`.
|
|
163
|
+
You can delete `$STATE_FILE` after this point — its job is done.
|
|
317
164
|
|
|
318
|
-
## Step
|
|
165
|
+
## Step 6: Take over as Claude lead
|
|
319
166
|
|
|
320
167
|
Read these files (do not paraphrase) and enter `Claude lead` mode:
|
|
321
168
|
|
|
@@ -330,26 +177,43 @@ Then proceed through the phases exactly as the lead prompt directs (Phase 1 cont
|
|
|
330
177
|
Inform the user with one short line:
|
|
331
178
|
> Took over as Claude lead for `<taskKey>` (`<task-type>`). Run dir: `<RUN_DIR_RELATIVE_PATH>`. Beginning Phase 1 (context loading).
|
|
332
179
|
|
|
180
|
+
## Persisting the PR template scope (release-handoff)
|
|
181
|
+
|
|
182
|
+
When `wizard render-args` returns a non-empty `pr-template-path` AND the state has `pr_template_scope == "project"` or `"global"`, run the matching config command BEFORE `render-bundle`:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# project scope
|
|
186
|
+
okstra config set pr-template-path "<path>" --scope project
|
|
187
|
+
# global scope (must be absolute or ~/-prefixed)
|
|
188
|
+
okstra config set pr-template-path "<path>" --scope global
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The scope is exposed via `wizard render-args` only as the `pr-template-path` value (1-shot override); the persist hint lives in the wizard state. Read it with:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('pr_template_scope',''))" "$STATE_FILE"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
(or just inspect the JSON state file directly — it is a plain serialized `WizardState`).
|
|
198
|
+
|
|
333
199
|
## Concurrency
|
|
334
200
|
|
|
335
201
|
- `prepare_task_bundle` serializes per-task via `~/.okstra/.locks/<task-key>.lock`. Concurrent skill invocations on the same task wait; different tasks proceed in parallel.
|
|
336
|
-
-
|
|
337
|
-
-
|
|
202
|
+
- Each wizard run owns its own `$STATE_FILE`; two parallel skill invocations do not collide.
|
|
203
|
+
- The skill must NOT call `okstra.sh` or any other bash entrypoint that would re-implement the orchestration. The wizard + `render-bundle` is the single authority.
|
|
338
204
|
|
|
339
205
|
## Failure Modes
|
|
340
206
|
|
|
341
207
|
| Symptom | Cause | Fix |
|
|
342
208
|
|---|---|---|
|
|
343
209
|
| `okstra runtime missing: ...` | First run on this machine, or stale install | `npx okstra@latest install` once, retry. |
|
|
344
|
-
| `
|
|
345
|
-
| `
|
|
346
|
-
| `
|
|
347
|
-
| `approved plan has no
|
|
348
|
-
| `task brief not found` | brief-path doesn't resolve relative to cwd or project-root | Re-ask Step 5 |
|
|
349
|
-
| record_start failed | `~/.okstra` lock or disk issue | Non-fatal — bundle is valid; warn and continue |
|
|
210
|
+
| `No module named okstra_ctl.wizard` | Install predates wizard module | `npx okstra@latest install` to refresh. |
|
|
211
|
+
| `wizard step` returns `ok: false` repeatedly | User keeps giving invalid answers | Echo the error verbatim and re-prompt the same step — do not advance. |
|
|
212
|
+
| `task root not found for <key>` | catalog entry stale or task-key typo | Restart the wizard (`okstra wizard init`) to refresh the pick list. |
|
|
213
|
+
| `approved plan has no APPROVED marker` | `implementation` without proper approval | Ask the user to add `APPROVED` to the plan, or pick a different task-type. |
|
|
350
214
|
|
|
351
215
|
## Output Rules
|
|
352
216
|
|
|
353
|
-
- Echo each `
|
|
354
|
-
- Never invent identity;
|
|
355
|
-
- After Step
|
|
217
|
+
- Echo each captured answer (`result.echo`) on one short line so the user sees what was registered.
|
|
218
|
+
- Never invent identity; if a `text` prompt returns an empty answer where the wizard rejects it, the user must retry.
|
|
219
|
+
- After Step 6, begin the lead workflow without re-summarizing the skill itself.
|
|
@@ -486,6 +486,7 @@ The script reads:
|
|
|
486
486
|
## Team State Persistence
|
|
487
487
|
|
|
488
488
|
Information to be recorded in the team-state JSON file:
|
|
489
|
+
- `teamName` — record the string that was passed to `TeamCreate(team_name: ...)`. Either `state.teamName` (root) or `state.team.teamName` (nested) is accepted by `scripts/okstra_token_usage/collect.py`. Be consistent within a single run. Without this value the Phase 7 collector falls back to `okstra-<task-id>` (short form), which does NOT match worker jsonls whose team needle carries the full multi-segment task key — every worker will then be recorded as `source: "unavailable"`.
|
|
489
490
|
- Current status of each worker role
|
|
490
491
|
- Start/end times for each worker
|
|
491
492
|
- Prompt history path for each worker
|
package/src/wizard.mjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { runPythonModule } from "./_python-helper.mjs";
|
|
2
|
+
import { resolvePaths } from "./paths.mjs";
|
|
3
|
+
|
|
4
|
+
const USAGE = `okstra wizard — interactive okstra-run input collector
|
|
5
|
+
|
|
6
|
+
Used by the okstra-run skill to drive the per-step prompt loop. Each
|
|
7
|
+
subcommand round-trips a JSON state file held by the skill.
|
|
8
|
+
|
|
9
|
+
Subcommands:
|
|
10
|
+
init seed a fresh wizard state and emit the first prompt
|
|
11
|
+
step submit an answer (or fetch the current prompt) and emit next
|
|
12
|
+
render-args emit the final --flag/value map for 'okstra render-bundle'
|
|
13
|
+
confirmation emit the multi-line confirmation echo block
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
okstra wizard init --state-file <path> --project-root <p> --project-id <id>
|
|
17
|
+
okstra wizard step --state-file <path> [--answer <value>]
|
|
18
|
+
okstra wizard render-args --state-file <path>
|
|
19
|
+
okstra wizard confirmation --state-file <path>
|
|
20
|
+
|
|
21
|
+
'init' auto-fills --workspace-root from 'okstra paths --field workspace',
|
|
22
|
+
so callers do not pass it.
|
|
23
|
+
|
|
24
|
+
All subcommands emit a single JSON object on stdout. On validation failure
|
|
25
|
+
'step' returns {ok:false, error, current} so the skill can re-prompt.
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
function parseFlags(args) {
|
|
29
|
+
const out = {};
|
|
30
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
31
|
+
const a = args[i];
|
|
32
|
+
if (!a.startsWith("--")) {
|
|
33
|
+
throw new Error(`unexpected positional argument: ${a}`);
|
|
34
|
+
}
|
|
35
|
+
const key = a.slice(2);
|
|
36
|
+
const next = args[i + 1];
|
|
37
|
+
if (next === undefined || next.startsWith("--")) {
|
|
38
|
+
throw new Error(`flag --${key} requires a value`);
|
|
39
|
+
}
|
|
40
|
+
out[key] = next;
|
|
41
|
+
i += 1;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function run(args) {
|
|
47
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
48
|
+
process.stdout.write(USAGE);
|
|
49
|
+
return args.length === 0 ? 2 : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [sub, ...rest] = args;
|
|
53
|
+
if (!["init", "step", "render-args", "confirmation"].includes(sub)) {
|
|
54
|
+
process.stderr.write(`error: unknown wizard subcommand '${sub}'\n\n${USAGE}`);
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let flags;
|
|
59
|
+
try {
|
|
60
|
+
flags = parseFlags(rest);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!flags["state-file"]) {
|
|
67
|
+
process.stderr.write("error: --state-file is required\n");
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// build python argv
|
|
72
|
+
const pyArgs = [sub, "--state-file", flags["state-file"]];
|
|
73
|
+
if (sub === "init") {
|
|
74
|
+
if (!flags["project-root"] || !flags["project-id"]) {
|
|
75
|
+
process.stderr.write("error: init requires --project-root and --project-id\n");
|
|
76
|
+
return 2;
|
|
77
|
+
}
|
|
78
|
+
const paths = await resolvePaths();
|
|
79
|
+
pyArgs.push("--workspace-root", paths.workspace);
|
|
80
|
+
pyArgs.push("--project-root", flags["project-root"]);
|
|
81
|
+
pyArgs.push("--project-id", flags["project-id"]);
|
|
82
|
+
} else if (sub === "step" && flags.answer !== undefined) {
|
|
83
|
+
pyArgs.push("--answer", flags.answer);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = await runPythonModule({
|
|
87
|
+
module: "okstra_ctl.wizard",
|
|
88
|
+
args: pyArgs,
|
|
89
|
+
stdio: "capture",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (result.code !== 0 && !result.stdout.trim()) {
|
|
93
|
+
process.stdout.write(
|
|
94
|
+
JSON.stringify(
|
|
95
|
+
{ ok: false, stage: "python", reason: result.stderr.trim() || "no output" },
|
|
96
|
+
null,
|
|
97
|
+
2,
|
|
98
|
+
) + "\n",
|
|
99
|
+
);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
process.stdout.write(result.stdout);
|
|
104
|
+
return result.code === 0 ? 0 : result.code;
|
|
105
|
+
}
|