okstra 0.88.2 → 0.90.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/docs/kr/architecture.md +1 -1
- package/docs/superpowers/plans/2026-06-18-subagent-pane-reclaim.md +483 -0
- package/docs/superpowers/specs/2026-06-18-subagent-pane-reclaim-design.md +150 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/TODO.md +36 -0
- package/runtime/bin/okstra-antigravity-exec.sh +1 -0
- package/runtime/bin/okstra-codex-exec.sh +1 -0
- package/runtime/bin/okstra-subagent-reclaim.sh +26 -0
- package/runtime/bin/okstra-trace-cleanup.sh +41 -13
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +18 -12
- package/runtime/prompts/profiles/implementation-planning.md +12 -1
- package/runtime/python/okstra_ctl/clarification_items.py +1 -1
- package/runtime/python/okstra_ctl/pane_reclaim.py +55 -0
- package/runtime/python/okstra_ctl/render.py +23 -3
- package/runtime/python/okstra_ctl/report_views.py +74 -17
- package/runtime/python/okstra_ctl/wizard.py +85 -6
- package/runtime/skills/okstra-run/SKILL.md +5 -2
- package/runtime/templates/reports/report.css +10 -1
- package/runtime/templates/reports/settings.template.json +20 -0
- package/src/doctor.mjs +12 -5
- package/src/install.mjs +1 -0
package/docs/kr/architecture.md
CHANGED
|
@@ -948,7 +948,7 @@ Phase 7 step 1.5 가 final-report MD 한 본을 입력으로 두 view 를 결정
|
|
|
948
948
|
- 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
949
|
- **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
950
|
- **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` 를 실행합니다.
|
|
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` 를 실행합니다. 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
952
|
- 디스크 누적은 `okstra-inspect logs` 흐름이 read-only 로 인벤토리 + cleanup 명령을 제안합니다 (실행은 사용자 copy-paste).
|
|
953
953
|
|
|
954
954
|
### Linked-worktree `.git/` write 권한 (codex / antigravity)
|
|
@@ -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 을 보호하므로 안전하다.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# SubagentStop 기반 worker pane 완료-즉시 회수 + 내성 fallback 설계
|
|
2
|
+
|
|
3
|
+
- 작성일: 2026-06-18
|
|
4
|
+
- 상태: 설계(승인 대기)
|
|
5
|
+
- 관련: [_common-contract.md](../../../prompts/profiles/_common-contract.md) phase-start cleanup, [okstra-trace-cleanup.sh](../../../scripts/okstra-trace-cleanup.sh), [tmux.py](../../../scripts/okstra_ctl/tmux.py), [settings.template.json](../../../templates/reports/settings.template.json)
|
|
6
|
+
|
|
7
|
+
## 1. 배경 — 근거로 확정된 실제 원인
|
|
8
|
+
|
|
9
|
+
`jobs:uploadfont:dev-9047` 의 implementation-planning run 에서 13번째 worker dispatch(antigravity gap-verify)가 하니스 에러로 실패했다:
|
|
10
|
+
|
|
11
|
+
> `Failed to create teammate pane: no space for new pane — no room for another tmux split`
|
|
12
|
+
|
|
13
|
+
이 run 의 lead 가 `state/session-work-summary.md` 5절에 자가 분석을 남겼으나, 그 두 전제가 디스크 데이터와 어긋났다. 코드/데이터로 재검증한 결과는 다음과 같다.
|
|
14
|
+
|
|
15
|
+
### 1.1 정정된 사실
|
|
16
|
+
|
|
17
|
+
- **에러 주체는 okstra 가 아니라 Claude Code 하니스다.** okstra 의 `split_worker_pane` 은 실패해도 조용히 degrade 한다([dispatch_core.py](../../../scripts/okstra_ctl/dispatch_core.py) `_start_tmux_or_degrade`). 위 메시지는 하니스의 teammate pane 생성이 낸 것이다.
|
|
18
|
+
- **"lead-pane.id 빈 값 → lead 가 tmux 밖"(자가분석 주장 4)은 사실과 다르다.** 그 run 의 `state/lead-pane.id` = `%319` 로, lead 는 tmux 안에 있었고 기록도 정상이었다. phase-start cleanup 의 lead-in-tmux 게이트는 통과 가능한 상태였다.
|
|
19
|
+
- **"no-team 이라 shutdown_request 가 skip 되어 pane 누적"(주장 3)은 인과가 틀렸다.** `shutdown_request` 는 FleetView roster 정리이지 tmux pane 회수가 아니다([_common-contract.md:42](../../../prompts/profiles/_common-contract.md) — dismissed teammate 는 idle pill 로 남는다고 명시). tmux pane 회수는 전적으로 `okstra-trace-cleanup.sh` 의 일이고 no-team 여부와 무관하다.
|
|
20
|
+
|
|
21
|
+
### 1.2 진짜 원인
|
|
22
|
+
|
|
23
|
+
lead 가 tmux 안이었으므로 pane 회수((a) `okstra-trace-cleanup.sh`)는 돌 수 있었는데 누적을 못 막았다. 두 구조적 사실 때문이다.
|
|
24
|
+
|
|
25
|
+
1. **cleanup 미실행 정황.** phase-start cleanup 은 prompt 계약일 뿐 코드 강제가 아니며, 그 run 의 Phase 진행 내역에 실행 기록이 없다.
|
|
26
|
+
2. **okstra 는 하니스가 만든 teammate pane 에 태그를 못 단다.** `tag_pane` 은 okstra 가 직접 split 한 pane 에만 붙는다([tmux.py](../../../scripts/okstra_ctl/tmux.py) `split_worker_pane` 내부). 하니스 teammate pane 의 식별 수단은 title 매칭뿐이고, 그건 하니스가 pane title 을 어떻게 다는지에 의존해 불확실하다.
|
|
27
|
+
|
|
28
|
+
그 run 의 pane 구성: 하니스 teammate pane 13개 + codex/antigravity wrapper 가 만든 trace pane 7개 = 한 window 에 20개 누적 → 분할 한도 초과.
|
|
29
|
+
|
|
30
|
+
## 2. 목표 / 비목표
|
|
31
|
+
|
|
32
|
+
### 목표
|
|
33
|
+
- 완료된 worker 의 pane 을 **완료 즉시** 회수해 한 window 의 누적이 분할 한도에 닿지 않게 한다(예방).
|
|
34
|
+
- 그럼에도 한도에 닿으면 run 이 멈추지 않고 자동으로 in-process 대체로 진행한다(내성).
|
|
35
|
+
|
|
36
|
+
### 비목표
|
|
37
|
+
- 하니스가 teammate pane 을 만드는 행위 자체를 막거나 바꾸지 않는다(okstra 통제 밖).
|
|
38
|
+
- 외부 CLI 워커(codex/antigravity)를 제거해 cross-verification 다양성을 훼손하지 않는다.
|
|
39
|
+
- FleetView roster 표시(idle pill) 정리는 본 설계 범위 밖(별개 메커니즘).
|
|
40
|
+
|
|
41
|
+
## 3. 제약 (설계가 마주한 벽)
|
|
42
|
+
|
|
43
|
+
| pane 종류 | 누가 만드나 | okstra 통제력 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| 외부 CLI trace pane (`<cli>-<role>-<pid>-tail`) | codex/antigravity wrapper = **okstra 코드** | 높음 — `@okstra_trace_run` 태그로 식별·회수 |
|
|
46
|
+
| teammate pane (`*-worker` title) | **하니스** (Agent 도구 / Teams Task) | 낮음 — 태그 불가, title 매칭에만 의존 |
|
|
47
|
+
|
|
48
|
+
→ 확실히 회수 가능한 것은 **trace pane**(태그). teammate pane 은 title 회수가 되는지 **실측 게이트**(§6)로 확정해야 하며, 안 되면 내성(④)으로 백업한다.
|
|
49
|
+
|
|
50
|
+
## 4. 설계
|
|
51
|
+
|
|
52
|
+
### ① 훅 포인트: `SubagentStop`(+`TaskCompleted`)
|
|
53
|
+
|
|
54
|
+
PreToolUse(Agent)는 병렬 dispatch 시 Agent 호출마다 N회 발화하므로, dispatch 직전 일괄 cleanup 은 **같은 batch 의 진행 중 형제 worker pane 을 죽인다**(claude-code-guide 가 공식 hooks 문서로 확인). 따라서 채택하지 않는다.
|
|
55
|
+
|
|
56
|
+
대신 **완료 이벤트**에 건다 — 완료된 그 worker 만 정리하므로 진행 중 worker 는 건드리지 않고, 누적이 batch 경계까지 쌓이지 않는다.
|
|
57
|
+
|
|
58
|
+
- no-team(Agent background) 경로 → **`SubagentStop`** (입력: `session_id`, `agent_id`, `agent_type`, `agent_transcript_path`).
|
|
59
|
+
- Teams(`TaskCreate`) 경로 → **`TaskCompleted`** (입력: `task_id`, `teammate_name`, `team_name`, `session_id`).
|
|
60
|
+
|
|
61
|
+
두 훅 모두 [settings.template.json](../../../templates/reports/settings.template.json) 의 `hooks` 에 신규 배선한다(기존 SessionEnd `--reap`, PreToolUse coding-preflight 는 유지). 둘 다 부수효과 전용 — 스크립트는 항상 `exit 0`, 도구/세션 진행을 차단하지 않는다.
|
|
62
|
+
|
|
63
|
+
### ② cleanup 에 "완료된 worker만" 안전 모드 추가
|
|
64
|
+
|
|
65
|
+
현재 `okstra-trace-cleanup.sh --run-dir <RUN_DIR>` 은 그 run 의 태그/title pane 을 **진행 중 여부와 무관하게** 모두 닫는다. 완료 이벤트 훅에서 안전하게 쓰려면, **결과 파일이 이미 존재하는(=완료된) worker 의 pane 만** 닫는 모드가 필요하다.
|
|
66
|
+
|
|
67
|
+
새 모드(가칭) `--reclaim-completed --run-dir <RUN_DIR>`:
|
|
68
|
+
- §6 실측 결과에 따라 닫는 대상이 정해진다.
|
|
69
|
+
- 항상 닫음(확실): `@okstra_trace_run == RUN_DIR` 태그 trace pane 중, 대응 worker 의 `result_path` 가 존재하는 것.
|
|
70
|
+
- 조건부(실측 통과 시): lead 세션 안에서 title 이 `*-worker` allowlist 에 맞고 대응 worker 의 `result_path` 가 존재하는 teammate pane.
|
|
71
|
+
- 진행 중(result 미존재) worker pane 은 **절대 닫지 않는다** — 이 가드가 본 모드의 핵심.
|
|
72
|
+
|
|
73
|
+
trace pane → worker 매핑은 pane title(`<cli>-<role>-<pid>-tail`)의 role 과 run-dir 의 worker 산출물 경로(result_path) 규약으로 해석한다. 정확한 매핑 규약은 구현 plan 에서 `dispatch_core` 의 `status_path_for_prompt` / `result_path` 명명과 대조해 확정한다.
|
|
74
|
+
|
|
75
|
+
### ③ session ↔ run-dir 매핑 기록 (훅이 run-dir 을 알기 위함)
|
|
76
|
+
|
|
77
|
+
`SubagentStop`/`TaskCompleted` 입력은 `session_id`(lead 세션)를 주지만, 훅의 `cwd` 가 run-dir 과 일치한다는 보장이 없고 run-dir 전용 env 도 없다(공식 문서 확인). 그리고 **현재 `active.jsonl` row 에는 `leadSessionId` 가 없다**([index.py](../../../scripts/okstra_ctl/index.py) `reserve_active` row — `runId/taskKey/runDirRel/...` 만).
|
|
78
|
+
|
|
79
|
+
따라서 lead 가 run 시작 시 자기 `session_id ↔ RUN_DIR` 매핑을 알려진 위치에 기록한다. 두 후보:
|
|
80
|
+
|
|
81
|
+
- **(권장)** `~/.okstra/sessions/<session_id>.run` 단일 파일에 현재 RUN_DIR(절대경로)을 기록. run 시작 시 갱신, SessionEnd `--reap` 가 정리. 한 세션이 여러 run 을 순차 구동하면 **가장 최근 run-dir** 로 덮어쓴다(완료 이벤트는 항상 최근 run 소속이므로 안전).
|
|
82
|
+
- (대안) `active.jsonl` row 에 `leadSessionId` 필드를 추가. 다만 한 세션 = 여러 active row 일 때 최근 row 선택 로직이 필요하고 중앙 락 경합을 늘린다.
|
|
83
|
+
|
|
84
|
+
훅 스크립트는 stdin JSON 의 `session_id` 로 이 파일을 읽어 RUN_DIR 을 얻은 뒤 ②를 호출한다. 매핑이 없으면(파일 부재) silent no-op.
|
|
85
|
+
|
|
86
|
+
### ④ 내성 — lead 계약의 pane-실패 fallback
|
|
87
|
+
|
|
88
|
+
하니스의 teammate pane 생성은 okstra Python 이 관여하지 않으므로 코드로 강제할 수 없다. lead 행동 계약([_common-contract.md](../../../prompts/profiles/_common-contract.md) convergence/dispatch 절)에 명시한다:
|
|
89
|
+
|
|
90
|
+
- worker dispatch 가 `no room for another tmux split`(또는 동등한 pane 생성 실패)로 거부되면, 그 worker 를 **`run_in_background` 없이 in-process** 로 재시도하거나, 외부 CLI 워커였다면 in-process claude 분석으로 대체하고 그 사실(provider 불가)을 run 기록에 남긴다.
|
|
91
|
+
- 이미 `session-work-summary.md` 의 완화방안에 등장한 동작을 계약으로 승격하는 것이며, 코드 강제가 아닌 prompt 지시임을 문서에 명시(declaration ≠ enforcement 규칙).
|
|
92
|
+
|
|
93
|
+
## 5. 데이터 흐름
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
run 시작 (lead)
|
|
97
|
+
└─ ~/.okstra/sessions/<session_id>.run := RUN_DIR (③)
|
|
98
|
+
|
|
99
|
+
worker N 완료
|
|
100
|
+
└─ 하니스가 SubagentStop / TaskCompleted 발화
|
|
101
|
+
└─ 훅 command: okstra-subagent-reclaim.sh (stdin: session_id, agent_*)
|
|
102
|
+
├─ session_id → RUN_DIR (sessions/<id>.run 읽기, 없으면 no-op)
|
|
103
|
+
└─ okstra-trace-cleanup.sh --reclaim-completed --run-dir RUN_DIR (②)
|
|
104
|
+
├─ result_path 존재하는 worker 의 trace pane(태그) kill [확실]
|
|
105
|
+
└─ (실측 통과 시) result_path 존재하는 worker 의 teammate pane(title) kill [조건부]
|
|
106
|
+
※ 진행 중(result 미존재) worker pane 은 보존
|
|
107
|
+
|
|
108
|
+
dispatch 가 pane 실패로 거부 (lead)
|
|
109
|
+
└─ in-process 대체로 진행 + 기록 (④)
|
|
110
|
+
|
|
111
|
+
session 종료
|
|
112
|
+
└─ SessionEnd: okstra-trace-cleanup.sh --reap (기존 유지)
|
|
113
|
+
└─ sessions/<session_id>.run 정리
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 6. 실측 게이트 (구현 첫 단계, 코드 작성 전)
|
|
117
|
+
|
|
118
|
+
문서로 확인 불가한 단 하나의 가정 — **하니스가 teammate pane title 을 `*-worker` allowlist 에 맞게 다는가.**
|
|
119
|
+
|
|
120
|
+
검증 절차: tmux 안에서 okstra worker(claude-worker) 1개를 띄운 직후 `tmux list-panes -a -F '#{pane_id}\t#{pane_title}'` 로 그 pane 의 실제 title 을 확인한다.
|
|
121
|
+
- 매칭됨 → ② 의 teammate pane 회수(조건부)를 활성화한다.
|
|
122
|
+
- 매칭 안 됨 → ② 는 trace pane 만 회수하고, teammate pane 누적은 ④ 내성에 전적으로 의존한다(설계 문서·계약에 그 한계를 명시).
|
|
123
|
+
|
|
124
|
+
이 결과가 ②의 범위를 확정하므로, 구현 plan 의 첫 task 로 둔다.
|
|
125
|
+
|
|
126
|
+
> **구현 후 결론 (2026-06-18).** 실측과 무관하게 **hook 경로의 teammate pane 회수는 구조적으로 불가**로 확정됐다. hook 은 완료 이벤트마다 worker 하나씩 발화하는데 teammate pane 은 generic title(`*claude-worker*` 등)로만 식별되고 per-worker 태그가 없어([okstra-trace-cleanup.sh](../../../scripts/okstra-trace-cleanup.sh) `collect_okstra_panes` 의 title allowlist), 같은 batch 의 진행 중 형제 pane 과 완료 pane 을 구분할 수 없다 → title kill 시 진행 중 worker 를 죽일 위험. 그래서 `--reclaim-completed` 는 title 스캔을 건너뛴다(`RECLAIM -eq 0` 가드). teammate pane 을 안전하게 회수할 수 있는 유일한 지점은 **batch 경계**(직전 batch 의 모든 worker 가 완료된 시점)이며, 그것이 lead 의 phase-start cleanup(`--run-dir`, RECLAIM=0)이다. 위 §6 의 title-매칭 여부는 이 batch-경계 스캔의 유효성에 대한 질문일 뿐 hook 가능성과는 직교한다. 따라서 ②의 teammate 부분은 hook 에 넣지 않고 lead 의 batch-경계 cleanup 에 맡긴다.
|
|
127
|
+
|
|
128
|
+
## 7. 변경 대상 (구현 plan 에서 구체화)
|
|
129
|
+
|
|
130
|
+
| 파일 | 변경 |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `templates/reports/settings.template.json` | `SubagentStop` + `TaskCompleted` 훅 신규 배선 |
|
|
133
|
+
| `scripts/okstra-subagent-reclaim.sh` (신규) | 훅 진입점 — stdin `session_id` → RUN_DIR → cleanup 호출 |
|
|
134
|
+
| `scripts/okstra-trace-cleanup.sh` | `--reclaim-completed` 모드(완료 worker pane 만) 추가 |
|
|
135
|
+
| `scripts/okstra_ctl/*` (run 시작 경로) | `~/.okstra/sessions/<session_id>.run` 매핑 기록 |
|
|
136
|
+
| `prompts/profiles/_common-contract.md` | ④ 내성 계약 명시(pane-실패 → in-process 대체) |
|
|
137
|
+
| `src/install.mjs` 관련 시드 | 신규 스크립트/훅이 설치 payload 에 포함되는지 확인 |
|
|
138
|
+
|
|
139
|
+
## 8. 테스트 전략
|
|
140
|
+
|
|
141
|
+
- `okstra-trace-cleanup.sh --reclaim-completed` 단위: result_path 존재/부재로 진행 중 worker pane 보존 vs 완료 pane 회수를 가르는지(태그 fixture 기반, tmux 없는 환경에서 list 모드로 검증).
|
|
142
|
+
- session↔run-dir 매핑 기록/조회 단위.
|
|
143
|
+
- 훅 진입점: stdin JSON(`session_id`) 파싱 → 매핑 부재 시 no-op, 존재 시 올바른 RUN_DIR 전달.
|
|
144
|
+
- e2e: 모의 worker 산출물 + 태그 pane 으로 "완료 즉시 회수, 진행 중 보존" 시나리오(tmux 가용 시).
|
|
145
|
+
|
|
146
|
+
## 9. 미해결 / 리스크
|
|
147
|
+
|
|
148
|
+
- **~~teammate pane title 회수 가능성~~ → 해소(2026-06-18).** §6 구현 후 결론 참조: hook 경로의 teammate pane 회수는 구조적으로 불가(generic title + per-worker 태그 부재). teammate pane 은 lead 의 batch-경계 phase-start cleanup 이 회수하며, 그 단계는 아래 de-gate 로 항상 실행된다.
|
|
149
|
+
- **~~한 세션 다중 run~~ → 무효(미채택).** 구현은 `sessions/<id>.run` 세션 매핑(③) 대신 `okstra-subagent-reclaim.sh` 가 `active.jsonl` 의 활성 run 전체를 순회하고 per-pane 완료 판정(`pane_reclaim`)으로 cross-run 안전을 보장하는 방식을 택했다 — session_id 매핑이 없으므로 이 리스크는 발생하지 않는다(§5 데이터 흐름의 session 매핑 서술은 미구현). 동시 병렬 run 도 완료 판정이 막아준다.
|
|
150
|
+
- **④ 내성은 코드 강제 불가** — prompt 계약이라 lead 가 따르지 않으면 무효(이번 원인과 동일 계열). **보강(2026-06-18, `b2278a6`):** phase-start cleanup / wrap-up disposition 의 lead-pane.id 해석 게이트를 제거해 lead 가 cleanup 스크립트를 *항상* 호출하도록 바꿨다(스크립트가 tmux 밖이면 자가 no-op). 이로써 "lead 가 lead-pane.id 를 오판해 정리를 통째로 skip" 하는 이번 원인 계열이 일차 방어선(①②)에서 제거된다. 내성(④)은 그래도 한도에 닿는 경우의 최후 안전망으로 남는다.
|