# Capability Selection — the broker is the authority (#24, P1-01) ## What changed `capability_broker.py` is now the **single capability-selection authority** (authority map `capability_selection` → v1_owner `ContextBroker.capability_selector`). Before #24 every run exposed all 19 tool schemas to the model and the broker was advisory. Now: - `select_for_job(task_description, need_tools=True)` returns an ordered tool-schema selection with a recorded reason. The operator feeds that exact list into `LocalToolRuntime(tools=...)` at both construction sites, so **unselected capabilities contribute zero prompt tokens** (schemas are excluded from the native `tools` payload, not just hidden from the prompt block). - **Verified floor**: every tool-using job sees at least `FLOOR_TOOLS = read_file, write_file, edit_file, list_directory, grep_files` plus the escape hatch — selection is never empty and never misses the floor for coding tasks. A zero-score task falls back to the floor with reason `"floor (no matches)"`. - **Escape hatch**: `find_capability` is a native read-only tool that is always exposed. Executing it queries the broker (`find_capability(query)`) and returns matching capability summaries with argument schemas; the runtime then **dynamically exposes the discovered native tools for the rest of the run** (within the existing 8-iteration budget — no extra iterations granted). Unknown-tool errors name the hatch so the model can recover in one step. - **Selection is deterministic** (keyword scorer over TOOLS-derived cards, ties broken by name) and bounded (`MAX_SELECTED = 10`). - **Task-family input** reuses `infer_task_family` (#29) for coherence with model routing. ## Ownership boundaries - Selection authority: `capability_broker.select_for_job`. - Execution authority: unchanged in `tool_executor` (#21) — the approval split (`tool_requires_approval` / `PreparedTool`) stays authoritative for gating; per-tool `risk` metadata informs selection, never bypasses approval. - Degrade-not-crash (P0-06): any broker failure falls back to the full TOOLS set (the pre-#24 behavior) and records a tier-1 `record_failure("capability_selection", ...)`. ## Cards `load_cards` validates the card index and **rebuilds when the file is missing, empty, corrupt, or stale** (mtime older than the registry). Native-tool cards are synthesized from `tool_executor.TOOLS` metadata — the single source for `risk` (`read-only` | `write` | `irreversible`), `preconditions`, and `side_effects` — so selection has full coverage even with no plugins installed. ## Resume round-trip `tool_names` is validated on resume, so the operator persists `exposed_tools` (the selected set plus any tool dynamically exposed via `find_capability` mid-run) into `pending_state`; `_runtime_for_model` rebuilds exactly that set for the resumed runtime. ## Trace Schema v3 (additive): `selected_tools`, `capability_reason`, `find_capability_calls`. v1/v2 rows and readers stay compatible. ## Verification - Frozen tests: `tests/test_p1_01_capability_authority.py`. - Canary: `src/rune/canary_p1_06.py`-style self-bootstrap in `src/rune/canary_p1_01.py` (registered in `.gitea/workflows/ci.yml`) — asserts ~5–10 exposed schemas, a byte-size drop in the tools payload, the escape-hatch round-trip, and the floor guarantee. - Composition: `compose_capability_selection(root, state_dir)` registers exactly one `CapabilityBroker` per composition root.