chore(memory): BDR-116 amendment (transition logs) + journal/TODO/contract w4a — feat model-router wave 4-A

This commit is contained in:
bchanot
2026-10-11 18:58:04 +02:00
parent 86845fcac8
commit 4506e0711f
5 changed files with 227 additions and 3 deletions
+1
View File
@@ -1413,4 +1413,5 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
- **Refs**: contract `.claude/tasks/contracts/2026-10-10-model-router-w3a-confirm-1201.md`, plan `.claude/tasks/plans/2026-10-10-model-router-w3a-confirm-1201.md`, commit 22455c0, [[BDR-115]], [[LRN-210]], [[LRN-211]], [[EVAL-043]].
- **Amendment (2026-10-11, wave 3-B, commit 604a6c4)**: clause (2) superseded: the dialog carries CONTEXT (skill description = first sentence of its SKILL.md frontmatter, five YAML forms, hardened read: name allowlist, stat kind file, size cap, `clean()` Cc/Cf + code points; agent description from `agent.offer`, bounded cache; phase `about` line, new 120-char field on the 11 routing.json phases, kept in memory not on Route) and the REAL next model id + effort. Options Later / Keep / Change (T3 Later / Keep). Change = model (4 fixed alias labels with tier role) → effort among those the phases headed by that alias offer (ascending, ≤ 4, one → skipped, zero → Later) → scope; the pair maps to an existing PHASE (rows stay phase names; row's current phase wins a tie; same phase = Keep); a pair no phase offers = a new phase by hand. Other anywhere = Later + toast (no echo). A main-row change toasts the real decision + `/route switch on` hint on a held downgrade. Three lenses rejected inline-route rows and dialog phase edits (2 BLOCKERs). First real use: user moved security-auditor to `judge` (opus/xhigh) through it; floor aligned (`model: opus`, 2026-10-11). Links [[EVAL-044]].
- **Amendment (2026-10-11, wave 3-C, commit cd8d72f)**: clause (6) extended: `/route forget <name|all|projects>` (composer only) clears decisions in the tracked file through the same writer: a name in every table (`confirmed`, `changed` → row restored to its recorded `from` when it is a string, the row still equals `to` and `from` is a known phase; `projects[*]` pruned), asked again at once; `all`/`projects` confirm through the engine dialog (Cancel first, anything else = cancelled) and hold the single-dialog slot; nothing written when nothing to forget; the answer names a restored row's file + values to realign (a floor aligned to the decision makes the census FAIL after the restore). No per-user decisions file (user: "seulement le forget"): another user inherits the shared memory and may forget. Residuals (security LOW/MEDIUM): raw error text in the answer, file fragments unsanitized in the answer, `floorFile` name not allowlisted, `__proto__` plan/apply mismatch, `changed.phases` restorable. Links [[EVAL-044]].
- **Amendment (2026-10-11, wave 4-A, commit 68d7ea6)**: logging rule: `verbose` (boolean) logs TRANSITIONS only, built from the decision itself inside `safely` before `next`: main "fable/high → fable/xhigh (<phase>: <cause>[, <model cause>])" once per turn per transition (causes: skill /<name>, route tool, prompt rule, prompt floor, dispatch, sticky /route, session; model: upgrade, switch, fallback: <alias> down); one hand-off line per agent dispatch deduped per turn per (type, target) "main … → agent <type> <alias>/<effort> (row <phase>|explicit|fallback|frontmatter, row|explicit|engine)"; later agent effort moves; an answered-by anomaly (agents only, canonical ids, once per agent+model). The host prefixes the plugin name: new lines carry none. The four stream lines are gone. Residuals W3-A/B/C closed: failure texts to the log, file fragments cleaned at render, floorFile allowlisted, `__proto__` refused, changed scan = rows, dialog Change offers shipped phases only (`mem.filePhases` = validated seed layer), post-answer write failure keeps the key asked. Rejected: a `trace` level (unrequested surface); detecting agent moves at turn.step (decided at spawn). Links [[EVAL-044]].
+1
View File
@@ -599,3 +599,4 @@ rules:
- model-router W3-A (first-use confirmation) landed 22455c0 on feature/model-router-confirm. User asked for it after the W2 merge; 3 pass-B answers. Plan r1→r4: correctness BLOCKER (Everywhere decision = census red → `changed` WARN exemption), robustness (first challenger killed by DNS, fresh one: project-tree layer dropped for security, single dialog in flight, keep-previous after first load), confirmation CONCERNS(6). Feater DONE (169 tests) + 4 rounds. Verifier ECARTS(3)/(2)/CONFORME; security BLOCK(1) REAL: password with `@`/`/` landed in the tracked key → strict parsing, no `local:` keys, output cap → re-verify ECARTS(1) fixtures → PASS. The working-tree mod was hot-loaded by the engine: my own dispatches opened T2 dialogs the user answered (verifier Keep, feater Keep, security-auditor → implement → user reset to verify); first misread as a test leak (LRN-212). GATE 0 oracle as heredoc not runnable → script. Full make test green (env red only). Docs P1-P13 user-approved (patch in flight); BDR-116, LRN-212, EVAL-043 written. Next: doc commit, memory commit, user merge + publish by hand; T1/T3 live checks open.
- model-router W3-B (dialog with context) landed 604a6c4 + data 32b71d2 + floor chore. User asked after W3-A: explain what is decided, OK/change/later, model then effort then scope. Plan r1→r3: all three lenses killed my inline-row format and the T3 phase edit (2 BLOCKERs avoided); efforts derived from the phases of the chosen model. Feater DONE first pass (229 tests), verifier ECARTS(2)→(1)→CONFORME, security PASS. GATE 0 heredoc CHECK not runnable again → script. Live during the gates: user answered the NEW dialog for security-auditor → opus/xhigh (the request's example; kept, floor aligned to opus, model-routing lock updated), plan-challenger → verify (reset on user go), orchestrate Keep (T3 check done). My `changed = {}` reset wiped a legit entry → census red for one commit → restored. Docs P1-P6 approved (patch in flight), BDR-116 amendment + EVAL-044 written. Next: doc commit, memory commit, user merge of feature/model-router-confirm (W3-A + W3-B) + publish by hand; T1 live check still open.
- model-router W3-C `/route forget` landed cd8d72f (+ data commit: escalate Keep). User asked whether decisions can be cleared and what another user can do; chose the command only, no per-user file. Plan r1→r3: three lenses CONCERNS (false `st.mem` premise, kind order, docs scope, census red after restoring an aligned floor, no confirmation on `all`), confirmation CONCERNS(3). Feater DONE; verifier ECARTS(2) with a REAL typo (`ForgetPlan`), then coverage only ×2 → cap → user accepted; security PASS (1 MEDIUM + 4 LOW parked). My own `route(escalate)` call opened the T3 dialog the user Kept. Branch feature/model-router-confirm now carries W3-A/B/C (14 commits), unmerged, unpushed; merge on signal; full make test running.
- model-router W4-A landed 68d7ea6 on feature/model-router-w4 (+ data commit). User: fewer logs (one line per real change with a cause), residuals, then the classifier. Plan r1→r3: three lenses (BLOCKER: an agent's model move is decided at spawn; `trace` level rejected as unrequested surface; reuse lastPlan/Loop; cause from the decision), confirmation CONCERNS(5) (hand-off line per dispatch deduped per type+target). Feater DONE (341 tests) + 4 rounds; verifier ECARTS(3) real (dedupe on text, double host prefix) then coverage ×2 → cap → user accepted; security PASS + 2 MEDIUM (raw skill names) fixed. Live: the terminal logs changed as soon as the feater saved; user Kept plan-challenger (judge) and the judge phase. Next: W4-B classifier; push-guard hotfix queued.
+7 -3
View File
@@ -27,10 +27,14 @@ migration of shifters/pins/model-gate in wave 2 after proof; names model-router
Plan `.claude/tasks/plans/2026-10-10-model-router-w3a-confirm-1201.md` r4, contract `2026-10-10-model-router-w3a-confirm-1201`. User decisions 2026-10-10: tracked routing.json reached through the plugin dir (no new link); blocking `$.ui.ask` dialog at first use; per skill row, agent row, main phase.
- [x] W3-A landed (22455c0, 2026-10-11): routing.json = phases + rows + decisions; T1/T2/T3 dialogs; project exceptions `projects[<normalized remote>]` in the tracked file (project-tree layer dropped: security); writers = dialog + `/route ask`; `/route pending`; census from the file with the `changed` WARN exemption; kit 86 → 190. Gates: 3 lenses + 1 confirmation (BLOCKER census), feater + 4 rounds, verifier CONFORME + re-verify, security BLOCK(1) credential leak fixed → PASS, full make test green (design-tool-gate env red). Live: T2 dialogs answered by the user (verifier Keep, feater Keep, security-auditor → implement then reset to verify on user go).
- [x] W3-B dialog with context (2026-10-11, 604a6c4, contract `2026-10-11-model-router-w3b-dialog-1240`, plan r3): descriptions (skill frontmatter / agent.offer / phase `about`), real id + effort in the question, Later / Keep / Change, model → effort (derived from the phases of that model) → scope, rows stay phases, T3 Later/Keep. 3 lenses (2 BLOCKERs on my r1 avoided) + 1 confirmation, feater + 2 rounds, verifier CONFORME (3rd), security PASS (4 LOW: symlinked SKILL.md read, TOCTOU, raw key in a log, any plugin hooking AskUserQuestion could answer). Live decisions: security-auditor → judge (kept, floor aligned `model: opus`), plan-challenger → verify (reset on user go), orchestrate Keep (T3 live check done).
- [ ] W3-B residuals: a Change answered Everywhere can store a machine-only phase name (defined in `~/.claude/model-router.json`) into the tracked file (security note, correctness); SKILL.md symlink follow (LOW); `descs` cap and "(custom)" label untested; the security-auditor's `changed.from = verify` entry stays in routing.json although the floor now equals the row (harmless; prune at release).
- [ ] trace the "auto mode: use Bash/sed instead of Edit/Write" instruction block that sub-agents see attributed to the model-router MCP instructions (security-auditor 2026-10-11): it is NOT in mods/model-router source; likely the harness's own auto-mode text rendered next to the mod's MCP block. Confirm the origin.
- [ ] W3-B residuals: (machine-only phase into the tracked file: CLOSED in W4-A); SKILL.md symlink follow (LOW); `descs` cap and "(custom)" label untested; the security-auditor's `changed.from = verify` entry stays in routing.json although the floor now equals the row (harmless; prune at release).
- [x] "auto mode: use Bash/sed" block attributed to the mod: traced 2026-10-11 by reading, it is the harness's own auto-mode instruction rendered after the mod's MCP block; not in the mod source. Closed.
- [x] W3-C `/route forget` (2026-10-11, cd8d72f, contract `2026-10-11-model-router-w3c-forget-1457`, plan r3): name/all/projects, guarded restore, confirmation dialog for all/projects, no-write paths, docs. 3 lenses + 1 confirmation, feater + 3 rounds (1 real defect: `ForgetPlan` typo), verifier at the cap on coverage (user accepted), security PASS. Kit 232 → 284. Live: orchestrate + escalate T3 Keeps seen.
- [ ] W3-C residuals (security, accepted): `forget not applied (${String(err)})` echoes the raw writer error (send to the log); file-sourced `from`/`to`/keys unsanitized in the answer (clean() them); `floorFile` builds a display path from an unvalidated key (SKILL_NAME check); `__proto__` accepted by `restoreOf` while `setKey` refuses it (plan/apply mismatch); `changed.phases` entries restorable (limit the scan to ROWS). Live smoke of `/route forget` from the terminal still unseen (criterion 7).
- [x] W3-C residuals closed in W4-A (68d7ea6). Live smoke of `/route forget` from the terminal still unseen.
- [x] W4-A transition logs + residuals (2026-10-11, 68d7ea6, contract `2026-10-11-model-router-w4a-logs-residuals-1637`, plan r3): verbose = transitions only (main line, hand-off per dispatch, agent moves, answered-by anomaly), stream lines removed, host prefix respected; residuals of W3-A/B/C closed (a)-(g). 3 lenses (BLOCKER: agent moves decided at spawn) + 1 confirmation, feater + 4 rounds, verifier at the cap on coverage (user accepted), security PASS + 2 MEDIUM fixed. Kit 284 → 358.
- [ ] W4-A residuals (LOW, accepted): `readLayer` logs `String(err)` of a JSON parse error (a file fragment can appear in the local log); `logCall`/`noteMain` ui calls on the step path sit outside `safely` (fail-open on routing, no security impact).
- [ ] push-guard hotfix: the PreToolUse hook denies a read-only command whose TEXT contains the push verb next to `git` (grep pattern, heredoc); match the command's own argv only.
- [ ] W4-B classifier (next): haiku `$.model.classify` on typed prompts → turn default route (plan/reflect/implement/apply/chat), first-use confirmation per phase, `/route classify on|off`, transition cause `classifier`.
- [ ] W3-A live checks still open: T1 (typed `/feat` etc.) dialog (T3 seen live 2026-10-11: orchestrate Keep); a parallel same-agent dispatch answered after > 10 s; what an unanswered dialog resolves to on the terminal (must be Later or nothing written). Record in the contract as `[gated]` when seen.
- [ ] W3-A residuals (security LOW, accepted): non-atomic cross-process write (two sessions, crash mid-write → file reads as failed, previous config kept); `host/path` of a private remote in the tracked file; a persistent write failure re-asks every use (`asked.delete` in the catch); `out.length` vs byte size at the cap; `changePatch` scope 'project' with a vanished key writes Everywhere (cwd change mid-dialog). Deferred: `/route set`, `/route confirm`, `(unconfirmed)` in show, dialog edits of phases, frontmatter auto-alignment, session-start warning when routing.json is dirty, per-machine `projects`.
- [ ] routing.json ships `confirmed` for verifier/feater (user Keeps) and phases verify/implement: every clone inherits them (by design: decisions travel). Revisit at release if unwanted.
@@ -0,0 +1,35 @@
# CONTRACT — model-router-w4a-logs-residuals
- date: 2026-10-11 | flow: feat | branch: feature/model-router-w4
- status: active
## REQUEST (verbatim — IMMUTABLE)
okm j'aimerais que tu corrige donc les residus techniques, et aussi que tu fasse cette wave optionnelle du classifieur. Et aussi, pour le moment on a beaucoup de log avec le mod de la route, par exemple [pasted: "model-router: step 3 answered by claude-sonnet-5-5", "model-router: step 18 agent af0b3fa5: claude-opus-5-5/xhigh → claude-opus-5-5/xhigh", "model-router: step 0 main: claude-fable-5-1/high → claude-fable-5-1/high" …] , est-ce auil y a moyen de reduire ces log la a juste on dit quand on chane de model / effort et 2 ou 3 mots de contextes. eviter les repetitions de model-router step. juste direm ah la on switch de ca a ca car blablabla.
## CLARIFICATIONS
(pass A: none. Split: W4-A = residuals + transition logs (this contract); W4-B = the prompt classifier (own contract))
Q: public shape (after the simplicity lens, orchestrator default, user may veto): `verbose` stays a boolean; `true` logs TRANSITIONS only ("model-router: <main|agent <type>>: <from> → <to> (<phase>: <cause>[, <model cause>])"); the four verbose stream lines (step, answered by, spawn, route-tool JSON, skill path) are removed, no `trace` level; `/route verbose on|off` unchanged [stated 2026-10-11]
Q: the "auto mode: use Bash/sed instead of Edit/Write" block attributed to the mod / A: traced by reading: it is the harness's own auto-mode instruction rendered right after the mod's MCP-instructions block; not in mods/model-router source; closed, no change [resolved 2026-10-11]
Q: the hand-off line fires on every rowed or explicit spawn, even when the pair equals main's (a dispatch is a hand-off) / A: accepted [stated 2026-10-11]
Q: three `clean()` sites are unreachable by construction (phases pass PHASE_KEY at load; `kept.to` equals a live slot's phase): kept as defence in depth without a test / A: accepted [gated 2026-10-11]
Q: the host prefixes every `$.ui.log` line with the plugin name (seen in the user's terminal): the new lines carry no literal "model-router: " prefix; the pre-existing warnings are left as they are [stated 2026-10-11]
Q: GATE 1 cap (verifier ECARTS(3): dedupe key + double prefix real, then coverage ×2; 358 kit tests) / A: user "Accepter et commiter"; security PASS then two MEDIUM (raw skill names in two lines) fixed with tests, no re-scan (two clean() calls); LOW parked: JSON parse error text in the load log, `logCall`/`noteMain` ui calls outside `safely` [gated 2026-10-11]
## ACCEPTANCE CRITERIA
1. `verbose: true` logs only transitions: main → one line per turn per (from → to) when the planned (model, effort) pair differs from main's previous plan (after /clear: from the engine input), built from the decision itself (effort cause = the slot that supplied it: `skill /<name>`, `route tool`, `sticky /route`, `prompt floor`, `dispatch`, `session`; model cause = the decision's why when the model moved: `upgrade`, `fallback: <alias> down`, `switch`), emitted inside `safely` BEFORE `next` (a log failure never unroutes a step); agent → one hand-off line per dispatch, deduped per turn per (type, to): "main <from> → agent <type> <to> (<model cause>, <effort cause>)" with causes `row <phase>` | `explicit` | `fallback: <alias> down` | `frontmatter` and `row` | `explicit` | `engine`, then one line per later effort move on that loop (`route tool`, `skill <name>`); a prompt-rule route reads "(<phase>: prompt rule)"; names cleaned (≤ 40 code points); no "step N" lines, no "answered by" lines, no spawn/route-JSON/skill-path lines at any level. `false` logs nothing but the existing warnings (`warnOnce`, `logCall`, `tierDownLog`, engine switch, config drops). One kit test per clause.
2. An agent answering on a model other than the planned one (canonical ids compared, `usage` null skipped) is logged once per (agent, model) at `true` ("model-router: agent verifier answered by opus instead of sonnet"); never for a `[1m]` suffix alone; main is not covered (the engine switch line exists). One kit test per clause.
3. Config: `verbose` stays boolean; an invalid override value (`"ON"`, 1, null) falls back to the default `false` with one log per load; `/route verbose on|off`, `/route show` and the session toggle unchanged. One kit test per clause.
4. Residuals closed (W3-C security + W3-B + W3-A): (a) `forget not applied` sends `String(err)` to `$.ui.log` and answers a generic text; (b) every file-sourced fragment in a forget answer (`from`, `to`, keys) passes `clean(…, 40)`; (c) `floorFile` returns undefined unless `SKILL_NAME` matches the name; (d) `restoreOf` refuses `__proto__` (plan and apply agree); (e) the `changed` scan is limited to ROWS (phases never restored); (f) a dialog Change offers and targets only SHIPPED phases (code defaults + the validated phases of routing.json, the machine override excluded), so a machine-only phase is never offered nor written into the tracked file; the empty case toasts "no shipped phase uses <alias>: add one to routing.json" → Later; (g) a failure AFTER the user answered keeps the key in `asked` (no re-ask every use; the next session asks again), a failure before the answer still leaves it unasked (that half judged by reading: the kit skips a throwing hook, no seam exists between `asked.add` and the answer [gated 2026-10-11]). One kit test per clause, each failing if its guard were removed.
5. Kit suite, mods suite, validate and census green.
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' && bash lib/tests/effort-routing.test.sh >/dev/null 2>&1 && echo W4A-GREEN
EXPECT: W4A-GREEN
EVIDENCE: MET exit=0 marker-found :: W4A-GREEN
6. Docs: README (its three `verbose` sites say "transitions only"), USAGE (`/route` row), CHANGELOG Unreleased (Changed: transition logs; Fixed: the residuals), `lib/effort-shift.md` unchanged or ≤ 1 line.
CHECK: grep -qi 'transition' README.md && grep -qi 'transition' USAGE.md && grep -qi 'transition' CHANGELOG.md && echo W4A-DOCS
EXPECT: W4A-DOCS
EVIDENCE: MET exit=0 marker-found :: W4A-DOCS
## FILE SCOPE
mods/model-router/hooks/register.ts, mods/model-router/hooks/register.test.ts, README.md, USAGE.md, CHANGELOG.md (one clause each), lib/effort-shift.md (≤ 1 line)
@@ -0,0 +1,183 @@
# PLAN r3 — model-router wave 4-A: transition logs + residuals (2026-10-11)
r1 → r2 after simplicity CONCERNS(4), robustness CONCERNS(5), correctness
FATAL(5) (BLOCKER: an agent's model move happens at spawn, invisible at
turn.step); r2 → r3 after the confirmation (correctness CONCERNS(5)).
Precedence r3 > r2 > r1. Contract
`.claude/tasks/contracts/2026-10-11-model-router-w4a-logs-residuals-*.md`.
Base: develop f1cea54. Corrected facts: `Plan` = {model, effort} and
`mainPlan` discards the `Call` (its `why`); `agentPlan` returns `e.model`
verbatim (the spawn hook rewrote it already); `TurnStepInput` carries only
`agentId`; `Loop` = {effort, explicitEffort}; `Routed` = {phase, route,
source} (no skill name, no rule word); `mainRoute` excludes `turnFloor`;
`mainEffort().by` ∈ sticky|turn|floor|engine; no `effortSource`; `st.lastPlan`
holds main's previous plan (nulled on /clear); `st.logged` / `turnLogged`
are the dedupe sets; `AgentSpawnInput.parentModel` exists; `askOr` swallows
a rejected ask into LATER; `forget not applied (${String(err)})` lives in
`forgetWrite` (and its twin in `askCommand`); `buildMemory` has no
models/tiers to validate phases; the user's override sets `verbose: true`.
## D1 Logs: `verbose` stays boolean; true = transitions only
- The four firehose `vlog` sites are DELETED (step line, "answered by",
spawn line, route-tool JSON, skill-path line). No `trace` level. `vlog`
stays the single gate; `/route verbose on|off`, argumentHint and show
texts unchanged.
- Main transition: `planStep` returns `{ plan, why }` where `why` =
per-axis cause words computed at decision time (no `$` call, no
re-decision): effort cause from `mainEffort().by` + its slot (`sticky
/route`, `route tool` (turn, source model), `skill <label>` (run/turn,
source skill|run), `prompt floor` (floor), `dispatch` (derived
orchestrate), `session`); model cause = the `Call.why` when it moved
(`upgrade`, `fallback: <alias> down`, `switch`) else none. `Routed`
gains an optional `label`: the skill name at `routeMainBySkill` /
`confirmSkill` (`/feat`), the rule's phase at `floorFromPrompt`.
Rendered: "model-router: main: <alias>/<effort> → <alias>/<effort>
(<phase>: <cause>[, <model cause>])", e.g. "(plan: skill /ship-feature)",
"(orchestrate: dispatch)", "(escalate: prompt floor)", "(reflect:
prompt rule)" (turn/source prompt; `defaultFromPrompt` sets the label),
"(custom: sticky /route)", "(reflect: skill /feat, upgrade)"; no route
in force → "(session[, <model cause>])". Model cause from `Call.why`:
upgrade → `upgrade`, fallback → `fallback: <alias> down`, downgrade →
`switch`. Comparison: `prev = st.lastPlan ?? {e.model, e.effort}`,
CAPTURED BEFORE `noteMain` overwrites `lastPlan` (after /clear or
`/route on|off`, which null `lastPlan`, the first step compares to the
engine input: stated omission); emitted INSIDE `safely(st, $,
'turn.step log', …)` BEFORE `next` (a log failure never unroutes a
step; own warn key); deduped per turn per (loop, from→to) through
`turnLogged` (derived orchestrate waves log once per turn).
- Agent HAND-OFF line (r3, a dispatch is a model change): emitted in
`afterSpawn` for every rowed or explicit spawn, deduped per turn per
(type, to) through `turnLogged` (three parallel challengers = one
line): "model-router: main <alias>/<effort> → agent <type>
<alias>/<effort> (<model cause>, <effort cause>)" where from = main's
`st.lastPlan` (or the engine's session model when none), to = the
EFFECTIVE sent plan (`started.model` canonical; effort = explicit ??
row effort, stripped on haiku), model cause = `row <phase>` | `explicit`
| `fallback: <alias> down` (in-tier move) | `frontmatter` (tier down,
nothing written), effort cause = `row` | `explicit` | `engine` (no row).
`Loop` gains optional `{ type, last: {model, effort}, cause }`: set in
`trackLoop` from the effective plan; `writeLoop` updates `effort` and
`cause` only (`route tool` | `skill <name>` | `explicit`); each agent
step compares `plan` to `loop.last ?? {e.model, e.effort}`, logs
"model-router: agent <type>: <from> → <to> (<cause>)" on a difference,
then sets `loop.last = plan` (haiku: effort undefined on both sides);
no Loop or no `type` → label `agent <id8>`; `endAgent` drops the entry;
caps as `MAX_AGENT_MODELS`.
- Aliases: `aliasOf(cfg, canonical(cfg, id))` with a `[1m]` marker kept,
the id itself when no alias matches; effort `-` when undefined; every
name in a line passes `clean(…, 40)`.
- Answered-by anomaly (agents only; main's engine switch is already
logged by `onAutoSwitch`): after `next`, inside `safely`, when
`result.usage` is non-null and `canonical(usage.model) !==
canonical(plan.model)`, log once per (agentId, model) via `st.logged`
key `answered:<id>:<model>`: "model-router: agent <type> answered by
<alias> instead of <alias>".
- Unconditional lines stay as today and count as warnings: `logCall`
(upgrade skipped / no fit), `tierDownLog`, `onAutoSwitch`, `warnOnce`,
config drops, toasts. The spinner suffix unchanged.
- Config: `verbose` invalid (`"ON"`, 1, null) → default `false` + one
log per load (mergeConfig falls back to the seed, stated).
## D2 Residuals
- (a) `forgetWrite` catch AND `askCommand` catch (the latter gains the
`RebuildFailed` branch "ask saved, config not rebuilt: /route reload"):
`$.ui.log("model-router: <cmd> failed: " + String(err))`, answers
"forget not applied, see the log" / "ask not applied, see the log".
- (b) `kept` becomes `{kind, name, reason}[]`; `clean(x, 40)` applied to
the file-sourced fragments (name, from, to) at RENDER time only
(`forgetAnswer`, `restoreClause`, `idleAnswer`, kept rendering); the
fixed reason texts untouched; `ForgetPlan`/`Restore` fields untouched
(a 45-char row name is restored byte-exact in the file; test).
- (c) `floorFile`: `SKILL_NAME.test(name)` else undefined.
- (d) `restoreOf`: `name === '__proto__'` → refused ("reserved name").
- (e) the `changed` scan iterates ROWS only.
- (f) `mem.filePhases` = exactly the seeded layer: `mergeConfig({
phases: file.phases }, noop).phases` (code defaults + file phases
validated against the DEFAULT models and tiers, override excluded),
computed in `assemble` AND `refreshMemory`; `effortsOf` and
`targetPhase` use it, so a machine-only phase is never offered nor
written; the existing empty-efforts toast reads "no shipped phase uses
<alias>: add one to routing.json". Tests: a null file phase (ignored,
no crash), a file without `phases` (code defaults offered), an
override-only `custom` (not offered, not written).
- (g) `confirmFirst`: `answered` flag set after `decide()` returns; the
catch deletes the key from `asked` only when unset. Tests: a throw
BETWEEN `asked.add` and the answer (a throwing `ui.toast` on an
unrecognised answer) → key unasked (fails if the delete is removed);
`world.failWrite` (post-answer) → key stays asked, one log; the three
existing tests asserting the old post-answer deletion are inverted
(authorized); the `failOnce` dedupe test rewritten with two different
rows failing.
## Steps
- [ ] S1 delete the four vlog sites; `planStep` → `{plan, why}`;
`Routed.label` (skill name, prompt rule phase); `Loop {type?, last?,
cause?}`; `lastPlan = null` on `/route on|off`.
- [ ] S2 `transitionLine` (main, in safely before next, turnLogged dedupe)
+ agent line in `afterSpawn` + agent step comparison + answered-by
anomaly; `aliasOf` canonical; names cleaned.
- [ ] S3 residuals (a)-(g).
- [ ] S4 docs: README (three `verbose` sites: the `/route` bullet, the key
list "default false", the commands table → "transitions only"),
USAGE `/route` row, CHANGELOG Unreleased (Changed: logs; Fixed: the
residuals). argumentHint unchanged.
- [ ] S5 kit tests (recordLogs recorder; a `usage`-bearing bottom step hook
where needed), one per clause: main unchanged over 3 steps → 0
lines; route tool plan → exactly one line with "(plan: route tool)";
typed `/feat` → "(reflect: skill /feat)"; derived orchestrate → one
line per turn even with two dispatch waves; ultrathink → "(escalate:
prompt floor)"; sticky `/route xhigh` → "(custom: sticky /route)";
sonnet session + typed `/feat` → ", upgrade"; feater spawn → one
agent line "agent feater: fable/high → sonnet/medium (row: feater
implement)" and 0 lines on its next steps; explicit model/effort
spawn words; in-agent route tool effort move → one agent line
"(route tool)"; answered-by anomaly once (usage hook), none when
usage null, none on a `[1m]` plan; a throwing `ui.log` leaves the
step routed; `verbose: false` → nothing; invalid override values
→ default + one log; /clear → first step compared to engine input;
the legacy four lines never appear at `true` (typed skill, route
tool, spawn, step); "explain why" prompt → "(reflect: prompt
rule)"; explicit-effort spawn and haiku spawn → one hand-off line
then 0 lines on their next steps; three parallel challengers → one
hand-off line; tier-down spawn → "frontmatter" cause; (a)-(g) each
with a mutation-sensitive test.
- Disposition: honors BDR-115/116 (no new writer; logs carry ids and
cleaned names only), LRN-210, LRN-212 (live mod: the terminal changes
as soon as the feater saves).
## Challenge ledger (r1 → r2)
- simp 1, corr 11/12 (`trace` surface) → removed; boolean kept.
- simp 2, corr 2, rob 3/6 (new Map, missing type/cause) → `st.lastPlan`
+ `Loop {type, last, cause}`, dropped in endAgent, capped.
- simp 3/4, corr 1 BLOCKER/3/4/5, rob 3 (why not derivable; spawn move
invisible) → `planStep` returns why from the decision; `Routed.label`;
agent line in `afterSpawn` with `parentModel`.
- simp 5, corr 6, rob 2 (answered-by) → agents only, canonical ids, null
usage skipped, `st.logged` dedupe.
- rob 1 (log failure unroutes) → safely before next, own warn key.
- rob 7 (flapping) → per-turn dedupe via turnLogged.
- rob 8 (names) → clean(40) on labels and causes.
- rob 9, corr 12 (invalid verbose) → default + one log, stated.
- rob 10 (/clear) → stated omission; reset at /route on.
- rob 11 (legacy lines untested) → S5 cases.
- simp 6, corr 9, rob 5 ((f) unvalidated / dead toast) → validated seed
layer in assemble + refreshMemory; toast folded.
- simp 7, corr 8 ((g) premise) → `answered` flag; tests inverted.
- rob 4 ((b) clean on plan fields) → render-time only.
- corr 10, rob 12 ((a) site + twin) → forgetWrite + askCommand.
## Confirmation ledger (r2 → r3)
- conf 1 (agent from-effort unsourced, non-changes) → hand-off line from
main's lastPlan, per-axis causes, per-turn dedupe per (type, to).
- conf 2 (loop.last bookkeeping) → last = effective sent plan, writeLoop
updates cause/effort only, each step sets last after comparing; tests.
- conf 3 (prompt rules unlabelled) → `prompt rule` cause + label.
- conf 4 ((b) kept strings) → kept as records, fragments cleaned at render.
- conf 5 ((g) pre-answer test) → throw between asked.add and the answer.
- conf 6 (prev before noteMain) → stated.
- conf 7 (why words, no-route render, alias fallback) → mapping spelled out.
- conf 8 ((f) formula) → `mergeConfig({phases}, noop).phases`.
- conf 9 ((a) askCommand) → RebuildFailed branch added.
- conf 10 (/route on reset) → S1.
- conf 11 (loop without type) → optional fields, id8 label.
- conf 12 (failOnce test) → two rows.