okstra 0.97.1 → 0.98.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 (74) hide show
  1. package/README.kr.md +4 -3
  2. package/README.md +4 -3
  3. package/docs/kr/architecture.md +2 -2
  4. package/docs/kr/cli.md +2 -0
  5. package/docs/kr/container.md +122 -0
  6. package/docs/project-structure-overview.md +12 -2
  7. package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +714 -0
  8. package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +125 -0
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/antigravity-worker.md +2 -18
  12. package/runtime/agents/workers/claude-worker.md +10 -33
  13. package/runtime/agents/workers/codex-worker.md +2 -18
  14. package/runtime/agents/workers/report-writer-worker.md +2 -10
  15. package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +5 -2
  16. package/runtime/prompts/profiles/_clarification-recommendation.md +1 -0
  17. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  18. package/runtime/prompts/profiles/_common-contract.md +3 -3
  19. package/runtime/prompts/profiles/_coverage-critic.md +17 -0
  20. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  21. package/runtime/prompts/profiles/_implementation-executor.md +2 -2
  22. package/runtime/prompts/profiles/_implementation-self-check.md +2 -1
  23. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  24. package/runtime/prompts/profiles/_stage-discipline.md +5 -8
  25. package/runtime/prompts/profiles/error-analysis.md +5 -5
  26. package/runtime/prompts/profiles/final-verification.md +4 -4
  27. package/runtime/prompts/profiles/implementation-planning.md +7 -7
  28. package/runtime/prompts/profiles/implementation.md +5 -5
  29. package/runtime/prompts/profiles/improvement-discovery.md +6 -6
  30. package/runtime/prompts/profiles/release-handoff.md +3 -3
  31. package/runtime/prompts/profiles/requirements-discovery.md +4 -4
  32. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  33. package/runtime/python/okstra_ctl/__init__.py +4 -1
  34. package/runtime/python/okstra_ctl/container.py +970 -0
  35. package/runtime/python/okstra_ctl/container_registry.py +93 -0
  36. package/runtime/python/okstra_ctl/error_zip.py +4 -4
  37. package/runtime/python/okstra_ctl/handoff.py +7 -1
  38. package/runtime/python/okstra_ctl/ids.py +40 -1
  39. package/runtime/python/okstra_ctl/listing.py +3 -5
  40. package/runtime/python/okstra_ctl/log_report.py +102 -0
  41. package/runtime/python/okstra_ctl/pane_reclaim.py +5 -4
  42. package/runtime/python/okstra_ctl/paths.py +40 -0
  43. package/runtime/python/okstra_ctl/plan_run_root.py +78 -0
  44. package/runtime/python/okstra_ctl/reconcile.py +4 -2
  45. package/runtime/python/okstra_ctl/resolve_task_key.py +54 -0
  46. package/runtime/python/okstra_ctl/run.py +48 -30
  47. package/runtime/python/okstra_ctl/stage_integrate.py +8 -2
  48. package/runtime/python/okstra_ctl/stage_targets.py +79 -0
  49. package/runtime/python/okstra_ctl/time_report.py +200 -0
  50. package/runtime/python/okstra_ctl/tmux.py +67 -0
  51. package/runtime/python/okstra_ctl/wizard.py +35 -20
  52. package/runtime/python/okstra_project/__init__.py +2 -0
  53. package/runtime/python/okstra_project/state.py +50 -2
  54. package/runtime/skills/okstra-brief/SKILL.md +17 -7
  55. package/runtime/skills/okstra-container/SKILL.md +169 -0
  56. package/runtime/skills/okstra-inspect/SKILL.md +64 -178
  57. package/runtime/skills/okstra-memory/SKILL.md +8 -6
  58. package/runtime/skills/okstra-run/SKILL.md +4 -4
  59. package/runtime/skills/okstra-schedule/SKILL.md +9 -16
  60. package/runtime/skills/okstra-setup/SKILL.md +10 -9
  61. package/runtime/templates/reports/brief.template.md +1 -1
  62. package/runtime/templates/reports/final-report.template.md +1 -1
  63. package/runtime/templates/reports/settings.template.json +3 -1
  64. package/runtime/validators/validate-implementation-plan-stages.py +11 -3
  65. package/src/cli-registry.mjs +28 -0
  66. package/src/commands/inspect/container.mjs +27 -0
  67. package/src/commands/inspect/log-report.mjs +26 -0
  68. package/src/commands/inspect/resolve-task-key.mjs +26 -0
  69. package/src/commands/inspect/task-list.mjs +26 -15
  70. package/src/commands/inspect/time-report.mjs +26 -0
  71. package/src/commands/lifecycle/check-project.mjs +7 -1
  72. package/src/commands/lifecycle/doctor.mjs +16 -0
  73. package/src/lib/skill-catalog.mjs +1 -0
  74. package/runtime/agents/TODO.md +0 -226
@@ -0,0 +1,714 @@
1
+ # okstra-container 로컬 유저-테스트 컨테이너 오케스트레이션 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:** 검증된(또는 임의) task-id 의 코드를 기존 `docker-compose.yml` 을 재사용해 task 별 컨테이너 그룹으로 띄우고, detached tmux 세션에서 컨테이너별 watcher 로 상시 감시하는 비선형 스킬 `okstra-container` 를 추가한다.
6
+
7
+ **Architecture:** PHASE_SEQUENCE 밖의 sub-command 디스패치 스킬(`okstra-inspect` 골격 복제). 모든 진입(스킬/`bin okstra`/`okstra.sh`)은 신규 python entrypoint `scripts/okstra_ctl/container.py::provision_container_group()` 로 수렴(단일 참조점). 산출물은 전부 `.okstra/tasks/<group>/<task-id>/container/` 아래. 컨테이너 존재 SSOT 는 docker 라벨, `registry.json` 은 보조 인덱스.
8
+
9
+ **Tech Stack:** Python 3 (`scripts/okstra_ctl/`), Node ESM (`src/commands/`), Bash (e2e), docker compose, tmux.
10
+
11
+ **관련 스펙:** [2026-06-21-okstra-container-local-user-test-design.md](../specs/2026-06-21-okstra-container-local-user-test-design.md)
12
+
13
+ ## Global Constraints
14
+
15
+ - **오케스트레이션형(B):** 설정 파일을 **절대 생성하지 않는다.** `docker-compose.yml` 부재 시 무엇이 없는지 명시하고 `PrepareError` 로 중단. Dockerfile 단독도 동일 중단.
16
+ - **단일 참조점:** 스킬/`bin okstra`/`okstra.sh` 는 모두 `provision_container_group()` (및 sub-command 디스패치)로 수렴. 한 진입점이 다른 진입점을 래핑 금지.
17
+ - **artifact-home:** okstra 산출물은 전부 `<PROJECT_ROOT>/.okstra/` 아래. 원본 프로젝트 파일 변경 금지. compose 입력 `.env` 는 worktree(okstra 소유) 기준으로 해소.
18
+ - **explicit args over env:** 모든 튜닝 값(`scan_interval_seconds`, `healthcheck_timeout_seconds`, `healthcheck_interval_seconds`)은 named 인자. env var 금지.
19
+ - **미러 트리:** python 로직은 `scripts/okstra_ctl/` 만 편집하고 `npm run build` 가 `runtime/python/okstra_ctl/` 를 재생성. **모든 task 완료 전 `npm run build` 1회 필수.** (테스트는 `scripts/` 를 직접 import 하므로 build 누락이 단위 테스트로 안 잡힌다.)
20
+ - **CI = `npm run check`:** 완료 선언 전 `npm run check` 가 exit 0 이어야 한다(test:js 실패 시 test:py 안 돔).
21
+ - **함수 길이:** 신규/대폭 수정 함수는 유효 50줄 이하. 초과 시 분할.
22
+ - **tmux 가용성 SSOT:** `tmux_available()` ([scripts/okstra_ctl/tmux.py:42](../../../scripts/okstra_ctl/tmux.py)) 한 곳. `process.env.TMUX` 판정 금지.
23
+
24
+ ---
25
+
26
+ ## File Structure
27
+
28
+ **신규 (python):**
29
+ - `scripts/okstra_ctl/container.py` — entrypoint(`provision_container_group`, sub-command `main`), env override 합성, compose argv, watcher loop.
30
+ - `scripts/okstra_ctl/container_registry.py` — flock-guarded 보조 인덱스(session/pane/findings).
31
+
32
+ **수정 (python):**
33
+ - `scripts/okstra_ctl/paths.py` — `container_paths()` 순수 경로 계산.
34
+ - `scripts/okstra_ctl/ids.py` — `compose_project_name()`, `build_container_session_name()`.
35
+ - `scripts/okstra_ctl/stage_integrate.py` — `integrate_stages(..., teardown=True)` 토글.
36
+ - `scripts/okstra_ctl/stage_targets.py` — `resolve_and_integrate_whole_task()` 공유 헬퍼.
37
+ - `scripts/okstra_ctl/run.py` — final-verification 분기를 공유 헬퍼 호출로 치환.
38
+ - `scripts/okstra_ctl/tmux.py` — `new_detached_session()`, `split_container_pane()`(신규 `@okstra_container_run` 태그).
39
+
40
+ **신규/수정 (Node):**
41
+ - `src/commands/inspect/container.mjs` — thin shim → python 위임.
42
+ - `src/cli-registry.mjs` — `container` 명령 등록.
43
+ - `src/lib/skill-catalog.mjs` — `USER_SKILL_NAMES` 에 `okstra-container`.
44
+ - `.claude-plugin/plugin.json` — skills 배열에 동순서 추가.
45
+ - `src/commands/lifecycle/doctor.mjs` — docker WARN 점검(별도 변수).
46
+ - `templates/reports/settings.template.json` — docker 권한 시드.
47
+
48
+ **신규 (스킬/문서):**
49
+ - `skills/okstra-container/SKILL.md`
50
+
51
+ **신규 (테스트):**
52
+ - `tests/container/test_*.py` (신규 도메인 폴더)
53
+ - `tests-js/cli-registry.test.mjs` 등 카운트 갱신
54
+ - `tests-e2e/scenario-15-okstra-container-up-down.sh`
55
+
56
+ ---
57
+
58
+ ## Task 1: container 경로/네이밍 순수 헬퍼
59
+
60
+ **Files:**
61
+ - Modify: `scripts/okstra_ctl/paths.py` (참고: `task_dir` 정의부, `compute_run_paths` 반환 dict 패턴)
62
+ - Modify: `scripts/okstra_ctl/ids.py` (참고: `slugify_task_segment` ≈ line 82, `_safe_fs_segment` ≈ line 48)
63
+ - Test: `tests/container/test_okstra_container_paths.py`
64
+
65
+ **Interfaces:**
66
+ - Produces:
67
+ - `container_paths(project_root, task_group, task_id) -> dict` — keys: `container_dir`, `env_override`, `registry`, `registry_lock`, `deploy_state`, `watchers_dir` (모두 `pathlib.Path`).
68
+ - `compose_project_name(project_id, task_group, task_id) -> str` — `^[a-z0-9][a-z0-9_-]*$`, 길이 상한 `CONTAINER_PROJECT_NAME_MAX = 60`.
69
+
70
+ - [ ] **Step 1: 실패 테스트 작성** — `tests/container/test_okstra_container_paths.py`
71
+
72
+ ```python
73
+ import sys
74
+ from pathlib import Path
75
+ from _paths import REPO_ROOT # tests/_paths.py 의 헤더 패턴 그대로
76
+
77
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
78
+ from okstra_ctl import paths, ids # noqa: E402
79
+
80
+
81
+ def test_container_paths_under_task_root(tmp_path):
82
+ p = paths.container_paths(tmp_path, "Auth Work", "TASK-42")
83
+ base = tmp_path / ".okstra" / "tasks" / "auth-work" / "task-42" / "container"
84
+ assert p["container_dir"] == base
85
+ assert p["env_override"] == base / "env.override"
86
+ assert p["registry"] == base / "registry.json"
87
+ assert p["registry_lock"] == base / "registry.json.lock"
88
+ assert p["deploy_state"] == base / "deploy-state.json"
89
+ assert p["watchers_dir"] == base / "watchers"
90
+
91
+
92
+ def test_compose_project_name_is_docker_safe():
93
+ name = ids.compose_project_name("proj", "grp", "task-1")
94
+ assert name == "okstra-proj-grp-task-1"
95
+ import re
96
+ assert re.fullmatch(r"[a-z0-9][a-z0-9_-]*", name)
97
+
98
+
99
+ def test_compose_project_name_truncates_preserving_task(tmp_path):
100
+ name = ids.compose_project_name("p" * 40, "g" * 40, "task-keep")
101
+ assert len(name) <= ids.CONTAINER_PROJECT_NAME_MAX
102
+ assert name.endswith("task-keep")
103
+ import re
104
+ assert re.fullmatch(r"[a-z0-9][a-z0-9_-]*", name)
105
+
106
+
107
+ def test_compose_project_name_all_nonalnum_segments():
108
+ name = ids.compose_project_name("***", "###", "ok")
109
+ import re
110
+ assert re.fullmatch(r"[a-z0-9][a-z0-9_-]*", name) # 빈 슬러그/연속 하이픈 없음
111
+ ```
112
+
113
+ - [ ] **Step 2: 실패 확인**
114
+
115
+ Run: `python3 -m pytest tests/container/test_okstra_container_paths.py -v`
116
+ Expected: FAIL (`AttributeError: module 'okstra_ctl.paths' has no attribute 'container_paths'`)
117
+
118
+ - [ ] **Step 3: `container_paths` 구현** — `scripts/okstra_ctl/paths.py` 에 추가. 기존 `task_dir`/slug 헬퍼를 재사용한다(레포의 실제 `task_dir` 시그니처를 먼저 확인하고 그 슬러그 규칙을 그대로 통과시킬 것 — 라벨↔경로 일치를 위해).
119
+
120
+ ```python
121
+ def container_paths(project_root, task_group, task_id):
122
+ """container 산출물 경로 묶음(순수 계산, mkdir 금지)."""
123
+ base = task_dir(project_root, task_group, task_id) / "container"
124
+ return {
125
+ "container_dir": base,
126
+ "env_override": base / "env.override",
127
+ "registry": base / "registry.json",
128
+ "registry_lock": base / "registry.json.lock",
129
+ "deploy_state": base / "deploy-state.json",
130
+ "watchers_dir": base / "watchers",
131
+ }
132
+ ```
133
+
134
+ - [ ] **Step 4: `compose_project_name` 구현** — `scripts/okstra_ctl/ids.py` 에 추가. **세그먼트 정규화는 `task_dir` 가 쓰는 slug 함수와 동일 함수**를 사용해 라벨↔경로 일치를 보장한다(리뷰 minor: slug 분기 방지).
135
+
136
+ ```python
137
+ CONTAINER_PROJECT_NAME_MAX = 60
138
+
139
+
140
+ def compose_project_name(project_id, task_group, task_id):
141
+ def seg(v):
142
+ s = slugify_task_segment(v) # task_dir 가 쓰는 동일 slug 함수 사용
143
+ return s or "x" # 빈 슬러그 방지(docker 는 비어있지 않은 [a-z0-9] 시작 요구)
144
+ proj, grp, task = seg(project_id), seg(task_group), seg(task_id)
145
+ name = f"okstra-{proj}-{grp}-{task}"
146
+ if len(name) <= CONTAINER_PROJECT_NAME_MAX:
147
+ return name
148
+ # task-id 세그먼트 우선 보존: 뒤에서부터 채우고 앞 세그먼트를 깎는다
149
+ tail = f"-{task}"
150
+ budget = CONTAINER_PROJECT_NAME_MAX - len("okstra") - len(tail)
151
+ head = f"-{proj}-{grp}"[:max(budget, 0)].rstrip("-")
152
+ return f"okstra{head}{tail}"
153
+ ```
154
+
155
+ - [ ] **Step 5: 통과 확인 + slug 일치 회귀 단언 추가** — 두 slug 함수가 같은 입력에 같은 출력을 내는지 단언을 테스트에 추가(리뷰 minor).
156
+
157
+ Run: `python3 -m pytest tests/container/test_okstra_container_paths.py -v`
158
+ Expected: PASS (4 tests)
159
+
160
+ - [ ] **Step 6: 커밋**
161
+
162
+ ```bash
163
+ git add scripts/okstra_ctl/paths.py scripts/okstra_ctl/ids.py tests/container/test_okstra_container_paths.py
164
+ git commit -m "feat(container): container 경로/compose project name 순수 헬퍼"
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Task 2: whole-task 통합 공유 헬퍼 추출 (단일 참조점)
170
+
171
+ **배경:** final-verification 의 whole-task stage 통합은 `run.py` 의 `_reserve_final_verification_target` else 분기(≈1519-1543, [run.py](../../../scripts/okstra_ctl/run.py))에 인라인돼 있고 `ctx`/`PrepareInputs` 에 강결합돼 직접 호출 불가. 그대로 래핑하면 단일 참조점 위반 → 순수 헬퍼로 추출한다. `integrate_stages` 는 Phase B 에서 stage worktree teardown 까지 수행하므로 컨테이너용 `teardown=False` 토글이 필요(스펙 §10: 컨테이너는 머지만, stage 트리 보존).
172
+
173
+ **Files:**
174
+ - Modify: `scripts/okstra_ctl/stage_integrate.py` (`integrate_stages` / `_teardown_all`)
175
+ - Modify: `scripts/okstra_ctl/stage_targets.py` (`resolve_whole_task_target`, `is_dirty_excluding_okstra`)
176
+ - Modify: `scripts/okstra_ctl/run.py` (`_reserve_final_verification_target` else 분기)
177
+ - Test: `tests/container/test_resolve_and_integrate_whole_task.py`
178
+
179
+ **Interfaces:**
180
+ - Produces:
181
+ - `integrate_stages(..., teardown: bool = True)` — `False` 면 Phase B(`_teardown_all`) skip. 기존 호출부 행위 불변(기본 True).
182
+ - `resolve_and_integrate_whole_task(*, project_id, task_group, task_id, task_worktree_path, stage_map, done_rows, anchor_base, teardown) -> dict` — `{head_commit, merged_stage_map, target_ref}` 등. 충돌 시 `PrepareError`, dirty 시 거부.
183
+
184
+ - [ ] **Step 1: `teardown` 토글 실패 테스트** — `tests/container/test_resolve_and_integrate_whole_task.py`
185
+
186
+ ```python
187
+ import sys
188
+ from _paths import REPO_ROOT
189
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
190
+ from okstra_ctl import stage_integrate # noqa: E402
191
+ import inspect
192
+
193
+
194
+ def test_integrate_stages_has_teardown_toggle():
195
+ sig = inspect.signature(stage_integrate.integrate_stages)
196
+ assert "teardown" in sig.parameters
197
+ assert sig.parameters["teardown"].default is True
198
+ ```
199
+
200
+ - [ ] **Step 2: 실패 확인**
201
+
202
+ Run: `python3 -m pytest tests/container/test_resolve_and_integrate_whole_task.py -v`
203
+ Expected: FAIL (`teardown` 파라미터 없음)
204
+
205
+ - [ ] **Step 3: `teardown` 토글 추가** — `scripts/okstra_ctl/stage_integrate.py`. `integrate_stages` 시그니처에 `teardown: bool = True` 추가, Phase B 직전에 `if teardown:` 가드. 기존 `run.py` 호출은 기본값으로 행위 불변.
206
+
207
+ - [ ] **Step 4: `resolve_and_integrate_whole_task` 실패 테스트(통합 동작)** — 임시 git worktree + stage 더미 fixture 로:
208
+
209
+ ```python
210
+ def test_whole_task_keeps_stage_trees_when_teardown_false(container_task_fixture):
211
+ res = stage_targets.resolve_and_integrate_whole_task(
212
+ teardown=False, **container_task_fixture.kwargs)
213
+ assert res["head_commit"]
214
+ assert container_task_fixture.stage_worktree.exists() # 보존
215
+
216
+
217
+ def test_whole_task_rejects_dirty_non_okstra(container_task_fixture):
218
+ container_task_fixture.write_dirty("src/x.py")
219
+ with pytest.raises(stage_targets.PrepareError):
220
+ stage_targets.resolve_and_integrate_whole_task(
221
+ teardown=False, **container_task_fixture.kwargs)
222
+ ```
223
+
224
+ > fixture(`container_task_fixture`)는 `tests/conftest.py` 격리 홈 패턴을 따르고, `git init` 한 task worktree + 1개 done stage 를 만든다. 충돌 케이스는 같은 파일을 두 stage 가 다르게 수정해 머지 충돌을 유도.
225
+
226
+ - [ ] **Step 5: 실패 확인**
227
+
228
+ Run: `python3 -m pytest tests/container/test_resolve_and_integrate_whole_task.py -v`
229
+ Expected: FAIL (`resolve_and_integrate_whole_task` 없음)
230
+
231
+ - [ ] **Step 6: 헬퍼 구현** — `scripts/okstra_ctl/stage_targets.py`. `integrate_stages(teardown=...)` 호출 → `IntegrateError`→`PrepareError` 변환 → `git rev-parse HEAD` → done `head_commit` ancestor 기반 merged map 계산(run.py 의 ground-truth 패턴 그대로) → `resolve_whole_task_target` 게이트 강제 → `is_dirty_excluding_okstra` 거부 게이트. 50줄 초과 시 ancestor-map 계산을 작은 헬퍼로 분리.
232
+
233
+ - [ ] **Step 7: `run.py` 분기 치환** — `_reserve_final_verification_target` else 분기의 인라인 로직을 `resolve_and_integrate_whole_task(teardown=True, ...)` 호출로 치환. `ctx` 주입은 run.py 측 유지(ctx 결합은 run 전용).
234
+
235
+ - [ ] **Step 8: 통과 + final-verification 회귀 확인**
236
+
237
+ Run: `python3 -m pytest tests/container/test_resolve_and_integrate_whole_task.py tests/run/ -v`
238
+ Expected: PASS (신규 + 기존 final-verification 테스트 회귀 없음)
239
+
240
+ - [ ] **Step 9: 커밋**
241
+
242
+ ```bash
243
+ git add scripts/okstra_ctl/stage_integrate.py scripts/okstra_ctl/stage_targets.py scripts/okstra_ctl/run.py tests/container/test_resolve_and_integrate_whole_task.py
244
+ git commit -m "refactor(container): whole-task 통합을 teardown 토글 공유 헬퍼로 추출"
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Task 3: container registry (flock-guarded 보조 인덱스)
250
+
251
+ **Files:**
252
+ - Create: `scripts/okstra_ctl/container_registry.py` (참고: `worktree_registry.py` 의 `_registry_lock`/`_load`/`_save` ≈80-122 — fcntl.flock LOCK_EX contextmanager + tmp+`os.replace` 원자 쓰기)
253
+ - Test: `tests/container/test_okstra_container_registry.py`
254
+
255
+ **Interfaces:**
256
+ - Consumes: `paths.container_paths()` (Task 1) 의 `registry`, `registry_lock`.
257
+ - Produces:
258
+ - `reserve(project_root, group, task_id, service, *, session_name, pane_id, findings_path) -> None`
259
+ - `lookup(project_root, group, task_id) -> dict` — `{"services": {svc: {session_name, pane_id, findings_path}}}`
260
+ - `release(project_root, group, task_id, service) -> None`
261
+ - `clear(project_root, group, task_id) -> None`
262
+
263
+ - [ ] **Step 1: 왕복 실패 테스트** — `tests/container/test_okstra_container_registry.py`
264
+
265
+ ```python
266
+ import sys
267
+ from _paths import REPO_ROOT
268
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
269
+ from okstra_ctl import container_registry as cr # noqa: E402
270
+
271
+
272
+ def test_reserve_then_lookup_roundtrip(tmp_path):
273
+ cr.reserve(tmp_path, "g", "t1", "web",
274
+ session_name="okstra-container-t1", pane_id="%5",
275
+ findings_path="/x/web-findings.md")
276
+ got = cr.lookup(tmp_path, "g", "t1")
277
+ assert got["services"]["web"]["pane_id"] == "%5"
278
+
279
+
280
+ def test_release_removes_service(tmp_path):
281
+ cr.reserve(tmp_path, "g", "t1", "web", session_name="s", pane_id="%1", findings_path="f")
282
+ cr.release(tmp_path, "g", "t1", "web")
283
+ assert "web" not in cr.lookup(tmp_path, "g", "t1")["services"]
284
+
285
+
286
+ def test_clear_empties_registry(tmp_path):
287
+ cr.reserve(tmp_path, "g", "t1", "web", session_name="s", pane_id="%1", findings_path="f")
288
+ cr.clear(tmp_path, "g", "t1")
289
+ assert cr.lookup(tmp_path, "g", "t1")["services"] == {}
290
+ ```
291
+
292
+ - [ ] **Step 2: 실패 확인**
293
+
294
+ Run: `python3 -m pytest tests/container/test_okstra_container_registry.py -v`
295
+ Expected: FAIL (모듈 없음)
296
+
297
+ - [ ] **Step 3: 구현** — `worktree_registry.py` 의 flock + 원자 쓰기 패턴을 복제하되, lock/registry 경로는 `container_paths()` 로 해소(전역 `worktrees/registry.json` 과 분리). 가벼운 dict 구조만 사용(무거운 dataclass 금지). 누락 파일은 `{"services": {}}` 로 초기화.
298
+
299
+ - [ ] **Step 4: 통과 확인**
300
+
301
+ Run: `python3 -m pytest tests/container/test_okstra_container_registry.py -v`
302
+ Expected: PASS (3 tests)
303
+
304
+ - [ ] **Step 5: 커밋**
305
+
306
+ ```bash
307
+ git add scripts/okstra_ctl/container_registry.py tests/container/test_okstra_container_registry.py
308
+ git commit -m "feat(container): flock-guarded container registry 보조 인덱스"
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Task 4: env.override 합성 + compose argv (순수 함수)
314
+
315
+ **Files:**
316
+ - Create: `scripts/okstra_ctl/container.py` (이 task 는 순수 함수 스켈레톤만)
317
+ - Test: `tests/container/test_container_compose_argv.py`
318
+
319
+ **Interfaces:**
320
+ - Consumes: Task 1 `container_paths`, `compose_project_name`.
321
+ - Produces:
322
+ - `merge_env(base_env: dict, override: dict) -> dict` — override 가 동일 키를 덮어씀(순수).
323
+ - `build_compose_up_argv(*, project_name, worktree_env_path, override_env_path, labels: dict) -> list[str]` — `--env-file` 순서 = worktree → override, 라벨 인자 포함.
324
+ - `OKSTRA_LABEL_KEYS = ("okstra.task-key", "okstra.project-name", "okstra.run-trace")`
325
+
326
+ - [ ] **Step 1: 실패 테스트** — `tests/container/test_container_compose_argv.py`
327
+
328
+ ```python
329
+ import sys
330
+ from _paths import REPO_ROOT
331
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
332
+ from okstra_ctl import container # noqa: E402
333
+
334
+
335
+ def test_merge_env_override_wins():
336
+ assert container.merge_env({"A": "1", "B": "2"}, {"B": "9"}) == {"A": "1", "B": "9"}
337
+
338
+
339
+ def test_compose_argv_env_file_order():
340
+ argv = container.build_compose_up_argv(
341
+ project_name="okstra-p-g-t", worktree_env_path="/wt/.env",
342
+ override_env_path="/ok/env.override",
343
+ labels={"okstra.task-key": "p:g:t"})
344
+ i = argv.index("--env-file")
345
+ assert argv[i + 1] == "/wt/.env"
346
+ j = argv.index("--env-file", i + 1)
347
+ assert argv[j + 1] == "/ok/env.override" # override 가 나중(우선)
348
+ assert "-p" in argv and "okstra-p-g-t" in argv
349
+
350
+
351
+ def test_compose_argv_carries_okstra_labels():
352
+ argv = container.build_compose_up_argv(
353
+ project_name="n", worktree_env_path="a", override_env_path="b",
354
+ labels={k: "v" for k in container.OKSTRA_LABEL_KEYS})
355
+ joined = " ".join(argv)
356
+ for k in container.OKSTRA_LABEL_KEYS:
357
+ assert f"{k}=v" in joined
358
+ ```
359
+
360
+ - [ ] **Step 2: 실패 확인**
361
+
362
+ Run: `python3 -m pytest tests/container/test_container_compose_argv.py -v`
363
+ Expected: FAIL (모듈/함수 없음)
364
+
365
+ - [ ] **Step 3: 구현** — `scripts/okstra_ctl/container.py` 생성. `merge_env`, `build_compose_up_argv`(`["docker","compose","-p",name,"--env-file",wt,"--env-file",override,"up","-d"]` + 라벨 `--label k=v`), `OKSTRA_LABEL_KEYS`. 라벨은 compose 의 프로젝트 레벨이 아니라 서비스 컨테이너에 붙어야 하므로, compose 가 라벨 주입을 직접 지원 안 하면 `up` 후 `docker compose ps -q` → `docker label` 대신 compose override 파일(`-f`)로 `labels:` 주입하는 방식을 택한다(구현 시 docker compose 버전 동작을 context7/docs 로 확인 — 라벨 주입 메커니즘이 버전 의존적).
366
+
367
+ > ⚠️ 구현 노트: compose 의 컨테이너 라벨 주입 방식(`x-` 확장 vs override `-f` vs `up --label` 미지원)을 실제 docker compose 문서로 확인할 것. SSOT 가 라벨이므로 라벨이 실제로 컨테이너에 붙는지가 핵심.
368
+
369
+ - [ ] **Step 4: 통과 확인**
370
+
371
+ Run: `python3 -m pytest tests/container/test_container_compose_argv.py -v`
372
+ Expected: PASS (3 tests)
373
+
374
+ - [ ] **Step 5: 커밋**
375
+
376
+ ```bash
377
+ git add scripts/okstra_ctl/container.py tests/container/test_container_compose_argv.py
378
+ git commit -m "feat(container): env override 합성 + compose up argv 순수 함수"
379
+ ```
380
+
381
+ ---
382
+
383
+ ## Task 5: detached 세션 + watcher/tail pane 헬퍼
384
+
385
+ **배경(리뷰 blocker):** `tag_pane()` 의 기본 옵션이 `@okstra_worker_run` 이고([tmux.py](../../../scripts/okstra_ctl/tmux.py) ≈145), reap scope([scripts/okstra-trace-cleanup.sh](../../../scripts/okstra-trace-cleanup.sh) ≈147 `_tag_in_scope`)는 `@okstra_trace_run` **OR** `@okstra_worker_run` 둘 다 본다. watcher pane 에 둘 중 하나라도 붙으면 SessionEnd `--reap` 이 Claude 종료 시 watcher 를 죽여 "세션 후 생존" 요구를 위반한다. → 신규 `@okstra_container_run` 태그만 사용.
386
+
387
+ **Files:**
388
+ - Modify: `scripts/okstra_ctl/tmux.py` (`tmux_available` ≈42, `tag_pane` ≈134-151, `split_worker_pane`)
389
+ - Modify: `scripts/okstra_ctl/ids.py`
390
+ - Test: `tests/container/test_container_session_name.py`, `tests/container/test_container_pane_tag.py`
391
+
392
+ **Interfaces:**
393
+ - Produces:
394
+ - `ids.build_container_session_name(project_id, group, task_id) -> "okstra-container-<slug>"`
395
+ - `tmux.new_detached_session(session_name, cwd, first_cmd) -> str` (session/pane id)
396
+ - `tmux.split_container_pane(session_pane, cwd, command, title, scope_value, kind) -> str|None` — 새 pane 을 `-t` 로 명시 지정, `set-pane -T <title>`, `@okstra_container_run <scope_value>` 태그만 부착. `session_pane` 가 빈값/None 이면 silently skip + 로그경로 반환(degrade).
397
+
398
+ - [ ] **Step 1: 세션명 + 태그 분리 실패 테스트**
399
+
400
+ ```python
401
+ # tests/container/test_container_session_name.py
402
+ def test_container_session_name_prefix():
403
+ from okstra_ctl import ids
404
+ assert ids.build_container_session_name("p", "g", "t-1") == "okstra-container-p-g-t-1"
405
+ ```
406
+
407
+ ```python
408
+ # tests/container/test_container_pane_tag.py — reap 태그 미사용을 정적으로 보장
409
+ def test_split_container_pane_uses_container_tag_only():
410
+ import inspect
411
+ from okstra_ctl import tmux
412
+ src = inspect.getsource(tmux.split_container_pane)
413
+ assert "@okstra_container_run" in src
414
+ assert "@okstra_trace_run" not in src
415
+ assert "@okstra_worker_run" not in src
416
+ ```
417
+
418
+ - [ ] **Step 2: 실패 확인**
419
+
420
+ Run: `python3 -m pytest tests/container/test_container_session_name.py tests/container/test_container_pane_tag.py -v`
421
+ Expected: FAIL
422
+
423
+ - [ ] **Step 3: `build_container_session_name` 구현** — `ids.py`, `slugify_task_segment` 재사용.
424
+
425
+ - [ ] **Step 4: `new_detached_session` + `split_container_pane` 구현** — `tmux.py`. `new_detached_session` 은 `cmd-rerun.sh` 의 detached spawn 패턴을 python 으로(`tmux new-session -d -s <name> -c <cwd> <cmd>`). `split_container_pane` 은 임의 `command` 를 받고 신규 `@okstra_container_run` 태그만 부착(`tag_pane` 의 기본 `@okstra_worker_run` 을 절대 거치지 않도록 옵션 명시 또는 별도 경로). watcher pane 제목은 `okstra-watcher-<service>` 규약 — `*-worker` 토큰 금지(리뷰 minor: title-allowlist reap 회피).
426
+
427
+ - [ ] **Step 5: 통과 확인**
428
+
429
+ Run: `python3 -m pytest tests/container/test_container_session_name.py tests/container/test_container_pane_tag.py -v`
430
+ Expected: PASS
431
+
432
+ - [ ] **Step 6: reap scope 회귀 grep** — `@okstra_container_run` 이 `okstra-trace-cleanup.sh` 의 reap scope 에 **포함되지 않음**을 확인.
433
+
434
+ Run: `rg -n '@okstra_container_run' scripts/okstra-trace-cleanup.sh; echo "exit=$?"`
435
+ Expected: 매치 없음 (exit=1)
436
+
437
+ - [ ] **Step 7: 커밋**
438
+
439
+ ```bash
440
+ git add scripts/okstra_ctl/tmux.py scripts/okstra_ctl/ids.py tests/container/test_container_session_name.py tests/container/test_container_pane_tag.py
441
+ git commit -m "feat(container): detached 세션 + container 전용 태그 pane 헬퍼"
442
+ ```
443
+
444
+ ---
445
+
446
+ ## Task 6: watcher (에러 트리거 2단계 스캔)
447
+
448
+ **Files:**
449
+ - Modify: `scripts/okstra_ctl/container.py` (watcher loop entrypoint)
450
+ - Test: `tests/container/test_container_watcher_scan.py`
451
+
452
+ **Interfaces:**
453
+ - Consumes: Task 1 `container_paths().watchers_dir`.
454
+ - Produces:
455
+ - `ERROR_PATTERNS` (정규식 화이트리스트), `scan_log_chunk(text, patterns) -> list[str]` (매칭 라인, 순수).
456
+ - `error_signature(line) -> str` (디바운스 키).
457
+ - `watcher_step(*, log_chunk, seen_signatures, findings_path, analyze) -> int` — 새 시그니처 수만큼 `analyze` 호출, findings append. `analyze` 는 주입(테스트 스텁 가능 = LLM 무의존).
458
+ - `run_watcher_loop(*, service, project_name, findings_path, scan_interval_seconds, analyze)` — 사용자 종료까지 지속.
459
+
460
+ - [ ] **Step 1: 실패 테스트**
461
+
462
+ ```python
463
+ # tests/container/test_container_watcher_scan.py
464
+ from okstra_ctl import container
465
+
466
+
467
+ def test_clean_logs_trigger_no_analysis(tmp_path):
468
+ calls = []
469
+ n = container.watcher_step(
470
+ log_chunk="all good\nrequest 200 ok\n", seen_signatures=set(),
471
+ findings_path=tmp_path / "f.md", analyze=lambda w: calls.append(w))
472
+ assert n == 0 and calls == []
473
+
474
+
475
+ def test_error_triggers_single_analysis(tmp_path):
476
+ calls = []
477
+ n = container.watcher_step(
478
+ log_chunk="ERROR boom at db\n", seen_signatures=set(),
479
+ findings_path=tmp_path / "f.md", analyze=lambda w: calls.append(w))
480
+ assert n == 1 and len(calls) == 1
481
+ assert (tmp_path / "f.md").exists()
482
+
483
+
484
+ def test_same_signature_debounced(tmp_path):
485
+ seen = set()
486
+ container.watcher_step(log_chunk="ERROR boom\n", seen_signatures=seen,
487
+ findings_path=tmp_path / "f.md", analyze=lambda w: None)
488
+ calls = []
489
+ container.watcher_step(log_chunk="ERROR boom\n", seen_signatures=seen,
490
+ findings_path=tmp_path / "f.md", analyze=lambda w: calls.append(w))
491
+ assert calls == [] # 2번째 동일 시그니처 skip
492
+ ```
493
+
494
+ - [ ] **Step 2: 실패 확인**
495
+
496
+ Run: `python3 -m pytest tests/container/test_container_watcher_scan.py -v`
497
+ Expected: FAIL
498
+
499
+ - [ ] **Step 3: 구현** — `scan_log_chunk`(정규식 매칭), `error_signature`(라인 정규화 후 해시/축약), `watcher_step`(새 시그니처만 `analyze` 호출 + findings append), `run_watcher_loop`(`docker compose logs --since <last>` 증분 fetch + `time.sleep(scan_interval_seconds)`). `analyze` 기본 구현은 codex/antigravity-worker 와 동일 pane 메커니즘으로 AI 에이전트 기동하되 **findings append 만 허용(read-only 보고 계약)** — 코드/설정 Edit/Write 금지를 프롬프트로 강제(스펙 §9 enforcement).
500
+
501
+ - [ ] **Step 4: 통과 확인**
502
+
503
+ Run: `python3 -m pytest tests/container/test_container_watcher_scan.py -v`
504
+ Expected: PASS (3 tests)
505
+
506
+ - [ ] **Step 5: 커밋**
507
+
508
+ ```bash
509
+ git add scripts/okstra_ctl/container.py tests/container/test_container_watcher_scan.py
510
+ git commit -m "feat(container): 에러 트리거 2단계 watcher 스캔"
511
+ ```
512
+
513
+ ---
514
+
515
+ ## Task 7: provision_container_group() 수렴 entrypoint + sub-command 디스패치
516
+
517
+ **Files:**
518
+ - Modify: `scripts/okstra_ctl/container.py` (`main`, `provision_container_group`, status/logs/stop-watcher/down)
519
+ - Modify: `scripts/okstra_ctl/__init__.py` (필요 헬퍼만 export)
520
+ - Test: `tests/container/test_container_up_gate.py`
521
+
522
+ **Interfaces:**
523
+ - Consumes: Task 2 `resolve_and_integrate_whole_task(teardown=False)`, Task 3 registry, Task 4 argv, Task 5 pane 헬퍼 + `tmux_available()`, Task 6 watcher.
524
+ - Produces:
525
+ - `provision_container_group(*, project_root, task_key, healthcheck_timeout_seconds=120, healthcheck_interval_seconds=3, scan_interval_seconds=...) -> dict`
526
+ - `main(argv) -> int` — argparse add_subparsers(`up`/`status`/`logs`/`stop-watcher`/`down`), `print(json.dumps(result))`, `__main__` 진입. (참고: `recap.py` ≈94-135 패턴)
527
+
528
+ - [ ] **Step 1: 게이트 실패 테스트(생성 금지·중단)**
529
+
530
+ ```python
531
+ # tests/container/test_container_up_gate.py
532
+ import pytest
533
+ from okstra_ctl import container
534
+
535
+
536
+ def test_up_aborts_when_no_compose(container_task_fixture):
537
+ # worktree 에 docker-compose.yml 없음
538
+ with pytest.raises(container.PrepareError) as e:
539
+ container.provision_container_group(**container_task_fixture.up_kwargs)
540
+ assert "docker-compose.yml" in str(e.value)
541
+
542
+
543
+ def test_up_aborts_with_dockerfile_only(container_task_fixture):
544
+ container_task_fixture.write_worktree_file("Dockerfile", "FROM scratch\n")
545
+ with pytest.raises(container.PrepareError) as e:
546
+ container.provision_container_group(**container_task_fixture.up_kwargs)
547
+ assert "docker-compose.yml" in str(e.value) # compose 없음으로 중단
548
+ ```
549
+
550
+ - [ ] **Step 2: 실패 확인**
551
+
552
+ Run: `python3 -m pytest tests/container/test_container_up_gate.py -v`
553
+ Expected: FAIL
554
+
555
+ - [ ] **Step 3: `provision_container_group` 구현(오케스트레이션, ≤50줄, 단계별 헬퍼 분리)** — 순서: ① `resolve_and_integrate_whole_task(teardown=False)` 로 소스트리/미통합 stage 머지(충돌 시 부분통합 상태 + 충돌파일 + 재시도 안전(already_merged 멱등) 안내 메시지로 `PrepareError`) → ② worktree 루트 `docker-compose.yml` 존재 검증(없으면 누락 파일명 명시·중단) → ③ bind mount/volume 스캔, worktree 밖 경로는 **경고 후 진행**(스펙 §6) → ④ env.override 합성(입력 `.env` 는 worktree 기준, Task 4) → ⑤ `build_compose_up_argv` 로 compose up → ⑥ healthcheck 폴링(`docker compose ps --format json`, `healthcheck_timeout_seconds`/`interval`, healthcheck 미정의 서비스는 `running` 종료조건, timeout 시 미기동 서비스 명시 중단) → ⑦ `tmux_available()` 면 detached 세션 + 서비스별 tail/watcher pane 스폰(부재 시 skip + "감시 비활성" 명시) → ⑧ registry 기록 + deploy-state.json.
556
+
557
+ - [ ] **Step 4: `main` 디스패치 + status/down 구현** — argparse 5 sub-command. `status`/`down`/`stop-watcher` 는 **docker 라벨 쿼리가 컨테이너 SSOT**. `down`/`down all` 은 라벨 쿼리와 **별개로** `@okstra_container_run` 태그/세션을 tmux 실측 스캔해 orphan watcher 회수(리뷰 major: registry drift). `plan_run_root`/`done_rows` 역추적 resolver 는 task-key 로 implementation-planning run-root 를 찾되 다중 run 시 최신 done 보유 run 선택, 모호하면 중단(스펙 §11).
558
+
559
+ - [ ] **Step 5: 통과 확인**
560
+
561
+ Run: `python3 -m pytest tests/container/ -v`
562
+ Expected: PASS (전체)
563
+
564
+ - [ ] **Step 6: 커밋**
565
+
566
+ ```bash
567
+ git add scripts/okstra_ctl/container.py scripts/okstra_ctl/__init__.py tests/container/test_container_up_gate.py
568
+ git commit -m "feat(container): provision_container_group 수렴 entrypoint + sub-command 디스패치"
569
+ ```
570
+
571
+ ---
572
+
573
+ ## Task 8: Node thin shim + CLI 레지스트리
574
+
575
+ **배경(리뷰 major):** shim 은 `src/commands/inspect/container.mjs` (introspection 계열 디렉터리), `cli-registry.mjs` 의 module 도 `./commands/inspect/container.mjs`. `recap.mjs` 가 실제로 `src/commands/inspect/recap.mjs` 에 있다.
576
+
577
+ **Files:**
578
+ - Create: `src/commands/inspect/container.mjs` (참고: `src/commands/inspect/recap.mjs` thin shim)
579
+ - Modify: `src/cli-registry.mjs` (참고: recap 등록 행)
580
+ - Modify: `tests-js/cli-registry.test.mjs` (카운트)
581
+ - Test: `node --test tests-js/cli-registry.test.mjs`
582
+
583
+ **Interfaces:**
584
+ - Produces: `run(args) -> Promise<number>` — `runPythonModule({module:'okstra_ctl.container', args, ...})` 위임.
585
+
586
+ - [ ] **Step 1: 카운트 갱신 + 등록** — `cli-registry.mjs` `COMMAND_REGISTRY` 에 `{name:'container', module:'./commands/inspect/container.mjs', export:'run', category:'introspection', summary:[...]}`.
587
+
588
+ - [ ] **Step 2: shim 작성** — `src/commands/inspect/container.mjs`, `recap.mjs` 패턴 그대로(`runPythonModule` 한 통로). import 경로 `../../lib/python-helper.mjs`.
589
+
590
+ - [ ] **Step 3: 테스트 카운트 정정(리뷰 blocker/major)** — `tests-js/cli-registry.test.mjs` 의 하드코딩 카운트를 **실제 라인 전수 갱신**: 총 `33→34` (테스트명 문자열, `registryNames.length`, `COMMANDS.size`, `seen.size` 등 33 이 나오는 **모든** 라인), introspection `21→22` (실제 라인 좌표 확인 후). 구현 시 `rg -n '\b33\b|\b21\b' tests-js/cli-registry.test.mjs` 로 전수 확인.
591
+
592
+ - [ ] **Step 4: 통과 확인**
593
+
594
+ Run: `node --test tests-js/cli-registry.test.mjs`
595
+ Expected: PASS (모든 카운트 단언 통과)
596
+
597
+ - [ ] **Step 5: smoke**
598
+
599
+ Run: `npm run build && node bin/okstra container --help`
600
+ Expected: container sub-command usage 출력, exit 0
601
+
602
+ - [ ] **Step 6: 커밋**
603
+
604
+ ```bash
605
+ git add src/commands/inspect/container.mjs src/cli-registry.mjs tests-js/cli-registry.test.mjs
606
+ git commit -m "feat(container): bin okstra container thin shim + CLI 등록"
607
+ ```
608
+
609
+ ---
610
+
611
+ ## Task 9: SKILL.md + 스킬/권한 등록
612
+
613
+ **Files:**
614
+ - Create: `skills/okstra-container/SKILL.md` (참고: `skills/okstra-inspect/SKILL.md` 디스패치 골격)
615
+ - Modify: `src/lib/skill-catalog.mjs` (`USER_SKILL_NAMES`)
616
+ - Modify: `.claude-plugin/plugin.json` (skills 배열, 동일 인덱스/순서)
617
+ - Modify: `templates/reports/settings.template.json` (docker 권한)
618
+ - Test: `node --test tests-js/skill-catalog.test.mjs`
619
+
620
+ - [ ] **Step 1: SKILL.md 작성** — frontmatter `description` 에 트리거 한 줄(자연어 + 명시 명령: "okstra container", "컨테이너 띄워", "유저 테스트 환경"). 본문 상단 표(`up`/`status`/`logs`/`stop-watcher`/`down` = 메뉴 SSOT). Step 0 부트스트랩: `okstra ensure-installed --runtime claude-code` + `okstra check-project --json` 두 줄을 각각 단독 Bash 호출(리터럴 `okstra` 시작, `$()`/`VAR=`/`&&`/`npx` fallback 금지). Step 1 dispatch: 트리거 표 분류, 모호 시 numbered 메뉴 + 마지막 "직접 입력"(3-옵션 picker, MEMORY: 추천 + 직접입력). 실행/Read 경로는 host 기준(`~/.okstra/...`), 코드 출처 포인터만 레포 경로.
621
+
622
+ - [ ] **Step 2: 스킬 카탈로그 등록** — `USER_SKILL_NAMES` 에 `'okstra-container'` 추가(SSOT).
623
+
624
+ - [ ] **Step 3: plugin.json 등록** — skills 배열에 `./skills/okstra-container` 를 `USER_SKILL_NAMES` 와 **동일 인덱스/순서**로 추가(`skill-catalog.test.mjs` deepEqual 순서 가드).
625
+
626
+ - [ ] **Step 4: docker 권한 시드** — `templates/reports/settings.template.json` `permissions.allow` 의 `Bash(claude:*)` 직후에 `Bash(docker)`, `Bash(docker:*)` 추가(직전 항목 트레일링 콤마). **`Bash(docker compose:*)` 는 `Bash(docker:*)` prefix-match 로 커버되는지 확인 후 중복이면 제외**(리뷰 minor, allowlist 중복 제거).
627
+
628
+ - [ ] **Step 5: 통과 확인**
629
+
630
+ Run: `node --test tests-js/skill-catalog.test.mjs tests-js/install-runtime.test.mjs`
631
+ Expected: PASS (deepEqual 순서 가드 통과)
632
+
633
+ - [ ] **Step 6: 커밋**
634
+
635
+ ```bash
636
+ git add skills/okstra-container/SKILL.md src/lib/skill-catalog.mjs .claude-plugin/plugin.json templates/reports/settings.template.json
637
+ git commit -m "feat(container): okstra-container SKILL + 스킬/docker 권한 등록"
638
+ ```
639
+
640
+ ---
641
+
642
+ ## Task 10: doctor docker WARN + e2e 시나리오
643
+
644
+ **배경(리뷰 blocker):** `doctor.mjs` 의 `allOk = results.every(r => r.ok)` 때문에 `results` 에 넣는 항목은 무엇이든 `allOk` 를 뒤집어 `[FAIL]` 로 렌더된다. docker 점검을 `results` 에 넣으면 docker 미설치 사용자가 `exit 1` 회귀 → `runtimeResolution` 처럼 **별도 변수 + `[WARN]`** 으로 출력하고 `allOk` 에서 제외.
645
+
646
+ **Files:**
647
+ - Modify: `src/commands/lifecycle/doctor.mjs` (참고: `runtimeResolution` WARN 출력 ≈200-203, `allOk` ≈245, JSON 모드 ≈246-261)
648
+ - Create: `tests-e2e/scenario-15-okstra-container-up-down.sh` (참고: `scenario-01`, `scenario-14`)
649
+ - Modify: `tests-e2e/README.md` (Index of scenarios)
650
+ - Test: `node --test tests-js/doctor-runtime.test.mjs`
651
+
652
+ - [ ] **Step 1: doctor docker WARN 추가** — `results` 배열에 넣지 말 것. `runProcess('docker', ['compose', 'version'])` 결과를 별도 변수(`dockerWarning`)로 두고 `[WARN]` 한 줄만 출력, `allOk` 비반영. JSON 모드엔 별도 키(`dockerWarning`)로 노출. `REQUIRED_SKILLS` 불변(okstra-container 강제 점검 금지).
653
+
654
+ - [ ] **Step 2: doctor 회귀 테스트 통과**
655
+
656
+ Run: `node --test tests-js/doctor-runtime.test.mjs`
657
+ Expected: PASS (REQUIRED_SKILLS 불변, docker 미설치 시 exit 0 유지)
658
+
659
+ - [ ] **Step 3: e2e 시나리오 작성** — `tests-e2e/scenario-15-okstra-container-up-down.sh`. `set -euo pipefail`, `mktemp -d` 로 `OKSTRA_HOME` + `PROJECT_ROOT`(`docker-compose.yml`/`.env` 시드), sandbox `bin/` 에 docker stub(argv 기록) + PATH prepend. `up <task-id>` 후 stub argv 에 okstra 라벨·두 `--env-file`·`-p <project-name>` grep, `down` 후 정리 확인, `trap cleanup EXIT`, 마지막 `echo PASS`. `OKSTRA_CTL_SKIP_*` 설정 금지(e2e 규약), `--project-root` 직접 전달.
660
+
661
+ - [ ] **Step 4: e2e 수동 PASS**
662
+
663
+ Run: `bash tests-e2e/scenario-15-okstra-container-up-down.sh`
664
+ Expected: 마지막 `PASS` 출력, exit 0
665
+
666
+ - [ ] **Step 5: README 인덱스 갱신** — `tests-e2e/README.md` 표에 scenario-15 행 추가.
667
+
668
+ - [ ] **Step 6: 커밋**
669
+
670
+ ```bash
671
+ git add src/commands/lifecycle/doctor.mjs tests-e2e/scenario-15-okstra-container-up-down.sh tests-e2e/README.md
672
+ git commit -m "feat(container): doctor docker WARN degrade + e2e 시나리오"
673
+ ```
674
+
675
+ ---
676
+
677
+ ## Task 11: 빌드 동기화 + 전체 CI 게이트
678
+
679
+ **Files:** (없음 — 검증 전용)
680
+
681
+ - [ ] **Step 1: runtime 미러 재생성**
682
+
683
+ Run: `npm run build`
684
+ Expected: `runtime/python/okstra_ctl/container.py` 등 신규 파일이 미러에 반영(exit 0)
685
+
686
+ - [ ] **Step 2: 전체 CI**
687
+
688
+ Run: `npm run check`
689
+ Expected: PASS (test:js → test:py 순서로 모두 exit 0)
690
+
691
+ - [ ] **Step 3: 설치 경로 smoke**
692
+
693
+ Run: `node bin/okstra doctor`
694
+ Expected: exit 0 (docker 미설치 시 `[WARN]` 한 줄, FAIL 아님)
695
+
696
+ - [ ] **Step 4: phase 계약 검증 회귀**
697
+
698
+ Run: `bash validators/validate-workflow.sh`
699
+ Expected: PASS (PHASE_SEQUENCE 미변경 확인)
700
+
701
+ - [ ] **Step 5: 최종 커밋(빌드 산출물 제외 — runtime/ 은 gitignore)**
702
+
703
+ ```bash
704
+ git status # runtime/ 변경이 staged 되지 않음을 확인(.gitignore)
705
+ git log --oneline -11 # T1~T10 + 검증 커밋 확인
706
+ ```
707
+
708
+ ---
709
+
710
+ ## Self-Review (작성자 체크리스트 결과)
711
+
712
+ - **Spec coverage:** §1~§9 전부 task 매핑됨(B/비선형=T7·T9, sub-command=T7·T8·T9, up 플로우=T7, 지속성/태그=T5, env/artifact-home=T4·T7, 네이밍/SSOT=T1·T7, 단일참조점/등록=T2·T7·T8·T9, YAGNI=Global Constraints). §10 확정사항 전부 반영(watcher=T6, healthcheck=T7, Dockerfile중단=T7, tmux degrade=T7, bind mount경고=T7, stage보존=T2, doctor WARN=T10). §11 잔여항목(plan-run-root resolver=T7 Step4, orphan reconcile=T7 Step4, truncation 형식=T1)도 task 내 명시.
713
+ - **Placeholder scan:** 모든 코드 step 에 실제 테스트/구현 코드 블록 포함. 기존 파일 내부는 anchor(file:line)로 참조하고 신규 코드는 전문 기재.
714
+ - **Type consistency:** `container_paths`/`compose_project_name`(T1) → T3·T4·T7 소비, `resolve_and_integrate_whole_task`(T2) → T7, `build_compose_up_argv`/`merge_env`(T4) → T7, `split_container_pane`/`new_detached_session`(T5) → T7, `watcher_step`/`run_watcher_loop`(T6) → T7. 시그니처 일치 확인.