forked from bchanot/claude
chore(tasks): model-router B1/B2 plans r2 after the 6-lens challenge round
This commit is contained in:
@@ -106,3 +106,67 @@ Optional seventh: the bridge context line names the floor when it wins.
|
||||
derived phase); supersedes the 1-A contract's one-slot precedence for
|
||||
prompt and slash sources.
|
||||
- LRN-206 (kit facts) applies to every new test.
|
||||
|
||||
## r2 — challenge round (3 lenses, 0 BLOCKER, 4 MAJOR): BINDING, overrides the sections above where they conflict
|
||||
R1. ONE decision helper, used by `mainPlan` AND by every answer text:
|
||||
`mainEffort(st, engine: StepIn['effort'])` → `{ effort, by }` with
|
||||
`by` ∈ `'floor' | 'sticky' | 'turn' | 'engine'`.
|
||||
`base = (st.userMain ?? st.turnMain)?.route.effort
|
||||
?? st.turnFloor?.route.effort ?? engine`
|
||||
`effort = floored(base, st.turnFloor?.route.effort)`; `by = 'floor'`
|
||||
when the floor raised or supplied the value, else the slot it came from.
|
||||
The user's level is therefore BOTH the turn's default (when no sticky
|
||||
or turn route names an effort) AND its minimum: a typed `/effort-low`
|
||||
lowers a turn that has no route (engine `high` → `low`), and a route
|
||||
can still go higher. No text function compares ranks on its own.
|
||||
R2. Model axis, one rule written once (contract updated):
|
||||
`st.userMain?.route.model ?? st.turnMain?.route.model ?? st.turnFloor?.route.model`,
|
||||
switch and window guard unchanged.
|
||||
R3. Prompt rule with `e.turnId !== undefined` (typed while a turn runs;
|
||||
`wait` is IGNORED: the engine queues every mid-turn prompt either way):
|
||||
write the floor NOW (higher of the existing floor and the new one)
|
||||
AND set `pendingPrompt` to it, so the turn that reads the prompt has it
|
||||
whichever it is. No `turnId` → write the floor (higher of two).
|
||||
`endMainTurn` promotes `pendingPrompt` into `turnFloor`. Two floors in
|
||||
one turn always keep the higher one (prompt rule and typed slash).
|
||||
R4. Truthful texts, all phrased from `mainEffort` (main branch only):
|
||||
- Skill bridge, route tool, typed `/effort-<l>`: when `by === 'floor'`
|
||||
and the result differs from what was asked, name the floor and its
|
||||
source (`ultrathink rule` or `typed /effort-<l>`) and add
|
||||
`/route clear to drop it`; when `by === 'sticky'`, the sticky
|
||||
sentence; the old fixed "sticky wins" sentences go.
|
||||
- `/effort-<l>` text: `Effort <l> set by model-router for the main loop
|
||||
this turn (minimum; a higher route still applies).` plus the floor
|
||||
or sticky outcome when one changes it.
|
||||
- main loop on a haiku model: print `effort - (haiku takes none)`.
|
||||
- model `route({clear})` on main with a floor set: append `; user floor
|
||||
<f> (<source>) still holds — /route clear drops it`.
|
||||
R5. Display: `mainText` / `statusLine` show ` · floor <f>` only when the
|
||||
floor carries an effort AND the router is on; the `skill.prompt` hook
|
||||
calls `refresh($, st)` after the slash write.
|
||||
R6. Persistent per-machine off switch (wiring challenge, user's "configurable"):
|
||||
config key `enabled: boolean` (default `true`) in
|
||||
`~/.claude/model-router.json` (untracked, per machine). `false` →
|
||||
`st.off = true` after every config load (session start, `/route
|
||||
reload`); `/route on` re-enables for the session only; `show` and the
|
||||
status line say `off (config)` vs `off`. Merged with `pickBool` like
|
||||
the other scalars; `DEFAULT_CONFIG.enabled = true`.
|
||||
R7. Tests (replace the list above where it differs): `runStep` takes a
|
||||
full `TurnStepInput` (from 'claude-code'); every floor test steps with
|
||||
engine effort `high` (or `xhigh`); every `test('…', async (` line ≤ 80
|
||||
chars with `floor` in the single-line name. At least 8 floor tests:
|
||||
ultrathink survives a model route · typed /effort-medium clamps low,
|
||||
lets max pass · typed /effort-low lowers an unrouted turn (engine high
|
||||
→ low) · survives a skill load · lifts a lower sticky then ends with
|
||||
the turn · main only (agent step unaffected) · /route clear removes it
|
||||
· mid-turn prompt (turnId + wait) is applied now AND promoted after the
|
||||
main turn.complete · mandatory text test: sticky `/route effort=low`,
|
||||
ultrathink, route tool `plan` → the answer names the floor.
|
||||
`enabled: false` cannot be reached in the kit (no fs, LRN-206): cover
|
||||
the off path through `/route off` and say so in a comment.
|
||||
R8. Residuals accepted (logged in TODO, not built): floor expiry depends on
|
||||
a main `turn.complete` reaching this mod (another plugin answering it
|
||||
without `next` would keep it); `skill.prompt` cannot tell a typed
|
||||
`/effort-<l>` from a sub-agent preload (no agentId; no repo agent
|
||||
preloads one); an incidental "ultrathink" in pasted text floors the
|
||||
turn (mitigated by R4 naming the source and the `/route clear` hint).
|
||||
|
||||
@@ -98,6 +98,66 @@ repo root `bash ~/.claude/lib/gates.sh run .claude/tasks/contracts/2026-10-08-mo
|
||||
skills-dir copy in that session; doctor reads `claude plugin list` from a
|
||||
fresh process, which sees only the skills-dir copy.
|
||||
|
||||
## r2 — challenge round (3 lenses, 0 BLOCKER, 5 MAJOR): BINDING, overrides the sections above where they conflict
|
||||
W1. ORDER: this plan runs AFTER the floor plan (B1) is committed and green
|
||||
on the same branch: the suite and doctor test whatever register.ts is
|
||||
on disk.
|
||||
W2. `.gitignore`: add ONLY `mods/*/tsconfig.json` with the comment
|
||||
`# mods/: the engine lays tsconfig.json beside a loaded mod; its
|
||||
.claude-plugin/types/ ignores itself`. (The types folder carries its
|
||||
own `.gitignore` holding `*`.)
|
||||
W3. Link step idempotent: `[ -L skills/model-router ] || ln -s
|
||||
../mods/model-router skills/model-router` (a bare `ln -s` re-run would
|
||||
create a nested link inside the mod).
|
||||
W4. `lib/tests/mods.test.sh` fail-soft and bounded:
|
||||
- capability probe, not presence: `command -v claude` AND `claude plugin
|
||||
test --help >/dev/null 2>&1`; otherwise ONE `SKIP: claude plugin test
|
||||
unavailable (<reason>) — validate/test not run` line, checks (1)-(2)
|
||||
still decide the exit code;
|
||||
- `claude plugin validate` and `claude plugin test` captured with `2>&1`;
|
||||
the validate verdict is the line matching `Validation passed`, with
|
||||
`warning` searched only in that captured output;
|
||||
- every CLI call bounded: `timeout 120` when available (coreutils /
|
||||
`gtimeout`), else a background-and-wait guard; a timeout is a FAIL
|
||||
naming it;
|
||||
- no mod found → FAIL (never vacuous).
|
||||
W5. doctor `── Mods ──` fail-soft under `set -euo pipefail`:
|
||||
- `[ -L "$link" ] || [ -e "$link" ]` BEFORE any readlink; compare with
|
||||
`[ "$link" -ef "$REPO/mods/<name>" ]` (handles logical vs physical
|
||||
repo paths), never string equality on `readlink -f`;
|
||||
- a missing link is `info "mod <name>: not linked (skills/<name> absent)
|
||||
— git checkout skills/<name> if wanted"`, NOT `fail` (a user may
|
||||
remove the link on purpose; doctor red forever would break
|
||||
update-all's final doctor run);
|
||||
- ONE `claude plugin list --json` call before the loop, inside
|
||||
`if ! out=$(claude plugin list --json 2>/dev/null); then warn "mods:
|
||||
claude plugin list failed — load state not checked"; out=""; fi`; the
|
||||
python3 parse reads stdin, exits 0 always, prints `enabled|disabled|
|
||||
absent|unknown` per name (any parse error → `unknown`);
|
||||
- wording: `pass "mod <name>: enabled as <name>@skills-dir"` (not
|
||||
"loaded": the list proves enablement, not a successful load);
|
||||
`disabled` → warn naming `"<name>@skills-dir": false`; `absent` →
|
||||
`warn "mod <name>: not listed as @skills-dir — run: claude plugin
|
||||
validate mods/<name> (policy, manifest or name conflict)"` (a fresh
|
||||
process rescans skills/, so a restart changes nothing); `unknown` →
|
||||
warn "list output not understood";
|
||||
- `claude` missing → nothing (doctor's Prerequisites section already
|
||||
fails on it); no override-file JSON check (the mod validates its own
|
||||
config and logs at session start).
|
||||
W6. CLAUDE.md `## mods/` also says: the only per-machine off switch is
|
||||
`"enabled": false` in `~/.claude/<name>.json` (untracked); an
|
||||
`enabledPlugins` `"<name>@skills-dir": false` entry works too but lands
|
||||
in the TRACKED settings.json, so it dirties every machine's tree; and
|
||||
that a hot-reload / `--plugin-dir` copy of the same name shadows the
|
||||
skills-dir copy for that session (docs plugins/loading "Name
|
||||
conflicts"), so the dev link in `~/.claude/dev-mods/<session>/` must
|
||||
be removed before `/reload-plugins` is read as a test of the skills-dir
|
||||
path.
|
||||
W7. `update-all.sh` runs `claude plugin update` over every listed plugin
|
||||
(lines ~606-618): a `@skills-dir` entry will produce one recurring
|
||||
warn there. Accepted residual, logged in TODO (an update-all edit is
|
||||
out of this contract's FILE SCOPE).
|
||||
|
||||
## Disposition
|
||||
- honors BDR-115 (mod in `mods/`, single source); amends its "Load:" line
|
||||
(PLUGIN_DIRS → skills-dir link), to be recorded at capitalize.
|
||||
|
||||
Reference in New Issue
Block a user