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.
- package/README.kr.md +1 -1
- package/README.md +1 -1
- package/docs/kr/architecture.md +1 -0
- package/docs/kr/cli.md +2 -1
- package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +993 -0
- package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +93 -0
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +447 -0
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +289 -0
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +774 -0
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +303 -0
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +387 -0
- package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +126 -0
- package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +180 -0
- package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +179 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +1 -0
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/launch.template.md +1 -0
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +16 -9
- package/runtime/prompts/profiles/_implementation-verifier.md +4 -1
- package/runtime/prompts/profiles/final-verification.md +7 -7
- package/runtime/prompts/profiles/implementation-planning.md +14 -7
- package/runtime/prompts/wizard/prompts.ko.json +3 -2
- package/runtime/python/okstra_ctl/analysis_packet.py +14 -2
- package/runtime/python/okstra_ctl/render.py +3 -0
- package/runtime/python/okstra_ctl/run.py +541 -41
- package/runtime/python/okstra_ctl/wizard.py +25 -7
- package/runtime/python/okstra_ctl/worktree.py +126 -9
- package/runtime/python/okstra_ctl/worktree_registry.py +88 -17
- package/runtime/schemas/final-report-v1.0.schema.json +36 -0
- package/runtime/skills/okstra-convergence/SKILL.md +14 -3
- package/runtime/skills/okstra-memory/SKILL.md +28 -5
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/templates/reports/final-report.template.md +12 -0
- package/runtime/templates/reports/final-verification-input.template.md +8 -5
- package/runtime/templates/reports/i18n/en.json +3 -1
- package/runtime/templates/reports/i18n/ko.json +3 -1
- package/runtime/validators/validate-implementation-plan-stages.py +57 -11
- package/runtime/validators/validate-run.py +143 -1
- package/runtime/validators/validate-workflow.sh +6 -1
- package/src/memory.mjs +50 -11
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
# final-verification 전체-task 게이트 + 단독-stage 모드 구현 계획
|
|
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:** final-verification 이 multi-stage 작업을 (A) 전체-task 모드(모든 stage done+머지 후 한 번) 또는 (B) 단독-stage 모드(`--stage N`)로 검증하고, 검증 target 과 entry gate 를 Python prep 에서 자동 해소·강제하게 한다.
|
|
6
|
+
|
|
7
|
+
**Architecture:** 검증 target(base/HEAD/worktree/stage 목록) 해소와 gate 검사(done·merge·clean)를 [run.py](../../../scripts/okstra_ctl/run.py) 의 bundle prep 단계로 단일화한다. 결정 로직은 git/registry IO 와 분리된 순수 함수 2개(`_resolve_whole_task_target` / `_resolve_single_stage_target`)로 두어 TDD 하고, 얇은 IO 래퍼가 facts 를 모아 디스패치한다. 해소된 snapshot 은 `{{VERIFICATION_TARGET}}` 으로 launch 템플릿에 주입한다. 설계: [2026-06-06-final-verification-whole-task-gate-design.md](../specs/2026-06-06-final-verification-whole-task-gate-design.md).
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3 (`scripts/okstra_ctl/run.py`, `worktree_registry.py`, `consumers.py`), pytest + e2e bash, jinja2 launch 템플릿, markdown 프로파일, JSON Schema + `validators/validate-run.py`, Node 빌드(`npm run build`).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 파일 구조
|
|
14
|
+
|
|
15
|
+
| 파일 | 책임 | 작업 |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `scripts/okstra_ctl/worktree_registry.py` | stage-key row 조회 getter | 수정 (`get_stage_row`) |
|
|
18
|
+
| `scripts/okstra_ctl/run.py` | target 해소 순수 함수 + IO 래퍼 + prepare 분기 + ctx 주입 | 수정 |
|
|
19
|
+
| `scripts/okstra_ctl/consumers.py` | done 행 `report_path` round-trip (이미 `**fields` 지원) | 테스트만 |
|
|
20
|
+
| `tests/test_final_verification_target.py` | 순수 함수 유닛 (U1–U6) | 신규 |
|
|
21
|
+
| `tests/test_get_stage_row.py` | registry getter 유닛 | 신규 |
|
|
22
|
+
| `tests/test_reserve_final_verification.py` | IO 래퍼 통합 (tmp git repo) | 신규 |
|
|
23
|
+
| `prompts/launch.template.md` | `{{VERIFICATION_TARGET}}` 주입 지점 | 수정 |
|
|
24
|
+
| `prompts/profiles/final-verification.md` | entry gate 두 모드, Source Implementation Report 목록, routing 제약 | 수정 |
|
|
25
|
+
| `prompts/profiles/_implementation-deliverable.md` | lead post-stage done 행에 `report_path` 포함 | 수정 |
|
|
26
|
+
| `scripts/okstra_ctl/wizard.py` | final-verification 서브플로우 stage picker + approved-plan + emit | 수정 |
|
|
27
|
+
| `validators/validate-run.py` | `verificationScope` + routing 규칙 + Source Implementation Report 다중 | 수정 |
|
|
28
|
+
| `templates/reports/final-verification-input.template.md` | 입력 템플릿 갱신 | 수정 |
|
|
29
|
+
| `tests-e2e/scenario-<id>-final-verification-modes.sh` | E1/E2/E3 | 신규 |
|
|
30
|
+
|
|
31
|
+
`runtime/` 는 build output — 커밋 제외. profile/template/wizard 변경 후 `npm run build` 로 동기화.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Task 1: registry `get_stage_row` getter (TDD)
|
|
36
|
+
|
|
37
|
+
**Files:**
|
|
38
|
+
- Modify: `scripts/okstra_ctl/worktree_registry.py`
|
|
39
|
+
- Test: `tests/test_get_stage_row.py`
|
|
40
|
+
|
|
41
|
+
stage-key row(`<task-key>#stage-N`)의 `worktree_path` / `base_ref` 를 단독-stage 모드가 읽어야 한다. 기존 `get_implementation_base` 패턴과 동일한 read-only getter 를 추가한다.
|
|
42
|
+
|
|
43
|
+
- [ ] **Step 1: 실패 테스트 작성**
|
|
44
|
+
|
|
45
|
+
Create `tests/test_get_stage_row.py`:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
"""Unit coverage for worktree_registry.get_stage_row.
|
|
49
|
+
|
|
50
|
+
conftest.py 의 autouse fixture 가 OKSTRA_HOME 을 임시 디렉터리로 격리하므로
|
|
51
|
+
registry write 가 사용자 ~/.okstra 를 오염시키지 않는다."""
|
|
52
|
+
import sys
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
LIB_DIR = Path(__file__).resolve().parents[1] / "scripts"
|
|
56
|
+
if str(LIB_DIR) not in sys.path:
|
|
57
|
+
sys.path.insert(0, str(LIB_DIR))
|
|
58
|
+
|
|
59
|
+
from okstra_ctl import worktree_registry as reg
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_get_stage_row_returns_reserved_row():
|
|
63
|
+
reg.reserve(
|
|
64
|
+
project_id="p", task_group="g", task_id="t",
|
|
65
|
+
worktree_path="/wt/stage-2", branch="fix-t-s2",
|
|
66
|
+
base_ref="abc123", stage_number=2,
|
|
67
|
+
)
|
|
68
|
+
row = reg.get_stage_row("p", "g", "t", 2)
|
|
69
|
+
assert row is not None
|
|
70
|
+
assert row["worktree_path"] == "/wt/stage-2"
|
|
71
|
+
assert row["base_ref"] == "abc123"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_get_stage_row_missing_returns_none():
|
|
75
|
+
assert reg.get_stage_row("p", "g", "t", 9) is None
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
79
|
+
|
|
80
|
+
Run: `python3 -m pytest tests/test_get_stage_row.py -v`
|
|
81
|
+
Expected: FAIL — `AttributeError: module ... has no attribute 'get_stage_row'`
|
|
82
|
+
|
|
83
|
+
- [ ] **Step 3: getter 구현**
|
|
84
|
+
|
|
85
|
+
In `scripts/okstra_ctl/worktree_registry.py`, 추가 (after `get_implementation_base`, around line 233):
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
def get_stage_row(
|
|
89
|
+
project_id: str, task_group: str, task_id: str, stage: int,
|
|
90
|
+
) -> Optional[dict]:
|
|
91
|
+
"""Return the stage-key registry row (worktree_path / base_ref / branch)
|
|
92
|
+
for `<task-key>#stage-<stage>`, or None when no such reservation exists."""
|
|
93
|
+
key = task_key(project_id, task_group, task_id, stage)
|
|
94
|
+
with _registry_lock():
|
|
95
|
+
data = _load()
|
|
96
|
+
return data["tasks"].get(key)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- [ ] **Step 4: 테스트 통과 확인**
|
|
100
|
+
|
|
101
|
+
Run: `python3 -m pytest tests/test_get_stage_row.py -v`
|
|
102
|
+
Expected: PASS (2 passed)
|
|
103
|
+
|
|
104
|
+
- [ ] **Step 5: 커밋**
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
git add scripts/okstra_ctl/worktree_registry.py tests/test_get_stage_row.py
|
|
108
|
+
git commit -m "feat(run): worktree_registry.get_stage_row 조회 getter"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Task 2: 전체-task target 순수 함수 (TDD)
|
|
114
|
+
|
|
115
|
+
**Files:**
|
|
116
|
+
- Modify: `scripts/okstra_ctl/run.py`
|
|
117
|
+
- Test: `tests/test_final_verification_target.py`
|
|
118
|
+
|
|
119
|
+
전체-task 모드의 결정 로직: 모든 Stage Map stage 가 done, 모든 done stage 가 HEAD ancestor, worktree clean(.okstra 제외) 이면 target 반환, 아니면 PrepareError. git/registry IO 는 인자로 주입받는 순수 함수.
|
|
120
|
+
|
|
121
|
+
- [ ] **Step 1: 실패 테스트 작성**
|
|
122
|
+
|
|
123
|
+
Create `tests/test_final_verification_target.py`:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
"""Unit coverage for run._resolve_whole_task_target / _resolve_single_stage_target."""
|
|
127
|
+
import sys
|
|
128
|
+
from pathlib import Path
|
|
129
|
+
|
|
130
|
+
import pytest
|
|
131
|
+
|
|
132
|
+
LIB_DIR = Path(__file__).resolve().parents[1] / "scripts"
|
|
133
|
+
if str(LIB_DIR) not in sys.path:
|
|
134
|
+
sys.path.insert(0, str(LIB_DIR))
|
|
135
|
+
|
|
136
|
+
from okstra_ctl import run # run._resolve_*, run.PrepareError, run._FVTarget
|
|
137
|
+
|
|
138
|
+
PrepareError = run.PrepareError
|
|
139
|
+
|
|
140
|
+
STAGE_MAP = [{"stage_number": 1}, {"stage_number": 2}]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _done(stage, head, report="r{}.md"):
|
|
144
|
+
return {"stage": stage, "status": "done", "head_commit": head,
|
|
145
|
+
"report_path": report.format(stage)}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_whole_task_ok():
|
|
149
|
+
target = run._resolve_whole_task_target(
|
|
150
|
+
stage_map=STAGE_MAP,
|
|
151
|
+
done_rows=[_done(1, "c1"), _done(2, "c2")],
|
|
152
|
+
anchor_base="base0",
|
|
153
|
+
task_worktree_path="/wt",
|
|
154
|
+
task_head="HEAD9",
|
|
155
|
+
task_dirty=False,
|
|
156
|
+
merged={1: True, 2: True},
|
|
157
|
+
)
|
|
158
|
+
assert target.scope == "whole-task"
|
|
159
|
+
assert target.base == "base0"
|
|
160
|
+
assert target.head == "HEAD9"
|
|
161
|
+
assert target.worktree_path == "/wt"
|
|
162
|
+
assert sorted(target.stages) == [1, 2]
|
|
163
|
+
assert sorted(target.reports) == ["r1.md", "r2.md"]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_whole_task_blocks_incomplete_stage():
|
|
167
|
+
with pytest.raises(PrepareError, match="stage 2 not done"):
|
|
168
|
+
run._resolve_whole_task_target(
|
|
169
|
+
stage_map=STAGE_MAP, done_rows=[_done(1, "c1")],
|
|
170
|
+
anchor_base="base0", task_worktree_path="/wt",
|
|
171
|
+
task_head="HEAD9", task_dirty=False, merged={1: True},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_whole_task_blocks_unmerged_stage():
|
|
176
|
+
with pytest.raises(PrepareError, match="not merged"):
|
|
177
|
+
run._resolve_whole_task_target(
|
|
178
|
+
stage_map=STAGE_MAP,
|
|
179
|
+
done_rows=[_done(1, "c1"), _done(2, "c2")],
|
|
180
|
+
anchor_base="base0", task_worktree_path="/wt",
|
|
181
|
+
task_head="HEAD9", task_dirty=False,
|
|
182
|
+
merged={1: True, 2: False},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_whole_task_blocks_dirty():
|
|
187
|
+
with pytest.raises(PrepareError, match="uncommitted source changes"):
|
|
188
|
+
run._resolve_whole_task_target(
|
|
189
|
+
stage_map=STAGE_MAP,
|
|
190
|
+
done_rows=[_done(1, "c1"), _done(2, "c2")],
|
|
191
|
+
anchor_base="base0", task_worktree_path="/wt",
|
|
192
|
+
task_head="HEAD9", task_dirty=True,
|
|
193
|
+
merged={1: True, 2: True},
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
198
|
+
|
|
199
|
+
Run: `python3 -m pytest tests/test_final_verification_target.py -v`
|
|
200
|
+
Expected: FAIL — `AttributeError: ... has no attribute '_resolve_whole_task_target'`
|
|
201
|
+
|
|
202
|
+
- [ ] **Step 3: 순수 함수 + 데이터클래스 구현**
|
|
203
|
+
|
|
204
|
+
In `scripts/okstra_ctl/run.py`, 추가 (after `_resolve_stage_base_commit`, around line 378). 먼저 파일 상단 import 영역에 `from dataclasses import dataclass, field` 가 이미 있는지 확인하고 없으면 추가. 그다음:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
@dataclass
|
|
208
|
+
class _FVTarget:
|
|
209
|
+
scope: str # "whole-task" | "single-stage"
|
|
210
|
+
base: str
|
|
211
|
+
head: str
|
|
212
|
+
worktree_path: str
|
|
213
|
+
stages: list # list[int]
|
|
214
|
+
reports: list # list[str], report_path 값(없으면 "")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _resolve_whole_task_target(
|
|
218
|
+
*, stage_map: list, done_rows: list, anchor_base: str,
|
|
219
|
+
task_worktree_path: str, task_head: str, task_dirty: bool,
|
|
220
|
+
merged: dict,
|
|
221
|
+
) -> "_FVTarget":
|
|
222
|
+
"""전체-task 검증 target. 모든 Stage Map stage 가 done + HEAD 에 머지 +
|
|
223
|
+
worktree clean 이어야 한다. 위반 시 PrepareError."""
|
|
224
|
+
done_by_stage = {r["stage"]: r for r in done_rows}
|
|
225
|
+
for s in stage_map:
|
|
226
|
+
n = s["stage_number"]
|
|
227
|
+
if n not in done_by_stage:
|
|
228
|
+
raise PrepareError(
|
|
229
|
+
f"final-verification(whole-task): stage {n} not done — "
|
|
230
|
+
f"run implementation --stage {n} first"
|
|
231
|
+
)
|
|
232
|
+
if not merged.get(n, False):
|
|
233
|
+
sha = done_by_stage[n].get("head_commit", "")
|
|
234
|
+
raise PrepareError(
|
|
235
|
+
f"final-verification(whole-task): stage {n} done commit "
|
|
236
|
+
f"{sha} not merged into task worktree HEAD — merge stage "
|
|
237
|
+
"branches then retry"
|
|
238
|
+
)
|
|
239
|
+
if task_dirty:
|
|
240
|
+
raise PrepareError(
|
|
241
|
+
"final-verification: worktree has uncommitted source changes "
|
|
242
|
+
"(outside .okstra/) — commit or stash before verifying"
|
|
243
|
+
)
|
|
244
|
+
stages = [s["stage_number"] for s in stage_map]
|
|
245
|
+
reports = [done_by_stage[n].get("report_path", "") for n in stages]
|
|
246
|
+
return _FVTarget(
|
|
247
|
+
scope="whole-task", base=anchor_base, head=task_head,
|
|
248
|
+
worktree_path=task_worktree_path, stages=stages, reports=reports,
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
- [ ] **Step 4: 테스트 통과 확인**
|
|
253
|
+
|
|
254
|
+
Run: `python3 -m pytest tests/test_final_verification_target.py -v`
|
|
255
|
+
Expected: PASS (4 passed)
|
|
256
|
+
|
|
257
|
+
- [ ] **Step 5: 커밋**
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
git add scripts/okstra_ctl/run.py tests/test_final_verification_target.py
|
|
261
|
+
git commit -m "feat(run): 전체-task final-verification target 순수 함수"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Task 3: 단독-stage target 순수 함수 (TDD)
|
|
267
|
+
|
|
268
|
+
**Files:**
|
|
269
|
+
- Modify: `scripts/okstra_ctl/run.py`
|
|
270
|
+
- Test: `tests/test_final_verification_target.py` (append)
|
|
271
|
+
|
|
272
|
+
단독-stage 모드: stage N 이 done 이고 stage worktree 가 존재하고 clean 이면 target 반환. 다른 stage 무관.
|
|
273
|
+
|
|
274
|
+
- [ ] **Step 1: 실패 테스트 추가**
|
|
275
|
+
|
|
276
|
+
Append to `tests/test_final_verification_target.py`:
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
def test_single_stage_ok():
|
|
280
|
+
target = run._resolve_single_stage_target(
|
|
281
|
+
requested_stage="2",
|
|
282
|
+
done_rows=[_done(1, "c1"), _done(2, "c2")],
|
|
283
|
+
stage_base="base2",
|
|
284
|
+
stage_worktree_path="/wt/stage-2",
|
|
285
|
+
stage_head="c2",
|
|
286
|
+
stage_dirty=False,
|
|
287
|
+
)
|
|
288
|
+
assert target.scope == "single-stage"
|
|
289
|
+
assert target.base == "base2"
|
|
290
|
+
assert target.head == "c2"
|
|
291
|
+
assert target.worktree_path == "/wt/stage-2"
|
|
292
|
+
assert target.stages == [2]
|
|
293
|
+
assert target.reports == ["r2.md"]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_single_stage_blocks_not_done():
|
|
297
|
+
with pytest.raises(PrepareError, match="stage 3 not done"):
|
|
298
|
+
run._resolve_single_stage_target(
|
|
299
|
+
requested_stage="3", done_rows=[_done(1, "c1")],
|
|
300
|
+
stage_base="x", stage_worktree_path="/wt/stage-3",
|
|
301
|
+
stage_head="x", stage_dirty=False,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def test_single_stage_blocks_missing_worktree():
|
|
306
|
+
with pytest.raises(PrepareError, match="stage worktree not found"):
|
|
307
|
+
run._resolve_single_stage_target(
|
|
308
|
+
requested_stage="2", done_rows=[_done(2, "c2")],
|
|
309
|
+
stage_base="base2", stage_worktree_path="",
|
|
310
|
+
stage_head="c2", stage_dirty=False,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_single_stage_blocks_dirty():
|
|
315
|
+
with pytest.raises(PrepareError, match="uncommitted source changes"):
|
|
316
|
+
run._resolve_single_stage_target(
|
|
317
|
+
requested_stage="2", done_rows=[_done(2, "c2")],
|
|
318
|
+
stage_base="base2", stage_worktree_path="/wt/stage-2",
|
|
319
|
+
stage_head="c2", stage_dirty=True,
|
|
320
|
+
)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
324
|
+
|
|
325
|
+
Run: `python3 -m pytest tests/test_final_verification_target.py -k single_stage -v`
|
|
326
|
+
Expected: FAIL — `AttributeError: ... has no attribute '_resolve_single_stage_target'`
|
|
327
|
+
|
|
328
|
+
- [ ] **Step 3: 순수 함수 구현**
|
|
329
|
+
|
|
330
|
+
In `scripts/okstra_ctl/run.py`, 추가 (after `_resolve_whole_task_target`):
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
def _resolve_single_stage_target(
|
|
334
|
+
*, requested_stage: str, done_rows: list, stage_base: str,
|
|
335
|
+
stage_worktree_path: str, stage_head: str, stage_dirty: bool,
|
|
336
|
+
) -> "_FVTarget":
|
|
337
|
+
"""단독-stage 검증 target. stage N 만 done + stage worktree 존재 + clean.
|
|
338
|
+
다른 stage 의 done/머지 여부와 무관. 위반 시 PrepareError."""
|
|
339
|
+
n = int(requested_stage)
|
|
340
|
+
done_by_stage = {r["stage"]: r for r in done_rows}
|
|
341
|
+
if n not in done_by_stage:
|
|
342
|
+
raise PrepareError(
|
|
343
|
+
f"final-verification(single-stage): stage {n} not done — "
|
|
344
|
+
f"run implementation --stage {n} first"
|
|
345
|
+
)
|
|
346
|
+
if not stage_worktree_path:
|
|
347
|
+
raise PrepareError(
|
|
348
|
+
f"final-verification(single-stage): stage worktree not found for "
|
|
349
|
+
f"stage {n} (torn down?) — use whole-task mode (--stage auto)"
|
|
350
|
+
)
|
|
351
|
+
if stage_dirty:
|
|
352
|
+
raise PrepareError(
|
|
353
|
+
"final-verification: worktree has uncommitted source changes "
|
|
354
|
+
"(outside .okstra/) — commit or stash before verifying"
|
|
355
|
+
)
|
|
356
|
+
return _FVTarget(
|
|
357
|
+
scope="single-stage", base=stage_base, head=stage_head,
|
|
358
|
+
worktree_path=stage_worktree_path, stages=[n],
|
|
359
|
+
reports=[done_by_stage[n].get("report_path", "")],
|
|
360
|
+
)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
- [ ] **Step 4: 테스트 통과 확인**
|
|
364
|
+
|
|
365
|
+
Run: `python3 -m pytest tests/test_final_verification_target.py -v`
|
|
366
|
+
Expected: PASS (8 passed)
|
|
367
|
+
|
|
368
|
+
- [ ] **Step 5: 커밋**
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
git add scripts/okstra_ctl/run.py tests/test_final_verification_target.py
|
|
372
|
+
git commit -m "feat(run): 단독-stage final-verification target 순수 함수"
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Task 4: IO 래퍼 + prepare 분기 + ctx 주입 (통합 테스트)
|
|
378
|
+
|
|
379
|
+
**Files:**
|
|
380
|
+
- Modify: `scripts/okstra_ctl/run.py`
|
|
381
|
+
- Test: `tests/test_reserve_final_verification.py`
|
|
382
|
+
|
|
383
|
+
facts(registry/consumers/git)를 모아 순수 함수로 디스패치하는 얇은 래퍼 `_reserve_final_verification_target` 를 만들고, `prepare_task_bundle` 의 task_type 분기에 연결한다. ctx 에 `VERIFICATION_TARGET` 블록을 채운다.
|
|
384
|
+
|
|
385
|
+
- [ ] **Step 1: 실패 테스트 작성 (tmp git repo + monkeypatch)**
|
|
386
|
+
|
|
387
|
+
Create `tests/test_reserve_final_verification.py`:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
"""Integration coverage for run._reserve_final_verification_target.
|
|
391
|
+
|
|
392
|
+
conftest.py 가 OKSTRA_HOME 을 격리하므로 registry 예약은 임시 home 에 쓰인다."""
|
|
393
|
+
import json
|
|
394
|
+
import subprocess
|
|
395
|
+
import sys
|
|
396
|
+
from pathlib import Path
|
|
397
|
+
|
|
398
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
399
|
+
LIB_DIR = REPO / "scripts"
|
|
400
|
+
if str(LIB_DIR) not in sys.path:
|
|
401
|
+
sys.path.insert(0, str(LIB_DIR))
|
|
402
|
+
|
|
403
|
+
from okstra_ctl import run
|
|
404
|
+
from okstra_ctl import worktree_registry as reg
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _git(cwd, *args):
|
|
408
|
+
subprocess.run(["git", *args], cwd=cwd, check=True,
|
|
409
|
+
capture_output=True, text=True)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _commit(cwd, name):
|
|
413
|
+
(Path(cwd) / name).write_text("x", encoding="utf-8")
|
|
414
|
+
_git(cwd, "add", name)
|
|
415
|
+
_git(cwd, "commit", "-m", name)
|
|
416
|
+
r = subprocess.run(["git", "rev-parse", "HEAD"], cwd=cwd,
|
|
417
|
+
capture_output=True, text=True, check=True)
|
|
418
|
+
return r.stdout.strip()
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def test_whole_task_target_ctx(tmp_path):
|
|
422
|
+
# task worktree git repo: base → stage1 commit → stage2 commit (선형 머지)
|
|
423
|
+
wt = tmp_path / "wt"
|
|
424
|
+
wt.mkdir()
|
|
425
|
+
_git(wt, "init", "-q")
|
|
426
|
+
_git(wt, "config", "user.email", "t@t")
|
|
427
|
+
_git(wt, "config", "user.name", "t")
|
|
428
|
+
base = _commit(wt, "base.txt")
|
|
429
|
+
c1 = _commit(wt, "s1.txt")
|
|
430
|
+
c2 = _commit(wt, "s2.txt")
|
|
431
|
+
|
|
432
|
+
# consumers.jsonl: stage 1,2 done
|
|
433
|
+
plan_run_root = tmp_path / "planrun"
|
|
434
|
+
plan_run_root.mkdir()
|
|
435
|
+
rows = [
|
|
436
|
+
{"impl_task_key": "k", "stage": 1, "status": "done",
|
|
437
|
+
"head_commit": c1, "report_path": "r1.md"},
|
|
438
|
+
{"impl_task_key": "k", "stage": 2, "status": "done",
|
|
439
|
+
"head_commit": c2, "report_path": "r2.md"},
|
|
440
|
+
]
|
|
441
|
+
(plan_run_root / "consumers.jsonl").write_text(
|
|
442
|
+
"\n".join(json.dumps(r) for r in rows) + "\n", encoding="utf-8")
|
|
443
|
+
|
|
444
|
+
reg.reserve(project_id="p", task_group="g", task_id="t",
|
|
445
|
+
worktree_path=str(wt), branch="fix-t", base_ref=base)
|
|
446
|
+
reg.set_implementation_base("p", "g", "t", base)
|
|
447
|
+
|
|
448
|
+
ctx = {"EXECUTOR_WORKTREE_PATH": str(wt)}
|
|
449
|
+
inp = run.PrepareInputs(
|
|
450
|
+
workspace_root=REPO, project_root=wt, project_id="p",
|
|
451
|
+
task_group="g", task_id="t", task_type="final-verification",
|
|
452
|
+
brief_path=tmp_path / "brief.md",
|
|
453
|
+
approved_plan_path=str(plan_run_root / "reports" / "plan.md"),
|
|
454
|
+
stage="auto",
|
|
455
|
+
)
|
|
456
|
+
stage_map = [{"stage_number": 1}, {"stage_number": 2}]
|
|
457
|
+
run._reserve_final_verification_target(inp, ctx, stage_map)
|
|
458
|
+
|
|
459
|
+
block = ctx["VERIFICATION_TARGET"]
|
|
460
|
+
assert "whole-task" in block
|
|
461
|
+
assert base in block # base ref injected
|
|
462
|
+
assert c2 in block # head SHA injected
|
|
463
|
+
assert "stage 2" in block.lower()
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
참고: `approved_plan_path` 의 `parents[1]` 가 `plan_run_root` 가 되도록 `<plan_run_root>/reports/plan.md` 로 둔다 ([run.py:1002](../../../scripts/okstra_ctl/run.py:1002) 의 `Path(approved_plan_path).resolve().parents[1]` 규칙).
|
|
467
|
+
|
|
468
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
469
|
+
|
|
470
|
+
Run: `python3 -m pytest tests/test_reserve_final_verification.py -v`
|
|
471
|
+
Expected: FAIL — `AttributeError: ... has no attribute '_reserve_final_verification_target'`
|
|
472
|
+
|
|
473
|
+
- [ ] **Step 3: IO 래퍼 구현**
|
|
474
|
+
|
|
475
|
+
In `scripts/okstra_ctl/run.py`, 추가 (after `_reserve_implementation_stages`, around line 1099):
|
|
476
|
+
|
|
477
|
+
```python
|
|
478
|
+
def _git_out(cwd, *args) -> str:
|
|
479
|
+
r = _subprocess.run(["git", "-C", str(cwd), *args],
|
|
480
|
+
capture_output=True, text=True)
|
|
481
|
+
return r.stdout.strip() if r.returncode == 0 else ""
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _is_ancestor(cwd, commit, head) -> bool:
|
|
485
|
+
if not commit or not head:
|
|
486
|
+
return False
|
|
487
|
+
r = _subprocess.run(
|
|
488
|
+
["git", "-C", str(cwd), "merge-base", "--is-ancestor", commit, head],
|
|
489
|
+
capture_output=True, text=True,
|
|
490
|
+
)
|
|
491
|
+
return r.returncode == 0
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _is_dirty_excluding_okstra(cwd) -> bool:
|
|
495
|
+
out = _git_out(cwd, "status", "--short", "--", ".", ":(exclude).okstra")
|
|
496
|
+
return bool(out.strip())
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _reserve_final_verification_target(
|
|
500
|
+
inp: "PrepareInputs", ctx: dict, ctx_stage_map: list,
|
|
501
|
+
) -> None:
|
|
502
|
+
"""final-verification 의 검증 target 을 registry/consumers/git 에서
|
|
503
|
+
해소하고 gate 를 강제한다. 위반 시 PrepareError. 결과를 ctx 의
|
|
504
|
+
VERIFICATION_* 키로 주입한다."""
|
|
505
|
+
from .consumers import read_consumers
|
|
506
|
+
from . import worktree_registry as _reg
|
|
507
|
+
|
|
508
|
+
plan_run_root = Path(inp.approved_plan_path).resolve().parents[1]
|
|
509
|
+
done_rows = [r for r in read_consumers(plan_run_root)
|
|
510
|
+
if r.get("status") == "done"]
|
|
511
|
+
|
|
512
|
+
if inp.stage and inp.stage != "auto":
|
|
513
|
+
n = int(inp.stage)
|
|
514
|
+
row = _reg.get_stage_row(inp.project_id, inp.task_group, inp.task_id, n)
|
|
515
|
+
wt_path = (row or {}).get("worktree_path", "")
|
|
516
|
+
stage_base = (row or {}).get("base_ref", "")
|
|
517
|
+
head = _git_out(wt_path, "rev-parse", "HEAD") if wt_path else ""
|
|
518
|
+
target = _resolve_single_stage_target(
|
|
519
|
+
requested_stage=inp.stage, done_rows=done_rows,
|
|
520
|
+
stage_base=stage_base, stage_worktree_path=wt_path,
|
|
521
|
+
stage_head=head,
|
|
522
|
+
stage_dirty=_is_dirty_excluding_okstra(wt_path) if wt_path else False,
|
|
523
|
+
)
|
|
524
|
+
ctx["EXECUTOR_WORKTREE_PATH"] = wt_path
|
|
525
|
+
else:
|
|
526
|
+
wt_path = ctx["EXECUTOR_WORKTREE_PATH"]
|
|
527
|
+
anchor = _reg.get_implementation_base(
|
|
528
|
+
inp.project_id, inp.task_group, inp.task_id) or ""
|
|
529
|
+
head = _git_out(wt_path, "rev-parse", "HEAD")
|
|
530
|
+
merged = {r["stage"]: _is_ancestor(wt_path, r.get("head_commit", ""), head)
|
|
531
|
+
for r in done_rows}
|
|
532
|
+
target = _resolve_whole_task_target(
|
|
533
|
+
stage_map=ctx_stage_map, done_rows=done_rows, anchor_base=anchor,
|
|
534
|
+
task_worktree_path=wt_path, task_head=head,
|
|
535
|
+
task_dirty=_is_dirty_excluding_okstra(wt_path), merged=merged,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
diff_stat = _git_out(target.worktree_path, "diff", "--stat",
|
|
539
|
+
f"{target.base}..{target.head}")
|
|
540
|
+
reports = "\n".join(
|
|
541
|
+
f" - stage {s}: `{rp or '(report_path 미기록)'}`"
|
|
542
|
+
for s, rp in zip(target.stages, target.reports)
|
|
543
|
+
)
|
|
544
|
+
ctx["VERIFICATION_TARGET"] = (
|
|
545
|
+
f"- **Verification scope:** `{target.scope}`\n"
|
|
546
|
+
f"- **Worktree:** `{target.worktree_path}`\n"
|
|
547
|
+
f"- **Verification base ref:** `{target.base}`\n"
|
|
548
|
+
f"- **Verification head SHA:** `{target.head}`\n"
|
|
549
|
+
f"- **Stages under verification:** {target.stages}\n"
|
|
550
|
+
f"- **Source implementation reports:**\n{reports}\n"
|
|
551
|
+
f"- **Verification diff stat:**\n```\n{diff_stat}\n```"
|
|
552
|
+
)
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
참고: `_subprocess` 는 run.py 가 이미 `import subprocess as _subprocess` 로 보유 (`_reserve_implementation_stages` 에서 사용 중 — [run.py:1032](../../../scripts/okstra_ctl/run.py:1032)).
|
|
556
|
+
|
|
557
|
+
- [ ] **Step 4: prepare 분기 + stage_map 파싱 연결**
|
|
558
|
+
|
|
559
|
+
In `scripts/okstra_ctl/run.py`, [run.py:1442](../../../scripts/okstra_ctl/run.py:1442) 의 분기를 확장:
|
|
560
|
+
|
|
561
|
+
```python
|
|
562
|
+
if inp.task_type == "implementation":
|
|
563
|
+
_reserve_implementation_stages(inp, ctx, ctx_stage_map)
|
|
564
|
+
elif inp.task_type == "final-verification":
|
|
565
|
+
_reserve_final_verification_target(inp, ctx, ctx_stage_map)
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
그리고 [run.py:817](../../../scripts/okstra_ctl/run.py:817) 부근에서 `ctx_stage_map` 이 implementation 에서만 파싱되므로, final-verification 도 (전체-task 모드의 Stage Map 열거용) 파싱하도록 조건 확장. 해당 `if inp.task_type == "implementation":` 블록의 plan 검증/파싱 중 **stage map 파싱만** final-verification 에도 적용:
|
|
569
|
+
|
|
570
|
+
```python
|
|
571
|
+
if inp.task_type in ("implementation", "final-verification"):
|
|
572
|
+
if not inp.approved_plan_path:
|
|
573
|
+
raise PrepareError(
|
|
574
|
+
f"--approved-plan is required for {inp.task_type}")
|
|
575
|
+
_validate_approved_plan(inp.approved_plan_path)
|
|
576
|
+
_validate_stage_structure(inp.approved_plan_path)
|
|
577
|
+
ctx_stage_map = _parse_stage_map_into_ctx(inp.approved_plan_path)
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
주의: 기존 implementation 전용 로직(`_apply_cli_approval`, `implementation_option` frontmatter 주입, `_validate_stage` 의 `inp.stage != "auto"` 등 [run.py:798-836](../../../scripts/okstra_ctl/run.py:798))은 implementation 분기에 그대로 남기고, 위 공통화는 plan 존재·stage map 파싱에 한정한다. 실제 구현 시 해당 블록을 읽고 implementation-only 부분과 공통 부분을 분리할 것.
|
|
581
|
+
|
|
582
|
+
- [ ] **Step 5: 테스트 통과 확인**
|
|
583
|
+
|
|
584
|
+
Run: `python3 -m pytest tests/test_reserve_final_verification.py -v`
|
|
585
|
+
Expected: PASS
|
|
586
|
+
|
|
587
|
+
- [ ] **Step 6: 커밋**
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
git add scripts/okstra_ctl/run.py tests/test_reserve_final_verification.py
|
|
591
|
+
git commit -m "feat(run): final-verification target IO 래퍼 + prepare 분기 + ctx 주입"
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## Task 5: consumers `report_path` round-trip + lead 계약
|
|
597
|
+
|
|
598
|
+
**Files:**
|
|
599
|
+
- Test: `tests/test_consumers_report_path.py` (신규)
|
|
600
|
+
- Modify: `prompts/profiles/_implementation-deliverable.md`
|
|
601
|
+
|
|
602
|
+
`append_consumer` 는 이미 `**fields` 로 임의 필드를 저장하므로 ([consumers.py:35](../../../scripts/okstra_ctl/consumers.py:35)) Python 변경은 없다. round-trip 회귀 테스트로 계약을 고정하고, lead 가 done 행에 `report_path` 를 넣도록 프로파일 계약을 갱신한다.
|
|
603
|
+
|
|
604
|
+
- [ ] **Step 1: round-trip 테스트 작성**
|
|
605
|
+
|
|
606
|
+
Create `tests/test_consumers_report_path.py`:
|
|
607
|
+
|
|
608
|
+
```python
|
|
609
|
+
"""report_path 필드가 consumers done 행에 round-trip 되는지 회귀 고정."""
|
|
610
|
+
import sys
|
|
611
|
+
from pathlib import Path
|
|
612
|
+
|
|
613
|
+
LIB_DIR = Path(__file__).resolve().parents[1] / "scripts"
|
|
614
|
+
if str(LIB_DIR) not in sys.path:
|
|
615
|
+
sys.path.insert(0, str(LIB_DIR))
|
|
616
|
+
|
|
617
|
+
from okstra_ctl import consumers as cons
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def test_report_path_round_trips(tmp_path):
|
|
621
|
+
cons.append_consumer(
|
|
622
|
+
tmp_path, impl_task_key="k", stage=1, status="done",
|
|
623
|
+
head_commit="c1", report_path="runs/k/reports/final-report-1.md",
|
|
624
|
+
)
|
|
625
|
+
rows = cons.read_consumers(tmp_path)
|
|
626
|
+
assert rows[0]["report_path"] == "runs/k/reports/final-report-1.md"
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
- [ ] **Step 2: 테스트 통과 확인 (이미 지원하므로 바로 PASS)**
|
|
630
|
+
|
|
631
|
+
Run: `python3 -m pytest tests/test_consumers_report_path.py -v`
|
|
632
|
+
Expected: PASS (1 passed) — `**fields` 가 이미 동작함을 고정.
|
|
633
|
+
|
|
634
|
+
- [ ] **Step 3: lead 계약 갱신**
|
|
635
|
+
|
|
636
|
+
In `prompts/profiles/_implementation-deliverable.md`, [_implementation-deliverable.md:52](../../../prompts/profiles/_implementation-deliverable.md:52) 의 done 행 append 항목에 `report_path` 추가. 기존:
|
|
637
|
+
|
|
638
|
+
```
|
|
639
|
+
- For EACH stage in this run's batch: append a `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` with `completed_at`, `carry_path`, and the SHA of HEAD. Use the okstra runtime's `consumers_mutex` helper (NOT a raw filesystem write) to honour the lock.
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
변경 후:
|
|
643
|
+
|
|
644
|
+
```
|
|
645
|
+
- For EACH stage in this run's batch: append a `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` with `completed_at`, `carry_path`, `report_path` (this run's final-report path relative to the run root), and the SHA of HEAD. Use the okstra runtime's `consumers_mutex` helper (NOT a raw filesystem write) to honour the lock. `report_path` lets `final-verification` cite each stage's originating report when assembling its Source Implementation Report list.
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
- [ ] **Step 4: 커밋**
|
|
649
|
+
|
|
650
|
+
```bash
|
|
651
|
+
git add tests/test_consumers_report_path.py prompts/profiles/_implementation-deliverable.md
|
|
652
|
+
git commit -m "feat(profiles): consumers done 행에 report_path + round-trip 회귀 테스트"
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## Task 6: launch 템플릿 `{{VERIFICATION_TARGET}}` 주입
|
|
658
|
+
|
|
659
|
+
**Files:**
|
|
660
|
+
- Modify: `prompts/launch.template.md`
|
|
661
|
+
|
|
662
|
+
final-verification run 의 launch 프롬프트에 검증 target snapshot 을 주입한다. implementation 의 `{{STAGE_BATCH_DIRECTIVE}}` 와 대칭 ([launch.template.md:18](../../../prompts/launch.template.md:18)).
|
|
663
|
+
|
|
664
|
+
- [ ] **Step 1: 주입 토큰 추가**
|
|
665
|
+
|
|
666
|
+
In `prompts/launch.template.md`, [launch.template.md:18](../../../prompts/launch.template.md:18) 의 `{{STAGE_BATCH_DIRECTIVE}}` 줄 바로 다음에 추가:
|
|
667
|
+
|
|
668
|
+
```
|
|
669
|
+
{{VERIFICATION_TARGET}}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
- [ ] **Step 2: 빈 값 기본 처리 확인**
|
|
673
|
+
|
|
674
|
+
implementation/기타 phase 에서는 `VERIFICATION_TARGET` 이 ctx 에 없을 수 있다. 렌더러가 미정의 토큰을 빈 문자열로 치환하는지(기존 `STAGE_BATCH_DIRECTIVE` 가 implementation 외 phase 에서 빈 값으로 처리되는 방식과 동일한지) 확인하고, 그렇지 않으면 `_reserve_final_verification_target` 미호출 경로(다른 task_type)에서 `ctx.setdefault("VERIFICATION_TARGET", "")` 를 prepare 공통부에 추가.
|
|
675
|
+
|
|
676
|
+
Run: `grep -n "STAGE_BATCH_DIRECTIVE\|setdefault\|VERIFICATION_TARGET" scripts/okstra_ctl/run.py`
|
|
677
|
+
Expected: `STAGE_BATCH_DIRECTIVE` 의 빈값 기본 처리 패턴을 찾아 동일하게 적용.
|
|
678
|
+
|
|
679
|
+
- [ ] **Step 3: 렌더 스모크**
|
|
680
|
+
|
|
681
|
+
Run: `node bin/okstra --version`
|
|
682
|
+
Expected: 정상 출력 (템플릿 파싱 오류 없음). 더해 `python3 -m pytest tests/ -k "template or render" -q` 가 깨지지 않는지 확인.
|
|
683
|
+
|
|
684
|
+
- [ ] **Step 4: 커밋**
|
|
685
|
+
|
|
686
|
+
```bash
|
|
687
|
+
git add prompts/launch.template.md scripts/okstra_ctl/run.py
|
|
688
|
+
git commit -m "feat(launch): final-verification 에 VERIFICATION_TARGET 주입"
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## Task 7: final-verification 프로파일 재작성 (두 모드)
|
|
694
|
+
|
|
695
|
+
**Files:**
|
|
696
|
+
- Modify: `prompts/profiles/final-verification.md`
|
|
697
|
+
|
|
698
|
+
entry gate 를 "단일 report" 전제에서 "Python prep 이 해소·강제하는 두 모드" 로 재작성하고, Source Implementation Report 를 목록으로, routing 을 모드별로 제약한다.
|
|
699
|
+
|
|
700
|
+
- [ ] **Step 1: entry gate 재작성**
|
|
701
|
+
|
|
702
|
+
In `prompts/profiles/final-verification.md`, `Pre-verification entry gate` 블록([final-verification.md:25](../../../prompts/profiles/final-verification.md:25))을 교체:
|
|
703
|
+
|
|
704
|
+
```
|
|
705
|
+
- Pre-verification entry gate (resolved & enforced by `okstra render-bundle` prep — the lead does NOT recompute it):
|
|
706
|
+
- the verification target (scope / worktree / base / head / stages / source reports / diff stat) is injected as the `VERIFICATION_TARGET` block. The lead MUST treat it as authoritative and MUST NOT re-pick a target from the brief.
|
|
707
|
+
- **whole-task scope** (`--stage auto`, default): prep has already verified every Stage Map stage is `status:done` in `consumers.jsonl`, every done stage's `head_commit` is an ancestor of the task worktree HEAD (all stage branches merged), and the worktree is clean outside `.okstra/`. If any check failed the run never started (PrepareError); a started whole-task run is therefore a fully-merged, clean target.
|
|
708
|
+
- **single-stage scope** (`--stage N`): prep verified stage N is `status:done` and its isolated stage worktree exists and is clean. Other stages' state is irrelevant. A single-stage run is a partial verification and MUST NOT recommend `release-handoff`.
|
|
709
|
+
- the lead still captures `git rev-parse HEAD` / `git status --short` from the injected worktree to confirm the analysis ran against the injected head; a mismatch is a `tool-failure`, not a silent proceed.
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
- [ ] **Step 2: Source Implementation Report 를 목록으로**
|
|
713
|
+
|
|
714
|
+
[final-verification.md:31](../../../prompts/profiles/final-verification.md:31) 의 `Source Implementation Report` deliverable 항목을 교체:
|
|
715
|
+
|
|
716
|
+
```
|
|
717
|
+
- **Source Implementation Report(s)**: the `VERIFICATION_TARGET` snapshot verbatim — verification scope, worktree path, base/head SHAs, the list of stages under verification, and one row per stage citing its originating implementation final-report (`report_path` from `consumers.jsonl`; render `(report_path unrecorded)` when absent). The lead injects this same snapshot into every analyser prompt (`**Verification scope:** / **Worktree:** / **Verification base ref:** / **Verification head SHA:** / **Verification diff stat:**`); a worker that cannot confirm its analysis ran against that exact head MUST record a `tool-failure`.
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
- [ ] **Step 3: routing 제약 추가**
|
|
721
|
+
|
|
722
|
+
[final-verification.md:38](../../../prompts/profiles/final-verification.md:38) 의 `Routing recommendation` 항목 끝에 한 문장 추가:
|
|
723
|
+
|
|
724
|
+
```
|
|
725
|
+
`release-handoff` is additionally allowed ONLY when the verification scope (the `Verification scope:` line of the injected `VERIFICATION_TARGET` block, recorded as the report's `verificationScope` field) is `whole-task`; a `single-stage` run is partial and routes to `implementation` / `done` even on an `accepted` verdict.
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
- [ ] **Step 4: 토큰 일관성 grep**
|
|
729
|
+
|
|
730
|
+
Run: `grep -rn "VERIFICATION_TARGET\|verificationScope\|whole-task\|single-stage" prompts/ scripts/okstra_ctl/run.py validators/validate-run.py`
|
|
731
|
+
Expected: `VERIFICATION_TARGET` 는 run.py(주입)·launch.template(치환)·profile(소비) 에서 일치; `verificationScope` 는 profile·validate-run·스키마 에서 일치; scope 값은 `whole-task`/`single-stage` 두 가지로만 등장.
|
|
732
|
+
|
|
733
|
+
- [ ] **Step 5: 커밋**
|
|
734
|
+
|
|
735
|
+
```bash
|
|
736
|
+
git add prompts/profiles/final-verification.md
|
|
737
|
+
git commit -m "feat(profiles/final-verification): 두 모드 entry gate + Source Implementation Report 목록 + routing 제약"
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## Task 8: wizard final-verification 서브플로우 (TDD)
|
|
743
|
+
|
|
744
|
+
**Files:**
|
|
745
|
+
- Modify: `scripts/okstra_ctl/wizard.py`
|
|
746
|
+
- Test: `tests/test_wizard_final_verification_stage.py` (신규)
|
|
747
|
+
|
|
748
|
+
final-verification 에 approved-plan picker 와 stage 3-옵션 picker(추천=전체 `auto` / 특정 stage / 직접 입력)를 노출하고, `selected_stage` 를 final-verification 에서도 render-args 로 emit 한다.
|
|
749
|
+
|
|
750
|
+
- [ ] **Step 1: 현재 wizard 흐름 확인**
|
|
751
|
+
|
|
752
|
+
Run: `grep -n "selected_stage\|_render_stage_picker\|approved_plan\|task_type ==\|task_type in\|render_args\|\"stage\"" scripts/okstra_ctl/wizard.py`
|
|
753
|
+
Expected: implementation 전용 stage/approved-plan 단계 위치 파악. 특히 [wizard.py:2353](../../../scripts/okstra_ctl/wizard.py:2353) `"stage": ... if state.task_type == "implementation" else ""` 와 stage picker step 의 `applies=` 조건.
|
|
754
|
+
|
|
755
|
+
- [ ] **Step 2: 실패 테스트 작성**
|
|
756
|
+
|
|
757
|
+
Create `tests/test_wizard_final_verification_stage.py`:
|
|
758
|
+
|
|
759
|
+
```python
|
|
760
|
+
"""final-verification 이 render-args 에 selected_stage 를 emit 하는지."""
|
|
761
|
+
import sys
|
|
762
|
+
from pathlib import Path
|
|
763
|
+
|
|
764
|
+
LIB_DIR = Path(__file__).resolve().parents[1] / "scripts"
|
|
765
|
+
if str(LIB_DIR) not in sys.path:
|
|
766
|
+
sys.path.insert(0, str(LIB_DIR))
|
|
767
|
+
|
|
768
|
+
from okstra_ctl import wizard as wiz
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def test_final_verification_emits_stage():
|
|
772
|
+
state = wiz.WizardState()
|
|
773
|
+
state.task_type = "final-verification"
|
|
774
|
+
state.selected_stage = "2"
|
|
775
|
+
args = wiz.render_args(state) # [wizard.py:2332] render_args(state) -> dict
|
|
776
|
+
assert args["stage"] == "2"
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def test_implementation_still_emits_stage():
|
|
780
|
+
state = wiz.WizardState()
|
|
781
|
+
state.task_type = "implementation"
|
|
782
|
+
state.selected_stage = "auto"
|
|
783
|
+
args = wiz._render_args(state)
|
|
784
|
+
assert args["stage"] == "auto"
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
- [ ] **Step 3: 테스트 실패 확인**
|
|
788
|
+
|
|
789
|
+
Run: `python3 -m pytest tests/test_wizard_final_verification_stage.py -v`
|
|
790
|
+
Expected: FAIL — final-verification 의 `stage` 가 `""` 로 emit 됨.
|
|
791
|
+
|
|
792
|
+
- [ ] **Step 4: emit 조건 확장**
|
|
793
|
+
|
|
794
|
+
In `scripts/okstra_ctl/wizard.py`, [wizard.py:2353](../../../scripts/okstra_ctl/wizard.py:2353):
|
|
795
|
+
|
|
796
|
+
```python
|
|
797
|
+
"stage": (state.selected_stage or "auto") if state.task_type in ("implementation", "final-verification") else "",
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
그리고 stage picker step 과 approved-plan step 의 `applies=` 람다([wizard.py:2007](../../../scripts/okstra_ctl/wizard.py:2007) 인근 그룹)를 final-verification 도 포함하도록 확장. stage picker 추천 라벨은 전체-task 기준으로 `auto` = "전체 task (모든 stage 머지 후 한 번)" 로 문구를 맞추고, 마지막 옵션은 항상 "직접 입력" 유지(피드백 규칙: 3-옵션 picker).
|
|
801
|
+
|
|
802
|
+
- [ ] **Step 5: 테스트 통과 확인**
|
|
803
|
+
|
|
804
|
+
Run: `python3 -m pytest tests/test_wizard_final_verification_stage.py -v`
|
|
805
|
+
Expected: PASS (2 passed)
|
|
806
|
+
|
|
807
|
+
- [ ] **Step 6: 커밋**
|
|
808
|
+
|
|
809
|
+
```bash
|
|
810
|
+
git add scripts/okstra_ctl/wizard.py tests/test_wizard_final_verification_stage.py
|
|
811
|
+
git commit -m "feat(wizard): final-verification 에 stage 3-옵션 picker + approved-plan"
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
---
|
|
815
|
+
|
|
816
|
+
## Task 9: validate-run 모드/라우팅 검증 (TDD)
|
|
817
|
+
|
|
818
|
+
**Files:**
|
|
819
|
+
- Modify: `validators/validate-run.py`
|
|
820
|
+
- Test: `tests/test_validate_run.py` (append; 파일 존재 위치는 Step 1 에서 확인)
|
|
821
|
+
|
|
822
|
+
`verificationScope` 필드를 인식하고, `release-handoff` routing 추천은 `whole-task` 일 때만 허용한다.
|
|
823
|
+
|
|
824
|
+
- [ ] **Step 1: 기존 final-verification 검증 블록 확인**
|
|
825
|
+
|
|
826
|
+
Run: `grep -n "_validate_final_verification_consistency\|verdictToken\|routingRecommendation\|failures.append" validators/validate-run.py`
|
|
827
|
+
Expected: 강제 함수는 `_validate_final_verification_consistency(data, failures)` ([validate-run.py:1066](../../../validators/validate-run.py:1066)) — `failures` 리스트에 메시지를 append 하는 방식. data dict 의 final-verification 필드명(`verdictToken` / `routingRecommendation` 등)을 정확히 확인.
|
|
828
|
+
|
|
829
|
+
- [ ] **Step 2: 실패 테스트 작성**
|
|
830
|
+
|
|
831
|
+
Append to `tests/test_validate_run.py` (없으면 신규 생성, import 는 기존 테스트 패턴을 따름):
|
|
832
|
+
|
|
833
|
+
```python
|
|
834
|
+
# vr = 기존 test_validate_run.py 가 validate-run.py 를 로드하는 방식 그대로 재사용
|
|
835
|
+
# (파일명에 하이픈이 있어 importlib.spec_from_file_location 로 로드 — 기존 패턴 따름).
|
|
836
|
+
# valid_fv_data() 는 기존 테스트의 final-verification 유효 data dict 빌더를 재사용하거나,
|
|
837
|
+
# 없으면 Step 1 에서 확인한 필수 필드로 최소 유효 dict 를 만드는 헬퍼로 추가.
|
|
838
|
+
|
|
839
|
+
def test_single_stage_cannot_route_release_handoff():
|
|
840
|
+
data = valid_fv_data()
|
|
841
|
+
data["verificationScope"] = "single-stage"
|
|
842
|
+
data["routingRecommendation"] = "release-handoff"
|
|
843
|
+
failures = []
|
|
844
|
+
vr._validate_final_verification_consistency(data, failures)
|
|
845
|
+
assert any("single-stage" in f for f in failures)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def test_whole_task_may_route_release_handoff():
|
|
849
|
+
data = valid_fv_data()
|
|
850
|
+
data["verificationScope"] = "whole-task"
|
|
851
|
+
data["verdictToken"] = "accepted"
|
|
852
|
+
data["routingRecommendation"] = "release-handoff"
|
|
853
|
+
failures = []
|
|
854
|
+
vr._validate_final_verification_consistency(data, failures)
|
|
855
|
+
assert not any("single-stage" in f for f in failures)
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
주: `vr` 모듈 핸들과 `valid_fv_data` 빌더는 기존 `tests/test_validate_run.py` 의 final-verification 테스트에서 쓰는 로더/빌더를 그대로 재사용한다(Step 1 에서 위치·이름 확인). 이 validator 파일은 하이픈 파일명이라 package import 가 불가하므로 importlib 로드가 맞다(run.py/wizard 와 다름).
|
|
859
|
+
|
|
860
|
+
- [ ] **Step 3: 테스트 실패 확인**
|
|
861
|
+
|
|
862
|
+
Run: `python3 -m pytest tests/test_validate_run.py -k "scope or release_handoff" -v`
|
|
863
|
+
Expected: FAIL (규칙 미구현).
|
|
864
|
+
|
|
865
|
+
- [ ] **Step 4: 검증 규칙 구현**
|
|
866
|
+
|
|
867
|
+
In `validators/validate-run.py`, `_validate_final_verification_consistency(data, failures)` ([validate-run.py:1066](../../../validators/validate-run.py:1066)) 본문 끝에 추가:
|
|
868
|
+
|
|
869
|
+
```python
|
|
870
|
+
scope = data.get("verificationScope", "whole-task")
|
|
871
|
+
if scope not in ("whole-task", "single-stage"):
|
|
872
|
+
failures.append(
|
|
873
|
+
f"verificationScope must be whole-task|single-stage, got {scope!r}")
|
|
874
|
+
routing = data.get("routingRecommendation", "")
|
|
875
|
+
if scope == "single-stage" and routing == "release-handoff":
|
|
876
|
+
failures.append(
|
|
877
|
+
"verificationScope=single-stage cannot recommend release-handoff "
|
|
878
|
+
"(partial verification)"
|
|
879
|
+
)
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
(data dict 의 routing 필드명이 `routingRecommendation` 이 아니면 Step 1 에서 확인한 실제 키로 맞춘다.)
|
|
883
|
+
|
|
884
|
+
- [ ] **Step 5: 테스트 통과 + 워크플로 검증**
|
|
885
|
+
|
|
886
|
+
Run: `python3 -m pytest tests/test_validate_run.py -v && bash validators/validate-workflow.sh`
|
|
887
|
+
Expected: PASS / OK.
|
|
888
|
+
|
|
889
|
+
- [ ] **Step 6: 커밋**
|
|
890
|
+
|
|
891
|
+
```bash
|
|
892
|
+
git add validators/validate-run.py tests/test_validate_run.py
|
|
893
|
+
git commit -m "feat(validators): final-verification verificationScope + release-handoff routing 제약"
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
898
|
+
## Task 10: 입력 템플릿 + 스키마 필드
|
|
899
|
+
|
|
900
|
+
**Files:**
|
|
901
|
+
- Modify: `templates/reports/final-verification-input.template.md`
|
|
902
|
+
- Modify: report data.json 스키마 (Step 1 에서 위치 확인)
|
|
903
|
+
|
|
904
|
+
`verificationScope` 와 stage 목록을 report data.json 스키마에 추가하고, 입력 템플릿이 두 모드를 안내하도록 갱신한다.
|
|
905
|
+
|
|
906
|
+
- [ ] **Step 1: 스키마 위치 확인**
|
|
907
|
+
|
|
908
|
+
Run: `grep -rn "verdictToken\|routingRecommendation\|final-verification" templates/ | grep -i "schema\|json"`
|
|
909
|
+
Expected: final-verification report data.json 스키마 파일 경로 파악.
|
|
910
|
+
|
|
911
|
+
- [ ] **Step 2: 스키마에 `verificationScope` 추가**
|
|
912
|
+
|
|
913
|
+
해당 스키마의 properties 에 추가 (정확한 들여쓰기는 파일에 맞춤):
|
|
914
|
+
|
|
915
|
+
```json
|
|
916
|
+
"verificationScope": { "type": "string", "enum": ["whole-task", "single-stage"] }
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
required 목록에 `verificationScope` 포함 여부는 기존 required 정책에 맞춘다(신규 필드라 pre-1.0 정책상 required 로 추가 가능).
|
|
920
|
+
|
|
921
|
+
- [ ] **Step 3: 입력 템플릿 갱신**
|
|
922
|
+
|
|
923
|
+
In `templates/reports/final-verification-input.template.md`, brief 가 worktree/base 를 수동 기입하던 안내가 있으면 제거하고, 다음 안내를 추가:
|
|
924
|
+
|
|
925
|
+
```
|
|
926
|
+
## 검증 모드
|
|
927
|
+
|
|
928
|
+
- 기본은 **전체-task** 검증입니다(`--stage auto`). 모든 Stage Map stage 가 구현·머지된 뒤 한 번 실행하세요.
|
|
929
|
+
- 특정 stage 만 격리 검증하려면 `--stage N` 으로 단독-stage 모드를 쓰세요(release-handoff 진입 불가).
|
|
930
|
+
- worktree / base / head 는 okstra 가 registry 와 consumers.jsonl 에서 자동 해소하므로 수동 기입하지 않습니다.
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
- [ ] **Step 4: 렌더 회귀**
|
|
934
|
+
|
|
935
|
+
Run: `python3 -m pytest tests/ -k "template or render" -q`
|
|
936
|
+
Expected: PASS (golden 갱신이 필요하면 빌드로 재생성 후 diff 검토).
|
|
937
|
+
|
|
938
|
+
- [ ] **Step 5: 커밋**
|
|
939
|
+
|
|
940
|
+
```bash
|
|
941
|
+
git add templates/
|
|
942
|
+
git commit -m "feat(templates): final-verification verificationScope 스키마 + 입력 안내 두 모드"
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
---
|
|
946
|
+
|
|
947
|
+
## Task 11: e2e 시나리오 + 빌드 + 전체 스위트
|
|
948
|
+
|
|
949
|
+
**Files:**
|
|
950
|
+
- Create: `tests-e2e/scenario-<id>-final-verification-modes.sh`
|
|
951
|
+
|
|
952
|
+
기존 e2e 패턴(`tests-e2e/scenario-<id>-<name>.sh`, `mktemp -d` OKSTRA_HOME, `trap` cleanup)을 따른다.
|
|
953
|
+
|
|
954
|
+
- [ ] **Step 1: 시나리오 작성 (E1/E2/E3)**
|
|
955
|
+
|
|
956
|
+
Create `tests-e2e/scenario-<id>-final-verification-modes.sh`. 기존 `tests-e2e/scenario-01-record-start-reconcile.sh` 의 골격(임시 OKSTRA_HOME, 임시 git project, render-bundle 호출)을 복제하고 다음 3 케이스를 검증:
|
|
957
|
+
|
|
958
|
+
- **E1 (whole-task pass):** 2-stage plan, stage 1·2 를 implementation 으로 돌려 consumers done 2줄 + 두 stage 브랜치를 task worktree 에 머지 → `render-bundle --task-type final-verification --stage auto` 가 성공하고 출력 bundle 에 `VERIFICATION_SCOPE=whole-task` 가 보일 것.
|
|
959
|
+
- **E2 (whole-task block):** stage 1 done+머지, stage 2 done 이지만 미머지 → `--stage auto` 가 PrepareError(`not merged`)로 비정상 종료(exit≠0)할 것.
|
|
960
|
+
- **E3 (single-stage pass):** stage 1 구현 직후 머지 전 → `--stage 1` 이 성공하고 `VERIFICATION_SCOPE=single-stage` 일 것.
|
|
961
|
+
|
|
962
|
+
각 케이스는 `render-bundle` 종료코드와 stdout grep 으로 단언한다(실제 worker 실행은 e2e 범위 밖).
|
|
963
|
+
|
|
964
|
+
- [ ] **Step 2: e2e 실행**
|
|
965
|
+
|
|
966
|
+
Run: `bash tests-e2e/scenario-<id>-final-verification-modes.sh`
|
|
967
|
+
Expected: 3 케이스 모두 통과 출력.
|
|
968
|
+
|
|
969
|
+
- [ ] **Step 3: 빌드 동기화**
|
|
970
|
+
|
|
971
|
+
Run: `npm run build`
|
|
972
|
+
Expected: 성공. `runtime/` 갱신은 커밋하지 않는다.
|
|
973
|
+
|
|
974
|
+
- [ ] **Step 4: 전체 스위트**
|
|
975
|
+
|
|
976
|
+
Run: `python3 -m pytest tests/ -q && bash validators/validate-workflow.sh`
|
|
977
|
+
Expected: 전부 PASS / OK.
|
|
978
|
+
|
|
979
|
+
- [ ] **Step 5: 커밋**
|
|
980
|
+
|
|
981
|
+
```bash
|
|
982
|
+
git add tests-e2e/scenario-<id>-final-verification-modes.sh
|
|
983
|
+
git commit -m "test(e2e): final-verification 전체-task/단독-stage 모드 시나리오"
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
---
|
|
987
|
+
|
|
988
|
+
## 미해결 / 구현 중 확정 항목
|
|
989
|
+
|
|
990
|
+
- **`plan_run_root` 해소** (spec §10): 본 계획은 final-verification 이 implementation 과 동일하게 `--approved-plan` 을 받아 `Path(approved_plan_path).parents[1]` 로 plan_run_root 를 도출하는 방식으로 확정했다(Task 4). wizard 가 final-verification 에서 approved-plan 을 반드시 받도록 Task 8 에서 강제한다.
|
|
991
|
+
- **stage worktree teardown** (spec §10): 단독-stage 모드에서 stage worktree 가 정리된 경우 `get_stage_row` 가 None → `worktree_path=""` → `_resolve_single_stage_target` 이 "stage worktree not found … use whole-task mode" PrepareError(Task 3 에서 커버).
|
|
992
|
+
- **golden 렌더 fixture**: Task 6/10 에서 렌더 결과 golden md 가 깨지면 `npm run build` 후 렌더러로 재생성하고 diff 를 검토해 갱신한다(수작업 전사 금지).
|
|
993
|
+
```
|