okstra 0.106.1 → 0.107.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.
@@ -32,10 +32,11 @@
32
32
  2. AI의 해석, 파일 링크, 용어 매핑, format 변환은 모두 `Augmentation` 또는 `> augmented:` blockquote에 둔다.
33
33
  3. augmentation은 네 label 중 하나를 가진다: `evidence-link`, `format-conversion`, `terminology-mapping`, `intent-inference`.
34
34
  4. `intent-inference`는 `Open Questions`의 `intent-check:`와 짝을 이룬다. 이 관계는 `validators/validate-brief.py`가 검사한다.
35
- 5. unresolved `terminology-mapping`은 `Open Questions`의 `terminology:`와 짝을 이룬다. 관계도 validator검사한다.
35
+ 5. `terminology-mapping` augmentation은 `Open Questions`의 `terminology:`와 짝을 이룬다(validator 검사). Step 4.5 결과 marker `applied glossary:` / `skipped glossary:`는 예외 — 짝 row필요 없다.
36
36
  6. reporter만 답할 수 있는 질문은 Step 6.5에서 모아 `## Reporter Confirmations`에 verbatim으로 기록한다.
37
37
  7. 티켓 분리/연결/순서 관계는 `## Related Task Graph`의 구조화된 table에 둔다. parent-id만으로 작업 순서를 추론하지 않는다.
38
38
  8. 모든 okstra-owned write는 `<PROJECT_ROOT>/.okstra/` 안에 둔다. 외부 파일은 reporter가 source로 명시한 경우에만 읽는다.
39
+ 9. `Open Questions`의 모든 row는 다섯 prefix 중 하나로 시작한다: `general:`, `terminology:`, `intent-check:`, `conversion-block:`, `adr-candidate:` (validator 강제). `adr-candidate:`는 신호일 뿐 — decision file은 `implementation-planning`이 `<PROJECT_ROOT>/.okstra/decisions/`에 쓴다.
39
40
 
40
41
  ## Preflight
41
42
 
@@ -201,10 +202,12 @@ brief 본문 top blockquote의 `Recommended next phase:`에 쓴다.
201
202
 
202
203
  이미 `[CONFIRMED <date> → RC-N]` marker가 있으면 pending에서 제외한다. 사용자에게 지금 답할지 묻고, 답하면 `## Reporter Confirmations`에 verbatim으로 기록한다. row는 삭제하지 않고 marker를 붙인다.
203
204
 
205
+ 한 run당 최대 12문항. pending이 12개를 넘으면 `conversion-block:` → `intent-check:` 순으로 상위 12개만 묻고, 나머지는 `partial`로 남긴 뒤 어떤 row가 남았는지 사용자에게 알린다.
206
+
204
207
  상태값:
205
208
 
206
- - `complete`: pending reporter-only row가 모두 답변됨.
207
- - `partial`: 일부만 답변됨.
209
+ - `complete`: pending reporter-only row가 모두 답변됨. validator는 모든 `intent-check:`/`conversion-block:` row에 `[CONFIRMED …]` marker가 있는지 검사한다.
210
+ - `partial`: 일부만 답변됨. validator는 최소 1개 row에 `[CONFIRMED …]` marker가 있는지 검사한다(아무것도 못 받았으면 `skipped`).
208
211
  - `skipped`: 사용자가 downstream phase에 넘기기로 함.
209
212
  - `pending`: handoff 전 상태로 보고 진행하지 않는다.
210
213
 
@@ -80,7 +80,7 @@ okstra container up --project-root <projectRoot> --task-key <task-key>
80
80
  - `final-verification(whole-task): stage N not done`: 해당 stage를 implementation으로 끝내야 한다고 말한다.
81
81
  - healthcheck 실패: failing service와 CLI가 제공한 `docker compose ... logs` line을 그대로 전달한다.
82
82
 
83
- 성공하면 stdout JSON을 파싱해 services, watcher pane, published ports를 요약한다. 이후 관리는 `okstra container status <task-key>`와 `down <task-key>`로 한다고 안내한다.
83
+ 성공하면 stdout JSON을 파싱해 services, watcher pane, published ports를 요약한다. 이후 관리는 `okstra container status <task-key>`와 `down <task-key>`로 한다고 안내한다. 띄운 뒤 *무엇을 확인할지*는 implementation 리포트의 §5.7.9 Manual User Test (Draft)를 가리킨다 — 그 단계와 기대 결과가 이 빌드의 수동 테스트 스크립트다.
84
84
 
85
85
  ## status
86
86
 
@@ -40,7 +40,7 @@ okstra ensure-installed --runtime claude-code
40
40
  okstra check-project --json
41
41
  ```
42
42
 
43
- runtime이나 project setup 없으면 `/okstra-setup` 안내 후 멈춘다. 이후 `projectRoot`를 literal 값으로 사용한다.
43
+ `check-project`는 Bash 호출의 cwd만 본다. cwd가 아닌 프로젝트(형제 repo·monorepo 하위·요청에 명시된 프로젝트)에서 `ok:false`는 setup 부재가 아니라 false negative다 — 바로 멈추지 말고 `okstra check-project --cwd <that-dir> --json`으로 재시도한다(`--cwd`는 leading `cd` 없이 프로젝트를 지정하는 sanctioned 방식). 그것도 `ok:false`일 때만 `/okstra-setup` 안내 후 멈춘다. 이후 `projectRoot`를 literal 값으로 사용하고, 이를 받는 sub-command CLI(`recap`, `context-cost` 등)에는 `--cwd`/`--project-root <projectRoot>`로 전달한다.
44
44
 
45
45
  ## intent routing
46
46
 
@@ -134,9 +134,9 @@ task 하나의 detail은 catalog를 먼저 보고, 필요하면 `.okstra/tasks/<
134
134
 
135
135
  catalog가 없으면 `.okstra/tasks/*/*/task-manifest.json` glob으로 fallback한다. disk manifest가 source of truth다.
136
136
 
137
- Re-run은 source run-manifest에서 `projectId`, `taskGroup`, `taskId`, `taskType`, `taskBriefPath`, workers, relatedTasks, model overrides 등을 뽑아 command를 조립한다. `implementation`의 `--base-ref`는 run-manifest에 없고 worktree registry에 있으므로 상황에 따라 묻는다.
137
+ Re-run은 source run-manifest에서 `projectId`, `taskGroup`, `taskId`, `taskType`, `taskBriefPath`, workers, relatedTasks, model overrides 등을 뽑아 command를 조립한다. `implementation`의 `--base-ref`는 run-manifest에 없고 worktree registry에 있으므로 상황에 따라 묻는다. migrated task(빈 `latestRunManifestPath`)는 바로 포기하지 않는다 — `runs/**/manifests/run-manifest-*.json`을 on-disk glob해 파일명 타임스탬프 최신을 고르고, glob도 비었을 때만 사용자에게 묻는다.
138
138
 
139
- Resume은 `latestResumeCommandPath` 또는 timeline entry의 `resumeCommandPath`를 확인하고 파일이 있으면 `bash <resume-command-path>`를 안내/실행한다.
139
+ Resume은 `latestResumeCommandPath` 또는 timeline entry의 `resumeCommandPath`를 확인하고 파일이 있으면 `bash <resume-command-path>`를 안내/실행한다. catalog/timeline 경로가 비었거나 옛 `.project-docs/okstra/...`를 가리키면 `runs/**/sessions/claude-resume-*.sh`를 glob해 타임스탬프 최신을 고른다. glob도 비었을 때만 "resume 없음"으로 선언한다.
140
140
 
141
141
  ## report
142
142
 
@@ -147,7 +147,7 @@ report path 해석 우선순위:
147
147
  3. `history/timeline.json`의 특정 run `reportPath`
148
148
  4. 특정 task-type fallback: `runs/<task-type>/reports/final-report-*.md`, stage-isolated path 포함
149
149
 
150
- report file 존재를 확인한 뒤, 읽을지 사용자에게 묻는다. 완료 signal은 있는데 file이 없으면 missing report로 말한다. 아직 완료되지 않았으면 current status와 workStatus를 보여준다.
150
+ 읽기 깊이를 요청에 맞춘다(final report 300+줄/50K+ 토큰). 요약/결론/통과 여부 질문("요약", "핵심만", "결론", "통과했어?")은 전체를 읽지 말고 `runs/<task-type-segment>/status/final-<task-type-segment>-<NNN>.status`(stage-isolated: `runs/<task-type-segment>/stage-<N>/status/`) sidecar의 판정 + report 선두 요약 블록만 읽는다. "전체/다 읽어/본문"일 때만 전체 ingest한다. 완료 signal은 있는데 file이 없으면 missing report로 말하고, 아직 완료되지 않았으면 current status와 workStatus를 보여준다.
151
151
 
152
152
  ## time
153
153
 
@@ -157,7 +157,7 @@ report file 존재를 확인한 뒤, 읽을지 사용자에게 묻는다. 완료
157
157
  okstra time-report <task-key> --project-root <projectRoot> --json
158
158
  ```
159
159
 
160
- 모든 `*Ms`는 `HH:MM:SS`로 변환해 보여준다. `CPU sum`은 lead와 workers 시간을 합친 overlapping cost이며 wall-clock이 아니다. 사용자가 wall-clock/phase breakdown을 물을 때만 `perRunWallClock`, `phaseTimelines`를 보여준다.
160
+ 모든 `*Ms`는 `HH:MM:SS`로 변환해 보여준다. `CPU sum`은 lead와 workers 시간을 합친 overlapping cost이며 wall-clock이 아니다. wall-clock 사용자가 명시적으로 물을 때만 `perRunWallClock`에서 보여준다. `단계별`/`stage별`/`어느 단계가 오래`는 task-type 뷰(By task type 표)가 기본 답이다 — '측정 불가'로 처리하지 않는다. intra-run `phaseTimelines`는 'Phase 1~7'·'어느 phase가' 같은 명시적 요청일 때만 렌더하고, 비어 있으면 헤드라인이 아니라 각주로만 언급한다.
161
161
 
162
162
  `unavailable[]`은 totals에 합치지 않고 별도 note로 표시한다.
163
163
 
@@ -26,7 +26,7 @@ okstra manager init --manager-id <manager-id> --json
26
26
  okstra manager discover-projects --json
27
27
  okstra manager new project --manager-id <manager-id> --project-id <project-id> --project-root <abs-path> [--role <role>] [--tag <tag>] --json
28
28
  okstra manager new task-group --manager-id <manager-id> --task-group <task-group> --json
29
- okstra manager new task --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --task <project-id:task-group:task-id> --json
29
+ okstra manager new task --manager-id <manager-id> --task-group <task-group> --task-id <task-id> [--task <project-id:task-group:task-id> ...] [--objective <text>] [--common-brief <path>] [--progress-mode <manual|auto>] --json
30
30
  okstra manager task assign --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --project-id <project-id> [--child-task-id <child-task-id>] [--role <role>] [--tag <tag>] [--assignment <text>] --json
31
31
  okstra manager task note --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --scope <shared|project> [--project-id <project-id>] --body <text> --json
32
32
  okstra manager task sync --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --json
@@ -36,11 +36,16 @@ okstra manager task run --manager-id <manager-id> --project-id <project-id> --ta
36
36
 
37
37
  ## 저장 모델
38
38
 
39
- Manager state 는 `~/.okstra/managers/<manager-id>/` 아래에 저장된다.
39
+ Manager state 는 `~/.okstra/managers/<manager-id>/` 아래에 저장되며, manager 루트와 task 디렉터리 두 계층으로 나뉜다.
40
+
41
+ manager 루트 직하:
40
42
 
41
43
  - `manager.json`: manager id / schema / createdAt
42
44
  - `projects.json`: 등록된 projectId, projectRoot, role, tags
43
- - `task-groups/<safe-group>/<safe-task>/manifest.json`: manager task objective, common brief, progress mode
45
+
46
+ task 디렉터리 `task-groups/<safe-group>/<safe-task>/` 아래:
47
+
48
+ - `manifest.json`: manager task objective, common brief, progress mode
44
49
  - `children.json`: child task plan, assignment, launch metadata
45
50
  - `directives.jsonl`: shared/project directive rows
46
51
  - `snapshots.json`: `task sync` 가 project-local `.okstra` 에서 읽어온 read-side snapshot
@@ -53,6 +58,7 @@ Manager state 는 `~/.okstra/managers/<manager-id>/` 아래에 저장된다.
53
58
 
54
59
  `task run` 은 child 작업을 직접 실행하지 않고 launch packet 을 준비한다. 반환 packet 의 핵심 필드는:
55
60
 
61
+ - `taskKey`: child 의 `project-id:task-group:task-id` (child 식별 공개 키 — `child-launch-prepared` event 에도 기록)
56
62
  - `backend`: `$TMUX` 가 있으면 `tmux-child-lead`, 없으면 `subagent-child-lead`
57
63
  - `workerDispatchBackend`: v1 에서는 항상 `subagent`
58
64
  - `projectRoot`: child project root
@@ -132,7 +132,7 @@ okstra wizard confirmation --state-file /tmp/okstra-wizard/state.json
132
132
  okstra wizard render-args --state-file /tmp/okstra-wizard/state.json
133
133
  ```
134
134
 
135
- `args` object의 모든 key를 `okstra render-bundle` flag로 전달한다. 빈 string 값도 명시적으로 전달한다.
135
+ `args` object의 key를 `okstra render-bundle` flag로 전달한다. 빈 string 값도 명시적으로 전달한다. 예외: `chain-stages` key는 render-bundle flag가 아니다 — Step 7 무인 연쇄 루프를 구동하는 값이라 flag로 넘기지 않는다(`run.py`는 `--stage`/`--stages`만 받는다). render-bundle block의 `--stage`는 implementation·final-verification 전용, `--stages`는 release-handoff 전용(빈 값 = whole-task)이다.
136
136
 
137
137
  ```bash
138
138
  okstra render-bundle \
@@ -189,7 +189,7 @@ implementation에서 Tier 3 conformance가 환경상 불가능할 때만 사용
189
189
  사용자에게 세 선택지를 준다.
190
190
 
191
191
  1. no-team background로 진행.
192
- 2. 대기. prepared artifacts는 보존된다.
192
+ 2. 대기 dispatch 보류, stage worktree·run context 보존. 점유 run 종료 후 같은 stage를 resume으로 재개하도록 resume 명령(`okstra-inspect` history → resume)을 사용자에게 출력한다.
193
193
  3. 직접 입력.
194
194
 
195
195
  이 picker는 스킬이 만드는 것이므로 wizard option 축약 금지 규칙과 별개다.
@@ -226,6 +226,22 @@ render-bundle 후 `<INSTRUCTION_SET_PATH>/claude-execution-prompt.md`를 verbati
226
226
  Took over as Claude lead for `<taskKey>` (`<task-type>`). Run dir: `<RUN_DIR_RELATIVE_PATH>`. Beginning Phase 1 (context loading).
227
227
  ```
228
228
 
229
+ 단일 원소 chain이면 Step 6 종료가 곧 run 종료다. 아래 Step 7은 `chain-stages` CSV가 2개 이상일 때만 적용된다.
230
+
231
+ ## implementation 무인 연쇄 (chain-stages)
232
+
233
+ `task-type == implementation`이고 render-args `chain-stages` CSV가 원소 2개 이상이면, 현재 세션이 오케스트레이터로서 stage를 의존성 순서대로 무인 연쇄 실행한다. 큐 = `chain-stages`를 `,`로 분해한 위상정렬 stage 리스트. 큐의 각 stage `N`에 대해 순서대로:
234
+
235
+ 1. render-bundle을 같은 인자로, 단 `--stage N`으로 재호출한다(base commit은 prepare가 predecessor의 done `head_commit`으로 자동 계산 — 손으로 넘기지 않는다). conformance waiver·concurrent-run·git-reconcile 게이트는 매 stage의 render-bundle마다 동일 적용.
236
+ 2. Step 6대로 Claude lead가 되어 그 stage의 Phase 1~7을 인라인 실행한다. Phase 6의 lead persistence가 `runs/<plan-task-key>/consumers.jsonl`에 그 stage의 `status:"done"` 행을 append한다.
237
+ 3. `done` 행 기록을 확인한 뒤 다음 stage로 넘어간다. stage 경계마다 컨텍스트(잔여 pane·완료 teammate)를 정리한다.
238
+ 4. stage 시작/완료마다 한 줄 보고: `stage N/<총개수> 시작` / `stage N done → 다음 K`.
239
+
240
+ 큐를 모두 소화하면 연쇄를 종료하고 완료를 보고한다.
241
+
242
+ - **다음 stage가 아직 ready 아님 — 정상 종료:** 큐의 stage가 다른 implementation run에 started/reserved로 점유돼 render-bundle이 `--stage N already in progress or reserved by another run`(StageTargetError)으로 거부되면, 이는 예외가 아니다 — 연쇄를 **정상 종료**하고 남은 큐를 보고한다(예: `남은 큐: stage 4, 5 — 점유 해제 후 okstra-run으로 재개`).
243
+ - **연쇄 중 예외 게이트:** render-bundle이 concurrent-run 충돌이나 git stale-SHA 재조정을 띄우면 **그 stage에서 연쇄를 멈추고** 게이트를 Step 5 절차대로 사용자에게 제시한다. 사용자가 해소하면 그 자리에서 남은 큐를 이어 재개한다. 데이터 손상·동시 점유 충돌은 사람이 확인 — 무인 연쇄의 안전 경계다.
244
+
229
245
  ## 금지 패턴
230
246
 
231
247
  - wizard가 내준 질문 순서를 바꾸기.
@@ -43,7 +43,7 @@ runtime이나 project setup이 없으면 `/okstra-setup` 안내 후 멈춘다.
43
43
 
44
44
  ## model gate
45
45
 
46
- 스킬 frontmatter는 `model: opus`다. 활성 모델이 Opus-class 또는 Fable-class가 아니면 사용자에게 전환을 권한다. 사용자가 lower model로 진행하겠다고 명시하면 진행하되, 생성 schedule 상단에 reduced-quality warning을 넣는다.
46
+ 스킬 frontmatter는 `model: opus`다. 활성 모델이 Opus-class 또는 Fable-class가 아니면 사용자에게 전환을 권한다. 사용자가 lower model로 진행하겠다고 명시하면 진행하되, 생성 schedule 상단에 verbatim warning 한 줄을 넣는다: `> ⚠️ Generated with <model> (not Opus). Cross-task synthesis quality may be reduced.`
47
47
 
48
48
  ## task-group 해석
49
49
 
@@ -53,6 +53,8 @@ runtime이나 project setup이 없으면 `/okstra-setup` 안내 후 멈춘다.
53
53
  4. raw `taskGroup` fallback은 쓰지 않는다.
54
54
  5. 매칭된 각 task의 `task-manifest.json`을 직접 읽는다. catalog는 stale일 수 있고 manifest가 authoritative다.
55
55
 
56
+ 0개 매칭이면 `해당 task-group을 찾을 수 없습니다.`를 출력하고 멈춘다.
57
+
56
58
  ## workStatus filter
57
59
 
58
60
  각 manifest에서 workStatus를 확인한다. 없거나 비어 있으면 `okstra-inspect`의 `status.4` inference table을 적용한다.
@@ -70,7 +72,7 @@ runtime이나 project setup이 없으면 `/okstra-setup` 안내 후 멈춘다.
70
72
  - `phase-done`
71
73
  - 그 밖의 non-done inferred/display 상태
72
74
 
73
- 필터 후 0개면 schedule file을 만들지 않는다.
75
+ 필터 후 0개면 schedule file을 만들지 않고 `해당 task-group의 모든 task가 done 상태입니다. 생성할 schedule이 없습니다.`를 출력한다.
74
76
 
75
77
  ## per-task data extraction
76
78
 
@@ -148,7 +150,7 @@ ambiguous하면 가장 가까운 phase에 두고 rationale을 phase section 상
148
150
  top-level 계약 sections:
149
151
 
150
152
  1. `## At a Glance`
151
- 2. `## Executive Summary`
153
+ 2. `## Executive Summary` (필수 subsection `### Effort Sizing 기준` 포함 — `validate-schedule.py`가 검사)
152
154
  3. `## Task Dependency Graph`
153
155
  4. `## Phase 1: Critical Fixes`
154
156
  5. `## Phase 2: Enhancements`
@@ -176,7 +178,7 @@ phase에 task가 없어도 heading을 유지하고 `_없음_`을 쓴다.
176
178
  > Source: okstra <mode> (<N> tasks included, <M> done excluded)
177
179
  ```
178
180
 
179
- title suffix `— Work Schedule`은 validator가 검사한다.
181
+ title suffix `— Work Schedule`은 validator가 검사한다. `<project-id>`는 `task-catalog.json` 최상위 `projectId`를 우선 쓰고, 없으면 첫 매칭 manifest의 `projectId`를 쓴다. 임의 값 생성 금지.
180
182
 
181
183
  ## At a Glance
182
184
 
@@ -215,8 +217,8 @@ enum 값:
215
217
 
216
218
  1. `**Problem**:`
217
219
  2. `**Solution**:`
218
- 3. `**Work Breakdown**:`
219
- 4. `**Verification Commands**:`
220
+ 3. `**Work Breakdown**:` — 뒤에 `| Step | File | Action | Detail |` table
221
+ 4. `**Verification Commands**:` — 뒤에 ` ```bash ` fenced block
220
222
  5. `**Rollback**:`
221
223
 
222
224
  `[NEEDS-OKSTRA-RUN]` 또는 `[PARSE-ERROR: <section>]` task는 가능한 field만 채우되 marker를 task heading 바로 아래 둔다.
@@ -242,6 +244,8 @@ arrow는 ASCII `->`만 사용한다.
242
244
 
243
245
  ## Gantt Chart
244
246
 
247
+ **directive override (최우선):** 아래 휴리스틱 적용 전 directive source의 `## Directive` section을 먼저 확인한다. 해석 순서(first hit): (1) `--directive-file <abs-path>` 인자, (2) `<PROJECT_ROOT>/.okstra/tasks/<task-group-segment>/schedule/instruction-set/analysis-material.md`, (3) 없으면 휴리스틱 그대로(정상 경로 — 경고·중단 없음). directive가 Gantt 렌더/생략을 지시하면 휴리스틱·skip 규칙을 덮어쓰고, 해당 section에 `> _Per Directive directive: <verbatim 발췌>._` 한 줄을 남긴다. directive가 day allocation·phase weight·sub-task 분해를 주면 bar 길이에 verbatim 반영한다.
248
+
245
249
  기본은 렌더한다. day signal이 하나라도 있으면 rough estimate라도 chart를 만든다.
246
250
 
247
251
  렌더 조건 예:
@@ -278,7 +282,7 @@ axis는 relative day count만 사용한다. calendar date, weekday, "오늘 + N"
278
282
 
279
283
  선택 A: 짧은 설명으로 inline replacement.
280
284
 
281
- 선택 B: 반복되는 code가 많으면 마지막 `## Glossary`에 모든 code를 해석한다.
285
+ 선택 B: 반복되는 code가 많으면 마지막 `## Glossary`에 모든 code를 해석한다. glossary table header는 정확히 `| Code | Description |`(영문 literal — validator가 검사). milestone code(`M1`, `M2` …)도 `FC-N`과 동일하게 glossary 미해석 시 validator fail.
282
286
 
283
287
  decision-item letter code(`A1`, `B2`, `C3`, `D4`)는 schedule에 넣지 않는다.
284
288
 
@@ -40,7 +40,7 @@
40
40
  npx -y okstra@latest install --runtime claude-code
41
41
  ```
42
42
 
43
- 이 명령은 idempotent로 취급한다. 이미 설치되어 있어도 다시 실행해 runtime, skill, agent payload를 현재 package 버전과 맞춘다. 실패하면 stderr를 그대로 사용자에게 보여준다. legacy `okstra-install.sh`로 우회하지 않는다.
43
+ 이 명령은 idempotent로 취급한다. 이미 설치되어 있어도 다시 실행해 runtime, skill, agent payload를 현재 package 버전과 맞춘다(agent payload = `~/.claude/agents/<worker>.md` worker 정의 + `~/.okstra/installed-agents.json` manifest). 실패하면 stderr를 그대로 사용자에게 보여준다. legacy `okstra-install.sh`로 우회하지 않는다.
44
44
 
45
45
  ## 명령 호출 규칙
46
46
 
@@ -123,7 +123,7 @@ okstra doctor --runtime claude-code
123
123
 
124
124
  짧게 다음 정보를 포함한다.
125
125
 
126
- - runtime 위치: `~/.okstra`
126
+ - runtime 위치: `~/.okstra` (설치 요약의 `version stamp: x.y.z` 줄을 함께 보여준다)
127
127
  - project metadata: `<PROJECT_ROOT>/.okstra/project.json`
128
128
  - `projectId`
129
129
  - 다음 단계: `/okstra-run`
@@ -635,7 +635,7 @@ Claude가 작성하는 최종 보고서는 아래 구조를 우선 사용합니
635
635
  - `## 1. 문제 또는 검증 대상 요약` — §6.1 Consensus / §6.2 Differences 표 각각 `Source items (worker:item)` 컬럼 보존 (cross-worker traceability).
636
636
  - `## 2. 에이전트별 실행 현황`
637
637
  - `## 3. Cross Verification 결과` — §2.1 Primary Evidence 에 `Source items (worker:item)` + `Source (path:line / log)` 컬럼.
638
- - `## 4. 최종 판단` — `implementation-planning` 의 §5.5.9 Plan Body Verification 은 `Verdict details` (5-열, plan item × worker) 하나로 emit.
638
+ - `## 4. 최종 판단` — `implementation-planning` 의 §5.5.9 Plan Body Verification 은 plan item 별로 그룹핑해 emit: 각 항목의 `subject`(무엇을 검증했는지 한 줄) 를 헤딩으로, 그 아래 워커별 `Worker / Verdict / Breakage kind / Note` 표를 붙이고, gate 값·verdict 토큰·breakage kind(a–f) 범례 3종을 함께 렌더한다.
639
639
  - `## 1. Clarification Items` — 통합 8-열 표 한 곳. 기존 §6.1 / §6.2 / §5.5.8 / §5.5.9 Open Questions 는 deprecated 되어 validator 가 등장 시 fail.
640
640
  - `## 6. 권장 다음 단계`
641
641
  - `## Token Usage Summary` — sentinel (`pending` / `N/A` / `--` / `?` / 빈 셀) 또는 zero (`0` / `$0.00`) 박제 시 validator 가 출고를 차단합니다. `Codex/Antigravity CLI 추가 비용` 행만 "CLI 미사용" 의미로 `$0.00` 허용.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.106.1",
3
+ "version": "0.107.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.106.1",
3
- "builtAt": "2026-06-28T06:22:45.711Z",
2
+ "package": "0.107.0",
3
+ "builtAt": "2026-07-01T03:36:52.358Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -94,7 +94,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
94
94
  - **Reading Confirmation** lives in the audit sidecar (`runs/<task-type>/worker-results/report-writer-worker-audit-<task-type>-<seq>.md`), never in the data.json or the main worker-results file.
95
95
  - Include all four convergence categories. The schema's `crossVerification.consensus` / `.differences` arrays carry full / partial / contested / worker-unique items; do not omit any.
96
96
  - Convergence round history goes in `crossVerification.roundHistory.rounds[]` with `round2SkippedReason`. When convergence is disabled, set `crossVerification.roundHistory` to `{"disabled": true}`. Values come verbatim from `state/convergence-<task-type>-<seq>.json` — do not recompute.
97
- - `verification-error` votes are their own verdict (`verdictDetails[].verdict` enum); they are NOT folded into AGREE / DISAGREE counts.
97
+ - `verification-error` votes are their own verdict (`planItems[].verdicts[].verdict` enum); they are NOT folded into AGREE / DISAGREE counts.
98
98
  - **Token Usage cells are `null` in Phase 6.** Leave `tokenUsage.lead.totalTokens` / `.billableTokens` / `.costUsd` (and the worker / grand rows, and per-row `executionStatus[].totalTokens` etc.) as JSON `null`. The renderer emits `--` for nulls. Phase 7's `okstra-token-usage.py --substitute-data` populates them and re-renders. **Never** write `0`, `"not-collected"`, `"--"`, or any sentinel value — those are how zeros sneak into the report.
99
99
  - If only one analysis worker produced usable output, perform a reduced-confidence write-up and say so explicitly (e.g. note in `executionStatus[].summary`).
100
100
  - If evidence is missing, write `"I don't know"` in the relevant statement field rather than fabricating confidence.
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # Purpose: Claude Code's Bash permission matcher requires explicit approval for
5
5
  # commands that contain shell metacharacters (stdout/stderr redirects, pipes).
6
- # `agy --print "<prompt>" --model <model> --add-dir <root> 2>>log > >(tee)`
6
+ # `agy --print "<prompt>" --model <model> --add-dir <root> 2>>log > <fifo>`
7
7
  # therefore triggers a permission prompt every dispatch even when `Bash(agy:*)`
8
8
  # is allowlisted, because the redirect tokens disqualify the simple-prefix match.
9
9
  #
@@ -128,6 +128,11 @@ log_path="${prompt_path%.md}.log"
128
128
  [[ "$log_path" == "$prompt_path" ]] && log_path="${prompt_path}.log"
129
129
  : > "$log_path"
130
130
 
131
+ # stdout-mirror FIFO path (created at the dispatch block below). Declared here,
132
+ # before the EXIT trap is defined, so the trap's cleanup reference is always set
133
+ # under `set -u`.
134
+ stdout_fifo=""
135
+
131
136
  # Heartbeat sidecar (`<prompt>.status.json`). See `okstra-codex-exec.sh` for
132
137
  # the full design comment — kept in lock-step. Two consumers:
133
138
  # 1. `antigravity-worker` step 8c reads `log_path` to capture a diagnostic
@@ -186,6 +191,7 @@ _okstra_status_finish() {
186
191
  if [[ -n "$caller_pane" && -n "$original_caller_title" ]]; then
187
192
  tmux select-pane -t "$caller_pane" -T "$original_caller_title" 2>/dev/null || true
188
193
  fi
194
+ [[ -n "$stdout_fifo" ]] && rm -f "$stdout_fifo" 2>/dev/null || true
189
195
  }
190
196
  trap _okstra_status_finish EXIT
191
197
 
@@ -236,10 +242,22 @@ fi
236
242
  # can SIGTERM from the watchdog.
237
243
  # stderr: appended to the live log only — keeps the wrapper's stderr clean.
238
244
  # exit: agy's own exit code is preserved by `wait`.
245
+ # stdout mirror via a named FIFO instead of a `> >(tee …)` process substitution:
246
+ # bash opens the substituted `/dev/fd/<n>` by pathname, which the macOS seatbelt
247
+ # sandbox around a non-tmux subagent's Bash tool denies (EPERM), so agy would
248
+ # never start. A FIFO under `.okstra/` is opened by an ordinary sandbox-writable
249
+ # path and works in every dispatch context while keeping agy a single
250
+ # addressable PID for the watchdog. See `okstra-codex-exec.sh` for the full note.
251
+ stdout_fifo="${log_path}.stdout.fifo"
252
+ rm -f "$stdout_fifo"
253
+ mkfifo "$stdout_fifo"
254
+ tee -a "$log_path" < "$stdout_fifo" &
255
+ stdout_tee_pid=$!
256
+
239
257
  agy --print "$(cat "$prompt_path")" --model "$model" "${add_dir_args[@]}" \
240
258
  --print-timeout "$PRINT_TIMEOUT" --dangerously-skip-permissions \
241
259
  2>> "$log_path" \
242
- > >(tee -a "$log_path") &
260
+ > "$stdout_fifo" &
243
261
  agy_pid=$!
244
262
 
245
263
  # Idle watchdog — see `okstra-codex-exec.sh` for the full rationale.
@@ -285,6 +303,9 @@ if [[ -n "$watchdog_pid" ]]; then
285
303
  wait "$watchdog_pid" 2>/dev/null || true
286
304
  fi
287
305
 
288
- wait 2>/dev/null || true
306
+ # Drain the stdout-mirror tee (agy's closed write end gives it EOF), then remove
307
+ # the FIFO (the EXIT trap also clears it on an abnormal exit).
308
+ wait "$stdout_tee_pid" 2>/dev/null || true
309
+ rm -f "$stdout_fifo"
289
310
 
290
311
  exit "$agy_exit"
@@ -61,6 +61,11 @@ fi
61
61
  log_path="${prompt_path%.md}.log"
62
62
  [[ "$log_path" == "$prompt_path" ]] && log_path="${prompt_path}.log"
63
63
  : > "$log_path"
64
+ # stdout/stderr mirror FIFO paths (created at the dispatch block below). Declared
65
+ # here, before the EXIT trap is defined, so the trap's cleanup references are
66
+ # always set under `set -u`.
67
+ stdout_fifo=""
68
+ stderr_fifo=""
64
69
  status_path="${prompt_path}.status.json"
65
70
  started_ts=$(date +%s)
66
71
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
@@ -76,15 +81,34 @@ _okstra_status_finish() {
76
81
  python3 "$script_dir/okstra-wrapper-status.py" \
77
82
  finish "$status_path" "$exit_code" "$ended_ts" "$duration_ms" \
78
83
  >>"$log_path" 2>&1 || true
84
+ { [[ -n "$stdout_fifo" ]] && rm -f "$stdout_fifo"; [[ -n "$stderr_fifo" ]] && rm -f "$stderr_fifo"; } 2>/dev/null || true
79
85
  }
80
86
  trap _okstra_status_finish EXIT
81
87
 
82
88
  cd "$project_root"
83
89
  set +e
90
+ # stdout and stderr are each mirrored to the live log AND the wrapper's own
91
+ # stdout/stderr. Process substitutions (`> >(tee …)`) do this, but bash opens the
92
+ # substituted `/dev/fd/<n>` by pathname, and the macOS seatbelt sandbox around a
93
+ # non-tmux subagent's Bash tool denies that open (EPERM) — claude then never
94
+ # starts and the wrapper exits with no output. Named FIFOs under `.okstra/` are
95
+ # opened by ordinary sandbox-writable paths, so the mirror works in every
96
+ # dispatch context (real tmux pane OR sandboxed subagent) while keeping claude a
97
+ # single addressable PID for the watchdog. See `okstra-codex-exec.sh` for the
98
+ # full note.
99
+ stdout_fifo="${log_path}.stdout.fifo"
100
+ stderr_fifo="${log_path}.stderr.fifo"
101
+ rm -f "$stdout_fifo" "$stderr_fifo"
102
+ mkfifo "$stdout_fifo" "$stderr_fifo"
103
+ tee -a "$log_path" < "$stdout_fifo" &
104
+ stdout_tee_pid=$!
105
+ tee -a "$log_path" < "$stderr_fifo" >&2 &
106
+ stderr_tee_pid=$!
107
+
84
108
  claude -p --model "$model" ${extra_args[@]+"${extra_args[@]}"} --output-format=stream-json \
85
109
  < "$prompt_path" \
86
- > >(tee -a "$log_path") \
87
- 2> >(tee -a "$log_path" >&2) &
110
+ > "$stdout_fifo" \
111
+ 2> "$stderr_fifo" &
88
112
  claude_pid=$!
89
113
 
90
114
  # Idle watchdog: if the live log sees no write for $idle_timeout_secs, SIGTERM
@@ -130,6 +154,9 @@ if [[ -n "$watchdog_pid" ]]; then
130
154
  kill "$watchdog_pid" 2>/dev/null || true
131
155
  wait "$watchdog_pid" 2>/dev/null || true
132
156
  fi
133
- wait 2>/dev/null || true
157
+ # Drain the stdout/stderr mirror tees (claude's closed write ends give them EOF),
158
+ # then remove the FIFOs (the EXIT trap also clears them on an abnormal exit).
159
+ wait "$stdout_tee_pid" "$stderr_tee_pid" 2>/dev/null || true
160
+ rm -f "$stdout_fifo" "$stderr_fifo"
134
161
 
135
162
  exit "$claude_exit"
@@ -167,6 +167,11 @@ log_path="${prompt_path%.md}.log"
167
167
  [[ "$log_path" == "$prompt_path" ]] && log_path="${prompt_path}.log"
168
168
  : > "$log_path"
169
169
 
170
+ # stdout-mirror FIFO path (created at the dispatch block below). Declared here,
171
+ # before the EXIT trap is defined, so the trap's cleanup reference is always set
172
+ # under `set -u`.
173
+ stdout_fifo=""
174
+
170
175
  # Heartbeat sidecar (`<prompt>.status.json`). The codex CLI streams progress
171
176
  # over stdout/stderr but the only structured signal the caller subagent
172
177
  # polls is `BashOutput`'s binary `running`/`completed` state. The sidecar
@@ -237,6 +242,7 @@ _okstra_status_finish() {
237
242
  if [[ -n "$caller_pane" && -n "$original_caller_title" ]]; then
238
243
  tmux select-pane -t "$caller_pane" -T "$original_caller_title" 2>/dev/null || true
239
244
  fi
245
+ [[ -n "$stdout_fifo" ]] && rm -f "$stdout_fifo" 2>/dev/null || true
240
246
  }
241
247
  trap _okstra_status_finish EXIT
242
248
 
@@ -307,10 +313,25 @@ fi
307
313
  # model instead of prompting, so the turn proceeds. The sandbox stays
308
314
  # `workspace-write`: this removes the approval gate, not the sandbox. It is the
309
315
  # codex-side equivalent of the antigravity wrapper's `--dangerously-skip-permissions`.
316
+ # stdout must reach BOTH the live log and the wrapper's own stdout (so the
317
+ # subagent's `BashOutput` captures the final text for Phase 5 synthesis). A
318
+ # process substitution (`> >(tee …)`) does this, but bash opens the substituted
319
+ # `/dev/fd/<n>` by pathname, and the macOS seatbelt sandbox wrapping a non-tmux
320
+ # subagent's Bash tool denies that open (EPERM) — codex then never starts and
321
+ # the wrapper exits with no output (the documented non-tmux dispatch failure). A
322
+ # named FIFO is opened by an ordinary path under `.okstra/` (sandbox-writable),
323
+ # so the mirror works in every dispatch context (real tmux pane OR sandboxed
324
+ # subagent) while keeping codex a single addressable PID for the watchdog.
325
+ stdout_fifo="${log_path}.stdout.fifo"
326
+ rm -f "$stdout_fifo"
327
+ mkfifo "$stdout_fifo"
328
+ tee -a "$log_path" < "$stdout_fifo" &
329
+ stdout_tee_pid=$!
330
+
310
331
  codex exec -C "$project_root" ${extra_args[@]+"${extra_args[@]}"} --model "$model" --sandbox workspace-write -c approval_policy=never - \
311
332
  < "$prompt_path" \
312
333
  2>> "$log_path" \
313
- > >(tee -a "$log_path") &
334
+ > "$stdout_fifo" &
314
335
  codex_pid=$!
315
336
 
316
337
  # Idle watchdog: poll the live log's mtime; if no write (stdout or stderr)
@@ -360,8 +381,10 @@ if [[ -n "$watchdog_pid" ]]; then
360
381
  wait "$watchdog_pid" 2>/dev/null || true
361
382
  fi
362
383
 
