okstra 0.34.0 → 0.36.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 (101) hide show
  1. package/README.kr.md +26 -16
  2. package/README.md +26 -16
  3. package/docs/kr/architecture.md +59 -45
  4. package/docs/kr/cli.md +61 -18
  5. package/docs/pr-template-usage.md +65 -0
  6. package/docs/project-structure-overview.md +358 -354
  7. package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +1 -1
  8. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1 -1
  9. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +1 -1
  10. package/docs/superpowers/plans/2026-05-20-final-report-language.md +1501 -0
  11. package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +1267 -0
  12. package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +1007 -0
  13. package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +720 -0
  14. package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +681 -0
  15. package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +1691 -0
  16. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +383 -0
  17. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +320 -0
  18. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +299 -0
  19. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +335 -0
  20. package/docs/task-process/README.md +74 -0
  21. package/docs/task-process/common-flow.md +166 -0
  22. package/docs/task-process/error-analysis.md +101 -0
  23. package/docs/task-process/final-verification.md +167 -0
  24. package/docs/task-process/implementation-planning.md +128 -0
  25. package/docs/task-process/implementation.md +149 -0
  26. package/docs/task-process/release-handoff.md +206 -0
  27. package/docs/task-process/requirements-discovery.md +115 -0
  28. package/package.json +1 -1
  29. package/runtime/BUILD.json +2 -2
  30. package/runtime/agents/SKILL.md +29 -13
  31. package/runtime/agents/workers/claude-worker.md +26 -0
  32. package/runtime/agents/workers/codex-worker.md +27 -1
  33. package/runtime/agents/workers/gemini-worker.md +27 -1
  34. package/runtime/agents/workers/report-writer-worker.md +8 -1
  35. package/runtime/bin/okstra-central.sh +6 -6
  36. package/runtime/bin/okstra-codex-exec.sh +49 -28
  37. package/runtime/bin/okstra-gemini-exec.sh +39 -21
  38. package/runtime/bin/okstra-render-final-report.py +13 -2
  39. package/runtime/bin/okstra-wrapper-status.py +155 -0
  40. package/runtime/bin/okstra.sh +2 -2
  41. package/runtime/prompts/profiles/_common-contract.md +11 -6
  42. package/runtime/prompts/profiles/error-analysis.md +3 -7
  43. package/runtime/prompts/profiles/implementation-planning.md +22 -21
  44. package/runtime/prompts/profiles/implementation.md +28 -11
  45. package/runtime/prompts/profiles/improvement-discovery.md +42 -0
  46. package/runtime/prompts/profiles/kr/_common-contract.md +92 -0
  47. package/runtime/prompts/profiles/kr/error-analysis.md +36 -0
  48. package/runtime/prompts/profiles/kr/final-verification.md +48 -0
  49. package/runtime/prompts/profiles/kr/implementation-planning.md +90 -0
  50. package/runtime/prompts/profiles/kr/implementation.md +144 -0
  51. package/runtime/prompts/profiles/kr/improvement-discovery.md +42 -0
  52. package/runtime/prompts/profiles/kr/release-handoff.md +104 -0
  53. package/runtime/prompts/profiles/kr/requirements-discovery.md +42 -0
  54. package/runtime/prompts/profiles/release-handoff.md +1 -1
  55. package/runtime/prompts/profiles/requirements-discovery.md +8 -12
  56. package/runtime/prompts/wizard/prompts.ko.json +230 -0
  57. package/runtime/python/lib/okstra/cli.sh +2 -49
  58. package/runtime/python/lib/okstra/globals.sh +21 -21
  59. package/runtime/python/lib/okstra/interactive.sh +7 -7
  60. package/runtime/python/okstra_ctl/clarification_items.py +3 -9
  61. package/runtime/python/okstra_ctl/consumers.py +53 -0
  62. package/runtime/python/okstra_ctl/final_report_schema.py +0 -7
  63. package/runtime/python/okstra_ctl/i18n.py +73 -0
  64. package/runtime/python/okstra_ctl/improvement_lenses.py +44 -0
  65. package/runtime/python/okstra_ctl/index.py +1 -1
  66. package/runtime/python/okstra_ctl/paths.py +23 -20
  67. package/runtime/python/okstra_ctl/render.py +147 -202
  68. package/runtime/python/okstra_ctl/render_final_report.py +53 -10
  69. package/runtime/python/okstra_ctl/run.py +292 -107
  70. package/runtime/python/okstra_ctl/run_context.py +22 -0
  71. package/runtime/python/okstra_ctl/seeding.py +186 -0
  72. package/runtime/python/okstra_ctl/wizard.py +348 -127
  73. package/runtime/python/okstra_ctl/workflow.py +21 -2
  74. package/runtime/python/okstra_ctl/worktree.py +54 -1
  75. package/runtime/python/okstra_project/resolver.py +4 -3
  76. package/runtime/python/okstra_token_usage/report.py +2 -2
  77. package/runtime/schemas/final-report-v1.0.schema.json +22 -16
  78. package/runtime/skills/okstra-brief/SKILL.md +124 -31
  79. package/runtime/skills/okstra-convergence/SKILL.md +2 -3
  80. package/runtime/skills/okstra-report-writer/SKILL.md +35 -15
  81. package/runtime/skills/okstra-run/SKILL.md +5 -4
  82. package/runtime/skills/okstra-schedule/SKILL.md +4 -4
  83. package/runtime/skills/okstra-setup/SKILL.md +27 -0
  84. package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
  85. package/runtime/templates/okstra.CLAUDE.md +104 -0
  86. package/runtime/templates/reports/final-report.template.md +93 -98
  87. package/runtime/templates/reports/i18n/en.json +135 -0
  88. package/runtime/templates/reports/i18n/ko.json +135 -0
  89. package/runtime/templates/reports/implementation-planning-input.template.md +18 -0
  90. package/runtime/templates/reports/improvement-discovery-input.template.md +78 -0
  91. package/runtime/templates/reports/task-brief.template.md +2 -2
  92. package/runtime/validators/lib/fixtures.sh +30 -0
  93. package/runtime/validators/lib/runners.sh +1 -1
  94. package/runtime/validators/validate-implementation-plan-stages.py +211 -0
  95. package/runtime/validators/validate-run.py +121 -26
  96. package/runtime/validators/validate-workflow.sh +2 -2
  97. package/runtime/validators/validate_improvement_report.py +275 -0
  98. package/src/config.mjs +18 -0
  99. package/src/install.mjs +41 -14
  100. package/src/setup.mjs +133 -1
  101. package/src/uninstall.mjs +21 -1
