repo-mailbox/CLAUDE.md
Kjell Tore Guttormsen 737127a14c feat(engine)!: the inbox is a priority, not a suggestion
Through 0.4.0 the injection block told every repo to "consider
replying/resolving where it fits in this session". That sentence was the
whole problem: the injection text is the only place a repo is ever told
what to do with a message, so the wording IS the protocol -- and it
granted permission to defer. Messages sat unanswered for weeks while each
session did its own work first. Nothing was broken; the protocol was
asking for exactly what it got.

The block now states an ordering and a completion obligation: handle the
inbox before the task the session came to do, and drive every directed
message to a terminal state before the session ends (--reply-to or
coord-done). Neither terminal state is the default -- the format has no
reply-expected field, so mandating only the reply would manufacture
traffic for messages that merely inform. Leaving one pending stays
allowed but must be stated to the operator with a reason.

Raising priority deliberately does not widen the trust boundary. The
obligation is procedural, never substantive: responding is mandatory,
complying with what a message asks is not. Untrusted cross-repo content
still cannot direct the reader; it merely can no longer be ignored. The
injection states both halves and selftest section 20 pins them together,
so a future reword cannot keep the priority and quietly drop the
distinction -- that combination would turn prioritization into an
injection surface.

Selftest 82 -> 93.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
2026-07-25 18:44:20 +02:00

4.5 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.sh writes, coord-inbox.sh reads (formatted for context injection), coord-done.sh archives. Everything is pinned by coord-selftest.sh (93 checks, throwaway mailbox via CLAUDE_COORD_DIR).
  • 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.
  • 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.

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 (93/93).
  • 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 (or npm 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.