okstra 0.89.0 → 0.91.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.
@@ -894,6 +894,7 @@ resume 판단 기준:
894
894
  Claude가 작성하는 최종 보고서는 아래 구조를 우선 사용합니다 (brief 의 augmentation 이 더 구체적인 형식을 요구할 때만 그것을 따릅니다).
895
895
 
896
896
  - `## Verdict Card` — **최상단 의무 섹션**. Final Conclusion / Verdict Token / Direction / Approval Required? / Next Step 5 행. Verdict Token / Direction / Next Step 셀은 본문 §2 (실행 현황) 와 §6 (다음 단계) 의 권위 셀과 byte-match 해야 합니다.
897
+ - `## 작업 배경과 근거` — **모든 task-type 의무 섹션** (data.json `rationale`). 검토자용 서술로 네 질문에 순서대로 답합니다: 왜 이 작업을 하는가(`motivation`) / 왜 이게 문제인가(`problem`) / 그래서 어떤 작업이 필요한가(`approach`) / 왜 이게 합리적 선택인가(`justification`). 표가 아니라 프로즈이며, 각 필드는 `path:line` · 보고서 ID(`C-001`) · `§5.4` 같은 증거 참조나 명시적 불충분 표시(`근거 불충분`) 중 하나를 반드시 포함해야 합니다 — `validators/validate-run.py` 의 `_validate_rationale_evidence` 가 둘 다 없는 필드를 fail 합니다.
897
898
  - (선택) `## 0. Clarification Response Carried In From Previous Run` — 직전 run 에서 응답이 carry-in 된 경우에만 렌더링. 빈 carry-in 일 때는 헤딩 자체를 출력하지 않습니다.
898
899
  - `## 1. 문제 또는 검증 대상 요약` — §6.1 Consensus / §6.2 Differences 표 각각 `Source items (worker:item)` 컬럼 보존 (cross-worker traceability).
899
900
  - `## 2. 에이전트별 실행 현황`
@@ -948,7 +949,7 @@ Phase 7 step 1.5 가 final-report MD 한 본을 입력으로 두 view 를 결정
948
949
  - tmux 가 reachable 한 lead 환경이면 wrapper 가 sibling pane 을 자동 분할해 `tail -F <log-path>` 를 띄웁니다. trace pane title 은 caller (worker) pane title 에 `-tail` 을 붙인 `<cli>-<role>-<pid>-tail` (e.g. `codex-worker-93421-tail`); 동일 시점에 caller (worker) pane title 은 `<cli>-<role>-<pid>` 로 셋팅됩니다. `<pid>` 는 wrapper 자기 자신의 PID 라서 동일 role 의 worker 가 둘 이상 동시에 spawn 돼도 서로 구분되고, 운영자는 `<caller> ↔ <caller>-tail` 로 시각적으로 매핑할 수 있습니다. **caller pane 해석** — Claude Code Bash tool 은 이제 `$TMUX` 와 `$TMUX_PANE` 를 둘 다 환경에서 제거하므로 env 변수에 의존하지 않습니다. wrapper 는 (1) prompt path 로부터 `<RUN_DIR>` (= `dirname(dirname(prompt_path))`, paths.py SSOT) 를 도출하고, (2) lead 가 자기 foreground pane 에서 1회 기록한 `<RUN_DIR>/state/lead-pane.id` 를 읽어 split anchor 로 씁니다 (background dispatch 에서도 신뢰 가능 — active-pane 추정과 달리 사용자가 pane 을 옮겨도 안전). 기록 파일이 없거나 pane 이 stale 이면 `tmux display-message -p '#{pane_id}'` (active pane) 으로 fallback. trace pane split 은 그 caller pane 을 `-t` 로 명시 anchor 합니다. role 은 wrapper 의 5번째 optional positional 인자이며, 누락 시 기본값 `worker`. caller pane title 은 capture 해두고 EXIT trap 에서 복원하므로 dispatch 사이의 stale title 이 남지 않습니다. focus 는 caller pane 으로 복귀하고, CLI 종료 후 pane 은 유지돼 스크롤백 가능. tmux 미reachable, split 실패, 구버전 tmux 등 모든 경로는 silent degrade.
949
950
  - **run-scoped 태깅으로 정리**: trace pane 의 `tail -F` 는 tmux 셸의 자식이라 Claude 가 종료돼도 살아남습니다. wrapper 는 spawn 한 pane 을 `tmux set-option -p @okstra_trace_run=<RUN_DIR>` 로 태깅하고, `okstra-trace-cleanup.sh` 는 `tmux list-panes -a` 에서 그 태그로 pane 을 server-wide 발견해 `tmux kill-pane` 합니다. tmux env 변수·pane-id registry 없이 동작하며, run-scoped 태그라 동시에 도는 다른 okstra run 의 trace pane 을 죽이지 않습니다. cleanup 은 두 진입 형태를 가집니다 — lead 가 `--run-dir <RUN_DIR>` 로 호출(해당 run 의 trace + worker-agent pane 정리)하거나, `templates/reports/settings.template.json` 의 `hooks.SessionEnd` 가 `--reap` 로 호출(`$CLAUDE_PROJECT_DIR/.okstra/` 하위 태그를 가진 trace pane 일괄 정리; 단일 run-dir 이 없는 종료 시점용). tmux 가 없거나 stale pane id 인 경우 silent degrade.
