okstra 0.29.0 → 0.30.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/README.kr.md CHANGED
@@ -185,6 +185,9 @@ Claude Code 세션 밖에서 task 를 시작하려면:
185
185
  - **PR 본문 템플릿 설정** (release-handoff) — PR 본문은 마크다운 템플릿에서 채워집니다. 해석 우선순위: 1회성 override (`--pr-template-path` 또는 okstra-run Step 6 prompt) → `<project_root>/.project-docs/okstra/project.json` 의 `prTemplatePath` → `~/.okstra/config.json` 의 `prTemplatePath` → 스킬 디폴트 `~/.claude/skills/okstra-run/templates/pr-body.template.md`. 템플릿 등록 명령: `okstra config set pr-template-path <path> [--scope project|global]` (project 스코프는 project root 기준 상대경로 허용, global 스코프는 절대경로 또는 `~/` 시작 경로만 허용). 현재 설정 확인: `okstra config get pr-template-path --scope all` 은 각 스코프 값 + 실제로 우승하는 경로(effective) 까지 보여줍니다. 디폴트 템플릿은 `## Summary` / `## Changes` / `## Test plan` / `## Linked issues` 4 섹션 + HTML 주석으로 lead 작성 가이드를 포함하며, PR 생성 직전에 lead 가 주석을 제거합니다.
186
186
  - **프로파일 워커 로스터 검증** — `--workers <csv>` 와 okstra-run Step 6 의 워커 prompt 는 해당 프로파일의 `Required workers:` 블록에 선언된 워커 ID 만 허용합니다. 프로파일에 없는 워커 (예: `release-handoff` 에서 `codex` / `gemini`) 를 요청하면 명확한 에러로 거절되고, 인터랙티브 prompt 도 프로파일이 실제로 받는 워커만 보여줍니다.