363
- # Drain the process-substitution tee so the final lines reach the live log
364
- # and the caller's stdout before exit.
365
- wait 2>/dev/null || true
384
+ # Drain the stdout-mirror tee so the final lines reach the live log and the
385
+ # caller's stdout before exit. codex's closed write end gives the tee EOF; then
386
+ # remove the FIFO (the EXIT trap also clears it on an abnormal exit).
387
+ wait "$stdout_tee_pid" 2>/dev/null || true
388
+ rm -f "$stdout_fifo"
366
389
 
367
390
  exit "$codex_exit"
@@ -144,6 +144,8 @@ If the answer is "no, I'd have to group several lines together and invent a name
144
144
  - A long `.map().filter().reduce()` chain where each stage does non-obvious work and no stage has a name.
145
145
  - Boolean expressions long enough that intent is buried — `if (a && b && !c && (d || e.f > 0) && ...)` without a named predicate.
146
146
  - A function that orchestrates 4+ distinct logical phases inline (*fetch → resolve → branch → persist → publish*), each more than a line or two. Even without deep nesting, that's enough work to warrant named sub-methods.
147
+ - A chain of `if (x === SomeEnum.A) return …` over the values of one enum — that's a hand-rolled switch. Use a real `switch (x)` so the cases line up and exhaustiveness is visible; it also drops the trailing fall-through `return` that an if-chain needs.
148
+ - A dense data literal — an object/struct/`{...}` carrying several inline ternaries or conditional spreads (`...(cond ? { x } : {})`) — assembled inline in a method that also does other work. Extract a named builder (`buildX(...)`) so the method reads as "compute X → assign X", and each conditional field gets a place to be understood.
147
149
 
148
150
  ### 50-line cap
149
151
 
@@ -8,6 +8,7 @@
8
8
  4. **Clean Code — names carry WHAT, comments explain WHY** — Identifiers must make intent obvious; if a comment would describe WHAT the code does, rename instead. Reserve comments for non-obvious WHY (hidden constraint, workaround, surprising invariant). Delete dead/commented-out code immediately — git history is the archive. *Example — WHAT (rename instead): `// increment counter` above `i++`. WHY (keep): `// retry up to 3x: upstream returns 502 during deploys`.*
9
9
  5. **Function length cap — 50 lines** — A single function/method body must stay within 50 lines, counting only effective code (exclude blank lines, comments, and pure data declarations such as large enums, lookup tables, or constant maps). Crossing the cap is an extraction signal, not a style nit. *Self-check: for any function newly added or substantially edited, count effective body lines; if over 50, split before declaring complete, or surface the violation and confirm with the user.*
10
10
  6. **Simplest conforming approach** — When choosing how to implement or fix something, pick the simplest, most direct solution path that still conforms to the project's architecture and established conventions. This is broader than KISS (#2): KISS bounds abstraction; this bounds the whole approach. Simplicity never licenses bending the structure or a convention — rule out non-conforming paths first, then take the plainest of what remains. Self-check: name the architecture/convention boundary the chosen approach respects; if a simpler path respects it too, take that one.
11
+ 7. **Fix at the cause, inside the change set** — An error surfacing during your work is fixed in the code that produced it — the files you are changing and the modules they directly call. A build/deploy/test failure is NOT a license to edit configuration or manifest files (`package.json`, lockfiles, `tsconfig`, CI config, build scripts) that were already passing before this change; a regression in something previously green almost always lives in the new code, not in long-stable config. If a config/manifest edit looks unavoidable, stop and confirm with the user first — state which file, why the current code can't carry the fix, and what was passing before. *Self-check: every file edited to resolve the error is in the current change set or a module it directly calls; no previously-passing config/manifest was touched to silence the failure without the user's explicit sign-off.* *Example violation: tests fail to import a new module, so you loosen `package.json`/`tsconfig` paths instead of fixing the import in the code you just wrote.*
11
12
 
12
13
  ## Routed resource selection
13
14
 
@@ -58,6 +59,7 @@ Per-file checks miss cross-cutting issues; each commit can be individually clean
58
59
 
59
60
  - [ ] **Domain-literal sweep:** `grep -rn` every domain enum value / predicate you added or touched in WHERE clauses, filters, or branches. The same literal at 2+ I/O sites is a *candidate* scattered decision — ask: would these sites change together when the business rule changes? Same decision → consolidate into one named constant or query builder in the domain layer and make every site reference it. Different decisions that merely share a value → leave them separate; coupling incidental duplication is worse than the repetition. (The identifier grep above does NOT catch this — sweep *values*, not just names.)
60
61
  - [ ] **Stand-alone name test for exports:** for each exported identifier, look at its siblings — can a caller pick the right one from the names alone? If a comment must explain which to use, the name fails; encode the distinguishing fact in it (e.g., the input shape: `parseRows` vs `parseRowsFromFlatItems`).
62
+ - [ ] **Self-mock sweep:** for every test file you added or edited, `grep` it for the SUT-stubbing patterns of this language (full list in `languages/*.md` → "Self-mock signals to refuse") — e.g. `spyOn(sut`, `sut.<method> = jest.fn`, `spyk(sut`, `Mockito.spy(`, `@Spy` paired with `@InjectMocks`, `patch(`-ing the class under test, `mockall::mock!` of the unit itself, plus private-reach hacks (`(sut as any).`, `ReflectionTestUtils.invokeMethod(sut`). Any hit where the stubbed/replaced target **is the unit under test** — not an injected collaborator — is a refused self-mock: delete the stub and exercise the real method, or the test only proves its own wiring and survives even if the real implementation is deleted. Mocking injected collaborators at the boundary stays fine; this sweep targets only stubs on the SUT itself. If a method on the SUT feels too painful to leave real, that's a design signal (extract it to a collaborator), not a license to stub it.
61
63
  - [ ] **No documented forks:** two deliberate variants of one capability must not survive as parallel implementations with a comment explaining the delta. Re-read both bodies and check the deltas are genuinely parametric: if they reduce to a few orthogonal options, collapse into one implementation taking explicit option parameters that encode them. If encoding the delta would take more than ~3 options, or add more branching than the duplication it removes, they are two capabilities — keep two implementations with distinct honest names and delete the "variant of" framing. Either way the comment-documented fork dies. "The divergence is documented" stays a refused rationalization, and a two-capabilities verdict must come from reading the bodies, not from reluctance to refactor.
62
64
 
63
65
  ## Boundaries
@@ -38,7 +38,7 @@ case "$file_path" in
38
38
  *.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs)
39
39
  lang="JavaScript-TypeScript"
40
40
  ref="languages/javascript-typescript.md"
41
- extra_hint=" If this file is part of a Node.js server (Express/Fastify/Nest/etc.), also read languages/nodejs.md."
41
+ extra_hint=" If this file is part of a Node.js server (Express/Fastify/Nest/etc.), also read frameworks/node-server.md."
42
42
  ;;
43
43
  *.py)
44
44
  lang="Python"
@@ -69,7 +69,7 @@ fi
69
69
  : > "$marker"
70
70
 
71
71
  skill_root="$HOME/.okstra/prompts/coding-preflight"
72
- msg="[coding-preflight] About to edit a ${lang} file (${file_path}). Before writing, you MUST Read ${skill_root}/${ref} plus ${skill_root}/clean-code.md (the skill is user-invocable:false — read the files directly).${extra_hint} If the project uses ports-and-adapters (domain/ + ports/ + adapters/, *.port.* files), also Read ${skill_root}/architecture/hexagonal.md. (Fires once per session per language.)"
72
+ msg="[coding-preflight] About to edit a ${lang} file (${file_path}). Before writing, you MUST Read ${skill_root}/${ref} plus ${skill_root}/clean-code.md (the skill is user-invocable:false — read the files directly).${extra_hint} If the project uses ports-and-adapters (domain/ + ports/ + adapters/, *.port.* files), also Read ${skill_root}/architectures/hexagonal.md. (Fires once per session per language.)"
73
73
 
74
74
  jq -nc \
75
75
  --arg event "PreToolUse" \
@@ -648,6 +648,8 @@ From the report-writer's draft of `## 5.4 Implementation Plan Deliverables`, lea
648
648
 
