fix(board): cut the rationale before extracting route traits, not after
route_cmd_for()'s four extractions searched the WHOLE route line with a greedy `.*trait=`, so a rationale that names a trait won the match over the field itself. route.sh --help asks for a rationale per score, so a real rationale names the traits routinely - this is the ordinary shape of the data, not an edge case. route.sh then correctly refused the prose, --plan printed command_missing=, and the operator read a broken PARSER as "that repo has no route line": Verifiseringsloven ansikt 4 aimed at our own tooling. Measured by .claude on four live STATE.md files before the order was written, then re-measured here rather than quoted: A/B-ing the pre-fix script against this one over the real tree at the same moment gives 25 tabs, command_missing 7 -> 2, and the five repos that gained a command (app-creator, catalog, claude-code-llm-wiki, llm-security-commons, portfolio-optimiser-commons) are exactly the five the order named. The two that remain are real and belong to their own repos: okr writes scope=multi-module, outside the vocabulary, and llm-security has no route line at all. The cut keys on the FIRST `rationale=`, not on `; rationale=`: everything from that token on is free text by definition, and the separator form would miss a line written without the space. F4's [^;>]* class is untouched - the two answer different questions and neither replaces the other. The board line needs no equivalent change, and that was measured rather than assumed: `grep -n 's/\.\*' scripts/board.sh` is the denominator, and the board line's grammar carries no free-text field. Selftest section 23 (7 checks, 252 -> 259) pins both failure directions, including the sharp inverse where the FIELD is invalid (known2) while the rationale holds a VALID value - before the fix that emitted a confident command= built on prose the STATE line never declared. Two known-positive controls prove the cut does not break the path that already worked. Closes order 20260820T204409Z-8157821110-from-.claude Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZa4oEsa93ac6pZQFEXqBF
This commit is contained in:
parent
29a94dd7e4
commit
b70786db46
3 changed files with 174 additions and 6 deletions
50
CLAUDE.md
50
CLAUDE.md
|
|
@ -347,7 +347,7 @@ marketplace plugin. Three components, one boundary:
|
|||
- **Board (`scripts/board.sh`):** cross-repo attention board. Reads STATE.md
|
||||
next-step blocks + board lines, `git status`, and mailbox pending counts, and
|
||||
prints one line per repo. Read-only by construction: it writes to no repo, no
|
||||
STATE.md and no mailbox. Pinned by `board-selftest.sh` (252 checks).
|
||||
STATE.md and no mailbox. Pinned by `board-selftest.sh` (259 checks).
|
||||
|
||||
**It lives here because the mailbox is one of its three inputs, and it carries
|
||||
the same axis distinction the mailbox does.** A pending count means *others
|
||||
|
|
@ -691,6 +691,52 @@ marketplace plugin. Three components, one boundary:
|
|||
it was exactly the unmeasured side effect the order flagged, and nothing
|
||||
in `~/repos` currently needs it (`.claude` has no repo children today).
|
||||
|
||||
**Ordre 20260820T204409Z (2026-08-20): `route_cmd_for()`'s four trait
|
||||
extractions searched the WHOLE route line, and `.*trait=` is greedy, so a
|
||||
rationale that names a trait won the match over the field itself.**
|
||||
`route.sh --help` asks for a rationale per score, so a real rationale names
|
||||
the traits routinely - this is the ordinary shape of the data, not an edge
|
||||
case. Measured by `.claude` on four live STATE.md files before the order was
|
||||
written: catalog's `path` read `known: notatet som skal skrives...`,
|
||||
app-creator's read `known. Ingen maskin sjekker...`, claude-code-llm-wiki's
|
||||
read `undetermined), det er en prosa/beslutnings...`; okr's read `known`
|
||||
correctly, because its rationale happens to name no trait - and that
|
||||
contrast is what isolates the parser from the data. `route.sh` then refuses
|
||||
the prose (exactly as it should - it was handed garbage, and it is not
|
||||
changed by this fix), `--plan` prints `command_missing=route-linjen kunne
|
||||
ikke tolkes`, and the operator reads a broken PARSER as "that repo has no
|
||||
route line": Verifiseringsloven ansikt 4 turned on our own tooling. The
|
||||
denominator was measured, not estimated - and then re-measured here rather
|
||||
than quoted: A/B-ing the pre-fix script against the fixed one over the real
|
||||
tree at the same moment gives 25 tabs, `command_missing` 7 -> 2, and the five
|
||||
repos that gained a command (app-creator, catalog, claude-code-llm-wiki,
|
||||
llm-security-commons, portfolio-optimiser-commons) are exactly the five the
|
||||
order named. The two that remain are real and belong to their own repos:
|
||||
`okr` writes `scope=multi-module`, outside the vocabulary, and `llm-security`
|
||||
has no route line at all.
|
||||
|
||||
**The fix cuts the line at the first `rationale=` before extracting, and
|
||||
keeps the `[^;>]*` class exactly as F4 left it.** The two are answers to
|
||||
different questions and neither replaces the other: F4 stopped a valid-
|
||||
looking token from being TRUNCATED into something `route.sh` would accept
|
||||
(`known2` -> `known`), this stops the search space from containing prose at
|
||||
all. The cut keys on `rationale=` rather than `; rationale=` because
|
||||
everything from that token on is free text by definition and the separator
|
||||
form would miss a line written without the space. A route line that placed a
|
||||
trait AFTER the rationale would lose it and degrade to `command_missing=` -
|
||||
the safe direction, and the one this fix exists to restore.
|
||||
The board line needs no equivalent change, and that was measured rather than
|
||||
assumed: `grep -n 's/\.\*' scripts/board.sh` is the denominator, and the
|
||||
board line's grammar (`status` / `blocked-on` / `next-cost`) carries no
|
||||
free-text field, so no trait token can occur on it twice. Selftest section
|
||||
23 pins both failure directions - the greedy repo that must now yield a
|
||||
command, and the sharp inverse where the FIELD is invalid (`known2`) while
|
||||
the rationale holds a VALID value: before the fix that emitted a confident
|
||||
`command=` built on prose the STATE line never declared, which is worse than
|
||||
the missing command it usually caused. Two known-positive controls
|
||||
(a trait-free rationale, and a route line with no `rationale=` at all) prove
|
||||
the cut does not break the path that already worked.
|
||||
|
||||
- **Route (`scripts/route.sh`):** pure calculator for the next session's model
|
||||
and effort. Takes four scored traits of the next task plus a required
|
||||
rationale, and prints one block of `key=value` lines: the rubric row, the rule
|
||||
|
|
@ -813,7 +859,7 @@ obligations in another repo.
|
|||
builtins only in hook and tests.
|
||||
- TDD: no behavior change without a failing selftest check first.
|
||||
`bash scripts/coord-selftest.sh` must exit 0 (230/230),
|
||||
`bash scripts/board-selftest.sh` must exit 0 (252/252),
|
||||
`bash scripts/board-selftest.sh` must exit 0 (259/259),
|
||||
`bash scripts/route-selftest.sh` must exit 0 (69/69),
|
||||
`bash scripts/orders-selftest.sh` must exit 0 (110/110) and
|
||||
`bash scripts/state-line-guard-selftest.sh` must exit 0 (40/40).
|
||||
|
|
|
|||
|
|
@ -2038,6 +2038,109 @@ check "board finds a dot-prefixed repo nested under a polyrepo container (depth
|
|||
|
||||
/bin/rm -rf "$DOT_ROOT" 2>/dev/null
|
||||
|
||||
# --- 23. Greedy .*trait= reads the rationale, not the field ----------------
|
||||
# Order 20260820T204409Z-8157821110-from-.claude, measured on four real
|
||||
# STATE.md files: route_cmd_for's four extractions use `sed -n
|
||||
# 's/.*path=\([^;>]*\).*/\1/p'`, and the `.*` prefix is GREEDY. route.sh
|
||||
# --help asks for a rationale per score, so the rationale routinely NAMES the
|
||||
# traits - and then `.*path=` matches the LAST occurrence, inside the free
|
||||
# text, instead of the field. route.sh correctly rejects the prose, the
|
||||
# command degrades to command_missing=, and the operator reads a broken
|
||||
# PARSER as "that repo has no route line" - Verifiseringsloven ansikt 4,
|
||||
# pointed at our own tooling. Denominator at the time: 5 of 27 tabs in the
|
||||
# day plan had no command, all five from this one defect.
|
||||
#
|
||||
# The board line needs no equivalent fix and this was measured, not assumed:
|
||||
# its grammar (status / blocked-on / next-cost) carries no free-text field,
|
||||
# so no trait token can occur twice on it. `grep -n 's/\.\*' scripts/board.sh`
|
||||
# is the denominator - the four route traits are the only extractions whose
|
||||
# search space contains prose.
|
||||
RG_ROOT="$(mktemp -d)"
|
||||
|
||||
# The defect itself: every field is valid, and the rationale names all four
|
||||
# traits the way a real one does. A fixture whose rationale named only SOME
|
||||
# traits would still pass through the wrong extraction path for the others
|
||||
# and go green having proven nothing.
|
||||
mkrepo "$RG_ROOT/repo-greedy-rationale"
|
||||
{
|
||||
echo "# STATE - repo-greedy-rationale"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=planned; blocked-on=-; next-cost=Sonnet 5/high -->"
|
||||
printf '%s%s\n' "<!-- route: path=known; verification=strong; reversibility=cheap; scope=local; " \
|
||||
"rationale=path=known fordi filene er kjent; verification=strong via selftest; reversibility=cheap; scope=local -->"
|
||||
echo "Neste steg for repo-greedy-rationale."
|
||||
} > "$RG_ROOT/repo-greedy-rationale/STATE.md"
|
||||
# --brief lists only repos that OWE a reply, so this fixture needs mail to be
|
||||
# reachable by that check at all. Without it the --brief assertion below could
|
||||
# never pass no matter what route_cmd_for did - a check that measures the
|
||||
# fixture's eligibility instead of the code under test.
|
||||
mkdir -p "$CLAUDE_COORD_DIR/repo-greedy-rationale/inbox"
|
||||
printf -- '---\nfrom: y\nto: repo-greedy-rationale\nsubject: s\n---\nbody\n' \
|
||||
> "$CLAUDE_COORD_DIR/repo-greedy-rationale/inbox/2026-msg1-from-y.md"
|
||||
|
||||
# Known-positive control: a rationale that names no trait at all must keep
|
||||
# producing the same command. This is what isolates the defect - the parser
|
||||
# already WORKS here, and a fix that truncated too much would break it.
|
||||
mkrepo "$RG_ROOT/repo-clean-rationale"
|
||||
{
|
||||
echo "# STATE - repo-clean-rationale"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=planned; blocked-on=-; next-cost=Sonnet 5/high -->"
|
||||
echo "<!-- route: path=known; verification=strong; reversibility=cheap; scope=local; rationale=ren mekanisk endring -->"
|
||||
echo "Neste steg for repo-clean-rationale."
|
||||
} > "$RG_ROOT/repo-clean-rationale/STATE.md"
|
||||
|
||||
# A route line with NO rationale field at all: truncating at '; rationale='
|
||||
# must be a no-op here, never eat the line.
|
||||
mkrepo "$RG_ROOT/repo-no-rationale"
|
||||
{
|
||||
echo "# STATE - repo-no-rationale"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=planned; blocked-on=-; next-cost=Sonnet 5/high -->"
|
||||
echo "<!-- route: path=known; verification=strong; reversibility=cheap; scope=local -->"
|
||||
echo "Neste steg for repo-no-rationale."
|
||||
} > "$RG_ROOT/repo-no-rationale/STATE.md"
|
||||
|
||||
# The sharp case, and the only one that fails in the OPPOSITE direction: the
|
||||
# real field is invalid (known2, the F4 token) while the rationale contains a
|
||||
# VALID one. Greedy extraction reads the rationale, route.sh's exact match
|
||||
# ACCEPTS it, and the plan emits a confident command built on a value the
|
||||
# STATE line never declared. A fix that merely made the refusal more likely
|
||||
# would not catch this - only reading the FIELD does.
|
||||
mkrepo "$RG_ROOT/repo-rationale-overrides"
|
||||
{
|
||||
echo "# STATE - repo-rationale-overrides"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=planned; blocked-on=-; next-cost=Sonnet 5/high -->"
|
||||
echo "<!-- route: path=known2; verification=strong; reversibility=cheap; scope=local; rationale=path=known; feltet over er feil -->"
|
||||
echo "Neste steg for repo-rationale-overrides."
|
||||
} > "$RG_ROOT/repo-rationale-overrides/STATE.md"
|
||||
|
||||
RG_PLAN="$("$BOARD" --roots "$RG_ROOT" --plan 2>/dev/null)"
|
||||
RG_CMD='^command=claude --model sonnet --effort high --advisor opus$'
|
||||
|
||||
printf '%s\n' "$RG_PLAN" | grep -A6 '^repo=repo-greedy-rationale$' | grep -q "$RG_CMD"
|
||||
check "route line whose rationale names all four traits still yields a command" $?
|
||||
printf '%s\n' "$RG_PLAN" | grep -A6 '^repo=repo-greedy-rationale$' | grep -q '^command_missing='; [ $? -ne 0 ]
|
||||
check "greedy-rationale repo is never reported as an unparseable route line" $?
|
||||
|
||||
printf '%s\n' "$RG_PLAN" | grep -A6 '^repo=repo-clean-rationale$' | grep -q "$RG_CMD"
|
||||
check "known-positive: a trait-free rationale keeps producing the same command" $?
|
||||
printf '%s\n' "$RG_PLAN" | grep -A6 '^repo=repo-no-rationale$' | grep -q "$RG_CMD"
|
||||
check "known-positive: a route line with no rationale field at all still parses" $?
|
||||
|
||||
printf '%s\n' "$RG_PLAN" | grep -A6 '^repo=repo-rationale-overrides$' | grep -q '^command_missing='
|
||||
check "an invalid FIELD is refused even when the rationale holds a valid value" $?
|
||||
printf '%s\n' "$RG_PLAN" | grep -A7 '^repo=repo-rationale-overrides$' | grep -q '^paste='; [ $? -ne 0 ]
|
||||
check "the rationale-overrides repo gets no paste line either" $?
|
||||
|
||||
# --brief shares route_cmd_for, so it must recover the same command.
|
||||
RG_BRIEF="$("$BOARD" --roots "$RG_ROOT" --brief 2>/dev/null)"
|
||||
printf '%s' "$RG_BRIEF" | grep -A4 'repo-greedy-rationale' | grep -qE '\$ claude --model sonnet'
|
||||
check "--brief recovers the command too (route_cmd_for has one reader)" $?
|
||||
|
||||
/bin/rm -rf "$RG_ROOT" 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "board-selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
|
|
|||
|
|
@ -593,13 +593,32 @@ route_cmd_for() {
|
|||
# instead of the refusal this defect (F4) exists to force. route.sh still
|
||||
# does the real rejection; this only stops the value from being mangled
|
||||
# into something valid before it gets there.
|
||||
rc_p="$(printf '%s' "$rc_line" | sed -n 's/.*path=\([^;>]*\).*/\1/p' \
|
||||
# ...and it is applied to the line with the RATIONALE CUT OFF, not the raw
|
||||
# line. `.*path=` is greedy, so it matches the LAST `path=` on the line - and
|
||||
# route.sh --help asks for a rationale per score, so a real rationale names
|
||||
# the traits routinely. Measured on four live STATE.md files (order
|
||||
# 20260820T204409Z): catalog's `path` read "known: notatet som skal
|
||||
# skrives...", app-creator's read "known. Ingen maskin sjekker...". route.sh
|
||||
# then correctly refuses the prose, the plan says command_missing=, and the
|
||||
# operator reads a broken PARSER as "that repo has no route line" -
|
||||
# Verifiseringsloven ansikt 4 aimed at our own tooling. Re-measured here by
|
||||
# A/B-ing the pre-fix script against this one over the real tree at the same
|
||||
# moment: 25 tabs, command_missing 7 -> 2, and the five that gained a command
|
||||
# are exactly the five the order named.
|
||||
# The cut is at the FIRST `rationale=`, not at `; rationale=`: everything
|
||||
# from that token on is free text by definition, and keying on the separator
|
||||
# would miss a line written without the space. A route line that put a trait
|
||||
# AFTER the rationale would lose it and degrade to command_missing= - the
|
||||
# safe direction, since the alternative is a confident command built on
|
||||
# prose, which is what this fixes.
|
||||
rc_fields="$(printf '%s' "$rc_line" | sed 's/rationale=.*//')"
|
||||
rc_p="$(printf '%s' "$rc_fields" | sed -n 's/.*path=\([^;>]*\).*/\1/p' \
|
||||
| sed -e 's/--$//' -e 's/[[:space:]]*$//' -e 's/^[[:space:]]*//')"
|
||||
rc_v="$(printf '%s' "$rc_line" | sed -n 's/.*verification=\([^;>]*\).*/\1/p' \
|
||||
rc_v="$(printf '%s' "$rc_fields" | sed -n 's/.*verification=\([^;>]*\).*/\1/p' \
|
||||
| sed -e 's/--$//' -e 's/[[:space:]]*$//' -e 's/^[[:space:]]*//')"
|
||||
rc_r="$(printf '%s' "$rc_line" | sed -n 's/.*reversibility=\([^;>]*\).*/\1/p' \
|
||||
rc_r="$(printf '%s' "$rc_fields" | sed -n 's/.*reversibility=\([^;>]*\).*/\1/p' \
|
||||
| sed -e 's/--$//' -e 's/[[:space:]]*$//' -e 's/^[[:space:]]*//')"
|
||||
rc_s="$(printf '%s' "$rc_line" | sed -n 's/.*scope=\([^;>]*\).*/\1/p' \
|
||||
rc_s="$(printf '%s' "$rc_fields" | sed -n 's/.*scope=\([^;>]*\).*/\1/p' \
|
||||
| sed -e 's/--$//' -e 's/[[:space:]]*$//' -e 's/^[[:space:]]*//')"
|
||||
bash "$ROUTE" --path "$rc_p" --verification "$rc_v" \
|
||||
--reversibility "$rc_r" --scope "$rc_s" --rationale brief 2>/dev/null \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue