okstra 0.51.0 → 0.53.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 (44) hide show
  1. package/README.kr.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/kr/architecture.md +1 -0
  4. package/docs/kr/cli.md +2 -1
  5. package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +993 -0
  6. package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +93 -0
  7. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +447 -0
  8. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +289 -0
  9. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +774 -0
  10. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +303 -0
  11. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +387 -0
  12. package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +126 -0
  13. package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +180 -0
  14. package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +179 -0
  15. package/package.json +1 -1
  16. package/runtime/BUILD.json +2 -2
  17. package/runtime/agents/workers/report-writer-worker.md +1 -0
  18. package/runtime/bin/lib/okstra/cli.sh +5 -1
  19. package/runtime/bin/okstra.sh +1 -0
  20. package/runtime/prompts/launch.template.md +1 -0
  21. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  22. package/runtime/prompts/profiles/_implementation-executor.md +16 -9
  23. package/runtime/prompts/profiles/_implementation-verifier.md +4 -1
  24. package/runtime/prompts/profiles/final-verification.md +7 -7
  25. package/runtime/prompts/profiles/implementation-planning.md +14 -7
  26. package/runtime/prompts/wizard/prompts.ko.json +3 -2
  27. package/runtime/python/okstra_ctl/analysis_packet.py +14 -2
  28. package/runtime/python/okstra_ctl/render.py +3 -0
  29. package/runtime/python/okstra_ctl/run.py +541 -41
  30. package/runtime/python/okstra_ctl/wizard.py +25 -7
  31. package/runtime/python/okstra_ctl/worktree.py +126 -9
  32. package/runtime/python/okstra_ctl/worktree_registry.py +88 -17
  33. package/runtime/schemas/final-report-v1.0.schema.json +36 -0
  34. package/runtime/skills/okstra-convergence/SKILL.md +14 -3
  35. package/runtime/skills/okstra-memory/SKILL.md +28 -5
  36. package/runtime/skills/okstra-run/SKILL.md +1 -1
  37. package/runtime/templates/reports/final-report.template.md +12 -0
  38. package/runtime/templates/reports/final-verification-input.template.md +8 -5
  39. package/runtime/templates/reports/i18n/en.json +3 -1
  40. package/runtime/templates/reports/i18n/ko.json +3 -1
  41. package/runtime/validators/validate-implementation-plan-stages.py +57 -11
  42. package/runtime/validators/validate-run.py +143 -1
  43. package/runtime/validators/validate-workflow.sh +6 -1
  44. package/src/memory.mjs +50 -11
@@ -0,0 +1,387 @@
1
+ # Stage Worktree 격리 P5 — 다중 의존 stage 자동 base 감지 구현 계획
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:** P3에서 `PrepareError`로 거부하던 다중 의존 stage(`depends-on X,Y…`)를 spec §9 옵션 A로 자동 처리한다 — task-key worktree HEAD를 candidate로 두고, 선행 stage들의 done commit이 모두 candidate의 ancestor면(=사용자가 머지함) candidate를 base로 발급, 아니면 머지 안내 `PrepareError`.
6
+
7
+ **Architecture:** 새 pure 헬퍼 `_commit_is_ancestor(project_root, ancestor, descendant)`가 `git merge-base --is-ancestor`를 감싼다. `_resolve_stage_base_commit`의 다중 의존 분기를 거부 → ancestor 검증으로 교체하고, 검증에 필요한 `candidate_base`/`project_root`를 새 인자로 받는다. `_reserve_implementation_stages`는 이미 계산한 task-key worktree HEAD(`head_sha`)를 candidate로, `inp.project_root`를 함께 넘긴다.
8
+
9
+ **Tech Stack:** Python 3 (`scripts/okstra_ctl/run.py`), pytest (real `git init` repos).
10
+
11
+ 설계 문서: [docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md](../specs/2026-06-06-stage-worktree-isolation-design.md) §2.2 / §9 #1
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ - Modify: `scripts/okstra_ctl/run.py`
18
+ - 신규 `_commit_is_ancestor()` (모듈 helper, `_resolve_stage_base_commit` 직전)
19
+ - `_resolve_stage_base_commit()` — 다중 의존 분기 교체 + 인자 2개 추가
20
+ - `_reserve_implementation_stages()` — `_resolve_stage_base_commit` 호출에 candidate/project_root 전달
21
+ - Test:
22
+ - `tests/test_okstra_run_stage_base.py` (기존 — 다중 의존 케이스 교체 + ancestor 케이스 추가)
23
+ - `tests/test_okstra_run_reserve_implementation_stages.py` (기존 — 다중 의존 거부 테스트를 머지/미머지 두 케이스로)
24
+
25
+ ---
26
+
27
+ ### Task 1: `_commit_is_ancestor` 헬퍼 + `_resolve_stage_base_commit` 다중 의존 자동 감지
28
+
29
+ **Files:**
30
+ - Modify: `scripts/okstra_ctl/run.py`
31
+ - Test: `tests/test_okstra_run_stage_base.py`
32
+
33
+ - [ ] **Step 1: Replace the multi-dependency tests in `test_okstra_run_stage_base.py`**
34
+
35
+ 기존 `test_multi_dependency_rejected_with_guidance` 함수를 삭제하고, 그 자리에 아래 4개를 넣는다. 파일 상단 import 에 `import subprocess` 와 `from pathlib import Path` 가 이미 있는지 확인하고 없으면 추가.
36
+
37
+ ```python
38
+ def _init_repo_with_commits(repo: Path):
39
+ """3개 커밋 c0→c1→c2 선형 히스토리를 만들고 (c0, c1, c2) sha 를 반환."""
40
+ subprocess.run(["git", "init", "-q", str(repo)], check=True)
41
+ subprocess.run(["git", "-C", str(repo), "config", "user.email", "t@e"], check=True)
42
+ subprocess.run(["git", "-C", str(repo), "config", "user.name", "t"], check=True)
43
+ shas = []
44
+ for i in range(3):
45
+ (repo / f"f{i}").write_text(f"{i}\n")
46
+ subprocess.run(["git", "-C", str(repo), "add", "."], check=True)
47
+ subprocess.run(["git", "-C", str(repo), "commit", "-q", "-m", f"c{i}"], check=True)
48
+ shas.append(subprocess.run(
49
+ ["git", "-C", str(repo), "rev-parse", "HEAD"],
50
+ capture_output=True, text=True, check=True).stdout.strip())
51
+ return shas # [c0, c1, c2]
52
+
53
+
54
+ def test_multi_dependency_returns_candidate_when_all_predecessors_ancestors(tmp_path):
55
+ repo = tmp_path / "repo"
56
+ c0, c1, c2 = _init_repo_with_commits(repo)
57
+ stage = _stage(4, [2, 3])
58
+ # 선행 2,3 의 done commit 이 c0,c1 — candidate c2 의 ancestor (선형 히스토리)
59
+ rows = [
60
+ {"stage": 2, "status": "done", "head_commit": c0},
61
+ {"stage": 3, "status": "done", "head_commit": c1},
62
+ ]
63
+ got = _resolve_stage_base_commit(
64
+ stage, consumer_done_rows=rows, anchor_base_commit="unused",
65
+ candidate_base=c2, project_root=repo,
66
+ )
67
+ assert got == c2
68
+
69
+
70
+ def test_multi_dependency_rejected_when_predecessor_not_merged(tmp_path):
71
+ repo = tmp_path / "repo"
72
+ c0, c1, c2 = _init_repo_with_commits(repo)
73
+ # 별도 분기 커밋 (candidate 의 ancestor 가 아님)
74
+ subprocess.run(["git", "-C", str(repo), "checkout", "-q", "-b", "side", c0], check=True)
75
+ (repo / "side").write_text("x\n")
76
+ subprocess.run(["git", "-C", str(repo), "add", "."], check=True)
77
+ subprocess.run(["git", "-C", str(repo), "commit", "-q", "-m", "side"], check=True)
78
+ side_sha = subprocess.run(
79
+ ["git", "-C", str(repo), "rev-parse", "HEAD"],
80
+ capture_output=True, text=True, check=True).stdout.strip()
81
+ stage = _stage(4, [2, 3])
82
+ rows = [
83
+ {"stage": 2, "status": "done", "head_commit": c1}, # ancestor of c2 ✓
84
+ {"stage": 3, "status": "done", "head_commit": side_sha}, # NOT ancestor of c2 ✗
85
+ ]
86
+ with pytest.raises(PrepareError, match="not merged into the task worktree"):
87
+ _resolve_stage_base_commit(
88
+ stage, consumer_done_rows=rows, anchor_base_commit="unused",
89
+ candidate_base=c2, project_root=repo,
90
+ )
91
+
92
+
93
+ def test_multi_dependency_rejected_when_predecessor_not_done(tmp_path):
94
+ repo = tmp_path / "repo"
95
+ c0, c1, c2 = _init_repo_with_commits(repo)
96
+ stage = _stage(4, [2, 3])
97
+ rows = [{"stage": 2, "status": "done", "head_commit": c0}] # stage 3 done 없음
98
+ with pytest.raises(PrepareError, match="predecessor stage 3 has no done"):
99
+ _resolve_stage_base_commit(
100
+ stage, consumer_done_rows=rows, anchor_base_commit="unused",
101
+ candidate_base=c2, project_root=repo,
102
+ )
103
+
104
+
105
+ def test_multi_dependency_rejected_without_candidate(tmp_path):
106
+ repo = tmp_path / "repo"
107
+ c0, c1, c2 = _init_repo_with_commits(repo)
108
+ stage = _stage(4, [2, 3])
109
+ rows = [
110
+ {"stage": 2, "status": "done", "head_commit": c0},
111
+ {"stage": 3, "status": "done", "head_commit": c1},
112
+ ]
113
+ with pytest.raises(PrepareError, match="candidate base missing"):
114
+ _resolve_stage_base_commit(
115
+ stage, consumer_done_rows=rows, anchor_base_commit="unused",
116
+ candidate_base="", project_root=repo,
117
+ )
118
+ ```
119
+
120
+ - [ ] **Step 2: Run test to verify it fails**
121
+
122
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_run_stage_base.py -v`
123
+ Expected: 신규 4개 FAIL — `_resolve_stage_base_commit()` got unexpected keyword `candidate_base` (현재 시그니처에 없음). 기존 독립/단일 의존 테스트는 PASS 유지.
124
+
125
+ - [ ] **Step 3: Add `_commit_is_ancestor` helper + rewrite multi-dep branch**
126
+
127
+ `run.py`에서 `_resolve_stage_base_commit` 정의 **바로 위**에 헬퍼 추가:
128
+
129
+ ```python
130
+ def _commit_is_ancestor(project_root, ancestor: str, descendant: str) -> bool:
131
+ """True iff `ancestor` is an ancestor of `descendant` in project_root's git
132
+ history (`git merge-base --is-ancestor`, exit 0). Used to detect whether a
133
+ predecessor stage's done commit has been merged into the task worktree HEAD."""
134
+ r = _subprocess.run(
135
+ ["git", "merge-base", "--is-ancestor", ancestor, descendant],
136
+ cwd=str(project_root), capture_output=True, text=True,
137
+ )
138
+ return r.returncode == 0
139
+ ```
140
+
141
+ 그리고 `_resolve_stage_base_commit`를 아래로 교체 (시그니처 + 다중 의존 분기):
142
+
143
+ ```python
144
+ def _resolve_stage_base_commit(
145
+ stage: dict,
146
+ consumer_done_rows: list,
147
+ anchor_base_commit: str,
148
+ candidate_base: str = "",
149
+ project_root=None,
150
+ ) -> str:
151
+ """Pick the git base commit a stage's isolated worktree branches from.
152
+
153
+ - 독립 (`depends-on (none)`): anchor (= implementation_base_commit fixed
154
+ at first stage entry).
155
+ - 단일 의존 (`depends-on X`): predecessor X 의 `done.head_commit`.
156
+ - 다중 의존 (`depends-on X,Y…`): spec §9 옵션 A 자동 감지. candidate_base
157
+ (= task-key worktree HEAD) 에 모든 선행 done commit 이 ancestor 면
158
+ candidate 를 반환(사용자가 선행을 머지함). 아니면 PrepareError.
159
+
160
+ Raises PrepareError on missing predecessor / anchor / unmerged predecessor."""
161
+ deps = stage.get("depends_on") or []
162
+ if len(deps) >= 2:
163
+ n = stage["stage_number"]
164
+ # 1) 모든 선행의 done head_commit 수집
165
+ pred_commits = {}
166
+ for d in deps:
167
+ head = next(
168
+ (r.get("head_commit") for r in consumer_done_rows
169
+ if r.get("stage") == d and r.get("status") == "done"
170
+ and r.get("head_commit")),
171
+ None,
172
+ )
173
+ if not head:
174
+ raise PrepareError(
175
+ f"predecessor stage {d} has no done row with head_commit "
176
+ f"in consumers.jsonl; multi-dependency stage {n} cannot start"
177
+ )
178
+ pred_commits[d] = head
179
+ # 2) candidate (task-key worktree HEAD) 필요
180
+ if not candidate_base or project_root is None:
181
+ raise PrepareError(
182
+ f"candidate base missing for multi-dependency stage {n}; "
183
+ "task-key worktree HEAD could not be resolved"
184
+ )
185
+ # 3) 모든 선행 done 이 candidate 의 ancestor 인지 (=사용자가 머지함)
186
+ for d, head in pred_commits.items():
187
+ if not _commit_is_ancestor(project_root, head, candidate_base):
188
+ raise PrepareError(
189
+ f"multi-dependency stage {n}: predecessor stage {d} "
190
+ f"({head[:8]}) is not merged into the task worktree "
191
+ f"({candidate_base[:8]}). Merge stage branches "
192
+ f"(e.g. the `-s{d}` branches) into the task worktree "
193
+ "(or into main, then refresh the worktree) and retry."
194
+ )
195
+ return candidate_base
196
+ if not deps:
197
+ if not anchor_base_commit:
198
+ raise PrepareError(
199
+ f"anchor base commit missing for independent stage "
200
+ f"{stage['stage_number']}; first-stage prepare should have "
201
+ "fixed it via worktree_registry.set_implementation_base"
202
+ )
203
+ return anchor_base_commit
204
+ # 단일 의존
205
+ pred = deps[0]
206
+ for row in consumer_done_rows:
207
+ if row.get("stage") == pred and row.get("status") == "done":
208
+ head = row.get("head_commit") or ""
209
+ if head:
210
+ return head
211
+ raise PrepareError(
212
+ f"predecessor stage {pred} has no done row with head_commit in "
213
+ "consumers.jsonl; cannot derive base for stage "
214
+ f"{stage['stage_number']}"
215
+ )
216
+ ```
217
+
218
+ - [ ] **Step 4: Run test to verify it passes**
219
+
220
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_run_stage_base.py -v`
221
+ Expected: PASS (독립/단일 기존 + 다중 의존 신규 4개).
222
+
223
+ - [ ] **Step 5: Commit**
224
+
225
+ ```bash
226
+ cd /Volumes/Workspaces/workspace/projects/Okstra
227
+ git add scripts/okstra_ctl/run.py tests/test_okstra_run_stage_base.py
228
+ git commit -m "feat(run): auto-detect multi-dependency stage base via ancestor check"
229
+ ```
230
+
231
+ ---
232
+
233
+ ### Task 2: `_reserve_implementation_stages` — candidate/project_root 전달 + e2e
234
+
235
+ **Files:**
236
+ - Modify: `scripts/okstra_ctl/run.py` (`_reserve_implementation_stages`의 `_resolve_stage_base_commit` 호출)
237
+ - Test: `tests/test_okstra_run_reserve_implementation_stages.py`
238
+
239
+ - [ ] **Step 1: Replace the multi-dep e2e test with merged/unmerged cases**
240
+
241
+ 기존 `test_multi_dependency_stage_prepare_rejected` 함수를 삭제하고 아래 2개로 교체. (helper `_init_repo`, `_stage`, `_Inp`, `_consumers_dir` 는 그대로 사용.)
242
+
243
+ ```python
244
+ def test_multi_dependency_starts_when_predecessors_merged(tmp_path):
245
+ """선행 2,3 의 done commit 이 task worktree HEAD 의 ancestor 면 다중 의존
246
+ stage 4 가 그 HEAD 를 base 로 발급된다."""
247
+ repo = tmp_path / "repo"
248
+ base_sha = _init_repo(repo) # c0 (현재 HEAD)
249
+ plan_path = _consumers_dir(tmp_path)
250
+
251
+ from okstra_ctl.run import _reserve_implementation_stages
252
+ from okstra_ctl.worktree_registry import reserve as registry_reserve, lookup
253
+ from okstra_ctl.consumers import append_consumer
254
+ registry_reserve(
255
+ project_id="proj", task_group="grp", task_id="tid",
256
+ worktree_path=str(repo), branch="feat-tid", base_ref=base_sha,
257
+ )
258
+ # 선행 2,3 done — head_commit 을 base_sha(=현재 HEAD) 로 두면 HEAD 의 ancestor(자기 자신).
259
+ plan_run_root = Path(plan_path).resolve().parents[1]
260
+ append_consumer(plan_run_root, impl_task_key="proj/grp/tid",
261
+ stage=2, status="done", head_commit=base_sha)
262
+ append_consumer(plan_run_root, impl_task_key="proj/grp/tid",
263
+ stage=3, status="done", head_commit=base_sha)
264
+
265
+ ctx = {
266
+ "TASK_KEY": "proj/grp/tid",
267
+ "TASK_GROUP_SEGMENT": "grp", "TASK_ID_SEGMENT": "tid",
268
+ "EXECUTOR_WORKTREE_PATH": str(repo),
269
+ "EXECUTOR_WORKTREE_BRANCH": "feat-tid",
270
+ "EXECUTOR_WORKTREE_BASE_REF": base_sha,
271
+ "EXECUTOR_WORKTREE_STATUS": "created",
272
+ }
273
+ stages = [_stage(2, []), _stage(3, []), _stage(4, [2, 3])]
274
+ _reserve_implementation_stages(
275
+ _Inp(project_root=repo, approved_plan_path=str(plan_path), stage="4"),
276
+ ctx, stages,
277
+ )
278
+ assert ctx["EXECUTOR_WORKTREE_BRANCH"] == "feat-tid-s4"
279
+ assert lookup("proj", "grp", "tid", stage_number=4) is not None
280
+
281
+
282
+ def test_multi_dependency_rejected_when_not_merged(tmp_path):
283
+ """선행 done commit 이 task worktree HEAD 의 ancestor 가 아니면(미머지)
284
+ 다중 의존 stage 가 PrepareError 로 거부된다."""
285
+ repo = tmp_path / "repo"
286
+ base_sha = _init_repo(repo)
287
+ plan_path = _consumers_dir(tmp_path)
288
+
289
+ from okstra_ctl.run import _reserve_implementation_stages, PrepareError
290
+ from okstra_ctl.worktree_registry import reserve as registry_reserve
291
+ from okstra_ctl.consumers import append_consumer
292
+ registry_reserve(
293
+ project_id="proj", task_group="grp", task_id="tid",
294
+ worktree_path=str(repo), branch="feat-tid", base_ref=base_sha,
295
+ )
296
+ plan_run_root = Path(plan_path).resolve().parents[1]
297
+ # 존재하지 않는/분기된 commit sha → HEAD 의 ancestor 아님
298
+ append_consumer(plan_run_root, impl_task_key="proj/grp/tid",
299
+ stage=2, status="done", head_commit=base_sha)
300
+ append_consumer(plan_run_root, impl_task_key="proj/grp/tid",
301
+ stage=3, status="done",
302
+ head_commit="0000000000000000000000000000000000000000")
303
+
304
+ ctx = {
305
+ "TASK_KEY": "proj/grp/tid",
306
+ "TASK_GROUP_SEGMENT": "grp", "TASK_ID_SEGMENT": "tid",
307
+ "EXECUTOR_WORKTREE_PATH": str(repo),
308
+ "EXECUTOR_WORKTREE_BRANCH": "feat-tid",
309
+ "EXECUTOR_WORKTREE_BASE_REF": base_sha,
310
+ "EXECUTOR_WORKTREE_STATUS": "created",
311
+ }
312
+ stages = [_stage(2, []), _stage(3, []), _stage(4, [2, 3])]
313
+ with pytest.raises(PrepareError, match="not merged into the task worktree"):
314
+ _reserve_implementation_stages(
315
+ _Inp(project_root=repo, approved_plan_path=str(plan_path), stage="4"),
316
+ ctx, stages,
317
+ )
318
+ ```
319
+
320
+ - [ ] **Step 2: Run test to verify it fails**
321
+
322
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_run_reserve_implementation_stages.py -v`
323
+ Expected: `test_multi_dependency_starts_when_predecessors_merged` FAILS (현재 `_reserve_implementation_stages`가 candidate/project_root 를 전달하지 않아 `_resolve_stage_base_commit`이 `candidate base missing` 으로 거부). `test_multi_dependency_rejected_when_not_merged` 는 이유는 다르지만(역시 candidate missing) 일단 raise 하므로 match 가 어긋나 FAIL 가능.
324
+
325
+ - [ ] **Step 3: Pass candidate_base + project_root in the call**
326
+
327
+ `_reserve_implementation_stages` 내부의 `_resolve_stage_base_commit(...)` 호출(현재):
328
+
329
+ ```python
330
+ stage_base = _resolve_stage_base_commit(
331
+ selected_stage, consumer_done_rows, anchor_base_commit=anchor,
332
+ )
333
+ ```
334
+
335
+ 를 아래로 교체 (이미 함수 안에서 `head_sha`= task-key worktree HEAD 를 계산해 둠 — 그것을 candidate 로):
336
+
337
+ ```python
338
+ stage_base = _resolve_stage_base_commit(
339
+ selected_stage, consumer_done_rows, anchor_base_commit=anchor,
340
+ candidate_base=head_sha, project_root=Path(inp.project_root),
341
+ )
342
+ ```
343
+
344
+ > 참고: `head_sha`는 anchor 고정 블록에서 `git rev-parse HEAD`(in `inp.project_root`)로 이미 계산된 변수다. 다중 의존 stage 시점에는 사용자가 선행 stage 들을 worktree 에 머지했다면 이 HEAD 가 선행 done 들을 ancestor 로 가진다. 독립/단일 의존 경로는 candidate 를 쓰지 않으므로 무영향.
345
+
346
+ - [ ] **Step 4: Run target tests**
347
+
348
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_run_reserve_implementation_stages.py -v`
349
+ Expected: PASS (기존 + 신규 2개).
350
+
351
+ - [ ] **Step 5: Full regression**
352
+
353
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/ -q`
354
+ Expected: PASS. 회귀 시 BLOCKED.
355
+
356
+ - [ ] **Step 6: Commit**
357
+
358
+ ```bash
359
+ cd /Volumes/Workspaces/workspace/projects/Okstra
360
+ git add scripts/okstra_ctl/run.py tests/test_okstra_run_reserve_implementation_stages.py
361
+ git commit -m "feat(run): wire multi-dependency base auto-detection into stage reservation"
362
+ ```
363
+
364
+ ---
365
+
366
+ ## Self-Review
367
+
368
+ **Spec coverage:**
369
+ - §9 #1 자동 감지: candidate=task-key HEAD, 선행 done 모두 ancestor → candidate, 아니면 PrepareError → Task 1 (헬퍼+분기) + Task 2 (wire-up) ✓
370
+ - §2.2 다중 의존 행 갱신 → 구현이 그 의미 ✓
371
+ - 옵션 B(octopus) → 비범위 유지 ✓
372
+
373
+ **Placeholder scan:** 없음 — 전 step 실제 코드.
374
+
375
+ **Type consistency:** `_commit_is_ancestor(project_root, ancestor, descendant) -> bool`; `_resolve_stage_base_commit(..., candidate_base="", project_root=None)`; 호출부가 `candidate_base=head_sha, project_root=Path(inp.project_root)`. P3에서 단일 의존/독립이 candidate 를 안 쓰므로 그 경로 무영향.
376
+
377
+ **위험 플래그(구현자 주의):**
378
+ - Task 2 e2e 는 실제 `git init` repo 의 HEAD(`base_sha`)를 선행 done commit 으로도 쓴다 — commit 은 자기 자신의 ancestor 이므로(`git merge-base --is-ancestor X X` exit 0) "머지됨" 케이스가 성립한다. 미머지 케이스는 all-zero sha 로 ancestor 검증을 실패시킨다(`merge-base --is-ancestor` 가 비존재 sha 에 비0 반환).
379
+ - `_resolve_stage_base_commit` 의 단일 의존/독립 분기는 **변경 금지** — 다중 의존 분기와 시그니처 인자 추가만.
380
+
381
+ ## 검증 (P5 완료 기준)
382
+
383
+ ```bash
384
+ cd /Volumes/Workspaces/workspace/projects/Okstra
385
+ python3 -m pytest tests/test_okstra_run_stage_base.py tests/test_okstra_run_reserve_implementation_stages.py -v
386
+ python3 -m pytest tests/ -q
387
+ ```
@@ -0,0 +1,126 @@
1
+ # final-verification 전체-task 게이트 설계
2
+
3
+ - 작성일: 2026-06-06
4
+ - 상태: 설계 승인됨 (사용자 승인 2026-06-06)
5
+ - 선행: [stage-worktree-isolation-design.md](2026-06-06-stage-worktree-isolation-design.md) (P1–P5), [final-verification-protocol-hardening](../plans/2026-06-02-final-verification-protocol-hardening.md)
6
+
7
+ ## 1. 배경 / 문제
8
+
9
+ `final-verification` 은 lifecycle 의 마지막 acceptance gate 다 — "완료된 작업의 잔존 결함·회귀 위험을 점검하고 release 판단" ([architecture.md:348](../../kr/architecture.md:348)). 개념상 **모든 stage 가 구현·머지된 뒤 한 번** 도는 게 맞고, 검증 메커니즘인 `git diff <implementation-base>..HEAD` ([final-verification.md:28](../../../prompts/profiles/final-verification.md:28)) 도 base 를 전체 anchor 로 잡으면 task 전체를 한 번에 커버할 수 있다.
10
+
11
+ 그러나 현재 entry gate 는 **단일 implementation report** 를 전제로 작성돼 있다:
12
+
13
+ - brief 가 **the** originating implementation final-report 1개를 인용해야 하고 ([final-verification.md:26](../../../prompts/profiles/final-verification.md:26)),
14
+ - "current checkout 이 그 report 의 commit list / diff summary 와 불일치하면 `blocked`" ([final-verification.md:29](../../../prompts/profiles/final-verification.md:29)).
15
+
16
+ stage-worktree-isolation 모델에서는 **한 run = 한 stage** ([stage-worktree-isolation-design.md:51](2026-06-06-stage-worktree-isolation-design.md:51)) 이므로 stage N개 → implementation report N개가 생긴다 ([_implementation-deliverable.md:53](../../../prompts/profiles/_implementation-deliverable.md:53)). 마지막에 한 번 돌릴 때 brief 가 그중 하나만 인용하면, 사용자가 모든 stage 를 머지한 task-key worktree HEAD 에는 다른 stage 의 commit 이 더 있어 line 29 의 단일-report 일치 검사가 mismatch → `blocked` 위험이 있다.
17
+
18
+ 즉 "전체-task 단일 게이트" 라는 의도와 "단일 report 전제" 라는 현재 계약이 정합되지 않은 상태다. 본 설계는 이를 해소하고, 추가로 **특정 run(stage)만 단독 검증** 하는 모드를 명시적으로 제공한다.
19
+
20
+ ## 2. 목표 / 비목표
21
+
22
+ 목표:
23
+ - final-verification 이 **두 모드**로 동작: (A) 전체-task 모드(기본), (B) 단독-stage 모드.
24
+ - 검증 target(base / HEAD / worktree / stage 목록)을 registry + `consumers.jsonl` + 승인 plan 의 Stage Map 에서 **자동 해소**한다. brief 의 worktree·base 수동 입력 제거.
25
+ - entry gate 가 "전부 준비됨" 을 **실제로 강제**한다(선언이 아니라 enforcement).
26
+
27
+ 비목표:
28
+ - 자동 stage 머지(여전히 사용자 수동 머지 — [stage-worktree-isolation-design.md:7](2026-06-06-stage-worktree-isolation-design.md:7)).
29
+ - octopus 머지 base 생성(옵션 B, 보류 유지).
30
+ - implementation / 다른 phase 의 worktree 모델 변경.
31
+
32
+ ## 3. 두 검증 모드
33
+
34
+ `final-verification` 에 `--stage auto|N` 인수 추가(implementation 과 대칭). 격리 모델에서 `1 run = 1 stage` 이므로 "특정 run" ≡ "특정 stage" 다. 모드는 report data.json 의 `verificationScope` 필드로 표기: `whole-task`(전체) | `single-stage`(단독). (기존 convergence 의 `verificationMode` "lightweight"/"full-reanalysis" 와는 별개 필드 — 토큰 충돌 회피.)
35
+
36
+ | 항목 | (A) 전체-task 모드 (`--stage auto`, 기본) | (B) 단독-stage 모드 (`--stage N`) |
37
+ |---|---|---|
38
+ | worktree | task-key worktree (모든 stage 머지된 공유본) | stage-key worktree `…/stage-N/` (registry 키 `<task-key>#stage-N`) |
39
+ | base | registry `implementation_base_commit` (전체 anchor) | registry stage-key row `base_ref` (stage N 격리 base) |
40
+ | HEAD | task-key worktree `git rev-parse HEAD` | stage-N worktree `git rev-parse HEAD` (= consumers done `head_commit`) |
41
+ | stage 범위 | Stage Map 전부 | stage N 단독 |
42
+ | gate: done | **모든** Stage Map stage 가 consumers `status:done` | **stage N** 만 consumers `status:done` |
43
+ | gate: 머지 | 모든 done stage `head_commit` 이 HEAD 의 ancestor | 해당 없음(격리 worktree, 머지 불요) |
44
+ | gate: clean | task-key worktree 가 `.okstra/` 제외 clean (§5) | stage-N worktree 가 `.okstra/` 제외 clean |
45
+ | routing | `release-handoff` 허용(verdict `accepted` 시) | `release-handoff` **금지** — `implementation` / `done` 등으로 제한 |
46
+
47
+ - **전체-task 모드** = 사용자가 의도한 "전부 머지된 뒤 마지막 한 번". release-handoff 로 가는 정식 게이트.
48
+ - **단독-stage 모드** = stage 구현 직후 머지 전, 격리 worktree 에서 그 stage 만 검증. 다른 stage 의 done/머지 여부와 무관. 부분 검증이므로 `accepted` 여도 release-handoff 로 진입 불가.
49
+
50
+ ## 4. target 자동 해소
51
+
52
+ | 요소 | 출처 |
53
+ |---|---|
54
+ | base (전체 모드) | `worktree_registry.get_implementation_base(project_id, task_group, task_id)` ([worktree_registry.py:222](../../../scripts/okstra_ctl/worktree_registry.py:222)) |
55
+ | base (단독 모드) | registry stage-key row `<task-key>#stage-N` 의 `base_ref` ([stage-worktree-isolation-design.md:78](2026-06-06-stage-worktree-isolation-design.md:78)) |
56
+ | worktree / HEAD | 동일 registry row 의 worktree 경로 → 그 worktree 의 `git rev-parse HEAD` |
57
+ | 전체 stage 목록 | 승인 plan 의 `## 5.5 Stage Map` 파싱 — `_parse_stage_map` 재사용 ([validate-implementation-plan-stages.py:55](../../../validators/validate-implementation-plan-stages.py:55), run.py 측 `_parse_stage_map_into_ctx` [run.py:381](../../../scripts/okstra_ctl/run.py:381)) |
58
+ | done stage + 각 `head_commit` | `consumers.read_consumers(plan_run_root)` 의 `status:done` 행 ([consumers.py:22](../../../scripts/okstra_ctl/consumers.py:22)) |
59
+ | stage 별 implementation report 경로(참고자료) | consumers done 행의 새 필드 `report_path` (§6) |
60
+
61
+ registry / consumers 는 `~/.okstra/` 아래 okstra artifact 라 read 허용 범위 안이다(artifact-home 규칙 위배 아님). brief 는 worktree·base 를 더 이상 손으로 적지 않는다.
62
+
63
+ ## 5. 강제 위치 — Python prep fail-fast
64
+
65
+ target 해소와 gate 검사(done · merge-ancestor · clean)는 전부 결정적 데이터·git 연산이므로 **lead 런타임이 아니라** [run.py](../../../scripts/okstra_ctl/run.py) 의 bundle prep 단계에서 수행하고, 위반 시 `PrepareError` 로 막는다. implementation 의 `_resolve_stage_base_commit` PrepareError 패턴과 동일하다 ([run.py:307](../../../scripts/okstra_ctl/run.py:307)).
66
+
67
+ - "선언(MUST)" 과 "강제(PrepareError)" 가 분리되지 않는다(전역 원칙 #3).
68
+ - 해소된 snapshot 은 launch 템플릿에 `{{VERIFICATION_TARGET}}` 블록으로 주입 — implementation 의 `{{STAGE_BATCH_DIRECTIVE}}` 주입과 대칭 ([launch.template.md:18](../../../prompts/launch.template.md:18)). 모든 analyser 가 동일 target snapshot(worktree / base / head / diff stat / mode / stage 목록)을 받는다.
69
+ - profile 의 entry gate 텍스트는 prep 가 보장하는 불변식을 **서술**하는 역할로 남고, 검사 책임은 Python 으로 단일화한다.
70
+
71
+ **clean 검사 범위 — `.okstra/` 제외.** "검증한 HEAD = 릴리스될 상태" 를 보장하되, okstra 가 검증 중 만드는 자체 산출물(`.okstra/**` 아래 report·manifest·worktree 메타 등 untracked/변경)이 게이트를 거짓으로 막지 않도록, clean 판정은 **소스 트리 한정**한다: `git status --short -- . ':(exclude).okstra'` 출력이 비어있으면 clean. 즉 `.okstra/` 밖의 staged / unstaged / untracked 변경이 하나라도 있으면 dirty → block. (artifact-home 규칙상 `.okstra/` 만이 okstra 메모리이므로, 그 밖의 미커밋 변경은 검증 누락 위험으로 간주.)
72
+
73
+ PrepareError 메시지(actionable):
74
+ - 미완료 stage: `"final-verification(whole-task): stage {N} not done — run implementation --stage {N} first"`.
75
+ - 미머지 stage: `"final-verification(whole-task): stage {N} done commit {sha} not merged into task worktree HEAD — merge stage branches then retry"`.
76
+ - dirty worktree: `"final-verification: worktree has uncommitted source changes (outside .okstra/) — commit or stash before verifying"`.
77
+
78
+ ## 6. consumers 행 확장 — `report_path`
79
+
80
+ 단독-stage 모드가 stage report 를 참고자료로 인용하고, 전체-task 모드가 stage report 목록을 Source Implementation Report 에 나열하려면, done 행이 그 stage 의 final-report 경로를 가져야 한다.
81
+
82
+ - `append_consumer(...)` 는 이미 `**fields` 를 받으므로 시그니처 변경 불필요 ([consumers.py:35](../../../scripts/okstra_ctl/consumers.py:35)). done 행 append 시 `report_path` 를 추가 필드로 전달.
83
+ - 기록 책임: lead post-stage persistence — done 행 append 지점 ([_implementation-deliverable.md:52](../../../prompts/profiles/_implementation-deliverable.md:52)) 에 `report_path` 를 포함하도록 계약 갱신.
84
+ - 읽기: `read_consumers` 가 dict 를 그대로 돌려주므로 소비 측에서 `row.get("report_path")` 로 읽는다(없으면 None — pre-1.0 라 호환 shim 없이, 새 필드 부재 시 참고자료 섹션만 비움).
85
+
86
+ ## 7. report / 라우팅 계약
87
+
88
+ - **Source Implementation Report** ([final-verification.md:31](../../../prompts/profiles/final-verification.md:31)): 단일 경로 → **목록**으로 확장. 전체-task 모드는 검증에 포함된 모든 done stage 의 `report_path` 와 stage 번호, 그리고 해소된 base/head SHA·worktree·diff stat·`verificationScope` 를 인용. 단독-stage 모드는 stage N 의 report 1개.
89
+ - **routing**: `release-handoff` 추천은 **전체-task 모드 + verdict `accepted`** 일 때만 허용. 단독-stage 모드는 `accepted` 여도 routing 을 `implementation` / `done` 으로 제한. (release-handoff 진입 게이트의 `accepted` 강제는 기존대로 유지 — validate-run 정적 매핑 `final-verification: pending-release-handoff` [validate-run.py:67](../../../validators/validate-run.py:67).)
90
+
91
+ ## 8. 영향 범위
92
+
93
+ | 파일 | 변경 |
94
+ |---|---|
95
+ | [run.py](../../../scripts/okstra_ctl/run.py) | final-verification target 해소 + gate(PrepareError) + `--stage auto\|N` 분기. `get_implementation_base` / `_parse_stage_map_into_ctx` / `read_consumers` 재사용. `{{VERIFICATION_TARGET}}` ctx 주입 |
96
+ | [consumers.py](../../../scripts/okstra_ctl/consumers.py:35) | done 행에 `report_path` 기록(write 호출부) — 시그니처는 `**fields` 라 불변 |
97
+ | [_implementation-deliverable.md:52](../../../prompts/profiles/_implementation-deliverable.md:52) | lead post-stage persistence: done 행에 `report_path` 포함 계약 |
98
+ | [final-verification.md:25](../../../prompts/profiles/final-verification.md:25) | entry gate 재작성(두 모드, prep 가 강제함을 서술), Source Implementation Report → 목록, routing 제약(release-handoff = 전체-task 한정) |
99
+ | [launch.template.md:18](../../../prompts/launch.template.md:18) | `{{VERIFICATION_TARGET}}` 주입 지점 |
100
+ | [wizard.py](../../../scripts/okstra_ctl/wizard.py:1204) | final-verification 서브플로우에 stage 3-옵션 picker(추천=전체 `auto` / 특정 stage N / 직접 입력) 노출. `selected_stage` 를 final-verification 에도 emit ([wizard.py:2353](../../../scripts/okstra_ctl/wizard.py:2353)). brief 단계에서 worktree·base 수동 입력 제거 |
101
+ | [validate-run.py](../../../validators/validate-run.py) | release-handoff routing 은 `verificationScope=whole-task` 일 때만, Source Implementation Report 다중 항목 허용, 새 필드(`verificationScope`, stage 목록) 검증 |
102
+ | templates / schema | report data.json 필드(`verificationScope`, 검증 stage 목록), [final-verification-input.template.md](../../../templates/reports/final-verification-input.template.md) 갱신 |
103
+
104
+ `runtime/` 는 build output — 커밋 제외. 소스 변경 후 `npm run build` 로 동기화.
105
+
106
+ ## 9. 테스트
107
+
108
+ | ID | 시나리오 | 기대 |
109
+ |---|---|---|
110
+ | U1 | `_resolve` 전체-task: 모든 stage done + 머지 + clean | target(base=anchor, head=task HEAD, stage 목록 전부) 반환 |
111
+ | U2 | 전체-task: stage 하나 미완료 | PrepareError(미완료 stage) |
112
+ | U3 | 전체-task: done stage 가 HEAD ancestor 아님(미머지) | PrepareError(미머지) |
113
+ | U4 | 전체-task: `.okstra/` 밖 dirty worktree | PrepareError(uncommitted) |
114
+ | U4b | 전체-task: `.okstra/` 안에만 변경(소스 clean) | 통과(게이트 안 걸림) |
115
+ | U5 | 단독-stage `--stage N`: stage N done | target(base=stage-key base_ref, head=stage worktree HEAD) 반환, 다른 stage 무관 |
116
+ | U6 | 단독-stage: stage N 미완료 | PrepareError |
117
+ | W1 | wizard final-verification 서브플로우 stage picker | auto / N / 직접입력 3-옵션, `selected_stage` emit |
118
+ | E1 | e2e: 2-stage 작업, 둘 다 done+머지 후 전체-task | 통과, release-handoff routing 허용 |
119
+ | E2 | e2e: stage 1 done, stage 2 미머지 상태에서 전체-task | block + 안내 |
120
+ | E3 | e2e: stage 1 구현 직후 `--stage 1` 단독 | 통과, routing 은 release-handoff 불가 |
121
+
122
+ ## 10. 미해결 / 후속
123
+
124
+ - 다른 phase(`requirements-discovery` / `error-analysis`)는 stage 개념이 없으므로 `--stage` 무시(현행대로 `""` emit). final-verification 만 신규 대상.
125
+ - `plan_run_root` 위치 해소: consumers.jsonl 은 plan-task-key run root 아래. final-verification 이 이 경로를 어떻게 얻는지(report → plan reference 경유 vs registry)는 구현 단계에서 run.py 기존 경로 해소 로직과 정합되게 확정한다.
126
+ - 단독-stage 모드의 stage-key worktree 가 teardown 된 뒤(사용자가 정리)에는 단독 검증 불가 — 그 경우 PrepareError 로 "stage worktree 없음, 전체-task 모드 사용" 안내.