The 15 Skill(effort-*) citers now call mcp__model-router__route per phase
(orchestrate at a dispatch span, reflect/plan for the skill's own level,
apply at the bookkeeping tail, escalate at the verify-secure caps); built-in
judgment dispatches carry an explicit effort= param. lib/effort-shift.md is
the route doctrine, lib/model-gate.md the mod rule (route answer = witness,
/route on as remedy). Deleted: skills/effort-*, lib/effort-pins.txt/.sh,
lib/model-check.sh, their tests, the installers' re-apply blocks. The mod
drops its Skill(effort-*) bridge. The tracked model:/effort: frontmatter
stays as the off-state floor, census-locked equal to the rows
(lib/tests/effort-routing.test.sh rewritten, 140 checks; analyzer → xhigh).
Contract .claude/tasks/contracts/2026-10-10-model-router-w2b-1045.md, plan
r4 § W2-B: GATE 0 MET, verifier ECARTS(7) then CONFORME 10/10, security
PASS, full make test green (design-tool-gate env red only).
Rows by role replace the pins as the live source (frontmatter stays as the
off-state floor): phases write=work/high and apply=work/low, 56 skill rows,
21 agent rows + Explore/Plan. Agents get the row's model at spawn (within
the tier, upward only, explicit params win, project-defined agents skipped
via agent.offer) and its effort per step. A typed slash of a rowed skill
routes main through a name-bound marker (composer|sdk|bridge, pending slot
mid-turn) or the idle fallback; a best-tier row lives in a runMain slot
that survives turn end and route calls. An unrowed skill leaves the route.
Typed /effort-* floor code removed (bridge kept until W2-B). The route
answer always names the id. Override rows accept null. Kit suite 58 → 88.
Contract .claude/tasks/contracts/2026-10-09-model-router-w2a-1546.md, plan
r4 .claude/tasks/plans/2026-10-09-model-router-w2-1546.md: 3 lenses + 2
confirmations, feater + 4 rounds, GATE 0 MET, verifier 3x ECARTS on test
coverage only (user-accepted at the cap), security PASS.
A full make test printed only the == headers and an aggregate exit code,
so finding the red suite meant re-running every suite one by one (the
pre-merge check of 2026-10-09 took 7.5 min for that reason). The loop now
prints FAIL <suite> as it happens and ends with 'all suites green' or
'<n> suite(s) red: <names>'; the exit code is unchanged.
Phases name absolute tiers (best fable>opus>sonnet, big opus>fable>sonnet,
work sonnet>opus, cheap haiku>sonnet) resolved to the first available full
id; per-model circuit breaker fed by StopFailure kinds (rate_limit,
overloaded, billing_error, model_not_found) and PostModelSwitch auto, with
episode backoff 15→300 min, cleared by a user /model or /route reload and
kept across /clear; fallback chain fable→opus→sonnet→haiku with the effort
unchanged; main loop upgrades to a phase's tier by itself under a context
cap (fails closed on unknown usage), downgrades only with the switch on,
sticky within a turn; derived orchestrate on background dispatches;
prompt default rules (plan/reflect, Unicode guards, skipped on slash
commands, floor matches and mid-turn). 58 plugin tests.
Tracked relative symlink skills/model-router -> ../mods/model-router: Claude
Code loads the mod in place as model-router@skills-dir wherever link.sh
links ~/.claude/skills (no CLAUDE_CODE_PLUGIN_DIRS: absolute paths in the
tracked settings.json). Engine-laid mods/*/tsconfig.json gitignored.
lib/tests/mods.test.sh: manifest name, link target, claude plugin validate
and test per mod, capability-probed, time-bounded, SKIP with reason.
doctor.sh: fail-soft Mods section (link by -ef, one guarded plugin list).
CLAUDE.md: mods/ section (loading, per-machine enabled:false switch,
dev-copy shadowing, tests).
One decision helper (mainEffort) feeds the plan and every answer text;
per-axis precedence (sticky > turn route > floor > engine); a mid-turn
prompt floors the running turn and the next; per-machine kill switch
"enabled": false in ~/.claude/model-router.json, kept across /clear and
across a failed reload; typed /effort-<l> attested at prompt.submit so a
sub-agent preload cannot floor the main loop. 30 plugin tests.
Security-gate round on the wave 1-A mod: /route answers only a composer
origin; an in-agent route call can no longer change the agent's model
(effort only, model fixed at spawn); config patterns capped (200 chars,
4096-char scan), phase keys restricted, override file refused above 64 KB,
additionalProperties false on the tool schema; every .catch logs once per
session; post-next bookkeeping isolated. 14 plugin tests, verifier 11/11.
Function-hooks plugin under mods/model-router: routes effort (and, behind a
flag, the model) of every main-loop request, sets built-in sub-agents' model
at spawn with full ids, answers Skill(effort-*) itself (single writer, no
pairing rule), exposes the route tool and /route, validates the optional
~/.claude/model-router.json. 11 plugin tests, validate + tsc clean.
Contract .claude/tasks/contracts/2026-10-08-model-router-w1a-1533.md.
Run D3 of manual-push mode (BDR-114). With every reader now failing
closed, the skill prose stops saying the lib and hooks still push on an
invalid value:
- capitalize STEP 5C / STEP 6: the invalid outcome is split on the ahead
count (nothing pushed vs pushed anyway by a stale fail-open hook or a
manual push); the verb's stderr line is quoted verbatim; neighbouring
closing lines carry push-mode qualifiers so none shadows the invalid
case; the --no-push lines follow the same rule.
- client-handover: "COMMIT + PUSH" labels become "COMMIT + PUSH STATE
READ"; the STEP 5 residual sentences no longer imply the pipeline
pushes; the invalid value is named as a case where the user pushes.
- release-executor: prep span checks the version format by reading the
string (never in a Bash command); manual mode and an invalid value
both leave main/develop local.
Run D2 of manual-push mode (BDR-114).
- push-guard sources lib/gitflow.sh once (absolute path) and reads each
candidate dir through gitflow_push_mode; a missing lib denies.
- Dir tokens are extracted as whole shell words: a fully quoted token
(inner apostrophe allowed) is resolved, a backslash-escaped space is
unescaped deterministically, a token mixing quoted and unquoted parts
is refused (fail closed) instead of resolving to its parent.
- A payload jq cannot parse is scanned as raw text with its JSON escapes
folded; a push-looking one gets the static deny through the trap.
- The 20-token cap runs before any per-token classification (a flood of
20 000 tokens is refused in 0.13 s; T58 locks it under 5 s).
- `case "$mode"` has a deny default; missing core tools warn and allow.
- T42 compares the deny list against main (the last release) instead of
HEAD; literal-true, mixed-token, broken-payload, lib-missing and
banner-on-bad-value cases added (98 checks).
- session-start banner reads the mode through the verb and shows
`push : manual (autopush bad)` on an unparseable value.
- tour hints quote "<abs project>".
Run D1 of manual-push mode (BDR-114). `git config --bool --default true
gitflow.autopush` only covered a MISSING key: an unparseable value made
git die with empty output, the `= false` test failed, and every push ran
again. A typo on a work machine silently re-enabled the pushes it was
meant to stop.
- lib/gitflow.sh: `_gitflow_push_off` reads the mode through the lib
verb (`push-mode`); anything but `auto` is push-off, and the verb's
stderr line names an invalid value during start/finish.
- Emitted post-commit/post-merge hooks (POSIX sh, standalone): push only
when the key reads `true` or is unset; `false` exits quietly; any
other result prints one stderr line ("NOT pushed, treated as manual
push mode") and exits 0. Mirrors gitflow_push_mode.
- .githooks/ and githooks/ regenerated files-only through `emit-hook`
(no config read or write; .git/config hash unchanged).
- hooks/unpushed-guard.sh: mode from the lib verb (absolute lib path
resolved before any cd, no temp file); anything but auto is manual;
the SessionStart line names an invalid or unreadable value.
- Tests: gitflow-test T18q block (invalid → start, hook and finish push
nothing and say so; `true` → the hook pushes; emitted hook is
POSIX-clean), unpushed-guard T14 rewritten.
Closes the non-gap observations the gates left on runs C1/C2:
- capitalize STEP 5C/6: heading no longer says "+ push"; the --no-push
fact read is its own paragraph and scoped to that path; the
auto-persisted line requires finish rc 0 AND ahead = 0; rc 5/2/6
(merged, branch not deleted) still report the push state; the
"not on origin" line carries the once-a-remote-exists hint.
- gitflow.sh push-mode: the raw config value echoed on stderr is reduced
to printable characters (LC_ALL=C, BSD tr safe) and capped at 64.
- gitflow-test.sh exports the hermetic git config env in the file, so a
bare run on a global-manual machine stays green.
- client-handover-writer: the branch allowlist refuses a leading dash.
Run C2 of manual-push mode (BDR-111/BDR-112). The four flows that pushed
on their own, or claimed the branch was on origin, now read the truth
after the fact and hand the user the exact command:
- client-handover-writer: the "Push to origin now?" question and its
push block are gone (the hooks had already pushed in auto-push mode;
push-guard denies it in manual mode). A reusable PUSH STATE READ
(branch, origin probe, `git rev-list --count origin/<br>..<br>`, the
verb only to word the reason) runs after commit-change, at the top of
the deploy pause, after "Deployed" and before each end report. The
branch name is validated against an allowlist before it is placed in
any command or hint (a hostile branch name is otherwise a shell
injection). Pending → the user pushes BEFORE the deploy pause; the
deploy brief says "after your push". `Push:` line in both reports.
- release-candidate STEP 6: two ahead counts + the verb; anything other
than auto with both counts 0 prints one user command
`! git push --atomic origin main develop v<X.Y.Z>` and stops; the tag
gate stays for auto mode; `hold` notes --follow-tags; version regex.
- release-executor: push claims qualified (auto-push mode, best effort).
- tour: mode-agnostic rule; STEP 3 reads one `git -C <project>` fact per
project (suffix-aware branch, --remotes=origin, origin probe) and the
summary row says on origin / local only with the user command.
Run C1 of manual-push mode (BDR-111/BDR-112).
- lib/gitflow.sh: `gitflow.sh push-mode` prints auto | manual | invalid
(rc 0; an invalid value is named on stderr). It is the one reader a
skill may call: the bare `git config … gitflow.*` read is denied to
Claude since run B. Ignores GITFLOW_NO_PUSH by design (documented).
- skills/capitalize/SKILL.md STEP 5C: the explicit `git push origin
develop` is gone — `finish` has pushed develop itself since BDR-095,
mode-aware since run A. 5C is now three separate read-only calls
(finish; push-mode; `git rev-list --count origin/develop..develop`)
and prose outcomes keyed on the real ahead count: pushed / manual push
mode, you push / not on origin / push FAILED / invalid value named,
plus a finish-failure outcome (merge vs delete rc distinguished).
STEP 6 closing lines and the recap carry every outcome; the
`--no-push` line reads the branch's own ahead count ("this disk only"
only when true). Invariant: no `git push` inside any Bash call; the
user hints are prose.
- skills/close/SKILL.md, lib/gitflow-aiguillage.md: "push" claims
qualified "in auto-push mode".
- lib/gitflow-test.sh T11b: six cases for the verb (default, true,
false, non-boolean with stderr + rc 0, corrupt config, usage).
Polish items from the gates are listed in TODO.md (C1 polish).