okstra 0.127.0 → 0.128.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.
@@ -21,6 +21,7 @@ The task manifest, task index, instruction set, runs, and history are collected
21
21
  │ ├── analysis-material.md
22
22
  │ ├── reference-expectations.md
23
23
  │ ├── task-brief.md
24
+ │ ├── verification-target.md # final-verification only; canonical target snapshot
24
25
  │ ├── directive.txt # optional (mirrors --directive)
25
26
  │ ├── final-report-schema.json
26
27
  │ ├── final-report-template.md
@@ -106,6 +107,8 @@ The resolved run directory collects execution history. It divides its contents i
106
107
  Worker prompt history is retained not under `/tmp`, but always as a canonical artifact under `prompts/` for the current run.
107
108
  Unlike before, `analysis-profile.md`, `analysis-material.md`, `reference-expectations.md`, `task-brief.md`, skill copies, and `final-report-template.md` are not duplicated for every run.
108
109
  These materials retain canonical copies in `instruction-set/` under the stable task root.
110
+ For `final-verification`, `verification-target.md` is also stable-task instruction-set state. Prepare owns the snapshot and stores its project-relative path plus SHA-256 digest in task/run manifests and active-run context. Initial analysis prompts carry the compact worktree/scope/base/head/path/digest identity and read this sidecar on demand instead of copying its diff stat.
111
+ Each wrapper `<prompt>.log` beside a persisted `<prompt>.md` is a growing execution transcript, not another prompt snapshot. `okstra log-report` pairs the siblings and reports prompt/transcript bytes separately while keeping the legacy log-oriented `sizeBytes` and `totalBytes` fields.
109
112
  When the same non-stage task type runs again, it reuses the same `runs/<task-type>/` run directory. Reruns of `implementation` and single-stage `final-verification` reuse their resolved `runs/<task-type>/stage-<N>/` run directories. In both cases, existing artifacts are not overwritten because run-level filenames are separated by `-<task-type>-<seq>` suffixes within type-specific subdirectories. Because `<seq>` is scanned independently for each category directory (`manifests/`, `prompts/`, `reports/`, `status/`, `state/`, `sessions/`, `worker-results/`) beneath the resolved run directory, values may differ by category even within the same run.
110
113
  If flat legacy artifacts remain at the top level of a task-type run directory, the next execution automatically moves them into the corresponding type-specific subdirectories.
111
114
 
@@ -262,6 +265,7 @@ Main contents:
262
265
  - expected report relative path
263
266
  - expected status relative path
264
267
  - prompt snapshot relative path
268
+ - final-verification target snapshot relative path and digest (empty for other task types)
265
269
  - `worker prompt directory relative path`
266
270
  - `worker prompt relative path by worker id`
267
271
  - current run status
@@ -299,14 +303,17 @@ After `okstra` runs, Claude should follow this default sequence when reading the
299
303
  3. Read `task-manifest.json`.
300
304
  4. If the current `state/active-run-context-<task-type>-<seq>.json` exists, read it as the primary input for lead Phase 1. This file is a compact intake that reconstructs run artifact paths from `identity` + `pathHints` clues. If it does not exist, fall back to the current `manifests/run-manifest-<task-type>-<seq>.json` and `team-state`.
301
305
  5. Read `instruction-set/analysis-profile.md` and `instruction-set/analysis-packet.md`.
306
+ For final-verification, use the compact target identity from active context and open `instruction-set/verification-target.md` only when its full mapping or diff stat is needed.
302
307
  6. Read `task-index.md` only when a quick summary is needed.
303
308
  7. Lazily read `analysis-material.md`, `reference-expectations.md`, `task-brief.md`, and `final-report-template.md` when the packet is insufficient or when source citations/report writing require them.
304
309
  8. Consult `history/timeline.json` and previous run results if necessary.
305
310
  9. As `Claude lead`, organize roles according to the current run's worker roster (`Claude worker`, `Codex worker`, and `Report writer worker` by default; `Antigravity worker` only when explicitly included).
306
311
  10. Save each selected worker prompt under the current run's `prompts/` directory at the assigned worker prompt history path before dispatching the worker.
307
- 14. Collect a result or terminal status for each required worker.
308
- 15. Unless the brief requires a more specific format, write the final Markdown report using the `final-report-template.md` structure.
309
- 16. Save the result directly to `reports/final-report-<task-type>-<seq>.md` for the current run and, if necessary, update `status/final-<task-type>-<seq>.status`, `manifests/run-manifest-<task-type>-<seq>.json`, `task-manifest.json`, and `task-index.md` to match the current state.
312
+ Every selected final-verification initial analysis worker receives the same full-core semantic body. Dispatch and Phase 7 validate compactness and normalized equality; report-writer and `-reverify-r<N>-` prompts are separate audiences.
313
+ 11. In convergence Round 0, classify matching semantic/ticket-set findings as full consensus and queue only single-worker findings. Reverify each queued finding through other workers only; lightweight reverify does not reload the original bundle.
314
+ 12. Collect a result or terminal status for each required worker.
315
+ 13. Unless the brief requires a more specific format, write the final Markdown report using the `final-report-template.md` structure.
316
+ 14. Save the result directly to `reports/final-report-<task-type>-<seq>.md` for the current run and, if necessary, update `status/final-<task-type>-<seq>.status`, `manifests/run-manifest-<task-type>-<seq>.json`, `task-manifest.json`, and `task-index.md` to match the current state.
310
317
 
311
318
  Recommended worker status values:
312
319
 
@@ -84,7 +84,7 @@ okstra's prepare responsibilities are consolidated in a single Python entry poin
84
84
  - Resolves task-brief / clarification-response paths (cwd first → PROJECT_ROOT fallback)
85
85
  - Computes the stable task root and all paths/sequences inside a per-task mutex (`~/.okstra/.locks/<task-key>.lock`), then persists them to `<run-dir>/manifests/run-context-<seq>.json`
86
86
  - Persists user input to `<run-dir>/manifests/run-inputs-<seq>.json`
87
- - Renders the instruction set (`analysis-profile.md`, `analysis-packet.md`, `analysis-material.md`, `task-brief.md`, `reference-expectations.md`, `final-report-template.md`, `final-report-schema.json`, optional `clarification-response.md`, optional `directive.txt`, `claude-execution-prompt.md`) and writes a run prompt snapshot
87
+ - Renders the instruction set (`analysis-profile.md`, `analysis-packet.md`, `analysis-material.md`, `task-brief.md`, `reference-expectations.md`, `final-report-template.md`, `final-report-schema.json`, optional `clarification-response.md`, optional `directive.txt`, final-verification-only `verification-target.md`, `claude-execution-prompt.md`) and writes a run prompt snapshot
88
88
  - Updates `task-manifest.json`, `task-index.md`, `run-manifest-*.json`, `history/timeline.json`, and `discovery/{latest-task,task-catalog}.json`
89
89
  - Writes a preassigned Claude session ID and `sessions/claude-resume-*.sh` (unless `--render-only` is used)
90
90
  - Records record_start in the central indexes (`~/.okstra/{active,recent}.jsonl`, `projects/<id>/{index.jsonl, meta.json}`)
@@ -304,6 +304,16 @@ The standard `okstra` workflow applies the following team contract consistently
304
304
  - Every attempted worker (`completed`, `timeout`, `error`) must have an assigned worker prompt history file under the current run's `prompts/` directory.
305
305
  - An unnamed generic parallel worker is not accepted as a substitute for a required role.
306
306
 
307
+ ### Final-verification prompt and re-verification boundaries
308
+
309
+ Final-verification uses one shared full-core responsibility for every selected initial analysis worker. Provider/model diversity supplies independent observations of the same requirements; it does not split acceptance criteria into disjoint worker scopes. `analysis-packet.md` carries the worker-facing verification procedure, while report-only deliverable and self-review guidance stays with the report writer.
310
+
311
+ At final-verification entry, `prepare_task_bundle()` snapshots the resolved worktree, verification scope, base ref, head ref, implementation/stage report mapping, and diff stat into `instruction-set/verification-target.md`. It records the sidecar's project-relative path and `sha256:<hex>` digest in the task/run manifest and active-run context. Initial prompts carry only six compact target identity headers plus one `analysis-packet.md` path; workers open the target sidecar on demand. The coding-preflight anchor remains implementation-only, regardless of whether an initial pane is displayed with the role `verifier`.
312
+
313
+ All selected initial prompt bodies must normalize to the same semantic content. One optional shared `## Run-specific directive` is bounded to 40 nonblank lines, and larger material is placed in the instruction set. `dispatch_core.py` and `codex_dispatch.py` enforce this before process launch; Phase 7 `validators/validate-run.py` applies the same `worker_prompt_contract.py` rules to the persisted initial prompt paths. Report-writer and `-reverify-r<N>-` prompts are outside this initial-prompt gate.
314
+
315
+ The convergence algorithm is unchanged: Round 0 immediately classifies findings with matching semantics and ticket sets from two or more workers as `full-consensus`; only single-worker findings enter the verification queue. For worker W, `items_for_W` excludes findings originating from W, and resolved findings are removed permanently. Lightweight reverify receives only that current batch and its embedded evidence, not the original analysis packet/profile/brief/instruction set. The report writer does not vote, and terminal worker non-results are recorded as `verification-error`, never fabricated as `DISAGREE`.
316
+
307
317
  ## Stable task identity
308
318
 
309
319
  `okstra` uses the following combination as its basic identifier:
@@ -733,6 +743,7 @@ Errors that occur while workers (Claude/Codex/Antigravity worker, Report writer,
733
743
  ### Live-log mirror (codex / antigravity wrapper)
734
744
 
735
745
  - On every dispatch, `scripts/okstra-codex-exec.sh` and `scripts/okstra-antigravity-exec.sh` create a `<prompt>.log` sidecar next to the prompt path and mirror stdout there through a named FIFO (which keeps the CLI a single addressable PID for the idle watchdog). stderr is appended to the same file, preserving the subagent stderr-capture contract, and the file is truncated on each dispatch. This solves the problem where a calling subagent polls `BashOutput` every 60 seconds, leaving users unable to detect a stalled state during long-running work such as large-codebase scans in analysis or cargo / pytest in implementation.
746
+ - The `.log` is an execution transcript containing wrapper/CLI output, not the original prompt. `okstra log-report` pairs it with the sibling persisted `.md` prompt and reports `transcriptBytes`, `promptBytes`, and their ratio separately. The compatibility fields `path`, `sizeBytes`, and `totalBytes` continue to mean transcript data, so existing consumers and transcript-size ordering remain stable.
736
747
  - **Per-block cap on the codex log copy** (`okstra_log_mirror` in `scripts/okstra-codex-exec.sh`): workers read their required inputs end-to-end per the Worker Preamble's *Reading rules*, so a single report read can dump 170KB+ into the log and observed sidecars reach 8MB. The mirror keeps the first `log_block_line_cap` (120) lines of each output block and replaces the remainder with a `[okstra log-mirror] N line(s) elided` marker, draining every 500 elided lines so the idle watchdog keeps seeing writes. **Only the log copy is capped** — the stdout passthrough stays byte-identical, so the dispatching subagent's `BashOutput` and Phase 5 synthesis are unaffected (`tests-js/codex-log-mirror.test.mjs` asserts that byte-identity). The marker set is codex-specific; the claude wrapper emits `--output-format=stream-json` and does not share this filter.
737
748
  - When tmux is reachable in the lead environment, the wrapper automatically splits a sibling pane and runs `tail -F <log-path>`. The trace-pane title appends `-tail` to the caller (worker) pane title: `<cli>-<role>-<pid>-tail` (for example, `codex-worker-93421-tail`). At the same time, the caller (worker) pane title is set to `<cli>-<role>-<pid>`. `<pid>` is the wrapper's own PID, so multiple workers with the same role spawned concurrently remain distinguishable, and operators can visually map `<caller> ↔ <caller>-tail`. **Caller-pane resolution**—because the Claude Code Bash tool now removes both `$TMUX` and `$TMUX_PANE` from the environment, the wrapper does not depend on environment variables. It (1) derives `<RUN_DIR>` as `dirname(dirname(prompt_path))` from the prompt path (paths.py SSOT), and (2) reads `<RUN_DIR>/state/lead-pane.id`, written once by the lead in its foreground pane, as the split anchor. This remains reliable for background dispatches, unlike active-pane guessing, even if the user changes panes. If the file is absent or the pane is stale, it falls back to `tmux display-message -p '#{pane_id}'` (the active pane). The trace split explicitly anchors to that caller pane with `-t`. The role is the wrapper's fifth optional positional argument and defaults to `worker`. The caller pane title is captured and restored by an EXIT trap, preventing stale titles across dispatches. Focus returns to the caller pane, and the trace pane remains after CLI exit so its scrollback is available. All paths silently degrade when tmux is unreachable, splitting fails, or tmux is outdated.
738
749
  - **Run-scoped tagging for cleanup**: A trace pane's `tail -F` is a child of the tmux shell and survives Claude's exit. The wrapper tags each spawned pane with `tmux set-option -p @okstra_trace_run=<RUN_DIR>`, and `okstra-trace-cleanup.sh` discovers panes server-wide from that tag via `tmux list-panes -a` and runs `tmux kill-pane`. It requires neither tmux environment variables nor a pane-ID registry. Because the tag is run-scoped, it does not kill trace panes from other simultaneous okstra runs. Cleanup has two entry forms: the lead invokes it with `--run-dir <RUN_DIR>` to clean traces and worker-agent panes for that run, or the `hooks.SessionEnd` entry in `templates/reports/settings.template.json` invokes it with `--reap` to clean all trace panes tagged below `$CLAUDE_PROJECT_DIR/.okstra/` when no single run directory exists at session end. Missing tmux and stale pane IDs silently degrade.
package/docs/cli.md CHANGED
@@ -639,6 +639,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
639
639
  | `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 |
640
640
  | `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 |
641
641
  | `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 |
642
+ | `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 |
642
643
  | `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 |
643
644
  | `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 |
644
645
  | `okstra pr <template\|branches\|gen> …` | Backend for the okstra-pr-gen skill. Git-only—no project registration required. `template list\|show <name\|default>\|add --name <name> (--content <text>\|--file <path>)\|path` manages PR body templates under `~/.okstra/template/pr/` (bundled fallback `src/commands/pr/default.md`); `branches` recommends a base branch; `gen --base <ref> [--template <name\|default>]` emits a JSON bundle of the template plus `<base>..HEAD` commits and `<base>...HEAD` diffstat |
@@ -219,7 +219,7 @@ Important modules:
219
219
 
220
220
  | Module | Role |
221
221
  |---|---|
222
- | `run.py` | `prepare_task_bundle()` single authority and CLI parser |
222
+ | `run.py` | `prepare_task_bundle()` single authority and CLI parser; for final-verification it also owns `verification-target.md` snapshot/digest materialization before manifests and prompts are rendered |
223
223
  | `implementation_stage.py` | `implementation` single-stage run orchestration — read the Stage Lifecycle Snapshot → pick an available Stage Map entry → provision an isolated stage worktree → publish the selected stage as run context (extracted from `run.py`) |
224
224
  | `stage_targets.py` | Stage readiness/verification policy SSOT — from the Stage Lifecycle Snapshot (`consumers.jsonl` ledger + carry sidecar backfill + active registry reservation) it decides which stage is runnable, which commit it branches from, and what final-verification checks. `order_stage_closure` topologically sorts (Kahn) the dependency closure of the wizard's multi-selected stage set to produce the unattended `chain-stages` chaining order |
225
225
  | `stage_fix_carry.py` | fix-run carry derivation for a re-run on an `implementation` stage whose latest final-report data.json carries verifier `FAIL` verdicts — collects the previous report path, previous run HEAD, failed verifiers, carried blocking findings, and a routing recommendation, which `run.py` renders into the analysis profile through the `{{FIX_RUN_CONTEXT}}` token. A first run, or a re-run after `PASS`, yields no carry and renders the token empty |
@@ -251,7 +251,7 @@ Important modules:
251
251
  | `run_index_row.py` | single reference point for creating / slimming / hydrating a `~/.okstra` run-index row — runId SSOT, preserves projectId raw |
252
252
  | `error_report.py`, `error_log_core.py`, `error_zip.py` | backend for the okstra-inspect errors/error-zip facets — `error_log_core` is the read-only core that globs/parses/aggregates `errors-*.jsonl`, `error_report` renders the errors facet, and `error_zip` collects cross-project run directories, allowlist-anonymizes, aggregates clusters, and produces a zip |
253
253
  | `worker_heartbeat.py`, `worker_liveness.py` | `worker_heartbeat` is the single definition of the `- PROGRESS:` heartbeat line shape and its 5-minute (+60s grace) cadence budget, shared by the Phase 7 audit (`validators/validate_session_conformance.py`) and the live probe; `worker_liveness` backs `okstra worker-liveness`, reporting a pending worker as `stalled` (heartbeat past the budget) or `did-not-launch` (no wrapper `.log`/`.status.json` past the launch grace) |
254
- | `log_report.py`, `time_report.py` | read-side backend for the okstra-inspect logs/time facets (`okstra log-report` is the wrapper sidecar log inventory, `okstra time-report` is per-task time aggregation) |
254
+ | `log_report.py`, `time_report.py` | read-side backend for the okstra-inspect logs/time facets (`okstra log-report` pairs each wrapper transcript `.log` with its sibling prompt `.md` and reports both byte counts without changing legacy transcript-size fields; `okstra time-report` is per-task time aggregation) |
255
255
  | `rollup.py` | read-side backend for the okstra-rollup skill — fans the catalog out per task-group (or the whole project) and deterministically aggregates each task's run count, elapsed time (raw ms), error count, and latest report path, plus group-level totals/status, category, and phase distribution. Reuses the `time_report`/`error_log_core` functions and delegates report-body synthesis to the skill |
256
256
  | `usage_report.py` | Read-only okstra-usage backend — scans the whole current project's recent run timelines, defaults to 30 days, and returns task-type coverage, raw/billable tokens, known USD cost, CPU-sum and wall-clock milliseconds, unavailable reason counts, and unmatched pricing models |
257
257
  | `json_registry.py` | shared flock + atomic JSON persistence for small okstra registries (`registry_lock`/`load_registry_json`/`save_registry_json`) — shared by `container_registry` and `worktree_registry` |
@@ -268,9 +268,9 @@ Important modules:
268
268
  | `manager_store.py` | Manager-owned state mutation — project membership, task planning, assignment, directives, event append |
269
269
  | `manager_sync.py` | One-way child project `.okstra` snapshot reader; corrupt child state becomes row-level `error` so other children continue |
270
270
  | `manager_launch.py` | Child launch packet and manager child context renderer; records `prepared` launch metadata/events without changing project-local task state |
271
- | `dispatch_core.py` | Backend-neutral worker dispatch core — worker execution/collection logic shared by any lead runtime (Claude/Codex/external) |
272
- | `codex_dispatch.py` | Codex lead CLI-worker dispatcher — the `okstra codex-dispatch` backend. Reads the run manifest to run the Codex-side supported worker subset and performs token-usage substitution, view render, follow-up, and validation |
273
- | `analysis_packet.py` | assembles the compact analysis-worker input packet for a task run (error-analysis phase worker input) |
271
+ | `dispatch_core.py` | Backend-neutral worker dispatch core — worker execution/collection logic shared by any lead runtime (Claude/Codex/external); gates selected final-verification initial prompts through the shared compact/equality contract before launch |
272
+ | `codex_dispatch.py` | Codex lead CLI-worker dispatcher — the `okstra codex-dispatch` backend. Reads the run manifest to run the Codex-side supported worker subset, applies the same final-verification initial-prompt gate, and performs token-usage substitution, view render, follow-up, and validation |
273
+ | `analysis_packet.py` | assembles the compact analysis-worker input packet for a task run; final-verification packets include worker verification procedure but omit report-only deliverable/self-review guidance |
274
274
  | `context_cost.py` | read-side context-cost estimator for a prepared okstra task bundle (the `okstra context-cost` backend) |
275
275
  | `schema_excerpt.py` | generates a task-type-scoped excerpt of the final-report schema — a schema reduction to inject into the worker/lead prompt |
276
276
  | `work_categories.py` | requirements-discovery work-category (domain) **SSOT** (`is_valid_category`) — the work-category allowlist is defined only here |
@@ -278,7 +278,8 @@ Important modules:
278
278
  | `lead_runtime.py` | lead runtime metadata shared by the render and prepare paths (`LeadRuntimeInfo`) |
279
279
  | `lead_events.py` | structured JSONL events emitted by non-Claude lead runtimes |
280
280
  | `team_reconcile.py` | stale team-member reconciliation at run-end teardown |
281
- | `worker_prompt_headers.py` | shared rendering of the worker prompt anchor header (`worker_prompt_headers`) |
281
+ | `worker_prompt_headers.py` | shared rendering of phase-aware worker prompt anchors (`worker_prompt_headers`): coding-preflight only for implementation and compact target identity for final-verification |
282
+ | `worker_prompt_contract.py` | deterministic compact initial final-verification prompt validator and normalized cross-worker equality SSOT, reused by dispatch adapters and the Phase 7 persisted-artifact validator |
282
283
  | `wrapper_status.py` | worker wrapper status sidecar reader — the host-side reader of the sidecar written by `okstra-wrapper-status.py` (the heartbeat writer) |
283
284
  | `task_target.py` | shared helper resolving `task-key → (task_root, project_root)` (`resolve_task_root`) |
284
285
  | `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/` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.127.0",
3
+ "version": "0.128.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.127.0",
3
- "builtAt": "2026-07-20T14:29:59.831Z",
2
+ "package": "0.128.0",
3
+ "builtAt": "2026-07-20T15:52:59.156Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -28,6 +28,8 @@ This contract governs **Phase 5.5 (Convergence loop)** — a *lead operating pha
28
28
 
29
29
  When this contract says "queue" without qualifier, it means the *verification queue*: the set of findings that are still candidates for re-verification in subsequent rounds. The queue shrinks monotonically as findings get classified as `full-consensus`, `partial-consensus`, or `worker-unique`. Findings classified into any of these three categories MUST NOT appear in any subsequent round's reverify prompt, for any worker.
30
30
 
31
+ An initial pane role `verifier` is still a Phase 4/5 analysis worker; it does not mean Phase 5.5 reverify. Only a queue-scoped dispatch whose prompt/result path carries `-reverify-r<N>-` performs the reverify step described by this contract.
32
+
31
33
  ## When to Use
32
34
 
33
35
  - When okstra lead Phase 5.5 (convergence loop) begins — immediately after all workers complete Phases 4 and 5
@@ -288,6 +290,8 @@ If none of the three is available, **abort the reverify dispatch for that role**
288
290
 
289
291
  Reverify prompts MUST NOT inject the Phase 2 `[Required reading]` clause:
290
292
 
293
+ Lightweight reverify does not require the original `analysis-packet.md`, `analysis-profile.md`, `task-brief.md`, or instruction set. Its complete input is the receiving worker's current `items_for_W` batch and the evidence embedded in those findings.
294
+
291
295
  - **Lightweight mode**: the clause directly contradicts the "Do NOT re-analyze the original source materials" instruction below. Including it forces workers to re-read the entire instruction-set per round per worker (3 workers × 2 rounds × 5+ files in the worst case) for no quality gain.
292
296
  - **Full-reanalysis mode**: workers DO need to re-read source materials, but only the analysis-worker file list (no `final-report-template.md`). If lead chooses to inject a reading clause here, it MUST mirror the audience-scoped enumeration in [okstra-lead-contract](./okstra-lead-contract.md) Phase 2 (no template).
293
297
 
@@ -208,6 +208,8 @@ These phases are governed by [team-contract](./team-contract.md). It is the cano
208
208
  - Worker output contract (sections 1–5 + optional Section 6; the Reading Confirmation block lives in the audit sidecar, never in the worker-results file — the preamble "Reading rules" section is canonical and the validator rejects violations), header standard, terminal statuses, errors-sidecar schema.
209
209
  - Token-usage tracking conventions.
210
210
 
211
+ For `final-verification`, Lead persists initial analysis prompts with one shared semantic body. Any genuine run delta appears under exactly one `## Run-specific directive` heading and applies to every selected analysis worker; Lead never shards verification requirements by worker, provider, or model. If the shared directive would exceed 40 nonblank lines, Lead writes it into the instruction set and adds the same reference to `analysis-packet.md` before dispatch. Phase 7 validates the persisted initial analysis prompts through the shared prompt contract before the run can pass.
212
+
211
213
  `Report writer worker` is NOT an analysis worker. Do not dispatch it in Phase 4/5 alongside analysis workers. It is invoked only in Phase 6 — see [report-writer](./report-writer.md).
212
214
 
213
215
  ### Phase 3 — Runtime adapter setup (BLOCKING)
@@ -27,6 +27,8 @@ Okstra tasks use one lead plus the exact worker assignments selected in the prep
27
27
 
28
28
  **Dispatch-prompt invariant.** Lead's dispatch prompt body for Claude / Codex / Antigravity workers MUST be byte-identical except for the role label and any wrapper-specific path headers (e.g. `**Worktree:**`, `**Errors sidecar path:**`). Lead MUST NOT bias the brief by inserting per-worker emphasis sentences ("you focus on X") into the body. Bias-by-prompt reproduces the historical failure mode where Claude commented only on assumptions, Codex only on code paths, and Antigravity only on requirements — leaving convergence with nothing to converge on.
29
29
 
30
+ Disjoint initial scopes are invalid triangulation. Every selected analysis worker owns the same common verification requirements; provider diversity supplies independent observations, not separate coverage slices. Do not shard the common scope by worker, provider, or model. Worker-specific depth belongs only in the non-voting Specialization Lens after the shared analysis is complete.
31
+
30
32
  ### Model Assignment Rules
31
33
 
32
34
  1. `resultContract.requiredWorkerRoles` in `task-manifest.json` (and the lead model metadata) is the canonical source. There is no role-level fallback — a missing assignment is a manifest defect, not a license to invent one.
@@ -63,7 +65,13 @@ Only workers selected from `recommendedWorkers` in `task-manifest.json` and `res
63
65
 
64
66
  Every worker prompt MUST start with the anchor headers rendered by `okstra_ctl.worker_prompt_headers.worker_prompt_headers()` (the generating SSOT — never hand-author or reorder them). Their meaning and extraction rules for workers are documented in the worker preamble §"Anchor headers". Phase-specific extra headers (implementation worktree, final-verification target snapshot, improvement-discovery grilling log) are listed there too.
65
67
 
66
- The body must include: role name, task type, task key, required bundle paths, assigned model, output contract, evidence handling rules, and any relevant config/deployment expectations from `reference-expectations.md`.
68
+ The `**Coding preflight pack:**` anchor is emitted only when `taskType == implementation`. A pane display role named `verifier` during `final-verification` is still an initial analysis worker; it neither receives implementation conventions nor becomes a Phase 5.5 reverify dispatch. Reverify is identified by the `-reverify-r<N>-` prompt/result path contract in [convergence](./convergence.md).
69
+
70
+ The body must include: role name, task type, task key, assigned model, output contract, evidence handling rules, and `analysis-packet.md` as the single primary analysis input. For `final-verification`, the compact target anchors and that packet path are sufficient; do not copy the full diff stat, source/fallback path inventory, profile sections, report-output sections, or lead self-review into the initial prompt.
71
+
72
+ A `final-verification` prompt may contain one optional `## Run-specific directive` section for a true run delta. It is limited to 40 nonblank lines; the nonblank prompt body excluding common/target anchors is limited to 96 lines. Larger shared material belongs in the instruction set and analysis packet. Before initial dispatch, the selected adapter validates each prompt and requires byte-identical normalized analysis bodies after removing only worker/model/role and worker-specific artifact-path metadata.
73
+
74
+ The Phase 7 run validator enforces the same rule against the persisted prompt paths recorded by the run manifest and team-state. `validators/validate-run.py` calls `_validate_final_verification_initial_prompts`, which delegates parsing and normalized equality to `okstra_ctl.worker_prompt_contract`; it ignores the report writer and `-reverify-r<N>-` prompts because they belong to different phases of the protocol. A dispatch-time bypass therefore remains a run-level contract violation.
67
75
 
68
76
  When a worker reads any project-relative path from the prompt, it MUST resolve it against `Project Root` (e.g. `<Project Root>/<Result Path>`) — never use bare relative paths that depend on cwd.
69
77
 
@@ -78,7 +86,7 @@ Send byte-identical dispatch prompts to every analysis worker per the "Dispatch-
78
86
  The lead does NOT inline `[Required reading]` or `[Error reporting]` blocks into worker prompts. Both contracts live in a single canonical file at `~/.okstra/templates/worker-prompt-preamble.md` (source: `templates/worker-prompt-preamble.md`). The lead injects the path via the `**Worker Preamble Path:**` anchor header and each worker Reads that file end-to-end before producing output.
79
87
 
80
88
  What the lead MUST still do per dispatch:
81
- - Inject the input file enumeration into the dispatch prompt body via an `## Inputs` section (or any heading the recipient agent expects), listing the actual project-relative primary inputs derived from the run's `instruction-set/`. For analysis workers, list `analysis-packet.md` as the required primary input and list task-brief / analysis-profile / analysis-material / reference-expectations / clarification-response as source/fallback paths only when useful. The preamble describes the rules; the lead provides the specific paths for THIS run.
89
+ - Inject the input file enumeration into the dispatch prompt body via an `## Inputs` section (or any heading the recipient agent expects), listing the actual project-relative primary inputs derived from the run's `instruction-set/`. For `final-verification` analysis workers, list only `analysis-packet.md` as the primary input; source files are reached on demand through that packet. Other phases may list source/fallback paths when useful. The preamble describes the rules; the lead provides the specific paths for THIS run.
82
90
  - Inject the absolute `**Errors log path:**` and `**Errors sidecar path:**` anchor headers — workers cannot synthesize these paths.
83
91
  - Omit the preamble pointer for reverify dispatches (Phase 5.5 lightweight mode) — see [convergence](./convergence.md) "Reverify prompt: required-reading suppression".
84
92
 
@@ -86,7 +94,7 @@ Audience-scoped file enumeration (performance optimization — mandatory):
86
94
 
87
95
  | Recipient | Files the lead lists under `## Inputs` |
88
96
  |---|---|
89
- | Claude / Codex / Antigravity analysis workers | `analysis-packet.md` as primary input; source/fallback paths may be listed below it but are not automatic first-read files |
97
+ | Claude / Codex / Antigravity analysis workers | `analysis-packet.md` as primary input; for `final-verification`, no source/fallback list is copied into the prompt |
90
98
  | Report writer worker (Phase 6) | task-brief, analysis-profile, analysis-material, reference-expectations, clarification-response (if carry-in), **plus** the instruction-set-local `final-report-template.md` (phase-stripped) and `final-report-schema.json` (per-task-type excerpt) — NOT the full `templates/reports/...` / `schemas/...` sources |
91
99
  | Reverify dispatches | none — the lead provides only the items to reverify |
92
100
 
@@ -29,18 +29,22 @@
29
29
  - **whole-task is a mutating phase, not a read-only one.** On entry, whole-task mode auto-merges (with `--no-ff`) the done stages not yet merged into the task branch to create an integration commit, then removes the cleanupable stage worktrees, registry stage-keys, and stage branches. If a merge conflict occurs it reports the conflicting files and aborts (the user resolves them manually and retries). A stage worktree with uncommitted changes remaining is preserved. Therefore the "fully-merged, clean target" the entry gate above refers to is the state after this auto-integration step completes, and whole-task final-verification must be treated as a mutating phase that creates the integration commit.
30
30
  - **single-stage scope** (`--stage N`): prep verified stage N is `status:done` and its isolated stage worktree exists and is clean. Other stages' state is irrelevant. A single-stage run is a partial verification: it MUST NOT recommend plain `release-handoff`, but MAY recommend `release-handoff(stage-group)` when the verdict is `accepted` — the stage becomes PR-eligible for a stage-group handoff.
31
31
  - the lead still captures `git status --short` from the injected worktree to confirm the analysis ran against the delivered work-tree state; an unexpected divergence (dirty tree outside `.okstra/`, missing worktree) is a `tool-failure`, not a silent proceed.
32
+ - Worker verification procedure:
33
+ - **Target confirmation:** verify the inline worktree, scope, base/head refs, target path, and digest against `verification-target.md` before analysis. Use the sidecar's stage/report mapping and complete diff stat; a missing sidecar, digest mismatch, dirty worktree outside `.okstra/`, or wrong head is a `tool-failure`, never a silent target reselection.
34
+ - **Evidence:** attach file:line, exact command + exit code, log excerpt, or MCP SELECT evidence to every finding. Mark a requirement as covered only when the cited artifact demonstrates it.
35
+ - **Tier 1 and Tier 2 read-only validation:** Tier 1 is the originating brief/approved plan `validation` set; Tier 2 is `<PROJECT_ROOT>/.okstra/project.json` `qaCommands`. Do not auto-detect commands from package manifests. A missing tier is `qa-command not configured: <category>`. Before execution, reject commands containing source/lockfile mutation tokens such as `--fix`, `--write`, ` -w`, ` -u`, `--snapshot-update`, `INSTA_UPDATE=<not-no>`, `cargo update`, or `npm install` without `ci`; record the exact denied token.
36
+ - **Tier 3 stage conformance:** read `<task_root>/qa/conformance-manifest.json`. In whole-task scope, run every non-exempt `entries[].runCommand` against the merged worktree; in single-stage scope, run only the matching `stageKey`. Use `qaEnv` only with a replica/test datastore, refresh `result-<stageKey>.json`, and interpret the exit code plus the last `QA-RESULT: PASS|FAIL` marker. Missing/non-PASS evidence is an acceptance-blocker recommendation. An exemption or user waiver is not executed; record its reason, and surface a waiver as a `conditional-accept` recommendation.
37
+ - **Manual user test:** read only the source implementation report's `implementation.manualUserTest`. Execute reproducible steps and record `pass`, `fail`, or `blocked` with observed evidence. Human-only or environment-unavailable steps remain `blocked` with the exact reason. Reaffirm an `applicable=false` exemption; do not execute planning `designPreparation` or manual-test PREP items directly.
38
+ - **Design-preparation carry-in:** read only source implementation report `missingInformation` rows whose `source` starts with `design-prep:`. Recommend `ifStillOpen: block` as an acceptance blocker and `ifStillOpen: follow-up` as residual risk. This phase does not read planning PREP sidecars directly or mutate planning snapshots.
39
+ - **Could-not-verify honesty:** use `not-configured`, `env-unavailable`, `rejected`, `gap`, or `blocked` as appropriate. Never convert unavailable evidence into an executed/pass claim.
40
+ - **Source-mutation prohibition:** verification may write only assigned okstra run artifacts. Do not edit source, schema, deployment, lockfile, or configuration files; route detected defects to a later phase.
32
41
  - Required deliverable shape (final report, in addition to the standard sections):
33
- - **Source Implementation Report(s)**: the `VERIFICATION_TARGET` snapshot verbatim — verification scope, worktree path, base ref, the list of stages under verification, and one row per stage citing its originating implementation final-report (`report_path` from `consumers.jsonl`; render `(report_path unrecorded)` when absent). The lead injects this same snapshot into every analyser prompt (`**Verification scope:** / **Worktree:** / **Verification base ref:** / **Verification diff stat:**`); a worker that cannot confirm its analysis ran against that worktree's delivered diff (against `base ref`) MUST record a `tool-failure`.
42
+ - **Source Implementation Report(s)**: the `VERIFICATION_TARGET` snapshot verbatim — verification scope, worktree path, base/head refs, the list of stages under verification, and one row per stage citing its originating implementation final-report (`report_path` from `consumers.jsonl`; render `(report_path unrecorded)` when absent). Every analyser prompt carries the same compact target identity (`**Verification scope:** / **Worktree:** / **Verification base ref:** / **Verification head ref:** / **Verification target path:** / **Verification target digest:**`) and reads the sidecar on demand for the complete diff stat. A worker that cannot confirm its analysis ran against that worktree's delivered diff MUST record a `tool-failure`.
34
43
  - **Verdict vocabulary**: Section 7 (`Final Verdict`) MUST include a `Verdict Token` field whose value is exactly one of `accepted`, `conditional-accept`, or `blocked`. `conditional-accept` requires an explicit, exhaustive list of conditions; ambiguous verdicts ("looks good", "mostly ready") are not allowed. Each condition MUST be recorded as a row in the **Conditional Acceptance Conditions** deliverable (`id` `CA-NNN`, `condition`, `evidenceRequired`, `blocksReleaseHandoff`). The validator enforces verdict↔deliverable consistency: `accepted` ⇒ zero acceptance blockers, `blocked` ⇒ at least one, `conditional-accept` ⇒ at least one condition, and a `release-handoff` routing recommendation is allowed only when the verdict is `accepted`. **Any Acceptance Blocker therefore forces the verdict off `accepted` (to `conditional-accept` or `blocked`); the gates below cite this rule instead of restating the arithmetic.**
35
44
  - **Acceptance Blockers block** (under section 4): one row per blocker with `id`, `severity` (`critical` / `major` / `minor`), evidence (file path, log excerpt, or test output), and the recommended follow-up phase (`error-analysis` or `implementation-planning`). Empty block is acceptable and preferred — render the single line `- No acceptance blockers found.`
36
45
  - **Residual Risk block** (under section 4): risks that are not blockers but should be tracked, each with mitigation owner and a trigger that would escalate them to a blocker.
37
46
  - **Validation Evidence**: for every requirement in the originating plan or task brief, cite the artifact (commit SHA, test output, log line, MCP SELECT result) that demonstrates coverage. Paraphrased "verified" claims without an artifact are rejected.
38
47
  - **Read-only command log**: any pre-existing test/validation command touched during this run MUST be listed with its exact command line and one honest status — `executed` (ran; carries its exit code) / `env-unavailable` (should run but cannot in this environment — missing replica DB, container, or service; carries the reason, never a faked pass) / `not-configured` (no such qa-command tier) / `rejected` (a mutating/denied token — skipped, carries the denied token). A check that could not run locally is recorded as `env-unavailable` with the reason — never silently dropped and never reported as `executed` with an invented exit code. Mutating-command prohibition is the shared read-only boundary (see Non-goals); it is not restated per row.
39
- - **Two-tier command lookup (shared with `implementation`):** when this phase performs its own independent re-validation, the command source is exactly the same two tiers `implementation` verifiers use — Tier 1 is the originating task brief / approved plan's `validation` set, Tier 2 is `<PROJECT_ROOT>/.okstra/project.json` under `qaCommands`. Auto-detecting tools from manifest files is forbidden; missing tiers are recorded as `qa-command not configured: <category>` rather than guessed. The `cmd` deny-list (`--fix`, `--write`, ` -w`, ` -u`, `--snapshot-update`, `INSTA_UPDATE=<not-no>`, `cargo update`, `npm install` without `ci`, etc.) is enforced identically. NOTE: runtime fail-fast validation (`okstra_ctl.qa_commands.validate_qa_commands`) only fires at `--task-type implementation` run-prep, so this phase MUST self-check each `qaCommands` entry against the deny-list before executing it — if a denied token is present, skip the command and record it as a `Read-only command log` line `qa-command rejected (denied token: <token>): <label>`.
40
- - **Tier 3 — stage conformance scripts (whole-task union):** because this phase verifies the **integrated, merged** state, it re-runs conformance against that state rather than per-stage. Read the task-level manifest `<task_root>/qa/conformance-manifest.json` (the directory is the `TASK_QA_PATH` token) and, in **whole-task scope**, run the `runCommand` of **every** `entries[]` item against the merged worktree, refreshing each `<task_root>/qa/result-<stageKey>.json` (`{ "stageKey", "overall": "PASS"|"FAIL"|"MISSING", "ranAt", "requirements" }`). In **single-stage scope**, run only the entry whose `stageKey` matches the verified stage. An entry carrying an `exemption` or user `waiver` is NOT executed — record the skip and reason; a `waiver` becomes a `conditional-accept` condition surfaced in the section 7 Verdict (conformance left unverified by user acknowledgement). Each `runCommand` runs in the worktree cwd with `qaEnv` env (replica DB DSN / app base URL / env file) — **replica / test environment only**, never shared / staging / prod, and the same source/lockfile mutation deny-list applies (a conformance script MAY mutate only its `qaEnv` replica datastore). Interpret each result from the exit code + stdout `QA-RESULT: PASS|FAIL` (last wins) and `REQ <id>: PASS|FAIL: <reason>` lines; no `QA-RESULT` marker → `MISSING`. Any entry whose result is not `PASS` (including `MISSING` or a never-run/missing sidecar) is an **Acceptance Blocker** (`major`+). This is the same gate the `validate-run.py` Tier 3 check enforces on the result sidecars.
41
- - **Manual user test results**: take each item from the source implementation report's §5.7.9 Manual User Test (Draft), execute the ones reproducible in this environment (e.g. `/okstra-container-build`, which runs `okstra container up`, then the documented steps), and record `result` (`pass` / `fail` / `blocked`) + observed value in §5.8.7 (data field `finalVerification.manualUserTest`). Steps that need human-only interaction this run cannot perform are recorded as `blocked` with the reason (handed to the user), never silently skipped. A failed manual test is an Acceptance Blocker. If the draft was an exemption (`applicable=false`), reaffirm the reason in one line (`applicable=false` + `exemptionReaffirm`).
42
- For manual testing, read only the source implementation report's `implementation.manualUserTest`. Do not execute planning `designPreparation` / `manual-user-test` PREP items directly. The implementation report has already reconciled the approved seed with the actual diff. Record reproducible results; human-only or unavailable environments become `result: blocked` with the exact reason.
43
- - **Non-manual design-preparation handoff**: read only source implementation report `missingInformation` rows whose `source` starts with `design-prep:`. Record `ifStillOpen: block` as an acceptance blocker and `ifStillOpen: follow-up` as residual risk. This phase does not read planning PREP sidecars directly, merge their inputs, or mutate the planning snapshot.
44
48
  - **Could-not-verify roll-up (§5.8.9)**: the template mechanically aggregates every not-confirmed check into one scannable list — `gap` requirement-coverage rows, `not-configured` / `env-unavailable` / `rejected` command rows, and `blocked` manual tests. You do not hand-author it, but you MUST give those rows their honest status so nothing unverified hides across sections: a check silently recorded as `executed`/`covered` will not surface in the roll-up. This is okstra's answer to "say what could not be verified this run."
45
49
  - **Routing recommendation**: the next safe phase — one of `release-handoff`, `done`, `error-analysis`, `implementation-planning` — tied to the verdict and blocker list. `release-handoff` is allowed ONLY when the Verdict Token is `accepted`. `release-handoff` is additionally allowed ONLY when the verification scope (the `Verification scope:` line of the injected `VERIFICATION_TARGET` block, recorded as the report's `verificationScope` field) is `whole-task`; a `single-stage` accepted run routes to `release-handoff(stage-group)` (or `implementation` / `done`); plain `release-handoff` remains whole-task-only. Enforcement: `validators/validate-run.py` rejects a `single-stage` report whose routing cites plain `release-handoff`.
46
50
  - **Verified-row recording** (single-stage scope only): when the Verdict Token is `accepted`, the lead MUST run `okstra handoff record-verified --plan-run-root <plan-run-root> --stage <N> --report-path <final-report.md path> --data-json <final-report data.json path>` and quote the command + exit code in the report. The helper re-validates taskType/scope/verdict from data.json, so a non-accepted or whole-task report is rejected at the tool layer.
@@ -41,8 +41,7 @@ PROFILE_SECTIONS_BY_TASK_TYPE = {
41
41
  "Self-review pass before finalising the report",
42
42
  ),
43
43
  "final-verification": (
44
- "Required deliverable shape",
45
- "Self-review pass before finalising the report",
44
+ "Worker verification procedure",
46
45
  ),
47
46
  "improvement-discovery": (
48
47
  "Phase 1.5 — Lead reflect-back grilling",
@@ -81,6 +80,7 @@ def build_analysis_packet(
81
80
  parts.extend(
82
81
  _intro_block(
83
82
  task_key,
83
+ task_type,
84
84
  instruction_set_relative_path,
85
85
  bool(clarification_response_path),
86
86
  )
@@ -113,6 +113,7 @@ def _packet_frontmatter(brief_text: str, task_key: str) -> str:
113
113
 
114
114
  def _intro_block(
115
115
  task_key: str,
116
+ task_type: str,
116
117
  instruction_set: str,
117
118
  has_clarification: bool,
118
119
  ) -> list[str]:
@@ -132,6 +133,10 @@ def _intro_block(
132
133
  f"- Analysis material: `{instruction_set}/analysis-material.md`",
133
134
  f"- Reference expectations: `{instruction_set}/reference-expectations.md`",
134
135
  ]
136
+ if task_type == "final-verification":
137
+ lines.append(
138
+ f"- Verification target: `{instruction_set}/verification-target.md`"
139
+ )
135
140
  if has_clarification:
136
141
  lines.append(
137
142
  f"- Clarification response: `{instruction_set}/clarification-response.md`"
@@ -27,6 +27,7 @@ from .path_hints import hydrate_active_run_context
27
27
  from .wrapper_status import status_path_for_prompt
28
28
  from .paths import task_dir
29
29
  from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_headers
30
+ from .worker_prompt_contract import validate_final_verification_prompt_paths
30
31
 
31
32
 
32
33
  SUPPORTED_CLI_WORKERS = {
@@ -182,6 +183,7 @@ def build_dispatch_plan(
182
183
  )
183
184
  for worker_id in selected_workers
184
185
  )
186
+ _validate_initial_final_verification_prompts(manifest, workers)
185
187
  return DispatchPlan(
186
188
  project_root=project_root,
187
189
  workspace_root=workspace_root,
@@ -193,6 +195,23 @@ def build_dispatch_plan(
193
195
  )
194
196
 
195
197
 
198
+ def _validate_initial_final_verification_prompts(
199
+ manifest: Mapping[str, Any],
200
+ workers: Sequence[WorkerDispatch],
201
+ ) -> None:
202
+ if manifest.get("taskType") != "final-verification":
203
+ return
204
+ prompt_paths = {
205
+ worker.worker_id: worker.prompt_path
206
+ for worker in workers
207
+ if worker.worker_id != REPORT_WRITER_WORKER_ID
208
+ and "-reverify-r" not in worker.prompt_path.name
209
+ }
210
+ errors = validate_final_verification_prompt_paths(prompt_paths)
211
+ if errors:
212
+ raise DispatchError("final-verification prompt contract: " + "; ".join(errors))
213
+
214
+
196
215
  def dispatch_plan(plan: DispatchPlan) -> int:
197
216
  _set_dispatch_mode(plan.team_state_path, _dispatch_mode(plan.workers))
198
217
  for worker in plan.workers:
@@ -702,6 +721,13 @@ def _analysis_input_lines(
702
721
  manifest: Mapping[str, Any],
703
722
  active_context: Mapping[str, Any],
704
723
  ) -> list[str]:
724
+ if manifest.get("taskType") == "final-verification":
725
+ packet_path = _instruction_path(
726
+ manifest,
727
+ active_context,
728
+ "analysisPacketPath",
729
+ )
730
+ return _existing_input_lines((("Primary analysis packet", packet_path),))
705
731
  inputs = [
706
732
  ("Primary analysis packet", _instruction_path(manifest, active_context, "analysisPacketPath")),
707
733
  ("Task brief", _instruction_path(manifest, active_context, "taskBriefPath")),
@@ -800,7 +826,7 @@ def _worktree_headers(
800
826
  ) -> list[str]:
801
827
  task_type = _require_string(manifest, "taskType")
802
828
  worktree = _worktree_path(manifest, active_context)
803
- if task_type not in {"implementation", "final-verification"} or not worktree:
829
+ if task_type != "implementation" or not worktree:
804
830
  return []
805
831
  headers = [f"**Worktree:** {worktree}"]
806
832
  if role == "executor":
@@ -18,6 +18,7 @@ from .final_report_paths import (
18
18
  from .lead_events import LeadEvent, append_lead_event
19
19
  from .path_hints import hydrate_active_run_context
20
20
  from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_headers
21
+ from .worker_prompt_contract import validate_final_verification_prompt_paths
21
22
  from .wrapper_status import read_wrapper_status, status_path_for_prompt
22
23
 
23
24
 
@@ -183,6 +184,7 @@ def build_dispatch_plan(
183
184
  report_writer_model,
184
185
  options,
185
186
  )
187
+ _validate_initial_final_verification_prompts(manifest, jobs)
186
188
  return DispatchPlan(
187
189
  project_root=project_root,
188
190
  workspace_root=workspace_root.resolve(),
@@ -195,6 +197,24 @@ def build_dispatch_plan(
195
197
  )
196
198
 
197
199
 
200
+ def _validate_initial_final_verification_prompts(
201
+ manifest: Mapping[str, Any],
202
+ jobs: Sequence[WorkerJob],
203
+ ) -> None:
204
+ if manifest.get("taskType") != "final-verification":
205
+ return
206
+ prompt_paths = {
207
+ job.worker_id: job.prompt_path
208
+ for job in jobs
209
+ if job.worker_id != REPORT_WRITER_WORKER_ID
210
+ and job.dispatch_kind == "initial"
211
+ and "-reverify-r" not in job.prompt_path.name
212
+ }
213
+ errors = validate_final_verification_prompt_paths(prompt_paths)
214
+ if errors:
215
+ raise DispatchError("final-verification prompt contract: " + "; ".join(errors))
216
+
217
+
198
218
  def dispatch_plan(plan: DispatchPlan, *, wait: bool = True) -> int:
199
219
  if wait:
200
220
  if any(job.backend == BACKEND_TMUX_PANE for job in plan.jobs):
@@ -53,6 +53,34 @@ def _per_task(files: list[dict]) -> list[dict]:
53
53
  return sorted(acc.values(), key=lambda a: a["totalBytes"], reverse=True)
54
54
 
55
55
 
56
+ def _prompt_pair(log_path: Path, transcript_bytes: int) -> dict:
57
+ prompt_path = log_path.with_suffix(".md")
58
+ if not prompt_path.is_file():
59
+ return {
60
+ "transcriptPath": str(log_path),
61
+ "transcriptBytes": transcript_bytes,
62
+ "promptPath": "",
63
+ "promptBytes": 0,
64
+ "transcriptToPromptRatio": None,
65
+ }
66
+ try:
67
+ prompt_bytes = prompt_path.stat().st_size
68
+ except OSError:
69
+ prompt_bytes = 0
70
+ ratio = (
71
+ round(transcript_bytes / prompt_bytes, 2)
72
+ if prompt_bytes > 0
73
+ else None
74
+ )
75
+ return {
76
+ "transcriptPath": str(log_path),
77
+ "transcriptBytes": transcript_bytes,
78
+ "promptPath": str(prompt_path),
79
+ "promptBytes": prompt_bytes,
80
+ "transcriptToPromptRatio": ratio,
81
+ }
82
+
83
+
56
84
  def scan_logs(logs_root: Path, project_root: Path, top: int = 20) -> dict:
57
85
  project_id = _project_id(project_root)
58
86
  files: list[dict] = []
@@ -68,16 +96,27 @@ def scan_logs(logs_root: Path, project_root: Path, top: int = 20) -> dict:
68
96
  continue
69
97
  meta = _parse(parts, p.stem)
70
98
  tk = f"{project_id}:{meta['taskGroup']}:{meta['taskId']}" if meta["taskGroup"] else ""
71
- files.append({"path": str(p), "sizeBytes": size, "mtimeEpoch": mtime,
72
- "taskKey": tk, **meta})
99
+ files.append({
100
+ "path": str(p),
101
+ "sizeBytes": size,
102
+ "mtimeEpoch": mtime,
103
+ "taskKey": tk,
104
+ **meta,
105
+ **_prompt_pair(p, size),
106
+ })
73
107
  files.sort(key=lambda f: f["sizeBytes"], reverse=True)
74
108
  return {
75
109
  "logsRoot": str(logs_root),
76
110
  "topLargest": files[:top],
77
111
  "perTask": _per_task(files),
78
- "totals": {"fileCount": len(files),
79
- "totalBytes": sum(f["sizeBytes"] for f in files),
80
- "taskCount": len({f["taskKey"] for f in files if f["taskKey"]})},
112
+ "totals": {
113
+ "fileCount": len(files),
114
+ "totalBytes": sum(f["sizeBytes"] for f in files),
115
+ "taskCount": len({f["taskKey"] for f in files if f["taskKey"]}),
116
+ "promptBytes": sum(f["promptBytes"] for f in files),
117
+ "transcriptBytes": sum(f["transcriptBytes"] for f in files),
118
+ "pairedFileCount": sum(1 for f in files if f["promptPath"]),
119
+ },
81
120
  }
82
121
 
83
122
 
@@ -59,6 +59,7 @@ def compact_active_run_context(
59
59
  "inputs": _compact_active_inputs(payload),
60
60
  "workers": _compact_active_workers(payload),
61
61
  "executorWorktree": dict(_mapping(payload.get("executorWorktree"))),
62
+ "verificationTarget": dict(_mapping(payload.get("verificationTarget"))),
62
63
  "lazyReadPlan": dict(_mapping(payload.get("lazyReadPlan"))),
63
64
  }
64
65
 
@@ -99,6 +100,7 @@ def hydrate_active_run_context(payload: Mapping[str, Any]) -> dict[str, Any]:
99
100
  "codingPreflightDir": ctx.get("OKSTRA_CODING_PREFLIGHT_DIR", ""),
100
101
  },
101
102
  "executorWorktree": dict(_mapping(payload.get("executorWorktree"))),
103
+ "verificationTarget": dict(_mapping(payload.get("verificationTarget"))),
102
104
  "sourceArtifacts": _hydrate_active_source_artifacts(ctx),
103
105
  "lazyReadPlan": dict(_mapping(payload.get("lazyReadPlan"))),
104
106
  }
@@ -190,6 +192,7 @@ def _hydrate_active_instruction_set(
190
192
  clarification_response = ""
191
193
  if inputs.get("hasClarificationResponse"):
192
194
  clarification_response = f"{instruction_set}/clarification-response.md"
195
+ target = _mapping(payload.get("verificationTarget"))
193
196
  return {
194
197
  "path": instruction_set,
195
198
  "analysisPacketPath": ctx.get("ANALYSIS_PACKET_RELATIVE_PATH", ""),
@@ -200,6 +203,8 @@ def _hydrate_active_instruction_set(
200
203
  "clarificationResponsePath": clarification_response,
201
204
  "finalReportTemplatePath": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
202
205
  "finalReportSchemaPath": ctx.get("FINAL_REPORT_SCHEMA_RELATIVE_PATH", ""),
206
+ "verificationTargetPath": str(target.get("path") or ""),
207
+ "verificationTargetDigest": str(target.get("digest") or ""),
203
208
  }
204
209
 
205
210
 
@@ -354,6 +354,8 @@ def _active_instruction_set(ctx: dict) -> dict:
354
354
  "clarificationResponsePath": clarification,
355
355
  "finalReportTemplatePath": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
356
356
  "finalReportSchemaPath": ctx.get("FINAL_REPORT_SCHEMA_RELATIVE_PATH", ""),
357
+ "verificationTargetPath": ctx.get("VERIFICATION_TARGET_RELATIVE_PATH", ""),
358
+ "verificationTargetDigest": ctx.get("VERIFICATION_TARGET_DIGEST", ""),
357
359
  }
358
360
 
359
361
 
@@ -387,6 +389,19 @@ def _active_executor_worktree(ctx: dict) -> dict:
387
389
  }
388
390
 
389
391
 
392
+ def _active_verification_target(ctx: dict) -> dict:
393
+ if ctx.get("TASK_TYPE") != "final-verification":
394
+ return {}
395
+ return {
396
+ "scope": ctx.get("VERIFICATION_SCOPE", ""),
397
+ "worktreePath": ctx.get("VERIFICATION_WORKTREE_PATH", ""),
398
+ "baseRef": ctx.get("VERIFICATION_BASE_REF", ""),
399
+ "headRef": ctx.get("VERIFICATION_HEAD_REF", ""),
400
+ "path": ctx.get("VERIFICATION_TARGET_RELATIVE_PATH", ""),
401
+ "digest": ctx.get("VERIFICATION_TARGET_DIGEST", ""),
402
+ }
403
+
404
+
390
405
  def _active_source_artifacts(ctx: dict) -> dict:
391
406
  return {
392
407
  "taskManifestPath": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
@@ -429,6 +444,7 @@ def render_active_run_context(active_context_path: str, ctx: dict) -> None:
429
444
  "errorLogs": _active_error_logs(ctx),
430
445
  "runtimeResources": _active_runtime_resources(ctx),
431
446
  "executorWorktree": _active_executor_worktree(ctx),
447
+ "verificationTarget": _active_verification_target(ctx),
432
448
  "sourceArtifacts": _active_source_artifacts(ctx),
433
449
  "lazyReadPlan": _active_lazy_read_plan(),
434
450
  }
@@ -1053,6 +1069,10 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1053
1069
  "analysisMaterialPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
1054
1070
  + "/analysis-material.md",
1055
1071
  "analysisPacketPath": ctx.get("ANALYSIS_PACKET_RELATIVE_PATH", ""),
1072
+ "verificationTargetPath": ctx.get(
1073
+ "VERIFICATION_TARGET_RELATIVE_PATH", ""
1074
+ ),
1075
+ "verificationTargetDigest": ctx.get("VERIFICATION_TARGET_DIGEST", ""),
1056
1076
  "taskBriefCopyPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
1057
1077
  + "/task-brief.md",
1058
1078
  "referenceExpectationsPath": ctx.get(
@@ -1278,6 +1298,8 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1278
1298
  "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
1279
1299
  "activeRunContextPath": ctx.get("ACTIVE_RUN_CONTEXT_RELATIVE_PATH", ""),
1280
1300
  "analysisPacketPath": ctx.get("ANALYSIS_PACKET_RELATIVE_PATH", ""),
1301
+ "verificationTargetPath": ctx.get("VERIFICATION_TARGET_RELATIVE_PATH", ""),
1302
+ "verificationTargetDigest": ctx.get("VERIFICATION_TARGET_DIGEST", ""),
1281
1303
  "workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
1282
1304
  "reportTemplatePath": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
1283
1305
  "validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
@@ -16,6 +16,7 @@ state passing, and are read once at the start.
16
16
  from __future__ import annotations
17
17
 
18
18
  import importlib.util
19
+ import hashlib
19
20
  import json
20
21
  import os
21
22
  import re
@@ -100,6 +101,7 @@ from . import implementation_stage as _implementation_stage
100
101
  from .stage_reconcile import (
101
102
  auto_reconcile_best_effort as _stage_auto_reconcile_best_effort,
102
103
  )
104
+ from .work_categories import resolve_work_category
103
105
  from .worktree import (
104
106
  WorktreeProvision,
105
107
  provision_task_worktree,
@@ -1578,20 +1580,70 @@ def _reserve_final_verification_target(
1578
1580
 
1579
1581
  diff_stat = _git_out(target.worktree_path, "diff", "--stat",
1580
1582
  f"{target.base}..{target.head}")
1583
+ ctx["VERIFICATION_SCOPE"] = target.scope
1584
+ ctx["VERIFICATION_WORKTREE_PATH"] = target.worktree_path
1585
+ ctx["VERIFICATION_BASE_REF"] = target.base
1586
+ ctx["VERIFICATION_HEAD_REF"] = target.head
1587
+ ctx["VERIFICATION_TARGET"] = _format_verification_target(target, diff_stat)
1588
+
1589
+
1590
+ def _format_verification_target(
1591
+ target: _stage_targets.FinalVerificationTarget,
1592
+ diff_stat: str,
1593
+ ) -> str:
1581
1594
  reports = "\n".join(
1582
1595
  f" - stage {s}: `{rp or '(report_path unrecorded)'}`"
1583
1596
  for s, rp in zip(target.stages, target.reports)
1584
1597
  )
1585
- ctx["VERIFICATION_TARGET"] = (
1598
+ return (
1586
1599
  f"- **Verification scope:** `{target.scope}`\n"
1587
1600
  f"- **Worktree:** `{target.worktree_path}`\n"
1588
1601
  f"- **Verification base ref:** `{target.base}`\n"
1602
+ f"- **Verification head ref:** `{target.head}`\n"
1589
1603
  f"- **Stages under verification:** {target.stages}\n"
1590
1604
  f"- **Source implementation reports:**\n{reports}\n"
1591
1605
  f"- **Verification diff stat:**\n```\n{diff_stat}\n```"
1592
1606
  )
1593
1607
 
1594
1608
 
1609
+ def write_verification_target_snapshot(
1610
+ instruction_set: Path,
1611
+ target_markdown: str,
1612
+ ) -> tuple[Path, str]:
1613
+ """Persist one normalized final-verification target and return its digest."""
1614
+ normalized = target_markdown.replace("\r\n", "\n").replace("\r", "\n")
1615
+ normalized = normalized.rstrip() + "\n"
1616
+ digest = "sha256:" + hashlib.sha256(normalized.encode("utf-8")).hexdigest()
1617
+ path = instruction_set / "verification-target.md"
1618
+ path.write_text(
1619
+ normalized + f"- **Verification target digest:** `{digest}`\n",
1620
+ encoding="utf-8",
1621
+ )
1622
+ return path, digest
1623
+
1624
+
1625
+ def _write_verification_target_artifact(
1626
+ inp: PrepareInputs,
1627
+ ctx: dict,
1628
+ instruction_set: Path,
1629
+ ) -> None:
1630
+ if inp.task_type != "final-verification":
1631
+ return
1632
+ target_markdown = str(ctx.get("VERIFICATION_TARGET") or "")
1633
+ if not target_markdown:
1634
+ raise PrepareError("final-verification target snapshot is missing")
1635
+ target_path, target_digest = write_verification_target_snapshot(
1636
+ instruction_set,
1637
+ target_markdown,
1638
+ )
1639
+ ctx["VERIFICATION_TARGET_PATH"] = str(target_path)
1640
+ ctx["VERIFICATION_TARGET_RELATIVE_PATH"] = (
1641
+ f"{ctx['INSTRUCTION_SET_RELATIVE_PATH']}/verification-target.md"
1642
+ )
1643
+ ctx["VERIFICATION_TARGET_DIGEST"] = target_digest
1644
+ ctx["VERIFICATION_TARGET"] = target_path.read_text(encoding="utf-8")
1645
+
1646
+
1595
1647
  def _write_instruction_set_sources(
1596
1648
  inp: PrepareInputs, ctx: dict, profile_content: str, review_material: str
1597
1649
  ) -> Path:
@@ -1599,6 +1651,7 @@ def _write_instruction_set_sources(
1599
1651
  reference-expectations 를 기록하고 디렉터리 경로를 돌려준다."""
1600
1652
  instruction_set = Path(ctx["INSTRUCTION_SET_PATH"])
1601
1653
  instruction_set.mkdir(parents=True, exist_ok=True)
1654
+ _write_verification_target_artifact(inp, ctx, instruction_set)
1602
1655
  profile_rendered = profile_content
1603
1656
  if inp.task_type == "implementation":
1604
1657
  profile_rendered += "\n\n{{DESIGN_PREP_CONTEXT}}\n\n{{FIX_RUN_CONTEXT}}"
@@ -1950,6 +2003,16 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
1950
2003
  task_id_segment = slugify(inp.task_id)
1951
2004
  task_key = f"{inp.project_id}:{inp.task_group}:{inp.task_id}"
1952
2005
 
2006
+ # Worktree branch namespace, workflow state and every rendered manifest read
2007
+ # the same resolved value, so a phase invoked without --work-category still
2008
+ # lands on the branch namespace the task was classified with.
2009
+ inp.work_category = resolve_work_category(
2010
+ inp.work_category,
2011
+ project_root=project_root,
2012
+ task_group=inp.task_group,
2013
+ task_id=inp.task_id,
2014
+ )
2015
+
1953
2016
  # ---- task worktree provisioning (every phase, every task-type) ----
1954
2017
  # One worktree per task-key: requirements-discovery, error-analysis,
1955
2018
  # implementation-planning and implementation phases of the same task
@@ -2371,7 +2434,8 @@ def main(argv: list[str]) -> int:
2371
2434
  help=(
2372
2435
  "Work-category classification for this task "
2373
2436
  "(bugfix / feature / refactor / ops / improvement). "
2374
- "Falls back to `unknown` when omitted."
2437
+ "When omitted, falls back to the category already recorded in "
2438
+ "task-manifest.json, then to `feature`."
2375
2439
  ),
2376
2440
  )
2377
2441
  p.add_argument(
@@ -85,6 +85,7 @@ from okstra_ctl.worktree import (
85
85
  preview_worktree_decision,
86
86
  )
87
87
  from okstra_ctl.paths import task_dir, task_runs_dir
88
+ from okstra_ctl.work_categories import resolve_work_category
88
89
  from okstra_ctl.run_context import latest_run_inputs
89
90
  from okstra_project.dirs import project_json_path
90
91
  from okstra_project.state import (
@@ -3126,6 +3127,17 @@ def _submit_fix_cycle_confirm(state: WizardState, value: str) -> Optional[str]:
3126
3127
  return f"fix-cycle: {v}"
3127
3128
 
3128
3129
 
3130
+ def _preview_work_category(state: WizardState) -> str:
3131
+ """okstra-run 경로는 --work-category 를 넘기지 않는다. prepare 가 쓰는 것과
3132
+ 같은 resolver 를 태워야 미리보기 브랜치명이 실제 생성 브랜치와 일치한다."""
3133
+ return resolve_work_category(
3134
+ "",
3135
+ project_root=Path(state.project_root),
3136
+ task_group=state.task_group,
3137
+ task_id=state.task_id,
3138
+ )
3139
+
3140
+
3129
3141
  def _worktree_preview_line(state: WizardState) -> Optional[str]:
3130
3142
  """confirm 직전 요약 블록에 넣을 worktree 미리보기 한 줄.
3131
3143
  final-verification 은 stage worktree 를 읽기 전용 재사용하므로 None."""
@@ -3138,9 +3150,7 @@ def _worktree_preview_line(state: WizardState) -> Optional[str]:
3138
3150
  project_id=state.project_id,
3139
3151
  task_group_segment=state.task_group,
3140
3152
  task_id_segment=state.task_id,
3141
- # okstra-run 경로는 --work-category 를 넘기지 않으므로 provision 도 ""(→ "task-" prefix)
3142
- # 브랜치를 만든다. 이 미리보기 브랜치명은 실제 생성 브랜치와 정확히 일치한다.
3143
- work_category="",
3153
+ work_category=_preview_work_category(state),
3144
3154
  base_ref=state.base_ref,
3145
3155
  )
3146
3156
  key = {
@@ -3167,7 +3177,8 @@ def _worktree_preview_line_impl(state: WizardState) -> str:
3167
3177
  path=str(parent_dir))
3168
3178
  decision = preview_stage_worktree_decision(
3169
3179
  project_id=state.project_id, task_group_segment=state.task_group,
3170
- task_id_segment=state.task_id, work_category="",
3180
+ task_id_segment=state.task_id,
3181
+ work_category=_preview_work_category(state),
3171
3182
  stage_number=int(stage),
3172
3183
  )
3173
3184
  key = ("worktree_impl_reuse" if decision.status == "reused"
@@ -8,6 +8,11 @@ profile / validators / wizard 는 이 모듈에서 enum 을 import 해야 한다
8
8
  """
9
9
  from __future__ import annotations
10
10
 
11
+ import json
12
+ from pathlib import Path
13
+
14
+ from .paths import task_dir
15
+
11
16
  WORK_CATEGORIES: tuple[str, ...] = (
12
17
  "bugfix",
13
18
  "feature",
@@ -16,6 +21,38 @@ WORK_CATEGORIES: tuple[str, ...] = (
16
21
  "improvement",
17
22
  )
18
23
 
24
+ DEFAULT_WORK_CATEGORY = "feature"
25
+
19
26
 
20
27
  def is_valid_category(value: str) -> bool:
21
28
  return value in WORK_CATEGORIES
29
+
30
+
31
+ def recorded_work_category(project_root: Path, task_group: str, task_id: str) -> str:
32
+ """task-manifest.json 에 이미 기록된 work-category. 없거나 enum 밖이면 ""."""
33
+ manifest = task_dir(project_root, task_group, task_id) / "task-manifest.json"
34
+ try:
35
+ data = json.loads(manifest.read_text(encoding="utf-8"))
36
+ except (OSError, json.JSONDecodeError):
37
+ return ""
38
+ value = str(data.get("workCategory") or "").strip()
39
+ return value if is_valid_category(value) else ""
40
+
41
+
42
+ def resolve_work_category(
43
+ explicit: str, *, project_root: Path, task_group: str, task_id: str
44
+ ) -> str:
45
+ """이번 run 이 실제로 쓸 work-category: 명시값 → task 에 기록된 분류 →
46
+ DEFAULT_WORK_CATEGORY.
47
+
48
+ requirements-discovery 가 분류를 task-manifest 에 기록해두면 이후 phase 는
49
+ `--work-category` 없이 실행돼도 같은 branch namespace 로 수렴한다. 어느 쪽도
50
+ 없을 때 `unknown`(→ `task/` 접두) 대신 기본 분류로 떨어뜨린다.
51
+ """
52
+ value = (explicit or "").strip()
53
+ if value:
54
+ return value
55
+ return (
56
+ recorded_work_category(project_root, task_group, task_id)
57
+ or DEFAULT_WORK_CATEGORY
58
+ )
@@ -0,0 +1,223 @@
1
+ """Compact initial final-verification prompt contract."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Mapping
7
+
8
+
9
+ MAX_FINAL_VERIFICATION_DIRECTIVE_LINES = 40
10
+ MAX_FINAL_VERIFICATION_BODY_LINES = 96
11
+
12
+ _DIRECTIVE_HEADING = "## Run-specific directive"
13
+ _PRIMARY_PACKET_RE = re.compile(
14
+ r"(?im)^-\s+Primary analysis packet:\s+`[^`\n]*analysis-packet\.md`\s*$"
15
+ )
16
+ _COPIED_SECTION_PATTERNS = (
17
+ ("Primary focus areas", re.compile(r"(?im)^(?:#{1,6}\s+|-\s+)Primary focus areas\b")),
18
+ (
19
+ "Required deliverable shape",
20
+ re.compile(r"(?im)^(?:#{1,6}\s+|-\s+)Required deliverable shape\b"),
21
+ ),
22
+ (
23
+ "Self-review pass",
24
+ re.compile(r"(?im)^(?:#{1,6}\s+|-\s+)Self-review pass\b"),
25
+ ),
26
+ )
27
+ _NON_BODY_PREFIXES = (
28
+ "**Project Root:**",
29
+ "**Prompt History Path:**",
30
+ "**Result Path:**",
31
+ "Assigned worker prompt history path:",
32
+ "**Worker Preamble Path:**",
33
+ "**Errors log path:**",
34
+ "**Errors sidecar path:**",
35
+ "**Read scope:**",
36
+ "**Worktree:**",
37
+ "**Verification scope:**",
38
+ "**Verification base ref:**",
39
+ "**Verification head ref:**",
40
+ "**Verification target path:**",
41
+ "**Verification target digest:**",
42
+ )
43
+ _REQUIRED_TARGET_PREFIXES = (
44
+ "**Worktree:**",
45
+ "**Verification scope:**",
46
+ "**Verification base ref:**",
47
+ "**Verification head ref:**",
48
+ "**Verification target path:**",
49
+ "**Verification target digest:**",
50
+ )
51
+ _WORKER_SPECIFIC_PREFIXES = (
52
+ "**Prompt History Path:**",
53
+ "**Result Path:**",
54
+ "Assigned worker prompt history path:",
55
+ "**Errors sidecar path:**",
56
+ "**Worker Result Path:**",
57
+ "**Model:**",
58
+ "**Pane role:**",
59
+ )
60
+ _WORKER_LABEL_RE = re.compile(
61
+ r"\b(?:Claude|Codex|Antigravity) worker\b",
62
+ re.IGNORECASE,
63
+ )
64
+
65
+
66
+ def validate_final_verification_initial_prompt(text: str) -> list[str]:
67
+ """Return deterministic compact-prompt contract violations."""
68
+ errors: list[str] = []
69
+ _reject_literal(
70
+ text,
71
+ "**Coding preflight pack:**",
72
+ "Coding preflight pack is forbidden for final-verification",
73
+ errors,
74
+ )
75
+ _reject_literal(
76
+ text,
77
+ "**Verification diff stat:**",
78
+ "inline Verification diff stat is forbidden; use verification-target.md",
79
+ errors,
80
+ )
81
+ _reject_literal(
82
+ text,
83
+ "## Source / fallback paths",
84
+ "Source / fallback paths is forbidden; use analysis-packet.md",
85
+ errors,
86
+ )
87
+ for label, pattern in _COPIED_SECTION_PATTERNS:
88
+ if pattern.search(text):
89
+ errors.append(f"copied {label} section is forbidden; use analysis-packet.md")
90
+ _validate_compact_target_identity(text, errors)
91
+ packet_count = len(_PRIMARY_PACKET_RE.findall(text))
92
+ if packet_count != 1:
93
+ errors.append(
94
+ "exactly one Primary analysis packet path is required "
95
+ f"(found {packet_count})"
96
+ )
97
+ directive_count = text.count(_DIRECTIVE_HEADING)
98
+ if directive_count > 1:
99
+ errors.append("at most one Run-specific directive section is allowed")
100
+ if directive_count == 1:
101
+ directive_lines = _directive_nonblank_lines(text)
102
+ if directive_lines > MAX_FINAL_VERIFICATION_DIRECTIVE_LINES:
103
+ errors.append(
104
+ "Run-specific directive exceeds "
105
+ f"{MAX_FINAL_VERIFICATION_DIRECTIVE_LINES} nonblank lines "
106
+ f"(found {directive_lines})"
107
+ )
108
+ body_lines = _body_nonblank_lines(text)
109
+ if body_lines > MAX_FINAL_VERIFICATION_BODY_LINES:
110
+ errors.append(
111
+ f"prompt body exceeds {MAX_FINAL_VERIFICATION_BODY_LINES} "
112
+ f"nonblank lines (found {body_lines})"
113
+ )
114
+ return errors
115
+
116
+
117
+ def normalise_analysis_prompt(text: str) -> str:
118
+ """Remove only permitted worker identity, model, role, and path deltas."""
119
+ normalized: list[str] = []
120
+ for line in text.replace("\r\n", "\n").replace("\r", "\n").splitlines():
121
+ stripped = line.strip()
122
+ prefix = next(
123
+ (candidate for candidate in _WORKER_SPECIFIC_PREFIXES if stripped.startswith(candidate)),
124
+ "",
125
+ )
126
+ if prefix:
127
+ normalized.append(f"{prefix} <worker-specific>")
128
+ continue
129
+ normalized.append(_WORKER_LABEL_RE.sub("<analysis-worker>", line.rstrip()))
130
+ return "\n".join(normalized).strip() + "\n"
131
+
132
+
133
+ def validate_analysis_prompt_set(prompts: Mapping[str, str]) -> list[str]:
134
+ """Require byte-identical normalized bodies for initial analysis workers."""
135
+ if len(prompts) < 2:
136
+ return []
137
+ normalized = {
138
+ worker_id: normalise_analysis_prompt(text)
139
+ for worker_id, text in sorted(prompts.items())
140
+ }
141
+ baseline_worker = next(iter(normalized))
142
+ baseline = normalized[baseline_worker]
143
+ different = [
144
+ worker_id
145
+ for worker_id, body in normalized.items()
146
+ if body != baseline
147
+ ]
148
+ if not different:
149
+ return []
150
+ workers = ", ".join([baseline_worker, *different])
151
+ return [f"normalized initial analysis prompts differ across workers: {workers}"]
152
+
153
+
154
+ def validate_final_verification_prompt_paths(
155
+ prompt_paths: Mapping[str, Path],
156
+ ) -> list[str]:
157
+ """Validate persisted initial prompts and their normalized equality."""
158
+ prompts: dict[str, str] = {}
159
+ errors: list[str] = []
160
+ for worker_id, path in sorted(prompt_paths.items()):
161
+ try:
162
+ text = path.read_text(encoding="utf-8")
163
+ except OSError as exc:
164
+ errors.append(f"{worker_id}: cannot read prompt {path}: {exc}")
165
+ continue
166
+ prompts[worker_id] = text
167
+ errors.extend(
168
+ f"{worker_id}: {error}"
169
+ for error in validate_final_verification_initial_prompt(text)
170
+ )
171
+ errors.extend(validate_analysis_prompt_set(prompts))
172
+ return errors
173
+
174
+
175
+ def _reject_literal(
176
+ text: str,
177
+ literal: str,
178
+ message: str,
179
+ errors: list[str],
180
+ ) -> None:
181
+ if literal in text:
182
+ errors.append(message)
183
+
184
+
185
+ def _validate_compact_target_identity(text: str, errors: list[str]) -> None:
186
+ stripped_lines = [line.strip() for line in text.splitlines()]
187
+ for prefix in _REQUIRED_TARGET_PREFIXES:
188
+ matches = [line for line in stripped_lines if line.startswith(prefix)]
189
+ if len(matches) != 1 or not matches[0][len(prefix):].strip():
190
+ errors.append(f"exactly one non-empty {prefix} header is required")
191
+ scope_line = next(
192
+ (line for line in stripped_lines if line.startswith("**Verification scope:**")),
193
+ "",
194
+ )
195
+ scope = scope_line.removeprefix("**Verification scope:**").strip()
196
+ if scope and scope not in {"whole-task", "single-stage"}:
197
+ errors.append("Verification scope must be whole-task or single-stage")
198
+ digest_line = next(
199
+ (
200
+ line
201
+ for line in stripped_lines
202
+ if line.startswith("**Verification target digest:**")
203
+ ),
204
+ "",
205
+ )
206
+ digest = digest_line.removeprefix("**Verification target digest:**").strip()
207
+ if digest and not re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
208
+ errors.append("Verification target digest must be sha256:<64 lowercase hex>")
209
+
210
+
211
+ def _directive_nonblank_lines(text: str) -> int:
212
+ section = text.split(_DIRECTIVE_HEADING, 1)[1]
213
+ section = re.split(r"(?m)^##\s+", section, maxsplit=1)[0]
214
+ return sum(1 for line in section.splitlines() if line.strip())
215
+
216
+
217
+ def _body_nonblank_lines(text: str) -> int:
218
+ return sum(
219
+ 1
220
+ for line in text.splitlines()
221
+ if line.strip()
222
+ and not any(line.strip().startswith(prefix) for prefix in _NON_BODY_PREFIXES)
223
+ )
@@ -41,19 +41,48 @@ def worker_prompt_headers(
41
41
  ) -> list[str]:
42
42
  """Render the team-contract worker prompt anchor headers."""
43
43
  prompt_path = _resolve_project_path(project_root, prompt_rel)
44
- return [
44
+ headers = [
45
45
  f"**Project Root:** {project_root}",
46
46
  f"**Prompt History Path:** {prompt_rel}",
47
47
  f"**Result Path:** {result_rel}",
48
48
  f"Assigned worker prompt history path: {prompt_path}",
49
49
  f"**Worker Preamble Path:** {_worker_preamble_path()}",
50
- f"**Coding preflight pack:** {_coding_preflight_pack_path(active_context)}",
50
+ ]
51
+ if _string_value(manifest.get("taskType")) == "implementation":
52
+ headers.append(
53
+ f"**Coding preflight pack:** {_coding_preflight_pack_path(active_context)}"
54
+ )
55
+ headers.extend([
51
56
  f"**Errors log path:** {_errors_log_path(project_root, manifest, active_context)}",
52
57
  (
53
58
  f"**Errors sidecar path:** "
54
59
  f"{_worker_errors_sidecar_path(project_root, manifest, active_context, worker_id)}"
55
60
  ),
56
61
  READ_SCOPE_HEADER,
62
+ ])
63
+ if _string_value(manifest.get("taskType")) == "final-verification":
64
+ headers.extend(_final_verification_target_headers(active_context))
65
+ return headers
66
+
67
+
68
+ def _final_verification_target_headers(
69
+ active_context: Mapping[str, Any],
70
+ ) -> list[str]:
71
+ target = active_context.get("verificationTarget")
72
+ if not isinstance(target, Mapping):
73
+ return []
74
+ fields = (
75
+ ("Worktree", "worktreePath"),
76
+ ("Verification scope", "scope"),
77
+ ("Verification base ref", "baseRef"),
78
+ ("Verification head ref", "headRef"),
79
+ ("Verification target path", "path"),
80
+ ("Verification target digest", "digest"),
81
+ )
82
+ return [
83
+ f"**{label}:** {_string_value(target.get(key))}"
84
+ for label, key in fields
85
+ if _string_value(target.get(key))
57
86
  ]
58
87
 
59
88
 
@@ -91,30 +91,37 @@ If a required header line is absent from the dispatch prompt, return `<SENTINEL_
91
91
 
92
92
  ## Anchor headers (lead-injected, BLOCKING)
93
93
 
94
- Every worker prompt MUST begin with these anchor headers, in this exact order, before any other content. Workers extract them verbatim.
94
+ Every initial worker prompt MUST begin with the common anchor headers below in this exact order, before any other content. An `implementation` prompt inserts the implementation-only coding-preflight anchor at the documented position. Workers extract the anchors verbatim.
95
95
 
96
96
  1. `**Project Root:** <absolute-path>` — required so the worker can self-anchor without relying on inherited cwd.
97
97
  2. `**Prompt History Path:** <project-relative-path>`
98
98
  3. `**Result Path:** <project-relative-path>` — canonical destination for the worker's result file.
99
99
  4. `Assigned worker prompt history path: <absolute-path>` — same as the prompt-history path but resolved against `Project Root`. Codex / Antigravity wrapper subagents extract this exact line.
100
100
  5. `**Worker Preamble Path:** <absolute-path>` — points to THIS file. Workers Read it end-to-end before doing anything else.
101
- 6. `**Coding preflight pack:** <absolute-path>` — points to the installed runtime resource pack under `<OKSTRA_HOME>/prompts/coding-preflight`. Implementation executors and verifiers use this to read `overview.md`, `clean-code.md`, and routed language/framework/architecture resources. Other worker roles may ignore it.
101
+ 6. `**Coding preflight pack:** <absolute-path>` — implementation-only; points to the installed runtime resource pack under `<OKSTRA_HOME>/prompts/coding-preflight`. It is present only when `taskType == implementation` and is absent from every non-implementation initial prompt. Implementation executors and implementation verifiers use it to read `overview.md`, `clean-code.md`, and routed language/framework/architecture resources. A pane display role named `verifier` does not activate this anchor in `final-verification`.
102
102
  7. `**Errors log path:** <absolute-path>` — run-level JSONL (see Error reporting above).
103
103
  8. `**Errors sidecar path:** <absolute-path>` — per-worker JSON (see Error reporting above).
104
104
  9. `**Read scope:** …` — restates the *Reading rules* allowlist inline. This file is anchor 5, a path the worker opens partway into its run; by then a host SessionStart hook or a global `CLAUDE.md` / `AGENTS.md` project-conditional has already pointed it at `graphify-out/`, skill catalogs, and similar non-okstra artifacts. Only the inline copy arrives before the worker's first tool call.
105
105
 
106
- **Exception — reverify dispatches.** A Phase 5.5 re-verification prompt (its `**Prompt History Path:**` carries a `-reverify-r<N>-` segment) deliberately omits anchors 5 and 6 and adds a `**Model:**` line — lightweight mode reads neither this file nor the preflight pack. The two errors-path anchors are NOT omitted; that gate applies to reverify prompts unchanged. The reverify anchor set is owned by `prompts/lead/convergence.md` "Required reverify-prompt anchor headers".
106
+ **Exception — reverify dispatches.** A Phase 5.5 re-verification prompt (its `**Prompt History Path:**` carries a `-reverify-r<N>-` segment) deliberately omits anchor 5 and adds a `**Model:**` line — lightweight mode reads neither this file nor the implementation-only preflight pack. The two errors-path anchors are NOT omitted; that gate applies to reverify prompts unchanged. The reverify anchor set is owned by `prompts/lead/convergence.md` "Required reverify-prompt anchor headers".
107
107
 
108
108
  For the **implementation phase** specifically, the dispatched prompt MUST also include:
109
109
 
110
110
  - `**Worktree:** <absolute-path>` — the task worktree path.
111
111
  - `cwd for every mutating command: <absolute-path>` — same as Worktree path; used by codex / antigravity wrappers as `--add-dir` / `--include-directories`.
112
112
 
113
- For the **final-verification phase** specifically, the dispatched prompt MUST also include the verification target snapshot so every analyser verifies the SAME target the lead captured at the entry gate:
113
+ For the **final-verification phase** specifically, the dispatched prompt MUST include the compact identity of the canonical verification-target sidecar so every analyser verifies the SAME target the lead captured at the entry gate:
114
114
 
115
115
  - `**Worktree:** <absolute-path>` — the checkout under verification (read-only).
116
+ - `**Verification scope:** <whole-task|single-stage>` — the selected verification mode.
116
117
  - `**Verification base ref:** <base-ref>` — the implementation base for the diff.
117
- - `**Verification diff stat:** <git diff --stat output>` — the captured diff-stat that bounds the verification scope.
118
+ - `**Verification head ref:** <head-ref>` — the exact delivered revision under verification.
119
+ - `**Verification target path:** <project-relative-path>` — the canonical `instruction-set/verification-target.md` sidecar containing stages, source reports, and the complete captured diff stat.
120
+ - `**Verification target digest:** sha256:<digest>` — the digest over the normalized snapshot body.
121
+
122
+ Read the sidecar on demand when the full stage/report mapping or diff stat is needed. Do not copy its `Verification diff stat` block into the dispatch prompt.
123
+
124
+ The initial analysis prompt may contain exactly one `## Run-specific directive` section only when the run has a shared delta not already captured by the packet. The section applies unchanged to every selected analysis worker and is limited to 40 nonblank lines. If it would be longer, the lead writes the shared material into the instruction set and adds its common reference to `analysis-packet.md`; workers must not accept provider-, model-, or worker-specific verification scopes.
118
125
 
119
126
  For the **improvement-discovery phase** specifically, the dispatched prompt MUST also include the resolved-scope log the lead wrote during Phase 1.5, so workers key off an explicit absolute path instead of re-deriving the run directory:
120
127
 
@@ -58,6 +58,9 @@ from okstra_ctl.design_surfaces import ( # noqa: E402
58
58
  detect_design_surfaces,
59
59
  expected_prep_plan_item_id,
60
60
  )
61
+ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
62
+ validate_final_verification_prompt_paths,
63
+ )
61
64
 
62
65
  TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
63
66
  ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
@@ -367,6 +370,48 @@ def effective_run_task_type(run_manifest: dict, task_manifest: dict) -> str:
367
370
  ).strip()
368
371
 
369
372
 
373
+ def _validate_final_verification_initial_prompts(
374
+ data: dict,
375
+ failures: list[str],
376
+ ) -> None:
377
+ """Validate the selected initial analysis prompts persisted for the run."""
378
+ if data.get("taskType") != "final-verification":
379
+ return
380
+
381
+ run_manifest = data.get("runManifest") or {}
382
+ team_contract = run_manifest.get("teamContract") or {}
383
+ selected_workers = team_contract.get("requiredWorkerRoles") or []
384
+ selected_ids = {
385
+ str(worker.get("workerId") or "").strip()
386
+ for worker in selected_workers
387
+ if isinstance(worker, dict)
388
+ }
389
+ project_root = Path(data["projectRoot"])
390
+ team_state = data.get("teamState") or {}
391
+ prompt_paths: dict[str, Path] = {}
392
+ for worker in team_state.get("workers") or []:
393
+ if not isinstance(worker, dict):
394
+ continue
395
+ worker_id = str(worker.get("workerId") or "").strip()
396
+ prompt_value = str(worker.get("promptPath") or "").strip()
397
+ if (
398
+ worker_id not in selected_ids
399
+ or worker_id == "report-writer"
400
+ or not prompt_value
401
+ or "-reverify-r" in Path(prompt_value).name
402
+ ):
403
+ continue
404
+ prompt_path = Path(prompt_value)
405
+ prompt_paths[worker_id] = (
406
+ prompt_path if prompt_path.is_absolute() else project_root / prompt_path
407
+ )
408
+
409
+ errors = validate_final_verification_prompt_paths(prompt_paths)
410
+ failures.extend(
411
+ f"final-verification prompt contract: {error}" for error in errors
412
+ )
413
+
414
+
370
415
  def _is_legal_concurrent_run_skip(
371
416
  team_create: object, concurrent_run_authorized: bool
372
417
  ) -> bool:
@@ -3524,6 +3569,15 @@ def main() -> int:
3524
3569
  # contain every required section. Substring checks below are a
3525
3570
  # safety net for hand-edited or pre-v1.0 reports.
3526
3571
  task_type = effective_run_task_type(run_manifest, task_manifest)
3572
+ _validate_final_verification_initial_prompts(
3573
+ {
3574
+ "taskType": task_type,
3575
+ "projectRoot": project_root,
3576
+ "runManifest": run_manifest,
3577
+ "teamState": team_state,
3578
+ },
3579
+ failures,
3580
+ )
3527
3581
  report_contracts = _normalize_report_contracts(
3528
3582
  run_manifest.get("reportContracts")
3529
3583
  )
@@ -6,9 +6,11 @@ Usage:
6
6
  okstra log-report [--project-root <dir>] [--cwd <dir>] [--top <N>] [--json]
7
7
 
8
8
  Output: JSON { ok, projectRoot, logsRoot, topLargest[], perTask[], totals }.
9
- Scans <project-root>/.okstra/tasks/**/runs/*/prompts/*.log. Sizes are raw bytes
10
- and mtimes are epoch seconds the caller formats KB/MB and ages. Read-only: it
11
- never deletes logs (cleanup commands stay in the skill).
9
+ Scans <project-root>/.okstra/tasks/**/runs/*/prompts/*.log. Each entry reports
10
+ the wrapper transcript as transcriptBytes and its sibling prompt Markdown as
11
+ promptBytes; sizeBytes remains the transcript-size compatibility field. Sizes
12
+ are raw bytes and mtimes are epoch seconds — the caller formats KB/MB and ages.
13
+ Read-only: it never deletes logs (cleanup commands stay in the skill).
12
14
  `;
13
15
 
14
16
  export async function run(args) {