feat(engine)!: counting is not delivering, and an empty inbox is not all clear

Reading IS delivery in this engine: coord-inbox.sh prints a broadcast and then
records it as seen. That made "what is pending elsewhere" unanswerable -- asking
the read path per repo would have consumed every repo's broadcast backlog as a
side effect, once, silently, and unrecoverably, since the seen set is delivery
history that retraction deliberately leaves alone.

coord-count.sh answers it by counting files and writing nothing: no seen set, no
.origin. It keys on MAILBOXES rather than repos -- it enumerates $COORD/* and
never scans a filesystem for checkouts -- so a repo without a mailbox is not
missing from the count, it is absent from the domain. Drained mailboxes are
omitted rather than reported as zero, because the question is "who is owed a
reply" and a list of zeroes answers a different one at every reader's expense.

The read path now closes with one aggregate line built from it. The case that
motivated this is the session whose own inbox is empty: it saw silence and
concluded "all clear" while mail sat unanswered everywhere else.

BREAKING (injection contract): the read path is no longer silent whenever THIS
repo has nothing pending. It is a silent no-op only when the whole mailbox is
empty. Coupling the line to having your own mail would have hidden it from its
only real audience. Three selftest assertions that used "no output at all" as a
proxy for "nothing was delivered" now assert the absence of the content itself,
which is what they always meant.

The line is an AGGREGATE of two integers, never a roster. A list of names would
reproduce other repos' situation inside this repo's injection -- the state
boundary the mailbox exists to respect -- and mailbox names are cross-repo
input. Two integers cannot carry anything that escapes the framing.

Its disclaimer is engine behavior, not politeness (Rule 7): the line lands
directly beneath "handle this inbox FIRST", and without it the numbers read as
an extension of that obligation and a session starts answering other repos'
mail. Pinned in selftest section 26 exactly as section 20 pins the priority
text. The hook's header drops "(unread messages)" for the same reason -- it
would now announce mail that does not exist.

Selftest 116 -> 136. Every new negative check is anchored to a positive
assertion in the same output, because a missing script makes "X is absent"
true by vacuity and would have gone green proving nothing.

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

View file

@ -8,7 +8,7 @@
*AI-generated: all code produced by Claude Code through dialog-driven development.*
![Version](https://img.shields.io/badge/version-0.7.0-blue)
![Version](https://img.shields.io/badge/version-0.8.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Hooks](https://img.shields.io/badge/hooks-1-green)
![Skills](https://img.shields.io/badge/skills-1-orange)
@ -71,7 +71,9 @@ The plugin ships empty: your mailbox is created lazily on first send, on your ma
**Receiving** is automatic: the SessionStart hook injects your repo's pending inbox and unseen broadcasts as context, with per-message reply/resolve hints. Since v0.5.0 the injection also states the priority contract — handle the inbox before the work the session came to do, and drive every directed message to a terminal state before the session ends (Rule 7).
**CLI.** The engine is three bash scripts in the plugin's `scripts/` directory; resolve them as `"${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/scripts/coord-<name>.sh"` (from a terminal, use the plugin's install path):
Since v0.8.0 it closes with one aggregate line about mail pending in *other* mailboxes, so an empty inbox no longer reads as "all clear" while messages sit unanswered elsewhere. Two integers, never a roster: naming the other mailboxes would put their situation inside your repo's injection, and the line explicitly disclaims the obligation it sits beneath — those counts are not yours to handle, and counting them delivered nothing.
**CLI.** The engine is four bash scripts in the plugin's `scripts/` directory; resolve them as `"${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/scripts/coord-<name>.sh"` (from a terminal, use the plugin's install path):
coord-send.sh --to <repo> --subject "<subject>" [--message "<text>"] # or body on stdin
coord-send.sh --broadcast --subject "<subject>" <<'BODY' ... BODY
@ -79,6 +81,7 @@ The plugin ships empty: your mailbox is created lazily on first send, on your ma
coord-send.sh --retract <filename> [--from <sender>] # retire your own broadcast
coord-inbox.sh [--repo <name>] # print pending (what the hook injects)
coord-done.sh <filename>... | --all # archive without replying
coord-count.sh [--exclude <mailbox>] # count pending per mailbox, delivering nothing
The reply/resolve hints the hook injects (`-> reply: coord-send --reply-to … | done without reply: coord-done …`) refer to these scripts.
@ -93,7 +96,7 @@ Cross-repo message content is untrusted input by design:
- **Atomic delivery:** the temp file is created inside the destination directory (dot-prefixed, invisible to the inbox glob), so the final rename never crosses filesystems and readers never observe a half-written message.
Every guarantee above is pinned by the 116-check selftest, including forgery-resistance regressions.
Every guarantee above is pinned by the 136-check selftest, including forgery-resistance regressions.
Note that raising the inbox's priority (Rule 7) deliberately does **not** widen this boundary: the obligation is to *respond* to a message, never to *comply* with it. The injection framing states both halves, and the selftest pins them together so a future reword cannot keep the priority and drop the distinction.
@ -115,7 +118,7 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
## Development
bash scripts/coord-selftest.sh # 116 checks against a throwaway mailbox
bash scripts/coord-selftest.sh # 136 checks against a throwaway mailbox
npm test # same selftest via node --test
TDD is the house rule: every behavior change lands with a failing selftest check first.