okstra 0.21.0 → 0.22.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
@@ -28,7 +28,7 @@ okstra/ npm 패키지 = repo 루트
28
28
  ├── tools/build.mjs runtime/ 동기화 스크립트 (prepack 에서 호출)
29
29
  ├── runtime/ gitignored 빌드 산출물; ~/.okstra 로 배포되는 유일한 자산
30
30
  ├── scripts/ python + bash 런타임 소스
31
- ├── skills/ Claude Code 스킬 마크다운 소스 (스킬 12종)
31
+ ├── skills/ Claude Code 스킬 마크다운 소스 (스킬 13종)
32
32
  ├── agents/ lead SKILL.md + workers/
33
33
  ├── prompts/, templates/, validators/
34
34
  ├── docs/kr/ 한국어 상세 매뉴얼 (architecture.md, cli.md)
@@ -87,7 +87,7 @@ okstra/ npm 패키지 = repo 루트
87
87
  npx -y okstra@latest install
88
88
  ```
89
89
 
90
- `~/.okstra/{lib/python, bin, version}`, `~/.claude/skills/` 아래 스킬 마크다운 12개, `~/.okstra/installed-skills.json` 을 생성합니다. 재실행은 idempotent — 파일별 hash 를 비교하고 바뀐 파일만 갱신합니다.
90
+ `~/.okstra/{lib/python, bin, version}`, `~/.claude/skills/` 아래 스킬 마크다운 13개, `~/.okstra/installed-skills.json` 을 생성합니다. 재실행은 idempotent — 파일별 hash 를 비교하고 바뀐 파일만 갱신합니다.
91
91
 
92
92
  검증:
93
93
 
package/README.md CHANGED
@@ -28,7 +28,7 @@ okstra/ npm package = repo root
28
28
  ├── tools/build.mjs runtime/ sync script (invoked by prepack)
29
29
  ├── runtime/ gitignored build output; the only thing shipped to ~/.okstra
30
30
  ├── scripts/ python + bash runtime sources
31
- ├── skills/ Claude Code skill markdown sources (12 skills)
31
+ ├── skills/ Claude Code skill markdown sources (13 skills)
32
32
  ├── agents/ lead SKILL.md + workers/
33
33
  ├── prompts/, templates/, validators/
34
34
  ├── tests/, tests-e2e/
@@ -86,7 +86,7 @@ okstra/ npm package = repo root
86
86
  npx -y okstra@latest install
87
87
  ```
88
88
 
89
- Provisions `~/.okstra/{lib/python, bin, version}`, the 12 skill markdown files under `~/.claude/skills/`, and `~/.okstra/installed-skills.json`. Re-running is idempotent — per-file hashes are compared and only changed files are touched.
89
+ Provisions `~/.okstra/{lib/python, bin, version}`, the 13 skill markdown files under `~/.claude/skills/`, and `~/.okstra/installed-skills.json`. Re-running is idempotent — per-file hashes are compared and only changed files are touched.
90
90
 
91
91
  Verify:
92
92
 
@@ -837,6 +837,8 @@ Claude가 작성하는 최종 보고서는 brief에 더 구체적인 형식이
837
837
 
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
+ - **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>` 만 출력하므로 사용자가 무엇이 닫힐지 시각적으로 확인할 수 있습니다.
840
842
  - 디스크 누적은 `okstra-logs` skill 이 read-only 로 인벤토리 + cleanup 명령을 제안합니다 (실행은 사용자 copy-paste).
841
843
 
842
844
  ### Linked-worktree `.git/` write 권한 (codex / gemini)
package/docs/kr/cli.md CHANGED
@@ -497,5 +497,5 @@ chmod +x ~/.local/bin/okstra-ctl
497
497
 
498
498
  ### Live-log sidecar
499
499
 
500
- codex / gemini wrapper 는 매 dispatch 마다 `runs/<task-type>/prompts/<worker>-prompt-<phase>-<seq>.log` sidecar 를 만들고 stdout / stderr 를 mirror 합니다. tmux 안에서 lead 를 띄우면 wrapper 가 자동으로 `tail -F` pane 을 분할합니다 (title: `<cli>-<role>-trace`). 사용량 인벤토리와 `find … -delete` cleanup 명령은 `okstra-logs` skill 이 read-only 로 제안합니다. 자세한 와이어링은 [`docs/kr/architecture.md`](architecture.md) 의 *Live-log mirror* 절 참고.
500
+ codex / gemini wrapper 는 매 dispatch 마다 `runs/<task-type>/prompts/<worker>-prompt-<phase>-<seq>.log` sidecar 를 만들고 stdout / stderr 를 mirror 합니다. tmux 안에서 lead 를 띄우면 wrapper 가 자동으로 `tail -F` pane 을 분할합니다 (title: `<cli>-<role>-trace`). 분할된 trace pane 은 caller `$TMUX_PANE` 으로 키된 registry 에 등록돼, Claude `/exit` 시 `SessionEnd` 훅이 `okstra-trace-cleanup.sh` 로 자동 정리합니다. 사용량 인벤토리와 `find … -delete` cleanup 명령은 `okstra-logs` skill 이 read-only 로 제안합니다. 자세한 와이어링은 [`docs/kr/architecture.md`](architecture.md) 의 *Live-log mirror* 절 참고.
501
501
 
@@ -165,6 +165,7 @@ okstra/
165
165
  | `okstra-central.sh` | `record_start` 중앙 lock 관리 |
166
166
  | `okstra-codex-exec.sh` | Codex worker executor (live log mirror, tmux trace pane) |
167
167
  | `okstra-gemini-exec.sh` | Gemini worker executor |
168
+ | `okstra-trace-cleanup.sh` | Claude `/exit` 시 trace pane registry 청소 (`SessionEnd` 훅에서 호출) |
168
169
  | `okstra-error-log.py` | Worker 오류 패턴 분석 |
169
170
  | `okstra-spawn-followups.py` | Phase 완료 후 다음 phase dispatch |
170
171
  | `okstra-token-usage.py` | Token collection CLI 엔트리 |
@@ -217,7 +218,7 @@ okstra/
217
218
 
218
219
  ---
219
220
 
220
- ### 3.7 `skills/` — 12개 Claude Code 슬래시 커맨드
221
+ ### 3.7 `skills/` — 13개 Claude Code 슬래시 커맨드
221
222
 
222
223
  | ID | 이름 | 공개 | 용도 |
223
224
  |----|------|------|------|
@@ -233,6 +234,7 @@ okstra/
233
234
  | 10 | okstra-report-finder | NO | 보고서 검색 (자동 트리거) |
234
235
  | 11 | okstra-time-summary | NO | 소요 시간 분석 |
235
236
  | 12 | okstra-logs | YES | 로그 인벤토리·정리 (worker wrapper `*.log` sidecars 조회 및 cleanup 제안) |
237
+ | 13 | okstra-brief | YES | 요구사항 문서·티켓·링크·대화로부터 `okstra-run` 입력용 task brief 마크다운 생성 |
236
238
 
237
239
  각 skill 구조: **Step 0 런타임 검증 → Step 1-N 작업 수행 → 실패 시 사용자 안내**.
238
240
 
@@ -383,4 +385,57 @@ okstra/
383
385
 
384
386
  ---
385
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
+
386
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.0",
3
+ "version": "0.22.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.0",
3
- "builtAt": "2026-05-14T11:48:06.992Z",
2
+ "package": "0.22.0",
3
+ "builtAt": "2026-05-14T15:54:01.726Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -44,7 +44,9 @@ Unlike the Codex / Gemini workers, you are an in-process Claude subagent — you
44
44
  - If the parent directory does not exist yet, create it before writing.
45
45
 
46
46
  4. Anchor all file operations to the absolute `Project Root` from the lead prompt. Use absolute paths — do NOT rely on inherited cwd. Never use `cd` to change directory.
47
- - **Executor exception (implementation phase only):** when this worker is dispatched as the `Executor` and the lead prompt provides an `EXECUTOR_WORKTREE_PATH` that differs from the session's inherited cwd, cwd-sensitive Bash commands (`cargo *`, `npm *`, `pnpm *`, `bun *`, `pytest`, `make *`, `go *`, language-toolchain test/build commands) MUST be prefixed with `cd <EXECUTOR_WORKTREE_PATH> && ` in the same Bash invocation — e.g. `cd /Users/.../worktrees/foo && cargo test -p bar`. Do NOT wrap the whole thing in `bash -lc "..."` or `bash -c "..."`; pass the chained command directly to the Bash tool so the leading `cd` token remains visible to the permission layer. The `cd` is scoped to the single Bash subshell and does not mutate the session's shell state, so this does not conflict with the "never use cd" rule above (which prevents the worker from drifting the session cwd across calls). Verifier roles do NOT use this exception — they read with absolute paths only.
47
+ - **Executor exception (implementation phase only):** when this worker is dispatched as the `Executor` and the lead prompt provides an `EXECUTOR_WORKTREE_PATH` that differs from the session's inherited cwd, cwd-sensitive Bash commands (`cargo *`, `npm *`, `pnpm *`, `bun *`, `pytest`, `make *`, `go *`, language-toolchain test/build commands) MUST be prefixed with `cd <EXECUTOR_WORKTREE_PATH> && ` in the same Bash invocation — e.g. `cd /Users/.../worktrees/foo && cargo test -p bar`. Do NOT wrap the whole thing in `bash -lc "..."` or `bash -c "..."`; pass the chained command directly to the Bash tool so the leading `cd` token remains visible to the permission layer. The `cd` is scoped to the single Bash subshell and does not mutate the session's shell state, so this does not conflict with the "never use cd" rule above (which prevents the worker from drifting the session cwd across calls).
48
+ - **Verifier QA-gate exception:** verifier roles MAY use the same `cd <WORKTREE> && <cmd>` shape when executing project-declared `qaCommands` (lint / format / typecheck / test) from `project.json`, since those commands are cwd-sensitive by nature. Outside the QA gate, verifiers still read with absolute paths only — do NOT use `cd` for file inspection.
49
+ - **No extra chaining beyond `cd && cmd`:** the permission matcher only allows the exact two-segment shape `cd <PATH> && <single-command>`. Do NOT append additional pipes, semicolons, redirects, or `&&` chains — e.g. `cd ... && cargo test ... 2>&1 | tail -20; echo "exit:$?"` will trigger a permission prompt every dispatch because the trailing `| tail`, `; echo`, and `2>&1` tokens disqualify the prefix match against `Bash(cargo:*)`. Let Claude Code capture the full stdout/stderr and exit code natively — do not post-process with `tail`, `head`, or `echo "exit:$?"`. If output truncation is genuinely needed, run the command first and read the result in a separate tool call.
48
50
 
49
51
  5. **MCP usage**: The canonical list of MCP servers and tools available for this run lives in the lead prompt's `## Available MCP Servers` section (sourced from `.project-docs/okstra/project.json`'s `mcpServers` array). When the task requires inspection of an external system covered by one of those servers, call the listed tool directly by name (e.g. `mcp__<server>__<tool>`). Do NOT shell out via `claude --mcp-cli call ...` or run the tool name as a Bash command — those are not valid invocation paths. If a server you need is not listed, record `MCP not available for this run` in your worker output rather than guessing a tool name.
50
52
 
@@ -172,6 +172,17 @@ if [[ -n "${TMUX:-}" ]]; then
172
172
  if [[ -n "$trace_pane" ]]; then
173
173
  tmux select-pane -t "$trace_pane" -T "codex-${role}-trace" 2>/dev/null || true
174
174
  tmux last-pane 2>/dev/null || true
175
+ # Register the spawned pane so the `SessionEnd` hook (see
176
+ # `okstra-trace-cleanup.sh`) can kill it when the caller's Claude
177
+ # session exits. Scope by caller `$TMUX_PANE` — the pane Claude itself
178
+ # is attached to — so concurrent Claude instances in the same tmux
179
+ # session do not stomp each other's trace panes.
180
+ if [[ -n "${TMUX_PANE:-}" ]]; then
181
+ registry_dir="${TMPDIR:-/tmp}/okstra-trace-panes"
182
+ mkdir -p "$registry_dir" 2>/dev/null || true
183
+ safe_pane="${TMUX_PANE//[^A-Za-z0-9]/_}"
184
+ printf '%s\n' "$trace_pane" >> "$registry_dir/${safe_pane}.list" 2>/dev/null || true
185
+ fi
175
186
  fi
176
187
  fi
177
188
 
@@ -121,6 +121,13 @@ if [[ -n "${TMUX:-}" ]]; then
121
121
  if [[ -n "$trace_pane" ]]; then
122
122
  tmux select-pane -t "$trace_pane" -T "gemini-${role}-trace" 2>/dev/null || true
123
123
  tmux last-pane 2>/dev/null || true
124
+ # See `okstra-codex-exec.sh` for the registry rationale — kept in lock-step.
125
+ if [[ -n "${TMUX_PANE:-}" ]]; then
126
+ registry_dir="${TMPDIR:-/tmp}/okstra-trace-panes"
127
+ mkdir -p "$registry_dir" 2>/dev/null || true
128
+ safe_pane="${TMUX_PANE//[^A-Za-z0-9]/_}"
129
+ printf '%s\n' "$trace_pane" >> "$registry_dir/${safe_pane}.list" 2>/dev/null || true
130
+ fi
124
131
  fi
125
132
  fi
