feat(route)!: Fable rows are a hand-written override, never a route.sh outcome

The operator removed the "Fable can only be suggested after a failed Opus
5/xhigh session" policy on 2026-08-06 - it stood in the way too often. Until
now that policy was implemented as code: --opus-xhigh-failed was the ONLY
way route.sh's SELECTION chain could reach rows 5-6 (Fable), so the rubric
still enforced a policy the operator had already dropped.

Given a choice between (A) deleting the branches and the flag outright, so
the calculator's output range closes at row 4, or (C) keeping a path that
renders a Fable row on explicit instruction with rule=operator-override, the
operator chose A (AskUserQuestion) - CLAUDE.md is explicit that the rubric
stays the only deterministic lookup and a Fable choice is now always a
deviation from it, recorded in STATE as an override rather than produced
here. board.sh is untouched and still parses "Fable 5/high"/"Fable 5/xhigh"
written by hand into a board line (route-selftest.sh section 6 now pins that
half directly, since route.sh can no longer produce the strings itself).

TDD: every affected selftest check was rewritten to fail against the
unmodified route.sh first (72->68/69, confirmed red), then route.sh was
edited to match. --opus-xhigh-failed is gone outright - passing it now exits
2 like any other unknown argument, not silently accepted as a no-op.

route-selftest.sh: 73 -> 69 checks (three checks tested command shapes
row_advisor() can no longer produce; the two row-5/row-6 reachability checks
in section 1 collapsed into one "the flag is gone" check). coord 191, board
142 unaffected. Suite total 406 -> 402.

Also folds in a standalone fix already pushed this session: section 14 was
gating the wrong CLI fact (whether "claude --advisor fable" is rejected,
which row_advisor() never depends on) instead of the one it actually rests
on (whether opus/sonnet can advise a FABLE main model). Re-pointed and
verified against the installed CC 2.1.226.

Verified before committing: grepped every repo under ~/repos for a route
line carrying --opus-xhigh-failed (none - one repo has it in prose only,
not in its <!-- route: --> comment) and ran board.sh --plan/--brief over the
real tree to confirm no repo's command line broke. Sent a follow-up
coord-send to catalog superseding an earlier now-stale "406" stat-line
correction with the current 402.

skills/route/SKILL.md: usage block, "last-session record" framing, and the
closing --opus-xhigh-failed paragraph rewritten to match. CLAUDE.md, README
and CHANGELOG updated (checks 73->69, badge 406->402, new 0.21.0 entry).
Version bumped 0.20.3 -> 0.21.0 across plugin.json, package.json and all
three skill frontmatters (breaking CLI removal at 0.x -> minor, per v0.20.0
precedent).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ap1WKHCDPcSfpjo4ds2dQX
This commit is contained in:
Kjell Tore Guttormsen 2026-08-09 22:03:05 +02:00
commit dd8f3ce042
10 changed files with 172 additions and 124 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "repo-mailbox", "name": "repo-mailbox",
"version": "0.20.3", "version": "0.21.0",
"description": "Local mailbox for coordination between Claude Code sessions in different repositories. Directed messages and broadcasts as plain Markdown files on your own disk, injected as context at session start. Local, private, no network.", "description": "Local mailbox for coordination between Claude Code sessions in different repositories. Directed messages and broadcasts as plain Markdown files on your own disk, injected as context at session start. Local, private, no network.",
"author": { "author": {
"name": "Kjell Tore Guttormsen" "name": "Kjell Tore Guttormsen"

View file

@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.21.0] - 2026-08-09
### Changed
- **`route.sh` no longer routes to Fable (rows 5-6).** The `--opus-xhigh-failed`
flag was, until 2026-08-06, the only way to reach the Fable rows — mirroring
a global policy that Fable could only be *suggested* after a failed Opus
5/xhigh session. The operator removed that policy ("for ofte ER Fable
riktig"); `route.sh`'s output range shrinks to rows 1-4 rather than gaining
a new trait-derived path to Fable, matching the updated policy's own
framing: a Fable choice is now always a deliberate deviation from the
rubric, recorded in STATE as an override, never produced by the calculator.
The flag itself is gone — passing it now exits 2 like any other unknown
argument. `board.sh` is untouched and still parses "Fable 5/high" /
"Fable 5/xhigh" written by hand into a board line.
`route-selftest.sh`: 73 -> 69 checks (three checks tested a command
`row_advisor()` can no longer produce; the two row-5/row-6 reachability
checks in section 1 collapsed into one "the flag is gone" check).
### Fixed
- **`route-selftest.sh` section 14 was gating the wrong CLI fact.** It checked
whether `claude --advisor fable` is rejected, but `row_advisor()` never
emits `--advisor fable` — it depends on the opposite direction, whether
`opus`/`sonnet` can advise a **fable main model**. Both checks happened to
agree when the section was written; they diverged once the CLI's capability
ordering changed; measured against the installed CC 2.1.226, fable is now
itself accepted as an advisor for opus/sonnet mains, while opus/sonnet
advising a fable main is still rejected. Re-pointed at the fact
`row_advisor()` actually depends on.
## [0.20.3] - 2026-08-09 ## [0.20.3] - 2026-08-09
### Fixed ### Fixed

View file

@ -192,7 +192,7 @@ marketplace plugin. Three components, one boundary:
rationale, and prints one block of `key=value` lines: the rubric row, the rule rationale, and prints one block of `key=value` lines: the rubric row, the rule
that fired, the `next-cost` value, a pasteable startup command, the one-row that fired, the `next-cost` value, a pasteable startup command, the one-row
cheaper fallback, and the STATE.md comment lines. Pinned by cheaper fallback, and the STATE.md comment lines. Pinned by
`route-selftest.sh` (73 checks). `route-selftest.sh` (69 checks).
**It is here because it is the WRITER for the field `board.sh` already reads.** **It is here because it is the WRITER for the field `board.sh` already reads.**
`next-cost` had a reader and no writer, so it was hand-typed every session and `next-cost` had a reader and no writer, so it was hand-typed every session and
@ -228,6 +228,19 @@ marketplace plugin. Three components, one boundary:
token — advisor validation runs before the empty-prompt check, so `-p ""` token — advisor validation runs before the empty-prompt check, so `-p ""`
reaches the validator and stops there. reaches the validator and stops there.
**Rows 5-6 are never a `route.sh` outcome.** Until 2026-08-06 they fired
only from an explicit `--opus-xhigh-failed` flag, mirroring a global
CLAUDE.md policy that Fable could only be *suggested* after a failed Opus
5/xhigh session. That policy was removed by operator decision — "for ofte
ER Fable riktig" — and the flag went with it rather than being repurposed:
`route.sh`'s output range is now closed at row 4, and a Fable choice is
always a hand-written deviation from the rubric, recorded in STATE as an
override per the model-selection rule in the global CLAUDE.md, never
produced by the calculator. `board.sh` still parses "Fable 5/high" and
"Fable 5/xhigh" written by hand into the board line — that parsing is what
the override actually uses, and it is pinned separately from anything
`route.sh` emits (route-selftest.sh section 6).
**`--last-effort` is MEASURED from `CLAUDE_EFFORT`, and the calculator must **`--last-effort` is MEASURED from `CLAUDE_EFFORT`, and the calculator must
never default it.** Claude Code exports that variable into every tool-use never default it.** Claude Code exports that variable into every tool-use
context as the session's current effort, so the caller reads it and passes it context as the session's current effort, so the caller reads it and passes it
@ -288,7 +301,7 @@ obligations in another repo.
- TDD: no behavior change without a failing selftest check first. - TDD: no behavior change without a failing selftest check first.
`bash scripts/coord-selftest.sh` must exit 0 (191/191), `bash scripts/coord-selftest.sh` must exit 0 (191/191),
`bash scripts/board-selftest.sh` must exit 0 (142/142) and `bash scripts/board-selftest.sh` must exit 0 (142/142) and
`bash scripts/route-selftest.sh` must exit 0 (73/73). `bash scripts/route-selftest.sh` must exit 0 (69/69).
- English for all code, docs, and commit messages (public repo). Norwegian - English for all code, docs, and commit messages (public repo). Norwegian
trigger aliases in the skill description are deliberate. trigger aliases in the skill description are deliberate.
- Conventional Commits: `type(scope): description`. - Conventional Commits: `type(scope): description`.

View file

@ -10,11 +10,11 @@ Session A in repo X leaves a message for repo Y; the next session in repo Y gets
*AI-generated: all code produced by Claude Code through dialog-driven development.* *AI-generated: all code produced by Claude Code through dialog-driven development.*
![Version](https://img.shields.io/badge/version-0.20.3-blue) ![Version](https://img.shields.io/badge/version-0.21.0-blue)
![Hooks](https://img.shields.io/badge/hooks-1-green) ![Hooks](https://img.shields.io/badge/hooks-1-green)
![Skills](https://img.shields.io/badge/skills-3-orange) ![Skills](https://img.shields.io/badge/skills-3-orange)
![CLI scripts](https://img.shields.io/badge/CLI_scripts-8-blue) ![CLI scripts](https://img.shields.io/badge/CLI_scripts-8-blue)
![Selftest checks](https://img.shields.io/badge/selftest_checks-406-blue) ![Selftest checks](https://img.shields.io/badge/selftest_checks-402-blue)
--- ---
@ -163,7 +163,7 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
bash scripts/coord-selftest.sh # 191 checks against a throwaway mailbox bash scripts/coord-selftest.sh # 191 checks against a throwaway mailbox
bash scripts/board-selftest.sh # 142 checks against a throwaway repo tree bash scripts/board-selftest.sh # 142 checks against a throwaway repo tree
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip bash scripts/route-selftest.sh # 69 checks, incl. the route->board round trip
npm test # all three selftests via node --test npm test # all three selftests via node --test
TDD is the house rule: every behavior change lands with a failing selftest check first. TDD is the house rule: every behavior change lands with a failing selftest check first.

View file

@ -1,6 +1,6 @@
{ {
"name": "repo-mailbox", "name": "repo-mailbox",
"version": "0.20.3", "version": "0.21.0",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {

View file

@ -33,8 +33,9 @@ field() {
echo "route-selftest" echo "route-selftest"
# --- 1. Every rubric row is reachable ------------------------------------- # --- 1. Every calculator row is reachable -----------------------------------
# A row that no trait combination can produce is dead policy. All six must fire. # A row that no trait combination can produce is dead policy. All four rows
# this calculator can output must fire from traits alone.
got="$(field known strong cheap local next-cost)" got="$(field known strong cheap local next-cost)"
[ "$got" = "Sonnet 5/high" ]; check "row 1: known/strong/cheap/local -> Sonnet 5/high" $? [ "$got" = "Sonnet 5/high" ]; check "row 1: known/strong/cheap/local -> Sonnet 5/high" $?
@ -48,13 +49,14 @@ got="$(field partial strong cheap local next-cost)"
got="$(field known strong cheap cross-cutting next-cost)" got="$(field known strong cheap cross-cutting next-cost)"
[ "$got" = "Opus 5/xhigh" ]; check "row 4: scope=cross-cutting -> Opus 5/xhigh" $? [ "$got" = "Opus 5/xhigh" ]; check "row 4: scope=cross-cutting -> Opus 5/xhigh" $?
got="$("$R" --path known --verification strong --reversibility cheap --scope local \ # Rows 5-6 (Fable) are the operator's hand-written override (policy decision
--rationale x --opus-xhigh-failed 2>/dev/null | sed -n 's/^next-cost=//p')" # 2026-08-06), never a rubric outcome - the calculator's output range is
[ "$got" = "Fable 5/high" ]; check "row 5: opus-xhigh-failed -> Fable 5/high" $? # closed at row 4. The flag that used to gate them into reach is gone
# outright, not merely disarmed: a caller passing it gets the same "unknown
got="$("$R" --path undetermined --verification strong --reversibility cheap --scope local \ # argument" as any other typo.
--rationale x --opus-xhigh-failed 2>/dev/null | sed -n 's/^next-cost=//p')" "$R" --path known --verification strong --reversibility cheap --scope local \
[ "$got" = "Fable 5/xhigh" ]; check "row 6: failed + undetermined -> Fable 5/xhigh" $? --rationale x --opus-xhigh-failed >/dev/null 2>&1
[ $? -eq 2 ]; check "--opus-xhigh-failed is gone: unknown argument, not a route to Fable" $?
# --- 2. Escalation is asymmetric ------------------------------------------ # --- 2. Escalation is asymmetric ------------------------------------------
# One trait escalates; a downgrade needs ALL of them. Underkill costs one # One trait escalates; a downgrade needs ALL of them. Underkill costs one
@ -77,29 +79,28 @@ got="$(field undetermined strong cheap local next-cost)"
[ "$got" = "Opus 5/high" ]; check "path=undetermined stops at row 3, not row 4" $? [ "$got" = "Opus 5/high" ]; check "path=undetermined stops at row 3, not row 4" $?
# --- 3. The emitted vocabulary is CLOSED ---------------------------------- # --- 3. The emitted vocabulary is CLOSED ----------------------------------
# Every one of the 3*3*3*3 trait combinations, in both history states, must # Every one of the 3*3*3*3 trait combinations must emit one of exactly FOUR
# emit one of exactly six strings. This is what structurally prevents a # strings - the calculator's whole output range now that Fable is reached
# seventh spelling from ever entering circulation: not a convention, an # only by a hand-written operator override, never by this script. This is
# enumeration. The board line's drift was possible only because the field had # what structurally prevents a fifth spelling from ever entering circulation:
# no writer with a closed range. # not a convention, an enumeration. The board line's drift was possible only
VOCAB="|Sonnet 5/high|Sonnet 5/xhigh|Opus 5/high|Opus 5/xhigh|Fable 5/high|Fable 5/xhigh|" # because the field had no writer with a closed range.
VOCAB="|Sonnet 5/high|Sonnet 5/xhigh|Opus 5/high|Opus 5/xhigh|"
bad=0; n=0 bad=0; n=0
for p in known partial undetermined; do for p in known partial undetermined; do
for v in strong weak none; do for v in strong weak none; do
for r in cheap costly one-way; do for r in cheap costly one-way; do
for s in local multi-file cross-cutting; do for s in local multi-file cross-cutting; do
for h in "" "--opus-xhigh-failed"; do n=$((n+1))
n=$((n+1)) out="$("$R" --path "$p" --verification "$v" --reversibility "$r" --scope "$s" \
out="$("$R" --path "$p" --verification "$v" --reversibility "$r" --scope "$s" \ --rationale x 2>/dev/null | sed -n 's/^next-cost=//p')"
--rationale x $h 2>/dev/null | sed -n 's/^next-cost=//p')" case "$VOCAB" in *"|$out|"*) ;; *) bad=$((bad+1)); echo " out-of-vocab: $p/$v/$r/$s [$out]" ;; esac
case "$VOCAB" in *"|$out|"*) ;; *) bad=$((bad+1)); echo " out-of-vocab: $p/$v/$r/$s [$out]" ;; esac
done
done done
done done
done done
done done
[ "$n" -eq 162 ] && [ "$bad" -eq 0 ] [ "$n" -eq 81 ] && [ "$bad" -eq 0 ]
check "all 162 trait combinations emit one of the 6 rubric rows" $? check "all 81 trait combinations emit one of the 4 calculator rows" $?
# --- 4. Every trait is REQUIRED ------------------------------------------- # --- 4. Every trait is REQUIRED -------------------------------------------
# verification carries the most signal and is the one most often left out, so # verification carries the most signal and is the one most often left out, so
@ -161,12 +162,13 @@ for combo in "known strong cheap local" "known weak cheap local" \
cost="$(field "$1" "$2" "$3" "$4" next-cost)" cost="$(field "$1" "$2" "$3" "$4" next-cost)"
rt_case "rt-$i" "$cost" rt_case "rt-$i" "$cost"
done done
for h in 5 6; do # Rows 5-6 are never emitted by route.sh any more (policy decision
if [ "$h" = "5" ]; then pp=known; else pp=undetermined; fi # 2026-08-06), but board.sh must still parse them back when the operator
cost="$("$R" --path "$pp" --verification strong --reversibility cheap --scope local \ # hand-writes a Fable board line - that is exactly the path that replaces the
--rationale x --opus-xhigh-failed 2>/dev/null | sed -n 's/^next-cost=//p')" # removed rubric outcome, so the literal strings are spliced in directly here
rt_case "rt-$h" "$cost" # rather than produced by "$R".
done rt_case "rt-5" "Fable 5/high"
rt_case "rt-6" "Fable 5/xhigh"
OUT="$(CLAUDE_COORD_DIR="$MBOX" "$BOARD" --roots "$ROOT" 2>/dev/null)" OUT="$(CLAUDE_COORD_DIR="$MBOX" "$BOARD" --roots "$ROOT" 2>/dev/null)"
for want in "Sonnet 5/high" "Sonnet 5/xhigh" "Opus 5/high" "Opus 5/xhigh" \ for want in "Sonnet 5/high" "Sonnet 5/xhigh" "Opus 5/high" "Opus 5/xhigh" \
@ -225,12 +227,13 @@ LINE3="$("$R" --path known --verification strong --reversibility cheap --scope l
check "'-->' in rationale cannot close the comment early" $? check "'-->' in rationale cannot close the comment early" $?
# --- 8b. The last-session record ------------------------------------------ # --- 8b. The last-session record ------------------------------------------
# Rows 5 and 6 are history-dependent, so without a record of how the previous # The record is the cheap proxy for whether the routing itself is any good:
# session went they are dead policy. The record is also the cheap proxy for # systematically high corrections on row 1 means the cheap row is too easy to
# whether the routing itself is any good: systematically high corrections on # reach, systematically zero on row 4 means escalation fires too readily. It
# row 1 means the cheap row is too easy to reach, systematically zero on row 4 # gets a WRITER here for the same reason next-cost needed one - a format with
# means escalation fires too readily. It gets a WRITER here for the same # only a reader drifts. It is pure telemetry - it never changes what the
# reason next-cost needed one - a format with only a reader drifts. # calculator outputs, Fable rows included, which are unreachable through it
# entirely now.
LAST="$("$R" --path known --verification strong --reversibility cheap --scope local \ LAST="$("$R" --path known --verification strong --reversibility cheap --scope local \
--rationale x --last-model "Opus 5" --last-effort xhigh \ --rationale x --last-model "Opus 5" --last-effort xhigh \
--last-completed no --last-corrections 3 2>/dev/null | sed -n 's/^route-last=//p')" --last-completed no --last-corrections 3 2>/dev/null | sed -n 's/^route-last=//p')"
@ -251,7 +254,7 @@ check "no route-last line when the record is omitted" "$rc"
--rationale x --last-corrections three >/dev/null 2>&1 --rationale x --last-corrections three >/dev/null 2>&1
[ $? -eq 2 ]; check "non-numeric --last-corrections exits 2" $? [ $? -eq 2 ]; check "non-numeric --last-corrections exits 2" $?
# The record is read back by the NEXT session to decide --opus-xhigh-failed, so # The record is read back by the NEXT session as evidence months from now, so
# its model and effort are compared, not just displayed. Leaving them as free # its model and effort are compared, not just displayed. Leaving them as free
# text would rebuild the exact reader-versus-writer drift this script exists to # text would rebuild the exact reader-versus-writer drift this script exists to
# kill, one field over. Both are closed sets: the row table's three model names # kill, one field over. Both are closed sets: the row table's three model names
@ -269,15 +272,16 @@ check "no route-last line when the record is omitted" "$rc"
--last-completed yes --last-corrections 0 >/dev/null 2>&1 --last-completed yes --last-corrections 0 >/dev/null 2>&1
[ $? -eq 0 ]; check "--last-model/-effort accept every legal value" $? [ $? -eq 0 ]; check "--last-model/-effort accept every legal value" $?
# The record is telemetry and must NOT silently fire the Fable rows. Only the # The record is telemetry and must NOT silently change what the calculator
# explicit flag does, because "an opus/xhigh session did not finish" also # outputs - a "completed=no" record describes what happened, and covers
# covers context exhaustion, an operator interrupt and a block on another repo # context exhaustion, an operator interrupt and a block on another repo just
# - none of which mean the MODEL failed at this step. Fable runs without an # as much as an actual model failure. Reading it as an inference would revive
# advisor, so this auto-escalation has to stay a judgement, not an inference. # exactly the escalation path the removed --opus-xhigh-failed flag used to
# gate deliberately.
got="$("$R" --path known --verification strong --reversibility cheap --scope local \ got="$("$R" --path known --verification strong --reversibility cheap --scope local \
--rationale x --last-model "Opus 5" --last-effort xhigh --last-completed no \ --rationale x --last-model "Opus 5" --last-effort xhigh --last-completed no \
--last-corrections 4 2>/dev/null | sed -n 's/^next-cost=//p')" --last-corrections 4 2>/dev/null | sed -n 's/^next-cost=//p')"
[ "$got" = "Sonnet 5/high" ]; check "a failed opus/xhigh record alone does NOT reach Fable" $? [ "$got" = "Sonnet 5/high" ]; check "a failed-session record alone does not change the routing outcome" $?
# A record is all four fields or none. A partial one emits `corrections=` with # A record is all four fields or none. A partial one emits `corrections=` with
# nothing after it, which reads back later exactly like a measured zero. # nothing after it, which reads back later exactly like a measured zero.
@ -469,26 +473,12 @@ adv="$(field known strong cheap cross-cutting command)"
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
check "row 4 via scope=cross-cutting at cheap stakes takes NO advisor" "$rc" check "row 4 via scope=cross-cutting at cheap stakes takes NO advisor" "$rc"
# Fable rejects every advisor (measured against 2.1.220, gated below), so both # Rows 5-6 (Fable) are unreachable through this calculator now - section 1
# triggers must yield to the row. Rows 5 and 6 fire on the failure flag # pins that directly, and section 3's closed vocabulary covers every command
# regardless of reversibility, so this combination is reachable. # this script can ever emit. There is nothing left here for "Fable takes no
adv="$("$R" --path known --verification strong --reversibility one-way --scope local \ # advisor" to test: that fact still holds (gated against the installed claude
--rationale x --opus-xhigh-failed 2>/dev/null | sed -n 's/^command=//p')" # by the CLI check below), but row_advisor()'s branch for it can no longer be
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi # exercised through the public interface.
check "row 5 (Fable) takes no advisor even at one-way stakes" "$rc"
adv="$("$R" --path undetermined --verification strong --reversibility one-way --scope local \
--rationale x --opus-xhigh-failed 2>/dev/null | sed -n 's/^command=//p')"
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
check "row 6 (Fable) takes no advisor even at one-way stakes" "$rc"
# The fallback is a real command the operator pastes under quota pressure, so
# the advisor decision has to be made per ROW, not once for the winning row.
# Row 5 falls back to row 4, which is Opus and CAN advise.
adv="$("$R" --path known --verification strong --reversibility one-way --scope local \
--rationale x --opus-xhigh-failed 2>/dev/null | sed -n 's/^fallback-command=//p')"
printf '%s' "$adv" | grep -q -- '--advisor opus'
check "Fable row falling back to an Opus row regains the advisor" $?
adv="$(field known strong one-way local fallback-command)" adv="$(field known strong one-way local fallback-command)"
printf '%s' "$adv" | grep -q -- '--advisor opus' printf '%s' "$adv" | grep -q -- '--advisor opus'

View file

@ -8,17 +8,31 @@
# and no writer, so its value was typed by hand every session and drifted into # and no writer, so its value was typed by hand every session and drifted into
# several competing spellings. Cleaning the data could not fix that. # several competing spellings. Cleaning the data could not fix that.
# A writer with a CLOSED output range can: this script can only ever emit one # A writer with a CLOSED output range can: this script can only ever emit one
# of six strings, so a seventh cannot enter circulation. # of four strings, so a fifth cannot enter circulation.
# #
# THE ROW TABLE IS THE POLICY, and it is the operator's rubric verbatim - # THE ROW TABLE IS THE POLICY, and it is the operator's rubric verbatim -
# moved here so there is one copy rather than one per repo: # moved here so there is one copy rather than one per repo. It has six rows;
# this calculator only ever computes four of them:
# #
# 1 Sonnet 5/high reading, summarizing, docs, mechanical refactor # 1 Sonnet 5/high reading, summarizing, docs, mechanical refactor
# 2 Sonnet 5/xhigh TDD cycle, known-root-cause bugfix, one-file change # 2 Sonnet 5/xhigh TDD cycle, known-root-cause bugfix, one-file change
# 3 Opus 5/high multi-file feature, architecture choice, hard debugging # 3 Opus 5/high multi-file feature, architecture choice, hard debugging
# 4 Opus 5/xhigh long autonomous run, big refactor, cross-repo migration # 4 Opus 5/xhigh long autonomous run, big refactor, cross-repo migration
# 5 Fable 5/high Opus 5/xhigh has ACTUALLY failed on this same step # 5 Fable 5/high deliberate choice for big-picture/review/planning work
# 6 Fable 5/xhigh open problem, no known solution path, longest horizon # 6 Fable 5/xhigh same, open-ended or longest horizon
#
# ROWS 5-6 ARE NEVER COMPUTED HERE. Until 2026-08-06 they fired only from an
# explicit --opus-xhigh-failed flag; that policy is REMOVED (operator
# decision), and nothing replaces it as a rubric outcome. A Fable choice is
# now always a deliberate deviation from this calculator - 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 produced here.
# Write "Fable 5/high" or "Fable 5/xhigh" into the board line by hand when
# that is the right call; board.sh still parses both (route-selftest.sh
# section 6 pins that half). The one fact worth carrying: a Fable session
# runs without an advisor (row_advisor() below, and gated against the
# installed claude by route-selftest.sh section 14) - informational, never a
# gate on reaching the row, since there is no longer a gate to reach.
# #
# Cheapest first, so the rubric's "always name one row cheaper as the quota # Cheapest first, so the rubric's "always name one row cheaper as the quota
# fallback" is row minus one, floored at row 1, correct by construction. # fallback" is row minus one, floored at row 1, correct by construction.
@ -40,9 +54,8 @@
# and a default would be indistinguishable from a real score when the log is # and a default would be indistinguishable from a real score when the log is
# read back to find out whether the ROUTING was wrong or the SCORING was. # read back to find out whether the ROUTING was wrong or the SCORING was.
# #
# SELECTION - first match wins, most expensive first: # SELECTION - first match wins, most expensive first. Rows 5-6 do not appear:
# row 6 history says Opus 5/xhigh failed here, and path=undetermined # they are never a trait-derived outcome (see above).
# row 5 history says Opus 5/xhigh failed here
# row 4 reversibility=one-way OR scope=cross-cutting # row 4 reversibility=one-way OR scope=cross-cutting
# row 3 path=partial|undetermined OR reversibility=costly OR scope=multi-file # row 3 path=partial|undetermined OR reversibility=costly OR scope=multi-file
# row 2 verification=weak|none # row 2 verification=weak|none
@ -66,8 +79,11 @@
# rows 3-4 only at reversibility=costly|one-way. The main model is already # rows 3-4 only at reversibility=costly|one-way. The main model is already
# Opus, so the advisor buys peer review, worth paying for when a # Opus, so the advisor buys peer review, worth paying for when a
# mistake is not cheap to undo. # mistake is not cheap to undo.
# rows 5-6 NEVER, and not as a judgement call: the CLI rejects every #
# advisor for a Fable main model. # Rows 5-6 never reach this logic at all - the calculator cannot select them
# (see above). Informational only: were the operator to hand-write a Fable
# command, it would carry no advisor either way, since the CLI rejects every
# advisor for a Fable main model.
# #
# The two triggers barely overlap: costly forces row 3 and one-way forces row # The two triggers barely overlap: costly forces row 3 and one-way forces row
# 4, so a Sonnet row always has reversibility=cheap. verification=none is # 4, so a Sonnet row always has reversibility=cheap. verification=none is
@ -95,18 +111,12 @@
# #
# Usage: # Usage:
# route.sh --path <v> --verification <v> --reversibility <v> --scope <v> # route.sh --path <v> --verification <v> --reversibility <v> --scope <v>
# --rationale <text> [--opus-xhigh-failed] # --rationale <text>
# #
# route.sh ... --last-model <Sonnet 5|Opus 5|Fable 5> # route.sh ... --last-model <Sonnet 5|Opus 5|Fable 5>
# --last-effort <low|medium|high|xhigh|max> # --last-effort <low|medium|high|xhigh|max>
# --last-completed <yes|no> --last-corrections <n> # --last-completed <yes|no> --last-corrections <n>
# #
# --opus-xhigh-failed is the ONLY way rows 5 and 6 can fire, and it means one
# narrow thing: an Opus 5/xhigh session already ran at THIS SAME next step and
# did not complete it. Not "a session failed" - context exhaustion, an operator
# interrupt or a block on another repo are not this flag. It escalates to a
# model that runs without an advisor, so when in doubt, leave it off.
#
# THE LAST-SESSION RECORD (the four --last-* fields, all or none) is what makes # THE LAST-SESSION RECORD (the four --last-* fields, all or none) is what makes
# any of this falsifiable. It records how the session that just ran actually # any of this falsifiable. It records how the session that just ran actually
# went, so the policy can later be judged against outcomes instead of against # went, so the policy can later be judged against outcomes instead of against
@ -114,9 +124,10 @@
# high counts on row 1 mean the cheap row is too easy to reach, systematically # high counts on row 1 mean the cheap row is too easy to reach, systematically
# zero on row 4 means escalation fires too readily. # zero on row 4 means escalation fires too readily.
# #
# All four fields are closed sets or numbers, and required together, because the # All four fields are closed sets or numbers, and required together, so the
# NEXT session reads the record back to decide --opus-xhigh-failed. Every one of # record reads back later as evidence rather than a guess - a partial record
# them must be MEASURED by the caller: --last-effort comes from CLAUDE_EFFORT, # would emit an empty value indistinguishable from a real measurement. Every
# one of them must be MEASURED by the caller: --last-effort comes from CLAUDE_EFFORT,
# which Claude Code exports into every tool-use context as the session's current # which Claude Code exports into every tool-use context as the session's current
# effort level. It is deliberately NOT defaulted from that variable here - a # effort level. It is deliberately NOT defaulted from that variable here - a
# calculator that reads its own environment stops being deterministic from its # calculator that reads its own environment stops being deterministic from its
@ -126,18 +137,17 @@
# off the startup command they typed. Omit the record rather than guess - a # off the startup command they typed. Omit the record rather than guess - a
# guessed value reads back as a measurement. # guessed value reads back as a measurement.
# #
# It is deliberately NOT wired to the Fable rows. Inferring "the model failed" # It is pure telemetry and never changes what the calculator outputs.
# from "the session did not finish" would fire on context exhaustion and on # Inferring "the model failed" from "the session did not finish" would fire on
# operator interrupts, which say nothing about the model. The record is # context exhaustion and on operator interrupts, which say nothing about the
# evidence; --opus-xhigh-failed is a judgement. Keeping them apart is why the # model - so the record stays descriptive, never a trigger.
# evidence stays worth reading.
# #
# Exit 0 on a decision, 2 on any bad or missing argument. ASCII only, # Exit 0 on a decision, 2 on any bad or missing argument. ASCII only,
# bash 3.2 safe. # bash 3.2 safe.
set -u set -u
export LC_ALL=C export LC_ALL=C
PATH_T=""; VERIF=""; REVERS=""; SCOPE=""; RATIONALE=""; RAT_SET=0; FAILED=0 PATH_T=""; VERIF=""; REVERS=""; SCOPE=""; RATIONALE=""; RAT_SET=0
L_MODEL=""; L_EFFORT=""; L_DONE=""; L_CORR=""; L_SET=0 L_MODEL=""; L_EFFORT=""; L_DONE=""; L_CORR=""; L_SET=0
die() { echo "route: $1" >&2; exit 2; } die() { echo "route: $1" >&2; exit 2; }
@ -152,7 +162,6 @@ while [ $# -gt 0 ]; do
--reversibility) need "$@"; REVERS="$2"; shift 2 ;; --reversibility) need "$@"; REVERS="$2"; shift 2 ;;
--scope) need "$@"; SCOPE="$2"; shift 2 ;; --scope) need "$@"; SCOPE="$2"; shift 2 ;;
--rationale) need "$@"; RATIONALE="$2"; RAT_SET=1; shift 2 ;; --rationale) need "$@"; RATIONALE="$2"; RAT_SET=1; shift 2 ;;
--opus-xhigh-failed) FAILED=1; shift ;;
--last-model) need "$@"; L_MODEL="$2"; L_SET=1; shift 2 ;; --last-model) need "$@"; L_MODEL="$2"; L_SET=1; shift 2 ;;
--last-effort) need "$@"; L_EFFORT="$2"; L_SET=1; shift 2 ;; --last-effort) need "$@"; L_EFFORT="$2"; L_SET=1; shift 2 ;;
--last-completed) need "$@"; L_DONE="$2"; L_SET=1; shift 2 ;; --last-completed) need "$@"; L_DONE="$2"; L_SET=1; shift 2 ;;
@ -196,7 +205,7 @@ if [ "$L_SET" -eq 1 ]; then
[ -n "$L_DONE" ] || die "--last-completed is required with a last-session record" [ -n "$L_DONE" ] || die "--last-completed is required with a last-session record"
[ -n "$L_CORR" ] || die "--last-corrections is required with a last-session record" [ -n "$L_CORR" ] || die "--last-corrections is required with a last-session record"
# Model and effort are closed sets here, not free text. The next session READS # Model and effort are closed sets here, not free text. The next session READS
# this record back to decide --opus-xhigh-failed, so a drifted spelling # this record back as evidence months from now, so a drifted spelling
# ("opus 5" for "Opus 5") rebuilds the reader-versus-writer drift this whole # ("opus 5" for "Opus 5") rebuilds the reader-versus-writer drift this whole
# script exists to remove, one field over. # script exists to remove, one field over.
case "$L_MODEL" in case "$L_MODEL" in
@ -217,11 +226,9 @@ if [ "$L_SET" -eq 1 ]; then
fi fi
# --- Selection: first match wins, most expensive first --------------------- # --- Selection: first match wins, most expensive first ---------------------
if [ "$FAILED" -eq 1 ] && [ "$PATH_T" = "undetermined" ]; then # Rows 5-6 (Fable) never appear: they are a hand-written operator override,
ROW=6; RULE="opus-xhigh-failed + path=undetermined" # never a trait-derived outcome (see the header note).
elif [ "$FAILED" -eq 1 ]; then if [ "$REVERS" = "one-way" ]; then
ROW=5; RULE="opus-xhigh-failed"
elif [ "$REVERS" = "one-way" ]; then
ROW=4; RULE="reversibility=one-way" ROW=4; RULE="reversibility=one-way"
elif [ "$SCOPE" = "cross-cutting" ]; then elif [ "$SCOPE" = "cross-cutting" ]; then
ROW=4; RULE="scope=cross-cutting" ROW=4; RULE="scope=cross-cutting"
@ -242,12 +249,12 @@ fi
# goes in the command the operator pastes. Emitting both from one table is the # goes in the command the operator pastes. Emitting both from one table is the
# point - two hand-maintained spellings of one decision is how they disagree. # point - two hand-maintained spellings of one decision is how they disagree.
# Aliases are gated against the installed claude by route-selftest.sh section # Aliases are gated against the installed claude by route-selftest.sh section
# 11, never assumed here. # 11, never assumed here. Rows 5-6 have no entry: $ROW can never be 5 or 6
# (see SELECTION above), so a case arm for them would be dead code.
row_name() { row_name() {
case "$1" in case "$1" in
1) echo "Sonnet 5/high" ;; 2) echo "Sonnet 5/xhigh" ;; 1) echo "Sonnet 5/high" ;; 2) echo "Sonnet 5/xhigh" ;;
3) echo "Opus 5/high" ;; 4) echo "Opus 5/xhigh" ;; 3) echo "Opus 5/high" ;; 4) echo "Opus 5/xhigh" ;;
5) echo "Fable 5/high" ;; 6) echo "Fable 5/xhigh" ;;
esac esac
} }
row_base_cmd() { row_base_cmd() {
@ -256,8 +263,6 @@ row_base_cmd() {
2) echo "claude --model sonnet --effort xhigh" ;; 2) echo "claude --model sonnet --effort xhigh" ;;
3) echo "claude --model opus --effort high" ;; 3) echo "claude --model opus --effort high" ;;
4) echo "claude --model opus --effort xhigh" ;; 4) echo "claude --model opus --effort xhigh" ;;
5) echo "claude --model fable --effort high" ;;
6) echo "claude --model fable --effort xhigh" ;;
esac esac
} }
@ -272,14 +277,17 @@ row_base_cmd() {
# row cheaper and the cheapest rows are Sonnet. # row cheaper and the cheapest rows are Sonnet.
# rows 3-4 (Opus) only at costly|one-way stakes, where the advisor is a # rows 3-4 (Opus) only at costly|one-way stakes, where the advisor is a
# peer review and being wrong is not cheap to undo. # peer review and being wrong is not cheap to undo.
# rows 5-6 (Fable) never. Not a judgement call: the CLI REJECTS every #
# advisor for fable ("cannot be used as an advisor"), and # Rows 5-6 (Fable) never reach this function - $ROW can only be 1-4 (see
# opus is refused as under-capable for a fable main model. # SELECTION above). Informational only: the only advisor this script ever
# Gated against the installed claude by selftest 14. # emits is opus (pinned by selftest 14's "the only advisor value ever emitted
# is opus"), and opus is refused as under-capable for a fable main model -
# measured against the installed claude, still true at CC 2.1.226 - so a
# hand-written Fable command carries no advisor either way.
# #
# costly forces row 3 and one-way forces row 4, so a Sonnet row always has # costly forces row 3 and one-way forces row 4, so a Sonnet row always has
# reversibility=cheap - the stakes rule can never reach rows 1-2, and the model # reversibility=cheap - the stakes rule can never reach rows 1-2, and the model
# rule never reaches rows 3-6. verification=none is deliberately NOT a trigger: # rule never reaches rows 3-4. verification=none is deliberately NOT a trigger:
# beyond the stakes rule it would only add cheap-to-reverse mistakes, and it # beyond the stakes rule it would only add cheap-to-reverse mistakes, and it
# would put an advisor on every docs session (known/none/cheap/local). # would put an advisor on every docs session (known/none/cheap/local).
# #

View file

@ -20,7 +20,7 @@ description: >-
user names no repo and no tool — choosing *between* repos is this skill. Not for user names no repo and no tool — choosing *between* repos is this skill. Not for
"where were we" inside the current repo: that is this repo's own STATE.md, "where were we" inside the current repo: that is this repo's own STATE.md,
already injected at session start. already injected at session start.
version: "0.20.3" version: "0.21.0"
--- ---
# board — which repo deserves the next session # board — which repo deserves the next session

View file

@ -15,7 +15,7 @@ description: >-
covers retiring a broadcast that has become wrong or obsolete: "retract that covers retiring a broadcast that has become wrong or obsolete: "retract that
broadcast", "that announcement is outdated, pull it", "trekk tilbake kringkastingen", broadcast", "that announcement is outdated, pull it", "trekk tilbake kringkastingen",
"den broadcasten er utdatert". "den broadcasten er utdatert".
version: "0.20.3" version: "0.21.0"
--- ---
# coord-send — natural-language front door for inter-repo messages # coord-send — natural-language front door for inter-repo messages

View file

@ -14,7 +14,7 @@ description: >-
the operator names no model and no tool — choosing the model for the next 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: session IS this skill. Not for choosing which REPO gets the next session:
that is the `board` skill. that is the `board` skill.
version: "0.20.3" version: "0.21.0"
--- ---
# route — what the next session should run with # route — what the next session should run with
@ -40,7 +40,6 @@ scoring step and are doing the thing this skill replaces.
--reversibility <cheap|costly|one-way> \ --reversibility <cheap|costly|one-way> \
--scope <local|multi-file|cross-cutting> \ --scope <local|multi-file|cross-cutting> \
--rationale "why these four scores" \ --rationale "why these four scores" \
[--opus-xhigh-failed] \
[--last-model <name> --last-effort <level> \ [--last-model <name> --last-effort <level> \
--last-completed <yes|no> --last-corrections <n>] --last-completed <yes|no> --last-corrections <n>]
@ -94,11 +93,24 @@ procrastination available, and it hides the real defect.
This check is worth more than the tokens the routing saves. Do not skip it by This check is worth more than the tokens the routing saves. Do not skip it by
scoring `partial` to keep things moving. 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. One fact worth carrying into that
override: a Fable session runs without an advisor.
## The last-session record ## The last-session record
Rows 5 and 6 (Fable) are history-dependent, so without a record they are dead Write it whenever all four fields are actually known, from what happened in
policy. Write it whenever all four fields are actually known, from what happened the session that is ending — never from what STATE.md prescribed:
in the session that is ending — never from what STATE.md prescribed:
- `--last-model` / `--last-effort` — what this session actually ran with. Both - `--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), are closed sets (`Sonnet 5|Opus 5|Fable 5`, and the verified effort levels),
@ -143,13 +155,6 @@ 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 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. a single value. Record the level the work was actually done at and say so.
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 ## Writing it into STATE.md
Three single-line HTML comments sit directly under the `👉 NESTE` heading, in Three single-line HTML comments sit directly under the `👉 NESTE` heading, in