okstra 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.kr.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/kr/architecture.md +1 -0
  4. package/docs/kr/cli.md +2 -1
  5. package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +993 -0
  6. package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +93 -0
  7. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +447 -0
  8. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +289 -0
  9. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +774 -0
  10. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +303 -0
  11. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +387 -0
  12. package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +126 -0
  13. package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +180 -0
  14. package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +179 -0
  15. package/package.json +1 -1
  16. package/runtime/BUILD.json +2 -2
  17. package/runtime/agents/workers/report-writer-worker.md +1 -0
  18. package/runtime/bin/lib/okstra/cli.sh +5 -1
  19. package/runtime/bin/okstra.sh +1 -0
  20. package/runtime/prompts/launch.template.md +1 -0
  21. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  22. package/runtime/prompts/profiles/_implementation-executor.md +16 -9
  23. package/runtime/prompts/profiles/_implementation-verifier.md +4 -1
  24. package/runtime/prompts/profiles/final-verification.md +7 -7
  25. package/runtime/prompts/profiles/implementation-planning.md +14 -7
  26. package/runtime/prompts/wizard/prompts.ko.json +3 -2
  27. package/runtime/python/okstra_ctl/analysis_packet.py +14 -2
  28. package/runtime/python/okstra_ctl/render.py +3 -0
  29. package/runtime/python/okstra_ctl/run.py +541 -41
  30. package/runtime/python/okstra_ctl/wizard.py +25 -7
  31. package/runtime/python/okstra_ctl/worktree.py +126 -9
  32. package/runtime/python/okstra_ctl/worktree_registry.py +88 -17
  33. package/runtime/schemas/final-report-v1.0.schema.json +36 -0
  34. package/runtime/skills/okstra-convergence/SKILL.md +14 -3
  35. package/runtime/skills/okstra-memory/SKILL.md +28 -5
  36. package/runtime/skills/okstra-run/SKILL.md +1 -1
  37. package/runtime/templates/reports/final-report.template.md +12 -0
  38. package/runtime/templates/reports/final-verification-input.template.md +8 -5
  39. package/runtime/templates/reports/i18n/en.json +3 -1
  40. package/runtime/templates/reports/i18n/ko.json +3 -1
  41. package/runtime/validators/validate-implementation-plan-stages.py +57 -11
  42. package/runtime/validators/validate-run.py +143 -1
  43. package/runtime/validators/validate-workflow.sh +6 -1
  44. package/src/memory.mjs +50 -11
