okstra 0.98.2 → 0.99.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.
Files changed (31) hide show
  1. package/docs/kr/architecture.md +5 -5
  2. package/docs/superpowers/plans/2026-06-23-phase-batch-cleanup-enforcement.md +409 -0
  3. package/package.json +1 -1
  4. package/runtime/BUILD.json +2 -2
  5. package/runtime/agents/workers/claude-worker.md +1 -1
  6. package/runtime/bin/okstra-team-reconcile.sh +7 -7
  7. package/runtime/prompts/coding-preflight/overview.md +1 -1
  8. package/runtime/prompts/lead/context-loader.md +1 -1
  9. package/runtime/prompts/lead/convergence.md +3 -4
  10. package/runtime/prompts/lead/okstra-lead-contract.md +26 -24
  11. package/runtime/prompts/lead/report-writer.md +6 -7
  12. package/runtime/prompts/lead/team-contract.md +4 -4
  13. package/runtime/prompts/profiles/_common-contract.md +16 -16
  14. package/runtime/prompts/profiles/forbidden-actions.json +1 -1
  15. package/runtime/prompts/profiles/release-handoff.md +2 -2
  16. package/runtime/python/okstra_ctl/paths.py +1 -5
  17. package/runtime/python/okstra_ctl/render.py +38 -49
  18. package/runtime/python/okstra_ctl/run.py +1 -1
  19. package/runtime/python/okstra_ctl/session.py +1 -1
  20. package/runtime/python/okstra_ctl/team_reconcile.py +10 -12
  21. package/runtime/python/okstra_ctl/worktree.py +2 -3
  22. package/runtime/python/okstra_ctl/worktree_registry.py +3 -3
  23. package/runtime/python/okstra_project/dirs.py +31 -4
  24. package/runtime/python/okstra_token_usage/collect.py +7 -5
  25. package/runtime/skills/okstra-run/SKILL.md +3 -3
  26. package/runtime/validators/lib/fixtures.sh +8 -7
  27. package/runtime/validators/validate-run.py +18 -15
  28. package/runtime/validators/validate_session_conformance.py +45 -6
  29. package/src/commands/lifecycle/config.mjs +2 -4
  30. package/src/commands/memory/memory.mjs +2 -3
  31. package/src/lib/paths.mjs +28 -2
@@ -233,8 +233,8 @@ per-process 환경 변수에 task 정체성·경로·workflow 상태를 보관
233
233
  - handoff된 메인 Claude는 `Claude lead`로 동작하며 orchestration과 final synthesis를 담당합니다.
234
234
  - standard workflow의 기본 worker role은 `Claude worker`, `Codex worker`, `Report writer worker`이며, `Antigravity worker`는 `--workers` 또는 프로필에서 명시할 때만 포함되는 옵션입니다.
235
235
  - worker 역할 분담과 최종 판단은 Claude가 task bundle을 읽고 수행합니다.
236
- - 사용자 홈에 설치된 okstra Claude assets(`~/.claude/skills`, `~/.claude/agents`) 는 Agent Teams 우선 시도하고, 구성이 불가능할 때만 sequential/background fallback 사용하도록 Claude 유도합니다.
237
- - **팀 lifecycle**: lead 는 Phase 3 에서 `TeamCreate(team_name: "okstra-<task-key>")` 팀을 만들고 워커를 멤버로 dispatch 합니다. run 종료 시 Phase 7 토큰 집계 이후 먼저 잔여 tmux pane 사용자에게 보여 주고 닫을지 확인한 뒤, Teams mode 에서만 worker teammate `okstra-<task-key>` 을 해제할지 다시 확인합니다. 사용자가 승인하면 `okstra-team-reconcile.sh` 로 dead-pane stale-active 멤버를 정리하고 `TeamDelete` 팀을 해제합니다. `TeamDelete` `~/.claude/teams/<team>/`·`~/.claude/tasks/<team>/` 지우고 토큰 집계 소스인 `~/.claude/projects/` jsonl 보존합니다. 사용자가 유지하거나 teardown 실패하면 worker teammate FleetView roster 에 남으며, lead 는 Teams/FleetView 에서 해당 team 을 삭제하라고 안내합니다 (`prompts/profiles/_common-contract.md` 의 *Run-end teammate teardown*). no-`team_name` fallback 에서는 팀이 없으므로 silent-skip.
236
+ - 사용자 홈에 설치된 okstra Claude assets(`~/.claude/skills`, `~/.claude/agents`) 는 `Agent(name: ...)` 워커를 dispatch 하도록 Claude 유도합니다 워커는 세션의 implicit team 자동 합류합니다.
237
+ - **팀 lifecycle (Claude Code v2.1.178+)**: v2.1.178 이 `TeamCreate` / `TeamDelete` 도구와 `Agent(...)` 의 `team_name` 파라미터를 제거했습니다. `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`(`okstra install` 이 `settings.json` 에 시드) 이면 세션마다 implicit team 1개가 시작 시점에 자동 생성됩니다. lead 는 Phase 3 에서 팀을 만드는 도구를 호출하지 않고, `teamName` audit 라벨과 `teamCreate: { attempted: false, status: "implicit", splitPane: <$TMUX 유무> }` 기록한 워커를 `Agent(name: "<role>-worker", run_in_background: true)`(team_name 없음)로 dispatch 합니다. split-pane teammate 는 `$TMUX` 가 설정돼 있고 `teammateMode: auto` 일 때 나타나며, tmux 밖이면 in-process 로 돕니다(둘 다 정상). run 종료 시 Phase 7 토큰 집계 이후 잔여 tmux pane 정리를 확인하고, split-pane run 에서만 worker teammate 정리할지 확인합니다. 승인 `okstra-team-reconcile.sh` 로 dead-pane stale-active 멤버를 inactive 로 정리하고 완료 teammate `SendMessage` shutdown_request 보냅니다 implicit team 자체를 지우는 도구는 없으며, 팀은 세션 종료와 함께 사라집니다. 사용자가 유지하면 teammate FleetView roster 에 남고, lead 는 Teams/FleetView 에서 제거하라고 안내합니다 (`prompts/profiles/_common-contract.md` 의 *Run-end teammate teardown*). Phase 7 토큰 집계는 `teamCreate.status` `implicit`/`skipped`/`error` `agentName` 기반으로 워커 세션을 찾습니다(implicit team 은 jsonl 을 `session-<leadSid>` 로 태깅하므로 okstra 의 `teamName` 라벨 needle 만으로는 lead 만 잡힘).
238
238
 
239
239
  ## Claude prompt contract
240
240
 
@@ -348,7 +348,7 @@ okstra phase 는 PRD / issue file 을 직접 쓰지 않습니다. 동등한 결
348
348
  - `implementation`을 제외한 모든 phase는 source code edit, build, migration, deployment, 그 밖의 state-mutating 명령을 금지합니다(`final-verification`은 read-only 테스트 명령만 허용). `implementation`은 승인된 plan의 파일 목록 안에서만 edit/commit이 허용되며, `git push`·publish·deploy·실제 migration·third-party write API는 여전히 금지됩니다.
