mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
plan-review: facts-only next step and schema; escalation stays an answer
The mind-facing texts promised mechanics that did not exist or advised a route of the host's choosing: "an answer reaches reviewers on the next paid cycle" (true only if the mind bought one, and the automatic delta that backed the REVISE_PLAN clause is gone), "a revised envelope ... its open requests can no longer be dispositioned", the deadline rail's "Proceed with your own best plan directly", and the cap rail's Swarm/hurry escape recipe. _next_step now states facts in every open branch: the open questions by id (questions to the author and document requests, from the ONE closure table, bounded to 8 ids), and the route an answer takes — recorded at $0 and merged by finding_id; beside the unchanged envelope only the named slots are asked again (one paid cycle, the rest kept at $0) and a slot that no longer raises its finding retires it; beside a changed envelope every slot reviews with the answers in view; whether to buy that cycle is the mind's; an envelope without items supersedes the wave with its answers kept, the identical one replays free. The below-quorum blocking sentence follows the configured enforcement (advisory: a reasoned reject closes; blocking: open until the raising slot retires it or a changed spec is reviewed). The Cyber branch carries the same facts for an open wave. At the cap the answer stays recorded evidence; the blocking tail names the released finalization, the held implementation and the owner's authority (raising the cap, an unstick) as owner authority, never reviewer approval. The deadline rail reports remaining time and the absent review; the cap head reports the open review, the released finalization and the owner's authority. The review_disposition schema, its items/rationale descriptions, the plan_task description and the reviewer_effort note state the same mechanism; the escalated question is documented as an answer the mind records (defer while the quiz is open, accept with the owner's decision) with no new decision value and no host shortcut. The blocking reminder states the route. Docs: CHECKLISTS "Cycles and closure" (REVISE_PLAN bullet and the replay paragraph) under the Communication decision; 06-agent-core (continuation per slot, answers merge by id, the addressed answer replaces the automatic delta, the escalated-question rule, the author call's items); handbook 06 gains the answer-channel bullet; the two chapter budgets are raised in this diff with their reasons. Owner authority: DECISIONS "Communication" item 2; D4 with Q-v = A; "Blocking installs: no skip"; Q-iv (facts) and the round-2 amendments (facts-only producers in every branch). Tests: tests/test_plan_review_answer_texts.py (open questions by id, present and absent; the route in every open branch and forbidden stale sentences; the cap texts; the schema and tool descriptions; the blocking reminder; the deadline and cap producers; a quiz answer never closes a finding while a later accept does); the render pins re-pinned; two w3 pins moved to the facts wording. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
parent
26f26e9a82
commit
ad034ce0a0
11 changed files with 312 additions and 99 deletions
|
|
@ -886,8 +886,10 @@ authority, and prose outside the array is not parsed.
|
|||
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
|
||||
with a rationale that rides into that next cycle, where reviewers mark it resolved or still open.
|
||||
either changes the spec (a new fingerprint, the next paid cycle) or answers a blocking finding
|
||||
and sends the unchanged envelope with that answer, which asks again only the slot that raised
|
||||
it (one paid cycle; every other slot keeps its recorded answer at $0) so it marks the finding
|
||||
resolved or still open.
|
||||
- **DEGRADED** — no parseable quorum. Not a verdict, but the dispatched panel PAID its cycle:
|
||||
the wave records OPEN with each slot's typed failure state (code and reset time when known),
|
||||
the control line reports DEGRADED honestly, and the recorded result replays for free ONLY
|
||||
|
|
@ -905,9 +907,12 @@ authority, and prose outside the array is not parsed.
|
|||
stays held), waiting via a one-shot `schedule_followup` and asking the owner stay open too.
|
||||
|
||||
Paid cycles per task are bounded by the owner's `OUROBOROS_REVIEW_MAX_CYCLES` (default 2,
|
||||
`unlimited` available). Replaying an identical envelope is free — identical including the
|
||||
evidence the host attaches for reviewers' `need_evidence` requests, so a request received in the
|
||||
last cycle makes the next envelope a new one. On cycle 2+ every reviewer sees
|
||||
`unlimited` available). Replaying an identical envelope without answers is free — identical
|
||||
including the evidence the host attaches for reviewers' `need_evidence` requests, so a request
|
||||
received in the last cycle makes the next envelope a new one. Answers merge by `finding_id`
|
||||
across calls; the identical envelope sent WITH `review_disposition` items is the addressed
|
||||
re-ask (one paid cycle for the named slots only), and no host path buys a panel the agent did
|
||||
not send. On cycle 2+ every reviewer sees
|
||||
all reviewers' findings from the previous cycle, the agent's dispositions and the spec delta, and
|
||||
its first duty is to adjudicate its OWN earlier findings (the rows whose finding_id starts with its
|
||||
panel seat): RESOLVED (the delta or the rationale answers it) and SUPERSEDED (the element it targeted
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1092,6 +1092,14 @@ and what enforces each.
|
|||
author completion separately, never rewriting criticism or hiding independent failed
|
||||
effects, unaccepted review or unfinished stops (DEVELOPMENT §11; ARCHITECTURE §3).
|
||||
No task scope review or commit-gate reuse.
|
||||
- Plan-review answers are durable statements merged by `finding_id`: a later answer
|
||||
supersedes only its own id, and two entries for one id in ONE call stay contradictory and
|
||||
open. Answering and re-asking are one optional call: an envelope with items records them
|
||||
first, then reviews; the unchanged envelope asks only the named slots, and every other
|
||||
slot's recorded row is replayed at $0 (`not_dispatched` + `replayed_from`, never a send),
|
||||
keyed by fingerprint equality. An addressed slot without a parseable answer keeps its
|
||||
findings; a quiz answer never closes a finding; no host path buys a panel the mind did
|
||||
not send (`tests/test_plan_review_answer_channel.py`).
|
||||
- Keep reviewer DIALOGUE evidence: typed `disposition_kind`/`obligation_id` identifies
|
||||
obligations; disclose unknown re-raise ids as `new`. Reopen rows with
|
||||
arguments intact. A terminal critic vote cannot deny author reaction or choose its stop. Blocking may save corrections and stop; advancement needs fresh
|
||||
|
|
|
|||
|
|
@ -731,17 +731,19 @@ def plan_review_reminder(decision: Dict[str, Any]) -> str:
|
|||
)
|
||||
if outcome == "REVIEW_REQUIRED":
|
||||
return (
|
||||
f"{tag} Blocking plan review remains REVIEW_REQUIRED. Re-call plan_task with a "
|
||||
"complete review_disposition as the only field, naming the latest fingerprint, "
|
||||
"then continue; do not rerun reviewers."
|
||||
f"{tag} Blocking plan review remains REVIEW_REQUIRED. Open need_evidence requests close "
|
||||
"with a $0 review_disposition naming the latest fingerprint; a blocking finding below quorum "
|
||||
"stays open until its slot no longer raises it in a later paid cycle (the unchanged envelope "
|
||||
"with your items asks only that slot) or a changed spec is reviewed without it. "
|
||||
"Implementation stays held while the review is open."
|
||||
)
|
||||
if outcome == "REVISE_PLAN":
|
||||
return (
|
||||
f"{tag} Blocking plan review requires a revised spec. Change the spec — it carries "
|
||||
"affected_paths, the files the work will change ([] when none) — and call "
|
||||
"plan_task again (or reject the blocking findings with a rationale via "
|
||||
"review_disposition). Continue analysis and non-mutating preparation, but do not "
|
||||
"begin the work before the review closes or a real task-wide rail fires."
|
||||
f"{tag} Blocking plan review is REVISE_PLAN. A changed spec — with affected_paths, the files "
|
||||
"the work will change ([] when none) — is a new envelope every slot reviews; the unchanged "
|
||||
"envelope with review_disposition items asks only the slots those items name. Analysis and "
|
||||
"non-mutating preparation remain open; the work starts after the review closes or a real "
|
||||
"task-wide rail fires."
|
||||
)
|
||||
return (
|
||||
f"{tag} Call plan_task with a concrete goal, plan and spec, whose affected_paths lists "
|
||||
|
|
|
|||
|
|
@ -164,6 +164,47 @@ def _quorum_unreachable_fact(wave: dict) -> str:
|
|||
)
|
||||
|
||||
|
||||
def _open_questions_line(wave: dict, enforcement: str) -> str:
|
||||
"""The open ``need_evidence`` ids, split into questions to the author (no locator) and
|
||||
document requests (a locator), read from the ONE closure table; '' when none is open."""
|
||||
from ouroboros.tools import plan_spec
|
||||
|
||||
findings = [f for f in wave.get("findings") or [] if isinstance(f, dict)]
|
||||
open_ids = set(plan_spec.closure_after_disposition(
|
||||
str(wave.get("aggregate") or ""), findings, wave.get("dispositions") or [], enforcement)["open_ids"])
|
||||
asks = [f for f in findings if f.get("class") == "need_evidence"
|
||||
and str(f.get("finding_id") or f.get("id") or "") in open_ids]
|
||||
questions = [f for f in asks if not str(f.get("locator") or "").strip()]
|
||||
documents = [f for f in asks if str(f.get("locator") or "").strip()]
|
||||
|
||||
def named(rows: list, field: str) -> str:
|
||||
shown = [f"{f.get('finding_id') or f.get('id')} ({f.get(field) or '?'})" for f in rows[:8]]
|
||||
return ", ".join(shown) + (f", +{len(rows) - 8} more" if len(rows) > 8 else "")
|
||||
|
||||
return ((f"Open questions to you: {named(questions, 'breaks')}. " if questions else "")
|
||||
+ (f"Open document requests: {named(documents, 'locator')}. " if documents else ""))
|
||||
|
||||
|
||||
def _answer_route(fp: str, *, at_cap: bool, cycles_paid: int, cap: Optional[int]) -> str:
|
||||
"""The route an answer takes, as facts: recorded at $0 and merged by id; beside the
|
||||
unchanged envelope it asks again only the seats it names (one paid cycle, the rest kept at
|
||||
$0); beside a changed one every seat reviews with the answers in view. Whether to buy that
|
||||
cycle is the mind's; at the cap the answer stays recorded evidence."""
|
||||
recorded = (f"An answer is recorded at $0 by plan_task(review_disposition={{review_fingerprint: '{fp}', "
|
||||
"items: [...]}) (accept = answered, the rationale is the answer; reject; defer = deferred "
|
||||
"openly) and merged by finding_id")
|
||||
if at_cap:
|
||||
return recorded + ("; the cycle cap is reached, so no paid cycle remains to show it to a reviewer: "
|
||||
"it stays recorded evidence. ")
|
||||
return recorded + (
|
||||
". The same call beside the unchanged goal/plan/spec asks only the slots its items name — one paid "
|
||||
f"cycle {cycles_paid + 1}{'' if cap is None else f' of {cap}'}; every other slot keeps its recorded "
|
||||
"answer at $0 — and a slot that no longer raises its finding retires it; beside a changed "
|
||||
"goal/plan/spec every slot reviews the new envelope with the answers in view. Whether to buy that "
|
||||
"cycle is yours. An envelope sent without items supersedes this wave (its recorded answers stay); "
|
||||
"the identical one replays free. ")
|
||||
|
||||
|
||||
def _next_step(wave: dict, *, enforcement: str, cap: Optional[int], cycles_paid: int) -> str:
|
||||
aggregate = str(wave.get("aggregate") or "")
|
||||
fp = str(wave.get("request_fingerprint") or "")
|
||||
|
|
@ -187,8 +228,8 @@ def _next_step(wave: dict, *, enforcement: str, cap: Optional[int], cycles_paid:
|
|||
author_note + "Cyber Pro: Ouroboros decides whether and how to continue. "
|
||||
"The recorded verdict, open findings and any unresolved physical reviewers remain "
|
||||
"independent facts; continuation does not close the wave or create a PASS. "
|
||||
f"The existing $0 plan_task(review_disposition={{review_fingerprint: '{fp}', items: [...]}}) "
|
||||
"can collect results or record a disposition without a new panel."
|
||||
+ ("" if bool(wave.get("closed")) else
|
||||
_open_questions_line(wave, enforcement) + _answer_route(fp, at_cap=at_cap, cycles_paid=cycles_paid, cap=cap))
|
||||
)
|
||||
if bool(wave.get("closed")):
|
||||
if plan_review_notes_are_annotatable(wave):
|
||||
|
|
@ -262,47 +303,31 @@ def _next_step(wave: dict, *, enforcement: str, cap: Optional[int], cycles_paid:
|
|||
text += _quorum_unreachable_fact(wave)
|
||||
elif aggregate == "REVIEW_REQUIRED":
|
||||
blocking = [f for f in wave.get("findings") or [] if f.get("class") == "blocking"]
|
||||
text = author_note + (
|
||||
"Notes are optional. Open need_evidence requests (an evidence locator or "
|
||||
"a question addressed to you by spec id) close with ONE $0 call: "
|
||||
f"plan_task(review_disposition={{review_fingerprint: '{fp}', items: [...]}}) — accept = "
|
||||
"answered (your rationale is the answer; "
|
||||
+ ("the cap leaves no further paid cycle to deliver it to reviewers), " if at_cap else
|
||||
"it reaches reviewers on the next paid cycle), ")
|
||||
+ "reject, or defer = deferred openly; no reviewer call, no cycle. A revised envelope "
|
||||
"supersedes this wave and its open requests can no longer be dispositioned. "
|
||||
)
|
||||
if blocking:
|
||||
text = (author_note + "Notes are optional and need no answer. " + _open_questions_line(wave, enforcement)
|
||||
+ _answer_route(fp, at_cap=at_cap, cycles_paid=cycles_paid, cap=cap))
|
||||
if blocking: # the closure table's per-finding rule, stated as a fact
|
||||
ids = ", ".join(str(f.get("finding_id") or f.get("id")) for f in blocking[:4])
|
||||
if enforcement == "advisory": # the closure table's per-finding rule, stated as a fact
|
||||
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 += (
|
||||
f"NOTE: {len(blocking)} BLOCKING finding(s) below quorum ({ids}) stay OPEN whatever "
|
||||
"you disposition: a changed spec, or a justified rejection judged in another paid "
|
||||
"cycle, closes them. "
|
||||
+ ("" if not at_cap else "The cycle cap is reached; no further paid panel is available. ")
|
||||
)
|
||||
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 enforcement == "advisory" else
|
||||
f"NOTE: {len(blocking)} BLOCKING finding(s) below quorum ({ids}) stay open after any "
|
||||
"disposition; they leave the review when the slot that raised them no longer raises it in a "
|
||||
"later paid cycle, or when a changed spec is reviewed without them. "
|
||||
) + ("The cycle cap is reached; no further paid panel is available. " if at_cap else "")
|
||||
else:
|
||||
text = author_note + (
|
||||
"Blocking findings: accept ⇒ change the spec and re-call plan_task (new fingerprint, "
|
||||
f"{'the cap is reached — no further paid cycle' if at_cap else 'next paid cycle ' + str(cycles_paid + 1) + ('' if cap is None else f' of {cap}')}); "
|
||||
"reject ⇒ record reject + rationale via review_disposition naming this fingerprint — "
|
||||
+ ("it is recorded as evidence; the cap leaves no further paid delta cycle to judge it. "
|
||||
if at_cap else
|
||||
"it rides into the next paid delta cycle where reviewers mark it resolved or still-open. ")
|
||||
+ "A disposition never closes REVISE_PLAN. "
|
||||
)
|
||||
blocking = [f for f in wave.get("findings") or [] if f.get("class") == "blocking"]
|
||||
ids = ", ".join(str(f.get("finding_id") or f.get("id")) for f in blocking[:6])
|
||||
text = (author_note + f"Blocking findings at quorum{f' ({ids})' if ids else ''}. A disposition records "
|
||||
"an answer; a disposition never closes REVISE_PLAN. " + _open_questions_line(wave, enforcement)
|
||||
+ _answer_route(fp, at_cap=at_cap, cycles_paid=cycles_paid, cap=cap))
|
||||
if enforcement == "blocking":
|
||||
text += (
|
||||
_BLOCKING_HOLDS
|
||||
+ (" — the cycle cap is reached: exits are owner unstick (Swarm/hurry), a revised spec "
|
||||
"once the owner raises OUROBOROS_REVIEW_MAX_CYCLES, or finalizing with "
|
||||
"outcome_tier=blocked_with_evidence." if at_cap else ".")
|
||||
+ (" — the cycle cap is reached: no paid panel remains; finalization is RELEASED, and finalizing "
|
||||
"now records outcome_tier=blocked_with_evidence with the review left OPEN and implementation "
|
||||
"still held; a revised spec buys a paid cycle once the owner raises OUROBOROS_REVIEW_MAX_CYCLES "
|
||||
"(owner authority, never reviewer approval)." if at_cap else ".")
|
||||
)
|
||||
if wave.get("quorum_unreachable") and not bool(wave.get("closed")):
|
||||
# B2b facts, never imperatives: the honest exits that exist alongside
|
||||
|
|
|
|||
|
|
@ -230,15 +230,21 @@ _DISPOSITION_SCHEMA = {
|
|||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"description": (
|
||||
"Answer the findings of the wave (ordinary dispositions use only this field); "
|
||||
"explicit author_action=finish|stop with author_disposition may also select a full current goal/plan/spec without a new reviewer. The wave is "
|
||||
"named by review_fingerprint. While that wave is still open with reviewer slots in "
|
||||
"flight, this call first COLLECTS what has settled at $0 without waiting (items may be "
|
||||
"[]); to wait longer, re-submit the same envelope. Notes never hold the wave; need_evidence "
|
||||
"closes at $0; under advisory a reject with rationale also closes a below-quorum blocking "
|
||||
"finding; otherwise a blocking finding stays open. A subsequent paid delta review may consider a changed spec or "
|
||||
"justified rejection when another paid cycle is available. Recording a disposition "
|
||||
"consumes no cycle and never closes REVISE_PLAN."
|
||||
"Answer the findings of the wave named by review_fingerprint. Alone, this field records the "
|
||||
"answers at $0, merged by finding_id (a later answer to the same id supersedes the earlier one; "
|
||||
"every other answer stays). Beside goal/plan/spec the answers are recorded first and the envelope "
|
||||
"is then reviewed: the unchanged envelope asks again ONLY the slots whose findings the items name, "
|
||||
"in one paid cycle, and every other slot keeps its recorded answer at $0; a changed envelope is "
|
||||
"reviewed by every slot with the answers in view. With author_action=finish|stop the items are "
|
||||
"recorded and the current goal/plan/spec may be selected without a new reviewer. While the wave "
|
||||
"still has reviewer slots in flight this call first COLLECTS what has settled at $0 without "
|
||||
"waiting (items may be []); to wait longer, re-submit the same envelope. A note or need_evidence "
|
||||
"finding closes at $0 by its answer; a blocking finding stays open until the slot that raised it "
|
||||
"no longer raises it or a changed spec is reviewed without it (under advisory a reject with its "
|
||||
"rationale also closes a below-quorum blocking finding). Recording answers consumes no cycle and "
|
||||
"never closes REVISE_PLAN. A question you escalate to the owner (escalate) stays open until you "
|
||||
"record its answer here: defer (rationale names the quiz) while the quiz is open, accept with the "
|
||||
"owner's decision in the rationale once decided. A quiz answer never closes a finding by itself."
|
||||
),
|
||||
"properties": {
|
||||
"review_fingerprint": {"type": "string"},
|
||||
|
|
@ -247,12 +253,16 @@ _DISPOSITION_SCHEMA = {
|
|||
"description": "Default none means no author action, even if author_disposition is filled; collect or answer findings only. finish/stop explicitly select the current plan. stop permits unfinished finalization only; finish never overrides Blocking review."},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"description": ("Answers by finding_id. Every item names its reviewer slot; beside the unchanged "
|
||||
"envelope those slots are asked again."),
|
||||
"items": {
|
||||
"type": "object", "additionalProperties": False,
|
||||
"properties": {
|
||||
"finding_id": {"type": "string"},
|
||||
"decision": {"type": "string", "enum": list(plan_spec.DISPOSITION_DECISIONS)},
|
||||
"rationale": {"type": "string"},
|
||||
"rationale": {"type": "string", "description": (
|
||||
"Your answer. For an escalated question: the quiz id while it is open (defer), "
|
||||
"the owner's decision once answered (accept).")},
|
||||
},
|
||||
"required": ["finding_id", "decision", "rationale"],
|
||||
},
|
||||
|
|
@ -276,11 +286,14 @@ def get_tools():
|
|||
"no open need_evidence (notes never change the verdict); need_evidence closes by "
|
||||
"review_disposition at no cost; under advisory enforcement a reject with its rationale "
|
||||
"also closes a blocking finding below quorum; REVISE_PLAN needs "
|
||||
"a changed spec or justified rejection judged by a subsequent paid delta review "
|
||||
"when another paid cycle is available. Cycles are bounded by the owner's Max review cycles; an unchanged "
|
||||
"a changed spec, or your answer re-judged by the slot that raised the finding. "
|
||||
"Cycles are bounded by the owner's Max review cycles; an unchanged "
|
||||
"envelope replays the recorded result for free (a locator a reviewer asked for "
|
||||
"with need_evidence is attached by the host next time and makes the envelope "
|
||||
"new; on an OPEN review a different reviewer_effort re-dispatches the panel, a CLOSED review stands for its envelope). Under blocking enforcement an "
|
||||
"new; on an OPEN review a different reviewer_effort re-dispatches the panel, a CLOSED review stands for its envelope); "
|
||||
"the unchanged envelope together with review_disposition items records the answers and asks again "
|
||||
"only the slots those items name (one paid cycle; the others keep their answers at $0), while a "
|
||||
"changed envelope with items is reviewed by every slot. Under blocking enforcement an "
|
||||
"open review holds implementation. An explicit review_disposition.author_action=stop "
|
||||
"permits unfinished finalization only. Advisory author_action=finish may select a corrected "
|
||||
"goal+plan+spec in the same call without another panel, citing the earlier review_fingerprint "
|
||||
|
|
@ -298,7 +311,7 @@ def get_tools():
|
|||
"spec": _SPEC_SCHEMA,
|
||||
"reviewer_effort": {**_REVIEWER_EFFORT_SCHEMA,
|
||||
"enum": ["default", *_REVIEWER_EFFORT_SCHEMA["enum"]], "default": "default",
|
||||
"description": "default uses the configured effort without an override; ignored when collecting or answering a recorded wave. " + _REVIEWER_EFFORT_SCHEMA["description"]},
|
||||
"description": "default uses the configured effort without an override; ignored when only collecting a recorded wave. " + _REVIEWER_EFFORT_SCHEMA["description"]},
|
||||
"review_disposition": _DISPOSITION_SCHEMA,
|
||||
},
|
||||
# Review, disposition-only, or explicit author selection with a current envelope.
|
||||
|
|
@ -1037,11 +1050,12 @@ def _cycles_exhausted(
|
|||
)
|
||||
if review_enforcement_blocks(enforcement):
|
||||
head += (
|
||||
"Blocking enforcement: the plan review stays OPEN, so implementation stays held — but "
|
||||
"Blocking enforcement: the plan review stays OPEN, so implementation stays held — and "
|
||||
"finalization is RELEASED so the task can end honestly instead of waiting for a panel it "
|
||||
"can no longer buy (owner decision D27). Your exits are an owner unstick (Swarm/hurry), a "
|
||||
"revised spec once the owner raises OUROBOROS_REVIEW_MAX_CYCLES, or finalizing now with "
|
||||
"outcome_tier=blocked_with_evidence. Do not start the work under an open blocking review."
|
||||
"can no longer buy: finalizing now records outcome_tier=blocked_with_evidence with the "
|
||||
"review left open. Recorded answers stay recorded evidence. Available authority beyond "
|
||||
"yours: the owner may raise OUROBOROS_REVIEW_MAX_CYCLES (a revised spec then buys a paid "
|
||||
"cycle) or perform an owner unstick — owner authority, never reviewer approval."
|
||||
)
|
||||
elif not review_enforcement_blocks("blocking"):
|
||||
head += "Cyber Pro permits proceeding by Ouroboros's judgment; the open review and spent cycles remain recorded facts."
|
||||
|
|
|
|||
|
|
@ -315,8 +315,9 @@ def plan_deadline_skip(ctx: ToolContext, *, emit: bool = False) -> str:
|
|||
f"review window of {int(scaled)}s (< {int(minimum)}s useful floor)."
|
||||
)
|
||||
return (
|
||||
f"PLAN_TASK_SKIPPED_DEADLINE: {cause} Proceed with your own best plan "
|
||||
"directly; do not re-call plan_task under this deadline."
|
||||
f"PLAN_TASK_SKIPPED_DEADLINE: {cause} No reviewer was dispatched and no plan review is open "
|
||||
f"for this envelope; remaining time {max(0, int(remaining))}s. The same call under this deadline "
|
||||
"returns this rail again: the deadline is the owner's task bound, not a reviewer verdict."
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
155
tests/test_plan_review_answer_texts.py
Normal file
155
tests/test_plan_review_answer_texts.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
"""The mind-facing texts of the answer channel state FACTS in every branch: the open
|
||||
questions by id, the route an answer takes (recorded at $0, merged by id, the unchanged
|
||||
envelope asks only the named slots), the escalated question that stays open until its
|
||||
answer is recorded, and the deadline/cap producers that report state instead of advising
|
||||
a plan of their own. Each pin fails with the old wording restored."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from ouroboros.tools.plan_render import _next_step
|
||||
from tests.test_plan_review_engine import ( # noqa: F401
|
||||
CLEAN, _call, _control, _finding, _state, harness,
|
||||
)
|
||||
|
||||
FP = "f" * 64
|
||||
|
||||
|
||||
def _wave(findings: list, aggregate: str = "REVIEW_REQUIRED", dispositions: list | None = None) -> dict:
|
||||
return {"aggregate": aggregate, "closed": False, "request_fingerprint": FP,
|
||||
"findings": findings, "dispositions": dispositions or []}
|
||||
|
||||
|
||||
def _q(fid: str, breaks: str = "claim_1", locator: str = "") -> dict:
|
||||
return {"finding_id": fid, "slot": fid.split(":")[0], "class": "need_evidence", "breaks": breaks, "locator": locator}
|
||||
|
||||
|
||||
def test_next_step_names_open_questions_by_id():
|
||||
for aggregate in ("REVIEW_REQUIRED", "REVISE_PLAN"):
|
||||
findings = [_q("s1:q1"), _q("s2:q2", "goal"), _q("s3:e1", "claim_1", "notes.md::tail=16"),
|
||||
{"finding_id": "s1:f1", "slot": "s1", "class": "blocking", "breaks": "claim_1"}]
|
||||
text = _next_step(_wave(findings, aggregate), enforcement="blocking", cap=5, cycles_paid=1)
|
||||
assert "Open questions to you: s1:q1 (claim_1), s2:q2 (goal). " in text
|
||||
assert "Open document requests: s3:e1 (notes.md::tail=16). " in text
|
||||
# answered questions leave the line; with none open the line is absent (two-sided)
|
||||
answered = [{"finding_id": fid, "decision": "accept", "rationale": "answered"} for fid in ("s1:q1", "s2:q2", "s3:e1")]
|
||||
text = _next_step(_wave(findings, aggregate, answered), enforcement="blocking", cap=5, cycles_paid=1)
|
||||
assert "Open questions to you" not in text and "Open document requests" not in text
|
||||
many = [_q(f"s1:q{i}") for i in range(10)]
|
||||
text = _next_step(_wave(many), enforcement="advisory", cap=5, cycles_paid=1)
|
||||
assert "s1:q7 (claim_1), +2 more. " in text and "s1:q9" not in text
|
||||
|
||||
|
||||
def test_next_step_states_the_answer_route_in_every_open_branch(monkeypatch):
|
||||
from ouroboros.tools import plan_render
|
||||
|
||||
blocking = [{"finding_id": "s1:f1", "slot": "s1", "class": "blocking", "breaks": "claim_1"}]
|
||||
for aggregate in ("REVIEW_REQUIRED", "REVISE_PLAN"):
|
||||
text = _next_step(_wave(blocking, aggregate), enforcement="blocking", cap=5, cycles_paid=1)
|
||||
assert f"plan_task(review_disposition={{review_fingerprint: '{FP}', items: [...]}})" in text
|
||||
assert "asks only the slots its items name — one paid cycle 2 of 5" in text
|
||||
assert "every other slot keeps its recorded answer at $0" in text and "Whether to buy that cycle is yours" in text
|
||||
assert "supersedes this wave (its recorded answers stay); the identical one replays free" in text
|
||||
for stale in ("reaches reviewers on the next paid cycle", "rides into the next paid delta cycle",
|
||||
"can no longer be dispositioned", "Swarm", "accept ⇒ change the spec"):
|
||||
assert stale not in text, stale
|
||||
below = _next_step(_wave(blocking), enforcement="blocking", cap=5, cycles_paid=1)
|
||||
assert "stay open after any disposition; they leave the review when the slot that raised them no longer raises it" in below
|
||||
advisory = _next_step(_wave(blocking), enforcement="advisory", cap=5, cycles_paid=1)
|
||||
assert "a reject with its rationale closes each one; accept or defer keeps it open" in advisory
|
||||
# Cyber Pro: the facts and the route ride the branch too, never for a closed wave.
|
||||
monkeypatch.setattr(plan_render, "review_enforcement_blocks", lambda _mode: False)
|
||||
cyber = _next_step(_wave([_q("s1:q1")]), enforcement="blocking", cap=5, cycles_paid=1)
|
||||
assert cyber.startswith("Cyber Pro: Ouroboros decides") and "Open questions to you: s1:q1 (claim_1). " in cyber
|
||||
assert "An answer is recorded at $0" in cyber
|
||||
closed = _next_step({**_wave([]), "aggregate": "GREEN", "closed": True}, enforcement="blocking", cap=5, cycles_paid=1)
|
||||
assert "An answer is recorded" not in closed
|
||||
|
||||
|
||||
def test_cap_reached_texts_report_state_and_authority_without_an_escape_recipe():
|
||||
blocking = [{"finding_id": "s1:f1", "slot": "s1", "class": "blocking", "breaks": "claim_1"}]
|
||||
text = _next_step(_wave(blocking, "REVISE_PLAN"), enforcement="blocking", cap=2, cycles_paid=2)
|
||||
assert "no paid cycle remains to show it to a reviewer: it stays recorded evidence" in text
|
||||
assert "finalization is RELEASED" in text and "implementation still held" in text
|
||||
assert "owner authority, never reviewer approval" in text and "Swarm" not in text and "unstick" not in text
|
||||
|
||||
|
||||
def test_disposition_schema_states_the_answer_route_and_escalation():
|
||||
from ouroboros.tools.plan_review import _DISPOSITION_SCHEMA, get_tools
|
||||
|
||||
description = _DISPOSITION_SCHEMA["description"]
|
||||
assert "merged by finding_id" in description
|
||||
assert "the unchanged envelope asks again ONLY the slots whose findings the items name" in description
|
||||
assert "a changed envelope is reviewed by every slot with the answers in view" in description
|
||||
assert "A quiz answer never closes a finding by itself" in description
|
||||
assert "defer (rationale names the quiz) while the quiz is open, accept with the owner's decision" in description
|
||||
assert "subsequent paid delta review" not in description
|
||||
items = _DISPOSITION_SCHEMA["properties"]["items"]
|
||||
assert "those slots are asked again" in items["description"]
|
||||
assert "the quiz id while it is open (defer)" in items["items"]["properties"]["rationale"]["description"]
|
||||
tool = next(t for t in get_tools() if t.name == "plan_task")
|
||||
desc = tool.schema["description"]
|
||||
assert "your answer re-judged by the slot that raised the finding" in desc
|
||||
assert "asks again only the slots those items name" in desc and "subsequent paid delta" not in desc
|
||||
effort = tool.schema["parameters"]["properties"]["reviewer_effort"]["description"]
|
||||
assert "ignored when only collecting a recorded wave" in effort and "answering" not in effort
|
||||
|
||||
|
||||
def test_blocking_reminder_states_the_answer_route():
|
||||
from ouroboros.owner_hurry import plan_review_reminder
|
||||
|
||||
held = plan_review_reminder({"outcome": "REVIEW_REQUIRED", "status": "open", "cycles_paid": 1})
|
||||
assert "REVIEW_REQUIRED" in held and "the unchanged envelope with your items asks only that slot" in held
|
||||
assert "do not rerun reviewers" not in held and "Implementation stays held" in held
|
||||
revise = plan_review_reminder({"outcome": "REVISE_PLAN", "status": "open", "cycles_paid": 1})
|
||||
assert "asks only the slots those items name" in revise and "a new envelope every slot reviews" in revise
|
||||
assert "the work starts after the review closes or a real task-wide rail fires" in revise
|
||||
|
||||
|
||||
def test_deadline_and_cap_producers_state_facts_not_advice(harness, monkeypatch): # noqa: F811
|
||||
"""The deadline rail names remaining time and the absent review; the cap rail names the
|
||||
open review, the released finalization and the owner's authority — neither advises the
|
||||
mind to proceed with a plan of its own or to seek a reviewer-free escape."""
|
||||
from ouroboros.tools import plan_review as pr
|
||||
from ouroboros.deadline_utils import utc_now
|
||||
from ouroboros.tools.plan_review_runtime import plan_deadline_skip
|
||||
|
||||
ctx = harness.make_ctx()
|
||||
ctx.task_metadata["deadline_at"] = (utc_now()).isoformat()
|
||||
monkeypatch.setattr(pr, "_plan_deadline_skip", plan_deadline_skip)
|
||||
text = plan_deadline_skip(ctx)
|
||||
assert text.startswith("PLAN_TASK_SKIPPED_DEADLINE:") and "remaining time 0s" in text
|
||||
assert "no plan review is open" in text and "proceed with your own" not in text.lower()
|
||||
monkeypatch.setenv("OUROBOROS_REVIEW_MAX_CYCLES", "1")
|
||||
harness.install({"s1": json.dumps([_finding("f1", "blocking", breaks="claim_1")]), "s2": CLEAN, "s3": CLEAN})
|
||||
ctx2 = harness.make_ctx(task_id="task-cap")
|
||||
_call(ctx2)
|
||||
spent = _call(ctx2, plan="A revised outline.")
|
||||
assert "PLAN_REVIEW_CYCLES_EXHAUSTED" in spent and "finalization is RELEASED" in spent
|
||||
assert "outcome_tier=blocked_with_evidence" in spent and "owner authority, never reviewer approval" in spent
|
||||
assert "Swarm" not in spent and "Do not start the work" not in spent
|
||||
|
||||
|
||||
def test_quiz_answer_never_closes_a_finding(harness): # noqa: F811
|
||||
"""A doc-only rule with no host shortcut to revert: an owner quiz answered through
|
||||
owner_quiz leaves the plan-review state byte-identical and the question open; the mind's
|
||||
later accept citing the decision is what closes it (two-sided)."""
|
||||
from ouroboros import owner_quiz
|
||||
from ouroboros.task_results import load_plan_review_state
|
||||
from ouroboros.tools import plan_review as pr
|
||||
|
||||
harness.install({"s1": json.dumps([_finding("q1", "need_evidence", breaks="claim_1", summary="which?")]),
|
||||
"s2": CLEAN, "s3": CLEAN})
|
||||
ctx = harness.make_ctx()
|
||||
assert _control(_call(ctx)) == {"outcome": "REVIEW_REQUIRED", "closed": False}
|
||||
before = json.dumps(load_plan_review_state(harness.drive, ctx.task_id), sort_keys=True)
|
||||
fp = _state(harness)["waves"][-1]["request_fingerprint"]
|
||||
owner_quiz.record_asked(harness.drive, ctx.task_id, quiz_id="quiz-1", question="which?",
|
||||
options=["the first", "the second"])
|
||||
answered = owner_quiz.record_answered(harness.drive, ctx.task_id, quiz_id="quiz-1", option_index=1,
|
||||
request_id="req-1")
|
||||
assert answered["ok"]
|
||||
assert json.dumps(load_plan_review_state(harness.drive, ctx.task_id), sort_keys=True) == before
|
||||
closed = pr._handle_plan_task(ctx, review_disposition={"review_fingerprint": fp, "items": [
|
||||
{"finding_id": "s1:q1", "decision": "accept", "rationale": "owner decided: the second (quiz-1)"}]})
|
||||
assert _control(closed) == {"outcome": "GREEN", "closed": True}
|
||||
|
|
@ -568,27 +568,26 @@ def test_closed_and_pending_plan_states_do_not_advertise_new_review_at_cap():
|
|||
def test_revise_plan_render_names_only_available_paid_cycles_and_exits(enforcement, cap):
|
||||
from ouroboros.tools.plan_render import _next_step
|
||||
|
||||
text = _next_step({"aggregate": "REVISE_PLAN", "closed": False},
|
||||
enforcement=enforcement, cap=cap, cycles_paid=2)
|
||||
assert "A disposition never closes REVISE_PLAN" in text
|
||||
text = _next_step({"aggregate": "REVISE_PLAN", "closed": False}, enforcement=enforcement, cap=cap, cycles_paid=2)
|
||||
assert "never closes REVISE_PLAN" in text
|
||||
assert "rides into the next paid delta cycle" not in text and "reaches reviewers on the next paid cycle" not in text
|
||||
if cap == 2:
|
||||
assert "rides into the next paid delta cycle" not in text
|
||||
assert "no further paid delta cycle" in text
|
||||
assert "no paid cycle remains" in text and "asks only the slots" not in text
|
||||
if enforcement == "blocking":
|
||||
assert "owner unstick (Swarm/hurry)" in text
|
||||
assert "owner authority, never reviewer approval" in text and "Swarm" not in text
|
||||
assert "once the owner raises OUROBOROS_REVIEW_MAX_CYCLES" in text
|
||||
assert "outcome_tier=blocked_with_evidence" in text
|
||||
else:
|
||||
assert "rides into the next paid delta cycle" in text
|
||||
assert "asks only the slots its items name" in text and "one paid cycle 3" in text
|
||||
assert "OUROBOROS_REVIEW_MAX_CYCLES" not in text
|
||||
if enforcement == "advisory":
|
||||
assert "Advisory enforcement: you may proceed" in text
|
||||
assert "host discloses" not in text and "your own final answer" in text
|
||||
evidence_text = _next_step({"aggregate": "REVIEW_REQUIRED", "closed": False},
|
||||
enforcement=enforcement, cap=cap, cycles_paid=2)
|
||||
assert "ONE $0 call" in evidence_text and "no reviewer call, no cycle" in evidence_text
|
||||
assert ("reaches reviewers on the next paid cycle" in evidence_text) is (cap != 2)
|
||||
assert ("no further paid cycle" in evidence_text) is (cap == 2)
|
||||
evidence_text = _next_step({"aggregate": "REVIEW_REQUIRED", "closed": False}, enforcement=enforcement, cap=cap, cycles_paid=2)
|
||||
assert "An answer is recorded at $0" in evidence_text and "merged by finding_id" in evidence_text
|
||||
assert "reaches reviewers on the next paid cycle" not in evidence_text
|
||||
assert ("no paid cycle remains" in evidence_text) is (cap == 2)
|
||||
assert ("asks only the slots its items name" in evidence_text) is (cap != 2)
|
||||
|
||||
|
||||
def test_loop_reminder_does_not_repromise_a_spent_panel(monkeypatch):
|
||||
|
|
|
|||
|
|
@ -591,10 +591,11 @@ def test_a_truncated_requested_document_dispatches_with_the_cut_named(harness):
|
|||
assert "cannot_verify" not in out
|
||||
|
||||
|
||||
def test_a_compacted_paid_predecessor_degrades_to_a_fresh_dispatch(harness, monkeypatch):
|
||||
"""When the prior exact wave is gone (compacted out of the hot state), the evidence
|
||||
continuation is a cache miss: the wave re-dispatches fresh and every slot row
|
||||
discloses the typed `prior_exact_wave_missing` cause."""
|
||||
def test_a_predecessor_the_state_cannot_name_dispatches_fresh_without_a_delta(harness, monkeypatch):
|
||||
"""When no paid predecessor can be named at all, the cycle has nothing to continue: it
|
||||
dispatches fresh like a first cycle and discloses nothing (a NAMED predecessor whose exact
|
||||
record is gone is the per-slot `prior_exact_wave_ref_missing` cause, pinned in
|
||||
``tests/test_phase4_plan_review_continuity.py``)."""
|
||||
(harness.workspace / "notes.md").write_text("deck notes\n", encoding="utf-8")
|
||||
ask = json.dumps([_finding("f1", "need_evidence", breaks="goal", locator="notes.md",
|
||||
summary="I need the notes")])
|
||||
|
|
@ -606,15 +607,14 @@ def test_a_compacted_paid_predecessor_degrades_to_a_fresh_dispatch(harness, monk
|
|||
assert len(sub.calls) == 1 # dispatched, not refused
|
||||
wave = _state(harness)["waves"][-1]
|
||||
assert wave["paid"] is True
|
||||
deltas = [d for row in wave["actors"] for d in row.get("capability_delta") or []]
|
||||
assert deltas and all(d["kind"] == "capability_delta" for d in deltas)
|
||||
assert {d["reason"] for d in deltas} == {"prior_exact_wave_missing"}
|
||||
assert not [d for row in wave["actors"] for d in row.get("capability_delta") or []]
|
||||
assert "cannot_verify" not in out
|
||||
|
||||
|
||||
def test_first_cycle_and_no_request_delta_cycle_carry_no_continuation_delta(harness):
|
||||
"""The `reviewer_requested` guard is load-bearing: a cycle with no reviewer request
|
||||
has no prior thread to continue, so it must not disclose a missing predecessor."""
|
||||
"""A first cycle has nothing to continue and discloses nothing; a delta cycle with no
|
||||
reviewer request CONTINUES every packet slot's recorded transcript, so it discloses
|
||||
nothing either."""
|
||||
sub = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
|
||||
_call(harness.make_ctx())
|
||||
wave = _state(harness)["waves"][-1]
|
||||
|
|
@ -829,7 +829,7 @@ def test_reviewer_question_holds_the_wave_until_a_free_disposition_and_its_answe
|
|||
[finding] = wave["findings"]
|
||||
assert finding["class"] == "need_evidence" and finding["breaks"] == "claim_1" and finding["locator"] == ""
|
||||
assert _state(harness).get("need_evidence_seen", []) == [] # a question is not a locator the host attaches
|
||||
assert "a question addressed to you by spec id" in first and "defer = deferred openly" in first
|
||||
assert "Open questions to you: s1:q1 (claim_1)." in first and "defer = deferred openly" in first
|
||||
answered = pr._handle_plan_task(ctx, review_disposition={
|
||||
"review_fingerprint": wave["request_fingerprint"],
|
||||
"items": [{"finding_id": "s1:q1", "decision": "accept", "rationale": "The board asked for five."}]})
|
||||
|
|
|
|||
|
|
@ -240,7 +240,9 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
|
|||
# 321400 -> 321600 (measured 321394): the closure sentence names the CONFIGURED enforcement against the hurry-projected advisory.
|
||||
# 321600 -> 321800 (measured 321594): the unanswered-slot floor names the terminal-absence rule and the lineage walk.
|
||||
# 323400 -> 327400 (merge of the moved target into the plan-review branch, measured 327146): both sides' paragraphs land together.
|
||||
"docs/architecture/06-agent-core.md": 327400,
|
||||
# 327400 -> 328400 (measured 328111): the addressed answer replaces the automatic delta; answers merge by
|
||||
# finding_id; continuation per slot; the escalated-question clause (each extended in place).
|
||||
"docs/architecture/06-agent-core.md": 328400,
|
||||
# 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
|
||||
|
|
@ -373,7 +375,9 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
|
|||
# 98300 -> 98600: the plan-review bullet gains the numbered-conversation delivery and the
|
||||
# observed-source read fact (extended in place) and two test pointers.
|
||||
# 98600 -> 99300 (merge of the moved target into the plan-review branch, measured 99030): both sides' paragraphs land together.
|
||||
"docs/development/06-rules-by-change-class.md": 99300,
|
||||
# 99300 -> 99800 (measured 99712): one bullet — plan-review answers merge by finding_id, the addressed re-ask
|
||||
# and its $0 replay rows, the quiz-answer rule; the base sat 270 bytes under.
|
||||
"docs/development/06-rules-by-change-class.md": 99800,
|
||||
"docs/development/07-managed-update-rule.md": 4166,
|
||||
"docs/development/08-mutation-attribution-rule.md": 2899,
|
||||
"docs/development/09-process-custody-rule.md": 10028,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue