okstra 0.69.0 → 0.71.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.
@@ -0,0 +1,158 @@
1
+ # brief entry-phase 한정 + release-handoff task-id 기반 stage 진입 — 설계
2
+
3
+ - 날짜: 2026-06-11
4
+ - 상태: 구현 완료 (사용자 승인 후 구현 — §5.3 의 manifest 갱신 항목만 구현 시 수정)
5
+ - 선행 설계: [2026-06-10-stage-group-handoff-design.md](2026-06-10-stage-group-handoff-design.md) (stage-group 백엔드), [2026-06-06-stage-worktree-isolation-design.md](2026-06-06-stage-worktree-isolation-design.md)
6
+
7
+ ## 1. 배경 / 문제
8
+
9
+ brief 는 비개발자·타 팀·아이디어·에러 신고 초안을 담는 **entry-phase 입력물**인데, 현재 구조는 모든 phase 에서 brief 를 의식처럼 요구한다. 사용자가 실제 run 에서 확인한 문제 세 가지:
10
+
11
+ 1. **위저드가 전 task-type 에 brief 를 묻는다.** brief 단계에 task-type 필터가 없고([wizard.py:2367-2382](../../../scripts/okstra_ctl/wizard.py:2367)), 기존 task 면 `brief_keep` 으로 유지/변경을 또 묻는다([wizard.py:2418-2425](../../../scripts/okstra_ctl/wizard.py:2418)). downstream phase 의 brief 는 task manifest 의 `taskBriefPath` 로 이미 자동 해소 가능한데도([wizard.py:830-837](../../../scripts/okstra_ctl/wizard.py:830)) 사용자 입력을 요구한다.
12
+ 2. **release-handoff 의 실질 입력은 final-verification final-report 인데 위저드는 brief 경로를 요구한다.** stage-group 모드 진입 조건이 "brief 가 단독-stage 검증 보고서 N 개를 인용"이라는 수동 의식이고([release-handoff.md:15](../../../prompts/profiles/release-handoff.md:15)), 그 brief 를 만들어 주는 자동화는 없다 — [release-handoff-input.template.md](../../../templates/reports/release-handoff-input.template.md) 는 렌더러가 없는 죽은 템플릿이며 단수 인용 슬롯뿐이다. 모드(whole-task/stage-group)를 사용자에게 보여주는 지점도 위저드·brief·게이트 어디에도 없다.
13
+ 3. **stage 자격 판정 인프라는 이미 있다.** `okstra_ctl.handoff.compute_eligibility` 가 approved plan 의 Stage Map + `consumers.jsonl`(done + `verified` accepted + 미-`pr`)로 stage 별 PR 자격을 판정한다([handoff.py:36-39](../../../scripts/okstra_ctl/handoff.py:36)). task-id 만 있으면 계산되는 정보를 brief 인용으로 중복 선언하게 하는 것은 단일 참조점 위반이다.
14
+
15
+ ## 2. 결정 요약 (사용자 Q&A)
16
+
17
+ | 질문 | 결정 |
18
+ |---|---|
19
+ | brief 는 어느 phase 입력인가 | entry phase(requirements-discovery / error-analysis / improvement-discovery) 전용. downstream 은 task-id 로 자동 carry-in |
20
+ | release-handoff 진입 입력 | brief 경로가 아니라 task-id. 검증 완료(eligible) stage 를 멀티선택하거나 전체 선택 |
21
+ | stage 선택 근거 | implementation-planning final-report 의 Stage Map + consumers 상태 (`handoff eligible` 재사용) |
22
+ | PR 생성 후 stage worktree 정리 | **범위 제외** (사용자 결정 — PR 리뷰 수정이 stage 브랜치를 재사용하는 흐름과 충돌 위험) |
23
+
24
+ ## 3. 목표 / 비목표
25
+
26
+ 목표:
27
+
28
+ - 위저드 brief 단계(`brief_path_pick`/`brief_path`/`brief_keep`)를 entry phase 에만 적용하고, downstream phase 는 manifest `taskBriefPath` 로 자동 해소한다.
29
+ - release-handoff 위저드 진입을 brief picker 대신 **eligible stage picker** 로 교체한다. 모드는 선택 결과로 자동 결정된다.
30
+ - prepare 가 release-handoff 의 입력 문서(검증 보고서 인용 래퍼)를 [release-handoff-input.template.md](../../../templates/reports/release-handoff-input.template.md) 로 **자동 생성**한다 — 죽은 템플릿의 부활, 수동 brief 작성 제거.
31
+ - `--stage`(impl/fv 의 Stage Map 실행/검증 선택)와 release-handoff 의 stage 묶음 선택을 **별개 채널**로 유지하되, 후자를 위저드 → render-bundle 로 전달하는 `--stages` flag 를 신설한다.
32
+
33
+ 비목표:
34
+
35
+ - PR 생성 후 worktree/branch/registry 정리 (사용자 결정으로 제외).
36
+ - `handoff eligible / assemble / record-*` 백엔드 로직 변경 — 그대로 재사용한다.
37
+ - 다른 phase 의 죽은 input 템플릿(`error-analysis-input` 등) 정비 — 후속 과제(§9).
38
+ - okstra-brief 스킬 변경 — entry phase 용도 그대로.
39
+ - prepare 의 `--task-brief` 필수 계약 변경 — 위저드/prepare 가 채워 넣는 방식만 바뀐다.
40
+
41
+ ## 4. 설계 A — brief 를 entry phase 전용으로
42
+
43
+ ### 4.1 task-type 분류 (SSOT 상수)
44
+
45
+ `wizard.py` 에 상수 신설 (기존 `_STAGE_SCOPED_TASK_TYPES` [wizard.py:89](../../../scripts/okstra_ctl/wizard.py:89) 와 나란히):
46
+
47
+ ```python
48
+ _BRIEF_ENTRY_TASK_TYPES = ("requirements-discovery", "error-analysis",
49
+ "improvement-discovery")
50
+ ```
51
+
52
+ downstream = 그 외 전부(implementation-planning, implementation, final-verification, release-handoff).
53
+
54
+ ### 4.2 위저드 단계 변경
55
+
56
+ - `S_BRIEF_PATH_PICK` / `S_BRIEF_PATH` / `S_BRIEF_KEEP` 의 `applies` 에 `s.task_type in _BRIEF_ENTRY_TASK_TYPES` 게이트 추가.
57
+ - 주의: 현재 신규 task 흐름은 brief 선택이 task-type 선택보다 **앞선다**([wizard.py:2373](../../../scripts/okstra_ctl/wizard.py:2373) `is_new_task is True and bool(task_group)`). 게이트를 넣으려면 신규 task 의 단계 순서를 task-type 먼저로 재배열해야 한다(§8 테스트로 고정). 기존 task 흐름은 이미 task-type 이후에 brief 를 묻는다.
58
+ - downstream + 기존 task: `_existing_task_brief()` 결과를 `state.brief_path` 에 **자동 주입**하고 brief 관련 단계를 모두 건너뛴다. 사용자에게 echo 한 줄로만 알린다 (`brief (carry-in): <path>`).
59
+ - downstream + brief 미등록 (신규 task 로 downstream type 을 고르거나 manifest 에 `taskBriefPath` 없음): 3-옵션 picker (run-prompt 추천 규칙 준수):
60
+ 1. (추천) entry phase 로 task-type 변경 — picker 가 entry 3종을 보여주고 task-type 단계로 되돌린다.
61
+ 2. brief 경로 직접 입력 — 탈출구. 손으로 쓴 brief 로 downstream 부터 시작하는 기존 워크플로 보존.
62
+ 3. 중단.
63
+ - release-handoff 는 §5 의 자동 생성 입력이 brief 역할을 대체하므로 이 picker 대상에서도 제외된다.
64
+
65
+ ### 4.3 계약 불변 사항
66
+
67
+ - prepare 의 `--task-brief` 필수 검증([run.py:2209-2212](../../../scripts/okstra_ctl/run.py:2209))과 downstream profile 들의 brief 인용(예: Requirement Coverage 가 brief heading 인용 — [implementation-planning.md:86](../../../prompts/profiles/implementation-planning.md:86))은 그대로다. brief 는 여전히 전 phase 의 요구사항 anchor 다 — 바뀌는 것은 "누가 경로를 대는가"(사용자 → 위저드 자동) 뿐이다.
68
+
69
+ ## 5. 설계 B — release-handoff: task-id 진입 + eligible stage picker
70
+
71
+ ### 5.1 위저드 흐름 (release-handoff 한정)
72
+
73
+ ```
74
+ task 선택(기존 task 전제) → task-type=release-handoff
75
+ → approved-plan 자동 해소 (fv 와 동일: _latest_implementation_planning_report
76
+ + frontmatter approved: true 확인; 실패 시 implementation-planning 선행 안내)
77
+ → S_HANDOFF_STAGE_PICK (신규):
78
+ compute_eligibility 직접 호출 (python import — shell-out 금지, 단일 참조점)
79
+ 옵션:
80
+ - "전체 task (whole-task 검증 기반)" — whole-task fv 보고서(accepted,
81
+ verificationScope=whole-task)가 존재할 때만 노출 → whole-task 모드
82
+ - eligible stage 멀티선택 (전체 eligible 선택 = "다 같이") → stage-group 모드
83
+ - blocked stage 는 reasons 와 함께 비선택 표시
84
+ → (이후 기존 단계: pr-template, 모델/directive 등 — 변경 없음)
85
+ ```
86
+
87
+ - brief 단계 없음, base-ref 단계 없음(현행 유지 — PR base 는 run 중 G1/Q2 가 묻는다).
88
+ - 모드 질문은 별도 단계가 아니라 **이 picker 의 선택지로 흡수**된다.
89
+
90
+ ### 5.2 인자 전달 — `--stages` 신설
91
+
92
+ - `render_args()` 가 release-handoff 일 때 신규 키 `stages` 를 채운다: whole-task = `""`, stage-group = `"2,3"` (csv).
93
+ - `render-bundle` / `okstra.sh` / `okstra_ctl.run` argparse 에 `--stages` 추가. **release-handoff 외 task-type 에서 비어 있지 않은 `--stages` 는 PrepareError** (어제 고친 `--stage` 빈 값 규칙과 동일하게, 빈 문자열은 미지정으로 통과 — [run.py:1124](../../../scripts/okstra_ctl/run.py:1124) 패턴).
94
+ - `--stage` 와 `--stages` 는 다른 채널이다: 전자 = impl/fv 의 Stage Map 실행/검증 선택, 후자 = release-handoff 의 PR 묶음 선택. 도움말에 명시한다.
95
+
96
+ ### 5.3 prepare — 입력 문서 자동 생성
97
+
98
+ release-handoff prepare 시:
99
+
100
+ 1. `compute_eligibility` 로 선택 stage 들의 자격을 **재검증** (위저드를 거치지 않은 CLI 경로도 같은 강제 지점을 통과).
101
+ 2. 각 선택 stage 의 단독 검증 보고서 경로·verdict 를 `consumers.jsonl` 의 `verified` 행(last-wins, [consumers.py:107-111](../../../scripts/okstra_ctl/consumers.py:107))에서 수집. whole-task 면 최신 whole-task fv final-report 에서 수집.
102
+ 3. [release-handoff-input.template.md](../../../templates/reports/release-handoff-input.template.md) 를 렌더링해 `<task_root>/release-handoff-input.md` 로 저장(매 run 덮어씀) → 이 파일이 그 run 의 brief 자리(`inp.brief_path`)를 채운다. manifest `taskBriefPath` 는 **갱신하지 않는다** — entry brief 포인터를 덮으면 이후 다른 phase 의 carry-in 이 handoff input 을 brief 로 오인하기 때문(구현 시 확정한 spec 수정).
103
+ 4. run-context 에 `HANDOFF_MODE` (`whole-task` | `stage-group`) 와 `HANDOFF_STAGES` 를 기록.
104
+
105
+ ### 5.4 템플릿 개정
106
+
107
+ `## Source Verification Report` 를 단수/복수 겸용으로 교체:
108
+
109
+ ```markdown
110
+ ## Source Verification Report
111
+
112
+ - Mode: `whole-task` | `stage-group`
113
+ - Reports (one row per cited final-verification report):
114
+
115
+ | Stage | Report path | Verdict Token (verbatim) | Run timestamp |
116
+ |---|---|---|---|
117
+ ```
118
+
119
+ whole-task 모드는 Stage 열을 `(all)` 한 행으로 채운다.
120
+
121
+ ### 5.5 profile 개정 ([release-handoff.md](../../../prompts/profiles/release-handoff.md))
122
+
123
+ - 진입 게이트(:13-15): "brief 가 인용해야 한다" → "prepare 가 생성한 input 문서 + run-context 의 `HANDOFF_MODE`/`HANDOFF_STAGES` 를 읽고, 인용된 각 Verdict Token 이 `accepted` 인지 확인한다" 로 교체.
124
+ - G2(:27): 위저드/CLI 에서 stage 가 이미 선택돼 있으므로 **재선택이 아니라 확인 표시**로 축소 — 선택 목록을 보여주고 진행. `okstra handoff eligible` 직접 호출 분기는 stage 선택 없이 진입한 비정상 경로의 방어로만 남긴다.
125
+ - assemble(:28) 이후 흐름(G1 base → assemble → Q2b → Q3, record-pr)은 불변.
126
+
127
+ ## 6. 설계 C — final-verification
128
+
129
+ stage picker(done 마킹 + 전체 task 검증)는 최근 작업으로 이미 사용자 모델과 일치한다. 이 설계에서는 §4 의 brief 자동 carry-in 만 적용된다.
130
+
131
+ ## 7. 영향 파일
132
+
133
+ | 파일 | 변경 |
134
+ |---|---|
135
+ | [scripts/okstra_ctl/wizard.py](../../../scripts/okstra_ctl/wizard.py) | `_BRIEF_ENTRY_TASK_TYPES`, brief 단계 게이트·자동 주입·미등록 picker, 신규 task 단계 순서 재배열, `S_HANDOFF_STAGE_PICK`, `render_args` `stages` 키 |
136
+ | [scripts/okstra_ctl/run.py](../../../scripts/okstra_ctl/run.py) | `--stages` argparse + 검증 + prepare 의 input 자동 생성·`HANDOFF_MODE`/`HANDOFF_STAGES` |
137
+ | [scripts/okstra_ctl/handoff.py](../../../scripts/okstra_ctl/handoff.py) | (변경 없음 — import 재사용) |
138
+ | [templates/reports/release-handoff-input.template.md](../../../templates/reports/release-handoff-input.template.md) | Source Verification Report 복수 양식 |
139
+ | [prompts/profiles/release-handoff.md](../../../prompts/profiles/release-handoff.md) | 진입 게이트·G2 개정 |
140
+ | [prompts/wizard/prompts.ko.json](../../../prompts/wizard/prompts.ko.json) | 신규/변경 prompt 문구 |
141
+ | [skills/okstra-run/SKILL.md](../../../skills/okstra-run/SKILL.md) | render-bundle 호출부 `--stages`, brief 단계 서술 갱신 |
142
+ | [scripts/okstra.sh](../../../scripts/okstra.sh) | `--stages` 전달 |
143
+ | [docs/kr/cli.md](../../../docs/kr/cli.md), [docs/kr/architecture.md](../../../docs/kr/architecture.md), [docs/task-process/release-handoff.md](../../../docs/task-process/release-handoff.md) | 계약 서술 갱신 |
144
+
145
+ ## 8. 테스트
146
+
147
+ - 위저드 단계 적용 매트릭스: entry 3종 = brief 단계 노출, downstream 4종 = 미노출 + 자동 주입 echo. 신규 task 단계 순서(task-type 이 brief 보다 먼저) 회귀.
148
+ - downstream + brief 미등록 → 3-옵션 picker 노출, 각 분기 동작.
149
+ - `S_HANDOFF_STAGE_PICK`: eligible/blocked 구성, whole-task 옵션 노출 조건, 멀티선택 → `render_args["stages"]` csv.
150
+ - `--stages` 게이트: release-handoff 외 task-type 의 비어 있지 않은 값 거부, 빈 값 통과 (기존 [tests/test_prepare_stage_flag_gate.py](../../../tests/test_prepare_stage_flag_gate.py) 패턴).
151
+ - prepare 입력 자동 생성: verified 행 인용 정확성(단위) + subprocess `--render-only` e2e 로 생성 파일·manifest 갱신 확인.
152
+ - profile 계약: validators 의 release-handoff 관련 체크가 있다면 갱신분 통과.
153
+
154
+ ## 9. 미해결 / 후속
155
+
156
+ - 다른 phase 의 죽은 input 템플릿([render.py:113-118](../../../scripts/okstra_ctl/render.py:113) 태그만 존재) 정리 또는 부활 — 별도 과제.
157
+ - PR 생성 후 stage worktree/branch/registry 정리 — 이번 범위에서 명시적으로 제외(사용자 결정). 재논의 시 PR 리뷰 수정 흐름([okstra-run SKILL.md:228](../../../skills/okstra-run/SKILL.md:228))과의 충돌을 먼저 풀어야 한다.
158
+ - okstra-brief 체인 다이어그램([SKILL.md:44-57](../../../skills/okstra-brief/SKILL.md:44))의 release-handoff 종단 표기 정합화.
@@ -0,0 +1,89 @@
1
+ # wizard whole-task final-verification 노출 설계
2
+
3
+ - 작성일: 2026-06-11
4
+ - 상태: 설계 승인됨 (사용자 승인 2026-06-11)
5
+ - 선행: [final-verification-whole-task-gate-design.md](2026-06-06-final-verification-whole-task-gate-design.md), 커밋 `54b9482` (stage worktree 기반 검증으로 위저드 단순화)
6
+
7
+ ## 1. 배경 / 문제
8
+
9
+ `final-verification` 은 두 검증 모드를 가진다 — (A) 전체-task 모드(모든 stage 머지 후 한 번), (B) 단독-stage 모드(격리 worktree에서 그 stage만). 이 두 모드와 자동 target 해소는 prepare 레이어에 이미 구현돼 있다([final-verification-whole-task-gate-design.md](2026-06-06-final-verification-whole-task-gate-design.md)).
10
+
11
+ 커밋 `54b9482` 은 단독-stage 위저드 UX를 단순화하면서(`base-ref`/branch-confirm 생략, 명시 stage 강제) **위저드에서 전체-task 모드를 선택할 길을 막았다**:
12
+
13
+ - [`_stage_auto_allowed`](../../../scripts/okstra_ctl/wizard.py:778) 가 `implementation` 에서만 `True` → final-verification stage picker 에 전체 옵션이 뜨지 않는다.
14
+ - [`_submit_stage_pick`](../../../scripts/okstra_ctl/wizard.py:1539) 와 [`render_args`](../../../scripts/okstra_ctl/wizard.py:2799) 가 final-verification + 비-명시-stage 를 거부한다.
15
+
16
+ 결과: 전체-task final-verification 은 CLI `okstra.sh --stage auto` (또는 stage 생략)로만 가능하고, okstra-run 위저드 사용자는 도달할 수 없다. prepare 는 이미 전체-task 를 지원하므로([run.py:1829](../../../scripts/okstra_ctl/run.py:1829), [run.py:1533](../../../scripts/okstra_ctl/run.py:1533)) **막힌 곳은 위저드 레이어뿐**이다.
17
+
18
+ ## 2. 목표 / 비목표
19
+
20
+ 목표:
21
+ - okstra-run 위저드의 final-verification stage picker 에서 **전체-task 검증을 명시 항목으로 선택** 가능하게 한다.
22
+ - picker 에 stage 별 **done 상태를 표시**해, 전체-task 가 안 되는 경우 어느 stage 가 미완인지 사용자가 바로 본다.
23
+ - prepare 계약(CLI `--stage`)을 **변경하지 않는다** — 위저드가 기존 전체-task 트리거(빈 stage)를 emit 한다.
24
+
25
+ 비목표:
26
+ - `implementation` 의 `auto` 토큰 의미("가장 낮은 ready stage")를 final-verification 에서 재사용하지 않는다. picker 라벨·내부 값 어디에도 `auto` 를 쓰지 않는다.
27
+ - 머지/clean/active 전제의 위저드 사전 검사 — 이는 prepare 의 PrepareError 게이트에 위임한다(§4).
28
+ - 자동 stage 머지(여전히 사용자 수동 머지).
29
+ - prepare / `_reserve_final_verification_target` / target 해소 로직 변경.
30
+
31
+ ## 3. 노출 형태 — stage picker 명시 항목
32
+
33
+ [`_build_stage_pick`](../../../scripts/okstra_ctl/wizard.py:1494) 가 final-verification 일 때:
34
+
35
+ 1. 각 stage 항목 라벨에 **done 마킹**을 붙인다. 예: `1: <제목> [done]` / `2: <제목> [미완]`.
36
+ 2. **모든** Stage Map stage 가 done 이면 picker 맨 위에 `전체 task 검증` 항목을 추가한다. done 이 아닌 stage 가 하나라도 있으면 이 항목을 노출하지 않는다 — picker 의 stage 별 `[미완]` 마킹이 그대로 보이므로 사용자가 왜 전체 검증이 불가능한지 자명하다.
37
+
38
+ 이 구조는 "stage 가 done 이 아닌 걸 보여준다" 는 요구를 충족하면서, 전체-task 선택 가능 여부도 같은 화면에서 드러낸다.
39
+
40
+ ## 4. done 상태 데이터원 — git 호출 없음
41
+
42
+ 위저드가 읽는 것은 prepare 와 동일한 SSOT 인 `consumers.jsonl` 의 `status:done` 행뿐이다. git 상태(머지/clean)는 위저드가 검사하지 않는다.
43
+
44
+ - `plan_run_root` 도출: `Path(approved_plan_path).resolve().parents[1]` ([run.py:1525](../../../scripts/okstra_ctl/run.py:1525) 와 동일 규칙).
45
+ - done 행: `backfill_done_from_carry(plan_run_root)` → `read_consumers(plan_run_root)` → `latest_done_by_stage(rows)` ([consumers.py:24](../../../scripts/okstra_ctl/consumers.py:24), [consumers.py:37](../../../scripts/okstra_ctl/consumers.py:37), [consumers.py:182](../../../scripts/okstra_ctl/consumers.py:182)).
46
+ - Stage Map: 기존 `_build_stage_pick` 이 이미 승인 plan 을 `_parse_stage_map` 으로 파싱한다([wizard.py:1499](../../../scripts/okstra_ctl/wizard.py:1499)). 그 stage 번호 집합과 done 집합을 비교한다.
47
+
48
+ 책임 분담:
49
+ - **위저드**: done 여부만 사전 표시(파일 읽기). 전체-task 항목 노출 게이트.
50
+ - **prepare**: 머지(`head_commit` 이 task worktree HEAD 의 ancestor) · clean · task-key worktree active 를 최종 강제. 미충족이면 기존 PrepareError 로 어느 stage 가 미머지인지 안내([run.py:587](../../../scripts/okstra_ctl/run.py:587), [run.py:594](../../../scripts/okstra_ctl/run.py:594), [worktree.py:655](../../../scripts/okstra_ctl/worktree.py:655)).
51
+
52
+ 위저드가 git 을 호출하지 않으므로 picker 빌드가 가볍고, 전제 강제는 한 곳(prepare)에 단일화된다.
53
+
54
+ ## 5. prepare 계약 — 빈 stage emit (계약 무변경)
55
+
56
+ 전체-task 선택의 위저드 내부 표현과 prepare 로 넘기는 값을 분리한다:
57
+
58
+ - 위저드 내부: `selected_stage` 에 명시 sentinel(예: `"whole-task"`)을 담는다.
59
+ - [`render_args`](../../../scripts/okstra_ctl/wizard.py:2792): 이 sentinel 을 **빈 stage(`""`)** 로 변환해 prepare 에 넘긴다.
60
+
61
+ prepare 는 빈/`auto` stage 를 이미 전체-task 로 해석한다(`if inp.stage and inp.stage != "auto"` 의 else 분기, [run.py:1829](../../../scripts/okstra_ctl/run.py:1829) · [run.py:1533](../../../scripts/okstra_ctl/run.py:1533)). 따라서 CLI `--stage` 계약·validator·prepare 분기 변경이 전혀 없고, 표면 어디에도 `auto` 가 노출되지 않는다.
62
+
63
+ base-ref / branch-confirm skip 은 현행 유지([`_base_ref_required`](../../../scripts/okstra_ctl/wizard.py:766), [`_branch_confirm_required`](../../../scripts/okstra_ctl/wizard.py:774)) — 전체-task·단독-stage 모두 base 가 prepare 에서 자동 해소되므로 위저드가 base 를 물을 필요가 없다.
64
+
65
+ ## 6. 게이트 함수 조정
66
+
67
+ | 함수 | 현재 | 변경 |
68
+ |---|---|---|
69
+ | [`_stage_auto_allowed`](../../../scripts/okstra_ctl/wizard.py:778) | `task_type == "implementation"` | final-verification 에서 "전체 task" 항목 노출을 **전 stage done** 조건으로 허용. (implementation 의 `auto` 와 의미가 다르므로 함수명/의도 재정의 또는 final-verification 전용 헬퍼 신설) |
70
+ | [`_submit_stage_pick`](../../../scripts/okstra_ctl/wizard.py:1539) | `auto` 만 특수 처리, final-verification+auto 거부 | "전체 task" sentinel 수용 |
71
+ | [`render_args`](../../../scripts/okstra_ctl/wizard.py:2799) | final-verification + (빈/auto) → `WizardError` | sentinel → 빈 stage 변환(§5); 단독은 기존대로 명시 번호 |
72
+ | [`confirmation_block`](../../../scripts/okstra_ctl/wizard.py:2872) | stage 값 그대로 표기 | sentinel 을 사람이 읽을 라벨("전체 task")로 표기 |
73
+
74
+ ## 7. 프롬프트 (prompts/wizard/prompts.ko.json)
75
+
76
+ `stage_pick` 프롬프트에 전체-task 항목 라벨과 stage done/미완 마킹 문자열을 추가한다. `54b9482` 가 제거한 `auto` 라벨은 복원하지 않는다 — 새 라벨은 `auto` 가 아닌 "전체 task 검증" 계열 문자열이다.
77
+
78
+ ## 8. 테스트
79
+
80
+ - `tests/test_wizard_stage_pick.py`: final-verification + 전 stage done → "전체 task" 항목 노출 / 일부 미완 → 미노출 + 해당 stage `[미완]` 마킹.
81
+ - `tests/test_wizard_final_verification_stage.py`: "전체 task" 선택 → `render_args` 가 빈 stage emit; 단독 선택 → 명시 번호 emit.
82
+ - `tests/test_okstra_ctl_wizard.py`: 전체-task 경로에서도 base-ref/branch-confirm 단계가 생략되는지(현행 불변 회귀).
83
+ - prepare 측 회귀는 `tests/test_final_verification_target.py` 기존 케이스로 충분(prepare 계약 무변경).
84
+
85
+ ## 9. 비변경 확인 (회귀 가드)
86
+
87
+ - prepare 분기·`_reserve_final_verification_target`·target 해소: 변경 없음.
88
+ - CLI `okstra.sh --stage`: 변경 없음(빈 stage = 전체-task 는 기존 계약).
89
+ - 단독-stage 위저드 UX(`54b9482` 도입분): 변경 없음.
@@ -51,8 +51,9 @@ sequenceDiagram
51
51
  participant WT as worktree registry
52
52
  participant FS as task artifacts
53
53
 
54
- W->>P: task-type=release-handoff, brief, optional pr-template-path
55
- P->>P: validate profile and brief
54
+ W->>P: task-type=release-handoff, approved-plan, stages csv, optional pr-template-path
55
+ P->>P: enforce stage eligibility (consumers verified/pr rows)
56
+ P->>FS: generate release-handoff-input.md (cited verification reports)
56
57
  P->>P: force workers=[]
57
58
  P->>T: resolve PR template
58
59
  P->>WT: reuse or provision task worktree
@@ -67,7 +68,7 @@ profile에 `Required workers:` block이 없고 `run.py`도 `release-handoff`에
67
68
 
68
69
  ```mermaid
69
70
  flowchart TD
70
- Brief[task brief] --> Source{Source Verification Report present?}
71
+ Brief[release-handoff-input.md<br/>generated by prepare] --> Source{Source Verification Report present?}
71
72
  Source -->|no| Block[blocked<br/>route final-verification]
72
73
  Source -->|yes| Verdict{Verdict Token == accepted?}
73
74
  Verdict -->|no| Block
@@ -82,8 +83,8 @@ flowchart TD
82
83
 
83
84
  lead는 사용자에게 push/PR 여부를 묻기 전에 다음을 확인한다.
84
85
 
85
- - task brief가 `## Source Verification Report`를 가리킨다.
86
- - report의 `## 7. Final Verdict`에 `Verdict Token = accepted`가 정확히 있다.
86
+ - prepare생성한 input 문서(`release-handoff-input.md`)의 `## Source Verification Report` 가 모드(`HANDOFF_MODE`)와 인용 보고서 표를 담고 있다. brief 는 entry phase 의 입력물이라 release-handoff 에는 없다 — 사용자의 stage 선택은 wizard `handoff_stage_pick` 또는 CLI `--stages` 로 prepare 전에 끝난다.
87
+ - 인용된 report 의 `## 7. Final Verdict`에 `Verdict Token = accepted`가 정확히 있다.
87
88
  - working tree가 clean이다.
88
89
  - 현재 branch가 `main`, `master`, `prod`, `preprod`, `staging`, `dev` 같은 base branch가 아니다.
89
90
  - `<base>..HEAD` commit range가 비어 있지 않다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.69.0",
3
- "builtAt": "2026-06-10T15:26:59.219Z",
2
+ "package": "0.71.0",
3
+ "builtAt": "2026-06-10T18:46:31.471Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -24,7 +24,10 @@ collect_required_arguments() {
24
24
  prompt_for_required_argument TASK_GROUP "Task Group"
25
25
  prompt_for_required_argument TASK_ID "Task ID"
26
26
  prompt_for_required_argument TASK_TYPE "Task Type"
27
- prompt_for_required_argument BRIEF_PATH "Task Brief Path"
27
+ if [[ "$TASK_TYPE" != "release-handoff" ]]; then
28
+ # release-handoff 는 brief 가 없다 — prepare 가 검증 보고서 인용 input 을 생성.
29
+ prompt_for_required_argument BRIEF_PATH "Task Brief Path"
30
+ fi
28
31
  }
29
32
 
30
33
  require_option_value() {
@@ -103,6 +106,10 @@ while [[ $# -gt 0 ]]; do
103
106
  STAGE="$(require_option_value --stage "${2-}")"
104
107
  shift 2
105
108
  ;;
109
+ --stages)
110
+ STAGES="$(require_option_value --stages "${2-}")"
111
+ shift 2
112
+ ;;
106
113
  --qa-waiver)
107
114
  QA_WAIVER="$(require_option_value --qa-waiver "${2-}")"
108
115
  shift 2
@@ -215,14 +215,16 @@ PY
215
215
  esac
216
216
  done <<<"$autofill_output"
217
217
 
218
- if [[ -z "$BRIEF_PATH" && -n "$manifest_brief" ]]; then
219
- BRIEF_PATH="$manifest_brief"
220
- printf 'autofill: brief-path from task-manifest.json: %s\n' "$BRIEF_PATH" >&2
221
- fi
222
218
  if [[ -z "$TASK_TYPE" && -n "$manifest_type" ]]; then
223
219
  TASK_TYPE="$manifest_type"
224
220
  printf 'autofill: task-type from manifest workflow.nextRecommendedPhase: %s\n' "$TASK_TYPE" >&2
225
221
  fi
222
+ # release-handoff 는 brief 입력이 없다 (prepare 가 검증 보고서 인용 input 을
223
+ # 생성) — task-type autofill 이후에 판정해야 하므로 순서가 위와 바뀌면 안 된다.
224
+ if [[ -z "$BRIEF_PATH" && -n "$manifest_brief" && "$TASK_TYPE" != "release-handoff" ]]; then
225
+ BRIEF_PATH="$manifest_brief"
226
+ printf 'autofill: brief-path from task-manifest.json: %s\n' "$BRIEF_PATH" >&2
227
+ fi
226
228
  }
227
229
 
228
230
  missing_required_arguments_summary() {
@@ -232,7 +234,8 @@ missing_required_arguments_summary() {
232
234
  [[ -z "$TASK_GROUP" ]] && missing+=("<task-group>")
233
235
  [[ -z "$TASK_ID" ]] && missing+=("<task-id>")
234
236
  [[ -z "$TASK_TYPE" ]] && missing+=("<task-type>")
235
- [[ -z "$BRIEF_PATH" ]] && missing+=("<brief-path>")
237
+ # release-handoff brief 입력이 없다 — prepare 가 input 문서를 생성한다.
238
+ [[ -z "$BRIEF_PATH" && "$TASK_TYPE" != "release-handoff" ]] && missing+=("<brief-path>")
236
239
 
237
240
  if (( ${#missing[@]} == 0 )); then
238
241
  printf '%s' ""
@@ -124,6 +124,7 @@ PY_ARGS=(
124
124
  [[ -n "${WORK_CATEGORY-}" ]] && PY_ARGS+=(--work-category "$WORK_CATEGORY")
125
125
  [[ -n "${BASE_REF-}" ]] && PY_ARGS+=(--base-ref "$BASE_REF")
126
126
  [[ -n "${STAGE-}" ]] && PY_ARGS+=(--stage "$STAGE")
127
+ [[ -n "${STAGES-}" ]] && PY_ARGS+=(--stages "$STAGES")
127
128
  [[ -n "${QA_WAIVER-}" ]] && PY_ARGS+=(--qa-waiver "$QA_WAIVER")
128
129
  [[ "$RENDER_ONLY" == "true" ]] && PY_ARGS+=(--render-only)
129
130
  [[ "$PLAN_VERIFICATION_ENABLED" == "false" ]] && PY_ARGS+=(--no-plan-verification)
@@ -11,8 +11,9 @@
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
- - **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.
14
+ - the run's input document (`release-handoff-input.md`, generated by prepare in place of a task brief briefs belong to entry phases only) carries a `## Source Verification Report` section with `Mode`, `Stages`, and one table row per cited `final-verification` final-report. The run context exposes the same selection as `HANDOFF_MODE` (`whole-task` | `stage-group`) and `HANDOFF_STAGES` (csv, empty for whole-task).
15
+ - **whole-task mode** (`HANDOFF_MODE=whole-task`): the lead opens the cited report and confirms its `Verdict Token` is exactly `accepted` and its `verificationScope` is `whole-task`.
16
+ - **stage-group mode** (`HANDOFF_MODE=stage-group`): the lead opens each cited single-stage report and confirms every `Verdict Token` is `accepted`. Eligibility was already enforced at prepare time (`okstra_ctl.handoff.compute_eligibility`) and is re-enforced by `okstra handoff assemble` — the lead never hand-computes it.
16
17
  - 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.
17
18
  - 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.
18
19
  - 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.
@@ -23,11 +24,10 @@
23
24
  - `push + PR` — push the feature branch, then open or reuse a pull request.
24
25
  - `skip` — record the verified state and end the run without any git command.
25
26
  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.
27
+ - **stage-group mode order**: G1 base branch first (same options as Q2 — the dependency-closure check needs `origin/<base>`), then G2 stage confirmation, then assemble, then Q2b/Q3 as usual with the collector branch as the PR head.
28
+ 1g. **G2 — stage confirmation**: the stage selection already happened before the run (wizard `handoff_stage_pick`, or the CLI `--stages` flag) and is fixed in `HANDOFF_STAGES`. Display it as a one-line confirmation (`PR 대상 stage: <csv> — 진행합니다`) and proceed; do NOT re-ask the multi-select. Only if `HANDOFF_MODE` is `stage-group` but `HANDOFF_STAGES` is empty (defensive, should not happen) run `okstra handoff eligible --plan-run-root <plan-run-root> --approved-plan <approved plan path>` and ask the user to pick from the eligible stages.
28
29
  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.
29
30
  2. **PR base branch** (only when the user picked `push + PR`) — present four options and capture exactly one:
30
- - `staging`
31
31
  - `preprod`
32
32
  - `main`
33
33
  - `직접 입력` (free-form branch name; lead validates the name exists on origin via `git ls-remote --heads origin <name>` and re-asks on failure)
@@ -123,6 +123,42 @@
123
123
  "label": "task brief markdown 의 경로를 알려주세요 (project root 기준 상대경로 또는 절대경로)",
124
124
  "echo_template": "brief: {value}"
125
125
  },
126
+ "handoff_stage_pick": {
127
+ "label": "PR 로 내보낼 범위를 선택하세요 (복수 선택 가능 — 검증 accepted + 미-PR stage 만 표시). 제외된 stage: {blocked}",
128
+ "echo_template": "handoff-scope: {value}",
129
+ "labels": {
130
+ "whole_task": "전체 task — whole-task 검증 기반 단일 PR (단독 선택)",
131
+ "stage": "stage {stage} (depends-on: {deps})",
132
+ "blocked_none": "없음"
133
+ },
134
+ "echo_variants": {
135
+ "whole_task": "handoff scope: 전체 task (whole-task 검증 기반)",
136
+ "stage_group": "handoff scope: stage-group ({stages})"
137
+ },
138
+ "errors": {
139
+ "no_plan": "release-handoff 는 이 task 의 implementation-planning final-report 가 필요합니다 — implementation-planning → implementation → final-verification 을 먼저 진행하세요.",
140
+ "plan_not_approved": "최신 plan 이 approved 상태가 아닙니다: {plan} — implementation 이 완료된 task 에서만 release-handoff 를 시작할 수 있습니다.",
141
+ "no_stage_map": "approved plan 에서 Stage Map 을 읽지 못했습니다: {plan}",
142
+ "nothing_eligible": "PR 로 내보낼 수 있는 범위가 없습니다 — 제외 사유: {blocked}. 단독-stage final-verification 의 accepted 기록(okstra handoff record-verified) 또는 whole-task 검증을 먼저 완료하세요.",
143
+ "none_selected": "최소 1개의 범위를 선택하세요",
144
+ "whole_task_exclusive": "'전체 task' 는 단독 선택만 가능합니다 — stage 묶음을 원하면 stage 번호들만 선택하세요",
145
+ "whole_task_missing": "accepted whole-task final-verification 보고서가 없습니다",
146
+ "not_eligible": "eligible 하지 않은 stage: {bad} (선택 가능: {eligible})"
147
+ }
148
+ },
149
+ "brief_carry": {
150
+ "label": "이 task 에 carry-in 할 brief 가 없습니다. {task_type} 는 entry phase 의 brief 를 이어받는 단계입니다 — 어떻게 진행할까요?",
151
+ "echo_template": "brief-carry: {value}",
152
+ "options": {
153
+ "__switch_entry__": "entry phase 로 전환 (추천) — requirements-discovery / error-analysis / improvement-discovery 부터 시작",
154
+ "__free_input__": "brief 경로 직접 입력",
155
+ "__abort__": "중단"
156
+ },
157
+ "echo_variants": {
158
+ "switch_entry": "brief-carry: entry phase 로 전환 — task-type 을 다시 선택합니다",
159
+ "abort": "brief-carry: 중단"
160
+ }
161
+ },
126
162
  "base_ref_pick": {
127
163
  "label": "이 task worktree 의 base branch?",
128
164
  "echo_template": "base-ref: {value}",
@@ -188,7 +224,10 @@
188
224
  "label_final_verification": "검증할 implementation stage 를 선택하세요.",
189
225
  "echo_template": "stage: {value}",
190
226
  "options": {
191
- "auto": "auto (다음 미완료 stage)"
227
+ "auto": "auto (다음 미완료 stage)",
228
+ "whole_task": "전체 task 검증 (모든 stage)",
229
+ "done_mark": "[done]",
230
+ "undone_mark": "[미완]"
192
231
  }
193
232
  },
194
233
  "directive_pick": {
@@ -367,6 +406,9 @@
367
406
  },
368
407
  "confirmation": {
369
408
  "header": "선택 확인:",
370
- "workers_implementation_default": " workers : (프로필 기본 — executor + verifier 2 + report-writer)"
409
+ "workers_implementation_default": " workers : (프로필 기본 — executor + verifier 2 + report-writer)",
410
+ "stage_whole_task": "전체 task",
411
+ "handoff_scope_whole_task": "전체 task (whole-task 검증 기반)",
412
+ "handoff_scope_stage_group": "stage-group ({stages})"
371
413
  }
372
414
  }
@@ -62,6 +62,35 @@ def compute_eligibility(stage_map: List[Dict[str, Any]],
62
62
  return out
63
63
 
64
64
 
65
+ def latest_whole_task_fv_accepted(project_root, project_id: str,
66
+ task_group: str, task_id: str) -> str:
67
+ """accepted whole-task final-verification 보고서 경로. 없으면 ''.
68
+
69
+ 판정의 SSOT 는 final-report 의 data.json 이다 (record_verified 와 동일
70
+ 필드: header.taskType / verificationScope / finalVerdict.verdictToken).
71
+ whole-task run 산출물만 평면 reports/ 에 남으므로 stage-* 는 걸리지 않는다."""
72
+ from okstra_project.state import find_task_root
73
+ root = find_task_root(Path(project_root),
74
+ f"{project_id}:{task_group}:{task_id}")
75
+ if root is None:
76
+ return ""
77
+ reports_dir = root / "runs" / "final-verification" / "reports"
78
+ for dj in sorted(reports_dir.glob("final-report-*.data.json"),
79
+ reverse=True):
80
+ try:
81
+ data = json.loads(dj.read_text(encoding="utf-8"))
82
+ except (OSError, json.JSONDecodeError):
83
+ continue
84
+ token = ((data.get("finalVerdict") or {}).get("verdictToken")
85
+ or "").strip().lower()
86
+ if ((data.get("header") or {}).get("taskType") == "final-verification"
87
+ and data.get("verificationScope") == "whole-task"
88
+ and token == "accepted"):
89
+ md = dj.with_name(dj.name[:-len(".data.json")] + ".md")
90
+ return str(md if md.is_file() else dj)
91
+ return ""
92
+
93
+
65
94
  def check_dependency_closure(
66
95
  selected: List[int],
67
96
  stage_map: List[Dict[str, Any]],
@@ -124,14 +124,17 @@ def compute_run_paths(
124
124
  timeline_file = history_dir / "timeline.json"
125
125
 
126
126
  run_dir = runs_dir / task_type_segment
127
- # implementation stage isolation: each stage's run artifacts live in a
128
- # dedicated `stage-<N>` subtree (mirrors the per-stage worktree) so two
129
- # concurrent `implementation` runs never share reports/state/worker-results.
127
+ # Stage isolation: each stage's run artifacts live in a dedicated
128
+ # `stage-<N>` subtree (mirrors the per-stage worktree) so two concurrent
129
+ # runs of the same task-key never share reports/state/worker-results.
130
+ # Applies to `implementation` and single-stage `final-verification`
131
+ # (whole-task final-verification has stage=None and stays flat).
130
132
  # consumers.jsonl + the worktree registry stay at the task-type level (the
131
133
  # shared stage ledger / occupancy SSOT); they are computed OUTSIDE this
132
134
  # function and are intentionally NOT stage-scoped. Other task-types have no
133
135
  # stage concept, so their run_dir is unchanged.
134
- if task_type_segment == "implementation" and stage is not None:
136
+ if (task_type_segment in ("implementation", "final-verification")
137
+ and stage is not None):
135
138
  run_dir = run_dir / f"stage-{int(stage)}"
136
139
  run_manifests = run_dir / "manifests"
137
140
  run_state = run_dir / "state"
@@ -1681,11 +1681,18 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1681
1681
  )
1682
1682
  else:
1683
1683
  team_name = f'okstra-{ctx.get("TASK_KEY", "")}'
1684
- stage = str(ctx.get("EFFECTIVE_STAGES", "") or "").strip()
1685
- if task_type == "implementation" and stage:
1684
+ impl_stage = str(ctx.get("EFFECTIVE_STAGES", "") or "").strip()
1685
+ fv_stage = str(ctx.get("RUN_STAGE", "") or "").strip()
1686
+ if task_type == "implementation" and impl_stage:
1686
1687
  # stage 격리 run 은 stage 별 team — 같은 task 의 다른 stage 가 남긴
1687
1688
  # team 과 이름이 충돌하지 않는다(worktree branch `-s<N>` 접미사와 동형).
1688
- team_name = f"{team_name}-s{stage}"
1689
+ team_name = f"{team_name}-s{impl_stage}"
1690
+ elif task_type == "final-verification" and fv_stage:
1691
+ # 단일-stage final-verification 도 stage 별 team. `-fv-` 를 끼워
1692
+ # 같은 stage 의 implementation team(`-s<N>`)과도 구분한다 — 둘은
1693
+ # 동시에 살아 있을 수 있다. whole-task 검증(RUN_STAGE 빈 값)은
1694
+ # 기본 이름 유지.
1695
+ team_name = f"{team_name}-fv-s{fv_stage}"
1689
1696
  team_creation_gate_block = (
1690
1697
  "## Team Creation Gate (BLOCKING)\n"
1691
1698
  "\n"