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,289 @@
|
|
|
1
|
+
# Stage Worktree 격리 P2 — started-exclusion 순수 빌딩블록 구현 계획
|
|
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:** A2(started-exclusion)의 순수 로직 — registry의 active stage 번호 조회 헬퍼와 `_resolve_effective_stages`의 `started`/`reserved` 제외 — 를 단위 테스트로 완결한다. worktree 발급·base 계산·`_reserve_implementation_stages` 연결은 P3.
|
|
6
|
+
|
|
7
|
+
**Architecture:** [run.py:264 `_resolve_effective_stages`](../../../scripts/okstra_ctl/run.py)는 현재 ready 판정에서 `done`만 본다. 여기에 `started_stages`/`reserved_stages`(둘 다 default 빈 set) 파라미터를 더해, ready 집합과 `--stage N` 단일 요청 모두에서 이미 점유된 stage를 제외한다. 점유 출처는 둘 — `consumers.jsonl`의 `started`(관찰)와 registry의 active stage-key 예약(SSOT). registry 조회는 P1의 stage-key 엔트리를 읽는 `list_active_stage_numbers` 헬퍼로 추가한다. 두 함수 모두 순수/조회라 단위 테스트로 끝난다.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3 (`scripts/okstra_ctl/worktree_registry.py`, `run.py`), pytest.
|
|
10
|
+
|
|
11
|
+
설계 문서: [docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md](../specs/2026-06-06-stage-worktree-isolation-design.md) §2.3, §3.3
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
- Modify: `scripts/okstra_ctl/worktree_registry.py`
|
|
18
|
+
- `list_active_stage_numbers()` — 신규 (P1의 stage-key 엔트리에서 active stage 번호 집합 조회)
|
|
19
|
+
- Modify: `scripts/okstra_ctl/run.py`
|
|
20
|
+
- `_resolve_effective_stages()` — `started_stages`/`reserved_stages` 파라미터 추가 ([run.py:233](../../../scripts/okstra_ctl/run.py))
|
|
21
|
+
- Test: `tests/test_okstra_worktree_registry.py`, `tests/test_okstra_run_stage_resolution.py` (신규 또는 기존 stage 테스트 파일)
|
|
22
|
+
|
|
23
|
+
P3(별도 plan)이 이 둘을 `_reserve_implementation_stages`에서 소비하고 worktree 발급·base 계산을 연결한다. P2는 그 의존만 만든다.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
### Task 1: registry `list_active_stage_numbers` 조회 헬퍼
|
|
28
|
+
|
|
29
|
+
**Files:**
|
|
30
|
+
- Modify: `scripts/okstra_ctl/worktree_registry.py` (신규 함수, `release` 위)
|
|
31
|
+
- Test: `tests/test_okstra_worktree_registry.py`
|
|
32
|
+
|
|
33
|
+
- [ ] **Step 1: Write the failing test**
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
def test_list_active_stage_numbers_returns_reserved_stages():
|
|
37
|
+
from okstra_ctl.worktree_registry import reserve, release, list_active_stage_numbers
|
|
38
|
+
# task-key 엔트리는 stage 가 아니므로 제외되어야 한다
|
|
39
|
+
reserve(project_id="p", task_group="g", task_id="t",
|
|
40
|
+
worktree_path="/wt/t", branch="feat-t", base_ref="main")
|
|
41
|
+
reserve(project_id="p", task_group="g", task_id="t", stage_number=2,
|
|
42
|
+
worktree_path="/wt/t/stage-2", branch="feat-t-s2", base_ref="b")
|
|
43
|
+
reserve(project_id="p", task_group="g", task_id="t", stage_number=3,
|
|
44
|
+
worktree_path="/wt/t/stage-3", branch="feat-t-s3", base_ref="b")
|
|
45
|
+
assert list_active_stage_numbers("p", "g", "t") == {2, 3}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_list_active_stage_numbers_excludes_released_and_other_tasks():
|
|
49
|
+
from okstra_ctl.worktree_registry import reserve, release, list_active_stage_numbers
|
|
50
|
+
reserve(project_id="p", task_group="g", task_id="t", stage_number=2,
|
|
51
|
+
worktree_path="/wt/t/stage-2", branch="feat-t-s2", base_ref="b")
|
|
52
|
+
# 다른 task 의 stage 는 섞이면 안 된다
|
|
53
|
+
reserve(project_id="p", task_group="g", task_id="other", stage_number=2,
|
|
54
|
+
worktree_path="/wt/o/stage-2", branch="feat-other-s2", base_ref="b")
|
|
55
|
+
assert list_active_stage_numbers("p", "g", "t") == {2}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_list_active_stage_numbers_empty_when_none():
|
|
59
|
+
from okstra_ctl.worktree_registry import list_active_stage_numbers
|
|
60
|
+
assert list_active_stage_numbers("p", "g", "nope") == set()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
64
|
+
|
|
65
|
+
Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_worktree_registry.py -k list_active_stage -v`
|
|
66
|
+
Expected: FAIL — `cannot import name 'list_active_stage_numbers'`.
|
|
67
|
+
|
|
68
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# worktree_registry.py — release() 정의 바로 위에 추가
|
|
72
|
+
def list_active_stage_numbers(
|
|
73
|
+
project_id: str, task_group: str, task_id: str,
|
|
74
|
+
) -> set:
|
|
75
|
+
"""Return the set of stage numbers with an active stage-key reservation
|
|
76
|
+
for this task. Used by the stage resolver to exclude stages a concurrent
|
|
77
|
+
run already holds (the occupancy SSOT). Excludes the task-key entry
|
|
78
|
+
(stage is None) and released entries."""
|
|
79
|
+
prefix = task_key(project_id, task_group, task_id) + "#stage-"
|
|
80
|
+
with _registry_lock():
|
|
81
|
+
data = _load()
|
|
82
|
+
out = set()
|
|
83
|
+
for key, row in data["tasks"].items():
|
|
84
|
+
if (key.startswith(prefix)
|
|
85
|
+
and row.get("status") == "active"
|
|
86
|
+
and row.get("stage") is not None):
|
|
87
|
+
out.add(row["stage"])
|
|
88
|
+
return out
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
92
|
+
|
|
93
|
+
Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_worktree_registry.py -v`
|
|
94
|
+
Expected: PASS.
|
|
95
|
+
|
|
96
|
+
- [ ] **Step 5: Commit**
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cd /Volumes/Workspaces/workspace/projects/Okstra
|
|
100
|
+
git add scripts/okstra_ctl/worktree_registry.py tests/test_okstra_worktree_registry.py
|
|
101
|
+
git commit -m "feat(worktree-registry): list active stage reservations for a task"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### Task 2: `_resolve_effective_stages` started/reserved 제외 (A2)
|
|
107
|
+
|
|
108
|
+
**Files:**
|
|
109
|
+
- Modify: `scripts/okstra_ctl/run.py:233` (`_resolve_effective_stages`)
|
|
110
|
+
- Test: `tests/test_okstra_run_stage_resolution.py` (신규 파일)
|
|
111
|
+
|
|
112
|
+
- [ ] **Step 1: Write the failing test**
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# tests/test_okstra_run_stage_resolution.py (신규)
|
|
116
|
+
import sys
|
|
117
|
+
from pathlib import Path
|
|
118
|
+
|
|
119
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
|
120
|
+
|
|
121
|
+
import pytest
|
|
122
|
+
|
|
123
|
+
from okstra_ctl.run import _resolve_effective_stages, PrepareError
|
|
124
|
+
|
|
125
|
+
# stages 는 _parse_stage_map_into_ctx 가 만드는 dict 모양과 동일:
|
|
126
|
+
# {"stage_number": int, "depends_on": list[int], "step_count": int}
|
|
127
|
+
def _stage(n, deps, steps=3):
|
|
128
|
+
return {"stage_number": n, "depends_on": deps, "step_count": steps}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_auto_raises_when_all_unfinished_stages_occupied():
|
|
132
|
+
stages = [_stage(1, []), _stage(2, []), _stage(3, [])]
|
|
133
|
+
# stage 1 done, stage 2 started(다른 run), stage 3 reserved(다른 run)
|
|
134
|
+
# → 잡을 ready 가 없음. P2 에서는 기존 raise 동작을 유지(호출자 의미 전환은 P3).
|
|
135
|
+
with pytest.raises(PrepareError, match="no stage is ready"):
|
|
136
|
+
_resolve_effective_stages(
|
|
137
|
+
stages, done_stages={1}, requested="auto",
|
|
138
|
+
started_stages={2}, reserved_stages={3},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_auto_picks_only_unoccupied_ready_stage():
|
|
143
|
+
stages = [_stage(1, []), _stage(2, []), _stage(3, [])]
|
|
144
|
+
got = _resolve_effective_stages(
|
|
145
|
+
stages, done_stages=set(), requested="auto",
|
|
146
|
+
started_stages={1}, reserved_stages=set(),
|
|
147
|
+
)
|
|
148
|
+
assert got == [2, 3] # 1 은 started 라 제외, 2·3 만 batch
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_numeric_stage_rejected_when_started():
|
|
152
|
+
stages = [_stage(2, [])]
|
|
153
|
+
with pytest.raises(PrepareError, match="already in progress or reserved"):
|
|
154
|
+
_resolve_effective_stages(
|
|
155
|
+
stages, done_stages=set(), requested="2",
|
|
156
|
+
started_stages={2}, reserved_stages=set(),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_numeric_stage_rejected_when_reserved():
|
|
161
|
+
stages = [_stage(2, [])]
|
|
162
|
+
with pytest.raises(PrepareError, match="already in progress or reserved"):
|
|
163
|
+
_resolve_effective_stages(
|
|
164
|
+
stages, done_stages=set(), requested="2",
|
|
165
|
+
started_stages=set(), reserved_stages={2},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_defaults_preserve_legacy_behavior():
|
|
170
|
+
# started/reserved 미지정 → 기존 동작 (done 만 고려)
|
|
171
|
+
stages = [_stage(1, []), _stage(2, [1])]
|
|
172
|
+
assert _resolve_effective_stages(stages, done_stages={1}, requested="auto") == [2]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
176
|
+
|
|
177
|
+
Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_run_stage_resolution.py -v`
|
|
178
|
+
Expected: FAIL — `_resolve_effective_stages()` got unexpected keyword `started_stages` (그리고 numeric reject 메시지 불일치).
|
|
179
|
+
|
|
180
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
# run.py — _resolve_effective_stages 교체 (시그니처 + 제외 로직)
|
|
184
|
+
def _resolve_effective_stages(
|
|
185
|
+
stages: list,
|
|
186
|
+
done_stages: set,
|
|
187
|
+
requested: str,
|
|
188
|
+
budget: int = RUN_STEP_BUDGET,
|
|
189
|
+
started_stages: set = None,
|
|
190
|
+
reserved_stages: set = None,
|
|
191
|
+
) -> list:
|
|
192
|
+
"""Return the ordered list of stage numbers this run executes.
|
|
193
|
+
|
|
194
|
+
`requested` is "auto" or a decimal string. For "auto" the run batches all
|
|
195
|
+
ready stages (depends-on all done, itself neither done nor occupied) in
|
|
196
|
+
stage-number order up to `budget` effective steps. A numeric request is a
|
|
197
|
+
single forced stage. `started_stages`/`reserved_stages` are stages a
|
|
198
|
+
concurrent run already holds (consumers.jsonl `started` and the registry
|
|
199
|
+
stage reservations); they are excluded so two simultaneous runs never pick
|
|
200
|
+
the same stage. Raises PrepareError on rejection cases."""
|
|
201
|
+
started_stages = started_stages or set()
|
|
202
|
+
reserved_stages = reserved_stages or set()
|
|
203
|
+
occupied = done_stages | started_stages | reserved_stages
|
|
204
|
+
if requested != "auto":
|
|
205
|
+
try:
|
|
206
|
+
n = int(requested)
|
|
207
|
+
except ValueError:
|
|
208
|
+
raise PrepareError(
|
|
209
|
+
f"--stage must be 'auto' or an integer, got {requested!r}"
|
|
210
|
+
)
|
|
211
|
+
target = next((s for s in stages if s["stage_number"] == n), None)
|
|
212
|
+
if target is None:
|
|
213
|
+
raise PrepareError(
|
|
214
|
+
f"--stage {n} not in Stage Map "
|
|
215
|
+
f"(have {[s['stage_number'] for s in stages]})"
|
|
216
|
+
)
|
|
217
|
+
if n in done_stages:
|
|
218
|
+
raise PrepareError(
|
|
219
|
+
f"--stage {n} already completed (consumers.jsonl status:done exists)"
|
|
220
|
+
)
|
|
221
|
+
if n in started_stages or n in reserved_stages:
|
|
222
|
+
raise PrepareError(
|
|
223
|
+
f"--stage {n} already in progress or reserved by another run"
|
|
224
|
+
)
|
|
225
|
+
return [n]
|
|
226
|
+
|
|
227
|
+
ready = [
|
|
228
|
+
s for s in stages
|
|
229
|
+
if s["stage_number"] not in occupied
|
|
230
|
+
and all(d in done_stages for d in s["depends_on"])
|
|
231
|
+
]
|
|
232
|
+
if not ready:
|
|
233
|
+
raise PrepareError(
|
|
234
|
+
"no stage is ready: every remaining stage has unsatisfied depends-on"
|
|
235
|
+
)
|
|
236
|
+
batch: list = []
|
|
237
|
+
total = 0
|
|
238
|
+
for s in ready:
|
|
239
|
+
sc = s.get("step_count", 0) or 0
|
|
240
|
+
if batch and total + sc > budget:
|
|
241
|
+
break
|
|
242
|
+
batch.append(s["stage_number"])
|
|
243
|
+
total += sc
|
|
244
|
+
return batch
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
> 변경점: 시그니처에 `started_stages`/`reserved_stages`를 더하고, ready 집합 계산과 `--stage N` numeric 경로 양쪽에서 `occupied`(= done ∪ started ∪ reserved)를 제외한다. **`if not ready: raise` 동작은 기존 그대로 유지** — backward compat를 위해 빈-리스트 의미 전환은 호출자(P3)와 함께 한다. P2 단계에서는 occupied로 ready가 비어도 기존과 동일하게 `PrepareError`가 난다(동시 병렬 둘째 run의 "정상 종료" 전환은 P3 호출자 레벨에서 점유 여부를 구분해 처리).
|
|
248
|
+
|
|
249
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
250
|
+
|
|
251
|
+
Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_run_stage_resolution.py -v`
|
|
252
|
+
Expected: PASS (5개).
|
|
253
|
+
|
|
254
|
+
- [ ] **Step 5: Run the broader suite for regressions**
|
|
255
|
+
|
|
256
|
+
Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/ -k "stage or resolve or reserve or multi_stage" -v`
|
|
257
|
+
Expected: PASS — occupied 제외는 순수 추가다. 기존 호출자 `_reserve_implementation_stages`는 `started_stages`/`reserved_stages`를 전달하지 않아 default 빈 set으로 동일 동작하고, `if not ready: raise`도 그대로 유지되므로 `test_q7`/multi-stage e2e 회귀가 없어야 한다. 만약 회귀가 나면 **BLOCKED 보고**(임의로 기존 테스트를 고치지 말 것).
|
|
258
|
+
|
|
259
|
+
- [ ] **Step 6: Commit**
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
cd /Volumes/Workspaces/workspace/projects/Okstra
|
|
263
|
+
git add scripts/okstra_ctl/run.py tests/test_okstra_run_stage_resolution.py
|
|
264
|
+
git commit -m "feat(run): exclude started/reserved stages from resolution (A2)"
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Self-Review
|
|
270
|
+
|
|
271
|
+
**Spec coverage (P2 범위):**
|
|
272
|
+
- §2.3 ready 집합 = depends-on done **+ started 없음 + registry 미예약** → Task 2 ✓ (점유 = done|started|reserved)
|
|
273
|
+
- §2.3 registry 예약이 점유 SSOT → Task 1 `list_active_stage_numbers`가 그 조회면 ✓
|
|
274
|
+
- §3.3 `--stage N` 점유 시 거부 → Task 2 numeric reject ✓
|
|
275
|
+
- worktree 발급·base 계산·`_reserve_implementation_stages` 연결·CLI → **P3 범위(본 plan 비포함)**
|
|
276
|
+
|
|
277
|
+
**Placeholder scan:** 없음 — 모든 step에 실제 코드.
|
|
278
|
+
|
|
279
|
+
**Type consistency:** `started_stages`/`reserved_stages`(set), `list_active_stage_numbers`(→ set) 이 Task 1↔2 간 일치. `_resolve_effective_stages` 반환 타입 list 유지.
|
|
280
|
+
|
|
281
|
+
**backward compat:** P2는 `_resolve_effective_stages`에 occupied 제외를 더하는 **순수 추가**다. `if not ready: raise`는 기존 그대로 유지하므로, `started_stages`/`reserved_stages`를 전달하지 않는 기존 호출자(`_reserve_implementation_stages`)는 default 빈 set으로 동일하게 동작한다. 동시 병렬 둘째 run의 "정상 종료" 의미 전환과 호출자 연결은 P3.
|
|
282
|
+
|
|
283
|
+
## 검증 (P2 완료 기준)
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
cd /Volumes/Workspaces/workspace/projects/Okstra
|
|
287
|
+
python3 -m pytest tests/test_okstra_worktree_registry.py tests/test_okstra_run_stage_resolution.py -v
|
|
288
|
+
python3 -m pytest tests/ -q # 전체 회귀
|
|
289
|
+
```
|