ouroboros/tests/test_plan_review_script.py
Ouroboros 82de26a2fd Resolve only declared file targets as the plan's constitutional fact
`plan_spec.resolve_constitutional` read every non-URL string in the spec's
`affected_resources` list as a file path. On the live install that turned the
prose item «Отдельный проект «TSMC — 10-летний инвестиционный анализ»» into "a
path under the Ouroboros repository": the plan was marked constitutional and
every reviewer of a deck-shaped plan received BIBLE.md plus ARCHITECTURE.md in
full, ~470k tokens per cycle. Across the recorded history not one of the 14
constitutional verdicts came from a real file path in that list, and the model
never once spelled a target as `file://` (0 of 186 locators).

The spec now says the two things separately (owner decisions 8=A, 9=A, 16=A):

* `affected_paths` — REQUIRED — is the files the work will CHANGE, and the only
  list the host resolves. `[]` is a real claim: "this work changes no files". A
  path under the system repository decides whether or not the file exists yet,
  because creating `ouroboros/new_module.py` IS self-modification.
* `affected_resources` is prose: systems, services, projects, people. Nothing in
  it reaches the filesystem.
* An `evidence` locator is something to LOOK AT. Reading a repository file is not
  changing it, so it no longer buys the pack on its own (16=A); when the work
  will also change that file, the file is in `affected_paths` and decides there.
  System-repo evidence reads are named in the disclosure note, so an author who
  expected the constitution can see why it did not come. Existence stopped
  deciding anything, so `_default_exists`/`evidence_exists` are gone with it.

A NEW spec submitted in the old mixed form is refused before any dispatch with
`PLAN_RESOURCE_FORM_REQUIRED`, quoting the field with a two-line example. The
refusal deliberately does NOT carry the `PLAN_SPEC_INVALID` token: that family
records a superseding attempt on the way out and would orphan an open wave, so
this one writes nothing and, when a wave is open, names its fingerprint and the
free `review_disposition` exit. Waves recorded under the old shape keep their
identity: `affected_paths` is never defaulted onto stored input, so their
`spec_hash` and `plan_fingerprint` are byte-identical (pinned against the three
live examples) and they still close at $0. `spec_delta` reports the appearance of
the key itself, because "stored before the field existed" and "changes no files"
are different claims.

No grace mechanism for the 31 open waves — already disclosed to the owner.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-09-15 17:08:58 +03:00

112 lines
5.1 KiB
Python

from __future__ import annotations
import asyncio
import importlib.util
import json
import types
def _load_script_module(repo_root):
path = repo_root / "scripts" / "run_plan_review.py"
spec = importlib.util.spec_from_file_location("run_plan_review_script", path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_run_plan_review_script_runs_the_engine_on_the_new_envelope(monkeypatch, tmp_path):
"""The operator script drives the SAME engine plan_task uses — goal + plan prose +
spec JSON + evidence locators — in an isolated drive root, and prints the recorded
wave (slots, validated findings, host aggregate) plus the coordinated output."""
import pathlib
from ouroboros.review_substrate import ReviewSlot
from ouroboros.tools import plan_review, plan_review_runtime
repo = pathlib.Path(__file__).resolve().parents[1]
script = _load_script_module(repo)
captured = {}
async def fake_run_slots(ctx, slots, *, system_prompt, user_content, session_task="",
session_root="", output_contract="", slot_messages=None,
slot_session_tasks=None, request_policy=None,
session_threads=None, retry_key="", reconcile_only=False,
reconciliation_identity=None, release_at_dispatch=False):
captured["task_id"] = ctx.task_id
captured["release_at_dispatch"] = release_at_dispatch
captured["slots"] = [s.slot_id for s in slots]
captured["system_prompt"] = system_prompt
captured["user_content"] = user_content
captured["retry_key"] = retry_key
captured["reconcile_only"] = reconcile_only
captured["reconciliation_identity"] = reconciliation_identity
return [{
"slot_id": s.slot_id, "model": s.model, "request_model": s.model, "route": "api_chat",
"host_file_read_attestation": "host_assembled_packet",
"text": "[]\nNO_FINDINGS", "error": None, "prompt_ref": {}, "response_ref": {},
"tokens_in": 1, "tokens_out": 1, "cost": 0.0,
} for s in slots]
slots = [ReviewSlot(slot_id="slot_1", model="fake/reviewer", effort="high")]
monkeypatch.setattr(plan_review, "_plan_review_slots", lambda: slots)
monkeypatch.setattr(plan_review_runtime, "plan_review_slots", lambda: slots)
monkeypatch.setattr(plan_review, "_run_plan_review_slots", fake_run_slots)
monkeypatch.setattr(plan_review, "get_review_enforcement", lambda: "advisory")
workspace = tmp_path / "ws"
workspace.mkdir()
plan_path = tmp_path / "plan.md"
plan_path.write_text("# Plan\n\nImplement the accepted phase.\n", encoding="utf-8")
spec_path = tmp_path / "spec.json"
spec_path.write_text(json.dumps({
"in_scope": ["the accepted phase"], "acceptance_claims": ["tests green"],
"invariants": ["no new settings"],
# An operator envelope carries the same required form as the agent's (owner 9=A).
"affected_paths": [],
}), encoding="utf-8")
evidence_file = workspace / "notes.txt"
evidence_file.write_text("inspect the existing SSOT", encoding="utf-8")
args = types.SimpleNamespace(
plan=str(plan_path),
goal="Test plan-review script",
spec_json=str(spec_path),
evidence=[str(evidence_file), "https://example.com/spec"],
subject_root=str(workspace),
drive_root=str(tmp_path / "drive"),
output="",
)
output = asyncio.run(script._run(args))
assert "RESOLVED PLAN REVIEW CONFIG" in output
assert "PLAN REVIEW WAVE" in output and "PLAN REVIEW COORDINATED OUTPUT" in output
assert captured["task_id"] == "plan-review-cli"
assert captured["slots"] == ["slot_1"]
assert captured["retry_key"].startswith("plan_review:")
assert captured["reconcile_only"] is False
assert captured["reconciliation_identity"]["epoch"] == captured["retry_key"]
assert "Implement the accepted phase." in captured["user_content"]
assert "tests green" in captured["user_content"]
assert "inspect the existing SSOT" in captured["user_content"]
assert "| https://example.com/spec | url_not_fetched |" in captured["user_content"]
assert "before the work starts" in captured["system_prompt"]
assert '"aggregate": "GREEN"' in output
assert (tmp_path / "drive" / "task_results" / "plan-review-cli.json").exists()
def test_run_plan_review_script_has_no_personal_key_fallback():
import pathlib
repo = pathlib.Path(__file__).resolve().parents[1]
text = (repo / "scripts" / "run_plan_review.py").read_text(encoding="utf-8")
assert "file1.txt" not in text
def test_run_plan_review_script_has_only_the_new_envelope_flags():
import pathlib
repo = pathlib.Path(__file__).resolve().parents[1]
text = (repo / "scripts" / "run_plan_review.py").read_text(encoding="utf-8")
for flag in ("--goal", "--plan", "--spec-json", "--evidence"):
assert flag in text
for retired in ("--context-level", "--plan-class", "--files-to-touch", "--include-tests", "--scout-handoff"):
assert retired not in text