chore(583): make per-agent model routing a hard constraint + PreToolUse gate
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
PR Gates / Docs update reminder (pull_request) Successful in 12s
PR Gates / decisions lifecycle (pull_request) Successful in 14s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m42s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 21m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m2s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
PR Gates / Docs update reminder (pull_request) Successful in 12s
PR Gates / decisions lifecycle (pull_request) Successful in 14s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m42s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 21m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m2s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The kickoff tells the orchestrator to route agents by capability, but that rule lived only in a prose paragraph. On 2026-07-25 a session dispatched two implementers (#436, #440) with `model` omitted, silently inheriting the orchestrator tier — while following every bullet in HARD CONSTRAINTS in the same session. The bulleted list is what functions as the checklist; prose above it reads as background. - HARD CONSTRAINTS: keyed routing bullet requiring the tier to be stated in the dispatch itself, so an invisible omission becomes visible output. Prose paragraph tightened to point at the key rather than restate the table (the kickoff is pasted into every session — duplication is a per-session tax, #542). - .claude/hooks/pretooluse-agent-model.sh: PreToolUse on Agent, `ask` when a committing agent is dispatched with no explicit `model`. Narrow by design — passes through read-only/recon types, `fork` (model override ignored by the tool), and any dispatch already naming a tier, because a gate that fires on every fan-out trains one-shot dismissal. `ask` not `deny`: routing is a judgment call with no derivable right answer, unlike the H6/H10 merge gate. - New decision record `process.per-agent-model-routing`; catalog regenerated. Decision matrix verified against 9 payloads incl. a replay of the dispatch that missed. decisions-validate: OK. fixes #583
This commit is contained in:
@@ -524,3 +524,52 @@ The dependencies API is unaffected: `POST /issues/{n}/dependencies` with `{"owne
|
||||
blocked-by correctly, though the bare `{index}` form returns 201 without reliably attaching — verify
|
||||
with the GET. (That last sentence is carried forward from an earlier revision of the handoff doc,
|
||||
commit `f93458c7`, where it was dropped by a later prune rather than disproved.)
|
||||
|
||||
## 2026-07-25 — Name the model tier for every dispatched agent; a PreToolUse gate makes the silent default visible (#583)
|
||||
|
||||
`key: process.per-agent-model-routing` · `status: active` · `since: 2026-07-25` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** State the model tier (and effort, where the client exposes it) in the dispatch itself for every delegated agent — bounded recon → cheapest fast tier at `low`; mechanical slice against a documented contract → mid tier; judgment-heavy work → orchestrator tier; independent review → a different model family than the implementer.
|
||||
**Signals:** subagent model routing · `model` omitted · silent tier inheritance · orchestrator tier for a mechanical slice · capability routing · prose rule vs HARD CONSTRAINT · dispatch-time checkpoint · paths: `.claude/hooks/pretooluse-agent-model.sh`, `docs/handoffs/chicorytv-issue-queue.md` · issues: #583, #436, #440
|
||||
**Mechanics:** `Agent` tool `model` parameter; PreToolUse hook on the existing `Agent|Task` matcher in `.claude/settings.json`.
|
||||
|
||||
On 2026-07-25 a session dispatched two implementers (#436, #440) with `model` omitted on both calls;
|
||||
both silently inherited the Opus orchestrator tier. #440 was a mechanical SPA slice against an
|
||||
already-shipped backend contract — a plausible mid-tier candidate.
|
||||
|
||||
The interesting part is *why*, because it wasn't forgetfulness. **Every rule in `HARD CONSTRAINTS` was
|
||||
followed in that same session** — worktree off `origin/main`, one committing agent per worktree,
|
||||
parallelize on disjoint slices, local gate before push. Routing was the one instruction living only in
|
||||
a prose paragraph, and it was the one that got defaulted. Treat that as the general lesson: in a
|
||||
kickoff doc that is pasted into every session, **the bulleted imperative list is what actually
|
||||
functions as the checklist**, and prose above it is read as background. A rule you want followed
|
||||
belongs in the list, keyed, or it is advisory in practice.
|
||||
|
||||
Three aggravating factors, all worth checking when writing any future rule here:
|
||||
|
||||
- **Scope gap.** The low-cost-routing paragraph is written entirely about queue selection and recon
|
||||
("bounded searches, inventories, log triage, report drafting"). It never named *implementers*, and
|
||||
gave no default for the bounded-but-not-trivial case — so the largest-cost dispatch fell in a gap.
|
||||
- **The wrong default is the silent one.** Omitting `model` produces no artifact. Nothing in the
|
||||
session report revealed the tier; the operator had to ask. Contrast the BOM trap
|
||||
(`process.bom-format-detection-recipe`), where a hook fires because a memory describing the trap
|
||||
demonstrably failed to prevent it twice in one day.
|
||||
- **Distance from the decision point.** The rule sits ~line 84 of the kickoff; dispatch happens after
|
||||
orientation, claiming, the bundle scan and doc reading.
|
||||
|
||||
Hence the two-part fix: a keyed HARD CONSTRAINT that requires the tier to be **stated out loud in the
|
||||
dispatch** (a self-correcting mechanism — it turns an invisible omission into visible output), plus
|
||||
`.claude/hooks/pretooluse-agent-model.sh`, which `ask`s when a **committing** agent is dispatched with
|
||||
no explicit `model`.
|
||||
|
||||
The hook is deliberately **narrow**, and the scoping is the load-bearing design choice: it passes
|
||||
through read-only/recon agent types, `fork` (whose `model` override is ignored by the tool by design,
|
||||
so prompting would be nonsense), and any dispatch that already names a tier. A gate that fires on
|
||||
every fan-out trains one-shot dismissal and becomes noise — which would leave it worse than no gate.
|
||||
It is `ask`, never `deny`: routing is a judgment call with no derivable right answer, unlike the
|
||||
H6/H10 merge gate (`release.merge-consent-autogrant`) which derives a verifiable state and can
|
||||
therefore grant or refuse outright.
|
||||
|
||||
**Known limitation:** review agents (`feature-dev:code-reviewer`) pass through, since they cannot
|
||||
commit — so the "prefer a different model family for independent review" half of the rule remains
|
||||
doc-only, unenforced. That half is the higher-value routing decision of the two; if cross-family
|
||||
review drift shows up again, gate it next.
|
||||
|
||||
Reference in New Issue
Block a user