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
6.8 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. -
Board (
scripts/board.sh): cross-repo attention board. Reads STATE.md next-step blocks + board lines,git status, and mailbox pending counts, and prints one line per repo. Read-only by construction: it writes to no repo, no STATE.md and no mailbox. Pinned byboard-selftest.sh(30 checks).It lives here because the mailbox is one of its three inputs, and it carries the same axis distinction the mailbox does. A pending count means others are waiting on this repo; who a repo waits on comes only from its board line, because the message format has no reply-to field. Enforcing that in one of two repos would not be enforcing it.
~/.claude/scripts/board.shis a deployed copy (the operator'sboard()shell function points at it), exactly as with thecoord-*scripts — this repo is the source of truth. -
Skills (
skills/coord-send/,skills/board/): natural-language front doors mapping user intent to engine invocations. No mailbox logic lives here either.boardadditionally owns the ranking — which repo wins and why — sinceboard.shdeliberately prints evidence and takes no position.
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) andbash scripts/board-selftest.shmust exit 0 (30/30). - 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.shandbash scripts/board-selftest.sh(ornpm test, the Node wrapper around both) - Hook smoke test:
node hooks/scripts/session-start.mjs(expects JSON on stdout) - Board smoke test:
bash scripts/board.sh(read-only, ~3s over the real tree)
Release
Version must agree across: .claude-plugin/plugin.json, package.json,
README version badge, skills/coord-send/SKILL.md and skills/board/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.