okstra 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -164,7 +164,7 @@ Notable flags added in 0.7.0 / 0.8.0:
164
164
 
165
165
  Recent workflow additions (post-0.8.0, on `main`):
166
166
 
167
- - **Isolated executor worktree for `--task-type implementation`** — prepare automatically runs `git worktree add ~/.okstra/worktrees/<project>/<group>/<task>-<seq>` on a fresh branch `<work-category-prefix>-<task-id-segment>-<seq>` branched from `HEAD`. Executor and verifiers operate inside that worktree so the caller's working tree stays clean, and the worktree is preserved after the run as the canonical artefact for PR authoring and rollback. Skip paths: when the caller is already inside another worktree or `project_root` is not a git repo, provisioning no-ops. Manual cleanup: `git worktree remove <path>` → `git branch -D <branch>`. Details: [`docs/kr/architecture.md`](docs/kr/architecture.md) (*Task type* section) and [`docs/kr/cli.md#--executor`](docs/kr/cli.md#--executor).
167
+ - **Isolated task worktree for every task-type** — prepare automatically runs `git worktree add ~/.okstra/worktrees/<project>/<group>/<task>/` on a fresh branch `<work-category-prefix>-<task-id-segment>` branched from the main worktree's `HEAD` the first time a task-key is seen. Every subsequent phase of the same task-key (`requirements-discovery` `error-analysis` → `implementation-planning` → `implementation`) reuses the same path and branch, so phase N inherits the working-tree state phase N-1 left behind. A global registry at `~/.okstra/worktrees/registry.json` (flock-guarded) reserves task-keys and branches across concurrent runs; all path/branch segments are sanitised (`/`, `:`, etc. → `-`). The worktree is preserved after every run for follow-up phases, PR authoring, and rollback. Skip paths: when the caller is already inside another worktree or `project_root` is not a git repo, provisioning no-ops. Manual cleanup: `git worktree remove <path>` → `git branch -D <branch>` plus removing the task-key entry from the registry. Details: [`docs/kr/architecture.md`](docs/kr/architecture.md) (*Task type* section) and [`docs/kr/cli.md#--executor`](docs/kr/cli.md#--executor).
168
168
  - **`release-handoff` lifecycle phase** — runs after `final-verification` returns `verdict=accepted`. The lead drafts a commit message and PR body via a Claude worker, then prompts the user with `AskUserQuestion` for three choices: action (`commit only` / `commit + PR` / `skip`), PR base branch (`staging` / `preprod` / `prod` / `main` / `dev` / free-form), and message handling (`use as-is` / `edit then proceed` / `cancel`). Only user-selected mutating git/gh commands run. Force-push, base-branch direct push, hook bypass (`--no-verify`), and release publishing (`gh release`, `npm publish`, ...) are forbidden. Source code is not edited in this phase. Profile: [`prompts/profiles/release-handoff.md`](prompts/profiles/release-handoff.md).
169
169
 
170
170
  ### 3.5 Ops commands
@@ -321,7 +321,7 @@ Claude launch prompt 본문은 항상 `prompts/launch.template.md` 템플릿에
321
321
  공통 제약:
322
322
 
323
323
  - `implementation`을 제외한 모든 phase는 source code edit, build, migration, deployment, 그 밖의 state-mutating 명령을 금지합니다(`final-verification`은 read-only 테스트 명령만 허용). `implementation`은 승인된 plan의 파일 목록 안에서만 edit/commit이 허용되며, `git push`·publish·deploy·실제 migration·third-party write API는 여전히 금지됩니다.
324
- - **`implementation` 격리 worktree (BLOCKING)**: `--task-type implementation` run은 prepare 단계에서 `okstra-ctl` 이 자동으로 `git worktree` 를 생성해 executor·verifier가 모두 안에서 작업하도록 강제합니다. 위치는 `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>-<run-seq>` 이고, 브랜치 이름은 `<work-category-prefix>-<task-id-segment>-<run-seq>` (예: `feat-dev-9436-001`, `fix-dev-7311-002`) 입니다. base ref 는 prepare 시점의 `HEAD`. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 되고 executor 는 project_root 에서 그대로 작업합니다. worktree 는 run 종료 후 자동 삭제되지 않으며 PR 작성·rollback 검증·후속 `final-verification` 의 권위 artefact 입니다. 수동 cleanup: `git -C <project_root> worktree remove <path>` `git -C <project_root> branch -D <branch>`. 자세한 동작은 `prompts/profiles/implementation.md` 의 *Executor worktree* 블록과 `agents/SKILL.md` 의 *Implementation phase: Executor binding Executor worktree* 섹션 참고.
324
+ - **모든 task-type 격리 worktree (BLOCKING)**: 모든 task-type 번째 phase prepare 단계에서 `okstra-ctl` 이 자동으로 task-key 단위 `git worktree` 를 생성하고, 같은 task-key 이후 phase (`requirements-discovery` → `error-analysis` → `implementation-planning` → `implementation`) 는 동일한 worktree·브랜치를 재사용합니다. 위치는 `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` (segment 의 `/`·`:` 등 특수문자는 `-` 로 정규화) 이고, 브랜치 이름은 `<work-category-prefix>-<task-id-segment>` (예: `feat-dev-9436`, `fix-dev-7311`) 입니다. base ref 는 첫 phase prepare 시점의 main worktree `HEAD`. `~/.okstra/worktrees/registry.json` (flock-guarded) 가 task-key → path/branch 매핑을 전역 관리해 동시 실행 시 path·branch 충돌을 방지합니다. `.project-docs/`, `.scratch/`, `graphify-out/` 은 main worktree 에서 symlink 로 연결되어 모든 task 가 동일한 shared state 를 봅니다. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 되고 executor 는 project_root 에서 그대로 작업합니다. worktree 는 run 종료 후 자동 삭제되지 않으며 후속 phase·PR 작성·rollback 검증의 권위 artefact 입니다. 수동 cleanup: `git -C <main-worktree> worktree remove <path>` `git -C <main-worktree> branch -D <branch>` + registry 항목 삭제. 자세한 동작은 `prompts/profiles/implementation.md` 의 *Task worktree* 블록과 `agents/SKILL.md` 의 *Task worktree (BLOCKING for every task-type)* 섹션 참고.
325
325
  - `implementation` 과 `release-handoff` 를 제외한 모든 phase 는 source code edit, build, migration, deployment, 그 밖의 state-mutating 명령을 금지합니다 (`final-verification` 은 read-only 테스트 명령만 허용). `implementation` 은 승인된 plan 의 파일 목록 안에서만 edit/commit 이 허용되며, `git push`·publish·deploy·실제 migration·third-party write API 는 여전히 금지됩니다. `release-handoff` 는 source code 자체는 수정하지 않고, 사용자가 메뉴로 선택한 commit / push / PR 명령만 실행합니다 (force push, base 브랜치 직접 push, hook bypass, release publish 는 여전히 금지).
326
326
  - 사용자가 "다음 단계 진행해" 같은 표현을 보내도, 그 발화만으로 다음 phase가 자동 시작되지 않습니다. 다음 phase는 새 `okstra.sh` 실행으로만 시작합니다.
327
327
  - **Authority & permissions assumption (HARD RULE — 모든 task-type 및 `okstra-schedule` 공통)**: 사용자(및 팀)는 예상되는 모든 작업에 대해 완전한 권한·승인 권한을 보유한다고 가정합니다. 외부 승인, 서드파티 액세스, 역할/IAM 권한, 조직적 sign-off, 법무·보안 검토, 벤더 협의, "권한 보유 여부 확인" 같은 항목을 routing 결정·missing inputs·clarification questions·risk·dependency·open questions·effort/day 추정에 포함하지 않습니다. okstra 내부 phase 핸드오프(`User Approval Request` 등)는 사용자 본인이 즉시 승인 가능한 내부 게이트이므로 영향 없으며, `implementation`의 forbidden actions(`git push`, prod deploy, shared-DB migration 등)도 권한 사유가 아닌 **안전 사유**로 계속 적용됩니다.
package/docs/kr/cli.md CHANGED
@@ -288,7 +288,7 @@ fallback 기본값은 아래와 같습니다.
288
288
  - Executor 의 모델은 provider 별 worker 모델 플래그를 그대로 재사용합니다. 즉 `--executor codex` 이면 Executor 의 모델은 `--codex-model` (기본 `gpt-5.5`), `--executor gemini` 이면 `--gemini-model` (기본 `auto`) 가 됩니다.
289
289
  - Claude/Codex/Gemini 세 verifier 는 executor provider 와 관계없이 항상 dispatch 됩니다. Executor 와 같은 provider 라도 별도 CLI 세션으로 verifier 가 호출되어 context 가 분리되므로 self-review 안전장치는 유지됩니다.
290
290
  - 실제 파일 변경은 Codex/Gemini 의 경우 각 CLI 의 auto-edit 모드 (예: `codex exec --full-auto`) 를 통해 일어나며, Claude-side Edit/Write tool 을 거치지 않습니다.
