feat(route): give next-cost a writer, and make the rubric the only copy

The board line's next-cost field had a reader (board.sh) and no writer, so
its value was retyped by hand every session and drifted into several
competing spellings. Cleaning the data could not fix that: the cause was the
missing write path.

route.sh is that writer. Four scored traits of the next task -- path,
verification, reversibility, scope -- plus a required rationale, run through
the operator's model rubric moved here as the single copy. The row table is a
closed set of six values, so a seventh spelling cannot enter circulation, and
route-selftest.sh section 6 runs the round trip (route emits -> board parses)
inside one repo rather than across two.

Two spellings of one decision come out of one table: the rubric name for the
board line, the CLI alias for the command the operator pastes. Effort levels
are the set pinned in this marketplace; model aliases are gated against the
installed claude rather than hardcoded.

Three things worth naming:

- Escalation is asymmetric. Any single trait escalates; the cheapest row needs
  all four at the cheap end. Underkill costs one session, overkill costs quota
  every session -- but a wrong architecture call costs more than either.
- The Fable rows fire only from an explicit judgement flag, never inferred
  from the last-session record. "The session did not finish" also covers
  context exhaustion and operator interrupts, which say nothing about the
  model, and Fable runs without an advisor.
- The trait block is a single-line HTML comment because board.sh's NESTE
  extractor skips only lines that START with '<!--'. Measured first: a YAML
  block or a multi-line comment silently replaces the repo's next step on the
  board with "next_task:". Pinned by section 7.

board.sh is untouched as a program; its header now points at route.sh for the
value set so this does not reopen the two-specs defect 305f168 closed.

Selftests: coord 136, board 30, route 47 (new), node 7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017peNgsxVt1BR4BTuMwiPoX
This commit is contained in:
Kjell Tore Guttormsen 2026-07-31 10:10:22 +02:00
commit 0d1183800e
8 changed files with 808 additions and 11 deletions

152
skills/route/SKILL.md Normal file
View file

