okstra 0.34.1 → 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 +12 -2
  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,1691 @@
1
+ # Improvement Discovery Task-Type 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:** 코드베이스 범위·lens 만 입력으로 받아 multi-worker 합의로 개선 후보 N개를 도출하는 새 okstra task-type `improvement-discovery` 도입.
6
+
7
+ **Architecture:** sidetrack entry-point (PHASE_SEQUENCE 외부). brief variant 식별자 `scope: codebase`. SSOT 모듈 1개에서 lens enum / cap 상수를 통일 — profile / brief skill / validator / wizard 가 모두 import. 양방향 grilling 두 지점 (brief Step 4 강화 budget 8 + Phase 1.5 reflect-back budget 12, 합 20). final-report `## 4. Improvement Candidates` 10-column 표. validator 가 11개 항목 강제.
8
+
9
+ **Tech Stack:** Python 3.x (pytest), Bash (e2e scenarios), Markdown (profile · template · skill · docs), 부분적으로 Node (wizard task_type 라인 1개).
10
+
11
+ **Spec:** [docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md](docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md)
12
+
13
+ **Branch:** 새 feature branch `feat/improvement-discovery` 에서 작업 (현재 `feat/report-language` 는 다른 in-flight feature).
14
+
15
+ ---
16
+
17
+ ## File Structure
18
+
19
+ | 경로 | 책임 | 신규/수정 |
20
+ |---|---|---|
21
+ | `scripts/okstra_ctl/improvement_lenses.py` | lens enum SSOT + cap 상수 (DEFAULT 8, ABSOLUTE 12, MIN 1, MAX 4) + SOURCE_WORKERS | 신규 |
22
+ | `scripts/okstra_ctl/workflow.py` | `PHASE_RULES["improvement-discovery"]` 및 `DEFAULT_NEXT_PHASE` 항목 추가 | 수정 |
23
+ | `scripts/okstra_ctl/wizard.py` | `TASK_TYPES` 에 1행 추가 | 수정 |
24
+ | `prompts/profiles/improvement-discovery.md` | 영문 profile (Phase 1.5 grilling 포함) | 신규 |
25
+ | `prompts/profiles/kr/improvement-discovery.md` | 한글 페어 | 신규 |
26
+ | `templates/reports/improvement-discovery-input.template.md` | 새 input 템플릿 + Phase 1.5 grilling 안내 + 빈 Improvement Candidates 표 | 신규 |
27
+ | `templates/reports/final-report.template.md` | `## 4.` 슬롯 phase 분기 안내에 improvement-discovery 케이스 추가 | 수정 |
28
+ | `agents/workers/claude-worker.md` · `codex-worker.md` · `gemini-worker.md` · `report-writer-worker.md` | Phase 1.5 grilling log 를 정답으로 사용하라는 한 줄 규칙 추가 | 수정 |
29
+ | `validators/validate-improvement-report.py` | 11개 항목 강제 | 신규 |
30
+ | `validators/validate-run.py` | `task_type == "improvement-discovery"` 분기 호출 | 수정 |
31
+ | `tests/test_okstra_improvement_lenses.py` | SSOT import 일관성 검증 | 신규 |
32
+ | `tests/test_validate_improvement_report.py` | validator 11항목 happy + sad path | 신규 |
33
+ | `tests/test_workflow_phase_sequence.py` | PHASE_SEQUENCE 에 없음 + PHASE_RULES 에 있음 검증 | 수정 |
34
+ | `tests/test_okstra_ctl_wizard.py` | TASK_TYPES 7개 검증 | 수정 |
35
+ | `tests-e2e/scenario-08-improvement-discovery-render-only.sh` | render-only 흐름 e2e | 신규 |
36
+ | `skills/okstra-brief/SKILL.md` | Step 1 2-level pick · Step 4 강화 rules 5개 · Step 5 frontmatter · Step 6 헤리스틱 | 수정 |
37
+ | `README.md` · `README.kr.md` · `docs/kr/cli.md` · `docs/kr/architecture.md` · `docs/project-structure-overview.md` · `CLAUDE.md` | improvement-discovery 행/절 추가 | 수정 |
38
+ | `CHANGES.md` | 한국어 entry + `사용자 영향:` 한 줄 | 수정 |
39
+
40
+ ---
41
+
42
+ # Milestone 1 — Phase 기반 (PR 1)
43
+
44
+ phase 자체가 render-only 로 작동 가능한 상태까지. validator 는 stub (항상 통과). brief skill 변경 없음.
45
+
46
+ ## Task 1: SSOT 모듈 작성
47
+
48
+ **Files:**
49
+ - Create: `scripts/okstra_ctl/improvement_lenses.py`
50
+ - Create: `tests/test_okstra_improvement_lenses.py`
51
+
52
+ - [ ] **Step 1: 실패하는 테스트 작성**
53
+
54
+ ```python
55
+ # tests/test_okstra_improvement_lenses.py
56
+ from scripts.okstra_ctl.improvement_lenses import (
57
+ LENSES,
58
+ DEFAULT_CANDIDATE_CAP,
59
+ ABSOLUTE_CANDIDATE_CAP,
60
+ MIN_PRIORITY_LENSES,
61
+ MAX_PRIORITY_LENSES,
62
+ SOURCE_WORKERS,
63
+ is_valid_lens,
64
+ is_valid_lens_subset,
65
+ is_within_candidate_cap,
66
+ )
67
+
68
+
69
+ def test_lenses_enum_is_eight_canonical_values():
70
+ assert LENSES == (
71
+ "performance",
72
+ "security",
73
+ "readability",
74
+ "architecture",
75
+ "test-coverage",
76
+ "dx",
77
+ "observability",
78
+ "accessibility",
79
+ )
80
+
81
+
82
+ def test_candidate_caps():
83
+ assert DEFAULT_CANDIDATE_CAP == 8
84
+ assert ABSOLUTE_CANDIDATE_CAP == 12
85
+ assert MIN_PRIORITY_LENSES == 1
86
+ assert MAX_PRIORITY_LENSES == 4
87
+
88
+
89
+ def test_source_workers_excludes_report_writer():
90
+ assert SOURCE_WORKERS == ("claude", "codex", "gemini")
91
+ assert "report-writer" not in SOURCE_WORKERS
92
+
93
+
94
+ def test_is_valid_lens():
95
+ assert is_valid_lens("performance")
96
+ assert not is_valid_lens("perf")
97
+ assert not is_valid_lens("")
98
+
99
+
100
+ def test_is_valid_lens_subset_size_bounds():
101
+ assert is_valid_lens_subset(["performance"])
102
+ assert is_valid_lens_subset(["performance", "security", "dx", "observability"])
103
+ assert not is_valid_lens_subset([])
104
+ assert not is_valid_lens_subset(["performance"] * 5)
105
+ assert not is_valid_lens_subset(["performance", "bogus"])
106
+
107
+
108
+ def test_is_within_candidate_cap():
109
+ assert is_within_candidate_cap(1, brief_cap=None)
110
+ assert is_within_candidate_cap(8, brief_cap=None)
111
+ assert not is_within_candidate_cap(9, brief_cap=None)
112
+ assert is_within_candidate_cap(12, brief_cap=12)
113
+ assert not is_within_candidate_cap(13, brief_cap=12)
114
+ assert not is_within_candidate_cap(15, brief_cap=20)
115
+ ```
116
+
117
+ - [ ] **Step 2: 테스트 실행 → fail**
118
+
119
+ Run: `python3 -m pytest tests/test_okstra_improvement_lenses.py -v`
120
+ Expected: `ModuleNotFoundError: No module named 'scripts.okstra_ctl.improvement_lenses'`
121
+
122
+ - [ ] **Step 3: SSOT 모듈 구현**
123
+
124
+ ```python
125
+ # scripts/okstra_ctl/improvement_lenses.py
126
+ """Improvement-discovery phase SSOT: lens enum and cap constants.
127
+
128
+ profile / brief skill / validator / wizard MUST import from this module.
129
+ Re-defining the enum anywhere else violates the single-reference-point rule
130
+ and is rejected by tests/test_okstra_improvement_lenses.py.
131
+ """
132
+ from __future__ import annotations
133
+
134
+ LENSES: tuple[str, ...] = (
135
+ "performance",
136
+ "security",
137
+ "readability",
138
+ "architecture",
139
+ "test-coverage",
140
+ "dx",
141
+ "observability",
142
+ "accessibility",
143
+ )
144
+
145
+ DEFAULT_CANDIDATE_CAP = 8
146
+ ABSOLUTE_CANDIDATE_CAP = 12
147
+ MIN_PRIORITY_LENSES = 1
148
+ MAX_PRIORITY_LENSES = 4
149
+
150
+ # report-writer is the AUTHOR of the final report; it never produces source
151
+ # findings. validators reject `report-writer:<id>` entries in Source workers.
152
+ SOURCE_WORKERS: tuple[str, ...] = ("claude", "codex", "gemini")
153
+
154
+
155
+ def is_valid_lens(value: str) -> bool:
156
+ return value in LENSES
157
+
158
+
159
+ def is_valid_lens_subset(values: list[str]) -> bool:
160
+ if not (MIN_PRIORITY_LENSES <= len(values) <= MAX_PRIORITY_LENSES):
161
+ return False
162
+ return all(v in LENSES for v in values)
163
+
164
+
165
+ def is_within_candidate_cap(count: int, *, brief_cap: int | None) -> bool:
166
+ cap = brief_cap if brief_cap is not None else DEFAULT_CANDIDATE_CAP
167
+ if cap < 1 or cap > ABSOLUTE_CANDIDATE_CAP:
168
+ return False
169
+ return 1 <= count <= cap
170
+ ```
171
+
172
+ - [ ] **Step 4: 테스트 실행 → pass**
173
+
174
+ Run: `python3 -m pytest tests/test_okstra_improvement_lenses.py -v`
175
+ Expected: 6 passed.
176
+
177
+ - [ ] **Step 5: commit**
178
+
179
+ ```bash
180
+ git add scripts/okstra_ctl/improvement_lenses.py tests/test_okstra_improvement_lenses.py
181
+ git commit -m "feat(scripts/okstra_ctl): improvement-discovery lens SSOT and helpers"
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Task 2: workflow.py 의 PHASE_RULES 등록
187
+
188
+ **Files:**
189
+ - Modify: `scripts/okstra_ctl/workflow.py:12-31` (PHASE_SEQUENCE / DEFAULT_NEXT_PHASE)
190
+ - Modify: `scripts/okstra_ctl/workflow.py:36-149` (PHASE_RULES dict)
191
+ - Modify: `tests/test_workflow_phase_sequence.py`
192
+
193
+ - [ ] **Step 1: 실패하는 테스트 추가 (test_workflow_phase_sequence.py)**
194
+
195
+ 이 파일에 다음 두 테스트를 추가:
196
+
197
+ ```python
198
+ def test_improvement_discovery_is_sidetrack_entry_point():
199
+ from scripts.okstra_ctl.workflow import (
200
+ PHASE_SEQUENCE, DEFAULT_NEXT_PHASE, PHASE_RULES,
201
+ )
202
+ assert "improvement-discovery" not in PHASE_SEQUENCE
203
+ assert DEFAULT_NEXT_PHASE["improvement-discovery"] == "pending-routing-decision"
204
+ assert "improvement-discovery" in PHASE_RULES
205
+
206
+
207
+ def test_improvement_discovery_phase_rules_shape():
208
+ from scripts.okstra_ctl.workflow import PHASE_RULES
209
+ rules = PHASE_RULES["improvement-discovery"]
210
+ assert "allowed" in rules and "forbidden" in rules
211
+ assert "lens" in rules["allowed"]
212
+ assert "Phase 1.5" in rules["allowed"]
213
+ assert "source code edits" in rules["forbidden"]
214
+ assert "implementation-planning" in rules["forbidden"]
215
+ ```
216
+
217
+ - [ ] **Step 2: 테스트 실행 → fail**
218
+
219
+ Run: `python3 -m pytest tests/test_workflow_phase_sequence.py -v -k improvement`
220
+ Expected: 2 failed (KeyError on `DEFAULT_NEXT_PHASE['improvement-discovery']`).
221
+
222
+ - [ ] **Step 3: workflow.py 수정**
223
+
224
+ `DEFAULT_NEXT_PHASE` dict (line ~21) 에 항목 추가:
225
+
226
+ ```python
227
+ DEFAULT_NEXT_PHASE = {
228
+ "requirements-discovery": "pending-routing-decision",
229
+ "improvement-discovery": "pending-routing-decision",
230
+ "error-analysis": "implementation-planning",
231
+ "implementation-planning": "implementation",
232
+ "implementation": "final-verification",
233
+ "final-verification": "pending-release-handoff",
234
+ "release-handoff": "done-or-follow-up",
235
+ }
236
+ ```
237
+
238
+ `PHASE_RULES` dict 의 `requirements-discovery` 항목 바로 뒤에 다음 항목을 삽입:
239
+
240
+ ```python
241
+ "improvement-discovery": {
242
+ "allowed": (
243
+ " - improvement candidate discovery within the lens whitelist defined in `scripts/okstra_ctl/improvement_lenses.py`\n"
244
+ " - top-N ranking (default 8, brief `candidate-cap` overrides within 1..12)\n"
245
+ " - per-candidate columns Lens / Scope / Severity / Effort / Consensus / Source workers / Recommended next-phase / Evidence (path:line)\n"
246
+ " - Phase 1.5 reflect-back grilling log at `runs/improvement-discovery/<seq>/state/phase-1.5-grilling.md`\n"
247
+ " - Phase 5.5 consensus classification (full / partial / contested / worker-unique)"
248
+ ),
249
+ "forbidden": (
250
+ " - source code edits of any kind\n"
251
+ " - implementation planning, root-cause analysis, builds, migrations, or deployments\n"
252
+ " - starting `implementation-planning`, `implementation`, `error-analysis`, or any other lifecycle phase inside this run\n"
253
+ " - generating candidates outside the lens whitelist (Lens enum violation rejects the report)\n"
254
+ " - exceeding the candidate cap (absolute cap 12)\n"
255
+ " - free external data fetch beyond the brief's Source Material or Phase 1.5 resolved scope\n"
256
+ " - interpreting user phrases like `다음 단계 진행해` as authorisation to enter another phase"
257
+ ),
258
+ },
259
+ ```
260
+
261
+ - [ ] **Step 4: 테스트 실행 → pass**
262
+
263
+ Run: `python3 -m pytest tests/test_workflow_phase_sequence.py -v`
264
+ Expected: 모든 테스트 pass (기존 + 신규 2개).
265
+
266
+ - [ ] **Step 5: commit**
267
+
268
+ ```bash
269
+ git add scripts/okstra_ctl/workflow.py tests/test_workflow_phase_sequence.py
270
+ git commit -m "feat(scripts/okstra_ctl): register improvement-discovery in workflow PHASE_RULES"
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Task 3: wizard.py 의 TASK_TYPES 등록
276
+
277
+ **Files:**
278
+ - Modify: `scripts/okstra_ctl/wizard.py:58-66` (TASK_TYPES 상수)
279
+ - Modify: `tests/test_okstra_ctl_wizard.py`
280
+
281
+ - [ ] **Step 1: 실패하는 테스트 추가**
282
+
283
+ ```python
284
+ def test_task_types_includes_improvement_discovery():
285
+ from scripts.okstra_ctl.wizard import TASK_TYPES, TASK_TYPE_VALUES
286
+ assert "improvement-discovery" in TASK_TYPE_VALUES
287
+ label_by_value = dict(TASK_TYPES)
288
+ assert "codebase scope" in label_by_value["improvement-discovery"].lower()
289
+ ```
290
+
291
+ - [ ] **Step 2: 테스트 실행 → fail**
292
+
293
+ Run: `python3 -m pytest tests/test_okstra_ctl_wizard.py -v -k improvement_discovery`
294
+ Expected: `assert "improvement-discovery" in TASK_TYPE_VALUES` AssertionError.
295
+
296
+ - [ ] **Step 3: wizard.py 의 TASK_TYPES 에 항목 추가**
297
+
298
+ `TASK_TYPES` 리스트 (line ~58) 의 `requirements-discovery` 행 바로 뒤에 한 줄 삽입:
299
+
300
+ ```python
301
+ TASK_TYPES: list[tuple[str, str]] = [
302
+ ("requirements-discovery", "Classify request and route to next safe phase"),
303
+ ("improvement-discovery", "Find improvement candidates within a codebase scope and lens whitelist"),
304
+ ("error-analysis", "Evidence-based root-cause analysis (no code changes)"),
305
+ ("implementation-planning", "Plan options + request user approval"),
306
+ ("implementation", "Execute approved plan (requires approved final-report)"),
307
+ ("final-verification", "Acceptance + residual-risk review"),
308
+ ("release-handoff", "Drive commit/push/PR — reuse the implementation task-key (new keys fail the empty-commits gate)"),
309
+ ]
310
+ ```
311
+
312
+ - [ ] **Step 4: 테스트 실행 → pass**
313
+
314
+ Run: `python3 -m pytest tests/test_okstra_ctl_wizard.py -v`
315
+ Expected: 모든 wizard 테스트 pass.
316
+
317
+ - [ ] **Step 5: commit**
318
+
319
+ ```bash
320
+ git add scripts/okstra_ctl/wizard.py tests/test_okstra_ctl_wizard.py
321
+ git commit -m "feat(scripts/okstra_ctl): expose improvement-discovery in wizard TASK_TYPES"
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Task 4: profile 파일 작성 (en + kr)
327
+
328
+ **Files:**
329
+ - Create: `prompts/profiles/improvement-discovery.md`
330
+ - Create: `prompts/profiles/kr/improvement-discovery.md`
331
+
332
+ - [ ] **Step 1: 영문 profile 작성 (improvement-discovery.md)**
333
+
334
+ ```markdown
335
+ # Improvement Discovery Profile
336
+
337
+ - Purpose: scan a codebase scope through a fixed lens whitelist and surface ranked improvement candidates with multi-worker consensus classification
338
+ - Required workers:
339
+ - claude
340
+ - codex
341
+ - gemini
342
+ - report-writer
343
+ - Optional workers (opt-in via `--workers`):
344
+ - none — every required worker stays required because lens diversity is the load-bearing value of this phase
345
+ {{INCLUDE:_common-contract.md}}
346
+ - Brief consumption (phase-specific addendum — shared rules live in `_common-contract.md` under "Brief handoff contract"):
347
+ - this phase REQUIRES a codebase-scan brief whose frontmatter contains `scope: codebase`. A brief without that marker is rejected before worker dispatch.
348
+ - the brief's `priority-lenses` MUST be a non-empty subset (size 1..4) of the lens whitelist defined in `scripts/okstra_ctl/improvement_lenses.py`. Lenses outside the whitelist are rejected.
349
+ - the brief's `scan-scope` defines the only paths workers may read for candidate evidence. `out-of-scope` paths MUST be ignored even when the codebase is otherwise reachable.
350
+ - the brief's `candidate-cap` (default 8 if absent, absolute cap 12) bounds the number of rows in `## 4. Improvement Candidates`.
351
+ - Apply the shared reporter-confirmation precondition as written. For this phase any unresolved `intent-check:` / `conversion-block:` row uses `Blocks=next-phase`.
352
+ - Primary focus areas:
353
+ - candidate discovery within the lens whitelist
354
+ - per-candidate evidence (path:line) and scope mapping
355
+ - per-candidate severity / effort / recommended-next-phase
356
+ - convergence classification (full / partial / contested / worker-unique) across workers
357
+ - Phase 1.5 — Lead reflect-back grilling (runs after Phase 1 context loading and before Phase 4 worker dispatch):
358
+ - Lead inspects scan-scope paths via `ls` / `Grep` / `Read` to map modules, entry points, dependencies, approximate LOC, and recent commit patterns.
359
+ - Lead emits a single reflect-back message covering: (a) understood scope per path (one-line summary), (b) understood meaning of each priority lens in this scope, (c) understood out-of-scope rationale, (d) ordered list of N open questions.
360
+ - For each open question Lead asks ONE `AskUserQuestion` with a `(Recommended)` answer drawn from a codebase-first inspection. Budget: at most 12 questions in this phase.
361
+ - Stop conditions (OR): all questions resolved / budget exhausted / user signals proceed.
362
+ - Lead persists the round at `runs/improvement-discovery/<seq>/state/phase-1.5-grilling.md` with one section per question (question / recommended / user answer) and a closing `Resolved scope` / `Resolved lenses` block. Worker prompts use this resolved block as the authoritative scope and lens definition.
363
+ - Decision-tree walk (bounded):
364
+ - When candidates branch on a structural question (e.g. "is module X meant to own this responsibility?"), resolve via `Read` / `Grep` first. Only escalate to the user inside the Phase 1.5 budget.
365
+ - Expected output emphasis:
366
+ - the `## 4. Improvement Candidates` table populated with rows that obey the 10-column schema from `validators/validate-improvement-report.py` (Cand ID `I-NNN`, Lens from whitelist, Title, Scope ⊆ scan-scope, Severity, Effort, Consensus, Source workers `<worker>:<id>` from {claude, codex, gemini}, Recommended next-phase ∈ {requirements-discovery, implementation-planning, error-analysis}, Evidence as path:line list)
367
+ - `## 2. Final Verdict` Verdict Token ∈ {`candidates-ready`, `no-candidates`, `blocked`}; Direction `routing`; Next Step "사용자에게 후보 K개 선택 의뢰 (## 4. 표 참조)"
368
+ - `## 6. Recommended Next Steps` first entry summarises per-candidate routing and proposes new task-key names of the form `<task-group>/imp-<Cand-ID>`
369
+ - Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
370
+ - if scan-scope or priority-lenses cannot be made concrete during Phase 1.5, end the run with Verdict Token `blocked`, populate `## 5. Clarification Items` with `Blocks=next-phase` rows, and do not run worker dispatch
371
+ - every clarification row carries a recommended answer + one-line rationale inside the `Expected form` cell
372
+ - Non-goals:
373
+ - concrete implementation plans, cost estimates, or code edits for any candidate
374
+ - inventing lenses outside the whitelist
375
+ - acting as a final-verification quality gate — this phase is discovery, not acceptance
376
+ - silently merging out-of-scope findings into in-scope candidates
377
+ ```
378
+
379
+ - [ ] **Step 2: 한글 profile 작성 (kr/improvement-discovery.md)**
380
+
381
+ ```markdown
382
+ # 개선 발견 프로파일
383
+
384
+ - 목적: 코드베이스 범위를 고정 lens 화이트리스트로 스캔해 multi-worker 합의 기반의 랭킹된 개선 후보를 도출
385
+ - 필수 워커:
386
+ - claude
387
+ - codex
388
+ - gemini
389
+ - report-writer
390
+ - 선택 워커 (`--workers` 로 opt-in):
391
+ - 없음 — lens 다양성이 이 phase 의 핵심 가치이므로 모두 필수
392
+ {{INCLUDE:_common-contract.md}}
393
+ - Brief 소비 (phase 별 부록 — 공통 규칙은 `_common-contract.md` 의 "Brief handoff contract" 참조):
394
+ - 이 phase 는 frontmatter 에 `scope: codebase` 마커가 있는 codebase-scan brief 를 요구한다. 마커가 없는 brief 는 worker dispatch 전에 거부된다.
395
+ - `priority-lenses` 는 `scripts/okstra_ctl/improvement_lenses.py` 의 lens 화이트리스트의 부분집합 (크기 1..4) 이어야 한다. 화이트리스트 밖 lens 는 거부.
396
+ - `scan-scope` 는 worker 가 후보 근거를 읽기 위해 접근할 수 있는 유일한 경로 집합이다. `out-of-scope` 경로는 코드베이스에서 접근 가능하더라도 무시한다.
397
+ - `candidate-cap` (없으면 기본 8, 절대 cap 12) 가 `## 4. Improvement Candidates` 행 개수를 제한한다.
398
+ - 공통 reporter-confirmation precondition 을 그대로 적용. 이 phase 에서 미해소 `intent-check:` / `conversion-block:` 행은 `Blocks=next-phase`.
399
+ - 주요 집중 영역:
400
+ - lens 화이트리스트 안의 후보 발굴
401
+ - 후보별 근거 (path:line) 와 scope 매핑
402
+ - 후보별 severity / effort / 권장 next-phase
403
+ - worker 간 convergence 분류 (full / partial / contested / worker-unique)
404
+ - Phase 1.5 — Lead reflect-back grilling (Phase 1 context loading 직후, Phase 4 worker dispatch 직전 실행):
405
+ - Lead 가 `ls` / `Grep` / `Read` 로 scan-scope path 를 1차 훑어 모듈 / 엔트리포인트 / 의존성 / 대략 LOC / 최근 commit 패턴 파악.
406
+ - Lead 가 한 메시지로 reflect-back: (a) path 별 한 줄 scope 요약, (b) 각 priority lens 의 이 scope 에서의 구체적 의미, (c) out-of-scope 근거, (d) 우선순위순 의문점 N개.
407
+ - 각 의문점마다 `(Recommended)` 답을 codebase-first 1차 조사로 도출한 뒤 `AskUserQuestion` 한 개. budget: 이 phase 에서 최대 12개.
408
+ - stop conditions (OR): 모든 의문점 resolved / budget 소진 / 사용자가 proceed 명시.
409
+ - Lead 가 `runs/improvement-discovery/<seq>/state/phase-1.5-grilling.md` 에 round 를 영속화한다 (질문별 question / recommended / user answer 와 마지막 `Resolved scope` / `Resolved lenses` 블록). worker prompt 는 이 resolved 블록을 정답으로 사용한다.
410
+ - 결정 트리 (bounded):
411
+ - 후보가 구조적 질문에서 분기할 때 `Read` / `Grep` 로 먼저 풀고, 그래도 안 풀리면 Phase 1.5 budget 안에서 사용자에게 escalation.
412
+ - 예상 출력 강조점:
413
+ - `## 4. Improvement Candidates` 표가 `validators/validate-improvement-report.py` 의 10-column schema 를 정확히 따른다 (Cand ID `I-NNN`, Lens 화이트리스트 안, Title, Scope ⊆ scan-scope, Severity, Effort, Consensus, Source workers `<worker>:<id>` from {claude, codex, gemini}, Recommended next-phase ∈ {requirements-discovery, implementation-planning, error-analysis}, Evidence path:line 리스트)
414
+ - `## 2. Final Verdict` Verdict Token ∈ {`candidates-ready`, `no-candidates`, `blocked`}, Direction `routing`, Next Step "사용자에게 후보 K개 선택 의뢰 (## 4. 표 참조)"
415
+ - `## 6. Recommended Next Steps` 첫 entry 가 후보별 라우팅 요약 + 새 task-key 작명 가이드 (`<task-group>/imp-<Cand-ID>`)
416
+ - Clarification 요청 정책 (phase 별 부록 — 공통 정책은 `_common-contract.md` 참조):
417
+ - Phase 1.5 동안 scan-scope 나 priority-lenses 를 구체화할 수 없으면 Verdict Token `blocked` 로 종료, `## 5. Clarification Items` 에 `Blocks=next-phase` 행 채움, worker dispatch 미실행
418
+ - 모든 clarification row 는 `Expected form` cell 에 추천 답 + 한 줄 근거 포함
419
+ - 비목표:
420
+ - 후보의 구체적 implementation plan / 비용 추정 / 코드 수정
421
+ - 화이트리스트 밖 lens 발명
422
+ - final-verification 의 품질 게이트로 동작 — 이 phase 는 발견이지 승인 단계가 아님
423
+ - out-of-scope finding 을 in-scope 후보로 슬며시 병합
424
+ ```
425
+
426
+ - [ ] **Step 3: profile 가 prepare_task_bundle 의 profile 로딩에 인식되는지 smoke 확인**
427
+
428
+ Run:
429
+ ```bash
430
+ python3 -c "
431
+ from pathlib import Path
432
+ from scripts.okstra_ctl.wizard import _profile_path, _load_profile_workers
433
+ ws = Path('.').resolve()
434
+ p = _profile_path(ws, 'improvement-discovery')
435
+ print('profile path:', p)
436
+ print('exists:', p.exists())
437
+ print('workers:', _load_profile_workers(ws, 'improvement-discovery'))
438
+ "
439
+ ```
440
+
441
+ Expected: `exists: True` + `workers: ['claude', 'codex', 'gemini', 'report-writer']`.
442
+
443
+ - [ ] **Step 4: commit**
444
+
445
+ ```bash
446
+ git add prompts/profiles/improvement-discovery.md prompts/profiles/kr/improvement-discovery.md
447
+ git commit -m "feat(prompts/profiles): improvement-discovery profile en and kr"
448
+ ```
449
+
450
+ ---
451
+
452
+ ## Task 5: input 템플릿 작성
453
+
454
+ **Files:**
455
+ - Create: `templates/reports/improvement-discovery-input.template.md`
456
+ - Modify: `templates/reports/final-report.template.md` (## 4. 슬롯 분기 안내)
457
+
458
+ - [ ] **Step 1: input 템플릿 작성**
459
+
460
+ ```markdown
461
+ ---
462
+ title: OKSTRA Improvement Discovery Input - {{TASK_KEY}}
463
+ id: {{FM_ID}}
464
+ tags: {{FM_TAGS}}
465
+ status: ready-for-agent
466
+ aliases: {{FM_ALIASES}}
467
+ date: {{TASK_DATE}}
468
+ task-id: "{{TASK_ID}}"
469
+ task-group: "{{TASK_GROUP}}"
470
+ project-id: "{{PROJECT_ID}}"
471
+ taskType: "{{FM_TASK_TYPE}}"
472
+ ---
473
+
474
+ # OKSTRA Improvement Discovery Input
475
+
476
+ ## Identity
477
+
478
+ - Project ID:
479
+ - Task Group:
480
+ - Task ID:
481
+ - Related Tasks:
482
+ - Issue / Ticket:
483
+ - 값이 비면 워커는 `Task ID` 로 폴백한다.
484
+ - Task Type: `improvement-discovery`
485
+ - Requested Outcome:
486
+
487
+ ## Scope (from brief frontmatter)
488
+
489
+ - scan-scope:
490
+ - out-of-scope:
491
+ - priority-lenses:
492
+ - candidate-cap (1..12, default 8):
493
+
494
+ ## Context
495
+
496
+ - Why this scope is being scanned now:
497
+ - Recent change context (last N commits to scan-scope):
498
+ - Stakeholders or owners of the scope:
499
+
500
+ ## Desired Outcome
501
+
502
+ - What kinds of improvements do you want surfaced?
503
+ - Anti-goals (improvements you do NOT want this run to propose):
504
+
505
+ ## Constraints
506
+
507
+ - Untouchable areas:
508
+ - Compatibility / deadline constraints:
509
+ - Performance / regression budget (if applicable):
510
+
511
+ ## Phase 1.5 — Lead Reflect-Back Grilling
512
+
513
+ This section is filled in by the lead during Phase 1.5 before worker dispatch.
514
+ Workers MUST read the resolved values from `runs/improvement-discovery/<seq>/state/phase-1.5-grilling.md`
515
+ rather than the unresolved brief.
516
+
517
+ - Reflect-back summary:
518
+ - Open questions (Q1..QN):
519
+ - Resolved scope:
520
+ - Resolved lenses:
521
+
522
+ ## Improvement Candidates (workers populate this)
523
+
524
+ | Cand ID | Lens | Title | Scope | Severity | Effort | Consensus | Source workers | Recommended next-phase | Evidence |
525
+ |---------|------|-------|-------|----------|--------|-----------|----------------|------------------------|----------|
526
+
527
+ ## Questions for Analysers
528
+
529
+ 1. Within the resolved scope and priority lenses, what are the highest-impact improvement candidates?
530
+ 2. Which candidates have full cross-worker consensus, and which are worker-unique?
531
+ 3. For each candidate, what is the safest next phase (requirements-discovery / implementation-planning / error-analysis)?
532
+ 4. Which candidates would you intentionally exclude despite being technically valid, and why?
533
+ 5. Are there any signals that the scope itself is mis-defined (and should be re-narrowed before discovery proceeds)?
534
+
535
+ ## Conversion Note
536
+
537
+ - Each candidate the user picks becomes a new okstra task. Suggested task-key: `<task-group>/imp-<Cand-ID>`.
538
+ - The candidate row's Recommended next-phase determines which `--task-type` to launch with.
539
+ ```
540
+
541
+ - [ ] **Step 2: final-report.template.md 의 `## 4.` 슬롯 분기 안내에 improvement-discovery 항목 추가**
542
+
543
+ `templates/reports/final-report.template.md` 의 `## 4.` 섹션 위 안내 주석을 찾는다 (phase-specific table 의 분기 설명). 그 분기 리스트에 한 줄 삽입:
544
+
545
+ ```markdown
546
+ - `improvement-discovery` → `## 4. Improvement Candidates` 10-column table (Cand ID / Lens / Title / Scope / Severity / Effort / Consensus / Source workers / Recommended next-phase / Evidence). Row count bounded by brief's `candidate-cap` (default 8, absolute max 12).
547
+ ```
548
+
549
+ - [ ] **Step 3: smoke — input 템플릿 placeholder 가 다른 template 들과 동일 syntax 인지 확인**
550
+
551
+ Run: `grep -c "{{TASK_KEY}}\|{{FM_ID}}\|{{TASK_GROUP}}" templates/reports/improvement-discovery-input.template.md`
552
+ Expected: ≥3 (모든 placeholder 가 mustache 패턴).
553
+
554
+ - [ ] **Step 4: commit**
555
+
556
+ ```bash
557
+ git add templates/reports/improvement-discovery-input.template.md templates/reports/final-report.template.md
558
+ git commit -m "feat(templates/reports): improvement-discovery input template and ## 4. slot branch"
559
+ ```
560
+
561
+ ---
562
+
563
+ ## Task 6: worker agent 1줄 추가 (4개 파일)
564
+
565
+ **Files:**
566
+ - Modify: `agents/workers/claude-worker.md`
567
+ - Modify: `agents/workers/codex-worker.md`
568
+ - Modify: `agents/workers/gemini-worker.md`
569
+ - Modify: `agents/workers/report-writer-worker.md`
570
+
571
+ 각 워커 파일에 phase 별 분기 안내가 있는 절을 찾아 (예: "Execution Rules" 섹션 끝) 다음 한 줄을 추가한다.
572
+
573
+ - [ ] **Step 1: claude-worker.md / codex-worker.md / gemini-worker.md 에 한 줄 추가**
574
+
575
+ 3개 파일 각각의 `## Execution Rules` 절 끝 (rule 번호 마지막 항목 아래) 에 다음 한 줄을 추가. 절 이름이 다르면 (`Execution rules`, `Operating rules` 등) 가장 가까운 규칙 절 끝.
576
+
577
+ ```markdown
578
+ - When `Task Type` is `improvement-discovery`, the lead's Phase 1.5 reflect-back log at `runs/improvement-discovery/<seq>/state/phase-1.5-grilling.md` is the authoritative scope and lens definition. Read its `Resolved scope` and `Resolved lenses` blocks and do NOT re-interpret the brief's raw `scan-scope` / `priority-lenses` fields. Findings that violate the resolved lens whitelist or scope are rejected by `validators/validate-improvement-report.py`.
579
+ ```
580
+
581
+ - [ ] **Step 2: report-writer-worker.md 에 한 줄 추가**
582
+
583
+ report-writer 의 "Writing rules" 또는 동등한 섹션 끝에:
584
+
585
+ ```markdown
586
+ - When the `Task Type` is `improvement-discovery`, populate `## 4. Improvement Candidates` with the 10-column schema enforced by `validators/validate-improvement-report.py`. Source the row IDs (`I-NNN`), lens whitelist, and Source workers patterns from `scripts/okstra_ctl/improvement_lenses.py` — do NOT introduce new lens names or worker prefixes.
587
+ ```
588
+
589
+ - [ ] **Step 3: 4개 파일 모두에서 새 줄이 정확히 들어갔는지 확인**
590
+
591
+ Run: `grep -l "improvement-discovery" agents/workers/*.md | wc -l`
592
+ Expected: `4`.
593
+
594
+ - [ ] **Step 4: commit**
595
+
596
+ ```bash
597
+ git add agents/workers/claude-worker.md agents/workers/codex-worker.md agents/workers/gemini-worker.md agents/workers/report-writer-worker.md
598
+ git commit -m "feat(agents/workers): point workers at Phase 1.5 grilling log for improvement-discovery"
599
+ ```
600
+
601
+ ---
602
+
603
+ ## Milestone 1 — 마무리 검증
604
+
605
+ - [ ] **빌드 + 전체 테스트 통과 확인**
606
+
607
+ Run:
608
+ ```bash
609
+ npm run build && python3 -m pytest tests/ -x
610
+ ```
611
+ Expected: 전체 통과. `runtime/` 디렉토리가 새 profile / 템플릿 / SSOT 모듈을 포함.
612
+
613
+ - [ ] **PR 1 생성 (현재 push 만 — PR 생성은 사용자 명시 요청 시)**
614
+
615
+ ```bash
616
+ git push -u origin feat/improvement-discovery
617
+ ```
618
+ 사용자 승인 시 `gh pr create` (이 plan 외부).
619
+
620
+ ---
621
+
622
+ # Milestone 2 — Validator + 테스트 + 문서 (PR 2)
623
+
624
+ PR 1 머지 후 시작. enforcement 완비 + 사용자 docs.
625
+
626
+ ## Task 7: validate-improvement-report.py 작성
627
+
628
+ **Files:**
629
+ - Create: `validators/validate-improvement-report.py`
630
+ - Create: `tests/test_validate_improvement_report.py`
631
+
632
+ - [ ] **Step 1: 실패하는 테스트 작성 (11항목 happy + sad)**
633
+
634
+ ```python
635
+ # tests/test_validate_improvement_report.py
636
+ import textwrap
637
+ from pathlib import Path
638
+
639
+ from validators.validate_improvement_report import (
640
+ validate_improvement_report,
641
+ ValidationResult,
642
+ )
643
+
644
+
645
+ HAPPY_BRIEF_FRONTMATTER = {
646
+ "scope": "codebase",
647
+ "priority-lenses": ["performance", "security"],
648
+ "scan-scope": ["src/foo/", "src/bar/baz.py"],
649
+ "out-of-scope": [],
650
+ "candidate-cap": 3,
651
+ }
652
+
653
+
654
+ def _happy_report_body() -> str:
655
+ return textwrap.dedent("""\
656
+ ## Verdict Card
657
+
658
+ | Verdict Token | Direction | Next Step |
659
+ |---------------|-----------|-----------|
660
+ | candidates-ready | routing | 사용자에게 후보 K개 선택 의뢰 (## 4. 표 참조) |
661
+
662
+ ## 1. Consensus / Differences
663
+
664
+ (omitted in this fixture)
665
+
666
+ ## 2. Final Verdict
667
+
668
+ | Verdict Token | Direction | Next Step |
669
+ |---------------|-----------|-----------|
670
+ | candidates-ready | routing | 사용자에게 후보 K개 선택 의뢰 (## 4. 표 참조) |
671
+
672
+ ## 3. Primary Evidence
673
+
674
+ - [E-001] src/foo/handler.py:42 — N+1 query in request loop
675
+
676
+ ## 4. Improvement Candidates
677
+
678
+ | Cand ID | Lens | Title | Scope | Severity | Effort | Consensus | Source workers | Recommended next-phase | Evidence |
679
+ |---------|------|-------|-------|----------|--------|-----------|----------------|------------------------|----------|
680
+ | I-001 | performance | Eliminate N+1 in request loop | src/foo/handler.py | high | M | full | claude:F-001, codex:1.2, gemini:F-3 | implementation-planning | src/foo/handler.py:42 |
681
+ | I-002 | security | Validate redirect target | src/bar/baz.py | medium | S | partial | claude:F-002, codex:1.5 | implementation-planning | src/bar/baz.py:18 |
682
+ | I-003 | architecture | Extract retry logic from handler | src/foo/handler.py | low | M | worker-unique | gemini:F-7 | requirements-discovery | src/foo/handler.py:120 |
683
+
684
+ ## 5. Clarification Items
685
+
686
+ | ID | Ticket ID | Kind | Statement | Expected form | Blocks | Status | User input |
687
+ |----|-----------|------|-----------|----------------|--------|--------|------------|
688
+
689
+ ## 6. Recommended Next Steps
690
+
691
+ - Candidate routing: I-001 → implementation-planning (`<task-group>/imp-I-001`); I-002 → implementation-planning; I-003 → requirements-discovery
692
+ """)
693
+
694
+
695
+ def _write_grilling_log(tmp_path: Path) -> Path:
696
+ state = tmp_path / "state"
697
+ state.mkdir()
698
+ log = state / "phase-1.5-grilling.md"
699
+ log.write_text(textwrap.dedent("""\
700
+ # Phase 1.5 Reflect-Back Grilling
701
+
702
+ ## Resolved scope
703
+
704
+ - src/foo/: 12 files, 1240 LOC
705
+ - src/bar/baz.py: 1 file
706
+
707
+ ## Resolved lenses
708
+
709
+ - performance
710
+ - security
711
+ """))
712
+ return log
713
+
714
+
715
+ def test_happy_path_passes(tmp_path):
716
+ run_dir = tmp_path / "run"
717
+ run_dir.mkdir()
718
+ _write_grilling_log(run_dir)
719
+ report = run_dir / "final-report.md"
720
+ report.write_text(_happy_report_body())
721
+ result = validate_improvement_report(
722
+ report_path=report,
723
+ run_dir=run_dir,
724
+ brief_frontmatter=HAPPY_BRIEF_FRONTMATTER,
725
+ )
726
+ assert result.ok, result.errors
727
+
728
+
729
+ def test_missing_section_4_fails(tmp_path):
730
+ run_dir = tmp_path / "run"
731
+ run_dir.mkdir()
732
+ _write_grilling_log(run_dir)
733
+ report = run_dir / "final-report.md"
734
+ body = _happy_report_body().replace("## 4. Improvement Candidates", "## 4. Other")
735
+ report.write_text(body)
736
+ result = validate_improvement_report(report, run_dir, HAPPY_BRIEF_FRONTMATTER)
737
+ assert not result.ok
738
+ assert any("## 4. Improvement Candidates" in e for e in result.errors)
739
+
740
+
741
+ def test_lens_outside_whitelist_fails(tmp_path):
742
+ run_dir = tmp_path / "run"; run_dir.mkdir(); _write_grilling_log(run_dir)
743
+ body = _happy_report_body().replace("performance | Eliminate", "perf | Eliminate")
744
+ (run_dir / "final-report.md").write_text(body)
745
+ result = validate_improvement_report(run_dir / "final-report.md", run_dir, HAPPY_BRIEF_FRONTMATTER)
746
+ assert not result.ok
747
+ assert any("Lens" in e and "perf" in e for e in result.errors)
748
+
749
+
750
+ def test_cand_id_pattern_fails(tmp_path):
751
+ run_dir = tmp_path / "run"; run_dir.mkdir(); _write_grilling_log(run_dir)
752
+ body = _happy_report_body().replace("| I-001 |", "| I-1 |")
753
+ (run_dir / "final-report.md").write_text(body)
754
+ result = validate_improvement_report(run_dir / "final-report.md", run_dir, HAPPY_BRIEF_FRONTMATTER)
755
+ assert not result.ok
756
+ assert any("Cand ID" in e for e in result.errors)
757
+
758
+
759
+ def test_scope_outside_scan_scope_fails(tmp_path):
760
+ run_dir = tmp_path / "run"; run_dir.mkdir(); _write_grilling_log(run_dir)
761
+ body = _happy_report_body().replace("src/foo/handler.py | high", "src/elsewhere/x.py | high")
762
+ (run_dir / "final-report.md").write_text(body)
763
+ result = validate_improvement_report(run_dir / "final-report.md", run_dir, HAPPY_BRIEF_FRONTMATTER)
764
+ assert not result.ok
765
+ assert any("Scope" in e and "elsewhere" in e for e in result.errors)
766
+
767
+
768
+ def test_candidate_cap_exceeded_fails(tmp_path):
769
+ fm = dict(HAPPY_BRIEF_FRONTMATTER); fm["candidate-cap"] = 2
770
+ run_dir = tmp_path / "run"; run_dir.mkdir(); _write_grilling_log(run_dir)
771
+ (run_dir / "final-report.md").write_text(_happy_report_body())
772
+ result = validate_improvement_report(run_dir / "final-report.md", run_dir, fm)
773
+ assert not result.ok
774
+ assert any("candidate-cap" in e or "exceeds" in e for e in result.errors)
775
+
776
+
777
+ def test_source_workers_includes_report_writer_fails(tmp_path):
778
+ run_dir = tmp_path / "run"; run_dir.mkdir(); _write_grilling_log(run_dir)
779
+ body = _happy_report_body().replace(
780
+ "claude:F-001, codex:1.2, gemini:F-3",
781
+ "claude:F-001, report-writer:R-1",
782
+ )
783
+ (run_dir / "final-report.md").write_text(body)
784
+ result = validate_improvement_report(run_dir / "final-report.md", run_dir, HAPPY_BRIEF_FRONTMATTER)
785
+ assert not result.ok
786
+ assert any("report-writer" in e for e in result.errors)
787
+
788
+
789
+ def test_worker_unique_with_consensus_full_fails(tmp_path):
790
+ run_dir = tmp_path / "run"; run_dir.mkdir(); _write_grilling_log(run_dir)
791
+ body = _happy_report_body().replace(
792
+ "| I-003 | architecture | Extract retry logic from handler | src/foo/handler.py | low | M | worker-unique | gemini:F-7 |",
793
+ "| I-003 | architecture | Extract retry logic from handler | src/foo/handler.py | low | M | full | gemini:F-7 |",
794
+ )
795
+ (run_dir / "final-report.md").write_text(body)
796
+ result = validate_improvement_report(run_dir / "final-report.md", run_dir, HAPPY_BRIEF_FRONTMATTER)
797
+ assert not result.ok
798
+ assert any("worker-unique" in e or "single" in e.lower() for e in result.errors)
799
+
800
+
801
+ def test_next_phase_outside_enum_fails(tmp_path):
802
+ run_dir = tmp_path / "run"; run_dir.mkdir(); _write_grilling_log(run_dir)
803
+ body = _happy_report_body().replace("implementation-planning |", "release-handoff |", 1)
804
+ (run_dir / "final-report.md").write_text(body)
805
+ result = validate_improvement_report(run_dir / "final-report.md", run_dir, HAPPY_BRIEF_FRONTMATTER)
806
+ assert not result.ok
807
+ assert any("next-phase" in e.lower() for e in result.errors)
808
+
809
+
810
+ def test_verdict_token_outside_enum_fails(tmp_path):
811
+ run_dir = tmp_path / "run"; run_dir.mkdir(); _write_grilling_log(run_dir)
812
+ body = _happy_report_body().replace("candidates-ready", "accepted")
813
+ (run_dir / "final-report.md").write_text(body)
814
+ result = validate_improvement_report(run_dir / "final-report.md", run_dir, HAPPY_BRIEF_FRONTMATTER)
815
+ assert not result.ok
816
+ assert any("Verdict Token" in e for e in result.errors)
817
+
818
+
819
+ def test_verdict_card_mismatch_fails(tmp_path):
820
+ run_dir = tmp_path / "run"; run_dir.mkdir(); _write_grilling_log(run_dir)
821
+ body = _happy_report_body().replace(
822
+ "| candidates-ready | routing | 사용자에게 후보 K개 선택 의뢰 (## 4. 표 참조) |",
823
+ "| candidates-ready | routing | mismatched |",
824
+ 1,
825
+ )
826
+ (run_dir / "final-report.md").write_text(body)
827
+ result = validate_improvement_report(run_dir / "final-report.md", run_dir, HAPPY_BRIEF_FRONTMATTER)
828
+ assert not result.ok
829
+ assert any("Verdict Card" in e for e in result.errors)
830
+
831
+
832
+ def test_missing_grilling_log_fails(tmp_path):
833
+ run_dir = tmp_path / "run"; run_dir.mkdir() # no state dir
834
+ (run_dir / "final-report.md").write_text(_happy_report_body())
835
+ result = validate_improvement_report(run_dir / "final-report.md", run_dir, HAPPY_BRIEF_FRONTMATTER)
836
+ assert not result.ok
837
+ assert any("phase-1.5-grilling" in e for e in result.errors)
838
+ ```
839
+
840
+ - [ ] **Step 2: 테스트 실행 → fail**
841
+
842
+ Run: `python3 -m pytest tests/test_validate_improvement_report.py -v`
843
+ Expected: `ModuleNotFoundError: No module named 'validators.validate_improvement_report'`.
844
+
845
+ - [ ] **Step 3: validator 구현**
846
+
847
+ ```python
848
+ # validators/validate_improvement_report.py
849
+ """Validator for final-report.md produced by the improvement-discovery phase.
850
+
851
+ Enforces the 11-item contract in
852
+ docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md §6.5.
853
+
854
+ Called by validators/validate-run.py when task_type == "improvement-discovery".
855
+ """
856
+ from __future__ import annotations
857
+
858
+ import re
859
+ from dataclasses import dataclass, field
860
+ from pathlib import Path
861
+
862
+ from scripts.okstra_ctl.improvement_lenses import (
863
+ LENSES,
864
+ DEFAULT_CANDIDATE_CAP,
865
+ ABSOLUTE_CANDIDATE_CAP,
866
+ SOURCE_WORKERS,
867
+ )
868
+
869
+
870
+ _VERDICT_TOKENS = ("candidates-ready", "no-candidates", "blocked")
871
+ _NEXT_PHASES = ("requirements-discovery", "implementation-planning", "error-analysis")
872
+ _CAND_ID_RE = re.compile(r"^I-\d{3}$")
873
+ _SOURCE_WORKER_RE = re.compile(r"^([a-z-]+):([A-Za-z0-9._-]+)$")
874
+ _CONSENSUS_VALUES = ("full", "partial", "contested", "worker-unique")
875
+
876
+
877
+ @dataclass
878
+ class ValidationResult:
879
+ ok: bool
880
+ errors: list[str] = field(default_factory=list)
881
+
882
+
883
+ def _read_section_table(body: str, heading: str) -> list[list[str]]:
884
+ """Return rows of the markdown pipe-table directly under ``heading``.
885
+ Each row is a list of trimmed cell values. Returns [] if heading absent
886
+ or no table follows.
887
+ """
888
+ pattern = rf"##\s+{re.escape(heading)}\b.*?\n(.*?)(?=\n##\s|\Z)"
889
+ m = re.search(pattern, body, flags=re.S)
890
+ if not m:
891
+ return []
892
+ section = m.group(1)
893
+ rows: list[list[str]] = []
894
+ for line in section.splitlines():
895
+ s = line.strip()
896
+ if not s.startswith("|") or not s.endswith("|"):
897
+ continue
898
+ cells = [c.strip() for c in s.strip("|").split("|")]
899
+ if all(set(c) <= set("-: ") for c in cells): # divider row
900
+ continue
901
+ rows.append(cells)
902
+ return rows
903
+
904
+
905
+ def _candidate_cap(brief_frontmatter: dict) -> int:
906
+ raw = brief_frontmatter.get("candidate-cap")
907
+ if raw is None:
908
+ return DEFAULT_CANDIDATE_CAP
909
+ try:
910
+ return int(raw)
911
+ except (TypeError, ValueError):
912
+ return DEFAULT_CANDIDATE_CAP
913
+
914
+
915
+ def _scope_subset(candidate_scope_csv: str, scan_scope: list[str], out_of_scope: list[str]) -> tuple[bool, str]:
916
+ paths = [p.strip() for p in candidate_scope_csv.split(",") if p.strip()]
917
+ if not paths:
918
+ return False, "empty Scope"
919
+ for p in paths:
920
+ if any(p == s or p.startswith(s.rstrip("/") + "/") for s in scan_scope):
921
+ for o in out_of_scope:
922
+ if p == o or p.startswith(o.rstrip("/") + "/"):
923
+ return False, f"Scope '{p}' is inside out-of-scope '{o}'"
924
+ continue
925
+ return False, f"Scope '{p}' is outside brief scan-scope {scan_scope}"
926
+ return True, ""
927
+
928
+
929
+ def validate_improvement_report(
930
+ report_path: Path, run_dir: Path, brief_frontmatter: dict
931
+ ) -> ValidationResult:
932
+ errors: list[str] = []
933
+ body = report_path.read_text(encoding="utf-8")
934
+
935
+ # Item 10 — Phase 1.5 grilling log must exist with resolved blocks
936
+ grilling = run_dir / "state" / "phase-1.5-grilling.md"
937
+ if not grilling.exists():
938
+ errors.append("missing phase-1.5-grilling.md log at runs/.../state/")
939
+ else:
940
+ gtext = grilling.read_text(encoding="utf-8")
941
+ if "Resolved scope" not in gtext:
942
+ errors.append("phase-1.5-grilling.md missing 'Resolved scope' block")
943
+ if "Resolved lenses" not in gtext:
944
+ errors.append("phase-1.5-grilling.md missing 'Resolved lenses' block")
945
+
946
+ # Item 1 — ## 4. Improvement Candidates exists with header row matching schema
947
+ rows = _read_section_table(body, "4. Improvement Candidates")
948
+ if not rows:
949
+ errors.append("missing or empty `## 4. Improvement Candidates` table")
950
+ return ValidationResult(ok=False, errors=errors)
951
+ header, *data = rows
952
+ expected_columns = [
953
+ "Cand ID", "Lens", "Title", "Scope", "Severity", "Effort",
954
+ "Consensus", "Source workers", "Recommended next-phase", "Evidence",
955
+ ]
956
+ if header != expected_columns:
957
+ errors.append(
958
+ f"`## 4. Improvement Candidates` header must be {expected_columns}, "
959
+ f"got {header}"
960
+ )
961
+
962
+ # Items 2-7 — per-row validation
963
+ seen_ids: set[str] = set()
964
+ cap = _candidate_cap(brief_frontmatter)
965
+ if cap < 1 or cap > ABSOLUTE_CANDIDATE_CAP:
966
+ errors.append(
967
+ f"brief candidate-cap {cap} out of allowed range 1..{ABSOLUTE_CANDIDATE_CAP}"
968
+ )
969
+ if len(data) > min(cap, ABSOLUTE_CANDIDATE_CAP):
970
+ errors.append(
971
+ f"row count {len(data)} exceeds candidate-cap {min(cap, ABSOLUTE_CANDIDATE_CAP)}"
972
+ )
973
+
974
+ scan_scope = brief_frontmatter.get("scan-scope") or []
975
+ out_of_scope = brief_frontmatter.get("out-of-scope") or []
976
+
977
+ for idx, row in enumerate(data, start=1):
978
+ if len(row) != 10:
979
+ errors.append(f"row {idx} has {len(row)} columns, expected 10")
980
+ continue
981
+ cand_id, lens_cell, _title, scope, _sev, _eff, consensus, source_workers, next_phase, _evidence = row
982
+
983
+ # Item 2 — Cand ID pattern + uniqueness
984
+ if not _CAND_ID_RE.match(cand_id):
985
+ errors.append(f"row {idx}: Cand ID '{cand_id}' must match I-NNN")
986
+ elif cand_id in seen_ids:
987
+ errors.append(f"row {idx}: duplicate Cand ID '{cand_id}'")
988
+ seen_ids.add(cand_id)
989
+
990
+ # Item 3 — Lens whitelist
991
+ lenses_in_row = [l.strip() for l in lens_cell.split(",") if l.strip()]
992
+ for l in lenses_in_row:
993
+ if l not in LENSES:
994
+ errors.append(f"row {idx}: Lens '{l}' is not in whitelist {LENSES}")
995
+ if not (1 <= len(lenses_in_row) <= 2):
996
+ errors.append(f"row {idx}: Lens cell must contain 1 or 2 values, got {len(lenses_in_row)}")
997
+
998
+ # Item 4 — Scope subset
999
+ ok, reason = _scope_subset(scope, scan_scope, out_of_scope)
1000
+ if not ok:
1001
+ errors.append(f"row {idx}: {reason}")
1002
+
1003
+ # Item 6 — Source workers pattern + SOURCE_WORKERS enum + worker-unique consistency
1004
+ workers_in_row: list[str] = []
1005
+ for token in source_workers.split(","):
1006
+ token = token.strip()
1007
+ if not token:
1008
+ continue
1009
+ m = _SOURCE_WORKER_RE.match(token)
1010
+ if not m:
1011
+ errors.append(f"row {idx}: Source workers token '{token}' must match <worker>:<id>")
1012
+ continue
1013
+ worker, _item = m.group(1), m.group(2)
1014
+ if worker not in SOURCE_WORKERS:
1015
+ errors.append(
1016
+ f"row {idx}: Source workers '{worker}' is not in {SOURCE_WORKERS} (report-writer excluded)"
1017
+ )
1018
+ workers_in_row.append(worker)
1019
+ if not workers_in_row:
1020
+ errors.append(f"row {idx}: Source workers cell is empty")
1021
+
1022
+ if consensus not in _CONSENSUS_VALUES:
1023
+ errors.append(
1024
+ f"row {idx}: Consensus '{consensus}' must be one of {_CONSENSUS_VALUES}"
1025
+ )
1026
+ if len(set(workers_in_row)) == 1 and consensus != "worker-unique":
1027
+ errors.append(
1028
+ f"row {idx}: single-source-worker entries must use Consensus=worker-unique"
1029
+ )
1030
+
1031
+ # Item 7 — Recommended next-phase enum
1032
+ if next_phase not in _NEXT_PHASES:
1033
+ errors.append(
1034
+ f"row {idx}: Recommended next-phase '{next_phase}' must be one of {_NEXT_PHASES}"
1035
+ )
1036
+
1037
+ # Item 8 — Final Verdict token enum
1038
+ final_verdict_rows = _read_section_table(body, "2. Final Verdict")
1039
+ if not final_verdict_rows:
1040
+ errors.append("missing `## 2. Final Verdict` block")
1041
+ else:
1042
+ token_cell = final_verdict_rows[-1][0] if final_verdict_rows[-1] else ""
1043
+ if token_cell not in _VERDICT_TOKENS:
1044
+ errors.append(
1045
+ f"`## 2. Final Verdict` Verdict Token '{token_cell}' must be one of {_VERDICT_TOKENS}"
1046
+ )
1047
+
1048
+ # Item 9 — Verdict Card byte-match with `## 2.`
1049
+ verdict_card_rows = _read_section_table(body, "Verdict Card")
1050
+ if verdict_card_rows and final_verdict_rows:
1051
+ if verdict_card_rows[-1] != final_verdict_rows[-1]:
1052
+ errors.append(
1053
+ "Verdict Card row must byte-match `## 2. Final Verdict` row"
1054
+ )
1055
+
1056
+ # Item 11 — lens enum SSOT byte-match (sanity check: every lens used in
1057
+ # body equals one listed in LENSES). The header schema check above already
1058
+ # ensures the column itself; here we confirm import contract by referencing
1059
+ # the constant — failing imports surface a different error class.
1060
+ _ = LENSES # SSOT live import; if the import broke, this file would not load.
1061
+
1062
+ return ValidationResult(ok=not errors, errors=errors)
1063
+ ```
1064
+
1065
+ - [ ] **Step 4: 테스트 실행 → pass**
1066
+
1067
+ Run: `python3 -m pytest tests/test_validate_improvement_report.py -v`
1068
+ Expected: 12 tests pass (happy + 11 sad).
1069
+
1070
+ - [ ] **Step 5: commit**
1071
+
1072
+ ```bash
1073
+ git add validators/validate_improvement_report.py tests/test_validate_improvement_report.py
1074
+ git commit -m "feat(validators): validate-improvement-report enforces 11-item contract"
1075
+ ```
1076
+
1077
+ ---
1078
+
1079
+ ## Task 8: validate-run.py 통합 분기
1080
+
1081
+ **Files:**
1082
+ - Modify: `validators/validate-run.py` (improvement-discovery 분기 추가)
1083
+
1084
+ - [ ] **Step 1: validate-run.py 의 task_type 분기 지점 grep**
1085
+
1086
+ Run: `grep -n "task_type\|task-type" validators/validate-run.py | head -20`
1087
+ 이 출력에서 task_type 분기 if/elif 블록을 찾는다.
1088
+
1089
+ - [ ] **Step 2: improvement-discovery 분기 추가**
1090
+
1091
+ 해당 분기에 다음 elif 절을 추가:
1092
+
1093
+ ```python
1094
+ elif task_type == "improvement-discovery":
1095
+ from validators.validate_improvement_report import validate_improvement_report
1096
+ result = validate_improvement_report(
1097
+ report_path=final_report_path,
1098
+ run_dir=run_dir,
1099
+ brief_frontmatter=brief_fm,
1100
+ )
1101
+ if not result.ok:
1102
+ for err in result.errors:
1103
+ errors.append(f"improvement-discovery: {err}")
1104
+ ```
1105
+
1106
+ (`brief_fm` 변수 이름과 `final_report_path` / `run_dir` 의 정확한 이름은 validate-run.py 의 기존 코드 패턴에 맞춘다. grep 결과로 확인.)
1107
+
1108
+ - [ ] **Step 3: 기존 validator 테스트 회귀 없음 확인**
1109
+
1110
+ Run: `python3 -m pytest tests/test_validate_run_report_format.py tests/test_validate_run_autofix.py -v`
1111
+ Expected: 모든 기존 테스트 pass.
1112
+
1113
+ - [ ] **Step 4: commit**
1114
+
1115
+ ```bash
1116
+ git add validators/validate-run.py
1117
+ git commit -m "feat(validators): wire validate-run into improvement-discovery validator"
1118
+ ```
1119
+
1120
+ ---
1121
+
1122
+ ## Task 9: e2e scenario-08 (render-only)
1123
+
1124
+ **Files:**
1125
+ - Create: `tests-e2e/scenario-08-improvement-discovery-render-only.sh`
1126
+
1127
+ - [ ] **Step 1: e2e 스크립트 작성**
1128
+
1129
+ ```bash
1130
+ #!/usr/bin/env bash
1131
+ # tests-e2e/scenario-08-improvement-discovery-render-only.sh
1132
+ #
1133
+ # Verify the improvement-discovery task-type renders end-to-end:
1134
+ # - okstra-brief writes a codebase-scan brief
1135
+ # - render-bundle produces an instruction-set including the new input template
1136
+ # - validator stub passes
1137
+ #
1138
+ # Uses an isolated OKSTRA_HOME and PROJECT_ROOT under mktemp.
1139
+
1140
+ set -euo pipefail
1141
+
1142
+ SOURCE_PATH="${BASH_SOURCE[0]}"
1143
+ SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE_PATH")" && pwd)"
1144
+ WORKSPACE_ROOT="$(cd -P "$SCRIPT_DIR/.." && pwd)"
1145
+
1146
+ PROJECT_ROOT="$(mktemp -d -t okstra-imp-disc.XXXXXX)"
1147
+ export OKSTRA_HOME="$(mktemp -d -t okstra-home.XXXXXX)"
1148
+ trap 'rm -rf "$PROJECT_ROOT" "$OKSTRA_HOME"' EXIT
1149
+
1150
+ OKSTRA_BIN="$WORKSPACE_ROOT/bin/okstra"
1151
+
1152
+ echo "==> Project root: $PROJECT_ROOT"
1153
+ echo "==> OKSTRA_HOME: $OKSTRA_HOME"
1154
+
1155
+ # 1. Seed a minimal project — git + project.json
1156
+ git -C "$PROJECT_ROOT" init -q
1157
+ git -C "$PROJECT_ROOT" config user.email test@example.com
1158
+ git -C "$PROJECT_ROOT" config user.name test
1159
+ mkdir -p "$PROJECT_ROOT/src/foo"
1160
+ cat > "$PROJECT_ROOT/src/foo/handler.py" <<'PY'
1161
+ def handle(items):
1162
+ out = []
1163
+ for item in items:
1164
+ out.append(item.related().load())
1165
+ return out
1166
+ PY
1167
+ git -C "$PROJECT_ROOT" add -A
1168
+ git -C "$PROJECT_ROOT" commit -q -m "seed"
1169
+
1170
+ mkdir -p "$PROJECT_ROOT/.project-docs/okstra"
1171
+ cat > "$PROJECT_ROOT/.project-docs/okstra/project.json" <<JSON
1172
+ {
1173
+ "projectId": "imp-disc-e2e",
1174
+ "projectRoot": "$PROJECT_ROOT"
1175
+ }
1176
+ JSON
1177
+
1178
+ # 2. Write a codebase-scan brief by hand (simulating the new okstra-brief flow)
1179
+ mkdir -p "$PROJECT_ROOT/.project-docs/okstra/briefs/refactor"
1180
+ BRIEF_PATH="$PROJECT_ROOT/.project-docs/okstra/briefs/refactor/imp-disc-001-scan-foo.md"
1181
+ cat > "$BRIEF_PATH" <<'MD'
1182
+ ---
1183
+ type: brief
1184
+ brief-id: imp-disc-001-scan-foo
1185
+ parent-id: self
1186
+ ticket-id: ""
1187
+ source-type: user-input
1188
+ task-group: refactor
1189
+ depth: 0
1190
+ created: 2026-05-21
1191
+ generator: e2e-fixture
1192
+ reporter-confirmations: complete
1193
+ scope: codebase
1194
+ priority-lenses: [performance, security]
1195
+ scan-scope: [src/foo/]
1196
+ out-of-scope: []
1197
+ candidate-cap: 3
1198
+ ---
1199
+
1200
+ # Task Brief: refactor/imp-disc-001-scan-foo
1201
+
1202
+ ## Context
1203
+ - Scanning src/foo/ to surface improvement candidates before owner handoff.
1204
+
1205
+ ## Desired Outcome
1206
+ - Top 3 candidates across performance and security lenses.
1207
+
1208
+ ## Constraints
1209
+ - None.
1210
+
1211
+ ## Scan Scope
1212
+ - src/foo/ — request handler module.
1213
+
1214
+ ## Priority Lenses
1215
+ - performance — N+1 / hot paths.
1216
+ - security — input validation around redirect targets.
1217
+
1218
+ ## Open Questions
1219
+ _(none)_
1220
+
1221
+ ## Augmentation
1222
+ _(none)_
1223
+ MD
1224
+
1225
+ # 3. render-bundle in render-only mode
1226
+ "$OKSTRA_BIN" render-bundle \
1227
+ --project-root "$PROJECT_ROOT" \
1228
+ --project-id "imp-disc-e2e" \
1229
+ --task-group "refactor" \
1230
+ --task-id "imp-disc-001-scan-foo" \
1231
+ --task-type "improvement-discovery" \
1232
+ --task-brief "$BRIEF_PATH" \
1233
+ --base-ref "HEAD" \
1234
+ --workers "" \
1235
+ --directive "" \
1236
+ --lead-model "" \
1237
+ --claude-model "" \
1238
+ --codex-model "" \
1239
+ --gemini-model "" \
1240
+ --report-writer-model "" \
1241
+ --related-tasks "" \
1242
+ --clarification-response "" \
1243
+ --pr-template-path "" \
1244
+ --executor "" \
1245
+ --approved-plan "" \
1246
+ --stage ""
1247
+
1248
+ # 4. Assert the instruction-set was produced with the improvement template
1249
+ INSTR_DIR=$(find "$PROJECT_ROOT/.project-docs/okstra/tasks/refactor/imp-disc-001-scan-foo" -type d -name "instruction-set" | head -1)
1250
+ [[ -d "$INSTR_DIR" ]] || { echo "FAIL: instruction-set dir not found"; exit 1; }
1251
+ grep -q "OKSTRA Improvement Discovery Input" "$INSTR_DIR"/*.md \
1252
+ || { echo "FAIL: input template not rendered"; exit 1; }
1253
+ grep -q "scope: codebase" "$INSTR_DIR"/*.md \
1254
+ || { echo "FAIL: scope frontmatter not propagated"; exit 1; }
1255
+
1256
+ echo "PASS: scenario-08-improvement-discovery-render-only"
1257
+ ```
1258
+
1259
+ - [ ] **Step 2: 실행 권한 부여 + 실행**
1260
+
1261
+ Run:
1262
+ ```bash
1263
+ chmod +x tests-e2e/scenario-08-improvement-discovery-render-only.sh
1264
+ bash tests-e2e/scenario-08-improvement-discovery-render-only.sh
1265
+ ```
1266
+ Expected: 마지막 줄에 `PASS: scenario-08-improvement-discovery-render-only`.
1267
+
1268
+ - [ ] **Step 3: commit**
1269
+
1270
+ ```bash
1271
+ git add tests-e2e/scenario-08-improvement-discovery-render-only.sh
1272
+ git commit -m "test(e2e): render-only scenario for improvement-discovery"
1273
+ ```
1274
+
1275
+ ---
1276
+
1277
+ ## Task 10: 사용자 docs 업데이트 (한국어 우선)
1278
+
1279
+ **Files:**
1280
+ - Modify: `README.md`, `README.kr.md`
1281
+ - Modify: `docs/kr/cli.md`, `docs/kr/architecture.md`, `docs/project-structure-overview.md`
1282
+ - Modify: `CLAUDE.md`
1283
+
1284
+ - [ ] **Step 1: README.md 와 README.kr.md 의 task-type 라인업 표에 행 추가**
1285
+
1286
+ 각 README 의 task-type 목록 / 라이프사이클 표에:
1287
+
1288
+ ```markdown
1289
+ | `improvement-discovery` | 코드베이스 범위·lens 안에서 개선 후보 N개 도출 (PHASE_SEQUENCE 외부 sidetrack entry-point) |
1290
+ ```
1291
+
1292
+ - [ ] **Step 2: docs/kr/cli.md 에 `--task-type improvement-discovery` 절 추가**
1293
+
1294
+ 다음 절을 적당한 위치에 (다른 task-type 절과 같은 구조로) 삽입:
1295
+
1296
+ ```markdown
1297
+ ### `--task-type improvement-discovery`
1298
+
1299
+ - 입력: frontmatter `scope: codebase` 마커가 있는 brief.
1300
+ - `priority-lenses`: 1–4개. lens 화이트리스트는 `scripts/okstra_ctl/improvement_lenses.py` 의 `LENSES` 상수.
1301
+ - `scan-scope`: 1개 이상의 경로.
1302
+ - `out-of-scope`: 선택.
1303
+ - `candidate-cap`: 1–12, 기본 8.
1304
+ - 출력: `## 4. Improvement Candidates` 표 (10 column).
1305
+ - Verdict Token: `candidates-ready` / `no-candidates` / `blocked`.
1306
+ - 라우팅: 자동 spin-off 없음. 사용자가 후보를 골라 새 task-id 로 `requirements-discovery` / `implementation-planning` / `error-analysis` 진입.
1307
+ - 워커: claude + codex + gemini + report-writer 모두 필수.
1308
+ - 양방향 grilling 두 지점: `okstra-brief` Step 4 강화 (budget 8) + lead 의 Phase 1.5 reflect-back (budget 12).
1309
+ ```
1310
+
1311
+ - [ ] **Step 3: docs/kr/architecture.md 에 라이프사이클 흐름도 추가**
1312
+
1313
+ 기존 lifecycle 절 근처에 다음 다이어그램과 한 단락 (markdown nested fence 충돌 회피 위해 outer 는 4-backtick fence):
1314
+
1315
+ ````markdown
1316
+ ### improvement-discovery (sidetrack entry-point)
1317
+
1318
+ ```
1319
+ [brief: scope=codebase + priority-lenses]
1320
+ ↓ okstra-run --task-type improvement-discovery
1321
+ [improvement-discovery]
1322
+ ↓ final-report (## 4. Improvement Candidates 후보 N개)
1323
+ ↓ (사용자가 후보 K개 선택, 각각 새 brief 작성)
1324
+ [requirements-discovery | implementation-planning | error-analysis] (선택된 후보별로 새 task-id 로)
1325
+ ```
1326
+
1327
+ `PHASE_SEQUENCE` 의 정식 멤버에 들어가지 않는 sidetrack entry-point. 단방향 라이프사이클을 깨지 않으면서 코드베이스 발견 시나리오를 흡수한다. lens 화이트리스트와 candidate-cap 은 `scripts/okstra_ctl/improvement_lenses.py` SSOT 1개에서 통일된다.
1328
+ ````
1329
+
1330
+ - [ ] **Step 4: docs/project-structure-overview.md 에 새 파일 항목 추가**
1331
+
1332
+ `scripts/okstra_ctl/` 절에 `improvement_lenses.py — improvement-discovery phase 의 lens enum SSOT` 한 줄.
1333
+ `validators/` 절에 `validate-improvement-report.py — improvement-discovery final-report 의 11항목 강제 validator` 한 줄.
1334
+
1335
+ - [ ] **Step 5: CLAUDE.md 의 "Where to find things" 표와 "Worker / agent contract" / "Gotchas" 절에 한 줄씩 추가**
1336
+
1337
+ - "Where to find things" 표:
1338
+ ```markdown
1339
+ | improvement-discovery 의 lens enum 정의 | [scripts/okstra_ctl/improvement_lenses.py](scripts/okstra_ctl/improvement_lenses.py) — SSOT |
1340
+ ```
1341
+ - "Worker / agent contract" 절 끝:
1342
+ ```markdown
1343
+ - `improvement-discovery` 는 PHASE_SEQUENCE 외부 sidetrack entry-point. lens 화이트리스트 외 후보는 `validators/validate-improvement-report.py` 가 거부.
1344
+ ```
1345
+ - "Gotchas" 절 끝:
1346
+ ```markdown
1347
+ - `improvement-discovery` 의 lens enum / cap 상수는 `scripts/okstra_ctl/improvement_lenses.py` 의 SSOT 외 어디에도 정의 금지 — 중복 정의 시 `tests/test_okstra_improvement_lenses.py` 가 fail.
1348
+ ```
1349
+
1350
+ - [ ] **Step 6: 문서 변경 일관성 grep**
1351
+
1352
+ Run: `grep -l "improvement-discovery" README.md README.kr.md docs/kr/cli.md docs/kr/architecture.md docs/project-structure-overview.md CLAUDE.md | wc -l`
1353
+ Expected: `6`.
1354
+
1355
+ - [ ] **Step 7: commit**
1356
+
1357
+ ```bash
1358
+ git add README.md README.kr.md docs/kr/cli.md docs/kr/architecture.md docs/project-structure-overview.md CLAUDE.md
1359
+ git commit -m "docs: document improvement-discovery task-type across user-facing docs"
1360
+ ```
1361
+
1362
+ ---
1363
+
1364
+ ## Task 11: CHANGES.md 한국어 entry
1365
+
1366
+ **Files:**
1367
+ - Modify: `CHANGES.md`
1368
+
1369
+ - [ ] **Step 1: 최상단에 entry 추가**
1370
+
1371
+ `CHANGES.md` 의 가장 위 (가장 최근 entry 바로 위) 에 삽입:
1372
+
1373
+ ```markdown
1374
+ ## improvement-discovery task-type 추가
1375
+
1376
+ - 새 task-type `improvement-discovery` 가 도입되었다. 코드베이스 범위와 lens 우선순위만 받아 multi-worker 합의 기반의 개선 후보 N개 (기본 8, 절대 cap 12) 를 도출한다.
1377
+ - `scripts/okstra_ctl/improvement_lenses.py` 가 lens enum 8개 / cap 상수의 SSOT. profile · brief skill · validator · wizard 모두 이 모듈을 참조한다.
1378
+ - 양방향 grilling 두 지점: `okstra-brief` Step 4 강화 + lead 의 Phase 1.5 reflect-back. 한 task 당 합 최대 20 questions.
1379
+ - `validators/validate-improvement-report.py` 가 final-report 의 `## 4. Improvement Candidates` 10-column schema 와 11개 항목을 강제.
1380
+ - `PHASE_SEQUENCE` 에는 포함되지 않은 sidetrack entry-point. 자동 spin-off 없음.
1381
+
1382
+ 사용자 영향: 코드베이스에서 개선처를 발견하려는 시나리오에서 새 `--task-type improvement-discovery` 를 사용할 수 있다. brief 는 `scope: codebase` frontmatter 와 `priority-lenses` / `scan-scope` 를 갖춰야 하며, 후보별로 사용자가 직접 새 task 를 시작해 후속 phase 로 진입한다.
1383
+ ```
1384
+
1385
+ - [ ] **Step 2: commit**
1386
+
1387
+ ```bash
1388
+ git add CHANGES.md
1389
+ git commit -m "docs(changes): add improvement-discovery task-type entry"
1390
+ ```
1391
+
1392
+ ---
1393
+
1394
+ ## Milestone 2 — 마무리 검증
1395
+
1396
+ - [ ] **전체 테스트 + e2e 통과**
1397
+
1398
+ Run:
1399
+ ```bash
1400
+ python3 -m pytest tests/ -x
1401
+ bash tests-e2e/scenario-08-improvement-discovery-render-only.sh
1402
+ bash validators/validate-workflow.sh
1403
+ ```
1404
+ Expected: 모두 pass.
1405
+
1406
+ - [ ] **PR 2 push (PR 생성은 사용자 명시 요청 시)**
1407
+
1408
+ ```bash
1409
+ git push origin feat/improvement-discovery
1410
+ ```
1411
+
1412
+ ---
1413
+
1414
+ # Milestone 3 — Brief skill 확장 (PR 3)
1415
+
1416
+ PR 2 머지 후 시작. 사용자 진입 UI 가 codebase-scan brief 를 만들 수 있게.
1417
+
1418
+ ## Task 12: okstra-brief Step 1 의 2-level pick
1419
+
1420
+ **Files:**
1421
+ - Modify: `skills/okstra-brief/SKILL.md` (Step 1)
1422
+
1423
+ - [ ] **Step 1: Step 1 첫 question 을 2-level pick 으로 갈음**
1424
+
1425
+ 기존 Step 1 의 `AskUserQuestion` 흐름을 다음 구조로 교체:
1426
+
1427
+ ```markdown
1428
+ ## Step 1: Choose input source
1429
+
1430
+ ### 1.0. brief variant
1431
+
1432
+ `AskUserQuestion` (tool constraint: max 4 options):
1433
+
1434
+ - **Label**: "Brief variant?"
1435
+ - **Options** (single-select):
1436
+ 1. `Reporter input` — reporter 의 발화 / 티켓 / 링크 / 자유 텍스트를 verbatim 으로 흡수. 기존 4-source 흐름.
1437
+ 2. `Codebase scan` — 코드베이스 범위와 lens 우선순위만 받아 `improvement-discovery` phase 의 입력을 생성.
1438
+
1439
+ ### 1.A. Reporter input (variant 1)
1440
+
1441
+ 기존 `1a` ~ `1d` 흐름을 그대로 진행한다 (변경 없음).
1442
+
1443
+ ### 1.B. Codebase scan (variant 2)
1444
+
1445
+ 순서:
1446
+
1447
+ 1. `AskUserQuestion` (free text):
1448
+ `"Scan scope — comma-separated paths inside the project (e.g. src/foo/, src/bar/baz.py)"` → `scan_scope` (CSV).
1449
+ 2. `AskUserQuestion` (multi-select, options = `LENSES` 의 8개):
1450
+ `"Priority lenses (pick 1–4)"` → `priority_lenses`.
1451
+ 3. `AskUserQuestion` (free text):
1452
+ `"Out-of-scope paths (optional, comma-separated)"` → `out_of_scope` (CSV, 빈 값 허용).
1453
+ 4. `AskUserQuestion` (free text):
1454
+ `"Candidate cap (1–12, default 8)"` → `candidate_cap` (정수, 빈 값 → 8).
1455
+ 5. `AskUserQuestion` (free text):
1456
+ `"Context — why is this scope being scanned now? One short paragraph."` → `context`.
1457
+ 6. `AskUserQuestion` (free text):
1458
+ `"Desired outcome — what kinds of improvements do you want surfaced? Anti-goals?"` → `desired_outcome`.
1459
+ 7. `AskUserQuestion` (free text):
1460
+ `"Constraints — untouchable areas, deadlines, compatibility (optional)"` → `constraints`.
1461
+
1462
+ 검증 (Step 4 sharpening 진입 전 1차 check):
1463
+ - `scan_scope` 의 각 path 가 `<PROJECT_ROOT>` 안에 실제 존재하는지 `ls` 로 확인 — 없으면 한 질문으로 정정.
1464
+ - `priority_lenses` 가 `scripts/okstra_ctl/improvement_lenses.py` 의 `LENSES` 부분집합 (1–4) 인지 확인 — 위반 시 enum 표시 + 재선택.
1465
+ - `candidate_cap` 이 1–12 정수인지 확인.
1466
+
1467
+ 이 검증이 통과하면 Step 2 (task key) 진입.
1468
+ ```
1469
+
1470
+ - [ ] **Step 2: Step 5 의 brief template frontmatter 에 codebase-scan 필드 추가**
1471
+
1472
+ 기존 Step 5 의 frontmatter 예시 블록에 다음 필드들을 옵션으로 표시 (변형 1: reporter-input → 비움; 변형 2: codebase-scan → 채움):
1473
+
1474
+ ```yaml
1475
+ scope: <reporter-input | codebase> # codebase-scan variant 에서만 'codebase'
1476
+ priority-lenses: [] # codebase-scan 전용
1477
+ scan-scope: [] # codebase-scan 전용
1478
+ out-of-scope: [] # codebase-scan 전용 (옵션)
1479
+ candidate-cap: 8 # codebase-scan 전용 (1..12)
1480
+ ```
1481
+
1482
+ 또 Step 5 의 Required Sections 표 (`reporter-input` 칸과 `codebase-scan` 칸 비교) 를 spec §4.3 의 표 그대로 옮긴다.
1483
+
1484
+ - [ ] **Step 3: Step 6 헤리스틱에 한 줄 추가**
1485
+
1486
+ 기존 Step 6 의 헤리스틱 표 끝에 한 줄:
1487
+
1488
+ ```markdown
1489
+ | `scope: codebase` frontmatter 가 있는 경우 | `improvement-discovery` |
1490
+ ```
1491
+
1492
+ - [ ] **Step 4: smoke — okstra-brief 가 새 흐름을 인식하는지 확인**
1493
+
1494
+ Run: `grep -c "codebase-scan\|scope: codebase\|priority-lenses\|scan-scope" skills/okstra-brief/SKILL.md`
1495
+ Expected: ≥6.
1496
+
1497
+ - [ ] **Step 5: commit**
1498
+
1499
+ ```bash
1500
+ git add skills/okstra-brief/SKILL.md
1501
+ git commit -m "feat(skills/okstra-brief): codebase-scan variant via 2-level Step 1 pick"
1502
+ ```
1503
+
1504
+ ---
1505
+
1506
+ ## Task 13: okstra-brief Step 4 강화 rules
1507
+
1508
+ **Files:**
1509
+ - Modify: `skills/okstra-brief/SKILL.md` (Step 4 sharpening)
1510
+
1511
+ - [ ] **Step 1: codebase-scan variant 전용 sharpening rules 5개 추가**
1512
+
1513
+ Step 4 의 sharpening pass 블록 끝에 다음 절을 추가:
1514
+
1515
+ ```markdown
1516
+ ### codebase-scan variant 전용 강화 규칙 (improvement-discovery)
1517
+
1518
+ 이 5개 규칙은 `scope: codebase` brief 에 한해 적용. budget 은 기본 6 → **8** 로 확대.
1519
+
1520
+ 1. **scan-scope 경로 존재 검증.** 각 path 를 `ls` / `Read` 로 확인. 없으면 한 질문으로 정정 (path 의 가장 가까운 후보를 `Recommended:` 로 제시).
1521
+ 2. **모호 path narrowing.** `"백엔드"`, `"결제 모듈"` 같은 추상 path 는 구체 디렉토리 1개 이상으로 narrowing. codebase-first 추측 후 한 질문으로 확정.
1522
+ 3. **priority-lenses 화이트리스트 검증.** `scripts/okstra_ctl/improvement_lenses.py` 의 `LENSES` 부분집합인지 확인. out-of-enum 이면 enum 내 가장 가까운 값을 `Recommended:` 로 제시.
1523
+ 4. **out-of-scope 일관성.** `out-of-scope` 의 각 path 가 `scan-scope` 의 부분집합인지 확인. 무관한 path 면 한 질문으로 제거 또는 scan-scope 에 흡수.
1524
+ 5. **lens 우선순위 근거 1줄.** 각 priority lens 가 *왜* 이 scope 에서 우선인지 brief 본문에 한 줄 없으면, codebase 1차 훑은 결과를 `Recommended:` 로 제시 후 한 질문.
1525
+
1526
+ stop conditions (codebase-scan 한정 추가):
1527
+ - `scan-scope` 의 모든 path 가 codebase 에 존재
1528
+ - `priority-lenses` 가 valid enum 부분집합 (1–4개)
1529
+ - 위 두 조건이 모두 만족되면 budget 이 남아 있어도 sharpening 종료 가능.
1530
+ ```
1531
+
1532
+ - [ ] **Step 2: smoke**
1533
+
1534
+ Run: `grep -c "codebase-scan variant 전용" skills/okstra-brief/SKILL.md`
1535
+ Expected: `1`.
1536
+
1537
+ - [ ] **Step 3: commit**
1538
+
1539
+ ```bash
1540
+ git add skills/okstra-brief/SKILL.md
1541
+ git commit -m "feat(skills/okstra-brief): sharpening rules for codebase-scan variant"
1542
+ ```
1543
+
1544
+ ---
1545
+
1546
+ ## Task 14: codebase-scan brief 생성 e2e
1547
+
1548
+ **Files:**
1549
+ - Create: `tests-e2e/scenario-09-improvement-brief-generation.sh`
1550
+
1551
+ - [ ] **Step 1: e2e 스크립트 작성**
1552
+
1553
+ ```bash
1554
+ #!/usr/bin/env bash
1555
+ # tests-e2e/scenario-09-improvement-brief-generation.sh
1556
+ #
1557
+ # Verify that an end-user okstra-brief invocation with codebase-scan variant
1558
+ # produces a brief file containing the required frontmatter fields.
1559
+
1560
+ set -euo pipefail
1561
+
1562
+ SOURCE_PATH="${BASH_SOURCE[0]}"
1563
+ SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE_PATH")" && pwd)"
1564
+ WORKSPACE_ROOT="$(cd -P "$SCRIPT_DIR/.." && pwd)"
1565
+
1566
+ PROJECT_ROOT="$(mktemp -d -t okstra-imp-brief.XXXXXX)"
1567
+ export OKSTRA_HOME="$(mktemp -d -t okstra-home.XXXXXX)"
1568
+ trap 'rm -rf "$PROJECT_ROOT" "$OKSTRA_HOME"' EXIT
1569
+
1570
+ # Seed minimal project
1571
+ git -C "$PROJECT_ROOT" init -q
1572
+ git -C "$PROJECT_ROOT" config user.email t@e.com
1573
+ git -C "$PROJECT_ROOT" config user.name t
1574
+ mkdir -p "$PROJECT_ROOT/src/foo" "$PROJECT_ROOT/.project-docs/okstra"
1575
+ cat > "$PROJECT_ROOT/src/foo/handler.py" <<'PY'
1576
+ def handle(items): return items
1577
+ PY
1578
+ cat > "$PROJECT_ROOT/.project-docs/okstra/project.json" <<JSON
1579
+ { "projectId": "imp-brief-e2e", "projectRoot": "$PROJECT_ROOT" }
1580
+ JSON
1581
+ git -C "$PROJECT_ROOT" add -A
1582
+ git -C "$PROJECT_ROOT" commit -q -m seed
1583
+
1584
+ # Simulate okstra-brief codebase-scan output (the skill is interactive — this
1585
+ # scenario asserts that a HAND-WRITTEN brief with the required frontmatter is
1586
+ # accepted by the validator + render-bundle path; the interactive flow is
1587
+ # covered by unit tests on the wizard).
1588
+ BRIEF="$PROJECT_ROOT/.project-docs/okstra/briefs/refactor/imp-002-scan-foo.md"
1589
+ mkdir -p "$(dirname "$BRIEF")"
1590
+ cat > "$BRIEF" <<'MD'
1591
+ ---
1592
+ type: brief
1593
+ brief-id: imp-002-scan-foo
1594
+ parent-id: self
1595
+ ticket-id: ""
1596
+ source-type: user-input
1597
+ task-group: refactor
1598
+ depth: 0
1599
+ created: 2026-05-21
1600
+ generator: okstra-brief
1601
+ reporter-confirmations: complete
1602
+ scope: codebase
1603
+ priority-lenses: [readability, dx]
1604
+ scan-scope: [src/foo/]
1605
+ out-of-scope: []
1606
+ candidate-cap: 5
1607
+ ---
1608
+
1609
+ # Task Brief: refactor/imp-002-scan-foo
1610
+
1611
+ ## Context
1612
+ - New team member onboarding to src/foo/; surface readability and DX wins.
1613
+
1614
+ ## Desired Outcome
1615
+ - 5 candidates ranked by impact.
1616
+
1617
+ ## Constraints
1618
+ - None.
1619
+
1620
+ ## Scan Scope
1621
+ - src/foo/ — handler module.
1622
+
1623
+ ## Priority Lenses
1624
+ - readability — naming and structure.
1625
+ - dx — debugging and tooling.
1626
+
1627
+ ## Open Questions
1628
+ _(none)_
1629
+
1630
+ ## Augmentation
1631
+ _(none)_
1632
+ MD
1633
+
1634
+ # Required frontmatter assertions
1635
+ grep -q "^scope: codebase$" "$BRIEF" || { echo "FAIL: scope missing"; exit 1; }
1636
+ grep -q "^priority-lenses:" "$BRIEF" || { echo "FAIL: priority-lenses missing"; exit 1; }
1637
+ grep -q "^scan-scope:" "$BRIEF" || { echo "FAIL: scan-scope missing"; exit 1; }
1638
+ grep -q "^candidate-cap:" "$BRIEF" || { echo "FAIL: candidate-cap missing"; exit 1; }
1639
+
1640
+ echo "PASS: scenario-09-improvement-brief-generation"
1641
+ ```
1642
+
1643
+ - [ ] **Step 2: 실행**
1644
+
1645
+ Run:
1646
+ ```bash
1647
+ chmod +x tests-e2e/scenario-09-improvement-brief-generation.sh
1648
+ bash tests-e2e/scenario-09-improvement-brief-generation.sh
1649
+ ```
1650
+ Expected: `PASS: scenario-09-improvement-brief-generation`.
1651
+
1652
+ - [ ] **Step 3: commit**
1653
+
1654
+ ```bash
1655
+ git add tests-e2e/scenario-09-improvement-brief-generation.sh
1656
+ git commit -m "test(e2e): codebase-scan brief frontmatter shape"
1657
+ ```
1658
+
1659
+ ---
1660
+
1661
+ ## Milestone 3 — 마무리 검증
1662
+
1663
+ - [ ] **전체 회귀 + 새 e2e 둘 다 pass**
1664
+
1665
+ Run:
1666
+ ```bash
1667
+ python3 -m pytest tests/ -x
1668
+ bash tests-e2e/scenario-08-improvement-discovery-render-only.sh
1669
+ bash tests-e2e/scenario-09-improvement-brief-generation.sh
1670
+ ```
1671
+ Expected: 모두 pass.
1672
+
1673
+ - [ ] **PR 3 push**
1674
+
1675
+ ```bash
1676
+ git push origin feat/improvement-discovery
1677
+ ```
1678
+
1679
+ ---
1680
+
1681
+ # 수용 기준 (spec §11 미러)
1682
+
1683
+ 이 plan 의 모든 task 가 완료되면 다음이 모두 참:
1684
+
1685
+ - [ ] `node bin/okstra` 의 wizard 에서 `improvement-discovery` task-type 이 선택 가능.
1686
+ - [ ] `okstra-brief` 가 codebase-scan variant brief 를 생성 가능, frontmatter 에 `scope: codebase` 포함.
1687
+ - [ ] `okstra render-bundle --task-type improvement-discovery` 가 instruction-set 까지 생성.
1688
+ - [ ] `validators/validate-improvement-report.py` 가 11항목 모두 검증 + happy/sad path 테스트 통과.
1689
+ - [ ] `tests-e2e/scenario-08-...` 와 `scenario-09-...` 둘 다 통과.
1690
+ - [ ] 한글 docs (`README.kr.md`, `docs/kr/cli.md`, `docs/kr/architecture.md`) 가 improvement-discovery 를 설명.
1691
+ - [ ] `CHANGES.md` 에 `사용자 영향:` line 이 포함된 entry.