mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-02 19:58:46 +00:00
Plan review: height rule, cycle-2 adjudication duty, seat line
The reviewer's blocking rule no longer turns a claim that cannot be checked as written into a blocker by construction: such a claim is a question to the author (need_evidence naming the claim id) or a note. The clause leaves plan_packet._BLOCKING_RULE and the Plan Review Checklist in the same commit, so the injected checklist and the code that assembles the packet keep agreeing. The rubric gains a subtraction voice (item 6: what could the spec drop without losing the goal, as a note); governance becomes item 7. On cycle 2 and later the convergence rule in the user packet makes a reviewer adjudicate its OWN earlier findings first: RESOLVED and SUPERSEDED are not repeated, STILL OPEN is re-emitted with the residual, and still-open holds only while the goal is unchanged, which the host states as one fact under the spec delta (Goal changed since cycle n: yes|no|unknown, unknown when the previous frozen spec body was truncated). Every slot's send ends with its own panel seat id after the cache-stable prefix (api rows, native rows and delegated sessions alike), so a shared packet can say which rows are the reviewer's own. The E2E marker sentence in the prompt head is byte-identical. docs/CHECKLISTS.md is a protected path; the edits here are the height clause deletion (Q-ii=A), the open-set wording of the Cycles and closure bullets (D4, Q-v=A), the rubric subtraction row and the cycle-2 duty sentence (the owner's convergence item: adjudicate own findings, subtraction voice). Amendments applied: A2 (no start-anchor sentence, no distinct-model tokens), A3 (packet fact only, no cycles-left card), B3 (seat, goal fact and duty after ROOT EXPLORATION LOG). tests/test_plan_spec.py entered the 1001-1500 band with the open-set and packet pins; its manifest rationale rides this commit. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
parent
575e2b0278
commit
4197a1c916
10 changed files with 146 additions and 35 deletions
|
|
@ -823,7 +823,7 @@ Every element has a host-minted id (`goal`, `claim_N`, `invariant_N`, `decision_
|
|||
Those ids are the only valid `breaks` targets. Ids may shift between cycles when the agent
|
||||
rewrites the spec — re-target `breaks` against the CURRENT ids using the Spec delta.
|
||||
|
||||
### The rubric (five domain-free questions + one for self-modification)
|
||||
### The rubric (six domain-free questions + one for self-modification)
|
||||
|
||||
| # | item | what to check |
|
||||
|---|------|---------------|
|
||||
|
|
@ -832,14 +832,15 @@ rewrites the spec — re-target `breaks` against the CURRENT ids using the Spec
|
|||
| 3 | constraints and invariants | Are the real constraints named — budget, deadline, safety, irreversibility, commitments to others? |
|
||||
| 4 | deferrals | Is anything deferred that will be expensive to change once the work has started? |
|
||||
| 5 | evidence sufficiency | Is the evidence enough to judge? If not, ask for exactly what is missing (`need_evidence` with a locator or a spec item id) instead of inventing a gap. |
|
||||
| 6 | governance (self-modification plans only) | Does the intention contradict BIBLE.md or a frozen contract? Name the principle or contract. |
|
||||
| 6 | subtraction | What could the spec drop (a claim, decision, invariant, deferral, path or guard) without losing the goal? Say it as a `note` naming the element id; removing is advice as legitimate as adding. |
|
||||
| 7 | governance (self-modification plans only) | Does the intention contradict BIBLE.md or a frozen contract? Name the principle or contract. |
|
||||
|
||||
### Height rule — what may block
|
||||
|
||||
A finding is **blocking** only if being wrong about it AFTER the work starts would invalidate work
|
||||
already done, violate a declared commitment, or make an acceptance claim unverifiable — and it
|
||||
MUST name the spec id it breaks. Everything else is a **note**. If missing evidence makes a claim
|
||||
structurally unverifiable, that is blocking against the claim, not a `need_evidence` request.
|
||||
MUST name the spec id it breaks. Everything else is a **note**. A claim you cannot check as written
|
||||
is a question to the author (`need_evidence` with the claim id in `breaks`) or a `note`, not a blocker.
|
||||
|
||||
- `blocking` — requires `breaks: <spec id>`. Without a valid id the host demotes it to a note and
|
||||
discloses the demotion.
|
||||
|
|
@ -871,15 +872,18 @@ authority, and prose outside the array is not parsed.
|
|||
|
||||
### Cycles and closure
|
||||
|
||||
- **GREEN** — no findings. Proceed.
|
||||
- **REVIEW_REQUIRED** — notes / `need_evidence`, or a blocking finding BELOW quorum. A
|
||||
note-only wave closes immediately in either enforcement mode. The agent closes outstanding
|
||||
`need_evidence` with a disposition (accept / reject with rationale / defer) — no new panel,
|
||||
no cost; notes do not need entries. Voluntary dispositions on current closed note-only
|
||||
waves remain available through the same call, without reopening or a paid cycle.
|
||||
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 a reject the next paid delta cycle judges.
|
||||
- **GREEN** — no blocking finding and no `need_evidence` without a disposition; notes never
|
||||
change the verdict, so a note-only wave is GREEN in either enforcement mode. Proceed.
|
||||
- **REVIEW_REQUIRED** — `need_evidence` without a disposition, or a blocking finding BELOW
|
||||
quorum. The agent closes outstanding `need_evidence` with a disposition (accept / reject with
|
||||
rationale / defer) — no new panel, no cost; notes do not need entries. Voluntary dispositions
|
||||
on closed note-only waves remain available through the same call, without reopening or a
|
||||
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
|
||||
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.
|
||||
|
|
@ -902,9 +906,13 @@ Paid cycles per task are bounded by the owner's `OUROBOROS_REVIEW_MAX_CYCLES` (d
|
|||
`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
|
||||
all reviewers' findings from the previous cycle, the agent's dispositions and the spec delta:
|
||||
a reformulation of an earlier finding is not a new finding, and a new blocking finding must say
|
||||
why it was invisible before. When the cap is spent under blocking enforcement the host holds
|
||||
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
|
||||
was removed or replaced) are not repeated; STILL OPEN is re-emitted naming the residual the answer
|
||||
does not cover, and only while the goal is unchanged (the packet states `Goal changed since cycle n`;
|
||||
a changed goal is judged afresh). Then: a reformulation of an earlier finding is not a new finding,
|
||||
and a new blocking finding must say why it was invisible before. When the cap is spent under blocking enforcement the host holds
|
||||
implementation and escalates with the typed `review_cycles_exhausted` reason; under advisory the
|
||||
agent may proceed with the wave open under a loud host disclosure. Explicit
|
||||
`review_disposition.author_action` plus author disposition may retain/select a full
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -205,6 +205,7 @@ BAND_PATHS = {
|
|||
"tests/test_onboarding_wizard.py": None,
|
||||
"tests/test_owner_stop_s3.py": "Entered the band from 821 lines: the S3 contract suite now covers retry-root aliasing, graceful-to-immediate hardening, stale-control drain races, hard deadline preservation, descendant settlement failure, and late resweep exactly-once root finalization.",
|
||||
"tests/test_packaged_runtime_and_lifecycle.py": None,
|
||||
"tests/test_plan_spec.py": "Entered the band from 940 lines: the open-set verdict, the per-finding advisory closure of a below-quorum blocking finding and the cycle-2 adjudication/goal-changed packet facts each pin both directions next to the closure-table and packet tests they extend; splitting the file would separate the aggregate pins from the closure pins that read the same fixtures.",
|
||||
"tests/test_project_routing_v664.py": "Main-chat routing suite: the project last-task-result contracts (registry pointer, bounded newest-first scan, durable-ts tie-break) stay with their sibling routing tests as one focused suite below the 1500-line band cap.",
|
||||
"tests/test_provider_contract_ci.py": "Provider canary matrix pins grew with the deepseek_direct row; split when the next provider lands.",
|
||||
"tests/test_provider_failure_reporting.py": "Entered the band from 938 lines: the provider-failure honesty rework added the retry-wall marker suites (entry-clear, all-retryable stamp, empty-response provider-failing gate, no-repay rail) beside the cost-validation suites (bool/NaN/inf/negative/huge-int at both boundaries) - one file per failure-reporting surface.",
|
||||
|
|
|
|||
|
|
@ -189,6 +189,9 @@ def dialogue_slot_inputs(slots: list, *, system_prompt: str, user_content: str,
|
|||
tokenizer_margin=155_000, slots=api)
|
||||
for slot in slots:
|
||||
sid = str(slot.slot_id)
|
||||
# A shared packet cannot say "your": each slot's send ends with its own seat, after the
|
||||
# cache-stable prefix, so a cycle-2 reviewer knows which earlier findings are its own.
|
||||
seat = f"\n## YOUR PANEL SEAT\n\n`{sid}`\n"
|
||||
if not slot_retrieves(slot):
|
||||
capacity = int(limits[sid]) * 4
|
||||
existing = messages.get(sid)
|
||||
|
|
@ -196,12 +199,12 @@ def dialogue_slot_inputs(slots: list, *, system_prompt: str, user_content: str,
|
|||
# Continuation history is already exact; only this turn's new
|
||||
# automatic source can shrink, never its prior paid inputs.
|
||||
total = _messages_char_count(existing)
|
||||
view, coverage[sid] = fit_dialogue_text(user_content, own, capacity - total + len(user_content))
|
||||
messages[sid] = [{**m, "content": view} if i == len(existing) - 1 and m.get("role") == "user" else dict(m)
|
||||
view, coverage[sid] = fit_dialogue_text(user_content, own, capacity - total + len(user_content) - len(seat))
|
||||
messages[sid] = [{**m, "content": view + seat} if i == len(existing) - 1 and m.get("role") == "user" else dict(m)
|
||||
for i, m in enumerate(existing)]
|
||||
else:
|
||||
view, coverage[sid] = fit_dialogue_text(user_content, own, capacity - len(system_prompt))
|
||||
messages[sid] = build_plan_review_messages(system_prompt, view, plan_user_stable_len(view))
|
||||
view, coverage[sid] = fit_dialogue_text(user_content, own, capacity - len(system_prompt) - len(seat))
|
||||
messages[sid] = build_plan_review_messages(system_prompt, view + seat, plan_user_stable_len(view))
|
||||
lengths[sid] = _messages_char_count(messages[sid])
|
||||
elif not slot_is_session(slot):
|
||||
bound = review_native_transcript_bound(slot.model, output_reserve=PLAN_REVIEW_MAX_TOKENS,
|
||||
|
|
@ -211,7 +214,7 @@ def dialogue_slot_inputs(slots: list, *, system_prompt: str, user_content: str,
|
|||
def first_send(task):
|
||||
return native_first_send_chars(session_root, surface="plan_review", role_hint=slot.role_hint,
|
||||
slot_id=sid, session_task=task, output_contract=PLAN_FINDINGS_ARRAY_CONTRACT, task_id=task_id)
|
||||
tasks[sid], coverage[sid] = fit_dialogue_text(session_task, own,
|
||||
tasks[sid], coverage[sid] = fit_dialogue_text(session_task + seat, own,
|
||||
native_landing_at(bound) - governance_read - 1, measure=first_send)
|
||||
elif own.get("file") and own.get("text"):
|
||||
instruction = (
|
||||
|
|
@ -223,9 +226,11 @@ def dialogue_slot_inputs(slots: list, *, system_prompt: str, user_content: str,
|
|||
f"using {own['locator']}::lines or ::bytes. Do not claim unread messages reviewed. "
|
||||
"File access is available; full-read coverage remains reviewer-declared, not host-attested.\n"
|
||||
)
|
||||
tasks[sid] = session_task.replace(str(own["text"]), instruction, 1)
|
||||
tasks[sid] = session_task.replace(str(own["text"]), instruction, 1) + seat
|
||||
coverage[sid] = {"source_sha256": own["sha256"], "source_bytes": own["bytes"],
|
||||
"inline_bytes": None, "full_file": own["file"], "read_coverage": "unobserved"}
|
||||
else:
|
||||
tasks[sid] = session_task + seat
|
||||
if slot_retrieves(slot):
|
||||
lengths[sid] = (first_send(tasks.get(sid, session_task)) if not slot_is_session(slot)
|
||||
else len(tasks.get(sid, session_task)))
|
||||
|
|
|
|||
|
|
@ -46,23 +46,34 @@ _RUBRIC = (
|
|||
"missing is the AUTHOR's judgment rather than a document, ask the author: a `need_evidence` "
|
||||
"finding whose `breaks` names the spec id the question is about, no locator needed; Ouroboros "
|
||||
"answers it in its disposition or escalates it. Do not invent a gap.",
|
||||
"6. Subtraction — what could the spec drop (a claim, decision, invariant, deferral, path or "
|
||||
"guard) without losing the goal? Say it as a `note` naming the element id; removing is advice "
|
||||
"as legitimate as adding.",
|
||||
)
|
||||
|
||||
_BLOCKING_RULE = (
|
||||
"A finding is `blocking` iff being wrong about it AFTER the work starts would invalidate work "
|
||||
"already done, violate a declared commitment, or make an acceptance claim unverifiable — and it "
|
||||
"MUST name `breaks`: the id of the spec element it breaks (goal for the intention as a whole, claim_N, invariant_N, decision_N, "
|
||||
"deferred_N). Everything else is a `note`. If missing evidence makes a claim STRUCTURALLY "
|
||||
"unverifiable, that is `blocking` with `breaks=<claim id>`, not `need_evidence`."
|
||||
"deferred_N). Everything else is a `note`. A claim you cannot check as written is a question to "
|
||||
"the author (`need_evidence` with the claim id in `breaks`) or a `note`, not a blocker."
|
||||
)
|
||||
|
||||
_CONVERGENCE_RULE = (
|
||||
"CONVERGENCE RULE (cycle ≥2): a reformulation of a prior finding is not a new finding; a new "
|
||||
"`blocking` finding must state why it was invisible in the previous cycle; a prior `blocking` "
|
||||
"finding that is still open must be re-emitted (same id, summary starting `still-open:`); a "
|
||||
"resolved one is simply not repeated. Spec ids may have SHIFTED between cycles (positional ids "
|
||||
"renumber when an element is dropped or reordered): re-target `breaks` against the CURRENT spec "
|
||||
"ids using the Spec delta (`renumbered: [{from, to, text}]`), never against the old ids."
|
||||
"CONVERGENCE RULE (cycle ≥2) — adjudicate your OWN earlier findings first. They are the rows "
|
||||
"below whose finding_id starts with your panel seat (named at the end of this packet). Read the "
|
||||
"author's dispositions and the Spec delta as the author's argument, and for each `blocking` "
|
||||
"finding and `need_evidence` you raised decide: RESOLVED — the delta or the rationale answers "
|
||||
"it: do not repeat it; SUPERSEDED — the element it targeted was removed or replaced: do not "
|
||||
"repeat it; STILL OPEN — re-emit it (same id and class, summary starting `still-open:`) naming "
|
||||
"the residual the answer does not cover. `still-open` holds only while the goal is unchanged: "
|
||||
"when the host says the goal changed, judge the new intention afresh — an earlier finding "
|
||||
"survives only where it still breaks a CURRENT element. Then review what changed: a "
|
||||
"reformulation of an earlier finding is not a new finding, and a NEW `blocking` finding must "
|
||||
"state why it was invisible in the previous cycle. Spec ids may have SHIFTED between cycles "
|
||||
"(positional ids renumber when an element is dropped or reordered): re-target `breaks` against "
|
||||
"the CURRENT spec ids using the Spec delta (`renumbered: [{from, to, text}]`), never against "
|
||||
"the old ids."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -118,7 +129,7 @@ def build_plan_review_system_prompt(
|
|||
]
|
||||
if constitutional:
|
||||
parts.append(
|
||||
"6. Governance (this plan touches Ouroboros's own body): does the spec contradict "
|
||||
"7. Governance (this plan touches Ouroboros's own body): does the spec contradict "
|
||||
"BIBLE.md or a frozen contract? Cite the principle.\n"
|
||||
)
|
||||
parts.append(f"\n## Blocking rule\n\n{_BLOCKING_RULE}\n")
|
||||
|
|
@ -226,6 +237,12 @@ def _render_prior_cycles(prior_cycles: list[dict], dispositions: list[dict], spe
|
|||
)
|
||||
lines.append("### Agent dispositions\n\n" + _json_block(dispositions or [], PACKET_PRIOR_CYCLES_CHARS) + "\n")
|
||||
lines.append("### Spec delta\n\n" + _json_block(spec_delta or {}, PACKET_PRIOR_CYCLES_CHARS) + "\n")
|
||||
# ONE host fact from the delta the host already computed: `still-open` holds only while the
|
||||
# goal is unchanged. `unknown` when the previous frozen spec body was truncated (no delta).
|
||||
goal_changed = (spec_delta or {}).get("goal_changed")
|
||||
previous = (prior_cycles[-1] if isinstance(prior_cycles[-1], Mapping) else {}).get("cycle_index", "?") if prior_cycles else "?"
|
||||
lines.append(f"Goal changed since cycle {previous}: "
|
||||
f"{'unknown' if not isinstance(goal_changed, bool) else 'yes' if goal_changed else 'no'}\n")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,13 @@ def test_mixed_delivery_keeps_full_file_and_exact_overflow_range(harness, monkey
|
|||
assert "LATEST CHOICE" in native and "exact omitted prefix" in native
|
||||
assert declarations[0]["mandatory_read_chars"] == len(packet)
|
||||
delegated = delivery["slot_session_tasks"]["delegated"]
|
||||
# Every slot's send ends with ITS OWN panel seat: a shared packet cannot say "your".
|
||||
from tests.test_plan_review_engine import _user_text
|
||||
last_user = {sid: _user_text(delivery["slot_messages"][sid][-1]["content"]) for sid in ("small", "large")}
|
||||
seat = {sid: f"\n## YOUR PANEL SEAT\n\n`{sid}`\n" for sid in last_user}
|
||||
assert all(last_user[sid].endswith(seat[sid]) for sid in last_user)
|
||||
assert last_user["large"].removesuffix(seat["large"]) == packet # the seat is the only addition
|
||||
assert native.endswith("\n## YOUR PANEL SEAT\n\n`native`\n") and delegated.endswith("\n## YOUR PANEL SEAT\n\n`delegated`\n")
|
||||
assert "MANDATORY FULL READ" in delegated and own["file"] in delegated
|
||||
assert "no numerical window evidence" in delegated and "1M" not in delegated
|
||||
assert "discussion discussion discussion" not in delegated
|
||||
|
|
@ -310,3 +317,35 @@ def test_snapshot_qualified_room_keeps_original_gap_disclosure(harness):
|
|||
active_root=harness.workspace, allowed_roots=[], resolve_chat=reader)
|
||||
assert json.loads(manifest["attached"][0]["text"])["text"] == "Retained explanation"
|
||||
assert any(row["reason"].startswith("chat_history_gap:") for row in manifest["omissions"])
|
||||
|
||||
|
||||
def test_the_seat_line_follows_the_cache_stable_prefix_on_every_api_slot(harness, monkeypatch):
|
||||
"""The per-slot seat is appended AFTER `## ROOT EXPLORATION LOG` (the cache-stable prefix
|
||||
stays byte-identical across api slots) and each slot gets its own id; reverted, no seat."""
|
||||
from types import SimpleNamespace
|
||||
from ouroboros.review_execution import ReviewRouteKind
|
||||
from ouroboros.tools import plan_spec, review_synthesis
|
||||
from ouroboros.tools.plan_dialogue import dialogue_slot_inputs
|
||||
from ouroboros.tools.plan_packet import build_plan_review_user_content, plan_user_stable_len
|
||||
from tests.test_plan_review_engine import DECK_SPEC, _user_text
|
||||
|
||||
spec, _ = plan_spec.normalize_spec({**DECK_SPEC, "goal": "Ship the deck"})
|
||||
packet = build_plan_review_user_content(
|
||||
objective="o", goal=spec["goal"], plan_prose="p", spec=spec,
|
||||
manifest={"declared": [], "attached": [], "omissions": []},
|
||||
prior_cycles=[], dispositions=[], spec_delta=None, root_exploration_log="ran: ls")
|
||||
def slot(name):
|
||||
return SimpleNamespace(slot_id=name, model="same/model", role_hint="plan reviewer", use_local=False,
|
||||
session_profile=name, route=ReviewRouteKind.API_CHAT, retrieves=False, native_retrieval=False)
|
||||
monkeypatch.setattr(review_synthesis, "per_slot_input_token_limits", lambda *a, **k: {"a": 200000, "b": 200000})
|
||||
delivery = dialogue_slot_inputs([slot("a"), slot("b")], system_prompt="governance", user_content=packet,
|
||||
session_task=packet, manifest={}, slot_messages={}, native_mandatory_chars=len(packet),
|
||||
session_root=str(harness.workspace), task_id="task-1")
|
||||
sent = {sid: _user_text(delivery["slot_messages"][sid][-1]["content"]) for sid in ("a", "b")}
|
||||
boundary = plan_user_stable_len(packet)
|
||||
assert boundary > 0 and sent["a"][:boundary] == sent["b"][:boundary] == packet[:boundary]
|
||||
assert sent["a"].endswith("\n## YOUR PANEL SEAT\n\n`a`\n") and sent["b"].endswith("\n## YOUR PANEL SEAT\n\n`b`\n")
|
||||
assert sent["a"].index("## YOUR PANEL SEAT") > sent["a"].index("## ROOT EXPLORATION LOG")
|
||||
# The recorded cache split is the same boundary: stable block, then the dynamic tail with the seat.
|
||||
blocks = delivery["slot_messages"]["a"][-1]["content"]
|
||||
assert isinstance(blocks, list) and blocks[0]["text"] == packet[:boundary] and blocks[-1]["text"].endswith("`a`\n")
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ def test_a_declared_new_source_file_is_constitutional_before_it_exists(harness):
|
|||
assert wave["constitutional"] is True
|
||||
assert "affected_paths" in wave["constitutional_note"] and str(target) in wave["constitutional_note"]
|
||||
system_prompt = _system_prompt(substrate)
|
||||
assert "## BIBLE.md (constitution" in system_prompt and "6. Governance" in system_prompt
|
||||
assert "## BIBLE.md (constitution" in system_prompt and "7. Governance" in system_prompt
|
||||
assert "## ARCHITECTURE.md (architecture and data flow" in system_prompt
|
||||
assert "on-demand pointer" not in system_prompt
|
||||
assert "REMINDER" not in out
|
||||
|
|
|
|||
|
|
@ -690,7 +690,7 @@ def test_constitutional_from_affected_paths_and_reminder_on_system_binding(harne
|
|||
# W3: a self-modification plan carries ARCHITECTURE.md inline, in full — not a map, not a pointer
|
||||
assert "## ARCHITECTURE.md" in system_prompt and "slots and quorum." in system_prompt
|
||||
assert "ARCHITECTURE navigation map" not in system_prompt
|
||||
assert "6. Governance" in system_prompt
|
||||
assert "7. Governance" in system_prompt # the rubric gained a subtraction voice at 6
|
||||
assert "REMINDER" not in out
|
||||
# (b) system binding, nothing declared: NOT constitutional (D29) + a reminder, BIBLE as pointer
|
||||
ctx = harness.make_ctx(active_workspace=False, task_id="task-2")
|
||||
|
|
@ -919,6 +919,11 @@ def test_packet_uses_the_REAL_checklist_section_and_its_findings_only_contract()
|
|||
assert retired not in lowered, retired
|
||||
assert "only a json array" in lowered and "NO_FINDINGS" in prompt
|
||||
assert "breaks" in lowered and "need_evidence" in lowered
|
||||
# The shipped checklist agrees with the packet: no height clause that turns an unverifiable
|
||||
# claim into a blocker, GREEN is the empty open set, and the rubric has a subtraction voice.
|
||||
assert "structurally unverifiable" not in section.lower() and "GREEN** — no findings" not in section
|
||||
assert "| 6 | subtraction |" in section and "| 7 | governance" in section
|
||||
assert "adjudicate its OWN earlier findings" in section and "Goal changed since cycle n" in section
|
||||
|
||||
|
||||
def test_diff_size_cap_is_route_aware():
|
||||
|
|
|
|||
|
|
@ -178,6 +178,35 @@ def test_spec_delta_reports_renumbered_ids_and_convergence_rule_says_retarget():
|
|||
assert "re-target `breaks` against the CURRENT spec ids" in user and "renumbered" in user
|
||||
|
||||
|
||||
def test_cycle_two_packet_carries_the_adjudication_duty_and_the_goal_fact():
|
||||
"""Cycle ≥2: the reviewer's first duty is to adjudicate its OWN earlier findings
|
||||
(RESOLVED / SUPERSEDED / STILL OPEN with the residual), and the host states whether the
|
||||
goal changed since the previous cycle; a cycle-1 packet carries neither."""
|
||||
from ouroboros.tools.plan_packet import build_plan_review_user_content
|
||||
|
||||
prev, _ = plan_spec.normalize_spec({"goal": "ship the deck", "affected_paths": []})
|
||||
same, _ = plan_spec.normalize_spec({"goal": "ship the deck", "affected_paths": [], "in_scope": ["five slides"]})
|
||||
moved, _ = plan_spec.normalize_spec({"goal": "ship a memo instead", "affected_paths": []})
|
||||
|
||||
def packet(spec, delta, cycle):
|
||||
return build_plan_review_user_content(
|
||||
objective="o", goal=spec["goal"], plan_prose="p", spec=spec,
|
||||
manifest={"declared": [], "attached": [], "omissions": []},
|
||||
prior_cycles=[{"cycle_index": cycle - 1, "aggregate": "REVIEW_REQUIRED", "findings": []}] if cycle > 1 else [],
|
||||
dispositions=[], spec_delta=delta, root_exploration_log=None, cycle_index=cycle)
|
||||
|
||||
unchanged = packet(same, plan_spec.spec_delta(prev, same), 2)
|
||||
for word in ("adjudicate your OWN earlier findings first", "RESOLVED", "SUPERSEDED", "STILL OPEN",
|
||||
"still breaks a CURRENT element", "Goal changed since cycle 1: no"):
|
||||
assert word in unchanged, word
|
||||
assert unchanged.index("## ROOT EXPLORATION LOG") < unchanged.index("Goal changed since cycle 1")
|
||||
assert "Goal changed since cycle 1: yes" in packet(moved, plan_spec.spec_delta(prev, moved), 2)
|
||||
truncated = packet(same, {"unavailable": "previous frozen spec body truncated"}, 2)
|
||||
assert "Goal changed since cycle 1: unknown" in truncated
|
||||
first = packet(same, None, 1)
|
||||
assert "Goal changed since" not in first and "adjudicate your OWN" not in first and "STILL OPEN" not in first
|
||||
|
||||
|
||||
# ------------------------------------------------------------ B2 constitutional
|
||||
|
||||
|
||||
|
|
@ -745,6 +774,10 @@ def test_system_prompt_stance_and_bible_gating():
|
|||
assert "OMISSION NOTE: ARCHITECTURE navigation map not supplied" in plain
|
||||
assert checklist in plain
|
||||
assert "Convergence rule" not in plain
|
||||
# The height rule sends an unverifiable claim back as a question or a note, never a blocker.
|
||||
assert "STRUCTURALLY" not in plain and "structurally unverifiable" not in lowered
|
||||
assert "A claim you cannot check as written is a question to the author" in plain
|
||||
assert "6. Subtraction" in plain and "7. Governance" not in plain
|
||||
assert "blocking" in lowered and "`breaks`" in plain
|
||||
assert "need_evidence" in plain
|
||||
assert "important brainstorming opportunity" in plain
|
||||
|
|
@ -758,6 +791,7 @@ def test_system_prompt_stance_and_bible_gating():
|
|||
cycle_index=2, enforcement="advisory", architecture_text="ARCH BODY",
|
||||
)
|
||||
assert "BIBLE BODY" in constitutional and "Governance" in constitutional
|
||||
assert "6. Subtraction" in constitutional and "7. Governance" in constitutional and "6. Governance" not in constitutional
|
||||
# W3: ARCHITECTURE.md rides inline, in full, in the self-modification pack
|
||||
assert "## ARCHITECTURE.md" in constitutional and "ARCH BODY" in constitutional
|
||||
# the convergence rule is cycle-dependent and now lives in the USER prior-cycles section
|
||||
|
|
|
|||
|
|
@ -200,7 +200,9 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
|
|||
# count), the per-finding advisory closure of a below-quorum blocking finding, the closed
|
||||
# REVIEW_REQUIRED written GREEN, and the author path's labelled critic pair REPLACE the
|
||||
# closure/aggregate/disclosed-gap sentences in place; the base sat 69 bytes under.
|
||||
"docs/architecture/06-agent-core.md": 318000,
|
||||
# 318000 -> 318700: the reviewer stance sentence names the cycle-2 adjudication duty, the
|
||||
# goal-changed fact, the subtraction voice and the per-slot seat line (REPLACED in place).
|
||||
"docs/architecture/06-agent-core.md": 318700,
|
||||
# 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue