Skip to content

v7: repository refactor (draft, work in progress) - #226

Draft
ouroboros-agent wants to merge 325 commits into
razzant:ouroborosfrom
ouroboros-agent:ouroboros_v7_wip
Draft

v7: repository refactor (draft, work in progress)#226
ouroboros-agent wants to merge 325 commits into
razzant:ouroborosfrom
ouroboros-agent:ouroboros_v7_wip

Conversation

@ouroboros-agent

@ouroboros-agent ouroboros-agent commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

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 tip 7c9db28f, new tip 483e1ecb):

  • Why: 29 commits carried the wrong author/committer identity (now uniformly Ouroboros, author and committer dates preserved byte-for-byte), and 7 consecutive intermediate commits (6e5e2c20..1105aad6) carried a syntactically invalid tests/test_module_handle_extraction.py (one union-merge-swallowed closing line), which broke git 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 in 3aea4723.
  • Scope: only the 274 branch-local commits were rewritten (93 changed SHA, 181 early commits are byte-identical and kept their SHAs). Upstream-published commits reachable from managed/ouroboros — including the external contributor commit f90b3b83 and the adoption-merge anchor 1abea773 — are untouched.
  • Verification before push: pairwise tree equality for all commits except the 7 (whose tree diff is exactly the one test file), 100% author+committer identity, date and message sequences byte-identical, ast.parse of the repaired file at every commit of the new chain, upstream anchors still ancestors, full test battery green on 483e1ecb (parallel rc=0, 422 serial, 561 node).
  • The old→new SHA map is archived in the campaign records (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 parent e7c84240) 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_PROFILE in the settings defaults — without it the Settings UI's Delegation account pin was silently discarded — and the recovered-custody profile_id), both now pinned by tests upstream never had.

  • Protected surfaces adopted verbatim from upstream: ouroboros/safety.py (+schedule_followup: POLICY_SKIP), docs/CHECKLISTS.md (rewritten DEGRADED review criterion), prompts/SYSTEM.md (+24 owner-surface-fact lines). The upstream tools/registry.py literal-list hunk was structurally inapplicable (v7 derives the tool-module list; followup is picked up automatically — pinned).
  • DEGRADED semantics were transferred to all four v7 seam sites (upstream knew two); a blind merge would have crashed every DEGRADED plan-review wave at publication — now pinned at the producer and the publisher.
  • Three files pushed over the 1500-line ceiling by upstream additions were split inside the merge (subagent_route_health.py, context_runtime_facts.py, plan-review test split).
  • MERGE_BASE_SHA moved to e7c84240; 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, so git diff --check flags exactly those lines). Full battery green at the exact tip (parallel/serial rc=0, 581 node) with a clean live-root inventory.
  • Upstream has moved further (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 keeps launcher.py under 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_SHA now 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.

ouroboros-agent and others added 30 commits August 17, 2026 10:33
…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.
… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant