Plan review: standing findings after parse, terminal absence only, predecessor kept

Review round 1 (three independent reviewers, one batch) on the open-set verdict:

- A seat's standing findings are judged once its parse result is final and only
  when its absence is terminal. An unparseable reply (prose, no findings array)
  is a non-answer that keeps the seat's earlier finding listed; a seat still
  awaiting the dispatch barrier, in flight or late-pending is a gap and carries
  nothing (06 §Plan construction and review: an answer that has not arrived is
  never a verdict). Before this, two clean seats plus one unparseable objector
  closed GREEN, and a barrier wave stamped "did not answer" on seats that were
  merely awaited.
- A same-fingerprint re-dispatch (a changed reviewer_effort on an open review)
  replaces its predecessor in the hot index; the replacing wave now keeps that
  predecessor's exact artifact reference (previous_wave_artifact, also kept by
  compact_wave), and free collection resolves the predecessor through it, so the
  standing seam and the prior-cycle packet see the real predecessor instead of
  the in-flight wave itself.
- A closed predecessor carries nothing (plan_standing_findings now reads closed,
  as its contract promised): a finding closed by a reasoned advisory reject is
  earned authority and is not resurrected on a later same-spec wave.
- Notes are neutral for the only-awaited stamp: a quorum whose only findings are
  notes, held open by an awaited seat, is still a mere wait (D4).
- Verdict text: the advisory NOTE states the cap fact like the blocking one; a $0
  not-sent seat with a carried finding says "not sent; its earlier finding is
  still listed"; the Reviews card prints the weaker-order line on compact waves
  too and the carried explanation on every terminal branch; skipped rows carry
  the seat's effort facts; the trace meta labels the historical critic pair.
- Docs: the closure sentence names the CONFIGURED enforcement against the
  hurry-projected advisory (06, 07); CHECKLISTS keys the changed-spec clause on
  the spec hash (D4 hunk, protected path: authority D4 / Q-v as before).

Tests: two-sided pins for the unparseable objector, the awaiting-then-refused
seat through the barrier and collection, the reject-closed predecessor, the
changed-spec subcase from an open objection, the note-neutral wait, and the two
card branches. Size: plan_review_runtime.py leaves the 1001-1500 band (1507,
manifest regenerated); task_results.py 1597; review_presentation.js 1599.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
Ouroboros 2026-09-26 16:11:33 +03:00
parent 0caac10e9e
commit 086241ed51
16 changed files with 188 additions and 38 deletions

View file

@ -882,8 +882,8 @@ authority, and prose outside the array is not parsed.
paid cycle. Under advisory enforcement a reject with its rationale also closes a below-quorum
blocking finding (per finding; accept or defer keeps it open until a changed spec is
reviewed). Under blocking enforcement a below-quorum blocking finding stays OPEN whatever the
disposition says: it closes only through a changed spec (a new fingerprint, the next paid
cycle) or the slot that raised it no longer raising it in a later paid cycle. A
disposition says: it closes only through a changed spec (a new spec hash reviewed in the next
paid cycle) or the slot that raised it no longer raising it in a later paid cycle. A
REVIEW_REQUIRED wave whose open set empties is recorded GREEN.
- **REVISE_PLAN** — blocking findings at quorum. A disposition can never close it: the agent
either changes the spec (a new fingerprint, the next paid cycle) or rejects a blocking finding

View file

