4.8 KiB
CONTRACT — model-router-floor (wave 1-B1: user effort floor for the turn)
- date: 2026-10-08 | flow: feat | branch: feature/model-router-mod
- status: active
REQUEST (verbatim — IMMUTABLE)
AskUserQuestion 2026-10-08, question: "Aujourd'hui, ultrathink met le tour en max, mais si je déclare une phase (ex. orchestrate) puis charge un skill, ton max est perdu pour la suite du tour. Ça contredit notre règle « un choix explicite bat la phase déduite ». Quel sens donner à ultrathink et à un /effort-x tapé par toi ?"
User's answer: "Plancher pour le tour (Recommended)" — option text: "Ton niveau est un minimum pour tout le tour. Les routes du modèle et des skills peuvent monter au-dessus (escalade à max), jamais descendre en dessous. Il passe aussi par-dessus un /route sticky plus bas."
Session rule this fixes (wave plan, user-approved 2026-10-08): "an explicit per-call choice (Agent model/effort param, /route, ultrathink) beats the derived phase for that span".
User, same turn: "continu avec Opus en /ultrathink".
CLARIFICATIONS
Q: which loops does the floor cover? / A: the MAIN loop only ("le tour" = the user's turn); sub-agents keep their own routes and pins. [orchestrator — derived, stated to the user]
Q: model axis / A: the floor constrains effort only. A floor route's model (none by default: ultrathink → escalate carries none) applies on main only when neither the sticky nor the turn route names a model, and only with the switch on. [orchestrator — internal]
Q: numeric or absent engine effort / A: the floor level replaces it (the user's explicit level wins over an unknown budget); the haiku effort omission still applies after flooring. [orchestrator — internal]
Q: who clears the floor / A: main turn end (a queued prompt's floor is then promoted), /route clear, /route off (pass-through). A model route({clear}), a Skill(effort-*) or any skill load never touches it. [orchestrator — derived from "jamais descendre en dessous"]
ACCEPTANCE CRITERIA
- Suite green with the new tests:
claude plugin testpasses with at least 19test(calls;claude plugin validatepasses with no warning; no line over 80 chars; noanytype. CHECK: cd mods/model-router && out=(claude plugin test . 2>&1); rc=?; echo "$out" | tail -n 3; [ $rc -eq 0 ] && [ "$(grep -cE '^\stest(' hooks/register.test.ts)" -ge 19 ] && v=$(claude plugin validate . 2>&1) && echo "$v" | grep -q 'Validation passed' && ! echo "$v" | grep -qi 'warning' && ! grep -nE '.{81,}' hooks/register.ts hooks/register.test.ts && ! grep -nE ':\sany\b||as any\b' hooks/register.ts && echo FLOOR-SUITE-OK EXPECT: FLOOR-SUITE-OK EVIDENCE: pending - Type-check clean against this build's declarations. CHECK: T=/Users/b.chanot/.claude/dev-mods/385f7190-70f5-4bdd-b0d8-e4566cd412fd/model-router/.claude-plugin/types; [ -d "$T" ] || T=/Users/b.chanot/Documents/claude/mods/model-router/.claude-plugin/types; W=$(mktemp -d) && printf '{"compilerOptions":{"target":"es2023","lib":["es2023"],"types":[],"module":"esnext","moduleResolution":"bundler","strict":true,"noUncheckedIndexedAccess":true,"noEmit":true,"skipLibCheck":true,"jsx":"react","jsxFactory":"h","jsxFragmentFactory":"Fragment"},"include":["%s/claude-code/index.d.ts","%s/claude-code-tools/index.d.ts","%s/hooks"]}' "$T" "$T" "$PWD/mods/model-router" > "$W/tsconfig.json" && (cd "$W" && npx --yes -p typescript@5 tsc -p tsconfig.json) && echo TSC-OK EXPECT: TSC-OK EVIDENCE: pending
- The floor is its own slot:
turnFlooris declared inState, initialised innewState, written by the prompt rule and by a typed/effort-<l>, cleared by/route clearand at main turn end; the suite carries at least 6 tests whose name containsfloor. CHECK: cd mods/model-router/hooks && [ "$(grep -c 'turnFloor' register.ts)" -ge 6 ] && [ "$(grep -cE "^\s*test('[^']*floor" register.test.ts)" -ge 6 ] && echo FLOOR-SLOT-OK EXPECT: FLOOR-SLOT-OK EVIDENCE: pending - Judged by reading: effective main effort = the higher (LEVELS order) of the floor and
(userMain ?? turnMain)?.route.effort ?? e.effort, the floor replacing a numeric or absent engine effort; the floor never applies to a sub-agent step;turnMainonly ever holds 'model' or 'skill' sources; the prompt rule writesturnFloor(orpendingPromptwhen typed mid-turn withwait), promoted intoturnFloorat main turn end; a non-effort skill load resetsturnMainonly; a modelroute({clear})clearsturnMainonly; every answer that the floor overrides says so truthfully (Skill bridge context, route tool text,/effort-<l>text);/route showand the status line display the floor; every criterion of.claude/tasks/contracts/2026-10-08-model-router-w1a-1533.mdstill holds; no function over 25 logic lines.
FILE SCOPE
mods/model-router/hooks/register.ts · mods/model-router/hooks/register.test.ts