okstra 0.129.0 → 0.130.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.
@@ -417,7 +417,7 @@ Each task type enforces phase-specific allowed and forbidden actions. A run crea
417
417
  Common constraints:
418
418
 
419
419
  - Every phase except `implementation` prohibits source-code edits, builds, migrations, deployments, and other state-mutating commands (`final-verification` allows read-only test commands only). `implementation` permits edits/commits only within the file list of the approved plan; `git push`, publish, deploy, real migration, and third-party write APIs remain prohibited.
420
- - **Isolated worktree for pre-implementation non-implementation phases (BLOCKING)**: The first pre-implementation non-implementation phase prepare creates a task-key `git worktree` through `okstra-ctl`. Pre-implementation non-implementation phases reuse the task-key worktree: `requirements-discovery` → `error-analysis` → `implementation-planning` use the same worktree and branch for the same task key. `implementation` does not reuse this task-key worktree; implementation uses a dedicated stage-specific worktree and branch for every stage/run, as described in the next item. The task-key worktree lives at `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` (special characters such as `/` and `:` in segments are normalized to `-`), and the branch is named `<work-category-namespace>/<task-id-segment>` (for example, `feature/dev-9436` or `fix/dev-7311`). The namespace is derived from work_category (`feature`·`improvement`→`feature/`, `bugfix`→`fix/`, `refactor`→`refactor/`, `ops`→`ops/`, unspecified→`task/`). The base ref is the commit selected by the user's `--base-ref` during the first phase's prepare. `~/.okstra/worktrees/registry.json` (guarded by flock) globally manages task-key → path/branch mappings to prevent path and branch collisions during concurrent runs. Configured sync directories are linked from the main worktree as symlinks to provide filesystem continuity across task checkouts (the sync list can be overridden by `worktreeSyncDirs` in `project.json` or the `OKSTRA_WORKTREE_SYNC_DIRS` environment variable; an empty array disables syncing). This sync does not expand the okstra context/write boundary. Provisioning is skipped when the caller is already inside another worktree or project_root is not a Git repository, and the executor works directly from project_root. The worktree is not automatically deleted after a run; it is the authoritative artifact for later phases, PR authoring, and rollback verification. Manual cleanup: `git -C <main-worktree> worktree remove <path>` → `git -C <main-worktree> branch -D <branch>` + remove the registry entry. See the *Task worktree* block in `prompts/profiles/implementation.md` and the *Task worktree (BLOCKING for every task-type)* section in `prompts/lead/okstra-lead-contract.md` for details.
420
+ - **Isolated worktree for pre-implementation non-implementation phases (BLOCKING)**: The first pre-implementation non-implementation phase prepare creates a task-key `git worktree` through `okstra-ctl`. Pre-implementation non-implementation phases reuse the task-key worktree: `requirements-discovery` → `error-analysis` → `implementation-planning` use the same worktree and branch for the same task key. `implementation` does not reuse this task-key worktree; implementation uses a dedicated stage-specific worktree and branch for every stage/run, as described in the next item. The task-key worktree lives at `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` (special characters such as `/` and `:` in segments are normalized to `-`), and the branch is named `<work-category-namespace>/<task-id-segment>` (for example, `feature/dev-9436` or `fix/dev-7311`). The namespace is derived from work_category (`feature`·`improvement`→`feature/`, `bugfix`→`fix/`, `refactor`→`refactor/`, `ops`→`ops/`, unspecified→`task/`). The work_category itself is resolved by `work_categories.resolve_work_category` as **explicit `--work-category` → the classification recorded in `task-manifest.json` → `feature`**, so the `task/` fallback is only reached when a task has no recorded classification at all; a run that omits the flag still inherits the namespace `requirements-discovery` classified. The base ref is the commit selected by the user's `--base-ref` during the first phase's prepare. `~/.okstra/worktrees/registry.json` (guarded by flock) globally manages task-key → path/branch mappings to prevent path and branch collisions during concurrent runs. Configured sync directories are linked from the main worktree as symlinks to provide filesystem continuity across task checkouts (the sync list can be overridden by `worktreeSyncDirs` in `project.json` or the `OKSTRA_WORKTREE_SYNC_DIRS` environment variable; an empty array disables syncing). This sync does not expand the okstra context/write boundary. Provisioning is skipped when the caller is already inside another worktree or project_root is not a Git repository, and the executor works directly from project_root. The worktree is not automatically deleted after a run; it is the authoritative artifact for later phases, PR authoring, and rollback verification. Manual cleanup: `git -C <main-worktree> worktree remove <path>` → `git -C <main-worktree> branch -D <branch>` + remove the registry entry. See the *Task worktree* block in `prompts/profiles/implementation.md` and the *Task worktree (BLOCKING for every task-type)* section in `prompts/lead/okstra-lead-contract.md` for details.
421
421
  - **Isolated implementation-stage worktrees (concurrent parallelism)**: The task-key worktree above is the model for `requirements-discovery` through `implementation-planning`. `implementation` tasks use **stage isolation**: **one run = one stage**, and every run receives an isolated worktree at `.../<task-id-segment>/stage-<N>/` (branch `<work-category-namespace>/<task-id-segment>-s<N>`). The registry reserves task keys and **stage keys** (`<task-key>#stage-<N>`) together under flock. The Stage Lifecycle Snapshot reads `done`/`started` entries in `consumers.jsonl`, carry-sidecar backfills, and reserved registry stages together, and removes them from the ready set (occupancy SSOT = registry). Thus, if the user starts two `implementation` runs simultaneously, they proceed on different independent stages without collision. Base selection: independent = common anchor (HEAD fixed at entry to the first stage); single dependency = predecessor's done commit; multiple dependencies = task worktree HEAD only if every predecessor is an ancestor (`git merge-base --is-ancestor`; otherwise `PrepareError`). The cost-aware-design ready-set batch has been retired because each stage needs an isolated branch and reserving two stage keys on one branch creates a branch-uniqueness collision, so it offers no benefit: sequential work uses the next run after a stage is done, and concurrent work uses separate runs, at equivalent cost. Select a stage with `--stage <auto|N>` or the wizard's `stage_pick`. The wizard's `stage_pick` is a multiselect that labels each stage with its state (`mark_done`/`mark_active`/`mark_ready`/`mark_blocked`), topologically sorts the dependency closure of the selection with Kahn's algorithm (`stage_targets.order_stage_closure`), and exports it as the `chain-stages` CSV in render-args. The `okstra-run` SKILL consumes this queue and sequentially executes N single-stage runs in dependency order as an unattended chain, advancing only after checking the Phase 6 `done` row for each stage. This is an orchestration layer only; the **one run = one stage** isolation invariant of the wizard and prepare remains unchanged. Not only worktrees but also **run artifacts (reports, state, worker results, manifests) are isolated per stage under `runs/implementation/stage-<N>/`**, so reports and state from two concurrently running stages do not mix. In contrast, `consumers.jsonl` and the worktree registry remain at the task-type root (`runs/implementation/`) because they are shared coordination sources of truth across stages.
422
422
  - **Isolation of single-stage final-verification run artifacts (concurrent parallelism)**: Single-stage `final-verification` (`--stage <N>`) also isolates run artifacts under `runs/final-verification/stage-<N>/`, like implementation, with independent sequences per stage, and appends `-fv-s<N>` to the team name. The `-fv-` delimiter prevents collisions with the same stage's implementation team (`-s<N>`) and with the default whole-task verification name. Thus, final-verification for multiple stages can run concurrently without mixing state, worker results, reports, or teams. It does not create a new worktree; it reuses the corresponding implementation stage worktree from the registry read-only and therefore does not reserve a registry stage key. The `-fv-s<N>` suffix on the `teamName` label is only for audit/display distinction. The actual team is the per-session implicit team (`session-<leadSid>`), so the pre-v2.1.178 hard failure caused by a `TeamCreate` name collision no longer occurs. Whole-task verification (empty stage value) retains the existing flat `runs/final-verification/` structure.
423
423
  - Every phase except `implementation` and `release-handoff` prohibits source-code edits, builds, migrations, deployments, and other state-mutating commands (`final-verification` permits read-only test commands only). `implementation` allows edits/commits only within the approved plan's file list; `git push`, publish, deploy, real migration, and third-party write APIs remain prohibited. `release-handoff` does not modify source code and executes only the commit / push / PR commands selected by the user in the menu (force push, direct push to the base branch, hook bypass, and release publication remain prohibited).
@@ -723,7 +723,7 @@ Write the actual Markdown report body to the file instead of metadata about save
723
723
 
724
724
  ## Final report views (HTML)
725
725
 
726
- Phase 7 step 1.5 deterministically generates a self-contained HTML view from a single final-report MD input.
726
+ The Phase 7 `render-views` step deterministically generates a self-contained HTML view from a single final-report MD input. The lead reaches it through `okstra report-finalize`, which runs the whole Phase 7 sequence (`scripts/okstra_ctl/report_finalize.py` — shared with the Codex lead adapter).
727
727
 
