okstra 0.173.0 → 0.174.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/docs/architecture/storage-model.md +13 -3
  2. package/docs/architecture.md +5 -21
  3. package/docs/cli.md +3 -2
  4. package/docs/container.md +1 -1
  5. package/docs/contributor-change-matrix.md +1 -1
  6. package/docs/project-structure-overview.md +13 -13
  7. package/docs/task-process/README.md +1 -1
  8. package/docs/task-process/implementation-planning.md +1 -1
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/claude-worker.md +1 -1
  12. package/runtime/bin/lib/okstra/globals.sh +1 -1
  13. package/runtime/bin/okstra-provider-exec.py +29 -12
  14. package/runtime/bin/okstra-trace-cleanup.sh +58 -129
  15. package/runtime/prompts/lead/adapters/cmux.md +2 -0
  16. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  17. package/runtime/prompts/lead/plan-body-verification.md +3 -3
  18. package/runtime/prompts/lead/report-writer.md +6 -6
  19. package/runtime/prompts/profiles/_common-contract.md +2 -2
  20. package/runtime/prompts/profiles/_implementation-executor.md +2 -0
  21. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  22. package/runtime/prompts/profiles/error-analysis.md +1 -1
  23. package/runtime/prompts/profiles/implementation-planning.md +12 -9
  24. package/runtime/prompts/profiles/implementation.md +2 -1
  25. package/runtime/prompts/profiles/release-handoff.md +1 -1
  26. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
  27. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
  28. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
  29. package/runtime/python/okstra_ctl/conformance.py +68 -0
  30. package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
  31. package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
  32. package/runtime/python/okstra_ctl/doctor.py +2 -2
  33. package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
  34. package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
  35. package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
  36. package/runtime/python/okstra_ctl/render_final_report.py +15 -19
  37. package/runtime/python/okstra_ctl/report_contract.py +0 -1
  38. package/runtime/python/okstra_ctl/report_finalize.py +68 -9
  39. package/runtime/python/okstra_ctl/run.py +43 -2
  40. package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
  41. package/runtime/python/okstra_ctl/scope_provenance.py +1 -1
  42. package/runtime/python/okstra_ctl/session.py +69 -12
  43. package/runtime/python/okstra_ctl/team.py +51 -25
  44. package/runtime/python/okstra_ctl/tmux.py +19 -149
  45. package/runtime/python/okstra_ctl/worker_request.py +2 -0
  46. package/runtime/python/okstra_ctl/worktree.py +69 -3
  47. package/runtime/python/okstra_token_usage/cli.py +1 -1
  48. package/runtime/python/okstra_token_usage/collect.py +66 -6
  49. package/runtime/skills/okstra-setup/references/project-config.md +11 -0
  50. package/runtime/templates/reports/settings.template.json +0 -24
  51. package/runtime/validators/lib/fixtures.sh +49 -17
  52. package/runtime/validators/validate-implementation-plan-stages.py +63 -3
  53. package/runtime/validators/validate-run.py +14 -473
  54. package/runtime/validators/validate_session_conformance.py +1 -1
  55. package/src/cli-registry.mjs +8 -1
  56. package/src/commands/execute/team.mjs +3 -3
  57. package/src/commands/execute/worktree-status.mjs +109 -0
  58. package/src/commands/lifecycle/install.mjs +0 -2
  59. package/src/commands/report/finalize.mjs +13 -6
  60. package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
  61. package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
  62. package/runtime/templates/reports/final-report.template.md +0 -1258
@@ -1,45 +1,45 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # okstra-trace-cleanup.sh — close tmux panes created during okstra runs.
3
+ # okstra-trace-cleanup.sh — close the harness-owned worker panes of an okstra run.
4
4
  #
5
- # Worker-compute panes are tmux-pane backend siblings. Their dispatcher tags
6
- # each pane it owns with a pane-level user option (`@okstra_worker_run=<RUN_DIR>`),
7
- # so panes are found server-wide by tag — no tmux env var or pane-id registry is
8
- # needed, and the run-scoped tag keeps concurrent okstra runs from closing each
9
- # other's panes.
5
+ # The panes this closes are the teammate panes the HARNESS splits for dispatched
6
+ # worker agents. okstra does not create them and cannot tag them, so they are
7
+ # found by a title allowlist scoped to the lead's window. They accumulate until
8
+ # the pane budget runs out, and `shutdown_request` only idles the agent without
9
+ # freeing its pane — this script is the only thing that closes them, which is why
10
+ # the lead calls it at every round boundary and before every user gate.
10
11
  #
11
- # Trace panes were `tail -F` siblings the provider wrappers split, tagged
12
- # `@okstra_trace_run` / `@okstra_status`. Those wrappers are now four-line
13
- # entrypoints and worker progress renders into the worker's own pane, so
14
- # NOTHING SPAWNS A TRACE PANE and neither tag has a writer left. The trace
15
- # paths below still run and simply match nothing; `--reclaim-completed`, which
16
- # keys on `@okstra_status`, is inert for the same reason. Kept rather than
17
- # deleted because the hooks that call this script are already seeded on user
18
- # machines — retiring the trace machinery is a deliberate follow-up.
12
+ # Two tag-driven paths used to live here and both are gone:
19
13
  #
20
- # Two invocation shapes:
14
+ # - Worker-compute panes of the `tmux-pane` dispatch backend, tagged
15
+ # `@okstra_worker_run`. That backend was removed — a worker now gets a cmux
16
+ # surface or a cli-wrapper subprocess, and neither is a tmux pane okstra owns.
17
+ # - Trace panes, tagged `@okstra_trace_run` / `@okstra_status`, which the
18
+ # provider wrappers used to split as `tail -F` siblings. The wrappers became
19
+ # four-line entrypoints and worker progress renders into the worker's own
20
+ # pane, so nothing had spawned a trace pane for some time.
21
21
  #
22
- # --run-dir <RUN_DIR> Used by the LEAD between phases and at wrap-up. Closes
23
- # (a) trace panes tagged with this run's dir and
24
- # (b) worker-agent panes the harness gives to dispatched
25
- # subagents (`claude-worker` / `codex-worker` /
26
- # `antigravity-worker` / `report-writer-worker`), identified
27
- # by a title allowlist scoped to the LEAD's WINDOW. The
28
- # lead pane is read from `<RUN_DIR>/state/lead-pane.id`
29
- # (recorded once by the lead in its own foreground pane —
30
- # reliable even though Claude Code's Bash tool strips
31
- # `$TMUX`/`$TMUX_PANE`); it scopes the title scan and is
32
- # NEVER killed. Window scope keeps a second lead running
33
- # in another window of the same session out of range.
22
+ # With both writers gone the tag scan matched nothing, so it was removed along
23
+ # with the two modes that existed only to drive it (`--reap`, which the SessionEnd
24
+ # hook called, and `--reclaim-completed`, which `okstra-subagent-reclaim.sh` drove
25
+ # from SubagentStop / TaskCompleted). Those hooks are gone from the seeded
26
+ # settings too.
34
27
  #
35
- # --reap Used by the `SessionEnd` hook, where no single run-dir
36
- # applies. Closes every trace pane whose tag points under
37
- # `$CLAUDE_PROJECT_DIR/.okstra/` (or every tagged trace
38
- # pane if that env var is unset). Harness-owned
39
- # worker-agent panes are left to the harness.
28
+ # Invocation:
29
+ #
30
+ # --run-dir <RUN_DIR> Closes the run's worker-agent panes (`claude-worker` /
31
+ # `codex-worker` / `antigravity-worker` /
32
+ # `report-writer-worker` and the FleetView teammate
33
+ # prefixes), scoped to the LEAD's WINDOW. The lead pane is
34
+ # read from `<RUN_DIR>/state/lead-pane.id` (recorded once
35
+ # by the lead in its own foreground pane — reliable even
36
+ # though Claude Code's Bash tool strips `$TMUX` /
37
+ # `$TMUX_PANE`); it scopes the scan and is NEVER killed.
38
+ # Window scope keeps a second lead running in another
39
+ # window of the same session out of range.
40
40
  #
41
41
  # `--list` (alias `--dry-run`) prints `<pane_id>\t<pane_title>` per pane instead
42
- # of killing — only meaningful with `--run-dir`.
42
+ # of killing.
43
43
  #
44
44
  # `--keep <substr>` (repeatable) spares any pane whose current title contains
45
45
  # <substr>, in both the kill and the list set. Used to preserve an in-flight
@@ -50,7 +50,7 @@
50
50
 
51
51
  set -u
52
52
 
53
- # Trace-pane caller resolution helper (okstra_resolve_caller_pane) — see
53
+ # Caller-pane resolution helper (okstra_resolve_caller_pane) — see
54
54
  # lib/okstra/tmux-pane.sh. Used as the lead-pane fallback below so a missing /
55
55
  # stale lead-pane.id resolves to the pane THIS process actually runs in (via
56
56
  # ancestor-PID ↔ tmux pane_pid matching), never a foreign active-client pane.
@@ -58,43 +58,26 @@ set -u
58
58
  _clean_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
59
59
  [ -r "$_clean_script_dir/lib/okstra/tmux-pane.sh" ] && . "$_clean_script_dir/lib/okstra/tmux-pane.sh"
