okstra 0.67.0 → 0.68.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/bin/okstra +7 -0
- package/docs/kr/architecture.md +17 -1
- package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +456 -0
- package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +1408 -0
- package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +1572 -0
- package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +1 -1
- package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +107 -0
- package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +105 -0
- package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +156 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +5 -4
- package/runtime/prompts/profiles/_common-contract.md +6 -6
- package/runtime/prompts/profiles/final-verification.md +3 -2
- package/runtime/prompts/profiles/release-handoff.md +12 -5
- package/runtime/prompts/wizard/prompts.ko.json +1 -1
- package/runtime/python/okstra_ctl/consumers.py +72 -5
- package/runtime/python/okstra_ctl/git_reconcile.py +322 -0
- package/runtime/python/okstra_ctl/handoff.py +348 -0
- package/runtime/python/okstra_ctl/render.py +44 -2
- package/runtime/python/okstra_ctl/run.py +88 -27
- package/runtime/python/okstra_ctl/wizard.py +25 -4
- package/runtime/python/okstra_ctl/worktree.py +10 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +40 -9
- package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
- package/runtime/skills/okstra-convergence/SKILL.md +1 -1
- package/runtime/skills/okstra-report-writer/SKILL.md +2 -2
- package/runtime/skills/okstra-run/SKILL.md +43 -3
- package/runtime/skills/okstra-team-contract/SKILL.md +2 -2
- package/runtime/validators/validate-run.py +49 -9
- package/src/git-reconcile.mjs +31 -0
- package/src/handoff.mjs +30 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 범위: `implementation` phase에서 stage를 **별도 git worktree로 격리**해, 사용자가 수동으로 동시에 띄운 여러 `implementation` run이 안전하게 서로 다른 stage를 진행하도록 한다. `started-exclusion`(A2)을 같은 설계에 통합한다.
|
|
5
5
|
- 비범위
|
|
6
6
|
- **자동 fan-out 없음** — okstra가 ready stage들을 여러 프로세스로 자동 분기하지 않는다. 병렬 트리거는 사용자가 stage별 run을 각각 기동하는 **수동 동시**만 지원한다.
|
|
7
|
-
- **okstra 자동 머지 없음** — stage 브랜치 합류는 사용자 수동 머지(또는 release-handoff 수집)다.
|
|
7
|
+
- **okstra 자동 머지 없음** — stage 브랜치 합류는 사용자 수동 머지(또는 release-handoff 수집)다. (2026-06-10 개정: release-handoff stage-group 모드의 수집 머지는 예외 — [2026-06-10-stage-group-handoff-design.md](2026-06-10-stage-group-handoff-design.md))
|
|
8
8
|
- `implementation` 외 phase(`requirements-discovery` / `error-analysis` / `implementation-planning` / `final-verification` / `release-handoff`)의 worktree 모델은 불변 — 기존 task-key worktree 1개 유지.
|
|
9
9
|
- ADR↔gitignore(C1)는 별도 plan. 다국어/i18n.
|
|
10
10
|
- 관계: [`2026-05-20-implementation-planning-multi-stage-design.md`](2026-05-20-implementation-planning-multi-stage-design.md)의 stage 개념·carry-in 모델 위에 선다. [`2026-06-04-stage-run-batching.md`](../plans/2026-06-04-stage-run-batching.md)가 known gap으로 남긴 **started-exclusion 미구현**을 본 설계가 해소한다. [`2026-06-04-stage-splitting-cost-aware-design.md`](2026-06-04-stage-splitting-cost-aware-design.md)의 "병렬=부수효과, run batch=비용 단위" 원칙과 양립한다(본 설계는 부수효과인 병렬을 **안전하게** 만들 뿐, 분할 기준을 바꾸지 않는다).
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# 동시-run team 가드: 후발 run no-team background 회피 설계
|
|
2
|
+
|
|
3
|
+
- 작성일: 2026-06-10
|
|
4
|
+
- 상태: 설계 승인됨 (구현 전, 실측 게이트 미통과)
|
|
5
|
+
- 범위: 같은 task-key 의 implementation run 이 동시에 돌 때 발생하는 `~/.claude/teams/` team config race 의 **예방**. 이미 발생한 race 의 복구는 범위 밖(§6).
|
|
6
|
+
|
|
7
|
+
## 1. 문제 (런타임으로 관찰)
|
|
8
|
+
|
|
9
|
+
사용자가 같은 task-key(`fontradar-v2-api:calcule-des-prix-1-1:dev-9185`)에 대해 두 Claude Code 세션을 동시에 돌렸다. 세션 A 는 implementation stage-4 의 Claude verifier 를 dispatch 하려 했고, 세션 B 는 stage-5 를 동시에 실행 중이었다. 결과:
|
|
10
|
+
|
|
11
|
+
| 관찰 | 내용 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| dispatch 실패 | stage-4 verifier 의 `Agent(... team_name ...)` 가 `Team config file unreadable (lock acquired, read failed)` 로 거부 |
|
|
14
|
+
| team config 소실 | stage-4 team 의 `~/.claude/teams/<team>/config.json` 이 사라짐 |
|
|
15
|
+
| 다른 team 정상 | `okstra-…-dev-9185-s5` team 은 정상 존재 (config.json 15K, 20:27 생성) |
|
|
16
|
+
| 코드는 무사 | stage-4 의 코드/worktree 는 intact, real-DB PASS 완료 — **데이터 손실 없음, dispatch 만 막힘** |
|
|
17
|
+
|
|
18
|
+
즉 stage-4·stage-5 는 코드상 독립(둘 다 deps 1,2,6)이라 worktree/코드 충돌은 없었고, **공유 상태(`~/.claude/teams/` team config)에서만 race** 가 났다.
|
|
19
|
+
|
|
20
|
+
## 2. 근본 원인 (코드로 확정)
|
|
21
|
+
|
|
22
|
+
1. **team 생성/삭제는 lead AI 의 하네스 도구 호출이라 okstra 의 flock 밖이다.** `prepare_task_bundle` 은 launch prompt 에 team 이름과 "Team Creation Gate (BLOCKING)" 블록만 렌더하고([scripts/okstra_ctl/render.py:1665](../../../scripts/okstra_ctl/render.py)) 즉시 반환한다. 실제 `TeamCreate`/`TeamDelete` 는 lead 가 Phase 3·7 에서 직접 호출한다([agents/SKILL.md:212](../../../agents/SKILL.md), [prompts/profiles/_common-contract.md:60](../../../prompts/profiles/_common-contract.md)). lead 가 team 을 들고 도는 수십 분간 okstra 의 Python 프로세스는 이미 종료돼 있어, TeamCreate~TeamDelete 임계구역을 flock 으로 감쌀 수단이 없다.
|
|
23
|
+
|
|
24
|
+
2. **worktree/registry 레이어는 stage-key 까지 격리되지만 team 레이어는 그 격리 밖이다.** worktree 프로비저닝은 task-key flock([scripts/okstra_ctl/locks.py:30](../../../scripts/okstra_ctl/locks.py) `worktree_provision_mutex`)으로, stage 예약은 registry flock([scripts/okstra_ctl/worktree_registry.py:264](../../../scripts/okstra_ctl/worktree_registry.py) `list_active_stage_numbers`)으로 직렬화된다. team_name 도 stage 별로 분리된다(`okstra-<task-key>-s<N>`, [scripts/okstra_ctl/render.py:1659](../../../scripts/okstra_ctl/render.py)). 그러나 이 어떤 okstra flock 도 team 생성/삭제를 감싸지 않는다.
|
|
25
|
+
|
|
26
|
+
3. **`Team config file unreadable (lock acquired, read failed)` 문자열은 okstra 레포 어디에도 없다**(grep 전수 확인). 이는 Claude Code 하네스의 team config I/O 계층이 내는 메시지이며, team config 파일의 atomicity 는 okstra 가 통제할 수 없다([scripts/okstra_ctl/team_reconcile.py:17](../../../scripts/okstra_ctl/team_reconcile.py) 주석: "harness behaviour that can only be confirmed inside a real run").
|
|
27
|
+
|
|
28
|
+
종합: 두 동시 run 이 같은 `~/.claude/teams/` 부모를 동시에 건드린 것이 race 의 원천이다. team 이름이 stage 별로 달라도(파일 경로는 안 겹쳐도) 하네스의 부모-디렉토리 단위 동작이 한쪽 team config 를 관측 불가능하게 만들 수 있다.
|
|
29
|
+
|
|
30
|
+
## 3. 검토한 대안과 채택 근거
|
|
31
|
+
|
|
32
|
+
| 방향 | stage 병렬 보존 | enforce 지점 | race 회피 | 기각/채택 |
|
|
33
|
+
|---|---|---|---|---|
|
|
34
|
+
| A. lead-driven advisory lock (TeamCreate~TeamDelete 구간 직렬화) | ✅ | lead 프롬프트(협조 의존) | △ 부분 | 기각 |
|
|
35
|
+
| B. 같은 task-key 동시 run 전체 직렬화 (한 번에 하나) | ❌ | Python | ✅ | 기각 |
|
|
36
|
+
| **C. 진입 가드 + 후발 no-team background 회피** (채택) | ✅ | Python 렌더(확정) | ✅ | 채택 |
|
|
37
|
+
|
|
38
|
+
- **A 기각**: flock 은 호출 프로세스 생존 중에만 유효한데 team 작업은 lead 행위라 PID-liveness 기반 advisory lock 으로만 구현된다. "선언만 하고 강제는 못 하는" 상태(CLAUDE.md "declaration vs enforcement" 위반 위험)이고, lead 가 acquire 를 빼먹으면 무효다.
|
|
39
|
+
- **B 기각**: stage 병렬성은 명시적 설계 기능이다([CLAUDE.md](../../../CLAUDE.md) "two concurrent implementation runs safely pick different ready stages — this is how stage parallelism works"). 전체 직렬화는 이 기능을 죽인다.
|
|
40
|
+
- **C 채택 근거 (핵심)**: 후발 run 이 team 을 **아예 만들지 않고** `run_in_background: true` + no `team_name` 으로 worker 를 dispatch 하면 `~/.claude/teams/` 를 건드리지 않아 race 가 원천 차단된다. 이 no-team 경로는 **okstra 에 이미 존재하는 Phase 5 fallback**([agents/SKILL.md:219](../../../agents/SKILL.md), [agents/SKILL.md:225](../../../agents/SKILL.md))이고, worker 완료는 team 유무와 무관하게 **result 파일 출현 polling** 으로 감지되므로([agents/SKILL.md:233](../../../agents/SKILL.md)) 분석 산출물은 동등하다. 잃는 것은 Teams split-pane 관찰성(FleetView)뿐이다. enforce 지점이 Python 렌더라 lead 협조에 기대지 않는다.
|
|
41
|
+
|
|
42
|
+
## 4. 설계
|
|
43
|
+
|
|
44
|
+
### 4.1 감지 (새 추적 없음)
|
|
45
|
+
|
|
46
|
+
`prepare_task_bundle` 은 implementation stage 선택 시 이미 `list_active_stage_numbers` 를 호출한다([scripts/okstra_ctl/run.py:1342](../../../scripts/okstra_ctl/run.py)). 이 호출은 task-key mutex 안에서 일어나고([scripts/okstra_ctl/run.py:1798](../../../scripts/okstra_ctl/run.py) `worktree_provision_mutex` → [run.py:1825](../../../scripts/okstra_ctl/run.py) stage 선택), 예약이 직렬화되므로 **후발 run 은 반드시 선발의 active stage 를 본다**(둘 다 못 보는 경우 없음).
|
|
47
|
+
|
|
48
|
+
판정: 선택된 stage 를 제외한 `reserved_stages` 가 비어있지 않으면 **동시-run**.
|
|
49
|
+
|
|
50
|
+
> 비대칭(한계): 선발 run 은 예약 시점에 후발이 아직 없어 후발을 영영 못 본다. 그러나 채택안에서는 **후발이 양보(no-team)** 하므로 선발의 team 이 보호된다 — 비대칭이 안전성을 깨지 않는다.
|
|
51
|
+
|
|
52
|
+
### 4.2 안전 기본값 — 동시-run 이면 no-team background 로 렌더
|
|
53
|
+
|
|
54
|
+
동시-run 감지 시 `prepare_task_bundle` 은 기본적으로 no-team 경로로 bundle 을 렌더한다:
|
|
55
|
+
|
|
56
|
+
- `render._build_team_creation_gate`(현 [render.py:1640](../../../scripts/okstra_ctl/render.py) 분기)에 동시-run 분기를 추가한다. "Team Creation Gate (BLOCKING)" 대신 **"Concurrent-run: no-team background" 블록**을 emit 한다. 이 블록은:
|
|
57
|
+
- lead 에게 `TeamCreate` 를 **시도하지 말고** 곧장 Phase 5(no-`team_name`, `run_in_background: true`)로 가라고 지시한다.
|
|
58
|
+
- team-state 에 `teamCreate: { attempted: false, status: "skipped", reason: "concurrent-run", concurrentStages: [<stages>] }` 를 **사전 기록**하라고 지시한다.
|
|
59
|
+
- `PrepareOutputs.extras`([scripts/okstra_ctl/run.py:309](../../../scripts/okstra_ctl/run.py))에 `concurrent_run: { detected: true, active_stages: [...] }` 를 담는다 — 대화형 분기(§4.3)와 비대화형 로그(§4.4)가 읽는다.
|
|
60
|
+
|
|
61
|
+
### 4.3 대화형 분기 (okstra-run 스킬)
|
|
62
|
+
|
|
63
|
+
스킬은 `extras.concurrent_run.detected` 가 참이면, conformance-waiver picker 와 **동형의 자체 3-옵션 recommendation picker**([skills/okstra-run/SKILL.md:192](../../../skills/okstra-run/SKILL.md))를 띄운다:
|
|
64
|
+
|
|
65
|
+
1. **(추천) 이대로 no-team background 로 진행** — 이미 §4.2 로 렌더된 bundle 을 그대로 사용. race 회피, Teams 관찰성만 포기.
|
|
66
|
+
2. **대기** — 지금 dispatch 를 보류한다. stage worktree·run-context 는 보존되고(registry release 는 worktree dir 을 보존한다 — [worktree_registry.py:283](../../../scripts/okstra_ctl/worktree_registry.py)) stage-key 예약은 이 stage 를 "내가 나중에 한다"로 유지한다. 다른 run 종료 후 **resume 명령으로 재개**(그때는 동시-run 이 아니므로 정상 team 경로). 스킬은 resume 명령을 산출물에 명시한다(메모리: next-step 내장).
|
|
67
|
+
3. **직접 입력** (마지막 옵션 고정 — 메모리: okstra run 입력은 항상 "직접 입력"으로 끝난다).
|
|
68
|
+
|
|
69
|
+
> 이 picker 는 스킬이 author 하는 것이라 run-prompt recommendation 규칙(1–2 추천 + 직접 입력)을 따른다 — wizard 가 emit 하는 `options[]` 가 아니므로 [skills/okstra-run/SKILL.md:52](../../../skills/okstra-run/SKILL.md) 의 "wizard option 을 줄이지 말라" 제약과 충돌하지 않는다.
|
|
70
|
+
|
|
71
|
+
### 4.4 비대화형 분기 (okstra.sh / node CLI)
|
|
72
|
+
|
|
73
|
+
사용자 상호작용이 불가하므로 **§4.2 안전 기본값(no-team background)으로 자동 진행** 하고, `extras.concurrent_run` 을 경고로 로그한다(`PROGRESS:` 또는 stderr). race 를 회피하면서 run 을 멈추지 않는다.
|
|
74
|
+
|
|
75
|
+
### 4.5 단일 수렴점
|
|
76
|
+
|
|
77
|
+
감지·렌더 결정은 `prepare_task_bundle` 안에서 한 번만 한다. 세 진입점(okstra-run 스킬 / okstra.sh / node CLI)은 모두 이 함수로 수렴하므로([CLAUDE.md](../../../CLAUDE.md) "All three must converge on `prepare_task_bundle()`"), 분기 UI(대화형 picker vs 비대화형 로그)만 진입점별로 다르고 결정 로직은 공유된다.
|
|
78
|
+
|
|
79
|
+
### 4.6 no-team fallback 의 새 legal 사유
|
|
80
|
+
|
|
81
|
+
현재 no-`team_name` fallback 은 "team-state `teamCreate.status == "error"` 일 때만 legal" 이다([agents/SKILL.md:231](../../../agents/SKILL.md)). 본 설계는 **`status == "skipped"` + `reason == "concurrent-run"`** 을 새 legal 사유로 추가한다. 수정 대상:
|
|
82
|
+
|
|
83
|
+
- [agents/SKILL.md:231](../../../agents/SKILL.md) — legal 사유에 concurrent-run skipped 추가.
|
|
84
|
+
- [prompts/profiles/_common-contract.md:53](../../../prompts/profiles/_common-contract.md) — Phase 7 teardown 의 "no-`team_name` fallback 에는 삭제할 team 이 없다" 조건이 skipped 에도 적용됨을 명시(이미 team 이 없으므로 silent-skip).
|
|
85
|
+
|
|
86
|
+
## 5. 구현 단위
|
|
87
|
+
|
|
88
|
+
| 단위 | 파일 | 변경 |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| 감지 플래그 | [scripts/okstra_ctl/run.py](../../../scripts/okstra_ctl/run.py) | stage 선택 결과에서 `reserved_stages\{선택}` 를 `extras.concurrent_run` 으로 전파 |
|
|
91
|
+
| 렌더 분기 | [scripts/okstra_ctl/render.py:1640](../../../scripts/okstra_ctl/render.py) | 동시-run 시 no-team 블록 + 사전 team-state 지시 emit |
|
|
92
|
+
| 대화형 picker | [skills/okstra-run/SKILL.md](../../../skills/okstra-run/SKILL.md) | `extras.concurrent_run` 분기 → 3-옵션 picker |
|
|
93
|
+
| no-team legal | [agents/SKILL.md:231](../../../agents/SKILL.md), [prompts/profiles/_common-contract.md:53](../../../prompts/profiles/_common-contract.md) | concurrent-run skipped 를 legal 사유로 |
|
|
94
|
+
| 빌드 | `npm run build` | 위 소스를 `runtime/` 로 동기화(직접 편집 금지) |
|
|
95
|
+
|
|
96
|
+
## 6. 한계 (명시)
|
|
97
|
+
|
|
98
|
+
- **후발이 대기·background 를 거부하고 team 생성을 강행** 하면 race 창은 다시 열린다. 진입 가드는 advisory 경계까지가 한계다(임계구역 lock 을 안 쓰기로 한 §3-A 의 귀결).
|
|
99
|
+
- **이미 발생한 race 의 복구**(config.json 소실 상태)는 본 설계 범위 밖이다. team_reconcile / 재생성은 별도 작업으로 다룬다.
|
|
100
|
+
- **미검증**: no-team background 가 dispatch→완료→convergence→리포트 전체 사이클을 끝까지 도는 것은 실제 run 으로만 최종 확인된다. 현재는 정적 설계 수준이다(화면에서 해당 dispatch 경로 자체는 관찰됨).
|
|
101
|
+
|
|
102
|
+
## 7. 테스트 계획
|
|
103
|
+
|
|
104
|
+
- **단위**: `list_active_stage_numbers` 가 비어있지 않을 때 `prepare_task_bundle` 결과의 `extras.concurrent_run.detected` 가 참이 되는지([tests/](../../../tests/)).
|
|
105
|
+
- **단위**: 동시-run 일 때 render 가 Team Creation Gate 대신 no-team 블록을 emit 하고 사전 team-state 지시(`status: "skipped", reason: "concurrent-run"`)를 포함하는지.
|
|
106
|
+
- **단위**: 동시-run 이 아닐 때(=`reserved_stages` 가 선택 stage 뿐) 기존 Team Creation Gate 가 그대로 나오는지(회귀 방지).
|
|
107
|
+
- **e2e**: 같은 task-key 두 implementation run 을 직렬로 시뮬레이션(첫 run 의 stage-key 예약을 active 로 둔 채 둘째 run prepare)해 둘째가 no-team 경로로 가는지([tests-e2e/](../../../tests-e2e/) scenario 추가).
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# git-reconcile — stale SHA 회복 3단 방어 — 설계
|
|
2
|
+
|
|
3
|
+
- 작성일: 2026-06-10
|
|
4
|
+
- 범위: okstra 밖에서 일어난 git 히스토리 변경(PR 리뷰 amend, rebase, squash merge, 외부 커밋, branch 삭제) 이후 `implementation` run 을 손편집 없이 이어가게 한다. 저장된 commit SHA 가 stale 해졌을 때의 **감지 → 내용 기반 자동 화해 → 확인 보정** 경로를 추가한다.
|
|
5
|
+
- 비범위
|
|
6
|
+
- **SHA → branch 명 저장 전환 없음** — SHA 는 계속 기록의 정본(불변 스냅샷)이다. branch 는 사람이 의도를 다시 알려줄 때의 *입력 수단*일 뿐이다. branch tip 은 가변이라 anchor 고정·done 스냅샷·ancestor 게이트의 불변식과 양립하지 않고, squash merge 후에는 branch tip 도 ancestor 가 아니므로 사용자 페인을 해결하지 못한다.
|
|
7
|
+
- okstra 가 머지를 대행하지 않는다 — 보정은 *기록*(`consumers.jsonl` / registry)의 화해이지 git 조작이 아니다.
|
|
8
|
+
- `implementation` 외 phase 의 task-key worktree 모델 불변.
|
|
9
|
+
- 관계: [`2026-06-06-stage-worktree-isolation-design.md`](2026-06-06-stage-worktree-isolation-design.md)의 base 결정 규칙(§2.2)·anchor 1회 고정(§3.2) 위에 선다. 그 설계가 도입한 ancestor 게이트의 known gap — 히스토리 재작성 시 회복 경로 부재 — 를 본 설계가 해소한다. [`scripts/okstra_ctl/reconcile.py`](../../../scripts/okstra_ctl/reconcile.py)는 run 상태(recent.jsonl) 보정 전용으로 별개 개념이며 건드리지 않는다.
|
|
10
|
+
|
|
11
|
+
## 1. 동기
|
|
12
|
+
|
|
13
|
+
stage 격리 설계는 base 를 SHA 로 고정한다: 독립 stage = anchor([`set_implementation_base`](../../../scripts/okstra_ctl/worktree_registry.py:199), 1회 고정 후 무시), 단일 의존 = 선행 done `head_commit`([run.py:526](../../../scripts/okstra_ctl/run.py:526)), 다중 의존 = `git merge-base --is-ancestor` 검증([run.py:454](../../../scripts/okstra_ctl/run.py:454), [run.py:506](../../../scripts/okstra_ctl/run.py:506)). 이 SHA 들이 okstra 밖의 작업으로 stale 해지면:
|
|
14
|
+
|
|
15
|
+
1. **rebase / squash merge** — 커밋 ID 가 바뀌어 저장된 SHA 가 candidate 의 ancestor 가 아니게 된다. 내용은 머지됐는데도 `PrepareError`("not merged")로 거부된다.
|
|
16
|
+
2. **PR 리뷰 반영 amend** — 내용까지 바뀐 새 커밋. 저장된 done SHA 는 검증 시점의 코드를 가리키지만 사용자 의도는 리뷰 반영본이다.
|
|
17
|
+
3. **SHA 소멸** — branch 삭제 + GC 후 [`_resolve_commit_sha`](../../../scripts/okstra_ctl/worktree.py:260) 실패 → [worktree.py:781](../../../scripts/okstra_ctl/worktree.py:781) 에서 거부.
|
|
18
|
+
|
|
19
|
+
현재 세 경우 모두 회복 경로가 없다. 사용자가 할 수 있는 것은 `~/.okstra/worktrees/registry.json` / `consumers.jsonl` 손편집뿐이다.
|
|
20
|
+
|
|
21
|
+
## 2. 핵심 원칙
|
|
22
|
+
|
|
23
|
+
1. **SHA = 정본, branch = 입력 수단.** 저장 포맷은 바꾸지 않는다. stale 시 branch(또는 사용자 지정 ref)에서 SHA 를 *다시 캡처*해 새 기록으로 append 한다.
|
|
24
|
+
2. **자동화 수위 (사용자 결정 2026-06-10): 내용 동일 = 자동, 내용 변경 = 확인.** patch-id 로 내용 동일성이 *증명*되는 경우(rebase / cherry-pick / 깨끗한 squash)만 확인 없이 자동 통과·자동 재기록하고 로그를 남긴다. 내용이 바뀐 경우(리뷰 반영 amend 등)는 반드시 사용자 확인(picker)을 거친다. 증명 없는 자동 진행은 금지 — 검증 안 된 커밋이 base 에 섞이는 것을 차단한다.
|
|
25
|
+
3. **append-only 유지.** 보정은 `consumers.jsonl` 에 새 done row 를 append 하는 방식이다(기존 row 수정/삭제 없음). 이를 위해 done-row 읽기를 last-wins 로 통일한다(§3.4 — 선행 조건).
|
|
26
|
+
4. **단일 reference point.** 감지·화해·보정 로직은 Python 모듈 1곳(`git_reconcile.py`)에 두고, prepare 경로·`git-reconcile` subcommand·skill picker 는 모두 그것을 소비한다.
|
|
27
|
+
|
|
28
|
+
## 3. 구성 요소
|
|
29
|
+
|
|
30
|
+
### 3.1 content-equivalence 검사기 — `scripts/okstra_ctl/git_reconcile.py` (신규)
|
|
31
|
+
|
|
32
|
+
`content_merged(project_root, commit, candidate, base) -> MatchResult`
|
|
33
|
+
|
|
34
|
+
1. `git merge-base --is-ancestor commit candidate` 성공 → 기존대로 통과(`ancestor`).
|
|
35
|
+
2. 실패 시 patch-id fallback, 두 granularity:
|
|
36
|
+
- **커밋 단위**: `git show <commit> | git patch-id --stable` 을 `merge-base(commit, candidate)..candidate` 구간 각 커밋의 patch-id 와 비교 — rebase / cherry-pick 커버.
|
|
37
|
+
- **범위 합산**: `git diff <base> <commit> | git patch-id --stable` (stage 작업 전체를 한 diff 로) 을 candidate 구간 커밋들의 patch-id 와 비교 — N 커밋이 1 커밋으로 합쳐진 squash merge 커버. `base` 는 해당 stage 의 base 기록(anchor 또는 선행 done SHA).
|
|
38
|
+
3. 일치 시 candidate 히스토리에서 매칭된 커밋 SHA 를 반환(`patch-equivalent`, 재기록에 사용). 불일치 → `not-merged`.
|
|
39
|
+
|
|
40
|
+
`commit` 자체가 unresolvable 이면 patch-id 를 계산할 수 없으므로 무조건 confirm 급이다(§3.2).
|
|
41
|
+
|
|
42
|
+
### 3.2 stale 분류기 — 같은 모듈
|
|
43
|
+
|
|
44
|
+
`classify(project_root, plan_run_root, task_key) -> StaleReport(JSON 직렬화 가능)`
|
|
45
|
+
|
|
46
|
+
task 단위로 다음을 점검하고 항목별 `auto` / `confirm` / `ok` 를 매긴다:
|
|
47
|
+
|
|
48
|
+
| 점검 | stale 조건 | 분류 |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| anchor (`implementation_base_commit`) | unresolvable | `confirm` (재고정 필요) |
|
|
51
|
+
| 각 stage 의 최신 done `head_commit` | unresolvable | `confirm` |
|
|
52
|
+
| 〃 | resolvable 이지만 해당 stage branch(`-s<N>`) tip 과 불일치 + patch-id 동등 | `auto` (tip 으로 재기록) |
|
|
53
|
+
| 〃 | 〃 + patch-id 불일치 (내용 변경) | `confirm` |
|
|
54
|
+
| 다중 의존 ancestor 게이트 | ancestor 아님 + `content_merged`=`patch-equivalent` | `auto` (매칭 SHA 로 재기록) |
|
|
55
|
+
| 〃 | ancestor 아님 + `not-merged` | `confirm` |
|
|
56
|
+
|
|
57
|
+
stage branch 가 이미 삭제된 경우 tip 비교는 건너뛰고 candidate(task-key worktree HEAD) 대상 `content_merged` 만 본다.
|
|
58
|
+
|
|
59
|
+
### 3.3 보정 명령 — `git-reconcile` subcommand
|
|
60
|
+
|
|
61
|
+
- Python 구현 1곳: `git_reconcile.py` 의 `check()` / `apply()`. Node CLI 는 `bin/okstra` 서브커맨드 표에 `git-reconcile` 추가 + `src/git-reconcile.mjs` 가 [`_python-helper.mjs`](../../../src/_python-helper.mjs)로 패스스루(기존 `migrate` 패턴). `okstra.sh` 는 run 런처(flag 기반)라 ops subcommand dispatch 가 없으므로 배선하지 않는다 — ops 명령은 node CLI 전용(기존 `migrate` 와 동일).
|
|
62
|
+
- `--check`: §3.2 리포트를 JSON 으로 출력 (기본 indent — 사람 가독, `--json` 시 compact 단일 라인 — 기계 파싱). confirm 항목 잔존 시 exit 2.
|
|
63
|
+
- `--apply`: `auto` 항목 일괄 보정. `confirm` 항목은 `--stage <N> --use-ref <branch|sha>` 로 개별 보정(ref 는 즉시 SHA 로 resolve 해 기록), `--reset-anchor <ref>` 로 anchor 재고정.
|
|
64
|
+
- 보정 = `consumers.jsonl` 에 `status="done"` row 재-append. 추가 필드: `reconciled: true`, `reconcile_reason: "<auto-patch-id|user-ref>"`, `replaced_commit: "<old sha>"`. [`append_consumer`](../../../scripts/okstra_ctl/consumers.py:36)에 optional 필드 전달을 허용한다.
|
|
65
|
+
|
|
66
|
+
### 3.4 done-row 읽기 last-wins 통일 (선행 조건)
|
|
67
|
+
|
|
68
|
+
현재 읽기 의미가 갈라져 있다: [run.py:489](../../../scripts/okstra_ctl/run.py:489)·[run.py:527](../../../scripts/okstra_ctl/run.py:527)은 첫 행 우선, [run.py:556](../../../scripts/okstra_ctl/run.py:556)(`done_by_stage` dict)은 마지막 행 우선. 보정 row 가 효력을 가지려면 last-wins 가 유일한 의미여야 한다.
|
|
69
|
+
|
|
70
|
+
- [`consumers.py`](../../../scripts/okstra_ctl/consumers.py)에 `latest_done_by_stage(rows) -> Dict[int, row]` helper 신설.
|
|
71
|
+
- 소비 지점 전부를 helper 로 수렴: [run.py:489](../../../scripts/okstra_ctl/run.py:489), [run.py:527](../../../scripts/okstra_ctl/run.py:527), [run.py:556](../../../scripts/okstra_ctl/run.py:556), [run.py:1467](../../../scripts/okstra_ctl/run.py:1467) 경로. ([`backfill_done_from_carry`](../../../scripts/okstra_ctl/consumers.py:115)의 `done_stages` set 은 존재 여부만 보므로 영향 없음.)
|
|
72
|
+
|
|
73
|
+
### 3.5 anchor 재고정
|
|
74
|
+
|
|
75
|
+
[`set_implementation_base`](../../../scripts/okstra_ctl/worktree_registry.py:199)의 "이미 있으면 무시" 규칙(동시 first-stage 수렴용)은 유지. `worktree_registry.py` 에 `reset_implementation_base(..., commit)` 를 추가하되 **호출자는 §3.3 `--reset-anchor` 하나뿐**이다 — prepare 경로가 자동으로 anchor 를 움직이는 일은 없다.
|
|
76
|
+
|
|
77
|
+
### 3.6 prepare / skill 배선
|
|
78
|
+
|
|
79
|
+
- **prepare 경로 자동 화해**: [`_resolve_stage_base_commit`](../../../scripts/okstra_ctl/run.py:465)과 final-verification 의 merged 검사([`_is_ancestor`](../../../scripts/okstra_ctl/run.py:1439), [run.py:1488](../../../scripts/okstra_ctl/run.py:1488))가 ancestor 실패 시 `content_merged` 를 호출한다. `patch-equivalent` 면 통과 + 보정 row 자동 append(다음 run 부터는 ancestor 검사로 바로 통과). `not-merged`/unresolvable 이면 `PrepareError` 메시지에 stale 리포트 요약과 `okstra git-reconcile --check` / `--apply` 명령 예시를 포함한다.
|
|
80
|
+
- **skill picker**: `skills/okstra-run/SKILL.md` 에 단계 추가 — prepare 가 위 안내를 담은 `PrepareError` 로 실패하면 `git-reconcile --check --json` 을 실행해 confirm 항목별 picker 를 제시한다. 옵션 구성은 추천 + 직접 입력 규칙을 따른다: ① "stage branch 현재 tip 으로 재기록 (추천)" ② "다른 ref 직접 입력" ③ "중단". 선택 시 `--apply --stage N --use-ref <ref>` 실행 후 prepare 재시도.
|
|
81
|
+
|
|
82
|
+
> declaration ↔ enforcement: "내용 변경은 확인 필수" 의 강제 지점은 **`apply()` 런타임 가드**다 — `confirm` 분류 항목은 `--use-ref` 없이 절대 보정되지 않으며, 이를 검증하는 pytest 가 박제한다. 프로파일/스킬 문구만으로 끝내지 않는다.
|
|
83
|
+
|
|
84
|
+
## 4. 시나리오별 동작
|
|
85
|
+
|
|
86
|
+
| okstra 밖에서 일어난 일 | 동작 |
|
|
87
|
+
|---|---|
|
|
88
|
+
| stage branch rebase (내용 동일) | patch-id 증명 → 자동 통과 + tip SHA 재기록 |
|
|
89
|
+
| squash merge 후 branch 삭제 | 범위 patch-id 일치 → 자동 통과 + squash SHA 재기록 |
|
|
90
|
+
| PR 리뷰 반영 amend (내용 변경) | `confirm` → picker → 사용자 ref 로 재기록 후 진행 |
|
|
91
|
+
| done SHA GC 로 소멸 | `confirm` → picker (ref 직접 입력) |
|
|
92
|
+
| anchor unresolvable | `confirm` → `--reset-anchor` 안내 |
|
|
93
|
+
| 외부 커밋이 단순히 쌓임 (재작성 없음) | stale 아님 — 기존 동작 그대로 |
|
|
94
|
+
|
|
95
|
+
## 5. 테스트
|
|
96
|
+
|
|
97
|
+
- **pytest (실 git)**: `mktemp` git repo 에서 rebase / cherry-pick / squash merge / amend / branch 삭제를 실제로 만들어 `content_merged` 와 `classify` 의 분류를 검증. mock 금지 — patch-id·merge-base 는 실제 git 실행으로만 검증된다.
|
|
98
|
+
- **last-wins 회귀**: 같은 stage 에 done row 2개(원본 + reconciled)를 append 한 뒤 base 해석·whole-task 게이트가 마지막 row 를 쓰는지 검증.
|
|
99
|
+
- **enforcement 테스트**: `confirm` 항목이 `--use-ref` 없이 `apply()` 로 보정되지 않음을 박제.
|
|
100
|
+
- **e2e**: `tests-e2e/scenario-<id>-git-reconcile-squash.sh` — stage1 done → squash merge → branch 삭제 → stage2 prepare 가 자동 통과하는 흐름.
|
|
101
|
+
|
|
102
|
+
## 6. 미해결 / 후속
|
|
103
|
+
|
|
104
|
+
- 다중 stage 가 섞인 octopus 머지·부분 머지(stage diff 의 일부만 반영)는 patch-id 로 증명 불가 → `confirm` 으로 떨어진다. 의도된 보수성.
|
|
105
|
+
- `--apply` 의 wizard(okstra.sh 대화형) 통합은 skill picker 가 안정된 뒤 별도 plan.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# release-handoff stage-group 모드 — 설계
|
|
2
|
+
|
|
3
|
+
- 작성일: 2026-06-10
|
|
4
|
+
- 상태: 설계 승인됨 (사용자 승인 2026-06-10)
|
|
5
|
+
- 선행: [stage-worktree-isolation-design.md](2026-06-06-stage-worktree-isolation-design.md), [final-verification-whole-task-gate-design.md](2026-06-06-final-verification-whole-task-gate-design.md)
|
|
6
|
+
|
|
7
|
+
## 1. 배경 / 문제
|
|
8
|
+
|
|
9
|
+
stage-worktree-isolation 모델에서 implementation 은 **1 run = 1 stage** 이고 stage 마다 격리 브랜치(`<prefix>-<task-id>-s<N>`)가 생긴다. 정식 release 경로는 "모든 stage 를 task-key 브랜치에 수동 머지 → whole-task final-verification `accepted` → release-handoff 로 task 당 PR 1개" 다 ([final-verification.md:40](../../../prompts/profiles/final-verification.md:40)).
|
|
10
|
+
|
|
11
|
+
문제: stage 단위가 작을 때 task 전체가 끝나기를 기다렸다 PR 하나로 내거나, 반대로 stage 하나짜리 PR 을 여러 번 내는 것 둘 다 비효율이다. 사용자는 **한 task 안에서 stage 일부를 골라 묶어 하나의 PR** 로 내고 싶다 (예: stage 2+3 → PR 1, stage 4+5 → PR 2).
|
|
12
|
+
|
|
13
|
+
현재 계약이 이를 막는 지점 세 곳:
|
|
14
|
+
|
|
15
|
+
1. release-handoff 진입 게이트가 whole-task `accepted` 보고서 1개를 요구 ([release-handoff.md:14](../../../prompts/profiles/release-handoff.md:14)).
|
|
16
|
+
2. 단독-stage 검증은 release-handoff 라우팅 금지 ([final-verification.md:29](../../../prompts/profiles/final-verification.md:29), [final-verification.md:40](../../../prompts/profiles/final-verification.md:40)).
|
|
17
|
+
3. release-handoff 는 커밋 생성 금지 ([release-handoff.md:57](../../../prompts/profiles/release-handoff.md:57)) + "okstra 자동 머지 없음" 비목표 ([stage-worktree-isolation-design.md:7](2026-06-06-stage-worktree-isolation-design.md)) — 독립 stage 들은 서로 다른 브랜치에 있어 묶음 PR 의 head 브랜치를 만들 주체가 없다.
|
|
18
|
+
|
|
19
|
+
## 2. 결정 요약 (브레인스토밍 Q&A)
|
|
20
|
+
|
|
21
|
+
| 결정점 | 선택 |
|
|
22
|
+
|---|---|
|
|
23
|
+
| 묶음 단위 | 한 task 안의 **stage 일부 그룹** → PR 1개 |
|
|
24
|
+
| 그룹 결정 시점 | **release-handoff 실행 중** 사용자가 PR 가능 stage 목록에서 선택 |
|
|
25
|
+
| 수집 브랜치(그룹 머지) 소유 | **release-handoff** 가 수집 브랜치 생성 + stage 브랜치 merge |
|
|
26
|
+
| 검증 게이트 | **stage 별 단독-stage `accepted` 합산** — 그룹 내 모든 stage 가 각자 accepted 면 진입 허용. 머지된 조합 자체는 미검증이며 충돌 프로브 + PR CI 가 보완 |
|
|
27
|
+
| 의존 폐포 위반 시 | **차단 + 안내** (선행 자동 포함 아님 — 조용한 범위 확장 방지) |
|
|
28
|
+
|
|
29
|
+
## 3. 목표 / 비목표
|
|
30
|
+
|
|
31
|
+
목표:
|
|
32
|
+
- release-handoff 에 **stage-group 모드** 추가: done + 단독 검증 accepted + 미-PR stage 들을 골라 수집 브랜치로 머지하고 PR 1개 생성.
|
|
33
|
+
- 자격 판정·수집 머지·PR 기록을 Python 헬퍼 한 곳에서 **강제** (선언이 아니라 enforcement).
|
|
34
|
+
- 기존 whole-task 경로는 불변 — stage-group 은 추가 모드다.
|
|
35
|
+
|
|
36
|
+
비목표:
|
|
37
|
+
- 여러 task 를 한 PR 로 묶기 (task-group 단위 PR).
|
|
38
|
+
- 그룹 조합에 대한 별도 final-verification 모드 신설 (게이트는 stage 별 accepted 합산; 머지 후 재검증은 사용자가 원하면 단독으로 수행).
|
|
39
|
+
- PR 머지·릴리스 발행 (기존 비목표 유지 — [release-handoff.md:99](../../../prompts/profiles/release-handoff.md:99)).
|
|
40
|
+
- implementation / planning 의 stage 모델·worktree 모델 변경.
|
|
41
|
+
|
|
42
|
+
## 4. 흐름
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
stage 2 done ── fv --stage 2 accepted ─┐
|
|
46
|
+
stage 3 done ── fv --stage 3 accepted ─┤
|
|
47
|
+
▼
|
|
48
|
+
release-handoff (stage-group 모드)
|
|
49
|
+
G1 PR base 선택 (기존 Q2 와 동일 — 의존 폐포 판정에 필요해 맨 앞으로)
|
|
50
|
+
G2 stage 선택 (PR 가능 목록에서 multi-select)
|
|
51
|
+
assemble: 수집 브랜치 생성 + merge ← okstra_ctl 헬퍼 (강제 지점)
|
|
52
|
+
Q2b 충돌 프로브 (기존)
|
|
53
|
+
Q3 PR 제목/본문 확인 (기존)
|
|
54
|
+
push + gh pr create (head = 수집 브랜치)
|
|
55
|
+
consumers 에 pr 행 기록
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
stage-group 모드에서는 base 선택이 stage 선택보다 **앞선다** — G2 조건 4(의존 폐포)의 ancestor 판정과 assemble 이 `origin/<chosen-base>` 를 필요로 하기 때문이다. whole-task 모드의 기존 Q1→Q2 순서는 불변.
|
|
59
|
+
|
|
60
|
+
### 4.1 진입 게이트 (stage-group 모드)
|
|
61
|
+
|
|
62
|
+
- brief 가 그룹 후보 stage 들의 **단독-stage final-verification 보고서 N 개**를 인용하고, 각각의 `Verdict Token` 이 `accepted` 인지 lead 가 확인한다. whole-task 모드의 단일 보고서 게이트([release-handoff.md:14](../../../prompts/profiles/release-handoff.md:14))와 병렬 분기.
|
|
63
|
+
- 워킹트리 clean / base 브랜치 금지 / 커밋 존재 게이트는 기존 그대로 ([release-handoff.md:16-18](../../../prompts/profiles/release-handoff.md:16)).
|
|
64
|
+
|
|
65
|
+
### 4.2 G2 — stage 선택
|
|
66
|
+
|
|
67
|
+
lead 가 헬퍼(§6)에 G1 에서 고른 base 를 넘겨 **PR 가능 stage 목록**을 받아 multi-select 로 제시한다. PR 가능 조건(전부 헬퍼가 판정):
|
|
68
|
+
|
|
69
|
+
1. consumers `status:done` ([consumers.py:37](../../../scripts/okstra_ctl/consumers.py:37) `latest_done_by_stage`).
|
|
70
|
+
2. 해당 stage 의 단독-stage final-verification `accepted` 기록 존재 (§5.2 `verified` 행).
|
|
71
|
+
3. 아직 어떤 `pr` 행에도 포함되지 않음 (§5.3).
|
|
72
|
+
4. **의존 폐포**: 모든 선행 stage 가 (a) 같은 그룹에 선택되었거나, (b) 이미 PR 머지되어 `origin/<chosen-base>` 의 ancestor (`git merge-base --is-ancestor <선행 done head_commit> origin/<base>`). 위반 시 해당 조합을 **차단**하고 "선행 stage <N> 을 그룹에 포함하거나 먼저 PR-머지 후 재시도" 안내. 근거: 단일-의존 stage 브랜치는 선행 브랜치 위에 적층되므로([stage-worktree-isolation-design.md §2.2](2026-06-06-stage-worktree-isolation-design.md)), 선행을 빼고 후행만 PR 하면 선행 커밋이 묵시적으로 따라간다.
|
|
73
|
+
|
|
74
|
+
### 4.3 assemble — 수집 브랜치 생성 + 머지
|
|
75
|
+
|
|
76
|
+
- base: registry task-key 행의 `implementation_base_commit` ([worktree_registry.py:72](../../../scripts/okstra_ctl/worktree_registry.py:72), [worktree_registry.py:239](../../../scripts/okstra_ctl/worktree_registry.py:239) `get_implementation_base`).
|
|
77
|
+
- 브랜치명: `<work-category-prefix>-<task-id>-g<stage들을 -로 연결>` (예: `feat-dev-9184-g2-3`). 정렬은 stage 번호 오름차순.
|
|
78
|
+
- 워크트리: `~/.okstra/worktrees/<project>/<group>/<task-id>/group-<id>/`, registry 키 `<task-key>#group-<id>` 를 flock 예약 — stage-key 예약과 동일 메커니즘.
|
|
79
|
+
- merge: 선택 stage 브랜치들을 stage 번호 순으로 `git merge` (merge 커밋 허용). **충돌 시 즉시 중단**(`git merge --abort` 후 종료) + 충돌 경로 목록과 "stage 간 충돌 — 수동 해소 또는 그룹 재구성" 안내. rebase / squash / force 금지 유지.
|
|
80
|
+
- 멱등성: 동일 그룹 키의 수집 브랜치가 이미 존재하면 — 그 HEAD 가 동일 stage head 들의 merge 결과(각 stage `done.head_commit` 이 모두 ancestor)일 때만 재사용, 아니면 에러로 중단하고 정리 절차 안내.
|
|
81
|
+
|
|
82
|
+
### 4.4 PR 초안 / 보고서
|
|
83
|
+
|
|
84
|
+
- PR 제목/본문 초안 규칙은 기존과 동일하되([release-handoff.md:44-49](../../../prompts/profiles/release-handoff.md:44)), 소스가 stage 별 implementation 보고서 + 단독 검증 보고서 N 개로 늘어난다. diff 범위는 `implementation_base_commit..<수집 브랜치 HEAD>`.
|
|
85
|
+
- 최종 보고서 deliverable 에 **Stage Group** 섹션 추가: 선택 stage 목록, 각 stage 의 검증 보고서 경로·Verdict Token, 수집 브랜치명, merge 커밋 SHA 목록, 의존 폐포 판정 결과.
|
|
86
|
+
|
|
87
|
+
## 5. 데이터 모델
|
|
88
|
+
|
|
89
|
+
### 5.1 registry — 그룹 키
|
|
90
|
+
|
|
91
|
+
stage-key 와 동형의 그룹 키 엔트리:
|
|
92
|
+
|
|
93
|
+
```jsonc
|
|
94
|
+
"<proj>/<group>/<task-id>#group-g2-3": {
|
|
95
|
+
"branch": "feat-dev-9184-g2-3",
|
|
96
|
+
"worktree_path": ".../<task-id>/group-g2-3",
|
|
97
|
+
"base_ref": "<implementation_base_commit>",
|
|
98
|
+
"stages": [2, 3],
|
|
99
|
+
"status": "active"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 5.2 consumers — `verified` 행 (신규)
|
|
104
|
+
|
|
105
|
+
단독-stage final-verification 이 `accepted` 로 끝나면 plan_run_root 의 consumers.jsonl 에 기록한다:
|
|
106
|
+
|
|
107
|
+
```jsonc
|
|
108
|
+
{"impl_task_key": "...", "stage": 2, "status": "verified", "verdict": "accepted", "report_path": "...", "ts": "..."}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- 현재 [consumers.py:50](../../../scripts/okstra_ctl/consumers.py:50) 의 `append_consumer` 는 `started|done` 만 허용 — 허용 집합을 확장한다.
|
|
112
|
+
- 이 행이 G2 조건 2의 SSOT 다. 보고서 파일을 재파싱하지 않고 consumers 만 읽어 자격을 판정한다.
|
|
113
|
+
|
|
114
|
+
### 5.3 consumers — `pr` 행 (신규)
|
|
115
|
+
|
|
116
|
+
handoff 가 PR 생성(또는 재사용 확인)에 성공하면 기록한다:
|
|
117
|
+
|
|
118
|
+
```jsonc
|
|
119
|
+
{"impl_task_key": "...", "stages": [2, 3], "status": "pr", "branch": "feat-dev-9184-g2-3", "url": "https://github.com/...", "ts": "..."}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- "이미 PR 된 stage" 판정(G2 조건 3)의 SSOT. whole-task handoff 도 동일 행을 기록한다(`stages` = 전체) — 두 경로의 중복-PR 방지가 한 데이터로 수렴.
|
|
123
|
+
|
|
124
|
+
## 6. 강제 지점 — okstra_ctl 헬퍼
|
|
125
|
+
|
|
126
|
+
자격 판정·assemble·기록은 lead 의 자유 git 호출이 아니라 `scripts/okstra_ctl/` 의 헬퍼 함수(예: `handoff_assemble`)와 CLI 서브커맨드로 단일화한다. lead 는 두 번 호출한다:
|
|
127
|
+
|
|
128
|
+
1. `handoff-eligible --base <chosen-base>` — PR 가능 stage 목록 + 각 stage 의 차단 사유(미검증/이미 PR/의존 폐포)를 JSON 으로 반환. G2 의 표시 데이터.
|
|
129
|
+
2. `handoff-assemble --stages 2,3 --base <chosen-base>` — 조건 재검증(fail-fast, [run.py](../../../scripts/okstra_ctl/run.py) 의 `PrepareError` 패턴) → registry 그룹 키 예약 → 수집 워크트리/브랜치 생성 → merge → 결과(브랜치명·HEAD·merge SHA 목록) JSON 반환. 위반·충돌 시 비정상 종료 + actionable 메시지.
|
|
130
|
+
|
|
131
|
+
`pr` 행 기록도 헬퍼 서브커맨드(`handoff-record-pr`)로 수행해 lead 의 수기 JSON append 를 금지한다.
|
|
132
|
+
|
|
133
|
+
> declaration ↔ enforcement: 프로파일의 MUST 문구는 헬퍼가 보장하는 불변식의 서술이고, 검사 책임은 Python 에 있다 — whole-task 게이트 설계 §5 와 동일 원칙 ([final-verification-whole-task-gate-design.md](2026-06-06-final-verification-whole-task-gate-design.md)).
|
|
134
|
+
|
|
135
|
+
## 7. 계약 변경점 (문서/프로파일)
|
|
136
|
+
|
|
137
|
+
| 파일 | 변경 |
|
|
138
|
+
|---|---|
|
|
139
|
+
| [release-handoff.md](../../../prompts/profiles/release-handoff.md) | 진입 게이트에 stage-group 분기(§4.1), G1·G2 신설, allowed actions 에 헬퍼 호출·수집 merge 추가, 커밋 금지 조항을 "수집 브랜치 위 merge 커밋만 허용, 파일 변경 커밋·history rewrite 금지"로 정밀화, deliverable 에 Stage Group 섹션 |
|
|
140
|
+
| [final-verification.md](../../../prompts/profiles/final-verification.md) | line 29·40 의 단독-stage 라우팅 금지를 "단독-stage `accepted` 는 `release-handoff(stage-group)` 라우팅 허용"으로 완화. whole-task 전용 문구는 "단일 task 전체 PR" 한정으로 수정. accepted 시 consumers `verified` 행 기록 의무 추가 |
|
|
141
|
+
| [stage-worktree-isolation-design.md](2026-06-06-stage-worktree-isolation-design.md) | 비목표 "okstra 자동 머지 없음"에 "release-handoff stage-group 수집 머지는 예외(본 설계)" 주석 |
|
|
142
|
+
| [docs/kr/architecture.md](../../kr/architecture.md) | release-handoff 절에 stage-group 모드·consumers `verified`/`pr` 행 계약 반영 |
|
|
143
|
+
|
|
144
|
+
## 8. 테스트
|
|
145
|
+
|
|
146
|
+
- unit (`tests/`):
|
|
147
|
+
- 자격 계산 — done/verified/pr/의존 폐포 각 조건의 포함·제외, ancestor 판정 분기.
|
|
148
|
+
- `append_consumer` 허용 집합 확장 (`verified`, `pr` 행 round-trip).
|
|
149
|
+
- registry 그룹 키 예약 — 동시 예약 직렬화, 기존 stage-key 와 비충돌.
|
|
150
|
+
- assemble — 정상 merge 그래프, 충돌 시 중단·정리, 멱등 재사용 vs 불일치 에러.
|
|
151
|
+
- e2e: `tests-e2e/scenario-<다음 빈 번호>-stage-group-handoff.sh` — 독립 stage 2개 done + verified → eligible 목록 확인 → assemble → 수집 브랜치 커밋 그래프(`git merge-base --is-ancestor` 양방향) 검증 → pr 행 기록 확인. 임시 `OKSTRA_HOME`(`mktemp -d`) + `trap` 정리 관례 준수.
|
|
152
|
+
|
|
153
|
+
## 9. 미해결 / 후속
|
|
154
|
+
|
|
155
|
+
- 수집 워크트리·브랜치의 PR 머지 후 정리는 기존 수동 절차(`git worktree remove` → `branch -D` → registry 키 삭제)를 따른다. 자동 GC 는 후속.
|
|
156
|
+
- 그룹 PR 이 base 에 머지된 뒤 남은 stage 들의 whole-task 검증 base 정합(이미 머지된 stage diff 의 중복 표시)은 whole-task 게이트의 기존 ancestor 검사로 흡수되는지 구현 단계에서 확인한다.
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
package/runtime/agents/SKILL.md
CHANGED
|
@@ -211,8 +211,9 @@ These phases are governed by [okstra-team-contract](./skills/okstra-team-contrac
|
|
|
211
211
|
|
|
212
212
|
`TeamCreate` MUST be the first Agent-related tool call after Phase 2 prompt preparation. Do not call `Agent(... team_name: ...)` for any worker until this phase has executed — the Agent tool rejects `team_name` for non-existent teams with `"team을 먼저 생성하거나 team_name 없이 호출해야 합니다"` / `"team must be created first or call without team_name"`, and silently stripping `team_name` to retry is NOT a valid recovery (it loses the Teams split-pane behavior and is indistinguishable from never having attempted Teams mode).
|
|
213
213
|
|
|
214
|
-
1. Call `TeamCreate(team_name:
|
|
215
|
-
2. Record the `TeamCreate` outcome in team-state under `teamCreate: { attempted: true, status: "ok"|"error", error?: <message> }` before any dispatch. This is the audit trail that justifies a later no-`team_name` fallback.
|
|
214
|
+
1. Call `TeamCreate(team_name: ..., description: "Lead-plus-worker okstra run for <task-key>")`. The team name comes verbatim from the launch prompt's Team Creation Gate block — `okstra-<task-key>`, and implementation stage runs append `-s<N>` (stage isolation: a leftover team from another stage of the same task must not collide).
|
|
215
|
+
2. Record the `TeamCreate` outcome in team-state under `teamCreate: { attempted: true, status: "ok"|"error", error?: <message> }` AND the exact name as `teamName` before any dispatch. This is the audit trail that justifies a later no-`team_name` fallback, and `teamName` is the SSOT every later consumer (teardown, reconcile, token collector) reads.
|
|
216
|
+
2-1. If `TeamCreate` fails with "team already exists" (stale leftover from an earlier attempt): call `TeamList`; if the team is listed in this session, `TeamDelete` it and retry step 1 once. If it is NOT listed, do NOT remove `~/.claude/teams/...` / `~/.claude/tasks/...` with shell commands on your own initiative — that is destructive harness-internal state and `rm -rf` is commonly denied by user permission rules. Ask the user via AskUserQuestion (recommended option: quarantine); on approval, move both dirs into `~/.okstra/trash/<UTC-timestamp>/` with `mv` (reversible), then retry step 1 once.
|
|
216
217
|
3. Verify `team-state.lead.sessionId` is populated. The `okstra.sh` exec path fills it automatically (`generate_claude_session_id` → `claude --session-id ...`). The render-only / in-session takeover path (`okstra-run` skill) auto-detects the live session's jsonl via `resolve_inproc_lead_session_id`, but the detector is best-effort and may return empty if `~/.claude/projects/<encoded-cwd>/` is unreadable or has no jsonl yet. If `lead.sessionId` is empty at this point, write the running session's id into team-state before proceeding — Phase 7 token-usage collection depends on it and will fail with `lead jsonl not found (sessionId=)` otherwise.
|
|
217
218
|
4. If `TeamCreate` succeeds, proceed to Phase 4 (dispatch with `team_name`).
|
|
218
219
|
5. If `TeamCreate` fails (tool unavailable, permission denied, environment lacks Agent Teams support), proceed to Phase 5 fallback (dispatch with `run_in_background: true` and no `team_name`).
|
|
@@ -227,7 +228,7 @@ Spawn **analysis workers only** in the same turn (Phase 4 in Teams mode; Phase 5
|
|
|
227
228
|
|
|
228
229
|
**Agent `model:` on dispatch (BLOCKING — assignment is otherwise ignored).** The `Claude worker` `Agent(...)` call MUST set `model: "<family token of that role's modelExecutionValue>"` (`fable` / `opus` / `sonnet` / `haiku`), per [okstra-team-contract](./skills/okstra-team-contract/SKILL.md) "Model Assignment Rules" #3–#4. The claude-worker definition is `model: inherit`, so omitting this parameter makes the worker silently run on the lead's model instead of its manifest assignment — the assigned-vs-actual deviation. `Codex worker` / `Gemini worker` are exempt: their CLI model is applied via the wrapper's own `--model` argument, so leave their Agent `model:` at `inherit` (rule #5).
|
|
229
230
|
|
|
230
|
-
The no-`team_name` fallback (Phase 5) is
|
|
231
|
+
The no-`team_name` fallback (Phase 5) is legal when team-state's `teamCreate.status` is `"error"` (TeamCreate attempted and failed) OR `"skipped"` with `reason: "concurrent-run"` (the launch prompt's "Concurrent-run: no-team background" gate pre-decided no team to avoid racing a concurrent same-task run's `~/.claude/teams/` config). In both cases `teamCreate` is recorded in team-state before any dispatch. If `teamCreate` is missing or `attempted: false`, the correct action when an Agent dispatch is rejected for a missing team is to GO BACK to Phase 3 and call `TeamCreate` — never to strip `team_name` and continue.
|
|
231
232
|
|
|
232
233
|
**Completion detection after dispatch (BLOCKING).** The `Agent(... team_name ...)` call returns `Spawned successfully` immediately; that ack is NOT completion. After dispatching the analysis workers (async), Lead MUST detect their completion via the self-scheduled polling protocol in [okstra-team-contract](./skills/okstra-team-contract/SKILL.md) "Worker-completion detection (self-scheduled polling)" — do NOT restate the algorithm here. Lead MUST NOT end its turn with a prose "waiting for workers" statement; that path stalls the run until the user manually nudges it.
|
|
233
234
|
|
|
@@ -366,7 +367,7 @@ After persistence, reply briefly in the resolved Report Language with: completio
|
|
|
366
367
|
| Mistake | Fix |
|
|
367
368
|
|---------|-----|
|
|
368
369
|
| Dispatching workers with `team_name` before calling `TeamCreate` (Phase 3 skipped) | Phase 3 is BLOCKING — call `TeamCreate` first. The Agent tool's `"team must be created first"` rejection is not an environment-availability signal |
|
|
369
|
-
| Stripping `team_name` and retrying when the Agent tool rejects the call for a non-existent team | This is silent loss of Teams split-pane mode. Correct action: go back to Phase 3 and call `TeamCreate`. The no-`team_name` fallback (Phase 5) is
|
|
370
|
+
| Stripping `team_name` and retrying when the Agent tool rejects the call for a non-existent team | This is silent loss of Teams split-pane mode. Correct action: go back to Phase 3 and call `TeamCreate`. The no-`team_name` fallback (Phase 5) is legal after `TeamCreate` was attempted and recorded as `error` in team-state, OR when the launch prompt's concurrent-run gate recorded `status: "skipped", reason: "concurrent-run"` |
|
|
370
371
|
| Substituting Claude lead reasoning for a worker result | Claude lead synthesizes only — spawn the worker |
|
|
371
372
|
| Skipping a worker silently | Always record terminal status with reason |
|
|
372
373
|
| Writing verdict before all workers report | Wait for all results or explicit terminal statuses |
|
|
@@ -49,16 +49,16 @@ profile document.
|
|
|
49
49
|
- The question MUST be a clean yes/no — do NOT offer "close some / keep some" partial answers, do NOT propose alternatives like "close only codex panes". The whole-set decision keeps the wrap-up predictable.
|
|
50
50
|
- This step is mandatory for every phase (`requirements-discovery`, `error-analysis`, `implementation-planning`, `implementation`, `final-verification`, `release-handoff`). It is silent-skipped when `<RUN_DIR>/state/lead-pane.id` is empty/absent (lead running outside tmux); the lead MUST NOT fabricate a synthetic pane list in that case.
|
|
51
51
|
- Run-end teammate teardown (shared — MUST run AFTER the pane disposition question and BEFORE `PROGRESS: complete`):
|
|
52
|
-
- The lead created the worker team in Phase 3 (`TeamCreate
|
|
53
|
-
- This step applies only when team-state's `teamCreate.status == "ok"` (Teams mode was actually used). In the no-`team_name` fallback there is no team to delete, so silent-skip.
|
|
52
|
+
- The lead created the worker team in Phase 3 (`TeamCreate` with the name recorded as team-state `teamName` — `okstra-<task-key>`, implementation stage runs `okstra-<task-key>-s<N>`). Worker teammates are NOT reclaimed on their own — without an explicit teardown they linger in the FleetView roster across this and later runs in the session. A lingering team also makes the next run of the same task-key collide on `TeamCreate` ("team already exists"), forcing the stale-team recovery path.
|
|
53
|
+
- This step applies only when team-state's `teamCreate.status == "ok"` (Teams mode was actually used). In the no-`team_name` fallback — whether `teamCreate.status` is `"error"` or `"skipped"` (`reason: "concurrent-run"`) — there is no team to delete, so silent-skip.
|
|
54
54
|
- After the pane disposition step above, the lead MUST ask the user whether to disband the worker teammates. This is a strict binary choice:
|
|
55
|
-
> worker teammate 팀
|
|
55
|
+
> worker teammate 팀 `<teamName>` 을 해제할까요?
|
|
56
56
|
> (예) 팀원 정리 / (아니오) 그대로 두기
|
|
57
|
-
- On `아니오` / `n` / `keep` → do not call `TeamDelete()`. Tell the user that the team will remain visible in FleetView/Teams and can be removed later from the Teams UI, or by asking the lead in this thread to clean up
|
|
57
|
+
- On `아니오` / `n` / `keep` → do not call `TeamDelete()`. Tell the user that the team will remain visible in FleetView/Teams and can be removed later from the Teams UI, or by asking the lead in this thread to clean up `<teamName>` — and that a later run of the same task-key/stage will hit a "team already exists" collision until it is removed.
|
|
58
58
|
- On `예` / `y` / `cleanup` → emit `PROGRESS: phase-7-teardown disbanding team`, then run the sequence below. Token-usage collection MUST already be complete — `TeamDelete` removes `~/.claude/teams/<team>/` + `~/.claude/tasks/<team>/` but NOT the `~/.claude/projects/` jsonls Phase 7 reads, yet the read MUST precede teardown.
|
|
59
|
-
1. Run `$HOME/.okstra/bin/okstra-team-reconcile.sh "
|
|
59
|
+
1. Run `$HOME/.okstra/bin/okstra-team-reconcile.sh "<teamName>"` (the team-state `teamName`) exactly once. It flips dead-pane stale-active members to inactive, and no-ops when tmux is unavailable or nothing is stale. Do NOT loop it.
|
|
60
60
|
2. Call `TeamDelete()` — the single synchronization point for teammate teardown. If it errors with an active-members message, send each named active member a structured `SendMessage(to: <name>, message: { type: "shutdown_request" })` — the `message` MUST be the object literal shown, NEVER a JSON string stuffed into a text field — wait briefly, run `okstra-team-reconcile.sh` one more time, retry `TeamDelete()` once, and proceed regardless of the result. NEVER loop and never use `TaskStop` (teammates are not background tasks — `TaskStop` 404s on a member address).
|
|
61
|
-
- If `TeamDelete()` is unavailable in the current host, or teardown still fails after the single retry, do not pretend cleanup succeeded. Report the exact residual action instead: `Teams/FleetView 에서 team
|
|
61
|
+
- If `TeamDelete()` is unavailable in the current host, or teardown still fails after the single retry, do not pretend cleanup succeeded. Report the exact residual action instead: `Teams/FleetView 에서 team <teamName> 삭제`, and if tmux panes were kept earlier also show the pane command `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"`.
|
|
62
62
|
- Report successful teardown in one short line (e.g. `worker teammate 팀 해제`, or `stale 멤버 1명 reconcile 후 팀 해제`) and proceed to the final `PROGRESS: complete ...` line.
|
|
63
63
|
- Brief handoff contract (shared — applies whenever the run consumes a task brief produced by `okstra-brief`):
|
|
64
64
|
- the brief is a **pre-discovery artifact**: it converts a domain-reporter's words (non-expert *or* developer) into expert-consumable form so this and later phases can run with zero fill-in questions to the operator. The brief is **not** authoritative on solution decisions; it is authoritative on the reporter's intent.
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
- Pre-verification entry gate (resolved & enforced by `okstra render-bundle` prep — the lead does NOT recompute it):
|
|
27
27
|
- the verification target (scope / worktree / base / head / stages / source reports / diff stat) is injected as the `VERIFICATION_TARGET` block. The lead MUST treat it as authoritative and MUST NOT re-pick a target from the brief.
|
|
28
28
|
- **whole-task scope** (`--stage auto`, default): prep has already verified every Stage Map stage is `status:done` in `consumers.jsonl`, every done stage's `head_commit` is an ancestor of the task worktree HEAD (all stage branches merged), and the worktree is clean outside `.okstra/`. If any check failed the run never started (PrepareError); a started whole-task run is therefore a fully-merged, clean target.
|
|
29
|
-
- **single-stage scope** (`--stage N`): prep verified stage N is `status:done` and its isolated stage worktree exists and is clean. Other stages' state is irrelevant. A single-stage run is a partial verification
|
|
29
|
+
- **single-stage scope** (`--stage N`): prep verified stage N is `status:done` and its isolated stage worktree exists and is clean. Other stages' state is irrelevant. A single-stage run is a partial verification: it MUST NOT recommend plain `release-handoff`, but MAY recommend `release-handoff(stage-group)` when the verdict is `accepted` — the stage becomes PR-eligible for a stage-group handoff.
|
|
30
30
|
- the lead still captures `git rev-parse HEAD` / `git status --short` from the injected worktree to confirm the analysis ran against the injected head; a mismatch is a `tool-failure`, not a silent proceed.
|
|
31
31
|
- Required deliverable shape (final report, in addition to the standard sections):
|
|
32
32
|
- **Source Implementation Report(s)**: the `VERIFICATION_TARGET` snapshot verbatim — verification scope, worktree path, base/head SHAs, the list of stages under verification, and one row per stage citing its originating implementation final-report (`report_path` from `consumers.jsonl`; render `(report_path unrecorded)` when absent). The lead injects this same snapshot into every analyser prompt (`**Verification scope:** / **Worktree:** / **Verification base ref:** / **Verification head SHA:** / **Verification diff stat:**`); a worker that cannot confirm its analysis ran against that exact head MUST record a `tool-failure`.
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
- **Read-only command log**: any pre-existing test/validation command executed during this run MUST be listed with its exact command line and exit code. No mutating commands may appear here.
|
|
38
38
|
- **Two-tier command lookup (shared with `implementation`):** when this phase performs its own independent re-validation, the command source is exactly the same two tiers `implementation` verifiers use — Tier 1 is the originating task brief / approved plan's `validation` set, Tier 2 is `<PROJECT_ROOT>/.okstra/project.json` under `qaCommands`. Auto-detecting tools from manifest files is forbidden; missing tiers are recorded as `qa-command not configured: <category>` and do NOT trigger a guess. The `cmd` deny-list (`--fix`, `--write`, ` -w`, ` -u`, `--snapshot-update`, `INSTA_UPDATE=<not-no>`, `cargo update`, `npm install` without `ci`, etc.) is enforced identically. NOTE: runtime fail-fast validation (`okstra_ctl.qa_commands.validate_qa_commands`) only fires at `--task-type implementation` run-prep, so this phase MUST self-check each `qaCommands` entry against the deny-list before executing it — if a denied token is present, skip the command and record it as a `Read-only command log` line `qa-command rejected (denied token: <token>): <label>`.
|
|
39
39
|
- **Tier 3 — stage conformance scripts (whole-task union):** because this phase verifies the **integrated, merged** state, it re-runs conformance against that state rather than per-stage. Read the task-level manifest `<task_root>/qa/conformance-manifest.json` (the directory is the `TASK_QA_PATH` token) and, in **whole-task scope**, run the `runCommand` of **every** `entries[]` item against the merged worktree, refreshing each `<task_root>/qa/result-<stageKey>.json` (`{ "stageKey", "overall": "PASS"|"FAIL"|"MISSING", "ranAt", "requirements" }`). In **single-stage scope**, run only the entry whose `stageKey` matches the verified stage. An entry carrying an `exemption` or user `waiver` is NOT executed — record the skip and reason; a `waiver` becomes a `conditional-accept` condition surfaced in the section 7 Verdict (conformance left unverified by user acknowledgement). Each `runCommand` runs in the worktree cwd with `qaEnv` env (replica DB DSN / app base URL / env file) — **replica / test environment only**, never shared / staging / prod, and the same source/lockfile mutation deny-list applies (a conformance script MAY mutate only its `qaEnv` replica datastore). Interpret each result from the exit code + stdout `QA-RESULT: PASS|FAIL` (last wins) and `REQ <id>: PASS|FAIL: <reason>` lines; no `QA-RESULT` marker → `MISSING`. Any entry whose result is not `PASS` (including `MISSING` or a never-run/missing sidecar) is an **Acceptance Blocker** (`major`+) — exactly like the DB real-execution gate above, since `accepted` requires zero blockers the verdict becomes `conditional-accept` / `blocked`. This is the same gate the `validate-run.py` Tier 3 check enforces on the result sidecars.
|
|
40
|
-
- **Routing recommendation**: the next safe phase — one of `release-handoff`, `done`, `error-analysis`, `implementation-planning` — tied to the verdict and blocker list. `release-handoff` is allowed ONLY when the Verdict Token is `accepted`. `release-handoff` is additionally allowed ONLY when the verification scope (the `Verification scope:` line of the injected `VERIFICATION_TARGET` block, recorded as the report's `verificationScope` field) is `whole-task`; a `single-stage` run
|
|
40
|
+
- **Routing recommendation**: the next safe phase — one of `release-handoff`, `done`, `error-analysis`, `implementation-planning` — tied to the verdict and blocker list. `release-handoff` is allowed ONLY when the Verdict Token is `accepted`. `release-handoff` is additionally allowed ONLY when the verification scope (the `Verification scope:` line of the injected `VERIFICATION_TARGET` block, recorded as the report's `verificationScope` field) is `whole-task`; a `single-stage` accepted run routes to `release-handoff(stage-group)` (or `implementation` / `done`); plain `release-handoff` remains whole-task-only. Enforcement: `validators/validate-run.py` rejects a `single-stage` report whose routing cites plain `release-handoff`.
|
|
41
|
+
- **Verified-row recording** (single-stage scope only): when the Verdict Token is `accepted`, the lead MUST run `okstra handoff record-verified --plan-run-root <plan-run-root> --stage <N> --report-path <final-report.md path> --data-json <final-report data.json path>` and quote the command + exit code in the report. The helper re-validates taskType/scope/verdict from data.json, so a non-accepted or whole-task report is rejected at the tool layer.
|
|
41
42
|
- Clarification request policy (phase-specific addendum — shared policy is in `_common-contract.md`):
|
|
42
43
|
- populate `## 1. Clarification Items` only when a blocker hinges on information only the user can supply (deployment intent, intended target environment, business-rule interpretation); use `Blocks=next-phase` for items that gate continuing to release-handoff
|
|
43
44
|
- Self-review pass before finalising the report (`Claude lead` runs this; do not delegate to a generic subagent):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Release Handoff Profile
|
|
2
2
|
|
|
3
|
-
- Purpose: take an `accepted` final-verification verdict for an already-committed implementation branch and turn it into a delivered push and/or pull request, with explicit user selection at every mutating step
|
|
3
|
+
- Purpose: take an `accepted` final-verification verdict for an already-committed implementation branch and turn it into a delivered push and/or pull request, with explicit user selection at every mutating step. Two modes: **whole-task** (default — the verified task branch becomes one PR) and **stage-group** (a user-selected subset of verified stages is merged into a collector branch and becomes one PR).
|
|
4
4
|
- **Execution model: single-lead, no worker dispatch.** This phase is a thin orchestrator over `git` / `gh`; it does NOT run team-mode, does NOT call `TeamCreate`, does NOT dispatch analysis or drafter sub-agents, and does NOT run convergence. The Claude lead performs every step inline (drafting PR text, asking the user, running git / gh, writing the final report) — see "Lead-only contract" below.
|
|
5
5
|
- Worker roster: none — this profile intentionally has no `- Required workers:` block; the run is executed entirely by the Claude lead.
|
|
6
6
|
- Lead-only contract (replaces the shared team contract for this phase):
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
- The shared "authority & permissions assumption" rule from the common contract still applies: assume the user holds every permission needed; do not block on hypothetical approvals.
|
|
12
12
|
- The shared "MCP read-only" rule still applies if the brief lists MCP servers, though most release-handoff runs do not use MCP.
|
|
13
13
|
- Pre-handoff entry gate (mandatory — refuse to start if any item fails):
|
|
14
|
-
- the task brief MUST cite the originating `final-verification` final-report path under `## Source Verification Report
|
|
14
|
+
- **whole-task mode**: the task brief MUST cite the originating `final-verification` final-report path under `## Source Verification Report`; the lead confirms its `Verdict Token` is exactly `accepted` and its `verificationScope` is `whole-task`.
|
|
15
|
+
- **stage-group mode**: the brief cites N single-stage `final-verification` reports (one per candidate stage); the lead confirms each `Verdict Token` is `accepted`. Eligibility is re-enforced by `okstra handoff` — the lead never hand-computes it.
|
|
15
16
|
- if the verdict is `conditional-accept`, `blocked`, or any other token (including ambiguous phrasing like "looks good"), the run MUST end immediately with status `blocked` and a routing recommendation back to `error-analysis` or `implementation-planning`. Do NOT prompt the user; Do NOT run any git command.
|
|
16
17
|
- the lead MUST capture `git status --short` and confirm the working tree is clean. Dirty state aborts the run; release-handoff packages the commits produced by `implementation`, it does not stage or commit changes.
|
|
17
18
|
- the lead MUST capture `git rev-parse --abbrev-ref HEAD` and record it as the **feature branch**. If the current branch is itself `main`, `master`, `prod`, `preprod`, `staging`, or `dev`, the run MUST end immediately — release-handoff never operates on a base branch.
|
|
@@ -22,6 +23,9 @@
|
|
|
22
23
|
- `push + PR` — push the feature branch, then open or reuse a pull request.
|
|
23
24
|
- `skip` — record the verified state and end the run without any git command.
|
|
24
25
|
If the user picks `skip`, route directly to the final-report self-review pass.
|
|
26
|
+
- **stage-group mode order**: G1 base branch first (same options as Q2 — the dependency-closure check needs `origin/<base>`), then G2 stage selection, then assemble, then Q2b/Q3 as usual with the collector branch as the PR head.
|
|
27
|
+
1g. **G2 — stage selection**: run `okstra handoff eligible --plan-run-root <plan-run-root> --approved-plan <approved plan path>` and present the returned stages (eligible ones selectable, blocked ones listed with their `reasons`) as a multi-select. At least one stage must be selected.
|
|
28
|
+
2g. **assemble**: run `okstra handoff assemble --plan-run-root <...> --approved-plan <...> --project-root <project root> --project-id <id> --task-group <g> --task-id <t> --work-category <c> --stages <csv> --base <chosen-base>`. Exit 2 means a stage-vs-stage merge conflict: show the `conflicts` paths and stop (route: reshape the group or resolve manually). Exit 1 means an eligibility/closure violation: show the error verbatim and re-ask G2. On success the returned `branch` is the PR head branch for every subsequent step.
|
|
25
29
|
2. **PR base branch** (only when the user picked `push + PR`) — present four options and capture exactly one:
|
|
26
30
|
- `staging`
|
|
27
31
|
- `preprod`
|
|
@@ -42,7 +46,7 @@
|
|
|
42
46
|
- `edit then proceed` — accept inline edits from the user, then proceed with the edited text.
|
|
43
47
|
- `cancel` — end the run without executing push or PR commands; record the cancellation in the final report.
|
|
44
48
|
- Inline drafting rules (Claude lead):
|
|
45
|
-
- read the run brief, the cited final-verification report, `git log --oneline <base>..HEAD`, and `git diff <base>..HEAD --stat` to ground the drafted text in actual committed changes.
|
|
49
|
+
- read the run brief, the cited final-verification report, `git log --oneline <base>..HEAD`, and `git diff <base>..HEAD --stat` to ground the drafted text in actual committed changes. In stage-group mode the draft is grounded on `git log <implementation_base_commit>..<collector HEAD>` / `git diff <implementation_base_commit>..<collector HEAD> --stat`, with source material = each selected stage's implementation report + its single-stage verification report.
|
|
46
50
|
- **PR body template** — the run context exposes `PR_TEMPLATE_PATH` and `PR_TEMPLATE_SOURCE`. The path MUST be an okstra-owned project artifact under `<PROJECT_ROOT>/.okstra/**` or a file already materialised into this run's artifact directory by the prepare step. The lead MUST `Read` this file verbatim, strip HTML comments, then fill in the placeholders. Do NOT hard-code a section list — the template is the source of truth for the structure. If the resolved file is missing or outside the okstra resource boundary at draft time, abort the run with a clear error rather than inventing a structure.
|
|
47
51
|
- produce **two artifacts** before showing them to the user:
|
|
48
52
|
1. **PR title** — by default the subject of the most recent implementation commit, or a concise Conventional Commits-style summary of the committed range.
|
|
@@ -50,11 +54,13 @@
|
|
|
50
54
|
- Allowed actions during the run (Claude lead only):
|
|
51
55
|
- read-only inspection: `git status`, `git status --short`, `git diff`, `git log`, `git rev-parse`, `git ls-remote --heads origin <name>`, `gh pr list --head <branch>`, `gh pr view <url>`.
|
|
52
56
|
- merge-conflict probe (only when the user picked `push + PR`): `git fetch origin <chosen-base>` and `git merge-tree --write-tree --merge-base origin/<chosen-base> HEAD origin/<chosen-base>`. Both are non-mutating with respect to the working tree.
|
|
53
|
-
- feature-branch push (only when the user picked `push + PR`): `git push -u origin <current-branch>`. The pushed ref MUST be the feature branch — never the chosen base branch.
|
|
57
|
+
- feature-branch push (only when the user picked `push + PR`): `git push -u origin <current-branch>`. The pushed ref MUST be the feature branch — never the chosen base branch. (stage-group mode: the collector branch returned by assemble)
|
|
54
58
|
- PR creation (only when the user picked `push + PR` AND no PR with the same head already exists on origin): `gh pr create --base <chosen-base> --head <current-branch> --title "<title>" --body "<body>"`. The title and body are the user-confirmed PR draft.
|
|
55
59
|
- PR reuse: if `gh pr list --head <branch> --state open --json url --jq '.[0].url'` returns a URL, treat that PR as already existing — record the URL in the final report and SKIP `gh pr create`.
|
|
60
|
+
- after `gh pr create` succeeds (or an existing PR is reused), the lead MUST run `okstra handoff record-pr --plan-run-root <...> --stages <csv> --branch <head branch> --url <pr url>` and quote the command + exit code in the final report. This applies to BOTH modes — whole-task runs record `--stages` as the full Stage Map list — so duplicate-PR prevention converges on one consumers record.
|
|
61
|
+
- stage-group helpers: `okstra handoff eligible`, `okstra handoff assemble`, `okstra handoff record-pr`. The assemble step is the ONLY path that may create commits (merge commits on the collector branch) in this phase.
|
|
56
62
|
- Forbidden actions (any occurrence → terminal status `contract-violated`):
|
|
57
|
-
- local commit commands of any kind (`git add`, `git commit`, `git restore --staged`, `git stash`).
|
|
63
|
+
- local commit commands of any kind (`git add`, `git commit`, `git restore --staged`, `git stash`), and any direct `git merge` / `git rebase` run by the lead. The single exception is the merge commits `okstra handoff assemble` itself creates on the collector branch — the lead never merges by hand.
|
|
58
64
|
- any of the following git push variants, regardless of intent or whether the user said "force it":
|
|
59
65
|
- `git push --force`
|
|
60
66
|
- `git push --force-with-lease`
|
|
@@ -88,6 +94,7 @@
|
|
|
88
94
|
- `- PR created: <url>` with title and base branch
|
|
89
95
|
- `- PR reused: <url>` when an existing PR was found via `gh pr list`
|
|
90
96
|
- `- PR creation skipped: <reason>` for any user-driven cancellation
|
|
97
|
+
- **Stage Group** (stage-group mode only): selected stages, each stage's single-stage verification report path + quoted `Verdict Token` row, collector branch name, merge commit SHAs from assemble, and the dependency-closure verdict (from the assemble output / error).
|
|
91
98
|
- **Routing recommendation**: explicit `done` token, since release-handoff is the terminal lifecycle phase. If the run ended in `skip` or `cancel`, the recommendation MUST also state whether re-entry into release-handoff is appropriate.
|
|
92
99
|
- Self-review pass before finalising the report (`Claude lead` runs this):
|
|
93
100
|
1. **Entry-gate audit** — section 2 cites the originating final-verification report path and the literal `Verdict Token` row with value `accepted`. If either is missing, the run is invalid and MUST be re-routed to `final-verification`.
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"in_worktree": "현재 worktree(`{path}`)에서 그대로 진행합니다(이미 non-main worktree) — 진행할까요?",
|
|
140
140
|
"not_git": "git 저장소가 아니므로 `{path}` 에서 직접 진행합니다 — 진행할까요?"
|
|
141
141
|
},
|
|
142
|
-
"options": { "proceed": "진행", "edit": "base-ref 다시 고르기" },
|
|
142
|
+
"options": { "proceed": "진행", "edit": "base-ref 다시 고르기", "abort": "중단" },
|
|
143
143
|
"echo_template": "branch-confirm: {value}"
|
|
144
144
|
},
|
|
145
145
|
"base_ref_text": {
|