okstra 0.21.1 → 0.23.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.
Files changed (38) hide show
  1. package/README.kr.md +3 -0
  2. package/README.md +3 -0
  3. package/bin/okstra +2 -0
  4. package/docs/kr/architecture.md +1 -0
  5. package/docs/project-structure-overview.md +53 -0
  6. package/package.json +1 -1
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/agents/workers/claude-worker.md +3 -1
  9. package/runtime/agents/workers/codex-worker.md +3 -1
  10. package/runtime/agents/workers/gemini-worker.md +3 -1
  11. package/runtime/agents/workers/report-writer-worker.md +16 -1
  12. package/runtime/bin/okstra-trace-cleanup.sh +44 -11
  13. package/runtime/prompts/profiles/_common-contract.md +12 -0
  14. package/runtime/prompts/profiles/release-handoff.md +18 -1
  15. package/runtime/python/okstra_ctl/index.py +2 -1
  16. package/runtime/python/okstra_ctl/pr_template.py +126 -0
  17. package/runtime/python/okstra_ctl/render.py +28 -2
  18. package/runtime/python/okstra_ctl/run.py +41 -2
  19. package/runtime/python/okstra_ctl/seeding.py +19 -0
  20. package/runtime/python/okstra_ctl/workers.py +20 -0
  21. package/runtime/skills/okstra-run/SKILL.md +68 -32
  22. package/runtime/skills/okstra-run/templates/pr-body.template.md +41 -0
  23. package/runtime/skills/okstra-setup/SKILL.md +37 -0
  24. package/runtime/skills/okstra-team-contract/SKILL.md +46 -1
  25. package/runtime/templates/prd/brief.template.md +1 -0
  26. package/runtime/templates/project-docs/task-index.template.md +1 -0
  27. package/runtime/templates/reports/error-analysis-input.template.md +1 -0
  28. package/runtime/templates/reports/final-report.template.md +2 -0
  29. package/runtime/templates/reports/final-verification-input.template.md +1 -0
  30. package/runtime/templates/reports/implementation-input.template.md +1 -0
  31. package/runtime/templates/reports/implementation-planning-input.template.md +1 -0
  32. package/runtime/templates/reports/quick-input.template.md +1 -0
  33. package/runtime/templates/reports/release-handoff-input.template.md +1 -0
  34. package/runtime/templates/reports/schedule.template.md +1 -0
  35. package/runtime/templates/reports/settings.template.json +4 -2
  36. package/runtime/templates/reports/task-brief.template.md +1 -0
  37. package/src/config.mjs +392 -0
  38. package/src/render-bundle.mjs +2 -1
package/README.kr.md CHANGED
@@ -167,6 +167,8 @@ Claude Code 세션 밖에서 task 를 시작하려면:
167
167
 
168
168
  - **`--task-type implementation` 격리 worktree 자동 provisioning** — prepare 단계에서 `okstra-ctl` 이 `git worktree add ~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>-<run-seq>` 를 수행해 격리된 working tree 와 브랜치 `<work-category-prefix>-<task-id-segment>-<run-seq>` (예: `feat-dev-9436-001`, `fix-dev-7311-002`) 를 만듭니다. base ref 는 사용자가 `--base-ref` 로 지정 (release-handoff PR base picker 와 동일한 메뉴: `main` / `dev` / `staging` / `preprod` / `prod` / 직접 입력). 첫 phase 에서는 필수이며, okstra-run skill 이 `AskUserQuestion` 으로 수집합니다 — 비대화형 호출자는 `--base-ref` 플래그를 직접 전달해야 prepare 가 통과합니다. Executor 와 verifier 모두 이 worktree 안에서 동작하므로 caller 의 작업 디렉터리는 깨끗하게 유지되고, worktree 는 PR 작성 · rollback 검증의 권위 artefact 로 남습니다. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 됩니다. 수동 cleanup: `git worktree remove <path>` → `git branch -D <branch>`. 상세: [`docs/kr/architecture.md`](docs/kr/architecture.md) *Task type* 섹션, [`docs/kr/cli.md#--executor`](docs/kr/cli.md#--executor).
169
169
  - **`release-handoff` lifecycle phase** — `final-verification` 이 `verdict=accepted` 를 반환한 직후에 실행되는 신규 phase. lead 가 Claude worker (drafter) 를 통해 commit message · PR body 후보를 만들고, `AskUserQuestion` 으로 사용자에게 (1) action (`commit only` / `commit + PR` / `skip`), (2) PR base branch (`staging` / `preprod` / `prod` / `main` / `dev` / 직접 입력), (3) message handling (`use as-is` / `edit then proceed` / `cancel`) 세 가지를 순서대로 묻습니다. 사용자가 메뉴로 선택한 git / gh 명령만 실행되고, force-push, base 브랜치 직접 push, hook bypass (`--no-verify`), release publish (`gh release`, `npm publish`, ...) 는 금지됩니다. 이 phase 에서는 소스 코드를 수정하지 않습니다. profile: [`prompts/profiles/release-handoff.md`](prompts/profiles/release-handoff.md).
170
+ - **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 가 주석을 제거합니다.
171
+ - **프로파일 워커 로스터 검증** — `--workers <csv>` 와 okstra-run Step 6 의 워커 prompt 는 해당 프로파일의 `Required workers:` 블록에 선언된 워커 ID 만 허용합니다. 프로파일에 없는 워커 (예: `release-handoff` 에서 `codex` / `gemini`) 를 요청하면 명확한 에러로 거절되고, 인터랙티브 prompt 도 프로파일이 실제로 받는 워커만 보여줍니다.
170
172
 
171
173
  ### 3.5 운영 명령
172
174
 
@@ -177,6 +179,7 @@ Claude Code 세션 밖에서 task 를 시작하려면:
177
179
  | `npx -y okstra@latest ensure-installed` | Idempotent 체크, stale 이면 자동 재설치 (스킬이 내부적으로 호출) |
178
180
  | `npx -y okstra@latest setup --project-id <id>` | 현재 프로젝트를 등록 (`.project-docs/okstra/project.json`) |
179
181
  | `npx -y okstra@latest check-project` | 현재 프로젝트가 `setup` 으로 등록됐는지 검증 |
182
+ | `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` 갱신) |
180
183
  | `npx -y okstra@latest uninstall` | 런타임 + 스킬 제거; 사용자 데이터(`recent.jsonl`, `projects/`, …)는 보존 |
181
184
  | `npx -y okstra@latest uninstall --purge -y` | 사용자 데이터까지 모두 제거 |
182
185
 
package/README.md CHANGED
@@ -166,6 +166,8 @@ Recent workflow additions (post-0.8.0, on `main`):
166
166
 
167
167
  - **Isolated task worktree for every task-type** — prepare automatically runs `git worktree add ~/.okstra/worktrees/<project>/<group>/<task>/` on a fresh branch `<work-category-prefix>-<task-id-segment>` branched from the **user-chosen base ref** (`--base-ref`, mirroring the `release-handoff` PR-base picker: `main` / `dev` / `staging` / `preprod` / `prod` / any local ref) the first time a task-key is seen. `--base-ref` is required on first phase; the okstra-run skill collects it via `AskUserQuestion`, non-interactive callers must pass the flag explicitly. Every subsequent phase of the same task-key (`requirements-discovery` → `error-analysis` → `implementation-planning` → `implementation`) reuses the same path and branch, so phase N inherits the working-tree state phase N-1 left behind. A global registry at `~/.okstra/worktrees/registry.json` (flock-guarded) reserves task-keys and branches across concurrent runs; all path/branch segments are sanitised (`/`, `:`, etc. → `-`). The worktree is preserved after every run for follow-up phases, PR authoring, and rollback. Skip paths: when the caller is already inside another worktree or `project_root` is not a git repo, provisioning no-ops. Manual cleanup: `git worktree remove <path>` → `git branch -D <branch>` plus removing the task-key entry from the registry. Details: [`docs/kr/architecture.md`](docs/kr/architecture.md) (*Task type* section) and [`docs/kr/cli.md#--executor`](docs/kr/cli.md#--executor).
168
168
  - **`release-handoff` lifecycle phase** — runs after `final-verification` returns `verdict=accepted`. The lead drafts a commit message and PR body via a Claude worker, then prompts the user with `AskUserQuestion` for three choices: action (`commit only` / `commit + PR` / `skip`), PR base branch (`staging` / `preprod` / `prod` / `main` / `dev` / free-form), and message handling (`use as-is` / `edit then proceed` / `cancel`). Only user-selected mutating git/gh commands run. Force-push, base-branch direct push, hook bypass (`--no-verify`), and release publishing (`gh release`, `npm publish`, ...) are forbidden. Source code is not edited in this phase. Profile: [`prompts/profiles/release-handoff.md`](prompts/profiles/release-handoff.md).
169
+ - **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.
170
+ - **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.
169
171
 
170
172
  ### 3.5 Ops commands
171
173
 
@@ -176,6 +178,7 @@ Recent workflow additions (post-0.8.0, on `main`):
176
178
  | `npx -y okstra@latest ensure-installed` | Idempotent check, auto-reinstall if stale (skills call this internally) |
177
179
  | `npx -y okstra@latest setup --project-id <id>` | Register the current project (`.project-docs/okstra/project.json`) |
178
180
  | `npx -y okstra@latest check-project` | Verify the current project has been registered with `setup` |
181
+ | `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`) |
179
182
  | `npx -y okstra@latest uninstall` | Remove runtime + skills; preserves user data (`recent.jsonl`, `projects/`, …) |
180
183
  | `npx -y okstra@latest uninstall --purge -y` | Remove everything including user data |
181
184
 
package/bin/okstra CHANGED
@@ -9,6 +9,7 @@ const COMMANDS = new Map([
9
9
  ["doctor", () => import("../src/doctor.mjs").then((m) => m.run)],
10
10
  ["setup", () => import("../src/setup.mjs").then((m) => m.run)],
11
11
  ["check-project", () => import("../src/check-project.mjs").then((m) => m.run)],
12
+ ["config", () => import("../src/config.mjs").then((m) => m.run)],
12
13
  ["task-list", () => import("../src/task-list.mjs").then((m) => m.run)],
13
14
  ["task-show", () => import("../src/task-show.mjs").then((m) => m.run)],
14
15
  ["worktree-lookup", () => import("../src/worktree-lookup.mjs").then((m) => m.run)],
@@ -40,6 +41,7 @@ Admin commands:
40
41
  setup Register the current project (.project-docs/okstra/project.json)
41
42
  doctor Diagnostic check of the installed runtime
42
43
  check-project Verify the current project has been registered with setup
44
+ config Read / write okstra settings (e.g. PR template path)
43
45
  paths Print runtime paths (workspace/agents/pythonpath/bin/home/version)
44
46
 
45
47
  Introspection commands (JSON output, used by skills to avoid python heredocs):
@@ -838,6 +838,7 @@ Claude가 작성하는 최종 보고서는 brief에 더 구체적인 형식이
838
838
  - `scripts/okstra-codex-exec.sh`, `scripts/okstra-gemini-exec.sh` 는 dispatch 마다 prompt path 옆에 `<prompt>.log` sidecar 를 만들고 stdout 을 거기로 mirror 합니다 (`tee`, `PIPESTATUS[0]` 로 종료코드 보존). stderr 은 같은 파일에 append (subagent stderr 캡처 contract 보존), 매 dispatch 시 truncate. 호출 subagent 의 `BashOutput` 폴링은 60s 간격이라 long-running run (analysis 의 large-codebase scan, implementation 의 cargo / pytest) 동안 사용자가 stalled state 를 탐지할 수 없는 문제를 해소합니다.
839
839
  - `$TMUX` 가 셋팅된 lead 환경이면 wrapper 가 sibling pane 을 자동 분할해 `tail -F <log-path>` 를 띄웁니다. pane title 은 `<cli>-<role>-trace` (e.g. `codex-worker-trace`, `gemini-worker-trace`); role 은 wrapper 의 5번째 optional positional 인자이며, 누락 시 기본값 `worker` 로 떨어집니다. caller 가 다른 라벨(예: `executor`)을 원하면 5번째 인자로 명시해야 합니다. focus 는 caller pane 으로 복귀하고, CLI 종료 후 pane 은 유지돼 스크롤백 가능. `$TMUX` 미설정, split 실패, 구버전 tmux 등 모든 경로는 silent degrade.
840
840
  - **Claude `/exit` 시 자동 정리**: trace pane 의 `tail -F` 는 tmux 셸의 자식이라 Claude 가 종료돼도 살아남는 문제를 막기 위해, wrapper 는 spawn 한 pane id 를 caller `$TMUX_PANE` 으로 키된 registry (`${TMPDIR:-/tmp}/okstra-trace-panes/<caller-pane>.list`) 에 append 합니다. `templates/reports/settings.template.json` 의 `hooks.SessionEnd` 가 `$HOME/.okstra/bin/okstra-trace-cleanup.sh` 를 호출해 자신의 caller pane registry 만 읽어 `tmux kill-pane` 합니다. caller pane 단위로 scope 가 잡혀 있어 같은 tmux 세션에 Claude 인스턴스가 여러 개 떠 있어도 서로의 trace pane 을 죽이지 않습니다. tmux 가 없거나 stale pane id 인 경우 silent degrade.
841
+ - **Phase 종료 시 사용자 확인**: 매 phase 의 마지막 단계로 lead 가 `okstra-trace-cleanup.sh --list` 로 등록된 pane 목록을 출력한 뒤 사용자에게 "모두 닫기 / 그대로 두기" 양자택일을 묻고 응답대로 처리합니다 (`prompts/profiles/_common-contract.md` 의 *Phase wrap-up* 항목). `$TMUX_PANE` 미설정 환경에서는 단계 자체가 silent-skip. `--list` 모드는 pane 을 죽이지 않고 `<pane_id>\t<pane_title>` 만 출력하므로 사용자가 무엇이 닫힐지 시각적으로 확인할 수 있습니다.
841
842
  - 디스크 누적은 `okstra-logs` skill 이 read-only 로 인벤토리 + cleanup 명령을 제안합니다 (실행은 사용자 copy-paste).
842
843
 
843
844
  ### Linked-worktree `.git/` write 권한 (codex / gemini)
@@ -385,4 +385,57 @@ okstra/
385
385
 
386
386
  ---
387
387
 
388
+ ## 부록 A. 보고서 row ID prefix 용어집
389
+
390
+ okstra 가 산출하는 final-report 와 worker-result 의 표·리스트는 섹션별 두-글자 prefix 로 행을 식별합니다. 같은 prefix 는 어느 task / phase / project 에서도 의미가 동일하므로, 한 run 의 `C-005` 를 후속 run 이나 follow-up task 에서 그대로 인용해 cross-reference 할 수 있습니다.
391
+
392
+ ### A.1 Final-report 행 ID
393
+
394
+ | Prefix | 의미 | 정의 위치 (template / contract) |
395
+ |--------|------|----------------------------------|
396
+ | `P-NNN` | Problem / Verification Target — 본 run 이 해결하려는 문제·검증 대상 한 줄 요약 | `## Summary of the Problem or Verification Target` |
397
+ | `C-NNN` | Consensus — 모든 worker 가 합의한 결론 | `### 1.1 Consensus` |
398
+ | `D-NNN` | Differences — worker 간 의미 있는 불일치 | `### 1.2 Differences` |
399
+ | `E-NNN` | Primary Evidence — 본 verdict 의 1차 근거 | `### 3.1 Primary Evidence` |
400
+ | `S-NNN` | Secondary Evidence / Alternate Interpretations — 보조 근거·대안 해석 | `### 3.2 Secondary Evidence or Alternate Interpretations` |
401
+ | `R-NNN` | Missing Information / Risks — 누락 정보·인지된 위험 | `## 4. Missing Information and Risks` |
402
+ | `RR-NNN` | Residual Risk — verdict 를 막진 않지만 추적해야 하는 잔여 위험 | `### 4.2 Residual Risk` |
403
+ | `OF-NNN` | Option File-structure rows — implementation-planning 의 옵션별 파일 책임 | `### 4.5.1 Option Candidates` |
404
+ | `DM-NNN` | Dependency / Migration risk — 순서·백필·feature-flag 선행 조건 | `### 4.5.5 Dependency / Migration Risk` |
405
+ | `RB-NNN` | Rollback step — revert 경로·트리거 신호 | `### 4.5.7 Rollback Strategy` |
406
+ | `OQ-NNN` | Open Question — pre-planning 에서 발견된 모호점 | `### 4.5.9 Open Questions` |
407
+ | `FU-NNN` | Follow-up Task — 본 run 범위 밖이지만 후속 처리해야 하는 항목 | `## 7. Follow-up Tasks` |
408
+ | `FU-VNN` | Verifier-harness Follow-up — verifier 환경 결함을 잡는 후속 (okstra 본체 fix) | `## 7.` (lead synthesis, `Origin = manual` 표기) |
409
+ | `A1, A2, …` | Additional materials requested — 다음 run 시작 전 사용자가 채워야 할 자료 요청. **run 사이에 ID 유지**, 답변 완료 시 `Status` 만 갱신 | `### 5.1 추가 자료 요청` |
410
+ | `Q1, Q2, …` | Questions for the user — 동일 규칙으로 run 간 ID 유지 | `### 5.2 사용자 확인 질문` |
411
+
412
+ ### A.2 Worker-result 행 ID (worker 가 자기 output 안에서 매기는 ID)
413
+
414
+ `okstra-team-contract` 가 정의하는 worker output 6개 섹션 중 1, 2 섹션이 prefix 를 갖습니다.
415
+
416
+ | Prefix | 의미 | Worker output 섹션 |
417
+ |--------|------|-------------------|
418
+ | `F-NNN` | Finding — worker 의 핵심 발견 | §1 Findings |
419
+ | `M-NNN` | Missing Information / Assumption — worker 가 인지한 누락 정보·가정 | §2 Missing Information or Assumptions |
420
+
421
+ §3 Safe Areas, §4 Uncertain Points, §5 Recommended Next Actions, §6 Specialization Lens 는 표 형태가 아니라 별도 prefix 가 없습니다.
422
+
423
+ ### A.3 Phase / 입력 도메인 약어
424
+
425
+ | 약어 | 의미 |
426
+ |------|------|
427
+ | `AC` | Acceptance Criteria — `final-verification-input.template.md` 의 `## Acceptance Criteria` 섹션에 사용자가 적어두는 합격 기준 (예: `AC1`, `AC2`, … `AC10`). Verifier 는 한 AC 당 한 줄로 `covered` / `not covered` 판정 |
428
+ | `OQ` | Open Question — implementation-planning 의 `### 4.5.9` 행 ID. final-report 의 다른 섹션에서 `OQ-A`, `OQ-D` 처럼 알파벳 인덱스로도 인용됨 |
429
+ | `FU-V` | Follow-up — Verifier harness 카테고리. `okstra-codex-exec.sh`, worktree provisioner 등 okstra 본체의 verifier 환경 결함을 잡는 후속 task |
430
+
431
+ ### A.4 Ticket / Verdict 토큰
432
+
433
+ | 토큰 | 위치 | 허용 값 |
434
+ |------|------|---------|
435
+ | `Verdict Token` | `## 2. Final Verdict` | `accepted`, `conditional-accept`, `blocked`, `not-applicable` (final-verification 만 의미 있음, release-handoff 가 진입 gate 로 사용) |
436
+ | `Direction` | `## 2. Final Verdict` | `continue-investigation`, `begin-implementation`, `approve`, `reject`, `hold` |
437
+ | `Ticket ID` | 모든 행 표에 컬럼으로 등장 | brief 의 `Issue / Ticket` 값 → 비어 있으면 `Task ID` → 둘 다 없으면 `unknown` |
438
+
439
+ ---
440
+
388
441
  *작성일: 2026-05-14 · 분석 대상: `claude/objective-einstein-250add` 브랜치 기준 okstra v0.20.1*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.21.1",
3
+ "version": "0.23.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.21.1",
3
- "builtAt": "2026-05-14T12:50:20.728Z",
2
+ "package": "0.23.0",
3
+ "builtAt": "2026-05-15T01:56:19.363Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -63,7 +63,9 @@ Before producing any output, you MUST read every input file enumerated in the `[
63
63
 
64
64
  ## Worker Output Structure
65
65
 
66
- When returning results, organize into the following sections in this exact order:
66
+ When returning results, start the file with a YAML frontmatter block, then organize the body into the following sections in this exact order.
67
+
68
+ **Frontmatter (mandatory)** — set `workerId: "claude"`. Copy `id`, `aliases`, `taskType`, `task-id`, `task-group`, `project-id`, `date` verbatim from the input files (`analysis-material.md` is canonical; if it lacks any field, record a `tool-failure` and stop). Full schema and a concrete example live in the `okstra-team-contract` skill's "Result Frontmatter" subsection.
67
69
 
68
70
  0. **Reading Confirmation** - one short line per input file confirming end-to-end reading (e.g. `- Read task-brief.md end-to-end (147 lines).`). If any file was skipped, record a `tool-failure` and do NOT produce sections 1–5.
69
71
  1. **Findings** - what you identified
@@ -130,7 +130,9 @@ Before producing any output, you MUST ensure the underlying Codex CLI run reads
130
130
 
131
131
  ## Worker Output Structure
132
132
 
133
- When returning results, organize into the following sections in this exact order:
133
+ When returning results, start the file with a YAML frontmatter block, then organize the body into the following sections in this exact order.
134
+
135
+ **Frontmatter (mandatory)** — set `workerId: "codex"`. Copy `id`, `aliases`, `taskType`, `task-id`, `task-group`, `project-id`, `date` verbatim from the input files (`analysis-material.md` is canonical; if it lacks any field, record a `tool-failure` and stop). Full schema and a concrete example live in the `okstra-team-contract` skill's "Result Frontmatter" subsection.
134
136
 
135
137
  0. **Reading Confirmation** - one short line per input file confirming end-to-end reading (e.g. `- Read task-brief.md end-to-end (147 lines).`). If any file was skipped, record a `tool-failure` and do NOT produce sections 1–5.
136
138
  1. **Findings** - what Codex identified
@@ -130,7 +130,9 @@ Before producing any output, you MUST ensure the underlying Gemini CLI run reads
130
130
 
131
131
  ## Worker Output Structure
132
132
 
133
- When returning results, organize into the following sections in this exact order:
133
+ When returning results, start the file with a YAML frontmatter block, then organize the body into the following sections in this exact order.
134
+
135
+ **Frontmatter (mandatory)** — set `workerId: "gemini"`. Copy `id`, `aliases`, `taskType`, `task-id`, `task-group`, `project-id`, `date` verbatim from the input files (`analysis-material.md` is canonical; if it lacks any field, record a `tool-failure` and stop). Full schema and a concrete example live in the `okstra-team-contract` skill's "Result Frontmatter" subsection.
134
136
 
135
137
  0. **Reading Confirmation** - one short line per input file confirming end-to-end reading (e.g. `- Read task-brief.md end-to-end (147 lines).`). If any file was skipped, record a `tool-failure` and do NOT produce sections 1–5.
136
138
  1. **Findings** - what Gemini identified
@@ -81,9 +81,22 @@ The validator (`validators/validate-run.py`) checks this file exists whenever th
81
81
 
82
82
  The lead's prompt provides this path as a second result destination — extract it from the `**Worker Result Path:**` line (or, if absent, derive it as `runs/<task-type>/worker-results/report-writer-worker-<task-type>-<seq>.md` under `Project Root`).
83
83
 
84
- The worker-results file must begin with the standard header from `okstra-team-contract`:
84
+ The worker-results file MUST begin with a YAML frontmatter block (set `workerId: "report-writer"`; copy `id`, `aliases`, `taskType`, `task-id`, `task-group`, `project-id`, `date` verbatim from `analysis-material.md` — full schema lives in the `okstra-team-contract` "Result Frontmatter" subsection), followed by the standard header:
85
85
 
86
86
  ```markdown
87
+ ---
88
+ title: OKSTRA Report Writer Worker Result - <task-key>
89
+ id: "<task-key with ':' replaced by '-'>"
90
+ aliases: ["<id>-<task-type>"]
91
+ tags: ["obsidian", "okstra", "worker-result", "<task-type>"]
92
+ taskType: "<task-type>"
93
+ workerId: "report-writer"
94
+ task-id: "<task-id>"
95
+ task-group: "<task-group>"
96
+ project-id: "<project-id>"
97
+ date: <YYYY-MM-DD>
98
+ ---
99
+
87
100
  # Report Writer Worker Analysis — <task-key>
88
101
 
89
102
  **Task:** <task-type>
@@ -92,6 +105,8 @@ The worker-results file must begin with the standard header from `okstra-team-co
92
105
  **Model:** Report writer worker, opus
93
106
  ```
94
107
 
108
+ The same frontmatter (with `workerId: "report-writer"`) MUST also appear on the final-report file you assemble — the `final-report.template.md` already encodes it, so simply preserve the template's frontmatter block when filling sections.
109
+
95
110
  Followed by a short body that:
96
111
  1. Names the canonical final-report file path written by this worker (relative to project root).
97
112
  2. Lists the input artifacts you reconciled (each analysis worker's result file path under `worker-results/`, plus the convergence-state file path if present).
@@ -1,26 +1,48 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # okstra-trace-cleanup.sh — kill tmux trace panes spawned by okstra worker
3
+ # okstra-trace-cleanup.sh — manage tmux trace panes spawned by okstra worker
4
4
  # wrappers (`okstra-codex-exec.sh`, `okstra-gemini-exec.sh`) for the current
5
5
  # Claude Code session.
6
6
  #
7
- # Invoked from the `SessionEnd` hook in
8
- # `templates/reports/settings.template.json`. The wrappers register every
9
- # pane they split into a registry file keyed by the caller's `$TMUX_PANE`
10
- # (i.e. the pane Claude itself is attached to). On Claude `/exit`, the hook
11
- # runs in that same pane's env, reads its own registry file, and kills each
12
- # registered pane.
7
+ # Two modes:
8
+ # (default) kill every registered pane and remove the registry file.
9
+ # Used by the `SessionEnd` hook in
10
+ # `templates/reports/settings.template.json` so panes do not
11
+ # outlive Claude `/exit`. Also callable manually by the lead at
12
+ # phase-end (see `_common-contract.md`).
13
+ # --list print one line per registered pane (`<pane_id>\t<pane_title>`)
14
+ # so the lead can show the user what *would* be closed before
15
+ # asking. Exits 0 with empty stdout when nothing is tracked.
13
16
  #
14
- # Scoping by caller `$TMUX_PANE` (not by tmux session) lets multiple Claude
15
- # instances coexist in the same tmux session without stomping each other's
16
- # trace panes.
17
+ # Pane registry is keyed by the caller's `$TMUX_PANE` the pane Claude
18
+ # itself is attached to. Multiple Claude instances in the same tmux session
19
+ # therefore do not stomp each other's trace panes.
17
20
  #
18
21
  # Failures are tolerated silently — a stale pane id, missing $TMUX, or a
19
22
  # locked tmux client must never prevent Claude from exiting cleanly.
20
23
 
21
24
  set -u
22
25
 
23
- # No tmux pane context → nothing to clean.
26
+ MODE="kill"
27
+ case "${1:-}" in
28
+ "") MODE="kill" ;;
29
+ --list) MODE="list" ;;
30
+ --dry-run) MODE="list" ;; # alias
31
+ -h|--help)
32
+ cat <<'USAGE'
33
+ usage: okstra-trace-cleanup.sh [--list]
34
+
35
+ (no args) kill every pane registered for $TMUX_PANE; remove registry file.
36
+ --list print "<pane_id>\t<pane_title>" per registered pane; no kill.
37
+ --dry-run alias for --list.
38
+ USAGE
39
+ exit 0 ;;
40
+ *)
41
+ printf 'okstra-trace-cleanup.sh: unknown option: %s\n' "$1" >&2
42
+ exit 2 ;;
43
+ esac
44
+
45
+ # No tmux pane context → nothing to clean / list.
24
46
  if [[ -z "${TMUX_PANE:-}" ]]; then
25
47
  exit 0
26
48
  fi
@@ -33,6 +55,17 @@ if [[ ! -f "$registry_file" ]]; then
33
55
  exit 0
34
56
  fi
35
57
 
58
+ if [[ "$MODE" == "list" ]]; then
59
+ while IFS= read -r pane_id; do
60
+ [[ -n "$pane_id" ]] || continue
61
+ # `display-message -p` resolves a *live* pane's title; for stale ids
62
+ # tmux exits non-zero — fall back to an empty title rather than failing.
63
+ title=$(tmux display-message -p -t "$pane_id" '#{pane_title}' 2>/dev/null || true)
64
+ printf '%s\t%s\n' "$pane_id" "$title"
65
+ done < "$registry_file"
66
+ exit 0
67
+ fi
68
+
36
69
  while IFS= read -r pane_id; do
37
70
  [[ -n "$pane_id" ]] || continue
38
71
  tmux kill-pane -t "$pane_id" 2>/dev/null || true
@@ -20,6 +20,18 @@ profile document.
20
20
  - This rule does NOT relax any phase-specific Forbidden actions list; safety rules in the per-profile document remain in force regardless of the user's authority.
21
21
  - Anti-escalation rule (shared):
22
22
  - treating "다음 단계 진행해" or equivalent user phrases as authorisation to start a *different* lifecycle phase is forbidden. The next phase begins only in a separate okstra run launched with the new `--task-type`. Per-profile documents may further restrict this within their own scope.
23
+ - Phase wrap-up — worker trace pane disposition (shared, MUST be the *last* step before returning control to the user):
24
+ - Codex / Gemini worker wrappers spawn `tail -F` trace panes in the lead's tmux session (`codex-<role>-trace`, `gemini-<role>-trace`). They survive every worker invocation by design so the operator can scroll back through the final output, but accumulate across phases and clutter the screen.
25
+ - When `$TMUX_PANE` is set, after the final-report file has been written and the routing recommendation has been issued, the lead MUST run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --list` exactly once. The output is a tab-separated `<pane_id>\t<pane_title>` list of every trace pane registered for this Claude session.
26
+ - If the list is empty, skip the question — there is nothing to ask about.
27
+ - Otherwise the lead MUST present the user with a strict binary choice **before** declaring the phase complete. Use one prompt of this shape (Korean preferred, English acceptable if the rest of the run is in English):
28
+ > 현재 phase 종료 시점입니다. 다음 worker trace pane 이 열려 있습니다 — 닫을까요?
29
+ > <인용된 `--list` 출력>
30
+ > (예) 모두 닫기 / (아니오) 그대로 두기
31
+ - On `예` / `y` / `close` → run `$HOME/.okstra/bin/okstra-trace-cleanup.sh` (no args) and report the kill count back in one sentence.
32
+ - On `아니오` / `n` / `keep` → leave the panes intact; remind the user that they will be cleaned up automatically when Claude `/exit` fires the `SessionEnd` hook.
33
+ - The question MUST be a clean yes/no — do NOT offer "close some / keep some" partial answers, do NOT propose alternatives like "close only codex panes". The whole-set decision keeps the wrap-up predictable.
34
+ - This step is mandatory for every phase (`requirements-discovery`, `error-analysis`, `implementation-planning`, `implementation`, `final-verification`, `release-handoff`). It is silent-skipped when `$TMUX_PANE` is unset (lead running outside tmux); the lead MUST NOT fabricate a synthetic pane list in that case.
23
35
  - Clarification request policy (shared — applies whenever a profile uses `## 5. Clarification Requests for the Next Run`):
24
36
  - section 5 MUST be split into two distinct sub-sections per `final-report-template.md` — `5.1 추가 자료 요청 (Additional Materials Requested)` for files/logs/screenshots/links the user must attach, and `5.2 사용자 확인 질문 (Questions for the User)` for decisions or facts only the user can confirm. Never mix material requests and decision questions in the same row or list.
25
37
  - write every entry in full, descriptive sentences that a non-developer can act on without further context. Avoid abbreviations and internal jargon. For each material request, state *why* it is needed, *where* the user can find it, and *where* to place it. For each question, state *why* the answer changes the next step, *what* is being asked in a complete sentence, and *what shape of answer* is expected (예/아니오, 보기 중 하나, 숫자/날짜, 짧은 서술 등); supply concrete option choices when applicable.
@@ -28,17 +28,28 @@
28
28
  - `main`
29
29
  - `직접 입력` (free-form branch name; lead validates the name exists on origin via `git ls-remote --heads origin <name>` and re-asks on failure)
30
30
  The chosen base MUST NOT equal the feature branch. If it does, re-ask.
31
+ 2b. **Pre-merge conflict probe** (only when the user picked `push + PR`) — before the push/PR step, the lead MUST refresh the base ref and probe for merge conflicts against it:
32
+ - run `git fetch origin <chosen-base>` (read-only on the local working tree).
33
+ - run `git merge-tree --write-tree --merge-base origin/<chosen-base> HEAD origin/<chosen-base>`. A non-zero exit code OR conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) in the output indicate a merge conflict against the chosen base.
34
+ - If no conflict is detected, proceed silently to Q3 (do NOT add a confirmation prompt — keep the happy path frictionless).
35
+ - If a conflict IS detected, present the conflicting paths (parsed from the `merge-tree` output) and capture exactly one:
36
+ - `proceed anyway` — continue to Q3; the PR will be opened with conflicts and the final report MUST flag this in `Merge Conflict Probe`.
37
+ - `change base branch` — return to Q2 and re-ask the base selection.
38
+ - `cancel` — end the run without push or PR; record the cancellation in the final report.
39
+ - The probe is read-only. It MUST NOT run `git merge`, `git rebase`, `git pull`, or any command that mutates the working tree or local refs.
31
40
  3. **PR title + PR body confirmation** — show the lead's inline draft verbatim and capture one of:
32
41
  - `use as-is` — proceed with the drafted text.
33
42
  - `edit then proceed` — accept inline edits from the user, then proceed with the edited text.
34
43
  - `cancel` — end the run without executing push or PR commands; record the cancellation in the final report.
35
44
  - Inline drafting rules (Claude lead):
36
45
  - read the run brief, the cited final-verification report, `git log --oneline <base>..HEAD`, and `git diff <base>..HEAD --stat` to ground the drafted text in actual committed changes.
46
+ - **PR body template** — the run context exposes `PR_TEMPLATE_PATH` (resolved by the prepare step in priority order: per-run override → `<project_root>/.project-docs/okstra/project.json` `prTemplatePath` → `~/.okstra/config.json` `prTemplatePath` → bundled default at `~/.claude/skills/okstra-run/templates/pr-body.template.md`) along with `PR_TEMPLATE_SOURCE` indicating which scope was used. The lead MUST `Read` this file verbatim, strip HTML comments, then fill in the placeholders. Do NOT hard-code a section list — the template is the source of truth for the structure. If the resolved file is missing at draft time, abort the run with a clear error rather than inventing a structure.
37
47
  - produce **two artifacts** before showing them to the user:
38
48
  1. **PR title** — by default the subject of the most recent implementation commit, or a concise Conventional Commits-style summary of the committed range.
39
- 2. **PR body** — markdown with sections `## Summary`, `## Changes`, `## Test plan`, `## Linked issues` (omit a section only if it is genuinely empty).
49
+ 2. **PR body** — markdown filled from `PR_TEMPLATE_PATH`. The user-confirmation step's diff (Q3 `edit then proceed`) is computed against the filled template, not against the raw template file.
40
50
  - Allowed actions during the run (Claude lead only):
41
51
  - read-only inspection: `git status`, `git status --short`, `git diff`, `git log`, `git rev-parse`, `git ls-remote --heads origin <name>`, `gh pr list --head <branch>`, `gh pr view <url>`.
52
+ - merge-conflict probe (only when the user picked `push + PR`): `git fetch origin <chosen-base>` and `git merge-tree --write-tree --merge-base origin/<chosen-base> HEAD origin/<chosen-base>`. Both are non-mutating with respect to the working tree.
42
53
  - feature-branch push (only when the user picked `push + PR`): `git push -u origin <current-branch>`. The pushed ref MUST be the feature branch — never the chosen base branch.
43
54
  - PR creation (only when the user picked `push + PR` AND no PR with the same head already exists on origin): `gh pr create --base <chosen-base> --head <current-branch> --title "<title>" --body "<body>"`. The title and body are the user-confirmed PR draft.
44
55
  - PR reuse: if `gh pr list --head <branch> --state open --json url --jq '.[0].url'` returns a URL, treat that PR as already existing — record the URL in the final report and SKIP `gh pr create`.
@@ -64,9 +75,14 @@
64
75
  - **User Selections**: a block recording each prompt and the user's verbatim answer.
65
76
  - Q1 action: `local only` | `push + PR` | `skip`.
66
77
  - Q2 PR base (if applicable): the chosen branch and how it was selected (menu pick vs free-form input).
78
+ - Q2b merge-conflict probe (if applicable): `clean` (no conflict, no prompt shown) | `proceed anyway` | `change base branch` | `cancel`. When a conflict was detected, list the conflicting paths.
67
79
  - Q3 title/body: `use as-is` | `edit then proceed` (with a diff between the lead's draft and the final text) | `cancel`.
68
80
  - **Executed Commands**: every git / gh command the lead actually ran, with its exit code and a one-line stdout/stderr summary. Read-only inspection commands MAY be summarised; mutating commands MUST be listed verbatim.
69
81
  - **Commit List**: each existing implementation commit in `git log <base>..HEAD`, with short/full SHA, subject line, and touched files. Release-handoff MUST NOT create new commits.
82
+ - **Merge Conflict Probe**: one of
83
+ - `- Not run (user picked local only or skip).`
84
+ - `- Clean — no conflicts against <base> at <origin/base SHA>.`
85
+ - `- Conflicts detected against <base> at <origin/base SHA>; user chose <proceed anyway | change base branch | cancel>. Conflicting paths: <list>.`
70
86
  - **Pull Request Outcome**: one of
71
87
  - `- No PR action requested.` (user picked `local only` or `skip`)
72
88
  - `- PR created: <url>` with title and base branch
@@ -79,6 +95,7 @@
79
95
  3. **Forbidden-action audit** — scan the run's session transcripts (`git`, `gh` invocations) for every entry in the Forbidden actions list above. Any occurrence means the run has crossed into unsafe territory and MUST be flagged as `contract-violated`.
80
96
  4. **Push-target audit** — for every `git push` recorded, confirm the refspec resolves to the feature branch, not the base branch.
81
97
  5. **Idempotency check** — if a PR with the same head already existed at run start, confirm the report records `PR reused` rather than a fresh `gh pr create` invocation.
98
+ 6. **Merge-conflict probe audit** — for any `push + PR` run, confirm the report's `Merge Conflict Probe` section is present and either records `Clean` or records `Conflicts detected` with the user's verbatim choice. A missing or unparseable probe entry on a `push + PR` run is a contract violation.
82
99
  - Non-goals:
83
100
  - re-litigating the final-verification verdict — release-handoff trusts the cited `accepted` verdict and does not reopen acceptance checks.
84
101
  - creating, amending, squashing, or rewriting commits. Commit production belongs to `implementation`.
@@ -57,6 +57,7 @@ def record_start(home: Path, *, project_id: str, project_root: str,
57
57
  run_dir_rel: str, final_report_rel: str,
58
58
  argv: list, cwd: str,
59
59
  env_overrides: dict,
60
+ okstra_version: str = "",
60
61
  brief_sha256: str = "",
61
62
  initial_status: str = "running",
62
63
  final_status_rel: str = "") -> str:
@@ -122,7 +123,7 @@ def record_start(home: Path, *, project_id: str, project_root: str,
122
123
  _replace_or_append_project_row(home, project_id, run_id, row)
123
124
  save_invocation(home, project_id, task_group, task_id, task_type, run_seq, {
124
125
  "runId": run_id,
125
- "okstraVersion": os.environ.get("OKSTRA_SCRIPT_VERSION", ""),
126
+ "okstraVersion": okstra_version or os.environ.get("OKSTRA_SCRIPT_VERSION", ""),
126
127
  "invokedAt": when,
127
128
  "cwd": cwd,
128
129
  "argv": argv,
@@ -0,0 +1,126 @@
1
+ """PR body 템플릿 경로 해석.
2
+
3
+ release-handoff 단계에서 lead 가 PR 본문을 작성할 때 사용하는 마크다운
4
+ 템플릿의 경로를 결정한다. 우선순위:
5
+
6
+ 1. per-run override (okstra-run Step 6 에서 입력)
7
+ 2. project: <project_root>/.project-docs/okstra/project.json 의 ``prTemplatePath``
8
+ 3. global: ~/.okstra/config.json 의 ``prTemplatePath``
9
+ 4. default: 스킬 설치 디렉터리의 ``okstra-run/templates/pr-body.template.md``
10
+
11
+ 경로는 절대경로 또는 ``~`` 시작 경로를 권장한다. 상대경로일 경우 project
12
+ 스코프는 ``project_root`` 기준, override 는 호출자 cwd 기준으로 해석한다.
13
+ global 스코프에서 상대경로는 모호하므로 거절한다.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+
22
+ _DEFAULT_FILENAME = "pr-body.template.md"
23
+
24
+
25
+ class PrTemplateError(Exception):
26
+ """invalid PR template configuration — surface to user."""
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ResolvedPrTemplate:
31
+ path: Path
32
+ source: str # "override" | "project" | "global" | "default"
33
+
34
+
35
+ def _okstra_home() -> Path:
36
+ override = os.environ.get("OKSTRA_HOME", "").strip()
37
+ return Path(override) if override else Path.home() / ".okstra"
38
+
39
+
40
+ def _default_template_candidates() -> list[Path]:
41
+ """디폴트 템플릿 후보 경로들 (우선순위 순)."""
42
+ out: list[Path] = []
43
+ env_dir = os.environ.get("OKSTRA_SKILLS_DIR", "").strip()
44
+ if env_dir:
45
+ out.append(Path(env_dir) / "okstra-run" / "templates" / _DEFAULT_FILENAME)
46
+ out.append(
47
+ Path.home() / ".claude" / "skills" / "okstra-run" / "templates" / _DEFAULT_FILENAME
48
+ )
49
+ return out
50
+
51
+
52
+ def _read_json_field(path: Path, field: str) -> str:
53
+ if not path.is_file():
54
+ return ""
55
+ try:
56
+ data = json.loads(path.read_text(encoding="utf-8"))
57
+ except (OSError, json.JSONDecodeError):
58
+ return ""
59
+ if not isinstance(data, dict):
60
+ return ""
61
+ return str(data.get(field) or "").strip()
62
+
63
+
64
+ def _resolve_project_relative(value: str, project_root: Path) -> Path:
65
+ p = Path(value).expanduser()
66
+ return p if p.is_absolute() else (Path(project_root) / p).resolve()
67
+
68
+
69
+ def resolve_pr_template_path(
70
+ project_root: Path,
71
+ override_path: str = "",
72
+ ) -> ResolvedPrTemplate:
73
+ """우선순위에 따라 PR 템플릿 경로를 해석한다.
74
+
75
+ Raises:
76
+ PrTemplateError: 설정된 경로가 존재하지 않거나, 디폴트조차 찾지 못한 경우.
77
+ """
78
+ project_root = Path(project_root)
79
+
80
+ # 1) per-run override
81
+ ov = (override_path or "").strip()
82
+ if ov:
83
+ p = _resolve_project_relative(ov, project_root)
84
+ if not p.is_file():
85
+ raise PrTemplateError(f"override PR template not found: {p}")
86
+ return ResolvedPrTemplate(path=p, source="override")
87
+
88
+ # 2) project.json
89
+ pj_path = Path(project_root) / ".project-docs" / "okstra" / "project.json"
90
+ pj_val = _read_json_field(pj_path, "prTemplatePath")
91
+ if pj_val:
92
+ p = _resolve_project_relative(pj_val, project_root)
93
+ if not p.is_file():
94
+ raise PrTemplateError(
95
+ f"project.json prTemplatePath points to missing file: {p} "
96
+ f"(configured in {pj_path})"
97
+ )
98
+ return ResolvedPrTemplate(path=p, source="project")
99
+
100
+ # 3) global config
101
+ gc_path = _okstra_home() / "config.json"
102
+ gv = _read_json_field(gc_path, "prTemplatePath")
103
+ if gv:
104
+ p = Path(gv).expanduser()
105
+ if not p.is_absolute():
106
+ raise PrTemplateError(
107
+ f"global config prTemplatePath must be absolute or start with '~/': got {gv!r} "
108
+ f"(configured in {gc_path})"
109
+ )
110
+ if not p.is_file():
111
+ raise PrTemplateError(
112
+ f"global prTemplatePath missing: {p} (configured in {gc_path})"
113
+ )
114
+ return ResolvedPrTemplate(path=p, source="global")
115
+
116
+ # 4) default
117
+ for cand in _default_template_candidates():
118
+ if cand.is_file():
119
+ return ResolvedPrTemplate(path=cand, source="default")
120
+
121
+ raise PrTemplateError(
122
+ "no PR template available: default skill template not found. "
123
+ f"Searched: {', '.join(str(c) for c in _default_template_candidates())}. "
124
+ "Reinstall okstra (`npx okstra install`) or set prTemplatePath in "
125
+ "project.json / ~/.okstra/config.json."
126
+ )
@@ -96,6 +96,16 @@ def _doc_type_from_template_path(template_path: str) -> str:
96
96
  return stem
97
97
 
98
98
 
99
+ def _frontmatter_id_from_task_key(task_key: str) -> str:
100
+ """task_key (`project_id:task_group:task_id`) 를 ID 형식으로 변환.
101
+
102
+ `:` 를 `-` 로 치환한 단일 문자열. 예시:
103
+ ``fontsninja-classifier-v2:DEV-9388:DEV-9429``
104
+ -> ``fontsninja-classifier-v2-DEV-9388-DEV-9429``
105
+ """
106
+ return (task_key or "").strip().replace(":", "-")
107
+
108
+
99
109
  def _frontmatter_mapping(ctx: dict) -> dict:
100
110
  task_id = (ctx.get("TASK_ID") or "").strip()
101
111
  project_id = (ctx.get("PROJECT_ID") or "").strip()
@@ -103,15 +113,28 @@ def _frontmatter_mapping(ctx: dict) -> dict:
103
113
  task_key = (ctx.get("TASK_KEY") or "").strip()
104
114
  task_date = (ctx.get("TASK_DATE") or "").strip()
105
115
  doc_type = (ctx.get("DOC_TYPE") or "").strip()
116
+ # task_type 은 ctx 키가 두 곳에 분포 — 직접 키(`TASK_TYPE`), 또는
117
+ # `ANALYSIS_TYPE` fallback (workflow 의 render mapping 과 동일 우선순위).
118
+ task_type = (ctx.get("TASK_TYPE") or ctx.get("ANALYSIS_TYPE") or "").strip()
119
+
120
+ fm_id = _frontmatter_id_from_task_key(task_key)
121
+ fm_id_scalar = f'"{fm_id}"' if fm_id else f'"{_FM_DEFAULT}"'
122
+ alias_value = f"{fm_id}-{task_type}" if (fm_id and task_type) else fm_id
106
123
  return {
107
124
  "{{TASK_KEY}}": _fm_scalar(task_key),
108
125
  "{{TASK_ID}}": _fm_scalar(task_id),
109
126
  "{{PROJECT_ID}}": _fm_scalar(project_id),
110
127
  "{{TASK_GROUP}}": _fm_scalar(task_group),
111
128
  "{{TASK_DATE}}": _fm_scalar(task_date),
112
- "{{FM_ID}}": _fm_array([task_id, project_id, task_group]),
113
- "{{FM_ALIASES}}": _fm_array([task_id, project_id, task_group]),
129
+ # task_key `:` 를 `-` 로 치환한 단일 스칼라.
130
+ # 예: "fontsninja-classifier-v2-DEV-9388-DEV-9429"
131
+ "{{FM_ID}}": fm_id_scalar,
132
+ # id 와 task-type 을 `-` 로 연결한 단일 alias 를 array 한 칸에 담는다
133
+ # (Obsidian aliases 컨벤션).
134
+ "{{FM_ALIASES}}": _fm_array([alias_value]) if alias_value else "[]",
114
135
  "{{FM_TAGS}}": _fm_tags(doc_type),
136
+ # 신규: 모든 okstra 산출물의 frontmatter 가 task type 을 명시한다.
137
+ "{{FM_TASK_TYPE}}": _fm_scalar(task_type),
115
138
  }
116
139
 
117
140
 
@@ -694,6 +717,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
694
717
  workflow = task_manifest.get("workflow", {}) if isinstance(task_manifest.get("workflow"), dict) else {}
695
718
  payload = {
696
719
  "schemaVersion": "1.0",
720
+ "okstraVersion": ctx.get("OKSTRA_VERSION", ""),
697
721
  "projectId": ctx.get("PROJECT_ID", ""),
698
722
  "taskGroup": ctx.get("TASK_GROUP", ""),
699
723
  "taskId": ctx.get("TASK_ID", ""),
@@ -907,6 +931,7 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
907
931
  "{{RELATED_TASKS_INLINE}}": ctx.get("RELATED_TASKS_INLINE", "None"),
908
932
  "{{RECOMMENDED_ANALYSERS}}": ", ".join(task_manifest.get("recommendedWorkers", [])),
909
933
  "{{LEAD_MODEL}}": rc.get("leadModel", ctx.get("LEAD_MODEL_DISPLAY", "")),
934
+ "{{OKSTRA_VERSION}}": ctx.get("OKSTRA_VERSION", ""),
910
935
  "{{LATEST_RUN_RELATIVE_PATH}}": task_manifest.get("latestRunPath", ctx.get("LATEST_RUN_RELATIVE_PATH", "")),
911
936
  "{{LATEST_REPORT_RELATIVE_PATH}}": task_manifest.get("latestReportPath", ctx.get("LATEST_REPORT_RELATIVE_PATH", "")),
912
937
  "{{TEAM_STATE_RELATIVE_PATH}}": task_manifest.get("teamStatePath", ctx.get("TEAM_STATE_RELATIVE_PATH", "")),
@@ -1161,6 +1186,7 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1161
1186
  "{{REPORT_WRITER_WORKER_ERRORS_SIDECAR_RELATIVE_PATH}}": ctx.get("REPORT_WRITER_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
1162
1187
  "{{LEAD_MODEL}}": lead_model,
1163
1188
  "{{LEAD_MODEL_EXECUTION_VALUE}}": lead_model_execution,
1189
+ "{{OKSTRA_VERSION}}": ctx.get("OKSTRA_VERSION", ""),
1164
1190
  "{{CLAUDE_WORKER_MODEL}}": ctx.get("CLAUDE_WORKER_MODEL_DISPLAY", ""),
1165
1191
  "{{CLAUDE_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get("CLAUDE_WORKER_MODEL_EXECUTION_VALUE", ""),
1166
1192
  "{{CODEX_WORKER_MODEL}}": ctx.get("CODEX_WORKER_MODEL_DISPLAY", ""),