ouroboros/tests/test_tool_classification_differential.py
Ouroboros 95078b23bc fix: recover callable tool names on exact catalog misses
Provide scoped MCP raw-to-wire discovery, pure pre-safety name resolution, and a shared policy-filtered refusal path for tool namespaces. Preserve exact dispatch and extension adoption; cover real registry consumers and classification.
2026-09-25 03:46:59 +03:00

745 lines
56 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""The single classifier answers exactly what the retired loop pair answered,
except on a table of deltas the owner approved.
The oracle is a golden snapshot of the OLD pair's answers, captured from the tree
named in ``GOLDEN_SOURCE_SHA``, not a copy of the old parser: a copy is dead code
that invites cleanup, a data file cannot drift silently.
Both directions fail. An unapproved divergence fails because the cutover would then
be changing behaviour nobody signed off. An APPROVED delta that no longer fires ALSO
fails, so the table cannot rot into a permanent excuse list — once a delta is gone,
its row goes with it.
"""
from __future__ import annotations
import json
import pathlib
from types import MappingProxyType
from typing import Mapping, NamedTuple
import pytest
from ouroboros._outcome_tool_errors import (
_BLOCKING_TOOL_STATUSES,
_NON_BLOCKING_READONLY_BLOCK_STATUSES,
_NON_BLOCKING_RECOVERABLE_STATUSES,
_OK_TOOL_STATUSES,
_POLICY_DENIAL_STATUSES,
_UNPARTITIONED_BUCKETS,
)
from ouroboros.loop_tool_execution import _typed_execution_failure, _typed_result_metadata
from ouroboros.tools.tool_result import TOOL_CODE_SPECS, LegacyTextResultAdapter
from tests.tool_classification_corpus import (
Case,
GOLDEN_SOURCE_SHA,
_MARKER_RE,
build_corpus,
harvested_identifiers,
harvested_native_codes,
typed_result,
)
GOLDEN_PATH = pathlib.Path(__file__).resolve().parent / "fixtures" / "legacy_tool_classification_0f715831.json"
class Delta(NamedTuple):
old_is_error: bool
old_status: str
new_is_error: bool
new_status: str
owner_item: str
reason: str
# Every entry traces to a numbered item of the delta list the owner approved
# (batch #3 answer 1=A, batch #4 answers 1-3). Nothing else may differ.
APPROVED_DELTAS: Mapping[str, Delta] = MappingProxyType({
# v7next F3.1 note: the reference table also carried rows for producers this
# lane did NOT cut over (the in-place core.py rows 2083-2086 — room writes,
# forward_to_worker, memory REJECTED — and the control_routing/control_runtime
# rows: swarm scope, steer/routing receipts, task-cancel pending). Their rows
# were deleted per this test's own unfired-rows rule and RETURN with those
# producers' cutover.
"ACCESS_DENIED": Delta(False, "ok", True, "blocked", "A.4", "an access denial recorded as success is the worst under-reporting"),
"ACTING_SUBAGENT_TOOL_NOT_GRANTED": Delta(False, "ok", True, "blocked", "A.4", "an ungranted tool for the acting subagent is a denial"),
"CANCEL_INTENT_PROJECTION_CORRUPT": Delta(False, "ok", True, "error", "A.5", "a corrupted cancel projection is an error, not a success"),
"CAPABILITY_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"CHILD_RESULT_LINEAGE_FORBIDDEN": Delta(False, "ok", True, "blocked", "A.4", "a refused child-result lineage is a denial"),
"CI_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"COGNITIVE_TOOL_REQUIRED": Delta(True, "cognitive_tool_required", False, "ok", "A.11", "owner batch #4: the cognitive redirect is a hint, the error flag is removed"),
"EXECUTOR_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
# This tree's post-cutoff producers emit five more *_UNAVAILABLE identifiers
# the reference corpus never saw; each is the same A.18 move (the generic
# `_UNAVAILABLE` marker keeps its own status instead of the coarse error).
"AUTHORITY_SOURCE_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"REVIEW_BINDING_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"REVIEW_STATE_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"SAFETY_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"SUBAGENT_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"EXTRACT_VIDEO_FRAMES_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"GH_TARGET_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"GIT_BRANCH_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"GH_TIMEOUT": Delta(False, "ok", True, "timeout", "A.2", "an expired GitHub operation is a timeout, not a success"),
"GIT_ERROR": Delta(False, "error", False, "git_error", "A.17", "the version-control refusal gets its own bucket; is_error is unchanged"),
"INVALID_ARG": Delta(False, "ok", True, "argument_error", "A.6", "a bad pull-request argument is an error, not a success"),
"MANAGED_UPDATE_STATE_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"MCP_DISABLED": Delta(False, "ok", True, "unavailable", "A.3", "an MCP provider that is off is unavailable, not a success"),
"MCP_TOOL_DISALLOWED": Delta(False, "ok", True, "blocked", "A.3", "an MCP tool refused by policy is a denial, not a success"),
"MCP_TOOL_ERROR": Delta(True, "error", True, "mcp_error", "A.17", "the MCP error gets its own bucket, homed to the blocking partition"),
# Owner decision on #1262 (item 3): a name the current MCP catalog does not list
# is the caller's unknown tool, not a provider outage; a known disabled server
# or an unlisted catalog keeps `unavailable` (MCP_DISABLED, MCP_CATALOG_UNAVAILABLE).
"MCP_TOOL_NOT_FOUND": Delta(False, "ok", True, "unknown_tool", "A.1262.3", "a name absent from the MCP catalog is an unknown tool, not a success and not an outage"),
"MCP_TOOL_TIMEOUT": Delta(False, "ok", True, "timeout", "A.3", "an expired MCP call is a timeout, not a success"),
"MUTATIVE_SUBAGENTS_DISABLED": Delta(False, "ok", True, "blocked", "A.4", "a disabled mutative subagent is a denial"),
"OCR_PDF_SCANNED_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"OCR_PDF_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"PYTHON_INTERPRETER_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"REVIEW_BLOCKED": Delta(False, "blocked", False, "review_blocked", "A.17", "the review refusal gets its own bucket; is_error is unchanged"),
"SKILLS_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"SKILL_EXEC_TIMEOUT": Delta(False, "ok", True, "timeout", "A.2", "an expired skill run is a timeout, not a success"),
"TASK_FORBIDDEN": Delta(False, "ok", True, "blocked", "A.4", "a forbidden task surface is a denial"),
"TOOL_ARG_ERROR": Delta(True, "error", True, "argument_error", "A.17", "the argument error gets its own bucket, homed to the blocking partition"),
"VIEW_IMAGE_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"YOUTUBE_TRANSCRIPT_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"body:empty": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"body:list": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"body:nested_only": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"body:prose": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"body:string_false": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"body:true": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"compose:exit:route+safety": Delta(False, "ok", True, "non_zero_exit", "A.7", "the safety wrapper no longer masks what it wraps"),
"compose:exit:safety": Delta(False, "ok", True, "non_zero_exit", "A.7", "the safety wrapper no longer masks what it wraps"),
"compose:integrate:route+safety": Delta(False, "ok", True, "integration_blocked", "A.7", "the safety wrapper no longer masks what it wraps"),
"compose:integrate:safety": Delta(False, "ok", True, "integration_blocked", "A.7", "the safety wrapper no longer masks what it wraps"),
"compose:protected:route+safety": Delta(False, "ok", True, "protected_blocked", "A.7", "the safety wrapper no longer masks what it wraps"),
"compose:protected:safety": Delta(False, "ok", True, "protected_blocked", "A.7", "the safety wrapper no longer masks what it wraps"),
"compose:reported:safety": Delta(False, "ok", True, "tool_reported_failure", "A.7", "the safety wrapper no longer masks what it wraps"),
"compose:timeout:route+safety": Delta(False, "ok", True, "timeout", "A.7", "the safety wrapper no longer masks what it wraps"),
"compose:timeout:safety": Delta(False, "ok", True, "timeout", "A.7", "the safety wrapper no longer masks what it wraps"),
"compose:violation:route+safety": Delta(False, "ok", True, "safety_violation", "A.7", "the safety wrapper no longer masks what it wraps"),
"compose:violation:safety": Delta(False, "ok", True, "safety_violation", "A.7", "the safety wrapper no longer masks what it wraps"),
"edge:autocorrect_line2": Delta(True, "shell_error", True, "non_zero_exit", "A.13", "the wrapper body is classified by its own first line, so the exit error is named precisely"),
"edge:autocorrect_line3": Delta(True, "shell_error", False, "ok_autocorrected", "A.13", "the loop's whole-remainder scan matched a marker three lines down; the body's first line governs"),
"edge:safety_inner_block": Delta(False, "ok", True, "resource_policy_blocked", "A.7", "the safety wrapper no longer masks what it wraps"),
"edge:unknown_tool": Delta(False, "ok", True, "unknown_tool", "A.1", "a call to a tool that does not exist was never a success"),
"envelope:empty": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"envelope:false": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"envelope:false_indented": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"envelope:list": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"envelope:nested_only": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"envelope:prose": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"envelope:string_false": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"envelope:true": Delta(False, "ok", False, "untyped", "A.17", "a dynamic provider body is untyped rather than assumed ok; is_error is unchanged"),
"native:ACCESS_BLOCKED:ACTING_SUBAGENT_TOOL_NOT_GRANTED": Delta(False, "ok", True, "blocked", "A.4", "same denial through its native code"),
# Same shape again: the `MUTATIVE_SUBAGENTS_DISABLED` identifier is already
# approved above under A.4, and the two subagent-constraint guards in
# `control_scheduling` now publish the code the adapter already assigned to
# their text. The golden is the RETIRED LOOP, so the same answer reached
# through the producer's own code shows up as a second row.
"native:ACCESS_BLOCKED:MUTATIVE_SUBAGENTS_DISABLED": Delta(False, "ok", True, "blocked", "A.4", "same denial as the MUTATIVE_SUBAGENTS_DISABLED identifier row, through the native code the subagent-constraint guards publish"),
"native:ACCESS_BLOCKED:MANAGED_UPDATE_IN_PROGRESS": Delta(False, "ok", True, "blocked", "A.4", "the managed-update denial is an access block its text never marked"),
"native:CAPABILITY_UNAVAILABLE:CAPABILITY_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"native:CAPABILITY_UNAVAILABLE:MANAGED_UPDATE_STATE_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
"native:CAPABILITY_UNAVAILABLE:PYTHON_INTERPRETER_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.18", "unavailability gets its own status name; the report bucket is unchanged"),
# Not a new owner decision: the identical move is already approved above for the
# `TOOL_ARG_ERROR` identifier, and the root-argument refusal in
# `core_file_tools._access_or_block` now publishes the code the adapter already
# assigned to its text. The golden is the RETIRED LOOP, so reaching the same
# answer through the producer's own code shows up as a second row.
"native:TOOL_ARG_ERROR:TOOL_ARG_ERROR": Delta(True, "error", True, "argument_error", "A.17", "same bucket as the TOOL_ARG_ERROR identifier row, through the native code the read/list/write/edit/search root guard publishes"),
# GitHub target refusals use the existing argument-error contract (A.6).
# The retired text classifier considered both warnings successful; the
# publisher now supplies the typed refusal while preserving the message.
"native:TOOL_ARG_ERROR:GH_TARGET_INVALID": Delta(False, "ok", True, "argument_error", "A.6", "an invalid repo argument prevents the GitHub operation; the typed result must report that refusal"),
"native:TOOL_ARG_ERROR:GH_TARGET_REQUIRED": Delta(False, "ok", True, "argument_error", "A.6", "a missing repo argument in a fileless Project prevents the GitHub operation; the typed result must report that refusal"),
# Same shape, same reason: `TASK_FORBIDDEN` is already approved above under A.4,
# and `forward_to_worker` now publishes the code the adapter gave that text.
# Owner batch #10 item 2 (A.20): refusing a write because the room's files belong
# to a promoted task is a policy denial, and it is answered by the tool whose
# family the caller asked for, so each surface keeps its own bucket.
# Owner batch #10 item 3 (A.20): forward_to_worker reported `ok` for every message
# it did NOT deliver. A worker that is gone or finished is an unavailable target;
# a worker being torn down refuses by policy. TASK_FORBIDDEN was already blocked.
# Owner item A.21 (batch #13): the control tools reported `ok` for routing they
# REFUSED or could not confirm. A rejected admission, a steer the supervisor
# declined and a Swarm scope denial are policy denials; an unconfirmed receipt
# is exactly the `unavailable` it describes, because the work may or may not
# exist and the caller must not report it as done. Every sentence is unchanged.
# A.21, the memory writers: `REJECTED` ends in none of the suffixes the family
# chain reads, so a scratchpad or identity write that was refused for a
# malformed argument reported ok — the one answer that tells the caller its
# arguments were fine.
"shape:cognitive_redirect": Delta(True, "cognitive_tool_required", False, "ok", "A.11", "owner batch #4, through the native producer"),
"shape:ephemeral_turn_denial": Delta(False, "ok", True, "blocked", "A.4",
"the decision-turn denial is an access block its own first line never marked"),
"shape:executor_crash": Delta(True, "error", True, "executor_error", "A.17", "the executor crash gets its own bucket, homed to the blocking partition"),
"shape:git_error_untyped_text": Delta(False, "ok", False, "git_error", "A.17", "the version-control refusal keeps its own bucket even when its unmarked text read as ok to the retired text chain"),
"shape:mcp_provider_error": Delta(False, "ok", True, "mcp_error", "A.3",
"the provider error was already typed; only the status beside it said success"),
"shape:review_blocked_untyped_text": Delta(False, "ok", False, "review_blocked", "A.17", "the review refusal keeps its own bucket even when its unmarked text read as ok to the retired text chain"),
# Producers whose TEXT IS ASSEMBLED AT RUNTIME. The static harvest pairs a code
# with a first line only when that line is a literal, so until the shapes below
# entered the corpus these eight status changes were real and invisible: the
# differential could not have failed on them, and neither could a mutation that
# moved one of these codes to another status.
"shape:extension_handler_error": Delta(True, "error", True, "extension_error", "A.17", "the extension error gets its own bucket, homed to the blocking partition"),
"shape:extension_async_timeout": Delta(True, "error", True, "timeout", "A.18", "an expired extension handler is named a timeout; the report bucket is unchanged"),
"shape:extension_not_live": Delta(True, "error", True, "unavailable", "A.18", "an extension that may not dispatch is unavailable; the report bucket is unchanged"),
"shape:mcp_disabled": Delta(False, "ok", True, "unavailable", "A.3", "same fix as MCP_DISABLED, through the native code the provider publishes"),
"shape:mcp_tool_not_found": Delta(False, "ok", True, "unknown_tool", "A.1262.3", "same fix as MCP_TOOL_NOT_FOUND, through the native code the provider publishes"),
"shape:mcp_transport_timeout": Delta(False, "ok", True, "timeout", "A.3", "same fix as MCP_TOOL_TIMEOUT, through the native code the provider publishes"),
"shape:unknown_tool_extension_down": Delta(False, "ok", True, "unavailable", "A.1",
"a call to a tool whose extension is not live was never a success; the registry publishes the more precise `unavailable` rather than `unknown_tool`"),
"shape:binding_arg_error": Delta(True, "error", True, "argument_error", "A.17", "same bucket as TOOL_ARG_ERROR, through the interpolated binding-error text"),
# Owner batch #10 item 1 (A.20): a media delivery that queued NOTHING reported
# `ok` on both axes, because its refusal sentence has no identifier for the
# adapter to key on. The three surfaces now name the failure themselves.
"shape:send_photo_no_chat": Delta(False, "ok", True, "unavailable", "A.20", "no owner chat to deliver into is an unavailable surface, not a sent photo"),
"shape:send_video_no_chat": Delta(False, "ok", True, "unavailable", "A.20", "no owner chat to deliver into is an unavailable surface, not a sent video"),
"shape:send_file_no_chat": Delta(False, "ok", True, "unavailable", "A.20", "no owner chat to deliver into is an unavailable surface, not a sent file"),
"shape:send_photo_read_failure": Delta(False, "ok", True, "error", "A.20", "an image the tool could not read was never delivered"),
"shape:send_photo_empty_payload": Delta(False, "ok", True, "error", "A.20", "an empty payload was never delivered"),
"shape:send_video_missing_file": Delta(False, "ok", True, "error", "A.20", "a missing video file was never delivered"),
"shape:send_file_missing_argument": Delta(False, "ok", True, "error", "A.20", "a call with no file_path delivered nothing"),
# A.21 control refusals stop reporting ok. These four sentences are the ones no
# harvest can reach: the two promotion receipts carry no warning marker for the
# identifier scan, and the two project-routing receipts reach their result
# through the swarm-handoff latch, so no (code, first line) pair exists either.
# A.21 continued, 14.09 receipt incident: these four rows used to be answered by
# the code the corpus DECLARED for them, and the declared codes were invented —
# every one of these producers returns a plain string. Classified for real, the
# family lands in `tool_reported_failure` beside the steer receipts below: the
# agent SEES the refusal, and the host's "no" does not degrade execution health.
# The retired pair answers a `⚠️ X_REJECTED` / `⚠️ X_UNCONFIRMED` first line the
# same generic ok it gave the markerless promotion sentences (the route rows,
# which carried the marker at capture time, hold that evidence), so the recorded
# golden answers still describe the promotion texts that now carry the marker.
"shape:promote_rejected": Delta(False, "ok", True, "tool_reported_failure", "A.21", "a promotion the supervisor refused created no task"),
"shape:promote_unconfirmed": Delta(False, "ok", True, "tool_reported_failure", "A.21", "an unconfirmed admission must not be reported as a created task"),
"shape:route_rejected": Delta(False, "ok", True, "tool_reported_failure", "A.21", "a project route the supervisor refused scheduled nothing"),
"shape:route_unconfirmed": Delta(False, "ok", True, "tool_reported_failure", "A.21", "an unconfirmed project route must not be reported as routed"),
# The same family through its IDENTIFIERS: `ROUTE_*`/`ROUTING_UNCONFIRMED`/
# `NEEDS_MANUAL_TARGET` end in none of the suffixes the generic marker chain
# reads, so a route that dispatched nothing and a picker card that replaced the
# route both reported an ordinary warning on a successful call.
"ROUTE_REJECTED": Delta(False, "ok", True, "tool_reported_failure", "A.21", "a route the supervisor refused is a refusal the agent must see, not a routed task"),
"ROUTE_UNCONFIRMED": Delta(False, "ok", True, "tool_reported_failure", "A.21", "a route with no confirmed receipt did not provably schedule anything"),
"ROUTING_UNCONFIRMED": Delta(False, "ok", True, "tool_reported_failure", "A.21", "an unconfirmed manual-target delivery dispatched no route and offered no options"),
"NEEDS_MANUAL_TARGET": Delta(False, "ok", True, "tool_reported_failure", "A.21", "a routing act that ended in a picker card dispatched no route"),
# A.21 across the remaining control leaves. Same blindness as above: a
# markerless sentence, or one that reaches its result through a helper the
# publication wraps, has no (code, first line) pair to harvest.
"shape:deep_self_review_unavailable": Delta(False, "ok", True, "unavailable", "A.21", "a deep self-review nobody can run is an unavailable capability, not a queued review"),
"shape:scratchpad_legacy_upgrade": Delta(False, "ok", True, "blocked", "A.21", "a scratchpad that needs a manual upgrade refused the append"),
"shape:proactive_message_no_chat": Delta(False, "ok", True, "argument_error", "A.21", "no chat to deliver into means nothing was queued"),
"shape:proactive_message_empty": Delta(False, "ok", True, "argument_error", "A.21", "an empty message was never queued"),
"shape:switch_model_unknown": Delta(False, "ok", True, "argument_error", "A.21", "an unknown model name switched nothing"),
"shape:subtask_depth_limit": Delta(False, "ok", True, "resource_constraint_blocked", "A.21", "a child beyond the depth limit was never scheduled; the limit is the constraint"),
"shape:subagent_capability_mismatch": Delta(False, "ok", True, "argument_error", "A.21", "a profile that cannot satisfy the declared capabilities scheduled no child"),
"shape:task_result_unknown_id": Delta(False, "ok", True, "unavailable", "A.21", "an id this tree never registered has no result to read"),
# Owner item A.22 — owner decision 2026-08-19 ("B": schedule_followup publishes
# native typed ToolResult; golden/corpus regeneration sanctioned). The adopted
# one-shot follow-up tool reported `ok` for every registration it REFUSED,
# because its sentences carry no identifier the adapter can key on: the same
# defect as A.21, on the tool that mints FUTURE ROOT TASKS, where "registered"
# and "refused" reading alike is the most expensive confusion in the family —
# an agent told to wait for an instant that will never come. The producer names
# each failure itself; every sentence is byte-identical.
"shape:followup_subagent_refused": Delta(False, "ok", True, "blocked", "A.22", "a delegated subagent may not mint future root tasks; the denial was reported as a registration"),
"shape:followup_task_id_required": Delta(False, "ok", True, "unavailable", "A.22", "no real task to own the durable record is an unavailable surface, not a scheduled follow-up"),
"shape:followup_run_at_invalid": Delta(False, "ok", True, "argument_error", "A.22", "an unparseable run_at scheduled nothing"),
"shape:followup_objective_required": Delta(False, "ok", True, "argument_error", "A.22", "an empty objective scheduled nothing"),
"shape:followup_text_too_long": Delta(False, "ok", True, "argument_error", "A.22", "an over-limit objective or context is refused whole — never truncated, never scheduled"),
"shape:followup_data_root_unresolved": Delta(False, "ok", True, "error", "A.22", "a drive root that could not be resolved wrote no record"),
"shape:followup_cap_reached": Delta(False, "ok", True, "resource_constraint_blocked", "A.22", "a follow-up beyond the per-task cap was never registered; the cap is the constraint"),
"shape:followup_persist_failed": Delta(False, "ok", True, "error", "A.22", "a follow-up the table refused to store does not exist"),
# A.23 — F6 rolling upstream sync (drift b9f7597f..8d13373b). Upstream's #440
# fix-forward added both browser markers to its failure-prefix table; the
# typed organ lands the same judgment as exact identifier codes. The golden
# tree predates the markers entirely, so its answer is the generic ok.
"BROWSER_SESSION_RETIRED": Delta(False, "ok", True, "timeout", "A.23", "an abandoned call's void result is the call's own timeout, never content"),
"BROWSER_BACKLOG_RETIRED_SESSIONS": Delta(False, "ok", True, "unavailable", "A.23", "the hung-session backlog refusal is browser unavailability, not a successful page read"),
# A.23 — the escalate verb landed with the F6 sync; its route-down refusal
# carries the `_UNAVAILABLE` marker, which the typed chain names as the
# unavailability it is (the retired pair collapsed it into generic error).
"ESCALATE_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.23", "an escalation route that cannot accept the question is an unavailable surface; error hid the retriable class"),
# A.24 — F6 rolling upstream sync #2 (drift 8d13373b..f3fbfdbb, #447 H1/В12).
# A structured `{"ok": false}` answer behind an appended host note used to
# read as SUCCESS: the retired pair json.loads()-ed the WHOLE composed text,
# which a trailing note makes unparseable. The composed payload is recovered
# before parsing now, so the tool's own self-report is believed again — the
# same defect class the 329 OSWorld rows measured, on the composition seam.
"compose:reported:route": Delta(False, "ok", True, "tool_reported_failure", "A.24", "a tool that reported its own failure is a failure, even behind an appended host note"),
"compose:reported:route+safety": Delta(False, "ok", True, "tool_reported_failure", "A.24", "a tool that reported its own failure is a failure, even behind two appended host notes"),
# A.25 — cross-focus publication refusals. The retired text chain only
# recognized the generic *_UNAVAILABLE suffix; stale/liveness names were
# warnings, while a TOOL_ prefix was still a generic execution failure. The one identifier register now recovers
# the producer's substrate/policy split as typed results: an unavailable
# target or projection is policy-denied availability, while a stale or
# unauthorized publication is an explicit policy block.
"FOCUS_PROJECTION_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.25", "a direct focus projection the host cannot accept is unavailable, not a generic execution error"),
"FOCUS_TASK_NOT_LIVE": Delta(False, "ok", True, "unavailable", "A.25", "a focus update for a settled task has no live publication target"),
"FOCUS_STALE": Delta(False, "ok", True, "blocked", "A.25", "a newer focus wins the CAS and blocks the stale publication"),
"FOCUS_SOURCE_UNRESOLVED": Delta(False, "ok", True, "unavailable", "A.25", "a focus source the named reader refused or cannot answer is unavailable evidence, not a published focus"),
"FOCUS_SOURCE_UNRETAINED": Delta(False, "ok", True, "unavailable", "A.25", "a focus whose source answer could not be stored has no retained evidence to publish"),
"TOOL_FORBIDDEN": Delta(True, "error", True, "blocked", "A.25", "an unauthorized project/focus operation is a policy denial, not a generic tool failure"),
# Owner's recovered transport WORK-ORDER B7 / #744: these producers now
# publish existing codes for known refusals. No text-adapter policy changed.
"native:LEGACY_BLOCKED:CHILD_RESULT_STALE": Delta(False, "ok", True, "blocked", "A.B7", "join_ledger refuses a disposition when the inspected child result changed"),
"native:LEGACY_BLOCKED:TASK_CANCEL_PENDING": Delta(False, "ok", True, "blocked", "A.B7", "forward_to_worker refuses a new steering write during cancellation"),
"native:LEGACY_BLOCKED:TASK_NOT_ACTIVE": Delta(False, "ok", True, "blocked", "A.B7", "forward_to_worker refuses delivery to a task that is not running"),
"native:LEGACY_UNAVAILABLE:CHILD_RESULT_STALE": Delta(False, "ok", True, "unavailable", "A.B7", "join_ledger has no current child result to bind, unlike a changed result's policy denial"),
"native:LEGACY_UNAVAILABLE:TASK_NOT_FOUND": Delta(False, "ok", True, "unavailable", "A.B7", "forward_to_worker has no registered target for this task id"),
"native:TOOL_ARG_ERROR:CHILD_RESULT_DISPOSITION_INVALID": Delta(False, "ok", True, "argument_error", "A.B7", "join_ledger rejects malformed disposition arguments before recording them"),
# Owner item I23: a typed refusal must be recorded as a refusal, not as ok.
# The single form already publishes the typed argument error above; the batch
# envelope, the per-entry rejections and the ledger-append path return the
# same sentence as a PLAIN STRING, so only the identifier table reaches them
# (and the stored traces of every past refusal). The register is BY CODE, so
# one row covers all four producers. CHILD_RESULT_DISPOSITION_PARTIAL keeps
# its warning: those entries did record.
"CHILD_RESULT_DISPOSITION_INVALID": Delta(False, "ok", True, "argument_error", "A.I23", "a disposition the ledger refused to record is an argument error, not a success"),
# The refused steer is the owner's own answer (batch #3, 6g = A): the agent
# SEES the refusal (is_error, and the policy-denial bucket tool_reported_failure
# already routes to), while the execution health axis stays undegraded because
# the refusal is not the agent's failure. Nothing new is declared: that status
# has been a non-failure for the ledger since v6.83.0.
"STEER_REJECTED": Delta(False, "ok", True, "tool_reported_failure", "A.I23", "a steer the host refused is a refusal the agent must see, not a delivered message"),
"STEER_UNCONFIRMED": Delta(False, "ok", True, "tool_reported_failure", "A.I23", "a steer with no confirmed receipt did not provably arrive, so it is not a success"),
"native:TOOL_ARG_ERROR:ERROR": Delta(False, "ok", True, "argument_error", "A.B7", "both commit entry points reject an empty commit message before attempting a commit"),
"native:TOOL_ARG_ERROR:REJECTED": Delta(False, "ok", True, "argument_error", "A.B7", "scratchpad and identity writers reject empty or malformed content before writing"),
"native:TOOL_ERROR:TASK_MESSAGE_UNWRITTEN": Delta(False, "ok", True, "error", "A.B7", "forward_to_worker failed to persist the requested message"),
})
# Deltas the classifier WOULD produce for which no producer exists, recorded so a
# later reader can tell "checked, unreachable" from "missed". Neither can appear in
# APPROVED_DELTAS: that table fails on rows that do not fire, and these cannot fire.
_DELTAS_WITHOUT_A_PRODUCER: Mapping[str, str] = MappingProxyType({
"SKILL_PAYLOAD_CONTROL_BLOCKED": (
"would move (is_error=True, skill_payload_control_blocked) -> (is_error=True, "
"blocked). The retired loop branch was its only mention; no producer emits "
"the identifier, so the corpus harvest never sees it and the generic "
"`_BLOCKED` marker would answer if one ever did."
),
"four nested SAFETY_WARNING wrappers": (
"would move ok -> error (LEGACY_TOOL_ERROR, wrapper_depth_exceeded). The "
"composer wraps at most once, so a body reaching depth four is a producer "
"quoting the wrapper's own text at itself, not a runtime shape."
),
})
def _golden() -> dict[str, dict]:
payload = json.loads(GOLDEN_PATH.read_text(encoding="utf-8"))
assert payload["source_sha"] == GOLDEN_SOURCE_SHA, "golden was captured from another tree"
return payload["entries"]
# New producer contracts have no historical answer: the recorded old SHA is
# unavailable. Keep the historical corpus intact and assert the new observed
# outcome explicitly rather than manufacture old evidence (04-AGENCY S1/S3).
CURRENT_PRODUCER_CONTRACTS = {
# Saved-setting selection uses the existing process access authority; its new
# foreground refusal remains blocked through both text and native ACCESS_BLOCKED.
"PROCESS_ENV_REFERENCE_BLOCKED": (True, "blocked"),
"SAFETY_ADVICE": (False, "ok"),
"LIGHT_MODE_REPO_CHANGED": (False, "ok"),
"BROWSER_ACTION_OUTCOME_UNKNOWN": (True, "error"),
# The actual skill-metadata target refusal publishes its specific native
# code; standalone historical-style text retains the generic blocked code.
"SKILL_PAYLOAD_BLOCKED": (True, "blocked"),
"native:SKILL_PAYLOAD_BLOCKED:SKILL_PAYLOAD_BLOCKED": (True, "skill_payload_blocked"),
# 03-PROJECTS: delegate_directory.integrate_directory_result cannot
# acknowledge these requested apply/discard operations as completed.
"INTEGRATE_DELEGATED_APPLY_UNCONFIRMED": (True, "integration_blocked"),
"INTEGRATE_DELEGATED_DISCARD_UNCONFIRMED": (True, "integration_blocked"),
# A reject request cannot undo direct effects already in the folder.
# The refusal belongs to this disposition, not to the earlier write.
"INTEGRATE_DIRECTORY_ALREADY_APPLIED": (True, "integration_blocked"),
"INTEGRATE_DIRECTORY_UNCONFIRMED": (True, "integration_blocked"),
# subagent_integration refuses to accept mismatched or unavailable file
# evidence, or to integrate direct folder results through another surface.
"INTEGRATE_DIRECTORY_OUTPUT_MISMATCH": (True, "integration_blocked"),
# A tree under the light per-task cap (a consciousness Act/Observe tree) may not land a
# patch on the Ouroboros repository in any install mode (16.09): a new identifier.
"INTEGRATE_CAPPED_TREE": (True, "integration_blocked"),
"INTEGRATE_DIRECTORY_SURFACE_MISMATCH": (True, "integration_blocked"),
"INTEGRATE_FILE_OUTPUTS_UNAVAILABLE": (True, "integration_blocked"),
# The harvest puts every identifier first. A standalone capture failure
# is an error; the actual successful-write suffix is pinned separately.
"OUTPUT_CAPTURE_FAILED": (True, "error"),
# The two promotion receipts gained the warning marker their identifier needs
# to be read at all (14.09): before it, `PROMOTE_REJECTED: task … was not
# scheduled` opened line 1 with no marker and every refused promotion was a
# SUCCESSFUL tool call. New identifiers to the harvest, so their live answer is
# asserted here instead of borrowing an old tree's answer for a text it never saw.
"PROMOTE_REJECTED": (True, "tool_reported_failure"),
"PROMOTE_UNCONFIRMED": (True, "tool_reported_failure"),
# ensure_project_scope joined the receipt rail (15.09): its refused / unconfirmed
# bind outcomes are new identifiers, registered beside the routing family and
# asserted live here for the same reason as the promotion receipts above.
"SCOPE_REJECTED": (True, "tool_reported_failure"),
"SCOPE_UNCONFIRMED": (True, "tool_reported_failure"),
"TOOL_ERROR": (True, "error"),
"native:TOOL_REPORTED_FAILURE:TOOL_ERROR": (True, "tool_reported_failure"),
# Release admission split its one PREFLIGHT_BLOCKED text in two: a source it
# could not read is unavailable evidence, not a candidate defect. The new
# identifier reaches its text through the `code` variable, so it is declared
# in the corpus' interpolated list and answered live here — the retired pair
# never saw a tree that emitted it.
"PREFLIGHT_UNAVAILABLE": (True, "unavailable"),
# Peer admission adds current producers; the historical fixture stays intact.
"TASK_CANCEL_STATE_UNAVAILABLE": (True, "unavailable"),
# #1262: an enabled MCP server with no current listing makes a name's existence
# unknown — the provider's unavailability, never the caller's unknown tool.
"MCP_CATALOG_UNAVAILABLE": (True, "unavailable"),
"TASK_FORBIDDEN": (True, "blocked"),
"native:LEGACY_BLOCKED:TASK_FORBIDDEN": (True, "blocked"),
}
def _live_answer(case) -> tuple[bool, str]:
typed = typed_result(case)
is_error = _typed_execution_failure(True, typed)
return is_error, _typed_result_metadata(case.tool, case.text, is_error, typed)["status"]
@pytest.mark.parametrize("subject", tuple(CURRENT_PRODUCER_CONTRACTS))
@pytest.mark.parametrize("detail", [": detail line\nbody line", " (fixture_tool): detail line\nbody line"])
def test_current_producer_contracts_have_explicit_live_answers(subject, detail):
"""New producers need present contracts, never fabricated old answers."""
code = subject.split(":", 2)[1] if subject.startswith("native:") else ""
identifier = subject.rsplit(":", 1)[-1]
case = Case("current:" + subject, subject, "read_file", "⚠️ " + identifier + detail, code)
assert _live_answer(case) == CURRENT_PRODUCER_CONTRACTS[subject]
@pytest.mark.parametrize("tool", ["write_file", "edit_text", "apply_patch", "edit_batch"])
def test_output_capture_warning_preserves_the_successful_write(tool):
"""workspace_file_outputs.capture_known_workspace_outputs runs post-write.
Its failure suffix says the write remains applied. The native writer keeps
its successful first line, and the caller must not retry the write itself.
"""
warning = ("⚠️ OUTPUT_CAPTURE_FAILED: out.txt: OSError: artifact destination unavailable. "
"The writes remain applied; do not repeat them to retry artifact capture.")
text = (f"{tool}: changes are already on disk in the selected folder; "
"no separate patch apply is needed.\n" + warning)
case = Case("current:capture-suffix:" + tool, "OUTPUT_CAPTURE_FAILED", tool, text)
assert _live_answer(case) == (False, "ok")
assert _live_answer(case._replace(text=warning)) == CURRENT_PRODUCER_CONTRACTS["OUTPUT_CAPTURE_FAILED"]
def test_single_classifier_matches_the_retired_pair_except_approved_deltas() -> None:
golden = _golden()
corpus = build_corpus()
assert len(corpus) >= 600, "the corpus collapsed; a harvest regression would hide every delta"
unexpected: list[tuple[str, dict, tuple[bool, str]]] = []
unfired = set(APPROVED_DELTAS)
for case in corpus:
marker = _MARKER_RE.match(case.text.strip())
identifier = marker.group(1) if marker else ""
if case.key not in golden and identifier in CURRENT_PRODUCER_CONTRACTS:
contract = CURRENT_PRODUCER_CONTRACTS.get(case.subject, CURRENT_PRODUCER_CONTRACTS[identifier])
assert _live_answer(case) == contract, case.key
continue
assert case.key in golden, f"no golden answer for {case.key}: regenerate before trusting this run"
old = golden[case.key]
live = _live_answer(case)
if live == (old["is_error"], old["status"]):
continue
delta = APPROVED_DELTAS.get(case.subject)
expected = None if delta is None else (
(delta.old_is_error, delta.old_status), (delta.new_is_error, delta.new_status)
)
if expected != ((old["is_error"], old["status"]), live):
unexpected.append((case.key, old, live))
else:
unfired.discard(case.subject)
assert not unexpected, f"unapproved classification changes: {unexpected[:12]}"
assert not unfired, f"approved deltas that no longer fire (delete the rows): {sorted(unfired)}"
def test_every_approved_delta_names_an_owner_item() -> None:
for subject, delta in APPROVED_DELTAS.items():
assert delta.owner_item.startswith("A."), subject
assert delta.reason.strip(), subject
assert (delta.old_is_error, delta.old_status) != (delta.new_is_error, delta.new_status), subject
def test_native_golden_answers_have_identical_retired_text_inputs() -> None:
"""The old pair ignores native codes; aliases must retain its exact input."""
corpus = {case.key: case for case in build_corpus()}
golden = _golden()
for key, native in corpus.items():
if not key.startswith("native:"):
continue
plain = corpus[f"ident:{key.split(':', 2)[2]}:plain"]
assert (native.tool, native.text) == (plain.tool, plain.text)
if key in golden:
assert golden[key] == golden[plain.key]
else:
identifier = native.subject.split(":", 2)[-1]
assert identifier in CURRENT_PRODUCER_CONTRACTS
contract = CURRENT_PRODUCER_CONTRACTS.get(native.subject, CURRENT_PRODUCER_CONTRACTS[identifier])
assert _live_answer(native) == contract
assert _live_answer(plain) == CURRENT_PRODUCER_CONTRACTS[identifier]
def test_shape_golden_answers_match_their_own_identifier_line() -> None:
"""A hand-added shape answer is derived from the golden, never invented.
The retired pair is a pure text chain over the result's first marker line,
so a producer shape opening with `⚠️ IDENT` records the answer the plain
`ident:IDENT` case already holds, whatever tool published it and whatever
detail follows. Every shape row captured from the golden's source tree obeys
that, which is what lets a NEW shape row reuse the recorded identifier
answer instead of guessing at a tree this repository no longer holds. It is
the sibling of the native rule asserted above: same evidence, one axis over.
A shape whose identifier the harvest cannot see has no row to compare and is
skipped: the scratchpad upgrade keeps its marker and its name in two
different literals, so no `ident:` case exists for it.
"""
golden = _golden()
checked = []
for case in build_corpus():
if not case.key.startswith("shape:"):
continue
marker = _MARKER_RE.match(case.text.strip())
if marker is None:
continue
plain = f"ident:{marker.group(1)}:plain"
if plain not in golden:
continue
assert golden[case.key] == golden[plain], case.key
checked.append(case.key)
assert len(checked) >= 20, "the marker-led shape rows collapsed; the rule lost its witnesses"
def test_every_delta_without_a_producer_is_named_with_its_reason() -> None:
"""The two unreachable deltas stay documented, never approved: an approved row
that cannot fire would fail the table's own staleness direction."""
assert set(_DELTAS_WITHOUT_A_PRODUCER).isdisjoint(APPROVED_DELTAS)
for subject, reason in _DELTAS_WITHOUT_A_PRODUCER.items():
assert reason.strip(), subject
assert "SKILL_PAYLOAD_CONTROL_BLOCKED" not in harvested_identifiers()
def test_every_native_code_is_covered_by_the_corpus() -> None:
"""A producer cannot publish a code the differential has never classified.
The text corpus is blind to a producer that assembles its text at runtime, and
the (code, first line) harvest is blind for the same reason: it needs a literal.
That blind spot let four extension terminals, both MCP unavailable terminals and
the registry's unknown-tool publish change status with no test able to notice.
Any new native code now fails here until a corpus case exercises it."""
covered = {case.code for case in build_corpus() if case.code}
uncovered = sorted(set(harvested_native_codes()) - covered)
assert not uncovered, (
"native producer codes no corpus case classifies (add a _PRODUCER_SHAPES "
f"entry transcribed from the producer): {uncovered}"
)
def test_a_self_reported_failure_is_telemetry_on_the_execution_axis() -> None:
"""Owner homing of `tool_reported_failure`, asserted where it is consumed.
A provider that RAN and answered `{"ok": false}` is is_error=True — the counters
and the anti-loop scan need that — but it must not degrade execution health,
because `outcomes._LEDGER_NON_FAILURE_STATUSES` has declared the SAME status a
non-failure since v6.83.0. Homing it as blocking-only made every unrecovered
ext_/mcp_/read `{"ok": false}` land in `unresolved` on one axis while the ledger
called it fine on the other. Asserted through the classifier, not by frozenset
membership, so a future re-homing has to face the contradiction again."""
from ouroboros._outcome_tool_errors import _classify_tool_errors
buckets = _classify_tool_errors({"tool_calls": [{
"tool": "ext_1_demo_screenshot",
"status": "tool_reported_failure",
"is_error": True,
"result": '{"ok": false, "error": "HTTP 500"}',
}]})
assert [row["tool"] for row in buckets["policy_denials"]] == ["ext_1_demo_screenshot"]
assert buckets["unresolved"] == []
# And the two consumers agree: the ledger says the same about the same status.
from ouroboros.outcomes import _LEDGER_NON_FAILURE_STATUSES
assert "tool_reported_failure" in _LEDGER_NON_FAILURE_STATUSES
# A recovered one is still credited: it is walked, not skipped.
recovered = _classify_tool_errors({"tool_calls": [
{"tool": "ext_1_demo_screenshot", "status": "tool_reported_failure",
"is_error": True, "args": {"path": "/x/shot.png"}},
{"tool": "ext_1_demo_screenshot", "status": "ok",
"is_error": False, "args": {"path": "/x/shot.png"}},
]})
assert len(recovered["recovered"]) == 1
assert recovered["policy_denials"] == []
def test_a_control_refusal_typed_unavailable_is_the_substrates_answer() -> None:
"""Owner homing of `unavailable` (spec §1.15), asserted where it is consumed.
A target the runtime cannot serve — a legacy control surface that is off, a
task id this tree never registered — answers with the typed `unavailable`
the A.21 producers ship (e.g. control_task_results' LEGACY_UNAVAILABLE). That
is the SUBSTRATE saying no, not the agent failing: it stays is_error=True and
blocking, but it must not land in `unresolved` and degrade execution health.
`argument_error` deliberately stays degrading — a malformed call is the
agent's own defect and feeds reflection. Asserted through the classifier, not
by frozenset membership, so a re-homing has to face the split again."""
from ouroboros._outcome_tool_errors import _classify_tool_errors
buckets = _classify_tool_errors({"tool_calls": [{
"tool": "get_task_result",
"status": "unavailable",
"is_error": True,
"result": "⚠️ LEGACY_UNAVAILABLE: no result recorded for task_00000000",
}]})
assert [row["tool"] for row in buckets["policy_denials"]] == ["get_task_result"]
assert buckets["unresolved"] == []
# The other half of the §1.15 split: the agent's own malformed call degrades.
mistake = _classify_tool_errors({"tool_calls": [{
"tool": "get_task_result",
"status": "argument_error",
"is_error": True,
"result": "⚠️ ROUTING_ARGUMENT: task_id is required",
}]})
assert mistake["policy_denials"] == []
assert [row["tool"] for row in mistake["unresolved"]] == ["get_task_result"]
def test_golden_covers_every_harvested_producer() -> None:
"""A producer added after the cutover has no golden answer, so it fails here
instead of silently entering the tree with an unverified classification."""
golden = _golden()
missing = [
identifier for identifier in harvested_identifiers()
if f"ident:{identifier}:plain" not in golden and identifier not in CURRENT_PRODUCER_CONTRACTS
]
assert not missing, f"new warning identifiers without a golden answer: {missing}"
def test_specific_identifiers_beat_their_family_and_families_beat_generic_markers() -> None:
"""Order, asserted as behaviour rather than as a position in a table."""
def bucket(text: str) -> str:
return TOOL_CODE_SPECS[LegacyTextResultAdapter.from_text("fixture_tool", text).code].outcome_bucket
assert bucket("⚠️ SHELL_CWD_BLOCKED: escapes roots") == "cwd_blocked"
assert bucket("⚠️ SHELL_EXIT_ERROR: exit_code=1") == "non_zero_exit"
assert bucket("⚠️ SHELL_ENV_ERROR: bad env") == "shell_error"
assert bucket("⚠️ RUN_SCRIPT_BLOCKED: interpreter") == "run_script_blocked"
assert bucket("⚠️ RUN_SCRIPT_LAUNCH_ERROR: boom") == "run_script_error"
assert bucket("⚠️ LIGHT_MODE_REPO_WRITE_BLOCKED: repo") == "light_mode_blocked"
assert bucket("⚠️ INTEGRATE_TARGET_ERROR: not git") == "integration_blocked"
assert bucket("⚠️ INTEGRATE_LOCK_TIMEOUT: busy") == "integration_blocked"
assert bucket("⚠️ WRITE_FILE_ERROR: boom") == "write_file_blocked"
assert bucket("⚠️ EDIT_TEXT_ERROR: old_str not found") == "edit_text_blocked"
assert bucket("⚠️ APPLY_PATCH_ERROR: occurrence miscount") == "edit_ops_blocked"
assert bucket("⚠️ EDIT_BATCH_ERROR: occurrence miscount") == "edit_ops_blocked"
assert bucket("⚠️ DATA_WRITE_ERROR: refused") == "data_blocked"
assert bucket("⚠️ SKILL_PAYLOAD_ARG_ERROR: bad selector") == "skill_payload_blocked"
assert bucket("⚠️ ROOT_REQUIRED_USER_FILES: retry") == "root_required_user_files"
assert bucket("⚠️ ROOT_REQUIRED_ACTIVE_WORKSPACE: retry") == "root_required_active_workspace"
assert bucket("⚠️ RESOURCE_CONSTRAINT_BLOCKED: no network") == "resource_constraint_blocked"
assert bucket("⚠️ RESOURCE_POLICY_BLOCKED: protected") == "resource_policy_blocked"
assert bucket("⚠️ UNKNOWN_COARSE_BLOCKED: generic") == "blocked"
assert bucket("⚠️ UNKNOWN_COARSE_ERROR: generic") == "error"
# The one negation in the retired chain: an autocorrected command that also
# exited non-zero must not read as a plain autocorrected success.
assert bucket("⚠️ SHELL_REGEX_AUTO_CORRECTED: fixed\n⚠️ SHELL_EXIT_ERROR: exit_code=1") == "non_zero_exit"
assert bucket("⚠️ SHELL_REGEX_AUTO_CORRECTED: fixed\nexit_code=0") == "ok_autocorrected"
def test_every_outcome_bucket_is_partitioned() -> None:
"""A new code cannot acquire a bucket the outcome classifier does not know.
Without this, a call can be an honest error while `unresolved`, `policy_denials`,
`recovered`, `cosmetic` and `ignored` are all empty — two numbers in one artifact
contradicting each other, neither of them wrong."""
known = (
set(_BLOCKING_TOOL_STATUSES)
| set(_POLICY_DENIAL_STATUSES)
| set(_NON_BLOCKING_RECOVERABLE_STATUSES)
| set(_NON_BLOCKING_READONLY_BLOCK_STATUSES)
| set(_OK_TOOL_STATUSES)
| set(_UNPARTITIONED_BUCKETS)
)
unhomed = sorted({spec.outcome_bucket for spec in TOOL_CODE_SPECS.values()} - known)
assert not unhomed, f"outcome buckets with no partition: {unhomed}"
# Everything deliberately left out is named, and nothing else is.
assert set(_UNPARTITIONED_BUCKETS) == {"vlm_error"}
# Text inspections that survive OUTSIDE the one classifier, with the reason each
# cannot be expressed as a tool-result code. The cap may shrink, never grow, and a
# module absent from this inventory may hold none at all: that is the executable
# form of "one adapter plus an inventory of residual string producers".
_RESIDUAL_TEXT_INSPECTIONS: Mapping[str, tuple[int, str]] = MappingProxyType({
"ouroboros/outcomes.py": (5, "the FINAL ANSWER and service-teardown text, for which no ToolResult exists"),
# ouroboros/reflection.py held six (all of them `_ERROR_MARKERS`) until owner
# item I24 replaced that scan with the typed codes the trace already carries.
# The row is gone rather than zeroed: a module absent from this inventory may
# hold none at all, which is exactly the claim now.
"ouroboros/memory.py": (1, "tools.jsonl rows appended by consciousness carry neither status nor code"),
"ouroboros/skill_review_prompt.py": (2, "skill review verdict text, not a tool result"),
"ouroboros/tools/github.py": (12, "private helper-failure checks between two functions of one tool"),
# Upstream re-homed the skill-publish helper checks into their own module
# (skill_publish_github.py) and split the advisory run into
# preflight_review_run.py on this tree — redistribution, not growth.
"ouroboros/skill_publish_github.py": (7, "private helper-failure checks between two functions of one tool"),
"ouroboros/tools/claude_advisory_review.py": (4, "private helper-failure checks between two functions of one tool"),
"ouroboros/tools/preflight_review_run.py": (3, "private helper-failure checks between two functions of one tool"),
# Post-cutoff upstream module: the request-wire custom-tool receipts compose
# their own ⚠️ argument-error texts; predates the organ, upstream truth.
"ouroboros/openai_chat_dispatch.py": (2, "private helper-failure checks between two functions of one tool"),
"ouroboros/tools/core_file_tools.py": (1, "private helper-failure check between two functions of one tool"),
# The two listing-side redaction markers moved with the restricted-subagent
# read-denial policy (F2 absorption: core_file_tools -> core_secret_paths); the
# inventory total is unchanged.
"ouroboros/tools/core_secret_paths.py": (2, "listing-side redaction markers of the restricted-subagent read-denial policy"),
"ouroboros/tools/services.py": (1, "private helper-failure check between two functions of one tool"),
"ouroboros/tools/core.py": (1, "private helper-failure check between two functions of one tool"),
"ouroboros/tools/control_delegation.py": (1, "private helper-failure check between two functions of one tool"),
})
_RESIDUAL_PATTERNS = ('startswith("⚠️', 'startswith(("⚠️', "_ERROR_MARKERS", "_INFRA_TEXT_PREFIXES")
def test_residual_text_inspection_inventory_does_not_grow() -> None:
root = pathlib.Path(__file__).resolve().parents[1]
counted: dict[str, int] = {}
for path in sorted((root / "ouroboros").rglob("*.py")):
rel = path.relative_to(root).as_posix()
if rel == "ouroboros/tools/tool_result.py":
continue # the one classifier IS the inspection
text = path.read_text(encoding="utf-8")
hits = sum(text.count(pattern) for pattern in _RESIDUAL_PATTERNS)
if hits:
counted[rel] = hits
new_modules = sorted(set(counted) - set(_RESIDUAL_TEXT_INSPECTIONS))
assert not new_modules, f"a new module started classifying result text: {new_modules}"
grew = {
rel: (hits, _RESIDUAL_TEXT_INSPECTIONS[rel][0])
for rel, hits in counted.items()
if hits > _RESIDUAL_TEXT_INSPECTIONS[rel][0]
}
assert not grew, f"residual text inspections grew: {grew}"
# The loop is the one that mattered: it holds none.
assert "ouroboros/loop_tool_execution.py" not in counted
@pytest.mark.parametrize("case_key", ["shape:shell_no_match_autocorrected", "shape:shell_ok"])
def test_process_facts_stay_typed_not_parsed(case_key: str) -> None:
"""The three post-rules that are NOT text classification keep working off meta."""
case = next(item for item in build_corpus() if item.key == case_key)
typed = typed_result(case)
meta = _typed_result_metadata(case.tool, case.text, False, typed)
assert meta["exit_code"] == dict(case.meta)["exit_code"]
expected = "ok_autocorrected" if dict(case.meta).get("shell_regex_auto_corrected") else "ok"
assert meta["status"] == expected