@ -500,7 +500,7 @@ Collection is the existing `review_disposition` mode addressed at the recorded w
An answer that has not arrived is a gap, never a failure or a verdict. The STORED wave is the fail-closed floor: an unanswered slot stays an `ok=False` row no quorum counts — on a same-spec cycle (equal `spec_hash`, whatever the roster, order or effort) it keeps the seat's still-open findings from the predecessor listed on the wave, stamped `carried_absent_answer` (`plan_spec.plan_standing_findings`), so silence never manufactures GREEN and casts no blocking-slot vote — and the open `DEGRADED` + `custody_pending` pair holds every gate with no fifth aggregate for older builds to refuse. Renderers read `plan_review_runtime.plan_wave_slot_census` before wording a slot: `awaiting` is `operation_state=pending_dispatch` only; `unresolved` is `in_flight`/`custody_lost`, named as unresolved on the line and by raw state in Reviews, never called waiting (`late_result_pending` is true for both); `uncollected` has a settled supplement not collected yet; `not_dispatched` is a $0 refusal, not a failure. Verdict and failure wording and the task's `degraded` stamp belong to collected slots only (the stamp keys on an open review, and a verdict-bearing wave is open only while its open set is non-empty): a finish over an only-awaited wave or panel keeps its disclosure and records `awaiting` (`execution.plan_review`, review axis; `owner_hurry.plan_wave_only_awaited`, `_outcome_receipts.review_runs_only_awaited`). `owner_hurry.force_plan_decision` makes ONE free collection of the current pending wave in every enforcement mode, hurry too, then projects the returned state (`plan_review_collect.collect_before_gate`). Advisory and hurry keep their local release; collection updates disclosed facts, never permission to proceed. Context health reads the canonical wave, never collecting: `PLAN REVIEW WAVE OPEN` names its fingerprint, pending-slot count and snapshot timestamp (not a physical start, not a liveness lease) and disappears once collection clears custody.
Closure follows the finding class over the wave's open set (`plan_spec.closure_after_disposition`, the ONE closure table, at initial synthesis and at later dispositions): GREEN — no blocking finding and no `need_evidence` without a disposition — closes immediately in either enforcement mode, so a note-only wave is GREEN; outstanding `need_evidence` closes through a disposition-only `plan_task` call (no model call, no cost: accept = answered, reject, defer = deferred openly; an answer reaches the reviewers on the next paid cycle, a revised envelope supersedes the wave with its open requests); a below-quorum blocking finding closes, per finding, under advisory enforcement by a reject with its rationale (accept or defer keep it open) and under blocking enforcement stays open until a changed spec is reviewed or the reviewer retires it in a later paid cycle; a REVIEW_REQUIRED wave whose open set empties is written GREEN on every write path with the closure note `closed_by_disposition`, while the four control validators keep accepting older closed REVIEW_REQUIRED rows; REVISE_PLAN is never closed by disposition — a subsequent paid delta review may evaluate a changed spec or a justified rejection when another cycle is available. A closed note-only wave still accepts voluntary `review_disposition` annotations through the same writer, spec/verdict and paid-cycle count fixed and the immutable predecessor retained: optional reasoning history, not a new plan gate, never another panel.
Closure follows the finding class over the wave's open set (`plan_spec.closure_after_disposition`, the ONE closure table, at initial synthesis and at later dispositions): GREEN — no blocking finding and no `need_evidence` without a disposition — closes immediately in either enforcement mode, so a note-only wave is GREEN; outstanding `need_evidence` closes through a disposition-only `plan_task` call (no model call, no cost: accept = answered, reject, defer = deferred openly; an answer reaches the reviewers on the next paid cycle, a revised envelope supersedes the wave with its open requests); a below-quorum blocking finding closes, per finding, under advisory enforcement by a reject with its rationale (accept or defer keep it open) and under blocking enforcement stays open until a changed spec is reviewed or the reviewer retires it in a later paid cycle (closure reads the CONFIGURED enforcement: an armed owner hurry projects advisory for the finalization gate only, never for closure); a REVIEW_REQUIRED wave whose open set empties is written GREEN on every write path with the closure note `closed_by_disposition`, while the four control validators keep accepting older closed REVIEW_REQUIRED rows; REVISE_PLAN is never closed by disposition — a subsequent paid delta review may evaluate a changed spec or a justified rejection when another cycle is available. A closed note-only wave still accepts voluntary `review_disposition` annotations through the same writer, spec/verdict and paid-cycle count fixed and the immutable predecessor retained: optional reasoning history, not a new plan gate, never another panel.
Paid cycles are bounded by the shared `OUROBOROS_REVIEW_MAX_CYCLES`; `cycles_paid` and the typed `cycles_exhausted` state count PAID cycles only. A wave is paid iff at least one reviewer slot was physically dispatched, and the proof is a settled row: a slot released at the barrier is $0 until its row proves the send, so the cycle is paid at collection, never at the barrier, and a nothing-dispatched wave of typed $0 skip rows stays unpaid and never replaces a paid predecessor. A barrier-dispatched panel is nevertheless committed money: while its wave is custody-pending and the cap has no room for another panel, a revised envelope first collects EVERY other custody-pending wave at $0 (`plan_review_collect.collect_before_supersede`) and, if the cap still has no room, is HELD with the typed `PLAN_REVIEW_IN_FLIGHT` refusal (`plan_review_collect.in_flight_hold`) so the current wave stays collectible (the per-case cap arithmetic: `plan_review_collect.py` docstrings). An identical envelope replays the recorded wave free, with one exception: an open wave whose blocking findings all carry valid reject dispositions may dispatch exactly one subsequent paid delta panel when another cycle is available.

View file

