okstra 0.199.1 → 0.199.3
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/cli.md +4 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +3 -2
- package/runtime/prompts/lead/adapters/cmux.md +2 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +3 -2
- package/runtime/prompts/lead/plan-body-verification.md +9 -5
- package/runtime/prompts/lead/report-writer.md +12 -9
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/forbidden-actions.json +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +5 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +8 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +10 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +5 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +7 -1
- package/runtime/python/okstra_ctl/blocking_checks.py +1 -1
- package/runtime/python/okstra_ctl/conformance.py +7 -0
- package/runtime/python/okstra_ctl/convergence_store.py +16 -1
- package/runtime/python/okstra_ctl/error_log_core.py +2 -1
- package/runtime/python/okstra_ctl/error_log_write.py +36 -0
- package/runtime/python/okstra_ctl/error_report.py +20 -1
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -4
- package/runtime/python/okstra_ctl/plan_items.py +5 -2
- package/runtime/python/okstra_ctl/plan_items_cli.py +94 -37
- package/runtime/python/okstra_ctl/qa_commands.py +26 -2
- package/runtime/python/okstra_ctl/report_assembly.py +13 -2
- package/runtime/python/okstra_ctl/report_finalize.py +108 -20
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +7 -0
- package/runtime/python/okstra_ctl/run.py +10 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +13 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -0
- package/runtime/skills/okstra-inspect/SKILL.md +4 -2
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/validators/forbidden_actions.py +3 -0
- package/runtime/validators/validate-run.py +105 -27
- package/runtime/validators/validate_session_conformance.py +5 -0
package/docs/cli.md
CHANGED
|
@@ -815,9 +815,9 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
815
815
|
| `okstra plan-items derivations --data <data.json> --response <user-response sidecar> [--clarification C-NNN]` | List the plan statements an answered clarification may have falsified. Extracts the symbols, paths, and ids the answer names (backticked spans plus `R-001` / `DEV-10174`-style ids) and reports every string in the plan body that mentions one, as a JSON pointer plus excerpt. Advisory: it says where a decision's subject is mentioned, never which mentions are now wrong — the supersession rule (`prompts/profiles/_common-contract.md`) requires the author to enumerate before editing, and this supplies the enumeration |
|
|
816
816
|
| `okstra plan-items <prepare\|prompt\|validate-prepared> --run-manifest <path> …` | Bind the implementation-planning verification queue to the run manifest. `prepare` extracts the exact queue from `--narrative` and, when `designPreparation.mode` is `no-design-inputs` and the Stage Map has one row, flips `convergence.planBodyVerification.gating` to `false` (stdout `Gating`). `prompt` emits its fixed lossless view, ending with the parser-facing `## Response format` block so the block reaches every verifier with the queue; `validate-prepared` proves the prepared queue still matches the narrative. With `--state <plan-body-verification.json>` the round is a re-verification: `prepare` also carries each queued item's recorded votes and `selfFixNote` into the envelope as `priorRounds`, `prompt` renders them as that item's `**Prior round dissent**` block behind a re-verification preamble, and `validate-prepared` re-derives the carry and rejects an envelope that dropped it. Python resolves the convergence-owned state path, so model callers never choose it. |
|
|
817
817
|
| `okstra plan-items seed --narrative <report-narrative.md> --state <plan-body-verification.json> [--prior-state <previous plan-body-verification.json>]` | Create the convergence-owned `planBodyVerification.planItems[]` rows every verdict lands in, from the same deterministic extraction `extract` uses. The historical v2 form is `--data <data.json>`. Idempotent by id: an existing row keeps its verdicts and carried fields. Reports `seeded` / `existing` counts. `--prior-state` carries the previous **run**'s verdicts into this one: a newly seeded item whose `contentHash` equals that run's `verifiedContentHash` for the same id inherits its `verdicts[]` and is tagged `carriedForwardFromSeq` with the seq read off the prior filename, so round 1 does not re-judge text nobody changed. A matching id alone never carries — `P-*` ids are positional and shift. It requires `--state`, refuses a prior state whose task root differs from the one `--state` lives under (the state file carries no task identity, so its path is the only identity there is), and when it carries anything it rewrites the sibling `plan-items-*.json` `dispatchQueue` the way `incremental-carry` does. Adds `carried` / `carriedForwardFromSeq` to the reported counts. |
|
|
818
|
-
| `okstra plan-items apply-verdicts --state <plan-body-verification.json> --result <worker-id>=<result.md>… --round <N>` | Read each worker's Markdown verdict directly, validate every current `P-*` id, reject duplicate worker submissions, and overwrite that round's stored verdicts. `--items <plan-items artifact>` narrows the assignment to what a partial round actually dispatched — pass the `--tie-vote` artifact on a critic tie round, or every item the critic was never given is refused as unanswered. The unanswered check still applies inside the narrowed set. The historical `--verdicts <file>` form remains automation compatibility only. Without `--append` every recorded verdict row of the queued items is replaced; when a row belongs to a round `complete-round` never closed, the command refuses before writing and names the `complete-round --round <M>` to run first. `--discard-open-rounds` replaces anyway — the recovery path when those rounds are being re-applied from their result files in order (the discarded rows are printed); it also restores `dispatchQueue` to the items those result files answer, since the persisted queue belongs to the latest round and an earlier round's verdicts would otherwise be refused as outside it. |
|
|
819
|
-
| `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N> [--self-fix-note <item-id>=<markdown-file>]… [--self-fix-group <cause-file>=<item-id>[,<item-id>...]]… [--self-fix-stop-reason <all-resolved\|no-progress\|max-rounds-reached>]` | After `plan-verify` succeeds, atomically derive and record the round's per-item votes, gate result, participant counts from the actual assigned roster, immutable completion time, convergence history, and optional self-fix notes/groups read from Markdown files. `--self-fix-group` requires `--self-fix-stop-reason` — there is no default. `--self-fix-stop-reason` alone records a stop for a round that rewrote nothing and leaves `selfFixGroups` / `selfFixRoundsApplied` untouched. Models do not write the state JSON. Stdout also carries `nextDispatch`.
|
|
820
|
-
| `okstra plan-items next-dispatch --state <plan-body-verification.json> [--run-manifest <path>]` | After `apply-verdicts`, decide whether this round opens a worker batch. `kind: none` — missing-dependency `UNVERIFIABLE` only, no new batch. `kind: worker-correction` — re-prompt only those workers; peers stay idle. `kind: critic-tie` — unsettled analyser 1-1 on a run that rostered a critic, `critic-worker` on those item ids only. `kind: user-decision` — the same 1-1 on a run with no critic rostered: no in-band vote can break it, so open one `okstra approval-decision open` per item (classification `noncritical-dissent`) plus its `## 1. Clarification Items` row and dispatch no further verification for them. `--run-manifest` is what tells the two apart (`invocationAssignments` `critic/*`); without it the answer stays `critic-tie`. A missing path is never environment-unverifiable. After the single automatic self-fix, remaining items route to `lead-decision` first, then `user-decision`, with no further automatic worker batch. |
|
|
818
|
+
| `okstra plan-items apply-verdicts --state <plan-body-verification.json> --result <worker-id>=<result.md>… --round <N>` | Read each worker's Markdown verdict directly, validate every current `P-*` id, reject duplicate worker submissions, and overwrite that round's stored verdicts. `--items <plan-items artifact>` narrows and persists the assignment shared by verdict validation and round completion to what a partial round actually dispatched — pass the `--tie-vote` artifact on a critic tie round, or every item the critic was never given is refused as unanswered. The unanswered check still applies inside the narrowed set. Earlier verdicts and completed-round history outside that set remain unchanged. The historical `--verdicts <file>` form remains automation compatibility only. Without `--append` every recorded verdict row of the queued items is replaced; when a row belongs to a round `complete-round` never closed, the command refuses before writing and names the `complete-round --round <M>` to run first. `--discard-open-rounds` replaces anyway — the recovery path when those rounds are being re-applied from their result files in order (the discarded rows are printed); it also restores `dispatchQueue` to the items those result files answer, since the persisted queue belongs to the latest round and an earlier round's verdicts would otherwise be refused as outside it. |
|
|
819
|
+
| `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N> [--items <plan-items artifact>] [--self-fix-note <item-id>=<markdown-file>]… [--self-fix-group <cause-file>=<item-id>[,<item-id>...]]… [--self-fix-stop-reason <all-resolved\|no-progress\|max-rounds-reached>]` | After `plan-verify` succeeds, atomically derive and record the round's per-item votes, gate result, participant counts from the actual assigned roster, immutable completion time, convergence history, and optional self-fix notes/groups read from Markdown files. `--self-fix-group` requires `--self-fix-stop-reason` — there is no default. `--self-fix-stop-reason` alone records a stop for a round that rewrote nothing and leaves `selfFixGroups` / `selfFixRoundsApplied` untouched. Use `--items` to restore the prepared partial queue when an older version saved critic verdicts but left the full queue in state; only the assigned items need verdicts for this round, and prior votes elsewhere remain unchanged. Models do not write the state JSON. Stdout also carries `nextDispatch`. With `gating=false`, critic corrections are exempt from the analyser round limit and do not require an even split. A later `--append` updates the critic verdict while retaining analyser votes and completed round history. A second automatic rewrite is rejected; the one rewrite may contain several cause groups. `selfFixRoundsApplied` remains its verification round number, not a rewrite count. |
|
|
820
|
+
| `okstra plan-items next-dispatch --state <plan-body-verification.json> [--run-manifest <path>]` | After `apply-verdicts`, decide whether this round opens a worker batch. `kind: none` — missing-dependency `UNVERIFIABLE` only, no new batch. `kind: worker-correction` — re-prompt only those workers; peers stay idle. `kind: critic-tie` — unsettled analyser 1-1 on a run that rostered a critic, `critic-worker` on those item ids only. `kind: user-decision` — the same 1-1 on a run with no critic rostered: no in-band vote can break it, so open one `okstra approval-decision open` per item (classification `correctness-critical` for unresolved kind `a` outside `P-Var-*` or kind `f` on `P-Req-*`, otherwise `noncritical-dissent`) plus its `## 1. Clarification Items` row and dispatch no further verification for them. `--run-manifest` is what tells the two apart (`invocationAssignments` `critic/*`); without it the answer stays `critic-tie`. A missing path is never environment-unverifiable. After the single automatic self-fix, remaining items route to `lead-decision` first, then `user-decision`, with no further automatic worker batch. |
|
|
821
821
|
| `okstra plan-items resolve-dissent --state <plan-body-verification.json> --item <P-id> --decision-file <markdown-file>` | Record the lead decision, its authority within agreed scope, and cited evidence after the one self-fix. Accepts only current, noncritical design judgements; facts, user-input requirements and non-results require user confirmation. Keeps original votes and appends the decision to the report dissent log. The decision expires when its content, scope or verdicts change. |
|
|
822
822
|
| `okstra plan-items correction-prompt --state <plan-body-verification.json> --run-manifest <path> --worker <id>` | Emit the planning-time environment-gap paragraph, then the assigned queue. The environment exception is first. Exits 2 unless `next-dispatch` named that worker as a blanket-UNVERIFIABLE correction target. |
|
|
823
823
|
| `okstra error-log append-observed --out <errors.jsonl> --task-key <key> --phase <task-type> --agent <assigned-worker-id> --agent-role worker --model <model> --error-type tool-failure --command-file <markdown-file> --command-kind <kind> --message-file <markdown-file> [--cause <cause> --evidence-file <kind=file>]…` | Worker-facing typed error recording surface. Python validates and serializes the JSONL record; a worker supplies scalar identity fields plus Markdown files for free-form command, message, and probe content, never a JSON sidecar or JSON argument. `sandbox-denied` requires both `targetProbe` and `controlProbe` evidence files. |
|
|
@@ -864,7 +864,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
864
864
|
| `okstra option-votes gaps --task-manifest <task-manifest.json> (--report <final-report .data.json> \| --narrative <report-writer narrative.md>) [--json]` | List the `implementation-option-selection` candidates that fail the ranking rule on the every-analyser clause alone, and name the analyser owing each vote. Round 1 runs the designers in parallel, so each votes only on the candidates it proposed and the merged set keeps a different hole per analyser; a run whose comparison had converged can end `blocked` with an empty `rankedOptions` for that reason alone. The lead reads this before concluding `blocked` and dispatches one vote-completion assignment per named analyser — a feasibility verdict on the named candidate and nothing else, so the run stays in `candidate-comparison` mode. A candidate carrying `safetyBlockers` or `unresolvedFeasibilityFacts`, or one that could not reach two `feasible` votes even with every missing vote, is excluded: another round would not change it. `--narrative` reads the writer's markdown before assembly and tolerates its value defects; `--report` reads a published record. |
|
|
865
865
|
| `okstra design-snapshot --narrative <report-narrative.md> --output <design-preparation.json>` | Detect implementation-planning design surfaces and write the detector-owned snapshot consumed by final report assembly. |
|
|
866
866
|
| `okstra plan-verify --narrative <report-narrative.md> --state <plan-body-verification.json>` | Recompute the plan-body gate from the convergence-owned state before `data.json` publication. `--report <historical-data.json>` remains the v2 reader. |
|
|
867
|
-
| `okstra report-finalize --project-root <dir> --run-manifest <path> --report <final-report.md>` | Run Phase 7 in the manifest's contract order. Contract v3 collects usage into team state, assembles all single-owner inputs into `data.json` once, translates (`translate`: for a non-English `reportLanguage` it materializes and dispatches the translator worker unless the `*.i18n.<lang>.json` sidecar already exists, and fails when the worker leaves none), then renders with that sidecar overlaid, spawns follow-ups, validates, records the run's conclusion and the group's start order into the task-group's `group-context.md` (`record-group-memory`, creating the file when absent), and tears down eligible stage worktrees. Contract v2 retains its historical in-place projection sequence as a read-only compatibility path. A failed step still runs every later check through `validate-run`; `record-group-memory` and `teardown-stages` are skipped so a failed run neither hands an unvalidated conclusion to sibling tasks nor reclaims worktrees. The result carries `nextInGroup` (the first task in start order not yet started) and, for a terminal pointer, `nextCommand` closes on starting it from its brief. Reports each step and prints the ordered `--only` recovery tail from the earliest failure. This is the shared path for every lead adapter. |
|
|
867
|
+
| `okstra report-finalize --project-root <dir> --run-manifest <path> --report <final-report.md>` | Run Phase 7 in the manifest's contract order. Contract v3 collects usage into team state, assembles all single-owner inputs into `data.json` once, translates (`translate`: for a non-English `reportLanguage` it materializes and dispatches the translator worker unless the `*.i18n.<lang>.json` sidecar already exists, and fails when the worker leaves none), then renders with that sidecar overlaid, spawns follow-ups, validates, records the run's conclusion and the group's start order into the task-group's `group-context.md` (`record-group-memory`, creating the file when absent), and tears down eligible stage worktrees. Contract v2 retains its historical in-place projection sequence as a read-only compatibility path. A failed step still runs every later check through `validate-run`; `record-group-memory` and `teardown-stages` are skipped so a failed run neither hands an unvalidated conclusion to sibling tasks nor reclaims worktrees. The result carries `nextInGroup` (the first task in start order not yet started) and, for a terminal pointer, `nextCommand` closes on starting it from its brief. Reports each step and prints the ordered `--only` recovery tail from the earliest failure. Repairable failures return `recovery.mode=same-run`, complete assembly owner issues, and `resumeCommand` using this manifest; `nextCommand` does not ask for a new run. A validator-only failure targeting an earlier phase retains that recovery target. Finalization step failures and run-bound contract exceptions from `plan-items` and `agent-prompt` write bounded runtime error records, with any logging failure reported separately. This is the shared path for every lead adapter. |
|
|
868
868
|
| `okstra render-views <final-report.data.json\|final-report.md>` | The Phase 7 `render-views` step, runnable on its own. Schema v2 data is rendered directly, and schema v3 data uses the same always-generated, task-specific human HTML path. The full reading copy uses `templates/reports/final-report-v2.template.md` and is rendered on demand with `okstra render-final-report`. Passing the Markdown sibling locates the same data.json. Schema v1 and quick reports keep the legacy conditional renderer. The Node wrapper calls `scripts/okstra-render-report-views.py`; `validators/validate-report-views.py` verifies source/schema/template digests, required human fields, form controls, external assets, diagram/table ID parity, Response ID parity, and that every in-page `href="#…"` lands on an element of the page. For a non-English report the command prints two counts: `translated N string(s) into <lang> (M left in English, K unresolved)` from the sidecar overlay, and `rendered R line(s) still in English on the <lang> page` from the written page itself — the second sees fields the extractor does not offer, so `M = 0` with `R > 0` means a reader-facing key is missing from `PROSE_KEYS`. |
|
|
869
869
|
| `okstra design-prep <list\|show\|write>` | Review AI-prepared implementation design requests, inspect their effective confirmed response, or append a confirmed user/wizard response without editing the planning report |
|
|
870
870
|
| `okstra wizard <init\|step\|render-args\|confirmation\|outcome> --state-file <path>` | Interactive input state machine for okstra-run, implemented by `okstra_ctl.wizard`. Seed a state file with `init`, then repeatedly call `step --answer <val>` to receive the next `Prompt` JSON. `--answer` is **required**; use `--no-submit` to peek at the next prompt without submitting a response. A `pick` with more choices than the host picker can display keeps `kind: "pick"` but adds `presentation: "numbered-text"`; render every option as a numbered Markdown list and submit the user's 1-based number, exact value, or exact label. Invalid, out-of-range, and ambiguous answers re-prompt without dropping choices. `render-args` returns the final `render-bundle` argument map, and `confirmation` returns the user echo block. On a completed wizard, `outcome` returns `renderArgs`, `persistActions`, and `confirmationText` together; project/global release-handoff PR-template persistence appears as `persistActions[].command == "config.set"`. For an `implementation` task type, `stage_pick` follows `approved_plan_pick` and selects the stage before `executor_pick`. The brief step appears only for entry task types—requirements-discovery, error-analysis, improvement-discovery, project-analysis, feature-analysis, and change-impact-analysis. Analysis inputs use `feature_evidence_pick` / `feature_evidence`, `project_evidence_pick` / `project_evidence`, and `analysis_target_pick` / `analysis_target`; a revision-requested report prioritizes its same-task, same-type rerun. Downstream lifecycle phases automatically carry the manifest brief, with a three-option `brief_carry` fallback when none is registered; `release-handoff` has no brief and enters multi-select `handoff_stage_pick` for eligible stage groups or the whole task |
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -47,9 +47,11 @@ After Phase 7 persistence, the last user-facing message of this run is the next
|
|
|
47
47
|
|
|
48
48
|
Close with one command the user can run now. The first matching row wins:
|
|
49
49
|
|
|
50
|
+
- Phase 7 failure with `recovery.mode: same-run` → continue correcting the owner inputs and execute `recovery.resumeCommand` in this run. Preserve user approvals and model choices. Read the report-writer recovery procedure before correcting its narrative. Do not close by asking the user to restart the phase for repairable artifact errors. If the only failure is `validate-run` and a blocked pointer targets an earlier phase, `recovery.mode: phase-reentry` preserves that target in `nextCommand`. Other validator findings are advisories, printed as `validate-run: advisory — <finding>`; follow the matching pointer row when the run passed.
|
|
51
|
+
|
|
50
52
|
- Open `blocks: approval` rows, or a `blocked-by-disagreement` gate whose approval rows carry no progress disposition → `/okstra-user-response` (name the `C-NNN` ids). An `accept-risk` / `select` / `answer` already recorded is not an open blocker. A report published with those rows open is not a failed run — the answers are the missing input, and a planning re-run without them reproduces the same block.
|
|
51
53
|
|
|
52
|
-
After `implementation-planning
|
|
54
|
+
After a successful `implementation-planning` finalization, stop on the approval row above. No open approval blocker → `/okstra-run` → `implementation` or `--approve`. Do not propose another planning run. Do not say `/okstra-inspect`. A blocking `validate-run` failure follows the recovery row first; a `validate-run` advisory is named in one line and nothing else — it did not fail the run.
|
|
53
55
|
|
|
54
56
|
For every other task type:
|
|
55
57
|
|
|
@@ -57,7 +59,6 @@ For every other task type:
|
|
|
57
59
|
- Pointer `status: terminal` → the lifecycle ends here. Say the task is finished and quote the pointer's `rationale`. Do not propose a run and do not send the user to `/okstra-inspect`: the decision is already made, so there is nothing to inspect. If this run registered follow-up tasks, name them and the command that starts one.
|
|
58
60
|
- Pointer `status: blocked` → the pointer's `rationale` names what is in the way and what to run. Quote it and issue that command — `/okstra-user-response` for the `C-NNN` ids it lists, or `/okstra-run` for the phase it names.
|
|
59
61
|
- Pointer `status: pending` with a `rationale` → the user's own input is what comes next. Quote the `rationale` and issue the command it names. Do not re-run the phase that just completed, and do not send the user to `/okstra-inspect`: a finished phase has nothing to inspect, and re-running it discards the result the user is being asked to act on.
|
|
60
|
-
- Phase 7 `validate-run` failed → one line naming the blocking cause, then `/okstra-run` to re-run this phase. Only a failure matching the blocking allowlist (`okstra_ctl.blocking_checks`) reaches this row; every other finding was demoted to an advisory, printed as `validate-run: advisory — <finding>`, and the run passed. Name those advisories in one line and take the command from the matching pointer row above — an advisory is not a reason to re-run.
|
|
61
62
|
- Otherwise → `/okstra-inspect status` for this task.
|
|
62
63
|
|
|
63
64
|
Name every file in this reply as a markdown link — `[<what it is>](<path>)`, path inside the parentheses — so the user can open it. `reportPaths.markdown` in the `report-finalize` result already carries this run's report, report record, and team state that way. Commands stay in backticks.
|
|
@@ -6,6 +6,8 @@ This adapter maps the neutral Okstra lead operations to a cmux session, where Ok
|
|
|
6
6
|
|
|
7
7
|
It overrides only the worker-dispatch portion of the selected host relay, not the host or lead contract. Your own runtime still decides how you read files, ask the user, and record your session — this file only decides how workers are started, awaited, and reclaimed.
|
|
8
8
|
|
|
9
|
+
Keep the selected host relay's dispatch permission guidance when calling `okstra team dispatch`. For a restricted Codex lead, its `Permission at the dispatch boundary` guidance applies to every live dispatch, including reverify and critic jobs; selecting cmux does not grant execution privileges.
|
|
10
|
+
|
|
9
11
|
## Capability declaration
|
|
10
12
|
|
|
11
13
|
| Field | Value |
|
|
@@ -501,9 +501,11 @@ After persistence, reply briefly in the resolved Report Language. **Lead this re
|
|
|
501
501
|
|
|
502
502
|
Pick the next action from this table; the first matching row wins:
|
|
503
503
|
|
|
504
|
+
- Phase 7 failure with `recovery.mode: same-run` → continue correcting the owner inputs and execute `recovery.resumeCommand` in this run. Preserve user approvals and model choices. Read the report-writer recovery procedure before correcting its narrative. Do not close by asking the user to restart the phase for repairable artifact errors. If the only failure is `validate-run` and a blocked pointer targets an earlier phase, `recovery.mode: phase-reentry` preserves that target in `nextCommand`. Other validator findings are advisories, printed as `validate-run: advisory — <finding>`; follow the matching pointer row when the run passed.
|
|
505
|
+
|
|
504
506
|
- Open `blocks: approval` rows, or a `blocked-by-disagreement` gate whose approval rows carry no progress disposition (`okstra_ctl.run._blocking_gate_survives_user_decision`) → `/okstra-user-response` (name the `C-NNN` ids). Do not start implementation until those answers exist. An `accept-risk` / `select` / `answer` already recorded is not an open blocker. A report published with those rows open is not a failed run — the answers are the missing input, and a planning re-run without them reproduces the same block.
|
|
505
507
|
|
|
506
|
-
After `implementation-planning`, stop on the
|
|
508
|
+
After `implementation-planning`, once finalization succeeds, stop on the approval row above. No open approval blocker → `/okstra-run` → `implementation` (asks `approve_plan_confirm`) or `--approve`. Do not propose another planning run. Do not say `/okstra-inspect`. A blocking `validate-run` failure follows the recovery row first; a `validate-run` advisory is named in one line and nothing else — it did not fail the run.
|
|
507
509
|
|
|
508
510
|
For every other task type:
|
|
509
511
|
|
|
@@ -512,7 +514,6 @@ For every other task type:
|
|
|
512
514
|
- Pointer `status: terminal` and the result carries `nextInGroup` → the task is finished and the task-group has a task not yet started: say this task is finished, then close on `/okstra-run` for `nextInGroup.briefId` from its `brief` path — the group's start order is the brief ordinal, and `nextCommand.note` already names the task and its brief.
|
|
513
515
|
- Pointer `status: blocked` → the pointer's `rationale` names what is in the way and what to run. Quote it and issue that command — `/okstra-user-response` for the `C-NNN` ids it lists, or `/okstra-run` for the phase it names.
|
|
514
516
|
- Pointer `status: pending` with a `rationale` → the user's own input is what comes next. Quote the `rationale` and issue the command it names. Do not re-run the phase that just completed, and do not send the user to `/okstra-inspect`: a finished phase has nothing to inspect, and re-running it discards the result the user is being asked to act on.
|
|
515
|
-
- Phase 7 `validate-run` failed → one line naming the blocking cause, then `/okstra-run` to re-run this phase with the recorded sidecar. Only a failure matching the blocking allowlist (`okstra_ctl.blocking_checks`) reaches this row; every other finding was demoted to an advisory, printed as `validate-run: advisory — <finding>`, and the run passed. Name those advisories in one line and take the command from the matching pointer row above — an advisory is not a reason to re-run.
|
|
516
517
|
- Otherwise → `/okstra-inspect status` for this task.
|
|
517
518
|
|
|
518
519
|
When the host native picker is available and two of those rows could apply, ask with that picker (recommended first). Do not end the turn after the status dump.
|
|
@@ -49,7 +49,7 @@ Plan-body verification is configured under `convergence.planBodyVerification` in
|
|
|
49
49
|
| `enabled` | `true` | If `false`, the round is skipped and the approval gate is not blocked by this round (legacy behaviour). |
|
|
50
50
|
| `maxRounds` | `1` | Upper bound. Plan-body verification is consistency / completeness checking, not fact checking — additional rounds rarely help. Range 1–3. |
|
|
51
51
|
| `selfFixMaxRounds` | `1` | Fixed limit, not configurable: one automatic report-writer rewrite at most. Skip when nothing is fixable. `plan_items_cli._record_self_fixes`, `validate-run._validate_self_fix_grouping`, and session activity validation enforce the limit. |
|
|
52
|
-
| `gating` | `true` | If `true` (default), `majority-disagree` blocks approval. If `false`, the round is advisory-only and never blocks approval. Prepare emits `true` because the plan does not exist yet. After the report-writer draft, `okstra plan-items prepare` (and `seed`) flip it to `false` when `designPreparation.mode` is `no-design-inputs` and the Stage Map has exactly one row. That path keeps extraction and one verification round and does not run the self-fix loop or a sweep batch. Two-or-more stages, a PREP item, or non-empty `designPreparation.items` keep `gating=true`. `--no-plan-verification` is the separate manual opt-out (`enabled=false`). **Enforced:** `okstra_ctl.plan_items.advisory_plan_body_gating`, `validators/validate-run.py` `_validate_advisory_plan_body_gating`. "never blocks approval" is enforced by the `gating is False` early return in `_recompute_plan_body_gate`, `_gate_blocking_causes`, `_validate_plan_body_clarification_matching`, and `_validate_self_fix_before_clarification` — an advisory round neither demands a `blocks=approval` row nor an exhausted self-fix budget. |
|
|
52
|
+
| `gating` | `true` | If `true` (default), `majority-disagree` blocks approval. If `false`, the round is advisory-only and never blocks approval. Prepare emits `true` because the plan does not exist yet. After the report-writer draft, `okstra plan-items prepare` (and `seed`) flip it to `false` when `designPreparation.mode` is `no-design-inputs` and the Stage Map has exactly one row. That path keeps extraction and one analyser verification round and does not run the self-fix loop or a sweep batch. Critic corrections are exempt from the analyser round limit and need not concern an even split. Record their actual round numbers and preserve completed history; they are not planner rewrites. Two-or-more stages, a PREP item, or non-empty `designPreparation.items` keep `gating=true`. `--no-plan-verification` is the separate manual opt-out (`enabled=false`). **Enforced:** `okstra_ctl.plan_items.advisory_plan_body_gating`, `validators/validate-run.py` `_validate_advisory_plan_body_gating`. "never blocks approval" is enforced by the `gating is False` early return in `_recompute_plan_body_gate`, `_gate_blocking_causes`, `_validate_plan_body_clarification_matching`, and `_validate_self_fix_before_clarification` — an advisory round neither demands a `blocks=approval` row nor an exhausted self-fix budget. |
|
|
53
53
|
|
|
54
54
|
Default values are emitted into the manifest by `scripts/okstra_ctl/render.py` (`_build_convergence_block`). The ctx knob `OKSTRA_PLAN_VERIFICATION=false` flips `planBodyVerification.enabled` to false. `gating=false` is not that opt-out: extraction and one round still run.
|
|
55
55
|
|
|
@@ -325,9 +325,9 @@ When `config.adversarial == true` (the default for `implementation-planning`; se
|
|
|
325
325
|
- The burden of proof sits on the plan: an item earns `AGREE` only if the verifier actively tried to break it and could not.
|
|
326
326
|
- The verifier MUST open the file paths / symbols / commands the item cites and confirm they exist and are **defined** as written. This is the one allowed widening of the lightweight "judge from internal consistency and stated commands / paths" rule — confirming the existence of cited paths is not "re-analyzing the original requirements". The widening stops at *definition*: a build/test command's **execution success** is out of scope here, because the planning worktree has no dependencies installed (§"Planning-time environment gap"). Confirm the script is declared; do not treat its failure to run as evidence against the plan.
|
|
327
327
|
- If a cited path / command / validation signal cannot be confirmed, the verifier responds `DISAGREE(<kind>)` with the applicable breakage kind (a–f); uncertainty resolves toward DISAGREE, not AGREE.
|
|
328
|
-
- **Single-vote-blocking kinds.** A reproduced `DISAGREE(a)` (cited path/symbol mismatch) on any item other than a `P-Var-*` one, or a reproduced `DISAGREE(f)` on a `P-Req-*` item, blocks on that one vote
|
|
328
|
+
- **Single-vote-blocking kinds.** A reproduced `DISAGREE(a)` (cited path/symbol mismatch) on any item other than a `P-Var-*` one, or a reproduced `DISAGREE(f)` on a `P-Req-*` item, blocks on that one vote unless the assigned critic settles the analyser split with `AGREE` / `SUPPLEMENT`. The critic can correct the original factual objection; the original vote remains in the audit history. **Enforced:** `validators/validate-run.py` `_single_vote_block_survives`. Kinds `b` / `c` / `e` do not auto-block on one unreproduced vote, but a blocking-kind minority with ≥2 participating votes is still `majority-disagree` and goes to the user — the majority does not silently pass it. **Rollback ordering (`d`) never blocks.** Because `a` is reserved for a concrete contradiction between two spelled-out references, an abbreviated path is raised as `b`, never `a`. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate`.
|
|
329
329
|
|
|
330
|
-
Plan-body verification stays **lightweight** even under this posture — the `verificationMode = "full-reanalysis"` forcing in [convergence](./convergence.md) §"Adversarial Verification Mode" applies to finding convergence only (see §"Mode constraint"); the adversarial posture here only changes verifier behaviour, not the mode. This raises verification *quality* (active refutation, plan-side burden). A reproduced fact (`a`, or `f` on P-Req) still blocks on one confirmed vote. A blocking-kind minority (`b`/`c`/`e`) with ≥2 participating votes goes to the user rather than passing as `has-dissent`. Rollback ordering (`d`) is advisory and never blocks. A lone surviving `DISAGREE` whose peer returned a non-result does NOT block — a worker failure must not make the gate stricter than a healthy roster would.
|
|
330
|
+
Plan-body verification stays **lightweight** even under this posture — the `verificationMode = "full-reanalysis"` forcing in [convergence](./convergence.md) §"Adversarial Verification Mode" applies to finding convergence only (see §"Mode constraint"); the adversarial posture here only changes verifier behaviour, not the mode. This raises verification *quality* (active refutation, plan-side burden). A reproduced fact (`a`, or `f` on P-Req) still blocks on one confirmed vote until the critic corrects that judgement. A blocking-kind minority (`b`/`c`/`e`) with ≥2 participating votes goes to the user rather than passing as `has-dissent`. Rollback ordering (`d`) is advisory and never blocks. A lone surviving `DISAGREE` whose peer returned a non-result does NOT block — a worker failure must not make the gate stricter than a healthy roster would.
|
|
331
331
|
|
|
332
332
|
## Round protocol (single round at default `maxRounds=1`)
|
|
333
333
|
|
|
@@ -369,6 +369,8 @@ CLI-wrapper calls follow the planned execution surface after
|
|
|
369
369
|
consume only `modelExecutionValue`. A missing or invalid invocation contract blocks the
|
|
370
370
|
round before any host or provider process starts.
|
|
371
371
|
|
|
372
|
+
For contract 3.0, `prepare` checks the selected-direction draft with the same semantic validator as publication before writing the queue. Correct all reported references, coverage rows and planned commands through the writer correction procedure, then retry in this run. **Enforced:** `report_assembly.validate_plan_draft` and `plan_items_cli._prepare`; `tests/report/test_report_assembly.py` checks that invalid coverage creates no prepared queue.
|
|
373
|
+
|
|
372
374
|
1. Lead runs `okstra plan-items prepare --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`, places the fixed output of `okstra plan-items prompt --run-manifest <run-manifest>` verbatim in every verifier prompt, then runs `okstra plan-items validate-prepared --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`. After a self-fix rewrite, pass `--state <plan-body-verification.json>` on prepare and validate-prepared so the dispatch queue is the changed items plus their stage closure, not the full extract, and so each re-dispatched item carries the previous round's dissent (§"Re-verification rounds (round 2+)"). Python resolves the one convergence-owned state path from that run identity. Dispatch only after that exact-match validation succeeds. The prompt is the dispatch queue: `observed` / `deferred` stages are omitted; plan-wide items (`P-Dir-1`, `P-Var-*`, `P-Dep-*`, `P-Opt-*`) stay. Unscoped `P-Val-*` / `P-Req-*` / `P-Rb-*` stay on the first round, then only when their own `contentHash` changed — a neighbour rewrite must not resweep them. **Enforced:** `okstra_ctl.plan_items.dispatch_item_ids` / `reverify_item_ids`. After `okstra incremental-carry`, the carry copies unchanged checklist verdicts, rewrites `dispatchQueue`, and updates the sibling `plan-items-*.json` so the next `okstra plan-items prompt` does not re-score a carried row.
|
|
373
375
|
|
|
374
376
|
**Then seed the landing table (BLOCKING):** `okstra plan-items seed --narrative <report-writer-narrative.md> --state <plan-body-verification.json> --run-manifest <run-manifest.json>`. `apply-verdicts` in step 8 refuses a verdict whose item has no `planBodyVerification.planItems[]` row. The report writer never owns that state, so the deterministic seed is the only creator of its rows. The seed is idempotent by id and never touches existing verdicts, so it is safe to re-run between rounds and after a self-fix re-extraction. It does refresh `contentHash` from the current extract. Skipping it makes step 8 fail with `plan-body state has no row for [...]`.
|
|
@@ -387,9 +389,9 @@ round before any host or provider process starts.
|
|
|
387
389
|
- `full-consensus` — all participating analysers `AGREE` (SUPPLEMENT counts as agree on the item itself).
|
|
388
390
|
- `partial-consensus` — majority `AGREE` with two or more blocking `DISAGREE`s. On kinds `b` / `c` / `e` this is scored `majority-disagree` and **blocks approval** so the user decides; it is not folded into a passing gate.
|
|
389
391
|
- `dissent-isolated` — only one worker `DISAGREE`s, others `AGREE`. On a blocking kind (`b` / `c` / `e`, and kind `a` on `P-Var-*`) this is scored `majority-disagree` and **blocks approval**. Advisory-only `DISAGREE(d)` and `P-Rb-*` stay recorded dissent and do not block. (Distinct from finding-convergence `worker-unique`, which means the *opposite*: only one worker AGREEs.)
|
|
390
|
-
- `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes; rollback-ordering `DISAGREE(d)` votes are advisory and excluded from the tally), OR any blocking-kind dissent with ≥2 participating votes (a minority `DISAGREE` is not outvoted), OR
|
|
392
|
+
- `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes; rollback-ordering `DISAGREE(d)` votes are advisory and excluded from the tally), OR any blocking-kind dissent with ≥2 participating votes (a minority `DISAGREE` is not outvoted), OR an unresolved single-vote-blocking kind fires: one reproduced `DISAGREE(a)` on any item other than a `P-Var-*` one, or one reproduced `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks approval**. A valid critic correction is scored before these blocking rules, whether or not the analysers split evenly.
|
|
391
393
|
- `needs-reverify` — one of two shapes the round could not settle.
|
|
392
|
-
- **An even split on a blocking kind.**
|
|
394
|
+
- **An even split on a blocking kind.** A panel splitting evenly (1-AGREE / 1-DISAGREE, 2-2, …) needs a critic decision. An unresolved single-vote-blocking kind remains `majority-disagree`; other unresolved splits are `needs-reverify`. Do **not** re-run the original two. Dispatch `critic-worker` immediately on those items only (`okstra plan-items prepare --tie-vote`, then `okstra plan-items prompt`). The prompt carries the analyser split and no other plan items. Read the answer with `okstra plan-items collect-verdicts --items <the `--tie-vote` plan-items artifact> --result critic-worker=<path> --output <envelope>` — `--items` takes that artifact, whose `dispatchQueue` is the tie items, so pointing the next step at the raw result is refused against this round's full queue. Record the critic vote as `verdicts[].worker = critic-worker` with `okstra plan-items apply-verdicts --append --items <the `--tie-vote` plan-items artifact> --result critic-worker=<path>` — `--items` persists the exact partial `dispatchQueue` used by verdict validation and `complete-round`. Earlier verdicts and completed-round history outside that queue remain unchanged. If a previous version already saved the critic votes but left the full queue in state, recover with `okstra plan-items complete-round --state <state> --run-manifest <manifest> --round <N> --items <the tie-vote plan-items artifact>`. Do not fabricate new-round votes for already agreed items. Without it the result is checked against the whole persisted round queue and refused for every item the critic was never given (measured 2026-09-10: a 7-item tie round refused against 44 items), and the only way through was `--verdicts`, which the CLI's own help calls a historical envelope. Critic `AGREE` / `SUPPLEMENT` settles the split to `has-dissent`, including an earlier `DISAGREE(a)` or `DISAGREE(f)` on `P-Req-*`. This decision corrects the disputed judgement before single-vote blocking is evaluated; it does not delete the original dissent. Critic `DISAGREE` on a blocking kind is `majority-disagree`. **Enforced:** `validators/validate-run.py` `_validate_unresolved_tie_was_reverified` fails an in-scope item that carries an even split on a blocking kind and has neither a `critic-worker` vote nor a `blocks: approval` clarification row, `_classify_plan_item_gate` scores the tie shape and fails a settled classification the votes do not support, and `okstra_ctl.plan_items.next_dispatch` returns kind `critic-tie` for exactly these items, so the tie round is the queue the CLI hands you rather than one you assemble. **With no critic on the roster the split is a user decision, not another round.** `okstra plan-items next-dispatch --run-manifest <current-run-manifest.json>` answers `user-decision` (not `critic-tie`) when the run's `invocationAssignments` carries no `critic/*` entry, and its `itemIds` are the tie items. For each of them do what step 8 does for a surviving `majority-disagree`: `okstra approval-decision open` with `approvalContext.classification` set to `correctness-critical` when `_is_correctness_critical` is true, or `noncritical-dissent` otherwise, plus the matching `## 1. Clarification Items` row at `Blocks=approval`. Dispatch no further verification for those items. The `Blocks=approval` row is what withholds approval until the user disposes, exactly as for any other approval row; the gate retains unresolved single-vote blockers as `majority-disagree` and folds other `needs-reverify` items into `passed-with-dissent`, so the round closes on the gate it actually scored. A tie left with neither a critic vote nor a decision row surfaces as recorded dissent plus an `advisories[]` entry, not a round-blocking failure. **Enforced:** `okstra_ctl.plan_items.critic_is_rostered` reads the roster and `next_dispatch` returns the kind; `validators/validate-run.py` `_validate_unresolved_tie_was_reverified` reads a `blocks: approval` row linked to the item as the settlement and emits its advisory only when neither settlement is recorded.
|
|
393
395
|
- **A lone dissent nobody cross-verified** — a single-vote-blocking kind fired but the item has **fewer than 2 participating non-error votes**, i.e. the lone dissent was never cross-verified because its peer returned `verification-error`. A single-vote-blocking kind means "one *confirmed* DISAGREE is enough"; an unconfirmed one is not, and on a `P-Var-*` item none fires at all — its kind `a` never blocks on one vote and takes a majority like `b` / `e`. This does **not** block approval — blocking on it would make a worker failure produce a stricter gate than a healthy roster, the same paradox the ≥2-vote majority rule already rules out. The item is re-dispatched in the next round (step 7); if it survives the round budget it is promoted per step 8 with a Statement that says verification never completed. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` returns `needs-reverify` for this shape and `_recompute_plan_body_gate` folds it into `passed-with-dissent`.
|
|
394
396
|
- `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
|
|
395
397
|
5. Gate result resolution:
|
|
@@ -839,3 +841,5 @@ An item with no recorded vote carrying a round number gets no block, and an enve
|
|
|
839
841
|
**Enforced:** `okstra plan-items validate-prepared --state <same state>` re-derives the carry and exits 2 when the prepared envelope's `priorRounds` does not match, alongside the `items` / `dispatchQueue` comparison it already made. A prepared queue that dropped the dissent cannot pass the step-1 validation the dispatch is gated on.
|
|
840
842
|
|
|
841
843
|
The two spellings are different anchors for different artifacts: `**Prior round dissent**` is the block `prompt` puts in the prompt, `**Prior dissent**` is the line the worker puts in its result. `scripts/okstra_ctl/verdict_blocks.py` parses the result line into the verdict block when it is present and leaves it empty when it is not, so an omitted answer line is still silent — the prompt is what is now guaranteed, not the response.
|
|
844
|
+
|
|
845
|
+
The analyser round limit is enforced by `plan_items_cli._validate_advisory_round` at verdict application and completion. It does not limit critic correction rounds or require an even split. `apply-verdicts --append` can update the critic’s current verdict in a later round while preserving analyser votes and completed round history. Normal invocation identity and result-provenance checks still apply. `validators/validate-run.py` `_critic_gate_class` applies valid critic corrections before analyser voting rules. `tests/run/test_plan_items.py` covers second and third critic rounds on tied and majority-dissent items; `tests/contract/test_plan_body_verification.py` covers critic correction of both agreement and disagreement.
|
|
@@ -123,18 +123,21 @@ For historical schema-v1 Markdown only, the following heading table remains a re
|
|
|
123
123
|
|
|
124
124
|
**Enforced:** `okstra_ctl.report_finalize.V3_STEP_ORDER` is the order — `report-finalize` runs the steps from that tuple, so the sequence cannot be reordered by a caller. Running the steps by hand is what this rule forbids, and that path is not reachable through the CLI.
|
|
125
125
|
|
|
126
|
-
Do not run the
|
|
126
|
+
Do not run the nine steps below manually. Invoke `okstra report-finalize`; contract 3.0 runs them in this order:
|
|
127
127
|
|
|
128
128
|
1. **`token-usage`** — collect usage into team state without touching the final record.
|
|
129
129
|
2. **`project-activity`** — report assembly validates every owner input and publishes the final record once.
|
|
130
|
-
3. **`
|
|
131
|
-
4. **`
|
|
132
|
-
5. **`
|
|
133
|
-
6. **`
|
|
134
|
-
7. **`
|
|
135
|
-
8. **`
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
3. **`preflight`** — run the same schema and conformance checks used by final validation before translation, without writing run status. Structural failure skips translation; rendering and final validation still record the failure. `--only translate` also includes this check.
|
|
131
|
+
4. **`translate`** — for a non-English `reportLanguage`, materialize and dispatch the translator worker and require its `*.i18n.<lang>.json` sidecar; a no-op for English or when the sidecar already exists.
|
|
132
|
+
5. **`render-views`** — render the Markdown reading copy and human HTML, with the translation sidecar overlaid.
|
|
133
|
+
6. **`spawn-followups`** — materialize registered follow-up tasks.
|
|
134
|
+
7. **`validate-run`** — validate the record, views, run manifest, and team state.
|
|
135
|
+
8. **`record-group-memory`** — write this run's conclusion (headline, decisions, watch-outs, open follow-ups, record path, next phase) and the group's start order into the task-group's `group-context.md` okstra region, creating the file when the group has none; skipped, like teardown, when an earlier step failed. Sibling tasks read it as `## Task-Group Memory`.
|
|
136
|
+
9. **`teardown-stages`** — remove eligible stage worktrees after successful validation.
|
|
137
|
+
|
|
138
|
+
After `report-finalize` returns with `ok: true`, the lead closes the run with the launch prompt's User closeout. A generated HTML file alone does not establish successful validation.
|
|
139
|
+
|
|
140
|
+
When the result carries `recovery.mode: same-run`, continue the authorized work in this run. Read every `recovery.issues` entry and failed step output. Correct writer-owned fields through the correction ledger procedure above; correct orchestrator-owned inputs through their owning commands. If plan content changes, prepare and verify the affected items before finalizing. Then execute `recovery.resumeCommand`, which retains this run manifest and resumes from the earliest failure, including reassembly of contract 3.0 inputs. After a narrative correction, check an existing translation with `okstra report-translate check-data --run-manifest <manifest>` and regenerate it if its source no longer matches. Keep previous approvals, user decisions, model selections and completed evidence. Do not ask for another `/okstra-run` to repair references, coverage summaries, narrative syntax or command spelling. Ask only when an unresolved user decision or an actual external prerequisite prevents the correction. Do not edit an installed runtime or another repository while recovering a consuming project's task. **Enforced:** `report_finalize._finalize_recovery` returns the same-run command and complete owner issues, `closeout_command` suppresses the new-run closeout for that result, and `tests/report/test_report_finalize.py` checks recovery after a failed step. Execution of the correction remains the lead's responsibility.
|
|
138
141
|
|
|
139
142
|
### The translation sidecar: the `translate` step
|
|
140
143
|
|
|
@@ -9,12 +9,14 @@ at Phase 5, BEFORE constructing the verifier worker dispatch prompts.
|
|
|
9
9
|
|
|
10
10
|
- Every verdict comes from a fresh session with no shared context, never from the session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
|
|
11
11
|
- Session isolation is the primary self-review safeguard: each verifier is a separate invocation with its own context window. Reusing the executor's model is acceptable. The model comes from the run's stored assignment.
|
|
12
|
-
- Verifiers read from the SAME working tree path the Executor used so they observe the exact diff the Executor produced.
|
|
12
|
+
- Verifiers read from the SAME working tree path the Executor used so they observe the exact diff the Executor produced. Source files, lockfiles, Git state, and shared links remain read-only. Declared verification commands may create their normal worktree-local build/cache outputs and install dependencies with a frozen lockfile. This is the bounded exception to the preceding write restriction; it does not permit source repairs, moving shared links, or redirecting build outputs outside the worktree. Run-owned logs remain in the run artifact directories.
|
|
13
13
|
|
|
14
14
|
**Enforced:** `_validate_verifier_command_log_is_read_only` in `validators/validate-run.py` scans every `verifierResults[].readOnlyCommandLog` for source-mutating commands (`sed -i`, `git checkout --`/`restore`/`reset --hard`/`stash`/`clean`/`apply`, `patch -p`, `rm -rf`, `truncate`). Read-only forms (`git stash list`, `git clean --dry-run`, `git apply --check`) pass.
|
|
15
15
|
|
|
16
16
|
## Verifier QA duties (independent re-run mandate)
|
|
17
17
|
|
|
18
|
+
`okstra_ctl.qa_commands.find_unfrozen_installs` rejects unfrozen npm/pnpm/yarn/bun installs in both the plan preflight and the verifier command log. If an approved command conflicts with these restrictions or cannot run in the provisioned worktree, report the exact checklist row and route to `implementation-planning`; do not substitute a command during verification.
|
|
19
|
+
|
|
18
20
|
Every verifier acts as a QA gate, not just a diff reviewer. Trusting the executor's reported evidence is forbidden — verifiers MUST reproduce it themselves from the same worktree path the executor used.
|
|
19
21
|
|
|
20
22
|
**Enforced:** `_validate_verifier_reran_independently` in `validators/validate-run.py` fails an empty `independentValidationRerun`. What you wrote in it is not pattern-matched — the wording check that used to reject phrasings citing the executor is gone, because it fired on reports that had reproduced the run and merely said so. Write the command you ran and its outcome; the Discrepancy rule below is what makes you name the divergence.
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
],
|
|
48
48
|
"implementation-planning": [
|
|
49
49
|
"source code edits of any kind (Edit/Write on project source files is forbidden)",
|
|
50
|
-
"file writes outside the run`s artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`)
|
|
50
|
+
"file writes outside the run`s artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`), including task-root QA scripts, manifest, and tsconfig (planning declares conformance commands and required dependencies; implementation writes these files); in particular, do not write to `docs/superpowers/specs/` or `docs/superpowers/plans/`",
|
|
51
51
|
"executing builds, migrations, deployments, or any state-mutating command",
|
|
52
52
|
"starting `implementation` inside this run (must be a separate run authorised by an approved deliverable from this phase), even if the user says \"다음 단계 진행해\"",
|
|
53
53
|
"dispatching parallel sub-agents beyond the required worker roster (okstra owns worker fan-out)",
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Implementation Planning Profile
|
|
2
2
|
|
|
3
|
+
Validation commands are executable inputs: preserve their newlines, quotes, and code bodies. Use frozen dependency installation (`npm ci`, `pnpm install --frozen-lockfile`, or the package manager's equivalent). Name QA scripts with absolute task-artifact paths when a command runs from a stage worktree; `.okstra/tasks/...` relative to that worktree does not point to the project task. `stage_validation_executability_errors` enforces command restrictions at planning validation, implementation entry, and report preflight, including advisory plan-body runs.
|
|
4
|
+
|
|
5
|
+
Plan for the actual worktree layout before approval. Shared documentation directories can be links to the main checkout. Choose a build command compatible with those links (for example, an installed Next.js version may provide `next build --webpack`); verify the available option rather than assuming it. Do not plan for a verifier to move links or repair its environment. Compare negative-case assertions with the brief and the script body: a requirement to cache existing assets does not establish that missing assets should be cached. Record any changed expectation in a new plan revision; preserve the earlier approved plan.
|
|
6
|
+
|
|
3
7
|
```yaml
|
|
4
8
|
roles:
|
|
5
9
|
- role: planner
|
|
@@ -185,7 +189,7 @@ roles:
|
|
|
185
189
|
Layout split (the implementer writes these, not this phase): executable scripts (conformance + any real-IO test) live under `<task_root>/qa/scripts/`; data sidecars (`conformance-manifest.json`, `result-*.json`) stay at the `qa/` root. This declaration is enforced at four layers: `validators/validate-implementation-plan-stages.py` check **S11** forces every stage to carry one of the two lines; at the planning boundary `validators/validate-run.py` `_validate_planning_conformance_declared` accepts a well-formed `Conformance tests:` line even when the script file and manifest entry are absent (malformed `requires` still fails); the matching `implementation` stage run that inherited `Conformance tests:` fails closed when the script file is missing (`_validate_conformance`); and the manifest JSON structure — including each entry's `script` living under `qa/scripts/` and a `runCommand` that does not change cwd — is enforced by `validate_conformance_manifest` when the implementer writes the entry.
|
|
186
190
|
- `### Stage Exit Contract` — predicted added/modified files, newly exposed identifiers/types/endpoints, downstream-usable resources.
|
|
187
191
|
- `### Stage Validation` — pre / mid / post exact commands or observable outcomes for this stage only.
|
|
188
|
-
- **Run-executable only (BLOCKING).** A `validationChecklist` row that carries `stageRefs` gates that stage's carry, so it MUST be executable by the implementation run itself — no deployment, no manual walk-through, no observation "by hand", no credentials the run does not hold. The implementation phase forbids deploys and holds no deploy credentials, so such a row can never pass and blocks a stage with zero code defects (observed: dev-10341 VC-013/VC-014). Manual or deployed-environment verification belongs in the brief's `External Gates` and final-verification's user-owned external QA — record it there without `stageRefs`. **Enforced:** `okstra_ctl.implementation_direction.validate_selected_direction_plan` (`
|
|
192
|
+
- **Run-executable only (BLOCKING).** A `validationChecklist` row that carries `stageRefs` gates that stage's carry, so it MUST be executable by the implementation run itself — no deployment, no manual walk-through, no observation "by hand", no credentials the run does not hold. The implementation phase forbids deploys and holds no deploy credentials, so such a row can never pass and blocks a stage with zero code defects (observed: dev-10341 VC-013/VC-014). Manual or deployed-environment verification belongs in the brief's `External Gates` and final-verification's user-owned external QA — record it there without `stageRefs`. **Enforced:** `okstra_ctl.implementation_direction.validate_selected_direction_plan` (`stage_validation_executability_errors`) rejects stage-gating rows whose observation text requires a manual or deployed-environment step.
|
|
189
193
|
- **Dependency precondition (stages that run the project toolchain).** The planning worktree is created without installed dependencies, so a stage whose steps call `npm` / `yarn` / `pytest` / `cargo` / equivalent cannot have those commands succeed at plan time — they exit `127`, not RED/GREEN. Declare the install **once** as a `phase: pre` row in `### Validation Checklist` (e.g. `VC-008 — the implementation run's stage worktree has workspace dependencies installed`) and have every such stage's `Stage Validation` cite that `VC-NNN` in its `pre:` line. Do not repeat the install commands per stage, and do not silently assume the tooling is present: a plan that never states the precondition produces steps whose commands never resolve, which the §5.5.9 round then reports as unverifiable. **Enforced (advisory):** `validators/validate-run.py` `_detect_missing_dependency_precondition` warns when a toolchain-invoking stage cites no `VC-NNN`, or cites one that is not `phase: pre`. Whether the cited row genuinely covers dependencies is a §5.5.9 judgement, not a machine check. Detection uses the token allowlist in `scripts/okstra_ctl/build_tools.py`; a project overrides it with `buildToolTokens` in `.okstra/project.json`.
|
|
190
194
|
- **Vertical-slice-first partition rule (1st-class):** the grouping anchor is a **thin end-to-end vertical slice** — one stage delivers a single user-observable increment, crossing whatever layers are needed (data → service → API → UI) to make that one increment work. File/module proximity is demoted to the **intra-slice grouping rule**: within a slice, keep steps touching the same file/directory/module together so the diff, PR, and rollback unit stay cohesive. **Horizontal layer-splitting is forbidden** — never carve "the DB layer" into one stage and "the service layer" into the next; that produces stages that ship no standalone user value. A stage is split ONLY when (a) a real `depends-on` data/contract dependency exists, (b) effective steps would exceed 8, or (c) it is a distinct vertical slice (a different user-value increment). Maximising the number of parallel stages is NOT a reason to split — parallelism is an emergent property of independent stages, never a partitioning goal. **Config propagation is not a slice (BLOCKING):** a dependent stage whose planned paths are all configuration/CI files (`Dockerfile`, `docker-compose*`, `*.yml` / `*.yaml`, `.env*`) merely propagates a value another stage introduced — it ships no user-value increment of its own. Fold that work into the introducing stage as its own step(s) and separate it by **commits**, never by stages: each extra stage costs a full implementation run (executor + verifiers, 30min+ observed) for a few config lines. A first stage with no dependency is exempt — a task whose whole scope is configuration is legitimate. **Enforced:** `okstra_ctl.implementation_direction.validate_selected_direction_plan` (`_micro_stage_fold_errors`).
|
|
191
195
|
- **Project-boundary partition rule (hard boundary):** a *project* boundary is either (a) a different repository / `PROJECT_ROOT`, or (b) a different top-level independently-deployable module within one repo. A stage maps to a single worktree on one repo/branch, so **no stage may contain edits belonging to more than one project** — this is a hard split that overrides the ≤8-step merging allowance; never co-locate two projects' changes in one stage to save a stage. Two cases:
|
|
@@ -178,6 +178,14 @@ Display the question only through the selected tool. Do not print it or its opti
|
|
|
178
178
|
|
|
179
179
|
## Codex dispatch details
|
|
180
180
|
|
|
181
|
+
### Permission at the dispatch boundary
|
|
182
|
+
|
|
183
|
+
Before a live CLI worker dispatch, check the current host's declared sandbox and approval policy. In a restricted Codex session, request the host's supported execution permission on the dispatch command itself (`sandbox_permissions: "require_escalated"` for `exec_command` when that mechanism is available and permitted). Scope the request to the prepared run and explain that it starts the selected worker processes. Follow the host's approval decision; a wizard confirmation or a successful preflight does not grant this permission.
|
|
184
|
+
|
|
185
|
+
Apply this guidance to both `okstra worker-dispatch` and the cmux override's `okstra team dispatch`, including initial workers, reverify, critic, report-writer, and retries. Read-only previews do not start workers. A session already authorized to execute outside the sandbox does not need another request. When the host prohibits escalation, preserve the pending invocation and report the execution constraint instead of trying an alternate launcher or weakening host controls.
|
|
186
|
+
|
|
187
|
+
The worker's `--sandbox danger-full-access` flag only selects the child Codex policy; it does not remove restrictions inherited from the parent process. If startup reports `Operation not permitted`, retain the exact error and distinguish initialization failure from a worker verdict. Retry only through the permitted host mechanism after the execution conditions change, within the existing retry limit; do not repeat the same restricted invocation. This is host-call guidance, not a permission grant or a runtime-enforced check.
|
|
188
|
+
|
|
181
189
|
- For convergence reverify, consume the persisted round plan exactly. This adapter may map and transport each returned batch, but it cannot change batch membership and does not classify findings or branch on task type, provider, or model identity.
|
|
182
190
|
- Do not invoke Claude Code team or subagent tools.
|
|
183
191
|
- The prepared run manifest and team-state are the dispatch authority. A `runner=native-session` assignment stays in the current Codex host; a `runner=cli-wrapper` assignment uses the registered provider wrapper. Unsupported explicitly requested workers fail; an adapter must not silently change the roster.
|
|
@@ -12,6 +12,7 @@ import sys
|
|
|
12
12
|
|
|
13
13
|
from ..invocation import AgentInvocationError
|
|
14
14
|
from ...worker_prompt_headers import WorkerPromptHeaderError
|
|
15
|
+
from ...error_log_write import record_runtime_failure
|
|
15
16
|
from ...dispatch_state import BACKEND_CLI_WRAPPER, BACKEND_CMUX_PANE, DispatchError
|
|
16
17
|
from .emit import _emit, _prepared_payload
|
|
17
18
|
from .inputs import AgentPromptCliError
|
|
@@ -295,4 +296,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
295
296
|
ValueError,
|
|
296
297
|
) as exc:
|
|
297
298
|
print(f"error: {exc}", file=sys.stderr)
|
|
299
|
+
manifest = getattr(args, "run_manifest", None)
|
|
300
|
+
if manifest:
|
|
301
|
+
root = Path(args.project_root).resolve()
|
|
302
|
+
logged = record_runtime_failure(
|
|
303
|
+
root / manifest, project_root=root,
|
|
304
|
+
command=f"agent-prompt {args.command}", exit_code=2, detail=str(exc),
|
|
305
|
+
)
|
|
306
|
+
if not logged["ok"]:
|
|
307
|
+
print(f"error-log: {logged['reason']}", file=sys.stderr)
|
|
298
308
|
return 2
|
|
@@ -28,6 +28,7 @@ from ...convergence_store import (
|
|
|
28
28
|
from ...worker_prompt_policy import (
|
|
29
29
|
CRITIC_VERIFY_DISPATCH_KIND,
|
|
30
30
|
PLAN_VERIFY_DISPATCH_KIND_PREFIX,
|
|
31
|
+
is_plan_critic_verification,
|
|
31
32
|
verification_dispatch_round,
|
|
32
33
|
)
|
|
33
34
|
from .inputs import AgentPromptCliError
|
|
@@ -81,7 +82,10 @@ def _dynamic_verifier_source(
|
|
|
81
82
|
and manifest.get("executionIdentityVersion") == 2
|
|
82
83
|
)
|
|
83
84
|
is_reverify = (
|
|
84
|
-
args.assignment_ref.startswith("reverify/")
|
|
85
|
+
(args.assignment_ref.startswith("reverify/") or is_plan_critic_verification(
|
|
86
|
+
task_type=str(manifest.get("taskType", "")),
|
|
87
|
+
assignment_ref=args.assignment_ref, dispatch_kind=args.dispatch_kind,
|
|
88
|
+
))
|
|
85
89
|
and args.audience == "reverification-worker"
|
|
86
90
|
)
|
|
87
91
|
source = args.source_role_execution_ref
|
|
@@ -10,6 +10,7 @@ from typing import Any, Mapping
|
|
|
10
10
|
|
|
11
11
|
from ...worker_prompt_policy import (
|
|
12
12
|
CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT,
|
|
13
|
+
is_plan_critic_verification,
|
|
13
14
|
resolve_prompt_plan_for_manifest,
|
|
14
15
|
)
|
|
15
16
|
from .inputs import AgentPromptCliError
|
|
@@ -35,7 +36,12 @@ def _validate_run_identity(
|
|
|
35
36
|
elif assignment_ref.startswith("critic/"):
|
|
36
37
|
scope = assignment_ref.split("/", 1)[1]
|
|
37
38
|
expected = CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT.get(scope, "")
|
|
38
|
-
if
|
|
39
|
+
if is_plan_critic_verification(
|
|
40
|
+
task_type=str(manifest.get("taskType", "")),
|
|
41
|
+
assignment_ref=assignment_ref, dispatch_kind=dispatch_kind,
|
|
42
|
+
):
|
|
43
|
+
expected = "reverification-worker"
|
|
44
|
+
elif not expected or dispatch_kind != "critic":
|
|
39
45
|
raise AgentPromptCliError("critic assignment identity is invalid")
|
|
40
46
|
# provider 대조는 여기 두지 않는다. critic 의 `worker_id` 는 배정 참조의
|
|
41
47
|
# 마지막 마디(`scope` / `acceptance`)이고 provider 이름이 아니다
|
|
@@ -139,7 +139,7 @@ _BLOCKING: tuple[tuple[str, str], ...] = (
|
|
|
139
139
|
"(scripts/okstra_ctl/implementation_stage.py:113) 옛 요청으로 진행한다.",
|
|
140
140
|
),
|
|
141
141
|
# selected-direction 계획의 두 조각. 주 집행은 조립 게시 게이트
|
|
142
|
-
# (report_assembly `
|
|
142
|
+
# (report_assembly `selected_direction_plan_errors`)이고, 여기는
|
|
143
143
|
# 게시 후 변조·우회 경로의 백스톱이다. dev-10341 실측: 이 접두 17건이
|
|
144
144
|
# advisory 로 통과 발행된 계획을 구현 진입이 그대로 하드 거부해 run 이
|
|
145
145
|
# wedge 됐다.
|
|
@@ -61,6 +61,13 @@ def normalize_conformance_script(
|
|
|
61
61
|
value = script[len(prefix):] if script.startswith(prefix) else script
|
|
62
62
|
while value.startswith("./"):
|
|
63
63
|
value = value[2:]
|
|
64
|
+
if task_root is not None and value.startswith(".okstra/"):
|
|
65
|
+
for parent in task_root.resolve().parents:
|
|
66
|
+
if parent.name == ".okstra" and task_root.resolve().is_relative_to(
|
|
67
|
+
parent / "tasks"
|
|
68
|
+
):
|
|
69
|
+
value = str((parent.parent / value).resolve())
|
|
70
|
+
break
|
|
64
71
|
if task_root is not None and value.startswith("/"):
|
|
65
72
|
try:
|
|
66
73
|
return Path(value).resolve().relative_to(task_root.resolve()).as_posix()
|
|
@@ -17,6 +17,7 @@ from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_obj
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
DYNAMIC_VERIFIER_SOURCE_ROLES = frozenset({
|
|
20
|
+
"critic",
|
|
20
21
|
"analyser",
|
|
21
22
|
"designer",
|
|
22
23
|
"planner",
|
|
@@ -163,12 +164,14 @@ def reserve_dynamic_verifier(
|
|
|
163
164
|
manifest = read_execution_manifest(manifest_path)
|
|
164
165
|
if manifest.legacy:
|
|
165
166
|
raise ExecutionManifestError("cannot reserve a dynamic verifier in v1 data")
|
|
167
|
+
authority = load_owned_json_object(manifest_path)
|
|
166
168
|
_validate_dynamic_verifier_request(
|
|
167
169
|
manifest.role_executions,
|
|
168
170
|
source_role_execution_ref,
|
|
169
171
|
duty_id,
|
|
172
|
+
task_type=str(authority.get("taskType", "")),
|
|
173
|
+
dispatch_kind=dispatch_kind or f"reverify-r{round_number}",
|
|
170
174
|
)
|
|
171
|
-
authority = load_owned_json_object(manifest_path)
|
|
172
175
|
if authority.get("taskKey") not in (None, task_key):
|
|
173
176
|
raise ExecutionManifestError("dynamic verifier taskKey does not match manifest")
|
|
174
177
|
project_root = _manifest_project_root(authority, manifest_path)
|
|
@@ -219,7 +222,12 @@ def _validate_dynamic_verifier_request(
|
|
|
219
222
|
roles: tuple[RoleExecution, ...],
|
|
220
223
|
source_role_execution_ref: str,
|
|
221
224
|
duty_id: str,
|
|
225
|
+
*,
|
|
226
|
+
task_type: str,
|
|
227
|
+
dispatch_kind: str,
|
|
222
228
|
) -> None:
|
|
229
|
+
from .worker_prompt_policy import is_plan_verify_dispatch_kind
|
|
230
|
+
|
|
223
231
|
try:
|
|
224
232
|
duty_role = role_for_duty(duty_id)
|
|
225
233
|
except RoleCatalogError as exc:
|
|
@@ -238,6 +246,13 @@ def _validate_dynamic_verifier_request(
|
|
|
238
246
|
raise ExecutionManifestError(
|
|
239
247
|
f"dynamic verifier source role is not eligible: {source.role}"
|
|
240
248
|
)
|
|
249
|
+
if source is not None and source.role == "critic" and not (
|
|
250
|
+
task_type == "implementation-planning"
|
|
251
|
+
and is_plan_verify_dispatch_kind(dispatch_kind)
|
|
252
|
+
):
|
|
253
|
+
raise ExecutionManifestError(
|
|
254
|
+
"critic source role is eligible only for planning verification"
|
|
255
|
+
)
|
|
241
256
|
|
|
242
257
|
|
|
243
258
|
def _manifest_project_root(
|
|
@@ -31,7 +31,7 @@ def parse_records(paths: list[Path]) -> tuple[list[dict], int]:
|
|
|
31
31
|
except (OSError, UnicodeDecodeError):
|
|
32
32
|
skipped += 1
|
|
33
33
|
continue
|
|
34
|
-
for line in text.splitlines():
|
|
34
|
+
for line_number, line in enumerate(text.splitlines(), 1):
|
|
35
35
|
line = line.strip()
|
|
36
36
|
if not line:
|
|
37
37
|
continue
|
|
@@ -42,6 +42,7 @@ def parse_records(paths: list[Path]) -> tuple[list[dict], int]:
|
|
|
42
42
|
continue
|
|
43
43
|
if isinstance(rec, dict):
|
|
44
44
|
rec["_sourceLog"] = str(path)
|
|
45
|
+
rec["_sourceLine"] = line_number
|
|
45
46
|
records.append(rec)
|
|
46
47
|
else:
|
|
47
48
|
skipped += 1
|
|
@@ -276,6 +276,42 @@ def append_observed(
|
|
|
276
276
|
return rec
|
|
277
277
|
|
|
278
278
|
|
|
279
|
+
def record_runtime_failure(
|
|
280
|
+
run_manifest_path: Path, *, command: str, exit_code: int, detail: str,
|
|
281
|
+
project_root: Path | None = None,
|
|
282
|
+
) -> dict:
|
|
283
|
+
"""실패를 감지한 실행 도구가 기록한다. 기록 실패는 원래 오류를 가리지 않는다."""
|
|
284
|
+
from .json_boundary import load_owned_object
|
|
285
|
+
from .worker_prompt_headers import WorkerPromptHeaderError, resolve_errors_log_path
|
|
286
|
+
|
|
287
|
+
result = {"ok": False, "path": "", "reason": ""}
|
|
288
|
+
try:
|
|
289
|
+
manifest = load_owned_object(run_manifest_path, artifact="run manifest")
|
|
290
|
+
root = project_root or Path(manifest["projectRoot"])
|
|
291
|
+
context_path = manifest.get("activeRunContextPath")
|
|
292
|
+
context = {}
|
|
293
|
+
if context_path:
|
|
294
|
+
path = root / context_path
|
|
295
|
+
context = load_owned_object(path, artifact="active run context")
|
|
296
|
+
out = resolve_errors_log_path(root, manifest, context).resolve()
|
|
297
|
+
out.relative_to((root / ".okstra").resolve())
|
|
298
|
+
result["path"] = str(out)
|
|
299
|
+
timestamp = _iso(_now_utc())
|
|
300
|
+
append_jsonl_line(out, {
|
|
301
|
+
"ts": timestamp, "recordedAt": timestamp,
|
|
302
|
+
"taskKey": manifest.get("taskKey"), "phase": manifest.get("taskType"),
|
|
303
|
+
"agent": "okstra", "agentRole": "runtime", "model": None,
|
|
304
|
+
"source": "runtime-observed", "errorType": "tool-failure",
|
|
305
|
+
"command": command, "commandKind": "runtime", "exitCode": exit_code,
|
|
306
|
+
"message": truncate_stderr(detail), "stderrExcerpt": None,
|
|
307
|
+
})
|
|
308
|
+
except (OSError, ValueError, TypeError, KeyError, WorkerPromptHeaderError) as exc:
|
|
309
|
+
result["reason"] = f"{type(exc).__name__}: {exc}"
|
|
310
|
+
return result
|
|
311
|
+
result["ok"] = True
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
|
|
279
315
|
def _worker_sidecar_record(
|
|
280
316
|
entry, *, recorded_at, task_key, agent, agent_role, model, identity, out_path=None,
|
|
281
317
|
):
|
|
@@ -100,12 +100,31 @@ def prior_run_error_digest(
|
|
|
100
100
|
)
|
|
101
101
|
],
|
|
102
102
|
)
|
|
103
|
+
examples: dict[tuple[str, str], str] = {}
|
|
104
|
+
for record in carried:
|
|
105
|
+
message = str(record.get("message") or "")[:1200]
|
|
106
|
+
command = str(record.get("command") or "")[:800]
|
|
107
|
+
if message or command:
|
|
108
|
+
examples[(message, command)] = (
|
|
109
|
+
f"{record['_sourceLog']}:{record['_sourceLine']}"
|
|
110
|
+
)
|
|
111
|
+
details = (
|
|
112
|
+
_md_table(
|
|
113
|
+
["Message", "Command", "Source"],
|
|
114
|
+
[
|
|
115
|
+
[message, command, source]
|
|
116
|
+
for (message, command), source in list(examples.items())[-20:]
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
if examples
|
|
120
|
+
else ""
|
|
121
|
+
)
|
|
103
122
|
return (
|
|
104
123
|
"# Prior-Run Errors\n\n"
|
|
105
124
|
"`contract-violation` and `tool-failure` records this task's earlier "
|
|
106
125
|
"runs wrote. These are not findings about the work — they are traps "
|
|
107
126
|
"that fired before and can fire again in this run.\n\n"
|
|
108
|
-
f"{table}\n"
|
|
127
|
+
f"{table}\n\n{details}\n"
|
|
109
128
|
)
|
|
110
129
|
|
|
111
130
|
|