Merge pull request #556 from razzant/claude/docs-consolidation-20260902
Some checks are pending
CI / quick-test (push) Waiting to run
CI / full-test (macos-latest) (push) Waiting to run
CI / full-test (ubuntu-latest) (push) Waiting to run
CI / full-test (windows-latest) (push) Waiting to run
CI / betterleaks-platform-smoke (macos-latest) (push) Waiting to run
CI / betterleaks-platform-smoke (ubuntu-latest) (push) Waiting to run
CI / betterleaks-platform-smoke (windows-latest) (push) Waiting to run
CI / integration-test (push) Waiting to run
CI / skill-smoke (macos-latest) (push) Waiting to run
CI / skill-smoke (ubuntu-latest) (push) Waiting to run
CI / skill-smoke (windows-latest) (push) Waiting to run
CI / marker-guards (push) Waiting to run
CI / docker-ui-smoke (push) Waiting to run
CI / build (dmg, macos-latest, macos-arm64, syft_1.50.0_darwin_arm64.tar.gz, syft, e32fdb9d47823fa633748a1efca2528fd77c37469ea93c9e40ab835da44e4cce) (push) Blocked by required conditions
CI / build (tar.gz, ubuntu-latest, linux-x86_64, syft_1.50.0_linux_amd64.tar.gz, syft, bf7b29ff57f06da30918266a0e1c2885a8f99784798d1bdb1628886aa015d788) (push) Blocked by required conditions
CI / build (zip, windows-latest, windows-x64, syft_1.50.0_windows_amd64.zip, syft.exe, 815ee6973ec5dff6a671d7f41b0e78835a8c45b91d5a39f4743ea1cee833d3be) (push) Blocked by required conditions
Claudexor platform gate (API keys — subscription auth NOT covered) / live · windows-latest · claude · API key only, subscription NOT covered (push) Waiting to run
Claudexor platform gate (API keys — subscription auth NOT covered) / live · macos-latest · codex · API key only, subscription NOT covered (push) Waiting to run
CI / ui-smoke (push) Waiting to run
CI / docker-portable-test (push) Waiting to run
CI / release-preflight (push) Blocked by required conditions
CI / vendor-package-smoke (push) Blocked by required conditions
CI / release (push) Blocked by required conditions
Claudexor platform gate (API keys — subscription auth NOT covered) / fixture · macos-latest · exact managed runtime, fake harness, no model (push) Waiting to run
Claudexor platform gate (API keys — subscription auth NOT covered) / fixture · ubuntu-latest · exact managed runtime, fake harness, no model (push) Waiting to run
Claudexor platform gate (API keys — subscription auth NOT covered) / fixture · windows-latest · exact managed runtime, fake harness, no model (push) Waiting to run
Claudexor platform gate (API keys — subscription auth NOT covered) / live · macos-latest · claude · API key only, subscription NOT covered (push) Waiting to run
Claudexor platform gate (API keys — subscription auth NOT covered) / live · ubuntu-latest · claude · API key only, subscription NOT covered (push) Waiting to run

Actualize and shrink the two resident governance documents.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
Anton Razzhigaev 2026-09-03 10:08:11 +00:00
commit 83a69e673a
14 changed files with 3016 additions and 5188 deletions

View file

@ -106,11 +106,17 @@ on every restart, but one personality that remembers its path.
(confirmed metadata / route-fingerprinted owner acknowledgement), never an
assumed default. The mode is permitted only when the tier-0 core (system
prompt, BIBLE.md, identity.md, scratchpad, durable knowledge index,
recent-dialogue horizon) stays always-loaded in full; the agent's self-body
documentation (ARCHITECTURE.md, DEVELOPMENT.md) stays RESIDENT in owner-`max`,
and narrowing it for a task-class is itself an owner-level horizon decision,
not a silent default; reduction is by relocation to on-demand reads with a
visible pointer, or by deeper consolidation, never silent truncation; the
recent-dialogue horizon) stays always-loaded in full; `docs/ARCHITECTURE.md`
stays RESIDENT in owner-`max` for every task class, and `docs/DEVELOPMENT.md`
stays resident when the work addresses Ouroboros's own body — a binding to an
external working tree, a subagent, or an external API/CLI/scheduled surface
receives a visible on-demand pointer instead unless the task itself
explicitly requires the handbook (that per-task requirement wins in either
direction), while project membership alone is not the signal (an owner-level
horizon decision, not a silent default; any further narrowing for a task
class is likewise the owner's decision);
reduction is by relocation to on-demand reads with a visible pointer, or by
deeper consolidation, never silent truncation; the
memory horizon is preserved (only granularity varies); whether the blocking
scope reviewer applies at all is governed by this same owner-selected mode as an
explicit policy coupling under P3 — in `max` the ≥1M blocking scope gate runs, in

File diff suppressed because one or more lines are too long

View file

@ -129,7 +129,7 @@ or Intent/Scope checklists are.
| 15 | Changing `supervisor/git_ops.py`, `launcher.py`, `server.py`, `ouroboros/tools/review_helpers.py`, `ouroboros/tools/git.py`, tests, or evolution scheduling/checkpoint code? | Prove two invariants before review spend: (1) pytest/preflight cannot mutate the live repo or live `data/` (`OUROBOROS_DATA_DIR` / `OUROBOROS_SETTINGS_PATH` must be isolated, and `OUROBOROS_MANAGED_BY_LAUNCHER` must not leak into test subprocesses); (2) autonomous restart/reset cannot erase active evolution work — it must either land a reviewed local commit or preserve a rescue/transaction recovery pointer and pause/stop the campaign. |
| 16 | Changing `devtools/`? | Keep operator tools isolated from runtime and package discovery: no runtime, web, or release imports from `devtools/`; no generated outputs under `repo/` or live `data/`; no secrets printed or committed. A touched devtool is reviewable executable code even when unrelated devtools stay compact in broad review packs. Domain-specific launch, routing, and methodology guidance stays with the relevant devtool. |
| 17 | Diff spawns OS processes (`subprocess.Popen` / `mp.Process` without a bounded wait)? | Route it through `ouroboros.process_custody.spawn_supervised` (or `record_process` write-through) with an explicit `scope` (`task`/`session`/`daemon`) so the orphan reaper can find it; `tests/test_process_custody.py` enforces the allowlist. |
| 18 | New/changed **test** spawns a real OS process, binds a real port, or mutates a module-level global/registry? | CI **and the hermetic commit gate** (`preflight_runner.py`, v6.88.0) both run the suite as a parallel `-m "not serial" -n auto` pass plus a serial pass. Mark such a test `@pytest.mark.serial` (or add its file to `_SERIAL_TEST_FILES` in `tests/conftest.py`) — otherwise it flakes on kill/port-reclaim timing, or crashes a worker, which the gate reports as a `PARALLEL_WORKER_CRASH` **hard block** (never retried, never a flake). Static screen before you commit: grep the new test for `subprocess.Popen`/`subprocess.run`, socket or fixed-port binding, `start_new_session=True`, and un-monkeypatched module globals — but syntax cannot prove mocking, so still classify it semantically. The `serial` marker is the fix for *crashes and interference only*: the parallel pass also carries a 300s per-test timeout, and the serial pass carries none, so marking a merely-slow test `serial` moves the hang into the one lane that cannot bound it — make it faster or split it instead. Every other test must stay parallel-safe: `tmp_path` (not fixed `/tmp/...` paths), `monkeypatch.setenv`/`setattr` (not bare `os.environ[...] =`), no execution-order assumptions, reset any mutated module global. See `docs/DEVELOPMENT.md` "The commit gate mirrors the CI split". |
| 18 | New/changed **test** spawns a real OS process, binds a real port, or mutates a module-level global/registry? | CI **and the hermetic commit gate** (`preflight_runner.py`) both run the suite as a parallel `-m "not serial" -n auto` pass plus a serial pass. Mark such a test `@pytest.mark.serial` (or add its file to `_SERIAL_TEST_FILES` in `tests/conftest.py`) — otherwise it flakes on kill/port-reclaim timing, or crashes a worker, which the gate reports as a `PARALLEL_WORKER_CRASH` **hard block** (never retried, never a flake). Static screen before you commit: grep the new test for `subprocess.Popen`/`subprocess.run`, socket or fixed-port binding, `start_new_session=True`, and un-monkeypatched module globals — but syntax cannot prove mocking, so still classify it semantically. The `serial` marker is the fix for *crashes and interference only*: the parallel pass also carries a 300s per-test timeout, and the serial pass carries none, so marking a merely-slow test `serial` moves the hang into the one lane that cannot bound it — make it faster or split it instead. Every other test must stay parallel-safe: `tmp_path` (not fixed `/tmp/...` paths), `monkeypatch.setenv`/`setattr` (not bare `os.environ[...] =`), no execution-order assumptions, reset any mutated module global. See `docs/DEVELOPMENT.md` "The commit gate mirrors the CI split". |
Rule: read before write. Never reconstruct `VERSION`, `pyproject.toml`
`version`, or the README badge from memory — one stale reconstruction creates
@ -162,13 +162,13 @@ Used by `commit_reviewed` for all changes to the Ouroboros repository.
| 4 | code_quality | Careful code review: bugs, logic errors, crashes, regressions, race conditions, resource leaks? | critical |
| 5 | security_issues | Security vulnerabilities: injection, path traversal, secret leakage, unsafe operations? | critical |
| 6 | tests_affected | Did code logic change without corresponding test changes? (PASS if only docs/config/memory changed, or if tests already cover the new behavior.) **Critical FAIL requires all three:** (a) name a specific behavior, code path, symbol, or failure scenario that THIS diff introduces or changes; (b) explain why existing or newly staged tests do NOT catch that specific scenario; (c) the gap is concrete, not speculative. Adjacent tests in the same module or for the same feature count as coverage. Requiring an additional overlapping selector/unit/e2e test is only justified when a second distinct failure mode is named explicitly. If the only concern is "I'd feel better with one more test," that is advisory, not critical. | critical |
| 7 | architecture_doc | New module, endpoint, or data flow added but ARCHITECTURE.md not updated? (Write "Not applicable" with PASS if no architectural change.) | critical |
| 7 | architecture_doc | Did the change REPLACE the current description of every node it touched in `docs/ARCHITECTURE.md` / `docs/DEVELOPMENT.md` (map rows, flows, invariants)? PASS when a new module, endpoint or data flow gains its one-line row and touched descriptions read as current truth; PASS with "Not applicable" when nothing architectural changed. FAIL when the edit only appended a version stamp, a decision codename, a "used to / previously" narrative, or a second statement of a fact already owned elsewhere in the docs (P6 present-tense map, P7 one home per fact — see DEVELOPMENT.md "Documentation contract"). | critical |
| 8 | version_bump | Does this commit leave VERSION unchanged, or leave release artifacts out of sync? Exception: external contribution commits are version-neutral per P9 (proposed) / CONTRIBUTING.md — the maintainer's integration commit performs the bump; unchanged carriers on a contributor PR are correct, not a FAIL. | critical |
| 9 | changelog_and_badge | VERSION bumped but README.md badge or changelog not updated? (PASS if VERSION not bumped.) | critical |
| 10 | tool_registration | New tool function added but not exported in `get_tools()` OR missing explicit entry in `ouroboros/safety.py::TOOL_POLICY`? (PASS if no new tool.) Both surfaces are required: `get_tools()` makes the tool visible; `TOOL_POLICY` makes the per-call safety routing explicit and is guarded by the `test_tool_policy_covers_all_builtin_tools` invariant. | critical |
| 11 | context_building | New data/memory files that should appear in LLM context (context.py) but don't? | advisory |
| 12 | knowledge_index | Knowledge base topics changed but memory/knowledge/index-full.md not updated? | advisory |
| 13 | self_consistency | Does this change affect behavior described in `BIBLE.md`, `prompts/`, `docs/`, or this checklist itself? Check explicitly: (a) version in `ARCHITECTURE.md` header matches `VERSION` file; (b) every tool name `prompts/SYSTEM.md` or `prompts/CONSCIOUSNESS.md` mentions exists in `get_tools()` (or the background whitelist) and means the same thing there — completeness is NOT required, the schemas are the catalog; a prompt edit must not restate a tool schema or a structurally enforced gate, and a prompt that gains a sentence loses an equivalent one; (c) JSONL log/memory file formats described in `ARCHITECTURE.md` match all readers/writers; (d) any behavioral change reflected in `prompts/CONSCIOUSNESS.md` if it affects background loop behavior; (e) DEVELOPMENT.md rules still accurate after the change. Severity must follow the shared `Critical surface whitelist` below — release metadata, tool schema, module map, behavioural documentation, or safety contracts are critical; commentary/prose/stylistic mismatches are advisory. | critical |
| 13 | self_consistency | Does this change affect behavior described in `BIBLE.md`, `prompts/`, `docs/`, or this checklist itself? Check explicitly: (a) version in `ARCHITECTURE.md` header matches `VERSION` file; (b) every tool name `prompts/SYSTEM.md` or `prompts/CONSCIOUSNESS.md` mentions exists in `get_tools()` (or the background whitelist) and means the same thing there — completeness is NOT required, the schemas are the catalog; a prompt edit must not restate a tool schema or a structurally enforced gate, and a prompt that gains a sentence loses an equivalent one; a prompt edit is never an incident patch — no keyword, regex or branch added so the model picks a particular tool or route (P5, DEVELOPMENT.md "LLM-first affordances"); and when the diff touches the code that assembles a prompt (`context.py`, review packet/template builders, advisory, consciousness, plan packet, loop nudges) the injected prompt text and that code still agree; (c) JSONL log/memory file formats described in `ARCHITECTURE.md` match all readers/writers; (d) any behavioral change reflected in `prompts/CONSCIOUSNESS.md` if it affects background loop behavior; (e) DEVELOPMENT.md rules still accurate after the change. Severity must follow the shared `Critical surface whitelist` below — release metadata, tool schema, module map, behavioural documentation, or safety contracts are critical; commentary/prose/stylistic mismatches are advisory. | critical |
| 14 | light_external_artifacts | If tool/runtime policy changed, does light mode still allow external user deliverables via `user_files`, task-scoped `task_drive`/`artifact_store`, and process `outputs` while blocking Ouroboros repo/control-plane mutation? (The external `claude_code_edit` cwd lane retired with the tool — D10.) Do review prompts avoid recommending `runtime_data/uploads` or skill payloads as generic artifact transport? | critical |
| 15 | cross_platform | Does the diff use platform-specific APIs (`os.kill`, `os.setsid`, `os.killpg`, `os.getpgid`, `fcntl`, `msvcrt`, `signal.SIGKILL`, `signal.SIGTERM`, `subprocess` with `start_new_session`/`creationflags`, hardcoded `/` or `\\` in filesystem paths) outside of `ouroboros/platform_layer.py`? Does it import Unix-only or Windows-only modules (`fcntl`, `msvcrt`, `winreg`, `resource`) at any level without a platform guard (`sys.platform`/`IS_WINDOWS` check)? | critical |
| 16 | changelog_accuracy | Do the exact wording, test counts, and minor description details in the README Version History row match what the diff actually does? Wording drift, off-by-one test counts, minor inaccuracies in descriptive prose — these belong here, NOT in `self_consistency` or `changelog_and_badge`. This item exists so reviewers have a dedicated advisory bucket for prose-level changelog imprecision that does not affect release metadata, runtime behavior, or safety contracts. | advisory |

File diff suppressed because it is too large Load diff

View file

@ -152,8 +152,14 @@ def reference_doc_sections(
``context_mode`` is the OWNER context mode and decides only the ARCHITECTURE
form (full in max, nav map in low — D-ARCH, owner 2026-08-08).
``include_development`` is the caller's mode-independent decision whether the
self-engineering handbook is inline (self-body/self-mod/evolution work) or an
on-demand pointer (project tasks — folder or not — and external surfaces).
self-engineering handbook is inline (work on Ouroboros's own body) or an
on-demand pointer. The structural signal is the active repo binding, never
project membership: an external worktree, a subagent, or an api/cli/scheduled
surface gets the pointer, while a direct turn inside a project room with no
workspace bound keeps the handbook. An explicit ``context_requires_development``
on the task wins in either direction. When it is absent, a truthy
``context_requires_self_body_docs`` on the task or its contract keeps the handbook
even for an external surface (see ``context.py`` D-DEV).
"""
parts: List[str] = []
on_demand: List[str] = []

View file

@ -1,6 +1,6 @@
"""Claudexor v3 control-plane transport.
Pure I/O, like every gateway (docs/DEVELOPMENT.md "Gateway Rules"): discover the
Pure I/O, like every gateway (docs/DEVELOPMENT.md "Gateway Boundary Pattern"): discover the
loopback daemon, negotiate the protocol, and translate the typed ``/v2`` surface
into Python primitives. No routing, no policy, no harness identity branches — the
caller asks for a CAPABILITY (an access profile, an opaque route id) and reads the

View file

@ -3,8 +3,9 @@
Project conversion stores a reference to the original owner row on the immutable
task binding. A Project room projects that row instead of copying it into
``chat.jsonl``. Terminal task projections append to that same canonical biography;
``chat_annotations.jsonl`` remains presentation-only and never owns routing or
Project state.
``chat_annotations.jsonl`` is presentation-first and owns no Project state; its
token-bound ``needs_manual_target`` decision card is the one routing-authority
exception.
"""
from __future__ import annotations

View file

@ -236,7 +236,7 @@ def _build_advisory_prompt(
expected_items = prompt_context.get("expected_items")
checklist_name = "Skill Review Checklist" if review_surface == "skill" else "Repo Commit Checklist"
if governance_by_retrieval:
# agent_session delivery: do NOT inline the ~830KB governance bodies —
# agent_session delivery: do NOT inline the governance bodies (hundreds of KB) —
# each becomes a resolvable absolute pointer plus a mandatory-read
# instruction, and the session reads the docs itself with its own
# tools. The authority for this form is the plan-review agent_session

View file

@ -11,7 +11,8 @@ creates and deletes baseline refs and worktree registrations in the target's
Reports per-run provision (baseline build + worktree checkout) and teardown
wall-clock, plus tree shape (file count, on-disk size). The delegated-snapshot
lifecycle this measures is documented in docs/ARCHITECTURE.md (C1 section).
lifecycle this measures is documented in docs/ARCHITECTURE.md,
"Delegated subagents".
"""
from __future__ import annotations

View file

@ -1167,7 +1167,7 @@ def test_schema_and_docs_split_git_staging_from_payload_live_apply():
assert "applied LIVE into the non-Git payload" in decision
arch = (pathlib.Path(__file__).resolve().parents[1] / "docs" /
"ARCHITECTURE.md").read_text(encoding="utf-8")
assert "differs is the staging substrate" in arch
assert "staging substrate differs" in arch
assert "A SKILL-PAYLOAD target captures through the payload adapter" in arch
assert "QUEUES the extension reconcile request" in arch

View file

@ -346,3 +346,188 @@ def test_prompt_tool_names_resolve_to_registered_tools(tmp_path):
f"prompts/CONSCIOUSNESS.md names bare identifiers that are neither registered tools "
f"nor classified non-tool identifiers: {sorted(unresolved_bare)}"
)
# --- Documentation contract enforcement (DEVELOPMENT.md "Documentation contract") ---
#
# Three deterministic checks keep the two resident docs a present-tense map:
# 1. residue: version stamps, decision codenames and "used to / previously"
# narrative may only SHRINK per `## ` section (baseline below, like the code
# size ratchet — a section that reaches zero may never grow again);
# 2. the endpoint table mirrors the executable route registries;
# 3. the settings table mirrors `config.SETTINGS_DEFAULTS` (env-only and retired
# rows are declared, not guessed).
# Language-tagged code fences (```yaml, ```python …) are examples and are not
# scanned; the plain ``` fence holding the §1 module tree IS scanned. The first
# ARCHITECTURE line carries the release version by contract and is skipped, as
# are DEVELOPMENT's "Mutable external-fact inventory" (dated provenance is the
# rule there) and the "Documentation contract" section that quotes the markers.
DOC_RESIDUE_PATTERNS = {
"version_stamp": r"\((?:v\d+\.\d+(?:\.\d+)?(?:-rc\.\d+)?)\)",
"version_narrative": r"\b(?:since|before|pre-)v\d+",
"narrative": r"\b(?:used to|previously|formerly|was deleted|replaces the earlier|gate[- ]round|round \d+)\b",
"codename_paren": r"\((?:GR|AR|BR|CR|D|Q|S|HQ|C|B)\d+[^)]{0,24}\)",
"codename_word": r"\bPoltergeist\b|\bphase [A-C]\d?\b|owner(?:-| )(?:decision|ratif)",
}
DOC_RESIDUE_SKIPPED_SUBSECTIONS = {
"docs/DEVELOPMENT.md": ("Mutable external-fact inventory", "Documentation contract"),
}
def doc_residue_counts(rel: str, text: str) -> dict:
"""Per-`## ` section counts of residue markers (see DOC_RESIDUE_PATTERNS)."""
counts: dict = {}
section = "(preamble)"
fence_lang = None
skipping = False
skipped = DOC_RESIDUE_SKIPPED_SUBSECTIONS.get(rel, ())
for lineno, line in enumerate(text.split("\n"), 1):
stripped = line.lstrip()
if stripped.startswith("```"):
fence_lang = (stripped[3:].strip() or "") if fence_lang is None else None
continue
if fence_lang: # language-tagged example block
continue
if fence_lang is None and line.startswith("## "):
section, skipping = line.strip(), False
if fence_lang is None and line.startswith("### "):
skipping = any(name in line for name in skipped)
if skipping or (rel == "docs/ARCHITECTURE.md" and lineno == 1):
continue
for kind, pattern in DOC_RESIDUE_PATTERNS.items():
hits = len(re.findall(pattern, line))
if hits:
counts.setdefault(section, {k: 0 for k in DOC_RESIDUE_PATTERNS})[kind] += hits
return counts
# Zero-residue ceilings recorded after the 07b53365 cleanup: an absent section or kind is a
# zero ceiling, so both documents are held at zero everywhere. A section row exists only
# while a re-baseline records a temporary non-zero ceiling, and such a row may only shrink;
# an absent section stays at zero.
DOC_RESIDUE_BASELINE = {
"docs/ARCHITECTURE.md": {},
"docs/DEVELOPMENT.md": {},
}
def test_resident_docs_residue_only_shrinks():
for rel, baseline in DOC_RESIDUE_BASELINE.items():
current = doc_residue_counts(rel, _read(rel))
for section, counts in current.items():
allowed = baseline.get(section, {})
for kind, hits in counts.items():
assert hits <= allowed.get(kind, 0), (
f"{rel} {section!r}: {kind} residue grew to {hits} (baseline "
f"{allowed.get(kind, 0)}); replace the node's description instead of "
"appending history (DEVELOPMENT.md 'Documentation contract')"
)
def _architecture_section(text: str, heading_prefix: str) -> str:
lines = text.split("\n")
start = next(i for i, l in enumerate(lines) if l.startswith(heading_prefix))
end = next((i for i in range(start + 1, len(lines)) if lines[i].startswith("## ")), len(lines))
return "\n".join(lines[start:end])
def test_architecture_endpoint_table_mirrors_route_registries(tmp_path):
"""Every mounted browser/CLI route and Host Service route has exactly one table
row in ARCHITECTURE §4, and no row names a route that is not mounted."""
from ouroboros.gateway.endpoint_index import HTTP_ENDPOINTS
from ouroboros.gateway import files as gateway_files
section = _architecture_section(_read("docs/ARCHITECTURE.md"), "## 4.")
rows = re.findall(r"^\| (GET|POST|PUT|PATCH|DELETE|ANY|WS|STATIC) \| `([^`]+)` \|", section, re.M)
host_prefix = "127.0.0.1:${OUROBOROS_HOST_SERVICE_PORT:-8767}"
documented_public = {f"{m} {p}" for m, p in rows if not p.startswith(host_prefix)}
documented_host = {f"{m} {p[len(host_prefix):]}" for m, p in rows if p.startswith(host_prefix)}
expected_public = set(HTTP_ENDPOINTS)
for route in gateway_files.file_browser_routes():
for method in sorted(getattr(route, "methods", None) or []):
if method in ("HEAD", "OPTIONS"):
continue
expected_public.add(f"{method} {route.path}")
# server-level surfaces that are not gateway routes but belong in the map
expected_public |= {"GET /", "WS /ws", "STATIC /static/*"}
# /api/extensions/{skill}/{rest:path} is mounted for every verb and documented once as ANY
expected_public = {e for e in expected_public if not e.endswith("/api/extensions/{skill}/{rest:path}")}
expected_public.add("ANY /api/extensions/{skill}/{rest:path}")
from starlette.routing import Route, WebSocketRoute
from ouroboros.gateway.host_service import create_host_service_app
expected_host = set()
for route in create_host_service_app(tmp_path).routes:
if isinstance(route, WebSocketRoute):
expected_host.add(f"WS {route.path}")
elif isinstance(route, Route):
if route.methods is None:
expected_host.add(f"ANY {route.path}")
else:
expected_host.update(
f"{method} {route.path}"
for method in route.methods
if method not in {"HEAD", "OPTIONS"}
)
else:
raise AssertionError(f"Unhandled Host Service route type: {type(route).__name__}")
assert documented_public == expected_public, (
f"missing rows: {sorted(expected_public - documented_public)}; "
f"stale rows: {sorted(documented_public - expected_public)}"
)
assert documented_host == expected_host, (
f"missing host rows: {sorted(expected_host - documented_host)}; "
f"stale host rows: {sorted(documented_host - expected_host)}"
)
assert len(rows) == len(documented_public) + len(documented_host), "duplicate endpoint rows"
# Rows the settings table documents on purpose although `config.SETTINGS_DEFAULTS`
# has no such key: operator env-only levers (never a settings.json carrier) and the
# retired alias whose migration the table still explains (pinned by test_review_cycles).
SETTINGS_TABLE_ENV_ONLY_ROWS = frozenset({
"OUROBOROS_TRUST_NONLOCAL_BIND_WITHOUT_PASSWORD", "OUROBOROS_DISABLE_MANAGED_UPDATES",
"OUROBOROS_PRESENTATION", "OUROBOROS_USER_FILES_ROOT", "OUROBOROS_OBSERVABILITY_KEEP_RAW",
"OUROBOROS_OBSERVABILITY_RETENTION_DAYS", "OUROBOROS_REVIEW_MODEL_TIMEOUT_SEC",
"OUROBOROS_REVIEW_MAX_TOKENS", "OUROBOROS_PREFLIGHT_TIMEOUT_SEC", "OUROBOROS_PREFLIGHT_SERIAL",
"OUROBOROS_BUNDLE_DIR",
})
SETTINGS_TABLE_RETIRED_ROWS = frozenset({"OUROBOROS_ACCEPTANCE_MAX_IMPROVEMENT_PASSES"})
def _normalize_default_cell(cell: str) -> str:
value = cell.strip()
value = re.sub(r"^`(.*)`$", r"\1", value)
if value in ('""', "(empty)", "(unset)", "unset", ""):
return ""
value = re.sub(r'^"(.*)"$', r"\1", value)
return value.split()[0].lower() if value else ""
def test_architecture_settings_table_mirrors_config_defaults():
"""Every `config.SETTINGS_DEFAULTS` key has one row in ARCHITECTURE's Default
settings table with the shipped default; every other row is a declared env-only
lever or retired alias."""
from ouroboros import config
section = _architecture_section(_read("docs/ARCHITECTURE.md"), "## 7.")
table = section[section.index("### Default settings"):]
rows = re.findall(r"^\| ([A-Z][A-Z0-9_]+) \| ([^|]*?) \|", table, re.M)
keys = [k for k, _ in rows]
assert len(keys) == len(set(keys)), f"duplicate settings rows: {sorted(k for k in set(keys) if keys.count(k) > 1)}"
documented = set(keys)
expected = set(config.SETTINGS_DEFAULTS)
assert expected <= documented, f"settings missing from the table: {sorted(expected - documented)}"
undeclared = documented - expected - SETTINGS_TABLE_ENV_ONLY_ROWS - SETTINGS_TABLE_RETIRED_ROWS
assert not undeclared, f"table rows that are neither shipped defaults nor declared env-only/retired: {sorted(undeclared)}"
mismatched = [
(key, cell, str(config.SETTINGS_DEFAULTS[key]))
for key, cell in rows
if key in expected and _normalize_default_cell(cell) != str(config.SETTINGS_DEFAULTS[key]).lower()
]
assert not mismatched, f"documented default differs from config.SETTINGS_DEFAULTS: {mismatched}"

View file

@ -188,7 +188,7 @@ def test_child_task_handoff_results_never_truncated():
# ---------------------------------------------------------------------------
def test_repo_read_default_max_lines_is_2000(tmp_path):
"""Default max_lines must be 2000 so ARCHITECTURE.md (~1285 lines) fits in one call."""
"""Default max_lines must be 2000 so ARCHITECTURE.md fits in one call."""
import inspect
from ouroboros.tools.core import _repo_read
sig = inspect.signature(_repo_read)
@ -212,7 +212,7 @@ def test_repo_read_schema_default_is_2000():
def test_repo_read_can_read_architecture_md_in_one_call(tmp_path):
"""A file of ARCHITECTURE.md length (~1285 lines) is returned fully with default max_lines."""
"""A file longer than the historical 1050-line default is returned fully with default max_lines."""
from ouroboros.tools.core import _repo_read
# Simulate a file slightly longer than the old 1050-line default
n_lines = 1300

View file

@ -269,7 +269,7 @@ _WIZARD_ON_SUMMARY_JS = "() => (document.querySelector('.step-title')?.textConte
@pytest.mark.ui_browser
def test_ui_smoke_agents_panel_wizard_finish_judges_the_roster(direct_server_with_data):
"""First-run wizard (docs/ARCHITECTURE.md §3): an unrouted entry added on the Agents step
"""First-run wizard (docs/ARCHITECTURE.md §2): an unrouted entry added on the Agents step
does not block Continue; Finish on the summary reports it and, back on Agents, the card
is already tinted and self-naming; the fix reconciles line and tint together and the
second Finish passes the wizard's own checks and enters saving (the save's provider

View file

@ -2882,7 +2882,8 @@ def test_ui_smoke_v679_subagent_depth_zero_round_trips_through_settings(direct_s
full page reload — and pins the three neighbouring states so the fix cannot silently break
them: 0 (no delegation), a normal positive value, and empty (falls back, does not persist
an invalid value). Screenshots are written for vision inspection; a saved screenshot is
not verification on its own (docs/DEVELOPMENT.md "Browser/mobile verification").
not verification on its own (docs/DEVELOPMENT.md "Responsive and accessible
behavior").
"""
pytest.importorskip("playwright.sync_api", reason="Playwright is not installed")
from playwright.sync_api import Error as PlaywrightError