feat(route): give the advisor a writer, on a need and per row

route.sh now emits `--advisor opus` into the startup command it prints.
The flag existed and worked, but nothing generated it, so it went unused:
the only mechanism that ever set an advisor here was `/advisor`, which
writes the global advisorModel setting -- every session, every repo -- and
was abandoned for burning quota. Nothing replaced it.

Two independent triggers, almost disjoint by construction:

  rows 1-2  always. Sonnet main model, so opus is a capability LIFT rather
            than a peer. Load-bearing: every fallback is one row cheaper and
            the cheap rows are Sonnet, so this makes the quota fallback safe.
  rows 3-4  only at reversibility=costly|one-way. Opus main model, so the
            advisor buys peer review where a mistake is not cheap to undo.
  rows 5-6  never. The CLI rejects every advisor for a Fable main model.

costly forces row 3 and one-way forces row 4, so a Sonnet row always has
reversibility=cheap and neither rule reaches the other's rows.
verification=none is deliberately not a third trigger: beyond the stakes
rule it adds only cheap-to-reverse mistakes, docs sessions among them.
Applied per ROW, so fallback-command carries its own correct answer.

route-selftest.sh 56 -> 73. Section 14 gates the three CLI facts the rule
rests on against the installed claude without spending a token: advisor
validation runs before the empty-prompt check, so `-p ""` reaches the
validator and stops. --help cannot gate this -- it short-circuits before
option validation, so an unknown flag would pass the gate untested.

Three pre-existing checks updated rather than worked around: two asserted
whole command strings that now carry the advisor, and section 11's effort
extraction swallowed the tail of the command line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8N8hQEJSWWtieWUx37txT
This commit is contained in:
Kjell Tore Guttormsen 2026-07-31 21:39:59 +02:00
commit 9cb405c2cd
10 changed files with 272 additions and 13 deletions

View file

