chore(memory): model-router wave 0 — plan, LRN-203/204, BLK-029, journal
This commit is contained in:
@@ -48,6 +48,7 @@ rules:
|
||||
| BLK-026 | 2026-10-06 | `make test` red on macOS: 13 suites, GNU-only idioms in suite + 7 libs (SIGPIPE under pipefail, `sed -i`, `wc` padding, `stat -c`, `realpath -m`, bare `timeout`, `grep -oP`) | resolved |
|
||||
| BLK-027 | 2026-10-06 | this machine never ran `make link`/`make plugin`: no global `core.hooksPath` → post-commit push never fired, branches landed ahead of upstream; 11 vendored skills + `~/.claude/.env` missing | resolved (link) / open (plugin) |
|
||||
| BLK-028 | 2026-10-06 | notify-attention on a VS Code client: bell + toast silent-degradation faults (merge of BLK-019 + BLK-020): terminalBell sound default off, ext hooks only terminals born after activation, Code muted in Windows mixer | resolved |
|
||||
| BLK-029 | 2026-10-08 | Claude Code mods (2.1.294): alias → id resolution for a model set by a hook lags the Agent tool's (`sonnet` → `claude-sonnet-5`, 404); Agent tool schema refuses full ids | upstream |
|
||||
|
||||
---
|
||||
|
||||
@@ -305,3 +306,11 @@ rules:
|
||||
- **Probe order (do FIRST, before server archaeology)**: fresh VS Code terminal, `printf '\a\a\033]777;notify;Test;hello\033\\'` → splits terminal path from client renderer; palette `Help: List Signal Sounds` → Terminal Bell preview bypasses terminal/BEL/hook/dtach/ext, isolates renderer audio in one step.
|
||||
- **Status**: resolved (BLK-019 2026-09-01, BLK-020 A+B 2026-09-02/03). Sources superseded by this entry; bodies kept for history.
|
||||
- **Reference**: `~/.claude/hooks/notify-attention.sh` header documents the setting; [[LRN-145]] terminalSequence-not-/dev/tty; silent-degradation class [[LRN-047]]; sources [[BLK-019]], [[BLK-020]].
|
||||
|
||||
## BLK-029 — Mods: model alias set by a hook resolves to a stale id (`sonnet` → `claude-sonnet-5`, 404) — 2026-10-08
|
||||
- **Friction**: model-router spike. Sub-agent routed by `agent.spawn` or `tool.call Agent` rewrite with alias `sonnet`/`haiku` → "model_not_found HTTP 404, model sent to the API: claude-sonnet-5". Same alias typed by the model in the Agent tool param → `claude-sonnet-5-5`, OK.
|
||||
- **Real cause**: two alias tables in the CLI (2.1.294): the Agent tool's is current, the function-hooks path's is stale. Not an access issue (`/model` lists all four tiers; explicit haiku/sonnet dispatches answered).
|
||||
- **Solution**: hooks write full ids (`claude-sonnet-5-5`, `claude-haiku-4-5-20251001`, `claude-opus-5-5`, `claude-fable-5-1`) from the mod's own table; Agent tool param rewrite stays alias-only (schema enum) → the mod sets the model at `agent.spawn`, not at the param.
|
||||
- **Status**: upstream (report to anthropics/claude-code with the request id `req_011Cfppp7VFt8z2Pd3zJrpUi`); workaround in model-router.
|
||||
- **Reference**: [[LRN-203]], plan `.claude/tasks/plans/2026-10-08-model-router-mod.md`.
|
||||
|
||||
|
||||
@@ -577,3 +577,5 @@ rules:
|
||||
- Merge (user go "tu peux merge dans develop"): final full suite green (46 suites minus the declared env red) + Health Stack shellcheck clean on the branch tip → `gitflow finish feature manual-push-mode` → develop 669db06, pushed, branch removed local + origin. 19 commits (runs A, B, C1/C2, D1/D2/D3 + docs + memory). User answered: only pushes change; commits/branches/local merges untouched; invalid value now fail-closed everywhere. User plan: dotfiles installer prompts for `gitflow.autopush` (default false) — told them the gitconfig template also needs `core.hooksPath` (the install wiped it). Open: user probe `! git push --dry-run` under autopush=false; AC6 env red (design-tool-gate); post-run-D residuals in TODO.
|
||||
- User tested manual-push mode on their machine: works (no auto push under `false`, bang-prefixed dry-run passes). Prompt handed over for the dotfiles repo: gitconfig template gets `core.hooksPath = ~/.claude/githooks` + `[gitflow] autopush = @AUTOPUSH@` rendered from an install question (default false, true/false only, unrendered placeholder = render failure). BDR-112 amended.
|
||||
|
||||
## 2026-10-08
|
||||
- model-router mod, wave 0 spike (user ask: one mod routes model + effort per request, replaces effort-* shifters + pins). Plan `.claude/tasks/plans/2026-10-08-model-router-mod.md`, 4 decisions by AskUserQuestion (spike-first main-loop switch, `CLAUDE_CODE_PLUGIN_DIRS` load, migration wave 2, names model-router / route / /route), rule "pin = entry default, sub-tasks route finer". Spike in dev-mods, hot reload on: `turn.step` effort rewrite proven (transcript `effort` field is the oracle, not `CLAUDE_EFFORT`); sub-agent model at `agent.spawn` + effort per step by agentId proven; main-loop fable → sonnet-5-5/low for 3 steps then back: works, one cold-cache step per switch INTO a model, return free. Found: hook-side alias resolver stale (`sonnet` → `claude-sonnet-5`, 404; Agent tool enum resolves the same alias to 5-5) → mod writes full ids only. feature/model-router-mod open, nothing committed yet (plan + TODO + journal pending).
|
||||
|
||||
@@ -1748,3 +1748,12 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
|
||||
## LRN-202 — Reading a stderr-then-stdout verb from a hook: `out=$(cmd 2>&1)`, last line = word, prefix line = reason; no temp file; lib path absolute before any cd
|
||||
- **Context**: unpushed-guard plan used `2>"${TMPDIR:-/tmp}/x.$$"` + cat + rm: fail-OPEN when TMPDIR is full (`|| mode=auto`), predictable path, symlink-followable on shared /tmp, leaked on kill; `mode=$(cmd 2>&1 >/dev/null)` captures ONLY stderr. push-guard's `mktemp` variant added `set -u` trap hazards. The verb writes its stderr line BEFORE its stdout word in one process, so `${out##*$'\n'}` is the word and the `gitflow.sh push-mode:` line is the reason (select by prefix, not `head -1`: a bash startup warning could precede it). Resolve the lib path to an absolute one BEFORE the hook's `cd "$cwd"` (a relative invocation otherwise resolves into the target repo).
|
||||
- **Apply**: hooks never touch temp files for a one-line capture; anything but the expected word is treated as the fail-closed state, never as the default. Links [[BDR-114]], [[LRN-196]], [[LRN-199]].
|
||||
|
||||
## LRN-203 — Mod hooks: model ALIAS set by a hook resolves through stale table (`sonnet` → `claude-sonnet-5`, 404); write full ids; oracle = transcript fields, not `CLAUDE_EFFORT`
|
||||
- **Context**: model-router spike 2026-10-08, CLI 2.1.294. `agent.spawn` or `tool.call Agent` param rewrite with alias `sonnet` → API got `claude-sonnet-5`, HTTP 404 model_not_found. Same alias passed by the model in the Agent tool param → `claude-sonnet-5-5`, fine. Full id `claude-sonnet-5-5` from hook → fine, every step answered by 5-5. Agent tool schema enum refuses full ids, so full ids reach API only via hooks. Effort rewrite at `turn.step` proven by transcript record field `effort` (high → medium); `CLAUDE_EFFORT` env + `perTurnEffort` stay at turn setting, blind to per-request rewrite.
|
||||
- **Apply**: any mod that sets a model carries its own alias → full-id table (one place to bump per tier release). Verify routing with transcript `message.model` + record `effort`, never env vars. Links [[BDR-108]] (aliases as pins: still right at the Agent-tool call site, wrong inside hooks), [[BLK-029]].
|
||||
|
||||
## LRN-204 — Main-loop model switch mid-turn works, costs one cold-cache step on the full context per switch INTO a model; return free (per-model cache, 1 h TTL)
|
||||
- **Context**: spike 2026-10-08, fable → `claude-sonnet-5-5`/low for 3 steps on ~260k context, then back. Conversation intact (tools, results, thinking blocks from another model in history: no error). First sonnet step cache_read 0 (full 260k billed), next steps 237k cached; return to fable step read 263k cached.
|
||||
- **Apply**: switch the main loop only for spans long enough to amortize one uncached read of the whole context (many mechanical steps), never per tool call; short mechanical work → small-context haiku sub-agent. Haiku 4.5 window 200k: a long main loop cannot go to haiku at all. Flag off by default in model-router. Links [[LRN-203]], [[BDR-107]].
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# TODO
|
||||
|
||||
## 2026-10-08 — model-router mod: one mod routes model + effort per request (feature/model-router-mod)
|
||||
Plan `.claude/tasks/plans/2026-10-08-model-router-mod.md`. Decisions 2026-10-08: main-loop
|
||||
model switch spike-first then flag off; load via `CLAUDE_CODE_PLUGIN_DIRS` + link.sh;
|
||||
migration of shifters/pins/model-gate in wave 2 after proof; names model-router / route / /route.
|
||||
- [x] W0 spike in dev-mods (hot reload): facts a-d established 2026-10-08 (plan file § Spike facts); e moved to W1.10
|
||||
- [ ] W1 core mod in `mods/model-router/` (config, route tool, /route, agents, skills, prompt rules, visibility, tests, install)
|
||||
- [ ] W2 migration: 15 skills off `Skill(effort-*)`, remove shifters + effort-pins + model-gate, census repointed, docs
|
||||
- [ ] W3 optional: step heuristics, haiku classifier, quota-aware downgrade, A/B
|
||||
|
||||
## 2026-09-30 — Higgsfield pack: CLI + skills in the install process, off by default (feature/higgsfield-pack)
|
||||
Contract `.claude/tasks/contracts/2026-09-30-higgsfield-pack-1412.md`, spec + plan under
|
||||
`docs/superpowers/` (transient). Approved 2026-09-30: toggle pack off by default, two toggles,
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# PLAN — model-router mod (feature/model-router-mod)
|
||||
|
||||
User ask 2026-10-08: one Claude Code mod that routes every request to the
|
||||
model and effort its task deserves, main loop and sub-agents alike, declared
|
||||
or automatic, configurable, loaded in every session (user scope). Replaces
|
||||
the five `effort-*` shifter skills, the `effort:` / `model:` frontmatter
|
||||
pins, `lib/effort-pins.txt` + `.sh` and `lib/model-gate.md` once proven.
|
||||
|
||||
Decisions taken 2026-10-08 (user, AskUserQuestion):
|
||||
- Main-loop MODEL switch: spike first, then behind a userConfig flag, off by
|
||||
default, applied only on explicit declaration. Main-loop EFFORT always routed.
|
||||
- Loading: `CLAUDE_CODE_PLUGIN_DIRS` in settings.json `env`, mod lives in the
|
||||
repo under `mods/model-router/`, symlinked by link.sh. No marketplace.
|
||||
- Migration: wave 2, after wave 1 is proven. Mod = single source of truth.
|
||||
- Names: mod `model-router`, tool `route` (model sees `mcp__model-router__route`),
|
||||
command `/route`, config `~/.claude/model-router.json`.
|
||||
|
||||
## Routing rule (user, 2026-10-08): pin = entry default, sub-tasks route finer
|
||||
The existing pins and `effort-*` shifters were built for this same goal with
|
||||
the tools of their time; the mod replaces them (or nearly). A pin is not to
|
||||
be contested one by one, but some were forced: a skill pinned to one level
|
||||
does many different things inside one run. So:
|
||||
- the entry pin of a skill or agent (today frontmatter / effort-pins.txt,
|
||||
tomorrow the config table) = the DEFAULT route of the run, never a ceiling
|
||||
or a floor;
|
||||
- inside the run every sub-task routes to its own phase: declared by the
|
||||
skill through the `route` tool (replaces `Skill(effort-*)` + pairing rule),
|
||||
or derived by the mod (Agent dispatch → orchestrate, Skill load → its
|
||||
entry phase, Read/Grep result → comprehension level, bookkeeping tail →
|
||||
mechanical);
|
||||
- an explicit per-call choice (Agent `model`/`effort` param, `/route`,
|
||||
`ultrathink`) beats the derived phase for that span;
|
||||
- wave 1 keeps the frontmatter pins as the entry defaults (the mod reads the
|
||||
same values), wave 2 moves them into the config table and deletes the
|
||||
frontmatter + shifters. Skills get their intra-run `route` calls in wave 2
|
||||
(the 15 `Skill(effort-*)` citers first).
|
||||
|
||||
## Spike facts so far (2026-10-08)
|
||||
- (a) main-loop effort rewrite at `turn.step` reaches the API: transcript
|
||||
records flip `effort: high` → `medium` after a `route` call. `CLAUDE_EFFORT`
|
||||
is NOT an oracle (turn-level setting); the transcript `effort` field is.
|
||||
- (c) sub-agent steps are visible by `agentId`; an explicit Agent `model`
|
||||
param resolves fine (sonnet → claude-sonnet-5-5, haiku → claude-haiku-4-5-20251001;
|
||||
haiku steps carry `effort: undefined`, no effort on that model).
|
||||
- ROOT CAUSE of the 404 (T1-T3, 2026-10-08): a model set by a hook as an
|
||||
ALIAS is resolved by a stale table (`sonnet` -> `claude-sonnet-5`, 404);
|
||||
the Agent tool's own enum resolves the same alias to `claude-sonnet-5-5`.
|
||||
T1 param rewrite + alias: 404. T2 spawn rewrite + alias: 404. T3b spawn
|
||||
rewrite + full id `claude-sonnet-5-5`: OK, every step answered by
|
||||
claude-sonnet-5-5 at effort medium (turn.step by agentId also OK).
|
||||
Rule for the mod: ALWAYS write full model ids from its own alias -> id
|
||||
table in the config (one place to bump when a tier ships). The Agent tool
|
||||
schema accepts only aliases, so full ids can only come from the hooks.
|
||||
To report upstream: hook-side alias resolution lags the tool's.
|
||||
- T4 main-loop model switch (fable -> claude-sonnet-5-5/low, switch on): WORKS.
|
||||
Steps 11 and 12 answered by claude-sonnet-5-5 at effort low, transcript
|
||||
records agree, thinking still produced (4.6 s), conversation intact (897
|
||||
messages, tools and results carried across). COST: the first step after a
|
||||
switch read 0 cached tokens on a ~260k context (cache is per model), the
|
||||
next step read 237k. Switching BACK to fable at step 14 read 263k cached
|
||||
tokens: the fable cache survived three sonnet steps (per-model caches,
|
||||
1 h TTL), so the return is free. Every switch INTO another model pays one
|
||||
cold-cache step on the full context. Consequence for the design: main-loop
|
||||
model switches only for spans long enough to amortize (many mechanical
|
||||
steps), never per tool call; short mechanical work goes to a haiku
|
||||
sub-agent whose context is small. Flag stays off by default.
|
||||
- Open: T3a (param rewrite + full id), fable/opus ids for the table,
|
||||
switching back mid-turn, behavior with thinking blocks from another model
|
||||
in history (no error seen), headless `-p` run.
|
||||
|
||||
## Harness facts (types 2.1.292, CLI 2.1.294)
|
||||
- `turn.step` (async generator) rewrites `model` and `effort` per request;
|
||||
`e.agentId` set inside a sub-agent loop. Pinned: turn, index, messageCount.
|
||||
- `agent.spawn` rewrites `model` (not effort); result carries `agentId`.
|
||||
- `tool.call {tool:'Agent'}` sees and rewrites the call's `model` / `effort`
|
||||
params; `tool.call {tool:'Skill'}` names the skill loading.
|
||||
- `$.tool.register` / `$.command.register` (`immediate: true` runs mid-turn).
|
||||
- `$.model.classify(text, labels)`, `$.session.usage().rateLimits`.
|
||||
- Hooks run under `claude -p` too (closes the BDR-107 headless gap).
|
||||
- Hook budget 10 s own code; `$` calls do not count.
|
||||
- Honest limit: a hook cannot know what the NEXT request will decide to do.
|
||||
Routing = declared phase (skill table, `route` tool, `/route`, prompt
|
||||
rules) + conservative after-the-fact heuristics on the following step.
|
||||
|
||||
## Phase table (proposal, config-driven)
|
||||
| phase | model | effort | when |
|
||||
|---|---|---|---|
|
||||
| plan | session | xhigh | brainstorm, plan, architecture, challenge synthesis, audit verdict |
|
||||
| reflect | session | high | diagnosis, reading to understand, contract, review |
|
||||
| orchestrate | session | medium | between dispatches, reading a report |
|
||||
| escalate | session | max | `ultrathink`, stuck loop, STOP relaunch |
|
||||
| judge | opus | xhigh | dispatched challengers, analyzers, audits (BDR-076) |
|
||||
| implement | sonnet | medium | code from a closed plan (feater, bugfixer, …) |
|
||||
| write | sonnet | medium | docs, prose from decided content |
|
||||
| verify | sonnet | xhigh | verifier, security-auditor |
|
||||
| mechanical | haiku | low | cp/mv, git bookkeeping, status collection, listing |
|
||||
|
||||
## Wave 0 — spike (dev-mods folder, hot reload, this session)
|
||||
- [x] W0.1 minimal mod: `/route` command, `route` tool, `turn.step` logging +
|
||||
rewrite, `agent.spawn` rewrite, `ultrathink` → max, spinner suffix
|
||||
- [x] W0.2 `claude plugin validate` clean; type-check with the header tsconfig
|
||||
- [x] W0.3 facts to establish, each with its evidence (usage.model, CLAUDE_EFFORT,
|
||||
debug log): (a) effort rewrite on main loop takes effect; (b) model rewrite
|
||||
on main loop mid-turn: works / breaks (thinking signatures, cache, tools);
|
||||
(c) sub-agent model via spawn + effort via step by agentId; (d) `/route`
|
||||
immediate mid-turn; (e) load via `CLAUDE_CODE_PLUGIN_DIRS` from settings env → moved to W1.10
|
||||
- [x] W0.4 record facts → journal + BDR draft; freeze wave 1 scope (facts in this file; registries pending user go)
|
||||
|
||||
## Wave 1 — core (repo `mods/model-router/`)
|
||||
- [ ] W1.1 config loader: `~/.claude/model-router.json` (phases, agents,
|
||||
skills, prompt rules, defaults); schema check; `/route reload`
|
||||
- [ ] W1.2 state: per-loop phase (main + agentId map), reset at `turn.start`
|
||||
to the prompt-derived phase; explicit > table > heuristic
|
||||
- [ ] W1.3 `route` tool + `/route [phase|show|reload|clear]` (immediate)
|
||||
- [ ] W1.4 agents: `tool.call Agent` param rewrite + `agent.spawn` model +
|
||||
`turn.step` effort by agentId; covers built-ins (Explore, Plan, general-purpose)
|
||||
- [ ] W1.5 skills: `tool.call Skill` → phase from the skills table; any
|
||||
skill load RESETS the main route to that skill's entry phase (table,
|
||||
else the frontmatter `effort:` the harness just applied), so no route
|
||||
declared earlier in the turn survives a skill change silently
|
||||
- [ ] W1.5b single-writer bridge for the legacy shifters (user, 2026-10-08:
|
||||
doublon + silent one-way conflict): `tool.call {tool:'Skill', skill:
|
||||
/^effort-/}` answers WITHOUT `next` (skill text never loaded, no pairing
|
||||
rule) and translates the level into a route on the calling loop;
|
||||
`skill.prompt {skill:/^effort-/}` does the same for a user-typed
|
||||
`/effort-max` and returns a one-line text. The 15 citers keep working
|
||||
untouched until wave 2 rewrites them to `route`. Rule: every effort
|
||||
change goes through the mod's state; frontmatter values are inputs.
|
||||
- [ ] W1.6 prompt rules: `ultrathink` → escalate; keyword → phase (effort up
|
||||
only; never a main-loop model change without declaration)
|
||||
- [ ] W1.7 visibility: Spinner suffix `· <model>/<effort>`, `$.ui.status`,
|
||||
`$.ui.log` when verbose, `/route show`
|
||||
- [ ] W1.8 userConfig: `mainLoopModelSwitch` (false), `verbose` (false),
|
||||
`classifier` (false)
|
||||
- [ ] W1.9 tests `*.test.ts` under `claude plugin test`; `claude plugin validate`
|
||||
- [ ] W1.10 install: `mods/` symlink + `CLAUDE_CODE_PLUGIN_DIRS` in settings.json
|
||||
env via link.sh; doctor line; README/USAGE/CHANGELOG
|
||||
- [ ] W1.11 contract + GATE 0 + fresh verifier + security gate; `make test`
|
||||
|
||||
## Wave 2 — migration (after wave 1 proven)
|
||||
- [ ] W2.1 15 skills `Skill(effort-*)` → `route` tool calls (lib/effort-shift.md rewritten)
|
||||
- [ ] W2.2 remove `skills/effort-*`, `lib/effort-pins.txt`, `lib/effort-pins.sh`,
|
||||
install/update steps, `effort:` frontmatter on skills and agents
|
||||
- [ ] W2.3 `lib/model-gate.md` + `lib/model-check.sh` → mod rule (reflect on a
|
||||
small model → raise); census tests repointed to the config table
|
||||
- [ ] W2.4 docs + CHANGELOG + registries (BDR, LRN, EVAL via effort-audit.py)
|
||||
|
||||
## Wave 3 — optional
|
||||
- [ ] W3.1 per-step heuristics (Read/Grep → +1 level next step; Agent return → orchestrate)
|
||||
- [ ] W3.2 haiku classifier on `prompt.submit` (`$.model.classify`)
|
||||
- [ ] W3.3 quota-aware downgrade from `$.session.usage().rateLimits`
|
||||
- [ ] W3.4 A/B via `lib/effort-audit.py`
|
||||
|
||||
## Risks
|
||||
- Main-loop model switch mid-turn unproven (W0.3b decides).
|
||||
- A mod bug cuts all routing at once: fail-open (`.catch` → `next(e)`), never deny.
|
||||
- Two sources of truth during wave 1 (pins + mod): mod must agree with the
|
||||
pins until wave 2 removes them.
|
||||
- API early access: types change between releases; pin the CLI version in the README.
|
||||
Reference in New Issue
Block a user