9.7 KiB
PLAN r3 — model-router wave 3-C: /route forget (2026-10-11)
r1 → r2 after simplicity CONCERNS(3), correctness CONCERNS(4), robustness
CONCERNS(4); r2 → r3 after the confirmation (correctness CONCERNS(3)).
Precedence r3 > r2 > r1. Contract
.claude/tasks/contracts/2026-10-11-model-router-w3c-forget-1457.md.
Base W3-B (604a6c4 + 32b71d2 + 938c3d1). Corrected facts: Memory holds
ok/ask/askLocal/base/confirmed/about/local/key only (no changed, no
projects); the composer gate lives in registerCommandHook, not in
handleCommand; writeNow always writes once the patch ran and toasts
UPDATED, returns false on refusal, throws when the rebuild fails after a
landed write; show prints no command list (the only visible list is the
argumentHint); routeMainBySkill copies a route into turnMain/runMain
and a rebuild never touches those slots; the shipped file holds one changed
row (security-auditor verify → judge) whose frontmatter floor was aligned
to opus: restoring it without re-aligning the floor makes the census
FAIL (no changed entry = no WARN exemption).
Behaviour (composer /route forget <arg>, exactly ONE argument)
- Parsing: no arg or more than one → "usage: /route forget <name|all|projects>".
Reserved words
allandprojects; a row or phase named like them cannot be forgotten by name (answer says "edit routing.json by hand"). - Guard: while a dialog is open (
st.asking !== null) → "answer the open dialog first", nothing written. The forget confirmation TAKES the slot itself (st.asking = 'forget:<target>'in a try/finally aroundaskOr) so no first-use dialog opens under it and two forgets cannot stack. - The decision runs on the FRESH file, twice: a PRE-read (after awaiting
st.writes, so a landed Keep is visible) decides the no-write paths and the confirmation counts; the PATCH itself re-runsforgetPlan(file, target)onwriteNow's own freshly readfile(the closure exports the counts actually applied; the answer reports those). Pre-read missing or unparsable → the writer's refusal path ("routing.json is missing or unreadable: nothing saved"); nothing to remove, no refused restore and the key(s) not inst.asked→ "nothing to forget for " / "nothing to forget", NO write; only a refused restore → its reason, NO write; key(s) only inst.asked→ reset them, NO write, "asked again at the next use, nothing was saved" (when the key is decided by the machine override or a project row, say "still decided by …" instead). <name>: for EVERY kind in KINDS (skills, agents, phases): deleteconfirmed.<kind>.<name>; restorechanged.<kind>.<name>→ row =fromONLY whenfromis a string, the row exists, the row equalschanged.to, andfromis a phase offile.phasesorDEFAULT_CONFIG.phases(else the entry is kept and the answer says why); deleteprojects[k].<kind>.<name>for every k, pruning an emptied kind table then an emptiedprojects[k]; thenst.asked.delete(key).all: the same over every entry ofconfirmed,changed,projects;projects: onlyprojectsemptied. Both ASK FIRST throughaskOr: "Forget decision(s): row(s) restored, project exception(s) in repo(s)?" options ["Cancel", "Forget"] (Cancel first; any other answer, a dismissed dialog or headless = cancelled, nothing written); skipped when every count is zero ("nothing to forget").- Write: ONE
writeRoutingpatch (the closure carries the counts out); outcomes: false → "nothing saved" (the writer's toast already explains); true → asked keys deleted, config rebuilt; a rebuild failure after a landed write is signalled by a dedicated error class (RebuildFailed, thrown bywriteNowthere and only there) → "saved, config not rebuilt: /route reload" (asked keys untouched); any other throw (patch, fs.write) → "forget not applied ()", asked keys untouched. - Answer, one formatter (used by every form, per restored row): "forgot
: confirmed, row(s) restored, project exception(s)
removed[, restore(s) kept: ]" + per restored row " (.
→ : at ; if was aligned to , set
model: , effort: and its lock in
lib/tests/model-routing.test.sh, then
make test)" where<file>=agents/<name>.mdorskills/<name>/SKILL.md(no file clause for a built-in agent such as Explore/Plan), alias = head of the phase's tier in the SHIPPEDDEFAULT_CONFIG.tiers(or the phase's model), effort from the phase;- "; ~/.claude/model-router.json still sets and wins here" when
mem.localhas it, checked only after a successful rebuild; - "; current run may keep : /route clear to apply" when a
restored SKILL row's
tophase equals the phase ofrunMainorturnMainwith sourcerun/skill; - "; other live sessions see it after /route reload".
- "; ~/.claude/model-router.json still sets and wins here" when
allprunes emptied kind tables inconfirmed,changed,projects(a kept unrestorablechangedentry stays and is reported).decisionscount = confirmed entries + changed entries + project rows (an entry counted once per table it sits in).- The route tool has no
forget; sub-agents cannot run commands; the origin gate is the existing one inregisterCommandHook.
Docs and comments (same commit)
argumentHintgainsforget <name|all|projects>(locked by a grep in the contract CHECK, not by a kit test); register.ts comment "Only a dialog answer or/route askwrites" → "… or/route ask//route forgetwrites".- README.md: the
/routesubcommand list (~138) gainsforget <name|all|projects>and the writers sentence (~140) names it; USAGE.md: the sentence "/route pendingliste …,/route ask off|on…" (~190) gains/route forget <nom|all|projects>(efface une décision, la ligne revient à la phase livrée); CHANGELOG.md: the argument form (~10) and the writers sentence (~11);lib/effort-shift.mdcommands line.
Steps
- S1
forgetPlan(file, target)(pure over the fresh file data: removals, restores, counts, skipped restores with reasons) andforgetCommand($, st, args)(arity, reserved words, asking guard, read, no-write paths, confirmation for all/projects, write viawriteRoutingwith a patch that applies the plan, outcome branches, asked reset, answer);handleCommandcaseforget; argumentHint. - S2 docs/comments above.
- S3 kit tests, folded: forget a confirmed skill → entry gone, asked
again in the same session,
/route pendinglists it; forget a changed agent → row restored, entry gone, the next spawn on the restored model, answer carries the realign clause with the file and values; forget a phase → confirmed.phases gone, T3 asks again; forget a name with a project exception in two repos → both rows removed, emptied keys pruned; undecided row → "nothing to forget", no write; asked-only key → reset, no write; unknown name → "nothing to forget for", no write; arity (two words → usage); reserved word collision answer; open dialog → refused;allcancelled → nothing written;allconfirmed → everything empty, counts in the answer;projects→ only projects emptied; restore refused (row ≠ to / from unknown) → entry kept, reason; override-pinned row → the "wins here" clause; restored skill with a run slot → the "/route clear" clause; missing file → "nothing saved"; rebuild throw → "saved, config not rebuilt"; route toolclearleaves decisions; the existing origin test extended withforget x;failWrite→ "forget not applied"; (argumentHint: grep lock in the contract, no kit test). - S4 live smoke after the feater (orchestrator, user): one
/route forget projects(or a name) typed in the terminal with the dialog answered → recorded[gated]in the contract ($.ui.askfrom animmediatecommand is unverified live). - Disposition: honors BDR-116 (user-only writers, same path/caps; the shared tracked memory stays the user's choice), LRN-210 (one clause per test, folded where one setup proves two clauses), LRN-212.
Challenge ledger (r1 → r2)
- simp 1, corr 1, rob 2 (false
st.mempremise) → plan over the fresh file, closure counts, pre-read no-write paths. - simp 2, corr 2, rob 7 (kind order, reserved words, no-op writes) → all kinds, removed-count rule, arity, reserved-word answer.
- simp 3, corr 4/9/10, rob (docs) → docs/comment scope, AC4 = argumentHint.
- simp 4 (projects form) → kept, one-liner, with the confirmation.
- simp 5/6, corr 3/7, rob 3/9 (restore + floors + validation) → guarded restore, realign clause with file + values, one formatter.
- corr 5, rob 8 (override-pinned) → "wins here" clause.
- corr 6, rob 6 (dialog in flight) → refuse while asking.
- corr 8, rob 4 (run slot) → "/route clear" clause.
- rob 1 (no confirmation) → askOr confirmation for all/projects, Cancel first.
- rob 5 (three write outcomes) → branches.
- simp 7 (test folds) → S3 folded.
Confirmation ledger (r2 → r3)
- conf 1 (throw mapping) →
RebuildFailedclass; other throws "not applied". - conf 2 (slot not taken) → forget takes
st.askingaround its askOr. - conf 3 (stale plan at write) → patch recomputes on its own file; pre-read
awaits
st.writes; applied counts reported. - conf 4 (run-slot rule) → kind skills, source run/skill, phase ==
to. - conf 5 (realign clause) → conditional, shipped tiers, no file for built-ins.
- conf 6 (argumentHint test) → grep lock in the contract CHECK.
- conf 7 (live dialog from a command) → S4 smoke.
- conf 8 (ambiguities a-f) → sentences added.
- conf 9 (docs anchors) → README list + USAGE anchor + CHANGELOG form.