# 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.sh` writes, `coord-inbox.sh` reads (formatted for context injection), `coord-done.sh` archives, `coord-count.sh` counts without delivering. Everything is pinned by `coord-selftest.sh` (136 checks, throwaway mailbox via `CLAUDE_COORD_DIR`). **Reading is delivering — counting is not.** `coord-inbox.sh` records 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.sh` exists 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 calls `coord-inbox.sh` and emits the `hookSpecificOutput.additionalContext` envelope. 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 by `board-selftest.sh` (28 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.sh` is a deployed copy (the operator's `board()` shell function points at it), exactly as with the `coord-*` 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. `board` additionally owns the *ranking* — which repo wins and why — since `board.sh` deliberately 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/` and `/.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`, no `readarray`/`mapfile`, no `|&`; guard `shift 2` with `$# -ge 2`; guard empty-array expansion under `set -u` with `${#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.sh` must exit 0 (136/136) and `bash scripts/board-selftest.sh` must exit 0 (28/28). - 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` and `bash scripts/board-selftest.sh` (or `npm 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.