Commit graph

71 commits

Author SHA1 Message Date
61aebad748 fix(board): --brief's zero-debt branch no longer claims zero pending mail v0.22.0
Found in review of the previous commit, before catalog tags 0.22.0: once
n_owe counts OWED repos rather than raw pending, its ==0 branch could fire
while a repo still held FYI-only mail, making "Ingen repo har uhaandtert
innboks" false at the exact moment it printed. Fixed to state only the
debt claim, and to name any FYI-only mailboxes found instead of letting
their existence go unmentioned. No fixture in the shared test tree ever
reached n_owe==0 (it always carries a debtor), so this needed its own
isolated-root fixture to pin.

Still part of the 0.22.0 release -- amends that changelog entry rather
than bumping again, since no tag exists yet.

board-selftest.sh: 150 -> 152 checks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGWMPskXBsTjMrrQ2GofFx
2026-08-13 21:07:39 +02:00
03e712a423 fix(board): plan/brief use actual debt, not raw pending mail
board.sh --plan group 2 and --brief conflated every unhandled inbox
message with an obligation to reply, including ones the sender
declared reply-expected: no (a notice, not a request). Reported by
morning-driver (2026-08-11), independently reproduced against the live
mailbox on 2026-08-13: 27 of 72 pending messages were notices. Both
paths now join against coord-count.sh's owed column instead, so a
done/deferred/blocked repo whose only mail is FYI no longer gets a
plan tab, and --brief no longer counts a notice as an obligation. The
table's raw INN column is unchanged by design.

While extending that join with a second lookup file, found and fixed
a more severe, independent defect: the existing $UNBLOCKS/$RECORDS
join used the NR==FNR awk idiom, which silently empties the entire
plan whenever the first file is empty -- i.e. whenever the repo tree
has zero blocked repos, a common, ordinary state, not an edge case.
Verified against the shipped 0.21.0 script. Fixed by matching on
FILENAME instead of cumulative line counts, for both lookup files.

