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.
@@ -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, render the full task bundle through the single python entrypoint, then take over as `Claude lead` in the current session.
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 and `okstra.sh` both call the exact same python function `okstra_ctl.run.prepare_task_bundle()`. The skill does NOT shell out to `okstra.sh`that would create a second orchestration path and reintroduce env-var leakage between the parent claude session and child bash.
10
+ **Single authority**: this skill drives `okstra wizard`, which owns every step (ordering, branching, validation). The skill is just a thin prompt-relay loopit 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 already 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").
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
- ## Prompt convention (use the right tool for the right input shape)
23
+ ## How the wizard talks to you
24
24
 
25
- `AskUserQuestion` always renders a picker UI with a forced auto-attached `Other` option. While the user types into `Other`, the picker re-renders and the experience feels out of sync. So:
25
+ Every wizard call returns JSON. The two shapes you'll see:
26
26
 
27
- - **Use `AskUserQuestion` ONLY when the answer is a fixed pick from a short option set** (2–4 distinct, mutually exclusive choices). Examples in this skill: task-type choice (Step 4), executor provider (claude/codex/gemini), model picker (default/opus/sonnet/haiku per provider), Use defaults vs Customize, Proceed vs Edit confirmation.
28
- - **For pure free-text inputs** (file paths, task identifiers, CSV strings, free directives, branch names typed by the user) **do NOT use `AskUserQuestion`**. Instead, write a plain text message (e.g. `"Task group (예: backend-api, INV-1234)을 알려주세요. 빈 줄이면 취소."`) and consume the user's NEXT message as the answer. Then validate and re-prompt with another plain text message on failure.
29
- - **For "menu + free-text" places** (base-ref pickers, PR base branch) — show the menu with `AskUserQuestion` listing only the canonical options + a literal option labeled `직접 입력`. When the user picks `직접 입력`, follow up with a **separate** plain text prompt and consume the next user message. Do NOT rely on `Other` auto-text inside the picker — its re-render is the root cause of the lag.
27
+ ```json
28
+ { "ok": true, "echo": "task-group: backend-api",
29
+ "next": { "step": "task_id", "kind": "text", "label": "...", "options": [], "echoTemplate": "..." } }
30
+ ```
30
31
 
31
- Echo each captured answer on one short line (e.g. `task-group: backend-api`) so the user sees what was registered, regardless of which prompt shape was used.
32
+ ```json
33
+ { "ok": false, "error": "approved plan has no APPROVED marker: ...",
34
+ "current": { "step": "approved_plan", "kind": "text", "label": "..." } }
35
+ ```
32
36
 
33
- ## Authority Files (disk-only no env var caching for per-run identity)
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
- Every step reads disk afresh. The `OKSTRA_*` env vars below identify the
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
- - `~/.okstra/version` okstra runtime version stamp
39
- - `<PROJECT_ROOT>/.project-docs/okstra/project.json`
40
- - `<PROJECT_ROOT>/.project-docs/okstra/discovery/{task-catalog,latest-task}.json`
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
- ## Step 0: Verify okstra runtime + project setup
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
- Do NOT hard-code or guess any okstra path. Every run loads them fresh from
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
- After Step 0 the following are guaranteed:
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
- | Variable | Meaning |
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 1: Resolve PROJECT_ROOT and projectId
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
- okstra check-project --cwd "$(pwd)"
92
- ```
78
+ STATE_FILE="$(mktemp -t okstra-wizard.XXXX.json)"
93
79
 
94
- - If `ok: true`: read `projectRoot` and `projectId` from the JSON.
95
- - If `ok: false`: ask the user with a **plain text prompt** (not `AskUserQuestion` — this is pure free text per the convention above) for an absolute project-root path; rerun with `okstra check-project --cwd <their input>`. Re-prompt with another plain text message on failure.
96
-
97
- ## Step 2: Choose task — existing vs new
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 is JSON `{ok, projectRoot, tasks: [...], latest: {...}|null}`.
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 4: Choose task-type
88
+ ## Step 3: Run the prompt loop
124
89
 
125
- `AskUserQuestion` with six fixed options:
90
+ Repeat until `next.kind == "done"`:
126
91
 
127
- | Option | Description |
128
- |---|---|
129
- | `requirements-discovery` | Classify request and route to next safe phase |
130
- | `error-analysis` | Evidence-based root-cause analysis (no code changes) |
131
- | `implementation-planning` | Plan options + request user approval |
132
- | `implementation` | Execute approved plan (requires `--approved-plan`) |
133
- | `final-verification` | Acceptance + residual-risk review |
134
- | `release-handoff` | Drive commit / push / PR with user-selected actions after `accepted` final-verification |
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
- For existing tasks, present `nextRecommendedPhase` as the first option (recommended default).
103
+ That is the entire interactive flow. The wizard handles:
137
104
 
138
- If `implementation` chosen, ask two more questions in order:
139
- - **Plain text prompt** (file path is pure free text): `"approved final-report.md 경로를 알려주세요 (APPROVED 마커가 있어야 합니다)"`. The underlying python `prepare_task_bundle` re-validates the marker, but you can pre-check with `grep`. Re-prompt with plain text on failure.
140
- - **`AskUserQuestion`** with three options (`claude` / `codex` / `gemini`) only this provider mutates project files; the other two run as read-only verifiers. Default `claude` (or `OKSTRA_DEFAULT_EXECUTOR` if set). Pass the answer through `PrepareInputs.executor`.
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
- ## Step 4.6: Base ref for the task worktree (first phase only)
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
- `okstra prepare` provisions a per-task git worktree on first phase of a task-key
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
- First, decide whether to ask:
118
+ When `next.step == "confirm"`, before relaying the picker, fetch the human-readable selection summary:
150
119
 
151
120
  ```bash
152
- okstra worktree-lookup "<project-id>" "<task-group>" "<task-id>"
121
+ okstra wizard confirmation --state-file "$STATE_FILE"
153
122
  ```
154
123
 
155
- Output JSON: `{ok: true, entry: null}` means no active worktree **ASK**. A
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
- - `REUSE` the registered worktree is reused; set `base_ref=""` and skip the
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
- Use the **menu + free-text two-step pattern** (per the convention above):
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
- git -C "$(git -C "$PROJECT_ROOT" rev-parse --path-format=absolute --git-common-dir | xargs dirname)" \
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
- This is the **only** set of worker IDs you may show or ask about. Never offer
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 "<project-id>" \
293
- --task-group "<task-group>" \
294
- --task-id "<task-id>" \
295
- --task-type "<task-type>" \
296
- --task-brief "<brief-path-from-user>" \
297
- --executor "<claude|codex|gemini or empty for default>" \
298
- --approved-plan "<approved-plan-or-empty>" \
299
- --base-ref "<chosen-ref-from-step-4.6 or empty when reusing existing worktree>" \
300
- --workers "<comma-separated worker list, or empty for profile default; MUST be empty for implementation>" \
301
- --directive "<directive or empty>" \
302
- --lead-model "..." --claude-model "..." --codex-model "..." \
303
- --gemini-model "..." --report-writer-model "..." \
304
- --related-tasks "..." \
305
- --clarification-response "<clarification-or-empty>" \
306
- --pr-template-path "<pr-template-override-or-empty; release-handoff only>"
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
- rendered lead-prompt text (because `--render-only` is on). Parse the labelled
311
- lines to get `TASK_ROOT`, `INSTRUCTION_SET_DIR`, and from there the
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
- The python function underneath is mutex-protected (`~/.okstra/.locks/<task-key>.lock`),
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 8: Take over as Claude lead
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
- - The skill must NOT call `okstra.sh` or any other bash entrypoint that would re-implement the orchestration. The python function is the single authority.
337
- - No env var carries identity across steps every step re-reads disk authority.
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
- | `OKSTRA_PYTHONPATH unbound` / `ModuleNotFoundError: okstra_project` | Step 0 was skipped or env vars dropped | Re-run Step 0; never invoke python without exporting `PYTHONPATH=$OKSTRA_PYTHONPATH`. |
345
- | `task root not found for <key>` | catalog entry stale or task-key typo | Re-run Step 2 (`okstra task-list`) and show available keys |
346
- | `PROJECT_ROOT 해석할 없습니다` | cwd outside okstra project, no git toplevel | Ask user for absolute path |
347
- | `approved plan has no recognised user-approval marker` | `implementation` without proper approval | Ask user to add `APPROVED` to the plan, or pick a different task-type |
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 `AskUserQuestion` outcome on one short line so user sees what was captured.
354
- - Never invent identity; re-ask if blank.
355
- - After Step 8, begin the lead workflow without re-summarizing the skill itself.
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
+ }