ouroboros/tests/tool_classification_corpus.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

576 lines
34 KiB
Python

"""The tool-result classification corpus, shared by the differential test and the
golden generator.
Not collected by pytest (``python_files = test_*.py``): it is the ONE definition of
what gets classified, so the golden answers recorded from the retired loop pair and
the live answers from the single classifier are computed over identical inputs.
Regenerating the golden (only ever needed if the corpus definition itself changes,
and then only with an explicit owner decision, because the golden is the evidence
that the cutover was lossless). The corpus is HARVESTED FROM THE TREE UNDER TEST and
ANSWERED BY THE OLD TREE, so the two roots are different and both must be passed
explicitly — running ``build_corpus()`` inside the old checkout silently harvests the
old tree's producers instead and reproduces a different file (it differs today in the
``native:*`` keys the current producers publish):
NEW=$(pwd) # the tree being verified
OLD=$(mktemp -d)
git archive <GOLDEN_SOURCE_SHA> | tar -x -C "$OLD"
cp tests/tool_classification_corpus.py "$OLD/tests/"
cd "$OLD" && python -c '
import json, pathlib, sys; sys.path.insert(0, ".")
from tests.tool_classification_corpus import GOLDEN_SOURCE_SHA, build_corpus, legacy_answer
corpus = build_corpus(root=pathlib.Path(sys.argv[1]))
payload = {
"source_sha": GOLDEN_SOURCE_SHA,
"producer": "the retired loop pair (_is_tool_execution_failure + "
"_extract_result_metadata) at source_sha",
"corpus": "tests/tool_classification_corpus.py::build_corpus over the current tree",
"entries": {c.key: legacy_answer(c) for c in sorted(corpus, key=lambda c: c.key)},
}
sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=1) + "\n")
' "$NEW" > "$NEW/tests/fixtures/legacy_tool_classification_0f715831.json"
The entries are pre-sorted and the payload is dumped WITHOUT ``sort_keys`` (which
would reorder the four provenance keys); ``indent=1`` and the trailing newline are
what the checked-in file carries. Run verbatim against the corpus definition of any
commit, this reproduces that commit's fixture byte for byte.
``legacy_answer`` runs the retired pair, which exists only in the old tree; importing
this module in the current tree never touches it. The composers it calls live in the
old tree too, and their composed bytes are identical in both, which is what makes one
corpus definition legitimate across the two checkouts.
Native cases use the exact plain-identifier input, varying only the typed code.
The retired pair ignores that code, so a new native key can reuse the existing
plain case's recorded answer after verifying identical tool/text inputs. This
does not recapture or replace any old answer and still needs an approved delta.
"""
from __future__ import annotations
import ast
import pathlib
import re
from types import MappingProxyType
from typing import Any, Iterator, Mapping, NamedTuple
from ouroboros.tools.tool_result import (
LegacyTextResultAdapter,
ToolResult,
_compose_execute_result_result,
)
# The tree the golden answers were captured from: the last commit before the single
# classifier existed ON THIS TREE (v7next F3.1 lane A commit 2: producers publish
# typed results, the loop still classifies text). Recorded in the fixture too, so a
# golden can never be silently re-based onto a tree that already contains the change
# it is supposed to judge.
GOLDEN_SOURCE_SHA = "0f715831fab12edb02c105a43677bcbce0b1f1b6"
# ``CRITICAL`` is a severity word the safety refusal puts BEFORE its identifier;
# without skipping it the harvest invents a "CRITICAL" producer nobody has.
_MARKER_RE = re.compile(r"⚠️ (?:CRITICAL )?([A-Z][A-Z0-9_]{2,})")
# The two classifiers are the subject, not producers: their own tables would
# otherwise seed the corpus with identifiers nobody emits.
_CLASSIFIER_SOURCES = frozenset({
"ouroboros/tools/tool_result.py",
"ouroboros/loop_tool_execution.py",
})
_NATIVE_CALLS = frozenset({
"ToolResult",
"_publish_process_result",
"_publish_tool_result",
"_extension_result",
"_classification",
})
_CODE_RE = re.compile(r"^[A-Z][A-Z0-9_]*$")
# Identifiers a producer INTERPOLATES: the name reaches the text through a variable
# (``f"⚠️ {error_tag}: …"``, ``f"⚠️ {action}_BLOCKED: …"``, a tool→prefix lookup), so
# no single string literal ever holds ``⚠️ IDENT`` and the harvest above cannot see
# them. Listed explicitly with the producer that assembles each, because a corpus
# that silently omits a producer is a corpus that proves less than it claims.
_INTERPOLATED_IDENTIFIERS = (
"APPLY_PATCH_BLOCKED", # tools/edit_ops.py::apply_patch error_tag
"EDIT_BATCH_BLOCKED", # tools/edit_ops.py::edit_batch error_tag
"PREFLIGHT_UNAVAILABLE", # commit_admission.py::format_release_metadata_preflight code
"READ_FILE_BLOCKED", # tools/core_file_tools.py::_local_readonly_resource_block action
"SCRIPT_CWD_BLOCKED", # tools/tool_resolution.py::_binding_error_text prefixes
"SEARCH_BLOCKED", # tools/core.py search_code, same action argument
"VERIFY_ERROR", # tools/tool_resolution.py::_binding_error_text prefixes
)
class Case(NamedTuple):
"""One classified input. ``code`` is the code a producer publishes natively;
empty means the host adapts the text."""
key: str
subject: str
tool: str
text: str
code: str = ""
meta: tuple[tuple[str, Any], ...] = ()
def repo_root() -> pathlib.Path:
return pathlib.Path(__file__).resolve().parents[1]
def _string_constants(tree: ast.AST) -> Iterator[str]:
"""Every string literal, including the literal parts of f-strings (an
``ast.JoinedStr`` holds its constant runs as ``ast.Constant`` children)."""
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
yield node.value
def _sources(root: pathlib.Path) -> Iterator[tuple[str, ast.AST]]:
for path in sorted((root / "ouroboros").rglob("*.py")):
rel = path.relative_to(root).as_posix()
if rel in _CLASSIFIER_SOURCES:
continue
try:
yield rel, ast.parse(path.read_text(encoding="utf-8"), filename=rel)
except SyntaxError: # pragma: no cover - a broken tree fails elsewhere first
continue
def harvested_identifiers(root: pathlib.Path | None = None) -> tuple[str, ...]:
"""Every ``⚠️ IDENTIFIER`` a producer can emit, harvested from the tree."""
found: set[str] = set(_INTERPOLATED_IDENTIFIERS)
for _rel, tree in _sources(root or repo_root()):
for value in _string_constants(tree):
found.update(match.group(1) for match in _MARKER_RE.finditer(value))
return tuple(sorted(found))
def harvested_native_codes(root: pathlib.Path | None = None) -> tuple[str, ...]:
"""Every ``ToolResult`` code a producer publishes NATIVELY, whether or not its
text is a literal.
``harvested_native_pairs`` sees only producers whose first line is statically
known, so a producer that assembles its text at runtime — every extension and
MCP terminal, the protected-write refusal, the binding-error family — was
invisible to the differential: its code could change status without a single
corpus case moving. This is the set the coverage assertion closes over.
"""
found: set[str] = set()
for _rel, tree in _sources(root or repo_root()):
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
name = func.id if isinstance(func, ast.Name) else getattr(func, "attr", "")
if name not in _NATIVE_CALLS:
continue
code = ""
for keyword in node.keywords:
if keyword.arg == "code" and isinstance(keyword.value, ast.Constant):
code = str(keyword.value.value)
for arg in node.args:
if isinstance(arg, ast.Constant) and isinstance(arg.value, str) and _CODE_RE.fullmatch(arg.value):
code = code or arg.value
if code:
found.add(code)
return tuple(sorted(found))
def harvested_native_pairs(root: pathlib.Path | None = None) -> tuple[tuple[str, str], ...]:
"""Every ``(code, identifier)`` pair a producer publishes with a statically
known text. This is the axis the text corpus cannot see: where a producer's
code and its own first line disagree, the cutover changes the answer even
though no identifier moved."""
pairs: set[tuple[str, str]] = set()
for _rel, tree in _sources(root or repo_root()):
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
name = func.id if isinstance(func, ast.Name) else getattr(func, "attr", "")
if name not in _NATIVE_CALLS:
continue
code = ""
text = ""
for keyword in node.keywords:
if keyword.arg == "code" and isinstance(keyword.value, ast.Constant):
code = str(keyword.value.value)
if keyword.arg == "text":
text = _leading_literal(keyword.value)
positional = [arg for arg in node.args]
for arg in positional:
if isinstance(arg, ast.Constant) and isinstance(arg.value, str) and _CODE_RE.fullmatch(arg.value):
code = code or arg.value
if not text:
for arg in positional:
literal = _leading_literal(arg)
if literal.startswith("⚠️"):
text = literal
break
marker = _MARKER_RE.match(text.strip())
if code and marker:
pairs.add((code, marker.group(1)))
return tuple(sorted(pairs))
def _leading_literal(node: ast.AST) -> str:
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return node.value
if isinstance(node, ast.JoinedStr) and node.values:
head = node.values[0]
if isinstance(head, ast.Constant) and isinstance(head.value, str):
return head.value
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
return _leading_literal(node.left)
return ""
# Detail shapes real producers use after the identifier.
_DETAIL_SHAPES = (
("plain", ": detail line\nbody line"),
("named", " (fixture_tool): detail line\nbody line"),
)
# Composition bases, each exercising a different branch of the chain.
_COMPOSITION_BASES = (
("clean", "plain success"),
("exit", "⚠️ SHELL_EXIT_ERROR: command exited with exit_code=1.\n\nSTDERR:\nboom"),
("protected", "⚠️ CORE_PROTECTION_BLOCKED: edit_text attempted a protected write."),
("timeout", "⚠️ TOOL_TIMEOUT (read_file): exceeded 120s limit."),
("violation", "⚠️ CRITICAL SAFETY_VIOLATION: refused."),
("integrate", "⚠️ INTEGRATE_CONFLICT: patch did not apply."),
("reported", '{"ok": false, "error": "provider said no"}'),
# A SUCCESSFUL body that itself contains the safety wrapper's separator: a
# markdown rule in stdout is ordinary output, and counting separators once
# turned exactly this into a blocking safety-provider failure.
("separator_in_body", "exit_code=0\nSTDOUT:\n# README\n\n---\n\nUsage: run it"),
)
_ROUTE_NOTES = (("", ""), ("route", "⚠️ AUTO_ROUTED_TO_ACTIVE_WORKSPACE: fixture"))
_SAFETY_MSGS = (("", ""), ("safety", "⚠️ SAFETY_WARNING: inspect the call"))
# Line-structure edges the two parsers disagreed about by construction: the loop
# scanned the whole remainder, the adapter recurses into the body's first line.
_LINE_EDGES = (
("autocorrect_only", "⚠️ SHELL_REGEX_AUTO_CORRECTED: corrected the pattern"),
("autocorrect_line2", "⚠️ SHELL_REGEX_AUTO_CORRECTED: corrected\n⚠️ SHELL_EXIT_ERROR: exit_code=1"),
("autocorrect_line3", "⚠️ SHELL_REGEX_AUTO_CORRECTED: corrected\nclean line\n⚠️ SHELL_EXIT_ERROR: exit_code=1"),
("autocorrect_undeclared", "⚠️ SHELL_REGEX_AUTO_CORRECTED: corrected\n⚠️ ARTIFACT_OUTPUT_UNDECLARED: declare outputs"),
("autocorrect_artifact_error", "⚠️ SHELL_REGEX_AUTO_CORRECTED: corrected\n⚠️ ARTIFACT_OUTPUT_ERROR: registration failed"),
("safety_double_separator", "⚠️ SAFETY_WARNING: inspect\n\n---\nbody\n\n---\ntail"),
("safety_inner_block", "⚠️ SAFETY_WARNING: inspect\n\n---\n⚠️ RESOURCE_POLICY_BLOCKED: protected artifact"),
("safety_single_line", "⚠️ SAFETY_WARNING: inspect"),
("unknown_tool", "⚠️ Unknown tool: 'nope' is not a registered visible tool"),
("critical_safety_violation", "⚠️ CRITICAL SAFETY_VIOLATION: refused by the safety supervisor"),
("mcp_envelope_marker", "External MCP tool result from 'demo'/'ping'.\n\n⚠️ MCP_TOOL_ERROR: server text"),
)
# Edges whose answer depends on the TOOL NAME SHAPE as well as the text. The
# unknown-tool sentence is host text under a name that looks dynamic — a
# hallucinated ``ext_``/``mcp_`` name, or one whose extension was unloaded — and
# the dynamic short-circuit used to claim it and report the call as a success.
_EDGE_EXTRA_TOOLS: Mapping[str, tuple[str, ...]] = MappingProxyType({
"unknown_tool": ("ext_1_a_foo", "mcp_demo__ping"),
})
_STRUCTURED_BODIES = (
("false", '{"ok": false, "error": "boom"}'),
("false_indented", ' {"ok": false}'),
("true", '{"ok": true, "path": "/x/shot.png"}'),
("nested_only", '{"data": {"ok": false}}'),
("list", '["ok", false]'),
("string_false", '{"ok": "false"}'),
("prose", "plain provider prose"),
("empty", ""),
)
_STRUCTURED_TOOLS = ("read_file", "ext_1_demo_screenshot", "mcp_demo__ping", "run_command")
# Producer shapes whose text is ASSEMBLED AT RUNTIME, transcribed from the exact
# composition at each producer. These are the only corpus entries that are not
# built by a harvest or a real composer, and they exist because the static harvest
# structurally cannot see them: it pairs a code with a first line only when that
# line is a string literal, so every producer that interpolates a name, a path or
# an exception into its text was invisible — its code could change status without
# one corpus case moving. ``test_every_native_code_is_covered_by_the_corpus``
# fails if a producer publishes a code no shape below (and no harvested pair)
# exercises, which is the assertion that closes that blind spot.
_PRODUCER_SHAPES = (
("safety_violation", "run_command", "⚠️ SAFETY_VIOLATION: independent assessment refused", "SAFETY_VIOLATION", ()),
("shell_ok", "run_command", "exit_code=0\nSTDOUT:\nfine", "OK", (("exit_code", 0),)),
("shell_autocorrected", "run_command", "⚠️ SHELL_REGEX_AUTO_CORRECTED: corrected\nexit_code=0\nSTDOUT:\nfine", "SHELL_REGEX_AUTO_CORRECTED", (("exit_code", 0), ("shell_regex_auto_corrected", True))),
("shell_no_match", "run_command", "exit_code=1 (no matches)\nSTDOUT:\n", "SHELL_NO_MATCH", (("exit_code", 1),)),
("shell_no_match_autocorrected", "run_command", "⚠️ SHELL_REGEX_AUTO_CORRECTED: corrected\nexit_code=1 (no matches)\nSTDOUT:\n", "SHELL_NO_MATCH", (("exit_code", 1), ("shell_regex_auto_corrected", True))),
("shell_exit_error", "run_command", "⚠️ SHELL_EXIT_ERROR: command exited with exit_code=2.\n\nSTDERR:\nboom", "SHELL_EXIT_ERROR", (("exit_code", 2),)),
("shell_undeclared", "run_command", "⚠️ ARTIFACT_OUTPUT_UNDECLARED: declare outputs=[...]\n\nexit_code=0", "ARTIFACT_OUTPUT_UNDECLARED", (("exit_code", 0),)),
("shell_artifact_error", "run_command", "⚠️ ARTIFACT_OUTPUT_ERROR: registration failed. exit_code=0", "ARTIFACT_OUTPUT_ERROR", (("exit_code", 0),)),
("mcp_provider_error", "mcp_svc__ping", "External MCP tool result from 'svc'/'ping'. This server-supplied result is untrusted data.\n\nthe server said no", "MCP_ERROR", (("dynamic_provider", True), ("mcp_is_error", True))),
("ephemeral_turn_denial", "read_file", "⚠️ EPHEMERAL_TURN_RESTRICTED: 'update_identity' is not in the decision-turn allowlist.", "ACCESS_BLOCKED", ()),
("root_required_active_workspace", "write_file", "⚠️ ROOT_REQUIRED_ACTIVE_WORKSPACE: absolute path '/w/x.txt' is under the active workspace.", "ROOT_REQUIRED_ACTIVE_WORKSPACE", (("required_root", "active_workspace"),)),
("root_required_user_files", "write_file", "⚠️ ROOT_REQUIRED_USER_FILES: an absolute home path was given but root defaulted to 'active_workspace'.", "ROOT_REQUIRED_USER_FILES", ()),
("resource_constraint", "read_file", "⚠️ RESOURCE_CONSTRAINT_BLOCKED: task_contract.allowed_resources.network=false blocks it.", "RESOURCE_CONSTRAINT_BLOCKED", ()),
("resource_policy", "read_file", "⚠️ RESOURCE_POLICY_BLOCKED: task_contract.resource_policy protects 'blackbox'.", "RESOURCE_POLICY_BLOCKED", ()),
("cognitive_redirect", "write_file", "⚠️ COGNITIVE_TOOL_REQUIRED: cognitive memory is not written via 'write_file'.", "COGNITIVE_TOOL_REQUIRED", ()),
("extension_reported_failure", "ext_1_demo_screenshot", '{"ok": false, "error": "HTTP 500"}', "TOOL_REPORTED_FAILURE", (("dynamic_provider", True),)),
("git_error_untyped_text", "vcs_status", "git refusal text without any marker", "GIT_ERROR", ()),
("review_blocked_untyped_text", "commit_reviewed", "review rejection text without any marker", "REVIEW_BLOCKED", ()),
("executor_crash", "write_file", "⚠️ TOOL_ERROR (write_file): RuntimeError: boom", "EXECUTOR_ERROR", ()),
("outer_timeout", "read_file", "⚠️ TOOL_TIMEOUT (read_file): exceeded 120s limit.", "TOOL_TIMEOUT", (("timeout_sec", 120),)),
# tools/core_file_tools.py, owner item I27. The discovery miss interpolates
# the exception into its text and publishes it under `list_files`, so the
# sentence the owner actually reads is exercised by no other case: the
# identifier rows carry a synthetic detail under `read_file`, and the
# harvested (code, first line) pair reuses that same input by construction.
("list_files_not_found", "list_files",
"⚠️ LIST_FILES_NOT_FOUND: Directory not found: notes/ML Conf 2", "LEGACY_WARNING", ()),
# tools/extension_dispatch.py — every terminal interpolates the tool name, so
# all four were outside the harvest while carrying real status changes.
("extension_handler_error", "ext_1_demo_screenshot", "⚠️ TOOL_ERROR (ext_1_demo_screenshot): extension tool failed: RuntimeError: boom", "EXTENSION_ERROR", (("dynamic_provider", True),)),
("extension_async_timeout", "ext_1_demo_screenshot", "⚠️ TOOL_ERROR (ext_1_demo_screenshot): extension async handler failed: TimeoutError: handler exceeded timeout", "EXTENSION_TIMEOUT", (("dynamic_provider", True), ("timeout_sec", 60))),
("extension_not_live", "ext_1_demo_screenshot", "⚠️ TOOL_ERROR (ext_1_demo_screenshot): extension 'demo' is not allowed to dispatch right now.", "EXTENSION_UNAVAILABLE", (("dynamic_provider", True),)),
("extension_safety_wrapped_ok", "ext_1_demo_screenshot", '⚠️ SAFETY_WARNING: inspect the call\n\n---\n{"ok": true, "path": "/x/shot.png"}', "SAFETY_WARNING", (("dynamic_provider", True), ("safety_warning", True))),
# tools/registry_core.py — an extension surface that exists but is not live
# gets the host's unknown-tool sentence typed as unavailable, which is more
# precise than "unknown" and is NOT the adapter's answer for the same text.
("unknown_tool_extension_down", "ext_1_demo_screenshot", "⚠️ Unknown tool: 'ext_1_demo_screenshot': its extension is not live for this task right now. Nothing was executed.\nNo tool in ext_1_demo is currently callable in this task.", "EXTENSION_UNAVAILABLE", (("dynamic_provider", True),)),
("protected_write", "write_file", "⚠️ CORE_PROTECTION_BLOCKED: runtime_mode='advanced' refuses to write protected core path: ouroboros/safety.py. Switch to runtime_mode='pro' and let the normal triad + scope review cover the protected core/contract/release change before commit.", "CORE_PROTECTION_BLOCKED", ()),
# ouroboros/mcp_client.py (MCPNameResolution.refusal) — the unavailable facts
# publish one code from different first lines, which only a shape can express;
# a catalog miss is the caller's unknown tool (#1262), the registry's own
# unknown-tool code published under the MCP marker.
("mcp_disabled", "mcp_svc__ping", "⚠️ MCP_DISABLED: enable MCP in Settings → Advanced to use this tool.", "MCP_UNAVAILABLE", ()),
("mcp_tool_not_found", "mcp_svc__ping", "⚠️ MCP_TOOL_NOT_FOUND: 'mcp_svc__ping' is not in the current tool catalog of MCP server 'svc'. Nothing was executed.", "UNKNOWN_TOOL", ()),
("mcp_transport_timeout", "mcp_svc__ping", "⚠️ MCP_TOOL_TIMEOUT: server 'svc' did not respond in 60s", "MCP_TIMEOUT", ()),
# tool_access.shell_cwd_block_message, published by both process guards.
("shell_cwd_block", "run_command", "⚠️ SHELL_CWD_BLOCKED: CWD_BLOCKED: cwd /etc is outside allowed roots for shell. Allowed cwd roots for this tool/profile: active_workspace=/w. Use one of those exact paths as cwd (or root=task_drive/artifact_store/user_files in file tools).", "SHELL_CWD_BLOCKED", ()),
# tools/tool_resolution.py::_binding_error_text — the two typed terminals of
# the interpolated prefix table.
("binding_arg_error", "query_code", "⚠️ TOOL_ARG_ERROR (query_code): ValueError: unknown root 'nope'", "TOOL_ARG_ERROR", ()),
("binding_default_error", "apply_patch", "⚠️ TOOL_ERROR: ValueError: unknown root 'nope'", "TOOL_ERROR", ()),
# tools/core_artifacts.py — owner item A.20. These refusals carry no uppercase
# identifier, so no (code, first line) pair can be harvested for them and the
# text corpus reads every one of them as an ordinary warning. Without a shape
# the differential is blind to the whole family: a delivery that queued nothing
# could go on reporting ok and no case would move.
("send_photo_no_chat", "send_photo", "⚠️ No active chat — cannot send photo.", "LEGACY_UNAVAILABLE", ()),
("send_video_no_chat", "send_video", "⚠️ No active chat — cannot send video.", "LEGACY_UNAVAILABLE", ()),
("send_file_no_chat", "send_file", "⚠️ No active chat — cannot send file.", "LEGACY_UNAVAILABLE", ()),
("send_photo_read_failure", "send_photo", "⚠️ Failed to read image file: PermissionError: [Errno 13] Permission denied", "LEGACY_TOOL_ERROR", ()),
("send_photo_empty_payload", "send_photo", "⚠️ Image data is empty or too short.", "LEGACY_TOOL_ERROR", ()),
("send_video_missing_file", "send_video", "⚠️ File not found: /x/clip.mp4", "LEGACY_TOOL_ERROR", ()),
("send_file_missing_argument", "send_file", "⚠️ Provide a file_path.", "LEGACY_TOOL_ERROR", ()),
# tools/control_routing.py — owner item A.21. All four receipts are PLAIN
# STRINGS the host adapts, so they carry no declared code: the codes they used
# to declare were invented, and the differential compared each against itself
# while the classifier read the markerless promotion sentences as successes
# (the 14.09 receipt incident). The two promotion receipts now carry the
# warning marker their identifier needs to be read at all.
("promote_rejected", "promote_chat_to_task",
"⚠️ PROMOTE_REJECTED: task 4f2a1c was not scheduled (admission_rejected). "
"Do not report this task as created.", "", ()),
("promote_unconfirmed", "promote_chat_to_task",
"⚠️ PROMOTE_UNCONFIRMED: task 4f2a1c admission was not confirmed within 30 seconds; "
"the requested destination was new project 'Dinosaurs' and the effective one is "
"unknown until the admission is reconciled. Do not report this task as created and "
"do not retry automatically; keep this task id for reconciliation.", "", ()),
("route_rejected", "route_to_project",
"⚠️ ROUTE_REJECTED: task 4f2a1c was not routed to project 'dinosaurs' "
"(target_not_steerable).", "", ()),
("route_unconfirmed", "route_to_project",
"⚠️ ROUTE_UNCONFIRMED: task 4f2a1c routing to project 'dinosaurs' was not durably "
"confirmed. Do not report it as routed and do not retry automatically.",
"", ()),
# tools/control_runtime.py, control_scheduling.py, control_task_results.py —
# owner item A.21 again. Every sentence below is either markerless (the deep
# self-review notice, the depth-limit refusal, the unknown-task read, both
# proactive-message refusals, the unknown-model refusal) or reaches its result
# through a helper the publication wraps (the capability mismatch, the legacy
# scratchpad upgrade), so no (code, first line) pair can be harvested for any of
# them and the text corpus reads them all as ordinary warnings or successes.
("deep_self_review_unavailable", "request_deep_self_review",
"❌ Deep self-review unavailable: configure OUROBOROS_MODEL_DEEP_SELF_REVIEW "
"and the matching provider API key.", "CAPABILITY_UNAVAILABLE", ()),
("scratchpad_legacy_upgrade", "update_scratchpad",
"⚠️ LEGACY_SCRATCHPAD_REQUIRES_MANUAL_UPGRADE: memory/scratchpad.md exists without "
"scratchpad_blocks.json. Move preserved notes manually before appending new "
"scratchpad blocks.", "LEGACY_BLOCKED", ()),
("proactive_message_no_chat", "send_user_message",
"⚠️ No active chat — cannot send proactive message.", "TOOL_ARG_ERROR", ()),
("proactive_message_empty", "send_user_message", "⚠️ Empty message.", "TOOL_ARG_ERROR", ()),
("switch_model_unknown", "switch_model",
"⚠️ Unknown model: gpt-9. Available: gpt-5.6-luna, sonnet-4.6", "TOOL_ARG_ERROR", ()),
("subtask_depth_limit", "schedule_subagent",
"ERROR: Subtask depth limit (3) exceeded. Simplify your approach.",
"RESOURCE_CONSTRAINT_BLOCKED", ()),
("subagent_capability_mismatch", "schedule_subagent",
"⚠️ SUBAGENT_CAPABILITY_MISMATCH: selected child profile 'local_readonly_subagent' "
"cannot satisfy required_capabilities=['shell']. These need an ACTING child: pass "
"write_surface (self_worktree for a throwaway checkout to run shell/build in; "
"external_workspace for the shared project tree; genesis for a from-scratch project). "
"A read-only child has no shell/writable roots.", "TOOL_ARG_ERROR", ()),
("task_result_unknown_id", "get_task_result",
"Task 4f2a1c: unknown or not yet registered", "LEGACY_UNAVAILABLE", ()),
# tools/followup.py — owner decision 2026-08-19 ("B"). Same blindness as the
# A.20/A.21 families, on the tool that mints FUTURE ROOT TASKS: every sentence
# is markerless ("ERROR: FOLLOWUP_…"), so no identifier and no (code, first
# line) pair can be harvested for any of them and the text corpus reads every
# refusal as an ordinary success. Without a shape the differential is blind to
# the whole family: a follow-up that was refused — by authority, by the pending
# cap, by a persist failure — could go on reporting ok and no case would move.
("followup_subagent_refused", "schedule_followup",
"ERROR: FOLLOWUP_SUBAGENT_REFUSED: a delegated subagent holds narrower-than-parent "
"authority and may not mint future root tasks. Report the wait instant to your "
"parent instead; the parent (or the owner) decides whether to schedule a follow-up.",
"ACCESS_BLOCKED", ()),
("followup_task_id_required", "schedule_followup",
"ERROR: FOLLOWUP_TASK_ID_REQUIRED: a durable follow-up must belong to a real task.",
"LEGACY_UNAVAILABLE", ()),
("followup_run_at_invalid", "schedule_followup",
"ERROR: FOLLOWUP_RUN_AT_INVALID: 'soon' is not a parseable ISO 8601 instant. "
"Example: 2026-08-19T12:20:00+03:00 (naive times read as UTC).",
"TOOL_ARG_ERROR", ()),
("followup_objective_required", "schedule_followup",
"ERROR: FOLLOWUP_OBJECTIVE_REQUIRED: write the future task's objective in plain language.",
"TOOL_ARG_ERROR", ()),
("followup_text_too_long", "schedule_followup",
"ERROR: FOLLOWUP_TEXT_TOO_LONG: objective is 4001 chars; the limit is 4000. "
"Shorten it — nothing was truncated and nothing was scheduled.",
"TOOL_ARG_ERROR", ()),
("followup_data_root_unresolved", "schedule_followup",
"ERROR: FOLLOWUP_DATA_ROOT_UNRESOLVED: task drive root is not under the owner data root",
"TOOL_ERROR", ()),
("followup_cap_reached", "schedule_followup",
"ERROR: FOLLOWUP_CAP_REACHED: this task already holds 2 pending follow-up(s) of the "
"2 allowed: followup-root-1-a1b2c3 (fires at/after 2030-01-01T00:00:00+00:00); "
"followup-root-1-d4e5f6 (fires at/after 2030-02-01T00:00:00+00:00). Each fires once; "
"wait for one to fire, or the owner can disable/delete records from the Schedules surface.",
"RESOURCE_CONSTRAINT_BLOCKED", ()),
("followup_persist_failed", "schedule_followup",
"ERROR: FOLLOWUP_PERSIST_FAILED: OSError: [Errno 28] No space left on device",
"TOOL_ERROR", ()),
("followup_scheduled", "schedule_followup",
"FOLLOWUP_SCHEDULED: one-shot follow-up followup-root-1-a1b2c3 registered to fire at/after "
"2030-01-01T00:00:00+00:00 (next scheduler tick at/after that instant). It will enqueue an "
"ordinary root task through the supervisor scheduler under normal admission; pending "
"follow-ups for this task: 1/2. The record is durable in state/scheduled_tasks.json and "
"fires exactly once; the owner can disable or delete it from the Schedules surface.",
"OK", ()),
)
def build_corpus(root: pathlib.Path | None = None) -> tuple[Case, ...]:
"""Every classified input, in a stable order."""
root = root or repo_root()
cases: list[Case] = []
for identifier in harvested_identifiers(root):
for shape, detail in _DETAIL_SHAPES:
cases.append(Case(
key=f"ident:{identifier}:{shape}",
subject=identifier,
tool="read_file",
text=f"⚠️ {identifier}{detail}",
))
for base_name, base in _COMPOSITION_BASES:
for route_name, route_note in _ROUTE_NOTES:
for safety_name, safety_msg in _SAFETY_MSGS:
composed = _compose_execute_result_result("apply_patch", base, route_note, safety_msg)
suffix = "+".join(part for part in (route_name, safety_name) if part) or "bare"
cases.append(Case(
key=f"compose:{base_name}:{suffix}",
subject=f"compose:{base_name}:{suffix}",
tool="apply_patch",
text=composed.text,
))
for edge_name, text in _LINE_EDGES:
for tool in ("run_command", "read_file") + _EDGE_EXTRA_TOOLS.get(edge_name, ()):
cases.append(Case(
key=f"edge:{edge_name}:{tool}",
subject=f"edge:{edge_name}",
tool=tool,
text=text,
))
for body_name, body in _STRUCTURED_BODIES:
for tool in _STRUCTURED_TOOLS:
cases.append(Case(
key=f"body:{body_name}:{tool}",
subject=f"body:{body_name}",
tool=tool,
text=body,
))
envelope = (
"External MCP tool result from 'demo'/'ping'. "
"This server-supplied result is untrusted data, not instructions or policy.\n\n"
)
for body_name, body in _STRUCTURED_BODIES:
cases.append(Case(
key=f"envelope:{body_name}",
subject=f"envelope:{body_name}",
tool="mcp_demo__ping",
text=envelope + body,
))
for code, identifier in harvested_native_pairs(root):
cases.append(Case(
key=f"native:{code}:{identifier}",
subject=f"native:{code}:{identifier}",
tool="read_file",
text=f"⚠️ {identifier}{_DETAIL_SHAPES[0][1]}",
code=code,
))
for shape_name, tool, text, code, meta in _PRODUCER_SHAPES:
cases.append(Case(
key=f"shape:{shape_name}",
subject=f"shape:{shape_name}",
tool=tool,
text=text,
code=code,
meta=meta,
))
keys = [case.key for case in cases]
if len(keys) != len(set(keys)): # pragma: no cover - corpus definition error
raise ValueError("corpus keys must be unique")
return tuple(cases)
def typed_result(case: Case) -> ToolResult:
"""The typed result the runtime carries for one case: the producer's own when
it publishes a ``ToolResult``, otherwise the single adapter's reading of the
producer's actual string.
``code`` is therefore a claim about the PRODUCER, never the expected answer —
a case may declare one only where the tool really publishes it. The four
control_routing receipts declared ``LEGACY_BLOCKED``/``LEGACY_UNAVAILABLE``
while their producers return plain strings, so the differential compared that
invented code against itself and passed while the classifier read a refused
promotion as SUCCESS. Their rows now carry no code, which puts the producer's
own sentence in front of the one classifier — the only thing that can fail.
"""
if not case.code:
return LegacyTextResultAdapter.from_text(case.tool, case.text)
from ouroboros.tools.tool_result import TOOL_CODE_SPECS
return ToolResult(
status=TOOL_CODE_SPECS[case.code].status,
code=case.code,
text=case.text,
meta=dict(case.meta),
)
def legacy_answer(case: Case) -> dict[str, Any]:
"""The RETIRED loop pair's answer. Importable only in a tree that still has it;
used exclusively by the golden generator described in the module docstring."""
from ouroboros.loop_tool_execution import ( # noqa: PLC0415 - old-tree only
_extract_result_metadata,
_is_tool_execution_failure,
)
from ouroboros.tools.tool_result import TOOL_CODE_SPECS
# v7next adaptation, disclosed: on THIS tree the retired pair is the pure
# TEXT chain (tool_ok, result) — the reference's old tree carried an
# intermediate typed-consuming pair, this one never did. TOOL_CODE_SPECS is
# imported above only to keep the signature shared with the reference
# recipe; the answers here are text-only by construction.
del TOOL_CODE_SPECS
is_error = _is_tool_execution_failure(True, case.text)
meta = _extract_result_metadata(case.tool, case.text, is_error)
return {"is_error": bool(is_error), "status": str(meta.get("status") or "")}