649
649
  Each plan item inherits the `[TICKETID: ...]` tag of its source section (per the standard ticket-tagging contract).
650
650
 
651
+ When extracting each item, lead also captures a **`subject`** — a plain one-line label (≤12 words) describing *what that item is* in the reader's terms, e.g. `P-Opt-1` → "Option A: upload v2 를 신규 모듈로 분리", `P-Step-1.1` → "Stage 1 Step 2: `npm run test:v2` 로 회귀 검증". This is a label-capture, not new analysis. The `subject` is what §5.5.9 renders as the per-item heading so the reader knows *what* each AGREE/DISAGREE is about without cross-referencing §4.5; a bare `P-*` ID with no subject is a contract violation.
652
+
651
653
  ### Plan-body verdict semantics
652
654
 
653
655
  The verdict tokens `AGREE` / `DISAGREE` / `SUPPLEMENT` are reused, but their meaning is plan-specific:
@@ -697,7 +699,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
697
699
  - all dispatches non-result → `aborted-non-result`
698
700
  - any `partial-consensus` / `dissent-isolated` present, no `majority-disagree` → `passed-with-dissent`
699
701
  - all items `full-consensus` → `passed`
700
- 6. Lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below) and populates `### 5.5.9 Plan Body Verification` in the final report (template at `templates/reports/final-report.template.md`). The §5.5.9 body uses a single `#### Verdict details` table (`Plan item / Worker / Verdict / Breakage kind / Note` — one row per plan-item × worker pair). The older wide `| Plan item | <worker1> | <worker2> | | Classification |` matrix and the former narrow `#### Verdict summary` card are both removed the matrix scaled horizontally with the worker count, and the summary only restated per-item classifications already derivable from the details table. The validator's `Plan Body Verification` + `Gate result:` substring checks gate this section.
702
+ 6. Lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below) and populates `### 5.5.9 Plan Body Verification` in the final report's data.json (`implementationPlanning.planBodyVerification`, schema `schemas/final-report-v1.0.schema.json`; template at `templates/reports/final-report.template.md`). The §5.5.9 body is **grouped by plan item**: `planItems[]`, each carrying its `id`, its plain-language `subject` (rendered as the item heading), an optional `sourceSection`, and a `verdicts[]` list (`worker / verdict / breakageKind / note`) — one verdict row per worker under that item. The renderer prints three fixed legends (gate values, verdict tokens, breakage kinds a–f) so the reader can decode every cell without opening this spec. The older flat `#### Verdict details` table (`Plan item / Worker / …`, one row per plan-item × worker pair) is superseded by the grouped layout it hid *what* each vote was about behind a bare `P-*` ID; the subject heading is the fix. The validator's `Plan Body Verification` + `Gate result:` substring checks still gate this section.
701
703
  7. For every `majority-disagree` item, lead adds a row to `## 1. Clarification Items` with:
702
704
  - new `C-<N>` ID (numbering continues from any existing rows)
703
705
  - `Statement` summarising the disagreement and the worker breakage `<kind>`
