Merge remote-tracking branch 'upstream/ouroboros' into presentation/truthful-cards

This commit is contained in:
Anton 2026-09-24 13:45:04 +03:00
commit 9520456161
14 changed files with 724 additions and 136 deletions

View file

@ -74,7 +74,7 @@ WHO is speaking is ONE host-minted fact on the event (`control_routing._routing_
Every promote/route refusal names its typed reason; producers holding a fact beyond the code (the workspace family, source and attachment staging, persistence failures, the project fence) add `detail`, the cause with its repair (self-explanatory codes such as `duplicate_task_id` or `worker_pool_unavailable` get no prose), and `_persist_promote_rejection` is the single durable writer. `workspace_admission.workspace_repair_hint` composes the workspace repair from the typed source of the refused folder; an empty Project whose genesis provisioning fails is typed `workspace_provisioning_failed` on the promotion path, never a fallback onto the system repo. The exact Ouroboros repository root named as `workspace_root` is the documented default, mapped to the `workspace="none"` sentinel and disclosed; a subfolder of the repository, the data drive and every `/api/tasks`, Presence and `source` caller stay refused, typed. Who is told follows who can narrate: a tool-issued act gets its receipt, error row and `detail`, and the model narrates; a host-issued act (`host_initiated`) gets ONE typed System row (`task_not_started`, or `task_start_unconfirmed` for an unconfirmed admission) in the chat the owner wrote in, from the one publication boundary `_handle_promote_chat_to_task` wraps around every outcome, never a bubble in Ouroboros's voice. A steering refusal whose owner act carries no owner-message receipt uses `steer_not_delivered` through `supervisor/steering.py::_refusal_row`, in the issuing chat. The Project start row is announced only once the task is enqueued. An admitted promote reports the destination the admission receipt returns, not the requested `project_id`/`project_name` (an implicit scope is re-resolved under the origin claim lock, §6 Project binding by task and by origin); a promote landing in a project other than the one the request's work already has says so: promote stays a free choice, disclosed. A promote by a root whose Swarm planning obligation is still UNMET (`force_plan` with no plan wave recorded, `owner_hurry.unmet_force_plan_obligation`) carries the obligation onto the new root and releases the promoter in the admission transaction (`supervisor/plan_obligation.py`), disclosed in the result; a met obligation moves nothing, and `ensure_project_scope` keeps it.
A routing/promote decision turn receives host-built ground truth (each project's registry `working_dir` plus bounded typed projections of the lane's recent ROOT results and live roots, never raw result text) through the registry row's durable `last_task_result_id` pointer, stamped by ROOTS only, with a bounded scan fallback; only the absent-pointer case writes back (a failing non-empty pointer is usually a split-drive result mid copy-back). A swarm child is no predecessor (reachable through its root) and counts as its own omission instead of evicting a root. The list is a HINT sized by `runtime_limits`; the door is the predicate in §10. A routing receipt on the SAME owner message is a fact, never a ban: a second root stays the model's judgment.
A routing/promote decision turn receives host-built ground truth (each project's registry `working_dir` plus bounded typed projections of the lane's recent ROOT results and live roots, never raw result text) through the registry row's durable `last_task_result_id` pointer, stamped by ROOTS only, with a bounded scan fallback; only the absent-pointer case writes back (a failing non-empty pointer is usually a split-drive result mid copy-back). A swarm child is not offered (reachable through its root) and counts as its own omission instead of evicting a root. The list is a HINT sized by `runtime_limits` and scoped to the lane that receives it (a pooled task holds none and names any id it read through `recent_tasks`/`get_task_result`); the door is the predicate in §10 on the result itself, never on the caller's room or the landing project, and a helper predecessor or a landing outside the predecessor's project is disclosed in the receipt (`_predecessor_notes`), a free choice like the second-project note. `list_projects`, `route_to_project` and the promote receipt read the project registry through the canonical data root, because a task on a forked execution drive never carries `state/projects.json`. A routing receipt on the SAME owner message is a fact, never a ban: a second root stays the model's judgment.
### Tool capability and execution
@ -88,7 +88,7 @@ Tool API v2 exposes neutral canonical names directly (`read_file`, `list_files`,
Filesystem tool output is self-locating: results use canonical `root:path` labels and `run_command`/`run_script` echo the resolved `cwd`. A direct Project room selects one active physical folder for reads, writes, edits, process cwd, VCS and delegation; governance stays at `system_repo`, and a missing folder keeps its selected address with an availability note, never a fallback to Ouroboros source. Plain folders support ordinary file/process work and directory delegation (`delegate_directory.py`); Git-only snapshot and integration paths fail typed when their target lacks Git. Physical file resolution preserves the requested address: an absolute path inside any selected base normalizes to that base, an absolute path with no root binds to the permitted root holding it, an outside absolute path is refused before `safe_relpath` can turn it into a similarly named file, and the repo basename-prefix, lineage and delegated-artifact read redirects share the resolver guards and handlers use (`ToolContext.repo_path`/`drive_path`). A Docker workspace's mapped backend absolute address (`workspace_executor.map_backend_path`) is accepted only inside `active_workspace`; other roots and unmapped absolute paths stay confined.
`user_files` is the first-class root for user-visible files under the owner's home (the Ouroboros repo and runtime control plane are refused); `task_drive` is task-scoped scratch; `artifact_store` is task-scoped under `data/task_results/artifacts/<task_id>/`, created lazily on a write or output registration, where deliverables written through `user_files` or declared process `outputs` are copied for audit, and a rewritten user-visible file keeps its prior copy under `task_results/artifact_versions/<task_id>/` (§1 tree). `subagent_projects` and `deliverables` grant `read`/`list`/`search` only; a read-only child reads `deliverables` and its lineage's (parent/root) `task_drive`/`artifact_store`, never a sibling's; a top-level task writes the Deliverables container through `user_files`. Process admission keeps the original argv and the prepared physical target, and that one binding serves every other authorized destination; command words, script examples and unknown interpreter effects establish no write intent — the selected Safety Supervisor receives the full task source (§6 Safety and runtime mode) — and the post-execution shell audit is observational: no replay, rollback, interpreter or attribution proof. Invalid path-like prose yields no finding. A failed audit adds a bounded diagnostic (typed results record its exception class in `output_audit_unavailable`) while keeping stdout/stderr and completed exit, signal, timeout and runtime facts; it cannot invent a spawn failure or undeclared output. Computed targets stay a disclosed parser limit, not grounds for a new semantic scanner.
`user_files` is the first-class root for user-visible files under the owner's home (the Ouroboros repo and runtime control plane are refused); `task_drive` is task-scoped scratch; `artifact_store` is task-scoped under `data/task_results/artifacts/<task_id>/`, created lazily on a write or output registration, where deliverables written through `user_files` or declared process `outputs` are copied for audit, and a rewritten user-visible file keeps its prior copy under `task_results/artifact_versions/<task_id>/` (§1 tree). `subagent_projects` and `deliverables` grant `read`/`list`/`search` only; a read-only child reads `deliverables` and its lineage's (parent/root) `task_drive`/`artifact_store`, never a sibling's; a continuation also reads the `task_drive`/`artifact_store` of the one predecessor its contract names (`predecessor_authority.source.task_id`, one hop, read-only; a child carrying the envelope reads it too) (issue #1232); a top-level task writes the Deliverables container through `user_files`. Process admission keeps the original argv and the prepared physical target, and that one binding serves every other authorized destination; command words, script examples and unknown interpreter effects establish no write intent — the selected Safety Supervisor receives the full task source (§6 Safety and runtime mode) — and the post-execution shell audit is observational: no replay, rollback, interpreter or attribution proof. Invalid path-like prose yields no finding. A failed audit adds a bounded diagnostic (typed results record its exception class in `output_audit_unavailable`) while keeping stdout/stderr and completed exit, signal, timeout and runtime facts; it cannot invent a spawn failure or undeclared output. Computed targets stay a disclosed parser limit, not grounds for a new semantic scanner.
#### Credential fence and byte masking
@ -331,7 +331,7 @@ WHERE a mutating run's changes are destined is the second, separate record — t
A payload target gets a standalone private Git snapshot (`subagent_worktrees.provision_payload_snapshot`): the live payload is never initialized as Git, and capture trusts nothing under the child-writable snapshot's `.git`. Disposition (`integrate_payload_patch`) applies a live, index-free `git apply` in no-repository mode under a whole-payload content-hash CAS (drift = typed conflict; identical content = idempotent applied); `GIT_CEILING_DIRECTORIES` is pinned at the payload's resolved PARENT (git still searches the ceiling entry itself, and an ancestor Git worktree above the runtime data root could otherwise make git skip every hunk at rc=0), and reserved paths refuse the WHOLE apply as `blocked_reserved_paths` with the candidate preserved. The post-apply outcome set is complete: a live loader hash equal to the recorded RESULT hash is the success; a hash equal to the recorded BASELINE hash with a non-empty touched set is a provable non-mutation that RESOLVES the apply intent (typed `INTEGRATE_APPLY_NO_OP`, the `apply_no_op` arm: no success, no disposition, no reconcile queued, retry lane open); anything else is the ambiguous mismatch, whose intent stays PENDING and whose reconcile marker IS queued (the payload did mutate). A successful apply queues the extension reconcile (`request_extension_reconcile`) and the skill's review goes STALE pending fresh `skill_preflight`/`skill_review`; a run whose ONLY change is a mode flip is already refused at CAPTURE time as `unreviewable_metadata_change`, so a live hash still equal to the baseline means nothing was written.
**A mutating run normally executes in a PRIVATE EXECUTION SNAPSHOT** (metered children keep sharing the tree; their patches integrate through `integrate_subagent_patch`: sha256-bound, 3-way `--index`, protected-path gated, genesis refused, `coop_already_in_tree` a no-op). At `delegate_start` the host snapshots tracked, staged and eligible untracked state, deciding sensitive/credential vetoes BEFORE hashing (`git add -A` would put secrets such as `.env` in the shared object database). Git's binary verdict for the whole untracked inventory comes from one index-versus-worktree `git diff --numstat` over a scratch index (two when empty files need their attribute verdict; `workspace_patch_capture.untracked_binary_verdicts`, shared with patch capture; a failed batch falls back to the per-file verdict with a warning), never one process per file by design. The machine-wide worktree ops lock (`subagent_worktrees._ops_lock`) guards SHARED metadata only — the registry file, a target's `.git/worktrees` and its baseline pin — held twice, briefly (row FIRST, then the ref, then `worktree add --no-checkout`, so a crash after the row is GC-nameable); the acting `self_worktree` lane is split the same way (`worktree add --no-checkout -b` + branch under the lock, `reset --hard --no-recurse-submodules` and deletion outside it — the body's own `post-checkout` hook no longer fires at provision either), and only the boot-time `prune_orphans` sweep and the millisecond genesis `git init` still do their work under it; listing, classifying, hashing, populating (`reset --hard --no-recurse-submodules`: what `worktree add` runs internally, minus the target's `post-checkout` hook) and copying run outside it (issue #1241: one 67k-file provision held the lock 40 minutes and every other mutating start timed out). A held lock refuses typed (`cause: lock_busy` + the holder's pid/task/op), a SIGKILLed holder is evicted by the owner-aware stale check, every refused snapshot provision is `definitely_unrun` with a durable `START_FAILED` row, and the start receipt discloses `snapshot` size/time. Disclosed residuals: the registry read-modify-write under the lock is O(registered rows, their per-file baseline maps included — moving those maps out of the row is a follow-up), and every eligible untracked text file is hashed into the TARGET's own object database, where it stays unreachable after the snapshot is removed until that repository's own gc. The baseline is pinned by `refs/ouroboros/delegated/` and checked out as a detached worktree; `scope.root` stays the authority target and `execution.workspaceRoot` names the snapshot. The host appends a separate typed binding after the immutable work order: the snapshot is writable and the authority is read-only until integration. Directory-copy runs use the engine-created copy and never the source folder. Full native access has no filesystem sandbox; the binding names the writable root but does not enforce it. Terminal capture records authority drift as evidence: ready-no-changes stays no-change with unknown authorship, while ready-with-changes keeps its private artifact and the locked baseline proof decides integration. Nested Git directories are excluded and disclosed; skill payloads use content-hash CAS. The binding is durable before POST and retry replays it; a GC-collected snapshot is a typed `execution_snapshot_missing` refusal. Worktrees live in `state/subagent_worktrees.json`; removal is explicit or custody-cross-checked startup GC, fail-closed on unreadable custody. The run still uses `live` from the engine view, so the scoped-HOME/`delegated` marker below applies.
**A mutating run normally executes in a PRIVATE EXECUTION SNAPSHOT** (metered children keep sharing the tree; their patches integrate through `integrate_subagent_patch`: sha256-bound, 3-way `--index`, protected-path gated, genesis refused, `coop_already_in_tree` a no-op). At `delegate_start` the host snapshots tracked, staged and eligible untracked state, deciding sensitive/credential vetoes BEFORE hashing (`git add -A` would put secrets such as `.env` in the shared object database). Git's binary verdict for the whole untracked inventory comes from one index-versus-worktree `git diff --numstat` over a scratch index (two when empty files need their attribute verdict; `workspace_patch_capture.untracked_binary_verdicts`, shared with patch capture; a failed batch falls back to the per-file verdict with a warning), never one process per file by design. The machine-wide worktree ops lock (`subagent_worktrees._ops_lock`) guards SHARED metadata only — the registry file, a target's `.git/worktrees` and its baseline pin — held twice, briefly (row FIRST, then the ref, then `worktree add --no-checkout`, so a crash after the row is GC-nameable); the acting `self_worktree` lane is split the same way (`worktree add --no-checkout -b` + branch under the lock, `reset --hard --no-recurse-submodules` and deletion outside it — the body's own `post-checkout` hook no longer fires at provision either), and only the boot-time `prune_orphans` sweep and the millisecond genesis `git init` still do their work under it; listing, classifying, hashing, populating (`reset --hard --no-recurse-submodules`: what `worktree add` runs internally, minus the target's `post-checkout` hook), copying the source's exact bytes over the checkout and one `update-index` re-recording their stat (a CRLF-converting checkout otherwise leaves every such file "modified" in the child's `git status`) run outside it (issue #1241: one 67k-file provision held the lock 40 minutes and every other mutating start timed out). A held lock refuses typed (`cause: lock_busy` + the holder's pid/task/op), a SIGKILLed holder is evicted by the owner-aware stale check, every refused snapshot provision is `definitely_unrun` with a durable `START_FAILED` row, and the start receipt discloses `snapshot` size/time. Disclosed residuals: the registry read-modify-write under the lock is O(registered rows, their per-file baseline maps included — moving those maps out of the row is a follow-up), and every eligible untracked text file is hashed into the TARGET's own object database, where it stays unreachable after the snapshot is removed until that repository's own gc. The baseline is pinned by `refs/ouroboros/delegated/` and checked out as a detached worktree; `scope.root` stays the authority target and `execution.workspaceRoot` names the snapshot. The host appends a separate typed binding after the immutable work order: the snapshot is writable and the authority is read-only until integration. Directory-copy runs use the engine-created copy and never the source folder. Full native access has no filesystem sandbox; the binding names the writable root but does not enforce it. Terminal capture records authority drift as evidence: ready-no-changes stays no-change with unknown authorship, while ready-with-changes keeps its private artifact and the locked baseline proof decides integration. Nested Git directories are excluded and disclosed; skill payloads use content-hash CAS. The binding is durable before POST and retry replays it; a GC-collected snapshot is a typed `execution_snapshot_missing` refusal. Worktrees live in `state/subagent_worktrees.json`; removal is explicit or custody-cross-checked startup GC, fail-closed on unreadable custody. The run still uses `live` from the engine view, so the scoped-HOME/`delegated` marker below applies.
At terminal, `delegate_wait` captures the run's diff against the baseline durably into the task's artifact store; NOTHING reaches the target automatically: the nanny explicitly applies or rejects through `integrate_delegated_patch`. Git and skill captures are whole-result operations: omitted `paths` and an exact empty list select the same captured result, disclosed when explicit; nonempty selectors stay directory-only. Engine-directory empty/subset semantics are unchanged. The staging substrate differs: a GIT workspace target applies under the repo git lock after PROVING no touched path drifted from `baseline_sha` (a plain `git apply` relocates hunks by offset; the touched-path set is read from `git apply --numstat` in BOTH directions, each naming only the paths it writes), then applies and STAGES, never commits. A SKILL-PAYLOAD target captures through the payload adapter over a parent-owned trusted index and applies LIVE into the non-Git payload: nothing is staged into any active root and no `.git` or index is created in the payload. The protected-path gate applies only when the target IS the Ouroboros body; a conflict (proven drift) is owned by the still-running nanny, with snapshot and patch persisting until explicit resolution or discard. Mutation rides an apply-intent protocol: a durable `delegate_run_patch_apply_started` row lands before any tree mutation, so on replay a pending intent without a disposition answers typed `INTEGRATE_DELEGATED_APPLY_AMBIGUOUS`, resolved only by explicit `acknowledge_ambiguous=true`, while the provably non-mutating outcomes (a lock error, proven baseline drift, a failed apply, a verified revert, a baseline-equal payload hash) RESOLVE the intent. `patch_verdict.py` is the ONE verdict writer for both pipelines: subjects are minted `run_<rid>` by the writer, never prefix-matched by readers, and each decision lands twice (artifact plus typed `delegate_run_patch_verdict` custody row), a failed artifact write disclosed on the row. `artifacts.delegated_capture_read_target` narrowly rebinds `artifact_store` READS for the owning task's own `delegated_runs/` prefix, and `delegate_shared.orphan_capture_read_target` extends the same read-only, one-directory READ to the terminal-owner ORPHAN the disposition rule authorizes (confirmed by `orphan_disposition_status`), so the actor that may dispose a patch can inspect it without wider write authority. A read-only child stays in Claudexor's default envelope: one transport with one derived difference, not a second pipeline.

View file

@ -28,7 +28,7 @@ This chapter is the short list of properties the rest of the book must not contr
24. **The thread that answers workers runs only queue-bounded work.** Work that scales with history, the daemon or the network runs off-thread, reads candidates before liveness (one in-memory live source under `_queue_lock`), stops mutating when its loop generation ends and is attach-only to the daemon once a stop is in flight. Residuals on the loop thread: the 300-s zombie reconcile, the exact ledger reads of invariant 28. Owner: `ouroboros/server_maintenance.py`.
25. **An answer that has not arrived is a gap — never a refusal, a failure, a verdict or an owner message.** A direct turn applies its acceptance fence in-process (admission lock, then `_queue_lock`, never the reverse); a pooled request is idempotent by token and acknowledged per request (`<token>.<req>.json`); a transition is re-sent once, a read never; only `sealed` is a seal, an absent row is not one; a fence that did not answer buys no model round: the panel advises on `admission_fence_available=false`, delivery seals again, and a blocking install accepts a reviewer-approved answer with the typed note `admission_close_unconfirmed`. Owners: `ouroboros/agent.py`, `supervisor/queue_transitions.py`, `ouroboros/loop_delivery.py`.
26. **A cross-process guard derives from the durable artifact it guards, never from process memory.** The usage-ledger compaction floor is the `source_size_bytes` the last committed pass stamped into the live header, so one pass throttles every process, fresh ones included; the per-process memo throttles only a pass that changed no bytes. Owner: `ouroboros/usage_compaction.py`.
27. **The predecessor list is a hint; the door is a predicate.** A room continues any result of its OWN project that is a ROOT, readable and not live (a live root is `steer_task`, not a second root); only a ROOT stamps the pointer. An emitted promote is durably `promotion_admission{status:"emitted"}`: pending, granting no schedule, owning no id, never `unknown`. Owners: `tools/control_routing.py`, `tools/control_events.py`.
27. **The predecessor list is a hint; the door is a predicate on the result.** Any actor holding a routing verb continues any settled, readable result (a live root is `steer_task`, not a second root; a pending promote is not a result), whichever project it belongs to, wherever the continuation lands and whether it is a root's or a helper's: the pointer is rebuilt from the task id, the successor's ceiling, origin and contract come from the caller and admission, and the predecessor's project or helper origin is disclosed in the receipt, never an admission condition. Only a ROOT stamps the pointer and only roots are offered. An emitted promote is durably `promotion_admission{status:"emitted"}`: pending, granting no schedule, owning no id, never `unknown`. Owners: `tools/control_routing.py`, `tools/control_events.py`.
28. **Money never reads a snapshot; a display never waits on money.** A usage-ledger display reader on the supervisor loop or a gateway thread passes `allow_stale`: a contended lock serves the last validated snapshot against the live limit; a cold memo raises — unknown, never zero. Whatever admits, reserves, settles or refuses spend reads under the lock: a snapshot may say "there is money", never refuse. Owners: `ouroboros/_usage_rows_memo.py`, `supervisor/state.py`.
### 10.1 Continuity data-flow map

View file

@ -171,9 +171,10 @@ def _task_result_ground_truth(row: Dict[str, Any]) -> Dict[str, Any]:
def _is_child_result(facts: Dict[str, Any]) -> bool:
"""A result that is NOT an owner root: it has a parent, or the subagent role.
ONE predicate for both readers of that fact - the manifest window that skips
children (owner decision batch 3, answer 6b=A) and the promote door, which
refuses them by the same rule. Reads a memoized fact row or a full result row.
ONE predicate for every reader of that fact - the manifest window that skips
children (owner decision batch 3, answer 6b=A), the pointer stamp that only a
root moves, and the promote receipt, which continues a named child with its
root disclosed. Reads a memoized fact row or a full result row.
"""
return bool(str(facts.get("parent_task_id") or "").strip()) or str(
facts.get("delegation_role") or "") == "subagent"
@ -248,14 +249,14 @@ def _cancel_state_facts(ctx: Any, task_id: str) -> Dict[str, Any]:
def _project_routing_manifest(ctx: Any, project_id: str) -> Dict[str, Any]:
"""The room's bounded HINT for a "continue this work" decision: the project's
recent ROOT results and the roots still live in it, each with the small typed
facts that separate the two choices - a finished root is promote's predecessor,
facts that separate the two choices - a settled root is promote's predecessor,
a live one is ``steer_task``.
A hint, never the door: promote's predicate admits an older root of the same
project too (ch. 10), so this window may be bounded without deciding what the
room can continue. Until it existed a room saw exactly ONE candidate, the
registry pointer, so a room whose pointer had moved could not name its own
interrupted root at all.
A hint, never the door: promote's predicate admits any settled result, listed
or not, of any project (ch. 10), so this window may be bounded without
deciding what the room can continue. Until it existed a room saw exactly ONE
candidate, the registry pointer, so a room whose pointer had moved could not
name its own interrupted root at all.
"""
finals, omissions = _recent_root_results(ctx, project_id)
active = [
@ -280,7 +281,7 @@ def _not_a_root_result(row: Dict[str, Any]) -> bool:
def _latest_project_task_result(ctx: Any, project_id: str) -> Optional[Dict[str, Any]]:
"""Newest ROOT task result bound to ``project_id`` (a child's is never the room's
continuation: the promote door refuses it, ``_is_child_result``) WITHOUT replaying the whole
last-result pointer: the hint offers roots only, ``_is_child_result``) WITHOUT replaying the whole
store (DEVELOPMENT "Projection over replay"). The registry row's durable
``last_task_result_id`` pointer (stamped at project-task finalization) is
read FIRST — one direct file fetch, immune to how many newer foreign
@ -544,8 +545,8 @@ def main_lane_routing_metadata(ctx: Any, chat_id: int) -> Dict[str, Any]:
Exactly what an owner turn in the same chat is handed — the Main routing manifest
and this chat's addressable roots — minus what is bound to an owner message (there
is none). One seam over the owner path, so a wake can never drift from what the
host says is addressable: without the manifest every predecessor the wake names is
refused as "not addressable" and it cannot continue prior work at all.
host shows an owner turn: the manifest is the hint both decide from, while the
door judges the named result itself, listed or not.
"""
facts = _decision_turn_metadata(ctx, int(chat_id or 0), "", {})
return dict(facts) if isinstance(facts, dict) else {}

View file

@ -11,8 +11,9 @@ Mutations of SHARED metadata — a target's ``.git/worktrees`` (add/prune), its
``refs/ouroboros/delegated/*`` pins and the registry file — are serialized by a
portable cross-process lock (the existing repo git lock is drive-root scoped,
not ``.git`` scoped). Tree-proportional work never runs under it (#1241):
listing, classifying, hashing, populating, copying and deleting a snapshot's
files happen outside the lock, so one huge inventory delays only its own task.
listing, classifying, hashing, populating, copying (and re-recording the copied
bytes' stat) and deleting a snapshot's files happen outside the lock, so one
huge inventory delays only its own task.
"""
from __future__ import annotations
@ -602,8 +603,9 @@ def provision_execution_snapshot(
the baseline and create the worktree's admin dir — row FIRST, so everything
after it is nameable by the startup GC — and once to finalize the row.
Listing, classifying (one git process for every binary verdict), hashing,
populating and copying run OUTSIDE it, so a huge untracked inventory delays
only its own task instead of refusing every other mutating start.
populating, copying and the one ``update-index`` that re-records the copied
bytes' stat run OUTSIDE it, so a huge untracked inventory delays only its
own task instead of refusing every other mutating start.
"""
from ouroboros.workspace_patch_capture import (
binary_verdict_candidates, untracked_binary_verdicts, untracked_capture_veto_reason)
@ -747,6 +749,7 @@ def provision_execution_snapshot(
# source's actual working bytes, not checkout's CRLF/smudge rewrite.
# Read the existing tree inventory so deletions, links and gitlinks
# keep Git's semantics and excluded paths can never enter the copy.
copied: List[bytes] = []
for item in manifest_raw.split(b"\0"):
metadata, separator, raw_path = item.partition(b"\t")
if separator and metadata.split()[0] in (b"100644", b"100755"):
@ -755,6 +758,17 @@ def provision_execution_snapshot(
if original.is_symlink():
raise OSError(f"snapshot input changed from a regular file: {relative}")
copy_artifact_file(original, wt_path / relative)
copied.append(raw_path)
# The checkout recorded each entry's stat for the bytes IT wrote; a
# CRLF/smudge rewrite (core.autocrlf=true is Git for Windows' default)
# then differs in size from the copied source bytes, and git trusts a size
# mismatch as a modification without re-hashing — every such file would
# read as modified in the child's `git status` for the run's whole life.
# One update-index re-hashes the copied bytes through the same clean
# filters the baseline used (identical blob) and re-records their stat.
if copied:
_git_env(wt_path, "update-index", "-z", "--stdin", env=dict(os.environ),
input_bytes=b"\0".join(copied) + b"\0")
# A concurrent source edit must not appear as the child's work.
# Use the same Git representation as ordinary patch capture, once
# for the whole tree, before the separately tracked file inputs.

View file

@ -106,8 +106,8 @@ def summarize_subagent_profile(profile: ToolProfile, *, effective_lane: str = ""
lane = str(effective_lane or "").strip()
if lane:
bits.append(f"model_lane={lane}")
lineage = (" (task_drive/artifact_store: its own, its parent's and its root task's files,"
" never a sibling's)" if "task_drive" in read_roots else "")
lineage = (" (task_drive/artifact_store: its own, its parent's, its root task's and, when its"
" contract names one, its predecessor's files, never a sibling's)" if "task_drive" in read_roots else "")
return (
"child capabilities — " + " · ".join(bits)
+ f"\nreadable={', '.join(read_roots) or 'none'}{lineage}"
@ -116,20 +116,22 @@ def summarize_subagent_profile(profile: ToolProfile, *, effective_lane: str = ""
def lineage_task_ids(ctx: Any) -> tuple[str, ...]:
"""Task ids whose ``task_drive``/``artifact_store`` this actor may READ: its own,
then ``parent_task_id`` and ``root_task_id`` from its own lineage fields (T4=A,
#1105) — never a sibling's, nothing found by walking the disk, malformed ids dropped."""
meta = getattr(ctx, "task_metadata", None)
meta = meta if isinstance(meta, dict) else {}
"""Task ids whose ``task_drive``/``artifact_store`` this actor may READ: its own, its parent's
and its root's (own lineage fields, T4=A #1105) and the ONE predecessor its contract names
(``task_contract.predecessor_authority.source.task_id``, one hop, #1232; a child carrying the
envelope reads it too) — never a sibling's, nothing found by walking the disk, malformed ids dropped."""
meta, contract = (v if isinstance(v, dict) else {} for v in (
getattr(ctx, "task_metadata", None), getattr(ctx, "task_contract", None)))
authority = (contract or meta).get("predecessor_authority")
source = authority.get("source") if isinstance(authority, dict) else None
ids = [task_id_for_artifacts(ctx)]
for key in ("parent_task_id", "root_task_id"):
for raw in (meta.get("parent_task_id"), meta.get("root_task_id"),
source.get("task_id") if isinstance(source, dict) else None):
try:
candidate = validate_task_id(meta.get(key))
ids.append(validate_task_id(raw))
except ValueError:
continue
if candidate not in ids:
ids.append(candidate)
return tuple(ids)
return tuple(dict.fromkeys(ids))
def _task_root_drives(ctx: Any) -> list[pathlib.Path]:

View file

@ -119,9 +119,11 @@ _PROMOTE_CHAT_DESCRIPTION = (
"task). `project_id` starts the new task in an existing project. If your task "
"carries a planning obligation (Swarm force_plan) that no plan review has met, the "
"obligation moves to the new task and your own further work here is unplanned. "
"When this new task continues one specific "
"completed result shown by the host (the Main manifest or Project last-result "
"preview), pass its internal id as `predecessor_task_id`; pass an empty string for fresh work. "
"When this new task continues one specific settled result (any settled status; from "
"the host manifest, recent_tasks or get_task_result — any project, the "
"list is a hint; a helper's result is continued with its root named), pass its internal "
"id as `predecessor_task_id`; pass an empty string for fresh work. A live root "
"(steer_task instead) or a pending promote is refused. "
"`workspace_root` points at a working folder. A project-scoped task inherits "
"the project's working folder as its ACTIVE WORKSPACE by default (its file/"
"shell/git tools operate there, not on the Ouroboros repo); pass "
@ -214,7 +216,7 @@ def get_tools() -> List[ToolEntry]:
"workspace_root": {"type": "string", "description": "Optional absolute working-folder path (validated at admission as an ordinary folder or Git worktree root outside the Ouroboros repo/data). Git-specific operations require a Git worktree; ordinary file and process work is supported directly in a validated folder. When omitted for a project-scoped task, the project's registered working_dir is used by default. Leave empty to work in Ouroboros's own repository (the Main default).", "default": ""},
"workspace": {"type": "string", "description": "Pass 'none' to opt OUT of the project room's default working folder (a folder-less task in a folder-ful project). Leave empty otherwise.", "default": ""},
"source": {"type": "string", "description": "Attach or clone the project's working folder in ONE move: a git URL (https://... or git@host:path — cloned server-side into the projects root; private repos fail typed auth_required) or an existing folder path (validated attach). The folder is registered on the project (provenance + trusted_at) and becomes this task's active workspace. Use for 'help me debug this GitHub repo / this folder' asks.", "default": ""},
"predecessor_task_id": {"type": "string", "description": "Required explicit selector: pass an empty string for fresh work, or the completed result id shown by the host routing manifest to continue it."},
"predecessor_task_id": {"type": "string", "description": "Required explicit selector: pass an empty string for fresh work, or the id of a settled result (any settled status; any project, the host list is a hint; a helper's result is continued with its root named) to continue it. A live root or a pending promote is refused."},
},
"required": ["objective", "predecessor_task_id"],
},
@ -264,15 +266,15 @@ def get_tools() -> List[ToolEntry]:
"CALL THIS TOOL with project_id='' and the owner's message: it emits the typed "
"needs_manual_target acknowledgement with host-validated task options and New task "
"in Project; prose alone cannot emit that typed choice. For brand-new work that is not yet a project, "
"use promote_chat_to_task instead. When continuing one completed result from the "
"Main host manifest, pass its internal `predecessor_task_id`; pass an empty string for fresh work. "
"Returns a visible routing receipt."
"use promote_chat_to_task instead. When continuing one settled result (any project; "
"the host list is a hint), pass its internal `predecessor_task_id`; pass an empty "
"string for fresh work. Returns a visible routing receipt."
),
"parameters": {"type": "object", "properties": {
"project_id": {"type": "string", "default": "", "description": "Target project id (filesystem-clean; see list_projects), or empty to emit typed needs_manual_target."},
"message": {"type": "string", "description": "The owner message / work to route into the project."},
"reason": {"type": "string", "default": "", "description": "Optional short why-this-project note (provenance)."},
"predecessor_task_id": {"type": "string", "description": "Required explicit selector: pass an empty string for fresh work, or the completed result id listed by the Main host manifest to continue it."},
"predecessor_task_id": {"type": "string", "description": "Required explicit selector: pass an empty string for fresh work, or the id of a settled result (any settled status; any project, the host list is a hint; a helper's result is continued with its root named) to continue it. A live root or a pending promote is refused."},
"candidates": {"type": "array", "items": {"type": "string"}, "description": "Optional, ONLY with project_id='': the task/project ids you consider plausible, in preference order. The typed picker shows them first; ids not in the host-built option list are ignored."},
}, "required": ["message", "predecessor_task_id"]},
}, _route_to_project),

View file

@ -20,6 +20,7 @@ from ouroboros.tools.control_events import (
_emit_and_wait_for_routing,
_promotion_pool_disabled_from_snapshot,
)
from ouroboros.tool_access_paths import canonical_data_root
from ouroboros.tools.registry import ToolContext
from ouroboros.utils import append_jsonl, utc_now_iso
@ -37,7 +38,7 @@ def _predecessor_selector_error(value: Any, tool_name: str) -> str:
if value is _MISSING_PREDECESSOR_SELECTOR or value is None:
return (
f"⚠️ TOOL_ARG_ERROR ({tool_name}): predecessor_task_id is required; "
"pass an empty string for fresh work or the host-listed result id to continue it"
"pass an empty string for fresh work or the id of a settled result to continue it"
)
return ""
@ -109,46 +110,46 @@ def _host_listed_predecessors(metadata: Dict[str, Any]) -> list:
return rows
def _predecessor_door_refusal(
ctx: ToolContext, result: Dict[str, Any], listed: bool,
) -> str:
"""Why this readable result may NOT be continued from here, or ``""``.
def _predecessor_door_refusal(result: Dict[str, Any]) -> str:
"""Why this readable result may NOT be continued, or ``""``.
The door is a PREDICATE, not membership of a bounded list: same project, a
ROOT, a readable result, not live. The list is a hint - a window that fits
16 rows became a ceiling saying "exactly one result is addressable", so a
room could not name its own interrupted root and promoted again instead,
minting the duplicate root the night ended with. Outside a room there is no
project to compare against, and an absent project is not a match: there the
host's own list still decides.
A PREDICATE on the result itself - settled, and a result rather than a pending
admission - never on where the caller sits, where the work lands or whether it
is a root's or a helper's: the pointer is rebuilt from the task id alone, the
successor inherits DATA (ceiling, origin and contract come from the caller and
admission), and a fresh root in the predecessor's project was always reachable.
The host list stays a hint; a foreign landing or a helper is disclosed, never refused.
"""
from ouroboros.routing_wait import is_emitted_admission_stub
from ouroboros.server_routing_context import _is_child_result
from ouroboros.task_status import SETTLED_STATUSES
if _is_child_result(result):
return (
"the selected predecessor is a delegated child result; name the ROOT task it "
"belongs to - a child's work is reachable through its root"
)
status = str(result.get("status") or "")
if is_emitted_admission_stub(result):
return ("the selected predecessor is a promote whose admission is still pending, not a "
"result; read get_task_result on it, and name a finished root instead")
"result; read get_task_result on it, and name a settled result instead")
if status not in SETTLED_STATUSES:
return (
f"the selected predecessor is still live (status {status or 'unknown'}); "
"steer_task continues a live root, and promoting it would start a second one"
)
room = _inherited_project_scope(ctx)
if not listed and not (room and room == str(result.get("project_id") or "")):
return (
"predecessor_task_id is not an addressable result in the host routing "
"manifest, and it is not a finished root task of this room's project"
)
return ""
def _predecessor_notes(predecessor_id: str, facts: Dict[str, Any], landed: str) -> str:
"""What the receipt says about the predecessor once, like the second-project note: a
helper's result names its root (or parent), a foreign landing names both projects."""
if not predecessor_id:
return ""
home, root, parent = (str(facts.get(k) or "") for k in ("project_id", "root_task_id", "parent_task_id"))
lineage = f"its root is {root}" if root else (f"its parent is {parent}" if parent else "its root is unknown")
notes = f" Note: predecessor {predecessor_id} is a delegated helper's result; {lineage}." if facts.get("helper") else ""
if home != str(landed or ""):
where = f"project '{home}'" if home else "the main chat"
here = f"project '{landed}'" if landed else "the main chat"
notes += f" Note: predecessor {predecessor_id} belongs to {where}; this continuation runs in {here} (your choice)."
return notes
def _attach_predecessor_authority_from_metadata(
ctx: ToolContext, evt: Dict[str, Any], predecessor_task_id: str = "",
) -> str:
@ -161,15 +162,10 @@ def _attach_predecessor_authority_from_metadata(
row for row in _host_listed_predecessors(metadata)
if str(row.get("task_id") or "") == selected_id
), None)
status_root = Path(str(
metadata.get("budget_drive_root")
or getattr(ctx, "budget_drive_root", "")
or ctx.drive_root
))
result = load_effective_task_result(status_root, selected_id, materialize_artifacts=False)
result = load_effective_task_result(canonical_data_root(ctx), selected_id, materialize_artifacts=False)
if not isinstance(result, dict) or not result:
return "the selected predecessor task result is missing or unreadable"
refusal = _predecessor_door_refusal(ctx, result, listed is not None)
refusal = _predecessor_door_refusal(result)
if refusal:
return refusal
if listed is not None:
@ -184,8 +180,17 @@ def _attach_predecessor_authority_from_metadata(
from ouroboros.agent_startup_checks import valid_task_result_authority_source
if valid_task_result_authority_source(source, selected_id):
from ouroboros.server_routing_context import _is_child_result
evt["predecessor_task_id"] = selected_id
evt["predecessor_authority_source"] = dict(source)
# Render-only facts for the receipt's notes; the caller pops them before
# emission, so the event carries nothing the supervisor never reads.
evt["predecessor_facts"] = {
"project_id": str(result.get("project_id") or ""), "helper": _is_child_result(result),
"root_task_id": str(result.get("root_task_id") or ""),
"parent_task_id": str(result.get("parent_task_id") or ""),
}
else:
return "the selected predecessor has no readable authority source"
return ""
@ -281,7 +286,7 @@ def _effective_scope_note(ctx: ToolContext, project_id: str) -> str:
try:
from ouroboros.projects_registry import get_project
name = str((get_project(Path(ctx.drive_root), pid) or {}).get("name") or "").strip()
name = str((get_project(canonical_data_root(ctx), pid) or {}).get("name") or "").strip()
except Exception:
log.debug("promote: effective project name lookup failed", exc_info=True)
return f" in project '{name}' ({pid})" if name and name != pid else f" in project '{pid}'"
@ -464,6 +469,7 @@ def _promote_chat_to_task(
"⚠️ AUTHORITY_SOURCE_UNAVAILABLE (promote_chat_to_task): "
+ predecessor_error
)
predecessor_facts = dict(evt.pop("predecessor_facts", None) or {})
_attach_client_surface(ctx, evt)
_attach_unmet_obligation(ctx, evt)
already_bound = _durable_project_of_request(ctx)
@ -483,6 +489,7 @@ def _promote_chat_to_task(
"Use wait_task/get_task_result if its result "
"is needed in this conversation."
+ _second_project_note(ctx, already_bound, effective_pid)
+ _predecessor_notes(str(evt.get("predecessor_task_id") or ""), predecessor_facts, effective_pid)
+ _obligation_moved_note(ctx, tid, confirmation.get("force_plan_transfer"))
)
return _finish_swarm_handoff(ctx, evt, response, status="scheduled")
@ -577,10 +584,12 @@ def _second_project_note(ctx: ToolContext, already_bound: str, effective_pid: st
def _list_projects(ctx: ToolContext, limit: int = 50) -> str:
"""Enumerate the owner's projects (id, name, recency) so the one mind can
decide whether a main-chat message belongs to an existing project."""
decide whether a main-chat message belongs to an existing project. The registry
lives on the CANONICAL data root: a forked execution drive never carries
``state/projects.json``, so reading the task's own drive answered "no projects"."""
try:
from ouroboros.projects_registry import projects_summary
rows = projects_summary(Path(ctx.drive_root), limit=max(1, min(int(limit or 50), 200)))
rows = projects_summary(canonical_data_root(ctx), limit=max(1, min(int(limit or 50), 200)))
except Exception as exc:
return f"⚠️ PROJECTS_ERROR: {type(exc).__name__}: {exc}"
if not rows:
@ -635,9 +644,10 @@ def _route_to_project(
)
if predecessor_error:
return "⚠️ AUTHORITY_SOURCE_UNAVAILABLE (route_to_project): " + predecessor_error
predecessor_facts = dict(predecessor_event.pop("predecessor_facts", None) or {})
requested_pid = str(project_id or "").strip()
pid = sanitize_project_id(requested_pid) if requested_pid and explicit_project_id_ok(requested_pid) else ""
proj = get_project(Path(ctx.drive_root), pid) if pid else None
proj = get_project(canonical_data_root(ctx), pid) if pid else None
failure = (
"target_unspecified" if not requested_pid
else "invalid_project_id" if not pid
@ -753,6 +763,8 @@ def _route_to_project(
response = (
f"✉️ Routed to project '{name}' ({pid}) as task {tid}; admission is durably "
f"scheduled ({mode}). I'll continue there; this chat stays free for you."
+ _predecessor_notes(str(evt.get("predecessor_task_id") or ""), predecessor_facts,
str(receipt.get("effective_project_id") or pid))
+ _obligation_moved_note(ctx, tid, receipt.get("force_plan_transfer"))
)
return _finish_swarm_handoff(ctx, evt, response, status="scheduled")

View file

@ -280,9 +280,8 @@ def record_task_finalization(
if is_root:
# The pointer answers "continue from here" for the ROOM, so only a ROOT may
# stamp it: a child finalizing after its root moved the room's single
# candidate onto work no owner ever addressed, and the room was then left
# naming a result the promote door refuses (the mirror below is root-only
# for the same reason).
# candidate onto work no owner ever addressed - a helper the hint never
# offers (the mirror below is root-only for the same reason).
record_project_last_result(project_id, tid, drive_root)
try:
_record_work_location(project_id, task)

View file

@ -935,22 +935,33 @@ def test_a_wake_starts_fresh_work_with_no_predecessor_and_needs_no_manifest(tmp_
assert "predecessor_task_id" not in promoted.pending_events[0]
def test_a_wake_without_the_manifest_still_refuses_an_unaddressable_predecessor(tmp_path):
"""The typed refusal is unchanged; only the facts the wake is given are new."""
def test_a_wake_without_the_manifest_continues_a_settled_root_and_still_refuses_a_live_one(tmp_path):
"""The door judges the root, not the facts a wake was handed: with no manifest at all
a wake continues a settled root on both verbs (the pointer is rebuilt from the durable
result and equals the one the manifest would have shown), while a live root keeps its
typed refusal toward steer_task and emits nothing."""
from ouroboros.projects_registry import create_project
from ouroboros.tools.control_routing import _promote_chat_to_task, _route_to_project
create_project(tmp_path, "racer", name="Racer")
_addressable_result(tmp_path)
preview = _addressable_result(tmp_path)
routed = _wake_routing_ctx(tmp_path)
out = _route_to_project(routed, "racer", "Continue the racer", predecessor_task_id="racer-old")
assert out.startswith("⚠️ AUTHORITY_SOURCE_UNAVAILABLE (route_to_project)")
assert "not an addressable result in the host routing manifest" in out
assert routed.pending_events == []
assert out.startswith("⚠️ ROUTE_UNCONFIRMED"), out
[route_evt] = routed.pending_events
assert route_evt["predecessor_authority_source"] == preview["authority_source"]
promoted = _wake_routing_ctx(tmp_path)
refused = _promote_chat_to_task(promoted, "Finish the racer", workspace="none",
predecessor_task_id="racer-old")
assert refused.startswith("⚠️ AUTHORITY_SOURCE_UNAVAILABLE (promote_chat_to_task)")
assert promoted.pending_events == []
_promote_chat_to_task(promoted, "Finish the racer", workspace="none", predecessor_task_id="racer-old")
[promote_evt] = promoted.pending_events
assert promote_evt["predecessor_task_id"] == "racer-old"
assert promote_evt["initiator"] == "consciousness"
(tmp_path / "task_results" / "racer-live.json").write_text(json.dumps({
"_schema_version": 1, "task_id": "racer-live", "status": "running", "project_id": "racer",
}), encoding="utf-8")
refused = _wake_routing_ctx(tmp_path)
out = _route_to_project(refused, "racer", "Continue the racer", predecessor_task_id="racer-live")
assert out.startswith("⚠️ AUTHORITY_SOURCE_UNAVAILABLE (route_to_project)") and "steer_task" in out
assert refused.pending_events == []

View file

@ -0,0 +1,282 @@
"""A top-level continuation reads the ONE predecessor it continues (owner 3A, #1232).
Measured on a live install: a continuation admitted with ``memory_mode=forked``
runs on its own headless drive (``data/state/headless_tasks/<id>/data``), called
``get_task_result(include_authority=True)`` on its predecessor, saw a registered
artifact path plus hash, and ``read_file`` on that path was refused ("outside
artifact root"). The lineage rule (T4=A, #1105) named the actor's own, parent's
and root's task files; the predecessor was not in that set. Now
``lineage_task_ids`` appends the predecessor's validated id from the ONE carrier a
running ToolContext has: ``task_contract["predecessor_authority"]["source"]
["task_id"]``, minted by ``validate_task_authority_sources`` at startup binding
(``agent_startup_checks``) and carried into the contract by
``build_task_contract``; ``task_metadata`` is read only when no contract rides
(the agent never copies the envelope into metadata). One hop only: the
envelope's ``previous_task_id`` is never read. READ only: a write into the
predecessor's drive stays refused while the task's own drive stays writable.
The read follows the envelope the actor carries: a delegated child inherits its
parent's envelope through the contract spread (``subagent_work_order`` copies the
parent contract whole) and reads that predecessor's files too, read-only, exactly
as it reads its parent's and root's; a child without the envelope keeps parent/root.
"""
from __future__ import annotations
import pathlib
from types import SimpleNamespace
import pytest
from ouroboros.artifacts import task_artifact_dir_path
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tool_access import (
_resolve_target_in_selected_base,
lineage_read_base,
lineage_task_ids,
)
from ouroboros.tools.registry import ToolContext, ToolRegistry
SUCCESSOR = "s2f0a1b2c3d4e5f60"
PRED = "pred-1"
GRANDPRED = "pred-0"
OTHER = "other-task"
PARENT = "p07499dc017c01f83"
ROOT = "r5173b7c3c15d4c0b"
CHILD = "c11ae4fd0aa111111"
def host_pointer(task_id):
"""The exact host-issued actor pointer ``valid_task_result_authority_source`` accepts."""
return {
"kind": "task_result",
"task_id": task_id,
"human_label": f"task {task_id}",
"tool": "get_task_result",
"arguments": {"task_id": task_id, "include_authority": True},
}
def predecessor_envelope(task_id, *, previous=""):
"""The shape startup binding mints: a bounded envelope whose chain cursor names
the hop BEFORE the predecessor and whose pull pointer rides LAST under ``source``."""
return {
"kind": "bounded_continuation_envelope",
"status": "done",
"previous_task_id": previous,
"source": host_pointer(task_id),
}
@pytest.fixture
def geometry(tmp_path, monkeypatch):
"""The predecessor's task files live on the CANONICAL data root; the
continuation runs on its own forked (headless) drive; the owner home is a
fake tmp home. The predecessor's own predecessor and a stranger have files
too, so the one-hop and no-walking rules have something to refuse."""
home = tmp_path / "home"
repo = tmp_path / "repo"
canonical = tmp_path / "data"
headless = tmp_path / "state" / "headless_tasks" / SUCCESSOR / "data"
for path in (home, repo, canonical, headless):
path.mkdir(parents=True)
monkeypatch.setattr(pathlib.Path, "home", lambda: home)
monkeypatch.setenv("OUROBOROS_USER_FILES_ROOT", str(home))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
monkeypatch.setenv("OUROBOROS_SAFETY_MODE", "off")
(repo / "README.md").write_text("repo readme\n", encoding="utf-8")
pred_drive = canonical / "task_drives" / PRED
pred_drive.mkdir(parents=True)
(pred_drive / "notes.md").write_text("PRED_DRIVE_BYTES\n", encoding="utf-8")
pred_artifacts = task_artifact_dir_path(canonical, PRED, create=True)
(pred_artifacts / "report.pdf.txt").write_text("PRED_ARTIFACT_BYTES\n", encoding="utf-8")
grand_drive = canonical / "task_drives" / GRANDPRED
grand_drive.mkdir(parents=True)
(grand_drive / "notes.md").write_text("GRANDPRED_BYTES\n", encoding="utf-8")
other_drive = canonical / "task_drives" / OTHER
other_drive.mkdir(parents=True)
(other_drive / "notes.md").write_text("OTHER_BYTES\n", encoding="utf-8")
other_artifacts = task_artifact_dir_path(canonical, OTHER, create=True)
(other_artifacts / "out.txt").write_text("OTHER_ARTIFACT_BYTES\n", encoding="utf-8")
return SimpleNamespace(
home=home, repo=repo, canonical=canonical, headless=headless,
pred_drive=pred_drive, pred_artifacts=pred_artifacts, grand_drive=grand_drive,
other_drive=other_drive, other_artifacts=other_artifacts,
)
def continuation_registry(geo, *, predecessor=PRED):
"""A top-level continuation of ``predecessor`` admitted with ``memory_mode=forked``:
it executes on its headless drive while the canonical root stays its budget root.
``predecessor=None`` gives the same task without any predecessor authority."""
ctx = ToolContext(
repo_dir=geo.repo, drive_root=geo.headless, task_id=SUCCESSOR, memory_mode="forked",
budget_drive_root=str(geo.canonical),
)
ctx.task_metadata = {"memory_mode": "forked", "budget_drive_root": str(geo.canonical)}
ctx.task_contract = {"predecessor_authority": predecessor_envelope(
predecessor, previous=GRANDPRED)} if predecessor else {}
registry = ToolRegistry(repo_dir=geo.repo, drive_root=ctx.drive_root)
registry.set_context(ctx)
return registry, ctx
def child_registry(geo):
"""A delegated read-only child of PARENT under ROOT whose contract carries the
parent's predecessor envelope, exactly as the parent contract spread delivers it."""
ctx = ToolContext(repo_dir=geo.repo, drive_root=geo.headless, task_id=CHILD)
ctx.budget_drive_root = str(geo.canonical)
ctx.task_metadata = {
"delegation_role": "subagent",
"parent_task_id": PARENT,
"root_task_id": ROOT,
"budget_drive_root": str(geo.canonical),
}
ctx.task_contract = {"predecessor_authority": predecessor_envelope(PRED)}
ctx.task_constraint = TaskConstraint(mode="local_readonly_subagent", allow_enable=False)
registry = ToolRegistry(repo_dir=geo.repo, drive_root=ctx.drive_root)
registry.set_context(ctx)
return registry, ctx
# --- (a) the continuation reads its predecessor's task files from a forked drive ---
def test_continuation_reads_its_predecessors_task_drive_from_a_forked_drive(geometry):
registry, ctx = continuation_registry(geometry)
target = geometry.pred_drive / "notes.md"
out = registry.execute("read_file", {"root": "task_drive", "path": str(target)})
assert "PRED_DRIVE_BYTES" in out, out
assert out.startswith("# task_drive:"), out
assert ctx.last_read_view["opened_root"] == "task_drive"
assert ctx.last_read_view["target"] == str(target.resolve())
def test_continuation_reads_its_predecessors_registered_artifact(geometry):
"""The live shape: the artifact path ``get_task_result(include_authority=True)``
registered, read with the artifact root named and with no root at all."""
registry, ctx = continuation_registry(geometry)
target = geometry.pred_artifacts / "report.pdf.txt"
named = registry.execute("read_file", {"root": "artifact_store", "path": str(target)})
bare = registry.execute("read_file", {"path": str(target)})
assert "PRED_ARTIFACT_BYTES" in named and named.startswith("# artifact_store:"), named
assert "PRED_ARTIFACT_BYTES" in bare, bare
assert ctx.last_read_view["opened_root"] == "artifact_store"
assert lineage_read_base(ctx, "artifact_store", target) == geometry.pred_artifacts.resolve()
assert lineage_read_base(ctx, "task_drive", geometry.pred_drive / "notes.md") == geometry.pred_drive.resolve()
# --- (b) a stranger's files and the hop before the predecessor stay refused -------
def test_a_strangers_drive_and_the_predecessors_own_predecessor_stay_refused(geometry):
registry, ctx = continuation_registry(geometry)
other = registry.execute("read_file", {"root": "task_drive", "path": str(geometry.other_drive / "notes.md")})
other_artifact = registry.execute(
"read_file", {"root": "artifact_store", "path": str(geometry.other_artifacts / "out.txt")})
grand = registry.execute("read_file", {"root": "task_drive", "path": str(geometry.grand_drive / "notes.md")})
assert "OTHER_BYTES" not in other and "outside selected root=task_drive" in other, other
assert "OTHER_ARTIFACT_BYTES" not in other_artifact, other_artifact
assert "outside selected root=artifact_store" in other_artifact, other_artifact
# ONE hop: the envelope's ``previous_task_id`` (GRANDPRED) is never a lineage member.
assert "GRANDPRED_BYTES" not in grand and "outside selected root=task_drive" in grand, grand
assert GRANDPRED not in lineage_task_ids(ctx) and OTHER not in lineage_task_ids(ctx)
# --- (c) the predecessor grant is a READ grant ------------------------------------
def test_a_write_into_the_predecessors_drive_is_refused_while_the_own_drive_stays_writable(geometry):
registry, ctx = continuation_registry(geometry)
target = geometry.pred_drive / "notes.md"
before = target.read_text(encoding="utf-8")
refused = registry.execute("write_file", {"root": "task_drive", "path": str(target), "content": "x"})
own = registry.execute("write_file", {"root": "task_drive", "path": "scratch.txt", "content": "mine"})
assert refused.startswith("⚠️") and "outside selected root=task_drive" in refused, refused
assert target.read_text(encoding="utf-8") == before
assert not own.startswith("⚠️"), own
assert (geometry.headless / "task_drives" / SUCCESSOR / "scratch.txt").read_text(encoding="utf-8") == "mine"
own_base = geometry.headless / "task_drives" / SUCCESSOR
assert _resolve_target_in_selected_base(
ctx, root="task_drive", base_path=own_base, path=str(target), operation="read") == target.resolve()
for operation in ("write", "edit"):
with pytest.raises(ValueError):
_resolve_target_in_selected_base(
ctx, root="task_drive", base_path=own_base, path=str(target), operation=operation)
# --- (d) without predecessor authority nothing changes ----------------------------
def test_a_task_without_predecessor_authority_keeps_todays_lineage(geometry):
registry, ctx = continuation_registry(geometry, predecessor=None)
assert lineage_task_ids(ctx) == (SUCCESSOR,)
out = registry.execute("read_file", {"root": "task_drive", "path": str(geometry.pred_drive / "notes.md")})
assert "PRED_DRIVE_BYTES" not in out and "outside selected root=task_drive" in out, out
ctx.task_metadata.update({"parent_task_id": PARENT, "root_task_id": ROOT})
assert lineage_task_ids(ctx) == (SUCCESSOR, PARENT, ROOT)
ctx.task_metadata["root_task_id"] = PARENT # parent IS the root: no duplicate
assert lineage_task_ids(ctx) == (SUCCESSOR, PARENT)
ctx.task_metadata["parent_task_id"] = "../escape" # malformed ids are dropped, not guessed
ctx.task_metadata["root_task_id"] = ""
assert lineage_task_ids(ctx) == (SUCCESSOR,)
top = ToolContext(repo_dir=geometry.repo, drive_root=geometry.canonical, task_id=ROOT)
assert lineage_task_ids(top) == (ROOT,)
def test_lineage_task_ids_names_the_predecessor_once_after_parent_and_root(geometry):
_registry, ctx = continuation_registry(geometry)
assert lineage_task_ids(ctx) == (SUCCESSOR, PRED)
ctx.task_metadata.update({"parent_task_id": PARENT, "root_task_id": ROOT})
assert lineage_task_ids(ctx) == (SUCCESSOR, PARENT, ROOT, PRED)
ctx.task_metadata["root_task_id"] = PRED # the predecessor IS the root: no duplicate
assert lineage_task_ids(ctx) == (SUCCESSOR, PARENT, PRED)
# A malformed or missing pointer id is dropped, never guessed from the envelope.
ctx.task_metadata = {}
ctx.task_contract["predecessor_authority"]["source"]["task_id"] = "../escape"
assert lineage_task_ids(ctx) == (SUCCESSOR,)
ctx.task_contract["predecessor_authority"]["source"] = "not-a-pointer"
assert lineage_task_ids(ctx) == (SUCCESSOR,)
ctx.task_contract["predecessor_authority"] = "not-an-envelope"
assert lineage_task_ids(ctx) == (SUCCESSOR,)
def test_metadata_carries_the_envelope_only_when_no_contract_rides(geometry):
"""The contract is the carrier on a running ToolContext (the agent never copies
the envelope into metadata); a context with no contract at all is read from
metadata, and a present contract is never second-guessed by metadata."""
_registry, ctx = continuation_registry(geometry, predecessor=None)
ctx.task_metadata["predecessor_authority"] = predecessor_envelope(PRED)
assert lineage_task_ids(ctx) == (SUCCESSOR, PRED)
ctx.task_contract = {"predecessor_authority": predecessor_envelope(OTHER)}
assert lineage_task_ids(ctx) == (SUCCESSOR, OTHER)
ctx.task_contract = {"lineage": {"delegation_role": "root"}}
assert lineage_task_ids(ctx) == (SUCCESSOR,)
# --- (e) a delegated child keeps parent/root even with an inherited envelope ------
def test_a_delegated_child_inherits_the_continuations_predecessor_read(geometry):
"""A child CAN carry ``predecessor_authority``: ``subagent_work_order`` copies the
parent's contract whole into the child task and ``build_task_contract`` preserves
the envelope. The read follows the envelope the actor carries - the child's lineage
grows by the predecessor and its files are readable, read-only, exactly as the
parent's and root's are; a child without the envelope keeps parent/root only."""
registry, ctx = child_registry(geometry)
assert lineage_task_ids(ctx) == (CHILD, PARENT, ROOT, PRED)
out = registry.execute("read_file", {"root": "task_drive", "path": str(geometry.pred_drive / "notes.md")})
assert "PRED_DRIVE_BYTES" in out, out
assert lineage_read_base(ctx, "task_drive", geometry.pred_drive / "notes.md") is not None
ctx.task_contract.pop("predecessor_authority", None)
ctx.task_metadata.pop("predecessor_authority", None)
assert lineage_task_ids(ctx) == (CHILD, PARENT, ROOT)

View file

@ -1,10 +1,14 @@
"""I7: what a project room may continue, and what stamps its last-result pointer.
"""What a routing verb may continue, and what stamps a project's last-result pointer.
On 19.09 a room offered exactly ONE predecessor candidate (the project's
last-result pointer, which a CHILD had overwritten), the model named the
interrupted root itself and was refused `AUTHORITY_SOURCE_UNAVAILABLE`, and the
retry without a predecessor minted a duplicate root. The list is a HINT; the
door is a predicate: same project, a root, a readable result, not live.
A room once offered exactly ONE predecessor candidate (the project's last-result
pointer, which a CHILD had overwritten), the model named the interrupted root
itself and was refused `AUTHORITY_SOURCE_UNAVAILABLE`, and the retry without a
predecessor minted a duplicate root. Later a coordinator task named the settled
roots of five other projects, each to be continued inside its own project, and
was refused seven times because the door compared the predecessor's project with
the CALLER's room. The list is a HINT; the door is a predicate on the result
itself: settled and readable - never where the caller sits, where the work lands
or whether it is a root's or a helper's; those facts are disclosed in the receipt.
"""
from __future__ import annotations
@ -50,6 +54,30 @@ def _door(ctx, task_id, evt=None):
return _attach_predecessor_authority_from_metadata(ctx, evt if evt is not None else {}, task_id)
def _confirm(monkeypatch, effective_project_id: str = ""):
"""The admission receipt names where the task actually landed."""
monkeypatch.setattr(
"ouroboros.tools.control_events._wait_for_promotion_admission",
lambda *_a, **_k: {"status": "scheduled", "effective_project_id": effective_project_id},
)
_TOWER_POINTER = {
"kind": "task_result", "task_id": "tower-root", "human_label": "another room's work",
"tool": "get_task_result", "arguments": {"task_id": "tower-root", "include_authority": True},
}
def _tower(tmp_path):
"""A second project with one settled root, exactly the shape the coordinator named."""
from ouroboros.projects_registry import create_project
from ouroboros.task_results import write_task_result
create_project(tmp_path, "tower", name="Tower")
write_task_result(tmp_path, "tower-root", "completed", project_id="tower",
objective="another room's work", ts="2026-08-10T00:00:01Z")
def test_a_root_the_room_manifest_lists_is_still_addressable(tmp_path):
"""I29 positive path (owner batch 3, answer 6b=A): the narrowing removed
CHILDREN from the window, and a listed owner root stays promotable."""
@ -133,13 +161,15 @@ def test_a_room_root_older_than_the_list_is_addressable_all_the_same(tmp_path, m
}
def test_a_child_result_is_never_the_continuation_and_the_host_stops_offering_it(tmp_path):
"""A pointer stamped by a child before this release still names a child: the
host offers the ROOT instead, and the door refuses the child if the model names
it all the same, saying where the work is reachable (I29 stays closed)."""
def test_a_helpers_result_is_continued_with_its_root_named_and_never_offered(tmp_path, monkeypatch):
"""A pointer stamped by a child before only roots stamped it still names a child:
the host offers the ROOT (the hint stays roots-only, owner decision 6b=A) and heals
the pointer, while a helper's result the model names on purpose is continued -
the receipt says whose helper it was and where its root is."""
import server
from ouroboros.projects_registry import create_project
from ouroboros.task_results import write_task_result
from ouroboros.tools.control_routing import _promote_chat_to_task
from ouroboros.tools.project_journal import record_project_last_result
project = create_project(tmp_path, "racer", name="Racer")
@ -159,21 +189,74 @@ def test_a_child_result_is_never_the_continuation_and_the_host_stops_offering_it
metadata["project_routing_manifest"]["final_results"]] == ["racer-root"]
evt: dict = {}
refusal = _door(_room_ctx(tmp_path, metadata), "racer-child", evt)
assert "delegated child result" in refusal and "root" in refusal
assert evt == {}
assert _door(_room_ctx(tmp_path, metadata), "racer-root") == ""
assert _door(_room_ctx(tmp_path, metadata), "racer-child", evt) == ""
assert evt["predecessor_task_id"] == "racer-child"
assert evt["predecessor_facts"] == {"project_id": "racer", "helper": True,
"root_task_id": "racer-root", "parent_task_id": "racer-root"}
root_evt: dict = {}
assert _door(_room_ctx(tmp_path, metadata), "racer-root", root_evt) == ""
assert root_evt["predecessor_facts"]["helper"] is False # a root is nobody's helper
_confirm(monkeypatch, effective_project_id="racer")
ctx = _room_ctx(tmp_path, metadata)
out = _promote_chat_to_task(ctx, "Continue the helper's work", workspace="none",
predecessor_task_id="racer-child")
assert out.startswith("OK: task"), out
assert "Note: predecessor racer-child is a delegated helper's result; its root is racer-root." in out
assert "belongs to" not in out # same project: nothing else to disclose
def test_another_projects_root_is_not_this_rooms_continuation(tmp_path):
import server
def test_a_helper_predecessor_is_named_on_the_route_verb_and_without_a_root_id(tmp_path, monkeypatch):
"""The helper note rides both verbs and names what the helper's row knows: its root,
else its parent, else that the root is unknown - it never goes silent on a helper
whose row carries only the subagent role."""
from ouroboros.projects_registry import create_project
from ouroboros.task_results import write_task_result
from ouroboros.tools.control_routing import _promote_chat_to_task, _route_to_project
create_project(tmp_path, "racer", name="Racer")
write_task_result(tmp_path, "racer-child", "completed", project_id="racer", objective="helper work",
parent_task_id="racer-root", root_task_id="racer-root", delegation_role="subagent")
write_task_result(tmp_path, "racer-nested", "completed", project_id="racer", objective="nested helper",
parent_task_id="racer-child", delegation_role="subagent")
write_task_result(tmp_path, "racer-orphan", "completed", project_id="racer", objective="role only",
delegation_role="subagent")
_confirm(monkeypatch, effective_project_id="racer")
routed = _room_ctx(tmp_path, {}, project_id="racer")
out = _route_to_project(routed, "racer", "continue the helper's work", predecessor_task_id="racer-child")
assert out.startswith("✉️ Routed to project 'Racer' (racer)"), out
assert "Note: predecessor racer-child is a delegated helper's result; its root is racer-root." in out
nested = _room_ctx(tmp_path, {}, project_id="racer")
out = _promote_chat_to_task(nested, "Continue the nested helper", workspace="none", predecessor_task_id="racer-nested")
assert "Note: predecessor racer-nested is a delegated helper's result; its parent is racer-child." in out
orphan = _room_ctx(tmp_path, {}, project_id="racer")
out = _promote_chat_to_task(orphan, "Continue the role-only helper", workspace="none", predecessor_task_id="racer-orphan")
assert "Note: predecessor racer-orphan is a delegated helper's result; its root is unknown." in out
def test_a_failed_root_is_a_settled_predecessor(tmp_path):
"""Settled means completed, failed or cancelled: the coordinator's first refused
predecessor had failed at its absolute ceiling and was still the work to continue."""
from ouroboros.task_results import write_task_result
write_task_result(tmp_path, "tower-failed", "failed", project_id="tower",
objective="ran out of ceiling", reason_code="absolute_ceiling")
evt: dict = {}
assert _door(_room_ctx(tmp_path, {}, project_id="racer"), "tower-failed", evt) == ""
assert evt["predecessor_authority_source"]["arguments"] == {"task_id": "tower-failed", "include_authority": True}
def test_another_projects_root_is_continued_from_this_room_and_the_hint_stays_room_local(tmp_path):
"""The room's manifest lists only its own roots - a hint - while the door judges the
root itself: another project's settled root is continued from here with a pointer
rebuilt from the durable result, never from a shown row."""
import server
from ouroboros.projects_registry import create_project
project = create_project(tmp_path, "racer", name="Racer")
create_project(tmp_path, "tower", name="Tower")
write_task_result(tmp_path, "tower-root", "completed", project_id="tower",
objective="another room's work", ts="2026-08-10T00:00:01Z")
_tower(tmp_path)
metadata = server._decision_turn_metadata(
_host_ctx(tmp_path), int(project["chat_id"]), "room-4", {"project_id": "racer"},
@ -181,9 +264,112 @@ def test_another_projects_root_is_not_this_rooms_continuation(tmp_path):
assert metadata["project_routing_manifest"]["final_results"] == []
evt: dict = {}
refusal = _door(_room_ctx(tmp_path, metadata), "tower-root", evt)
assert "not an addressable result in the host routing manifest" in refusal
assert evt == {}
assert _door(_room_ctx(tmp_path, metadata), "tower-root", evt) == ""
assert evt["predecessor_task_id"] == "tower-root"
assert evt["predecessor_authority_source"] == _TOWER_POINTER
assert evt["predecessor_facts"] == {"project_id": "tower", "helper": False, "root_task_id": "", "parent_task_id": ""}
def test_a_pooled_task_continues_another_rooms_root_into_that_room_in_one_hop(tmp_path, monkeypatch):
"""The coordinator shape: a pooled task carries no host manifest at all (its metadata is
the client surface and its own contract), sits in one room, and names a settled root
of another project as the predecessor of work it sends INTO that project. Both verbs
schedule it in one hop, the event carries the rebuilt pointer and nothing else new,
and a landing in the predecessor's own project has nothing to disclose."""
from ouroboros.projects_registry import create_project
from ouroboros.tools.control_routing import _promote_chat_to_task, _route_to_project
create_project(tmp_path, "coord", name="Coordination")
_tower(tmp_path)
metadata = {"client_surface": {"channel": "web"}, "task_contract": {"objective": "coordinate"}}
_confirm(monkeypatch, effective_project_id="tower")
promoted = _room_ctx(tmp_path, metadata, project_id="coord")
out = _promote_chat_to_task(promoted, "Continue the tower work", project_id="tower",
workspace="none", predecessor_task_id="tower-root")
assert out.startswith("OK: task"), out
assert "belongs to" not in out
[evt] = promoted.pending_events
assert evt["project_id"] == "tower"
assert evt["predecessor_task_id"] == "tower-root"
assert evt["predecessor_authority_source"] == _TOWER_POINTER
assert "predecessor_facts" not in evt
routed = _room_ctx(tmp_path, metadata, project_id="coord")
out = _route_to_project(routed, "tower", "continue the tower work", predecessor_task_id="tower-root")
assert out.startswith("✉️ Routed to project 'Tower' (tower)"), out
assert "belongs to" not in out
[evt] = routed.pending_events
assert evt["predecessor_task_id"] == "tower-root"
assert evt["predecessor_authority_source"] == _TOWER_POINTER
assert "predecessor_facts" not in evt
def test_a_landing_outside_the_predecessors_project_is_disclosed_once(tmp_path, monkeypatch):
"""A free choice, said in the receipt like the second-project note: a continuation
landing in another project names the predecessor's own project, on both verbs; one
landing at home carries no such sentence, so the note cannot fire unconditionally."""
from ouroboros.projects_registry import create_project
from ouroboros.tools.control_routing import _promote_chat_to_task, _route_to_project
create_project(tmp_path, "racer", name="Racer")
_tower(tmp_path)
note = "Note: predecessor tower-root belongs to project 'tower'; this continuation runs in project 'racer' (your choice)."
_confirm(monkeypatch, effective_project_id="racer")
ctx = _room_ctx(tmp_path, {}, project_id="racer")
out = _promote_chat_to_task(ctx, "Continue elsewhere", project_id="racer", workspace="none",
predecessor_task_id="tower-root")
assert out.startswith("OK: task") and note in out, out
routed = _room_ctx(tmp_path, {}, project_id="racer")
out = _route_to_project(routed, "racer", "continue elsewhere", predecessor_task_id="tower-root")
assert out.startswith("✉️ Routed to project 'Racer' (racer)") and note in out, out
_confirm(monkeypatch, effective_project_id="tower")
home = _room_ctx(tmp_path, {}, project_id="racer")
out = _promote_chat_to_task(home, "Continue at home", project_id="tower", workspace="none",
predecessor_task_id="tower-root")
assert out.startswith("OK: task") and "belongs to" not in out, out
def test_a_main_root_is_continued_from_a_room_and_its_home_is_named(tmp_path, monkeypatch):
"""A project-less (Main) root was reachable only through the Main lane's list; the door
judges the root, so a room names it too, and the receipt says it comes from Main."""
from ouroboros.projects_registry import create_project
from ouroboros.task_results import write_task_result
from ouroboros.tools.control_routing import _promote_chat_to_task
create_project(tmp_path, "racer", name="Racer")
write_task_result(tmp_path, "main-root", "completed", project_id="", objective="main work")
evt: dict = {}
assert _door(_room_ctx(tmp_path, {}, project_id="racer"), "main-root", evt) == ""
assert evt["predecessor_task_id"] == "main-root" and evt["predecessor_facts"]["project_id"] == ""
_confirm(monkeypatch, effective_project_id="racer")
ctx = _room_ctx(tmp_path, {}, project_id="racer")
out = _promote_chat_to_task(ctx, "Continue the main work here", project_id="racer",
workspace="none", predecessor_task_id="main-root")
assert "predecessor main-root belongs to the main chat; this continuation runs in project 'racer'" in out
def test_a_public_conversation_continues_a_settled_root_and_still_lands_without_a_project(tmp_path, monkeypatch):
"""Presence holds the routing verb under its own ceiling: the door judges the root,
while the promote still strips project, workspace and source - the ceiling is the
caller's, never the predecessor's - and the receipt names where the predecessor lives."""
from ouroboros.tools.control_routing import _promote_chat_to_task
_tower(tmp_path)
_confirm(monkeypatch, effective_project_id="")
ctx = _room_ctx(tmp_path, {"presence": {"binding_id": "b" * 32}}, project_id="")
out = _promote_chat_to_task(ctx, "Continue the tower work", project_id="tower",
workspace="none", predecessor_task_id="tower-root")
assert out.startswith("OK: task"), out
[evt] = ctx.pending_events
assert evt["project_id"] == "" and evt["presence"] == {"binding_id": "b" * 32}
assert evt["predecessor_task_id"] == "tower-root"
assert "predecessor tower-root belongs to project 'tower'; this continuation runs in the main chat" in out
def test_a_live_root_is_steer_territory_not_a_predecessor(tmp_path):
@ -246,9 +432,11 @@ def test_an_unreadable_predecessor_still_answers_authority_source_unavailable(tm
assert not (tmp_path / "task_results" / "racer-gone.json").exists()
def test_outside_a_room_the_host_list_still_decides(tmp_path):
"""The quiet direction of the same guard: with no room project there is no
`same project` to evaluate, so an unlisted id stays unaddressable."""
def test_outside_a_room_an_unlisted_settled_root_is_continued_too(tmp_path):
"""With no room there is no project to compare against, and none is needed: a Main
turn names any settled root, listed (the host's own pointer) or not (a pointer
rebuilt from the durable result); a missing id stays the one case no predicate
can rescue, and it still emits nothing."""
import server
from ouroboros.projects_registry import create_project
from ouroboros.task_results import write_task_result
@ -259,12 +447,18 @@ def test_outside_a_room_the_host_list_still_decides(tmp_path):
metadata = server._decision_turn_metadata(_host_ctx(tmp_path), 1, "main-1", {})
main_ctx = _room_ctx(tmp_path, {"client_message_id": "main-1"}, project_id="")
refusal = _door(main_ctx, "racer-root")
assert "not an addressable result in the host routing manifest" in refusal
evt: dict = {}
assert _door(main_ctx, "racer-root", evt) == ""
assert evt["predecessor_task_id"] == "racer-root"
assert evt["predecessor_authority_source"]["arguments"] == {"task_id": "racer-root", "include_authority": True}
listed_ctx = _room_ctx(tmp_path, metadata, project_id="")
assert _door(listed_ctx, "racer-root") == ""
gone: dict = {}
assert _door(main_ctx, "never-existed", gone) == "the selected predecessor task result is missing or unreadable"
assert gone == {}
def test_only_a_root_finalization_moves_the_projects_pointer(tmp_path):
"""The pointer answers "continue from here" for the ROOM; a child that
@ -297,7 +491,7 @@ def test_only_a_root_finalization_moves_the_projects_pointer(tmp_path):
def test_the_self_heal_scan_never_offers_or_stamps_a_child(tmp_path):
"""The lookup's fallback scan is the pointer's SECOND writer: with no pointer
yet and a child as the project's newest result, it answers with the newest ROOT
and stamps that, never the child the door would refuse."""
and stamps that, never the child the hint does not offer."""
import os
import server

View file

@ -122,7 +122,16 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
# private-snapshot paragraph now states the worktree ops lock's scope (shared
# metadata only, row-then-ref order, batched binary verdict, typed busy refusal)
# — rationale-layer text BIBLE P6 requires.
"docs/architecture/06-agent-core.md": 306000,
# 306000 -> 306800: the predecessor door is a predicate on the result, never on the
# caller's room, the landing project or the root/helper distinction (the disclosed
# notes replace the one-clause pointer to §10); the registry read of the routing
# verbs on a forked execution drive and the predecessor's task files as a lineage
# read are new facts of the paragraphs they extend. The merged base sat 147 bytes
# under the previous budget.
# 306800 -> 307100 (#1247 fix-forward; measured 306832 on the merged chapter): the
# populate sentence names the post-copy stat re-record that keeps a CRLF-converting
# checkout clean.
"docs/architecture/06-agent-core.md": 307100,
"docs/architecture/07-configuration.md": 36991,
# 18947 -> 19287: CI failure collection now documents diagnostic desktop builds while release remains gated.
"docs/architecture/08-git-branching-ci-and-build.md": 19287,
@ -140,7 +149,11 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
# residual sentence of the off-thread invariant; the rule itself has no older text.
# +200 (2026-09-22): invariant 10 names the process-local fingerprint memos
# and their fallback rule; the base sat 23 bytes under the previous budget.
"docs/architecture/10-key-invariants.md": 21300,
# 21300 -> 21700: invariant 27 states the door as a predicate on the root (any actor
# holding a routing verb, any project, a disclosed landing) with the reason the
# room comparison protected nothing; the earlier one-clause form is replaced, and
# the base sat 34 bytes under the previous budget.
"docs/architecture/10-key-invariants.md": 21700,
"docs/architecture/11-frozen-contracts-v1.md": 24194,
# +400 (#1213): Presence turns are named as actors without cross-focus catalogue or focus authority.
"docs/architecture/12-host-service-companions-and-chat-ids.md": 11400,

View file

@ -24,6 +24,38 @@ def _ctx(tmp_path, events=None, *, task_metadata=None, **overrides):
return types.SimpleNamespace(**values)
def test_a_task_on_a_forked_drive_reads_the_registry_from_the_canonical_root(tmp_path, monkeypatch):
"""A promoted task with a workspace runs on a forked execution drive that never
carries ``state/projects.json``; the routing verbs read the registry through the
canonical data root, so such a task lists, routes into and names the owner's
projects. A context without a canonical root still reads its own drive."""
from ouroboros.tools.control_routing import _effective_scope_note, _promote_chat_to_task
create_project(tmp_path, "racer", name="Racer")
child = tmp_path / "state" / "headless_tasks" / "fork-1" / "data"
child.mkdir(parents=True)
forked = _ctx(child, task_metadata={"budget_drive_root": str(tmp_path)}, budget_drive_root=str(tmp_path))
assert "racer — Racer" in _list_projects(forked)
out = _route_to_project(forked, "racer", "continue the engine tuning", predecessor_task_id="")
assert out.startswith("⚠️ ROUTE_UNCONFIRMED:"), out
assert forked.pending_events[0]["project_id"] == "racer"
assert _effective_scope_note(forked, "racer") == " in project 'Racer' (racer)"
monkeypatch.setattr(
"ouroboros.tools.control_events._wait_for_promotion_admission",
lambda *_a, **_k: {"status": "scheduled", "effective_project_id": "racer"},
)
promoted = _ctx(child, task_metadata={"budget_drive_root": str(tmp_path)}, budget_drive_root=str(tmp_path))
out = _promote_chat_to_task(promoted, "tune the engine", project_id="racer", workspace="none", predecessor_task_id="")
assert out.startswith("OK: task") and "in project 'Racer' (racer)" in out, out
# The quiet direction: no canonical root at all means the task's own drive is the registry.
own_drive = _ctx(child)
assert _list_projects(own_drive).startswith("No projects yet")
assert "target_not_found" in _route_to_project(own_drive, "racer", "msg", predecessor_task_id="")
def test_route_to_existing_project_emits_event_and_receipt(tmp_path):
create_project(tmp_path, "racer", name="Racer")
# The origin identity is captured at INGRESS and rides task_metadata by

View file

@ -22,7 +22,7 @@ import time
import pytest
from ouroboros import artifacts, subagent_worktrees as wt, workspace_patch_capture as capture
from ouroboros.platform_layer import _lock_identity
from ouroboros.platform_layer import _lock_identity, pid_is_alive
from tests._delegated_transport_shared import _owned_gateway_uses_each_test_transport # noqa: F401
from tests.test_delegated_full_access import full_run # noqa: F401
from tests.test_delegated_run_isolation import _git, _nanny_ctx, _seed_target
@ -147,32 +147,49 @@ def test_a_parked_provision_does_not_block_another(tmp_path, monkeypatch, same_t
_HOLDER = """
import os, pathlib, sys, time
sys.path.insert(0, sys.argv[3])
import os, pathlib, sys
sys.path.insert(0, sys.argv[2])
from ouroboros.platform_layer import acquire_exclusive_file_lock
fd = acquire_exclusive_file_lock(
pathlib.Path(sys.argv[1]), timeout_sec=5, stale_sec=600,
metadata=f"pid={os.getpid()} task=t-holder op=provision since=2026-09-24T00:00:00Z target=/tmp/with space")
assert fd is not None
print("HELD", flush=True)
time.sleep(float(sys.argv[2]))
print("HELD", os.getpid(), flush=True)
sys.stdin.readline() # hold until the test closes our stdin (or kills us)
"""
def _hold_lock(snaps: pathlib.Path, seconds: float) -> subprocess.Popen:
def _hold_lock(snaps: pathlib.Path) -> "tuple[subprocess.Popen, int]":
"""A live lock holder in another process; returns it with the pid the holder
itself wrote into the lock (on Windows a venv ``python.exe`` is a launcher whose
CHILD is the interpreter, so ``proc.pid`` is not that pid)."""
snaps.mkdir(parents=True, exist_ok=True)
proc = subprocess.Popen(
[sys.executable, "-c", _HOLDER, str(snaps / wt._LOCK_NAME), str(seconds), str(REPO)],
stdout=subprocess.PIPE, text=True)
assert proc.stdout.readline().strip() == "HELD"
return proc
[sys.executable, "-c", _HOLDER, str(snaps / wt._LOCK_NAME), str(REPO)],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
banner = proc.stdout.readline().split()
assert banner[:1] == ["HELD"], banner
return proc, int(banner[1])
def _release_holder(proc: subprocess.Popen, holder_pid: int) -> None:
proc.kill()
proc.stdin.close() # the interpreter behind a launcher exits on EOF too
proc.wait()
# Wait for the HOLDER (not only the launcher) to be gone: it may still hold
# the OS-level lock for a moment after EOF, and the dead-holder phase below
# rewrites the lock file by hand.
deadline = time.monotonic() + 10
while pid_is_alive(holder_pid) and time.monotonic() < deadline:
time.sleep(0.05)
assert not pid_is_alive(holder_pid)
def test_a_live_holder_is_a_typed_refusal_and_a_dead_holder_is_evicted(tmp_path, monkeypatch):
target = _seed_target(tmp_path)
snaps, data = tmp_path / "snaps", tmp_path / "data"
monkeypatch.setattr(wt, "_LOCK_TIMEOUT_SEC", 0.5)
holder = _hold_lock(snaps, 30)
holder, holder_pid = _hold_lock(snaps)
try:
started = time.monotonic()
with pytest.raises(wt.WorktreeOpsLockBusy) as info:
@ -181,10 +198,9 @@ def test_a_live_holder_is_a_typed_refusal_and_a_dead_holder_is_evicted(tmp_path,
# refusal arrives after the one wait, not after a second discard wait.
assert time.monotonic() - started < 3
finally:
holder.kill()
holder.wait()
_release_holder(holder, holder_pid)
busy = info.value
assert busy.holder == {"pid": str(holder.pid), "task": "t-holder", "op": "provision",
assert busy.holder == {"pid": str(holder_pid), "task": "t-holder", "op": "provision",
"since": "2026-09-24T00:00:00Z", "target": "/tmp/with space"}
assert busy.waited_sec > 0 and "t-holder" in str(busy)
# Nothing was registered, pinned or checked out for the refused attempt.
@ -423,11 +439,20 @@ def test_a_failure_after_the_provisional_row_leaves_nothing_and_a_crash_is_gc_re
assert not list(snaps.glob("dlg_*"))
def test_populate_matches_worktree_add_and_runs_no_target_hook(tmp_path, monkeypatch):
@pytest.mark.parametrize("autocrlf", [False, True])
def test_populate_matches_worktree_add_and_runs_no_target_hook(tmp_path, monkeypatch, autocrlf):
"""``worktree add --no-checkout`` + ``reset --hard --no-recurse-submodules`` is what
git's own ``worktree add`` runs — a target with ``submodule.recurse=true`` must
still snapshot — minus the target's post-checkout hook, which no longer executes
project-authored code at provision."""
project-authored code at provision. Under ``core.autocrlf=true`` (Git for
Windows' default) the checkout writes CRLF and the raw-bytes copy restores the
source's LF, so the copied entries' stat must be re-recorded or every such file
reads as modified in the child's ``git status`` (CI on windows-latest, #1247)."""
# Both legs pin the setting explicitly: on windows-latest the system config
# already says true, so an unset "False" leg would not be the working side.
config = tmp_path / "gitconfig"
config.write_text(f"[core]\n\tautocrlf = {'true' if autocrlf else 'false'}\n", encoding="utf-8")
monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(config))
sub = tmp_path / "sub"
sub.mkdir()
_git(sub, "init", "-q")
@ -457,6 +482,7 @@ def test_populate_matches_worktree_add_and_runs_no_target_hook(tmp_path, monkeyp
assert (exec_root / "vendored").is_dir() and (exec_root / "tracked.txt").read_text(encoding="utf-8") == "one\ntwo\n"
assert not (exec_root / ".hook_ran").exists() and not (target / ".hook_ran").exists()
assert _git(exec_root, "status", "--porcelain").stdout == ""
assert (exec_root / ".gitmodules").read_bytes() == (target / ".gitmodules").read_bytes()
assert "160000" in _git(exec_root, "ls-files", "-s", "vendored").stdout
# The guard: without --no-recurse-submodules the populate fails on this target.
@ -483,7 +509,7 @@ def test_snapshot_facts_ride_the_start_receipt_as_disclosure_only(tmp_path):
assert _snapshot_facts(None) == {}
# Facts, not a threshold: no runtime module consumes them to refuse or truncate.
consumers = sorted(
str(path.relative_to(REPO)) for path in (REPO / "ouroboros").rglob("*.py")
path.relative_to(REPO).as_posix() for path in (REPO / "ouroboros").rglob("*.py")
if "provisioning_sec" in path.read_text(encoding="utf-8"))
assert consumers == ["ouroboros/subagent_worktrees.py", "ouroboros/tools/delegate.py",
"ouroboros/tools/delegate_integration.py"]