187
187
  - **Phase 6 plan-body verification (implementation-planning 전용)** — Report writer worker 가 final-report draft 를 작성한 직후, User Approval gate 직전에 lead 가 1 라운드의 사후 검증을 추가로 돌립니다. 합성된 `## 4.5` plan 본문에서 `P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*` plan-item 을 추출해 모든 analyser 워커에게 `AGREE` / `DISAGREE(a-e)` / `SUPPLEMENT` 평결을 요청합니다. 집계된 gate 결과는 `passed` / `passed-with-dissent` / `blocked-by-disagreement` / `aborted-non-result` 중 하나. 앞 둘은 final-report 상단의 `- [ ] Approved` 마커를 렌더하고, 뒤 둘은 `majority-disagree` 항목을 `## 5. Clarification Items` 의 `Blocks=approval` row 로 변환합니다. 빠른 반복용 opt-out: `--no-plan-verification` (기본값: 활성). 자세한 라운드 프로토콜은 [`skills/okstra-convergence/SKILL.md`](skills/okstra-convergence/SKILL.md) 의 "Plan-body verification mode" 섹션과 [`docs/kr/cli.md#--no-plan-verification`](docs/kr/cli.md#--no-plan-verification).
188
+ - **Brief = translation layer + Step 6.5 reporter batch confirmation** — `okstra-brief` 가 외부 입력 (이슈 ticket, 요구사항 문서, 사용자 메시지) 을 verbatim 으로 옮기되 okstra 가 추가한 부분은 labelled augmentation 으로 구분하는 translation layer 가 됐습니다. Step 6.5 가 brief 가 옮기는 과정에서 의미 변화가 발생했는지 사용자에게 일괄 확인받아 `Reporter Confirmations` 섹션에 기록하고, 모든 분석 profile 은 이 섹션의 존재를 phase 분석 진입 precondition 으로 강제합니다 (validator: `validators/validate-brief.py`).
189
+ - **Artifact-home rule (`.project-docs/okstra/`)** — okstra 가 사용자 프로젝트에 쓰는 모든 파일은 `<project>/.project-docs/okstra/` subtree 안에만 위치합니다. 외부 경로 (`CONTEXT.md`, `docs/adr/`, `.scratch/`, 소스 코드) 는 read-only reference 이며 부재해도 정상 상태입니다. okstra-internal 등가물: 용어집 `glossary.md`, 결정 기록 `decisions/<NNNN>-<slug>.md` (`implementation-planning` phase 에서 평가).
190
+ - **Dual-format final-report views** — Phase 7 가 `final-report-<task-type>-<seq>.md` 를 쓰면 `okstra render-views` 가 같은 `reports/` 폴더에 두 view 를 자동 생성합니다: AI 다음-phase 입력용 슬림 markdown, 사람 reviewer 용 self-contained HTML (CSS/JS 인라인, 외부 URL 0). HTML 의 `Export user response` 버튼은 `## 5. Clarification Items` 입력을 `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md` 사이드카로 직렬화해 다음 phase 가 소비합니다. 원본 MD 는 어떤 경우에도 view 생성으로 인해 수정되지 않습니다.
188
191
 
189
192
  ### 3.5 운영 명령
190
193
 
@@ -196,6 +199,7 @@ Claude Code 세션 밖에서 task 를 시작하려면:
196
199
  | `npx -y okstra@latest setup --project-id <id>` | 현재 프로젝트를 등록 (`.project-docs/okstra/project.json`) |
197
200
  | `npx -y okstra@latest check-project` | 현재 프로젝트가 `setup` 으로 등록됐는지 검증 |
198
201
  | `npx -y okstra@latest config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | okstra 설정 읽기/쓰기. 현재 지원 키: `pr-template-path` (project.json 또는 `~/.okstra/config.json` 의 `prTemplatePath` 갱신) |
202
+ | `npx -y okstra@latest render-views <final-report.md>` | final-report MD 한 본을 입력으로 슬림 MD + HTML 두 view 를 (재)생성 (Phase 7 step 1.5; 멱등) |
199
203
  | `npx -y okstra@latest uninstall` | 런타임 + 스킬 제거; 사용자 데이터(`recent.jsonl`, `projects/`, …)는 보존 |
200
204
  | `npx -y okstra@latest uninstall --purge -y` | 사용자 데이터까지 모두 제거 |
201
205
 
package/README.md CHANGED
@@ -184,6 +184,9 @@ Recent workflow additions (post-0.8.0, on `main`):
184
184
  - **Configurable PR body template** (release-handoff) — the PR body is filled from a markdown template chosen in priority order: per-run override (`--pr-template-path` or the okstra-run Step 6 prompt) → `<project_root>/.project-docs/okstra/project.json` `prTemplatePath` → `~/.okstra/config.json` `prTemplatePath` → bundled skill default at `~/.claude/skills/okstra-run/templates/pr-body.template.md`. Register a template with `okstra config set pr-template-path <path> [--scope project|global]` (project scope accepts paths relative to the project root; global scope requires absolute or `~/`-prefixed). `okstra config get pr-template-path --scope all` reports every scope plus the effective winner. The bundled default ships `## Summary` / `## Changes` / `## Test plan` / `## Linked issues` with HTML comment guidance that the lead strips before opening the PR.
185
185
  - **Profile-roster worker validation** — `--workers <csv>` (and the okstra-run Step 6 worker prompt) are now restricted to the worker IDs declared by the chosen profile's `Required workers:` block. Asking for `codex` / `gemini` on a profile that does not list them (e.g. `release-handoff`) is rejected with a clear error, and the interactive prompt only offers workers the profile actually accepts.
186
186
  - **Phase 6 plan-body verification (implementation-planning only)** — after the Report writer worker authors the final-report draft and before the User Approval gate, the lead now runs one additional verification round: it extracts `P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*` items from the consolidated `## 4.5` plan body and dispatches them to every analyser worker as `AGREE` / `DISAGREE(a-e)` / `SUPPLEMENT`. The aggregated gate result is one of `passed` / `passed-with-dissent` / `blocked-by-disagreement` / `aborted-non-result`; only the first two render the top-of-report `- [ ] Approved` marker, the other two convert `majority-disagree` items into `## 5. Clarification Items` rows with `Blocks=approval`. Disable for fast iteration with `--no-plan-verification` (default: enabled). Details: [`skills/okstra-convergence/SKILL.md`](skills/okstra-convergence/SKILL.md) "Plan-body verification mode" and [`docs/kr/cli.md#--no-plan-verification`](docs/kr/cli.md#--no-plan-verification).
187
+ - **Brief as translation layer + reporter batch confirmation (Step 6.5)** — `okstra-brief` now produces the brief as a translation layer that preserves external inputs (issue ticket, requirements doc, user message) verbatim while labelling okstra augmentations. A new Step 6.5 walks the user through a single batch confirmation of any rewordings, recorded in a `Reporter Confirmations` section. Every analysis profile blocks phase entry until this section exists (validator: `validators/validate-brief.py`).
188
+ - **Artifact-home rule (`.project-docs/okstra/`)** — okstra writes only inside `<project>/.project-docs/okstra/`. External paths (`CONTEXT.md`, `docs/adr/`, `.scratch/`, source code) are read-only references; their absence is normal. okstra-internal equivalents: `glossary.md` for terminology and `decisions/<NNNN>-<slug>.md` for decision records (evaluated in `implementation-planning`).
189
+ - **Dual-format final-report views** — after Phase 7 writes `final-report-<task-type>-<seq>.md`, `okstra render-views` automatically emits two sibling views in the same `reports/` directory: a slim Markdown for downstream AI input and a self-contained HTML (inline CSS/JS, no external URLs) for human review. The HTML lets a reviewer fill in `## 5. Clarification Items` decisions and export them to `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md`, which the next phase consumes as input. The original MD is never modified by view generation.
187
190
 
188
191
  ### 3.5 Ops commands
189
192
 
@@ -195,6 +198,7 @@ Recent workflow additions (post-0.8.0, on `main`):
195
198
  | `npx -y okstra@latest setup --project-id <id>` | Register the current project (`.project-docs/okstra/project.json`) |
196
199
  | `npx -y okstra@latest check-project` | Verify the current project has been registered with `setup` |
197
200
  | `npx -y okstra@latest config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | Read / write okstra settings; initial key `pr-template-path` (writes `prTemplatePath` to project.json or `~/.okstra/config.json`) |
201
+ | `npx -y okstra@latest render-views <final-report.md>` | Regenerate the slim-MD + HTML sibling views from a final-report MD (Phase 7 step 1.5; idempotent) |
198
202
  | `npx -y okstra@latest uninstall` | Remove runtime + skills; preserves user data (`recent.jsonl`, `projects/`, …) |
199
203
  | `npx -y okstra@latest uninstall --purge -y` | Remove everything including user data |
200
204
 
@@ -35,6 +35,7 @@
35
35
  - [Required team contract](#required-team-contract)
36
36
  - [Stable task identity](#stable-task-identity)
37
37
  - [Project self-registration](#project-self-registration)
38
+ - [Artifact-home rule](#artifact-home-rule)
38
39
  - [Task type](#task-type)
39
40
  - [표준 task type](#표준-task-type)
40
41
  - [Phase 간 정보 전달](#phase-간-정보-전달)
@@ -62,9 +63,11 @@
62
63
  - [8. 구현 후 최종 검토](#8-구현-후-최종-검토)
63
64
  - [9. 같은 task 재개](#9-같은-task-재개)
64
65
  - [Lifecycle status and resume](#lifecycle-status-and-resume)
65
- - [Preferred final report structure](#preferred-final-report-structure)
66
+ - [Final report structure](#final-report-structure)
67
+ - [Final report views (slim MD + HTML)](#final-report-views-slim-md--html)
66
68
  - [Worker error collection (optional sidecar)](#worker-error-collection-optional-sidecar)
67
69
  - [Token usage and cost accounting](#token-usage-and-cost-accounting)
70
+ - [Validators](#validators)
68
71
  - [Practical notes](#practical-notes)
69
72
  - [Related documents](#related-documents)
70
73
 
@@ -307,6 +310,19 @@ Claude launch prompt 본문은 항상 `prompts/launch.template.md` 템플릿에
307
310
 
308
311
  `okstra-ctl` 의 reindex/backfill 도 신규 모델에서 권위 소스를 변경했습니다. 과거에는 `examples/projects/*.conf.sh` 를 source 했지만, 지금은 `~/.okstra/projects/<projectId>/meta.json` (record_start 가 위 project.json 정보를 mirror 한 결과) 을 스캔하여 (projectId, projectRoot) 매핑을 복원합니다. `OKSTRA_PROJECT_DEFINITION_DIR_OVERRIDE` 환경변수도 함께 폐기되었습니다.
309
312
 
313
+ ## Artifact-home rule
314
+
315
+ okstra 가 사용자 프로젝트에 생성·수정·삭제하는 모든 파일은 `<PROJECT_ROOT>/.project-docs/okstra/` subtree 안에만 위치합니다. 외부 경로 (예: `<PROJECT_ROOT>/CONTEXT.md`, `<PROJECT_ROOT>/docs/adr/`, `<PROJECT_ROOT>/.scratch/`, 소스 코드) 는 **read-only reference** 입니다. okstra phase 는 그 파일들이 존재하면 읽을 수 있지만 부재해도 정상 상태로 간주하며, 자체 판단으로 외부 경로에 쓰지 않습니다.
316
+
317
+ 유일한 예외: brief 의 `Source Material` 또는 `Reporter Confirmations` 섹션에서 사용자가 **verbatim** 으로 특정 외부 파일 편집을 요청한 경우. 해당 편집을 수행하는 phase 는 자신의 final-report 에 사용자 원문 인용을 함께 남겨야 합니다.
318
+
319
+ okstra 는 자기 subtree 안에 자체 institutional memory 를 유지합니다.
320
+
321
+ - `<PROJECT_ROOT>/.project-docs/okstra/glossary.md` — run 을 가로지르며 누적되는 okstra 용어집. 외부 `CONTEXT.md` 류 skill 의 기능을 흡수합니다.
322
+ - `<PROJECT_ROOT>/.project-docs/okstra/decisions/<NNNN>-<slug>.md` — okstra 의 결정 기록. 외부 ADR 시스템의 기능을 흡수합니다. 평가 시점은 `implementation-planning` phase 이며 `okstra-brief` 단계에서는 후보만 표시합니다.
323
+
324
+ okstra phase 는 PRD / issue file 을 직접 쓰지 않습니다. 동등한 결정 산출물은 `requirements-discovery` 와 `implementation-planning` 이 `.project-docs/okstra/` 내부에 만듭니다.
325
+
310
326
  ## Task type
311
327
 
312
328
  `task-type`은 이번 run의 목적과 profile 선택, 그리고 lifecycle phase 라우팅을 동시에 결정합니다.
@@ -417,9 +433,14 @@ task manifest, task index, instruction-set, runs, history가 이 루트 아래
417
433
  그리고 `--render-only`가 아니면 handoff된 Claude session이 보통 아래 결과 파일을 현재 run에 추가합니다.
418
434
  - `sessions/claude-resume-<task-type>-<seq>.sh`
419
435
  - `reports/final-report-<task-type>-<seq>.md`
436
+ - `reports/final-report-<task-type>-<seq>.slim.md` *(Phase 7 결정론적 후처리: AI 다음-phase 입력용 슬림 markdown)*
437
+ - `reports/final-report-<task-type>-<seq>.html` *(Phase 7 결정론적 후처리: 사람 reviewer 용 self-contained HTML, CSS/JS 인라인)*
438
+ - `user-responses/user-response-<task-type>-<seq>.md` *(HTML 의 `Export user response` 버튼이 생성하는 사이드카; 사용자가 채워 저장하면 다음 phase 가 입력으로 소비)*
439
+ - `worker-results/<worker>-audit-<task-type>-<seq>.md` *(워커별 Reading Confirmation 사이드카; 본문이 아니라 audit 용)*
420
440
  - `status/final-<task-type>-<seq>.status`
421
- 최종 결과 파일 둘은 `okstra`가 stdout을 저장해서 만드는 파일이 아닙니다.
441
+ 최종 결과 파일 (`final-report` MD / status) 은 `okstra`가 stdout을 저장해서 만드는 파일이 아닙니다.
422
442
  `okstra`가 준비한 task bundle을 바탕으로 Claude가 현재 run 안에 직접 작성하는 결과물입니다.
443
+ slim MD / HTML 두 view 는 `okstra render-views <final-report.md>` (Phase 7 step 1.5) 가 final-report MD 한 본을 입력으로 결정론적으로 생성합니다. 원본 MD 는 view 생성으로 인해 수정되지 않습니다.
423
444
  반면 `sessions/claude-resume-<task-type>-<seq>.sh`는 `okstra`가 Claude launch 전에 미리 생성하는 interruption recovery helper입니다.
424
445
 
425
446
  run directory는 task-type 단위로 task 실행 이력을 모으고, 내부를 `manifests/`, `state/`, `prompts/`, `reports/`, `status/`, `sessions/`, `worker-results/`처럼 유형별 하위 폴더로 나눈 뒤 각 run-level artifact와 result 파일을 `-<task-type>-<seq>` suffix(per-category 3-digit zero-padded counter, 예: `001`, `002`)로 구분합니다.
@@ -611,6 +632,8 @@ canonical metadata는 항상 `task-manifest.json`을 기준으로 확인합니
611
632
 
612
633
  `okstra`는 brief-first 구조입니다. brief 가 분석의 정본 입력이며, 워커가 필요로 할 추가 자료(보고서, 코드 스니펫, 로그 등)는 brief 내부의 `Evidence and Source Materials` 섹션에 inline 또는 path 로 모두 포함시킵니다.
613
634
 
635
+ brief 는 **translation layer** 입니다 — 외부 입력 (이슈 트래커 ticket, 요구사항 문서, 사용자 메시지) 을 okstra-readable 형식으로 옮기되, 원문은 verbatim 으로 보존하고 okstra 가 추가한 부분은 labelled augmentation 으로 명확히 구분합니다. okstra-brief skill 의 산출이 정식 SSOT 이며, 분석 워커들이 phase 시작 전에 일률적으로 읽어들이는 단일 입력입니다.
636
+
614
637
  brief에는 보통 아래를 포함합니다.
615
638
 
616
639
  - 문제 설명
@@ -622,6 +645,10 @@ brief에는 보통 아래를 포함합니다.
622
645
  - worker에게 줄 질문
623
646
  - 기대 출력
624
647
  - 이전 run 또는 연관 task 정보
648
+ - (선택) glossary 추가 후보 — okstra-brief Step 4.5 가 `<PROJECT_ROOT>/.project-docs/okstra/glossary.md` 에 직접 기록
649
+ - (선택) decisions 후보 — `implementation-planning` phase 에서 평가 후 `.project-docs/okstra/decisions/<NNNN>-<slug>.md` 로 승격
650
+
651
+ okstra-brief 의 Step 6.5 는 **reporter batch confirmation** 입니다. brief 가 외부 입력을 옮기는 도중 의미 변화가 발생했는지를 사용자에게 일괄 확인받고 그 결과를 `Reporter Confirmations` 섹션에 기록합니다. 모든 분석 profile 은 이 섹션의 존재를 phase 분석 진입의 precondition 으로 강제합니다 (validator: `validators/validate-brief.py`).
625
652
 
626
653
  기본 템플릿:
627
654
 
@@ -796,22 +823,42 @@ resume 판단 기준:
796
823
  - 다음 phase 시작: `workflow.nextRecommendedPhase`가 구체적인 phase면 그 값으로 다음 `okstra.sh` 실행을 준비합니다.
797
824
  - 추가 자료 필요: `routingStatus=pending` 또는 `nextRecommendedPhase=pending-routing-decision`이면 brief 보강이 먼저입니다.
798
825
 
799
- ## Preferred final report structure
826
+ ## Final report structure
800
827
 
801
828
  기본 최종 보고서 템플릿은 `templates/reports/final-report.template.md`입니다.
802
- Claude가 작성하는 최종 보고서는 brief 더 구체적인 형식이 없다면 아래 구조를 우선 사용합니다.
829
+ Claude가 작성하는 최종 보고서는 아래 구조를 우선 사용합니다 (brief 의 augmentation 이 더 구체적인 형식을 요구할 때만 그것을 따릅니다).
803
830
 
804
- 1. 문제 또는 검증 대상 요약
805
- 2. 에이전트별 실행 현황
806
- 3. Cross Verification 결과
807
- 4. 최종 판단
808
- 5. 근거 세부 분석
809
- 6. 누락 정보 리스크
810
- 7. 권장 다음 단계
831
+ - `## Verdict Card` **최상단 의무 섹션**. Final Conclusion / Verdict Token / Direction / Approval Required? / Next Step 5 행. Verdict Token / Direction / Next Step 셀은 본문 §2 (실행 현황) 와 §6 (다음 단계) 의 권위 셀과 byte-match 해야 합니다.
832
+ - (선택) `## 0. Clarification Response Carried In From Previous Run` — 직전 run 에서 응답이 carry-in 된 경우에만 렌더링. 빈 carry-in 일 때는 헤딩 자체를 출력하지 않습니다.
833
+ - `## 1. 문제 또는 검증 대상 요약` — §1.1 Consensus / §1.2 Differences 표 각각 `Source items (worker:item)` 컬럼 보존 (cross-worker traceability).
834
+ - `## 2. 에이전트별 실행 현황`
835
+ - `## 3. Cross Verification 결과` — §3.1 Primary Evidence 에 `Source items (worker:item)` + `Source (path:line / log)` 컬럼.
836
+ - `## 4. 최종 판단` `implementation-planning` 의 §4.5.9 Plan Body Verification 은 두 표 (`Verdict summary` 4-열, `Verdict details` 5-열, plan item × worker) 로 분할 emit.
837
+ - `## 5. Clarification Items` — 통합 8-열 표 한 곳. 기존 §5.1 / §5.2 / §4.5.8 / §4.5.9 Open Questions 는 deprecated 되어 validator 가 등장 시 fail.
838
+ - `## 6. 권장 다음 단계`
839
+ - `## Token Usage Summary` — sentinel (`pending` / `N/A` / `--` / `?` / 빈 셀) 또는 zero (`0` / `$0.00`) 박제 시 validator 가 출고를 차단합니다. `Codex/Gemini CLI 추가 비용` 행만 "CLI 미사용" 의미로 `$0.00` 허용.
840
+
841
+ 워커 출력의 `## 0. Reading Confirmation` 블록은 본문에 두지 않고 `runs/<task-type>/worker-results/<worker>-audit-<task-type>-<seq>.md` 사이드카에 작성합니다 (validator 강제).
811
842
 
812
843
  차이점이 실질적으로 없으면 억지로 대비를 만들지 말고, 차이가 없음을 명시합니다.
813
844
  저장 실패나 세션 제한에 대한 메타 설명 대신 실제 Markdown 보고서 본문을 파일에 작성해야 합니다.
814
845
 
846
+ ## Final report views (slim MD + HTML)
847
+
848
+ Phase 7 step 1.5 가 final-report MD 한 본을 입력으로 두 view 를 결정론적으로 자동 생성합니다.
849
+
850
+ - `reports/final-report-<task-type>-<seq>.slim.md` — AI 다음-phase 입력용. 장식·캡션·sentinel 셀을 제거한 토큰-경제 버전. `validate-run.py` 의 phase substring 검사를 byte-identical 로 통과합니다.
851
+ - `reports/final-report-<task-type>-<seq>.html` — 사람 reviewer 용 self-contained HTML. CSS / JS 인라인 임베드 (외부 URL 0), system color 다크모드, sticky header, 인쇄 대응. §5 `C-*` 행의 의사결정 입력 (체크박스 / 셀렉트 / textarea) 을 화면에서 채우고 `Export user response` 버튼으로 사이드카 markdown 을 생성합니다.
852
+
853
+ 진입점:
854
+
855
+ - Python 단일 reference: `scripts/okstra_ctl/report_views.py` (`slim_markdown(...)`, `render_html(..., css, js)`, `serialize_user_response(...)`). HTML 내 JS `buildUserResponseMarkdown` 은 Python `serialize_user_response` 와 **byte-identical** (Node `vm.runInThisContext` 단위 테스트로 자동 검증).
856
+ - CLI: `scripts/okstra-render-report-views.py <final-report.md>` 또는 Node 위임 wrapper `bin/okstra render-views <md>`.
857
+ - 검증: `validators/validate-report-views.py` — slim 의 phase substring 보존, HTML 내 form control 위치, 외부 URL 부재, Response ID parity (`C-*` ↔ HTML), 두 모듈 substring 상수 drift 모두 검사.
858
+ - 사용자 응답 사이드카 스키마 SSOT: `templates/reports/user-response.template.md`.
859
+
860
+ 원본 final-report MD 는 어떤 경우에도 view 생성으로 인해 수정되지 않습니다.
861
+
815
862
  ## Worker error collection (optional sidecar)
816
863
 
817
864
  워커(Claude/Codex/Gemini worker, Report writer, Claude lead) 실행 중 발생한 에러를 단일 시계열 로그로 수집해 사후 회고에 사용할 수 있습니다.
@@ -854,8 +901,19 @@ Claude가 작성하는 최종 보고서는 brief에 더 구체적인 형식이
854
901
  - Claude lead/workers: `~/.claude/projects/<cwd-as-dashes>/<sessionId>.jsonl`의 per-message `message.usage`
855
902
  - Codex CLI: `~/.codex/sessions/Y/M/D/rollout-*.jsonl`의 마지막 `total_token_usage.total_tokens`
856
903
  - Gemini CLI: `~/.gemini/tmp/*/chats/session-*.json`의 per-message `tokens.total`
857
- - billable-equivalent token math와 USD cost estimation을 함께 기록합니다. Anthropic billing ratio(`cache_creation=1.25x`, `cache_read=0.1x`, `output=5x`)를 반영합니다.
858
- - 가격표는 helper 상단에서 중앙 관리합니다. 모델 가격이 바뀌면 helper의 `*_PRICING` 상수를 갱신해야 합니다.
904
+ - billable-equivalent token math와 USD cost estimation을 함께 기록합니다. Anthropic billing ratio(`cache_creation_5m=1.25x`, `cache_creation_1h=2.0x`, `cache_read=0.1x`, `output=5x`)를 반영합니다. transcript 의 `usage.cache_creation.ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens` 분해가 있으면 분리 집계합니다.
905
+ - 가격표는 `scripts/okstra_token_usage/pricing.py` 에서 중앙 관리합니다. 모델 가격이 바뀌면 거기서 갱신합니다. 가격 매칭에 실패한 모델 id 는 `usageSummary.unmatchedModels` 필드로 사용자에게 노출됩니다 (silent zero 사고 방지).
906
+
907
+ ## Validators
908
+
909
+ phase 산출물의 출고 가능 여부를 강제하는 진입점:
910
+
911
+ - `validators/validate-workflow.sh` — phase contract 통합 검증.
912
+ - `validators/validate-run.py` — run-level final-report 본문 contract (Verdict Card 존재, deprecated §5.1/§5.2/§4.5.8/§4.5.9 Open Questions 부재, Plan Body Verification gate × Approval 마커 cross-check, Token Usage sentinel/zero 차단, 워커-결과 audit 사이드카 존재).
913
+ - `validators/validate-report-views.py` — slim MD / HTML view 의 phase substring 보존 및 form-control 영역 검사.
914
+ - `validators/validate-brief.py` — brief schema (front-matter, `Reporter Confirmations` 섹션 존재, root parent-id self 규칙, slug 컨벤션 등) 강제. `bash validators/validate-brief.sh <brief.md>` 가 thin wrapper.
915
+
916
+ 각 validator 는 contract 위반 시 `contract-violated` exit code 로 phase 를 차단합니다. 위반은 다음 phase 실행 시점에 적용되므로 이전 산출물은 그대로 둡니다.
859
917
 
860
918
  ## Practical notes
861
919
 
@@ -883,6 +941,9 @@ Claude가 작성하는 최종 보고서는 brief에 더 구체적인 형식이
883
941
  - 워커 에러는 옵션 sidecar `runs/<task-type>/logs/errors-<task-type>-<seq>.jsonl`로 수집되며 lead가 단독 writer입니다. 진입점 helper는 `scripts/okstra-error-log.py`입니다.
884
942
  - 토큰 사용 및 비용 집계는 `scripts/okstra-token-usage.py`가 담당하며.
885
943
  - `okstra.sh`는 worker CLI 호출 anchoring을 위해 절대 projectRoot를 강제합니다.
944
+ - `okstra wizard step` 은 `--answer <val>` 을 **필수** 로 받습니다. 응답을 줄 차례가 아니라 다음 prompt 만 미리 보고 싶다면 `--no-submit` 으로 peek 합니다.
945
+ - `okstra history` 는 manifest fallback / 페이지네이션 / 필터를 지원하며, `--base-ref` 는 워크트리 registry 에서 해석합니다.
946
+ - 사용자 프로젝트에 대한 모든 쓰기는 `<PROJECT_ROOT>/.project-docs/okstra/` 안에만 발생합니다 (Artifact-home rule 참조).
886
947
 
887
948
  ## Related documents
888
949
 
package/docs/kr/cli.md CHANGED
@@ -32,6 +32,7 @@
32
32
  - [`--work-category`](#--work-category)
33
33
  - [`--related-tasks`](#--related-tasks)
34
34
  - [`--render-only`](#--render-only)
35
+ - [`--no-plan-verification`](#--no-plan-verification)
35
36
  - [Interactive input flow](#interactive-input-flow)
36
37
  - [Confirmation flow](#confirmation-flow)
37
38
  - [okstra Control Center — 설치 / 자주 쓰는 명령](#okstra-control-center--설치--자주-쓰는-명령)
@@ -517,6 +518,7 @@ chmod +x ~/.local/bin/okstra-ctl
517
518
  | 진행 중 run 보기 | `okstra-ctl tail active` |
518
519
  | 단일 run 결과 메타 | `okstra-ctl show <runId-or-prefix>` |
519
520
  | 결과 보고서 경로 | `okstra-ctl open <runId-or-prefix>` |
521
+ | final-report 두 view 재생성 | `okstra render-views <final-report.md>` |
520
522
  | 단일 재실행 | `okstra-ctl rerun <runId-or-prefix> --yes` |
521
523
  | 다중 재실행 | `okstra-ctl rerun --filter --project X --status failed --yes` |
522
524
  | 가장 최근 재실행 | `okstra-ctl rerun last --project X --task-group Y --yes` |
@@ -535,7 +537,10 @@ chmod +x ~/.local/bin/okstra-ctl
535
537
  | `okstra worktree-lookup <task-key>` | `worktree_registry.lookup` 결과 (예약된 path / branch / base ref / 현재 상태) |
536
538
  | `okstra plan-validate <plan-path>` | `_validate_approved_plan` — approval marker 인식 결과와 sanitization 후 diff |
537
539
  | `okstra render-bundle <args…>` | `prepare_task_bundle(render_only=True)` 의 thin shim — `python3 -m okstra_ctl.run --render-only` 와 동일 시그니처 |
538
- | `okstra wizard <init\|step\|render-args\|confirmation> --state-file <path>` | okstra-run 인터랙티브 입력 상태머신 (`okstra_ctl.wizard`). `init` 으로 state file 시드한 skill `step --answer <val>` 반복 호출하면 다음 `Prompt` JSON 받음. `render-args` 최종 `render-bundle` 인자 맵, `confirmation` 사용자 echo 블록을 반환 |
540
+ | `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 검사 |
541
+ | `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 블록을 반환 |
542
+ | `okstra history [--limit N] [--offset M] [--project <id>] [--status <enum>]` | run history 페이지네이션 / 필터 조회. 중앙 인덱스가 비어 있으면 프로젝트별 task-manifest 들을 스캔해 fallback 으로 채움. `--base-ref` 는 워크트리 registry 에서 해석 |
543
+ | `okstra config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | 영구 설정 관리 (예: `pr-template-path`). 원자적 JSON 쓰기. global scope 의 상대경로는 거절 |
539
544
 
540
545
  > 모든 subcommand 는 `bin/okstra` 가 spawn 하는 python 헬퍼 (`src/_python-helper.mjs`) 가 `PYTHONPATH` 와 `~/.okstra/lib/python` 을 wire 합니다. 직접 `python3 -m okstra_ctl.*` 으로 호출하면 `PYTHONPATH` 를 사용자가 직접 셋업해야 합니다.
541
546
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.29.0",
3
+ "version": "0.30.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.29.0",
3
- "builtAt": "2026-05-17T06:30:39.418Z",
2
+ "package": "0.30.0",
3
+ "builtAt": "2026-05-17T08:21:41.472Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -73,12 +73,93 @@ GEMINI_MODEL_OPTIONS = ["default", "gemini-3-pro-preview", "gemini-3-flash-previ
73
73
  # special pick value: start a brand-new task
74
74
  TASK_PICK_NEW_TOKEN = "__new__"
75
75
 
76
+ # Pick-vs-free-text tokens shared by suggestion-aware prompts.
77
+ PICK_USE_SUGGESTED = "__use_suggested__"
78
+ PICK_TYPE_CUSTOM = "__free_input__"
79
+
80
+ # Lines of `key: value` we pull from a brief markdown frontmatter. The
81
+ # parser is intentionally lightweight (no yaml dep) and tolerant — a
82
+ # malformed brief returns an empty dict.
83
+ _BRIEF_FRONTMATTER_LINE_RE = re.compile(r"^([a-zA-Z0-9_\-]+)\s*:\s*(.*)$")
84
+
85
+
86
+ def _parse_brief_frontmatter(path: Path) -> dict[str, str]:
87
+ """Read the YAML-style frontmatter at the top of a brief markdown file
88
+ and return a flat ``{key: value}`` map.
89
+
90
+ Returns ``{}`` if the file is unreadable, has no frontmatter, or the
91
+ frontmatter is malformed. Comments (``# ...``) and quoted values are
92
+ stripped. Placeholder values like ``<task-group>`` are kept verbatim;
93
+ callers decide whether to treat them as a real suggestion.
94
+ """
95
+ try:
96
+ text = path.read_text(encoding="utf-8")
97
+ except OSError:
98
+ return {}
99
+ if not text.startswith("---"):
100
+ return {}
101
+ lines = text.splitlines()
102
+ if not lines or lines[0].strip() != "---":
103
+ return {}
104
+ out: dict[str, str] = {}
105
+ for line in lines[1:]:
106
+ if line.strip() == "---":
107
+ break
108
+ # strip trailing inline comment
109
+ comment_idx = line.find("#")
110
+ if comment_idx >= 0:
111
+ line = line[:comment_idx]
112
+ m = _BRIEF_FRONTMATTER_LINE_RE.match(line.strip())
113
+ if not m:
114
+ continue
115
+ key, val = m.group(1), m.group(2).strip()
116
+ # strip matching quotes
117
+ if (len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"')):
118
+ val = val[1:-1]
119
+ out[key] = val
120
+ return out
121
+
122
+
123
+ def _looks_like_template_placeholder(value: str) -> bool:
124
+ """Treat ``<task-group>``, ``<...>``, empty strings, and ``self`` as
125
+ non-suggestions. Anything else (a real slug-like value) is honored."""
126
+ v = (value or "").strip()
127
+ if not v:
128
+ return True
129
+ if v.startswith("<") and v.endswith(">"):
130
+ return True
131
+ if v.lower() in ("self", "tbd", "n/a", "na", "none"):
132
+ return True
133
+ return False
134
+
135
+
136
+ def _brief_suggestions(path: Path) -> tuple[str, str]:
137
+ """Return ``(task_group_suggestion, task_id_suggestion)`` extracted from
138
+ the brief's frontmatter, or empty strings when no usable value exists.
139
+
140
+ - ``task_group`` ← frontmatter ``task-group``.
141
+ - ``task_id`` ← frontmatter ``brief-id`` (which matches the
142
+ filename stem in okstra-brief output and is the
143
+ strongest single identifier of the task).
144
+
145
+ A brief without frontmatter, or with placeholder values, yields two
146
+ empty strings — callers fall back to plain-text input.
147
+ """
148
+ fm = _parse_brief_frontmatter(path)
149
+ tg_raw = fm.get("task-group", "")
150
+ bid_raw = fm.get("brief-id", "")
151
+ tg = "" if _looks_like_template_placeholder(tg_raw) else tg_raw
152
+ tid = "" if _looks_like_template_placeholder(bid_raw) else bid_raw
153
+ return tg, tid
154
+
76
155
 
77
156
  # ---- Step IDs ------------------------------------------------------------
78
157
 
79
158
  S_TASK_PICK = "task_pick"
80
159
  S_TASK_GROUP = "task_group"
160
+ S_TASK_GROUP_TEXT = "task_group_text"
81
161
  S_TASK_ID = "task_id"
162
+ S_TASK_ID_TEXT = "task_id_text"
82
163
  S_TASK_TYPE = "task_type"
83
164
  S_BRIEF_KEEP = "brief_keep"
84
165
  S_BRIEF_PATH = "brief_path"
@@ -123,6 +204,13 @@ class WizardState:
123
204
  task_group: str = ""
124
205
  task_id: str = ""
125
206
  existing_brief_path: str = ""
207
+ # brief-derived suggestions (new-task flow only; set when brief is
208
+ # accepted, cleared if the user picks "type custom" so the next
209
+ # `_build_*` falls back to plain text input)
210
+ task_group_suggestion: str = ""
211
+ task_id_suggestion: str = ""
212
+ task_group_pending_text: bool = False
213
+ task_id_pending_text: bool = False
126
214
 
127
215
  # task-type + dependents
128
216
  task_type: str = ""
@@ -421,24 +509,98 @@ def _submit_task_pick(state: WizardState, value: str) -> Optional[str]:
421
509
 
422
510
 
423
511
  def _build_task_group(state: WizardState) -> Prompt:
512
+ sugg = state.task_group_suggestion
513
+ if sugg:
514
+ return Prompt(
515
+ step=S_TASK_GROUP, kind="pick",
516
+ label=f"Task group? (brief 추천: {sugg})",
517
+ options=[
518
+ _opt(PICK_USE_SUGGESTED, f"brief 값 사용: {sugg}"),
519
+ _opt(PICK_TYPE_CUSTOM, "다른 값 입력"),
520
+ ],
521
+ echo_template="task-group: {value}",
522
+ )
424
523
  return Prompt(step=S_TASK_GROUP, kind="text",
425
524
  label="Task group 을 알려주세요 (예: backend-api, INV-1234, refactor)",
426
525
  echo_template="task-group: {value}")
427
526
 
428
527
 
429
528
  def _submit_task_group(state: WizardState, value: str) -> Optional[str]:
529
+ if state.task_group_suggestion:
530
+ if value == PICK_USE_SUGGESTED:
531
+ state.task_group = _slug_or_die(
532
+ state.task_group_suggestion, "task_group"
533
+ )
534
+ state.task_group_pending_text = False
535
+ return f"task-group: {state.task_group} (brief)"
536
+ if value == PICK_TYPE_CUSTOM:
537
+ state.task_group_pending_text = True
538
+ return f"task-group: (직접 입력)"
539
+ raise WizardError(
540
+ f"expected {PICK_USE_SUGGESTED!r} or {PICK_TYPE_CUSTOM!r}, "
541
+ f"got: {value!r}"
542
+ )
543
+ state.task_group = _slug_or_die(value, "task_group")
544
+ state.task_group_pending_text = False
545
+ return f"task-group: {state.task_group}"
546
+
547
+
548
+ def _build_task_group_text(state: WizardState) -> Prompt:
549
+ return Prompt(step=S_TASK_GROUP_TEXT, kind="text",
550
+ label="Task group 을 입력해주세요 (예: backend-api, INV-1234, refactor)",
551
+ echo_template="task-group: {value}")
552
+
553
+
554
+ def _submit_task_group_text(state: WizardState, value: str) -> Optional[str]:
430
555
  state.task_group = _slug_or_die(value, "task_group")
556
+ state.task_group_pending_text = False
431
557
  return f"task-group: {state.task_group}"
432
558
 
433
559
 
434
560
  def _build_task_id(state: WizardState) -> Prompt:
561
+ sugg = state.task_id_suggestion
562
+ if sugg:
563
+ return Prompt(
564
+ step=S_TASK_ID, kind="pick",
565
+ label=f"Task id? (brief 추천: {sugg})",
566
+ options=[
567
+ _opt(PICK_USE_SUGGESTED, f"brief 값 사용: {sugg}"),
568
+ _opt(PICK_TYPE_CUSTOM, "다른 값 입력"),
569
+ ],
570
+ echo_template="task-id: {value}",
571
+ )
435
572
  return Prompt(step=S_TASK_ID, kind="text",
436
573
  label="Task id 를 알려주세요 (예: login-error-analysis, dev-9043)",
437
574
  echo_template="task-id: {value}")
438
575
 
439
576
 
440
577
  def _submit_task_id(state: WizardState, value: str) -> Optional[str]:
578
+ if state.task_id_suggestion:
579
+ if value == PICK_USE_SUGGESTED:
580
+ state.task_id = _slug_or_die(state.task_id_suggestion, "task_id")
581
+ state.task_id_pending_text = False
582
+ return f"task-id: {state.task_id} (brief)"
583
+ if value == PICK_TYPE_CUSTOM:
584
+ state.task_id_pending_text = True
585
+ return f"task-id: (직접 입력)"
586
+ raise WizardError(
587
+ f"expected {PICK_USE_SUGGESTED!r} or {PICK_TYPE_CUSTOM!r}, "
588
+ f"got: {value!r}"
589
+ )
441
590
  state.task_id = _slug_or_die(value, "task_id")
591
+ state.task_id_pending_text = False
592
+ return f"task-id: {state.task_id}"
593
+
594
+
595
+ def _build_task_id_text(state: WizardState) -> Prompt:
596
+ return Prompt(step=S_TASK_ID_TEXT, kind="text",
597
+ label="Task id 를 입력해주세요 (예: login-error-analysis, dev-9043)",
598
+ echo_template="task-id: {value}")
599
+
600
+
601
+ def _submit_task_id_text(state: WizardState, value: str) -> Optional[str]:
602
+ state.task_id = _slug_or_die(value, "task_id")
603
+ state.task_id_pending_text = False
442
604
  return f"task-id: {state.task_id}"
443
605
 
444
606
 
@@ -502,6 +664,14 @@ def _build_brief_path(state: WizardState) -> Prompt:
502
664
  def _submit_brief_path(state: WizardState, value: str) -> Optional[str]:
503
665
  p = _require_file(value, Path(state.project_root), "task brief")
504
666
  state.brief_path = str(p)
667
+ # When the user is starting a brand-new task, pull task-group /
668
+ # task-id candidates from the brief frontmatter so the next two
669
+ # prompts can offer them as a one-click pick instead of forcing a
670
+ # free-text retype of what the brief already declares.
671
+ if state.is_new_task and not state.task_group and not state.task_id:
672
+ tg, tid = _brief_suggestions(p)
673
+ state.task_group_suggestion = tg
674
+ state.task_id_suggestion = tid
505
675
  return f"brief: {p}"
506
676
 
507
677
 
@@ -1002,15 +1172,57 @@ STEPS: list[Step] = [
1002
1172
  applies=lambda s: s.is_new_task is None,
1003
1173
  build=_build_task_pick, submit=_submit_task_pick,
1004
1174
  owns=("is_new_task", "task_group", "task_id", "task_type",
1005
- "existing_brief_path", "profile_workers")),
1175
+ "existing_brief_path", "profile_workers",
1176
+ "task_group_suggestion", "task_id_suggestion",
1177
+ "task_group_pending_text", "task_id_pending_text")),
1178
+ Step(S_BRIEF_PATH,
1179
+ applies=lambda s: (
1180
+ not s.brief_path
1181
+ and (
1182
+ # new-task flow: collect brief FIRST so task-group /
1183
+ # task-id can be offered as one-click picks from the
1184
+ # brief's frontmatter.
1185
+ (s.is_new_task is True)
1186
+ # existing-task flow: brief comes after task-type and
1187
+ # the optional brief-keep step (unchanged behavior).
1188
+ or (s.is_new_task is False
1189
+ and S_TASK_TYPE in s.answered
1190
+ and (s.keep_existing_brief is False
1191
+ or not s.existing_brief_path))
1192
+ )
1193
+ ),
1194
+ build=_build_brief_path, submit=_submit_brief_path,
1195
+ owns=("brief_path", "task_group_suggestion", "task_id_suggestion")),
1006
1196
  Step(S_TASK_GROUP,
1007
- applies=lambda s: bool(s.is_new_task) and not s.task_group,
1197
+ applies=lambda s: (bool(s.is_new_task)
1198
+ and bool(s.brief_path)
1199
+ and not s.task_group
1200
+ and not s.task_group_pending_text),
1008
1201
  build=_build_task_group, submit=_submit_task_group,
1009
- owns=("task_group",)),
1202
+ owns=("task_group", "task_group_pending_text")),
1203
+ Step(S_TASK_GROUP_TEXT,
1204
+ applies=lambda s: (bool(s.is_new_task)
1205
+ and bool(s.brief_path)
1206
+ and not s.task_group
1207
+ and s.task_group_pending_text),
1208
+ build=_build_task_group_text, submit=_submit_task_group_text,
1209
+ owns=("task_group", "task_group_pending_text")),
1010
1210
  Step(S_TASK_ID,
1011
- applies=lambda s: bool(s.is_new_task) and bool(s.task_group) and not s.task_id,
1211
+ applies=lambda s: (bool(s.is_new_task)
1212
+ and bool(s.brief_path)
1213
+ and bool(s.task_group)
1214
+ and not s.task_id
1215
+ and not s.task_id_pending_text),
1012
1216
  build=_build_task_id, submit=_submit_task_id,
1013
- owns=("task_id",)),
1217
+ owns=("task_id", "task_id_pending_text")),
1218
+ Step(S_TASK_ID_TEXT,
1219
+ applies=lambda s: (bool(s.is_new_task)
1220
+ and bool(s.brief_path)
1221
+ and bool(s.task_group)
1222
+ and not s.task_id
1223
+ and s.task_id_pending_text),
1224
+ build=_build_task_id_text, submit=_submit_task_id_text,
1225
+ owns=("task_id", "task_id_pending_text")),
1014
1226
  Step(S_TASK_TYPE,
1015
1227
  applies=lambda s: (s.is_new_task is not None
1016
1228
  and (s.is_new_task is False or bool(s.task_id))
@@ -1023,15 +1235,7 @@ STEPS: list[Step] = [
1023
1235
  and s.keep_existing_brief is None
1024
1236
  and S_TASK_TYPE in s.answered),
1025
1237
  build=_build_brief_keep, submit=_submit_brief_keep,
1026
- owns=("keep_existing_brief", "brief_path")),
1027
- Step(S_BRIEF_PATH,
1028
- applies=lambda s: (S_TASK_TYPE in s.answered
1029
- and not s.brief_path
1030
- and (s.is_new_task
1031
- or s.keep_existing_brief is False
1032
- or (not s.is_new_task and not s.existing_brief_path))),
1033
- build=_build_brief_path, submit=_submit_brief_path,
1034
- owns=("brief_path",)),
1238
+ owns=("keep_existing_brief",)),
1035
1239
  Step(S_BASE_REF_PICK,
1036
1240
  applies=lambda s: (S_TASK_TYPE in s.answered
1037
1241
  and s.reuse_worktree is False
@@ -1240,6 +1444,8 @@ def _reset_from(state: WizardState, target_step: str) -> None:
1240
1444
  _FIELD_DEFAULTS: dict[str, Any] = {
1241
1445
  "is_new_task": None, "task_group": "", "task_id": "",
1242
1446
  "existing_brief_path": "", "task_type": "",
1447
+ "task_group_suggestion": "", "task_id_suggestion": "",
1448
+ "task_group_pending_text": False, "task_id_pending_text": False,
1243
1449
  "profile_workers": [], "keep_existing_brief": None,
1244
1450
  "brief_path": "", "reuse_worktree": None, "base_ref": "",
1245
1451
  "base_ref_pending_text": False, "approved_plan_path": "",
@@ -648,11 +648,15 @@ reporter-confirmations: <complete | partial | pending | skipped> # set by Step
648
648
  > Recommended next phase: <requirements-discovery | error-analysis> ← from Step 6
649
649
  > Handoff contract: see `prompts/profiles/_common-contract.md` § "Brief handoff contract"
650
650
 
651
- ## Source Material (verbatim — do not modify)
651
+ ## Source Material
652
652
 
653
+ <!-- author guidance — strip out at fill-in time:
653
654
  Paste each source separately and as-is. No paraphrasing, summarizing, or
654
655
  restructuring. Format conversion (e.g. Jira ADF → Markdown) is allowed and
655
- must be annotated in the header meta.
656
+ must be annotated in the header meta. Heading was originally
657
+ "Source Material (verbatim — do not modify)" — the parenthetical is a
658
+ reviewer note, not body text.
659
+ -->
656
660
 
657
661
  ### Source 1 — <type: file | linear | jira | github | notion | url | user-input>
658
662
 
@@ -665,30 +669,31 @@ must be annotated in the header meta.
665
669
  <Paste the raw source here without changing a single character.>
666
670
  ```
667
671
 
668
- ### Source 2...
669
-
670
- (Repeat as needed.)
672
+ <!-- Repeat `### Source N…` blocks as needed. -->
671
673
 
672
674
  ## Context
673
675
 
674
676
  <Background / scope / why now. If self-evident from Source Material, quote
675
677
  it briefly and stop. Use the blockquote below when augmentation is needed.>
676
678
 
677
- > augmented: <label> — <Interpretation added by the skill or user. Label
678
- > MUST be one of: `evidence-link` / `format-conversion` /
679
- > `terminology-mapping` / `intent-inference`. Do NOT add any extra
680
- > interpretation outside this blockquote.>
679
+ > augmented: <label> — <Interpretation added by the skill or user.>
680
+
681
+ <!-- label MUST be one of: `evidence-link` / `format-conversion` /
682
+ `terminology-mapping` / `intent-inference`. Do NOT add any extra
683
+ interpretation outside the `> augmented:` blockquote. -->
681
684
 
682
685
  ## Problem / Symptom
683
686
 
684
687
  <Current state. For bugs: repro / observed / expected. For greenfield: gap
685
- between current and desired. Same source-quote + `> augmented:` rule as
686
- above.>
688
+ between current and desired.>
689
+
690
+ <!-- Same source-quote + `> augmented:` rule as the Context section. -->
687
691
 
688
692
  ## Desired Outcome
689
693
 
690
- <Shape of success. Do NOT prescribe a solution — that belongs to
691
- implementation-planning.>
694
+ <Shape of success.>
695
+
696
+ <!-- Do NOT prescribe a solution — that belongs to implementation-planning. -->
692
697
 
693
698
  ## Constraints
694
699
 
@@ -701,14 +706,18 @@ none.>
701
706
 
702
707
  ## Open Questions
703
708
 
709
+ <!-- author guidance — strip out at fill-in time:
704
710
  Prefix every row with one of these signals so the next phase knows how to
705
711
  handle it. Free-form rows are allowed only as `general:`.
706
712
 
713
+ Allowed signals:
707
714
  - `general: <unresolved question the user flagged>`
708
- - `terminology: <reporter word> — needs canonical resolution against <PROJECT_ROOT>/.project-docs/okstra/glossary.md`
715
+ - `terminology: <reporter word> — needs canonical resolution against
716
+ <PROJECT_ROOT>/.project-docs/okstra/glossary.md`
709
717
  - `intent-check: <restated inference> — confirm with reporter`
710
718
  (auto-paired with every `intent-inference` augmentation)
711
- - `conversion-block: <reporter statement> — could not be mapped to project vocabulary; reporter query required`
719
+ - `conversion-block: <reporter statement> — could not be mapped to project
720
+ vocabulary; reporter query required`
712
721
  - `adr-candidate: <topic>` — signal only; `implementation-planning`
713
722
  evaluates and, if accepted, drafts a decision file at
714
723
  `<PROJECT_ROOT>/.project-docs/okstra/decisions/<NNNN>-<slug>.md`.
@@ -718,11 +727,14 @@ Use `_(none)_` only if every signal is empty. `intent-check:` and
718
727
  from this list — they receive a `[CONFIRMED <YYYY-MM-DD> → RC-N]`
719
728
  marker that links to the corresponding entry under
720
729
  `## Reporter Confirmations`.
730
+ -->
731
+
732
+ - <fill in one row per signal, or replace with `_(none)_`>
721
733
 
722
734
  ## Reporter Confirmations
723
735
 
724
- Populated by Step 6.5. Each subsection records one reporter answer
725
- verbatim, with a link back to the originating `Open Questions` row.
736
+ <!-- Populated by Step 6.5. Each subsection records one reporter answer
737
+ verbatim, with a link back to the originating `Open Questions` row. -->
726
738
 
727
739
  _(none — pending or skipped)_
728
740
 
@@ -737,6 +749,7 @@ _(none — pending or skipped)_
737
749
 
738
750
  ## Augmentation
739
751
 
752
+ <!-- author guidance — strip out at fill-in time:
740
753
  Cross-references / interpretation / context added by the user or skill that
741
754
  is not in the original source. May be empty. Keep this section visually
742
755
  separated from Source Material — never inline it inside Source Material.
@@ -744,44 +757,59 @@ separated from Source Material — never inline it inside Source Material.
744
757
  Every entry below must start with one of the four labels:
745
758
  `evidence-link` / `format-conversion` / `terminology-mapping` /
746
759
  `intent-inference`. Unlabelled entries are forbidden.
760
+ -->
747
761
 
748
762
  ### Domain alignment
749
763
 
750
- Observations from Step 3b and the outcome of Step 4.5 (glossary
751
- applied vs. skipped). The actual glossary edits live in
764
+ <!-- author guidance strip out at fill-in time:
765
+ Observations from Step 3b and the outcome of Step 4.5 (glossary applied
766
+ vs. skipped). The actual glossary edits live in
752
767
  `<PROJECT_ROOT>/.project-docs/okstra/glossary.md` when applied; this
753
- section records what happened. Decision candidates are NOT recorded
754
- here — they flow through `Open Questions` as `adr-candidate:` rows for
768
+ section records what happened. Decision candidates are NOT recorded here —
769
+ they flow through `Open Questions` as `adr-candidate:` rows for
755
770
  `implementation-planning` to evaluate (and, if accepted, draft into
756
771
  `<PROJECT_ROOT>/.project-docs/okstra/decisions/`).
757
772
 
773
+ Allowed entry shapes:
758
774
  - `terminology-mapping: <reporter word> → <okstra glossary canonical>` —
759
775
  routine glossary alignment, paired with `terminology:` in Open Questions
760
776
  when unresolved.
761
- - `terminology-mapping: applied glossary: <term> → <PROJECT_ROOT>/.project-docs/okstra/glossary.md` /
762
- `terminology-mapping: skipped glossary: <term> = <definition>` — Step
763
- 4.5 outcomes.
764
- - Use `_(none)_` if every alignment entry is empty.
777
+ - `terminology-mapping: applied glossary: <term> → <PROJECT_ROOT>/.project-docs/okstra/glossary.md`
778
+ - `terminology-mapping: skipped glossary: <term> = <definition>` —
779
+ Step 4.5 outcomes.
780
+ Use `_(none)_` if every alignment entry is empty.
781
+ -->
782
+
783
+ - <fill in one entry per alignment, or replace with `_(none)_`>
765
784
 
766
785
  ### Evidence links (file / symbol resolution)
767
786
 
768
- - `evidence-link: <reporter phrase> → <relative path>:<line>` /
769
- `evidence-link: <reporter phrase> → <symbol> in <relative path>`
770
- - `_(none)_` if none.
787
+ <!-- Allowed entry shapes:
788
+ `evidence-link: <reporter phrase> → <relative path>:<line>` or
789
+ `evidence-link: <reporter phrase> → <symbol> in <relative path>`.
790
+ Use `_(none)_` if none. -->
791
+
792
+ - <fill in one entry per link, or replace with `_(none)_`>
771
793
 
772
794
  ### Intent inferences
773
795
 
774
- Every entry here is an unverified hypothesis. Each one MUST have a paired
775
- `intent-check:` row under Open Questions.
796
+ <!-- Every entry here is an unverified hypothesis. Each one MUST have a
797
+ paired `intent-check:` row under Open Questions.
798
+
799
+ Allowed entry shape:
800
+ `intent-inference: <reporter phrase> → <qualitative restatement>`
801
+ (qualitative only — never invent numeric thresholds).
802
+ Use `_(none)_` if none. -->
776
803
 
777
- - `intent-inference: <reporter phrase> <qualitative restatement>`
778
- (qualitative only — never invent numeric thresholds)
779
- - `_(none)_` if none.
804
+ - <fill in one entry per inference, or replace with `_(none)_`>
780
805
 
781
806
  ### Format conversions
782
807
 
783
- - `format-conversion: <ref> — <e.g. Jira ADF → Markdown, semantics preserved>`
784
- - `_(none)_` if none.
808
+ <!-- Allowed entry shape:
809
+ `format-conversion: <ref> <e.g. Jira ADF → Markdown, semantics preserved>`.
810
+ Use `_(none)_` if none. -->
811
+
812
+ - <fill in one entry per conversion, or replace with `_(none)_`>
785
813
  ````
786
814
 
787
815
  ### Frontmatter rules
@@ -966,6 +994,16 @@ started.
966
994
  is allowed, and the conversion must be annotated in the `format:` meta.
967
995
  Augmentation / interpretation goes only into the `Augmentation` section
968
996
  or a `> augmented:` blockquote inside the required section.
997
+ - **No template author-guidance in the artifact body.** The Step 5
998
+ template above carries author guidance in HTML comments
999
+ (`<!-- ... -->`); preserve those comments when writing the brief but do
1000
+ NOT promote them to body prose. Section headings stay clean — never copy
1001
+ parenthetical reviewer notes onto the heading itself (e.g. emit
1002
+ `## Source Material`, not `## Source Material (verbatim — do not modify)`).
1003
+ Placeholder prose inside angle brackets (`<Background / scope / why
1004
+ now…>`) is intentional and is meant to be replaced with the reporter's
1005
+ actual content; do not strip the angle-bracket placeholders, replace
1006
+ them.
969
1007
  - If the tracker MCP is not connected, do not guess — ask the user to paste
970
1008
  the body or skip the source.
971
1009
  - If a URL fetch fails or hits an auth wall, do not guess — ask for a