okstra 0.166.1 → 0.167.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 +1 -1
- package/docs/cli.md +1 -1
- package/docs/pr-template-usage.md +3 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +1 -1
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +1 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +13 -0
- package/runtime/python/okstra_ctl/consumers.py +26 -8
- package/runtime/python/okstra_ctl/dispatch_core.py +47 -0
- package/runtime/python/okstra_ctl/dispatch_state.py +6 -0
- package/runtime/python/okstra_ctl/pr_template.py +1 -1
- package/runtime/python/okstra_ctl/team.py +2 -1
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +6 -0
- package/runtime/schemas/final-report-v2.0.schema.json +102 -1
- package/runtime/skills/okstra-setup/references/project-config.md +1 -1
- package/runtime/templates/reports/final-report.template.md +17 -2
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +2 -1
- package/runtime/validators/validate-run.py +56 -0
package/README.md
CHANGED
|
@@ -225,7 +225,7 @@ Major workflow changes added to `main` after 0.8.0:
|
|
|
225
225
|
|
|
226
226
|
- **Automatic isolated worktrees for every task type** — During preparation, `okstra-ctl` runs `git worktree add ~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>` once per task key to create an isolated working tree and a `<work-category-namespace>/<task-id-segment>` branch (for example, `feature/dev-9436` or `fix/dev-7311`). The user chooses the base ref with `--base-ref`, using the same choices as the release-handoff PR base picker: `main`, `dev`, `staging`, `preprod`, `prod`, or a custom value. It is required in the first phase; the okstra-run skill collects it through `AskUserQuestion`, while non-interactive callers must pass `--base-ref` explicitly. Later **non-`implementation`** phases for the same task key (`requirements-discovery` → `error-analysis` → `implementation-planning` → `final-verification` → `release-handoff`) reuse the same path and branch. `implementation` runs are **stage-isolated**: each run executes one stage in its own `.../<task>/stage-<N>/` worktree on a `<work-category-namespace>/<task>-s<N>` branch, so independent stages with `depends-on (none)` can run concurrently without sharing a tree. The registry reserves both task keys and **stage keys** with flock. Provisioning is skipped when the caller is already in another worktree or project_root is not a Git repository; stage isolation degrades to a flat path in those cases. Manual cleanup: `git worktree remove <path>` → `git branch -D <branch>` plus release/removal of the registry entry. Details: [`docs/architecture.md`](docs/architecture.md), in the *Task type* section, and [`docs/cli.md#--executor`](docs/cli.md#--executor).
|
|
227
227
|
- **`release-handoff` lifecycle phase** — runs immediately after `final-verification` returns `verdict=accepted`. The current Okstra lead drafts the candidate messages and PR body inline, then uses the selected host adapter's user-prompt operation for the delivery choices. Only the Git/GitHub CLI commands selected through those menus are run. Force pushes, direct pushes to the base branch, hook bypasses (`--no-verify`), and release publication (`gh release`, `npm publish`, and similar commands) are prohibited. This phase does not edit source code. Profile: [`prompts/profiles/release-handoff.md`](prompts/profiles/release-handoff.md).
|
|
228
|
-
- **Configurable PR body template** (release-handoff) — PR bodies are populated from a Markdown template selected in this order: one-time override (`--pr-template-path` or the okstra-run Step 6 prompt) → `prTemplatePath` in `<project_root>/.okstra/project.json` → `prTemplatePath` in `~/.okstra/config.json` → the skill default at `~/.claude/skills/templates/
|
|
228
|
+
- **Configurable PR body template** (release-handoff) — PR bodies are populated from a Markdown template selected in this order: one-time override (`--pr-template-path` or the okstra-run Step 6 prompt) → `prTemplatePath` in `<project_root>/.okstra/project.json` → `prTemplatePath` in `~/.okstra/config.json` → the skill default at `~/.claude/skills/templates/pr/pr-body.template.md`. Register a template with `okstra config set pr-template-path <path> [--scope project|global]`; project scope accepts a path relative to the project root, while global scope requires an absolute path or a path beginning with `~/`. `okstra config get pr-template-path --scope all` prints every scoped value and the effective winner. The default template contains `## Summary`, `## Changes`, `## Test plan`, and `## Linked issues`, plus HTML comment guidance that the lead removes immediately before PR creation.
|
|
229
229
|
- **Profile worker-roster validation** — `--workers <csv>` and the okstra-run Step 6 worker prompt accept only the worker IDs declared in the selected profile's `Required workers:` block. Requesting a worker absent from the profile—for example, `codex` or `antigravity` for `release-handoff`—fails with a clear error, and the interactive prompt shows only workers accepted by that profile.
|
|
230
230
|
- **Host-aware lead adapters** — `okstra-run` resolves the current harness through the same dynamic host registry used by the terminal front door. Claude Code, Codex, Antigravity, Grok, and Kimi keep their matching provider assignment native; `external` remains the explicit all-CLI host. Host and provider are separate axes, and every non-native worker assignment uses its provider's registered CLI wrapper. `leadAssignment` and every `workerAssignments[]` row record provider, model, and `runner`. `okstra codex-run` and `okstra codex-dispatch` remain low-level artifact/dispatch commands.
|
|
231
231
|
- **Multi-stage `implementation-planning` / `implementation`** — `implementation-planning` always produces a Stage Map and N stage sections. Each stage has no more than six steps, and stages with `depends-on (none)` can be implemented concurrently in separate `implementation` runs. Each `implementation` invocation runs a single stage, selected with `--stage <auto|N>`, and creates an evidence sidecar at `carry/stage-<N>.json` for automatic carry-in to the next stage. The `implementation-planning` run directory accumulates `consumers.jsonl` reverse links that record which run consumed each stage.
|
package/docs/cli.md
CHANGED
|
@@ -130,7 +130,7 @@ For standard values and phase-specific responsibilities, see [Task type](#--task
|
|
|
130
130
|
- `out-of-scope`: optional.
|
|
131
131
|
- `candidate-cap`: 1–12; default 8.
|
|
132
132
|
- Output: the `## 5.9 Improvement Candidates` table with 11 columns: Cand ID / Lens / Title / Scope / Severity / Effort / Consensus / Source workers / Recommended next-phase / Expected behavior after / Evidence.
|
|
133
|
-
- Verdict Token: `candidates-ready` / `no-candidates`
|
|
133
|
+
- Verdict Token: `analysis-complete` / `analysis-partial` / `blocked` — the shared analysis enum, which is what `schemas/final-report-v2.0.schema.json` admits. Finding no candidates is not a verdict: the run stays `analysis-complete` and records an empty candidate set with a `no-candidate` row per lens. (`candidates-ready` / `no-candidates` appear only in a schema-v1 legacy report's `## 7. Final Verdict`.)
|
|
134
134
|
- Routing: there is no automatic spin-off. The user selects candidates and starts each under a new task ID with `requirements-discovery`, `implementation-planning`, or `error-analysis`.
|
|
135
135
|
- Workers: claude + codex + antigravity + report-writer are all required.
|
|
136
136
|
- Primary-pass assignment: selected analyser instances are enumerated in `requiredWorkerRoles` order, then the lead rotates the primary pass across the resolved priority lenses. Provider/model names do not affect the order, and every analyser still covers every resolved lens after its primary pass.
|
|
@@ -19,8 +19,8 @@ If the file named in any of the 4 steps does not exist, it fails immediately wit
|
|
|
19
19
|
|
|
20
20
|
## 2. Default candidate paths
|
|
21
21
|
|
|
22
|
-
1. `$OKSTRA_SKILLS_DIR/templates/
|
|
23
|
-
2. `~/.claude/skills/templates/
|
|
22
|
+
1. `$OKSTRA_SKILLS_DIR/templates/pr/pr-body.template.md` — only when the `OKSTRA_SKILLS_DIR` environment variable is set.
|
|
23
|
+
2. `~/.claude/skills/templates/pr/pr-body.template.md` — the standard location `npx okstra install` installs to.
|
|
24
24
|
|
|
25
25
|
The candidates are tried in priority order, and if all are absent it ends with an explicit error as follows.
|
|
26
26
|
|
|
@@ -28,7 +28,7 @@ The candidates are tried in priority order, and if all are absent it ends with a
|
|
|
28
28
|
|
|
29
29
|
## 3. The original inside the source repository
|
|
30
30
|
|
|
31
|
-
- [`templates/
|
|
31
|
+
- [`templates/pr/pr-body.template.md`](../templates/pr/pr-body.template.md) — the original that `npx okstra install` copies to the §2 default location. To change the copy, edit this file and install again.
|
|
32
32
|
|
|
33
33
|
## 4. Configuration commands — persistence
|
|
34
34
|
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -63,7 +63,7 @@ persisted prompt lacks the heading `Coding-conventions preflight`
|
|
|
63
63
|
|
|
64
64
|
## Stage execution contract (this run owns one stage)
|
|
65
65
|
|
|
66
|
-
- **Sidecar evidence writer (BLOCKING).** When this stage's Stage Validation `post` commands all succeed, the Executor MUST emit a JSON object with **exactly** these fields (spec `docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md` §3.2), as a fenced ```json``` block in the worker result under the heading `### Stage Carry Evidence`, and the lead MUST persist it to `runs/<impl-task-key>/carry/stage-<N>.json`. The schema is inlined here because a CLI-wrapper executor runs in its own process and never receives a native worker definition — a carry emitted with an ad-hoc shape (`stage`/`files`/`validation`) is a `contract-violated` gap the lead must rewrite by hand:
|
|
66
|
+
- **Sidecar evidence writer (BLOCKING).** You emit the evidence; you do NOT write the file. `carry/stage-<N>.json` sits inside your task scope, so nothing stops your tools from creating it — but the lead's post-stage persistence is what writes it, and only on a non-`FAIL` verdict. Your own gate passes before the verifier's, so a file you write exists exactly when the verdict may still withhold it: the lead then refuses to overwrite, the report says the sidecar was withheld, and the disk disagrees with the record. Emit the block and stop. When this stage's Stage Validation `post` commands all succeed, the Executor MUST emit a JSON object with **exactly** these fields (spec `docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md` §3.2), as a fenced ```json``` block in the worker result under the heading `### Stage Carry Evidence`, and the lead MUST persist it to `runs/<impl-task-key>/carry/stage-<N>.json`. The schema is inlined here because a CLI-wrapper executor runs in its own process and never receives a native worker definition — a carry emitted with an ad-hoc shape (`stage`/`files`/`validation`) is a `contract-violated` gap the lead must rewrite by hand:
|
|
67
67
|
|
|
68
68
|
```json
|
|
69
69
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
- v1 legacy branch: when validating or rerendering an existing schema-v1 report, preserve its `## 5.9 Improvement Candidates` table and legacy Markdown contract; do not rewrite that historical data into v2 implicitly.
|
|
55
55
|
- the `## 5.9 Improvement Candidates` table populated with rows that obey the 11-column schema from `validators/validate_improvement_report.py` (Cand ID `I-NNN`, Lens from whitelist, Title, Scope ⊆ scan-scope, Severity, Effort, Consensus, Source workers `<worker>:<id>` from {claude, codex, antigravity}, Recommended next-phase ∈ {requirements-discovery, implementation-planning, error-analysis}, Expected behavior after, Evidence as path:line list). `Expected behavior after` states, in one observable sentence, what becomes different once the candidate is applied — it is the seed of the downstream brief's `EB-NNN` / `EO-NNN`. A candidate you cannot write this cell for is a preference, not a finding: drop it rather than filling the cell with a restatement of the title.
|
|
56
56
|
- `Consensus` cells in `## 5.9 Improvement Candidates` use the table enum exactly: `full`, `partial`, `contested`, `worker-unique`. Map convergence's `full-consensus` / `partial-consensus` labels to `full` / `partial` before writing the table.
|
|
57
|
-
-
|
|
57
|
+
- Verdict Token — **branch-specific, and the two branches do not share a vocabulary.** On the current v2 branch use the shared analysis enum: `analysis-complete` when every resolved lens was examined, `analysis-partial` when one could not be, `blocked` when the scan itself could not run. `schemas/final-report-v2.0.schema.json` admits only those three for `verdictCard.verdictToken` / `finalVerdict.verdictToken`, so a v2 report carrying `candidates-ready` fails Phase 7. **Finding no candidates is not a verdict**: it is an empty `candidates[]` plus a `lensCoverage[]` row per lens with `status: no-candidate` and its evidence-backed rationale — the verdict stays `analysis-complete`. `candidates-ready` / `no-candidates` belong to the v1 legacy `## 7. Final Verdict` Markdown alone, where `validators/validate_improvement_report.py` enforces them. Both branches: Direction `routing`; Next Step "ask the user to select K candidates (see the ## 5.9 table)".
|
|
58
58
|
- `## 3. Recommended Next Steps` first entry summarises per-candidate routing and proposes new task-key names of the form `<task-group>/imp-<Cand-ID>`
|
|
59
59
|
- author the shared schema-v2 report fields plus `improvementDiscovery.candidates[]`, `improvementDiscovery.lensCoverage[]`, `improvementDiscovery.selectionLimit`, and `improvementDiscovery.userNarrative` in data.json. `candidates[]` carries the same 11 logical fields described above; `lensCoverage[]` records either candidate IDs or an evidence-backed no-candidate rationale for every resolved lens. `schemas/final-report-v2.0.schema.json` and `validators/validate_improvement_report.py` enforce this contract. The renderers independently derive AI handoff Markdown and human HTML; never author a free-form report.
|
|
60
60
|
- Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
|
|
@@ -77,7 +77,7 @@ Render every numbered item as its option label followed by its description verba
|
|
|
77
77
|
| `read_artifacts` | Read the manifest-provided paths through the current Antigravity host file interface. |
|
|
78
78
|
| `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
|
|
79
79
|
| `prompt_user` | Ask through the current host text/question interface and stop at approval gates until an explicit answer arrives. |
|
|
80
|
-
| `dispatch_worker` | Dispatch every `runner=native-session` Antigravity assignment through the current host. Dispatch every `runner=cli-wrapper` assignment through its registered provider wrapper. |
|
|
80
|
+
| `dispatch_worker` | Dispatch every `runner=native-session` Antigravity assignment through the current host. Dispatch every `runner=cli-wrapper` assignment through its registered provider wrapper. **Not in a cmux run:** when the run manifest's `terminalBackend` is `cmux-pane`, `prompts/lead/adapters/cmux.md` overrides this row — start every worker with `okstra team dispatch`, this host's native path included, so okstra owns the panes and the user can watch the work. |
|
|
81
81
|
| `await_workers` | Await native host workers through the host primitive and CLI workers through their status sidecars, then verify terminal state and Result Paths. |
|
|
82
82
|
| `redispatch_worker` | Start a fresh native worker or CLI wrapper attempt according to the persisted assignment and record the supplied dispatch kind. |
|
|
83
83
|
| `shutdown_workers` | Perform host or process cleanup only for resources owned by this run. |
|
|
@@ -136,7 +136,7 @@ For a `host-text` mapping, render each numbered item as its option label followe
|
|
|
136
136
|
| `read_artifacts` | Use the host file-read primitive and preserve the core contract's read order. |
|
|
137
137
|
| `write_artifact` | Use the host file-write primitive only for paths authorized by the active lifecycle phase. |
|
|
138
138
|
| `prompt_user` | Use the native question tool for approvals and clarifications; do not infer an answer from silence. |
|
|
139
|
-
| `dispatch_worker` | Dispatch each assignment through `Agent(name: "<role>", run_in_background: true)` without `team_name`; use an in-process worker for `runner=native-session` and the assigned provider's wrapper worker for `runner=cli-wrapper`. |
|
|
139
|
+
| `dispatch_worker` | Dispatch each assignment through `Agent(name: "<role>", run_in_background: true)` without `team_name`; use an in-process worker for `runner=native-session` and the assigned provider's wrapper worker for `runner=cli-wrapper`. **Not in a cmux run:** when the run manifest's `terminalBackend` is `cmux-pane`, `prompts/lead/adapters/cmux.md` overrides this row — start every worker with `okstra team dispatch`, this host's native path included, so okstra owns the panes and the user can watch the work. |
|
|
140
140
|
| `await_workers` | Arm one background shell poll for the pending Result Paths; the spawn acknowledgement is not completion. |
|
|
141
141
|
| `redispatch_worker` | Dispatch a fresh `Agent(...)` session with the same prompt plus the core reverify/retry reason. |
|
|
142
142
|
| `shutdown_workers` | For each confirmed-complete worker selected for cleanup, send `SendMessage(to: <name>, message: { type: "shutdown_request" })` to idle the roster member **and** call `TaskStop(task_id: "<name>")` to stop its background task. Both are required; neither subsumes the other. |
|
|
@@ -77,7 +77,7 @@ Render every numbered item as its option label followed by its description verba
|
|
|
77
77
|
| `read_artifacts` | Read the manifest-provided paths through the current host's file interface. |
|
|
78
78
|
| `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
|
|
79
79
|
| `prompt_user` | Ask through the host text/question interface and stop at approval gates until an explicit answer arrives. |
|
|
80
|
-
| `dispatch_worker` | Dispatch every `runner=native-session` assignment with the current Codex host's native worker/session primitive. Pass only `runner=cli-wrapper` assignments to `okstra codex-dispatch --project-root <root> --run-manifest <path> --workers <ids>`; use `--dry-run` first when the core requires a dispatch preview. |
|
|
80
|
+
| `dispatch_worker` | Dispatch every `runner=native-session` assignment with the current Codex host's native worker/session primitive. Pass only `runner=cli-wrapper` assignments to `okstra codex-dispatch --project-root <root> --run-manifest <path> --workers <ids>`; use `--dry-run` first when the core requires a dispatch preview. **Not in a cmux run:** when the run manifest's `terminalBackend` is `cmux-pane`, `prompts/lead/adapters/cmux.md` overrides this row — start every worker with `okstra team dispatch`, this host's native path included, so okstra owns the panes and the user can watch the work. |
|
|
81
81
|
| `await_workers` | Await native host workers through the host primitive and CLI workers through synchronous dispatch, then verify team-state terminal records and Result Paths for both. |
|
|
82
82
|
| `redispatch_worker` | Start a fresh native worker or `okstra codex-dispatch` attempt according to the persisted assignment's `runner`, and record the retry/reverify dispatch kind. |
|
|
83
83
|
| `shutdown_workers` | Perform process cleanup when a wrapper remains live; otherwise this operation is a no-op recorded in state. |
|
|
@@ -77,7 +77,7 @@ Render every numbered item as its option label followed by its description verba
|
|
|
77
77
|
| `read_artifacts` | Read the manifest-provided paths through the current host's file or shell interface. |
|
|
78
78
|
| `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
|
|
79
79
|
| `prompt_user` | Ask through the host text/question interface and require an explicit approval or clarification response. |
|
|
80
|
-
| `dispatch_worker` | Run `okstra team dispatch --project-root <root> --run-manifest <path>`; use `--dry-run` first when the core requires a dispatch preview. |
|
|
80
|
+
| `dispatch_worker` | Run `okstra team dispatch --project-root <root> --run-manifest <path>`; use `--dry-run` first when the core requires a dispatch preview. **Not in a cmux run:** when the run manifest's `terminalBackend` is `cmux-pane`, `prompts/lead/adapters/cmux.md` overrides this row — start every worker with `okstra team dispatch`, this host's native path included, so okstra owns the panes and the user can watch the work. |
|
|
81
81
|
| `await_workers` | Run `okstra team await --project-root <root> --run-manifest <path>` through the host's asynchronous shell facility. |
|
|
82
82
|
| `redispatch_worker` | Create the core-specified fresh jobs file and dispatch it with a new `dispatchKind`; never reuse a live worker conversation. |
|
|
83
83
|
| `shutdown_workers` | Run `okstra team teardown --project-root <root> --run-manifest <path>` only after the user-approved cleanup gate. |
|
|
@@ -76,7 +76,7 @@ Render every numbered item as its option label followed by its description verba
|
|
|
76
76
|
| `read_artifacts` | Read the manifest-provided paths through the current Grok host file interface. |
|
|
77
77
|
| `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
|
|
78
78
|
| `prompt_user` | Ask through the current host text interface and wait for an explicit answer. |
|
|
79
|
-
| `dispatch_worker` | Follow each persisted assignment's `runner` and use the common host dispatch boundary. |
|
|
79
|
+
| `dispatch_worker` | Follow each persisted assignment's `runner` and use the common host dispatch boundary. **Not in a cmux run:** when the run manifest's `terminalBackend` is `cmux-pane`, `prompts/lead/adapters/cmux.md` overrides this row — start every worker with `okstra team dispatch`, this host's native path included, so okstra owns the panes and the user can watch the work. |
|
|
80
80
|
| `await_workers` | Await through the selected common dispatch backend, then verify terminal state and Result Paths. |
|
|
81
81
|
| `redispatch_worker` | Start a fresh attempt from the persisted assignment and record the supplied dispatch kind. |
|
|
82
82
|
| `shutdown_workers` | Clean up only host or process resources owned by this run. |
|
|
@@ -76,7 +76,7 @@ Render every numbered item as its option label followed by its description verba
|
|
|
76
76
|
| `read_artifacts` | Read the manifest-provided paths through the current Kimi host file interface. |
|
|
77
77
|
| `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
|
|
78
78
|
| `prompt_user` | Ask through the current host text interface and wait for an explicit answer. |
|
|
79
|
-
| `dispatch_worker` | Follow each persisted assignment's `runner` and use the common host dispatch boundary. |
|
|
79
|
+
| `dispatch_worker` | Follow each persisted assignment's `runner` and use the common host dispatch boundary. **Not in a cmux run:** when the run manifest's `terminalBackend` is `cmux-pane`, `prompts/lead/adapters/cmux.md` overrides this row — start every worker with `okstra team dispatch`, this host's native path included, so okstra owns the panes and the user can watch the work. |
|
|
80
80
|
| `await_workers` | Await through the selected common dispatch backend, then verify terminal state and Result Paths. |
|
|
81
81
|
| `redispatch_worker` | Start a fresh attempt from the persisted assignment and record the supplied dispatch kind. |
|
|
82
82
|
| `shutdown_workers` | Clean up only host or process resources owned by this run. |
|
|
@@ -44,7 +44,13 @@ PROFILE_SECTIONS = (
|
|
|
44
44
|
"Non-goals",
|
|
45
45
|
)
|
|
46
46
|
WORKER_PROFILE_SECTIONS_BY_TASK_TYPE = {
|
|
47
|
+
# `Brief consumption` carries the reporter-confirmation rules — do not infer an
|
|
48
|
+
# unconfirmed `intent-check:` / `conversion-block:`, carry it forward as a
|
|
49
|
+
# blocker instead. Those bind the analysis worker, and the packet is the only
|
|
50
|
+
# profile text a worker receives, so a phase that omits the section ships a
|
|
51
|
+
# worker that can turn an unconfirmed intent into a settled requirement.
|
|
47
52
|
"requirements-discovery": (
|
|
53
|
+
"Brief consumption",
|
|
48
54
|
"Worker discovery procedure",
|
|
49
55
|
),
|
|
50
56
|
"error-analysis": (
|
|
@@ -52,6 +58,7 @@ WORKER_PROFILE_SECTIONS_BY_TASK_TYPE = {
|
|
|
52
58
|
"Worker diagnosis procedure",
|
|
53
59
|
),
|
|
54
60
|
"implementation-planning": (
|
|
61
|
+
"Brief consumption",
|
|
55
62
|
"Worker planning procedure",
|
|
56
63
|
"Pre-planning context exploration",
|
|
57
64
|
"Design principles applied when scoring options",
|
|
@@ -60,7 +67,13 @@ WORKER_PROFILE_SECTIONS_BY_TASK_TYPE = {
|
|
|
60
67
|
"Worker verification procedure",
|
|
61
68
|
),
|
|
62
69
|
"improvement-discovery": (
|
|
70
|
+
"Brief consumption",
|
|
63
71
|
"Worker candidate procedure",
|
|
72
|
+
# Every analyser examines every resolved lens but leads with its own, and
|
|
73
|
+
# keeps duplicate / conflicting relations. Held only in the lead's profile,
|
|
74
|
+
# the workers converge on the same easy lens and the cross-check thins out
|
|
75
|
+
# while the aggregate still looks complete.
|
|
76
|
+
"Worker diversity rule",
|
|
64
77
|
),
|
|
65
78
|
}
|
|
66
79
|
CLARIFICATION_SECTIONS = (
|
|
@@ -27,6 +27,11 @@ CONSUMERS_FILENAME = "consumers.jsonl"
|
|
|
27
27
|
# carrying a confirmed regression as complete.
|
|
28
28
|
STAGE_LIFECYCLE_STATUSES = ("started", "done", "failed")
|
|
29
29
|
|
|
30
|
+
# The lead has already ruled on a stage whose last row is one of these — `done`
|
|
31
|
+
# closed it, `failed` deliberately did not. Neither may be re-derived from disk:
|
|
32
|
+
# closing a stage is the lead's verdict gate, not a file's existence.
|
|
33
|
+
_LEAD_SETTLED_STATUSES = ("done", "failed")
|
|
34
|
+
|
|
30
35
|
|
|
31
36
|
@dataclass(frozen=True)
|
|
32
37
|
class StageConsumerState:
|
|
@@ -308,16 +313,29 @@ def _carry_dir(plan_run_root: Path) -> Path:
|
|
|
308
313
|
def backfill_done_from_carry(plan_run_root: Path) -> int:
|
|
309
314
|
"""Recover missing `done` rows from carry sidecars (carry is SSOT).
|
|
310
315
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
Recovers the crash window between the lead writing the carry file and
|
|
317
|
+
appending the `done` row: for every `runs/implementation/carry/stage-<N>.json`
|
|
318
|
+
that is complete and whose stage has no terminal row yet, append a `done` row
|
|
319
|
+
with the head commit read from the carry. Returns the number of rows
|
|
320
|
+
recovered. Stages with no carry or an unfinished carry are skipped, so the
|
|
321
|
+
dependency gate still legitimately blocks genuinely-unstarted stages.
|
|
322
|
+
|
|
323
|
+
A stage the lead has already ruled on is never re-derived from disk. That
|
|
324
|
+
includes `failed`: the executor emits its carry evidence when its own Tier 1/2
|
|
325
|
+
validation passes, which happens BEFORE the verifier's gate, so a carry file
|
|
326
|
+
exists for exactly the runs where the verifier then returned `FAIL`. Reading
|
|
327
|
+
the file as completion would promote a stage carrying a confirmed regression
|
|
328
|
+
to `done` and let its dependents proceed — and it would contradict this
|
|
329
|
+
module's own rule that a stage's position is its last lifecycle row."""
|
|
316
330
|
carry_dir = _carry_dir(plan_run_root)
|
|
317
331
|
if not carry_dir.is_dir():
|
|
318
332
|
return 0
|
|
319
333
|
existing = read_consumers(plan_run_root)
|
|
320
|
-
|
|
334
|
+
settled_stages = {
|
|
335
|
+
stage
|
|
336
|
+
for stage, status in last_lifecycle_status_by_stage(existing).items()
|
|
337
|
+
if status in _LEAD_SETTLED_STATUSES
|
|
338
|
+
}
|
|
321
339
|
key_by_stage: Dict[Any, str] = {}
|
|
322
340
|
fallback_key = ""
|
|
323
341
|
for r in existing:
|
|
@@ -335,7 +353,7 @@ def backfill_done_from_carry(plan_run_root: Path) -> int:
|
|
|
335
353
|
if not isinstance(carry, dict):
|
|
336
354
|
continue
|
|
337
355
|
stage = _carry_stage_number(carry, cf.name)
|
|
338
|
-
if stage is None or stage in
|
|
356
|
+
if stage is None or stage in settled_stages:
|
|
339
357
|
continue
|
|
340
358
|
if not _carry_is_complete(carry):
|
|
341
359
|
continue
|
|
@@ -358,6 +376,6 @@ def backfill_done_from_carry(plan_run_root: Path) -> int:
|
|
|
358
376
|
carry_path=carry_path,
|
|
359
377
|
source="carry-backfill",
|
|
360
378
|
)
|
|
361
|
-
|
|
379
|
+
settled_stages.add(stage)
|
|
362
380
|
recovered += 1
|
|
363
381
|
return recovered
|
|
@@ -31,6 +31,7 @@ from .dispatch_state import (
|
|
|
31
31
|
transition_worker_status as _transition_worker_status,
|
|
32
32
|
string_list as _string_list,
|
|
33
33
|
string_value as _string_value,
|
|
34
|
+
TEARDOWN_BEFORE_TERMINAL_REASON,
|
|
34
35
|
utc_now as _utc_now,
|
|
35
36
|
validate_dispatch_prompts as _validate_dispatch_prompts,
|
|
36
37
|
worker_jobs_from_file as _worker_jobs_from_file,
|
|
@@ -231,6 +232,7 @@ def await_dispatches(
|
|
|
231
232
|
timeout_seconds: int | None = None,
|
|
232
233
|
heartbeat_seconds: int = 30,
|
|
233
234
|
) -> int:
|
|
235
|
+
_correct_teardown_marked_dispatches(plan)
|
|
234
236
|
deadline = time.monotonic() + timeout_seconds if timeout_seconds is not None else None
|
|
235
237
|
last_heartbeat = 0.0
|
|
236
238
|
while True:
|
|
@@ -643,6 +645,35 @@ def _outcome_from_completed(handle: WorkerHandle) -> WorkerOutcome:
|
|
|
643
645
|
)
|
|
644
646
|
|
|
645
647
|
|
|
648
|
+
def _correct_teardown_marked_dispatches(plan: DispatchPlan) -> None:
|
|
649
|
+
"""Let the wrapper's own exit settle a record teardown wrote off.
|
|
650
|
+
|
|
651
|
+
`okstra team teardown` marks every non-terminal dispatch `error` — right for a
|
|
652
|
+
worker it killed, wrong for one that had already exited while its record was
|
|
653
|
+
still `running`, which happens when the lead awaited by polling artifacts
|
|
654
|
+
instead of `team await` (the component that transitions the record). The run
|
|
655
|
+
then reports `error` for workers whose wrapper exited 0 with every output in
|
|
656
|
+
place, and nothing could undo it: teardown's `error` drops the record out of
|
|
657
|
+
the running set this function's caller scans, so a later await never saw it.
|
|
658
|
+
|
|
659
|
+
Only a success is corrected, and only from process-level evidence — the
|
|
660
|
+
wrapper's terminal status plus its exit code, judged by the same
|
|
661
|
+
`_outcome_from_status` path every other dispatch goes through. A non-zero exit
|
|
662
|
+
or a missing output stays an error, and no retry is spawned: teardown has
|
|
663
|
+
already taken the panes, so re-running here would start work nobody is
|
|
664
|
+
watching.
|
|
665
|
+
"""
|
|
666
|
+
for record in _teardown_marked_dispatches(plan.team_state_path):
|
|
667
|
+
status_path = _optional_path(record.get("statusSidecarPath"))
|
|
668
|
+
status = read_wrapper_status(status_path) if status_path else None
|
|
669
|
+
if status is None or not status.is_terminal:
|
|
670
|
+
continue
|
|
671
|
+
outcome = _outcome_from_status(record, status)
|
|
672
|
+
if outcome.returncode != 0 or outcome.missing_completion_paths or outcome.timeout:
|
|
673
|
+
continue
|
|
674
|
+
_finish_record(plan, record, outcome)
|
|
675
|
+
|
|
676
|
+
|
|
646
677
|
def _advance_running_dispatches(plan: DispatchPlan, records: Sequence[Mapping[str, Any]]) -> None:
|
|
647
678
|
for record in records:
|
|
648
679
|
status_path = _optional_path(record.get("statusSidecarPath"))
|
|
@@ -811,6 +842,22 @@ def _update_dispatch_status(
|
|
|
811
842
|
_write_json(team_state_path, payload)
|
|
812
843
|
|
|
813
844
|
|
|
845
|
+
def _teardown_marked_dispatches(team_state_path: Path) -> list[Mapping[str, Any]]:
|
|
846
|
+
"""Records `okstra team teardown` wrote off, matched by its own reason string
|
|
847
|
+
so a genuine dispatch error is never mistaken for one."""
|
|
848
|
+
payload = _load_json_object(team_state_path, "team-state")
|
|
849
|
+
dispatches = payload.get("workerDispatches")
|
|
850
|
+
if not isinstance(dispatches, list):
|
|
851
|
+
return []
|
|
852
|
+
return [
|
|
853
|
+
record
|
|
854
|
+
for record in dispatches
|
|
855
|
+
if isinstance(record, dict)
|
|
856
|
+
and record.get("status") == "error"
|
|
857
|
+
and _string_value(record.get("reason")) == TEARDOWN_BEFORE_TERMINAL_REASON
|
|
858
|
+
]
|
|
859
|
+
|
|
860
|
+
|
|
814
861
|
def _running_dispatches(team_state_path: Path) -> list[Mapping[str, Any]]:
|
|
815
862
|
payload = _load_json_object(team_state_path, "team-state")
|
|
816
863
|
dispatches = payload.get("workerDispatches")
|
|
@@ -58,6 +58,12 @@ def detect_terminal_backend() -> str:
|
|
|
58
58
|
LIVENESS_AUDIT_HEARTBEAT = "audit-heartbeat"
|
|
59
59
|
LIVENESS_WRAPPER_STATUS = "wrapper-status"
|
|
60
60
|
|
|
61
|
+
# `okstra team teardown` stamps this on a dispatch it wrote off, and a later
|
|
62
|
+
# `okstra team await` matches it to tell those apart from a genuine dispatch
|
|
63
|
+
# error before letting the wrapper's own exit settle them. Two sides of one fact,
|
|
64
|
+
# so the string lives here rather than in whichever module wrote it first.
|
|
65
|
+
TEARDOWN_BEFORE_TERMINAL_REASON = "teardown before terminal status"
|
|
66
|
+
|
|
61
67
|
# The worktree argument grants the wrapper `--add-dir` write access outside
|
|
62
68
|
# project-root. Only the phases whose workers mutate a stage worktree get it;
|
|
63
69
|
# analysis phases write their artifacts under project-root (see the contract in
|
|
@@ -6,7 +6,7 @@ release-handoff 단계에서 lead 가 PR 본문을 작성할 때 사용하는
|
|
|
6
6
|
1. per-run override (okstra-run Step 6 에서 입력)
|
|
7
7
|
2. project: <project_root>/.okstra/project.json 의 ``prTemplatePath``
|
|
8
8
|
3. global: ~/.okstra/config.json 의 ``prTemplatePath``
|
|
9
|
-
4. default: ``$OKSTRA_HOME/templates/
|
|
9
|
+
4. default: ``$OKSTRA_HOME/templates/pr/pr-body.template.md`` (구버전
|
|
10
10
|
``~/.claude/skills/...`` 후보는 backward-compat 로 유지)
|
|
11
11
|
|
|
12
12
|
경로는 절대경로 또는 ``~`` 시작 경로를 권장한다. 상대경로일 경우 project
|
|
@@ -19,6 +19,7 @@ from . import tmux
|
|
|
19
19
|
from .adapters.dispatch import provider_worker_wrappers
|
|
20
20
|
from .adapters.dispatch.cmux import dispatch_port_for_terminal_backend
|
|
21
21
|
from .application.dispatch_assignments import dispatch_assignments
|
|
22
|
+
from .dispatch_state import TEARDOWN_BEFORE_TERMINAL_REASON
|
|
22
23
|
from .dispatch_core import (
|
|
23
24
|
BACKEND_CMUX_PANE,
|
|
24
25
|
BACKEND_TMUX_PANE,
|
|
@@ -256,7 +257,7 @@ def _mark_teardown_errors(team_state_path: Path) -> None:
|
|
|
256
257
|
for record in payload.get("workerDispatches", []):
|
|
257
258
|
if isinstance(record, dict) and record.get("status") not in _TERMINAL_STATUSES:
|
|
258
259
|
record["status"] = "error"
|
|
259
|
-
record["reason"] =
|
|
260
|
+
record["reason"] = TEARDOWN_BEFORE_TERMINAL_REASON
|
|
260
261
|
_write_json(team_state_path, payload)
|
|
261
262
|
|
|
262
263
|
|
|
@@ -55,6 +55,12 @@ def _looks_like_file_path(path: str) -> bool:
|
|
|
55
55
|
return False
|
|
56
56
|
if re.fullmatch(r"[0-9a-fA-F]{7,64}", path):
|
|
57
57
|
return False
|
|
58
|
+
# A dotted name is the usual filename signal, and an IPv4 literal is dots and
|
|
59
|
+
# digits too. `127.0.0.1:13306` in a Read-only command log is a host and port —
|
|
60
|
+
# the worker cannot record an Evidence read of it, so reading it as a citation
|
|
61
|
+
# fails the run over something it has no way to satisfy.
|
|
62
|
+
if re.fullmatch(r"\d{1,3}(?:\.\d{1,3}){3}", path):
|
|
63
|
+
return False
|
|
58
64
|
return (
|
|
59
65
|
"/" in path
|
|
60
66
|
or "." in Path(path).name
|
|
@@ -726,9 +726,10 @@
|
|
|
726
726
|
"type": "object",
|
|
727
727
|
"description": "RENDER_IF taskType == release-handoff. §5.6 deliverables.",
|
|
728
728
|
"required": [
|
|
729
|
-
"
|
|
729
|
+
"handoffScope",
|
|
730
730
|
"featureBranchState",
|
|
731
731
|
"userSelections",
|
|
732
|
+
"localCheckoutOutcome",
|
|
732
733
|
"executedCommands",
|
|
733
734
|
"commitList",
|
|
734
735
|
"mergeConflictProbe",
|
|
@@ -737,6 +738,37 @@
|
|
|
737
738
|
"userNarrative"
|
|
738
739
|
],
|
|
739
740
|
"additionalProperties": false,
|
|
741
|
+
"allOf": [
|
|
742
|
+
{
|
|
743
|
+
"if": {
|
|
744
|
+
"properties": {
|
|
745
|
+
"handoffScope": {
|
|
746
|
+
"properties": {
|
|
747
|
+
"mode": {
|
|
748
|
+
"const": "stage-group"
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
"required": [
|
|
752
|
+
"mode"
|
|
753
|
+
]
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
"required": [
|
|
757
|
+
"handoffScope"
|
|
758
|
+
]
|
|
759
|
+
},
|
|
760
|
+
"then": {
|
|
761
|
+
"required": [
|
|
762
|
+
"sourceVerificationReports"
|
|
763
|
+
]
|
|
764
|
+
},
|
|
765
|
+
"else": {
|
|
766
|
+
"required": [
|
|
767
|
+
"sourceVerificationReport"
|
|
768
|
+
]
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
],
|
|
740
772
|
"properties": {
|
|
741
773
|
"handoffScope": {
|
|
742
774
|
"type": "object",
|
|
@@ -761,11 +793,25 @@
|
|
|
761
793
|
},
|
|
762
794
|
"collectorBranch": {
|
|
763
795
|
"type": "string"
|
|
796
|
+
},
|
|
797
|
+
"mergeCommits": {
|
|
798
|
+
"type": "array",
|
|
799
|
+
"description": "Assemble's merge commit SHAs, in the order the stages were merged into the collector branch. Without them the collector branch's contents cannot be traced back to the stages that produced them once the branch moves on.",
|
|
800
|
+
"items": {
|
|
801
|
+
"type": "string",
|
|
802
|
+
"minLength": 7
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
"dependencyClosureVerdict": {
|
|
806
|
+
"type": "string",
|
|
807
|
+
"minLength": 1,
|
|
808
|
+
"description": "Whether the selected stages are closed under their dependencies, and on what basis. A stage-group PR that ships a stage without its predecessor is the failure this records."
|
|
764
809
|
}
|
|
765
810
|
}
|
|
766
811
|
},
|
|
767
812
|
"sourceVerificationReport": {
|
|
768
813
|
"type": "object",
|
|
814
|
+
"description": "whole-task mode: the one final-verification report this handoff ships. Required unless handoffScope.mode is stage-group.",
|
|
769
815
|
"required": [
|
|
770
816
|
"path",
|
|
771
817
|
"verdictTokenQuote"
|
|
@@ -782,6 +828,59 @@
|
|
|
782
828
|
}
|
|
783
829
|
}
|
|
784
830
|
},
|
|
831
|
+
"sourceVerificationReports": {
|
|
832
|
+
"type": "array",
|
|
833
|
+
"description": "stage-group mode: one row per selected stage, because that handoff ships several verified stages and a single report path cannot say which stage each verdict belongs to.",
|
|
834
|
+
"minItems": 1,
|
|
835
|
+
"items": {
|
|
836
|
+
"type": "object",
|
|
837
|
+
"required": [
|
|
838
|
+
"stage",
|
|
839
|
+
"path",
|
|
840
|
+
"verdictTokenQuote"
|
|
841
|
+
],
|
|
842
|
+
"additionalProperties": false,
|
|
843
|
+
"properties": {
|
|
844
|
+
"stage": {
|
|
845
|
+
"type": "integer",
|
|
846
|
+
"minimum": 1
|
|
847
|
+
},
|
|
848
|
+
"path": {
|
|
849
|
+
"type": "string",
|
|
850
|
+
"minLength": 1
|
|
851
|
+
},
|
|
852
|
+
"verdictTokenQuote": {
|
|
853
|
+
"type": "string",
|
|
854
|
+
"minLength": 1
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
"localCheckoutOutcome": {
|
|
860
|
+
"type": "object",
|
|
861
|
+
"description": "What the `local checkout` action did, including when it was not taken. `removedWorktree` is the command's own field: a non-empty path means the okstra worktree was removed, an empty string means it was already gone and the branch survived. Recording a removal that did not happen is the error this shape exists to prevent.",
|
|
862
|
+
"required": [
|
|
863
|
+
"status"
|
|
864
|
+
],
|
|
865
|
+
"additionalProperties": false,
|
|
866
|
+
"properties": {
|
|
867
|
+
"status": {
|
|
868
|
+
"enum": [
|
|
869
|
+
"not-run",
|
|
870
|
+
"checked-out"
|
|
871
|
+
]
|
|
872
|
+
},
|
|
873
|
+
"branch": {
|
|
874
|
+
"type": "string"
|
|
875
|
+
},
|
|
876
|
+
"mainWorktreePath": {
|
|
877
|
+
"type": "string"
|
|
878
|
+
},
|
|
879
|
+
"removedWorktree": {
|
|
880
|
+
"type": "string"
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
},
|
|
785
884
|
"featureBranchState": {
|
|
786
885
|
"type": "object",
|
|
787
886
|
"required": [
|
|
@@ -835,7 +934,9 @@
|
|
|
835
934
|
]
|
|
836
935
|
},
|
|
837
936
|
"h3": {
|
|
937
|
+
"description": "The PR title/body confirmation. `not-run` when H1 was `local checkout` or `skip` — those routes never reach the question, and inventing an answer for a question that was never asked is the failure this value prevents.",
|
|
838
938
|
"enum": [
|
|
939
|
+
"not-run",
|
|
839
940
|
"use as-is",
|
|
840
941
|
"edit then proceed",
|
|
841
942
|
"cancel"
|
|
@@ -155,7 +155,7 @@ overwriting silently.
|
|
|
155
155
|
## D. Project PR body template (release-handoff)
|
|
156
156
|
|
|
157
157
|
`release-handoff` fills the PR body from a template — by default
|
|
158
|
-
`~/.okstra/templates/
|
|
158
|
+
`~/.okstra/templates/pr/pr-body.template.md`. Most projects want their own
|
|
159
159
|
(e.g. `.github/PULL_REQUEST_TEMPLATE.md`). Pre-registration during setup is
|
|
160
160
|
opt-in; the same prompt is offered again on the first `release-handoff` run,
|
|
161
161
|
so deferring (`Later`) is safe.
|
|
@@ -544,13 +544,22 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
|
|
|
544
544
|
- Mode: `{{ releaseHandoff.handoffScope.mode | mdcell }}`
|
|
545
545
|
{% if releaseHandoff.handoffScope.stages %}- Stages: {{ releaseHandoff.handoffScope.stages | join(', ') | mdcell }}
|
|
546
546
|
{% endif %}{% if releaseHandoff.handoffScope.collectorBranch %}- Collector branch: `{{ releaseHandoff.handoffScope.collectorBranch | mdcell }}`
|
|
547
|
+
{% endif %}{% if releaseHandoff.handoffScope.mergeCommits %}- Assemble merge commits: {{ releaseHandoff.handoffScope.mergeCommits | join(', ') | mdcell }}
|
|
548
|
+
{% endif %}{% if releaseHandoff.handoffScope.dependencyClosureVerdict %}- Dependency closure: {{ releaseHandoff.handoffScope.dependencyClosureVerdict | mdcell }}
|
|
547
549
|
{% endif %}
|
|
548
550
|
{% endif %}
|
|
549
551
|
### 5.6.1 Source Verification Report
|
|
550
|
-
|
|
552
|
+
{% if releaseHandoff.sourceVerificationReports %}
|
|
553
|
+
| Stage | Report path (project-relative) | Quoted `Verdict Token` row |
|
|
554
|
+
|-------|--------------------------------|----------------------------|
|
|
555
|
+
{% for row in releaseHandoff.sourceVerificationReports -%}
|
|
556
|
+
| {{ row.stage | mdcell }} | `{{ row.path | mdcell }}` | {{ row.verdictTokenQuote | mdcell }} |
|
|
557
|
+
{% endfor %}
|
|
558
|
+
{%- else %}
|
|
551
559
|
- Path (project-relative): `{{ releaseHandoff.sourceVerificationReport.path }}`
|
|
552
560
|
- Quoted `Verdict Token` row from that report's `## 7.` table:
|
|
553
561
|
> {{ releaseHandoff.sourceVerificationReport.verdictTokenQuote | mdquote(2) }}
|
|
562
|
+
{% endif %}
|
|
554
563
|
|
|
555
564
|
### 5.6.2 Feature Branch & Working-Tree State{% if t("releaseHandoff.branchStateAside") != "captured at run start" %} ({{ t("releaseHandoff.branchStateAside") }}){% endif %}
|
|
556
565
|
|
|
@@ -571,7 +580,7 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
|
|
|
571
580
|
{% endif %}
|
|
572
581
|
| H2 | {{ t("releaseHandoff.h2Body") }} | `{{ (releaseHandoff.userSelections.h2 or t("releaseHandoff.h2DefaultLabel")) | mdcell }}` | {{ t("releaseHandoff.h2OptionsLabel") }} |
|
|
573
582
|
| H2b | Merge conflict probe | `{{ (releaseHandoff.userSelections.h2b or releaseHandoff.mergeConflictProbe.kind) | mdcell }}` | `not-run` / `clean` / `proceed anyway` / `change base branch` / `cancel` |
|
|
574
|
-
| H3 | {{ t("releaseHandoff.h3Body") }} | `{{ releaseHandoff.userSelections.h3 | mdcell }}` | `use as-is` / `edit then proceed` / `cancel` |
|
|
583
|
+
| H3 | {{ t("releaseHandoff.h3Body") }} | `{{ releaseHandoff.userSelections.h3 | mdcell }}` | `not-run` / `use as-is` / `edit then proceed` / `cancel` |
|
|
575
584
|
|
|
576
585
|
### 5.6.4 Executed Commands
|
|
577
586
|
|
|
@@ -623,6 +632,12 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
|
|
|
623
632
|
|
|
624
633
|
{{ releaseHandoff.routingRecommendation }}
|
|
625
634
|
|
|
635
|
+
### 5.6.9 Local Checkout Outcome
|
|
636
|
+
|
|
637
|
+
- Status: `{{ releaseHandoff.localCheckoutOutcome.status | mdcell }}`
|
|
638
|
+
{% if releaseHandoff.localCheckoutOutcome.status == 'checked-out' %}- Checked out `{{ releaseHandoff.localCheckoutOutcome.branch | mdcell }}` into `{{ releaseHandoff.localCheckoutOutcome.mainWorktreePath | mdcell }}`
|
|
639
|
+
- okstra worktree: {% if releaseHandoff.localCheckoutOutcome.removedWorktree %}`{{ releaseHandoff.localCheckoutOutcome.removedWorktree | mdcell }}` removed{% else %}none removed — already torn down, so the branch survived{% endif %}
|
|
640
|
+
{% endif %}
|
|
626
641
|
{% endif %}
|
|
627
642
|
{% if header.taskType == 'implementation' %}
|
|
628
643
|
## 5.7 Implementation Deliverables
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
<h2>{{ t('tasks.release-handoff.commits-delivered') }}</h2>
|
|
40
40
|
{% if handoff.commitList is mapping %}<p>{{ t('tasks.release-handoff.there-are-no-commits') }}</p>{% else %}
|
|
41
41
|
<table><thead><tr><th>{{ t('tasks.release-handoff.commit') }}</th><th>{{ t('tasks.release-handoff.subject') }}</th><th>{{ t('tasks.release-handoff.files') }}</th></tr></thead><tbody>{% for row in handoff.commitList %}<tr>{{ row_key(pairs=[("SHA", row.shortSha), ("Order", loop.index)]) }}<td>{{ row.subject | inline_code }}</td><td>{% for file in row.files %}<code>{{ file }}</code>{% if not loop.last %} {% endif %}{% endfor %}</td></tr>{% endfor %}</tbody></table>{% endif %}
|
|
42
|
-
<p data-report-field="releaseHandoff.
|
|
42
|
+
{% if handoff.sourceVerificationReports %}<p data-report-field="releaseHandoff.sourceVerificationReports"><strong>{{ t('tasks.release-handoff.the-verification-report-this-rests-on') }}</strong>{% for row in handoff.sourceVerificationReports %} — stage {{ row.stage }}: <code>{{ row.path }}</code> · {{ row.verdictTokenQuote | inline_code }}{% endfor %}</p>
|
|
43
|
+
{% else %}<p data-report-field="releaseHandoff.sourceVerificationReport"><strong>{{ t('tasks.release-handoff.the-verification-report-this-rests-on') }}</strong> — <code>{{ handoff.sourceVerificationReport.path }}</code> · {{ handoff.sourceVerificationReport.verdictTokenQuote | inline_code }}</p>{% endif %}
|
|
43
44
|
</section>
|
|
44
45
|
|
|
45
46
|
<section data-report-section="next-action">
|
|
@@ -693,6 +693,58 @@ def _is_legal_concurrent_run_skip(
|
|
|
693
693
|
)
|
|
694
694
|
|
|
695
695
|
|
|
696
|
+
def _validate_cmux_workers_were_dispatched_by_okstra(
|
|
697
|
+
team_state: dict,
|
|
698
|
+
workers: list,
|
|
699
|
+
dispatched_statuses: set[str],
|
|
700
|
+
failures: list[str],
|
|
701
|
+
) -> None:
|
|
702
|
+
"""Under cmux, okstra owns the worker panes — so it must have started them.
|
|
703
|
+
|
|
704
|
+
`prompts/lead/adapters/cmux.md` overrides the host relay's worker-dispatch
|
|
705
|
+
mapping: every lead goes through `okstra team dispatch`, a Claude Code lead
|
|
706
|
+
included. A lead that follows its own relay instead and starts the worker
|
|
707
|
+
in-process gets no pane, and the user cannot see the work — which is the whole
|
|
708
|
+
point of the adapter owning them.
|
|
709
|
+
|
|
710
|
+
The tell is the absent record, not the backend: `okstra team` appends a
|
|
711
|
+
`workerDispatches[]` row for every worker it starts, and an in-process worker
|
|
712
|
+
leaves none. Backend alone cannot separate the two, because a pane dispatch
|
|
713
|
+
that degrades legitimately records `backendType: cli-wrapper`
|
|
714
|
+
(`dispatch_core._dispatch_record`). A worker that was never attempted has no
|
|
715
|
+
row either, so only attempted statuses are checked.
|
|
716
|
+
"""
|
|
717
|
+
adapter = team_state.get("leadAdapter")
|
|
718
|
+
if not isinstance(adapter, dict):
|
|
719
|
+
return
|
|
720
|
+
if str(adapter.get("name", "")).strip() != "cmux":
|
|
721
|
+
return
|
|
722
|
+
recorded = {
|
|
723
|
+
str(row.get("workerId", "")).strip()
|
|
724
|
+
for row in team_state.get("workerDispatches") or []
|
|
725
|
+
if isinstance(row, dict)
|
|
726
|
+
}
|
|
727
|
+
missing = []
|
|
728
|
+
for worker in workers:
|
|
729
|
+
if not isinstance(worker, dict):
|
|
730
|
+
continue
|
|
731
|
+
if str(worker.get("status", "")).strip() not in dispatched_statuses:
|
|
732
|
+
continue
|
|
733
|
+
worker_id = str(worker.get("workerId", "")).strip()
|
|
734
|
+
if worker_id and worker_id not in recorded:
|
|
735
|
+
missing.append(worker_id)
|
|
736
|
+
if missing:
|
|
737
|
+
failures.append(
|
|
738
|
+
"team-state.workerDispatches has no record for "
|
|
739
|
+
f"{', '.join(sorted(missing))} in a cmux run — those workers ran "
|
|
740
|
+
"without okstra starting them, so they had no pane and the user "
|
|
741
|
+
"could not watch the work. Under cmux every lead dispatches through "
|
|
742
|
+
"`okstra team dispatch`, a Claude Code lead included "
|
|
743
|
+
"(prompts/lead/adapters/cmux.md); the host relay's in-process "
|
|
744
|
+
"`Agent(...)` mapping does not apply to this run."
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
696
748
|
def validate_team_state(
|
|
697
749
|
team_state: dict,
|
|
698
750
|
project_root: Path,
|
|
@@ -777,6 +829,10 @@ def validate_team_state(
|
|
|
777
829
|
f"Found: `{tc_status}`."
|
|
778
830
|
)
|
|
779
831
|
|
|
832
|
+
_validate_cmux_workers_were_dispatched_by_okstra(
|
|
833
|
+
team_state, workers, dispatched_statuses, failures
|
|
834
|
+
)
|
|
835
|
+
|
|
780
836
|
by_role: dict[str, dict] = {}
|
|
781
837
|
for worker in workers:
|
|
782
838
|
if not isinstance(worker, dict):
|