Also repoints the README's governance link at repo-standard's
canonical GOVERNANCE.md (was pointing at the marketplace's copy),
per org-ops D11.

board-selftest.sh: 142 -> 150 checks. Version 0.21.0 -> 0.22.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGWMPskXBsTjMrrQ2GofFx
2026-08-13 20:54:55 +02:00
b02c2880e3 Revert "docs(readme): drop the static selftest-checks badge"
This reverts commit 78bb997.

The badge isn't decorative: it's the input to the catalog's per-axis
stat-mirror-gate (check-versions.mjs:49-59), which cross-checks the
catalog's own stat line against this badge on every release. Dropping
it silently ungates that number -- exactly the defect class v0.20.2
was released to close, and the one check-versions.mjs cites by name
as "the standing proof that ungated numbers rot" (repo-mailbox's own
selftest count drifted to 251 while badge-less).

org-ops's census 06 (BADGE-STATIC-CLAIM) reads the badge as "a claim
dressed as evidence" because nothing visibly runs behind it. That's a
false-positive premise here: the count is verified, just off-badge --
by a script in another repo, at release time, not by a live CI run.
Answering the census with that mechanism instead of complying with
"link it or drop it".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015eUDLeEf8P4dithWDRhSqH
2026-08-11 21:39:14 +02:00
78bb997884 docs(readme): drop the static selftest-checks badge
org-ops census 06 (BADGE-STATIC-CLAIM) flags it as a claim dressed as
evidence: the badge asserts a run that nothing verifies. This repo has
no CI/workflow to link it to (no .github/workflows, no Forgejo Actions),
so dropping is the only real fix -- linking to a nonexistent run would
just be a different unverified claim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015eUDLeEf8P4dithWDRhSqH
2026-08-11 21:36:42 +02:00
6c144174bd docs(release): document the manual check-versions discipline until catalog's pre-flight gate ships
Catalog confirmed --write --commit does not close the ERROR window (execFileSync
gate runs before the --commit conditional regardless of the flag) and is
evaluating a pre-flight gate as a future direction, not yet implemented. Per
their explicit request, document the interim discipline instead of a gate that
doesn't exist yet: run check-versions.mjs manually and confirm 0 ERROR before
--write, even for an unrelated plugin's error.
2026-08-10 20:36:19 +02:00
dd8f3ce042 feat(route)!: Fable rows are a hand-written override, never a route.sh outcome v0.21.0
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
2026-08-09 22:03:05 +02:00
4821ba8d4b fix(route-selftest): re-point section 14's CLI gate at the fact row_advisor() actually depends on
The pre-existing failure (route-selftest 72/73, verified with git stash
against clean main) was measuring the wrong direction: it checked whether
"claude --advisor fable" is rejected, but route.sh's row_advisor() never
emits --advisor fable at all - it depends on the opposite fact, whether
opus/sonnet can advise a FABLE main model. Both checks happened to agree
when written; they diverged once the CLI's capability ordering changed.

Measured against the installed CC 2.1.226: opus/sonnet advising a fable
main is still rejected ("cannot advise ... must be at least as capable"),
which is the fact rows 5-6's advisor suppression rests on and now stays
pinned. Also pinned row 1-2's check to an explicit --model sonnet rather
than the ambient default. Separately worth flagging to the operator (not
acted on here): fable itself is now accepted as an advisor for opus/sonnet
mains, which CLAUDE.md's standing note about CC 2.1.220 does not reflect.

route-selftest.sh: 73/73.
2026-08-09 21:45:12 +02:00
652dabc48e chore(release): prepare v0.20.3 - dot-prefixed repo name fix v0.20.3
Version bump for f425a11 (coord-send.sh/coord-count.sh/coord-sweep.sh
dot-prefixed repo name fix). Bumps plugin.json, package.json, README
badge, and the three skill frontmatters together; CHANGELOG entry added.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHmf1VfCvaYXamJxe5y6Vt
2026-08-09 21:14:31 +02:00
f425a11311 fix(coord-send): stop rejecting dot-prefixed repo names
coord-send.sh:154 guarded the --to target with a `.*` case arm, which
rejects every leading-dot name instead of just `.` and `..`. A
dot-prefixed name is a real repo identity (basename of a git toplevel
under a hidden directory, e.g. ~/.claude) and was reported unreachable
by morning-driver via coord message 20260809T103138Z. Narrowed the
guard to reject exactly `.` and `..`, matching the equivalent guards
already used elsewhere in this file and in coord-done.sh.

coord-count.sh and coord-sweep.sh both enumerate the mailbox root with
a bare "$COORD"/* glob, which by construction never matches a
dot-prefixed directory - confirmed empirically before this change.
Fixing only the send-side guard would have let a dot-prefixed mailbox
receive mail that neither script could ever report or close on its
grace window. Both now also glob "$COORD"/.[!.]* to reach hidden
mailboxes without matching "." or "..".

coord-selftest.sh: 183 -> 191 checks, section 30 covers the fix and
both enumeration paths. Doc counts (README badge, CLAUDE.md) updated
to match; the catalog's mirrored badge is untouched pending release.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHmf1VfCvaYXamJxe5y6Vt
2026-08-09 21:09:23 +02:00
ddc978d4c1 docs(readme): trim the badge row to five, recolor the selftest count
repo-standard's presentation check flagged two WARNs. Both land on the
badge row, and one of them collides with a decision made yesterday.

BADGE-STATIC-CLAIM: green is CI idiom for "passing", and this repo has
no CI by design (a stated non-goal — the forge has no runner). Linking
the badge to a real run is therefore impossible, but DROPPING it is not
free either: the catalog's stat-mirror gate is per-axis and badge-gated,
so an unbadged axis is skipped silently and the number it protects can
rot again (measured 6 and 251 against a true 8 and 398). The badge stays
and turns blue, which reads as a count rather than a pass state. The
catalog parser splits label/value/color and ignores the color, so the
gate is unaffected.

BADGE-COUNT: seven badges, past the point a row reads as clutter. Five
of them are load-bearing — version is ERROR-gated by the catalog, and
hooks/skills/CLI scripts/selftest checks are the four axes it mirrors.
Platform and License are not: the first restates the opening line, the
second restates the License section that already links LICENSE. Dropping
those two lands exactly on five without ungating anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bWgoaf46fu6DagYQyDVR5
2026-08-04 11:49:29 +02:00
cbcd236050 docs(readme): meet the repo-standard gate — matching opening line, non-goals, changelog link
The gate (repo-standard v0.1.1) reported 3 ERROR against this repo:

- README-DESC: the first line after the H1 was the `coord` rename note, not
  the forge description. That line is the only place a machine can check
  description == catalog == README from inside the repo, so it now carries the
  forge description verbatim (179 codepoints, unchanged upstream) and the
  rename note moves one paragraph down.
- HEADING-MISSING `## Non-goals`: added. It answers "is this for me?" — not a
  state store, not a network protocol, not an issue tracker, not a
  conversation, the board never writes, and no CI.
- HEADING-MISSING `## Changelog`: added as a link to CHANGELOG.md.

The one remaining finding is a WARN on the `Selftest checks` badge and it
stands deliberately: the badge counts the checks in the suite, it does not
assert a passing run, and the gate's own escape hatch is to link the badge to
a real run — there is no runner to link to. The honest substitute the standard
asks for is now stated in Non-goals and in Development.

README only; no code path touched. 398/398 selftest checks still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bWgoaf46fu6DagYQyDVR5
2026-08-03 21:55:20 +02:00
5c182578e6 chore(release): prepare v0.20.2 - README badges for CLI scripts and selftest checks v0.20.2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7V99BXv1v5pWHUwY63it
2026-08-03 10:16:26 +02:00
dbc68fbc73 docs(readme): badge the CLI scripts and selftest checks axes
The catalog's stat-mirror gate is per-axis and badge-gated: an axis
the plugin does not badge is skipped silently, and these two were the
ones measured stale (6 and 251 against a true 8 and 398). Adding the
badges here lets the gate start enforcing them once the next release
tags this README.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7V99BXv1v5pWHUwY63it
2026-08-03 10:13:54 +02:00
123d40e4b4 fix(board): track an open HTML comment through to its closer v0.20.1
Skipping lines that START with the comment opener is not the same as
skipping a comment. Every continuation line of a wrapped comment under
the NESTE heading still looked like prose, so the excerpt became comment
internals instead of the next step. Both the board line and the route
line wrap easily; measured, 1 of 28 repos was affected, with the symptom
held down only by the convention of keeping those comments one-line.

An open comment is now tracked to its closer, and a NESTE block that is
nothing but a comment reports an empty block rather than promoting the
comment's own text.

Two limits are the format, not leftovers: an HTML comment body may not
contain the closer at all, so a rationale quoting it still ends its own
comment early.

The fixture is the real shape - one-line, wrapped, one-line, prose -
because a lone wrapped comment passes even with a flag that never
resets, and the continuation marker sits at the start of its line
because the table's 38-char cut would otherwise truncate it away and
the check would pass against the broken code too.

board-selftest 138 -> 142.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014K262DRWBJzEpWoMsBjB8J
2026-08-03 09:53:25 +02:00
d0a5ffe515 feat(board)!: rank --plan on five ordered groups, planned above in-progress v0.20.0
Replaces the weighted score shipped in 0.19.0 with five lookups: chain-root
credit, unhandled inbox, planned, in-progress, undeclared status. Within a
group: that group's own quantity, then a Sonnet next-cost, then oldest plan.

The score's objection is accepted, not forgotten, and is written into board.sh
and CLAUDE.md so a later session reads it as decided rather than as an unfixed
defect: a group order cannot express "owes one message AND releases two others"
as one quantity. What the score could not do was hold still for the format's
second consumer - re-tuning one weight against another silently reorders a
parser in another repo, and no test here can catch that.

planned now ranks above in-progress, inverted by the same decision: converting a
decision into motion is the slow step; live work is already moving.

Debt stays uncapped and never excluded. One group below chain-root credit is not
the cap declined at 0.19.0 - the debtor keeps its tab, its most-owed-first
position, and its why=inbox:N. Pinned by a discriminating fixture the score
would fail: a root releasing one repo outranks a repo owing four.

board-selftest 134 -> 138. Suite 183 + 138 + 73 = 394.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6ULuFCPMNYAPNN3pAjsXQ
2026-08-03 06:55:21 +02:00
e5e9cbe847 feat(board): rank --plan on one score, with transitive chain-root credit v0.19.0
blocked-on was scanned and then used for a single display string; it never
reached the ordering. A blocked chain's root could therefore rank below the
repos waiting on it - measured on the real tree, the root opened at tab 14
while the two repos it held up opened at tabs 5 and 8.

--plan now ranks on one score instead of four hard buckets, because four
groups cannot express "this repo owes one message and releases two others":

    40 x repos released transitively + 15 x inbox
    +10 in-progress / +5 planned / +2 undeclared, +3 for a Sonnet row

Chain-root credit follows blocked-on transitively to the first repo that is
not itself blocked and credits only that root. Opening a blocked repo
releases nobody - its own next step is by definition waiting. A cycle, a
blocked-on naming an unscanned repo, and a blocked repo with no target all
credit NOBODY: inventing a root there yields a plan that looks correct and
sends the operator to the wrong repo.

Debt is deliberately uncapped. A cap was proposed alongside the credit and
declined: owing a reply is the other axis from a repo's own next step, and
answering is often what unblocks a chain. The weight carries that decision -
15 per message exceeds the sum of every other bonus (13), so one message
more still outranks any combination of status and cost. At the originally
proposed 12 a tiebreak could flip the debt order, which is the cap arriving
through the back door.

blocked-on becomes a record field, inserted BEFORE neste so no existing field
index moves: neste is free prose lifted from a STATE.md and may carry a
literal '|' from a markdown table, so it stays last.

why= now names the dominant term rather than the first rule that matched, so
a block says what opening it would release instead of repeating inbox:N.

Format is otherwise unchanged for both declared consumers: tab=, repo=, dir=,
why=, status=, neste=, command=/command_missing=, paste= and the fokus_*
header all keep their names and positions.

board-selftest 118 -> 134 (chain, cycle, dangling-edge and format fixtures
written first, all failing before the change).
2026-08-02 22:38:40 +02:00
917a902172 fix(board): anchor the NESTE marker to the heading form
The awk reader matched /NESTE/ unanchored, so any prose line that merely
mentioned the word (not just the real ## NESTE heading) set the flag and the
excerpt became whatever line followed the false match, not the real next
step.

Closed by operator decision 2026-08-02: one anchor form for all repos,
^#+[[:space:]].*NESTE. Measured 26/27 repos already use the heading form;
the one exception now renders "(tom NESTE-blokk)" instead of a misleading
fragment, which is the intended, honest failure mode until it switches
over - already flagged directly and on the same shape as the existing
"MERK: N repo mangler board-linje" line.

board-selftest 116 -> 118 (new fixture proves the false-match failure before
the fix, both checks green after).
2026-08-02 20:52:17 +02:00
402597b1e5 fix(board): read emphasis on the status token, not only on the slug
The marker reader accepted **slug**: done and missed slug: **done**. That was
never a rule - it was whichever example happened to be in front of us when the
regex was written. The second form is in live use, and it makes a repo that HAS
declared look silent to --focus, which is the exact failure the held-back
report exists to surface.

Found by acting on the report's own output instead of reading it: enumerating
the held-back population turned up a repo whose declaration we were dropping
ourselves. The report blamed the repo; the defect was here.

The evidence field reports the status unwrapped - the emphasis is markdown the
operator typed, not part of the token, and "**done**" in a key=value field
reads as a value. board-selftest 114 -> 116.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:34:53 +02:00
ecbb34ae48 chore(release): prepare v0.18.0 - version bump, and the entry point --focus was missing v0.18.0
release-plugin.mjs does not bump these; it REFUSES to release until they
already match the target (release-plugin.mjs:64). The bump belongs in this
repo, not in the catalog session, so the catalog step is now unblocked:
plugin.json, package.json, README badge and all three SKILL.md frontmatters
move to 0.18.0 together, and CHANGELOG's [Unreleased] becomes [0.18.0].

More important than the numbers: --focus was reachable by nobody. The board
SKILL.md is the operator's only entry point and it documented only --plan, so
"fokusdag pa X" would have routed to an unfocused plan on a released 0.18.0.
The skill now passes the user's phrasing through VERBATIM - resolving prose to
a slug in the skill would put a guess in front of a deterministic lookup, and
fokus= already reports what the phrase resolved to. It also relays the three
disclosure lines rather than trimming them as noise, since this is the one
rendering that hides repos.

check-versions: 0 ERROR, 1 WARN (catalog ref lags plugin.json - the documented
transient state before the ref bump).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:15:52 +02:00
dd04356920 docs(changelog): correct the check count to what the suite actually reports
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:09:15 +02:00
040480890d fix(board): an empty held-back set renders a value, not a dangling colon
With nobody held back, fokus_utenfor= ended at a bare colon. sed could not
supply the placeholder it looked like it was supplying: given an empty string
there is no input line for the substitution to run on, so it silently did
nothing. That is the shape command_missing= exists to keep out of this format
- a field whose value carries nothing.

Zero repos mentioning a slug without declaring it is a real answer and now
looks like one. board-selftest 113 -> 114.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:08:32 +02:00
3fe159e75d docs(board): scrub a real topic slug out of a public-mirror comment
The worked example in the focus comment was a live slug from another repo's
private topic register. This repo mirrors publicly, and the decision file
deliberately avoids naming slugs for that reason - the code has to hold the
same line. Replaced with a shaped placeholder; behaviour untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:04:35 +02:00
9bbfe9facd docs(focus): record what shipped against the condition it was permitted under
The decision file was written before any code existed and said so in its
opening line. That line is now false, and the fix is not to delete the tense
but to name it: everything above "What shipped" is the record of what the
feature was ALLOWED to be, and the closing section reports what was built
against it.

Also records the one thing the decision could not have known, because it was
found by reading the consumer's parser rather than assuming it: paragraph mode
discards any block without tab=, so neither a '#' comment nor a bare key=value
in the header ever reaches the driver's records.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:02:44 +02:00
f9d2c927ab feat(board): give --plan a focus cutoff that reports what it held back
--focus "<prose>" narrows the day plan to repos whose STATE.md DECLARES a
matching topic marker. Measured on the real tree: 26 blocks to 6.

The filter alone was never the feature. --plan documents that it takes one
position (the order), hides nothing, and labels what it cannot rank rather
than dropping it, so a silent cutoff would break a property the format had
already written down. The same run prints the slugs the prose resolved to,
how many blocks it removed, the repos that MENTION a resolved slug with no
marker line - named, not counted - and how many STATE.md it searched. Each
surviving block carries the declaration it survived on.

Enumerated rather than counted because the decisive find behind this feature
was invisible to every string measurement until the held-back population was
listed. "nevner", never "dekker": this reports text found, and board.sh has
no grounds for a claim about relevance. The slug vocabulary comes from the
scanned STATE.md themselves, so the "no other file" invariant survives.

Emitted as key=value, not '#' comments: the consumer's parser runs in
paragraph mode and drops any block without tab=, so a comment would reach the
operator on the terminal path and vanish on the driver path - the one case
where the cutoff is applied unseen.

board-selftest 89 -> 113. The narrowing check is comparative, since every
presence-only assertion passes against a no-op filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 19:58:25 +02:00
cb77f2889d docs(focus): record the condition --focus rests on, before it is built
A cross-repo exchange settled that --plan may grow a --focus filter only if
the same run reports the repos it held back. The decision constrains a future
feature, but it lived only in a private mailbox and a local-only STATE.md, so
nothing durable pointed at it.

The argument is an invariant, not a preference: --plan documents that it has
no cutoff and labels rather than drops, and --focus IS a cutoff. Measuring the
held-back population also turned up the find that carries the decision - a
heavy consumer of the subject, invisible to every strict-marker measurement in
the exchange, recovered only by enumerating what the filter would have hidden.

Other repos are unnamed by design: this mirror is public, and which repo
carries which malformed marker line is that repo's business.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USD9q3Fr2unp58fe8YvapW
2026-08-02 18:42:45 +02:00
50f6420da5 docs(changelog): reconstruct the missing 0.15.0 entry
The log jumped straight from 0.16.0 to 0.14.0 even though 0.15.0 shipped
and is tagged - a log gap, not a code gap, but this repo mirrors publicly
and nothing machine-checks changelog prose.

Reconstructed from v0.15.0 alone: the tag points exactly at 9dd24c3
(--brief + brief-nightly.sh). STATE.md's pointer to also use 6de2408
(the malformed-route-line test) was a stale premise - `git merge-base
--is-ancestor 6de2408 v0.15.0` says no, it landed 6 minutes after the tag
and only entered history via v0.16.0. Checked out both tags into scratch
trees and ran board-selftest.sh live to get 36 -> 49 from PASS=, not from
commit-message prose (this repo's README has printed a wrong count
before). Grepped the launchd plist for leaked absolute paths before
repeating the "placeholders only" claim: clean.

Left the published [0.16.0] entry's "51 -> 86" as-is per operator
call - the 49 -> 51 gap (6de2408's two checks, landed untagged between
the tags) is explained in the new entry instead of rewriting an
already-tagged entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017orCFDkmp88fLnqDR3chdJ
2026-08-02 16:05:53 +02:00
4080e22abd docs(ghostty): a ghost surface is detectable in one property read
The measurement identified realized surfaces by counting shell processes, which
is what was available while probing. morning-driver, building against this
document, found a cheaper and stronger test in the sdef: `working directory` is
readable on a terminal, and a surface with no shell process reports it EMPTY -
exactly the ghost signature the broken creation API produces.

That turns "has this surface been realized" from an inference into a
machine-checkable question, which is the guard anything driving Ghostty needs
before it types into a pane.

The same property answers one of the document's own open questions: it goes
empty when a shell exits and returns when the prompt is back, so "has this pane
returned to a prompt" can be polled rather than guessed at with a delay.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011eKRzhD1RqY2MxvDEkrJAM
2026-08-02 15:57:27 +02:00
5bea1f8651 feat(board): give the plan a paste= line the operator can actually use v0.17.0
--plan shipped with dir= and command= as separate fields. That is right for a
driver, which moves the pane itself and then types the command, and unusable
for a person: the first operator to read a block could not act on it at all.

Joining an absolute path from one field with a command from another is not a
saved line of output. It is the step where a session gets started in the wrong
repository - the same failure the absolute dir= exists to prevent, moved from
the machine to the human.

paste= is the two already joined, cd <dir> && <command>, one thing to select.
Both forms stay: the two consumers want the same fact shaped differently, and
neither shape serves both.

Emitted only alongside command=, never once per block. A `paste=cd X && ` with
nothing after it would run the cd and then a bare newline, which fails
SILENTLY - the operator ends up in the right directory with no session started
and no error to notice.

board-selftest: 86 -> 89 checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011eKRzhD1RqY2MxvDEkrJAM
2026-08-02 07:34:30 +02:00
6d0fe14d0f feat(board): render the day plan as a third rendering of the same scan v0.16.0
The table says what the state of every repo is; the briefing says who is
waiting. Neither says which repos to open a tab for today, in what order,
and with which command - so a day plan was assembled by hand every morning
from a table that deliberately takes no position.

--plan is built on exactly the argument --brief was: a lookup over data the
scan already holds, zero model calls, and route.sh already derives the
per-repo command. The order IS the position it takes, and the only one -
there is no cutoff, so nothing is hidden.

The rule most likely to be "fixed" into a defect: repos owing mail rank
first regardless of status. Excluding blocked or done is a claim about a
repo's OWN next step, which by definition cannot be moved, while owing an
answer is the other axis entirely - and answering is often what unblocks
it. Measured on the real tree, two of 26 planned repos were done with an
unhandled inbox, so the fixture tree pins done-with-debt and
blocked-with-debt rather than the general rule alone. Repos with no board
line rank last and are LABELLED, because the table already prints a note
about them and dropping them silently would repeat that defect.

key=value rather than prose, because the plan has two consumers: the
operator, and a separate repo driving a terminal from it. Prose would make
the rendered format an API no test here could hold stable for a consumer
living in another repo. dir= is absolute because a new pane inherits its
anchor's directory; command_missing= carries both no-command causes,
because a driver reading ^command= would type a blank line into a live pane.

route_cmd_for() becomes the single reader of the route-line grammar, shared
with --brief, distinguishing the two failure causes by exit code rather
than by an empty string. --brief output is unchanged.

Driving a terminal from the plan stays OUT of this repo, on the measurement
in docs/ghostty-orchestration-measurement.md rather than on taste.

board-selftest: 51 -> 86 checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011eKRzhD1RqY2MxvDEkrJAM
2026-08-02 06:40:22 +02:00
5d9c76763d docs(board): measure what Ghostty 1.3.1 can actually be driven to do
The gate question before any day-plan design: can Ghostty be scripted to open
a tab with two vertical panes, each in its own directory, with a command
running in one? Measured against the installed 1.3.1 rather than reasoned
about, because this is undocumented platform behavior.

Yes, but not through the documented API. new window / new tab / split return
correctly shaped objects that never spawn a shell - nine probe windows, shell
count flat at 29 - and every call that needs the process fails with "Terminal
surface model is not available". That is issue #12730, a 1.3.1 regression
closed as not planned, and 1.3.1 is the newest release. surface configuration
is collateral: all four of its useful properties ride that path.

What does work is perform action on an ALREADY REALIZED surface. new_tab and
new_split produce live shells (29 -> 31), and input text + send key drives
them - two panes cd'd to different repos reported distinct directories and
distinct PIDs. The working directory becomes a typed cd, so the broken path is
not needed at all.

Three constraints fell out of the measurement and are recorded because each
was found by breaking something: pane targeting must diff tab ids, since
last tab of w is not the new tab and a probe that trusted position typed into
a tab it did not create; new tabs inherit the anchor's directory; and
count of terminals is not a pane count.

Recommends splitting the work - the plan here as a third rendering of the
board scan, the Ghostty driver in a separate repo. The argument is the
measurement: a preview API that breaks in 1.4, whose documented path is
already broken, with a blast radius reaching into other repos' live sessions,
does not belong behind the mailbox.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GfDGWyyhnM26J4p93GSk2L
2026-08-02 06:15:47 +02:00
8e207c6c49 test(sweep): stop the --days 0 check from racing a one-second cutoff
Measured, not guessed: the check failed 7 runs in 20, not once. The cause is
a same-second collision, reproduced deterministically - a notice minted at
20260801205457 against a cutoff of 20260801205457 survives, the same notice
60s older is closed.

coord-sweep.sh is right and is left alone. Its cutoff is second-granular and
it closes strictly older messages, which spares rather than closes at the
boundary; at any real --days value one second is unobservable. Relaxing that
guard to <= would make a destructive script more aggressive to satisfy a test.

So the test was claiming what the code does not promise: that a notice minted
earlier in the same run is necessarily older at second granularity. Under a
second of work separates the two, so it was a coin flip. Aged by 5 seconds
through the existing age_it, which keeps it well inside the default 14-day
window and clear of the boundary. No sleep: that would have hidden the answer
rather than fixed it.

Selftest 182 -> 183, 20/20 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GfDGWyyhnM26J4p93GSk2L
2026-08-01 23:01:32 +02:00
6de2408b28 test(brief): pin the malformed route line, the case that will actually occur
The briefing had checks for a VALID route line and for a MISSING one, but
not for one that exists and carries a value outside route.sh's closed set -
which is the case that arrives on its own as 23 hand-written route lines
drift. route.sh exits 2, the command comes back empty, and without a check
nothing stops a future edit from printing a bare "$ ": the shape of a
runnable command carrying nothing, which is worse than an absent command
because it reads as copied-and-pasteable.

Two checks, because the marker appearing and the command line NOT appearing
are different claims and only both together describe the behavior. Verified
the fixture fails for the right reason: `--path knwon` exits 2 with
"unknown value".

board-selftest 49 -> 51. No behavior change; no version bump.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017orCFDkmp88fLnqDR3chdJ
2026-08-01 22:44:35 +02:00
9dd24c3446 feat(brief): render the nightly cross-repo briefing without a model v0.15.0
The operator has more repos than they can hold in their head, and the
question that actually costs them is "who is waiting on me, and what does
answering cost". board.sh already scans for it; nothing rendered it in a
form an unattended job could leave behind.

--brief is a second RENDERING of that scan, never a second scan. It prints
NESTE uncut, because the 38-character cut is the table column's property
and not the record's - the value used to be truncated at record-build time,
which left the cut string as the only copy. Each startup command is derived
by CALLING route.sh with that repo's own four traits; next-cost alone
cannot produce it, since the advisor flag is a property of the ROW and two
rows can share a model/effort pair while differing on it. A repo with no
route line is told so rather than handed a guess.

It cross-checks itself against coord-count.sh, and that is the substance of
the change rather than a nicety. The repo scan and the mailbox are two
different populations: a mailbox can carry a name no scan will ever produce
- a declared non-git surface (CLAUDE_COORD_REPO, e.g. ~/repos) or a
checkout outside the roots. Measured on the real mailbox: 11 repos / 21
messages in the briefing against coord-count's 12 / 22, the missing one
being the declared surface `repos`. A briefing that only walked the scan
would answer "who is waiting on you" with a number it quietly knew was
short.

Zero model calls, which was the deciding property. Measured against 2.1.220
under subscription auth: --max-budget-usd DOES bite (terminal_reason
budget_exhausted, exit 1), but it aborts AFTER turn one - floor ~0.25
USD-equivalent per turn on claude-opus-5[1m]. It is a runaway brake, not a
pre-flight gate, so a nightly claude -p job would draw on the same quota
pool as interactive work every night. Determinism removes the question.

board.sh stays read-only: the file write lives in brief-nightly.sh, which
renders to a temp file and renames it into place, and treats an EMPTY
render as a FAILED one - board prints nothing when its scan roots do not
exist, which is what a mistyped path or a moved home looks like, and a
plain `> file` redirect would destroy yesterday's briefing on a bad launchd
environment.

The launchd template carries placeholders, not absolute paths: this repo is
mirrored publicly and a plist is the one file here that would need a home
directory in it. It points at a checkout rather than the plugin cache,
which is version-pinned and would break silently on the next bump.

board-selftest 36 -> 49. One check pins a defect caught only by eye against
the real tree: fold copies its input's missing trailing newline, which ran
the command onto the tail of the NESTE prose and produced a briefing whose
commands could not be copied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017orCFDkmp88fLnqDR3chdJ
2026-08-01 22:38:46 +02:00
459c9feec0 feat(sweep): close the aged FYI backlog without a human in the loop v0.14.0
A notice needs no answer, but it is still re-injected at every session
start until someone closes it by hand. Across enough repositories that
hand-closing IS the manual work the mailbox was meant to remove, and the
pending count -- the operator's only signal -- drowns in messages that
were never going to be acted on: 9 of 22 pending messages across 12
mailboxes were pure notices when this was written.

coord-sweep.sh closes exactly one mechanically decidable class:
reply-expected: no, older than a grace window (default 14 days). A
message that owes a reply is never touched, at any age, with any flag --
answering it would mean deciding something on the receiving repo's
behalf, which is the one thing this system exists to prevent.

Four properties are load-bearing, not incidental:

- Dry-run is the default, inverted from the rest of the engine. The
  others print or deliver; this one destroys pending state, so the safe
  direction has to be what you get by forgetting a flag.
- Closing goes through coord-done.sh --repo, never mv, so the archive
  layout and the _broadcast refusal stay in one place.
- Age is read from the filename prefix, never the file. An unreadable
  age is never treated as old: fail-safe, not fail-open.
- Every closure is logged with sender and subject. Directed messages
  have no seen-tracking, so the sweep cannot tell "seen and ignored"
  from "never delivered" -- a notice can be closed unread, and the log
  is the only thing standing between that and silent data loss.

The reply-expected read is bounded to the frontmatter block, matching
coord-count.sh: a body line claiming it at column 0 is untrusted
cross-repo input and must not close its own message.

No scheduler, no launchd unit, no skill front door -- the script does
nothing until invoked.

Selftest 159 -> 182. The log check caught a real defect during
development: the first implementation read from/subject AFTER
coord-done.sh had moved the file, logging empty values and quietly
defeating the only safeguard the design has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uwcak9j4m9JijDKeFmptww
2026-08-01 22:00:42 +02:00
9cb405c2cd feat(route): give the advisor a writer, on a need and per row v0.13.0
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
2026-07-31 21:39:59 +02:00
6ffe089b68 docs(coord-send): correct a false claim the fallback fix shipped with
v0.12.1's fix removed the ~/.claude/scripts fallback but its own commentary
still asserted the deployed copy was "real and intentional (the operator's
own terminal aliases point at it)". Checked directly with the operator:
they invoke coord-send and board only through their Skill front doors, never
a personal terminal alias. board.sh and route.sh never had a deployed copy
at all -- only the five coord-*.sh scripts did, and their one measured
effect was as an accidental fallback target for Claude sessions' own Bash
tool calls, the exact bug 0.12.1 fixed. With no owner and no remaining
function, the five files under ~/.claude/scripts/ have been deleted.

No behavior change -- coord-selftest.sh still 159/159.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJMJanRHkUhEf9EYxMpbM5
2026-07-31 18:41:34 +02:00
6e369936f9 chore(release): 0.12.1 -- coord-send stops silently falling back to a stale copy v0.12.1
Version sync across plugin.json, package.json, README badge, CHANGELOG and
all three skill frontmatters, plus the 156 -> 159 selftest-count references
that follow from the previous commit's three new checks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJMJanRHkUhEf9EYxMpbM5
2026-07-31 18:25:39 +02:00
34f263cf28 fix(coord-send): drop the ~/.claude fallback in the skill's own script path
A Bash tool call never has CLAUDE_PLUGIN_ROOT set as a real shell variable --
only the skill's own markdown rendering resolves that bare token -- so the
${CLAUDE_PLUGIN_ROOT:-$HOME/.claude} fallback silently won every time the
engine line was actually executed, routing through whatever the operator's
personal ~/.claude/scripts/coord-send.sh happened to be instead of the
plugin's own bundled script. Same defect class board.sh was already fixed
for; coord-send still had it. Reported live by org-ops (2026-07-31): a
--fyi send hit an old deployed copy lacking the flag.

coord-selftest.sh grows three checks (156 -> 159) pinning the SKILL.md
engine line and ruling the fallback out entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJMJanRHkUhEf9EYxMpbM5
2026-07-31 18:22:44 +02:00
dffc1f5e47 chore(release): 0.12.0 -- a session measures the effort it ran with v0.12.0
Bumps the six version-bearing files and gives ac62a38 its changelog entry.

Until the tag exists the plugin cache keeps serving the 0.11.0 skill, which
instructs every new session that effort is not observable and to ask the
operator for it. The fix is committed and pushed but not in circulation, so
the release is the part that changes behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxRVZm1busUeuXd5iPkK36
2026-07-31 17:28:19 +02:00
ac62a38b40 fix(route): measure the effort a session ran with, instead of asking for it
The last-session record exists to make the routing policy falsifiable, and
it only is if --last-effort is measured. route.sh documented the opposite as
settled fact: that the effort a session ran with is not observable from
inside that session. That was true when written and is not now. Claude Code
exports CLAUDE_EFFORT into every tool-use context as the session's current
effort level, so a Bash call reads it directly.

The premise had a cost. With effort unobservable, the record could only be
completed by asking the operator at session end, which made it block on their
presence -- all four fields or none. That is also the weaker measurement, and
in the same way the previous board line is: the operator reads the effort off
the startup command they typed, so both sources report what was PRESCRIBED
rather than what was RUN. They come apart exactly when the record would be
most interesting, which is what a session that silently ran xhigh under a
board line saying high already showed.

Reading it makes all four fields knowable from inside the ending session, so
the record no longer waits on anyone. The skill does the reading; route.sh
deliberately does NOT default from the variable, because a calculator that
consults its environment is no longer deterministic from its arguments and
the route->board round trip in selftest section 6 rests on that.

Section 13 also pins the trap this opens: skill frontmatter overrides the
session effort while that skill is active, so an effort: field in route's own
SKILL.md would make the reading report the skill instead of the session --
a measurement quietly measuring itself, with nothing in the output to show
it happened.

Also corrects the neighbouring claim that the model is readable from the
environment. There is no CLAUDE_MODEL; the session takes it from what it
knows itself to be running as.

route-selftest 50 -> 56.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gfa1nvwGXdST2MHvbs6htD
2026-07-31 17:21:10 +02:00
6735298752 chore(release): 0.11.0 -- a message can say it needs no answer, and 0.10.0 gets its changelog v0.11.0
Bumps plugin.json, package.json, the README badge and the three skill
frontmatters, because release-plugin.mjs blocks on a plugin.json that does not
already stand at the target version.

Also closes a gap this release would otherwise have inherited: v0.10.0 shipped
without a CHANGELOG section, and the next-cost fix sat under [Unreleased] even
though it is inside v0.9.0..v0.10.0. Labelling that block 0.11.0 would have
made a second wrong claim, so 0.10.0 now has its own section, written from the
commits in the tag rather than from memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016iJoZVmU2guTEZcMghk88z
2026-07-31 15:50:41 +02:00
5754d67a6b feat(engine): make a bulk close state what Rule 7 says must be stated
coord-done.sh was the third script that acts on pending messages and the only
one that did not learn the field. --all archives every pending message in one
call, including the ones whose sender declared it expects a reply - the exact
outcome Rule 7 exists to prevent, now reachable with no friction and no trace.

The behavior stays. The receiver keeps both terminal states by design (section
20), and a bulk close is legitimate; refusing would move a decision that belongs
to the operator into the script. What was wrong was the silence: Rule 7 requires
leaving a message unanswered to be STATED, and a command reporting only a total
made the thing that has to be stated invisible.

So --all now names the number it closed without a reply, and only then - a line
that always fires is one nobody reads. A named close stays quiet, because naming
a message is already deliberate, one message at a time.

coord-selftest 151 -> 156.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016iJoZVmU2guTEZcMghk88z
2026-07-31 15:48:42 +02:00
c0ccb1d611 feat(engine): let a message say it needs no answer, and count debt without losing sight of the rest
Rule 7 (0.5.0) shipped an obligation on a format with four fields, none of
which could tell a question from a notice. Two consequences fell out of that
gap: the injection had to name both terminal states and prefer neither, and
coord-count.sh had to treat every unarchived file as a reply owed. The fifth
field closes both.

coord-send.sh --fyi writes reply-expected: no; omitting it writes yes. Absent
means expected, because every message already on disk lacks the field - so a
forgotten flag over-counts debt, which is visible, rather than creating debt
nobody sees. A reply is not a special case. A broadcast is always no: --reply-to
resolves inside the recipient's own mailbox and a broadcast never lands there,
so there is no reply path to promise.

coord-count.sh now prints TWO integers per mailbox, not one. Replacing pending
with debt was the obvious reading of "count debt rather than unarchived
messages" and it is wrong here: board.sh counts the same inbox files itself, so
a debt-only count would put two different numbers under one name with nothing to
reconcile them, and a mailbox holding only notices would read as empty while its
messages keep being re-injected.

The field is frontmatter and only frontmatter - a body line claiming
"reply-expected: no" at column 0 cannot silence a real debt, and a file without
valid frontmatter counts as owing a reply.

Section 20's wording changed because its stated reason expired, but its second
half matters more now, not less: the marking is a DECLARATION, not an
instruction. Without that clause one word in an untrusted message becomes a
lever that mints obligations in another repo.

coord-selftest 136 -> 151.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016iJoZVmU2guTEZcMghk88z
2026-07-31 15:46:14 +02:00
261a75bd7b test(board): make the no-commit check fail when the row is missing
The check read a row that was not there and passed: an absent `repo-c` leaves
the captured line empty, and an empty string contains no day count, so the
assertion succeeded by finding nothing rather than by reading the column. Every
other new check had a real red state; this one's was "column absent", which is
a different failure than the one it claims to guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QS6gYYgYgJAz7VjcFHEzbb
2026-07-31 11:07:35 +02:00
6a09c9caa1 feat(board): split the age column, so a silent repo cannot hide behind a fresh plan
ALDER carried two readings depending on which branch a repo fell into: STATE.md
mtime for repos that had one, last-commit age for repos that did not. The
consequence was one-sided -- a repo WITH a STATE.md reported only how recently
its plan was touched, so one that had not committed in a year looked exactly as
alive as one worked on this morning, and no column contradicted it.

Now each column means one thing. ALDER is the plan's age and is blank where
there is no plan; SISTE is the last commit, read for every repo, and blank where
there are no commits -- a day count there would be a fabricated reading, which
is what the old -1d was.

The four ranking rules stay four: the new column is evidence, never sort input.
The sort key was already a separate field from the displayed age, so bucket 5
keeps ordering by commit age while showing nothing in ALDER.

The fixture tree was missing the only case that discriminates -- a repo with a
STATE.md AND an old last commit -- so the checks land with it, plus its mirror
image (old plan, fresh commit) to pin that the two ages read independently and
that neither became the sort key. Backdating sets GIT_COMMITTER_DATE, not
--date=: %ct is the committer date, and an author-only backdate would have left
the red test failing for the wrong reason.

board-selftest: 30 -> 36 checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QS6gYYgYgJAz7VjcFHEzbb
2026-07-31 11:04:27 +02:00
dbf7391d57 chore(release): 0.10.0 -- route ships, and the board skill stops advertising a path that is gone v0.10.0
Version bump across plugin.json, package.json, the README badge and all three
SKILL.md frontmatters.

Also removes the last false statement in the public docs: the board skill still
sold a `~/.claude/scripts/` fallback as portability, but that copy was deleted
when the skill was made the only entry point. Tagging it would have published a
path that does not resolve, next to a sentence claiming it does. One entry point
is the point -- two entries pointed at two copies and made it impossible to know
which one ran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017peNgsxVt1BR4BTuMwiPoX
2026-07-31 10:22:53 +02:00
a435a031db 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
2026-07-31 10:19:30 +02:00
0d1183800e feat(route): give next-cost a writer, and make the rubric the only copy
The board line's next-cost field had a reader (board.sh) and no writer, so
its value was retyped by hand every session and drifted into several
competing spellings. Cleaning the data could not fix that: the cause was the
missing write path.

route.sh is that writer. Four scored traits of the next task -- path,
verification, reversibility, scope -- plus a required rationale, run through
the operator's model rubric moved here as the single copy. The row table is a
closed set of six values, so a seventh spelling cannot enter circulation, and
route-selftest.sh section 6 runs the round trip (route emits -> board parses)
inside one repo rather than across two.

Two spellings of one decision come out of one table: the rubric name for the
board line, the CLI alias for the command the operator pastes. Effort levels
are the set pinned in this marketplace; model aliases are gated against the
installed claude rather than hardcoded.

Three things worth naming:

- Escalation is asymmetric. Any single trait escalates; the cheapest row needs
  all four at the cheap end. Underkill costs one session, overkill costs quota
  every session -- but a wrong architecture call costs more than either.
- The Fable rows fire only from an explicit judgement flag, never inferred
  from the last-session record. "The session did not finish" also covers
  context exhaustion and operator interrupts, which say nothing about the
  model, and Fable runs without an advisor.
- The trait block is a single-line HTML comment because board.sh's NESTE
  extractor skips only lines that START with '<!--'. Measured first: a YAML
  block or a multi-line comment silently replaces the repo's next step on the
  board with "next_task:". Pinned by section 7.

board.sh is untouched as a program; its header now points at route.sh for the
value set so this does not reopen the two-specs defect 305f168 closed.

Selftests: coord 136, board 30, route 47 (new), node 7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017peNgsxVt1BR4BTuMwiPoX
2026-07-31 10:10:22 +02:00
305f1687b7 fix(board): one spec for next-cost, and it is the rubric's
The board line's cost field had two specifications and one of them was this
script. `--help` prescribed a versionless `sonnet/xhigh`; the global model
rubric it points at spells the model `Sonnet 5 / xhigh`. A session following
either was correct by its own source, so the field drifted with nobody making
a mistake. Measured across a real 44-repo tree: 14 board lines carrying eight
distinct strings, and not one of them the form documented here - the only
spelling nobody used was ours.

The help now prescribes the rubric's, which is also what most lines already
carry. The parser is untouched and stays lenient: it reads to `;` or `-->` so
the rubric's spacing survives, and the fixtures still vary the spelling on
purpose to pin that. Read-side leniency was never the defect; it is what makes
a documentation-only fix sufficient.

No normalization at read, deliberately. Board lines are rewritten every session
by convention, so an unambiguous spec self-heals the existing lines in about one
session per active repo - mapping variants would be permanent code migrating
data that migrates itself. Nothing compares the field mechanically today either:
KOST is a display column and the board sorts on a numeric bucket key, so the
defect was real but latent. Two checks (28 -> 30) pin the documented form,
red first, because cleaning the data cannot fix a spec that regenerates the
divergence every session. Validation at write has no write path to attach to:
nothing in this repo emits a board line outside the fixtures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
2026-07-28 22:06:45 +02:00
f7ad4741a3 docs(board): keep the axis rationale, drop the private referents
The v0.9.0 comments explained the attention-vs-topic axis by pointing at the
operator's private cross-repo register by path, naming an internal incident
code, and quoting a measured count of the operator's own repo population.
This repo mirrors to a public remote, so operational metadata about a private
machine does not belong in it - the reasoning does, the referents do not.

Rewritten self-contained: the topic-axis contrast now stands on its own terms
without citing a file a public reader cannot see, the defect class is named
rather than coded, and the worktree rationale keeps WHY .git is tested with
-e (a worktree or submodule has .git as a FILE) while dropping the census
that motivated measuring it. Illustrative paths stay; they carry no fact.

Comments only, no behavior change: 136 + 28 selftest checks and 6/6 node
tests unchanged. No version bump, so the v0.9.0 tag and the catalog ref stay
where they are - and with them the pre-scrub text, which history and that tag
still carry. Un-publishing that is a history rewrite and the operator's call,
not a side effect of a comment edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
2026-07-28 21:35:42 +02:00