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 +2 -2
- package/README.md +2 -2
- package/docs/kr/architecture.md +2 -0
- package/docs/kr/cli.md +1 -1
- package/docs/project-structure-overview.md +56 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +3 -1
- package/runtime/bin/okstra-codex-exec.sh +11 -0
- package/runtime/bin/okstra-gemini-exec.sh +7 -0
- package/runtime/bin/okstra-trace-cleanup.sh +75 -0
- package/runtime/prompts/profiles/_common-contract.md +12 -0
- package/runtime/prompts/profiles/release-handoff.md +2 -1
- package/runtime/python/okstra_ctl/index.py +2 -1
- package/runtime/python/okstra_ctl/pr_template.py +126 -0
- package/runtime/python/okstra_ctl/render.py +3 -0
- package/runtime/python/okstra_ctl/run.py +41 -2
- package/runtime/python/okstra_ctl/seeding.py +19 -0
- package/runtime/python/okstra_ctl/workers.py +20 -0
- package/runtime/skills/okstra-run/SKILL.md +27 -8
- package/runtime/skills/okstra-run/templates/pr-body.template.md +41 -0
- package/runtime/templates/reports/final-report.template.md +1 -0
- package/runtime/templates/reports/settings.template.json +13 -2
- package/src/install.mjs +1 -0
- package/src/render-bundle.mjs +2 -1
- package/src/uninstall.mjs +1 -0
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 스킬 마크다운 소스 (스킬
|
|
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/` 아래 스킬 마크다운
|
|
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 (
|
|
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
|
|
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
|
|
package/docs/kr/architecture.md
CHANGED
|
@@ -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/` —
|
|
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
package/runtime/BUILD.json
CHANGED
|
@@ -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).
|
|
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
|
|
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 .
|
|
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
|
-
|
|
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
|
-
|
|
211
|
+
**Before asking any worker/model question, resolve the profile's allowed roster:**
|
|
212
212
|
|
|
213
|
-
|
|
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),
|
|
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
|
|
134
|
+
"Bash($HOME/.okstra/bin/:*)",
|
|
135
135
|
|
|
136
136
|
"Bash(gemini)",
|
|
137
137
|
"Bash(gemini:*)",
|
|
138
|
-
|
|
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
package/src/render-bundle.mjs
CHANGED
|
@@ -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\`)
|