@ -156,7 +156,7 @@ A registry of `config.SETTINGS_DEFAULTS` (exact defaults canonical in `settings_
| OUROBOROS_OBSERVABILITY_RETENTION_DAYS | (retired) | Retired (`RETIRED_SETTING_KEYS`): rows are kept indefinitely and the reader never deletes, so the knob had no reader; stored value stripped at load, env inert |
| OUROBOROS_REVIEW_MODEL_TIMEOUT_SEC | (unset) | Env-only: logical review timeout (absent = route-owned; late in-flight results stay in custody) |
| OUROBOROS_REVIEW_MAX_TOKENS | 65536 | Env-only: reviewer output budget, clamped to the 8192 floor |
| OUROBOROS_REVIEW_ENFORCEMENT | advisory | Review enforcement: advisory/blocking (closed enum; anything else coerces to the default) |
| OUROBOROS_REVIEW_ENFORCEMENT | advisory | Review enforcement: advisory/blocking (closed enum; anything else coerces to the default); plan-review closure of a below-quorum blocking finding by a reasoned reject reads this configured value, never the hurry-projected advisory the finalization gate uses |
| OUROBOROS_PREFLIGHT_TIMEOUT_SEC | 1800 | Env-only: TOTAL wall-clock budget for the hermetic test preflight (node lane + both passes); Android entry defaults to 3600, explicit override wins; teardown/containment remain in `preflight_runner.py`/`process_containment.py` |
| OUROBOROS_PREFLIGHT_SERIAL | unset | Env-only: `1` selects one serial pytest pass; scrubbed from the candidate environment |
| OUROBOROS_PREFLIGHT_TEST_WORKERS | (unset) | Env-only: xdist workers for the hermetic parallel pass (floor 2, else `os.cpu_count()`); read from the OPERATOR environment, scrubbed from the candidate |

View file

@ -517,6 +517,7 @@ def _typed_result_metadata(
):
meta["plan_review_outcome"] = plan_outcome
meta["plan_review_closed"] = plan_closed
meta["plan_review_historical_critic"] = bool(tool_result.meta.get("plan_review_historical_critic"))
if isinstance(tool_result, ToolResult) and tool_result.meta.get("post_commit_tests") == "failed":
# A preserved commit whose post-commit tests failed is a SUCCESS that
# still holds a failure the reflection triggers must see. The producer

View file

@ -464,9 +464,9 @@ def plan_wave_only_awaited(wave: Any) -> bool:
(the census names no unresolved, uncollected, refused or failed slot), and the
recorded answers hold no verdict of their own beneath the stored placeholder. A
collected blocking or ``need_evidence`` finding keeps the wave open whatever the
awaited slots answer, and a quorum of answers that raised findings IS a critic
verdict the wait merely postpones — neither reads as a mere wait. A roster or a
quorum the typed facts cannot vouch for is never a mere wait either."""
awaited slots answer, so it never reads as a mere wait; notes are neutral (they
never move the verdict), so a quorum whose only findings are notes is still a mere
wait. A roster or a quorum the typed facts cannot vouch for is never a mere wait."""
from ouroboros.tools.plan_review_runtime import plan_wave_slot_census
if not isinstance(wave, dict) or not wave.get("custody_pending"):
@ -480,10 +480,8 @@ def plan_wave_only_awaited(wave: Any) -> bool:
return False
counts = wave.get("counts") if isinstance(wave.get("counts"), dict) else {}
quorum = counts.get("quorum")
if (type(quorum) is not int or quorum <= 0 or not isinstance(findings, list)
or any(not isinstance(item, dict) or item.get("class") != "note" for item in findings)):
return False
return len(census["answered"]) < quorum or not findings
return not (type(quorum) is not int or quorum <= 0 or not isinstance(findings, list)
or any(not isinstance(item, dict) or item.get("class") != "note" for item in findings))
def plan_review_class_facts(wave: Any, *, awaited: bool) -> Dict[str, Any]:

View file

@ -159,7 +159,6 @@ BAND_PATHS = {
"ouroboros/tools/control_task_results.py": "serial addressed turns: await_messages (the mailbox wait, its window bounds and catalog entry) lives beside wait_task/wait_tasks, whose transport-wait peek and cache-horizon note it shares; splitting the three waits would separate one reader from its consumers",
"ouroboros/tools/core.py": "D05 ledger split (rows 311-349): read/list and owner-chat delivery spans moved to core_file_tools/core_artifacts; facade re-enters the band from above (2283 -> 1373) and shrinks further when the residual catalog split lands",
"ouroboros/tools/plan_review.py": "Entered the band from 999 lines: the required-affected_paths form (owner 9=A) added the schema field and the PLAN_RESOURCE_FORM_REQUIRED refusal, which must name the task's open wave and the $0 disposition exit \u2014 it belongs beside the one preamble both the paid and dry-run paths share, not in the pure plan_spec companion that owns no task state.",
"ouroboros/tools/plan_review_runtime.py": "Entered the band from 986 lines: timeout custody synthesis joined the existing plan-review runtime owner while preserving profile-continuity disclosures and typed health facts during target integration.",
"ouroboros/tools/registry_core.py": "ToolRegistry owns the registry class behind the protected facade; guard and dispatch implementations live in sibling leaves.",
"ouroboros/tools/review.py": "D06 F2.3a re-entry by extraction: the multi-model fan-out moved to review_multi_model.py (1550->1269); the remaining single-owner review cycle machinery lands in the 1001-1500 band with headroom",
"ouroboros/tools/skill_exec.py": None,

View file

@ -1478,6 +1478,12 @@ def record_plan_review_wave(
return state
waves = [w for w in state.get("waves") or [] if str(w.get("request_fingerprint") or "") != fingerprint]
recorded = copy.deepcopy(wave)
# A paid re-dispatch that REPLACES its same-fingerprint predecessor keeps that
# predecessor's exact artifact reachable (standing findings, prior cycles, facts).
for prior in previous:
ref = prior.get("wave_artifact") if prior.get("cycle_index") != wave.get("cycle_index") else prior.get("previous_wave_artifact")
if isinstance(ref, dict) and ref and not recorded.get("previous_wave_artifact"):
recorded["previous_wave_artifact"] = copy.deepcopy(ref)
history = [item for prior in previous for item in prior.get("historical_supplements") or []]
if history:
recorded["historical_supplements"] = copy.deepcopy(history)

View file

@ -278,6 +278,7 @@ def _next_step(wave: dict, *, enforcement: str, cap: Optional[int], cycles_paid:
text += (
f"NOTE: {len(blocking)} BLOCKING finding(s) below quorum ({ids}): a reject with its "
"rationale closes each one; accept or defer keeps it open until a changed spec is reviewed. "
+ ("" if not at_cap else "The cycle cap is reached; no further paid panel is available. ")
)
else:
text += (
@ -415,7 +416,8 @@ def _render_wave(
+ (f" · room snapshot read {a['room_read_coverage'].get('covered_chars')}/{a['room_read_coverage'].get('complete_chars')} "
f"chars ({a['room_read_coverage'].get('provenance')})" if isinstance(a.get("room_read_coverage"), dict) else "")
+ f" · {_actor_outcome(a, slot_class.get(id(a), ''))}"
+ (" · did not answer; its earlier finding is still listed" if a.get("carried_findings") else "")
+ ((" · not sent" if a.get("operation_state") == "not_dispatched" else " · did not answer")
+ "; its earlier finding is still listed" if a.get("carried_findings") else "")
+ (f" · disclosures: {', '.join(a['disclosures'])}" if a.get("disclosures") else "")
for a in wave.get("actors") or []
] or ["(no actor records)"]

View file

@ -277,7 +277,13 @@ def in_flight_resume_inputs(
)}
previous = None
previous_fingerprint = str(existing.get("previous_fingerprint") or "")
if previous_fingerprint:
replaced = existing.get("previous_wave_artifact") if isinstance(existing.get("previous_wave_artifact"), dict) else {}
if replaced: # a same-fingerprint re-dispatch replaced its predecessor in the hot index: read the exact copy
try:
previous = read_wave(state_root, task_id, replaced)
except (OSError, ValueError, json.JSONDecodeError):
return {"error": "Prior exact plan-review authority is unreadable; in-flight reconciliation is refused."}
elif previous_fingerprint:
from ouroboros.task_results import plan_review_wave
previous = plan_review_wave(state, previous_fingerprint)
@ -663,7 +669,7 @@ def compact_wave(wave: Dict[str, Any]) -> Dict[str, Any]:
"closed": bool(wave.get("closed")),
"paid": bool(wave.get("paid")),
"wave_artifact": copy.deepcopy(wave.get("wave_artifact") or {}),
**{key: copy.deepcopy(wave[key]) for key in ("historical_supplements", "retry_key", "custody_pending", "ordered_weaker") if key in wave},
**{key: copy.deepcopy(wave[key]) for key in ("historical_supplements", "retry_key", "custody_pending", "ordered_weaker", "previous_wave_artifact") if key in wave},
**({"author_disposition": copy.deepcopy(wave["author_disposition"])}
if isinstance(wave.get("author_disposition"), dict) else {}),
**({"spec_source_ref": copy.deepcopy(wave["spec_source_ref"])} if wave.get("spec_source_ref") else {}),

View file

@ -631,9 +631,6 @@ def synthesize_plan_review_wave(
slot = slots_by_id.get(sid)
if reviewer_effort and slot is not None and not str(getattr(slot, "declared_effort", "") or ""):
disclosures.append("reviewer_effort_not_applied") # a compound route slug kept its encoded effort
carried = [] if ok else [dict(f) for f in (standing or {}).get(sid) or [] if isinstance(f, Mapping)]
if carried:
disclosures.append(f"findings_carried_absent_answer:{len(carried)}")
if ok:
parsed, parse_error = plan_spec.parse_findings(str(row.get("text") or ""))
if parse_error:
@ -645,6 +642,14 @@ def synthesize_plan_review_wave(
)
disclosures += finding_disclosures
seen_after |= set(slot_seen)
# Standing findings ride only a TERMINAL absence, judged once ``ok`` is final: an
# unparseable reply is a non-answer, while a seat still awaiting the barrier, in
# flight or late-pending is a gap, never an answer (06 §Plan construction and review).
pending = bool(row.get("late_result_pending")) or str(row.get("operation_state") or "settled") in (
"pending_dispatch", "in_flight", "custody_lost")
carried = [] if ok or pending else [dict(f) for f in (standing or {}).get(sid) or [] if isinstance(f, Mapping)]
if carried:
disclosures.append(f"findings_carried_absent_answer:{len(carried)}")
slot_results.append({"slot": row.get("slot_id"), "model": row.get("model"),
"ok": ok, "findings": findings, "error": error or None,
**({"carried": carried} if carried else {})})
@ -1163,6 +1168,7 @@ def plan_health_skip_rows(slots: list, evidence: Optional[Dict[str, Dict[str, st
"model": str(getattr(slot, "model", "") or ""),
"request_model": str(getattr(slot, "model", "") or ""),
"route": "agent_session", "host_file_read_attestation": None, "text": "",
"effort": str(getattr(slot, "effort", "") or ""), "declared_effort": str(getattr(slot, "declared_effort", "") or ""),
"error": (
f"health_skip[{code}]: the pre-fan-out panel health snapshot shows this "
f"slot's delegated route window spent{f' (resets {reset})' if reset else ''}; "
@ -1448,6 +1454,7 @@ def plan_slot_fit(slots: list, *, prompt_chars: int, quorum: int, slot_prompt_ch
"request_model": str(getattr(slot, "model", "") or ""),
"route": "agent_session" if slot_is_session(slot) else "api_chat",
"host_file_read_attestation": None, "text": "",
"effort": str(getattr(slot, "effort", "") or ""), "declared_effort": str(getattr(slot, "declared_effort", "") or ""),
"error": (f"preflight_oversize: assembled packet ~{estimated:,} estimated tokens exceeds "
f"this slot's calibrated input cap {cap:,}"),
"prompt_ref": {}, "response_ref": {}, "tokens_in": 0, "tokens_out": 0, "cost": 0.0,

View file

@ -960,7 +960,7 @@ def plan_standing_findings(previous: Optional[dict], spec: dict, enforcement: st
order, so a seat that fails to answer a same-spec cycle keeps its objection listed
(``plan_review_runtime.synthesize_plan_review_wave``). A changed spec, a closed predecessor or no
predecessor carries nothing."""
if not isinstance(previous, dict) or not isinstance(previous.get("spec"), dict):
if not isinstance(previous, dict) or not isinstance(previous.get("spec"), dict) or previous.get("closed"):
return {}
if str(previous.get("spec_hash") or spec_hash(previous["spec"])) != spec_hash(spec):
return {}

View file

@ -70,10 +70,10 @@ ONLY_AWAITED = {
[_row("s1", ok=True), _awaiting("s2"), _awaiting("s3")], answered=1, findings=[_NOTE]),
"a clean quorum held open by the last slot": _wave(
[_row("s1", ok=True), _row("s2", ok=True), _awaiting("s3")], answered=2),
"a quorum whose only findings are notes: notes never move the verdict": _wave(
[_row("s1", ok=True), _row("s2", ok=True), _awaiting("s3")], answered=2, findings=[_NOTE]),
}
NOT_ONLY_AWAITED = {
"a quorum of answers raised findings: a critic verdict sits beneath the placeholder": _wave(
[_row("s1", ok=True), _row("s2", ok=True), _awaiting("s3")], answered=2, findings=[_NOTE]),
"a wait beside a real failure": _wave(
[_row("s1", failure_code="run_failed"), _awaiting("s2"), _awaiting("s3")]),
"a wait beside a typed $0 refusal": _wave(

View file

@ -868,6 +868,84 @@ def test_a_compound_route_slug_keeps_its_effort_and_discloses_the_unapplied_orde
assert all(a["declared_effort"] == "" for a in _state(harness, "task-plain")["waves"][-1]["actors"])
def test_a_reject_closed_predecessor_carries_nothing_on_a_later_same_spec_wave(harness, monkeypatch):
"""A below-quorum blocking finding closed by a reasoned reject under advisory is earned
authority: a later same-spec re-dispatch with its objecting seat silent carries nothing
from that CLOSED predecessor (an OPEN one carries it: the tests below)."""
from tests.test_plan_review_reconciliation import _collect
monkeypatch.setenv("OUROBOROS_REVIEW_MAX_CYCLES", "5")
harness.state["enforcement"] = "advisory"
_patch_health(monkeypatch, lambda slots: {})
_effort_aware_builder(harness, monkeypatch)
objection = json.dumps([_finding("n1", "blocking", breaks="claim_1", summary="Friday is impossible")])
sub = harness.install({"s1": objection, "s2": CLEAN, "s3": CLEAN})
ctx = harness.make_ctx()
assert _control(_call(ctx, reviewer_effort="low")) == {"outcome": "REVIEW_REQUIRED", "closed": False}
fp = _state(harness)["waves"][-1]["request_fingerprint"]
closed = _collect(ctx, fp, items=[{"finding_id": "s1:n1", "decision": "reject", "rationale": "Friday is a hard date"}])
assert _control(closed) == {"outcome": "GREEN", "closed": True}
sub.answers = {"s1": "", "s2": CLEAN, "s3": CLEAN}
later = _call(ctx, reviewer_effort="max")
assert _control(later) == {"outcome": "GREEN", "closed": True}
assert not any(f.get("carried_absent_answer") for f in _state(harness)["waves"][-1]["findings"])
def test_an_unparseable_objector_reply_still_carries_its_finding(harness, monkeypatch):
"""Standing findings are judged once ``ok`` is final: an objecting seat that answers prose
(no findings array) on a same-spec re-dispatch is a non-answer, so its earlier blocking
finding stays listed and the wave stays REVIEW_REQUIRED; a clean answer retires it."""
monkeypatch.setenv("OUROBOROS_REVIEW_MAX_CYCLES", "5")
_patch_health(monkeypatch, lambda slots: {})
_effort_aware_builder(harness, monkeypatch)
objection = json.dumps([_finding("n1", "blocking", breaks="claim_1", summary="Friday is impossible")])
sub = harness.install({"s1": objection, "s2": CLEAN, "s3": CLEAN})
ctx = harness.make_ctx()
assert _control(_call(ctx, reviewer_effort="low")) == {"outcome": "REVIEW_REQUIRED", "closed": False}
sub.answers = {"s1": "I have nothing to add this round.", "s2": CLEAN, "s3": CLEAN}
prose = _call(ctx, reviewer_effort="max")
assert _control(prose) == {"outcome": "REVIEW_REQUIRED", "closed": False}
wave = _state(harness)["waves"][-1]
s1 = next(a for a in wave["actors"] if a["slot_id"] == "s1")
assert s1["ok"] is False and s1["carried_findings"] == 1
assert [f["finding_id"] for f in wave["findings"] if f.get("carried_absent_answer")] == ["s1:n1"]
assert wave["counts"]["parseable"] == 2 and "did not answer; its earlier finding is still listed" in prose
sub.answers = {"s1": CLEAN, "s2": CLEAN, "s3": CLEAN}
assert _control(_call(ctx, reviewer_effort="xhigh")) == {"outcome": "GREEN", "closed": True}
def test_a_seat_awaiting_the_barrier_carries_nothing_until_its_absence_is_terminal(harness, monkeypatch):
"""At the dispatch barrier every fresh row is ``pending_dispatch``: a gap, never a
non-answer, so no standing finding is carried and nothing says «did not answer»;
once collection settles the objecting seat as a $0 ``not_dispatched`` refusal, its
earlier finding is carried and the actor line says «not sent»."""
from tests.test_plan_review_reconciliation import _collect, _install_barrier_substrate
monkeypatch.setenv("OUROBOROS_REVIEW_MAX_CYCLES", "5")
_patch_health(monkeypatch, lambda slots: {})
_effort_aware_builder(harness, monkeypatch)
objection = json.dumps([_finding("n1", "blocking", breaks="claim_1", summary="Friday is impossible")])
harness.install({"s1": objection, "s2": CLEAN, "s3": CLEAN})
ctx = harness.make_ctx()
assert _control(_call(ctx, reviewer_effort="low")) == {"outcome": "REVIEW_REQUIRED", "closed": False}
calls = []
_install_barrier_substrate(monkeypatch, calls, refused={"s1"})
barrier = _call(ctx, reviewer_effort="max")
assert _control(barrier) == {"outcome": "DEGRADED", "closed": False}
wave = _state(harness)["waves"][-1]
assert wave["custody_pending"] is True
assert not any(f.get("carried_absent_answer") for f in wave["findings"])
assert all(not a.get("carried_findings") for a in wave["actors"])
assert "earlier finding is still listed" not in barrier
settled = _collect(ctx, wave["request_fingerprint"])
assert _control(settled) == {"outcome": "REVIEW_REQUIRED", "closed": False}
wave = _state(harness)["waves"][-1]
s1 = next(a for a in wave["actors"] if a["slot_id"] == "s1")
assert s1["operation_state"] == "not_dispatched" and s1["carried_findings"] == 1
assert [f["finding_id"] for f in wave["findings"] if f.get("carried_absent_answer")] == ["s1:n1"]
assert "not sent; its earlier finding is still listed" in settled
def test_a_changed_order_never_retires_a_silent_objectors_finding(harness, monkeypatch):
"""Standing findings by same spec hash and same seat: after s1 raised a blocking
finding at `low`, a `max` re-dispatch on the SAME spec where s1 fails to answer
@ -899,10 +977,16 @@ def test_a_changed_order_never_retires_a_silent_objectors_finding(harness, monke
sub.answers = {"s1": CLEAN, "s2": CLEAN, "s3": CLEAN}
assert _control(_call(ctx, reviewer_effort="xhigh")) == {"outcome": "GREEN", "closed": True}
assert not any(f.get("carried_absent_answer") for f in _state(harness)["waves"][-1]["findings"])
# A changed spec is a fresh judgement: s1's silence there carries nothing.
# A changed spec is a fresh judgement: on a second task whose wave is OPEN on s1's
# objection, the changed spec with s1 silent carries nothing (the guard is spec-hash
# equality), while the same silence on the unchanged spec carries it (above).
other = harness.make_ctx(task_id="task-2")
sub.answers = {"s1": objection, "s2": CLEAN, "s3": CLEAN}
assert _control(_call(other, reviewer_effort="low")) == {"outcome": "REVIEW_REQUIRED", "closed": False}
sub.answers = {"s1": "", "s2": CLEAN, "s3": CLEAN}
changed = _call(ctx, spec={**DECK_SPEC, "in_scope": ["a 6-slide deck"]}, reviewer_effort="low")
changed = _call(other, spec={**DECK_SPEC, "in_scope": ["a 6-slide deck"]}, reviewer_effort="low")
assert _control(changed) == {"outcome": "GREEN", "closed": True}
assert not any(f.get("carried_absent_answer") for f in _state(harness, "task-2")["waves"][-1]["findings"])
assert "did not answer; its earlier finding is still listed" not in changed

View file

@ -232,7 +232,8 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
# observed-source read facts; the capture paragraph gains the snapshot-vs-inline clause.
# 320500 -> 321400 (merge of the moved target into the plan-review branch, measured 321261): both
# sides' replaced paragraphs land together; no text was appended by the merge itself.
"docs/architecture/06-agent-core.md": 321400,
# 321400 -> 321600 (measured 321394): the closure sentence names the CONFIGURED enforcement against the hurry-projected advisory.
"docs/architecture/06-agent-core.md": 321600,
# 36991 -> 37300: the facade paragraph names the three loop constants runtime_limits.py
# gained (events batch bound, budget-projection retry interval); no older text to displace.
# 37300 -> 38400 (PR #1207): the Z.ai (`zai::`) direct provider gets its own route
@ -241,7 +242,8 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
# 38400 -> 38700 (PR #1300): one settings row for the extra-CA trust bundle; the base sat 33 bytes under.
# 38700 -> 39000: OUROBOROS_EFFORT_REVIEW and the reviewer-slot row description name the plan
# order that outranks a pinned row effort (compound slugs keep theirs); the base sat 139 under.
"docs/architecture/07-configuration.md": 39000,
# 39000 -> 39100 (measured 38988): the review-enforcement row names what plan-review closure reads.
"docs/architecture/07-configuration.md": 39100,
# 18947 -> 19287: CI failure collection now documents diagnostic desktop builds while release remains gated.
# 19287 -> 20560 (#1215): three contracts the chapter had no older text for — the
# ONE reusable browser lane and the two triggers that share it (the unfiltered

View file

@ -533,18 +533,18 @@ function planFindingLines(wave) {
function planActorAvailabilityLines(wave) {
// The bug report's own bar: a result that was never received must say so
// explicitly instead of contributing silently-zero findings. A row names the
// model and quotes the engine's reported sentence; the failure code and the
// slot id stay in the task detail and Logs (an unresolved slot keeps its raw state).
// explicitly instead of contributing silently-zero findings. A row names the model and
// quotes the engine's sentence; failure code and slot id stay in the task detail and Logs.
const lines = [];
for (const actor of (Array.isArray(wave.actors) ? wave.actors : [])) {
if (!actor || typeof actor !== 'object' || actor.ok !== false) continue;
const model = text(actor.model) || 'reviewer';
const cause = text(actor.reported_cause).split(/\s+/).join(' ');
const carried = finiteCount(actor.carried_findings) ? '; its earlier finding is still listed' : '';
if (actorAwaiting(actor)) lines.push(`${model} · awaiting${sinceLocalTime(actor.awaiting_since)}`);
else if (actorUnresolved(actor)) lines.push(`${model} · no answer${cause ? ` — "${cause}"` : ''}${sinceLocalTime(actor.awaiting_since)}`);
else if (text(actor.operation_state) === 'not_dispatched') lines.push(`${model} · not sent`);
else lines.push(`${model} · unavailable${cause ? ` — "${cause}"` : ''}${finiteCount(actor.carried_findings) ? ' · did not answer; its earlier finding is still listed' : ''}`);
else if (text(actor.operation_state) === 'not_dispatched') lines.push(`${model} · not sent${carried}`);
else lines.push(`${model} · unavailable${cause ? ` — "${cause}"` : ''}${carried ? ` · did not answer${carried}` : ''}`);
}
return lines;
}
@ -561,6 +561,14 @@ function planWaveDetail(wave) {
wave.reason ? `Reason: ${text(wave.reason)}` : '',
];
const counts = wave.counts && typeof wave.counts === 'object' ? wave.counts : {};
// A panel ordered weaker than the owner's setting says so seat by seat (typed fact; the
// verdict token is never recoloured), on compact waves too since compaction keeps the fact.
const weaker = wave.ordered_weaker && typeof wave.ordered_weaker === 'object'
? Object.entries(wave.ordered_weaker).filter(([, row]) => row && typeof row === 'object') : [];
if (weaker.length) {
lines.push(`Reviewers ordered weaker than your setting: ${weaker
.map(([sid, row]) => `${sid} ${text(row.effort) || '?'} (setting ${text(row.owner_effort) || '?'})`).join(', ')}`);
}
if (wave.compact) {
// A compacted wave keeps counts while its finding bodies moved to the
// immutable wave artifact; name that remainder instead of rendering a
@ -580,14 +588,6 @@ function planWaveDetail(wave) {
.filter((key) => finiteCount(counts[key]) != null)
.map((key) => `${finiteCount(counts[key])} ${key}`);
if (countParts.length) lines.push(`Findings: ${countParts.join(' · ')}`);
// A panel the mind ordered weaker than the owner's effort setting says so, seat by
// seat, from the wave's typed fact; the verdict token is never recoloured for it.
const weaker = wave.ordered_weaker && typeof wave.ordered_weaker === 'object'
? Object.entries(wave.ordered_weaker).filter(([, row]) => row && typeof row === 'object') : [];
if (weaker.length) {
lines.push(`Reviewers ordered weaker than your setting: ${weaker
.map(([sid, row]) => `${sid} ${text(row.effort) || '?'} (setting ${text(row.owner_effort) || '?'})`).join(', ')}`);
}
lines.push(...planFindingLines(wave));
lines.push(...planActorAvailabilityLines(wave));
const findingsShown = (Array.isArray(wave.findings) ? wave.findings : []).length;

View file

@ -351,3 +351,48 @@ test('the hydrate status node swaps its message text across the loading→error
assert.match(current.children[0].innerHTML, /Loading review details/);
assert.equal(current.children.length, 1);
});
test('a compact Plan wave still names reviewers ordered weaker than the setting', () => {
const fingerprint = 'c'.repeat(64);
const group = planReviewGroupFromTaskDetail({
task_id: 'root',
plan_review_state: {
schema_version: 2,
current_attempt: {},
waves: [{
compact: true, request_fingerprint: fingerprint, cycle_index: 1, aggregate: 'GREEN', closed: true,
counts: { findings: 0, blocking: 0, dispositions: 0 },
wave_artifact: { root: 'artifact_store', path: 'w.json', sha256: 'abc123def4567890', bytes: 321 },
ordered_weaker: { slot_1: { effort: 'low', owner_effort: 'xhigh' } },
}],
waves_omitted: 0,
},
});
const detail = group.attempts[0].detailText;
assert.match(detail, /Reviewers ordered weaker than your setting: slot_1 low \(setting xhigh\)/);
assert.match(detail, /Finding bodies compacted/);
});
test('a carried finding is explained on a not-sent seat and on a failed seat alike', () => {
const fingerprint = 'e'.repeat(64);
const detail = (actor) => planReviewGroupFromTaskDetail({
task_id: 'root',
plan_review_state: {
schema_version: 2,
current_attempt: { fingerprint, status: 'open' },
waves: [{
request_fingerprint: fingerprint, cycle_index: 2, aggregate: 'REVIEW_REQUIRED', closed: false, paid: true,
counts: { blocking: 1, note: 0, need_evidence: 0, parseable: 2, quorum: 2 }, findings: [],
actors: [{ slot_id: 'slot_1', model: 'm/a', ...actor }],
}],
waves_omitted: 0,
},
}).attempts[0].detailText;
assert.match(detail({ ok: false, operation_state: 'not_dispatched', error: 'health_skip', carried_findings: 1 }),
/m\/a · not sent; its earlier finding is still listed/);
assert.match(detail({ ok: false, error: 'transport died', reported_cause: 'transport died', carried_findings: 1 }),
/m\/a · unavailable — "transport died" · did not answer; its earlier finding is still listed/);
assert.doesNotMatch(detail({ ok: false, operation_state: 'not_dispatched', error: 'health_skip' }), /earlier finding/);
assert.doesNotMatch(detail({ ok: false, error: 'transport died' }), /earlier finding/);
});