950
951
  - **phase 전환 시 자동 정리 + worker-agent pane 포함**: `okstra-trace-cleanup.sh --run-dir <RUN_DIR>` 는 태깅된 trace pane 뿐 아니라 dispatch 된 서브에이전트가 점유하는 worker-agent pane(title `claude-worker` / `codex-worker` / `antigravity-worker` / `report-writer-worker`)도 lead 세션(`tmux list-panes -s -t <lead-pane>`) 범위에서 title allowlist 로 식별해 닫습니다(worker-agent pane 은 harness 소유라 태깅 불가). 세션 scope 와 lead 자기 pane 제외는 `<RUN_DIR>/state/lead-pane.id` 로 결정되며, lead 자신의 pane 은 title 이 걸려도 절대 죽이지 않습니다. lead 는 새 phase 의 worker 를 dispatch 하기 직전(`PROGRESS: phase-5.5-convergence` / `phase-6-synthesis` 마커 직전) 이 스크립트를 `--run-dir` 로 호출해 이전 phase 의 pane 을 prompt 없이 정리합니다.
951
- - **Phase 종료 시 사용자 확인**: run 최종 종료 시점(마지막 단계)에 lead 가 `okstra-trace-cleanup.sh --list --run-dir <RUN_DIR>` 로 잔여 okstra pane(worker-agent + trace) 목록을 출력한 뒤 사용자에게 "모두 닫기 / 그대로 두기" 양자택일을 묻고 응답대로 처리합니다 (`prompts/profiles/_common-contract.md` 의 *Phase wrap-up* 항목). 그 다음 Teams mode 인 경우 worker teammate 팀을 해제할지 별도로 묻고, 승인 시 `okstra-team-reconcile.sh` + `TeamDelete` 를 실행합니다. `<RUN_DIR>/state/lead-pane.id` 비어 있는(=tmux 밖) 환경에서는 pane 단계만 silent-skip 하며, teammate 단계는 team-state 의 `teamCreate.status` 판단합니다. `--list` 모드는 pane 을 죽이지 않고 `<pane_id>\t<pane_title>` 만 출력하므로 사용자가 무엇이 닫힐지 시각적으로 확인할 수 있습니다.
952
+ - **Phase 종료 시 사용자 확인**: run 최종 종료 시점(마지막 단계)에 lead 가 `okstra-trace-cleanup.sh --list --run-dir <RUN_DIR>` 로 잔여 okstra pane(worker-agent + trace) 목록을 출력한 뒤 사용자에게 "모두 닫기 / 그대로 두기" 양자택일을 묻고 응답대로 처리합니다 (`prompts/profiles/_common-contract.md` 의 *Phase wrap-up* 항목). 그 다음 Teams mode 인 경우 worker teammate 팀을 해제할지 별도로 묻고, 승인 시 `okstra-team-reconcile.sh` + `TeamDelete` 를 실행합니다. lead 는 이 pane 단계를 `lead-pane.id` 직접 해석해 게이트하지 않고 **항상** 스크립트를 호출하며, tmux 밖이면 스크립트가 pane 목록으로 안전하게 no-op 합니다. teammate 단계는 `teamCreate.status` 가 아니라 on-disk team config(`~/.claude/teams/session-*/config.json` 의 `leadSessionId` 매칭) 존재로 판단합니다. `--list` 모드는 pane 을 죽이지 않고 `<pane_id>\t<pane_title>` 만 출력하므로 사용자가 무엇이 닫힐지 시각적으로 확인할 수 있습니다.
952
953
  - 디스크 누적은 `okstra-inspect logs` 흐름이 read-only 로 인벤토리 + cleanup 명령을 제안합니다 (실행은 사용자 copy-paste).
953
954
 
954
955
  ### Linked-worktree `.git/` write 권한 (codex / antigravity)
@@ -977,7 +978,7 @@ Phase 7 step 1.5 가 final-report MD 한 본을 입력으로 두 view 를 결정
977
978
  phase 산출물의 출고 가능 여부를 강제하는 진입점:
978
979
 
979
980
  - `validators/validate-workflow.sh` — phase contract 통합 검증.
980
- - `validators/validate-run.py` — run-level final-report 본문 contract (Verdict Card 존재, deprecated §6.1/§6.2/§5.5.8/§5.5.9 Open Questions 부재, Plan Body Verification gate × Approval 마커 cross-check, Token Usage sentinel/zero 차단, 워커-결과 audit 사이드카 존재).
981
+ - `validators/validate-run.py` — run-level final-report 본문 contract (Verdict Card 존재, `rationale` 4개 필드의 증거-앵커 강제(`_validate_rationale_evidence`), deprecated §6.1/§6.2/§5.5.8/§5.5.9 Open Questions 부재, Plan Body Verification gate × Approval 마커 cross-check, Token Usage sentinel/zero 차단, 워커-결과 audit 사이드카 존재).
981
982
  - `validators/validate-report-views.py` — slim MD / HTML view 의 phase substring 보존 및 form-control 영역 검사.
982
983
  - `validators/validate-brief.py` — brief schema (front-matter, `Reporter Confirmations` 섹션 존재, root parent-id self 규칙, slug 컨벤션 등) 강제. `bash validators/validate-brief.sh <brief.md>` 가 thin wrapper.
983
984
 
@@ -179,7 +179,8 @@ Top-level scripts:
179
179
  | `okstra-render-report-views.py` | Render slim MD + HTML views from final-report Markdown |
180
180
  | `okstra-error-log.py` | Normalize worker/lead error sidecars |
181
181
  | `okstra-spawn-followups.py` | Follow-up spawning helper |
182
- | `okstra-trace-cleanup.sh` | tmux okstra pane cleanup (worker-agent + trace, lead pane 제외) |
182
+ | `okstra-trace-cleanup.sh` | tmux okstra pane cleanup (worker-agent + trace, lead pane 제외); `--reclaim-completed` 모드는 `@okstra_status` 가 종료(stage=exited)인 trace pane 만 회수하고 진행 중 pane 은 보존 |
183
+ | `okstra-subagent-reclaim.sh` | 활성 run 을 순회하며 완료된 trace pane 만 회수하는 엔트리 (`SubagentStop`/`TaskCompleted` 훅 배선) |
183
184
 
184
185
  ### 4.3 `scripts/okstra_ctl/` — Python orchestration core
185
186
 
@@ -204,6 +205,7 @@ Important modules:
204
205
  | `wizard.py` | `okstra-run` prompt state machine; user-facing Korean strings live in `prompts/wizard/prompts.ko.json` |
205
206
  | `index.py`, `jsonl.py`, `reconcile.py`, `listing.py`, `batch.py`, `backfill.py` | `~/.okstra` run index and history operations |
206
207
  | `session.py`, `tmux.py`, `seeding.py`, `locks.py`, `invocation.py`, `sequence.py`, `ids.py`, `material.py` | Supporting lifecycle helpers |
208
+ | `pane_reclaim.py` | 완료 trace pane 회수 대상 판정; 진행-중 status 집합을 `reconcile.NON_TERMINAL_RECENT_STATUSES` SSOT 에서 import |
207
209
  | `improvement_lenses.py` | improvement-discovery phase 의 lens enum SSOT + cap 상수 (DEFAULT 8, ABSOLUTE 12, MIN/MAX PRIORITY 1/4, SOURCE_WORKERS) |
208
210
 
209
211
  ### 4.4 `scripts/okstra_project/`
@@ -0,0 +1,483 @@
1
+ # Worker pane 완료-즉시 회수 + 내성 fallback 구현 계획
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** 외부 CLI worker(codex/antigravity)의 trace pane 을 완료 즉시 회수하고, pane 생성 실패 시 in-process 대체로 run 이 멈추지 않게 하여 tmux pane 한도 초과를 방지한다.
6
+
7
+ **Architecture:** wrapper 가 trace pane 에 자기 status 파일 경로를 태그(`@okstra_status`)로 단다 → `SubagentStop`/`TaskCompleted` 훅이 `active.jsonl` 의 활성 run 을 순회하며 `okstra-trace-cleanup.sh --reclaim-completed` 를 호출 → 그 모드는 status 가 `exited`(완료)인 trace pane 만 닫고 진행 중은 보존한다. 하니스가 만드는 teammate pane 은 okstra 가 태그할 수 없어 회수 대상에서 제외하고, lead 계약의 내성 fallback 으로 다룬다.
8
+
9
+ **Tech Stack:** Bash(wrapper/cleanup/hook 엔트리), Python(`okstra_ctl` — 완료 판정은 기존 `wrapper_status.read_wrapper_status` 재사용), tmux user-options(`@okstra_*`), Claude Code hooks.
10
+
11
+ ## Global Constraints
12
+
13
+ - `runtime/` 는 빌드 산출물 — 직접 수정 금지. 소스는 `scripts/`, `templates/` 에 두고 `npm run build` 로 동기화([CLAUDE.md](../../../CLAUDE.md)).
14
+ - 사용자 영향 수정은 seed/template 에 — 훅은 [templates/reports/settings.template.json](../../../templates/reports/settings.template.json) 에 배선(개인 `.claude/` 아님).
15
+ - Bash 는 thin 엔트리/어댑터. 판정 로직은 Python 모듈 재사용 — Bash 에 비즈니스 로직 복제 금지.
16
+ - 새/대폭 수정 함수는 유효 50줄 이내.
17
+ - pre-1.0 — 하위호환 shim 없음.
18
+ - 문서/주석 한국어(코드 식별자·경로·CLI 인자는 영어).
19
+ - 완료 판정 SSOT: `wrapper_status.WrapperStatus.is_terminal`(`stage == "exited"`) — 새 판정 로직 만들지 말 것.
20
+ - "worktree" 용어는 실제 git worktree 에만 — stage 폴더 상위는 "상위 디렉터리".
21
+
22
+ ---
23
+
24
+ ## Task 0: 실측 게이트 — teammate pane title 확인 (코드 작성 전 1회)
25
+
26
+ 설계의 단 하나 미검증 가정: **하니스가 teammate(subagent) pane title 을 `*-worker` 로 다는가.** 이 결과가 회수 범위를 정한다(trace pane 만 vs teammate pane 도). tmux 안에서 실측한다.
27
+
28
+ **Files:** 없음(조사).
29
+
30
+ - [ ] **Step 1: tmux 세션 안에서 okstra worker 1개를 띄운 직후 pane title 덤프**
31
+
32
+ tmux 안에서 `/okstra-run` 또는 직접 dispatch 로 worker 1개를 띄운 뒤:
33
+
34
+ Run: `tmux list-panes -a -F '#{pane_id}\t#{pane_title}\t#{@okstra_trace_run}\t#{@okstra_worker_run}'`
35
+
36
+ - [ ] **Step 2: 판정 기록**
37
+
38
+ - teammate pane title 이 `claude-worker` / `codex-worker` / `antigravity-worker` / `report-writer-worker` 중 하나와 `*<title>*` 매칭되면 → **회수 가능**. 이 plan 의 Task 2 끝 "조건부 확장" 을 적용한다.
39
+ - 매칭 안 되거나(빈 title, 하니스 고유 라벨) 같은 title 이 여러 worker 에 중복돼 진행중/완료 구분이 불가하면 → **trace pane 만 회수**. teammate 누적은 Task 5 내성에 의존(설계대로). 이 경우 Task 2 "조건부 확장" 은 건너뛴다.
40
+ - 결과를 이 파일 Task 0 아래 한 줄로 적어 후속 task 가 참조하게 한다.
41
+
42
+ > 실측 불가 환경(현재 세션이 tmux 밖)이면: trace pane 만 회수하는 경로로 진행하고, teammate 회수는 미적용으로 둔다. 이는 안전한 기본값이다(없는 기능을 가정해 진행 중 pane 을 죽이는 것보다 낫다).
43
+
44
+ ---
45
+
46
+ ## Task 1: wrapper 가 trace pane 에 `@okstra_status` 태그 추가
47
+
48
+ trace pane 을 그 worker 의 status 파일과 연결한다. cleanup 이 이 태그로 완료 여부를 읽는다.
49
+
50
+ **Files:**
51
+ - Modify: `scripts/okstra-codex-exec.sh` (trace pane 태그 블록, 현재 `@okstra_trace_run` 설정부 근처 ~272-281)
52
+ - Modify: `scripts/okstra-antigravity-exec.sh` (동일 블록 ~214-220)
53
+
54
+ **Interfaces:**
55
+ - Consumes: 각 wrapper 가 이미 보유한 `$status_path`(`${prompt_path%.md}.status.json`, codex:182 / antigravity:138)와 `$trace_pane`(split 결과 pane id).
56
+ - Produces: trace pane 의 tmux user-option `@okstra_status=<status_path 절대경로>`.
57
+
58
+ - [ ] **Step 1: codex wrapper — `@okstra_trace_run` 설정 직후 status 태그 추가**
59
+
60
+ `scripts/okstra-codex-exec.sh` 의 `tmux set-option -p -t "$trace_pane" @okstra_trace_run "$run_dir"` 줄 **직후**에 추가:
61
+
62
+ ```bash
63
+ [[ -n "$status_path" ]] && tmux set-option -p -t "$trace_pane" @okstra_status "$status_path" 2>/dev/null || true
64
+ ```
65
+
66
+ - [ ] **Step 2: antigravity wrapper — 동일 추가**
67
+
68
+ `scripts/okstra-antigravity-exec.sh` 의 동일한 `@okstra_trace_run` 설정 줄 직후에 같은 한 줄을 추가(변수명 `$status_path` / `$trace_pane` 동일).
69
+
70
+ - [ ] **Step 3: 태그 라인이 두 파일에 들어갔는지 확인**
71
+
72
+ Run: `grep -n '@okstra_status' scripts/okstra-codex-exec.sh scripts/okstra-antigravity-exec.sh`
73
+ Expected: 두 파일 각각 1줄.
74
+
75
+ - [ ] **Step 4: 커밋**
76
+
77
+ ```bash
78
+ git add scripts/okstra-codex-exec.sh scripts/okstra-antigravity-exec.sh
79
+ git commit -m "feat(wrapper): trace pane 에 @okstra_status 태그를 달아 완료 회수 연결"
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Task 2: `okstra-trace-cleanup.sh --reclaim-completed` 모드
85
+
86
+ 완료된(`stage == "exited"`) worker 의 trace pane 만 닫고 진행 중은 보존하는 모드. 완료 판정은 Python SSOT 를 호출한다.
87
+
88
+ **Files:**
89
+ - Create: `scripts/okstra_ctl/pane_reclaim.py` (완료 판정 wrapper — 테스트 대상)
90
+ - Modify: `scripts/okstra-trace-cleanup.sh` (인자 파싱 + collect 에 완료 필터)
91
+ - Test: `tests/test_pane_reclaim.py`
92
+
93
+ **Interfaces:**
94
+ - Consumes: trace pane 의 `@okstra_status` 태그(Task 1), `@okstra_trace_run` 태그(기존).
95
+ - Produces: `pane_reclaim.is_completed_status(status_path: str) -> bool` (CLI: `python3 -m okstra_ctl.pane_reclaim <status_path>` → exit 0 완료 / 1 미완료·부재). `okstra-trace-cleanup.sh --reclaim-completed --run-dir <RUN_DIR>`.
96
+
97
+ - [ ] **Step 1: 완료 판정 테스트 작성 (실패)**
98
+
99
+ `tests/test_pane_reclaim.py`:
100
+
101
+ ```python
102
+ from pathlib import Path
103
+ from okstra_ctl.pane_reclaim import is_completed_status
104
+
105
+
106
+ def _write(p: Path, stage: str) -> Path:
107
+ p.write_text('{"stage": "%s", "exit_code": 0}' % stage, encoding="utf-8")
108
+ return p
109
+
110
+
111
+ def test_exited_status_is_completed(tmp_path):
112
+ assert is_completed_status(str(_write(tmp_path / "w.status.json", "exited"))) is True
113
+
114
+
115
+ def test_started_status_is_not_completed(tmp_path):
116
+ assert is_completed_status(str(_write(tmp_path / "w.status.json", "started"))) is False
117
+
118
+
119
+ def test_missing_status_is_not_completed(tmp_path):
120
+ assert is_completed_status(str(tmp_path / "absent.status.json")) is False
121
+ ```
122
+
123
+ - [ ] **Step 2: 테스트 실패 확인**
124
+
125
+ Run: `python3 -m pytest tests/test_pane_reclaim.py -v`
126
+ Expected: FAIL — `ModuleNotFoundError: okstra_ctl.pane_reclaim`
127
+
128
+ - [ ] **Step 3: `pane_reclaim.py` 구현 (기존 SSOT 재사용)**
129
+
130
+ `scripts/okstra_ctl/pane_reclaim.py`:
131
+
132
+ ```python
133
+ """완료(terminal) worker 판정 — pane 회수용. 판정 SSOT 는
134
+ wrapper_status.read_wrapper_status 이며 여기서 재사용한다."""
135
+ from __future__ import annotations
136
+
137
+ import sys
138
+ from pathlib import Path
139
+
140
+ from .wrapper_status import read_wrapper_status
141
+
142
+
143
+ def is_completed_status(status_path: str) -> bool:
144
+ status = read_wrapper_status(Path(status_path))
145
+ return status is not None and status.is_terminal
146
+
147
+
148
+ def main(argv: list[str]) -> int:
149
+ if len(argv) != 1:
150
+ return 1
151
+ return 0 if is_completed_status(argv[0]) else 1
152
+
153
+
154
+ if __name__ == "__main__":
155
+ raise SystemExit(main(sys.argv[1:]))
156
+ ```
157
+
158
+ - [ ] **Step 4: 테스트 통과 확인**
159
+
160
+ Run: `python3 -m pytest tests/test_pane_reclaim.py -v`
161
+ Expected: PASS (3 passed)
162
+
163
+ - [ ] **Step 5: `okstra-trace-cleanup.sh` 에 `--reclaim-completed` 인자 추가**
164
+
165
+ 옵션 파싱(`while` 루프 `case`)에 추가:
166
+
167
+ ```bash
168
+ --reclaim-completed) MODE="reclaim" ;;
169
+ ```
170
+
171
+ `MODE` 초기값 주석에 `reclaim` 추가. `--reclaim-completed` 는 `--run-dir` 필수(기존 검증 재사용).
172
+
173
+ - [ ] **Step 6: collect 에 완료 필터 적용**
174
+
175
+ `collect_okstra_panes` 의 (1) 태그 스캔 루프에서, `MODE == "reclaim"` 일 때 trace pane 은 `@okstra_status` 가 완료일 때만 포함하도록 분기. tmux 수집 포맷에 `@okstra_status` 컬럼을 추가하고 판정을 끼운다:
176
+
177
+ ```bash
178
+ while IFS=$'\t' read -r pid trace_tag worker_tag status_tag; do
179
+ [[ -n "$pid" ]] || continue
180
+ [[ "$pid" == "$lead_pane" ]] && continue
181
+ if _tag_in_scope "$trace_tag" || _tag_in_scope "$worker_tag"; then
182
+ if [[ "$MODE" == "reclaim" ]]; then
183
+ # reclaim 모드: 완료(stage=exited)된 worker 의 pane 만. status 태그가
184
+ # 없거나(teammate/비-wrapper pane) 미완료면 보존.
185
+ [[ -n "$status_tag" ]] || continue
186
+ python3 -m okstra_ctl.pane_reclaim "$status_tag" || continue
187
+ fi
188
+ panes+=("$pid")
189
+ fi
190
+ done < <(tmux list-panes -a \
191
+ -F '#{pane_id}'$'\t''#{@okstra_trace_run}'$'\t''#{@okstra_worker_run}'$'\t''#{@okstra_status}' \
192
+ 2>/dev/null || true)
193
+ ```
194
+
195
+ `MODE == "reclaim"` 에서는 (2) title-allowlist teammate 스캔을 **건너뛴다**(Task 0 미통과 기본값 — teammate pane 보존). `MODE` 가 `kill`/`list` 일 때는 기존 동작 유지. `kill` 분기는 `MODE in (kill, reclaim)` 가 동일 kill 경로를 타도록 마지막 실행부 조건을 `[[ "$MODE" != "list" ]]` 로 둔다.
196
+
197
+ > **Task 0 통과 시에만** — teammate pane 도 회수하려면, reclaim 모드에서 (2) title 스캔을 살리되 각 teammate pane 의 완료 판정을 추가해야 한다. 단 teammate pane 엔 `@okstra_status` 태그가 없으므로(okstra 가 못 닮) 완료를 디스크에서 역매핑할 수단이 필요하다. Task 0 결과가 "회수 가능"이면 그 매핑(title→worker→status_path)을 별도 스텝으로 설계해 추가하고, 아니면 이 박스는 무시한다.
198
+
199
+ - [ ] **Step 7: PYTHONPATH 확인 — cleanup 이 `okstra_ctl` 를 import 할 수 있는지**
200
+
201
+ `okstra-trace-cleanup.sh` 가 `python3 -m okstra_ctl.pane_reclaim` 를 호출하려면 `scripts/` 가 `sys.path` 에 있어야 한다. 스크립트 상단(기존 `_clean_script_dir` 도출 직후)에 추가:
202
+
203
+ ```bash
204
+ export PYTHONPATH="$_clean_script_dir${PYTHONPATH:+:$PYTHONPATH}"
205
+ ```
206
+
207
+ 설치 레이아웃에서 `pane_reclaim.py`/`wrapper_status.py` 가 `$_clean_script_dir` 기준 어디에 놓이는지 [src/install.mjs](../../../src/install.mjs) 의 python 페이로드 경로와 대조해 PYTHONPATH 를 맞춘다(repo 는 `scripts/`, 설치본은 `~/.okstra/lib/python/`).
208
+
209
+ - [ ] **Step 8: e2e 시나리오 — 완료 pane 회수 / 진행 중 보존**
210
+
211
+ `tests-e2e/scenario-<id>-pane-reclaim.sh`: `mktemp -d` OKSTRA_HOME, tmux 가용할 때만 실행(없으면 skip). 모의 run-dir 에 두 worker prompt+status(`exited` 1개, `started` 1개)와 태그된 모의 pane 2개를 만들고, `--reclaim-completed --list` 가 `exited` pane 만 출력하는지 검증. `trap` 으로 OKSTRA_HOME 정리.
212
+
213
+ - [ ] **Step 9: 단위 테스트 통과 + 커밋**
214
+
215
+ Run: `python3 -m pytest tests/test_pane_reclaim.py -v && bash -n scripts/okstra-trace-cleanup.sh`
216
+ Expected: PASS + 문법 오류 없음
217
+
218
+ ```bash
219
+ git add scripts/okstra_ctl/pane_reclaim.py scripts/okstra-trace-cleanup.sh tests/test_pane_reclaim.py tests-e2e/scenario-*-pane-reclaim.sh
220
+ git commit -m "feat(cleanup): --reclaim-completed 모드 — 완료된 trace pane 만 회수"
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Task 3: 훅 진입점 `okstra-subagent-reclaim.sh` — 활성 run 순회
226
+
227
+ `SubagentStop`/`TaskCompleted` 가 주는 stdin 은 run-dir 을 직접 주지 않고 `cwd` 도 run-dir 보장이 없다. session_id 매핑 대신 `active.jsonl` 의 활성 run 을 모두 순회한다 — 완료 판정(Task 2)이 cross-run 안전을 보장하므로 다른 run 의 진행 중 pane 을 건드리지 않는다.
228
+
229
+ **Files:**
230
+ - Create: `scripts/okstra-subagent-reclaim.sh`
231
+ - Test: `tests/test_subagent_reclaim_targets.py` (run-dir 추출 로직을 Python helper 로 분리해 테스트)
232
+
233
+ **Interfaces:**
234
+ - Consumes: stdin JSON(무시 가능 — 어느 worker 가 끝났든 활성 run 전체를 reclaim), `~/.okstra/active.jsonl`(`runDirRel`/`projectRoot`/`status`).
235
+ - Produces: `pane_reclaim.active_run_dirs(home: Path) -> list[Path]` (status 가 종료 상태가 아닌 run 들의 절대 run-dir).
236
+
237
+ - [ ] **Step 1: active run-dir 추출 테스트 작성 (실패)**
238
+
239
+ `tests/test_subagent_reclaim_targets.py`:
240
+
241
+ ```python
242
+ import json
243
+ from pathlib import Path
244
+ from okstra_ctl.pane_reclaim import active_run_dirs
245
+
246
+
247
+ def test_active_run_dirs_resolves_absolute(tmp_path):
248
+ home = tmp_path
249
+ (home / "active.jsonl").write_text(
250
+ json.dumps({"status": "running", "runDirRel": "tasks/g/t/runs/x", "projectRoot": "/proj"}) + "\n",
251
+ encoding="utf-8",
252
+ )
253
+ dirs = active_run_dirs(home)
254
+ assert dirs == [home / "tasks/g/t/runs/x"]
255
+
256
+
257
+ def test_finished_runs_excluded(tmp_path):
258
+ home = tmp_path
259
+ (home / "active.jsonl").write_text(
260
+ json.dumps({"status": "done", "runDirRel": "tasks/g/t/runs/x", "projectRoot": "/proj"}) + "\n",
261
+ encoding="utf-8",
262
+ )
263
+ assert active_run_dirs(home) == []
264
+ ```
265
+
266
+ > **구현 전 확인:** `runDirRel` 의 base 가 `home` 인지 `projectRoot` 인지 [index.py](../../../scripts/okstra_ctl/index.py) `reserve_active` 호출부에서 확정하고, 위 테스트의 기대 경로를 그에 맞춘다. `status` 의 "활성" 집합(`reserving`/`running`/...)도 같은 파일에서 확인해 제외 조건을 맞춘다.
267
+
268
+ - [ ] **Step 2: 테스트 실패 확인**
269
+
270
+ Run: `python3 -m pytest tests/test_subagent_reclaim_targets.py -v`
271
+ Expected: FAIL — `active_run_dirs` 없음
272
+
273
+ - [ ] **Step 3: `active_run_dirs` 를 `pane_reclaim.py` 에 추가**
274
+
275
+ ```python
276
+ import json
277
+
278
+ _FINISHED = {"done", "complete", "error", "aborted"} # index.py 의 종료 status 와 대조해 확정
279
+
280
+
281
+ def active_run_dirs(home: Path) -> list[Path]:
282
+ path = home / "active.jsonl"
283
+ if not path.is_file():
284
+ return []
285
+ dirs: list[Path] = []
286
+ for line in path.read_text(encoding="utf-8").splitlines():
287
+ line = line.strip()
288
+ if not line:
289
+ continue
290
+ try:
291
+ row = json.loads(line)
292
+ except json.JSONDecodeError:
293
+ continue
294
+ rel = row.get("runDirRel")
295
+ if not rel or row.get("status") in _FINISHED:
296
+ continue
297
+ dirs.append(home / rel)
298
+ return dirs
299
+ ```
300
+
301
+ - [ ] **Step 4: 테스트 통과 확인**
302
+
303
+ Run: `python3 -m pytest tests/test_subagent_reclaim_targets.py -v`
304
+ Expected: PASS
305
+
306
+ - [ ] **Step 5: 훅 엔트리 스크립트 작성**
307
+
308
+ `scripts/okstra-subagent-reclaim.sh`:
309
+
310
+ ```bash
311
+ #!/usr/bin/env bash
312
+ #
313
+ # okstra-subagent-reclaim.sh — SubagentStop / TaskCompleted 훅 엔트리.
314
+ # stdin(JSON)은 읽고 버린다(어느 worker 가 끝났든 활성 run 전체의 완료 pane 만
315
+ # 회수하므로). active.jsonl 의 각 활성 run 에 대해 cleanup --reclaim-completed.
316
+ # 모든 실패는 조용히 삼킨다 — 훅은 세션 진행을 절대 막지 않는다.
317
+ set -u
318
+ cat >/dev/null 2>&1 || true # drain stdin
319
+
320
+ _dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
321
+ home="${OKSTRA_HOME:-$HOME/.okstra}"
322
+ export PYTHONPATH="$_dir${PYTHONPATH:+:$PYTHONPATH}"
323
+
324
+ while IFS= read -r run_dir; do
325
+ [[ -n "$run_dir" ]] || continue
326
+ "$_dir/okstra-trace-cleanup.sh" --reclaim-completed --run-dir "$run_dir" 2>/dev/null || true
327
+ done < <(python3 -m okstra_ctl.pane_reclaim --active-dirs "$home" 2>/dev/null || true)
328
+
329
+ exit 0
330
+ ```
331
+
332
+ `pane_reclaim.main` 에 `--active-dirs <home>` 서브커맨드를 추가해 `active_run_dirs` 결과를 한 줄씩 출력하게 한다(기존 단일 status_path 판정과 분기).
333
+
334
+ - [ ] **Step 6: `pane_reclaim.main` 에 `--active-dirs` 분기 추가**
335
+
336
+ ```python
337
+ def main(argv: list[str]) -> int:
338
+ if len(argv) == 2 and argv[0] == "--active-dirs":
339
+ for d in active_run_dirs(Path(argv[1])):
340
+ print(d)
341
+ return 0
342
+ if len(argv) == 1:
343
+ return 0 if is_completed_status(argv[0]) else 1
344
+ return 1
345
+ ```
346
+
347
+ - [ ] **Step 7: 실행권한 + 문법 + 테스트**
348
+
349
+ Run: `chmod +x scripts/okstra-subagent-reclaim.sh && bash -n scripts/okstra-subagent-reclaim.sh && python3 -m pytest tests/test_subagent_reclaim_targets.py tests/test_pane_reclaim.py -v`
350
+ Expected: PASS
351
+
352
+ - [ ] **Step 8: 커밋**
353
+
354
+ ```bash
355
+ git add scripts/okstra-subagent-reclaim.sh scripts/okstra_ctl/pane_reclaim.py tests/test_subagent_reclaim_targets.py
356
+ git commit -m "feat(hook): okstra-subagent-reclaim.sh — 활성 run 의 완료 pane 회수 엔트리"
357
+ ```
358
+
359
+ ---
360
+
361
+ ## Task 4: 훅 배선 — `settings.template.json`
362
+
363
+ 완료 이벤트에 reclaim 엔트리를 배선한다. no-team(Agent) 경로는 `SubagentStop`, Teams 경로는 `TaskCompleted`.
364
+
365
+ **Files:**
366
+ - Modify: `templates/reports/settings.template.json` (`hooks` 객체)
367
+
368
+ - [ ] **Step 1: 두 이벤트 훅 추가**
369
+
370
+ `hooks` 객체에 추가(기존 `PreToolUse`, `SessionEnd` 유지):
371
+
372
+ ```json
373
+ "SubagentStop": [
374
+ {
375
+ "hooks": [
376
+ {
377
+ "type": "command",
378
+ "command": "$HOME/.okstra/bin/okstra-subagent-reclaim.sh"
379
+ }
380
+ ]
381
+ }
382
+ ],
383
+ "TaskCompleted": [
384
+ {
385
+ "hooks": [
386
+ {
387
+ "type": "command",
388
+ "command": "$HOME/.okstra/bin/okstra-subagent-reclaim.sh"
389
+ }
390
+ ]
391
+ }
392
+ ]
393
+ ```
394
+
395
+ - [ ] **Step 2: JSON 유효성 확인**
396
+
397
+ Run: `python3 -c "import json; json.load(open('templates/reports/settings.template.json'))"`
398
+ Expected: 오류 없음
399
+
400
+ - [ ] **Step 3: 설치 payload 에 새 스크립트가 포함되는지 확인**
401
+
402
+ [src/install.mjs](../../../src/install.mjs) 가 `scripts/okstra-*.sh` 를 `~/.okstra/bin/` 으로 복사하는 규칙에 `okstra-subagent-reclaim.sh` 가 자동 포함되는지(glob/명시 목록) 확인. 명시 목록이면 추가한다.
403
+
404
+ Run: `grep -n 'okstra-trace-cleanup\|okstra-.*\.sh\|bin/' src/install.mjs | head`
405
+
406
+ - [ ] **Step 4: 커밋**
407
+
408
+ ```bash
409
+ git add templates/reports/settings.template.json src/install.mjs
410
+ git commit -m "feat(hook): SubagentStop/TaskCompleted 에 pane reclaim 배선"
411
+ ```
412
+
413
+ ---
414
+
415
+ ## Task 5: 내성 계약 — `_common-contract.md`
416
+
417
+ 하니스 teammate pane 생성 실패는 okstra Python 이 못 보므로 코드 강제 불가. lead 행동 계약에 명시한다(declaration, prompt 지시임을 분명히).
418
+
419
+ **Files:**
420
+ - Modify: `prompts/profiles/_common-contract.md` (dispatch/convergence 절)
421
+
422
+ - [ ] **Step 1: pane-실패 fallback 문단 추가**
423
+
424
+ 기존 dispatch 관련 절에 추가(한국어/영어는 그 파일의 기존 언어 관행에 맞춤):
425
+
426
+ > **Pane-budget fallback (tolerance).** worker dispatch 가 `no room for another tmux split`(또는 동등한 teammate pane 생성 실패)로 거부되면, 그 worker 를 `run_in_background` 없이 in-process 로 재시도하거나, 외부 CLI worker 였다면 in-process claude 분석으로 **대체**하고 그 사실(provider 불가)을 run 기록·convergence 노트에 남긴다. 완료된 외부 CLI worker 의 trace pane 은 `SubagentStop` 훅이 자동 회수하지만, 하니스가 만든 teammate pane 누적은 okstra 가 직접 회수할 수 없으므로 이 fallback 이 최후 방어선이다. (이는 코드 강제가 아닌 prompt 지시다.)
427
+
428
+ - [ ] **Step 2: declaration ≠ enforcement 규칙 준수 확인**
429
+
430
+ 이 문단이 "MUST/BLOCKING" 같은 강제 용어를 enforcement 없이 쓰지 않는지 확인(글로벌 5.5). "최후 방어선"·"대체한다" 수준의 행동 지시로 유지하고, 코드 강제가 아님을 명시한 현재 문구를 지킨다.
431
+
432
+ - [ ] **Step 3: 커밋**
433
+
434
+ ```bash
435
+ git add prompts/profiles/_common-contract.md
436
+ git commit -m "docs(contract): pane-budget fallback — pane 실패 시 in-process 대체"
437
+ ```
438
+
439
+ ---
440
+
441
+ ## Task 6: 빌드 동기화 + 전체 검증
442
+
443
+ **Files:** 없음(빌드/검증).
444
+
445
+ - [ ] **Step 1: 빌드**
446
+
447
+ Run: `npm run build`
448
+ Expected: `done. NN/NN sources synced.`
449
+
450
+ - [ ] **Step 2: 전체 단위 + 워크플로 검증**
451
+
452
+ Run: `python3 -m pytest tests/ -q && bash validators/validate-workflow.sh`
453
+ Expected: 모두 통과
454
+
455
+ - [ ] **Step 3: CLI 스모크 + doctor**
456
+
457
+ Run: `node bin/okstra --version && node bin/okstra doctor`
458
+ Expected: 정상
459
+
460
+ - [ ] **Step 4: CHANGES.md 항목 추가 (사용자 영향)**
461
+
462
+ `CHANGES.md` 최상단 날짜 섹션에 항목 추가 — trace pane 완료-즉시 회수 + 내성 fallback 이 pane 한도 초과를 완화한다는 `사용자 영향:` 줄 포함. 한국어.
463
+
464
+ - [ ] **Step 5: 커밋**
465
+
466
+ ```bash
467
+ git add CHANGES.md
468
+ git commit -m "docs(changes): worker trace pane 완료-회수 + 내성 fallback"
469
+ ```
470
+
471
+ ---
472
+
473
+ ## 완료 기준
474
+
475
+ - 외부 CLI worker 완료 시 그 trace pane 이 `SubagentStop`/`TaskCompleted` 훅으로 자동 회수되고, 진행 중 worker 의 pane 은 보존된다(e2e 시나리오로 입증).
476
+ - pane 생성 실패 시 lead 가 in-process 대체로 진행하도록 계약에 명시된다.
477
+ - 전체 pytest + validate-workflow 통과, 빌드 동기화 완료.
478
+ - teammate pane 회수는 Task 0 실측 결과에 따라 적용/미적용이 명확히 기록된다.
479
+
480
+ ## 알려진 한계 (spec §9 와 일치)
481
+
482
+ - 하니스 teammate pane 은 okstra 가 태그·식별을 보장할 수 없어, Task 0 미통과 시 회수 대상이 아니다 — 그 누적은 Task 5 내성에 의존하며, 내성은 코드 강제가 불가능한 prompt 계약이다.
483
+ - 한 세션 다중 동시 run 시 active.jsonl 순회가 모든 run 을 reclaim 하지만, 완료 판정이 진행 중 pane 을 보호하므로 안전하다.