Skip to content

Intent Surface — Seam 8, Phases 2-5 (complete)

Status: kickoff slice (Phases 2-3) shipped on feat/au-intent-surface; the CPD (Phase 1, feat/au-cpd) and this doc's §7 remaining work (Phases 4-5 — CPD-backed ranking, the calibrated-outcomes learning loop, resolution caching, the intent-surface skill content (now folded into graph-runtime-and-governance, since moved to graph-os's own skill pack; see the note below), and the A/B selection-accuracy harness) shipped on feat/au-seam8-complete (NOT merged/pushed — awaiting review). Parent plan: plans/program-design-2026-07-11-epistemic-tool-routing.md. Concepts: CONCEPT:AU-ECO.mcp.intent-surface-condensed-collapse (the surface collapse) / CONCEPT:AU-ECO.mcp.intent-surface-tool-lifecycle (the load→use→unload lifecycle) / CONCEPT:AU-ECO.mcp.intent-surface-cpd-ranking (CPD-backed resolver ranking) / CONCEPT:AU-ECO.mcp.intent-surface-outcome-learning (the calibrated-outcomes learning loop) / CONCEPT:AU-ECO.mcp.intent-surface-resolution-cache (bounded resolution caching) / CONCEPT:AU-ECO.mcp.intent-surface-selection-accuracy (the A/B measurement harness).

Naming note (post-collapse, feature/authority-convergence). This doc's §5 table and the kg-* skill names it references throughout are historical: the ~73 granular kg-* skills it describes (one per wrapped verb/tool) were folded into 13 broad domain skills (agent_utilities/skills/graph-*/agent-utilities-*) so the skill layer matches this same doc's own condensed-surface philosophy one level up. Those 7 graph-* domain skills have since moved to graph-os's own skill pack (graph_os/skills/graph-*, taken over in graph-os commit 704ce45) and are removed from agent-utilities, which retains only the 4 agent-utilities-* skills. Every verb named below is still reachable exactly as documented; find its current skill home via graph-os's graph-runtime-and-governance skill's "Coverage governance" section or the mapping in §5.

Retirement note (current). The retired tool-mode profile switch described in §2-§3 below (condensed/verbose/both/intent) has since been retired. There is now exactly one MCP tool contract, unconditionally: graph-os serves only the intent tools (ask, find, write, act, manage, why, plus the two MCP Apps launchers), each taking action + params (+ optional natural-language intent) — not the hints_json shape this doc describes below. The ~95 granular action-routed tools this doc calls the "condensed" surface still register fully on a private backing FastMCP server and dispatch through the same _execute_tool/REGISTERED_TOOLS core; they are simply never listed, reached instead via act(action="<tool>.<op>") or find(action="describe"). There is no gating tag, no per-deployment mode, and no load_tools/unload_tools escape hatch on graph-os itself (that session-visibility mechanism lives on in the standalone mcp-multiplexer gateway for fleet tools). See agent_utilities/mcp/intent_contract.py and agent_utilities/mcp/graphos_surface.py for the current, authoritative contract; the rest of this document is retained as the historical design record of how the collapse was built.

1. Problem

graph-os's condensed (default) MCP surface is already ~95 tools. Past a point, more tools lowers LLM tool-selection accuracy — worst for Haiku / local / small-context models. The granular tools (and every REST route) must stay exactly as capable as today; only the LLM-facing tool list needed a smaller, additional front door.

2. What this ships (historical — see the retirement note above for the current contract)

flowchart TD
    subgraph Intent["graph-os — the one tool contract (always on)"]
        V["6 intent verbs\nask · find · write · act · manage · why"]
        R["Resolver\n(intent_tools.resolve_intent)"]
        G["Granular backing tools\n(~95 operations, registered but never\nlisted in tools/list)"]

        V --> R
        R -->|"ranked capability + why"| G
    end

    X["_execute_tool core\n(REGISTERED_TOOLS)"]
    REST["REST gateway\n(ACTION_TOOL_ROUTES)"]

    G --> X
    X --> REST

    style Intent fill:#efe,stroke:#8a8

