v7: repository refactor (draft, work in progress) - #226
Draft
ouroboros-agent wants to merge 325 commits into
Draft
v7: repository refactor (draft, work in progress)#226ouroboros-agent wants to merge 325 commits into
ouroboros-agent wants to merge 325 commits into
Conversation
…ter received (D04) The verbatim gate, run over the tree with the S1 settings lane integrated, flagged two rows: `config.py::SETTINGS_DEFAULTS` and `::RETIRED_SETTING_KEYS` moved into `settings_defaults.py` as-is, then S1's spec 4.3.6 commit retired the three no-op knobs (SOFT/HARD_TIMEOUT, PLAN_TASK_SWARM_HEARTBEAT_STALE) from the defaults and listed them as retired — an approved observable delta, not a verbatim move. - MIGRATION_v7.md: both rows now say what happened and carry a delta id. - scripts/v7_migration.py / tests/test_v7_migration_ledger.py: `D04` (spec 4.3.6 retired settings knobs) joins the approved delta ids and is the pinned expectation for exactly these two rows.
supervisor/events.py was the largest supervisor giant (4288 lines, 205k bytes). The seams follow the families the dispatch table already had, not a line budget: - events_chat_delivery.py (311): what reaches the owner's chat — text, photo, video, document, the typing indicator — plus the bound project chat id every other family routes onto, and the two-level final-answer dedupe. - events_subagent_admission.py (509): the census of a root's live tree, the depth/breadth caps and their reservation arithmetic, the composed delegated prompt, the resolved write surface and external-workspace binding, and the typed rejection or scheduled metadata the requester reads back. - events_schedule_task.py (482): the gates the schedule handler needs — chat target, semantic duplicate over description and parent context — the composed queue payload, and every refusal including rejected-worktree cleanup. - events_project_routing.py (562): where a chat turn becomes a task and where a project scope is bound: routing receipts, off-loop source preparation, the durable rejection record, rollback of a promoted task that never queued, and the registry side of project scope and digests. - events_task_done.py (807): terminal-event resolution — the authoritative cost projection from the physical-attempt ledger, the AR2-3 durable validation and its lifecycle-fault lane, the durable-write fault lane, the single-shot provider-death notice, and the one dispatch that delivers and releases. - events_coop_checkpoint.py (181): off-loop cooperative checkpoints at tree quiescence with the per-root in-flight latch and its replay rule. - events_evolution_done.py (153): terminal handling of an evolution task. - events_budget.py (284): worker-reported usage and the two budget fences. - events_worker_reports.py (245): heartbeats, dispatch resolution, metrics, forwarded log lines, the acceptance-fence acknowledgement, skill-lifecycle notices, and the bounded external-wait lease. - events_runtime_controls.py (316): events that change the runtime's posture rather than one task's state. _close_campaign_after_owner_stop moves to supervisor/queue_transitions.py instead of a family module: the owner-stop backstop and stop_evolution_tasks are one honesty rule and now live in one place. What stays in events.py (692) is the dispatch table, its loop, and the schedule_task handler — the one function the exact (path, qualname) function manifest tracks, whose relocation the shrink-only ratchet would read as new debt at a new key. Verbatim: all 81 top-level symbols are byte-identical to their text at bf1f5c5, proven by AST source-segment comparison — 77 moved, 4 stayed. No body line was rewritten. Families never import the dispatcher; events.py re-exports every moved name plus its whole prior import surface, so its public import surface and every monkeypatch target that resolves through it are unchanged. The one observable difference is the logger name on moved log lines, which now names the owning module (the same effect the earlier task_reaper and steering extractions had). Protected lists: supervisor/events.py is a HOT_CODE_PATHS entry, so all ten families join that list in this commit; no other protected list names it. Tests retargeted to the new owners, each because the seam it patches moved: coop-checkpoint spawn and the racing subagent predicate, the lifecycle-fault coop probes, the provider-death registry, the llm_usage and review-wave writers, the terminal-guard source read, and the enforce harness clock (which now has to reach every owner the event pump can enter). No assertion was weakened.
supervisor/task_lifecycle.py held two things that never touch each other's state: the CASCADE protocol (fence registries, the per-cascade token sequence, the subtree sweep, the admission fences) and CUSTODY — the one settle owner of a durable cancel intent. Custody moves to supervisor/cancel_custody.py (893), the same module-size boundary that already produced cancel_publication.py and queue_transitions.py; task_lifecycle.py keeps the cascade protocol (765). The seam deliberately does NOT cut the cascade: cancel_task_by_id, its exclusive cascade postcondition, the protected-set drop in its finally, _cancel_subtree_ sweep and the three module globals they share (CANCELLED_ROOT_FENCES, _ACTIVE_CASCADE_FENCES, _CASCADE_TOKEN_SEQ) stay in one module, because splitting them would turn a module-local invariant into a cross-module mutable global. A structural test pins that. _queue_module moves with custody (its five heaviest callers) and is re-exported, so there is still exactly one lazy queue handle for this family. Verbatim: all 36 top-level symbols are byte-identical to their text at bf1f5c5 — 16 moved, 20 stayed — proven by AST source-segment comparison. task_lifecycle re-exports every moved name, so supervisor.queue remains the single public import surface and the fence monkeypatches in eight test modules still bind the same objects. Protected lists: supervisor/task_lifecycle.py appears in none of SAFETY_CRITICAL_PATHS, FROZEN_CONTRACT_PATHS, RELEASE_INVARIANT_PATHS, _REVIEW_STACK_PATHS or HOT_CODE_PATHS, so the new owner needs no list entry. Two tests retargeted, both because they read the moved source or patch a moved seam: the drive-cleanup ordering contract now reads cancel_custody.py, and the mid-teardown crash test patches _finish_captured_running on its owner. Neither assertion changed.
An event with no answer is dropped: the dispatcher logs "no handler" and the fact the producer meant to report is gone. That hole is invisible from either end alone — plan_task_deadline_skip has been emitted and discarded since it was written, and the schedule_task dispatch key has advertised a capability no producer ever used. supervisor/event_taxonomy.py names, for every event kind the runtime can produce, WHO answers it and HOW, in four tiers: worker_handler (the dispatch table), server_intercept (the server's drain loop, because restarting the process is not something the supervisor thread can do to itself), nested_log_event (it rides inside log_event.data and the nested branch answers it), telemetry_only (recorded as a fact, no action). It is data — it imports nothing from the runtime and dispatches nothing, so EVENT_HANDLERS remains the single execution authority. tests/test_event_taxonomy.py reads both ends: the dispatch table and the worker_handler tier must be the same set, each handled event must be answered by the module the table names, every producer the AST scan discovers must have a declared disposition, and every declared producer file must still name its event. The scan is a disclosed lower bound (it resolves dict literals and one level of local binding), so it can only add failures for real producers; the per-row producer check covers the direction it cannot. Two behaviour changes fall out, both spec 4.3.12: - The dead "schedule_task" KEY is removed from EVENT_HANDLERS. The function is untouched and still serves schedule_subagent, which is its only producer. - A dispatch miss now consults the taxonomy: a declared tier is recorded under its tier in events.jsonl instead of being dropped as unknown. That gives plan_task_deadline_skip the disposition it never had. An UNDECLARED event keeps the loud unknown_worker_event path, so the taxonomy cannot turn a genuine hole into a quiet ledger row. Writing the table surfaced a second producer in the same class, disclosed rather than fixed: ouroboros/tools/ci.py emits a "progress" event that nothing renders. It is declared telemetry_only so the fact survives, and the note in the table says plainly that no live rendering exists — that is a producer expectation for a later lane, not something this commit invents behaviour for. supervisor/events.py is a HOT_CODE_PATHS entry, so the taxonomy joins that list in the same commit.
ouroboros/safety.py runs inside every worker process, and it reached for two
host facts instead of being handed them:
- `from supervisor.state import update_budget_from_usage` at module scope. An
import-time edge from the agent core into the supervisor package makes the
code that must be isolated from the host depend on it to load at all; every
sibling that needs the same writer (agent_task_pipeline, reflection,
post_task_evolution, semantic_dedup, improvement_backlog) already reaches it
at call time.
- `pathlib.Path("../data")` as the observability root whenever the context did
not name one. That spelling resolves to the real data root only by coincidence
of the dev layout (cwd under repo/); anywhere else it writes records into
whatever directory sits beside the current one. It is the same defect the
review coordinator's ISO-DRIP default already had.
Both become context questions. `_safety_drive_root(ctx)` prefers the context's
drive root and otherwise reads the absolute configured data root late off the
config module, the same resolution order the review surfaces use.
`_record_safety_usage(ctx, usage)` prefers a ledger writer the context provides,
so a caller that owns its own accounting is charged where it lives, and
otherwise reaches this process's supervisor state at call time. Behaviour for
every current caller is unchanged; what is gone is the import-time coupling and
the cwd-relative guess.
Nothing else in the safety path moves: the policy table, the verdict parsing,
the routing resolution and the once-per-50-calls provider ground-truth cadence
are untouched.
ouroboros/safety.py is in SAFETY_CRITICAL_PATHS; spec 4.4 authorises exactly
this change and the diff is confined to the import line, two call sites, and the
two helpers they call.
The one test that pinned the old module-level name now patches the real ledger
module for the default path and additionally proves the injected path — a
context that owns its accounting is charged there and the supervisor module is
not touched. Two new tests pin the class: no import-time supervisor edge, and an
absolute observability root that a context's own root still wins over. A
devtools test that read the moved llm_usage projection out of events.py now
reads its owner.
…keys OUROBOROS_SOFT_TIMEOUT_SEC, OUROBOROS_HARD_TIMEOUT_SEC and OUROBOROS_PLAN_TASK_SWARM_HEARTBEAT_STALE_SEC have been no-ops since the idle, deadline, absolute-ceiling and reaper rails replaced them. The supervisor kept acting as if they were live, which is how a retired setting keeps looking like a control the owner can turn. - supervisor/workers.py drops three module globals that were written by init and read by nothing: SOFT_TIMEOUT_SEC, HARD_TIMEOUT_SEC, and the third copy of TOTAL_BUDGET_LIMIT (supervisor.state is the budget authority; the S5 read-only audit confirmed this triplicate is dead). init keeps the three arguments its last caller still supplies, and says in its own docstring that it reads none of them. - supervisor/queue.py stops rebinding the two timeout constants. They were rebound to the same two literals on every init, which reads as configuration; init now inspects the values for the deprecation notice and discards them. The names stay importable because the owner status command is their last reader. - supervisor/state.py::status_text stops printing "soft=600s, hard=1800s" and names the live rails instead. Both arguments are accepted and ignored. - The terminal-bench container no longer forwards the two keys, where a no-op key made a run look configured when it was not. - The three ARCHITECTURE settings rows say what is now true: the runtime keeps no copy, renders nothing, and forwards nothing. What this half cannot finish, and who owns it: the settings-side retirement (RETIRED_SETTING_KEYS, gateway/settings.py _IMMEDIATE_KEYS, config.py defaults) belongs to lane S1, and server.py — which reads the two settings, passes them to workers.init, and imports the two queue constants for /status — belongs to lane S2. Everything here is therefore landed compatibly: no signature loses a parameter and no name a caller imports disappears, so S1 and S2 can retire the last arguments without a flag day. Untouched by design: until_deadline (a live cap), OUROBOROS_MODEL_FALLBACK (a live bench contract) and stall_rounds_threshold.
supervisor/workers.py held two worlds in one module: the POOL — repo/drive roots, size, the worker table, the shared PENDING/RUNNING refs, the crash clock, spawn/respawn/kill/health/assignment — and the code that runs INSIDE a worker child process, where none of that state exists. supervisor/worker_process.py (232) takes the second: the entry point, the repo/drive root binding it performs before it can read anything, the log-sink filter that keeps types with a dedicated event sibling from double-broadcasting, and the crash record the parent would otherwise never see. worker_main stays a module-level function so platforms that spawn rather than fork can still pickle it by name, and a test pins that (module, qualname, round-trip). Verbatim: all 6 moved symbols are byte-identical to their text at d28d6849, and the 84 that stayed are unchanged, proven by AST source-segment comparison. workers.py re-exports every moved name. This is a partial split, and the report says so plainly: the pool half (2698) cannot follow verbatim. Every remaining cluster — promotion, assignment, health, spawn/kill, the direct-chat lane — reads module globals that supervisor.init and init_queue_refs REBIND (REPO_DIR, DRIVE_ROOT, MAX_WORKERS, PENDING, RUNNING, QUEUE_SEQ_COUNTER_REF), and 67 test sites monkeypatch those exact names. Moving such a cluster requires either rewriting the moved bodies to read a module-qualified handle (not verbatim) or duplicating the binding (two sources of truth, and the monkeypatches then bind the wrong copy). The seam that resolves it is spec 4.3.14 QueueState, which this lane's plan defers behind the delegation gap analysis. The child-process boundary is the one that does NOT need it. Protected lists: supervisor/workers.py appears in none of them, so the new owner needs no entry. One test retargeted: the extension-reload ordering contract parses worker_main out of a source file by path and now reads its owner. Its assertions are unchanged.
…y the S3 rebase resolution
… new debt Owner decision (plan §1.9, batch №8 item 2): the FUNCTION_DEBT transition validator refused any key that was absent from the previous manifest, so an extraction could not carry an oversized function into its leaf — the exact qualname at a new path counted as fresh debt and the function had to stay in the parent (S3's `_handle_schedule_task` in `supervisor/events.py`). `validate_manifest_transition` now treats a key whose qualname left exactly one path and appeared at exactly one other path in the same transition as a relocation: the count is unchanged and the ratchet still names the function. A fresh >300-line function, a swap onto a different qualname, an addition beside the original, or an ambiguous many-to-one move are still refused — the test that used to pin "no swap at equal cardinality" now pins exactly this contract (the earlier stricter clause was a policy the owner relaxed, not a bug it caught). DEVELOPMENT.md states the rule.
…change (D11) The relocation commit renamed `test_transition_rejects_function_swap_even_at_same_cardinality` to `test_transition_allows_a_same_qualname_relocation_but_not_a_swap` without a ledger row; the migration validator (and the S2 gate) caught the untracked rename. The row now records it under D11 (plan §1.9 batch №8 item 2: the FUNCTION_DEBT transition rule) — the registry comment names D10/D11 and the ledger test pins the expectation.
server.py was 2986 lines: the ASGI composition, the lifespan, the supervisor loop, owner-message routing, restart/panic orchestration and every periodic sweep in one module. The seams follow the responsibilities it already had, and each one is bounded by the module state it may not take with it. - server_process.py (38): the facts every server leaf shares — the drive root, the "server" logger every server module writes to, and the restart-request signals with their setter. Extracted first because a leaf that needed any of them would otherwise have to import the composition root back. - server_routing_context.py (442): bounded projections one owner turn may address — addressable roots, a project's last-result ground truth, the Main manifest, decision-turn metadata, chat-to-project classification. - server_owner_routing.py (531): where a single owner message goes — attachment staging, the one unambiguous mailbox delivery, the typed bubble-free receipt, the decision-lane dispatch, and the /evolve off stop transaction. - server_liveness.py (136): the two silent-wedge predicates, the owner alert, and the watchdog thread that runs outside the loop it watches. - server_maintenance.py (249): the startup and periodic upkeep a supervisor generation owes the drive — custody reaping, delegated-run reconciliation, fail-closed snapshot GC, zombie reconciles, startup task recovery. - server_restart.py (284): the restart transaction from request through the loop-tick drain to the exit signal, plus the shutdown teardown arguments the lifespan path shares. What stays in server.py (1459) is the composition root and exactly the state a leaf cannot hold without a back-edge: REPO_DIR (its fallback is Path(__file__).parent, which only resolves correctly at the repo root), the bound event loop, the actually bound port, the supervisor-generation handles (_supervisor_ready/_error/_thread/_consciousness), the launcher-managed flag, the exit codes, the panic entry point, the route table and app assembly, the lifespan, the supervisor loop, and the owner-command dispatch that calls the panic entry. server.py leaves GIANT_PATHS and MODULE_DEBT_1500 for the 1001-1500 band with a rationale. Verbatim: all 84 top-level symbols of the pre-split module are byte-identical at bf1f5c5, proven by AST source-segment comparison — 45 in their new owner, 39 still in server.py. No body text changed. The only edits to text that did not move are three "# noqa: F401" markers on base64/subprocess/read_json_dict, whose last users moved out; the imports stay so server.py's import surface is unchanged. Leaves deliberately omit "from __future__ import annotations" because the pre-split module did not have it. Leaves never import server; server re-exports every moved name, so server.<name> keeps the same object. Five tests that monkeypatched a moved seam now patch its new owner: the drain trio and the restart tail (test_promote_chat_flow, test_server_shutdown, test_evolution_state_integrity_v3), the decision-turn project lookup (test_project_routing_v664), the snapshot prune drive root (test_delegated_run_isolation), and the custody skill probe (test_delegated_subagent_transport, retargeted through a module-level short alias so the byte-capped file shrinks by 10 bytes instead of growing). test_osworld_cu_bridge's exact-path grandfather contract used server.py as its one hardcoded ROOT-level debt sample, which paying server.py down out of the giant layer would have turned red for the wrong reason. That block now derives its root samples from the live manifest — the technique the test already used for nested paths, and the one its own comment asks for — and keeps the four-distinct-spellings assertion so an empty loop cannot retire the contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t learns them Emergency Stop 2A is about to change HOW server_control.execute_panic_stop learns the port the server bound. These pins land first, against the current behaviour, so the change is measured against an observable contract instead of against its own implementation. Pinned: a panic raised through the server sweeps the ACTUALLY bound main port (9123 here, never the hardcoded 8765 that would kill a stranger's listener on a custom-port install); the host-service port follows it; a raising main-port sweep still costs neither the default-port fallback nor the host-service sweep; the teardown order ends with the child/port sweep and then os._exit with the panic exit code, with the durable panic flag already written; and _emergency_process_cleanup remains a separate path that returns instead of hard-exiting. Every destructive operation is neutralized in the harness — no real process, port, daemon or interpreter teardown runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…1, D07) Emergency Stop 2A. The panic sweep needs the port the server actually bound, and it used to get it by importing the server module back from inside server_control — a lazy back-edge from a host leaf to the composition root, present only so one integer could travel upward. execute_panic_stop now takes that integer as a keyword-only bound_port with a default of None, and server._execute_panic_stop passes _actual_bound_port(). The default install port 8765 stays the fallback in both of its roles: for a caller that has no bound port to give, and for a sweep that raises. The cleanup order, the fail-soft guards, the host-service sweep that follows, and os._exit with the panic exit code are unchanged, and _emergency_process_cleanup is deliberately NOT merged with panic — it finalizes tasks with an honest interrupted reason and returns, which is a different contract. The three existing callers that pass only the old kwargs (test_server_shutdown.py, test_server_control_panic_daemon.py, test_post_task_evolution.py) are untouched and still pass, which is what the default is for. The back-edge check is written as a class, not an instance: it scans every ouroboros/server_*.py leaf for an import of the server module at any depth, so a future lazy import inside a function cannot quietly restore it. D07 is this item's own id in the shared delta registry, which allocates one id per plan 4.3 item: D03 belongs to lane S1's settings normalization seam and D04 to the retired-knobs item, so reusing either here would make the ledger unreadable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(spec 4.3.5) The last start-time settings mutator. The server lifespan used to write the boot normalization back into settings.json whenever it changed and the file already existed — a guarded write, carried because the launcher had the same one, and kept correct only by that guard. With lane S1's read seam landed, no reader needs it: gateway/settings.py, onboarding_host.py and gateway/onboarding.py all re-derive the same normalization on read. So boot now APPLIES it to the process environment and writes nothing, which satisfies the fresh-install rule by construction instead of by a condition that has to be got right — on a host where the server starts BEFORE first-run onboarding, a boot write is the one thing that could author the first bytes of a file every fresh-install proof requires to be absent. Removed: the disclosure comment, the SETTINGS_PATH import and the guarded save_settings call. Kept, in the same order: the apply_runtime_provider_defaults(load_settings()) call, _apply_settings_to_env, then initialize_runtime_mode_baseline() — the baseline still pins against the values boot just applied. No seam call replaces the write. Two tests pinned the old guarded write by source text. Both now pin the contract: test_onboarding_host asserts on the lifespan's SYNTAX (no save_settings call, no SETTINGS_PATH import, and the apply -> env -> baseline order intact), so a comment that merely mentions save_settings can neither satisfy nor break it; test_server_runtime's paired assertion says the launcher and the server both persist nothing. The delta id is D03, the shared id for the spec 4.3.5 settings seam that S1 opened. The Emergency Stop 2A row in the previous commit moves to D07, its own spec 4.3.11 id, now that the registry is one id per plan item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fence
Characterization only (C1/C2) — no production change. The mint reads
`state/cancel_intents.json` strictly and refuses a malformed file with a typed
`CancelIntentProjectionCorrupt`; the five NON-minting mutators
(`mark_finalize_control_drained`, `mark_intent_scope`, `claim_intent`,
`release_claim`, `settle_intent`) read it softly, so they answer exactly what
they answer for a data root that never had an intent.
The tests state that indistinguishability as an equality between the corrupt
and the absent case, cover both corruption shapes (malformed container and a
malformed `intents` value under a valid container), and follow it to the
caller: `task_lifecycle._claim_intent` maps the soft answer onto its `{}`
"no active intent, legacy path" branch, which is the shape that drops the
claim-first exclusion custody relies on.
C2 pins the opposite side deliberately: the watchdog's enforcement read
(`active_intents(..., disclose_corruption=True)`) stays fail-soft-but-loud —
`log.error` plus a typed `projection_corrupt_refused` forensic row, zero tasks
fed into custody. That is reading-for-behaviour, not authoring-a-record, and
it is pinned so write-side strictness cannot be mistaken for a licence to
change it.
The mint already refused a corrupt `state/cancel_intents.json` with a typed
`CancelIntentProjectionCorrupt`. The five non-minting mutators read it softly,
so a projection nobody could read answered exactly like a projection nobody
had written into: `claim_intent` returned None, custody's `_claim_intent`
mapped that onto its `{}` "no active intent, legacy path" branch, and the
teardown ran without the claim-first exclusion that exists to stop two
custodies from settling the same task twice. The later settle then no-opped.
Fixed as the class it is, on both levels the mint already checks: the five
mutators pass `strict_existing_dict=True` AND `_load_intents(strict=True)`, so
a malformed container and a malformed `intents` value under a valid envelope
are corruption for all of them. The refusal is one shared helper
(`_refuse_corrupt`) that the mint now uses too, so every refusal appends the
same `projection_corrupt_refused` forensic row naming the operation refused.
Every existing caller already has the right answer for a raised mutation, and
none of them crashes a live path:
- `task_lifecycle._claim_intent:849-851` documents a raised claim as "cannot
tell whether a live owner exists, treat as refused" -> custody exits
`failed` having touched nothing;
- `_settle_intent:883`, `_release_intent_claim:903`, the cascade
postcondition's settle (`:477`) and `_record_cascade_scope:509` catch and
log, leaving the intent OPEN for the watchdog;
- `workers._drop_cancelled_pending:2165` maps a raised claim to
`claim_refused` (task leaves the queue, intent stays), `:2200`/`:2222` log;
- `task_results.fail_tasks:1286` does the same, `:1306`/`:1327` log;
- `loop._mark_owner_stop_control_drained:3266` catches everything, so the
round loop is untouched; the drain stamp now discloses through the typed
forensic row instead of the two-attempt `owner_stop_stamp_failed` row.
Reading for BEHAVIOUR is deliberately unchanged: `active_intents` (and the
watchdog's `disclose_corruption=True` enforcement read) still fails soft and
loud, pinned by C2 so this commit cannot be read as licence to harden it.
MIGRATION_v7.md: six D03 rows (the five mutators + `_load_intents`) and one
disclosure row for `_SCHEMA_VERSION` — written on every envelope, dispatched
on by nobody. `scripts/v7_migration.py` approves D03; the ledger test carries
the S6 block. ARCHITECTURE/DEVELOPMENT state the mutator-vs-reader rule.
Characterization only (C3/C4) — no production change.
`state/subagent_worktrees.json` is the third durable registry in the family and
the only one that answers "malformed" with "empty":
- `_load_registry` returns `[]` for every malformed shape, so a live snapshot
reads as missing (`find_execution_snapshot` -> None) while its checkout and
its `refs/ouroboros/delegated/*` baseline ref are both on disk;
- the startup GC reports `{"removed": [], "kept": []}` — indistinguishable
from a genuinely clean sweep, not from "unknowable";
- `prune_orphans` then REPLACES the malformed bytes with a valid empty
registry, and so does the next `provision_execution_snapshot` (it reads,
appends, writes back), which is the same {}-collapse the cancel-intent and
terminal-delivery registries refuse. After it the checkout and the pinned
ref are stranded with nothing naming them and no recovery material left.
C4 pins the write half: when the registry write fails, the Git branch leaves
both the checkout and the baseline ref behind, while the payload sibling
already removes its directory (test_delegated_skill_payload.py:881).
O2. `_load_registry` now separates ABSENT (an ordinary empty registry — the first-write case) from MALFORMED (a fact of its own). Everything that authors a record or acts destructively on one reads with `strict=True` and gets a typed `SubagentWorktreeRegistryCorrupt`: both provisioning branches, both removals, the two prunes, and `find_execution_snapshot` (a false "snapshot missing" sent a delegate retry off to provision a replacement for a binding that was still on disk). The bytes are kept and one `subagent_worktree_registry_corrupt` event names the refused operation. The inspection listing (`list_worktrees`) stays soft: it displays what it can and destroys nothing — the same reader-vs-author split the cancel-intent projection draws. The two failures this closes are collapse-to-empty, not read errors: `prune_orphans` wrote its `kept` list over the malformed bytes, and `provision_*` read-appends-writes, so one new row replaced every unreadable one. After either, the checkout on disk and the `refs/ouroboros/delegated/*` ref pinning its baseline had nothing left naming them, while `prune_execution_snapshots` reported an empty removed/kept sweep that reads as clean. Refusing a destructive sweep over an unknowable keep-set is what the same GC already does when the custody log is unreadable (`server.py`). O3. The Git branch's registration moves inside a cleanup scope, like the payload branch: a failed registry write removes the checkout AND deletes the baseline ref it pinned, instead of leaving a snapshot nothing can name plus a ref holding its commit against git's own GC. No live path crashes: every production caller already guards. `server.py`'s startup prunes (`:1140`, `:1958`) and `events.py:2551` catch and log — the operation is skipped, which is the fail-closed answer; the delegate paths (`delegate_integration._provision_snapshot`, `delegate.py:1055`, `subagent_integration._dispose_delegated`) either wrap the call already or surface through the tool registry's typed `TOOL_ERROR` envelope. MIGRATION_v7.md: nine D03 rows plus a disclosure row for `_KIND_DELEGATED_EXEC` — this registry carries no `schema_version` and its two row shapes are told apart only by that string (the acting-worktree row has no `kind` at all). Size ratchet: the module enters the 1001-1500 band with its rationale.
…o fix The hypothesis was that `owner_stop._settle_descendants_hard` calls the shared subtree sweep with no cascade token, so the fences it plants join no protected set and an unrelated cascade's `_prune_cancellation_fences` can evict them mid-episode. The test does not reproduce it, and the reason matters: - eviction requires the entry to be older than the recency GRACE window (300s); the owner-stop fences are planted a moment before, so a concurrent prune skips them exactly as it skips a just-completed cascade's root; - the sweep plants every id and calls the prune inside ONE hold of the queue lock, so a racing prune sees either no fence of that tree or all of them (the test runs a real second thread to check, and tolerates only the root-only snapshot the two-phase entry legitimately produces); - a long episode keeps refusing late admission because each hold tick RE-STAMPS the root's fence, and a task scheduled under that root matches the root entry directly (`root_task_id`, or the first step of the ancestry walk). So a cascade token would not change the outcome either: its life is the sweep call, a window in which the fences are already young. The aged-descendant case IS evictable — pinned in its own test — but it needs a >300s episode, a registry over the 4096 cap, a concurrent cascade pruning in that window, and a new task whose `root_task_id` is not the stopped root. Reported as a disclosed residual rather than patched: a reviewer finding is a hypothesis, and this one did not survive its own test. Test-only; no production change, no migration row.
`tools/delegate.py` states that the operator's Claudexor daemon control token never leaves `gateways/claudexor.py`. Loopback-only, never-returned and absent-from-the-refusal-text were pinned; the headline itself was not. This drives ONE delegated run (start + wait) through the REAL gateway — a live token in the Authorization header, `httpx.MockTransport` underneath — and then greps every surface the run produced: the POST /v2/runs body including `instructions` and `prompt`, every other request body of the run, every row of the durable `logs/events.jsonl` (the `delegate_run_start_requested` row stores the canonical body for replay, so a leak there is permanent), every file staged under `delegated_runs/` on the task drive, the two verb payloads, and finally every file anywhere under the task drive. Two guards keep the result honest. The first test asserts the token IS on the wire in the one place it belongs, so a fixture that quietly carried no token cannot make the rest pass for the wrong reason. The last test is a negative control: with the token deliberately appended to `instructions`, the same three greps go red — on the wire, in the replay row, and in the staged artifact. The payload is deliberately larger than the tool-result budget so the terminal detail is STAGED rather than delivered inline; otherwise the artifact grep would have nothing to read. Test-only; zero production change.
Convention update from the integrator: semantic delta ids are a shared registry, one id per plan §4.3 item, and §4.3.13 (cancellation/delegation fail-closed registries) is D08. The fifteen S6 rows move off the provisional D03 (which belongs to §4.3.5) and `APPROVED_SEMANTIC_DELTAS` releases it, so each lane adds only its own id and the sets merge at integration. Row text unchanged, and deliberately free of the word "verbatim": nothing in this lane moved byte-for-byte — every row is the same identity with a stated behaviour delta, or a disclosure with no behaviour change at all.
…owners
Four properties the protocol relies on that nothing asserted structurally, so a
relocation could grow or move one of them silently. Every enumeration walks
`ouroboros/` and `supervisor/` by SYMBOL through the AST, never by a hard-coded
module list, so an extraction moves rows inside the checked-in manifests instead
of hiding a call site from them.
C7 — the 36 call sites that can write a terminal status (constant or dynamic;
the dynamic ones count because the reducer, not the caller, decides). A new
writer fails with "add a manifest row or route through an existing owner"; a
moved one fails naming the row to retarget. Plus the property the manifest
guards: one `write_task_result`, with the lock and the monotonic reducer inside
it.
C8 — `settle_intent` has exactly four callers and exactly ONE passes
`allow_cascade_scope=True` (the cascade postcondition, which owes the tree's
summary before it settles). The atomic scope refusal inside the locked mutate is
pinned beside the count, because the count is a guard and the mutate is the
mechanism.
C9 — owed-before-settle restated as the two contracts that exist: the natural
path owes BEFORE the durable write, and the cancel lanes write, owe, then SETTLE
the intent. Checked per settle CALL and only for calls publishing a real
cancellation, so the `not_found` and `already_settled` branches are not forced to
invent a deliverable. The uniform registration-failure rule is pinned to its one
shared helper, and the nine lanes that deliberately owe nothing are a checked-in
list with the reason each has nothing to deliver.
C10 — the split-drive root. The agent tool's `_status_drive_root` resolves to the
CANONICAL root in both live shapes, so S5's suspected divergence is not one: the
first term is `budget_drive_root` (which production always sets to the canonical
root in the same block that points `drive_root` at a child drive — asserted), and
the third fallback is reached only when `drive_root` IS canonical. The HTTP and
project-deletion ingresses mint at `app.state.drive_root`, bound from the same
`DATA_DIR` the queue's `DRIVE_ROOT` gets. One real divergence is pinned as the
benign noise it is: the mint's already-settled probe reads the task result at the
INTENT root, so a split-drive child that already completed still gets an intent,
which custody settles `already_settled` one round later.
Also fixes the stale docstring at `ouroboros/task_finalization.py:96` ("Called
right after durable result persistence" — the natural-path call site is BEFORE),
with its disclosure row.
The one place a cancellation settles without having proved the owner was told anything. A cascade root with no lineage chat has nothing to send, so the delivery seam records a typed `terminal_delivery_handoff` row instead — "consciously not owed", not silently dropped. But `deliver_cascade_summary` starts from `owed = True` and only overwrites it when an EVENT exists, so on the handoff lane it reports "owed" whether or not that row landed; if the append also fails, nothing records the tree's outcome and the postcondition settles the root's cascade intent anyway. Four tests: the healthy chat-less lane (handoff row written, reported owed), the residual itself (append made to fail — nothing recorded, still reported owed), the postcondition's dependence on exactly that return value (`is not False` -> settle, with the honest not-owed branch beside it), and a with-chat control proving the normal lane answers with a REGISTERED row rather than a default. Not fixed: this is upstream's own disclosed phase-A residual (`docs/ARCHITECTURE.md`), it needs two rare failures at once, and the owner's answer (batch 6, 5=A) is to pin it in v7 and raise it upstream. The pin exists so a refactor cannot flip it in either direction by accident.
…ners The C7/C8/C9 structural inventories were written against the pre-S3 paths and predicted exactly this retarget: after the supervisor split, the custody lanes (`cancel_task_custody`, `_finish_captured_*`, `_finalize_cancel_intent_on_miss`, `_settle_intent`) live in `supervisor/cancel_custody.py` and the task_done / promote / schedule writers in their `events_*` family owners, while `cancel_task_by_id` (with the exclusive cascade postcondition) deliberately stayed in `supervisor/task_lifecycle.py` beside `_cancel_subtree_sweep`. Manifest rows only; every reason string and assertion unchanged.
…erry-pick union had resurrected the stale verbatim wording)
tests/test_delegated_subagent_transport.py was 6174 lines — the largest test module in the repo — covering a dozen unrelated questions about delegated runs. Its banner sections move verbatim into siblings named for the question they answer: - tests/test_delegated_executor_axis.py the harness setting, the rule table, the dispatch behind them - tests/test_delegated_run_profile.py the access profile a run may hold and the guards on it - tests/test_delegated_run_accounting.py what a run costs and when the cost becomes durable - tests/test_delegated_run_containment.py the delegated marker and the boundary it must deliver - tests/test_delegated_run_custody.py custody rows that outlive the worker that wrote them - tests/test_delegated_cancellation_settlement.py cancel and settle claim only what they verified - tests/test_delegated_result_delivery.py a large result delivered whole or declared partial - tests/test_delegated_reconciliation.py restart sweeps and parent terminalization - tests/test_delegated_wait_window.py the wait window, its clamp, its bounds, its stream - tests/test_delegated_wait_timeline.py the advance list and the rolling timeline - tests/test_delegated_subagent_transport.py the daemon transport itself and its failure class The autouse gateway fixture and the stubs/context builders used by more than one theme live once in tests/_delegated_transport_shared.py and are imported where the original module defined them; no helper is copied. The parent keeps re-exporting the two names it still uses. Every moved block is byte-identical to its source (173 top-level symbols proved by AST source-segment comparison) and the suite still collects 206 tests. MIGRATION_v7.md gains a row per relocated symbol — moved tests, fixtures and stubs are owned by their new module, test-private import bindings keep their canonical production provider — tests/test_v7_migration_ledger.py registers them in the S7a block, and the size ratchet drops the module from GIANT_PATHS, MODULE_DEBT_1500 and BYTE_DEBT.
tests/test_runtime_mode_elevation.py was 2141 lines whose banner sections answered six unrelated questions about the owner-only runtime mode. They move verbatim into siblings named for the surface they cover: - tests/test_runtime_mode_data_write.py the _data_write/_data_read fence under the drive - tests/test_runtime_mode_owner_endpoints.py the settings API body and the owner endpoints - tests/test_runtime_mode_authorship.py who may author a mode decision - tests/test_runtime_mode_launcher_bridges.py the launcher confirmation bridges - tests/test_runtime_mode_write_guards.py the deterministic command/write guards - tests/test_runtime_mode_elevation.py the save_settings chokepoint itself The isolation fixture, the disk seeder and the drive-context builder are used by several of them, so they live once in tests/_runtime_mode_elevation_shared.py. `isolated_settings` is requested by name as a test parameter, so each consumer re-binds the imported fixture through a module attribute: a direct import of a name that reappears as a parameter is an F811 redefinition under the CI ruff gate, and no test signature may change. Every moved block is byte-identical to its source (71 top-level symbols proved by AST source-segment comparison) and the suite still collects 110 tests. MIGRATION_v7.md, the S7a ledger block and the size ratchet follow the move; the module leaves GIANT_PATHS and MODULE_DEBT_1500.
tests/test_claudexor_owned_daemon.py was 2110 lines answering five different questions, only two of which were about the owned daemon. Its banner sections move verbatim into siblings named for the surface they cover: - tests/test_claudexor_login_accounts.py manifest auth blocks, vouching, account removal - tests/test_claudexor_login_jobs.py the no-terminal login jobs and the input proxy - tests/test_claudexor_executor_frame.py the executor fact that reaches the chat frame - tests/test_claudexor_status_payload.py the status fan-out and the wake endpoint - tests/test_claudexor_owned_daemon.py the isolation root, discovery and lifecycle Every helper had a single theme's consumers, so each travels with its cases and no shared module is needed. Every moved block is byte-identical to its source (42 top-level symbols proved by AST source-segment comparison) and the suite still collects 66 tests. docs/ARCHITECTURE.md names the executor-chip row's pin, so that cell now points at tests/test_claudexor_executor_frame.py where the assembler, allowlist and contract-mirror cases live. MIGRATION_v7.md, the S7a ledger block and the size ratchet follow the move; the module leaves GIANT_PATHS and MODULE_DEBT_1500.
tests/test_delivery_forced_finalization.py was 1889 lines. Its banner sections and the unbannered rails above them move verbatim into siblings named for the question they answer: - tests/test_delivery_forced_suffix_binding.py the child suffix and evidence bound into the candidate - tests/test_delivery_forced_owner_refresh.py the owner arrival refresh and what supersedes a pass - tests/test_delivery_control_latch.py resolving the armed delivery-control latch - tests/test_delivery_forced_absorption_acceptance.py the children_unabsorbed acceptance rail - tests/test_delivery_forced_acceptance_bypass.py the typed acceptance-bypass ledger records - tests/test_delivery_forced_finalization.py what a hard exit preserves and discloses The two context builders used by more than one theme live in tests/_delivery_forced_shared.py; `_forced_test_context` keeps its old identity in the parent because four unrelated suites import it from there (test_v671_acceptance_convergence, test_v678_acceptance_state, test_owner_stop_s3, test_owner_hurry_s3), so no consumer is retargeted. The parent, which had no module docstring, gains one naming what it now owns. Every moved block is byte-identical to its source (39 top-level symbols proved by AST source-segment comparison) and the suite still collects 43 tests. MIGRATION_v7.md, the S7a ledger block and the size ratchet follow the move; the module leaves GIANT_PATHS and MODULE_DEBT_1500.
tests/test_promote_chat_flow.py was 1811 lines covering promotion, routing, binding, steering and provisioning under one name. Its blocks move verbatim into siblings named for the question they answer: - tests/test_project_chat_routing.py what a project chat sees and how a message reaches its task - tests/test_project_task_binding.py binding a task to a project and the events that follow - tests/test_chat_steering.py choosing a steer target and delivering to it once - tests/test_promote_workspace_provisioning.py the genesis workspace a file-less promotion provisions - tests/test_promote_chat_flow.py the promotion event, its project, and the enqueued task The autouse projects-root isolation fixture is applied by all five, so it lives once in tests/_promote_chat_shared.py and the parent keeps re-exporting it. Every moved block is byte-identical to its source (43 top-level symbols proved by AST source-segment comparison) and the suite still collects 63 tests. MIGRATION_v7.md, the S7a ledger block and the size ratchet follow the move; the module leaves GIANT_PATHS and MODULE_DEBT_1500.
Records what each of the seven conflicts was resolved to and why, the declaration-by-declaration verbatim proof for the seven re-homed llm hunks, how the two settings-seam designs compose rather than replace each other, why the Windows runtime cluster was the one that left launcher.py, and the three upstream assertions adapted at the test. Operator artifact; it leaves the product tree at integration. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The ledger had no rows for the three methods the upstream cutoff ADDED to LLMClient and this merge re-homed: _new_remote_client and probe_provider_readiness into _ProviderRoutingMixin, _new_gigachat_client into _GigaChatLaneMixin. The migration gates were green because nothing demanded them, which is precisely the hole — three real moves outside the exact inventory, free to drift or vanish unnoticed. Three rows join the other seven llm re-homes, the same names join llm_mixin_symbols_by_owner (what buckets a row as implemented) and _MIXIN_OWNERS (what the extraction test enforces), and the composed-member digest moves once, with the three additions and the reason recorded in the test's own docstring so the next reader can tell a sanctioned move from an unexplained one. The rest is honesty, resolved against both parents rather than by growing code on a frozen sync: The generic save merges a full snapshot instead of re-merging onto a document read under the file lock. That is inherited, not introduced: upstream 8028f1d reads, merges and writes a snapshot with no re-read anywhere, and v7's _owner_write_settings has been `lambda _current: settings` — reading the fresh document and deliberately keeping the caller's — byte-identically at 45cf457 and here. Nothing load-bearing was dropped; what protects the generic save from the other gateway writers is that its read and write are one transaction under the document lock. A new test pins exactly that as a lost-update race, and fails when the lock is neutered. The claim that lock order is document then file "at every call site" overclaimed a closed set that does not exist. It is rewritten to name the real one — five dedicated endpoints, the generic save, onboarding — and the writers outside it are listed as a disclosed residual: control_runtime's timeout setter and the launcher's saver take the file lock only, both untouched by this sync, and the launcher is a separate process an in-process lock could never serialize. Onboarding's blocking read before its thread hop is disclosed the same way: the whole api_onboarding_complete body is byte-identical on both parents and here, so the placement is upstream's, not this union's. And the note claiming llm_probe.py changed by one line is replaced by its actual delta: one executable statement plus two pieces of prose that would otherwise have named the wrong owner. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
An operator artifact lives with the campaign records; a durable copy is archived outside the tree. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The mocked pip timeout always takes the branch that logs through git_ops.DRIVE_ROOT, and this was the one caller of that logger with no root binding of its own -- one process-global drift away from appending test events to the LIVE supervisor log, which full-battery serial runs did nondeterministically. Bound to tmp_path like every other caller; the branch now provably writes inside the test root. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The spec baseline names git_ops.DRIVE_ROOT and message_bus.DATA_DIR as the module-global roots missing from _bind_pytest_runtime_roots; the per-test binding that stopped the live supervisor-log leak was the instance fix, this is the class. The ledger row for the relocated dependency-sync case now states its deliberate divergence, the migration base note names the frozen cutoff 8028f1d, and the settings-seam sentence in ARCHITECTURE names the closed set and the two disclosed pre-seam writers instead of overclaiming every writer. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
…root The session binder starts every root global on the pytest temp root, but a test that rebinds one without restoring it poisons every later test in its worker -- the observed symptom was a correctly-written victim test appending to the LIVE supervisor log while the poisoner stayed invisible. The autouse invariant now checks the four root globals after every test and names the poisoning test itself. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Its teardown deleted the session's own OUROBOROS_* env values before the restore reload, so config came back on the pre-pytest LIVE default and every later test in the worker inherited the poisoned root — the source of the nondeterministic live supervisor-log appends the new conftest invariant flagged across all nine cases of this suite. monkeypatch.undo first, reload second: the module lands back on the pytest root. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
A synthesis-phase census of the frozen tree checked every entry in the ARCHITECTURE module tree against the real path it names, and every module-qualified symbol reference in ARCHITECTURE/DEVELOPMENT against the module that actually defines or re-exports it. Seven statements no longer matched the tree they describe: - three tools/ leaves (delegate_terminal, delegate_payload_patch, subagent_integration_delegated) sat at the ouroboros/ indentation level, so the tree asserted a package none of them live in; - review_session_verdict.py carried a two-space indent, the only entry in the block that read as a repo-root file rather than an ouroboros/ member; - launcher_windows_runtime.py was named nowhere in docs/, the one module of the 132 the campaign added that no doc mentioned; - delegate_evidence.py was described as re-exported from delegate_custody.py wholesale, which is true only of task_execution_evidence — the two stamp writers are imported from the leaf directly; - loop._resolve_forced_delivery_control and llm.py::_chat_gigachat named modules that no longer carry those names after the L-B and L1 splits; - review_synthesis.py was credited with disposition validation, which moved to plan_spec.py in full; - normalize_runtime_mode was credited with an onboarding sharer that never clamps: onboarding refuses an out-of-enum value instead. Two settings claims were also overstated rather than stale. The document lock names two out-of-seam writers where the code has three (the packaged bootstrap writer says so about itself), and the thread-hop rule is not universal: onboarding completion deliberately reads before the hop so the locked precondition can see an interleaving at all. Documentation only; no runtime behaviour is touched. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The census named two substrates the campaign had no acceptance artifact for: which module owns each durable path, and who actually consumes each retained compatibility facade. Both were answerable only by reading every writer and every call site, so both are now written down. docs/PERSISTENCE_OWNERS.md walks the data root path by path -- settings, the state plane, locks, skill state and payloads, the Claudexor home and the update transaction, logs, observability, services, archive, uploads, memory, the project facts store, task results and artifacts, the swarm scratch plane, and the three roots outside data/. Each row names the writer(s), the reader that branches on the content, and who prunes it, derived from the path constructor and the actual write call rather than from the ARCHITECTURE tree. Two indexes fall out of that: every path with more than one independent writer, and every path with no pruning mechanism at all. The settings section stays consistent with the seam disclosure the D03 rows and the read-seam pin already carry rather than deriving a second writer list. docs/FACADE_CONSUMERS.md classifies all 1837 bindings the 42 retained facades re-export, by AST over the product tree, the peripheral tree and the suite -- resolving the call-time parent handles so a leaf reading _loop().X counts as a consumer. Each facade gets its runtime callers, its monkeypatch surface, and the bindings nothing reads at all, beside the identity pin that already governs its name list. The pin lists are cited, never restated. Deriving them surfaced divergences between the ARCHITECTURE data layout and the code: paths the tree documents that nothing produces, planes the code produces that the tree omits, and glosses that name the wrong owner. Under code freeze these are recorded in the map's last section rather than fixed. Documentation only; no runtime behaviour is touched. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Re-checking the derived numbers against the tree caught four statements that were true in spirit and wrong in detail, all in the two new documents: - the loop facade's runtime consumers are nine of its eleven leaves, not all of them: loop_llm_call and loop_tool_execution read nothing back through the parent, and saying "its nine leaves" mis-stated the family size; - tools/control.py has seven control_* leaves contributing bindings, not six; - the tool registry's public/private line does not fall where the underscore does. Three private bindings have live runtime consumers (notably _authorized_managed_update_resolver, read from the facade by four review and edit modules), while the public BrowserState is read only by tests, so the paragraph now splits on measured consumers instead; - config.py was credited with ten private bindings as its whole compatibility surface, but thirteen of its bindings have no reader, and not all of those are private. The settings section also dropped an unsupported superlative: settings.json is the file with two writer-set pins, not the most-written file in the tree. Documentation only; no runtime behaviour is touched. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The audit was right that the invariant's docstring claimed more than its four checked globals: the settings path and the state/queue drive roots are rebound by the same session binder and were unchecked. All seven now sit in the table, so the guard's coverage matches its claim. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
The census answers "which files got smaller". Nothing answered the inverse: for each thing the system does, who owns it now, how you get in, and what proves it did not move silently. docs/ARCHITECTURE.md walks the directory tree file by file, which is the wrong index for that question. docs/DOMAIN_MAP.md is that index. Nineteen domains, every one carrying its owning modules (facade named beside its leaves), its entry points, the suites that pin it, and one sentence on what v7 did to it. Assignment is by behaviour, not directory -- supervisor/evolution_lifecycle.py sits in the self-evolution domain and tools/followup.py in the supervisor one -- and it is mechanically complete: all 411 tracked runtime modules land in exactly one domain, none twice, and all 132 the campaign added carry a mark. Rows cite ARCHITECTURE, FACADE_CONSUMERS, PERSISTENCE_OWNERS and the migration ledger rather than restating them. Three domains have a zero runtime delta and are named as such rather than left to be inferred: safety/guards, memory/consciousness/evolution, and the frozen contracts (11 modules before and after, +5 lines across the package). Deriving it surfaced two coverage facts the module tree does not show: eighteen pre-existing runtime modules are named nowhere in ARCHITECTURE.md, and fourteen campaign-added leaves (the control_*, git_* and shell_* families) live in its prose but not in its tree. Both are standing curation choices, not v7 staleness; under code freeze they are recorded in the map's coverage section, and every one of the thirty-two now has an owner row. Documentation only; no runtime behaviour is touched. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
…d 0" 435 -> 664 test files is the kind of number that invites the question the census left open: where did each one come from, and did anything quietly disappear. docs/TEST_DISPOSITION.md answers it per file, mechanically. Three exclusive buckets over the 298 files added under tests/ and web/tests/: 180 split from a named giant (the added path is a destination of a ledger row whose source is a test file -- MIGRATION_v7.md is the authority, the split inventories in tests/_v7_ledger_inventories.py carry the same maps as data), 93 new coverage for a named campaign change, and 25 adopted with upstream (added by a commit reachable from the PR razzant#257 sync base but not from the pre-v7 reference). No file falls in two buckets. The (b) bucket is broken down further into owner-split pins, the typed tool-result cutover, the provider-route goldens, the S6 characterizations that deliberately pin a residual without fixing it, the ledger/evidence gates, and new behaviour. The census's "deleted 0" is wrong by one, and the correction matters for provenance rather than for hygiene: tests/test_planning_swarm_adaptive_wait.py was deleted by d6210b1, upstream's own plan-review spec-gate redesign, and arrived here through the adoption merge. No campaign wave deleted a test file; where a v7 split retired a test-side symbol the ledger row carries a retired: destination and the file survives. Per-wave counts close on 298 against the campaign's own commit-subject labels. No giant was replaced by its split -- every source still exists and still holds its residue, the largest going 6178 -> 366 lines under the same name. Documentation only; no runtime behaviour is touched. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
…the key bytes land The E2E harness wrote the isolated server's settings.json - which carries a live provider key on the paid lane - with a default-umask write_text, leaving the key world-readable on a shared host (observed live as mode 664). The file is now opened O_CREAT 0600 with an unconditional fchmod (O_CREAT's mode only applies on creation), and a lane-free contract test pins both the create and the rewrite path.
- DOMAIN_MAP: D13/D15/D19 claimed 'zero runtime delta'; each in fact carries a small approved line-level delta (safety.py +48/-6 host-root resolver + accounting sink + schedule_followup POLICY_SKIP, runtime_mode_policy.py +23/-2 family widening; reflection/consolidator/consciousness +25/-14; task_contract.py +6/-1). The claims now separate 'no structural delta' (true) from the disclosed line-level deltas. - ARCHITECTURE/DEVELOPMENT: the budget-drain fail_tasks settle site is documented as if live, but it has no production caller anywhere in history - budget exhaustion pauses work before dispatch (budget_scope_paused) rather than draining it. Both docs now state the site is test-pinned against future wiring, not exercised.
…, not the caller's env The fail-closed root invariant derived its guarded root from OUROBOROS_DATA_DIR first, so the mandatory four-variable isolated battery (OUROBOROS_DATA_DIR= $TMP/data) aimed the guard at its own temp root while ~/ouro/data went unwatched - blind to exactly the poisoner class it exists to name. The guard now defends the UNION of the env-independent canonical home root (resolved ~/Ouroboros/data, same semantics as supervisor.state.assert_test_data_path) and any caller-provided roots. Mutation-probed: a test binding config.DATA_DIR to the live root under the four-variable invocation is now named; former poisoner/victim/env-override suites stay green (48 passed).
…alue The harness DOES persist the key - into the isolated server's own 0600 settings.json, the only place the server can read credentials from. Say so instead of denying it; the never-prints and never-touches-live claims stay.
…survive non-path globals Two defects in the invariant found by its first full battery: (1) as an autouse fixture its teardown ran BEFORE the test's own monkeypatch.undo, flagging three properly-restored live-fuse tests as poisoners; moved into the existing pytest_runtest_teardown hook wrapper, after fixture finalization. The first attempt at that move silently did nothing - conftest already defined pytest_runtest_teardown 400 lines below, and the later definition shadowed the new one - so the check is a helper called from the ONE existing hook. (2) a test that injects a non-pathlike SETTINGS_PATH stand-in crashed the check inside pathlib; non-fspath values are skipped (they cannot name a live root). Mutation probe still caught: an unrestored config.DATA_DIR bound to the live root errors at the poisoning test's teardown; the three fuse tests and the poisoner/victim regression set are green (103 passed).
…, one stale script line)
Panel seat gpt-5.6-sol (xhigh) returned NEEDS FIXES with 7 findings; 6 accepted
and fixed here, 1 rejected with evidence (the S-stream per-leaf logger namespace
is already a disclosed residual at MIGRATION_v7.md:74-77, not an unledgered
delta):
- PERSISTENCE_OWNERS: observability blobs DO have a runtime behavioural reader
(latest_llm_response_text dereferences full_payload_ref for the terminal
salvage chain); 'the only age-based GC' was false - all five age-pruned
planes are now listed (service logs, consumed one-shot receipts, headless
task drives, task drives, terminal-root task trees).
- ARCHITECTURE endpoint index: added the missing POST /api/tasks/{id}/hurry
and PATCH /api/claudexor/credential-profiles/{harness}/{profile_id} rows.
- FACADE_CONSUMERS: 'nothing imports server.py at runtime' narrowed - cli.py
imports it to call main() but reads none of the retained bindings.
- TEST_DISPOSITION: 32 base-tree giants was arithmetic drift - 33, naming the
web giant (harness_accounts.test.js, 1882 lines at base).
- scripts/v7_migration.py: dropped the stale duplicate line crediting D35 with
D38's agent/usage handles.
Gates: test_docs_sync + test_gateway_parity + test_v7_prologue_evidence = 56
passed; ruff F clean.
…le panel finding)
The QUEUE_SNAPSHOT_PATH shadow collapse (supervisor.state as sole owner) is
harness-observable - an isolation harness must call state.init; queue.init
alone no longer redirects the snapshot - yet its retired row carried
{"id":"none"}, which the ledger reserves for observable-identical moves. The
row now carries D18 (the queue single-authority mechanism), the registration
map in the inventory test records it with the rationale, and the two
queue_snapshot rows whose 'body otherwise unchanged' claim hid the
_state.QUEUE_SNAPSHOT_PATH use-time read now name it and point at the
authority row. Production is invariant (server startup and worker boot always
bind state first). Plus the usage_attempts sole-writer row now cross-refs the
legacy-import caller plane. Gates: test_v7_migration_ledger + test_docs_sync
= 7 passed.
The round-1 fix replaced one false absolute with another: subagent worktrees (subagent_worktrees.py::prune_orphans via _startup_worktree_prune) are the sixth plane cut by the same age_cutoff - already described by this document's own worktree rows. The count now comes from the exhaustive production age_cutoff call-site list, and the bullet notes the worktree root lives beside the data root.
VERSION 6.105.1 -> 7.0.0, propagated by the canonical writer (release_sync.sync_release_metadata): pyproject.toml, uv.lock, web/package.json, GATEWAY_CONTRACT_VERSION, README badge + download URLs, site/docs install pages, ARCHITECTURE header. Version History gains the 7.0.0 major row (P9 limits hold: 1 major / 5 minor / 5 patch rows). version_carrier_desyncs = [], check_history_limit = []; gates test_smoke + test_update_carriers + test_gateway_parity + test_docs_sync all green. Release assets for the new download URLs appear when the owner tags and builds - tagging and promotion stay the owner's step.
…te the bytes os.open without O_BINARY opens the fd in CRT text mode on Windows, so os.write silently rewrites \n as \r\n. Latent upstream behavior, exposed by the v7 frozen tool manifest: build_frozen_tool_manifest writes canonical bytes through this branch and its own byte-exact verify then refuses the file it just wrote - failing 15 tests AND the Windows build itself (Ouroboros.spec builds the manifest through the same code). getattr(os, 'O_BINARY', 0) is a POSIX no-op. The non-fsync branch and the append-fd sites share the latent translation but feed only newline-tolerant readers; left untouched under the acceptance freeze and disclosed here.
… macOS worker kill) First-ever Windows/macOS run of the campaign suites; every failure classified and fixed at its root, runtime untouched: - fixtures_e2e_cancellation: fchmod is POSIX-only (hasattr guard + chmod fallback); the 0600-mode contract test is skipif non-POSIX (mode bits are advisory on Windows). - external_review/cancel_protocol probes: compare canonical resolved paths, not case/short-name-sensitive strings; Path-compare pinned roots. - core_native_results/registry_core: expectations built with os.sep / as_posix / mirrored !r instead of assuming POSIX spellings; the directory-write refusal accepts Windows' PermissionError beside IsADirectoryError. - consciousness: read tools.jsonl as utf-8 (cp1252 mojibake on the snowman). - launcher_server_reaper: normpath'd fixture paths; the ps/getuid enumeration test is POSIX-only by construction. - update_carriers: write the ARCHITECTURE fixture as utf-8 (em dash vs cp1252). - shell_run_shell: signal 9's honest name is SIG9 where SIGKILL is absent. - owner_stop_fences: compare against the planted stale stamp - Windows' ~15ms clock tick can mint the identical 'now' twice. - v7_evidence/v7_migration (scripts): the tracked-path JSON rides stdin, not argv (Windows' 32767-char command-line cap = the real WinError 206); child pythons get SystemRoot/TEMP/TMP forwarded or they cannot boot on Windows. - conftest: test_v7_migration_ledger joins the serial lane - its ~4min wall-clock trips the parallel pass's 300s thread timeout on slower runners and kills the whole xdist worker (measured: 72MB peak RSS, not memory). Linux validation: 628 passed / 12 skipped over the 24 affected+mandatory files; ledger serial 1 passed (229s); ruff F clean.
- v7_evidence runtime probe: ntpath.expanduser ignores HOME entirely, so the probe child dies in Path.home() without USERPROFILE - set it beside HOME (POSIX ignores it). And a bare CalledProcessError from a CI runner is undiagnosable: the probe now re-raises with the child's stderr tail (the first Windows cycle was blind exactly here). - registry light-redirect route pin: the refusal CONTRACT (status/code/text/ meta) passed on Windows; only the adapter-count route pin failed. On Windows pytest's 8.3 short-form tmp_path makes the access-layer detector's resolve/relative_to miss, and the resolution-layer detector answers with a native ToolResult (0 adapter calls) - the second line of defense, same product outcome. The count expectation is now platform-honest with the mechanism documented.
…e pin The surfaced stderr (previous commit) named the real probe killer: the child prints canonical JSON to Windows' default cp1252 stdout and dies on the first non-cp1252 character - both ends now pinned utf-8 (PYTHONIOENCODING for the child, encoding= for the parent's decode). And the registry route pin was over-generalized: only the PATH-based user_files scenario switches to the native resolution-layer route on Windows; the cognitive scenario detects by args and keeps the adapter route on every OS - the expectation is now per-scenario.
…venance is The committed prologue evidence regenerates byte-exact on Linux AND macOS CI; on Windows the baseline runtime probe reports platform-sensitive facts (runtime_probe payload differs), which was never part of the artifact's provenance - the evidence was authored on POSIX. skipif(non-POSIX) with the observed delta named; Windows portability of the probe is a post-7.0.0 backlog item, not this pin's contract.
…inding) Without fchmod the live key bytes landed BEFORE the best-effort chmod, and Windows' chmod does not produce an owner-only DACL - a secret the harness cannot protect. paid_model_and_key() now fail-closed skips the paid lane off POSIX; only the mock lane's stub value can reach the chmod fallback, and the scenarios docstring's 0600-before-key-bytes claim is true again everywhere the lane runs.
With the real clock, Windows' ~15ms tick stamps several of the six recorded pairs with the IDENTICAL observed_at; 'newest' becomes ambiguous for retention and the 1.5-wins assertion flakes (first seen matrix round 5 - the test had passed rounds 1-4, i.e. a genuine tick-race, same class as the owner_stop_fences fix). Each record now gets its own monkeypatched utc_now_iso, one second apart - deterministic on every OS.
…d child) - review_binary_context: the read-error tests simulate corruption by deleting a LOOSE git object, but 'git merge' spawns a detached 'gc --auto' that can pack objects first on a busy runner - the pack copy then satisfies the read the test just broke (one flake in ~10 full runs). The fixture repo now disables auto gc/maintenance: determinism over background tidiness. - evolution_state_integrity nested-conftest child: a bare exit-1 was undiagnosable (the probe class) - the failure now carries the child's stderr tail; USERPROFILE is forwarded beside SystemRoot/TEMP since the conftest now resolves the canonical home root and ntpath ignores HOME.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft: v7 repository refactor. MILESTONE: the module-size debt registry is EMPTY — GIANT_PATHS and MODULE_DEBT_1500 both paid down to zero (independently censused: no source file over 1500 lines). Landed: T-stream, S-stream, L-A, W1-W5 + wave D, A.21, upstream adoption through v6.104.0, D02 plan-result seam, L-B loop.py 7021→696 (D33), 1A carrier engine (D34), G1 git_ops 1992→605 (D35), DEL1 delegate family (D36), TS1+TS2 seven test giants → 31 themed suites with exact node-id parity, L-C review stack (D37), L-C2 agent/pipeline/usage (D38), FIX batch, contributor trust boundary via conservative import closure, six-lane docs/ledger heal. 259 first-parent commits over base 353fd97 at head 9e3a2ac. Hermetic AT THIS TREE: 10075 parallel / 422 serial / 561 node passed, zero failures; evidence/migration/ratchet/ruff green. DISCLOSED: 29 earlier-pushed commits (L-B, 1A) carry the operator's author identity instead of Ouroboros, and 7 earlier-pushed commits hold a syntactically broken intermediate test file (integration-union artifact; bisect through them is impaired) — a whole-branch history rewrite is offered to the owner as an open decision. Remaining: L3 facade retirement (lane running), T3, final managed rebase + census, E2E, synthesis panel, 7.0.0 carriers, owner manual gate.
🤖 Generated with Claude Code
History rewrite disclosure (2026-08-19, owner-approved). The branch was rewritten once with
--force-with-lease(old tip7c9db28f, new tip483e1ecb):Ouroboros, author and committer dates preserved byte-for-byte), and 7 consecutive intermediate commits (6e5e2c20..1105aad6) carried a syntactically invalidtests/test_module_handle_extraction.py(one union-merge-swallowed closing line), which brokegit bisect. Each of the 7 now carries the minimal one-line repair; for 5 of them the repaired file is byte-identical to the fix the branch itself later landed in3aea4723.managed/ouroboros— including the external contributor commitf90b3b83and the adoption-merge anchor1abea773— are untouched.ast.parseof the repaired file at every commit of the new chain, upstream anchors still ancestors, full test battery green on483e1ecb(parallel rc=0, 422 serial, 561 node).sha_map_old_new_20260819.tsv); review comments made before this point may reference pre-rewrite SHAs.Upstream adoption v6.105.0/v6.105.1 (2026-08-19). One merge commit (
734ac4fc, second parente7c84240) plus follow-ups adopt 117 upstream files into the split v7 tree (18 hunk re-homes into v7 leaf modules, 26 manual conflict resolutions, 12 new upstream files). Independently reviewed by an adversarial seat before landing; the review caught and we restored two dropped unified-accounts hunks (OUROBOROS_SUBAGENT_PROFILEin the settings defaults — without it the Settings UI's Delegation account pin was silently discarded — and the recovered-custodyprofile_id), both now pinned by tests upstream never had.ouroboros/safety.py(+schedule_followup: POLICY_SKIP),docs/CHECKLISTS.md(rewritten DEGRADED review criterion),prompts/SYSTEM.md(+24 owner-surface-fact lines). The upstreamtools/registry.pyliteral-list hunk was structurally inapplicable (v7 derives the tool-module list;followupis picked up automatically — pinned).subagent_route_health.py,context_runtime_facts.py, plan-review test split).MERGE_BASE_SHAmoved toe7c84240; 62 ledger rows added, 9 verbatim rows re-synced to upstream text. Disclosed residual: three upstream-born literals carry trailing whitespace (kept byte-faithful for the verbatim provenance pin, sogit diff --checkflags exactly those lines). Full battery green at the exact tip (parallel/serial rc=0, 581 node) with a clean live-root inventory.8028f1df) since the adopted SHA; that drift is deliberately not part of this merge and lands with the final pre-release sync.Final upstream sync and code freeze (2026-08-20). The upstream cutoff is frozen at
8028f1df(owner decision): one merge commit adopts PR #257 (linux browser-mode, 42 files — 23 byte-identical adoptions, 11 manual unions, 2 re-homes into v7 leaves, 6 new files; one in-merge split keepslauncher.pyunder the 1500-line ceiling). Adversarially reviewed before landing; the review's two HIGH findings (three missing ledger rows for re-homed LLM client methods; the settings-seam scope claim) were fixed/scoped and the delta review returned SAFE.MERGE_BASE_SHAnow names the frozen cutoff.Test-hermeticity class closed on top of the sync: a settings-suite fixture teardown resurrected the pre-pytest live config root for the rest of its worker, which made later, correctly-written tests append to the live supervisor log nondeterministically. The stack lands the poisoning-fixture fix, a root binding in the one unbound logging test, the two spec-named root globals in the shared pytest binder, and a fail-closed autouse invariant that names any future poisoner. Full battery at the tip: 10313 parallel / 431 serial / 584 node, rc=0 everywhere, live root untouched by the suite.
This SHA starts the code freeze: remaining work is census/docs, E2E, the synthesis review panel on this frozen base, and the 7.0.0 carriers.