From 22455c051f1df3ac4aad564630abbc270071c82f Mon Sep 17 00:00:00 2001 From: bchanot Date: Sun, 11 Oct 2026 11:48:25 +0200 Subject: [PATCH 01/12] =?UTF-8?q?feat(model-router):=20wave=203-A=20?= =?UTF-8?q?=E2=80=94=20first-use=20route=20confirmation,=20decision=20memo?= =?UTF-8?q?ry,=20project=20exceptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit routing.json (tracked, reached through the plugin directory) is now the single source of the phase table and of every skill/agent row, plus the decisions: confirmed rows/phases, changed rows (from/to) and projects exceptions keyed by a normalized git remote (credentials never stored, no machine paths). First use of a rowed typed skill, a rowed agent spawn or a main-loop phase opens the engine's dialog (Later / Keep / two alternative phases; Other = a phase name); a change asks Everywhere or This project only. One dialog at a time, never in headless, never inside an agent, never written by the model: only a dialog answer or /route ask writes, serialized, size-capped, never creating the file. Layers: routing.json < ~/.claude/model-router.json; the project tree is never read. /route pending, /route ask on|off. Census reads rows and phases from the file and tolerates a user-changed row (WARN). Kit suite 86 → 190 tests. Contract .claude/tasks/contracts/2026-10-10-model-router-w3a-confirm-1201.md, plan r4: 3 lenses + 1 confirmation, feater + 4 rounds, GATE 0 MET, verifier CONFORME then re-verify after security, security BLOCK(1) fixed then PASS. Live: T2 dialogs answered by the user from the hot-loaded mod. --- lib/effort-shift.md | 4 + lib/tests/effort-routing.test.sh | 169 ++- mods/model-router/hooks/register.test.ts | 1347 +++++++++++++++++++++- mods/model-router/hooks/register.ts | 809 +++++++++++-- mods/model-router/routing.json | 144 +++ 5 files changed, 2299 insertions(+), 174 deletions(-) create mode 100644 mods/model-router/routing.json diff --git a/lib/effort-shift.md b/lib/effort-shift.md index dc487d0..52f685c 100644 --- a/lib/effort-shift.md +++ b/lib/effort-shift.md @@ -48,6 +48,10 @@ Builtin `/effort` is NOT a lever inside a run: rows and routes outrank it. typed marker (unverified live 2026-10-10). - Headless (`-p`, SDK) runs the hooks, so routing works there too. - Mod off: typed agents fall back to their `model:`/`effort:` frontmatter. +- First use of a row asks once (Keep, Later or another phase, then + Everywhere or this project only); the answer is kept in + `mods/model-router/routing.json`. `/route pending` lists what is still + unconfirmed, `/route ask off|on` toggles the dialog. Measure the split any time: `python3 ~/.claude/lib/effort-audit.py` (thinking/output/cache tokens per scope, model and effort). diff --git a/lib/tests/effort-routing.test.sh b/lib/tests/effort-routing.test.sh index e2b9214..3d7f855 100755 --- a/lib/tests/effort-routing.test.sh +++ b/lib/tests/effort-routing.test.sh @@ -1,12 +1,16 @@ #!/usr/bin/env bash # lib/tests/effort-routing.test.sh — wave-2 census of the model-router rows. -# Drift lock: every tracked skill/agent row in mods/model-router/hooks/ -# register.ts equals its frontmatter (the off-state floor), the D3 wiring -# markers sit in the orchestrators, no shifter citer survives. +# Drift lock: every tracked skill/agent row of mods/model-router/routing.json +# equals its frontmatter (the off-state floor), except a row the user changed +# through the first-use dialog (WARN floor drift: the floor may lag a +# confirmed decision); the D3 wiring markers sit in the orchestrators, no +# shifter citer survives. # shellcheck disable=SC2015,SC2016 # A && ok || ko is deliberate (ok/ko never fail) set -u R="$(cd "$(dirname "$0")/../.." && pwd)" -REG="$R/mods/model-router/hooks/register.ts" +MOD="$R/mods/model-router" +RJ="$MOD/routing.json" +REG="$MOD/hooks/register.ts" pass=0; fail=0 ok() { pass=$((pass+1)); } ko() { fail=$((fail+1)); printf 'FAIL %s\n' "$1"; } @@ -16,84 +20,145 @@ lacks() { if grep -qF "$2" "$R/$1"; then ko "$1 must NOT contain: $2"; else ok; fm() { awk 'NR==1&&/^---$/{p=1;next} p&&/^---$/{exit} p' "$1"; } fm_val() { fm "$1" | grep -E "^$2: [a-z]+$" | head -1 | cut -d' ' -f2; } -# ── register.ts parsers (awk/sed on the DEFAULT_CONFIG literal) ────────── -# rows -> "name phase" per row -rows() { - awk -v s="$2" '$0 ~ "^ "s": \\{"{f=1;next} f&&/^ \},?$/{f=0} f' "$1" \ - | grep -v '^ *//' | grep -oE "('[^']+'|[A-Za-z0-9_-]+): '[a-z]+'" \ - | sed -E "s/'//g; s/: / /" -} -# phase_effort -> " " -phase_effort() { - awk '/^ phases: \{/{f=1;next} f&&/^ \},?$/{f=0} f' "$1" \ - | sed -nE "s/^ *$2: \{ tier: '([a-z]+)', effort: '([a-z]+)' \},?$/\1 \2/p" +# ── routing.json (rows, phases, decisions) and the tier heads ──────────── +# dump -> "skills|agents ", "phases

+# ", "changed " lines +dump() { + python3 -I - "$1" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])) +for kind in ("skills", "agents"): + for k, v in d.get(kind, {}).items(): + print(kind, k, v) +for k, v in d.get("phases", {}).items(): + print("phases", k, v.get("tier", ""), v.get("effort", "")) +for kind in ("skills", "agents"): + for k, v in d.get("changed", {}).get(kind, {}).items(): + print("changed", kind, k, v["from"], v["to"]) +PY } +# lookups on a dump text: row_of D kind name · phase_effort D phase · +# changed_of D kind name -> "from to" +row_of() { printf '%s\n' "$1" | awk -v k="$2" -v n="$3" '$1==k&&$2==n{print $3}'; } +phase_effort() { printf '%s\n' "$1" | awk -v p="$2" '$1=="phases"&&$2==p{print $3, $4}'; } +changed_of() { printf '%s\n' "$1" | awk -v k="$2" -v n="$3" '$1=="changed"&&$2==k&&$3==n{print $4, $5}'; } # tier_head -> first alias of the tier list tier_head() { awk '/^ tiers: \{/{f=1;next} f&&/^ \},?$/{f=0} f' "$1" \ | sed -nE "s/^ *$2: \['([a-z]+)'.*$/\1/p" } -row_of() { rows "$REG" "$1" | awk -v n="$2" '$1==n{print $2}'; } +# the fallback copy of the phases inside the mod's code: " " +code_phase() { + awk '/^ phases: \{/{f=1;next} f&&/^ \},?$/{f=0} f' "$REG" \ + | sed -nE "s/^ *$1: \{ tier: '([a-z]+)', effort: '([a-z]+)' \},?$/\1 \2/p" +} +# field_of -> that phase's frontmatter value +field_of() { + local pe tier + pe="$(phase_effort "$1" "$2")" + if [ "$3" = effort ]; then printf '%s\n' "${pe#* }"; return; fi + tier="${pe% *}"; tier_head "$REG" "$tier" +} +# verdict : ok when the +# frontmatter equals the row's phase; warn when the row was changed through +# the dialog (changed.from is the shipped phase, changed.to the row) and the +# frontmatter still holds the shipped value; fail otherwise (a Keep alone, +# an empty or unknown value, a hand edit after a change) +verdict() { + local D="$1" kind="$2" name="$3" field="$4" got="$5" phase="$6" ch from fromval + [ -n "$got" ] || { echo fail; return; } + [ "$got" = "$(field_of "$D" "$phase" "$field")" ] && { echo ok; return; } + ch="$(changed_of "$D" "$kind" "$name")"; from="${ch% *}" + [ -n "$ch" ] && [ "${ch#* }" = "$phase" ] || { echo fail; return; } + fromval="$(field_of "$D" "$from" "$field")" + [ -n "$fromval" ] && [ "$got" = "$fromval" ] && echo warn || echo fail +} +# floor_check