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.
- package/docs/architecture/storage-model.md +10 -3
- package/docs/architecture.md +12 -1
- package/docs/cli.md +1 -0
- package/docs/project-structure-overview.md +7 -6
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/convergence.md +4 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -0
- package/runtime/prompts/lead/team-contract.md +11 -3
- package/runtime/prompts/profiles/final-verification.md +10 -6
- package/runtime/python/okstra_ctl/analysis_packet.py +7 -2
- package/runtime/python/okstra_ctl/codex_dispatch.py +27 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +20 -0
- package/runtime/python/okstra_ctl/log_report.py +44 -5
- package/runtime/python/okstra_ctl/path_hints.py +5 -0
- package/runtime/python/okstra_ctl/render.py +22 -0
- package/runtime/python/okstra_ctl/run.py +66 -2
- package/runtime/python/okstra_ctl/wizard.py +15 -4
- package/runtime/python/okstra_ctl/work_categories.py +37 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +223 -0
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +31 -2
- package/runtime/templates/worker-prompt-preamble.md +12 -5
- package/runtime/validators/validate-run.py +54 -0
- package/src/commands/inspect/log-report.mjs +5 -3
|
@@ -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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
package/docs/architecture.md
CHANGED
|
@@ -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`
|
|
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
|
|
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
|
|
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
package/runtime/BUILD.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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({
|
|
72
|
-
|
|
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": {
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
10
|
-
|
|
11
|
-
|
|
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) {
|