okstra 0.103.0 → 0.105.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.
@@ -5,6 +5,9 @@ Use this matrix before changing high-risk repo contracts. Update the source file
5
5
  | Change | Must update | Tests |
6
6
  |---|---|---|
7
7
  | Add CLI flag | `src/`, `scripts/okstra_ctl/run.py`, `docs/kr/cli.md`, `prompts/wizard/` | JS CLI tests and pytest CLI contracts |
8
+ | Add Node subcommand | `src/cli-registry.mjs`, `src/commands/`, `docs/kr/cli.md`, `docs/project-structure-overview.md` | `tests-js/cli-registry.test.mjs` plus command-specific JS/Python tests |
9
+ | Add public skill | `src/lib/skill-catalog.mjs`, `.claude-plugin/plugin.json`, `skills/<name>/SKILL.md`, `docs/for-ai/README.md`, `docs/project-structure-overview.md`, `README*` | `tests-js/skill-catalog.test.mjs`, `tests/contract/test_docs_runtime_contract.py` |
10
+ | Change manager contract | `scripts/okstra_ctl/manager_*.py`, `src/commands/manager.mjs`, `skills/okstra-manager/SKILL.md`, `docs/for-ai/skills/okstra-manager.md`, `docs/kr/cli.md`, `docs/kr/architecture/storage-model.md` | `tests-js/manager.test.mjs`, `tests/test_okstra_manager_*.py` |
8
11
  | Add phase | `scripts/okstra_ctl/workflow.py`, `prompts/profiles/`, `validators/`, `tests/` | workflow and validation contract tests |
9
12
  | Change worker roster | `prompts/profiles/*.md`, `scripts/okstra_ctl/workers.py`, `tests/contract/test_repo_contracts.py` | worker roster contract tests |
10
13
  | Change report section | `schemas/final-report-v1.0.schema.json`, `templates/reports/final-report.template.md`, `scripts/okstra_ctl/render_final_report.py`, `validators/validate-run.py` | final-report schema, renderer, and validator tests |
@@ -19,6 +19,7 @@
19
19
  | 새 프로젝트나 새 머신에서 okstra 설치/초기화 | `okstra-setup` | [`skills/okstra-setup.md`](skills/okstra-setup.md) |
20
20
  | 요구사항, 티켓, 링크, 코드베이스 스캔, error-zip을 okstra 입력 brief로 변환 | `okstra-brief` | [`skills/okstra-brief.md`](skills/okstra-brief.md) |
21
21
  | 현재 Claude Code 세션에서 okstra run 시작 또는 다음 phase 실행 | `okstra-run` | [`skills/okstra-run.md`](skills/okstra-run.md) |
22
+ | 여러 프로젝트에 걸친 okstra task 묶음, 할당, sync snapshot, child launch packet 관리 | `okstra-manager` | [`skills/okstra-manager.md`](skills/okstra-manager.md) |
22
23
  | 상태, history, report, time, logs, cost, errors, error-zip, recap 확인 | `okstra-inspect` | [`skills/okstra-inspect.md`](skills/okstra-inspect.md) |
23
24
  | task-group 전체의 클라이언트용 작업 일정 생성 | `okstra-schedule` | [`skills/okstra-schedule.md`](skills/okstra-schedule.md) |
24
25
  | 대화/결정/선호/요구사항을 전역 Memory Book에 저장 또는 검색 | `okstra-memory` | [`skills/okstra-memory.md`](skills/okstra-memory.md) |
@@ -43,11 +44,12 @@
43
44
 
44
45
  ## 공개 스킬 목록
45
46
 
46
- `src/lib/skill-catalog.mjs` 기준 공개 스킬은 다음 7개다.
47
+ `src/lib/skill-catalog.mjs` 기준 공개 스킬은 다음 8개다.
47
48
 
48
49
  - `okstra-setup`
49
50
  - `okstra-brief`
50
51
  - `okstra-run`
52
+ - `okstra-manager`
51
53
  - `okstra-memory`
52
54
  - `okstra-inspect`
53
55
  - `okstra-schedule`
@@ -0,0 +1,62 @@
1
+ # okstra-manager
2
+
3
+ 여러 프로젝트에 걸친 okstra task 를 하나의 manager-owned context 로 묶을 때 사용한다. 원문 계약은 [`skills/okstra-manager/SKILL.md`](../../../skills/okstra-manager/SKILL.md), CLI 구현은 [`src/commands/manager.mjs`](../../../src/commands/manager.mjs) 와 [`scripts/okstra_ctl/manager_cli.py`](../../../scripts/okstra_ctl/manager_cli.py) 이다.
4
+
5
+ ## 언제 쓰나
6
+
7
+ - 사용자가 "여러 프로젝트", "cross-project", "okstra manager", "프로젝트 묶어서" 같은 의도를 말한다.
8
+ - manager 아래에 프로젝트를 등록하거나 후보 프로젝트를 찾는다.
9
+ - 공통 task-group/task-id 아래 child project task 를 계획하고 역할/지시를 배정한다.
10
+ - child project `.okstra` 상태를 manager snapshot 으로 동기화하거나 status 를 본다.
11
+ - 특정 child task 실행을 위한 launch packet 과 manager child context 를 준비한다.
12
+
13
+ ## 실행 규칙
14
+
15
+ 1. 모든 명령은 literal `okstra` 로 시작한다. 셸 변수, `$(...)`, `&&`, `eval`, 선행 env assignment 로 감싸지 않는다.
16
+ 2. JSON stdout 이 source of truth 다. manager 상태나 child launch args 를 문서/메모리에서 재구성하지 않는다.
17
+ 3. `--workspace-root` 는 Node wrapper 가 소유한다. 사용자가 넘기면 CLI 가 거부한다.
18
+ 4. `new project` 의 `--project-root` 는 이미 존재하는 디렉터리여야 한다. 그 안의 `.okstra/project.json` 이 없을 때만 setup-equivalent registration 을 수행한다.
19
+ 5. public child task identity 는 `project-id:task-group:task-id` 이다. `new task --task` 예시는 full key form 을 우선 보여준다.
20
+ 6. 같은 manager task id 아래 실제 child task id 가 다르면 `task assign` 과 `task run` 에 `--child-task-id <id>` 를 같이 넘긴다.
21
+
22
+ ## 명령 표면
23
+
24
+ ```bash
25
+ okstra manager init --manager-id <manager-id> --json
26
+ okstra manager discover-projects --json
27
+ okstra manager new project --manager-id <manager-id> --project-id <project-id> --project-root <abs-path> [--role <role>] [--tag <tag>] --json
28
+ okstra manager new task-group --manager-id <manager-id> --task-group <task-group> --json
29
+ okstra manager new task --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --task <project-id:task-group:task-id> --json
30
+ okstra manager task assign --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --project-id <project-id> [--child-task-id <child-task-id>] [--role <role>] [--tag <tag>] [--assignment <text>] --json
31
+ okstra manager task note --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --scope <shared|project> [--project-id <project-id>] --body <text> --json
32
+ okstra manager task sync --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --json
33
+ okstra manager task status --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --json
34
+ okstra manager task run --manager-id <manager-id> --project-id <project-id> --task-group <task-group> --task-id <task-id> [--child-task-id <child-task-id>] --json
35
+ ```
36
+
37
+ ## 저장 모델
38
+
39
+ Manager state 는 `~/.okstra/managers/<manager-id>/` 아래에 저장된다.
40
+
41
+ - `manager.json`: manager id / schema / createdAt
42
+ - `projects.json`: 등록된 projectId, projectRoot, role, tags
43
+ - `task-groups/<safe-group>/<safe-task>/manifest.json`: manager task objective, common brief, progress mode
44
+ - `children.json`: child task plan, assignment, launch metadata
45
+ - `directives.jsonl`: shared/project directive rows
46
+ - `snapshots.json`: `task sync` 가 project-local `.okstra` 에서 읽어온 read-side snapshot
47
+ - `events.jsonl`: `task-created`, `child-launch-prepared` 같은 manager event
48
+ - `child-context/<safe-project>-<safe-task>.md`: `task run` 이 만든 child lead context
49
+
50
+ 비 ASCII task-group/task-id 처럼 slug 가 비는 segment 는 `u-<sha1-prefix>` path segment 를 사용하지만, manifest 와 child `taskKey` 는 원래 입력값을 보존한다.
51
+
52
+ ## Child launch
53
+
54
+ `task run` 은 child 작업을 직접 실행하지 않고 launch packet 을 준비한다. 반환 packet 의 핵심 필드는:
55
+
56
+ - `backend`: `$TMUX` 가 있으면 `tmux-child-lead`, 없으면 `subagent-child-lead`
57
+ - `workerDispatchBackend`: v1 에서는 항상 `subagent`
58
+ - `projectRoot`: child project root
59
+ - `contextPath`: manager child context markdown
60
+ - `runArgs`: host launcher 가 사용할 `okstra run ... --directive "Read manager child context: ..."` 인자
61
+
62
+ packet 생성이 성공하면 `children.json` 의 해당 child launch 상태가 `prepared` 로 갱신되고, `events.jsonl` 에 `child-launch-prepared` 가 append 된다. 실패해도 project-local task state 는 수정하지 않는다.
@@ -93,20 +93,13 @@ worker prompt history는 `/tmp`가 아니라 항상 현재 run의 `prompts/` 아
93
93
  같은 task-type으로 다시 실행하면 동일한 `runs/<task-type>/` 폴더를 재사용하지만, 유형별 하위 폴더 아래에서 run-level 파일명이 `-<task-type>-<seq>` suffix로 분리되므로 기존 산출물을 덮어쓰지 않습니다. `<seq>`는 카테고리 디렉토리(`manifests/`, `prompts/`, `reports/`, `status/`, `state/`, `sessions/`, `worker-results/`)별로 독립 스캔되므로 같은 run에서도 카테고리별 값이 다를 수 있습니다.
94
94
  이전 flat legacy artifact가 task-type run 폴더 최상위에 남아 있으면 다음 실행 시 해당 유형별 하위 폴더로 자동 정리합니다.
95
95
 
96
- ### 3. Project-level discovery and skill documents
96
+ ### 3. Project-level discovery and installed skill assets
97
97
  프로젝트 공용 discovery pointer는 아래 경로에 생성합니다.
98
98
 
99
99
  ```text
100
100
  <target-project>/.okstra/discovery/latest-task.json
101
101
  <target-project>/.okstra/discovery/task-catalog.json
102
102
  ```
103
- scripts/okstra.sh workflow가 사용하는 project-local Claude assets는 아래 경로들에 seed합니다.
104
-
105
- ```text
106
- <target-project>/.claude/skills/okstra/SKILL.md
107
- <target-project>/.claude/skills/<sub-skill>/SKILL.md
108
- <target-project>/.claude/agents/<agent>.md
109
- ```
110
103
 
111
104
  역할 구분:
112
105
 
@@ -123,6 +116,34 @@ scripts/okstra.sh workflow가 사용하는 project-local Claude assets는 아래
123
116
  - `.project-docs/ai/okstra/okstra-guide.md`
124
117
  - `.project-docs/ai/okstra/worker-catalog.md`
125
118
 
119
+ ### 4. Manager-owned cross-project state
120
+
121
+ `okstra manager` 는 project-local `.okstra` 정본을 대체하지 않고, 여러 project-local task 를 묶는 전역 manager state 를 사용자 홈에 저장합니다.
122
+
123
+ ```text
124
+ ~/.okstra/managers/<manager-id>/
125
+ ├── manager.json
126
+ ├── projects.json
127
+ └── task-groups/<safe-task-group>/<safe-task-id>/
128
+ ├── manifest.json
129
+ ├── children.json
130
+ ├── directives.jsonl
131
+ ├── snapshots.json
132
+ ├── events.jsonl
133
+ └── child-context/<safe-project-id>-<safe-child-task-id>.md
134
+ ```
135
+
136
+ 저장 권위:
137
+
138
+ - `manager.json`: manager identity 와 schema version.
139
+ - `projects.json`: manager 에 등록된 projectId / projectRoot / role / tags. `projectRoot` 는 이미 존재하는 디렉터리여야 하며, project-local `.okstra/project.json` 이 없을 때만 setup-equivalent registration 을 수행합니다.
140
+ - `manifest.json`, `children.json`, `directives.jsonl`: manager-owned plan, child assignment, shared/project directive.
141
+ - `snapshots.json`: `task sync` 가 child project `.okstra` 에서 읽어온 read-side snapshot. project state 의 source of truth 가 아닙니다.
142
+ - `events.jsonl`: `task-created`, `child-launch-prepared` 같은 manager event.
143
+ - `child-context/*.md`: `task run` 이 준비한 child lead context. sibling project report/snapshot 은 read-side source material 로만 전달합니다.
144
+
145
+ Path segment 는 slug 로 정규화합니다. 비 ASCII 값처럼 slug 가 비는 경우에는 `u-<sha1-prefix>` fallback segment 를 쓰지만, `manifest.json` 과 child `taskKey` 는 원래 입력값(`project-id:task-group:task-id`)을 보존합니다.
146
+
126
147
  ## Task manifest contract
127
148
 
128
149
  `task-manifest.json`은 Claude가 task continuity를 이해할 때 기준이 되는 canonical metadata 파일입니다.
@@ -77,7 +77,7 @@
77
77
 
78
78
  okstra 의 prepare 책임은 단일 python 진입점 [`okstra_ctl.run.prepare_task_bundle`](../../scripts/okstra_ctl/run.py) 에 모여 있습니다. 이 함수가 다음을 한 트랜잭션으로 수행합니다.
79
79
 
80
- - okstra 설치 자산(`~/.claude/skills/okstra/...`, `~/.claude/agents/...`, `~/.okstra/bin/...`) 존재 확인
80
+ - okstra 설치 자산(`~/.agents/skills/okstra-*`, optional `~/.claude/skills/okstra-*` / `~/.claude/agents/*-worker.md`, `~/.okstra/bin/...`) 존재 확인
81
81
  - `<PROJECT_ROOT>/.okstra/project.json` self-registration (또는 projectId 일치 검증)
82
82
  - task type → `prompts/profiles/<task-type>.md` 로드 + 권장 workers 추출
83
83
  - 사용자 worker/model 오버라이드 정규화 (Claude/Codex/Antigravity/Report-writer 각각 display ↔ execution-value 매핑)
@@ -122,6 +122,7 @@ okstra 의 prepare 책임은 단일 python 진입점 [`okstra_ctl.run.prepare_ta
122
122
  - [`okstra_ctl.session`](../../scripts/okstra_ctl/session.py) · [`okstra_ctl.seeding`](../../scripts/okstra_ctl/seeding.py) — Claude session id / resume command / 설치 검증 / runtime settings.
123
123
  - [`okstra_ctl.{ids,index,invocation,jsonl,project_meta,reconcile,resolver,sequence,batch,backfill,listing,locks,tmux}`](../../scripts/okstra_ctl/) — 중앙 인덱스 (`~/.okstra`) 의 기존 모듈군.
124
124
  - [`okstra_project.{resolver,state}`](../../scripts/okstra_project/) — PROJECT_ROOT 해석 + project.json upsert + task-catalog/manifest reader.
125
+ - [`okstra_ctl.manager_cli`](../../scripts/okstra_ctl/manager_cli.py), [`manager_store`](../../scripts/okstra_ctl/manager_store.py), [`manager_sync`](../../scripts/okstra_ctl/manager_sync.py), [`manager_launch`](../../scripts/okstra_ctl/manager_launch.py), [`manager_paths`](../../scripts/okstra_ctl/manager_paths.py) — `okstra manager` 의 cross-project manager state, one-way project snapshot sync, child launch packet/context 생성.
125
126
 
126
127
  ### Bash entry points (thin)
127
128
 
@@ -146,9 +147,9 @@ okstra 의 prepare 책임은 단일 python 진입점 [`okstra_ctl.run.prepare_ta
146
147
  - [`prompts/lead/okstra-lead-contract.md`](../../prompts/lead/okstra-lead-contract.md) — main okstra lead contract. 런타임 리소스(`~/.okstra/prompts/lead/`)이며 agent skill 이 아닙니다.
147
148
  - [`skills/okstra-setup/SKILL.md`](../../skills/okstra-setup/SKILL.md) — **첫 실행 부트스트랩**. `okstra install` + `project.json` 생성.
148
149
  - [`skills/okstra-run/SKILL.md`](../../skills/okstra-run/SKILL.md) — **현재 claude 세션 안에서 okstra task 를 시작**하는 in-session 진입점. `prepare_task_bundle` 직접 호출.
149
- - 사용자 호출 가능 스킬은 `skills/okstra-setup/SKILL.md`, `skills/okstra-brief/SKILL.md`, `skills/okstra-run/SKILL.md`, `skills/okstra-memory/SKILL.md`, `skills/okstra-inspect/SKILL.md`, `skills/okstra-schedule/SKILL.md`, `skills/okstra-container-build/SKILL.md` 7종뿐이며, 이것만 agent skill home 으로 복사됩니다 — brief 작성, phase 진행, 전역 Memory Book 저장/검색, status/history/report/time/logs/cost/errors/recap read-side, schedule 보조, 그리고 `okstra-container-build` 는 검증 완료된 task 코드를 docker compose 그룹으로 배포하고 컨테이너별 watcher 로 감시하는 비선형(PHASE_SEQUENCE 외부) 컨테이너 배포/감시 스킬. `okstra-inspect` 의 read-side facet 은 `skills/okstra-inspect/SKILL.md` 의 sub-command 표가 정본인 8종입니다. `okstra-inspect logs` 는 codex/antigravity wrapper 가 매 dispatch 마다 `runs/<task-type>/prompts/<worker>-prompt-<phase>-<seq>.log` 로 남기는 live-log sidecar 의 인벤토리·정리 안내(read-only), `okstra-inspect cost` 는 `okstra context-cost` 결과 요약, `okstra-inspect errors` 는 task 의 okstra-run 에러 로그를 타임스탬프 markdown error-report 로 모아 렌더하고 요약을 출력, `okstra-inspect recap` 은 task 의 run 간 phase 전·후 요약에 더해 `.okstra` 산출물에 대한 자유 Q&A 까지 답합니다.
150
+ - 사용자 호출 가능 스킬은 `skills/okstra-setup/SKILL.md`, `skills/okstra-brief/SKILL.md`, `skills/okstra-run/SKILL.md`, `skills/okstra-manager/SKILL.md`, `skills/okstra-memory/SKILL.md`, `skills/okstra-inspect/SKILL.md`, `skills/okstra-schedule/SKILL.md`, `skills/okstra-container-build/SKILL.md` 8종뿐이며, 이것만 agent skill home 으로 복사됩니다 — brief 작성, phase 진행, cross-project manager task 조정, 전역 Memory Book 저장/검색, status/history/report/time/logs/cost/errors/recap read-side, schedule 보조, 그리고 `okstra-container-build` 는 검증 완료된 task 코드를 docker compose 그룹으로 배포하고 컨테이너별 watcher 로 감시하는 비선형(PHASE_SEQUENCE 외부) 컨테이너 배포/감시 스킬. `okstra-manager` 는 `okstra manager` CLI JSON/launch packet 을 source of truth 로 사용하며, manager-owned plan/assignment/directive/snapshot/event 는 `~/.okstra/managers/<manager-id>/` 아래에 저장합니다. `okstra-inspect` 의 read-side facet 은 `skills/okstra-inspect/SKILL.md` 의 sub-command 표가 정본인 8종입니다. `okstra-inspect logs` 는 codex/antigravity wrapper 가 매 dispatch 마다 `runs/<task-type>/prompts/<worker>-prompt-<phase>-<seq>.log` 로 남기는 live-log sidecar 의 인벤토리·정리 안내(read-only), `okstra-inspect cost` 는 `okstra context-cost` 결과 요약, `okstra-inspect errors` 는 task 의 okstra-run 에러 로그를 타임스탬프 markdown error-report 로 모아 렌더하고 요약을 출력, `okstra-inspect recap` 은 task 의 run 간 phase 전·후 요약에 더해 `.okstra` 산출물에 대한 자유 Q&A 까지 답합니다.
150
151
  - 내부 운영 계약 — `context-loader` / `team-contract` / `convergence` / `report-writer` 와 lead 계약 — 은 `prompts/lead/*.md` 로, 구현/검증 워커의 언어별 coding preflight 는 `prompts/coding-preflight/*` (overview 라우터 + clean-code + languages/frameworks/architectures 3단계 선택) 로 이동했습니다. 모두 `~/.okstra/prompts/` 에 설치되는 런타임 리소스이며 skill discovery 대상이 아닙니다. generated launch prompt 가 lead 에게 절대 경로를 제공하고, 재설치 시 과거 `okstra-context-loader` / `okstra-team-contract` / `okstra-convergence` / `okstra-report-writer` / `okstra-coding-preflight` / `okstra` skill 디렉터리는 exact-name prune 됩니다.
151
- - 플러그인 매니페스트: [`../../.claude-plugin/plugin.json`](../../.claude-plugin/plugin.json) — `npx skills@latest add Devonshin/okstra` 보조 채널이 참조. 일반 셋업에는 `npx okstra@latest install` 을 사용한다. 플러그인 매니페스트는 사용자 진입점 7개(`okstra-setup`, `okstra-brief`, `okstra-run`, `okstra-memory`, `okstra-inspect`, `okstra-schedule`, `okstra-container-build`)만 노출한다.
152
+ - 플러그인 매니페스트: [`../../.claude-plugin/plugin.json`](../../.claude-plugin/plugin.json) — `npx skills@latest add Devonshin/okstra` 보조 채널이 참조. 일반 셋업에는 `npx okstra@latest install` 을 사용한다. 플러그인 매니페스트는 사용자 진입점 8개(`okstra-setup`, `okstra-brief`, `okstra-run`, `okstra-manager`, `okstra-memory`, `okstra-inspect`, `okstra-schedule`, `okstra-container-build`)만 노출한다.
152
153
  - 설치 위치: `~/.claude/skills/<name>/SKILL.md` 또는 `~/.agents/skills/<name>/SKILL.md`.
153
154
  - 릴리스 절차: [`../../RELEASING.md`](../../RELEASING.md) — npm publish 흐름과 release-please / manual fallback.
154
155
 
@@ -175,7 +176,7 @@ okstra 의 prepare 단계는 디스크 권위 + 단일 python 진입점 모델
175
176
  │ ┌────────────┴────────────────┐ │
176
177
  │ ▼ ▼ │
177
178
  │ on-disk authority ~/.okstra (central index) │
178
- │ <PROJECT_ROOT>/.project-docs/ per-task mutex + record_start │
179
+ │ <PROJECT_ROOT>/.okstra/ per-task mutex + record_start │
179
180
  └────────────────────────────────────────────────────────────────┘
180
181
  ```
181
182
 
package/docs/kr/cli.md CHANGED
@@ -620,7 +620,7 @@ chmod +x ~/.local/bin/okstra-ctl
620
620
 
621
621
  ### `okstra` Node CLI — admin / introspection subcommands
622
622
 
623
- `okstra` Node CLI (`bin/okstra`) 는 installer/admin 명령과 skill / agent 가 사용하는 introspection 명령을 함께 제공합니다. Python 런타임을 직접 호출하지 않고 Node wrapper를 통하므로 `PYTHONPATH` wiring은 `src/_python-helper.mjs`가 처리합니다.
623
+ `okstra` Node CLI (`bin/okstra`) 는 installer/admin 명령과 skill / agent 가 사용하는 introspection 명령을 함께 제공합니다. Python 런타임을 직접 호출하지 않고 Node wrapper를 통하므로 `PYTHONPATH` wiring은 `src/lib/python-helper.mjs`가 처리합니다.
624
624
 
625
625
  | 명령 | 용도 |
626
626
  |---|---|
@@ -633,7 +633,7 @@ chmod +x ~/.local/bin/okstra-ctl
633
633
  | `okstra check-project [--json]` | 현재 프로젝트가 등록되었는지 검증 |
634
634
  | `okstra config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | 영구 설정 관리 (예: `pr-template-path`). 원자적 JSON 쓰기 |
635
635
  | `okstra memory <add\|list\|search\|show\|archive>` | `~/.okstra/memory-book` 전역 대화 메모리 관리. 프로젝트 `.okstra/` 와 분리된 user-home 저장소이며, `okstra 에 정리해서 보관해` 자연어 스킬의 CLI 기반 |
636
- | `okstra manager <init\|discover-projects\|new\|task>` | 여러 프로젝트의 okstra task 를 manager-owned context 로 묶는 공개 CLI. `new project`, `new task-group`, `new task` 로 manager 계획을 만들고, `task assign` / `task note` / `task sync` / `task status` / `task run` 으로 프로젝트별 할당과 snapshot 을 관리한다. child task key 는 공개 문서에서 `project-id:task-group:task-id` 전체 형식을 사용하며, `task run` 은 child launch context packet 을 JSON 으로 반환한다 |
636
+ | `okstra manager <init\|discover-projects\|new\|task>` | 여러 프로젝트의 okstra task 를 manager-owned context 로 묶는 공개 CLI. `new project`, `new task-group`, `new task` 로 manager 계획을 만들고, `task assign` / `task note` / `task sync` / `task status` / `task run` 으로 프로젝트별 할당과 snapshot 을 관리한다. `new project --project-root` 는 이미 존재하는 디렉터리만 받으며, 그 안의 `.okstra/project.json` 이 없을 때만 setup-equivalent registration 을 수행한다. child task key 는 공개 문서에서 `project-id:task-group:task-id` 전체 형식을 사용하고, 같은 manager task 안에서 child task id 가 다르면 `--child-task-id` 로 exact child 를 지정한다. `task run` 은 child lead 를 직접 실행하지 않고 `prepared` launch metadata/event 와 child launch context packet 을 JSON 으로 반환한다 |
637
637
  | `okstra migrate [--apply] [--cwd <dir>] [--quiet]` | 한 번만 실행: 프로젝트 산출물 루트를 `.project-docs/okstra/` → `.okstra/` 로 이동. 기본 dry-run, `--apply` 로 실제 실행. `git mv` (git worktree) + 빈 `.project-docs/` 제거 + `<PROJECT>/CLAUDE.md` import 라인 + `.gitignore` + `~/.okstra/{recent,active}.jsonl` + `~/.okstra/worktrees/registry.json` 의 해당 프로젝트 row 동기화. 이미 `.okstra/` 가 존재하거나 레거시 디렉토리가 없으면 exit 1 로 거부. v0.x 말까지 유지 후 제거 예정 |
638
638
  | `okstra task-list [--project-root <path>]` | `list_project_tasks` + `read_latest_task` 결과를 합쳐 task 카탈로그 + 최근 task 를 JSON 으로 반환 |
639
639
  | `okstra task-show <task-key> [--project-root <path>]` | task-manifest.json 의 workflow / phase / status 요약 |
@@ -647,7 +647,7 @@ chmod +x ~/.local/bin/okstra-ctl
647
647
  | `okstra wizard <init\|step\|render-args\|confirmation> --state-file <path>` | okstra-run 인터랙티브 입력 상태머신 (`okstra_ctl.wizard`). `init` 으로 state file 을 시드한 뒤 skill 이 `step --answer <val>` 을 반복 호출하면 다음 `Prompt` JSON 을 받음. `--answer` 는 **필수**. 응답을 주지 않고 다음 prompt 만 미리 보고 싶다면 `--no-submit` 으로 peek. `render-args` 는 최종 `render-bundle` 인자 맵, `confirmation` 은 사용자 echo 블록을 반환. `implementation` task type 에서는 `approved_plan_pick` 직후 `stage_pick` 단계가 추가되어 실행할 stage 를 선택하고, `executor_pick` 으로 넘어갑니다. brief 단계는 entry task-type(requirements-discovery / error-analysis / improvement-discovery)에서만 나오며, downstream 은 manifest 의 brief 를 자동 carry-in 하고(미등록 시 `brief_carry` 3-옵션 fallback), `release-handoff` 는 brief 없이 `handoff_stage_pick` 멀티선택(eligible stage 묶음 / 전체 task)으로 진입합니다 |
648
648
  | `okstra token-usage ...` | 설치된 `okstra-token-usage.py` 를 감싸 run token usage 수집/치환을 수행. 세션 jsonl 은 기본적으로 `$OKSTRA_HOME/cache/token-usage/` 의 byte cursor 캐시로 증분 스캔하며, `--no-cache` 로 캐시를 우회해 전체 재스캔을 강제할 수 있음(정확성 폴백) |
649
649
 
650
- > 모든 subcommand 는 `bin/okstra` 가 spawn 하는 python 헬퍼 (`src/_python-helper.mjs`) 가 `PYTHONPATH` 와 `~/.okstra/lib/python` 을 wire 합니다. 직접 `python3 -m okstra_ctl.*` 으로 호출하면 `PYTHONPATH` 를 사용자가 직접 셋업해야 합니다.
650
+ > 모든 subcommand 는 `bin/okstra` 가 spawn 하는 python 헬퍼 (`src/lib/python-helper.mjs`) 가 `PYTHONPATH` 와 `~/.okstra/lib/python` 을 wire 합니다. 직접 `python3 -m okstra_ctl.*` 으로 호출하면 `PYTHONPATH` 를 사용자가 직접 셋업해야 합니다.
651
651
 
652
652
  ### Live-log sidecar
653
653
 
@@ -153,17 +153,29 @@ Runtime/install asset changes follow this checklist:
153
153
  | `setup` | `src/commands/lifecycle/setup.mjs` | Create/update `<PROJECT_ROOT>/.okstra/project.json` |
154
154
  | `check-project` | `src/commands/lifecycle/check-project.mjs` | Verify project registration |
155
155
  | `config` | `src/commands/lifecycle/config.mjs` | Read/write project/global settings such as PR template path |
156
+ | `migrate` | `src/commands/lifecycle/migrate.mjs` | One-shot legacy `.project-docs/okstra` → `.okstra` migration helper |
157
+ | `git-reconcile` | `src/commands/execute/git-reconcile.mjs` | Reconcile stale stage SHAs after external git history changes |
158
+ | `handoff` | `src/commands/execute/handoff.mjs` | Stage-group release-handoff eligibility / assemble / record helpers |
159
+ | `integrate-stages` | `src/commands/execute/integrate-stages.mjs` | Merge verified stages into the task worktree and clean stage worktrees |
156
160
  | `task-list`, `task-show` | `src/commands/inspect/task-list.mjs`, `src/commands/inspect/task-show.mjs` | Task/run introspection for skills |
161
+ | `resolve-task-key` | `src/commands/inspect/resolve-task-key.mjs` | Resolve a bare task-id to candidate task-keys from the project catalog |
162
+ | `time-report`, `log-report`, `error-report`, `error-zip` | `src/commands/inspect/*.mjs` | Read-side task runtime, wrapper log, and error aggregation helpers |
157
163
  | `context-cost` | `src/commands/inspect/context-cost.mjs` | Estimate task bundle file/read context cost |
158
164
  | `worktree-lookup` | `src/commands/execute/worktree-lookup.mjs` | Look up a task-key's registered worktree |
159
165
  | `plan-validate` | `src/commands/execute/plan-validate.mjs` | Check approved-plan approval marker |
160
166
  | `render-bundle` | `src/commands/execute/render-bundle.mjs` | Preview `prepare_task_bundle(render_only=True)` |
167
+ | `run` | `src/commands/execute/run.mjs` | Host-aware execution front door (`auto` → Claude/Codex/external path selection) |
168
+ | `codex-run`, `codex-dispatch` | `src/commands/execute/codex-*.mjs` | Codex lead dry-run bundle preparation and CLI-backed worker dispatch |
169
+ | `team` | `src/commands/execute/team.mjs` | External lead tmux-pane worker dispatch / await / teardown |
161
170
  | `render-views` | `src/commands/report/render-views.mjs` | Generate slim MD + self-contained HTML report views |
171
+ | `render-final-report`, `inject-report-index` | `src/commands/report/*.mjs` | Render final-report Markdown from data.json and inject top-of-report index anchors |
162
172
  | `wizard` | `src/commands/execute/wizard.mjs` | Drive the `okstra-run` interactive state machine |
163
173
  | `token-usage` | `src/commands/execute/token-usage.mjs` | Wrap installed Python token usage CLI |
174
+ | `spawn-followups`, `error-log` | `src/commands/execute/*.mjs` | Follow-up task bundle creation and run error-log append helpers |
164
175
  | `memory` | `src/commands/memory/memory.mjs` | Store/find global conversation memory under `~/.okstra/memory-book` |
165
176
  | `recap` | `src/commands/inspect/recap.mjs` | `okstra recap <assemble\|record>` Node wrapper backing the okstra-inspect `recap` facet |
166
177
  | `container` | `src/commands/inspect/container.mjs` | `bin okstra container` thin shim into `scripts/okstra_ctl/container.py` for the okstra-container-build skill |
178
+ | `manager` | `src/commands/manager.mjs` | Thin shim into `scripts/okstra_ctl/manager_cli.py` for cross-project manager state and child launch packets |
167
179
 
168
180
  `src/lib/python-helper.mjs` centralizes Node → Python execution so command modules do not duplicate subprocess wiring.
169
181
 
@@ -176,7 +188,7 @@ Top-level scripts:
176
188
  | `okstra.sh` | Bash CLI wrapper around `prepare_task_bundle`, optionally launches `claude` |
177
189
  | `okstra-ctl.sh` | Bash control center for list/show/open/rerun/reconcile/project commands |
178
190
  | `okstra-central.sh` | Central run index writer / reconciler entrypoint |
179
- | `okstra-codex-exec.sh`, `okstra-antigravity-exec.sh` | Worker CLI wrappers with log/status sidecars |
191
+ | `okstra-claude-exec.sh`, `okstra-codex-exec.sh`, `okstra-antigravity-exec.sh` | Worker CLI wrappers with log/status sidecars |
180
192
  | `okstra-wrapper-status.py` | Heartbeat sidecar writer used by worker wrappers |
181
193
  | `okstra-token-usage.py` | Token usage CLI entrypoint |
182
194
  | `okstra-render-final-report.py` | Render final-report Markdown from data.json |
@@ -227,6 +239,11 @@ Important modules:
227
239
  | `container.py` | okstra-container-build 공개 스킬의 `okstra container` 수렴 entrypoint — `provision_container_group` + `up`/`status`/`logs`/`stop-watcher`/`down` 디스패치, env override 합성, compose argv 조립, 컨테이너별 watcher 기동 |
228
240
  | `container_registry.py` | flock-guarded 보조 인덱스 — 컨테이너 그룹별 tmux session/pane 및 watcher findings 추적 |
229
241
  | `plan_run_root.py` | `approved_plan_path` → `plan_run_root` 파생 + task-key 역추적 공유 헬퍼 |
242
+ | `manager_cli.py` | `okstra manager` Python entrypoint — manager init/discover/new/task subcommands and JSON output |
243
+ | `manager_paths.py` | Manager state path SSOT under `~/.okstra/managers/<manager-id>/`; slug fallback uses `u-<sha1-prefix>` when a safe segment would be empty |
244
+ | `manager_store.py` | Manager-owned state mutation — project membership, task planning, assignment, directives, event append |
245
+ | `manager_sync.py` | One-way child project `.okstra` snapshot reader; corrupt child state becomes row-level `error` so other children continue |
246
+ | `manager_launch.py` | Child launch packet and manager child context renderer; records `prepared` launch metadata/events without changing project-local task state |
230
247
 
231
248
  ### 4.4 `scripts/okstra_project/`
232
249
 
@@ -326,17 +343,17 @@ The lead operating contract moved out of `agents/` to `prompts/lead/okstra-lead-
326
343
  1. Resolve project root and verify/upsert `project.json`.
327
344
  2. Resolve profile, required workers, model assignments, executor provider.
328
345
  3. Compute task/run paths and persist run context under `runs/<task-type>/manifests/`.
329
- 4. Provision or reuse the task-key worktree.
346
+ 4. Provision or reuse the task-key worktree, or the selected implementation stage worktree for stage-isolated runs.
330
347
  5. Materialize `instruction-set/` files and lead prompt snapshot.
331
348
  6. Persist run inputs, team state, task manifest, task index, run manifest, timeline, discovery pointers.
332
349
  7. Record the run in `~/.okstra/{active,recent}.jsonl` and project index.
333
350
 
334
351
  ### 5.2 Worktree model
335
352
 
336
- One task-key owns one worktree:
353
+ Non-`implementation` phases share one task-key worktree:
337
354
 
338
355
  ```text
339
- ~/.okstra/worktrees/<project-id>/<task-group>/<task-id>/
356
+ ~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/
340
357
  ```
341
358
 
342
359
  Branch name:
@@ -345,7 +362,16 @@ Branch name:
345
362
  <work-category-prefix>-<task-id-segment>
346
363
  ```
347
364
 
348
- The same worktree/branch is reused across all phases. `worktreeSyncDirs`, `worktreeSyncFiles`, and `worktreeSnapshotFiles` provide filesystem continuity only; they do not expand okstra's context/write boundary.
365
+ That task-key worktree is reused for `requirements-discovery` through `implementation-planning`, and by whole-task verification / handoff flows after implementation stages have been integrated. `worktreeSyncDirs`, `worktreeSyncFiles`, and `worktreeSnapshotFiles` provide filesystem continuity only; they do not expand okstra's context/write boundary.
366
+
367
+ `implementation` is stage-isolated: one run owns one `stage-<N>` worktree and branch, with stage-key reservation in `worktree_registry.py`.
368
+
369
+ ```text
370
+ ~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/stage-<N>/
371
+ <work-category-prefix>-<task-id-segment>-s<N>
372
+ ```
373
+
374
+ Single-stage `final-verification --stage <N>` reuses the matching implementation stage worktree read-only, while run artifacts are isolated under `runs/final-verification/stage-<N>/`. Whole-task final-verification keeps the flat `runs/final-verification/` shape.
349
375
 
350
376
  ### 5.3 Final report model
351
377
 
@@ -368,13 +394,14 @@ The Markdown is derived, not the authoring source. The schema is the contract.
368
394
  ~/.okstra/
369
395
  ├── version
370
396
  ├── lib/python/{okstra_ctl,okstra_project,okstra_token_usage,okstra_vendor,lib/}
371
- ├── bin/{okstra.sh,okstra-codex-exec.sh,okstra-antigravity-exec.sh,...}
397
+ ├── bin/{okstra.sh,okstra-claude-exec.sh,okstra-codex-exec.sh,okstra-antigravity-exec.sh,...}
372
398
  ├── templates/
373
399
  ├── installed-skills.json
374
400
  ├── installed-agents.json
375
401
  ├── active.jsonl
376
402
  ├── recent.jsonl
377
403
  ├── projects/<project-id>/{meta.json,index.jsonl}
404
+ ├── managers/<manager-id>/{manager.json,projects.json,task-groups/...}
378
405
  ├── worktrees/{registry.json,registry.lock,<project>/<group>/<task>/}
379
406
  ├── archive/
380
407
  └── .locks/
@@ -481,4 +508,4 @@ Clarifications now live in the unified `## 1. Clarification Items` table. Deprec
481
508
 
482
509
  ---
483
510
 
484
- *Updated: 2026-06-05 · Source of truth checked against `package.json`, `bin/okstra`, `tools/build.mjs`, `scripts/`, `skills/`, `agents/`, `templates/`, `schemas/`, `validators/`, and tests.*
511
+ *Updated: 2026-06-27 · Source of truth checked against `package.json`, `bin/okstra`, `src/cli-registry.mjs`, `src/lib/skill-catalog.mjs`, `tools/build.mjs`, `scripts/`, `skills/`, `agents/`, `templates/`, `schemas/`, `validators/`, and tests.*
@@ -42,7 +42,7 @@ flowchart TD
42
42
  | `error-analysis` | [error-analysis.md](error-analysis.md) | 증상과 evidence로 원인 후보와 검증 경로를 찾는다. |
43
43
  | `implementation-planning` | [implementation-planning.md](implementation-planning.md) | 구현 옵션, 검증, rollback, approval gate가 있는 계획을 만든다. |
44
44
  | `implementation` | [implementation.md](implementation.md) | 승인된 계획을 executor가 구현하고 verifier가 독립 검증한다. |
45
- | `final-verification` | [final-verification.md](final-verification.md) | 구현 결과를 read-only로 acceptance 판단한다. |
45
+ | `final-verification` | [final-verification.md](final-verification.md) | 구현 결과의 whole-task 또는 single-stage acceptance 판정한다. |
46
46
  | `release-handoff` | [release-handoff.md](release-handoff.md) | accepted verdict 뒤 push/PR handoff를 lead-only로 수행한다. |
47
47
 
48
48
  ## 4. 핵심 코드 위치
@@ -52,10 +52,13 @@ flowchart TD
52
52
  | okstra-run skill 절차 | [`skills/okstra-run/SKILL.md`](../../skills/okstra-run/SKILL.md) |
53
53
  | wizard state machine | [`scripts/okstra_ctl/wizard.py`](../../scripts/okstra_ctl/wizard.py) |
54
54
  | wizard prompt text | [`prompts/wizard/prompts.ko.json`](../../prompts/wizard/prompts.ko.json) |
55
- | render-bundle Node shim | [`src/render-bundle.mjs`](../../src/render-bundle.mjs) |
55
+ | render-bundle Node shim | [`src/commands/execute/render-bundle.mjs`](../../src/commands/execute/render-bundle.mjs) |
56
56
  | bundle 생성 단일 진입점 | [`scripts/okstra_ctl/run.py`](../../scripts/okstra_ctl/run.py) |
57
+ | implementation stage 선택/provision | [`scripts/okstra_ctl/implementation_stage.py`](../../scripts/okstra_ctl/implementation_stage.py) |
58
+ | stage target/base/verification 정책 | [`scripts/okstra_ctl/stage_targets.py`](../../scripts/okstra_ctl/stage_targets.py) |
57
59
  | phase boundary | [`scripts/okstra_ctl/workflow.py`](../../scripts/okstra_ctl/workflow.py) |
58
60
  | task worktree | [`scripts/okstra_ctl/worktree.py`](../../scripts/okstra_ctl/worktree.py) |
61
+ | stage-group handoff | [`scripts/okstra_ctl/handoff.py`](../../scripts/okstra_ctl/handoff.py) |
59
62
  | worker roster parser | [`scripts/okstra_ctl/workers.py`](../../scripts/okstra_ctl/workers.py) |
60
63
  | lead operating contract | [`prompts/lead/okstra-lead-contract.md`](../../prompts/lead/okstra-lead-contract.md) |
61
64
  | phase profiles | [`prompts/profiles/`](../../prompts/profiles/) |
@@ -68,7 +71,6 @@ flowchart TD
68
71
  | `requirements-discovery` | 공통 질문만 | profile/brief/base-ref 존재 | multi-worker analysis, convergence 1 round default | `pending-routing-decision` |
69
72
  | `error-analysis` | 공통 질문만 | profile/brief/base-ref 존재 | multi-worker analysis, convergence 2 rounds default | `implementation-planning` |
70
73
  | `implementation-planning` | 공통 질문만 | profile/brief/base-ref 존재 | multi-worker analysis + Phase 6 plan-body verification | `implementation` |
71
- | `implementation` | approved plan, stage, executor | approved marker, stage map, QA command deny-list | executor writes, verifiers read-only | `final-verification` |
72
- | `final-verification` | 공통 질문만 | profile/brief/base-ref 존재 | multi-worker read-only verification | `pending-release-handoff` |
73
- | `release-handoff` | PR template override/scope | PR template resolution, empty worker roster | single-lead, no TeamCreate, no workers | `done-or-follow-up` |
74
-
74
+ | `implementation` | approved plan, stage multi-pick, executor | approved marker, stage map, stage-key reservation, QA command deny-list | one run = one stage; executor writes in isolated stage worktree, verifiers read-only | `final-verification` |
75
+ | `final-verification` | approved plan, stage pick(whole-task or single-stage) | `VERIFICATION_TARGET` 해소; whole-task 자동 통합/teardown 또는 single-stage worktree reuse | whole-task may integrate stages first; analyser verification itself is read-only | `pending-release-handoff` |
76
+ | `release-handoff` | handoff scope(stage-group or whole-task), PR template override/scope | accepted verification eligibility, generated `release-handoff-input.md`, empty worker roster | single-lead; whole-task PR or stage-group collector branch/PR | `done-or-follow-up` |
@@ -93,7 +93,7 @@ sequenceDiagram
93
93
  Skill->>FS: read claude-execution-prompt.md
94
94
  ```
95
95
 
96
- `render-bundle`은 `--workspace-root`와 `--render-only`를 직접 붙인다. 따라서 okstra-run 경로의 initial run status는 `prepared`, task status는 `instruction-set-generated`로 시작한다.
96
+ `render-bundle`의 Node shim은 [`src/commands/execute/render-bundle.mjs`](../../src/commands/execute/render-bundle.mjs)다. 이 shim이 `--workspace-root`, `--render-only`, runtime resolution 인자를 직접 붙인다. 따라서 okstra-run 경로의 initial run status는 `prepared`, task status는 `instruction-set-generated`로 시작한다.
97
97
 
98
98
  ## 5. Claude lead phase 1-7
99
99
 
@@ -131,8 +131,15 @@ flowchart TD
131
131
  Runs --> Reports[reports/final-report-*.md<br/>reports/*.data.json]
132
132
  Runs --> State[state/*.json]
133
133
  Runs --> Status[status/final-status-*.json]
134
+ Runs --> ImplStage[implementation/stage-N/...]
135
+ Runs --> FvStage[final-verification/stage-N/...]
136
+ Runs --> Carry[implementation/carry/stage-N.json]
137
+ Runs --> Consumers[implementation-planning/consumers.jsonl]
138
+ Root --> Handoff[release-handoff-input.md]
134
139
  ```
135
140
 
141
+ `implementation`과 단일-stage `final-verification`은 run 산출물을 `stage-<N>/` 아래에 격리한다. `implementation`의 carry sidecar와 `consumers.jsonl`은 stage 간 조율 ledger라 phase root에 남는다. `release-handoff`는 brief를 받지 않고 prepare가 `release-handoff-input.md`를 생성한다.
142
+
136
143
  `runtime/`은 build output이므로 이 흐름을 고칠 때는 source인 `scripts/`, `skills/`, `agents/`, `prompts/`, `templates/`, `validators/`를 수정하고 `npm run build`로 갱신한다.
137
144
 
138
145
  ## 7. 공통 분기 규칙
@@ -157,10 +164,12 @@ flowchart TD
157
164
 
158
165
  중요한 점은 `Use defaults`가 model 기본값을 뜻한다는 것이다. non-implementation task-type에서는 defaults를 골라도 worker roster 질문은 계속 나온다. 반대로 `implementation`은 worker override 질문이 없다. profile default roster와 executor binding을 사용한다.
159
166
 
167
+ Worktree 규칙은 phase별로 다르다. `requirements-discovery`부터 `implementation-planning`까지는 task-key worktree를 재사용한다. `implementation`은 task-key worktree를 anchor로 삼되 실제 실행은 stage-key(`stage-<N>`) worktree와 `runs/implementation/stage-<N>/` 산출물에서 한 stage씩 격리한다. `final-verification --stage N`은 해당 implementation stage worktree를 읽기 대상으로 재사용하고, whole-task 모드는 task-key worktree에 stage commit을 자동 통합한 뒤 검증 target을 만든다.
168
+
160
169
  ## 8. 주의할 불일치 지점
161
170
 
162
171
  - `okstra-run` wizard 경로는 implementation approved plan에 이미 approval marker가 있어야 통과한다. `scripts/okstra.sh`에는 `--approve`가 있어 checkbox를 CLI ack로 flip할 수 있지만, 현재 wizard `render_args()`에는 `approve` flag가 없다.
163
- - `release-handoff`도 task worktree provisioning 대상이다. 정상적인 handoff는 같은 task-key의 implementation/final-verification worktree재사용하는 흐름이 안전하다. 새 task로 시작하면 새 worktree가 만들어지고, profile"implementation commit이 있어야 함" entry gate에서 막힐 수 있다.
172
+ - `release-handoff`는 brief가 없다. prepare가 accepted final-verification report인용해 `release-handoff-input.md`를 만들고, wizard`handoff_stage_pick` 또는 CLI `--stages`가 whole-task/stage-group 범위를 고정한다.
173
+ - `release-handoff`도 task worktree provisioning 대상이다. 정상적인 handoff는 같은 task-key의 implementation/final-verification 결과를 재사용하는 흐름이 안전하다. 새 task로 시작하면 새 worktree가 만들어지고, profile의 "implementation commit이 있어야 함" entry gate에서 막힐 수 있다.
164
174
  - `release-handoff`는 profile에 `Required workers:` block이 없다. runtime도 worker roster를 강제로 empty로 만든다.
165
175
  - 모든 task-type은 한 run 안에서 다음 lifecycle phase를 시작하지 않는다. "다음 단계 진행해"는 현재 phase 산출을 마무리하라는 뜻으로만 해석한다.
166
-
@@ -21,10 +21,10 @@
21
21
  flowchart TD
22
22
  Start[/okstra-run/] --> Common[공통 task identity flow]
23
23
  Common --> Type[task-type = final-verification]
24
- Type --> Worktree{active task worktree?}
25
- Worktree -->|yes| Defaults[Use defaults / Customize]
26
- Worktree -->|no| BaseRef[base-ref pick/text]
27
- BaseRef --> Defaults
24
+ Type --> PlanPick[approved plan pick]
25
+ PlanPick --> Approved[approval marker confirm]
26
+ Approved --> Stage[stage pick<br/>whole-task or stage number]
27
+ Stage --> Defaults[Use defaults / Customize]
28
28
  Defaults -->|defaults| Workers[worker roster pick<br/>claude/codex/report-writer default]
29
29
  Defaults -->|customize| Models[lead + worker model prompts]
30
30
  Models --> Extras[directive, related tasks, clarification]
@@ -35,38 +35,47 @@ flowchart TD
35
35
 
36
36
  `final-verification`은 analyser roster가 있는 non-implementation phase다. 따라서 `Use defaults`를 골라도 worker roster prompt는 계속 나온다. 기본 required workers는 `claude`, `codex`, `report-writer`이고, `antigravity`는 opt-in이다.
37
37
 
38
+ 이 phase는 `base-ref`를 직접 묻지 않는다. wizard는 approved plan의 Stage Map을 기준으로 whole-task 또는 단일 stage를 선택하고, prepare가 registry/`consumers.jsonl`/git 상태에서 `VERIFICATION_TARGET`을 해소한다.
39
+
38
40
  ## 3. entry gate
39
41
 
40
42
  ```mermaid
41
43
  sequenceDiagram
42
- participant Lead as Claude lead
43
- participant Brief as task brief
44
- participant Impl as implementation final-report
44
+ participant P as prepare_task_bundle
45
+ participant C as consumers.jsonl
46
+ participant Reg as worktree registry
45
47
  participant Git as verification worktree
48
+ participant Lead as Claude lead
46
49
 
47
- Lead->>Brief: Source Implementation Report section read
48
- Lead->>Impl: required sections present?
49
- Lead->>Git: inspect same worktree named by report
50
- Lead->>Git: git rev-parse HEAD
51
- Lead->>Git: git status --short
52
- Lead->>Git: git diff --stat <implementation-base>..HEAD
53
- Git-->>Lead: verification target snapshot
54
- Lead->>Lead: commit list and diff summary match?
50
+ P->>C: backfill carry and read done rows
51
+ P->>Reg: resolve task or stage worktree
52
+ alt whole-task
53
+ P->>Git: merge done stage commits into task worktree
54
+ P->>Git: teardown clean stage worktrees/registry keys
55
+ else single-stage
56
+ P->>Git: reuse selected stage worktree
57
+ end
58
+ P->>Git: resolve base/head/diff stat
59
+ P-->>Lead: inject VERIFICATION_TARGET
60
+ Lead->>Lead: verify against injected target only
55
61
  ```
56
62
 
57
- lead는 worker dispatch 전에 verification target을 고정한다. 다음 중 하나라도 실패하면 검증하지 않고 `blocked`로 끝내며 `implementation` 또는 `implementation-planning`으로 되돌린다.
63
+ prepare는 worker dispatch 전에 verification target을 고정한다. 다음 중 하나라도 실패하면 lead prompt가 시작되지 않고 `PrepareError`로 멈춘다.
64
+
65
+ - approved plan의 Stage Map을 읽을 수 없다.
66
+ - whole-task인데 Stage Map의 모든 stage가 `done`이 아니다.
67
+ - whole-task 자동 통합 중 merge conflict가 난다.
68
+ - single-stage인데 선택 stage의 implementation stage worktree가 없거나 dirty다.
69
+ - verification worktree가 `.okstra/` 밖 dirty state다.
58
70
 
59
- - task brief에 `## Source Implementation Report`가 없다.
60
- - 인용된 implementation final report가 없거나 필수 section이 빠져 있다.
61
- - report가 named worktree를 가리키는데 lead가 다른 checkout을 보고 있다.
62
- - implementation report의 commit list / diff summary와 현재 checkout이 맞지 않는다.
63
- - delivered code change commit이 없다.
71
+ 시작된 lead는 `VERIFICATION_TARGET`을 권위로 취급한다. worktree/base/head/stage/source report를 brief에서 다시 고르지 않는다.
64
72
 
65
73
  ## 4. 검증 실행 흐름
66
74
 
67
75
  ```mermaid
68
76
  flowchart TD
69
- Gate[entry gate passed] --> Lead[Claude lead fixes target snapshot]
77
+ Gate[entry gate passed] --> Target[injected VERIFICATION_TARGET]
78
+ Target --> Lead[Claude lead confirms target snapshot]
70
79
  Lead --> CW[Claude verifier<br/>read-only]
71
80
  Lead --> XW[Codex verifier<br/>read-only]
72
81
  Lead --> GW{Antigravity opt-in?}
@@ -110,7 +119,7 @@ flowchart TD
110
119
 
111
120
  `## 7. Final Verdict`에는 `Verdict Token` field가 정확히 하나 들어가야 하며 값은 다음 셋 중 하나다.
112
121
 
113
- - `accepted`: release-handoff 넘겨도 되는 상태
122
+ - `accepted`: whole-task이면 plain `release-handoff`, single-stage이면 `release-handoff(stage-group)` 후보가 되는 상태
114
123
  - `conditional-accept`: 조건을 모두 명시해야 하며, 조건이 gate면 다음 phase를 막는다
115
124
  - `blocked`: acceptance blocker가 있어 release-handoff로 갈 수 없는 상태
116
125
 
@@ -132,6 +141,7 @@ flowchart TD
132
141
  final report에는 최소 다음이 필요하다.
133
142
 
134
143
  - originating implementation final-report path
144
+ - verification scope(`whole-task` 또는 `single-stage`)와 injected `VERIFICATION_TARGET`
135
145
  - inspected worktree path
136
146
  - implementation base ref와 run start head SHA
137
147
  - quoted commit list / diff summary
@@ -146,15 +156,16 @@ final report에는 최소 다음이 필요하다.
146
156
 
147
157
  ```mermaid
148
158
  flowchart TD
149
- FV[final-verification] --> Allowed[read-only inspect/test/validate]
159
+ FV[final-verification] --> Runtime[prepare-owned whole-task integration<br/>or single-stage target resolution]
160
+ FV --> Allowed[lead read-only inspect/test/validate]
150
161
  FV -. forbidden .-> Edit[source edit or config edit]
151
162
  FV -. forbidden .-> Fix[in-run bug fix]
152
- FV -. forbidden .-> Mutate[mutating command]
163
+ FV -. forbidden .-> Mutate[lead-owned mutating command]
153
164
  FV -. forbidden .-> Expand[scope expansion]
154
165
  FV -. forbidden .-> Hide[hide verifier dissent]
155
166
  ```
156
167
 
157
- source edit, follow-up fix, scope expansion은 전부 금지다. 결함을 발견하면 현재 run 안에서 고치지 않고 final report에 blocker로 기록한 뒤 새 `error-analysis` 또는 `implementation-planning` 입력으로 넘긴다.
168
+ whole-task mode의 stage merge/teardown은 prepare가 수행하는 runtime-owned 통합 단계다. 그 이후 lead 검증은 read-only다. source edit, follow-up fix, scope expansion은 전부 금지다. 결함을 발견하면 현재 run 안에서 고치지 않고 final report에 blocker로 기록한 뒤 새 `error-analysis` 또는 `implementation-planning` 입력으로 넘긴다.
158
169
 
159
170
  ## 8. 확인한 코드
160
171
 
@@ -27,8 +27,9 @@ flowchart TD
27
27
  BaseRef --> PlanPick
28
28
  PlanPick --> Approved{APPROVED marker present?}
29
29
  Approved -->|no| Retry[re-prompt same step]
30
- Approved -->|yes| Stage[stage pick<br/>auto or number]
31
- Stage --> Executor[executor pick<br/>claude/codex/antigravity]
30
+ Approved -->|yes| Stage[stage multi-pick<br/>ready/active markers]
31
+ Stage --> Chain[render-args<br/>stage + chain-stages]
32
+ Chain --> Executor[executor pick<br/>claude/codex/antigravity]
32
33
  Executor --> Defaults[Use defaults / Customize]
33
34
  Defaults -->|defaults| Confirm
34
35
  Defaults -->|customize| Models[lead + executor model + report-writer model]
@@ -37,7 +38,9 @@ flowchart TD
37
38
  Confirm --> Render[render-bundle]
38
39
  ```
39
40
 
40
- `implementation`은 worker override 질문이 없다. wizard `render_args()`가 `workers`를 빈 문자열로 내보내고, runtime이 profile default roster를 사용한다. 현재 okstra-run wizard 경로는 승인 checkbox를 직접 flip하는 `--approve`를 노출하지 않는다. plan file에는 이미 recognized approval marker가 있어야 한다.
41
+ `implementation`은 worker override 질문이 없다. wizard `render_args()`가 `workers`를 빈 문자열로 내보내고, runtime이 profile default roster를 사용한다. `stage_pick`은 완료/진행중/준비됨/대기 상태를 보여주는 multi-pick이다. 선택한 stage 집합은 의존성 closure와 위상정렬을 거쳐 `chain-stages` CSV가 되고, 실제 run은 그중 stage만 실행한다.
42
+
43
+ 현재 okstra-run wizard 경로는 승인 checkbox를 직접 flip하는 `--approve`를 노출하지 않는다. plan file에는 이미 recognized approval marker가 있어야 한다.
41
44
 
42
45
  ## 3. runtime gate
43
46
 
@@ -47,16 +50,21 @@ sequenceDiagram
47
50
  participant P as prepare_task_bundle
48
51
  participant Plan as approved final-report
49
52
  participant QA as project.json qaCommands
50
- participant Stage as stage validator
53
+ participant Stage as stage target policy
54
+ participant Reg as worktree registry
55
+ participant WT as stage worktree
51
56
 
52
57
  W->>P: task-type=implementation, approved-plan, stage, executor
53
58
  P->>Plan: file exists?
54
59
  P->>Plan: approval marker regex matches?
55
60
  P->>Plan: unresolved Blocks=approval rows?
56
- P->>Stage: validate stage structure
61
+ P->>Stage: parse Stage Map and recover carry/done rows
62
+ P->>Reg: read active stage-key reservations
63
+ P->>Stage: select exactly one ready stage
64
+ P->>WT: provision stage-N worktree + branch
57
65
  P->>QA: validate qaCommands deny-list
58
66
  P->>P: executor provider in resolved roster?
59
- P->>P: provision/reuse worktree
67
+ P->>P: namespace run artifacts under stage-N
60
68
  P-->>W: prepared implementation prompt or PrepareError
61
69
  ```
62
70
 
@@ -86,16 +94,21 @@ Executor만 project file을 mutate할 수 있다. verifier는 같은 worktree에
86
94
  ```mermaid
87
95
  flowchart LR
88
96
  Plan[approved plan<br/>Stage Map] --> Parse[parse stage map]
89
- Parse --> Done[read consumers.jsonl<br/>done stages]
90
- Done --> Resolve{stage arg}
91
- Resolve -->|auto| Next[lowest unblocked unfinished stage]
97
+ Parse --> Done[backfill carry<br/>read consumers.jsonl]
98
+ Done --> Reserved[read registry<br/>active stage reservations]
99
+ Reserved --> Resolve{stage arg}
100
+ Resolve -->|auto| Next[lowest ready<br/>not done/started/reserved]
92
101
  Resolve -->|number| Forced[selected stage]
93
- Next --> Started[append consumer status=started]
94
- Forced --> Started
95
- Started --> Run[implementation executes selected stage]
102
+ Next --> Base[resolve stage base commit]
103
+ Forced --> Base
104
+ Base --> WT[create/reuse stage worktree]
105
+ WT --> Started[append consumer status=started]
106
+ Started --> Run[implementation executes one selected stage]
96
107
  ```
97
108
 
98
- stage 선택은 `auto` 또는 숫자다. 다른 task-type에서 `--stage`가 오면 `PrepareError`다. wizard는 approved plan에서 Stage Map을 parse해 stage picker에 `auto`와 stage를 보여준다.
109
+ stage 선택은 `auto` 또는 숫자다. 다른 task-type에서 `--stage`가 오면 `PrepareError`다. runtime은 `consumers.jsonl`의 `done`/`started`와 registry의 active stage-key를 함께 보고 점유 stage를 제외한다.
110
+
111
+ stage worktree base는 dependency 형태로 정한다. 독립 stage는 첫 implementation 진입 때 고정한 task-key worktree HEAD를 anchor로 삼고, 단일 의존 stage는 선행 stage의 done `head_commit`에서 분기한다. 다중 의존 stage는 모든 선행 done commit이 task-key worktree HEAD의 ancestor인지 확인한 뒤 그 HEAD에서 분기한다.
99
112
 
100
113
  ## 6. 산출물
101
114
 
@@ -113,12 +126,14 @@ flowchart TD
113
126
  final report에는 최소 다음이 필요하다.
114
127
 
115
128
  - approved plan path와 quoted approval marker
129
+ - selected stage, isolated stage worktree path, run artifact path(`runs/implementation/stage-<N>/`)
116
130
  - commit SHA, message, plan step mapping
117
131
  - diff summary와 per-file summary
118
132
  - out-of-plan edits block
119
133
  - plan validation command의 실제 stdout/stderr와 exit code
120
134
  - TDD failing-then-passing evidence
121
135
  - verifier별 independent validation rerun result
136
+ - `carry/stage-<N>.json` evidence sidecar와 `consumers.jsonl` started/done row
122
137
  - rollback verification
123
138
  - follow-up tasks table
124
139
 
@@ -146,4 +161,3 @@ flowchart TD
146
161
  - [`validators/validate-implementation-plan-stages.py`](../../validators/validate-implementation-plan-stages.py)
147
162
  - [`scripts/okstra_ctl/qa_commands.py`](../../scripts/okstra_ctl/qa_commands.py)
148
163
  - [`prompts/lead/okstra-lead-contract.md`](../../prompts/lead/okstra-lead-contract.md)
149
-
@@ -14,7 +14,7 @@
14
14
 
15
15
  ## 1. 목적
16
16
 
17
- `release-handoff`는 `accepted` verdict가 나온 already-committed implementation branch를 push하거나 PR로 넘기는 terminal phase다. commit을 만들지 않고, 검증된 diff를 그대로 포장한다.
17
+ `release-handoff`는 `accepted` verdict가 나온 already-committed implementation 결과를 push하거나 PR로 넘기는 terminal phase다. whole-task mode는 검증된 task branch를 그대로 포장한다. stage-group mode는 선택 stage들을 collector branch에 assemble해 한 PR로 묶을 수 있으며, 이때 생성되는 merge commit은 `okstra handoff assemble`만 만든다.
18
18
 
19
19
  이 phase는 worker dispatch가 없다. `claude`, `codex`, `report-writer` roster를 쓰지 않으며 Claude lead가 inline으로 git/gh inspection, 사용자 질문, PR draft, final report 작성을 모두 수행한다.
20
20
 
@@ -24,7 +24,9 @@
24
24
  flowchart TD
25
25
  Start[/okstra-run/] --> Common[공통 task identity flow]
26
26
  Common --> Type[task-type = release-handoff]
27
- Type --> Worktree{active task worktree?}
27
+ Type --> Plan[approved plan auto/pick]
28
+ Plan --> Scope[handoff stage pick<br/>whole-task or eligible stages]
29
+ Scope --> Worktree{active task worktree?}
28
30
  Worktree -->|yes| Defaults[Use defaults / Customize]
29
31
  Worktree -->|no| BaseRef[base-ref pick/text]
30
32
  BaseRef --> Defaults
@@ -32,14 +34,14 @@ flowchart TD
32
34
  Defaults -->|customize| Models[lead model prompt]
33
35
  Models --> Extras[directive, related tasks, clarification]
34
36
  Extras --> Template[PR template override?]
35
- Template --> Scope[save template to project/global?]
36
- Scope --> Confirm
37
+ Template --> TemplateScope[save template to project/global?]
38
+ TemplateScope --> Confirm
37
39
  Confirm --> Render[render-bundle]
38
40
  ```
39
41
 
40
- `release-handoff`는 worker roster prompt가 없다. wizard의 `render_args()`는 `pr-template-path`를 release-handoff일 때만 포함하고, runtime은 worker list를 강제로 empty로 만든다.
42
+ `release-handoff`는 worker roster prompt가 없다. wizard의 `render_args()`는 `pr-template-path`를 release-handoff일 때만 포함하고, runtime은 worker list를 강제로 empty로 만든다. scope 선택은 prepare 전에 끝난다. whole-task는 accepted whole-task verification report가 있어야 하고, stage-group은 accepted single-stage verification으로 `verified` 처리됐지만 아직 `pr`로 덮이지 않은 stage만 후보가 된다.
41
43
 
42
- 주의할 점은 이 phase도 task worktree provisioning 대상이라는 것이다. 정상 흐름은 같은 task-key의 implementation/final-verification worktree를 재사용한다. 새 task로 시작하면 새 branch가 만들어질 수 있고, entry gate의 "implementation commit exists" 조건에서 막힐 가능성이 높다.
44
+ 주의할 점은 이 phase도 task worktree provisioning 대상이라는 것이다. 정상 흐름은 같은 task-key의 implementation/final-verification 결과를 재사용한다. 새 task로 시작하면 새 branch가 만들어질 수 있고, entry gate의 "implementation commit exists" 조건에서 막힐 가능성이 높다.
43
45
 
44
46
  ## 3. prepare 단계
45
47
 
@@ -72,7 +74,10 @@ flowchart TD
72
74
  Source -->|no| Block[blocked<br/>route final-verification]
73
75
  Source -->|yes| Verdict{Verdict Token == accepted?}
74
76
  Verdict -->|no| Block
75
- Verdict -->|yes| Status{git status --short clean?}
77
+ Verdict -->|yes| Mode{HANDOFF_MODE}
78
+ Mode -->|stage-group| Eligible[each selected stage<br/>verified and not in PR]
79
+ Mode -->|whole-task| Status{git status --short clean?}
80
+ Eligible --> Status
76
81
  Status -->|no| Dirty[blocked<br/>dirty tree]
77
82
  Status -->|yes| Branch{current branch is base branch?}
78
83
  Branch -->|yes| BaseBlock[blocked<br/>never operate on base branch]
@@ -84,7 +89,8 @@ flowchart TD
84
89
  lead는 사용자에게 push/PR 여부를 묻기 전에 다음을 확인한다.
85
90
 
86
91
  - prepare 가 생성한 input 문서(`release-handoff-input.md`)의 `## Source Verification Report` 가 모드(`HANDOFF_MODE`)와 인용 보고서 표를 담고 있다. brief 는 entry phase 의 입력물이라 release-handoff 에는 없다 — 사용자의 stage 선택은 wizard `handoff_stage_pick` 또는 CLI `--stages` 로 prepare 전에 끝난다.
87
- - 인용된 report `## 7. Final Verdict`에 `Verdict Token = accepted`가 정확히 있다.
92
+ - whole-task mode에서는 인용 report `verificationScope=whole-task`이고 `Verdict Token = accepted`여야 한다.
93
+ - stage-group mode에서는 인용된 각 single-stage report가 `Verdict Token = accepted`여야 하며, prepare/`okstra handoff assemble`이 eligibility와 dependency closure를 다시 강제한다.
88
94
  - working tree가 clean이다.
89
95
  - 현재 branch가 `main`, `master`, `prod`, `preprod`, `staging`, `dev` 같은 base branch가 아니다.
90
96
  - `<base>..HEAD` commit range가 비어 있지 않다.
@@ -99,7 +105,11 @@ stateDiagram-v2
99
105
  Gate --> Q1: action selection
100
106
  Q1 --> LocalCheckout: local checkout
101
107
  Q1 --> Skip: skip
102
- Q1 --> Q2: push + PR
108
+ Q1 --> Q2: push + PR whole-task
109
+ Q1 --> G2: push + PR stage-group
110
+ state "base select + stage confirmation / okstra handoff assemble" as Assemble
111
+ G2 --> Assemble
112
+ Assemble --> Q3: collector branch ready
103
113
  Q2 --> Probe: choose PR base
104
114
  Probe --> Q3: no conflict
105
115
  Probe --> Conflict: conflict detected
@@ -124,6 +134,8 @@ stateDiagram-v2
124
134
 
125
135
  `push + PR`에서만 merge-conflict probe를 한다.
126
136
 
137
+ stage-group mode에서는 `local checkout`을 제공하지 않는다. `push + PR`을 고른 뒤 PR base를 먼저 선택하고, 이미 고정된 `HANDOFF_STAGES`를 확인한 다음 `okstra handoff assemble`이 collector branch를 만든다. 이후 conflict probe와 PR title/body 확인은 collector branch를 head로 삼는다.
138
+
127
139
  ```mermaid
128
140
  flowchart TD
129
141
  PushPR[push + PR selected] --> Fetch[git fetch origin chosen-base]
@@ -170,11 +182,13 @@ flowchart TD
170
182
  final report에는 최소 다음이 필요하다.
171
183
 
172
184
  - originating final-verification report path와 quoted `accepted` verdict row
185
+ - handoff mode(`whole-task` 또는 `stage-group`)와 selected stages
173
186
  - feature branch와 run start `git status --short`
174
187
  - 사용자 선택 기록
175
188
  - 실행한 모든 git/gh command와 exit code
176
189
  - implementation commit list
177
190
  - merge-conflict probe 결과
191
+ - stage-group이면 collector branch, merge commit SHA, dependency-closure 결과
178
192
  - PR 생성, 재사용, 생략 결과
179
193
  - routing recommendation `done`
180
194
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.103.0",
3
+ "version": "0.105.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.103.0",
3
- "builtAt": "2026-06-27T12:06:27.647Z",
2
+ "package": "0.105.0",
3
+ "builtAt": "2026-06-27T14:17:33.391Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -4,54 +4,33 @@ docker 라벨은 컨테이너 존재 여부의 기준값(SSOT)이지만 tmux ses
4
4
  findings 파일 경로 같은 운영 메타데이터는 추적하지 못한다. 이 모듈은 task root
5
5
  하위 ``container/registry.json`` 에 그런 보조 정보를 가벼운 dict 로 보관한다.
6
6
 
7
- flock + tmp+``os.replace`` 원자 쓰기 idiom 은 ``worktree_registry`` 의
8
- ``_registry_lock``/``_load``/``_save``그대로 복제했고, lock/registry 경로만
9
- ``paths.container_paths()`` 로 해소해 전역 ``~/.okstra/worktrees/registry.json``
10
- 과 분리한다. dict 구조는 ``{"services": {<svc>: {session_name, pane_id,
11
- findings_path}}}`` 한 가지뿐 — worktree_registry 의 dataclass 는 복제하지 않는다.
7
+ flock + tmp+``os.replace`` 원자 쓰기 idiom 은 ``json_registry`` 의 공용
8
+ Interface사용하고, lock/registry 경로만 ``paths.container_paths()`` 로
9
+ 해소해 전역 ``~/.okstra/worktrees/registry.json`` 과 분리한다. dict 구조는
10
+ ``{"services": {<svc>: {session_name, pane_id, findings_path}}}`` 한 가지뿐 —
11
+ worktree_registry 의 dataclass 는 복제하지 않는다.
12
12
  """
13
13
  from __future__ import annotations
14
14
 
15
- import contextlib
16
- import fcntl
17
- import json
18
- import os
19
15
  from pathlib import Path
20
16
 
17
+ from .json_registry import load_registry_json, registry_lock, save_registry_json
21
18
  from . import paths
22
19
 
23
20
 
24
- @contextlib.contextmanager
25
21
  def _registry_lock(lock_path: Path):
26
- """``container/registry.json.lock`` 에 대한 배타 flock. worktree_registry 의
27
- ``_registry_lock`` 과 동일한 패턴이되 경로를 인자로 받는다(전역 lock 과 분리)."""
28
- lock_path.parent.mkdir(parents=True, exist_ok=True)
29
- if not lock_path.exists():
30
- lock_path.touch()
31
- f = lock_path.open("r+")
32
- try:
33
- fcntl.flock(f.fileno(), fcntl.LOCK_EX)
34
- yield
35
- finally:
36
- f.close()
22
+ """``container/registry.json.lock`` 에 대한 배타 flock."""
23
+ return registry_lock(lock_path)
37
24
 
38
25
 
39
26
  def _load(registry_path: Path) -> dict:
40
- if not registry_path.exists():
41
- return {"services": {}}
42
- try:
43
- data = json.loads(registry_path.read_text())
44
- except (OSError, json.JSONDecodeError):
45
- return {"services": {}}
27
+ data = load_registry_json(registry_path, lambda: {"services": {}})
46
28
  data.setdefault("services", {})
47
29
  return data
48
30
 
49
31
 
50
32
  def _save(registry_path: Path, data: dict) -> None:
51
- registry_path.parent.mkdir(parents=True, exist_ok=True)
52
- tmp = registry_path.with_suffix(".json.tmp")
53
- tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
54
- os.replace(tmp, registry_path)
33
+ save_registry_json(registry_path, data)
55
34
 
56
35
 
57
36
  def reserve(
@@ -0,0 +1,43 @@
1
+ """Shared flock + atomic JSON persistence for small okstra registries."""
2
+ from __future__ import annotations
3
+
4
+ import contextlib
5
+ import fcntl
6
+ import json
7
+ import os
8
+ from collections.abc import Callable, Iterator
9
+ from pathlib import Path
10
+
11
+
12
+ @contextlib.contextmanager
13
+ def registry_lock(lock_path: Path) -> Iterator[None]:
14
+ """Hold an exclusive flock on ``lock_path``."""
15
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
16
+ if not lock_path.exists():
17
+ lock_path.touch()
18
+ handle = lock_path.open("r+")
19
+ try:
20
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
21
+ yield
22
+ finally:
23
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
24
+ handle.close()
25
+
26
+
27
+ def load_registry_json(path: Path, default_factory: Callable[[], dict]) -> dict:
28
+ """Load a registry dict, or a fresh default when absent/corrupt."""
29
+ if not path.exists():
30
+ return default_factory()
31
+ try:
32
+ data = json.loads(path.read_text(encoding="utf-8"))
33
+ except (OSError, json.JSONDecodeError):
34
+ return default_factory()
35
+ return data if isinstance(data, dict) else default_factory()
36
+
37
+
38
+ def save_registry_json(path: Path, data: dict) -> None:
39
+ """Persist registry JSON via temp file + ``os.replace``."""
40
+ path.parent.mkdir(parents=True, exist_ok=True)
41
+ tmp = path.with_suffix(".json.tmp")
42
+ tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
43
+ os.replace(tmp, path)
@@ -4,17 +4,22 @@ from __future__ import annotations
4
4
  from pathlib import Path
5
5
  from typing import Mapping
6
6
 
7
- from okstra_ctl.jsonl import read_jsonl
7
+ from okstra_ctl.jsonl import append_jsonl, read_jsonl
8
8
 
9
9
  from .manager_paths import (
10
10
  child_context_path,
11
11
  children_json_path,
12
12
  directives_jsonl_path,
13
+ events_jsonl_path,
13
14
  projects_json_path,
14
15
  snapshots_json_path,
15
16
  task_manifest_path,
16
17
  )
17
- from .manager_store import ManagerError, _now_iso, _read_json, _read_json_default
18
+ from .manager_store import ManagerError, _now_iso, _read_json, _read_json_default, _write_json
19
+
20
+
21
+ WORKER_DISPATCH_BACKEND = "subagent"
22
+ LAUNCH_STATUS_PREPARED = "prepared"
18
23
 
19
24
 
20
25
  def select_child_lead_backend(env: Mapping[str, str]) -> str:
@@ -135,6 +140,47 @@ def _render_context(
135
140
  return "\n".join(lines)
136
141
 
137
142
 
143
+ def _record_launch_prepared(
144
+ *,
145
+ home: Path,
146
+ manager_id: str,
147
+ task_group: str,
148
+ task_id: str,
149
+ children: dict,
150
+ child: dict,
151
+ packet: dict,
152
+ created_at: str,
153
+ ) -> None:
154
+ launch = child.get("launch")
155
+ if not isinstance(launch, dict):
156
+ launch = {}
157
+ launch.update(
158
+ {
159
+ "status": LAUNCH_STATUS_PREPARED,
160
+ "childLeadBackend": packet["backend"],
161
+ "workerDispatchBackend": WORKER_DISPATCH_BACKEND,
162
+ "taskKey": packet["taskKey"],
163
+ "contextPath": packet["contextPath"],
164
+ "preparedAt": created_at,
165
+ }
166
+ )
167
+ child["launch"] = launch
168
+ _write_json(children_json_path(home, manager_id, task_group, task_id), children)
169
+ append_jsonl(
170
+ events_jsonl_path(home, manager_id, task_group, task_id),
171
+ {
172
+ "event": "child-launch-prepared",
173
+ "createdAt": created_at,
174
+ "projectId": packet["projectId"],
175
+ "taskKey": packet["taskKey"],
176
+ "backend": packet["backend"],
177
+ "workerDispatchBackend": WORKER_DISPATCH_BACKEND,
178
+ "contextPath": packet["contextPath"],
179
+ "runArgs": packet["runArgs"],
180
+ },
181
+ )
182
+
183
+
138
184
  def build_launch_packet(
139
185
  home: Path,
140
186
  manager_id: str,
@@ -159,6 +205,7 @@ def build_launch_packet(
159
205
  _validate_child_task_key(child, target_task_key)
160
206
  project_root = _validated_project_root(project)
161
207
  backend = select_child_lead_backend(env or {})
208
+ created_at = now or _now_iso()
162
209
  context_path = child_context_path(home, manager_id, task_group, task_id, project_id, child_task_id_value)
163
210
  context_path.parent.mkdir(parents=True, exist_ok=True)
164
211
  context_path.write_text(
@@ -171,14 +218,14 @@ def build_launch_packet(
171
218
  ),
172
219
  encoding="utf-8",
173
220
  )
174
- return {
221
+ packet = {
175
222
  "managerId": manager_id,
176
223
  "taskGroup": task_group,
177
224
  "taskId": task_id,
178
225
  "projectId": project_id,
179
226
  "taskKey": target_task_key,
180
227
  "backend": backend,
181
- "workerDispatchBackend": "subagent",
228
+ "workerDispatchBackend": WORKER_DISPATCH_BACKEND,
182
229
  "projectRoot": project_root,
183
230
  "contextPath": str(context_path),
184
231
  "runArgs": [
@@ -194,5 +241,16 @@ def build_launch_packet(
194
241
  "--directive",
195
242
  f"Read manager child context: {context_path}",
196
243
  ],
197
- "createdAt": now or _now_iso(),
244
+ "createdAt": created_at,
198
245
  }
246
+ _record_launch_prepared(
247
+ home=home,
248
+ manager_id=manager_id,
249
+ task_group=task_group,
250
+ task_id=task_id,
251
+ children=children,
252
+ child=child,
253
+ packet=packet,
254
+ created_at=created_at,
255
+ )
256
+ return packet
@@ -1,6 +1,7 @@
1
1
  """Pure path computation for okstra manager state."""
2
2
  from __future__ import annotations
3
3
 
4
+ import hashlib
4
5
  from pathlib import Path
5
6
  import os
6
7
 
@@ -21,9 +22,11 @@ def _require_id(value: str, label: str) -> str:
21
22
 
22
23
 
23
24
  def _slug_segment(value: str, label: str) -> str:
24
- segment = slugify(_require_id(value, label))
25
+ cleaned = _require_id(value, label)
26
+ segment = slugify(cleaned)
25
27
  if not segment:
26
- raise ManagerPathError(f"{label} must contain at least one ASCII alphanumeric character")
28
+ digest = hashlib.sha1(cleaned.encode("utf-8")).hexdigest()[:12]
29
+ return f"u-{digest}"
27
30
  return segment
28
31
 
29
32
 
@@ -93,6 +96,6 @@ def child_context_path(
93
96
  project_id: str,
94
97
  child_task_id: str,
95
98
  ) -> Path:
96
- safe_project = slugify(_require_id(project_id, "project-id"))
97
- safe_task = slugify(_require_id(child_task_id, "task-id"))
99
+ safe_project = _slug_segment(project_id, "project-id")
100
+ safe_task = _slug_segment(child_task_id, "task-id")
98
101
  return task_root(home, manager_id, task_group, task_id) / "child-context" / f"{safe_project}-{safe_task}.md"
@@ -126,9 +126,11 @@ def link_project(
126
126
  role: str,
127
127
  tags: list[str],
128
128
  now: str | None = None,
129
- ) -> dict:
129
+ ) -> dict:
130
130
  load_manager(home, manager_id)
131
131
  root = Path(project_root).resolve()
132
+ if not root.is_dir():
133
+ raise ManagerError(f"projectRoot is not a directory: {root}")
132
134
  project_json = project_json_path(root)
133
135
  if project_json.is_file():
134
136
  try:
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  from pathlib import Path
5
5
 
6
- from okstra_project import TASKS_RELATIVE, parse_task_key, read_task_catalog, read_task_manifest, slugify
6
+ from okstra_project import TASKS_RELATIVE, StateError, parse_task_key, read_task_catalog, read_task_manifest, slugify
7
7
 
8
8
  from .manager_paths import children_json_path, projects_json_path, snapshots_json_path, task_manifest_path
9
9
  from .manager_store import _now_iso, _read_json, _read_json_default, _write_json
@@ -52,9 +52,9 @@ def _resolve_task_root_read_only(project_root: Path, task_key: str) -> Path | No
52
52
  return None
53
53
 
54
54
 
55
- def _snapshot_child(project_root: Path, child: dict) -> dict:
55
+ def _base_child_snapshot(child: dict) -> dict:
56
56
  task_key = str(child.get("taskKey") or "")
57
- base = {
57
+ return {
58
58
  "projectId": str(child.get("projectId") or ""),
59
59
  "taskGroup": str(child.get("taskGroup") or ""),
60
60
  "taskId": str(child.get("taskId") or ""),
@@ -62,10 +62,18 @@ def _snapshot_child(project_root: Path, child: dict) -> dict:
62
62
  "exists": False,
63
63
  "taskRoot": "",
64
64
  }
65
- task_dir = _resolve_task_root_read_only(project_root, task_key)
66
- if task_dir is None:
67
- return base
68
- manifest = read_task_manifest(task_dir) or {}
65
+
66
+
67
+ def _snapshot_child(project_root: Path, child: dict) -> dict:
68
+ base = _base_child_snapshot(child)
69
+ task_key = base["taskKey"]
70
+ try:
71
+ task_dir = _resolve_task_root_read_only(project_root, task_key)
72
+ if task_dir is None:
73
+ return base
74
+ manifest = read_task_manifest(task_dir) or {}
75
+ except StateError as exc:
76
+ return {**base, "error": str(exc)}
69
77
  workflow = _workflow(manifest)
70
78
  outcome = _phase_outcome(manifest)
71
79
  return {
@@ -23,10 +23,6 @@ file is useful for debugging.
23
23
  """
24
24
  from __future__ import annotations
25
25
 
26
- import contextlib
27
- import fcntl
28
- import json
29
- import os
30
26
  import time
31
27
  from dataclasses import dataclass
32
28
  from pathlib import Path
@@ -34,6 +30,8 @@ from typing import Optional
34
30
 
35
31
  from okstra_project.dirs import okstra_home
36
32
 
33
+ from .json_registry import load_registry_json, registry_lock, save_registry_json
34
+
37
35
 
38
36
  REGISTRY_FILENAME = "registry.json"
39
37
  LOCK_FILENAME = "registry.lock"
@@ -77,23 +75,9 @@ class WorktreeEntry:
77
75
  stages: Optional[list] = None
78
76
 
79
77
 
80
- @contextlib.contextmanager
81
78
  def _registry_lock():
82
- """Exclusive flock on `<worktrees>/registry.lock`. Mirrors the
83
- `central_lock` pattern from `locks.py` but scoped to the registry
84
- so we do not serialise unrelated central operations.
85
- """
86
- root = _okstra_worktrees_dir()
87
- root.mkdir(parents=True, exist_ok=True)
88
- lockfile = root / LOCK_FILENAME
89
- if not lockfile.exists():
90
- lockfile.touch()
91
- f = lockfile.open("r+")
92
- try:
93
- fcntl.flock(f.fileno(), fcntl.LOCK_EX)
94
- yield
95
- finally:
96
- f.close()
79
+ """Exclusive flock on `<worktrees>/registry.lock`."""
80
+ return registry_lock(_okstra_worktrees_dir() / LOCK_FILENAME)
97
81
 
98
82
 
99
83
  def _registry_path() -> Path:
@@ -101,24 +85,14 @@ def _registry_path() -> Path:
101
85
 
102
86
 
103
87
  def _load() -> dict:
104
- p = _registry_path()
105
- if not p.exists():
106
- return {"tasks": {}, "branches": {}}
107
- try:
108
- data = json.loads(p.read_text())
109
- except (OSError, json.JSONDecodeError):
110
- return {"tasks": {}, "branches": {}}
88
+ data = load_registry_json(_registry_path(), lambda: {"tasks": {}, "branches": {}})
111
89
  data.setdefault("tasks", {})
112
90
  data.setdefault("branches", {})
113
91
  return data
114
92
 
115
93
 
116
94
  def _save(data: dict) -> None:
117
- p = _registry_path()
118
- p.parent.mkdir(parents=True, exist_ok=True)
119
- tmp = p.with_suffix(".json.tmp")
120
- tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
121
- os.replace(tmp, p)
95
+ save_registry_json(_registry_path(), data)
122
96
 
123
97
 
124
98
  def lookup(