@ -1,6 +1,6 @@
{
"name": "repo-mailbox",
"version": "0.12.1",
"version": "0.13.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.",
"author": {
"name": "Kjell Tore Guttormsen"

View file

@ -5,6 +5,51 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.13.0] - 2026-07-31
### Added
- **`route.sh` now emits `--advisor opus` into the startup command, per row and
on a need.** The advisor is a second, stronger model consulted at key moments
during a session; Claude Code exposes it both as a launch flag and as the
global `advisorModel` setting that `/advisor` writes. The setting was tried
and abandoned here because it applies to every session in every repo and
burned quota accordingly — but nothing replaced it, so the flag went
effectively unused and `route.sh` never mentioned it. Two independent
triggers now decide it:
- **rows 1-2, always.** The main model is Sonnet, so `opus` is a capability
*lift* rather than a peer — Opus judgement at Sonnet cost. This half is
load-bearing: every `fallback-command` is one row cheaper and the cheapest
rows are the Sonnet ones, so this is what makes the quota fallback safe to
take.
- **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
mistake is not cheap to undo.
- **rows 5-6, never.** Not a judgement call: the CLI rejects every advisor
for a Fable main model, and refuses `opus` as under-capable for one.
The two triggers are almost disjoint by construction, since `costly` forces
row 3 and `one-way` forces row 4 — so a Sonnet row always has
`reversibility=cheap`, and neither rule can reach the other's rows.
`verification=none` is deliberately **not** a third trigger: beyond the stakes
rule the only cases it would add are mistakes that are cheap to reverse, docs
sessions (`known/none/cheap/local`) among them. The decision is applied per
ROW rather than once, so `fallback-command` carries its own correct answer.
`route-selftest.sh` grows 17 checks (56 -> 73). Section 14 pins the rule and
gates the three CLI facts it rests on against the installed `claude` without
spending a token: advisor validation runs before the empty-prompt check, so
`-p ""` reaches the validator and stops there. `--help` cannot serve as that
gate — it short-circuits before option validation, so even an unknown flag
exits 0 and the gate would pass without testing anything.
`skills/route/SKILL.md` gains the two failure modes worth naming: paste
`command` verbatim rather than dropping the advisor as noise, never add one to
a command that came back without it, and remember that `--advisor` is a launch
flag — `/clear` reuses the process and keeps whatever advisor the session
started with, so it is not enough when the advisor changes.
## [0.12.1] - 2026-07-31
### Fixed

View file

@ -59,7 +59,7 @@ marketplace plugin. Three components, one boundary:
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
cheaper fallback, and the STATE.md comment lines. Pinned by
`route-selftest.sh` (56 checks).
`route-selftest.sh` (73 checks).
**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
@ -77,6 +77,24 @@ marketplace plugin. Three components, one boundary:
and belongs to the skill; turning scores into a row is a lookup and takes zero
model calls.
**`--advisor opus` is emitted per ROW, on a need, never unconditionally.**
Rows 1-2 always carry it (Sonnet main model, so opus is a capability lift —
and since every fallback is one row cheaper and the cheap rows are Sonnet,
this is what makes the quota fallback safe to take); rows 3-4 only at
`reversibility=costly|one-way` (Opus main model, so it buys peer review where
a mistake is not cheap to undo); rows 5-6 never, because the CLI rejects every
advisor for a Fable main model. The alternative — the global `advisorModel`
setting written by `/advisor` — is what this replaces: it applies to every
session in every repo, which is how it burned quota before. The two triggers
are almost disjoint by construction, since `costly` forces row 3 and `one-way`
forces row 4, so a Sonnet row always has `reversibility=cheap`.
`verification=none` is deliberately NOT a third trigger: beyond the stakes
rule it would only add mistakes that are cheap to reverse, docs sessions
(`known/none/cheap/local`) among them. Section 14 pins the rule and gates the
three CLI facts it rests on against the installed `claude` without spending a
token — advisor validation runs before the empty-prompt check, so `-p ""`
reaches the validator and stops there.
**`--last-effort` is MEASURED from `CLAUDE_EFFORT`, and the calculator must
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
@ -137,7 +155,7 @@ obligations in another repo.
- TDD: no behavior change without a failing selftest check first.
`bash scripts/coord-selftest.sh` must exit 0 (159/159),
`bash scripts/board-selftest.sh` must exit 0 (36/36) and
`bash scripts/route-selftest.sh` must exit 0 (56/56).
`bash scripts/route-selftest.sh` must exit 0 (73/73).
- English for all code, docs, and commit messages (public repo). Norwegian
trigger aliases in the skill description are deliberate.
- Conventional Commits: `type(scope): description`.

View file

@ -8,7 +8,7 @@
*AI-generated: all code produced by Claude Code through dialog-driven development.*
![Version](https://img.shields.io/badge/version-0.12.1-blue)
![Version](https://img.shields.io/badge/version-0.13.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Hooks](https://img.shields.io/badge/hooks-1-green)
![Skills](https://img.shields.io/badge/skills-3-orange)
@ -134,7 +134,7 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
bash scripts/coord-selftest.sh # 159 checks against a throwaway mailbox
bash scripts/board-selftest.sh # 36 checks against a throwaway repo tree
bash scripts/route-selftest.sh # 56 checks, incl. the route->board round trip
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip
npm test # all three selftests via node --test
TDD is the house rule: every behavior change lands with a failing selftest check first.

View file

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

View file

@ -306,11 +306,13 @@ check "board + route + route-last stacked still yield prose in NESTE" $?
# --- 9. Startup command and fallback --------------------------------------
# Two spellings of ONE decision: the rubric name for the board line, the CLI
# alias for the command the operator pastes. They must never disagree.
# These assert the WHOLE string, advisor included, so section 14's rule cannot
# be widened without a deliberate edit here - the exact-match is the tripwire.
cmd="$(field partial strong cheap local command)"
[ "$cmd" = "claude --model opus --effort high" ]; check "command mirrors the row (opus/high)" $?
cmd="$(field known strong cheap local command)"
[ "$cmd" = "claude --model sonnet --effort high" ]; check "command mirrors the row (sonnet/high)" $?
[ "$cmd" = "claude --model sonnet --effort high --advisor opus" ]; check "command mirrors the row (sonnet/high)" $?
# The rubric requires ALWAYS naming one row cheaper as the quota fallback.
fb="$(field partial strong cheap local fallback)"
@ -322,7 +324,7 @@ fb="$(field known strong cheap local fallback)"
# The fallback needs its own pasteable command or the operator translates by
# hand at exactly the moment they are under quota pressure.
fbc="$(field partial strong cheap local fallback-command)"
[ "$fbc" = "claude --model sonnet --effort xhigh" ]; check "fallback ships its own command" $?
[ "$fbc" = "claude --model sonnet --effort xhigh --advisor opus" ]; check "fallback ships its own command" $?
# --- 10. The command carries no 'cd' --------------------------------------
# One repo per terminal tab: a startup command prefixed with cd is wrong by
@ -338,7 +340,10 @@ check "no emitted command contains a cd prefix" "$rc"
# Model aliases are whatever the INSTALLED claude accepts - never hardcoded
# without a gate, because an alias that stops resolving turns every emitted
# command into a paste that fails.
efforts="$(printf '%s' "$out" | sed -n 's/^command=claude --model [a-z]* --effort //p')"
# Capture the effort TOKEN only. Anything may legitimately follow it on the
# command line (--advisor does, since section 14), and a match that swallowed
# the tail would report a valid effort as invalid.
efforts="$(printf '%s' "$out" | sed -n 's/^command=claude --model [a-z]* --effort \([a-z]*\).*/\1/p')"
case "|low|medium|high|xhigh|max|" in *"|$efforts|"*) rc=0 ;; *) rc=1 ;; esac
check "emitted effort is in the verified effort set" "$rc"
@ -409,6 +414,125 @@ check "route.sh no longer claims effort is unobservable from inside" "$rc"
grep -q 'CLAUDE_EFFORT' "$R"
check "route.sh names the source the caller should measure from" $?
# --- 14. The advisor: capability gap first, stakes second -----------------
# The advisor is a second, stronger model consulted mid-task, so a session
# carrying one costs more than a session without. It must therefore fire where
# there is a NEED and nowhere else, or it decays into the always-on
# advisorModel setting it exists to replace. Two independent needs qualify,
# and they turn out to be almost disjoint.
#
# 1. THE MAIN MODEL IS SONNET (rows 1-2). Here opus is a genuine capability
# lift, not a peer: opus judgement at sonnet cost. This is the load-bearing
# half, because every fallback-command is one row cheaper and the cheapest
# rows are Sonnet - so this is what makes the QUOTA FALLBACK safe to take.
# Pinned below by the row-3-falls-back-to-row-2 case.
#
# 2. A MISTAKE IS EXPENSIVE TO UNDO (reversibility costly|one-way). On an Opus
# row the advisor is a peer review rather than a lift, which is worth
# paying for when being wrong is not cheap to reverse.
#
# The two barely overlap: costly forces row 3 and one-way forces row 4, so a
# Sonnet row ALWAYS has reversibility=cheap and trigger 2 can never reach it.
# Rule 1 covers the Sonnet rows, rule 2 covers the Opus rows.
#
# What is deliberately NOT a trigger: verification=none on its own. The set it
# would add beyond rule 2 is exactly {verification=none AND
# reversibility=cheap} - mistakes that are cheap to reverse. It survives on the
# Sonnet rows only via rule 1, which is about the model, not the trait.
adv="$(field known strong cheap local command)"
printf '%s' "$adv" | grep -q -- '--advisor opus'
check "row 1 (Sonnet/high) always carries an advisor" $?
adv="$(field known weak cheap local command)"
printf '%s' "$adv" | grep -q -- '--advisor opus'
check "row 2 (Sonnet/xhigh) always carries an advisor" $?
adv="$(field known strong costly local command)"
printf '%s' "$adv" | grep -q -- '--advisor opus'
check "reversibility=costly carries an advisor onto an Opus row" $?
adv="$(field known strong one-way local command)"
printf '%s' "$adv" | grep -q -- '--advisor opus'
check "reversibility=one-way carries an advisor onto an Opus row" $?
# The Opus rows reached WITHOUT expensive stakes are the whole no-advisor set.
adv="$(field partial strong cheap local command)"
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
check "row 3 via path=partial at cheap stakes takes NO advisor" "$rc"
adv="$(field known strong cheap multi-file command)"
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
check "row 3 via scope=multi-file at cheap stakes takes NO advisor" "$rc"
adv="$(field known strong cheap cross-cutting command)"
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"
# Fable rejects every advisor (measured against 2.1.220, gated below), so both
# triggers must yield to the row. Rows 5 and 6 fire on the failure flag
# regardless of reversibility, so this combination is reachable.
adv="$("$R" --path known --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 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)"
printf '%s' "$adv" | grep -q -- '--advisor opus'
check "row 4 fallback to row 3 keeps the advisor at one-way stakes" $?
# THE POINT OF RULE 1, pinned. Row 3 at cheap stakes takes no advisor, but its
# quota fallback is row 2 - a Sonnet row - which does. Dropping a row under
# quota pressure must not silently drop the safety net with it.
adv="$(field partial strong cheap local fallback-command)"
printf '%s' "$adv" | grep -q -- '--advisor opus'
check "an Opus row with no advisor still falls back to an advised Sonnet row" $?
adv="$(field known strong cheap cross-cutting fallback-command)"
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
check "row 4 falling back to row 3 at cheap stakes stays unadvised" "$rc"
# Closed vocabulary, same reason as the row table: one spelling in circulation.
# fable is not merely weaker here, it is REJECTED as an advisor outright.
allout="$("$R" --path known --verification strong --reversibility one-way \
--scope cross-cutting --rationale x 2>/dev/null)"
bad="$(printf '%s' "$allout" | grep -o -- '--advisor [a-z0-9-]*' | grep -v -- '--advisor opus')"
[ -z "$bad" ]; check "the only advisor value ever emitted is opus:${bad:- none other}" $?
if command -v claude >/dev/null 2>&1; then
# A real gate that spends NO tokens. Advisor validation runs BEFORE the
# empty-prompt check, so `-p ""` reaches the validator and then exits: a
# valid advisor fails on the missing prompt, an invalid one fails on itself.
# --help cannot gate this - it short-circuits before option validation, so
# even an unknown flag exits 0 and the gate would pass without testing.
ADVOUT="$(claude --advisor opus -p "" 2>&1 | head -3)"
if printf '%s' "$ADVOUT" | grep -q 'unknown option'; then rc=1; else rc=0; fi
check "installed claude accepts the --advisor flag at all" "$rc"
if printf '%s' "$ADVOUT" | grep -q 'cannot be used as an advisor'; then rc=1; else rc=0; fi
check "installed claude accepts opus as an advisor" "$rc"
# The suppression on rows 5 and 6 is load-bearing only while this holds.
FABOUT="$(claude --advisor fable -p "" 2>&1 | head -3)"
printf '%s' "$FABOUT" | grep -q 'cannot be used as an advisor'
check "installed claude still rejects fable as an advisor" $?
else
echo " skip - claude not on PATH, advisor flag gate not run"
fi
echo ""
echo "route-selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1

View file

@ -53,6 +53,30 @@
# overkill costs quota every session - but a wrong architecture decision in a
# published plugin costs more than either.
#
# THE ADVISOR is emitted into the command as '--advisor opus' - a second,
# stronger model consulted at key moments during the session. It is added on a
# NEED, never unconditionally: an always-on advisor is the global advisorModel
# setting, which burns quota on every session in every repo and is the thing
# this rule exists to replace. Two independent needs qualify:
#
# rows 1-2 ALWAYS. The main model is Sonnet, so opus is a capability LIFT
# rather than a peer - opus judgement at sonnet cost. This is what
# makes the FALLBACK safe to take: every fallback is one row
# cheaper, and the cheapest rows are the Sonnet ones.
# 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
# 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.
#
# 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
# deliberately not a third trigger - beyond the stakes rule it would only add
# mistakes that are cheap to reverse, docs sessions among them.
#
# Applied per ROW, so 'fallback-command' carries its own correct answer rather
# than the winning row's.
#
# WHERE IT DISAGREES WITH THE RUBRIC'S EXAMPLES. The rows are task-type labels;
# the traits are a different classification over the same six outcomes. They
# part company by one row on the two cheapest rows - documentation scores
@ -226,7 +250,7 @@ row_name() {
5) echo "Fable 5/high" ;; 6) echo "Fable 5/xhigh" ;;
esac
}
row_cmd() {
row_base_cmd() {
case "$1" in
1) echo "claude --model sonnet --effort high" ;;
2) echo "claude --model sonnet --effort xhigh" ;;
@ -237,6 +261,39 @@ row_cmd() {
esac
}
# THE ADVISOR is a second, stronger model consulted mid-task. It costs real
# tokens per session, so it fires on a NEED and nowhere else - an unconditional
# advisor is just the global advisorModel setting, which is the thing this
# replaces. Two independent needs qualify, and they are almost disjoint:
#
# rows 1-2 (Sonnet) ALWAYS. opus is a capability LIFT here, not a peer:
# opus judgement at sonnet cost. This half is what makes
# the fallback-command safe, since every fallback is one
# row cheaper and the cheapest rows are Sonnet.
# 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.
# rows 5-6 (Fable) never. Not a judgement call: the CLI REJECTS every
# advisor for fable ("cannot be used as an advisor"), and
# opus is refused as under-capable for a fable main model.
# Gated against the installed claude by selftest 14.
#
# 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
# rule never reaches rows 3-6. verification=none is deliberately NOT a trigger:
# 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).
#
# Applied per ROW rather than once, because the fallback is a real command the
# operator pastes under quota pressure and must carry its own correct answer.
row_advisor() {
case "$1" in
1|2) echo " --advisor opus" ;;
3|4) case "$2" in costly|one-way) echo " --advisor opus" ;; *) echo "" ;; esac ;;
*) echo "" ;;
esac
}
row_cmd() { printf '%s%s\n' "$(row_base_cmd "$1")" "$(row_advisor "$1" "$REVERS")"; }
FB=$((ROW - 1)); [ "$FB" -ge 1 ] || FB=1
# The rationale is free text from a session and lands inside an HTML comment on

View file

@ -14,7 +14,7 @@ description: >-
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,
already injected at session start.
version: "0.12.1"
version: "0.13.0"
---
# 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
broadcast", "that announcement is outdated, pull it", "trekk tilbake kringkastingen",
"den broadcasten er utdatert".
version: "0.12.1"
version: "0.13.0"
---
# 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
session IS this skill. Not for choosing which REPO gets the next session:
that is the `board` skill.
version: "0.12.1"
version: "0.13.0"
---
# route — what the next session should run with
@ -181,9 +181,24 @@ Give the operator the two closing-line items and nothing more:
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, `--advisor opus` included.** The calculator decides
the advisor per row, and it is not decoration: on a Sonnet row it is what lifts
the session to Opus judgement at Sonnet cost, which is what makes the cheaper
`fallback-command` safe to take under quota pressure. Dropping it because it
looks like noise silently removes that. Equally, never *add* it to a command
that came back without one — an unconditional advisor is the global
`advisorModel` setting, which costs quota in every session in every repo and is
the failure mode this rule replaces. `route.sh --help` carries the full rule.
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.
**`--advisor` is part of that comparison, not an afterthought.** It is a launch
flag, so `/clear` reuses the process and keeps whatever advisor the session
started with. If `command` carries `--advisor opus` and this session was not
launched with it, `/clear` is *not* enough — the operator needs `/exit` and the
full command, or the advisor silently never appears.
Do not paste the whole output block. One row, the rule that produced it, the
command.