Merge pull request #1931 from ouroboros-agent/claude/test-health-20261016

Tests: one process owner per run, a tiered commit gate and a critical cleanup
This commit is contained in:
Ouroboros 2026-10-16 21:29:08 +03:00 • committed by GitHub
commit e4ab6cabbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
152 changed files with 4066 additions and 2880 deletions

View file

@ -1,7 +1,7 @@
# Ouroboros CI — Five-tier cross-platform testing and release pipeline
#
# Tier 1 (Quick): Push to ouroboros or PR targeting ouroboros → Ubuntu-only tests
# Tier 2 (Full): Every PR and every ouroboros push → Windows/macOS; stable / manual / tag → full 3-OS matrix
# Tier 2 (Full): Every ouroboros push → Windows/macOS (PRs: selected scope, pr-selected-os); stable / manual / tag → full 3-OS matrix
# Tier 1.5 (SQLite floor): Push to ouroboros or PR targeting ouroboros → store modules on ubuntu-22.04's system SQLite 3.37.2
# Tier 2.5 (Integration): Manual / tag (branch pushes: provider-canary-push.yml) → Real-provider tests, reusable provider-canary.yml (~2 min)
# Tier 2.6 (Skill smoke): Push to ouroboros-stable / manual / tag → LIVE OuroborosHub official-skill install smoke (3-OS, ~5 min) + review→grant→enable-persistence flow on one cheap reviewer slot (ubuntu-only step, OPENROUTER_API_KEY, ~$1.2/run)
@ -116,9 +116,8 @@ jobs:
# The hermetic commit gate runs this exact suite (ouroboros/preflight_node.py);
# keeping the step here keeps gate and CI mirrored in both directions.
- name: Run browser-module tests (node --test — mirrored by the hermetic commit gate)
if: ${{ !cancelled() && steps.setup_node.outcome == 'success' }}
shell: bash
run: cd web && node --test tests/*.test.js
if: ${{ !cancelled() && steps.setup_python.outcome == 'success' && steps.setup_node.outcome == 'success' && github.event_name != 'pull_request' }}
run: python scripts/run_tests.py --node
# ESLint `no-undef` is the second, independent layer of the browser no-undef
# gate (owner decision D-13): the hermetic commit gate keeps the dependency-free
# acorn walker in the suite above; this layer is CI-only, lockfile-frozen
@ -134,18 +133,25 @@ jobs:
# observed platform: the serial pass has no per-test timeout (the commit gate's
# serial pass has none either), so the ceiling is what bounds a hang there. Its
# expiry fails that step, and with it the job, without cancelling the job: the
# evidence step below still names the test. On Linux and macOS the killed step's
# pytest can linger beside the later steps until the runner cleans up the job.
# evidence step below still names the test. `scripts/run_tests.py` owns each pass
# (lifeline guardian, overload stop), so a killed step leaves no tree behind.
- name: Run tests (parallel — excludes the costly marker lanes AND the serial real-process suites)
id: tests_parallel
timeout-minutes: 90
if: ${{ !cancelled() && steps.setup_python.outcome == 'success' }}
run: python -m pytest tests/ -m "not serial and not integration and not browser and not ui_browser and not ui_browser_docker and not portable_detail and not skill_smoke and not size_ratchet" -n auto --dist loadscope --max-worker-restart=0 --timeout=300 --timeout-method=thread -q --tb=short --ci-evidence-dir="${{ runner.temp }}/ci-evidence/parallel"
if: ${{ !cancelled() && steps.setup_python.outcome == 'success' && github.event_name != 'pull_request' }}
run: python scripts/run_tests.py tests/ -m "not serial and not integration and not browser and not ui_browser and not ui_browser_docker and not portable_detail and not skill_smoke and not size_ratchet" -n auto --dist loadscope --max-worker-restart=0 --timeout=300 --timeout-method=thread -q --tb=short --ci-evidence-dir="${{ runner.temp }}/ci-evidence/parallel"
- name: Run tests (serial — real subprocess/port/global-state suites that flake under -n)
id: tests_serial
timeout-minutes: 60
if: ${{ !cancelled() && steps.setup_python.outcome == 'success' }}
run: python -m pytest tests/ -m "serial and not integration and not browser and not ui_browser and not ui_browser_docker and not portable_detail and not skill_smoke and not size_ratchet" -q --tb=short --ci-evidence-dir="${{ runner.temp }}/ci-evidence/serial"
if: ${{ !cancelled() && steps.setup_python.outcome == 'success' && github.event_name != 'pull_request' }}
run: python scripts/run_tests.py tests/ -m "serial and not integration and not browser and not ui_browser and not ui_browser_docker and not portable_detail and not skill_smoke and not size_ratchet" -q --tb=short --ci-evidence-dir="${{ runner.temp }}/ci-evidence/serial"
- name: Run PR tests (contract floor + affected modules and complete Node lane)
id: tests_selected
timeout-minutes: 150
if: ${{ !cancelled() && steps.setup_python.outcome == 'success' && steps.setup_node.outcome == 'success' && github.event_name == 'pull_request' }}
env:
PR_BASE: ${{ github.event.pull_request.base.sha }}
run: python scripts/run_tests.py --selected --base "$PR_BASE" --sequential --evidence-dir="${{ runner.temp }}/ci-evidence"
# Whom the reference-book rule binds (scripts/book_growth_mode.py): the size
# lane below blocks, warns or passes a grown book by this mode.
- name: Decide the reference-book growth mode
@ -190,7 +196,63 @@ jobs:
uses: ./.github/actions/test-evidence
with:
name: quick-test
passes: parallel=${{ steps.tests_parallel.outcome }} serial=${{ steps.tests_serial.outcome }} size=${{ steps.tests_size.outcome }}
passes: parallel=${{ github.event_name == 'pull_request' && (steps.tests_selected.outputs.parallel_outcome || steps.tests_selected.outcome) || steps.tests_parallel.outcome }} serial=${{ github.event_name == 'pull_request' && (steps.tests_selected.outputs.serial_outcome || steps.tests_selected.outcome) || steps.tests_serial.outcome }} size=${{ steps.tests_size.outcome }}
# ──────────────────────────────────────────────────────────────────
# Pull requests on Windows and macOS: the same selected scope as quick-test
# (contract floor, affected modules, complete Node lane), so a platform break
# in the changed area shows before the merge. The full matrix runs after it;
# on Windows a full escalation runs the floor and leaves the rest to its four landed shards (--defer-full).
# ──────────────────────────────────────────────────────────────────
pr-selected-os:
if: ${{ github.event_name == 'pull_request' && github.base_ref == 'ouroboros' }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
env:
OUROBOROS_PREFLIGHT_REQUIRE_PLUGINS: "1"
OUROBOROS_PREFLIGHT_REQUIRE_NODE: "1"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ./.github/actions/setup-python-env
id: setup_python
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
if: ${{ !cancelled() }}
id: setup_node
with:
node-version: '22'
- name: Set up Windows POSIX test utilities
if: ${{ !cancelled() && runner.os == 'Windows' }}
id: posix_tools
uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
with:
msystem: MSYS
release: false
update: true
install: rsync
- name: Select Windows POSIX test utilities
id: select_posix_tools
if: ${{ !cancelled() && runner.os == 'Windows' && steps.posix_tools.outcome == 'success' }}
shell: pwsh
run: '"${{ steps.posix_tools.outputs.msys2-location }}\usr\bin" >> $env:GITHUB_PATH'
- name: Run PR tests (contract floor + affected modules and complete Node lane)
id: tests_selected
timeout-minutes: 150
if: ${{ !cancelled() && steps.setup_python.outcome == 'success' && steps.setup_node.outcome == 'success' && (runner.os != 'Windows' || (steps.posix_tools.outcome == 'success' && steps.select_posix_tools.outcome == 'success')) }}
run: python scripts/run_tests.py --selected --base ${{ github.event.pull_request.base.sha }} --sequential --evidence-dir="${{ runner.temp }}/ci-evidence" ${{ runner.os == 'Windows' && '--defer-full' || '' }}
- name: Publish test evidence
if: ${{ !cancelled() && steps.setup_python.outcome == 'success' }}
continue-on-error: true
uses: ./.github/actions/test-evidence
with:
name: pr-selected-${{ matrix.os }}
passes: parallel=${{ steps.tests_selected.outputs.parallel_outcome || steps.tests_selected.outcome }} serial=${{ steps.tests_selected.outputs.serial_outcome || steps.tests_selected.outcome }}
# ──────────────────────────────────────────────────────────────────
# Benchmark methodology tests (owner decision R42). The devtools/benchmarks
@ -255,7 +317,7 @@ jobs:
run: /usr/bin/python3 -I -S scripts/safe_test.py --temp-parent "${{ runner.temp }}" -- .venv/bin/python -m pytest tests/test_store.py tests/test_store_import.py tests/test_store_switch.py tests/test_custody_store_rows.py tests/test_store_cutover.py tests/test_store_activation.py -q --tb=short --timeout=300 --timeout-method=thread
# ──────────────────────────────────────────────────────────────────
# Tier 2: Windows/macOS for every PR and every landed `ouroboros` push (Ubuntu is
# Tier 2: Full Windows/macOS for every landed `ouroboros` push (Ubuntu is
# quick-test there); full 3-OS matrix for stable, manual runs and tags. The landed
# push is what tells a later PR that a Windows/macOS failure is inherited. It runs
# in this repository only: an install that pushes its own commits to a private copy
@ -266,7 +328,6 @@ jobs:
github.ref == 'refs/heads/ouroboros-stable'
|| github.event_name == 'workflow_dispatch'
|| startsWith(github.ref, 'refs/tags/v')
|| (github.event_name == 'pull_request' && github.base_ref == 'ouroboros')
|| (github.event_name == 'push' && github.ref == 'refs/heads/ouroboros' && github.repository == 'razzant/ouroboros')
# The growth-mode step reads the pull request's label events.
permissions:
@ -275,7 +336,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: ${{ fromJSON((github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/ouroboros')) && '["macos-latest"]' || '["ubuntu-latest","macos-latest"]') }}
os: ${{ fromJSON((github.event_name == 'push' && github.ref == 'refs/heads/ouroboros') && '["macos-latest"]' || '["ubuntu-latest","macos-latest"]') }}
shard: [0] # Unsharded macOS/Ubuntu; Windows alone is split by whole file.
include:
- {os: windows-latest, shard: 1}
@ -316,12 +377,10 @@ jobs:
shell: pwsh
run: '"${{ steps.posix_tools.outputs.msys2-location }}\usr\bin" >> $env:GITHUB_PATH'
# Independent checks keep running after a red sibling; setup outcomes suppress cascaded noise.
# The hermetic commit gate runs this exact suite (ouroboros/preflight_node.py);
# `shell: bash` keeps the glob working on the Windows runner too.
# The hermetic commit gate runs this exact suite (ouroboros/preflight_node.py).
- name: Run browser-module tests (node --test — mirrored by the hermetic commit gate)
if: ${{ !cancelled() && steps.setup_node.outcome == 'success' && (runner.os != 'Windows' || (steps.posix_tools.outcome == 'success' && steps.select_posix_tools.outcome == 'success')) && matrix.shard <= 1 }}
shell: bash
run: cd web && node --test tests/*.test.js
if: ${{ !cancelled() && steps.setup_python.outcome == 'success' && steps.setup_node.outcome == 'success' && (runner.os != 'Windows' || (steps.posix_tools.outcome == 'success' && steps.select_posix_tools.outcome == 'success')) && matrix.shard <= 1 }}
run: python scripts/run_tests.py --node
# ESLint `no-undef` is the second, independent layer of the browser no-undef
# gate (owner decision D-13): the hermetic commit gate keeps the dependency-free
# acorn walker in the suite above; this layer is CI-only, lockfile-frozen
@ -349,17 +408,18 @@ jobs:
--collect-only -q --default-suite-manifest
--ci-evidence-dir="${{ runner.temp }}/ci-evidence/default-manifest/serial"
# A command-line `-m` REPLACES the pyproject addopts markexpr, so repeat the default
# marker-lane exclusions and AND them with the serial split. --timeout guards hangs.
# marker-lane exclusions and AND them with the serial split. --timeout guards hangs;
# `scripts/run_tests.py` passes the arguments through and owns the process tree.
- name: Run tests (parallel — excludes the costly marker lanes AND the serial real-process suites)
id: tests_parallel
timeout-minutes: 90
if: ${{ !cancelled() && steps.setup_python.outcome == 'success' && (runner.os != 'Windows' || (steps.posix_tools.outcome == 'success' && steps.select_posix_tools.outcome == 'success')) }}
run: python -m pytest tests/ -m "not serial and not integration and not browser and not ui_browser and not ui_browser_docker and not portable_detail and not skill_smoke and not size_ratchet" -n auto --dist loadscope --max-worker-restart=0 --timeout=300 --timeout-method=thread -q --tb=short --ci-evidence-dir="${{ runner.temp }}/ci-evidence/parallel" ${{ runner.os == 'Windows' && format('--default-suite-shard={0}/4', matrix.shard) || '' }}
run: python scripts/run_tests.py tests/ -m "not serial and not integration and not browser and not ui_browser and not ui_browser_docker and not portable_detail and not skill_smoke and not size_ratchet" -n auto --dist loadscope --max-worker-restart=0 --timeout=300 --timeout-method=thread -q --tb=short --ci-evidence-dir="${{ runner.temp }}/ci-evidence/parallel" ${{ runner.os == 'Windows' && format('--default-suite-shard={0}/4', matrix.shard) || '' }}
- name: Run tests (serial — real subprocess/port/global-state suites that flake under -n)
id: tests_serial
timeout-minutes: 60
if: ${{ !cancelled() && steps.setup_python.outcome == 'success' && (runner.os != 'Windows' || (steps.posix_tools.outcome == 'success' && steps.select_posix_tools.outcome == 'success')) }}
run: python -m pytest tests/ -m "serial and not integration and not browser and not ui_browser and not ui_browser_docker and not portable_detail and not skill_smoke and not size_ratchet" -q --tb=short --ci-evidence-dir="${{ runner.temp }}/ci-evidence/serial" ${{ runner.os == 'Windows' && format('--default-suite-shard={0}/4', matrix.shard) || '' }}
run: python scripts/run_tests.py tests/ -m "serial and not integration and not browser and not ui_browser and not ui_browser_docker and not portable_detail and not skill_smoke and not size_ratchet" -q --tb=short --ci-evidence-dir="${{ runner.temp }}/ci-evidence/serial" ${{ runner.os == 'Windows' && format('--default-suite-shard={0}/4', matrix.shard) || '' }}
# Whom the reference-book rule binds (scripts/book_growth_mode.py): the size
# lane below blocks, warns or passes a grown book by this mode.
- name: Decide the reference-book growth mode

View file

@ -73,7 +73,7 @@ MAX_LANES = 6
STAGGER_BOUNDS = (2.0, 3.0)
# The commit gate's hermetic pytest pass runs INSIDE each lane server and resolves ``-n auto`` to the host's
# CPU count (128 here) with no ceiling: the paid run of 2026-09-04 started >= 104 xdist workers per self-mod
# lane, three lanes at once, on a shared host. The runtime's own lever (``preflight_runner._PREFLIGHT_WORKERS_ENV``,
# lane, three lanes at once, on a shared host. The runtime's own lever (``test_parallelism.WORKERS_ENV``,
# floor 2 so PREFLIGHT_PARALLELISM_LOST can never trip) is set in this process and forwarded by ``IsolatedServer``
# in settings-authoritative mode (its keep-list); the budget is split evenly across the concurrent lanes so the
# whole stand stays within the shared-host rule of at most 16 pytest workers.

View file

@ -155,7 +155,7 @@ diff, but it is not loaded as a standalone checklist the way the Change Review
|---|-------|-----|
| 1 | `VERSION`, `web/package.json`, `web/modules/api_types.js` (`GATEWAY_CONTRACT_VERSION`), the `README.md` badge, the named direct-download links (`README.md`, `site/install/index.html`, `docs/install/index.html`), the latest `README.md` history row, and the `docs/ARCHITECTURE.md` header — are they all carrying the *author-facing* spelling (for example `4.50.0-rc.3`)? And do `pyproject.toml` and the `uv.lock` root version carry the **PEP 440 canonical form** of that same version (for example `4.50.0rc3`)? | `read_file` each file before editing. Never reconstruct version strings from memory — the in-context copy may be stale. The `VERSION` vs `pyproject.toml` divergence is intentional: `pyproject.toml` must satisfy PEP 440 so pip / build / twine accept it, while `VERSION` / tags / README / ARCHITECTURE use the author-facing spelling. `tests/test_packaging_sync.py::test_version_file_and_pyproject_are_synced` enforces the relationship via `ouroboros.tools.release_sync._normalize_pep440`. |
| 2 | Preparing any commit → is `VERSION` bumped? | Under BIBLE.md P9, every commit is a release. A `VERSION` bump is mandatory for every commit, including docs/config/memory changes. Update every carrier together — `VERSION`, `pyproject.toml`, `uv.lock`, `web/package.json`, `web/modules/api_types.js`, the `README.md` badge / history row / download links, both install pages' download links, `docs/ARCHITECTURE.md` header; `ouroboros/tools/release_sync.py::version_carrier_desyncs()` checks the file carriers (the git tag follows on commit). Exception: a version-neutral contribution keeps every carrier byte-identical — an external contribution commit (P9 / CONTRIBUTING.md; the maintainer's integration commit performs the bump), or a commit prepared in a body candidate, which P9 lets the installation adopt locally as it is. Choose that form or a numbered release deliberately and state it; never a partial bump. |
| 3 | New or changed logic → does an existing or newly staged test assert on the specific scenario it introduces? | Name the scenario your code handles in plain words. If no test asserts on THAT named scenario, write or update one now. "Tests exist for the module" is not the same as "tests cover this new behavior". |
| 3 | New or changed logic → does an existing or newly staged test assert on the specific scenario it introduces? | Name the scenario in plain words and the test that distinguishes it; extend that test, write a new one only when none does, and retire assertions the change supersedes in the same diff. A pin of wording, source text, call presence or AST shape does not cover behaviour: review owns meaning. "Tests exist for the module" is not "tests cover this behaviour". |
| 4 | Shared log / memory / replay format changed? | Grep every reader and writer first. JSONL logs (`events.jsonl`, `task_reflections.jsonl`, replay indexes), durable state files (`advisory_review.json`, `review_continuations/*.json`), and memory note/history pairs (a knowledge note / its `knowledge_history.jsonl` records) must stay coherent across every consumer. |
| 5 | New validation guard, input filter, or edge-case check? | Before the first commit attempt, name three concrete ways it could break: wrong bounds, legitimate inputs it silently blocks, platform-specific edge cases. If you cannot name three, think longer. One honest minute here is cheaper than one reviewer round. |
| 6 | New tool added? | `get_tools()` exports it, its schema description says WHEN to choose it (each profile receives its visible schema set every round, so the schema is the SSOT of the per-tool contract; `prompts/SYSTEM.md` is the cross-tool selection policy and mentions a tool only when the change alters that policy, never as a catalog entry; mechanism documentation lives in ARCHITECTURE/DEVELOPMENT), the handler signature matches the declared schema, and (if it mutates repo state) it is routed through the reviewed commit path rather than ad-hoc `run_command`. Also give it one explicit safety policy, its `ToolEntry.safety_policy` or an `ouroboros/safety.py::TOOL_POLICY` entry (`skip` for trusted built-ins, `check` for opaque or outward-facing ones) — `test_tool_policy_covers_all_builtin_tools` fails otherwise, and a tool without one pays a light-model LLM call per invocation. |
@ -170,7 +170,7 @@ diff, but it is not loaded as a standalone checklist the way the Change Review
| 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`) 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 use a parallel `-m "not serial" -n auto` pass plus a serial pass. Mark it `@pytest.mark.serial` (or add its file to `_SERIAL_TEST_FILES` in `tests/conftest.py`) only for a shared resource it cannot isolate — a fixed port, a real daemon, a process-global its fixture cannot restore; an owned child, a private root or port 0 is parallel-safe. 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). Syntax (`subprocess`, sockets, `start_new_session=True`, module globals) is only a screen; classify semantically. `serial` fixes crashes and interference only: the serial pass has no per-test timeout, so a merely slow test is made faster or split instead. Every other test stays parallel-safe (`docs/DEVELOPMENT.md` "Parallel CI and the `serial` marker"). |
Rule: read before write. Never reconstruct `VERSION`, `pyproject.toml`
`version`, or the README badge from memory — one stale reconstruction creates
@ -225,10 +225,10 @@ Ouroboros binds Ouroboros as the author, never the subject repository.
| 1 | secrets_check | Are secrets, API keys, .env files, credentials present in the diff? | critical |
| 2 | code_quality | Careful code review: bugs, logic errors, crashes, regressions, race conditions, resource leaks? | critical |
| 3 | security_issues | Security vulnerabilities an outside input can reach: injection from model, tool, web or skill input; path traversal out of a confinement the code promises; secret leakage to a third party? Defenses against the owner or the subject's own process are not security findings. | critical |
| 4 | 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. Existing tests in any module or layer count when they exercise the changed behavior and would catch the identified failure; module or feature proximity alone is not 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 |
| 4 | 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. Existing tests in any module or layer count when they exercise the changed behavior and would catch the identified failure; module or feature proximity alone is not coverage. Requiring an additional overlapping selector/unit/e2e test is only justified when a second distinct failure mode is named explicitly. Wanting one more test is not a finding. Surplus direction, advisory: name a newly staged test that only pins wording, source text or shape, or duplicates an existing oracle, with the test to extend; retiring such a test is not a weakening. Public contracts, machine-read book shape and architecture boundaries stay pinned. | critical |
| 5 | architecture_doc | Did the change REPLACE the current description of every node it touched in the subject's architecture / engineering documents (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 or the subject keeps no such document. A removed or relocated claim names its surviving owner or evidence of retirement; registry tests prove membership, not preservation of meaning. 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 (a present-tense map, one home per fact). | critical |
| 6 | 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 the subject's platform abstraction layer, when it has one? Does it import Unix-only or Windows-only modules (`fcntl`, `msvcrt`, `winreg`, `resource`) at any level without a platform guard (a `sys.platform` check or the subject's equivalent)? | critical |
| 7 | capability_regression | Does the diff REMOVE or NARROW a previously-supported user-facing behavior or capability — a tool/flag/mode/path that worked before now errors or is gated tighter (e.g. a new `is_dir`/existence guard that blocks a legitimate create, a tightened allowlist that drops a real path, a removed fallback)? If so, is it INTENTIONAL and disclosed as a breaking/capability change in the commit message + changelog? Accidental capability removal is the failure class this item names. Ask whether a golden "from zero" test would have caught it. **Guard-change trigger (executable requirements, not an essay):** ADDING or TIGHTENING a guard, filter, allowlist, or deny rule IS a capability change and fires this item. For such a diff the reviewer must verify two things: (a) the diff STAGES A POSITIVE TEST that exercises a legitimate flow THROUGH the new guard and proves it still succeeds — a negative "it blocks X" test alone is insufficient (a gate can pass its own probe while breaking every real run); (b) the diff or its disclosure NAMES THE SURVIVING POSITIVE PATH — the concrete actor and flow that still work after the change. A guard change that stages no surviving-path test is a capability-regression finding, not a safety improvement. A guard keyed on model or provider identity (id, prefix, family, provider name) always fires this item; its positive test uses an identity that the diff's own literals do not contain, and a test that proves only refusal is insufficient. **Owner acceptance:** a narrowing counts as OWNER-ACCEPTED only when the subject's owner actually decided it (the owner's words or answer for this work, or a standing disclosure the subject keeps for this purpose) or it lies within authority the owner had already delegated for this work; the plan council's answers are the reviewers' view, not the owner's consent. Owner acceptance makes the finding advisory (disclosed, non-blocking). Intent wording, a commit-message disclosure, or a changelog row alone is disclosure, NOT acceptance. This names who accepts a narrowing; it adds no approval step. Severity follows the `Critical surface whitelist` below — silently removing a documented user-facing capability or a release contract is critical; removing or loosening an internal guard, proof or re-check is advisory and needs one line naming the residual; an owner-accepted narrowing or an internal-only refactor is advisory. | advisory |
| 7 | capability_regression | Does the diff REMOVE or NARROW a previously-supported user-facing behavior or capability — a tool/flag/mode/path that worked before now errors or is gated tighter (e.g. a new `is_dir`/existence guard that blocks a legitimate create, a tightened allowlist that drops a real path, a removed fallback)? If so, is it INTENTIONAL and disclosed as a breaking/capability change in the commit message + changelog? Accidental capability removal is the failure class this item names. Ask whether a golden "from zero" test would have caught it. **Guard-change trigger (executable requirements, not an essay):** ADDING or TIGHTENING a guard, filter, allowlist, or deny rule IS a capability change and fires this item. For such a diff the reviewer must verify two things: (a) the diff names and runs a positive test, existing or newly staged, that walks a legitimate flow THROUGH the new guard and proves it still succeeds — a negative "it blocks X" test alone is insufficient (a gate can pass its own probe while breaking every real run); (b) the diff or its disclosure NAMES THE SURVIVING POSITIVE PATH — the concrete actor and flow that still work after the change. A guard change without passing surviving-path evidence is a capability-regression finding, not a safety improvement; a missing *new* test is not. A guard keyed on model or provider identity (id, prefix, family, provider name) always fires this item; its positive test uses an identity that the diff's own literals do not contain, and a test that proves only refusal is insufficient. **Owner acceptance:** a narrowing counts as OWNER-ACCEPTED only when the subject's owner actually decided it (the owner's words or answer for this work, or a standing disclosure the subject keeps for this purpose) or it lies within authority the owner had already delegated for this work; the plan council's answers are the reviewers' view, not the owner's consent. Owner acceptance makes the finding advisory (disclosed, non-blocking). Intent wording, a commit-message disclosure, or a changelog row alone is disclosure, NOT acceptance. This names who accepts a narrowing; it adds no approval step. Severity follows the `Critical surface whitelist` below — silently removing a documented user-facing capability or a release contract is critical; removing or loosening an internal guard, proof or re-check is advisory and needs one line naming the residual; an owner-accepted narrowing or an internal-only refactor is advisory. | advisory |
| 8 | changelog_accuracy | Do the exact wording, test counts, and minor description details in the subject's changelog or release-notes entry match what the diff actually does? Wording drift, off-by-one test counts, minor inaccuracies in descriptive prose — these belong here, NOT in a release-metadata or consistency item. 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. (PASS with "Not applicable" when the subject keeps no changelog.) | advisory |
| 9 | perf_lifecycle | If the diff changes data readers or an endpoint, poller, subscription, timer, startup, shutdown or other batch operation: evaluate the total read cost as history, object count and project size grow, including nested repeats, cold caches and contention for shared resources. Where that growth can materially hurt responsiveness, is there proportionate evidence at representative scale, and was redundant work or within-operation reuse considered before adding infrastructure? This remains advisory, with no universal time limit, mandatory heavy benchmark or new approval gate. Does any interaction-path read scan an unbounded store per request/message/tick (a full-table read filtered in code is such a scan)? Is a subscription/observer/interval/listener added without a paired disposer? Does O(history) work run on a poll/stream path? Does a GET handler perform new steady-state durable writes? Where the subject documents these invariants, check against its definitions rather than re-deriving them here. (PASS with "Not applicable" if the diff touches none of these surfaces.) | advisory |
@ -402,7 +402,7 @@ Ownership` section is delivered beside them.
| # | item | what to check | severity when FAIL |
|---|------|---------------|--------------------|
| 10 | bible_compliance | Does the diff violate any BIBLE.md principle? | critical |
| 11 | development_compliance | Does it follow DEVELOPMENT.md patterns? Check explicitly: (a) naming conventions (snake_case modules/vars, PascalCase classes, UPPER_SNAKE_CASE constants); (b) entity type rules and dependency direction — Gateway classes contain ONLY transport, no business logic; Tool functions are thin wrappers; provider- and transport-specific decisions never flow back into core policy (judged by the `Shared Contract Ownership` section delivered with this checklist); (c) module and function size follow DEVELOPMENT.md "Module Size & Complexity" (official CI enforces the gates, so a stale or growing size-debt entry is review debt to flag, not a local commit block); (d) one owner per decision — a local test of the diff that packet seats apply too: nothing the diff adds (state, branch, layer or wrapper) decides again what an existing owner decides; a derived view stays a view, never extra truth; new durable state needs an invariant that existing facts cannot reconstruct. A finding names the exact symbol, the owner or fact it duplicates and the smaller contract-preserving alternative; diff size, line count and file count alone are not findings (P7). For an added surface (module, state file, ledger, resolver, cache, retry path, tool, endpoint or background loop) name the existing mechanism `docs/ARCHITECTURE.md` maps for the need, or state its absence in one line; this judges a CHANGE, not a plan. A second home in another module is one finding: a seat asked Part 2 reports it under coupling `architecture_fit`, a packet seat here. Tracked material needs a continuing purpose (DEVELOPMENT.md "Documentation contract"): retire finished campaign machinery instead of keeping it with presence-only tests; a retired prose or spelling pin names the surviving behaviour test or the requirement that is gone; keep book-shape, public-contract and architectural-boundary checks; (e) new LLM calls go through the shared `LLMClient`/`llm.py` layer, not ad-hoc HTTP clients; (f) cognitive artifacts follow BIBLE.md P1's record/view contract: retain exact sources before replacing a body, identify partial views and their delivered ranges with resolvable sources, and preserve operative decisions, provenance and the required resident core; a partial view is disclosure, not evidence of complete review or authority; (g) new `get_tools()` exports follow the ToolEntry pattern in registry.py; (h) provider independence — no core capability (agent loop, multi-model commit review, the coupling question, memory/context flows) silently requires a second provider or OpenRouter, and every supported single direct provider keeps its model AND review-pool rows self-fillable (DEVELOPMENT.md "Provider Independence"); (i) a claimed-complete visible UI change carries vision-inspected evidence from a relevant real consumer flow; an uninspected screenshot is insufficient; states, viewports and engines are risk-selected, mobile/WebKit are not universal, and an unavailable optional engine alone is not degradation. | critical |
| 11 | development_compliance | Does it follow DEVELOPMENT.md patterns? Check explicitly: (a) naming conventions (snake_case modules/vars, PascalCase classes, UPPER_SNAKE_CASE constants); (b) entity type rules and dependency direction — Gateway classes contain ONLY transport, no business logic; Tool functions are thin wrappers; provider- and transport-specific decisions never flow back into core policy (judged by the `Shared Contract Ownership` section delivered with this checklist); (c) module and function size follow DEVELOPMENT.md "Module Size & Complexity" (official CI enforces the gates, so a stale or growing size-debt entry is review debt to flag, not a local commit block); (d) one owner per decision — a local test of the diff that packet seats apply too: nothing the diff adds (state, branch, layer or wrapper) decides again what an existing owner decides; a derived view stays a view, never extra truth; new durable state needs an invariant that existing facts cannot reconstruct. A finding names the exact symbol, the owner or fact it duplicates and the smaller contract-preserving alternative; diff size, line count and file count alone are not findings (P7). For an added surface (module, state file, ledger, resolver, cache, retry path, tool, endpoint or background loop) name the existing mechanism `docs/ARCHITECTURE.md` maps for the need, or state its absence in one line; this judges a CHANGE, not a plan. A second home in another module is one finding: a seat asked Part 2 reports it under coupling `architecture_fit`, a packet seat here. Tracked material needs a continuing purpose (DEVELOPMENT.md "Documentation contract"): retire finished campaign machinery instead of keeping it with presence-only tests; tests retire under DEVELOPMENT.md "Retiring tests" (a verdict line each; wording pins by class, a structural scan of a live rule rewritten or kept, merges with a named survivor); keep book-shape, public-contract and architectural-boundary checks; (e) new LLM calls go through the shared `LLMClient`/`llm.py` layer, not ad-hoc HTTP clients; (f) cognitive artifacts follow BIBLE.md P1's record/view contract: retain exact sources before replacing a body, identify partial views and their delivered ranges with resolvable sources, and preserve operative decisions, provenance and the required resident core; a partial view is disclosure, not evidence of complete review or authority; (g) new `get_tools()` exports follow the ToolEntry pattern in registry.py; (h) provider independence — no core capability (agent loop, multi-model commit review, the coupling question, memory/context flows) silently requires a second provider or OpenRouter, and every supported single direct provider keeps its model AND review-pool rows self-fillable (DEVELOPMENT.md "Provider Independence"); (i) a claimed-complete visible UI change carries vision-inspected evidence from a relevant real consumer flow; an uninspected screenshot is insufficient; states, viewports and engines are risk-selected, mobile/WebKit are not universal, and an unavailable optional engine alone is not degradation. | critical |
| 12 | version_bump | Release carriers: an ordinary commit is a numbered release (P9) — is `VERSION` bumped, does every other carrier agree with it (the file carriers `release_sync.version_carrier_desyncs()` checks, the `docs/ARCHITECTURE.md` header included) and does README carry the badge and the changelog row for the new version? A version-neutral contribution — an external contribution commit (P9 / CONTRIBUTING.md; the maintainer's integration commit bumps) or a commit prepared in a body candidate that P9 lets the installation adopt as it is — instead keeps EVERY carrier byte-identical and takes no tag. A partial carrier change fails in either form; the exception waives no other item. | critical |
| 13 | tool_registration | New tool not exported in `get_tools()`, OR without exactly one explicit safety policy (`ToolEntry.safety_policy` or `ouroboros/safety.py::TOOL_POLICY`, guarded by `test_tool_policy_covers_all_builtin_tools`), OR a new or lowered `skip` it does not earn? (PASS if no new tool or policy change.) `skip` fits a trusted built-in whose effects host guards already fence; opaque or outward-facing tools keep `check`. | critical |
| 14 | self_consistency | Does this change affect behavior described in `BIBLE.md`, `prompts/`, `docs/`, or this checklist itself? Check explicitly: (a) every tool name a prompt (`prompts/*.md`) mentions exists in `get_tools()` and means the same thing there — completeness is NOT required, the schemas are the catalog; a concrete prompt contradiction with BIBLE, another prompt or a book follows the shared critical threshold and surface whitelist; a prompt that restates mechanism, a tool's own description, a structurally enforced gate or a rule stated elsewhere is advisory, and a prompt that gains a sentence loses an equivalent one; a keyword, regex, branch, threshold or counter that stands in for judgment in a prompt violates 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; (b) JSONL log/memory file formats described in `ARCHITECTURE.md` match all readers/writers; (c) any behavioral change reflected in the wake template `prompts/CONSCIOUSNESS.md` if it affects wakes; (d) DEVELOPMENT.md rules still accurate after the change. Severity must follow the shared `Critical surface whitelist` with the body carriers below — release metadata, tool schema, module map, behavioural documentation, safety guarding or frozen contracts are critical; commentary/prose/stylistic mismatches are advisory. | critical |

View file

@ -127,7 +127,7 @@ row here, count-anchored both ways.
| `state/delegate_recovery/`, `state/delegate_recovery_transactions/` (+`active.json`), `state/delegate_supervision/` | `ouroboros/delegate_recovery.py`, `delegate_supervision.py` (+ startup sweep `delegate_state_sweep.py`) | own `schema: 1` on supervision/transactions (a returning restart adds `return_ids`/`queue_ids` via `restart_retention.prepare_restart_returns` and boot stamps `returns_restored_at` once, #1563); recovery rows fingerprinted, unversioned | terminal+age startup sweep: terminal-status recovery rows (`vetoed`/`adopted`), unreferenced transactions and settled-task supervision files past GC retention; live/resumable rows, `active.json`, no-result tasks and unreadables kept fail-closed; unreadable custody log skips the sweep | interrupted delegated runs cancelled instead of adopted; duplicate wake replay; planned handoffs vetoed |
| `state/delegate_actor_claims/*.lock`, `state/.payload_delegation_claim.lock` | `ouroboros/delegate_custody.py`, `delegate_start_claims.py` | none — locks | unlinked on release | no durable state |
| `state/code_intel/<root-sha>/inventory.json` | `ouroboros/code_intelligence.py` | own `schema_version: 5` (older/malformed rebuilt); hash-keyed local outlines, imports, calls, routes and coverage, plus bounded import specifiers/anchors with method/completeness; no source bodies, general occurrence lists or cross-file joins | compact rewrite on change (excl. created_at); startup prunes stale roots by mtime past GC retention; child terminal file adoption drops `state/code_intel`; imports/joins recheck bytes and current paths, incl. nested repos | pure derived cache; re-index; older caches rebuild on use (see [code navigation](code-navigation.md)) |
| `state/preflight_receipts/<key>.json` | `ouroboros/preflight_receipts.py` `FullGateReceipt.publish` (atomic replace; written only by `preflight_runner.run_hermetic_pytest` after a complete default gate: node lane if present, parallel pass on >=2 observed xdist workers, serial pass, proven containment, executing source and installation unmoved) | own `schema: 1`; `identity` (HEAD/tree/index, workload fingerprint with interpreter and Node, Python-installation metadata, Git and executing-source digests; unbound local trust: other executables tests find, e.g. ripgrep, and package edits absent from metadata) whose canonical-JSON sha256 names the file; `authenticity: unsigned` | one file per identity on the CANONICAL data root, read only by that runner; no TTL, cap or cleanup; a later red run deletes nothing (one historical green run, not the latest attempt); unreadable or mismatched reads as absent | deletion only costs a rerun of the full battery; unsigned (owner 3A): a local writer can forge one to skip that rerun — reviewers still see the exhibit and a pull request still runs CI |
| `state/preflight_receipts/<key>.json` | `ouroboros/preflight_receipts.py` `FullGateReceipt.publish` (atomic replace; written only by `preflight_runner.run_hermetic_pytest` after a complete full gate (the default recipe or a full-tier selection): node lane if present, parallel pass on two observed xdist workers or the one admitted, serial pass, proven containment, executing source and installation unmoved) | own `schema: 1`; `identity` (HEAD/tree/index, workload fingerprint with interpreter and Node, Python-installation metadata, Git and executing-source digests; unbound local trust: other executables tests find, e.g. ripgrep, and package edits absent from metadata) whose canonical-JSON sha256 names the file; `authenticity: unsigned` | one file per identity on the CANONICAL data root, read by that runner and by adoption of exactly its HEAD/tree (`preflight_qualification.receipt_for_commit`); no TTL, cap or cleanup; a later red run deletes nothing (one historical green run, not the latest attempt); unreadable or mismatched reads as absent | deletion only costs a rerun of the full battery; unsigned (owner 3A): a local writer can forge one to skip that rerun — reviewers still see the exhibit and a pull request still runs CI |
| `state/extension_reconcile/` (+`failed/`) | `ouroboros/extension_reconcile_queue.py` | none — not needed (one-shot markers) | consumed by server loop; after 5 attempts moved to `failed/`, where markers age-prune past GC retention (the failure fact stays durable in events.jsonl) | pending worker→server reconciles lost; re-toggle heals |
| `state/workspace_executor_processes/` | `ouroboros/workspace_executor.py` | own `schema_version: 1` + owner tag | unlink on stop; stale rows filtered at read (pid/cmd-sha) | service processes survive unreaped |
| `state/acceptance_fence_acks/<token>.<req>.json` | `supervisor/events_worker_reports.py` (writer, one file per pooled-worker request), `ouroboros/agent.py` (the requester reads and unlinks only its own `req`; direct turns fence in-process and write none) | none — transport ack | inline GC on write (255 newest / 3600 s); a late ack nobody waits for ages out here | the waiting worker re-sends a transition once (a read never), then records a typed `supervisor_ack_unavailable` unknown — never read as a refusal |

View file

@ -253,7 +253,8 @@ ouroboros/ — agent core and shared runtime (§6)
review.py — Size and complexity inventory behind the shrink-only ceilings and the diff pulse (§6 Structural gates, Commit preflight)
size_ratchet_manifest.py — Generated data-only size-debt manifest (`scripts/regenerate_size_ratchet.py`)
review_execution_projection.py — Reviewer-execution projection; kinds `api` | `harness` | `native`, so the owner can tell a retrieving review from a packet review on the same model
preflight_runner.py, preflight_node.py, preflight_receipts.py — Hermetic test gate, Node lane, receipts (§6 Hermetic preflight proof; §8)
preflight_runner.py, preflight_node.py, preflight_receipts.py, test_parallelism.py — Hermetic gate, Node, receipts, worker budget (§6; §8)
test_selection.py, preflight_inventory.py, preflight_qualification.py — Scope, execution inventory, full adoption proof (§6)
review_substrate.py — Review seat coordinator: independent seats, per-actor records that keep transport, parse, verdict, coverage and quorum distinct (§6 Review stack, Task acceptance)
review_custody.py, review_source_closure.py, review_operation.py, review_live_join.py — Reviewer workers: no-resend custody, retained owner-bound inputs, panel waits/checkpoints, existing-producer collection/live joins (§6 Task acceptance)
review_owner_custody.py — Paid attempts record `(server session, pid)` (§6 Paid stamp and owner custody)
@ -264,7 +265,7 @@ ouroboros/ — agent core and shared runtime (§6)
review_session_custody.py, review_checking.py — Delegated-review recovery validation and the pre-POST durable invocation checkpoint; writable seats' private checking copies (§6 Review delivery)
review_slot_cancel.py — A cancel outcome reports only what it proved; a succeeded run whose result read fails is the typed `ReviewSessionSucceededResultUnavailable`, never "may still be live"
review_actor_aggregation.py, review_session_usage.py, review_thread_continuity.py — Completed-actor contract aggregation; delegated-session usage attribution; Claudexor thread operations for delegated plan reviewers
commit_admission.py — Deterministic commit-admission SSOT (release checks, staged-Python syntax, `run_tests_preflight_with_proof`); the commit gate delegates here (§6 Hermetic preflight proof)
commit_admission.py — Release/syntax admission and bound test proof (§6)
reviewer_slot_config.py — The one review-pool builder, `review_pool_slots`, over enabled `review_eligible` catalog rows (§7 Review pool)
review_pool_migration.py, review_pool_receipts.py — The review-lane → review-pool migration at the settings read seam and its durable receipts (the `state/review_migrations/` snapshot is the rollback source) (§11.4)
review_run_isolation.py — Contributor review isolation before `config`: private whole-data root, pinned host settings and panel, cumulative cap, attach-only engine (§6 Delegated subagents; Monetary authority and projections)
@ -518,7 +519,7 @@ ouroboros/ — agent core and shared runtime (§6)
subagent_integration.py — integrate_subagent_patch (sha256, 3-way --index, protected-path gated, genesis refused), external-workspace audited verdict, `coop_already_in_tree` no-op, compare_subagent_patches
patch_verdict.py — The one verdict writer for both patch pipelines: subjects are minted by the writer (`run_<rid>`), never prefix-matched by readers; artifact plus typed `delegate_run_patch_verdict` custody row
delegate_start_claims.py — Serializes custody recheck and `START_REQUESTED`; transport/run waits are outside locks. Unsent retries may hold actor/payload locks, never root/store locks
process_containment.py — Env-token container membership (`OURO_PROC_CONTAINER_*`: Linux /proc, macOS `ps -E`, Windows Job Object); an alive-or-undeterminable member is an honest hard-block answer, never a kill guarantee (§6 Delegated subagents)
process_containment.py, process_lifeline.py, process_overload.py — Token/Job custody, POSIX owner-death guardian and RSS stop; unconfirmed cleanup blocks; supervision, not a quota (DEVELOPMENT §9)
process_scope.py, process_census.py — work scopes (membership, records, task-end/Panic reap, engine index); kernel-state readers (§9)
process_custody.py — `spawn_supervised` and the durable process_ledger.jsonl; `reap_orphaned_processes` with strict identity and `retained_purposes`; the parent lifeline; `stop_ledgered_processes` requires measured identity and confirmed exit (Runtime topology below; §9)
obligations.py — Atomic current sets (incl. `owner_questions`); owning transitions publish before work and retire after discharge

View file

@ -386,9 +386,9 @@ Every Advisory or Cyber Pro outcome returns its complete result and exact `revie
#### Hermetic preflight proof
Only `preflight_runner.py` mints `ctx._preflight_test_proof`, after green lanes and containment, binding HEAD, tree, index, pass specs, isolated environment, interpreter and Node. Child PATH carries selected Node; workload normalization shares the temp-parent default (DEVELOPMENT § Build & CI). Tests may read HEAD: a new commit needs a new run; skip/no-suite `None` is never green. Assembly, lanes and typed `PREFLIGHT_*` blocks: DEVELOPMENT "The commit gate mirrors the CI split"; no silent skip or serial fallback. Unproven teardown retains the temp tree and Git registration: deleting live inputs would destroy custody.
Only `preflight_runner.py` mints `ctx._preflight_test_proof` after green lanes and containment, binding HEAD/tree/index, pass specs, isolated environment, interpreter and Node. `test_selection.py` selects floor, changed tests, direct importers and widening; `preflight_inventory.py` reconciles executed IDs, and records keep tier, base/policy, reasons and counts. Missing floor or execution blocks; verifier changes force full (DEVELOPMENT "The tiered commit and CI gates"). Unknown teardown retains the tree and Git registration.
A complete default run (every lane, ≥2 observed parallel workers) writes `state/preflight_receipts/<key>.json`: one historical green run, unsigned, reused by a later process or Restart. Its identity adds Git, the selected Python installation's metadata and the executing source, pinned once at code load (older loaded code tests, never mints or accepts); launch descriptors and containment tokens never miss. Unbound local trust: forgery, other executables tests find (ripgrep), package edits absent from metadata. Seats' sized, hashed briefs carry the frozen `preflight_execution` exhibit; a pending rejoin keeps it.
Selected proof never qualifies adoption, managed updates or evolution publication; adoption and publication accept only existing full evidence of the exact commit (`preflight_qualification.py`), never a new run. Only a complete full run (all lanes, two workers or the one admitted) writes `state/preflight_receipts/<key>.json`, unsigned, reusable across processes. Identity adds Git, Python metadata and load-pinned source; older code cannot mint/accept. A new HEAD requires a run; skips grant no proof. Forgery, other executables and unrecorded package edits stay outside its trust. Seats' hashed briefs carry the frozen `preflight_execution`; pending rejoins keep it.
#### Commit preflight

View file

@ -10,21 +10,21 @@ The local `ouroboros-stable` ref is also a recovery fallback maintained by expli
### CI topology
`.github/workflows/ci.yml` has 24 jobs, grouped by trigger and secret exposure:
`.github/workflows/ci.yml` has 25 jobs, grouped by trigger and secret exposure:
| Tier | Jobs | Trust boundary |
|---|---|---|
| Fork-safe PR validation | `quick-test`, `sqlite-floor` (the store modules on ubuntu-22.04's system SQLite 3.37.2), `betterleaks-platform-smoke`, `benchmark-methodology` (also `ouroboros-stable` pushes and `v*` tags) | `ouroboros` pushes, pull requests into `ouroboros`, manual runs; read-only, no provider secrets, never `pull_request_target` |
| Fork-safe PR validation | `quick-test`, `pr-selected-os`, `sqlite-floor` (store modules on ubuntu-22.04's SQLite 3.37.2), `betterleaks-platform-smoke`, `benchmark-methodology` (also `ouroboros-stable` pushes and `v*` tags) | PRs into `ouroboros` run the selected floor + affected modules (Windows defers full escalations to landed shards); `quick-test` is full on pushes/dispatch; read-only, no provider secrets or `pull_request_target` |
| Android source/build proof | `android-test` | Every run; `android/tests`, release tests and disposable-key APK compilation; no publisher secrets |
| Android emulator proof | `android-emulator-smoke` | Tags or verified `android/` changes; install/start and same-UID PackageInstaller readback on an x86_64 API matrix; no publisher secrets |
| Browser consumer proof | `ui-browser.yml`, called by `ci.yml` and `ui-browser-push.yml` | One reusable complete lane: PRs, manual runs and `v*` tags via `ci.yml`; every `ouroboros` push via `ui-browser-push.yml`, which alone owns the partial diagnostic scenario. No path filter: Makefile-, spec- and requirements-only merges are read by tests. Four `ui-shard` jobs, one `ui-manifest` collection and the `ui-smoke` verdict, which reconciles the shards against the manifest and refuses a red session. No schedule, no provider secrets |
| Opt-in lanes, no schedule | `system-e2e-mock` (keyless; manual runs and `v*` tags, on the release bar), `e2e-live` (`OUROBOROS_E2E_LIVE_OPENROUTER_KEY`, `$30` cap) | `e2e-live` starts only from a dispatch that opts in through the `e2e_live` input; without the secret it is one summary line and green, never a pretend run. Both upload their servers' journals, never a settings file |
| Ordinary desktop tests | `full-test-runs` → `full-test` (no secrets) | PRs and in-repo `ouroboros` pushes run Windows/macOS beside Ubuntu quick-test; stable pushes, dispatch and `v*` tags run all three OSes. Windows has four whole-file shards with both default passes, POSIX utilities and mandatory proof. Shard 1 collects unsharded manifests and runs node/ESLint, size, import and Chromium attachment/composer checks (empty/skipped fails). The verdict needs the matrix and both pass proofs; subtests stay, only parent calls count as duplicate executions. macOS/Ubuntu stay unsharded |
| Ordinary desktop tests | `full-test-runs` → `full-test` (no secrets) | In-repo `ouroboros` pushes run Windows/macOS beside quick-test; stable pushes, dispatch and `v*` tags run all three OSes. `scripts/run_tests.py` owns every pass. Windows has four whole-file shards with both default passes, POSIX utilities and mandatory proof. Shard 1 collects unsharded manifests and runs node/ESLint, size, import and Chromium attachment/composer checks (empty/skipped fails). The verdict needs the matrix and both pass proofs; subtests stay, only parent calls count as duplicate executions. macOS/Ubuntu stay unsharded |
| Trusted skill matrix | `skill-smoke` (`OPENROUTER_API_KEY`) | `ouroboros-stable` pushes, manual runs, `v*` tags; never pull requests |
| Trusted provider runs | `integration-test` (required), `integration-optional`; body in `provider-canary.yml` | Manual/tag callers select disjoint markers from `credential_required`; push wrapper keeps default `all` on the three shared branches. Producers stay strict: optional RED may redden the workflow but is no release dependency. Its floor is omitted; required skips/missing block official tags. Pre-tag qualification reads required jobs and every required canary, not aggregate green. Scoped artifacts avoid collisions. No PRs/schedule; explicit secrets only |
| Tag-only gates and release chain | `release-preflight` needs successful `full-test`, required `integration-test` and `system-e2e-mock`; a valid tag may run desktop `build` diagnostically after a red preflight. `release` additionally needs build, `skill-smoke`, `ui-smoke`, `marker-guards`, `docker-ui-smoke`, `docker-portable-test` and `windows-proof`. The latter checks/attests the final ZIP without secrets; configured `windows-sign` alone holds its signing Environment. Android proofs gate its optional pair; `vendor-package-smoke` is informational | Tags publish only after all required jobs pass. Smoke gates also run on manual dispatch without secrets. Required provider failures or inconclusive skips block publication, never hidden by diagnostic build success |
Quick and full-test runners enforce `size_ratchet` (local runs warn): tip manifest exactness and shrink-only transition against `OURO_SIZE_RATCHET_BASE_REF`. An unresolvable base uses the tree-verified parent; a resolvable base without a manifest fails closed, since copying one would launder debt. Each composed book is also compared with the event base; only that check skips if no base resolves. `scripts/book_growth_mode.py` derives `OURO_BOOK_GROWTH_MODE` (`block|warn|approved`) from PR metadata with `pull-requests: read` (DEVELOPMENT "Documentation contract"). Both jobs run `cd web && node --test tests/*.test.js`, mirrored by `ouroboros/preflight_node.py`. Skill review precedes downloaded plugin imports: untrusted payload code must never share provider credentials; a missing required key is red.
Quick and full-test runners enforce `size_ratchet` (local runs warn): tip manifest exactness and shrink-only transition against `OURO_SIZE_RATCHET_BASE_REF`. An unresolvable base uses the tree-verified parent; a resolvable base without a manifest fails closed, since copying one would launder debt. Each composed book is also compared with the event base; only that check skips if no base resolves. `scripts/book_growth_mode.py` derives `OURO_BOOK_GROWTH_MODE` (`block|warn|approved`) from PR metadata with `pull-requests: read` (DEVELOPMENT "Documentation contract"). Both tiers run the complete Node lane (`preflight_node.py`). Skill review precedes downloaded plugin imports: untrusted payload code must never share provider credentials; a missing required key is red.
Shared UI and keyless wait/repair browser fixtures run the dirty Git candidate in an independent checkout (`tests/candidate_checkout.py`) and prove through `/api/health` and static sentinels that the served assets and imported Python came from that copy (recipe: DEVELOPMENT "Safe local verification and dirty browser candidates"). `ouroboros/test_environment.py` gives pytest, preflight and server children scrubbed temporary roots before imports.

View file

@ -145,8 +145,9 @@ Track assets with a continuing purpose (product, contributors, verification, leg
requirements, evidence for public claims). Plans, campaign backlog, review packets and
run receipts belong in the external work area or durable task evidence, not the
tracked tree; a test preserving their presence gives them no product role. Retire
campaign tooling when its purpose ends. Review enforces this, without automatic
deletion or filename matching.
campaign tooling when its purpose ends. Name a new test module for the behaviour or
owner it protects, not a round, batch, issue, PR or version. Review enforces this,
without automatic deletion or filename matching.
Both are reference books: an entrypoint plus one chapter file per subject under
`docs/architecture/` or `docs/development/`. The entrypoint's ordered `## Chapters`

View file

@ -38,6 +38,6 @@ The operation runs no tests. The contributor lane first runs the proposal's herm
A pull request into `ouroboros` leaves every version carrier byte-identical to its target (the carrier list and the one projection that writes them: ARCHITECTURE §10, invariant 2). Use `preflight_review(commit_message="...", deterministic_only=True, source="worktree" | "index")` for free release diagnostics and a separately labelled worktree book balance before review spend; it grants no freshness. Keep source failures separate from candidate findings; verify partial staging with unchanged files, index and state (`tests/test_release_metadata_diagnostics.py`; contracts: ARCHITECTURE §6 "Commit preflight"). At integration `release_sync.sync_release_metadata()` projects the chosen version; `release_metadata_findings()` checks carriers, the current README row and the P9 limits; changelog prose and trimming stay deliberate; diagnostics add no autofix. Packaging: ARCHITECTURE §8 "Build scripts".
Hermetic preflight isolation: §14 "The commit gate mirrors the CI split". A test rebinds every imported process-global root and fails closed on the live data root; setting `OUROBOROS_DATA_DIR` alone is not isolation. Enforced by `tests/conftest.py`. A reviewed local commit is the durability boundary of self-modification; an `origin` push and CI are follow-up signals, not prerequisites for its survival (ARCHITECTURE §8).
Hermetic preflight isolation: §14 "The tiered commit and CI gates". A test rebinds every imported process-global root and fails closed on the live data root; setting `OUROBOROS_DATA_DIR` alone is not isolation. Enforced by `tests/conftest.py`. A reviewed local commit is the durability boundary of self-modification; an `origin` push and CI are follow-up signals, not prerequisites for its survival (ARCHITECTURE §8).
---

View file

@ -9,7 +9,7 @@ Managed updates rewrite the live tree under owner authority while other work run
- Record restoration intent in the existing transaction before apply and consume it on every completion (restored, preserved or incomplete); failed persistence keeps the marker, and an interrupted replay never resets or reapplies over returned or later work (`supervisor/update_merge.py`).
- Schema 1 is the default; stash restoration and a rollback carrying stashed work need schema 2 before old code is checked out: an older strict reader refuses an interrupted new state without a reset. `stash_restored` means a recorded successful apply, never intent. Restart the actual desktop app, Docker/service or direct server, not its browser tab; in-app Restart refuses while a restoration is pending.
- The resolver stages the full merge, tracked binaries included; review gets exact staged mode/blob/size and parent object ids, and missing metadata blocks.
- A managed merge commit requires a green full suite on the exact candidate (“The commit gate mirrors the CI split”). Any non-commit resolver terminal rolls back and best-effort preserves the attempt on its failed-update branch; recovery needs the fresh rescue, not that branch. Boot holds a Paused commit (`postcommit_resume`); its Stop rolls back.
- A managed merge commit requires a green full suite on the exact candidate (“The tiered commit and CI gates”). Any non-commit resolver terminal rolls back and best-effort preserves the attempt on its failed-update branch; recovery needs the fresh rescue, not that branch. Boot holds a Paused commit (`postcommit_resume`); its Stop rolls back.
- Fresh rescue precedes every destructive rollback and boot re-materialization; a capture failure is fail-open but durably disclosed with a transaction pointer (the choke point: ARCHITECTURE §2).
- Manual Restore fences writers and pins HEAD before reset. Promotion resolves one development SHA for local QA and remote push.

View file

@ -1,15 +1,17 @@
# Process Custody Rule
Each work-started process has one lifetime owner able to observe/end it by measured identity: task, server generation, delegated run, test session or installation. At scope end, stop or explicitly hand its processes to a longer-lived owner, never init. Kernel-verified membership: ledger, `OURO_{INSTALL,TASK}_SCOPE_<hex>=1` marker or recorded group, never by command line. Release/terminal paths attach/settle, never provision/download/spawn/admit. Failed reads/signals mean unverified, not gone. Retire only on a recorded settled/no-census outcome (`test_task_scope_lifetime.py`, `test_exit_does_not_admit.py`).
Each work-started process has one lifetime owner: task, server generation, delegated run, test session or installation. End or hand it to a longer-lived owner, never init. Verify identity by ledger, `OURO_{INSTALL,TASK}_SCOPE_<hex>=1` or recorded group, never command line. Terminal paths settle, never provision/spawn/admit. Failed reads/signals mean unverified; retire only on recorded settled/no-census outcomes (`test_task_scope_lifetime.py`, `test_exit_does_not_admit.py`).
**Spawn chokepoint.** Long-lived `Popen`/`mp.Process` without bounded same-call waits use `process_custody.spawn_supervised`; existing Popen managers immediately `record_process`. Scopes: `task|session|daemon`; installation daemons survive generations. Plain `start_parent_lifeline` entrypoints apply `lifeline_parent_env()` LAST, excluding inherited grandparent identity; multiprocessing uses its parent sentinel (`test_pid1_parent_lifeline.py`). Worker trees use only `worker_pool_lifecycle.kill_worker_tree`, sparing ledger daemon roots (`live_daemon_root_pids`); task cancel/timeout and quiescent idle retirement also keep retained services. Other supervisor `kill_pid_tree` calls are defects. Ledger/fingerprint/skill-companion exception: ARCHITECTURE §1 Runtime topology. `test_process_custody.py` allowlists bounded helpers.
**Spawn chokepoint.** `Popen`/`mp.Process` without bounded same-call waits use `process_custody.spawn_supervised`; existing managers immediately `record_process`. Scopes: `task|session|daemon`; installation daemons survive generations. Plain `start_parent_lifeline` applies `lifeline_parent_env()` LAST, excluding grandparent identity; multiprocessing uses its parent sentinel (`test_pid1_parent_lifeline.py`). Worker kills use `worker_pool_lifecycle.kill_worker_tree`, sparing `live_daemon_root_pids`; cancel/timeout and idle retirement also keep retained services. Other supervisor `kill_pid_tree` calls are defects. Ledger/fingerprint/companion exceptions: ARCHITECTURE §1 Runtime topology. Bounded-helper allowlist: `test_process_custody.py`.
**Daemon stop.** `OwnedClaudexorDaemon.stop_outcome` (Panic, Restart) separates authenticated cooperative shutdown from forced signals (ARCHITECTURE §9). Read-only resolution never ensures/installs a runtime, starts a daemon, probes accounts or inherits another home's socket override. Stop uses the exact existing Node (`resolve_cli_command(require_npm=False)`), on Windows and bundled-only installs too. RPC ack or lease release proves no physical exit; an error-code string grants no signal. Bound the manager lock; keep preparation/network/exit waits outside. Bounds: `CLAUDEXOR_OPERATOR_STOP_TIMEOUT_SEC`, `CLAUDEXOR_STOP_EXIT_WAIT_SEC` (`runtime_limits`).
**Daemon stop.** `OwnedClaudexorDaemon.stop_outcome` (Panic, Restart) separates authenticated cooperative shutdown from forced signals (ARCHITECTURE §9). Read-only resolution never provisions a runtime/daemon, probes accounts or inherits another home's socket override. Stop uses the existing Node (`resolve_cli_command(require_npm=False)`), including Windows/bundled installs. RPC ack or lease release proves no physical exit; error codes grant no signal. Bound the manager lock; keep preparation/network/exit waits outside. Bounds: `CLAUDEXOR_OPERATOR_STOP_TIMEOUT_SEC`, `CLAUDEXOR_STOP_EXIT_WAIT_SEC` (`runtime_limits`).
**Failed start.** Only `ExitFact.failed_without_control` latches daemon spawning; `claudexor_startup_failure.py` labels the child's own log for diagnosis, never policy. Harvest at spawn, attach and stop (`_settle_exited_child`), sharing the reap lock. Releases are the periodic sweep, explicit Refresh, live attach or a new manager; add no backoff/counter/cooldown, host heap sizing, writer-lease policy or third retrier. `NODE_OPTIONS` remains the operator escape hatch. Run settlement alone grants no retirement: `still_continuable` keeps ordinary open/unknown owner lines and Continue offers; review/persistent lifetimes stay separate. Under the retirement lock, a complete second custody read proving no unsettled project run lets only typed `project_has_threads` discharge `PROJECT_RETIRE_FAILED` as `PROJECT_RETIRED {project_kept:true}`; other outcomes stay retryable. Automatic body collection preserves these rules and owns each physical job until actual exit, without delaying Panic (ARCHITECTURE §6/§9).
**Failed start.** Only `ExitFact.failed_without_control` latches daemon spawning. `claudexor_startup_failure.py` diagnoses the child log, never sets policy. Harvest under the reap lock at spawn/attach/stop (`_settle_exited_child`); release through sweep, Refresh, live attach or a new manager. No backoff/counter/cooldown, host heap sizing, writer-lease policy or third retrier; `NODE_OPTIONS` stays available. Settlement alone cannot retire: `still_continuable` keeps open/unknown owner lines and Continue; review/persistent lifetimes stay separate. Under the retirement lock, a complete second custody read proving no unsettled project run lets typed `project_has_threads` discharge `PROJECT_RETIRE_FAILED` as `PROJECT_RETIRED {project_kept:true}`; other outcomes stay retryable. Body collection follows this and owns jobs until exit without delaying Panic (ARCHITECTURE §6/§9).
**Lifetime across platforms.** Ordinary close and forced worker/server/stray cleanup spare the shared daemon's whole subtree. Never extend this to all skill companions, broaden their reserved-port/service ancestry rules or make another port a kill target; a recorded server is signalled only on its re-proved birth/command identity. Windows birth, command, Job, restart-handle and prior-generation stop claims need native Windows evidence (psutil is Windows-only; embedded/frozen imports need packaging proof); portable fixtures prove none. See ARCHITECTURE §1 "Platform substrate".
**Platforms.** Close and forced worker/server/stray cleanup spare the shared daemon subtree. Skill-companion reserved-port/service ancestry rules stay scoped; another port grants no kill authority. Re-prove recorded servers' birth/command identity before signalling. Windows birth, command, Job, restart-handle and prior-generation stops need native evidence; portable fixtures prove none (psutil: Windows-only; frozen imports: packaging proof). ARCHITECTURE §1 "Platform substrate".
**Restart and Panic.** Separate stop from next-start selection; never hot-swap a pin. Planned restart stops the daemon only for a changed pin (`server_restart._stop_owned_daemon_for_new_pin`, after worker custody, before optional cleanup); never a veto (`test_planned_restart_engine_pin.py`); quiescence checks all members before releasing a writer fence (`test_process_custody_liveness.py`). Manual Restart's update gate and `safe_restart` refuse before stopping; `_stop_owned_work` follows durable no-resume flags. Reuse `request_cancel`, `kill_workers(reconcile_delegate_custody=False)`, `reconcile_orphaned_runs` over `read_owned_gateway` and `stop_outcome`; never ensure a gateway from cancel to daemon stop or infer stop outcomes from private manager errors. Unconfirmed steps keep custody and critical diagnostics, never defer or veto Restart; add no state, drain, lease or event kind: startup sweep recovers (`test_manual_restart_execution.py`). Panic is irrevocable: failed ingress start calls the executor synchronously; manager/diagnostic faults spare other requests; bounded writes precede final exit (`test_server_control_panic_daemon.py`). Test all four continuations in `test_planned_delegate_assignment.py`: S1 saved-work/queue returns bind the successor; undispatched owner-wait/planned carriers keep their own gates without interprocess once claims.
Out-of-process extension HTTP responses run in the child: the runner owns staging, Popen registration and cleanup; `extension_route_stream` owns pipe frames and ASGI delivery; spawn, registration, pipe shutdown and termination stay off the ASGI loop; cancellation binds to the loaded bundle before spawn; no total or pre-header deadline applies (ARCHITECTURE §3).
Extension HTTP responses run in the child: the runner owns staging, registration and cleanup; `extension_route_stream` owns pipe frames and ASGI delivery; spawn, registration, pipe shutdown and termination stay off the ASGI loop; cancellation binds to the loaded bundle before spawn; no total or pre-header deadline applies (ARCHITECTURE §3).
**Test runs.** Use `ProcessContainer(lifeline=True, overload=True)`: a POSIX pipe guardian survives owner death; Windows uses the existing kill-on-close Job. Reap before release. `config.TEST_*` and `test_parallelism.py` own physical budgets; nested runners default to one slot. `overload_stop` names the largest member and ends red; a sampler the host cannot run disables only the stop, loudly. Supervision is no OS quota: RSS is summed and detached members are found by token every five seconds; token-dropping children and simultaneous guardian death remain residuals. Fault matrix: `test_process_owner.py`.

View file

@ -17,24 +17,27 @@ Default local pytest excludes seven costly or environment-dependent lanes — `i
`skill_smoke` and `size_ratchet` tests never also carry the `serial` marker or join `_SERIAL_TEST_FILES`: the `and not <lane>` markexprs are the lane barrier, and single-lane assignment keeps each test's placement unambiguous. A new opt-in lane registers its marker in `pyproject.toml`, adds a collect-only zero-test guard in CI, and keeps the default local addopts free of network and Docker requirements.
### Retiring test pins
### Retiring tests
Record the retired proposition and named behaviour-test survivor (or obsolete
requirement) in PR evidence, not a tracked ledger. Run the survivor and a falsifying
negative. CHECKLISTS Body item 11(d) owns the preservation rule and review.
Each removed or rewritten test gets a verdict line in PR evidence, not a ledger:
obsolete (what ended it), wording owned by review (which item), rewritten, or merged
(the survivor). Wording pins retire by class: a re-runnable rule and a sampled
independent read; a structural scan of a live rule is rewritten into behaviour or
kept. A merge runs its survivor and one falsifying negative per failure mode.
CHECKLISTS Body item 11(d) owns the review.
### Parallel CI and the `serial` marker
CI runs the default suite in two passes (`.github/workflows/ci.yml`, runners `quick-test` / `full-test-runs`): a parallel pass, `-m "not serial and <the seven lane exclusions>"` with `PARALLEL_PASS_FLAGS` (`-n auto --dist loadscope --max-worker-restart=0 --timeout=300 --timeout-method=thread`), then a flag-free serial pass, `-m "serial and <the same exclusions>"`. Windows alone adds `--default-suite-shard=K/4` (`tests/default_lane.py`): greedy packing by collected default-node count keeps whole files together across both passes, with path/index tie breaks. Shard 1 also collects unsharded `--default-suite-manifest` witnesses in fresh processes. Two rules preserve the split:
CI runs the default suite in two passes, each owned by `scripts/run_tests.py` (`.github/workflows/ci.yml`, runners `quick-test` / `full-test-runs`): a parallel pass, `-m "not serial and <the seven lane exclusions>"` with `PARALLEL_PASS_FLAGS` (`-n auto --dist loadscope --max-worker-restart=0 --timeout=300 --timeout-method=thread`), then a flag-free serial pass, `-m "serial and <the same exclusions>"`. Windows alone adds `--default-suite-shard=K/4` (`tests/default_lane.py` packs whole files by collected count; shard 1 adds the unsharded manifest witnesses). Two rules preserve the split:
- Mark real-process and real-port tests, and tests that mutate process-global state without reliable fixture isolation, `@pytest.mark.serial` (or add the file to `_SERIAL_TEST_FILES` in `tests/conftest.py`): under `-n` such a test flakes on kill/reap or port-reclaim timing or crashes its worker, and with `--max-worker-restart=0` a dead worker fails its whole co-located batch as spurious failures in unrelated files.
- Keep every other test parallel-safe so it stays in the fast pass: `tmp_path` (never a fixed path), `monkeypatch.setenv`/`delenv`/`setattr` for environment and attribute changes, no execution-order assumptions. A module-global mutation reliably snapshot-and-restored by a fixture may stay in the parallel pass (pattern: `tests/conftest.py::_isolate_workspace_executor_globals`).
- Mark `@pytest.mark.serial` (or list the file in `_SERIAL_TEST_FILES` in `tests/conftest.py`) only for a shared resource the test cannot isolate: a fixed port, a real daemon, a process-global its fixture cannot restore. An owned child, a private root or port 0 is not one. Under `-n` such a test flakes on kill/reap or port-reclaim timing or crashes its worker, and with `--max-worker-restart=0` a dead worker fails its co-located batch. A merely slow test is made faster or split, never `serial`: that pass has no per-test timeout.
- Keep every other test parallel-safe: `tmp_path` (never a fixed path), `monkeypatch.setenv`/`delenv`/`setattr`, no execution-order assumptions; a fixture-restored global stays parallel (`tests/conftest.py::_isolate_workspace_executor_globals`).
### The local battery entry point
`python scripts/run_tests.py` (`make test`) runs Node (missing = red `NOT_RUN`), then all default tests, even after a pass; extra pytest args select focused work. It imports the gate's lane exclusions/parallel flags. Default xdist pins serial files to `--serial-shards` groups (`_pin_lane_groups`) under the parallel timeout; `--sequential` restores CI/gate's authoritative split. Recheck overlap-only failures sequentially. `tests/test_run_tests_script.py` enforces this; local feedback mints no hermetic proof.
`python scripts/run_tests.py` (`make test`) always runs the full Node and default Python battery; pytest arguments select focused work. `--selected [--base REF]` uses the commit selector (default base `HEAD`), prints tier/rule/module count and runs both marker passes with paths. `--extra-test PATH` or `--full-tests` widens it with `--test-reason`. Missing required Node is red `NOT_RUN`. No hermetic proof is minted.
`--evidence-dir DIR` passes explicit pytest `--ci-evidence-dir` paths under one absolute root: `overlapped`, `parallel`/`serial`, or `focused`; Node is unchanged. Never export the path in `PYTEST_ADDOPTS`: nested pytest could overwrite the parent's `events.jsonl`/`results.json`. A child may name its own directory; sequential reuse after producer exit remains allowed.
Full default overlaps bounded `--serial-shards` groups; `--sequential` restores the gate split. Recheck overlap-only failures sequentially. `--evidence-dir DIR` gives each pass its own explicit `--ci-evidence-dir`; selected runs also write `selection.json` with scope and reconciled inventory. Never export evidence via `PYTEST_ADDOPTS`: nested pytest could overwrite its parent's results.
### Safe local verification and dirty browser candidates
@ -59,17 +62,19 @@ OUROBOROS_RUN_UI_SMOKE=1 OUROBOROS_EXPECT_BROWSER_ENGINES=chromium,webkit \
`tests/ci_evidence.py` and `.github/actions/test-evidence` report Actions producer outcomes separately from testcase counts: passing cases never override a nonzero exit, skips and absent credentials remain explicit, and raw JUnit is never uploaded. Diagnostic failures report `diagnostics_incomplete` without changing release eligibility. Shard proof is authoritative: `reconcile-shards` rejects missing/unreadable/red projections, wrong assignments, duplicate or unexecuted nodes, and nonzero sessions, using each producer's highest attempt. UI uses four `ui-shard` jobs plus `ui-manifest`; Windows default passes use independent manifests on shard 1 and `full-test` reconciliation. Proof export/upload failures also fail the producing attempt, preventing fallback to old evidence. Step ceilings fail without cancelling the job, leaving evidence steps runnable; dead xdist workers report phase `crash`. UI shards additionally stop cooperatively between tests via `--session-timeout`. The `viewport`/`inflight` dispatches in `ui-browser-push.yml` are partial diagnostics, never full proof.
### The commit gate mirrors the CI split
### The tiered commit and CI gates
`preflight_runner.run_hermetic_pytest` mirrors CI in one disposable checkout and scrubbed root: Node (`cd web && node --test tests/*.test.js`), then parallel `not serial` and flag-free `serial`. No web suite means no Node prerequisite; a present suite never silently disappears for missing Node. `LANE_EXCLUSION_EXPR`/`PARALLEL_PASS_FLAGS` are the SSOT pinned against both CI jobs. Candidate capture/apply uses one hardened worktree-vs-`HEAD` binary diff; failure is `PREFLIGHT_CANDIDATE_ASSEMBLY`, never a test failure (binding: ARCHITECTURE §6 "Hermetic preflight proof"). The gate's dependency-free `web/tests/no_undef.test.js` acorn walker and CI's extra ESLint `no-undef` (`web/eslint.config.js`, `npm ci`) remain independent checks.
Ordinary `commit_reviewed` runs the floor in `test_selection.FLOOR_TESTS`, changed test modules, direct AST importers of changed product modules, and authored widening. Each floor entry names its invariant; missing or empty modules block. Selection reads the assembled candidate, dirty/untracked files included, against `HEAD` before commit and `HEAD~1` afterwards. PR CI on every OS uses the same selector against the PR base; on a full escalation Windows runs the floor and leaves the rest to its landed shards.
`preflight_node.node_environment` projects its bundled-first Node into child PATH for the gate and `scripts/run_tests.py`, before pytest collection, retaining other entries. Arbitrary bare pytest/`safe_test` commands and explicit test-owned child environments keep their own policy. The gate fingerprints the effective PATH. Contributor rules:
Imports are a minimum: `extra_tests` covers indirect/dynamic consumers, prompts, skills and cross-language effects; `full_tests` covers unresolved impact. Both require `test_reason`, retained in the commit record. Verifier changes force full: runner/selector, floor, shared test helpers or data, pytest configuration, and dependency locks beyond a release bump. Product fan-out alone never triggers a threshold. Records name tier, candidate/base/policy and reasons, with execution counts; missing or duplicate execution blocks.
- The candidate cannot weaken the pass: `PYTEST_*`, `NODE_OPTIONS` and owner runtime state (`OUROBOROS_*`, secret-suffixed keys and every settings key `config.apply_settings_to_env` projects) are scrubbed, so the verdict cannot depend on the operator's install profile; required plugins are probed outside candidate control; post-commit checks also inspect `HEAD~1`, so suite deletion cannot hide after the commit exists; exit status owns the verdict, rendered diagnostics do not. `OUROBOROS_PREFLIGHT_SERIAL=1` is the explicit temporary rollback lever, never a silent fallback.
- A red post-commit gate is warning-only for an ordinary commit (the local commit is preserved for forensics); evolution publication refuses to auto-push while the warning stands; inside a managed update the gate blocks boot promotion and routes through rollback, and an incomplete rollback leaves `gate_blocked` so boot retries recovery instead of promoting the rejected merge.
- The managed mandate is "the full suite provably ran green on the exact committed tree", not "run it twice": reuse is the runner's process-held proof or, once a process loaded the merge, its unsigned receipt of a complete default run — never `tests_evidence` or the event log. A new commit needs a new run; a skip, no applicable suite or a mocked `None` mints neither.
- Process containment is unconditional, after a green pass too: Windows uses a kill-on-close Job Object; POSIX uses an environment membership token plus a process-group backstop, and promises honest detection with a fail-closed verdict for attributed members, not guaranteed teardown of an arbitrary detached process (`tests/test_preflight_process_containment.py`). A crashed worker, a timeout-killed worker, a missing plugin, containment failure and ordinary test failure keep distinct diagnostics.
- Mark process, port and global-state tests `serial`; make a merely slow test faster or split it, because the gate's and CI's serial pass has no per-test timeout and a serial test consumes the remaining total gate budget.
Full qualification remains mandatory for managed updates, evolution publication and exact-commit adoption (existing full evidence only), and CI on `ouroboros` merges, stable pushes, tags and dispatch. A selected proof never mints `FullGateReceipt` or satisfies these consumers. Full receipts may reuse a complete run of the same HEAD/tree/index/workload and installation; a new HEAD needs a new run. Skip/no-suite `None` grants no proof. Ordinary post-commit failures preserve the local commit with a warning; evolution refuses publication, and managed updates roll back or retain `gate_blocked` until recovery.
`preflight_runner.run_hermetic_pytest` runs the entire Node lane, parallel `not serial` and flag-free `serial` in one disposable checkout. `LANE_EXCLUSION_EXPR`/`PARALLEL_PASS_FLAGS` own exclusions and flags. Missing required Node/plugins blocks. `preflight_node.node_environment` puts bundled-first Node on the gate's and CLI's child PATH, which is fingerprinted; other pytest and child environments keep their policy. The `web/tests/no_undef.test.js` acorn walker and CI's lockfile-frozen ESLint (`web/eslint.config.js`, `npm ci`) remain independent checks.
Candidate capture uses one hardened worktree-vs-`HEAD` binary diff; assembly failure is `PREFLIGHT_CANDIDATE_ASSEMBLY`. It scrubs `PYTEST_*`, `NODE_OPTIONS`, settings and credentials; plugin probes run outside candidate control. Post-commit baselines include `HEAD~1` to catch suite deletion; exit status owns the verdict. `OUROBOROS_PREFLIGHT_SERIAL=1` is an explicit rollback lever, never silent fallback.
Containment is checked even after green (§9 "Test runs"); unknown or surviving members block and retain the tree. Worker crash, timeout, missing plugin, containment and test failures keep distinct diagnoses. Proof binding: ARCHITECTURE §6.
### GitHub Actions: secrets in step-level `if:` conditions

View file

@ -127,11 +127,11 @@ def authorize(ctx: Any, commit: str, *, reason: str) -> Dict[str, Any]:
mode = effective_runtime_mode(get_runtime_mode(), getattr(ctx, "task_metadata", None))
return authorize_row(canonical_data_root(ctx), row, commit, reason=reason, runtime_mode=mode,
task_id=str(getattr(ctx, "task_id", "") or ""))
task_id=str(getattr(ctx, "task_id", "") or ""), ctx=ctx)
def authorize_row(data_dir: pathlib.Path, row: Dict[str, Any], commit: str, *, reason: str,
runtime_mode: str = "advanced", task_id: str = "") -> Dict[str, Any]:
runtime_mode: str = "advanced", task_id: str = "", ctx: Any = None) -> Dict[str, Any]:
"""The checks and the durable handoff; shared by the tool and the evolution supervisor path."""
from ouroboros import body_candidate
@ -192,6 +192,13 @@ def authorize_row(data_dir: pathlib.Path, row: Dict[str, Any], commit: str, *, r
if current.get("phase") in _PENDING and current.get("phase") != "authorized":
raise AdoptionRefused(
"ADOPTION_PENDING", f"adoption {current.get('id')} is {current.get('phase')}; it must settle first.")
from ouroboros.preflight_qualification import adoption_test_evidence
# Existing full green evidence of exactly this commit; adoption never runs the battery itself.
try:
qualification = adoption_test_evidence(data_dir, candidate, cand, ctx=ctx)
except (ValueError, OSError, subprocess.SubprocessError) as exc:
raise AdoptionRefused("ADOPTION_TESTS_REQUIRED", str(exc)) from exc
directory = helper_dir(data_dir)
if directory.exists():
_archive(data_dir, current, f"replaced:{current.get('phase')}" if current else "stale_helper")
@ -203,6 +210,7 @@ def authorize_row(data_dir: pathlib.Path, row: Dict[str, Any], commit: str, *, r
"candidate_path": str(candidate), "task_id": task_id, "reason": str(reason or ""),
"switch": rows, "deps_command": _dependency_command(serving, rows), "authorized_at": utc_now_iso(),
"unreviewed_commits": unreviewed, "attempts": 0, "events": [],
"tests": qualification,
}
# The helper is captured from the body that is running, never from the candidate.
shutil.copyfile(pathlib.Path(body_switch.__file__), directory / "switch.py")

View file

@ -354,6 +354,19 @@ class PreflightTestProof(NamedTuple):
index_tree: str
workload: tuple
head: str
selection: object = None
@property
def tier(self) -> str:
if self.selection is not None:
return self.selection.tier
from ouroboros.preflight_runner import DEFAULT_PYTEST_ARGS
specs = self.workload[0]
complete = ([spec[0] for spec in specs] == ["parallel", "serial"]
and all(spec[1][0] == "tests/" for spec in specs))
legacy = len(specs) == 1 and tuple(specs[0][1]) == tuple(DEFAULT_PYTEST_ARGS)
return "full" if complete or legacy else "selected"
def covers(self, candidate: PreflightTestProof | None) -> bool:
return candidate is not None and self == candidate
@ -390,6 +403,7 @@ def log_preflight_test_proof(ctx, proof: PreflightTestProof, *, reused: bool, ph
from a receipt keeps that receipt as its provenance; any other row lends nothing.
"""
from ouroboros.preflight_receipts import canonical_data_root
from ouroboros.preflight_inventory import scope_record
fingerprint = workload_fingerprint(proof.workload)
prior = getattr(ctx, "_preflight_test_disclosure", None)
@ -405,6 +419,7 @@ def log_preflight_test_proof(ctx, proof: PreflightTestProof, *, reused: bool, ph
"pass_seconds": dict(passes or ()), "budget_sec": proof.workload[1],
"head": proof.head, "tree": proof.tree, "index_tree": proof.index_tree,
"workload_fingerprint": fingerprint,
**scope_record(proof, getattr(ctx, "_preflight_test_inventory", None)),
**({"receipt": receipt} if receipt is not None else {}),
**({"empty_lanes": list(empty_lanes)} if isinstance(empty_lanes, list) else {}),
}
@ -421,7 +436,7 @@ def log_preflight_test_proof(ctx, proof: PreflightTestProof, *, reused: bool, ph
def preflight_test_workload(
repo, *, timeout=None, pytest_args=None, passes=None, agent_python=None, probe_module="",
repo, *, timeout=None, pytest_args=None, passes=None, agent_python=None, probe_module="", selection=None,
) -> tuple:
"""Effective runner inputs, with generated paths and probe names normalized."""
import sys
@ -429,19 +444,25 @@ def preflight_test_workload(
from ouroboros.preflight_node import candidate_node_tests, resolve_node
from ouroboros.process_containment import CONTAINMENT_ENV_PREFIX
from ouroboros.test_environment import default_temp_parent
from ouroboros.preflight_inventory import MODULE, selection_passes
from ouroboros.test_parallelism import requested_workers
base = default_temp_parent() / "ouroboros-preflight-contract"
env = pr._preflight_env(base, base / "repo", create=False)
# The workload names the requested worker count. The count free memory admits
# is an execution fact frozen in the run's own probe, so a memory sample never
# changes what a proof covers, and naming the workload reads no memory at all.
env = pr._preflight_env(base, base / "repo", create=False, workers=requested_workers(os.cpu_count() or 1))
# Containment membership stays in the child env (an outer reap must see the
# tree), but its keys are per-launch nonces, not test inputs.
environment = hashlib.sha256(json.dumps({key: value for key, value in env.items()
if not key.startswith(CONTAINMENT_ENV_PREFIX)},
sort_keys=True).encode()).hexdigest()
python = agent_python or os.environ.get("OUROBOROS_AGENT_PYTHON") or sys.executable or "python3"
specs = pr._preflight_pass_specs(pytest_args) if passes is None else passes
specs = passes if passes is not None else (selection_passes(selection) if selection else pr._preflight_pass_specs(pytest_args))
node_tests = tuple(candidate_node_tests(repo))
return (
tuple((p.label, tuple(pr._WORKER_PROBE_MODULE if probe_module and arg == probe_module else arg
tuple((p.label, tuple(pr._WORKER_PROBE_MODULE if probe_module and arg == probe_module else
MODULE if arg.startswith(MODULE + "_") else arg
for arg in p.args), p.parallel) for p in specs),
pr._resolve_preflight_timeout(pr._DEFAULT_PREFLIGHT_TIMEOUT_SEC if timeout is None else timeout),
environment, _executable_identity(python),
@ -451,7 +472,7 @@ def preflight_test_workload(
def capture_preflight_test_subject(
repo, *, timeout=None, pytest_args=None, passes=None, agent_python=None, probe_module="",
repo, *, timeout=None, pytest_args=None, passes=None, agent_python=None, probe_module="", selection=None,
) -> PreflightTestProof | None:
"""Describe the actual checkout before execution, or decline reuse.
@ -475,26 +496,43 @@ def capture_preflight_test_subject(
head = head.stdout.strip()
workload = preflight_test_workload(
repo, timeout=timeout, pytest_args=pytest_args, passes=passes,
agent_python=agent_python, probe_module=probe_module,
agent_python=agent_python, probe_module=probe_module, selection=selection,
)
return PreflightTestProof(tree, index_tree, workload, head)
return PreflightTestProof(tree, index_tree, workload, head, selection)
except (OSError, RuntimeError, subprocess.SubprocessError, TypeError, ValueError):
log.debug("test workload could not be bound; no proof reuse", exc_info=True)
return None
def preflight_test_proof_matches(ctx, repo) -> bool:
def preflight_test_proof_matches(ctx, repo, *, require_full=True) -> bool:
proof = getattr(ctx, "_preflight_test_proof", None)
return isinstance(proof, PreflightTestProof) and proof.covers(capture_preflight_test_subject(repo))
if not isinstance(proof, PreflightTestProof) or (require_full and proof.tier != "full"):
return False
from ouroboros.test_selection import refresh_selection
selection = refresh_selection(pathlib.Path(repo), proof.selection) if proof.selection else None
return proof.covers(capture_preflight_test_subject(repo, selection=selection))
def preflight_test_workload_unchanged(proof, repo, *, timeout, pytest_args) -> bool:
try:
return proof.workload == preflight_test_workload(repo, timeout=timeout, pytest_args=pytest_args)
return (pytest_args is not None or proof.tier == "full") and proof.workload == preflight_test_workload(
repo, timeout=timeout, pytest_args=pytest_args)
except (OSError, RuntimeError, subprocess.SubprocessError, TypeError, ValueError):
return False # inability to bind reuse is not a failed test
def selected_workload_unchanged(proof, repo, *, timeout, selection) -> bool:
"""Selected proof reuse is separate from the full-only managed workload check."""
from ouroboros.test_selection import refresh_selection
try:
return selection == refresh_selection(pathlib.Path(repo), selection) and proof.workload == preflight_test_workload(
repo, timeout=timeout, selection=selection)
except (OSError, RuntimeError, subprocess.SubprocessError, TypeError, ValueError):
return False
def run_tests_preflight_with_proof(ctx: ToolContext, *, runner) -> Optional[str]:
"""Run the caller's seam; only the hermetic runner can attest execution.

View file

@ -1,11 +1,6 @@
"""
Ouroboros — Shared configuration (single source of truth).
Paths, the settings-file lifecycle (locked load/normalize/save plus environment
projection) and mode-aware write authority. The vocabularies it reads through —
shipped defaults, closed scales, model slots, reviewer routes, numeric limits —
live in sibling leaves and are re-exported here, so ``ouroboros.config`` remains
the one import surface for settings knowledge.
"""Shared configuration: paths, locked settings lifecycle and mode-aware writes.
Defaults, scales, model slots, routes and limits live in sibling leaves and are
re-exported here: one import surface for settings knowledge.
"""
from __future__ import annotations
@ -137,6 +132,12 @@ from ouroboros.runtime_limits import (
)
from ouroboros.update_channels import UPDATE_SETTINGS_DEFAULTS, normalize_update_channel # noqa: F401
# Physical safety budgets, not test-value judgments or kernel-enforced quotas.
TEST_WORKER_PEAK_RSS_BYTES = 1024**3 # Provisional 1 GiB; loaded P0 observed 690 MiB/worker.
TEST_MEMORY_RESERVE_FRACTION = 0.10 # Keep physical headroom for the OS and other work.
TEST_PROCESS_SAMPLE_SEC = 1.0 # Poll latency; allocations between samples can exceed the bound.
TEST_PROCESS_TOKEN_SCAN_SEC = 5.0 # Detached members are found by token this often; the scan reads every environment.
def resolve_task_execution_timeout_sec(task: dict, *, global_ceiling=_CURRENT_TASK_CEILING) -> Optional[float]:
"""The leaf's bound arithmetic with this facade's (patchable) configured ceiling."""

View file

@ -758,16 +758,17 @@ def _file_artifact(kind: str, path: pathlib.Path, **facts: Any) -> Dict[str, Any
"size": path.stat().st_size if path.exists() else 0, **facts}
def finalize_task_artifacts(parent_drive_root: pathlib.Path, task: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Write patch/memory-export artifacts for a completed headless task under the task's custody lock, the
one lock every canonical-store publisher takes (copy-back, ref retry, settlement, mailbox cleanup), so no
two publishers place or list one task's files at once; a held lock is ``CustodyBusy`` (nothing failed)."""
def finalize_task_artifacts(parent_drive_root: pathlib.Path, task: Dict[str, Any],
*, lock_timeout_sec: Optional[float] = None) -> List[Dict[str, Any]]:
"""Publish under the custody lock shared by copy-back, ref retry, settlement and mailbox cleanup.
Publishers never place/list one task's files concurrently; an exhausted lock budget is ``CustodyBusy``."""
from ouroboros.task_custody import CustodyBusy, task_custody_lock
task_id = str(task.get("id") or "")
if not task_id:
return []
with task_custody_lock(parent_drive_root, task_id, timeout_sec=PUBLICATION_LOCK_SEC) as locked:
budget = PUBLICATION_LOCK_SEC if lock_timeout_sec is None else lock_timeout_sec
with task_custody_lock(parent_drive_root, task_id, timeout_sec=budget) as locked:
if not locked:
raise CustodyBusy(f"custody lock of {task_id} is held by another publisher")
return _finalize_task_artifacts_locked(parent_drive_root, task, task_id)

View file

@ -0,0 +1,151 @@
"""Host-written pytest inventory for selected gate attempts (never receipt authority)."""
from __future__ import annotations
from collections import Counter
import hashlib
import json
from pathlib import Path
import uuid
MODULE = "ouroboros_selection_inventory"
# Loaded explicitly by each controller; worker hooks forward their collection via
# xdist. No candidate module imports or persisted selection manifest are needed.
SOURCE = '''import json
from pathlib import Path
import pytest
_path = Path(__RESULT_PATH__)
_worker = False
def _emit(event, **fields):
if not _worker:
with _path.open("a", encoding="utf-8") as stream:
stream.write(json.dumps(dict(event=event, **fields)) + "\\n")
def pytest_configure(config):
global _worker
_worker = hasattr(config, "workerinput")
_emit("start")
def pytest_collection_finish(session):
if not _worker and session.items:
_emit("collection", ids=[item.nodeid for item in session.items])
@pytest.hookimpl(optionalhook=True)
def pytest_xdist_node_collection_finished(node, ids):
_emit("collection", ids=list(ids))
def pytest_runtest_logreport(report):
if report.when == "teardown" or report.skipped or report.failed:
_emit("report", nodeid=report.nodeid, when=report.when, outcome=report.outcome)
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_sessionfinish(session, exitstatus):
yield
_emit("finish", exitcode=int(session.exitstatus))
'''
def selection_passes(selection, *, probe_module="", temp_root=None):
from ouroboros import preflight_runner as pr
passes = []
for spec in pr._preflight_pass_specs(probe_module=probe_module):
module = MODULE
if temp_root is not None:
module += "_" + uuid.uuid4().hex
probe = pr._probe_dir(Path(temp_root))
probe.mkdir(parents=True, exist_ok=True)
result = Path(temp_root) / f"inventory-{spec.label}.jsonl"
result.write_text("", encoding="utf-8")
(probe / f"{module}.py").write_text(SOURCE.replace("__RESULT_PATH__", repr(str(result))), encoding="utf-8")
args = [arg for arg in spec.args if arg != "tests/"]
passes.append(pr._make_pass(spec.label, [*selection.targets, *args, "-p", module]))
return passes
def read_inventory(temp_root, label):
try:
rows = [json.loads(line) for line in
(Path(temp_root) / f"inventory-{label}.jsonl").read_text(encoding="utf-8").splitlines()]
collections = [row["ids"] for row in rows if row["event"] == "collection"]
reports = [row for row in rows if row["event"] == "report"]
result = {"expected": collections[0] if collections else [], "collections": collections,
"executed": [r["nodeid"] for r in reports if r["when"] == "teardown"],
"skipped": [r["nodeid"] for r in reports if r["outcome"] == "skipped"],
"failed": [r["nodeid"] for r in reports if r["outcome"] == "failed"],
"exitcode": rows[-1].get("exitcode") if rows else None}
expected, executed = result["expected"], result["executed"]
if not all(isinstance(value, list) and all(isinstance(item, str) for item in value)
for value in (expected, executed, result["skipped"], result["failed"])):
raise ValueError("invalid node inventory")
problem = ""
if any(collection != expected for collection in result["collections"]):
problem = "worker collections disagree"
elif len(set(expected)) != len(expected) or Counter(executed) != Counter(expected):
problem = "expected and executed test ids differ (missing or duplicate execution)"
elif result["exitcode"] not in (0, 5) or result["failed"]:
problem = "test session did not finish green"
result.pop("collections")
return result, problem
except (OSError, ValueError, KeyError, TypeError) as exc:
return {}, f"test inventory unavailable: {exc}"
_LISTED_IDS = 50 # bounded excerpt of ids worth reading; counts and the digest cover the rest
def compact_inventory(inventory):
"""Counts, a digest of the expected ids and only the ids worth reading.
The complete inventory stays with the run (and in the CLI's selection.json); a
durable record repeated in events, exhibits, ledgers and handoffs never carries it.
"""
compact = {}
for label, row in (inventory or {}).items():
expected, executed = row.get("expected", []), row.get("executed", [])
compact[label] = {
"expected": len(expected), "executed": len(executed), "skipped": len(row.get("skipped", [])),
"digest": hashlib.sha256("\n".join(sorted(expected)).encode("utf-8")).hexdigest(),
"failed": row.get("failed", [])[:_LISTED_IDS],
"missing": sorted((Counter(expected) - Counter(executed)).elements())[:_LISTED_IDS],
"unexpected": sorted((Counter(executed) - Counter(expected)).elements())[:_LISTED_IDS],
"exitcode": row.get("exitcode"),
}
return compact
def scope_record(proof, inventory=None):
selection = getattr(proof, "selection", None)
rule = "full default suite" if proof.tier == "full" else "custom pytest selection"
return {"tier": proof.tier, **(selection.record() if selection else {"rule": rule}),
"candidate": proof.tree, "head": proof.head, "index_tree": proof.index_tree,
**({"inventory": compact_inventory(inventory)} if inventory is not None else {})}
def inventory_problem(selection, inventory):
"""Every floor module must collect; opt-in-only affected modules may be excluded."""
from ouroboros.test_selection import FLOOR_TESTS
collected = {node.split("::", 1)[0] for row in inventory.values() for node in row.get("expected", [])}
missing = sorted(set(FLOOR_TESTS) - collected)
return "floor modules collected no tests: " + ", ".join(missing) if missing else ""
def record_failed_attempt(ctx, selection, subject, inventory):
"""Keep partial execution visible without granting it any proof authority."""
if ctx is None or selection is None or getattr(ctx, "_preflight_tests_passed", False):
return
from ouroboros.preflight_receipts import canonical_data_root
from ouroboros.utils import append_jsonl, utc_now_iso
event = {"type": "preflight_test_attempt", "ts": utc_now_iso(), "status": "failed",
**(scope_record(subject, inventory) if subject else selection.record())}
ctx._preflight_test_disclosure = event
try:
root = canonical_data_root(ctx)
if root:
append_jsonl(Path(root) / "logs/events.jsonl", event)
except Exception:
pass # Diagnostics cannot replace the actual failure.

View file

@ -32,10 +32,10 @@ from ouroboros.platform_layer import probe_node_version, resolve_bundled_node
NODE_MIN_VERSION = (20, 11)
# The web package boundary and its test glob. `web/package.json` runs
# `node --test tests/*.test.js` FROM web/, and both CI jobs mirror that exact
# step; the parity test derives the CI command from these two constants, so the
# gate, CI, and the package script cannot drift apart silently. The gate globs
# in Python (sorted, cross-platform) instead of relying on a shell.
# `node --test tests/*.test.js` FROM web/; the gate and both CI jobs (through
# `scripts/run_tests.py --node`) run `candidate_node_tests` over these two
# constants, so the gate, CI, and the package script cannot drift apart silently.
# The lane globs in Python (sorted, cross-platform) instead of relying on a shell.
WEB_DIR = "web"
TESTS_GLOB = "tests/*.test.js"
@ -165,7 +165,7 @@ def run_node_tests(
# reaped unconditionally, and a live-or-undeterminable leftover is a hard
# block whatever the exit code said. Run from the candidate's web/ with
# web-relative paths so imports resolve exactly as under `npm test`.
container = ProcessContainer()
container = ProcessContainer(lifeline=True, overload=True)
started = time.monotonic()
# NODE_OPTIONS admits --test-only/--test-*-pattern/--require: an inherited
# operator value can silently green a red suite — same class the pytest
@ -173,7 +173,7 @@ def run_node_tests(
env = pr._preflight_env(temp_root, worktree)
env.pop("NODE_OPTIONS", None)
proc = container.spawn(
[node, "--test", *files],
[node, "--test", f"--test-concurrency={env['PYTEST_XDIST_AUTO_NUM_WORKERS']}", *files],
cwd=str(pathlib.Path(worktree) / WEB_DIR),
env=env,
stdout=subprocess.PIPE,

View file

@ -0,0 +1,74 @@
"""Full exact-commit test evidence for adoption and evolution publication.
Neither consumer runs tests. The full battery runs where a long wait belongs (a
reviewed commit with ``full_tests``, evolution's forced post-commit run); these
consumers accept only its evidence, for exactly the commit in question.
"""
from __future__ import annotations
import json
from pathlib import Path
import subprocess
_HOW = ("a run that also carried uncommitted or untracked files does not count; commit it with full_tests=true "
"from a clean checkout so the post-commit run tests exactly this tree, then request the restart again")
def _git(repo, *args):
return subprocess.run(["git", *args], cwd=str(repo), capture_output=True, text=True, timeout=60)
def _commit_tree(repo, commit) -> str:
tree = _git(repo, "rev-parse", "--verify", f"{commit}^{{tree}}")
return "" if tree.returncode else tree.stdout.strip()
def full_proof_error(ctx, repo, commit="") -> str:
from ouroboros.commit_admission import preflight_test_proof_matches
proof = getattr(ctx, "_preflight_test_proof", None)
try:
# A named commit needs a run of exactly its tree: green uncommitted or untracked
# bytes on top of it are another body.
if (getattr(ctx, "_preflight_tests_passed", False) and proof is not None
and (not commit or (proof.head == commit and proof.tree == proof.index_tree == _commit_tree(repo, commit)))
and preflight_test_proof_matches(ctx, repo)):
return ""
except (OSError, ValueError, RuntimeError, subprocess.SubprocessError):
pass
return "a full green exact-tree test proof is required; selected, skipped or stale evidence cannot qualify"
def receipt_for_commit(data_dir, repo, commit) -> dict:
"""A coherent durable receipt of a complete full run of exactly ``commit``, or ``{}``."""
from ouroboros.preflight_receipts import RECEIPT_SCHEMA, RECEIPTS_DIR, _record_problem
tree = _commit_tree(repo, commit)
if not tree:
return {}
# The run must have carried the Node lane exactly when this commit has web tests.
node_ran = any(name.endswith(".test.js") for name in _git(repo, "ls-tree", "-r", "--name-only", commit, "--",
"web/tests").stdout.split())
for path in sorted((Path(data_dir) / "state" / RECEIPTS_DIR).glob("*.json")):
try:
document = json.loads(path.read_text(encoding="utf-8"))
identity = document["identity"]
if (identity["schema"] == RECEIPT_SCHEMA and identity["head"] == commit
and identity["tree"] == identity["index_tree"] == tree
and not _record_problem(document, identity, node_ran)):
return {"source": "receipt", "key": path.stem, "head": commit, "tree": tree,
"created_at": document["created_at"], "phase": document["phase"]}
except (OSError, ValueError, KeyError, TypeError):
continue
return {}
def adoption_test_evidence(data_dir, repo, commit, ctx=None) -> dict:
"""The task's own full proof of ``commit`` or a durable receipt of it; never a new run."""
if ctx is not None and not full_proof_error(ctx, repo, commit):
proof = ctx._preflight_test_proof
return {"tier": "full", "source": "proof", "head": proof.head, "tree": proof.tree}
receipt = receipt_for_commit(data_dir, repo, commit)
if receipt:
return {"tier": "full", **receipt}
raise ValueError(f"no full green test run of {str(commit)[:12]} is on record; {_HOW}")

View file

@ -7,9 +7,10 @@ when everything matches, shown to reviewers, unsigned. This module keeps ONE
small JSON per exact identity under the caller's canonical data root. Only
``preflight_runner.run_hermetic_pytest`` writes it, after a complete default
run (the node lane when the candidate carries web tests, the parallel
``not serial`` pass green on at least two observed xdist workers, the flag-free
serial pass, a proven containment reap), and only that runner reads it, at its
existing reuse seam after the baseline and assembly checks.
``not serial`` pass green on two observed xdist workers or the one admitted, the
flag-free serial pass, a proven containment reap). That runner reads it at its
existing reuse seam after the baseline and assembly checks; adoption reads it for
exactly its commit (``preflight_qualification.receipt_for_commit``).
The identity is the process-held proof (tested HEAD, tree, index and effective
workload), the installation the test interpreter loads (probed under the exact
@ -219,8 +220,8 @@ def _record_problem(document: Any, identity: dict, node_ran: bool) -> str:
Proportional coherence, not a forgery defence (the file is unsigned): the lanes must
be the ones THIS identity's recipe requires — the node lane exactly when the candidate
carries web tests, a non-empty parallel lane on at least two observed workers, the
serial lane — and the record must keep the provenance of the run that wrote it.
carries web tests, a non-empty parallel lane on two observed workers (or the one the
budget admitted), the serial lane — and the record must keep its run's provenance.
"""
if not isinstance(document, dict):
return "not a JSON object"
@ -231,8 +232,10 @@ def _record_problem(document: Any, identity: dict, node_ran: bool) -> str:
lanes, seconds = document.get("lanes"), document.get("pass_seconds")
if not isinstance(lanes, dict) or lanes.get("node") != ("passed" if node_ran else "absent"):
return "the node lane does not match the recipe this candidate requires"
workers = lanes.get("parallel_workers")
if type(workers) is not int or workers < 2 or lanes.get("serial") not in ("passed", "empty"):
# The live proof's rule: two observed workers, or the one the memory budget admitted.
workers, admitted = lanes.get("parallel_workers"), lanes.get("admitted_workers", 2)
if (type(workers) is not int or type(admitted) is not int or admitted < 1
or workers < min(2, admitted) or lanes.get("serial") not in ("passed", "empty")):
return "the parallel/serial lane evidence is incomplete"
if (not isinstance(seconds, dict) or set(seconds) != {"parallel", "serial"}
or any(type(value) not in (int, float) or value < 0 for value in seconds.values())):
@ -270,6 +273,8 @@ class FullGateReceipt:
return cls(None, {}, "no tested subject (no caller ctx, or the checkout could not be bound)")
if [spec.label for spec in passes] != ["parallel", "serial"] or probe_module not in passes[0].args:
return cls(None, {}, "not the complete default recipe")
if subject.tier != "full":
return cls(None, {}, "selected evidence cannot mint a FullGateReceipt")
root = canonical_data_root(ctx)
if root is None:
return cls(None, {}, "no canonical data root")
@ -317,9 +322,9 @@ class FullGateReceipt:
"""Write the receipt after a complete green run whose identity held throughout.
Called only once every lane is green and containment proven. A source or
installation that moved during the run, an empty parallel lane or fewer than
two observed workers decline; a write failure is logged. Neither changes the
verdict the runner returns.
installation that moved during the run, an empty parallel lane or fewer
observed workers than the gate requires (two, or the one admitted) decline; a
write failure is logged. Neither changes the verdict the runner returns.
"""
from ouroboros import preflight_runner as pr
from ouroboros.utils import atomic_write_json, utc_now_iso
@ -327,9 +332,9 @@ class FullGateReceipt:
if self.path is None:
return
try:
workers = len(pr._observed_worker_ids(temp_root))
workers, admitted = len(pr._observed_worker_ids(temp_root)), pr._allocated_worker_count(temp_root)
reason = ("the parallel pass collected no tests" if "parallel" in empty_passes else
f"{workers} observed xdist worker(s)" if workers < pr._MIN_PREFLIGHT_WORKERS else "")
f"{workers} observed xdist worker(s)" if workers < min(pr._MIN_PREFLIGHT_WORKERS, admitted) else "")
if not reason:
reason = _source_status()[1]
if not reason:
@ -344,7 +349,7 @@ class FullGateReceipt:
"schema": RECEIPT_SCHEMA, "identity": self.identity, "result": "passed",
"authenticity": "unsigned",
"lanes": {"node": "passed" if self.node_ran else "absent", "parallel_workers": workers,
"serial": "empty" if "serial" in empty_passes else "passed"},
"admitted_workers": admitted, "serial": "empty" if "serial" in empty_passes else "passed"},
"pass_seconds": dict(timings), "phase": phase,
"task_id": str(getattr(ctx, "task_id", "") or ""), "created_at": utc_now_iso(),
})
@ -392,7 +397,7 @@ def execution_exhibit(ctx: Any, *, otherwise: str = "") -> dict:
"the run completed but minted no proof of the tested subject" if completed
else "no green hermetic run (disabled or no applicable suite)")}
try:
bound = preflight_test_proof_matches(ctx, ctx.repo_dir)
bound = preflight_test_proof_matches(ctx, ctx.repo_dir, require_full=False)
except Exception:
bound = False
if not bound:
@ -406,7 +411,9 @@ def execution_exhibit(ctx: Any, *, otherwise: str = "") -> dict:
recipe = (["node"] if proof.workload[4] else []) + [str(spec[0]) for spec in proof.workload[0]]
empty = event.get("empty_lanes")
empty = [lane for lane in recipe if lane in empty] if isinstance(empty, list) else None
return {"status": "passed", "authenticity": "unsigned", "evidence": evidence,
return {"status": "passed", "authenticity": "unsigned", "evidence": evidence, "tier": proof.tier,
**{key: event[key] for key in ("base", "policy", "rule", "files", "reasons", "extra_tests",
"full_tests", "test_reason", "expected_files", "omitted_files", "inventory") if key in event},
"lanes": [lane for lane in recipe if lane not in empty] if empty is not None else [],
**({"empty_lanes": empty} if empty else {}),
"tested": {"head": proof.head, "tree": proof.tree, "index_tree": proof.index_tree},
@ -447,8 +454,12 @@ def execution_section(exhibit: Any) -> str:
else "; which of its lanes ran tests is not disclosed")
if empty:
observed += f"; lanes {', '.join(empty)} collected no tests, so no test ran there"
scope = ""
if exhibit.get("tier") == "selected" and isinstance(exhibit.get("files"), list):
scope = (f" over {len(exhibit['files'])} selected test modules ({exhibit.get('omitted_files', 0)} "
f"not run; {exhibit.get('rule', 'floor, direct importers and widening')})")
return ("## Preflight test execution (host fact)\n"
f"The hermetic test gate passed{observed}.\n"
f"The {exhibit.get('tier', 'full')} hermetic test gate passed{scope}{observed}.\n"
f"Tested checkout: HEAD {tested.get('head')}, tree {tested.get('tree')}, index "
f"{tested.get('index_tree')}. Evidence: {how}.\n"
"Unsigned host fact, not a verdict: decide yourself which further checks this change needs.")

View file

@ -471,37 +471,30 @@ def _copy_untracked(repo_dir: pathlib.Path, worktree: pathlib.Path) -> None:
shutil.copy2(src, dst)
# The parallel lane is only a parallel lane if xdist really starts more than one
# worker. `-n auto` resolves through PYTEST_XDIST_AUTO_NUM_WORKERS, which the
# operator environment can carry: an inherited "1" runs the whole pass on a
# single worker while the argv still says `-n`, so `PreflightPass.parallel` stays
# True, the pass returns green, and the gate has proved nothing about the
# parallel-only defects it exists to catch. That value is therefore never
# inherited — `_preflight_env` scrubs the whole PYTEST_* namespace and re-injects
# the count resolved here, which is clamped so it can never fall below two.
# Require real concurrency when the admitted budget permits it. Physical room
# or an explicit parent allocation may admit one worker; that is not proof of
# concurrency. The host freezes its admitted count beside the nonce probe, so
# a later memory sample cannot change the pass's expected evidence.
_MIN_PREFLIGHT_WORKERS = 2
# Operator lever for the parallel pass's xdist worker count, floored at two and
# sized per concurrent build lane by the rule in docs/DEVELOPMENT.md. It also
# keeps the nested fixture repos in tests/test_preflight_runner.py (1-3 probe
# tests each) from spending minutes on a full `-n auto` fan-out. Read from the
# OPERATOR environment, never forwarded to the candidate: the scrub drops it.
_PREFLIGHT_WORKERS_ENV = "OUROBOROS_PREFLIGHT_TEST_WORKERS"
def _preflight_worker_count() -> int:
"""How many xdist workers the parallel pass must start — always >= 2."""
raw = os.environ.get(_PREFLIGHT_WORKERS_ENV, "")
if raw.strip():
try:
return max(_MIN_PREFLIGHT_WORKERS, int(raw.strip()))
except ValueError:
pass
return max(_MIN_PREFLIGHT_WORKERS, os.cpu_count() or _MIN_PREFLIGHT_WORKERS)
"""CPU-based default, capped by physical room and a parent test allocation."""
from ouroboros.test_parallelism import worker_budget
return worker_budget(os.cpu_count() or 1)
def _allocated_worker_count(temp_root: pathlib.Path) -> int:
try:
return int((_probe_dir(temp_root) / "admitted-workers").read_text(encoding="utf-8"))
except FileNotFoundError:
return _preflight_worker_count()
# Forcing the flags and the plugins still only proves what the gate ASKED for.
# The pass is accepted as parallel only if xdist really started >= 2 workers, and
# that is observed from inside the run: a tiny plugin the gate writes into its own
# The pass must actually start min(2, admitted budget) workers, observed inside
# the run: a tiny plugin the gate writes into its own
# disposable temp root (never into the candidate worktree) records one file per
# worker id. Reading it back is the difference between "the argv said -n" and
# "concurrency actually happened".
@ -559,6 +552,7 @@ def _install_worker_probe(temp_root: pathlib.Path) -> str:
module = f"{_WORKER_PROBE_MODULE}_{uuid.uuid4().hex}"
probe = _probe_dir(temp_root)
probe.mkdir(parents=True, exist_ok=True)
(probe / "admitted-workers").write_text(str(_preflight_worker_count()), encoding="utf-8")
(probe / f"{module}.py").write_text(
_WORKER_PROBE_SOURCE.replace("__WORKER_LOG__", repr(str(_worker_log_dir(temp_root)))),
encoding="utf-8",
@ -574,9 +568,11 @@ def _observed_worker_ids(temp_root: pathlib.Path) -> set:
return set()
def _preflight_env(temp_root: pathlib.Path, repo_worktree: pathlib.Path, *, create=True) -> dict:
def _preflight_env(temp_root: pathlib.Path, repo_worktree: pathlib.Path, *, create=True,
workers: Optional[int] = None) -> dict:
from ouroboros.test_environment import isolated_environment, without_launch_descriptors
from ouroboros.preflight_node import node_environment
from ouroboros.test_parallelism import NESTED_WORKERS_ENV
temp_root = pathlib.Path(temp_root).resolve(strict=False)
# The scrub drops settings keys (`test_environment.settings_keys`), secret suffixes,
@ -590,7 +586,8 @@ def _preflight_env(temp_root: pathlib.Path, repo_worktree: pathlib.Path, *, crea
# Re-injected AFTER the scrub, so `-n auto` resolves to a count this process
# chose rather than one the operator environment happened to carry. Inert for
# the serial/legacy passes, which pass no `-n` at all.
env["PYTEST_XDIST_AUTO_NUM_WORKERS"] = str(_preflight_worker_count())
env["PYTEST_XDIST_AUTO_NUM_WORKERS"] = str(_allocated_worker_count(temp_root) if workers is None else workers)
env[NESTED_WORKERS_ENV] = env["PYTEST_XDIST_AUTO_NUM_WORKERS"]
return env
@ -821,7 +818,7 @@ def _execute_pytest_pass(
"""
from ouroboros.process_containment import ProcessContainer
container = ProcessContainer()
container = ProcessContainer(lifeline=True, overload=True)
# `container.spawn`, not `Popen` + `adopt`: it applies the process-group/session
# kwargs AND leaves no window between the process existing and being contained.
# On Windows job membership only takes effect at assignment, so a descendant
@ -1199,46 +1196,30 @@ def run_hermetic_pytest(
max_output: int = 8000,
phase: str = "post_commit",
ctx=None,
selected: bool = False, base: str | None = None, extra_tests=(), full_tests=False, test_reason="",
) -> Optional[str]:
"""Run pytest against the candidate diff in a disposable worktree.
"""Run Node and both pytest lanes in one disposable candidate checkout.
Mirrors CI: the web node lane (``preflight_node``, on candidates carrying
``web/tests/*.test.js``), then a parallel ``not serial`` pass, then a
``serial`` pass — one worktree/env, ONE shared total budget. Fails fast on
the first red lane, so the output never truncates the failing section away.
A full run is the default; reviewed ordinary commits request ``selected``.
Selection uses the assembled candidate, HEAD before commit or HEAD~1 after,
plus authored widening. Only complete full runs may mint durable receipts.
Skip/no suite returns None without proof; failures return bounded diagnostics.
OUROBOROS_PREFLIGHT_TIMEOUT_SEC owns the shared total lane budget.
Returns ``None`` on success or when no suite applies, otherwise an error.
An optional caller ctx retains proof only of completed green lanes and
containment; a matching workload reuses it, or another process's unsigned
receipt of a complete default run (``preflight_receipts``), after the
baseline/assembly checks; only a complete default run mints that receipt.
``OUROBOROS_PREFLIGHT_TIMEOUT_SEC`` overrides ``timeout`` for all callers.
``phase`` selects the deleted-suite baseline (see ``_TESTS_BASELINE_REFS``):
the pre-commit review passes ``PRE_COMMIT_PHASE`` so it compares against HEAD
only, while the default post-commit verification also consults HEAD~1,
because by then the deletion it is looking for is already in HEAD.
The candidate is assembled as ONE hardened ``git diff --binary … HEAD``
capture (external drivers, textconv, colour and operator prefix configs
pinned off; payload kept as raw bytes end to end) applied to a clean
worktree at HEAD, plus a copy of the untracked files: an exact tracked
projection of the live worktree plus its safe non-ignored untracked
entries — for every source-index state, including a merge in progress,
whose unmerged entries the former staged+unstaged diff pair rendered as
stubs and ``--cc`` hunks that ``git apply`` dropped or rejected. The
untracked side keeps ``_copy_untracked``'s long-standing boundaries:
ignored files are absent, and untracked symlinks are dereferenced to
regular files (non-file entries skipped), so the candidate is not
literally byte-equal to the worktree in those corners. When the source index
is merged, its exact ``write-tree`` snapshot is also installed into the
disposable index (without updating files) and verified before tests run.
A genuinely unmerged source index retains the live-resolution projection.
Assembly captures one hardened, raw-byte worktree-vs-HEAD diff (including
live merge resolutions), applies it with LF preserved, and copies non-ignored
untracked files. Untracked symlinks are dereferenced, non-files skipped.
A merged source index is installed and verified without changing files; an
unmerged index retains the live-resolution projection. The phase also selects
the deleted-suite baseline: HEAD before commit, HEAD plus HEAD~1 afterwards.
Unconfirmed teardown retains the checkout and its Git registration.
"""
previous_proof = getattr(ctx, "_preflight_test_proof", None)
previous_inventory = getattr(ctx, "_preflight_test_inventory", None)
if ctx is not None:
ctx._preflight_test_proof = None
ctx._preflight_tests_passed = False
ctx._preflight_test_inventory = None
timeout = _resolve_preflight_timeout(timeout)
# Checked BEFORE anything runs. `_diagnosis` renders inside this budget, so a
# non-positive one produced an empty string for a real failure — which the
@ -1290,11 +1271,9 @@ def run_hermetic_pytest(
worktree = temp_root / "repo"
worktree_added = False
unproven_teardown = ""
selection, subject, inventory = None, None, {}
try:
# The probe is written BEFORE the specs are built, and the nonce name it
# returns is what the parallel pass loads. Building the specs first and
# discarding the name left `-p` pointing at a stem no file carries, which
# is a pytest usage error, not a run.
# Install before compiling specs: -p must name the actual nonce module.
probe_module = _install_worker_probe(temp_root)
passes = _preflight_pass_specs(pytest_args, probe_module=probe_module)
# Before ANY candidate code is on disk, let alone imported.
@ -1308,12 +1287,7 @@ def run_hermetic_pytest(
"\n".join(problems), max_output,
)
# `-c core.autocrlf=false -c core.eol=lf`: check HEAD out byte-for-byte so
# the hermetic worktree matches the bytes the `--binary` candidate diff was
# captured against. Without it a Windows runner (`core.autocrlf=true`)
# materializes HEAD with CRLF, and the LF candidate then applies onto a
# CRLF base — the capture is byte-faithful but the checkout it lands on is
# not. Paired with the same override on `_apply_diff`.
# Preserve HEAD's LF bytes on Windows too, paired with _apply_diff.
add = _run_git(
repo,
["-c", "core.autocrlf=false", "-c", "core.eol=lf",
@ -1359,18 +1333,29 @@ def run_hermetic_pytest(
)
from ouroboros.commit_admission import (
PreflightTestProof, capture_preflight_test_subject, log_preflight_test_proof,
preflight_test_workload_unchanged,
preflight_test_workload_unchanged, selected_workload_unchanged,
)
from ouroboros.preflight_receipts import FullGateReceipt
from ouroboros.preflight_inventory import selection_passes, read_inventory, inventory_problem
if selected:
from ouroboros.test_selection import select_tests
selection = select_tests(worktree, base=base or ("HEAD" if phase == PRE_COMMIT_PHASE else "HEAD~1"),
extra_tests=extra_tests, full_tests=full_tests, test_reason=test_reason)
if pytest_args is not None:
raise ValueError("selected scope cannot be combined with custom pytest arguments")
passes = selection_passes(selection, probe_module=probe_module, temp_root=temp_root)
subject = capture_preflight_test_subject(
worktree, timeout=timeout, pytest_args=pytest_args,
passes=passes, agent_python=agent_python, probe_module=probe_module,
passes=passes, agent_python=agent_python, probe_module=probe_module, selection=selection,
) if ctx is not None else None
if selection is not None and ctx is not None and subject is None:
raise ValueError("selected test subject could not be bound")
can_reuse = isinstance(previous_proof, PreflightTestProof) and previous_proof.covers(subject)
receipt = None if can_reuse else FullGateReceipt.prepare(ctx, subject, passes, probe_module, agent_python, temp_root, worktree)
if can_reuse or receipt.covered():
ctx._preflight_test_proof = previous_proof if can_reuse else subject
ctx._preflight_test_inventory = previous_inventory if can_reuse else None
ctx._preflight_tests_passed = True
log_preflight_test_proof(ctx, ctx._preflight_test_proof, reused=True, phase=phase, receipt=receipt and receipt.disclosure(),
empty_lanes=receipt and receipt.empty_lanes)
@ -1384,23 +1369,25 @@ def run_hermetic_pytest(
empty_passes: list[str] = []
timings: list[tuple[str, float]] = []
for spec in passes:
# Keep the exact float remainder; rounding up would exceed the
# shared total budget (including the preceding node lane).
# Do not round the remaining shared budget up.
remaining = timeout - (time.monotonic() - started)
if remaining <= 0:
return (
f"⚠️ PRE_PUSH_TEST_ERROR: the {spec.label} pass never started — the "
f"total budget of {timeout} seconds was exhausted by the earlier pass(es)"
)
# `started` sizes the shared budget; the header reports THIS pass's
# own duration, or a fast serial pass would be blamed for the whole
# gate's wall-clock after a slow parallel one.
# Headers report this pass, not cumulative shared-budget time.
pass_started = time.monotonic()
unproven_teardown = f"the {spec.label} pass did not return, so its teardown is unconfirmed"
returncode, output, reap_error = _execute_pytest_pass(
agent_python, worktree, temp_root, spec.args, remaining
)
unproven_teardown = reap_error
inventory_error = ""
if selection is not None:
inventory[spec.label], inventory_error = read_inventory(temp_root, spec.label)
if ctx is not None:
ctx._preflight_test_inventory = inventory
elapsed = time.monotonic() - pass_started
timings.append((spec.label, round(elapsed, 1)))
if reap_error:
@ -1431,6 +1418,8 @@ def run_hermetic_pytest(
f"in the {spec.label} pass (total budget {timeout} seconds)",
output, max_output,
)
if inventory_error and returncode in (0, _PYTEST_EXIT_NO_TESTS):
return f"⚠️ PRE_PUSH_TEST_ERROR: PREFLIGHT_INVENTORY: {spec.label}: {inventory_error}"
if returncode == _PYTEST_EXIT_NO_TESTS:
empty_passes.append(spec.label)
continue
@ -1438,11 +1427,11 @@ def run_hermetic_pytest(
# Use the actual nonce flag, not the stem or spec.parallel:
# custom argv gets no host probe; red/empty lanes need no count.
observed = _observed_worker_ids(temp_root)
if len(observed) < _MIN_PREFLIGHT_WORKERS:
if len(observed) < (required := min(_MIN_PREFLIGHT_WORKERS, _allocated_worker_count(temp_root))):
return _diagnosis(
"⚠️ PRE_PUSH_TEST_ERROR: PREFLIGHT_PARALLELISM_LOST (hard block): "
f"the {spec.label} pass returned green on {len(observed)} xdist "
f"worker(s), fewer than the {_MIN_PREFLIGHT_WORKERS} it requires",
f"worker(s), fewer than the {required} its admitted budget requires",
"A pass that never ran concurrently proves nothing about the "
"parallel-only defects this gate exists to catch, so it is not "
"accepted as green. Check the repository's pytest configuration for "
@ -1470,17 +1459,23 @@ def run_hermetic_pytest(
"⚠️ PRE_PUSH_TEST_ERROR: no tests were collected in any preflight pass — "
"a repository with a tests/ directory must yield at least one runnable test"
)
if selection and (problem := inventory_problem(selection, inventory)):
return f"⚠️ PRE_PUSH_TEST_ERROR: PREFLIGHT_INVENTORY: {problem}"
if ctx is not None:
ctx._preflight_tests_passed = True
# Missing node-lane execution cannot mint a complete proof. The
# normal producer returns rc=0 only after its containment check.
if (subject
and preflight_test_workload_unchanged(subject, worktree, timeout=timeout, pytest_args=pytest_args)
and (selected_workload_unchanged(subject, worktree, timeout=timeout, selection=selection) if selection
else preflight_test_workload_unchanged(subject, worktree, timeout=timeout, pytest_args=pytest_args))
and (not subject.workload[4] or (node_result or {}).get("returncode") == 0)):
ctx._preflight_test_proof = subject
receipt.publish(ctx, temp_root, worktree, agent_python, empty_passes, phase, timings)
log_preflight_test_proof(ctx, subject, reused=False, phase=phase, passes=timings, receipt=receipt.disclosure(),
empty_lanes=empty_passes)
if selection and ctx._preflight_test_proof is None:
ctx._preflight_tests_passed = False
raise ValueError("selected test scope changed during execution; no proof was minted")
return None
except subprocess.TimeoutExpired:
failure = f"⚠️ PRE_PUSH_TEST_ERROR: pytest timed out after {timeout} seconds"
@ -1489,5 +1484,7 @@ def run_hermetic_pytest(
except Exception as exc:
failure = f"⚠️ PRE_PUSH_TEST_ERROR: hermetic preflight failed: {exc}"
finally:
from ouroboros.preflight_inventory import record_failed_attempt
record_failed_attempt(ctx, selection, subject, inventory)
_release_hermetic_tree(repo, temp_root, worktree, worktree_added, unproven_teardown)
return _with_retention_notice(failure, unproven_teardown, worktree, max_output)

View file

@ -269,7 +269,7 @@ class ProcessContainer:
teardown — but only when the API confirms it). Prefer ``spawn`` over ``Popen`` + ``adopt``:
POSIX ``adopt`` can neither plant the token nor vouch for a pid or group."""
def __init__(self) -> None:
def __init__(self, *, lifeline: bool = False, overload: bool = False) -> None:
self._job = None
# The pid `spawn` started and the group it leads: known WITHOUT having to be read.
self._root = 0
@ -281,6 +281,11 @@ class ProcessContainer:
self._setup_error = ""
# Unique per instance: a nested preflight and its outer run never claim each other's.
self._token = f"{CONTAINMENT_ENV_PREFIX}{uuid.uuid4().hex}"
self._protect_owner = lifeline
self._watch_overload = overload
self._lifeline = self._watch = None
self._birth = ""
self._protection_error = ""
def containment_env(self) -> "dict[str, str]":
"""Environment entries that make a process and its descendants members. ``spawn`` applies
@ -298,6 +303,9 @@ class ProcessContainer:
kwargs.update(group_kwargs)
env = kwargs.get("env")
env = dict(os.environ if env is None else env)
# Explicit child environments must not sever an enclosing run's custody.
env.update({key: value for key, value in os.environ.items()
if key.startswith(CONTAINMENT_ENV_PREFIX)})
env.pop(_LAUNCHER_AUTHORITY_ENV, None)
env.pop(_LAUNCHER_REPO_AUTHORITY_ENV, None)
kwargs["env"] = {**env, **self.containment_env()}
@ -306,7 +314,18 @@ class ProcessContainer:
self._suspended = True
if flags:
kwargs["creationflags"] = flags
proc = subprocess.Popen(argv, **kwargs)
if self._protect_owner and not _pl.IS_WINDOWS:
from ouroboros.process_lifeline import ProcessLifeline
self._lifeline = ProcessLifeline(self._token)
try:
if self._watch_overload:
from ouroboros.process_overload import OverloadWatch
from ouroboros.test_parallelism import nested_workers
self._watch = OverloadWatch(self, nested_workers(kwargs["env"]))
proc = subprocess.Popen(argv, **kwargs)
except BaseException:
self.reap()
raise
# Knowledge no scan can re-derive: enumeration claims members by READING them, so a root
# turning nondumpable before the first scan is in no list. `start_new_session` makes it its
# own group LEADER (pgid == pid); enumerating any OTHER pgid would report bystanders.
@ -315,6 +334,17 @@ class ProcessContainer:
self._pgid = pgid if pgid and pgid == self._root else 0
self._start_ticks = _pl._proc_start_ticks(self._root)
self.adopt(proc)
try:
if self._protect_owner or self._watch_overload:
self._birth = _pl.process_start_time(self._root)
if self._lifeline is not None:
self._lifeline.attach(self._root, self._birth, self._pgid, self._start_ticks)
if self._watch is not None:
self._watch.start()
except BaseException:
self.reap()
proc.wait(timeout=5)
raise
return proc
def adopt(self, proc: subprocess.Popen) -> None:
@ -388,7 +418,46 @@ class ProcessContainer:
undetermined.append(pid)
return alive, undetermined, ""
def stop_tree(self) -> None:
"""Stop this invocation, keeping attribution alive for the final scan."""
if _pl.IS_WINDOWS:
if self._job is not None:
error = _pl.terminate_job(self._job)
if error:
self._protection_error = "; ".join(part for part in (self._protection_error, error) if part)
elif self._token:
from ouroboros.process_lifeline import kill_attributed_tree
kill_attributed_tree(self._token, self._root, self._birth)
def reap(self) -> str:
"""Stop protection, reap, and only then release the independent guardian."""
if self._watch is not None:
watch, self._watch = self._watch, None
try:
self._protection_error = watch.stop() or self._protection_error
except Exception as exc:
self._protection_error = f"overload watch stop unconfirmed: {exc}"
error = ""
try:
if self._protect_owner or self._watch_overload:
try:
self.stop_tree()
except Exception as exc:
error = f"contained tree stop unconfirmed: {exc}"
try:
error = "; ".join(part for part in (error, self._reap()) if part)
except Exception as exc:
error = "; ".join(part for part in (error, f"container reap unconfirmed: {exc}") if part)
finally:
if self._lifeline is not None:
lifeline, self._lifeline = self._lifeline, None
try:
error = lifeline.finish(released=not error) or error
except Exception as exc:
error = "; ".join(part for part in (error, f"lifeline release unconfirmed: {exc}") if part)
return "; ".join(part for part in (self._protection_error, error) if part)
def _reap(self) -> str:
"""SCAN the container; return "" only when nothing of it is left, else why not. Detection,
not guaranteed teardown. ONE bounded best-effort kill sweep runs first; after it nothing is
ever signalled again, so a member is targeted at most once and the unavoidable
@ -433,6 +502,8 @@ class ProcessContainer:
def close(self) -> None:
"""Release the container handle. Inert after ``reap``, which closes it itself."""
if self._lifeline is not None or self._watch is not None:
self.reap()
if _pl.IS_WINDOWS and self._job is not None:
_pl.close_job(self._job)
self._job = None

View file

@ -0,0 +1,144 @@
"""Invocation-scoped POSIX cleanup after a ProcessContainer owner dies.
The guardian is outside the workload's session. Only the owner holds the pipe's
write end; EOF survives SIGKILL and never depends on a Python finally block.
Windows needs no guardian: ProcessContainer's non-inheritable kill-on-close Job
handle is closed by the kernel on owner death, including TerminateProcess.
This is supervision, not a sandbox: a detached child that discards its token
before observation, or killing both owner and guardian, can defeat POSIX cleanup.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import select
import subprocess
import sys
from types import SimpleNamespace
from ouroboros import platform_layer as pl
# Bounds on the guardian protocol/exit, not on the workload's useful lifetime.
_HANDSHAKE_SEC = 10.0
_GUARDIAN_EXIT_SEC = 30.0
def kill_attributed_tree(token: str, root: int = 0, birth: str = "") -> None:
"""End the measured root's tree, then token-bearing detached descendants.
Neither a stale pid/pgid nor a command-name match grants signal authority.
POSIX still has the unavoidable exit/reuse race between validation and kill;
the existing container's survivor scan reports unconfirmed cleanup.
"""
from ouroboros.process_containment import (
ENV_ASSIGNMENT_PRESENT, pid_environment_assignment_state, pids_with_env_marker,
)
if root > 0 and birth and pl.process_start_time(root) == birth:
pl.kill_process_tree(SimpleNamespace(pid=root))
members = pids_with_env_marker(token)
if members is None:
raise RuntimeError("lifeline could not enumerate the contained process tree")
for pid in members:
if pid != os.getpid() and pid_environment_assignment_state(pid, token, "1") == ENV_ASSIGNMENT_PRESENT:
pl.force_kill_pid(pid)
class ProcessLifeline:
"""An owner-held pipe; release only after the workload has been reaped."""
def __init__(self, token: str):
from ouroboros.process_containment import ProcessContainer
# This container owns the guardian itself, with a DIFFERENT token. The
# watched token travels over stdin, never argv/environment (which would
# make the guardian attribute itself or its ps helpers to the workload).
self._container = ProcessContainer()
self._proc = None
try:
self._proc = self._container.spawn(
[sys.executable, "-I", "-B", "-c",
"import sys; sys.path.insert(0, sys.argv[1]); "
"from ouroboros.process_lifeline import _guardian_main; _guardian_main()",
str(Path(__file__).resolve().parents[1])],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
)
ready, _, _ = select.select([self._proc.stdout], [], [], _HANDSHAKE_SEC)
if not ready or self._proc.stdout.read(1) != b"R":
raise RuntimeError("process lifeline failed to start")
self._proc.stdout.close()
self._send({"token": token})
except BaseException:
self.finish(released=False)
raise
def _send(self, message: dict) -> None:
self._proc.stdin.write((json.dumps(message) + "\n").encode("utf-8"))
self._proc.stdin.flush()
def attach(self, pid: int, birth: str, pgid: int, since: int) -> None:
self._send({"root": pid, "birth": birth, "pgid": pgid, "since": since})
def finish(self, *, released: bool) -> str:
proc, self._proc = self._proc, None
if proc is None:
return ""
error = ""
try:
if proc.stdin is not None and not proc.stdin.closed:
try:
if released:
proc.stdin.write(b'{"release":true}\n')
proc.stdin.flush()
except (BrokenPipeError, OSError) as exc:
error = f"process lifeline lost: {exc}"
finally:
proc.stdin.close()
# EOF asks for cleanup; normal release asks for stand-down. Wait
# either way, so successful test runs never accumulate guardians.
try:
code = proc.wait(timeout=_GUARDIAN_EXIT_SEC)
if code:
error = error or f"process lifeline exited {code}"
except subprocess.TimeoutExpired:
error = "process lifeline did not exit after pipe closure"
finally:
error = self._container.reap() or error
self._container.close()
if proc.stdout is not None:
proc.stdout.close()
proc.wait(timeout=5)
return error
def _guardian_main() -> None:
"""Private pipe protocol: initialize, optionally identify root, release/EOF."""
token, root, birth, pgid, since = "", 0, "", 0, 0
sys.stdout.buffer.write(b"R")
sys.stdout.buffer.flush()
for line in sys.stdin.buffer:
try:
record = json.loads(line)
except (ValueError, UnicodeError):
# Owner may have died during its final write. Keep the last complete
# ownership record; the token was delivered before workload spawn.
continue
if record.get("release"):
return
token = record.get("token", token)
root, birth = record.get("root", root), record.get("birth", birth)
pgid, since = record.get("pgid", pgid), record.get("since", since)
if token:
kill_attributed_tree(token, root, birth)
# Reuse the same bounded verification as a live owner, including
# detached grandchildren created while the first tree sweep ran.
from ouroboros.process_containment import ProcessContainer
container = ProcessContainer()
container._token, container._root = token, root
container._pgid, container._start_ticks = pgid, since
error = container.reap()
if error:
print(f"PROCESS_LIFELINE_UNCONFIRMED {error}", file=sys.stderr, flush=True)
raise SystemExit(1)

View file

@ -0,0 +1,181 @@
"""Sample an owned test tree and stop it before it consumes physical headroom.
RSS is summed (shared pages may be counted more than once). This intentionally
conservative admission/stop budget is not an OS memory/PID/file-descriptor quota.
Rapid growth between samples can exceed it, particularly on macOS.
"""
from __future__ import annotations
from dataclasses import dataclass
import os
from pathlib import Path
import subprocess
import sys
import threading
import time
from ouroboros import platform_layer as pl
@dataclass(frozen=True)
class MemberMemory:
pid: int
rss_bytes: int
name: str
def _in_job(pid: int, job) -> bool:
"""Windows membership comes from the held Job, including reparented members."""
import ctypes
from ctypes import wintypes
def read(handle):
answer = wintypes.BOOL()
if not pl._kernel32.IsProcessInJob(handle, job, ctypes.byref(answer)):
raise OSError(ctypes.get_last_error(), "IsProcessInJob failed")
return bool(answer.value)
error, member = pl._windows_query(pid, read)
if error == 87: # The process exited while the process table was read.
return False
if error:
return False # An inaccessible stranger is not an attributed member.
return bool(member)
def token_members(container) -> set[int]:
"""Members that carry the container's token, detached ones included: one whole-table scan."""
from ouroboros.process_containment import pids_with_env_marker
pids = pids_with_env_marker(container._token)
if pids is None:
raise RuntimeError("contained process membership is unavailable")
return set(pids)
def member_memory(container, detached=None) -> list[MemberMemory]:
"""Read live RSS only for the container's Job, token or measured tree.
``detached`` reuses the last token scan; the measured tree is re-read every sample.
"""
if pl.IS_WINDOWS:
import psutil
if container._job is None:
return []
members = []
for proc in psutil.process_iter(["pid", "name", "memory_info"]):
try:
if _in_job(proc.pid, container._job):
info = proc.info["memory_info"]
if info is None:
raise RuntimeError(f"RSS unavailable for Job member {proc.pid}")
members.append(MemberMemory(proc.pid, info.rss, proc.info["name"] or "unknown"))
except psutil.NoSuchProcess:
continue
return members
owned = set(token_members(container) if detached is None else detached)
if container._birth and pl.process_start_time(container._root) == container._birth:
owned.update([container._root, *pl.collect_descendant_pids(container._root)])
owned.discard(os.getpid())
if os.path.isdir("/proc"): # Linux: slim images ship no ps; the kernel table is authoritative
page, members = os.sysconf("SC_PAGE_SIZE"), []
for pid in owned:
try:
resident = int(Path(f"/proc/{pid}/statm").read_text(encoding="ascii").split()[1])
name = Path(f"/proc/{pid}/comm").read_text(encoding="utf-8", errors="replace").strip()
except (OSError, ValueError, IndexError):
continue # exited between enumeration and the read
members.append(MemberMemory(pid, resident * page, name))
return members
# One ps RSS table per sample; psutil is intentionally a Windows-only dependency.
result = subprocess.run(["ps", "-A", "-o", "pid=,rss=,comm="],
capture_output=True, text=True, timeout=5, check=True)
members = []
for line in result.stdout.splitlines():
fields = line.split(maxsplit=2)
if len(fields) == 3 and int(fields[0]) in owned:
members.append(MemberMemory(int(fields[0]), int(fields[1]) * 1024, fields[2]))
return members
def stop_reason(total: int, memory, *, limit: int, floor: int, reserve: int) -> str:
"""Why an owned tree must stop now, or "".
A tree past its limit always stops. Under the machine's physical reserve only
a tree that outgrew what admission gave it stops: a normal run is never
killed for another program's memory pressure.
"""
if total > limit:
return "tree over its physical budget"
if memory is not None and memory.available_bytes < reserve and total > floor:
return "machine under its reserve and tree past its admitted size"
return ""
class OverloadWatch:
"""A joined owner thread; a stop always survives as a nonempty reap error."""
def __init__(self, container, slots: int = 1):
from ouroboros.config import TEST_PROCESS_SAMPLE_SEC, TEST_WORKER_PEAK_RSS_BYTES
from ouroboros.test_parallelism import memory_budget, physical_memory
initial = physical_memory()
# The admitted workers and their controller always fit, so admission never
# starts a tree that this watch would then kill. Unknown memory leaves the floor.
self.floor_bytes = (max(1, slots) + 1) * TEST_WORKER_PEAK_RSS_BYTES
self.limit_bytes = max(memory_budget(initial) if initial else 0, self.floor_bytes)
self.degraded = ""
self._container = container
self._interval = TEST_PROCESS_SAMPLE_SEC
self._done = threading.Event()
self._thread = None
self.diagnostic = ""
def start(self) -> None:
thread = threading.Thread(target=self._run, name="test-overload-watch", daemon=True)
thread.start()
self._thread = thread # A failed start must never leave an unjoinable thread.
def _run(self) -> None:
from ouroboros.config import TEST_MEMORY_RESERVE_FRACTION, TEST_PROCESS_TOKEN_SCAN_SEC
from ouroboros.test_parallelism import physical_memory
detached, scanned = None, None
while not self._done.is_set():
try:
if not pl.IS_WINDOWS and (scanned is None or time.monotonic() - scanned >= TEST_PROCESS_TOKEN_SCAN_SEC):
detached, scanned = token_members(self._container), time.monotonic()
members = member_memory(self._container, detached)
memory = physical_memory()
except Exception as exc:
# A sample this host cannot take is skipped (said once); it never fails a
# healthy run or ends the watch. The tree is still owned and reaped.
if not self.degraded:
self.degraded = f"overload watch sample failed, the memory stop skips it: {exc}"
print(self.degraded, file=sys.stderr, flush=True)
else:
total = sum(member.rss_bytes for member in members)
reserve = int(memory.total_bytes * TEST_MEMORY_RESERVE_FRACTION) if memory else 0
why = stop_reason(total, memory, limit=self.limit_bytes, floor=self.floor_bytes, reserve=reserve)
if members and why:
largest = max(members, key=lambda member: member.rss_bytes)
self.diagnostic = (
f"overload_stop: {why}; tree_rss={total} limit={self.limit_bytes} floor={self.floor_bytes} "
f"available={memory.available_bytes if memory else 'unknown'} reserve={reserve}; "
f"largest pid={largest.pid} name={largest.name!r} rss={largest.rss_bytes}"
)
try:
self._container.stop_tree()
except Exception as exc:
self.diagnostic += f"; tree stop unconfirmed: {exc}"
return
if self._done.wait(self._interval):
return
def stop(self) -> str:
self._done.set()
if self._thread is not None:
self._thread.join() # all process-table/memory probes have bounded waits
return self.diagnostic

View file

@ -299,5 +299,5 @@ BYTE_BASELINE_DEBT = {
}
BYTE_DEBT = {
"tests/test_devtools_benchmarks.py": 326504,
"tests/test_devtools_benchmarks.py": 324631,
}

View file

@ -0,0 +1,159 @@
"""Physical admission budget shared by the test entrypoints and nested runners.
This bounds initial fan-out, not later allocation: the process owner separately
watches its tree for overload. A failed memory probe admits one worker.
"""
from __future__ import annotations
from dataclasses import dataclass
import os
from pathlib import Path
import re
import subprocess
import sys
from typing import Mapping, Optional, Sequence
# Transport state must survive the OUROBOROS_ runtime-settings scrub, just like
# ProcessContainer's membership token. A test explicitly allocates extra slots
# to a child by supplying this key in that child's environment.
NESTED_WORKERS_ENV = "OURO_TEST_WORKER_BUDGET"
WORKERS_ENV = "OUROBOROS_PREFLIGHT_TEST_WORKERS"
@dataclass(frozen=True)
class MemorySnapshot:
total_bytes: int
available_bytes: int
def _native_output(argv: Sequence[str]) -> str:
# Bounded, same-call OS probes; no test process or daemon is started here.
return subprocess.run(argv, check=True, capture_output=True, text=True,
encoding="utf-8", timeout=2).stdout
def physical_memory() -> Optional[MemorySnapshot]:
"""Read total and reclaimable physical memory using native OS facilities."""
try:
if sys.platform == "win32":
import psutil # The packaged Windows runtime includes psutil.
memory = psutil.virtual_memory()
total, available = memory.total, memory.available
elif sys.platform == "darwin":
total = int(_native_output(["/usr/sbin/sysctl", "-n", "hw.memsize"]).strip())
vm = _native_output(["/usr/bin/vm_stat"])
page_size = int(re.search(r"page size of (\d+) bytes", vm).group(1))
pages = {name.strip(): int(value.strip().rstrip("."))
for name, value in (line.split(":", 1) for line in vm.splitlines()[1:] if ":" in line)}
# Inactive/file-backed memory is reclaimable. Purgeable is already
# included in active/inactive, so counting it again overstates room.
available = page_size * sum(pages[name] for name in
("Pages free", "Pages inactive", "Pages speculative"))
elif sys.platform.startswith("linux"):
fields = {name: int(value.split()[0]) * 1024 for name, value in
(line.split(":", 1) for line in Path("/proc/meminfo").read_text(encoding="utf-8").splitlines())}
total, available = fields["MemTotal"], fields["MemAvailable"]
# Inside a container, its cgroup v2 limit is the physical room, not the host's RAM;
# reclaimable file cache counts as room, as MemAvailable counts it on the host.
limit = Path("/sys/fs/cgroup/memory.max")
if limit.is_file() and (cap := limit.read_text(encoding="ascii").strip()) != "max":
used = int(Path("/sys/fs/cgroup/memory.current").read_text(encoding="ascii"))
try:
stat = dict(line.split() for line in Path("/sys/fs/cgroup/memory.stat").read_text(
encoding="ascii").splitlines() if len(line.split()) == 2)
except OSError:
stat = {}
used = max(0, used - int(stat.get("inactive_file", 0)))
total, available = min(total, int(cap)), min(available, max(0, int(cap) - used))
else:
return None
if total <= 0 or available < 0:
return None
return MemorySnapshot(total, min(total, available))
except (OSError, ValueError, KeyError, AttributeError, ImportError, subprocess.SubprocessError):
return None
def memory_budget(memory: MemorySnapshot) -> int:
"""Leave the configured physical reserve outside the admitted test tree."""
from ouroboros.config import TEST_MEMORY_RESERVE_FRACTION
return max(0, memory.available_bytes - int(memory.total_bytes * TEST_MEMORY_RESERVE_FRACTION))
def _positive(value: object, default: int) -> int:
try:
return max(1, int(str(value).strip()))
except ValueError:
return default
def nested_workers(env: Mapping[str, str]) -> int:
"""A child has one slot unless its test explicitly allocates more."""
return _positive(env.get(NESTED_WORKERS_ENV, "1"), 1)
def requested_workers(cpu_workers: Optional[int] = None, *, env: Optional[Mapping[str, str]] = None) -> int:
"""The deterministic request: the override or CPU default within parent slots."""
env = os.environ if env is None else env
requested = _positive(env.get(WORKERS_ENV, ""), cpu_workers or os.cpu_count() or 1)
if NESTED_WORKERS_ENV in env:
requested = min(requested, nested_workers(env))
return max(1, requested)
def worker_budget(cpu_workers: Optional[int] = None, *, env: Optional[Mapping[str, str]] = None) -> int:
"""Keep the caller's CPU default, capped by physical room and parent slots."""
from ouroboros.config import TEST_WORKER_PEAK_RSS_BYTES
memory = physical_memory()
capacity = max(1, memory_budget(memory) // TEST_WORKER_PEAK_RSS_BYTES) if memory else 1
return max(1, min(requested_workers(cpu_workers, env=env), capacity))
def _cap_requested(value, slots: int, *, pytest_count: bool = False) -> str:
text = str(value)
if pytest_count and text in {"auto", "logical"}:
return str(slots)
try:
count = int(text)
except ValueError:
return text # Preserve the native CLI's refusal of invalid input.
return str(min(slots, count)) if count > 0 else text
def bounded_test_command(command, env: Mapping[str, str]):
"""Apply the child allocation to recognizable Node and pytest argv lists.
Popen's shared test seam covers legacy helpers too. Shell strings remain
untouched; their owners must use argv or a runner reading the budget key.
"""
if not isinstance(command, (list, tuple)) or not command:
return command
args = list(command)
executable = Path(os.fsdecode(args[0])).name.lower()
slots = nested_workers(env)
if executable in {"node", "node.exe"} and "--test" in args:
option = "--test-concurrency"
supplied = False
for index, arg in enumerate(args):
if arg == option:
supplied = True
if index + 1 < len(args):
args[index + 1] = _cap_requested(args[index + 1], slots)
if isinstance(arg, str) and arg.startswith(option + "="):
supplied = True
args[index] = option + "=" + _cap_requested(arg.split("=", 1)[1], slots)
if not supplied:
args.insert(args.index("--test") + 1, f"{option}={slots}")
elif (executable.startswith("pytest") or
any(args[i:i + 2] == ["-m", "pytest"] for i in range(len(args) - 1))):
for index, arg in enumerate(args):
if arg in {"-n", "--numprocesses"} and index + 1 < len(args):
args[index + 1] = _cap_requested(args[index + 1], slots, pytest_count=True)
elif isinstance(arg, str) and (arg.startswith("--numprocesses=") or
arg.startswith("-n") and len(arg) > 2):
value = arg.split("=", 1)[1] if "=" in arg else arg[2:]
prefix = "--numprocesses=" if arg.startswith("--") else "-n"
args[index] = prefix + _cap_requested(value, slots, pytest_count=True)
return args

277
ouroboros/test_selection.py Normal file
View file

@ -0,0 +1,277 @@
"""Commit/PR test scope: a contract floor, direct imports and authored widening.
Selection is computed from the candidate's files, including dirty and untracked
inputs. It is a minimum, not a claim about dynamic or indirect consumers.
"""
from __future__ import annotations
import ast
from dataclasses import asdict, dataclass
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import subprocess
from typing import Iterable
# Minimal witnesses of the contracts verification and every install rely on.
# Replacing a witness is a verifier change and requires full qualification.
# The whole Node lane is unconditional in both runners, outside this Python list.
FLOOR_TESTS = {
"tests/test_contracts.py": "frozen ABI and tool/context protocols",
"tests/test_gateway_parity.py": "HTTP/WS wire schemas and router parity",
"tests/test_packaging_sync.py": "release identity and public install carriers",
"tests/test_process_custody.py": "every long-lived child has a lifetime owner",
"tests/test_hermetic_data_root.py": "tests cannot bind the live data roots",
"tests/test_preflight_process_containment.py": "a leaked test process cannot pass the gate",
"tests/test_smoke.py": "the shipped Python modules import",
"tests/test_safety_policy.py": "every built-in tool has an explicit safety policy",
}
SELECTION_RULE = "floor + changed tests + direct importers + authored widening"
# Bind the policy this process loaded, even if its source changes later on disk.
_POLICY_SOURCE = Path(__file__).read_bytes()
_APPARATUS = frozenset({
"pyproject.toml", "pytest.ini", ".pytest.ini", "setup.cfg", "tox.ini",
"uv.lock", "requirements.txt", "requirements-runtime.lock",
"web/package.json", "web/package-lock.json", "site/pnpm-lock.yaml",
"ouroboros/preflight_runner.py", "ouroboros/preflight_node.py",
"ouroboros/preflight_receipts.py", "ouroboros/commit_admission.py",
"ouroboros/test_selection.py", "ouroboros/preflight_inventory.py",
"ouroboros/preflight_qualification.py", "scripts/run_tests.py", "scripts/safe_test.py",
"ouroboros/test_environment.py", ".github/workflows/ci.yml",
"ouroboros/process_containment.py", "ouroboros/process_lifeline.py",
"ouroboros/process_overload.py", "ouroboros/test_parallelism.py",
})
def _git(repo: Path, *args: str) -> bytes:
# Git inspection is bounded transport, never a test-runner timeout.
return subprocess.run(["git", *args], cwd=repo, capture_output=True,
check=True, timeout=30).stdout
def changed_paths(repo: Path, base: str) -> tuple[str, tuple[str, ...]]:
"""Resolve once; --no-renames keeps both sides of a module rename in scope."""
oid = _git(repo, "rev-parse", "--verify", "--end-of-options", f"{base}^{{commit}}").decode().strip()
raw = _git(repo, "diff", "--no-ext-diff", "--no-textconv", "--no-renames", "--name-only", "-z", oid, "--")
raw += _git(repo, "ls-files", "--others", "--exclude-standard", "-z")
return oid, tuple(sorted({os.fsdecode(p) for p in raw.split(b"\0") if p}))
def is_test_module(path: str) -> bool:
p = PurePosixPath(path)
return bool(p.parts) and p.parts[0] == "tests" and p.name.startswith("test_") and p.suffix == ".py"
def apparatus_change(path: str) -> bool:
p = PurePosixPath(path)
# Under tests/, everything but a test module (helpers, conftest, fixture data) is shared apparatus.
return (path in _APPARATUS or path in FLOOR_TESTS
or (p.parts[0] == "tests" and not is_test_module(path))
or (p.name.startswith("requirements") and p.suffix in {".txt", ".lock"}))
# Release carriers bumped by every numbered release (P9): only a change beyond the
# release version makes them apparatus, or selection would never apply to a release.
_RELEASE_CARRIERS = frozenset({"pyproject.toml", "uv.lock", "web/package.json", "web/package-lock.json"})
_PYPROJECT_VERSION = re.compile(r'^version\s*=\s*"([^"]+)"', re.M)
def _release_versions(repo: Path, base: str) -> set[str]:
"""Both spellings of the base and candidate release version (VERSION and its PEP 440 form)."""
versions = set()
for name in ("VERSION", "pyproject.toml"):
texts = []
try:
texts.append(_git(repo, "show", f"{base}:{name}").decode("utf-8"))
except (subprocess.SubprocessError, UnicodeError):
pass
try:
texts.append((repo / name).read_text(encoding="utf-8"))
except OSError:
pass
for text in texts:
if name == "VERSION":
versions.add(text.strip())
elif match := _PYPROJECT_VERSION.search(text):
versions.add(match.group(1))
return {version for version in versions if version}
def _version_only_change(repo: Path, base: str, path: str, versions: set[str]) -> bool:
try:
old = _git(repo, "show", f"{base}:{path}").decode("utf-8")
new = (repo / path).read_text(encoding="utf-8")
except (OSError, UnicodeError, subprocess.SubprocessError):
return False
for version in sorted(versions, key=len, reverse=True):
old, new = old.replace(version, "\0"), new.replace(version, "\0")
return bool(versions) and old == new
def _module(path: str) -> str:
parts = list(PurePosixPath(path).with_suffix("").parts)
if parts[-1] == "__init__":
parts.pop()
return ".".join(parts)
def direct_imports(source: str, path: str) -> set[str]:
"""Include imports in functions/TYPE_CHECKING; do not follow transitive edges."""
imports: set[str] = set()
package = path.removesuffix(".py").split("/")[:-1]
for node in ast.walk(ast.parse(source, filename=path)):
if isinstance(node, ast.Import):
imports.update(alias.name for alias in node.names)
elif isinstance(node, ast.ImportFrom):
prefix = ".".join(package[:len(package) - node.level + 1]) if node.level else ""
module = ".".join(part for part in (prefix, node.module or "") if part)
if module:
imports.add(module)
imports.update(f"{module}.{alias.name}" for alias in node.names if alias.name != "*")
return imports
# Every proof check re-selects; parsing ~2,000 test modules each time cost seconds per commit.
# Keyed by content, so an edited module is parsed again; the bound only caps memory.
_IMPORT_SCANS: dict[tuple[str, bytes], frozenset[str]] = {}
_IMPORT_SCAN_ENTRIES = 50_000
def _scanned_imports(repo: Path, path: str) -> frozenset[str]:
source = (repo / path).read_text(encoding="utf-8")
key = (path, hashlib.blake2b(source.encode("utf-8"), digest_size=16).digest())
if key not in _IMPORT_SCANS:
if len(_IMPORT_SCANS) >= _IMPORT_SCAN_ENTRIES:
_IMPORT_SCANS.clear()
_IMPORT_SCANS[key] = frozenset(direct_imports(source, path))
return _IMPORT_SCANS[key]
def validate_widening(repo: Path, extra_tests=(), full_tests=False, reason="") -> tuple[str, ...]:
if type(full_tests) is not bool or not isinstance(extra_tests, (list, tuple)):
raise ValueError("extra_tests must be a list of paths and full_tests must be a boolean")
if not isinstance(reason, str):
raise ValueError("test_reason must be a string")
if (extra_tests or full_tests) and not reason.strip():
raise ValueError("test_reason is required when widening with extra_tests or full_tests")
normalized = set()
for path in extra_tests:
if not isinstance(path, str) or "\\" in path or ".." in PurePosixPath(path).parts:
raise ValueError(f"extra_tests needs repository-relative test modules: {path!r}")
p = PurePosixPath(path)
if p.is_absolute() or not is_test_module(path) or not (repo / path).is_file():
raise ValueError(f"extra_tests module is missing or invalid: {path!r}")
if not (repo / path).resolve().is_relative_to(repo.resolve()):
raise ValueError(f"extra_tests module escapes the candidate: {path!r}")
normalized.add(p.as_posix())
return tuple(sorted(normalized))
@dataclass(frozen=True)
class TestSelection:
__test__ = False
tier: str
base: str
files: tuple[str, ...]
reasons: tuple[tuple[str, str], ...]
changed: tuple[str, ...]
policy: str
available: int
extra_tests: tuple[str, ...] = ()
full_tests: bool = False
test_reason: str = ""
@property
def targets(self) -> tuple[str, ...]:
return ("tests/",) if self.tier == "full" else self.files
def record(self) -> dict:
record = {**asdict(self), "rule": SELECTION_RULE, "omitted_files": self.available - len(self.files)}
# A full run would list every module; its count and the rule say the same far smaller.
record["files"] = record["expected_files"] = list(self.files) if self.tier == "selected" else []
record["file_count"] = len(self.files)
return record
def select_from_paths(repo: Path, paths: Iterable[str], *, base: str,
extra_tests=(), full_tests=False, test_reason="") -> TestSelection:
"""Pure candidate scan seam, also used for historical shadow comparisons."""
repo = Path(repo)
extra = validate_widening(repo, extra_tests, full_tests, test_reason)
missing = [path for path in FLOOR_TESTS if not (repo / path).is_file()]
if missing:
raise ValueError("required gate floor is missing: " + ", ".join(missing))
changed = tuple(sorted(set(paths)))
files = tuple(sorted(p.relative_to(repo).as_posix() for p in (repo / "tests").rglob("test_*.py")))
reasons = [(path, f"floor: {invariant}") for path, invariant in FLOOR_TESTS.items()]
versions = _release_versions(repo, base) if _RELEASE_CARRIERS & set(changed) else set()
full = [(p, "verification apparatus changed") for p in changed if apparatus_change(p)
and not (p in _RELEASE_CARRIERS and _version_only_change(repo, base, p, versions))]
if full_tests:
full.append(("*", "author requested full: " + test_reason.strip()))
selected = set(FLOOR_TESTS)
modules = {_module(p) for p in changed if p.endswith(".py") and not p.startswith("tests/")}
for path in files:
if path in changed:
selected.add(path)
reasons.append((path, "changed test module"))
if not full and modules:
try:
imports = _scanned_imports(repo, path)
except (SyntaxError, UnicodeError) as exc:
# An unavailable graph grants no extra automatic escalation rule.
raise ValueError(f"import scan unavailable for {path}; repair it or request full_tests with test_reason") from exc
bound = sorted(name for name in modules if any(i == name or i.startswith(name + ".") for i in imports))
if bound:
selected.add(path)
reasons.append((path, "direct import: " + ", ".join(bound)))
reasons.extend((p, "author widening: " + test_reason.strip()) for p in extra)
selected.update(extra)
# Source digest binds policy changes even before someone updates a version label.
policy = hashlib.sha256(_POLICY_SOURCE + json.dumps(FLOOR_TESTS, sort_keys=True).encode()).hexdigest()
return TestSelection("full" if full else "selected", base, files if full else tuple(sorted(selected)),
tuple(sorted(reasons + full)), changed, policy, len(files), extra, full_tests, test_reason.strip())
def select_tests(repo: Path, *, base="HEAD", extra_tests=(), full_tests=False, test_reason="") -> TestSelection:
oid, paths = changed_paths(Path(repo), base)
return select_from_paths(Path(repo), paths, base=oid, extra_tests=extra_tests,
full_tests=full_tests, test_reason=test_reason)
def refresh_selection(repo: Path, selection: TestSelection) -> TestSelection:
return select_tests(repo, base=selection.base, extra_tests=selection.extra_tests,
full_tests=selection.full_tests, test_reason=selection.test_reason)
def widening_properties() -> dict:
"""Tool-schema SSOT: the model chooses consumers the import graph cannot see."""
return {
"extra_tests": {"type": "array", "items": {"type": "string"}, "default": [],
"description": "Add test module paths for indirect/dynamic, prompt, skill or cross-language consumers beyond the floor and direct imports; explain in test_reason."},
"full_tests": {"type": "boolean", "default": False,
"description": "Request the full battery when impact is unresolved or broad qualification is needed; explain in test_reason. Adoption, managed updates and evolution publication require full regardless."},
"test_reason": {"type": "string", "default": "",
"description": "Why extra_tests or full_tests widens verification; retained with the commit's test evidence."},
}
def bind_commit_test_request(ctx, kwargs) -> str:
request = {key: kwargs.pop(key, default) for key, default in
(("extra_tests", []), ("full_tests", False), ("test_reason", ""))}
try:
request["extra_tests"] = validate_widening(Path(ctx.repo_dir), **{
"extra_tests": request["extra_tests"], "full_tests": request["full_tests"], "reason": request["test_reason"]})
except ValueError as exc:
return f"⚠️ TOOL_ARG_ERROR: {exc}. Nothing was staged or reviewed."
ctx._commit_test_request = request
return ""
def commit_test_options(ctx, *, force=False) -> dict:
request = getattr(ctx, "_commit_test_request", None)
return {"selected": True, **request} if request is not None and not force else {}

View file

@ -1343,6 +1343,7 @@ def _review_ledger_facts(ctx: ToolContext, commit_message: str, *, goal: str, sc
"critical_findings": list(combined_findings or getattr(ctx, "_last_review_critical_findings", []) or []),
"advisory_findings": list(getattr(ctx, "_last_review_advisory_findings", []) or []),
"tests": _review_tests_facts(ctx, tests_passed),
"test_request": dict(getattr(ctx, "_commit_test_request", None) or {}),
"preflight": _review_preflight_facts(ctx),
# The exhibit frozen before this attempt's dispatch, never re-derived at settle.
"preflight_execution": dict(getattr(ctx, "_preflight_execution", None) or {"status": "unobserved"}),
@ -1362,19 +1363,16 @@ def name_review_record(ctx: ToolContext, result: Any) -> Any:
def _review_tests_facts(ctx: ToolContext, tests_passed: Any) -> Dict[str, Any]:
"""``passed`` only for THIS candidate: the runner's flag is process state that outlives
the checkout it tested, so it counts only when the process-held test proof still covers
the current tree/index/workload (``commit_admission.PreflightTestProof``); a skipped
or stale run is ``NOT_RUN`` with its reason, never a passed result borrowed from an
earlier candidate. The exhibit this attempt froze for its seats governs too: one that
never entered the tests (a deterministic refusal) is ``NOT_RUN`` although a kept
earlier proof still covers the candidate."""
"""Passed requires a proof covering this tree/index/workload, not merely a kept flag.
Skipped/stale runs are NOT_RUN. The frozen exhibit governs too: deterministic
refusal means tests never started, even when an older proof still covers the tree.
"""
if tests_passed is not True:
return {"policy": "NOT_RUN", "result": "unknown"}
try:
from ouroboros.commit_admission import preflight_test_proof_matches
bound = bool(preflight_test_proof_matches(ctx, ctx.repo_dir))
bound = bool(preflight_test_proof_matches(ctx, ctx.repo_dir, require_full=False))
except Exception:
bound = False
exhibit = getattr(ctx, "_preflight_execution", None)

View file

@ -193,7 +193,7 @@ def self_change_tool_entries() -> list:
"restart, after the running generation's readers have stopped."),
"parameters": {"type": "object", "properties": {
"reason": {"type": "string"},
"adopt_commit": {"type": "string", "description": "Exact reviewed candidate commit SHA to adopt at this restart."},
"adopt_commit": {"type": "string", "description": "Exact reviewed candidate commit SHA to adopt at this restart; it needs a full green test run of that commit on record (commit it with full_tests=true)."},
}, "required": ["reason"]},
}, _request_restart),
]

View file

@ -19,6 +19,7 @@ from typing import Any, Dict, List, Optional, Tuple
from ouroboros.config import get_runtime_mode # noqa: F401
from ouroboros.tools.review_helpers import review_enforcement_blocks
from ouroboros.test_selection import bind_commit_test_request, commit_test_options, widening_properties
from ouroboros.runtime_mode_policy import (
core_patch_notice, # noqa: F401
format_protected_paths, # noqa: F401
@ -643,10 +644,8 @@ def _run_pre_push_tests(ctx: ToolContext, force: bool = False) -> Optional[str]:
return None
if not force and os.environ.get("OUROBOROS_PRE_PUSH_TESTS", "1") != "1":
return None
# NO `tests/` existence check here: whether the repository is in scope is
# run_hermetic_pytest's decision. This entry point runs POST-commit, so it
# compares HEAD and HEAD~1 (the default phase) — a candidate that deleted the
# suite is a hard block, while a repo that never had one is out of scope.
# The runner checks HEAD and HEAD~1: deleting the suite blocks;
# a repository that never had tests stays out of scope.
try:
from ouroboros.preflight_runner import run_hermetic_pytest
@ -654,7 +653,7 @@ def _run_pre_push_tests(ctx: ToolContext, force: bool = False) -> Optional[str]:
return run_hermetic_pytest(
pathlib.Path(ctx.repo_dir),
max_output=MAX_TEST_OUTPUT,
ctx=ctx,
ctx=ctx, **commit_test_options(ctx, force=force),
)
except Exception as e:
log.warning(f"Pre-push tests failed with exception: {e}", exc_info=True)
@ -1043,6 +1042,8 @@ def _publish_reviewed_commit(
def _commit_reviewed(ctx: ToolContext, commit_message: str, *args: Any, **kwargs: Any) -> str:
"""The public commit handler: an outcome of a call that wrote a review record names it."""
if error := bind_commit_test_request(ctx, kwargs):
return _publish_tool_result(ctx, ToolResult(status="error", code="TOOL_ARG_ERROR", text=error))
return name_review_record(ctx, _repo_commit_push(ctx, commit_message, *args, **kwargs))
@ -1366,11 +1367,9 @@ def _repo_commit_push(ctx: ToolContext, commit_message: str,
if receipt_error:
return receipt_error
if not _managed_tx and not publication_paused:
# Ordinary self-modification contract: post-commit tests are reported
# as a warning. Managed-update merges already ran them as the BLOCKING
# assisted_post_commit_tests gate right after the commit above.
# Evolution publication requires full even when the author skipped preflight.
try:
run_operation(ctx, _post_commit_result, ctx, commit_message, skip_tests, test_warning_ref)
run_operation(ctx, _post_commit_result, ctx, commit_message, skip_tests, test_warning_ref, force=bool(evolution_claim))
except OwnerPauseRefused as exc:
publication_paused = True
tag_info += f" [post-commit tests not started: owner pause ({exc})]"
@ -1382,7 +1381,8 @@ def _repo_commit_push(ctx: ToolContext, commit_message: str,
)
if publication_error:
return publication_error
push_status = _auto_push_after_review(ctx, publication_paused=publication_paused) if not body_candidate.is_bound(ctx) else body_candidate.publication_note(ctx)
push_status = (_evolution_test_publication_stop(ctx, commit_sha, test_warning_ref[0])
or (_auto_push_after_review(ctx, publication_paused=publication_paused) if not body_candidate.is_bound(ctx) else body_candidate.publication_note(ctx)))
ctx.last_reviewed_commit_sha = commit_sha
body_candidate.record_reviewed_commit(ctx, commit_sha)
if attribution_binding is not None:
@ -1449,6 +1449,7 @@ def get_tools() -> List[ToolEntry]:
"checks, the tests (see skip_tests) and the panel still run; the opposite of preflight_reviewer."
)
commit_properties = {
**widening_properties(),
"root": {"type": "string", "enum": ["system_repo"], "default": "system_repo", "description": "The reviewed commit lands in Ouroboros's own body only; review any other root with review_change and commit it with ordinary git."},
"commit_message": {"type": "string"},
"paths": {"type": "array", "items": {"type": "string"}, "description": "Optional subset of task-attributed paths. Omitted computes candidates; empty never stages the whole tree."},
@ -1569,6 +1570,7 @@ from ouroboros.tools.git_evolution import ( # noqa: E402,F401
_check_evolution_commit_stage,
_evolution_commit_authority,
_evolution_publication_stopped_result,
_evolution_test_publication_stop,
_preserve_evolution_orphan,
_record_evolution_commit_intent,
_record_evolution_commit_receipt,

View file

@ -355,6 +355,23 @@ def _record_evolution_commit_receipt(
return message
def _evolution_test_publication_stop(ctx: ToolContext, commit_sha: str, test_warning: str) -> str:
"""Withhold an evolution push that lacks full green proof of this exact commit.
Unlike lost authority this keeps the reviewed commit's provenance and settles the
attempt; only publication waits, and adoption still refuses the commit without it.
"""
if str(ctx.current_task_type or "") != "evolution":
return ""
from ouroboros.preflight_qualification import full_proof_error
error = test_warning or full_proof_error(ctx, ctx.repo_dir, commit_sha)
if not error:
return ""
ctx.last_push_succeeded = False
return f" [EVOLUTION_PUBLICATION_STOPPED: {error.strip()}; the reviewed commit stays local, nothing was pushed]"
def _evolution_publication_stopped_result(
ctx: ToolContext, commit_message: str, commit_sha: str, test_warning: str,
created_tag: str = "", started_at: float = 0.0,

View file

@ -803,6 +803,8 @@ def _run_review_preflight_tests(ctx: "Any", timeout: Optional[int] = None, *, fo
# Pre-commit entry point: the deleted-suite baseline is HEAD alone.
run_kwargs = {"max_output": MAX_OUTPUT, "phase": PRE_COMMIT_PHASE, "ctx": ctx}
from ouroboros.test_selection import commit_test_options
run_kwargs.update(commit_test_options(ctx, force=force))
if timeout is not None:
run_kwargs["timeout"] = timeout
output = run_hermetic_pytest(pathlib.Path(repo_dir), **run_kwargs)

View file

@ -259,10 +259,9 @@ account citing corrected direct sources, not by merely reselecting the old one.
I read every reader and writer, update docs, prompts, and tests in the same
diff, keep `docs/ARCHITECTURE.md` rationale in sync for non-obvious
decisions, and run focused tests before review. `python scripts/run_tests.py`
is the fast full battery (arguments forward a focused run); a bare
`pytest tests/` is one slow process, and the reviewed commit runs the full
battery itself or reuses a matching receipt unless tests are explicitly
skipped.
runs all tests; commits use floor + importers. I add `extra_tests`
or `full_tests` with `test_reason` for indirect/dynamic or unresolved impact.
Adoption, managed updates and evolution publication require full tests.
- I preserve my own work: never delete or overwrite a viable result, candidate,
or unique input without a recoverable copy; save a working deliverable as
soon as I have one, then improve copies.

View file

@ -3,6 +3,8 @@
python scripts/run_tests.py full battery (node lane + every default-lane test)
python scripts/run_tests.py --sequential full battery exactly as the commit gate and CI split it
python scripts/run_tests.py --selected --base HEAD commit/PR floor + affected tests, with both lanes
python scripts/run_tests.py --node only the complete Node lane (CI runs it as its own step)
python scripts/run_tests.py --evidence-dir DIR pytest diagnostics in DIR/<phase>, passed by argv
python scripts/run_tests.py tests/test_x.py focused run; every argument is forwarded to pytest
@ -21,11 +23,13 @@ isolation is `tests/conftest.py`'s job and is not repeated either.
from __future__ import annotations
import argparse
import json
import os
import pathlib
import shlex
import subprocess
import sys
import tempfile
import time
REPO = pathlib.Path(__file__).resolve().parents[1]
@ -37,16 +41,15 @@ _PYTEST_EXIT_NO_TESTS = 5
def _workers() -> int:
"""What `-n auto` will resolve to: the operator override, else xdist's own count."""
raw = os.environ.get("OUROBOROS_PREFLIGHT_TEST_WORKERS", "").strip()
if raw.isdigit():
return max(2, int(raw))
"""Keep xdist's CPU default within the physical and inherited allocation."""
from ouroboros.test_parallelism import worker_budget
try: # xdist prefers physical cores whenever psutil is importable
import psutil
physical = psutil.cpu_count(logical=False)
except Exception:
physical = None
return max(2, physical or os.cpu_count() or 2)
return worker_budget(physical or os.cpu_count() or 1)
def serial_shards(workers: int) -> int:
@ -82,14 +85,35 @@ def battery_commands(sequential: bool) -> list[tuple[str, list[str]]]:
def _run(label: str, argv: list[str], cwd: pathlib.Path) -> int:
from ouroboros.preflight_node import node_environment
from ouroboros.process_containment import ProcessContainer
from ouroboros.test_parallelism import NESTED_WORKERS_ENV, bounded_test_command
print(f"\n=== {label}: {shlex.join(argv[2:] if argv[0] == sys.executable else argv)}", flush=True)
started = time.monotonic()
env = node_environment(os.environ)
override = env.get("OUROBOROS_PREFLIGHT_TEST_WORKERS", "").strip()
if override.isdigit(): # the gate steers `-n auto` the same way (preflight_runner._preflight_env)
env["PYTEST_XDIST_AUTO_NUM_WORKERS"] = str(max(2, int(override)))
code = subprocess.call(argv, cwd=str(cwd), env=env)
env["PYTEST_XDIST_AUTO_NUM_WORKERS"] = str(_workers())
env[NESTED_WORKERS_ENV] = env["PYTEST_XDIST_AUTO_NUM_WORKERS"]
container = ProcessContainer(lifeline=True, overload=True)
proc = None
try:
proc = container.spawn(bounded_test_command(argv, env), cwd=str(cwd), env=env)
code = proc.wait()
except KeyboardInterrupt:
code = 130
finally:
try:
diagnostic = container.reap()
if proc is not None:
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
diagnostic = "; ".join(filter(None, [diagnostic, "test root did not exit after reap"]))
if diagnostic:
print(f"=== {label}: {diagnostic}", file=sys.stderr, flush=True)
finally:
container.close()
if diagnostic and code in (0, _PYTEST_EXIT_NO_TESTS):
code = 1
print(f"=== {label}: exit {code} in {time.monotonic() - started:.0f}s", flush=True)
return code
@ -110,12 +134,105 @@ def _node_lane() -> int:
print(f"\n=== node: NOT_RUN — {found}; the web lane needs node >= {floor}, so this is not the full "
"battery: install node or pass focused targets", flush=True)
return 1
return _run("node", [node, "--test", *files], REPO / WEB_DIR)
return _run("node", [node, "--test", f"--test-concurrency={_workers()}", *files], REPO / WEB_DIR)
def _selected(args, evidence) -> int:
from ouroboros.preflight_inventory import inventory_problem, read_inventory, selection_passes
from ouroboros.preflight_runner import _probe_dir, _install_worker_probe
from ouroboros.test_selection import select_tests
try:
selection = select_tests(REPO, base=args.base or "HEAD", extra_tests=args.extra_test,
full_tests=args.full_tests, test_reason=args.test_reason)
except (ValueError, OSError, subprocess.SubprocessError) as exc:
print(f"selection failed: {exc}", file=sys.stderr)
return 2
print(f"=== {selection.tier}: {len(selection.files)}/{selection.available} Python modules; "
f"{selection.record()['rule']}; base {selection.base}; complete Node lane", flush=True)
# Retained diagnostics; process ownership belongs to _run, as in the full CLI.
root = evidence or pathlib.Path(tempfile.mkdtemp(prefix="ouroboros-selected-"))
root.mkdir(parents=True, exist_ok=True)
record = selection.record()
record["inventory"] = {}
record["outcomes"] = {"parallel": "skipped", "serial": "skipped"}
output = root / "selection.json"
output.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
print(f"=== selection evidence: {output}", flush=True)
if args.defer_full and selection.tier == "full":
selection = _defer_full(selection)
record = {**selection.record(), "inventory": {}, "outcomes": record["outcomes"], "deferred_full": True}
output.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
code = _node_lane()
previous = os.environ.get("PYTHONPATH")
completed = False
os.environ["PYTHONPATH"] = os.pathsep.join(filter(None, (str(_probe_dir(root)), previous)))
try:
probe = _install_worker_probe(root)
for spec in selection_passes(selection, probe_module=probe, temp_root=root) if code == 0 else []:
command = [sys.executable, "-m", "pytest", *spec.args,
"--ci-evidence-dir", str(root / spec.label)]
code = _run(spec.label, command, REPO)
inventory, problem = read_inventory(root, spec.label)
record["inventory"][spec.label] = inventory
record["outcomes"][spec.label] = "success" if code in (0, _PYTEST_EXIT_NO_TESTS) and not problem else "failure"
output.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
if code not in (0, _PYTEST_EXIT_NO_TESTS):
break
code = int(bool(problem))
if problem:
print(f"inventory failed: {problem}", file=sys.stderr)
break
if code == 0 and (problem := inventory_problem(selection, record["inventory"])):
print(problem, file=sys.stderr)
code = 1
completed = True
finally:
if previous is None:
os.environ.pop("PYTHONPATH", None)
else:
os.environ["PYTHONPATH"] = previous
record["outcome"] = "success" if completed and code == 0 else "failure"
output.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
if destination := os.environ.get("GITHUB_OUTPUT"):
with open(destination, "a", encoding="utf-8") as stream:
for label, outcome in record["outcomes"].items():
stream.write(f"{label}_outcome={outcome}\n")
print(f"=== {selection.tier} battery: {'GREEN' if code == 0 else 'RED'}", flush=True)
return code
def _defer_full(selection):
"""A full escalation this leg cannot finish (Windows runs full in four landed shards).
The leg still runs the PR minimum, the contract floor and the Node lane, and says
what it left to the landed matrix.
"""
import dataclasses
from ouroboros.test_selection import FLOOR_TESTS
why = "; ".join(f"{path}: {reason}" for path, reason in selection.reasons
if reason == "verification apparatus changed" or reason.startswith("author requested full"))
note = f"full scope deferred to the landed matrix; this leg runs the contract floor and the Node lane ({why})"
print(f"=== {note}", flush=True)
if destination := os.environ.get("GITHUB_STEP_SUMMARY"):
with open(destination, "a", encoding="utf-8") as stream:
stream.write(f"**{note}**\n")
return dataclasses.replace(selection, tier="selected", files=tuple(sorted(FLOOR_TESTS)))
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--sequential", action="store_true", help="two passes, exactly as the commit gate and CI")
parser.add_argument("--selected", action="store_true", help="floor + affected tests and complete Node lane")
parser.add_argument("--base", help="Git base for --selected; defaults to HEAD")
parser.add_argument("--extra-test", action="append", default=[], help="widen selection with a test module")
parser.add_argument("--full-tests", action="store_true", help="widen --selected to the full battery")
parser.add_argument("--test-reason", default="", help="reason for authored widening")
parser.add_argument("--defer-full", action="store_true",
help="with --selected: on a full escalation run only the floor and Node lane, and say so")
parser.add_argument("--node", action="store_true", help="only the complete Node lane, owned like every pass")
parser.add_argument("--evidence-dir", type=pathlib.Path,
help="write pytest diagnostics under DIR/<phase>; never inherited by nested pytest",
metavar="DIR")
@ -124,6 +241,16 @@ def main(argv: list[str] | None = None) -> int:
if not (REPO / "tests" / "conftest.py").is_file():
print(f"{REPO} is not an Ouroboros checkout (no tests/conftest.py)", file=sys.stderr)
return 2
if args.node:
if args.selected or args.sequential or pytest_args:
parser.error("--node runs only the Node lane")
return _node_lane()
if args.selected:
if pytest_args:
parser.error("--selected cannot be combined with arbitrary pytest arguments")
return _selected(args, evidence)
if args.base or args.extra_test or args.full_tests or args.test_reason or args.defer_full:
parser.error("--base, --defer-full and widening options require --selected")
if pytest_args:
if args.sequential:
parser.error("--sequential selects the full two-pass battery; drop it or drop the pytest arguments")
@ -131,6 +258,8 @@ def main(argv: list[str] | None = None) -> int:
pytest_args = ["--ci-evidence-dir", str(evidence / "focused"), *pytest_args]
return _run("focused", [sys.executable, "-m", "pytest", *pytest_args], REPO)
started = time.monotonic()
print(f"=== full: {sum(1 for _ in (REPO / 'tests').rglob('test_*.py'))} Python modules; "
"full default suite; complete Node lane", flush=True)
code = _node_lane()
ran = 0
for label, command in battery_commands(args.sequential) if code == 0 else []:

View file

@ -105,8 +105,16 @@ def make_serving(root: pathlib.Path) -> pathlib.Path:
return repo
def stub_adoption_evidence(monkeypatch) -> None:
"""Miniature bodies exercise switching, not the battery; the real full-evidence
acceptance/refusal boundary is exercised in test_preflight_selection.py."""
monkeypatch.setattr("ouroboros.preflight_qualification.adoption_test_evidence",
lambda *args, **kwargs: {"tier": "full", "source": "stub", "head": args[2]})
def isolate(monkeypatch, root: pathlib.Path) -> pathlib.Path:
"""Point the worktree registry and data root at ``root``; returns the data dir."""
stub_adoption_evidence(monkeypatch)
data = root / "data"
(data / "state").mkdir(parents=True, exist_ok=True)
(data / "logs").mkdir(parents=True, exist_ok=True)

View file

@ -139,6 +139,14 @@ def render_summary(root: Path, *, producer_outcome: str, artifact_outcome: str,
"""Reporter failure cannot replace producer truth; missing reports stay unknown."""
data, error = _final(root)
available = not error
# The selected CLI writes scope beside the pass directories before execution.
# Its actual tier may escalate to full; never describe a subset as full.
try:
selection = json.loads((Path(root).parent / "selection.json").read_text(encoding="utf-8"))
if scope == "full" and selection.get("tier") in ("selected", "full"):
scope = selection["tier"]
except (OSError, ValueError, AttributeError):
pass
lines = [
"## CI test evidence" + (f" — {_cell(label)}" if label else "")
+ (" — PARTIAL DIAGNOSTIC" if scope != "full" else ""),
@ -661,6 +669,9 @@ class _Results:
lane = getattr(self.config, "_default_suite_lane", None)
if lane:
facts["default_" + lane["pass"]] = lane
crash = getattr(self.config, "_ouroboros_worker_crash", None)
if crash:
facts["worker_crash"] = crash
write_json(output_dir(self.config) / "results.json", _redact(facts, self.secrets))
except Exception as error:
# Diagnostics alone never alter session.exitstatus or expose a body.

View file

@ -215,6 +215,9 @@ _PYTEST_POPEN_PATCHED = False
def _isolated_child_env(value) -> dict:
from ouroboros.process_containment import CONTAINMENT_ENV_PREFIX
from ouroboros.test_parallelism import NESTED_WORKERS_ENV, nested_workers
child_env = dict(value)
synthetic_home = (child_env.get("HOME") or child_env.get("USERPROFILE"))
synthetic_home = synthetic_home and synthetic_home != _PYTEST_DEFAULTS["HOME"]
@ -236,6 +239,12 @@ def _isolated_child_env(value) -> dict:
child_env.pop(key, None)
child_env["OUROBOROS_PYTEST_ACTIVE"] = "1"
child_env["OUROBOROS_TEST_LIVE_DATA_ROOT"] = _PYTEST_CHILD_LIVE_ROOT
# Custom env={} must retain every outer owner's attribution. Otherwise a
# test child that changes its process group escapes the owner's token scan.
child_env.update({key: value for key, value in os.environ.items()
if key.startswith(CONTAINMENT_ENV_PREFIX)})
child_env[NESTED_WORKERS_ENV] = str(nested_workers(child_env))
child_env["PYTEST_XDIST_AUTO_NUM_WORKERS"] = child_env[NESTED_WORKERS_ENV]
return child_env
@ -247,11 +256,19 @@ def _install_pytest_child_isolation() -> None:
@functools.wraps(_ORIGINAL_POPEN_INIT)
def isolated_init(self, *args, **kwargs):
from ouroboros.test_parallelism import bounded_test_command
positional = list(args)
if len(positional) > 10:
positional[10] = _isolated_child_env(os.environ if positional[10] is None else positional[10])
env = positional[10]
else:
kwargs["env"] = _isolated_child_env(os.environ if kwargs.get("env") is None else kwargs["env"])
env = kwargs["env"]
if positional:
positional[0] = bounded_test_command(positional[0], env)
elif "args" in kwargs:
kwargs["args"] = bounded_test_command(kwargs["args"], env)
return _ORIGINAL_POPEN_INIT(self, *positional, **kwargs)
subprocess.Popen.__init__ = isolated_init
@ -464,6 +481,20 @@ def _pin_lane_groups(items, shards: int) -> None:
items.sort(key=lambda item: item.get_closest_marker("serial") is None)
@pytest.hookimpl(tryfirst=True)
def pytest_cmdline_main(config):
from ouroboros.test_parallelism import NESTED_WORKERS_ENV, nested_workers
# Generated entry scripts can call pytest.main directly, bypassing the argv
# seam. xdist resolves -n auto later in this hook; cap explicit counts too.
count = getattr(config.option, "numprocesses", None)
if count and NESTED_WORKERS_ENV in os.environ:
slots = nested_workers(os.environ)
config.option.numprocesses = min(count, slots) if isinstance(count, int) else slots
# xdist's own tryfirst hook may already have expanded -n into gateways.
config.option.tx = config.option.tx[:slots]
@pytest.hookimpl(tryfirst=True)
def pytest_configure(config):
config.addinivalue_line("markers", "scale: storage operation-count acceptance measurements")
@ -532,6 +563,12 @@ def _claim_fresh_basetemp(root: pathlib.Path) -> str:
def pytest_sessionstart(session): # noqa: ARG001
from ouroboros.test_parallelism import NESTED_WORKERS_ENV
session.config._ouroboros_session = session
# This process already owns its allocation. Test-created runners begin with
# one slot; tests of real concurrency opt in by setting their child's key.
os.environ[NESTED_WORKERS_ENV] = "1"
_bind_pytest_runtime_roots()
_install_pytest_child_isolation()
repo_root = pathlib.Path(__file__).resolve().parents[1]
@ -539,6 +576,9 @@ def pytest_sessionstart(session): # noqa: ARG001
def pytest_sessionfinish(session, exitstatus): # noqa: ARG001
crash = getattr(session.config, "_ouroboros_unowned_crash", None)
if crash and not getattr(session.config, "_ouroboros_worker_crash", None):
_record_worker_crash(*crash) # after the run, so "unfinished" names only what never finished
# Under pytest-xdist this hook fires on the controller AND every worker process against the
# SHARED repo root. Run the repo-root pollution sweep + exitstatus mutation ONLY on the
# controller (the single authority): otherwise workers race the same shutil.rmtree and each
@ -1085,6 +1125,24 @@ def _isolate_workspace_executor_globals():
svc._SERVICES.update(saved_svc_services)
@pytest.fixture(autouse=True)
def _restore_supervisor_queue_refs():
"""Undo a test's ``init_queue_refs`` rebinding of the supervisor queue's maps.
Fixtures bind private PENDING/RUNNING objects and rarely bind them back, so a
later test in the same worker read the earlier one's queue (a scheduled task
became an extra history read in an unrelated module).
"""
try:
from supervisor import queue
except Exception:
yield
return
saved = (queue.PENDING, queue.RUNNING, queue.QUEUE_SEQ_COUNTER_REF)
yield
queue.PENDING, queue.RUNNING, queue.QUEUE_SEQ_COUNTER_REF = saved
@pytest.fixture(autouse=True)
def _isolate_repo_writer_gate():
"""Reset the process-global repo-writer admission latch between tests.
@ -1215,10 +1273,88 @@ def _fail_if_a_thread_leaked(item):
f"test it pollutes", pytrace=False)
@pytest.hookimpl(optionalhook=True)
@pytest.hookimpl(optionalhook=True, trylast=True)
def pytest_testnodedown(node, error): # noqa: ARG001
# pytest-xdist controller: merge each worker's leak list (shipped via workeroutput below).
_THREAD_LEAKS.extend(getattr(node, "workeroutput", {}).get("thread_leaks", []))
if error is None:
return
session = getattr(node.config, "_ouroboros_session", None)
reason = f"xdist worker {node.gateway.id} died: {error}"
controller = node.config.pluginmanager.get_plugin("dsession")
if not _owned_run():
# Nobody outer reaps this run: xdist's own crash handling lets running tests finish
# and clean up their children. The facts are recorded when the session ends.
if controller is not None and not hasattr(node.config, "_ouroboros_unowned_crash"):
node.config._ouroboros_unowned_crash = (node, reason, controller.sched)
return
if session is not None:
session.shouldfail = reason
if controller is not None:
# xdist's loop reads its own shouldstop, not Session.shouldfail. Stop
# scheduling before it redistributes the dead worker's pending items.
controller.shouldstop = reason
controller._max_worker_restart = 0
controller.triggershutdown()
scheduler = controller.sched
# With no collected/assigned test xdist emits no crash report. Its
# native schedulers expose precisely the work remove_node would use.
pending = True
if hasattr(scheduler, "assigned_work"):
pending = any(not done for unit in scheduler.assigned_work.get(node, {}).values()
for done in unit.values())
elif hasattr(scheduler, "node2pending"):
pending = bool(scheduler.node2pending.get(node))
if getattr(scheduler, "collection", None) is None or not pending:
_record_worker_crash(node, reason, scheduler)
pytest.exit(reason, returncode=1)
def _owned_run() -> bool:
"""An outer ProcessContainer will reap this run, so aborting live workers strands nothing.
Without one (a bare pytest), xdist's own crash handling lets running tests finish and
clean up their children: nobody else would.
"""
from ouroboros.process_containment import CONTAINMENT_ENV_PREFIX
return any(key.startswith(CONTAINMENT_ENV_PREFIX) for key in os.environ)
def _record_worker_crash(node, reason, scheduler):
collected = getattr(scheduler, "collection", None)
completed = getattr(node.config, "_ouroboros_completed_tests", set())
node.config._ouroboros_worker_crash = {
"worker": node.gateway.id, "reason": str(reason),
"collection_available": collected is not None,
"unfinished": sorted(set(collected or []) - completed),
}
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_logreport(report):
outcome = yield
node = getattr(report, "node", None)
config = getattr(node, "config", None)
session = getattr(config, "_ouroboros_session", None)
if config is not None:
completed = getattr(config, "_ouroboros_completed_tests", set())
if report.when in {"teardown", "???"} or report.failed or report.skipped:
completed.add(report.nodeid)
config._ouroboros_completed_tests = completed
if report.when == "???" and session is not None and session.shouldfail:
controller = config.pluginmanager.get_plugin("dsession")
_record_worker_crash(node, session.shouldfail, controller.sched)
if not _owned_run():
return
# All consumers have now recorded xdist's native crash report. Do not
# await another worker's running test: bounded gateway teardown and the
# outer ProcessContainer own shutdown; evidence retains unfinished IDs.
error = pytest.exit.Exception(str(session.shouldfail), returncode=1)
if hasattr(outcome, "force_exception"):
outcome.force_exception(error)
else: # pluggy < 1.1, still allowed by pytest 7's dependency range.
raise error
def pytest_terminal_summary(terminalreporter):

View file

@ -19,9 +19,6 @@ guard. One section per Q10 item:
from __future__ import annotations
import json
import pathlib
REPO = pathlib.Path(__file__).resolve().parents[1]
# --- Q10 item 1: OUROBOROS_SCOPE_REVIEW_FLOOR ---------------------------------
@ -81,27 +78,6 @@ def test_scope_review_floor_and_semantic_owner_detectors_are_gone():
assert not hasattr(registry, "_detect_safety_mode_self_lowering")
def test_scope_review_floor_left_no_source_remnants():
"""Grep-level absence over the runtime tree: no code path may still spell
the retired key or endpoint (docs/history and the retirement entry itself
are the only legitimate spellings)."""
allowed = {
pathlib.Path("ouroboros/settings_defaults.py"), # RETIRED_SETTING_KEYS entry
pathlib.Path("ouroboros/tools/registry_guard_process.py"), # historical incident comment
}
offenders = []
for root in ("ouroboros", "supervisor", "web", "prompts"):
for path in sorted((REPO / root).rglob("*")):
if not path.is_file() or path.suffix not in {".py", ".js", ".md", ".html"}:
continue
text = path.read_text(encoding="utf-8", errors="replace")
if "SCOPE_REVIEW_FLOOR" in text.upper() or "scope-review-floor" in text:
rel = path.relative_to(REPO)
if rel not in allowed:
offenders.append(rel.as_posix())
assert not offenders, f"retired floor surface respelled in: {offenders}"
# --- Q10 item 2: fail_tasks (budget-drain terminalizer) -----------------------
def test_fail_tasks_is_gone_and_pause_scenario_supersedes_it():
@ -116,15 +92,6 @@ def test_fail_tasks_is_gone_and_pause_scenario_supersedes_it():
assert "E13" in SCENARIOS
def test_fail_tasks_left_no_source_remnants():
offenders = []
for root in ("ouroboros", "supervisor", "web", "prompts"):
for path in sorted((REPO / root).rglob("*.py")):
if "fail_tasks" in path.read_text(encoding="utf-8", errors="replace"):
offenders.append(path.relative_to(REPO).as_posix())
assert not offenders, f"removed fail_tasks respelled in: {offenders}"
# --- Q10 item 3: until_deadline / stall_rounds_threshold aliases --------------
def test_deadline_pacing_aliases_are_gone_from_the_contract_shape():
@ -156,32 +123,3 @@ def test_deadline_no_longer_lifts_the_improvement_count_axis(monkeypatch):
assert effective_max_improvement_passes({**profile, "max_improvement_passes": 1}) == 1
params = inspect.signature(effective_max_improvement_passes).parameters
assert "has_deadline" not in params
def test_deadline_aliases_left_no_source_remnants():
"""FUNCTIONAL remnants only: a resurrected alias needs the QUOTED string
literal (a comparison, dict key or event type); prose comments disclosing
the removal legitimately keep the bare word.
One exemption: the ABI-2 quarantine classifier
(``ouroboros/task_result_schema.py``) is the removal's ENFORCEMENT arm —
it must spell the retired token to REFUSE a stored contract still
carrying it (reason ``retired_contract_until_deadline``, ledger f30
entry 4). That comparison sends rows to quarantine; it cannot make the
alias functional again."""
functional = (
'"until_deadline"', "'until_deadline'",
'"stall_rounds_threshold"', "'stall_rounds_threshold'",
"deprecated_task_pacing_alias",
)
refusal_only = {"ouroboros/task_result_schema.py"}
offenders = []
for root in ("ouroboros", "supervisor", "web", "prompts"):
for path in sorted((REPO / root).rglob("*.py")):
rel = path.relative_to(REPO).as_posix() # Windows walks with backslashes
if rel in refusal_only:
continue
text = path.read_text(encoding="utf-8", errors="replace")
if any(tok in text for tok in functional):
offenders.append(rel)
assert not offenders, f"removed pacing aliases respelled in: {offenders}"

View file

@ -1,80 +0,0 @@
"""ABI-6 removal pins: the three F3.0 P1 removals, proven negatively.
The F3.0 opening train removed three dead surfaces (`03a835b9`, `e94f063d`):
the `_call_llm_with_retry` alias in `ouroboros/loop.py`, and the second
with-children cost rollup `compute_cost_with_children` plus the unused
`format_handoff_message` in `ouroboros/task_status.py`. Each removal was
gated only by the suites of the code that SURVIVED, so every one of those
hooks stays green if an alias comes back — a positive suite cannot notice an
extra name. The ADOPTION row R ABI-6 carries that as a disclosed residual:
"proven by surviving positive suites plus grep-level absence, not by
dedicated negative pins". This file is the missing negative pin.
The assertions are deliberately two-sided. Absence alone is satisfied by
deleting the module, so each retired name is paired with the canonical
surface that replaced it: a resurrection is red, and so is losing the
replacement.
"""
from __future__ import annotations
import importlib
import inspect
import re
import pytest
# (former owner module, retired name, what carries the job now)
RETIRED = (
("ouroboros.loop", "_call_llm_with_retry",
"the public call_llm_with_retry (the one patch point)"),
("ouroboros.task_status", "compute_cost_with_children",
"the producer rollup in agent_task_pipeline/post_task_synthesis, "
"projected through cost_projection.py"),
("ouroboros.task_status", "format_handoff_message",
"the absorption / wait_tasks projections"),
("ouroboros.task_status", "_handoff_snippet",
"nothing — private helper of the removed formatter"),
("ouroboros.task_status", "HANDOFF_SNIPPET_CHARS",
"nothing — private constant of the removed formatter"),
)
def _names_the_symbol(source: str, name: str) -> bool:
"""Whole-identifier match, so `fake_call_llm_with_retry` in a test double
or `call_llm_with_retry` itself never counts as the retired spelling."""
return re.search(rf"(?<![A-Za-z0-9_]){re.escape(name)}\b", source) is not None
@pytest.mark.parametrize(("module_name", "retired", "successor"), RETIRED,
ids=[f"{m.rsplit('.', 1)[-1]}.{n}" for m, n, _ in RETIRED])
def test_a_retired_p1_name_is_absent_from_its_former_owner(module_name, retired, successor):
module = importlib.import_module(module_name)
assert not hasattr(module, retired), (
f"{module_name}.{retired} is back; the job belongs to {successor}")
# A re-export binds the attribute too, but a re-introduced *def* under a
# conditional or inside a class body would not — the source check catches
# the spelling wherever it reappears in the former owner.
assert not _names_the_symbol(inspect.getsource(module), retired), (
f"{module_name} names {retired} again")
def test_the_surviving_llm_retry_surface_is_the_public_one():
"""The alias existed "for source-inspecting/monkeypatched tests"; every
such test targets the public name, which must therefore still be there."""
from ouroboros import loop
assert callable(loop.call_llm_with_retry)
def test_the_one_with_children_cost_rollup_is_the_projection_ssot():
"""`compute_cost_with_children` was the SECOND, diverging rollup. The
remaining one is the producer path's, read through the projection SSOT."""
from ouroboros import cost_projection, task_status
assert callable(cost_projection.live_root_cost_projection)
assert callable(cost_projection.cost_projection)
# task_status keeps its own non-rollup readers (the child lookup the
# removal's own commit preserved), so the module is not simply gone.
assert callable(task_status.find_child_tasks)

View file

@ -5,7 +5,6 @@ from pathlib import Path
from types import SimpleNamespace
import pytest
from tests._governance_docs_shared import architecture_text, development_text
def test_partial_trajectory_is_non_resolving_but_complete_trajectory_resolves():
@ -687,15 +686,6 @@ def test_budget_ladder_stops_shedding_after_predecessor_fits():
)
def test_acceptance_docs_have_complete_sentence_boundaries():
development = development_text()
architecture = architecture_text()
assert "silent false green. The\n Every forced rail" not in development
assert "never certify success; An OPEN plan wave" not in architecture
assert "never had claims. an unresolved reference" not in architecture
def test_refused_slot_reuses_the_already_persisted_prompt(monkeypatch, tmp_path):
import ouroboros.review_substrate as substrate

View file

@ -75,9 +75,26 @@ def test_the_sweep_never_follows_a_symlink_out_of_the_drive(tmp_path):
assert report == {"removed": 1, "kept": 0, "skipped": 2, "errors": 0}
def test_startup_prune_sweeps_run_the_media_prune():
import inspect
def test_startup_prune_sweeps_run_the_media_prune(tmp_path, monkeypatch):
import ouroboros.server_maintenance as sm
assert "prune_agent_media_uploads" in inspect.getsource(sm._run_deferred_startup_prunes)
shots = tmp_path / "uploads" / "screenshots"
shots.mkdir(parents=True)
old_shot = shots / "old.png"
old_shot.write_bytes(b"agent image")
owner_upload = shots.parent / "owner.png"
owner_upload.write_bytes(b"owner image")
for path in (old_shot, owner_upload):
os.utime(path, (_OLD, _OLD))
monkeypatch.setattr(sm, "DATA_DIR", tmp_path)
monkeypatch.setattr(sm, "_STARTUP_PRUNES_OWED", [True])
monkeypatch.setattr(sm, "_STARTUP_TREES_OWED", [False])
monkeypatch.setattr(sm, "_STARTUP_SOURCE_PRUNES_OWED", [False])
monkeypatch.setattr(sm, "_startup_worktree_prune", lambda: None)
monkeypatch.setattr(sm, "_prune_delegated_snapshots", lambda: None)
sm._run_deferred_startup_prunes()
assert not old_shot.exists()
assert owner_upload.read_bytes() == b"owner image"

View file

@ -66,39 +66,6 @@ def _read(name: str) -> str:
return (MODULES / name).read_text(encoding="utf-8")
def _swept_sources(*, with_tests: bool) -> list[tuple[str, str, str]]:
"""(label, text, line-comment marker) for every source an address can hide in.
The sweep this file first shipped looked at FOUR modules. That is precisely
why a stale pointer survived in ``ouroboros/tools/plan_review.py``, which no
sweep was looking at: the owner was told to "Fix Reviewer Slots in Settings"
long after the section stopped existing under that name. So the scan is now
the whole surface this repo authors, not the files the move happened to touch.
``with_tests`` is off for the copy sweep and on for the address sweep. A test
asserting that a retired WORD is absent has to spell that word; a test
carrying a retired ADDRESS in a comment misdirects its next reader exactly
like production code does.
"""
out: list[tuple[str, str, str]] = []
groups: list[tuple[list[pathlib.Path], str | None]] = [
(sorted((REPO_ROOT / "web" / "modules").glob("*.js")), "//"),
(sorted((REPO_ROOT / "web").glob("*.css")), None),
(sorted((REPO_ROOT / "ouroboros").rglob("*.py")), "#"),
(sorted((REPO_ROOT / "supervisor").rglob("*.py")), "#"),
]
if with_tests:
groups.append((sorted((REPO_ROOT / "web" / "tests").glob("*.js")), "//"))
groups.append((sorted((REPO_ROOT / "tests").glob("*.py")), "#"))
for paths, comment in groups:
for path in paths:
if path.resolve() == pathlib.Path(__file__).resolve():
continue
out.append((str(path.relative_to(REPO_ROOT)),
path.read_text(encoding="utf-8"), comment or "\0"))
return out
def _page_markup() -> str:
return "\n".join(_read(name) for name in PAGE_SOURCES)
@ -224,80 +191,6 @@ def test_the_two_path_roots_are_behind_a_collapsed_disclosure() -> None:
)
# ---------------------------------------------------------------------------
# The naming sweep
# ---------------------------------------------------------------------------
def test_no_owner_facing_coding_agent_copy_remains_on_the_agents_surfaces() -> None:
"""D-10, owner verbatim: «мне не нравится "coding" потому что эти агенты не
только для кода используются, например для создания презентаций и любые
tasks». Product names (Claude Code, Codex, Cursor) are unaffected — they are
trademarks, not the generic label.
Scoped to OWNER-FACING copy: comments are skipped, because a comment that
explains why the word was retired has to be able to spell it. NOT scoped to
the modules this phase happened to edit — a sweep narrowed to the changed
files only proves the change, never the rule."""
offenders: list[str] = []
for label, text, comment in _swept_sources(with_tests=False):
for lineno, line in enumerate(text.splitlines(), 1):
if line.lstrip().startswith(comment):
continue
if re.search(r"coding[- ]agent", line, re.IGNORECASE):
offenders.append(f"{label}:{lineno}: {line.strip()}")
assert not offenders, (
"owner-facing copy still says \"coding agent\" (D-10)\n" + "\n".join(offenders))
# Every address this phase INVALIDATED, with the place that replaced it. A
# pointer is not decoration: it is an instruction, and one that names a heading
# no tab carries wastes the owner's time and quietly claims the feature is
# missing. Comments count — a developer sent to the Models page for the reviewer
# rows loses the same minutes the owner does.
STALE_ADDRESSES = (
(r"Reviewer Slots",
"reviewers are the rows marked Reviewer in Agents → Available subagents"),
(r"Models[- ]page|Models tab \(Reviewer",
"the reviewer rows and the subagent route left the Models page for Agents (D-10)"),
(r"Providers\s*(?:→|->)\s*Harness Accounts",
"accounts are Agents → Accounts now, grouped per family (D-10)"),
(r"Models\s*(?:→|->)\s*(?:Subagents|Reviewer)",
"delegation and reviewers are Agents → Available subagents (D-10)"),
)
def _stale_address_hits(text: str) -> list[tuple[int, str, str]]:
hits: list[tuple[int, str, str]] = []
for lineno, line in enumerate(text.splitlines(), 1):
for pattern, correction in STALE_ADDRESSES:
if re.search(pattern, line):
hits.append((lineno, line.strip(), correction))
return hits
def test_no_source_still_sends_anyone_to_a_section_this_phase_removed() -> None:
"""The class the narrow sweep let through.
D-10 moved Harness Accounts out of Providers and renamed Reviewer Slots, and
the addresses that named those places stayed behind — in owner-facing copy
(``plan_review`` told the owner to "Fix Reviewer Slots in Settings") and in
the comments a maintainer navigates by. Both are wrong in the same way, so
both are swept, across every source this repo authors rather than the
handful the move touched.
Historical narration is welcome — but it must describe the old place
("the accounts section under the Providers tab") rather than repeat the dead
address verbatim, so a reader searching for a heading finds only live ones.
"""
offenders: list[str] = []
for label, text, _comment in _swept_sources(with_tests=True):
for lineno, line, correction in _stale_address_hits(text):
offenders.append(f"{label}:{lineno}: {line}\n → {correction}")
assert not offenders, (
"a stale section address survived the move:\n" + "\n".join(offenders))
def test_a_spent_window_is_emphasised_rather_than_dimmed() -> None:
"""Caught by looking at the rendered tab: `.harness-exhausted` dimmed the whole
row to 0.62 opacity, which greyed out the very sentence reporting the limit
@ -323,23 +216,3 @@ def test_a_spent_window_is_emphasised_rather_than_dimmed() -> None:
"sentence and disables-looking buttons that still work"
)
assert "var(--status-warn-" in rule.group(1)
def test_the_reviewer_disclosure_stopped_advising_against_the_default() -> None:
"""The sentence used to end "keep at least one API reviewer row to avoid the
fallback", which asks the owner to undo the ratified default (D-3: with a
subscription connected, everything that can run on one does, and a triad is
never half API and half subscription). Its carrier — the conditional
all-delegated warning about a task-acceptance API fallback — is gone with the
fallback itself (owner R2/R12, 2026-09-01): acceptance follows the rows. The
ONE-TIME packet→retrieving migration disclosure that was the last server-side
sentence left with the review lanes (PR-3): delivery is a field of the catalog
row the owner saves, so no sentence advises for or against a delivery."""
config = (REPO_ROOT / "ouroboros" / "reviewer_slot_config.py").read_text(encoding="utf-8")
assert "Keep at least one API reviewer row" not in config
assert "never fall back" not in config and "stays API-only" not in config
assert "acceptance_delivery_disclosure" not in config and "Keep an api_chat row" not in config
# Reviewers are catalog rows now; their editor carries the standing rule.
editor = " ".join(_read("subagents_settings.js").split())
assert "keep at least one API row" not in editor.lower()
assert "waits for capacity rather than silently falling back to API spend" in editor

View file

@ -49,14 +49,6 @@ def test_every_first_party_window_uses_a_persistent_profile():
assert calls == 5, f"expected 5 first-party webview.start() calls, found {calls}"
def test_the_owner_approved_cost_is_written_down_where_it_is_paid():
# A flag that makes local site data persist on disk is an owner decision,
# not an implementation detail; the rationale must survive the next reader.
main = _read("launcher.py")
assert "cookies and website data" in main
assert "ouroboros.theme" in main
def test_the_select_arrow_is_a_theme_token_not_a_baked_colour():
ui = _read("web/ui.css")
dark, light = ui.split(':root[data-theme="light"]', 1)

View file

@ -51,6 +51,8 @@ def reexports(manifest):
# ---------------------------------------------------------------------------
def test_owner_of_resolves_a_module_path_a_dotted_module_and_a_symbol():
from ouroboros.code_intelligence import build_code_inventory
(llm,) = owner_of(REPO, "ouroboros/llm.py")
assert (llm.domain, llm.via) == ("D02", "module_path")
assert llm.domain_title # the human vocabulary rides along
@ -60,7 +62,14 @@ def test_owner_of_resolves_a_module_path_a_dotted_module_and_a_symbol():
"supervisor/state.py", "D08", "dotted_module",
)
owners = owner_of(REPO, "protected_path_category")
# This oracle asks about one real definition, not every repository language.
# Keep the live carrier/parse witness without indexing tests, JS and fixtures;
# the tiny repository below exercises owner_of's default inventory path.
inventory = build_code_inventory(
REPO, scope="ouroboros/runtime_mode_policy.py", persist=False,
)
assert not inventory.enumeration_limits
owners = owner_of(REPO, "protected_path_category", inventory=inventory)
assert any(
(o.module, o.domain, o.via) == ("ouroboros/runtime_mode_policy.py", "D13", "symbol_definition")
for o in owners
@ -342,6 +351,10 @@ def test_query_code_owner_of_qualifies_partial_symbol_coverage(tmp_path, monkeyp
(repo / "ouroboros" / "a.py").write_text("def shared():\n pass\n", encoding="utf-8")
(repo / "ouroboros" / "z.py").write_text(
"def shared():\n pass\n\n\ndef late():\n pass\n", encoding="utf-8")
assert [(row.module, row.via) for row in owner_of(repo, "shared")] == [
("ouroboros/a.py", "symbol_definition"),
("ouroboros/z.py", "symbol_definition"),
]
registry = ToolRegistry(repo_dir=repo, drive_root=tmp_path / "data")
def owner(arg):

View file

@ -16,7 +16,6 @@ def _read(path: pathlib.Path) -> str:
def test_available_subagents_is_one_canonical_settings_editor() -> None:
editor = _read(MODULES / "subagents_settings.js")
host = _read(MODULES / "settings.js")
assert "<h3>Available subagents</h3>" in editor
assert "OUROBOROS_SUBAGENTS" in editor
assert "collectSubagentsSettings" in host
assert "OUROBOROS_SUBAGENT_HARNESS" not in editor
@ -51,17 +50,12 @@ def test_a_fresh_subagent_row_invites_and_only_a_save_attempt_makes_it_red() ->
assert "row._uiAttempted = true" in editor
assert "Boolean(row._uiAttempted) && " in editor
assert "row._uiAttempted && errors.length" in primitives
assert "Choose how this subagent runs: an API model or an agent session." in primitives
host = _read(MODULES / "settings.js")
# Every Save click is an attempt — including one another field's validation
# then aborts — so the stamp precedes the cadence check's early return.
save = host[host.index("byId('btn-save-settings').addEventListener"):]
assert save.index("noteSubagentsSaveAttempt();") < save.index("const { messages: errors, subject } = renderValidation();")
assert "Every-N cadence needs" in host
assert "agentsStep?.noteSaveAttempt?.();" in _read(MODULES / "onboarding_wizard.js")
# Errors name the card the way its heading does, never a bare "Row N".
assert "`Subagent ${index + 1} ${text}`" in editor
assert "`Row ${index + 1}`" not in editor
def test_the_review_pool_is_a_catalog_mark_and_the_route_primitives_stay_reviewer_free() -> None:
@ -82,19 +76,14 @@ def test_the_review_pool_is_a_catalog_mark_and_the_route_primitives_stay_reviewe
def test_heavy_card_is_gone_but_provider_test_contract_and_controls_remain() -> None:
ui = _read(MODULES / "settings_ui.js")
host = _read(MODULES / "settings.js")
setup = _read(ROOT / "ouroboros" / "settings_setup_contract.py")
assert "['Heavy'," not in ui
assert "s-model-heavy" not in ui
assert "OUROBOROS_MODEL_HEAVY" not in host
light_copy = "Fast summaries, lightweight internal work and the default Fast scout. Empty uses Main."
# Role labels and copy come from the shared setup contract, not a second
# hand-maintained card table in Settings.
assert "modelRolesHost('settings-model-roles')" in ui
assert "modelRoles.load(s," in host
assert "slot.note" in _read(MODULES / "model_roles.js")
assert light_copy in setup
assert "all deep subagents" not in ui
assert "all deep subagents" not in setup
assert "data-provider-test" in ui
assert "PROVIDER_TEST_INPUTS" in ui
assert "apiClient.providerTest({ provider_id: provider, overrides })" in host
@ -124,7 +113,6 @@ def test_preview_contract_and_task_only_agy_copy_are_explicit() -> None:
assert "{ harness: 'agy' }" in step
assert "familyLabel(family.harness" in step
assert "{ harness: 'agy', label:" not in step
assert "task-only and does" in step
assert "@typedef {Object} SubagentLastDelegation" in types
assert "@property {SubagentLastDelegation=} subagent_last_delegation" in types
@ -142,11 +130,7 @@ def test_generated_preview_is_background_and_whole_draft_clean_gated() -> None:
def test_status_refresh_and_active_task_copy_keep_the_frozen_semantics() -> None:
editor = _read(MODULES / "subagents_settings.js")
host = _read(MODULES / "settings.js")
assert "await boundedStatusRefresh(store);" in editor
assert "Saved rows remain unchanged" in editor
assert "take effect for new child tasks" in host
assert "the current task keeps its existing routes" in host
def test_new_frontend_modules_stay_within_the_context_target() -> None:

View file

@ -474,14 +474,12 @@ def test_legacy_heavy_read_vocabulary_does_not_leak_into_new_projection(
def test_clb_operator_adapters_transport_canonical_actor_without_active_heavy():
patch_root = REPO / "devtools/benchmarks/continual_learning/operator_patches"
host_patch = (patch_root / "_launcher.v6560.patch").read_text(encoding="utf-8")
docker_patch = (patch_root / "clb_env_campaign_overrides.v6745.patch").read_text(
encoding="utf-8"
)
official_patch = (patch_root / "adapter_official_submission.v681.patch").read_text(
encoding="utf-8"
)
assert "Canonical benchmark actor bytes are authored by run_clb.py" in host_patch
assert '"OUROBOROS_SUBAGENTS"):' in docker_patch
assert "OUROBOROS_SUBAGENTS=os.environ.get" in official_patch
assert "OUROBOROS_MODEL_HEAVY" not in official_patch

View file

@ -633,16 +633,6 @@ def test_activity_rows_show_a_held_budget_row_as_paused_not_queued():
assert "|| heldRow(t)" in source # consulted by the pending-row pause predicate
def test_runbook_does_not_promise_a_managed_outage_window_the_runtime_has_no_rail_for():
"""F8: with no deadline and an unlimited absolute ceiling, a managed task's
transport-outage episode has NO window of its own — the 6h operation-window
fallback belongs to other operations. The runbook names the optional rails an
operator can set instead of promising a timeout that does not exist."""
source = _repo_file("devtools", "benchmarks", "continual_learning", "RUNBOOK.md")
assert "6h operation window from episode entry" not in source
assert "OUROBOROS_TASK_ABS_CEILING_SEC" in source and "idle reaper" in source
# --------------------------------------------------------------------------- consumption publication holds
def _granted_loop(tmp_path, monkeypatch, task_id):

View file

@ -35,12 +35,6 @@ def _read(rel: str) -> str:
# ────────── Bug 1 (polish): the retired loop's rows replay honestly ──────────
def test_log_events_has_no_background_loop_projector():
src = _read("web/modules/log_events.js")
assert "consciousness_state" not in src
assert "bg-consciousness" not in src
def test_history_never_fabricates_a_terminal_status_for_legacy_bg_rows(tmp_path):
from ouroboros.gateway.history import make_chat_history_endpoint

View file

@ -59,16 +59,9 @@ class TestBuildSh:
assert "playwright install --only-shell chromium" in src, (
"build.sh must call 'playwright install --only-shell chromium' on macOS"
)
assert "Skipping bundled WebKit on macOS" in src, (
"build.sh must document the macOS WebKit packaging policy"
)
assert "Removing stale bundled WebKit payloads from macOS package tree" in src
assert 'rglob(".local-browsers")' in src
assert 'glob("webkit-*")' in src
assert "shutil.rmtree" in src
assert "managed" in src and "Playwright cache" in src, (
"build.sh must leave WebKit available through the managed runtime cache"
)
assert "playwright install webkit" not in src
assert "pw.webkit.launch" not in src
@ -653,7 +646,6 @@ class TestRepoBundleReleaseTagGuard:
def test_dirty_tree_refusal_reports_the_offending_paths(self):
src = _read("scripts/build_repo_bundle.py")
assert '"status", "--porcelain"' in src
assert '"Commit or stash changes first so the embedded managed repo matches the packaged code.\\n"' in src
assert "+ status" in src

View file

@ -1,10 +1,9 @@
"""WS2-e (capinv-447) contract tests: one-author projections + honest results.
Covers H2 (write-like typing across the two profile projections), H3 (module
load failures reach the capability ledger and survive schemas() rebuilds), H4
(typed preflight_blocked reason_kind), H5 (repo_commit_ready SSOT is
enforcement-aware), D3 (search filter receipt), D4 (export member policy reuses
the workspace-patch SSOT), G10 (attachment routes unified + rule-named
load failures reach the capability ledger and survive schemas() rebuilds),
D3 (search filter receipt), D4 (export member policy reuses the workspace-patch
SSOT), G10 (attachment routes unified + rule-named
reasons), G3 (npm version pins + manual-dependency disclosure), and E5 (MCP
pagination, injective tool slugs, resource/structuredContent fidelity).
"""
@ -115,21 +114,6 @@ def test_module_load_failure_recorded_and_survives_schema_rebuilds(tmp_path, mon
# H5 — the retired advisory gate (3A) projects no readiness axis anywhere
# ---------------------------------------------------------------------------
def test_the_retired_advisory_gate_has_no_readiness_projection_left():
"""Decision 3A retired the advisory gate; its `repo_commit_ready` mirror said
"yes" on every surface and made the author chase a readiness that nothing
decides. Neither the function nor the context heading exists any more."""
import inspect
from ouroboros import agent_task_pipeline, review_state
assert not hasattr(review_state, "advisory_commit_ready")
source = inspect.getsource(agent_task_pipeline)
assert "Live repo gate" not in source
assert "Advisory readiness" not in source
assert "repo_commit_ready" not in source
# ---------------------------------------------------------------------------
# D3 — search filter receipt
# ---------------------------------------------------------------------------

View file

@ -1,8 +1,8 @@
"""The layered change-review checklist (docs/CHECKLISTS.md): the universal
`Change Review Checklist` core every reviewed change carries, and the
`Ouroboros Body Layer` appended only when the subject IS the body
(`review_body_fact.layer_for`). Pins the loader, the numbering contract, the
fingerprint and the retirement of the former single-section name."""
(`review_body_fact.layer_for`). Pins the loader, the numbering contract, and the
fingerprint."""
from __future__ import annotations
@ -24,7 +24,6 @@ from ouroboros.tools.review_helpers import (
REPO = pathlib.Path(__file__).resolve().parent.parent
CHECKLIST = REPO / "docs" / "CHECKLISTS.md"
RETIRED_SECTION_NAME = "Repo Commit" + " Checklist" # spelled apart so this file never carries the name
ITEM_ROW = re.compile(r"^\| (\d+) \| ([a-z_]+) \|", re.MULTILINE)
@ -85,16 +84,6 @@ def test_the_core_layer_names_no_body_document():
assert marker not in core, marker
def test_the_retired_section_name_is_gone_from_the_executing_code_and_docs():
hits: list[str] = []
for tree in ("ouroboros", "prompts", "docs"):
for path in sorted((REPO / tree).rglob("*")):
if path.is_file() and path.suffix in {".py", ".md", ".txt", ".toml", ".json"}:
if RETIRED_SECTION_NAME in path.read_text(encoding="utf-8", errors="replace"):
hits.append(path.relative_to(REPO).as_posix())
assert hits == [], hits
def test_fingerprint_names_the_rules_that_ran():
core = checklist_fingerprint("core")
body = checklist_fingerprint("body")

View file

@ -1058,12 +1058,28 @@ def test_every_canonical_store_publisher_takes_the_one_custody_lock(tmp_path):
settlement, so it takes the same custody lock and answers ``CustodyBusy`` while it is held."""
data, drive, _store = _nested_child(tmp_path)
task = {"id": TASK, "drive_root": str(drive)}
lock_budget = 0.01 # Exercise real contention without waiting out the production budget.
with task_custody.task_custody_lock(data, TASK) as locked:
assert locked
with pytest.raises(task_custody.CustodyBusy):
headless.finalize_task_artifacts(data, task)
headless.finalize_task_artifacts(data, task, lock_timeout_sec=lock_budget)
assert not (artifacts.task_artifact_dir_path(data, TASK) / "memory_export.json").exists()
assert headless.finalize_task_artifacts(data, task) # released: the same work completes
assert headless.finalize_task_artifacts(data, task, lock_timeout_sec=lock_budget) # released: the same work completes
def test_artifact_publication_keeps_the_production_lock_budget(tmp_path, monkeypatch):
from contextlib import nullcontext
budgets = []
def unavailable(_root, _task_id, *, timeout_sec):
budgets.append(timeout_sec)
return nullcontext(False)
monkeypatch.setattr(task_custody, "task_custody_lock", unavailable)
with pytest.raises(task_custody.CustodyBusy):
headless.finalize_task_artifacts(tmp_path, {"id": TASK})
assert budgets == [30.0] # Production publication may wait for a competing writer.
@pytest.mark.parametrize("when", ["before_the_custody_lock", "before_the_row_lock"])

View file

@ -380,7 +380,8 @@ def _run_shell(step, tmp_path, *, diagnostic="viewport", result=0, scope="all"):
def test_actual_producer_shell_preserves_success_and_failure_exit(tmp_path, workflow, job, step, result):
completed, args = _run_shell(_steps(workflow, job)[step], tmp_path, result=result)
assert completed.returncode == result, completed.stderr
assert args.count("pytest") == 1 and "summarize" not in args
# One producer: pytest itself, or the owned runner that forwards its pytest arguments.
assert args.count("pytest") + args.count("scripts/run_tests.py") == 1 and "summarize" not in args
OFFICIAL, FORK, TAG = "razzant/ouroboros", "someone/private-copy", "refs/tags/v7.5.2"
@ -714,11 +715,38 @@ def test_ordinary_jobs_name_their_failures_without_owning_the_result(jobname, ar
evidence = next(step for step in job["steps"] if step.get("uses") == EVIDENCE_ACTION)
assert evidence["continue-on-error"] is True and "!cancelled()" in evidence["if"]
assert evidence["with"] == {"name": artifact, "passes": " ".join(
f"{label}=${{{{ steps.tests_{label}.outcome }}}}" for label in PASSES)}
f"{label}=${{{{ " + (f"github.event_name == 'pull_request' && (steps.tests_selected.outputs.{label}_outcome"
" || steps.tests_selected.outcome) || "
if jobname == "quick-test" and label != "size" else "")
+ f"steps.tests_{label}.outcome }}}}" for label in PASSES)}
assert job["steps"].index(evidence) > max(job["steps"].index(steps[f"tests_{label}"])
for label in PASSES)
def test_pull_requests_run_the_selected_scope_on_windows_and_macos():
job = _workflow("ci.yml")["jobs"]["pr-selected-os"]
assert "pull_request" in job["if"] and job["strategy"]["matrix"]["os"] == ["windows-latest", "macos-latest"]
producer = _steps("ci.yml", "pr-selected-os")["tests_selected"]
assert "scripts/run_tests.py --selected --base ${{ github.event.pull_request.base.sha }} --sequential" in producer["run"]
# Unsharded Windows cannot finish a full escalation in its ceiling; its four landed shards run it.
assert producer["run"].endswith("${{ runner.os == 'Windows' && '--defer-full' || '' }}")
evidence = next(step for step in job["steps"] if step.get("uses") == EVIDENCE_ACTION)
# A step killed before run_tests.py writes its outputs still reports through its own outcome.
assert evidence["with"]["passes"] == " ".join(
f"{label}=${{{{ steps.tests_selected.outputs.{label}_outcome || steps.tests_selected.outcome }}}}"
for label in ("parallel", "serial"))
@pytest.mark.parametrize("job", ["quick-test", "full-test-runs"])
def test_every_landed_test_producer_runs_under_the_owned_runner(job):
"""One owner per test run: the full passes and the Node lane run through scripts/run_tests.py."""
steps = _workflow("ci.yml")["jobs"][job]["steps"]
producers = [step["run"] for step in steps if step.get("id") in {"tests_parallel", "tests_serial"}]
node = [step["run"] for step in steps if step.get("name", "").startswith("Run browser-module tests")]
assert len(producers) == 2 and all(run.startswith("python scripts/run_tests.py tests/ -m ") for run in producers)
assert node == ["python scripts/run_tests.py --node"]
def test_evidence_action_steps_are_independent_diagnostics():
upload, report = _evidence_action()["runs"]["steps"]
for step in (upload, report): # Neither waits for the other's success nor fails the caller.

View file

@ -359,9 +359,22 @@ def test_successful_upload_without_a_url_is_incomplete(tmp_path):
("import time; time.sleep(60)", ("-n", "2", "--dist", "loadscope", "--max-worker-restart=0",
"--timeout=2", "--timeout-method=thread")),
], ids=["exit", "ci-timeout"])
def test_dead_xdist_worker_is_a_named_crash_and_not_an_unknown_session(tmp_path, body, args):
def test_dead_xdist_worker_is_a_named_crash_and_not_an_unknown_session(tmp_path, monkeypatch, body, args):
monkeypatch.setenv("OURO_TEST_WORKER_BUDGET", "2")
result, public, _ = _producer(tmp_path, f"""
def test_dies():
import json, time
from pathlib import Path
deadline = time.monotonic() + 10
while True:
try:
rows = [json.loads(line) for line in Path({str(tmp_path / 'public/events.jsonl')!r}).read_text(encoding='utf-8').splitlines()]
except (OSError, ValueError):
rows = []
if any(row.get('phase') == 'teardown' and row.get('nodeid', '').endswith('::test_survives') for row in rows):
break
assert time.monotonic() < deadline
time.sleep(.01)
{body}
class TestOtherScope:
def test_survives(self, request):
@ -437,7 +450,8 @@ def test_killed_session_names_the_test_in_flight_and_earlier_failures(tmp_path):
f"::error title=Test in flight when the session was killed::{blocked}"]
def test_killed_xdist_session_names_every_test_in_flight(tmp_path):
def test_killed_xdist_session_names_every_test_in_flight(tmp_path, monkeypatch):
monkeypatch.setenv("OURO_TEST_WORKER_BUDGET", "2")
public = _kill_when(tmp_path, _blocking(tmp_path, """
def test_blocks_a():
block()

View file

@ -13,6 +13,7 @@ import yaml
from ouroboros.preflight_runner import LANE_EXCLUSION_EXPR, PARALLEL_PASS_FLAGS
from tests.ci_evidence import file_shards, reconcile_shards
from ouroboros.test_parallelism import NESTED_WORKERS_ENV
ROOT = Path(__file__).resolve().parents[1]
SHA, RUN = "a" * 40, "42"
@ -69,6 +70,7 @@ def test_real_pytest_manifests_and_xdist_prove_both_passes_by_whole_file(tmp_pat
result = subprocess.run(
[sys.executable, "-I", "-S", str(ROOT / "scripts/safe_test.py"),
"--temp-parent", str(tmp_path), "--", sys.executable, str(entry), *args],
env={**os.environ, NESTED_WORKERS_ENV: "2"},
capture_output=True, text=True, encoding="utf-8", timeout=90)
assert result.returncode == 0, result.stdout + result.stderr
data = json.loads((public / "results.json").read_text(encoding="utf-8"))
@ -205,7 +207,7 @@ def test_windows_matrix_keeps_the_same_passes_and_once_per_os_checks(tmp_path):
for name in ("book_mode", "tests_size", "attachment_browser_install", "attachment_browser", "composer_browser"):
assert "matrix.shard <= 1" in steps[name]["if"]
for step in job["steps"]:
if ("node --test" in step.get("run", "") or "lint:undef" in step.get("run", "")
if ("run_tests.py --node" in step.get("run", "") or "lint:undef" in step.get("run", "")
or "test_no_core_a2a_telegram_imports.py" in step.get("run", "")):
assert "matrix.shard <= 1" in step["if"]
mandatory = [step for step in job["steps"] if step.get("name", "").startswith(("Require both Windows", "Upload the Windows"))]

View file

@ -516,7 +516,6 @@ def test_three_os_gate_installs_the_reviewed_runtime_instead_of_floating_npm():
assert '--effort "${{ matrix.effort }}"' in workflow
assert "- os: ubuntu-latest\n harness: codex" in workflow
assert "- os: windows-latest\n harness: codex" in workflow
assert "vendor-broken on native Windows" in workflow
for dependency_surface in (
".github/actions/setup-python-env/**",
"pyproject.toml",

View file

@ -70,8 +70,3 @@ def test_every_named_websocket_type_is_indexed_and_every_section_reference_exist
headings = {int(n) for n in re.findall(r"^## (\d+)\. ", DOC, re.M)}
for reference in re.findall(r"§(\d+)", DOC):
assert int(reference) in headings, f"§{reference} does not exist"
def test_the_document_no_longer_calls_any_route_untyped():
assert "typed: false" not in DOC and "Status: partial" not in DOC
assert "WIDGET_HOST_PROTOCOL.md" in DOC and "Idempotency-Key" in DOC

View file

@ -508,18 +508,6 @@ def test_chat_history_does_not_emit_client_surface(tmp_path):
# Prompt-fact parity: SYSTEM.md product claims stay bound to the code (AC8)
# ---------------------------------------------------------------------------
def test_system_prompt_reload_claim_is_code_bound():
system_md = (REPO / "prompts" / "SYSTEM.md").read_text(encoding="utf-8")
if "reload-on-SHA" not in system_md and "reloads itself" not in system_md:
return # the claim was removed together with its obligation
ws_js = (REPO / "web" / "modules" / "ws.js").read_text(encoding="utf-8")
assert "sha-change" in ws_js or "Reload on SHA change" in ws_js, (
"SYSTEM.md claims the UI auto-reloads on SHA change, but the ws.js "
"reload path is gone — the prompt now states a false product fact "
"(the exact failure class this feature exists to close)"
)
def test_frontend_sends_raw_observables_without_device_taxonomy():
chat_js = (REPO / "web" / "modules" / "chat.js").read_text(encoding="utf-8")
surface_js = (REPO / "web" / "modules" / "client_surface.js").read_text(encoding="utf-8")

View file

@ -1,189 +1,4 @@
"""ABI 7.0 reviewer comma-list retirement checks.
Grep-level checker over ``ouroboros/`` + ``web/`` (+ ``supervisor/``) pinning
that the retired reviewer comma-list surface stays retired. Three sweeps:
1. every textual mention of a retired comma key (the list is snapped
DYNAMICALLY from ``RETIRED_COMMA_LIST_SETTING_KEYS`` — a key added to the
retirement SSOT is swept automatically) must sit on the count-anchored
allowlist of legitimate remnants below;
2. comma-``split`` parsing inside model/review modules is count-anchored to
the known derived-plane parsers — the review-configuration modules
themselves carry NO comma parsing (the structured
``OUROBOROS_REVIEWER_SLOTS`` is the one configuration surface);
3. removed route plumbing stays removed, and the retired
per-row route envs are IGNORED at runtime (retired-envs-are-ignored pin).
Allowlist discipline follows tests/test_gateway_abi3_removals.py: PER-SITE and
COUNT-ANCHORED — a new mention in an allowlisted file breaks the anchor and
fails; a stale row (no mention matches) fails too, so the allowlist can only
shrink deliberately.
"""
from __future__ import annotations
import ast
import pathlib
import re
from collections import Counter
from ouroboros.settings_defaults import RETIRED_COMMA_LIST_SETTING_KEYS
_ROOT = pathlib.Path(__file__).resolve().parents[1]
_SWEEP_BASES = ("ouroboros", "web", "supervisor")
_SWEEP_SUFFIXES = (".py", ".js", ".html", ".css")
def _sweep_files():
for base in _SWEEP_BASES:
for path in sorted((_ROOT / base).rglob("*")):
if path.suffix in _SWEEP_SUFFIXES and path.is_file():
yield path
# (posix path, retired key) -> (reason, exact mention count).
# Every row belongs to one of these legitimate remnant classes:
# retirement-SSOT — the list that declares the keys retired;
# derived env plane — the comma ENV spellings of the two model lists live on
# as the runtime projection for the API-pinned surfaces (never settings);
# raw-dict tolerance — normalizers that accept a raw dict fed directly
# (tests/tools), ABI-10-commented in place;
# retirement prose — comments/docstrings that NAME the key to say it is
# retired and ignored.
_RETIRED_KEY_MENTION_ALLOWLIST = {
# -- retirement SSOT (declares the keys retired; feeds the RC auditor).
("ouroboros/settings_defaults.py", "OUROBOROS_REVIEW_MODELS"): ("retirement SSOT", 2),
("ouroboros/settings_defaults.py", "OUROBOROS_SCOPE_REVIEW_MODELS"): ("retirement SSOT", 2),
("ouroboros/settings_defaults.py", "OUROBOROS_SCOPE_REVIEW_MODEL"): ("retirement SSOT", 2),
("ouroboros/settings_defaults.py", "OUROBOROS_REVIEW_ROUTES"): ("retirement SSOT", 2),
("ouroboros/settings_defaults.py", "OUROBOROS_SCOPE_REVIEW_ROUTES"): ("retirement SSOT", 2),
("ouroboros/settings_defaults.py", "OUROBOROS_ADVISORY_REVIEW_ROUTE"): ("retirement SSOT", 2),
# -- raw-dict tolerance: direct-provider and prior-default review/scope
# normalization over dicts fed directly (load_settings purges the keys
# first; ABI-10-commented).
("ouroboros/server_runtime.py", "OUROBOROS_REVIEW_MODELS"): ("raw-dict review normalization", 5),
("ouroboros/server_runtime.py", "OUROBOROS_SCOPE_REVIEW_MODELS"): ("raw-dict scope review normalization", 7),
("ouroboros/server_runtime.py", "OUROBOROS_SCOPE_REVIEW_MODEL"): ("raw-dict scope review normalization", 7),
# -- retirement prose (names the key to say it is retired/ignored).
("ouroboros/review_execution.py", "OUROBOROS_REVIEW_ROUTES"): ("retirement prose", 1),
("ouroboros/review_execution.py", "OUROBOROS_SCOPE_REVIEW_ROUTES"): ("retirement prose", 1),
}
def test_retired_comma_key_mentions_are_allowlisted():
keys = sorted(RETIRED_COMMA_LIST_SETTING_KEYS, key=len, reverse=True)
assert keys, "retirement SSOT list is empty — the sweep has no subject"
observed: Counter = Counter()
for path in _sweep_files():
text = path.read_text(encoding="utf-8", errors="replace")
rel = path.relative_to(_ROOT).as_posix()
for key in keys:
# Negative lookahead keeps OUROBOROS_SCOPE_REVIEW_MODEL from also
# counting every OUROBOROS_SCOPE_REVIEW_MODELS mention.
count = len(re.findall(rf"{key}(?![A-Z_])", text))
if count:
observed[(rel, key)] = count
expected = {site: count for site, (_reason, count) in _RETIRED_KEY_MENTION_ALLOWLIST.items()}
new_sites = {site: n for site, n in observed.items() if site not in expected}
assert not new_sites, f"NEW retired comma-key mentions (extend or clean): {new_sites}"
stale = {site: n for site, n in expected.items() if site not in observed}
assert not stale, f"stale allowlist rows (remnant gone — delete the row): {stale}"
drifted = {site: (observed[site], expected[site])
for site in observed if observed[site] != expected[site]}
assert not drifted, f"mention count drifted (observed, allowlisted): {drifted}"
# posix path -> (reason, exact count of comma-split call sites). The review
# configuration modules themselves must stay comma-split-free.
_COMMA_SPLIT_ALLOWLIST = {
"ouroboros/model_slots.py": ("generic model-list parser (OUROBOROS_MODEL_FALLBACKS et al.)", 1),
"ouroboros/provider_models.py": ("declared-model list parser over the derived plane", 1),
"ouroboros/gateway/models.py": ("Provider Test deterministic-model resolver", 1),
"web/modules/model_roles.js": ("existing ordered fallback model setting, never reviewer configuration", 1),
}
def _count_comma_split_calls_py(text: str) -> int:
"""AST-level count of ``<expr>.split(",")`` / ``.rsplit(",")`` calls —
positional or keyword extras (``maxsplit=``), spacing and quote style
cannot evade it (the setattr-scan lesson: a parser gate scans SYNTAX,
not one string spelling). The separator may arrive as EITHER of the
first two positionals (bound ``s.split(",")`` / unbound
``str.split(raw, ",")``) or the ``sep=`` keyword regardless of
positional count (unbound ``str.split(raw, sep=",")``)."""
tree = ast.parse(text)
count = 0
for node in ast.walk(tree):
if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)):
continue
if node.func.attr not in ("split", "rsplit"):
continue
sep_candidates = list(node.args[:2]) + [
kw.value for kw in node.keywords if kw.arg == "sep"
]
if any(
isinstance(candidate, ast.Constant) and candidate.value == ","
for candidate in sep_candidates
):
count += 1
return count
def test_comma_split_model_parsing_is_allowlisted():
observed: Counter = Counter()
for path in _sweep_files():
rel = path.relative_to(_ROOT).as_posix()
if not re.search(r"model|review", rel):
continue
text = path.read_text(encoding="utf-8", errors="replace")
if path.suffix == ".py":
count = _count_comma_split_calls_py(text)
else:
# Non-Python mirrors keep the textual scan (no Python AST there).
count = len(re.findall(r"""split\(\s*(?:","|',')\s*""", text))
if count:
observed[rel] = count
expected = {rel: count for rel, (_reason, count) in _COMMA_SPLIT_ALLOWLIST.items()}
assert dict(observed) == expected, (
f"comma-split drift in model/review modules: observed={dict(observed)}, "
f"allowlisted={expected}"
)
def test_comma_split_ast_scan_sees_the_evasion_spellings():
"""Self-test of the gate's detector: the plain-syntax evasions the string
regexp missed are counted, and a non-comma split is not."""
assert _count_comma_split_calls_py('raw.split(",", maxsplit=-1)') == 1
assert _count_comma_split_calls_py("raw.split( ',' )") == 1
assert _count_comma_split_calls_py("raw.rsplit(',')") == 1
assert _count_comma_split_calls_py('raw.split(sep=",")') == 1
# Unbound-method evasions (fix-round 2, claim 7): positional args present
# AND the separator hiding in sep=, or riding as the second positional.
assert _count_comma_split_calls_py('str.split(raw, sep=",")') == 1
assert _count_comma_split_calls_py('str.split(raw, ",")') == 1
assert _count_comma_split_calls_py('str.rsplit(raw, ",")') == 1
assert _count_comma_split_calls_py('raw.split(";")') == 0
assert _count_comma_split_calls_py("raw.split()") == 0
assert _count_comma_split_calls_py("str.split(raw)") == 0
def test_phase5_route_plumbing_stays_removed():
"""Removed route plumbing stays removed: no per-row route env plumbing, no
advisory route env constant, anywhere under the swept trees."""
retired_symbols = (
"configured_review_routes",
"TRIAD_REVIEW_ROUTES_ENV",
"SCOPE_REVIEW_ROUTES_ENV",
"ADVISORY_REVIEW_ROUTE_ENV",
"route_env_key",
)
hits = []
for path in _sweep_files():
text = path.read_text(encoding="utf-8", errors="replace")
for symbol in retired_symbols:
if re.search(rf"\b{symbol}\b", text):
hits.append(f"{path.relative_to(_ROOT).as_posix()}: {symbol}")
assert not hits, f"phase-5 route plumbing remnants: {hits}"
"""Retired reviewer route environment keys cannot override the catalog pool."""
def test_retired_route_envs_are_ignored(monkeypatch):

View file

@ -1,19 +1,9 @@
"""ABI 7.0 (ABI-10, owner 5.4=A): the comma-list remnant sweep — the phase CI gate.
Grep-level checker pinning that the legacy reviewer comma-list migration read
stays gone: no migration-read branches, the settings vocabulary carries the
comma keys only as RETIRED, and no bench settings template configures
reviewers through them. Live reviews read the catalog pool; historical raw-dict
normalizers may still name the comma keys. The sweep pins SETTINGS-plane and
migration-branch absence, while test_comma_list_remnant_sweep owns the textual
remnant allowlist.
"""
"""Current review-pool states and compatibility with retired comma-list settings."""
from __future__ import annotations
import json
import pathlib
import re
_ROOT = pathlib.Path(__file__).resolve().parents[1]
@ -29,32 +19,11 @@ _PHASE5_ROUTE_KEYS = (
)
def _python_files() -> list[pathlib.Path]:
files: list[pathlib.Path] = []
for base in ("ouroboros", "supervisor"):
files.extend((_ROOT / base).rglob("*.py"))
return files
def test_no_migration_read_branches_remain():
"""The reviewer_slot_config legacy block and its helpers stay deleted."""
retired_symbols = ("_legacy_rows", "_legacy_config", "_shared_session_route_spec")
hits: list[str] = []
for path in _python_files():
text = path.read_text(encoding="utf-8", errors="replace")
for symbol in retired_symbols:
if re.search(rf"\b{symbol}\b", text):
hits.append(f"{path.relative_to(_ROOT)}: {symbol}")
assert not hits, f"migration-read remnants: {hits}"
def test_review_pool_state_vocabulary_is_structured_empty_error():
"""The pool's state vocabulary is structured|empty|error — never ``legacy``."""
from ouroboros.reviewer_slot_config import review_pool_state
from tests.review_pool_rosters import pool_roster, pool_seat
text = (_ROOT / "ouroboros" / "reviewer_slot_config.py").read_text(encoding="utf-8")
assert 'source="legacy"' not in text and '"legacy"' not in text
assert review_pool_state(None)["state"] == "empty"
assert review_pool_state(pool_roster(pool_seat("a", "m/one")))["state"] == "structured"
assert review_pool_state("{not json")["state"] == "error"
@ -101,23 +70,3 @@ def test_no_bench_settings_template_configures_reviewers_via_comma_keys():
if key in data:
hits.append(f"{path.relative_to(_ROOT)}: {key}")
assert not hits, f"bench templates still carry retired comma keys: {hits}"
def test_prose_no_longer_promises_a_comma_list_migration():
"""The rewritten prose markers stay rewritten (review.py:965 class)."""
for rel in ("ouroboros/tools/review.py", "ouroboros/reviewer_slot_config.py",
"ouroboros/review_model_routes.py"):
text = (_ROOT / rel).read_text(encoding="utf-8")
assert "migrated comma-lists" not in text, rel
assert "старый читается" not in text, rel
def test_the_derived_projection_left_with_the_lanes():
"""5.4=A removed the migration READ; PR-3 removed the lane projection too — the
comma plane has no writer, and ``apply_settings_to_env`` projects no floor."""
import inspect
from ouroboros import config, reviewer_slot_config
assert not hasattr(reviewer_slot_config, "project_reviewer_slots_into_env")
assert "project_reviewer_slots_into_env" not in inspect.getsource(config.apply_settings_to_env)

View file

@ -318,16 +318,6 @@ def test_valid_capsule_recompacts_and_corrupt_capsule_stays_raw(monkeypatch, tmp
assert cc._atomic_units(corrupt_hash) == ()
def test_legacy_omission_api_is_deleted():
source = cc.pathlib.Path(cc.__file__).read_text(encoding="utf-8")
for forbidden in (
"_SUMMARY_INPUT_LIMIT", "_excerpt_for_summary", "_compact_argument_value",
"LONG_STRING", "CONTENT_OMITTED", "def compact_tool_history(",
"_round_has_protected_content", "_tool_round_spans",
):
assert forbidden not in source
def test_manual_compact_context_copy_is_truthful_about_summary_and_provenance():
from ouroboros.tools.compact_context import _compact_context, get_tools

View file

@ -322,16 +322,7 @@ def test_an_unreadable_ledger_refuses_the_start_instead_of_assuming_free(data_ro
def test_the_wake_prompt_money_framing_matches_what_the_ledger_actually_records():
"""The prompt's three money words are the reducer's, not a separate vocabulary."""
import pathlib
prompt = (pathlib.Path(__file__).resolve().parent.parent / "prompts" / "CONSCIOUSNESS.md").read_text(
encoding="utf-8")
assert "Distinguish incremental cash cost from subscription quota and time." in prompt
assert "A confirmed zero-cost call spends no cash; an undisclosed cost is unknown." in prompt
assert "Existing limits still apply." in prompt
# A confirmed zero and an undisclosed price are DIFFERENT ledger rows, which is
# exactly the distinction the sentence above asks this mind to make.
"""The reducer distinguishes free calls, unknown prices, and observed quota."""
free = {"kind": "subscription_session", "state": "settled", "cost_usd": 0.0,
"cost_final": True, "pricing_known": True}
undisclosed = {"kind": "subscription_session", "state": "settled", "cost_usd": None,
@ -340,8 +331,7 @@ def test_the_wake_prompt_money_framing_matches_what_the_ledger_actually_records(
assert _summary([free])["accounted_usd"] == 0.0 and _summary([free])["unknown_unmetered"] == 0
assert _summary([undisclosed])["unknown_unmetered"] == 1
# The allowance is the only budget the prompt promises; there is no quota one.
assert set(allowance.allowance_window.__doc__.split()) >= {"allowance"}
# The allowance is the only budget; observed quota is not another budget.
assert not hasattr(allowance, "quota_window")
# Time and quota stay separate OBSERVED axes of the same reducer, never budgets.
session = _summary([{**free, "subscription_route": "r", "subscription_reset_at": "2026-09-17T00:00:00Z"}])

View file

@ -9,7 +9,6 @@ block.
from __future__ import annotations
import inspect
import json
import pytest
@ -18,18 +17,6 @@ from ouroboros.context import build_runtime_section, build_user_content
from tests._context_shared import _make_health_env
def test_build_llm_messages_has_no_recorder_only_soft_cap_chain():
from ouroboros import context as context_module
from ouroboros.context import build_llm_messages
assert "soft_cap_tokens" not in inspect.signature(build_llm_messages).parameters
assert not hasattr(context_module, "apply_message_token_soft_cap")
source = inspect.getsource(build_llm_messages)
assert "estimated_tokens_before" not in source
assert "trimmed_sections" not in source
assert "context_fit" in source
@pytest.mark.parametrize("enforcement", ["blocking", "advisory"])
def test_force_plan_metadata_adds_structured_notice_without_rewriting_user_text(
monkeypatch, enforcement,

View file

@ -830,28 +830,7 @@ def test_clb_operator_patches_are_tracked_with_an_unambiguous_apply_order():
assert [p.name for p in required] == ["base_url", "repo_dir"]
assert all(p.default is inspect.Parameter.empty for p in required)
assert "runtime_attestation(self._server.base_url, pathlib.Path(clone)," in attest
# ...and the signature the patch DOCUMENTS must name it too, or the next reader copies
# the broken call back out of the docstring.
assert "runtime_attestation(base_url, repo_dir, *, expected_version=" in attest
assert "OBO_ALLOW_EVOLVED_VOLUME" in attest
readme = (patches / "README.md").read_text(encoding="utf-8")
# The stack order must name the three predecessor bridge patches explicitly.
for name in ("run_clbench_bridge_agent.v6560.patch", "clb_acceptance_claims.v674.patch",
"clb_disabled_tools_env.v6745.patch"):
assert name in readme
order = readme.index("Apply order (unambiguous)")
assert order < readme.index("clb_multi_instance_outcomes.v6746.patch")
def test_clb_methodology_states_poker_instance_count_and_outcome_semantics():
text = (REPO_ROOT / "devtools" / "benchmarks" / "continual_learning"
/ "METHODOLOGY.md").read_text(encoding="utf-8")
assert "`exploitable_poker` = 120" in text
assert "--num-instances 120" in text
assert "auto_resolved_no_agent_turn" in text
assert "Accepted overshoot (owner decision)" in text
assert "late_instance_outcomes_fill" in text
assert "Disclosed residual" in text
def test_collect_results_normalizes_bridge_traces(tmp_path):

View file

@ -6,61 +6,6 @@ from ouroboros.tools.scope_review_contract import COUPLING_QUESTION_IDS
ROOT = Path(__file__).resolve().parents[1]
def test_public_contributor_flow_is_agent_first_and_route_neutral():
guide = (ROOT / "CONTRIBUTING.md").read_text(encoding="utf-8")
readme = (ROOT / "README.md").read_text(encoding="utf-8")
assert "against lowercase `ouroboros`, not `main` or `ouroboros-stable`" in guide
assert "Coding agents and people must read [CONTRIBUTING.md]" in readme
for path in (
"BIBLE.md", "docs/ARCHITECTURE.md", "docs/DEVELOPMENT.md",
"docs/CHECKLISTS.md",
):
assert path in guide
# 11=A: only the checklist SSOT is read cover-to-cover; the other project
# documents are mapped by headings with relevant sections read in full.
assert "[`docs/CHECKLISTS.md`](docs/CHECKLISTS.md)" in guide
assert "**in full**" in guide
assert "navigation map" in guide
assert "read every section relevant" in guide
# Negative pins: the retired blanket contract must not resurface anywhere
# in the two entry documents (README routes contributors here).
assert "read these files **in full**" not in guide
assert "The required project documents were read in full" not in guide
assert "in full before editing" not in readme.replace(
"reading every section relevant to your change in full before editing", "")
# 10=A: the agentic checklist review is the main path and must cover every
# coupling item with the runtime output contract.
assert "agentic checklist review" in guide
for item in COUPLING_QUESTION_IDS:
assert item in guide
assert "scripts/validate_scope_receipt.py" in guide
assert "separate agent context" in guide
assert "Reviewing in the authoring conversation does not count" in guide
assert "Mark the review `NOT_RUN`" in guide
# The script lane states what a scope reviewer is owed (a change-relative
# required-source floor) instead of a window requirement (#395).
assert "maintainer tooling" in guide
assert "SCOPE_REVIEW_BLOCKED" in guide
assert "change-relative" in guide
assert "does not depend on a very large reviewer window" in guide
assert "Reading coverage is diagnostic" in guide
assert "never remove a responding reviewer from quorum" in guide
assert "SHAPE, not truth" in guide
assert "--contributor" in guide
assert "--base-ref upstream/ouroboros" in guide
# D31: the command runs from a clean checkout of the target base (the installed
# body) and names the proposal; the proposal is never that checkout's HEAD.
assert "git worktree add --detach ../ouroboros-review upstream/ouroboros" in guide
assert "--head-ref <your-branch>" in guide
assert "--head-ref HEAD" not in guide
assert "installed body's review flow and rules" in guide
assert "re-runs itself" not in guide
assert "review-packet.zip" in guide
assert "evidence, not a promise to merge" in guide
assert "OpenRouter" not in guide
def test_pull_request_template_has_one_universal_agent_review_block():
template = (ROOT / ".github" / "PULL_REQUEST_TEMPLATE.md").read_text(
encoding="utf-8"

View file

@ -1,4 +1,4 @@
"""Pure structural checks for the CyberGym benchmark profile and documentation.
"""Pure structural checks for the CyberGym benchmark profile.
These tests intentionally do not import CyberGym, Docker, inspect, browser, or
any other optional evaluator dependency. They validate the repository-owned
@ -12,7 +12,6 @@ from pathlib import Path
from devtools.benchmarks.common import launcher_audit
from ouroboros.configured_subagents import parse_configured_subagents
from tests._governance_docs_shared import architecture_text
REPO = Path(__file__).resolve().parents[1]
PROFILE = REPO / "devtools" / "benchmarks" / "cybergym" / "settings_base.json"
@ -132,61 +131,3 @@ def test_cybergym_is_registered_and_passes_the_shared_structural_audit():
assert relative in launcher_audit.MIGRATED_LAUNCHERS
assert path in launcher_audit.launcher_paths()
assert launcher_audit.audit_launcher(path) == []
def test_benchmark_inventory_points_to_cybergym_docs():
common_readme = (REPO / "devtools" / "benchmarks" / "README.md").read_text(
encoding="utf-8"
)
architecture = architecture_text(REPO)
assert "cybergym/" in common_readme
assert "devtools/benchmarks/cybergym/" in architecture
def test_cybergym_docs_pin_the_owner_approved_contract():
readme = (PROFILE.parent / "README.md").read_text(encoding="utf-8")
methodology = (PROFILE.parent / "METHODOLOGY.md").read_text(encoding="utf-8")
combined = f"{readme}\n{methodology}"
required = (
MODEL,
"7656b71d07da6694e262f9c34ea994cd4849c0eb",
"bde190ded494e52bc684b66073b436c9d992c7c6",
"9cea452cc1e1a3703e0f60c2dfc8642430aab9f50433f976581509de58c7048f",
"1,507",
"Level 1",
"final.poc",
"any-of",
"raw_vul_exit_code not in {0, 71, 300}",
"raw_fix_exit_code == 0",
"build_isolated_settings",
"OUROBOROS_OR_PROVIDER",
"only",
"order",
"cybergym-internal",
"Internal=false",
"unrestricted outbound",
"mandatory trajectory audit",
"issue tracker or bug reports",
"ready-made PoC",
"rootless",
"Docker `--network host`",
"OUROBOROS_MAX_SUBAGENT_DEPTH=0",
"schedule_subagent",
"delegate_start",
"claude_code_edit",
"OUROBOROS_TASK_ABS_CEILING_SEC=21600",
"USD 3,000",
"six hours",
"admit_benchmark_run",
"finalize_run_manifest",
"append-only",
"cleanup",
"verbatim",
"workspace_custody_timeout",
)
for phrase in required:
assert phrase in combined, phrase
assert "template" in readme.lower()
assert "applied" in readme.lower()
assert "leaderboard" in readme.lower()
assert "workspace_custody_timeout" in readme and "verbatim" in readme

View file

@ -1188,44 +1188,6 @@ def test_delegate_schemas_teach_the_escalation_verb():
assert "escalate to your human" not in schemas[name]
def test_expiry_notes_teach_the_escalation_verb():
"""Delta finding: the REPEAT delegate_wait rides the expiry path, whose
notes are decision points too — both branches name the escalate verb and
never the retired progress/your-human dead ends."""
import inspect
import ouroboros.delegate_progress as dp
source = inspect.getsource(dp)
assert "escalate an above-authority question with the" in source
assert "raise it with the escalate verb (parent-first)" in source
assert "escalate to your human via a progress message" not in source
assert "escalate to your human," not in source
# The dispatch charter is an LLM-facing user message too (agent.py appends
# it): the same dead end must not survive there. The campaign owner of
# dispatch_executor_note is agent_dispatch (subagent_dispatch_notes is the
# historical import facade).
import ouroboros.agent_dispatch as dn
charter = inspect.getsource(dn)
assert "escalated with the escalate verb (parent-first" in charter
assert "goes to your human via progress" not in charter
# No delegated-question surface may teach direct-to-human ESCALATION: the
# hierarchy (parent-first) is the ONLY route (decision 31). Semantic
# variants of the escalation phrasing, not just the exact retired lines
# (the live progress STREAM legitimately mentions the human).
import ouroboros.tools.delegate as dtool
import ouroboros.delegate_interactions as di
for module in (dp, dn, di, dtool):
text = inspect.getsource(module)
for phrase in ("escalated to its human", "escalated to your human",
"escalated a question to its human",
"goes to your human", "surface it to your human"):
assert phrase not in text, (module.__name__, phrase)
def test_definite_answer_refusals_publish_as_recorded_refusals(tmp_path, monkeypatch):
"""Owner Q8A: a refusal the engine confirmed (rejected rows) is an agent fault,
an absent run a substrate refusal; neither is an OK observation."""

View file

@ -706,18 +706,6 @@ def test_the_terminal_payload_carries_resumable_and_one_line_per_continued_try()
# --------------------------------------------------------------------------- unchanged neighbours
@pytest.mark.parametrize("chapter,marker,codes", [
("development/06-rules-by-change-class.md", "- `continue_from`", (
continuation.REFUSAL_SNAPSHOT_MISSING, continuation.REFUSAL_SNAPSHOT_RELEASED,
"continuation_handover_busy")),
("architecture/06-agent-core.md", "**Continuation**", (continuation.REFUSAL_SNAPSHOT_MISSING,)),
])
def test_continuation_docs_name_the_snapshot_custody_refusals(chapter, marker, codes):
text = (pathlib.Path(__file__).resolve().parents[1] / "docs" / chapter).read_text(encoding="utf-8")
paragraph = text[text.index(marker):].split("\n\n", 1)[0]
assert all(f"`{code}`" in paragraph for code in codes)
assert "refuses ONLY on the four floors" not in paragraph
def test_requested_cap_is_clamped_by_the_deadline_and_the_remaining_lifetime(tmp_path, monkeypatch):
"""The cap basis is still RECORDED beside the number (a fact for the parent),

View file

@ -17,7 +17,6 @@ real ``read_file`` stamp, never a helper call standing in for them:
from __future__ import annotations
import hashlib
import inspect
import json
import pathlib
from dataclasses import replace
@ -145,11 +144,6 @@ def test_main_runtime_data_read_of_the_same_exact_source_is_credited(main_loop,
def test_every_read_enters_the_one_acknowledgement_once(geo, monkeypatch):
"""No second hook, no prefix estimate: a direct read and an unrelated read each
call the single acknowledgement owner once; only the exact source credits."""
from ouroboros.tools import core_file_tools
source = inspect.getsource(core_file_tools)
assert source.count("acknowledge_staged_output") == 2, "one import + one call in _stamp_read_view"
assert "tool_result_limit" not in inspect.getsource(output._acknowledge_staged_output)
ctx = _top(geo, ROOT)
read_next = _stage(geo, ctx, monkeypatch)["output_delivery"]["read_next"]
calls = []

View file

@ -460,7 +460,7 @@ def test_capture_transports_utf8_with_nul_and_loader_junk_stays_out(tmp_path, mo
entry = _payload_entry(handle, skill)
capture = _capture_terminal_patch(ctx, entry)
assert capture["status"] == "ready_with_changes", capture
manifest = json.loads(pathlib.Path(capture["manifest_artifact"]).read_text())
manifest = json.loads(pathlib.Path(capture["manifest_artifact"]).read_text(encoding="utf-8"))
assert manifest["capture_kind"] == "skill_payload"
assert set(manifest["tracked_changed"]) == {"table.txt", "plugin.py", "notes.txt"}
assert manifest["blocked_reserved_paths"] == []
@ -576,7 +576,7 @@ def test_reserved_path_patch_refuses_whole_apply_and_preserves_candidate(tmp_pat
ctx, skill, handle, entry, capture = _captured(tmp_path, monkeypatch, edit=edit)
assert capture["status"] == "ready_with_changes", capture
manifest = json.loads(pathlib.Path(capture["manifest_artifact"]).read_text())
manifest = json.loads(pathlib.Path(capture["manifest_artifact"]).read_text(encoding="utf-8"))
assert manifest["blocked_reserved_paths"] == [".clawhub.json"]
out = _integrate_delegated_patch(ctx, "run-p1", "apply", "")
assert "INTEGRATE_DELEGATED_RESERVED_PATHS" in out, out
@ -884,18 +884,15 @@ def test_reconcile_queue_failure_degrades_the_receipt_honestly(tmp_path, monkeyp
custody._CUSTODY.clear()
@pytest.mark.parametrize("iteration", range(50))
def test_parallel_starts_on_same_payload_yield_exactly_one_winner(tmp_path, monkeypatch, iteration):
"""Gate fix 5 + Sol delta 5a: deterministic REQUESTED→STARTED window, x50.
def test_parallel_starts_on_same_payload_yield_exactly_one_winner(tmp_path, monkeypatch):
"""Gate fix 5 + Sol delta 5a: one deterministic REQUESTED→STARTED window.
Both starts pass the cheap early check and provision; the winner then claims
(durable START_REQUESTED) and is HELD inside the gateway POST — after its
request row, before its STARTED row. The loser runs its locked busy predicate
strictly inside that window: the old two-pass read could miss a holder whose
transition landed between the passes; the single-pass snapshot cannot. Fifty
fresh-drive cases prove the interleaving is stable, not schedule-lucky.
In the parallel lane each repeat has its own pytest timeout; repeated real
provisioning and store imports must not consume one repeat's hang budget.
transition landed between the passes; the single-pass snapshot cannot. The
barrier and events force that ordering, so repetition adds no new schedule.
(Both starts are one task here; distinct owners: tests/test_custody_keyed_readers.py.)"""
import threading
from contextlib import contextmanager
@ -909,7 +906,7 @@ def test_parallel_starts_on_same_payload_yield_exactly_one_winner(tmp_path, monk
real_provision = integration._provision_payload_snapshot
real_snapshot, real_open = current.payload_snapshot, store.open_store
inside = threading.local()
base = tmp_path / f"iter{iteration}"
base = tmp_path / "race"
base.mkdir()
ctx = _payload_ctx(base, monkeypatch)
_seed_skill(base / "data")
@ -950,7 +947,7 @@ def test_parallel_starts_on_same_payload_yield_exactly_one_winner(tmp_path, monk
# the busy snapshot's read transaction closed and BEFORE its
# projections are built — the exact hole a two-pass (re-reading)
# predicate had. Only one shared read still reports busy here.
fired.append(iteration)
fired.append(True)
release.set()
thread.join(timeout=30)
@ -969,16 +966,17 @@ def test_parallel_starts_on_same_payload_yield_exactly_one_winner(tmp_path, monk
thread = threading.Thread(target=_winner, name="winner")
thread.start()
loser = json.loads(_exact_payload_start(
ctx, "start loser", root="skill_payload", bucket="external",
skill_name="alpha"))
release.set()
thread.join(timeout=60)
assert not thread.is_alive(), f"iteration {iteration}: winner hung"
assert loser["status"] == "refused", (iteration, loser)
assert loser["reason"] == "payload_delegation_busy", (iteration, loser)
assert winner_out and winner_out[0]["status"] == "started", (iteration, winner_out)
assert fired == [iteration], f"iteration {iteration}: the window hook never ran"
try:
loser = json.loads(_exact_payload_start(
ctx, "start loser", root="skill_payload", bucket="external", skill_name="alpha"))
finally:
release.set()
thread.join(timeout=60)
assert not thread.is_alive(), "winner hung"
assert loser["status"] == "refused", loser
assert loser["reason"] == "payload_delegation_busy", loser
assert winner_out and winner_out[0]["status"] == "started", winner_out
assert fired == [True], "the window hook never ran"
custody._CUSTODY.clear()

View file

@ -3439,24 +3439,6 @@ def test_gaia_epistemic_instruction_shape_and_all_solvers():
assert audit._strip_prompt_boilerplate("Q." + GAIA_EPISTEMIC_INSTRUCTION).strip() == "Q."
def test_epistemic_rule_stays_out_of_the_global_system_prompt():
"""Owner Q20/Q22 scoped the rule to the GAIA adapter ONLY: no global grounding duty in
`prompts/SYSTEM.md` (it would push the runtime into searching for trivia) and no typed
contract field. This is the invariant that keeps a future 'while we are here' edit honest."""
system_md = (REPO_ROOT / "prompts" / "SYSTEM.md").read_text(encoding="utf-8").lower()
for banned in (
"epistemic honesty",
"source your external claims",
"source your claims",
"cite a primary source",
"check it against a primary source",
):
assert banned not in system_md, f"SYSTEM.md must not carry the GAIA grounding rule: {banned}"
contracts = (REPO_ROOT / "ouroboros" / "contracts" / "task_contract.py").read_text(encoding="utf-8")
assert "epistemic" not in contracts.lower(), "Q20/Q22 explicitly rejected a typed contract field"
def test_gaia_claude_code_solver_uses_stream_json_and_writes_trace(monkeypatch, tmp_path):
from devtools.benchmarks.gaia.inspect_solver import claude_code_solver as cc
@ -3558,7 +3540,6 @@ def test_gaia_audit_gold_verbatim_alone_is_weak_only(tmp_path):
assert audit._distinctive_gold(gold)
def test_gaia_score_leakage_adjusted(tmp_path):
from devtools.benchmarks.gaia import score_gaia
@ -3700,24 +3681,6 @@ def test_gaia_distinct_same_basename_declarations_both_stage(tmp_path, monkeypat
assert contents == [b"/shared_files/a/doc.pdf", b"/shared_files/b/doc.pdf"]
def test_programbench_instruction_states_tree_ships_as_is():
"""v6.74.4: the PB instruction must carry the true submission model (live
tree, .git dropped, uncommitted edits ship) and the final compile.sh check,
and must no longer claim a fresh checkout."""
template = " ".join((
Path(__file__).resolve().parents[1]
/ "devtools" / "benchmarks" / "programbench" / "instruction_template.md"
).read_text(encoding="utf-8").split())
assert "CURRENT state of your working tree" in template
assert "uncommitted edits DO ship" in template
assert "The exporter also excludes" in template
assert "`.ouroboros/`" in template and "at ANY depth" in template
assert "run `./compile.sh` one final time" in template
# The negated truth stays; the old false claim must be gone.
assert "not from a fresh checkout" in template
assert "on a fresh checkout" not in template
def test_programbench_submission_tarball_contract(tmp_path):
"""v6.74.4 (codex finding 1): the instruction's submission model must match
the exporter — uncommitted source ships from the LIVE tree; .git, root

View file

@ -22,7 +22,6 @@ import os
import pathlib
import tempfile
import pytest
from tests._governance_docs_shared import governance_doc_text
from tests.test_swarm_host_admission import host as _promotion_host
from tests._store_shared import seed_result, unreadable_result
@ -93,18 +92,6 @@ def test_max_mode_inlines_architecture_and_development_in_full():
assert "## DEVELOPMENT.md" in text
def test_plan_review_docs_pin_honest_continuation_from_retained_sources():
"""A plan participant continues only from its own retained delivery and reply; an
unreadable source is a fresh send the ask row discloses (`continued`), never a borrowed
transcript. Both books state it; the retired engine's refusal code is not live text."""
repo = pathlib.Path(__file__).resolve().parents[1]
for relative in ("docs/ARCHITECTURE.md", "docs/DEVELOPMENT.md"):
text = governance_doc_text(relative, repo)
assert "an unreadable source means a fresh send" in text and "`continued`" in text, relative
assert "plan_review_exact_artifact_unavailable" not in text, relative
def test_forked_task_captures_canonical_memory_and_exact_api_context():
import json

View file

@ -1,10 +1,4 @@
"""Guardrails for architecture docs after UI/routing overhaul.
README prose pins were retired in v5.8.3-rc.5 — the README is intentionally
allowed to evolve its marketing copy without dragging tests along; the
ARCHITECTURE.md pins below are the load-bearing rationale-layer guards
(P6) that must survive every doc-touch commit.
"""
"""Reference-book structure, executable inventories and prompt identifiers."""
import os
import pathlib
@ -56,19 +50,6 @@ def test_component_basename_does_not_borrow_another_explicit_path():
assert not _names_basename("test_s3_task_control_browser.py", "browser.py")
def test_the_domain_manifest_is_reachable_from_the_handbook():
"""The domain SSOT and its generated map were reachable from neither doc.
A contributor is told to read DEVELOPMENT.md before moving code, and moving
code across domains is exactly what `ouroboros/domains.toml` gates. With no
pointer, the first they learned of it was a red `check_domains` run.
"""
development = _read("docs/DEVELOPMENT.md")
for pointer in ("ouroboros/domains.toml", "docs/DOMAIN_MAP.md",
"scripts/check_domains.py --write"):
assert pointer in development, f"DEVELOPMENT.md never mentions {pointer}"
def test_recent_abi_retirements_section_carries_the_abi_70_window():
"""Section 11.4 carries the compatibility facts an upgrading operator needs.
@ -90,40 +71,12 @@ def test_recent_abi_retirements_section_carries_the_abi_70_window():
def test_model_send_design_note_matches_the_observability_contract():
"""A reconstruction mismatch is observable without blocking dispatch.
`ouroboros/model_send_seal.py` implements the rule pinned by
`tests/test_model_send_seal.py`: a reconstruction mismatch is a typed
durable fact and the call is NOT blocked — dispatch authority stays with
the pre-existing in-memory identity re-check. A design note that outranks
the code it describes is how the next author reintroduces the gate.
"""
note = _read("docs/MODEL_SEND_OBSERVABILITY.md")
note_flat = " ".join(note.split())
assert (REPO / "ouroboros" / "model_send_seal.py").exists()
assert "refuse dispatch with the existing `PhysicalAttemptPreparationFailed`" \
not in note_flat
assert "It never calls `verify_sealed_candidate` or reads that record back" in note_flat
assert "facts; they do not gate a later model call" in note_flat
def test_settings_docs_name_every_key_owner_and_what_startup_persists():
"""Three settings-layer claims the code contradicts (all red pre-fix).
1. The Default settings table skipped two live `SETTINGS_DEFAULTS` keys
(`OUROBOROS_CONTEXT_MODE_AUTO_LOW`, `OUROBOROS_CLAWHUB_REGISTRY_URL`).
2. Startup was described as persisting NOTHING, while the server lifespan's
`load_settings()` runs `normalize_and_persist_context_mode_compat`,
which rewrites the compat pair when it changed under a held lock.
3. Invariant 3 and the README pointed at the `config.py` facade as the
owner of defaults, after the v7next split moved the vocabularies into
sibling leaves.
"""
"""Keep the live settings and their rows in the tool-read reference table."""
arch = _read("docs/ARCHITECTURE.md")
arch_flat = " ".join(arch.split())
readme_flat = " ".join(_read("README.md").split())
development = _read("docs/DEVELOPMENT.md")
from ouroboros.settings_defaults import SETTINGS_DEFAULTS
@ -131,42 +84,9 @@ def test_settings_docs_name_every_key_owner_and_what_startup_persists():
assert key in SETTINGS_DEFAULTS
assert f"| {key} |" in arch, f"{key} is missing from the settings table"
# Startup persistence: the compat migration is a real write, not "nothing".
# (Other "persists nothing" statements in this document are about the
# onboarding failure path and a no-change owner transform, both true.)
assert "boot provider normalization in-process and persists nothing" not in arch_flat
assert "normalize_and_persist_context_mode_compat" in arch_flat
# 4. What retired is the persistent auto-Low MECHANISM, not the key: the
# startup sentence called `OUROBOROS_CONTEXT_MODE` itself retired while
# the settings table right below it documents the same key as the live
# owner-selected horizon.
assert "retired `OUROBOROS_CONTEXT_MODE`" not in arch_flat
# Ownership: the leaves own the vocabularies; config.py stays the facade.
# One statement of the book names every owner leaf together.
owners = ("settings_defaults", "settings_scales", "model_slots",
"review_model_routes", "runtime_limits", "settings_integrity")
assert any(all(owner in line for owner in owners) for line in arch.splitlines()), owners
assert "exact settings and defaults live in" not in readme_flat
assert "settings_defaults.py" in readme_flat
assert "an SSOT in `config.py` `SETTINGS_DEFAULTS`" not in development
assert "`settings_defaults.py`" in development
def test_architecture_does_not_claim_usage_response_is_the_only_usage_reader():
"""`_usage_response` normalizes for accounting; it does not own the block.
The row claimed "The only reader of a provider's usage block", but every
provider adapter reads the raw `usage` dict for its own response envelope
(`llm_openai_compatible.py:285`, `llm_anthropic.py`, `llm_local.py`,
`local_model.py`). A false absolute in the component map is how the next
author "consolidates" a read that was never centralized here; the honest
claim is the narrower one the two importers support.
"""
arch_flat = " ".join(_read("docs/ARCHITECTURE.md").split())
assert "The only reader of a provider's usage block" not in arch_flat
assert "_usage_response.py" in arch_flat
"""Shared normalization and adapter response reads remain separate consumers."""
for module in ("ouroboros/usage_accounting.py", "ouroboros/loop_llm_call.py"):
assert "from ouroboros._usage_response import" in _read(module), module
assert 'resp_dict.get("usage")' in _read("ouroboros/llm_openai_compatible.py")
@ -267,218 +187,13 @@ def test_checklist_item_numbers_cited_outside_the_checklist_name_the_current_ite
assert not re.search(r"CHECKLISTS items? (?:2|13)\([a-z]\)", dev)
def test_architecture_mentions_shared_log_grouping_and_direct_provider_review_fallback():
arch = _read("docs/ARCHITECTURE.md")
assert "log_events.js" in arch
# Direct-provider review fallback: the exclusive-provider resolver and the
# model-id migration it relies on are named by their code identifiers.
assert "_exclusive_direct_remote_provider_env" in arch
assert "migrate_model_value" in arch
# The Claude Runtime Status surface is RETIRED with the Claude-SDK
# advisory transport (owner-consented, 2026-08-29): the doc must not
# resurrect its UI plumbing.
assert "refreshClaudeCodeStatus" not in arch
assert "claudeRuntimeHasError" not in arch
def test_architecture_limits_finality_and_verdict_claims_to_actual_rows():
arch = _read("docs/ARCHITECTURE.md")
assert "outcome_final" in arch
assert "task_summary" in arch
assert "Both Main rows, the Project thread rows" not in arch
def test_architecture_maps_cache_split_and_total_budget_authorities():
arch = _read("docs/ARCHITECTURE.md")
assert "_usage_cache_splits.py" in arch
settings_row = next(
line for line in arch.splitlines() if "settings_setup_contract.py" in line
)
assert "resolve_total_budget_usd" in settings_row
def test_architecture_documents_skill_schedule_lifecycle_and_evolution_light_block():
arch = _read("docs/ARCHITECTURE.md")
# v6.9 RC2: skill schedule readiness SSOT, lifecycle resync, tombstone
# retention, DST contract, and the evolution light-mode hard block.
assert "resync_skill_schedules()" in arch
assert "skill_readiness_for_execution()" in arch
# The automatic experience-review write-back is retired; lessons, the Pattern Register
# and the backlog are authored through knowledge_write.
assert "MEMORY_ACTIONS_JSON" not in arch and "apply_memory_actions" not in arch
assert "Lessons, the Pattern Register and the improvement backlog are authored, never host-generated" in arch
def test_chat_id_addressing_docs_match_the_code_that_routes_it():
"""The chat-0 doctrine is documented where implementers read it (P6).
ARCHITECTURE previously claimed history replay renders chat-0 frames in the
Main stream; the Main filter drops them, so a reader following the doc would
look for a CLI run's dialogue in a surface that never shows it. That claim
must never come back.
"""
arch = _read("docs/ARCHITECTURE.md")
development = _read("docs/DEVELOPMENT.md")
assert "renders them in the Main stream" not in arch
assert "HIDDEN_CHAT_ID" in arch
# The headless address is decided at admission (both outcomes have identifiers).
assert "log_addressing.ingress_chat_id" in arch
assert "WEB_UI_CHAT_ID" in arch
# Naming is part of the same admission contract.
assert "metadata.title" in arch
# A degraded delivery names its own cause: the finish tool's wire form and
# the salvage rail's code.
assert "finish_task" in arch and "answer_sha256" in arch
assert "host_salvage" in arch
design = _read("docs/DESIGN.md")
assert "Where a card does show a cause, it says it in the owner's" in design
assert "the record keeps the machine code" in design
# The rule itself lives with the other anti-patterns, not only in a changelog.
assert "notification_chat_route" in development and "coerce_chat_identity" in development
assert "tests/test_chat_id_truthiness_guard.py" in development
def test_consciousness_prompt_is_the_wake_message_of_an_ordinary_main_turn():
"""The wake-up runs on Main's system prompt and tools (owner decision В15); this file
is its USER message: no private capability catalog, no round or interval limits."""
consciousness = _read("prompts/CONSCIOUSNESS.md")
assert consciousness.startswith("You are Ouroboros. No one has asked for a task; this turn is yours.")
assert "A pause is a legitimate decision" in consciousness
assert "Distinguish incremental cash cost from subscription quota" in consciousness
assert "ordinary turn, not an assigned deliverable" in consciousness
for retired in ("You can:", "up to 10 rounds", "Default wakeup", "background consciousness mode",
"Doing nothing is a fine outcome", "`set_next_wakeup`", "`escalate`"):
assert retired not in consciousness, retired
def test_phase3_governance_language_is_pinned_without_new_qa_surface():
bible = _read("BIBLE.md")
development = _read("docs/DEVELOPMENT.md")
system = _read("prompts/SYSTEM.md")
authoring = _read("docs/CREATING_SKILLS.md")
checklists = _read("docs/CHECKLISTS.md")
development_flat = " ".join(development.split())
assert (
"Uncertainty calls for judgment, not permission: within its legitimate "
"authority, Ouroboros decides autonomously."
) in bible
assert (
"Structural depth is not scope breadth: choose the smallest change that "
"eliminates the proven failure class."
) in bible
# The heading DOC_RESIDUE_SKIPPED_SUBSECTIONS selects, and its guard test.
# The heading is selected by DOC_RESIDUE_SKIPPED_SUBSECTIONS below.
assert "External facts: unknown is not no" in development
assert "tests/test_model_name_invariance.py" in development_flat
for text in (system, authoring, checklists):
flat = " ".join(text.split())
assert "real consumer flow" in flat
assert "screenshot" in flat.lower()
assert "vision" in flat.lower()
assert "not a universal" in flat or "not universal" in flat or "no universal" in flat
# Structure of the external-facts table (checked last so a header change
# does not mask the assertions above).
assert "| Location | Fact | Evidence & re-probe | If stale |" in development
def test_continuity_projection_contract_is_mirrored_across_governance_docs():
"""Keep the partial-input rule and its concrete data-flow map from drifting."""
bible = " ".join(_read("BIBLE.md").split()).replace("**", "")
architecture = " ".join(_read("docs/ARCHITECTURE.md").split())
development = " ".join(_read("docs/DEVELOPMENT.md").split())
checklists = _read("docs/CHECKLISTS.md")
assert (
"Disclosure is not sufficiency. An omission marker keeps a record honest; "
"it does not make the record complete. Where material is omitted, the "
"disclosure must name a source this actor can actually resolve. A view known "
"to be partial may not authorize PASS, a destructive rewrite, or replacement "
"of the full contract it was cut from."
) in bible
assert "Continuity data-flow map" in architecture
assert "state/skill_review_root_tasks.jsonl" in development
assert "state/skill_review_root_tasks.jsonl" in architecture
assert "SKILL_REVIEW_ROOT_TASKS_WARN_BYTES" in architecture
assert "memory/chronicle/records.jsonl" in architecture
assert "nine os.stat calls" in _read("ouroboros/agent_startup_checks.py")
for item in (
"source_completeness",
"canonical_memory_fork",
"display_identity_replay",
):
assert item in checklists
def test_architecture_names_all_window_surfaces_and_settlement_order():
architecture = _read("docs/ARCHITECTURE.md")
assert "reviewer_window.resolve_reviewer_window" in architecture
assert "ReviewerWindow.sizing_window" in architecture
assert "scope_window" in architecture
def test_phase3_widget_authoring_docs_match_recursive_schema_v1():
development = _read("docs/DEVELOPMENT.md")
authoring = _read("docs/CREATING_SKILLS.md")
architecture = _read("docs/ARCHITECTURE.md")
checklists = _read("docs/CHECKLISTS.md")
authoring_flat = " ".join(authoring.split())
for text in (development, authoring, architecture, checklists):
for component in ("group", "metric", "callout"):
assert component in text
assert "maximum depth of 8" in authoring
assert "256 nodes" in authoring
assert "stable tree path" in authoring
assert "transitively passive" in authoring_flat
assert "dynamic_ui_schema" in authoring
def test_architecture_mirror_matches_the_split_axes_contracts():
"""XG-2.2/XG-2.3 (v6.87.28 review gate): the P6 mirror tracks schedule-vs-dispatch.
Every pin here failed on the pre-fix docs: the module map claimed
`swarm_efficiency.lanes_used` while `_build_swarm_efficiency` emits
`lanes_requested`; `subagents.py` was said to own task-group compaction after
`compact_task_group` was deleted; the control map said `schedule_subagent`
surfaces effective lane(s) after `_finalize_schedule_emission` went
request-only; the `swarm_fanout` enumeration promised requested/effective
lanes after `_emit_swarm_fanout` dropped `effective_model_lanes`; and both
`wait_tasks` field enumerations omitted the emitted `capability_delta`.
"""
arch = _read("docs/ARCHITECTURE.md")
development = _read("docs/DEVELOPMENT.md")
arch_flat = " ".join(arch.split())
dev_flat = " ".join(development.split())
# swarm_efficiency reports the REQUEST: lanes_requested, never lanes_used.
assert "lanes_requested" in arch
# A fanned-out root also reports the depth REQUEST beside those lanes.
assert "`requested_depth`" in arch
assert "lanes_used" not in arch
# Task-group compaction left with the degenerate lane fan-out (v6.87.28).
assert "task-group compaction" not in arch
assert "compact_task_group" not in arch
# schedule_subagent reports the request only; the axes resolve at dispatch.
assert "schedule_subagent surfaces effective_lane(s)" not in arch_flat
# swarm_fanout carries the requested lane; a wave event written before any
# child starts cannot know what the children ran on.
assert "requested/effective lanes" not in arch
# The canonical projection's field list includes capability_delta; the
# handbook points to that owner instead of another drifting enumeration.
projection = next(line for line in arch.splitlines() if "_compact_child_projection" in line)
assert "capability_delta" in projection
assert "control_task_results._wait_for_tasks" in dev_flat
# Identifiers the prompts legitimately name in backticks that are NOT tools:
# parameter names, resource roots, write surfaces, typed outcome/status tokens
# and runtime-context keys. A NEW snake_case identifier in a prompt must either
@ -495,6 +210,7 @@ PROMPT_NON_TOOL_IDENTIFIERS = frozenset({
"write_root", "write_surface",
# tool parameters named as cross-tool policy
"project_id", "project_name", "recommended_use", "review_rebuttal", "subagent_id", "working_note",
"extra_tests", "full_tests", "test_reason",
# typed outcomes / statuses / runtime-context keys
"needs_manual_target", "started_uncustodied", "owner_client",
# safety policy class names (ouroboros/safety.py TOOL_POLICY values) and
@ -769,15 +485,3 @@ def test_architecture_settings_table_mirrors_config_defaults():
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}"
def test_the_handbook_names_both_layers_of_the_browser_no_undef_gate():
"""Two surfaces answer "does every browser identifier resolve?": the
dependency-free acorn walker the hermetic commit gate runs, and ESLint's
`no-undef` that CI runs as an independent second opinion (D-13). A
contributor who sees only one of them either removes the "redundant"
other or, seeing a CI-only red, looks for a gate that never ran it.
"""
development = _read("docs/DEVELOPMENT.md")
for pointer in ("web/tests/no_undef.test.js", "web/eslint.config.js", "npm ci"):
assert pointer in development, f"DEVELOPMENT.md never mentions {pointer}"

View file

@ -202,14 +202,18 @@ def test_preflight_worker_cap_reaches_every_lane_and_is_recorded(tmp_path, monke
stand must set the runtime's own lever to ``max(2, 16 // lanes)`` in the process every lane
server inherits, override an ambient value, and record the applied number in the manifest
and in each lane row. The runtime reads exactly that key (pinned here, not modified)."""
from ouroboros import preflight_runner
from ouroboros import preflight_runner, test_parallelism
_short_tmp(monkeypatch)
assert run_live_lanes.PREFLIGHT_WORKERS_ENV == preflight_runner._PREFLIGHT_WORKERS_ENV
assert run_live_lanes.PREFLIGHT_WORKERS_ENV == test_parallelism.WORKERS_ENV
assert run_live_lanes.PREFLIGHT_WORKERS_FLOOR == preflight_runner._MIN_PREFLIGHT_WORKERS
assert run_live_lanes.parse_args(["--stub", "--lanes", "1"]).preflight_test_workers == 16
assert run_live_lanes.parse_args(["--stub", "--lanes", "4"]).preflight_test_workers == 4
assert run_live_lanes.parse_args(["--stub", "--lanes", "6"]).preflight_test_workers == 2 # floor at MAX_LANES
monkeypatch.setenv(run_live_lanes.PREFLIGHT_WORKERS_ENV, "128") # the operator shell must lose
# The stand is the outermost runner here: no inherited test slot or busy host caps its lever.
monkeypatch.setenv(test_parallelism.NESTED_WORKERS_ENV, "16")
monkeypatch.setattr(test_parallelism, "physical_memory",
lambda: test_parallelism.MemorySnapshot(100 * 1024 ** 3, 100 * 1024 ** 3))
seen: dict = {}
def lane(job, args, out, template, stagger, states, seed, budget=None, *, key="", seed_sha=""):

View file

@ -1,12 +1,4 @@
"""Structural contracts for the semantic-no-op supervisor event-handler extraction.
v7next D08/F2.2: the full reference split. The quiet families landed with the
D08 lane; the F2.2 cancel/custody lane moved the task-done and evolution-done
families into their reference-named leaves, the cancel ingress into
events_runtime_controls, and the owner-stop campaign closure beside
stop_evolution_tasks in queue_transitions — all emitted from tip bytes.
This suite pins the completed shape explicitly.
"""
"""Supervisor event-family boundaries, compatibility identities, and hot paths."""
from __future__ import annotations
@ -121,18 +113,6 @@ _MOVED_OWNERS = {
"_close_campaign_after_owner_stop": queue_transitions,
}
# The dispatcher keeps the table, its loop and the module logger, plus the
# unrowed post-cutoff upstream helpers. The cancel/custody organ moved out
# with the F2.2 lane; this inventory is pinned so a later edit cannot
# silently grow or shrink the facade.
_STAYED = ("EVENT_HANDLERS", "dispatch_event", "log")
_UNROWED_UPSTREAM = (
"_handle_main_llm_call_state",
"_parent_delegation_budget",
"_routing_attachments",
)
def test_event_families_never_import_the_dispatcher_they_serve():
"""The dependency is one-way at module scope (the two handle leaves import
the parent only inside the call-time handle), which is what keeps the
@ -188,17 +168,6 @@ def test_every_event_family_is_a_hot_code_path_like_its_dispatcher():
assert rel in HOT_CODE_PATHS, rel
def test_the_dispatcher_kept_only_the_table_the_loop_and_the_unrowed_helpers():
tree = ast.parse(pathlib.Path(events.__file__).read_text(encoding="utf-8"))
defined = []
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
defined.append(node.name)
elif isinstance(node, ast.Assign):
defined.extend(t.id for t in node.targets if isinstance(t, ast.Name))
assert sorted(defined) == sorted(_STAYED + _UNROWED_UPSTREAM)
def test_f22_queue_and_worker_leaves_keep_the_hot_label():
"""F2 conformance item 3: the cancel/custody organ moved hot bodies into
queue/worker leaves too - the family loop above cannot see those, so pin

View file

@ -324,15 +324,28 @@ def test_task_metrics_are_persisted_and_forwarded_to_live_logs(tmp_path):
assert pushed[0]["tool_errors"] == 1
def test_llm_usage_serializer_carries_web_search_sources():
"""The llm_usage serializer must persist web_search_sources (GAIA
campaign contract). Moved here from tests/test_devtools_benchmarks.py:
after the D08 split the serializer lives with its budget family, and
this suite owns the llm_usage event surface."""
import pathlib
src = (pathlib.Path(__file__).resolve().parent.parent
/ "supervisor" / "events_budget.py").read_text(encoding="utf-8")
assert "web_search_sources" in src
def test_llm_usage_serializer_carries_web_search_sources(tmp_path):
"""Provider search citations reach both the durable audit and the live log."""
from types import SimpleNamespace
from supervisor import events
sources = [
{"url": "https://example.test/one", "title": "First", "content": "First excerpt"},
{"url": "https://example.test/two", "title": "Second", "content": "Second excerpt"},
]
frames = []
ctx = SimpleNamespace(DRIVE_ROOT=tmp_path, bridge=SimpleNamespace(push_log=frames.append))
events._handle_llm_usage(
{"type": "llm_usage", "task_id": "search-task", "usage": {"web_search_sources": sources}},
ctx,
)
written = json.loads((tmp_path / "logs" / "events.jsonl").read_text(encoding="utf-8"))
assert written["web_search_sources"] == sources
assert written["task_id"] == "search-task"
assert frames == [written]
def test_effort_facts_with_legacy_option_status_survive_logs_without_notices(tmp_path):

View file

@ -297,6 +297,7 @@ def test_only_evolution_push_stays_under_git_lock(
"_evolution_publication_stopped_result",
lambda *a, **k: order.append("authority") or "",
)
monkeypatch.setattr(git_tools, "_evolution_test_publication_stop", lambda *a, **k: "") # tested below on its own
monkeypatch.setattr(git_tools, "_auto_push", lambda *a, **k: order.append("push") or "")
monkeypatch.setattr(
git_tools,
@ -382,6 +383,52 @@ def test_revoked_publication_does_not_record_or_anchor_success(tmp_path, monkeyp
assert ctx._coupling_review_history == {"keep": True}
@pytest.mark.parametrize("tests_red", [False, True], ids=["full_green", "red_postcommit"])
def test_a_test_refusal_withholds_the_push_but_keeps_reviewed_provenance(tmp_path, monkeypatch, tests_red):
"""Unlike lost authority, a red or proof-less post-commit run only withholds publication."""
from ouroboros import body_candidate, mutation_attribution, preflight_qualification
from ouroboros.tools import git as git_tools
claim = {"campaign_id": "camp", "transaction_id": "tx", "task_id": "evo"}
sha = "d" * 40
attempts, baselines, reviewed, pushed = [], [], [], []
monkeypatch.setattr(git_tools, "_task_attributed_commit_paths", lambda *a, **k: (None, None, "", ("root", "task")))
monkeypatch.setattr(git_tools, "_check_overlapping_review_attempt", lambda *a, **k: "")
_patch_commit_seam(monkeypatch, "_record_commit_attempt", lambda *a, **k: attempts.append(k.get("status") or a[2]))
monkeypatch.setattr(git_tools, "_acquire_git_lock", lambda *a, **k: pathlib.Path("lock"))
monkeypatch.setattr(git_tools, "_release_git_lock", lambda *a, **k: None)
monkeypatch.setattr(git_tools, "_prepare_review_commit_worktree", lambda *a, **k: (False, ""))
_patch_commit_seam(monkeypatch, "_evolution_commit_authority", lambda *a, **k: (claim, {"ok": True}))
_patch_commit_seam(monkeypatch, "_verify_reviewed_commit_binding", lambda *a, **k: (True, ""))
_patch_commit_seam(monkeypatch, "_run_reviewed_stage_cycle", lambda *a, **k: {
"status": "passed", "pre_fingerprint": {"fingerprint": "pre"},
"post_fingerprint": {"fingerprint": "post", "binding": {}}})
_patch_commit_seam(monkeypatch, "run_cmd", lambda cmd, cwd=None: sha if cmd[:3] == ["git", "rev-parse", "HEAD"] else "")
monkeypatch.setattr(git_tools, "_auto_tag_on_version_bump", lambda *a, **k: "")
_patch_commit_seam(monkeypatch, "_record_evolution_commit_receipt", lambda *a, **k: "")
def post_commit(ctx, message, skip_tests, warning, force=False):
if tests_red:
warning[0] = "TESTS_FAILED (commit preserved): tests/test_far_away.py::test_x"
return warning[0]
return None
monkeypatch.setattr(git_tools, "_post_commit_result", post_commit)
monkeypatch.setattr(preflight_qualification, "full_proof_error", lambda *a, **k: "")
monkeypatch.setattr(git_tools, "_auto_push_after_review", lambda *a, **k: pushed.append(True) or " [pushed]")
monkeypatch.setattr(body_candidate, "record_reviewed_commit", lambda ctx, commit, **k: reviewed.append(commit))
monkeypatch.setattr(mutation_attribution, "advance_mutation_baseline", lambda *a, **k: baselines.append(a))
ctx = SimpleNamespace(repo_dir=tmp_path, drive_root=tmp_path, branch_dev="ouroboros",
current_task_type="evolution", task_id="evo",
task_metadata={"evolution_transaction": claim}, _coupling_review_history={})
result = git_tools._repo_commit_push(ctx, "test commit")
assert ctx.last_reviewed_commit_sha == sha and reviewed == [sha]
assert ("EVOLUTION_PUBLICATION_STOPPED" in result) is tests_red
assert pushed == ([] if tests_red else [True])
def test_postcommit_binding_failure_contains_evolution_commit(tmp_path, monkeypatch):
from ouroboros.tools import git as git_tools

View file

@ -15,8 +15,6 @@ from ouroboros import (
extension_surface_names,
)
REPO = pathlib.Path(__file__).parents[1]
_LEAVES = (
extension_registry_state,
extension_surface_names,
@ -175,23 +173,3 @@ def test_the_public_loader_surface_is_unchanged():
]
for name in extension_loader.__all__:
assert hasattr(extension_loader, name), name
def test_extension_extraction_size_bounds_have_meaningful_headroom():
from ouroboros.review import BAND_MODULE_MAX_LINES, TARGET_MODULE_LINES
from ouroboros.size_ratchet_manifest import BAND_PATHS
counts = {
module.__name__: len(
pathlib.Path(module.__file__).read_text(encoding="utf-8").splitlines()
)
for module in (extension_loader, *_LEAVES)
}
assert counts["ouroboros.extension_loader"] <= TARGET_MODULE_LINES
for module, count in counts.items():
path = module.replace(".", "/") + ".py"
limit = TARGET_MODULE_LINES
if path in BAND_PATHS:
assert BAND_PATHS[path], f"new extension band entry needs its rationale: {path}"
limit = BAND_MODULE_MAX_LINES
assert count <= limit, f"{path}: {count} lines exceeds its {limit}-line bound"

View file

@ -132,8 +132,6 @@ def test_external_review_script_is_a_wrapper_over_the_review_operation():
import scripts.run_external_review as module
source = Path("scripts/run_external_review.py").read_text(encoding="utf-8")
assert "v6.10.0" not in source
assert "Google Colab" not in source
# Contributor lane: the review operation over the frozen base..head subject.
assert "ouroboros.tools import review_change" in source
assert 'root="system_repo", surface="change"' in source
@ -164,22 +162,6 @@ def test_external_review_script_defaults_to_pro_mode():
assert 'setdefault("OUROBOROS_RUNTIME_MODE", "pro")' in source
def test_the_operator_lane_sets_no_retired_diff_aware_knob():
"""The commit gate pays the suite on every diff (owner answer A, 2026-10-08), so nothing
reads ``OUROBOROS_PREFLIGHT_DIFF_AWARE`` any more; the lane neither sets it nor leaves a
mention for an operator to copy. The whole tree under test has no reader of the name."""
name = "OUROBOROS_PREFLIGHT_" + "DIFF_AWARE"
assert name not in Path("scripts/run_external_review.py").read_text(encoding="utf-8")
readers = [
path.relative_to(REPO_ROOT).as_posix()
for folder in ("ouroboros", "supervisor", "scripts", "web", "docs", "prompts")
for path in (REPO_ROOT / folder).rglob("*")
if path.is_file() and path.suffix in {".py", ".js", ".md", ".json"}
and name in path.read_text(encoding="utf-8", errors="ignore")
]
assert readers == []
def test_external_review_script_resolves_models_and_efforts(monkeypatch):
for key in (
"OPENAI_API_KEY",

View file

@ -1,20 +0,0 @@
"""The public workspace contract must keep ordinary folders visible."""
from pathlib import Path
from tests._governance_docs_shared import architecture_text
ROOT = Path(__file__).resolve().parents[1]
def test_external_workspace_docs_distinguish_plain_folders_from_git_operations():
readme = (ROOT / "README.md").read_text(encoding="utf-8")
architecture = architecture_text(ROOT)
control = (ROOT / "ouroboros" / "tools" / "control.py").read_text(encoding="utf-8")
assert "ordinary folders or separate Git worktree roots" in readme
assert "ordinary file and process work runs directly" in readme
assert "workspace_admission.validate_workspace_root()" in architecture
assert "ordinary folder or Git worktree root" in control
assert "File/process work supports ordinary folders; Git operations need a worktree" in control
assert "External workspaces must be separate Git worktree roots" not in readme
assert "must be a git worktree root outside" not in control

View file

@ -34,8 +34,6 @@ def test_new_file_discard_and_context_menu_clamp_regressions():
def test_files_page_explains_manager_role_and_directory_affordance():
source = _read("web/modules/files.js")
assert "This is a file manager, not a chat attachment picker." in source
assert "Open a folder or file from the left panel to browse, preview, or edit its contents." in source
assert "button.type = 'button';" in source
assert "(entry.type === 'file' ? formatFileSize(entry.size) : 'open')" in source

View file

@ -85,14 +85,6 @@ class TestTelegramChatIdRemoval:
assert "telegram_chat_id" not in hints, cls.__name__
assert "transport" in hints, cls.__name__
def test_no_runtime_producer_left(self):
# The history mapper was the ONLY emitter; grep-level absence pin.
import inspect
from ouroboros.gateway import history as gateway_history
source = inspect.getsource(gateway_history)
assert '"telegram_chat_id": ' not in source
def test_legacy_stored_row_replays_without_reemitting_the_key(self, tmp_path):
from ouroboros.gateway.history import _collect_chat_rows

View file

@ -369,16 +369,10 @@ def _commit(repo, message, files=(), delete=()):
return _git(repo, "rev-parse", "HEAD")
@pytest.fixture
def replace_scene(tmp_path, monkeypatch):
"""The real update doors (``post``; ``apply`` is replace): preflight prepare (rescue, keep ref,
intent), tx, checkout and the fenced rollback. Only the fetching plan, the worker fence/revival
and the restart callback are recorded. The local branch carries one owner commit the target
lacks, so a keep ref is made."""
from ouroboros.gateway import control
from supervisor import update_merge
repo = _init_repo(tmp_path)
@pytest.fixture(scope="session")
def replacement_history(tmp_path_factory):
"""Build the common four-commit history once; expose immutable bytes, never its root."""
repo = _init_repo(tmp_path_factory.mktemp("git-lock-history"))
root = _commit(repo, "root", {"BIBLE.md": "constitution\n", "ouroboros/store.py": "# store\n",
"ouroboros/usage_store.py": "# usage\n", "seed.txt": "seed\n"})
base = _commit(repo, "owner work", {"local.txt": "owner commit\n"})
@ -388,6 +382,33 @@ def replace_scene(tmp_path, monkeypatch):
_git(repo, "checkout", "-q", "ouroboros")
(repo / ".git" / git_ops.MANAGED_REPO_META_NAME).write_text(
json.dumps({"managed_local_branch": "ouroboros"}), encoding="utf-8")
paths = sorted(repo.rglob("*"))
directories = tuple(path.relative_to(repo) for path in paths if path.is_dir())
files = tuple((path.relative_to(repo), path.read_bytes(), path.stat().st_mode & 0o777)
for path in paths if path.is_file())
return base, target, storeless, directories, files
@pytest.fixture
def replace_scene(tmp_path, monkeypatch, replacement_history):
"""The real update doors (``post``; ``apply`` is replace): preflight prepare (rescue, keep ref,
intent), tx, checkout and the fenced rollback. Only the fetching plan, the worker fence/revival
and the restart callback are recorded. The local branch carries one owner commit the target
lacks, so a keep ref is made."""
from ouroboros.gateway import control
from supervisor import update_merge
base, target, storeless, directories, files = replacement_history
repo = tmp_path / "repo"
repo.mkdir()
# Every worktree file AND .git file gets its own inode: no hardlinks,
# shared object alternates, mutable config/index, refs or lock files.
for name in directories:
(repo / name).mkdir(parents=True, exist_ok=True)
for name, body, mode in files:
path = repo / name
path.write_bytes(body)
path.chmod(mode)
data = tmp_path / "data"
(data / "logs").mkdir(parents=True)
for name, value in {"REPO_DIR": repo, "DRIVE_ROOT": data, "BRANCH_DEV": "ouroboros"}.items():

View file

@ -37,8 +37,6 @@ from tests._git_review_pipeline_shared import (
)
@pytest.fixture
def git_ctx(tmp_path):
"""Yield ``(git_module, ToolContext)`` — the canonical git pipeline setup."""
@ -204,11 +202,6 @@ class TestAutoPushBehavior:
assert hasattr(git_mod, "_auto_push")
assert callable(git_mod._auto_push)
def test_auto_push_is_best_effort(self):
git_mod = _get_git_module()
source = inspect.getsource(git_mod._auto_push)
assert "except Exception" in source
assert "non-fatal" in source.lower() or "non_fatal" in source.lower()
def test_managed_tests_gate_before_tag_and_ordinary_tests_keep_prior_order(self):
git_mod = _get_git_module()
@ -261,12 +254,6 @@ class TestRemoteConfigSurfacing:
source = (pathlib.Path(REPO) / "ouroboros" / "gateway" / "settings.py").read_text(encoding="utf-8")
assert '"warnings"' in source
def test_remote_credentials_migration_not_wired_at_startup(self):
"""Legacy token-in-URL migration is no longer run on startup."""
server_path = pathlib.Path(REPO) / "server.py"
source = server_path.read_text(encoding="utf-8")
assert "migrate_remote_credentials" not in source
# --- credential helper safety (legacy migration retired) ---
@ -275,17 +262,10 @@ class TestRemoteCredentialConfiguration:
git_ops = _get_git_ops_module()
assert not hasattr(git_ops, "migrate_remote_credentials")
def test_configure_remote_uses_local_credential_helper(self):
git_ops = _get_git_ops_module()
configure_source = inspect.getsource(git_ops.configure_remote)
helper_source = inspect.getsource(git_ops._configure_credential_helper)
assert "_configure_credential_helper" in configure_source
assert ".git/credentials" in helper_source
def test_startup_setup_only_configures_current_settings_token(self):
server_runtime = importlib.import_module("ouroboros.server_runtime")
source = inspect.getsource(server_runtime.setup_remote_if_configured)
assert "migrate_remote_credentials" not in source
assert "configure_personal_remote" in source
@ -324,16 +304,6 @@ class TestSandboxCoversRepoWrite:
) == ["BIBLE.md"]
# --- index-full instruction fix ---
class TestIndexFullInstruction:
def test_system_md_warns_against_index_full(self):
system_md = pathlib.Path(REPO) / "prompts" / "SYSTEM.md"
content = system_md.read_text(encoding="utf-8")
assert "Do NOT call" in content or "reserved internal name" in content
assert "knowledge_list" in content
# ---------------------------------------------------------------------------
# Check 7: P9 history limits in _preflight_check (v4.41.0)
# ---------------------------------------------------------------------------

View file

@ -73,6 +73,7 @@ def _stub_preflight_lanes(repo, monkeypatch):
subprocess.run(["git", "add", "-A"], cwd=repo, check=True, capture_output=True)
monkeypatch.setenv("OUROBOROS_PRE_PUSH_TESTS", "1")
monkeypatch.setenv("OUROBOROS_PREFLIGHT_TEST_WORKERS", "2")
monkeypatch.setenv("OURO_TEST_WORKER_BUDGET", "2") # nested slots: the gate must really run two workers
monkeypatch.delenv("OUROBOROS_PREFLIGHT_SERIAL", raising=False)
monkeypatch.delenv("OUROBOROS_PREFLIGHT_TIMEOUT_SEC", raising=False)
monkeypatch.setattr(pr, "_verify_preflight_plugins", lambda *a: [])
@ -655,20 +656,6 @@ def _look(monkeypatch, calls):
monkeypatch.setattr(commit_gate, "run_commit_preflight", look)
def test_the_system_prompt_says_which_look_the_commit_record_counts():
"""The model's own prompt states the record rule the tests below pin: only the row
named in the ``commit_reviewed`` call itself is the commit's preflight; a separate
``preflight_review`` is an early look of its own; no name records not performed."""
import pathlib
text = (pathlib.Path(__file__).resolve().parents[1] / "prompts" / "SYSTEM.md").read_text(encoding="utf-8")
phrase = " ".join(text.split())
assert ("the commit's record counts an early look only when that call itself names the row "
"(`commit_reviewed(preflight_reviewer=…)`)") in phrase
assert "a separate `preflight_review` is an early look of its own" in phrase
assert "a commit that names no row records its preflight as not performed" in phrase
@pytest.mark.parametrize("skip, status", [(False, "not_performed"), (True, "skipped")])
def test_without_a_named_row_the_record_states_the_fact_and_nothing_looks(candidate, monkeypatch, skip, status):
calls = []
@ -706,35 +693,10 @@ def test_a_doc_only_diff_pays_the_suite_like_any_other_diff(candidate, monkeypat
def test_every_site_states_the_one_tests_rule_for_a_documentation_diff():
"""Owner answer A (2026-10-08): the suite runs before any commit to the body, a
documentation-only diff included; ``skip_tests`` is the one exemption. The gate's
condition, its comments, the checklist line and the test modules that once pinned the
retired carve say the same thing, so no site describes a doc-only diff as exempt."""
"""The gate condition preserves the test exemption and managed proof requirement."""
import inspect
import pathlib
import tests.test_git_review_tests_gate as tests_gate
import tests.test_skip_tests_doc_only as classifier_tests
repo = pathlib.Path(git.__file__).resolve().parents[2]
checklist = (repo / "docs" / "CHECKLISTS.md").read_text(encoding="utf-8")
sites = {
"git._preflight_and_tests_gate": inspect.getsource(git._preflight_and_tests_gate),
"git._managed_candidate_needs_proof": inspect.getsource(git._managed_candidate_needs_proof),
"docs/CHECKLISTS.md": checklist,
"tests/test_git_review_tests_gate.py": tests_gate.__doc__ or "",
"tests/test_skip_tests_doc_only.py": classifier_tests.__doc__ or "",
"this module": TestReleaseMetadataPreflight.test_doc_only_carve_is_the_commit_gate_classifier.__doc__ or "",
}
retired = ("exempt from the suite", "preflight bypass", "skip_tests/doc-only",
"the tests rule applies", "diff-aware")
for name, text in sites.items():
for phrase in retired:
assert phrase not in text, (name, phrase)
assert "if not message and (not skip_tests or _managed_needs_proof):" in sites["git._preflight_and_tests_gate"]
assert "documentation has tests too" in sites["git._preflight_and_tests_gate"]
assert "a documentation-only diff included" in checklist
assert "a documentation-only diff included" in sites["tests/test_git_review_tests_gate.py"]
assert "if not message and (not skip_tests or _managed_needs_proof):" in inspect.getsource(git._preflight_and_tests_gate)
def test_a_syntax_error_blocks_before_the_tests_and_the_look(candidate, monkeypatch):

View file

@ -17,14 +17,6 @@ def _heartbeat_ctx(tmp_path, task):
), pushed
def test_routine_heartbeat_is_not_rendered_as_chat_message() -> None:
from supervisor import queue
source = inspect.getsource(queue._enforce_task_timeouts_locked)
assert "running for" not in source
assert "heartbeat_lag=" not in source
def test_liveness_and_incident_controls_remain_active() -> None:
from supervisor import queue
@ -302,7 +294,5 @@ def test_routine_cancel_outcomes_publish_nothing_to_the_owner_chat(monkeypatch)
_handle_cancel_task({"task_id": ""}, _Ctx2())
assert sent == []
# The handler no longer carries the prose that leaked, nor a send at all: source pin.
src = inspect.getsource(_handle_cancel_task)
for leaked in ("teardown confirmed", "had already finished", "no such live task", "send_with_budget("):
assert leaked not in src
# Custody owns presentation; this event handler must not send its own row.
assert "send_with_budget(" not in inspect.getsource(_handle_cancel_task)

View file

@ -11,8 +11,8 @@ Three structural guarantees, pinned independently of any single writer:
(``write_bytes_atomic``, ``write_text_atomic``, ``atomic_write_json``,
``supervisor.state.atomic_write_text``) replaces through it, so guarding it
guards them all rather than one writer at a time (RES-14b).
3. Batch recipe: a real run of the update-merge suites under a throwaway HOME
plus full OUROBOROS_* isolation leaves the live-shaped data root untouched
3. Batch recipe: a real child pytest running supervisor writers under a throwaway
HOME plus full OUROBOROS_* isolation leaves the live-shaped data root untouched
(pre/post inventory delta must be empty) — the AGENTS hermeticity etalon
(``find <live-root> -newermt <start>`` empty) as a regression test.
"""
@ -30,6 +30,93 @@ from ouroboros.utils import append_jsonl, atomic_write_json, write_bytes_atomic,
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
def _run_hermetic_child_pytest(args, env):
from ouroboros.platform_layer import kill_process_tree
from ouroboros.process_containment import ProcessContainer
container = ProcessContainer()
proc = None
try:
proc = container.spawn(
[sys.executable, "-m", "pytest", "-q", "-x", "-p", "no:cacheprovider", *args],
cwd=str(REPO_ROOT), env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
)
# Bound a stuck child; this is a custody budget, never a speed assertion.
stdout, stderr = proc.communicate(timeout=900)
return subprocess.CompletedProcess(proc.args, proc.returncode, stdout, stderr)
finally:
try:
if proc is not None and proc.poll() is None:
kill_process_tree(proc)
finally:
try:
error = container.reap() # Includes descendants after an already-exited root.
if proc is not None:
# Bound kernel reaping after the kill, not application behavior.
proc.wait(timeout=10)
assert not error, error
finally:
container.close()
if proc is not None:
for stream in (proc.stdout, proc.stderr):
if stream is not None:
stream.close()
_HERMETIC_WRITER_SUITE = '''
import json
import os
import pathlib
import pytest
def test_supervisor_roots_and_real_writers_are_isolated(monkeypatch):
from ouroboros import config
from ouroboros.utils import append_jsonl, atomic_write_json, write_bytes_atomic, write_text_atomic
from supervisor import git_ops, queue, state, update_merge, workers
root = pathlib.Path(os.environ["OUROBOROS_DATA_DIR"]).resolve()
live = pathlib.Path(LIVE_SHAPED).resolve()
assert root != live and live not in root.parents
assert config.DATA_DIR.resolve() == root
assert config.SETTINGS_PATH.resolve() == root / "settings.json"
for owner in (git_ops, queue, state, workers):
assert owner.DRIVE_ROOT.resolve() == root, owner.__name__
assert state.STATE_PATH == root / "state" / "state.json"
assert queue.QUEUE_SNAPSHOT_PATH == root / "state" / "queue_snapshot.json"
assert git_ops.REPO_DIR == workers.REPO_DIR == root / "repo"
# The update-merge logging path that originally leaked, with no fake writer.
assert update_merge._log_supervisor({"type": "hermetic_writer_probe", "probe": True})
rows = [json.loads(line) for line in
(root / "logs" / "supervisor.jsonl").read_text(encoding="utf-8").splitlines()]
assert [row["probe"] for row in rows if row.get("type") == "hermetic_writer_probe"] == [True]
direct = root / "logs" / "direct.jsonl"
assert append_jsonl(direct, {"probe": True})
assert json.loads(direct.read_text(encoding="utf-8")) == {"probe": True}
writers = (
("bytes", lambda p: write_bytes_atomic(p, b'{"probe": true}')),
("text", lambda p: write_text_atomic(p, '{"probe": true}')),
("json", lambda p: atomic_write_json(p, {"probe": True})),
("state", lambda p: state.atomic_write_text(p, '{"probe": true}')),
)
for label, write in writers:
target = root / "writer-probe" / (label + ".json")
write(target)
assert json.loads(target.read_text(encoding="utf-8")) == {"probe": True}, label
# Both shared writer seams must also refuse the parent's synthetic live root.
monkeypatch.setenv("OUROBOROS_TEST_LIVE_DATA_ROOT", str(live))
monkeypatch.delenv("OUROBOROS_ALLOW_LIVE_DATA_TESTS", raising=False)
with pytest.raises(RuntimeError, match="PYTEST_LIVE_DATA_WRITE_BLOCKED"):
append_jsonl(live / "logs" / "probe.jsonl", {"probe": True})
with pytest.raises(RuntimeError, match="PYTEST_LIVE_DATA_WRITE_BLOCKED"):
atomic_write_json(live / "state" / "probe.json", {"probe": True})
'''
def test_append_jsonl_fails_closed_on_live_root_write(monkeypatch, tmp_path):
fake_live = tmp_path / "fake-live"
monkeypatch.setenv("OUROBOROS_PYTEST_ACTIVE", "1")
@ -72,20 +159,18 @@ def test_every_atomic_writer_fails_closed_on_live_root_write(monkeypatch, tmp_pa
@pytest.mark.serial
def test_update_merge_suites_leave_live_shaped_root_untouched(tmp_path):
"""The E2E class pin: the exact suites that leaked in the live repro run
under a throwaway HOME + full env isolation, and the live-shaped root
(``$HOME/Ouroboros/data``) shows an EMPTY pre/post inventory delta."""
"""Keep the historical node's child-startup/writer isolation witness.
Update/stash scenarios run in their own modules; replaying them here only
repeats their git fixtures. This child reaches their actual logging writer
and both durable-write seams after the real conftest isolation boundary.
"""
fake_home = tmp_path / "home"
iso = tmp_path / "iso"
fake_home.mkdir()
# The suites commit in throwaway repos with repo-local identity, but a
# hermetic HOME must still offer a global one for any stray `git commit`.
(fake_home / ".gitconfig").write_text(
"[user]\n\tname = hermetic-pin\n\temail = hermetic-pin@example.invalid\n",
encoding="utf-8",
)
env = {**os.environ,
"HOME": str(fake_home),
"USERPROFILE": str(fake_home),
"OUROBOROS_APP_ROOT": str(iso),
"OUROBOROS_REPO_DIR": str(iso / "repo"),
"OUROBOROS_DATA_DIR": str(iso / "data"),
@ -98,14 +183,18 @@ def test_update_merge_suites_leave_live_shaped_root_untouched(tmp_path):
pre_inventory = sorted(str(p) for p in live_shaped.rglob("*")) if live_shaped.exists() else []
assert pre_inventory == []
proc = subprocess.run(
[sys.executable, "-m", "pytest", "-q", "-x", "-p", "no:cacheprovider",
"tests/test_update_dirty_stash.py", "tests/test_update_merge_plan.py"],
cwd=str(REPO_ROOT), env=env, capture_output=True, text=True, timeout=900,
suite = tmp_path / "child-suite"
suite.mkdir()
child = suite / "test_root_writers.py"
child.write_text(f"LIVE_SHAPED = {str(live_shaped)!r}\n" + _HERMETIC_WRITER_SUITE,
encoding="utf-8")
proc = _run_hermetic_child_pytest(
["-p", "tests.conftest", "-o", "addopts=", "-n", "0",
"--rootdir", str(suite), "--confcutdir", str(suite), str(child)], env,
)
assert proc.returncode == 0, (proc.stdout or "")[-4000:] + (proc.stderr or "")[-2000:]
post_inventory = sorted(str(p) for p in live_shaped.rglob("*")) if live_shaped.exists() else []
assert post_inventory == [], (
f"update-merge suites leaked into the live-shaped data root: {post_inventory}"
f"supervisor writers leaked into the live-shaped data root: {post_inventory}"
)

View file

@ -1,34 +1,4 @@
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
def test_prompts_do_not_infer_current_human_from_authors():
system = (REPO_ROOT / "prompts" / "SYSTEM.md").read_text(encoding="utf-8")
consciousness = (REPO_ROOT / "prompts" / "CONSCIOUSNESS.md").read_text(encoding="utf-8")
memory = (REPO_ROOT / "ouroboros" / "memory.py").read_text(encoding="utf-8")
assert "my human" in system
assert "I do not know their name" in system
assert "README, BIBLE, git history, or author" in system
assert "your human" in consciousness and "the user" not in consciousness
assert "I do not yet know my human's name or profile" in memory
assert "Anton" not in system
assert "Razzhigaev" not in system
def test_live_task_message_marker_uses_my_human_wording(tmp_path):
system = (REPO_ROOT / "prompts" / "SYSTEM.md").read_text(encoding="utf-8")
# v7 L-B split: sweep the whole loop family (facade + leaves).
loop_dir = REPO_ROOT / "ouroboros"
loop = "".join(
path.read_text(encoding="utf-8")
for path in [loop_dir / "loop.py", *sorted(loop_dir.glob("loop_*.py"))]
)
tools = (REPO_ROOT / "ouroboros" / "tools" / "core.py").read_text(encoding="utf-8")
assert "[Message from my human]" in system
# The drained mailbox text (plus its optional surface note) must still go
# through the owner-marking wrapper before injection.
import queue
@ -67,47 +37,3 @@ def test_live_task_message_marker_uses_my_human_wording(tmp_path):
"[Message from independent task t-source]")
assert rendered["ancestor_task"].startswith("[Message from ancestor task t-source]")
assert rendered[""] == rendered["ancestor_task"], "unknown provenance keeps the tree fallback"
assert "[Message from my human]" not in tools
assert "[Owner message during task]" not in system
assert "[Owner message during task]" not in loop
def test_system_prompt_carries_outcome_honesty_and_capability_acquisition():
"""v6.29.0 doctrine pins: the outcome-honesty doctrine and the capability-
acquisition boldness clause must stay in SYSTEM.md. The doctrine now says
the three endings in WORDS: the ledger identifiers belong to the reviewers'
JSON contract, and a prompt that spells them is a prompt the model parrots
back to its human. v6.60.0: the FINAL ANSWER marker doctrine deliberately
MOVED to the per-task contract (answer_protocol) — the default prompt must
NOT carry it (ordinary tasks never see the marker)."""
import pathlib
text = (pathlib.Path(__file__).parent.parent / "prompts" / "SYSTEM.md").read_text(encoding="utf-8")
assert "### Outcome honesty" in text
# Whitespace-normalized: the doctrine sentence is line-wrapped in the file.
normalized = " ".join(text.split())
assert "I do not abandon an owed answer" in normalized
assert "Presence observation may deliberately end silently without leaving accepted work unfinished" in normalized
assert "blocked_with_evidence" not in text
assert "best_effort" not in text
# Whitespace-normalized: a line-wrapped "FINAL\nANSWER" must not slip past.
assert "FINAL ANSWER" not in " ".join(text.split())
assert "## Capability Acquisition" in text
assert "NOT a \"broad fallback or shim\"" in text
# #1323: the owner-approved generic opening, byte for byte, replaces the narrow
# "acquisition step, not a blocker" opening; the concrete dependency means stay.
section = text.split("## Capability Acquisition", 1)[1].split("\n## ", 1)[0]
assert section.strip().startswith(
"Before declaring a task blocked, establish what capability or resource is actually missing, "
"using evidence available within the task\u2019s scope. Distinguish unavailability from lack of "
"authority to use it. When an authorized means is available, use it; otherwise name the specific "
"blocker and the next action. Availability alone grants no permission.")
assert "not a blocker" not in section
assert "(`pip`/`uv`/`pip3`/`brew`/`apt`)" in section and "credential" not in section.lower()
def test_public_publishing_still_requires_creator_permission():
system = (REPO_ROOT / "prompts" / "SYSTEM.md").read_text(encoding="utf-8")
assert "Do not publish or make content publicly accessible" in system
assert "permission from the creator" in system
assert "My human may grant that permission only if they" in system

View file

@ -1,9 +1,9 @@
"""A timed-out image call stays findable: real blocked reads, the real executor, recover_image().
"""A timed-out image call stays findable through the executor and recover_image().
The executor that abandons a timed-out tool reports a bare TOOL_TIMEOUT; its worker keeps the
paid attempt and may still adopt the response. A wire mock cannot block a socket read, so each
peer is a real 127.0.0.1 HTTP server whose chosen reply waits for the test. The deadline clamp
is the real one.
Recovery-state permutations pause an in-process HTTP exchange and inject only the caller's
expired wait. The real worker, gateway, ledger and late adoption still run. Four socket cases
keep the physical deadline proof: an engine result, late headers, a trickling body and a URL
image download. Every call checks the real near-deadline clamp.
"""
from __future__ import annotations
@ -65,6 +65,33 @@ class _Peer(ThreadingHTTPServer):
self.loop.join()
class _Exchange:
"""Pause a transport exchange without waiting out another physical deadline."""
url, loop = "http://127.0.0.1:1", None # intercepted before any socket can be opened
def __init__(self, app, hold=lambda _request: False):
self.app, self.hold, self.requests = app, hold, []
self.release, self.entered = threading.Event(), threading.Event()
def send(self, request):
assert request.url.host == "127.0.0.1", request.url
self.requests.append((request.method, request.url.path))
if self.hold(request):
self.entered.set()
assert self.release.wait(60), "test did not release its paused image exchange"
return self.app(request)
def expired_wait(self, future, _timeout):
# This bounds a broken test's rendezvous, not a product timeout or outcome.
assert self.entered.wait(10), "the image worker did not reach the selected exchange"
assert not future.done(), "the exchange must still be held when the caller leaves"
raise TimeoutError("injected expiry after the image exchange entered")
def close(self):
self.release.set()
class _Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
@ -113,7 +140,11 @@ class _Handler(BaseHTTPRequestHandler):
@pytest.fixture
def peers():
opened, before = [], set(threading.enumerate())
yield lambda *args: opened.append(_Peer(*args)) or opened[-1]
def open_peer(*args, real_socket=True):
opened.append((_Peer if real_socket else _Exchange)(*args))
return opened[-1]
yield open_peer
for peer in opened:
peer.release.set()
for thread in set(threading.enumerate()) - before - {peer.loop for peer in opened}:
@ -133,8 +164,8 @@ def _nth(method, path, nth=1):
return hold
def _engine(monkeypatch, peers, wire, hold=lambda _request: False):
peer = peers(wire, hold)
def _engine(monkeypatch, peers, wire, hold=lambda _request: False, *, real_socket=True):
peer = peers(wire, hold, real_socket=real_socket)
port = int(peer.url.rsplit(":", 1)[1])
gateway = lambda: cx.ClaudexorGateway(cx.DaemonEndpoint("127.0.0.1", port, "test-control-token")) # noqa: E731
monkeypatch.setattr("ouroboros.claudexor_daemon.read_owned_gateway", gateway)
@ -143,8 +174,9 @@ def _engine(monkeypatch, peers, wire, hold=lambda _request: False):
return peer
def _direct(monkeypatch, peers, app, hold=lambda _request: False, shape=lambda _request: None):
peer = peers(app, hold, shape)
def _direct(monkeypatch, peers, app, hold=lambda _request: False, shape=lambda _request: None,
*, real_socket=True):
peer = peers(app, hold, shape) if real_socket else peers(app, hold, real_socket=False)
monkeypatch.setattr(image_api, "resolve_image_target", lambda _model: {
"provider": "openai", "model": "gpt-image-2", "base_url": peer.url + "/v1",
"api_key": "fixture-secret", "headers": {}})
@ -154,12 +186,17 @@ def _direct(monkeypatch, peers, app, hold=lambda _request: False, shape=lambda _
return peer
def _run(ctx, monkeypatch, peer, arguments, *, name="generate_image", during=None, keep_blocked=False):
def _run(ctx, monkeypatch, peer, arguments, *, name="generate_image", during=None,
keep_blocked=False, expire_after_entry=False):
"""One call through the real clamp, executor, registry and wait owner. ``during`` runs
after the executor answered and before the blocked peer is released."""
if peer.release.is_set():
peer.release = threading.Event() # a spent release must not pre-release this call's hold
monkeypatch.setattr(httpx.HTTPTransport, "handle_request", _loopback_only)
if isinstance(peer, _Exchange):
peer.entered.clear()
monkeypatch.setattr(httpx.HTTPTransport, "handle_request", lambda _transport, request: peer.send(request))
else:
monkeypatch.setattr(httpx.HTTPTransport, "handle_request", _loopback_only)
registry, task = _setup(ctx, monkeypatch)
# Stay inside the integer clamp bucket even when both reads share a Windows clock tick.
deadline = datetime.now(timezone.utc) + timedelta(seconds=effective_finalization_reserve_sec(ctx) + _CLAMP_SEC + 0.5)
@ -170,7 +207,11 @@ def _run(ctx, monkeypatch, peer, arguments, *, name="generate_image", during=Non
before, started = set(threading.enumerate()), time.monotonic()
with model_wait.task_model_wait_scope(task=task, drive_root=ctx.drive_root,
event_queue=None, worker_slot_held=False) as owner:
result = execution._execute_with_timeout(registry, call, ctx.drive_root / "logs", timeout, ctx.task_id)
with monkeypatch.context() as wait:
if expire_after_entry:
assert isinstance(peer, _Exchange)
wait.setattr(execution, "future_result", peer.expired_wait)
result = execution._execute_with_timeout(registry, call, ctx.drive_root / "logs", timeout, ctx.task_id)
elapsed = time.monotonic() - started
assert owner.control_reason() is None, "the tool's own bound must not end the task"
seen = during(registry) if during else None
@ -220,7 +261,9 @@ def test_timed_out_engine_attempt_is_listed_then_recovered_by_its_original_id(ct
hold = {"create-lost": _nth("POST", "/v2/model-operations"), "poll": _nth("GET", "/v2/model-operations/op-one"),
"result": _nth("GET", _RESULT), "ack": _nth("POST", "/v2/model-operations/op-one/ack"),
"recovery": _nth("GET", _RESULT, 2)}.get(stage, lambda _request: False)
peer = _engine(monkeypatch, peers, wire, hold)
# The result case retains the real blocked socket / 15-second executor boundary.
# Other stages preserve all recovery assertions without repeating that wall wait.
peer = _engine(monkeypatch, peers, wire, hold, real_socket=stage == "result")
acks = lambda: [call for call in wire.calls if call.url.path.endswith("/ack")] # noqa: E731
arguments, name = {"prompt": "fixture", "model": _MODEL}, "generate_image"
if stage == "recovery":
@ -230,7 +273,8 @@ def test_timed_out_engine_attempt_is_listed_then_recovered_by_its_original_id(ct
arguments, name = {"request_id": _summary(first["result"])["request_id"]}, "recover_image"
found = {}
registry, result, elapsed, _ = _run(ctx, monkeypatch, peer, arguments, name=name,
during=lambda registry: found.update(_discover(registry, ctx, peer)))
during=lambda registry: found.update(_discover(registry, ctx, peer)),
expire_after_entry=stage not in {"none", "result"})
[listed] = found["attempts"]
request_id = listed["request_id"]
assert found["has_more"] is False and (listed["model"], listed["provider"]) == (_MODEL, "claudexor")
@ -305,18 +349,20 @@ def test_timed_out_direct_attempt_is_listed_then_recovered_by_its_original_id(ct
def test_concurrent_same_task_attempts_are_listed_separately_and_never_across_tasks(ctx, monkeypatch, peers):
def app(_request):
return httpx.Response(200, json={"data": [{"b64_json": base64.b64encode(PNG).decode()}], "padding": "." * 600})
peer = _direct(monkeypatch, peers, app, shape=lambda request: "trickle" if request.method == "POST" else None)
peer = _direct(monkeypatch, peers, app, hold=lambda request: request.method == "POST", real_socket=False)
other = ToolContext(repo_dir=ctx.repo_dir, drive_root=ctx.drive_root, task_id="other-image-task",
workspace_root=ctx.workspace_root, workspace_mode="external")
usage_accounting.reserve_attempt(usage_accounting.AttemptRequest(
model="openai::gpt-image-2", provider="openai", force_unknown_reservation=True, drive_root=ctx.drive_root,
task_id=other.task_id, category="image_generation", source="imagegen"))
arguments = {"prompt": "fixture", "model": "openai::gpt-image-2"}
_registry, first, elapsed, _ = _run(ctx, monkeypatch, peer, arguments, keep_blocked=True)
_registry, first, elapsed, _ = _run(ctx, monkeypatch, peer, arguments, keep_blocked=True,
expire_after_entry=True)
_honest_timeout(first, elapsed)
found = {}
_registry, second, elapsed, _ = _run(ctx, monkeypatch, peer, arguments,
during=lambda registry: found.update(_discover(registry, ctx, peer)))
during=lambda registry: found.update(_discover(registry, ctx, peer)),
expire_after_entry=True)
_honest_timeout(second, elapsed)
ids = [row["request_id"] for row in found["attempts"]]
# Both in-flight attempts, newest first; neither is chosen, the other task's is absent.

View file

@ -16,7 +16,6 @@ surface that used to carry the ghost.
from __future__ import annotations
import ast
import inspect
import pathlib
import subprocess
@ -83,29 +82,6 @@ def test_neither_init_still_asks_a_caller_for_a_value_it_discards():
assert "hard_timeout" not in inspect.signature(workers.init).parameters
def test_the_deprecation_event_path_is_gone_with_its_keys():
"""A retired key has nothing left to be loud about; keeping the emitter
would leave a dead branch that only a future non-default value could
reach — and there can be no future value."""
from supervisor import queue
source = inspect.getsource(queue)
assert "deprecated_settings_ignored" not in source
assert not hasattr(queue, "_emit_timeout_deprecation_once")
assert not hasattr(queue, "_timeout_deprecation_emitted")
def test_status_no_longer_renders_a_legacy_timeout_line():
"""``/status`` printed ``legacy_timeouts_ignored: soft=600s, hard=1800s``
on every request — an owner-visible report of two constants."""
from supervisor import state
assert list(inspect.signature(state.status_text).parameters) == [
"workers_dict", "pending_list", "running_dict",
]
assert "legacy_timeouts_ignored" not in inspect.getsource(state.status_text)
def test_no_runtime_or_settings_surface_still_names_either_key():
"""The grep-class assertion. The N−1 and N−2 audit fixtures are the files
that must keep them: frozen byte-copies of real v6.113.4 / v6.87.5 settings
@ -188,14 +164,3 @@ def test_surface_scan_excludes_ignored_exports_but_checks_tracked_and_new_source
test_no_runtime_or_settings_surface_still_names_either_key()
(tmp_path / 'new.py').write_text('current = True')
test_no_runtime_or_settings_surface_still_names_either_key()
def test_the_ledger_row_is_not_a_second_dispatcher_for_the_status_line():
"""Guards the shape of the removal itself: ``status_text`` must not have
grown a replacement knob while losing the old one."""
tree = ast.parse((REPO / "supervisor" / "state.py").read_text(encoding="utf-8"))
fn = next(node for node in tree.body
if isinstance(node, ast.FunctionDef) and node.name == "status_text")
names = {node.value for node in ast.walk(fn)
if isinstance(node, ast.Constant) and isinstance(node.value, str)}
assert not any("TIMEOUT_SEC" in name for name in names)

View file

@ -8,15 +8,16 @@ left to the maintainer.)
from __future__ import annotations
import pathlib
REPO = pathlib.Path(__file__).resolve().parents[1]
def test_backoff_doubled_with_cap():
"""Backoff stays exponential (x4 base) and per-class capped: 30s for
generic retryable errors, 60s for transient provider classes (v6.28.0)."""
src = (REPO / "ouroboros" / "loop_llm_call.py").read_text(encoding="utf-8")
assert "2.0 ** attempt * 4" in src # doubled per-attempt backoff
assert "min(2 ** attempt * 2, 30)" not in src # old value gone
assert "_TRANSIENT_BACKOFF_CAP_SEC if is_transient else 30.0" in src
generic retryable errors, the transient cap for provider classes."""
from ouroboros.loop_llm_call import _TRANSIENT_BACKOFF_CAP_SEC, _retry_backoff_sec
assert _retry_backoff_sec(0, False) == 4.0
assert _retry_backoff_sec(1, False) == 8.0
assert _retry_backoff_sec(2, False) == 16.0
assert _retry_backoff_sec(3, False) == 30.0
assert _retry_backoff_sec(4, False) == 30.0
assert _retry_backoff_sec(0, True) == min(4.0, _TRANSIENT_BACKOFF_CAP_SEC)
assert _retry_backoff_sec(10, True) == float(_TRANSIENT_BACKOFF_CAP_SEC)

View file

@ -558,10 +558,3 @@ class TestSupportedParametersFilter:
)
# Unknown model → cache miss → None → no stripping
assert kwargs.get("temperature") == 0.2
def test_no_version_specific_sampling_gate_remains(self):
import pathlib
llm_text = pathlib.Path(__file__).resolve().parents[1].joinpath("ouroboros", "llm.py").read_text(encoding="utf-8")
assert "claude-opus-4-7" not in llm_text
assert "claude-opus-4.7" not in llm_text

View file

@ -80,24 +80,6 @@ def test_manual_request_uses_the_shared_typed_materializer(tmp_path, monkeypatch
assert context.tools._ctx._pending_compaction is None
def test_old_main_trigger_authorities_are_deleted():
from pathlib import Path
source = Path("ouroboros/loop.py").read_text(encoding="utf-8")
budget = Path("ouroboros/context_budget.py").read_text(encoding="utf-8")
for symbol in (
"EMERGENCY_COMPACTION_CHARS",
"LOW_EMERGENCY_COMPACTION_CHARS",
"COMPACTION_HYSTERESIS_REGION_GROWTH",
"COMPACTION_HYSTERESIS_ROUNDS",
"_compaction_floor_chars",
"_emergency_keep_recent",
):
assert symbol not in source
assert symbol not in budget
assert "round_idx > 6" not in source
# --- earlier capsules are re-folded only after the provider's own refusal ----------------------
def _long_earlier_capsule(run, monkeypatch):

View file

@ -50,7 +50,7 @@ def _read_network_wait_events(tmp_path):
path = tmp_path / "events.jsonl"
if not path.exists():
return []
rows = [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
return [row for row in rows if row.get("type") == "network_wait"]
@ -645,7 +645,7 @@ def test_end_to_end_episode_through_real_retry_helper(tmp_path, monkeypatch):
assert len(sleeps) == 2
phases = [row["phase"] for row in _read_network_wait_events(tmp_path)]
assert phases == ["entered", "waiting", "waiting", "recovered"]
rows = [json.loads(line) for line in (tmp_path / "events.jsonl").read_text().splitlines() if line.strip()]
rows = [json.loads(line) for line in (tmp_path / "events.jsonl").read_text(encoding="utf-8").splitlines() if line.strip()]
api_errors = [row for row in rows if row.get("type") == "llm_api_error"]
assert api_errors and all(row.get("error_kind") == "transport_unavailable" for row in api_errors)
@ -773,11 +773,14 @@ def test_finalize_now_during_episode_takes_no_resend_terminal_via_mailbox(tmp_pa
monkeypatch.delenv("USE_LOCAL_FALLBACK", raising=False)
registry = ToolRegistry(repo_dir=tmp_path, drive_root=tmp_path)
notes = []
start = time.monotonic()
result, usage, trace = run_llm_loop(**_loop_kwargs(tmp_path, registry, notes))
elapsed = time.monotonic() - start
def forbidden_sleep(_seconds):
pytest.fail("finalize_now must wake the episode before any backoff sleep")
# Patch this transport's clock namespace, leaving unrelated store/fixture waits alone.
monkeypatch.setattr(loop_transport, "time", SimpleNamespace(
monotonic=time.monotonic, sleep=forbidden_sleep))
result, usage, trace = run_llm_loop(**_loop_kwargs(tmp_path, registry, notes))
assert elapsed < 3.5 # the wake check saw the control before the 4 s backoff: no sleep ran
assert calls["n"] == 1 # the woken redial exited at the round top: zero further dials
assert usage.get("execution_status") == "infra_failed"
assert usage.get("reason_code") == "provider_unavailable"
@ -885,7 +888,7 @@ def test_episode_ledger_evidence_has_zero_dispatched_paid_attempts(tmp_path, mon
**_loop_kwargs(tmp_path, ToolRegistry(repo_dir=tmp_path, drive_root=tmp_path), notes, llm=llm))
assert result == "done"
rows = [json.loads(line) for line in (tmp_path / "events.jsonl").read_text().splitlines() if line.strip()]
rows = [json.loads(line) for line in (tmp_path / "events.jsonl").read_text(encoding="utf-8").splitlines() if line.strip()]
entered_idx = next(i for i, r in enumerate(rows) if r.get("type") == "network_wait" and r.get("phase") == "entered")
last_wait_idx = max(i for i, r in enumerate(rows) if r.get("type") == "network_wait" and r.get("phase") == "waiting")
window = rows[entered_idx:last_wait_idx + 1]

View file

@ -179,9 +179,10 @@ def test_actual_exit_consumers_keep_blocked_discovery_visible_then_accept_exit(r
import server
from ouroboros import body_adoption
from tests.test_body_adoption_consumers import EXITED
from tests.body_candidate_support import make_serving, make_ctx, rich_commit, restart_receipt
from tests.body_candidate_support import make_serving, make_ctx, rich_commit, restart_receipt, stub_adoption_evidence
from ouroboros import body_candidate, body_switch
stub_adoption_evidence(monkeypatch) # this test exits maintenance, not the test battery
serving = make_serving(root.parent / "body")
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "pro")
# These are disposable repositories/data, never the installation.

View file

@ -132,13 +132,6 @@ def test_mcp_css_defines_required_classes(settings_css: str) -> None:
assert 'class="form-field ui-field"' in source
def test_settings_ui_mcp_section_describes_hot_reload(settings_ui_source: str) -> None:
"""The UI copy must communicate that MCP changes are hot-reloadable
(it's the ergonomic difference vs A2A which requires restart)."""
assert "Hot-reloadable" in settings_ui_source or "hot-reloadable" in settings_ui_source
assert "untrusted third-party data" in settings_ui_source
def test_mcp_ui_roundtrip_preserves_environment_and_unsupported_fields():
"""Execute the real JS projection; this is not a browser/visual receipt."""
node = _node_bin()

View file

@ -7,7 +7,6 @@ retention; the compatibility entry point is intentionally non-destructive.
from __future__ import annotations
import inspect
import json
import pytest
@ -51,12 +50,6 @@ def test_maintenance_does_not_create_missing_journals_or_directories(tmp_path):
assert not root.exists()
def test_startup_prune_still_reaches_compatibility_entry_point():
import ouroboros.server_maintenance as maintenance
assert "compact_memory_journal_snapshots" in inspect.getsource(maintenance._run_deferred_startup_prunes)
def test_size_observation_does_not_follow_a_journal_symlink(tmp_path):
target = tmp_path / "elsewhere"
target.write_bytes(b"secret data")
@ -81,12 +74,15 @@ def test_startup_event_publishes_normal_journal_sizes(tmp_path, monkeypatch):
journal.write_bytes(b"full historical text\n")
rows = []
monkeypatch.setattr(maintenance, "DATA_DIR", tmp_path)
monkeypatch.setattr(maintenance, "_STARTUP_PRUNES_OWED", [True])
monkeypatch.setattr(maintenance, "_STARTUP_TREES_OWED", [False])
monkeypatch.setattr(maintenance, "_STARTUP_SOURCE_PRUNES_OWED", [False])
monkeypatch.setattr(maintenance, "_startup_worktree_prune", lambda: None)
monkeypatch.setattr(maintenance, "_prune_delegated_snapshots", lambda: None)
monkeypatch.setattr(state, "append_jsonl", lambda _path, row: rows.append(row))
report = compact_memory_journal_snapshots(tmp_path)
maintenance._prune_event("memory_journal_observation", ("journal_bytes", "errors"), report=report)
assert len(rows) == 1
assert rows[0]["report"]["journal_bytes"]["memory/knowledge_history.jsonl"] == len(b"full historical text\n")
maintenance._run_deferred_startup_prunes()
[observation] = [row for row in rows if row["type"] == "memory_journal_observation"]
assert observation["report"]["journal_bytes"]["memory/knowledge_history.jsonl"] == len(b"full historical text\n")
assert journal.read_bytes() == b"full historical text\n"
# Pin the real startup caller, not only this unit invocation.
source = inspect.getsource(maintenance._run_deferred_startup_prunes)
assert '_prune_event("memory_journal_observation", ("journal_bytes", "errors")' in source

View file

@ -11,7 +11,6 @@ still nominates notes writes none.
from __future__ import annotations
import inspect
import json
import pytest
@ -211,4 +210,3 @@ def test_the_reader_less_knowledge_journal_is_no_longer_written(tmp_path):
assert store.write_knowledge_note(_address(tmp_path), "# Alex\n\nFirst.").ok
assert not (tmp_path / "memory" / "knowledge_journal.jsonl").exists()
assert (tmp_path / "memory" / "knowledge_history.jsonl").exists()
assert "knowledge_journal" not in inspect.getsource(store)

View file

@ -1,186 +0,0 @@
"""People are a subject of Ouroboros's memory, stated in its own prompts.
Nothing in the runtime prompts used to say that understanding a person is
durable knowledge: SYSTEM.md scoped the knowledge base to operational facts,
recipes and gotchas, the background checklist asked only for a gotcha, a recipe
or a pattern, and reflection nominated "self-knowledge". The observable result
on a live install was that everything ever written about the human had been
dictated word for word, never noticed and never inferred.
These are vocabulary pins for the memory contract — which memory each kind of
understanding has, and where it lives. They pin the words that carry each
contract, not a model's behaviour; dropping one of these contracts is the defect.
"""
from __future__ import annotations
import pathlib
import re
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
def _read(rel: str) -> str:
return (REPO_ROOT / rel).read_text(encoding="utf-8")
def _section(text: str, heading: str) -> str:
"""The body of one `## `/`### ` section, whitespace-normalized."""
marker = re.search(rf"^#{{2,3}} {re.escape(heading)}\s*$", text, re.MULTILINE)
assert marker, f"section {heading!r} is gone"
rest = text[marker.end():]
end = re.search(r"^#{2,3} ", rest, re.MULTILINE)
return " ".join((rest[: end.start()] if end else rest).split())
def _claims(section: str) -> list:
"""Sentence-ish units, lowercased: a contract may move between sentences."""
return [part.strip() for part in re.split(r"(?<=[.;:])\s+", section.lower()) if part.strip()]
def test_system_memory_section_makes_people_a_subject_of_knowledge():
memory = _section(_read("prompts/SYSTEM.md"), "Memory")
claims = _claims(memory)
# Knowledge is understanding of every kind, people explicitly included.
assert any("people" in c and ("knowledge" in c or "understanding" in c) for c in claims)
# Attribution: told me / observed / inferred are kept apart.
assert any("infer" in c and ("told me" in c or "observed" in c) for c in claims)
# A correction is situated evidence, not a standing rule.
assert any("correction" in c and "standing rule" in c for c in claims)
# Repeating an interpretation does not make it more true.
assert any("interpretation" in c and ("one interpretation" in c or "not new" in c) for c in claims)
# The authored summary is the resident face of a note.
assert any("summary" in c and "index" in c for c in claims)
# The overview is the shared orientation loaded into every context.
assert any("overview" in c and "context" in c for c in claims)
# Understanding of people is global, whatever room the work happens in.
assert any("people" in c and "global" in c for c in claims)
# What I learn about a person is written in the same turn, not left to a later summary.
assert any("same turn" in c and "person" in c for c in claims)
# The old scoping, which is what made the knowledge base operational-only.
assert "Durable operational facts, recipes, and gotchas go to knowledge topics" not in memory
def test_system_memory_section_keeps_its_earlier_obligations():
"""The section gained clauses; it must not have lost the ones that were load
bearing — provenance, the reserved index name, and dated external knowledge."""
memory = _section(_read("prompts/SYSTEM.md"), "Memory")
assert "I distinguish known, stale, missing, and inferred" in memory
assert "`knowledge_list` shows the topics" in memory
assert "`knowledge/index-full.md` is a reserved internal name" in memory
assert "I `knowledge_read` its topic first" in memory
assert "stale unless recently verified" in memory
def test_system_states_the_chronicle_contract_and_each_writer_in_one_home():
"""The chronicle's writers in the mind's own prompt: the mind seals, marks and reads
by address; the overview is the mind's; a Light helper drafts one page or part only
while the wakes are off; an offer to fold the old retelling is settled by a global
mark. A delegated child's start and its drafts live in Delegation, the
self-contained assignment in Memory. The two claims the chronicle made false
(nothing project-related hidden, read-only helpers writing nothing) are gone."""
system = _read("prompts/SYSTEM.md")
memory, delegation, tools = (_section(system, name) for name in ("Memory", "Delegation", "Tools"))
claims = _claims(memory)
for tool in ("`chronicle_write`", "`memory_mark`", "`memory_read`"):
assert tool in memory, tool
assert any("overview" in c and "my own words" in c and "helpers do not write it" in c for c in claims)
assert any("light helper" in c and "one page or part" in c and "wakes are off" in c for c in claims)
assert any("fold" in c and "ordinary background task" in c for c in claims)
assert any("global mark" in c and "`memory_mark`" in c for c in claims)
# One home each: the child's start and drafts in Delegation, the assignment rule in Memory.
assert "never my whole dialogue history" in delegation
assert "drafts in its own name, which the integrating mind accepts or rejects" in delegation
assert "drafts in its own name" not in memory
assert "must stand on its own" in memory and "must stand on its own" not in delegation
# The memory tools and the chronicle's own path are named where file writes are ruled out.
assert "`chronicle_write`, `memory_mark`" in tools and "`memory/chronicle/*`" in tools
assert "chronicle page and part drafts in their own name" in tools
# Everything the host guarantees a child at start, its memory marks included (memory_view.ROLE_DEFAULTS).
start = next(c for c in _claims(delegation) if "never my whole dialogue history" in c)
assert all(part in start for part in ("room's page", "global memory marks", "whole assignment", "verbatim"))
assert "which I accept or reject" not in delegation # a child that schedules a grandchild does not decide
flat = " ".join(system.split())
assert "nothing project-related is hidden" not in flat
assert "Read-only helpers do not write" not in flat
def test_system_names_the_natural_moments_to_seal_without_a_command_or_a_host_check():
"""The sealing rule stands word for word; right after its first sentence one sentence names the natural
moments to look at what has closed and seal it — a wake, the end of a root task — and that the view shows
what is still unsealed and since when. It is an observation about moments, not a command and not a host
check: no threshold, no reminder, no 'must'; the model decides (owner decision 2A)."""
memory = _section(_read("prompts/SYSTEM.md"), "Memory")
rule = ("People's words and my own replies stay verbatim while their conversation is open and fits; when a "
"matter closes — by its sense, not by a calendar — or the host says it no longer fits, I seal it with "
"`chronicle_write` as a page in my own words written from those rows, quoting exactly the decisive words "
"and my promises; every line of the record says who wrote it — my reply, a child's report, a host fact, "
"a helper's draft.")
moments = ("A wake and the end of a root task are natural moments to look at what has closed since my last page "
"— here and in the live rooms — and seal it; the view shows what is still unsealed and since when.")
assert f"{rule} {moments} What must stay in view I mark with `memory_mark`;" in memory
assert memory.count("natural moments") == 1
assert "natural moments" not in _read("prompts/CONSCIOUSNESS.md") # one home: the wake text does not restate it
# The standing pages line of the view is what "since when" points at: the status sentence about the old
# retelling speaks of the story status, not of my pages, so the two do not contradict.
claims = _claims(memory)
assert any("story status" in c and "old retelling" in c and "not yet all folded" in c for c in claims)
assert not any("no page" in c or "pages sealed" in c for c in claims)
def test_system_learns_a_person_through_the_relationship_not_only_on_demand():
human = _section(_read("prompts/SYSTEM.md"), "Environment and My Human")
claims = _claims(human)
# My human is a relationship, and the neutral form before acquaintance stays.
assert any("my human" in c and "relationship" in c for c in claims)
assert "I do not know their name or personal profile by default" in human
# Conversation, shared work and — by its own judgment — public work.
assert any("conversation" in c or "work we do together" in c for c in claims)
assert any("public work" in c for c in claims)
# A plain question is allowed, the name first (owner decision 10A).
assert any(("ask" in c or "question" in c) and "name" in c for c in claims)
# What is understood lives in knowledge, under the person's name.
assert any("name" in c and ("knowledge" in c or "understand" in c) for c in claims)
# "owner" is authority, never the person (owner decision 2A).
assert any("owner" in c and "authority" in c for c in claims)
# The dead end this replaced: ask, learn it, and never revisit it.
assert "if I need a name or preference, I ask and then learn it in memory" not in human
def test_memory_vocabulary_is_not_dressed_up_as_tool_names():
"""Backticked `summary`/`overview` would read as tool identifiers both to the
model and to the prompt identifier audit in tests/test_docs_sync.py."""
for rel in ("prompts/SYSTEM.md", "prompts/CONSCIOUSNESS.md"):
text = _read(rel)
for word in ("summary", "overview"):
assert f"`{word}`" not in text, f"{rel} backticks {word}"
def test_the_people_memory_contract_has_exactly_one_home():
"""The wake-up message is an ordinary Main turn's USER text, so it runs on the
system prompt that already states this contract. It used to restate it — which
made the same obligation editable in two places and drifted the moment one
changed. The contract is pinned above, in its single home; what is pinned here
is that the wake does not carry a second copy of it (P7 one home per fact)."""
memory = _section(_read("prompts/SYSTEM.md"), "Memory")
claims = _claims(memory)
# Still exactly the obligations the wake used to duplicate.
assert any("revise" in c and ("in place" in c or "same turn" in c) for c in claims)
assert any("one interpretation" in c for c in claims)
wake = " ".join(_read("prompts/CONSCIOUSNESS.md").split()).lower()
for duplicated in ("people you talk with", "rather than minting a new one",
"knowledge_read", "knowledge_write", "contradictions"):
assert duplicated not in wake, f"the wake restates {duplicated!r}"
def test_wake_template_addresses_its_human_not_a_user():
consciousness = _read("prompts/CONSCIOUSNESS.md")
assert "Speak to your human" in consciousness
assert "your human" in consciousness and "the user" not in consciousness

View file

@ -236,19 +236,3 @@ def test_data_read_missing_non_memory_path_uses_narrower_sentinel(tmp_path):
assert "DATA_NOT_YET_CREATED" in result
assert "lazily on first write" not in result
assert "not guaranteed" in result
# ---------------------------------------------------------------------------
# Section header annotation
# ---------------------------------------------------------------------------
def test_memory_section_headers_indicate_already_loaded():
"""Inspect the source: build_memory_sections must annotate identity
and scratchpad so the agent doesn't re-read them."""
import inspect
import ouroboros.context as ctx_mod
src = inspect.getsource(ctx_mod.build_memory_sections)
assert "memory/scratchpad.md" in src
assert "memory/identity.md" in src
assert "already loaded" in src
assert "do not re-read" in src

Some files were not shown because too many files have changed in this diff Show more