diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c0d616a..3a323e4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -29,7 +29,7 @@ claude-config/ ├── mods/ # Claude Code mods (function-hooks plugins), loaded through the skills/ symlink ├── skills-external/ # Vendored skill packs: gstack submodule, design skills, superpowers, agent-skills, MengTo scroll skills, 21st and Higgsfield packs (machine-owned copies gitignored) ├── templates/ # Per-project templates (CLAUDE.md, settings, memory registries, deploy runbook, gitignore) -└── lib/ # Shared libs: gitflow, profiles, vendoring, effort pins, gates, archetypes, tests +└── lib/ # Shared libs: gitflow, profiles, vendoring, route doctrine, gates, archetypes, tests ``` ## Architecture principles diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dac3e8..3ec6fde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and this project ## [Unreleased] ### Added -- **model-router mod**: `mods/model-router/`, a Claude Code mod (function-hooks plugin), routes the effort of every main-loop request from a phase table, along with the model and effort of the built-in sub-agents (Explore on sonnet/medium, Plan on opus/xhigh). It answers `Skill(effort-*)` itself, so the five `effort-*` skills no longer load while it is on. `ultrathink` in a prompt and a typed `/effort-` set the main turn's default and minimum effort. The model gets a `route` tool and the user a `/route` command (`show|clear|off|on|reload||model= effort=|switch on|off|verbose on|off`). Optional per-machine config `~/.claude/model-router.json`, where `"enabled": false` turns it off on that machine. The spinner suffix and the status line show the route in force. It loads in every session through the tracked symlink `skills/model-router` (`model-router@skills-dir`). `make doctor` gains a Mods section; suite `make test suite=lib/tests/mods.test.sh`. Known limits: the main loop switches model only with `mainModelSwitch` on (default off, one cold-cache step per switch into another model), and the hooks send full model ids, so the `models` table has to follow new versions. +- **model-router mod**: `mods/model-router/`, a Claude Code mod (function-hooks plugin), routes every repo skill and agent from phase rows (`plan`, `reflect`, `orchestrate`, `escalate`, `judge`, `implement`, `write`, `verify`, `explore`, `apply`, `mechanical`; built-ins Explore on sonnet/medium, Plan on opus/xhigh). A typed skill routes the main loop to its row, and a best-tier row holds across turns in a run slot until `/route clear`, `/route off`, a user `/model` or a typed skill on a non-best row. Agents get their row's model at spawn (within the tier, upward only) and its effort on every step; explicit Agent params win. Orchestrators declare their phases through the `mcp__model-router__route` tool. `ultrathink` in a prompt sets the turn's minimum effort and `/route effort=max` holds until `/route clear`; the built-in `/effort` is not a lever inside a run. `/route show` names the run slot (`main: run `), and a `null` row in the override drops a default row. The model gets a `route` tool and the user a `/route` command (`show|clear|off|on|reload||model= effort=|switch on|off|verbose on|off`). Optional per-machine config `~/.claude/model-router.json`, where `"enabled": false` turns it off on that machine. The spinner suffix and the status line show the route in force. It loads in every session through the tracked symlink `skills/model-router` (`model-router@skills-dir`). `make doctor` gains a Mods section; suite `make test suite=lib/tests/mods.test.sh`. Known limits: the main loop switches model only with `mainModelSwitch` on (default off, one cold-cache step per switch into another model), and the hooks send full model ids, so the `models` table has to follow new versions. - **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
` 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/
..
` 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). ### Changed +- `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`). `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. - `gitflow start` and `finish` warn on stderr when a base is behind origin and cannot fast-forward, instead of a silent `git pull --ff-only || true` (T18l, T18n). - `/close` (`/capitalize` STEP 5C) no longer runs its own push of develop: `gitflow finish` already pushes develop in auto-push mode (BDR-095). The closing line reports the real state, read after the merge: pushed, manual push mode with the `! git push origin develop` to run, not on origin, push failed, or an invalid `gitflow.autopush` value named and treated as manual push mode. A finish whose merge landed but whose branch delete failed (rc 5/2/6) still reports the push state. @@ -18,6 +19,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and this project - `/release-candidate` STEP 6: in manual push mode, with an invalid mode value, or when main or develop is not on origin, Claude pushes nothing and prints one command for the user, `! git push --atomic origin main develop v`. The tag-push question remains for auto-push mode with both branches on origin. The version must match `^[0-9]+\.[0-9]+\.[0-9]+$` before it enters a command or tag; `release-executor` checks it too and blocks on anything else. - `/tour`: each summary row says `on origin` or `local only` with the `! git -C "" push -u origin ` to run. The tour never pushes or retries. +### Removed +- 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-` no longer exists: use `ultrathink` or `/route effort=max`. Breaking: the next release is 3.0.0. + ### Fixed - `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 ) — 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='' is not a boolean (git rc )`. 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. diff --git a/README.md b/README.md index 801d907..33fa880 100644 --- a/README.md +++ b/README.md @@ -71,10 +71,14 @@ commands, settings, secrets, maintenance. Doctrine: the session model (Fable) does main-loop reflection ONLY — brainstorm, plan, contract, audit judgment, gates, loop decisions — enforced -by a blocking gate (`lib/model-gate.md` + `lib/model-check.sh`) at the entry -of the 15 reflection skills (the orchestrators plus `/analyze`). Nothing dispatched inherits silently: -typed agents carry a frontmatter pin, built-ins get an explicit `model=` at -every call site. +by a blocking gate (`lib/model-gate.md`) at the entry +of the 15 reflection skills (the orchestrators plus `/analyze`): the skill +calls the model-router `route` tool and its answer, which names the model +id the main loop runs on, is the witness. A non-Fable/Opus id or the mod +off stops the skill (`/route on` resumes the mod). Nothing dispatched +inherits silently: typed agents run on their model-router row, their +`model:` frontmatter being the off-state floor; built-ins get an explicit +`model=` at every call site. | Agent | Model | Tier | |---|---|---| @@ -100,31 +104,37 @@ children are dispatched `model:"fable"` (they carry reflection). ## Effort routing (BDR-107, BDR-108) Second axis of the same table: how hard each phase thinks. Session default -`high`. Every typed agent carries an `effort:` pin next to its `model:` (low -appliers, medium executors, high judgment, xhigh challengers and gates; none -on haiku, which rejects the parameter). Every user-invoked skill carries an -entry level (`/status` low … `/ship-feature` xhigh); the vendored externals -(design stack, superpowers, agent-skills, MengTo scroll skills, 21st) get theirs from -`lib/effort-pins.txt`, re-applied by `lib/effort-pins.sh` after every -vendoring step. Orchestrators shift per phase through the `effort-low` … -`effort-max` skills (`lib/effort-shift.md`, always sent with another tool -call: a lone Skill call applies nothing (mod off)). Model pins stay tier aliases +`high`. The model-router mod (below) holds the live source: one phase row +per repo skill and agent, each phase naming a tier and an effort level +(`plan` best/xhigh, `reflect` best/high, `orchestrate` best/medium, +`escalate` best/max, `judge` big/xhigh, `implement` work/medium, `write` +work/high, `verify` work/xhigh, `explore` work/medium, `apply` work/low, +`mechanical` cheap/low). The `model:` and `effort:` frontmatter of every +typed agent and user-invoked skill stays as the off-state floor, kept equal +to the rows by the census `lib/tests/effort-routing.test.sh`: low +appliers, medium executors, high writers, xhigh judgment and gates (none +on haiku, which rejects the parameter); `/status` low … `/ship-feature` +xhigh. The vendored externals (design stack, superpowers, agent-skills, +MengTo scroll skills, 21st) get their level from their row only. +Orchestrators declare each phase through the `mcp__model-router__route` +tool (`lib/effort-shift.md`): `orchestrate` at a dispatch span, `reflect` +or `plan` when reflection resumes, `apply` at the bookkeeping tail, +`escalate` at the verify-secure caps. Model pins stay tier aliases (`sonnet`, `opus`, `haiku`, `fable`): the latest version of a tier is also -the cheapest or same-priced, so the quality/price trade-off is tier × effort, -never version. Census `lib/tests/effort-routing.test.sh`; transcript audit -`python3 lib/effort-audit.py`. +the cheapest or same-priced, so the quality/price trade-off is tier × +effort, never version. Transcript audit `python3 lib/effort-audit.py`. ### model-router mod `mods/model-router/` is a Claude Code mod (a function-hooks plugin) that applies this table per request. It loads in every session through the tracked symlink `skills/model-router`, as `model-router@skills-dir`. -- Main loop: every request gets the effort of the phase in force. The mod answers `Skill(effort-*)` itself and applies the level from the next request on, so the five `effort-*` skills no longer load while it is on. -- Built-in sub-agents: Explore runs on sonnet/medium, Plan on opus/xhigh. An explicit `model` on the Agent call wins. -- User floor: `ultrathink` in a prompt, or a typed `/effort-`, sets the main turn's default and minimum effort. -- `/route` (user command) shows or sets the route: `show`, `clear`, `off`, `on`, `reload`, a phase name, `model= effort=`, `switch on|off`, `verbose on|off`. The model sets routes through a `route` tool. +- 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. +- 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`, a phase name, `model= effort=`, `switch on|off`, `verbose on|off`. `/route show` names the run slot when one holds (`main: run `). The model sets routes through a `route` tool. - The spinner suffix and the status line under the prompt show the route in force. -Optional per-machine config: `~/.claude/model-router.json`. Keys: `models` (alias → full id), `windows` (context window per full id), `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). `/route reload` re-reads it. +Optional per-machine config: `~/.claude/model-router.json`. Keys: `models` (alias → full id), `windows` (context window per full id), `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). A `null` value in `agents` or `skills` drops a default row. `/route reload` re-reads it. 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. @@ -218,7 +228,6 @@ 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) | | `/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) | -| `/effort-low` … `/effort-max` | Effort shifters the orchestrators send per phase (answered by the model-router mod when on); typed by you, they set the main turn's default and minimum effort | | `/route` | model-router mod: show or set the main-loop route (show, clear, off, on, reload, , model=… effort=…, switch on\|off, verbose on\|off) | > This table lists personal skills. Gstack skills (investigate, review, retro, diff --git a/USAGE.md b/USAGE.md index 3a002d1..15869b9 100644 --- a/USAGE.md +++ b/USAGE.md @@ -174,19 +174,20 @@ Tu veux... ### Niveau d'effort -Chaque commande démarre à un niveau de réflexion fixé dans son frontmatter -(`effort:`) : low pour la tenue de registre (`/status`, `/close`, +Chaque commande a une ligne de phase dans le mod model-router +(`mods/model-router/`, actif dans chaque session), qui fixe son niveau de +réflexion : low pour la tenue de registre (`/status`, `/close`, `/commit-change`), medium pour le courant (`/gitflow`, `/prune-memory`), -high pour un fix ou un refactor (`/feat`, `/hotfix`, `/bugfix`, `/refactor`, -audits avec fix), xhigh pour l'architecture et l'audit avant validation -(`/ship-feature`, `/onboard`, `/analyze`). Les orchestrateurs décalent -ensuite le niveau par phase (`lib/effort-shift.md`), et `/effort-max` tapé à -la main relance un tour bloqué au maximum. Les skills externes vendorés -(pile design, superpowers, agent-skills, skills scroll MengTo, 21st) reçoivent leur niveau de -`lib/effort-pins.txt`. Un skill chargé seul par Claude n'applique pas son -niveau : il doit partir avec un autre appel d'outil dans le même message. +high pour un fix ou un refactor (`/feat`, `/hotfix`, `/bugfix`, +`/refactor`, audits avec fix), xhigh pour l'architecture et l'audit avant +validation (`/ship-feature`, `/onboard`, `/analyze`). Le frontmatter +(`model:`, `effort:`) garde les mêmes valeurs et sert de plancher quand le +mod est coupé. Les orchestrateurs déclarent ensuite chaque phase via +l'outil `mcp__model-router__route` (`lib/effort-shift.md`). Les skills +externes vendorés (pile design, superpowers, agent-skills, skills scroll +MengTo, 21st) ont aussi leur ligne. -Avec le mod model-router (`mods/model-router/`, actif dans chaque session), le niveau suit la phase à chaque requête. Le mod répond lui-même à `Skill(effort-*)` : le niveau s'applique dès la requête suivante et le texte des skills `effort-*` n'est plus chargé. Écrire `ultrathink` dans un prompt, ou taper `/effort-`, fixe le niveau par défaut et le minimum du tour principal. Les sous-agents intégrés suivent leur route : Explore en sonnet/medium, Plan en opus/xhigh. `/route` affiche ou fixe la route (`/route show`, `/route clear`, `/route off`). La config par machine, optionnelle, vit dans `~/.claude/model-router.json` ; `"enabled": false` y coupe le mod sur cette machine. +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 `). La config par machine, optionnelle, vit dans `~/.claude/model-router.json` ; `"enabled": false` y coupe le mod sur cette machine, et une ligne à `null` y retire une ligne par défaut. ## Les plugins — décision rapide