Merge feature/model-router-w4 into develop
This commit is contained in:
@@ -1413,4 +1413,9 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
|
|||||||
- **Refs**: contract `.claude/tasks/contracts/2026-10-10-model-router-w3a-confirm-1201.md`, plan `.claude/tasks/plans/2026-10-10-model-router-w3a-confirm-1201.md`, commit 22455c0, [[BDR-115]], [[LRN-210]], [[LRN-211]], [[EVAL-043]].
|
- **Refs**: contract `.claude/tasks/contracts/2026-10-10-model-router-w3a-confirm-1201.md`, plan `.claude/tasks/plans/2026-10-10-model-router-w3a-confirm-1201.md`, commit 22455c0, [[BDR-115]], [[LRN-210]], [[LRN-211]], [[EVAL-043]].
|
||||||
- **Amendment (2026-10-11, wave 3-B, commit 604a6c4)**: clause (2) superseded: the dialog carries CONTEXT (skill description = first sentence of its SKILL.md frontmatter, five YAML forms, hardened read: name allowlist, stat kind file, size cap, `clean()` Cc/Cf + code points; agent description from `agent.offer`, bounded cache; phase `about` line, new 120-char field on the 11 routing.json phases, kept in memory not on Route) and the REAL next model id + effort. Options Later / Keep / Change (T3 Later / Keep). Change = model (4 fixed alias labels with tier role) → effort among those the phases headed by that alias offer (ascending, ≤ 4, one → skipped, zero → Later) → scope; the pair maps to an existing PHASE (rows stay phase names; row's current phase wins a tie; same phase = Keep); a pair no phase offers = a new phase by hand. Other anywhere = Later + toast (no echo). A main-row change toasts the real decision + `/route switch on` hint on a held downgrade. Three lenses rejected inline-route rows and dialog phase edits (2 BLOCKERs). First real use: user moved security-auditor to `judge` (opus/xhigh) through it; floor aligned (`model: opus`, 2026-10-11). Links [[EVAL-044]].
|
- **Amendment (2026-10-11, wave 3-B, commit 604a6c4)**: clause (2) superseded: the dialog carries CONTEXT (skill description = first sentence of its SKILL.md frontmatter, five YAML forms, hardened read: name allowlist, stat kind file, size cap, `clean()` Cc/Cf + code points; agent description from `agent.offer`, bounded cache; phase `about` line, new 120-char field on the 11 routing.json phases, kept in memory not on Route) and the REAL next model id + effort. Options Later / Keep / Change (T3 Later / Keep). Change = model (4 fixed alias labels with tier role) → effort among those the phases headed by that alias offer (ascending, ≤ 4, one → skipped, zero → Later) → scope; the pair maps to an existing PHASE (rows stay phase names; row's current phase wins a tie; same phase = Keep); a pair no phase offers = a new phase by hand. Other anywhere = Later + toast (no echo). A main-row change toasts the real decision + `/route switch on` hint on a held downgrade. Three lenses rejected inline-route rows and dialog phase edits (2 BLOCKERs). First real use: user moved security-auditor to `judge` (opus/xhigh) through it; floor aligned (`model: opus`, 2026-10-11). Links [[EVAL-044]].
|
||||||
- **Amendment (2026-10-11, wave 3-C, commit cd8d72f)**: clause (6) extended: `/route forget <name|all|projects>` (composer only) clears decisions in the tracked file through the same writer: a name in every table (`confirmed`, `changed` → row restored to its recorded `from` when it is a string, the row still equals `to` and `from` is a known phase; `projects[*]` pruned), asked again at once; `all`/`projects` confirm through the engine dialog (Cancel first, anything else = cancelled) and hold the single-dialog slot; nothing written when nothing to forget; the answer names a restored row's file + values to realign (a floor aligned to the decision makes the census FAIL after the restore). No per-user decisions file (user: "seulement le forget"): another user inherits the shared memory and may forget. Residuals (security LOW/MEDIUM): raw error text in the answer, file fragments unsanitized in the answer, `floorFile` name not allowlisted, `__proto__` plan/apply mismatch, `changed.phases` restorable. Links [[EVAL-044]].
|
- **Amendment (2026-10-11, wave 3-C, commit cd8d72f)**: clause (6) extended: `/route forget <name|all|projects>` (composer only) clears decisions in the tracked file through the same writer: a name in every table (`confirmed`, `changed` → row restored to its recorded `from` when it is a string, the row still equals `to` and `from` is a known phase; `projects[*]` pruned), asked again at once; `all`/`projects` confirm through the engine dialog (Cancel first, anything else = cancelled) and hold the single-dialog slot; nothing written when nothing to forget; the answer names a restored row's file + values to realign (a floor aligned to the decision makes the census FAIL after the restore). No per-user decisions file (user: "seulement le forget"): another user inherits the shared memory and may forget. Residuals (security LOW/MEDIUM): raw error text in the answer, file fragments unsanitized in the answer, `floorFile` name not allowlisted, `__proto__` plan/apply mismatch, `changed.phases` restorable. Links [[EVAL-044]].
|
||||||
|
- **Amendment (2026-10-11, wave 4-A, commit 68d7ea6)**: logging rule: `verbose` (boolean) logs TRANSITIONS only, built from the decision itself inside `safely` before `next`: main "fable/high → fable/xhigh (<phase>: <cause>[, <model cause>])" once per turn per transition (causes: skill /<name>, route tool, prompt rule, prompt floor, dispatch, sticky /route, session; model: upgrade, switch, fallback: <alias> down); one hand-off line per agent dispatch deduped per turn per (type, target) "main … → agent <type> <alias>/<effort> (row <phase>|explicit|fallback|frontmatter, row|explicit|engine)"; later agent effort moves; an answered-by anomaly (agents only, canonical ids, once per agent+model). The host prefixes the plugin name: new lines carry none. The four stream lines are gone. Residuals W3-A/B/C closed: failure texts to the log, file fragments cleaned at render, floorFile allowlisted, `__proto__` refused, changed scan = rows, dialog Change offers shipped phases only (`mem.filePhases` = validated seed layer), post-answer write failure keeps the key asked. Rejected: a `trace` level (unrequested surface); detecting agent moves at turn.step (decided at spawn). Links [[EVAL-044]].
|
||||||
|
- **Amendment (2026-10-11, wave 4-B, commit e54a2b5)**: prompt classifier closes the main-loop gap (plain prompt, no skill, no agent, no route in force). Composer `prompt.submit`, trimmed text ≥ 12 chars not starting with `/`, no prompt rule matched, no sticky/turn/run/floor route → one `$.model.classify(sample, [design, analysis, coding, bookkeeping, chat])`, sample = whole text ≤ 2000 code points else head 500 + tail 1500, no `model` option (engine small model). Label → phase (design→plan, analysis→reflect, coding→implement, bookkeeping→apply, chat/unknown → nothing) written as the TURN route, EFFORT ONLY (no tier: main model never moves on a guess), may lower, replaced by any later route tool call or typed skill, ignored at a step whose engine effort ≠ session baseline (`engineBase`: a user `/effort` beats the guess). Bounded: race with `$.clock.sleep(3000)`; timeout/rejection count, user abort neither counts nor writes, every guard re-checked after the await (a route landing meanwhile wins); 3 failures in a row → pause `cooldownMinutes` (×60000, kept across `/clear`, cleared by `/route reload`, one toast + status marker ` · classifier: paused`); skipped while the haiku id is marked down or main runs on haiku. No dialog, no routing.json write on a classified route. Config key `classify` (default true, `pickSwitch`), `/route classify on|off` session toggle surviving a rebuild, `/route show` `classify:` field, status `route: classifier <phase>`, verbose line `(<phase>: classifier)`.
|
||||||
|
- **Why**: user 2026-10-11: routing must also cover the main conversation when no skill or agent is invoked. Effort-only because a model flip on a 3-s guess would fight the breaker and the user's `/model`; turn-scoped because a guess must never outlive its prompt.
|
||||||
|
- **Alternatives rejected**: first-use dialog on classified phases (the TODO line said so; a dialog per plain prompt is noise and the phase is already confirmed through T3); writing the guess into routing.json; classifying mid-turn or sub-agent prompts; unbounded await (robustness BLOCKER r1: the classify call sat on the critical path with no clock race); `classify` as a tracked-file writer (session toggle + config key instead).
|
||||||
|
- **Gates**: plan r1 → r3 (simplicity CONCERNS(4), robustness FATAL(6) BLOCKER unbounded await, correctness CONCERNS(4); confirmation robustness CONCERNS(4)); feater DONE (423) + 1 round (trim, argumentHint, repeated undefined: 428, each mutation-proven); GATE 0 MET; verifier ECARTS(2) coverage → CONFORME on #2; security PASS (0 findings; LOW accepted: pasted text can steer one turn's effort up, README says so). Abort paths judged by reading (`[gated]` clarification). Residual: `smallModelBusy` checks `cfg.models.haiku` while classify uses the engine's default small model (mismatch only when the two differ). Live AC9 open.
|
||||||
|
- **Refs**: contract `.claude/tasks/contracts/2026-10-11-model-router-w4b-classifier-1858.md`, plan r3 same slug. Links [[EVAL-045]], [[LRN-210]].
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ rules:
|
|||||||
| EVAL-042 | 2026-10-10 | model-router W2 plan r1 → r4: 3 lenses (1 BLOCKER), 2 confirmations (1 BLOCKER then 0); W2-A verifier 3× ECARTS on coverage clauses only, W2-B ECARTS(7) → CONFORME; gate A→B read from engine records | second confirmation paid again (BLOCKER on my own r2 slot); compound coverage criterion = endless ECARTS; engine jsonl replaces the live log |
|
| EVAL-042 | 2026-10-10 | model-router W2 plan r1 → r4: 3 lenses (1 BLOCKER), 2 confirmations (1 BLOCKER then 0); W2-A verifier 3× ECARTS on coverage clauses only, W2-B ECARTS(7) → CONFORME; gate A→B read from engine records | second confirmation paid again (BLOCKER on my own r2 slot); compound coverage criterion = endless ECARTS; engine jsonl replaces the live log |
|
||||||
| EVAL-043 | 2026-10-11 | model-router W3-A: 3 lenses + 1 confirmation (census BLOCKER, project-tree layer dropped), feater DONE + 4 rounds, verifier 3× then re-verify after a REAL security BLOCK (credential fragment in the tracked key) | challenge + security gates both earned their cost; coverage-shaped criteria still cost 3 verifier rounds; live mod side effects misread as a test leak |
|
| EVAL-043 | 2026-10-11 | model-router W3-A: 3 lenses + 1 confirmation (census BLOCKER, project-tree layer dropped), feater DONE + 4 rounds, verifier 3× then re-verify after a REAL security BLOCK (credential fragment in the tracked key) | challenge + security gates both earned their cost; coverage-shaped criteria still cost 3 verifier rounds; live mod side effects misread as a test leak |
|
||||||
| EVAL-044 | 2026-10-11 | model-router W3-B (dialog with context): 3 lenses converged on 2 BLOCKERs of my r1 (inline rows, phase edits) + 1 confirmation; feater DONE + 2 coverage rounds; verifier 3× to CONFORME; security PASS; user's first real decision through the new dialog | the lenses pay most when the plan adds a data shape; coverage criteria still cost rounds; live dialogs during the gates must be announced and their data reconciled before commit |
|
| EVAL-044 | 2026-10-11 | model-router W3-B (dialog with context): 3 lenses converged on 2 BLOCKERs of my r1 (inline rows, phase edits) + 1 confirmation; feater DONE + 2 coverage rounds; verifier 3× to CONFORME; security PASS; user's first real decision through the new dialog | the lenses pay most when the plan adds a data shape; coverage criteria still cost rounds; live dialogs during the gates must be announced and their data reconciled before commit |
|
||||||
|
| EVAL-045 | 2026-10-11 | model-router W4-B (prompt classifier): 3 lenses (BLOCKER: unbounded classify await) + 1 confirmation; feater DONE + 1 round; verifier ECARTS(2) → CONFORME; security PASS 0 findings; LRN-210 applied (one coverage clause per criterion) | first wave since W2 with no verifier cap: per-clause criteria + abort paths declared unverifiable up front cut the rounds to one |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -411,3 +412,9 @@ Dogfood: 3 blind lenses attacked the v1 plan for the plan-challenge feature itse
|
|||||||
- **Anomaly**: my r1 proposed a second row shape to satisfy "model then effort" literally; the derived-effort list satisfies it with zero new data shapes. During the gates the user answered the NEW dialog live: security-auditor → opus/xhigh (the request's example), plan-challenger → verify (reset on user go); my reset of `changed` wiped a legitimate entry and left the committed file census-red for one commit.
|
- **Anomaly**: my r1 proposed a second row shape to satisfy "model then effort" literally; the derived-effort list satisfies it with zero new data shapes. During the gates the user answered the NEW dialog live: security-auditor → opus/xhigh (the request's example), plan-challenger → verify (reset on user go); my reset of `changed` wiped a legitimate entry and left the committed file census-red for one commit.
|
||||||
- **Action**: when a request says "choose X then Y", look for the existing shape that already encodes (X, Y) before adding one; reconcile routing.json decisions one by one (never `changed = {}`), and run the census before every data commit. Links [[EVAL-043]], [[BDR-116]], [[LRN-212]].
|
- **Action**: when a request says "choose X then Y", look for the existing shape that already encodes (X, Y) before adding one; reconcile routing.json decisions one by one (never `changed = {}`), and run the census before every data commit. Links [[EVAL-043]], [[BDR-116]], [[LRN-212]].
|
||||||
|
|
||||||
|
## EVAL-045 — model-router W4-B: per-clause criteria and a declared-unverifiable path ended the verifier loop in one round
|
||||||
|
- **Date**: 2026-10-11
|
||||||
|
- **Output checked**: plan `.claude/tasks/plans/2026-10-11-model-router-w4b-classifier-1858.md` r1 → r3; diff e54a2b5 (register.ts +211, 428 kit tests, 4 docs).
|
||||||
|
- **Method**: simplicity CONCERNS(4), robustness FATAL(6) (BLOCKER: the classify await sat unbounded on the prompt's critical path → `$.clock.sleep` race, abort not counted, post-await re-check), correctness CONCERNS(4) → r2; confirmation robustness CONCERNS(4) → r3. Feater DONE (423, 5 mutation proofs); GATE 0 MET; verifier #1 ECARTS(2) (no test on `trim`, none on the argumentHint) → feater (+5 tests, each failing under its mutation; first trim tests passed without the trim because the length guard caught them, lengthened) → verifier #2 CONFORME 8/8, criterion 9 gated. Security PASS, 0 findings, 1 LOW accepted.
|
||||||
|
- **Anomaly**: none on the loop. Verifier observations worth keeping: the sub-agent test proves only spawn+step never calls classify (`prompt.submit` has no agent form in the API types); the 3-s tests pin the upper bound only; the post-await re-check is tested through the route-landing case alone. Security note: `smallModelBusy` guards `cfg.models.haiku`, classify uses the engine small model.
|
||||||
|
- **Action**: keep the W4-B contract shape as the template (one clause per criterion, kit-untestable paths listed in CLARIFICATIONS before the first verifier); the live AC9 check is the real oracle for label quality (drop `bookkeeping` if it misfires). Links [[EVAL-044]], [[BDR-116]], [[LRN-210]], [[LRN-212]].
|
||||||
|
|||||||
@@ -599,3 +599,5 @@ rules:
|
|||||||
- model-router W3-A (first-use confirmation) landed 22455c0 on feature/model-router-confirm. User asked for it after the W2 merge; 3 pass-B answers. Plan r1→r4: correctness BLOCKER (Everywhere decision = census red → `changed` WARN exemption), robustness (first challenger killed by DNS, fresh one: project-tree layer dropped for security, single dialog in flight, keep-previous after first load), confirmation CONCERNS(6). Feater DONE (169 tests) + 4 rounds. Verifier ECARTS(3)/(2)/CONFORME; security BLOCK(1) REAL: password with `@`/`/` landed in the tracked key → strict parsing, no `local:` keys, output cap → re-verify ECARTS(1) fixtures → PASS. The working-tree mod was hot-loaded by the engine: my own dispatches opened T2 dialogs the user answered (verifier Keep, feater Keep, security-auditor → implement → user reset to verify); first misread as a test leak (LRN-212). GATE 0 oracle as heredoc not runnable → script. Full make test green (env red only). Docs P1-P13 user-approved (patch in flight); BDR-116, LRN-212, EVAL-043 written. Next: doc commit, memory commit, user merge + publish by hand; T1/T3 live checks open.
|
- model-router W3-A (first-use confirmation) landed 22455c0 on feature/model-router-confirm. User asked for it after the W2 merge; 3 pass-B answers. Plan r1→r4: correctness BLOCKER (Everywhere decision = census red → `changed` WARN exemption), robustness (first challenger killed by DNS, fresh one: project-tree layer dropped for security, single dialog in flight, keep-previous after first load), confirmation CONCERNS(6). Feater DONE (169 tests) + 4 rounds. Verifier ECARTS(3)/(2)/CONFORME; security BLOCK(1) REAL: password with `@`/`/` landed in the tracked key → strict parsing, no `local:` keys, output cap → re-verify ECARTS(1) fixtures → PASS. The working-tree mod was hot-loaded by the engine: my own dispatches opened T2 dialogs the user answered (verifier Keep, feater Keep, security-auditor → implement → user reset to verify); first misread as a test leak (LRN-212). GATE 0 oracle as heredoc not runnable → script. Full make test green (env red only). Docs P1-P13 user-approved (patch in flight); BDR-116, LRN-212, EVAL-043 written. Next: doc commit, memory commit, user merge + publish by hand; T1/T3 live checks open.
|
||||||
- model-router W3-B (dialog with context) landed 604a6c4 + data 32b71d2 + floor chore. User asked after W3-A: explain what is decided, OK/change/later, model then effort then scope. Plan r1→r3: all three lenses killed my inline-row format and the T3 phase edit (2 BLOCKERs avoided); efforts derived from the phases of the chosen model. Feater DONE first pass (229 tests), verifier ECARTS(2)→(1)→CONFORME, security PASS. GATE 0 heredoc CHECK not runnable again → script. Live during the gates: user answered the NEW dialog for security-auditor → opus/xhigh (the request's example; kept, floor aligned to opus, model-routing lock updated), plan-challenger → verify (reset on user go), orchestrate Keep (T3 check done). My `changed = {}` reset wiped a legit entry → census red for one commit → restored. Docs P1-P6 approved (patch in flight), BDR-116 amendment + EVAL-044 written. Next: doc commit, memory commit, user merge of feature/model-router-confirm (W3-A + W3-B) + publish by hand; T1 live check still open.
|
- model-router W3-B (dialog with context) landed 604a6c4 + data 32b71d2 + floor chore. User asked after W3-A: explain what is decided, OK/change/later, model then effort then scope. Plan r1→r3: all three lenses killed my inline-row format and the T3 phase edit (2 BLOCKERs avoided); efforts derived from the phases of the chosen model. Feater DONE first pass (229 tests), verifier ECARTS(2)→(1)→CONFORME, security PASS. GATE 0 heredoc CHECK not runnable again → script. Live during the gates: user answered the NEW dialog for security-auditor → opus/xhigh (the request's example; kept, floor aligned to opus, model-routing lock updated), plan-challenger → verify (reset on user go), orchestrate Keep (T3 check done). My `changed = {}` reset wiped a legit entry → census red for one commit → restored. Docs P1-P6 approved (patch in flight), BDR-116 amendment + EVAL-044 written. Next: doc commit, memory commit, user merge of feature/model-router-confirm (W3-A + W3-B) + publish by hand; T1 live check still open.
|
||||||
- model-router W3-C `/route forget` landed cd8d72f (+ data commit: escalate Keep). User asked whether decisions can be cleared and what another user can do; chose the command only, no per-user file. Plan r1→r3: three lenses CONCERNS (false `st.mem` premise, kind order, docs scope, census red after restoring an aligned floor, no confirmation on `all`), confirmation CONCERNS(3). Feater DONE; verifier ECARTS(2) with a REAL typo (`ForgetPlan`), then coverage only ×2 → cap → user accepted; security PASS (1 MEDIUM + 4 LOW parked). My own `route(escalate)` call opened the T3 dialog the user Kept. Branch feature/model-router-confirm now carries W3-A/B/C (14 commits), unmerged, unpushed; merge on signal; full make test running.
|
- model-router W3-C `/route forget` landed cd8d72f (+ data commit: escalate Keep). User asked whether decisions can be cleared and what another user can do; chose the command only, no per-user file. Plan r1→r3: three lenses CONCERNS (false `st.mem` premise, kind order, docs scope, census red after restoring an aligned floor, no confirmation on `all`), confirmation CONCERNS(3). Feater DONE; verifier ECARTS(2) with a REAL typo (`ForgetPlan`), then coverage only ×2 → cap → user accepted; security PASS (1 MEDIUM + 4 LOW parked). My own `route(escalate)` call opened the T3 dialog the user Kept. Branch feature/model-router-confirm now carries W3-A/B/C (14 commits), unmerged, unpushed; merge on signal; full make test running.
|
||||||
|
- model-router W4-A landed 68d7ea6 on feature/model-router-w4 (+ data commit). User: fewer logs (one line per real change with a cause), residuals, then the classifier. Plan r1→r3: three lenses (BLOCKER: an agent's model move is decided at spawn; `trace` level rejected as unrequested surface; reuse lastPlan/Loop; cause from the decision), confirmation CONCERNS(5) (hand-off line per dispatch deduped per type+target). Feater DONE (341 tests) + 4 rounds; verifier ECARTS(3) real (dedupe on text, double host prefix) then coverage ×2 → cap → user accepted; security PASS + 2 MEDIUM (raw skill names) fixed. Live: the terminal logs changed as soon as the feater saved; user Kept plan-challenger (judge) and the judge phase. Next: W4-B classifier; push-guard hotfix queued.
|
||||||
|
- model-router W4-B prompt classifier landed e54a2b5 on feature/model-router-w4. Plain composer prompt with no route in force → `$.model.classify` (5 labels, head 500 + tail 1500) → turn effort only, 3-s clock race, 3 failures pause, `/route classify on|off`. Plan r1→r3 (robustness BLOCKER: unbounded await), feater DONE + 1 round (trim, argumentHint), verifier ECARTS(2) → CONFORME, security PASS 0 findings. No live routing.json change this wave. Branch carries W4-A + W4-B (5 commits), unmerged, unpushed: merge on signal, user publishes. Live AC9 (plain prompts labelled) open.
|
||||||
|
|||||||
@@ -27,16 +27,21 @@ migration of shifters/pins/model-gate in wave 2 after proof; names model-router
|
|||||||
Plan `.claude/tasks/plans/2026-10-10-model-router-w3a-confirm-1201.md` r4, contract `2026-10-10-model-router-w3a-confirm-1201`. User decisions 2026-10-10: tracked routing.json reached through the plugin dir (no new link); blocking `$.ui.ask` dialog at first use; per skill row, agent row, main phase.
|
Plan `.claude/tasks/plans/2026-10-10-model-router-w3a-confirm-1201.md` r4, contract `2026-10-10-model-router-w3a-confirm-1201`. User decisions 2026-10-10: tracked routing.json reached through the plugin dir (no new link); blocking `$.ui.ask` dialog at first use; per skill row, agent row, main phase.
|
||||||
- [x] W3-A landed (22455c0, 2026-10-11): routing.json = phases + rows + decisions; T1/T2/T3 dialogs; project exceptions `projects[<normalized remote>]` in the tracked file (project-tree layer dropped: security); writers = dialog + `/route ask`; `/route pending`; census from the file with the `changed` WARN exemption; kit 86 → 190. Gates: 3 lenses + 1 confirmation (BLOCKER census), feater + 4 rounds, verifier CONFORME + re-verify, security BLOCK(1) credential leak fixed → PASS, full make test green (design-tool-gate env red). Live: T2 dialogs answered by the user (verifier Keep, feater Keep, security-auditor → implement then reset to verify on user go).
|
- [x] W3-A landed (22455c0, 2026-10-11): routing.json = phases + rows + decisions; T1/T2/T3 dialogs; project exceptions `projects[<normalized remote>]` in the tracked file (project-tree layer dropped: security); writers = dialog + `/route ask`; `/route pending`; census from the file with the `changed` WARN exemption; kit 86 → 190. Gates: 3 lenses + 1 confirmation (BLOCKER census), feater + 4 rounds, verifier CONFORME + re-verify, security BLOCK(1) credential leak fixed → PASS, full make test green (design-tool-gate env red). Live: T2 dialogs answered by the user (verifier Keep, feater Keep, security-auditor → implement then reset to verify on user go).
|
||||||
- [x] W3-B dialog with context (2026-10-11, 604a6c4, contract `2026-10-11-model-router-w3b-dialog-1240`, plan r3): descriptions (skill frontmatter / agent.offer / phase `about`), real id + effort in the question, Later / Keep / Change, model → effort (derived from the phases of that model) → scope, rows stay phases, T3 Later/Keep. 3 lenses (2 BLOCKERs on my r1 avoided) + 1 confirmation, feater + 2 rounds, verifier CONFORME (3rd), security PASS (4 LOW: symlinked SKILL.md read, TOCTOU, raw key in a log, any plugin hooking AskUserQuestion could answer). Live decisions: security-auditor → judge (kept, floor aligned `model: opus`), plan-challenger → verify (reset on user go), orchestrate Keep (T3 live check done).
|
- [x] W3-B dialog with context (2026-10-11, 604a6c4, contract `2026-10-11-model-router-w3b-dialog-1240`, plan r3): descriptions (skill frontmatter / agent.offer / phase `about`), real id + effort in the question, Later / Keep / Change, model → effort (derived from the phases of that model) → scope, rows stay phases, T3 Later/Keep. 3 lenses (2 BLOCKERs on my r1 avoided) + 1 confirmation, feater + 2 rounds, verifier CONFORME (3rd), security PASS (4 LOW: symlinked SKILL.md read, TOCTOU, raw key in a log, any plugin hooking AskUserQuestion could answer). Live decisions: security-auditor → judge (kept, floor aligned `model: opus`), plan-challenger → verify (reset on user go), orchestrate Keep (T3 live check done).
|
||||||
- [ ] W3-B residuals: a Change answered Everywhere can store a machine-only phase name (defined in `~/.claude/model-router.json`) into the tracked file (security note, correctness); SKILL.md symlink follow (LOW); `descs` cap and "(custom)" label untested; the security-auditor's `changed.from = verify` entry stays in routing.json although the floor now equals the row (harmless; prune at release).
|
- [ ] W3-B residuals: (machine-only phase into the tracked file: CLOSED in W4-A); SKILL.md symlink follow (LOW); `descs` cap and "(custom)" label untested; the security-auditor's `changed.from = verify` entry stays in routing.json although the floor now equals the row (harmless; prune at release).
|
||||||
- [ ] trace the "auto mode: use Bash/sed instead of Edit/Write" instruction block that sub-agents see attributed to the model-router MCP instructions (security-auditor 2026-10-11): it is NOT in mods/model-router source; likely the harness's own auto-mode text rendered next to the mod's MCP block. Confirm the origin.
|
- [x] "auto mode: use Bash/sed" block attributed to the mod: traced 2026-10-11 by reading, it is the harness's own auto-mode instruction rendered after the mod's MCP block; not in the mod source. Closed.
|
||||||
- [x] W3-C `/route forget` (2026-10-11, cd8d72f, contract `2026-10-11-model-router-w3c-forget-1457`, plan r3): name/all/projects, guarded restore, confirmation dialog for all/projects, no-write paths, docs. 3 lenses + 1 confirmation, feater + 3 rounds (1 real defect: `ForgetPlan` typo), verifier at the cap on coverage (user accepted), security PASS. Kit 232 → 284. Live: orchestrate + escalate T3 Keeps seen.
|
- [x] W3-C `/route forget` (2026-10-11, cd8d72f, contract `2026-10-11-model-router-w3c-forget-1457`, plan r3): name/all/projects, guarded restore, confirmation dialog for all/projects, no-write paths, docs. 3 lenses + 1 confirmation, feater + 3 rounds (1 real defect: `ForgetPlan` typo), verifier at the cap on coverage (user accepted), security PASS. Kit 232 → 284. Live: orchestrate + escalate T3 Keeps seen.
|
||||||
- [ ] W3-C residuals (security, accepted): `forget not applied (${String(err)})` echoes the raw writer error (send to the log); file-sourced `from`/`to`/keys unsanitized in the answer (clean() them); `floorFile` builds a display path from an unvalidated key (SKILL_NAME check); `__proto__` accepted by `restoreOf` while `setKey` refuses it (plan/apply mismatch); `changed.phases` entries restorable (limit the scan to ROWS). Live smoke of `/route forget` from the terminal still unseen (criterion 7).
|
- [x] W3-C residuals closed in W4-A (68d7ea6). Live smoke of `/route forget` from the terminal still unseen.
|
||||||
|
- [x] W4-A transition logs + residuals (2026-10-11, 68d7ea6, contract `2026-10-11-model-router-w4a-logs-residuals-1637`, plan r3): verbose = transitions only (main line, hand-off per dispatch, agent moves, answered-by anomaly), stream lines removed, host prefix respected; residuals of W3-A/B/C closed (a)-(g). 3 lenses (BLOCKER: agent moves decided at spawn) + 1 confirmation, feater + 4 rounds, verifier at the cap on coverage (user accepted), security PASS + 2 MEDIUM fixed. Kit 284 → 358.
|
||||||
|
- [ ] W4-A residuals (LOW, accepted): `readLayer` logs `String(err)` of a JSON parse error (a file fragment can appear in the local log); `logCall`/`noteMain` ui calls on the step path sit outside `safely` (fail-open on routing, no security impact).
|
||||||
|
- [ ] push-guard hotfix: the PreToolUse hook denies a read-only command whose TEXT contains the push verb next to `git` (grep pattern, heredoc); match the command's own argv only.
|
||||||
|
- [x] W4-B classifier (2026-10-11, e54a2b5, contract `2026-10-11-model-router-w4b-classifier-1858`, plan r3): `$.model.classify` on plain composer prompts with no route in force → turn effort only (design→plan, analysis→reflect, coding→implement, bookkeeping→apply, chat→nothing), 3-s race, abort not counted, 3 failures pause `cooldownMinutes`, no dialog, no file write, `classify` config key, `/route classify on|off`, status + verbose line. 3 lenses (BLOCKER: unbounded await) + 1 confirmation, feater + 1 round, verifier CONFORME (2nd), security PASS. Kit 358 → 428. Deviation from the TODO line: no first-use confirmation on classified phases (noise per prompt; T3 already confirms the phase).
|
||||||
|
- [ ] W4-B residuals: `smallModelBusy` checks `cfg.models.haiku` but classify uses the engine's default small model (guard misses when they differ); abort paths judged by reading only; the 3-s tests pin the upper bound only. Live AC9 open: type a few plain prompts, read `route: classifier <phase>` (verbose line with `/route verbose on`), drop `bookkeeping` if it misfires.
|
||||||
- [ ] W3-A live checks still open: T1 (typed `/feat` etc.) dialog (T3 seen live 2026-10-11: orchestrate Keep); a parallel same-agent dispatch answered after > 10 s; what an unanswered dialog resolves to on the terminal (must be Later or nothing written). Record in the contract as `[gated]` when seen.
|
- [ ] W3-A live checks still open: T1 (typed `/feat` etc.) dialog (T3 seen live 2026-10-11: orchestrate Keep); a parallel same-agent dispatch answered after > 10 s; what an unanswered dialog resolves to on the terminal (must be Later or nothing written). Record in the contract as `[gated]` when seen.
|
||||||
- [ ] W3-A residuals (security LOW, accepted): non-atomic cross-process write (two sessions, crash mid-write → file reads as failed, previous config kept); `host/path` of a private remote in the tracked file; a persistent write failure re-asks every use (`asked.delete` in the catch); `out.length` vs byte size at the cap; `changePatch` scope 'project' with a vanished key writes Everywhere (cwd change mid-dialog). Deferred: `/route set`, `/route confirm`, `(unconfirmed)` in show, dialog edits of phases, frontmatter auto-alignment, session-start warning when routing.json is dirty, per-machine `projects`.
|
- [ ] W3-A residuals (security LOW, accepted): non-atomic cross-process write (two sessions, crash mid-write → file reads as failed, previous config kept); `host/path` of a private remote in the tracked file; a persistent write failure re-asks every use (`asked.delete` in the catch); `out.length` vs byte size at the cap; `changePatch` scope 'project' with a vanished key writes Everywhere (cwd change mid-dialog). Deferred: `/route set`, `/route confirm`, `(unconfirmed)` in show, dialog edits of phases, frontmatter auto-alignment, session-start warning when routing.json is dirty, per-machine `projects`.
|
||||||
- [ ] routing.json ships `confirmed` for verifier/feater (user Keeps) and phases verify/implement: every clone inherits them (by design: decisions travel). Revisit at release if unwanted.
|
- [ ] routing.json ships `confirmed` for verifier/feater (user Keeps) and phases verify/implement: every clone inherits them (by design: decisions travel). Revisit at release if unwanted.
|
||||||
|
|
||||||
- [ ] W2 residuals: probe 4 unobserved (above); the push-guard hook denies a read-only grep (or a heredoc) whose TEXT contains the push verb next to `git` (security-auditor + orchestrator 2026-10-10, false positives, reworded); ~27 loose "sonnet pin"/"opus pin" shorthand sites kept (true by census); README config key list omits `tiers`/`fallback`/`cooldownMinutes`/`mainUpgrade`/`upgradeMaxTokens` (doc audit item 6, pre-existing); MIGRATION.md "Upgrading to 3.0.0" at release time; `skillCalls`/`spawning` counters global; offers keyed by name.
|
- [ ] W2 residuals: probe 4 unobserved (above); the push-guard hook denies a read-only grep (or a heredoc) whose TEXT contains the push verb next to `git` (security-auditor + orchestrator 2026-10-10, false positives, reworded); ~27 loose "sonnet pin"/"opus pin" shorthand sites kept (true by census); README config key list omits `tiers`/`fallback`/`cooldownMinutes`/`mainUpgrade`/`upgradeMaxTokens` (doc audit item 6, pre-existing); MIGRATION.md "Upgrading to 3.0.0" at release time; `skillCalls`/`spawning` counters global; offers keyed by name.
|
||||||
- [ ] W3 optional: step heuristics, haiku classifier, quota-aware downgrade, A/B
|
- [ ] W3 optional: step heuristics, quota-aware downgrade, A/B (haiku classifier: done W4-B)
|
||||||
|
|
||||||
## 2026-09-30 — Higgsfield pack: CLI + skills in the install process, off by default (feature/higgsfield-pack)
|
## 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
|
Contract `.claude/tasks/contracts/2026-09-30-higgsfield-pack-1412.md`, spec + plan under
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# CONTRACT — model-router-w4a-logs-residuals
|
||||||
|
- date: 2026-10-11 | flow: feat | branch: feature/model-router-w4
|
||||||
|
- status: active
|
||||||
|
|
||||||
|
## REQUEST (verbatim — IMMUTABLE)
|
||||||
|
okm j'aimerais que tu corrige donc les residus techniques, et aussi que tu fasse cette wave optionnelle du classifieur. Et aussi, pour le moment on a beaucoup de log avec le mod de la route, par exemple [pasted: "model-router: step 3 answered by claude-sonnet-5-5", "model-router: step 18 agent af0b3fa5: claude-opus-5-5/xhigh → claude-opus-5-5/xhigh", "model-router: step 0 main: claude-fable-5-1/high → claude-fable-5-1/high" …] , est-ce auil y a moyen de reduire ces log la a juste on dit quand on chane de model / effort et 2 ou 3 mots de contextes. eviter les repetitions de model-router step. juste direm ah la on switch de ca a ca car blablabla.
|
||||||
|
|
||||||
|
## CLARIFICATIONS
|
||||||
|
(pass A: none. Split: W4-A = residuals + transition logs (this contract); W4-B = the prompt classifier (own contract))
|
||||||
|
Q: public shape (after the simplicity lens, orchestrator default, user may veto): `verbose` stays a boolean; `true` logs TRANSITIONS only ("model-router: <main|agent <type>>: <from> → <to> (<phase>: <cause>[, <model cause>])"); the four verbose stream lines (step, answered by, spawn, route-tool JSON, skill path) are removed, no `trace` level; `/route verbose on|off` unchanged [stated 2026-10-11]
|
||||||
|
Q: the "auto mode: use Bash/sed instead of Edit/Write" block attributed to the mod / A: traced by reading: it is the harness's own auto-mode instruction rendered right after the mod's MCP-instructions block; not in mods/model-router source; closed, no change [resolved 2026-10-11]
|
||||||
|
|
||||||
|
Q: the hand-off line fires on every rowed or explicit spawn, even when the pair equals main's (a dispatch is a hand-off) / A: accepted [stated 2026-10-11]
|
||||||
|
|
||||||
|
Q: three `clean()` sites are unreachable by construction (phases pass PHASE_KEY at load; `kept.to` equals a live slot's phase): kept as defence in depth without a test / A: accepted [gated 2026-10-11]
|
||||||
|
Q: the host prefixes every `$.ui.log` line with the plugin name (seen in the user's terminal): the new lines carry no literal "model-router: " prefix; the pre-existing warnings are left as they are [stated 2026-10-11]
|
||||||
|
|
||||||
|
Q: GATE 1 cap (verifier ECARTS(3): dedupe key + double prefix real, then coverage ×2; 358 kit tests) / A: user "Accepter et commiter"; security PASS then two MEDIUM (raw skill names in two lines) fixed with tests, no re-scan (two clean() calls); LOW parked: JSON parse error text in the load log, `logCall`/`noteMain` ui calls outside `safely` [gated 2026-10-11]
|
||||||
|
|
||||||
|
## ACCEPTANCE CRITERIA
|
||||||
|
1. `verbose: true` logs only transitions: main → one line per turn per (from → to) when the planned (model, effort) pair differs from main's previous plan (after /clear: from the engine input), built from the decision itself (effort cause = the slot that supplied it: `skill /<name>`, `route tool`, `sticky /route`, `prompt floor`, `dispatch`, `session`; model cause = the decision's why when the model moved: `upgrade`, `fallback: <alias> down`, `switch`), emitted inside `safely` BEFORE `next` (a log failure never unroutes a step); agent → one hand-off line per dispatch, deduped per turn per (type, to): "main <from> → agent <type> <to> (<model cause>, <effort cause>)" with causes `row <phase>` | `explicit` | `fallback: <alias> down` | `frontmatter` and `row` | `explicit` | `engine`, then one line per later effort move on that loop (`route tool`, `skill <name>`); a prompt-rule route reads "(<phase>: prompt rule)"; names cleaned (≤ 40 code points); no "step N" lines, no "answered by" lines, no spawn/route-JSON/skill-path lines at any level. `false` logs nothing but the existing warnings (`warnOnce`, `logCall`, `tierDownLog`, engine switch, config drops). One kit test per clause.
|
||||||
|
2. An agent answering on a model other than the planned one (canonical ids compared, `usage` null skipped) is logged once per (agent, model) at `true` ("model-router: agent verifier answered by opus instead of sonnet"); never for a `[1m]` suffix alone; main is not covered (the engine switch line exists). One kit test per clause.
|
||||||
|
3. Config: `verbose` stays boolean; an invalid override value (`"ON"`, 1, null) falls back to the default `false` with one log per load; `/route verbose on|off`, `/route show` and the session toggle unchanged. One kit test per clause.
|
||||||
|
4. Residuals closed (W3-C security + W3-B + W3-A): (a) `forget not applied` sends `String(err)` to `$.ui.log` and answers a generic text; (b) every file-sourced fragment in a forget answer (`from`, `to`, keys) passes `clean(…, 40)`; (c) `floorFile` returns undefined unless `SKILL_NAME` matches the name; (d) `restoreOf` refuses `__proto__` (plan and apply agree); (e) the `changed` scan is limited to ROWS (phases never restored); (f) a dialog Change offers and targets only SHIPPED phases (code defaults + the validated phases of routing.json, the machine override excluded), so a machine-only phase is never offered nor written into the tracked file; the empty case toasts "no shipped phase uses <alias>: add one to routing.json" → Later; (g) a failure AFTER the user answered keeps the key in `asked` (no re-ask every use; the next session asks again), a failure before the answer still leaves it unasked (that half judged by reading: the kit skips a throwing hook, no seam exists between `asked.add` and the answer [gated 2026-10-11]). One kit test per clause, each failing if its guard were removed.
|
||||||
|
5. Kit suite, mods suite, validate and census green.
|
||||||
|
CHECK: cd mods/model-router && out="$(claude plugin test . 2>&1)" && printf '%s\n' "$out" | grep -qE '[0-9]+ pass' && ! printf '%s\n' "$out" | grep -qE '[1-9][0-9]* fail' && claude plugin validate . 2>&1 | grep -q 'passed' && cd ../.. && make test suite=lib/tests/mods.test.sh 2>&1 | grep -q 'all suites green' && bash lib/tests/effort-routing.test.sh >/dev/null 2>&1 && echo W4A-GREEN
|
||||||
|
EXPECT: W4A-GREEN
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: W4A-GREEN
|
||||||
|
6. Docs: README (its three `verbose` sites say "transitions only"), USAGE (`/route` row), CHANGELOG Unreleased (Changed: transition logs; Fixed: the residuals), `lib/effort-shift.md` unchanged or ≤ 1 line.
|
||||||
|
CHECK: grep -qi 'transition' README.md && grep -qi 'transition' USAGE.md && grep -qi 'transition' CHANGELOG.md && echo W4A-DOCS
|
||||||
|
EXPECT: W4A-DOCS
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: W4A-DOCS
|
||||||
|
|
||||||
|
## FILE SCOPE
|
||||||
|
mods/model-router/hooks/register.ts, mods/model-router/hooks/register.test.ts, README.md, USAGE.md, CHANGELOG.md (one clause each), lib/effort-shift.md (≤ 1 line)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# CONTRACT — model-router-w4b-classifier
|
||||||
|
- date: 2026-10-11 | flow: feat | branch: feature/model-router-w4
|
||||||
|
- status: active
|
||||||
|
|
||||||
|
## REQUEST (verbatim — IMMUTABLE)
|
||||||
|
okm j'aimerais que tu corrige donc les residus techniques, et aussi que tu fasse cette wave optionnelle du classifieur. [earlier, same day: "est-ce quil y a une comande pour clear…"; "tu me dit le routing de tout skills et agents, mais c'est valable aussi pour la conversatio principale ? Si tu invoaue ni skill ni agebt il faut que ca route aussi on est d'accord ?" → orchestrator: the plain conversation routes only through prompt keywords, ultrathink, derived orchestrate and the route tool; the missing piece is the planned haiku classifier on each typed prompt → user chose it]
|
||||||
|
|
||||||
|
## CLARIFICATIONS
|
||||||
|
(pass A: none — the W1 plan § W3.2 and the 2026-10-11 exchange define outcome and scope)
|
||||||
|
Q: public shape (after three lenses, orchestrator default, user may veto): labels `design | analysis | coding | bookkeeping | chat` mapped to plan / reflect / implement / apply / none; the classified phase becomes the TURN DEFAULT route carrying its EFFORT ONLY (never a model move), and only when NO route is in force (no typed skill, no run slot, no route tool, no sticky `/route`, no floor, no keyword rule, not mid-turn, composer only); no first-use dialog on it (visibility: status line `route: classifier <phase>` and the transition line with `/route verbose on`); `classify` is a machine config key (default `true`, `"classify": false` in `~/.claude/model-router.json` turns it off) with a session toggle `/route classify on|off`; the call is bounded to 3 s and pauses for `cooldownMinutes` after 3 failures; one small-model call per typed prompt of ≥ 12 characters [stated 2026-10-11]
|
||||||
|
|
||||||
|
Q: the abort paths (`next.signal.aborted` first in the catch and after the await; classify's own `aborted` cause not counted) cannot be driven by the kit (no abortable dispatch, a throwing bottom hook becomes "no implementation") / A: judged by reading; the live check covers the rest [gated 2026-10-11]
|
||||||
|
|
||||||
|
## ACCEPTANCE CRITERIA
|
||||||
|
1. On a composer `prompt.submit` with `classify` on, not mid-turn, a trimmed text ≥ 12 chars not starting with `/`, no prompt rule matched and NO route in force on main (no sticky, turn, run or floor), the mod calls `$.model.classify(<first 500 + last 1500 chars>, [design, analysis, coding, bookkeeping, chat])` once, with no `model` option, and writes the turn route `{ effort }` of the mapped phase with source `classifier` (`chat` or an unknown label → nothing); it never runs for a sub-agent prompt, a non-composer origin (so never under `-p`), mid-turn, with the mod off, with `classify` off, when a rule matched, or when a route is in force. One kit test per clause (a bottom `model.classify` mock).
|
||||||
|
2. The classified route is turn-scoped (gone at `turn.complete`), carries the effort only (the main model never moves because of it: no tier), may lower the effort (coding → medium, bookkeeping → low), is replaced by any later route tool call or typed skill in the turn, and is ignored at a step whose engine effort differs from the session's baseline (a user `/effort` beats it). One kit test per clause.
|
||||||
|
3. Bounded and fail-safe: the call races a 3 s `$.clock.sleep` (timeout → no route, counts as a failure); a user interrupt (`next.signal.aborted`) neither counts nor writes; every guard is re-checked after the await (a route landing meanwhile wins); a rejection or timeout counts, three in a row pause the classifier for `cooldownMinutes` (×60000, kept across `/clear`, cleared by `/route reload`) with one toast and a status marker, a settled answer resets the counter; an unknown label neither warns nor counts; the classifier is skipped while the haiku id is marked down or the main loop runs on haiku. The prompt always proceeds. One kit test per clause.
|
||||||
|
4. No dialog and no file write: a classified route opens no first-use dialog and never writes routing.json. One kit test (no `ui.ask`, no `fs.write` on a classified prompt).
|
||||||
|
5. Config and command: `classify` is a Config key (default `true`) read from the machine override through `pickSwitch` (invalid → default + one log); `/route classify on|off` is a session toggle (composer only) that survives a rebuild like `switch`/`verbose`; `/route show` prints `classify: on|off` on the `router:` line; the argumentHint lists it. One kit test per clause.
|
||||||
|
6. Visibility: the status line reads `route: classifier <phase>` and the main transition line "(<phase>: classifier)" (with verbose on). One kit test per clause.
|
||||||
|
7. Kit suite, mods suite, validate and census green.
|
||||||
|
CHECK: cd mods/model-router && out="$(claude plugin test . 2>&1)" && printf '%s\n' "$out" | grep -qE '[0-9]+ pass' && ! printf '%s\n' "$out" | grep -qE '[1-9][0-9]* fail' && claude plugin validate . 2>&1 | grep -q 'passed' && cd ../.. && make test suite=lib/tests/mods.test.sh 2>&1 | grep -q 'all suites green' && bash lib/tests/effort-routing.test.sh >/dev/null 2>&1 && echo W4B-GREEN
|
||||||
|
EXPECT: W4B-GREEN
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: W4B-GREEN
|
||||||
|
8. Docs: README (`classifier` paragraph in the model-router section: what, cost, `/route classify`), USAGE (`/route` row + one sentence in "Niveau d'effort"), CHANGELOG Unreleased Added, `lib/effort-shift.md` (≤ 2 lines).
|
||||||
|
CHECK: grep -qi 'classif' README.md && grep -qi 'classif' USAGE.md && grep -qi 'classif' CHANGELOG.md && grep -qi 'classif' lib/effort-shift.md && echo W4B-DOCS
|
||||||
|
EXPECT: W4B-DOCS
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: W4B-DOCS
|
||||||
|
9. Live check (orchestrator + user, after the gates, `[gated]`): a few plain prompts typed in the terminal, each with the label it got (status line `route: classifier <phase>`, transition line with verbose on); drop `bookkeeping` if it misfires.
|
||||||
|
|
||||||
|
## FILE SCOPE
|
||||||
|
mods/model-router/hooks/register.ts, mods/model-router/hooks/register.test.ts, README.md, USAGE.md, CHANGELOG.md, lib/effort-shift.md, lib/tests/effort-routing.test.sh (only if a lock needs it)
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
# PLAN r3 — model-router wave 4-A: transition logs + residuals (2026-10-11)
|
||||||
|
|
||||||
|
r1 → r2 after simplicity CONCERNS(4), robustness CONCERNS(5), correctness
|
||||||
|
FATAL(5) (BLOCKER: an agent's model move happens at spawn, invisible at
|
||||||
|
turn.step); r2 → r3 after the confirmation (correctness CONCERNS(5)).
|
||||||
|
Precedence r3 > r2 > r1. Contract
|
||||||
|
`.claude/tasks/contracts/2026-10-11-model-router-w4a-logs-residuals-*.md`.
|
||||||
|
Base: develop f1cea54. Corrected facts: `Plan` = {model, effort} and
|
||||||
|
`mainPlan` discards the `Call` (its `why`); `agentPlan` returns `e.model`
|
||||||
|
verbatim (the spawn hook rewrote it already); `TurnStepInput` carries only
|
||||||
|
`agentId`; `Loop` = {effort, explicitEffort}; `Routed` = {phase, route,
|
||||||
|
source} (no skill name, no rule word); `mainRoute` excludes `turnFloor`;
|
||||||
|
`mainEffort().by` ∈ sticky|turn|floor|engine; no `effortSource`; `st.lastPlan`
|
||||||
|
holds main's previous plan (nulled on /clear); `st.logged` / `turnLogged`
|
||||||
|
are the dedupe sets; `AgentSpawnInput.parentModel` exists; `askOr` swallows
|
||||||
|
a rejected ask into LATER; `forget not applied (${String(err)})` lives in
|
||||||
|
`forgetWrite` (and its twin in `askCommand`); `buildMemory` has no
|
||||||
|
models/tiers to validate phases; the user's override sets `verbose: true`.
|
||||||
|
|
||||||
|
## D1 Logs: `verbose` stays boolean; true = transitions only
|
||||||
|
- The four firehose `vlog` sites are DELETED (step line, "answered by",
|
||||||
|
spawn line, route-tool JSON, skill-path line). No `trace` level. `vlog`
|
||||||
|
stays the single gate; `/route verbose on|off`, argumentHint and show
|
||||||
|
texts unchanged.
|
||||||
|
- Main transition: `planStep` returns `{ plan, why }` where `why` =
|
||||||
|
per-axis cause words computed at decision time (no `$` call, no
|
||||||
|
re-decision): effort cause from `mainEffort().by` + its slot (`sticky
|
||||||
|
/route`, `route tool` (turn, source model), `skill <label>` (run/turn,
|
||||||
|
source skill|run), `prompt floor` (floor), `dispatch` (derived
|
||||||
|
orchestrate), `session`); model cause = the `Call.why` when it moved
|
||||||
|
(`upgrade`, `fallback: <alias> down`, `switch`) else none. `Routed`
|
||||||
|
gains an optional `label`: the skill name at `routeMainBySkill` /
|
||||||
|
`confirmSkill` (`/feat`), the rule's phase at `floorFromPrompt`.
|
||||||
|
Rendered: "model-router: main: <alias>/<effort> → <alias>/<effort>
|
||||||
|
(<phase>: <cause>[, <model cause>])", e.g. "(plan: skill /ship-feature)",
|
||||||
|
"(orchestrate: dispatch)", "(escalate: prompt floor)", "(reflect:
|
||||||
|
prompt rule)" (turn/source prompt; `defaultFromPrompt` sets the label),
|
||||||
|
"(custom: sticky /route)", "(reflect: skill /feat, upgrade)"; no route
|
||||||
|
in force → "(session[, <model cause>])". Model cause from `Call.why`:
|
||||||
|
upgrade → `upgrade`, fallback → `fallback: <alias> down`, downgrade →
|
||||||
|
`switch`. Comparison: `prev = st.lastPlan ?? {e.model, e.effort}`,
|
||||||
|
CAPTURED BEFORE `noteMain` overwrites `lastPlan` (after /clear or
|
||||||
|
`/route on|off`, which null `lastPlan`, the first step compares to the
|
||||||
|
engine input: stated omission); emitted INSIDE `safely(st, $,
|
||||||
|
'turn.step log', …)` BEFORE `next` (a log failure never unroutes a
|
||||||
|
step; own warn key); deduped per turn per (loop, from→to) through
|
||||||
|
`turnLogged` (derived orchestrate waves log once per turn).
|
||||||
|
- Agent HAND-OFF line (r3, a dispatch is a model change): emitted in
|
||||||
|
`afterSpawn` for every rowed or explicit spawn, deduped per turn per
|
||||||
|
(type, to) through `turnLogged` (three parallel challengers = one
|
||||||
|
line): "model-router: main <alias>/<effort> → agent <type>
|
||||||
|
<alias>/<effort> (<model cause>, <effort cause>)" where from = main's
|
||||||
|
`st.lastPlan` (or the engine's session model when none), to = the
|
||||||
|
EFFECTIVE sent plan (`started.model` canonical; effort = explicit ??
|
||||||
|
row effort, stripped on haiku), model cause = `row <phase>` | `explicit`
|
||||||
|
| `fallback: <alias> down` (in-tier move) | `frontmatter` (tier down,
|
||||||
|
nothing written), effort cause = `row` | `explicit` | `engine` (no row).
|
||||||
|
`Loop` gains optional `{ type, last: {model, effort}, cause }`: set in
|
||||||
|
`trackLoop` from the effective plan; `writeLoop` updates `effort` and
|
||||||
|
`cause` only (`route tool` | `skill <name>` | `explicit`); each agent
|
||||||
|
step compares `plan` to `loop.last ?? {e.model, e.effort}`, logs
|
||||||
|
"model-router: agent <type>: <from> → <to> (<cause>)" on a difference,
|
||||||
|
then sets `loop.last = plan` (haiku: effort undefined on both sides);
|
||||||
|
no Loop or no `type` → label `agent <id8>`; `endAgent` drops the entry;
|
||||||
|
caps as `MAX_AGENT_MODELS`.
|
||||||
|
- Aliases: `aliasOf(cfg, canonical(cfg, id))` with a `[1m]` marker kept,
|
||||||
|
the id itself when no alias matches; effort `-` when undefined; every
|
||||||
|
name in a line passes `clean(…, 40)`.
|
||||||
|
- Answered-by anomaly (agents only; main's engine switch is already
|
||||||
|
logged by `onAutoSwitch`): after `next`, inside `safely`, when
|
||||||
|
`result.usage` is non-null and `canonical(usage.model) !==
|
||||||
|
canonical(plan.model)`, log once per (agentId, model) via `st.logged`
|
||||||
|
key `answered:<id>:<model>`: "model-router: agent <type> answered by
|
||||||
|
<alias> instead of <alias>".
|
||||||
|
- Unconditional lines stay as today and count as warnings: `logCall`
|
||||||
|
(upgrade skipped / no fit), `tierDownLog`, `onAutoSwitch`, `warnOnce`,
|
||||||
|
config drops, toasts. The spinner suffix unchanged.
|
||||||
|
- Config: `verbose` invalid (`"ON"`, 1, null) → default `false` + one
|
||||||
|
log per load (mergeConfig falls back to the seed, stated).
|
||||||
|
|
||||||
|
## D2 Residuals
|
||||||
|
- (a) `forgetWrite` catch AND `askCommand` catch (the latter gains the
|
||||||
|
`RebuildFailed` branch "ask saved, config not rebuilt: /route reload"):
|
||||||
|
`$.ui.log("model-router: <cmd> failed: " + String(err))`, answers
|
||||||
|
"forget not applied, see the log" / "ask not applied, see the log".
|
||||||
|
- (b) `kept` becomes `{kind, name, reason}[]`; `clean(x, 40)` applied to
|
||||||
|
the file-sourced fragments (name, from, to) at RENDER time only
|
||||||
|
(`forgetAnswer`, `restoreClause`, `idleAnswer`, kept rendering); the
|
||||||
|
fixed reason texts untouched; `ForgetPlan`/`Restore` fields untouched
|
||||||
|
(a 45-char row name is restored byte-exact in the file; test).
|
||||||
|
- (c) `floorFile`: `SKILL_NAME.test(name)` else undefined.
|
||||||
|
- (d) `restoreOf`: `name === '__proto__'` → refused ("reserved name").
|
||||||
|
- (e) the `changed` scan iterates ROWS only.
|
||||||
|
- (f) `mem.filePhases` = exactly the seeded layer: `mergeConfig({
|
||||||
|
phases: file.phases }, noop).phases` (code defaults + file phases
|
||||||
|
validated against the DEFAULT models and tiers, override excluded),
|
||||||
|
computed in `assemble` AND `refreshMemory`; `effortsOf` and
|
||||||
|
`targetPhase` use it, so a machine-only phase is never offered nor
|
||||||
|
written; the existing empty-efforts toast reads "no shipped phase uses
|
||||||
|
<alias>: add one to routing.json". Tests: a null file phase (ignored,
|
||||||
|
no crash), a file without `phases` (code defaults offered), an
|
||||||
|
override-only `custom` (not offered, not written).
|
||||||
|
- (g) `confirmFirst`: `answered` flag set after `decide()` returns; the
|
||||||
|
catch deletes the key from `asked` only when unset. Tests: a throw
|
||||||
|
BETWEEN `asked.add` and the answer (a throwing `ui.toast` on an
|
||||||
|
unrecognised answer) → key unasked (fails if the delete is removed);
|
||||||
|
`world.failWrite` (post-answer) → key stays asked, one log; the three
|
||||||
|
existing tests asserting the old post-answer deletion are inverted
|
||||||
|
(authorized); the `failOnce` dedupe test rewritten with two different
|
||||||
|
rows failing.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
- [ ] S1 delete the four vlog sites; `planStep` → `{plan, why}`;
|
||||||
|
`Routed.label` (skill name, prompt rule phase); `Loop {type?, last?,
|
||||||
|
cause?}`; `lastPlan = null` on `/route on|off`.
|
||||||
|
- [ ] S2 `transitionLine` (main, in safely before next, turnLogged dedupe)
|
||||||
|
+ agent line in `afterSpawn` + agent step comparison + answered-by
|
||||||
|
anomaly; `aliasOf` canonical; names cleaned.
|
||||||
|
- [ ] S3 residuals (a)-(g).
|
||||||
|
- [ ] S4 docs: README (three `verbose` sites: the `/route` bullet, the key
|
||||||
|
list "default false", the commands table → "transitions only"),
|
||||||
|
USAGE `/route` row, CHANGELOG Unreleased (Changed: logs; Fixed: the
|
||||||
|
residuals). argumentHint unchanged.
|
||||||
|
- [ ] S5 kit tests (recordLogs recorder; a `usage`-bearing bottom step hook
|
||||||
|
where needed), one per clause: main unchanged over 3 steps → 0
|
||||||
|
lines; route tool plan → exactly one line with "(plan: route tool)";
|
||||||
|
typed `/feat` → "(reflect: skill /feat)"; derived orchestrate → one
|
||||||
|
line per turn even with two dispatch waves; ultrathink → "(escalate:
|
||||||
|
prompt floor)"; sticky `/route xhigh` → "(custom: sticky /route)";
|
||||||
|
sonnet session + typed `/feat` → ", upgrade"; feater spawn → one
|
||||||
|
agent line "agent feater: fable/high → sonnet/medium (row: feater
|
||||||
|
implement)" and 0 lines on its next steps; explicit model/effort
|
||||||
|
spawn words; in-agent route tool effort move → one agent line
|
||||||
|
"(route tool)"; answered-by anomaly once (usage hook), none when
|
||||||
|
usage null, none on a `[1m]` plan; a throwing `ui.log` leaves the
|
||||||
|
step routed; `verbose: false` → nothing; invalid override values
|
||||||
|
→ default + one log; /clear → first step compared to engine input;
|
||||||
|
the legacy four lines never appear at `true` (typed skill, route
|
||||||
|
tool, spawn, step); "explain why" prompt → "(reflect: prompt
|
||||||
|
rule)"; explicit-effort spawn and haiku spawn → one hand-off line
|
||||||
|
then 0 lines on their next steps; three parallel challengers → one
|
||||||
|
hand-off line; tier-down spawn → "frontmatter" cause; (a)-(g) each
|
||||||
|
with a mutation-sensitive test.
|
||||||
|
- Disposition: honors BDR-115/116 (no new writer; logs carry ids and
|
||||||
|
cleaned names only), LRN-210, LRN-212 (live mod: the terminal changes
|
||||||
|
as soon as the feater saves).
|
||||||
|
|
||||||
|
## Challenge ledger (r1 → r2)
|
||||||
|
- simp 1, corr 11/12 (`trace` surface) → removed; boolean kept.
|
||||||
|
- simp 2, corr 2, rob 3/6 (new Map, missing type/cause) → `st.lastPlan`
|
||||||
|
+ `Loop {type, last, cause}`, dropped in endAgent, capped.
|
||||||
|
- simp 3/4, corr 1 BLOCKER/3/4/5, rob 3 (why not derivable; spawn move
|
||||||
|
invisible) → `planStep` returns why from the decision; `Routed.label`;
|
||||||
|
agent line in `afterSpawn` with `parentModel`.
|
||||||
|
- simp 5, corr 6, rob 2 (answered-by) → agents only, canonical ids, null
|
||||||
|
usage skipped, `st.logged` dedupe.
|
||||||
|
- rob 1 (log failure unroutes) → safely before next, own warn key.
|
||||||
|
- rob 7 (flapping) → per-turn dedupe via turnLogged.
|
||||||
|
- rob 8 (names) → clean(40) on labels and causes.
|
||||||
|
- rob 9, corr 12 (invalid verbose) → default + one log, stated.
|
||||||
|
- rob 10 (/clear) → stated omission; reset at /route on.
|
||||||
|
- rob 11 (legacy lines untested) → S5 cases.
|
||||||
|
- simp 6, corr 9, rob 5 ((f) unvalidated / dead toast) → validated seed
|
||||||
|
layer in assemble + refreshMemory; toast folded.
|
||||||
|
- simp 7, corr 8 ((g) premise) → `answered` flag; tests inverted.
|
||||||
|
- rob 4 ((b) clean on plan fields) → render-time only.
|
||||||
|
- corr 10, rob 12 ((a) site + twin) → forgetWrite + askCommand.
|
||||||
|
|
||||||
|
## Confirmation ledger (r2 → r3)
|
||||||
|
- conf 1 (agent from-effort unsourced, non-changes) → hand-off line from
|
||||||
|
main's lastPlan, per-axis causes, per-turn dedupe per (type, to).
|
||||||
|
- conf 2 (loop.last bookkeeping) → last = effective sent plan, writeLoop
|
||||||
|
updates cause/effort only, each step sets last after comparing; tests.
|
||||||
|
- conf 3 (prompt rules unlabelled) → `prompt rule` cause + label.
|
||||||
|
- conf 4 ((b) kept strings) → kept as records, fragments cleaned at render.
|
||||||
|
- conf 5 ((g) pre-answer test) → throw between asked.add and the answer.
|
||||||
|
- conf 6 (prev before noteMain) → stated.
|
||||||
|
- conf 7 (why words, no-route render, alias fallback) → mapping spelled out.
|
||||||
|
- conf 8 ((f) formula) → `mergeConfig({phases}, noop).phases`.
|
||||||
|
- conf 9 ((a) askCommand) → RebuildFailed branch added.
|
||||||
|
- conf 10 (/route on reset) → S1.
|
||||||
|
- conf 11 (loop without type) → optional fields, id8 label.
|
||||||
|
- conf 12 (failOnce test) → two rows.
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# PLAN r3 — model-router wave 4-B: prompt classifier (2026-10-11)
|
||||||
|
|
||||||
|
r1 → r2 after simplicity CONCERNS(4), robustness FATAL(6) (BLOCKER: the
|
||||||
|
classify await is unbounded on the critical path), correctness CONCERNS(4).
|
||||||
|
r2 → r3 after the confirmation (robustness CONCERNS(4)). Precedence
|
||||||
|
r3 > r2 > r1. Contract `.claude/tasks/contracts/2026-10-11-model-router-
|
||||||
|
w4b-classifier-1858.md`. Base: feature/model-router-w4 at 86845fc.
|
||||||
|
Corrected facts: default keyword rules never run mid-turn and only write
|
||||||
|
plan/reflect (they never lower); `mainEffort` ranks sticky > turn > run >
|
||||||
|
floor > engine and `mainModel` ranks turn above floor; `runMain` holds a
|
||||||
|
typed best-tier skill's route across the run's turns; `$` calls do not
|
||||||
|
count against the hook budget (an unbounded `classify` await freezes the
|
||||||
|
prompt), while `$.clock.sleep` does; `ClassifyOptions` has `model` only;
|
||||||
|
`claude -p` prompts arrive with origin `sdk`; the breaker is fed by
|
||||||
|
`classic.StopFailure` only; `warnOnce` fires once per session; config
|
||||||
|
switches (`enabled`, `verbose`, `mainModelSwitch`) are Config keys read
|
||||||
|
through `pickSwitch` with session toggles; `confirmPhase` (T3) is
|
||||||
|
confirm-only, so a dialog on a classified phase would decide nothing.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- D1 no first-use dialog on classified phases (Later and Keep would apply
|
||||||
|
the same route). Visibility = the status line (`route: classifier
|
||||||
|
<phase>`) and the W4-A transition line (needs `/route verbose on`).
|
||||||
|
- D2 `classify` is a Config key (default `true`), read from the machine
|
||||||
|
override through `pickSwitch` (invalid → default + one log), with a
|
||||||
|
session toggle `/route classify on|off` next to `switch`/`verbose`,
|
||||||
|
printed on the `router:` line of `/route show`. Persistent off =
|
||||||
|
`"classify": false` in `~/.claude/model-router.json`. routing.json is
|
||||||
|
NOT touched; no new writer.
|
||||||
|
- D3 the classifier fills only an EMPTY route: it runs only when no route
|
||||||
|
is in force on main (`mainRoute(st) === null && st.turnFloor === null`:
|
||||||
|
no sticky `/route`, no turn route from a rule/skill/tool, no run slot
|
||||||
|
from a typed skill, no floor), evaluated after `routeFromPrompt`. Real
|
||||||
|
coverage, stated in the docs: a typed best-tier skill (feat, bugfix,
|
||||||
|
ship-feature…) holds its run slot across turns, so nothing is
|
||||||
|
classified until `/route clear`, `/route off`, a user `/model` or a
|
||||||
|
typed non-best skill; a non-best skill's row is turn-scoped, so its
|
||||||
|
follow-up prompts ARE classified (accepted: those rows are low/medium
|
||||||
|
and a complex follow-up may rightly rise). Never mid-turn (`e.turnId !==
|
||||||
|
undefined` → skip, as the rules). Never for a non-composer origin (so
|
||||||
|
never under `-p`). Never for a sub-agent prompt (prompt.submit is main
|
||||||
|
only; a test proves an agent's prompt is not classified).
|
||||||
|
- D4 a classified route carries the EFFORT ONLY: `turnMain = { phase,
|
||||||
|
route: { effort: cfg.phases[phase].effort }, source: 'classifier' }`.
|
||||||
|
No tier, so the main model never moves because of a guess (no cold
|
||||||
|
context re-reads on opus/sonnet sessions, no cost surprise). It may
|
||||||
|
lower the effort (coding = medium, bookkeeping = low); turn-scoped.
|
||||||
|
An explicit user `/effort` beats it: `st.engineBase` = the engine
|
||||||
|
effort of the first main step of the session (kept across /clear);
|
||||||
|
at `turn.step`, when `e.effort !== st.engineBase` (the user moved the
|
||||||
|
builtin effort) a `classifier`-sourced turn route is ignored for that
|
||||||
|
step (the engine effort applies) — "un choix explicite bat la phase
|
||||||
|
déduite" (BDR-115 amendment b).
|
||||||
|
- D5 bounded call: `label = await Promise.race([classify, $.clock.sleep(
|
||||||
|
CLASSIFY_MS = 3000, { signal: next.signal }).then(() => TIMEOUT)])`
|
||||||
|
inside try/catch: in the catch, `next.signal.aborted` is tested FIRST
|
||||||
|
(a user interrupt: no count, no write, return); a TIMEOUT counts as a
|
||||||
|
failure (D7) and gives no route; after the await every guard of D8 is
|
||||||
|
RE-CHECKED (`st.off`, the switch, no route in force, not paused) before
|
||||||
|
the write, and `next.signal.aborted` skips it (a late result never
|
||||||
|
lands in a later turn). The losing classify promise is left to settle
|
||||||
|
(the kit offers no cancel); stated.
|
||||||
|
- D6 labels are descriptive and mapped: `design` → plan, `analysis` →
|
||||||
|
reflect, `coding` → implement, `bookkeeping` → apply, `chat` → none
|
||||||
|
(lookup with `hasKey`; a mapped phase without an effort → no route).
|
||||||
|
The call passes no `model` option (the engine's small fast model; no
|
||||||
|
alias from a hook, LRN-203). The sample = the whole trimmed text when
|
||||||
|
≤ `CLASSIFY_MAX = 2000` code points, else the first `CLASSIFY_HEAD =
|
||||||
|
500` + last `CLASSIFY_TAIL = 1500` code points (cut by code points,
|
||||||
|
never a lone surrogate). A pasted tail can steer the label for one
|
||||||
|
turn (effort only, no file write): accepted and documented.
|
||||||
|
- D7 failures: a rejected call (classify's own `aborted` cause excepted)
|
||||||
|
or a TIMEOUT increments `st.classifyFails`; a settled answer resets it;
|
||||||
|
at `CLASSIFY_FAILS = 3` the classifier pauses until `clock.now() +
|
||||||
|
cooldownMinutes * 60000` (`st.classifyUntil`) with one toast
|
||||||
|
"classifier paused <n> min (3 failures)" and the status marker
|
||||||
|
`classifier: paused`; both fields are in `resetSession`'s kept set and
|
||||||
|
cleared by `/route reload` (like the breaker); the expiry check runs
|
||||||
|
BEFORE the route-in-force guard (the marker never stays stale); an
|
||||||
|
unknown label neither warns nor counts; the classifier is skipped while
|
||||||
|
`cfg.models.haiku` (a stated proxy for the engine's small model) is in
|
||||||
|
`st.down`, and when the main loop itself runs on haiku (the effort is
|
||||||
|
stripped there anyway).
|
||||||
|
- D8 guards summary (in order): `st.off` → `cfg.classify` (+ toggle) →
|
||||||
|
composer → `e.turnId === undefined` → not a slash → trimmed length ≥ 12
|
||||||
|
→ no rule matched this prompt → no route in force → not paused, model
|
||||||
|
not down → classify.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
- [ ] S1 config: `classify` key (DEFAULT true, pickSwitch), toggle,
|
||||||
|
`/route classify on|off`, show `router:` line, argumentHint.
|
||||||
|
- [ ] S2 `classifyPrompt($, st, e, next)` in the prompt.submit hook after
|
||||||
|
`routeFromPrompt` and the typed marker; `LABELS`/`LABEL_PHASE`
|
||||||
|
tables; the sample helper; the race; the failure counter and pause;
|
||||||
|
the effort-only route write with source `classifier`; `Source`
|
||||||
|
gains `'classifier'`; `slotWord` maps it to `classifier`; the
|
||||||
|
status line already prints `route: <source> <phase>`.
|
||||||
|
- [ ] S3 docs: README (classifier paragraph: what, one small-model call
|
||||||
|
per typed prompt when no route is in force, effort only, `/route
|
||||||
|
classify off` or `"classify": false` per machine, where to see it:
|
||||||
|
status line, `/route verbose on`); the `/route` subcommand lists
|
||||||
|
(README ×2, USAGE) gain `classify on|off`; the machine keys lists
|
||||||
|
(README, USAGE) gain `classify`; USAGE "Niveau d'effort" one
|
||||||
|
sentence; CHANGELOG Unreleased Added; `lib/effort-shift.md` ≤ 2
|
||||||
|
lines (the plain conversation routes by content now).
|
||||||
|
- [ ] S4 kit tests (a bottom `model.classify` mock; `$.clock` mock for
|
||||||
|
the race), one per clause: called once with the head+tail sample
|
||||||
|
and the five labels, no model option; each label → its phase effort
|
||||||
|
only (no tier on the route) and `chat`/`undefined` → none; never
|
||||||
|
resolving → no route after 3 s, prompt proceeds; rejection ×3 →
|
||||||
|
pause + toast + status marker, then no call during the cooldown,
|
||||||
|
a success resets the counter; `aborted` → no count, no warn; small
|
||||||
|
model down → not called; not called: short text, slash, sdk origin,
|
||||||
|
mid-turn (`turnId`), rule matched (keyword and ultrathink), sticky
|
||||||
|
`/route`, run slot held, floor carried, `classify` off (config,
|
||||||
|
toggle), mod off, an agent's prompt; turn-scoped (gone at
|
||||||
|
turn.complete); effort lowered to low for bookkeeping; the main
|
||||||
|
model unchanged on a sonnet session for `design`; `/route show`
|
||||||
|
prints `classify: on`; status line `route: classifier reflect`;
|
||||||
|
W4-A line "(reflect: classifier)"; invalid config value; 3 timeouts
|
||||||
|
→ pause; a dispatch abort mid-race → no count, no write; a `/route
|
||||||
|
<phase>` landing during the await → the classifier result dropped;
|
||||||
|
pause expiry at cooldown-1 ms (still paused) and +1 ms (resumes);
|
||||||
|
`/clear` keeps the pause, `/route reload` clears it; a user
|
||||||
|
`/effort` change (engine effort ≠ baseline) → the classified effort
|
||||||
|
ignored at the step; a 2000-code-point text sent whole, a longer
|
||||||
|
one head+tail with no lone surrogate; `constructor` label → none;
|
||||||
|
a phase without effort → none; main on haiku → not called; a
|
||||||
|
non-best skill's follow-up prompt classified; a plain prompt after
|
||||||
|
a finished best-tier run not classified until `/route clear`.
|
||||||
|
- Edge cases: a prompt that is only a URL or code (the classifier
|
||||||
|
decides; no special-casing); every classified prompt waits one small-
|
||||||
|
model round trip (typically under a second, 3 s at most: stated in the
|
||||||
|
README and measured in the AC9 live check); a `[1m]` session (no
|
||||||
|
effect: effort only).
|
||||||
|
- Disposition: honors BDR-115 (no alias from a hook, no model write, no
|
||||||
|
file write), BDR-116 (writers unchanged; no new dialog), LRN-203,
|
||||||
|
LRN-210, LRN-212 (live mod: the first plain prompt after the feater
|
||||||
|
saves is classified).
|
||||||
|
- Docs note for the user: a misclassification lowers one turn's effort
|
||||||
|
only; `/route <phase>` or `ultrathink` overrides it at once.
|
||||||
|
|
||||||
|
## Challenge ledger (r1 → r2)
|
||||||
|
- simp 1, corr 3/7, rob 6 (dialog decides nothing / misstates) → D1 dropped.
|
||||||
|
- simp 2, corr 11, rob 11 (flag plumbing, new writer) → D2 Config key.
|
||||||
|
- simp 3/4, corr 1/2/12, rob 3/4 (mid-turn, run slot, floor) → D3.
|
||||||
|
- rob 5 (model flips per turn) → D4 effort only.
|
||||||
|
- rob 1 BLOCKER, corr 9 (unbounded await, late write) → D5.
|
||||||
|
- simp 5, corr 4 (alias from a hook) → D6 no model option.
|
||||||
|
- corr 8, simp 8 (label words) → D6 descriptive labels mapped.
|
||||||
|
- rob 7 (head-only sample) → D6 head + tail.
|
||||||
|
- rob 2, corr 5 (failure handling, warn spent) → D7.
|
||||||
|
- corr 6, rob 9 (`-p`) → D3 never.
|
||||||
|
- corr 10 (sub-agent clause) → S4 test.
|
||||||
|
- simp 6, rob 10 (visibility) → status line + verbose note.
|
||||||
|
- simp 7 (doc lists) → S3 lists named.
|
||||||
|
|
||||||
|
## Confirmation ledger (r2 → r3)
|
||||||
|
- conf 1 (timeouts uncounted) → timeouts count toward the pause.
|
||||||
|
- conf 2 (abort lands in the counted branch) → `next.signal.aborted`
|
||||||
|
tested first in the catch.
|
||||||
|
- conf 3 (classifier overrides a user `/effort`) → `engineBase` guard at
|
||||||
|
the step: an explicit builtin effort beats a classified route.
|
||||||
|
- conf 4 (run coverage) → D3 states the real coverage; docs name it.
|
||||||
|
- conf 5 (guards not re-checked after the await) → re-checked.
|
||||||
|
- conf 6 (pause bookkeeping) → ×60000, kept across /clear, cleared on
|
||||||
|
reload, expiry checked first, boundary tests.
|
||||||
|
- conf 7 (sample, labels) → whole text ≤ 2000 cp, code-point cuts,
|
||||||
|
hasKey, no-effort phase skipped.
|
||||||
|
- conf 8 (down check) → haiku id as a stated proxy; skip on a haiku main.
|
||||||
|
- conf 9 (pasted tail) → accepted, documented.
|
||||||
|
- conf 10 (latency) → stated in the README and measured live.
|
||||||
@@ -11,7 +11,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and this project
|
|||||||
- **model-router first-use confirmation**: `mods/model-router/routing.json` (tracked) is the single source of the phase table and of every skill and agent row, and keeps the decisions: `confirmed`, `changed` (`from`/`to`), `projects` exceptions keyed by the origin remote reduced to `host/path` (credentials and local paths never stored), and `ask`. The first use of a rowed typed skill, a rowed agent spawn or a main-loop phase declared through the `route` tool opens a dialog with context: the skill's description (first sentence of its `SKILL.md` frontmatter) or the agent's, the phase with its `about` line (a new field on each of the 11 phases) and the model id and effort the next step really runs on. A row offers Later, Keep or Change; a main-loop phase Later or Keep. Change asks the model (fable, opus, sonnet, haiku with their tier), then the effort among those the phases of that model use (skipped when there is only one), then Everywhere or This project only; the pair maps to an existing phase (rows stay phase names, the same phase counts as Keep, a pair no phase offers is added by hand as a new phase). A skill-row change toasts the model and effort it now runs on, with the `/route switch on` hint when the main loop holds back a downgrade; a free-text answer counts as Later. One dialog at a time, never in headless (`-p`) runs or inside a sub-agent. Only a dialog answer, `/route ask on|off` or `/route forget` (which takes decisions back; a changed row returns to its shipped phase, the frontmatter floors stay yours to realign) writes the file (serialized, 64 KiB cap, never created when absent); each write asks you to commit it from the config repo. `/route pending` lists unconfirmed rows and phases. Layers: routing.json < `~/.claude/model-router.json` (its `ask` wins); a project's `.claude/model-router.json` is never read. Tests: `mods/model-router/hooks/register.test.ts`.
|
- **model-router first-use confirmation**: `mods/model-router/routing.json` (tracked) is the single source of the phase table and of every skill and agent row, and keeps the decisions: `confirmed`, `changed` (`from`/`to`), `projects` exceptions keyed by the origin remote reduced to `host/path` (credentials and local paths never stored), and `ask`. The first use of a rowed typed skill, a rowed agent spawn or a main-loop phase declared through the `route` tool opens a dialog with context: the skill's description (first sentence of its `SKILL.md` frontmatter) or the agent's, the phase with its `about` line (a new field on each of the 11 phases) and the model id and effort the next step really runs on. A row offers Later, Keep or Change; a main-loop phase Later or Keep. Change asks the model (fable, opus, sonnet, haiku with their tier), then the effort among those the phases of that model use (skipped when there is only one), then Everywhere or This project only; the pair maps to an existing phase (rows stay phase names, the same phase counts as Keep, a pair no phase offers is added by hand as a new phase). A skill-row change toasts the model and effort it now runs on, with the `/route switch on` hint when the main loop holds back a downgrade; a free-text answer counts as Later. One dialog at a time, never in headless (`-p`) runs or inside a sub-agent. Only a dialog answer, `/route ask on|off` or `/route forget` (which takes decisions back; a changed row returns to its shipped phase, the frontmatter floors stay yours to realign) writes the file (serialized, 64 KiB cap, never created when absent); each write asks you to commit it from the config repo. `/route pending` lists unconfirmed rows and phases. Layers: routing.json < `~/.claude/model-router.json` (its `ask` wins); a project's `.claude/model-router.json` is never read. Tests: `mods/model-router/hooks/register.test.ts`.
|
||||||
- **Manual-push mode**: `git config gitflow.autopush false` (human-set) now stops every push the gitflow lib makes, not only the post-commit / post-merge hooks. `gitflow start` and `finish` branch, commit and merge locally and push nothing; `gitflow delete` leaves the `origin/` copy in place and prints `git push origin --delete <br>` for the user to run. `hooks/unpushed-guard.sh` stays silent at turn end in this mode and opens each session with one `ℹ manual push mode:` line counting the commits no remote holds across every local branch; an unparseable or unreadable `gitflow.autopush` value is treated as manual push mode too, and that line names it. `hooks/push-guard.sh` (PreToolUse, `Bash|Monitor`) refuses any `git push` Claude types while `gitflow.autopush` reads false in the session cwd or in a literal `-C`/`cd` directory the command names (global config counts outside a repo); the refusal tells the user to run it with `! git push`. It reads the mode through the same lib verb as every other reader and fails closed: an unparseable or unreadable value reads as manual, and an internal error, a missing `lib/gitflow.sh`, more than 20 distinct directory tokens in one command (capped before any token is classified), a `cd`/`-C` directory token mixing quoted and unquoted parts, or a payload jq cannot parse whose raw text looks like a push refuse the push (these pathological cases fire in auto mode too). Directory tokens are read as whole shell words, adjacent quoted segments and backslash escapes included. In manual mode it over-blocks any command where a `push` word follows a `git` token; the misses listed in its header fall to a new `autoMode.soft_deny` rule that no request in the turn clears. The session banner adds `🔒 push : manual (autopush=false) — ! git push` when the key reads false, and `🔒 push : manual (autopush bad) — ! git push` when the value is invalid. Skills read the mode through a new lib verb, `bash ~/.claude/lib/gitflow.sh push-mode`: it prints `auto`, `manual` or `invalid` (rc 0) and names an invalid value on stderr (printable characters only, 64 at most). It is the one reader a skill may call, since the `git config` read of the key is denied to Claude. Skills push nothing on their own, except the `/release-candidate` tag in auto-push mode on an explicit go. Every "on origin" or "not pushed" line they print comes from `git rev-list --count origin/<br>..<br>` read after the fact, with the complete `! git …` command when something is left for the user to push. An invalid value (anything but unset, true or false, or a read that fails) is manual push mode for every reader and is named where it is read (see Fixed). Tests: `lib/gitflow-test.sh` T11b (push-mode verb), T18m and T18q blocks, `lib/tests/unpushed-guard.test.sh` T10-T16, `lib/tests/push-guard.test.sh` (98 checks).
|
- **Manual-push mode**: `git config gitflow.autopush false` (human-set) now stops every push the gitflow lib makes, not only the post-commit / post-merge hooks. `gitflow start` and `finish` branch, commit and merge locally and push nothing; `gitflow delete` leaves the `origin/` copy in place and prints `git push origin --delete <br>` for the user to run. `hooks/unpushed-guard.sh` stays silent at turn end in this mode and opens each session with one `ℹ manual push mode:` line counting the commits no remote holds across every local branch; an unparseable or unreadable `gitflow.autopush` value is treated as manual push mode too, and that line names it. `hooks/push-guard.sh` (PreToolUse, `Bash|Monitor`) refuses any `git push` Claude types while `gitflow.autopush` reads false in the session cwd or in a literal `-C`/`cd` directory the command names (global config counts outside a repo); the refusal tells the user to run it with `! git push`. It reads the mode through the same lib verb as every other reader and fails closed: an unparseable or unreadable value reads as manual, and an internal error, a missing `lib/gitflow.sh`, more than 20 distinct directory tokens in one command (capped before any token is classified), a `cd`/`-C` directory token mixing quoted and unquoted parts, or a payload jq cannot parse whose raw text looks like a push refuse the push (these pathological cases fire in auto mode too). Directory tokens are read as whole shell words, adjacent quoted segments and backslash escapes included. In manual mode it over-blocks any command where a `push` word follows a `git` token; the misses listed in its header fall to a new `autoMode.soft_deny` rule that no request in the turn clears. The session banner adds `🔒 push : manual (autopush=false) — ! git push` when the key reads false, and `🔒 push : manual (autopush bad) — ! git push` when the value is invalid. Skills read the mode through a new lib verb, `bash ~/.claude/lib/gitflow.sh push-mode`: it prints `auto`, `manual` or `invalid` (rc 0) and names an invalid value on stderr (printable characters only, 64 at most). It is the one reader a skill may call, since the `git config` read of the key is denied to Claude. Skills push nothing on their own, except the `/release-candidate` tag in auto-push mode on an explicit go. Every "on origin" or "not pushed" line they print comes from `git rev-list --count origin/<br>..<br>` read after the fact, with the complete `! git …` command when something is left for the user to push. An invalid value (anything but unset, true or false, or a read that fails) is manual push mode for every reader and is named where it is read (see Fixed). Tests: `lib/gitflow-test.sh` T11b (push-mode verb), T18m and T18q blocks, `lib/tests/unpushed-guard.test.sh` T10-T16, `lib/tests/push-guard.test.sh` (98 checks).
|
||||||
|
|
||||||
|
- **model-router prompt classifier**: a plain prompt typed in the composer (12 characters or more, not a slash command, not mid-turn, never under `-p` or in a sub-agent) that no keyword rule, skill, run slot, `/route` or floor covers is labelled by one call to the engine's small model (`design`, `analysis`, `coding`, `bookkeeping`, `chat`), bounded to 3 s. The label maps to plan, reflect, implement or apply and that phase's effort becomes the turn's default: effort only, no model move, no dialog, no file write, gone at the end of the turn, replaced by any route call or typed skill, and beaten by a `/effort` you set yourself. Three failures in a row pause it for `cooldownMinutes` (one toast, `classifier: paused` in the status line). `"classify": false` in `~/.claude/model-router.json` and `/route classify on|off` turn it off; `/route show` prints `classify:`. The status line reads `route: classifier <phase>` and `/route verbose on` adds `(<phase>: classifier)`.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- model-router `verbose: true` logs transitions only: one line when main changes model or effort (`model-router: main: fable/high → fable/xhigh (plan: route tool)`), one hand-off line per dispatch and one per later effort move of an agent, each with its cause; the per-step, "answered by", spawn, route-tool and skill-path lines are gone. An agent answering on a model other than the planned one is said once. A non-boolean `verbose` falls back to `false`, said once.
|
||||||
- `security-auditor` runs on the `judge` row (opus/xhigh) by a first-use decision; its frontmatter floor is aligned (`model: opus`).
|
- `security-auditor` runs on the `judge` row (opus/xhigh) by a first-use decision; its frontmatter floor is aligned (`model: opus`).
|
||||||
- `lib/model-gate.md` calls the model-router `route` tool and takes its answer as the witness; with the mod off the gate stops and names `/route on`. The `model:` / `effort:` frontmatter of agents and skills is now the off-state floor, census-locked equal to the rows (`lib/tests/effort-routing.test.sh`; a row changed through the first-use dialog passes with a `WARN floor drift` line while its frontmatter still holds the shipped value). `analyzer` effort goes from high to xhigh. Built-in judgment dispatches carry an explicit `effort=`.
|
- `lib/model-gate.md` calls the model-router `route` tool and takes its answer as the witness; with the mod off the gate stops and names `/route on`. The `model:` / `effort:` frontmatter of agents and skills is now the off-state floor, census-locked equal to the rows (`lib/tests/effort-routing.test.sh`; a row changed through the first-use dialog passes with a `WARN floor drift` line while its frontmatter still holds the shipped value). `analyzer` effort goes from high to xhigh. Built-in judgment dispatches carry an explicit `effort=`.
|
||||||
- `settings.json` denies every write form of the human-only `gitflow.*` keys (18 entries): any `git … config` spelling, section remove/rename, `git -c`, the git config env overrides, and Edit/Write of `.git/config`, `.gitconfig` and `~/.config/git/config`. Side effect: Claude can no longer read `gitflow.autopush` through `git config` either; hooks and `lib/gitflow.sh` still read it. The `hard_deny` rule on routing around a guardrail now names PreToolUse hook refusals.
|
- `settings.json` denies every write form of the human-only `gitflow.*` keys (18 entries): any `git … config` spelling, section remove/rename, `git -c`, the git config env overrides, and Edit/Write of `.git/config`, `.gitconfig` and `~/.config/git/config`. Side effect: Claude can no longer read `gitflow.autopush` through `git config` either; hooks and `lib/gitflow.sh` still read it. The `hard_deny` rule on routing around a guardrail now names PreToolUse hook refusals.
|
||||||
@@ -25,6 +28,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and this project
|
|||||||
- The five `effort-low` … `effort-max` skills, `lib/effort-pins.txt`, `lib/effort-pins.sh` and `lib/model-check.sh`, with their tests and the effort-pin re-apply steps of `install-plugins.sh` and `update-all.sh`. The model-router rows replace them. A typed `/effort-<level>` no longer exists: use `ultrathink` or `/route effort=max`. Breaking: the next release is 3.0.0.
|
- The five `effort-low` … `effort-max` skills, `lib/effort-pins.txt`, `lib/effort-pins.sh` and `lib/model-check.sh`, with their tests and the effort-pin re-apply steps of `install-plugins.sh` and `update-all.sh`. The model-router rows replace them. A typed `/effort-<level>` no longer exists: use `ultrathink` or `/route effort=max`. Breaking: the next release is 3.0.0.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- model-router residuals of the confirmation waves: a failed `/route forget` or `/route ask` write logs its cause and answers a generic text; forget answers clean every file-sourced name, phase and key; a restore names a frontmatter file only for a valid skill name and refuses a `__proto__` row; `/route forget` never restores `changed.phases`; the dialog's Change offers and writes shipped phases only (a machine-only phase never reaches routing.json, and an alias no shipped phase uses says so); a failure after the user answered keeps the key asked instead of asking again at every use.
|
||||||
- `gitflow delete` (and `finish`) land on the base that contains the branch and drop the branch's upstream before `git branch -d`, so a branch whose upstream lags (manual-push mode) is deleted instead of refused by git (T18k).
|
- `gitflow delete` (and `finish`) land on the base that contains the branch and drop the branch's upstream before `git branch -d`, so a branch whose upstream lags (manual-push mode) is deleted instead of refused by git (T18k).
|
||||||
- An invalid `gitflow.autopush` value (not a boolean, or a config read that fails) no longer pushes. The post-commit / post-merge hooks and every push site of `lib/gitflow.sh` (`start`, `finish`, the `origin/` cleanup of `delete`) read it as auto and pushed; they now push nothing and say why. Each hook run prints `gitflow post-commit: gitflow.autopush unreadable (git rc <n>) — NOT pushed, treated as manual push mode; fix the value by hand` (post-merge likewise), and the lib passes through the `push-mode` verb's line, `gitflow.sh push-mode: gitflow.autopush='<value>' is not a boolean (git rc <n>)`. A repo with its own committed `.githooks/` (onboarded projects) keeps running its old hooks, which still push on an invalid value, until a session start runs `reconcile-hooks` and rewrites them; commit that refresh so other clones get it. Tests: `lib/gitflow-test.sh` T18q block.
|
- An invalid `gitflow.autopush` value (not a boolean, or a config read that fails) no longer pushes. The post-commit / post-merge hooks and every push site of `lib/gitflow.sh` (`start`, `finish`, the `origin/` cleanup of `delete`) read it as auto and pushed; they now push nothing and say why. Each hook run prints `gitflow post-commit: gitflow.autopush unreadable (git rc <n>) — NOT pushed, treated as manual push mode; fix the value by hand` (post-merge likewise), and the lib passes through the `push-mode` verb's line, `gitflow.sh push-mode: gitflow.autopush='<value>' is not a boolean (git rc <n>)`. A repo with its own committed `.githooks/` (onboarded projects) keeps running its old hooks, which still push on an invalid value, until a session start runs `reconcile-hooks` and rewrites them; commit that refresh so other clones get it. Tests: `lib/gitflow-test.sh` T18q block.
|
||||||
|
|
||||||
|
|||||||
@@ -135,12 +135,13 @@ effort, never version. Transcript audit `python3 lib/effort-audit.py`.
|
|||||||
- Main loop: every request gets the route in force. A typed skill with a row routes the main loop to it; a best-tier row (`plan`, `reflect`, `orchestrate`, `escalate`) holds across turns in a run slot until `/route clear`, `/route off`, a user `/model` or a typed skill on a non-best row. A skill without a row leaves the route as it is.
|
- Main loop: every request gets the route in force. A typed skill with a row routes the main loop to it; a best-tier row (`plan`, `reflect`, `orchestrate`, `escalate`) holds across turns in a run slot until `/route clear`, `/route off`, a user `/model` or a typed skill on a non-best row. A skill without a row leaves the route as it is.
|
||||||
- Sub-agents: a routed agent gets its row's model at spawn, within its tier and only upward from its frontmatter model, and the row's effort on every step. Explicit `model` / `effort` params on the Agent call win; a project-defined agent of the same name keeps its own definition. Built-ins: Explore runs on sonnet/medium, Plan on opus/xhigh.
|
- Sub-agents: a routed agent gets its row's model at spawn, within its tier and only upward from its frontmatter model, and the row's effort on every step. Explicit `model` / `effort` params on the Agent call win; a project-defined agent of the same name keeps its own definition. Built-ins: Explore runs on sonnet/medium, Plan on opus/xhigh.
|
||||||
- Levers inside a run: `ultrathink` in a prompt sets the turn's minimum effort; `/route effort=max` holds until `/route clear`. The built-in `/effort` is not a lever inside a run, rows and routes outrank it.
|
- Levers inside a run: `ultrathink` in a prompt sets the turn's minimum effort; `/route effort=max` holds until `/route clear`. The built-in `/effort` is not a lever inside a run, rows and routes outrank it.
|
||||||
- `/route` (user command) shows or sets the route: `show`, `clear`, `off`, `on`, `reload`, `pending` (rows and phases not confirmed yet), `ask on|off` (first-use dialog on or off), `forget <name|all|projects>` (takes decisions back: a changed row returns to its shipped phase; `all` and `projects` ask first), a phase name, `model=<alias|id> effort=<level>`, `switch on|off`, `verbose on|off`. `/route show` names the run slot when one holds (`main: run <phase>`). The model sets routes through a `route` tool.
|
- Prompt classifier: a plain prompt typed in the composer (12 characters or more, not a slash command) that no keyword rule, skill, run slot, `/route` or floor covers is sent to the engine's small fast model, one call, bounded to 3 seconds (the prompt waits for it, typically under a second). The label (`design`, `analysis`, `coding`, `bookkeeping`, `chat`) maps to plan, reflect, implement or apply (`chat` routes nothing), and that phase's effort becomes the turn's default. Effort only: the main model never moves because of a guess, no dialog opens and no file is written. The route is gone at the end of the turn, a later route call or typed skill replaces it, and a `/effort` you set yourself beats it. A typed best-tier skill holds its run slot across turns, so nothing is classified there until `/route clear`. Long prompts are sampled (first 500 and last 1500 code points), so pasted text at the end can steer one turn's effort. Three failures in a row pause it for `cooldownMinutes` (one toast, `classifier: paused` in the status line). Never in a sub-agent, mid-turn or under `-p`. Off per session with `/route classify off`, per machine with `"classify": false`. The status line reads `route: classifier <phase>`; `/route verbose on` adds the transition line.
|
||||||
|
- `/route` (user command) shows or sets the route: `show`, `clear`, `off`, `on`, `reload`, `pending` (rows and phases not confirmed yet), `ask on|off` (first-use dialog on or off), `forget <name|all|projects>` (takes decisions back: a changed row returns to its shipped phase; `all` and `projects` ask first), a phase name, `model=<alias|id> effort=<level>`, `switch on|off`, `classify on|off` (the prompt classifier, below), `verbose on|off` (logs transitions only: each change of model or effort, with a few words of cause). `/route show` names the run slot when one holds (`main: run <phase>`). The model sets routes through a `route` tool.
|
||||||
- First use: the first time a rowed skill is typed, a rowed agent is spawned or a phase is declared on the main loop through the `route` tool, the mod asks once whether the route is right. The question gives context: the skill's description (first sentence of its `SKILL.md` frontmatter) or the agent's, the phase with its `about` line, and the model id and effort the next step really runs on; a declared phase also says what uses it (rows, prompt rules). A row offers Later, Keep or Change; a declared phase offers Later or Keep. Change asks the model (fable, opus, sonnet, haiku, each shown with the tier it heads), then the effort among those the phases on that model use (shipped table: fable medium, high, xhigh or max; sonnet low, medium, high or xhigh; opus and haiku have one level each, so no question), then Everywhere or This project only. Rows stay phase names, so the pair maps to an existing phase: the row's current phase wins a tie and the same phase counts as Keep; a pair no phase offers needs a new phase added by hand in routing.json. Everywhere moves the row and records the shipped phase under `changed`; This project only stores an exception under `projects`, keyed by the origin remote reduced to `host/path` (no credentials, no local paths; a remote that cannot be read that way offers no project choice). Keep lands under `confirmed`; Later, a dismissed dialog or a free-text answer asks again next session. After a skill row changes, a toast names the model and effort that skill now runs on, plus the `/route switch on` hint when the main loop holds back a downgrade. One dialog at a time, never in a headless (`-p`) run, never inside a sub-agent.
|
- First use: the first time a rowed skill is typed, a rowed agent is spawned or a phase is declared on the main loop through the `route` tool, the mod asks once whether the route is right. The question gives context: the skill's description (first sentence of its `SKILL.md` frontmatter) or the agent's, the phase with its `about` line, and the model id and effort the next step really runs on; a declared phase also says what uses it (rows, prompt rules). A row offers Later, Keep or Change; a declared phase offers Later or Keep. Change asks the model (fable, opus, sonnet, haiku, each shown with the tier it heads), then the effort among those the phases on that model use (shipped table: fable medium, high, xhigh or max; sonnet low, medium, high or xhigh; opus and haiku have one level each, so no question), then Everywhere or This project only. Rows stay phase names, so the pair maps to an existing phase: the row's current phase wins a tie and the same phase counts as Keep; a pair no phase offers needs a new phase added by hand in routing.json. Everywhere moves the row and records the shipped phase under `changed`; This project only stores an exception under `projects`, keyed by the origin remote reduced to `host/path` (no credentials, no local paths; a remote that cannot be read that way offers no project choice). Keep lands under `confirmed`; Later, a dismissed dialog or a free-text answer asks again next session. After a skill row changes, a toast names the model and effort that skill now runs on, plus the `/route switch on` hint when the main loop holds back a downgrade. One dialog at a time, never in a headless (`-p`) run, never inside a sub-agent.
|
||||||
- Only a dialog answer, `/route ask on|off` or `/route forget` writes `routing.json`, never the model. Writes are serialized, capped at 64 KiB, and refused when the file is missing (it is never created). Each write leaves the config repo dirty; a toast reminds you to commit it from there.
|
- Only a dialog answer, `/route ask on|off` or `/route forget` writes `routing.json`, never the model. Writes are serialized, capped at 64 KiB, and refused when the file is missing (it is never created). Each write leaves the config repo dirty; a toast reminds you to commit it from there.
|
||||||
- The spinner suffix and the status line under the prompt show the route in force.
|
- The spinner suffix and the status line under the prompt show the route in force.
|
||||||
|
|
||||||
Config layers: `mods/model-router/routing.json` (tracked: phases with their tier, effort and `about` line, rows, decisions, `ask`), then the optional per-machine `~/.claude/model-router.json`, which wins. A `.claude/model-router.json` inside a project is never read. Machine keys: `models` (alias → full id), `windows` (context window per full id), `tiers` (ordered alias lists per tier: `best` fable>opus>sonnet, `big` opus>fable>sonnet, `work` sonnet>opus, `cheap` haiku>sonnet; the first alias not down is used), `fallback` (the rank order of the aliases, best first, used by the breaker), `cooldownMinutes` (how long a model stays marked down after an availability error, 15 by default, doubling per episode up to 300), `mainUpgrade` (default `true`: the main loop may move up to a phase's tier), `upgradeMaxTokens` (default 200000: no main-loop upgrade above this context size, since an upgrade re-reads the whole context cold), `phases`, `agents`, `skills`, `prompt` (rules), `mainModelSwitch` (default `false`), `verbose` (default `false`), `spinner` (default `true`), `enabled` (default `true`; `false` turns the mod off on that machine), `ask` (overrides routing.json's `ask` on that machine). A `null` value in `agents` or `skills` drops a row. Edit phases and rows by hand in routing.json (a phase's `about`, 120 characters at most, is read from there only and shown in the dialog; a model and effort pair no phase offers needs a new phase there); without it the mod runs the code's default phases, with no rows and no dialog. `/route reload` re-reads both files.
|
Config layers: `mods/model-router/routing.json` (tracked: phases with their tier, effort and `about` line, rows, decisions, `ask`), then the optional per-machine `~/.claude/model-router.json`, which wins. A `.claude/model-router.json` inside a project is never read. Machine keys: `models` (alias → full id), `windows` (context window per full id), `tiers` (ordered alias lists per tier: `best` fable>opus>sonnet, `big` opus>fable>sonnet, `work` sonnet>opus, `cheap` haiku>sonnet; the first alias not down is used), `fallback` (the rank order of the aliases, best first, used by the breaker), `cooldownMinutes` (how long a model stays marked down after an availability error, 15 by default, doubling per episode up to 300), `mainUpgrade` (default `true`: the main loop may move up to a phase's tier), `upgradeMaxTokens` (default 200000: no main-loop upgrade above this context size, since an upgrade re-reads the whole context cold), `phases`, `agents`, `skills`, `prompt` (rules), `mainModelSwitch` (default `false`), `classify` (default `true`; `false` stops the prompt classifier on that machine), `verbose` (default `false`; `true` logs transitions only, a non-boolean value falls back to `false`), `spinner` (default `true`), `enabled` (default `true`; `false` turns the mod off on that machine), `ask` (overrides routing.json's `ask` on that machine). A `null` value in `agents` or `skills` drops a row. Edit phases and rows by hand in routing.json (a phase's `about`, 120 characters at most, is read from there only and shown in the dialog; a model and effort pair no phase offers needs a new phase there); without it the mod runs the code's default phases, with no rows and no dialog. `/route reload` re-reads both files.
|
||||||
|
|
||||||
Limits: the main loop changes model only with `mainModelSwitch` on, and each switch into another model costs one cold-cache step. The hooks send full model ids, so the `models` table has to follow new model versions.
|
Limits: the main loop changes model only with `mainModelSwitch` on, and each switch into another model costs one cold-cache step. The hooks send full model ids, so the `models` table has to follow new model versions.
|
||||||
|
|
||||||
@@ -234,7 +235,7 @@ a different package, ships its own conflicting `graphify` bin) — see
|
|||||||
| `/profile` | Activate a skill profile (web / seo / web-full / full / max / backend / design / dev / qa / audit / minimal) (default: full) |
|
| `/profile` | Activate a skill profile (web / seo / web-full / full / max / backend / design / dev / qa / audit / minimal) (default: full) |
|
||||||
| `/tour` | Grouped all-axes sweep — cleanup + security + reconcile + doc, fix and loop until clean |
|
| `/tour` | Grouped all-axes sweep — cleanup + security + reconcile + doc, fix and loop until clean |
|
||||||
| `/site-motion` | Site-level motion: scroll engine choice, page transitions, pin/scrub sequencing across a page or Astro route (design stack) |
|
| `/site-motion` | Site-level motion: scroll engine choice, page transitions, pin/scrub sequencing across a page or Astro route (design stack) |
|
||||||
| `/route` | model-router mod: show or set the main-loop route (show, clear, off, on, reload, pending, ask on\|off, <phase>, model=… effort=…, switch on\|off, verbose on\|off) |
|
| `/route` | model-router mod: show or set the main-loop route (show, clear, off, on, reload, pending, ask on\|off, <phase>, model=… effort=…, switch on\|off, classify on\|off, verbose on\|off: transitions only) |
|
||||||
|
|
||||||
> This table lists personal skills. Gstack skills (investigate, review, retro,
|
> This table lists personal skills. Gstack skills (investigate, review, retro,
|
||||||
> office-hours, cso…) and marketplace plugins add many more — run
|
> office-hours, cso…) and marketplace plugins add many more — run
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ Tu veux...
|
|||||||
| `/pdf-translate` | Traduire un PDF vers une autre langue | Sortie HTML fidèle (images, layout, style préservés) |
|
| `/pdf-translate` | Traduire un PDF vers une autre langue | Sortie HTML fidèle (images, layout, style préservés) |
|
||||||
| `/impeccable` | Audit/polish design + détecteur anti-slop déterministe | 23 verbes ; `npx impeccable detect` (exit 0/2) |
|
| `/impeccable` | Audit/polish design + détecteur anti-slop déterministe | 23 verbes ; `npx impeccable detect` (exit 0/2) |
|
||||||
| `/tour` | Sweep groupé sur un ou plusieurs projets | Sécu + nettoyage + reconcile + doc, boucle jusqu'à un pass propre |
|
| `/tour` | Sweep groupé sur un ou plusieurs projets | Sécu + nettoyage + reconcile + doc, boucle jusqu'à un pass propre |
|
||||||
| `/route` | Voir ou fixer la route du mod model-router | show / clear / off / on / reload / pending / ask on\|off / <phase> / model=… effort=… / switch on\|off / verbose on\|off |
|
| `/route` | Voir ou fixer la route du mod model-router | show / clear / off / on / reload / pending / ask on\|off / <phase> / model=… effort=… / switch on\|off / classify on\|off (classifieur de prompt) / verbose on\|off (transitions seules) |
|
||||||
| `/profile` | Changer le profil de skills | web / seo / web-full / full / max / backend / design / dev / qa / audit / minimal |
|
| `/profile` | Changer le profil de skills | web / seo / web-full / full / max / backend / design / dev / qa / audit / minimal |
|
||||||
|
|
||||||
> Cette table couvre les skills personnels principaux. Les plugins (gstack,
|
> Cette table couvre les skills personnels principaux. Les plugins (gstack,
|
||||||
@@ -187,7 +187,7 @@ l'outil `mcp__model-router__route` (`lib/effort-shift.md`). Les skills
|
|||||||
externes vendorés (pile design, superpowers, agent-skills, skills scroll
|
externes vendorés (pile design, superpowers, agent-skills, skills scroll
|
||||||
MengTo, 21st) ont aussi leur ligne.
|
MengTo, 21st) ont aussi leur ligne.
|
||||||
|
|
||||||
Taper un skill qui a une ligne route la boucle principale dessus. Une ligne du tier best (plan, reflect, orchestrate, escalate) tient d'un tour à l'autre pendant tout le run, jusqu'à `/route clear`, `/route off`, un `/model` tapé ou un skill d'un autre tier tapé. Pour relancer un tour bloqué : `ultrathink` dans le prompt (plancher du tour) ou `/route effort=max` (tient jusqu'à `/route clear`). Le `/effort` intégré n'a pas d'effet dans un run : les lignes et les routes passent devant. Les sous-agents reçoivent le modèle de leur ligne au lancement (dans leur tier, jamais en dessous de leur frontmatter) et son niveau à chaque étape ; un `model` ou `effort` explicite sur l'appel gagne. Explore tourne en sonnet/medium, Plan en opus/xhigh. `/route show` affiche la route en cours, slot de run compris (`main: run <phase>`). Première utilisation : la première fois qu'un skill avec ligne est tapé, qu'un agent avec ligne est lancé ou qu'une phase est déclarée sur la boucle principale via l'outil `route`, le mod demande une fois si la route convient. La question donne le contexte : la description du skill (première phrase du frontmatter de son `SKILL.md`) ou celle de l'agent, la phase avec sa ligne `about` (champ de la phase dans `routing.json`), puis le modèle et le niveau réels de l'étape suivante ; une phase déclarée dit aussi qui l'utilise. Pour une ligne : Later, Keep ou Change ; pour une phase déclarée : Later ou Keep. Change demande le modèle (fable, opus, sonnet, haiku, chacun avec son tier), puis le niveau parmi ceux des phases de ce modèle (fable : medium, high, xhigh ou max ; sonnet : low, medium, high ou xhigh ; opus et haiku n'en ont qu'un, la question est sautée), puis Everywhere ou This project only (exception rangée sous le remote origin du dépôt réduit à `host/path`, jamais d'identifiants). Une ligne reste un nom de phase : le couple modèle et niveau désigne une phase existante, et la même phase qu'avant vaut Keep. Un couple qu'aucune phase n'offre s'ajoute à la main comme nouvelle phase dans `routing.json`. Après un changement sur un skill, un toast donne le modèle et le niveau réels, et rappelle `/route switch on` quand la boucle principale refuse de descendre. La réponse est écrite dans `routing.json` ; Later, un texte libre (Other) ou un dialogue fermé redemande à une session suivante. Un seul dialogue à la fois, jamais en headless (`-p`) ni dans un sous-agent, et le modèle n'écrit jamais ce fichier. Chaque réponse laisse le repo de config modifié : commite-le depuis ce repo. `/route pending` liste ce qui reste à confirmer, `/route ask off|on` coupe ou rallume le dialogue, `/route forget <nom|all|projects>` efface une décision (une ligne changée revient à la phase livrée ; `all` et `projects` demandent d'abord).
|
Taper un skill qui a une ligne route la boucle principale dessus. Une ligne du tier best (plan, reflect, orchestrate, escalate) tient d'un tour à l'autre pendant tout le run, jusqu'à `/route clear`, `/route off`, un `/model` tapé ou un skill d'un autre tier tapé. Pour relancer un tour bloqué : `ultrathink` dans le prompt (plancher du tour) ou `/route effort=max` (tient jusqu'à `/route clear`). Le `/effort` intégré n'a pas d'effet dans un run : les lignes et les routes passent devant. Un prompt tapé sans règle, skill ni route en cours passe par un classifieur (un appel au petit modèle, 3 secondes au plus) : son niveau d'effort devient celui du tour, sans jamais changer de modèle ; `/route classify off` ou `"classify": false` dans la config machine le coupe, et ton propre `/effort` passe devant. Les sous-agents reçoivent le modèle de leur ligne au lancement (dans leur tier, jamais en dessous de leur frontmatter) et son niveau à chaque étape ; un `model` ou `effort` explicite sur l'appel gagne. Explore tourne en sonnet/medium, Plan en opus/xhigh. `/route show` affiche la route en cours, slot de run compris (`main: run <phase>`). Première utilisation : la première fois qu'un skill avec ligne est tapé, qu'un agent avec ligne est lancé ou qu'une phase est déclarée sur la boucle principale via l'outil `route`, le mod demande une fois si la route convient. La question donne le contexte : la description du skill (première phrase du frontmatter de son `SKILL.md`) ou celle de l'agent, la phase avec sa ligne `about` (champ de la phase dans `routing.json`), puis le modèle et le niveau réels de l'étape suivante ; une phase déclarée dit aussi qui l'utilise. Pour une ligne : Later, Keep ou Change ; pour une phase déclarée : Later ou Keep. Change demande le modèle (fable, opus, sonnet, haiku, chacun avec son tier), puis le niveau parmi ceux des phases de ce modèle (fable : medium, high, xhigh ou max ; sonnet : low, medium, high ou xhigh ; opus et haiku n'en ont qu'un, la question est sautée), puis Everywhere ou This project only (exception rangée sous le remote origin du dépôt réduit à `host/path`, jamais d'identifiants). Une ligne reste un nom de phase : le couple modèle et niveau désigne une phase existante, et la même phase qu'avant vaut Keep. Un couple qu'aucune phase n'offre s'ajoute à la main comme nouvelle phase dans `routing.json`. Après un changement sur un skill, un toast donne le modèle et le niveau réels, et rappelle `/route switch on` quand la boucle principale refuse de descendre. La réponse est écrite dans `routing.json` ; Later, un texte libre (Other) ou un dialogue fermé redemande à une session suivante. Un seul dialogue à la fois, jamais en headless (`-p`) ni dans un sous-agent, et le modèle n'écrit jamais ce fichier. Chaque réponse laisse le repo de config modifié : commite-le depuis ce repo. `/route pending` liste ce qui reste à confirmer, `/route ask off|on` coupe ou rallume le dialogue, `/route forget <nom|all|projects>` efface une décision (une ligne changée revient à la phase livrée ; `all` et `projects` demandent d'abord).
|
||||||
|
|
||||||
La config par machine, optionnelle, vit dans `~/.claude/model-router.json` et passe devant `routing.json` ; `"enabled": false` y coupe le mod sur cette machine, `"ask": false` y coupe le dialogue, une ligne à `null` y retire une ligne. Un `.claude/model-router.json` dans un projet n'est jamais lu. Les phases et les lignes se modifient à la main dans `routing.json` ; `/route reload` relit les deux fichiers.
|
La config par machine, optionnelle, vit dans `~/.claude/model-router.json` et passe devant `routing.json` ; `"enabled": false` y coupe le mod sur cette machine, `"ask": false` y coupe le dialogue, une ligne à `null` y retire une ligne. Un `.claude/model-router.json` dans un projet n'est jamais lu. Les phases et les lignes se modifient à la main dans `routing.json` ; `/route reload` relit les deux fichiers.
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ Builtin `/effort` is NOT a lever inside a run: rows and routes outrank it.
|
|||||||
- A skill typed while a background agent is live routes only through the
|
- A skill typed while a background agent is live routes only through the
|
||||||
typed marker (unverified live 2026-10-10).
|
typed marker (unverified live 2026-10-10).
|
||||||
- Headless (`-p`, SDK) runs the hooks, so routing works there too.
|
- Headless (`-p`, SDK) runs the hooks, so routing works there too.
|
||||||
|
- A plain typed prompt with no route in force gets its turn effort from a
|
||||||
|
small-model classifier (effort only; `/route classify off`; never `-p`).
|
||||||
- Mod off: typed agents fall back to their `model:`/`effort:` frontmatter.
|
- Mod off: typed agents fall back to their `model:`/`effort:` frontmatter.
|
||||||
- First use of a row asks once, with context (Later, Keep or Change: model,
|
- First use of a row asks once, with context (Later, Keep or Change: model,
|
||||||
then effort, then Everywhere or this project only); the answer is kept in
|
then effort, then Everywhere or this project only); the answer is kept in
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+554
-100
File diff suppressed because it is too large
Load Diff
@@ -145,14 +145,16 @@
|
|||||||
"verifier": "verify",
|
"verifier": "verify",
|
||||||
"feater": "implement",
|
"feater": "implement",
|
||||||
"doc-syncer": "write",
|
"doc-syncer": "write",
|
||||||
"security-auditor": "judge"
|
"security-auditor": "judge",
|
||||||
|
"plan-challenger": "judge"
|
||||||
},
|
},
|
||||||
"phases": {
|
"phases": {
|
||||||
"verify": "verify",
|
"verify": "verify",
|
||||||
"implement": "implement",
|
"implement": "implement",
|
||||||
"write": "write",
|
"write": "write",
|
||||||
"orchestrate": "orchestrate",
|
"orchestrate": "orchestrate",
|
||||||
"escalate": "escalate"
|
"escalate": "escalate",
|
||||||
|
"judge": "judge"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"changed": {
|
"changed": {
|
||||||
|
|||||||
Reference in New Issue
Block a user