349
349
  - **모든 task-type 격리 worktree (BLOCKING)**: 모든 task-type 의 첫 번째 phase prepare 단계에서 `okstra-ctl` 이 자동으로 task-key 단위 `git worktree` 를 생성하고, 같은 task-key 의 이후 phase (`requirements-discovery` → `error-analysis` → `implementation-planning` → `implementation`) 는 동일한 worktree·브랜치를 재사용합니다. 위치는 `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` (segment 의 `/`·`:` 등 특수문자는 `-` 로 정규화) 이고, 브랜치 이름은 `<work-category-prefix>-<task-id-segment>` (예: `feat-dev-9436`, `fix-dev-7311`) 입니다. base ref 는 첫 phase prepare 시점의 main worktree `HEAD`. `~/.okstra/worktrees/registry.json` (flock-guarded) 가 task-key → path/branch 매핑을 전역 관리해 동시 실행 시 path·branch 충돌을 방지합니다. configured sync dirs 는 main worktree 에서 symlink 로 연결되어 task checkout 사이의 filesystem continuity 를 제공합니다 (sync 대상 목록은 `project.json` 의 `worktreeSyncDirs` 또는 `OKSTRA_WORKTREE_SYNC_DIRS` 환경변수로 override 가능; 빈 배열이면 sync 비활성화). 이 sync 는 okstra context/write boundary 를 확장하지 않습니다. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 되고 executor 는 project_root 에서 그대로 작업합니다. worktree 는 run 종료 후 자동 삭제되지 않으며 후속 phase·PR 작성·rollback 검증의 권위 artefact 입니다. 수동 cleanup: `git -C <main-worktree> worktree remove <path>` → `git -C <main-worktree> branch -D <branch>` + registry 항목 삭제. 자세한 동작은 `prompts/profiles/implementation.md` 의 *Task worktree* 블록과 `prompts/lead/okstra-lead-contract.md` 의 *Task worktree (BLOCKING for every task-type)* 섹션 참고.
350
350
  - **implementation stage 격리 worktree (동시 병렬)**: 위 task-key 단위 worktree 는 `requirements-discovery`~`implementation-planning` 의 모델입니다. `implementation` task 는 **stage 격리** 로 동작합니다 (spec `docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md`) — **한 run = 한 stage**, 각 run 이 `.../<task-id-segment>/stage-<N>/` (브랜치 `<prefix>-<task-id-segment>-s<N>`) 격리 worktree 를 발급받습니다. registry 가 task-key 와 **stage-key** (`<task-key>#stage-<N>`) 를 함께 flock 예약하고, `_resolve_effective_stages` 가 `consumers.jsonl` 의 `started` + registry 예약 stage 를 ready 집합에서 제외하므로(점유 SSOT = registry), 사용자가 두 `implementation` run 을 동시에 띄우면 서로 다른 독립 stage 를 충돌 없이 진행합니다. base 결정: 독립 = 공통 anchor(첫 stage 진입 HEAD 고정), 단일 의존 = 선행 done commit, 다중 의존 = 선행이 모두 ancestor 인 task worktree HEAD(`git merge-base --is-ancestor`; 미머지 시 `PrepareError`). cost-aware-design 의 ready-set batch 는 stage 마다 격리 branch 가 필요해 의미를 잃으므로(같은 branch 에 두 stage-key reserve 시 branch-uniqueness 충돌) 폐기되었고, 순차 진행은 stage done 후 다음 run, 동시 진행은 별도 run 으로 — cost 등가. `--stage <auto|N>` 또는 wizard `stage_pick` 으로 stage 를 선택합니다. worktree 뿐 아니라 **run 산출물(report·state·worker-results·manifest)도 `runs/implementation/stage-<N>/` 로 stage 별 격리**되므로 동시 실행하는 두 stage 의 보고서·상태가 섞이지 않습니다. 반면 `consumers.jsonl` 과 worktree registry 는 stage 간 공유되는 조율 SSOT 라 task-type 루트(`runs/implementation/`)에 그대로 둡니다.
351
- - **단일-stage final-verification 의 run 산출물 격리 (동시 병렬)**: 단독-stage `final-verification`(`--stage <N>`)도 implementation 과 동일하게 run 산출물을 `runs/final-verification/stage-<N>/` 하위에 격리하고(seq 도 stage 별 독립), 팀 이름에 `-fv-s<N>` 접미사를 붙입니다 — `-fv-` 구분자로 같은 stage 의 implementation 팀(`-s<N>`)과도, 전체-task 검증의 기본 이름과도 충돌하지 않습니다. 따라서 여러 stage 의 final-verification 을 동시에 띄워도 state·worker-results·보고서·팀이 섞이지 않습니다. worktree 는 새로 만들지 않고 해당 implementation stage worktree 를 registry 에서 read-only 로 재사용하며, 그래서 registry stage-key 예약도 하지 않습니다 **같은 stage final-verification 동시에 번** 띄우는 경우는 격리되지 않고 `TeamCreate` 이름 충돌로 즉시 실패합니다(알려진 제약). 전체-task 검증(stage 빈 값)은 기존 평면 `runs/final-verification/` 구조를 유지합니다.
351
+ - **단일-stage final-verification 의 run 산출물 격리 (동시 병렬)**: 단독-stage `final-verification`(`--stage <N>`)도 implementation 과 동일하게 run 산출물을 `runs/final-verification/stage-<N>/` 하위에 격리하고(seq 도 stage 별 독립), 팀 이름에 `-fv-s<N>` 접미사를 붙입니다 — `-fv-` 구분자로 같은 stage 의 implementation 팀(`-s<N>`)과도, 전체-task 검증의 기본 이름과도 충돌하지 않습니다. 따라서 여러 stage 의 final-verification 을 동시에 띄워도 state·worker-results·보고서·팀이 섞이지 않습니다. worktree 는 새로 만들지 않고 해당 implementation stage worktree 를 registry 에서 read-only 로 재사용하며, 그래서 registry stage-key 예약도 하지 않습니다. `teamName` 라벨에 `-fv-s<N>` 접미사를 붙이는 것은 audit/표시용 구분일 뿐이며, 실제 팀은 세션별 implicit team(`session-<leadSid>`)이라 v2.1.178 이전의 `TeamCreate` 이름 충돌 hard-fail 이상 발생하지 않습니다. 전체-task 검증(stage 빈 값)은 기존 평면 `runs/final-verification/` 구조를 유지합니다.
352
352
  - `implementation` 과 `release-handoff` 를 제외한 모든 phase 는 source code edit, build, migration, deployment, 그 밖의 state-mutating 명령을 금지합니다 (`final-verification` 은 read-only 테스트 명령만 허용). `implementation` 은 승인된 plan 의 파일 목록 안에서만 edit/commit 이 허용되며, `git push`·publish·deploy·실제 migration·third-party write API 는 여전히 금지됩니다. `release-handoff` 는 source code 자체는 수정하지 않고, 사용자가 메뉴로 선택한 commit / push / PR 명령만 실행합니다 (force push, base 브랜치 직접 push, hook bypass, release publish 는 여전히 금지).
353
353
  - 사용자가 "다음 단계 진행해" 같은 표현을 보내도, 그 발화만으로 다음 phase가 자동 시작되지 않습니다. 다음 phase는 새 `okstra.sh` 실행으로만 시작합니다.
354
354
  - **Authority & permissions assumption (모든 task-type 및 `okstra-schedule` 공통)**: 사용자(및 팀)는 예상되는 모든 작업에 대해 완전한 권한·승인 권한을 보유한다고 가정합니다. 외부 승인, 서드파티 액세스, 역할/IAM 권한, 조직적 sign-off, 법무·보안 검토, 벤더 협의, "권한 보유 여부 확인" 같은 항목을 routing 결정·missing inputs·clarification questions·risk·dependency·open questions·effort/day 추정에 포함하지 않습니다. okstra 내부 phase 핸드오프(`implementation-planning`의 `approved:` frontmatter 등)는 사용자 본인이 즉시 승인 가능한 내부 게이트이므로 영향 없으며, `implementation`의 forbidden actions(`git push`, prod deploy, shared-DB migration 등)도 권한 사유가 아닌 **안전 사유**로 계속 적용됩니다.
@@ -683,7 +683,7 @@ Phase 7 step 1.5 가 final-report MD 한 본을 입력으로 두 view 를 결정
683
683
  - 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.
684
684
  - **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.
685
685
  - **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 없이 정리합니다.
686
- - **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>` 만 출력하므로 사용자가 무엇이 닫힐지 시각적으로 확인할 수 있습니다.
686
+ - **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* 항목). 그 다음 split-pane run 인 경우 worker teammate 정리할지 별도로 묻고, 승인 시 `okstra-team-reconcile.sh` dead-pane 멤버를 inactive 로 정리한 뒤 각 완료 teammate 에 `SendMessage` shutdown_request 보냅니다(`TeamDelete` 는 v2.1.178 에서 제거 — implicit team 은 세션 종료와 함께 사라짐). 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>` 만 출력하므로 사용자가 무엇이 닫힐지 시각적으로 확인할 수 있습니다.
687
687
  - 디스크 누적은 `okstra-inspect logs` 흐름이 read-only 로 인벤토리 + cleanup 명령을 제안합니다 (실행은 사용자 copy-paste).
688
688
 
689
689
  ### Linked-worktree `.git/` write 권한 (codex / antigravity)
@@ -738,7 +738,7 @@ phase 산출물의 출고 가능 여부를 강제하는 진입점:
738
738
  - project-level current-task convenience pointer는 `.okstra/discovery/latest-task.json`입니다.
739
739
  - project-level canonical task inventory는 `.okstra/discovery/task-catalog.json`입니다.
740
740
  - okstra skill asset은 `okstra install` 시점에 존재하는 agent home 기준으로 seed됩니다: `~/.claude/skills/` + `~/.claude/agents/`, `~/.codex/skills/`, 또는 fallback `~/.omp/agent/skills/` (per-project seeding은 더 이상 수행되지 않음).
741
- - seeded okstra Claude assets는 Agent Teams 우선, sequential/background fallback 차선 규칙을 Claude에게 제공합니다. Codex/OMP targets 는 skill markdown 만 받습니다.
741
+ - seeded okstra Claude assets는 세션의 implicit team 에 `Agent(name: ...)` 워커를 dispatch 하는 규칙을 Claude에게 제공합니다(v2.1.178 이 `TeamCreate`/`TeamDelete` 제거). Codex/OMP targets 는 skill markdown 만 받습니다.
742
742
  - 최종 판단은 스크립트가 아니라 Claude가 수행합니다.
743
743
  - stable task key를 유지해야 이후 bug 추적, 재수정, 재검증이 가능합니다.
744
744
  - 워커 에러는 옵션 sidecar `runs/<task-type>/logs/errors-<task-type>-<seq>.jsonl`로 수집되며 lead가 단독 writer입니다. 진입점 helper는 `scripts/okstra-error-log.py`입니다.
@@ -0,0 +1,409 @@
1
+ # Phase-batch cleanup 강제화 Implementation Plan
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:** lead 가 phase 전환(배치 경계)마다 이전 팀원·pane 을 정리하도록, 정리 단계를 검증 가능한 `PROGRESS: phase-batch-cleanup` 체크포인트로 만들고 conformance 검증기가 누락 시 run 을 `contract-violated` 로 실패시킨다.
6
+
7
+ **Architecture:** 정리 로직 자체는 이미 `prompts/profiles/_common-contract.md` "Phase-start cleanup" 에 존재하나 **강제 장치가 없어** lead 가 조용히 건너뛴다(검증되는 PROGRESS 마커 목록에 cleanup 항목 부재). freeform 상태줄을 구조화 마커로 승격하고, 기존 `_check_progress_checkpoints` 의 timestamp-anchor 패턴(이미 `_check_implementation_sidecar_reads` 에서 검증됨)을 그대로 써서 "convergence round 1 직전" 과 "report-writer dispatch 직전" 두 경계에 cleanup 마커가 선행하는지 검사한다. 부수적으로 cleanup 스크립트가 `lead_pane` 미해석 시 worker-agent pane 정리를 통째로 건너뛰던 약점을 태그-기반 anchor fallback 으로 보강한다(Task 2, 선택).
8
+
9
+ **Tech Stack:** Python 3 (검증기 `validators/`, pytest `tests/`), Bash (`scripts/okstra-trace-cleanup.sh`), Markdown 계약 문서(`prompts/`), Node 빌드(`tools/build.mjs` via `npm run build`).
10
+
11
+ ## Global Constraints
12
+
13
+ - `runtime/` 는 빌드 산출물 — **직접 수정 금지**. 소스(`prompts/`, `scripts/`, `validators/`)만 고치고 `npm run build` 로 동기화한다.
14
+ - 수정은 반드시 **소스 파일**에 — 개인 `.claude/` 가 아니라 end-user 에게 전파되는 seed/template/소스 파일에.
15
+ - CI 게이트 = `npm run check` 가 exit 0 (test:js 실패 시 test:py 가 안 돌므로 통합 명령으로 확인).
16
+ - 신규/대폭 수정 함수는 effective 50줄 이하.
17
+ - 마커 토큰명: 정확히 **`phase-batch-cleanup`** (validator·계약 문서·테스트 3곳 철자 일치 필수).
18
+ - 문서 산문은 한국어, 코드·경로·식별자·CLI 인자는 영어.
19
+
20
+ ---
21
+
22
+ ## File Structure
23
+
24
+ | 파일 | 책임 | 작업 |
25
+ |---|---|---|
26
+ | `validators/validate_session_conformance.py` | conformance 검증기. `_check_progress_checkpoints` 가 필수 PROGRESS 체크포인트를 `require()`/anchor 로 강제 | 신규 헬퍼 `_check_batch_cleanup_checkpoints` 추가 + 호출부 배선 |
27
+ | `tests/contract/test_validate_session_conformance.py` | 검증기 단위 테스트. `Fixture` 빌더 + `HAPPY_PROGRESS` 공용 시퀀스 | `HAPPY_PROGRESS` 에 cleanup 마커 추가, `Fixture.write_convergence_state` 헬퍼 추가, 신규 테스트 3종 |
28
+ | `prompts/lead/okstra-lead-contract.md` | lead 계약 SSOT. "Progress reporting (BLOCKING)" 의 필수 마커 목록 + Enforcement 문구 | 마커 1줄 추가 + enforcement 문구에 cleanup 검사 포함 |
29
+ | `prompts/profiles/_common-contract.md` | "Phase-start cleanup" 정의(공유 sidecar) | freeform 상태줄 → 구조화 마커 emit 으로 문구 교체 |
30
+ | `scripts/okstra-trace-cleanup.sh` | (Task 2, 선택) run 의 trace+worker-agent pane 닫기 | `lead_pane` 미해석 시 태그-anchor 세션 fallback |
31
+ | `tests/worktree/test_okstra_trace_cleanup.py` | (Task 2, 선택) cleanup 스크립트 real-tmux 테스트 | anchor fallback 테스트 1종 |
32
+ | `runtime/**` | 빌드 산출물 | 손대지 않음 — `npm run build` 가 갱신 |
33
+
34
+ **왜 검증기 + 계약 문서를 한 Task 로 묶는가:** 마커 정의(계약)와 강제(검증기)는 함께 배포돼야 의미가 있다 — 검사 없는 마커, 또는 문서화 안 된 마커를 강제하는 검증기는 둘 다 미완성이라 리뷰어가 함께 reject 한다.
35
+
36
+ ---
37
+
38
+ ## Task 1: cleanup 마커 강제화 (검증기 + 계약 문서)
39
+
40
+ **Files:**
41
+ - Modify: `validators/validate_session_conformance.py` (헬퍼 추가 ~line 349 뒤, 호출부 `_check_progress_checkpoints` line 395-407)
42
+ - Test: `tests/contract/test_validate_session_conformance.py` (`HAPPY_PROGRESS` line 131-142, `Fixture` class line 154-232, 신규 테스트는 `test_missing_checkpoints_each_reported` line 347 부근 뒤)
43
+ - Modify: `prompts/lead/okstra-lead-contract.md` (line 103 뒤 마커 추가, line 112 enforcement 문구)
44
+ - Modify: `prompts/profiles/_common-contract.md` (line 37, line 43)
45
+
46
+ **Interfaces:**
47
+ - Consumes: 기존 `_check_progress_checkpoints(evidence, team_state, run_dir, suffix, errors)`, `_convergence_rounds_ran(run_dir, suffix) -> bool`, `by_phase: dict[str, list[tuple[ts, line]]]`, `_DISPATCHED_STATUSES`, `_is_report_writer(worker)`.
48
+ - Produces: `_check_batch_cleanup_checkpoints(by_phase, convergence_ran: bool, report_writer_dispatched: bool, errors: list[str]) -> None` — `phase-batch-cleanup` 마커가 두 배치 경계에 선행하지 않으면 `errors` 에 문자열 append.
49
+ - 새 마커 토큰: `phase-batch-cleanup` (라인 형식 `PROGRESS: phase-batch-cleanup panes=<n> teammates=<m>`).
50
+
51
+ - [ ] **Step 1: 실패 테스트 작성 — report-writer 직전 cleanup 누락**
52
+
53
+ `tests/contract/test_validate_session_conformance.py` 의 `test_missing_checkpoints_each_reported`(line 347) 바로 뒤에 추가:
54
+
55
+ ```python
56
+ def test_missing_batch_cleanup_before_report_writer_fails(tmp_path, module):
57
+ """report-writer dispatch(phase-6-synthesis) 직전에 phase-batch-cleanup
58
+ 마커가 없으면 실패 — 이전 배치 pane/teammate 가 안 치워진 채 다음 배치가
59
+ 뜨는(화면 누적) 상태를 잡는다."""
60
+ fx = Fixture(tmp_path)
61
+ progress = [(ts, line) for ts, line in HAPPY_PROGRESS
62
+ if "phase-batch-cleanup" not in line]
63
+ fx.write_jsonl([_text_record(ts, line) for ts, line in progress])
64
+ fx.write_heartbeat()
65
+ errors = "\n".join(fx.validate(module).errors)
66
+ assert "phase-batch-cleanup" in errors
67
+ assert "phase-6-synthesis" in errors
68
+ ```
69
+
70
+ - [ ] **Step 2: 테스트 실패 확인**
71
+
72
+ Run: `python3 -m pytest tests/contract/test_validate_session_conformance.py::test_missing_batch_cleanup_before_report_writer_fails -v`
73
+ Expected: FAIL — `HAPPY_PROGRESS` 에 아직 `phase-batch-cleanup` 이 없어 `progress` 필터가 무의미하고, 검증기도 마커를 요구하지 않아 `errors` 가 비어 `assert "phase-batch-cleanup" in errors` 가 깨진다.
74
+
75
+ - [ ] **Step 3: 검증기 헬퍼 구현**
76
+
77
+ `validators/validate_session_conformance.py` 의 `_check_worker_checkpoint_lines`(line 323-348) 정의 **뒤**, `_check_progress_checkpoints` **앞**에 추가:
78
+
79
+ ```python
80
+ def _check_batch_cleanup_checkpoints(
81
+ by_phase: dict[str, list[tuple[str, str]]],
82
+ convergence_ran: bool,
83
+ report_writer_dispatched: bool,
84
+ errors: list[str],
85
+ ) -> None:
86
+ """phase-batch-cleanup: 각 배치 경계 직전에 이전 배치 pane/teammate 정리가
87
+ 실제로 일어났는지 (prompts/profiles/_common-contract.md 'Phase-start cleanup').
88
+ R1=convergence round 1 직전, R2=report-writer dispatch 직전(수렴이 있었으면
89
+ 마지막 라운드 이후 별도 1회). ISO-8601 ts 는 lexicographic 비교가 곧 시간순."""
90
+ detail = "prompts/profiles/_common-contract.md 'Phase-start cleanup'"
91
+ cleanup_ts = sorted(ts for ts, _line in by_phase.get("phase-batch-cleanup", []))
92
+ conv_ts = sorted(ts for ts, _line in by_phase.get("phase-5.5-convergence", []))
93
+ synth_ts = sorted(ts for ts, _line in by_phase.get("phase-6-synthesis", []))
94
+
95
+ if convergence_ran and conv_ts and not any(ts <= conv_ts[0] for ts in cleanup_ts):
96
+ errors.append(
97
+ "PROGRESS checkpoint missing: no `phase-batch-cleanup` line before the "
98
+ f"first `phase-5.5-convergence` round ({conv_ts[0]}) — the prior analysis "
99
+ f"batch's panes/teammates must be cleared first ({detail})."
100
+ )
101
+
102
+ if report_writer_dispatched and synth_ts:
103
+ lower = conv_ts[-1] if (convergence_ran and conv_ts) else ""
104
+ if not any(lower <= ts <= synth_ts[0] for ts in cleanup_ts):
105
+ errors.append(
106
+ "PROGRESS checkpoint missing: no `phase-batch-cleanup` line before "
107
+ f"`phase-6-synthesis` ({synth_ts[0]}) — the prior batch must be cleared "
108
+ f"before dispatching report-writer ({detail})."
109
+ )
110
+ ```
111
+
112
+ - [ ] **Step 4: 호출부 배선**
113
+
114
+ `_check_progress_checkpoints` 의 마지막 블록(현재 line 395-407)을 교체. 기존:
115
+
116
+ ```python
117
+ require(
118
+ "phase-5.5-convergence",
119
+ _convergence_rounds_ran(run_dir, suffix),
120
+ "at the start of each convergence round (state artifact records totalRounds >= 1)",
121
+ )
122
+ report_writer = next((w for w in workers if _is_report_writer(w)), None)
123
+ require(
124
+ "phase-6-synthesis",
125
+ report_writer is not None
126
+ and str(report_writer.get("status", "")).strip() in _DISPATCHED_STATUSES,
127
+ "at the start of Phase 6 (report-writer dispatch)",
128
+ )
129
+ require("phase-7-persist", True, "at the start of Phase 7")
130
+ ```
131
+
132
+ 교체 후:
133
+
134
+ ```python
135
+ convergence_ran = _convergence_rounds_ran(run_dir, suffix)
136
+ require(
137
+ "phase-5.5-convergence",
138
+ convergence_ran,
139
+ "at the start of each convergence round (state artifact records totalRounds >= 1)",
140
+ )
141
+ report_writer = next((w for w in workers if _is_report_writer(w)), None)
142
+ report_writer_dispatched = (
143
+ report_writer is not None
144
+ and str(report_writer.get("status", "")).strip() in _DISPATCHED_STATUSES
145
+ )
146
+ require(
147
+ "phase-6-synthesis",
148
+ report_writer_dispatched,
149
+ "at the start of Phase 6 (report-writer dispatch)",
150
+ )
151
+ require("phase-7-persist", True, "at the start of Phase 7")
152
+ _check_batch_cleanup_checkpoints(
153
+ by_phase, convergence_ran, report_writer_dispatched, errors
154
+ )
155
+ ```
156
+
157
+ - [ ] **Step 5: HAPPY_PROGRESS 에 cleanup 마커 추가**
158
+
159
+ `HAPPY_PROGRESS`(line 131-142)의 `phase-6-synthesis`(`T.format(30)`) 항목 **앞**에 한 줄 삽입:
160
+
161
+ ```python
162
+ (T.format(20), "PROGRESS: phase-5-collect worker=claude-worker status=completed"),
163
+ (T.format(21), "PROGRESS: phase-5-collect worker=codex-worker status=completed"),
164
+ (T.format(25), "PROGRESS: phase-batch-cleanup panes=2 teammates=2"),
165
+ (T.format(30), "PROGRESS: phase-6-synthesis dispatching report-writer-worker"),
166
+ ```
167
+
168
+ `HAPPY_PROGRESS` 는 `test_happy_path_passes` / `test_codex_lead_events_...` / `test_external_lead_events_...` 3곳이 공유하므로 이 한 줄로 세 happy-path 가 모두 green 유지. (happy-path 는 convergence state artifact 가 없어 `convergence_ran=False` → R1 미적용, R2 만 `lower=""` 로 적용되어 `T.format(25) <= T.format(30)` 충족.)
169
+
170
+ - [ ] **Step 6: 실패 테스트가 통과로 바뀌는지 확인**
171
+
172
+ Run: `python3 -m pytest tests/contract/test_validate_session_conformance.py -v`
173
+ Expected: PASS — Step 1 의 신규 테스트 + 기존 happy-path 3종 포함 전부 통과.
174
+
175
+ - [ ] **Step 7: convergence 경계 테스트 2종 추가**
176
+
177
+ 먼저 `Fixture` class(line 154-232)에 헬퍼 추가 — `validate` 메서드(line 236) 앞:
178
+
179
+ ```python
180
+ def write_convergence_state(self, total_rounds: int = 1) -> None:
181
+ (self.run_dir / "state" / f"convergence-{self.suffix}.json").write_text(
182
+ json.dumps({"totalRounds": total_rounds}), encoding="utf-8"
183
+ )
184
+ ```
185
+
186
+ 그리고 Step 1 테스트 뒤에 추가:
187
+
188
+ ```python
189
+ def test_batch_cleanup_before_convergence_required_when_rounds_ran(tmp_path, module):
190
+ """convergence 가 실제로 돌았으면(round 1 직전) 그 앞에 cleanup 마커가
191
+ 있어야 한다 — 분석 워커 배치가 안 치워진 채 reverify 가 뜨는 걸 잡는다."""
192
+ fx = Fixture(tmp_path)
193
+ fx.write_convergence_state(total_rounds=1)
194
+ progress = [
195
+ (T.format(1), "PROGRESS: phase-1-intake reading task bundle"),
196
+ (T.format(2), "PROGRESS: phase-1-intake complete"),
197
+ (T.format(3), "PROGRESS: phase-2-prompts preparing 3 worker prompts"),
198
+ (T.format(4), "PROGRESS: phase-3-team-create attempting TeamCreate"),
199
+ (T.format(5), "PROGRESS: phase-4-dispatch worker=claude-worker model=opus"),
200
+ (T.format(6), "PROGRESS: phase-4-dispatch worker=codex-worker model=gpt-5.5"),
201
+ (T.format(20), "PROGRESS: phase-5-collect worker=claude-worker status=completed"),
202
+ (T.format(21), "PROGRESS: phase-5-collect worker=codex-worker status=completed"),
203
+ # NOTE: round 1 직전 cleanup 누락
204
+ (T.format(22), "PROGRESS: phase-5.5-convergence round=1 queue=3"),
205
+ (T.format(28), "PROGRESS: phase-batch-cleanup panes=2 teammates=2"),
206
+ (T.format(30), "PROGRESS: phase-6-synthesis dispatching report-writer-worker"),
207
+ (T.format(40), "PROGRESS: phase-7-persist updating manifests"),
208
+ ]
209
+ fx.write_jsonl([_text_record(ts, line) for ts, line in progress])
210
+ fx.write_heartbeat()
211
+ errors = "\n".join(fx.validate(module).errors)
212
+ assert "phase-batch-cleanup" in errors
213
+ assert "phase-5.5-convergence" in errors
214
+
215
+
216
+ def test_batch_cleanup_two_boundaries_pass(tmp_path, module):
217
+ """convergence round 1 직전 + report-writer 직전 두 번 cleanup 하면 통과."""
218
+ fx = Fixture(tmp_path)
219
+ fx.write_convergence_state(total_rounds=1)
220
+ progress = [
221
+ (T.format(1), "PROGRESS: phase-1-intake reading task bundle"),
222
+ (T.format(2), "PROGRESS: phase-1-intake complete"),
223
+ (T.format(3), "PROGRESS: phase-2-prompts preparing 3 worker prompts"),
224
+ (T.format(4), "PROGRESS: phase-3-team-create attempting TeamCreate"),
225
+ (T.format(5), "PROGRESS: phase-4-dispatch worker=claude-worker model=opus"),
226
+ (T.format(6), "PROGRESS: phase-4-dispatch worker=codex-worker model=gpt-5.5"),
227
+ (T.format(20), "PROGRESS: phase-5-collect worker=claude-worker status=completed"),
228
+ (T.format(21), "PROGRESS: phase-5-collect worker=codex-worker status=completed"),
229
+ (T.format(7), "PROGRESS: phase-batch-cleanup panes=3 teammates=3"),
230
+ (T.format(22), "PROGRESS: phase-5.5-convergence round=1 queue=3"),
231
+ (T.format(28), "PROGRESS: phase-batch-cleanup panes=2 teammates=2"),
232
+ (T.format(30), "PROGRESS: phase-6-synthesis dispatching report-writer-worker"),
233
+ (T.format(40), "PROGRESS: phase-7-persist updating manifests"),
234
+ ]
235
+ fx.write_jsonl([_text_record(ts, line) for ts, line in progress])
236
+ fx.write_heartbeat()
237
+ result = fx.validate(module)
238
+ assert result.errors == []
239
+ assert result.ok
240
+ ```
241
+
242
+ 주의: `T.format(7)` 이 list 상 `T.format(20/21)` 뒤에 오지만, 검증기는 `by_phase` 안에서 ts 로 정렬(`sorted`)하므로 list 순서 무관 — `T.format(7) <= T.format(22)`(round 1) 이 성립해 R1 충족. (`T = "2026-06-08T01:{:02d}:00Z"` 의 `{:02d}` 가 항상 zero-pad → `01:07:00 <= 01:22:00` lexicographic = 시간순 보장. 확인 완료, 추가 조치 불필요.)
243
+
244
+ - [ ] **Step 8: 신규 테스트 3종 통과 확인**
245
+
246
+ Run: `python3 -m pytest tests/contract/test_validate_session_conformance.py -v`
247
+ Expected: PASS (전체).
248
+
249
+ - [ ] **Step 9: 계약 문서 — 필수 마커 목록에 추가**
250
+
251
+ `prompts/lead/okstra-lead-contract.md` line 103 (`phase-6-synthesis` 마커) **앞**에 한 줄 삽입:
252
+
253
+ ```markdown
254
+ - `PROGRESS: phase-batch-cleanup panes=<n> teammates=<m>` — 이전 배치 pane·완료 teammate 를 정리한 직후, 각 배치 경계에서(① 첫 `phase-5.5-convergence` round 직전 ② `phase-6-synthesis` report-writer dispatch 직전). 카운트만 노출하고 `%NNN`/lead-pane.id/raw Agent name 은 절대 노출하지 않는다. 첫 배치(분석 워커 dispatch) 직전엔 치울 게 없어 no-op 이며 마커도 생략.
255
+ ```
256
+
257
+ - [ ] **Step 10: 계약 문서 — Enforcement 문구 갱신**
258
+
259
+ `prompts/lead/okstra-lead-contract.md` line 112 의 Enforcement 문단에서, 검사 항목 열거에 cleanup 을 포함하도록 수정. 기존 `... including the per-worker `phase-4-dispatch` / `phase-5-collect` lines, which must name each worker's role.` 를:
260
+
261
+ ```markdown
262
+ ... including the per-worker `phase-4-dispatch` / `phase-5-collect` lines (which must name each worker's role) and the `phase-batch-cleanup` lines that MUST precede the first `phase-5.5-convergence` round and the `phase-6-synthesis` report-writer dispatch.
263
+ ```
264
+
265
+ - [ ] **Step 11: `_common-contract.md` — freeform 상태줄을 구조화 마커로 교체**
266
+
267
+ `prompts/profiles/_common-contract.md` line 43 의 마지막 문장에서 freeform 예시(`이전 배치 okstra pane 3개 · teammate 2명 정리`)를 마커 emit 으로 교체. 기존:
268
+
269
+ ```markdown
270
+ - Both (a) and (b) run **automatically without any interactive prompt**. Still emit one short, plain-language status line for the pair (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:** ...
271
+ ```
272
+
273
+ 교체 후(앞부분만 수정, `**Effect caveat:**` 이후는 유지):
274
+
275
+ ```markdown
276
+ - Both (a) and (b) run **automatically without any interactive prompt**. After running them, emit the checkpoint line `PROGRESS: phase-batch-cleanup panes=<n> teammates=<m>` (counts only — NEVER expose `%NNN`, `lead-pane.id`, or raw Agent names) and proceed; this is the BLOCKING checkpoint the Phase 7 validator requires before each convergence round and before the report-writer dispatch (`prompts/lead/okstra-lead-contract.md` "Progress reporting (BLOCKING)"). **Effect caveat:** ...
277
+ ```
278
+
279
+ 그리고 line 37 의 boundary 설명이 마커 기준임을 명확히 — 기존 `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`` 는 그대로 두되, 이미 정확하므로 수정 불필요(확인만).
280
+
281
+ - [ ] **Step 12: 마커명 일관성 grep**
282
+
283
+ Run: `grep -rn "phase-batch-cleanup" prompts/ validators/ tests/`
284
+ Expected: `okstra-lead-contract.md`(마커 목록 + enforcement), `_common-contract.md`(emit 문구), `validate_session_conformance.py`(헬퍼), `test_validate_session_conformance.py`(HAPPY_PROGRESS + 테스트 3종) — 철자 `phase-batch-cleanup` 로 전부 일치, 오타/변형 없음.
285
+
286
+ - [ ] **Step 13: 빌드 + 전체 CI**
287
+
288
+ Run: `npm run build && npm run check`
289
+ Expected: `npm run build` 가 `runtime/` 동기화(특히 `runtime/prompts/`, `runtime/python/`), `npm run check` exit 0.
290
+
291
+ - [ ] **Step 14: 커밋**
292
+
293
+ ```bash
294
+ git add validators/validate_session_conformance.py \
295
+ tests/contract/test_validate_session_conformance.py \
296
+ prompts/lead/okstra-lead-contract.md \
297
+ prompts/profiles/_common-contract.md \
298
+ runtime/
299
+ git commit -m "feat(conformance): enforce phase-batch-cleanup checkpoint before each batch boundary"
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Task 2 (선택, defense-in-depth): cleanup 스크립트 anchor fallback
305
+
306
+ **근거:** Task 1 로 lead 가 정리를 실제 수행하면, tmux 안에서 실행된 run 은 `lead-pane.id` 가 정상 기록되어 `okstra-trace-cleanup.sh` 가 worker-agent pane 까지 닫는다. 다만 `lead-pane.id` 기록이 실패하거나 foreign pane 으로 해석되면 worker-agent title 스캔(`scripts/okstra-trace-cleanup.sh:165`)이 통째로 skip 되어 `claude-worker` 류 pane 이 남는다. 같은 run 의 태그된 trace pane 을 세션 anchor 로 써서 이 경우를 보강한다. **이 Task 는 mixed run(codex/antigravity 가 있어 `-tail` trace pane 이 존재)에서만 효과** — 순수 claude run 은 태그 anchor 가 없어 여전히 `lead-pane.id` 의존. 그래서 선택 사항.
307
+
308
+ **Files:**
309
+ - Modify: `scripts/okstra-trace-cleanup.sh` (`collect_okstra_panes` line 131-181)
310
+ - Test: `tests/worktree/test_okstra_trace_cleanup.py` (real-tmux, tmux 없으면 skip)
311
+
312
+ **Interfaces:**
313
+ - Consumes: 기존 tag 스캔이 채우는 `panes` 배열, `lead_pane`, `_tag_in_scope`.
314
+ - Produces: `lead_pane` 미해석 시에도 태그된 in-scope pane 의 세션에서 worker-agent title 스캔 수행.
315
+
316
+ - [ ] **Step 1: 실패 테스트 작성**
317
+
318
+ `tests/worktree/test_okstra_trace_cleanup.py` 기존 패턴(`_new_detached_session`/`_split_pane`/`@okstra_trace_run` 태깅)을 따라 추가. lead-pane.id 를 **쓰지 않고**, 태그된 trace pane + `claude-worker` 제목 pane 을 같은 세션에 두고 `--list` 가 worker-agent pane 을 집어내는지 검증:
319
+
320
+ ```python
321
+ def test_worker_agent_pane_found_via_tag_anchor_without_lead_pane_id(session_name, tmp_path):
322
+ if not _TMUX_SPAWN_AVAILABLE:
323
+ pytest.skip("tmux detached sessions not available")
324
+ run_dir = tmp_path / "run"
325
+ (run_dir / "state").mkdir(parents=True) # lead-pane.id 는 일부러 쓰지 않음
326
+
327
+ lead = _new_detached_session(session_name)
328
+ trace = _split_pane(session_name)
329
+ worker = _split_pane(session_name)
330
+ _tmux("set-option", "-p", "-t", trace, "@okstra_trace_run", str(run_dir.resolve()))
331
+ _tmux("select-pane", "-t", worker, "-T", "claude-worker")
332
+
333
+ res = subprocess.run(
334
+ ["bash", str(SCRIPT), "--list", "--run-dir", str(run_dir)],
335
+ capture_output=True, text=True, check=False,
336
+ )
337
+ out_ids = {line.split("\t", 1)[0] for line in res.stdout.strip().splitlines() if line}
338
+ assert worker in out_ids, res.stdout # anchor fallback 으로 worker-agent pane 포착
339
+ assert lead not in out_ids # lead pane 은 절대 미포함
340
+ ```
341
+
342
+ (헬퍼 시그니처는 파일 상단 실제 정의와 맞춘다 — `_split_pane` 가 `session` 만 받는지, 반환이 pane_id 인지 line 85-90 확인.)
343
+
344
+ - [ ] **Step 2: 테스트 실패 확인**
345
+
346
+ Run: `python3 -m pytest tests/worktree/test_okstra_trace_cleanup.py::test_worker_agent_pane_found_via_tag_anchor_without_lead_pane_id -v`
347
+ Expected: FAIL — `lead_pane` 미해석 → 현재 코드는 title 스캔 자체를 skip → `worker` pane 이 출력에 없음.
348
+
349
+ - [ ] **Step 3: anchor fallback 구현**
350
+
351
+ `scripts/okstra-trace-cleanup.sh` `collect_okstra_panes`(line 131-181) 수정. (1) tag 스캔 루프에서 in-scope 첫 pane 을 `scope_anchor` 로 기억, (2) title 스캔을 `lead_pane` 과 `scope_anchor` 두 세션에 대해 수행(dedup 은 기존 line 178-180 이 처리). 기존 `local pid trace_tag worker_tag status_tag title` 선언에 `scope_anchor` 추가하고, in-scope 매치 직후 `[[ -z "$scope_anchor" ]] && scope_anchor="$pid"` 한 줄 추가. title 스캔 블록(line 165-175)을 교체:
352
+
353
+ ```bash
354
+ # (2) Title-allowlisted worker-agent panes. Scope to the lead's session AND
355
+ # (fallback when lead-pane.id was unrecorded or resolved to a foreign pane) the
356
+ # session of any tagged in-scope pane found above — both are THIS run's session,
357
+ # so the scan never reaches unrelated sessions. dedup below drops overlaps.
358
+ if [[ "$REAP" -eq 0 && "$RECLAIM" -eq 0 ]]; then
359
+ local -a _scopes=()
360
+ [[ -n "$lead_pane" ]] && _scopes+=("$lead_pane")
361
+ [[ -n "$scope_anchor" ]] && _scopes+=("$scope_anchor")
362
+ local _scope
363
+ for _scope in "${_scopes[@]}"; do
364
+ while IFS=$'\t' read -r pid title; do
365
+ [[ -n "$pid" ]] || continue
366
+ [[ "$pid" == "$lead_pane" ]] && continue
367
+ case "$title" in
368
+ *claude-worker*|*codex-worker*|*antigravity-worker*|*report-writer-worker*)
369
+ panes+=("$pid") ;;
370
+ esac
371
+ done < <(tmux list-panes -s -t "$_scope" \
372
+ -F '#{pane_id}'$'\t''#{pane_title}' 2>/dev/null || true)
373
+ done
374
+ fi
375
+ ```
376
+
377
+ - [ ] **Step 4: 테스트 통과 확인**
378
+
379
+ Run: `python3 -m pytest tests/worktree/test_okstra_trace_cleanup.py -v`
380
+ Expected: PASS (신규 + 기존 전부). lead-pane.id 가 있는 기존 테스트도 `_scopes` 에 `lead_pane` 우선 포함되어 동작 동일.
381
+
382
+ - [ ] **Step 5: 빌드 + CI + 커밋**
383
+
384
+ ```bash
385
+ npm run build && npm run check
386
+ git add scripts/okstra-trace-cleanup.sh tests/worktree/test_okstra_trace_cleanup.py runtime/
387
+ git commit -m "fix(cleanup): scope worker-agent pane scan via tagged anchor when lead pane unresolved"
388
+ ```
389
+
390
+ ---
391
+
392
+ ## 범위 밖 (이번 계획에서 다루지 않음)
393
+
394
+ - **완료 감지 강제화(Fix 3):** 사용자의 최초 질문이었으나 스크린샷상 이번 run 은 감지가 동작 중(`@report-writer-2 came to rest` → result-missing 분류까지 진행). lead 가 idle 알림 대신 항상 background 폴링 루프를 띄우게 하는 enforcement 는 별도 설계 대상(`team-contract.md` "Worker-completion detection")이라 분리. 필요 시 후속 계획으로.
395
+ - **roster idle-pill 잔상:** `_common-contract.md:43` 의 "Effect caveat" — Claude Code 가 mid-run roster 항목을 외과적으로 지우는 도구를 노출하지 않음. tmux **pane** 은 이 계획으로 닫히지만 FleetView **pill** 잔상은 harness 한계로 남음(고칠 수 없음, 문서화된 상태 유지).
396
+
397
+ ---
398
+
399
+ ## Self-Review
400
+
401
+ **1. Spec coverage:**
402
+ - "phase 전환 시 정리 안 됨 → 화면 복잡" 근본원인(강제 부재) → Task 1 (마커 + 검증기).
403
+ - "정리하고 다음 팀원 생성" 두 경계(convergence 직전 / report-writer 직전) → Task 1 R1+R2.
404
+ - pane 이 실제로 안 닫히는 보조 결함(`lead_pane` skip) → Task 2.
405
+ - 최초 질문(완료 감지) → 범위 밖으로 명시 + 근거.
406
+
407
+ **2. Placeholder scan:** 모든 Step 에 실제 코드/명령/기대출력 기재. TBD/"적절히 처리" 없음. `T` zero-pad 가정은 Step 7 에서 grep 확인 + 대체값 명시로 방어.
408
+
409
+ **3. Type consistency:** 마커 토큰 `phase-batch-cleanup` 4개 파일 동일(Step 12 grep 으로 강제). 헬퍼 시그니처 `_check_batch_cleanup_checkpoints(by_phase, convergence_ran, report_writer_dispatched, errors)` — 호출부(Step 4)와 정의(Step 3) 인자 순서·이름 일치. `by_phase` 값 타입 `(ts, line)` — 기존 line 358-360 과 동일. `_convergence_rounds_ran`/`_DISPATCHED_STATUSES`/`_is_report_writer` 모두 기존 심볼 재사용.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.98.2",
3
+ "version": "0.99.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.98.2",
3
- "builtAt": "2026-06-22T12:25:00.654Z",
2
+ "package": "0.99.0",
3
+ "builtAt": "2026-06-22T18:58:09.887Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -74,7 +74,7 @@ Follow the **Worker output sections** contract in the preamble you Read via the
74
74
 
75
75
  ## Stop Condition (BLOCKING)
76
76
 
77
- When Lead dispatches you with `team_name` (Teams mode), its `Agent()` call returns `Spawned successfully` **immediately** and does NOT block on your completion — Lead detects your completion by self-scheduled polling of your worker-results file (see `team-contract` "Worker-completion detection (self-scheduled polling)"). Therefore you MUST write your worker-results file at the canonical Result Path before returning: that file's appearance is the ONLY completion signal Lead uses. Lingering after your worker-results file is on disk extends Phase 4 wall-clock time for the entire run and delays convergence. Be deliberate about stopping.
77
+ When Lead dispatches you with `run_in_background: true`, its `Agent()` call returns `Spawned successfully` **immediately** and does NOT block on your completion — Lead detects your completion by self-scheduled polling of your worker-results file (see `team-contract` "Worker-completion detection (self-scheduled polling)"). Therefore you MUST write your worker-results file at the canonical Result Path before returning: that file's appearance is the ONLY completion signal Lead uses. Lingering after your worker-results file is on disk extends Phase 4 wall-clock time for the entire run and delays convergence. Be deliberate about stopping.
78
78
 
79
79
  After your `Write` to the assigned worker-results file (path provided by Lead as `**Result Path:**` — the canonical anchor header defined in `team-contract` "Worker Prompt Composition" — or derived under `runs/<task-type>/worker-results/claude-worker-<task-type>-<seq>.md`) succeeds:
80
80
 
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
3
  # okstra-team-reconcile.sh — flip dead-pane stale-active team members to
4
- # inactive so the lead's `TeamDelete()` can disband the team in one shot.
4
+ # inactive so run-end teammate dismissal leaves a clean FleetView roster.
5
5
  #
6
6
  # A Claude Code team member clears its own `isActive` flag in
7
7
  # `~/.claude/teams/<team>/config.json` when its `Agent()` dispatch returns. A
8
- # member whose tmux pane died WITHOUT that flip stays `isActive: true`, and
9
- # `TeamDelete` then refuses the whole team ("active members remain") an error
10
- # no re-sent `shutdown_request` can clear, since the addressee is already gone.
11
- # This reconciles exactly that case; it never touches a live-pane member, the
12
- # lead, or a member with no recorded pane (those are left for graceful
13
- # shutdown). It no-ops when tmux is unavailable or nothing is stale.
8
+ # member whose tmux pane died WITHOUT that flip stays `isActive: true` and keeps
9
+ # showing as a live member that no re-sent `shutdown_request` can clear, since
10
+ # the addressee is already gone. This reconciles exactly that case; it never
11
+ # touches a live-pane member, the lead, or a member with no recorded pane (those
12
+ # are left for graceful shutdown). It no-ops when tmux is unavailable or nothing
13
+ # is stale.
14
14
  #
15
15
  # Usage: okstra-team-reconcile.sh [--list] <team-name>
16
16
  # --list report what WOULD be deactivated; do not write (alias --dry-run).
@@ -7,7 +7,7 @@
7
7
  3. **YAGNI — build only for current requirements** — No speculative parameters, optional configs, "future-proof" hooks, or pre-1.0 backwards-compat shims. *Self-check: every newly introduced identifier has ≥1 current internal caller, or was explicitly user-requested.* *Example violation: adding `options?: { retries?, timeout? }` parameter when the current call passes nothing.*
8
8
  4. **Clean Code — names carry WHAT, comments explain WHY** — Identifiers must make intent obvious; if a comment would describe WHAT the code does, rename instead. Reserve comments for non-obvious WHY (hidden constraint, workaround, surprising invariant). Delete dead/commented-out code immediately — git history is the archive. *Example — WHAT (rename instead): `// increment counter` above `i++`. WHY (keep): `// retry up to 3x: upstream returns 502 during deploys`.*
9
9
  5. **Function length cap — 50 lines** — A single function/method body must stay within 50 lines, counting only effective code (exclude blank lines, comments, and pure data declarations such as large enums, lookup tables, or constant maps). Crossing the cap is an extraction signal, not a style nit. *Self-check: for any function newly added or substantially edited, count effective body lines; if over 50, split before declaring complete, or surface the violation and confirm with the user.*
