# User-approved model routing

The parent owns authorization and decisions. `ask_sai` is a bounded classifier
recommending task-to-candidate fit, not an autonomous scheduler or acceptance
judge. Discover it with tool search if it is not declared. Do not silently
substitute another classifier or routing service when it is unavailable.

## Approve a pool

Run `node <skill>/scripts/cli-workers.mjs catalog` for locally configured physical
chat models, supported reasoning levels, context limits, and catalog pricing.
This reads configured model/auth metadata without generating an answer or
refreshing remote model catalogs; it does not prove provider health or quality.
Extension-only provider registrations are not loaded by this metadata helper;
if a desired model is absent, resolve its configuration explicitly rather than
loosening exact-match checks or selecting a similar name.

Use the interactive question tool when available. Ask together for independent
choices: approved model/provider combinations and reasoning levels, restrictions
and preferences, fallback-or-ask policy, maximum concurrency, worker timeout,
maximum correction rounds, and spending policy. Turn combinations into stable
candidate IDs. Reasoning options must come from each model's supported levels.
For a large catalog, ask the user for preferred providers/models first rather
than dumping the entire list. Preserve supplied selections.

Store `pool.json` outside the source checkout in the run-control directory:

```json
{
  "approved": true,
  "candidates": [
    {
      "id": "writer-medium",
      "provider": "<exact-provider>",
      "model": "<exact-model-id>",
      "thinking": "medium",
      "roles": ["implementation", "correction"],
      "evidence": "User prefers this model for routine implementation; not a benchmark result."
    },
    {
      "id": "review-high",
      "provider": "<exact-provider>",
      "model": "<exact-model-id>",
      "thinking": "high",
      "roles": ["review", "integration"],
      "evidence": "<supplied capability evidence or unknown>"
    }
  ],
  "fallback": "ask",
  "minimumConfidence": 0.75,
  "maxConcurrency": 2,
  "timeoutSeconds": 1800,
  "maxCorrectionRounds": 2,
  "spendingPolicy": "<user-approved policy>"
}
```

These are example limits to propose, not preauthorized settings. A fallback ID
must itself be approved, available, compatible with the task, and allowed for
its role. An ineligible fallback means ask, not broaden the pool. Record user
approval in the board; a JSON boolean is a receipt of approval, not its source.
If the pool changes, preserve its prior version for existing worker receipts.

## Build the routing evidence

For each ready task supply only what is needed to classify fit:
- Task ID, objective, role, owned systems/files, and accepted prerequisites.
- Task constraints: language, tool needs, code/context scale, architectural
  uncertainty, security/data-loss risk, acceptance criteria and validation.
- Eligible candidates with exact provider/model/thinking, known context/output
  limits, relevant capability observations, and user preferences.
- User cost/latency priorities and explicit restrictions.

Do not send credentials, private conversation dumps, full source files, or
sensitive paths merely to select a model. Evidence goes to Sai's provider;
obtain permission if the supplied material has an additional sharing boundary.
Record unknown qualities as unknown. Catalog names, price, reasoning labels,
and context capacity do not prove coding quality or measured latency. Include
forked-history context size in fit; a short task brief does not remove inherited
history, and existing compaction entries still govern effective model context.

## Ask a narrow choice question per task

Call `ask_sai` with one question for each independent ready assignment (at most
8 per call, and never above the actual tool's limits). Use `state` for the
sanitized evidence. Each question is `type: "choice"`; its criteria map stable
eligible candidate IDs to concrete fit descriptions, plus
`insufficient_evidence`. Example structure:

```js
const result = await tools.ask_sai({
  state: "<approved candidates, evidence, user priorities, task contracts>",
  questions: {
    T01: {
      type: "choice",
      instructions: "Which approved provider/model/reasoning candidate best fits T01 under the supplied constraints?",
      criteria: {
        "writer-medium": "<exact combination and supplied evidence of fit>",
        "review-high": "<exact combination and supplied evidence of fit>",
        "insufficient_evidence": "The evidence does not support a choice, or no approved candidate satisfies the task constraints."
      }
    }
  }
});
```

The criteria must include only candidates eligible for this task's role. If no
candidate is eligible, ask the user without a classifier call. If exactly one
is eligible, use it after compatibility checks; do not spend a classification
call to repeat the only possible assignment.

Inspect the actual returned schema rather than inventing an `answers` shape.
Handle tool errors, `failure`, refused/truncated evidence, missing task answers,
unknown choice IDs, `insufficient_evidence`, and confidence below the approved
threshold. Missing confidence is uncertainty, not a made-up high score. Use the
approved compatible fallback or ask; do not re-query until a desired answer wins.

## Validate and record

The parent checks role eligibility, exact catalog identity, supported reasoning,
context fit, restrictions, and budget policy before launching. The helper
rechecks model availability/reasoning at launch and records resolved settings
at completion. It refuses unavailable exact models and unsupported levels;
no fuzzy matching, virtual-model routing, or automatic provider/model fallback.

Record task ID, eligible candidates, recommendation, confidence if supplied,
fallback reason if used, chosen triple, and catalog/evidence timestamp in the
board and launch `routing` object. Routing recommendations are not performance
measurements. Update capability observations only from actual evidence and
re-route materially changed tasks, not every status check.
