chore(tasks): model-router W1-C contract + plan (absolute tiers, breaker fallback, derived phases)
This commit is contained in:
@@ -13,6 +13,7 @@ migration of shifters/pins/model-gate in wave 2 after proof; names model-router
|
||||
- [x] W1-B2 wiring (contract `2026-10-08-model-router-wiring-1835`, 2026-10-09): tracked symlink `skills/model-router` → `../mods/model-router` loads as `model-router@skills-dir` (fresh-process `claude plugin list --json` proves it), `.gitignore` `mods/*/tsconfig.json`, `lib/tests/mods.test.sh` (4 checks, capability probe, bounded, SKIP), doctor `── Mods ──` fail-soft, CLAUDE.md `## mods/`. Plan r2 from 3 lenses (5 MAJOR), feater DONE, gap round (4b label + unbounded probe), verifier CONFORME 8/8, security PASS
|
||||
- [ ] W1-B2 residuals (accepted): `claude plugin <unknown> --help` returns 0 → the suite's capability probe can pass on a CLI without `plugin test` and then FAIL instead of SKIP (fail-closed; fix = grep the probe output for the test usage line); doctor `claude plugin list --json` unbounded; doctor `echo -e` helpers interpolate `$_mod` (tracked folder names only); fallback timeout guard orphans grandchildren (only without coreutils timeout); `update-all.sh` runs `claude plugin update` over `@skills-dir` → one recurring warn (needs an update-all edit)
|
||||
- [x] W1 close-out (2026-10-09): doc-sync opus audit SIGNIFICANT → user go all 10 → patched (README effort routing + Explore row + /route, USAGE, ARCHITECTURE mods/, CHANGELOG Unreleased) b22f894; hot-reload link removed from `~/.claude/dev-mods/<session>/`; BDR-115 amended (a6e2003). Pending user: `/reload-plugins` in this session (new sessions load the skills-dir copy by themselves); merge decision on feature/model-router-mod (`gitflow finish`, human signal)
|
||||
- [ ] W1-C adaptive tiers (user 2026-10-09: "un système logique et optimisé", no /route typed, works on a haiku session, falls back when fable has no credit): contract `2026-10-09-model-router-tiers-1237`, plan same slug. Phases name absolute tiers (best/big/work/cheap), availability breaker (turn error/refusal + engine auto switch, 15 min cooldown), fallback chain fable→opus→sonnet→haiku, main upgrade on / downgrade gated, prompt default rules (plan/reflect keywords FR+EN) + dispatch push/pop + optional classifier
|
||||
- [ ] W2 migration (after wave 1 proven in daily use): 15 skills `Skill(effort-*)` → `route` tool calls; remove `skills/effort-*`, `lib/effort-pins.txt/.sh`, install/update steps, `effort:` frontmatter on skills and agents; repo agents into the mod's `agents` table (verify the spawn/first-step ordering first); `lib/model-gate.md` + `lib/model-check.sh` → mod rule; census tests repointed; `lib/effort-shift.md` rewritten; docs
|
||||
- [ ] W2 migration: 15 skills off `Skill(effort-*)`, remove shifters + effort-pins + model-gate, census repointed, docs
|
||||
- [ ] W3 optional: step heuristics, haiku classifier, quota-aware downgrade, A/B
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# CONTRACT — model-router-tiers (wave 1-C: absolute tiers, availability fallback, derived phases)
|
||||
- date: 2026-10-09 | flow: feat | branch: feature/model-router-mod
|
||||
- status: active
|
||||
|
||||
## REQUEST (verbatim — IMMUTABLE)
|
||||
User (fr): "j'aimerais ne pas avoir a reflechir a tout ca, donc ne pas lancer les /route moi meme par exemple. On vois le reflect, orchestratm escalate et plan session (fable) par du preincipe que la sessino est sur fable de base ? Si on est sur haiku j'aimerias que ca fonctionne aussi avec les models adapte. et d'ailleurs que se passe til quand on a plus de credit fable, cela passe sur opus xhigh ? il faut se fallback. ET surtout oui avec un systeme adaptatif et pour la reflexion pour trouver une solution ou des idees, il faut le meilleur, pour en faire le plan en se basant sur cette reflexion. Bref un systeme logique et optimise. /ultrathink . ensuite je reload puginm tu test, on commit puis on passe a la vague 2"
|
||||
|
||||
## CLARIFICATIONS
|
||||
Q: "session" phases / A: no phase keeps "the session model" any more: every phase names a TIER (`best`, `big`, `work`, `cheap`), an ordered list of aliases; the first AVAILABLE alias wins. plan/reflect/orchestrate/escalate → `best` (fable, then opus, then sonnet). A haiku session asked to plan runs the plan on fable. [user: "si on est sur haiku j'aimerais que ca fonctionne aussi avec les models adaptés"]
|
||||
Q: what is "available" / A: the mod cannot read per-model quota (rateLimits are account windows: five_hour, seven_day). Availability = a circuit breaker: a model is DOWN for `cooldownMinutes` (default 15) after a main or agent turn ends with `reason: 'error'` or `'refusal'` on it, or when the engine itself switched away from it (`classic.PostModelSwitch`, `source: 'auto'`). A down model is skipped in every tier and in the `fallback` chain; `/route show` lists down models with their reset time; `/route reload` clears the breaker. [orchestrator — derived from the engine's declarations]
|
||||
Q: no credit left on fable / A: main loop on a down model → next available alias of the `fallback` chain (`fable, opus, sonnet, haiku`), effort unchanged (plan stays xhigh → "opus xhigh"), always allowed (a down model yields nothing), logged and shown. [user: "il faut se fallback"]
|
||||
Q: main-loop model moves / A: UPGRADE (phase tier ranks above the current model) allowed by default (`mainUpgrade: true`); DOWNGRADE (cheaper model) still gated by `mainModelSwitch` (default false: one cold-cache step per switch, and haiku's window may not fit); same rank → no switch. [orchestrator — cost/quality trade-off, stated to the user]
|
||||
Q: no `/route` typed by the user / A: phases come from (1) prompt rules at turn start (keyword → phase as the turn's DEFAULT route, source 'prompt', overridable; `ultrathink` stays a FLOOR), (2) the model's own `route` calls and the skills table, (3) derived: an Agent dispatch from main pushes `orchestrate` and the previous route is restored when the last live agent ends, (4) optional classifier (`classifier: false` by default) that asks the engine's small model for a phase label when no rule matched. [user: "ne pas lancer les /route moi-même"]
|
||||
Q: default prompt rules / A: floor: `\bultrathink\b` → escalate. Defaults: `\b(plan|planifie|planning|brainstorm|architecture|con[cç]ois|design)\b` → plan; `\b(pourquoi|why|explique|explain|analyse|analyze|comprendre|understand|review|audit)\b` → reflect. No default rule lowers a turn (no `mechanical` rule): lowering is explicit (route tool, skills). [orchestrator — conservative defaults, user-editable in the override]
|
||||
Q: typed `/effort-<l>` / A: unchanged (floor + default of the turn, B1).
|
||||
|
||||
## ACCEPTANCE CRITERIA
|
||||
1. Suite green: `claude plugin test` passes with at least 40 `test(` calls; `claude plugin validate` passes with no warning; no line over 80 chars; no `any` type.
|
||||
CHECK: cd mods/model-router && out=$(claude plugin test . 2>&1); rc=$?; echo "$out" | tail -n 3; [ $rc -eq 0 ] && [ "$(grep -cE '^\s*test\(' hooks/register.test.ts)" -ge 40 ] && 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 ':\s*any\b|<any>|as any\b' hooks/register.ts && echo TIERS-SUITE-OK
|
||||
EXPECT: TIERS-SUITE-OK
|
||||
EVIDENCE: pending
|
||||
2. Type-check clean against this build's declarations.
|
||||
CHECK: 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
|
||||
3. Tiers in the config: `tiers` (best/big/work/cheap), `fallback`, `cooldownMinutes`, `mainUpgrade`, `classifier` exist in DEFAULT_CONFIG; every default phase names a tier, none a bare model; `/route show` prints the resolved model of each phase and a `down:` line.
|
||||
CHECK: cd mods/model-router/hooks && grep -q "tiers:" register.ts && grep -q "fallback:" register.ts && grep -q "cooldownMinutes" register.ts && grep -q "mainUpgrade" register.ts && grep -q "classifier" register.ts && [ "$(awk '/^const DEFAULT_CONFIG/,/^}/' register.ts | grep -cE "tier: '(best|big|work|cheap)'")" -ge 10 ] && ! awk '/^const DEFAULT_CONFIG/,/^}/' register.ts | grep -qE "^\s+[a-z]+: \{ model: '" && grep -q "down:" register.ts && echo TIERS-CONFIG-OK
|
||||
EXPECT: TIERS-CONFIG-OK
|
||||
EVIDENCE: pending
|
||||
4. Tests prove (names contain the quoted word): `tier` — a `plan` route on a session model `claude-haiku-4-5-20251001` makes the main step run on `claude-fable-5-1` at xhigh (upgrade, default on); `downgrade` — a `mechanical` route on a fable session leaves the model unchanged while `mainModelSwitch` is off; `fallback` — after a main `turn.complete` with `reason: 'error'` on fable, the next main step runs on `claude-opus-5-5` with its effort unchanged, and after `/route reload` fable is used again; `breaker` — a `classic.PostModelSwitch` with `source: 'auto'` from fable marks fable down and `/route show` lists it; `spawn` — `Explore` spawns on `claude-opus-5-5` while sonnet is down; `derived` — an Agent tool call from main sets `orchestrate` and the previous route (`plan`) is back after the last agent's `turn.complete`; `default rule` — a prompt "planifie la migration" sets the `plan` route as the turn default and a later `route` tool call overrides it; `floor` tests from B1 still pass.
|
||||
CHECK: cd mods/model-router/hooks && for w in tier downgrade fallback breaker spawn derived "default rule"; do grep -qE "test\('[^']*$w" register.test.ts || { echo "missing test: $w"; exit 1; }; done && echo TIERS-TESTS-OK
|
||||
EXPECT: TIERS-TESTS-OK
|
||||
EVIDENCE: pending
|
||||
5. Judged by reading: ONE resolver (`resolveModel`) turns a tier name, an alias or a full id into the first AVAILABLE full id (tier → list → skip down → alias → id; a bare alias or id passes through even when down, since explicit means explicit); rank = position in `fallback`; the main-loop decision is: route model wanted → upgrade allowed by `mainUpgrade`, downgrade gated by `mainModelSwitch` + window, same rank → keep; no route and current model down → next available in `fallback`; the breaker is fed only by `turn.complete` `reason` in `error|refusal` (main: the last main step's model; agent: the loop's spawn model) and by `classic.PostModelSwitch` `source: 'auto'` (from_model), never by an aborted turn; the derived `orchestrate` push/pop never overrides a route the model declared after the dispatch; prompt default rules write `turnMain` (source 'prompt'), floor rules write `turnFloor`; the classifier runs only when `classifier` is true and no rule matched, through `$.model.classify`, labels limited to the phase names plus `other`, any failure → no route; explicit Agent params still win; agent model fixed at spawn; every B1/1-A criterion still holds; no function over 25 logic lines; truthful texts name the resolved model and say "fallback" when the breaker chose it.
|
||||
|
||||
## FILE SCOPE
|
||||
mods/model-router/hooks/register.ts · mods/model-router/hooks/register.test.ts
|
||||
@@ -116,6 +116,21 @@ does many different things inside one run. So:
|
||||
Explore without params spawned on `claude-sonnet-5-5`, all 3 steps
|
||||
`medium` (no spawn/step race observed).
|
||||
|
||||
## Decisions 2026-10-09 (user, after the wave-1 table was shown)
|
||||
- No "session model" phase: every phase names an ABSOLUTE tier (best = fable,
|
||||
opus, sonnet · big = opus, fable, sonnet · work = sonnet, opus · cheap = haiku,
|
||||
sonnet); a haiku session asked to plan runs on fable. Reflection and planning
|
||||
always on the best available model.
|
||||
- Availability = circuit breaker (turn error/refusal, engine auto switch), not
|
||||
quota reading (rateLimits are account windows). Fallback fable → opus →
|
||||
sonnet → haiku, effort unchanged ("plus de crédit fable → opus xhigh").
|
||||
- The user never types /route: prompt default rules, dispatch push/pop
|
||||
(orchestrate while agents run, previous route restored), skills table
|
||||
(wave 2), optional classifier. Main upgrade allowed by default, downgrade
|
||||
still gated (cold-cache cost).
|
||||
- Sequence agreed: 1-C lands → user /reload-plugins → live test → commit →
|
||||
wave 2.
|
||||
|
||||
## Wave 0 — spike (dev-mods folder, hot reload, this session)
|
||||
- [x] W0.1 minimal mod: `/route` command, `route` tool, `turn.step` logging +
|
||||
rewrite, `agent.spawn` rewrite, `ultrathink` → max, spinner suffix
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# PLAN — model-router wave 1-C: absolute tiers, availability fallback, derived phases (dispatch-ready)
|
||||
Contract: .claude/tasks/contracts/2026-10-09-model-router-tiers-1237.md
|
||||
Code: mods/model-router/hooks/register.ts (read in full) and register.test.ts.
|
||||
API truth: mods/model-router/.claude-plugin/types/claude-code/index.d.ts
|
||||
(TurnStepInput, TurnCompleteInput + TurnCompleteReason, SessionRateLimit,
|
||||
`classic.PostModelSwitch` → PostModelSwitchHookInput, `$.session.model`,
|
||||
`$.model.classify`, 'claude-code/testing'), .../claude-code-tools/index.d.ts.
|
||||
|
||||
## Why (user, 2026-10-09)
|
||||
1. "Session model" phases assumed Fable. On a haiku session, `plan` at xhigh on
|
||||
haiku is wrong. Phases must name ABSOLUTE tiers.
|
||||
2. When Fable has no credit left, routing must fall back (plan → opus xhigh).
|
||||
3. The user never types `/route`: phases are derived (prompt wording,
|
||||
dispatch spans, skills) or declared by the model.
|
||||
4. Reflection and planning always get the best available model.
|
||||
|
||||
## Engine facts (read in the declarations today)
|
||||
- `SessionRateLimit.kind` ∈ five_hour | seven_day | spend_limit: account
|
||||
windows, NOT per model → availability cannot be read from quotas.
|
||||
- A dead request: `turn.step` result `usage: null`, `stopReason: null`;
|
||||
`turn.complete` `reason: 'error'` (retries exhausted) or `'refusal'`
|
||||
(refused, no fallback model); `'aborted'` = the user interrupted.
|
||||
- `classic.PostModelSwitch` fires on every main-model change with
|
||||
`from_model`, `to_model`, `source` ∈ command|picker|sdk|auto|resume,
|
||||
`context_tokens`, `prompt_cache_warm`.
|
||||
- `turn.step` `e.model` = the id the engine resolved (session's or a
|
||||
fallback's); `$.session.model()` = the main loop's model as `/model` shows.
|
||||
- `$.model.classify(text, labels, { model? })` → label | undefined, rejects
|
||||
on failure; default = the engine's small fast model.
|
||||
|
||||
## Config (additions; everything else unchanged)
|
||||
```ts
|
||||
type Route = { model?: string; effort?: Level } // model: TIER name, alias or full id
|
||||
type PromptRule = { pattern: string; phase: string; mode?: 'floor' | 'default' }
|
||||
type Config = {
|
||||
…existing…
|
||||
tiers: Record<string, string[]> // tier → ordered alias preference
|
||||
fallback: string[] // alias order, best first; = rank
|
||||
cooldownMinutes: number // breaker hold
|
||||
mainUpgrade: boolean // main loop may switch UP to a phase's tier
|
||||
classifier: boolean // ask the small model when no rule matched
|
||||
}
|
||||
```
|
||||
DEFAULT_CONFIG changes:
|
||||
- `tiers`: best `['fable','opus','sonnet']`, big `['opus','fable','sonnet']`,
|
||||
work `['sonnet','opus']`, cheap `['haiku','sonnet']`.
|
||||
- `fallback`: `['fable','opus','sonnet','haiku']`. `cooldownMinutes: 15`.
|
||||
`mainUpgrade: true`. `classifier: false`.
|
||||
- phases: plan `{ model: 'best', effort: 'xhigh' }`, reflect `{ best, high }`,
|
||||
orchestrate `{ best, medium }`, escalate `{ best, max }`, judge `{ big, xhigh }`,
|
||||
implement `{ work, medium }`, write `{ work, medium }`, verify `{ work, xhigh }`,
|
||||
explore `{ work, medium }`, mechanical `{ cheap, low }`. (Keep the `model`
|
||||
field name: a tier name is a model NAME the resolver understands; no new
|
||||
`tier` field. AC3 greps `tier: '…'`?? NO: AC3 is written against
|
||||
`model: 'best'`-style entries? → see AC3 note below.)
|
||||
- prompt: `[{ pattern: '\\bultrathink\\b', phase: 'escalate', mode: 'floor' },
|
||||
{ pattern: '\\b(plan|planifie|planning|brainstorm|architecture|con[cç]ois|design)\\b', phase: 'plan', mode: 'default' },
|
||||
{ pattern: '\\b(pourquoi|why|explique|explain|analyse|analyze|comprendre|understand|review|audit)\\b', phase: 'reflect', mode: 'default' }]`.
|
||||
`mode` absent → 'default'.
|
||||
AC3 note for the executor: the contract's CHECK counts `tier: '(best|big|work|cheap)'`
|
||||
in DEFAULT_CONFIG and refuses `model: '` entries there. So the PHASE type gets
|
||||
an explicit `tier?: string` field: `type Route = { tier?: string; model?: string;
|
||||
effort?: Level }`; a route resolves `tier` first, then `model`. Default phases
|
||||
use `tier:`. `/route model=<x>` keeps writing `model` (alias or id). The route
|
||||
tool keeps `phase`/`effort`/`clear` only.
|
||||
Validation: `tiers` values = non-empty arrays of `models` keys (bad entries
|
||||
dropped, logged); `fallback` = array of `models` keys, deduplicated, non-empty
|
||||
(else default); phase `tier` must be a `tiers` key; `cooldownMinutes` positive
|
||||
integer; `mode` ∈ floor|default.
|
||||
|
||||
## Resolution (ONE resolver, used by spawn, main plan and texts)
|
||||
```ts
|
||||
function availableIn(st, aliases: string[]): string | undefined
|
||||
// first alias whose full id is not down (st.down.get(id) > now → down)
|
||||
function resolveModel(st, name: string): string
|
||||
// tier name → availableIn(tiers[name]) ?? first alias → id
|
||||
// alias → id (explicit: never skipped when down); full id → itself
|
||||
function resolveRoute(st, route: Route): string | undefined
|
||||
// route.tier ? resolveModel(st, route.tier) : route.model ? resolveModel(st, route.model) : undefined
|
||||
function rank(st, id: string): number
|
||||
// index in cfg.fallback of the alias whose id prefixes `id` (strip "[1m]"); unknown → fallback.length
|
||||
```
|
||||
`now` comes from `$.clock.now()` (read once per hook call that needs it).
|
||||
|
||||
## Breaker (`st.down: Map<string, number>` full id → until ms; `st.lastMainModel: string`)
|
||||
- `turn.complete`: main (`e.agentId` undefined) with `reason` ∈ error|refusal →
|
||||
`markDown(st, st.lastMainModel)`; agent with that reason → `markDown(loop.model)`
|
||||
(Loop gains `model: string`, the model the engine reported at spawn,
|
||||
`started.model`). `aborted`/`answer` → nothing.
|
||||
- `classic.PostModelSwitch` with `e.source === 'auto'` → `markDown(e.from_model)`.
|
||||
- `markDown` logs ALWAYS (not only verbose): `model-router: <id> unavailable
|
||||
until <HH:MM>; routing falls back`. `/route reload` and `session.end` clear
|
||||
the map. `show()` gets a `down: <id> until <HH:MM>, …` or `down: none` line.
|
||||
|
||||
## Main-loop model decision (`mainModel` rewritten)
|
||||
```
|
||||
wanted = resolveRoute(st, (userMain ?? turnMain ?? turnFloor)?.route) // per-axis as today
|
||||
cur = e.model
|
||||
if cur is down and wanted is undefined → wanted = nextAvailable(st, cur) // fallback chain after cur's alias
|
||||
if wanted undefined or sameRank(wanted, cur) → cur
|
||||
if rank(wanted) < rank(cur) (better) → cfg.mainUpgrade ? wanted : cur
|
||||
if rank(wanted) > rank(cur) (cheaper) → cfg.mainModelSwitch && windowOk ? wanted : cur
|
||||
if cur is down and wanted defined → wanted (always: nothing to lose)
|
||||
```
|
||||
`st.lastMainModel = plan.model` at every main step. `sameRank` compares
|
||||
aliases (so `claude-fable-5-1` vs `claude-fable-5-1[1m]` never flips).
|
||||
The log/status show `→ fallback` when the breaker chose the model.
|
||||
|
||||
## Spawn (`spawnRoute` / `registerSpawn`)
|
||||
`wanted = resolveRoute(st, route)`; explicit `e.model` still wins. Store
|
||||
`loop.model = started.model`. (Explicit alias given by the caller while down:
|
||||
left alone, explicit means explicit; note in the tool description.)
|
||||
|
||||
## Derived phases (no user action)
|
||||
D1. Dispatch push/pop: in the Agent `tool.call` hook, when `e.agentId` is
|
||||
undefined (main) and `st.turnMain?.source !== 'model'` written AFTER the
|
||||
dispatch… simpler rule: on a main Agent call, if `st.resumeMain` is
|
||||
unset, `st.resumeMain = st.turnMain ?? NONE` and `st.turnMain = { phase:
|
||||
'orchestrate', route: phases.orchestrate, source: 'derived' }`. When an
|
||||
agent's `turn.complete` leaves `st.loops` empty AND `st.turnMain?.source
|
||||
=== 'derived'` → `st.turnMain = st.resumeMain` (NONE → null), clear
|
||||
`resumeMain`. A `route` call or skill load in between replaces turnMain
|
||||
(source model/skill) so the pop is skipped and `resumeMain` cleared at
|
||||
the next main `turn.complete` (endMainTurn clears both). Source type gains
|
||||
`'derived'`.
|
||||
D2. Prompt default rules (`mode: 'default'`): write `turnMain = { phase,
|
||||
route, source: 'prompt' }` (NOT the floor) — overridable by routes and
|
||||
skills; floor rules unchanged. Mid-turn prompt with a default rule → only
|
||||
`pendingPrompt`-like handling for FLOOR rules stays; a default rule typed
|
||||
mid-turn is ignored (the running turn has its own routes).
|
||||
D3. Classifier: when `cfg.classifier` and no rule matched and the prompt is
|
||||
composer-origin and idle (no `turnId`): `label = await
|
||||
$.model.classify(e.text.slice(0, MAX_PROMPT_SCAN), [...phaseNames,
|
||||
'other'])` in try/catch; a phase label → default route (source 'prompt');
|
||||
anything else → nothing. Document the cost in the config comment.
|
||||
|
||||
## Texts
|
||||
`routedText`/`mainNote`/`show`: print the RESOLVED id and `(fallback)` when
|
||||
the breaker skipped a better alias; `(tier best → claude-fable-5-1)`.
|
||||
Spinner/status unchanged shape.
|
||||
|
||||
## Tests (register.test.ts; names must contain the contract's words)
|
||||
Reuse the boot helper; full typed inputs; bottom hooks (`agent.spawn`,
|
||||
`turn.complete`, `classic.PostModelSwitch` — read its input type for the
|
||||
required fields; `prompt.submit`). Engine effort `high` in steps.
|
||||
- `tier: a plan route upgrades a haiku session to fable at xhigh` — route tool
|
||||
`plan`, step with `model: 'claude-haiku-4-5-20251001'` → bottom sees
|
||||
`claude-fable-5-1` and `xhigh`.
|
||||
- `downgrade: mechanical on fable keeps the model while the switch is off`.
|
||||
- `fallback: an error turn on fable moves the next main step to opus` — step on
|
||||
fable (sets lastMainModel), `$.turn.complete({ reason: 'error', agentId
|
||||
undefined, … })`, step on fable → bottom sees `claude-opus-5-5`, effort
|
||||
unchanged; then `/route reload` → step on fable stays fable.
|
||||
- `breaker: PostModelSwitch auto marks the old model down` — `$.classic.PostModelSwitch({ from_model: 'claude-fable-5-1', to_model: 'claude-opus-5-5', source: 'auto', … })` → `/route show` lists `claude-fable-5-1` under `down:`.
|
||||
- `spawn: Explore goes to opus while sonnet is down` — mark sonnet down
|
||||
through an agent error turn (spawn Explore via bottom hook returning a1,
|
||||
`$.turn.complete({ agentId: 'a1', reason: 'error' })`), spawn again → bottom
|
||||
`e.model === 'claude-opus-5-5'`.
|
||||
- `derived: a dispatch pushes orchestrate and pops the previous plan route`
|
||||
— route tool `plan`, `$.tool.call({ tool: 'Agent', … })` with a bottom hook,
|
||||
`/route show` main line shows `derived orchestrate`; `$.turn.complete({
|
||||
agentId: 'a1', reason: 'answer' })` (loop registered via spawn) → main line
|
||||
shows `model plan` again.
|
||||
- `default rule: "planifie la migration" routes the turn to plan, a route call overrides`.
|
||||
- Keep every B1 `floor` test and all earlier tests green (≥ 40 tests total).
|
||||
|
||||
## Constraints
|
||||
- ≤ 25 logic lines per function (extract helpers: `availableIn`, `rank`,
|
||||
`markDown`, `nextAvailable`, `decideMain`, `pushOrchestrate`, `popOrchestrate`,
|
||||
`applyDefaultRule`, `classifyPrompt`), 80 chars/line, no `any`, state in
|
||||
the closure, fail-open `.catch` with `warnOnce` on every new hook
|
||||
(`classic.PostModelSwitch`), the route tool schema unchanged.
|
||||
- Do not touch: the hardening (caps, `safely`, attestation), the Skill
|
||||
bridge, the floor slot semantics.
|
||||
- Verify: validate, the contract's tsc CHECK, `claude plugin test .`, AC3/AC4
|
||||
greps, `gates.sh run` on the contract.
|
||||
|
||||
## Disposition
|
||||
- honors BDR-115 and its amendment (one resolver, calling-loop writes,
|
||||
truthful texts, per-machine config); supersedes "a phase without model
|
||||
keeps the loop's model" (every default phase now names a tier).
|
||||
- honors BDR-076 (dispatched judgment on opus first: `big` = opus, fable,
|
||||
sonnet) and BDR-066 (execution on sonnet: `work`).
|
||||
- LRN-203: hooks still write full ids (the resolver's output).
|
||||
- LRN-204: downgrade on main stays gated; upgrade accepted (quality over one
|
||||
cold-cache step).
|
||||
- Deferred: repo agents' frontmatter pins cannot fall back (the mod does not
|
||||
see them in wave 1) → wave 2 moves them into the table with tiers.
|
||||
Reference in New Issue
Block a user