okstra 0.70.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.
- package/docs/kr/architecture.md +5 -1
- package/docs/kr/cli.md +8 -2
- package/docs/superpowers/specs/2026-06-11-brief-entry-only-handoff-stage-entry-design.md +158 -0
- package/docs/task-process/release-handoff.md +6 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +8 -1
- package/runtime/bin/lib/okstra/interactive.sh +8 -5
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/profiles/release-handoff.md +5 -5
- package/runtime/prompts/wizard/prompts.ko.json +39 -1
- package/runtime/python/okstra_ctl/handoff.py +29 -0
- package/runtime/python/okstra_ctl/run.py +167 -7
- package/runtime/python/okstra_ctl/wizard.py +239 -29
- package/runtime/skills/okstra-run/SKILL.md +3 -1
- package/runtime/templates/reports/release-handoff-input.template.md +10 -6
- package/src/render-bundle.mjs +9 -1
package/docs/kr/architecture.md
CHANGED
|
@@ -373,7 +373,9 @@ okstra phase 는 PRD / issue file 을 직접 쓰지 않습니다. 동등한 결
|
|
|
373
373
|
- **whole-task (기본)**: task 전체를 1개 PR 로 내보냅니다. 기존 동작입니다.
|
|
374
374
|
- **stage-group**: 단독-stage final-verification 에서 `accepted` 를 받은 stage 들 중 일부를 골라, 수집(collector) 브랜치로 묶어 1개 PR 로 내보냅니다. task 전체가 끝나기를 기다리지 않고 검증 완료된 stage 묶음 단위로 PR 을 낼 수 있습니다.
|
|
375
375
|
|
|
376
|
-
|
|
376
|
+
진입은 brief 없이 **task-id 기반**입니다 — brief 는 entry phase 의 입력물이고, release-handoff 는 prepare 가 approved plan 의 Stage Map + `consumers.jsonl` 로 자격을 판정한 뒤 검증 보고서를 인용하는 input 문서(`<task_root>/release-handoff-input.md`)를 자동 생성합니다. stage 선택은 okstra-run wizard 의 `handoff_stage_pick` 멀티선택(eligible stage 묶음 / accepted whole-task 보고서가 있으면 전체 task) 또는 CLI `--stages <csv>` 로 들어오고, run-context 의 `HANDOFF_MODE` / `HANDOFF_STAGES` 로 lead 에 노출됩니다.
|
|
377
|
+
|
|
378
|
+
stage-group 의 상호작용 순서: **G1 base 선택 → G2 stage 확인(선택은 prepare 전에 끝남 — 재질문 없음) → assemble(수집 브랜치 생성 + 선택 stage 머지) → 충돌 프로브 → PR 초안 → push/PR**.
|
|
377
379
|
|
|
378
380
|
- 자격 판정의 SSOT 는 `consumers.jsonl` 의 두 행입니다 — `verified`(어떤 stage 가 단독-stage final-verification 에서 accepted 됨), `pr`(어떤 stage 들이 어느 PR 로 나갔는지). `verified` 인데 아직 `pr` 에 안 들어간 stage 가 자격 후보입니다.
|
|
379
381
|
- worktree registry 는 stage-group 점유를 `<task-key>#group-<id>` 키로 예약하고, 수집 브랜치 이름은 `<work-category-prefix>-<task-id-segment>-g2-3` (예: 선택한 stage 가 2·3 이면 `-g2-3`) 형태입니다.
|
|
@@ -680,6 +682,8 @@ canonical metadata는 항상 `task-manifest.json`을 기준으로 확인합니
|
|
|
680
682
|
|
|
681
683
|
`okstra`는 brief-first 구조입니다. brief 는 외부 입력과 okstra 보강을 보존하는 정본 source material 이며, 워커가 필요로 할 추가 자료(보고서, 코드 스니펫, 로그 등)는 brief 내부의 `Evidence and Source Materials` 섹션에 inline 또는 path 로 모두 포함시킵니다.
|
|
682
684
|
|
|
685
|
+
brief 의 **입력 시점은 entry phase 전용**입니다 — 사용자가 brief 경로를 직접 대는 것은 `requirements-discovery` / `error-analysis` / `improvement-discovery` 뿐이고, downstream phase(implementation-planning / implementation / final-verification)는 task manifest 의 `taskBriefPath` 를 자동 carry-in 합니다 (okstra-run wizard 가 묻지 않음; 미등록 시 entry 전환을 추천하는 fallback picker). `release-handoff` 는 brief 자체가 없으며 prepare 가 검증 보고서 인용 input 문서를 생성합니다.
|
|
686
|
+
|
|
683
687
|
brief 는 **translation layer** 입니다 — 외부 입력 (이슈 트래커 ticket, 요구사항 문서, 사용자 메시지) 을 okstra-readable 형식으로 옮기되, 원문은 verbatim 으로 보존하고 okstra 가 추가한 부분은 labelled augmentation 으로 명확히 구분합니다. `okstra-brief` skill 의 산출이 source SSOT 이고, `prepare_task_bundle()` 은 분석 phase 마다 여기서 필요한 frontmatter, task-specific brief 섹션, reference expectations, carry-in clarification, directive 를 추출해 `instruction-set/analysis-packet.md` 를 만듭니다. 분석 워커의 1차 입력은 이 compact packet 이며, 원본 brief 와 profile/material 파일은 근거 확인이나 누락 보완이 필요할 때만 여는 fallback evidence 입니다.
|
|
684
688
|
|
|
685
689
|
brief에는 보통 아래를 포함합니다.
|
package/docs/kr/cli.md
CHANGED
|
@@ -132,6 +132,12 @@ interactive terminal에서 실행하면 다음 규칙이 추가로 적용됩니
|
|
|
132
132
|
분석의 기준이 되는 task brief 문서 경로입니다.
|
|
133
133
|
상대 경로는 대상 프로젝트 루트를 기준으로 해석됩니다.
|
|
134
134
|
|
|
135
|
+
`release-handoff` 에는 적용되지 않습니다 — brief 는 entry phase
|
|
136
|
+
(requirements-discovery / error-analysis / improvement-discovery)의 입력물이고,
|
|
137
|
+
release-handoff 는 prepare 가 검증 보고서를 인용하는 input 문서
|
|
138
|
+
(`<task_root>/release-handoff-input.md`)를 자동 생성해 brief 자리를 채웁니다.
|
|
139
|
+
release-handoff 에 비어 있지 않은 `--task-brief` 를 주면 즉시 거부됩니다.
|
|
140
|
+
|
|
135
141
|
예:
|
|
136
142
|
|
|
137
143
|
- `.project-docs/linear/feature/8858/okstra-task-brief.md`
|
|
@@ -593,9 +599,9 @@ chmod +x ~/.local/bin/okstra-ctl
|
|
|
593
599
|
| `okstra task-show <task-key> [--project-root <path>]` | task-manifest.json 의 workflow / phase / status 요약 |
|
|
594
600
|
| `okstra worktree-lookup <task-key>` | `worktree_registry.lookup` 결과 (예약된 path / branch / base ref / 현재 상태) |
|
|
595
601
|
| `okstra plan-validate <plan-path>` | `_validate_approved_plan` — frontmatter `approved` 인식 결과와 Blocks=approval 미해결 행 진단 |
|
|
596
|
-
| `okstra render-bundle <args…> [--stage <auto\|N>]` | `prepare_task_bundle(render_only=True)` 의 thin shim — `python3 -m okstra_ctl.run --render-only` 와 동일 시그니처. `--stage` 는 `implementation` / `final-verification` 전용: 실행(검증)할 Stage Map 항목 지정. `implementation` 은 `auto` (기본값) = 의존성이 만족된 가장 빠른 미완료 stage, `<N>` = 강제 지정. `final-verification` 은 `<N>` = 해당 stage 단독 검증(산출물이 `runs/final-verification/stage-<N>/` 에 격리되고 팀 이름에 `-fv-s<N>` 접미사), 빈 값 = 전체-task 검증(평면 구조 유지) |
|
|
602
|
+
| `okstra render-bundle <args…> [--stage <auto\|N>] [--stages <csv>]` | `prepare_task_bundle(render_only=True)` 의 thin shim — `python3 -m okstra_ctl.run --render-only` 와 동일 시그니처. `--stage` 는 `implementation` / `final-verification` 전용: 실행(검증)할 Stage Map 항목 지정. `implementation` 은 `auto` (기본값) = 의존성이 만족된 가장 빠른 미완료 stage, `<N>` = 강제 지정. `final-verification` 은 `<N>` = 해당 stage 단독 검증(산출물이 `runs/final-verification/stage-<N>/` 에 격리되고 팀 이름에 `-fv-s<N>` 접미사), 빈 값 = 전체-task 검증(평면 구조 유지). `--stages <csv>` 는 `release-handoff` 전용(별개 채널): PR 로 묶을 stage 번호들(stage-group 모드), 빈 값 = whole-task 모드. prepare 가 eligibility(`done`+`verified` accepted+미-`pr`)를 강제하고 검증 보고서 인용 input 문서를 자동 생성한다 |
|
|
597
603
|
| `okstra render-views <final-report.md>` | Phase 7 step 1.5 — 토큰 치환된 final-report MD 한 본을 입력으로 sibling `*.slim.md` (AI 입력용) + `*.html` (사람용 self-contained) 두 view 를 결정론적으로 생성. 원본 MD 는 수정하지 않음. Node 위임 wrapper는 `scripts/okstra-render-report-views.py` 를 호출. `validators/validate-report-views.py` 가 substring 보존 / form-control 위치 / Response ID parity 를 검사 |
|
|
598
|
-
| `okstra wizard <init\|step\|render-args\|confirmation> --state-file <path>` | okstra-run 인터랙티브 입력 상태머신 (`okstra_ctl.wizard`). `init` 으로 state file 을 시드한 뒤 skill 이 `step --answer <val>` 을 반복 호출하면 다음 `Prompt` JSON 을 받음. `--answer` 는 **필수**. 응답을 주지 않고 다음 prompt 만 미리 보고 싶다면 `--no-submit` 으로 peek. `render-args` 는 최종 `render-bundle` 인자 맵, `confirmation` 은 사용자 echo 블록을 반환. `implementation` task type 에서는 `approved_plan_pick` 직후 `stage_pick` 단계가 추가되어 실행할 stage 를 선택하고, `executor_pick` 으로
|
|
604
|
+
| `okstra wizard <init\|step\|render-args\|confirmation> --state-file <path>` | okstra-run 인터랙티브 입력 상태머신 (`okstra_ctl.wizard`). `init` 으로 state file 을 시드한 뒤 skill 이 `step --answer <val>` 을 반복 호출하면 다음 `Prompt` JSON 을 받음. `--answer` 는 **필수**. 응답을 주지 않고 다음 prompt 만 미리 보고 싶다면 `--no-submit` 으로 peek. `render-args` 는 최종 `render-bundle` 인자 맵, `confirmation` 은 사용자 echo 블록을 반환. `implementation` task type 에서는 `approved_plan_pick` 직후 `stage_pick` 단계가 추가되어 실행할 stage 를 선택하고, `executor_pick` 으로 넘어갑니다. brief 단계는 entry task-type(requirements-discovery / error-analysis / improvement-discovery)에서만 나오며, downstream 은 manifest 의 brief 를 자동 carry-in 하고(미등록 시 `brief_carry` 3-옵션 fallback), `release-handoff` 는 brief 없이 `handoff_stage_pick` 멀티선택(eligible stage 묶음 / 전체 task)으로 진입합니다 |
|
|
599
605
|
| `okstra token-usage ...` | 설치된 `okstra-token-usage.py` 를 감싸 run token usage 수집/치환을 수행. 세션 jsonl 은 기본적으로 `$OKSTRA_HOME/cache/token-usage/` 의 byte cursor 캐시로 증분 스캔하며, `--no-cache` 로 캐시를 우회해 전체 재스캔을 강제할 수 있음(정확성 폴백) |
|
|
600
606
|
|
|
601
607
|
> 모든 subcommand 는 `bin/okstra` 가 spawn 하는 python 헬퍼 (`src/_python-helper.mjs`) 가 `PYTHONPATH` 와 `~/.okstra/lib/python` 을 wire 합니다. 직접 `python3 -m okstra_ctl.*` 으로 호출하면 `PYTHONPATH` 를 사용자가 직접 셋업해야 합니다.
|
|
@@ -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 종단 표기 정합화.
|
|
@@ -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,
|
|
55
|
-
P->>P:
|
|
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[
|
|
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
|
-
-
|
|
86
|
-
-
|
|
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
package/runtime/BUILD.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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' ""
|
package/runtime/bin/okstra.sh
CHANGED
|
@@ -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
|
-
-
|
|
15
|
-
- **
|
|
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
|
|
27
|
-
1g. **G2 — stage
|
|
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}",
|
|
@@ -371,6 +407,8 @@
|
|
|
371
407
|
"confirmation": {
|
|
372
408
|
"header": "선택 확인:",
|
|
373
409
|
"workers_implementation_default": " workers : (프로필 기본 — executor + verifier 2 + report-writer)",
|
|
374
|
-
"stage_whole_task": "전체 task"
|
|
410
|
+
"stage_whole_task": "전체 task",
|
|
411
|
+
"handoff_scope_whole_task": "전체 task (whole-task 검증 기반)",
|
|
412
|
+
"handoff_scope_stage_group": "stage-group ({stages})"
|
|
375
413
|
}
|
|
376
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]],
|
|
@@ -291,6 +291,10 @@ class PrepareInputs:
|
|
|
291
291
|
# prepare-time 에 task-level conformance 매니페스트 entry.waiver 를 채운다.
|
|
292
292
|
qa_waiver: str = ""
|
|
293
293
|
stage: str = "auto"
|
|
294
|
+
# release-handoff 전용: PR 로 내보낼 stage 묶음 (csv, 예: "2,3"). 빈 값 =
|
|
295
|
+
# whole-task 모드. `--stage`(impl/fv 의 Stage Map 실행/검증 선택)와는
|
|
296
|
+
# 별개 채널이다.
|
|
297
|
+
stages: str = ""
|
|
294
298
|
clarification_response_path: str = "" # absolute or empty
|
|
295
299
|
# release-handoff 전용: PR 본문 템플릿 1회성 override. 빈 문자열이면
|
|
296
300
|
# project.json → global config → 스킬 디폴트 순으로 해석된다.
|
|
@@ -1064,7 +1068,17 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
|
|
|
1064
1068
|
를 검증하고, implementation 일 때 stage map 을 파싱해 돌려준다 (그 외엔 빈 리스트)."""
|
|
1065
1069
|
if not project_root.is_dir():
|
|
1066
1070
|
raise PrepareError(f"project root not found: {project_root}")
|
|
1067
|
-
if
|
|
1071
|
+
if inp.stages and inp.task_type != "release-handoff":
|
|
1072
|
+
raise PrepareError(
|
|
1073
|
+
"--stages is only meaningful with --task-type release-handoff; "
|
|
1074
|
+
f"got {inp.task_type}"
|
|
1075
|
+
)
|
|
1076
|
+
if inp.task_type == "release-handoff":
|
|
1077
|
+
# brief 는 entry phase 의 입력물 — release-handoff 는 검증 보고서를
|
|
1078
|
+
# 인용하는 input 문서를 prepare 가 직접 생성해 brief 자리에 채운다
|
|
1079
|
+
# (_materialize_release_handoff_input, 이 검증 직후 실행).
|
|
1080
|
+
pass
|
|
1081
|
+
elif not inp.brief_path.is_file():
|
|
1068
1082
|
raise PrepareError(f"task brief not found: {inp.brief_path}")
|
|
1069
1083
|
ctx_stage_map: list = []
|
|
1070
1084
|
# implementation 과 final-verification 은 둘 다 승인된 plan 의 Stage Map 을
|
|
@@ -1121,7 +1135,9 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
|
|
|
1121
1135
|
"--implementation-option is only meaningful with --task-type "
|
|
1122
1136
|
"implementation and --approved-plan <path>"
|
|
1123
1137
|
)
|
|
1124
|
-
|
|
1138
|
+
# wizard 는 모든 flag 를 빈 값 포함 항상 전달하므로(`--stage ""`),
|
|
1139
|
+
# 빈 문자열은 "stage 미지정" 으로 받아들인다.
|
|
1140
|
+
if inp.stage not in ("", "auto"):
|
|
1125
1141
|
raise PrepareError(
|
|
1126
1142
|
"--stage is only meaningful with --task-type implementation or "
|
|
1127
1143
|
f"final-verification; got {inp.task_type}"
|
|
@@ -1133,6 +1149,113 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
|
|
|
1133
1149
|
return ctx_stage_map
|
|
1134
1150
|
|
|
1135
1151
|
|
|
1152
|
+
def _collect_handoff_source_report_rows(
|
|
1153
|
+
rows: list, nums: list,
|
|
1154
|
+
) -> list:
|
|
1155
|
+
"""선택 stage 들의 마지막 verified 행에서 인용 표 행을 만든다 (last-wins)."""
|
|
1156
|
+
last_verified: dict = {}
|
|
1157
|
+
for r in rows:
|
|
1158
|
+
if r.get("status") == "verified" and isinstance(r.get("stage"), int):
|
|
1159
|
+
last_verified[r["stage"]] = r
|
|
1160
|
+
return [
|
|
1161
|
+
f"| {n} | {last_verified[n].get('report_path', '')} "
|
|
1162
|
+
f"| `{last_verified[n].get('verdict', '')}` |"
|
|
1163
|
+
for n in nums
|
|
1164
|
+
]
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def _materialize_release_handoff_input(
|
|
1168
|
+
workspace_root: Path, project_root: Path, inp: "PrepareInputs",
|
|
1169
|
+
) -> dict:
|
|
1170
|
+
"""release-handoff 의 입력 문서를 검증 보고서 인용으로 자동 생성한다.
|
|
1171
|
+
|
|
1172
|
+
brief 는 entry phase 의 입력물이라 release-handoff 에는 없다 — 이 run 의
|
|
1173
|
+
`--task-brief` 자리는 여기서 생성된 input 문서가 채운다. stage 자격은
|
|
1174
|
+
okstra_ctl.handoff 의 SSOT 판정을 prepare 에서도 그대로 강제한다.
|
|
1175
|
+
반환: ctx 에 올릴 {"HANDOFF_MODE": ..., "HANDOFF_STAGES": ...}."""
|
|
1176
|
+
from .consumers import read_consumers
|
|
1177
|
+
from .handoff import (HandoffError, _require_eligible,
|
|
1178
|
+
latest_whole_task_fv_accepted)
|
|
1179
|
+
from .paths import task_dir
|
|
1180
|
+
from .render import render_template_with_ctx
|
|
1181
|
+
from .run_context import _now_task_date
|
|
1182
|
+
|
|
1183
|
+
# Path("") 는 "." 로 정규화된다 — 빈 brief 센티널은 둘 다로 들어올 수 있다.
|
|
1184
|
+
if str(inp.brief_path).strip() not in ("", "."):
|
|
1185
|
+
raise PrepareError(
|
|
1186
|
+
"--task-brief is not accepted for --task-type release-handoff — "
|
|
1187
|
+
"the input document is generated from the cited final-verification "
|
|
1188
|
+
"reports (pick stages via --stages, or leave it empty for "
|
|
1189
|
+
"whole-task)"
|
|
1190
|
+
)
|
|
1191
|
+
if not inp.approved_plan_path:
|
|
1192
|
+
raise PrepareError(
|
|
1193
|
+
"release-handoff requires --approved-plan "
|
|
1194
|
+
"<implementation-planning final-report> — the Stage Map and the "
|
|
1195
|
+
"consumers ledger drive stage eligibility"
|
|
1196
|
+
)
|
|
1197
|
+
plan = Path(inp.approved_plan_path)
|
|
1198
|
+
if not plan.is_file():
|
|
1199
|
+
raise PrepareError(f"approved plan not found: {plan}")
|
|
1200
|
+
stage_map = _parse_stage_map_into_ctx(str(plan))
|
|
1201
|
+
if not stage_map:
|
|
1202
|
+
raise PrepareError(f"approved plan has no parsable Stage Map: {plan}")
|
|
1203
|
+
rows = read_consumers(plan.resolve().parents[1])
|
|
1204
|
+
|
|
1205
|
+
if inp.stages:
|
|
1206
|
+
try:
|
|
1207
|
+
nums = sorted({int(x) for x in inp.stages.split(",") if x.strip()})
|
|
1208
|
+
except ValueError:
|
|
1209
|
+
raise PrepareError(
|
|
1210
|
+
f"--stages must be a comma-separated int list, got {inp.stages!r}"
|
|
1211
|
+
)
|
|
1212
|
+
if not nums:
|
|
1213
|
+
raise PrepareError("--stages must select at least one stage")
|
|
1214
|
+
try:
|
|
1215
|
+
_require_eligible(stage_map, rows, nums)
|
|
1216
|
+
except HandoffError as exc:
|
|
1217
|
+
raise PrepareError(str(exc)) from exc
|
|
1218
|
+
mode = "stage-group"
|
|
1219
|
+
stages_csv = ",".join(str(n) for n in nums)
|
|
1220
|
+
report_rows = _collect_handoff_source_report_rows(rows, nums)
|
|
1221
|
+
else:
|
|
1222
|
+
report = latest_whole_task_fv_accepted(
|
|
1223
|
+
project_root, inp.project_id, inp.task_group, inp.task_id)
|
|
1224
|
+
if not report:
|
|
1225
|
+
raise PrepareError(
|
|
1226
|
+
"whole-task release-handoff requires an accepted whole-task "
|
|
1227
|
+
"final-verification report — run final-verification first, "
|
|
1228
|
+
"or pass --stages <csv> for a stage-group PR"
|
|
1229
|
+
)
|
|
1230
|
+
mode = "whole-task"
|
|
1231
|
+
stages_csv = ""
|
|
1232
|
+
report_relative = relative_to_project_root(Path(report), project_root)
|
|
1233
|
+
report_rows = [f"| (all) | {report_relative or report} | `accepted` |"]
|
|
1234
|
+
|
|
1235
|
+
template = (workspace_root / "templates" / "reports"
|
|
1236
|
+
/ "release-handoff-input.template.md")
|
|
1237
|
+
if not template.is_file():
|
|
1238
|
+
raise PrepareError(
|
|
1239
|
+
f"release-handoff input template missing: {template}.{_INSTALL_HINT}"
|
|
1240
|
+
)
|
|
1241
|
+
out = (task_dir(project_root, inp.task_group, inp.task_id)
|
|
1242
|
+
/ "release-handoff-input.md")
|
|
1243
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
1244
|
+
render_template_with_ctx(str(template), str(out), {
|
|
1245
|
+
"TASK_KEY": f"{inp.project_id}:{inp.task_group}:{inp.task_id}",
|
|
1246
|
+
"TASK_ID": inp.task_id,
|
|
1247
|
+
"TASK_GROUP": inp.task_group,
|
|
1248
|
+
"PROJECT_ID": inp.project_id,
|
|
1249
|
+
"TASK_TYPE": inp.task_type,
|
|
1250
|
+
"TASK_DATE": _now_task_date(),
|
|
1251
|
+
"HANDOFF_MODE": mode,
|
|
1252
|
+
"HANDOFF_STAGES": stages_csv or "(all)",
|
|
1253
|
+
"HANDOFF_SOURCE_REPORTS": "\n".join(report_rows),
|
|
1254
|
+
})
|
|
1255
|
+
inp.brief_path = out
|
|
1256
|
+
return {"HANDOFF_MODE": mode, "HANDOFF_STAGES": stages_csv}
|
|
1257
|
+
|
|
1258
|
+
|
|
1136
1259
|
def _apply_qa_waiver_if_requested(inp: "PrepareInputs", project_root: Path) -> None:
|
|
1137
1260
|
"""`--qa-waiver` 가 있으면 task-level 매니페스트 entry 의 waiver 를 채운다."""
|
|
1138
1261
|
if not inp.qa_waiver:
|
|
@@ -1822,6 +1945,13 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1822
1945
|
final_report_template = assets.final_report_template
|
|
1823
1946
|
ctx_stage_map = _validate_prepare_inputs(project_root, inp)
|
|
1824
1947
|
|
|
1948
|
+
# release-handoff: 검증 보고서 인용 input 문서를 생성해 brief 자리에 채운다.
|
|
1949
|
+
# 이후의 모든 brief 소비 경로(material/instruction-set 복사)는 그대로 동작한다.
|
|
1950
|
+
handoff_tokens = {"HANDOFF_MODE": "", "HANDOFF_STAGES": ""}
|
|
1951
|
+
if inp.task_type == "release-handoff":
|
|
1952
|
+
handoff_tokens = _materialize_release_handoff_input(
|
|
1953
|
+
workspace_root, project_root, inp)
|
|
1954
|
+
|
|
1825
1955
|
verify_installation(workspace_root)
|
|
1826
1956
|
_register_and_check_project(project_root, inp)
|
|
1827
1957
|
|
|
@@ -1989,6 +2119,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1989
2119
|
"RECOMMENDED_ANALYSERS": selected_reviewers,
|
|
1990
2120
|
"PR_TEMPLATE_PATH": pr_template_path_str,
|
|
1991
2121
|
"PR_TEMPLATE_SOURCE": pr_template_source,
|
|
2122
|
+
**handoff_tokens,
|
|
1992
2123
|
"CLAUDE_SESSION_ID": claude_session_id,
|
|
1993
2124
|
"CLARIFICATION_RESPONSE_PATH": inp.clarification_response_path,
|
|
1994
2125
|
"CLARIFICATION_RESPONSE_RELATIVE_PATH": clarification_relative,
|
|
@@ -2113,7 +2244,14 @@ def main(argv: list[str]) -> int:
|
|
|
2113
2244
|
p.add_argument("--task-group", required=True)
|
|
2114
2245
|
p.add_argument("--task-id", required=True)
|
|
2115
2246
|
p.add_argument("--task-type", required=True)
|
|
2116
|
-
p.add_argument(
|
|
2247
|
+
p.add_argument(
|
|
2248
|
+
"--task-brief", default="", dest="task_brief",
|
|
2249
|
+
help=(
|
|
2250
|
+
"Required for every task-type except release-handoff (whose input "
|
|
2251
|
+
"document is generated by prepare from the cited "
|
|
2252
|
+
"final-verification reports)."
|
|
2253
|
+
),
|
|
2254
|
+
)
|
|
2117
2255
|
p.add_argument("--directive", default="")
|
|
2118
2256
|
p.add_argument("--workers", default="", dest="workers_override")
|
|
2119
2257
|
p.add_argument("--lead-model", default="")
|
|
@@ -2135,6 +2273,15 @@ def main(argv: list[str]) -> int:
|
|
|
2135
2273
|
"consumers.jsonl status:done. Numeric '<N>' = force that stage."
|
|
2136
2274
|
),
|
|
2137
2275
|
)
|
|
2276
|
+
p.add_argument(
|
|
2277
|
+
"--stages", default="", dest="stages",
|
|
2278
|
+
help=(
|
|
2279
|
+
"release-handoff only. Comma-separated stage numbers to bundle "
|
|
2280
|
+
"into one PR (stage-group mode); empty = whole-task mode. "
|
|
2281
|
+
"Distinct from --stage (implementation/final-verification "
|
|
2282
|
+
"Stage Map selection)."
|
|
2283
|
+
),
|
|
2284
|
+
)
|
|
2138
2285
|
p.add_argument(
|
|
2139
2286
|
"--approve",
|
|
2140
2287
|
action="store_true",
|
|
@@ -2206,10 +2353,22 @@ def main(argv: list[str]) -> int:
|
|
|
2206
2353
|
args = p.parse_args(argv)
|
|
2207
2354
|
|
|
2208
2355
|
project_root = Path(args.project_root).expanduser().resolve()
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2356
|
+
if args.task_type == "release-handoff":
|
|
2357
|
+
if args.task_brief:
|
|
2358
|
+
print(
|
|
2359
|
+
"--task-brief is not accepted for --task-type release-handoff "
|
|
2360
|
+
"— the input document is generated from the cited "
|
|
2361
|
+
"final-verification reports",
|
|
2362
|
+
file=__import__("sys").stderr,
|
|
2363
|
+
)
|
|
2364
|
+
return 1
|
|
2365
|
+
brief_abs = Path("") # prepare 가 input 문서를 생성해 채운다
|
|
2366
|
+
else:
|
|
2367
|
+
brief_abs = resolve_user_file(args.task_brief, project_root)
|
|
2368
|
+
if brief_abs is None:
|
|
2369
|
+
print(f"task brief not found: {args.task_brief}",
|
|
2370
|
+
file=__import__("sys").stderr)
|
|
2371
|
+
return 1
|
|
2213
2372
|
clarification_abs = ""
|
|
2214
2373
|
if args.clarification_response_path:
|
|
2215
2374
|
cr = resolve_user_file(args.clarification_response_path, project_root)
|
|
@@ -2244,6 +2403,7 @@ def main(argv: list[str]) -> int:
|
|
|
2244
2403
|
approved_plan_path=args.approved_plan_path,
|
|
2245
2404
|
qa_waiver=args.qa_waiver,
|
|
2246
2405
|
stage=args.stage,
|
|
2406
|
+
stages=args.stages,
|
|
2247
2407
|
clarification_response_path=clarification_abs,
|
|
2248
2408
|
pr_template_path=args.pr_template_path,
|
|
2249
2409
|
render_only=args.render_only,
|
|
@@ -88,6 +88,13 @@ EXECUTORS = ["claude", "codex", "gemini"]
|
|
|
88
88
|
# (or the whole task via `auto`). Both gate the approved-plan + stage steps.
|
|
89
89
|
_STAGE_SCOPED_TASK_TYPES = ("implementation", "final-verification")
|
|
90
90
|
|
|
91
|
+
# brief 는 entry phase 의 입력물(비개발자/타 팀의 아이디어·에러 초안)이다.
|
|
92
|
+
# downstream phase 는 task manifest 의 taskBriefPath 를 자동 carry-in 하며
|
|
93
|
+
# 위저드에서 brief 를 묻지 않는다 (release-handoff 는 prepare 가 검증 보고서
|
|
94
|
+
# 인용 input 문서를 자동 생성하므로 brief 자체가 없다).
|
|
95
|
+
_BRIEF_ENTRY_TASK_TYPES = ("requirements-discovery", "improvement-discovery",
|
|
96
|
+
"error-analysis")
|
|
97
|
+
|
|
91
98
|
CANONICAL_BASE_REFS = ["main", "dev", "staging", "preprod", "prod"]
|
|
92
99
|
BASE_REF_FREE_INPUT_TOKEN = "__free_input__"
|
|
93
100
|
|
|
@@ -230,12 +237,14 @@ S_TASK_TYPE_TEXT = "task_type_text"
|
|
|
230
237
|
S_BRIEF_KEEP = "brief_keep"
|
|
231
238
|
S_BRIEF_PATH_PICK = "brief_path_pick"
|
|
232
239
|
S_BRIEF_PATH = "brief_path"
|
|
240
|
+
S_BRIEF_CARRY = "brief_carry"
|
|
233
241
|
S_BASE_REF_PICK = "base_ref_pick"
|
|
234
242
|
S_BASE_REF_TEXT = "base_ref_text"
|
|
235
243
|
S_APPROVED_PLAN_PICK = "approved_plan_pick"
|
|
236
244
|
S_APPROVED_PLAN = "approved_plan"
|
|
237
245
|
S_APPROVE_PLAN_CONFIRM = "approve_plan_confirm"
|
|
238
246
|
S_STAGE_PICK = "stage_pick"
|
|
247
|
+
S_HANDOFF_STAGE_PICK = "handoff_stage_pick"
|
|
239
248
|
S_EXECUTOR = "executor"
|
|
240
249
|
S_CRITIC_PICK = "critic_pick"
|
|
241
250
|
S_CRITIC_TEXT = "critic_text"
|
|
@@ -334,6 +343,11 @@ class WizardState:
|
|
|
334
343
|
critic: str = ""
|
|
335
344
|
critic_pending_text: bool = False
|
|
336
345
|
|
|
346
|
+
# release-handoff: PR 로 내보낼 범위. mode 는 stages 선택의 파생값이다 —
|
|
347
|
+
# whole-task = 빈 stages(whole-task 검증 기반 단일 PR), stage-group = csv.
|
|
348
|
+
handoff_mode: str = "" # "" | "whole-task" | "stage-group"
|
|
349
|
+
handoff_stages: str = "" # csv ("2,3"), whole-task 면 ""
|
|
350
|
+
|
|
337
351
|
# resume: 직전 run-inputs 재사용 여부 (None=미응답, True=재사용, False=재입력)
|
|
338
352
|
reuse_previous: Optional[bool] = None
|
|
339
353
|
|
|
@@ -767,6 +781,11 @@ def _base_ref_required(state: WizardState) -> bool:
|
|
|
767
781
|
return state.task_type != "final-verification" and state.reuse_worktree is False
|
|
768
782
|
|
|
769
783
|
|
|
784
|
+
def _brief_resolved(state: WizardState) -> bool:
|
|
785
|
+
"""brief 입력이 끝났는가. release-handoff 는 brief 가 없는 phase 라 항상 True."""
|
|
786
|
+
return bool(state.brief_path) or state.task_type == "release-handoff"
|
|
787
|
+
|
|
788
|
+
|
|
770
789
|
def _base_ref_ready(state: WizardState) -> bool:
|
|
771
790
|
return not _base_ref_required(state) or S_BASE_REF_PICK in state.answered
|
|
772
791
|
|
|
@@ -1215,6 +1234,23 @@ def _build_task_type(state: WizardState) -> Prompt:
|
|
|
1215
1234
|
echo_template=t["echo_template"])
|
|
1216
1235
|
|
|
1217
1236
|
|
|
1237
|
+
def _carry_in_existing_brief(state: WizardState) -> str:
|
|
1238
|
+
"""downstream task-type 이 등록된 brief 를 자동 carry-in 한다. 주입한 경로(또는 '')."""
|
|
1239
|
+
if state.task_type in _BRIEF_ENTRY_TASK_TYPES:
|
|
1240
|
+
return ""
|
|
1241
|
+
if state.task_type == "release-handoff":
|
|
1242
|
+
return "" # prepare 가 검증 보고서 인용 input 문서를 자동 생성한다
|
|
1243
|
+
if state.brief_path or not state.existing_brief_path:
|
|
1244
|
+
return ""
|
|
1245
|
+
p = Path(state.existing_brief_path)
|
|
1246
|
+
if not p.is_absolute():
|
|
1247
|
+
p = Path(state.project_root) / p
|
|
1248
|
+
if not p.is_file():
|
|
1249
|
+
return "" # manifest 경로가 깨졌으면 brief_carry 단계가 처리한다
|
|
1250
|
+
state.brief_path = state.existing_brief_path
|
|
1251
|
+
return state.brief_path
|
|
1252
|
+
|
|
1253
|
+
|
|
1218
1254
|
def _apply_task_type(state: WizardState, value: str) -> str:
|
|
1219
1255
|
if value not in TASK_TYPE_VALUES:
|
|
1220
1256
|
raise WizardError(
|
|
@@ -1222,6 +1258,11 @@ def _apply_task_type(state: WizardState, value: str) -> str:
|
|
|
1222
1258
|
f"(expected one of: {', '.join(TASK_TYPE_VALUES)})"
|
|
1223
1259
|
)
|
|
1224
1260
|
state.task_type = value
|
|
1261
|
+
# brief_carry 의 "entry phase 로 전환" 이 task-type 을 리셋한 뒤 submit() 이
|
|
1262
|
+
# brief_carry 를 answered 로 되돌려 놓는다 — task-type 을 다시 고르는 시점에
|
|
1263
|
+
# 퍼지해야 downstream 재선택 시 carry 단계가 다시 나온다.
|
|
1264
|
+
state.answered = [a for a in state.answered if a != S_BRIEF_CARRY]
|
|
1265
|
+
carried = _carry_in_existing_brief(state)
|
|
1225
1266
|
state.profile_workers = _load_profile_workers(
|
|
1226
1267
|
Path(state.workspace_root), value
|
|
1227
1268
|
)
|
|
@@ -1231,6 +1272,8 @@ def _apply_task_type(state: WizardState, value: str) -> str:
|
|
|
1231
1272
|
# Reuse-worktree is decided once identity is final. Recompute here so
|
|
1232
1273
|
# subsequent base-ref step knows whether to apply.
|
|
1233
1274
|
state.reuse_worktree = _resolve_reuse_worktree(state)
|
|
1275
|
+
if carried:
|
|
1276
|
+
return f"task-type: {value}\nbrief (carry-in): {carried}"
|
|
1234
1277
|
return f"task-type: {value}"
|
|
1235
1278
|
|
|
1236
1279
|
|
|
@@ -1357,6 +1400,37 @@ def _submit_brief_path(state: WizardState, value: str) -> Optional[str]:
|
|
|
1357
1400
|
return f"brief: {p}"
|
|
1358
1401
|
|
|
1359
1402
|
|
|
1403
|
+
BRIEF_CARRY_SWITCH_ENTRY = "__switch_entry__"
|
|
1404
|
+
BRIEF_CARRY_ABORT = "__abort__"
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
def _build_brief_carry(state: WizardState) -> Prompt:
|
|
1408
|
+
t = _p(state.workspace_root, "brief_carry", task_type=state.task_type)
|
|
1409
|
+
return Prompt(
|
|
1410
|
+
step=S_BRIEF_CARRY, kind="pick",
|
|
1411
|
+
label=t["label"],
|
|
1412
|
+
options=[_opt(k, v) for k, v in t["options"].items()],
|
|
1413
|
+
echo_template=t["echo_template"],
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
def _submit_brief_carry(state: WizardState, value: str) -> Optional[str]:
|
|
1418
|
+
t = _p(state.workspace_root, "brief_carry", task_type=state.task_type)
|
|
1419
|
+
if value == BRIEF_CARRY_SWITCH_ENTRY:
|
|
1420
|
+
_reset_from(state, S_TASK_TYPE)
|
|
1421
|
+
return t["echo_variants"]["switch_entry"]
|
|
1422
|
+
if value == PICK_TYPE_CUSTOM:
|
|
1423
|
+
state.brief_path_pending_text = True
|
|
1424
|
+
return None
|
|
1425
|
+
if value == BRIEF_CARRY_ABORT:
|
|
1426
|
+
state.aborted = True
|
|
1427
|
+
return t["echo_variants"]["abort"]
|
|
1428
|
+
raise WizardError(
|
|
1429
|
+
f"expected '{BRIEF_CARRY_SWITCH_ENTRY}', {PICK_TYPE_CUSTOM!r}, "
|
|
1430
|
+
f"or '{BRIEF_CARRY_ABORT}', got: {value!r}"
|
|
1431
|
+
)
|
|
1432
|
+
|
|
1433
|
+
|
|
1360
1434
|
def _build_base_ref_pick(state: WizardState) -> Prompt:
|
|
1361
1435
|
t = _p(state.workspace_root, "base_ref_pick")
|
|
1362
1436
|
recommended_suffix = t["options"].get("_RECOMMENDED_SUFFIX", "")
|
|
@@ -1602,6 +1676,105 @@ def _submit_stage_pick(state: WizardState, answer: str) -> Optional[str]:
|
|
|
1602
1676
|
return f"stage: {answer}"
|
|
1603
1677
|
|
|
1604
1678
|
|
|
1679
|
+
def _handoff_msgs(state: WizardState) -> dict:
|
|
1680
|
+
"""handoff_stage_pick 의 JSON 텍스트 묶음 (label 미사용 조회용)."""
|
|
1681
|
+
return _p(state.workspace_root, "handoff_stage_pick", blocked="")
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
def _resolve_handoff_plan(state: WizardState) -> Path:
|
|
1685
|
+
"""release-handoff 의 approved plan 을 질문 없이 자동 해소한다.
|
|
1686
|
+
|
|
1687
|
+
plan 미존재/미승인은 사용자가 고칠 대상이 아니라 라이프사이클 선행 단계
|
|
1688
|
+
누락이므로 picker 대신 안내 메시지로 즉시 실패한다."""
|
|
1689
|
+
if state.approved_plan_path:
|
|
1690
|
+
return Path(state.approved_plan_path)
|
|
1691
|
+
t = _handoff_msgs(state)
|
|
1692
|
+
latest = _latest_implementation_planning_report(state)
|
|
1693
|
+
if latest is None:
|
|
1694
|
+
raise WizardError(t["errors"]["no_plan"])
|
|
1695
|
+
p, fully_approved = _classify_approved_plan(
|
|
1696
|
+
str(latest), Path(state.project_root))
|
|
1697
|
+
if not fully_approved:
|
|
1698
|
+
raise WizardError(t["errors"]["plan_not_approved"].format(plan=p))
|
|
1699
|
+
state.approved_plan_path = str(p)
|
|
1700
|
+
return p
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
def _handoff_eligibility(state: WizardState) -> list:
|
|
1704
|
+
"""stage 별 PR 자격 — okstra_ctl.handoff 의 SSOT 판정을 그대로 재사용한다."""
|
|
1705
|
+
from okstra_ctl.handoff import compute_eligibility
|
|
1706
|
+
from okstra_ctl.run import _parse_stage_map_into_ctx
|
|
1707
|
+
from okstra_ctl.consumers import read_consumers
|
|
1708
|
+
plan = _resolve_handoff_plan(state)
|
|
1709
|
+
stage_map = _parse_stage_map_into_ctx(str(plan))
|
|
1710
|
+
if not stage_map:
|
|
1711
|
+
raise WizardError(
|
|
1712
|
+
_handoff_msgs(state)["errors"]["no_stage_map"].format(plan=plan))
|
|
1713
|
+
rows = read_consumers(plan.resolve().parents[1])
|
|
1714
|
+
return compute_eligibility(stage_map, rows)
|
|
1715
|
+
|
|
1716
|
+
|
|
1717
|
+
def _latest_whole_task_fv_accepted(state: WizardState) -> str:
|
|
1718
|
+
"""accepted whole-task final-verification 보고서 경로 — handoff 모듈 SSOT 위임."""
|
|
1719
|
+
from okstra_ctl.handoff import latest_whole_task_fv_accepted
|
|
1720
|
+
return latest_whole_task_fv_accepted(
|
|
1721
|
+
state.project_root, state.project_id, state.task_group, state.task_id)
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
def _build_handoff_stage_pick(state: WizardState) -> Prompt:
|
|
1725
|
+
elig = _handoff_eligibility(state)
|
|
1726
|
+
eligible = [e for e in elig if e["eligible"]]
|
|
1727
|
+
blocked = [e for e in elig if not e["eligible"]]
|
|
1728
|
+
whole_task_report = _latest_whole_task_fv_accepted(state)
|
|
1729
|
+
msgs = _handoff_msgs(state)
|
|
1730
|
+
blocked_summary = ("; ".join(
|
|
1731
|
+
f"stage {e['stage']} ({', '.join(e['reasons'])})" for e in blocked)
|
|
1732
|
+
or msgs["labels"]["blocked_none"])
|
|
1733
|
+
if not eligible and not whole_task_report:
|
|
1734
|
+
raise WizardError(
|
|
1735
|
+
msgs["errors"]["nothing_eligible"].format(blocked=blocked_summary))
|
|
1736
|
+
t = _p(state.workspace_root, "handoff_stage_pick", blocked=blocked_summary)
|
|
1737
|
+
options: list[Option] = []
|
|
1738
|
+
if whole_task_report:
|
|
1739
|
+
options.append(_opt(WHOLE_TASK_STAGE, t["labels"]["whole_task"]))
|
|
1740
|
+
stage_label = t["labels"]["stage"]
|
|
1741
|
+
for e in eligible:
|
|
1742
|
+
deps = ", ".join(str(d) for d in e["depends_on"]) or "-"
|
|
1743
|
+
options.append(_opt(str(e["stage"]),
|
|
1744
|
+
stage_label.format(stage=e["stage"], deps=deps)))
|
|
1745
|
+
return Prompt(
|
|
1746
|
+
step=S_HANDOFF_STAGE_PICK, kind="pick", multi=True,
|
|
1747
|
+
label=t["label"], options=options,
|
|
1748
|
+
echo_template=t["echo_template"],
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
def _submit_handoff_stage_pick(state: WizardState, value: str) -> Optional[str]:
|
|
1753
|
+
t = _handoff_msgs(state)
|
|
1754
|
+
picks = [v.strip() for v in (value or "").split(",") if v.strip()]
|
|
1755
|
+
if not picks:
|
|
1756
|
+
raise WizardError(t["errors"]["none_selected"])
|
|
1757
|
+
if WHOLE_TASK_STAGE in picks:
|
|
1758
|
+
if len(picks) > 1:
|
|
1759
|
+
raise WizardError(t["errors"]["whole_task_exclusive"])
|
|
1760
|
+
if not _latest_whole_task_fv_accepted(state):
|
|
1761
|
+
raise WizardError(t["errors"]["whole_task_missing"])
|
|
1762
|
+
state.handoff_mode = "whole-task"
|
|
1763
|
+
state.handoff_stages = ""
|
|
1764
|
+
return t["echo_variants"]["whole_task"]
|
|
1765
|
+
eligible = {str(e["stage"]) for e in _handoff_eligibility(state)
|
|
1766
|
+
if e["eligible"]}
|
|
1767
|
+
bad = [p for p in picks if p not in eligible]
|
|
1768
|
+
if bad:
|
|
1769
|
+
raise WizardError(t["errors"]["not_eligible"].format(
|
|
1770
|
+
bad=", ".join(bad), eligible=", ".join(sorted(eligible))))
|
|
1771
|
+
nums = sorted({int(p) for p in picks})
|
|
1772
|
+
state.handoff_mode = "stage-group"
|
|
1773
|
+
state.handoff_stages = ",".join(str(n) for n in nums)
|
|
1774
|
+
return t["echo_variants"]["stage_group"].format(
|
|
1775
|
+
stages=state.handoff_stages)
|
|
1776
|
+
|
|
1777
|
+
|
|
1605
1778
|
def _suggest_latest_final_report(state: WizardState) -> str:
|
|
1606
1779
|
"""task 의 모든 phase runs 디렉토리에서 가장 최근 final-report-*.md 의 relpath 를 반환.
|
|
1607
1780
|
|
|
@@ -2364,16 +2537,40 @@ STEPS: list[Step] = [
|
|
|
2364
2537
|
and s.task_group_pending_text),
|
|
2365
2538
|
build=_build_task_group_text, submit=_submit_task_group_text,
|
|
2366
2539
|
owns=("task_group", "task_group_pending_text")),
|
|
2540
|
+
# 신규 task 흐름 순서: task-group → task-type → (entry 면 brief) → task-id.
|
|
2541
|
+
# brief 는 entry phase 전용 입력이므로 task-type 이 정해진 뒤에만 물을 수 있다.
|
|
2542
|
+
Step(S_TASK_TYPE,
|
|
2543
|
+
applies=lambda s: (s.is_new_task is not None
|
|
2544
|
+
and (s.is_new_task is False or bool(s.task_group))
|
|
2545
|
+
and S_TASK_TYPE not in s.answered),
|
|
2546
|
+
build=_build_task_type, submit=_submit_task_type,
|
|
2547
|
+
owns=("task_type", "profile_workers", "profile_optional_workers",
|
|
2548
|
+
"reuse_worktree")),
|
|
2549
|
+
Step(S_TASK_TYPE_TEXT,
|
|
2550
|
+
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
2551
|
+
and not s.task_type
|
|
2552
|
+
and S_TASK_TYPE_TEXT not in s.answered),
|
|
2553
|
+
build=_build_task_type_text, submit=_submit_task_type_text,
|
|
2554
|
+
owns=("task_type", "profile_workers", "profile_optional_workers",
|
|
2555
|
+
"reuse_worktree")),
|
|
2556
|
+
Step(S_BRIEF_KEEP,
|
|
2557
|
+
applies=lambda s: (not s.is_new_task
|
|
2558
|
+
and s.task_type in _BRIEF_ENTRY_TASK_TYPES
|
|
2559
|
+
and bool(s.existing_brief_path)
|
|
2560
|
+
and s.keep_existing_brief is None
|
|
2561
|
+
and S_TASK_TYPE in s.answered),
|
|
2562
|
+
build=_build_brief_keep, submit=_submit_brief_keep,
|
|
2563
|
+
owns=("keep_existing_brief",)),
|
|
2367
2564
|
Step(S_BRIEF_PATH_PICK,
|
|
2368
2565
|
applies=lambda s: (
|
|
2369
2566
|
not s.brief_path
|
|
2370
2567
|
and not s.brief_path_pending_text
|
|
2371
2568
|
and S_BRIEF_PATH_PICK not in s.answered
|
|
2569
|
+
and S_TASK_TYPE in s.answered
|
|
2570
|
+
and s.task_type in _BRIEF_ENTRY_TASK_TYPES
|
|
2372
2571
|
and (
|
|
2373
|
-
|
|
2572
|
+
s.is_new_task is True
|
|
2374
2573
|
or (s.is_new_task is False
|
|
2375
|
-
and S_TASK_TYPE in s.answered
|
|
2376
|
-
and bool(s.task_type)
|
|
2377
2574
|
and (s.keep_existing_brief is False
|
|
2378
2575
|
or not s.existing_brief_path))
|
|
2379
2576
|
)
|
|
@@ -2385,49 +2582,39 @@ STEPS: list[Step] = [
|
|
|
2385
2582
|
and S_BRIEF_PATH not in s.answered,
|
|
2386
2583
|
build=_build_brief_path, submit=_submit_brief_path,
|
|
2387
2584
|
owns=("brief_path", "task_group_suggestion", "task_id_suggestion")),
|
|
2585
|
+
# downstream task-type 인데 carry-in 할 brief 가 없을 때의 fallback.
|
|
2586
|
+
# release-handoff 는 brief 자체가 없는 phase 라 대상에서 빠진다.
|
|
2587
|
+
Step(S_BRIEF_CARRY,
|
|
2588
|
+
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
2589
|
+
and bool(s.task_type)
|
|
2590
|
+
and s.task_type not in _BRIEF_ENTRY_TASK_TYPES
|
|
2591
|
+
and s.task_type != "release-handoff"
|
|
2592
|
+
and not s.brief_path
|
|
2593
|
+
and not s.brief_path_pending_text),
|
|
2594
|
+
build=_build_brief_carry, submit=_submit_brief_carry,
|
|
2595
|
+
owns=("brief_path_pending_text",)),
|
|
2388
2596
|
Step(S_TASK_ID,
|
|
2389
2597
|
applies=lambda s: (bool(s.is_new_task)
|
|
2390
|
-
and bool(s.brief_path)
|
|
2391
2598
|
and bool(s.task_group)
|
|
2599
|
+
and _brief_resolved(s)
|
|
2392
2600
|
and not s.task_id
|
|
2393
2601
|
and not s.task_id_pending_text),
|
|
2394
2602
|
build=_build_task_id, submit=_submit_task_id,
|
|
2395
2603
|
owns=("task_id", "task_id_pending_text")),
|
|
2396
2604
|
Step(S_TASK_ID_TEXT,
|
|
2397
2605
|
applies=lambda s: (bool(s.is_new_task)
|
|
2398
|
-
and bool(s.brief_path)
|
|
2399
2606
|
and bool(s.task_group)
|
|
2607
|
+
and _brief_resolved(s)
|
|
2400
2608
|
and not s.task_id
|
|
2401
2609
|
and s.task_id_pending_text),
|
|
2402
2610
|
build=_build_task_id_text, submit=_submit_task_id_text,
|
|
2403
2611
|
owns=("task_id", "task_id_pending_text")),
|
|
2404
|
-
Step(S_TASK_TYPE,
|
|
2405
|
-
applies=lambda s: (s.is_new_task is not None
|
|
2406
|
-
and (s.is_new_task is False or bool(s.task_id))
|
|
2407
|
-
and S_TASK_TYPE not in s.answered),
|
|
2408
|
-
build=_build_task_type, submit=_submit_task_type,
|
|
2409
|
-
owns=("task_type", "profile_workers", "profile_optional_workers",
|
|
2410
|
-
"reuse_worktree")),
|
|
2411
|
-
Step(S_TASK_TYPE_TEXT,
|
|
2412
|
-
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
2413
|
-
and not s.task_type
|
|
2414
|
-
and S_TASK_TYPE_TEXT not in s.answered),
|
|
2415
|
-
build=_build_task_type_text, submit=_submit_task_type_text,
|
|
2416
|
-
owns=("task_type", "profile_workers", "profile_optional_workers",
|
|
2417
|
-
"reuse_worktree")),
|
|
2418
|
-
Step(S_BRIEF_KEEP,
|
|
2419
|
-
applies=lambda s: (not s.is_new_task
|
|
2420
|
-
and bool(s.existing_brief_path)
|
|
2421
|
-
and s.keep_existing_brief is None
|
|
2422
|
-
and S_TASK_TYPE in s.answered
|
|
2423
|
-
and bool(s.task_type)),
|
|
2424
|
-
build=_build_brief_keep, submit=_submit_brief_keep,
|
|
2425
|
-
owns=("keep_existing_brief",)),
|
|
2426
2612
|
Step(S_BASE_REF_PICK,
|
|
2427
2613
|
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
2428
2614
|
and _base_ref_required(s)
|
|
2429
2615
|
and S_BASE_REF_PICK not in s.answered
|
|
2430
|
-
and
|
|
2616
|
+
and _brief_resolved(s)
|
|
2617
|
+
and (not s.is_new_task or bool(s.task_id))),
|
|
2431
2618
|
build=_build_base_ref_pick, submit=_submit_base_ref_pick,
|
|
2432
2619
|
owns=("base_ref", "base_ref_pending_text")),
|
|
2433
2620
|
Step(S_BASE_REF_TEXT,
|
|
@@ -2468,6 +2655,13 @@ STEPS: list[Step] = [
|
|
|
2468
2655
|
and S_STAGE_PICK not in s.answered),
|
|
2469
2656
|
build=_build_stage_pick, submit=_submit_stage_pick,
|
|
2470
2657
|
owns=("selected_stage",)),
|
|
2658
|
+
Step(S_HANDOFF_STAGE_PICK,
|
|
2659
|
+
applies=lambda s: (s.task_type == "release-handoff"
|
|
2660
|
+
and bool(s.task_group)
|
|
2661
|
+
and bool(s.task_id)
|
|
2662
|
+
and S_HANDOFF_STAGE_PICK not in s.answered),
|
|
2663
|
+
build=_build_handoff_stage_pick, submit=_submit_handoff_stage_pick,
|
|
2664
|
+
owns=("handoff_mode", "handoff_stages", "approved_plan_path")),
|
|
2471
2665
|
Step(S_EXECUTOR,
|
|
2472
2666
|
applies=lambda s: (s.task_type == "implementation"
|
|
2473
2667
|
and bool(s.approved_plan_path)
|
|
@@ -2644,7 +2838,8 @@ def _identity_ready(s: WizardState) -> bool:
|
|
|
2644
2838
|
"""All identity questions (task pick → executor for impl) answered."""
|
|
2645
2839
|
if not s.task_type:
|
|
2646
2840
|
return False
|
|
2647
|
-
|
|
2841
|
+
# release-handoff 는 brief 가 없다 — prepare 가 검증 보고서 인용 input 을 생성한다.
|
|
2842
|
+
if not s.brief_path and s.task_type != "release-handoff":
|
|
2648
2843
|
return False
|
|
2649
2844
|
if _base_ref_required(s) and S_BASE_REF_PICK not in s.answered:
|
|
2650
2845
|
return False
|
|
@@ -2656,6 +2851,9 @@ def _identity_ready(s: WizardState) -> bool:
|
|
|
2656
2851
|
if s.task_type == "implementation":
|
|
2657
2852
|
if not s.executor:
|
|
2658
2853
|
return False
|
|
2854
|
+
if (s.task_type == "release-handoff"
|
|
2855
|
+
and S_HANDOFF_STAGE_PICK not in s.answered):
|
|
2856
|
+
return False
|
|
2659
2857
|
return True
|
|
2660
2858
|
|
|
2661
2859
|
|
|
@@ -2708,6 +2906,7 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
2708
2906
|
"base_ref_pending_text": False, "approved_plan_path": "",
|
|
2709
2907
|
"approved_plan_pending_text": False,
|
|
2710
2908
|
"selected_stage": "auto",
|
|
2909
|
+
"handoff_mode": "", "handoff_stages": "",
|
|
2711
2910
|
"executor": "", "critic": "", "critic_pending_text": False,
|
|
2712
2911
|
"reuse_previous": None,
|
|
2713
2912
|
"use_defaults": None, "workers_override": "",
|
|
@@ -2873,6 +3072,7 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
2873
3072
|
"critic": state.critic,
|
|
2874
3073
|
"approved-plan": state.approved_plan_path,
|
|
2875
3074
|
"stage": stage,
|
|
3075
|
+
"stages": state.handoff_stages,
|
|
2876
3076
|
"base-ref": base_ref,
|
|
2877
3077
|
"workers": workers,
|
|
2878
3078
|
"directive": state.directive,
|
|
@@ -2934,6 +3134,16 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
2934
3134
|
lines.append(f" stage : {stage}")
|
|
2935
3135
|
if state.clarification_response_path:
|
|
2936
3136
|
lines.append(f" clarification : {state.clarification_response_path}")
|
|
3137
|
+
if state.task_type == "release-handoff" and state.handoff_mode:
|
|
3138
|
+
scope = (
|
|
3139
|
+
_msg(state.workspace_root, "confirmation",
|
|
3140
|
+
"handoff_scope_whole_task")
|
|
3141
|
+
if state.handoff_mode == "whole-task"
|
|
3142
|
+
else _msg(state.workspace_root, "confirmation",
|
|
3143
|
+
"handoff_scope_stage_group",
|
|
3144
|
+
stages=state.handoff_stages)
|
|
3145
|
+
)
|
|
3146
|
+
lines.append(f" handoff scope : {scope}")
|
|
2937
3147
|
if state.task_type == "release-handoff" and state.pr_template_path:
|
|
2938
3148
|
lines.append(f" pr-template : {state.pr_template_path} ({state.pr_template_scope or 'once'})")
|
|
2939
3149
|
return "\n".join(lines)
|
|
@@ -121,9 +121,10 @@ That is the entire interactive flow. The wizard handles:
|
|
|
121
121
|
|
|
122
122
|
- new-vs-existing task split (남은 작업 — `workStatus != done` — 최신순 3개 추천 + 직접 입력), task-group / task-id slug validation (각각 최근 후보 3개 추천 + 직접 입력),
|
|
123
123
|
- task-type pick (추천 3개 — `nextRecommendedPhase` recommended / 현재 phase 재실행 / 라이프사이클 다음 단계 — + 직접 입력; 직접 입력은 후속 `text` 단계에서 전체 task-type 화이트리스트로 검증),
|
|
124
|
-
- brief path
|
|
124
|
+
- brief path — **entry task-type(requirements-discovery / error-analysis / improvement-discovery)에서만 질문** (same-group `.okstra/briefs/<task-group>/**/*.md` candidates first, direct input last; `유지 / 변경` for existing entry tasks). downstream task-type 은 manifest 의 brief 를 자동 carry-in 하고, 등록 brief 가 없으면 `brief_carry` 3-옵션(entry 전환 추천 / 직접 입력 / 중단)이 뜬다. `release-handoff` 는 brief 단계가 아예 없다 — prepare 가 검증 보고서 인용 input 문서를 생성한다,
|
|
125
125
|
- base-ref pick + git rev-parse validation (skipped when reusing an active worktree),
|
|
126
126
|
- `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + stage pick (`auto` = 의존성 충족된 가장 빠른 미완료 stage, 또는 특정 stage 번호) + executor pick,
|
|
127
|
+
- `release-handoff`-only sub-flow: approved plan 자동 해소 후 `handoff_stage_pick` 멀티선택 — eligible stage 묶음(stage-group) 또는 전체 task(accepted whole-task 검증 보고서 존재 시) 선택; 결과는 render-args 의 `stages` 키(csv, whole-task 면 빈 값)로 나간다,
|
|
127
128
|
- `Use defaults / Customize` branch with profile-aware worker/model questions,
|
|
128
129
|
- `release-handoff` PR template override + persist scope,
|
|
129
130
|
- final `Proceed / Edit` confirmation; on `Edit` the wizard asks which step to rewind to and clears every later answer.
|
|
@@ -166,6 +167,7 @@ okstra render-bundle \
|
|
|
166
167
|
--critic "<args.critic>" \
|
|
167
168
|
--approved-plan "<args.approved-plan>" \
|
|
168
169
|
--stage "<args.stage>" \
|
|
170
|
+
--stages "<args.stages>" \
|
|
169
171
|
--base-ref "<args.base-ref>" \
|
|
170
172
|
--workers "<args.workers>" \
|
|
171
173
|
--directive "<args.directive>" \
|
|
@@ -25,11 +25,15 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
25
25
|
|
|
26
26
|
## Source Verification Report
|
|
27
27
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
28
|
+
- Mode: `{{HANDOFF_MODE}}`
|
|
29
|
+
- Stages: `{{HANDOFF_STAGES}}`
|
|
30
|
+
- Reports (one row per cited `final-verification` final-report; every Verdict Token MUST be `accepted`):
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
| Stage | Report path (project-relative) | Verdict Token |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
{{HANDOFF_SOURCE_REPORTS}}
|
|
35
|
+
|
|
36
|
+
> This section is generated by `okstra render-bundle` from the consumers ledger (`verified` rows) / the latest accepted whole-task report — the lead re-confirms each cited report's `Verdict Token` and MUST end the run immediately (route back to `final-verification`) if any token is not `accepted`. Release-handoff never operates on `conditional-accept` or `blocked` outcomes.
|
|
33
37
|
|
|
34
38
|
## Working-Tree Snapshot (filled at run start)
|
|
35
39
|
|
|
@@ -83,5 +87,5 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
83
87
|
|
|
84
88
|
## Conversion Note
|
|
85
89
|
|
|
86
|
-
- This
|
|
87
|
-
-
|
|
90
|
+
- This document is generated by `okstra render-bundle --task-type release-handoff` and serves as the run's input in place of a task brief (briefs belong to entry phases only).
|
|
91
|
+
- It reuses the same `Task Group` and `Task ID` as the originating implementation / final-verification runs so the handoff stays attached to the same task history.
|
package/src/render-bundle.mjs
CHANGED
|
@@ -18,7 +18,15 @@ Usage:
|
|
|
18
18
|
[--gemini-model <m>] [--report-writer-model <m>] \\
|
|
19
19
|
[--related-tasks <list>] [--base-ref <ref>] \\
|
|
20
20
|
[--clarification-response <path>] [--work-category <cat>] \\
|
|
21
|
-
[--pr-template-path <path>]
|
|
21
|
+
[--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>]
|
|
22
|
+
|
|
23
|
+
--stage implementation / final-verification only
|
|
24
|
+
--stages release-handoff only: PR stage bundle csv (empty = whole-task)
|
|
25
|
+
--pr-template-path release-handoff only
|
|
26
|
+
|
|
27
|
+
release-handoff takes NO --task-brief (briefs belong to entry phases) —
|
|
28
|
+
prepare generates the run's input document from the cited verification
|
|
29
|
+
reports instead.
|
|
22
30
|
|
|
23
31
|
All flags pass through unchanged to \`python3 -m okstra_ctl.run\`. The
|
|
24
32
|
shim auto-supplies \`--workspace-root\` (from \`okstra paths --field workspace\`)
|