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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.88.2",
3
+ "version": "0.90.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.88.2",
3
- "builtAt": "2026-06-17T07:14:48.667Z",
2
+ "package": "0.90.0",
3
+ "builtAt": "2026-06-17T18:09:19.361Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -184,7 +184,43 @@ fabricate a `timeout` status against a missing artifact.
184
184
 
185
185
  ---
186
186
 
187
+ ## 항목 X1 — cross-repo carry packet 소비 자동화 (okstra-brief) [미착수, 2026-06-17]
188
+
189
+ ### 배경
190
+
191
+ cross-repo 작업은 구조적으로 repo 별 별도 okstra run 이 필수다(worktree 단일 repo 바인딩 · run-index/registry 단일 project 키 · 편집 allowlist 가 해당 트리로 제한). **생산자 계약은 이미 구현됨** — implementation-planning 계획서가 `## Cross-Repo Carry — <repo>` 부록을 세 하위 섹션(Preconditions / Work for B / How to start)으로 방출한다([prompts/profiles/implementation-planning.md](../prompts/profiles/implementation-planning.md), 커밋 `cfd7c2c`).
192
+
193
+ **소비자 쪽은 현재 수동**이다: B repo 에서 `okstra-brief` → variant `Reporter input` → source `File` 로 A 계획서 절대경로를 붙여넣으면 **파일 전체가 verbatim 으로** Source Material 에 들어가([skills/okstra-brief/SKILL.md:147](../skills/okstra-brief/SKILL.md), [SKILL.md:162-164](../skills/okstra-brief/SKILL.md)), B 의 planning 이 A 자신의 stage·decision 노이즈에서 "B 가 할 일 / precondition" 을 직접 골라내야 한다.
194
+
195
+ ### 목표
196
+
197
+ B 의 `okstra-brief` 가 A 계획서에서 **`## Cross-Repo Carry — <repo>` 섹션만 추출**해 구조화 시드: `Work for B` → B 의 requirements(R-ID), `Preconditions` → B 의 외부 의존 + 관측 확인 신호(구현 대상 아님), recognition caveat 동반.
198
+
199
+ ### 변경 대상 파일
200
+
201
+ - [skills/okstra-brief/SKILL.md](../skills/okstra-brief/SKILL.md) — 신규 brief variant(또는 `File` 소스가 `## Cross-Repo Carry` 섹션을 감지하면 자동 분기) + 섹션 추출 단계.
202
+ - (선택) `## Cross-Repo Carry — <repo>` 파싱 헬퍼 — 순수 마크다운 파싱. 스킬·CLI 가 같이 쓰면 단일 참조점으로 `scripts/okstra_ctl/` Python 모듈에 둔다.
203
+
204
+ ### 결정해야 할 것
205
+
206
+ 1. 신규 brief variant 로 노출할지 vs `File` 소스 자동 감지로 처리할지.
207
+ 2. A 계획서를 수동 경로 입력 vs 최근 run 자동 추천(`okstra task-list`)으로 찾을지.
208
+ 3. `Preconditions` 를 B brief 의 어느 섹션(Open Questions / 외부 의존 전용 섹션)으로 매핑할지.
209
+
210
+ ### 불변 제약 (자동화해도 반드시 유지)
211
+
212
+ - B 는 자기 권한·worktree 로 setup/brief/run 을 실행 — 자동화가 "A 가 B 에 쓰기" 를 허용하면 안 됨.
213
+ - 경계를 넘는 읽기는 B 가 A 경로를 **명시 인용**하는 형태만(artifact-home, [SKILL.md:90-92](../skills/okstra-brief/SKILL.md)).
214
+ - A 의 done-state 는 B 게이트가 자동 인정하지 않음 — 자동화는 carry 를 구조화할 뿐 B 의 독립성은 그대로.
215
+
216
+ ### 결정
217
+
218
+ 2026-06-17 — 사용자가 당분간 수동 흐름 유지를 선택. 자동화는 본 항목으로 보류. 진행 전 brainstorming 으로 위 3개 결정 사항을 확정한다.
219
+
220
+ ---
221
+
187
222
  ## 변경 이력
188
223
 
189
224
  - 2026-05-03 — 작성. 수정 A(Claude worker Stop Condition) 동시 머지에 따른 후속 항목 기록.
190
225
  - 2026-06-10 — 수정 B(Leader-side 워커 soft timeout) 및 수정 C 후보(Phase 4 background polling / worker-results 파일 폴링 루프)를 self-scheduled polling 작업으로 해소 표기. 정본: okstra-team-contract "Worker-completion detection (self-scheduled polling)" 섹션, 설계: docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md.
226
+ - 2026-06-17 — 항목 X1(cross-repo carry packet 소비 자동화) 기록. 생산자 계약은 커밋 `cfd7c2c` 로 구현됨, 소비자(okstra-brief 자동 추출)는 사용자 선택으로 보류.
@@ -218,6 +218,7 @@ if [[ -n "$caller_pane" ]]; then
218
218
  # Tag with this run's dir for `okstra-trace-cleanup.sh --run-dir`. See
219
219
  # `okstra-codex-exec.sh` for the rationale — kept in lock-step.
220
220
  [[ -n "$run_dir" ]] && tmux set-option -p -t "$trace_pane" @okstra_trace_run "$run_dir" 2>/dev/null || true
221
+ [[ -n "$status_path" ]] && tmux set-option -p -t "$trace_pane" @okstra_status "$status_path" 2>/dev/null || true
221
222
  tmux last-pane 2>/dev/null || true
222
223
  fi
223
224
  fi
@@ -280,6 +280,7 @@ if [[ -n "$caller_pane" ]]; then
280
280
  # assumption. The run-scoped tag also stops concurrent okstra runs from
281
281
  # stomping each other's trace panes.
282
282
  [[ -n "$run_dir" ]] && tmux set-option -p -t "$trace_pane" @okstra_trace_run "$run_dir" 2>/dev/null || true
283
+ [[ -n "$status_path" ]] && tmux set-option -p -t "$trace_pane" @okstra_status "$status_path" 2>/dev/null || true
283
284
  tmux last-pane 2>/dev/null || true
284
285
  fi
