okstra 0.69.0 → 0.70.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 +1 -0
- package/docs/kr/cli.md +2 -1
- package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +526 -0
- package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +89 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/wizard/prompts.ko.json +6 -2
- package/runtime/python/okstra_ctl/paths.py +7 -4
- package/runtime/python/okstra_ctl/render.py +10 -3
- package/runtime/python/okstra_ctl/run.py +10 -3
- package/runtime/python/okstra_ctl/wizard.py +81 -25
- package/runtime/skills/okstra-convergence/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
package/docs/kr/architecture.md
CHANGED
|
@@ -353,6 +353,7 @@ okstra phase 는 PRD / issue file 을 직접 쓰지 않습니다. 동등한 결
|
|
|
353
353
|
- `implementation`을 제외한 모든 phase는 source code edit, build, migration, deployment, 그 밖의 state-mutating 명령을 금지합니다(`final-verification`은 read-only 테스트 명령만 허용). `implementation`은 승인된 plan의 파일 목록 안에서만 edit/commit이 허용되며, `git push`·publish·deploy·실제 migration·third-party write API는 여전히 금지됩니다.
|
|
354
354
|
- **모든 task-type 격리 worktree (BLOCKING)**: 모든 task-type 의 첫 번째 phase prepare 단계에서 `okstra-ctl` 이 자동으로 task-key 단위 `git worktree` 를 생성하고, 같은 task-key 의 이후 phase (`requirements-discovery` → `error-analysis` → `implementation-planning` → `implementation`) 는 동일한 worktree·브랜치를 재사용합니다. 위치는 `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` (segment 의 `/`·`:` 등 특수문자는 `-` 로 정규화) 이고, 브랜치 이름은 `<work-category-prefix>-<task-id-segment>` (예: `feat-dev-9436`, `fix-dev-7311`) 입니다. base ref 는 첫 phase prepare 시점의 main worktree `HEAD`. `~/.okstra/worktrees/registry.json` (flock-guarded) 가 task-key → path/branch 매핑을 전역 관리해 동시 실행 시 path·branch 충돌을 방지합니다. configured sync dirs 는 main worktree 에서 symlink 로 연결되어 task checkout 사이의 filesystem continuity 를 제공합니다 (sync 대상 목록은 `project.json` 의 `worktreeSyncDirs` 또는 `OKSTRA_WORKTREE_SYNC_DIRS` 환경변수로 override 가능; 빈 배열이면 sync 비활성화). 이 sync 는 okstra context/write boundary 를 확장하지 않습니다. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 되고 executor 는 project_root 에서 그대로 작업합니다. worktree 는 run 종료 후 자동 삭제되지 않으며 후속 phase·PR 작성·rollback 검증의 권위 artefact 입니다. 수동 cleanup: `git -C <main-worktree> worktree remove <path>` → `git -C <main-worktree> branch -D <branch>` + registry 항목 삭제. 자세한 동작은 `prompts/profiles/implementation.md` 의 *Task worktree* 블록과 `agents/SKILL.md` 의 *Task worktree (BLOCKING for every task-type)* 섹션 참고.
|
|
355
355
|
- **implementation stage 격리 worktree (동시 병렬)**: 위 task-key 단위 worktree 는 `requirements-discovery`~`implementation-planning` 의 모델입니다. `implementation` task 는 **stage 격리** 로 동작합니다 (spec `docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md`) — **한 run = 한 stage**, 각 run 이 `.../<task-id-segment>/stage-<N>/` (브랜치 `<prefix>-<task-id-segment>-s<N>`) 격리 worktree 를 발급받습니다. registry 가 task-key 와 **stage-key** (`<task-key>#stage-<N>`) 를 함께 flock 예약하고, `_resolve_effective_stages` 가 `consumers.jsonl` 의 `started` + registry 예약 stage 를 ready 집합에서 제외하므로(점유 SSOT = registry), 사용자가 두 `implementation` run 을 동시에 띄우면 서로 다른 독립 stage 를 충돌 없이 진행합니다. base 결정: 독립 = 공통 anchor(첫 stage 진입 HEAD 고정), 단일 의존 = 선행 done commit, 다중 의존 = 선행이 모두 ancestor 인 task worktree HEAD(`git merge-base --is-ancestor`; 미머지 시 `PrepareError`). cost-aware-design 의 ready-set batch 는 stage 마다 격리 branch 가 필요해 의미를 잃으므로(같은 branch 에 두 stage-key reserve 시 branch-uniqueness 충돌) 폐기되었고, 순차 진행은 stage done 후 다음 run, 동시 진행은 별도 run 으로 — cost 등가. `--stage <auto|N>` 또는 wizard `stage_pick` 으로 stage 를 선택합니다. worktree 뿐 아니라 **run 산출물(report·state·worker-results·manifest)도 `runs/implementation/stage-<N>/` 로 stage 별 격리**되므로 동시 실행하는 두 stage 의 보고서·상태가 섞이지 않습니다. 반면 `consumers.jsonl` 과 worktree registry 는 stage 간 공유되는 조율 SSOT 라 task-type 루트(`runs/implementation/`)에 그대로 둡니다.
|
|
356
|
+
- **단일-stage final-verification 의 run 산출물 격리 (동시 병렬)**: 단독-stage `final-verification`(`--stage <N>`)도 implementation 과 동일하게 run 산출물을 `runs/final-verification/stage-<N>/` 하위에 격리하고(seq 도 stage 별 독립), 팀 이름에 `-fv-s<N>` 접미사를 붙입니다 — `-fv-` 구분자로 같은 stage 의 implementation 팀(`-s<N>`)과도, 전체-task 검증의 기본 이름과도 충돌하지 않습니다. 따라서 여러 stage 의 final-verification 을 동시에 띄워도 state·worker-results·보고서·팀이 섞이지 않습니다. worktree 는 새로 만들지 않고 해당 implementation stage worktree 를 registry 에서 read-only 로 재사용하며, 그래서 registry stage-key 예약도 하지 않습니다 — **같은 stage 의 final-verification 을 동시에 두 번** 띄우는 경우는 격리되지 않고 `TeamCreate` 이름 충돌로 즉시 실패합니다(알려진 제약). 전체-task 검증(stage 빈 값)은 기존 평면 `runs/final-verification/` 구조를 유지합니다.
|
|
356
357
|
- `implementation` 과 `release-handoff` 를 제외한 모든 phase 는 source code edit, build, migration, deployment, 그 밖의 state-mutating 명령을 금지합니다 (`final-verification` 은 read-only 테스트 명령만 허용). `implementation` 은 승인된 plan 의 파일 목록 안에서만 edit/commit 이 허용되며, `git push`·publish·deploy·실제 migration·third-party write API 는 여전히 금지됩니다. `release-handoff` 는 source code 자체는 수정하지 않고, 사용자가 메뉴로 선택한 commit / push / PR 명령만 실행합니다 (force push, base 브랜치 직접 push, hook bypass, release publish 는 여전히 금지).
|
|
357
358
|
- 사용자가 "다음 단계 진행해" 같은 표현을 보내도, 그 발화만으로 다음 phase가 자동 시작되지 않습니다. 다음 phase는 새 `okstra.sh` 실행으로만 시작합니다.
|
|
358
359
|
- **Authority & permissions assumption (모든 task-type 및 `okstra-schedule` 공통)**: 사용자(및 팀)는 예상되는 모든 작업에 대해 완전한 권한·승인 권한을 보유한다고 가정합니다. 외부 승인, 서드파티 액세스, 역할/IAM 권한, 조직적 sign-off, 법무·보안 검토, 벤더 협의, "권한 보유 여부 확인" 같은 항목을 routing 결정·missing inputs·clarification questions·risk·dependency·open questions·effort/day 추정에 포함하지 않습니다. okstra 내부 phase 핸드오프(`implementation-planning`의 `approved:` frontmatter 등)는 사용자 본인이 즉시 승인 가능한 내부 게이트이므로 영향 없으며, `implementation`의 forbidden actions(`git push`, prod deploy, shared-DB migration 등)도 권한 사유가 아닌 **안전 사유**로 계속 적용됩니다.
|
package/docs/kr/cli.md
CHANGED
|
@@ -355,6 +355,7 @@ fallback 기본값은 아래와 같습니다.
|
|
|
355
355
|
- **Claude executor 의 cwd 처리**: Claude Bash tool 은 per-call cwd 인자를 받지 않고 lead session 의 cwd 를 상속하므로, cwd 에 민감한 toolchain (`cargo`, `npm`, `pnpm`, `bun`, `pytest`, `make`, `go` 등) 을 worktree 안에서 실행하려면 호출을 `cd {{EXECUTOR_WORKTREE_PATH}} && <cmd>` 로 prefix 해야 합니다. 단일 Bash 호출 안에서 `cd` 가 leading token 으로 남아야 Claude Code 의 permission auto-allow 가 정상 동작하므로 `bash -lc "..."` / `bash -c "..."` 로 감싸지 않습니다 (감싸면 `cd` 가 가려져 매 호출마다 permission prompt 가 발생). `git -C <path>`, `cargo --manifest-path`, `pytest --rootdir` 처럼 작업 디렉터리 플래그를 받는 도구는 `cd && ` chain 대신 해당 플래그를 우선 사용합니다. Edit/Write/Read tool 은 이미 절대경로를 사용하므로 별도 cwd 처리가 필요 없습니다. 이 규칙은 Claude executor 에만 적용되고 codex / gemini executor 는 CLI wrapper 가 cwd 를 주입합니다.
|
|
356
356
|
- **Task worktree (모든 task-type 자동 격리)**: 모든 task-type 의 첫 번째 phase prepare 단계에서 `okstra-ctl` 이 `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` 에 `git worktree` 를 생성하고, 브랜치 `<work-category-prefix>-<task-id-segment>` 를 main worktree `HEAD` 에서 분기합니다. 같은 task-key 의 이후 phase 는 동일한 path/branch 를 재사용하므로 status 가 `reused` 로 기록됩니다 (run-prep 시점에 새 `git worktree add` 가 일어나지 않음). 모든 segment 의 `/`·`:` 등 특수문자는 `-` 로 정규화되며, `~/.okstra/worktrees/registry.json` 가 task-key → path/branch 매핑을 전역 관리합니다 (flock-guarded). Executor 의 Edit/Write/build/test/commit, verifier 의 read 는 모두 이 worktree 안에서 수행됩니다. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 되고 status 가 `skipped-in-worktree` / `skipped-not-git` 로 기록됩니다. 경로·브랜치 충돌은 `PrepareError` 로 즉시 실패시키며, run 종료 후 worktree 는 자동 삭제하지 않습니다 (수동: `git worktree remove` → `git branch -D` + registry 항목 삭제). **단, 아래 implementation stage 격리는 예외입니다.**
|
|
357
357
|
- **implementation stage 격리 (동시 병렬)**: 위 task-key 단위 worktree 는 `requirements-discovery`~`implementation-planning` 에만 해당합니다. `implementation` task 의 각 run 은 **stage 별 격리 worktree** (`~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/stage-<N>/`, 브랜치 `<work-category-prefix>-<task-id-segment>-s<N>`)에서 실행됩니다. registry 가 stage-key (`<task-key>#stage-<N>`) 를 flock 으로 원자 예약하고, `_resolve_effective_stages` 가 `consumers.jsonl` 의 `started` 행 + registry 예약 stage 를 제외하며, stage 선택부터 worktree 생성·registry 예약까지가 task-key 단위 프로비저닝 mutex(`~/.okstra/.locks/worktree-provision/`) 한 임계구역 안에서 수행되므로, 두 `implementation` run 을 동시에 띄우면 서로 다른 ready stage 를 안전하게 잡습니다 (**한 run = 한 stage**). stage worktree 의 base 는 의존 종류로 결정됩니다 — 독립(`depends-on (none)`) = 공통 anchor(첫 stage 진입 시 task-key worktree HEAD 1회 고정), 단일 의존(`depends-on X`) = 선행 stage 의 done `head_commit`, 다중 의존(`depends-on X,Y…`) = 선행들이 모두 머지된 task worktree HEAD(`git merge-base --is-ancestor` 로 검증, 미머지 시 `PrepareError` 로 머지 안내). 실행할 stage 는 `--stage <auto|N>` (`okstra.sh`/`render-bundle` 공통) 또는 okstra-run wizard 의 `stage_pick` 단계로 지정합니다. `project_root` 가 git repo 가 아니거나 nested worktree 면 stage 격리도 평면 동작으로 degrade 합니다.
|
|
358
|
+
- **단일-stage final-verification 산출물 격리**: `--task-type final-verification --stage <N>` 은 해당 implementation stage worktree 를 registry 에서 read-only 로 재사용하면서, run 산출물은 `runs/final-verification/stage-<N>/` 하위에 stage 별로 격리하고 팀 이름에 `-fv-s<N>` 접미사를 붙입니다. 서로 다른 stage 의 final-verification 을 동시에 띄워도 state·worker-results·팀이 충돌하지 않습니다 (같은 stage 를 동시에 두 번 띄우는 것은 격리되지 않음 — TeamCreate 이름 충돌로 즉시 실패). 전체-task 검증(stage 빈 값)은 평면 `runs/final-verification/` 를 유지합니다.
|
|
358
359
|
|
|
359
360
|
예:
|
|
360
361
|
|
|
@@ -592,7 +593,7 @@ chmod +x ~/.local/bin/okstra-ctl
|
|
|
592
593
|
| `okstra task-show <task-key> [--project-root <path>]` | task-manifest.json 의 workflow / phase / status 요약 |
|
|
593
594
|
| `okstra worktree-lookup <task-key>` | `worktree_registry.lookup` 결과 (예약된 path / branch / base ref / 현재 상태) |
|
|
594
595
|
| `okstra plan-validate <plan-path>` | `_validate_approved_plan` — frontmatter `approved` 인식 결과와 Blocks=approval 미해결 행 진단 |
|
|
595
|
-
| `okstra render-bundle <args…> [--stage <auto\|N>]` | `prepare_task_bundle(render_only=True)` 의 thin shim — `python3 -m okstra_ctl.run --render-only` 와 동일 시그니처. `--stage` 는 `implementation`
|
|
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 검증(평면 구조 유지) |
|
|
596
597
|
| `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 를 검사 |
|
|
597
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` 으로 넘어갑니다 |
|
|
598
599
|
| `okstra token-usage ...` | 설치된 `okstra-token-usage.py` 를 감싸 run token usage 수집/치환을 수행. 세션 jsonl 은 기본적으로 `$OKSTRA_HOME/cache/token-usage/` 의 byte cursor 캐시로 증분 스캔하며, `--no-cache` 로 캐시를 우회해 전체 재스캔을 강제할 수 있음(정확성 폴백) |
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# wizard whole-task final-verification 노출 Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** okstra-run 위저드의 final-verification stage picker 에서 "전체 task 검증" 을 명시 항목으로 선택할 수 있게 한다(prepare 계약 무변경, `auto` 토큰 미사용).
|
|
6
|
+
|
|
7
|
+
**Architecture:** 위저드 레이어만 수정한다. picker 가 `consumers.jsonl` 의 done 행을 읽어 stage 별 done 마킹을 붙이고, 전 stage done 일 때만 "전체 task" 항목(내부 sentinel `__whole_task__`)을 노출한다. `render_args` 가 sentinel 을 빈 stage(`""`)로 변환해 prepare 의 기존 whole-task 경로를 탄다. 머지/clean/active 전제는 prepare 의 PrepareError 게이트에 위임한다.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3 (`scripts/okstra_ctl/wizard.py`), JSON 프롬프트(`prompts/wizard/prompts.ko.json`), pytest.
|
|
10
|
+
|
|
11
|
+
**Spec:** [2026-06-11-wizard-whole-task-final-verification-design.md](../specs/2026-06-11-wizard-whole-task-final-verification-design.md)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
- `scripts/okstra_ctl/wizard.py` — sentinel 상수, done/Stage-Map 헬퍼, picker·submit·render_args·confirmation 수정.
|
|
18
|
+
- `prompts/wizard/prompts.ko.json` — `steps.stage_pick.options` 에 whole-task·done 마킹 라벨 추가.
|
|
19
|
+
- `tests/test_wizard_stage_pick.py` — picker·submit·render_args 회귀 + 신규 케이스.
|
|
20
|
+
- `tests/test_wizard_whole_task_fv.py` — done 헬퍼 + whole-task 노출 게이트 신규 테스트.
|
|
21
|
+
|
|
22
|
+
기존 단독-stage UX(`54b9482`)·prepare·CLI 계약은 건드리지 않는다.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Task 1: sentinel 상수 + done/Stage-Map 헬퍼
|
|
27
|
+
|
|
28
|
+
**Files:**
|
|
29
|
+
- Modify: `scripts/okstra_ctl/wizard.py` (토큰 블록 1354-1360 근처, 헬퍼는 `_stage_auto_allowed` 1778 부근)
|
|
30
|
+
- Test: `tests/test_wizard_whole_task_fv.py` (create)
|
|
31
|
+
|
|
32
|
+
- [ ] **Step 1: 실패 테스트 작성**
|
|
33
|
+
|
|
34
|
+
`tests/test_wizard_whole_task_fv.py`:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import importlib
|
|
38
|
+
import json
|
|
39
|
+
import sys
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _load_wizard():
|
|
46
|
+
if "okstra_ctl.wizard" in sys.modules:
|
|
47
|
+
return sys.modules["okstra_ctl.wizard"]
|
|
48
|
+
if str(REPO / "scripts") not in sys.path:
|
|
49
|
+
sys.path.insert(0, str(REPO / "scripts"))
|
|
50
|
+
return importlib.import_module("okstra_ctl.wizard")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _plan_run(tmp_path: Path, done_stages: list[int]) -> Path:
|
|
54
|
+
"""plan_run_root/reports/plan.md + plan_run_root/consumers.jsonl 를 만들고
|
|
55
|
+
approved plan 경로를 돌려준다. plan_run_root == plan.resolve().parents[1]."""
|
|
56
|
+
run = tmp_path / "run"
|
|
57
|
+
(run / "reports").mkdir(parents=True)
|
|
58
|
+
plan = run / "reports" / "plan.md"
|
|
59
|
+
fixture = (REPO / "tests" / "fixtures" / "plans"
|
|
60
|
+
/ "valid_three_stage_parallel.md").read_text(encoding="utf-8")
|
|
61
|
+
plan.write_text(fixture + "\n- [x] Approved\n", encoding="utf-8")
|
|
62
|
+
rows = [json.dumps({"status": "done", "stage": n,
|
|
63
|
+
"impl_task_key": "k", "head_commit": f"sha{n}"})
|
|
64
|
+
for n in done_stages]
|
|
65
|
+
(run / "consumers.jsonl").write_text(
|
|
66
|
+
("\n".join(rows) + "\n") if rows else "", encoding="utf-8")
|
|
67
|
+
return plan
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_done_stage_numbers_reads_consumers(tmp_path):
|
|
71
|
+
wizard = _load_wizard()
|
|
72
|
+
plan = _plan_run(tmp_path, [1, 3])
|
|
73
|
+
state = wizard.WizardState(
|
|
74
|
+
workspace_root=str(REPO), project_root=str(tmp_path),
|
|
75
|
+
project_id="demo", task_type="final-verification",
|
|
76
|
+
approved_plan_path=str(plan))
|
|
77
|
+
assert wizard._done_stage_numbers(state) == {1, 3}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_whole_task_allowed_true_when_all_done(tmp_path):
|
|
81
|
+
wizard = _load_wizard()
|
|
82
|
+
plan = _plan_run(tmp_path, [1, 2, 3])
|
|
83
|
+
state = wizard.WizardState(
|
|
84
|
+
workspace_root=str(REPO), project_root=str(tmp_path),
|
|
85
|
+
project_id="demo", task_type="final-verification",
|
|
86
|
+
approved_plan_path=str(plan))
|
|
87
|
+
assert wizard._whole_task_allowed(state) is True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_whole_task_allowed_false_when_partial(tmp_path):
|
|
91
|
+
wizard = _load_wizard()
|
|
92
|
+
plan = _plan_run(tmp_path, [1, 2])
|
|
93
|
+
state = wizard.WizardState(
|
|
94
|
+
workspace_root=str(REPO), project_root=str(tmp_path),
|
|
95
|
+
project_id="demo", task_type="final-verification",
|
|
96
|
+
approved_plan_path=str(plan))
|
|
97
|
+
assert wizard._whole_task_allowed(state) is False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_whole_task_allowed_false_for_implementation(tmp_path):
|
|
101
|
+
wizard = _load_wizard()
|
|
102
|
+
plan = _plan_run(tmp_path, [1, 2, 3])
|
|
103
|
+
state = wizard.WizardState(
|
|
104
|
+
workspace_root=str(REPO), project_root=str(tmp_path),
|
|
105
|
+
project_id="demo", task_type="implementation",
|
|
106
|
+
approved_plan_path=str(plan))
|
|
107
|
+
assert wizard._whole_task_allowed(state) is False
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
111
|
+
|
|
112
|
+
Run: `python3 -m pytest tests/test_wizard_whole_task_fv.py -v`
|
|
113
|
+
Expected: FAIL — `AttributeError: module ... has no attribute '_done_stage_numbers'`
|
|
114
|
+
|
|
115
|
+
- [ ] **Step 3: 최소 구현**
|
|
116
|
+
|
|
117
|
+
`scripts/okstra_ctl/wizard.py` 의 토큰 블록(`_REUSE_LAST_TOKEN = "__reuse_last__"` 등이 있는 1354-1360 근처)에 sentinel 추가:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
WHOLE_TASK_STAGE = "__whole_task__"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`_stage_auto_allowed` 정의(1778 근처) 바로 아래에 헬퍼 3개 추가:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
def _parse_stage_objects(state: WizardState) -> list:
|
|
127
|
+
"""승인 plan 의 Stage Map stage 객체 목록. validator 의 _parse_stage_map 재사용.
|
|
128
|
+
`_build_stage_pick` 과 `_whole_task_allowed` 가 공유한다."""
|
|
129
|
+
import importlib.util as _ilu
|
|
130
|
+
import sys as _sys
|
|
131
|
+
plan_text = Path(state.approved_plan_path).read_text(encoding="utf-8")
|
|
132
|
+
validator_path = (Path(state.workspace_root) / "validators"
|
|
133
|
+
/ "validate-implementation-plan-stages.py")
|
|
134
|
+
spec = _ilu.spec_from_file_location("_ip_stage_v_wizard", str(validator_path))
|
|
135
|
+
if spec is None or spec.loader is None:
|
|
136
|
+
raise WizardError(f"cannot load stage validator at {validator_path}")
|
|
137
|
+
mod = _ilu.module_from_spec(spec)
|
|
138
|
+
_sys.modules["_ip_stage_v_wizard"] = mod
|
|
139
|
+
try:
|
|
140
|
+
spec.loader.exec_module(mod)
|
|
141
|
+
stages, _errs = mod._parse_stage_map(plan_text)
|
|
142
|
+
finally:
|
|
143
|
+
_sys.modules.pop("_ip_stage_v_wizard", None)
|
|
144
|
+
return stages
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _done_stage_numbers(state: WizardState) -> set:
|
|
148
|
+
"""approved plan 을 소비한 implementation run 들의 consumers.jsonl 에서
|
|
149
|
+
done 처리된 stage 번호 집합. git 호출 없음 — 파일 읽기만(prepare 와 동일 SSOT)."""
|
|
150
|
+
if not state.approved_plan_path:
|
|
151
|
+
return set()
|
|
152
|
+
from .consumers import (read_consumers, backfill_done_from_carry,
|
|
153
|
+
latest_done_by_stage)
|
|
154
|
+
plan_run_root = Path(state.approved_plan_path).resolve().parents[1]
|
|
155
|
+
backfill_done_from_carry(plan_run_root)
|
|
156
|
+
rows = read_consumers(plan_run_root)
|
|
157
|
+
return set(latest_done_by_stage(rows).keys())
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _whole_task_allowed(state: WizardState) -> bool:
|
|
161
|
+
"""final-verification 이고 Stage Map 의 모든 stage 가 done 일 때만 True.
|
|
162
|
+
위저드는 done 만 본다 — 머지/clean/active 는 prepare 게이트가 강제한다."""
|
|
163
|
+
if state.task_type != "final-verification":
|
|
164
|
+
return False
|
|
165
|
+
if not state.approved_plan_path:
|
|
166
|
+
return False
|
|
167
|
+
stages = _parse_stage_objects(state)
|
|
168
|
+
if not stages:
|
|
169
|
+
return False
|
|
170
|
+
done = _done_stage_numbers(state)
|
|
171
|
+
return all(s.stage_number in done for s in stages)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- [ ] **Step 4: 테스트 통과 확인**
|
|
175
|
+
|
|
176
|
+
Run: `python3 -m pytest tests/test_wizard_whole_task_fv.py -v`
|
|
177
|
+
Expected: 4 PASS
|
|
178
|
+
|
|
179
|
+
- [ ] **Step 5: 커밋**
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
git add scripts/okstra_ctl/wizard.py tests/test_wizard_whole_task_fv.py
|
|
183
|
+
git commit -m "feat(wizard): final-verification done/whole-task 헬퍼 추가"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Task 2: stage picker 에 done 마킹 + 전체 task 항목
|
|
189
|
+
|
|
190
|
+
**Files:**
|
|
191
|
+
- Modify: `scripts/okstra_ctl/wizard.py:1494-1533` (`_build_stage_pick`)
|
|
192
|
+
- Modify: `prompts/wizard/prompts.ko.json` (`steps.stage_pick.options`)
|
|
193
|
+
- Test: `tests/test_wizard_whole_task_fv.py`
|
|
194
|
+
|
|
195
|
+
- [ ] **Step 1: 실패 테스트 추가**
|
|
196
|
+
|
|
197
|
+
`tests/test_wizard_whole_task_fv.py` 끝에 추가(`_plan_run` 헬퍼 재사용):
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
def test_picker_shows_whole_task_when_all_done(tmp_path):
|
|
201
|
+
wizard = _load_wizard()
|
|
202
|
+
plan = _plan_run(tmp_path, [1, 2, 3])
|
|
203
|
+
state = wizard.WizardState(
|
|
204
|
+
workspace_root=str(REPO), project_root=str(tmp_path),
|
|
205
|
+
project_id="demo", task_type="final-verification",
|
|
206
|
+
approved_plan_path=str(plan))
|
|
207
|
+
prompt = wizard._build_stage_pick(state)
|
|
208
|
+
values = [o.value for o in prompt.options]
|
|
209
|
+
assert wizard.WHOLE_TASK_STAGE in values
|
|
210
|
+
assert "auto" not in values
|
|
211
|
+
assert values == [wizard.WHOLE_TASK_STAGE, "1", "2", "3"]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_picker_hides_whole_task_when_partial(tmp_path):
|
|
215
|
+
wizard = _load_wizard()
|
|
216
|
+
plan = _plan_run(tmp_path, [1, 2])
|
|
217
|
+
state = wizard.WizardState(
|
|
218
|
+
workspace_root=str(REPO), project_root=str(tmp_path),
|
|
219
|
+
project_id="demo", task_type="final-verification",
|
|
220
|
+
approved_plan_path=str(plan))
|
|
221
|
+
prompt = wizard._build_stage_pick(state)
|
|
222
|
+
values = [o.value for o in prompt.options]
|
|
223
|
+
assert wizard.WHOLE_TASK_STAGE not in values
|
|
224
|
+
# stage 3 미완 마킹이 라벨에 보인다
|
|
225
|
+
label_3 = next(o.label for o in prompt.options if o.value == "3")
|
|
226
|
+
assert "미완" in label_3
|
|
227
|
+
label_1 = next(o.label for o in prompt.options if o.value == "1")
|
|
228
|
+
assert "done" in label_1
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
232
|
+
|
|
233
|
+
Run: `python3 -m pytest tests/test_wizard_whole_task_fv.py -k picker -v`
|
|
234
|
+
Expected: FAIL — whole-task 항목이 없고 done 마킹이 없음
|
|
235
|
+
|
|
236
|
+
- [ ] **Step 3: 구현**
|
|
237
|
+
|
|
238
|
+
`prompts/wizard/prompts.ko.json` 의 `steps.stage_pick.options` 를 다음으로 교체:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
"options": {
|
|
242
|
+
"auto": "auto (다음 미완료 stage)",
|
|
243
|
+
"whole_task": "전체 task 검증 (모든 stage)",
|
|
244
|
+
"done_mark": "[done]",
|
|
245
|
+
"undone_mark": "[미완]"
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`scripts/okstra_ctl/wizard.py` 의 `_build_stage_pick` 본문을 교체(파싱은 `_parse_stage_objects` 로 위임):
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
def _build_stage_pick(state: WizardState) -> Prompt:
|
|
253
|
+
"""Parse the Stage Map from the approved plan and build the stage picker."""
|
|
254
|
+
t = _p(state.workspace_root, "stage_pick")
|
|
255
|
+
stages = _parse_stage_objects(state)
|
|
256
|
+
is_fv = state.task_type == "final-verification"
|
|
257
|
+
label = (
|
|
258
|
+
t.get("label_final_verification", t["label"])
|
|
259
|
+
if is_fv else t["label"]
|
|
260
|
+
)
|
|
261
|
+
done = _done_stage_numbers(state) if is_fv else set()
|
|
262
|
+
options = []
|
|
263
|
+
if _stage_auto_allowed(state):
|
|
264
|
+
options.append(_opt("auto", t["options"]["auto"]))
|
|
265
|
+
if is_fv and stages and all(s.stage_number in done for s in stages):
|
|
266
|
+
options.append(_opt(
|
|
267
|
+
WHOLE_TASK_STAGE,
|
|
268
|
+
t["options"].get("whole_task", "전체 task 검증 (모든 stage)"),
|
|
269
|
+
))
|
|
270
|
+
for s in stages:
|
|
271
|
+
depends = ",".join(map(str, s.depends_on)) or "(none)"
|
|
272
|
+
suffix = ""
|
|
273
|
+
if is_fv:
|
|
274
|
+
mark = (t["options"].get("done_mark", "[done]")
|
|
275
|
+
if s.stage_number in done
|
|
276
|
+
else t["options"].get("undone_mark", "[미완]"))
|
|
277
|
+
suffix = f" {mark}"
|
|
278
|
+
options.append(_opt(
|
|
279
|
+
str(s.stage_number),
|
|
280
|
+
f"{s.stage_number}: {s.title} "
|
|
281
|
+
f"[depends-on: {depends} | steps: {s.step_count}]{suffix}",
|
|
282
|
+
))
|
|
283
|
+
return Prompt(
|
|
284
|
+
step=S_STAGE_PICK, kind="pick",
|
|
285
|
+
label=label,
|
|
286
|
+
options=options,
|
|
287
|
+
echo_template=t["echo_template"],
|
|
288
|
+
)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- [ ] **Step 4: 테스트 통과 + 기존 picker 회귀 확인**
|
|
292
|
+
|
|
293
|
+
Run: `python3 -m pytest tests/test_wizard_whole_task_fv.py tests/test_wizard_stage_pick.py -v`
|
|
294
|
+
Expected: 신규 picker 2 PASS + `test_final_verification_stage_pick_excludes_auto`(이제 stage 별 라벨에 `[미완]` 가 붙지만 value 는 `["1","2","3"]` 유지) 포함 기존 전부 PASS
|
|
295
|
+
|
|
296
|
+
> 주의: `test_final_verification_stage_pick_excludes_auto` 는 done 행이 없는 plan 을 쓰므로 whole-task 항목이 안 뜨고 `values == ["1","2","3"]` 가 유지된다. 라벨 문자열은 검사하지 않으므로 마킹 추가와 무관하다.
|
|
297
|
+
|
|
298
|
+
- [ ] **Step 5: 커밋**
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
git add scripts/okstra_ctl/wizard.py prompts/wizard/prompts.ko.json tests/test_wizard_whole_task_fv.py
|
|
302
|
+
git commit -m "feat(wizard): final-verification picker 에 done 마킹 + 전체 task 항목"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Task 3: submit 이 전체 task sentinel 수용
|
|
308
|
+
|
|
309
|
+
**Files:**
|
|
310
|
+
- Modify: `scripts/okstra_ctl/wizard.py:1536-1552` (`_submit_stage_pick`)
|
|
311
|
+
- Test: `tests/test_wizard_whole_task_fv.py`
|
|
312
|
+
|
|
313
|
+
- [ ] **Step 1: 실패 테스트 추가**
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
def test_submit_accepts_whole_task_for_fv(tmp_path):
|
|
317
|
+
wizard = _load_wizard()
|
|
318
|
+
state = wizard.WizardState(
|
|
319
|
+
workspace_root=str(REPO), project_root=str(tmp_path),
|
|
320
|
+
project_id="demo", task_type="final-verification")
|
|
321
|
+
result = wizard._submit_stage_pick(state, wizard.WHOLE_TASK_STAGE)
|
|
322
|
+
assert state.selected_stage == wizard.WHOLE_TASK_STAGE
|
|
323
|
+
assert result == f"stage: {wizard.WHOLE_TASK_STAGE}"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_submit_rejects_whole_task_for_implementation(tmp_path):
|
|
327
|
+
wizard = _load_wizard()
|
|
328
|
+
state = wizard.WizardState(
|
|
329
|
+
workspace_root=str(REPO), project_root=str(tmp_path),
|
|
330
|
+
project_id="demo", task_type="implementation")
|
|
331
|
+
try:
|
|
332
|
+
wizard._submit_stage_pick(state, wizard.WHOLE_TASK_STAGE)
|
|
333
|
+
assert False, "expected WizardError"
|
|
334
|
+
except wizard.WizardError as exc:
|
|
335
|
+
assert "final-verification" in str(exc)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
339
|
+
|
|
340
|
+
Run: `python3 -m pytest tests/test_wizard_whole_task_fv.py -k submit -v`
|
|
341
|
+
Expected: FAIL — sentinel 이 `int()` 분기로 빠져 "stage number" 에러
|
|
342
|
+
|
|
343
|
+
- [ ] **Step 3: 구현**
|
|
344
|
+
|
|
345
|
+
`_submit_stage_pick` 을 교체:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
def _submit_stage_pick(state: WizardState, answer: str) -> Optional[str]:
|
|
349
|
+
if not answer:
|
|
350
|
+
raise WizardError("value required")
|
|
351
|
+
if answer == "auto":
|
|
352
|
+
if not _stage_auto_allowed(state):
|
|
353
|
+
raise WizardError(
|
|
354
|
+
"final-verification requires an explicit stage number"
|
|
355
|
+
)
|
|
356
|
+
elif answer == WHOLE_TASK_STAGE:
|
|
357
|
+
if state.task_type != "final-verification":
|
|
358
|
+
raise WizardError(
|
|
359
|
+
"whole-task verification is only valid for final-verification"
|
|
360
|
+
)
|
|
361
|
+
else:
|
|
362
|
+
try:
|
|
363
|
+
int(answer)
|
|
364
|
+
except ValueError:
|
|
365
|
+
raise WizardError(
|
|
366
|
+
f"answer must be 'auto', whole-task, or a stage number, "
|
|
367
|
+
f"got {answer!r}"
|
|
368
|
+
)
|
|
369
|
+
state.selected_stage = answer
|
|
370
|
+
return f"stage: {answer}"
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
- [ ] **Step 4: 테스트 통과 + 기존 submit 회귀 확인**
|
|
374
|
+
|
|
375
|
+
Run: `python3 -m pytest tests/test_wizard_whole_task_fv.py tests/test_wizard_stage_pick.py -k submit -v`
|
|
376
|
+
Expected: 신규 2 + 기존 submit 케이스 전부 PASS
|
|
377
|
+
|
|
378
|
+
- [ ] **Step 5: 커밋**
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
git add scripts/okstra_ctl/wizard.py tests/test_wizard_whole_task_fv.py
|
|
382
|
+
git commit -m "feat(wizard): _submit_stage_pick 이 전체 task sentinel 수용"
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Task 4: render_args 빈 stage 변환 + confirmation 라벨
|
|
388
|
+
|
|
389
|
+
**Files:**
|
|
390
|
+
- Modify: `scripts/okstra_ctl/wizard.py:2792-2806` (`render_args` stage 결정부)
|
|
391
|
+
- Modify: `scripts/okstra_ctl/wizard.py:2872-2878` (`confirmation_block` stage 표기부)
|
|
392
|
+
- Test: `tests/test_wizard_whole_task_fv.py`
|
|
393
|
+
|
|
394
|
+
- [ ] **Step 1: 실패 테스트 추가**
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
def test_render_args_whole_task_emits_empty_stage(tmp_path):
|
|
398
|
+
wizard = _load_wizard()
|
|
399
|
+
plan = _plan_run(tmp_path, [1, 2, 3])
|
|
400
|
+
state = wizard.WizardState(
|
|
401
|
+
workspace_root=str(REPO), project_root=str(tmp_path),
|
|
402
|
+
project_id="demo", task_type="final-verification",
|
|
403
|
+
approved_plan_path=str(plan),
|
|
404
|
+
selected_stage=wizard.WHOLE_TASK_STAGE)
|
|
405
|
+
args = wizard.render_args(state)
|
|
406
|
+
assert args["stage"] == ""
|
|
407
|
+
assert args["base-ref"] == "" # final-verification 은 base 자동 해소
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def test_render_args_single_stage_fv_keeps_number(tmp_path):
|
|
411
|
+
wizard = _load_wizard()
|
|
412
|
+
plan = _plan_run(tmp_path, [2])
|
|
413
|
+
state = wizard.WizardState(
|
|
414
|
+
workspace_root=str(REPO), project_root=str(tmp_path),
|
|
415
|
+
project_id="demo", task_type="final-verification",
|
|
416
|
+
approved_plan_path=str(plan), selected_stage="2")
|
|
417
|
+
args = wizard.render_args(state)
|
|
418
|
+
assert args["stage"] == "2"
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def test_confirmation_block_labels_whole_task(tmp_path):
|
|
422
|
+
wizard = _load_wizard()
|
|
423
|
+
plan = _plan_run(tmp_path, [1, 2, 3])
|
|
424
|
+
state = wizard.WizardState(
|
|
425
|
+
workspace_root=str(REPO), project_root=str(tmp_path),
|
|
426
|
+
project_id="demo", task_group="g", task_id="t",
|
|
427
|
+
task_type="final-verification", approved_plan_path=str(plan),
|
|
428
|
+
selected_stage=wizard.WHOLE_TASK_STAGE)
|
|
429
|
+
block = wizard.confirmation_block(state)
|
|
430
|
+
assert "전체 task" in block
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
434
|
+
|
|
435
|
+
Run: `python3 -m pytest tests/test_wizard_whole_task_fv.py -k "render_args or confirmation" -v`
|
|
436
|
+
Expected: FAIL — sentinel 이 그대로 stage 로 새거나 `WizardError`("requires an explicit stage number")
|
|
437
|
+
|
|
438
|
+
- [ ] **Step 3: 구현**
|
|
439
|
+
|
|
440
|
+
`render_args` 의 stage 결정 블록(현재 `elif state.task_type == "final-verification":` 분기, 2799-2804)을 교체:
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
if state.task_type == "implementation":
|
|
444
|
+
stage = state.selected_stage or "auto"
|
|
445
|
+
elif state.task_type == "final-verification":
|
|
446
|
+
if state.selected_stage == WHOLE_TASK_STAGE:
|
|
447
|
+
stage = "" # prepare 가 빈 stage 를 whole-task 로 해석
|
|
448
|
+
elif not state.selected_stage or state.selected_stage == "auto":
|
|
449
|
+
raise WizardError(
|
|
450
|
+
"final-verification requires an explicit stage number"
|
|
451
|
+
)
|
|
452
|
+
else:
|
|
453
|
+
stage = state.selected_stage
|
|
454
|
+
else:
|
|
455
|
+
stage = ""
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
`confirmation_block` 의 stage 표기 블록(2874-2878)을 교체:
|
|
459
|
+
|
|
460
|
+
```python
|
|
461
|
+
stage = (
|
|
462
|
+
"전체 task"
|
|
463
|
+
if state.selected_stage == WHOLE_TASK_STAGE
|
|
464
|
+
else (state.selected_stage
|
|
465
|
+
or ("auto" if state.task_type == "implementation"
|
|
466
|
+
else "(not selected)"))
|
|
467
|
+
)
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
- [ ] **Step 4: 테스트 통과 + render_args 회귀 확인**
|
|
471
|
+
|
|
472
|
+
Run: `python3 -m pytest tests/test_wizard_whole_task_fv.py tests/test_wizard_stage_pick.py -k "render_args or confirmation" -v`
|
|
473
|
+
Expected: 신규 3 + 기존 render_args 케이스(`test_render_args_includes_stage_for_implementation` 등) 전부 PASS
|
|
474
|
+
|
|
475
|
+
- [ ] **Step 5: 커밋**
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
git add scripts/okstra_ctl/wizard.py tests/test_wizard_whole_task_fv.py
|
|
479
|
+
git commit -m "feat(wizard): render_args 가 전체 task sentinel 을 빈 stage 로 변환"
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Task 5: 통합 검증 + CHANGES 기록
|
|
485
|
+
|
|
486
|
+
**Files:**
|
|
487
|
+
- Modify: `CHANGES.md`
|
|
488
|
+
- Test: 전체 위저드 스위트
|
|
489
|
+
|
|
490
|
+
- [ ] **Step 1: 전체 위저드 테스트**
|
|
491
|
+
|
|
492
|
+
Run: `python3 -m pytest tests/test_wizard_whole_task_fv.py tests/test_wizard_stage_pick.py tests/test_wizard_final_verification_stage.py tests/test_okstra_ctl_wizard.py -v`
|
|
493
|
+
Expected: 전부 PASS (whole-task 경로에서도 base-ref/branch-confirm 생략이 유지되는지 `test_okstra_ctl_wizard.py` 가 커버)
|
|
494
|
+
|
|
495
|
+
- [ ] **Step 2: build 후 runtime sync 확인**
|
|
496
|
+
|
|
497
|
+
Run: `npm run build && node bin/okstra --version`
|
|
498
|
+
Expected: 빌드 성공, 버전 출력. `runtime/` 에 prompts.ko.json·wizard.py 변경분이 반영됨(직접 편집 금지, 빌드로만).
|
|
499
|
+
|
|
500
|
+
- [ ] **Step 3: CHANGES.md 항목 추가**
|
|
501
|
+
|
|
502
|
+
`CHANGES.md` 의 최상단 날짜 섹션(`## 2026-06-11`, 없으면 생성)에 추가:
|
|
503
|
+
|
|
504
|
+
```markdown
|
|
505
|
+
### feat(wizard): final-verification 위저드에서 전체 task 검증 선택 지원
|
|
506
|
+
|
|
507
|
+
- **배경**: `54b9482` 이후 okstra-run 위저드의 `final-verification` 은 명시 stage 만 선택 가능해, 모든 stage 를 머지한 뒤 한 번 도는 전체-task 검증을 CLI `--stage auto` 로만 할 수 있었다.
|
|
508
|
+
- **해결**: stage picker 에 stage 별 done 마킹을 표시하고, 모든 stage 가 done 이면 "전체 task 검증" 항목을 노출한다. 선택 시 prepare 에 빈 stage 를 넘겨 기존 전체-task 경로를 탄다(`auto` 토큰 미사용, prepare 계약 무변경). 머지/clean 미충족은 prepare 의 기존 PrepareError 가 안내한다.
|
|
509
|
+
- 사용자 영향: 다음 release + `npx -y okstra@latest install` 후, 모든 stage 가 done 이면 위저드에서 "전체 task 검증" 을 골라 task 전체를 한 번에 검증할 수 있다.
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
- [ ] **Step 4: 커밋**
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
git add CHANGES.md runtime/
|
|
516
|
+
git commit -m "docs(changes): wizard 전체 task final-verification 지원 기록 + runtime 빌드"
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## Self-Review 결과
|
|
522
|
+
|
|
523
|
+
- **Spec coverage**: §3 노출(Task 2) · §4 done 데이터원(Task 1) · §5 빈 stage emit(Task 4) · §6 게이트 함수(Task 2~4) · §7 프롬프트(Task 2) · §8 테스트(Task 1~5) — 전부 매핑됨.
|
|
524
|
+
- **Placeholder**: 모든 코드 스텝에 실제 본문 포함, TODO 없음.
|
|
525
|
+
- **Type/이름 일관성**: `WHOLE_TASK_STAGE` sentinel 이 Task 1 정의 → 2·3·4 에서 동일 이름 사용. `_parse_stage_objects`/`_done_stage_numbers`/`_whole_task_allowed` 시그니처 일관.
|
|
526
|
+
- **회귀**: 각 Task Step 4 가 기존 `test_wizard_stage_pick.py` 를 함께 돌려 단독-stage UX 불변을 확인.
|
|
@@ -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` 도입분): 변경 없음.
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -188,7 +188,10 @@
|
|
|
188
188
|
"label_final_verification": "검증할 implementation stage 를 선택하세요.",
|
|
189
189
|
"echo_template": "stage: {value}",
|
|
190
190
|
"options": {
|
|
191
|
-
"auto": "auto (다음 미완료 stage)"
|
|
191
|
+
"auto": "auto (다음 미완료 stage)",
|
|
192
|
+
"whole_task": "전체 task 검증 (모든 stage)",
|
|
193
|
+
"done_mark": "[done]",
|
|
194
|
+
"undone_mark": "[미완]"
|
|
192
195
|
}
|
|
193
196
|
},
|
|
194
197
|
"directive_pick": {
|
|
@@ -367,6 +370,7 @@
|
|
|
367
370
|
},
|
|
368
371
|
"confirmation": {
|
|
369
372
|
"header": "선택 확인:",
|
|
370
|
-
"workers_implementation_default": " workers : (프로필 기본 — executor + verifier 2 + report-writer)"
|
|
373
|
+
"workers_implementation_default": " workers : (프로필 기본 — executor + verifier 2 + report-writer)",
|
|
374
|
+
"stage_whole_task": "전체 task"
|
|
371
375
|
}
|
|
372
376
|
}
|
|
@@ -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
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
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
|
|
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
|
-
|
|
1685
|
-
|
|
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{
|
|
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"
|
|
@@ -1868,7 +1868,13 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1868
1868
|
):
|
|
1869
1869
|
if inp.task_type == "final-verification" and inp.stage and inp.stage != "auto":
|
|
1870
1870
|
worktree = _single_stage_final_verification_worktree(inp)
|
|
1871
|
+
# Single-stage final-verification namespaces its run path under
|
|
1872
|
+
# runs/final-verification/stage-<N> (same isolation as
|
|
1873
|
+
# implementation) so concurrent per-stage verifications never
|
|
1874
|
+
# share state/reports/worker-results.
|
|
1875
|
+
fv_stage_arg = int(inp.stage)
|
|
1871
1876
|
else:
|
|
1877
|
+
fv_stage_arg = None
|
|
1872
1878
|
try:
|
|
1873
1879
|
worktree = provision_task_worktree(
|
|
1874
1880
|
task_type=inp.task_type,
|
|
@@ -1890,8 +1896,9 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1890
1896
|
# lands in runs/implementation/stage-<N>. The registry stage-key is
|
|
1891
1897
|
# reserved exactly once here (inside provision_stage_worktree), and
|
|
1892
1898
|
# the surrounding mutex makes the registry read in stage selection
|
|
1893
|
-
# and that reserve atomic.
|
|
1894
|
-
#
|
|
1899
|
+
# and that reserve atomic. Other task-types skip this selection;
|
|
1900
|
+
# single-stage final-verification threads its explicit stage via
|
|
1901
|
+
# fv_stage_arg, everything else keeps stage_arg=None (flat paths).
|
|
1895
1902
|
if inp.task_type == "implementation":
|
|
1896
1903
|
impl_stage_selection = _select_and_provision_implementation_stage(
|
|
1897
1904
|
inp, ctx_stage_map, task_group_segment, task_id_segment,
|
|
@@ -1904,7 +1911,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1904
1911
|
_clear_stale_stage_waiver(inp, project_root, impl_stage_selection.stage)
|
|
1905
1912
|
else:
|
|
1906
1913
|
impl_stage_selection = None
|
|
1907
|
-
stage_arg =
|
|
1914
|
+
stage_arg = fv_stage_arg
|
|
1908
1915
|
|
|
1909
1916
|
ctx = compute_and_write_run_context(
|
|
1910
1917
|
workspace_root=workspace_root, project_root=project_root,
|
|
@@ -779,6 +779,54 @@ def _stage_auto_allowed(state: WizardState) -> bool:
|
|
|
779
779
|
return state.task_type == "implementation"
|
|
780
780
|
|
|
781
781
|
|
|
782
|
+
def _parse_stage_objects(state: WizardState) -> list:
|
|
783
|
+
"""승인 plan 의 Stage Map stage 객체 목록. validator 의 _parse_stage_map 재사용.
|
|
784
|
+
`_build_stage_pick` 과 `_whole_task_allowed` 가 공유한다."""
|
|
785
|
+
import importlib.util as _ilu
|
|
786
|
+
import sys as _sys
|
|
787
|
+
plan_text = Path(state.approved_plan_path).read_text(encoding="utf-8")
|
|
788
|
+
validator_path = (Path(state.workspace_root) / "validators"
|
|
789
|
+
/ "validate-implementation-plan-stages.py")
|
|
790
|
+
spec = _ilu.spec_from_file_location("_ip_stage_v_wizard", str(validator_path))
|
|
791
|
+
if spec is None or spec.loader is None:
|
|
792
|
+
raise WizardError(f"cannot load stage validator at {validator_path}")
|
|
793
|
+
mod = _ilu.module_from_spec(spec)
|
|
794
|
+
_sys.modules["_ip_stage_v_wizard"] = mod
|
|
795
|
+
try:
|
|
796
|
+
spec.loader.exec_module(mod)
|
|
797
|
+
stages, _errs = mod._parse_stage_map(plan_text)
|
|
798
|
+
finally:
|
|
799
|
+
_sys.modules.pop("_ip_stage_v_wizard", None)
|
|
800
|
+
return stages
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _done_stage_numbers(state: WizardState) -> set:
|
|
804
|
+
"""approved plan 을 소비한 implementation run 들의 consumers.jsonl 에서
|
|
805
|
+
done 처리된 stage 번호 집합. git 호출 없음 — 파일 읽기만(prepare 와 동일 SSOT)."""
|
|
806
|
+
if not state.approved_plan_path:
|
|
807
|
+
return set()
|
|
808
|
+
from .consumers import (read_consumers, backfill_done_from_carry,
|
|
809
|
+
latest_done_by_stage)
|
|
810
|
+
plan_run_root = Path(state.approved_plan_path).resolve().parents[1]
|
|
811
|
+
backfill_done_from_carry(plan_run_root)
|
|
812
|
+
rows = read_consumers(plan_run_root)
|
|
813
|
+
return set(latest_done_by_stage(rows).keys())
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _whole_task_allowed(state: WizardState) -> bool:
|
|
817
|
+
"""final-verification 이고 Stage Map 의 모든 stage 가 done 일 때만 True.
|
|
818
|
+
위저드는 done 만 본다 — 머지/clean/active 는 prepare 게이트가 강제한다."""
|
|
819
|
+
if state.task_type != "final-verification":
|
|
820
|
+
return False
|
|
821
|
+
if not state.approved_plan_path:
|
|
822
|
+
return False
|
|
823
|
+
stages = _parse_stage_objects(state)
|
|
824
|
+
if not stages:
|
|
825
|
+
return False
|
|
826
|
+
done = _done_stage_numbers(state)
|
|
827
|
+
return all(s.stage_number in done for s in stages)
|
|
828
|
+
|
|
829
|
+
|
|
782
830
|
def _existing_task_brief(project_root: Path, task_key: str) -> str:
|
|
783
831
|
"""Read taskBriefPath from manifest for an existing task. Empty if none."""
|
|
784
832
|
root = find_task_root(project_root, task_key)
|
|
@@ -1358,6 +1406,7 @@ _REUSE_LAST_TOKEN = "__reuse_last__"
|
|
|
1358
1406
|
_SIBLINGS_TOKEN = "__siblings__"
|
|
1359
1407
|
_LATEST_REPORT_TOKEN = "__latest_report__"
|
|
1360
1408
|
_PROJECT_DEFAULT_TOKEN = "__project_default__"
|
|
1409
|
+
WHOLE_TASK_STAGE = "__whole_task__"
|
|
1361
1410
|
|
|
1362
1411
|
|
|
1363
1412
|
def _list_implementation_planning_reports(
|
|
@@ -1493,37 +1542,31 @@ def _submit_approve_plan_confirm(state: WizardState, value: str) -> Optional[str
|
|
|
1493
1542
|
|
|
1494
1543
|
def _build_stage_pick(state: WizardState) -> Prompt:
|
|
1495
1544
|
"""Parse the Stage Map from the approved plan and build the stage picker."""
|
|
1496
|
-
import importlib.util as _ilu
|
|
1497
|
-
import sys as _sys
|
|
1498
1545
|
t = _p(state.workspace_root, "stage_pick")
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
Path(state.workspace_root) / "validators"
|
|
1502
|
-
/ "validate-implementation-plan-stages.py"
|
|
1503
|
-
)
|
|
1504
|
-
spec = _ilu.spec_from_file_location("_ip_stage_v_wizard", str(validator_path))
|
|
1505
|
-
if spec is None or spec.loader is None:
|
|
1506
|
-
raise WizardError(f"cannot load stage validator at {validator_path}")
|
|
1507
|
-
mod = _ilu.module_from_spec(spec)
|
|
1508
|
-
_sys.modules["_ip_stage_v_wizard"] = mod
|
|
1509
|
-
try:
|
|
1510
|
-
spec.loader.exec_module(mod)
|
|
1511
|
-
stages, _errs = mod._parse_stage_map(plan_text)
|
|
1512
|
-
finally:
|
|
1513
|
-
_sys.modules.pop("_ip_stage_v_wizard", None)
|
|
1546
|
+
stages = _parse_stage_objects(state)
|
|
1547
|
+
is_fv = state.task_type == "final-verification"
|
|
1514
1548
|
label = (
|
|
1515
1549
|
t.get("label_final_verification", t["label"])
|
|
1516
|
-
if
|
|
1517
|
-
else t["label"]
|
|
1550
|
+
if is_fv else t["label"]
|
|
1518
1551
|
)
|
|
1552
|
+
done = _done_stage_numbers(state) if is_fv else set()
|
|
1519
1553
|
options = []
|
|
1520
1554
|
if _stage_auto_allowed(state):
|
|
1521
1555
|
options.append(_opt("auto", t["options"]["auto"]))
|
|
1556
|
+
if _whole_task_allowed(state):
|
|
1557
|
+
options.append(_opt(WHOLE_TASK_STAGE, t["options"]["whole_task"]))
|
|
1522
1558
|
for s in stages:
|
|
1523
1559
|
depends = ",".join(map(str, s.depends_on)) or "(none)"
|
|
1560
|
+
suffix = ""
|
|
1561
|
+
if is_fv:
|
|
1562
|
+
mark = (t["options"]["done_mark"]
|
|
1563
|
+
if s.stage_number in done
|
|
1564
|
+
else t["options"]["undone_mark"])
|
|
1565
|
+
suffix = f" {mark}"
|
|
1524
1566
|
options.append(_opt(
|
|
1525
1567
|
str(s.stage_number),
|
|
1526
|
-
f"{s.stage_number}: {s.title}
|
|
1568
|
+
f"{s.stage_number}: {s.title} "
|
|
1569
|
+
f"[depends-on: {depends} | steps: {s.step_count}]{suffix}",
|
|
1527
1570
|
))
|
|
1528
1571
|
return Prompt(
|
|
1529
1572
|
step=S_STAGE_PICK, kind="pick",
|
|
@@ -1541,12 +1584,19 @@ def _submit_stage_pick(state: WizardState, answer: str) -> Optional[str]:
|
|
|
1541
1584
|
raise WizardError(
|
|
1542
1585
|
"final-verification requires an explicit stage number"
|
|
1543
1586
|
)
|
|
1587
|
+
elif answer == WHOLE_TASK_STAGE:
|
|
1588
|
+
if not _whole_task_allowed(state):
|
|
1589
|
+
raise WizardError(
|
|
1590
|
+
"whole-task verification requires final-verification "
|
|
1591
|
+
"with all stages done"
|
|
1592
|
+
)
|
|
1544
1593
|
else:
|
|
1545
1594
|
try:
|
|
1546
1595
|
int(answer)
|
|
1547
1596
|
except ValueError:
|
|
1548
1597
|
raise WizardError(
|
|
1549
|
-
f"answer must be 'auto' or a stage number,
|
|
1598
|
+
f"answer must be 'auto', whole-task, or a stage number, "
|
|
1599
|
+
f"got {answer!r}"
|
|
1550
1600
|
)
|
|
1551
1601
|
state.selected_stage = answer
|
|
1552
1602
|
return f"stage: {answer}"
|
|
@@ -2797,11 +2847,14 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
2797
2847
|
if state.task_type == "implementation":
|
|
2798
2848
|
stage = state.selected_stage or "auto"
|
|
2799
2849
|
elif state.task_type == "final-verification":
|
|
2800
|
-
if
|
|
2850
|
+
if state.selected_stage == WHOLE_TASK_STAGE:
|
|
2851
|
+
stage = "" # prepare 가 빈 stage 를 whole-task 로 해석
|
|
2852
|
+
elif not state.selected_stage or state.selected_stage == "auto":
|
|
2801
2853
|
raise WizardError(
|
|
2802
2854
|
"final-verification requires an explicit stage number"
|
|
2803
2855
|
)
|
|
2804
|
-
|
|
2856
|
+
else:
|
|
2857
|
+
stage = state.selected_stage
|
|
2805
2858
|
else:
|
|
2806
2859
|
stage = ""
|
|
2807
2860
|
pr_template = (
|
|
@@ -2872,8 +2925,11 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
2872
2925
|
if state.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
2873
2926
|
lines.append(f" approved-plan : {state.approved_plan_path}")
|
|
2874
2927
|
stage = (
|
|
2875
|
-
state.
|
|
2876
|
-
|
|
2928
|
+
_msg(state.workspace_root, "confirmation", "stage_whole_task")
|
|
2929
|
+
if state.selected_stage == WHOLE_TASK_STAGE
|
|
2930
|
+
else (state.selected_stage
|
|
2931
|
+
or ("auto" if state.task_type == "implementation"
|
|
2932
|
+
else "(not selected)"))
|
|
2877
2933
|
)
|
|
2878
2934
|
lines.append(f" stage : {stage}")
|
|
2879
2935
|
if state.clarification_response_path:
|
|
@@ -572,7 +572,7 @@ Promoted blockers enter `## 5.8 Acceptance Blockers`; since `accepted` requires
|
|
|
572
572
|
|
|
573
573
|
### State
|
|
574
574
|
|
|
575
|
-
Critic output lives
|
|
575
|
+
Critic output lives in the run's `worker-results/` directory (`runs/final-verification/worker-results/` for whole-task verification, `runs/final-verification/stage-<N>/worker-results/` for single-stage), filename `<provider>-critic-final-verification-<seq>.md`. The convergence state `config.critic` summary (see §"Coverage critic pass") records `mode: "acceptance-devils-advocate"`, `candidatesProposed`, `confirmedBlockers`, `downgradedToResidual` (optional v1.2 fields; readers treat absence as null).
|
|
576
576
|
|
|
577
577
|
## Output
|
|
578
578
|
|
|
@@ -299,7 +299,7 @@ B. **`task-manifest.json` (direct):** if catalog missing, slugify task-group / t
|
|
|
299
299
|
|
|
300
300
|
C. **`timeline.json` (specific run):** for a specific date or run, read `.okstra/tasks/<group-segment>/<id-segment>/history/timeline.json`, filter `runs[]` by `runTimestamp` / `status` / `taskType`, use `runs[].reportPath`.
|
|
301
301
|
|
|
302
|
-
D. **Specific task-type (fallback):** `latestReportPath` is task-type-agnostic. For a specific task-type's latest report, look under `.okstra/tasks/<group-segment>/<id-segment>/runs/<task-type-segment>/reports/`, filename pattern `final-report-<task-type-segment>-<NNN>.md` per `scripts/okstra_ctl/sequence.py:31`. Highest seq is latest. Cross-verify with `timeline.json`'s `runs[].taskType` filter.
|
|
302
|
+
D. **Specific task-type (fallback):** `latestReportPath` is task-type-agnostic. For a specific task-type's latest report, look under `.okstra/tasks/<group-segment>/<id-segment>/runs/<task-type-segment>/reports/`, filename pattern `final-report-<task-type-segment>-<NNN>.md` per `scripts/okstra_ctl/sequence.py:31`. Highest seq is latest. Stage-isolated runs (`implementation`, single-stage `final-verification`) keep their reports one level deeper at `runs/<task-type-segment>/stage-<N>/reports/` — scan those subdirectories too; seq is independent per stage. Cross-verify with `timeline.json`'s `runs[].taskType` filter.
|
|
303
303
|
|
|
304
304
|
### report.2 — Confirm existence
|
|
305
305
|
|