repo-mailbox/skills/route/SKILL.md
Kjell Tore Guttormsen 5e5bc4a66e feat(route,board): strike the advisor rule, add board.sh --row <repo>
Order 20260912T202210Z-7588027378-from-.claude, operator decision
2026-09-12 (helhetlig vurdering av arbeidssystemet, cut row 3 and the
board.sh --row improvement row). One order, two parts, one version bump.

THE ADVISOR RULE IS STRUCK. route.sh and board.sh --dispatch emit no
--advisor at all. The rule fired per ROW on a need - always on the Sonnet
rows (a capability lift, which is what made the quota fallback safe to
take), and on the Opus rows at reversibility=costly|one-way - and it read
well. It was killed by a MEASUREMENT, not by taste: of 54 dispatches the
PM issued 08.-12.09, ZERO carried the flag, because sessions are started
by hand from the model and effort rather than from the whole emitted
line. A rule nothing honours is not a policy, and an emitted value nobody
acts on is decoration in a field whose only job is to be evidence. The
advisor is now what it already was in practice: an operator decision per
session, said in one sentence in route.sh --help.

The comments that rested on the rule were REWRITTEN, not left standing.
board.sh --dispatch still refuses a --model/--effort pair, but the reason
is no longer "the advisor is a property of the ROW": it is that the rubric
has exactly one copy, and a dispatch taking the model directly would be a
second, unscored way to reach the same decision - recording no traits, no
rationale and no next-cost, so nothing afterwards could say whether the
routing or the scoring was wrong. A comment defending a removed mechanism
is how the next session restores it. Both skills carry the correction.

Pinned as an ABSENCE over the whole trait space - 81 combinations, every
line of output, with a known-positive control proving the sweep's grep
can find a planted advisor - rather than on four sampled rows, because
the claim is that no path emits it. board.sh --dispatch at
reversibility=costly is pinned separately: that is the exact input a
reintroduced rule would fire on. The literal string is absent from
route.sh entirely, including the paragraph recording what was struck (it
says "an opus advisor flag" in words), because a blunt grep cannot tell a
description from a specification. Backward compatibility is pinned rather
than assumed: a route line carrying a legacy advisor= field still parses
and still yields a command - measured, 0 of 48 route lines in ~/repos
carry one, but a reader that broke on an unknown field would turn last
month's STATE.md into "that repo has no route line". The three CLI gates
section 14 carried went with the rule; the suite no longer depends on the
installed claude at all.

board.sh --row <repo> IS THE SEVENTH RENDERING of the same scan, never a
second scan, read-only like every other one. (The order calls it the
sixth; by this file's own numbering --inbox-plan is the fourth and
--dispatch the fifth. Corrected rather than carried wrong.) It exists
because the columns WERE misread: on 11.09 the PM read FLY off the table
by eye and got it wrong, while every other rendering a program consumes
is already key=value. inn, ordre and fly are three separate fields
because they are three separate facts; status is the bare token, never
the table's blocked>target display, with blocked-on beside it; neste is
last and uncut. An unknown repo exits 2 and writes NOTHING to stdout - an
empty block would read as a repo whose every column is blank, which is a
real and different state.

upushet is the ONE field that is not a rendering of the scan, and it is
named rather than blended in: nothing in the scan measures it, so it is
read once, for the named repo only, and never enters the table, the plan
or the briefing. It reads the remote-TRACKING ref, not the remote, so
upushet=N honestly means "the local ref says N"; a repo with no upstream
reports ?, never 0.

The row fixture's three counts are three DIFFERENT integers (3/2/1), and
that is the finding worth recording. Built first with 2/1/1, it was
mutation-tested by making fly read the ORDRE field - the exact 11.09
misreading - and the check stayed GREEN, because the two fields held the
same digit. A fixture that cannot tell two columns apart is the defect
wearing a passing test, inside the section written to prevent it.

