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>
This commit is contained in:
Ouroboros 2026-09-15 17:03:33 +03:00
parent b8c46e57ed
commit 82de26a2fd
18 changed files with 521 additions and 140 deletions

View file

@ -784,7 +784,9 @@ def sw1_stub_script(_clone: pathlib.Path) -> dict:
"goal": "Survey the repository with two parallel scouts.",
"plan": "Schedule two scouts, wait for both, summarize.",
"spec": {"deliverables": ["Two scout results summarized."],
"acceptance_claims": ["Both scouts completed and were absorbed."]}}},
"acceptance_claims": ["Both scouts completed and were absorbed."],
# Required on every submitted spec (owner 9=A); a survey changes no file.
"affected_paths": []}}},
scout("A", "list the top-level directories"),
scout("B", "list the test modules under tests/system_e2e"),
wait_step,

View file

@ -1750,9 +1750,9 @@ Plan review, task acceptance, commit review, and deep self-review answer differe
#### Plan construction and review
`plan_task` reviews an INTENTION before the work starts — the same organ for code, research, deliverables, and actions (BIBLE P3). The obligation is constitutional and the finalization gate is structural (`owner_hurry.force_plan_decision`); the order in which a task asks, explores and plans is the mind's judgment, and no prompt choreographs it. The submitted envelope carries the goal, the plan prose, and a typed domain-neutral SPEC (`in_scope`, `non_goals`, `acceptance_claims`, `invariants`, `decisions` with rejected alternatives, `deferred`, `affected_resources`, `evidence`); `ouroboros/tools/plan_spec.py` validates and normalizes its full operative content without shortening strings or dropping excess items, then mints the ids that are the only valid `breaks` targets (`goal`, `claim_N`, `invariant_N`, `decision_N`, `deferred_N`) — host-minted positionally, because a caller-chosen id could shadow another target and corrupt what a blocking finding `breaks` — and hashes it. Governance documents always come from the system repository; declared targets and evidence resolve against `active_repo_dir_for(ctx)`, and a path escaping the active subject or an unreadable root is a named omission, never a silent gap.
`plan_task` reviews an INTENTION before the work starts — the same organ for code, research, deliverables, and actions (BIBLE P3). The obligation is constitutional and the finalization gate is structural (`owner_hurry.force_plan_decision`); the order in which a task asks, explores and plans is the mind's judgment, and no prompt choreographs it. The submitted envelope carries the goal, the plan prose, and a typed domain-neutral SPEC (`in_scope`, `non_goals`, `acceptance_claims`, `invariants`, `decisions` with rejected alternatives, `deferred`, `affected_paths` — REQUIRED, the files the work will CHANGE, `[]` when it changes none — `affected_resources` (the same question in words: systems, services, projects, people, never resolved as paths), `evidence`); a submitted spec without `affected_paths` is refused before any dispatch with the typed `PLAN_RESOURCE_FORM_REQUIRED`, which records nothing and leaves an open wave collectable; `ouroboros/tools/plan_spec.py` validates and normalizes its full operative content without shortening strings or dropping excess items, then mints the ids that are the only valid `breaks` targets (`goal`, `claim_N`, `invariant_N`, `decision_N`, `deferred_N`) — host-minted positionally, because a caller-chosen id could shadow another target and corrupt what a blocking finding `breaks` — and hashes it. Governance documents always come from the system repository; declared targets and evidence resolve against `active_repo_dir_for(ctx)`, and a path escaping the active subject or an unreadable root is a named omission, never a silent gap.
ONE structural fact tiers the governance pack: `constitutional` is true iff a declared `affected_resources`/`evidence` PATH locator resolves under the Ouroboros system repository (an `evidence` path only if it exists). A constitutional plan carries BIBLE.md and ARCHITECTURE.md in full (an `api_chat` row inline; a retrieving `agent_session` row as mandatory full reads) — the constitutional packet is not tiered, so assembling it without either is a typed failure, never a disclosure — and every other plan carries the runtime heading-derived navigation maps (`context_layout.generate_doc_nav_map`, never a copy) plus resolvable pointers. There is no plan-kind taxonomy, no agent-declared `plan_class`, no planning scouts, and no plan Atlas. The ONE caller-facing strength axis is the envelope's optional `reviewer_effort`: the panel's effort for THIS order, placed on the default rung of each reviewer row's ladder (`reviewer_slot_config.row_effort`: an explicit per-row effort, then a compound Cursor/Agy route slug, then the declaration, then the owner's `OUROBOROS_EFFORT_REVIEW` setting), passed as an ARGUMENT of `plan_review_slots` only, so the commit gate, scope, acceptance and skill review keep reading the untouched rows. Effort is roster identity: a different declaration re-dispatches a paid panel within `OUROBOROS_REVIEW_MAX_CYCLES`, the same one replays free, and a wave records the declaration (`reviewer_effort`) so a collection rebuilds the roster it was dispatched with. Scoping against BIBLE P1 ("Cognitive horizon is part of continuity"): this is the review panel's strength for one order, ordered by the mind under the owner's setting as the default (the owner chose this scoping: the setting is the default and Ouroboros may order stronger or weaker per envelope), never the core's own model, effort or horizon; the disclosed residual is that a cheap panel's closed GREEN is earned authority for that envelope and a later stronger order does not reopen it. The last-execution projection keeps a declared effort apart from the row's saved effort (`declared_effort`).
ONE structural fact tiers the governance pack: `constitutional` is true iff a declared `affected_paths` locator resolves under the Ouroboros system repository, whether or not that file exists yet (creating `ouroboros/new_module.py` IS self-modification). Nothing else resolves as a path: `affected_resources` is prose, and an `evidence` locator is something to LOOK AT — reading a repository file is not changing it, so it never buys the pack on its own, and the system-repo reads are named in the disclosure note instead. A constitutional plan carries BIBLE.md and ARCHITECTURE.md in full (an `api_chat` row inline; a retrieving `agent_session` row as mandatory full reads) — the constitutional packet is not tiered, so assembling it without either is a typed failure, never a disclosure — and every other plan carries the runtime heading-derived navigation maps (`context_layout.generate_doc_nav_map`, never a copy) plus resolvable pointers. There is no plan-kind taxonomy, no agent-declared `plan_class`, no planning scouts, and no plan Atlas. The ONE caller-facing strength axis is the envelope's optional `reviewer_effort`: the panel's effort for THIS order, placed on the default rung of each reviewer row's ladder (`reviewer_slot_config.row_effort`: an explicit per-row effort, then a compound Cursor/Agy route slug, then the declaration, then the owner's `OUROBOROS_EFFORT_REVIEW` setting), passed as an ARGUMENT of `plan_review_slots` only, so the commit gate, scope, acceptance and skill review keep reading the untouched rows. Effort is roster identity: a different declaration re-dispatches a paid panel within `OUROBOROS_REVIEW_MAX_CYCLES`, the same one replays free, and a wave records the declaration (`reviewer_effort`) so a collection rebuilds the roster it was dispatched with. Scoping against BIBLE P1 ("Cognitive horizon is part of continuity"): this is the review panel's strength for one order, ordered by the mind under the owner's setting as the default (the owner chose this scoping: the setting is the default and Ouroboros may order stronger or weaker per envelope), never the core's own model, effort or horizon; the disclosed residual is that a cheap panel's closed GREEN is earned authority for that envelope and a later stronger order does not reopen it. The last-execution projection keeps a declared effort apart from the row's saved effort (`declared_effort`).
Declared evidence is resolved by `ouroboros/tools/plan_evidence.py` against exactly two allowed roots — the active workspace and the system repository — with the shared sensitive-name policy applied to locator and target; every refused, missing, truncated, oversized, binary, or URL locator becomes a typed omission row in the manifest (the host never fetches a URL), and a `need_evidence` locator a reviewer names is attached by the host on the next cycle through the same policy. What that policy cannot attach is a named `[reviewer-requested]` omission row the panel is dispatched with and judges with, never a reason to run no reviewer; the evidence continuation uses a fresh full-packet dispatch only when no exact artifact reference exists, disclosed per slot as a `capability_delta`. An unreadable referenced artifact instead fails closed with `plan_review_exact_artifact_unavailable` and never mints replacement authority. A locator may carry an exact range — `::lines=A-B`, `::bytes=A-B`, `::tail=N`, or `::symbol=Name` for `.py` sources — and a source above the per-item byte bound is attached head-first with the cut named. The manifest hash joins the spec hash and `constitutional` in the wave fingerprint, so changing what the reviewers can see changes the identity of the review. The root exploration log remains outside that identity. Automatic own-room evidence uses the recorded source selected by the author-request identity described below.

View file

@ -813,8 +813,9 @@ MUST include these artifacts as **first-class context sections** — not as
optional or opportunistic inclusions via touched-file packs.
Plan review is the one flow whose governance pack is tiered, and by ONE
structural fact — whether a declared `affected_resources` target, or an
EXISTING `evidence` path, resolves under the Ouroboros system repository —
structural fact — whether a declared `affected_paths` target (a file the work
will CHANGE) resolves under the Ouroboros system repository; `affected_resources`
is prose the host never resolves, and an `evidence` path is a read, not a change —
never by prose and never by a plan-kind
taxonomy, which is what keeps classification un-gameable. This is a tiering,
not an omission: before any work exists the reviewer's subject is the
@ -859,7 +860,7 @@ The context-delivery registry:
| Advisory pre-review (`tools/claude_advisory_review.py`) | Two delivery classes: an `api_chat` row runs the bounded NATIVE inspection episode (governance docs reached through its read-only tools); an `agent_session` row receives a resolvable pointer marked MANDATORY FULL READ and the session reads the full doc itself — retrieval is disclosed (native reads are host-observed; vendor-session reads are not) | same two delivery classes | same two delivery classes |
| Scope review (`tools/scope_review.py`) | full canonical doc + Atlas accounting; a size terminal or a degradation rung under a cold density cap takes the shared cold-start density rung above (one probe, one rebuild, `density_probe` ladder step) | full canonical doc + Atlas accounting | full canonical doc + Atlas accounting |
| Skill review (`skill_review.py`) | full inline (`api_chat`) / mandatory full source-root read (`agent_session`) | full inline (`api_chat`) / mandatory full source-root read (`agent_session`) | full inline (`api_chat`) / mandatory full source-root read (`agent_session`) |
| Plan review (`tools/plan_review.py`) | full for a SELF-MODIFICATION plan (structural path fact: a declared `affected_resources` target, or an existing `evidence` path, resolves under the system repo); otherwise a heading-derived navigation map of BIBLE.md generated at runtime (never a copy) | inline, in full, for a self-modification plan; otherwise the lossless navigation map + a resolvable pointer (W3) | named on-demand pointer; a reviewer that needs it returns `need_evidence` (an exact `::lines=A-B` range for one section) and the host attaches what the evidence policy allows on the next cycle, naming every absence |
| Plan review (`tools/plan_review.py`) | full for a SELF-MODIFICATION plan (structural path fact: a declared `affected_paths` target resolves under the system repo, whether or not the file exists yet; an `evidence` read of a repo file does not); otherwise a heading-derived navigation map of BIBLE.md generated at runtime (never a copy) | inline, in full, for a self-modification plan; otherwise the lossless navigation map + a resolvable pointer (W3) | named on-demand pointer; a reviewer that needs it returns `need_evidence` (an exact `::lines=A-B` range for one section) and the host attaches what the evidence policy allows on the next cycle, naming every absence |
| Deep self-review (`deep_self_review.py`) | Three deliveries on the `deep_review` row. Packed api row: full canonical doc + Atlas accounting; a required set unfit under a cold density cap gets one bounded probe send and one rebuild, and a pack still unfit is the typed `deep_self_review_pack_unfit` refusal asking the owner to switch the row (no automatic fallback). Native inspection episode / agent session: MANDATORY full read at the repository root, named with its size in the task; the native episode's `read_file` receipts carry the delivered line extent and the host merges the repository-root intervals for BIBLE.md afterwards — `read` only on full coverage, else `partial`/`missing`/`unobserved` — disclosed in the report header and as a typed `capability_delta` (host-observed reads); a session's reads are `unobserved`. Memory (up to seven whitelisted files) is INLINED byte-exact on every delivery, each entry's disposition disclosed (task text, `deep_review_memory`, header `memory=n/7`) — never receipt-checked | Packed: full (max) / navigation map (low) + Atlas accounting. Retrieving rows: navigation map (`generate_doc_nav_map`), sections read on demand | Packed: full canonical doc + Atlas accounting. Retrieving rows: navigation map, sections read on demand (CHECKLISTS.md likewise) |
Skill Review keeps the full stable governance/host prefix for cache-friendly

View file

@ -537,7 +537,8 @@ def plan_review_reminder(decision: Dict[str, Any]) -> str:
if status == "legacy_open_requires_resubmission":
return (
f"{tag} An open plan review from a previous schema cannot be honored. Re-call "
"plan_task with your goal, plan and spec to start a fresh review before finalizing."
"plan_task with your goal, plan and spec — including affected_paths, the files the "
"work will change ([] when none) — to start a fresh review before finalizing."
)
from ouroboros.review_cycles import review_max_cycles
@ -578,13 +579,15 @@ def plan_review_reminder(decision: Dict[str, Any]) -> str:
)
if outcome == "REVISE_PLAN":
return (
f"{tag} Blocking plan review requires a revised spec. Change the spec and call "
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."
)
return (
f"{tag} Call plan_task with a concrete goal, plan and spec. If review infrastructure "
f"{tag} Call plan_task with a concrete goal, plan and spec, whose affected_paths lists "
"the files the work will change ([] when none). If review infrastructure "
"is unavailable, continue analysis and non-mutating preparation, but do not begin the "
"work before the review closes or a real task-wide rail fires."
)

View file

