coord-sweep.sh shipped in 0.10.0 and had never run once against the real
mailbox. Measured 2026-09-03 with a denominator (docs/2026-09-03-coordination-
debt-measurement.md): 55 mailbox directories, 52 with an inbox/, 27 pending
directed messages - 23 of them pure notices, re-injected at every session start
in repos nobody had opened. The script was correct and unreachable.
WP5 (order 20260902T113745Z-1254925290) asked for a mechanism and named two
candidates. The measurement chose neither, and the first session returned the
order saying so: bulk-ack for pure notices was already built - it is this
script - so the second candidate would have been two copies of one policy, and
the broadcast class converges on its own (reading sets seen), with 34 of 263
unread pairs belonging to two mailboxes no session can hold, so a TTL would
have closed those rather than reduced them. The operator then chose the window
and authorized the schedule.
launchd/com.ktg.repo-mailbox-sweep.plist runs --write --days 14 daily at 05:30.
That is the entire behavioural change. The window is written out in the plist
rather than inherited from the script's default: it is a policy constant chosen
on a measured distribution (30d -> 0 messages, 14d -> 7, 7d -> 13), so a later
change to DAYS=14 must not silently change what an unattended job closes across
51 other repos. It runs BEFORE the 06:00 briefing agent, which scans the same
mailbox this mutates, so the morning briefing reports the debt that remains
rather than counting notices being closed underneath it.
coord-selftest.sh section 38 pins the launchd templates (242 -> 257 checks). A
wrong program path is the one defect here that nothing catches at runtime: the
agent loads cleanly and then silently never runs, with no output to be wrong
and no exit status to read. launchctl list proves an agent is LOADED, never
that it is RIGHT. The section covers every plist in launchd/, not only the new
one - the plist grammar gets one reader rather than one per agent - while
board-selftest.sh section 9 keeps owning brief-nightly.sh's behaviour. Each
plist must name a script that exists here, carry a Label matching its filename,
keep its __CHECKOUT__/__HOME__ placeholders (public mirror), and never point
into the version-pinned plugin cache.
The cache assertion runs on the extracted path, never the whole file - caught
by the check itself on its first run: the brief plist's header explains in
prose why it does not point at the cache, and a file-wide grep read that
explanation as the defect it warns about, the same shape as prose saying
status=done triggering the board's done-guard. Four controls present; mutation-
verified against the real file, where a one-letter typo (coord-sweeep.sh) turns
exactly that check red. XML well-formedness is deliberately not checked:
plutil is not coreutils, and malformed XML already fails loudly at launchctl
load - the opposite of the silent failure this section exists for.
Also fixes the README selftest-checks badge, stale at 529 since 0.25.0; the
real total is 868 (257 + 368 + 73 + 116 + 54).
Suites: coord 257, board 368, route 73, orders 116, guard 54. npm test 11/11.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`coord-order-done --return` rewrites the order file's mtime, and both age
surfaces read mtime, so putting an order back reset the very reading that
says how long it has waited. An order returned three times could never look
old - on the one surface that exists precisely so a repo nobody opens still
shows something.
Found by reading the board right after this repo returned an order of its
own, not by looking for it: a file whose name says 2026-09-02 rendered
`ORDRE 1:0d` and `pending, 0d old` minutes later. Verified live after the
fix: the same order now reads 1d.
Two questions, two sources, and only one of them moved. A PENDING order's
age is "how long has this sat with no owner" = now - delivery time, which
only the filename carries and nothing rewrites. A CLAIMED order's age is
"how long has it been in flight", which is the claim's own mtime and was
already right. So oldest_pending_age() sits BESIDE oldest_order_age(), and
pending_age_of() beside age_of() - switching FLY to the filename would
answer the delivery question in the column that asks the flight question.
An unparseable filename yields "?" for the whole reading, never a
fabricated 0, because an unmeasured order could be the oldest one.
TDD, red first: orders-selftest section 11 (110 -> 116) and board-selftest
section 30 (360 -> 368), each asserting its own ground truth before
anything depends on it, with controls that a freshly delivered order still
reads 0d and that FLY did not move. Mutation-verified in both files:
restoring the mtime read turns exactly the defect checks red and leaves
every control green.
Section 28's fixtures were rewritten as part of this rather than
incidentally: they encoded their ages in `touch -t` while their filenames
held fixed 2026-01/2026-08 dates, which a filename-based reading makes both
wrong and time-dependent. They now compute their stems from `date -v` and
the section asserts two ground truths, the filename for ORDRE and the mtime
for FLY.
Order 20260903T185736Z-1290610855 (.claude). Version 0.32.1 across all
seven files; no catalog change in this session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ships two things that belong in one bump because they are one plugin.
`board.sh --voyage` (85cd628, 2026-08-31) sat one commit past v0.31.0,
unreleased and unpushed. Every entry point on this machine runs the
version-pinned plugin cache, so the feature existed in git and did not
exist for anyone using it.
`route.sh --last-model` learned "Fable 5.1" (2bec7fe) with both boundary
decisions taken and written down: "Fable 5" is kept because the row table
itself spells rows 5-6 with it, and the set was widened rather than
replaced by form validation because only a closed set catches a version
that never shipped.
Version sites, enumerated rather than swept - a blank sed would rewrite
the historical prose in CLAUDE.md that deliberately keeps its old number:
plugin.json, package.json, the README badge, and four SKILL.md
(coord-send, board, route, dispatch). CHANGELOG entry added.
Selftest counts corrected where they are asserted as prose: route 69 ->
73 in CLAUDE.md and README.
npm test: 11/11. coord 242, board 360, route 73, orders 110, guard 54.
Order: 20260901T185028Z-3612729538-from-.claude
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two commits landed after v0.30.0 and neither reaches anyone: every entry
point on this machine runs the version-pinned plugin cache, not the tree.
Measured 2026-08-29 by .claude before this order was written - cache
board.sh 1626 lines against the repo's 1739, md5 differing - so the board
skill was still running pre-4369499 code. A commit is not a delivery.
Minor rather than patch: ee5a86a adds a reading the table did not have
(ORDRE and FLY carry the age of the oldest order), while 4369499 repairs
a consumer that discarded a signal the engine already sent. No new
key=value key, no new exit code, no new env var, no sort-axis change.
Version bumped in all seven files that carry it: .claude-plugin/plugin.json,
package.json, the README badge and the four SKILL.md frontmatters. The
0.30.0 strings remaining in CLAUDE.md, board.sh and board-selftest.sh are
historical prose about what was measured on that release and are correct
as they stand - a blanket sed would have falsified them.
CHANGELOG 0.31.0 written for a reader of the board rather than a reader
of the diff: what the columns now say, and what --brief no longer claims.
Suites under real /bin/bash 3.2 after the bump: coord 242/242, board
325/325, route 69/69, orders 110/110, guard 54/54. npm test 11/11.
No tag and no catalog change, as ordered - release-plugin.mjs lives in
catalog and mints the tag and the ref atomically; a hand-made tag here is
exactly the drift that script exists to prevent.
Order: 20260829T071929Z-786087280-from-.claude
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two commits landed after v0.29.0 and neither is reachable: every entry point
on this machine runs the version-pinned plugin cache, not the tree.
Minor rather than patch because the behaviour is EXTENDED, not merely
repaired - coord-count.sh and board.sh gained a new exit status (3, "the
world you named is not there"), and pre-state-line-guard.mjs gained
CLAUDE_STATE_MAX_LINES. Exit codes are a contract, and the installed cache
still runs the old one.
Version synced across all seven version-bearing files - plugin.json,
package.json, the README badge, and the four SKILL.md frontmatters - so
release-plugin.mjs's plugin.json == README-badge == target check passes.
The three remaining 0.29.0 mentions (CLAUDE.md:915, CLAUDE.md:960,
board.sh:1388) are historical prose about when things shipped and must keep
saying 0.29.0.
CHANGELOG covers BOTH unreleased commits, not just the newest: d8fdeaa's
five defects were one class - a failed measurement rendering as a
reassuring value - and the entry carries what that session found about
itself, that --plan's free-capacity test had to move from "$7 + 0" to a
string comparison against "0", because "?" coerces to 0 in arithmetic and
would have certified an UNMEASURED tree as free capacity. The defect
reappearing one layer down, wearing the fix as a disguise.
No tag and no catalog change: that is the catalog session's order, which
runs release-plugin.mjs to do the tag and the ref bump atomically. Doing
both here would take a decision on the catalog's behalf without its
context.
Suites verified under system bash 3.2, not Homebrew 5.3: coord 242, board
300, route 69, orders 110, guard 54; npm test 11/11.
Order 20260826T115026Z-998059640-from-.claude
ORDRE counted pending orders only, so a repo with one order in flight and a
repo with no orders at all both printed 0 - the same digit for two opposite
facts. Measured 2026-08-23: two panes stood open and idle for 45 hours holding
finished orders, with full quota authorised, and no column on the board
reported it.
FLY counts orders/claimed/ - the same queue in its other state, never summed
with ORDRE and never a fourth axis. It does NOT mean a session is alive:
nothing un-claims an order when the claiming session dies, and one order on
the live mailbox had been claimed for 117 hours. The legend denies the
liveness reading in those words, pinned as a check on the legend text.
--plan now names free capacity as ledig_antall=N plus one ledig=<repo>
(<status>) line per repo with nothing owed, no pending order, nothing in
flight, a clean tree, at done or deferred. All four conditions are required:
measured on the real tree, 4 of 17 done/deferred repos were not free. Lines,
never blocks - the plan's second consumer discards a block with no tab=, so a
block would be visible to the operator and invisible to the driver.
Process inspection was considered and refused; the selftest asserts the
absence of pgrep/pkill/lsof structurally with a known-positive control. The
full argument, and the gap left open, are in
docs/2026-08-23-free-capacity-investigation.md.
board-selftest: 259 -> 281 checks, all green. npm test 11/11.
Order 20260823T162951Z-941745020-from-.claude
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hfbfr8ERC63kuAfWeHYHQb
Five fixes were committed and pushed after v0.27.0 and none is reachable:
every entry point on this machine runs the version-pinned plugin cache, not
this tree. This bump is step one of making them reachable - the tag and the
catalog ref follow together via release-plugin.mjs, never one without the
other.
Carries: dot-prefixed repo discovery (3e5add0), the bare-name defect in the
three order-verb emitters (393499c), --to refused on a control character in
both write sides (29a94dd), the FILENAME== focus-filter join (2928c28), and
route_cmd_for reading the rationale instead of the field (b70786d).
Version bumped in all seven places CLAUDE.md's Release section names:
plugin.json, package.json, the README badge, and the frontmatter of
skills/coord-send, skills/board, skills/route, skills/dispatch. CHANGELOG
entry added alongside.
Pre-release gate, all five suites plus npm: coord 230, board 259 (was 252 -
section 23 added 7 checks), route 69, orders 110, guard 40, node 11/11.
Catalog pre-flight run by hand first per the order: 12 plugins, 0 ERROR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZa4oEsa93ac6pZQFEXqBF
ORDRE 59. A dispatched order used to live only in a scratch prompt file
passed through argv, so it died with the pane it was typed into. Measured
2026-08-17: one order was dispatched three times over 90 minutes before it
was worked, because the first two tabs ran something else and the order
left no trace in the receiving repo at all.
New channel `~/.claude/coord/<repo>/orders/`, beside `inbox/` and never
merged with it. The axis is authorization: inbox content is untrusted
cross-repo data that may never instruct a session (Rule 6), a dispatch
order is operator-authorized work by construction. One channel carrying
both classes would mean either mail that can instruct or orders that
cannot, so the infrastructure is reused and the channel is not.
Four one-verb engines: coord-order-send.sh (write), coord-order-inbox.sh
(read, writes nothing at all), coord-order-claim.sh (atomic claim),
coord-order-done.sh (executed with a commit pointer / --no-commit with a
reason / --return with a reason).
The claim is a rename with no check-then-act step, so of N racing sessions
exactly one finds the source and the rest get ENOENT. The test that proves
it spawns 20 claimers BARRIERED on a start flag - unbarriered children do
not race at all - and runs the identical harness against a deliberately
racy `[ -e src ] && cp && rm` as a known-negative control, which must
produce many winners. Without that control, "exactly one winner" is
indistinguishable from "the race never happened".
Channel separation is pinned structurally, not only behaviourally: no mail
script may contain the string `orders`, with a known-positive control
proving the grep can find. coord-done cannot archive an order and
coord-order-claim cannot claim a message.
board gains an ORDRE column beside INN, counted with the identical idiom
and never summed with it: INN is "others are waiting on YOU", ORDRE is
"work is waiting on this REPO". Claimed orders are excluded - the column
answers what a session can pick up. board.sh --dispatch --order-id emits a
thin starter carrying only the id and the four steps, so the order text has
exactly one home; the id is validated shell-clean and must be pending in
the target's queue.
SessionStart injects the queue as its own block below the mailbox block.
Two channels, two blocks, mail first: it carries Rule 7, and the queue
order is mail -> orders -> STATE's NESTE.
Also folds in dde392d (board prefix-match fix), which landed after the
0.26.0 bump and before any tag. v0.26.0 was never tagged, so 0.27.0 is the
release that carries all of it.
Suites: coord 220, board 237, route 69, orders 97, guard 40; npm test 11/11.
Antakelse 4 (atomic claim) and antakelse 6 (morning --plan-file --dry-run
reports 1 of 1 for the thin starter) both measured, not assumed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iB7ipXGgEpv9imYoVmr2
The done-guard (d16a3f5) landed after 0.25.0 was already tagged and published
on 95ac710, with the catalog ref pinned to it (catalog 01161f0). Moving a
published tag would swap content under a name consumers may already have
fetched, so the guard gets its own release instead.
Version bumped in all seven places: plugin.json, package.json, the README
badge, and the coord-send / board / route / dispatch skill frontmatters.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4LMWBQGmufmBU6UdvJZ2E
Version bumped across plugin.json, package.json, the README version badge and
all four skill frontmatters (coord-send, board, route, dispatch), plus the
README skills (3 -> 4) and selftest-checks (433 -> 529) badges. CHANGELOG
entry for --dispatch/the dispatch skill and the coord-send reply-mode fix.
NOT released: no tag, no catalog ref bump. Both require pushing to Forgejo,
and the operator has held the push window closed. The catalog gate is clear
(check-versions.mjs: 12 plugins, 0 ERROR; the single WARN is this bump
itself), so the release runs the moment the window opens.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ett8uHMDLir6trFaMzrYRu
"Start a session in repo X, on order Y, at cost Z" was produced by hand, and
it misfired four times on 2026-08-16 across two repos. Three distinct holes,
all measured, all closed here:
1. A bare `claude --model X --effort Y` forces the operator to type Go, and
the session then guesses its task out of STATE.md. The emitted command
carries the prompt in argv: `... "$(cat <file>)"`. Verified directly that
this passes the file's bytes as ONE argv element with no re-evaluation, so
$(...), backticks, quotes and UTF-8 in the prompt BODY are inert - only the
PATH is expanded, so it must be absolute and shell-clean.
2. --no-go stops only the follow-up Go message, never the work (morning:806).
The plan-file form says so in its own output, not just in a comment.
3. A session dispatching its own next session gets an empty plan: morning's
plan_drop_open (morning:1788) drops a block whose repo already has a pane,
and --dry-run says "0 of 1", which reads as a broken plan file. --dispatch
therefore emits two forms, chosen by --target-pane: a plan block, or a
bare paste line for the tab that already exists (and no tab= key at all,
so it can never be fed to morning as a plan).
Generator ownership, the question left open for two sessions: it goes in
board.sh, which already owns the block format including paste=. A second
emitter of tab=/repo=/dir=/command=/paste= would be two copies of one file
format. Read-only survives - the prompt file and the plan file are written by
the caller, the brief-nightly.sh split unchanged.
--target-pane yes|no is REQUIRED with no default, the same rule --last-effort
carries: it is a measurement (morning --probe-panes, which works without a
tty), and the dry-run cannot substitute for it - run from a Claude session
morning reports "window: unknown ... assuming an empty window" and
plan_drop_open never fires, so a dry-run gate would pass the self-dispatch
case every time.
Cost comes from route.sh's row table; --dispatch deliberately takes no
--model/--effort, because --advisor opus is a property of the ROW and a
dispatch taking the model directly has no honest source for that flag.
New skills/dispatch/SKILL.md is the front door. board-selftest 183 -> 217.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ett8uHMDLir6trFaMzrYRu