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
88 lines
4.5 KiB
Markdown
88 lines
4.5 KiB
Markdown
# 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.
|