feat(model-router): wave 4-B — prompt classifier
Plain composer prompts with no route in force get a turn-scoped effort from $.model.classify (design/analysis/coding/bookkeeping/chat → phase effort, effort only, never the model). Bounded by a 3 s clock race, a user interrupt neither counts nor writes, three failures in a row pause it for cooldownMinutes (one toast, status marker, cleared by /route reload). No dialog, no routing.json write on a classified route. New Config key `classify` (default on, pickSwitch), `/route classify on|off` session toggle, `/route show` line and argumentHint. An explicit user /effort beats the guess (engineBase check). Docs: README classifier paragraph, USAGE row + sentence, CHANGELOG, effort-shift.md. Kit suite 428 tests.
This commit is contained in:
@@ -11,6 +11,8 @@ 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`.
|
||||
- **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
|
||||
- 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`).
|
||||
|
||||
Reference in New Issue
Block a user