fix(route): close the record's drift surface and the confounded smoke test

Six follow-ups on 0d11838, three of which mattered.

The model-alias gate passed for the wrong reason: after the quoted-alias grep
it fell back to an unanchored grep for the bare word, which matches "opus"
anywhere in `claude --help` and would have reported success even if --model
stopped accepting the alias. A gate that cannot fail is worse than no gate.
Only the quoted form is matched now, and the failing aliases are named.

--last-model and --last-effort were unvalidated free text while the other two
record fields were gated. The next session READS the record back to decide
--opus-xhigh-failed, so a drifted spelling there rebuilds the exact
reader-versus-writer drift this script exists to remove, one field over. Both
are closed sets now: the row table's three model names and the verified effort
levels. That also makes the record's sanitizing dead code, so it is gone.

The skill told future sessions to write the record "every session" while
STATE documented that the effort level is not observable from inside a running
session. A session following both would have fabricated the value, and a
fabricated effort reads back later as a measurement. The skill now says: ask
the operator, and omit the record rather than guess -- explicitly including
that reading it off the previous board line measures what was PRESCRIBED, not
what was RUN.

Also: README said "seven bash scripts" (nine files, six user-facing) and its
skills badge still said 2; selftest counts updated to 50.

Verified, not assumed: the installed plugin cache at 0.9.0 contains only
board and coord-send, so route is not discoverable until a release bumps it --
the smoke test STATE had queued before release would have failed with 127 for
a reason unrelated to the skill. STATE reordered to release-then-test. The
manifest is auto_discover, so no skills array needs an entry.
check-versions.mjs is green (11 OK, 0 ERROR) with the new skill at 0.9.0.

Selftests: coord 136, board 30, route 50, 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:19:30 +02:00
commit a435a031db
5 changed files with 70 additions and 16 deletions

View file