Suites under /bin/bash 3.2, before -> after: coord 257 -> 257, board
393 -> 427, route 73 -> 73 (13 advisor checks and 3 CLI gates out, 15
absence/legacy checks in, and it no longer varies with claude being on
PATH), orders 116 -> 116, state-line-guard 54 -> 54. Sum 893 -> 927,
README badge updated to the measured sum. npm test 12/12, fail 0.

Verified live against the real tree, not only fixtures: --row
repo-mailbox reports fly=1 beside ordre=0 (the distinction that was
misread), --row on the nested key from-ai-to-chitta/content-sadhguru
resolves, and an unknown repo exits 2.

No tag, no push, no catalog change - that is the operator's
release-plugin.mjs run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:45:47 +02:00

11 KiB
Raw Blame History

name description version
route 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. 0.34.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" \
         [--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.

Fable rows are never this calculator's output

route.sh only ever emits rows 1-4. Rows 5-6 (Fable) fired from an explicit --opus-xhigh-failed flag until 2026-08-06, when the operator removed that policy; nothing replaced it as a trait-derived outcome. Choosing Fable is now always a deliberate deviation from the rubric — CLAUDE.md is explicit that the rubric stays the only deterministic lookup and a departure from it is recorded in STATE as an override, never as something this skill produces. If Fable is the right call for the next step's form (big-picture, review, planning), write the board line and the rule by hand — board.sh still parses "Fable 5/high" and "Fable 5/xhigh" — and say so plainly in the rationale rather than scoring the four traits to land there.

The last-session record

Write it whenever all four fields are actually known, from what happened in the session that is ending — never from what STATE.md prescribed:

  • --last-model / --last-effort — what this session actually ran with. Both are closed sets (Sonnet 5|Opus 5|Fable 5, and the verified effort levels), because the next session compares these values rather than just displaying them.
  • --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.

Measure the effort, never infer it. Read what this session actually resolved:

echo "$CLAUDE_EFFORT"

CLAUDE_EFFORT is Claude Code's own current effort level, exported into every tool-use context — which is why a Bash call can read it. Pass it verbatim as --last-effort. The model is not in the environment (there is no CLAUDE_MODEL); take it from what this session knows itself to be running as.

Two sources are wrong on purpose, and both fail the same way. The previous board line holds what was prescribed, not what was run — the two come apart exactly when the record would be most interesting. Asking the operator launders that same prescription through a human, who is reading it off the startup command they typed rather than off the running process. Confirming a measured value with them is fine; sourcing it from them is not.

So the record no longer waits on anyone: all four fields are knowable from inside the session that is ending. Still omit it entirely — all four or none — if any one of them is genuinely unknown. A guessed value is worse than a missing one, because it reads back later as a measurement.

This file must never declare an effort: frontmatter field. Skill frontmatter overrides the session effort while the skill is active, so the reading above would report this skill's effort instead of the session's — a measurement measuring itself, with nothing in the output to show it happened. Pinned by route-selftest.sh section 13.

One honest limit: CLAUDE_EFFORT is the current level, so if the operator changed it mid-session with /effort, "the effort this session ran with" is not a single value. Record the level the work was actually done at and say so.

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.

Paste command verbatim. It is the row's whole spelling: model and effort, and nothing else. Do not add flags the calculator did not emit.

The advisor is not this skill's to decide. Until 2026-09-12 the rubric appended an opus advisor on some rows; that rule is struck, on a measurement — of 54 dispatches issued 08.12.09 not one carried it, because sessions are started by hand from the model and effort. The advisor is now an operator decision per session. Never add --advisor to an emitted command on your own initiative; if the operator asks for one, that is their call and it belongs in the handover as what it is — an operator choice, not a rubric outcome.

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.

An advisor the operator asked for is part of that comparison. It is a launch flag, so /clear reuses the process and keeps whatever advisor the session started with — a session that is to gain or lose one needs /exit and a full command, never /clear. The rubric never puts it there; the operator does.

Do not paste the whole output block. One row, the rule that produced it, the command.