60
60
 
61
- # --reclaim-completed shells out to `python3 -m okstra_ctl.pane_reclaim`. The
62
- # package is a bin-sibling in the repo layout (scripts/okstra_ctl) and under
63
- # $OKSTRA_HOME/lib/python in the installed layout. Put both on PYTHONPATH so the
64
- # import resolves regardless of where this script runs from.
65
- _okstra_home="${OKSTRA_HOME:-$HOME/.okstra}"
66
- export PYTHONPATH="${_clean_script_dir}:${_okstra_home}/lib/python${PYTHONPATH:+:$PYTHONPATH}"
67
-
68
61
  MODE="kill" # kill | list
69
- RECLAIM=0 # 1: trace pane 은 @okstra_status 가 완료(exited)일 때만 회수 (--reclaim-completed)
70
- REAP=0
71
62
  run_dir=""
72
63
  KEEP_PATTERNS=() # --keep <substr>: panes whose title contains substr are spared from kill/list
73
64
  while [[ $# -gt 0 ]]; do
74
65
  case "$1" in
75
- --list|--dry-run) MODE="list" ;;
76
- --reclaim-completed) RECLAIM=1 ;;
77
- --reap) REAP=1 ;;
66
+ --list|--dry-run) MODE="list" ;;
78
67
  --run-dir) shift; run_dir="${1-}" ;;
79
68
  --run-dir=*) run_dir="${1#--run-dir=}" ;;
80
69
  --keep) shift; KEEP_PATTERNS+=("${1-}") ;;
81
70
  --keep=*) KEEP_PATTERNS+=("${1#--keep=}") ;;
82
71
  -h|--help)
83
72
  cat <<'USAGE'
84
- usage: okstra-trace-cleanup.sh (--run-dir <RUN_DIR> [--list] [--reclaim-completed] [--keep <substr>]... | --reap)
73
+ usage: okstra-trace-cleanup.sh --run-dir <RUN_DIR> [--list] [--keep <substr>]...
85
74
 
86
- --run-dir okstra run directory; closes that run's trace + worker-agent panes.
87
- --list with --run-dir: print "<pane_id>\t<pane_title>" per pane; no kill.
75
+ --run-dir okstra run directory; closes that run's worker-agent panes.
76
+ --list print "<pane_id>\t<pane_title>" per pane; no kill.
88
77
  --dry-run alias for --list.
89
78
  --keep <substr> exclude any pane whose title contains <substr> from the
90
79
  kill/list set (repeatable). Used to spare an in-flight
91
80
  report-writer at a round boundary.
92
- --reclaim-completed with --run-dir: restrict trace panes to those whose
93
- @okstra_status sidecar is terminal (stage=exited); in-flight
94
- and teammate panes are preserved. Skips the title-allowlist
95
- teammate scan. Combinable with --list.
96
- --reap close every okstra trace pane under $CLAUDE_PROJECT_DIR/.okstra
97
- (SessionEnd hook; no single run-dir applies).
98
81
  USAGE
99
82
  exit 0 ;;
100
83
  *)
@@ -104,29 +87,16 @@ USAGE
104
87
  shift
105
88
  done
106
89
 
107
- if [[ "$REAP" -eq 0 && -z "$run_dir" ]]; then
108
- printf 'okstra-trace-cleanup.sh: --run-dir <RUN_DIR> (or --reap) is required\n' >&2
90
+ if [[ -z "$run_dir" ]]; then
91
+ printf 'okstra-trace-cleanup.sh: --run-dir <RUN_DIR> is required\n' >&2
109
92
  exit 2
110
93
  fi
111
94
 
112
- # Canonicalize paths used in tag string-compares. The wrappers tag panes with
113
- # `pwd -P` (symlink-resolved), so the scope paths must be resolved the same way
114
- # — else a symlinked component (e.g. macOS /tmp -> /private/tmp) makes the
115
- # compare miss. Fall back to the literal value if the dir does not resolve.
116
- _resolve() { (cd "$1" 2>/dev/null && pwd -P) || printf '%s' "$1"; }
117
- [[ -n "$run_dir" ]] && run_dir="$(_resolve "$run_dir")"
118
- project_dir=""
119
- [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && project_dir="$(_resolve "$CLAUDE_PROJECT_DIR")"
120
-
121
- # Lead pane. For a run, prefer the value the lead recorded in its own foreground
122
- # pane; fall back to the active-pane probe. Rejected if the recorded pane is
123
- # gone. For --reap there is no run state — probe the active pane, used only to
124
- # avoid killing whatever pane the reap runs from.
95
+ # Lead pane. Prefer the value the lead recorded in its own foreground pane; fall
96
+ # back to the active-pane probe. Rejected if the recorded pane is gone.
125
97
  lead_pane=""
126
- if [[ "$REAP" -eq 0 ]]; then
127
- lead_pane_file="$run_dir/state/lead-pane.id"
128
- [[ -r "$lead_pane_file" ]] && lead_pane="$(head -n1 "$lead_pane_file" 2>/dev/null || true)"
129
- fi
98
+ lead_pane_file="$run_dir/state/lead-pane.id"
99
+ [[ -r "$lead_pane_file" ]] && lead_pane="$(head -n1 "$lead_pane_file" 2>/dev/null || true)"
130
100
  if [[ -z "$lead_pane" ]] || ! tmux display-message -p -t "$lead_pane" '#{pane_id}' >/dev/null 2>&1; then
131
101
  if type okstra_resolve_caller_pane >/dev/null 2>&1; then
132
102
  lead_pane="$(okstra_resolve_caller_pane 2>/dev/null || true)"
@@ -135,17 +105,6 @@ if [[ -z "$lead_pane" ]] || ! tmux display-message -p -t "$lead_pane" '#{pane_id
135
105
  fi
136
106
  fi
137
107
 
138
- # Does a trace pane's tag belong to the set we are closing?
139
- _tag_in_scope() {
140
- local tag="$1"
141
- if [[ "$REAP" -eq 1 ]]; then
142
- [[ -z "$tag" ]] && return 1
143
- [[ -n "$project_dir" ]] && { [[ "$tag" == "$project_dir/"* ]]; return; }
144
- return 0 # no project scope available → reap every tagged trace pane
145
- fi
146
- [[ "$tag" == "$run_dir" ]]
147
- }
148
-
149
108
  _title_in_okstra_scope() {
150
109
  local title="$1"
151
110
  case "$title" in
@@ -168,7 +127,6 @@ _title_in_okstra_scope() {
168
127
  }
169
128
 
170
129
  # A collected pane whose current title contains any --keep substring is spared.
171
- # Applied at the final emit so both the tag scan and the title scan honour it.
172
130
  _keep_excluded() {
173
131
  local pid="$1" title pat
174
132
  (( ${#KEEP_PATTERNS[@]} )) || return 1
@@ -181,54 +139,25 @@ _keep_excluded() {
181
139
 
182
140
  collect_okstra_panes() {
183
141
  local -a panes=()
184
- local pid trace_tag worker_tag status_tag title
185
-
186
- # (1) Trace and worker-compute panes tagged in scope — found server-wide by
187
- # tag, so no tmux env var or pane-id registry is needed. Each `@okstra_*`
188
- # column carries a leading `x` sentinel (stripped after read): tmux's `-F`
189
- # drops an UNSET user option AND its adjacent tab, which would otherwise
190
- # shift later columns left (e.g. an empty worker tag stealing the status
191
- # value). The sentinel keeps every column present so positional parsing holds.
192
- # `#x` strip 은 실제 태그 값을 깎지 않는다: 모든 태그 값(trace_run/worker_run/
193
- # status)은 절대경로라 `/` 로 시작 → `x` 로 시작하는 일이 없어 sentinel 만 벗겨진다.
194
- while IFS=$'\t' read -r pid trace_tag worker_tag status_tag; do
195
- trace_tag="${trace_tag#x}"; worker_tag="${worker_tag#x}"; status_tag="${status_tag#x}"
142
+ local pid title
143
+
144
+ # Title-allowlisted worker-agent panes in the lead's WINDOW. `list-panes -t
145
+ # <pane>` (no `-s`, no `-a`) resolves the window containing that pane and lists
146
+ # only that window's panes. Split-pane teammates always land in the lead's
147
+ # window, so this catches all of THIS run's worker panes while a second okstra
148
+ # lead in another window of the same tmux session is left untouched. Skipped
149
+ # when the lead pane is unknown.
150
+ [[ -n "$lead_pane" ]] || return 0
151
+ while IFS=$'\t' read -r pid title; do
196
152
  [[ -n "$pid" ]] || continue
197
153
  [[ "$pid" == "$lead_pane" ]] && continue
198
- if _tag_in_scope "$trace_tag" || _tag_in_scope "$worker_tag"; then
199
- if [[ "$RECLAIM" -eq 1 ]]; then
200
- # reclaim 모드: 완료(stage=exited)된 worker 의 pane 만. status 태그가
201
- # 없거나(teammate/비-wrapper pane) 미완료면 보존.
202
- [[ -n "$status_tag" ]] || continue
203
- python3 -m okstra_ctl.pane_reclaim "$status_tag" || continue
204
- fi
154
+ if _title_in_okstra_scope "$title"; then
205
155
  panes+=("$pid")
206
156
  fi
207
- done < <(tmux list-panes -a \
208
- -F '#{pane_id}'$'\t''x#{@okstra_trace_run}'$'\t''x#{@okstra_worker_run}'$'\t''x#{@okstra_status}' \
209
- 2>/dev/null || true)
210
- # (2) Title-allowlisted worker-agent panes in the lead's WINDOW. Only for a
211
- # run (reap leaves these harness-owned panes to the harness). `list-panes -t
212
- # <pane>` (no `-s`, no `-a`) resolves the window containing that pane and
213
- # lists only that window's panes. Split-pane teammates always land in the
214
- # lead's window, so this catches all of THIS run's worker panes while a second
215
- # okstra lead in another window of the same tmux session — whose in-flight
216
- # worker panes are untagged and title-only — is left untouched. Skipped when
217
- # the lead pane is unknown. reclaim 모드는 teammate pane 을 회수하지 않으므로
218
- # (완료 판정 불가, trace-only) 이 스캔을 건너뛴다.
219
- if [[ "$REAP" -eq 0 && "$RECLAIM" -eq 0 && -n "$lead_pane" ]]; then
220
- while IFS=$'\t' read -r pid title; do
221
- [[ -n "$pid" ]] || continue
222
- [[ "$pid" == "$lead_pane" ]] && continue
223
- if _title_in_okstra_scope "$title"; then
224
- panes+=("$pid")
225
- fi
226
- done < <(tmux list-panes -t "$lead_pane" \
227
- -F '#{pane_id}'$'\t''#{pane_title}' 2>/dev/null || true)
228
- fi
157
+ done < <(tmux list-panes -t "$lead_pane" \
158
+ -F '#{pane_id}'$'\t''#{pane_title}' 2>/dev/null || true)
229
159
 
230
- # Dedupe — a live trace pane can match both the tag scan and the title scan.
231
- # Then drop any pane a --keep pattern spares (in-flight report-writer).
160
+ # Drop any pane a --keep pattern spares (in-flight report-writer).
232
161
  if (( ${#panes[@]} )); then
233
162
  printf '%s\n' "${panes[@]}" | awk 'NF && !seen[$0]++' | while IFS= read -r _pid; do
234
163
  _keep_excluded "$_pid" && continue
@@ -34,6 +34,8 @@ It overrides only the worker-dispatch portion of the selected host relay, not th
34
34
  | `record_lead_event` | Append progress and activity records to the manifest-provided `leadEventsPath`. Emit the matching `PROGRESS:` line and, when an activity record is required, the immediately following `ACTIVITY:` line from the same structured fields. |
35
35
  | `collect_usage` | Collect artifact/CLI-log-backed usage through the existing Okstra token-usage path; never substitute another runtime's session log. |
36
36
 
37
+ An `implementation` run calls `dispatch_worker` twice: once for the Executor, then — after `await_workers` settles it — once for the verifiers. That second call's `--workers` list must omit the Executor's worker ID: it is materialized as the Executor on every dispatch, so a batch still carrying it is refused again. A verifier started beside the Executor observes base HEAD instead of the stage diff, so a single batch holding both is refused by `scripts/okstra_ctl/dispatch_core.py` `_validate_implementation_phase_order`, `--dry-run` included.
38
+
37
39
  ## Pane placement is not yours to compute
38
40
 
39
41
  Okstra creates, sizes, labels, and closes every worker pane. Do not issue terminal-multiplexer commands of any kind — not to place a worker, not to resize the lead, not to reclaim a finished round. Pane geometry depends on the display, and a lead that recomputes it per run gets it wrong differently on every host.
@@ -181,7 +181,7 @@ For `--task-type implementation` runs, the task bundle additionally pins one of
181
181
  - `instruction-set/analysis-profile.md` — top "Executor binding" block (provider, display name, model, runner, and dispatch mode)
182
182
  - `runs/implementation/manifests/run-manifest-*.json` — `teamContract.executor` object (the same binding plus `appliesTo: "implementation"`)
183
183
 
184
- Lead MUST dispatch Edit/Write-bearing work only through that executor binding: use the host primitive with `hostModelValue` for `runner=native-session`, or `okstra worker-dispatch` with `modelExecutionValue` for `runner=cli-wrapper`. The other two providers still run as read-only verifiers in the same run; the executor's own provider is *also* dispatched separately as a verifier in a fresh session, so the diff is reviewed context-isolated. Session isolation is the primary self-review safeguard — same-model executor and same-provider verifier is acceptable in distinct sessions. A different model variant (e.g. executor=opus / Claude verifier=sonnet) is recommended but not mandatory.
184
+ Lead MUST dispatch Edit/Write-bearing work only through that executor binding: use the host primitive with `hostModelValue` for `runner=native-session`, or `okstra worker-dispatch` with `modelExecutionValue` for `runner=cli-wrapper`. The other providers in the roster still run as read-only verifiers in the same run; the executor's own provider does not, because its worker ID materializes as the executor on every dispatch — so the diff is reviewed context-isolated by the remaining verifiers. Session isolation is the primary self-review safeguard — a verifier reusing the executor's model variant is acceptable in a distinct session. A different model variant (e.g. executor=opus / Claude verifier=sonnet) is recommended but not mandatory.
185
185
 
186
186
  Executor is chosen at run-prep time via `--executor <claude|codex|antigravity>` (or `OKSTRA_DEFAULT_EXECUTOR`, fallback `claude`); the model used by the executor is taken from the corresponding worker model flag (`--claude-model` / `--codex-model` / `--antigravity-model`). For CLI-backed executors, the underlying file mutation happens inside the executor CLI's own auto-edit mode (e.g. `codex exec --sandbox workspace-write`), not through the lead runtime's `write_artifact` operation.
187
187
 
@@ -53,7 +53,7 @@ The shared Majority definition and the auto-disable rule (fewer than 2 analyser
53
53
 
54
54
  ## Plan-item extraction (Round 0 equivalent)
55
55
 
56
- From the report-writer's draft of `## 5.4 Implementation Plan Deliverables`, the lead creates the verification queue only through this sequence (see also `templates/reports/final-report.template.md` §5.5.9):
56
+ From the report-writer's draft of `## 5.4 Implementation Plan Deliverables`, the lead creates the verification queue only through this sequence (see also `templates/reports/final-report-v2.template.md` §5.5.9):
57
57
 
58
58
  ```text
59
59
  okstra plan-items extract --data <data.json> --output <state>/plan-items-....json
@@ -124,7 +124,7 @@ The verdict tokens `AGREE` / `DISAGREE` / `SUPPLEMENT` are reused, but their mea
124
124
  `P-Prep-S<stage>-<kind>` applies the same verdict tokens and adds these disposition checks:
125
125
 
126
126
  - `inline-contract`: AGREE only when the contract contains the kind-specific fields needed to implement the stage; a label or generic prose is insufficient.
127
- - `prep-item`: AGREE only when every referenced PREP item has a concrete `aiProposal`, correct `stageRefs`/`kind`, and state-specific safeguards required by `schemas/final-report-v1.0.schema.json`.
127
+ - `prep-item`: AGREE only when every referenced PREP item has a concrete `aiProposal`, correct `stageRefs`/`kind`, and state-specific safeguards required by `schemas/final-report-v2.0.schema.json`.
128
128
  - `not-applicable`: AGREE only when the rationale is consistent with the stage action; otherwise DISAGREE with `fixability` (`planner-fixable` when the plan can supply the missing contract, `needs-user-input` only for genuinely external facts).
129
129
  - A declared `blocked` item is not itself a plan-body failure. Missing or duplicate coverage, an empty proposal, a mismatched reference, or an unjustified disposition is a failure and receives `DISAGREE(<kind>)` with `fixability`.
130
130
 
@@ -254,7 +254,7 @@ round before any host or provider process starts.
254
254
 
255
255
  **How the corrective round is recorded.** The first prompt was dispatched, so it is immutable — `--replace-undispatched` refuses it, correctly. Materialize the correction under a NEW `--invocation-id` and a new prompt path. Before linking its result, retire the first attempt's link: `okstra agent-prompt reject-result --run-manifest <path> --dispatch-id <first dispatch id> --superseded-by <corrective dispatch id> --reason "<what was wrong with the returned result>"`. Without that step the corrective `link-result` fails with `agent result is already linked to another dispatch`, which is how a worker that ran for twenty minutes and wrote a good result ends up unrecordable. Nothing is deleted: the rejected link stays in `agentResultLinks` carrying `supersededBy` and `rejectionReason`, so the ledger shows both attempts and why the second exists.
256
256
 
257
- Then lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below), **appending this round** — one new `roundHistory[]` entry plus this round's votes on each verified item's `planItems[].rounds[]`. The file accumulates across rounds; it is never truncated to the latest one. After `okstra plan-verify` exits 0, lead sets that new round's `completedAt` to the current ISO 8601 UTC time exactly once; a prior round's `completedAt` is immutable. Lead then populates `### 5.5.9 Plan Body Verification` in the final report's data.json (`implementationPlanning.planBodyVerification`, schema `schemas/final-report-v1.0.schema.json`; template at `templates/reports/final-report.template.md`). The §5.5.9 body is **grouped by plan item**: `planItems[]`, each carrying its `id`, its plain-language `subject` (rendered as the item heading), an optional `sourceSection`, an optional `clarificationId` (the `C-<N>` this item blocks on when `majority-disagree`), and a `verdicts[]` list (`worker / verdict / breakageKind / note`) — one verdict row per worker under that item. The renderer prints three fixed legends (gate values, verdict tokens, breakage kinds a–f) so the reader can decode every cell without opening this spec. The older flat `#### Verdict details` table (`Plan item / Worker / …`, one row per plan-item × worker pair) is superseded by the grouped layout — it hid *what* each vote was about behind a bare `P-*` ID; the subject heading is the fix. The validator's `Plan Body Verification` + `Gate result:` substring checks still gate this section.
257
+ Then lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below), **appending this round** — one new `roundHistory[]` entry plus this round's votes on each verified item's `planItems[].rounds[]`. The file accumulates across rounds; it is never truncated to the latest one. After `okstra plan-verify` exits 0, lead sets that new round's `completedAt` to the current ISO 8601 UTC time exactly once; a prior round's `completedAt` is immutable. Lead then populates `### 5.5.9 Plan Body Verification` in the final report's data.json (`implementationPlanning.planBodyVerification`, schema `schemas/final-report-v2.0.schema.json`; template at `templates/reports/final-report-v2.template.md`). The §5.5.9 body is **grouped by plan item**: `planItems[]`, each carrying its `id`, its plain-language `subject` (rendered as the item heading), an optional `sourceSection`, an optional `clarificationId` (the `C-<N>` this item blocks on when `majority-disagree`), and a `verdicts[]` list (`worker / verdict / breakageKind / note`) — one verdict row per worker under that item. The renderer prints three fixed legends (gate values, verdict tokens, breakage kinds a–f) so the reader can decode every cell without opening this spec. The older flat `#### Verdict details` table (`Plan item / Worker / …`, one row per plan-item × worker pair) is superseded by the grouped layout — it hid *what* each vote was about behind a bare `P-*` ID; the subject heading is the fix. The validator's `Plan Body Verification` + `Gate result:` substring checks still gate this section.
258
258
  7. **Self-fix loop (one rewrite, targeting planner-fixable defects).** After round 1, lead may run one report-writer rewrite when at least one `majority-disagree` item has a majority of its `DISAGREE` verdicts at `fixability == planner-fixable`. The targeted re-verification after that rewrite is round 2. After round 2, stop automatic self-fix regardless of outcome. Classify every remaining item as `user-decision`, `noncritical-dissent`, or `correctness-critical`. A second automatic self-fix is a contract violation. The fixed order is initial verification → one planner self-fix → targeted re-verification → user gate.
259
259
  - **Group the targets by cause before instructing (BLOCKING).** Blocked items are usually several derivatives of one defect — one constant declared twice, one responsibility given two owners — and the coverage rows that cite them fail as a consequence, not independently. Lead MUST partition this round's targets into cause groups and instruct each group as **"remove this cause"**, naming the derivatives it accounts for. **Handing report-writer a bare item list is forbidden**: patched one at a time, each correction leaves the sibling sections still asserting the old value, so the next round re-finds the same family and the budget drains without converging. Record the partition in `planBodyVerification.selfFixGroups[]` (`round`, `causeSummary`, `itemIds`). One group per item is a legitimate outcome only when the items genuinely share no cause — recorded that way, it is a visible diagnosis rather than a skipped one. **Enforced:** `validators/validate-run.py` `_validate_self_fix_grouping` requires the partition, ties `selfFixRoundsApplied` to the highest recorded round, and fails any corrected item that belongs to no group.
260
260
  - lead instructs report-writer to rewrite the items in each cause group (NOT a full draft regeneration; procedure in [report-writer](./report-writer.md) §"Self-fix rewrite").
@@ -261,17 +261,17 @@ For those v1 reports, the final report must contain section headings whose **lin
261
261
  | 7 | `Validation Checklist` | `### Validation Checklist (Korean gloss)` |
262
262
  | 8 | `Rollback` | `### Rollback Strategy (Korean gloss)` |
263
263
  | 9 | `Requirement Coverage` | `### Requirement Coverage (Korean gloss)` |
264
- | 10 | `Plan Body Verification` + `Gate result:` | `### Plan Body Verification (Korean gloss)` containing a `Gate result:` line — copy `templates/reports/final-report.template.md §5.5.9` verbatim. Validator checks both substrings. |
264
+ | 10 | `Plan Body Verification` + `Gate result:` | `### Plan Body Verification (Korean gloss)` containing a `Gate result:` line — copy `templates/reports/final-report-v2.template.md §5.5.9` verbatim. Validator checks both substrings. |
265
265
  | 11 | `Cross-Project Dependencies` | `### Cross-Project Dependencies (Korean gloss)` — `_(none)_` when the plan is single-project. |
266
266
  | 12 | `Decision Drafts` | `### Decision Drafts (Korean gloss)` |
267
267
 
268
268
  **Never author a `User Approval Request` section.** The approval gate moved to the YAML frontmatter `approved: true|false` field, and `validators/validate-run.py` fails any report containing a `## User Approval Request` block or a `### 5.5.8 User Approval Request` stub. This paragraph replaces an earlier row that listed the heading as required — following it produced a report the validator rejected.
269
269
 
270
- `Implementation Design Preparation` is NOT in the substring list: `templates/reports/final-report.template.md` §5.5.10 renders that heading from `implementationPlanning.designPreparation`, so it is enforced by the schema + renderer rather than by a heading scan.
270
+ `Implementation Design Preparation` is NOT in the substring list: `templates/reports/final-report-v2.template.md` §5.5.10 renders that heading from `implementationPlanning.designPreparation`, so it is enforced by the schema + renderer rather than by a heading scan.
271
271
 
272
272
  The English keyword is mandatory and the body of each section is written in English, like everything else you author — see "Writing Guidelines" below. For non-`implementation-planning` runs, omit this entire block — these headings are NOT validator-checked for other task-types.
273
273
 
274
- The final-report template `templates/reports/final-report.template.md` Section 5.5 already encodes this contract — copy that block verbatim and fill in.
274
+ The final-report template `templates/reports/final-report-v2.template.md` Section 5.5 already encodes this contract — copy that block verbatim and fill in.
275
275
 
276
276
  ### Self-fix rewrite (plan-body self-fix round)
277
277
 
@@ -305,7 +305,7 @@ When the run's `task-type` is `final-verification`, the report's `## 7. Final Ve
305
305
 
306
306
  For every other task-type, set the `Verdict Token` cell to `not-applicable`. Do NOT omit the row — the template renders it for all task-types and downstream tooling expects the field to exist.
307
307
 
308
- The final-report template `templates/reports/final-report.template.md` Section 7 already encodes this contract — copy that block verbatim and fill in.
308
+ The final-report template `templates/reports/final-report-v2.template.md` Section 7 already encodes this contract — copy that block verbatim and fill in.
309
309
 
310
310
  ### Release-handoff section contract (release-handoff runs only)
311
311
 
@@ -313,7 +313,7 @@ When the run's `task-type` is `release-handoff`, the final report MUST include S
313
313
 
314
314
  **Single-lead authorship (release-handoff only):** release-handoff has no worker roster. The 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 contract's dispatch / resume / fallback machinery applies ONLY when `Report writer worker` is in the roster (i.e. every task-type other than `release-handoff`).
315
315
 
316
- The final-report template `templates/reports/final-report.template.md` Section 5.6 already encodes this contract — copy that block verbatim and fill in. For non-`release-handoff` runs, omit Section 5.6 entirely.
316
+ The final-report template `templates/reports/final-report-v2.template.md` Section 5.6 already encodes this contract — copy that block verbatim and fill in. For non-`release-handoff` runs, omit Section 5.6 entirely.
317
317
 
318
318
  ### Mandatory worker-result pointer and audit sidecar (BLOCKING)
319
319
 
@@ -337,7 +337,7 @@ The selected report-writer preamble defines that audit shape. **Enforced:** `val
337
337
 
338
338
  ### Main Body Section
339
339
 
340
- Section numbering follows `templates/reports/final-report.template.md` exactly — that file is the documentation SSOT for section names and ordering. For full body structure at authoring time, consult your run's **phase-stripped** `final-report-template.md` (the per-task-type instruction-set copy defined in Phase 6 dispatch item 10); the "copy that block verbatim" references below mean the §-block as it appears in that stripped copy, not a re-read of the full source.
340
+ Section numbering follows `templates/reports/final-report-v2.template.md` exactly — that file is the documentation SSOT for section names and ordering. For full body structure at authoring time, consult your run's **phase-stripped** `final-report-template.md` (the per-task-type instruction-set copy defined in Phase 6 dispatch item 10); the "copy that block verbatim" references below mean the §-block as it appears in that stripped copy, not a re-read of the full source.
341
341
 
342
342
  **Verdict Card (top-of-report, mandatory).** Render `## Verdict Card` between the report header and the (conditional) Approval block. Its `Verdict Token` and `Direction` cells MUST byte-match the corresponding cells in `## 7. Final Verdict`. Its `Next Step`, the final-verdict next step, and the first item of `## 3. Recommended Next Steps` MUST name the same route target, though the actionable command and prose need not be byte-identical. Divergence is `contract-violated`.
343
343
 
@@ -12,7 +12,7 @@ profile document.
12
12
  - **Phase 5.5 (convergence — peer review by workers)**: workers peer-review each other's findings across up to `effectiveMaxRounds` rounds; the lead mediates but does not vote. See `prompts/lead/convergence.md` for the round protocol (replay of findings, `AGREE` / `DISAGREE` / `SUPPLEMENT` verdicts), queue invariants, and final classification (`full-consensus` / `partial-consensus` / `contested` / `worker-unique`). For `requirements-discovery`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `project-analysis`, `feature-analysis`, and `change-impact-analysis` this phase runs in **adversarial mode** (`convergence.adversarial=true`): verifiers try to refute each finding against its cited evidence and the burden of proof sits on the claim — see that skill's §"Adversarial Verification Mode".
13
13
  - Do NOT conclude "no peer review happens" from the roster alone — every profile that lists ≥2 analyser workers runs convergence by default (`convergence.enabled=true` in `task-manifest.json`).
14
14
  - For a new `implementation-planning` run, the plan-body sequence is initial verification → one planner self-fix → targeted re-verification → user gate. The initial verification is round 1, the targeted re-verification is round 2, and a second automatic self-fix is a contract violation. A user-directed correction does not consume the automatic self-fix limit, and a verification failure after that correction does not restart the automatic loop.
15
- - **provider-unavailable fallback (tolerance).** A worker dispatch can fail to produce a result for two distinct reasons, and both take the same recovery path. (1) **Pane budget:** the dispatch is rejected with `no room for another tmux split` (or an equivalent teammate-pane creation failure). (2) **Sandbox CLI-start failure (non-tmux path):** an external CLI worker wrapper exits non-zero within seconds with empty stdout and its live-log shows `operation not permitted`. In either case the lead spends the one shared retry budget through the assignment's recorded runner. If the provider is still unavailable, record that terminal status and continue only under the convergence quorum rules; never replace it silently with a fixed provider or count a substitute as the original provider's vote. Completed external-CLI worker panes are reclaimed by the selected runtime adapter's resource lifecycle. (This is a prompt instruction, not a code-enforced gate.)
15
+ - **provider-unavailable fallback (tolerance).** A worker dispatch can fail to produce a result for two distinct reasons, and both take the same recovery path. (1) **Pane budget:** the dispatch is rejected with `no room for another tmux split` (or an equivalent teammate-pane creation failure) — this is the harness running out of room for its own teammate panes, not okstra placing a worker. (2) **Sandbox CLI-start failure:** an external CLI worker wrapper exits non-zero within seconds with empty stdout and its live-log shows `operation not permitted`. In either case the lead spends the one shared retry budget through the assignment's recorded runner. If the provider is still unavailable, record that terminal status and continue only under the convergence quorum rules; never replace it silently with a fixed provider or count a substitute as the original provider's vote. Completed external-CLI workers hold no pane of their own; the harness teammate panes are reclaimed by the selected runtime adapter's resource lifecycle. (This is a prompt instruction, not a code-enforced gate.)
16
16
  - Dual-audience final-report contract (shared):
17
17
  - data.json is the sole authored report artifact. AI handoff Markdown and human HTML are independently derived from it; neither derived artifact is the other's source.
18
18
  - User-facing information belongs in `humanSummary` and the selected task block's `userNarrative`; it must not exist only in Markdown. The HTML human main body explains the result with those fields plus task facts.
@@ -69,7 +69,7 @@ profile document.
69
69
  - `origin` — who raised it. `worker-finding` (an analyser or verifier reached it on its own evidence), `material-gap` (neither the brief nor the codebase answers it), or `lead-directed` (the lead's own judgment, **including anything the lead instructed a worker to raise**). A lead that seeds its conclusion into a worker prompt and then reports the worker's agreement as an independent finding has mislabelled the row; that shape is what let one run block on a question its own lead had authored.
70
70
  - `userConfirmation` — what happened before the row was written. `asked-and-answered`, `asked-awaiting` (asked, no answer yet), or `deferred-no-interactive-session` (this run had no user to ask). Record an answer in `userInput` and move `status` to `answered`.
71
71
  - Neither field is required once `status` is `answered` / `resolved` — the record lives in `userInput` by then.
72
- - **Legacy canonical column schema (must match `templates/reports/final-report.template.md` §1 exactly):** every `## 1. Clarification Items` table has exactly these 4 columns, in this order:
72
+ - **Legacy canonical column schema (must match `templates/reports/final-report-v2.template.md` §1 exactly):** every `## 1. Clarification Items` table has exactly these 4 columns, in this order:
73
73
  `| <record-meta> | Statement | Expected form | User input |` (the first header is the i18n `columns.recordMeta` label — `Record`).
74
74
  The five short fields (ID, Ticket ID, Kind, Blocks, Status) are stacked inside the single record-meta cell, one per line separated by `<br>`, in this fixed order (mirrors the §2.1 Primary-Evidence meta column):
75
75
 
@@ -94,6 +94,8 @@ persisted prompt lacks the heading `Coding-conventions preflight`
94
94
  - **local git operations only**: `git add`, `git commit`. Prefer small commits keyed to plan steps.
95
95
  - **Explicit-path staging only (BLOCKING).** Stage exactly the files a plan step touches by naming each path — `git add <path1> <path2>`. Never use the bulk forms `git add .`, `git add -A`, `git add -u`, or `git add --all`: they sweep in unrelated edits, stray build output, and editor scratch files. Never use `git add -f` (force) under any circumstance — `-f` is the *only* way a `.gitignore`d path reaches a commit, and a forced ignored file is always a defect. If `git add <path>` reports the path is ignored, that file does not belong in the commit; do not override it — leave it untracked.
96
96
  - **No ignored / okstra files in commits (BLOCKING).** Before every `git commit`, run `git diff --cached --name-only` and pipe the staged paths through `git check-ignore --stdin --no-index`; abort the commit if it reports **any** match — a staged path that `.gitignore` excludes means a bulk-add or `-f` leaked it in. The check explicitly covers `.okstra/` (and `.project-docs/` when the legacy symlink is present): `.okstra/**` is gitignored, and force-staging it onto the stage branch is the one way these verification artifacts reach the upstream PR. Conformance/qa evidence belongs in the carry sidecar and verifier result — committing it is never correct, even when a step's instructions seem to ask for it.
97
+ - **Clean-tree checks use `okstra worktree-status --check-clean` (BLOCKING).** The worktree you execute in carries okstra's own provisioning — `.okstra`, the synced entries (`.project-docs`, `.claude`, …), and the nested `stage-<N>/` worktree — so `git status --porcelain` is never empty here and its emptiness proves nothing about your stage. Use `okstra worktree-status --check-clean`: it asks the same question over source paths only and exits 1 when dirty, so it chains as an assertion. This applies to a check you write yourself AND to one you inherit: when a plan step's `command` cell asserts cleanliness with a bare `git status`, run the okstra command in its place and record the substitution in `Out-of-plan edits` — the step's intent was a clean stage, not a literal empty status.
98
+ - **Never read an `.okstra/` artifact from a git object (BLOCKING).** `.okstra/**` is gitignored and never committed (see the staging rules above), so `git cat-file -e <tag>:.okstra/…`, `git show <tag>:.okstra/…`, and every variant of that read fail by construction — the answer is never "commit it after all". When a plan step's command depends on such a read, the step is unsatisfiable as written: read the artifact from the working tree instead, record the deviation, and carry the artifact forward through the stage carry sidecar.
97
99
  - **Commit message format (mandatory)**: every commit message MUST follow Conventional Commits — `<type>(<scope>): <subject>` for the first line, optional body separated by a blank line, optional footer. Constraints:
98
100
  - `<type>` MUST be one of: `feat` / `fix` / `perf` / `revert` / `deps` / `docs` / `refactor` / `build` / `ci` / `chore` / `test`. When the repo is `release-please`-managed, this aligns the commit with a configured changelog section.
99
101
  - `<scope>` SHOULD be the plan step identifier or the primary module touched (e.g. `feat(report-writer): ...`). Omit the parentheses only when no meaningful scope applies.
@@ -10,8 +10,8 @@ at Phase 5, BEFORE constructing the verifier worker dispatch prompts.
10
10
  ## Verifier roles (resolved at run-prep time)
11
11
 
12
12
  - **Verifier dispatch labelling.** The core functional role label is `<provider>-verifier` (here, and identically in `final-verification`). Provider, role, and model identity are owned by `prompts/lead/okstra-lead-contract.md` "Model assignments"; the selected runtime adapter owns provider-native dispatch-label mapping (including any `name` / `**Pane role:**` fields) and token-attribution wiring under its "Semantic operation mapping". This functional label is NOT what the run's PROGRESS checkpoints carry: `phase-4-dispatch` / `phase-5-collect` name the roster role team-state records (`Claude worker`, `Codex worker`), because that is the entry the Phase 7 conformance check matches them against.
13
- - The verifier slots are `Claude verifier` and `Codex verifier`, plus `Antigravity verifier` **only when `antigravity` is in the resolved `--workers` roster**. Every verifier in the resolved roster is dispatched regardless of which provider holds the executor role; the executor's own provider is run *separately* as a verifier (a fresh CLI session with no shared context) so that no verdict is produced from the same session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
14
- - Session isolation — not model-variant divergence — is the primary self-review safeguard: each verifier is a separate CLI invocation with its own context window, so reusing the same model variant for executor and same-provider verifier is acceptable. Different model variants (e.g. executor=opus / Claude verifier=sonnet) remain recommended when available.
13
+ - The verifier slots are `Claude verifier` and `Codex verifier`, plus `Antigravity verifier` **only when `antigravity` is in the resolved `--workers` roster**. Every verifier in the resolved roster is dispatched except the one whose worker ID holds the executor role this run: that ID materializes as the executor on every dispatch (`scripts/okstra_ctl/worker_prompt_policy.py`), so the executor's own provider has no separate verifier session in the current plumbing — a follow-up design item. Independence still holds where it counts: every verdict comes from a fresh CLI session with no shared context, never from the session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
14
+ - Session isolation — not model-variant divergence — is the primary self-review safeguard: each verifier is a separate CLI invocation with its own context window, so a verifier reusing the executor's model variant is acceptable. Different model variants (e.g. executor=opus / Claude verifier=sonnet) remain recommended when available.
15
15
  - Phase-specific model defaults override the shared defaults: `Claude verifier`=`opus`, `Codex verifier`=`gpt-5.6-sol`, `Antigravity verifier`=`gemini-3.1-pro` (only when present in the roster). The `Executor`'s model is taken from the provider-specific worker model corresponding to `--executor`: claude→`--claude-model` (default `opus`), codex→`--codex-model` (default `gpt-5.6-sol`), antigravity→`--antigravity-model` (default `gemini-3.1-pro`).
16
16
  - Verifiers read from the SAME working tree path the Executor used so they observe the exact diff the Executor produced. Verifiers remain strictly read-only there.
17
17
 
@@ -27,7 +27,7 @@
27
27
  - Structured diagnosis and routing contract:
28
28
  - `errorAnalysis` is the source of truth for reproduction status, `EA-NNN` cause candidates, the sharp next diagnostic, and the next route.
29
29
  - A route to `implementation-option-selection` requires a credible leading cause referenced by `routing.leadingCauseId` and `begin-option-selection` as the direction. A route back to `error-analysis` requires the sharp next diagnostic and `continue-investigation` as the direction.
30
- - Structure is enforced by `schemas/final-report-v1.0.schema.json` `$defs.ErrorAnalysis`. Cross-field diagnosis and route semantics are enforced by `validators/validate-run.py::_validate_error_analysis_consistency`.
30
+ - Structure is enforced by `schemas/final-report-v2.0.schema.json` `$defs.ErrorAnalysis`. Cross-field diagnosis and route semantics are enforced by `validators/validate-run.py::_validate_error_analysis_consistency`.
31
31
  - Primary focus areas:
32
32
  - symptom and trigger clarification
33
33
  - root-cause candidates
@@ -58,13 +58,13 @@
58
58
  - one `endStateCoverage` row per brief end-state id, whose `coveredBy` names the `R-NNN` row that carries it. The two tables are a chain, not duplicates: `endStateCoverage` proves no reporter requirement was dropped, `requirementCoverage` proves each one reaches a stage.
59
59
  - Implementation Design Preparation (`implementation-design-prep-v1`, BLOCKING):
60
60
  - **Detector SSOT:** the planner MUST run the V1 detector defined by `scripts/okstra_ctl/design_surfaces.py` (`detect_design_surfaces()` over the detector's `RULES`) and MUST NOT invent or copy a second keyword list into the plan or prompt. **Enforced:** `validators/validate-run.py` `_validate_detector_coverage` reruns that detector and compares every `(stage, kind)` plus its trigger evidence.
61
- - **Exactly-once coverage:** for every detector-produced `(stage, kind)`, the planner MUST write exactly one `designSurfaceCoverage` row on that stage. **Enforced:** `validators/validate-run.py` `_validate_detector_coverage` rejects missing, duplicate, extra-detector-kind, or evidence-mismatched rows; `schemas/final-report-v1.0.schema.json` `$defs.DesignSurfaceCoverage` enforces the row shape.
62
- - **Disposition:** a row MUST use `inline-contract` only when the stage already states the kind-specific minimum implementation contract; otherwise it MUST use `prep-item` and reference one or more `designPreparation.items`. `not-applicable` is legal only with a concrete rationale consistent with the stage action. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignSurfaceCoverage` enforces the disposition-specific fields, and `prompts/lead/plan-body-verification.md` `P-Prep-S<stage>-<kind>` verifies semantic sufficiency.
63
- - **AI-prepared proposal:** every referenced PREP item MUST record `kind`, `stageRefs`, `need`, evidence-cited `knownFacts`, `openQuestions`, a concrete evidence-backed `aiProposal` (`summary`, `details`, `assumptions`, `evidence`, `confidence`), `humanConfirmation`, explicit `status`, and the safest reversible default available. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignPrepItem` / `$defs.DesignPrepProposal` enforce required fields, `validators/validate-run.py` `_validate_prep_references` enforces the bidirectional stage/kind link, and `prompts/lead/plan-body-verification.md` rejects empty or non-implementable proposals.
64
- - **Status choice:** prefer `provisional` with a `workingAssumption`, concrete `guardrails`, `reviewAt`, `ifStillOpen`, and canonical `requestPath`; use `blocked` only for business policy, external authority, a destructive migration decision, or the absence of any safe reversible assumption. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignPrepItem` enforces state-specific fields, `validators/validate-run.py` `_validate_design_prep_states` enforces confirmation/request invariants, and `prompts/lead/plan-body-verification.md` judges whether the disposition is justified. A declared `blocked` status does not by itself fail plan-body verification.
61
+ - **Exactly-once coverage:** for every detector-produced `(stage, kind)`, the planner MUST write exactly one `designSurfaceCoverage` row on that stage. **Enforced:** `validators/validate-run.py` `_validate_detector_coverage` rejects missing, duplicate, extra-detector-kind, or evidence-mismatched rows; `schemas/final-report-v2.0.schema.json` `$defs.DesignSurfaceCoverage` enforces the row shape.
62
+ - **Disposition:** a row MUST use `inline-contract` only when the stage already states the kind-specific minimum implementation contract; otherwise it MUST use `prep-item` and reference one or more `designPreparation.items`. `not-applicable` is legal only with a concrete rationale consistent with the stage action. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.DesignSurfaceCoverage` enforces the disposition-specific fields, and `prompts/lead/plan-body-verification.md` `P-Prep-S<stage>-<kind>` verifies semantic sufficiency.
63
+ - **AI-prepared proposal:** every referenced PREP item MUST record `kind`, `stageRefs`, `need`, evidence-cited `knownFacts`, `openQuestions`, a concrete evidence-backed `aiProposal` (`summary`, `details`, `assumptions`, `evidence`, `confidence`), `humanConfirmation`, explicit `status`, and the safest reversible default available. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.DesignPrepItem` / `$defs.DesignPrepProposal` enforce required fields, `validators/validate-run.py` `_validate_prep_references` enforces the bidirectional stage/kind link, and `prompts/lead/plan-body-verification.md` rejects empty or non-implementable proposals.
64
+ - **Status choice:** prefer `provisional` with a `workingAssumption`, concrete `guardrails`, `reviewAt`, `ifStillOpen`, and canonical `requestPath`; use `blocked` only for business policy, external authority, a destructive migration decision, or the absence of any safe reversible assumption. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.DesignPrepItem` enforces state-specific fields, `validators/validate-run.py` `_validate_design_prep_states` enforces confirmation/request invariants, and `prompts/lead/plan-body-verification.md` judges whether the disposition is justified. A declared `blocked` status does not by itself fail plan-body verification.
65
65
  - **External-reality anchoring (`external-interface` / `transformation-mapping` surfaces):** these two detector kinds are correct only against data whose shape lives *outside this repository* (a third-party response body / external payload format). For such a surface the referenced PREP item's `aiProposal` MUST derive the assumed shape — selectors, field paths, response structure — from a **captured real sample** and cite it in `knownFacts` / `evidence` (source + capture time); a shape invented from internal reasoning and marked `confidence: high` is the disallowed move, because a plan built on an assumed shape yields an implementation whose parser and fixture only ever agree with each other. When the brief supplies no sample and none is capturable at plan time, the item MUST stay `provisional` with a `workingAssumption` that the external shape is unverified against reality, a `guardrails` line forbidding the implementation from presenting a synthetic-fixture green run as reality-verified, and an `ifStillOpen` that routes to user confirmation against real data — it MUST NOT be dispositioned `inline-contract` / settled. **Enforced (semantic):** the surface's presence is machine-checked by `_validate_detector_coverage`; whether its proposal's evidence is genuinely external is judged by the §5.5.9 `P-Prep-S<stage>-<kind>` round (this phase runs it adversarially).
66
- - **Planner-only test surface:** add `manual-user-test` only when a test prerequisite changes the implementation interface or acceptance contract; the V1 detector never emits it. **Enforced:** `validators/validate-run.py` `_validate_detector_coverage` rejects detector-produced `manual-user-test`, `schemas/final-report-v1.0.schema.json` permits its planner-authored shape, and `prompts/lead/plan-body-verification.md` verifies the stage-action rationale.
67
- - **Trivial task:** when the detector returns no surfaces and no interface/acceptance-changing manual test input exists, `designPreparation` MUST use `mode: no-design-inputs`, an empty `items` array, and a concrete reason tied to the plan. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignPreparation` requires the reason and empty array for that mode; `validators/validate-run.py` `_validate_design_prep_contract` validates the marked V1 payload.
66
+ - **Planner-only test surface:** add `manual-user-test` only when a test prerequisite changes the implementation interface or acceptance contract; the V1 detector never emits it. **Enforced:** `validators/validate-run.py` `_validate_detector_coverage` rejects detector-produced `manual-user-test`, `schemas/final-report-v2.0.schema.json` permits its planner-authored shape, and `prompts/lead/plan-body-verification.md` verifies the stage-action rationale.
67
+ - **Trivial task:** when the detector returns no surfaces and no interface/acceptance-changing manual test input exists, `designPreparation` MUST use `mode: no-design-inputs`, an empty `items` array, and a concrete reason tied to the plan. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.DesignPreparation` requires the reason and empty array for that mode; `validators/validate-run.py` `_validate_design_prep_contract` validates the marked V1 payload.
68
68
  - Approval gate (phase-specific addendum to shared authority rule):
69
69
  - The YAML frontmatter `approved: true|false` field is the only authorised approval gate. report-writer always emits `approved: false`. The user clears it either by (a) editing the frontmatter line to `approved: true` directly, or (b) invoking the next phase with `--approve` so the CLI flips the frontmatter on the user's behalf. `okstra_ctl.run._validate_approved_plan` reads this field and refuses entry until it is `true`.
70
70
  - Cross-verification mode:
@@ -103,7 +103,7 @@
103
103
  - Legacy candidate-comparison requires at least two implementation options. **Each option must include**:
104
104
  - **File Structure**: an explicit list of files to create / modify / delete with each file's responsibility (one-line each). Use the form `Create: path — responsibility` / `Modify: path:line-range — change summary` / `Delete: path — reason`. Write every `path` in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...` / a trailing `/…`); an abbreviated path does not resolve and is rejected by plan-body verification as a kind-b path mismatch.
105
105
  - **Two-tier change description.** Each `fileStructure` row carries `summary` **and** optional `details`, and they are not interchangeable:
106
- - `summary` — one plain-language sentence a reviewer who has never opened this file can follow: what changes and why it is needed. Name behaviour and domain nouns, not identifiers. No function/type/variable names, no call chains, no line numbers, no conditional logic spelled out. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.OptionCandidate.fileStructure.items.summary` caps it at 120 characters, so a dense identifier dump fails schema validation.
106
+ - `summary` — one plain-language sentence a reviewer who has never opened this file can follow: what changes and why it is needed. Name behaviour and domain nouns, not identifiers. No function/type/variable names, no call chains, no line numbers, no conditional logic spelled out. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.OptionCandidate.fileStructure.items.summary` caps it at 120 characters, so a dense identifier dump fails schema validation.
107
107
  - `details` — the technical specifics that used to be crammed into `summary`: exact symbol names, signatures, comparison semantics, line ranges, enum members. Omit the field when there is nothing beyond the summary.
108
108
  - Bad `summary` (identifier dump, no readable claim): `Widen the return of retrieveFontFamilyGroupWithFontVersionId to also yield the matched FontFamily (own key and enabled flag) and the matched Font.status.`
109
109
  - Good — `summary`: `Carry enough information out of the font lookup to tell whether the matched family is switched off.` / `details`: `Add the matched FontFamily (own key + enabled) and Font.status to the return of retrieveFontFamilyGroupWithFontVersionId. Both are already in scope in the :102-104 loop and dropped at return.`
@@ -111,12 +111,12 @@
111
111
  - estimated blast radius (units, configs, deployment manifests, data migrations)
112
112
  - trade-off matrix across options (rows = options, columns at minimum: complexity, risk, reversibility, test coverage cost, rollout cost)
113
113
  - recommended option with rationale tied to the design principles above
114
- - `recommendedOption.testSeams` is a mandatory array with one row per boundary a test injects at and replaces: `boundary`, `injectedAs`, and `replacedInTest`. An empty array is legal only with a defensible reason. `schemas/final-report-v1.0.schema.json` requires the array and all three row fields; the §5.5.9 `P-Var-*` round rejects a seam whose construction or wiring point is not actually replaceable.
114
+ - `recommendedOption.testSeams` is a mandatory array with one row per boundary a test injects at and replaces: `boundary`, `injectedAs`, and `replacedInTest`. An empty array is legal only with a defensible reason. `schemas/final-report-v2.0.schema.json` requires the array and all three row fields; the §5.5.9 `P-Var-*` round rejects a seam whose construction or wiring point is not actually replaceable.
115
115
  - Record non-blocking working assumptions inside the Recommended Option. A required answer belongs in a `Blocks=approval` clarification instead. Omit the assumptions only when there are none.
116
116
  - The YAML frontmatter carries `implementation-option:` directly under `approved:` so the user can select an Option Candidate after planning.
117
117
  - Required deliverable shape (final report, in addition to the standard sections):
118
118
  - In the selected-direction branch, `directionRealization` is the sole design payload. Its `fileStructure`, interfaces, blast radius, test seams, assumptions, and invariants concretize the snapshot without introducing another option or recommendation.
119
- - **Variation-point analysis (`variationPointAnalysis`, mandatory — every plan emits the block, rendered as §5.5.11):** declare `hasMultipleImplementations`, and when it is `true`, one `points[]` row per varying behavior carrying `behavior`, the two or more `implementations` that serve it, `evidence` (a `path:line`, or the sibling task / stage that already implements it), and an `extractionDecision` of `extract` / `interfaceKind` / `coveredBy` (the Stage Map stage that builds the interface) / `rationale`. **A `false` declaration is not an omission — it is a claim**, so it carries a written `noVariationRationale` and an empty `points` array; the two are mutually exclusive, because declared points would be silently dropped from verification under a `false` header. A project whose `.okstra/project.json` sets `architecture.style: hexagonal` extracts a point as a port (`interfaceKind: "port"`), never as a shared helper. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.VariationPointAnalysis` / `$defs.VariationPoint` make the block required and pin the row shape; `validators/validate-run.py` `_validate_variation_point_analysis` rejects a `false` declaration with no rationale, a `false` declaration carrying points, a `true` declaration with no point, an `extract: true` decision naming no `interfaceKind` or no `coveredBy`, and a hexagonal project extracting as anything but a port; and every point becomes a `P-Var-<N>` plan item judged in §5.5.9 (`prompts/lead/plan-body-verification.md`) — a plan declaring no variation point is still verified, through the lone `P-Var-0`.
119
+ - **Variation-point analysis (`variationPointAnalysis`, mandatory — every plan emits the block, rendered as §5.5.11):** declare `hasMultipleImplementations`, and when it is `true`, one `points[]` row per varying behavior carrying `behavior`, the two or more `implementations` that serve it, `evidence` (a `path:line`, or the sibling task / stage that already implements it), and an `extractionDecision` of `extract` / `interfaceKind` / `coveredBy` (the Stage Map stage that builds the interface) / `rationale`. **A `false` declaration is not an omission — it is a claim**, so it carries a written `noVariationRationale` and an empty `points` array; the two are mutually exclusive, because declared points would be silently dropped from verification under a `false` header. A project whose `.okstra/project.json` sets `architecture.style: hexagonal` extracts a point as a port (`interfaceKind: "port"`), never as a shared helper. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.VariationPointAnalysis` / `$defs.VariationPoint` make the block required and pin the row shape; `validators/validate-run.py` `_validate_variation_point_analysis` rejects a `false` declaration with no rationale, a `false` declaration carrying points, a `true` declaration with no point, an `extract: true` decision naming no `interfaceKind` or no `coveredBy`, and a hexagonal project extracting as anything but a port; and every point becomes a `P-Var-<N>` plan item judged in §5.5.9 (`prompts/lead/plan-body-verification.md`) — a plan declaring no variation point is still verified, through the lone `P-Var-0`.
120
120
  - **Stage Map (mandatory — always emitted, even when N=1):** a table of all stages with `stage | title | depends-on | step-count | exit-contract-summary`. `depends-on` is `(none)` or a comma-separated stage number list. Stages with `depends-on (none)` can be implemented in parallel by two simultaneous `implementation` runs.
121
121
  - **Keep the table at exactly 5 columns** — do NOT add a column. `validators/validate-implementation-plan-stages.py` parses `stage | title | depends-on | step-count | exit-contract-summary` and silently skips any row that is not exactly 5 cells, so a 6th column would drop every stage and bypass S2–S11.
122
122
  - **Multi-project plans only** (the plan's work spans more than one project — see the Project-boundary partition rule below): prefix each stage's `title` cell with a `[<project>]` tag (e.g. `[okstra] Add X`) so the project each stage belongs to is readable at a glance, and add exactly one line directly under the Stage Map table — `Cross-project parallelism: <which per-project stages run in parallel, which are sequenced, and the cross-project dependency that forces each sequencing>`. Single-project plans omit both the tag and the line.
@@ -132,6 +132,9 @@
132
132
  - **Per-stage subsections** (`## 5.5.<i> Stage <i>: <title>` for each `i`), each containing the four required subsections:
133
133
  - `### Carry-In` — for `depends-on (none)`: task-brief only. Otherwise: each depended-on stage's static exit contract + runtime sidecar path `runs/<impl-key>/carry/stage-<i>.json` placeholder.
134
134
  - `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | outcome | expected`. `outcome` is one word — `PASS` or `FAIL` — and `expected` is the sentence saying what that looks like here; a verdict written inside the sentence is not read as one. The `files` cell lists each touched path in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...`), which does not resolve and is rejected by plan-body verification as a kind-b path mismatch. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change (no lower time bound; it may span several files that change together); for code steps include actual code or diff sketch. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`outcome` = `FAIL`) — the RED step encodes the case set, not a single happy-path assertion; at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`outcome` = `PASS`); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section (mirrors the executor's per-step exemption in `_implementation-executor.md`). Validator S10c enforces RED-first + GREEN; the `outcome` cell agreeing with its `RED:` / `GREEN:` prefix is a schema conditional (`StageStepRow.allOf`), so a plan whose data.json says otherwise never reaches the validator. S10e rejects a `TDD exemption:` whose reason is not one of doc-only / config-only / pure-rename (both in `validators/validate-implementation-plan-stages.py`).
135
+ - **The `command` cell runs inside an okstra task worktree, not a bare checkout (BLOCKING).** okstra provisions `.okstra`, the configured sync entries (`.project-docs`, `.claude`, …), and — for `implementation` — a nested `stage-<N>/` worktree into the tree the step executes in. Two consequences bind every command you write:
136
+ - **Clean-tree assertions use `okstra worktree-status --check-clean`.** A bare `git status --porcelain` is never empty there, so an assertion built on one fails on okstra's scaffolding rather than on the stage's work. The okstra command asks the same question over source paths only and exits 1 when dirty, so it chains: `okstra worktree-status --check-clean && git tag -f stage-<N>-exit`. Validator S13 rejects the bare form.
137
+ - **Never read an `.okstra/` artifact back out of a git object.** `.okstra/**` is gitignored and never committed — the executor aborts a commit that stages an ignored path and the verifier reports a committed `.okstra` path as a branch defect — so `git cat-file -e <tag>:.okstra/…`, `git show <tag>:.okstra/…`, and every variant of that read can never resolve, at any tag, in any stage. A later stage that needs a QA artifact reads it from the working tree or receives it through the carry sidecar / verifier result; do not design a stage contract around one being reachable from a tag. Validator S12 rejects the read.
135
138
  - **Per-stage conformance declaration (mandatory one line, in the stage section — same placement freedom as `TDD exemption:`):** the stage MUST carry exactly one of:
136
139
  - `Conformance tests: stage-<N> — <task_root>/qa/scripts/stage-<N>.<ext> (requires=[db|io|http|external,...])` — a Tier3 verification script that proves this stage's upstream requirements (brief / requirements-discovery / error-analysis / improvement-discovery → this stage's `Acceptance`) hold against **real** DB rows, real endpoints, or the real external API — NOT mocks. When you emit this line you MUST also (a) write the script to `<task_root>/qa/scripts/stage-<N>.<ext>` and (b) add a matching entry to `<task_root>/qa/conformance-manifest.json` with fields `stageKey` (= `<task-id>-stage-<N>`), `script`, `runCommand`, `requirementIds`, `requires` (subset of `{db, io, http, external}`), `passContract`, `exemption: null`, `waiver: null`. The script's standard interface: a `main` that exits `0`=PASS / non-zero=FAIL, and whose stdout ends with `QA-RESULT: PASS|FAIL` followed by one `REQ <id>: PASS|FAIL: <reason>` line per requirement. When the verification body is a test spec, author it with the project's own test framework (devDependency) invoked via a discovery override at `<task_root>/qa/scripts/` (jest: `--config <project config> --roots <task_root>/qa/scripts`) — never hand-roll `describe`/`expect` and never widen the project's own test config; for TypeScript specs also write `<task_root>/qa/scripts/tsconfig.json` extending the project tsconfig with the runner's `types` entry so editors resolve the file.
137
140
  - `Conformance exemption: <reason>` — only for stages that touch no db/io/http/external surface, or where unit tests fully cover the increment. (If the eventual `implementation` diff actually touches one of those surfaces, `validate-run.py`'s diff-surface cross-check is BLOCKING — an exemption cannot hide a real db/io/http/external change.)
@@ -16,7 +16,8 @@
16
16
  - Executor model: `{{EXECUTOR_MODEL_DISPLAY}}` (CLI launch value: `{{EXECUTOR_MODEL_EXECUTION_VALUE}}`; host-native launch value: `{{EXECUTOR_HOST_MODEL_VALUE}}`)
17
17
  - Executor runner: `{{EXECUTOR_RUNNER}}`
18
18
  - Executor dispatch mode: `{{EXECUTOR_DISPATCH_MODE}}`
19
- - Wherever this profile mentions the `Executor`, it refers to the role bound above. **Every** analysis provider in the resolved roster is also dispatched as a verifier — including the executor's own provider, which runs *separately* as a fresh session with no shared context so no verdict comes from the session that wrote the diff (`_implementation-verifier.md` owns this rule). Verifier dispatches remain strictly read-only.
19
+ - Wherever this profile mentions the `Executor`, it refers to the role bound above. Every analysis provider in the resolved roster **except `{{EXECUTOR_WORKER_ID}}`** is dispatched as a verifier, each in a fresh session with no shared context, so no verdict comes from the session that wrote the diff (`_implementation-verifier.md` owns this rule). The executor's own provider gets **no** second, verifier-role session in the current plumbing: `worker_prompt_policy` materializes that worker ID as the executor on every dispatch, so there is no way to ask for one — treat that as a follow-up design item, never as something to work around from the lead. Verifier dispatches remain strictly read-only.
20
+ - **Dispatch order (BLOCKING).** Executor and verifiers are **two** dispatches, never one batch — a batch holding both makes every verifier observe base HEAD instead of the stage diff. Dispatch the executor alone, settle it with `okstra team await`, then dispatch the verifiers with a `--workers` list that **omits `{{EXECUTOR_WORKER_ID}}`**: that worker ID is materialized as the executor on every dispatch, so a second batch still carrying it is refused again. **Enforcement:** `scripts/okstra_ctl/dispatch_core.py` `_validate_implementation_phase_order` refuses such a batch, in `--dry-run` as well as in a real dispatch.
20
21
  {{INCLUDE:_common-contract.md}}
21
22
  {{INCLUDE:_stage-discipline.md}}
22
23
  - Pre-implementation gate (mandatory — refuse to start if any item fails):
@@ -9,7 +9,7 @@
9
9
  - Lead-only contract (replaces the shared team contract for this phase):
10
10
  - The host-native Okstra lead is the sole agent for this run. No worker dispatch, no teammates, no parallel sub-agents, no convergence loop.
11
11
  - The lead drafts the PR title and PR body **inline** by reading the run brief, the cited final-verification report, `git log --oneline <base>..HEAD`, and `git diff <base>..HEAD --stat`. No drafter worker is dispatched.
12
- - The lead authors the final-report file directly (no `Report writer worker` dispatch). The report still conforms to the standard `templates/reports/final-report.template.md` structure, including the `## 5.6 Release Handoff Deliverables` section.
12
+ - The lead authors the final-report file directly (no `Report writer worker` dispatch). The report still conforms to the standard `templates/reports/final-report-v2.template.md` structure, including the `## 5.6 Release Handoff Deliverables` section.
13
13
  - The shared anti-escalation rule from the common contract still applies: do not start any other lifecycle phase from inside this run.
14
14
  - 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.
15
15
  - The shared "MCP read-only" rule still applies if the brief lists MCP servers, though most release-handoff runs do not use MCP.
@@ -39,16 +39,11 @@ def default_worker_dispatch_port(
39
39
  if provider_registry is not None
40
40
  else None
41
41
  )
42
- default_backend = (
43
- dispatch_core.BACKEND_TMUX_PANE
44
- if descriptor.launch_mode == "team"
45
- else dispatch_core.BACKEND_CLI_WRAPPER
46
- )
47
42
  return CliWrapperDispatchPort(
48
43
  supported_worker_wrappers=wrappers,
49
44
  unsupported_worker_label=f"{descriptor.id} lead",
50
45
  required_lead_runtime=descriptor.id,
51
- default_backend=default_backend,
46
+ default_backend=dispatch_core.BACKEND_CLI_WRAPPER,
52
47
  )
53
48
 
54
49