@@ -0,0 +1,93 @@
1
+ # stage 병렬 안전화 + 잔여 수정 작업 백로그
2
+
3
+ > **성격:** 이 문서는 단일 기능 구현 계획이 아니라, `fontradar-v2-api:dev-9184` stage 병렬 실행 사고를 계기로 도출된 okstra 본체 수정 후보들의 **작업 목록 + 우선순위 + 착수 순서**다. 각 항목은 착수 시점에 개별 `*-design.md`(spec) / `*.md`(plan) 한 쌍으로 분화한다.
4
+
5
+ **배경:** 사용자가 같은 task(`dev-9184`)의 stage2/stage3를 동시에 두 `implementation` run으로 띄웠고, 두 run이 **같은 task-key worktree·브랜치를 공유**해 커밋이 한 브랜치에 인터리브됐다. 코드 자체는 plan의 파일-서로소 설계대로 갈라져 있어 사후 cherry-pick으로 stage별 브랜치 분리에는 성공했으나, 이 사고는 아래 구조적 결함들을 드러냈다.
6
+
7
+ **핵심 결정 (이 백로그의 전제):**
8
+ - **C1 = (b)** — ADR(`.okstra/decisions/`)을 **git에 커밋하지 않는다.** okstra의 institutional memory는 로컬 `.okstra/` 안에만 둔다. 사용자의 `.okstra` gitignore 정책을 존중한다. (trade-off: ADR이 git/CI/타 머신에서 공유되지 않는다 — 의도된 선택.)
9
+
10
+ ---
11
+
12
+ ## 우선순위 / 착수 순서
13
+
14
+ | 순위 | 항목 | 그룹 | 규모 | 착수 형태 |
15
+ |---|---|---|---|---|
16
+ | 1 | **A1** stage별 worktree 격리 | 병렬 안전 | 큼 | **spec 먼저** (이번에 착수) |
17
+ | 2 | **A2** started-exclusion 구현 | 병렬 안전 | 중간 | A1 spec에 통합 |
18
+ | 3 | **C1** ADR git 커밋 중단 | gitignore 존중 | 작음~중간 | 단독 plan |
19
+ | 4 | **B1** okstra-run SKILL stage_pick 문서 | UX 일관성 | 작음 | 단독(즉시) |
20
+ | 5 | **B2** `okstra.sh` `--stage` 패스스루 | UX 일관성 | 중간 | B1/A1과 묶음 |
21
+
22
+ > 사용자 지시에 따라 **A1 spec부터** 착수한다. A2는 A1과 동일한 stage 예약·점유 경로를 건드리므로 A1 spec에 함께 설계한다.
23
+
24
+ ---
25
+
26
+ ## 그룹 A — stage 병렬을 안전하게
27
+
28
+ ### A1. stage별 worktree 격리 (핵심, 설계 변경)
29
+
30
+ - **증상:** worktree가 **task-key 단위 1개**로 발급되어([worktree.py](../../../scripts/okstra_ctl/worktree.py)), 같은 task의 stage2/stage3 병렬 run이 같은 worktree·브랜치를 공유한다. 두 executor가 동일 디렉토리에서 파일을 편집하고 같은 브랜치에 커밋하면 충돌·덮어쓰기가 발생한다. dev-9184 사고의 근본 원인.
31
+ - **현재 설계 위치:**
32
+ - worktree 발급/registry 예약: [worktree.py](../../../scripts/okstra_ctl/worktree.py)
33
+ - stage 예약(점유): `_reserve_implementation_stages` ([run.py:890](../../../scripts/okstra_ctl/run.py)), `_resolve_effective_stages` ([run.py:233](../../../scripts/okstra_ctl/run.py))
34
+ - executor의 stage batch 실행: [_implementation-executor.md:30](../../../prompts/profiles/_implementation-executor.md)
35
+ - **방향(사용자 제안):** "의존 stage부터 worktree를 새로 나눈다."
36
+ - `depends-on (none)` stage끼리는 **공통 base**에서 각자 worktree를 떠 진짜 병렬.
37
+ - `depends-on`이 있는 stage는 **선행 stage의 done commit을 base**로 worktree를 분기 → carry-in(파일·커밋)이 성립.
38
+ - registry 예약 키를 현재 `task-id` 단위에서 `task-id/stage-N`(또는 ready-set batch) 단위로 확장. flock 직렬성은 유지.
39
+ - **결정해야 할 설계 포인트(spec에서 다룸):**
40
+ 1. worktree 격리 단위 = stage 단위인가, run-batch 단위인가? (A2의 batch 개념과 정합)
41
+ 2. 병렬 stage의 PR/브랜치 네이밍 (현재 `task-<id>` 단일 → `task-<id>-stage-<N>`?)
42
+ 3. 모든 stage done 이후 브랜치 합류(머지) 절차 — release-handoff와의 접점.
43
+ 4. 기존 단일-worktree 흐름과의 관계(pre-1.0이므로 호환 shim 없음 — 단일 흐름을 대체할지).
44
+ - **규모:** 큼. **이번에 spec(`docs/superpowers/specs/`)부터 작성.**
45
+
46
+ ### A2. started-exclusion 구현
47
+
48
+ - **증상:** `auto` stage 선택 로직이 `consumers.jsonl`의 `done`만 보고 `started`를 무시한다([run.py:264](../../../scripts/okstra_ctl/run.py)). 따라서 stage2가 `started`(미완)인 상태에서 다른 run이 또 `auto`로 돌면 같은 ready-set을 중복 점유한다. dev-9184에서 stage2·stage3가 둘 다 `started`로 찍힌 직접 원인.
49
+ - **기존 기록:** known gap으로 문서화돼 있음 — [stage-run-batching.md:13](2026-06-04-stage-run-batching.md), `tests/test_e2e_multi_stage_q1_q9.py::test_q7`가 현 동작을 박제.
50
+ - **방향:** ready 집합 계산 시 `started`(미완) stage도 제외 + worktree registry의 stage-key 예약과 연동(A1과 동일 경로).
51
+ - **규모:** 중간. **A1 spec에 통합 설계.**
52
+
53
+ ---
54
+
55
+ ## 그룹 B — stage 선택 UX 일관성
56
+
57
+ ### B1. okstra-run SKILL.md stage_pick 문서 누락
58
+
59
+ - **증상:** implementation sub-flow 설명이 *"approved-plan path + executor pick"*만 적고 **stage_pick 단계를 누락**한다([SKILL.md:125](../../../skills/okstra-run/SKILL.md)). 실제 wizard에는 존재한다([wizard.py:1994](../../../scripts/okstra_ctl/wizard.py)). 문서만 보면 "stage 선택이 없다"고 오해하게 된다.
60
+ - **수정 위치:** [skills/okstra-run/SKILL.md:125](../../../skills/okstra-run/SKILL.md) — sub-flow 목록에 stage_pick 한 줄 추가(approved-plan 직후, executor 직전).
61
+ - **규모:** 작음(문서). **즉시 처리 가능.**
62
+
63
+ ### B2. `okstra.sh` / `cli.sh`에 `--stage` 플래그 부재
64
+
65
+ - **증상:** `--stage`가 `run.py` argparse에만 정의돼 있고([run.py:1363](../../../scripts/okstra_ctl/run.py)), CLI 래퍼 [`okstra.sh`](../../../scripts/okstra.sh)·[`cli.sh`](../../../scripts/lib/okstra/cli.sh)는 파싱·전달하지 않는다. 결과적으로 CLI 래퍼로는 stage를 지정할 수 없고 인터랙티브 wizard 또는 `python -m okstra_ctl.run` 직접 호출로만 가능하다(단일 reference point 위반에 가까움).
66
+ - **수정 위치:** [`scripts/okstra.sh`](../../../scripts/okstra.sh) PY_ARGS 빌더, [`scripts/lib/okstra/cli.sh`](../../../scripts/lib/okstra/cli.sh) 옵션 파서, (필요 시) `interactive.sh`.
67
+ - **규모:** 중간. **A1(병렬 stage UX 정비) 또는 B1과 묶어 처리.**
68
+
69
+ ---
70
+
71
+ ## 그룹 C — ADR ↔ gitignore 충돌
72
+
73
+ ### C1. ADR을 git에 커밋하지 않도록 변경 — 결정: (b)
74
+
75
+ - **증상:** okstra는 ADR을 **git에 커밋하길 의도**한다 — [implementation-planning.md:91](../../../prompts/profiles/implementation-planning.md) *"so the `implementation` run commits the file inside okstra's subtree"*. 그러나 사용자가 `.okstra` 전체를 gitignore하면, executor가 `git add -f`로 gitignore를 우회해 ADR만 추적 상태가 된다(매 run 반복). dev-9184에서 `0001-...md`가 git 추적된 원인.
76
+ - **결정 (b):** ADR 파일은 `.okstra/decisions/<NNNN>-<slug>.md`에 **생성하되 git에 커밋하지 않는다.** institutional memory는 로컬 `.okstra/`에만 존재한다.
77
+ - **수정 위치:**
78
+ - [implementation-planning.md:91](../../../prompts/profiles/implementation-planning.md) — "commits the file" 문구를 "creates the file (NOT git-committed; `.okstra` is gitignored local memory)"로 수정. stepwise step 문구도 "Create … (do NOT `git add`/commit)"로.
79
+ - [_implementation-executor.md:54](../../../prompts/profiles/_implementation-executor.md) — local git ops 절에 ".okstra/ 경로는 절대 `git add -f` 하지 않는다" 가드 1줄 추가.
80
+ - [_common-contract.md:76](../../../prompts/profiles/_common-contract.md) — "decision files land only at .okstra/decisions" 유지(위치는 맞음), "not tracked by git" 명시 추가.
81
+ - **trade-off(수용):** ADR이 git/CI/타 머신에서 공유되지 않음. 필요해지면 사용자가 프로젝트 `.gitignore`에 `!.okstra/decisions/` 예외를 직접 두는 별도 경로로 전환 가능(현 결정의 비범위).
82
+ - **규모:** 작음~중간. **단독 plan.**
83
+
84
+ ---
85
+
86
+ ## seed 규칙 준수
87
+
88
+ 모든 수정은 사용자 메모리 `feedback_okstra_fixes_target_end_users`에 따라 **seed/template/프로파일 파일**에 가한다 — `runtime/`(빌드 산출물)·개인 `.claude/`가 아니다. 각 항목 착수 시 `declaration vs enforcement`(okstra `CLAUDE.md` rule 3)를 점검한다: 프로파일의 MUST/금지 문구마다 validator·런타임 가드 등 실제 강제 지점을 함께 명시한다.
89
+
90
+ ## 다음 단계
91
+
92
+ 1. **A1 spec 착수** — `docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md`(가제)로 위 A1 설계 포인트 4개를 결정. A2를 같은 spec에 통합.
93
+ 2. 이후 C1 → B1 → B2 순으로 분화.
@@ -0,0 +1,447 @@
1
+ # Stage Worktree 격리 P1 — registry/compute 빌딩블록 구현 계획
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:** stage worktree 격리의 하위 빌딩블록 — worktree registry의 stage-key 예약, `implementation_base_commit` 1회 고정, `compute_worktree_path`/`compute_branch_name`의 stage 분기 — 를 순수 단위 테스트로 완결한다. provision/base 계산 조립은 P2.
6
+
7
+ **Architecture:** 기존 `worktree_registry.py`의 `task_key`/`reserve`/`lookup`을 optional `stage_number`로 확장해 `<task-key>#stage-<N>` 키를 같은 flock·branches 충돌 검사 경로로 예약한다. `WorktreeEntry`에 `stage`·`implementation_base_commit` 필드를 더해 `_load`된 row가 `WorktreeEntry(**row)`로 안전하게 복원되게 한다. `compute_*`는 `stage_number=None`이면 기존 동작을 그대로 유지(다른 phase 호환).
8
+
9
+ **Tech Stack:** Python 3 (`scripts/okstra_ctl/worktree_registry.py`, `worktree.py`), pytest (`tests/`, conftest가 `OKSTRA_HOME`을 tmp로 격리).
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.1, §3.2
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ - Modify: `scripts/okstra_ctl/worktree_registry.py`
18
+ - `WorktreeEntry` dataclass — `stage: Optional[int] = None`, `implementation_base_commit: str = ""` 추가 ([worktree_registry.py:57](../../../scripts/okstra_ctl/worktree_registry.py))
19
+ - `task_key()` — optional `stage_number` ([worktree_registry.py:46](../../../scripts/okstra_ctl/worktree_registry.py))
20
+ - `reserve()` / `lookup()` — optional `stage_number` ([worktree_registry.py:130](../../../scripts/okstra_ctl/worktree_registry.py), [:115](../../../scripts/okstra_ctl/worktree_registry.py))
21
+ - `set_implementation_base()` / `get_implementation_base()` — 신규 (멱등 고정/조회)
22
+ - Modify: `scripts/okstra_ctl/worktree.py`
23
+ - `compute_worktree_path()` — optional `stage_number` ([worktree.py:489](../../../scripts/okstra_ctl/worktree.py))
24
+ - `compute_branch_name()` — optional `stage_number` ([worktree.py:511](../../../scripts/okstra_ctl/worktree.py))
25
+ - Test: `tests/test_okstra_worktree_registry.py`, `tests/test_okstra_worktree.py` (기존 파일에 추가)
26
+
27
+ 각 task는 하나의 빌딩블록을 완결한다. provision/run.py 연결은 본 plan 범위 밖(P2).
28
+
29
+ ---
30
+
31
+ ### Task 1: registry `WorktreeEntry`·`task_key` stage 확장
32
+
33
+ **Files:**
34
+ - Modify: `scripts/okstra_ctl/worktree_registry.py:57` (`WorktreeEntry`), `:46` (`task_key`)
35
+ - Test: `tests/test_okstra_worktree_registry.py`
36
+
37
+ - [ ] **Step 1: Write the failing test**
38
+
39
+ ```python
40
+ # tests/test_okstra_worktree_registry.py 끝에 추가
41
+ from okstra_ctl.worktree_registry import WorktreeEntry # 상단 import 에 합쳐도 됨
42
+
43
+
44
+ def test_task_key_with_stage_appends_stage_suffix():
45
+ from okstra_ctl.worktree_registry import task_key
46
+ assert task_key("proj", "grp", "tid") == "proj/grp/tid"
47
+ assert task_key("proj", "grp", "tid", stage_number=2) == "proj/grp/tid#stage-2"
48
+
49
+
50
+ def test_worktree_entry_defaults_stage_none_and_empty_base_commit():
51
+ e = WorktreeEntry(
52
+ task_key="proj/grp/tid", project_id="proj", task_group="grp",
53
+ task_id="tid", worktree_path="/wt", branch="feat-tid",
54
+ base_ref="main", created_at="2026-06-06T00:00:00",
55
+ )
56
+ assert e.stage is None
57
+ assert e.implementation_base_commit == ""
58
+ ```
59
+
60
+ - [ ] **Step 2: Run test to verify it fails**
61
+
62
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_worktree_registry.py::test_task_key_with_stage_appends_stage_suffix tests/test_okstra_worktree_registry.py::test_worktree_entry_defaults_stage_none_and_empty_base_commit -v`
63
+ Expected: FAIL — `task_key() got an unexpected keyword argument 'stage_number'` / `WorktreeEntry` has no `stage`.
64
+
65
+ - [ ] **Step 3: Write minimal implementation**
66
+
67
+ ```python
68
+ # worktree_registry.py — task_key (기존 시그니처 교체)
69
+ def task_key(
70
+ project_id: str, task_group: str, task_id: str,
71
+ stage_number: Optional[int] = None,
72
+ ) -> str:
73
+ """Canonical task-key. With stage_number, returns the stage-scoped
74
+ key `<proj>/<group>/<task>#stage-<N>` used to reserve a per-stage
75
+ worktree independently of the task-key entry."""
76
+ base = f"{project_id}/{task_group}/{task_id}"
77
+ if stage_number is not None:
78
+ return f"{base}#stage-{stage_number}"
79
+ return base
80
+ ```
81
+
82
+ ```python
83
+ # worktree_registry.py — WorktreeEntry 에 두 필드 추가 (status 아래)
84
+ @dataclass
85
+ class WorktreeEntry:
86
+ task_key: str
87
+ project_id: str
88
+ task_group: str
89
+ task_id: str
90
+ worktree_path: str
91
+ branch: str
92
+ base_ref: str
93
+ created_at: str
94
+ last_phase: str = ""
95
+ status: str = "active" # "active" | "released"
96
+ stage: Optional[int] = None
97
+ implementation_base_commit: str = ""
98
+ ```
99
+
100
+ - [ ] **Step 4: Run test to verify it passes**
101
+
102
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_worktree_registry.py -v`
103
+ Expected: PASS (신규 2개 + 기존 전부).
104
+
105
+ - [ ] **Step 5: Commit**
106
+
107
+ ```bash
108
+ cd /Volumes/Workspaces/workspace/projects/Okstra
109
+ git add scripts/okstra_ctl/worktree_registry.py tests/test_okstra_worktree_registry.py
110
+ git commit -m "feat(worktree-registry): add stage-scoped task_key and entry fields"
111
+ ```
112
+
113
+ ---
114
+
115
+ ### Task 2: registry `reserve`/`lookup` stage-key 예약
116
+
117
+ **Files:**
118
+ - Modify: `scripts/okstra_ctl/worktree_registry.py:130` (`reserve`), `:115` (`lookup`)
119
+ - Test: `tests/test_okstra_worktree_registry.py`
120
+
121
+ - [ ] **Step 1: Write the failing test**
122
+
123
+ ```python
124
+ def test_reserve_and_lookup_stage_key_independent_from_task_key():
125
+ from okstra_ctl.worktree_registry import reserve, lookup
126
+ # task-key 엔트리
127
+ reserve(project_id="p", task_group="g", task_id="t",
128
+ worktree_path="/wt/t", branch="feat-t", base_ref="main")
129
+ # stage-key 엔트리 (독립)
130
+ reserve(project_id="p", task_group="g", task_id="t", stage_number=2,
131
+ worktree_path="/wt/t/stage-2", branch="feat-t-s2", base_ref="abc123")
132
+
133
+ base = lookup("p", "g", "t")
134
+ s2 = lookup("p", "g", "t", stage_number=2)
135
+ assert base.worktree_path == "/wt/t" and base.stage is None
136
+ assert s2.worktree_path == "/wt/t/stage-2" and s2.stage == 2
137
+
138
+
139
+ def test_reserve_same_stage_twice_raises():
140
+ from okstra_ctl.worktree_registry import reserve
141
+ reserve(project_id="p", task_group="g", task_id="t", stage_number=2,
142
+ worktree_path="/wt/t/stage-2", branch="feat-t-s2", base_ref="abc")
143
+ with pytest.raises(RuntimeError, match="already has a worktree registered"):
144
+ reserve(project_id="p", task_group="g", task_id="t", stage_number=2,
145
+ worktree_path="/wt/t/stage-2b", branch="feat-t-s2b", base_ref="def")
146
+ ```
147
+
148
+ - [ ] **Step 2: Run test to verify it fails**
149
+
150
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_worktree_registry.py::test_reserve_and_lookup_stage_key_independent_from_task_key tests/test_okstra_worktree_registry.py::test_reserve_same_stage_twice_raises -v`
151
+ Expected: FAIL — `reserve()`/`lookup()` got unexpected keyword `stage_number`.
152
+
153
+ - [ ] **Step 3: Write minimal implementation**
154
+
155
+ ```python
156
+ # worktree_registry.py — lookup (시그니처 + key 생성 + stage row)
157
+ def lookup(
158
+ project_id: str, task_group: str, task_id: str,
159
+ stage_number: Optional[int] = None,
160
+ ) -> Optional[WorktreeEntry]:
161
+ key = task_key(project_id, task_group, task_id, stage_number)
162
+ with _registry_lock():
163
+ data = _load()
164
+ row = data["tasks"].get(key)
165
+ if not row:
166
+ return None
167
+ return WorktreeEntry(task_key=key, **row)
168
+ ```
169
+
170
+ ```python
171
+ # worktree_registry.py — reserve (stage_number 추가; key 생성 + row 에 stage 기록)
172
+ def reserve(
173
+ *,
174
+ project_id: str,
175
+ task_group: str,
176
+ task_id: str,
177
+ worktree_path: str,
178
+ branch: str,
179
+ base_ref: str,
180
+ phase: str = "",
181
+ stage_number: Optional[int] = None,
182
+ ) -> WorktreeEntry:
183
+ key = task_key(project_id, task_group, task_id, stage_number)
184
+ now = time.strftime("%Y-%m-%dT%H:%M:%S%z") or time.strftime("%Y-%m-%dT%H:%M:%S")
185
+ with _registry_lock():
186
+ data = _load()
187
+ if key in data["tasks"]:
188
+ existing = data["tasks"][key]
189
+ raise RuntimeError(
190
+ f"task-key already has a worktree registered: {key} → "
191
+ f"{existing['worktree_path']} (branch {existing['branch']}). "
192
+ "Use `lookup` to reuse it, or release it before reserving anew."
193
+ )
194
+ owner = data["branches"].get(branch)
195
+ if owner and owner != key:
196
+ raise RuntimeError(
197
+ f"branch {branch!r} is already registered to a different "
198
+ f"task-key: {owner}. Choose a different work-category or "
199
+ "release the conflicting task first."
200
+ )
201
+ row = {
202
+ "project_id": project_id,
203
+ "task_group": task_group,
204
+ "task_id": task_id,
205
+ "worktree_path": worktree_path,
206
+ "branch": branch,
207
+ "base_ref": base_ref,
208
+ "created_at": now,
209
+ "last_phase": phase,
210
+ "status": "active",
211
+ "stage": stage_number,
212
+ }
213
+ data["tasks"][key] = row
214
+ data["branches"][branch] = key
215
+ _save(data)
216
+ return WorktreeEntry(task_key=key, **row)
217
+ ```
218
+
219
+ > 주의: `row`에 `stage` 키를 항상 넣으므로 `WorktreeEntry(task_key=key, **row)`가 `stage`를 받는다. 기존(stage 없는) registry 파일의 row는 `stage` 키가 없어 `WorktreeEntry`의 default `None`이 적용된다 — Task 1에서 default를 추가했기에 안전하다.
220
+
221
+ - [ ] **Step 4: Run test to verify it passes**
222
+
223
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_worktree_registry.py -v`
224
+ Expected: PASS (신규 + 기존 전부; 기존 `reserve`/`lookup` 호출은 `stage_number` 기본 None 으로 동일 동작).
225
+
226
+ - [ ] **Step 5: Commit**
227
+
228
+ ```bash
229
+ cd /Volumes/Workspaces/workspace/projects/Okstra
230
+ git add scripts/okstra_ctl/worktree_registry.py tests/test_okstra_worktree_registry.py
231
+ git commit -m "feat(worktree-registry): reserve/lookup stage-scoped worktree entries"
232
+ ```
233
+
234
+ ---
235
+
236
+ ### Task 3: registry `implementation_base_commit` 멱등 고정/조회
237
+
238
+ **Files:**
239
+ - Modify: `scripts/okstra_ctl/worktree_registry.py` (신규 함수 2개, `release` 위에 배치)
240
+ - Test: `tests/test_okstra_worktree_registry.py`
241
+
242
+ - [ ] **Step 1: Write the failing test**
243
+
244
+ ```python
245
+ def test_set_implementation_base_is_idempotent():
246
+ from okstra_ctl.worktree_registry import reserve, set_implementation_base, get_implementation_base
247
+ reserve(project_id="p", task_group="g", task_id="t",
248
+ worktree_path="/wt/t", branch="feat-t", base_ref="main")
249
+ first = set_implementation_base("p", "g", "t", "commit-AAA")
250
+ second = set_implementation_base("p", "g", "t", "commit-BBB") # 무시됨
251
+ assert first == "commit-AAA"
252
+ assert second == "commit-AAA" # 이미 고정된 값 반환 (멱등)
253
+ assert get_implementation_base("p", "g", "t") == "commit-AAA"
254
+
255
+
256
+ def test_get_implementation_base_none_when_unset():
257
+ from okstra_ctl.worktree_registry import reserve, get_implementation_base
258
+ reserve(project_id="p", task_group="g", task_id="t",
259
+ worktree_path="/wt/t", branch="feat-t", base_ref="main")
260
+ assert get_implementation_base("p", "g", "t") is None
261
+
262
+
263
+ def test_set_implementation_base_missing_task_key_raises():
264
+ from okstra_ctl.worktree_registry import set_implementation_base
265
+ with pytest.raises(RuntimeError, match="no task-key entry"):
266
+ set_implementation_base("p", "g", "missing", "commit-AAA")
267
+ ```
268
+
269
+ - [ ] **Step 2: Run test to verify it fails**
270
+
271
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_worktree_registry.py -k implementation_base -v`
272
+ Expected: FAIL — `cannot import name 'set_implementation_base'`.
273
+
274
+ - [ ] **Step 3: Write minimal implementation**
275
+
276
+ ```python
277
+ # worktree_registry.py — release() 정의 바로 위에 추가
278
+ def set_implementation_base(
279
+ project_id: str, task_group: str, task_id: str, commit: str,
280
+ ) -> str:
281
+ """Fix the shared base commit for this task's implementation stages,
282
+ once. Idempotent: if already set, the existing value is returned and
283
+ `commit` is ignored (so two concurrent first-stage runs converge).
284
+ Raises RuntimeError when the task-key entry does not exist."""
285
+ key = task_key(project_id, task_group, task_id)
286
+ with _registry_lock():
287
+ data = _load()
288
+ row = data["tasks"].get(key)
289
+ if row is None:
290
+ raise RuntimeError(
291
+ f"no task-key entry to anchor implementation base: {key}"
292
+ )
293
+ already = row.get("implementation_base_commit")
294
+ if already:
295
+ return already
296
+ row["implementation_base_commit"] = commit
297
+ _save(data)
298
+ return commit
299
+
300
+
301
+ def get_implementation_base(
302
+ project_id: str, task_group: str, task_id: str,
303
+ ) -> Optional[str]:
304
+ """Return the fixed implementation base commit, or None when unset /
305
+ task-key missing."""
306
+ key = task_key(project_id, task_group, task_id)
307
+ with _registry_lock():
308
+ data = _load()
309
+ row = data["tasks"].get(key)
310
+ if row is None:
311
+ return None
312
+ return row.get("implementation_base_commit") or None
313
+ ```
314
+
315
+ - [ ] **Step 4: Run test to verify it passes**
316
+
317
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_worktree_registry.py -v`
318
+ Expected: PASS.
319
+
320
+ - [ ] **Step 5: Commit**
321
+
322
+ ```bash
323
+ cd /Volumes/Workspaces/workspace/projects/Okstra
324
+ git add scripts/okstra_ctl/worktree_registry.py tests/test_okstra_worktree_registry.py
325
+ git commit -m "feat(worktree-registry): idempotent implementation_base_commit anchor"
326
+ ```
327
+
328
+ ---
329
+
330
+ ### Task 4: `compute_worktree_path` / `compute_branch_name` stage 분기
331
+
332
+ **Files:**
333
+ - Modify: `scripts/okstra_ctl/worktree.py:489` (`compute_worktree_path`), `:511` (`compute_branch_name`)
334
+ - Test: `tests/test_okstra_worktree.py`
335
+
336
+ - [ ] **Step 1: Write the failing test**
337
+
338
+ ```python
339
+ # tests/test_okstra_worktree.py 끝에 추가
340
+ def test_compute_worktree_path_appends_stage_segment(monkeypatch, tmp_path):
341
+ monkeypatch.setenv("OKSTRA_HOME", str(tmp_path))
342
+ from okstra_ctl.worktree import compute_worktree_path
343
+ base = compute_worktree_path(
344
+ project_id="proj", task_group_segment="grp", task_id_segment="tid")
345
+ staged = compute_worktree_path(
346
+ project_id="proj", task_group_segment="grp", task_id_segment="tid",
347
+ stage_number=2)
348
+ assert base.name == "tid"
349
+ assert staged.parent.name == "tid"
350
+ assert staged.name == "stage-2"
351
+
352
+
353
+ def test_compute_branch_name_appends_stage_suffix():
354
+ from okstra_ctl.worktree import compute_branch_name
355
+ base = compute_branch_name(work_category="feature", task_id_segment="tid")
356
+ staged = compute_branch_name(
357
+ work_category="feature", task_id_segment="tid", stage_number=3)
358
+ assert base == "feat-tid"
359
+ assert staged == "feat-tid-s3"
360
+ ```
361
+
362
+ - [ ] **Step 2: Run test to verify it fails**
363
+
364
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_worktree.py -k "stage_segment or stage_suffix" -v`
365
+ Expected: FAIL — `compute_worktree_path()`/`compute_branch_name()` got unexpected keyword `stage_number`.
366
+
367
+ - [ ] **Step 3: Write minimal implementation**
368
+
369
+ ```python
370
+ # worktree.py — compute_worktree_path (시그니처 + stage segment)
371
+ def compute_worktree_path(
372
+ *,
373
+ project_id: str,
374
+ task_group_segment: str,
375
+ task_id_segment: str,
376
+ stage_number: Optional[int] = None,
377
+ ) -> Path:
378
+ """Pure path computation. One worktree dir per task-key, or per
379
+ `<task-key>/stage-<N>` when stage_number is given (implementation
380
+ stage isolation). Uses `OKSTRA_HOME` when set (test hook), else
381
+ `~/.okstra`."""
382
+ okstra_home = os.environ.get("OKSTRA_HOME", "").strip()
383
+ base = Path(okstra_home) if okstra_home else (Path.home() / ".okstra")
384
+ path = (
385
+ base / "worktrees"
386
+ / _safe_segment(project_id)
387
+ / _safe_segment(task_group_segment)
388
+ / _safe_segment(task_id_segment)
389
+ )
390
+ if stage_number is not None:
391
+ path = path / f"stage-{stage_number}"
392
+ return path
393
+ ```
394
+
395
+ ```python
396
+ # worktree.py — compute_branch_name (시그니처 + stage suffix)
397
+ def compute_branch_name(
398
+ *,
399
+ work_category: str,
400
+ task_id_segment: str,
401
+ stage_number: Optional[int] = None,
402
+ ) -> str:
403
+ """One branch per task-key, or `<prefix>-<task-id>-s<N>` for an
404
+ implementation stage worktree."""
405
+ name = f"{_work_category_prefix(work_category)}-{_safe_segment(task_id_segment)}"
406
+ if stage_number is not None:
407
+ name = f"{name}-s{stage_number}"
408
+ return name
409
+ ```
410
+
411
+ - [ ] **Step 4: Run test to verify it passes**
412
+
413
+ Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_worktree.py -v`
414
+ Expected: PASS (신규 + 기존; 기존 호출은 `stage_number=None` 으로 동일 경로/브랜치).
415
+
416
+ - [ ] **Step 5: Commit**
417
+
418
+ ```bash
419
+ cd /Volumes/Workspaces/workspace/projects/Okstra
420
+ git add scripts/okstra_ctl/worktree.py tests/test_okstra_worktree.py
421
+ git commit -m "feat(worktree): stage-scoped path and branch computation"
422
+ ```
423
+
424
+ ---
425
+
426
+ ## Self-Review
427
+
428
+ **Spec coverage (P1 범위):**
429
+ - §3.1 worktree 키/브랜치 stage 분기 → Task 4 ✓
430
+ - §3.2 registry stage-key 엔트리 → Task 1·2 ✓
431
+ - §3.2 `implementationBaseCommit` 1회 고정(flock) → Task 3 ✓ (멱등으로 동시 첫 진입 수렴)
432
+ - §2.3 stage-key 중복 예약 거부(런타임 강제) → Task 2 `test_reserve_same_stage_twice_raises` ✓
433
+ - provision/base 계산/run.py 연결/CLI/프로파일 → **P2·P3 범위(본 plan 비포함, 의도된 경계)**
434
+
435
+ **Placeholder scan:** 없음 — 모든 step에 실제 테스트·구현 코드.
436
+
437
+ **Type consistency:** `stage_number` 파라미터명·`stage`/`implementation_base_commit` 필드명·`set_implementation_base`/`get_implementation_base` 함수명이 Task 1→4 전반 일관. `WorktreeEntry(**row)`가 Task 2의 `row["stage"]`와 Task 1의 `stage` 필드, Task 3의 `implementation_base_commit` 필드로 안전.
438
+
439
+ **경계 확인:** P1은 어떤 호출자도 새 함수를 사용하지 않는다(빌딩블록만). 이는 YAGNI 위반이 아니라 **P2가 즉시 소비할 의존**이며, P1 단독으로도 registry/compute 단위 테스트가 green이다.
440
+
441
+ ## 검증 (P1 완료 기준)
442
+
443
+ ```bash
444
+ cd /Volumes/Workspaces/workspace/projects/Okstra
445
+ python3 -m pytest tests/test_okstra_worktree_registry.py tests/test_okstra_worktree.py -v
446
+ # 기대: 신규 + 기존 전부 PASS, 다른 phase 경로/브랜치 회귀 없음
447
+ ```