@@ -0,0 +1,720 @@
1
+ # Wizard Messages JSON SOT (A1 Follow-up) Implementation Plan
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:** Phase A1 의 JSON SOT 를 wizard.py 의 모든 user-facing 한글 문자열 (`_submit_*` echo variants, `WizardError` 메시지, `confirmation_block` 출력) 까지 확장한다. wizard.py 의 한글 0 (docstring 제외) 달성.
6
+
7
+ **Architecture:**
8
+ 1. JSON SOT (`prompts/wizard/prompts.ko.json`) 스키마 확장 — step entry 안에 `echo_variants` / `errors` 서브필드 추가, top-level `confirmation` 섹션 추가.
9
+ 2. wizard.py 에 `_load_wizard_root(workspace_root)` 헬퍼 (전체 root 캐시) + `_msg(workspace_root, section, key, **vars)` 헬퍼 (top-level 섹션 lookup) 추가. 기존 `_p()` 의 반환 dict 에 `echo_variants`/`errors` 패스스루.
10
+ 3. 7 한글 사이트 (3 `_submit_*` echo + 2 `WizardError` + 2 `confirmation_block` 라인) atomic migrate.
11
+ 4. Korean grep gate scope 를 `_submit_*` + `confirmation_block` 까지 확장 — wizard.py 코드 라인 한글 0 (docstring 제외) 정적 보장.
12
+
13
+ **Tech Stack:** Python 3 stdlib (`json`, `pathlib`), pytest.
14
+
15
+ **Reference:** [docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md](../specs/2026-05-20-okstra-run-prompt-sot-design.md) §3. 본 plan 은 Phase A1 의 plan scope 외였던 follow-up 작업.
16
+
17
+ ---
18
+
19
+ ## File Structure
20
+
21
+ **수정 only**
22
+ - `prompts/wizard/prompts.ko.json` — Task 1
23
+ - `scripts/okstra_ctl/wizard.py` — Task 2 (loader/`_msg`) + Task 3 (사이트 migration)
24
+ - `tests/test_wizard_prompts.py` — Task 2 (헬퍼 테스트) + Task 4 (grep gate scope 확장)
25
+ - `CHANGES.md` — Task 4
26
+
27
+ 손대지 않음: `runtime/` (build output), 다른 모듈.
28
+
29
+ ---
30
+
31
+ ## Migration target inventory (구체적)
32
+
33
+ | # | wizard.py 위치 | 현재 한글 | JSON 신규 키 |
34
+ |---|---|---|---|
35
+ | 1 | `_submit_task_group:643` | `"task-group: (직접 입력)"` | `steps.task_group.echo_variants.free_input` |
36
+ | 2 | `_submit_task_id:694` | `"task-id: (직접 입력)"` | `steps.task_id.echo_variants.free_input` |
37
+ | 3 | `_submit_brief_keep:769` | `f"brief: {state.brief_path} (유지)"` | `steps.brief_keep.echo_variants.kept` (with `{brief_path}` placeholder) |
38
+ | 4 | `_submit_approved_plan_pick:900` | `"기본 approved-plan 경로를 찾을 수 없습니다. '다른 경로 입력'을 선택하세요."` | `steps.approved_plan_pick.errors.default_not_found` |
39
+ | 5 | `_submit_workers_override:1080` | `"워커를 최소 1개 선택해주세요"` | `steps.workers_override.errors.min_one_required` |
40
+ | 6 | `confirmation_block:1717` | `"선택 확인:"` | `confirmation.header` |
41
+ | 7 | `confirmation_block:1727` | `" workers : (프로필 기본 — executor + verifier 2 + report-writer)"` | `confirmation.workers_implementation_default` |
42
+
43
+ ---
44
+
45
+ ## Task 1: JSON SOT 스키마 확장
46
+
47
+ **Files:**
48
+ - Modify: `prompts/wizard/prompts.ko.json`
49
+
50
+ 이 task 의 commit 후에도 동작 동일 — JSON 신규 필드는 caller 없음 (Task 3 에서 사용).
51
+
52
+ - [ ] **Step 1: 신규 필드 추가**
53
+
54
+ `prompts/wizard/prompts.ko.json` 의 5개 step entry 에 `echo_variants` / `errors` 서브필드 추가 + 최상위 `confirmation` 섹션 추가:
55
+
56
+ `steps.task_group` 에 다음 키 추가:
57
+ ```json
58
+ "echo_variants": {
59
+ "free_input": "task-group: (직접 입력)"
60
+ }
61
+ ```
62
+
63
+ `steps.task_id` 에:
64
+ ```json
65
+ "echo_variants": {
66
+ "free_input": "task-id: (직접 입력)"
67
+ }
68
+ ```
69
+
70
+ `steps.brief_keep` 에:
71
+ ```json
72
+ "echo_variants": {
73
+ "kept": "brief: {brief_path} (유지)"
74
+ }
75
+ ```
76
+
77
+ `steps.approved_plan_pick` 에:
78
+ ```json
79
+ "errors": {
80
+ "default_not_found": "기본 approved-plan 경로를 찾을 수 없습니다. '다른 경로 입력'을 선택하세요."
81
+ }
82
+ ```
83
+
84
+ `steps.workers_override` 에:
85
+ ```json
86
+ "errors": {
87
+ "min_one_required": "워커를 최소 1개 선택해주세요"
88
+ }
89
+ ```
90
+
91
+ 최상위 (top-level, `steps` 와 형제) 에 신규 섹션:
92
+ ```json
93
+ "confirmation": {
94
+ "header": "선택 확인:",
95
+ "workers_implementation_default": " workers : (프로필 기본 — executor + verifier 2 + report-writer)"
96
+ }
97
+ ```
98
+
99
+ JSON 인덴트는 기존 파일 패턴 따름 (2-space).
100
+
101
+ - [ ] **Step 2: 유효성 확인**
102
+
103
+ ```bash
104
+ python3 -m json.tool prompts/wizard/prompts.ko.json > /dev/null && echo "JSON valid"
105
+ ```
106
+
107
+ ```bash
108
+ python3 << 'PY'
109
+ import json
110
+ d = json.load(open('prompts/wizard/prompts.ko.json'))
111
+ assert d['steps']['task_group']['echo_variants']['free_input'] == "task-group: (직접 입력)"
112
+ assert d['steps']['task_id']['echo_variants']['free_input'] == "task-id: (직접 입력)"
113
+ assert "{brief_path}" in d['steps']['brief_keep']['echo_variants']['kept']
114
+ assert "approved-plan" in d['steps']['approved_plan_pick']['errors']['default_not_found']
115
+ assert d['steps']['workers_override']['errors']['min_one_required'] == "워커를 최소 1개 선택해주세요"
116
+ assert d['confirmation']['header'] == "선택 확인:"
117
+ assert "report-writer" in d['confirmation']['workers_implementation_default']
118
+ print("OK: all 7 new entries present")
119
+ PY
120
+ ```
121
+
122
+ - [ ] **Step 3: 회귀 통과 (변동 없음)**
123
+
124
+ ```bash
125
+ python3 -m pytest tests/ -x
126
+ ```
127
+ Expected: 604 passed, 1 skipped (baseline 유지 — 새 JSON 키는 caller 없어서 영향 없음).
128
+
129
+ - [ ] **Step 4: Commit**
130
+
131
+ ```bash
132
+ git add prompts/wizard/prompts.ko.json
133
+ git commit -m "feat(prompts): add echo_variants/errors/confirmation sections to wizard SOT"
134
+ ```
135
+
136
+ No Claude trailer, no Co-Authored-By, no `🤖 Generated with` footer.
137
+
138
+ ---
139
+
140
+ ## Task 2: `_load_wizard_root` + `_msg` 헬퍼 추가 + `_p()` 확장 (TDD)
141
+
142
+ **Files:**
143
+ - Modify: `scripts/okstra_ctl/wizard.py`
144
+ - Modify: `tests/test_wizard_prompts.py`
145
+
146
+ 이 task 의 commit 후에도 동작 동일 — 새 헬퍼는 정의됐을 뿐 caller 없음 (Task 3).
147
+
148
+ - [ ] **Step 1: Failing tests 추가**
149
+
150
+ `tests/test_wizard_prompts.py` 끝에 추가:
151
+
152
+ ```python
153
+ def test_p_includes_echo_variants():
154
+ """_p() 반환 dict 가 echo_variants 도 포함해야 한다."""
155
+ t = _p(str(REPO_ROOT), "task_group")
156
+ assert "echo_variants" in t
157
+ assert t["echo_variants"]["free_input"] == "task-group: (직접 입력)"
158
+
159
+
160
+ def test_p_includes_errors():
161
+ """_p() 반환 dict 가 errors 도 포함해야 한다."""
162
+ t = _p(str(REPO_ROOT), "approved_plan_pick")
163
+ assert "errors" in t
164
+ assert "approved-plan" in t["errors"]["default_not_found"]
165
+
166
+
167
+ def test_p_echo_variants_default_empty():
168
+ """echo_variants 가 없는 step 도 빈 dict 로 안전하게 반환."""
169
+ t = _p(str(REPO_ROOT), "task_pick")
170
+ assert t.get("echo_variants", {}) == {} or "echo_variants" not in t or t["echo_variants"] == {}
171
+
172
+
173
+ def test_msg_returns_confirmation_header():
174
+ """_msg() 가 top-level confirmation 섹션의 값을 반환."""
175
+ from okstra_ctl.wizard import _msg
176
+ result = _msg(str(REPO_ROOT), "confirmation", "header")
177
+ assert result == "선택 확인:"
178
+
179
+
180
+ def test_msg_unknown_section_raises():
181
+ """미정의 섹션 → WizardError."""
182
+ from okstra_ctl.wizard import _msg
183
+ with pytest.raises(WizardError) as exc:
184
+ _msg(str(REPO_ROOT), "nonexistent_section", "any_key")
185
+ assert "nonexistent_section" in str(exc.value)
186
+
187
+
188
+ def test_msg_unknown_key_raises():
189
+ """섹션 안에 키 없으면 WizardError."""
190
+ from okstra_ctl.wizard import _msg
191
+ with pytest.raises(WizardError) as exc:
192
+ _msg(str(REPO_ROOT), "confirmation", "nonexistent_key_xyz")
193
+ assert "nonexistent_key_xyz" in str(exc.value)
194
+
195
+
196
+ def test_msg_applies_placeholder():
197
+ """_msg() 가 placeholder 를 vars 로 치환 (workers_implementation_default 같은 정적 케이스는 placeholder 없음 — 함수 동작 확인용)."""
198
+ from okstra_ctl.wizard import _msg
199
+ # Use brief_keep echo_variants which has {brief_path} placeholder via _p, but also test _msg behavior with a synthetic case.
200
+ # The real placeholder test: workers_implementation_default has no placeholder, but _msg should still handle empty vars.
201
+ result = _msg(str(REPO_ROOT), "confirmation", "workers_implementation_default")
202
+ assert "프로필 기본" in result
203
+ ```
204
+
205
+ - [ ] **Step 2: 테스트 실패 확인**
206
+
207
+ ```bash
208
+ python3 -m pytest tests/test_wizard_prompts.py -v 2>&1 | tail -15
209
+ ```
210
+ Expected: 신규 7 tests 모두 FAIL (import error: `_msg` 없음, 또는 `echo_variants`/`errors` 키 없음).
211
+
212
+ - [ ] **Step 3: wizard.py 에 `_load_wizard_root` + `_msg` 구현 + `_p()` 확장**
213
+
214
+ `scripts/okstra_ctl/wizard.py` 의 기존 `_load_wizard_prompts` 정의 직전에 새 함수 추가:
215
+
216
+ ```python
217
+ _WIZARD_ROOT_CACHE: dict[str, dict] = {}
218
+
219
+
220
+ def _load_wizard_root(workspace_root: str) -> dict:
221
+ """Load and cache the full wizard prompts JSON root for the given workspace_root.
222
+
223
+ Returns the entire parsed JSON object (with `schema_version`, `locale`, `steps`,
224
+ optional `confirmation` etc. at top level). Used internally by
225
+ `_load_wizard_prompts` (which returns `steps`) and `_msg` (which reads
226
+ top-level sections like `confirmation`).
227
+ """
228
+ if workspace_root in _WIZARD_ROOT_CACHE:
229
+ return _WIZARD_ROOT_CACHE[workspace_root]
230
+ path = Path(workspace_root) / "prompts" / "wizard" / "prompts.ko.json"
231
+ if not path.is_file():
232
+ raise WizardError(
233
+ f"wizard prompt SOT not found: {path}. "
234
+ "Re-run `okstra install` or check the workspace_root."
235
+ )
236
+ try:
237
+ raw = json.loads(path.read_text(encoding="utf-8"))
238
+ except json.JSONDecodeError as exc:
239
+ raise WizardError(f"wizard prompt JSON malformed: {path}: {exc}") from exc
240
+ if not isinstance(raw, dict):
241
+ raise WizardError(f"wizard prompt JSON root must be an object: {path}")
242
+ if raw.get("schema_version") != 1:
243
+ raise WizardError(
244
+ f"wizard prompt schema_version mismatch (expected 1): {path}"
245
+ )
246
+ if raw.get("locale") != "ko":
247
+ raise WizardError(
248
+ f"wizard prompt locale unsupported (expected 'ko'): {path}"
249
+ )
250
+ steps = raw.get("steps")
251
+ if not isinstance(steps, dict):
252
+ raise WizardError(f"wizard prompt 'steps' missing or not a dict: {path}")
253
+ _WIZARD_ROOT_CACHE[workspace_root] = raw
254
+ return raw
255
+ ```
256
+
257
+ 기존 `_load_wizard_prompts` 함수를 `_load_wizard_root` 의 thin wrapper 로 교체:
258
+
259
+ ```python
260
+ def _load_wizard_prompts(workspace_root: str) -> dict:
261
+ """Backwards-compatible accessor returning the `steps` dict."""
262
+ return _load_wizard_root(workspace_root)["steps"]
263
+ ```
264
+
265
+ (기존 `_WIZARD_PROMPTS_CACHE` 변수 — 더 이상 직접 사용 안 함. 새 `_WIZARD_ROOT_CACHE` 가 root 를 캐싱하므로 `_load_wizard_prompts` 가 매번 `_load_wizard_root["steps"]` 를 호출해도 캐시 hit. 옛 `_WIZARD_PROMPTS_CACHE` 는 dead code — 제거.)
266
+
267
+ 기존 `_p` 함수의 return 에 echo_variants / errors 추가:
268
+
269
+ ```python
270
+ def _p(workspace_root: str, step_id: str, **vars: str) -> dict:
271
+ """... (기존 docstring 유지, 다음 한 줄 추가)
272
+ Returned dict also includes 'echo_variants' and 'errors' sub-dicts (may be empty).
273
+ """
274
+ steps = _load_wizard_prompts(workspace_root)
275
+ raw = steps.get(step_id)
276
+ if raw is None:
277
+ raise WizardError(f"unknown wizard step_id: {step_id!r}")
278
+ label_template = raw.get("label", "")
279
+ try:
280
+ label = label_template.format(**vars)
281
+ except KeyError as exc:
282
+ missing = exc.args[0] if exc.args else "<unknown>"
283
+ raise WizardError(
284
+ f"missing placeholder {missing!r} for wizard step {step_id!r}"
285
+ ) from exc
286
+ return {
287
+ "label": label,
288
+ "echo_template": raw.get("echo_template", ""),
289
+ "options": raw.get("options", {}),
290
+ "echo_variants": raw.get("echo_variants", {}),
291
+ "errors": raw.get("errors", {}),
292
+ }
293
+ ```
294
+
295
+ 새 `_msg` 함수 추가 (`_p` 정의 직후):
296
+
297
+ ```python
298
+ def _msg(workspace_root: str, section: str, key: str, **vars: str) -> str:
299
+ """Look up a top-level message (e.g., `confirmation.header`) and interpolate.
300
+
301
+ Returns the string value at `<section>.<key>`. Raises WizardError if the
302
+ section or key is unknown, or if a required placeholder is missing.
303
+
304
+ Use for wizard-level messages (confirmation block etc.). For step-scoped
305
+ messages (echo_variants, errors), use `_p()` instead.
306
+ """
307
+ root = _load_wizard_root(workspace_root)
308
+ sect = root.get(section)
309
+ if not isinstance(sect, dict):
310
+ raise WizardError(f"unknown wizard section: {section!r}")
311
+ template = sect.get(key)
312
+ if template is None:
313
+ raise WizardError(f"unknown wizard message: {section}.{key!r}")
314
+ try:
315
+ return template.format(**vars)
316
+ except KeyError as exc:
317
+ missing = exc.args[0] if exc.args else "<unknown>"
318
+ raise WizardError(
319
+ f"missing placeholder {missing!r} for wizard message {section}.{key!r}"
320
+ ) from exc
321
+ ```
322
+
323
+ - [ ] **Step 4: 단위 테스트 + 회귀 통과 확인**
324
+
325
+ ```bash
326
+ python3 -m pytest tests/test_wizard_prompts.py -v 2>&1 | tail -20
327
+ ```
328
+ Expected: 모든 기존 9 + 신규 7 = 16 tests PASS.
329
+
330
+ ```bash
331
+ python3 -m pytest tests/ -x
332
+ ```
333
+ Expected: 611 passed, 1 skipped (604 + 7 new).
334
+
335
+ - [ ] **Step 5: Commit**
336
+
337
+ ```bash
338
+ git add scripts/okstra_ctl/wizard.py tests/test_wizard_prompts.py
339
+ git commit -m "feat(wizard): add _load_wizard_root + _msg helpers; extend _p() with echo_variants/errors"
340
+ ```
341
+
342
+ No Claude trailer, no Co-Authored-By, no `🤖 Generated with` footer.
343
+
344
+ ---
345
+
346
+ ## Task 3: 7 한글 사이트 atomic migration
347
+
348
+ **Files:**
349
+ - Modify: `scripts/okstra_ctl/wizard.py`
350
+
351
+ 부분 마이그레이션 시 SOT 가 깨지므로 한 commit. 동작 보존 — pytest 회귀가 검증.
352
+
353
+ - [ ] **Step 1: `_submit_task_group:643` 교체**
354
+
355
+ `scripts/okstra_ctl/wizard.py` line 643 부근:
356
+
357
+ **Before:**
358
+ ```python
359
+ if value == PICK_TYPE_CUSTOM:
360
+ state.task_group_pending_text = True
361
+ return f"task-group: (직접 입력)"
362
+ ```
363
+
364
+ **After:**
365
+ ```python
366
+ if value == PICK_TYPE_CUSTOM:
367
+ state.task_group_pending_text = True
368
+ t = _p(state.workspace_root, "task_group")
369
+ return t["echo_variants"]["free_input"]
370
+ ```
371
+
372
+ - [ ] **Step 2: `_submit_task_id:694` 교체**
373
+
374
+ `scripts/okstra_ctl/wizard.py` line 694 부근:
375
+
376
+ **Before:**
377
+ ```python
378
+ if value == PICK_TYPE_CUSTOM:
379
+ state.task_id_pending_text = True
380
+ return f"task-id: (직접 입력)"
381
+ ```
382
+
383
+ **After:**
384
+ ```python
385
+ if value == PICK_TYPE_CUSTOM:
386
+ state.task_id_pending_text = True
387
+ t = _p(state.workspace_root, "task_id")
388
+ return t["echo_variants"]["free_input"]
389
+ ```
390
+
391
+ - [ ] **Step 3: `_submit_brief_keep:769` 교체**
392
+
393
+ `scripts/okstra_ctl/wizard.py` line 769 부근:
394
+
395
+ **Before:**
396
+ ```python
397
+ if state.keep_existing_brief:
398
+ state.brief_path = state.existing_brief_path
399
+ return f"brief: {state.brief_path} (유지)"
400
+ ```
401
+
402
+ **After:**
403
+ ```python
404
+ if state.keep_existing_brief:
405
+ state.brief_path = state.existing_brief_path
406
+ t = _p(state.workspace_root, "brief_keep", existing_brief_path=state.existing_brief_path)
407
+ return t["echo_variants"]["kept"].format(brief_path=state.brief_path)
408
+ ```
409
+
410
+ (주의: `_p()` 의 `vars` 는 `label` 만 보간 — `echo_variants` 의 placeholder 는 별도 `.format()` 필요. 또는 `brief_path` 가 위 `existing_brief_path` 와 동일한 값이므로 단순화 가능.)
411
+
412
+ 대안 (더 간단):
413
+ ```python
414
+ if state.keep_existing_brief:
415
+ state.brief_path = state.existing_brief_path
416
+ t = _p(state.workspace_root, "brief_keep")
417
+ return t["echo_variants"]["kept"].format(brief_path=state.brief_path)
418
+ ```
419
+
420
+ - [ ] **Step 4: `_submit_approved_plan_pick:900` WizardError 교체**
421
+
422
+ `scripts/okstra_ctl/wizard.py` line 900 부근:
423
+
424
+ **Before:**
425
+ ```python
426
+ if default is None:
427
+ raise WizardError(
428
+ "기본 approved-plan 경로를 찾을 수 없습니다. '다른 경로 입력'을 선택하세요."
429
+ )
430
+ ```
431
+
432
+ **After:**
433
+ ```python
434
+ if default is None:
435
+ t = _p(state.workspace_root, "approved_plan_pick")
436
+ raise WizardError(t["errors"]["default_not_found"])
437
+ ```
438
+
439
+ - [ ] **Step 5: `_submit_workers_override:1080` WizardError 교체**
440
+
441
+ `scripts/okstra_ctl/wizard.py` line 1080 부근:
442
+
443
+ **Before:**
444
+ ```python
445
+ if not chosen:
446
+ raise WizardError("워커를 최소 1개 선택해주세요")
447
+ ```
448
+
449
+ **After:**
450
+ ```python
451
+ if not chosen:
452
+ t = _p(state.workspace_root, "workers_override")
453
+ raise WizardError(t["errors"]["min_one_required"])
454
+ ```
455
+
456
+ - [ ] **Step 6: `confirmation_block:1717` + `:1727` 교체**
457
+
458
+ `scripts/okstra_ctl/wizard.py` line 1715-1748 부근의 `confirmation_block` 함수:
459
+
460
+ **Before:**
461
+ ```python
462
+ def confirmation_block(state: WizardState) -> str:
463
+ """Human-readable echo of the resolved selections (for the Confirm step)."""
464
+ lines: list[str] = ["선택 확인:"]
465
+ lines.append(f" task-type : {state.task_type}")
466
+ ...
467
+ if state.task_type == "implementation":
468
+ lines.append(f" executor : {state.executor or '(default)'}")
469
+ lines.append(
470
+ f" workers : (프로필 기본 — executor + verifier 2 + report-writer)"
471
+ )
472
+ else:
473
+ ...
474
+ ```
475
+
476
+ **After:**
477
+ ```python
478
+ def confirmation_block(state: WizardState) -> str:
479
+ """Human-readable echo of the resolved selections (for the Confirm step)."""
480
+ header = _msg(state.workspace_root, "confirmation", "header")
481
+ lines: list[str] = [header]
482
+ lines.append(f" task-type : {state.task_type}")
483
+ ...
484
+ if state.task_type == "implementation":
485
+ lines.append(f" executor : {state.executor or '(default)'}")
486
+ lines.append(
487
+ _msg(state.workspace_root, "confirmation", "workers_implementation_default")
488
+ )
489
+ else:
490
+ ...
491
+ ```
492
+
493
+ - [ ] **Step 7: 한글 grep 점검 (informational — 정식 게이트는 Task 4)**
494
+
495
+ ```bash
496
+ grep -nP '[가-힣]' scripts/okstra_ctl/wizard.py
497
+ ```
498
+ Expected: docstring (line ~1054-1055 `_build_workers_override`) + 주석만. 코드 라인 한글 0.
499
+
500
+ - [ ] **Step 8: 회귀 통과**
501
+
502
+ ```bash
503
+ python3 -m pytest tests/ -x
504
+ ```
505
+ Expected: 611 passed, 1 skipped.
506
+
507
+ 기존 wizard 테스트:
508
+ ```bash
509
+ python3 -m pytest tests/test_okstra_ctl_wizard.py -v 2>&1 | tail -15
510
+ ```
511
+ Expected: PASS.
512
+
513
+ - [ ] **Step 9: Commit**
514
+
515
+ ```bash
516
+ git add scripts/okstra_ctl/wizard.py
517
+ git commit -m "refactor(wizard): externalize _submit_* echo + WizardError + confirmation_block strings"
518
+ ```
519
+
520
+ No Claude trailer, no Co-Authored-By, no `🤖 Generated with` footer.
521
+
522
+ ---
523
+
524
+ ## Task 4: Korean grep gate scope 확장 + CHANGES.md + 최종 회귀
525
+
526
+ **Files:**
527
+ - Modify: `tests/test_wizard_prompts.py` — `test_no_korean_in_build_functions` 의 scope 를 `_build_*` + `_submit_*` + `confirmation_block` 까지 확장
528
+ - Modify: `CHANGES.md`
529
+
530
+ - [ ] **Step 1: grep gate scope 확장**
531
+
532
+ `tests/test_wizard_prompts.py` 의 `test_no_korean_in_build_functions` 함수 명을 `test_no_korean_in_wizard_functions` 로 변경 + 함수 범위 detect regex 확장:
533
+
534
+ **Before** (기존):
535
+ ```python
536
+ def test_no_korean_in_build_functions():
537
+ ...
538
+ for i, line in enumerate(src_lines):
539
+ m = re.match(r'^def (_build_\w+)\(', line)
540
+ ...
541
+ ```
542
+
543
+ **After:**
544
+ ```python
545
+ def test_no_korean_in_wizard_functions():
546
+ """`_build_*` / `_submit_*` / `confirmation_block` 함수의 비-주석 코드 라인에 한글 0.
547
+
548
+ Scope: user-facing 한글이 코드에 다시 들어오는 회귀 차단.
549
+ 정책: 주석/docstring 안의 한글은 허용 (정책상 외부화 대상 아님).
550
+ """
551
+ ...
552
+ SCOPED_FUNCTION_RE = re.compile(r'^def (_build_\w+|_submit_\w+|confirmation_block)\(')
553
+ for i, line in enumerate(src_lines):
554
+ m = SCOPED_FUNCTION_RE.match(line)
555
+ ...
556
+ ```
557
+
558
+ 전체 함수 body 변경 (실제 구현 시 기존 detection 로직 그대로 사용하되 regex 만 위처럼 교체):
559
+
560
+ ```python
561
+ def test_no_korean_in_wizard_functions():
562
+ """`_build_*` / `_submit_*` / `confirmation_block` 함수의 비-주석 코드 라인에 한글 0.
563
+
564
+ Scope: user-facing 한글이 코드에 다시 들어오는 회귀 차단.
565
+ 정책:
566
+ - 주석 라인 (# 시작) 안의 한글은 허용
567
+ - docstring 블록 (`\"\"\"...\"\"\"`) 안의 한글은 허용
568
+ - 그 외 user-facing UI 문자열은 `prompts/wizard/prompts.ko.json` 에 있어야 함.
569
+ """
570
+ import re
571
+
572
+ KOREAN = re.compile(r"[가-힣]")
573
+ SCOPED_FUNCTION_RE = re.compile(r'^def (_build_\w+|_submit_\w+|confirmation_block)\(')
574
+ src_lines = WIZARD_PY.read_text(encoding="utf-8").splitlines()
575
+
576
+ function_ranges: list[tuple[str, int, int]] = []
577
+ current_name: str | None = None
578
+ current_start: int | None = None
579
+ for i, line in enumerate(src_lines):
580
+ m = SCOPED_FUNCTION_RE.match(line)
581
+ if m:
582
+ if current_name is not None and current_start is not None:
583
+ function_ranges.append((current_name, current_start, i))
584
+ current_name = m.group(1)
585
+ current_start = i
586
+ continue
587
+ if current_name is not None and re.match(r'^def \w+\(|^[A-Za-z_]|^@', line):
588
+ function_ranges.append((current_name, current_start, i))
589
+ current_name = None
590
+ current_start = None
591
+ if current_name is not None and current_start is not None:
592
+ function_ranges.append((current_name, current_start, len(src_lines)))
593
+
594
+ offenders: list[str] = []
595
+ for name, start, end in function_ranges:
596
+ in_docstring = False
597
+ i = start + 1
598
+ while i < end:
599
+ line = src_lines[i]
600
+ stripped = line.strip()
601
+ if not in_docstring and (stripped.startswith('"""') or stripped.startswith("'''")):
602
+ quote = stripped[:3]
603
+ rest = stripped[3:]
604
+ if rest.endswith(quote) and len(rest) >= 3:
605
+ pass
606
+ else:
607
+ in_docstring = True
608
+ i += 1
609
+ continue
610
+ if in_docstring:
611
+ if stripped.endswith('"""') or stripped.endswith("'''"):
612
+ in_docstring = False
613
+ i += 1
614
+ continue
615
+ if stripped.startswith("#"):
616
+ i += 1
617
+ continue
618
+ code_part = line.split("#", 1)[0]
619
+ if KOREAN.search(code_part):
620
+ offenders.append(f" {name} ({WIZARD_PY.name}:{i+1}): {line.rstrip()[:120]}")
621
+ i += 1
622
+
623
+ assert not offenders, (
624
+ "wizard 함수 본문에 user-facing 한글 문자열이 남아 있음 — "
625
+ "prompts/wizard/prompts.ko.json 으로 이동 필요:\n" + "\n".join(offenders)
626
+ )
627
+ ```
628
+
629
+ - [ ] **Step 2: 게이트 실행 + 통과 확인**
630
+
631
+ ```bash
632
+ python3 -m pytest tests/test_wizard_prompts.py::test_no_korean_in_wizard_functions -v
633
+ ```
634
+ Expected: PASS (Task 3 후 wizard.py 코드 라인 한글 0).
635
+
636
+ 만약 FAIL — 메시지가 짚는 한글이 `_build_*` / `_submit_*` / `confirmation_block` 안에 잔존. 그 사이트를 JSON 으로 이동.
637
+
638
+ 전체 회귀:
639
+ ```bash
640
+ python3 -m pytest tests/ -x
641
+ ```
642
+ Expected: 611 passed, 1 skipped.
643
+
644
+ - [ ] **Step 3: CHANGES.md 항목 추가**
645
+
646
+ `CHANGES.md` 의 `## 2026-05-20` 섹션 최상단 (Phase A1 entry 위) 에:
647
+
648
+ ```markdown
649
+ ### refactor(wizard): wizard 의 모든 user-facing 한글 JSON SOT 단일화 (A1 follow-up)
650
+
651
+ - Phase A1 의 plan scope 외였던 잔존 한글 외부화: `_submit_*` echo 변형 (3개), `WizardError` 메시지 (2개), `confirmation_block` 라인 (2개) 을 `prompts/wizard/prompts.ko.json` 으로 이전.
652
+ - 스키마 확장: step entry 안에 `echo_variants` / `errors` 서브필드, 최상위 `confirmation` 섹션 추가. `schema_version` 은 1 유지 (backward compatible 추가).
653
+ - 신규 헬퍼: `_load_wizard_root(workspace_root)` (전체 root 캐시) + `_msg(workspace_root, section, key, **vars)` (top-level 섹션 lookup). 기존 `_p()` 는 echo_variants / errors 패스스루.
654
+ - 정적 게이트 scope 확장: `test_no_korean_in_wizard_functions` 가 `_build_*` + `_submit_*` + `confirmation_block` 함수 본문 모두 검사. wizard.py 코드 라인 한글 0 보장 (docstring 제외).
655
+ - 사용자 영향: 없음 (UI 동일). 컨트리뷰터 영향: wizard 의 모든 user-facing 문자열이 한 JSON 파일에. i18n 시 `prompts.<locale>.json` 하나만 추가하면 됨.
656
+ ```
657
+
658
+ - [ ] **Step 4: npm build smoke**
659
+
660
+ ```bash
661
+ npm run build
662
+ ```
663
+ Expected: 에러 없음.
664
+
665
+ 검증:
666
+ ```bash
667
+ python3 -c "import json; d=json.load(open('runtime/prompts/wizard/prompts.ko.json')); assert d['confirmation']['header'] == '선택 확인:'; print('OK')"
668
+ ```
669
+ Expected: OK.
670
+
671
+ - [ ] **Step 5: 최종 회귀 + e2e**
672
+
673
+ ```bash
674
+ python3 -m pytest tests/ -v 2>&1 | tail -10
675
+ ```
676
+ Expected: 611 passed, 1 skipped.
677
+
678
+ ```bash
679
+ bash tests-e2e/scenario-01-record-start-reconcile.sh
680
+ ```
681
+ Expected: PASS.
682
+
683
+ - [ ] **Step 6: Commit**
684
+
685
+ ```bash
686
+ git add tests/test_wizard_prompts.py CHANGES.md
687
+ git commit -m "test(wizard): extend Korean grep gate to _submit_* + confirmation_block"
688
+ ```
689
+
690
+ (CHANGES.md + gate scope 확장이 같은 PR 의 연관 작업이라 한 commit. 또는 분리해도 OK:
691
+ ```bash
692
+ git add tests/test_wizard_prompts.py
693
+ git commit -m "test(wizard): extend Korean grep gate to _submit_* + confirmation_block"
694
+ git add CHANGES.md
695
+ git commit -m "docs(changes): note wizard messages JSON SOT extension"
696
+ ```
697
+ )
698
+
699
+ No Claude trailer, no Co-Authored-By, no `🤖 Generated with` footer.
700
+
701
+ ---
702
+
703
+ ## Self-Review 체크리스트
704
+
705
+ - [x] **Spec coverage**: 7 한글 사이트 모두 Task 3 에 명시. JSON 신규 키 Task 1 에 명시. 헬퍼 (Task 2). 게이트 scope 확장 (Task 4). 모두 커버.
706
+ - [x] **Placeholder scan**: TBD/TODO 없음. 모든 step 에 실제 코드/명령/grep 패턴.
707
+ - [x] **Type consistency**: `_load_wizard_root(workspace_root: str) -> dict` / `_msg(workspace_root: str, section: str, key: str, **vars: str) -> str` / `_p()` 확장 반환 키 (`echo_variants`, `errors`) — 모든 task 에서 일관.
708
+ - [x] **Atomic migration**: Task 3 한 commit. 부분 migration 위험 없음.
709
+ - [x] **Backward compatibility**: 기존 `_load_wizard_prompts` API 보존 (steps return). schema_version 1 유지 (새 필드는 additive).
710
+
711
+ ---
712
+
713
+ ## 실행 옵션
714
+
715
+ Plan complete and saved to [docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md](docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md). 두 가지 실행 방식:
716
+
717
+ 1. **Subagent-Driven (recommended)** — task 마다 새로운 subagent + spec/code quality 2-stage review.
718
+ 2. **Inline Execution** — 본 세션에서 executing-plans 로 batch 실행.
719
+
720
+ 어떤 방식으로 진행할까요?