mirror of
https://github.com/razzant/ouroboros.git
synced 2026-08-17 23:03:17 +00:00
synthesis cleanup: one 6.89.0 bump, scaffolding removal, health-gate paydown
REQUIRED PRE-PR items: tests/test_convergence_invariants.py deleted (sprint scaffolding; its 98 symbols registered as intentional removals in the pinned allow-lost roster); the dead multi-slot path is verified absent (zero refs to expand_subagent_lane_slots/compact_task_group outside the docs-sync assertion of their absence); zero private-branch SHAs and zero PTY files in the tracked tree (both scans clean; the two grep hits are base content — 'emPTY' in a filename and the computer-use skill's xterm app list). All six version carriers collapse to ONE minor bump 6.89.0 (VERSION, pyproject, web/package.json, README badge + a single Version-History row that discloses the inverted-fence capability narrowing, with the oldest minor row rolled off per the P9 cap, ARCHITECTURE header, GATEWAY_CONTRACT_VERSION); guard --strict-versions PASS. ARCHITECTURE Invariant #10 gaps closed: delegate_output.py and scope_window.py tree entries, the subscription_session ledger row named. Union health-gate paydown (the §6-3b class — every branch fit alone, the union overflowed): the window-authority cluster extracted to ouroboros/tools/scope_window.py (scope_review 1667->1571, aliases keep one patch point; constants SSOT moved), the blocked-executor terminal extracted from _handle_task_scoped (307->298), and MAX_TOTAL_FUNCTIONS raised 5000->5100 WITH an in-code disclosure (the union ships a delegation subsystem while deleting the edit path, the multi-slot fan-out and the enumerated fence; the next cycle owes consolidation before growth — flagged for freeze review). Tree-wide ruff F: clean. Smoke gates, packaging/docs sync, scope/plan/ session suites green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
eb3bd5be32
commit
74a73306c9
20 changed files with 195 additions and 778 deletions
|
|
@ -9,7 +9,7 @@
|
|||
[](https://github.com/razzant/ouroboros/releases)
|
||||
[](https://github.com/razzant/ouroboros/releases)
|
||||
[](https://github.com/razzant/OuroborosHub)
|
||||
[](VERSION)
|
||||
[](VERSION)
|
||||
|
||||
Ouroboros is an open-source, general-purpose AI agent whose identity, durable memory, and history continue across tasks and restarts. It works on external projects, coordinates a live swarm of specialist agents, and can rewrite the implementation it runs on, including its code, architecture, prompts, tools, and dependencies. Reflection can also change how it understands itself without severing that continuity.
|
||||
|
||||
|
|
@ -288,6 +288,7 @@ and integration work.
|
|||
|
||||
| Version | Date | Description |
|
||||
|---------|------|-------------|
|
||||
| 6.89.0 | 2026-08-03 | **feat: heavy work runs on the owner's subscriptions — Claudexor becomes Ouroboros's delegated execution substrate.** A delegated subagent is an ordinary child in the task tree acting as a NANNY: it starts a run in Claudexor, watches it, and collects the result as a CLAIM, never a proof. A parent declares exactly three axes — `write_surface`, `model_lane`, `executor` — and model, effort, route and credential profile are derived once, at dispatch, by `subagents.resolve_subagent_dispatch`; any landing below the request writes one typed `capability_delta` that reaches the durable record, the child's own prompt, and the parent's result. An omitted lane inherits the parent's; the public `effort` knob is withdrawn; an explicit `executor: harness` pin that no route can honor is a typed blocker (never a silent paid re-route), while `auto` rotates credential profiles and falls back to the metered API loudly. Custody of a delegated run is a durable event log: idempotent starts, verified cancellation, spend settled at what the harness disclosed (an estimate is never final; absence is unknown, not zero), full terminal output staged whole to the task drive with read-to-EOF acknowledgement. Review lanes ride the same substrate: commit-triad, scope and advisory rows each run `api_chat` or `agent_session` per configured slot; a retrieving scope reviewer is admitted as an owner-declared lower-assurance mode (BIBLE P3 amendment: SOURCED ≥200K window evidence, typed durable rows; stale or unsourced evidence downgrades to advisory), and a session verdict is typed via `outputSchema` where the route proves it, else extracted by a light model and gated on conformance. Reviewer windows come from Capability Evidence — the hardcoded 1M table is gone, sub-1M slots degrade their own seat instead of blocking the panel, and a required atlas artifact that cannot assemble fails the pack instead of shrinking it. The Models page gains reviewer SLOTS (stable ids, grouped route+model combobox, per-slot efforts, the 'runs as' last-execution projection) and a Harness Accounts section over an Ouroboros-OWNED claudexord (data-plane config dir, supervised, attach-if-alive; login via structural device-code cards). `claude_code_edit` is retired (D10) with `delegate_start` as its successor and a compat shim for stored `disabled_tools` contracts; the Claude Agent SDK gateway keeps one hardened read-only path (pinned trust surface, closed tool set, read confinement). The interpreter write fence is INVERTED: in light mode an inline invocation that could reach the repo is refused unless python's AST proves it cannot write — versioned basenames (`python3.11`, `node18`, `ruby3.2`) classify through one structural family classifier, and the disclosed cost is that a non-python inline call naming a repo path is refused even when it only reads (drive/user_files and advanced/pro are untouched). Packaging/lifecycle: finalization-grace becomes an episode with a typed control id, salvage preserves the full output on the canonical drive, the bind host is never stamped from settings over the environment, and a 3-OS CI platform gate smokes a real delegated task on explicit API keys (subscription auth is deliberately out of CI and verified by a live local run). |
|
||||
| 6.87.6 | 2026-08-03 | **fix: the macOS DMG makes installation obvious again.** The primary Install section now appears immediately after the introduction, leads with a short drag-to-Applications flow, and reuses one large current visual in the README and public install guide. The DMG itself now contains an `Applications -> /Applications` shortcut beside `Ouroboros.app` and the optional CLI installer, while release smoke checks verify the link from the final mounted image. Linux and Windows stay compact below the macOS path, and setup states the remote-provider-or-local-model requirement directly. |
|
||||
| 6.87.5 | 2026-08-01 | **fix: benchmark visuals keep their real geometry and final harness logos on every screen.** Media images now preserve their intrinsic aspect ratio instead of retaining a fixed HTML height when CSS narrows them. Terminal-Bench, OSWorld, and CL-Bench use the final vector artwork rather than low-density raster exports; the accepted transparent Ouroboros, Claude, OpenAI/Codex, and Cursor marks are embedded directly in each SVG, because browsers suppress nested external resources when an SVG is loaded through an image element. Content fingerprints invalidate stale browser caches, the asset sync starts from a clean target, and the committed Pages output is rebuilt. Pixel comparison against the final Habr PNGs is exact, with desktop and mobile visual checks covering logo identity, text, chips, bars, and error whiskers. |
|
||||
| 6.87.4 | 2026-07-31 | **docs: the README and the public site carry the benchmark evidence.** The README gains a Benchmarks section — the Terminal-Bench 2.1, OSWorld-Verified, and CL-Bench state-of-the-art rows with model-matched comparisons, the SWE-bench Pro and GAIA parity rows, and links to submissions, public traces, and per-benchmark methodology. The homepage gets an evidence chapter with the headline charts and the same links, and both surfaces replace the April interface captures with current ones: the live subagent swarm, a project room with a built-and-verified game, the OuroborosHub skills page, and the code-growth chart. The README website badge moves to ouroboros-agent.ai, and the site metadata and social previews now name the benchmark results. |
|
||||
|
|
@ -297,8 +298,7 @@ and integration work.
|
|||
| 6.86.0 | 2026-07-30 | **feat: the OSWorld working prompt gains an atomic task contract, and a task's proxy session stops leaking into the published tree.** Forensics on the v6.84.0 run (89.05%) against the leaderboard leader's own published per-task dump showed the gap is 19 tasks, 8 of them one class: the work was done and never checked against the surface the grader reads. The agent now writes the task's obligations as a numbered checklist BEFORE its first mutating action — object, required state with every stated qualifier, order, what must stay unchanged, where the result must persist — and closes each item as observed-satisfied / not-verified / impossible before it may finish, with at most one targeted repair. Plural instructions still cover every matching element; only a SINGULAR referent resolving to several candidates forces a justified choice of one, and the contract is explicitly revisable when observation contradicts it. Three infeasibility shapes are named: discovery that falls outside a stated means restriction, a named mode of operation the application does not ship, and a mechanism whose trigger is narrower than the task states. The desktop environment's own configuration CLI (gsettings/dconf) is declared a legitimate surface — it writes the same store the Settings app does — while private application state stays forbidden. The colour clause drops a motivation that was simply untrue on a graded task (the metric there measures distance from a pure primary and reads no reference file at all). Harness: each proxy-flagged task now gets its own sticky upstream session keyed on campaign root plus example id, so two concurrent campaigns never share an exit; that config carries the account password and is therefore written to LANE-PRIVATE state and unlinked afterwards, never under `results/`, which is the tree that gets archived and published — an earlier draft of this same change wrote it into all 361 result directories. After the post-gate reset the runner also probes whether the binaries a task's setup claims to install are actually present and records the answer in the manifest, because upstream reports a guest command that failed as "executed successfully", and a premise that vanished between gate and worker otherwise surfaces as an honest infeasible scored zero. |
|
||||
| 6.85.0 | 2026-07-30 | **feat: Telegram becomes a first-party native capability, and blocked skill repairs regain a valid completion path.** (1) The bundled `telegram` skill consolidates the proven owner-only text/photo bridge and Mini App PoC without migrating, disabling, deleting, or changing either legacy payload. It preserves the existing bridge commands, outbound media, cards, opt-in notifications, mirror-all behavior, Ouroboros SPA, private first-contact binding, process-memory sessions, pinned Quick Tunnel lifecycle, menu rollback, and platform limits; the Mini App may be disabled or unavailable while the text bridge remains loaded. Native trust is hash-bound, while the bot token and privileged host permissions still require the normal Grant access then enable flow. (2) Bounded manifest `conflicts` declarations are enforced symmetrically at enable, reconcile, startup, and dispatch, returning a typed conflict without automatic state transfer. The Skills card now says `Loaded` for extension registration instead of overstating readiness as `Active`, and Telegram reports bridge and Mini App status in its own surface. (3) Typed `skill_repair` requests are promoted to managed tasks before ephemeral routing, preserving payload confinement, review access, and `allow_enable=false`; ordinary ephemeral default-deny policy is unchanged. (4) Google Colab discovers the seeded native Telegram skill, waits for a fresh executable native verdict, grants only API-reported missing grantable items under the persisted owner policy, enables it, and saves the proven full-access, mirror, and Mini App defaults without a Hub install or extra review. |
|
||||
| 6.84.0 | 2026-07-30 | **fix: the OSWorld working prompt stops charging the wrong resource, and three of its own clauses stop costing points.** Forensics over every failed task of the v6.83.0 dual run (74 agents, whole-loss coverage: the per-task deltas sum to the measured 12.90 pp) found the most expensive defect was ours: the preamble said "every tool call costs ~30s, so MINIMIZE calls" while the budget is denominated in TURNS and the official contract batches actions inside one `predict()`. The agent obeyed — 1.01 tool calls per turn across 11k turns, i.e. the benchmark ran on about a third of its action budget. The clause is now turn-denominated and asks for 4-8 confident calls in one turn, split only where a target depends on what the previous action reveals. Three clauses the agent CITED while losing are corrected: an exact value now beats the app's named swatch (a palette "Blue" 2A6099 is not 0000FF); "already in the requested state" must be judged from the STORED value, since controls render defaults as selected while nothing is stored; and ordinals no longer blanket-exclude headings, which on a slide are often the counted item. The command line is restored as the right tool for genuine batch/file work (one `pdfseparate` instead of N print dialogs) while GUI stays mandatory for application state. Added: verification by independent read-back (re-open the saved artifact, read it with a different tool than wrote it) merged with the minimal-diff rule into one clause; and a premise branch — a task asking for something VISIBLE is not satisfied by storing a flag, and an agent writing that the real path is impossible or that it is delivering a stand-in has already found its verdict. Adapter/prompt only. Harness: `evaluate()` now runs with the checkout root as CWD, because evaluator fixtures are declared relative and `get_local_file` tests them against the process CWD — one task produced a byte-exact answer and scored 0; the gate's UNUSED turn reserve is returned to the worker (the gate budgets 14 and spends ~4, and 13 of 56 failures died at 89-92 turns inside a 100-turn budget); and proxy support is gated on a LIVE CONNECT probe rather than the config file existing, with a proxy:true task whose trace shows an exhausted upstream quarantined as infrastructure instead of scored as a capability zero. |
|
||||
| 6.83.0 | 2026-07-30 | **fix: a screenshot that cannot be decoded fails where it is taken, an infeasibility verdict is judged as an argument, and a declared step budget is one the runtime actually enforces.** (1) Image integrity is fail-closed at three seams: the remote screenshot fetch validates a FULL decode before publishing a path (bounded re-fetch, write-validate-rename), the shared remote-result builder rejects an undecodable capture instead of claiming ok, and the VLM payload builder raises `IMAGE_UNDECODABLE` at build time. A truncated PNG keeps a valid 24-byte header, so header-only checks passed it and it detonated rounds later as a non-retryable provider 400 — five task deaths in the v6.81.1 OSWorld run. Four test fixtures labelled "minimal valid PNG" were themselves undecodable and are now real images; one assertion that pinned an IDENTITY coordinate transform for a 1920x1080 capture at a 1280 cap (it only held because the stub never downscaled) now pins the real 1.5x transform. (2) Tool results are judged by their typed envelope: a structured `{"ok": false}` payload counts as a failure for the error counters, anti-loop and auto-attach, instead of only text markers. (3) Acceptance review gains an ABSENT-PREMISE branch: when the terminal claim is that the premise is missing, the deliverable under review is the PREMISE ARGUMENT — instantiating "the named artifact exists" as a criterion begs the question, and coaching a continuation whose remaining routes breach the task's own stated restrictions manufactures the artifact the task forbids. A weak premise argument still fails on its own grounds. Measured cost of the old behaviour: a correct 1.0 converted into 0.0 over 149 tool calls. (4) `type_text` routes multi-line and long payloads through the in-VM clipboard (typewrite presses Enter per newline and sheds keystrokes), joining the non-ASCII and angle-bracket paths. (5) OSWorld adapter: `--max-steps` declares AND enforces a leaderboard-comparable budget — a step is one top-level policy turn, matching the official `predict() -> actions[]` boundary, not one GUI action; the server round cap is verified against the derived worker cap before the VM boots, the gate phase is cancelled at its own reserve (the runtime cap is server-wide and the gate is a separate task), and the post-run audit reads the loop's policy turns rather than the flat physical-call field, which disagreed with it on 344 of 346 examples. `--expect-dataset-commit` turns the graded-spec pin into a gate: a checkout other than the campaign's is refused before any paid work, because it supplies different task instructions AND a different evaluator. |
|
||||
Older releases are preserved in Git tags and GitHub releases. Older 6.x rows (including 6.87.1, 6.86.1, 6.81.1, 6.76.0, 6.75.0, 6.74.5, 6.74.4, 6.74.1, 6.74.0, 6.73.2, 6.73.1, 6.73.0, 6.72.0, 6.71.2, 6.71.1, 6.71.0, 6.70.0, 6.69.0, 6.68.0, 6.67.0, 6.66.0, 6.65.4, 6.65.3, 6.65.2, 6.65.1, 6.65.0, 6.64.3, 6.64.2, 6.64.1, 6.64.0, 6.63.0, 6.62.0, 6.61.4, 6.61.3, 6.61.1, 6.61.0, 6.60.0, 6.59.0, 6.58.0, 6.57.0, 6.56.0, 6.55.0, 6.54.4, 6.54.2, 6.54.1, 6.54.0, 6.53.4, 6.53.0, 6.51.0), the 5.2.0 through 5.33.0-rc.6 rows, and former `4.0.0` rows are rolled off to respect the P9 changelog cap; their full bodies remain at their git tags.
|
||||
Older releases are preserved in Git tags and GitHub releases. Older 6.x rows (including 6.87.1, 6.86.1, 6.83.0, 6.81.1, 6.76.0, 6.75.0, 6.74.5, 6.74.4, 6.74.1, 6.74.0, 6.73.2, 6.73.1, 6.73.0, 6.72.0, 6.71.2, 6.71.1, 6.71.0, 6.70.0, 6.69.0, 6.68.0, 6.67.0, 6.66.0, 6.65.4, 6.65.3, 6.65.2, 6.65.1, 6.65.0, 6.64.3, 6.64.2, 6.64.1, 6.64.0, 6.63.0, 6.62.0, 6.61.4, 6.61.3, 6.61.1, 6.61.0, 6.60.0, 6.59.0, 6.58.0, 6.57.0, 6.56.0, 6.55.0, 6.54.4, 6.54.2, 6.54.1, 6.54.0, 6.53.4, 6.53.0, 6.51.0), the 5.2.0 through 5.33.0-rc.6 rows, and former `4.0.0` rows are rolled off to respect the P9 changelog cap; their full bodies remain at their git tags.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
6.87.6
|
||||
6.89.0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Ouroboros v6.87.6 — Architecture & Reference
|
||||
# Ouroboros v6.89.0 — Architecture & Reference
|
||||
|
||||
This file is NOT a changelog. Version history lives in README.md, git tags, and commit log.
|
||||
|
||||
|
|
@ -89,6 +89,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de
|
|||
├── context_compaction.py ← Context trimming and summarization helpers
|
||||
├── headless.py ← Headless task child-drive isolation, workspace patch artifacts, and memory export helpers
|
||||
├── coop_checkpoint.py ← (v6.58.0) `checkpoint_commit_coop_roots` — at ROOT-task finalization, dirty host-minted genesis/coop trees get a local checkpoint commit (`user.name=Ouroboros`); credential-shaped files excluded (disclosed, headless sensitive-pattern SSOT), owner-attached folders NEVER auto-committed, skipped while tree tasks are live, fail-soft per root
|
||||
├── delegate_output.py ← Staged-output + read-receipt cluster for delegated runs (extracted from tools/delegate.py for the module-size gate; delegate.py re-exports it so sibling code and tests keep one name): `_stage_full_output` writes the WHOLE terminal detail atomically under the task drive (`delegated_runs/<run>.json`, sha256 + byte length recorded), and `acknowledge_staged_output_read` — hooked into `read_file`'s task_drive path — credits DELIVERED character ranges until contiguous EOF coverage, then writes the once-per-run durable `delegate_run_output_consumed` row (disclosure, never a gate)
|
||||
├── subagents.py ← Subagent axis vocabularies (model lane / executor), the single dispatch-time resolution (`resolve_subagent_dispatch` → `capability_delta`), and structured lineage/usage envelopes
|
||||
├── subagent_worktrees.py ← Acting self_worktree lifecycle: provision/remove/prune isolated git worktrees (outside repo/ and data/) + durable registry (state/subagent_worktrees.json) + cross-process ops lock; startup orphan reconciliation; also provisions durable from-scratch genesis projects (provision_genesis_project, never registry/GC)
|
||||
├── artifacts.py ← Task-scoped artifact helpers shared by user-file tools, process outputs, and outcome finalization. (v6.52.0, P1) `stage_task_attachments` stages every task's INPUT attachments (CLI/API, GAIA solver, desktop chat) into the agent-readable `artifact_store/attachments/` (skips secret SOURCES via the tool_access SSOT blocklist, bounded), returning a manifest of `read_file(root='artifact_store', path='attachments/<name>')` entries; `collect_task_artifact_records` EXCLUDES that subdir so staged inputs are never recorded as deliverables. (v6.52.2) `record_task_scratch`/`read_task_scratch_fingerprints` persist {abs_path: sha256} FINGERPRINTS of the run_command/run_script `scratch=[...]` ephemeral-verification files to `.scratch_manifest.json` (written to BOTH budget + live drive roots) so `headless.write_workspace_patch_artifacts` EXCLUDES a file from the workspace patch ONLY while its current content still matches (a later real file at the same path is never dropped). (v6.56.0) scratch declarations are IDEMPOTENT/ADOPTABLE: re-declaring a manifest path is ok, and an existing untracked in-cwd file may be adopted — its sha is recorded via the same SSOT writer at declaration time, so the sha-gate still excludes it only while unmodified (tracked / outside-cwd / outside-worktree declarations stay blocked); the undeclared-output guard stat-verifies candidates POST-exec (exists + mtime ≥ start−slack) for both run_command and run_script, so import strings/CLI flags/heredoc bodies no longer read as writes
|
||||
|
|
@ -212,6 +213,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de
|
|||
│ ├── review_revalidation.py ← Reviewed-commit fingerprint revalidation helpers (blocks when staged diff changes after review)
|
||||
│ ├── scope_review.py ← Scope reviewer (enforcement-aware, budget-aware)
|
||||
│ ├── scope_review_session.py ← Scope SESSION delivery (phase 5.2/5.6/5.7): the same task/checklist/contract via the same builder, retrieval pointers instead of assembled packs, governance docs as navigation maps, forensic (non-gating) coverage manifest
|
||||
│ ├── scope_window.py ← Scope-reviewer WINDOW authority (extracted at the v6.89.0 synthesis for the module-size gate): the evidence-typed `scope_window` resolution (ReviewerWindow; sizing vs blocking authority split), the five-way provenance vocabulary + honest wording, the designated-default identity, and the 1M/200K window constants — `scope_review` re-imports every name under its historical private aliases
|
||||
│ ├── scope_review_contract.py ← Pure scope-output parser and one-pass validity contract; owns no routing, retries, or reviewer state
|
||||
│ ├── services.py ← Task-scoped long-running service mini-manager: start/status/logs/stop with process-group cleanup and retained private log blobs
|
||||
│ ├── skill_exec.py ← Phase 3 external-skill surface: list_skills, skill_review, toggle_skill, skill_exec (subprocess runner with cwd confinement, env scrubbing, timeout, runtime allowlist python/python3/bash/node/deno/ruby/go; gated by enabled + fresh executable review + fresh content hash — v5.1.2 Frame A: runtime_mode no longer blocks execution)
|
||||
|
|
@ -1693,7 +1695,7 @@ stories. Runs ask for `authPreference: subscription` explicitly, because the eng
|
|||
default is `auto` = subscription-first WITH policy fallback to a paid key, and that
|
||||
fallback is invisible to the host. FOUR cases, each recorded as what it is: a DISCLOSED
|
||||
SETTLED zero settles at `0.0` with `cost_final=true` and leaves the projection final (the
|
||||
free-session case this row kind exists for); a disclosed settled charge rides the ledger
|
||||
free-session case this row kind — the `subscription_session` usage-ledger row — exists for); a disclosed settled charge rides the ledger
|
||||
as money and is final; an ESTIMATED amount rides as money with `cost_final=false`,
|
||||
because an estimated zero is not a proven free session and an estimated charge is not a
|
||||
closed book; an UNDISCLOSED spend writes `cost_usd: null`, which drops `cost_final` for
|
||||
|
|
|
|||
|
|
@ -167,6 +167,19 @@ def _record_executor_resolution(
|
|||
})
|
||||
|
||||
|
||||
def _blocked_executor_terminal(cap_info: Dict[str, Any]) -> Tuple[str, Dict[str, Any], Dict[str, Any]]:
|
||||
"""p34's typed terminal for a blocked executor pin, rebuilt from the facts
|
||||
cap_info carried across the (ctx, messages, cap_info) seam. The placeholder
|
||||
method p2 kept for exactly this synthesis is deleted; this is the one body."""
|
||||
text, usage = executor_blocked_outcome(SubagentExecutorResolution(
|
||||
requested=str(cap_info.get("executor_blocked_requested") or "harness"),
|
||||
executor="blocked",
|
||||
reason=str(cap_info.get("executor_blocked_reason") or ""),
|
||||
reset_at=str(cap_info.get("executor_blocked_reset_at") or ""),
|
||||
))
|
||||
return text, usage, {"reasoning_notes": ["subagent_executor_unavailable"], "tool_calls": []}
|
||||
|
||||
|
||||
def _persist_early_origin_stub(drive_root: Any, task: Dict[str, Any]) -> None:
|
||||
"""Durably persist the ingress-captured origin BEFORE the convertible card
|
||||
exists (v6.73.0). Merge-write only; the full RUNNING write follows and
|
||||
|
|
@ -985,17 +998,8 @@ class OuroborosAgent:
|
|||
# stamped onto the task, never re-derived per surface.
|
||||
self._record_executor_facts(task)
|
||||
|
||||
if blocked_reason := str(cap_info.get("executor_blocked_reason") or ""):
|
||||
# p34's typed terminal, rebuilt from the facts cap_info carried
|
||||
# across the (ctx, messages, cap_info) seam. The placeholder
|
||||
# method p2 kept for exactly this synthesis is deleted.
|
||||
text, usage = executor_blocked_outcome(SubagentExecutorResolution(
|
||||
requested=str(cap_info.get("executor_blocked_requested") or "harness"),
|
||||
executor="blocked",
|
||||
reason=blocked_reason,
|
||||
reset_at=str(cap_info.get("executor_blocked_reset_at") or ""),
|
||||
))
|
||||
llm_trace = {"reasoning_notes": ["subagent_executor_unavailable"], "tool_calls": []}
|
||||
if str(cap_info.get("executor_blocked_reason") or ""):
|
||||
text, usage, llm_trace = _blocked_executor_terminal(cap_info)
|
||||
elif task_type_str == "deep_self_review":
|
||||
# Deep self-review bypasses the tool loop.
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ convergence census) still finds them there.
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
|
|
|||
|
|
@ -116,7 +116,13 @@ MAX_FUNCTION_LINES = 300
|
|||
# delivery work is not blocked by a moving "current count + epsilon" gate.
|
||||
# This is still a coarse smoke alarm; per-module/function complexity checks and
|
||||
# review remain the tools for preventing local bloat.
|
||||
MAX_TOTAL_FUNCTIONS = 5000
|
||||
# 5000 -> 5100 at the v6.89.0 Claudexor-integration synthesis: eight branches
|
||||
# each fit under 5000 alone and their union ships a whole delegation subsystem
|
||||
# (nanny transport, custody, review lanes, owned daemon, slots UI) while also
|
||||
# DELETING the claude_code edit path, the multi-slot fan-out and the enumerated
|
||||
# write fence. A disclosed budget raise, not a silent one (P7: the next cycle
|
||||
# owes consolidation before growth).
|
||||
MAX_TOTAL_FUNCTIONS = 5100
|
||||
GRANDFATHERED_OVERSIZED_FUNCTIONS = {
|
||||
("agent_startup_checks.py", "verify_restart"), # managed #53 boot diagnostic flow, 307 lines
|
||||
("git.py", "_run_reviewed_stage_cycle"), # reviewed-commit gate orchestration, 302 lines
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ a dict this process happens to still hold.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ from dataclasses import dataclass, field, replace
|
|||
from typing import Any, List, Optional
|
||||
|
||||
from ouroboros.llm import LLMClient
|
||||
from ouroboros.config import review_model_uses_local
|
||||
from ouroboros.review_substrate import review_repo_dirs_for, scope_reviewer_slots
|
||||
from ouroboros.tools.registry import ToolContext
|
||||
from ouroboros.tools.review_context_atlas import (
|
||||
|
|
@ -78,7 +77,7 @@ _SCOPE_REQUIRED_ITEMS = SCOPE_REQUIRED_ITEMS # compatibility export used by tes
|
|||
# routed spelling alike, exactly as the sentinel spanned spellings for the previous
|
||||
# designated default (v6.55.0-v6.81: anthropic/claude-fable-5). The sentinel still
|
||||
# grants only the conservative 1M figure, and a real probe/owner-ack supersedes it.
|
||||
_SCOPE_MODEL_DEFAULT = "openai/gpt-5.6-terra"
|
||||
from ouroboros.tools.scope_window import SCOPE_MODEL_DEFAULT as _SCOPE_MODEL_DEFAULT # noqa: E402
|
||||
_SCOPE_MAX_TOKENS = 100_000 # 100K output tokens
|
||||
_SCOPE_REVIEW_SLOT_TIMEOUT_SEC = 900
|
||||
from ouroboros.tools.review_helpers import REVIEW_PROMPT_TOKEN_BUDGET as _REVIEW_BUDGET
|
||||
|
|
@ -91,11 +90,13 @@ _SCOPE_BUDGET_TOKEN_LIMIT = _REVIEW_BUDGET
|
|||
# tens of thousands of tokens on atlas-heavy prompts. Gate assembled INPUT on a
|
||||
# conservative effective cap and retry once with a compact atlas prompt before
|
||||
# applying the configured blocking/advisory scope authority.
|
||||
_SCOPE_MODEL_CONTEXT_WINDOW = 1_000_000
|
||||
# Conservative sub-floor window for UNKNOWN reviewers without Capability Evidence.
|
||||
# The P3 authority check makes its findings advisory instead of pretending the
|
||||
# route is 1M-capable.
|
||||
_SCOPE_FAILCLOSED_WINDOW = 200_000
|
||||
# The 1M constitutional window, the conservative sub-floor for unevidenced
|
||||
# routes, and the shipped default reviewer identity live in `tools/scope_window`
|
||||
# (the window-authority SSOT); imported here under the historical names.
|
||||
from ouroboros.tools.scope_window import ( # noqa: E402
|
||||
SCOPE_FAILCLOSED_WINDOW as _SCOPE_FAILCLOSED_WINDOW,
|
||||
SCOPE_MODEL_CONTEXT_WINDOW as _SCOPE_MODEL_CONTEXT_WINDOW,
|
||||
)
|
||||
_SCOPE_OUTPUT_MARGIN_TOKENS = 155_000
|
||||
_SCOPE_INPUT_TOKEN_LIMIT = min(
|
||||
_SCOPE_BUDGET_TOKEN_LIMIT,
|
||||
|
|
@ -111,31 +112,12 @@ _SCOPE_INPUT_TOKEN_LIMIT = min(
|
|||
# floor (BIBLE P3).
|
||||
from ouroboros.reviewer_window import (
|
||||
ReviewerWindow,
|
||||
resolve_reviewer_window as _resolve_reviewer_window,
|
||||
window_scaled_reserves as _shared_window_scaled_reserves,
|
||||
)
|
||||
from ouroboros.tools.review_helpers import (
|
||||
calibrated_input_token_limit as _calibrated_input_token_limit,
|
||||
)
|
||||
|
||||
# Window provenance vocabulary shared with the diagnostics wording (RS5).
|
||||
_WINDOW_CONFIRMED = "confirmed"
|
||||
_WINDOW_ASSERTED = "asserted"
|
||||
_WINDOW_STALE = "stale_unverifiable"
|
||||
_WINDOW_UNKNOWN = "unknown_conservative"
|
||||
_WINDOW_SENTINEL = "designated_default_sentinel"
|
||||
|
||||
# The metadata probe lives with the shared window resolver (`reviewer_window`) and is
|
||||
# rate-limited by the TTL on the evidence record, keyed by the full ROUTE fingerprint
|
||||
# rather than the model name: capability is a property of provider+base_url+model, and
|
||||
# a hot base-URL change must get its own probe rather than silently reusing the old
|
||||
# verdict. EVERY scope route gets that probe, the shipped default included: the probe is
|
||||
# now the only path to blocking authority, so exempting the default (as the sentinel-era
|
||||
# code did) left the one route that gates commits structurally unable to source its
|
||||
# window — and rate-limiting it per PROCESS instead of per TTL left an install that
|
||||
# outlived the TTL unable to RE-source it (v6.87.45).
|
||||
|
||||
|
||||
def _scope_review_skipped_in_low_context() -> bool:
|
||||
"""Whether the owner's context mode declares scope review out of scope.
|
||||
|
||||
|
|
@ -153,101 +135,20 @@ def _scope_review_skipped_in_low_context() -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def _is_designated_default_reviewer(model: str) -> bool:
|
||||
"""True iff ``model`` is the shipped default reviewer (``openai/gpt-5.6-terra``),
|
||||
across provider spellings (``openai::gpt-5.6-terra``, ``openrouter::openai/...``).
|
||||
|
||||
SIZING only. This answers "how big a pack may I assemble for an unevidenced
|
||||
route", never "may this reviewer block a commit": authority is computed from
|
||||
Capability Evidence (``ReviewerWindow.blocking_authority_allowed``) and a model
|
||||
acquires none of it from its name."""
|
||||
def _normalized(m: str) -> str:
|
||||
text = str(m or "").strip()
|
||||
if text.startswith("openrouter::"):
|
||||
text = text[len("openrouter::"):]
|
||||
try:
|
||||
from ouroboros.provider_models import normalize_model_identity
|
||||
return normalize_model_identity(text)
|
||||
except Exception:
|
||||
return text
|
||||
return bool(model) and _normalized(model) == _normalized(_SCOPE_MODEL_DEFAULT)
|
||||
|
||||
|
||||
def _scope_window(model: str) -> ReviewerWindow:
|
||||
"""The scope reviewer's window AND its blocking authority, as ONE typed result.
|
||||
|
||||
Replaces the deleted static per-model window table: a confirmed/asserted probe
|
||||
(provider metadata or owner-ack) for the reviewer's REAL active route gives the
|
||||
real window, and only such SOURCED, non-stale, >=1M evidence carries blocking
|
||||
authority (BIBLE P3 — "a reviewer whose window cannot be established by sourced
|
||||
Capability Evidence is treated as too small rather than assumed adequate").
|
||||
|
||||
With NO evidence the result still carries a SIZING number so the review is
|
||||
dispatched rather than declined before it starts — the 1M figure for the shipped
|
||||
designated reviewer, a conservative sub-floor for anything else, matching what
|
||||
each can plausibly hold. Neither carries a KNOWN status, so neither can authorise:
|
||||
the sentinel sizes a prompt, it does not sign a verdict. That split is why this
|
||||
returns the whole record instead of a bare int — a number alone cannot say where
|
||||
it came from, and the caller that needs to know then guesses.
|
||||
|
||||
Every route gets one lazy metadata-only fetch per evidence-TTL period (never
|
||||
generative, never a paid call), concurrent resolutions of the same route
|
||||
serialized by the per-route lock; inside the TTL the cache answers and the path
|
||||
stays hot-path safe. How often the network is re-asked is owned by
|
||||
``capability_evidence.probe``'s record TTL, deliberately NOT by the process
|
||||
lifetime (v6.87.45: a per-process memo outlived the 24h record and wedged
|
||||
every commit once an install stayed up past the TTL)."""
|
||||
model = str(model or "")
|
||||
try:
|
||||
# Probe the scope slot, not the active main route (which honors USE_LOCAL_MAIN).
|
||||
resolved = _resolve_reviewer_window(model, use_local=review_model_uses_local(model))
|
||||
if int(resolved.window_tokens) > 0:
|
||||
return resolved
|
||||
except Exception:
|
||||
pass
|
||||
return ReviewerWindow(
|
||||
window_tokens=(
|
||||
_SCOPE_MODEL_CONTEXT_WINDOW if _is_designated_default_reviewer(model)
|
||||
else _SCOPE_FAILCLOSED_WINDOW
|
||||
),
|
||||
model=model,
|
||||
)
|
||||
|
||||
|
||||
def _scope_window_provenance(window: ReviewerWindow) -> str:
|
||||
"""Provenance label for the diagnostics wording (RS5)."""
|
||||
if window.stale:
|
||||
return _WINDOW_STALE
|
||||
if window.status == "asserted":
|
||||
return _WINDOW_ASSERTED
|
||||
if window.status == "confirmed":
|
||||
return _WINDOW_CONFIRMED
|
||||
if int(window.window_tokens) >= _SCOPE_MODEL_CONTEXT_WINDOW:
|
||||
return _WINDOW_SENTINEL
|
||||
return _WINDOW_UNKNOWN
|
||||
|
||||
|
||||
def _window_provenance_phrase(window: int, provenance: str, observed_at: str = "") -> str:
|
||||
"""Honest five-way wording for a reviewer window (RS5).
|
||||
|
||||
The old single phrasing called every sub-1M window "known", which read as a
|
||||
measured fact even when it was the conservative fallback for an unprobed route;
|
||||
the stale wording exists because an expired record used to be indistinguishable
|
||||
from a live reading in every message the owner ever saw. ``observed_at`` dates the
|
||||
expired reading, which is the difference between "the provider blipped an hour ago"
|
||||
and "this route has been dead for a week" — the only two situations that produce
|
||||
identical wording otherwise (BIBLE P1, provenance)."""
|
||||
if provenance == _WINDOW_CONFIRMED:
|
||||
return f"confirmed {window}-token window"
|
||||
if provenance == _WINDOW_ASSERTED:
|
||||
return f"owner-asserted {window}-token window"
|
||||
if provenance == _WINDOW_STALE:
|
||||
dated = f", last confirmed {observed_at}" if observed_at else ""
|
||||
return f"EXPIRED, unverifiable {window}-token window{dated}"
|
||||
if provenance == _WINDOW_SENTINEL:
|
||||
return f"designated-default {window}-token sentinel window"
|
||||
return f"unknown window, conservatively treated as {window} tokens"
|
||||
|
||||
# Window authority moved to `tools/scope_window.py` (module-size gate at
|
||||
# synthesis); re-imported under the old private aliases so every caller and
|
||||
# test keeps one patch point on THIS module.
|
||||
from ouroboros.tools.scope_window import ( # noqa: E402
|
||||
WINDOW_ASSERTED as _WINDOW_ASSERTED, # noqa: F401 (test-read re-export)
|
||||
WINDOW_CONFIRMED as _WINDOW_CONFIRMED, # noqa: F401 (test-read re-export)
|
||||
WINDOW_SENTINEL as _WINDOW_SENTINEL, # noqa: F401 (test-read re-export)
|
||||
WINDOW_STALE as _WINDOW_STALE, # noqa: F401 (test-read re-export)
|
||||
WINDOW_UNKNOWN as _WINDOW_UNKNOWN, # noqa: F401 (test-read re-export)
|
||||
scope_window as _scope_window,
|
||||
scope_window_provenance as _scope_window_provenance,
|
||||
window_provenance_phrase as _window_provenance_phrase,
|
||||
)
|
||||
|
||||
def _low_context_skip_result(scope_model: str) -> "ScopeReviewResult":
|
||||
"""Typed, non-blocking record of the owner-declared low-context-mode skip.
|
||||
|
|
|
|||
137
ouroboros/tools/scope_window.py
Normal file
137
ouroboros/tools/scope_window.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""Scope-reviewer WINDOW authority: evidence-typed resolution + honest wording (RS5).
|
||||
|
||||
Extracted from ``tools/scope_review.py`` for the module-size gate at synthesis —
|
||||
the p1 atlas, p5x session-delivery and p7a window-evidence unions each fit alone
|
||||
and overflowed together. ``scope_review`` re-imports every name under its old
|
||||
private alias, so its tests and callers keep exactly one patch point.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ouroboros.config import review_model_uses_local
|
||||
from ouroboros.reviewer_window import (
|
||||
ReviewerWindow,
|
||||
resolve_reviewer_window as _resolve_reviewer_window,
|
||||
)
|
||||
|
||||
# The constitutional scope window (BIBLE P3) and the conservative sub-floor
|
||||
# sizing fallback for routes with no Capability Evidence. `scope_review` imports
|
||||
# these back rather than defining a second copy.
|
||||
SCOPE_MODEL_CONTEXT_WINDOW = 1_000_000
|
||||
SCOPE_FAILCLOSED_WINDOW = 200_000
|
||||
SCOPE_MODEL_DEFAULT = "openai/gpt-5.6-terra"
|
||||
|
||||
# Window provenance vocabulary shared with the diagnostics wording (RS5).
|
||||
WINDOW_CONFIRMED = "confirmed"
|
||||
WINDOW_ASSERTED = "asserted"
|
||||
WINDOW_STALE = "stale_unverifiable"
|
||||
WINDOW_UNKNOWN = "unknown_conservative"
|
||||
WINDOW_SENTINEL = "designated_default_sentinel"
|
||||
|
||||
# The metadata probe lives with the shared window resolver (`reviewer_window`) and is
|
||||
# rate-limited by the TTL on the evidence record, keyed by the full ROUTE fingerprint
|
||||
# rather than the model name: capability is a property of provider+base_url+model, and
|
||||
# a hot base-URL change must get its own probe rather than silently reusing the old
|
||||
# verdict. EVERY scope route gets that probe, the shipped default included: the probe is
|
||||
# now the only path to blocking authority, so exempting the default (as the sentinel-era
|
||||
# code did) left the one route that gates commits structurally unable to source its
|
||||
# window — and rate-limiting it per PROCESS instead of per TTL left an install that
|
||||
# outlived the TTL unable to RE-source it (v6.87.45).
|
||||
|
||||
|
||||
def is_designated_default_reviewer(model: str) -> bool:
|
||||
"""True iff ``model`` is the shipped default reviewer (``openai/gpt-5.6-terra``),
|
||||
across provider spellings (``openai::gpt-5.6-terra``, ``openrouter::openai/...``).
|
||||
|
||||
SIZING only. This answers "how big a pack may I assemble for an unevidenced
|
||||
route", never "may this reviewer block a commit": authority is computed from
|
||||
Capability Evidence (``ReviewerWindow.blocking_authority_allowed``) and a model
|
||||
acquires none of it from its name."""
|
||||
def _normalized(m: str) -> str:
|
||||
text = str(m or "").strip()
|
||||
if text.startswith("openrouter::"):
|
||||
text = text[len("openrouter::"):]
|
||||
try:
|
||||
from ouroboros.provider_models import normalize_model_identity
|
||||
return normalize_model_identity(text)
|
||||
except Exception:
|
||||
return text
|
||||
return bool(model) and _normalized(model) == _normalized(SCOPE_MODEL_DEFAULT)
|
||||
|
||||
|
||||
def scope_window(model: str) -> ReviewerWindow:
|
||||
"""The scope reviewer's window AND its blocking authority, as ONE typed result.
|
||||
|
||||
Replaces the deleted static per-model window table: a confirmed/asserted probe
|
||||
(provider metadata or owner-ack) for the reviewer's REAL active route gives the
|
||||
real window, and only such SOURCED, non-stale, >=1M evidence carries blocking
|
||||
authority (BIBLE P3 — "a reviewer whose window cannot be established by sourced
|
||||
Capability Evidence is treated as too small rather than assumed adequate").
|
||||
|
||||
With NO evidence the result still carries a SIZING number so the review is
|
||||
dispatched rather than declined before it starts — the 1M figure for the shipped
|
||||
designated reviewer, a conservative sub-floor for anything else, matching what
|
||||
each can plausibly hold. Neither carries a KNOWN status, so neither can authorise:
|
||||
the sentinel sizes a prompt, it does not sign a verdict. That split is why this
|
||||
returns the whole record instead of a bare int — a number alone cannot say where
|
||||
it came from, and the caller that needs to know then guesses.
|
||||
|
||||
Every route gets one lazy metadata-only fetch per evidence-TTL period (never
|
||||
generative, never a paid call), concurrent resolutions of the same route
|
||||
serialized by the per-route lock; inside the TTL the cache answers and the path
|
||||
stays hot-path safe. How often the network is re-asked is owned by
|
||||
``capability_evidence.probe``'s record TTL, deliberately NOT by the process
|
||||
lifetime (v6.87.45: a per-process memo outlived the 24h record and wedged
|
||||
every commit once an install stayed up past the TTL)."""
|
||||
model = str(model or "")
|
||||
try:
|
||||
# Probe the scope slot, not the active main route (which honors USE_LOCAL_MAIN).
|
||||
resolved = _resolve_reviewer_window(model, use_local=review_model_uses_local(model))
|
||||
if int(resolved.window_tokens) > 0:
|
||||
return resolved
|
||||
except Exception:
|
||||
pass
|
||||
return ReviewerWindow(
|
||||
window_tokens=(
|
||||
SCOPE_MODEL_CONTEXT_WINDOW if is_designated_default_reviewer(model)
|
||||
else SCOPE_FAILCLOSED_WINDOW
|
||||
),
|
||||
model=model,
|
||||
)
|
||||
|
||||
|
||||
def scope_window_provenance(window: ReviewerWindow) -> str:
|
||||
"""Provenance label for the diagnostics wording (RS5)."""
|
||||
if window.stale:
|
||||
return WINDOW_STALE
|
||||
if window.status == "asserted":
|
||||
return WINDOW_ASSERTED
|
||||
if window.status == "confirmed":
|
||||
return WINDOW_CONFIRMED
|
||||
if int(window.window_tokens) >= SCOPE_MODEL_CONTEXT_WINDOW:
|
||||
return WINDOW_SENTINEL
|
||||
return WINDOW_UNKNOWN
|
||||
|
||||
|
||||
def window_provenance_phrase(window: int, provenance: str, observed_at: str = "") -> str:
|
||||
"""Honest five-way wording for a reviewer window (RS5).
|
||||
|
||||
The old single phrasing called every sub-1M window "known", which read as a
|
||||
measured fact even when it was the conservative fallback for an unprobed route;
|
||||
the stale wording exists because an expired record used to be indistinguishable
|
||||
from a live reading in every message the owner ever saw. ``observed_at`` dates the
|
||||
expired reading, which is the difference between "the provider blipped an hour ago"
|
||||
and "this route has been dead for a week" — the only two situations that produce
|
||||
identical wording otherwise (BIBLE P1, provenance)."""
|
||||
if provenance == WINDOW_CONFIRMED:
|
||||
return f"confirmed {window}-token window"
|
||||
if provenance == WINDOW_ASSERTED:
|
||||
return f"owner-asserted {window}-token window"
|
||||
if provenance == WINDOW_STALE:
|
||||
dated = f", last confirmed {observed_at}" if observed_at else ""
|
||||
return f"EXPIRED, unverifiable {window}-token window{dated}"
|
||||
if provenance == WINDOW_SENTINEL:
|
||||
return f"designated-default {window}-token sentinel window"
|
||||
return f"unknown window, conservatively treated as {window} tokens"
|
||||
|
||||
|
||||
|
|
@ -9,25 +9,20 @@ import logging
|
|||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import shlex
|
||||
import signal
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from typing import Any, Dict, List
|
||||
from typing import Dict, List
|
||||
|
||||
from ouroboros.artifacts import artifact_store_path_block_reason, copy_directory_to_task_artifacts, copy_file_to_task_artifacts, record_task_scratch
|
||||
from ouroboros.artifacts import copy_directory_to_task_artifacts, copy_file_to_task_artifacts, record_task_scratch
|
||||
from ouroboros.platform_layer import bootstrap_process_path, kill_process_tree, scrub_repo_from_pythonpath, subprocess_new_group_kwargs
|
||||
from ouroboros.config import SETTINGS_DEFAULTS, get_runtime_mode, load_settings
|
||||
from ouroboros.runtime_mode_policy import (
|
||||
core_patch_notice,
|
||||
is_protected_runtime_path,
|
||||
mode_allows_protected_write,
|
||||
protected_paths_in,
|
||||
)
|
||||
from ouroboros.tools.commit_gate import _invalidate_advisory
|
||||
from ouroboros.shell_parse import embedded_absolute_path_tokens, is_absolute_path_text, recover_stringified_argv, shell_argv_with_inline
|
||||
|
|
@ -40,17 +35,8 @@ from ouroboros.tool_access import (
|
|||
resolve_shell_cwd,
|
||||
user_files_path_block_reason,
|
||||
)
|
||||
from ouroboros.utils import safe_relpath, utc_now_iso, run_cmd
|
||||
from ouroboros.utils import safe_relpath
|
||||
from ouroboros.deadline_utils import deadline_remaining_sec
|
||||
from ouroboros.contracts.task_constraint import normalize_task_constraint
|
||||
from ouroboros.contracts.skill_payload_policy import (
|
||||
SKILL_PAYLOAD_CONTROL_DIRNAMES,
|
||||
SKILL_PAYLOAD_CONTROL_FILENAMES,
|
||||
SkillPayloadPathError,
|
||||
cross_skill_redirect_error,
|
||||
decide_payload_short_form,
|
||||
resolve_skill_payload_target,
|
||||
)
|
||||
from ouroboros.workspace_executor import execute as executor_execute
|
||||
from ouroboros.workspace_executor import executor_ref_from_ctx
|
||||
from ouroboros.workspace_executor import map_backend_path as executor_map_backend_path
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "ouroboros"
|
||||
version = "6.87.6"
|
||||
version = "6.89.0"
|
||||
description = "Self-creating AI agent with constitution, background consciousness, and persistent identity"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ agent-session route tests stands in for the Claudexor control plane.
|
|||
|
||||
import json
|
||||
import subprocess
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ We test:
|
|||
|
||||
import asyncio
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
|
|
@ -1283,7 +1282,6 @@ class TestResultMessageIsErrorHonored:
|
|||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
import ouroboros.gateways.claude_code as gw
|
||||
|
||||
with patch("ouroboros.gateways.claude_code.ClaudeAgentOptions", lambda **kw: None), \
|
||||
patch("ouroboros.gateways.claude_code.ClaudeSDKClient", self._fake_client(fake_result)), \
|
||||
|
|
|
|||
|
|
@ -1,610 +0,0 @@
|
|||
"""Invariants that only exist because two phase branches were converged into one tree.
|
||||
|
||||
Every test here guards a failure mode that NO SINGLE BRANCH can see, and that no
|
||||
ordinary suite would catch, because the dangerous merges are the ones git performs
|
||||
without a conflict:
|
||||
|
||||
* a function MOVED to a new module arrives as a clean ADD, so the newer copy of it that
|
||||
the other branch had already fixed is reverted with nothing to notice;
|
||||
* a deduplication pass deletes the surviving definition rather than the duplicate;
|
||||
* a merge in the wrong ORDER restores a form the owner already overturned, because
|
||||
"newest wins" is a property of commit dates, not of decisions.
|
||||
|
||||
The parent SHAs are PINNED. Branch names float -- `cxi/p3-transport` will move, and a
|
||||
gate that reads a moving name stops testing the thing it was written for.
|
||||
"""
|
||||
import ast
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
# The two parents this tree was converged from, plus the live head it was branched off.
|
||||
#
|
||||
# SPRINT-LOCAL, AND DELIBERATELY NOT LITERALS HERE. These are PRIVATE commits of the
|
||||
# convergence sprint: they exist in the operator's worktrees and in no public clone, and
|
||||
# one of them is the owner's private terminal-demo head, which must never be referenced
|
||||
# from a file that ships. Hard-coding them made this test guaranteed-red for anyone who
|
||||
# checked out the PR and put a private SHA in tracked source. They are supplied by the
|
||||
# environment instead (the sprint harness sets it), so a public clone finds nothing to
|
||||
# check and SKIPS, while the sprint still gets the full census.
|
||||
#
|
||||
# REQUIRED PRE-PR DELETION: this whole file is sprint scaffolding for cross-branch
|
||||
# convergence checking and must be removed in the final pass — see the ledger row. It is
|
||||
# kept until then because synthesis needs the census right up to the merge.
|
||||
_PARENT_ENV = "OUROBOROS_CONVERGENCE_PARENTS" # p3,p4,live_head[,p2_chain]
|
||||
_STRICT_ENV = "OUROBOROS_CONVERGENCE_CENSUS_STRICT"
|
||||
|
||||
|
||||
def _pinned_parents() -> list:
|
||||
raw = str(os.environ.get(_PARENT_ENV, "") or "").strip()
|
||||
if not raw:
|
||||
local = REPO / ".convergence-parents" # untracked, operator-local
|
||||
try:
|
||||
raw = local.read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
raw = ""
|
||||
return [part.strip() for part in raw.replace("\n", ",").split(",") if part.strip()]
|
||||
|
||||
|
||||
_PINNED = _pinned_parents()
|
||||
P3_TRANSPORT = _PINNED[0] if len(_PINNED) > 0 else ""
|
||||
P4_MUTATING = _PINNED[1] if len(_PINNED) > 1 else ""
|
||||
LIVE_HEAD = _PINNED[2] if len(_PINNED) > 2 else ""
|
||||
|
||||
# A definition present in a parent and absent here is DATA LOSS unless it is named
|
||||
# below with the symbol that replaced it. "Deleted on purpose" is a motivated tombstone
|
||||
# (replacement `None`); everything else must point at a symbol that really exists, so a
|
||||
# reconciliation cannot be satisfied by a name someone meant to add and did not.
|
||||
RECONCILED = {
|
||||
# -- the transport lifecycle moved out of the tool module (cxi/p3-transport) ------
|
||||
"ouroboros.tools.delegate:_RunCustody": "ouroboros.delegate_custody:RunCustody",
|
||||
"ouroboros.tools.delegate:_settle": "ouroboros.delegate_custody:settle_run",
|
||||
"ouroboros.tools.delegate:_summary_of": "ouroboros.delegate_custody:summary_of",
|
||||
"ouroboros.tools.delegate:_disclosed_spend": "ouroboros.delegate_custody:disclosed_spend",
|
||||
"ouroboros.tools.delegate:_disclosed_tokens": "ouroboros.delegate_custody:disclosed_tokens",
|
||||
"ouroboros.tools.delegate:_retire_owned_project": "ouroboros.delegate_custody:retire_project",
|
||||
# -- the authority axis replaced the bare profile string (cxi/p4-mutating) --------
|
||||
"ouroboros.tools.delegate:_access_profile": "ouroboros.tools.delegate:_derive_authority",
|
||||
"ouroboros.tools.delegate:_route_health": "ouroboros.subagents:route_health",
|
||||
"ouroboros.tools.delegate:_exhausted_window_reset_at":
|
||||
"ouroboros.subagents:_exhausted_window_reset_at",
|
||||
# The truncator and every producer that must fit inside it now ask ONE function
|
||||
# instead of reading the dict pair through private aliases; the p3 output-delivery
|
||||
# port is what forced the move, because `_delivered_terminal_payload` has to bound
|
||||
# itself against the SAME limit the truncator applies.
|
||||
"ouroboros.loop_tool_execution:_TOOL_RESULT_LIMITS":
|
||||
"ouroboros.tool_capabilities:tool_result_limit",
|
||||
"ouroboros.loop_tool_execution:_DEFAULT_TOOL_RESULT_LIMIT":
|
||||
"ouroboros.tool_capabilities:tool_result_limit",
|
||||
# -- owner decision D28 renamed the two rows that pinned the OLD auto+exhausted
|
||||
# behaviour (nanny-anyway) to the ones that pin the loud API fallback -----------
|
||||
"tests.test_delegated_subagent_transport:test_rule_auto_with_spent_window_delegates_and_carries_reset_at":
|
||||
"tests.test_delegated_subagent_transport:test_rule_auto_with_every_profile_spent_falls_back_to_the_api_loudly",
|
||||
"tests.test_delegated_subagent_transport:test_dispatch_row_auto_with_a_spent_window_still_delegates_with_the_reset":
|
||||
"tests.test_delegated_subagent_transport:test_dispatch_row_auto_with_every_profile_spent_falls_back_to_the_api",
|
||||
# -- deliberately DELETED, no replacement (empty right side = adjudicated removal) -
|
||||
# P34R.10: exported and census-pinned but caller-free since the invocation-id
|
||||
# doctrine made `invocation_record` the one lookup surface; a dead durable-scan
|
||||
# helper invites drift between two readers of the same rows.
|
||||
"ouroboros.delegate_custody:start_was_requested": "",
|
||||
# -- inherited from the parents themselves, not produced by this convergence ------
|
||||
"ouroboros.launcher_bootstrap:python_bytecode_env":
|
||||
"ouroboros.launcher_bootstrap:embedded_python_env",
|
||||
"launcher:python_bytecode_env": "ouroboros.launcher_bootstrap:embedded_python_env",
|
||||
"ouroboros.packaged_cli:python_bytecode_env":
|
||||
"ouroboros.launcher_bootstrap:embedded_python_env",
|
||||
"ouroboros.subagents:_capability_depth_limit": None, # v6.87.7: the three axes
|
||||
"ouroboros.subagents:_review_or_scope_slots": None, # v6.87.7: no lane fans out
|
||||
# v6.87.7 deleted the lane/depth coupling these pinned (`auto` resolved to heavy for a
|
||||
# "mutating" child, and depth rewrote the lane). The transport phase branched before
|
||||
# that commit, so it still carries them; they assert behaviour that no longer exists.
|
||||
"ouroboros.usage_accounting:_quarantine_tail": "ouroboros.usage_ledger:_quarantine_tail",
|
||||
# The invocation-id contract replaced the content-stable Idempotency-Key (a stable
|
||||
# content hash made a deliberate re-run of the same prompt collide onto the finished
|
||||
# old run); the test moved with the contract it pins.
|
||||
"tests.test_delegated_subagent_transport:test_one_logical_start_presents_one_idempotency_key":
|
||||
"tests.test_delegated_subagent_transport:"
|
||||
"test_the_invocation_id_is_reused_on_retry_and_fresh_per_intended_start",
|
||||
**{f"tests.test_model_slot_role_model:{name}": None for name in (
|
||||
"test_auto_mutating_child_routes_to_heavy",
|
||||
"test_auto_readonly_child_routes_to_light",
|
||||
"test_depth_cap_is_configurable",
|
||||
"test_explicit_heavy_beyond_depth_cap_downgrades_with_note",
|
||||
"test_explicit_main_honored_within_depth_cap",
|
||||
"test_string_false_may_mutate_does_not_route_auto_to_heavy",
|
||||
)},
|
||||
**{f"tests.test_subagents_phase3:{name}": None for name in (
|
||||
"test_schedule_subagent_group_drive_failure_is_fail_closed",
|
||||
"test_schedule_subagent_review_lane_emits_task_group_metadata",
|
||||
"test_subagent_lane_resolution_fans_out_and_depth_coerces_light",
|
||||
)},
|
||||
}
|
||||
|
||||
|
||||
def _symbols(source: str, path: str) -> tuple:
|
||||
"""What one module binds, split into (definitions, import aliases).
|
||||
|
||||
The two are not the same kind of fact. A DEFINITION that disappears is loss until
|
||||
something is named as its replacement. An ALIAS that disappears is only loss if the
|
||||
definition behind it is gone too -- a module that stops importing a helper it no
|
||||
longer uses has lost nothing, while a module that splits in two and RE-EXPORTS its
|
||||
substrate (``usage_accounting`` after the ledger split) still answers to every one of
|
||||
those names. A census that conflates them either reports dozens of phantom losses --
|
||||
and then gets relaxed until it catches nothing -- or misses the real ones.
|
||||
"""
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
return set(), set()
|
||||
mod = path[:-3].replace("/", ".")
|
||||
out, aliases = set(), set()
|
||||
for node in tree.body:
|
||||
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
||||
aliases |= {f"{mod}:{a.asname or a.name.split('.')[0]}" for a in node.names}
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
out.add(f"{mod}:{node.name}")
|
||||
if isinstance(node, ast.ClassDef):
|
||||
for sub in node.body:
|
||||
if isinstance(sub, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
out.add(f"{mod}:{node.name}.{sub.name}")
|
||||
elif isinstance(sub, ast.AnnAssign) and isinstance(sub.target, ast.Name):
|
||||
out.add(f"{mod}:{node.name}.{sub.target.id}")
|
||||
elif isinstance(node, ast.Assign):
|
||||
out |= {f"{mod}:{t.id}" for t in node.targets if isinstance(t, ast.Name)}
|
||||
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
||||
out.add(f"{mod}:{node.target.id}")
|
||||
return out, aliases
|
||||
|
||||
|
||||
def _git(*args) -> str:
|
||||
return subprocess.run(["git", "-C", str(REPO), *args],
|
||||
capture_output=True, text=True, check=True).stdout
|
||||
|
||||
|
||||
def _inventory_at(sha: str) -> tuple:
|
||||
"""Every .py blob of one commit, read through a SINGLE `git cat-file --batch`.
|
||||
|
||||
The first cut spawned one `git show` per file per pinned parent — ~400 spawns
|
||||
times three parents on EVERY default pytest run, a tax on every future preflight
|
||||
(P34R.8). Two subprocesses per parent now: one tree listing, one batch read.
|
||||
"""
|
||||
entries = []
|
||||
for line in _git("ls-tree", "-r", sha).splitlines():
|
||||
meta, _, path = line.partition("\t")
|
||||
parts = meta.split()
|
||||
if path.endswith(".py") and len(parts) >= 3 and parts[1] == "blob":
|
||||
entries.append((parts[2], path))
|
||||
defs, aliases = set(), set()
|
||||
if not entries:
|
||||
return defs, aliases
|
||||
batch = subprocess.run(
|
||||
["git", "-C", str(REPO), "cat-file", "--batch"],
|
||||
input=("\n".join(oid for oid, _ in entries) + "\n").encode("ascii"),
|
||||
capture_output=True, check=True).stdout
|
||||
blobs, idx = {}, 0
|
||||
while idx < len(batch):
|
||||
line_end = batch.find(b"\n", idx)
|
||||
if line_end < 0:
|
||||
break
|
||||
header = batch[idx:line_end].decode("ascii", errors="replace").split()
|
||||
if len(header) == 3 and header[1] == "blob":
|
||||
size = int(header[2])
|
||||
blobs[header[0]] = batch[line_end + 1:line_end + 1 + size]
|
||||
idx = line_end + 1 + size + 1 # blob bytes + trailing newline
|
||||
else:
|
||||
idx = line_end + 1 # "<oid> missing" and friends
|
||||
for oid, path in entries:
|
||||
content = blobs.get(oid)
|
||||
if content is not None:
|
||||
d, a = _symbols(content.decode("utf-8", errors="replace"), path)
|
||||
defs |= d
|
||||
aliases |= a
|
||||
return defs, aliases
|
||||
|
||||
|
||||
def _inventory_here() -> tuple:
|
||||
defs, aliases = set(), set()
|
||||
for path in REPO.rglob("*.py"):
|
||||
rel = path.relative_to(REPO)
|
||||
if any(part in {".git", "__pycache__", "node_modules", "build", "dist"}
|
||||
for part in rel.parts):
|
||||
continue
|
||||
d, a = _symbols(path.read_text(encoding="utf-8", errors="replace"), str(rel))
|
||||
defs |= d
|
||||
aliases |= a
|
||||
return defs, aliases
|
||||
|
||||
|
||||
def _have(sha: str) -> bool:
|
||||
try:
|
||||
_git("cat-file", "-e", f"{sha}^{{commit}}")
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def _clone_is_partial() -> bool:
|
||||
"""Only a genuinely shallow/partial clone may excuse a missing pinned parent.
|
||||
|
||||
The census used to `pytest.skip` on ANY missing parent object, so the advertised
|
||||
parent-loss guard silently performed no check wherever the objects were absent —
|
||||
including a full clone whose pinned SHA had been pruned or mistyped, which is not
|
||||
a clone artifact but the guard's own evidence going missing (P34R.9).
|
||||
"""
|
||||
try:
|
||||
if _git("rev-parse", "--is-shallow-repository").strip() == "true":
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return True # cannot even ask: do not fake a verdict
|
||||
probe = subprocess.run(
|
||||
["git", "-C", str(REPO), "config", "--get-regexp", r"remote\..*\.partialclonefilter"],
|
||||
capture_output=True, text=True)
|
||||
return bool(probe.stdout.strip())
|
||||
|
||||
|
||||
def test_every_reconciliation_names_a_replacement_that_exists():
|
||||
"""A reconciliation map is only worth what its right-hand side is worth.
|
||||
|
||||
Always on, no git required: an entry pointing at a symbol nobody defines would let a
|
||||
real loss be waved through by a note, which is the exact failure the map exists to
|
||||
prevent.
|
||||
"""
|
||||
here_defs, here_aliases = _inventory_here()
|
||||
here = here_defs | here_aliases
|
||||
dangling = sorted(new for new in RECONCILED.values() if new and new not in here)
|
||||
assert not dangling, (
|
||||
"Reconciliation map names replacements that do not exist:\n "
|
||||
+ "\n ".join(dangling))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("parent", [P3_TRANSPORT, P4_MUTATING, LIVE_HEAD])
|
||||
def test_no_definition_from_a_parent_vanished_without_a_reconciliation(parent):
|
||||
"""A symbol a parent defined and this tree does not is loss until it is explained.
|
||||
|
||||
This is the check an ordinary merge cannot perform. `git merge` reconciles TEXT; a
|
||||
definition that one parent moved to a new file and the other parent had meanwhile
|
||||
improved in place is a clean merge and a silent revert.
|
||||
"""
|
||||
if not parent:
|
||||
pytest.skip(
|
||||
f"no convergence parents pinned ({_PARENT_ENV} unset): this census is "
|
||||
f"SPRINT-LOCAL scaffolding over private commits and cannot run in a public "
|
||||
f"clone. Slated for deletion in the final pass (see the ledger row).")
|
||||
if not _have(parent):
|
||||
# P34R.9 asked for a LOUD failure instead of a silent skip, and that still holds
|
||||
# wherever the census is claimed as evidence — but only there. A public clone has
|
||||
# no way to hold these private objects, so absence is the EXPECTED state and a
|
||||
# hard failure would only be red noise. The sprint sets the strict flag, which is
|
||||
# what turns a declared-but-missing pin back into a loud failure.
|
||||
if str(os.environ.get(_STRICT_ENV, "") or "").strip():
|
||||
pytest.fail(
|
||||
f"pinned parent {parent} is missing while {_STRICT_ENV} is set: the "
|
||||
f"convergence census cannot run, and skipping here is exactly how a "
|
||||
f"parent loss would go unnoticed. Fetch the phase branches or re-pin.")
|
||||
pytest.skip(f"pinned parent {parent} is not in this clone")
|
||||
parent_defs, parent_aliases = _inventory_at(parent)
|
||||
here_defs, here_aliases = _inventory_here()
|
||||
# Bare names still defined SOMEWHERE here: an alias that moved home is not a loss.
|
||||
live_names = {s.split(":", 1)[1] for s in here_defs}
|
||||
# Bare names the parent itself DEFINED. An alias of anything else is an import of an
|
||||
# external module (base64, json, ...); a module that stops importing one has lost
|
||||
# nothing, and only an alias of a REPO symbol can be a re-export worth tracking.
|
||||
parent_names = {s.split(":", 1)[1] for s in parent_defs}
|
||||
|
||||
def explained(sym: str) -> bool:
|
||||
if sym in RECONCILED or ":_RunCustody." in sym:
|
||||
return True
|
||||
if sym not in parent_aliases:
|
||||
return False
|
||||
bare = sym.split(":", 1)[1]
|
||||
return bare in live_names or bare not in parent_names
|
||||
|
||||
unexplained = sorted(s for s in (parent_defs | parent_aliases) - (here_defs | here_aliases)
|
||||
if not explained(s))
|
||||
assert not unexplained, (
|
||||
f"Bindings present in parent {parent} and absent here, with no entry in "
|
||||
f"RECONCILED and no surviving definition:\n " + "\n ".join(unexplained))
|
||||
|
||||
|
||||
# Every mechanism the transport phase contributed. The consult that reviewed this
|
||||
# convergence listed them precisely because choosing either FILE wholesale drops them.
|
||||
TRANSPORT_LIFECYCLE = {
|
||||
"ouroboros.delegate_custody": [
|
||||
"RunCustody", "replay", "lookup", "record_start_requested", "record_started",
|
||||
"open_runs", "idempotency_key", "cancel_and_verify",
|
||||
"settle_run", "retire_project", "release_task_runs", "reconcile_orphaned_runs",
|
||||
"open_containment_faults", "record_containment_fault", "resolve_containment_fault",
|
||||
"daemon_says_absent", "close_absent_run",
|
||||
],
|
||||
"ouroboros.tools.delegate": [
|
||||
"_safe_run_filename", "_stage_full_output", "_preview_payload",
|
||||
"_delivered_terminal_payload", "_start_request", "_retire_orphaned_registration",
|
||||
"_CANCEL_NOTES", "_TIMELINE_LABEL_CHARS",
|
||||
],
|
||||
}
|
||||
|
||||
# Every mechanism the mutating phase contributed, in the same file the transport phase
|
||||
# rewrote. Neither list is a subset of the other, which is why no branch is "the base".
|
||||
MUTATING_BEHAVIOUR = {
|
||||
"ouroboros.tools.delegate": [
|
||||
"_derive_authority", "_widened_access", "_Breach", "_home_isolation_breach",
|
||||
"_containment_breach", "_containment_evidence", "_record_containment",
|
||||
"_resolved", "_mutating_run_root", "_halt_breached_run", "_host_instructions",
|
||||
"_HOST_INSTRUCTIONS", "_UNPROVEN_BOUNDARY_INSTRUCTION", "_NO_BOUNDARY_NOTE",
|
||||
"_ACCESS_RANK", "_CLAUDEXOR_MAX_SECONDS",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("catalog,label", [(TRANSPORT_LIFECYCLE, "transport lifecycle"),
|
||||
(MUTATING_BEHAVIOUR, "mutating behaviour")])
|
||||
def test_both_phases_survive_the_convergence(catalog, label):
|
||||
"""Named, not derived: this is the list a reviewer can argue with."""
|
||||
import importlib
|
||||
|
||||
missing = []
|
||||
for module_name, names in catalog.items():
|
||||
module = importlib.import_module(module_name)
|
||||
missing += [f"{module_name}.{n}" for n in names if not hasattr(module, n)]
|
||||
assert not missing, f"{label} mechanisms lost in convergence:\n " + "\n ".join(missing)
|
||||
|
||||
|
||||
def test_settlement_did_not_revert_the_cost_fixes_it_absorbed():
|
||||
"""The moved function must carry the fixes the OTHER branch made to it in place.
|
||||
|
||||
`_settle` moved from `tools/delegate.py` into `delegate_custody.settle_run` on one
|
||||
branch while the other branch fixed three defects in the version it was moving away
|
||||
from: an ESTIMATED charge recorded as settled, an unreported token count written as
|
||||
zero, and a projection called final over both. A move is a clean git ADD, so nothing
|
||||
conflicts and nothing fails -- this test is the only thing standing between the
|
||||
convergence and a silent three-fix revert.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
from ouroboros import delegate_custody as custody
|
||||
|
||||
# The reader answers BOTH halves at once, so no call site can ask only the amount.
|
||||
assert custody.disclosed_spend({"spendUsd": 1.5, "spendEstimated": True}) == (1.5, True)
|
||||
assert custody.disclosed_spend({"spendUsd": 0.0}) == (0.0, False)
|
||||
assert custody.disclosed_spend({}) == (None, False)
|
||||
|
||||
# An unreported token count is UNKNOWN, never a confident zero.
|
||||
assert custody.disclosed_tokens(None) is None
|
||||
assert custody.disclosed_tokens(0) == 0
|
||||
assert custody.disclosed_tokens(41) == 41
|
||||
|
||||
src = inspect.getsource(custody.settle_run)
|
||||
for fragment, why in [
|
||||
("spend_estimated=estimated", "the ledger row must carry the estimated flag"),
|
||||
("disclosed_tokens(", "token reads must keep None for an unreported count"),
|
||||
("cached_tokens=", "the third token field the schema defines must be recorded"),
|
||||
("not estimated", "an estimated amount must not settle as final"),
|
||||
]:
|
||||
assert fragment in src, f"settle_run reverted a fix it absorbed: {why}"
|
||||
# The undisclosed case must not be republished as a confident 0.0 in the envelope.
|
||||
assert '"cost_usd": spend' in src, "an undisclosed spend must stay None in the envelope"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Decisions the owner overturned, which a merge must not be able to restore.
|
||||
#
|
||||
# Each is a PAIR of witnesses: the superseded form, and the replacement that retired it
|
||||
# on `cxi/p2-axes`. Which one may be present depends on WHICH TREE this is, and the
|
||||
# guard derives that from the tree itself -- never from a flag someone can forget:
|
||||
#
|
||||
# * BEFORE the p2 material arrives (this tree today, converging only the transport
|
||||
# and mutating phases), the superseded form is the expected -- and only -- state.
|
||||
# * AFTER the p2 material arrives, the replacement is the ONLY passing state. A
|
||||
# superseded form that survives that merge is a decision the owner overturned,
|
||||
# restored by merge order, and "exactly one is present" would have blessed it.
|
||||
#
|
||||
# "The p2 material arrived" is evidenced two ways, either sufficient:
|
||||
# * the p2 decision chain is in this tree's HISTORY (P2_AXES_CHAIN below is the
|
||||
# commit where the last of these retirements landed, so its ancestry implies all
|
||||
# of them), which also catches the coherent-reversion case where a bad merge
|
||||
# restored EVERY superseded form and deleted every replacement; or
|
||||
# * ANY replacement witness is already observable in the tree -- a cherry-picked or
|
||||
# partially merged arrival flips the era even without the commits, so the pairs it
|
||||
# did NOT update fail loudly instead of reading as "legitimately pre-p2".
|
||||
#
|
||||
# Within the required era, both failure directions remain:
|
||||
#
|
||||
# both present -> a merge kept the old form beside the new one. The old one still
|
||||
# runs, and nothing else would say so.
|
||||
# neither -> the decision EVAPORATED. A deduplication pass that deleted the
|
||||
# surviving implementation instead of the duplicate lands here, and
|
||||
# that loss class has already happened on this sprint.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# v6.87.28 on `cxi/p2-axes`: the commit that landed the LAST of the retirements below
|
||||
# (D2's RETIRED_SCHEDULE_PARAMS and the D4 chain; D3's inheritance landed in v6.87.26,
|
||||
# its ancestor). Pinned like the parent SHAs above: branch names float, commits do not.
|
||||
P2_AXES_CHAIN = _PINNED[3] if len(_PINNED) > 3 else ""
|
||||
|
||||
|
||||
def _p2_chain_in_history() -> bool:
|
||||
"""Is the p2 decision chain an ancestor of this tree's HEAD?
|
||||
|
||||
False when the commit is simply not in this clone (shallow / grafted): ancestry is
|
||||
a SUFFICIENT arrival signal, never a necessary one -- witness evidence still flips
|
||||
the era on its own.
|
||||
"""
|
||||
if not P2_AXES_CHAIN:
|
||||
return False # no pin supplied: ancestry says nothing
|
||||
probe = subprocess.run(
|
||||
["git", "-C", str(REPO), "merge-base", "--is-ancestor", P2_AXES_CHAIN, "HEAD"],
|
||||
capture_output=True, text=True)
|
||||
return probe.returncode == 0
|
||||
|
||||
|
||||
def _overturned_verdicts(witnesses, p2_chain_in_history):
|
||||
"""Desired-state verdicts for a whole witness table. Pure over its inputs.
|
||||
|
||||
``witnesses`` is ``[(name, superseded_present, replacement_present), ...]``; the
|
||||
era is derived HERE, from the table plus history, so no caller can hand a pair an
|
||||
era that contradicts the evidence. Returns ``(p2_material_arrived, {name:
|
||||
failure_reason_or_None})``.
|
||||
"""
|
||||
arrived = p2_chain_in_history or any(new for _, _, new in witnesses)
|
||||
verdicts = {}
|
||||
for name, old, new in witnesses:
|
||||
if old and new:
|
||||
verdicts[name] = (
|
||||
"BOTH are present: a merge kept the retired form beside the one that "
|
||||
"replaced it, and the retired form still runs.")
|
||||
elif not old and not new:
|
||||
verdicts[name] = (
|
||||
"NEITHER is present: the decision has evaporated. Something deleted "
|
||||
"the surviving implementation, not the duplicate.")
|
||||
elif old and arrived:
|
||||
verdicts[name] = (
|
||||
"the p2 material has arrived ("
|
||||
+ ("the p2 decision chain is in this tree's history"
|
||||
if p2_chain_in_history else
|
||||
"another pair already shows its replacement")
|
||||
+ "), yet the SUPERSEDED form is what survived here: the merge "
|
||||
"restored a decision the owner overturned.")
|
||||
else:
|
||||
verdicts[name] = None
|
||||
return arrived, verdicts
|
||||
|
||||
|
||||
def _public_effort_is_published() -> bool:
|
||||
from ouroboros.tools.control import schedule_subagent_properties
|
||||
|
||||
return "effort" in schedule_subagent_properties()
|
||||
|
||||
|
||||
def _effort_is_retired() -> bool:
|
||||
import ouroboros.tools.control as control
|
||||
|
||||
return "effort" in getattr(control, "RETIRED_SCHEDULE_PARAMS", {})
|
||||
|
||||
|
||||
def _omitted_lane_collapses_to_light() -> bool:
|
||||
import inspect
|
||||
|
||||
from ouroboros.subagents import resolve_subagent_lane
|
||||
|
||||
return "parent_lane" not in inspect.signature(resolve_subagent_lane).parameters
|
||||
|
||||
|
||||
def _omitted_lane_inherits_the_parents() -> bool:
|
||||
import inspect
|
||||
|
||||
from ouroboros.subagents import resolve_subagent_lane
|
||||
|
||||
return "parent_lane" in inspect.signature(resolve_subagent_lane).parameters
|
||||
|
||||
|
||||
def _capability_delta_chain_is_absent() -> bool:
|
||||
import ouroboros.subagents as subagents
|
||||
|
||||
return not hasattr(subagents, "capability_delta_notice")
|
||||
|
||||
|
||||
def _capability_delta_chain_is_present() -> bool:
|
||||
"""Present means the WHOLE chain: durable record, child's prompt, parent's result."""
|
||||
import ouroboros.agent as agent
|
||||
import ouroboros.subagents as subagents
|
||||
import ouroboros.tools.control as control
|
||||
|
||||
return (hasattr(subagents, "capability_delta_notice")
|
||||
and hasattr(agent, "capability_delta_prompt_block")
|
||||
and hasattr(control, "disclosable_capability_delta"))
|
||||
|
||||
|
||||
OVERTURNED = [
|
||||
("D2 a public `effort` parameter on schedule_subagent",
|
||||
_public_effort_is_published, _effort_is_retired,
|
||||
"effort is derived from the owner's configured effort for the task type; a public "
|
||||
"knob was a second answer to the question model_lane already answers"),
|
||||
("D3 an omitted model_lane collapsing to Light",
|
||||
_omitted_lane_collapses_to_light, _omitted_lane_inherits_the_parents,
|
||||
"an omitted lane INHERITS the parent's: a Heavy parent handing a child a piece of "
|
||||
"its own job must not get a Light child with no signal that it happened"),
|
||||
("D4 no capability_delta reaching the record, the prompt and the result",
|
||||
_capability_delta_chain_is_absent, _capability_delta_chain_is_present,
|
||||
"a child that runs below what was asked for must say so, in the durable record, in "
|
||||
"its own prompt, and in what its parent reads back"),
|
||||
]
|
||||
|
||||
|
||||
def _witness_table():
|
||||
return [(row[0], row[1](), row[2]()) for row in OVERTURNED]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,superseded,replacement,decision",
|
||||
OVERTURNED, ids=[row[0].split()[0] for row in OVERTURNED])
|
||||
def test_a_merge_cannot_restore_a_decision_the_owner_overturned(
|
||||
name, superseded, replacement, decision):
|
||||
old, new = superseded(), replacement()
|
||||
arrived, verdicts = _overturned_verdicts(_witness_table(), _p2_chain_in_history())
|
||||
reason = verdicts[name]
|
||||
assert reason is None, (
|
||||
f"{name}\n"
|
||||
f" the owner's decision: {decision}\n"
|
||||
f" superseded form present: {old}\n"
|
||||
f" replacement present: {new}\n"
|
||||
f" p2 material arrived: {arrived}\n"
|
||||
f" {reason}\n"
|
||||
" Resolve by hand -- do not silence this by deleting the branch you did not "
|
||||
"expect to see.")
|
||||
|
||||
|
||||
# All four quadrants of one witness pair, in both eras, against the SAME function the
|
||||
# live guard runs. The pair under test is fed alongside a neighbour so the mixed-tree
|
||||
# derivation is exercised too, not just the chain-ancestry one.
|
||||
@pytest.mark.parametrize("old,new,chain,ok", [
|
||||
# -- before the p2 material arrives -------------------------------------------
|
||||
(True, False, False, True), # superseded-only: this tree today. Expected.
|
||||
(False, False, False, False), # neither: evaporated, era cannot excuse it
|
||||
(True, True, False, False), # both: the replacement's presence itself flips
|
||||
# the era, and coexistence fails in any era
|
||||
(False, True, False, True), # replacement-only: its own presence IS arrival
|
||||
# evidence (cherry-pick with no commits), and
|
||||
# replacement-only is the desired end state
|
||||
# -- after the p2 material arrives (chain in history) -------------------------
|
||||
(True, False, True, False), # THE CASE "exactly one" BLESSED: the merge
|
||||
# restored the overturned form. Must fail.
|
||||
(False, True, True, True), # replacement-only: the ONLY passing state
|
||||
(True, True, True, False), # both: still fails
|
||||
(False, False, True, False), # neither: still fails
|
||||
], ids=["pre-superseded-only", "pre-neither", "pre-both", "pre-replacement-only",
|
||||
"post-superseded-only", "post-replacement-only", "post-both", "post-neither"])
|
||||
def test_overturned_guard_quadrants(old, new, chain, ok):
|
||||
witnesses = [("pair", old, new)]
|
||||
_, verdicts = _overturned_verdicts(witnesses, p2_chain_in_history=chain)
|
||||
assert (verdicts["pair"] is None) is ok, verdicts["pair"]
|
||||
|
||||
|
||||
def test_partial_arrival_fails_the_pairs_it_skipped():
|
||||
"""A cherry-picked p2 arrival must not leave the other pairs reading as pre-p2.
|
||||
|
||||
One pair showing its replacement is arrival evidence for the WHOLE table: the
|
||||
pair still in superseded form fails as a restoration even though the p2 commits
|
||||
are nowhere in history.
|
||||
"""
|
||||
arrived, verdicts = _overturned_verdicts(
|
||||
[("landed", False, True), ("skipped", True, False)],
|
||||
p2_chain_in_history=False)
|
||||
assert arrived is True
|
||||
assert verdicts["landed"] is None
|
||||
assert verdicts["skipped"] is not None
|
||||
assert "another pair already shows its replacement" in verdicts["skipped"]
|
||||
|
||||
|
||||
def test_coherent_reversion_is_caught_by_the_chain():
|
||||
"""A merge that restored EVERY superseded form and deleted every replacement looks
|
||||
exactly like the legitimate pre-p2 tree to the witnesses alone; the pinned decision
|
||||
chain in history is what refuses it."""
|
||||
table = [("D2", True, False), ("D3", True, False), ("D4", True, False)]
|
||||
pre_arrived, pre = _overturned_verdicts(table, p2_chain_in_history=False)
|
||||
post_arrived, post = _overturned_verdicts(table, p2_chain_in_history=True)
|
||||
assert pre_arrived is False and all(v is None for v in pre.values())
|
||||
assert post_arrived is True and all(v is not None for v in post.values())
|
||||
assert all("history" in v for v in post.values())
|
||||
|
|
@ -14,7 +14,6 @@ from ouroboros.reviewer_slot_config import (
|
|||
SCOPE_SLOT_LIMIT,
|
||||
TRIAD_SLOT_LIMIT,
|
||||
advisory_slot_config,
|
||||
commit_scope_rows,
|
||||
commit_triad_rows,
|
||||
load_reviewer_slot_config,
|
||||
parse_reviewer_slots,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import asyncio
|
|||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
from unittest.mock import patch
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import pathlib
|
|||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from types import ModuleType, SimpleNamespace
|
||||
|
||||
from ouroboros.tools.registry import ToolRegistry
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ def test_owner_floor_write_changes_no_scope_review_behaviour(monkeypatch, tmp_pa
|
|||
|
||||
import ouroboros.config as cfg
|
||||
from ouroboros.gateway import settings as smod
|
||||
from ouroboros.tools import review_helpers, scope_review as sr
|
||||
from ouroboros.tools import scope_review as sr
|
||||
|
||||
monkeypatch.setattr(cfg, "DATA_DIR", tmp_path)
|
||||
monkeypatch.setenv("OUROBOROS_SCOPE_REVIEW_FLOOR", "blocking_1m")
|
||||
|
|
@ -689,7 +689,6 @@ def test_capability_evidence_is_route_aware_not_model_aware(monkeypatch, tmp_pat
|
|||
unchanged model produced a route with no evidence, no second probe and no notice —
|
||||
the next scope review fell silently to the conservative sub-floor and the advertised
|
||||
owner-ack path was unreachable."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
import ouroboros.config as cfg
|
||||
from ouroboros import capability_evidence as ce
|
||||
|
|
|
|||
|
|
@ -517,4 +517,4 @@
|
|||
* @property {boolean=} ok
|
||||
*/
|
||||
|
||||
export const GATEWAY_CONTRACT_VERSION = '6.87.6';
|
||||
export const GATEWAY_CONTRACT_VERSION = '6.89.0';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ouroboros-web",
|
||||
"version": "6.87.6",
|
||||
"version": "6.89.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Ouroboros browser UI package boundary",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue