41 lines
9.8 KiB
Markdown
41 lines
9.8 KiB
Markdown
# CONTRACT — model-router-w3a-confirm
|
|
- date: 2026-10-10 | flow: feat | branch: feature/model-router-confirm (to start off develop)
|
|
- status: active
|
|
|
|
## REQUEST (verbatim — IMMUTABLE)
|
|
une fois fini j'aimerais que pour les premiere fois, les premiers switch de model et de'effort, on est un prompt qui demqnde de confirmer si on utilise bien ce model ou si moi j'en recommande un autre. Ca permet de voir si le routing correspond bien a nos besoin dans la pratique. sois faire un truc qui se souvient en userscop (si ca demande de confirmer la route sur un autre projet pour tel tache, que ca ne ele redemande jamais sur un autre projet. une memoire de ce qu'on decide, et ca met a jour la table de routing existante si il y a des changements, et si on fait un changement demander si c'est une exception pour ce projet ou non. Faire un truc qui demande au debut, mais qui est persistant une fois demander sur tout les projet et qui se redploi automatiquement comment tout le mod.
|
|
|
|
## CLARIFICATIONS
|
|
Q: where does the decision memory live? / A: "le 1 [fichier suivi dans le repo], mais il faut qu'il soit lisible et écrivable par tous les projets, donc le déployer (ln -s) dans le .claude du home à l'install, comme le reste" → tracked `mods/model-router/routing.json`, reached from every project through the EXISTING link `~/.claude/skills/model-router` → `../mods/model-router` (no new link: the plugin's own directory, `$.plugin`, resolves to it) [gated 2026-10-10]
|
|
Q: how is the question asked? / A: blocking dialog at the moment of the switch (`$.ui.ask`, the engine's AskUserQuestion), options Keep / Change / Later; never in headless; `/route ask off` cuts it [gated 2026-10-10]
|
|
Q: granularity? / A: per skill row, per agent row, per main-loop phase declared through the route tool; each once, across projects [gated 2026-10-10]
|
|
Q: public names (orchestrator default, user may veto): `/route pending`, `/route ask on|off` (`/route set` and `/route confirm` deferred after the simplicity lens); dialog texts in English like the rest of the mod; project exceptions live in `routing.json` under `projects[<normalized repo key>]` (r3: the project-tree file `<project>/.claude/model-router.json` was dropped after the robustness lens showed a cloned repo could re-route the user's gate agents and that writes would land in foreign trees) [stated 2026-10-10, user may veto]
|
|
|
|
Q: live T2 dialogs answered during the run (verifier → judge, feater → judge, Everywhere; the working-tree mod was hot-loaded by the engine without /reload-plugins) / A: user "Restaurer les deux" → routing.json reset to the shipped rows, confirmed/changed emptied; criterion 9 evidence: T2 dialog seen and written twice (texts and scope question confirmed live) [gated 2026-10-10]
|
|
|
|
Q: live decisions during the gates (verifier Keep, feater Keep, security-auditor → implement Everywhere) / A: user "Revenir à verify" for security-auditor (row reset, its confirmed/changed entries removed); the two Keeps stay. Criterion 9 evidence: T2 dialog seen 5 times live (Keep, Change + scope, dismissed/Later), the mod hot-loaded from the working tree by the engine [gated 2026-10-11]
|
|
Q: security gate BLOCK(1) credential fragment in the remote key / A: fixed (strict URL/scp parsing, credentials never read), `local:` path keys removed (no remote → Everywhere/Later only), output size cap; re-verify ECARTS(1) on guard fixtures → closed; re-scan PASS [gated 2026-10-11]
|
|
|
|
## ACCEPTANCE CRITERIA
|
|
1. `mods/model-router/routing.json` (tracked) is the single source of the phase table (11 full routes) and of the skill and agent rows (56 skill rows, 21 agent rows + Explore/Plan of plan r4 § Row tables), plus `confirmed` (skills/agents/phases → the confirmed phase) and `ask` (boolean); every row value is a phase key of that table; `DEFAULT_CONFIG.skills` and `.agents` in `register.ts` are `{}` (its `phases` stay as the fallback when the file is unreadable); rows and phases arrive from the file at load (session.start, or lazily once after a `/reload-plugins`), on `/route reload`, after each write, after `/clear` and on a cwd change.
|
|
CHECK: bash .claude/tasks/contracts/w3a-routing-file.sh
|
|
EXPECT: W3A-ROUTING-FILE
|
|
EVIDENCE: MET exit=0 marker-found :: W3A-ROUTING-FILE
|
|
2. Config layers, later wins: `routing.json` (phases, rows, then its `projects[<repo key>]` rows for the current repo) < `~/.claude/model-router.json` (machine override, its `ask: false` honored); a `.claude/model-router.json` inside the project tree is NEVER read; phases merge first (an invalid or missing routing.json phase falls back to the code default by name, logged), rows validate against the final table; a failed layer is skipped only at the first load, afterwards a failed read keeps the whole previous config; session toggles survive a rebuild; `/clear` keeps the config and re-reads at the next prompt. One kit test per clause.
|
|
3. First use asks once, one dialog: a typed skill with a row (main, allowed origin), a rowed agent spawn without an explicit `model` (`parentAgentId` undefined), a main-loop phase declared through the route tool → `$.ui.ask` whose question carries the REAL next model id and effort (decideFor/mainEffort on main, spawnTarget + explicit effort on spawn) and the options Later / Keep / <alt phase> / <alt phase> (T3: Later / Keep, only for a phase-key call); the file is re-read right before the dialog and the key re-checked as decided (another session's decision is seen); Keep writes `confirmed.<kind>.<name> = phase` and `confirmed.phases[phase]` and applies the row; an alt or a valid "Other" phase asks the scope (Everywhere → `routing.json` row + `changed.<kind>.<name> = {from, to}`; This project only → `routing.json` `projects[<normalized repo key>]` row, base row untouched, `confirmed` = the base row so no other project asks; one serialized write; a key failure offers Everywhere only), the new route applies to the current decision at once; any other answer (Later, dismissed, rejected in headless, unknown phase) → default applied, nothing written, not asked again this session; at most one dialog in flight: a concurrent use (same or another key) applies its current route unasked; a phase endorsed by a Keep is not asked at T3; a row that exists in `projects[key]` or in the machine override counts as decided; never asked inside a sub-agent (`parentAgentId` set), with an explicit `model` param, with the mod off, with `ask` false, or while routing.json is unreadable. One kit test per clause.
|
|
4. Writes happen only from a dialog answer or the composer `/route ask` command (never from the route tool, a prompt rule, or any model-originated event); only to `${$.plugin.root}/routing.json`, never into a project tree; the writer refuses when the file is absent or unparsable (toast, answer = Later) and never creates it; read-modify-write of the whole JSON (2-space, key order phases, skills, agents, projects, confirmed, changed, ask), serialized through one promise chain; a write or rebuild failure logs once, applies the default and leaves the key unasked; after a write the config is rebuilt (swapped only on a successful read) and a toast says "routing.json updated: commit it from the config repo (chore branch)". One kit test per clause + reading.
|
|
5. `/route pending` lists the rows and phases not yet confirmed (asked this session first, then the rest); `/route ask on|off` toggles asking and writes `ask`; `/route reload` re-reads the three layers. (`set`, `confirm`, a show suffix: deferred.) One kit test per command.
|
|
6. The kit suite and the mods suite are green; `claude plugin validate` passes.
|
|
CHECK: cd mods/model-router && out="$(claude plugin test . 2>&1)" && printf '%s\n' "$out" | grep -qE '[0-9]+ pass' && ! printf '%s\n' "$out" | grep -qE '[1-9][0-9]* fail' && claude plugin validate . 2>&1 | grep -q 'passed' && cd ../.. && make test suite=lib/tests/mods.test.sh 2>&1 | grep -q 'all suites green' && echo W3A-MOD-GREEN
|
|
EXPECT: W3A-MOD-GREEN
|
|
EVIDENCE: MET exit=0 marker-found :: W3A-MOD-GREEN
|
|
7. `lib/tests/effort-routing.test.sh` reads rows AND phases from `routing.json` (only the tier heads still come from `register.ts`), locks `DEFAULT_CONFIG.phases` equal to the file's phases, keeps the drift lock except for a row with a `changed.<kind>.<name>` entry whose `from` route equals the frontmatter and whose `to` equals the row (then a `WARN floor drift` line, no FAIL; a Keep-only drift, an empty or unknown frontmatter value, a hand edit after a change still FAIL; flip-tested), and is green; `lib/effort-shift.md` names the first-use dialog, `/route pending` and `/route ask` in ≤ 6 added lines.
|
|
CHECK: grep -q 'routing.json' lib/tests/effort-routing.test.sh && [ "$(grep -c 'register.ts' lib/tests/effort-routing.test.sh)" -le 3 ] && grep -q 'floor drift' lib/tests/effort-routing.test.sh && bash lib/tests/effort-routing.test.sh >/dev/null 2>&1 && grep -q '/route pending' lib/effort-shift.md && [ "$(wc -l < lib/effort-shift.md)" -le 66 ] && echo W3A-CENSUS-DOC
|
|
EXPECT: W3A-CENSUS-DOC
|
|
EVIDENCE: MET exit=0 marker-found :: W3A-CENSUS-DOC
|
|
8. Hook budget: no dialog or file write on the `turn.step` path; a `$.ui.ask` in flight never blocks a second, unrelated spawn of another agent name beyond the dialog itself; the ask is awaited outside `safely` in the owning hook with a `.catch` → Later. Judged by reading.
|
|
9. Live checks after the user's `/reload-plugins` (EVIDENCE lines added by the orchestrator; the answers committed on the branch before finish): one dialog at each of the three sites; a parallel same-agent dispatch answered after more than 10 s routes the unowned spawn without a timeout; an unanswered dialog on the terminal resolves to "Later" or to nothing written.
|
|
|
|
## FILE SCOPE
|
|
mods/model-router/routing.json (new), mods/model-router/hooks/register.ts, mods/model-router/hooks/register.test.ts, lib/tests/effort-routing.test.sh, lib/effort-shift.md; .claude/tasks/contracts/w3a-routing-file.sh (oracle, orchestrator)
|