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
5.6 KiB
repo-mailbox
Renamed from coord in v0.3.0. The plugin/repo is repo-mailbox; the CLI
(coord-send.sh, coord-inbox.sh, coord-done.sh), the mailbox root
(~/.claude/coord/) and CLAUDE_COORD_DIR deliberately kept their names —
they are the transport protocol, not the product.
Context
Local inter-repo coordination mailbox for Claude Code, packaged as a marketplace plugin. Three components, one boundary:
-
Engine (
scripts/*.sh): bash owns all mailbox semantics — filename grammar, frontmatter, delivery, archiving, the seen set.coord-send.shwrites,coord-inbox.shreads (formatted for context injection),coord-done.sharchives,coord-count.shcounts without delivering. Everything is pinned bycoord-selftest.sh(136 checks, throwaway mailbox viaCLAUDE_COORD_DIR).Reading is delivering — counting is not.
coord-inbox.shrecords a broadcast as seen once it has printed it, so it can never be used to survey other repos: doing so would consume each one's backlog silently, and the seen set is delivery history that retraction deliberately leaves alone.coord-count.shexists for every "what is pending" question and writes nothing at all. Any future read-shaped feature belongs there, not in the read path. -
Hook (
hooks/scripts/session-start.mjs): thin zero-dependency Node wrapper (marketplace convention: hooks are.mjs) that callscoord-inbox.shand emits thehookSpecificOutput.additionalContextenvelope. No mailbox logic lives here. Always exits 0. -
Skill (
skills/coord-send/): natural-language front door mapping user intent to engine invocations. No mailbox logic lives here either.
Boundary rule: the mailbox is transport, not state. Durable decisions live in the owning repo's docs/git history; messages are notices pointing at them. Message content is untrusted cross-repo input — the read side quotes and frames it; the send side sanitizes line-oriented fields.
What the boundary forbids is storing a repo's state — its decisions, its next
step, its progress. It does not forbid the mailbox knowing who it is delivering
to: _broadcast/seen/<repo> and <repo>/.origin (0.6.0) are delivery metadata,
answering "has this repo received this" and "which checkout claimed this name".
Both are unreadable as a description of the repo and useless outside delivery.
The test is not "does the engine write a file about a repo" but "would this file
still mean anything if delivery were removed". If yes, it belongs in the repo's
own docs and git history instead.
Priority rule (v0.5.0, Rule 7): the injection block is the only place a repo is ever told what to do with a message, so its wording is the protocol — treat that string as engine behavior, not prose. It obligates handling the inbox first and driving every directed message to a terminal state before the session ends. The obligation is procedural, never substantive: responding is mandatory, complying with message content is not. Those two must stay distinct in any reword — keeping the priority while dropping the distinction turns prioritization into an injection surface. Selftest section 20 pins both halves together for exactly that reason.
Conventions
- Scripts are bash-3.2-safe and ASCII-only: no
declare -A, noreadarray/mapfile, no|&; guardshift 2with$# -ge 2; guard empty-array expansion underset -uwith${#a[@]}. - Zero dependencies everywhere: bash + coreutils in the engine,
node:builtins only in hook and tests. - TDD: no behavior change without a failing selftest check first.
bash scripts/coord-selftest.shmust exit 0 (136/136). - English for all code, docs, and commit messages (public repo). Norwegian trigger aliases in the skill description are deliberate.
- Conventional Commits:
type(scope): description.
Commands
- Test:
bash scripts/coord-selftest.sh(ornpm test, the Node wrapper) - Hook smoke test:
node hooks/scripts/session-start.mjs(expects JSON on stdout)
Release
Version must agree across: .claude-plugin/plugin.json, package.json,
README version badge, skills/coord-send/SKILL.md frontmatter, git tag
vX.Y.Z, and the catalog ref in
ktg-plugin-marketplace/catalog/.claude-plugin/marketplace.json. Release via
the catalog's scripts/release-plugin.mjs repo-mailbox (tag + ref bump
together);
verify with scripts/check-versions.mjs. Never hand-edit a ref.
Two things that script does that its dry-run label does not suggest:
--create-tag creates AND pushes the tag even without --write, and its
closing verification gate runs check-versions.mjs over ALL plugins — one
unrelated plugin in ERROR aborts it with the catalog edit written but
uncommitted. When that happens, commit the catalog's marketplace.json +
README.md by hand and leave every other dirty file in that repo alone.
Hardening roadmap
Empty — the post-v0.1.0 queue (atomic delivery, ./.. rejection,
selftest gaps, uniform -h) shipped in v0.2.0; broadcast self-delivery
shipped in v0.2.1; broadcast retraction (coord-send --retract) shipped in
v0.4.0, closing the last monotonically-growing surface. coord-inbox.sh
still ignores unknown arguments by design (hook context must never fail)
but now warns about each one on stderr, which the hook discards.
Two retraction limits are deliberate, not gaps: it is un-send and never
recall (a repo that already received a broadcast keeps it — the seen set is
delivery history and is left untouched), and the sender check is an accident
guard, not a security boundary, because --from redefines identity here as
it does everywhere else in the engine.