285
286
  fi
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # okstra-subagent-reclaim.sh — SubagentStop / TaskCompleted 훅 엔트리.
4
+ #
5
+ # stdin(JSON)은 읽고 버린다: 어느 worker 가 끝났든 활성 run 전체의 "완료" pane 만
6
+ # 회수하므로 session_id 매핑이 필요 없다. active.jsonl 의 각 활성 run 에 대해
7
+ # okstra-trace-cleanup.sh --reclaim-completed 를 호출한다 — 완료 판정(pane_reclaim)이
8
+ # cross-run 안전을 보장하므로 다른 run 의 진행 중 pane 은 건드리지 않는다.
9
+ #
10
+ # 훅은 세션 진행을 절대 막지 않는다: 모든 실패는 조용히 삼키고 항상 exit 0.
11
+ set -u
12
+
13
+ cat >/dev/null 2>&1 || true # drain stdin
14
+
15
+ _dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
16
+ home="${OKSTRA_HOME:-$HOME/.okstra}"
17
+ # pane_reclaim 패키지는 repo 레이아웃(scripts/okstra_ctl)과 설치 레이아웃
18
+ # ($OKSTRA_HOME/lib/python) 양쪽에 있을 수 있으므로 둘 다 PYTHONPATH 에 둔다.
19
+ export PYTHONPATH="${_dir}:${home}/lib/python${PYTHONPATH:+:$PYTHONPATH}"
20
+
21
+ while IFS= read -r run_dir; do
22
+ [[ -n "$run_dir" ]] || continue
23
+ "$_dir/okstra-trace-cleanup.sh" --reclaim-completed --run-dir "$run_dir" >/dev/null 2>&1 || true
24
+ done < <(python3 -m okstra_ctl.pane_reclaim --active-dirs "$home" 2>/dev/null || true)
25
+
26
+ exit 0
@@ -46,24 +46,37 @@ set -u
46
46
  _clean_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
47
47
  [ -r "$_clean_script_dir/lib/okstra/tmux-pane.sh" ] && . "$_clean_script_dir/lib/okstra/tmux-pane.sh"
48
48
 
49
+ # --reclaim-completed shells out to `python3 -m okstra_ctl.pane_reclaim`. The
50
+ # package is a bin-sibling in the repo layout (scripts/okstra_ctl) and under
51
+ # $OKSTRA_HOME/lib/python in the installed layout. Put both on PYTHONPATH so the
52
+ # import resolves regardless of where this script runs from.
53
+ _okstra_home="${OKSTRA_HOME:-$HOME/.okstra}"
54
+ export PYTHONPATH="${_clean_script_dir}:${_okstra_home}/lib/python${PYTHONPATH:+:$PYTHONPATH}"
55
+
49
56
  MODE="kill" # kill | list
57
+ RECLAIM=0 # 1: trace pane 은 @okstra_status 가 완료(exited)일 때만 회수 (--reclaim-completed)
50
58
  REAP=0
51
59
  run_dir=""
52
60
  while [[ $# -gt 0 ]]; do
53
61
  case "$1" in
54
- --list|--dry-run) MODE="list" ;;
55
- --reap) REAP=1 ;;
62
+ --list|--dry-run) MODE="list" ;;
63
+ --reclaim-completed) RECLAIM=1 ;;
64
+ --reap) REAP=1 ;;
56
65
  --run-dir) shift; run_dir="${1-}" ;;
57
66
  --run-dir=*) run_dir="${1#--run-dir=}" ;;
58
67
  -h|--help)
59
68
  cat <<'USAGE'
60
- usage: okstra-trace-cleanup.sh (--run-dir <RUN_DIR> [--list] | --reap)
69
+ usage: okstra-trace-cleanup.sh (--run-dir <RUN_DIR> [--list] [--reclaim-completed] | --reap)
61
70
 
62
- --run-dir okstra run directory; closes that run's trace + worker-agent panes.
63
- --list with --run-dir: print "<pane_id>\t<pane_title>" per pane; no kill.
64
- --dry-run alias for --list.
65
- --reap close every okstra trace pane under $CLAUDE_PROJECT_DIR/.okstra
66
- (SessionEnd hook; no single run-dir applies).
71
+ --run-dir okstra run directory; closes that run's trace + worker-agent panes.
72
+ --list with --run-dir: print "<pane_id>\t<pane_title>" per pane; no kill.
73
+ --dry-run alias for --list.
74
+ --reclaim-completed with --run-dir: restrict trace panes to those whose
75
+ @okstra_status sidecar is terminal (stage=exited); in-flight
76
+ and teammate panes are preserved. Skips the title-allowlist
77
+ teammate scan. Combinable with --list.
78
+ --reap close every okstra trace pane under $CLAUDE_PROJECT_DIR/.okstra
79
+ (SessionEnd hook; no single run-dir applies).
67
80
  USAGE
68
81
  exit 0 ;;
69
82
  *)
@@ -117,24 +130,39 @@ _tag_in_scope() {
117
130
 
118
131
  collect_okstra_panes() {
119
132
  local -a panes=()
120
- local pid trace_tag worker_tag title
133
+ local pid trace_tag worker_tag status_tag title
121
134
 
122
135
  # (1) Trace and worker-compute panes tagged in scope — found server-wide by
123
- # tag, so no tmux env var or pane-id registry is needed.
124
- while IFS=$'\t' read -r pid trace_tag worker_tag; do
136
+ # tag, so no tmux env var or pane-id registry is needed. Each `@okstra_*`
137
+ # column carries a leading `x` sentinel (stripped after read): tmux's `-F`
138
+ # drops an UNSET user option AND its adjacent tab, which would otherwise
139
+ # shift later columns left (e.g. an empty worker tag stealing the status
140
+ # value). The sentinel keeps every column present so positional parsing holds.
141
+ # `#x` strip 은 실제 태그 값을 깎지 않는다: 모든 태그 값(trace_run/worker_run/
142
+ # status)은 절대경로라 `/` 로 시작 → `x` 로 시작하는 일이 없어 sentinel 만 벗겨진다.
143
+ while IFS=$'\t' read -r pid trace_tag worker_tag status_tag; do
144
+ trace_tag="${trace_tag#x}"; worker_tag="${worker_tag#x}"; status_tag="${status_tag#x}"
125
145
  [[ -n "$pid" ]] || continue
126
146
  [[ "$pid" == "$lead_pane" ]] && continue
127
147
  if _tag_in_scope "$trace_tag" || _tag_in_scope "$worker_tag"; then
148
+ if [[ "$RECLAIM" -eq 1 ]]; then
149
+ # reclaim 모드: 완료(stage=exited)된 worker 의 pane 만. status 태그가
150
+ # 없거나(teammate/비-wrapper pane) 미완료면 보존.
151
+ [[ -n "$status_tag" ]] || continue
152
+ python3 -m okstra_ctl.pane_reclaim "$status_tag" || continue
153
+ fi
128
154
  panes+=("$pid")
129
155
  fi
130
156
  done < <(tmux list-panes -a \
131
- -F '#{pane_id}'$'\t''#{@okstra_trace_run}'$'\t''#{@okstra_worker_run}' \
157
+ -F '#{pane_id}'$'\t''x#{@okstra_trace_run}'$'\t''x#{@okstra_worker_run}'$'\t''x#{@okstra_status}' \
132
158
  2>/dev/null || true)
133
159
  # (2) Title-allowlisted worker-agent panes in the lead's session. Only for a
134
160
  # run (reap leaves these harness-owned panes to the harness). `list-panes -s
135
161
  # -t <pane>` resolves the session containing that pane, so the scan never
136
162
  # reaches other sessions (no `-a`). Skipped when the lead pane is unknown.
137
- if [[ "$REAP" -eq 0 && -n "$lead_pane" ]]; then
163
+ # reclaim 모드는 teammate pane 회수하지 않으므로(완료 판정 불가, trace-only)
164
+ # 이 스캔을 건너뛴다.
165
+ if [[ "$REAP" -eq 0 && "$RECLAIM" -eq 0 && -n "$lead_pane" ]]; then
138
166
  while IFS=$'\t' read -r pid title; do
139
167
  [[ -n "$pid" ]] || continue
140
168
  [[ "$pid" == "$lead_pane" ]] && continue
@@ -208,7 +208,7 @@ These phases are governed by [team-contract](./team-contract.md). It is the cano
208
208
 
209
209
  0. **Surface the Teams tools first (deferred-tool environments) — BLOCKING.** `TeamCreate` / `TeamDelete` / `SendMessage` / `TaskList` are commonly **deferred tools**: present by name but not callable until their schemas are loaded via ToolSearch. Before step 1, load them with the explicit selector form `ToolSearch("select:TeamCreate,TeamDelete,SendMessage,TaskList")` — NOT a keyword query. Keyword-search ranking is non-deterministic while MCP servers are still (re)connecting (this is why the failure surfaces on `implementation` entry, after a context compaction re-injects the deferred-tool list mid-reconnect, far more than on earlier phases), so a keyword miss is NOT evidence the environment lacks Agent Teams. If the `select:` result comes back without `TeamCreate`, retry the same `select:` call ONCE. A non-empty result means the tool exists — proceed to step 1 and let the actual `TeamCreate` return value decide ok/error. Only when the explicit `select:` for `TeamCreate` is empty on BOTH attempts may you treat the environment as lacking Agent Teams support and go to step 5.
210
210
  1. Call `TeamCreate(team_name: ..., description: "Lead-plus-worker okstra run for <task-key>")`. The team name comes verbatim from the launch prompt's Team Creation Gate block — `okstra-<task-key>`, and implementation stage runs append `-s<N>` (stage isolation: a leftover team from another stage of the same task must not collide).
211
- 2. Record the `TeamCreate` outcome in team-state under `teamCreate: { attempted: true, status: "ok"|"error", error?: <message> }` AND the exact name as `teamName` before any dispatch. This is the audit trail that justifies a later no-`team_name` fallback, and `teamName` is the SSOT every later consumer (teardown, reconcile, token collector) reads.
211
+ 2. Record the `TeamCreate` outcome in team-state under `teamCreate: { attempted: true, status: "ok"|"error", error?: <message> }` AND the exact name as `teamName` before any dispatch. This is the audit trail that justifies a later no-`team_name` fallback. `teamName` is only an audit/display label — the harness never creates a `~/.claude/teams/<teamName>/` directory (the on-disk team is `session-<leadSessionId-prefix>`), so run-end teardown and reconcile resolve the real team from disk via `lead.sessionId`, NOT from `teamName` (see `_common-contract.md` "Run-end teammate teardown").
212
212
  2-1. If `TeamCreate` fails with "team already exists" (stale leftover from an earlier attempt): call `TeamList`; if the team is listed in this session, `TeamDelete` it and retry step 1 once. If it is NOT listed, do NOT remove `~/.claude/teams/...` / `~/.claude/tasks/...` with shell commands on your own initiative — that is destructive harness-internal state and `rm -rf` is commonly denied by user permission rules. Ask the user via AskUserQuestion (recommended option: quarantine); on approval, move both dirs into `~/.okstra/trash/<UTC-timestamp>/` with `mv` (reversible), then retry step 1 once.
213
213
  3. Verify `team-state.lead.sessionId` is populated. The `okstra.sh` exec path fills it automatically (`generate_claude_session_id` → `claude --session-id ...`). The render-only / in-session takeover path (`okstra-run` skill) auto-detects the live session's jsonl via `resolve_inproc_lead_session_id`, but the detector is best-effort and may return empty if `~/.claude/projects/<encoded-cwd>/` is unreadable or has no jsonl yet. If `lead.sessionId` is empty at this point, write the running session's id into team-state before proceeding — Phase 7 token-usage collection depends on it and will fail with `lead jsonl not found (sessionId=)` otherwise.
214
214
  4. If `TeamCreate` succeeds, proceed to Phase 4 (dispatch with `team_name`).
@@ -16,6 +16,7 @@ profile document.
16
16
  - **Phase 4 / 5 (independent analysis)**: analyser workers (`claude`, `codex`, `antigravity` when opted in) produce findings independently and have no access to one another's outputs. `report-writer` does not analyse.
17
17
  - **Phase 5.5 (convergence — peer review by workers)**: the lead replays each analyser's findings to the *other* analysers and collects `AGREE` / `DISAGREE` / `SUPPLEMENT` verdicts across up to `effectiveMaxRounds` rounds. Workers act as peer reviewers of each other's findings in this phase; the lead mediates but does not vote. See `prompts/lead/convergence.md` for the round protocol, queue invariants, and final classification (`full-consensus` / `partial-consensus` / `contested` / `worker-unique`). For `requirements-discovery`, `error-analysis`, and `implementation-planning` this phase runs in **adversarial mode** (`convergence.adversarial=true`): verifiers try to refute each finding against its cited evidence and the burden of proof sits on the claim — see that skill's §"Adversarial Verification Mode".
18
18
  - Do NOT conclude "no peer review happens" from the roster alone — every profile that lists ≥2 analyser workers runs convergence by default (`convergence.enabled=true` in `task-manifest.json`).
19
+ - **Pane-budget fallback (tolerance).** If a worker dispatch is rejected with `no room for another tmux split` (or an equivalent teammate-pane creation failure), the lead retries that worker in-process without `run_in_background`, or — if it was an external CLI worker — **substitutes** an in-process `claude` analysis, and records that fact (provider unavailable) in the run log and the convergence notes. Completed external-CLI worker trace panes are reclaimed automatically by the `SubagentStop` hook, but okstra cannot directly reclaim the teammate panes the harness creates, so this fallback is the last line of defence against pane-budget exhaustion. (This is a prompt instruction, not a code-enforced gate.)
19
20
  - Tooling — read-only MCP availability (shared):
20
21
  - MCP is not implicit okstra context. Query an MCP server only when the task brief explicitly lists it as source material for this run. Any MCP-derived finding MUST cite server, table, and the SELECT used. MCP MUST NEVER be used as a write path — schema/data mutations go through repository migration files reviewed by humans.
21
22
  - Resource boundary (shared — artifact-home rule):
@@ -30,16 +31,19 @@ profile document.
30
31
  - Anti-escalation rule (shared):
31
32
  - 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.
32
33
  - Run-start pane recording (shared — runs ONCE at run start, before the FIRST worker dispatch):
33
- - The codex/antigravity wrappers now self-anchor their trace pane by walking their own ancestor PIDs against tmux `pane_pid`s (see `lib/okstra/tmux-pane.sh`), so they no longer depend on this file. The lead still records its own pane id here for the cleanup steps below (which-pane-to-never-kill) and as the "am I in tmux" gate. A bare `tmux display-message -p '#{pane_id}'` is NOT reliable for this — Claude Code's Bash tool strips `$TMUX`/`$TMUX_PANE`, so that command returns the most-recently-active *client's* pane (often a different session, or a foreign pane when the lead is launched outside tmux entirely). The lead therefore records via the same ancestry resolver.
34
- - The lead MUST run once, at run start: `mkdir -p "<RUN_DIR>/state" && { . "$HOME/.okstra/bin/lib/okstra/tmux-pane.sh" 2>/dev/null && okstra_resolve_caller_pane; } > "<RUN_DIR>/state/lead-pane.id" 2>/dev/null || true` (substitute the run's absolute `RUN_DIR`). When the lead is not inside a tmux pane (e.g. Claude launched from the GUI app) no ancestor matches a pane, the file is empty, and every pane step below silently no-ops that empty/absent file is the single signal that the lead is not in tmux.
35
- - This recording is **silent internal setup**: the lead MUST emit NOTHING to the user about it — no pane id, no `%NNN`, no `lead-pane.id` path, no announcement that the phase-start-reset / wrap-up-disposition steps "activate". Just record the file and proceed.
36
- - Phase-start pane reset (shared — runs BEFORE dispatching each new worker batch):
37
- - okstra creates two kinds of tmux pane per run: (a) **worker-agent panes** the harness gives to dispatched subagents (titled `claude-worker` / `codex-worker` / `antigravity-worker` / `report-writer-worker`), and (b) **trace panes** the codex/antigravity wrappers spawn (`<cli>-<role>-<pid>-tail`). Both accumulate across internal phases because each new phase dispatches a fresh worker batch and the prior panes are never reclaimed.
38
- - When `<RUN_DIR>/state/lead-pane.id` is non-empty (the lead is in tmux), the lead MUST run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"` **immediately before** dispatching the next phase's workersi.e. just before emitting each `PROGRESS: phase-5.5-convergence round=<N>` marker and just before `PROGRESS: phase-6-synthesis dispatching report-writer-worker`. This closes every prior-phase okstra pane (worker-agent + trace) for this run, while NEVER killing the lead's own pane.
39
- - This is **automatic and silent** NO user prompt. Report it in one short, plain-language line (e.g. `이전 phase okstra pane 3개 정리`) and proceed never expose internal identifiers (`%NNN`, `lead-pane.id`, step names like "phase-start pane reset"). It is silent-skipped when the lead is not in tmux; the lead MUST NOT fabricate a synthetic pane list in that case.
34
+ - The codex/antigravity wrappers now self-anchor their trace pane by walking their own ancestor PIDs against tmux `pane_pid`s (see `lib/okstra/tmux-pane.sh`), so they no longer depend on this file. The lead still records its own pane id here so the cleanup script below can know which pane to never kill and can detect tmux absence itself — the lead does NOT read it back to gate those steps. A bare `tmux display-message -p '#{pane_id}'` is NOT reliable for this — Claude Code's Bash tool strips `$TMUX`/`$TMUX_PANE`, so that command returns the most-recently-active *client's* pane (often a different session, or a foreign pane when the lead is launched outside tmux entirely). The lead therefore records via the same ancestry resolver.
35
+ - The lead MUST run once, at run start: `mkdir -p "<RUN_DIR>/state" && { . "$HOME/.okstra/bin/lib/okstra/tmux-pane.sh" 2>/dev/null && okstra_resolve_caller_pane; } > "<RUN_DIR>/state/lead-pane.id" 2>/dev/null || true` (substitute the run's absolute `RUN_DIR`). When the lead is not inside a tmux pane (e.g. Claude launched from the GUI app) no ancestor matches a pane and the file is empty. The cleanup steps below do NOT read this file to decide whether to run — they always run, and the script itself reads `lead-pane.id` (with its own ancestry fallback) only to know which pane to never kill and to detect tmux absence. The lead never interprets this file to gate a pane step.
36
+ - This recording is **silent internal setup**: the lead MUST emit NOTHING to the user about it — no pane id, no `%NNN`, no `lead-pane.id` path, no announcement of the phase-start-cleanup / wrap-up-disposition steps. Just record the file and proceed.
37
+ - Phase-start cleanup prior-batch panes + completed teammates (shared — runs BEFORE dispatching each new worker batch, at the SAME boundaries: just before each `PROGRESS: phase-5.5-convergence round=<N>` marker and just before `PROGRESS: phase-6-synthesis dispatching report-writer-worker`):
38
+ - **(a) tmux panes.** okstra creates two kinds of tmux pane per run: **worker-agent panes** the harness gives to dispatched subagents (titled `claude-worker` / `codex-worker` / `antigravity-worker` / `report-writer-worker`), and **trace panes** the codex/antigravity wrappers spawn (`<cli>-<role>-<pid>-tail`). Both accumulate because each new batch spawns fresh panes and the prior ones are never reclaimed. The lead MUST ALWAYS run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"` immediately before dispatching the next batch — NEVER gating this call on its own reading of `lead-pane.id`. The script self-resolves the lead pane and is a safe no-op outside tmux (it returns an empty pane list), so an always-run call cannot do harm; it closes every prior-batch okstra pane (worker-agent + trace) for this run while NEVER killing the lead's own pane. The lead MUST NOT fabricate a synthetic pane list.
39
+ - **(b) completed teammates (Teams mode only).** Each convergence round / critic / gap-verification / report-writer dispatch spawns teammates under fresh Agent `name`s (`<role>-reverify-r<N>`, `<provider>-worker-critic`, `report-writer-worker`, …); completed teammates do NOT leave the FleetView roster on their own — they linger as idle pills until run-end teardown, so a long run's roster grows unreadable. Before dispatching the next batch, the lead MUST send `SendMessage(to: <name>, message: { type: "shutdown_request" })` the `message` MUST be the object literal shown, NEVER a JSON string stuffed into a text field to every teammate it spawned earlier in THIS run **whose completion is already confirmed** (its Result Path exists / it is in the self-scheduled-polling done set). This is the same graceful-shutdown primitive used at `Run-end teammate teardown`, applied per batch instead of once at the end.
40
+ - **Only confirmed-complete teammates, never the lead.** NEVER send a shutdown_request to a teammate whose result file has not yet appeared. In particular the critic runs CONCURRENTLY with the first reverify round (see `convergence` "Coverage critic pass"), so it MUST NOT be shut down until its own result is collected. NEVER target the lead's own session.
41
+ - **Do NOT call `TeamDelete` here.** The team stays alive for the remaining batches; only individual completed teammates are dismissed. `TeamDelete` remains a run-end-only step (`Run-end teammate teardown`).
42
+ - Silent-skip entirely in the no-`team_name` fallback (background subagents never join the roster) and in `tmux-pane` backend runs (external-lead workers are not session teammates).
43
+ - Both (a) and (b) are **automatic and silent** — NO user prompt. Report the pair in one short, plain-language line (e.g. `이전 배치 okstra pane 3개 · teammate 2명 정리`) and proceed — never expose internal identifiers (`%NNN`, `lead-pane.id`, raw Agent names, step names like "phase-start cleanup"). **Effect caveat:** Claude Code exposes no tool to surgically delete a roster entry mid-run, so a dismissed teammate may still show as an idle pill; the shutdown still reclaims its session and prevents the roster from growing with live members. On the FIRST batch of a run nothing has been dispatched yet — both steps no-op.
40
44
  - Phase wrap-up — okstra pane disposition (shared, runs AFTER Phase 7 persistence/token collection and BEFORE teammate teardown):
41
45
  - At run end the only residual okstra panes are the LAST phase's (e.g. the `report-writer-worker` agent pane and any codex/antigravity trace pane). `okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>"` returns one tab-separated `<pane_id>\t<pane_title>` line per residual okstra pane (worker-agent + trace) for this run.
42
- - When `<RUN_DIR>/state/lead-pane.id` is non-empty, 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 --run-dir "<RUN_DIR>"` exactly once. The output lists every residual okstra pane (worker-agent + trace) for this run, never the lead's own pane.
46
+ - After the final-report file has been written and the routing recommendation has been issued, the lead MUST ALWAYS run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>"` exactly once — NEVER gating this call on its own reading of `lead-pane.id`. The output lists every residual okstra pane (worker-agent + trace) for this run, never the lead's own pane; outside tmux it is a safe no-op (empty output).
43
47
  - If the list is empty, skip the question — there is nothing to ask about (the phase-start resets above usually already cleared prior phases).
44
48
  - 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):
45
49
  > 현재 phase 종료 시점입니다. 다음 okstra pane 이 열려 있습니다 — 닫을까요?
@@ -48,16 +52,18 @@ profile document.
48
52
  - On `예` / `y` / `close` → run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"` and report the kill count back in one sentence.
49
53
  - On `아니오` / `n` / `keep` → leave the panes intact; remind the user that they will be cleaned up automatically when Claude `/exit` fires the `SessionEnd` hook (`--reap`).
50
54
  - 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.
51
- - This step is mandatory for every phase (`requirements-discovery`, `error-analysis`, `implementation-planning`, `implementation`, `final-verification`, `release-handoff`). It is silent-skipped when `<RUN_DIR>/state/lead-pane.id` is empty/absent (lead running outside tmux); the lead MUST NOT fabricate a synthetic pane list in that case.
55
+ - This step is mandatory for every phase (`requirements-discovery`, `error-analysis`, `implementation-planning`, `implementation`, `final-verification`, `release-handoff`). Whether there is anything to ask about is decided by the `--list` output, NOT by the lead's reading of `lead-pane.id`: an empty list skip the question (above); the lead MUST NOT fabricate a synthetic pane list.
52
56
  - Run-end teammate teardown (shared — MUST run AFTER the pane disposition question and BEFORE `PROGRESS: complete`):
53
57
  - The lead created the worker team in Phase 3 (`TeamCreate` with the name recorded as team-state `teamName` — `okstra-<task-key>`, implementation stage runs `okstra-<task-key>-s<N>`). Worker teammates are NOT reclaimed on their own — without an explicit teardown they linger in the FleetView roster across this and later runs in the session. A lingering team also makes the next run of the same task-key collide on `TeamCreate` ("team already exists"), forcing the stale-team recovery path.
54
- - This step applies only when team-state's `teamCreate.status == "ok"` (Teams mode was actually used). In the no-`team_name` fallbackwhether `teamCreate.status` is `"error"` or `"skipped"` (`reason: "concurrent-run"`) there is no team to delete, so silent-skip.
58
+ - **Whether to run this step is decided by on-disk fact, NOT by `teamCreate.status` alone.** A mid-run context compaction can make the lead wrongly conclude Agent Teams is unavailable and record `teamCreate.status: "error"` even though `TeamCreate` had succeeded and workers joined so at run-end `status` is not a trustworthy "is there a team" signal. The harness names the team directory `session-<leadSessionId-prefix>` (NOT `okstra-<task-key>`; the `team_name` passed to `TeamCreate` is a logical label that never becomes a directory name). Resolve existence from disk: find the `~/.claude/teams/session-*/config.json` whose `leadSessionId` equals team-state `lead.sessionId`.
59
+ - **Matching on-disk config found, with members beyond the lead** → a real team exists; run the teardown sequence below regardless of what `teamCreate.status` says.
60
+ - **No matching on-disk config** → there is genuinely nothing to delete (the real no-`team_name` / concurrent-run case) → silent-skip.
55
61
  - After the pane disposition step above, the lead MUST ask the user whether to disband the worker teammates. This is a strict binary choice:
56
62
  > worker teammate 팀 `<teamName>` 을 해제할까요?
57
63
  > (예) 팀원 정리 / (아니오) 그대로 두기
58
64
  - On `아니오` / `n` / `keep` → do not call `TeamDelete()`. Tell the user that the team will remain visible in FleetView/Teams and can be removed later from the Teams UI, or by asking the lead in this thread to clean up `<teamName>` — and that a later run of the same task-key/stage will hit a "team already exists" collision until it is removed.
59
- - On `예` / `y` / `cleanup` → emit `PROGRESS: phase-7-teardown disbanding team`, then run the sequence below. Token-usage collection MUST already be complete — `TeamDelete` removes `~/.claude/teams/<team>/` + `~/.claude/tasks/<team>/` but NOT the `~/.claude/projects/` jsonls Phase 7 reads, yet the read MUST precede teardown.
60
- 1. Run `$HOME/.okstra/bin/okstra-team-reconcile.sh "<teamName>"` (the team-state `teamName`) exactly once. It flips dead-pane stale-active members to inactive, and no-ops when tmux is unavailable or nothing is stale. Do NOT loop it.
65
+ - On `예` / `y` / `cleanup` → emit `PROGRESS: phase-7-teardown disbanding team`, then run the sequence below. Token-usage collection MUST already be complete — `TeamDelete` takes no argument and removes the current session's team directory (`~/.claude/teams/session-<leadSessionId-prefix>/` + the matching `~/.claude/tasks/...`) but NOT the `~/.claude/projects/` jsonls Phase 7 reads, yet the read MUST precede teardown.
66
+ 1. Run `$HOME/.okstra/bin/okstra-team-reconcile.sh "<resolved-config-path>"` the on-disk `~/.claude/teams/session-<…>/config.json` resolved in the existence check above, NOT team-state `teamName` (which resolves to a non-existent `~/.claude/teams/okstra-<task-key>/` directory and silently makes reconcile a no-op, leaving dead-pane stale-active members that then block `TeamDelete`) exactly once. It flips dead-pane stale-active members to inactive, and no-ops when tmux is unavailable or nothing is stale. Do NOT loop it.
61
67
  2. Call `TeamDelete()` — the single synchronization point for teammate teardown. If it errors with an active-members message, send each named active member a structured `SendMessage(to: <name>, message: { type: "shutdown_request" })` — the `message` MUST be the object literal shown, NEVER a JSON string stuffed into a text field — wait briefly, run `okstra-team-reconcile.sh` one more time, retry `TeamDelete()` once, and proceed regardless of the result. NEVER loop and never use `TaskStop` (teammates are not background tasks — `TaskStop` 404s on a member address).
62
68
  - If `TeamDelete()` is unavailable in the current host, or teardown still fails after the single retry, do not pretend cleanup succeeded. Report the exact residual action instead: `Teams/FleetView 에서 team <teamName> 삭제`, and if tmux panes were kept earlier also show the pane command `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"`.
63
69
  - Report successful teardown in one short line (e.g. `worker teammate 팀 해제`, or `stale 멤버 1명 reconcile 후 팀 해제`) and proceed to the final `PROGRESS: complete ...` line.
@@ -66,6 +66,8 @@
66
66
  - trade-off matrix across options (rows = options, columns at minimum: complexity, risk, reversibility, test coverage cost, rollout cost)
67
67
  - recommended option with rationale tied to the design principles above
68
68
  - **Stage Map (mandatory — always emitted, even when N=1):** a table of all stages with `stage | title | depends-on | step-count | exit-contract-summary`. `depends-on` is `(none)` or a comma-separated stage number list. Stages with `depends-on (none)` can be implemented in parallel by two simultaneous `implementation` runs.
69
+ - **Keep the table at exactly 5 columns** — do NOT add a column. `validators/validate-implementation-plan-stages.py` parses `stage | title | depends-on | step-count | exit-contract-summary` and silently skips any row that is not exactly 5 cells, so a 6th column would drop every stage and bypass S2–S11.
70
+ - **Multi-project plans only** (the plan's work spans more than one project — see the Project-boundary partition rule below): prefix each stage's `title` cell with a `[<project>]` tag (e.g. `[okstra] Add X`) so the project each stage belongs to is readable at a glance, and add exactly one line directly under the Stage Map table — `Cross-project parallelism: <which per-project stages run in parallel, which are sequenced, and the cross-project dependency that forces each sequencing>`. Single-project plans omit both the tag and the line.
69
71
  - **Per-stage slice declaration (mandatory lines, directly under the `## 5.5.<i> Stage <i>:` heading, before `### Carry-In`):**
70
72
  - `Slice value: <the one user-observable increment this stage delivers, end-to-end>` — describe WHAT starts working from the consumer's view (e.g. "X 를 조회하면 Y 가 반환된다"), NOT a layer name ("repository 추가"). Validator S10a rejects a missing/empty value.
71
73
  - `Acceptance: <the observable pass condition or the exact command>` — the signal that proves the slice is done; normally the same test command that the `RED:` step below flips to PASS. Validator S10b rejects a missing/empty value.
@@ -84,7 +86,16 @@
84
86
  - `### Stage Exit Contract` — predicted added/modified files, newly exposed identifiers/types/endpoints, downstream-usable resources.
85
87
  - `### Stage Validation` — pre / mid / post exact commands or observable outcomes for this stage only.
86
88
  - **Vertical-slice-first partition rule (1st-class):** the grouping anchor is a **thin end-to-end vertical slice** — one stage delivers a single user-observable increment, crossing whatever layers are needed (data → service → API → UI) to make that one increment work. File/module proximity is demoted to the **intra-slice grouping rule**: within a slice, keep steps touching the same file/directory/module together so the diff, PR, and rollback unit stay cohesive. **Horizontal layer-splitting is forbidden** — never carve "the DB layer" into one stage and "the service layer" into the next; that produces stages that ship no standalone user value. A stage is split ONLY when (a) a real `depends-on` data/contract dependency exists, (b) effective steps would exceed 6, or (c) it is a distinct vertical slice (a different user-value increment). Maximising the number of parallel stages is NOT a reason to split — parallelism is an emergent property of independent stages, never a partitioning goal.
89
+ - **Project-boundary partition rule (hard boundary):** a *project* boundary is either (a) a different repository / `PROJECT_ROOT`, or (b) a different top-level independently-deployable module within one repo. A stage maps to a single worktree on one repo/branch, so **no stage may contain edits belonging to more than one project** — this is a hard split that overrides the ≤6-step merging allowance; never co-locate two projects' changes in one stage to save a stage. Two cases:
90
+ - **Same repo, different modules** — put each project/module's increment in its own stage. Their file sets are disjoint, so they default to `depends-on (none)` and run in parallel; add a `depends-on` link ONLY when a real cross-module contract / shared-schema / deploy-order dependency exists.
91
+ - **Different repos** — a single okstra task **cannot** span repos: every stage worktree is a `git worktree add` in one repo's main checkout (one `<project-id>`), the run-index / manifests / registry are keyed to that one project, and the edit allowlist only covers that project's tree plus `~/.okstra/worktrees/**`. This is a structural limit, not a style preference. Therefore cross-repo work MUST be split into **a separate okstra run per repo** — never modelled as stages of one task. State, in the Stage Map `Cross-project parallelism:` line, whether those per-repo runs can proceed in parallel. To avoid re-deriving shared analysis, the second repo's run should consume the first run's relevant plan/decision artifacts as brief Source Material (see the cross-repo carry note below).
92
+ - **Parallel-feasibility check (mandatory for every multi-project plan):** disjoint files (S9 below) is necessary but NOT sufficient for parallelism — a cross-project API/contract/schema/deploy-order dependency forces sequencing even when no file overlaps. For each pair of projects, explicitly determine and record (in the `Cross-project parallelism:` line) whether they are independent (run in parallel) or sequenced (and the exact dependency that forces the order).
87
93
  - **Parallel-safety invariant (BLOCKING):** any two stages that are both `depends-on (none)` MUST predict disjoint file sets in their `Stage Exit Contract`. Two parallel `implementation` runs would otherwise edit the same file concurrently. Work touching a shared file must either go in one stage or be ordered with `depends-on`. Enforced by `validators/validate-implementation-plan-stages.py` check S9.
94
+ - **Cross-repo carry packet (multi-repo plans only — the carry note referenced above):** when the plan identifies work in a repo *other* than this run's project, that work cannot be implemented here (Different-repos rule above). For each other repo, add a top-level report appendix section `## Cross-Repo Carry — <repo>` with these three mandatory subsections, written so a reader who has never seen this run can act cold:
95
+ - `### Preconditions already covered by this run` — what THIS run's repo delivers that `<repo>`'s work depends on (an endpoint, a published contract, a migration, a package version). For each, give the **observable verification signal** `<repo>`'s run must confirm before/while implementing (PR merged, endpoint live, version published) — NOT a `depends-on` stage link. A cross-repo dependency cannot be a `depends-on`: that gate resolves commits inside one repo's git graph only (`scripts/okstra_ctl/run.py` `_resolve_stage_base_commit`), so it is structurally incapable of pointing at another repo's commit.
96
+ - `### Work for <repo> to implement` — the self-contained B-portion: its requirements restated as fresh `R-NNN` from `<repo>`'s perspective, the proposed stages/steps, affected files, and any decision drafts. Carry ONLY what `<repo>` still has to build — never carry this run's already-`done` stages as if they were `<repo>` stages.
97
+ - `### How to start <repo>'s run` — the exact handoff: in `<repo>` the user runs `okstra-brief` citing **this report's absolute path as Source Material** (the only sanctioned cross-`<PROJECT_ROOT>` read — `okstra-brief` reads outside `<PROJECT_ROOT>/.okstra/**` only when the user explicitly cites the path), then `okstra-run` `<repo>`'s own planning → implementation seeded by it.
98
+ - **Recognition caveat (write it verbatim into the section):** `<repo>`'s run is structurally independent — its done-set comes only from its OWN `runs/.../consumers.jsonl`, its stage numbers are its own, and `depends-on` resolves against its own git graph. This run's completed stages are therefore NOT auto-recognised as `done` by `<repo>`'s gate (and must not be — they are this repo's work). The carry packet is narrative input that seeds `<repo>`'s planning; `<repo>` re-plans and implements only its own portion, treating this run's deliverables as the external preconditions above. This run MUST NOT write into `<repo>`'s tree or its `.okstra/`; it only emits the carry section in its own report.
88
99
  - **Stage exit contract is the carry surface:** keep it as narrow as possible. Wider surface = more downstream coupling.
89
100
  - dependency / migration risk assessment (ordering constraints, data backfills, feature-flag prerequisites, repo-internal sequencing)
90
101
  - validation checklist (pre / mid / post) — each item is an exact command or observable outcome
@@ -116,4 +127,4 @@
116
127
  5. **Scope check** — if the recommended plan now spans multiple independent subsystems, recommend splitting into separate planning runs rather than shipping an oversized plan.
117
128
  6. **Review-rule preflight check** — if a project review rule pack exists, map each relevant rule to the recommended option. Reject the draft if it knowingly creates a violation that the later PR reviewer would flag, unless the plan records a specific rationale and follow-up. In particular, scan for repeated helper stacks across planned files, tests that assert delegation to the same calculator/helper they exercise, public names that hide side effects, domain rules placed in repositories/adapters, and APIs made dead by this change.
118
129
  7. **Plan-body verification reconciliation (BLOCKING for implementation-planning).** Inspect the `### 5.5.9 Plan Body Verification` verdict table. For every plan-item row classified as `majority-disagree → C-<N>`, the corresponding `C-<N>` row MUST exist in `## 1. Clarification Items` with `Kind` chosen per the standard policy and `Blocks=approval`. Do NOT create a parallel `Open Questions` block under the implementation plan body — the unified table is the single home. Conversely, the `Classification` column's `C-<N>` reference and the `## 1. Clarification Items` `ID` column MUST match 1:1; an orphan on either side is a contract violation. For `partial-consensus` and `worker-unique` plan-items, the dissenting opinion lives in §5.5.9 `Dissent log` and is NOT promoted to §5.
119
- 8. **Stage Map self-check** — for every stage, count the effective rows of its `Stepwise Execution Order` table by hand; reject the draft if any stage exceeds 6. Confirm each stage declares a non-empty `Slice value:` and `Acceptance:` line, the three `Test case (success|boundary|failure):` lines (or carries a `TDD exemption:` line), and that its first step `action` starts with `RED:` with a later `GREEN:` — this is what validator S10 enforces, including S10d on the test-case lines. Read each stage's three test-case lines as a reviewer: reject any that restates the happy path in all three slots, leaves `boundary` blank, or writes `N/A` where a real edge input exists. Walk the `depends-on` graph and confirm it is a DAG (no cycle, no self-reference). For each `depends-on` link, confirm it encodes a real data/contract dependency — do NOT add links to serialise unrelated work, and do NOT split a stage merely to create more parallel stages. **Parallel-safety:** for every pair of `depends-on (none)` stages, confirm their `Stage Exit Contract` predicted file sets are disjoint; if they share a file, merge them or add a `depends-on` link (validator S9 rejects overlap).
130
+ 8. **Stage Map self-check** — for every stage, count the effective rows of its `Stepwise Execution Order` table by hand; reject the draft if any stage exceeds 6. Confirm each stage declares a non-empty `Slice value:` and `Acceptance:` line, the three `Test case (success|boundary|failure):` lines (or carries a `TDD exemption:` line), and that its first step `action` starts with `RED:` with a later `GREEN:` — this is what validator S10 enforces, including S10d on the test-case lines. Read each stage's three test-case lines as a reviewer: reject any that restates the happy path in all three slots, leaves `boundary` blank, or writes `N/A` where a real edge input exists. Walk the `depends-on` graph and confirm it is a DAG (no cycle, no self-reference). For each `depends-on` link, confirm it encodes a real data/contract dependency — do NOT add links to serialise unrelated work, and do NOT split a stage merely to create more parallel stages. **Parallel-safety:** for every pair of `depends-on (none)` stages, confirm their `Stage Exit Contract` predicted file sets are disjoint; if they share a file, merge them or add a `depends-on` link (validator S9 rejects overlap). **Project-boundary:** confirm no stage mixes edits from two projects (different repo/`PROJECT_ROOT` or different top-level deployable module); if any stage does, split it per project. For multi-project plans, confirm each stage's `title` carries its `[<project>]` tag and the `Cross-project parallelism:` line under the table records the parallel-vs-sequenced determination (with the forcing dependency) for every project pair; for cross-repo work, confirm it is split into separate per-repo runs (required — one run structurally cannot touch another repo) rather than crammed into one task's stages.
@@ -64,7 +64,7 @@ class ClarificationItem:
64
64
  # normalization so the ID parses as a bare token for clarification parsing AND
65
65
  # the HTML view's `C-\d+` form detection, and so the anchor never leaks into
66
66
  # the HTML view as html-escaped literal text.
67
- _CELL_ANCHOR_RE = re.compile(r'<a id="[^"]*"></a>')
67
+ _CELL_ANCHOR_RE = re.compile(r'<a id="([^"]*)"></a>')
68
68
 
69
69
 
70
70
  def _strip_backticks(cell: str) -> str:
@@ -0,0 +1,55 @@
1
+ """완료(terminal) worker 판정 — pane 회수용. 판정 SSOT 는
2
+ wrapper_status.read_wrapper_status 이며 여기서 재사용한다."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from .reconcile import NON_TERMINAL_RECENT_STATUSES
10
+ from .wrapper_status import read_wrapper_status
11
+
12
+
13
+ def is_completed_status(status_path: str) -> bool:
14
+ status = read_wrapper_status(Path(status_path))
15
+ return status is not None and status.is_terminal
16
+
17
+
18
+ def active_run_dirs(home: Path) -> list[Path]:
19
+ """active.jsonl 의 진행 중 run 들의 절대 run-dir 목록.
20
+ runDirRel 의 base 는 projectRoot(paths.compute_run_paths 의 _rel 기준)."""
21
+ path = home / "active.jsonl"
22
+ if not path.is_file():
23
+ return []
24
+ dirs: list[Path] = []
25
+ for line in path.read_text(encoding="utf-8").splitlines():
26
+ line = line.strip()
27
+ if not line:
28
+ continue
29
+ try:
30
+ row = json.loads(line)
31
+ except json.JSONDecodeError:
32
+ continue
33
+ rel = row.get("runDirRel")
34
+ root = row.get("projectRoot")
35
+ # 회수 대상(진행 중) 집합은 reconcile 의 NON_TERMINAL_RECENT_STATUSES 가
36
+ # SSOT. reserving 은 아직 run-dir 산출물이 없어(= 회수할 pane 이 없어) 이
37
+ # 집합에 들어있지 않으므로 자연히 제외된다(allowlist).
38
+ if not rel or not root or row.get("status") not in NON_TERMINAL_RECENT_STATUSES:
39
+ continue
40
+ dirs.append(Path(root) / rel)
41
+ return dirs
42
+
43
+
44
+ def main(argv: list[str]) -> int:
45
+ if len(argv) == 2 and argv[0] == "--active-dirs":
46
+ for run_dir in active_run_dirs(Path(argv[1])):
47
+ print(run_dir)
48
+ return 0
49
+ if len(argv) == 1:
50
+ return 0 if is_completed_status(argv[0]) else 1
51
+ return 1
52
+
53
+
54
+ if __name__ == "__main__":
55
+ raise SystemExit(main(sys.argv[1:]))
@@ -17,6 +17,7 @@ session id 등) 를 덧붙여 전달한다.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import hashlib
20
21
  import json
21
22
  import re
22
23
  import sys
@@ -1658,6 +1659,23 @@ def build_available_mcp_servers_block(project_root: Path) -> str:
1658
1659
  # --------------------------------------------------------------------------- #
1659
1660
 
1660
1661
 
1662
+ def sanitize_team_name(base: str, suffix: str = "") -> str:
1663
+ """`base`(+`suffix`)를 TeamCreate 의 team_name 패턴에 맞춘다.
1664
+
1665
+ TeamCreate 는 team_name 을 `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$` 로 검증한다(콜론
1666
+ 불가·최대 64자). 그런데 okstra 의 base 는 `okstra-<TASK_KEY>` 이고 TASK_KEY 는
1667
+ `project:group:task-id` 라 콜론을 포함하며 64자를 쉽게 넘는다. 불허문자를 '-' 로
1668
+ 치환하고, 초과 시 stage 격리용 `suffix`(`-s<N>` / `-fv-s<N>`)는 보존하면서 원본
1669
+ 해시 8자로 절단해 유일성을 유지한다(suffix 만으로 stage 를 구분하는 호출부 계약).
1670
+ """
1671
+ safe = re.sub(r"[^A-Za-z0-9_-]+", "-", base).strip("-")
1672
+ limit = 64 - len(suffix)
1673
+ if len(safe) > limit:
1674
+ digest = hashlib.sha1(base.encode("utf-8")).hexdigest()[:8]
1675
+ safe = safe[: max(0, limit - 9)].rstrip("-") + "-" + digest
1676
+ return safe + suffix
1677
+
1678
+
1661
1679
  def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1662
1680
  """Populate ctx in-place with derived lead-prompt tokens.
1663
1681
 
@@ -1874,19 +1892,21 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1874
1892
  " Teams split-pane view is lost."
1875
1893
  )
1876
1894
  else:
1877
- team_name = f'okstra-{ctx.get("TASK_KEY", "")}'
1895
+ base_name = f'okstra-{ctx.get("TASK_KEY", "")}'
1878
1896
  impl_stage = str(ctx.get("EFFECTIVE_STAGES", "") or "").strip()
1879
1897
  fv_stage = str(ctx.get("RUN_STAGE", "") or "").strip()
1898
+ stage_suffix = ""
1880
1899
  if task_type == "implementation" and impl_stage:
1881
1900
  # stage 격리 run 은 stage 별 team — 같은 task 의 다른 stage 가 남긴
1882
1901
  # team 과 이름이 충돌하지 않는다(worktree branch `-s<N>` 접미사와 동형).
1883
- team_name = f"{team_name}-s{impl_stage}"
1902
+ stage_suffix = f"-s{impl_stage}"
1884
1903
  elif task_type == "final-verification" and fv_stage:
1885
1904
  # 단일-stage final-verification 도 stage 별 team. `-fv-` 를 끼워
1886
1905
  # 같은 stage 의 implementation team(`-s<N>`)과도 구분한다 — 둘은
1887
1906
  # 동시에 살아 있을 수 있다. whole-task 검증(RUN_STAGE 빈 값)은
1888
1907
  # 기본 이름 유지.
1889
- team_name = f"{team_name}-fv-s{fv_stage}"
1908
+ stage_suffix = f"-fv-s{fv_stage}"
1909
+ team_name = sanitize_team_name(base_name, stage_suffix)
1890
1910
  team_creation_gate_block = (
1891
1911
  "## Team Creation Gate (BLOCKING)\n"
1892
1912
  "\n"