728
728
  - `reports/final-report-<task-type>-<seq>.html` — self-contained HTML for human reviewers. CSS / JS are embedded inline (zero external URLs), with system-color dark mode, sticky header, and print support. Reviewers can fill in decision inputs for §1 `C-*` rows—checkboxes, selects, and textareas—in the browser, then generate sidecar Markdown with the `Export user response` button.
729
729
  - **Reader Summary dashboard + reader mode**: Renders a top-level dashboard based on `readerSummary` (falling back to `verdictCard` when absent) and offers `Action` / `Audit` / `Full` reader-mode toggles. The default is `Action`, which shows only Reader Summary / Verdict Card / Clarification Items / Recommended Next Steps / Follow-up Tasks. Audit sections such as Evidence, Cross Verification, Execution Status, Token Usage, Plan Body Verification, and Round History are expanded in `Audit` / `Full`.
package/docs/cli.md CHANGED
@@ -464,10 +464,11 @@ Classifies the work in this task. Allowed values:
464
464
  - `refactor`
465
465
  - `ops`
466
466
  - `improvement`
467
- - `unknown` (default)
468
467
 
469
468
  Normally, the `requirements-discovery` phase infers work-category automatically. In a lifecycle that skips that phase—for example, one starting directly at `implementation-planning`—use this flag to classify the work explicitly. The value is preserved in `task-manifest.json` and used for grouping by the schedule and status skills.
470
469
 
470
+ The effective value for a run is resolved by `scripts/okstra_ctl/work_categories.py` (`resolve_work_category`) in this order: **the explicit flag → the classification already recorded in the task's `task-manifest.json` → `feature`** (`DEFAULT_WORK_CATEGORY`). This is why a later phase run without `--work-category` still lands on the same branch namespace that `requirements-discovery` classified—for example `feature/dev-9436` rather than the `task/` fallback. The `okstra-run` in-session wizard never asks for this flag, so it always takes the resolved path; its worktree preview runs the same resolver, so the preview matches the branch that is actually created. Already-created `task/…` branches are not renamed automatically—the registry key and `consumers.jsonl` history would drift, so they need manual cleanup.
471
+
471
472
  Example:
472
473
 
473
474
  ```bash
@@ -645,6 +646,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
645
646
  | `okstra manager <init\|discover-projects\|new\|task>` | Public CLI for grouping cross-project okstra tasks into manager-owned context. `new project`, `new task-group`, and `new task` create manager plans; `task assign`, `task note`, `task sync`, `task status`, and `task run` manage per-project assignments and snapshots. `new project --project-root` accepts only existing directories and performs setup-equivalent registration only if `.okstra/project.json` is absent. Public documentation uses the full `project-id:task-group:task-id` child task key; when child task IDs differ within the same manager task, select the exact child with `--child-task-id`. `task run` does not execute the child lead directly; it returns `prepared` launch metadata/event and a child launch-context packet as JSON |
646
647
  | `okstra rollup [--task-group <group>] [--project-root <dir>] [--cwd <dir>]` | Read-only backend for the okstra-rollup skill. For every catalog task, or one task group, it emits JSON with per-task run counts, raw duration in ms, error counts, latest report paths, group totals, and status/category/phase distributions. Omitting `--task-group` targets the whole project catalog. The caller skill formats raw ms as HH:MM:SS and synthesizes report prose. Use the `okstra inspect` family for a single-task drill-down |
647
648
  | `okstra usage-report [--days <positive-int>] [--project-root <dir>] [--cwd <dir>] [--json]` | Read-only backend for the okstra-usage skill. Defaults to the whole current project's last 30 days and emits task-type run coverage, raw/billable tokens, known USD cost, CPU-sum milliseconds, wall-clock milliseconds, unavailable reason counts, and unmatched pricing models |
649
+ | `okstra worker-liveness [--audit <path>]… [--prompt <path>]… [--max-idle <seconds>] [--launch-grace <seconds>] [--json]` | Judge whether pending workers are still alive so the lead's poll ends a stalled wait early instead of paying the full deadline. Both selectors repeat and may be mixed in one call. `--audit` takes a claude-worker audit sidecar and reports `stalled` when its `- PROGRESS:` heartbeat is past the idle budget; `--prompt` takes a CLI-wrapper prompt-history path and reports `did-not-launch` when no sibling `.log`/`.status.json` appeared past the launch grace. The two selectors are not interchangeable: only the `okstra-*-exec.sh` wrappers write the `.log`/`.status.json` pair, so pointing `--prompt` at an in-process claude-worker reports `did-not-launch` for a healthy worker every time past the grace — probe those with `--audit`. Healthy probes report `live`. It only judges—it never kills or re-dispatches. Exit 1 on a problem verdict, so a poll loop can branch without parsing JSON. The heartbeat line shape and the 5-minute (+60s grace) budget come from the `okstra_ctl.worker_heartbeat` SSOT shared with the Phase 7 audit (`validators/validate_session_conformance.py`), so a worker the live probe passes cannot fail the post-hoc audit on cadence |
648
650
  | `okstra log-report [--project-root <dir>] [--cwd <dir>] [--top <N>] [--json]` | Read-only inventory of wrapper transcript `.log` files and their sibling prompt `.md` files. Each ranked entry preserves `path` / `sizeBytes` for compatibility and also reports `transcriptPath`, `transcriptBytes`, `promptPath`, `promptBytes`, and `transcriptToPromptRatio`; totals distinguish prompt bytes from transcript bytes and count paired files. Ranking remains transcript-size descending |
649
651
  | `okstra recap <assemble\|record\|note> <task-root\|task-key> …` | Backend for the okstra-inspect `recap` facet. `assemble` is read-only and prints a JSON summary of phase transitions across a task's runs. `record --kind <summary\|qa> --mode <artifact\|code> --answer <text> [--question <text>] [--citation <path:line> …]` appends one line to `<task-root>/recap/recap-log.jsonl` and never mutates other artifacts. `note --kind <verification-evidence\|decision-draft\|analysis-note> --slug <topic> --purpose <text> --scope-note <text> (--body <markdown>\|--body-file <path>)` writes an agent-authored note to `<task-root>/notes/` and prints its path plus the `--clarification-response` argument for feeding it into a later run |
650
652
  | `okstra user-response <list\|show\|write> …` | Backend for the `/okstra-user-response` skill: answer a task's open clarification questions in-session and write the response sidecar. `list --home <dir> --project <id> [--limit <n>]` finds reports with open questions; `show --report <md>` reads one report's questions; `write --report <md> --answers <json> [--approval <json>] [--task-key <key>]` writes the sidecar. Each answer carries a `disposition` of `answer` or `reframe`; a `reframe` is carried into the next run as a re-scoped brief. JSON output; exit 0 ok / 1 error |
@@ -662,7 +664,8 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
662
664
  | `okstra codex-run <args…>` | Codex lead-adapter dry-run entry point. Accepts the same arguments as `render-bundle` but owns `--render-only --lead-runtime codex`. It prepares the task bundle and prints the prompt for the Codex lead without dispatching workers |
663
665
  | `okstra codex-dispatch --project-root <dir> --run-manifest <path> [--workers codex,antigravity,report-writer]` | Read a run manifest prepared by `codex-run` and execute the Codex-side supported worker subset. Without `--workers`, unsupported roster members such as `claude` are skipped; explicitly requesting one fails. The report writer requires opt-in with `--enable-codex-report-writer --report-writer-codex-model <model>`. On success, it automatically performs token-usage substitution, HTML view rendering, follow-up task-stub generation, and run validation |
664
666
  | `okstra team dispatch --project-root <dir> --run-manifest <path> [--workers <csv>] [--jobs-file <path>] [--dry-run]` / `okstra team await --project-root <dir> --run-manifest <path> [--json]` / `okstra team teardown --project-root <dir> --run-manifest <path> [--dry-run] [--json]` | Read a `leadRuntime=external` run manifest and dispatch, await, or tear down tmux-pane workers. If a tmux pane cannot be created, gracefully degrade to the CLI wrapper and record the fallback in `workerDispatches[].degradedFrom` |
665
- | `okstra render-views <final-report.md>` | Phase 7 step 1.5: deterministically create a human-facing self-contained sibling `*.html` view from one final-report Markdown file after token substitution. The source Markdown is unchanged. The Node delegation wrapper calls `scripts/okstra-render-report-views.py`; `validators/validate-report-views.py` verifies form-control placement, absence of external URLs, stale source digests, and Response ID parity |
667
+ | `okstra report-finalize --project-root <dir> --run-manifest <path> --report <final-report.md>` | Run the whole Phase 7 post-report sequence in its contractual order: `token-usage` → `render-views` `spawn-followups` `validate-run`. Stops at the first non-zero exit and names the failing step; every step is idempotent so re-running after a fix is safe. This is the same code path (`scripts/okstra_ctl/report_finalize.py`) the Codex lead adapter runs automatically after its report-writer completes, so a Claude-led and a Codex-led run finalize identically. `--workspace-root` is owned by the Node wrapper. Prefer this over invoking the four steps individually |
668
+ | `okstra render-views <final-report.md>` | The Phase 7 `render-views` step, runnable on its own: deterministically create a human-facing self-contained sibling `*.html` view from one final-report Markdown file after token substitution. The source Markdown is unchanged. The Node delegation wrapper calls `scripts/okstra-render-report-views.py`; `validators/validate-report-views.py` verifies form-control placement, absence of external URLs, stale source digests, and Response ID parity |
666
669
  | `okstra design-prep <list\|show\|write>` | Review AI-prepared implementation design requests, inspect their effective confirmed response, or append a confirmed user/wizard response without editing the planning report |
667
670
  | `okstra wizard <init\|step\|render-args\|confirmation\|outcome> --state-file <path>` | Interactive input state machine for okstra-run, implemented by `okstra_ctl.wizard`. Seed a state file with `init`, then repeatedly call `step --answer <val>` to receive the next `Prompt` JSON. `--answer` is **required**; use `--no-submit` to peek at the next prompt without submitting a response. `render-args` returns the final `render-bundle` argument map, and `confirmation` returns the user echo block. On a completed wizard, `outcome` returns `renderArgs`, `persistActions`, and `confirmationText` together; project/global release-handoff PR-template persistence appears as `persistActions[].command == "config.set"`. For an `implementation` task type, `stage_pick` follows `approved_plan_pick` and selects the stage before `executor_pick`. The brief step appears only for entry task types—requirements-discovery, error-analysis, and improvement-discovery. Downstream phases automatically carry the manifest brief, with a three-option `brief_carry` fallback when none is registered; `release-handoff` has no brief and enters multi-select `handoff_stage_pick` for eligible stage groups or the whole task |
668
671
  | `okstra token-usage ...` | Wrap the installed `okstra-token-usage.py` to collect and substitute run token usage. Session JSONL is incrementally scanned by default through a byte-cursor cache at `$OKSTRA_HOME/cache/token-usage/`; `--no-cache` bypasses the cache and forces a full rescan as an accuracy fallback |
@@ -177,6 +177,8 @@ Runtime/install asset changes follow this checklist:
177
177
  | `run` | `src/commands/execute/run.mjs` | Host-aware execution front door (`auto` → Claude/Codex/external path selection) |
178
178
  | `codex-run`, `codex-dispatch` | `src/commands/execute/codex-*.mjs` | Codex lead dry-run bundle preparation and CLI-backed worker dispatch |
179
179
  | `team` | `src/commands/execute/team.mjs` | External lead tmux-pane worker dispatch / await / teardown |
180
+ | `convergence` | `src/commands/execute/convergence.mjs` | Internal admin CLI for the deterministic Phase 5.5 convergence engine (`seed`/`plan-round`/`apply-round`/`finalize`/`validate`; Python: `okstra_ctl.convergence`) |
181
+ | `report-finalize` | `src/commands/report/finalize.mjs` | Run the whole Phase 7 post-report sequence in contractual order (Python: `okstra_ctl.report_finalize`) — the single reference point shared with the Codex lead adapter |
180
182
  | `render-views` | `src/commands/report/render-views.mjs` | Generate the self-contained HTML report view |
181
183
  | `render-final-report`, `inject-report-index` | `src/commands/report/*.mjs` | Render final-report Markdown from data.json and inject top-of-report index anchors |
182
184
  | `wizard` | `src/commands/execute/wizard.mjs` | Drive the `okstra-run` interactive state machine, including the final outcome envelope |
@@ -194,6 +196,8 @@ Runtime/install asset changes follow this checklist:
194
196
 
195
197
  `src/lib/python-helper.mjs` centralizes Node → Python execution so command modules do not duplicate subprocess wiring.
196
198
 
199
+ `src/lib/helper-scripts.mjs` is the SSOT list of the Python helper scripts that `okstra <cmd>` subcommands front through `runInstalledScript()`. `okstra preflight` asserts every one of them resolves under `~/.okstra/bin` using the same resolver the dispatch path uses, so a stale install fails at the cheap environment gate instead of at the blocking lead step that calls it mid-run. A contract test keeps the list in step with the `scriptName:` literals in `src/commands/**`.
200
+
197
201
  ### 4.2 `scripts/` — Runtime source
198
202
 
199
203
  Top-level scripts:
@@ -260,6 +264,7 @@ Important modules:
260
264
  | `session.py`, `tmux.py`, `seeding.py`, `locks.py`, `invocation.py`, `sequence.py`, `ids.py`, `material.py` | Supporting lifecycle helpers |
261
265
  | `pane_reclaim.py` | decides which completed trace panes are reclaim targets; imports the in-progress status set from the `reconcile.NON_TERMINAL_RECENT_STATUSES` SSOT |
262
266
  | `improvement_lenses.py` | lens enum SSOT + cap constants for the improvement-discovery phase (DEFAULT 8, ABSOLUTE 12, MIN/MAX PRIORITY 1/4, SOURCE_WORKERS) |
267
+ | `improvement_assignment.py` | improvement-discovery primary-pass lens assignment — round-robins the resolved `requiredWorkerRoles` order over the resolved priority lenses (`assign_primary_lenses`) and validates the resulting map (`validate_primary_lens_assignments`). Only the primary pass rotates; every analyser still confirms the full lens set afterwards |
263
268
  | `container.py` | the `okstra container` convergence entrypoint of the okstra-container-build public skill — `provision_container_group` + `up`/`status`/`logs`/`stop-watcher`/`down` dispatch, env-override synthesis, compose argv assembly, and per-container watcher startup |
264
269
  | `container_registry.py` | flock-guarded auxiliary index — tracks per-container-group tmux session/pane and watcher findings |
265
270
  | `plan_run_root.py` | shared helper deriving `approved_plan_path` → `plan_run_root` and back-tracing the task-key |
@@ -285,6 +290,7 @@ Important modules:
285
290
  | `convergence_engine.py` | pure `ConvergenceEngine` reducer — seeds Round 0 working state, plans roster-aware rounds, applies structured outcomes, finalizes schema v1.2, and validates replayable state without dispatch or filesystem ownership |
286
291
  | `convergence_store.py`, `convergence_migration.py` | atomic JSON persistence plus legacy/new-engine seed decisions; valid terminal finals are reused, while invalid state requires byte-preserving archival before restart |
287
292
  | `convergence.py` | `okstra convergence` internal CLI orchestration for `seed`, `plan-round`, `apply-round`, `finalize`, and `validate`; it composes the reducer, store, and migration policy without duplicating their decisions |
293
+ | `report_finalize.py` | Phase 7 post-report sequence **SSOT** — runs `token-usage` → `render-views` → `spawn-followups` → `validate-run` in that load-bearing order, stops at the first non-zero exit and names the failing step. Both lead paths converge here: the Codex adapter calls it in-process (`codex_dispatch`), a Claude-led run reaches it through `okstra report-finalize`. Neither reimplements the sequence |
288
294
  | `wrapper_status.py` | worker wrapper status sidecar reader — the host-side reader of the sidecar written by `okstra-wrapper-status.py` (the heartbeat writer) |
289
295
  | `task_target.py` | shared helper resolving `task-key → (task_root, project_root)` (`resolve_task_root`) |
290
296
  | `graphify_cmd.py` | scope wrapper for the okstra-graphify skill — `resolve_scope` + `assemble` + `okstra graphify` subcommand dispatch. Forces the corpus to `.okstra` and outputs to `.okstra/graph/` |
@@ -417,7 +423,7 @@ Non-`implementation` phases share one task-key worktree:
417
423
  ~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/
418
424
  ```
419
425
 
420
- Branch name (namespace by work_category — `feature`/`improvement` → `feature/`, `bugfix` → `fix/`, `refactor` → `refactor/`, `ops` → `ops/`, unset → `task/`):
426
+ Branch name (namespace by work_category — `feature`/`improvement` → `feature/`, `bugfix` → `fix/`, `refactor` → `refactor/`, `ops` → `ops/`, unset → `task/`; work_category resolves as explicit flag → `task-manifest.json` classification → `feature`, so `task/` is reached only when nothing was ever recorded):
421
427
 
422
428
  ```text
423
429
  <work-category-namespace>/<task-id-segment>
@@ -573,4 +579,4 @@ Clarifications now live in the unified `## 1. Clarification Items` table. Deprec
573
579
 
574
580
  ---
575
581
 
576
- *Updated: 2026-06-27 · Source of truth checked against `package.json`, `bin/okstra`, `src/cli-registry.mjs`, `src/lib/skill-catalog.mjs`, `tools/build.mjs`, `scripts/`, `skills/`, `agents/`, `templates/`, `schemas/`, `validators/`, and tests.*
582
+ *Updated: 2026-07-21 · Source of truth checked against `package.json`, `bin/okstra`, `src/cli-registry.mjs`, `src/lib/skill-catalog.mjs`, `tools/build.mjs`, `scripts/`, `skills/`, `agents/`, `templates/`, `schemas/`, `validators/`, and tests.*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.129.0",
3
+ "version": "0.130.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.129.0",
3
- "builtAt": "2026-07-20T18:48:35.313Z",
2
+ "package": "0.130.0",
3
+ "builtAt": "2026-07-21T10:12:29.478Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -207,6 +207,13 @@ and the run-level error log staying empty.
207
207
  --stderr-excerpt-file "<captured-stderr-path or omit>"
208
208
  ```
209
209
 
210
+ Keep `--message` to the error you actually observed (`HTTP 429`,
211
+ `connection refused`, `1045 access denied`) — asserting that a sandbox or
212
+ permission boundary blocked the call requires `--context-json` carrying
213
+ `cause` plus both `causeEvidence` probes, and an unevidenced block claim in
214
+ `--message` is rejected on the spot (and again on dump if you route it to
215
+ the sidecar instead).
216
+
210
217
  The lead prompt provides `**Errors log path:**`, `<task-key>`, and
211
218
  `<phase>` alongside the prompt history path. If any of these are
212
219
  missing, fall back to logging to the worker errors sidecar instead —
@@ -207,6 +207,13 @@ and the run-level error log staying empty.
207
207
  --stderr-excerpt-file "<captured-stderr-path or omit>"
208
208
  ```
209
209
 
210
+ Keep `--message` to the error you actually observed (`HTTP 429`,
211
+ `connection refused`, `1045 access denied`) — asserting that a sandbox or
212
+ permission boundary blocked the call requires `--context-json` carrying
213
+ `cause` plus both `causeEvidence` probes, and an unevidenced block claim in
214
+ `--message` is rejected on the spot (and again on dump if you route it to
215
+ the sidecar instead).
216
+
210
217
  The lead prompt provides `**Errors log path:**`, `<task-key>`, and
211
218
  `<phase>` alongside the prompt history path. If any of these are
212
219
  missing, fall back to logging to the worker errors sidecar instead —
@@ -81,7 +81,7 @@ Write a Reading Confirmation block to your **audit sidecar** at `runs/<task-type
81
81
 
82
82
  You author the final-report data.json (the JSON SSOT). You author it against the `<instruction-set>/final-report-schema.json` excerpt — its `$defs` enumerate every row shape, enum value, and cross-field constraint that applies to this run's task-type. The validator and renderer both consume the **full** `schemas/final-report-v1.0.schema.json` (the excerpt is a faithful task-type-scoped subset of it), so a data.json that satisfies the excerpt is a data.json that validates and renders correctly.
83
83
 
84
- The rendered markdown (`final-report-<task-type>-<seq>.md`) is produced by `scripts/okstra-render-final-report.py` immediately after you write the data.json. The HTML view (`*.html`) is produced from the markdown by Phase 7 step 1.5 (`scripts/okstra-render-report-views.py`). The data.json is the only file you write; the rest are derived.
84
+ The rendered markdown (`final-report-<task-type>-<seq>.md`) is produced by `scripts/okstra-render-final-report.py` immediately after you write the data.json. The HTML view (`*.html`) is produced from the markdown by the lead's Phase 7 `okstra report-finalize` run, not by you. The data.json is the only file you write; the rest are derived.
85
85
 
86
86
  Rules (the schema enforces most of these — they are listed here so you know *what* to populate, not *how* to validate):
87
87
 
@@ -102,6 +102,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
102
102
  - Cite file paths and line numbers in every `evidence.primary[].source` / `consensus[].evidence` cell.
103
103
  - Preserve every analysis worker's ticket tagging — every row's `ticketId` field carries the ticket key or the task-fallback. For single-ticket runs, set `ticketCoverage` to `{"singleTicket": "<ticket>"}`. For runs that do not require ticket tagging (`release-handoff`, `final-verification`), set `ticketCoverage` to `{"omit": true}`.
104
104
  - For `implementation-planning`, populate `implementationPlanning.requirementCoverage` with one row per concrete requirement from the brief / packet, using IDs `R-001`, `R-002`, ... in source order. `coveredBy` MUST name the specific Option Candidate plus Stage/Step that satisfies the requirement. Use `status: "covered"` only when the report's plan actually covers it; otherwise use `gap` or `blocked C-NNN` and ensure the corresponding `Clarification Items` row blocks approval. Do not collapse this into `ticketCoverage`; ticket coverage is not requirement coverage.
105
+ - For `implementation-planning`, each `requirementCoverage` row's `source` is a graded cell, not prose — free text like `"carry-in from requirements-discovery C-001"` is rejected. Write exactly one of: `brief:<heading>`, where the heading literally exists in the task brief; `derived:R-NNN — <one-line reason>`, whose chain must terminate at a `brief:` or `contract:` row of the same table without cycling; or `contract:<rule>`, for artifacts okstra's own phase contract mandates, whose allowlist is exactly the two tokens `decision-record-step` (the §5.4 Decision Drafts materialization step) and `glossary-step` (the glossary proposal step) — any other rule name is rejected, so never invent one. (Maintainer SSOT for that allowlist: `scripts/okstra_ctl/scope_provenance.py` in the okstra repo.) A requirement you cannot source this way does not belong in the table: put it in `clarificationItems[]` with `Blocks=approval`. **Enforced:** `validators/validate-run.py` `_validate_requirement_provenance`. In the same table, anchor every stage number in `coveredBy` to a `Stage` / `Stages` word (`Stage 2`, `Stages 1-3`) — `_validate_stage_has_requirement` reads that cell as prose and fails the plan when a Stage Map stage is cited by no row.
105
106
  - For `implementation-planning`, also populate `implementationPlanning.decisionDrafts` (one row per decision meeting all three decision-record criteria; `[]` otherwise) and `implementationPlanning.skippedAdrCandidates` (evaluated-but-dropped adr-candidates; `[]` otherwise). The schema excerpt enumerates the row shape; the renderer emits §5.4 `### Decision Drafts`. When `decisionDrafts` is non-empty, the plan's stages MUST carry a stepwise step that creates `.okstra/decisions/<NNNN>-<slug>.md` (validate-run gates this).
106
107
  - When the `Task Type` is `improvement-discovery`, populate `## 5.9 Improvement Candidates` with the 10-column schema enforced by `validators/validate_improvement_report.py`. Source the row IDs (`I-NNN`), lens whitelist, and Source workers patterns from `scripts/okstra_ctl/improvement_lenses.py` — do NOT introduce new lens names or worker prefixes. `improvement-discovery` is NOT in the data.json schema enum, so author its markdown directly (not via `okstra-render-final-report.py`). Immediately after writing the markdown, run (`Bash`): `okstra inject-report-index <markdown path> --report-language <en|ko>`. That adds the top-of-report Index plus `I-NNN` / `C-NNN` scroll anchors; the run validator fails the report when the Index anchor is absent.
107
108
 
@@ -23,6 +23,26 @@ ALLOWED_AGENTS = {
23
23
  ALLOWED_AGENT_ROLES = {"lead", "worker", "report-writer"}
24
24
  SUPPORTED_SIDECAR_SCHEMA_VERSIONS = {1}
25
25
 
26
+ ALLOWED_CAUSES = {
27
+ "sandbox-denied", "service-unavailable", "auth-failed", "unknown",
28
+ }
29
+ # A `sandbox-denied` claim is only admissible with both probes attached.
30
+ CAUSE_EVIDENCE_FIELDS = ("targetProbe", "controlProbe")
31
+ # Both probes share a record's PIPE_BUF_BYTES budget with stderrExcerpt, so
32
+ # they cannot reuse the 2048 cap that assumes stderrExcerpt owns it alone.
33
+ CAUSE_PROBE_MAX_BYTES = 256
34
+ # Backstop vocabulary: scanned in `message` only — never in stderrExcerpt,
35
+ # where a kernel's real "Operation not permitted" is legitimate content.
36
+ # Deliberately excludes bare "blocked"/"blocks": everyday English that would
37
+ # reject honest records like "test blocked on upstream dependency".
38
+ _BLOCKING_CLAIM_TERMS = (
39
+ "sandbox", "not permitted", "permission denied", "eperm",
40
+ )
41
+ # The backstop targets *unclassified* blocking claims. A worker that declared
42
+ # a specific cause has already done the honest work — `auth-failed` legitimately
43
+ # reads "permission denied" (MySQL 1045).
44
+ _UNCLASSIFIED_CAUSES = (None, "unknown")
45
+
26
46
 
27
47
  def _now_utc():
28
48
  return dt.datetime.now(dt.timezone.utc)
@@ -32,24 +52,80 @@ def _iso(t):
32
52
  return t.isoformat()
33
53
 
34
54
 
35
- def truncate_stderr(s):
36
- """Truncate stderr text to STDERR_EXCERPT_MAX_BYTES, multibyte-safe."""
55
+ def _truncate_utf8(s, limit):
56
+ """Truncate to `limit` bytes without splitting a multibyte character."""
37
57
  if s is None:
38
58
  return None
39
59
  encoded = s.encode("utf-8")
40
- if len(encoded) <= STDERR_EXCERPT_MAX_BYTES:
60
+ if len(encoded) <= limit:
41
61
  return s
42
- cut = encoded[:STDERR_EXCERPT_MAX_BYTES]
43
- # 멀티바이트 경계 보호: 디코드 가능해질 때까지 끝 바이트 제거
62
+ cut = encoded[:limit]
44
63
  while cut:
45
64
  try:
46
- decoded = cut.decode("utf-8")
47
- return decoded + TRUNCATION_SUFFIX
65
+ return cut.decode("utf-8") + TRUNCATION_SUFFIX
48
66
  except UnicodeDecodeError:
49
67
  cut = cut[:-1]
50
68
  return TRUNCATION_SUFFIX
51
69
 
52
70
 
71
+ def truncate_stderr(s):
72
+ """Truncate stderr text to STDERR_EXCERPT_MAX_BYTES, multibyte-safe."""
73
+ return _truncate_utf8(s, STDERR_EXCERPT_MAX_BYTES)
74
+
75
+
76
+ def normalize_cause_context(context, *, message):
77
+ """Validate a record's cause claim and return the normalized context.
78
+
79
+ Raises ValueError on three conditions:
80
+ - `cause` is set to a value outside ALLOWED_CAUSES;
81
+ - `cause` is 'sandbox-denied' but the two probes are missing or blank —
82
+ those probes are what distinguish a real denial from an unreachable
83
+ or auth-gated target, the misdiagnosis this gate exists to stop;
84
+ - `message` asserts a block in prose while the record left its cause
85
+ unclassified, which would smuggle the same claim past the gate.
86
+ """
87
+ cause = context.get("cause") if isinstance(context, dict) else None
88
+
89
+ if cause is not None and cause not in ALLOWED_CAUSES:
90
+ raise ValueError(
91
+ f"invalid cause: {cause!r} (allowed: {sorted(ALLOWED_CAUSES)})"
92
+ )
93
+
94
+ if cause == "sandbox-denied":
95
+ evidence = context.get("causeEvidence")
96
+ if not isinstance(evidence, dict):
97
+ raise ValueError(
98
+ "cause 'sandbox-denied' requires context.causeEvidence with "
99
+ f"{list(CAUSE_EVIDENCE_FIELDS)}"
100
+ )
101
+ normalized_evidence = {}
102
+ for field in CAUSE_EVIDENCE_FIELDS:
103
+ value = evidence.get(field)
104
+ if not isinstance(value, str) or not value.strip():
105
+ raise ValueError(
106
+ f"cause 'sandbox-denied' requires a non-empty "
107
+ f"context.causeEvidence.{field}: record the command and "
108
+ f"its raw output that proves the claim"
109
+ )
110
+ normalized_evidence[field] = _truncate_utf8(
111
+ value, CAUSE_PROBE_MAX_BYTES
112
+ )
113
+ return {**context, "causeEvidence": normalized_evidence}
114
+
115
+ if message and cause in _UNCLASSIFIED_CAUSES:
116
+ lowered = message.lower()
117
+ hit = next((t for t in _BLOCKING_CLAIM_TERMS if t in lowered), None)
118
+ if hit:
119
+ raise ValueError(
120
+ f"message asserts a blocking claim ({hit!r}) without "
121
+ "context.cause='sandbox-denied' + context.causeEvidence. "
122
+ "Either attach the two probes, or state the cause you "
123
+ "actually verified."
124
+ )
125
+
126
+ return context
127
+
128
+
53
129
  def append_jsonl_line(path, record):
54
130
  """Append a single JSON record as one line to ``path``.
55
131
 
@@ -119,6 +195,9 @@ def append_observed(
119
195
  raise ValueError(f"invalid agent: {agent!r}")
120
196
  if agent_role not in ALLOWED_AGENT_ROLES:
121
197
  raise ValueError(f"invalid agentRole: {agent_role!r}")
198
+ # Runs before append_jsonl_line so a rejected claim leaves no trace in the
199
+ # log: a written-then-flagged record is still a record someone can cite.
200
+ context = normalize_cause_context(context, message=message)
122
201
  ts = _iso(now or _now_utc())
123
202
  rec = {
124
203
  "ts": ts,
@@ -159,6 +238,7 @@ def dump_from_worker_sidecar(
159
238
  - ``agent`` or ``agent_role`` is not in the allow-lists
160
239
  - sidecar ``schemaVersion`` is not in ``SUPPORTED_SIDECAR_SCHEMA_VERSIONS``
161
240
  - any entry's ``errorType`` is not in ``ALLOWED_ERROR_TYPES``
241
+ - any entry asserts a blocking cause without its required evidence
162
242
 
163
243
  Returns 0 (no-op) if the sidecar file does not exist or its
164
244
  ``errors`` list is empty.
@@ -187,6 +267,9 @@ def dump_from_worker_sidecar(
187
267
  et = e.get("errorType")
188
268
  if et not in ALLOWED_ERROR_TYPES:
189
269
  raise ValueError(f"invalid errorType in sidecar: {et!r}")
270
+ entry_context = normalize_cause_context(
271
+ e.get("context"), message=e.get("message")
272
+ )
190
273
  rec = {
191
274
  "ts": e.get("ts"),
192
275
  "recordedAt": recorded_at,
@@ -203,7 +286,7 @@ def dump_from_worker_sidecar(
203
286
  "durationMs": e.get("durationMs"),
204
287
  "message": e.get("message"),
205
288
  "stderrExcerpt": truncate_stderr(e.get("stderrExcerpt")),
206
- "context": e.get("context"),
289
+ "context": entry_context,
207
290
  }
208
291
  append_jsonl_line(out_path, rec)
209
292
  count += 1
@@ -271,6 +271,8 @@ okstra error-log append-observed \
271
271
  --stderr-excerpt "<last stderr lines, or use --stderr-excerpt-file>"
272
272
  ```
273
273
 
274
+ Keep `--message` to the error actually observed — asserting that a sandbox or permission boundary blocked the call requires `--context-json` carrying `cause` plus both `causeEvidence` probes, and an unevidenced block claim in `--message` is rejected. If an `append-from-worker` dump is rejected for that reason, correct the offending sidecar entry and re-run the dump instead of skipping it: the dump aborts at the rejected entry, so every later entry in that sidecar never reaches the run log.
275
+
274
276
  The wrapper subagent records this through its selected adapter — Lead does NOT need to re-record. Token usage is not inferred from dispatch return values; call `collect_usage` at the start of Phase 7.
275
277
 
276
278
  ## Phase 5.5: Convergence loop
@@ -81,42 +81,33 @@ Phase 6 first produces the final-report data.json at `runs/<task-type>/reports/f
81
81
 
82
82
  For an implementation-planning run, the Report writer worker owns the Phase 6 design assessment snapshot: it writes `designPreparation` and every stage's `designSurfaceCoverage` into data.json from the detector output and consolidated plan. It does not create user inputs, consume a user answer as if it were part of that snapshot, or materialize `design-prep-requests/`; `schemas/final-report-v1.0.schema.json` and `validators/validate-run.py` `_validate_design_prep_contract` enforce the snapshot shape, detector coverage, and references.
83
83
 
84
- The four Phase 7 steps below MUST execute in this exact order. Reordering them is the recurring root cause of reports shipping with `--` token cells, Section 3 missing follow-up entries, or Section 4 rows never spawning.
84
+ Phase 7 post-processing is **one command**. `okstra report-finalize` owns the ordered sequence — it is the same code path the Codex lead adapter runs automatically, so a Claude-led run and a Codex-led run finalize identically:
85
85
 
86
- 1. **Collect usage.** Call `collect_usage` through the selected adapter with substitution enabled by the existing `okstra token-usage` workflow. One invocation aggregates `leadUsage` / `workers[].usage` / `usageSummary` into team-state, populates `tokenUsage` and the execution-status usage fields in data.json, and re-invokes the renderer so the markdown carries real numbers.
86
+ ```bash
87
+ okstra report-finalize \
88
+ --project-root <project_root> \
89
+ --run-manifest <runDirectoryPath>/manifests/run-manifest-<task-type>-<seq>.json \
90
+ --report <runDirectoryPath>/reports/final-report-<task-type>-<seq>.md
91
+ ```
87
92
 
88
- ```bash
89
- okstra token-usage \
90
- <runDirectoryPath>/state/team-state-<task-type>-<seq>.json \
91
- --write --summary \
92
- --substitute-data <runDirectoryPath>/reports/final-report-<task-type>-<seq>.data.json
93
- ```
93
+ Do NOT run the four steps below by hand. Hand-running them is the recurring root cause of reports shipping with `--` token cells, a missing html sibling, Section 3 missing follow-up entries, or Section 4 rows never spawning — the order is load-bearing and a skipped step surfaces only later, as a validator `contract-violated`. Every step is idempotent, so after fixing a reported failure just re-run the same command.
94
+
95
+ The steps it executes, in this contractual order, and the contract each one carries:
96
+
97
+ 1. **`token-usage` — collect usage.** Aggregates `leadUsage` / `workers[].usage` / `usageSummary` into team-state, populates `tokenUsage` and the execution-status usage fields in data.json, and re-invokes the renderer so the markdown carries real numbers.
94
98
 
95
99
  The data.json paths populated: `tokenUsage.lead.{totalTokens,billableTokens,costUsd}`, the `worker` / `grand` rows, `tokenUsage.cli.costUsd`, and each `executionStatus[].{totalTokens,billableTokens,costUsd,durationMs,cliTotalTokens,cliCostUsd}` for rows whose role matches a team-state worker. The data.json MUST already exist (Phase 6 output).
96
100
 
97
101
  For implementation-planning, this Phase 7 canonical render calls `materialize_design_prep_requests()` after token substitution and creates deterministic request files only for `provisional` / `blocked` items. Later answers are append-only user-input sidecars; request generation and user input never rewrite the assessment fields, so the source report remains immutable as the design-input snapshot after this render. `validators/validate-run.py` `_validate_design_prep_requests` enforces request existence, canonical path, content, and assessment fingerprint.
98
- 2. **Validate and render the report artifacts.** Always invoke the report-view renderer after the substituted data.json passes its schema/run checks; the renderer decides whether an html sibling is warranted:
99
-
100
- ```bash
101
- okstra render-views \
102
- <runDirectoryPath>/reports/final-report-<task-type>-<seq>.md
103
- ```
102
+ 2. **`render-views` render the human report artifact.** Runs against the substituted markdown; the renderer itself decides whether an html sibling is warranted.
104
103
 
105
104
  Output (idempotent — re-running overwrites):
106
105
  - `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — single-file self-contained human view, **generated when the report has at least one §1 `C-*` clarification row OR an implementation-planning Plan Approval widget target** (a sibling `final-report-*.data.json` carrying `implementationPlanning.optionCandidates`). Clarification rows with `Status` ∈ {`open`, `answered`} embed form widgets (`<select>` for enum-style decisions, `<input>` for material / data-point kinds, `<textarea>` fallback); an `Export user response` button serialises form values to a markdown sidecar (schema in [`templates/reports/user-response.template.md`](../../templates/reports/user-response.template.md)) and downloads it as `user-response-<task-type>-<seq>.md`; the user saves it to `runs/<task-type>/user-responses/` (the renderer pre-creates that directory), and `--resume-clarification` auto-appends every sidecar found there to the next run's `clarification-response.md` (`clarification_items.clarification_response_with_sidecars`). The original final-report MD is **never** mutated by user input — the sidecar is the single write target.
107
106
  - the implementation-planning report renders a **Plan Approval** section at the end of the body (implementation-option `<select>` + an approval checkbox) — disabled while any §1 `Blocks: approval` row is unresolved. Checking approval and exporting embeds a `## APPROVAL` block in the sidecar body, and the implementation-start wizard's approve-confirm step detects it and, after user confirmation, applies it through the existing `--approve` / `--implementation-option` path.
108
107
  - When the report has **no** `C-*` clarification rows and is **not** a Plan Approval widget target, the html carries no interactive forms (it would only duplicate the MD), so the renderer prints `html: skipped (...)` and writes nothing. This is the expected state for such runs — `validators/validate-report-views.py` treats "no C-* rows + no approval target + no html" as a pass, not a missing artifact.
109
108
 
110
- This step runs after usage collection so token placeholders are substituted in any rendered html and before routing persistence so the html artifact, when generated, exists for the validator step that checks it.
111
- 3. **Complete routing and follow-up persistence.** Run the follow-up task spawner when Section 4 is non-empty. It turns the report's `## 4. Follow-up Tasks` rows into `tasks/<task-group>/<new-task-id>/` stubs.
112
-
113
- ```bash
114
- okstra spawn-followups \
115
- <runDirectoryPath>/reports/final-report-<task-type>-<seq>.data.json \
116
- --project-root <project_root> \
117
- --task-group <task-group> \
118
- --parent-task-key <task-key>
119
- ```
109
+ It runs after usage collection so token placeholders are substituted in any rendered html, and before routing persistence so the html artifact, when generated, exists for the validator step that checks it.
110
+ 3. **`spawn-followups` routing and follow-up persistence.** Turns the report's `## 4. Follow-up Tasks` rows into `tasks/<task-group>/<new-task-id>/` stubs.
120
111
 
121
112
  Behaviour contract:
122
113
  - Idempotent: rows whose target dir exists are reported as `existing` and skipped. Reruns of the same parent task are safe.
@@ -131,7 +122,9 @@ The four Phase 7 steps below MUST execute in this exact order. Reordering them i
131
122
  ```
132
123
 
133
124
  The status file is written after routing and follow-up persistence completes.
134
- 4. **Execute the run-scoped cleanup gate.** Call `shutdown_workers` only after usage collection, all persistence work, and explicit user approval under [okstra-lead-contract](./okstra-lead-contract.md) "Run-scoped worker-resource lifecycle". If the user keeps resources, leave the selected adapter's resources intact and surface its manual cleanup guidance.
125
+ 4. **`validate-run` — validate the finished run.** Checks the completed artifact set, including the report-views contract that catches a missing or stale html sibling. A failure here names the specific contract; fix it and re-run `okstra report-finalize`.
126
+
127
+ After `okstra report-finalize` reports `"ok": true`, **execute the run-scoped cleanup gate.** Call `shutdown_workers` only after that success, all persistence work, and explicit user approval under [okstra-lead-contract](./okstra-lead-contract.md) "Run-scoped worker-resource lifecycle". If the user keeps resources, leave the selected adapter's resources intact and surface its manual cleanup guidance.
135
128
 
136
129
  ## Final Report Structure
137
130
 
@@ -357,7 +350,7 @@ Persistence steps that must be performed in Phase 7:
357
350
  - [ ] 6. **Generate final status file**: `runs/<task-type>/status/final-<task-type>-<seq>.status` (if necessary)
358
351
  - [ ] 7. **Save convergence state**: `runs/<task-type>/state/convergence-<task-type>-<seq>.json` (when convergence is enabled)
359
352
  - [ ] 8. **Spawn follow-up task stubs**: run `okstra spawn-followups` against the final-report per the canonical spawn rule defined in "Phase 7 follow-up task spawner" above. Do not restate the trigger condition here — that section is the single source of truth. The script is idempotent across reruns.
360
- - [ ] 9. **Human HTML report** (conditional): `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — produced by Phase 7 step 1.5 per its generation predicate (≥1 §1 `C-*` clarification row OR an implementation-planning Plan Approval widget target; that step is the single source for the exact condition). Reports matching neither condition legitimately have no html sibling; do not treat its absence as a missing artifact.
353
+ - [ ] 9. **Human HTML report** (conditional): `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — produced by the Phase 7 `render-views` step per its generation predicate (≥1 §1 `C-*` clarification row OR an implementation-planning Plan Approval widget target; that step is the single source for the exact condition). Reports matching neither condition legitimately have no html sibling; do not treat its absence as a missing artifact.
361
354
 
362
355
  ### Response after Persistence
363
356
 
@@ -119,6 +119,21 @@ Terminal statuses that can be recorded for a worker:
119
119
  4. A process exit, spawn acknowledgement, pane creation, or Result Path alone is insufficient when the backend contract requires additional status/audit artifacts.
120
120
  5. Apply the single shared retry budget through `redispatch_worker`; after the second failure, record the terminal status and proceed with reduced confidence.
121
121
 
122
+ ### Mid-run liveness probes
123
+
124
+ Between wakes, `okstra worker-liveness` is the **only** sanctioned way to ask whether a pending worker is still alive. Do NOT hand-roll a polling script, an `ls` / `stat` loop, or any ad-hoc file-existence check: a lead that writes its own probe owns that probe's bugs, and those bugs surface as *worker* failures — a shell quoting slip silently turns the probe into a no-op that reports health it never measured.
125
+
126
+ Each probe matches exactly one dispatch backend, and passing a worker to the wrong flag returns a confident wrong answer:
127
+
128
+ | Worker | Backend | Flag | What it reads |
129
+ |---|---|---|---|
130
+ | `claude-worker` | in-process subagent | `--audit` | its audit sidecar's newest `- PROGRESS:` heartbeat |
131
+ | `codex-worker` / `antigravity-worker` | CLI wrapper | `--prompt` | `<prompt>.log` / `<prompt>.status.json` |
132
+
133
+ The mismatch is not intermittent, it is guaranteed: only the `okstra-*-exec.sh` wrappers ever write `<prompt>.log` / `<prompt>.status.json`, so an in-process worker produces neither by construction. Probing a `claude-worker` with `--prompt` therefore reports `did-not-launch` for a worker that is running normally, every time, as soon as the 60s launch grace passes. Probe in-process workers with `--audit`.
134
+
135
+ Branch on the exit code, not the JSON: `0` = every probe healthy, `1` = at least one `stalled` / `did-not-launch`. The probe reports; it never kills or re-dispatches. Acting on an unhealthy verdict means spending the existing one-retry budget below.
136
+
122
137
  ## Lead Redispatch Policy on Result-Missing
123
138
 
124
139
  After each worker subagent returns (regardless of role), Lead MUST verify the canonical result file exists at the absolute path resolved from the `**Result Path:**` anchor header (against `**Project Root:**`). The check is identical for in-process workers (claude-worker) and CLI-wrapper workers (codex-worker / antigravity-worker).
@@ -128,8 +143,8 @@ After each worker subagent returns (regardless of role), Lead MUST verify the ca
128
143
  - The wrapper subagent returned an explicit `*_RESULT_MISSING` sentinel (codex-worker / antigravity-worker step 8c — `CODEX_RESULT_MISSING` / `ANTIGRAVITY_RESULT_MISSING`).
129
144
  - The result file is absent at the resolved absolute path even though the worker returned without a `*_RESULT_MISSING` sentinel — for example, claude-worker returned its final assistant message but never persisted the artifact, or the wrapper exited 0 and the codex/antigravity sub-agent forwarded raw stdout despite the contract.
130
145
  - The result file exists but cannot be parsed (frontmatter unreadable, sections 1–5 entirely missing). A truncated file in the middle of section 5 is NOT covered here — it goes to the validator's regular `error` path, not the retry path.
131
- - `okstra worker-liveness` reports the worker `did-not-launch` — neither `<prompt-path>.log` nor `<prompt-path>.status.json` exists past the launch grace (default 60s). The wrapper writes its status sidecar before invoking the CLI and hard-fails loudly with a distinct exit code on every argument check before that, so the absence of BOTH artifacts means the dispatch itself never reached the script. Without this trigger the only evidence was a lead noticing two missing files by eye, and the run paid the full polling cap for a worker that never started.
132
- - `okstra worker-liveness` reports the worker `stalled` — its audit sidecar's newest `- PROGRESS:` heartbeat is older than the cadence budget, or the sidecar carries no heartbeat at all. This is the in-process equivalent of the CLI wrappers' idle watchdog: the wrapper reaps a silent CLI itself, but nothing reaped a silent `claude-worker` until its deadline.
146
+ - `okstra worker-liveness --prompt` reports a **CLI-wrapper** worker (`codex` / `antigravity`) `did-not-launch` — neither `<prompt-path>.log` nor `<prompt-path>.status.json` exists past the launch grace (default 60s). The wrapper writes its status sidecar before invoking the CLI and hard-fails loudly with a distinct exit code on every argument check before that, so the absence of BOTH artifacts means the dispatch itself never reached the script. Without this trigger the only evidence was a lead noticing two missing files by eye, and the run paid the full polling cap for a worker that never started.
147
+ - `okstra worker-liveness --audit` reports an **in-process** worker (`claude-worker`) `stalled` — its audit sidecar's newest `- PROGRESS:` heartbeat is older than the cadence budget, or the sidecar carries no heartbeat at all. This is the in-process equivalent of the CLI wrappers' idle watchdog: the wrapper reaps a silent CLI itself, but nothing reaped a silent `claude-worker` until its deadline.
133
148
  - The result file exists but its audit sidecar does not, at `runs/<task-type>/worker-results/<worker>-audit-<task-type>-<seq>.md`. Workers write both in the same step, so a result without a sidecar means the Reading Confirmation block — the only evidence the worker read its inputs — was never produced. `validate-run.py` fails the run on this at Phase 7 either way (`validate_worker_results_audit`); checking it here spends the existing one-retry budget while the role can still be re-dispatched, instead of surfacing hours later when the worker session is gone.
134
149
 
135
150
  **One-retry policy:**
@@ -120,7 +120,15 @@
120
120
  - validation checklist (pre / mid / post) — each item is an exact command or observable outcome
121
121
  - rollback strategy — exact revert path (commits, flags, migrations) and the signal that triggers rollback
122
122
  - **Requirement admissibility (scope boundary):** a brief line becomes a Requirement Coverage row only when **a stage can satisfy it by changing files in this repository** — source, tests, config, or deployment *manifest files*. A line whose satisfaction needs a person's approval, a ticket status change, or an action against live infrastructure (applying a manifest, a cutover, creating a dashboard or alert, validating in staging/production) is NOT a requirement for this phase: it belongs to the brief's `## External Gates`, and this plan neither creates a stage for it nor cites it in coverage. Briefs generated by `okstra-brief-gen` pre-split these into `## Acceptance Criteria` (admissible) and `## External Gates` (not); when reading an older brief that carries a raw Definition-of-Done checklist, apply the same test line by line. The boundary is the *action*, not the topic — "add the flag to `values-prod.yaml`" is admissible, "apply that manifest to prod" is not. Planning an operational stage this phase cannot execute (see the run-scope rule above forbidding deployments) produces steps whose commands never resolve, which the §5.5.9 gate then correctly blocks — the plan must not create that deadlock in the first place.
123
- - **Requirement Coverage (mandatory, §5.5.8):** one row per concrete requirement from the task brief / packet. Assign stable IDs `R-001`, `R-002`, ... in source order. Columns: `ID | Source | Requirement | Covered by option / stage / step | Status`. `Source` cites the brief heading or file/line where the requirement came from. `Covered by` must name the specific Option Candidate and Stage/Step that satisfies it, not just "recommended option". **Enforced:** `validators/validate-run.py` `_validate_requirement_coverage_covered_by` fails a `covered` row whose `coveredBy` is bare "recommended option", names no Option/Stage/Step anchor, or cites a Stage number absent from the Stage Map (whether the cited step *actually satisfies* the requirement remains a worker `DISAGREE(f)` judgment). `Status` is one of `covered`, `gap`, or `blocked C-NNN`. If any row is `gap` or `blocked C-NNN`, the Plan Body Verification gate MUST NOT be `passed` / `passed-with-dissent`; add a matching `Blocks=approval` row for the blocker and keep `approved: false`.
123
+ - **Scope provenance (BLOCKING):** every Requirement Coverage row's `Source` cell must be exactly one of three forms, and every Stage Map stage must be cited by at least one coverage row's `Covered by`. The forms:
124
+ - `brief:<heading>` — a heading that literally exists in the task brief. A heading the brief does not contain is a fabricated requirement.
125
+ - `derived:R-NNN — <one-line rationale>` — derived from another row in the same table. The chain must terminate at a `brief:` or `contract:` row and must not cycle. Use this for genuine technical consequences: a migration stage is `derived:R-003 — R-003 cannot be satisfied without a schema change`.
126
+ - `contract:<rule>` — an artifact okstra's own phase contract mandates, so it has no brief line to cite. The allowlist is exactly `decision-record-step` (the §5.4 Decision Drafts materialization step) and `glossary-step` (the glossary proposal step); the SSOT is `scripts/okstra_ctl/scope_provenance.py`. Never widen this form to launder work the brief did not ask for.
127
+ An item you can give none of these three sources to is **not a requirement and not a stage**. Its only admissible outlet is a `## 1. Clarification Items` row with `Blocks=approval`, carrying the recommendation format from `_clarification-recommendation.md`. Do not fold it into an option, a stage, or a step "while we are in here" — that is the scope expansion this rule exists to stop. This makes concrete the planning-input rule that any change beyond what `Requirement Summary` explicitly demands is out of scope by default. **Enforced:** `validators/validate-run.py` `_validate_requirement_provenance` (source resolution) and `_validate_stage_has_requirement` (no stage without a requirement).
128
+ - **The reach of this gate — do not over-trust it.** What is mechanically enforced is the *form* of each source, that a `brief:` heading literally exists, that a `derived:` chain terminates without cycling, and that no stage is uncited. What is **not** enforced is whether the cited source genuinely demands the requirement. Every brief carries the same generic headings (`## Acceptance Criteria`, `## Context`, …), so attaching invented work to a real heading still parses clean. The gate's value is that it forces every item to name an origin and makes fabrication explicit and auditable — judging whether that origin actually demands the item remains a reviewer / `DISAGREE(f)` responsibility, and passing this gate is never evidence that the scope is justified.
129
+ - **Stage citation format:** the reverse check reads each `Covered by` cell as prose, so a stage counts as cited only when its number is anchored to a `Stage` / `Stages` word on the same line. These all read: `Stage 2`; `Stage 1, Stage 2, Stage 3`; `Stages 1, 2, 3`; `Stages 1, 2, and 3`; ranges (`Stages 1-3`, `Stages 1 to 3`, `Stages 1 through 3`); and `and` / `&` conjunctions. A bare number with no `stage` word anchoring it is NOT read as a citation, so `covered by the recommended option, step 4` cites nothing. **Enforced:** `validators/validate-run.py` `_validate_stage_has_requirement` fails the plan when any Stage Map stage is cited by no coverage row.
130
+ - Because that reader only sees prose, it cannot tell a citation from a mention: `Stage 1 (superseded by Stage 2)` still counts Stage 1 as cited, and one row citing `Stages 1-64` rubber-stamps every stage in the map. Cite the stages a requirement is actually satisfied by — not stages merely mentioned, and never a blanket range standing in for the work of checking.
131
+ - **Requirement Coverage (mandatory, §5.5.8):** one row per concrete requirement from the task brief / packet. Assign stable IDs `R-001`, `R-002`, ... in source order. Columns: `ID | Source | Requirement | Covered by option / stage / step | Status`. `Source` follows the three-form grammar defined in the **Scope provenance** rule above — a free-form `file:line` is not one of the three forms and is rejected. When this run's brief is a fan-out packet, the task manifest's `taskBriefPath` points at that packet file, so `brief:` cites the packet's own headings (`## Scope`, `## Evidence`, `## Requirement Provenance`) — not the headings of the upstream user brief the packet came from. `Covered by` must name the specific Option Candidate and Stage/Step that satisfies it, not just "recommended option". **Enforced:** `validators/validate-run.py` `_validate_requirement_coverage_covered_by` fails a `covered` row whose `coveredBy` is bare "recommended option", names no Option/Stage/Step anchor, or cites a Stage number absent from the Stage Map (whether the cited step *actually satisfies* the requirement remains a worker `DISAGREE(f)` judgment). `Status` is one of `covered`, `gap`, or `blocked C-NNN`. If any row is `gap` or `blocked C-NNN`, the Plan Body Verification gate MUST NOT be `passed` / `passed-with-dissent`; add a matching `Blocks=approval` row for the blocker and keep `approved: false`.
124
132
  - **Review-rule compliance plan:** when a project-local review rule pack is found, each Option Candidate MUST include the design implication of those rules in its File Structure / interfaces / blast-radius notes. For any helper or data transform used by more than one changed service, the plan must either place it in a shared module or explicitly justify why duplication is intentional. For any test step, the plan must state the observable behavior being asserted, not the internal collaborator call being pinned. For any exported/public method added or renamed, the step must carry the intended noun/side-effect semantics so implementation names can be reviewed before code is written.
125
133
  - the YAML frontmatter MUST include the line `approved: false` (report-writer always emits the unflipped value). The user authorises the next `implementation` run by flipping it to `approved: true` (manual edit or `--approve` CLI). Do NOT recreate any `User Approval Request` body block — the validator fails reports that contain one (see `validators/validate-run.py` deprecated patterns).
126
134
  - the YAML frontmatter MUST include the line `implementation-option:` directly under `approved:` (report-writer always emits it with an **empty value**). The user selects which Option Candidate the next `implementation` run executes by filling this line with that option's name (manual edit or `--implementation-option <name>` CLI). When left empty, the `implementation` run falls back to the `Recommended Option`.
@@ -144,7 +152,7 @@
144
152
  2. **Placeholder scan** — search the report for the patterns in the No-placeholder rule above and fix inline.
145
153
  3. **Internal consistency** — option file lists, trade-off matrix, and recommended step list must agree on file paths, names, and signatures. A symbol called `clearLayers()` in the matrix and `clearFullLayers()` in the steps is a bug.
146
154
  4. **Ambiguity check** — any requirement that could be read two ways must be made explicit or moved to the `## 1. Clarification Items` table as a `Blocks=approval` row.
147
- 5. **Scope check** — if the recommended plan now spans multiple independent subsystems, recommend splitting into separate planning runs rather than shipping an oversized plan.
155
+ 5. **Scope check** — if the recommended plan now spans multiple independent subsystems, recommend splitting into separate planning runs rather than shipping an oversized plan. Then walk the plan in the expansion direction: for every stage, name the Requirement Coverage row that demanded it, and for every requirement row, read its `Source` cell as a skeptic — does the cited brief heading actually exist, and does a `derived:` rationale state a real technical consequence rather than a preference? Move anything that fails to a `Blocks=approval` clarification row.
148
156
  6. **Review-rule preflight check** — if a project review rule pack exists, map each relevant rule to the recommended option. Reject the draft if it knowingly creates a violation that the later PR reviewer would flag, unless the plan records a specific rationale and follow-up. In particular, scan for repeated helper stacks across planned files, tests that assert delegation to the same calculator/helper they exercise, public names that hide side effects, domain rules placed in repositories/adapters, and APIs made dead by this change.
149
157
  7. **Plan-body verification reconciliation (BLOCKING for implementation-planning).** For every §5.5.9 `planItems[]` entry whose verdicts make it `majority-disagree`, set that item's `clarificationId` to a `C-<N>` row that MUST exist in `## 1. Clarification Items` with `Kind` chosen per the standard policy and `Blocks=approval`. **Enforced:** `validators/validate-run.py` `_validate_plan_body_clarification_matching` recomputes each item's class and fails when a majority-disagree item has no `clarificationId`, or its `clarificationId` is dangling / points at a non-`approval` row. For `partial-consensus` and `dissent-isolated` plan-items, the dissenting opinion lives in §5.5.9 `Dissent log` and is NOT promoted to §5.
150
158
  8. **Stage Map self-check** — for every stage, count the effective rows of its `Stepwise Execution Order` table by hand; reject the draft if any stage exceeds 8. Confirm each stage declares a non-empty `Slice value:` and `Acceptance:` line, the three `Test case (success|boundary|failure):` lines (or carries a `TDD exemption:` line), and that its first step `action` starts with `RED:` with a later `GREEN:` — this is what validator S10 enforces, including S10d on the test-case lines. Read each stage's three test-case lines as a reviewer: reject any that restates the happy path in all three slots, leaves `boundary` blank, or writes `N/A` where a real edge input exists. Walk the `depends-on` graph and confirm it is a DAG (no cycle, no self-reference). For each `depends-on` link, confirm it encodes a real data/contract dependency — do NOT add links to serialise unrelated work, and do NOT split a stage merely to create more parallel stages. **Parallel-safety:** for every pair of `depends-on (none)` stages, confirm their `Stage Exit Contract` predicted file sets are disjoint; if they share a file, merge them or add a `depends-on` link (validator S9 rejects overlap). **Project-boundary:** confirm no stage mixes edits from two projects (different repo/`PROJECT_ROOT` or different top-level deployable module); if any stage does, split it per project. For multi-project plans, confirm each stage's `title` carries its `[<project>]` tag and the `Cross-project parallelism:` line under the table records the parallel-vs-sequenced determination (with the forcing dependency) for every project pair; for cross-repo work, confirm it is split into separate per-repo runs (required — one run structurally cannot touch another repo) rather than crammed into one task's stages.
@@ -35,6 +35,7 @@
35
35
  `domain`(work-category 5-enum: bugfix / feature / refactor / ops / improvement),
36
36
  `depends-on`(an inline list of unit-ids within the same fan-out `[unit-001]`, or `[]` if none),
37
37
  `recommended-next-phase`(error-analysis | implementation-planning) are filled in.
38
+ Each packet MUST carry a non-empty `## Requirement Provenance` section whose every bullet is `brief:<heading>` (a heading that literally exists in the task brief) or `contract:<rule>`. `derived:` is not admissible in a packet — cross-packet derivation cannot be resolved from a single packet, so each unit anchors directly on the brief. A unit you cannot source that way is not a work item: raise it as a clarification row instead of publishing a packet for it. **Enforced:** `validators/validate_fanout.py` `_check_provenance`.
38
39
  - in `runs/requirements-discovery/fan-out/index.md`, list the packets in depends-on topological order
39
40
  as a numbered list (`1. unit-001`) (a generated view; explicitly do not hand-edit). The depends-on graph
40
41
  must be a DAG — `validate_fanout` rejects a cycle as a validation failure, so if a cycle appears,
@@ -61,7 +62,7 @@
61
62
  - if any blocking input is missing at the time of writing the final report, populate `## 1. Clarification Items` in `final-report-template.md` (a single unified table; `Blocks=next-phase` for items the next run cannot start without)
62
63
  - prefer concrete questions whose answers map directly to a routing decision (`bugfix` vs `feature`, `error-analysis` vs `implementation-planning`, etc.). State each option in plain language with one sentence describing what choosing it would mean for the next phase.
63
64
  {{INCLUDE:_clarification-recommendation.md}}
64
- - **Codebase-first ambiguity resolution (defect rule)**: any ambiguity that can be answered by `Read` / `Grep` / file inspection MUST be resolved that way and recorded with file:line evidence. Writing a clarification row for something the codebase already answers is a defect of this phase.
65
+ - **Codebase-first ambiguity resolution (defect rule)**: any ambiguity that can be answered by `Read` / `Grep` / file inspection MUST be resolved that way and recorded with file:line evidence. Writing a clarification row for something the codebase already answers is a defect of this phase. **Boundary — facts only, never intent:** this rule governs questions of *fact* ("what does this code do", "where is this called", "does this field exist"). It never governs questions of *intent* ("should we do this at all", "is this in scope", "which outcome does the reporter want"). Scope and intent are not the kind of question a codebase can answer, so inspecting files never discharges them — raise them as clarification rows. Resolving a scope question by inference and building on that inference is the mirror defect, and a more expensive one.
65
66
  - **Evidence note required inside `Statement`**: every clarification row includes `Evidence checked: <path:line>` or `Evidence checked: none — <human-only reason>` in the `Statement` cell. `none` is allowed ONLY when the row's nature is "only a human can answer this" (reporter intent, business priority, external authority). A row with `none` that *could* have been answered by the codebase is a defect.
66
67
  - Cross-verification mode:
67
68
  - Phase 5.5 convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each worker's finding by directly re-inspecting the cited evidence; the burden of proof sits on the claim. See `prompts/lead/convergence.md` §"Adversarial Verification Mode". A single evidence-backed refutation prevents a finding from reaching consensus.