@ -45,7 +45,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` (47 checks).
`route-selftest.sh` (50 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
@ -104,7 +104,7 @@ halves together for exactly that reason.
- TDD: no behavior change without a failing selftest check first.
`bash scripts/coord-selftest.sh` must exit 0 (136/136),
`bash scripts/board-selftest.sh` must exit 0 (30/30) and
`bash scripts/route-selftest.sh` must exit 0 (47/47).
`bash scripts/route-selftest.sh` must exit 0 (50/50).
- 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

@ -11,7 +11,7 @@
![Version](https://img.shields.io/badge/version-0.9.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-2-orange)
![Skills](https://img.shields.io/badge/skills-3-orange)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
---
@ -83,7 +83,7 @@ It lives here because it is the **writer** for the cost field the board already
Scoring is judgement and belongs to the skill; turning scores into a row is a lookup and costs no model calls. One deliberate side effect is worth more than the tokens saved: a next step that cannot be scored `known` or `partial`, with no design phase planned, is an **underspecified task description** — the answer is to rewrite the step, not to upgrade the model.
**CLI.** The engine is seven bash scripts in the plugin's `scripts/` directory; resolve them as `"${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/scripts/coord-<name>.sh"` (from a terminal, use the plugin's install path):
**CLI.** The engine is six user-facing bash scripts in the plugin's `scripts/` directory (plus three selftests); resolve them as `"${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/scripts/coord-<name>.sh"` (from a terminal, use the plugin's install path):
coord-send.sh --to <repo> --subject "<subject>" [--message "<text>"] # or body on stdin
coord-send.sh --broadcast --subject "<subject>" <<'BODY' ... BODY
@ -133,7 +133,7 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
bash scripts/coord-selftest.sh # 136 checks against a throwaway mailbox
bash scripts/board-selftest.sh # 30 checks against a throwaway repo tree
bash scripts/route-selftest.sh # 47 checks, incl. the route->board round trip
bash scripts/route-selftest.sh # 50 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

@ -251,6 +251,24 @@ check "no route-last line when the record is omitted" "$rc"
--rationale x --last-corrections three >/dev/null 2>&1
[ $? -eq 2 ]; check "non-numeric --last-corrections exits 2" $?
# The record is read back by the NEXT session to decide --opus-xhigh-failed, so
# 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
# kill, one field over. Both are closed sets: the row table's three model names
# and the verified effort levels.
"$R" --path known --verification strong --reversibility cheap --scope local \
--rationale x --last-model "opus 5" --last-effort xhigh \
--last-completed no --last-corrections 1 >/dev/null 2>&1
[ $? -eq 2 ]; check "--last-model rejects a non-rubric spelling" $?
"$R" --path known --verification strong --reversibility cheap --scope local \
--rationale x --last-model "Opus 5" --last-effort extreme \
--last-completed no --last-corrections 1 >/dev/null 2>&1
[ $? -eq 2 ]; check "--last-effort rejects a value outside the verified set" $?
"$R" --path known --verification strong --reversibility cheap --scope local \
--rationale x --last-model "Fable 5" --last-effort medium \
--last-completed yes --last-corrections 0 >/dev/null 2>&1
[ $? -eq 0 ]; check "--last-model/-effort accept every legal value" $?
# The record is telemetry and must NOT silently fire the Fable rows. Only the
# explicit flag does, because "an opus/xhigh session did not finish" also
# covers context exhaustion, an operator interrupt and a block on another repo
@ -325,13 +343,16 @@ case "|low|medium|high|xhigh|max|" in *"|$efforts|"*) rc=0 ;; *) rc=1 ;; esac
check "emitted effort is in the verified effort set" "$rc"
if command -v claude >/dev/null 2>&1; then
# Match ONLY the quoted alias as --model documents it. An unanchored grep for
# the bare word would hit "opus" anywhere in the help text and pass even if
# --model stopped accepting the alias entirely - a gate that reports success
# without testing anything, which is worse than no gate.
HELPTXT="$(claude --help 2>&1)"
miss=0
miss=""
for alias in fable opus sonnet; do
printf '%s' "$HELPTXT" | grep -q "'$alias'" || \
printf '%s' "$HELPTXT" | grep -q "$alias" || miss=$((miss+1))
printf '%s' "$HELPTXT" | grep -q "'$alias'" || miss="$miss $alias"
done
[ "$miss" -eq 0 ]; check "installed claude still accepts fable/opus/sonnet aliases" $?
[ -z "$miss" ]; check "installed claude documents the aliases route emits:${miss:- all three}" $?
else
echo " skip - claude not on PATH, model alias gate not run"
fi

View file

@ -73,7 +73,8 @@
# route.sh --path <v> --verification <v> --reversibility <v> --scope <v>
# --rationale <text> [--opus-xhigh-failed]
#
# route.sh ... --last-model <name> --last-effort <level>
# route.sh ... --last-model <Sonnet 5|Opus 5|Fable 5>
# --last-effort <low|medium|high|xhigh|max>
# --last-completed <yes|no> --last-corrections <n>
#
# --opus-xhigh-failed is the ONLY way rows 5 and 6 can fire, and it means one
@ -89,6 +90,13 @@
# high counts on row 1 mean the cheap row is too easy to reach, systematically
# zero on row 4 means escalation fires too readily.
#
# All four fields are closed sets or numbers, and required together, because the
# NEXT session reads the record back to decide --opus-xhigh-failed. Note what
# the caller must supply rather than infer: the effort a session ran with is not
# observable from inside that session, and reading it back off the previous
# board line would measure what was PRESCRIBED, not what was RUN. Omit the
# record rather than guess - a guessed value reads back as a measurement.
#
# It is deliberately NOT wired to the Fable rows. Inferring "the model failed"
# from "the session did not finish" would fire on context exhaustion and on
# operator interrupts, which say nothing about the model. The record is
@ -158,6 +166,18 @@ if [ "$L_SET" -eq 1 ]; then
[ -n "$L_EFFORT" ] || die "--last-effort 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"
# 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
# ("opus 5" for "Opus 5") rebuilds the reader-versus-writer drift this whole
# script exists to remove, one field over.
case "$L_MODEL" in
"Sonnet 5"|"Opus 5"|"Fable 5") ;;
*) die "--last-model: '$L_MODEL' is not a row-table model (Sonnet 5|Opus 5|Fable 5)" ;;
esac
case "$L_EFFORT" in
low|medium|high|xhigh|max) ;;
*) die "--last-effort: '$L_EFFORT' is not a verified effort level (low|medium|high|xhigh|max)" ;;
esac
case "$L_DONE" in
yes|no) ;;
*) die "--last-completed: unknown value '$L_DONE' (yes|no)" ;;
@ -231,9 +251,9 @@ echo "command=$(row_cmd "$ROW")"
echo "fallback=$(row_name "$FB")"
echo "fallback-command=$(row_cmd "$FB")"
echo "route-line=<!-- route: path=$PATH_T; verification=$VERIF; reversibility=$REVERS; scope=$SCOPE; rationale=$RAT_CLEAN -->"
# No sanitizing needed on the record: all four fields are validated against
# closed sets above, so none of them can carry a newline or a '-->'.
if [ "$L_SET" -eq 1 ]; then
LM="$(printf '%s' "$L_MODEL" | tr '\r\n' ' ' | tr -d '\000-\037' | sed 's/-->/-- >/g')"
LE="$(printf '%s' "$L_EFFORT" | tr '\r\n' ' ' | tr -d '\000-\037' | sed 's/-->/-- >/g')"
echo "route-last=<!-- route-last: model=$LM; effort=$LE; completed=$L_DONE; corrections=$L_CORR -->"
echo "route-last=<!-- route-last: model=$L_MODEL; effort=$L_EFFORT; completed=$L_DONE; corrections=$L_CORR -->"
fi
exit 0

View file

@ -97,15 +97,28 @@ 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:
policy. 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.
- `--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.
**Effort is not observable from inside a running session.** The model is
readable from the environment; the effort level the operator launched with is
not. So do not infer it, and in particular do not read it back from the previous
board line — that measures what was *prescribed*, not what was *run*, and the
two come apart exactly when the record would be most interesting. Ask the
operator for the effort level when closing the session. If they are not there to
ask, **omit the record entirely** — it is all four fields or none. A guessed
effort is worse than a missing one, because it reads back later as a
measurement.
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