okstra 0.49.0 → 0.51.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.
Files changed (86) hide show
  1. package/README.kr.md +8 -7
  2. package/README.md +8 -7
  3. package/bin/okstra +2 -0
  4. package/docs/kr/architecture.md +23 -24
  5. package/docs/kr/cli.md +6 -6
  6. package/docs/project-structure-overview.md +13 -9
  7. package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +559 -0
  8. package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +121 -0
  9. package/docs/task-process/error-analysis.md +1 -1
  10. package/docs/task-process/final-verification.md +1 -1
  11. package/docs/task-process/release-handoff.md +1 -1
  12. package/docs/task-process/requirements-discovery.md +1 -1
  13. package/package.json +1 -1
  14. package/runtime/BUILD.json +2 -2
  15. package/runtime/agents/SKILL.md +18 -14
  16. package/runtime/agents/workers/claude-worker.md +4 -4
  17. package/runtime/agents/workers/codex-worker.md +3 -3
  18. package/runtime/agents/workers/gemini-worker.md +3 -3
  19. package/runtime/agents/workers/report-writer-worker.md +3 -3
  20. package/runtime/bin/lib/okstra/cli.sh +8 -1
  21. package/runtime/bin/lib/okstra/globals.sh +3 -0
  22. package/runtime/bin/lib/okstra/interactive.sh +14 -12
  23. package/runtime/bin/lib/okstra/usage.sh +6 -0
  24. package/runtime/bin/okstra-render-report-views.py +1 -1
  25. package/runtime/bin/okstra-team-reconcile.sh +28 -0
  26. package/runtime/bin/okstra.sh +2 -0
  27. package/runtime/prompts/launch.template.md +4 -2
  28. package/runtime/prompts/profiles/_common-contract.md +15 -15
  29. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  30. package/runtime/prompts/profiles/_implementation-executor.md +3 -3
  31. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  32. package/runtime/prompts/profiles/error-analysis.md +1 -1
  33. package/runtime/prompts/profiles/final-verification.md +2 -2
  34. package/runtime/prompts/profiles/implementation-planning.md +10 -9
  35. package/runtime/prompts/profiles/implementation.md +1 -1
  36. package/runtime/prompts/profiles/improvement-discovery.md +5 -5
  37. package/runtime/prompts/profiles/release-handoff.md +2 -2
  38. package/runtime/prompts/profiles/requirements-discovery.md +2 -2
  39. package/runtime/python/okstra_ctl/analysis_packet.py +259 -0
  40. package/runtime/python/okstra_ctl/clarification_items.py +11 -11
  41. package/runtime/python/okstra_ctl/context_cost.py +308 -0
  42. package/runtime/python/okstra_ctl/migrate.py +2 -12
  43. package/runtime/python/okstra_ctl/paths.py +22 -0
  44. package/runtime/python/okstra_ctl/render.py +285 -126
  45. package/runtime/python/okstra_ctl/render_final_report.py +32 -1
  46. package/runtime/python/okstra_ctl/report_views.py +12 -12
  47. package/runtime/python/okstra_ctl/run.py +510 -248
  48. package/runtime/python/okstra_ctl/sequence.py +2 -5
  49. package/runtime/python/okstra_ctl/team_reconcile.py +131 -0
  50. package/runtime/python/okstra_ctl/wizard.py +219 -136
  51. package/runtime/python/okstra_ctl/workflow.py +1 -1
  52. package/runtime/python/okstra_ctl/worktree.py +13 -5
  53. package/runtime/schemas/final-report-v1.0.schema.json +4 -0
  54. package/runtime/skills/okstra-brief/SKILL.md +1 -1
  55. package/runtime/skills/okstra-coding-preflight/SKILL.md +69 -0
  56. package/runtime/skills/okstra-coding-preflight/architecture/hexagonal.md +116 -0
  57. package/runtime/skills/okstra-coding-preflight/clean-code.md +254 -0
  58. package/runtime/skills/okstra-coding-preflight/languages/java.md +64 -0
  59. package/runtime/skills/okstra-coding-preflight/languages/javascript-typescript.md +87 -0
  60. package/runtime/skills/okstra-coding-preflight/languages/kotlin.md +69 -0
  61. package/runtime/skills/okstra-coding-preflight/languages/nodejs.md +66 -0
  62. package/runtime/skills/okstra-coding-preflight/languages/python.md +179 -0
  63. package/runtime/skills/okstra-coding-preflight/languages/rust.md +105 -0
  64. package/runtime/skills/okstra-coding-preflight/languages/sql.md +68 -0
  65. package/runtime/skills/okstra-context-loader/SKILL.md +12 -6
  66. package/runtime/skills/okstra-convergence/SKILL.md +8 -8
  67. package/runtime/skills/okstra-inspect/SKILL.md +100 -1
  68. package/runtime/skills/okstra-report-writer/SKILL.md +27 -23
  69. package/runtime/skills/okstra-run/SKILL.md +3 -1
  70. package/runtime/skills/okstra-team-contract/SKILL.md +8 -5
  71. package/runtime/templates/reports/final-report.template.md +188 -187
  72. package/runtime/templates/reports/i18n/en.json +4 -4
  73. package/runtime/templates/reports/i18n/ko.json +4 -4
  74. package/runtime/templates/reports/implementation-planning-input.template.md +1 -1
  75. package/runtime/templates/reports/release-handoff-input.template.md +1 -1
  76. package/runtime/templates/reports/user-response.template.md +1 -1
  77. package/runtime/templates/worker-prompt-preamble.md +4 -4
  78. package/runtime/validators/lib/fixtures.sh +2 -2
  79. package/runtime/validators/validate-implementation-plan-stages.py +9 -9
  80. package/runtime/validators/validate-report-views.py +10 -10
  81. package/runtime/validators/validate-run.py +36 -36
  82. package/runtime/validators/validate_improvement_report.py +8 -8
  83. package/src/_python-helper.mjs +3 -3
  84. package/src/context-cost.mjs +27 -0
  85. package/src/install.mjs +1 -0
  86. package/src/uninstall.mjs +1 -0
@@ -0,0 +1,559 @@
1
+ # okstra wizard 멀티탭 배치 프롬프트 구현 계획
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** customize 분기에서 서로 의존 없는 픽 step들을 멀티탭 `AskUserQuestion` 한 번으로 묶어 wizard 입력 왕복 수를 줄인다.
6
+
7
+ **Architecture:** 기존 `Step` 레지스트리는 그대로 두고 "방출 계층"에만 그룹 개념을 추가한다. `next_prompt`가 첫 적용 가능한 미답변 step이 그룹 멤버이면 같은 그룹의 적용가능·미답변 픽 멤버를 최대 4개까지 모아 새 `kind="pick_group"` 프롬프트로 내보내고, `submit`은 JSON `--answer`를 각 멤버 `submit()`으로 라우팅한다. `answered`/`owns`/edit-rewind/`_ready_for_confirm`은 전부 개별 step id 단위로 유지된다.
8
+
9
+ **Tech Stack:** Python 3 (`scripts/okstra_ctl/wizard.py`), pytest (`tests/`), okstra-run 스킬 마크다운.
10
+
11
+ 설계 문서: [docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md](../specs/2026-06-05-wizard-batch-prompts-design.md)
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ - Modify: `scripts/okstra_ctl/wizard.py`
18
+ - `Prompt` 데이터클래스 — `questions` 필드 + `to_json` 확장 ([wizard.py:299](../../../scripts/okstra_ctl/wizard.py))
19
+ - 그룹 상수/정의 — S_* 상수 블록 직후 ([wizard.py:206](../../../scripts/okstra_ctl/wizard.py))
20
+ - `_build_group_prompt` 신규 + `next_prompt` 수정 ([wizard.py:2221](../../../scripts/okstra_ctl/wizard.py))
21
+ - `_submit_group` 신규 + `submit` 수정 ([wizard.py:2232](../../../scripts/okstra_ctl/wizard.py))
22
+ - Modify: `skills/okstra-run/SKILL.md` — `pick_group` 렌더 규칙 ([SKILL.md:41](../../../skills/okstra-run/SKILL.md))
23
+ - Modify: `tests/test_okstra_ctl_wizard.py` — 그룹 흐름 반영 (`test_error_analysis_full_customize` 등)
24
+ - Create: `tests/test_wizard_pick_group.py` — 그룹 방출/제출 단위 테스트
25
+
26
+ ---
27
+
28
+ ## Task 1: `pick_group` 데이터 모델
29
+
30
+ **Files:**
31
+ - Modify: `scripts/okstra_ctl/wizard.py:299-318` (`Prompt`)
32
+ - Modify: `scripts/okstra_ctl/wizard.py:206` (그룹 상수)
33
+ - Test: `tests/test_wizard_pick_group.py`
34
+
35
+ - [ ] **Step 1: 실패 테스트 작성**
36
+
37
+ `tests/test_wizard_pick_group.py` 생성:
38
+
39
+ ```python
40
+ """pick_group 방출/제출 단위 테스트."""
41
+ from __future__ import annotations
42
+
43
+ import json
44
+ import sys
45
+ from pathlib import Path
46
+
47
+ LIB_DIR = Path(__file__).resolve().parent.parent / "scripts"
48
+ sys.path.insert(0, str(LIB_DIR))
49
+
50
+ from okstra_ctl.wizard import ( # noqa: E402
51
+ GROUP_MAX_TABS,
52
+ GROUP_MODELS,
53
+ PROMPT_GROUPS,
54
+ Option,
55
+ Prompt,
56
+ )
57
+
58
+
59
+ def test_pick_group_to_json_carries_questions():
60
+ members = [
61
+ Prompt(step="lead_model", kind="pick", label="Lead",
62
+ options=[Option("default", "default")]),
63
+ Prompt(step="claude_model", kind="pick", label="Claude",
64
+ options=[Option("sonnet", "sonnet")]),
65
+ ]
66
+ p = Prompt(step=GROUP_MODELS, kind="pick_group",
67
+ label="모델", questions=members)
68
+ out = p.to_json()
69
+ assert out["kind"] == "pick_group"
70
+ assert [q["step"] for q in out["questions"]] == ["lead_model", "claude_model"]
71
+ assert out["questions"][0]["options"][0]["value"] == "default"
72
+ assert out["questions"][1]["multi"] is False
73
+
74
+
75
+ def test_group_definitions_cover_model_and_option_picks():
76
+ assert GROUP_MAX_TABS == 4
77
+ assert "lead_model" in PROMPT_GROUPS[GROUP_MODELS]
78
+ assert "report_writer_model" in PROMPT_GROUPS[GROUP_MODELS]
79
+ ```
80
+
81
+ - [ ] **Step 2: 실패 확인**
82
+
83
+ Run: `python3 -m pytest tests/test_wizard_pick_group.py -v`
84
+ Expected: FAIL — `ImportError: cannot import name 'GROUP_MODELS'` / `Prompt` has no `questions`.
85
+
86
+ - [ ] **Step 3: 구현 — `Prompt.questions` + 그룹 상수**
87
+
88
+ `scripts/okstra_ctl/wizard.py` `Prompt` 데이터클래스 ([:299](../../../scripts/okstra_ctl/wizard.py)) 에 필드 추가 (`multi` 줄 바로 아래):
89
+
90
+ ```python
91
+ multi: bool = False # only meaningful when kind == "pick"
92
+ # only meaningful when kind == "pick_group": one entry per AskUserQuestion tab
93
+ questions: list["Prompt"] = field(default_factory=list)
94
+ ```
95
+
96
+ `to_json` ([:309](../../../scripts/okstra_ctl/wizard.py)) 를 교체:
97
+
98
+ ```python
99
+ def to_json(self) -> dict[str, Any]:
100
+ out = {
101
+ "step": self.step,
102
+ "kind": self.kind,
103
+ "label": self.label,
104
+ "options": [asdict(o) for o in self.options],
105
+ "help": self.help,
106
+ "echoTemplate": self.echo_template,
107
+ "multi": self.multi,
108
+ }
109
+ if self.kind == "pick_group":
110
+ out["questions"] = [
111
+ {"step": q.step, "label": q.label,
112
+ "options": [asdict(o) for o in q.options],
113
+ "multi": q.multi}
114
+ for q in self.questions
115
+ ]
116
+ return out
117
+ ```
118
+
119
+ S_* 상수 블록 직후 (`S_DONE = "done"` 다음, [:206](../../../scripts/okstra_ctl/wizard.py)) 에 그룹 정의 추가:
120
+
121
+ ```python
122
+ # ---- 멀티탭 배치 프롬프트 그룹 (방출 계층 전용) ----
123
+ # 그룹 id 는 S_* 가 아니므로 prompts JSON SOT / step-id 동기화 검사 대상이 아니다.
124
+ GROUP_MODELS = "models"
125
+ GROUP_OPTIONS = "options"
126
+ GROUP_MAX_TABS = 4 # AskUserQuestion 의 질문(탭) 수 한도
127
+
128
+ # 멤버는 모두 서로 의존이 없는 단일선택 픽 step 이어야 한다.
129
+ # *_TEXT 후속 / workers_override / pr_template_scope 는 의존성 때문에 개별 유지.
130
+ PROMPT_GROUPS: dict[str, tuple[str, ...]] = {
131
+ GROUP_MODELS: (S_LEAD_MODEL, S_EXECUTOR_MODEL, S_CLAUDE_MODEL,
132
+ S_CODEX_MODEL, S_GEMINI_MODEL, S_REPORT_WRITER_MODEL),
133
+ GROUP_OPTIONS: (S_DIRECTIVE_PICK, S_RELATED_TASKS_PICK,
134
+ S_CLARIFICATION_PICK, S_PR_TEMPLATE_PICK),
135
+ }
136
+ GROUP_LABELS: dict[str, str] = {
137
+ GROUP_MODELS: "모델 선택 (탭별로 선택)",
138
+ GROUP_OPTIONS: "추가 옵션 (탭별로 선택)",
139
+ }
140
+ _STEP_TO_GROUP: dict[str, str] = {
141
+ sid: gid for gid, ids in PROMPT_GROUPS.items() for sid in ids
142
+ }
143
+ ```
144
+
145
+ - [ ] **Step 4: 통과 확인**
146
+
147
+ Run: `python3 -m pytest tests/test_wizard_pick_group.py -v`
148
+ Expected: PASS (2 passed)
149
+
150
+ - [ ] **Step 5: 커밋**
151
+
152
+ ```bash
153
+ git add scripts/okstra_ctl/wizard.py tests/test_wizard_pick_group.py
154
+ git commit -m "feat(wizard): add pick_group prompt model and group definitions"
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Task 2: `next_prompt` 그룹 방출
160
+
161
+ **Files:**
162
+ - Modify: `scripts/okstra_ctl/wizard.py:2221-2229` (`next_prompt`)
163
+ - Test: `tests/test_wizard_pick_group.py`
164
+
165
+ - [ ] **Step 1: 실패 테스트 작성**
166
+
167
+ `tests/test_wizard_pick_group.py` 에 추가:
168
+
169
+ ```python
170
+ from okstra_ctl.wizard import next_prompt # noqa: E402
171
+
172
+
173
+ def _models_state():
174
+ """모델 그룹 직전까지 진행된 상태를 직접 구성한다 (비-implementation)."""
175
+ from okstra_ctl.wizard import WizardState
176
+ s = WizardState(workspace_root=".", project_root=".", project_id="p")
177
+ s.task_type = "error-analysis"
178
+ s.brief_path = "/tmp/brief.md"
179
+ s.base_ref = "main"
180
+ s.profile_workers = ["claude", "codex", "report-writer"]
181
+ s.profile_optional_workers = ["gemini"]
182
+ s.use_defaults = False
183
+ s.workers_override = "claude,codex,report-writer"
184
+ s.critic = "off"
185
+ # identity/critic/workers 단계를 answered 로 마킹해 모델 그룹이 첫 미답변이 되게 한다.
186
+ s.answered = [
187
+ "task_pick", "brief_path", "task_group", "task_id", "task_type",
188
+ "base_ref_pick", "critic_pick", "defaults_or_custom",
189
+ "workers_override",
190
+ ]
191
+ return s
192
+
193
+
194
+ def test_next_prompt_emits_models_group():
195
+ p = next_prompt(_models_state())
196
+ assert p.kind == "pick_group"
197
+ assert p.step == GROUP_MODELS
198
+ steps = [q.step for q in p.questions]
199
+ # 로스터에 gemini 없음 → gemini_model 제외, executor 는 비-impl 이라 제외
200
+ assert steps == ["lead_model", "claude_model", "codex_model",
201
+ "report_writer_model"]
202
+ assert len(steps) <= GROUP_MAX_TABS
203
+
204
+
205
+ def test_models_group_caps_at_four_then_emits_remainder():
206
+ s = _models_state()
207
+ s.profile_workers = ["claude", "codex", "gemini", "report-writer"]
208
+ s.profile_optional_workers = []
209
+ s.workers_override = "claude,codex,gemini,report-writer"
210
+ p = next_prompt(s)
211
+ assert p.kind == "pick_group"
212
+ assert [q.step for q in p.questions] == [
213
+ "lead_model", "claude_model", "codex_model", "gemini_model"]
214
+ # 첫 4개를 answered 처리하면 5번째(report_writer)는 단일 픽으로 방출
215
+ s.answered += ["lead_model", "claude_model", "codex_model", "gemini_model"]
216
+ p2 = next_prompt(s)
217
+ assert p2.kind == "pick"
218
+ assert p2.step == "report_writer_model"
219
+ ```
220
+
221
+ - [ ] **Step 2: 실패 확인**
222
+
223
+ Run: `python3 -m pytest tests/test_wizard_pick_group.py -k next_prompt -v`
224
+ Expected: FAIL — `next_prompt` 가 `kind == "pick"` (S_LEAD_MODEL) 를 반환.
225
+
226
+ - [ ] **Step 3: 구현**
227
+
228
+ `scripts/okstra_ctl/wizard.py` `next_prompt` ([:2221](../../../scripts/okstra_ctl/wizard.py)) 를 교체하고 헬퍼를 그 위에 추가:
229
+
230
+ ```python
231
+ def _build_group_prompt(state: WizardState, group_id: str) -> Prompt:
232
+ """그룹의 적용가능·미답변 픽 멤버를 최대 GROUP_MAX_TABS 개 모은다.
233
+
234
+ 멤버가 1개뿐이면 멀티탭 UI가 불필요하므로 그 멤버의 평범한 픽을 반환한다.
235
+ """
236
+ members: list[Prompt] = []
237
+ for sid in PROMPT_GROUPS[group_id]:
238
+ if sid in state.answered:
239
+ continue
240
+ step = STEP_BY_ID[sid]
241
+ if not step.applies(state):
242
+ continue
243
+ members.append(step.build(state))
244
+ if len(members) >= GROUP_MAX_TABS:
245
+ break
246
+ if len(members) == 1:
247
+ return members[0]
248
+ return Prompt(step=group_id, kind="pick_group",
249
+ label=GROUP_LABELS[group_id], questions=members)
250
+
251
+
252
+ def next_prompt(state: WizardState) -> Prompt:
253
+ if state.confirmed:
254
+ return Prompt(step=S_DONE, kind="done")
255
+ for step in STEPS:
256
+ if step.id in state.answered:
257
+ continue
258
+ if step.applies(state):
259
+ group_id = _STEP_TO_GROUP.get(step.id)
260
+ if group_id is not None:
261
+ return _build_group_prompt(state, group_id)
262
+ return step.build(state)
263
+ return Prompt(step=S_DONE, kind="done")
264
+ ```
265
+
266
+ - [ ] **Step 4: 통과 확인**
267
+
268
+ Run: `python3 -m pytest tests/test_wizard_pick_group.py -k next_prompt -v`
269
+ Expected: PASS (2 passed)
270
+
271
+ - [ ] **Step 5: 커밋**
272
+
273
+ ```bash
274
+ git add scripts/okstra_ctl/wizard.py tests/test_wizard_pick_group.py
275
+ git commit -m "feat(wizard): emit pick_group in next_prompt with 4-tab cap"
276
+ ```
277
+
278
+ ---
279
+
280
+ ## Task 3: `submit` 그룹 라우팅
281
+
282
+ **Files:**
283
+ - Modify: `scripts/okstra_ctl/wizard.py:2232-2246` (`submit`)
284
+ - Test: `tests/test_wizard_pick_group.py`
285
+
286
+ - [ ] **Step 1: 실패 테스트 작성**
287
+
288
+ `tests/test_wizard_pick_group.py` 에 추가:
289
+
290
+ ```python
291
+ import pytest # noqa: E402
292
+
293
+ from okstra_ctl.wizard import WizardError, submit # noqa: E402
294
+
295
+
296
+ def test_submit_group_routes_json_to_members():
297
+ s = _models_state()
298
+ answer = json.dumps({
299
+ "lead_model": "default",
300
+ "claude_model": "sonnet",
301
+ "codex_model": "gpt-5.5",
302
+ "report_writer_model": "default",
303
+ })
304
+ result = submit(s, answer)
305
+ assert s.claude_model == "sonnet"
306
+ assert s.codex_model == "gpt-5.5"
307
+ assert s.lead_model == "" # default → 빈 문자열
308
+ assert s.report_writer_model == ""
309
+ # 모든 멤버가 개별적으로 answered 처리됨
310
+ for sid in ("lead_model", "claude_model", "codex_model",
311
+ "report_writer_model"):
312
+ assert sid in s.answered
313
+ assert result["next"]["step"] != GROUP_MODELS
314
+
315
+
316
+ def test_submit_group_missing_key_defaults():
317
+ s = _models_state()
318
+ result = submit(s, json.dumps({"claude_model": "sonnet"}))
319
+ assert s.claude_model == "sonnet"
320
+ assert s.lead_model == ""
321
+ assert "lead_model" in s.answered
322
+
323
+
324
+ def test_submit_group_invalid_json_raises():
325
+ s = _models_state()
326
+ with pytest.raises(WizardError):
327
+ submit(s, "not-json")
328
+
329
+
330
+ def test_submit_group_invalid_model_raises_and_no_marking():
331
+ s = _models_state()
332
+ with pytest.raises(WizardError):
333
+ submit(s, json.dumps({"claude_model": "totally-unknown-model"}))
334
+ # 검증 실패 → 어떤 멤버도 answered 로 마킹되지 않음(전부 재-프롬프트)
335
+ assert "claude_model" not in s.answered
336
+ assert "lead_model" not in s.answered
337
+ ```
338
+
339
+ - [ ] **Step 2: 실패 확인**
340
+
341
+ Run: `python3 -m pytest tests/test_wizard_pick_group.py -k submit -v`
342
+ Expected: FAIL — `submit` 가 group step을 `STEP_BY_ID[prompt.step]` 에서 찾다 `KeyError`.
343
+
344
+ - [ ] **Step 3: 구현**
345
+
346
+ `scripts/okstra_ctl/wizard.py` `submit` ([:2232](../../../scripts/okstra_ctl/wizard.py)) 를 교체하고 헬퍼를 그 위에 추가:
347
+
348
+ ```python
349
+ def _submit_group(state: WizardState, prompt: Prompt, value: str) -> dict[str, Any]:
350
+ """pick_group 답(JSON 객체)을 각 멤버 submit() 으로 라우팅한다.
351
+
352
+ 전부 검증 통과한 뒤에만 멤버들을 answered 로 마킹한다(부분 적용 금지).
353
+ 멤버 submit 이 WizardError 를 던지면 그대로 전파되어 같은 그룹을 재-프롬프트한다.
354
+ """
355
+ try:
356
+ answers = json.loads(value or "{}")
357
+ except json.JSONDecodeError as exc:
358
+ raise WizardError(f"pick_group answer must be a JSON object: {exc}")
359
+ if not isinstance(answers, dict):
360
+ raise WizardError("pick_group answer must be a JSON object")
361
+ echoes: list[str] = []
362
+ for q in prompt.questions:
363
+ echo = STEP_BY_ID[q.step].submit(state, str(answers.get(q.step, "") or ""))
364
+ if echo:
365
+ echoes.append(echo)
366
+ for q in prompt.questions:
367
+ if q.step not in state.answered:
368
+ state.answered.append(q.step)
369
+ nxt = next_prompt(state)
370
+ return {"echo": "; ".join(echoes), "next": nxt.to_json()}
371
+
372
+
373
+ def submit(state: WizardState, value: str) -> dict[str, Any]:
374
+ """Validate the answer for the *currently active* step and advance.
375
+
376
+ Returns {"echo": "...", "next": <Prompt JSON>}. Raises WizardError on
377
+ validation failure (caller may re-prompt).
378
+ """
379
+ prompt = next_prompt(state)
380
+ if prompt.kind == "done":
381
+ return {"echo": "", "next": prompt.to_json()}
382
+ if prompt.kind == "pick_group":
383
+ return _submit_group(state, prompt, value)
384
+ step = STEP_BY_ID[prompt.step]
385
+ echo = step.submit(state, value or "")
386
+ if prompt.step not in state.answered:
387
+ state.answered.append(prompt.step)
388
+ nxt = next_prompt(state)
389
+ return {"echo": echo or "", "next": nxt.to_json()}
390
+ ```
391
+
392
+ > 참고: `_validate_model` 가 WizardError 를 던지면 그 시점 이전 멤버의 state 필드는 이미
393
+ > 설정되어 있지만 `answered` 에는 추가되지 않는다. 재-프롬프트 시 같은 그룹이 다시 나오고
394
+ > 사용자가 다시 고른 값으로 덮어쓰므로 결과적으로 안전하다.
395
+
396
+ - [ ] **Step 4: 통과 확인**
397
+
398
+ Run: `python3 -m pytest tests/test_wizard_pick_group.py -v`
399
+ Expected: PASS (전체)
400
+
401
+ - [ ] **Step 5: 커밋**
402
+
403
+ ```bash
404
+ git add scripts/okstra_ctl/wizard.py tests/test_wizard_pick_group.py
405
+ git commit -m "feat(wizard): route pick_group JSON answers to member submits"
406
+ ```
407
+
408
+ ---
409
+
410
+ ## Task 4: 기존 customize 흐름 테스트 갱신
411
+
412
+ **Files:**
413
+ - Modify: `tests/test_okstra_ctl_wizard.py:389-501` (`test_error_analysis_full_customize`)
414
+ - Modify: `tests/test_okstra_ctl_wizard.py` (그 외 `S_LEAD_MODEL`/`directive_pick` 단일픽을 가정하는 테스트)
415
+
416
+ - [ ] **Step 1: 영향 테스트 식별**
417
+
418
+ Run: `python3 -m pytest tests/test_okstra_ctl_wizard.py -v`
419
+ Expected: 모델/옵션 단일픽을 가정하던 테스트가 FAIL (now `pick_group`). 실패 목록을 확인한다.
420
+
421
+ - [ ] **Step 2: `test_error_analysis_full_customize` 갱신**
422
+
423
+ `tests/test_okstra_ctl_wizard.py:444-466` (lead~report-writer 개별 블록) 을 그룹 제출로 교체:
424
+
425
+ ```python
426
+ # 7) 모델 그룹 (lead + claude + codex + report-writer, 로스터에 gemini 없음)
427
+ p = next_prompt(state)
428
+ assert p.kind == "pick_group"
429
+ assert p.step == "models"
430
+ assert [q.step for q in p.questions] == [
431
+ "lead_model", "claude_model", "codex_model", "report_writer_model"]
432
+ submit(state, json.dumps({
433
+ "lead_model": "default",
434
+ "claude_model": "sonnet",
435
+ "codex_model": "gpt-5.5",
436
+ "report_writer_model": "default",
437
+ }))
438
+ assert state.lead_model == ""
439
+ assert state.claude_model == "sonnet"
440
+ assert state.codex_model == "gpt-5.5"
441
+ ```
442
+
443
+ 이어서 `tests/test_okstra_ctl_wizard.py:468-481` (directive/related/clarification 개별 블록) 을 옵션 그룹 제출로 교체:
444
+
445
+ ```python
446
+ # 8) 옵션 그룹 (directive + related-tasks + clarification 픽)
447
+ p = next_prompt(state)
448
+ assert p.kind == "pick_group"
449
+ assert p.step == "options"
450
+ assert [q.step for q in p.questions] == [
451
+ "directive_pick", "related_tasks_pick", "clarification_pick"]
452
+ submit(state, json.dumps({
453
+ "directive_pick": "__skip__",
454
+ "related_tasks_pick": "__skip__",
455
+ "clarification_pick": "__skip__",
456
+ }))
457
+ ```
458
+
459
+ 파일 상단 import 에 `import json` 이 없으면 추가한다 (이미 있으면 생략).
460
+
461
+ - [ ] **Step 3: 다른 영향 테스트 갱신**
462
+
463
+ Step 1 에서 FAIL 한 나머지 테스트(예: `test_implementation_customize_has_executor_model_and_no_workers_override` [:611](../../../tests/test_okstra_ctl_wizard.py), 그 외 `assert p.step == S_LEAD_MODEL` / `== "directive_pick"` 를 직접 가정하는 테스트)를 동일 패턴으로 갱신한다. implementation 모델 그룹 멤버는 `["lead_model", "executor_model", "report_writer_model"]` 이다.
464
+
465
+ 각 실패 테스트에서:
466
+ - `assert p.step == S_LEAD_MODEL` → 그룹 방출 (`p.kind == "pick_group"`, `p.step == "models"`) 로 변경하고, 개별 `submit(state, "...")` 호출을 단일 JSON `submit` 으로 합친다.
467
+ - `assert p.step == "directive_pick"` → `options` 그룹 방출로 변경.
468
+
469
+ - [ ] **Step 4: 전체 통과 확인**
470
+
471
+ Run: `python3 -m pytest tests/test_okstra_ctl_wizard.py tests/test_wizard_pick_group.py -v`
472
+ Expected: PASS (전체)
473
+
474
+ - [ ] **Step 5: 커밋**
475
+
476
+ ```bash
477
+ git add tests/test_okstra_ctl_wizard.py
478
+ git commit -m "test(wizard): adapt customize-branch tests to grouped prompts"
479
+ ```
480
+
481
+ ---
482
+
483
+ ## Task 5: SKILL.md `pick_group` 렌더 규칙
484
+
485
+ **Files:**
486
+ - Modify: `skills/okstra-run/SKILL.md:41-50`, `:96-98`
487
+
488
+ - [ ] **Step 1: "How the wizard talks to you" 섹션에 규칙 추가**
489
+
490
+ `skills/okstra-run/SKILL.md` 의 `kind` 설명 목록 ([:43](../../../skills/okstra-run/SKILL.md)) 바로 아래(`kind: "pick"` + `multi: true` 항목 다음)에 추가:
491
+
492
+ ```markdown
493
+ - `kind: "pick_group"` → render a SINGLE `AskUserQuestion` whose `questions` array maps 1:1 to the wizard's `questions[]`. For each entry use `questions[].label`, `questions[].options[].label`, and `multiSelect: questions[].multi`. Collect the user's chosen `options[].value` per tab, build a JSON object keyed by each `questions[].step`, and submit it as a single literal `--answer '{"lead_model":"opus","claude_model":"default",...}'`. A tab the user leaves at its default still gets its `"default"`/`""` value in the JSON. Never split a `pick_group` into multiple `AskUserQuestion` calls — the wizard already capped it at 4 tabs and will emit any remainder as the next prompt.
494
+ ```
495
+
496
+ - [ ] **Step 2: 프롬프트 루프 섹션에 동일 분기 추가**
497
+
498
+ `skills/okstra-run/SKILL.md` Step 3 의 Render 목록 ([:96](../../../skills/okstra-run/SKILL.md)) 에 추가:
499
+
500
+ ```markdown
501
+ - `pick_group` → one `AskUserQuestion` with one question per `questions[]` entry (tab). Map each tab's selected `value` back by `questions[].step`, assemble a JSON object, and submit it as a single literal `--answer '<json>'`.
502
+ ```
503
+
504
+ - [ ] **Step 3: 빌드 동기화**
505
+
506
+ Run: `npm run build`
507
+ Expected: 성공, `runtime/` 에 SKILL.md 반영.
508
+
509
+ - [ ] **Step 4: 커밋**
510
+
511
+ ```bash
512
+ git add skills/okstra-run/SKILL.md runtime/
513
+ git commit -m "docs(skills/okstra-run): render pick_group as one multi-tab AskUserQuestion"
514
+ ```
515
+
516
+ ---
517
+
518
+ ## Task 6: 회귀 검증
519
+
520
+ **Files:** (없음 — 검증만)
521
+
522
+ - [ ] **Step 1: 전체 단위 테스트**
523
+
524
+ Run: `python3 -m pytest tests/ -q`
525
+ Expected: 전체 PASS (특히 `tests/test_wizard_prompts.py` step-id 동기화/orphan 검사 — 그룹 상수는 `S_*` 가 아니므로 영향 없음).
526
+
527
+ - [ ] **Step 2: phase contract validator**
528
+
529
+ Run: `bash validators/validate-workflow.sh`
530
+ Expected: 통과.
531
+
532
+ - [ ] **Step 3: CLI smoke**
533
+
534
+ Run: `node bin/okstra --version`
535
+ Expected: 버전 출력.
536
+
537
+ - [ ] **Step 4: CHANGES.md 항목 추가**
538
+
539
+ `CHANGES.md` 상단에 사용자 영향 항목 추가:
540
+
541
+ ```markdown
542
+ - okstra-run: customize 단계의 모델/옵션 선택을 멀티탭으로 묶어 입력 왕복 횟수를 줄임.
543
+ 사용자 영향: lead/worker/report 모델과 directive/related-tasks/clarification 픽을 한 화면에서 선택.
544
+ ```
545
+
546
+ - [ ] **Step 5: 커밋**
547
+
548
+ ```bash
549
+ git add CHANGES.md
550
+ git commit -m "docs(changes): log multi-tab wizard prompts"
551
+ ```
552
+
553
+ ---
554
+
555
+ ## Self-Review 메모
556
+
557
+ - **Spec coverage**: pick_group 모델(§3.1)=Task1, 그룹정의(§3.2)=Task1, next_prompt(§3.3)=Task2, 제출(§3.4)=Task3, SKILL(§3.5)=Task5, 테스트(§5)=Task3·4·6. 전 항목 매핑됨.
558
+ - **Type 일관성**: `GROUP_MODELS`/`GROUP_OPTIONS`/`GROUP_MAX_TABS`/`PROMPT_GROUPS`/`GROUP_LABELS`/`_STEP_TO_GROUP`/`_build_group_prompt`/`_submit_group`/`Prompt.questions` 식별자가 전 태스크에서 동일 표기.
559
+ - **비영향**: `use_defaults=True` 경로, identity 단계, confirm/edit-target, `render_args`, `_ready_for_confirm` 의 개별 step 검사 모두 불변.
@@ -0,0 +1,121 @@
1
+ # okstra wizard 멀티탭 배치 프롬프트 설계
2
+
3
+ - 작성일: 2026-06-05
4
+ - 대상: `scripts/okstra_ctl/wizard.py`, `skills/okstra-run/SKILL.md`
5
+ - 상태: 설계 승인 대기
6
+
7
+ ## 1. 배경 / 문제
8
+
9
+ `okstra-run` 스킬은 wizard 상태 머신이 내보내는 프롬프트를 한 개씩
10
+ `AskUserQuestion` 으로 렌더하고, 사용자가 답하면 다음 프롬프트를 받는 **답-대기 왕복**
11
+ 구조다 ([wizard.py:2238](../../../scripts/okstra_ctl/wizard.py), [SKILL.md:96](../../../skills/okstra-run/SKILL.md)).
12
+
13
+ `use_defaults=False`(customize) 분기에서는 서로 의존이 없는 픽이 줄줄이 개별 프롬프트로
14
+ 나온다:
15
+
16
+ - 모델: `lead_model` → (impl) `executor_model` / (그 외) 로스터별 `claude_model`·`codex_model`·`gemini_model` → `report_writer_model` — 최대 5회
17
+ - 옵션: `directive_pick` → `related_tasks_pick` → `clarification_pick` → (release-handoff) `pr_template_pick` — 최대 4회
18
+
19
+ 각 픽이 별도 왕복이라 전체 입력에 시간이 오래 걸린다. **비슷한 유형의 독립 질문을 탭으로
20
+ 묶어 한 번의 `AskUserQuestion` 으로 받아** 왕복 수를 줄이는 것이 목표다.
21
+
22
+ ## 2. 핵심 원칙
23
+
24
+ **기존 `Step` 은 그대로 두고 "방출(presentation) 계층" 에만 배치 개념을 추가한다.**
25
+
26
+ - `answered` / `owns` / edit-rewind(`_reset_from`) / `_ready_for_confirm` 의 `custom_ids`
27
+ 검사는 전부 **개별 step id 단위**로 유지된다 ([wizard.py:2149](../../../scripts/okstra_ctl/wizard.py)).
28
+ - 그룹은 "서로 의존이 없는 픽 step 들을 한 화면에 모아 내보내는" 래퍼일 뿐, step 자체를
29
+ 대체하지 않는다. 따라서 검증·되감기 로직은 무손상이다.
30
+
31
+ ## 3. 변경 사항
32
+
33
+ ### 3.1 새 prompt kind `pick_group`
34
+
35
+ `Prompt` 직렬화에 멀티-질문 모양을 추가한다 ([wizard.py:299](../../../scripts/okstra_ctl/wizard.py)).
36
+
37
+ ```json
38
+ {
39
+ "step": "<group-id>",
40
+ "kind": "pick_group",
41
+ "questions": [
42
+ { "step": "lead_model", "label": "...", "options": [ {"value":"..","label":".."} ], "multi": false },
43
+ { "step": "claude_model", "label": "...", "options": [...], "multi": false }
44
+ ]
45
+ }
46
+ ```
47
+
48
+ - `questions[]` 한 항목 = `AskUserQuestion` 탭 하나.
49
+ - 각 항목은 기존 step 의 `build()` 가 만든 `label` / `options` 를 그대로 재사용한다(중복 정의 금지).
50
+ - 기존 `kind: "pick"` / `"text"` / `"done"` 은 변경 없음.
51
+
52
+ ### 3.2 그룹 정의
53
+
54
+ customize 분기의 **픽 step 만** 그룹화한다. 순서가 있는 명시적 정의를 wizard.py 에 둔다.
55
+
56
+ | 그룹 id | 멤버 step (적용 가능할 때만) |
57
+ |---|---|
58
+ | `models` | `lead_model`, (impl) `executor_model` / (그 외) `claude_model`·`codex_model`·`gemini_model`, `report_writer_model` |
59
+ | `options` | `directive_pick`, `related_tasks_pick`, `clarification_pick`, (release-handoff) `pr_template_pick` |
60
+
61
+ **개별 유지(그룹화 금지) 대상**:
62
+
63
+ - `*_TEXT` 후속(`directive`, `related_tasks`, `clarification`, `pr_template`): "직접 입력"
64
+ 선택 시에만 나타나는 조건부 텍스트 입력. `AskUserQuestion` 은 텍스트 입력을 지원하지
65
+ 않으므로 ([SKILL.md:50](../../../skills/okstra-run/SKILL.md)) 개별 텍스트 프롬프트로 유지.
66
+ - `workers_override`: 어떤 모델 탭(claude/codex/gemini)이 적용되는지를 결정하므로 `models`
67
+ 그룹보다 **반드시 선행**해야 한다. 개별 유지.
68
+ - `pr_template_scope`: `pr_template_path` 가 정해진 뒤에야 적용되므로 개별 유지.
69
+
70
+ ### 3.3 엔진 `next_prompt` 수정
71
+
72
+ [wizard.py:2238](../../../scripts/okstra_ctl/wizard.py) 의 다음-프롬프트 결정 로직:
73
+
74
+ 1. 기존대로 첫 번째 "적용 가능 + 미답변" step 을 찾는다.
75
+ 2. 그 step 이 그룹 멤버가 아니면 기존 `Prompt` 를 그대로 반환(동작 불변).
76
+ 3. 그룹 멤버이면 같은 그룹의 **적용 가능 + 미답변 픽 멤버**를 정의 순서대로 **최대 4개**까지
77
+ 모아 `pick_group` 으로 반환한다.
78
+ - `AskUserQuestion` 의 질문 수 한도가 4개이기 때문.
79
+ - 비-implementation 풀 로스터(lead+claude+codex+gemini+report = 5)는 첫 4개만 한 배치로
80
+ 나가고, 답변 후 5번째가 다음 배치로 자동 분리된다. **스킬에 청크 로직 불필요**, 최악 2화면.
81
+
82
+ ### 3.4 제출 경로 수정
83
+
84
+ [wizard.py:2233](../../../scripts/okstra_ctl/wizard.py) 의 advance, CLI `--answer` 파싱([wizard.py:2363](../../../scripts/okstra_ctl/wizard.py)):
85
+
86
+ - 현재 활성 프롬프트가 `pick_group` 이면 `--answer` 를 **JSON 객체 문자열**로 받는다.
87
+ ```
88
+ okstra wizard step --state-file <path> --answer '{"lead_model":"opus","claude_model":"default","report_writer_model":""}'
89
+ ```
90
+ - 엔진은 각 키를 해당 멤버 step 의 `submit()` 으로 라우팅하고, 각 멤버를 개별적으로
91
+ `answered` 에 추가한다.
92
+ - 키 누락 / 빈 값("" 또는 "default")은 기존 `_validate_model` 규칙대로 "phase 기본값"으로
93
+ 처리된다 ([wizard.py:418](../../../scripts/okstra_ctl/wizard.py)). 멤버 중 하나라도
94
+ 검증 실패하면 `ok:false` 로 같은 그룹을 재-프롬프트한다(부분 적용 금지: 전부 검증 통과해야
95
+ `answered` 마킹).
96
+
97
+ ### 3.5 SKILL.md 렌더 규칙 추가
98
+
99
+ [SKILL.md:41-50](../../../skills/okstra-run/SKILL.md), [SKILL.md:96](../../../skills/okstra-run/SKILL.md):
100
+
101
+ - `kind: "pick_group"` → `questions[]` 를 탭(질문)으로 갖는 `AskUserQuestion` **1회** 호출.
102
+ 탭마다 `label` + `options`, `multiSelect` 는 `questions[].multi`.
103
+ - 사용자의 탭별 선택값(`options[].value`)을 `questions[].step` 키로 묶어 JSON 문자열을 만들고,
104
+ 단일 `--answer '<json>'` 으로 제출.
105
+ - 리터럴-토큰 권한 규칙 유지: `--answer` 값은 셸 변수/`$(...)` 없이 리터럴 JSON 문자열로 전달.
106
+ - 검증 실패(`ok:false`) 시 동일 그룹 재-프롬프트.
107
+
108
+ ## 4. 영향 / 비영향
109
+
110
+ - **동작 변경**: customize 분기의 모델·옵션 픽이 멀티탭으로 묶여 왕복 횟수 감소
111
+ (모델 3~5픽 → 1~2회, 옵션 3~4픽 → 1회).
112
+ - **불변**: `use_defaults=True` 경로, identity 단계(task pick/type/base-ref/plan/executor),
113
+ confirm/branch_confirm/edit-target, render-bundle 인자 매핑([wizard.py:2249](../../../scripts/okstra_ctl/wizard.py)).
114
+ - **"직접 입력"** 을 고른 항목만 텍스트 후속이 개별 프롬프트로 남는다(불가피).
115
+
116
+ ## 5. 테스트
117
+
118
+ - 기존 wizard 단위 테스트(`tests/`) 가 개별 step 계약을 그대로 통과해야 한다(그룹은 방출 계층만 변경).
119
+ - 신규: `pick_group` 방출(멤버 4개 초과 → 2배치 분리), JSON `--answer` 라우팅(부분 검증 실패 시
120
+ 재-프롬프트), workers_override 선행 → models 그룹 로스터 반영을 커버하는 테스트 추가.
121
+ - `node bin/okstra --version` / `bash validators/validate-workflow.sh` 회귀 확인.
@@ -87,7 +87,7 @@ final report는 다음을 담아야 한다.
87
87
  - evidence-backed cause analysis
88
88
  - uncertainty boundary
89
89
  - practical next diagnostic steps
90
- - blocking uncertainty가 있으면 `## 5. Clarification Items`, 보통 `Blocks=next-phase`
90
+ - blocking uncertainty가 있으면 `## 1. Clarification Items`, 보통 `Blocks=next-phase`
91
91
 
92
92
  금지되는 것은 source edit, refactor, fix attempt, implementation design artifact, build/migration/deploy 실행이다. code나 log로 답할 수 있는 ambiguity를 사용자 질문으로 넘기는 것도 profile상 defect다.
93
93