126
133
 
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # okstra-trace-cleanup.sh — manage tmux trace panes spawned by okstra worker
4
+ # wrappers (`okstra-codex-exec.sh`, `okstra-gemini-exec.sh`) for the current
5
+ # Claude Code session.
6
+ #
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.
16
+ #
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.
20
+ #
21
+ # Failures are tolerated silently — a stale pane id, missing $TMUX, or a
22
+ # locked tmux client must never prevent Claude from exiting cleanly.
23
+
24
+ set -u
25
+
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.
46
+ if [[ -z "${TMUX_PANE:-}" ]]; then
47
+ exit 0
48
+ fi
49
+
50
+ registry_dir="${TMPDIR:-/tmp}/okstra-trace-panes"
51
+ safe_pane="${TMUX_PANE//[^A-Za-z0-9]/_}"
52
+ registry_file="$registry_dir/${safe_pane}.list"
53
+
54
+ if [[ ! -f "$registry_file" ]]; then
55
+ exit 0
56
+ fi
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
+
69
+ while IFS= read -r pane_id; do
70
+ [[ -n "$pane_id" ]] || continue
71
+ tmux kill-pane -t "$pane_id" 2>/dev/null || true
72
+ done < "$registry_file"
73
+
74
+ rm -f "$registry_file" 2>/dev/null || true
75
+ exit 0
@@ -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.
@@ -34,9 +34,10 @@
34
34
  - `cancel` — end the run without executing push or PR commands; record the cancellation in the final report.
35
35
  - Inline drafting rules (Claude lead):
36
36
  - 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.
37
+ - **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
38
  - produce **two artifacts** before showing them to the user:
38
39
  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).
40
+ 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
41
  - Allowed actions during the run (Claude lead only):
41
42
  - 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>`.
42
43
  - 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.
@@ -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
+ )
@@ -694,6 +694,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
694
694
  workflow = task_manifest.get("workflow", {}) if isinstance(task_manifest.get("workflow"), dict) else {}
695
695
  payload = {
696
696
  "schemaVersion": "1.0",
697
+ "okstraVersion": ctx.get("OKSTRA_VERSION", ""),
697
698
  "projectId": ctx.get("PROJECT_ID", ""),
698
699
  "taskGroup": ctx.get("TASK_GROUP", ""),
699
700
  "taskId": ctx.get("TASK_ID", ""),
@@ -907,6 +908,7 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
907
908
  "{{RELATED_TASKS_INLINE}}": ctx.get("RELATED_TASKS_INLINE", "None"),
908
909
  "{{RECOMMENDED_ANALYSERS}}": ", ".join(task_manifest.get("recommendedWorkers", [])),
909
910
  "{{LEAD_MODEL}}": rc.get("leadModel", ctx.get("LEAD_MODEL_DISPLAY", "")),
911
+ "{{OKSTRA_VERSION}}": ctx.get("OKSTRA_VERSION", ""),
910
912
  "{{LATEST_RUN_RELATIVE_PATH}}": task_manifest.get("latestRunPath", ctx.get("LATEST_RUN_RELATIVE_PATH", "")),
911
913
  "{{LATEST_REPORT_RELATIVE_PATH}}": task_manifest.get("latestReportPath", ctx.get("LATEST_REPORT_RELATIVE_PATH", "")),
912
914
  "{{TEAM_STATE_RELATIVE_PATH}}": task_manifest.get("teamStatePath", ctx.get("TEAM_STATE_RELATIVE_PATH", "")),
@@ -1161,6 +1163,7 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1161
1163
  "{{REPORT_WRITER_WORKER_ERRORS_SIDECAR_RELATIVE_PATH}}": ctx.get("REPORT_WRITER_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
1162
1164
  "{{LEAD_MODEL}}": lead_model,
1163
1165
  "{{LEAD_MODEL_EXECUTION_VALUE}}": lead_model_execution,
1166
+ "{{OKSTRA_VERSION}}": ctx.get("OKSTRA_VERSION", ""),
1164
1167
  "{{CLAUDE_WORKER_MODEL}}": ctx.get("CLAUDE_WORKER_MODEL_DISPLAY", ""),
1165
1168
  "{{CLAUDE_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get("CLAUDE_WORKER_MODEL_EXECUTION_VALUE", ""),
1166
1169
  "{{CODEX_WORKER_MODEL}}": ctx.get("CODEX_WORKER_MODEL_DISPLAY", ""),
@@ -49,6 +49,7 @@ from .seeding import (
49
49
  SettingsLinkError,
50
50
  cleanup_obsolete_generated_docs,
51
51
  ensure_project_settings_symlink,
52
+ installed_version,
52
53
  verify_installation,
53
54
  )
54
55
  from .session import (
@@ -56,7 +57,12 @@ from .session import (
56
57
  resolve_inproc_lead_session_id,
57
58
  write_claude_resume_command_file,
58
59
  )
59
- from .workers import normalize_workers, resolve_profile_workers
60
+ from .pr_template import PrTemplateError, resolve_pr_template_path
61
+ from .workers import (
62
+ normalize_workers,
63
+ resolve_profile_workers,
64
+ validate_workers_against_profile,
65
+ )
60
66
  from .workflow import compute_workflow_state
61
67
  from .worktree import provision_task_worktree
62
68
 
@@ -102,6 +108,9 @@ class PrepareInputs:
102
108
  base_ref: str = ""
103
109
  approved_plan_path: str = ""
104
110
  clarification_response_path: str = "" # absolute or empty
111
+ # release-handoff 전용: PR 본문 템플릿 1회성 override. 빈 문자열이면
112
+ # project.json → global config → 스킬 디폴트 순으로 해석된다.
113
+ pr_template_path: str = ""
105
114
  render_only: bool = False
106
115
  refresh_assets: bool = False
107
116
  approve_plan_ack: bool = False
@@ -310,6 +319,7 @@ def _record_start(
310
319
  argv=canonical_argv,
311
320
  cwd=cwd,
312
321
  env_overrides={},
322
+ okstra_version=ctx.get("OKSTRA_VERSION", ""),
313
323
  initial_status=initial_status,
314
324
  brief_sha256=brief_sha256,
315
325
  )
@@ -495,12 +505,28 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
495
505
  if inp.task_type == "release-handoff":
496
506
  workers: list[str] = []
497
507
  else:
498
- profile_workers_csv = ",".join(resolve_profile_workers(profile_file))
508
+ profile_workers = resolve_profile_workers(profile_file)
509
+ profile_workers_csv = ",".join(profile_workers)
499
510
  workers = normalize_workers(inp.workers_override or profile_workers_csv)
511
+ if inp.workers_override.strip():
512
+ validate_workers_against_profile(workers, profile_workers)
500
513
  if not workers:
501
514
  raise PrepareError(f"no workers resolved for profile: {inp.task_type}")
502
515
  selected_reviewers = ",".join(workers)
503
516
 
517
+ # ---- PR template resolution (release-handoff only) ----
518
+ pr_template_path_str = ""
519
+ pr_template_source = ""
520
+ if inp.task_type == "release-handoff":
521
+ try:
522
+ resolved_tpl = resolve_pr_template_path(
523
+ Path(inp.project_root), inp.pr_template_path
524
+ )
525
+ except PrTemplateError as exc:
526
+ raise PrepareError(f"PR template resolution failed: {exc}") from exc
527
+ pr_template_path_str = str(resolved_tpl.path)
528
+ pr_template_source = resolved_tpl.source
529
+
504
530
  # ---- model assignments ----
505
531
  lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "opus")
506
532
  claude_default = _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "sonnet")
@@ -634,6 +660,8 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
634
660
  ctx.update({
635
661
  "REVIEW_PROFILE": inp.task_type,
636
662
  "SELECTED_REVIEWERS": selected_reviewers,
663
+ "PR_TEMPLATE_PATH": pr_template_path_str,
664
+ "PR_TEMPLATE_SOURCE": pr_template_source,
637
665
  "CLAUDE_SESSION_ID": claude_session_id,
638
666
  "CLARIFICATION_RESPONSE_PATH": inp.clarification_response_path,
639
667
  "CLARIFICATION_RESPONSE_FILE": inp.clarification_response_path,
@@ -666,6 +694,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
666
694
  "LATEST_REPORT_PATH": "",
667
695
  "LATEST_REPORT_RELATIVE_PATH": "",
668
696
  "RENDER_ONLY": "true" if inp.render_only else "false",
697
+ "OKSTRA_VERSION": installed_version(),
669
698
  **workflow_state,
670
699
  })
671
700
 
@@ -853,6 +882,15 @@ def main(argv: list[str]) -> int:
853
882
  ),
854
883
  )
855
884
  p.add_argument("--clarification-response", default="", dest="clarification_response_path")
885
+ p.add_argument(
886
+ "--pr-template-path",
887
+ default="",
888
+ dest="pr_template_path",
889
+ help=(
890
+ "release-handoff 전용 1회성 PR 본문 템플릿 경로. 빈 값이면 "
891
+ "project.json → ~/.okstra/config.json → 스킬 디폴트 순으로 해석."
892
+ ),
893
+ )
856
894
  p.add_argument("--render-only", action="store_true", dest="render_only")
857
895
  p.add_argument("--refresh-assets", action="store_true", dest="refresh_assets")
858
896
  p.add_argument(
@@ -917,6 +955,7 @@ def main(argv: list[str]) -> int:
917
955
  base_ref=args.base_ref,
918
956
  approved_plan_path=args.approved_plan_path,
919
957
  clarification_response_path=clarification_abs,
958
+ pr_template_path=args.pr_template_path,
920
959
  render_only=args.render_only,
921
960
  refresh_assets=args.refresh_assets,
922
961
  approve_plan_ack=args.approve_plan_ack,
@@ -23,6 +23,25 @@ class SettingsLinkError(Exception):
23
23
  """`<project>/.claude/settings.local.json` symlink provisioning 실패."""
24
24
 
25
25
 
26
+ def installed_version() -> str:
27
+ """Read the version stamp written by `okstra install` to `~/.okstra/version`.
28
+
29
+ Returns an empty string if the stamp is missing or unreadable. Callers use
30
+ the result to label generated artifacts (run manifests, final reports) so
31
+ that consumers can tell which okstra release produced a given run — and
32
+ so that report readers can distinguish behaviour drift across upgrades
33
+ without having to dig through git history.
34
+
35
+ The stamp lives at `_okstra_home() / "version"`. `OKSTRA_HOME` overrides
36
+ the home directory for tests.
37
+ """
38
+ version_file = _okstra_home() / "version"
39
+ try:
40
+ return version_file.read_text(encoding="utf-8").strip()
41
+ except OSError:
42
+ return ""
43
+
44
+
26
45
  def required_install_paths() -> list[Path]:
27
46
  """okstra install 이 채워야 하는 최소 자산 경로."""
28
47
  okstra_home = Path.home() / ".okstra"
@@ -68,3 +68,23 @@ def normalize_workers(value: str) -> list[str]:
68
68
  seen.add(v)
69
69
  out.append(v)
70
70
  return out
71
+
72
+
73
+ def validate_workers_against_profile(
74
+ workers: list[str], profile_workers: list[str]
75
+ ) -> None:
76
+ """프로파일이 `Required workers:` 로 로스터를 선언했다면, 사용자
77
+ override 가 그 부분집합인지 검증한다.
78
+
79
+ `profile_workers` 가 비어 있으면(프로파일이 로스터를 선언하지 않은
80
+ 구버전) 검증을 건너뛴다 — 하위 호환을 위해.
81
+ """
82
+ if not profile_workers:
83
+ return
84
+ allowed = set(profile_workers)
85
+ extras = [w for w in workers if w not in allowed]
86
+ if extras:
87
+ raise WorkersError(
88
+ "workers not allowed by profile roster: "
89
+ f"{','.join(extras)} (profile allows: {','.join(profile_workers)})"
90
+ )
@@ -208,19 +208,37 @@ Do NOT ask for `workers_override` in implementation — the profile's required r
208
208
 
209
209
  ### 6b. Other phases (`requirements-discovery`, `error-analysis`, `implementation-planning`, `final-verification`, `release-handoff`)
210
210
 
211
- Ask each in turn (model prompts use `AskUserQuestion` with the option lists above; others are free text):
211
+ **Before asking any worker/model question, resolve the profile's allowed roster:**
212
212
 
213
- 1. `AskUserQuestion` `"참여 워커 목록 (쉼표 구분, 빈 칸 = 프로필 기본값 claude,codex,report-writer). 선택지: claude, codex, gemini, report-writer — gemini는 옵션이므로 필요할 때 명시"` (free text) → `workers_override`
213
+ ```python
214
+ from okstra_ctl.workers import resolve_profile_workers
215
+ profile_workers = resolve_profile_workers(Path("<OKSTRA_PROMPTS_PROFILES_DIR>/<task-type>.md"))
216
+ ```
217
+
218
+ This is the **only** set of worker IDs you may show or ask about. Never offer
219
+ workers outside this list. Special cases:
220
+
221
+ - If `profile_workers` is empty (e.g., `release-handoff` is lead-only with no
222
+ `- Required workers:` block), **skip the worker question and all
223
+ worker-model questions entirely** — only ask lead model, directive, related,
224
+ clarification. The backend forces `workers=[]` for these profiles.
225
+ - Otherwise, the worker question must enumerate **only** `profile_workers` —
226
+ do NOT show `claude, codex, gemini, report-writer` blindly.
227
+
228
+ Ask each in turn (model prompts use `AskUserQuestion` with the option lists above; others are free text). Skip any worker-model prompt whose worker is not in `profile_workers`.
229
+
230
+ 1. (only when `profile_workers` is non-empty) `AskUserQuestion` `"참여 워커 목록 (쉼표 구분, 빈 칸 = 프로필 기본값 <profile_workers_csv>). 선택지: <profile_workers_csv>"` (free text) → `workers_override`. Validate the answer is a subset of `profile_workers`; re-ask on failure. (Backend will also reject violations with `WorkersError`.)
214
231
  2. `AskUserQuestion` `"리더(Claude lead) 모델?"` (Claude options) → `lead_model`
215
- 3. `AskUserQuestion` `"claude 워커 모델?"` (Claude options) → `claude_model`
216
- 4. `AskUserQuestion` `"codex 워커 모델?"` (Codex options) → `codex_model`
217
- 5. `AskUserQuestion` `"gemini 워커 모델?"` (Gemini options) → `gemini_model`
218
- 6. `AskUserQuestion` `"리포트 작성자 모델?"` (Claude options) → `report_writer_model`
232
+ 3. (only if `claude` ∈ resolved workers) `AskUserQuestion` `"claude 워커 모델?"` (Claude options) → `claude_model`
233
+ 4. (only if `codex` ∈ resolved workers) `AskUserQuestion` `"codex 워커 모델?"` (Codex options) → `codex_model`
234
+ 5. (only if `gemini` ∈ resolved workers) `AskUserQuestion` `"gemini 워커 모델?"` (Gemini options) → `gemini_model`
235
+ 6. (only if `report-writer` ∈ resolved workers) `AskUserQuestion` `"리포트 작성자 모델?"` (Claude options) → `report_writer_model`
219
236
  7. `AskUserQuestion` `"추가 directive (선택, 빈 칸 가능)"` (free text) → `directive`
220
237
  8. `AskUserQuestion` `"관련 task id 목록, 쉼표 구분 (선택, 빈 칸 가능)"` (free text) → `related_tasks_raw`
221
238
  9. `AskUserQuestion` `"clarification-response 파일 경로 (follow-up 시에만, 빈 칸 가능)"` (free text) → `clarification_response_path`
239
+ 10. (only when `task_type == "release-handoff"`) `AskUserQuestion` `"PR 본문 템플릿 경로 1회성 override (빈 칸 = project.json → ~/.okstra/config.json → 스킬 디폴트 순으로 자동 해석)"` (free text) → `pr_template_path`. The backend (`okstra_ctl.pr_template.resolve_pr_template_path`) validates the file exists and surfaces `PrTemplateError` on failure. If the user wants to persist the choice instead of a one-shot override, tell them to set `prTemplatePath` in `<project_root>/.project-docs/okstra/project.json` (project scope) or `~/.okstra/config.json` (global scope).
222
240
 
223
- For prompts whose target worker is NOT in the resolved workers list (after override), skip the prompt and present a single line such as `gemini-model 생략 (workers에 gemini 없음)`.
241
+ For prompts whose target worker is NOT in the resolved workers list (after override), present a single confirmation line such as `gemini-model 생략 (workers에 gemini 없음)` so the user can see why the question was skipped.
224
242
 
225
243
  ## Step 6.5: Confirm selections before rendering
226
244
 
@@ -267,7 +285,8 @@ okstra render-bundle \
267
285
  --lead-model "..." --claude-model "..." --codex-model "..." \
268
286
  --gemini-model "..." --report-writer-model "..." \
269
287
  --related-tasks "..." \
270
- --clarification-response "<clarification-or-empty>"
288
+ --clarification-response "<clarification-or-empty>" \
289
+ --pr-template-path "<pr-template-override-or-empty; release-handoff only>"
271
290
  ```
272
291
 
273
292
  Stdout prints `okstra task root:`, `okstra instruction-set:`, and the full
@@ -0,0 +1,41 @@
1
+ <!--
2
+ okstra release-handoff 기본 PR 본문 템플릿.
3
+
4
+ 이 파일은 사용자 정의 PR 템플릿이 없을 때 사용됩니다. 우선순위:
5
+ 1. okstra-run Step 6 에서 입력한 per-run override 경로
6
+ 2. <project-root>/.project-docs/okstra/project.json 의 `prTemplatePath`
7
+ 3. ~/.okstra/config.json 의 `prTemplatePath`
8
+ 4. 이 디폴트 파일
9
+
10
+ 프로젝트 또는 전역 설정으로 자체 템플릿을 쓰려면 위 경로 중 하나에
11
+ `prTemplatePath` 키를 추가하세요. (절대경로 또는 project_root 기준 상대경로)
12
+
13
+ 플레이스홀더는 release-handoff 의 Claude lead 가 다음 입력을 근거로
14
+ 직접 채웁니다:
15
+ - run brief 의 의도/스코프
16
+ - 인용된 final-verification 리포트의 verdict 근거
17
+ - `git log --oneline <base>..HEAD` 의 commit 범위
18
+ - `git diff <base>..HEAD --stat` 의 변경 파일 통계
19
+
20
+ 빈 섹션은 그대로 두지 말고 통째로 삭제합니다. HTML 주석은 PR 생성 전에
21
+ 모두 제거됩니다.
22
+ -->
23
+
24
+ ## Summary
25
+
26
+ <!-- 1–3 bullets. 변경의 동기(WHY)와 결과(WHAT changed at a high level). -->
27
+
28
+ ## Changes
29
+
30
+ <!-- 영역별로 묶은 구체적 변경 목록. 파일/모듈 단위 그룹화 권장. -->
31
+
32
+ ## Test plan
33
+
34
+ <!-- 리뷰어가 따라 할 수 있는 검증 절차. 자동/수동 모두 가능. -->
35
+
36
+ - [ ] <검증 항목>
37
+ - [ ] <검증 항목>
38
+
39
+ ## Linked issues
40
+
41
+ <!-- `Refs: TICKET-123`, `Closes #N`, 관련 PR 링크 등. 없으면 섹션 통째로 삭제. -->
@@ -16,6 +16,7 @@ project-id: "{{PROJECT_ID}}"
16
16
  - Task Type: {{TASK_TYPE}}
