mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-02 19:58:46 +00:00
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:
parent
0caac10e9e
commit
086241ed51
16 changed files with 188 additions and 38 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)"]
|
||||
|
|
|
|||
|
|
@ -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 {}),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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/);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue