okstra 0.125.1 → 0.125.2
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/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/plan-body-verification.md +16 -8
- package/runtime/prompts/lead/report-writer.md +3 -1
- package/runtime/python/okstra_ctl/render.py +3 -0
- package/runtime/schemas/final-report-v1.0.schema.json +9 -1
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/validators/validate-run.py +32 -8
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -42,6 +42,7 @@ Plan-body verification is configured under `convergence.planBodyVerification` in
|
|
|
42
42
|
|---------|---------|-------------|
|
|
43
43
|
| `enabled` | `true` | If `false`, the round is skipped and the top-of-report Approval marker is rendered unconditionally (legacy behaviour). |
|
|
44
44
|
| `maxRounds` | `1` | Upper bound. Plan-body verification is consistency / completeness checking, not fact checking — additional rounds rarely help. Range 1–3. |
|
|
45
|
+
| `selfFixMaxRounds` | `3` | Upper bound on the report-writer self-fix loop (§"Round protocol" step 7). Range 1–5. The loop also stops early on no-progress, so this is a ceiling, not a target. |
|
|
45
46
|
| `gating` | `true` | If `true` (default), `majority-disagree` blocks the Approval marker. If `false`, the round is advisory-only and the marker always renders. |
|
|
46
47
|
|
|
47
48
|
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.
|
|
@@ -126,27 +127,33 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
126
127
|
- `partial-consensus` — majority `AGREE`, dissenting `DISAGREE` recorded.
|
|
127
128
|
- `dissent-isolated` — only one worker `DISAGREE`s, others `AGREE` — treat as `partial-consensus` for gate purposes; record dissent. (Distinct from finding-convergence `worker-unique`, which means the *opposite*: only one worker AGREEs. Plan-body classifications use this dedicated label to avoid the collision.)
|
|
128
129
|
- `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes), OR any single-vote-blocking kind fires: one `DISAGREE(a)` / `DISAGREE(d)` on any item, or one `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks the Approval marker**.
|
|
130
|
+
- `needs-reverify` — 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. This does **not** block the Approval marker — 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`.
|
|
129
131
|
- `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
|
|
130
132
|
5. Gate result resolution:
|
|
131
133
|
- any `majority-disagree` item present AND `gating=true` → `blocked-by-disagreement`
|
|
132
134
|
- all dispatches non-result → `aborted-non-result`
|
|
133
|
-
- any `partial-consensus` / `dissent-isolated` present, no `majority-disagree` → `passed-with-dissent`
|
|
135
|
+
- any `partial-consensus` / `dissent-isolated` / `needs-reverify` present, no `majority-disagree` → `passed-with-dissent`
|
|
134
136
|
- all items `full-consensus` → `passed`
|
|
135
137
|
6. Lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below) and 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.
|
|
136
|
-
7. **Self-fix
|
|
138
|
+
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:
|
|
137
139
|
- lead instructs report-writer to rewrite only those planner-fixable items for correction (NOT a full draft regeneration — patch only the plan section each `P-*` points to; procedure in [report-writer](./report-writer.md) §"Self-fix rewrite").
|
|
138
140
|
- 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.
|
|
139
|
-
- lead re-runs plan-body verification
|
|
141
|
+
- 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.
|
|
140
142
|
- 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.
|
|
141
|
-
-
|
|
143
|
+
- **Loop termination.** Record the round count in `planBodyVerification.selfFixRoundsApplied` and why the loop stopped in `planBodyVerification.selfFixStopReason`:
|
|
144
|
+
- `all-resolved` — no planner-fixable `majority-disagree` item remains. Exit.
|
|
145
|
+
- `no-progress` — the round resolved **zero** planner-fixable items relative to the previous round. Exit even with budget left: the same rewrite would repeat. Newly *introduced* defects count against progress, so a rewrite that trades one defect for another stops the loop rather than churning.
|
|
146
|
+
- `max-rounds-reached` — `selfFixRoundsApplied == selfFixMaxRounds`. Exit.
|
|
147
|
+
- `not-attempted` — the loop never ran because no item qualified.
|
|
148
|
+
The `no-progress` and `max-rounds-reached` exits are what make the loop terminate; `selfFixMaxRounds` alone is the backstop.
|
|
142
149
|
- a `majority-disagree` item with a majority of `needs-user-input` is NOT a self-fix target — it goes straight to the next step's clarification promotion.
|
|
143
|
-
8. For every `majority-disagree` item **that remains after the self-fix
|
|
150
|
+
8. For every `majority-disagree` item **that remains after the self-fix loop** (items not resolved by self-fix, or with a `needs-user-input` majority from the start), lead adds a row to `## 1. Clarification Items` with:
|
|
144
151
|
- new `C-<N>` ID (numbering continues from any existing rows)
|
|
145
152
|
- `Statement` summarising the disagreement and the worker breakage `<kind>`
|
|
146
153
|
- `Kind` chosen per the standard policy (usually `decision` for option-level conflicts, `data-point` for path/symbol mismatches)
|
|
147
154
|
- `Blocks=approval`
|
|
148
155
|
- the item's `planItems[].clarificationId` set to that `C-<N>` (1:1 link). `validators/validate-run.py` `_validate_plan_body_clarification_matching` recomputes each item's class and fails when a majority-disagree item's `clarificationId` is missing, dangling, or points at a non-`approval` row.
|
|
149
|
-
- a `planner-fixable` item that survives the self-fix
|
|
156
|
+
- a `planner-fixable` item that survives the self-fix loop is still promoted, but its `Statement` MUST state "planner self-fix attempted but unresolved" and name the stop reason. `validators/validate-run.py` `_validate_self_fix_before_clarification` fails when a planner-fixable majority item is promoted while the budget is not exhausted — it requires `selfFixRoundsApplied >= 1` **and** `selfFixStopReason` in `{no-progress, max-rounds-reached}`, so neither `all-resolved` nor `not-attempted` can excuse a promotion.
|
|
150
157
|
9. The top-of-report `- [ ] Approved` marker line is rendered if and only if the Gate result is `passed` or `passed-with-dissent`. `validators/validate-run.py` `validate_phase_boundary` enforces this correspondence; manually adding the marker line when the gate did not pass is a contract violation.
|
|
151
158
|
|
|
152
159
|
## `plan-body-verification-<task-type>-<seq>.json` schema
|
|
@@ -160,7 +167,8 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
160
167
|
"gating": true,
|
|
161
168
|
"verificationMode": "lightweight",
|
|
162
169
|
"gateResult": "passed | passed-with-dissent | blocked-by-disagreement | aborted-non-result",
|
|
163
|
-
"
|
|
170
|
+
"selfFixRoundsApplied": 0,
|
|
171
|
+
"selfFixStopReason": "not-attempted | all-resolved | no-progress | max-rounds-reached",
|
|
164
172
|
"planItems": [
|
|
165
173
|
{
|
|
166
174
|
"id": "P-Opt-1",
|
|
@@ -193,7 +201,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
193
201
|
|
|
194
202
|
`planItems[].votes.<worker>` is the verbatim verdict token emitted by the worker — `AGREE | DISAGREE(<a|b|c|d|e|f>) | SUPPLEMENT` — or `verification-error` for terminal non-result dispatches. The `DISAGREE` token retains its `<kind>` suffix so the breakage class is recoverable from the state file alone.
|
|
195
203
|
|
|
196
|
-
`planBodyVerification.
|
|
204
|
+
`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`).
|
|
197
205
|
|
|
198
206
|
## Plan-body reverify prompt
|
|
199
207
|
|
|
@@ -235,7 +235,9 @@ When lead instructs a self-fix round (passing the target `P-*` list + the reason
|
|
|
235
235
|
- requirement-coverage mapping error → remap to a real option/stage/step or XP row.
|
|
236
236
|
- missing/weak design-prep contract → fix with kind-specific inline detail or an AI-prepared PREP item carrying a concrete `aiProposal`. Do NOT invent facts requiring user/external authority — keep them `blocked` + request material.
|
|
237
237
|
|
|
238
|
-
After correcting, leave `self-fixed in round <N>: <what was fixed>` in that `planItems[].selfFixNote` and
|
|
238
|
+
After correcting, leave `self-fixed in round <N>: <what was fixed>` in that `planItems[].selfFixNote` and set `planBodyVerification.selfFixRoundsApplied` to the number of rounds run so far. `needs-user-input` items are NEVER a correction target — they are promoted to clarification as-is.
|
|
239
|
+
|
|
240
|
+
Lead may instruct several rounds (bounded by `selfFixMaxRounds`, see [plan-body-verification](./plan-body-verification.md) §"Round protocol" step 7). Each round targets only the items still broken, so a round that fixes nothing new ends the loop — correcting an item by breaking a sibling counts as no progress, not progress.
|
|
239
241
|
|
|
240
242
|
### Final-verification verdict token contract (BLOCKING)
|
|
241
243
|
|
|
@@ -1160,6 +1160,8 @@ def _build_convergence_block(ctx: dict) -> dict:
|
|
|
1160
1160
|
False otherwise
|
|
1161
1161
|
- `planBodyVerification` is implementation-planning specific; the key is
|
|
1162
1162
|
always emitted (dead-letter on other phases) so the schema stays stable.
|
|
1163
|
+
Its `selfFixMaxRounds` default 3 bounds the report-writer self-fix loop
|
|
1164
|
+
that runs before a planner-fixable defect is promoted to the user.
|
|
1163
1165
|
|
|
1164
1166
|
ctx knobs honoured:
|
|
1165
1167
|
- `OKSTRA_PLAN_VERIFICATION`: "true" | "false" | "" (empty → default True).
|
|
@@ -1199,6 +1201,7 @@ def _build_convergence_block(ctx: dict) -> dict:
|
|
|
1199
1201
|
"planBodyVerification": {
|
|
1200
1202
|
"enabled": plan_verify_enabled,
|
|
1201
1203
|
"maxRounds": 1,
|
|
1204
|
+
"selfFixMaxRounds": 3,
|
|
1202
1205
|
"gating": True,
|
|
1203
1206
|
},
|
|
1204
1207
|
}
|
|
@@ -1844,7 +1844,15 @@
|
|
|
1844
1844
|
"aborted-non-result"
|
|
1845
1845
|
]
|
|
1846
1846
|
},
|
|
1847
|
-
"
|
|
1847
|
+
"selfFixRoundsApplied": { "type": "integer", "minimum": 0 },
|
|
1848
|
+
"selfFixStopReason": {
|
|
1849
|
+
"enum": [
|
|
1850
|
+
"not-attempted",
|
|
1851
|
+
"all-resolved",
|
|
1852
|
+
"no-progress",
|
|
1853
|
+
"max-rounds-reached"
|
|
1854
|
+
]
|
|
1855
|
+
},
|
|
1848
1856
|
"planItems": {
|
|
1849
1857
|
"type": "array",
|
|
1850
1858
|
"items": {
|
|
@@ -262,10 +262,10 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
|
|
|
262
262
|
|
|
263
263
|
### Validation Checklist{% if t("sectionAside.validationChecklist") != "Validation Checklist" %} ({{ t("sectionAside.validationChecklist") }}){% endif %}
|
|
264
264
|
|
|
265
|
-
|
|
|
266
|
-
|
|
265
|
+
| {{ t("columns.recordMeta") }} | Check | Command / Observation | Expected outcome |
|
|
266
|
+
|--------|-------|------------------------|-------------------|
|
|
267
267
|
{% for row in implementationPlanning.validationChecklist -%}
|
|
268
|
-
| {{ row.id | mdcell }}
|
|
268
|
+
| **{{ row.id | mdcell }}**<br>Ticket: `{{ row.ticketId | mdcell }}`<br>Phase: {{ row.phase | mdcell }} | {{ row.check | mdcell }} | `{{ row.commandOrObservation | mdcell }}` | {{ row.expectedOutcome | mdcell }} |
|
|
269
269
|
{% endfor %}
|
|
270
270
|
|
|
271
271
|
### Rollback Strategy{% if t("sectionAside.rollbackStrategy") != "Rollback Strategy" %} ({{ t("sectionAside.rollbackStrategy") }}){% endif %}
|
|
@@ -1800,8 +1800,8 @@ _SINGLE_VOTE_BLOCKING_KINDS = {"a", "d"}
|
|
|
1800
1800
|
def _classify_plan_item_gate(item: dict) -> str:
|
|
1801
1801
|
"""Recompute one plan item's gate class from its per-worker verdicts,
|
|
1802
1802
|
per `prompts/lead/plan-body-verification.md` "Round protocol". Returns one of
|
|
1803
|
-
``majority-disagree`` / ``
|
|
1804
|
-
``all-non-result``. Collapses ``partial-consensus`` and
|
|
1803
|
+
``majority-disagree`` / ``needs-reverify`` / ``has-dissent`` /
|
|
1804
|
+
``full-consensus`` / ``all-non-result``. Collapses ``partial-consensus`` and
|
|
1805
1805
|
``dissent-isolated`` into ``has-dissent`` because they resolve to the
|
|
1806
1806
|
same gate value; only the majority-disagree boundary changes the gate.
|
|
1807
1807
|
"""
|
|
@@ -1827,6 +1827,13 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
1827
1827
|
# `a`/`d` for any item; `f` only for P-Req items (requirement coverage).
|
|
1828
1828
|
is_req = str(item.get("id") or "").upper().startswith("P-REQ")
|
|
1829
1829
|
if disagree_kinds & _SINGLE_VOTE_BLOCKING_KINDS or (is_req and "f" in disagree_kinds):
|
|
1830
|
+
# "One confirmed DISAGREE" presupposes the item was actually
|
|
1831
|
+
# cross-verified. When the peer returned a non-result nothing confirmed
|
|
1832
|
+
# the dissent, so blocking here would reproduce the same
|
|
1833
|
+
# worker-failure-makes-the-gate-stricter paradox the majority branch
|
|
1834
|
+
# below guards against. Route it to a re-verify round instead.
|
|
1835
|
+
if len(non_error) < 2:
|
|
1836
|
+
return "needs-reverify"
|
|
1830
1837
|
return "majority-disagree"
|
|
1831
1838
|
# Otherwise a genuine majority is required — and a majority needs at least
|
|
1832
1839
|
# two participating votes, so a lone surviving DISAGREE (its peer returned a
|
|
@@ -1852,7 +1859,7 @@ def _recompute_plan_body_gate(pbv: dict) -> str | None:
|
|
|
1852
1859
|
return "aborted-non-result"
|
|
1853
1860
|
if any(c == "majority-disagree" for c in classes):
|
|
1854
1861
|
return "blocked-by-disagreement"
|
|
1855
|
-
if any(c
|
|
1862
|
+
if any(c in ("has-dissent", "needs-reverify") for c in classes):
|
|
1856
1863
|
return "passed-with-dissent"
|
|
1857
1864
|
return "passed"
|
|
1858
1865
|
|
|
@@ -2402,8 +2409,14 @@ def _validate_plan_body_clarification_matching(data: dict, failures: list[str])
|
|
|
2402
2409
|
)
|
|
2403
2410
|
|
|
2404
2411
|
|
|
2412
|
+
# Stop reasons that justify promoting a still-broken planner-fixable item to the
|
|
2413
|
+
# user: the self-fix budget ran out, or a round produced no net resolution so
|
|
2414
|
+
# further rounds would repeat themselves.
|
|
2415
|
+
_SELF_FIX_EXHAUSTED_REASONS = frozenset({"max-rounds-reached", "no-progress"})
|
|
2416
|
+
|
|
2417
|
+
|
|
2405
2418
|
def _validate_self_fix_before_clarification(data: dict, failures: list[str]) -> None:
|
|
2406
|
-
"""A planner-fixable defect MUST
|
|
2419
|
+
"""A planner-fixable defect MUST exhaust the self-fix budget before it is
|
|
2407
2420
|
promoted to a `## 1. Clarification Items` row. Closes the hole where the
|
|
2408
2421
|
lead dumps a fixable plan defect (abbreviated path, prose command,
|
|
2409
2422
|
placeholder, coverage remap) onto the user instead of having report-writer
|
|
@@ -2418,7 +2431,14 @@ def _validate_self_fix_before_clarification(data: dict, failures: list[str]) ->
|
|
|
2418
2431
|
round_count = pbv.get("roundCount")
|
|
2419
2432
|
if not isinstance(round_count, int) or round_count < 1:
|
|
2420
2433
|
return
|
|
2421
|
-
|
|
2434
|
+
rounds_applied = pbv.get("selfFixRoundsApplied")
|
|
2435
|
+
stop_reason = pbv.get("selfFixStopReason")
|
|
2436
|
+
budget_exhausted = (
|
|
2437
|
+
isinstance(rounds_applied, int)
|
|
2438
|
+
and rounds_applied >= 1
|
|
2439
|
+
and stop_reason in _SELF_FIX_EXHAUSTED_REASONS
|
|
2440
|
+
)
|
|
2441
|
+
if budget_exhausted:
|
|
2422
2442
|
return
|
|
2423
2443
|
for item in pbv.get("planItems") or []:
|
|
2424
2444
|
if not isinstance(item, dict):
|
|
@@ -2431,12 +2451,16 @@ def _validate_self_fix_before_clarification(data: dict, failures: list[str]) ->
|
|
|
2431
2451
|
]
|
|
2432
2452
|
fixable = [v for v in disagrees if v.get("fixability") == "planner-fixable"]
|
|
2433
2453
|
if disagrees and len(fixable) * 2 > len(disagrees):
|
|
2454
|
+
allowed = " / ".join(sorted(_SELF_FIX_EXHAUSTED_REASONS))
|
|
2434
2455
|
failures.append(
|
|
2435
2456
|
"final-report data.json: plan item "
|
|
2436
2457
|
f"`{item.get('id') or '<unknown>'}` is majority-disagree with a "
|
|
2437
|
-
"planner-fixable majority but
|
|
2438
|
-
"
|
|
2439
|
-
"
|
|
2458
|
+
f"planner-fixable majority but the self-fix budget is not "
|
|
2459
|
+
f"exhausted (`selfFixRoundsApplied`={rounds_applied!r}, "
|
|
2460
|
+
f"`selfFixStopReason`={stop_reason!r}; need >=1 rounds and a stop "
|
|
2461
|
+
f"reason of {allowed}). A planner-fixable defect MUST be corrected "
|
|
2462
|
+
"by report-writer self-fix rounds until the budget runs out or a "
|
|
2463
|
+
"round makes no progress, before it becomes a clarification row "
|
|
2440
2464
|
"(plan-body-verification.md Self-fix round)."
|
|
2441
2465
|
)
|
|
2442
2466
|
|