chore(tasks): model-router B1/B2 plans r2 after the 6-lens challenge round
This commit is contained in:
@@ -10,24 +10,27 @@ User, same turn: "continu avec Opus en /ultrathink".
|
|||||||
|
|
||||||
## CLARIFICATIONS
|
## CLARIFICATIONS
|
||||||
Q: which loops does the floor cover? / A: the MAIN loop only ("le tour" = the user's turn); sub-agents keep their own routes and pins. [orchestrator — derived, stated to the user]
|
Q: which loops does the floor cover? / A: the MAIN loop only ("le tour" = the user's turn); sub-agents keep their own routes and pins. [orchestrator — derived, stated to the user]
|
||||||
Q: model axis / A: the floor constrains effort only. A floor route's model (none by default: ultrathink → escalate carries none) applies on main only when neither the sticky nor the turn route names a model, and only with the switch on. [orchestrator — internal]
|
Q: model axis / A: one rule: `userMain?.route.model ?? turnMain?.route.model ?? turnFloor?.route.model`, applied only with the switch on (r2). [orchestrator — internal]
|
||||||
|
Q (r2): default vs minimum / A: the user's level is the turn's default when no sticky or turn route names an effort AND its minimum; a typed `/effort-low` therefore still lowers an unrouted turn. Derived from the chosen option ("Ton niveau est un minimum pour tout le tour") plus the challenge finding that a pure floor would make `/effort-low` a no-op. [orchestrator — r2]
|
||||||
|
Q (r2): mid-turn prompt / A: applied to the running turn AND kept for the next (`wait` ignored, the engine queues either way). [orchestrator — r2]
|
||||||
|
Q (r2): per-machine off switch / A: `"enabled": false` in `~/.claude/model-router.json` (untracked); an `enabledPlugins` entry would dirty the tracked settings.json on every machine. [orchestrator — r2, from the user's "configurable"]
|
||||||
Q: numeric or absent engine effort / A: the floor level replaces it (the user's explicit level wins over an unknown budget); the haiku effort omission still applies after flooring. [orchestrator — internal]
|
Q: numeric or absent engine effort / A: the floor level replaces it (the user's explicit level wins over an unknown budget); the haiku effort omission still applies after flooring. [orchestrator — internal]
|
||||||
Q: who clears the floor / A: main turn end (a queued prompt's floor is then promoted), `/route clear`, `/route off` (pass-through). A model `route({clear})`, a `Skill(effort-*)` or any skill load never touches it. [orchestrator — derived from "jamais descendre en dessous"]
|
Q: who clears the floor / A: main turn end (a queued prompt's floor is then promoted), `/route clear`, `/route off` (pass-through). A model `route({clear})`, a `Skill(effort-*)` or any skill load never touches it. [orchestrator — derived from "jamais descendre en dessous"]
|
||||||
|
|
||||||
## ACCEPTANCE CRITERIA
|
## ACCEPTANCE CRITERIA
|
||||||
1. Suite green with the new tests: `claude plugin test` passes with at least 19 `test(` calls; `claude plugin validate` passes with no warning; no line over 80 chars; no `any` type.
|
1. Suite green with the new tests: `claude plugin test` passes with at least 22 `test(` calls; `claude plugin validate` passes with no warning; no line over 80 chars; no `any` type.
|
||||||
CHECK: cd mods/model-router && out=$(claude plugin test . 2>&1); rc=$?; echo "$out" | tail -n 3; [ $rc -eq 0 ] && [ "$(grep -cE '^\s*test\(' hooks/register.test.ts)" -ge 19 ] && v=$(claude plugin validate . 2>&1) && echo "$v" | grep -q 'Validation passed' && ! echo "$v" | grep -qi 'warning' && ! grep -nE '.{81,}' hooks/register.ts hooks/register.test.ts && ! grep -nE ':\s*any\b|<any>|as any\b' hooks/register.ts && echo FLOOR-SUITE-OK
|
CHECK: cd mods/model-router && out=$(claude plugin test . 2>&1); rc=$?; echo "$out" | tail -n 3; [ $rc -eq 0 ] && [ "$(grep -cE '^\s*test\(' hooks/register.test.ts)" -ge 22 ] && v=$(claude plugin validate . 2>&1) && echo "$v" | grep -q 'Validation passed' && ! echo "$v" | grep -qi 'warning' && ! grep -nE '.{81,}' hooks/register.ts hooks/register.test.ts && ! grep -nE ':\s*any\b|<any>|as any\b' hooks/register.ts && echo FLOOR-SUITE-OK
|
||||||
EXPECT: FLOOR-SUITE-OK
|
EXPECT: FLOOR-SUITE-OK
|
||||||
EVIDENCE: pending
|
EVIDENCE: pending
|
||||||
2. Type-check clean against this build's declarations.
|
2. Type-check clean against this build's declarations.
|
||||||
CHECK: T=/Users/b.chanot/.claude/dev-mods/385f7190-70f5-4bdd-b0d8-e4566cd412fd/model-router/.claude-plugin/types; [ -d "$T" ] || T=/Users/b.chanot/Documents/claude/mods/model-router/.claude-plugin/types; W=$(mktemp -d) && printf '{"compilerOptions":{"target":"es2023","lib":["es2023"],"types":[],"module":"esnext","moduleResolution":"bundler","strict":true,"noUncheckedIndexedAccess":true,"noEmit":true,"skipLibCheck":true,"jsx":"react","jsxFactory":"h","jsxFragmentFactory":"Fragment"},"include":["%s/claude-code/index.d.ts","%s/claude-code-tools/index.d.ts","%s/hooks"]}' "$T" "$T" "$PWD/mods/model-router" > "$W/tsconfig.json" && (cd "$W" && npx --yes -p typescript@5 tsc -p tsconfig.json) && echo TSC-OK
|
CHECK: T=/Users/b.chanot/.claude/dev-mods/385f7190-70f5-4bdd-b0d8-e4566cd412fd/model-router/.claude-plugin/types; [ -d "$T" ] || T=/Users/b.chanot/Documents/claude/mods/model-router/.claude-plugin/types; W=$(mktemp -d) && printf '{"compilerOptions":{"target":"es2023","lib":["es2023"],"types":[],"module":"esnext","moduleResolution":"bundler","strict":true,"noUncheckedIndexedAccess":true,"noEmit":true,"skipLibCheck":true,"jsx":"react","jsxFactory":"h","jsxFragmentFactory":"Fragment"},"include":["%s/claude-code/index.d.ts","%s/claude-code-tools/index.d.ts","%s/hooks"]}' "$T" "$T" "$PWD/mods/model-router" > "$W/tsconfig.json" && (cd "$W" && npx --yes -p typescript@5 tsc -p tsconfig.json) && echo TSC-OK
|
||||||
EXPECT: TSC-OK
|
EXPECT: TSC-OK
|
||||||
EVIDENCE: pending
|
EVIDENCE: pending
|
||||||
3. The floor is its own slot: `turnFloor` is declared in `State`, initialised in `newState`, written by the prompt rule and by a typed `/effort-<l>`, cleared by `/route clear` and at main turn end; the suite carries at least 6 tests whose name contains `floor`.
|
3. The floor is its own slot: `turnFloor` is declared in `State`, initialised in `newState`, written by the prompt rule and by a typed `/effort-<l>`, cleared by `/route clear` and at main turn end; the suite carries at least 8 tests whose name contains `floor`; one helper `mainEffort` decides the main effort; the config key `enabled` exists.
|
||||||
CHECK: cd mods/model-router/hooks && [ "$(grep -c 'turnFloor' register.ts)" -ge 6 ] && [ "$(grep -cE "^\s*test\('[^']*floor" register.test.ts)" -ge 6 ] && echo FLOOR-SLOT-OK
|
CHECK: cd mods/model-router/hooks && [ "$(grep -c 'turnFloor' register.ts)" -ge 6 ] && [ "$(grep -cE "^\s*test\('[^']*floor" register.test.ts)" -ge 8 ] && grep -q 'mainEffort' register.ts && grep -q 'enabled' register.ts && echo FLOOR-SLOT-OK
|
||||||
EXPECT: FLOOR-SLOT-OK
|
EXPECT: FLOOR-SLOT-OK
|
||||||
EVIDENCE: pending
|
EVIDENCE: pending
|
||||||
4. Judged by reading: effective main effort = the higher (LEVELS order) of the floor and `(userMain ?? turnMain)?.route.effort ?? e.effort`, the floor replacing a numeric or absent engine effort; the floor never applies to a sub-agent step; `turnMain` only ever holds 'model' or 'skill' sources; the prompt rule writes `turnFloor` (or `pendingPrompt` when typed mid-turn with `wait`), promoted into `turnFloor` at main turn end; a non-effort skill load resets `turnMain` only; a model `route({clear})` clears `turnMain` only; every answer that the floor overrides says so truthfully (Skill bridge context, route tool text, `/effort-<l>` text); `/route show` and the status line display the floor; every criterion of `.claude/tasks/contracts/2026-10-08-model-router-w1a-1533.md` still holds; no function over 25 logic lines.
|
4. Judged by reading: effective main effort comes from ONE helper `mainEffort` used by `mainPlan` and by every answer text: base `(userMain ?? turnMain)?.route.effort ?? turnFloor?.route.effort ?? e.effort`, then floored by `turnFloor` (LEVELS order; a numeric or absent value is replaced); the floor never applies to a sub-agent step; `turnMain` only ever holds 'model' or 'skill' sources; the prompt rule writes `turnFloor` (keeping the higher of two) and, when typed mid-turn (`turnId` set, `wait` ignored), also `pendingPrompt`, promoted into `turnFloor` at main turn end; `"enabled": false` in the override file makes every hook pass through after each config load (`/route on` re-enables for the session); a non-effort skill load resets `turnMain` only; a model `route({clear})` clears `turnMain` only; every answer that the floor overrides says so truthfully (Skill bridge context, route tool text, `/effort-<l>` text); `/route show` and the status line display the floor; every criterion of `.claude/tasks/contracts/2026-10-08-model-router-w1a-1533.md` still holds; no function over 25 logic lines.
|
||||||
|
|
||||||
## FILE SCOPE
|
## FILE SCOPE
|
||||||
mods/model-router/hooks/register.ts · mods/model-router/hooks/register.test.ts
|
mods/model-router/hooks/register.ts · mods/model-router/hooks/register.test.ts
|
||||||
|
|||||||
@@ -19,18 +19,22 @@ Q: doctor scope / A: per mod: the loading link resolves into the repo mod dir; `
|
|||||||
CHECK: [ -L skills/model-router ] && [ "$(readlink skills/model-router)" = "../mods/model-router" ] && ! git check-ignore -q skills/model-router && echo LINK-OK
|
CHECK: [ -L skills/model-router ] && [ "$(readlink skills/model-router)" = "../mods/model-router" ] && ! git check-ignore -q skills/model-router && echo LINK-OK
|
||||||
EXPECT: LINK-OK
|
EXPECT: LINK-OK
|
||||||
EVIDENCE: pending
|
EVIDENCE: pending
|
||||||
2. Engine-laid files are ignored: a mod's root `tsconfig.json` and anything under its `.claude-plugin/types/`; the tracked mod files are not ignored.
|
2. Engine-laid files are ignored for ANY mod: a mod's root `tsconfig.json` (root `.gitignore`; the `.claude-plugin/types/` folder ignores itself); the tracked mod files are not ignored.
|
||||||
CHECK: git check-ignore -q mods/model-router/tsconfig.json && git check-ignore -q mods/model-router/.claude-plugin/types/claude-code/index.d.ts && ! git check-ignore -q mods/model-router/hooks/register.ts && ! git check-ignore -q mods/model-router/.claude-plugin/plugin.json && echo IGNORE-OK
|
CHECK: git check-ignore -q mods/model-router/tsconfig.json && git check-ignore -q mods/zz-future/tsconfig.json && ! git check-ignore -q mods/model-router/hooks/register.ts && ! git check-ignore -q mods/model-router/.claude-plugin/plugin.json && [ -z "$(git status --short mods/)" ] && echo IGNORE-OK
|
||||||
EXPECT: IGNORE-OK
|
EXPECT: IGNORE-OK
|
||||||
EVIDENCE: pending
|
EVIDENCE: pending
|
||||||
3. `lib/tests/mods.test.sh` passes on the repo and fails on a fixture that lacks the loading link (positive control through `MODS_ROOT`).
|
3. `lib/tests/mods.test.sh` passes on the repo, fails on a fixture that lacks the loading link, fails on an empty `mods/`, and SKIPs (exit 0) the CLI checks when `claude plugin test` is unavailable (probe by capability, PATH-shadowed `claude` in the control).
|
||||||
CHECK: make test suite=lib/tests/mods.test.sh >/dev/null 2>&1 && W=$(mktemp -d) && mkdir -p "$W/mods" "$W/skills" && cp -R mods/model-router "$W/mods/" && ! MODS_ROOT="$W" bash lib/tests/mods.test.sh >/dev/null 2>&1 && ln -s ../mods/model-router "$W/skills/model-router" && MODS_ROOT="$W" bash lib/tests/mods.test.sh >/dev/null 2>&1 && echo MODS-SUITE-OK
|
CHECK: make test suite=lib/tests/mods.test.sh >/dev/null 2>&1 && W=$(mktemp -d) && mkdir -p "$W/mods" "$W/skills" "$W/bin" && cp -R mods/model-router "$W/mods/" && ! MODS_ROOT="$W" bash lib/tests/mods.test.sh >/dev/null 2>&1 && ln -s ../mods/model-router "$W/skills/model-router" && MODS_ROOT="$W" bash lib/tests/mods.test.sh >/dev/null 2>&1 && printf '#!/bin/sh\nexit 1\n' > "$W/bin/claude" && chmod +x "$W/bin/claude" && PATH="$W/bin:$PATH" MODS_ROOT="$W" bash lib/tests/mods.test.sh 2>&1 | grep -q '^SKIP' && E=$(mktemp -d) && mkdir -p "$E/mods" "$E/skills" && ! MODS_ROOT="$E" bash lib/tests/mods.test.sh >/dev/null 2>&1 && echo MODS-SUITE-OK
|
||||||
EXPECT: MODS-SUITE-OK
|
EXPECT: MODS-SUITE-OK
|
||||||
EVIDENCE: pending
|
EVIDENCE: pending
|
||||||
4. `doctor.sh` prints a `── Mods ──` section with a ✓ line for the model-router loading link.
|
4. `doctor.sh` prints a `── Mods ──` section with a ✓ line for model-router; with the link absent (HOME pointed at a scratch `.claude` whose `skills/` lacks the link) the section prints an info line, doctor reaches its summary and exits 0 for that section's sake (no new error).
|
||||||
CHECK: out=$(bash doctor.sh 2>&1); echo "$out" | sed -n '/── Mods ──/,/^$/p' | grep -q '✓.*model-router' && echo DOCTOR-MODS-OK
|
CHECK: out=$(bash doctor.sh 2>&1); echo "$out" | sed -n '/── Mods ──/,/^$/p' | grep -q '✓.*model-router' && H=$(mktemp -d) && mkdir -p "$H/.claude/skills" && o2=$(HOME="$H" bash doctor.sh 2>&1); echo "$o2" | sed -n '/── Mods ──/,/^$/p' | grep -qi 'not linked' && echo "$o2" | grep -q '═══' && echo DOCTOR-MODS-OK
|
||||||
EXPECT: DOCTOR-MODS-OK
|
EXPECT: DOCTOR-MODS-OK
|
||||||
EVIDENCE: pending
|
EVIDENCE: pending
|
||||||
|
4b. The mod is enabled through the tracked link in a FRESH process: `claude plugin list --json` lists `model-router@skills-dir` with `enabled: true` (run after the dev-mods link is removed, see W6).
|
||||||
|
CHECK: claude plugin list --json 2>/dev/null | python3 -c 'import json,sys; rows=json.load(sys.stdin); ok=any(r.get("id")=="model-router@skills-dir" and r.get("enabled") is True for r in rows); sys.exit(0 if ok else 1)' && echo LOADED-OK
|
||||||
|
EXPECT: LOADED-OK
|
||||||
|
EVIDENCE: pending
|
||||||
5. `CLAUDE.md` has a `## mods/` section naming the `skills/<name>` relative symlink, the `@skills-dir` origin, why not `CLAUDE_CODE_PLUGIN_DIRS`, the gitignored engine-laid files, `~/.claude/<name>.json`, the suite command and how to turn a mod off.
|
5. `CLAUDE.md` has a `## mods/` section naming the `skills/<name>` relative symlink, the `@skills-dir` origin, why not `CLAUDE_CODE_PLUGIN_DIRS`, the gitignored engine-laid files, `~/.claude/<name>.json`, the suite command and how to turn a mod off.
|
||||||
CHECK: grep -q '^## mods/' CLAUDE.md && grep -q '@skills-dir' CLAUDE.md && grep -q 'CLAUDE_CODE_PLUGIN_DIRS' CLAUDE.md && grep -q 'mods.test.sh' CLAUDE.md && grep -q '<name>.json' CLAUDE.md && grep -q '@skills-dir": false' CLAUDE.md && echo CLAUDEMD-OK
|
CHECK: grep -q '^## mods/' CLAUDE.md && grep -q '@skills-dir' CLAUDE.md && grep -q 'CLAUDE_CODE_PLUGIN_DIRS' CLAUDE.md && grep -q 'mods.test.sh' CLAUDE.md && grep -q '<name>.json' CLAUDE.md && grep -q '@skills-dir": false' CLAUDE.md && echo CLAUDEMD-OK
|
||||||
EXPECT: CLAUDEMD-OK
|
EXPECT: CLAUDEMD-OK
|
||||||
@@ -39,7 +43,9 @@ Q: doctor scope / A: per mod: the loading link resolves into the repo mod dir; `
|
|||||||
CHECK: shellcheck lib/tests/mods.test.sh doctor.sh && make test suite=lib/tests/doctrine-citers.test.sh >/dev/null 2>&1 && echo HEALTH-OK
|
CHECK: shellcheck lib/tests/mods.test.sh doctor.sh && make test suite=lib/tests/doctrine-citers.test.sh >/dev/null 2>&1 && echo HEALTH-OK
|
||||||
EXPECT: HEALTH-OK
|
EXPECT: HEALTH-OK
|
||||||
EVIDENCE: pending
|
EVIDENCE: pending
|
||||||
7. Judged by reading: no change to settings.json, link.sh or any install script; the user's settings.json working-tree diff is untouched; the suite SKIPs (explicit SKIP line, exit 0 for that part) only the `claude`-dependent checks when `claude` is absent, and fails when no mod is found at all; doctor's new section never increments the core-link counter (`_LINK_PASS`) and never fails on a missing `claude`; the CLAUDE.md section is terse English matching the file's style.
|
7. Judged by reading: no change to settings.json, link.sh or any install script; the suite probes the CAPABILITY (`claude plugin test --help`), bounds every CLI call in time, captures `2>&1`, SKIPs with a reason, fails when no mod is found; doctor's section is fail-soft under `set -euo pipefail` (existence test before readlink, `-ef` comparison, one guarded `claude plugin list --json`, python exits 0 with `unknown` on any parse error), never increments `_LINK_PASS`, says "enabled" not "loaded", treats a missing link as info; the link step is idempotent; CLAUDE.md names the per-machine `"enabled": false` switch, the tracked-settings cost of `enabledPlugins`, and the dev-copy shadowing rule; the CLAUDE.md section is terse English matching the file's style.
|
||||||
|
Q (r2): ordering / A: this contract runs after the floor contract (`2026-10-08-model-router-floor-1835`) is committed and green. [orchestrator]
|
||||||
|
Q (r2): update-all `claude plugin update` over `@skills-dir` / A: accepted residual (one recurring warn), logged in TODO; out of FILE SCOPE. [orchestrator]
|
||||||
|
|
||||||
## FILE SCOPE
|
## FILE SCOPE
|
||||||
skills/model-router (new symlink) · .gitignore · lib/tests/mods.test.sh (new) · doctor.sh · CLAUDE.md
|
skills/model-router (new symlink) · .gitignore · lib/tests/mods.test.sh (new) · doctor.sh · CLAUDE.md
|
||||||
|
|||||||
@@ -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
|
derived phase); supersedes the 1-A contract's one-slot precedence for
|
||||||
prompt and slash sources.
|
prompt and slash sources.
|
||||||
- LRN-206 (kit facts) applies to every new test.
|
- 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
|
skills-dir copy in that session; doctor reads `claude plugin list` from a
|
||||||
fresh process, which sees only the skills-dir copy.
|
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
|
## Disposition
|
||||||
- honors BDR-115 (mod in `mods/`, single source); amends its "Load:" line
|
- honors BDR-115 (mod in `mods/`, single source); amends its "Load:" line
|
||||||
(PLUGIN_DIRS → skills-dir link), to be recorded at capitalize.
|
(PLUGIN_DIRS → skills-dir link), to be recorded at capitalize.
|
||||||
|
|||||||
Reference in New Issue
Block a user