@ -0,0 +1,152 @@
---
name: route
description: >-
Decide which model and reasoning effort the NEXT session should run with, by
scoring four traits of the next task and running them through the rubric row
table (`route.sh`). Use at session end, whenever STATE.md's next step is
written or rewritten, and whenever the operator asks what to launch the next
session with: "what model should I use next", "which effort level", "close
the session", "wrap up", "update STATE", "what should the next session run
with", "is opus overkill here", "give me the startup command". Also triggers
on Norwegian phrasings: "hvilken modell neste økt", "hvilken effort", "avslutt
sesjonen", "oppdater STATE", "hva skal neste økt kjøre med", "er opus
overkill", "gi meg oppstartskommandoen", "modell og effort". Trigger even when
the operator names no model and no tool — choosing the model for the next
session IS this skill. Not for choosing which REPO gets the next session:
that is the `board` skill.
version: "0.9.0"
---
# route — what the next session should run with
The operator's closing line requires two things every single session: the model
and effort for the next session, and a pasteable startup command. This skill
produces both from evidence instead of from a hunch, and leaves the scoring
behind in STATE.md so a wrong call can be found later.
**The division of labour is the whole design.** Scoring the four traits is
judgement and belongs to you. Turning scores into a model is a lookup, and
`route.sh` does it deterministically — same scores, same answer, every time,
at zero token cost. Never "reason your way" to a model. If you find yourself
weighing whether the task feels hard enough for Opus, you have skipped the
scoring step and are doing the thing this skill replaces.
## The engine
ROUTE="${CLAUDE_PLUGIN_ROOT}/scripts/route.sh"
"$ROUTE" --path <known|partial|undetermined> \
--verification <strong|weak|none> \
--reversibility <cheap|costly|one-way> \
--scope <local|multi-file|cross-cutting> \
--rationale "why these four scores" \
[--opus-xhigh-failed] \
[--last-model <name> --last-effort <level> \
--last-completed <yes|no> --last-corrections <n>]
There is no deployed copy anywhere else and no fallback path — one entry point
is deliberate. Exit 2 means a bad or missing argument; read stderr and fix the
call rather than dropping the flag. `route.sh --help` carries the full row table
and the reasoning behind it.
The script writes nothing. It prints `row`, `rule`, `next-cost`, `command`,
`fallback`, `fallback-command`, `route-line` and (when the record is given)
`route-last`. **You** paste those into STATE.md.
## Scoring the four traits
Score the task the NEXT session will do — the one in the `👉 NESTE` block —
not the one that just finished. Read the next step as written before scoring;
if you cannot score it from what is written, that is a finding (see below).
| Trait | Score it by asking |
|---|---|
| `path` | Is the solution route already described? `known` = the steps are written down or the pattern exists in this repo. `partial` = the shape is clear, one real decision is open. `undetermined` = it has to be found first. |
| `verification` | Will a machine catch the error? `strong` = tests, types or a compiler fail on it. `weak` = a smoke test or manual run would probably surface it. `none` = prose, API shape, a security judgement — a wrong answer just sits there. |
| `reversibility` | `cheap` = a commit away. `costly` = touches published state, needs a migration or a follow-up release. `one-way` = a pushed tag, a public interface, a deletion. |
| `scope` | `local` = one file or one function. `multi-file` = several files, one repo. `cross-cutting` = many subsystems, or more than one repo. |
`verification` carries the most signal and is the trait most often skipped.
Strong verification means a cheap model's mistakes get caught and corrected —
cheap model plus tight feedback beats an expensive model without it. When
nothing verifies the output, model quality is the only defence left.
Three rules that keep the scoring honest:
- **Score the task, never the feeling.** "Hard", "complex" and "important" are
not traits here. They are unfalsifiable, and they always resolve upward.
- **`--rationale` is required and is the point.** It is where a misscore is
caught weeks later, when the recommendation turns out to have been wrong. One
sentence naming the evidence: "the pattern exists in handlers/, but the error
handling is undecided" — not "medium difficulty".
- **When torn between two scores, take the more expensive one.** Escalation is
asymmetric by design: any one trait escalates, and row 1 needs all four at
the cheap end.
## The specification check you get for free
If the next step is scored `path=undetermined` and no design phase is planned,
**the task description is underspecified — the model is not too small.** Say so,
and rewrite the next step until it can be scored. Upgrading the model to
compensate for a vague specification is the most expensive form of
procrastination available, and it hides the real defect.
This check is worth more than the tokens the routing saves. Do not skip it by
scoring `partial` to keep things moving.
## The last-session record
Rows 5 and 6 (Fable) are history-dependent, so without a record they are dead
policy. Write it every session, from what actually happened in the session that
is ending:
- `--last-model` / `--last-effort` — what this session actually ran with.
- `--last-completed yes|no` — did this session finish the next step the previous
STATE.md set out? Answer about that step, not about the session in general.
- `--last-corrections <n>` — how many rounds of rework it took. This is the
cheap proxy for whether the routing was right.
Read the previous `route-last` line out of STATE.md before overwriting it.
Pass `--opus-xhigh-failed` **only** when it says an `Opus 5`/`xhigh` session ran
at this same next step and did not complete it. A session that ran out of
context, was interrupted, or stalled waiting on another repo is not a model
failure — and Fable runs without an advisor, so a wrong escalation here is
expensive. When it is ambiguous, leave the flag off.
## Writing it into STATE.md
Three single-line HTML comments sit directly under the `👉 NESTE` heading, in
this order:
<!-- board: status=in-progress; blocked-on=-; next-cost=Opus 5/high -->
<!-- route: path=partial; verification=strong; ...; rationale=... -->
<!-- route-last: model=Opus 5; effort=xhigh; completed=yes; corrections=1 -->
Splice the emitted `next-cost` value into the existing board line — leave
`status` and `blocked-on` alone, they answer a different question and this skill
knows nothing about them.
**They must stay single-line comments.** `board.sh` reads the first line under
the heading that is not blank, not a heading, and does not *start* with `<!--`,
and shows it as that repo's next step across every repo. A YAML block or a
comment broken across lines therefore replaces the repo's next step on the board
with `next_task:`. Measured, not assumed — `route-selftest.sh` section 7 pins it.
## Reporting it
Give the operator the two closing-line items and nothing more:
- **Modell neste økt:** the `next-cost` value, plus the `fallback` one row
cheaper for quota pressure. Name the `rule` that fired — "path=partial" — so
the call is auditable rather than asserted.
- **Oppstartskommando:** the `command` string, in its own code block, with
`/exit` named. **Never prefix it with `cd`:** one repo per terminal tab, so
the working directory is already right. If the next step belongs in a
different repo, say so in plain words — that is a different tab, not a `cd`.
If `command` and `fallback-command` are the same as the current session's model,
say `/clear` is enough instead — but only if no newly installed plugin or skill
needs a fresh process to be picked up.
Do not paste the whole output block. One row, the rule that produced it, the
command.