@ -156,6 +156,7 @@ BAND_PATHS = {
"ouroboros/tools/commit_gate.py": "Grew INTO the band by the review-wave fix binding the actor reference (delivery class) into the commit review contract fingerprint \u2014 same-module contract identity, splitting it would separate the fingerprint from its gate.",
"ouroboros/tools/core.py": "D05 ledger split (rows 311-349): read/list and owner-chat delivery spans moved to core_file_tools/core_artifacts; facade re-enters the band from above (2283 -> 1373) and shrinks further when the residual catalog split lands",
"ouroboros/tools/delegate.py": "D07 finisher DEL1 split brought the nanny-verb monolith DOWN from the 1600 hard cap into the band (1600->1263); terminal-evidence family extracted to tools/delegate_terminal_evidence.py, shrink-only direction",
"ouroboros/tools/plan_review.py": "Entered the band from 999 lines: the required-affected_paths form (owner 9=A) added the schema field and the PLAN_RESOURCE_FORM_REQUIRED refusal, which must name the task's open wave and the $0 disposition exit \u2014 it belongs beside the one preamble both the paid and dry-run paths share, not in the pure plan_spec companion that owns no task state.",
"ouroboros/tools/plan_review_runtime.py": "Entered the band from 986 lines: timeout custody synthesis joined the existing plan-review runtime owner while preserving profile-continuity disclosures and typed health facts during target integration.",
"ouroboros/tools/registry_core.py": "F3.1 typed-organ re-split (D04 rows 156/167/170/171/174/175): the tip ToolRegistry class body re-homed whole from the protected registry facade; the guard/dispatch surface already left for its sibling leaves, and the class shrinks further only with the ABI-8 post-release handler conversion.",
"ouroboros/tools/review.py": "D06 F2.3a re-entry by extraction: the multi-model fan-out moved to review_multi_model.py (1550->1269); the remaining single-owner review cycle machinery lands in the 1001-1500 band with headroom",

View file

@ -2,7 +2,7 @@
Owner-approved redesign (2026-08-15, plan §6/§7): the agent submits a SPEC (goal,
in_scope, non_goals, acceptance_claims, invariants, decisions, deferred,
affected_resources, evidence) plus prose; the host normalizes it (``plan_spec``),
affected_paths, affected_resources, evidence) plus prose; the host normalizes it (``plan_spec``),
resolves ONE structural fact (``constitutional``), attaches the declared evidence
bounded with every omission named, builds the lean packet (``plan_packet``), fans it
across the configured reviewer rows through the shared review substrate (api_chat
@ -181,11 +181,21 @@ _SPEC_SCHEMA = {
},
"description": "Deliberately deferred until the work is underway; ids deferred_1..N — a blocking finding may name any spec id it breaks: goal, claim_N, invariant_N, decision_N or deferred_N itself.",
},
"affected_paths": {
"type": "array", "items": {"type": "string"},
"description": (
"REQUIRED. The FILES the work will CHANGE — paths relative to the subject "
"workspace root, absolute, or file://; send [] when this work changes no files. "
"A path under the Ouroboros system repository makes the plan constitutional "
"(BIBLE + ARCHITECTURE go to reviewers), whether or not the file exists yet. "
"This is the ONLY field read as file paths."
),
},
"affected_resources": {
"type": "array", "items": {"type": "string"},
"description": (
"What the work will CHANGE (paths, systems, services). Paths resolving under "
"the Ouroboros system repository make the plan constitutional (BIBLE goes to reviewers)."
"What else the work will CHANGE, described in words: systems, services, projects, "
"people. Descriptions only — never file paths; nothing here is resolved as a path."
),
},
"evidence": {
@ -193,12 +203,12 @@ _SPEC_SCHEMA = {
"description": (
"What reviewers should LOOK AT: file paths, task:<id> of a prior task, URLs. "
"The host attaches files bounded (never fetching URLs) and names every omission. "
"An EXISTING path here that resolves under the Ouroboros system repository also "
"makes the plan constitutional (BIBLE + ARCHITECTURE go to reviewers); a path that "
"does not exist does not, and the skip is disclosed."
"Reading a repository file here is not changing it and does not make the plan "
"constitutional; only listing it under affected_paths does."
),
},
},
"required": ["affected_paths"],
}
_DISPOSITION_SCHEMA = {
@ -252,7 +262,8 @@ def get_tools():
"new; a different reviewer_effort re-dispatches a paid panel). Under blocking enforcement an "
"open review holds finalization; under advisory you may proceed with the "
"review open and the host discloses it. Declare evidence reviewers need; "
"declare affected_resources so a self-modification gets the constitutional pack."
"affected_paths is required — the files the work will change ([] when none) — "
"and is what gives a self-modification the constitutional pack."
),
"parameters": {
"type": "object",
@ -407,6 +418,34 @@ def _reviewer_requested_locators(ctx: ToolContext, state_root: pathlib.Path) ->
seen.append(loc)
return seen, dropped
def _resource_form_refusal(ctx: ToolContext, state_root: pathlib.Path) -> str:
"""The old-form refusal text: name the missing field, show it, and protect an open wave.
Nothing durable is written, so a task whose previous wave is still open keeps it — and is
told how to close it at $0 instead of re-buying a panel it cannot afford."""
detail = ""
try:
_root, task_id = _planning_state_location(ctx)
wave = current_plan_review_wave(load_plan_review_state(state_root, task_id)) or {}
except (OSError, TimeoutError, ValueError):
wave = {}
if wave and not wave.get("closed"):
detail = (
"\nThe open plan-review wave "
f"{wave.get('request_fingerprint') or '(fingerprint not recorded)'} is untouched: answer "
"it first with review_disposition naming that fingerprint (free, no reviewer call), or "
"re-send this spec in the form above."
)
return (
"ERROR: PLAN_RESOURCE_FORM_REQUIRED: spec.affected_paths is required — the FILES this work "
"will change, as their own list; send [] when it changes no files. affected_resources is "
"now prose (systems, services, projects, people) and is never resolved as a path. "
"For example:\n"
' "affected_paths": ["ouroboros/tools/plan_review.py"],\n'
' "affected_resources": ["the plan-review organ", "the owner\'s review budget"]\n'
"No reviewer was called and nothing was recorded." + detail
)
def _prepare_plan_inputs(ctx: ToolContext, request: "_PlanRequest", state_root: pathlib.Path, *, persist: bool = False) -> dict:
"""The ONE preamble the paid path and the dry-run seam share: normalize the spec (with the
envelope's goal injected), resolve the subject roots, derive `constitutional`, attach the
@ -423,21 +462,29 @@ def _prepare_plan_inputs(ctx: ToolContext, request: "_PlanRequest", state_root:
if errors:
return {"error": "ERROR: PLAN_SPEC_INVALID: " + "; ".join(errors) + ". No reviewer was called.",
"code": "TOOL_ARG_ERROR"}
if isinstance(raw_spec, dict) and "affected_paths" not in raw_spec:
# Owner 9=A: a spec in the old mixed form is refused BEFORE any paid dispatch, because
# `affected_resources` used to be read as a path list — a prose item became "a file under
# the Ouroboros repo" and bought every reviewer the whole constitution (~470k tokens/cycle)
# for a deck. The refusal carries its OWN code: a message containing `PLAN_SPEC_INVALID`
# takes the durable superseding-attempt path below and would orphan an open wave.
return {"error": _resource_form_refusal(ctx, state_root), "code": "TOOL_ARG_ERROR"}
from ouroboros.review_substrate import review_repo_dirs_for
try:
system_root, active_root = review_repo_dirs_for(ctx)
except ValueError as exc:
return {"error": f"ERROR: PLAN_SUBJECT_ROOT_INVALID: {exc}", "code": "TOOL_ERROR"}
locators = list(spec["affected_resources"]) + list(spec["evidence"])
affected_paths = list(spec.get("affected_paths") or [])
locators = affected_paths + list(spec["evidence"])
constitutional, constitutional_note = plan_spec.resolve_constitutional(
active_root=active_root, system_repo_root=system_root,
affected_resources=spec["affected_resources"], evidence=spec["evidence"],
affected_paths=affected_paths, evidence=spec["evidence"],
payload_roots=_plan_payload_roots(ctx, locators),
)
reminder = (
"REMINDER: affected_resources is empty; if this work will change Ouroboros's own body, "
"declare those paths so reviewers receive the constitutional pack (BIBLE)."
if active_root == system_root and not spec["affected_resources"] else ""
"REMINDER: affected_paths is empty; if this work will change Ouroboros's own body, "
"list those files so reviewers receive the constitutional pack (BIBLE)."
if active_root == system_root and not affected_paths else ""
)
declared_evidence = list(spec["evidence"]) # W3: earlier-cycle need_evidence is HOST-attached
try:

View file

@ -22,7 +22,7 @@ from hashlib import sha256
import json
import pathlib
import re
from typing import Any, Callable, Iterable, Mapping, Optional
from typing import Any, Iterable, Mapping, Optional
from ouroboros.config import adaptive_quorum
from ouroboros.contracts.task_contract import normalize_acceptance_claims
@ -57,10 +57,12 @@ AUTHOR_DISPOSITION_SCHEMA = {
}
DISPOSITION_DECISIONS = ("accept", "reject", "defer")
_SPEC_STRING_LISTS = ("in_scope", "non_goals", "invariants", "affected_resources", "evidence")
_SPEC_STRING_LISTS = (
"in_scope", "non_goals", "invariants", "affected_paths", "affected_resources", "evidence",
)
_SPEC_KEYS = frozenset({
"goal", "in_scope", "non_goals", "acceptance_claims", "invariants",
"decisions", "deferred", "affected_resources", "evidence",
"decisions", "deferred", "affected_paths", "affected_resources", "evidence",
})
_URL_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.\-]*://")
_FILE_SCHEME = "file://"
@ -224,6 +226,9 @@ def normalize_spec(raw: Mapping[str, Any] | None) -> tuple[dict, list[str]]:
Invariant ids are positional (``invariant_1..N``) because the schema keeps
``invariants: [str]``; ``spec_ids``/``spec_with_ids`` derive them; the id
``goal`` is reserved for the intention as a whole (D32).
``affected_paths`` (the files the work will CHANGE) appears in the normalized
spec only when the input declared it, so a spec stored before the field existed
keeps its recorded identity byte for byte.
"""
errors: list[str] = []
omissions: list[str] = []
@ -240,6 +245,13 @@ def normalize_spec(raw: Mapping[str, Any] | None) -> tuple[dict, list[str]]:
)
spec: dict[str, Any] = {"goal": goal}
for key in _SPEC_STRING_LISTS:
# `affected_paths` is the ONE key that is never defaulted: a spec RECORDED before the
# field existed must normalize to the same bytes as it did then, or its `spec_hash` /
# `plan_fingerprint` move and the open wave it belongs to becomes uncollectable. New
# submissions always carry the key — `plan_review` refuses the old mixed form before
# any paid dispatch, so the absence here only ever means "stored under the old shape".
if key == "affected_paths" and key not in raw:
continue
spec[key] = _string_list(raw.get(key), key, errors, omissions)
seen = {GOAL_ID, *(f"invariant_{i}" for i in range(1, len(spec["invariants"]) + 1))}
spec["acceptance_claims"] = _normalize_claims(raw.get("acceptance_claims"), errors, omissions, seen)
@ -248,8 +260,8 @@ def normalize_spec(raw: Mapping[str, Any] | None) -> tuple[dict, list[str]]:
spec["normalization_omissions"] = omissions
ordered = {key: spec[key] for key in (
"goal", "in_scope", "non_goals", "acceptance_claims", "invariants", "decisions",
"deferred", "affected_resources", "evidence", "normalization_omissions",
)}
"deferred", "affected_paths", "affected_resources", "evidence", "normalization_omissions",
) if key in spec}
return ordered, errors
@ -333,7 +345,9 @@ def spec_delta(prev_spec: Mapping[str, Any] | None, spec: Mapping[str, Any]) ->
(added / removed / changed content); plain string lists diff by text; the
goal is a changed flag; ``renumbered`` lists elements whose text moved to a
different id (positional ids shift when an earlier element is dropped or
reordered). ``prev_spec=None`` means everything is added.
reordered); ``declared_keys_changed`` reports a spec whose set of declared keys
itself moved (a spec stored before ``affected_paths`` existed declares no such
key at all). ``prev_spec=None`` means everything is added.
"""
prev = dict(prev_spec or {})
ids: dict[str, list[str]] = {"added": [], "removed": [], "changed": []}
@ -350,12 +364,21 @@ def spec_delta(prev_spec: Mapping[str, Any] | None, spec: Mapping[str, Any]) ->
"removed": [t for t in before_l if t not in after_l],
}
goal_changed = str(prev.get("goal") or "") != str(spec.get("goal") or "")
changed = goal_changed or any(ids.values()) or any(
# A key that appears or disappears between two normalized specs is a change even when both
# sides read as empty: `affected_paths` ABSENT (stored before the field existed) and
# `affected_paths: []` ("this work changes no files") are different claims, and the spec
# hash already says so — the delta the reviewer reads must not say they are the same.
declared_keys_changed = prev_spec is not None and (
{key for key in prev if key != "normalization_omissions"}
!= {key for key in spec if key != "normalization_omissions"}
)
changed = goal_changed or declared_keys_changed or any(ids.values()) or any(
entry["added"] or entry["removed"] for entry in lists.values()
)
return {
"changed": changed,
"goal_changed": goal_changed,
"declared_keys_changed": declared_keys_changed,
"ids": ids,
"lists": lists,
"renumbered": _renumbered(prev, spec),
@ -454,35 +477,31 @@ def _under(path: pathlib.Path, root: pathlib.Path) -> bool:
return False
def _default_exists(path: pathlib.Path) -> bool:
"""Fail CLOSED: only a definite "this path is not there" lets an evidence locator skip
the constitutional escalation. An unreadable path (permissions, a transient I/O error)
counts as present, because the safe error is carrying the constitution needlessly, never
dropping it on a filesystem hiccup."""
try:
return path.exists()
except (OSError, ValueError, RuntimeError):
return True
def resolve_constitutional(
*,
active_root: str | pathlib.Path,
system_repo_root: str | pathlib.Path,
affected_resources: Iterable[str],
affected_paths: Iterable[str],
evidence: Iterable[str],
payload_roots: Iterable[str | pathlib.Path] = (),
evidence_exists: Optional[Callable[[pathlib.Path], bool]] = None,
) -> tuple[bool, str]:
"""ONE structural fact: does this plan touch Ouroboros's own body?
"""ONE structural fact: will this plan CHANGE Ouroboros's own body?
Port of ``plan_review_runtime.resolve_plan_class`` without the enum. True iff
any ``affected_resources`` / ``evidence`` PATH locator (relative resolved
against ``active_root``, absolute or ``file://`` as-is) resolves to or under
the system repository. The active binding alone does NOT decide (owner
decision D29: a plan bound to the system repo that declares no system path
is not constitutional). Skill-payload paths under ``payload_roots`` are
exempt (data plane, as today). URLs and ``task:``/``chat:`` locators never make it
True iff a declared ``affected_paths`` locator (relative resolved against
``active_root``, absolute or ``file://`` as-is) resolves to or under the
system repository — whether or not the file exists yet, because creating
``ouroboros/new_module.py`` IS self-modification. ``affected_resources`` is
never read here: treating every non-URL string in it as a path is what turned
a Russian sentence about a project into "a file under the repo" and billed the
whole constitution to a deck plan (owner decisions 8=A/9=A/16=A).
``evidence`` is what to LOOK AT — reading a repository file is not changing it,
so an evidence locator never decides on its own (16=A); when the work will also
change that file it is in ``affected_paths`` and decides there. System-repo
evidence reads are named in the note so the author can see why the constitutional
pack was not bought (P1). The active binding alone does NOT decide (owner
decision D29: a plan bound to the system repo that declares no system path is
not constitutional). Skill-payload paths under ``payload_roots`` are exempt
(data plane, as today). URLs and ``task:``/``chat:`` locators never make it
true. Returns ``(constitutional, note)`` — the note names the deciding
locator for disclosure.
"""
@ -498,9 +517,10 @@ def resolve_constitutional(
# proposal to drop system-repo-nested payload roots (S-B05) was REJECTED for that
# reason; the residual it names requires control of the payload predicate itself.
payloads = [pathlib.Path(p).resolve(strict=False) for p in payload_roots]
exists = evidence_exists if evidence_exists is not None else _default_exists
skipped: list[str] = []
for label, locators in (("affected_resources", affected_resources), ("evidence", evidence)):
def _system_locators(locators: Iterable[str]):
"""Lazily yield the declared locators that are PATHS resolving under the system repo,
verbatim — the first one is enough to decide, so nothing further is resolved."""
for raw in locators or []:
locator = str(raw or "").strip()
if not _is_path_locator(locator):
@ -509,28 +529,31 @@ def resolve_constitutional(
if resolved is None or any(_under(resolved, payload) for payload in payloads):
continue
if resolved == system or _under(resolved, system):
# An `affected_resources` target counts whether or not it exists yet — creating
# `ouroboros/new_module.py` IS self-modification. An `evidence` locator is only
# something to LOOK AT: a path that does not exist (a typo, a file belonging to
# another workspace) must not drag the constitutional pack in behind it — but the
# skip is DISCLOSED, never reported as "no locator resolved" (P1).
if label == "evidence" and not exists(resolved):
skipped.append(locator)
continue
# The locator is quoted VERBATIM (never ``repr``): the note is disclosure a
# reviewer copies back, and ``repr`` doubles every backslash of a Windows path.
return True, (
f"constitutional: {label} locator '{locator}' resolves under the "
"Ouroboros system repository (structural fact)"
)
if skipped:
listed = ", ".join(f"'{item}'" for item in skipped[:5])
return False, (
"not constitutional: the only system-repo locators declared are EVIDENCE paths that do "
f"not exist ({listed}) — declare them under "
"affected_resources if the work will change them"
yield locator
for target in _system_locators(affected_paths):
# The locator is quoted VERBATIM (never ``repr``): the note is disclosure a
# reviewer copies back, and ``repr`` doubles every backslash of a Windows path.
return True, (
f"constitutional: affected_paths locator '{target}' resolves under the "
"Ouroboros system repository (structural fact)"
)
return False, "not constitutional: no declared locator resolves under the Ouroboros system repository"
reads: list[str] = []
for read in _system_locators(evidence):
reads.append(read)
if len(reads) >= 5: # the note names them; a sixth adds nothing a reviewer acts on
break
if reads:
listed = ", ".join(f"'{item}'" for item in reads)
return False, (
"not constitutional: the system-repo locators declared are EVIDENCE reads "
f"({listed}), and reading a repository file is not changing it — list a file "
"under affected_paths if the work will change it"
)
return False, (
"not constitutional: no declared affected_paths locator resolves under the "
"Ouroboros system repository"
)
# ----------------------------------------------------------------------- findings

View file

@ -14,7 +14,10 @@ Usage (from anywhere):
``spec.json`` follows the plan_task spec schema: in_scope, non_goals, acceptance_claims,
invariants, decisions[{choice, rejected, why}], deferred[{what, why_safe_to_defer}],
affected_resources, evidence (``--evidence`` values are appended to it).
affected_paths (REQUIRED — the files the work will CHANGE, ``[]`` when it changes none; this
is the only list resolved as paths, and a path under the system repo makes the plan
constitutional), affected_resources (the same question in words: systems, services, projects,
people), evidence (``--evidence`` values are appended to it).
"""
from __future__ import annotations

View file

@ -253,6 +253,8 @@ S11_GOAL = "Write the w3a plan-review smoke note."
S11_SPEC_V1 = {
"in_scope": ["w3a plan-review smoke"],
"acceptance_claims": ["The plan-review smoke completes with a recorded chronicle."],
# Required on every submitted spec (owner 9=A); the smoke note changes no repository file.
"affected_paths": [],
}
S11_SPEC_V2 = {
**S11_SPEC_V1,

View file

@ -9,8 +9,8 @@ from ouroboros.tools import plan_packet, plan_spec
@pytest.mark.parametrize("field", ["goal", "in_scope", "non_goals", "invariants",
"affected_resources", "evidence", "acceptance_claims",
"decisions", "deferred"])
"affected_paths", "affected_resources", "evidence",
"acceptance_claims", "decisions", "deferred"])
def test_operative_tail_changes_normalized_identity_and_current_packet(field):
prefix = "яё𐍈🚀\n" * 500
raw = {"goal": "Full plan", field: prefix + "TAIL_A"}

View file

@ -83,7 +83,8 @@ def test_full_plan_review_disposition_repeat_and_tail_delta(_harness):
ctx = _harness.make_ctx()
goal = "full chosen goal\n" * 22_000 + "GOAL_TAIL"
spec = {"in_scope": ["full requirement\n" * 22_000 + "SCOPE_TAIL_A"],
"acceptance_claims": ["full criterion\n" * 22_000 + "CLAIM_TAIL"]}
"acceptance_claims": ["full criterion\n" * 22_000 + "CLAIM_TAIL"],
"affected_paths": []} # required on every submitted spec (owner 9=A)
assert _control(_call(ctx, spec, goal=goal)) == {"outcome": "REVIEW_REQUIRED", "closed": True}
state = _state(_harness)
fingerprint = state["waves"][-1]["request_fingerprint"]

View file

@ -0,0 +1,262 @@
"""The plan spec's resource form: `affected_paths` is the ONE list the host resolves.
Owner decisions 8=A / 9=A / 16=A, taken after a live wave: the old `affected_resources` list
was read as "everything that is not a URL is a file path", so the prose item «Отдельный проект
«TSMC — 10-летний инвестиционный анализ»» resolved to 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 live history not one of the 14
constitutional verdicts came from a real file path in that list.
What is pinned here: a declared CHANGE target decides (existing or not); prose never resolves
and never reaches the filesystem; an evidence READ of a repository file is named but buys
nothing; a spec submitted in the old mixed form is refused before any dispatch without writing
anything; and a wave recorded under the old shape still closes for free.
"""
from __future__ import annotations
import copy
import json
from ouroboros.tools import plan_spec
from tests.test_plan_review_engine import ( # noqa: F401 — `harness` is the fixture
CLEAN,
DECK_SPEC,
_call,
_control,
_finding,
_state,
harness,
)
# Verbatim from the live wave of task 12102247e791ff1e (structural-health sprint evidence,
# plan-resource-live-examples.json): the list that made a deck plan constitutional.
TSMC_RESOURCES = [
"Отдельный проект «TSMC — 10-летний инвестиционный анализ»",
"Файлы исследования, моделей и PDF внутри его project workspace",
"Финальные пользовательские deliverables",
]
# The three plan-review waves recorded on the live install while `affected_resources` was still
# resolved as a path list (structural-health sprint evidence, plan-resource-live-examples.json:
# tasks 12102247e791ff1e, 0be6fb0690f44bbd, 6596589bb3674aaa). Their resource lists are verbatim;
# the plan bodies stay on the owner's machine. The digests were taken from this module BEFORE
# `affected_paths` existed — a stored wave must keep its identity or the task it belongs to can
# never collect or dispose of it again.
STORED_LEGACY_WAVES = (
("12102247e791ff1e",
["Отдельный проект «TSMC — 10-летний инвестиционный анализ»",
"Файлы исследования, моделей и PDF внутри его project workspace",
"Финальные пользовательские deliverables"],
[],
"8832d9db81cbd61dea584c193130e25b3c63fea25b007b7978456f07f92e1f82",
"40bc4f76e9053c02066ee0900a6ebde424af70450adfe2e2fdf6ccff1b87d337"),
("0be6fb0690f44bbd",
["/Users/anton/Ouroboros/projects/TSMC___10-____________________________"],
[],
"a8aea6bb3adbb7dcb635dbebb7161d238d34168502420b7be4fd4f4c1692f06f",
"8e652cd2dbbce9ebfa1ecafe147cea4adf41672551995e83659b2e712d869bcb"),
("6596589bb3674aaa",
["/Users/anton/Ouroboros/data/skills/external/context-lens",
"/Users/anton/Ouroboros/projects/Context_Lens___________________________________"],
["data/skills/external/context-lens/SKILL.md"],
"2e2764e09bc74383c082bae1eabc44c9acb24c5c8cd4048c4e77187375bbc240",
"9acfa5071662c07c1f716c5e15a0fd1648c56b64df96a3e5aae1b9289e91fbff"),
)
def test_stored_specs_from_before_affected_paths_keep_their_recorded_identity():
"""`affected_paths` is never defaulted onto input that did not declare it: normalizing a
spec recorded under the old shape yields the same key list, the same spec hash and the same
plan fingerprint it was recorded with."""
for task_id, resources, evidence, spec_digest, fingerprint in STORED_LEGACY_WAVES:
raw = {"goal": f"stored plan wave {task_id}", "affected_resources": list(resources),
"evidence": list(evidence)}
spec, errors = plan_spec.normalize_spec(raw)
assert errors == []
assert "affected_paths" not in spec
assert list(spec) == [
"goal", "in_scope", "non_goals", "acceptance_claims", "invariants", "decisions",
"deferred", "affected_resources", "evidence", "normalization_omissions",
]
assert plan_spec.spec_hash(spec) == spec_digest
assert plan_spec.plan_fingerprint(
spec["goal"], "stored prose", spec, "manifest-hash", False) == fingerprint
def test_spec_delta_sees_the_affected_paths_claim_appear_and_then_move():
"""Absent ("stored before the field existed") and `[]` ("this work changes no files") are
different claims, so the delta the next reviewer reads must not call them the same."""
base = {"goal": "g", "affected_resources": ["the uploader"]}
legacy, _ = plan_spec.normalize_spec(base)
declares_none, _ = plan_spec.normalize_spec({**base, "affected_paths": []})
declares_file, _ = plan_spec.normalize_spec({**base, "affected_paths": ["ouroboros/uploader.py"]})
appeared = plan_spec.spec_delta(legacy, declares_none)
assert appeared["changed"] and appeared["declared_keys_changed"]
assert appeared["prev_hash"] != appeared["hash"]
moved = plan_spec.spec_delta(declares_none, declares_file)
assert moved["changed"] and moved["declared_keys_changed"] is False
assert moved["lists"]["affected_paths"] == {"added": ["ouroboros/uploader.py"], "removed": []}
assert plan_spec.spec_delta(declares_file, declares_file)["changed"] is False
def _system_prompt(substrate, index=0):
return substrate.calls[index]["request"].messages[0]["content"][0]["text"]
def test_a_declared_new_source_file_is_constitutional_before_it_exists(harness): # noqa: F811
"""A file the work will CREATE has no bytes and no parent directory on disk yet; writing
`ouroboros/new/deep/module.py` IS self-modification, so the full pack must ride."""
target = harness.system / "ouroboros" / "new" / "deep" / "module.py"
assert not target.exists() and not target.parent.exists()
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
out = _call(harness.make_ctx(), spec={**DECK_SPEC, "affected_paths": [str(target)]})
wave = _state(harness)["waves"][-1]
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 "## ARCHITECTURE.md (architecture and data flow" in system_prompt
assert "on-demand pointer" not in system_prompt
assert "REMINDER" not in out
def test_prose_resources_never_reach_the_filesystem_and_never_buy_the_pack(harness, monkeypatch): # noqa: F811
"""The TSMC list, exactly as the live wave sent it: descriptions of a project, its files and
its deliverables. With no change target declared the plan is ordinary, the constitutional
resolver resolves nothing at all, and the host reminds the agent that the list it CAN use
for that is `affected_paths`."""
resolved: list[str] = []
real = plan_spec._resolve_locator_path
monkeypatch.setattr(
plan_spec, "_resolve_locator_path",
lambda locator, root: (resolved.append(locator), real(locator, root))[1],
)
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
spec = {**DECK_SPEC, "affected_paths": [], "affected_resources": list(TSMC_RESOURCES)}
# `active_workspace=False` binds the task to the system repo itself: the one situation where
# an empty change-target list is worth a reminder (D29 keeps the binding from deciding).
out = _call(harness.make_ctx(active_workspace=False), spec=spec)
assert resolved == []
wave = _state(harness)["waves"][-1]
assert wave["constitutional"] is False
assert wave["constitutional_note"] == (
"not constitutional: no declared affected_paths locator resolves under the "
"Ouroboros system repository"
)
assert "REMINDER: affected_paths is empty" in out
system_prompt = _system_prompt(substrate)
assert "## BIBLE.md (constitution" not in system_prompt and "on-demand pointer" in system_prompt
def test_reading_a_repository_file_is_not_changing_it(harness): # noqa: F811
"""Owner 16=A. The same locator, twice: as evidence alone it is named in the note and the
pack stays a pointer; listed as a change target too, it escalates."""
bible = str(harness.system / "BIBLE.md")
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
_call(harness.make_ctx(), spec={**DECK_SPEC, "affected_paths": [], "evidence": [bible]})
read_only = _state(harness)["waves"][-1]
assert read_only["constitutional"] is False
assert "EVIDENCE reads" in read_only["constitutional_note"] and bible in read_only["constitutional_note"]
assert "## BIBLE.md (constitution" not in _system_prompt(substrate)
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
_call(harness.make_ctx(task_id="task-2"),
spec={**DECK_SPEC, "affected_paths": [bible], "evidence": [bible]})
changing = _state(harness, "task-2")["waves"][-1]
assert changing["constitutional"] is True
assert "affected_paths" in changing["constitutional_note"]
assert "## BIBLE.md (constitution" in _system_prompt(substrate)
def test_a_legacy_form_submission_is_refused_and_records_nothing(harness): # noqa: F811
"""Owner 9=A: the old mixed form is refused BEFORE any dispatch. The refusal quotes the
field with an example, and — because its code is not the `PLAN_SPEC_INVALID` family that
supersedes the current attempt — the task's recorded plan state does not move a byte."""
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
ctx = harness.make_ctx()
assert _control(_call(ctx))["closed"] is True
before = json.dumps(_state(harness), sort_keys=True, ensure_ascii=False)
legacy = {key: value for key, value in DECK_SPEC.items() if key != "affected_paths"}
legacy["affected_resources"] = list(TSMC_RESOURCES)
out = _call(ctx, spec=legacy)
assert "PLAN_RESOURCE_FORM_REQUIRED" in out and "spec.affected_paths is required" in out
assert '"affected_paths": ["ouroboros/tools/plan_review.py"]' in out
assert '"affected_resources": ["the plan-review organ"' in out
assert len(substrate.calls) == 1 # the refused submission called no reviewer
assert json.dumps(_state(harness), sort_keys=True, ensure_ascii=False) == before
def test_a_legacy_form_submission_over_an_open_wave_points_at_the_free_exit(harness): # noqa: F811
"""The expensive mistake this prevents: re-submitting into a refusal while an OPEN wave is
still the live obligation. The refusal names that wave and the $0 way to answer it."""
ask = json.dumps([_finding("f1", "need_evidence", breaks="goal", summary="who signs off?")])
harness.install({"s1": ask, "s2": CLEAN, "s3": CLEAN})
ctx = harness.make_ctx()
assert _control(_call(ctx)) == {"outcome": "REVIEW_REQUIRED", "closed": False}
open_fingerprint = _state(harness)["waves"][-1]["request_fingerprint"]
before = json.dumps(_state(harness), sort_keys=True, ensure_ascii=False)
legacy = {key: value for key, value in DECK_SPEC.items() if key != "affected_paths"}
out = _call(ctx, spec=legacy)
assert "PLAN_RESOURCE_FORM_REQUIRED" in out
assert open_fingerprint in out and "review_disposition" in out
assert json.dumps(_state(harness), sort_keys=True, ensure_ascii=False) == before
def test_a_wave_stored_before_affected_paths_still_closes_at_zero_cost(harness): # noqa: F811
"""No grace mechanism was built for the waves already open (owner, disclosed) — they must
simply stay answerable. A wave recorded under the old spec shape closes through
`review_disposition` with no reviewer call, and its recorded identity does not move."""
from ouroboros import task_results
from ouroboros.tools import plan_review_artifacts as artifacts
from ouroboros.tools.plan_review import _apply_disposition
legacy_spec, errors = plan_spec.normalize_spec({
"goal": "Deliver the TSMC analysis",
"affected_resources": list(TSMC_RESOURCES),
})
assert errors == [] and "affected_paths" not in legacy_spec
stored_hash = plan_spec.spec_hash(legacy_spec)
fingerprint = "c" * 64
wave = {
"schema_version": 2, "cycle_index": 1, "request_fingerprint": fingerprint,
"goal": legacy_spec["goal"], "spec": legacy_spec, "spec_hash": stored_hash,
"aggregate": "REVIEW_REQUIRED", "closed": False, "paid": True, "dispositions": [],
"findings": [{"finding_id": "s1:f1", "id": "f1", "class": "need_evidence", "breaks": "goal",
"locator": "", "summary": "who signs this off?", "recommendation": ""}],
}
artifacts.record_exact_wave(
harness.drive, "task-1", wave, copy.deepcopy(wave), need_evidence_seen=[], page_size=32,
)
task_results.record_plan_review_attempt(
harness.drive, "task-1", fingerprint=fingerprint, status="open",
)
paid_before = int(_state(harness).get("cycles_paid") or 0)
substrate = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
out = _apply_disposition(harness.make_ctx(), {"review_fingerprint": fingerprint, "items": [
{"finding_id": "s1:f1", "decision": "accept", "rationale": "the owner signs it off"},
]})
assert _control(out) == {"outcome": "REVIEW_REQUIRED", "closed": True}
assert substrate.calls == []
state = _state(harness)
assert int(state.get("cycles_paid") or 0) == paid_before
closed = state["waves"][-1]
assert closed["request_fingerprint"] == fingerprint and closed["closed"] is True
assert closed["spec_hash"] == stored_hash and closed["aggregate"] == "REVIEW_REQUIRED"
assert "affected_paths" not in closed["spec"]

View file

@ -355,7 +355,7 @@ def test_malformed_reviewer_slots_block_plan_review_before_any_dispatch(tmp_path
patch.object(pr, "_run_plan_review_slots",
side_effect=AssertionError("no reviewer dispatch")),
):
result = pr._handle_plan_task(ctx, plan="P", goal="G", spec={"in_scope": ["x"]})
result = pr._handle_plan_task(ctx, plan="P", goal="G", spec={"in_scope": ["x"], "affected_paths": []})
assert "Invalid reviewer-slot configuration blocks plan review" in result
assert "not valid JSON" in result
@ -410,7 +410,7 @@ def test_expired_explicit_deadline_skips_before_any_reviewer(monkeypatch, tmp_pa
monkeypatch.setattr(pr, "_plan_review_slots",
lambda: (_ for _ in ()).throw(AssertionError("expired deadline must skip")))
out = asyncio.run(pr._run_plan_review_async(
ctx, pr._PlanRequest(goal="G", plan="P", spec={"in_scope": ["x"]}),
ctx, pr._PlanRequest(goal="G", plan="P", spec={"in_scope": ["x"], "affected_paths": []}),
))
assert out.startswith("PLAN_TASK_SKIPPED_DEADLINE: the task deadline has expired")
@ -437,7 +437,7 @@ def test_expired_deadline_replays_a_recorded_wave_but_never_pays(monkeypatch, tm
monkeypatch.setattr(pr, "_plan_review_slots",
lambda: (_ for _ in ()).throw(AssertionError("no panel under a dead deadline")))
out = asyncio.run(pr._run_plan_review_async(
ctx, pr._PlanRequest(goal="G", plan="P", spec={"in_scope": ["x"]}),
ctx, pr._PlanRequest(goal="G", plan="P", spec={"in_scope": ["x"], "affected_paths": []}),
))
assert out.startswith("PLAN_TASK_SKIPPED_DEADLINE:")
attempt = load_plan_review_state(tmp_path, ctx.task_id)["current_attempt"]
@ -512,8 +512,10 @@ class TestPlanReviewToolRegistration(unittest.TestCase):
spec = params["spec"]["properties"]
self.assertEqual(set(spec), {
"in_scope", "non_goals", "acceptance_claims", "invariants", "decisions",
"deferred", "affected_resources", "evidence",
"deferred", "affected_paths", "affected_resources", "evidence",
})
# The ONE list the host resolves as file paths, and the one a submitted spec must carry.
self.assertEqual(params["spec"]["required"], ["affected_paths"])
disposition = params["review_disposition"]
self.assertEqual(disposition["required"], ["review_fingerprint", "items"])
decision = disposition["properties"]["items"]["items"]["properties"]["decision"]

View file

@ -134,7 +134,10 @@ DECK_SPEC = {
"invariants": ["deliver by Friday", "no confidential numbers"],
"decisions": [{"choice": "one chart per slide", "rejected": ["tables"], "why": "audience"}],
"deferred": [{"what": "color palette", "why_safe_to_defer": "cosmetic"}],
"affected_resources": [],
# `affected_paths` is REQUIRED on every submitted spec (owner 9=A): a deck changes no
# repository file, and `[]` is how a plan says exactly that.
"affected_paths": [],
"affected_resources": ["the Q3 board deck"],
"evidence": [],
}
@ -669,13 +672,14 @@ def test_evidence_omissions_reach_the_packet_and_the_wave(harness):
assert manifest["attached"][0]["sha256"] and "text" not in manifest["attached"][0]
def test_constitutional_from_affected_resources_and_reminder_on_system_binding(harness):
def test_constitutional_from_affected_paths_and_reminder_on_system_binding(harness):
sub = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
# (a) workspace binding, an absolute path into the system repo declared as affected
spec = {**DECK_SPEC, "affected_resources": [str(harness.system / "ouroboros" / "loop.py")]}
# (a) workspace binding, an absolute path into the system repo declared as a CHANGE target
# (owner 8=A: `affected_resources` is prose now and buys nothing, so the trigger is here)
spec = {**DECK_SPEC, "affected_paths": [str(harness.system / "ouroboros" / "loop.py")]}
out = _call(harness.make_ctx(), spec=spec)
wave = _state(harness)["waves"][-1]
assert wave["constitutional"] is True and "affected_resources" in wave["constitutional_note"]
assert wave["constitutional"] is True and "affected_paths" in wave["constitutional_note"]
system_prompt = sub.calls[0]["request"].messages[0]["content"][0]["text"]
assert "## BIBLE.md" in system_prompt and "Principle 3: Immune Integrity" in system_prompt
# W3: a self-modification plan carries ARCHITECTURE.md inline, in full — not a map, not a pointer
@ -688,7 +692,7 @@ def test_constitutional_from_affected_resources_and_reminder_on_system_binding(h
out2 = _call(ctx)
wave2 = _state(harness, "task-2")["waves"][-1]
assert wave2["constitutional"] is False
assert "REMINDER: affected_resources is empty" in out2
assert "REMINDER: affected_paths is empty" in out2
system_prompt2 = sub.calls[1]["request"].messages[0]["content"][0]["text"]
assert "Principle 3: Immune Integrity\n\nreview." not in system_prompt2
assert "on-demand pointer" in system_prompt2
@ -861,8 +865,16 @@ def test_retired_swarm_keys_are_dropped_on_settings_load(tmp_path, monkeypatch):
def test_ratchet_module_sizes():
"""The engine entered the 1001-1500 band with the required-`affected_paths` form gate; the
exact debt is owned by ouroboros/size_ratchet_manifest.py, so the pin here is the band plus
the rule that a banded module must carry a rationale there."""
from ouroboros.size_ratchet_manifest import BAND_PATHS
repo = pathlib.Path(pr.__file__).resolve().parents[2]
assert len((repo / "ouroboros" / "tools" / "plan_review.py").read_text(encoding="utf-8").splitlines()) < 1000
lines = len((repo / "ouroboros" / "tools" / "plan_review.py").read_text(encoding="utf-8").splitlines())
assert lines <= 1500
if lines > 1000:
assert (BAND_PATHS.get("ouroboros/tools/plan_review.py") or "").strip()
assert len((repo / "ouroboros" / "config.py").read_text(encoding="utf-8").splitlines()) <= 1600

View file

@ -62,6 +62,8 @@ def test_run_plan_review_script_runs_the_engine_on_the_new_envelope(monkeypatch,
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")

View file

@ -128,7 +128,7 @@ def test_constitutional_packet_without_architecture_is_a_typed_failure(harness):
agent sees, never a reviewer wave that silently lacks the document."""
sub = harness.install({"s1": CLEAN, "s2": CLEAN, "s3": CLEAN})
(harness.system / "docs" / "ARCHITECTURE.md").unlink()
spec = {**DECK_SPEC, "affected_resources": [str(harness.system / "ouroboros" / "loop.py")]}
spec = {**DECK_SPEC, "affected_paths": [str(harness.system / "ouroboros" / "loop.py")]}
out = _call(harness.make_ctx(), spec=spec)
assert "ARCHITECTURE.md" in out and "W3" in out
assert sub.calls == [] # no reviewer was called
@ -372,7 +372,7 @@ def test_session_task_is_the_compact_form_with_governance_by_mandatory_retrieval
resolvable locators, never ~500k chars inline; api rows still get them inline."""
harness.state["slots"] = _slots(("api1", "m/a"), ("sess1", "cursor=grok", "session"), ("api2", "m/b"))
sub = harness.install({"api1": CLEAN, "sess1": CLEAN, "api2": CLEAN})
spec = {**DECK_SPEC, "affected_resources": [str(harness.system / "ouroboros" / "loop.py")]}
spec = {**DECK_SPEC, "affected_paths": [str(harness.system / "ouroboros" / "loop.py")]}
_call(harness.make_ctx(), spec=spec)
request = sub.calls[0]["request"]
api_system = request.messages[0]["content"][0]["text"]
@ -383,9 +383,12 @@ def test_session_task_is_the_compact_form_with_governance_by_mandatory_retrieval
assert "slots and quorum." not in task and "Principle 3: Immune Integrity\n\nreview." not in task
assert "Plan Review Checklist" in task and "REDACTED snapshot" in task
# the governance documents are the ONE raw-read exception, even when the agent ALSO declared
# BIBLE.md as evidence (declaring it makes the plan constitutional): no contradictory orders
# BIBLE.md as evidence beside changing it (the change target is what makes the plan
# constitutional — owner 16=A): no contradictory orders
sub = harness.install({"api1": CLEAN, "sess1": CLEAN, "api2": CLEAN})
_call(harness.make_ctx(task_id="task-2"), spec={**DECK_SPEC, "evidence": [str(harness.system / "BIBLE.md")]})
_call(harness.make_ctx(task_id="task-2"), spec={**DECK_SPEC,
"affected_paths": [str(harness.system / "BIBLE.md")],
"evidence": [str(harness.system / "BIBLE.md")]})
task2 = sub.calls[0]["request"].session_task
assert "MANDATORY FULL READS" in task2 and "even if the agent also declared them as evidence" in task2
assert f"### {harness.system / 'BIBLE.md'}" in task2 # the redacted snapshot is still there too
@ -416,7 +419,8 @@ def test_state_stays_persistable_at_the_worst_case_request_bounds(tmp_path):
"decisions": [{"choice": wide, "why": wide,
"rejected": [wide] * 8} for _ in range(n_items)],
"deferred": [{"what": wide, "why_safe_to_defer": wide} for _ in range(n_items)],
"affected_resources": [f"{wide[:-4]}a{i:03d}" for i in range(n_items)],
"affected_paths": [f"{wide[:-4]}a{i:03d}" for i in range(n_items)],
"affected_resources": [f"{wide[:-4]}r{i:03d}" for i in range(n_items)],
"evidence": [f"{wide[:-4]}e{i:03d}" for i in range(n_items)],
}
normalized, errors = plan_spec.normalize_spec(spec)
@ -689,13 +693,17 @@ def test_both_reviewer_routes_learn_the_range_selectors(harness):
assert "::lines=A-B" in request.session_task
def test_the_plan_spec_schema_discloses_both_halves_of_the_constitutional_trigger():
"""The trigger reads `affected_resources` AND an existing `evidence` path; the schema the
agent sees says so, including that a non-existent path does not count."""
def test_the_plan_spec_schema_names_affected_paths_as_the_only_resolved_list():
"""The trigger reads ONE list — `affected_paths` — and the schema the agent sees says so:
required, files only, `[]` when none, and the other two lists explicitly not resolved."""
assert pr._SPEC_SCHEMA["required"] == ["affected_paths"]
props = pr._SPEC_SCHEMA["properties"]
assert "system repository" in props["affected_resources"]["description"]
paths = props["affected_paths"]["description"]
assert "REQUIRED" in paths and "system repository" in paths and "[]" in paths
resources = props["affected_resources"]["description"]
assert "never file paths" in resources and "system repository" not in resources
evidence = props["evidence"]["description"]
assert "system repository" in evidence and "EXISTING" in evidence
assert "does not make the plan" in evidence and "affected_paths" in evidence
# ------------------------------------------------------------- in-flight honesty (P1-4)

View file

@ -28,7 +28,8 @@ CODE_SPEC = {
"invariants": ["no new dependencies", "public API unchanged"],
"decisions": [{"choice": "exponential backoff", "rejected": ["fixed delay"], "why": "bursty upstream"}],
"deferred": [{"what": "metrics emission", "why_safe_to_defer": "observability only"}, "log wording"],
"affected_resources": ["ouroboros/uploader.py"],
"affected_paths": ["ouroboros/uploader.py"],
"affected_resources": ["the upload pipeline", "the storage provider"],
"evidence": ["ouroboros/uploader.py", "https://example.com/spec", "task:abc123"],
}
@ -40,7 +41,8 @@ DECK_SPEC = {
"invariants": ["deadline Friday 17:00", "no confidential customer names"],
"decisions": [{"choice": "one message per slide", "rejected": ["dense slides"], "why": "board attention"}],
"deferred": ["colour palette"],
"affected_resources": [],
"affected_paths": [],
"affected_resources": ["the board deck"],
"evidence": [],
}
@ -62,7 +64,7 @@ def test_normalize_spec_mints_ids_and_keeps_schema():
assert spec["normalization_omissions"] == []
assert list(spec) == [
"goal", "in_scope", "non_goals", "acceptance_claims", "invariants", "decisions",
"deferred", "affected_resources", "evidence", "normalization_omissions",
"deferred", "affected_paths", "affected_resources", "evidence", "normalization_omissions",
]
@ -184,11 +186,11 @@ def test_constitutional_active_binding_alone_does_not_decide(tmp_path):
system = tmp_path / "repo"
system.mkdir()
ok, note = plan_spec.resolve_constitutional(
active_root=system, system_repo_root=system, affected_resources=[], evidence=[],
active_root=system, system_repo_root=system, affected_paths=[], evidence=[],
)
assert ok is False and note.startswith("not constitutional")
ok, _ = plan_spec.resolve_constitutional(
active_root=system, system_repo_root=system, affected_resources=["ouroboros/loop.py"], evidence=[],
active_root=system, system_repo_root=system, affected_paths=["ouroboros/loop.py"], evidence=[],
)
assert ok is True # a relative path under the (system) active root still decides
@ -200,56 +202,63 @@ def test_constitutional_declared_paths_relative_absolute_and_file_scheme(tmp_pat
workspace.mkdir()
ok, note = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system,
affected_resources=["../repo/ouroboros/loop.py"], evidence=[],
affected_paths=["../repo/ouroboros/loop.py"], evidence=[],
)
assert ok and "affected_resources" in note
# An EVIDENCE locator counts only when it actually exists: a plan that merely wants to
# LOOK at a repo file is constitutional, a typo pointing at nothing is not (E2E finding).
assert ok and "affected_paths" in note
# Owner 16=A: an EVIDENCE locator is something to LOOK AT, and reading a repo file is not
# changing it — existing or not, it never buys the constitutional pack on its own. Existence
# therefore stopped deciding anything, and the check went with it.
(system / "BIBLE.md").write_text("# constitution\n", encoding="utf-8")
ok, note = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system,
affected_resources=["src/app.py"], evidence=[str(system / "BIBLE.md")],
affected_paths=["src/app.py"], evidence=[str(system / "BIBLE.md")],
)
assert ok and "evidence" in note
assert ok is False and "EVIDENCE reads" in note and str(system / "BIBLE.md") in note
ok, _ = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system,
affected_resources=["src/app.py"], evidence=[str(system / "NO_SUCH_FILE.md")],
affected_paths=["src/app.py"], evidence=[str(system / "NO_SUCH_FILE.md")],
)
assert ok is False
# ...while a TARGET counts whether or not it exists yet — creating a module is self-modification.
ok, _ = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system,
affected_resources=[str(system / "ouroboros" / "brand_new_module.py")], evidence=[],
affected_paths=[str(system / "ouroboros" / "brand_new_module.py")], evidence=[],
)
assert ok is True
ok, _ = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system,
affected_resources=["src/app.py"], evidence=["docs/x.md"],
affected_paths=["src/app.py"], evidence=["docs/x.md"],
)
assert ok is False
# B-06: a system-repo path dressed as file:// is still a path.
ok, note = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system,
affected_resources=[f"file://{system}/ouroboros/loop.py"], evidence=[],
affected_paths=[f"file://{system}/ouroboros/loop.py"], evidence=[],
)
assert ok is True and "file://" in note
def test_constitutional_evidence_selector_classifies_the_source_path(tmp_path):
def test_constitutional_selector_classifies_the_source_path(tmp_path):
"""A `::lines=A-B` suffix names a RANGE of one file: the path in front of it decides, on
either list — as a change target it escalates, as an evidence read it is only named."""
system = tmp_path / "repo"
workspace = tmp_path / "workspace"
workspace.mkdir()
evidence = _write(system / "docs" / "ARCHITECTURE.md", "# Architecture\n")
source = _write(system / "docs" / "ARCHITECTURE.md", "# Architecture\n")
ok, note = plan_spec.resolve_constitutional(
active_root=workspace,
system_repo_root=system,
affected_resources=[],
evidence=[f"{evidence}::lines=1-1"],
active_root=workspace, system_repo_root=system,
affected_paths=[f"{source}::lines=1-1"], evidence=[],
)
assert ok is True
assert f"{evidence}::lines=1-1" in note
assert f"{source}::lines=1-1" in note
ok, note = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system,
affected_paths=[], evidence=[f"{source}::lines=1-1"],
)
assert ok is False
assert f"{source}::lines=1-1" in note and "EVIDENCE reads" in note
def test_constitutional_payload_exempt_url_task_never_and_empty_false(tmp_path):
@ -261,7 +270,7 @@ def test_constitutional_payload_exempt_url_task_never_and_empty_false(tmp_path):
(system / "x").mkdir(parents=True)
ok, _ = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system,
affected_resources=[str(payload / "SKILL.md")], evidence=[], payload_roots=[payload],
affected_paths=[str(payload / "SKILL.md")], evidence=[], payload_roots=[payload],
)
assert ok is False
# A payload root that resolves INSIDE the system repo still exempts: the roots come
@ -271,22 +280,22 @@ def test_constitutional_payload_exempt_url_task_never_and_empty_false(tmp_path):
nested = system / "data" / "skills" / "native" / "demo"
ok, _ = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system,
affected_resources=[str(nested / "SKILL.md")], evidence=[], payload_roots=[nested],
affected_paths=[str(nested / "SKILL.md")], evidence=[], payload_roots=[nested],
)
assert ok is False
# A system path OUTSIDE every payload root is still constitutional.
ok, _ = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system,
affected_resources=[str(system / "ouroboros" / "loop.py")], evidence=[], payload_roots=[nested],
affected_paths=[str(system / "ouroboros" / "loop.py")], evidence=[], payload_roots=[nested],
)
assert ok is True
ok, _ = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system, affected_resources=[],
active_root=workspace, system_repo_root=system, affected_paths=[],
evidence=["https://github.com/razzant/ouroboros/blob/main/ouroboros/loop.py", "task:xyz"],
)
assert ok is False
ok, note = plan_spec.resolve_constitutional(
active_root=workspace, system_repo_root=system, affected_resources=[], evidence=[],
active_root=workspace, system_repo_root=system, affected_paths=[], evidence=[],
)
assert ok is False and note.startswith("not constitutional")
@ -399,7 +408,7 @@ def test_resolve_evidence_and_constitutional_never_raise_on_hostile_locators(tmp
assert manifest["attached"] == []
for locator in (long, "loop1", "\x00bad"):
ok, _ = plan_spec.resolve_constitutional(
active_root=root, system_repo_root=tmp_path / "repo", affected_resources=[locator], evidence=[],
active_root=root, system_repo_root=tmp_path / "repo", affected_paths=[locator], evidence=[],
)
assert ok is False
# A fifo / device is a non-regular file: refused, never opened (would block forever).
@ -869,11 +878,14 @@ def test_capped_reviewer_requests_are_rendered_even_when_the_agent_declared_noth
def test_domain_independence_deck_spec_without_paths(tmp_path):
spec, errors = plan_spec.normalize_spec(DECK_SPEC)
assert errors == [] and spec["affected_resources"] == [] and spec["evidence"] == []
# A deck changes no file: `affected_paths` is the empty claim, while the prose list still
# says what the work touches — and neither one is resolved into a file read.
assert errors == [] and spec["affected_paths"] == [] and spec["evidence"] == []
assert spec["affected_resources"] == ["the board deck"]
assert plan_spec.spec_ids(spec) == frozenset({"goal", "claim_1", "claim_2", "invariant_1", "invariant_2", "decision_1", "deferred_1"})
assert len(plan_spec.spec_hash(spec)) == 64
ok, _ = plan_spec.resolve_constitutional(
active_root=tmp_path / "ws", system_repo_root=tmp_path / "repo", affected_resources=spec["affected_resources"], evidence=spec["evidence"],
active_root=tmp_path / "ws", system_repo_root=tmp_path / "repo", affected_paths=spec["affected_paths"], evidence=spec["evidence"],
)
assert ok is False
manifest = plan_evidence.resolve_evidence(spec["evidence"], active_root=tmp_path, allowed_roots=[tmp_path])

View file

@ -1,6 +1,6 @@
"""Structural constitutional escalation for ``plan_task`` (formerly v6.61.0 plan_class
escalation): the ONE path fact ``constitutional`` — declared ``affected_resources`` /
``evidence`` locators resolving under the Ouroboros system repository (owner D29:
escalation): the ONE path fact ``constitutional`` — declared ``affected_paths`` locators
(the files the work will CHANGE) resolving under the Ouroboros system repository (owner D29:
the active binding alone never decides) — with the skill-payload exemption the
retired ``resolve_plan_class`` applied. Kept from the original file: the
path-escalation and payload-exemption cases, ported to ``plan_spec.resolve_constitutional``
@ -34,7 +34,7 @@ def _resolve(ctx, affected, evidence=()):
active = ctx.active_repo_dir()
return resolve_constitutional(
active_root=active, system_repo_root=system,
affected_resources=list(affected), evidence=list(evidence),
affected_paths=list(affected), evidence=list(evidence),
payload_roots=plan_payload_roots(ctx, list(affected) + list(evidence)),
)
@ -44,7 +44,7 @@ def _resolve(ctx, affected, evidence=()):
def test_system_repo_paths_make_the_plan_constitutional(tmp_path):
ctx = _ctx(tmp_path) # active workspace IS the system repo
ok, note = _resolve(ctx, ["ouroboros/loop.py"])
assert ok and "affected_resources" in note
assert ok and "affected_paths" in note
# D29: the binding alone never decides — nothing declared, not constitutional.
ok, note = _resolve(ctx, [])
assert ok is False and note.startswith("not constitutional")
@ -56,15 +56,15 @@ def test_external_workspace_paths_stay_non_constitutional_unless_absolute_into_s
# An ABSOLUTE path back into the system repo escalates even from a workspace.
abs_sys = str(tmp_path / "sys" / "ouroboros" / "config.py")
ok, note = _resolve(ctx, ["src/app.py", abs_sys])
assert ok and "affected_resources" in note
# A declared EVIDENCE locator under the system repo escalates too — when it exists.
assert ok and "affected_paths" in note
# Owner 16=A: a declared EVIDENCE locator under the system repo is a READ, not a change —
# it never escalates on its own, existing or not, and the host says which reads it saw
# instead of reporting "no locator resolved".
(tmp_path / "sys" / "BIBLE.md").write_text("# constitution\n", encoding="utf-8")
ok, note = _resolve(ctx, ["src/app.py"], evidence=[str(tmp_path / "sys" / "BIBLE.md")])
assert ok and "evidence" in note
# A system-repo evidence path that does NOT exist (a typo) must not drag the constitutional
# pack in, and the host says exactly that instead of "no locator resolved".
assert ok is False and "EVIDENCE reads" in note
ok, note = _resolve(ctx, ["src/app.py"], evidence=[str(tmp_path / "sys" / "NOPE.md")])
assert ok is False and "do not exist" in note
assert ok is False and "EVIDENCE reads" in note
# --- skill-payload exemption (data plane, never self-modification by itself) ---------
@ -100,7 +100,7 @@ def test_native_bucket_is_not_exempt(tmp_path):
# Native skills are repo-seeded territory — the payload predicate does not admit them.
ctx = _ctx(tmp_path)
ok, note = _resolve(ctx, ["data/skills/native/x/plugin.py"])
assert ok and "affected_resources" in note
assert ok and "affected_paths" in note
def test_drive_resolution_failure_skips_the_exemption(tmp_path):