okstra 0.159.0 → 0.161.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/architecture/storage-model.md +2 -0
- package/docs/architecture.md +2 -1
- package/docs/cli.md +8 -3
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +3 -0
- package/docs/for-ai/skills/okstra-run.md +2 -1
- package/docs/for-ai/skills/okstra-user-response.md +5 -5
- package/docs/project-structure-overview.md +5 -1
- package/docs/task-process/implementation.md +28 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/okstra-claude-exec.sh +4 -1
- package/runtime/prompts/host-orchestration/README.md +18 -0
- package/runtime/prompts/host-orchestration/implementation.md +57 -0
- package/runtime/prompts/launch.template.md +10 -1
- package/runtime/prompts/lead/adapters/claude-code.md +1 -1
- package/runtime/prompts/lead/adapters/cmux.md +67 -0
- package/runtime/prompts/lead/context-loader.md +5 -2
- package/runtime/prompts/lead/convergence.md +3 -1
- package/runtime/prompts/lead/plan-body-verification.md +21 -2
- package/runtime/prompts/lead/team-contract.md +2 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +11 -1
- package/runtime/prompts/profiles/_common-contract.md +3 -1
- package/runtime/prompts/profiles/implementation-planning.md +2 -0
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +3 -0
- package/runtime/python/okstra_ctl/clarification_items.py +9 -0
- package/runtime/python/okstra_ctl/cmux.py +531 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +6 -6
- package/runtime/python/okstra_ctl/convergence.py +168 -11
- package/runtime/python/okstra_ctl/dispatch_core.py +76 -7
- package/runtime/python/okstra_ctl/dispatch_state.py +16 -0
- package/runtime/python/okstra_ctl/error_issue.py +640 -0
- package/runtime/python/okstra_ctl/error_report.py +56 -0
- package/runtime/python/okstra_ctl/error_zip.py +23 -10
- package/runtime/python/okstra_ctl/incremental_scope.py +159 -19
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +18 -5
- package/runtime/python/okstra_ctl/issue_signals.py +186 -0
- package/runtime/python/okstra_ctl/lead_runtime.py +30 -2
- package/runtime/python/okstra_ctl/paths.py +38 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +167 -3
- package/runtime/python/okstra_ctl/profile_show.py +134 -0
- package/runtime/python/okstra_ctl/recap.py +63 -0
- package/runtime/python/okstra_ctl/render.py +7 -2
- package/runtime/python/okstra_ctl/render_final_report.py +7 -22
- package/runtime/python/okstra_ctl/report_translation.py +4 -0
- package/runtime/python/okstra_ctl/report_views.py +7 -3
- package/runtime/python/okstra_ctl/run.py +54 -3
- package/runtime/python/okstra_ctl/run_audit.py +477 -0
- package/runtime/python/okstra_ctl/team.py +50 -11
- package/runtime/python/okstra_ctl/user_response.py +25 -10
- package/runtime/python/okstra_ctl/verdict_blocks.py +183 -0
- package/runtime/python/okstra_ctl/wizard.py +64 -10
- package/runtime/python/okstra_ctl/worker_audit_check.py +44 -0
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +207 -0
- package/runtime/python/okstra_ctl/worker_heartbeat.py +9 -3
- package/runtime/python/okstra_ctl/worker_liveness.py +81 -9
- package/runtime/schemas/final-report-v1.0.schema.json +14 -0
- package/runtime/schemas/final-report-v2.0.schema.json +51 -1
- package/runtime/skills/okstra-inspect/SKILL.md +3 -1
- package/runtime/skills/okstra-inspect/facets/error-issue.md +77 -0
- package/runtime/skills/okstra-inspect/facets/run-audit.md +34 -0
- package/runtime/skills/okstra-run/SKILL.md +28 -10
- package/runtime/skills/okstra-user-response/SKILL.md +18 -18
- package/runtime/templates/reports/final-report.template.md +4 -0
- package/runtime/templates/reports/html/i18n/en.json +5 -1
- package/runtime/templates/reports/html/i18n/ko.json +5 -1
- package/runtime/templates/reports/html/macros/forms.html +15 -0
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +1 -0
- package/runtime/templates/reports/i18n/en.json +2 -0
- package/runtime/validators/validate-run.py +267 -208
- package/runtime/validators/validate-workflow.sh +6 -0
- package/runtime/validators/validate_session_conformance.py +135 -31
- package/src/cli-registry.mjs +34 -0
- package/src/commands/execute/incremental-scope.mjs +10 -0
- package/src/commands/execute/worker-audit-check.mjs +35 -0
- package/src/commands/inspect/error-issue.mjs +27 -0
- package/src/commands/inspect/profile-show.mjs +29 -0
- package/src/commands/inspect/run-audit.mjs +26 -0
|
@@ -409,7 +409,9 @@ re-read the task brief, instruction-set, or report template.
|
|
|
409
409
|
...
|
|
410
410
|
```
|
|
411
411
|
|
|
412
|
-
When persisting votes, map SURVIVES→`agree`, SURVIVES-WITH-CAVEAT→`supplement`, REFUTED→`disagree`, and UNVERIFIABLE→`
|
|
412
|
+
When persisting votes, map SURVIVES→`agree`, SURVIVES-WITH-CAVEAT→`supplement`, REFUTED→`disagree`, and UNVERIFIABLE→`unverifiable`; copy the stated Basis into `votes.<worker>.disagreeBasis` (null for non-REFUTED verdicts). Every vote requires a non-empty `explanation`.
|
|
413
|
+
|
|
414
|
+
UNVERIFIABLE is **not** `verification-error`. A verifier that opened the evidence and could not check it participated in the round; a `verification-error` is a verifier that failed to answer. The classifier counts only non-error votes, so folding the two shrinks the participating roster without saying so — a round where one analyser answers UNVERIFIABLE throughout would read as a two-way cross-check while reporting three voters. **Enforced:** `okstra convergence collect-results` applies this mapping (`okstra_ctl.verdict_blocks.ADVERSARIAL_VERDICTS`); do not transcribe votes by hand.
|
|
413
415
|
|
|
414
416
|
### Full Re-analysis Re-verification Prompt
|
|
415
417
|
|
|
@@ -204,13 +204,19 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
204
204
|
**Record the cause, not just the outcome.** The gate value names the outcome; `planBodyVerification.gateBlockedBy` (array) names every input that blocked it — `majority-disagree`, `coverage-gap`, `non-result`. Two independent inputs can block: a `majority-disagree` plan item, and a Requirement Coverage `gap` / `blocked C-NNN` row (`prompts/profiles/implementation-planning.md` §"Requirement Coverage"). A coverage-only block still renders as `blocked-by-disagreement` because that is the only blocking non-abort value, so **without `gateBlockedBy` the report asserts a worker disagreement that never happened** and the reader hunts for a dissent that does not exist. Leave the array empty for a passing gate. **Enforced:** `validators/validate-run.py` `_validate_gate_blocked_by` cross-checks the declared causes against the recorded verdicts and coverage rows, and fails a passing gate that has a blocking coverage row — the coverage rule was prose-only before.
|
|
205
205
|
|
|
206
206
|
**A coverage row citing this run's own `C-NNN` is not an independent blocker.** When a coverage row's `blocked C-NNN` points at a clarification that step 8 below promoted from a `majority-disagree` item in *this same run*, that blocker is already counted once as the plan item. Counting it again as a coverage gap makes the run block on a clarification it just authored, and the row carries into the next run as a fresh blocker — the Requirement Coverage ↔ Clarification cycle. Such rows are excluded from `coverage-gap`. **Enforced:** `validators/validate-run.py` `_independent_coverage_blockers`.
|
|
207
|
-
6. Lead records `planBodyVerification.participatingAnalysers` as `{rostered, voting}` — how many analysers the roster carried, and how many actually returned a non-error vote. The gate arithmetic is unchanged, but a shrunken roster loosens it silently: with two participating analysers a 1-AGREE / 1-DISAGREE split is a tie, so it never reaches `majority-disagree` and the dissent passes as `dissent-isolated`. A reader comparing two runs' gate values cannot see that without this pair. **Enforced:** `validators/validate-run.py` `_validate_participating_analysers` recomputes `voting` from the recorded verdicts and fails a declared figure the table denies. That pair still counts only *whether* a worker voted: an analyser that answers the same verdict to every item is carried in `voting` as a third opinion while contributing no refutation signal, so the gate reads as a three-way cross-check backed by two. **Enforced (advisory):** `validators/validate-run.py` `_detect_uniform_verifier` reports any worker whose every vote in the round was one verdict, with its item count — it does not fail the run, because a unanimous round is also a legitimate outcome and no ratio separates the two reliably.
|
|
207
|
+
6. Lead records `planBodyVerification.participatingAnalysers` as `{rostered, voting}` — how many analysers the roster carried, and how many actually returned a non-error vote. The gate arithmetic is unchanged, but a shrunken roster loosens it silently: with two participating analysers a 1-AGREE / 1-DISAGREE split is a tie, so it never reaches `majority-disagree` and the dissent passes as `dissent-isolated`. A reader comparing two runs' gate values cannot see that without this pair. **Enforced:** `validators/validate-run.py` `_validate_participating_analysers` recomputes `voting` from the recorded verdicts and fails a declared figure the table denies. That pair still counts only *whether* a worker voted: an analyser that answers the same verdict to every item is carried in `voting` as a third opinion while contributing no refutation signal, so the gate reads as a three-way cross-check backed by two. **Enforced (advisory):** `validators/validate-run.py` `_detect_uniform_verifier` reports any worker whose every vote in the round was one verdict, with its item count — it does not fail the run, because a unanimous round is also a legitimate outcome and no ratio separates the two reliably. **Copy every such warning into `planBodyVerification.uniformVerifiers[]`** as `{worker, verdict, itemCount}`; the renderer prints it directly beneath the gate value in both the Markdown and HTML reports. A warning that exists only in the scorer's JSON is not a warning the report's reader ever sees, and the gate line alone reads as a wider cross-check than the round actually was.
|
|
208
|
+
|
|
209
|
+
**Check each verifier's verdict distribution before the next round.** Read the `okstra plan-verify` warnings alongside the gate value. Two shapes mean the roster was narrower than it looks: a verifier whose every vote was one token, and a verifier that returned no vote for items it was assigned. Both are contract violations of the adversarial posture, not stylistic preferences — the verifier is told to open the cited evidence and judge it.
|
|
210
|
+
|
|
211
|
+
When either fires, re-dispatch that verifier with a correction paragraph stating the exact nature of the violation and what IS checkable in this worktree. A byte-identical re-dispatch reproduces the same failure; a corrected one recovered 37 substantive verdicts from a worker whose first attempt answered `UNVERIFIABLE` to all 80 items. The environment exception in §"Planning-time environment gap" covers **running build and test commands only** — whether a referenced path exists, whether a command is declared in `package.json`, and whether the plan is internally consistent are all checkable without it, and a blanket "capability constraints prevent workspace resolution" is not a valid answer to any of them.
|
|
212
|
+
|
|
213
|
+
Then lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below), **appending this round** — one new `roundHistory[]` entry plus this round's votes on each verified item's `planItems[].rounds[]`. The file accumulates across rounds; it is never truncated to the latest one. Lead then populates `### 5.5.9 Plan Body Verification` in the final report's data.json (`implementationPlanning.planBodyVerification`, schema `schemas/final-report-v1.0.schema.json`; template at `templates/reports/final-report.template.md`). The §5.5.9 body is **grouped by plan item**: `planItems[]`, each carrying its `id`, its plain-language `subject` (rendered as the item heading), an optional `sourceSection`, an optional `clarificationId` (the `C-<N>` this item blocks on when `majority-disagree`), and a `verdicts[]` list (`worker / verdict / breakageKind / note`) — one verdict row per worker under that item. The renderer prints three fixed legends (gate values, verdict tokens, breakage kinds a–f) so the reader can decode every cell without opening this spec. The older flat `#### Verdict details` table (`Plan item / Worker / …`, one row per plan-item × worker pair) is superseded by the grouped layout — it hid *what* each vote was about behind a bare `P-*` ID; the subject heading is the fix. The validator's `Plan Body Verification` + `Gate result:` substring checks still gate this section.
|
|
208
214
|
7. **Self-fix loop (up to `selfFixMaxRounds`, targeting planner-fixable defects).** After aggregation, while at least one `majority-disagree` item has a majority of its `DISAGREE` verdicts at `fixability == planner-fixable`, lead runs self-fix rounds **before** promoting anything to the user:
|
|
209
215
|
- **Group the targets by cause before instructing (BLOCKING).** Blocked items are usually several derivatives of one defect — one constant declared twice, one responsibility given two owners — and the coverage rows that cite them fail as a consequence, not independently. Lead MUST partition this round's targets into cause groups and instruct each group as **"remove this cause"**, naming the derivatives it accounts for. **Handing report-writer a bare item list is forbidden**: patched one at a time, each correction leaves the sibling sections still asserting the old value, so the next round re-finds the same family and the budget drains without converging. Record the partition in `planBodyVerification.selfFixGroups[]` (`round`, `causeSummary`, `itemIds`). One group per item is a legitimate outcome only when the items genuinely share no cause — recorded that way, it is a visible diagnosis rather than a skipped one. **Enforced:** `validators/validate-run.py` `_validate_self_fix_grouping` requires the partition, ties `selfFixRoundsApplied` to the highest recorded round, and fails any corrected item that belongs to no group.
|
|
210
216
|
- lead instructs report-writer to rewrite the items in each cause group (NOT a full draft regeneration; procedure in [report-writer](./report-writer.md) §"Self-fix rewrite").
|
|
211
217
|
- missing or weak `P-Prep-*` contracts are repaired by adding kind-specific inline detail or an AI-prepared PREP item with a concrete proposal. Facts that require user or external authority remain `blocked` and keep their request material; never invent those facts during self-fix.
|
|
212
218
|
- **Drop plan items whose element the round deleted.** A self-fix rewrite may remove a plan element (a validation check, a rollback row). `P-*` ids are positional, so a deletion shifts every later row and silently re-points surviving verdicts at their neighbours — and a verdict recorded against a removed element keeps blocking a gate while being unfindable in the plan, so reading the plan never reveals the cause. After each round, re-extract plan items with `okstra plan-items extract` and re-verify any item whose `subject` no longer matches; never carry the old vote forward across a shift. **Enforced:** `validators/validate-run.py` `_validate_verdicts_match_current_subjects` (re-pointing) and `_validate_plan_item_extraction_completeness` (dangling ids).
|
|
213
|
-
- lead re-runs plan-body verification (focused on the corrected items + adjacent items the rewrite touched, plus any `needs-reverify` items whose peer failed to vote last round). After re-verification, overwrite `planItems[].verdicts` with the new verdicts. **The round's verdicts MUST be transcribed into `planBodyVerification.planItems[].verdicts` in the final report's data.json before the gate is declared** — the gate is re-derived from that table, so declaring a gate over an empty one leaves it unauditable. **Enforced:** `_validate_round_recorded_verdicts`.
|
|
219
|
+
- lead re-runs plan-body verification (focused on the corrected items + adjacent items the rewrite touched, plus any `needs-reverify` items whose peer failed to vote last round). After re-verification, overwrite `planItems[].verdicts` with the new verdicts. **The round's verdicts MUST be transcribed into `planBodyVerification.planItems[].verdicts` in the final report's data.json before the gate is declared** — the gate is re-derived from that table, so declaring a gate over an empty one leaves it unauditable. **Enforced:** `_validate_round_recorded_verdicts`. Transcribe with `okstra plan-items collect-verdicts --result <worker>=<path> … --items <plan-items.json> --output <verdicts.json>` then `okstra plan-items apply-verdicts --data <data.json> --verdicts <verdicts.json>`, never with a per-round script: the CLI reads the response shape this section fixes and **fails** on an assigned item the worker left unanswered, on a verdict for an item outside the queue, and on a `DISAGREE` with no breakage kind. A hand-written regex reports none of those — it drops them, and the round is then scored on a table that silently does not match the queue.
|
|
214
220
|
- for an item whose `majority-disagree` was resolved by self-fix, record `self-fixed in round <N>: <what was fixed>` in `planItems[].selfFixNote`. A resolved item does not create a clarification.
|
|
215
221
|
- **Each round is a worker batch.** Before dispatching round N ≥ 2, reclaim the previous round's completed verifiers exactly as at any other batch boundary ([okstra-lead-contract](./okstra-lead-contract.md) "Run-scoped worker-resource lifecycle") and emit `PROGRESS: phase-batch-cleanup panes=<n>`, then announce the round with `PROGRESS: phase-5.5.9-plan-verify round=<N> items=<count>`. Saying a round will "reuse" the previous verifiers and then dispatching under fresh names leaves every prior round holding its panes — five rounds of that is what exhausts the pane budget and blocks the next dispatch. **Enforced:** `validators/validate_session_conformance.py` `_check_plan_verify_cleanup_checkpoints` requires both lines once the state file records two or more rounds.
|
|
216
222
|
- **Round completion.** A round is complete only after the renderer has run on the corrected data.json, lead has appended the round to the state file per step 6, lead has reconciled instructed groups against applied corrections — every `itemIds` entry either carries a `selfFixNote` or is still recorded as broken — and **`okstra plan-verify --report <report>` exits 0** (step 5). A round left with a non-zero exit carries its defect into the next round's inputs, which is how a mis-scored gate survives a whole self-fix budget. A round that was instructed but never rendered has not happened, and counting it inflates the budget that gates promotion. The state-file append is not optional bookkeeping: the next re-verification overwrites data.json's `planItems[].verdicts`, so a round that never reached `roundHistory[]` leaves no record anywhere of what it blocked on — which is the whole reason this file exists. **Enforced:** `validators/validate-run.py` `_validate_plan_body_state_rounds` requires one `roundHistory[]` entry per round `1..roundCount`, each carrying its own `gateResult` and cited by at least one item's `rounds[]`, and requires the file's `selfFixRoundsApplied` to match the report's.
|
|
@@ -318,6 +324,19 @@ The per-round structures mirror the finding-convergence state artifact ([converg
|
|
|
318
324
|
|
|
319
325
|
`planItems[].rounds[].classification` enum: `full-consensus | partial-consensus | dissent-isolated | majority-disagree | needs-reverify | contested`. `needs-reverify` is the peer-error shape from §"Round protocol" step 4 (a single-vote-blocking kind with fewer than 2 participating non-error votes) — it survives into the state file when the round budget runs out before the re-dispatch resolves it, and `_recompute_plan_body_gate` folds it into `passed-with-dissent`. `contested` only appears when `maxRounds > 1`; at default `maxRounds=1` any otherwise-unresolved item folds into `partial-consensus` per the round protocol above.
|
|
320
326
|
|
|
327
|
+
`okstra plan-verify` scores the gate in its own vocabulary, which folds two of these labels together because only the `majority-disagree` boundary moves the gate. **Do not re-derive the mapping** — the scorer emits `gate.items[].stateClassification` with the state-file value already resolved, so record that. **Enforced:** `validators/validate-run.py` `_state_classification`.
|
|
328
|
+
|
|
329
|
+
| `gate.items[].classification` | `planItems[].rounds[].classification` | Condition |
|
|
330
|
+
|---|---|---|
|
|
331
|
+
| `full-consensus` | `full-consensus` | no `DISAGREE` |
|
|
332
|
+
| `has-dissent` | `dissent-isolated` | exactly one `DISAGREE` |
|
|
333
|
+
| `has-dissent` | `partial-consensus` | two or more `DISAGREE`, a tie included |
|
|
334
|
+
| `majority-disagree` | `majority-disagree` | — |
|
|
335
|
+
| `needs-reverify` | `needs-reverify` | — |
|
|
336
|
+
| `all-non-result` | `needs-reverify` | no non-error vote at all |
|
|
337
|
+
|
|
338
|
+
`all-non-result` has no state-file label of its own: it is the `needs-reverify` shape at its limit, since "fewer than 2 participating non-error votes" covers zero. `contested` never appears on the left because the scorer does not produce it.
|
|
339
|
+
|
|
321
340
|
`planItems[].rounds[].votes.<worker>` stores `AGREE | DISAGREE(<a|b|c|d|e|f>) | SUPPLEMENT` as emitted. A terminal non-result dispatch produces a non-result `verification-error` for each assigned item; a completed dispatch produces a per-item `verification-error` when that item is `UNVERIFIABLE`. Either `verification-error` MUST NOT be converted to `DISAGREE` and is excluded from consensus. The `DISAGREE` token retains its `<kind>` suffix so the breakage class is recoverable from the state file alone.
|
|
322
341
|
|
|
323
342
|
`planBodyVerification.selfFixRoundsApplied` (int, default 0): how many self-fix rounds actually ran. `planBodyVerification.selfFixStopReason` (enum, default `not-attempted`): why the loop exited — see §"Round protocol" step 7. `planItems[].verdicts[].fixability` is each `DISAGREE`'s `planner-fixable | needs-user-input` judgement, recorded in the final data.json (`§5.5.9`).
|
|
@@ -160,8 +160,9 @@ After each worker subagent returns (regardless of role), Lead MUST verify the ca
|
|
|
160
160
|
- The result file is absent at the resolved absolute path even though the worker returned without a `*_RESULT_MISSING` sentinel — for example, claude-worker returned its final assistant message but never persisted the artifact, or the wrapper exited 0 and the codex/antigravity sub-agent forwarded raw stdout despite the contract.
|
|
161
161
|
- The result file exists but cannot be parsed (frontmatter unreadable, sections 1–5 entirely missing). A truncated file in the middle of section 5 is NOT covered here — it goes to the validator's regular `error` path, not the retry path.
|
|
162
162
|
- `okstra worker-liveness --team-state <path> --worker <id>` reports a **CLI-wrapper** worker (`codex` / `antigravity`) `did-not-launch` — neither `<prompt-path>.log` nor `<prompt-path>.status.json` exists after the persisted `startedAt` plus the launch grace (default 60s). The wrapper writes its status sidecar before invoking the CLI and hard-fails loudly with a distinct exit code on every argument check before that, so the absence of BOTH artifacts means the dispatch itself never reached the script. Without this trigger the only evidence was a lead noticing two missing files by eye, and the run paid the full polling cap for a worker that never started.
|
|
163
|
-
- `okstra worker-liveness --team-state <path> --worker <id>` reports an **in-process** worker `stalled` — its registered audit sidecar's newest `- PROGRESS:` heartbeat is older than the cadence budget, or the sidecar carries no heartbeat at all. This is the in-process equivalent of the CLI wrappers' idle watchdog: the wrapper reaps a silent CLI itself, but nothing reaped a silent in-process worker until its deadline. The same selector serves both worker kinds — the sidecar is reused when a worker is re-dispatched, so the probe needs the row's `startedAt` to tell the previous attempt's last heartbeat apart from this dispatch's silence.
|
|
163
|
+
- `okstra worker-liveness --team-state <path> --worker <id>` reports an **in-process** worker `stalled` — its registered audit sidecar's newest `- PROGRESS:` heartbeat is older than the cadence budget, or the sidecar carries no heartbeat at all. This is the in-process equivalent of the CLI wrappers' idle watchdog: the wrapper reaps a silent CLI itself, but nothing reaped a silent in-process worker until its deadline. The same selector serves both worker kinds — the sidecar is reused when a worker is re-dispatched, so the probe needs the row's `startedAt` to tell the previous attempt's last heartbeat apart from this dispatch's silence. A budget breach is not a verdict on its own: the probe re-reads the sidecar after half that stage's budget and reports `stalled` only when the newest heartbeat has not advanced, so an unhealthy verdict costs that confirmation window before it returns. That window is what separates a worker inside one long uninterruptible tool call — which cannot append a heartbeat at all — from a worker that died, and measuring it is the probe's job, not yours: do not second-guess a verdict by checking mtimes yourself. Tune it with `--stall-confirm <seconds>`; `0` restores the immediate verdict.
|
|
164
164
|
- The result file exists but its audit sidecar does not, at `runs/<task-type>/worker-results/<worker>-audit-<task-type>-<seq>.md`. Workers write both in the same step, so a result without a sidecar means the Reading Confirmation block — the only evidence the worker read its inputs — was never produced. `validate-run.py` fails the run on this at Phase 7 either way (`validate_worker_results_audit`); checking it here spends the existing one-retry budget while the role can still be re-dispatched, instead of surfacing hours later when the worker session is gone.
|
|
165
|
+
- `okstra worker-audit-check --run-dir <runs/<task-type>/> --task-type <t> --seq <n> --worker <id>` exits 2 on a backticked `path:line` citation in the worker's result that has no matching Evidence read row in its audit sidecar. Run it the moment you collect each result. Phase 7 enforces the same rules from the same implementation (`okstra_ctl.worker_audit_ledger`), but by then the worker session is gone and the only remaining moves are editing the result yourself — which destroys the audit chain the ledger exists to provide — or ending the run `contract-violated`. While the session is alive, `SendMessage` to the worker so it corrects its own citation; that costs about a minute against a re-dispatch or a failed run.
|
|
165
166
|
|
|
166
167
|
**One-retry policy:**
|
|
167
168
|
|
|
@@ -1 +1,11 @@
|
|
|
1
|
-
- every clarification row carries
|
|
1
|
+
- every `Kind=decision` clarification row carries its choices in `options[]`, never as prose inside `expectedForm`. Each option is an object with six fields:
|
|
2
|
+
- `role` — `recommended` for the single best answer, `alternative` for the rest. Exactly one option per row is `recommended`.
|
|
3
|
+
- `answer` — the choice itself, phrased so the user can pick it as-is. Keep it to a short phrase (roughly 120 characters); the reasoning and the consequences have their own fields below.
|
|
4
|
+
- `rationale` — one sentence on why this option is on the board.
|
|
5
|
+
- `scopeImpact` — tokens drawn from `{in-repo, cross-repo, new-schema, deferrable}`. Exactly one of `in-repo` / `cross-repo`, which answer the same question and are mutually exclusive; `new-schema` and `deferrable` are optional additions.
|
|
6
|
+
- `addedWork` — one sentence naming the work this choice creates that the other choices do not. Name the work, not a cost adjective.
|
|
7
|
+
- `directionChange` — one sentence naming what this choice reverses: an approved plan item, a recorded decision, an earlier answer. When it reverses nothing, say so.
|
|
8
|
+
- the three impact fields answer three different questions — how far the change reaches, what new work it creates, and what it overturns. Someone choosing between options needs all three, so never fold them into one sentence: whichever axis is easiest to write would silently stand in for the other two.
|
|
9
|
+
- a row that omits `options[]`, offers fewer than two, or marks zero or two options as `recommended` is incomplete and must be completed before the report is finalised.
|
|
10
|
+
- `expectedForm` states only the *shape* of the answer — one of the options, a file path, a number, a date. It never lists the choices again; two sources for one fact leave consumers disagreeing about which is authoritative.
|
|
11
|
+
- **Enforced:** `validators/validate-run.py` `_validate_clarification_options` for `error-analysis`, `implementation-planning`, `improvement-discovery`, and `requirements-discovery` on schema-v2 reports; the field shapes by `schemas/final-report-v2.0.schema.json` `$defs.ClarificationOption`.
|
|
@@ -72,9 +72,11 @@ profile document.
|
|
|
72
72
|
Profile-specific addenda may tighten cell content but MUST NOT add, remove, rename, or reorder columns, nor change the meta-cell field order. The `ID` is `C-NNN` (3-digit zero-padded), the `Status` ∈ `{open, answered, resolved, obsolete}`, and the `Kind` / `Blocks` legal values are listed below.
|
|
73
73
|
- In schema-v1 Markdown and worker-result tables, section 1 is a **single unified table** per `final-report-template.md`. Every clarification item is one row. Do not split it into sub-sections or create a parallel question table.
|
|
74
74
|
- each row's `Kind` column picks one of `{material, decision, data-point}`: `material` for files / snapshots / logs / screenshots the user must attach (the `User input` cell will hold a path or URL); `decision` for choices and yes/no confirmations only the user can make; `data-point` for a single number, ID, date, or short string the user can answer inline. A `decision` alternative must be a terminal choice the user can pick as-is; if acting on an alternative still requires the user to supply a concrete value (a path, string, number, or file), that value is its own `data-point` / `material` row — never phrase a data-entry action (e.g. "specify the path", "enter a value") as a selectable `decision` option, because the rendered `<select>` cannot capture the value the option demands. Items that mix "yes/no + file path if yes" are one row of `Kind=material` with the combined expectation written into `Expected form`.
|
|
75
|
+
- **One decision per row.** A `decision` row asks one question. When a single option bundles two independent decisions — ones the user could answer differently — split the row, even though each option in it is individually pickable. Bundling forces the user to buy the expensive half in order to get the cheap half, and the answer then records agreement to something they were never asked about. The tell is usually the option's own `scopeImpact`: an option that has to claim `cross-repo` because *one* of the two things it bundles reaches another repository is carrying two decisions of very different cost. Whether two clauses are one decision or two is a judgement, so no validator checks it; this rule and the §5.5.9 adversarial round are the enforcement, and a verifier citing the `scopeImpact` mismatch is what makes a `DISAGREE` on it concrete rather than a matter of taste.
|
|
75
76
|
- each row's `Blocks` column picks one of `{approval, next-phase, none}`. `approval` is reserved for items that gate an approval action, especially the `implementation-planning` `approved:` frontmatter flip; outside `implementation-planning`, unresolved brief reporter-confirmation rows use `next-phase` instead. `next-phase` blocks the next run from starting cleanly. `none` is informational/audit-only.
|
|
76
77
|
- write every entry in full, descriptive sentences that a non-developer can act on without further context. Avoid abbreviations and internal jargon. The `Statement` cell must state *what* is needed, *why* the answer / attachment changes the next step, and (for `material`) *where* the user can find it and *where* to place it. The `Expected form` cell must state the answer shape (yes/no, one of the options, number/date, file path, short description, etc.); supply concrete option choices when applicable.
|
|
77
|
-
-
|
|
78
|
+
- **Schema-v2 authors do not use the string grammar below.** A v2 `Kind=decision` row carries its choices in `options[]` (see the Clarification recommendation fragment for the field list); the renderer and the `okstra user-response` picker both build their selectable options from that array, so a choice that exists only in prose is a choice the user cannot pick. The rest of this bullet governs schema-v1 tables and analysis-worker result tables, which have only string cells.
|
|
79
|
+
- if a schema-v1 table or an analysis-worker result table requires a recommended answer, alternatives, or an evidence-check note, encode it inside the existing 4-column schema: put evidence notes in `Statement` as `Evidence checked: <path:line>` or `Evidence checked: none — <human-only reason>`, and put recommendations/options in `Expected form` as `Recommended: (a) <answer> — <rationale>; Alternatives: (b) <option> (c) <option>`. The recommended answer is always the first option and MUST carry the `(a)` label; alternatives continue the same letter sequence from `(b)` (a lone alternative is `(b) <option>`, never restart at `(a)`), so the full option set reads `(a) (b) (c) …` in order and renders each as its own selectable option. Do **not** append a pick-one answer-space summary such as `(pick 1 of A / B)` or `(pick N of …)` to `<options>` — the rendered `<select>` already enforces single choice, and that annotation leaks verbatim into an option label. Do not add `Recommended`, `Evidence`, `Alternatives`, or `evidence-checked` columns, and do not break the merged record-meta cell back into separate columns.
|
|
78
80
|
- For schema v2, data.json is canonical and the HTML exports answers to a user-response sidecar; the source report is never edited. `--resume-clarification` carries those answers into the next run. The lower-level `--clarification-response <path>` remains available for scripted runs.
|
|
79
81
|
- When a response is carried in, reconcile every prior `clarificationItems[]` row against new evidence and update its status to `resolved` or `obsolete` before issuing the next verdict. Schema-v1 compatibility Markdown may additionally render its conditional Section 0; schema-v2 AI Markdown records decisions under `## Clarification and User Decisions`.
|
|
80
82
|
- **Supersession (BLOCKING).** Reconciling the `C-*` row is only half of incorporating an answer. An answer does not merely *add* a decision — it *invalidates* whatever the previous run wrote under the opposite assumption. Before issuing the next decision, walk the prior deliverable prose for every statement the answer makes false and **delete or rewrite it**, then record the retirement. Adding the new decision while leaving the contradicting sentence in place puts two opposite instructions for the same symbol in one document; the implementer must then guess which is live, and the next verification round correctly blocks on it. In `implementation-planning` this record is `implementationPlanning.supersessionLedger[]` — one entry per answered clarification, either `disposition: superseded` (with the retired statement, its replacement, and the sections revised) or `disposition: no-dependent-statement` (with a rationale). **Enforced:** `validators/validate-run.py` `_validate_supersession_ledger` requires an entry per answered clarification; whether the claim is *true* is what the §5.5.9 adversarial round tests.
|
|
@@ -149,6 +149,7 @@
|
|
|
149
149
|
- dependency / migration risk assessment (ordering constraints, data backfills, feature-flag prerequisites, repo-internal sequencing)
|
|
150
150
|
- **Cross-Project Dependencies (conditionally required):** when the plan depends on work in another project / repo / published package, add (a) a `kind: cross-project` DM row to `dependencyMigrationRisk`, and (b) a matching `XP-NNN` row to `crossProjectDependencies`. An upstream-precondition row must have concrete `requiredWork` / `verificationSignal` / `howToStart` — `validators/validate-run.py` enforces that a DM `cross-project` ⇒ at least one `direction: upstream-precondition` XP row, and the schema enforces non-empty row fields. A cross-project dependency is recorded as this structured precondition, not as a soft Recommended Next Step. A single-project plan uses an empty array.
|
|
151
151
|
- **recommendedNextSteps policy:** keep the substance of cross-project preconditions/carries in `crossProjectDependencies`, and put in `§3 Recommended Next Steps` only a pointer to that section (`§5.4 Cross-Project Dependencies`) — no double recording.
|
|
152
|
+
- **Resuming from an approval blocker (BLOCKING).** When this report carries any `blocks: approval` clarification, one `recommendedNextSteps` entry MUST name the `--answered-clarifications` re-run, in its `text` or in one of its `commands`. The reader of the report is the person who has to act, and until now the mechanics of resuming lived only in the lead prompt — which is read after the next run has already started. Point them at `okstra recap assemble`, which prints the answered ids, the exact flag value, the sidecar paths, and whether the re-verification would fall back to full. Do not restate those values here: they are unknown while you write, because the user has not answered yet. **Enforced:** `validators/validate-run.py` `_validate_rerun_guidance`.
|
|
152
153
|
- validation checklist (pre / mid / post) — each item is an exact command or observable outcome
|
|
153
154
|
- rollback strategy — exact revert path (commits, flags, migrations) and the signal that triggers rollback
|
|
154
155
|
- **Requirement admissibility (scope boundary):** a brief line becomes a Requirement Coverage row only when **a stage can satisfy it by changing files in this repository** — source, tests, config, or deployment *manifest files*. A line whose satisfaction needs a person's approval, a ticket status change, or an action against live infrastructure (applying a manifest, a cutover, creating a dashboard or alert, validating in staging/production) is NOT a requirement for this phase: it belongs to the brief's `## External Gates`, and this plan neither creates a stage for it nor cites it in coverage. Briefs generated by `okstra-brief-gen` pre-split these into the end-state sections `## Expected Behavior` / `## Preserved Behavior` / `## Expected Outcome` (admissible) and `## External Gates` (not); when reading an older brief that carries a raw Definition-of-Done checklist, apply the same test line by line. The boundary is the *action*, not the topic — "add the flag to `values-prod.yaml`" is admissible, "apply that manifest to prod" is not. Planning an operational stage this phase cannot execute (see the run-scope rule above forbidding deployments) produces steps whose commands never resolve, which the §5.5.9 gate then correctly blocks — the plan must not create that deadlock in the first place.
|
|
@@ -199,3 +200,4 @@
|
|
|
199
200
|
9. **Cross-project dependency check** — confirm you have not missed a dependency on another repo / another top-level deployable module / a published package. If `dependencyMigrationRisk` has a `kind: cross-project` row, confirm a matching `direction: upstream-precondition` `XP-NNN` row exists in `crossProjectDependencies`, and re-read as a reviewer whether its `requiredWork` is the concrete work the other side must actually build rather than an abstract phrase ("other side's work done") — validator S only checks existence, so concreteness is the self-review's responsibility. Confirm cross-repo work is split into a separate run + XP row instead of being crammed into one task's stages, and that the cross-project substance is not duplicated in `§3 Recommended Next Steps` but lives only in `§5.4 Cross-Project Dependencies`.
|
|
200
201
|
10. **Decision-draft materialization check** — when `decisionDrafts` is non-empty, confirm as a reviewer which stage's stepwise order contains the matching materialization step (creating `.okstra/decisions/<NNNN>-<slug>.md`) and that the number of drafts corresponds 1:1 with the materialization steps. The validator only checks the *existence* of the step, so the `<NNNN>-<slug>` correctness and count correspondence are the self-review's responsibility.
|
|
201
202
|
11. **Variation-point & seam check** — read `variationPointAnalysis` as a skeptic. Is `hasMultipleImplementations` honest against the brief and the sibling code you inspected during pre-planning, or was `false` chosen because it is the cheaper field to fill? For every point with `extract: true`, confirm the `extractionDecision` names a real interface (a `port` for a hexagonal project, not a shared helper) and a `coveredBy` stage that exists in the Stage Map — an interface no stage builds is a decision nobody executes. Then read the recommended option's `testSeams`: each `injectedAs` must name a construction or wiring point a test can actually substitute at, not a symbol the test would have to re-implement — a seam nothing can be injected into leaves the executor writing self-mocks. An empty `testSeams` array is only acceptable when you can defend it in one sentence; the validator accepts it either way, so this is the check that catches an unfilled field posing as a decision.
|
|
203
|
+
12. **Approval blast-radius check (BLOCKING).** Every `Blocks=approval` clarification row must be reachable *from* the plan, not only *into* it: at least one `planItems[]` entry carrying that id as `clarificationId`, or one `requirementCoverage` row blocked on it in `status` or `approvalDisposition`. Item 7 covers only rows this run promoted from a majority-disagree plan item; a blocker raised any other way can still withhold approval while recording nothing it affects. The cost lands on the re-run: `okstra incremental-scope` resolves impacted stages from exactly these two links and treats an id that traces to no stage as grounds to re-verify every stage, so one unlinked blocker turns an incremental re-run into a full one. **Enforced:** `validators/validate-run.py` `_validate_approval_clarification_backtrace`.
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
`okstra-run --task-brief <packet path>` (this phase does not directly start any downstream run).
|
|
65
65
|
- Decision-tree walk (bounded):
|
|
66
66
|
- When the brief's `Desired Outcome`, classification, or routing target depends on a chain of decisions, walk that chain one branch at a time. Each branch is one `Clarification Items` row, not a free-form interview.
|
|
67
|
-
- For every clarification row,
|
|
67
|
+
- For every `Kind=decision` clarification row, author `options[]` as the Clarification recommendation fragment specifies: exactly one `role: recommended`, and every option carrying `scopeImpact` / `addedWork` / `directionChange`. Consequences belong in those fields — appending them to the option text puts them where nothing reads them.
|
|
68
68
|
- **Codebase-first rule**: if a branch can be resolved by `Read` / `Grep` / file inspection, resolve it that way and record `Evidence checked: <path:line>` in the `Statement` cell. Do NOT escalate to the user.
|
|
69
69
|
- Budget: the unified `## 1. Clarification Items` table caps at the smaller of (a) one row per unresolved decision branch, (b) 8 rows total. Beyond the cap, fold remaining ambiguity into the routing recommendation's risk notes.
|
|
70
70
|
- Expected output emphasis:
|
|
@@ -564,6 +564,9 @@
|
|
|
564
564
|
"worktree_impl_new": " worktree : stage {stage} 새 worktree `{path}` (브랜치 `{branch}`, base 는 run 준비 시 해소)",
|
|
565
565
|
"worktree_impl_reuse": " worktree : 기존 stage {stage} worktree `{path}` (브랜치 `{branch}`)",
|
|
566
566
|
"worktree_impl_auto": " worktree : stage 자동 선택 — `{path}` 아래 stage-<N>/ worktree 생성/재사용",
|
|
567
|
+
"reverify_scope_incremental": " reverify-scope: incremental 가능 — 답변된 항목이 모두 직전 리포트의 stage 에 연결됨 (최종 확정은 run 시점 base-ref 비교)",
|
|
568
|
+
"reverify_scope_unlinked": " reverify-scope: full 예상 — {ids} 이(가) 직전 리포트의 어느 stage 에도 연결되지 않아 범위를 좁힐 수 없음",
|
|
569
|
+
"reverify_scope_full": " reverify-scope: full 예상 — {reason}",
|
|
567
570
|
"stage_whole_task": "전체 task",
|
|
568
571
|
"handoff_scope_whole_task": "전체 task (whole-task 검증 기반)",
|
|
569
572
|
"handoff_scope_stage_group": "stage-group ({stages})"
|
|
@@ -271,10 +271,16 @@ def _v2_row(entry: dict) -> Optional[dict]:
|
|
|
271
271
|
raw_blocks=raw_blocks,
|
|
272
272
|
raw_status=raw_status,
|
|
273
273
|
)
|
|
274
|
+
options = entry.get("options")
|
|
274
275
|
return {
|
|
275
276
|
"item": item,
|
|
276
277
|
"statement": str(entry.get("statement") or ""),
|
|
277
278
|
"expected_form": str(entry.get("expectedForm") or ""),
|
|
279
|
+
"options": (
|
|
280
|
+
[option for option in options if isinstance(option, dict)]
|
|
281
|
+
if isinstance(options, list)
|
|
282
|
+
else []
|
|
283
|
+
),
|
|
278
284
|
}
|
|
279
285
|
|
|
280
286
|
|
|
@@ -350,6 +356,9 @@ def parse_section_1_rows(report_text: str) -> list[dict]:
|
|
|
350
356
|
"item": item,
|
|
351
357
|
"statement": cells[s_col] if 0 <= s_col < len(cells) else "",
|
|
352
358
|
"expected_form": cells[e_col] if 0 <= e_col < len(cells) else "",
|
|
359
|
+
# v1 cells are strings; the structured options live only in v2's
|
|
360
|
+
# data sibling. The key stays so callers never branch on schema.
|
|
361
|
+
"options": [],
|
|
353
362
|
})
|
|
354
363
|
return rows
|
|
355
364
|
|