11 KiB
11 KiB
PLAN r3 — model-router wave 3-B: a dialog with context, model then effort on change (2026-10-11)
r1 → r2 after simplicity CONCERNS(3), robustness FATAL(5), correctness
FATAL(8): all three rejected inline-route rows and dialog edits of phases.
r2 → r3 after the confirmation (correctness CONCERNS(2)). Precedence r3 > r2 > r1. Contract .claude/tasks/contracts/2026-10-11-model-router-w3b-dialog-1240.md.
Base = W3-A (22455c0, plan r4, BDR-116). Facts: agent.offer payload carries
description and source; $.ui.ask takes 2-4 labels + Other; cfg.models
maps the four aliases to ids; each default alias heads one tier (best fable,
big opus, work sonnet, cheap haiku); readCapped exists (size cap, stat).
Decisions
- D1 rows stay PHASE NAMES everywhere (routing.json, projects, override). No inline route rows. The dialog's model+effort choice maps to a phase.
- D2 the dialog never edits a phase: T3 offers Later / Keep only, with context. Phases change by hand in routing.json + DEFAULT_CONFIG (census lock kept). BDR-116 unchanged.
- D3 one description rule for every kind, hardened read, no cache.
Texts (ASK1)
clean(s, n): FIRST fold every\sand\p{Z}(incl. U+2028/2029) to a space, THEN strip\p{Cc}and\p{Cf}, collapse spaces, trim; when longer than n code points keep n-1 and append "…" (total ≤ n). Applied to every description andabout(no answer echo anywhere).- Leads are KEPT as the first words (existing assertions stay valid):
"First route for /feat — . Routed to reflect (): claude-fable-5-1
at high. OK?", "First dispatch of security-auditor — . Routed to
verify (): claude-sonnet-5-5 at xhigh. OK?", "First use of
orchestrate on the main loop — ; used by …: … OK?". Without a
description the " — " part is omitted; without
aboutthe parenthesis is omitted. - Skill row desc =
skillDesc($, st, name): name must match^[A-Za-z0-9][A-Za-z0-9._:-]*$(else name only);readCappedof${HOME}/.claude/skills/<name>/SKILL.md(stat kind must befile, size cap as the override; HOME from$.env.get, unset → name only); parse the frontmatterdescription:in its five forms (plain,|,>/>-, single-quoted with'', double-quoted): unquote, join the block lines with spaces, cut at the first sentence end (./.at end), then clean(…, 140); any failure → name only. Read at ask time, once (the key is asked at most once per session). The kind check lives in a skill-specific wrapper aroundreadCapped(config reads untouched). - Agent row: the offer description goes through the SAME rule (sentence
cut + clean 140) WHEN STORED at
agent.offer(bounded cacheMap<type, {source, description}>); absent → name only. - Phase (T3): "First use of orchestrate on the main loop — ; used by
rows[, the derived route of dispatches][, prompt rule(s)]:
claude-fable-5-1 at medium. OK?"; consumers derived:
orchestrate→ "the derived route of dispatches" (hardcoded, pushOrchestrate), prompt rules counted fromcfg.prompt.filter(r => r.phase === name)(floor rules named "floor rule"). Options ["Later", "Keep"]. - Row options: ["Later", "Keep", "Change"]. At EVERY step the
askOrLATER (dismissed, headless) is checked first and is silent; any other non-label answer = Later + toast "answer not recognised, default kept" (no echo).ALTS, the alt slice inoptionsForand the free-text phase branch ofdecideare removed. - Main-row texts keep the real-decision rule (decideFor/mainEffort); spawn texts keep spawnTarget + explicit effort.
Change flow (rows only)
- ASK2 "Which model for
<name>?": four FIXED labels from the default aliases, each with its tier role ("fable (best)", "opus (big)", "sonnet (work)", "haiku (cheap)"; role = the tier headed by the alias incfg.tiers, "(custom)" when none); Other or an unknown label = Later + toast. - ASK3 "Which effort on ?": the efforts of the phases (in
cfg.phases) whose tier head is the chosen alias, deduplicated and sorted ASCENDING by LEVELS (fable: medium high xhigh max; opus: xhigh; sonnet: low medium high xhigh; haiku: low) → at most 4 labels (the first 4); exactly one → ASK3 skipped; zero → Later + toast "no phase uses "; Other = Later. - Target phase = the row's CURRENT phase when it matches (alias, effort),
else the first matching phase in
cfg.phasesorder. Same phase as now → treated as Keep (nochangedentry). - ASK4 scope as W3-A: ["Everywhere", "This project only"], or ["Everywhere", "Later"] when there is no repo key.
- After a CHANGE on a MAIN row (T1, never on Keep), computed in
confirmSkillfromskillRow+decideFor: toast "/<name>now runs at " plus ", main moves up only:/route switch onallows a downgrade" when the chosen alias ranks below the session model and the switch is off; the new route is re-picked into the current decision (T1/T2 as W3-A). - Storage unchanged from W3-A:
changed.<kind>.<name> = {from (first), to},confirmed.<kind>.<name> = to(strings),projects[key]rows.
about
- routing.json:
phases.<name>.about(≤ 120 chars, clean) on the 11 phases; loaded intomem.about: Record<phase, string>(part of the memory rebuilt with the config, so it survives /clear with cfg; never onRoute, never in DEFAULT_CONFIG; the censuscode_phaseregex untouched); a non-string or overlongaboutis dropped + logged once; a phase withoutabout→ no parenthesis. acceptPhase ignores the key.
Steps
- S1 routing.json
about×11 (one line each from the W2 phase table); loader + map + validation. - S2 descriptions: offer cache
{source, description};skillDesc(allowlist, readCapped, kind check, YAML forms, sentence cut);clean. - S3 ASK1 texts + options Later/Keep/Change; Other → Later + toast;
dead code removed (
ALTS, alt slice, free-text branch). - S4 change flow ASK2 → ASK3 (derived efforts) → ASK4; target-phase rule; same-phase = Keep; main toast with the real decision.
- S5
lib/effort-shift.mddialog lines (≤ 4 changed). - S6a migrate the W3-A dialog tests: every
asks([...])answering a phase name at ASK1 becomes Keep or a Change chain (ASK1 → ASK2 → [ASK3] → ASK4; theasked[n]indexes shift accordingly); option-list assertions become Later/Keep/Change (T3 Later/Keep); the lead-text assertions (:2266 "First dispatch of feater", :2395 "First use of orchestrate on the main loop") stay valid by construction; :1914 expects the new toast "answer not recognised, default kept"; the two free-text tests (:1871 "a free-text phase counts as an alt", :1878 "equal to the current phase is a Keep") are DELETED (authorized: they test the removed branch); "a phase name answer is a Later" kept. World/helper extensions:offerOftakes a description; the World gains a per-path statkindsoverride and a HOME override (unset). - S6b new tests, one per clause: skill text with each YAML form (plain,
|,>-, 'sq' with'', "dq"), missing SKILL.md → name only, a bad name (../x) → name only, stat kind ≠ file → name only, a SKILL.md over the cap → name only, HOME unset → name only, a 500-char description with control, bidi and U+2028 chars → ≤ 140 clean code points ending in "…"; a Keep on a main row → no "now runs" toast; a Change → the toast with the switch hint; zero-effort alias → Later + toast; dismissed ASK2 → silent Later; agent text from the offer description; T3 text withaboutand consumers; ASK2 labels; ASK3 derived efforts per alias (fable 4, opus → skipped, sonnet 4, haiku → skipped); security-auditor Change → opus → (skipped) → Everywhere → rowjudge, spawn on claude-opus-5-5 at xhigh now; feater Change → sonnet → high →write; Explore Change → sonnet → medium → staysexplore(current phase wins) → treated as Keep, nochanged;/featChange → sonnet → medium →implement, toast names "moves up only" with the switch off; Other at ASK1/ASK2/ASK3 → Later + toast, nothing written; project scope with the derived phase;aboutoverlong → dropped + logged. - Disposition: honors BDR-116 rules (1), (3)-(6) unchanged (rows = phases,
dialog never edits phases, writers/paths/caps as is); AMENDS its clause
(2): options become Later/Keep/Change with a model-then-effort chain,
Other = Later (successor amendment written at CAPITALIZE, append-only); BDR-115 (full ids from
cfg.models), LRN-210 (one clause per test), LRN-212 (live mod: announce dialogs). - Docs (README/USAGE/CHANGELOG lines on the dialog) drift after this wave: STEP 6 doc-sync, out of this contract's scope.
Challenge ledger (r1 → r2)
- simp 1/2, rob 2/3/4/7/11/12, corr 1/2/3/9/10/12 (inline rows) → D1: rows stay phases; efforts derived from the alias's phases; target-phase rule with tie-break; same-phase = Keep.
- simp 3, rob 1 BLOCKER/5/6/13, corr 4/5 (T3 phase edits) → D2: T3 Later/Keep with context; BDR-116 unchanged.
- simp 6, rob 8/9, corr 8 (descriptions) → D3 single rule, five YAML forms, hardened read, allowlist, clean() with Cc/Cf and code points.
- simp 7, corr 14 (
abouton Route) → separate map, validated at load. - simp 8, corr 13 (ASK2 Other, aliases) → fixed four labels, Other = Later.
- simp 9 (dead code) → S3 removal.
- rob 10, corr 6 (main model pick is a placebo) → toast with the real decision and the switch hint.
- corr 7 (W3-A tests) → S6a migration step.
- corr 11 (no-key scope) → W3-A behaviour kept ([Everywhere, Later]).
- corr 15 (docs, AC5 oracle) → doc-sync noted; contract AC5 reworded.
Confirmation ledger (r2 → r3)
- conf 1 (S6a under-listed) → leads kept by construction; toast text,
deleted free-text tests,
asked[n]shifts listed. - conf 2 (read-hardening clauses untested) → S6b tests + World extensions.
- conf 3 (ASK3 order) → ascending by LEVELS; tie-break in
cfg.phasesorder. - conf 4 (clean order, length) → fold first, strip, n-1 + "…".
- conf 5 (dismissed ASK2/3 toasted) → LATER checked first, silent.
- conf 6 (zero efforts, texts, echo) → Later + toast; ASK2/ASK3 texts; no echo.
- conf 7 (toast on Keep) → Change only, computed in confirmSkill.
- conf 8 (BDR-116 clause 2) → amendment at CAPITALIZE.
- conf 9 (
aboutacross /clear) → inmem, rebuilt with cfg. - conf 10 (agents vs skills rule) → same rule at offer time, bounded.
- conf 11 (hardcoded consumers) → derived from cfg.prompt.
- conf 12 (kind check placement) → skill wrapper around readCapped.