10
-
10
+ 6. **Simplest conforming approach** — When choosing how to implement or fix something, pick the simplest, most direct solution path that still conforms to the project's architecture and established conventions. This is broader than KISS (#2): KISS bounds abstraction; this bounds the whole approach. Simplicity never licenses bending the structure or a convention — rule out non-conforming paths first, then take the plainest of what remains. Self-check: name the architecture/convention boundary the chosen approach respects; if a simpler path respects it too, take that one.
11
11
 
12
12
  ## Routed resource selection
13
13
 
@@ -149,6 +149,6 @@ Information produced after completing this contract:
149
149
  - Reference list of config files/deployment manifests and task-level expected values
150
150
  - Current run status and presence of existing worker results
151
151
  - Current run prompt history contract for attempted workers
152
- - Candidate `teamName` for Phase 3 hand-off: `okstra-<task-key>` (with task-key slugified per Step 1's slug rule); implementation stage runs append `-s<N>` — the launch prompt's Team Creation Gate block carries the final name verbatim
152
+ - Candidate `teamName` for Phase 3 hand-off: `okstra-<task-key>` (with task-key slugified per Step 1's slug rule); implementation stage runs append `-s<N>` — the launch prompt's Teammate Dispatch Gate block carries the final name verbatim
153
153
  - Current Claude `lead.sessionId` (the in-flight Claude Code session) — required by `team-contract` when registering the lead in `team-state.json`
154
154
  - Resume command path: from `task-manifest.json` → `latestResumeCommandPath` (fallback: latest `runs/<task-type>/sessions/claude-resume-*.sh` by mtime). Never reconstruct the filename — the `<seq>` counter is category-local and may diverge from `manifests/`.
@@ -258,20 +258,19 @@ Agent(
258
258
  prompt: "<re-verification prompt with findings batch>",
259
259
  name: "<role-slug>-reverify-r<N>",
260
260
  subagent_type: "<same as initial execution>",
261
- team_name: "<teamName recorded in team-state>",
261
+ run_in_background: true, # no team_name — removed in CC v2.1.178; joins the session's implicit team
262
262
  model: "<same as initial execution>",
263
263
  mode: "auto"
264
264
  )
265
265
  ```
266
266
 
267
- - Agent Teams mode: Spawn within an existing team
268
- - Fallback mode: Spawn with `run_in_background: true` and no `team_name`
267
+ - Claude / in-process worker: spawn with `Agent(name: ..., run_in_background: true)` and no `team_name` — teammates join the session's implicit team (split-pane when `$TMUX` is set)
269
268
  - CLI (Codex / Antigravity) worker: inject `**Pane role:** worker-reverify-r<N>` (the Agent `name` minus the `<cli>-` prefix) into the reverify prompt body so the tmux trace pane reads `<cli>-worker-reverify-r<N>` per [okstra-lead-contract](./okstra-lead-contract.md) "Agent `name` on dispatch".
270
269
 
271
270
  For `tmux-pane` backend runs, do not use the Agent snippet. For each reverify round, write a jobs file at `runs/<task-type>/state/reverify-jobs-r<N>-<task-type>-<seq>.json` with `dispatchKind: "reverify-r<N>"` and worker entries containing `workerId`, `provider`, `role` (set to `worker-reverify-r<N>` for the pane title), `modelExecutionValue`, `promptPath`, `resultPath`, `workerResultPath`, and `completionPaths`; then run `okstra team dispatch --project-root <dir> --run-manifest <path> --dispatch-kind reverify-r<N> --jobs-file <jobs-file>` followed by `okstra team await --project-root <dir> --run-manifest <path>`.
272
271
 
273
272
 
274
- **Completion detection per round (BLOCKING).** Each round dispatches a variable set (1..N) of reverify workers asynchronously; the `Agent(... team_name ...)` calls return `Spawned successfully` immediately, which is NOT completion. Lead MUST detect each round's completion via the self-scheduled polling protocol in [team-contract](./team-contract.md) "Worker-completion detection (self-scheduled polling)", with the pending set reconstructed from that round's dispatched workers' Result Paths — do NOT restate the algorithm here. Lead MUST NOT treat the spawn ack as completion and MUST NOT end its turn with a prose "waiting" statement.
273
+ **Completion detection per round (BLOCKING).** Each round dispatches a variable set (1..N) of reverify workers asynchronously; the `Agent(name: ..., run_in_background: true)` calls return `Spawned successfully` immediately, which is NOT completion. Lead MUST detect each round's completion via the self-scheduled polling protocol in [team-contract](./team-contract.md) "Worker-completion detection (self-scheduled polling)", with the pending set reconstructed from that round's dispatched workers' Result Paths — do NOT restate the algorithm here. Lead MUST NOT treat the spawn ack as completion and MUST NOT end its turn with a prose "waiting" statement.
275
274
 
276
275
  ### Required reverify-prompt anchor headers (BLOCKING)
277
276