okstra 0.67.0 → 0.69.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 +25 -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 +8 -7
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +3 -3
- package/runtime/agents/workers/gemini-worker.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +2 -2
- package/runtime/prompts/launch.template.md +2 -2
- package/runtime/prompts/profiles/_common-contract.md +6 -6
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -0
- package/runtime/prompts/profiles/_implementation-executor.md +3 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -0
- package/runtime/prompts/profiles/final-verification.md +3 -2
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +12 -5
- package/runtime/prompts/wizard/prompts.ko.json +5 -5
- package/runtime/python/okstra_ctl/conformance.py +17 -0
- 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 +175 -44
- package/runtime/python/okstra_ctl/wizard.py +89 -22
- package/runtime/python/okstra_ctl/worktree.py +28 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +40 -9
- package/runtime/python/okstra_token_usage/collect.py +27 -0
- package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
- package/runtime/skills/okstra-convergence/SKILL.md +3 -3
- package/runtime/skills/okstra-report-writer/SKILL.md +8 -8
- package/runtime/skills/okstra-run/SKILL.md +43 -3
- package/runtime/skills/okstra-team-contract/SKILL.md +7 -7
- package/runtime/validators/validate-run.py +51 -11
- package/src/_python-helper.mjs +52 -0
- package/src/error-log.mjs +19 -0
- package/src/git-reconcile.mjs +31 -0
- package/src/handoff.mjs +30 -0
- package/src/inject-report-index.mjs +22 -0
- package/src/render-final-report.mjs +22 -0
- package/src/render-views.mjs +9 -48
- package/src/spawn-followups.mjs +23 -0
- package/src/token-usage.mjs +3 -34
|
@@ -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
|
|
|
@@ -245,13 +246,13 @@ After each worker terminates, BEFORE classifying its terminal status, verify the
|
|
|
245
246
|
After each worker terminates (any terminal status), if its errors sidecar exists, dump it to the run error log using the same resolved paths from the launch prompt:
|
|
246
247
|
|
|
247
248
|
```bash
|
|
248
|
-
|
|
249
|
+
okstra error-log append-from-worker \
|
|
249
250
|
--sidecar <absolute-sidecar-path-from-launch-prompt> \
|
|
250
251
|
--out <absolute-errors-log-path-from-launch-prompt> \
|
|
251
252
|
--task-key <taskKey> --agent <agent> --agent-role <role> --model <model>
|
|
252
253
|
```
|
|
253
254
|
|
|
254
|
-
For Codex/Gemini wrappers: if the CLI returns non-zero, times out, or hits a rate limit, immediately call `okstra
|
|
255
|
+
For Codex/Gemini wrappers: if the CLI returns non-zero, times out, or hits a rate limit, immediately call `okstra error-log append-observed --error-type cli-failure ...` with the captured exit code, duration, message, and stderr excerpt. The wrapper subagent does this from inside its own Bash tool — Lead does NOT need to re-record. Token usage is NOT available from Agent tool results in real time; it is collected post-hoc at the start of Phase 7.
|
|
255
256
|
|
|
256
257
|
## Phase 5.5: Convergence loop
|
|
257
258
|
|
|
@@ -270,7 +271,7 @@ When `task-manifest.json` does not set `convergence.maxRounds`, lead MUST resolv
|
|
|
270
271
|
|
|
271
272
|
**Confirmed findings are pruned from the queue.** Findings classified as `full-consensus`, `partial-consensus`, or `worker-unique` MUST NOT appear in any subsequent round's reverify prompt for any worker. `contested` is a final classification assigned only when the last executed round completes and the queue is still non-empty — it is NEVER an intermediate queue label.
|
|
272
273
|
|
|
273
|
-
If any re-verification batch yields a `verification-error` terminal status, or a worker result fails the contract, Lead MUST record one event per violation via `
|
|
274
|
+
If any re-verification batch yields a `verification-error` terminal status, or a worker result fails the contract, Lead MUST record one event per violation via `okstra error-log append-observed --error-type contract-violation --agent <offending-agent> ...`. Use `agent: "claude-lead"` only when the violation is detected internally without a specific worker.
|
|
274
275
|
|
|
275
276
|
If convergence is disabled, proceed directly to Phase 6 with the raw worker results.
|
|
276
277
|
|
|
@@ -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 |
|
|
@@ -104,7 +104,7 @@ If you find yourself thinking "let me double-check section 3" or "I should read
|
|
|
104
104
|
|
|
105
105
|
## Error reporting
|
|
106
106
|
|
|
107
|
-
This agent is responsible for recording its own tool failures via `
|
|
107
|
+
This agent is responsible for recording its own tool failures via `okstra error-log`:
|
|
108
108
|
|
|
109
109
|
**Path extraction (BLOCKING).** Before recording anything, extract the absolute sidecar path from the lead's dispatch prompt body:
|
|
110
110
|
|
|
@@ -103,7 +103,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
|
|
|
103
103
|
|
|
104
104
|
c. **Result-file existence check (exit 0 only).** If `exit_code == 0` BUT no file exists at the extracted Result Path, the Codex CLI returned 0 without producing the analysis artifact. Observed failure mode: the CLI streams analysis prose on stdout, hits its token budget or a sandbox EPERM mid-`Write`, and exits 0 with the artifact never persisted. Forwarding the partial stdout silently degrades lead synthesis (the case that motivated this rule), so this path is required.
|
|
105
105
|
1. Capture the final ~10 lines of the wrapper's live log for diagnostics — single Bash call: `tail -n 10 "${prompt_path%.md}.log"` (substitute the literal absolute prompt-history path; the wrapper writes the log next to it per the §"trace pane" comment in `okstra-codex-exec.sh`). Write the captured lines to a temp file (e.g. `<errors-sidecar-dir>/codex-result-missing-tail.txt`) so `--stderr-excerpt-file` can reference it.
|
|
106
|
-
2. Record a `cli-failure` event directly to the run-level error log via the exact `okstra
|
|
106
|
+
2. Record a `cli-failure` event directly to the run-level error log via the exact `okstra error-log append-observed` template in §"Error reporting" — substitute `--exit-code 0`, `--duration-ms <observed-ms>`, `--message "okstra-codex-exec.sh exited 0 but no result file at <abs-path>"`, and `--stderr-excerpt-file <temp-tail-path>`.
|
|
107
107
|
3. Return `CODEX_RESULT_MISSING: codex exited 0 but result file absent at <abs-path>` instead of the raw stdout. The lead is responsible for deciding redispatch per `okstra-team-contract` "Lead Redispatch Policy on Result-Missing".
|
|
108
108
|
|
|
109
109
|
d. **Normal return.** Otherwise (`exit_code == 0` AND result file exists), return the wrapper's accumulated stdout from `BashOutput`, prefixed by exactly one model-identity line copied verbatim from the `**Model:** Codex worker, <execution-value>` line in the lead prompt (per Worker Preamble → "Return message to the lead"):
|
|
@@ -175,7 +175,7 @@ This contract mirrors the `okstra-team-contract` skill's Worker Output Contract
|
|
|
175
175
|
## Error reporting
|
|
176
176
|
|
|
177
177
|
The wrapper agent (this Codex worker subagent) is responsible for recording
|
|
178
|
-
two kinds of errors via `
|
|
178
|
+
two kinds of errors via `okstra error-log`:
|
|
179
179
|
|
|
180
180
|
**Path extraction (BLOCKING).** Before recording anything, extract the
|
|
181
181
|
following two absolute paths verbatim from the lead's dispatch prompt body:
|
|
@@ -207,7 +207,7 @@ and the run-level error log staying empty.
|
|
|
207
207
|
the dispatched `bash_id`:
|
|
208
208
|
|
|
209
209
|
```bash
|
|
210
|
-
|
|
210
|
+
okstra error-log append-observed \
|
|
211
211
|
--out "<absolute-errors-log-path-from-lead-prompt>" \
|
|
212
212
|
--task-key "<task-key>" \
|
|
213
213
|
--phase "<phase>" \
|
|
@@ -103,7 +103,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
|
|
|
103
103
|
|
|
104
104
|
c. **Result-file existence check (exit 0 only).** If `exit_code == 0` BUT no file exists at the extracted Result Path, the Gemini CLI returned 0 without producing the analysis artifact. Observed failure mode: the CLI streams analysis prose on stdout, hits its token budget or a sandbox EPERM mid-`Write`, and exits 0 with the artifact never persisted. Forwarding the partial stdout silently degrades lead synthesis (the case that motivated this rule), so this path is required.
|
|
105
105
|
1. Capture the final ~10 lines of the wrapper's live log for diagnostics — single Bash call: `tail -n 10 "${prompt_path%.md}.log"` (substitute the literal absolute prompt-history path; the wrapper writes the log next to it per the §"trace pane" comment in `okstra-gemini-exec.sh`). Write the captured lines to a temp file (e.g. `<errors-sidecar-dir>/gemini-result-missing-tail.txt`) so `--stderr-excerpt-file` can reference it.
|
|
106
|
-
2. Record a `cli-failure` event directly to the run-level error log via the exact `okstra
|
|
106
|
+
2. Record a `cli-failure` event directly to the run-level error log via the exact `okstra error-log append-observed` template in §"Error reporting" — substitute `--exit-code 0`, `--duration-ms <observed-ms>`, `--message "okstra-gemini-exec.sh exited 0 but no result file at <abs-path>"`, and `--stderr-excerpt-file <temp-tail-path>`.
|
|
107
107
|
3. Return `GEMINI_RESULT_MISSING: gemini exited 0 but result file absent at <abs-path>` instead of the raw stdout. The lead is responsible for deciding redispatch per `okstra-team-contract` "Lead Redispatch Policy on Result-Missing".
|
|
108
108
|
|
|
109
109
|
d. **Normal return.** Otherwise (`exit_code == 0` AND result file exists), return the wrapper's accumulated stdout from `BashOutput`, prefixed by exactly one model-identity line copied verbatim from the `**Model:** Gemini worker, <execution-value>` line in the lead prompt (per Worker Preamble → "Return message to the lead"):
|
|
@@ -175,7 +175,7 @@ This contract mirrors the `okstra-team-contract` skill's Worker Output Contract
|
|
|
175
175
|
## Error reporting
|
|
176
176
|
|
|
177
177
|
The wrapper agent (this Gemini worker subagent) is responsible for recording
|
|
178
|
-
two kinds of errors via `
|
|
178
|
+
two kinds of errors via `okstra error-log`:
|
|
179
179
|
|
|
180
180
|
**Path extraction (BLOCKING).** Before recording anything, extract the
|
|
181
181
|
following two absolute paths verbatim from the lead's dispatch prompt body:
|
|
@@ -207,7 +207,7 @@ and the run-level error log staying empty.
|
|
|
207
207
|
the dispatched `bash_id`:
|
|
208
208
|
|
|
209
209
|
```bash
|
|
210
|
-
|
|
210
|
+
okstra error-log append-observed \
|
|
211
211
|
--out "<absolute-errors-log-path-from-lead-prompt>" \
|
|
212
212
|
--task-key "<task-key>" \
|
|
213
213
|
--phase "<phase>" \
|
|
@@ -101,9 +101,9 @@ Rules (the schema enforces most of these — they are listed here so you know *w
|
|
|
101
101
|
- Cite file paths and line numbers in every `evidence.primary[].source` / `consensus[].evidence` cell.
|
|
102
102
|
- Preserve every analysis worker's ticket tagging — every row's `ticketId` field carries the ticket key or the task-fallback. For single-ticket runs, set `ticketCoverage` to `{"singleTicket": "<ticket>"}`. For runs that do not require ticket tagging (`release-handoff`, `final-verification`), set `ticketCoverage` to `{"omit": true}`.
|
|
103
103
|
- For `implementation-planning`, populate `implementationPlanning.requirementCoverage` with one row per concrete requirement from the brief / packet, using IDs `R-001`, `R-002`, ... in source order. `coveredBy` MUST name the specific Option Candidate plus Stage/Step that satisfies the requirement. Use `status: "covered"` only when the report's plan actually covers it; otherwise use `gap` or `blocked C-NNN` and ensure the corresponding `Clarification Items` row blocks approval. Do not collapse this into `ticketCoverage`; ticket coverage is not requirement coverage.
|
|
104
|
-
- When the `Task Type` is `improvement-discovery`, populate `## 5.9 Improvement Candidates` with the 10-column schema enforced by `validators/validate-improvement-report.py`. Source the row IDs (`I-NNN`), lens whitelist, and Source workers patterns from `scripts/okstra_ctl/improvement_lenses.py` — do NOT introduce new lens names or worker prefixes. `improvement-discovery` is NOT in the data.json schema enum, so author its markdown directly (not via `okstra-render-final-report.py`). Immediately after writing the markdown, run (`Bash`): `
|
|
104
|
+
- When the `Task Type` is `improvement-discovery`, populate `## 5.9 Improvement Candidates` with the 10-column schema enforced by `validators/validate-improvement-report.py`. Source the row IDs (`I-NNN`), lens whitelist, and Source workers patterns from `scripts/okstra_ctl/improvement_lenses.py` — do NOT introduce new lens names or worker prefixes. `improvement-discovery` is NOT in the data.json schema enum, so author its markdown directly (not via `okstra-render-final-report.py`). Immediately after writing the markdown, run (`Bash`): `okstra inject-report-index <markdown path> --report-language <en|ko>`. That adds the top-of-report Index plus `I-NNN` / `C-NNN` scroll anchors; the run validator fails the report when the Index anchor is absent.
|
|
105
105
|
|
|
106
|
-
Write the data.json with your `Write` tool
|
|
106
|
+
Write the data.json (and the audit sidecar `.md`) with your `Write` tool — that is the canonical authoring path, and okstra ships no hook that blocks `.md` writes (its only settings hook is the `SessionEnd` trace-cleanup; the coding-preflight hook emits reminders but never blocks). A Bash heredoc is acceptable ONLY when a specific `Write` call is genuinely rejected by the host environment, and it MUST produce byte-identical content — do not reach for it pre-emptively. Then invoke the renderer (`Bash`): `okstra render-final-report <data.json path>`. Confirm both files exist and respond with a short status line prefixed by your model identity, copied verbatim from the `**Model:** Report writer worker, <modelExecutionValue>` line in your dispatch prompt (per Worker Preamble → "Return message to the lead"):
|
|
107
107
|
|
|
108
108
|
```
|
|
109
109
|
**Model:** Report writer worker, <modelExecutionValue>
|
|
@@ -67,8 +67,8 @@ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at
|
|
|
67
67
|
- When dispatching any worker you MUST inject **two header lines** into the dispatch prompt body so the worker subagent can record errors without guessing paths:
|
|
68
68
|
- `**Errors log path:** <absolute run-level errors log path>`
|
|
69
69
|
- `**Errors sidecar path:** <absolute per-worker sidecar path matching the dispatched worker>`
|
|
70
|
-
- These lines are the canonical contract — worker subagents extract them verbatim and pass them to `okstra
|
|
71
|
-
- After each worker terminates, dump its sidecar into the run-level errors log via `
|
|
70
|
+
- These lines are the canonical contract — worker subagents extract them verbatim and pass them to `okstra error-log append-observed --out ...` (run-level cli-failure / contract-violation events) and to their internal sidecar writes (worker-reported tool-failure events) respectively.
|
|
71
|
+
- After each worker terminates, dump its sidecar into the run-level errors log via `okstra error-log append-from-worker --sidecar <sidecar-path> --out <run-errors-log-path> --task-key {{TASK_KEY}} --agent <worker-id> --agent-role worker --model <assigned-model-execution-value>` (per `okstra-team-contract` Worker Output Contract).
|
|
72
72
|
|
|
73
73
|
## Executor Worktree
|
|
74
74
|
|