@@ -720,6 +722,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
720
722
  "planItems": [
721
723
  {
722
724
  "id": "P-Opt-1",
725
+ "subject": "Option A: upload v2 를 신규 모듈로 분리",
723
726
  "sourceSection": "4.5.1",
724
727
  "ticketId": "<id-or-unknown>",
725
728
  "votes": {"claude-worker": "AGREE", "codex-worker": "AGREE"},
@@ -728,6 +731,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
728
731
  },
729
732
  {
730
733
  "id": "P-Step-3",
734
+ "subject": "Stage 2 Step 3: 마이그레이션 실행 후 회귀 테스트",
731
735
  "sourceSection": "4.5.4",
732
736
  "ticketId": "TICKET-123",
733
737
  "votes": {"claude-worker": "DISAGREE(a)", "codex-worker": "DISAGREE(a)"},
@@ -335,7 +335,7 @@ Lead's responsibilities in this sub-step (in order):
335
335
  2. Dispatch a single plan-body reverify round to every analyser worker in the roster (`claude`, `codex`, and `antigravity` when opted in). `Report writer worker` is NOT a participant in this round.
336
336
  3. Aggregate verdicts and resolve the gate result to one of `passed` / `passed-with-dissent` / `blocked-by-disagreement` / `aborted-non-result`.
337
337
  4. Write `runs/<task-type>/state/plan-body-verification.json` (schema in the convergence contract).
338
- 5. Populate `### 5.5.9 Plan Body Verification` in the final-report file (template at `templates/reports/final-report.template.md` §5.5.9 — Round count, Gate result, Verdict table, Dissent log).
338
+ 5. Populate `### 5.5.9 Plan Body Verification` in the final-report file (template at `templates/reports/final-report.template.md` §5.5.9 — Round count, Gate result, per-item verdict tables grouped under each plan item's `subject`, Dissent log).
339
339
  6. For every `majority-disagree` plan item, append a row to `## 1. Clarification Items` with `Blocks=approval` and the 1:1 ID match in the verdict table's `Classification` column (`majority-disagree → C-<N>`). Do NOT create a parallel `Open Questions` block — see `prompts/profiles/implementation-planning.md` self-review step 6 for the orphan-on-either-side contract.
340
340
  7. Conditionally render the top-of-report `- [ ] Approved` marker line: present iff gate ∈ {passed, passed-with-dissent}, absent iff gate ∈ {blocked-by-disagreement, aborted-non-result}. `validators/validate-run.py` `validate_phase_boundary` enforces this correspondence. Manually flipping a blocked gate to passing in order to render the marker is a contract violation.
341
341
 
@@ -1458,7 +1458,7 @@
1458
1458
 
1459
1459
  "PlanBodyVerification": {
1460
1460
  "type": "object",
1461
- "required": ["roundCount", "gateResult", "verdictDetails", "dissentLog"],
1461
+ "required": ["roundCount", "gateResult", "planItems", "dissentLog"],
1462
1462
  "additionalProperties": false,
1463
1463
  "properties": {
1464
1464
  "roundCount": { "type": "integer", "minimum": 0 },
@@ -1470,20 +1470,32 @@
1470
1470
  "aborted-non-result"
1471
1471
  ]
1472
1472
  },
1473
- "verdictDetails": {
1473
+ "planItems": {
1474
1474
  "type": "array",
1475
1475
  "items": {
1476
1476
  "type": "object",
1477
- "required": ["planItem", "worker", "verdict"],
1477
+ "required": ["id", "subject", "verdicts"],
1478
1478
  "additionalProperties": false,
1479
1479
  "properties": {
1480
- "planItem": { "type": "string", "minLength": 1 },
1481
- "worker": { "type": "string", "minLength": 1 },
1482
- "verdict": {
1483
- "enum": ["AGREE", "DISAGREE", "SUPPLEMENT", "verification-error"]
1484
- },
1485
- "breakageKind": { "type": "string" },
1486
- "note": { "type": "string" }
1480
+ "id": { "type": "string", "minLength": 1 },
1481
+ "subject": { "type": "string", "minLength": 1 },
1482
+ "sourceSection": { "type": "string" },
1483
+ "verdicts": {
1484
+ "type": "array",
1485
+ "items": {
1486
+ "type": "object",
1487
+ "required": ["worker", "verdict"],
1488
+ "additionalProperties": false,
1489
+ "properties": {
1490
+ "worker": { "type": "string", "minLength": 1 },
1491
+ "verdict": {
1492
+ "enum": ["AGREE", "DISAGREE", "SUPPLEMENT", "verification-error"]
1493
+ },
1494
+ "breakageKind": { "type": "string" },
1495
+ "note": { "type": "string" }
1496
+ }
1497
+ }
1498
+ }
1487
1499
  }
1488
1500
  }
1489
1501
  },
@@ -156,7 +156,7 @@ Every container group in the project (no task-key needed):
156
156
  okstra container down --project-root <projectRoot> --all
157
157
  ```
158
158
 
159
- Parse the stdout JSON (`{downed, orphanPanesReaped}`). Report each torn-down `projectName` and the orphan panes reaped. `down` removes containers with `docker rm -f` — confirm with the user before running `--all`, since it takes down every okstra container group in the project at once. A single-task `down` is safe to run directly once the task-key is resolved.
159
+ Parse the stdout JSON (`{downed, orphanPanesReaped}`). Report each torn-down `projectName` and the orphan panes reaped. `down` runs `docker compose -p <name> down --remove-orphans` — a compose-native teardown that removes the project's containers and the compose network, but deliberately keeps named volumes (e.g. DB data) by NOT passing `-v`. Confirm with the user before running `--all`, since it takes down every okstra container group in the project at once. A single-task `down` is safe to run directly once the task-key is resolved.
160
160
 
161
161
  ---
162
162
 
@@ -332,14 +332,24 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
332
332
  - **Round count**: `{{ implementationPlanning.planBodyVerification.roundCount }}`
333
333
  - **Gate result**: `{{ implementationPlanning.planBodyVerification.gateResult }}`
334
334
 
335
+ > {{ t("implementationPlanning.planBodyGateLegend") }}
336
+
335
337
  #### Verdict details
336
338
 
337
- | Plan item | Worker | Verdict | Breakage kind | Note |
338
- |-----------|--------|---------|---------------|------|
339
- {% for row in implementationPlanning.planBodyVerification.verdictDetails -%}
340
- | {{ row.planItem | mdcell }} | {{ row.worker | mdcell }} | {{ row.verdict | mdcell }} | {{ (row.breakageKind or '--') | mdcell }} | {{ (row.note or '') | mdcell }} |
339
+ {{ t("implementationPlanning.planBodyVerdictLegend") }}
340
+
341
+ {{ t("implementationPlanning.planBodyBreakageLegend") }}
342
+
343
+ {% for item in implementationPlanning.planBodyVerification.planItems %}
344
+ **{{ item.id }} — {{ item.subject }}**{% if item.sourceSection %} ({{ t("implementationPlanning.planBodySourceLabel") }} {{ item.sourceSection }}){% endif %}
345
+
346
+
347
+ | Worker | Verdict | Breakage kind | Note |
348
+ |--------|---------|---------------|------|
349
+ {% for v in item.verdicts %}| {{ v.worker | mdcell }} | {{ v.verdict | mdcell }} | {{ (v.breakageKind or '--') | mdcell }} | {{ (v.note or '') | mdcell }} |
341
350
  {% endfor %}
342
351
 
352
+ {% endfor %}
343
353
  #### Dissent log
344
354
 
345
355
  {% if implementationPlanning.planBodyVerification.dissentLog | length == 0 -%}
@@ -48,7 +48,7 @@
48
48
  "ticketCoverage": "Summary of the core problems, requirements, and verification targets this run covered.",
49
49
  "executionStatus": "At-a-glance table of each worker's status, assigned model, and key findings.",
50
50
  "sourceItemsRule": "The `Source items` column shows which worker items each consensus row was synthesised from, as `<worker>:<item-id>` pairs.",
51
- "planBodyVerification": "Cross-verification results for each item in the plan body.",
51
+ "planBodyVerification": "Per-item cross-verification of each plan-body item (option, stage, step, dependency, validation, rollback, requirement coverage) for whether it is executable as written and internally consistent. Whether each option actually *solves* the problem (requirement satisfaction) is judged separately under Requirement Coverage, not here.",
52
52
  "clarificationItems": "Items that need your answer or supporting material before the next step."
53
53
  },
54
54
  "tokenSummary": {
@@ -91,6 +91,10 @@
91
91
  "noRoundsNote": "No reverify rounds executed (all findings reached consensus at grouping)."
92
92
  },
93
93
  "implementationPlanning": {
94
+ "planBodyGateLegend": "Gate values — `passed`: agreed, no dissent · `passed-with-dissent`: a minority dissent remains but the gate passes (a majority dissent would block approval) · `blocked-by-disagreement`: majority dissent blocks approval · `aborted-non-result`: verification itself produced no result.",
95
+ "planBodyVerdictLegend": "Verdict — **AGREE**: executable as written and internally consistent with other items · **SUPPLEMENT**: item is sound but a dependency / edge case / precondition is missing · **DISAGREE**: has a defect (see Breakage kind) · **verification-error**: the worker produced no result.",
96
+ "planBodyBreakageLegend": "Breakage kind — a: cited file path/symbol mismatches another step or option · b: command is not executable or is ambiguous · c: validation signal is not observable · d: rollback violates commit/dependency order · e: contradicts the trade-off matrix · f: requirement-coverage row does not map to an option/stage/step that actually satisfies the requirement. (`--` = not applicable)",
97
+ "planBodySourceLabel": "source §",
94
98
  "optionInterfacesLabel": "Affected interfaces / public contracts / downstream consumers",
95
99
  "optionBlastRadiusLabel": "Blast radius estimate",
96
100
  "recommendedTableHeaderLabel": "Item",
@@ -48,7 +48,7 @@
48
48
  "ticketCoverage": "이 run 이 다룬 핵심 문제·요구사항·검증 대상 요약입니다.",
49
49
  "executionStatus": "각 worker 의 상태·배정 모델·핵심 finding 을 한눈에 보는 표입니다.",
50
50
  "sourceItemsRule": "`Source items` 열은 이 합의 항목이 어느 워커의 어느 항목에서 합성됐는지를 `<worker>:<item-id>` 형식으로 표기합니다.",
51
- "planBodyVerification": "계획 본문의 각 항목을 plan-item 단위로 교차 검증한 결과입니다.",
51
+ "planBodyVerification": "계획 본문의 각 항목(옵션·스테이지·스텝·의존성·검증·롤백·요구사항 커버리지)이 적힌 대로 실행 가능하고 서로 모순이 없는지 워커들이 항목별로 교차 검증한 결과입니다. 각 옵션이 '문제를 실제로 푸는지'(요구사항 충족)는 여기가 아니라 Requirement Coverage 에서 별도로 판정합니다.",
52
52
  "clarificationItems": "다음 진행 전에 사용자의 답변이나 자료 첨부가 필요한 항목입니다."
53
53
  },
54
54
  "tokenSummary": {
@@ -91,6 +91,10 @@
91
91
  "noRoundsNote": "재검증 라운드 미실행 (그룹핑 단계에서 전부 합의)."
92
92
  },
93
93
  "implementationPlanning": {
94
+ "planBodyGateLegend": "게이트 결과 값 뜻 — `passed`: 이견 없이 통과 · `passed-with-dissent`: 소수 워커의 반대가 남았으나 통과(반대가 다수였다면 승인 차단) · `blocked-by-disagreement`: 다수 반대로 승인 차단 · `aborted-non-result`: 검증 자체가 결과를 내지 못함.",
95
+ "planBodyVerdictLegend": "판정(Verdict) — **AGREE**: 적힌 대로 실행 가능하고 다른 항목과 내부적으로 일관됨 · **SUPPLEMENT**: 항목 자체는 타당하나 의존성·엣지케이스·전제조건이 누락됨 · **DISAGREE**: 결함이 있음(결함 유형 참조) · **verification-error**: 워커 검증이 결과를 내지 못함.",
96
+ "planBodyBreakageLegend": "결함 유형(Breakage kind) — a: 인용한 파일 경로/심볼이 다른 스텝·옵션과 불일치 · b: 명령이 실행 불가하거나 모호함 · c: 검증 신호가 관측 불가 · d: 롤백이 커밋/의존성 순서를 위반 · e: 트레이드오프 매트릭스와 모순 · f: 요구사항 커버리지 행이 요구사항을 실제로 충족하는 옵션/스테이지/스텝에 매핑되지 않음. (`--` = 해당 없음)",
97
+ "planBodySourceLabel": "출처 §",
94
98
  "optionInterfacesLabel": "영향 인터페이스 / 공개 계약 / 다운스트림 소비자",
95
99
  "optionBlastRadiusLabel": "폭발 반경 추정",
96
100
  "recommendedTableHeaderLabel": "항목",