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:
Ouroboros 2026-09-26 10:42:00 +03:00
parent 575e2b0278
commit 4197a1c916
10 changed files with 146 additions and 35 deletions

View file

@ -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

View file

@ -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.",

View file

@ -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)))

View file

@ -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)

View file

@ -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")

View file

@ -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

View file

@ -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():

View file

@ -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

View file

@ -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