291
- - **Executor worktree (자동 격리)**: prepare 단계에서 `okstra-ctl` 이 `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>-<run-seq>` 에 `git worktree` 를 생성하고, 브랜치 `<work-category-prefix>-<task-id-segment>-<run-seq>` 를 prepare 시점 `HEAD` 에서 분기합니다. Executor 의 Edit/Write/build/test/commit 모두 이 worktree 안에서 수행되며, verifier 들도 같은 worktree 를 읽어 동일한 diff 를 봅니다. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 되고 status 가 `skipped-in-worktree` / `skipped-not-git` 로 기록됩니다. 경로·브랜치 충돌은 `PrepareError` 로 즉시 실패시키며, run 종료 후 worktree 는 자동 삭제하지 않습니다(수동: `git worktree remove` → `git branch -D`).
291
+ - **Task worktree (모든 task-type 자동 격리)**: 모든 task-type 의 첫 번째 phase prepare 단계에서 `okstra-ctl` 이 `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` 에 `git worktree` 를 생성하고, 브랜치 `<work-category-prefix>-<task-id-segment>` 를 main worktree `HEAD` 에서 분기합니다. 같은 task-key 의 이후 phase 는 동일한 path/branch 를 재사용하므로 status 가 `reused` 로 기록됩니다 (run-prep 시점에 새 `git worktree add` 가 일어나지 않음). 모든 segment 의 `/`·`:` 등 특수문자는 `-` 로 정규화되며, `~/.okstra/worktrees/registry.json` 가 task-key → path/branch 매핑을 전역 관리합니다 (flock-guarded). Executor 의 Edit/Write/build/test/commit, verifier 의 read 는 모두 이 worktree 안에서 수행됩니다. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 되고 status 가 `skipped-in-worktree` / `skipped-not-git` 로 기록됩니다. 경로·브랜치 충돌은 `PrepareError` 로 즉시 실패시키며, run 종료 후 worktree 는 자동 삭제하지 않습니다 (수동: `git worktree remove` → `git branch -D` + registry 항목 삭제).
292
292
 
293
293
  예:
294
294
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.11.0",
3
+ "version": "0.12.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.11.0",
3
- "builtAt": "2026-05-12T17:46:50.784Z",
2
+ "package": "0.12.0",
3
+ "builtAt": "2026-05-12T18:39:56.088Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -107,18 +107,21 @@ Lead MUST dispatch Edit/Write-bearing work only through the `workerAgent` declar
107
107
 
108
108
  Executor is chosen at run-prep time via `--executor <claude|codex|gemini>` (or `OKSTRA_DEFAULT_EXECUTOR`, fallback `claude`); the model used by the executor is taken from the corresponding worker model flag (`--claude-model` / `--codex-model` / `--gemini-model`). For Codex/Gemini executors, the underlying file mutation happens inside the executor CLI's own auto-edit mode (e.g. `codex exec --full-auto`), not through Claude-side Edit/Write tools.
109
109
 
110
- #### Executor worktree (BLOCKING for `implementation`)
110
+ #### Task worktree (BLOCKING for every task-type)
111
111
 
112
- For `--task-type implementation` runs, `okstra-ctl` provisions a dedicated `git worktree` at run-prep time. Lead, the Executor, and every verifier MUST treat the provisioned worktree as the canonical working directory.
112
+ `okstra-ctl` provisions a dedicated `git worktree` per task-key at run-prep time of the **first** phase, and every subsequent phase of the same task-key reuses the same worktree on the same branch. Lead, the Executor, and every verifier MUST treat the provisioned worktree as the canonical working directory regardless of task-type.
113
113
 
114
- - Location: `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>-<run-seq>` (override `OKSTRA_HOME` only for tests).
115
- - Branch: `<work-category-prefix>-<task-id-segment>-<run-seq>` (e.g. `feat-dev-9436-001`, `fix-dev-7311-002`). Branched from `HEAD` of `project_root` at prep time; base SHA is recorded in `EXECUTOR_WORKTREE_BASE_REF`.
116
- - The worktree path, branch, base ref, and provisioning status (`created` | `skipped-in-worktree` | `skipped-not-git`) are exposed through the launch prompt's `## Executor Worktree` section and the implementation profile's worktree block.
117
- - **Skip conditions** (worktree provisioning is a no-op; executor uses `project_root` directly):
114
+ - One worktree per task-key — `requirements-discovery`, `error-analysis`, `implementation-planning`, and `implementation` all share the same path and branch so phase N inherits the working-tree state phase N-1 left behind.
115
+ - Location: `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` (override `OKSTRA_HOME` only for tests). All segments are sanitised `/`, `:`, and other special chars collapse to `-`.
116
+ - Branch: `<work-category-prefix>-<task-id-segment>` (e.g. `feat-dev-9436`, `fix-dev-7311`). Branched from `HEAD` of the repo's **main** worktree at the first phase's prep time; base SHA is recorded in `EXECUTOR_WORKTREE_BASE_REF`.
117
+ - A global registry at `~/.okstra/worktrees/registry.json` (flock-guarded) maps each task-key to its path + branch and prevents concurrent runs from colliding. Branch names are globally unique across task-keys on this machine.
118
+ - Sync dirs (`.project-docs`, `.scratch`, `graphify-out` by default; override with `OKSTRA_WORKTREE_SYNC_DIRS`) are symlinked from the **main worktree** so every task observes the same shared state irrespective of which checkout invoked okstra.
119
+ - The path, branch, base ref, and provisioning status (`created` | `reused` | `skipped-in-worktree` | `skipped-not-git`) are exposed through the launch prompt's `## Executor Worktree` section and the implementation profile's worktree block.
120
+ - **Skip conditions** (worktree provisioning is a no-op; task uses `project_root` directly):
118
121
  - `project_root` is already inside a non-main worktree (the run reuses the caller's worktree to avoid nesting).
119
122
  - `project_root` is not inside a git repository at all.
120
- - **Failure mode**: any other error during `git worktree add` (path collision, branch collision, detached-HEAD with no SHA) raises `PrepareError`. Re-run after manually removing the stale path/branch — the worktree is intentionally not garbage-collected.
121
- - **Lifecycle**: kept after the run for manual PR authoring, rollback verification, and follow-up `final-verification` runs. Manual cleanup: `git -C <project_root> worktree remove <path>` then `git -C <project_root> branch -D <branch>`.
123
+ - **Failure mode**: any other error during `git worktree add` (on-disk path collision not tracked by the registry, branch collision with a different task-key, detached-HEAD with no SHA) raises `PrepareError`. Re-run after manually removing the stale path/branch — the worktree is intentionally not garbage-collected.
124
+ - **Lifecycle**: kept after the run for follow-up phases, manual PR authoring, rollback verification, and `final-verification`. Manual cleanup: `git -C <main-worktree> worktree remove <path>` then `git -C <main-worktree> branch -D <branch>`; remove the corresponding registry entry by hand or via `okstra worktree release <task-key>` (when available).
122
125
 
123
126
  ## Phase 1: Task-bundle intake and required reading order
124
127
 
@@ -14,31 +14,7 @@ Invoke the `okstra` skill now. Read the manifests below for all task metadata, p
14
14
  - Phase advancement requires a new okstra invocation launched with `--task-type {{WORKFLOW_NEXT_RECOMMENDED_PHASE}}` after this run's final report is written and approved. The lead must not write source code, run builds/migrations/deployments, or otherwise produce artifacts of a different phase from inside this run.
15
15
  - See `Lifecycle Phase Boundaries` in the okstra skill (`agents/SKILL.md`) for the canonical rules and the phase-transition checklist.
16
16
 
17
- ## Team Creation Gate (BLOCKING)
18
-
19
- Before any `Agent` dispatch for workers, you MUST perform Phase 3 of the
20
- `okstra` skill (`agents/SKILL.md` → "Phase 3 — Team creation"). Skipping
21
- this gate silently degrades the run to in-process background dispatch and
22
- loses the Teams split-pane observability surface, even though worker
23
- outputs may still appear correct on disk.
24
-
25
- Required actions, in order, regardless of how many workers are selected
26
- for this run (roster comes from `resultContract.requiredWorkerRoles` in
27
- `task-manifest.json` — it may be 1, 2, 3, or more workers):
28
-
29
- 1. Invoke the `okstra-team-contract` skill and verify the selected worker
30
- roster against `task-manifest.json`'s `resultContract.requiredWorkerRoles`.
31
- 2. Call `TeamCreate(team_name: "okstra-{{TASK_KEY}}", description: ...)`.
32
- 3. Record the outcome in team-state under
33
- `teamCreate: { attempted: true, status: "ok" | "error", error?: <msg> }`
34
- BEFORE any `Agent(...)` worker dispatch.
35
- 4. Only after `teamCreate` is persisted may you dispatch workers — with
36
- `team_name` on success, or with `run_in_background: true` and no
37
- `team_name` ONLY when `teamCreate.status == "error"` was recorded.
38
-
39
- If the Agent tool rejects a dispatch with `"team must be created first"` /
40
- `"team을 먼저 생성하거나 team_name 없이 호출해야 합니다"`, the correct
41
- response is to go back to step 2 — NOT to strip `team_name` and retry.
17
+ {{TEAM_CREATION_GATE}}
42
18
 
43
19
  ## Project Root
44
20
 
@@ -70,9 +46,10 @@ response is to go back to step 2 — NOT to strip `team_name` and retry.
70
46
  - Branch: `{{EXECUTOR_WORKTREE_BRANCH}}`
71
47
  - Base ref: `{{EXECUTOR_WORKTREE_BASE_REF}}`
72
48
  - Note: `{{EXECUTOR_WORKTREE_NOTE}}`
73
- - For `implementation` runs with status `created`, the Executor MUST perform every Edit / Write / build / test command inside the working tree path above. Verifiers (Claude / Codex / Gemini) read from the SAME path so they observe the exact diff produced by the Executor.
74
- - The worktree is preserved after the run (no automatic cleanup). PR authoring, manual review, and rollback verification happen from the worktree branch.
75
- - For any other status (`skipped-non-implementation`, `skipped-in-worktree`, `skipped-not-git`), the run operates directly in `{{PROJECT_ROOT}}` and this section is informational only.
49
+ - For any task-type with status `created` or `reused`, every role (lead, executor, verifier) MUST anchor reads and writes to the working tree path above phase N inherits the working-tree state phase N-1 left behind. Verifiers read from the SAME path so they observe the exact diff produced by the Executor.
50
+ - Branch and path are globally reserved per task-key via `~/.okstra/worktrees/registry.json`; concurrent okstra runs on this machine cannot collide.
51
+ - The worktree is preserved after every run (no automatic cleanup) and is reused by every subsequent phase of the same task-key. Manual cleanup when fully done: `git worktree remove <path>` → `git branch -D <branch>` + remove the task-key entry from the registry.
52
+ - For status `skipped-in-worktree` or `skipped-not-git`, the run operates directly in `{{PROJECT_ROOT}}` and this section is informational only.
76
53
 
77
54
  ## Available MCP Servers
78
55
 
@@ -27,17 +27,17 @@
27
27
  - **CLI ack** — the user runs `okstra ... --task-type implementation --approved-plan <path> --approve`. The CLI invocation itself is modelled as the user's act of approval; the runtime (`okstra_ctl.run._apply_cli_approval`) flips the checkbox in the report file and appends an audit line `- 승인 일시 (CLI ack): <ISO8601> — recorded by \`okstra --approve\`` before the standard regex validation runs. Use this when running unattended or when you want a single command to both approve and launch the next phase.
28
28
  - The `--approve` flag is **only meaningful with `--task-type implementation` and `--approved-plan <path>`**. Passing it with any other task-type causes `PrepareError` (the runtime refuses to silently ignore approval signals). It is also a no-op if the file already carries a valid approval marker (idempotent — only an audit line is appended, the marker is not re-toggled).
29
29
  - the file's `Recommended option` and its bite-sized step list become the authoritative scope for this run; any deviation must be justified in the final report and routed back to a new `implementation-planning` run instead of being silently expanded.
30
- - Executor worktree (provisioned by `okstra-ctl` at run-prep time, fixed for this run):
31
- - Status: `{{EXECUTOR_WORKTREE_STATUS}}` (one of: `created` | `skipped-in-worktree` | `skipped-not-git`)
32
- - Working tree path: `{{EXECUTOR_WORKTREE_PATH}}` — when status is `created`, this is a fresh `git worktree` rooted under `~/.okstra/worktrees/<project>/<task-group>/<task-id>-<seq>`; when skipped, this is the caller's `project_root`.
33
- - Branch: `{{EXECUTOR_WORKTREE_BRANCH}}` — empty when status is `skipped-*`. The branch name encodes `<work-category-prefix>-<task-id-segment>-<seq>`.
34
- - Base ref: `{{EXECUTOR_WORKTREE_BASE_REF}}` — commit SHA the worktree was branched from; canonical `<base>` for every `git diff` / `git log` in this run.
30
+ - Task worktree (provisioned by `okstra-ctl` at the first phase's run-prep time, reused for every subsequent phase of this task-key):
31
+ - Status: `{{EXECUTOR_WORKTREE_STATUS}}` (one of: `created` | `reused` | `skipped-in-worktree` | `skipped-not-git`)
32
+ - Working tree path: `{{EXECUTOR_WORKTREE_PATH}}` — when status is `created` or `reused`, this is the task's `git worktree` rooted at `~/.okstra/worktrees/<project>/<task-group>/<task-id>/` (segments sanitised — `/` `:` → `-`). When skipped, this is the caller's `project_root`.
33
+ - Branch: `{{EXECUTOR_WORKTREE_BRANCH}}` — empty when status is `skipped-*`. The branch name encodes `<work-category-prefix>-<task-id-segment>` and is globally unique across task-keys via `~/.okstra/worktrees/registry.json`.
34
+ - Base ref: `{{EXECUTOR_WORKTREE_BASE_REF}}` — commit SHA the worktree was branched from at the first phase; canonical `<base>` for every `git diff` / `git log` in this run.
35
35
  - Provisioning note: `{{EXECUTOR_WORKTREE_NOTE}}`
36
- - **Executor behaviour**: when status is `created`, the Executor MUST run every Edit / Write / build / test / commit command with the working tree path above as cwd. Treat it as `project_root` for the duration of this run. Do NOT mutate the caller's original checkout. Do NOT `cd` out of the worktree to reach files; if a file outside the worktree is needed, the dependency is a planning gap — record it in `Out-of-plan edits` and continue.
36
+ - **Executor behaviour**: when status is `created` or `reused`, the Executor MUST run every Edit / Write / build / test / commit command with the working tree path above as cwd. Treat it as `project_root` for the duration of this run. Do NOT mutate the caller's original checkout. Do NOT `cd` out of the worktree to reach files; if a file outside the worktree is needed, the dependency is a planning gap — record it in `Out-of-plan edits` and continue.
37
37
  - **Verifier behaviour**: all three verifier roles read from the SAME working tree path so they observe the exact diff the Executor produced. Verifiers remain strictly read-only there.
38
- - **Lifecycle**: the worktree is kept after the run completes (no automatic cleanup). It is the canonical artefact for manual PR authoring, rollback verification, and follow-up `final-verification` runs. Cleanup, when desired, is manual: `git -C <project_root> worktree remove <path>` followed by `git -C <project_root> branch -D <branch>`.
38
+ - **Lifecycle**: the worktree is kept after the run completes (no automatic cleanup) and is reused by every subsequent phase of the same task-key. Cleanup, when the task is fully done, is manual: `git -C <main-worktree> worktree remove <path>` followed by `git -C <main-worktree> branch -D <branch>`, plus removing the task-key entry from `~/.okstra/worktrees/registry.json`.
39
39
  - **Skipped paths**: when status is `skipped-in-worktree` or `skipped-not-git`, the executor operates in `project_root` as before. Cite the status in the final report's metadata header so reviewers know which path was taken.
40
- - **Synced state directories (symlinks into the original `project_root`)**: at provision time `okstra-ctl` symlinks `.project-docs/`, `.scratch/`, and `graphify-out/` from the original `project_root` into the worktree (override via `OKSTRA_WORKTREE_SYNC_DIRS`; empty string disables). These are NOT independent copies — writes through them land in `project_root`. Inside this run the executor MUST confine writes under these paths to its own task scope (i.e. only `.project-docs/okstra/tasks/<this-task-id>/...`). Reading from elsewhere under the symlinks (other tasks, `graphify-out/GRAPH_REPORT.md`, `.scratch/` issues) is allowed and expected for context.
40
+ - **Synced state directories (symlinks into the MAIN worktree)**: at provision time `okstra-ctl` symlinks `.project-docs/`, `.scratch/`, and `graphify-out/` from the repo's **main worktree** into the task worktree (override via `OKSTRA_WORKTREE_SYNC_DIRS`; empty string disables). These are NOT independent copies — writes through them land in the main worktree. Inside this run the executor MUST confine writes under these paths to its own task scope (i.e. only `.project-docs/okstra/tasks/<this-task-id>/...`). Reading from elsewhere under the symlinks (other tasks, `graphify-out/GRAPH_REPORT.md`, `.scratch/` issues) is allowed and expected for context.
41
41
  - Pre-implementation context exploration (executor before first edit):
42
42
  - **Mandatory skill invocation — `tdd`**: BEFORE the first `Edit` or `Write` call, the executor MUST invoke the `tdd` skill via the `Skill` tool and follow its red-green-refactor loop for every code change in this run. This is a hard requirement, not a recommendation; skipping it is a `contract-violated` outcome. The skill governs HOW each step is executed (failing test first → minimal implementation → refactor); it does not override the approved plan's WHAT/file scope.
43
43
  - Order of operations per plan step: (1) write/extend the test that captures the step's acceptance criterion and confirm it fails for the right reason, (2) commit the failing test (`test(<scope>): ...`), (3) implement the minimum change to make it pass, (4) commit the implementation (`feat|fix(<scope>): ...`), (5) refactor without changing behaviour and commit separately if any cleanup is made (`refactor(<scope>): ...`). The failing-then-passing transition between steps (2) and (4) is the `TDD evidence` required by the final report.
@@ -1,14 +1,15 @@
1
1
  # Release Handoff Profile
2
2
 
3
3
  - Purpose: take an `accepted` final-verification verdict and turn it into a delivered commit and/or pull request, with explicit user selection at every mutating step
4
- - Required workers:
5
- - claude
6
- - report-writer
7
- {{INCLUDE:_common-contract.md}}
8
- - Team contract (phase-specific overrides):
9
- - `Claude lead` is the **executor of every git / gh command** in this phase. Workers never call mutating git commands themselves.
10
- - `Claude worker` (drafter) is read-only and produces **commit message candidate(s) and a PR body candidate** in markdown; the lead presents these to the user, accepts edits, and only then runs git.
11
- - Codex / Gemini workers are NOT part of this profile's roster (see `Required workers:` above); the shared contract's `Gemini worker must always be attempted` clause does not apply to release-handoff.
4
+ - **Execution model: single-lead, no worker dispatch.** This phase is a thin orchestrator over `git` / `gh`; it does NOT run team-mode, does NOT call `TeamCreate`, does NOT dispatch analysis or drafter sub-agents, and does NOT run convergence. The Claude lead performs every step inline (drafting commit/PR text, asking the user, running git, writing the final report) — see "Lead-only contract" below.
5
+ - Required workers: *(none — this profile intentionally has no `- Required workers:` block; the run is executed entirely by the Claude lead)*
6
+ - Lead-only contract (replaces the shared team contract for this phase):
7
+ - The Claude lead is the sole agent for this run. No `Agent(...)` worker dispatch, no `TeamCreate`, no parallel sub-agents, no convergence loop.
8
+ - The lead drafts the commit message and PR body **inline** by reading the run brief, the cited final-verification report, and `git diff <base>..HEAD --stat`. No drafter worker is dispatched.
9
+ - The lead authors the final-report file directly (no `Report writer worker` dispatch). The report still conforms to the standard `okstra-final-report.template.md` structure, including the `## 4.6 Release Handoff Deliverables` section.
10
+ - The shared anti-escalation rule from the common contract still applies: do not start any other lifecycle phase from inside this run.
11
+ - The shared "authority & permissions assumption" rule from the common contract still applies: assume the user holds every permission needed; do not block on hypothetical approvals.
12
+ - The shared "MCP read-only" rule still applies if the brief lists MCP servers, though most release-handoff runs do not use MCP.
12
13
  - Pre-handoff entry gate (mandatory — refuse to start if any item fails):
13
14
  - the task brief MUST cite the originating `final-verification` final-report path under `## Source Verification Report`. The lead opens that file and confirms section `## 2. Final Verdict` contains exactly the token `accepted`.
14
15
  - if the verdict is `conditional-accept`, `blocked`, or any other token (including ambiguous phrasing like "looks good"), the run MUST end immediately with status `blocked` and a routing recommendation back to `error-analysis` or `implementation-planning`. Do NOT prompt the user; do NOT run any git command.
@@ -28,18 +29,17 @@
28
29
  - `dev`
29
30
  - `직접 입력` (free-form branch name; lead validates the name exists on origin via `git ls-remote --heads origin <name>` and re-asks on failure)
30
31
  The chosen base MUST NOT equal the feature branch. If it does, re-ask.
31
- 3. **Commit message + PR body confirmation** — show the `Claude worker` drafter's output verbatim and capture one of:
32
- - `use as-is` — proceed with the drafter's text.
32
+ 3. **Commit message + PR body confirmation** — show the lead's inline draft verbatim and capture one of:
33
+ - `use as-is` — proceed with the drafted text.
33
34
  - `edit then proceed` — accept inline edits from the user, then proceed with the edited text.
34
35
  - `cancel` — end the run without executing any git command; record the cancellation in the final report.
35
- - Drafter worker contract (`Claude worker`):
36
- - reads the run brief, the cited final-verification report, and `git diff <base>..HEAD --stat` to ground its output in actual changes.
37
- - produces **two artifacts** in its worker result:
38
- 1. **Commit message candidate** — a single message in Conventional Commits style (`<type>(<scope>): <subject>` + optional body + optional footer). When `commit + PR` will be opened against a `release-please`-managed repo, the type MUST match a configured changelog section (`feat` / `fix` / `perf` / `revert` / `deps` / `docs` / `refactor` / `build` / `ci` / `chore` / `test`).
39
- 2. **PR body candidate** — markdown with sections `## Summary`, `## Changes`, `## Test plan`, `## Linked issues` (omit a section only if it is genuinely empty).
40
- - is strictly **read-only**: MUST NOT run any git mutating command, MUST NOT call `gh pr create`, MUST NOT call `git push`.
41
- - if the drafter cannot produce output (no diff exists, repo is bare, etc.), it returns terminal status `not-run` with a reason; the lead falls back to a manually-authored message captured directly from the user.
42
- - Allowed actions during the run (Claude lead only — workers stay read-only):
36
+ - Inline drafting rules (Claude lead):
37
+ - read the run brief, the cited final-verification report, and `git diff <base>..HEAD --stat` to ground the drafted text in actual changes.
38
+ - produce **two artifacts** before showing them to the user:
39
+ 1. **Commit message** — a single message in Conventional Commits style (`<type>(<scope>): <subject>` + optional body + optional footer). When `commit + PR` will be opened against a `release-please`-managed repo, the type MUST match a configured changelog section (`feat` / `fix` / `perf` / `revert` / `deps` / `docs` / `refactor` / `build` / `ci` / `chore` / `test`).
40
+ 2. **PR body** — markdown with sections `## Summary`, `## Changes`, `## Test plan`, `## Linked issues` (omit a section only if it is genuinely empty).
41
+ - if the diff is empty or no commit can be produced (working tree already matches the base), record "no staged changes; commit skipped" in the final report and skip `git commit` while still proceeding to the PR step if requested.
42
+ - Allowed actions during the run (Claude lead only):
43
43
  - read-only inspection: `git status`, `git status --short`, `git diff`, `git log`, `git rev-parse`, `git ls-remote --heads origin <name>`, `gh pr list --head <branch>`, `gh pr view <url>`.
44
44
  - local commit: `git add -- <path>...` (prefer explicit file paths over `git add -A` / `git add .`), `git commit -m "<message>"`. Re-use the user-confirmed message exactly.
45
45
  - feature-branch push (only when the user picked `commit + PR`): `git push -u origin <current-branch>`. The pushed ref MUST be the feature branch — never the chosen base branch.
@@ -59,7 +59,7 @@
59
59
  - source-code edits, refactors, or any modification to files outside the run's own artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`). The diff being shipped MUST be exactly what the prior `implementation` run produced; release-handoff packages it, it does not re-author it.
60
60
  - executing any mutating command the user did NOT select. Examples: opening a PR when the user picked `commit only`; running `git commit` when the user picked `skip`; switching the PR base branch silently after the user already chose one.
61
61
  - retrying a failed git / gh command with weaker safety flags. If `git push` fails with non-fast-forward, the lead MUST stop, explain the failure to the user, and ask for instructions — it MUST NOT add `--force`.
62
- - dispatching parallel sub-agents beyond the required worker roster.
62
+ - `TeamCreate`, `Agent(...)` dispatch of any kind, or any other parallel sub-agent fan-out. This phase runs entirely under the Claude lead.
63
63
  - silently treating an unrecognised user reply as one of the menu options. If the user's answer does not match a presented choice, re-ask the question verbatim.
64
64
  - Required deliverable shape (final report, in addition to the standard sections):
65
65
  - **Source Verification Report**: relative path of the originating `final-verification` final-report file plus the literal quoted `## 2. Final Verdict` line that read `accepted`.
@@ -67,7 +67,7 @@
67
67
  - **User Selections**: a block recording each prompt and the user's verbatim answer.
68
68
  - Q1 action: `commit only` | `commit + PR` | `skip`.
69
69
  - Q2 PR base (if applicable): the chosen branch and how it was selected (menu pick vs free-form input).
70
- - Q3 message/body: `use as-is` | `edit then proceed` (with a diff between the drafter's text and the final text) | `cancel`.
70
+ - Q3 message/body: `use as-is` | `edit then proceed` (with a diff between the lead's draft and the final text) | `cancel`.
71
71
  - **Executed Commands**: every git / gh command the lead actually ran, with its exit code and a one-line stdout/stderr summary. Read-only inspection commands MAY be summarised; mutating commands MUST be listed verbatim.
72
72
  - **Commit List**: each commit SHA (short and full), its subject line, and the files it touched. If no commit was produced (idempotent no-op), state `- No commit was produced (working tree had no staged changes).`
73
73
  - **Pull Request Outcome**: one of
@@ -76,7 +76,7 @@
76
76
  - `- PR reused: <url>` when an existing PR was found via `gh pr list`
77
77
  - `- PR creation skipped: <reason>` for any user-driven cancellation
78
78
  - **Routing recommendation**: explicit `done` token, since release-handoff is the terminal lifecycle phase. If the run ended in `skip` or `cancel`, the recommendation MUST also state whether re-entry into release-handoff is appropriate.
79
- - Self-review pass before finalising the report (`Claude lead` runs this; do not delegate to a generic subagent):
79
+ - Self-review pass before finalising the report (`Claude lead` runs this):
80
80
  1. **Entry-gate audit** — section 2 cites the originating final-verification report path and the literal `accepted` verdict line. If either is missing, the run is invalid and MUST be re-routed to `final-verification`.
81
81
  2. **User-selection traceability** — every executed mutating command maps to a user selection captured in the report. Any mutating command without a corresponding user answer is a contract violation.
82
82
  3. **Forbidden-action audit** — scan the run's session transcripts (`git`, `gh` invocations) for every entry in the Forbidden actions list above. Any occurrence means the run has crossed into unsafe territory and MUST be flagged as `contract-violated`.
@@ -86,4 +86,4 @@
86
86
  - re-litigating the final-verification verdict — release-handoff trusts the cited `accepted` verdict and does not reopen acceptance checks.
87
87
  - opening additional PRs, releases, or deployments beyond the single PR the user chose to create.
88
88
  - merging the PR. Merging is a separate, manual step performed by the user (or by repo automation) after release-handoff ends; the lead MUST NOT call `gh pr merge`.
89
- - escalating beyond the menu choices on user phrasing — every mutating action requires an explicit menu selection (the shared anti-escalation rule applies, with this phase-specific tightening).
89
+ - escalating beyond the menu choices on user phrasing — every mutating action requires an explicit menu selection.
@@ -986,7 +986,55 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
986
986
  else "- `Gemini worker` is not selected for this run, so no Gemini attempt is required."
987
987
  )
988
988
 
989
+ # Team creation gate block: phase-conditional.
990
+ # - release-handoff (and any other phase that renders with no workers
991
+ # selected) is single-lead and MUST NOT call `TeamCreate`. Emit a
992
+ # short notice instead of the BLOCKING gate.
993
+ # - All other phases keep the full team-creation contract.
994
+ task_type = ctx.get("ANALYSIS_TYPE", "")
995
+ if task_type == "release-handoff" or not selected:
996
+ team_creation_gate_block = (
997
+ "## Single-Lead Phase (no team creation)\n"
998
+ "\n"
999
+ "This run is single-lead. There is no worker roster, no\n"
1000
+ "`TeamCreate` call, no `Agent(...)` worker dispatch, and no\n"
1001
+ "convergence loop. The Claude lead performs every step inline\n"
1002
+ "(reading inputs, drafting commit / PR text when applicable,\n"
1003
+ "asking the user, running git / gh, and writing the final\n"
1004
+ "report). Do NOT call `TeamCreate` or dispatch any sub-agent\n"
1005
+ "from this run — that would be a contract violation."
1006
+ )
1007
+ else:
1008
+ team_creation_gate_block = (
1009
+ "## Team Creation Gate (BLOCKING)\n"
1010
+ "\n"
1011
+ "Before any `Agent` dispatch for workers, you MUST perform Phase 3 of the\n"
1012
+ "`okstra` skill (`agents/SKILL.md` → \"Phase 3 — Team creation\"). Skipping\n"
1013
+ "this gate silently degrades the run to in-process background dispatch and\n"
1014
+ "loses the Teams split-pane observability surface, even though worker\n"
1015
+ "outputs may still appear correct on disk.\n"
1016
+ "\n"
1017
+ "Required actions, in order, regardless of how many workers are selected\n"
1018
+ "for this run (roster comes from `resultContract.requiredWorkerRoles` in\n"
1019
+ "`task-manifest.json` — it may be 1, 2, 3, or more workers):\n"
1020
+ "\n"
1021
+ "1. Invoke the `okstra-team-contract` skill and verify the selected worker\n"
1022
+ " roster against `task-manifest.json`'s `resultContract.requiredWorkerRoles`.\n"
1023
+ f"2. Call `TeamCreate(team_name: \"okstra-{ctx.get('TASK_KEY', '')}\", description: ...)`.\n"
1024
+ "3. Record the outcome in team-state under\n"
1025
+ " `teamCreate: { attempted: true, status: \"ok\" | \"error\", error?: <msg> }`\n"
1026
+ " BEFORE any `Agent(...)` worker dispatch.\n"
1027
+ "4. Only after `teamCreate` is persisted may you dispatch workers — with\n"
1028
+ " `team_name` on success, or with `run_in_background: true` and no\n"
1029
+ " `team_name` ONLY when `teamCreate.status == \"error\"` was recorded.\n"
1030
+ "\n"
1031
+ "If the Agent tool rejects a dispatch with `\"team must be created first\"` /\n"
1032
+ "`\"team을 먼저 생성하거나 team_name 없이 호출해야 합니다\"`, the correct\n"
1033
+ "response is to go back to step 2 — NOT to strip `team_name` and retry."
1034
+ )
1035
+
989
1036
  mapping = {
1037
+ "{{TEAM_CREATION_GATE}}": team_creation_gate_block,
990
1038
  "{{PROJECT_ID}}": ctx.get("PROJECT_ID", ""),
991
1039
  "{{TASK_GROUP}}": ctx.get("TASK_GROUP", ""),
992
1040
  "{{TASK_ID}}": ctx.get("TASK_ID", ""),
@@ -58,7 +58,7 @@ from .session import (
58
58
  )
59
59
  from .workers import normalize_workers, resolve_profile_workers
60
60
  from .workflow import compute_workflow_state
61
- from .worktree import provision_implementation_worktree
61
+ from .worktree import provision_task_worktree
62
62
 
63
63
  APPROVED_PLAN_PATTERN = re.compile(
64
64
  r"^[ \t]*(?:[-*+][ \t]+)?(APPROVED([ \t]|:|$)|\[x\][ \t]*Approved|"
@@ -457,10 +457,17 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
457
457
  raise PrepareError(f"project.json upsert failed for {project_root}: {exc}") from exc
458
458
 
459
459
  # ---- workers resolution ----
460
- profile_workers_csv = ",".join(resolve_profile_workers(profile_file))
461
- workers = normalize_workers(inp.workers_override or profile_workers_csv)
462
- if not workers:
463
- raise PrepareError(f"no workers resolved for profile: {inp.task_type}")
460
+ # release-handoff is intentionally single-lead (no worker dispatch, no
461
+ # TeamCreate, no convergence). The profile has no `- Required workers:`
462
+ # block; force an empty roster regardless of any user override so the
463
+ # rendered task bundle stays consistent with the profile contract.
464
+ if inp.task_type == "release-handoff":
465
+ workers: list[str] = []
466
+ else:
467
+ profile_workers_csv = ",".join(resolve_profile_workers(profile_file))
468
+ workers = normalize_workers(inp.workers_override or profile_workers_csv)
469
+ if not workers:
470
+ raise PrepareError(f"no workers resolved for profile: {inp.task_type}")
464
471
  selected_reviewers = ",".join(workers)
465
472
 
466
473
  # ---- model assignments ----
@@ -517,21 +524,22 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
517
524
  task_type=inp.task_type, run_seq_override=run_seq_override,
518
525
  )
519
526
 
520
- # ---- executor worktree provisioning (implementation phase only) ----
521
- # The reports-seq is reused as the worktree-seq so the on-disk worktree
522
- # path is colocated with the artefacts produced by this run.
527
+ # ---- task worktree provisioning (every phase, every task-type) ----
528
+ # One worktree per task-key: requirements-discovery, error-analysis,
529
+ # implementation-planning and implementation phases of the same task
530
+ # all share this directory and branch. The global registry handles
531
+ # reservation across concurrent runs.
523
532
  try:
524
- worktree = provision_implementation_worktree(
533
+ worktree = provision_task_worktree(
525
534
  task_type=inp.task_type,
526
535
  project_root=project_root,
527
536
  project_id=inp.project_id,
528
537
  task_group_segment=ctx["TASK_GROUP_SEGMENT"],
529
538
  task_id_segment=ctx["TASK_ID_SEGMENT"],
530
- run_seq=int(ctx["RUN_REPORTS_SEQ"]),
531
539
  work_category=inp.work_category,
532
540
  )
533
541
  except RuntimeError as exc:
534
- raise PrepareError(f"executor worktree provisioning failed: {exc}") from exc
542
+ raise PrepareError(f"task worktree provisioning failed: {exc}") from exc
535
543
 
536
544
  ctx.update({
537
545
  "EXECUTOR_WORKTREE_PATH": worktree.path,
@@ -127,11 +127,11 @@ PHASE_RULES: dict[str, dict[str, str]] = {
127
127
  " - entering this phase only when the cited final-verification report's verdict is exactly `accepted`\n"
128
128
  " - asking the user (via `AskUserQuestion` / interactive prompt) which delivery action to take: `commit only`, `commit + PR`, or `skip` (end the run)\n"
129
129
  " - asking the user to pick a PR base branch from `staging` | `preprod` | `prod` | `main` | `dev` | a user-supplied branch name\n"
130
- " - dispatching the `Claude worker` (drafter) to produce candidate commit message(s) and PR body in markdown; the lead reviews and offers them to the user before any git command runs\n"
130
+ " - drafting commit message(s) and PR body **inline as the Claude lead** (no drafter worker, no `Agent` dispatch); the lead reviews its own draft with the user before any git command runs\n"
131
131
  " - local git operations: `git status`, `git diff`, `git log`, `git add`, `git commit -m`\n"
132
132
  " - pushing the current feature branch to its origin remote via `git push -u origin <current-branch>` (the feature branch only — NEVER the base branch)\n"
133
133
  " - creating a pull request via `gh pr create --base <chosen-base> --head <current-branch>`; if a PR with the same head already exists, surface its URL and skip creation\n"
134
- " - recording the executed actions, commit SHAs, PR URL, and user selections in the final report"
134
+ " - the lead writes the final report directly (no `Report writer worker` dispatch); the report still conforms to the standard final-report template"
135
135
  ),
136
136
  "forbidden": (
137
137
  " - entering this phase when the cited final-verification verdict is `conditional-accept` or `blocked`, or when no final-verification report is cited\n"
@@ -141,7 +141,7 @@ PHASE_RULES: dict[str, dict[str, str]] = {
141
141
  " - bypassing git hooks (`--no-verify`, `-n`), bypassing GPG signing, or otherwise disabling repo-configured safeguards\n"
142
142
  " - release-publishing commands: `gh release`, `npm publish`, `cargo publish`, `pip publish`, `docker push`, `terraform apply`, `kubectl apply` against non-local clusters\n"
143
143
  " - executing any command the user did NOT select (e.g. if the user picked `commit only`, opening a PR is forbidden; if the user picked `skip`, the run ends without git commands)\n"
144
- " - dispatching parallel sub-agents beyond the required worker roster (`Claude worker` drafter + `Report writer worker`)\n"
144
+ " - `TeamCreate`, `Agent(...)` worker dispatch, parallel sub-agent fan-out, or any team-mode orchestration this phase runs single-lead\n"
145
145
  " - silently retrying a failed git/gh command with weaker flags (e.g. retrying `git push` with `--force` after a non-fast-forward rejection)"
146
146
  ),
147
147
  },
@@ -1,19 +1,34 @@
1
- """Implementation-phase git worktree provisioning.
1
+ """Per-task git worktree provisioning.
2
2
 
3
- Implementation runs operate on an isolated git worktree rooted under
4
- `~/.okstra/worktrees/<project_id>/<task_group_segment>/<task_id_segment>-<run_seq>`.
5
- The executor mutates files there; verifiers read from the same path.
6
- The worktree is always kept after the run for inspection, manual PR
7
- authoring, and rollback verification.
3
+ Every okstra task regardless of task-type/phase runs inside an
4
+ isolated git worktree rooted at
5
+ `~/.okstra/worktrees/<project_id>/<task_group>/<task_id>/`. The same
6
+ worktree is reused across all phases of one task-key (requirements-
7
+ discovery error-analysis → implementation-planning → implementation),
8
+ so phase N picks up exactly the working-tree state phase N-1 left
9
+ behind.
8
10
 
9
- Pre-conditions handled here:
10
- - Skip non-`implementation` task-types entirely.
11
- - Skip when `project_root` itself already sits inside a non-main git
12
- worktree (the run reuses the caller's working tree).
13
- - Refuse to clobber an existing path or branch — raise PrepareError.
11
+ A global registry (`worktree_registry.py`) maps task-keys to the
12
+ on-disk path + branch and serialises reservations, so two concurrent
13
+ okstra runs cannot collide on the same path or branch name.
14
14
 
15
- Side effects: `git worktree add -b <branch> <path> <base_ref>` is invoked
16
- in `project_root`. The function does NOT chdir.
15
+ Pre-conditions handled here:
16
+ - Skip when `project_root` is not a git repo (degrade gracefully).
17
+ - Skip when `project_root` itself is already a non-main worktree
18
+ (caller's tree is already an isolated workspace; reuse it).
19
+ - Re-entry of the same task-key returns the existing worktree.
20
+ - Branch / path collisions across task-keys raise PrepareError-like
21
+ RuntimeError.
22
+
23
+ Side effects:
24
+ - `git worktree add -b <branch> <path> <base_ref>` invoked in the
25
+ main worktree of `project_root` (NOT `project_root` itself when it
26
+ is also a worktree — base must be the main checkout).
27
+ - Per-task sync dirs (.project-docs, .scratch, graphify-out by
28
+ default) symlinked from the **main worktree** into the new
29
+ worktree so every task sees the same shared state, irrespective of
30
+ which worktree the caller invoked okstra from.
31
+ - The function does NOT chdir.
17
32
  """
18
33
  from __future__ import annotations
19
34
 
@@ -23,17 +38,20 @@ from dataclasses import dataclass
23
38
  from pathlib import Path
24
39
  from typing import Optional
25
40
 
41
+ from .ids import _safe_fs_segment
42
+ from . import worktree_registry
43
+
26
44
 
27
45
  OKSTRA_WORKTREES_RELATIVE = Path(".okstra/worktrees")
28
46
 
29
47
 
30
48
  # Project-root directories that hold okstra task state, ignored by git, or
31
49
  # otherwise required for the executor to operate but NOT carried across by
32
- # `git worktree add`. Each is symlinked from project_root into the new
33
- # worktree at provision time. Symlinks (not copies) so the executor sees
34
- # the live state and disk/CPU cost stays near zero; the trade-off is that
35
- # any write through the link reaches the original project_root, which is
36
- # acceptable because the executor only writes inside its own task-scoped
50
+ # `git worktree add`. Each is symlinked from the MAIN worktree into the new
51
+ # worktree at provision time. Symlinks (not copies) so every task sees the
52
+ # live shared state and disk/CPU cost stays near zero; the trade-off is
53
+ # that any write through the link reaches the main worktree, which is
54
+ # acceptable because okstra only writes inside its own task-scoped
37
55
  # subdirectory (e.g. `.project-docs/okstra/tasks/<task-id>/runs/...`).
38
56
  #
39
57
  # Override via the `OKSTRA_WORKTREE_SYNC_DIRS` env var: a colon-separated
@@ -62,32 +80,45 @@ _WORK_CATEGORY_PREFIX = {
62
80
 
63
81
  @dataclass
64
82
  class WorktreeProvision:
65
- """Result of `provision_implementation_worktree`.
83
+ """Result of `provision_task_worktree`.
66
84
 
67
85
  status:
68
86
  - "created": fresh worktree at `path` on `branch`
69
- - "skipped-non-implementation": task-type was not `implementation`
70
- - "skipped-in-worktree": project_root is already inside a non-main
87
+ - "reused": registry already had this task-key; same path/branch
88
+ returned and no new `git worktree add` was executed
89
+ - "skipped-in-worktree": project_root is itself a non-main
71
90
  worktree; the run reuses `project_root` and no new worktree is
72
- materialised
91
+ materialised (registry NOT updated — that caller is already
92
+ isolated by virtue of its own worktree)
73
93
  - "skipped-not-git": project_root has no `.git` (worktree path
74
94
  cannot be provisioned; degrade gracefully)
75
95
  """
76
96
  status: str
77
- path: str = "" # absolute path of the executor worktree (or project_root when reused)
97
+ path: str = "" # absolute path of the task worktree (or project_root when reused)
78
98
  branch: str = "" # branch checked out in the worktree (empty when reused / not-git)
79
99
  base_ref: str = "" # commit SHA the worktree was branched from (empty when not created)
80
100
  note: str = "" # human-readable explanation, surfaced in team-state / manifests
81
101
 
82
102
 
103
+ def _safe_segment(value: str) -> str:
104
+ """Sanitise a single path/branch segment.
105
+
106
+ Forbidden chars (`/`, `:`, spaces, anything outside `[a-z0-9-]`)
107
+ are collapsed to `-`. Empty result becomes `_` so we never create
108
+ an empty path component. Delegates to the canonical slugifier in
109
+ `ids.py` to stay in lock-step with run-id / manifest segmentation.
110
+ """
111
+ return _safe_fs_segment(value)
112
+
113
+
83
114
  def _work_category_prefix(work_category: str) -> str:
84
115
  key = (work_category or "").strip().lower()
85
116
  return _WORK_CATEGORY_PREFIX.get(key, "task")
86
117
 
87
118
 
88
- def _git(project_root: Path, *args: str) -> subprocess.CompletedProcess:
119
+ def _git(cwd: Path, *args: str) -> subprocess.CompletedProcess:
89
120
  return subprocess.run(
90
- ["git", "-C", str(project_root), *args],
121
+ ["git", "-C", str(cwd), *args],
91
122
  capture_output=True, text=True, check=False,
92
123
  )
93
124
 
@@ -101,7 +132,6 @@ def _is_inside_non_main_worktree(project_root: Path) -> bool:
101
132
  per_tree = _git(project_root, "rev-parse", "--git-dir")
102
133
  if common.returncode != 0 or per_tree.returncode != 0:
103
134
  return False
104
- # Both paths can be relative to project_root; resolve before compare.
105
135
  common_abs = (project_root / common.stdout.strip()).resolve()
106
136
  per_tree_abs = (project_root / per_tree.stdout.strip()).resolve()
107
137
  return common_abs != per_tree_abs
@@ -117,13 +147,31 @@ def _branch_exists(project_root: Path, branch: str) -> bool:
117
147
  return res.returncode == 0
118
148
 
119
149
 
120
- def _head_sha(project_root: Path) -> str:
121
- res = _git(project_root, "rev-parse", "HEAD")
150
+ def _head_sha(cwd: Path) -> str:
151
+ res = _git(cwd, "rev-parse", "HEAD")
122
152
  if res.returncode != 0:
123
153
  return ""
124
154
  return res.stdout.strip()
125
155
 
126
156
 
157
+ def _main_worktree_path(project_root: Path) -> Path:
158
+ """Locate the repository's MAIN worktree (the original checkout).
159
+
160
+ `git worktree list --porcelain` lists worktrees in a stable order
161
+ where the first `worktree <path>` block is the main checkout.
162
+ Falls back to `project_root` if parsing fails — caller still gets
163
+ a working path, sync-dir links just point at the caller's tree
164
+ (the prior behaviour).
165
+ """
166
+ res = _git(project_root, "worktree", "list", "--porcelain")
167
+ if res.returncode != 0:
168
+ return project_root
169
+ for line in res.stdout.splitlines():
170
+ if line.startswith("worktree "):
171
+ return Path(line[len("worktree "):].strip())
172
+ return project_root
173
+
174
+
127
175
  def _resolve_sync_dirs() -> tuple[str, ...]:
128
176
  """Return the list of project-root-relative dirs to symlink into the
129
177
  new worktree. Reads `OKSTRA_WORKTREE_SYNC_DIRS` if set (colon-separated,
@@ -138,11 +186,12 @@ def _resolve_sync_dirs() -> tuple[str, ...]:
138
186
  return tuple(part for part in (p.strip() for p in raw.split(":")) if part)
139
187
 
140
188
 
141
- def _link_sync_dirs(project_root: Path, worktree_path: Path) -> list[str]:
142
- """Symlink each configured project-root dir into the new worktree.
189
+ def _link_sync_dirs(source_root: Path, worktree_path: Path) -> list[str]:
190
+ """Symlink each configured dir from `source_root` (the MAIN
191
+ worktree) into the new worktree.
143
192
 
144
193
  Skip rules:
145
- - Source missing in project_root → silently skipped.
194
+ - Source missing in `source_root` → silently skipped.
146
195
  - Target path already exists in worktree (e.g. tracked content
147
196
  checked out by `git worktree add`) → skipped to avoid clobbering
148
197
  version-controlled files.
@@ -153,7 +202,7 @@ def _link_sync_dirs(project_root: Path, worktree_path: Path) -> list[str]:
153
202
  """
154
203
  notes: list[str] = []
155
204
  for rel in _resolve_sync_dirs():
156
- src = (project_root / rel).resolve()
205
+ src = (source_root / rel).resolve()
157
206
  if not src.exists():
158
207
  continue
159
208
  dst = worktree_path / rel
@@ -170,17 +219,20 @@ def compute_worktree_path(
170
219
  project_id: str,
171
220
  task_group_segment: str,
172
221
  task_id_segment: str,
173
- run_seq: int,
174
222
  ) -> Path:
175
- """Pure path computation. Mirrors `okstra_root` location convention.
223
+ """Pure path computation. One worktree dir per task-key.
176
224
 
177
- Uses `OKSTRA_HOME` when set (test hook), else `~/.okstra`.
225
+ Uses `OKSTRA_HOME` when set (test hook), else `~/.okstra`. Note
226
+ there is NO run-seq segment — every phase of the same task-key
227
+ shares this dir.
178
228
  """
179
229
  okstra_home = os.environ.get("OKSTRA_HOME", "").strip()
180
230
  base = Path(okstra_home) if okstra_home else (Path.home() / ".okstra")
181
231
  return (
182
- base / "worktrees" / project_id
183
- / task_group_segment / f"{task_id_segment}-{int(run_seq):03d}"
232
+ base / "worktrees"
233
+ / _safe_segment(project_id)
234
+ / _safe_segment(task_group_segment)
235
+ / _safe_segment(task_id_segment)
184
236
  )
185
237
 
186
238
 
@@ -188,46 +240,40 @@ def compute_branch_name(
188
240
  *,
189
241
  work_category: str,
190
242
  task_id_segment: str,
191
- run_seq: int,
192
243
  ) -> str:
193
- return f"{_work_category_prefix(work_category)}-{task_id_segment}-{int(run_seq):03d}"
244
+ """One branch per task-key. No run-seq — phases share the branch."""
245
+ return f"{_work_category_prefix(work_category)}-{_safe_segment(task_id_segment)}"
194
246
 
195
247
 
196
- def provision_implementation_worktree(
248
+ def provision_task_worktree(
197
249
  *,
198
250
  task_type: str,
199
251
  project_root: Path,
200
252
  project_id: str,
201
253
  task_group_segment: str,
202
254
  task_id_segment: str,
203
- run_seq: int,
204
255
  work_category: str,
205
256
  ) -> WorktreeProvision:
206
- """Materialise (or skip) the executor worktree for this run.
257
+ """Materialise (or reuse) the task worktree for this run.
207
258
 
208
- The caller passes the same `run_seq` used by the reports/manifests
209
- artefacts so the worktree directory is colocated by sequence number.
259
+ First phase of a task-key creates the worktree on a new branch.
260
+ Subsequent phases of the same task-key look up the registry and
261
+ return the existing path + branch unchanged.
210
262
 
211
263
  Raises:
212
- PrepareError-like RuntimeError when worktree creation fails
213
- (path clash, branch clash, `git worktree add` non-zero). The
214
- caller (`run.py`) catches and re-raises as PrepareError to keep
215
- a single error surface.
264
+ RuntimeError when worktree creation fails (path clash on disk
265
+ that the registry does not know about, branch clash with a
266
+ different task-key, `git worktree add` non-zero). The caller
267
+ (`run.py`) catches and re-raises as PrepareError to keep a
268
+ single error surface.
216
269
  """
217
- if task_type != "implementation":
218
- return WorktreeProvision(
219
- status="skipped-non-implementation",
220
- path=str(project_root),
221
- note="worktree provisioning skipped: task-type is not 'implementation'",
222
- )
223
-
224
270
  if not _is_git_repo(project_root):
225
271
  return WorktreeProvision(
226
272
  status="skipped-not-git",
227
273
  path=str(project_root),
228
274
  note=(
229
275
  "worktree provisioning skipped: project_root is not inside a git "
230
- "repository; executor will operate directly on project_root"
276
+ "repository; task will operate directly on project_root"
231
277
  ),
232
278
  )
233
279
 
@@ -237,44 +283,62 @@ def provision_implementation_worktree(
237
283
  path=str(project_root),
238
284
  note=(
239
285
  "worktree provisioning skipped: project_root is already inside a "
240
- "non-main git worktree; executor reuses the caller's worktree"
286
+ "non-main git worktree; task reuses the caller's worktree"
287
+ ),
288
+ )
289
+
290
+ safe_project = _safe_segment(project_id)
291
+ safe_group = _safe_segment(task_group_segment)
292
+ safe_task = _safe_segment(task_id_segment)
293
+
294
+ # Registry lookup first — same task-key across phases must reuse.
295
+ existing = worktree_registry.lookup(safe_project, safe_group, safe_task)
296
+ if existing is not None and existing.status == "active":
297
+ worktree_registry.touch_phase(safe_project, safe_group, safe_task, task_type)
298
+ return WorktreeProvision(
299
+ status="reused",
300
+ path=existing.worktree_path,
301
+ branch=existing.branch,
302
+ base_ref=existing.base_ref,
303
+ note=(
304
+ f"task worktree reused at {existing.worktree_path} on branch "
305
+ f"{existing.branch} (base {existing.base_ref[:12]}); phase {task_type}"
241
306
  ),
242
307
  )
243
308
 
244
309
  worktree_path = compute_worktree_path(
245
- project_id=project_id,
246
- task_group_segment=task_group_segment,
247
- task_id_segment=task_id_segment,
248
- run_seq=run_seq,
310
+ project_id=safe_project,
311
+ task_group_segment=safe_group,
312
+ task_id_segment=safe_task,
249
313
  )
250
314
  branch = compute_branch_name(
251
315
  work_category=work_category,
252
- task_id_segment=task_id_segment,
253
- run_seq=run_seq,
316
+ task_id_segment=safe_task,
254
317
  )
255
318
 
256
319
  if worktree_path.exists():
257
320
  raise RuntimeError(
258
- f"executor worktree path already exists: {worktree_path}. "
259
- "Remove it with `git worktree remove <path>` (or `rm -rf` if it is "
260
- "not a registered worktree) before retrying this implementation run."
321
+ f"task worktree path already exists but is not in the registry: "
322
+ f"{worktree_path}. Remove it with `git worktree remove <path>` "
323
+ "(or `rm -rf` if it is not a registered worktree) before retrying."
261
324
  )
262
325
  if _branch_exists(project_root, branch):
263
326
  raise RuntimeError(
264
- f"executor worktree branch already exists: {branch}. "
265
- "Delete it (`git branch -D <branch>`) or bump OKSTRA_RUN_SEQ_OVERRIDE "
266
- "before retrying."
327
+ f"task worktree branch already exists: {branch}. "
328
+ "Delete it (`git branch -D <branch>`) or choose a different "
329
+ "work-category before retrying."
267
330
  )
268
331
 
269
- base_ref = _head_sha(project_root)
332
+ main_root = _main_worktree_path(project_root)
333
+ base_ref = _head_sha(main_root)
270
334
  if not base_ref:
271
335
  raise RuntimeError(
272
- "could not resolve HEAD sha in project_root; cannot create worktree"
336
+ "could not resolve HEAD sha in main worktree; cannot create task worktree"
273
337
  )
274
338
 
275
339
  worktree_path.parent.mkdir(parents=True, exist_ok=True)
276
340
  res = _git(
277
- project_root,
341
+ main_root,
278
342
  "worktree", "add", "-b", branch, str(worktree_path), base_ref,
279
343
  )
280
344
  if res.returncode != 0:
@@ -283,16 +347,35 @@ def provision_implementation_worktree(
283
347
  f"{(res.stderr or res.stdout).strip()}"
284
348
  )
285
349
 
286
- linked = _link_sync_dirs(project_root, worktree_path)
350
+ # Sync dirs sourced from the MAIN worktree so every task sees the
351
+ # same shared state regardless of which checkout invoked okstra.
352
+ linked = _link_sync_dirs(main_root, worktree_path)
287
353
  linked_suffix = f"; linked {', '.join(linked)}" if linked else ""
288
354
 
355
+ try:
356
+ worktree_registry.reserve(
357
+ project_id=safe_project,
358
+ task_group=safe_group,
359
+ task_id=safe_task,
360
+ worktree_path=str(worktree_path),
361
+ branch=branch,
362
+ base_ref=base_ref,
363
+ phase=task_type,
364
+ )
365
+ except RuntimeError:
366
+ # Roll back the on-disk worktree so the next attempt is not
367
+ # blocked by the lingering directory / branch.
368
+ _git(main_root, "worktree", "remove", "--force", str(worktree_path))
369
+ _git(main_root, "branch", "-D", branch)
370
+ raise
371
+
289
372
  return WorktreeProvision(
290
373
  status="created",
291
374
  path=str(worktree_path),
292
375
  branch=branch,
293
376
  base_ref=base_ref,
294
377
  note=(
295
- f"executor worktree created at {worktree_path} on branch {branch} "
296
- f"(base {base_ref[:12]}){linked_suffix}"
378
+ f"task worktree created at {worktree_path} on branch {branch} "
379
+ f"(base {base_ref[:12]}; phase {task_type}){linked_suffix}"
297
380
  ),
298
381
  )
@@ -0,0 +1,211 @@
1
+ """Global task-worktree registry.
2
+
3
+ Tracks which `(project_id, task_group, task_id)` task-key owns which
4
+ on-disk worktree path and branch, across concurrent okstra runs on the
5
+ same machine. The registry lives under `OKSTRA_HOME` (default
6
+ `~/.okstra`) and is guarded by an `fcntl` exclusive lock so that two
7
+ processes cannot race to reserve the same path or branch.
8
+
9
+ Why a global registry:
10
+ - A single task-key spans multiple phases (requirements-discovery →
11
+ error-analysis → implementation-planning → implementation). All
12
+ phases must land in the **same** worktree on the **same** branch.
13
+ Re-entry from any phase must look up the existing entry instead of
14
+ creating a duplicate.
15
+ - Two different task-keys must never collide on the same branch name.
16
+ A global branch index makes that detectable cheaply.
17
+ - Cleanup of stale entries (worktree dir removed manually) needs a
18
+ single source of truth.
19
+
20
+ The registry is intentionally JSON-on-disk (no SQLite): the data set is
21
+ tiny (one row per active task on this machine) and the human-readable
22
+ file is useful for debugging.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import contextlib
27
+ import fcntl
28
+ import json
29
+ import os
30
+ import time
31
+ from dataclasses import dataclass
32
+ from pathlib import Path
33
+ from typing import Optional
34
+
35
+
36
+ REGISTRY_FILENAME = "registry.json"
37
+ LOCK_FILENAME = "registry.lock"
38
+
39
+
40
+ def _okstra_worktrees_dir() -> Path:
41
+ home_env = os.environ.get("OKSTRA_HOME", "").strip()
42
+ base = Path(home_env) if home_env else (Path.home() / ".okstra")
43
+ return base / "worktrees"
44
+
45
+
46
+ def task_key(project_id: str, task_group: str, task_id: str) -> str:
47
+ """Canonical task-key string used as the registry primary key.
48
+
49
+ Segments are NOT re-slugified here — callers must pass already
50
+ sanitised segments (see `worktree._safe_segment`). The key form
51
+ `<project>/<group>/<task>` is the same shape used for filesystem
52
+ paths so a key can be visually correlated with the worktree dir.
53
+ """
54
+ return f"{project_id}/{task_group}/{task_id}"
55
+
56
+
57
+ @dataclass
58
+ class WorktreeEntry:
59
+ task_key: str
60
+ project_id: str
61
+ task_group: str
62
+ task_id: str
63
+ worktree_path: str
64
+ branch: str
65
+ base_ref: str
66
+ created_at: str
67
+ last_phase: str = ""
68
+ status: str = "active" # "active" | "released"
69
+
70
+
71
+ @contextlib.contextmanager
72
+ def _registry_lock():
73
+ """Exclusive flock on `<worktrees>/registry.lock`. Mirrors the
74
+ `central_lock` pattern from `locks.py` but scoped to the registry
75
+ so we do not serialise unrelated central operations.
76
+ """
77
+ root = _okstra_worktrees_dir()
78
+ root.mkdir(parents=True, exist_ok=True)
79
+ lockfile = root / LOCK_FILENAME
80
+ if not lockfile.exists():
81
+ lockfile.touch()
82
+ f = lockfile.open("r+")
83
+ try:
84
+ fcntl.flock(f.fileno(), fcntl.LOCK_EX)
85
+ yield
86
+ finally:
87
+ f.close()
88
+
89
+
90
+ def _registry_path() -> Path:
91
+ return _okstra_worktrees_dir() / REGISTRY_FILENAME
92
+
93
+
94
+ def _load() -> dict:
95
+ p = _registry_path()
96
+ if not p.exists():
97
+ return {"tasks": {}, "branches": {}}
98
+ try:
99
+ data = json.loads(p.read_text())
100
+ except (OSError, json.JSONDecodeError):
101
+ return {"tasks": {}, "branches": {}}
102
+ data.setdefault("tasks", {})
103
+ data.setdefault("branches", {})
104
+ return data
105
+
106
+
107
+ def _save(data: dict) -> None:
108
+ p = _registry_path()
109
+ p.parent.mkdir(parents=True, exist_ok=True)
110
+ tmp = p.with_suffix(".json.tmp")
111
+ tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
112
+ os.replace(tmp, p)
113
+
114
+
115
+ def lookup(project_id: str, task_group: str, task_id: str) -> Optional[WorktreeEntry]:
116
+ """Return the registered entry for this task-key, or None.
117
+
118
+ Does not validate that `worktree_path` still exists on disk — that
119
+ is the caller's responsibility (so reclaim logic can decide policy).
120
+ """
121
+ key = task_key(project_id, task_group, task_id)
122
+ with _registry_lock():
123
+ data = _load()
124
+ row = data["tasks"].get(key)
125
+ if not row:
126
+ return None
127
+ return WorktreeEntry(task_key=key, **row)
128
+
129
+
130
+ def reserve(
131
+ *,
132
+ project_id: str,
133
+ task_group: str,
134
+ task_id: str,
135
+ worktree_path: str,
136
+ branch: str,
137
+ base_ref: str,
138
+ phase: str = "",
139
+ ) -> WorktreeEntry:
140
+ """Atomically insert a new entry. Raises RuntimeError if the
141
+ task-key already exists or the branch is already owned by a
142
+ different task-key. Callers should `lookup()` first when re-entry
143
+ is expected.
144
+ """
145
+ key = task_key(project_id, task_group, task_id)
146
+ now = time.strftime("%Y-%m-%dT%H:%M:%S%z") or time.strftime("%Y-%m-%dT%H:%M:%S")
147
+ with _registry_lock():
148
+ data = _load()
149
+ if key in data["tasks"]:
150
+ existing = data["tasks"][key]
151
+ raise RuntimeError(
152
+ f"task-key already has a worktree registered: {key} → "
153
+ f"{existing['worktree_path']} (branch {existing['branch']}). "
154
+ "Use `lookup` to reuse it, or release it before reserving anew."
155
+ )
156
+ owner = data["branches"].get(branch)
157
+ if owner and owner != key:
158
+ raise RuntimeError(
159
+ f"branch {branch!r} is already registered to a different "
160
+ f"task-key: {owner}. Choose a different work-category or "
161
+ "release the conflicting task first."
162
+ )
163
+ row = {
164
+ "project_id": project_id,
165
+ "task_group": task_group,
166
+ "task_id": task_id,
167
+ "worktree_path": worktree_path,
168
+ "branch": branch,
169
+ "base_ref": base_ref,
170
+ "created_at": now,
171
+ "last_phase": phase,
172
+ "status": "active",
173
+ }
174
+ data["tasks"][key] = row
175
+ data["branches"][branch] = key
176
+ _save(data)
177
+ return WorktreeEntry(task_key=key, **row)
178
+
179
+
180
+ def touch_phase(project_id: str, task_group: str, task_id: str, phase: str) -> None:
181
+ """Record the most recent phase observed on this worktree.
182
+ Best-effort: silently no-ops if the task-key is not registered.
183
+ """
184
+ key = task_key(project_id, task_group, task_id)
185
+ with _registry_lock():
186
+ data = _load()
187
+ row = data["tasks"].get(key)
188
+ if not row:
189
+ return
190
+ row["last_phase"] = phase
191
+ _save(data)
192
+
193
+
194
+ def release(project_id: str, task_group: str, task_id: str) -> Optional[WorktreeEntry]:
195
+ """Mark the entry as `released` (worktree dir intact — preservation
196
+ is the project's policy). The branch index is freed so future
197
+ reservations of the same branch name are not blocked.
198
+ Returns the prior entry, or None when not found.
199
+ """
200
+ key = task_key(project_id, task_group, task_id)
201
+ with _registry_lock():
202
+ data = _load()
203
+ row = data["tasks"].get(key)
204
+ if not row:
205
+ return None
206
+ row["status"] = "released"
207
+ # Free the branch slot so reuse / new task can reclaim the name.
208
+ if data["branches"].get(row["branch"]) == key:
209
+ del data["branches"][row["branch"]]
210
+ _save(data)
211
+ return WorktreeEntry(task_key=key, **row)
@@ -199,7 +199,9 @@ The final-report template `okstra-final-report.template.md` Section 4.5 already
199
199
 
200
200
  ### Release-handoff section contract (release-handoff runs only)
201
201
 
202
- When the run's `task-type` is `release-handoff`, the final report MUST include Section `## 4.6 Release Handoff Deliverables` with all seven sub-sections (`4.6.1` Source Verification Report, `4.6.2` Feature Branch & Working-Tree State, `4.6.3` User Selections, `4.6.4` Executed Commands, `4.6.5` Commit List, `4.6.6` Pull Request Outcome, `4.6.7` Routing Recommendation). The drafter does **not** invent values for these sub-sections — every entry is dictated by the lead's recorded git/gh command log and the user's verbatim answers to the H1/H2/H3 menu prompts. If the user picked `skip` (H1) or `cancel` (H3), keep 4.6.3 populated but leave 4.6.4–4.6.6 explicitly empty per the template's empty-state lines.
202
+ When the run's `task-type` is `release-handoff`, the final report MUST include Section `## 4.6 Release Handoff Deliverables` with all seven sub-sections (`4.6.1` Source Verification Report, `4.6.2` Feature Branch & Working-Tree State, `4.6.3` User Selections, `4.6.4` Executed Commands, `4.6.5` Commit List, `4.6.6` Pull Request Outcome, `4.6.7` Routing Recommendation). Every entry is dictated by the lead's recorded git/gh command log and the user's verbatim answers to the H1/H2/H3 menu prompts. If the user picked `skip` (H1) or `cancel` (H3), keep 4.6.3 populated but leave 4.6.4–4.6.6 explicitly empty per the template's empty-state lines.
203
+
204
+ **Single-lead authorship (release-handoff only):** release-handoff has no worker roster (no `Report writer worker`, no `Claude worker` drafter). The Claude lead authors the final-report file directly — there is no `Report writer worker` dispatch to perform in Phase 6, no resume-safe dispatch concern, and no mandatory worker-results file for a report-writer role. The rest of this skill's dispatch / resume / fallback machinery applies ONLY when `Report writer worker` is in the roster (i.e. every task-type other than `release-handoff`).
203
205
 
204
206
  The final-report template `okstra-final-report.template.md` Section 4.6 already encodes this contract — copy that block verbatim and fill in. For non-`release-handoff` runs, omit Section 4.6 entirely.
205
207