okstra 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.kr.md +26 -16
- package/README.md +26 -16
- package/docs/kr/architecture.md +59 -45
- package/docs/kr/cli.md +61 -18
- package/docs/pr-template-usage.md +65 -0
- package/docs/project-structure-overview.md +358 -354
- package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +1 -1
- package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1 -1
- package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +1 -1
- package/docs/superpowers/plans/2026-05-20-final-report-language.md +1501 -0
- package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +1267 -0
- package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +1007 -0
- package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +720 -0
- package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +681 -0
- package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +1691 -0
- package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +383 -0
- package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +320 -0
- package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +299 -0
- package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +335 -0
- package/docs/task-process/README.md +74 -0
- package/docs/task-process/common-flow.md +166 -0
- package/docs/task-process/error-analysis.md +101 -0
- package/docs/task-process/final-verification.md +167 -0
- package/docs/task-process/implementation-planning.md +128 -0
- package/docs/task-process/implementation.md +149 -0
- package/docs/task-process/release-handoff.md +206 -0
- package/docs/task-process/requirements-discovery.md +115 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +29 -13
- package/runtime/agents/workers/claude-worker.md +26 -0
- package/runtime/agents/workers/codex-worker.md +27 -1
- package/runtime/agents/workers/gemini-worker.md +27 -1
- package/runtime/agents/workers/report-writer-worker.md +8 -1
- package/runtime/bin/okstra-central.sh +6 -6
- package/runtime/bin/okstra-codex-exec.sh +49 -28
- package/runtime/bin/okstra-gemini-exec.sh +39 -21
- package/runtime/bin/okstra-render-final-report.py +13 -2
- package/runtime/bin/okstra-wrapper-status.py +155 -0
- package/runtime/bin/okstra.sh +2 -2
- package/runtime/prompts/profiles/_common-contract.md +11 -6
- package/runtime/prompts/profiles/error-analysis.md +3 -7
- package/runtime/prompts/profiles/implementation-planning.md +22 -21
- package/runtime/prompts/profiles/implementation.md +28 -11
- package/runtime/prompts/profiles/improvement-discovery.md +42 -0
- package/runtime/prompts/profiles/kr/_common-contract.md +92 -0
- package/runtime/prompts/profiles/kr/error-analysis.md +36 -0
- package/runtime/prompts/profiles/kr/final-verification.md +48 -0
- package/runtime/prompts/profiles/kr/implementation-planning.md +90 -0
- package/runtime/prompts/profiles/kr/implementation.md +144 -0
- package/runtime/prompts/profiles/kr/improvement-discovery.md +42 -0
- package/runtime/prompts/profiles/kr/release-handoff.md +104 -0
- package/runtime/prompts/profiles/kr/requirements-discovery.md +42 -0
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +8 -12
- package/runtime/prompts/wizard/prompts.ko.json +230 -0
- package/runtime/python/lib/okstra/cli.sh +2 -49
- package/runtime/python/lib/okstra/globals.sh +21 -21
- package/runtime/python/lib/okstra/interactive.sh +7 -7
- package/runtime/python/okstra_ctl/clarification_items.py +3 -9
- package/runtime/python/okstra_ctl/consumers.py +53 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +0 -7
- package/runtime/python/okstra_ctl/i18n.py +73 -0
- package/runtime/python/okstra_ctl/improvement_lenses.py +44 -0
- package/runtime/python/okstra_ctl/index.py +1 -1
- package/runtime/python/okstra_ctl/paths.py +23 -20
- package/runtime/python/okstra_ctl/render.py +147 -202
- package/runtime/python/okstra_ctl/render_final_report.py +53 -10
- package/runtime/python/okstra_ctl/run.py +292 -107
- package/runtime/python/okstra_ctl/run_context.py +22 -0
- package/runtime/python/okstra_ctl/seeding.py +186 -0
- package/runtime/python/okstra_ctl/wizard.py +348 -127
- package/runtime/python/okstra_ctl/workflow.py +21 -2
- package/runtime/python/okstra_ctl/worktree.py +54 -1
- package/runtime/python/okstra_project/resolver.py +4 -3
- package/runtime/python/okstra_token_usage/report.py +2 -2
- package/runtime/schemas/final-report-v1.0.schema.json +22 -16
- package/runtime/skills/okstra-brief/SKILL.md +124 -31
- package/runtime/skills/okstra-convergence/SKILL.md +2 -3
- package/runtime/skills/okstra-report-writer/SKILL.md +35 -15
- package/runtime/skills/okstra-run/SKILL.md +5 -4
- package/runtime/skills/okstra-schedule/SKILL.md +4 -4
- package/runtime/skills/okstra-setup/SKILL.md +27 -0
- package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
- package/runtime/templates/okstra.CLAUDE.md +104 -0
- package/runtime/templates/reports/final-report.template.md +93 -98
- package/runtime/templates/reports/i18n/en.json +135 -0
- package/runtime/templates/reports/i18n/ko.json +135 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +18 -0
- package/runtime/templates/reports/improvement-discovery-input.template.md +78 -0
- package/runtime/templates/reports/task-brief.template.md +2 -2
- package/runtime/validators/lib/fixtures.sh +30 -0
- package/runtime/validators/lib/runners.sh +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +211 -0
- package/runtime/validators/validate-run.py +121 -26
- package/runtime/validators/validate-workflow.sh +2 -2
- package/runtime/validators/validate_improvement_report.py +275 -0
- package/src/config.mjs +18 -0
- package/src/install.mjs +41 -14
- package/src/setup.mjs +133 -1
- package/src/uninstall.mjs +21 -1
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
# implementation-planning multi-stage 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:** `implementation-planning` task 가 항상 `Stage Map + N stages` 구조로 산출하고, `implementation` task 가 stage 단위로 분할 실행되며 stage 간 carry-in 이 정적 명세 + sidecar evidence JSON 으로 자동 전달되도록 한다.
|
|
6
|
+
|
|
7
|
+
**Architecture:** seed 단계의 profile / template 갱신 → 신규 validator 가 산출을 강제 → `run.py` 가 `--stage` 인수와 auto 선택 알고리즘으로 stage 단위 실행 → `wizard.py` 에 동적 stage picker → worker / lead prompt 가 evidence sidecar 와 `consumers.jsonl` 을 작성. carry-in 은 (a) `Stage Exit Contract` 정적 명세 + (b) `runs/<impl-key>/carry/stage-<N>.json` 런타임 evidence 두 갈래로 자동 흡수.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3.x (argparse / fcntl / pytest), Markdown profile 파일, bash entry. 신규 의존성 없음.
|
|
10
|
+
|
|
11
|
+
**Spec 참조:** [docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md](../specs/2026-05-20-implementation-planning-multi-stage-design.md)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Task DAG (병렬 wave)
|
|
16
|
+
|
|
17
|
+
| Task | depends-on | wave | 한 줄 요약 |
|
|
18
|
+
|------|------------|------|-----------|
|
|
19
|
+
| 1 | (none) | wave-1 | Stage validator `validate-implementation-plan-stages.py` + 테스트 |
|
|
20
|
+
| 2 | (none) | wave-1 | `paths.py` 의 carry dir + `consumers.jsonl` helper + lock + 테스트 |
|
|
21
|
+
| 3 | (none) | wave-1 | `implementation-planning.md` profile + input template 갱신 |
|
|
22
|
+
| 4 | (none) | wave-1 | `implementation.md` profile 갱신 (stage 추출 / sidecar / PR 분리) |
|
|
23
|
+
| 5 | 1, 2 | wave-2 | `run.py` 에 `--stage` 인수 + validator 호출 + `parsed_stage_map` ctx |
|
|
24
|
+
| 6 | 2, 5 | wave-3 | `run.py` 의 auto stage 선택 + `consumers.jsonl` append |
|
|
25
|
+
| 7 | 1, 5 | wave-3 | `wizard.py` 의 `stage_pick` step 동적 추가 |
|
|
26
|
+
| 8 | 4 | wave-2 | `agents/workers/{claude,codex,gemini}-worker.md` evidence JSON 책임 |
|
|
27
|
+
| 9 | 2, 4, 8 | wave-3 | lead prompt 의 sidecar 저장 + consumers 갱신 + stage PR 본문 |
|
|
28
|
+
| 10 | 1–9 | wave-4 | e2e 시나리오 자동화 (Q1~Q9 of spec §9) |
|
|
29
|
+
|
|
30
|
+
wave-1 (Task 1·2·3·4) 은 4 개 동시 가능. wave-2 (Task 5·8) 도 동시 가능. wave-3 (Task 6·7·9) 동시 가능. wave-4 는 최종.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Task 1: Stage validator 신규
|
|
35
|
+
|
|
36
|
+
**Files:**
|
|
37
|
+
- Create: `validators/validate-implementation-plan-stages.py`
|
|
38
|
+
- Create: `tests/test_validate_implementation_plan_stages.py`
|
|
39
|
+
- Create: `tests/fixtures/plans/valid_one_stage.md`
|
|
40
|
+
- Create: `tests/fixtures/plans/valid_three_stage_parallel.md`
|
|
41
|
+
- Create: `tests/fixtures/plans/invalid_step_overflow.md`
|
|
42
|
+
- Create: `tests/fixtures/plans/invalid_depends_on_cycle.md`
|
|
43
|
+
- Create: `tests/fixtures/plans/invalid_missing_subsections.md`
|
|
44
|
+
|
|
45
|
+
- [ ] **Step 1: Write failing test for valid 1-stage plan**
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# tests/test_validate_implementation_plan_stages.py
|
|
49
|
+
import subprocess
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
|
|
52
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
53
|
+
VALIDATOR = REPO / "validators" / "validate-implementation-plan-stages.py"
|
|
54
|
+
FIXTURES = REPO / "tests" / "fixtures" / "plans"
|
|
55
|
+
|
|
56
|
+
def _run(fixture: str):
|
|
57
|
+
return subprocess.run(
|
|
58
|
+
["python3", str(VALIDATOR), "--plan", str(FIXTURES / fixture)],
|
|
59
|
+
capture_output=True, text=True,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def test_one_stage_valid():
|
|
63
|
+
r = _run("valid_one_stage.md")
|
|
64
|
+
assert r.returncode == 0, r.stderr
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- [ ] **Step 2: Create the valid 1-stage fixture**
|
|
68
|
+
|
|
69
|
+
`tests/fixtures/plans/valid_one_stage.md`:
|
|
70
|
+
```markdown
|
|
71
|
+
# Final Report
|
|
72
|
+
|
|
73
|
+
## 4.5 Stage Map
|
|
74
|
+
|
|
75
|
+
| stage | title | depends-on | step-count | exit-contract-summary |
|
|
76
|
+
|-------|-------|-----------|------------|------------------------|
|
|
77
|
+
| 1 | only stage | (none) | 2 | src/foo.ts:bar |
|
|
78
|
+
|
|
79
|
+
## 4.5.1 Stage 1: only stage
|
|
80
|
+
|
|
81
|
+
### Carry-In
|
|
82
|
+
- task-brief 의 요구사항
|
|
83
|
+
|
|
84
|
+
### Stepwise Execution Order
|
|
85
|
+
|
|
86
|
+
| step | action | files | command | expected |
|
|
87
|
+
|------|--------|-------|---------|----------|
|
|
88
|
+
| 1 | write failing test | tests/test_foo.py | pytest -k bar | FAIL |
|
|
89
|
+
| 2 | implement bar | src/foo.ts | pytest -k bar | PASS |
|
|
90
|
+
|
|
91
|
+
### Stage Exit Contract
|
|
92
|
+
- 추가: src/foo.ts:bar
|
|
93
|
+
|
|
94
|
+
### Stage Validation
|
|
95
|
+
- post: pytest -k bar
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- [ ] **Step 3: Run test, expect failure (validator not yet implemented)**
|
|
99
|
+
|
|
100
|
+
Run: `pytest tests/test_validate_implementation_plan_stages.py::test_one_stage_valid -v`
|
|
101
|
+
Expected: FAIL with `FileNotFoundError` or `returncode != 0`.
|
|
102
|
+
|
|
103
|
+
- [ ] **Step 4: Implement validator skeleton (S1–S4)**
|
|
104
|
+
|
|
105
|
+
`validators/validate-implementation-plan-stages.py`:
|
|
106
|
+
```python
|
|
107
|
+
#!/usr/bin/env python3
|
|
108
|
+
"""S1–S8 checks for the Stage Map structure of an approved
|
|
109
|
+
implementation-planning final-report.md. Run from prepare_task_bundle
|
|
110
|
+
of `implementation` task or standalone."""
|
|
111
|
+
|
|
112
|
+
from __future__ import annotations
|
|
113
|
+
|
|
114
|
+
import argparse
|
|
115
|
+
import re
|
|
116
|
+
import sys
|
|
117
|
+
from dataclasses import dataclass
|
|
118
|
+
from pathlib import Path
|
|
119
|
+
from typing import List, Tuple
|
|
120
|
+
|
|
121
|
+
STAGE_MAP_HEADING = re.compile(r"^##\s+4\.5\s+Stage\s+Map\b", re.M)
|
|
122
|
+
STAGE_SECTION = re.compile(
|
|
123
|
+
r"^##\s+4\.5\.(\d+)\s+Stage\s+\1\s*:\s*(.+)$", re.M
|
|
124
|
+
)
|
|
125
|
+
REQUIRED_SUBSECTIONS = ("Carry-In", "Stepwise Execution Order",
|
|
126
|
+
"Stage Exit Contract", "Stage Validation")
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class StageMeta:
|
|
130
|
+
stage_number: int
|
|
131
|
+
title: str
|
|
132
|
+
depends_on: List[int]
|
|
133
|
+
step_count: int
|
|
134
|
+
exit_contract_summary: str
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class ValidationError:
|
|
138
|
+
code: str # S1..S8
|
|
139
|
+
stage: int # 0 = global
|
|
140
|
+
message: str
|
|
141
|
+
|
|
142
|
+
def _check_stage_map_present(text: str) -> List[ValidationError]:
|
|
143
|
+
if not STAGE_MAP_HEADING.search(text):
|
|
144
|
+
return [ValidationError("S1", 0,
|
|
145
|
+
"section '## 4.5 Stage Map' is missing")]
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
def main(argv: List[str]) -> int:
|
|
149
|
+
p = argparse.ArgumentParser()
|
|
150
|
+
p.add_argument("--plan", required=True)
|
|
151
|
+
args = p.parse_args(argv)
|
|
152
|
+
text = Path(args.plan).read_text(encoding="utf-8")
|
|
153
|
+
errors: List[ValidationError] = []
|
|
154
|
+
errors.extend(_check_stage_map_present(text))
|
|
155
|
+
if errors:
|
|
156
|
+
for e in errors:
|
|
157
|
+
print(f"{e.code} stage={e.stage}: {e.message}", file=sys.stderr)
|
|
158
|
+
return 1
|
|
159
|
+
return 0
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
sys.exit(main(sys.argv[1:]))
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- [ ] **Step 5: Run test, confirm S1 passes for 1-stage fixture**
|
|
166
|
+
|
|
167
|
+
Run: `pytest tests/test_validate_implementation_plan_stages.py::test_one_stage_valid -v`
|
|
168
|
+
Expected: PASS.
|
|
169
|
+
|
|
170
|
+
- [ ] **Step 6: Add failing tests for S2 (monotonic) – S8 (DAG)**
|
|
171
|
+
|
|
172
|
+
Append to `tests/test_validate_implementation_plan_stages.py`:
|
|
173
|
+
```python
|
|
174
|
+
def test_three_stage_parallel_valid():
|
|
175
|
+
r = _run("valid_three_stage_parallel.md")
|
|
176
|
+
assert r.returncode == 0, r.stderr
|
|
177
|
+
|
|
178
|
+
def test_step_overflow_rejected():
|
|
179
|
+
r = _run("invalid_step_overflow.md")
|
|
180
|
+
assert r.returncode == 1
|
|
181
|
+
assert "S5" in r.stderr
|
|
182
|
+
|
|
183
|
+
def test_missing_subsections_rejected():
|
|
184
|
+
r = _run("invalid_missing_subsections.md")
|
|
185
|
+
assert r.returncode == 1
|
|
186
|
+
assert "S4" in r.stderr
|
|
187
|
+
|
|
188
|
+
def test_depends_on_cycle_rejected():
|
|
189
|
+
r = _run("invalid_depends_on_cycle.md")
|
|
190
|
+
assert r.returncode == 1
|
|
191
|
+
assert "S8" in r.stderr
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
And create the three fixtures:
|
|
195
|
+
- `valid_three_stage_parallel.md`: stage 1·2 with `(none)`, stage 3 with `1, 2`, each ≤ 6 steps.
|
|
196
|
+
- `invalid_step_overflow.md`: stage 1 with 7 step rows in `Stepwise Execution Order`.
|
|
197
|
+
- `invalid_missing_subsections.md`: stage 1 missing the `Stage Validation` heading.
|
|
198
|
+
- `invalid_depends_on_cycle.md`: stage 1 `depends-on 2`, stage 2 `depends-on 1`.
|
|
199
|
+
|
|
200
|
+
(Use the same template as `valid_one_stage.md` with the relevant mutation.)
|
|
201
|
+
|
|
202
|
+
- [ ] **Step 7: Implement S2–S8 checks**
|
|
203
|
+
|
|
204
|
+
Extend `validators/validate-implementation-plan-stages.py`:
|
|
205
|
+
```python
|
|
206
|
+
def _parse_stage_map(text: str) -> Tuple[List[StageMeta], List[ValidationError]]:
|
|
207
|
+
# Locate the table immediately after "## 4.5 Stage Map"
|
|
208
|
+
m = STAGE_MAP_HEADING.search(text)
|
|
209
|
+
if not m:
|
|
210
|
+
return [], [] # S1 already reported
|
|
211
|
+
body = text[m.end():]
|
|
212
|
+
rows = []
|
|
213
|
+
for line in body.splitlines():
|
|
214
|
+
if line.startswith("##"):
|
|
215
|
+
break
|
|
216
|
+
if not line.strip().startswith("|"):
|
|
217
|
+
continue
|
|
218
|
+
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
219
|
+
if len(cells) != 5 or cells[0] in ("stage", "-----") or set(cells[0]) <= set("-"):
|
|
220
|
+
continue
|
|
221
|
+
try:
|
|
222
|
+
n = int(cells[0])
|
|
223
|
+
except ValueError:
|
|
224
|
+
continue
|
|
225
|
+
depends_raw = cells[2].strip()
|
|
226
|
+
depends = [] if depends_raw in ("(none)", "") else [
|
|
227
|
+
int(x.strip()) for x in depends_raw.split(",") if x.strip()
|
|
228
|
+
]
|
|
229
|
+
try:
|
|
230
|
+
step_count = int(cells[3])
|
|
231
|
+
except ValueError:
|
|
232
|
+
step_count = -1
|
|
233
|
+
rows.append(StageMeta(n, cells[1], depends, step_count, cells[4]))
|
|
234
|
+
errors: List[ValidationError] = []
|
|
235
|
+
for i, r in enumerate(rows, start=1):
|
|
236
|
+
if r.stage_number != i:
|
|
237
|
+
errors.append(ValidationError("S2", r.stage_number,
|
|
238
|
+
f"stage numbers must be 1..N monotonic, got {r.stage_number} at row {i}"))
|
|
239
|
+
return rows, errors
|
|
240
|
+
|
|
241
|
+
def _check_each_stage_section(text: str, stages: List[StageMeta]) -> List[ValidationError]:
|
|
242
|
+
errs: List[ValidationError] = []
|
|
243
|
+
for s in stages:
|
|
244
|
+
if not re.search(rf"^##\s+4\.5\.{s.stage_number}\s+Stage\s+{s.stage_number}\s*:",
|
|
245
|
+
text, re.M):
|
|
246
|
+
errs.append(ValidationError("S3", s.stage_number,
|
|
247
|
+
f"stage section '## 4.5.{s.stage_number} Stage {s.stage_number}:' missing"))
|
|
248
|
+
continue
|
|
249
|
+
# Slice the stage's section body
|
|
250
|
+
start = re.search(rf"^##\s+4\.5\.{s.stage_number}\s+Stage\s+{s.stage_number}\s*:",
|
|
251
|
+
text, re.M).end()
|
|
252
|
+
nxt = re.search(rf"^##\s+4\.5\.{s.stage_number + 1}\s+Stage\s+", text[start:], re.M)
|
|
253
|
+
section = text[start: start + nxt.start()] if nxt else text[start:]
|
|
254
|
+
for sub in REQUIRED_SUBSECTIONS:
|
|
255
|
+
if not re.search(rf"^###\s+{re.escape(sub)}\b", section, re.M):
|
|
256
|
+
errs.append(ValidationError("S4", s.stage_number,
|
|
257
|
+
f"required subsection '### {sub}' missing"))
|
|
258
|
+
# S5: effective step count
|
|
259
|
+
steps = _count_effective_steps(section)
|
|
260
|
+
if steps > 6:
|
|
261
|
+
errs.append(ValidationError("S5", s.stage_number,
|
|
262
|
+
f"effective step count {steps} exceeds 6"))
|
|
263
|
+
if s.step_count != steps and s.step_count >= 0:
|
|
264
|
+
errs.append(ValidationError("S7", s.stage_number,
|
|
265
|
+
f"Stage Map step-count={s.step_count} but real count={steps}"))
|
|
266
|
+
return errs
|
|
267
|
+
|
|
268
|
+
def _count_effective_steps(section: str) -> int:
|
|
269
|
+
m = re.search(r"^###\s+Stepwise Execution Order\b", section, re.M)
|
|
270
|
+
if not m:
|
|
271
|
+
return 0
|
|
272
|
+
body = section[m.end():]
|
|
273
|
+
nxt = re.search(r"^###\s+\w", body, re.M)
|
|
274
|
+
if nxt:
|
|
275
|
+
body = body[: nxt.start()]
|
|
276
|
+
count = 0
|
|
277
|
+
for line in body.splitlines():
|
|
278
|
+
s = line.strip()
|
|
279
|
+
if not s or s.startswith("<!--") or set(s) <= set("|-: "):
|
|
280
|
+
continue
|
|
281
|
+
if s.startswith("|") and not s.startswith("| step") and not set(s.replace("|", "").strip()) <= set("-: "):
|
|
282
|
+
count += 1
|
|
283
|
+
return count
|
|
284
|
+
|
|
285
|
+
def _check_depends_on(stages: List[StageMeta]) -> List[ValidationError]:
|
|
286
|
+
errs: List[ValidationError] = []
|
|
287
|
+
valid = {s.stage_number for s in stages}
|
|
288
|
+
for s in stages:
|
|
289
|
+
for d in s.depends_on:
|
|
290
|
+
if d == s.stage_number:
|
|
291
|
+
errs.append(ValidationError("S8", s.stage_number, "self depends-on"))
|
|
292
|
+
elif d not in valid:
|
|
293
|
+
errs.append(ValidationError("S6", s.stage_number,
|
|
294
|
+
f"depends-on {d} does not exist"))
|
|
295
|
+
# DAG cycle (Kahn)
|
|
296
|
+
indeg = {s.stage_number: len(s.depends_on) for s in stages}
|
|
297
|
+
graph = {s.stage_number: [] for s in stages}
|
|
298
|
+
for s in stages:
|
|
299
|
+
for d in s.depends_on:
|
|
300
|
+
if d in graph:
|
|
301
|
+
graph[d].append(s.stage_number)
|
|
302
|
+
queue = [n for n, k in indeg.items() if k == 0]
|
|
303
|
+
visited = 0
|
|
304
|
+
while queue:
|
|
305
|
+
n = queue.pop()
|
|
306
|
+
visited += 1
|
|
307
|
+
for m in graph[n]:
|
|
308
|
+
indeg[m] -= 1
|
|
309
|
+
if indeg[m] == 0:
|
|
310
|
+
queue.append(m)
|
|
311
|
+
if visited != len(stages):
|
|
312
|
+
errs.append(ValidationError("S8", 0, "depends-on graph has a cycle"))
|
|
313
|
+
return errs
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Then wire them into `main()`:
|
|
317
|
+
```python
|
|
318
|
+
def main(argv: List[str]) -> int:
|
|
319
|
+
p = argparse.ArgumentParser()
|
|
320
|
+
p.add_argument("--plan", required=True)
|
|
321
|
+
args = p.parse_args(argv)
|
|
322
|
+
text = Path(args.plan).read_text(encoding="utf-8")
|
|
323
|
+
errors: List[ValidationError] = []
|
|
324
|
+
errors.extend(_check_stage_map_present(text))
|
|
325
|
+
stages, s2_errs = _parse_stage_map(text)
|
|
326
|
+
errors.extend(s2_errs)
|
|
327
|
+
if stages:
|
|
328
|
+
errors.extend(_check_each_stage_section(text, stages))
|
|
329
|
+
errors.extend(_check_depends_on(stages))
|
|
330
|
+
if errors:
|
|
331
|
+
for e in errors:
|
|
332
|
+
print(f"{e.code} stage={e.stage}: {e.message}", file=sys.stderr)
|
|
333
|
+
return 1
|
|
334
|
+
return 0
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
- [ ] **Step 8: Run all Task 1 tests + commit**
|
|
338
|
+
|
|
339
|
+
Run: `pytest tests/test_validate_implementation_plan_stages.py -v`
|
|
340
|
+
Expected: 5 tests pass.
|
|
341
|
+
|
|
342
|
+
Then:
|
|
343
|
+
```bash
|
|
344
|
+
git add validators/validate-implementation-plan-stages.py \
|
|
345
|
+
tests/test_validate_implementation_plan_stages.py \
|
|
346
|
+
tests/fixtures/plans/
|
|
347
|
+
git commit -m "feat(validators): add implementation-plan stage validator (S1–S8)"
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Task 2: paths + consumers helper + lock
|
|
353
|
+
|
|
354
|
+
**Files:**
|
|
355
|
+
- Modify: `scripts/okstra_ctl/paths.py:56-98` (add `carry/` and `consumers.jsonl` to `compute_run_paths`)
|
|
356
|
+
- Modify: `scripts/okstra_ctl/run_context.py:44-63` (add `consumers_mutex` helper)
|
|
357
|
+
- Create: `scripts/okstra_ctl/consumers.py` (append-only writer + reader)
|
|
358
|
+
- Create: `tests/test_consumers_jsonl.py`
|
|
359
|
+
|
|
360
|
+
- [ ] **Step 1: Write failing test for `consumers.jsonl` append + readback**
|
|
361
|
+
|
|
362
|
+
`tests/test_consumers_jsonl.py`:
|
|
363
|
+
```python
|
|
364
|
+
import json
|
|
365
|
+
from pathlib import Path
|
|
366
|
+
from scripts.okstra_ctl.consumers import append_consumer, read_consumers
|
|
367
|
+
|
|
368
|
+
def test_append_and_read(tmp_path):
|
|
369
|
+
plan_root = tmp_path / "runs" / "implementation-planning" / "plan-key"
|
|
370
|
+
plan_root.mkdir(parents=True)
|
|
371
|
+
append_consumer(plan_root, impl_task_key="impl-A", stage=1,
|
|
372
|
+
status="started", started_at="2026-05-20T00:00:00+09:00",
|
|
373
|
+
head_commit="abc000")
|
|
374
|
+
append_consumer(plan_root, impl_task_key="impl-A", stage=1,
|
|
375
|
+
status="done", completed_at="2026-05-20T00:10:00+09:00",
|
|
376
|
+
carry_path="runs/impl-A/carry/stage-1.json")
|
|
377
|
+
rows = read_consumers(plan_root)
|
|
378
|
+
assert len(rows) == 2
|
|
379
|
+
assert rows[0]["status"] == "started"
|
|
380
|
+
assert rows[1]["status"] == "done"
|
|
381
|
+
|
|
382
|
+
def test_append_is_idempotent(tmp_path):
|
|
383
|
+
plan_root = tmp_path / "p"
|
|
384
|
+
plan_root.mkdir()
|
|
385
|
+
for _ in range(3):
|
|
386
|
+
append_consumer(plan_root, impl_task_key="impl-B", stage=2,
|
|
387
|
+
status="started", started_at="2026-05-20T00:00:00+09:00",
|
|
388
|
+
head_commit="zzz")
|
|
389
|
+
rows = read_consumers(plan_root)
|
|
390
|
+
assert len(rows) == 1
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
- [ ] **Step 2: Run test, expect ModuleNotFoundError**
|
|
394
|
+
|
|
395
|
+
Run: `pytest tests/test_consumers_jsonl.py -v`
|
|
396
|
+
Expected: ImportError on `scripts.okstra_ctl.consumers`.
|
|
397
|
+
|
|
398
|
+
- [ ] **Step 3: Implement `consumers.py`**
|
|
399
|
+
|
|
400
|
+
`scripts/okstra_ctl/consumers.py`:
|
|
401
|
+
```python
|
|
402
|
+
"""Append-only writer / reader for `consumers.jsonl` under a plan run's task root.
|
|
403
|
+
|
|
404
|
+
A row's identity for idempotency is the tuple
|
|
405
|
+
(impl_task_key, stage, status)
|
|
406
|
+
so the same (started / done) record is never duplicated."""
|
|
407
|
+
|
|
408
|
+
from __future__ import annotations
|
|
409
|
+
|
|
410
|
+
import json
|
|
411
|
+
import fcntl
|
|
412
|
+
from pathlib import Path
|
|
413
|
+
from typing import Any, Dict, List, Optional
|
|
414
|
+
|
|
415
|
+
from .run_context import consumers_mutex
|
|
416
|
+
|
|
417
|
+
CONSUMERS_FILENAME = "consumers.jsonl"
|
|
418
|
+
|
|
419
|
+
def _path(plan_run_root: Path) -> Path:
|
|
420
|
+
return plan_run_root / CONSUMERS_FILENAME
|
|
421
|
+
|
|
422
|
+
def read_consumers(plan_run_root: Path) -> List[Dict[str, Any]]:
|
|
423
|
+
p = _path(plan_run_root)
|
|
424
|
+
if not p.exists():
|
|
425
|
+
return []
|
|
426
|
+
out = []
|
|
427
|
+
for line in p.read_text(encoding="utf-8").splitlines():
|
|
428
|
+
line = line.strip()
|
|
429
|
+
if not line:
|
|
430
|
+
continue
|
|
431
|
+
out.append(json.loads(line))
|
|
432
|
+
return out
|
|
433
|
+
|
|
434
|
+
def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
435
|
+
status: str, **fields: Any) -> None:
|
|
436
|
+
if status not in ("started", "done"):
|
|
437
|
+
raise ValueError(f"unsupported status: {status}")
|
|
438
|
+
with consumers_mutex(plan_run_root.name):
|
|
439
|
+
existing = read_consumers(plan_run_root)
|
|
440
|
+
for row in existing:
|
|
441
|
+
if (row.get("impl_task_key") == impl_task_key
|
|
442
|
+
and row.get("stage") == stage
|
|
443
|
+
and row.get("status") == status):
|
|
444
|
+
return # idempotent
|
|
445
|
+
record: Dict[str, Any] = {
|
|
446
|
+
"impl_task_key": impl_task_key,
|
|
447
|
+
"stage": stage,
|
|
448
|
+
"status": status,
|
|
449
|
+
**fields,
|
|
450
|
+
}
|
|
451
|
+
with _path(plan_run_root).open("a", encoding="utf-8") as f:
|
|
452
|
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
- [ ] **Step 4: Add `consumers_mutex` to `run_context.py`**
|
|
456
|
+
|
|
457
|
+
In `scripts/okstra_ctl/run_context.py`, just below the existing `task_mutex`, add:
|
|
458
|
+
```python
|
|
459
|
+
@contextmanager
|
|
460
|
+
def consumers_mutex(plan_task_key: str) -> Iterator[None]:
|
|
461
|
+
"""Per-plan-key mutex for appending to consumers.jsonl."""
|
|
462
|
+
home = _okstra_home()
|
|
463
|
+
locks = home / ".locks"
|
|
464
|
+
locks.mkdir(parents=True, exist_ok=True)
|
|
465
|
+
safe = plan_task_key.replace("/", "_").replace(":", "_")
|
|
466
|
+
path = locks / f"{safe}.consumers.lock"
|
|
467
|
+
with path.open("a+") as f:
|
|
468
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
469
|
+
try:
|
|
470
|
+
yield
|
|
471
|
+
finally:
|
|
472
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
- [ ] **Step 5: Run test, confirm both tests pass**
|
|
476
|
+
|
|
477
|
+
Run: `pytest tests/test_consumers_jsonl.py -v`
|
|
478
|
+
Expected: 2 tests pass.
|
|
479
|
+
|
|
480
|
+
- [ ] **Step 6: Extend `compute_run_paths` with `carry/` directory**
|
|
481
|
+
|
|
482
|
+
In `scripts/okstra_ctl/paths.py`, inside `compute_run_paths` (around line 70+), add `run_carry = run_dir / "carry"` next to `worker_results`, and return it in the dict (key: `"run_carry"`). Add a corresponding `mkdir(parents=True, exist_ok=True)` call wherever the existing directories are created (search for `worker_results.mkdir`).
|
|
483
|
+
|
|
484
|
+
```python
|
|
485
|
+
# Inside compute_run_paths, after worker_results = run_dir / "worker-results"
|
|
486
|
+
run_carry = run_dir / "carry"
|
|
487
|
+
# ...later, alongside other mkdir lines:
|
|
488
|
+
run_carry.mkdir(parents=True, exist_ok=True)
|
|
489
|
+
# ...in the returned dict:
|
|
490
|
+
"run_carry": run_carry,
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
- [ ] **Step 7: Commit**
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
git add scripts/okstra_ctl/consumers.py \
|
|
497
|
+
scripts/okstra_ctl/run_context.py \
|
|
498
|
+
scripts/okstra_ctl/paths.py \
|
|
499
|
+
tests/test_consumers_jsonl.py
|
|
500
|
+
git commit -m "feat(okstra-ctl): add consumers.jsonl writer + carry/ run dir"
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Task 3: `implementation-planning.md` profile + input template
|
|
506
|
+
|
|
507
|
+
**Files:**
|
|
508
|
+
- Modify: `prompts/profiles/implementation-planning.md` — lines 50–53 (section heading contract) and 61–67 (stepwise execution order body)
|
|
509
|
+
- Modify: `templates/reports/implementation-planning-input.template.md` — replace step-list area with stage examples
|
|
510
|
+
|
|
511
|
+
- [ ] **Step 1: Update the section heading contract (line 50–53)**
|
|
512
|
+
|
|
513
|
+
Replace the BLOCKING string list (line 51) with:
|
|
514
|
+
```markdown
|
|
515
|
+
- The final report MUST include section headings containing each of the following exact strings: `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `Stage Exit Contract`, `Stage Validation`, `Dependency`, `Validation Checklist`, `Rollback`, `User Approval Request`.
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
The old keyword `Stepwise Execution Order` is removed from the BLOCKING list because it now lives **inside** each `## 4.5.<i> Stage <i>:` section, and the new validator (Task 1) checks for it per-stage instead of as a single global heading.
|
|
519
|
+
|
|
520
|
+
- [ ] **Step 2: Replace lines 61–67 (deliverable shape body) with the stage block**
|
|
521
|
+
|
|
522
|
+
Replace this region:
|
|
523
|
+
```markdown
|
|
524
|
+
- **stepwise execution order for the recommended option**, written as bite-sized tasks:
|
|
525
|
+
- each step is one action completable in roughly 2–5 minutes ...
|
|
526
|
+
- every step names exact file paths and exact commands; for code steps, include the actual code or the diff sketch — not a description
|
|
527
|
+
- prefer TDD ordering (failing test → implementation → green → commit) when the touched area has or can have tests
|
|
528
|
+
```
|
|
529
|
+
With:
|
|
530
|
+
```markdown
|
|
531
|
+
- **Stage Map (mandatory — always emitted, even when N=1):** a table of all stages with `stage | title | depends-on | step-count | exit-contract-summary`. `depends-on` is `(none)` or a comma-separated stage number list. Stages with `depends-on (none)` can be implemented in parallel by two simultaneous `implementation` runs.
|
|
532
|
+
- **Per-stage subsections** (`## 4.5.<i> Stage <i>: <title>` for each `i`), each containing the four required subsections:
|
|
533
|
+
- `### Carry-In` — for `depends-on (none)`: task-brief 만. Otherwise: each depended-on stage's static exit contract + runtime sidecar path `runs/<impl-key>/carry/stage-<i>.json` placeholder.
|
|
534
|
+
- `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | expected`. **Effective row count ≤ 6** (excluding header / divider / blank). Each step is one action completable in 2–5 minutes; for code steps include actual code or diff sketch; prefer TDD ordering (failing test → implementation → green → commit).
|
|
535
|
+
- `### Stage Exit Contract` — predicted added/modified files, newly exposed identifiers/types/endpoints, downstream-usable resources.
|
|
536
|
+
- `### Stage Validation` — pre / mid / post exact commands or observable outcomes for this stage only.
|
|
537
|
+
- **Parallelisation-first rule (1st-class):** the writer MUST prefer the partition that maximises the number of `depends-on (none)` stages. Given two partitions with equal total step count, the one with fewer `depends-on` edges wins. Conservative "let's serialise to be safe" groupings are forbidden — each `depends-on` link is justified by a concrete data/contract dependency, not a vague risk concern.
|
|
538
|
+
- **Stage exit contract is the carry surface:** keep it as narrow as possible. Wider surface = more downstream coupling.
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
- [ ] **Step 3: Update the self-review block (line 84–90)**
|
|
542
|
+
|
|
543
|
+
Append to the self-review pass list:
|
|
544
|
+
```markdown
|
|
545
|
+
7. **Stage Map self-check** — for every stage, count the effective rows of its `Stepwise Execution Order` table by hand; reject the draft if any stage exceeds 6. Walk the `depends-on` graph and confirm it is a DAG (no cycle, no self-reference). For each `depends-on` link, ask "can this be removed by re-partitioning?" — if yes, re-partition and re-count.
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
- [ ] **Step 4: Update input template — add stage example block**
|
|
549
|
+
|
|
550
|
+
In `templates/reports/implementation-planning-input.template.md`, append a new section after the existing scope block:
|
|
551
|
+
```markdown
|
|
552
|
+
## Stage Output Shape (reference)
|
|
553
|
+
|
|
554
|
+
This run's final report MUST emit `## 4.5 Stage Map` and `## 4.5.<i> Stage <i>` sections per the implementation-planning profile §"Required deliverable shape". Two illustrative shapes:
|
|
555
|
+
|
|
556
|
+
### Shape A — single stage (small work)
|
|
557
|
+
| stage | title | depends-on | step-count | exit-contract-summary |
|
|
558
|
+
|-------|-------|-----------|------------|------------------------|
|
|
559
|
+
| 1 | tiny rename | (none) | 2 | src/foo.ts:renamedFoo |
|
|
560
|
+
|
|
561
|
+
### Shape B — three stages, two parallel
|
|
562
|
+
| stage | title | depends-on | step-count | exit-contract-summary |
|
|
563
|
+
|-------|-------|-----------|------------|------------------------|
|
|
564
|
+
| 1 | foo API skeleton | (none) | 4 | src/foo/api.ts:exportedFoo |
|
|
565
|
+
| 2 | baz settings split | (none) | 2 | src/baz/settings.ts, env BAZ_MODE |
|
|
566
|
+
| 3 | bar integration | 1, 2 | 3 | src/bar/use-foo.ts, GET /bar |
|
|
567
|
+
|
|
568
|
+
Stages 1 and 2 in Shape B are `depends-on (none)` → can be run by two parallel `implementation` runs.
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
- [ ] **Step 5: Commit**
|
|
572
|
+
|
|
573
|
+
```bash
|
|
574
|
+
git add prompts/profiles/implementation-planning.md \
|
|
575
|
+
templates/reports/implementation-planning-input.template.md
|
|
576
|
+
git commit -m "docs(profile): require Stage Map + per-stage subsections in implementation-planning"
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## Task 4: `implementation.md` profile
|
|
582
|
+
|
|
583
|
+
**Files:**
|
|
584
|
+
- Modify: `prompts/profiles/implementation.md` — line 69 (approved-plan extraction), plus an appended section on stage selection / sidecar / PR
|
|
585
|
+
|
|
586
|
+
- [ ] **Step 1: Update line 69 (extract from approved plan)**
|
|
587
|
+
|
|
588
|
+
Replace the single line:
|
|
589
|
+
```markdown
|
|
590
|
+
- re-read the approved plan end-to-end and extract: file list, step order, validation commands, rollback path
|
|
591
|
+
```
|
|
592
|
+
With:
|
|
593
|
+
```markdown
|
|
594
|
+
- re-read the approved plan end-to-end and parse the `## 4.5 Stage Map`. Determine **start stage**:
|
|
595
|
+
- if `--stage <N>` is supplied, use N. Otherwise auto = the lowest stage number whose `depends-on` are all recorded as `status:done` in `runs/<plan-key>/consumers.jsonl` AND that itself has no `status:done` row. Multiple stages may match — two parallel `implementation` runs may pick different ones and proceed concurrently.
|
|
596
|
+
- load every `runs/<plan-key>/carry/stage-<i>.json` for `i ∈ depends-on(start_stage)` and inject them into the executor's working context as "runtime carry-in". For `depends-on (none)` stages, no sidecar load — task-brief only.
|
|
597
|
+
- extract the **start stage's** file list, step order, Stage Validation commands, Stage Exit Contract, and rollback path. These — not the whole plan — are the authoritative scope for this run.
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
- [ ] **Step 2: Insert a new section "Stage execution contract" after line 75**
|
|
601
|
+
|
|
602
|
+
After the "drift rule / out-of-plan rule" block, insert:
|
|
603
|
+
```markdown
|
|
604
|
+
- Stage execution contract (this run owns exactly one stage of the plan):
|
|
605
|
+
- **Sidecar evidence writer (BLOCKING).** When the start stage's Stage Validation `post` commands all succeed, the Executor MUST emit a JSON object matching the schema in `docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md` §3.2 and the lead MUST persist it to `runs/<impl-task-key>/carry/stage-<N>.json`. The file MUST NOT exist before the run starts (overwrite is refused — see `--force-stage` non-goal).
|
|
606
|
+
- **Reverse link (BLOCKING).** Before the first Edit/Write, append a `status:"started"` row to `runs/<plan-task-key>/consumers.jsonl` (lock via the okstra runtime). On stage completion, append a `status:"done"` row with `carry_path` populated.
|
|
607
|
+
- **One-PR-per-stage.** This run creates exactly one PR titled `Stage <N>: <stage title>`. The PR body MUST include:
|
|
608
|
+
- `## Stage` — number and title (from Stage Map row).
|
|
609
|
+
- `## Carry-In summary` — depends-on list + cited identifiers/SHAs from each loaded sidecar (omit when depends-on is empty).
|
|
610
|
+
- `## Next stage` — next stage number/title or `(last stage)`.
|
|
611
|
+
Stage PRs link back to each other in their bodies (`Previous: #<n>, Next: #<m>` lines) so a reviewer can navigate the chain.
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
- [ ] **Step 3: Update "Plan link & approval evidence" deliverable (line 112) to include stage info**
|
|
615
|
+
|
|
616
|
+
Replace the bullet:
|
|
617
|
+
```markdown
|
|
618
|
+
- **Plan link & approval evidence**: path to the approved `final-report.md` and the exact quoted approval marker
|
|
619
|
+
```
|
|
620
|
+
With:
|
|
621
|
+
```markdown
|
|
622
|
+
- **Plan link & approval evidence**: path to the approved `final-report.md`, the exact quoted approval marker, AND the executed stage number / title quoted from the Stage Map row.
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
- [ ] **Step 4: Add "Stage sidecar evidence" deliverable**
|
|
626
|
+
|
|
627
|
+
Append a new bullet to the Required deliverable shape list (after "Out-of-plan edits block"):
|
|
628
|
+
```markdown
|
|
629
|
+
- **Stage sidecar evidence**: the JSON payload of `runs/<impl-task-key>/carry/stage-<N>.json` is embedded verbatim in a fenced ```json``` block, AND the `consumers.jsonl` rows this run appended are quoted line-by-line, so reviewers can audit the carry surface without grepping artifact directories.
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
- [ ] **Step 5: Commit**
|
|
633
|
+
|
|
634
|
+
```bash
|
|
635
|
+
git add prompts/profiles/implementation.md
|
|
636
|
+
git commit -m "docs(profile): teach implementation runs to pick a stage and emit carry sidecar"
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Task 5: `run.py` — `--stage` arg + validator wiring + `parsed_stage_map`
|
|
642
|
+
|
|
643
|
+
**Files:**
|
|
644
|
+
- Modify: `scripts/okstra_ctl/run.py` — argparse block (lines 920–948), `_validate_approved_plan` (line 146), `prepare_task_bundle` implementation branch (lines 473–483)
|
|
645
|
+
- Create: `tests/test_run_stage_arg.py`
|
|
646
|
+
|
|
647
|
+
depends-on: Task 1, Task 2.
|
|
648
|
+
|
|
649
|
+
- [ ] **Step 1: Write failing test — `--stage 1` parses to `inp.stage`**
|
|
650
|
+
|
|
651
|
+
`tests/test_run_stage_arg.py`:
|
|
652
|
+
```python
|
|
653
|
+
import subprocess, sys
|
|
654
|
+
from pathlib import Path
|
|
655
|
+
|
|
656
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
657
|
+
|
|
658
|
+
def test_stage_arg_accepted_for_implementation(tmp_path):
|
|
659
|
+
# bundle a minimal valid stage plan into tmp_path
|
|
660
|
+
plan = tmp_path / "final-report.md"
|
|
661
|
+
plan.write_text(Path("tests/fixtures/plans/valid_one_stage.md")
|
|
662
|
+
.read_text(encoding="utf-8"), encoding="utf-8")
|
|
663
|
+
# Approve it
|
|
664
|
+
plan.write_text(plan.read_text() + "\n- [x] Approved\n", encoding="utf-8")
|
|
665
|
+
r = subprocess.run(
|
|
666
|
+
[sys.executable, "-m", "okstra_ctl.run",
|
|
667
|
+
"--task-type", "implementation",
|
|
668
|
+
"--task-group", "demo", "--task-id", "demo-1",
|
|
669
|
+
"--project-root", str(tmp_path),
|
|
670
|
+
"--project-id", "demo",
|
|
671
|
+
"--approved-plan", str(plan),
|
|
672
|
+
"--stage", "1",
|
|
673
|
+
"--render-only"],
|
|
674
|
+
capture_output=True, text=True, cwd=str(REPO),
|
|
675
|
+
)
|
|
676
|
+
assert r.returncode == 0, r.stderr
|
|
677
|
+
|
|
678
|
+
def test_stage_arg_rejected_outside_implementation(tmp_path):
|
|
679
|
+
r = subprocess.run(
|
|
680
|
+
[sys.executable, "-m", "okstra_ctl.run",
|
|
681
|
+
"--task-type", "error-analysis",
|
|
682
|
+
"--task-group", "demo", "--task-id", "demo-2",
|
|
683
|
+
"--project-root", str(tmp_path),
|
|
684
|
+
"--project-id", "demo",
|
|
685
|
+
"--stage", "1",
|
|
686
|
+
"--render-only"],
|
|
687
|
+
capture_output=True, text=True, cwd=str(REPO),
|
|
688
|
+
)
|
|
689
|
+
assert r.returncode != 0
|
|
690
|
+
assert "stage" in r.stderr.lower()
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
- [ ] **Step 2: Run test, expect failure**
|
|
694
|
+
|
|
695
|
+
Run: `pytest tests/test_run_stage_arg.py -v`
|
|
696
|
+
Expected: both tests fail (current argparse rejects `--stage` everywhere).
|
|
697
|
+
|
|
698
|
+
- [ ] **Step 3: Add `--stage` argument in `run.py` argparse block**
|
|
699
|
+
|
|
700
|
+
In `scripts/okstra_ctl/run.py` around line 925, after the `--approved-plan` declaration, insert:
|
|
701
|
+
```python
|
|
702
|
+
p.add_argument(
|
|
703
|
+
"--stage", default="auto", dest="stage",
|
|
704
|
+
help=(
|
|
705
|
+
"implementation task only. Which Stage Map entry to execute. "
|
|
706
|
+
"'auto' (default) = lowest-numbered stage whose depends-on are all "
|
|
707
|
+
"consumers.jsonl status:done. Numeric '<N>' = force that stage."
|
|
708
|
+
),
|
|
709
|
+
)
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
And in the dataclass `PrepareInputs` (search for it earlier in the file), add:
|
|
713
|
+
```python
|
|
714
|
+
stage: str = "auto"
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
In the inputs-construction block (where `PrepareInputs(...)` is built from argparse `args`), pass through `stage=args.stage`.
|
|
718
|
+
|
|
719
|
+
- [ ] **Step 4: Reject `--stage` outside `implementation` and call the new validator**
|
|
720
|
+
|
|
721
|
+
Replace `prepare_task_bundle` lines 473–483 with:
|
|
722
|
+
```python
|
|
723
|
+
if inp.task_type == "implementation":
|
|
724
|
+
if not inp.approved_plan_path:
|
|
725
|
+
raise PrepareError(
|
|
726
|
+
"task-type implementation requires --approved-plan <path-to-final-report.md>"
|
|
727
|
+
)
|
|
728
|
+
if inp.approve_plan_ack:
|
|
729
|
+
_apply_cli_approval(inp.approved_plan_path)
|
|
730
|
+
_validate_approved_plan(inp.approved_plan_path)
|
|
731
|
+
_validate_stage_structure(inp.approved_plan_path) # NEW
|
|
732
|
+
ctx["parsed_stage_map"] = _parse_stage_map_into_ctx(inp.approved_plan_path)
|
|
733
|
+
else:
|
|
734
|
+
if inp.stage != "auto":
|
|
735
|
+
raise PrepareError(
|
|
736
|
+
f"--stage is only meaningful with --task-type implementation; got {inp.task_type}"
|
|
737
|
+
)
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
- [ ] **Step 5: Implement `_validate_stage_structure` and `_parse_stage_map_into_ctx`**
|
|
741
|
+
|
|
742
|
+
In `scripts/okstra_ctl/run.py`, near the existing `_validate_approved_plan` (line 146 area), add:
|
|
743
|
+
```python
|
|
744
|
+
import subprocess as _subprocess
|
|
745
|
+
|
|
746
|
+
_VALIDATOR_PATH = (
|
|
747
|
+
Path(__file__).resolve().parents[2] / "validators"
|
|
748
|
+
/ "validate-implementation-plan-stages.py"
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
def _validate_stage_structure(plan_path: str) -> None:
|
|
752
|
+
r = _subprocess.run(
|
|
753
|
+
["python3", str(_VALIDATOR_PATH), "--plan", plan_path],
|
|
754
|
+
capture_output=True, text=True,
|
|
755
|
+
)
|
|
756
|
+
if r.returncode != 0:
|
|
757
|
+
raise PrepareError(
|
|
758
|
+
f"approved plan failed stage validation:\n{r.stderr.strip()}"
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
def _parse_stage_map_into_ctx(plan_path: str) -> list[dict]:
|
|
762
|
+
text = Path(plan_path).read_text(encoding="utf-8")
|
|
763
|
+
# Reuse the validator's parser by importing it
|
|
764
|
+
sys.path.insert(0, str(_VALIDATOR_PATH.parent))
|
|
765
|
+
try:
|
|
766
|
+
import importlib.util
|
|
767
|
+
spec = importlib.util.spec_from_file_location("ip_stage_v", _VALIDATOR_PATH)
|
|
768
|
+
mod = importlib.util.module_from_spec(spec) # type: ignore
|
|
769
|
+
spec.loader.exec_module(mod) # type: ignore
|
|
770
|
+
stages, _errs = mod._parse_stage_map(text)
|
|
771
|
+
finally:
|
|
772
|
+
sys.path.pop(0)
|
|
773
|
+
return [
|
|
774
|
+
{
|
|
775
|
+
"stage_number": s.stage_number,
|
|
776
|
+
"title": s.title,
|
|
777
|
+
"depends_on": s.depends_on,
|
|
778
|
+
"step_count": s.step_count,
|
|
779
|
+
"exit_contract_summary": s.exit_contract_summary,
|
|
780
|
+
}
|
|
781
|
+
for s in stages
|
|
782
|
+
]
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
- [ ] **Step 6: Run tests, expect pass + commit**
|
|
786
|
+
|
|
787
|
+
Run: `pytest tests/test_run_stage_arg.py tests/test_validate_implementation_plan_stages.py -v`
|
|
788
|
+
Expected: all tests pass.
|
|
789
|
+
|
|
790
|
+
```bash
|
|
791
|
+
git add scripts/okstra_ctl/run.py tests/test_run_stage_arg.py
|
|
792
|
+
git commit -m "feat(run.py): add --stage arg and wire stage-map validator"
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
---
|
|
796
|
+
|
|
797
|
+
## Task 6: `run.py` — auto stage selection + consumers append
|
|
798
|
+
|
|
799
|
+
**Files:**
|
|
800
|
+
- Modify: `scripts/okstra_ctl/run.py` — `prepare_task_bundle` implementation branch (add auto-resolve + consumers append)
|
|
801
|
+
- Create: `tests/test_auto_stage_selection.py`
|
|
802
|
+
|
|
803
|
+
depends-on: Task 2, Task 5.
|
|
804
|
+
|
|
805
|
+
- [ ] **Step 1: Failing test for auto selection**
|
|
806
|
+
|
|
807
|
+
`tests/test_auto_stage_selection.py`:
|
|
808
|
+
```python
|
|
809
|
+
import subprocess, sys, json
|
|
810
|
+
from pathlib import Path
|
|
811
|
+
|
|
812
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
813
|
+
|
|
814
|
+
def _prepare(tmp_path, plan_fixture, stage_arg, consumers_lines=None):
|
|
815
|
+
plan = tmp_path / "final-report.md"
|
|
816
|
+
plan.write_text(
|
|
817
|
+
Path(f"tests/fixtures/plans/{plan_fixture}").read_text() + "\n- [x] Approved\n"
|
|
818
|
+
)
|
|
819
|
+
plan_root = tmp_path / ".project-docs" / "okstra" / "tasks" / "demo" / "demo-1" \
|
|
820
|
+
/ "runs" / "implementation-planning"
|
|
821
|
+
plan_root.mkdir(parents=True)
|
|
822
|
+
if consumers_lines:
|
|
823
|
+
(plan_root / "consumers.jsonl").write_text(
|
|
824
|
+
"\n".join(json.dumps(x) for x in consumers_lines) + "\n"
|
|
825
|
+
)
|
|
826
|
+
r = subprocess.run(
|
|
827
|
+
[sys.executable, "-m", "okstra_ctl.run",
|
|
828
|
+
"--task-type", "implementation",
|
|
829
|
+
"--task-group", "demo", "--task-id", "demo-1",
|
|
830
|
+
"--project-root", str(tmp_path),
|
|
831
|
+
"--project-id", "demo",
|
|
832
|
+
"--approved-plan", str(plan),
|
|
833
|
+
"--stage", stage_arg,
|
|
834
|
+
"--render-only"],
|
|
835
|
+
capture_output=True, text=True, cwd=str(REPO),
|
|
836
|
+
)
|
|
837
|
+
return r, plan_root
|
|
838
|
+
|
|
839
|
+
def test_auto_picks_stage_1_when_none_done(tmp_path):
|
|
840
|
+
r, _ = _prepare(tmp_path, "valid_three_stage_parallel.md", "auto")
|
|
841
|
+
assert r.returncode == 0
|
|
842
|
+
assert "selected stage: 1" in r.stdout
|
|
843
|
+
|
|
844
|
+
def test_auto_picks_stage_2_when_stage_1_done(tmp_path):
|
|
845
|
+
r, _ = _prepare(tmp_path, "valid_three_stage_parallel.md", "auto",
|
|
846
|
+
consumers_lines=[
|
|
847
|
+
{"impl_task_key": "impl-A", "stage": 1, "status": "started",
|
|
848
|
+
"started_at": "x", "head_commit": "y"},
|
|
849
|
+
{"impl_task_key": "impl-A", "stage": 1, "status": "done",
|
|
850
|
+
"completed_at": "z", "carry_path": "..."},
|
|
851
|
+
])
|
|
852
|
+
# stage 2 is depends-on (none) so it's still pickable.
|
|
853
|
+
# Algorithm picks lowest unsatisfied → here stage 2.
|
|
854
|
+
assert r.returncode == 0
|
|
855
|
+
assert "selected stage: 2" in r.stdout
|
|
856
|
+
|
|
857
|
+
def test_auto_blocks_on_unsatisfied_depends_on(tmp_path):
|
|
858
|
+
# stage 3 has depends-on 1,2 — neither done. auto should pick stage 1.
|
|
859
|
+
r, _ = _prepare(tmp_path, "valid_three_stage_parallel.md", "auto")
|
|
860
|
+
assert "selected stage: 1" in r.stdout
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
- [ ] **Step 2: Run, expect failure**
|
|
864
|
+
|
|
865
|
+
Run: `pytest tests/test_auto_stage_selection.py -v`
|
|
866
|
+
Expected: all three fail (auto resolution not yet implemented).
|
|
867
|
+
|
|
868
|
+
- [ ] **Step 3: Implement auto resolution in `run.py`**
|
|
869
|
+
|
|
870
|
+
Inside the `if inp.task_type == "implementation":` branch (after `_parse_stage_map_into_ctx`):
|
|
871
|
+
```python
|
|
872
|
+
# Resolve effective stage
|
|
873
|
+
from .consumers import read_consumers
|
|
874
|
+
plan_run_root = Path(inp.approved_plan_path).resolve().parents[1] # .../runs/implementation-planning/
|
|
875
|
+
consumed = read_consumers(plan_run_root)
|
|
876
|
+
done_stages = {r["stage"] for r in consumed if r.get("status") == "done"}
|
|
877
|
+
stages = ctx["parsed_stage_map"]
|
|
878
|
+
if inp.stage == "auto":
|
|
879
|
+
chosen = None
|
|
880
|
+
for s in stages:
|
|
881
|
+
if s["stage_number"] in done_stages:
|
|
882
|
+
continue
|
|
883
|
+
if all(d in done_stages for d in s["depends_on"]):
|
|
884
|
+
chosen = s
|
|
885
|
+
break
|
|
886
|
+
if chosen is None:
|
|
887
|
+
raise PrepareError(
|
|
888
|
+
"no stage is ready: every remaining stage has unsatisfied depends-on"
|
|
889
|
+
)
|
|
890
|
+
inp.stage = str(chosen["stage_number"])
|
|
891
|
+
else:
|
|
892
|
+
try:
|
|
893
|
+
n = int(inp.stage)
|
|
894
|
+
except ValueError:
|
|
895
|
+
raise PrepareError(f"--stage must be 'auto' or an integer, got {inp.stage!r}")
|
|
896
|
+
target = next((s for s in stages if s["stage_number"] == n), None)
|
|
897
|
+
if target is None:
|
|
898
|
+
raise PrepareError(f"--stage {n} not in Stage Map (have {[s['stage_number'] for s in stages]})")
|
|
899
|
+
if n in done_stages:
|
|
900
|
+
raise PrepareError(f"--stage {n} already completed (consumers.jsonl status:done exists)")
|
|
901
|
+
inp.stage = str(n)
|
|
902
|
+
ctx["effective_stage"] = int(inp.stage)
|
|
903
|
+
print(f"selected stage: {inp.stage}", file=sys.stdout)
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
- [ ] **Step 4: Append `started` row to consumers.jsonl during prepare**
|
|
907
|
+
|
|
908
|
+
Right after the resolution block above:
|
|
909
|
+
```python
|
|
910
|
+
from .consumers import append_consumer
|
|
911
|
+
import datetime, subprocess as _sp
|
|
912
|
+
head = _sp.run(["git", "rev-parse", "HEAD"],
|
|
913
|
+
cwd=inp.project_root, capture_output=True, text=True)
|
|
914
|
+
append_consumer(
|
|
915
|
+
plan_run_root,
|
|
916
|
+
impl_task_key=ctx["TASK_KEY"],
|
|
917
|
+
stage=int(inp.stage),
|
|
918
|
+
status="started",
|
|
919
|
+
started_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
920
|
+
head_commit=head.stdout.strip() if head.returncode == 0 else "",
|
|
921
|
+
)
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
- [ ] **Step 5: Run tests, expect pass**
|
|
925
|
+
|
|
926
|
+
Run: `pytest tests/test_auto_stage_selection.py -v`
|
|
927
|
+
Expected: 3 tests pass.
|
|
928
|
+
|
|
929
|
+
- [ ] **Step 6: Add test for `--stage <N>` rejection when already done**
|
|
930
|
+
|
|
931
|
+
Append to `tests/test_auto_stage_selection.py`:
|
|
932
|
+
```python
|
|
933
|
+
def test_explicit_stage_rejected_when_done(tmp_path):
|
|
934
|
+
r, _ = _prepare(tmp_path, "valid_three_stage_parallel.md", "1",
|
|
935
|
+
consumers_lines=[
|
|
936
|
+
{"impl_task_key": "impl-A", "stage": 1, "status": "started",
|
|
937
|
+
"started_at": "x", "head_commit": "y"},
|
|
938
|
+
{"impl_task_key": "impl-A", "stage": 1, "status": "done",
|
|
939
|
+
"completed_at": "z", "carry_path": "..."},
|
|
940
|
+
])
|
|
941
|
+
assert r.returncode != 0
|
|
942
|
+
assert "already completed" in r.stderr
|
|
943
|
+
```
|
|
944
|
+
Run: `pytest tests/test_auto_stage_selection.py::test_explicit_stage_rejected_when_done -v`
|
|
945
|
+
Expected: pass.
|
|
946
|
+
|
|
947
|
+
- [ ] **Step 7: Commit**
|
|
948
|
+
|
|
949
|
+
```bash
|
|
950
|
+
git add scripts/okstra_ctl/run.py tests/test_auto_stage_selection.py
|
|
951
|
+
git commit -m "feat(run.py): auto-resolve next ready stage and append consumers.jsonl"
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
---
|
|
955
|
+
|
|
956
|
+
## Task 7: `wizard.py` — dynamic `stage_pick` step
|
|
957
|
+
|
|
958
|
+
**Files:**
|
|
959
|
+
- Modify: `scripts/okstra_ctl/wizard.py` — STEPS list (line ~1581) + new `_build_stage_pick` / `_submit_stage_pick` functions
|
|
960
|
+
- Create: `tests/test_wizard_stage_pick.py`
|
|
961
|
+
|
|
962
|
+
depends-on: Task 1, Task 5.
|
|
963
|
+
|
|
964
|
+
- [ ] **Step 1: Failing test**
|
|
965
|
+
|
|
966
|
+
`tests/test_wizard_stage_pick.py`:
|
|
967
|
+
```python
|
|
968
|
+
import json, subprocess, sys
|
|
969
|
+
from pathlib import Path
|
|
970
|
+
|
|
971
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
972
|
+
|
|
973
|
+
def _wizard(*args):
|
|
974
|
+
return subprocess.run(
|
|
975
|
+
[sys.executable, "-m", "okstra_ctl.wizard", *args],
|
|
976
|
+
capture_output=True, text=True, cwd=str(REPO),
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
def test_stage_pick_step_emitted_for_three_stage_plan(tmp_path):
|
|
980
|
+
state = tmp_path / "wiz.json"
|
|
981
|
+
plan = tmp_path / "plan.md"
|
|
982
|
+
plan.write_text(
|
|
983
|
+
Path("tests/fixtures/plans/valid_three_stage_parallel.md").read_text()
|
|
984
|
+
+ "\n- [x] Approved\n"
|
|
985
|
+
)
|
|
986
|
+
# init + answer through to the approved_plan step
|
|
987
|
+
_wizard("init", "--state-file", str(state),
|
|
988
|
+
"--project-root", str(tmp_path), "--project-id", "demo")
|
|
989
|
+
# ... (drive state to stage_pick — exact step depends on wizard.py STEPS order)
|
|
990
|
+
# For the test, we instead drive the state file directly using public API:
|
|
991
|
+
from scripts.okstra_ctl.wizard import WizardState, next_prompt
|
|
992
|
+
s = WizardState(
|
|
993
|
+
workspace_root=str(REPO),
|
|
994
|
+
task_type="implementation",
|
|
995
|
+
approved_plan_path=str(plan),
|
|
996
|
+
# ... minimal fields ...
|
|
997
|
+
)
|
|
998
|
+
prompt = next_prompt(s)
|
|
999
|
+
# Walk forward until we hit S_STAGE_PICK
|
|
1000
|
+
visited_ids = []
|
|
1001
|
+
while prompt.kind != "done":
|
|
1002
|
+
visited_ids.append(prompt.step)
|
|
1003
|
+
s.answered.add(prompt.step)
|
|
1004
|
+
prompt = next_prompt(s)
|
|
1005
|
+
assert "stage_pick" in visited_ids
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
- [ ] **Step 2: Run, expect failure**
|
|
1009
|
+
|
|
1010
|
+
Run: `pytest tests/test_wizard_stage_pick.py -v`
|
|
1011
|
+
Expected: fail (`stage_pick` step does not exist yet).
|
|
1012
|
+
|
|
1013
|
+
- [ ] **Step 3: Add `S_STAGE_PICK` constant + builder + submitter**
|
|
1014
|
+
|
|
1015
|
+
In `scripts/okstra_ctl/wizard.py` near the existing step ID constants:
|
|
1016
|
+
```python
|
|
1017
|
+
S_STAGE_PICK = "stage_pick"
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
Add builder (mirroring `_build_approved_plan_pick` at line 909):
|
|
1021
|
+
```python
|
|
1022
|
+
def _build_stage_pick(state: WizardState) -> Prompt:
|
|
1023
|
+
# Parse Stage Map from the approved plan to build picker options.
|
|
1024
|
+
text = Path(state.approved_plan_path).read_text(encoding="utf-8")
|
|
1025
|
+
import importlib.util
|
|
1026
|
+
spec = importlib.util.spec_from_file_location(
|
|
1027
|
+
"ip_stage_v",
|
|
1028
|
+
Path(state.workspace_root) / "validators" / "validate-implementation-plan-stages.py",
|
|
1029
|
+
)
|
|
1030
|
+
mod = importlib.util.module_from_spec(spec) # type: ignore
|
|
1031
|
+
spec.loader.exec_module(mod) # type: ignore
|
|
1032
|
+
stages, _ = mod._parse_stage_map(text)
|
|
1033
|
+
options = [_opt("auto", "auto (다음 미완료 stage)")]
|
|
1034
|
+
for s in stages:
|
|
1035
|
+
options.append(_opt(str(s.stage_number),
|
|
1036
|
+
f"{s.stage_number}: {s.title} [depends-on: "
|
|
1037
|
+
f"{','.join(map(str, s.depends_on)) or '(none)'} | "
|
|
1038
|
+
f"steps: {s.step_count}]"))
|
|
1039
|
+
return Prompt(
|
|
1040
|
+
step=S_STAGE_PICK, kind="pick",
|
|
1041
|
+
label="실행할 stage 를 선택하세요. auto 는 의존성이 만족된 가장 빠른 미완료 stage 를 자동으로 잡습니다.",
|
|
1042
|
+
options=options,
|
|
1043
|
+
echo_template="stage: {value}",
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
def _submit_stage_pick(state: WizardState, answer: str) -> str:
|
|
1047
|
+
if not answer:
|
|
1048
|
+
return "value required"
|
|
1049
|
+
if answer != "auto":
|
|
1050
|
+
try:
|
|
1051
|
+
int(answer)
|
|
1052
|
+
except ValueError:
|
|
1053
|
+
return f"answer must be 'auto' or a stage number, got {answer!r}"
|
|
1054
|
+
state.selected_stage = answer
|
|
1055
|
+
return f"stage: {answer}"
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
Add field to `WizardState`:
|
|
1059
|
+
```python
|
|
1060
|
+
selected_stage: str = "auto"
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
- [ ] **Step 4: Register in STEPS list**
|
|
1064
|
+
|
|
1065
|
+
Find the global `STEPS = [...]` list (line ~1581). Insert immediately after the `S_APPROVED_PLAN_PICK` step:
|
|
1066
|
+
```python
|
|
1067
|
+
Step(
|
|
1068
|
+
id=S_STAGE_PICK,
|
|
1069
|
+
applies=lambda s: s.task_type == "implementation"
|
|
1070
|
+
and s.approved_plan_path
|
|
1071
|
+
and S_STAGE_PICK not in s.answered,
|
|
1072
|
+
build=_build_stage_pick,
|
|
1073
|
+
submit=_submit_stage_pick,
|
|
1074
|
+
owns=("selected_stage",),
|
|
1075
|
+
),
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
- [ ] **Step 5: Update `render-args` to surface `--stage`**
|
|
1079
|
+
|
|
1080
|
+
Find the `cmd_render_args` (or equivalent) that emits `args` dict at the end. Add:
|
|
1081
|
+
```python
|
|
1082
|
+
if state.task_type == "implementation":
|
|
1083
|
+
out["stage"] = state.selected_stage or "auto"
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
And update the `okstra-run` skill's Step 5 invocation example (in the SKILL.md, line ~152 area) — append `--stage "<args.stage>"` to the `render-bundle` call list. (No code change needed if the skill already iterates `args` keys generically; otherwise update.)
|
|
1087
|
+
|
|
1088
|
+
- [ ] **Step 6: Run tests + commit**
|
|
1089
|
+
|
|
1090
|
+
Run: `pytest tests/test_wizard_stage_pick.py -v`
|
|
1091
|
+
Expected: pass.
|
|
1092
|
+
|
|
1093
|
+
```bash
|
|
1094
|
+
git add scripts/okstra_ctl/wizard.py tests/test_wizard_stage_pick.py \
|
|
1095
|
+
skills/okstra-run/SKILL.md
|
|
1096
|
+
git commit -m "feat(wizard): add dynamic stage_pick step for implementation"
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
---
|
|
1100
|
+
|
|
1101
|
+
## Task 8: worker prompts — evidence JSON output
|
|
1102
|
+
|
|
1103
|
+
**Files:**
|
|
1104
|
+
- Modify: `agents/workers/claude-worker.md`
|
|
1105
|
+
- Modify: `agents/workers/codex-worker.md`
|
|
1106
|
+
- Modify: `agents/workers/gemini-worker.md`
|
|
1107
|
+
|
|
1108
|
+
depends-on: Task 4.
|
|
1109
|
+
|
|
1110
|
+
- [ ] **Step 1: Locate the verifier / final-validation section in each worker prompt**
|
|
1111
|
+
|
|
1112
|
+
Run: `grep -n -E "(verifier|final|Audit sidecar)" agents/workers/*.md | head -40`
|
|
1113
|
+
Confirm the location of the "after all validations pass" block in each worker.
|
|
1114
|
+
|
|
1115
|
+
- [ ] **Step 2: Append the evidence emission requirement to each worker prompt**
|
|
1116
|
+
|
|
1117
|
+
Append the following block to the **end** of each of `agents/workers/{claude,codex,gemini}-worker.md`:
|
|
1118
|
+
```markdown
|
|
1119
|
+
## Stage evidence emission (BLOCKING, implementation task only)
|
|
1120
|
+
|
|
1121
|
+
When this run's `task_type` is `implementation` and you are acting as the **Executor**, after the Stage Validation `post` commands all return exit code 0 you MUST emit a single JSON document matching `docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md` §3.2:
|
|
1122
|
+
|
|
1123
|
+
```json
|
|
1124
|
+
{
|
|
1125
|
+
"schemaVersion": 1,
|
|
1126
|
+
"sourcePlanPath": "<approved-plan path>",
|
|
1127
|
+
"stageNumber": <int>,
|
|
1128
|
+
"stageTitle": "<from Stage Map>",
|
|
1129
|
+
"completedAt": "<ISO-8601 with tz>",
|
|
1130
|
+
"stageCommitRange": { "base": "<sha>", "head": "<sha>" },
|
|
1131
|
+
"filesChanged": ["<rel/path>", "..."],
|
|
1132
|
+
"newIdentifiers": ["<name>", "..."],
|
|
1133
|
+
"stepResults": [{"step": <int>, "status": "done", "commit": "<sha>"}],
|
|
1134
|
+
"validationsPassed": ["<label>", "..."],
|
|
1135
|
+
"notes": []
|
|
1136
|
+
}
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
Emit this as a fenced ```json``` block in your worker result under the heading `### Stage Carry Evidence`. The lead (`Claude lead`) is responsible for persisting the block as `runs/<impl-task-key>/carry/stage-<N>.json` — you do not write the file yourself.
|
|
1140
|
+
|
|
1141
|
+
This applies only when `task_type` is `implementation`. For other task types, skip this block entirely.
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
- [ ] **Step 3: Verify by grep**
|
|
1145
|
+
|
|
1146
|
+
Run: `grep -l "Stage Carry Evidence" agents/workers/*.md`
|
|
1147
|
+
Expected: three files matched (claude, codex, gemini).
|
|
1148
|
+
|
|
1149
|
+
- [ ] **Step 4: Commit**
|
|
1150
|
+
|
|
1151
|
+
```bash
|
|
1152
|
+
git add agents/workers/claude-worker.md \
|
|
1153
|
+
agents/workers/codex-worker.md \
|
|
1154
|
+
agents/workers/gemini-worker.md
|
|
1155
|
+
git commit -m "docs(workers): require Stage Carry Evidence JSON in implementation runs"
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
---
|
|
1159
|
+
|
|
1160
|
+
## Task 9: lead prompt — sidecar persistence + consumers `done` row
|
|
1161
|
+
|
|
1162
|
+
**Files:**
|
|
1163
|
+
- Modify: `prompts/profiles/implementation.md` — add "Lead post-stage persistence" section
|
|
1164
|
+
|
|
1165
|
+
depends-on: Task 2, Task 4, Task 8.
|
|
1166
|
+
|
|
1167
|
+
- [ ] **Step 1: Append the new section**
|
|
1168
|
+
|
|
1169
|
+
Append to `prompts/profiles/implementation.md` (after the Self-review pass block, before "In-phase debugging"):
|
|
1170
|
+
```markdown
|
|
1171
|
+
- Lead post-stage persistence (BLOCKING — runs after the Executor emits `### Stage Carry Evidence`):
|
|
1172
|
+
- Parse the executor's `### Stage Carry Evidence` JSON block. If absent or unparsable, end with status `contract-violated` and route to a follow-up `error-analysis`.
|
|
1173
|
+
- Write the JSON verbatim to `runs/<impl-task-key>/carry/stage-<N>.json`. Refuse to overwrite an existing file (one stage = one sidecar; re-runs are out of scope for this version).
|
|
1174
|
+
- 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.
|
|
1175
|
+
- Quote both files' new contents (the sidecar JSON in full, the new consumers row by itself) in the final report's `Stage sidecar evidence` deliverable section.
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
- [ ] **Step 2: Grep-verify the new section is uniquely named**
|
|
1179
|
+
|
|
1180
|
+
Run: `grep -c "Lead post-stage persistence" prompts/profiles/implementation.md`
|
|
1181
|
+
Expected: 1.
|
|
1182
|
+
|
|
1183
|
+
- [ ] **Step 3: Commit**
|
|
1184
|
+
|
|
1185
|
+
```bash
|
|
1186
|
+
git add prompts/profiles/implementation.md
|
|
1187
|
+
git commit -m "docs(profile): make lead persist Stage Carry Evidence and consumers row"
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
---
|
|
1191
|
+
|
|
1192
|
+
## Task 10: e2e scenarios (spec §9 Q1–Q9)
|
|
1193
|
+
|
|
1194
|
+
**Files:**
|
|
1195
|
+
- Create: `tests-e2e/test_implementation_plan_multi_stage.py` (or `.sh` matching existing tests-e2e style — check `tests-e2e/` layout first)
|
|
1196
|
+
|
|
1197
|
+
depends-on: Task 1–9.
|
|
1198
|
+
|
|
1199
|
+
- [ ] **Step 1: Inspect `tests-e2e/` to match its convention**
|
|
1200
|
+
|
|
1201
|
+
Run: `ls tests-e2e/`
|
|
1202
|
+
Pick the closest existing test to model after (pytest-style or bash-style) — adopt that style.
|
|
1203
|
+
|
|
1204
|
+
- [ ] **Step 2: Implement Q1 — 1-stage plan, full happy path**
|
|
1205
|
+
|
|
1206
|
+
Sketch (pytest style assumed):
|
|
1207
|
+
```python
|
|
1208
|
+
def test_q1_one_stage_plan_happy_path(tmp_project):
|
|
1209
|
+
# 1. Seed an APPROVED 1-stage plan in tmp_project's planning run dir
|
|
1210
|
+
# 2. Invoke `okstra render-bundle --task-type implementation --stage auto ...`
|
|
1211
|
+
# 3. Assert the rendered ctx has effective_stage == 1
|
|
1212
|
+
# 4. Simulate executor: write the Stage Carry Evidence JSON
|
|
1213
|
+
# 5. Lead persistence: write carry/stage-1.json + consumers.jsonl done row
|
|
1214
|
+
# 6. Assert both files exist with expected contents
|
|
1215
|
+
...
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
(Use the same harness existing e2e tests use to render bundles and to simulate worker output.)
|
|
1219
|
+
|
|
1220
|
+
- [ ] **Step 3: Implement Q2–Q9**
|
|
1221
|
+
|
|
1222
|
+
Translate each spec §9 row into a test case. Use these IDs as test names so the mapping stays obvious:
|
|
1223
|
+
- `test_q2_three_stage_plan_first_run_picks_stage_1`
|
|
1224
|
+
- `test_q3_after_stage_1_done_run_2_picks_stage_2_and_loads_sidecar`
|
|
1225
|
+
- `test_q4_step_seven_rejected_by_validator_at_prepare`
|
|
1226
|
+
- `test_q5_depends_on_nonexistent_stage_rejected_S6`
|
|
1227
|
+
- `test_q6_force_stage_when_already_done_errors`
|
|
1228
|
+
- `test_q7_parallel_runs_pick_distinct_none_stages`
|
|
1229
|
+
- `test_q8_partial_depends_on_blocks_higher_stage`
|
|
1230
|
+
- `test_q9_cycle_rejected_S8`
|
|
1231
|
+
|
|
1232
|
+
- [ ] **Step 4: Run the e2e suite**
|
|
1233
|
+
|
|
1234
|
+
Run: `pytest tests-e2e/test_implementation_plan_multi_stage.py -v`
|
|
1235
|
+
Expected: all 9 tests pass.
|
|
1236
|
+
|
|
1237
|
+
- [ ] **Step 5: Commit**
|
|
1238
|
+
|
|
1239
|
+
```bash
|
|
1240
|
+
git add tests-e2e/test_implementation_plan_multi_stage.py
|
|
1241
|
+
git commit -m "test(e2e): cover Q1–Q9 scenarios from multi-stage design spec"
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
- [ ] **Step 6: Final verification — full test suite**
|
|
1245
|
+
|
|
1246
|
+
Run: `pytest tests/ tests-e2e/ -v`
|
|
1247
|
+
Expected: all green.
|
|
1248
|
+
|
|
1249
|
+
---
|
|
1250
|
+
|
|
1251
|
+
## Self-review (post-write)
|
|
1252
|
+
|
|
1253
|
+
(performed by plan author after writing — fixed inline)
|
|
1254
|
+
|
|
1255
|
+
- **Spec coverage:** spec §1 mot →§2 5 원칙 → §3 model → §4 흐름 → §5 변경 파일 → §6 wizard → §9 시나리오 모두 task 매핑됨. spec §5.9 (release-handoff stage PR 일람) 와 §8 미해결 항목들은 의도적으로 별도 후속 spec 으로 분리(§8 명시) — 본 plan 의 비범위.
|
|
1256
|
+
- **Placeholder scan:** "TBD" / "TODO" / "implement later" 없음. Task 7 의 `# ... minimal fields ...` 자리는 wizard 의 `WizardState` 정확한 필드 목록을 plan 실행자가 wizard.py 의 dataclass 정의를 보고 채워야 하는 자리 — 이건 fixture set-up 의 본질이라 정상.
|
|
1257
|
+
- **Type consistency:** `StageMeta` 5 필드(`stage_number / title / depends_on / step_count / exit_contract_summary`) 가 Task 1, 5, 7 에서 동일. `consumers.jsonl` row identity (`impl_task_key, stage, status`) 도 Task 2, 6, 9 에서 동일.
|
|
1258
|
+
- **No-placeholder rule:** code-touching step 마다 실제 코드 블록 또는 정확한 diff 명세 포함. 문서 변경은 정확한 before/after 인용.
|
|
1259
|
+
|
|
1260
|
+
---
|
|
1261
|
+
|
|
1262
|
+
**Plan complete and saved.** 다음 단계 — 실행 방식 선택:
|
|
1263
|
+
|
|
1264
|
+
1. **Subagent-Driven (recommended)** — fresh subagent per task, two-stage review between tasks, fast iteration.
|
|
1265
|
+
2. **Inline Execution** — execute tasks in this session using `superpowers:executing-plans` skill, batch execution with checkpoints.
|
|
1266
|
+
|
|
1267
|
+
어느 쪽으로 진행할까요?
|