(Historical note: the kickoff design below shipped this as an opt-in the retired intent tool mode profile alongside a default condensed mode and a load_tools/unload_tools escape hatch on graph-os itself. Both the mode switch and the local gate were later retired — the diagram above reflects the current, unconditional contract; see the retirement note at the top of this document.)

  1. Six intent-verb tools (agent_utilities/mcp/tools/intent_tools.py) — ask (NL/UQL read), find (capability discovery), write, act, manage, why. Each takes action: str (an operation id "<tool>.<op>", or "describe") + params: dict (structured args) + optional intent: str (natural language, routed when action is empty) + execute: bool. Small, fixed schema in either case of how multiple granular tools exist behind it. (The kickoff design below originally shaped this as intent + hints_json; the condensed action/params shape superseded it — see the retirement note above.)
  2. The resolver (resolve_intent/dispatch_intent) — ranks every REGISTERED_TOOLS entry tagged for that verb (TOOL_VERBS) with a dependency-free lexical scorer (token overlap + a name-coverage tie-breaker). Focused action facades publish Literal[...] action contracts into the generated manifest; an exact multi-word action phrase such as code context contributes route evidence, while an explicit unpinned action first narrows candidates to its current owning tools and scores only that selected operation. The resolver then picks the top candidate and dispatches it through the same _execute_tool core every condensed tool uses, and returns {"result", "routing", "executed"} — routing carries the chosen tool/action, matched terms, alternatives considered, and a plain-English "why". ask also falls back to nl_query (the engine's own NL planner) when the winning candidate needs structured args the caller didn't provide.
  3. (Retired) the profile switch — the kickoff design gave the retired tool-mode switch a 4th value, intent, alongside condensed/verbose/both (mcp/verbose_tools.py). In intent mode the condensed tools still registered fully (REST + _execute_tool + REGISTERED_TOOLS — nothing lost) but were also tagged GATED_TAG (+ the mode-independent GRANULAR_TAG) and held back from a session's default tool list, while condensed (the default) stayed unaffected. The whole mode switch has since been retired: graph-os now serves only the intent contract, unconditionally, with no per-deployment toggle.
  4. (Retired) the escape hatch, both directions — the fleet load_tools/unload_tools meta-tools (mcp/multiplexer.py) manage session-visibility for external fleet tools (MCPMultiplexer._exposed); the kickoff design extended the SAME mechanism to graph-os's own gated tools (_local_gated) so load_tools(tools=["graph_query"]) can reveal an exact granular tool with no mounting needed, and unload_tools retract it again. graph-os no longer gates its own tools this way — the granular backing tools are simply always reachable via act/find(action="describe"), with no local-gate state to load/unload. The load_tools/unload_tools mechanism itself lives on, unchanged, for the standalone mcp-multiplexer gateway's fleet tools.
  5. (Retired) responsible tool usage — the load→use→unload lifecycle (CONCEPT:AU-ECO.mcp.intent-surface-tool-lifecycle). load_tools(..., auto_unload=true) marked a tool for automatic retraction the moment it was next called, and the manage intent verb exposed the same lifecycle core directly. With graph-os's own gating retired, this lifecycle no longer applies to graph-os's granular tools — reclaiming context there is now moot, since they were never individually listed in the first place. It remains the live mechanism for fleet tools mounted through mcp-multiplexer.

3. Resolver design — CPD-backed, with a graceful pre-CPD fallback

resolve_intent/dispatch_intent now rank each capability against its OWN generated Capability Power Descriptor (docs/capabilities-power.json — one_line/examples/does[] action names + intent_verbs, unioned with the hand-curated TOOL_VERBS entry so switching to the CPD only ever ADDS routing surface, never narrows it) when one exists for that tool. A tool absent from the CPD set (a brand-new tool ahead of the next gen_capability_power.py --write, or the CPD JSON missing entirely — a lean/headless install with no docs/) falls back per-capability to the original local candidate table (REGISTERED_TOOLS docstrings + TOOL_VERBS + _graphos_action_manifest) — never an error, never a silent gap. CapabilityCandidate/resolve_intent/dispatch_intent's shape is unchanged, exactly as this kickoff's original design intended; routing.capability_source reports which substrate actually produced the winning ranking for that dispatch.

On top of the CPD-backed ranking, two more seams landed in the same slice (agent_utilities/mcp/tools/intent_tools.py):

  • Calibrated-outcomes learning loop (CONCEPT:AU-ECO.mcp.intent-surface-outcome-learning). Every dispatch_intent call feeds its success/failure back into OutcomeRouter (agent_utilities/orchestration/outcome_router.py, itself a thin wrapper over CapabilityIndex.record_outcome/reward_of — the SAME durable-bandit mechanism ReasonerRouter/variant_pool.evolve_profile already share, not a second learner), keyed verb:tool. resolve_intent blends each candidate's reward EMA into its lexical score, so a capability that keeps failing under a verb sinks in the ranking and one that keeps succeeding rises — real calibrated-outcomes routing, distinct from the CPD JSON's own (always-empty at static-generation time) calibrated_outcomes field.
  • Resolution caching (CONCEPT:AU-ECO.mcp.intent-surface-resolution-cache). Ranked (non-pinned) resolutions are served from a small bounded in-process LRU keyed by (verb, normalized intent, hints, top_k) plus two monotonic counters — the candidate-table generation (bumps when the CPD/tool surface rebuilds) and the reward epoch (bumps on every recorded outcome) — so a repeated intent is served from cache until the routing policy it was ranked under actually changes.

4. Kickoff-slice scope note (historical) — all closed by §7's follow-up work

The original kickoff slice (Phases 2-3) deliberately shipped without a per-capability CPD, an outcome-learning loop, a dedicated intent-surface skill, or an A/B measurement — each was a named follow-up (§7). All four landed together on feat/au-seam8-complete:

  • Per-capability CPD — feat/au-cpd (Phase 1) shipped docs/capabilities-power.json; the resolver now ranks against it (§3).
  • Calibrated-outcomes / bandit learning loop — resolve_intent/dispatch_intent now record and blend in a learned reward EMA via OutcomeRouter (§3).
  • Dedicated skill — originally agent_utilities/skills/kg-intent/SKILL.md (tier: meta), now folded into graph-runtime-and-governance's "Manage tool visibility responsibly" workflow step (that skill has since moved to graph-os's own skill pack and is removed from agent-utilities), which documents the resolver/dispatcher mechanism directly; ask/find/ write/act/manage/why remain in skill_coverage.INTENTIONALLY_UNSKILLED (correctly — a meta concern never claims verb coverage), with the comment there pointing at that section.
  • A/B selection-accuracy measurement — scripts/measure_intent_routing_accuracy.py + agent_utilities/knowledge_graph/retrieval/intent_selection_accuracy.py (originally a 21-case hand-labelled corpus across all five dispatching verbs, now 25 — see D-ORC-19 in §7) + the tests/unit/test_intent_selection_accuracy.py regression tripwire. Measured 2026-07-11: top-1 76.19%, top-3 85.71% against the original 21-case corpus (a live run of the real resolver, not a fabricated number) — see §7.

5. Skill sweep — preserving every kg-* skill under the condensed intent surface

Scope: agent-utilities' own skills only (agent_utilities/skills/**), not the 785-skill fleet. Principle: zero feature lost — every kg-* skill still documents its exact granular tool(s) unchanged; each one now ALSO explains how to reach that tool when the retired intent tool mode is active.

What changed, uniformly, in all 53 verb-wrapping kg-*/kg-modality-* skills (tier core or modality — computed from skill_coverage.discover_skills(), the SAME machinery the MCP⇄REST⇄skill parity gate uses): one standardized note inserted right after the # kg-<name> heading (frontmatter, ## Invoke, and every other section untouched).

Post-collapse note: the "Skill" column below names the former kg-* slug each row documented at the time this note shipped. On feature/authority-convergence all 53 were folded into 13 broad domain skills (agent_utilities/skills/graph-*/agent-utilities-*) — the tool/verb mapping is still exactly accurate, but agent_utilities/skills/kg-<name>/ no longer exists on disk. The 7 graph-* domain skills have since moved to graph-os's own skill pack and are removed from agent-utilities, which retains only the 4 agent-utilities-* skills. Cross-reference graph-os's graph-runtime-and-governance skill's "Coverage governance" section (or python -m agent_utilities.mcp.skill_coverage) for which broad skill documents a given tool today.

Condensed intent-surface note (Seam 8). Under the small/cheap-LLM profile (the retired intent tool mode), <tool(s)> is/are held back from the default tool list (nothing removed — REST + _execute_tool still reach it/them exactly as documented below). Two ways to use this skill unchanged: (1) load_tools(tools=["<tool>"]) once per session (as below), then proceed exactly as documented; or (2) call the <verb> intent verb with the same natural-language request — the resolver routes to <tool> for the operator and returns the result plus a routing justification. The retired default condensed tool mode was completely unaffected.

Skill Wrapped tool(s) Verb
kg-analyze graph_analyze ask
kg-ask graph_ask, ask_data ask
kg-broker graph_broker act
kg-bus graph_bus act
kg-code graph_code, graph_code_nav ask
kg-configure graph_configure manage
kg-context graph_context ask
kg-document-tree graph_document_tree ask
kg-etl graph_etl write
kg-evaluate graph_evaluate why
kg-explain graph_explain why
kg-extract-concepts concept_registry find
kg-feedback graph_feedback why
kg-feeds graph_feeds act
kg-fork graph_fork act
kg-gis graph_gis ask
kg-goals graph_goals, spec_ticket act
kg-hydrate graph_hydrate manage
kg-ingest graph_ingest, source_sync, source_drain, source_connector, document_process write
kg-kvcache graph_kvcache manage
kg-loops graph_loops act
kg-memory graph_memory write
kg-message graph_message act
kg-modality-analytics engine_analytics, engine_datascience, engine_graphlearn, graph_learn ask
kg-modality-blob engine_blob write
kg-modality-channels engine_channels, engine_broker act
kg-modality-finance engine_finance ask
kg-modality-ledger engine_ledger write
kg-modality-mining engine_mining, graph_mine, graph_mine_deep ask
kg-modality-nodes-edges engine_nodes, engine_edges, engine_graph, engine_lifecycle write
kg-modality-streaming engine_streaming act
kg-modality-timeseries engine_timeseries write
kg-modality-txn engine_txn act
kg-observe graph_observe why
kg-ontology graph_ontology, ontology_property_types, ontology_value_types, ontology_interface, ontology_sampling_profile, ontology_function, ontology_derive, ontology_link_materialize, ontology_leanix_sync, object_edits, object_index, object_permissioning, object_set write
kg-ops-causal graph_ops_causal why
kg-orchestrate graph_orchestrate act
kg-persist-report research_artifact ask
kg-promql graph_promql ask
kg-query graph_query, nl_query ask
kg-reach graph_reach act
kg-research graph_research ask
kg-runvcs graph_runvcs act
kg-sandbox graph_sandbox act
kg-schedules graph_schedules manage
kg-search graph_search, graph_search_synthesis, graph_federated_search ask
kg-secret graph_secret manage
kg-sessions graph_sessions, ingest_sessions, usage_query manage
kg-share graph_share write
kg-table graph_table ask
kg-traces graph_traces ask
kg-write graph_write write
kg-writeback graph_writeback write

act seeds its natural-language text into graph_orchestrate.task. With no agent_name, GraphOS resolves an ingested skill/workflow in the KG and delegates the whole execution to local vLLM; authenticated child MCP schemas remain internal. The non-read preview/plan_ref contract still governs the dispatch, and the result returns only bounded output, opaque provenance, tool names/statuses, and any approval request. For pinned orchestration intents, the agent and server hint aliases normalize to agent_name; do not provide an alias and agent_name with different values. Every other hint must be a documented parameter of the selected tool and is rejected before a preview is stored.

Not touched (correctly exempt — tier: meta/surface, not verb wrappers): the former kg-capability-builder (folded into agent-utilities-development), kg-coverage-doctor and kg-mux-extend/kg-mux-use (folded into graph-runtime-and-governance), kg-delegate (folded into graph-orchestration-and-automation) — the latter two skills have since moved to graph-os's own skill pack and are removed from agent-utilities — plus, from the separate agent-webui package (not part of this collapse), kg-webui-admin, kg-webui-dashboards, kg-webui-extraction, kg-webui-graphviz, kg-webui-ontology-operator, kg-webui-swe.

Higher-level docs also updated (mention tool names/the retired tool-mode switch in prose, not a per-capability wrapper): - agent_utilities/skills/skill_graphs/agent-utilities/tools/SKILL.md — the platform's own tool reference gained a top-of-file note pointing at this doc. - agent_utilities/skills/skill_graphs/agent-utilities/SKILL.md — the one-line tool list now notes the intent profile alternative. - the graph-os repo's graph_os/skills/graphos-genesis/SKILL.md — the env-var canon section (the retired tool-mode switch enum) now lists intent as a 4th valid value with a one-line explanation, so a genesis-provisioned deployment's drift-guard/docs stay accurate. (No code change needed — check_env_var_drift.py only checks for the KEY's presence, not an enum of values.)

Checked NOT needed: agent-utilities-self-evolution, agent-utilities-deployment, agent-utilities-source-integration, autonomous-contribution skills reference graph-os tool names only as illustrative examples of existing behavior (e.g. graph_write + graph_query in a smoke test) that remains equally true under any retired tool mode — nothing in them assumes a specific tool-visibility default, so no edit was needed to keep them accurate. (The latter two have since moved to graph-os's own skill pack and are removed from agent-utilities.)

6. Tests

  • tests/unit/test_intent_surface.py — the resolver + dispatch_intent (hermetic: a fake REGISTERED_TOOLS entry, no live engine): the required end-to-end proof (ask resolves + dispatches via _execute_tool + returns the justification), the NL-planner fallback, the explicit-tool-hint pin, a dispatch failure reported as structured error (not a crash), and that graph_query — the tool graph-query-and-explanation documents (now graph-os's own skill) — still resolves under ask (no feature lost).
  • tests/unit/test_intent_surface_build_server.py — builds the REAL graph-os server (bootstrap=False, no live engine) under the retired intent tool mode: verbs + REST twins register, the granular surface (graph_query, graph_write, nl_query, …) stays fully registered, and the default condensed mode is unaffected (regression guard).
  • tests/test_intent_surface_gating.py — the local-gate + lifecycle mechanism against a real FastMCP server + Client: a gated tool is hidden by default, load_tools reveals it (and it is then actually callable), unload_tools retracts it by exact name / whole server / toolset tag, auto_unload retracts it automatically right after its next call (and load_tools brings it straight back), and the manage verb's lifecycle shortcut reaches the same core.
  • tests/unit/mcp/test_verbose_tools.py::test_surface_intent_mode_gates_condensed_tools — intent mode in register_tool_surface tags condensed tools GATED_TAG/GRANULAR_TAG and gated_tool_names() surfaces them, without registering a verbose 1:1 surface.
  • tests/unit/test_gateway_mcp_parity.py (existing, unmodified contract) stays green: every intent verb gets a REST twin via the SAME generic ACTION_TOOL_ROUTES mechanism nl_query/ ask_data already use, mounted by the SAME generic include _mount_rest_routes — no bespoke REST wiring was needed.
  • tests/conftest.py's _isolate_registered_tools fixture was extended to ALSO snapshot/restore ACTION_TOOL_ROUTES (previously only REGISTERED_TOOLS) — a test that builds the server under the retired intent tool mode was otherwise the first thing in the whole suite to add a conditionally-registered ACTION_TOOL_ROUTES entry, which leaked into later tests as a false "phantom route" (every existing dynamic entry, e.g. nl_query, is added unconditionally on every build, so this gap never surfaced before).

§7 follow-up tests (added on feat/au-seam8-complete):

  • tests/unit/test_intent_surface.py::test_resolver_ranks_against_the_generated_cpd_when_available and ::test_dispatch_reports_cpd_capability_source_for_a_cpd_backed_tool — a real CPD-backed tool (graph_query) is ranked using its CPD text and dispatch_intent reports the CPD as capability_source.
  • ::test_outcome_learning_biases_a_later_resolution — two lexically-tied fake tools; a recorded success on one flips a LATER unpinned resolution's top pick in its favor.
  • ::test_resolution_cache_hits_repeat_intent_misses_a_different_one — the same (verb, intent) hits the bounded cache (identical ranking, no new entry); a different intent is a fresh miss.
  • tests/unit/test_intent_selection_accuracy.py — the A/B corpus is bounded (15-30 cases) and covers all five dispatching verbs; live-measured accuracy stays above a floor set with headroom below the measured baseline (top-1 76.19% / top-3 85.71%).
  • tests/conftest.py gained _isolate_intent_outcome_learning — resets the shared OutcomeRouter + resolution cache between EVERY test in the suite (not just test_intent_surface.py's own local fixture), so a dispatch outcome recorded against a REAL tool (e.g. graph_query) in one test file can never bias ranking in an unrelated, later test.

7. Program phases 4-5 — CLOSED on feat/au-seam8-complete

All five items below shipped together; each is cross-referenced to where it landed.

  1. Merge feat/au-cpd and swap the resolver onto it — DONE. feat/au-cpd (Phase 1) had already merged to main ahead of this slice; _build_candidates/resolve_intent in agent_utilities/mcp/tools/intent_tools.py now rank each capability against its own CPD (one_line/examples/does[]/intent_verbs, unioned with TOOL_VERBS) with a graceful per-capability fallback to the pre-CPD lexical table (§3). CapabilityCandidate/ resolve_intent/dispatch_intent's shape is unchanged.
  2. Calibrated-outcomes learning loop — DONE (CONCEPT:AU-ECO.mcp.intent-surface-outcome-learning). dispatch_intent records every execution's success/failure into OutcomeRouter (reusing CapabilityIndex.record_outcome/reward_of — no second learner); resolve_intent blends the learned reward EMA into its lexical score. Proven by tests/unit/test_intent_surface.py::test_outcome_learning_biases_a_later_resolution — two lexically-tied fake tools, one recorded success, the later unpinned resolution flips to prefer it.
  3. Cache resolution — DONE (CONCEPT:AU-ECO.mcp.intent-surface-resolution-cache). A bounded in-process LRU (_RESOLUTION_CACHE, 256 entries) keyed by (verb, normalized intent, hints, top_k, candidate-table generation, reward epoch) — the last two counters make it a policy-version-aware cache: a CPD/tool-surface rebuild or a freshly recorded outcome invalidates exactly the entries that can have used it, without a manual flush. A bare in-process cache was used rather than the Seam 6 KV-cache seam (that seam caches LLM KV blocks keyed on prompt-prefix reuse; this cache holds ranked CapabilityCandidate lists keyed on normalized intent — a different cache shape, better served by its own small LRU than by overloading the LLM KV-cache layer). Proven by test_resolution_cache_hits_repeat_intent_misses_a_different_one.
  4. Feed dispatch outcomes back into calibrated routing — DONE, folded into item 2. The checked-in CPD JSON's own calibrated_outcomes field stays empty at static-generation time (no live engine reachable when gen_capability_power.py --write runs) — the REAL calibrated-outcomes signal for live routing is the in-process OutcomeRouter reward EMA the resolver blends in, which is exactly what a bandit needs (fast, updated per-call) and is exposed per-dispatch as routing.calibrated_outcome_reward.
  5. A dedicated tier: meta skill for the intent surface — DONE: originally agent_utilities/skills/kg-intent/SKILL.md, now graph-os's own graph-runtime-and-governance skill's "Manage tool visibility responsibly" step — the six verbs, the CPD/learning/caching mechanism, the load→use→unload lifecycle, and when to use condensed vs. intent mode.

A/B selection-accuracy measurement (design doc §4 phase 4) also shipped in this slice: scripts/measure_intent_routing_accuracy.py (CLI) + agent_utilities/knowledge_graph/retrieval/intent_selection_accuracy.py (originally the 21-case corpus + measurement function) + tests/unit/test_intent_selection_accuracy.py (CI regression tripwire). Measured against the real, CPD-backed resolver: top-1 76.19% (16/21), top-3 85.71% (18/21) — a live run, not a fabricated number (see the module docstring for method and why "naming the tool directly" is the 100%-by-definition baseline this is traded against, not a competing measurement). A model-in-the-loop task-success soak (vs. a small/cheap model, per the original phase-4 wording) remains a natural follow-up once a model-eval harness is wired to this corpus — this slice measures resolver accuracy alone, which is the mechanism Seam 8 itself owns.

D-ORC-19 / D-INT-4 (2026-08-01) — the 21-case corpus above measured resolver accuracy in general but contained exactly ONE delegation case, and it only passed because its wording contained the target tool's own name-token ("orchestrate"). No case named an ingested SKILL, the actual operator delegation workflow, so the floor stayed green while unpinned delegation ("act: Delegate to the servicenow-incident-management skill: ...") routed to an unrelated tool 100% of the time — the skill's own name tokenized to servicenow/incident/management, which out-scored graph_orchestrate's single skill doc-token hit (graph_incident 0.3383 vs graph_orchestrate 0.1111). Fixed by (1) adding four skill-naming delegation cases (none containing "orchestrate") to the corpus, growing it to 25, and (2) teaching the resolver a structural skill-delegation shape: any CPD that declares a skill_name input parameter (today only graph_orchestrate — derived from typed_io.input_params, not the tool's name, so a future delegation façade qualifies automatically) gets a flat +0.5 score bonus whenever the intent text matches a hyphenated-slug-adjacent-to-the-word-"skill" pattern (_SKILL_DELEGATION_RE) — independent of what domain words make up the slug, so naming any skill no longer fights the router. Re-measured on the 25-case corpus at the same commit: top-1 76.00% (19/25), top-3 84.00% (21/25), with all 5 delegation cases (the original plus the 4 new ones) now top-1 hits; the same corpus against the pre-fix resolver measured top-1 64.00% (16/25), top-3 80.00% (20/25). Unpinned plain-language skill delegation is therefore now correctly routed; pinning (hints_json={"tool": "graph_orchestrate"}) remains supported and unaffected.