17
17
  - Report Owner: `Claude lead`
18
18
  - Lead Model: `{{LEAD_MODEL}}`
19
+ - Okstra Version: `{{OKSTRA_VERSION}}`
19
20
  - Clarification Response Carried In: `{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}`
20
21
 
21
22
  ## User Approval Request (사용자 승인 게이트)
@@ -131,11 +131,13 @@
131
131
  "Bash(codex exec:*)",
132
132
  "Bash(okstra)",
133
133
  "Bash(okstra:*)",
134
- "Bash($HOME/.okstra/bin/okstra-codex-exec.sh:*)",
134
+ "Bash($HOME/.okstra/bin/:*)",
135
135
 
136
136
  "Bash(gemini)",
137
137
  "Bash(gemini:*)",
138
- "Bash($HOME/.okstra/bin/okstra-gemini-exec.sh:*)",
138
+
139
+ "Bash($HOME/.okstra/bin/okstra-trace-cleanup.sh)",
140
+ "Bash($HOME/.okstra/bin/okstra-trace-cleanup.sh:*)",
139
141
 
140
142
  "Bash(claude)",
141
143
  "Bash(claude:*)",
@@ -143,5 +145,14 @@
143
145
  "mcp__test-context7__resolve-library-id",
144
146
  "mcp__test-context7__query-docs"
145
147
  ]
148
+ },
149
+ "hooks": {
150
+ "SessionEnd": [
151
+ {
152
+ "hooks": [
153
+ { "type": "command", "command": "$HOME/.okstra/bin/okstra-trace-cleanup.sh" }
154
+ ]
155
+ }
156
+ ]
146
157
  }
147
158
  }
package/src/install.mjs CHANGED
@@ -20,6 +20,7 @@ const BIN_ENTRYPOINTS = [
20
20
  "okstra.sh",
21
21
  "okstra-codex-exec.sh",
22
22
  "okstra-gemini-exec.sh",
23
+ "okstra-trace-cleanup.sh",
23
24
  "okstra-central.sh",
24
25
  "okstra-token-usage.py",
25
26
  "okstra-error-log.py",
@@ -17,7 +17,8 @@ Usage:
17
17
  [--lead-model <m>] [--claude-model <m>] [--codex-model <m>] \\
18
18
  [--gemini-model <m>] [--report-writer-model <m>] \\
19
19
  [--related-tasks <list>] [--base-ref <ref>] \\
20
- [--clarification-response <path>] [--work-category <cat>]
20
+ [--clarification-response <path>] [--work-category <cat>] \\
21
+ [--pr-template-path <path>] # release-handoff only
21
22
 
22
23
  All flags pass through unchanged to \`python3 -m okstra_ctl.run\`. The
23
24
  shim auto-supplies \`--workspace-root\` (from \`okstra paths --field workspace\`)
package/src/uninstall.mjs CHANGED
@@ -7,6 +7,7 @@ const BIN_ENTRYPOINTS = [
7
7
  "okstra.sh",
8
8
  "okstra-codex-exec.sh",
9
9
  "okstra-gemini-exec.sh",
10
+ "okstra-trace-cleanup.sh",
10
11
  "okstra-central.sh",
11
12
  "okstra-token-usage.py",
12
13
  "okstra-error-log.py",