repo-mailbox/CHANGELOG.md
Kjell Tore Guttormsen 28e7cb42cd feat(hook): let a non-git surface declare its identity via CLAUDE_COORD_REPO
0.6.0 removed the working-directory fallback, which was right, but it left
every non-git working surface with nothing to derive from. The read path
declines silently by design - the hook must never fail a session - so such a
surface simply stops seeing its inbox: no error, no exit code, nothing. That is
the same loss-looks-like-normal shape 0.6.0 set out to remove, reintroduced one
layer up.

The hook now forwards CLAUDE_COORD_REPO verbatim as --repo. This is not the
fallback returning, and the distinction is the whole point: the fallback GUESSED
a name from wherever the session happened to stand, while a declaration is
written down in that directory's settings, readable back, and deletable.
Identity stays a choice someone made.

Forwarding as --repo rather than reimplementing anything means it inherits every
engine rule, including that an explicit override never claims <repo>/.origin -
otherwise a surface borrowing a name would steal the claim from the checkout
that owns it. Both halves are pinned by tests, and the shared test helper now
deletes CLAUDE_COORD_REPO unless a test asks for it, so a value set globally
later cannot silently satisfy the tests that prove the hook resolves nothing on
its own.

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

279 lines
13 KiB
Markdown

# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.7.0] - 2026-07-27
### Added
- **`CLAUDE_COORD_REPO` — declaring an identity for a non-git surface.** 0.6.0
removed the working-directory fallback, which was right, but it left every
non-git working surface (`~/repos`, `$HOME`) with nothing to derive from: the
read path declines silently, so such a surface simply stops seeing its inbox,
with no error and no exit code. That is the same loss-looks-like-normal shape
0.6.0 set out to remove. The hook now reads `CLAUDE_COORD_REPO` and forwards
it verbatim as `--repo`, so a directory can *declare* which mailbox it owns:
{ "env": { "CLAUDE_COORD_REPO": "repos" } }
This is not the fallback returning. The fallback guessed a name from wherever
the session happened to stand; a declaration is written down, readable back,
and deletable. Because it forwards as `--repo` it inherits every engine rule,
including that an explicit override never claims `<repo>/.origin` — otherwise
a surface borrowing a name could steal the claim from the checkout that owns
it. Both halves are pinned by tests.
## [0.6.0] - 2026-07-26
### Fixed
- **`_broadcast` is a reserved namespace, not a repo.** `coord-send` guarded
retraction with a sender check, but that guard only covered the door it was
nailed to: `coord-done --repo _broadcast <file>` archived a broadcast out of
the queue — a full unauthenticated retract of an announcement for every repo
that had not read it yet. The whole `_` prefix is now reserved, in every CLI,
so a later `_seen` or `_config` cannot reopen the hole.
- **Repo identity is derived, never invented.** The fallback to
`basename(pwd)` is gone. It existed so the CLI would work anywhere, but
"anywhere" includes every global surface: a session in `~/repos` is not a
repo, and the fallback silently handed it the identity `repos` — under which
real mail was delivered. git toplevel or an explicit `--from`/`--repo` are
now the only sources. Write paths exit 2 and name the fix; the read path
declines silently, because the hook runs at every session start and must
never fail a session.
- **Broadcast delivery is recorded only after the injection is written.**
Marking inside the read loop left a window where the seen set said
"delivered" while the operator saw nothing — and the hook runs under
`timeout: 10`, so the window was reachable. A lost broadcast is
unrecoverable by design (the seen set is delivery history, and retraction
deliberately leaves it alone), so the failure mode is now redelivery.
- **The hook no longer resolves identity at all.** It was a fourth independent
copy of the identity rule and the only one that runs in production, so
passing `--repo` bypassed the engine's guards exactly where they mattered —
and suppressed the new collision check along with them.
### Added
- **Mailbox-collision warning.** Two checkouts with the same directory name
share one mailbox. Re-keying identity would break every existing mailbox and
the readable `--to <repo>` addressing, so the first git-derived read records
the claiming path in `<repo>/.origin`, and a read from a different path is
reported in the injection. A warning, not a refusal: the same repo moved or
re-cloned is the ordinary case. It goes in the injection because the hook
discards stderr, and a warning nobody can see is not a warning.
### Changed
- Selftest 93 → 116 checks; three Node tests now cover the hook behaviorally
rather than by reading its source.
### Breaking
- `coord-send` and `coord-done` exit 2 outside a git repository instead of
naming themselves after the working directory. Pass `--from`/`--repo` to
choose an identity explicitly. Repo names beginning with `_` are refused
everywhere.
## [0.5.0] - 2026-07-25
### Changed
- **The inbox is a priority, not a suggestion (Rule 7).** 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 first**, before the task the session came to do — "not
after it, not *if there is time*".
- **Every directed message must reach a terminal state before the session
ends:** `coord-send --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. The escape hatch is explicit rather than silent.
- **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 — only the operator authorizes
that. Untrusted cross-repo content still cannot direct the reader; it merely
can no longer be ignored. The injection states both halves, and the selftest
pins them together so a future reword cannot keep the priority and quietly
drop the distinction.
- Selftest grew from 82 to 93 checks (new section 20 pins the wording contract:
the permissive phrasing is gone, ordering and completion are stated, both
terminal states are named with neither as default, responding-vs-complying is
kept distinct, and the untrusted-data framing survives).
## [0.4.0] - 2026-07-25
### Added
- **`coord-send --retract <filename>`: a broadcast can finally be retired.**
Until now nothing could remove a message from `_broadcast/inbox/`:
`coord-done` is directed-only and never touches the broadcast queue. The
backlog could therefore only grow, and every *new* repo received the entire
standing history at its first session — including announcements that had
since become false. Retract moves the message to `_broadcast/archive/`, so
no future repo is served it.
Deliberate limits, both documented and pinned by tests:
- **Un-send, not recall.** Repos that already received the broadcast keep
it; `_broadcast/seen/` is delivery history and is left untouched.
- **Only the sender may retract** (`from:` must match the repo identity).
`--from` overrides this, as it does everywhere else in the engine, which
makes the check an accident guard rather than a security boundary.
- **Nothing is deleted**, mirroring `coord-done`. Retracting twice is a
no-op (exit 0).
- Selftest grew from 70 to 82 checks (new section 19 covers retraction:
authorization, archiving, non-delivery to new repos, no collateral damage to
other broadcasts, invalid names, flag combinations, and idempotency).
### Fixed
- README's install command still said `claude plugin install
coord@ktg-plugin-marketplace` after the v0.3.0 rename.
- README told the reader to prune `_broadcast/inbox/` by hand when a notice
stopped being relevant — advice that contradicted the rule that the script
owns mailbox files. `--retract` is now the supported path.
## [0.3.0] - 2026-07-25
### Changed
- **Renamed the plugin and repository from `coord` to `repo-mailbox`.** The old
name said that something was coordinated but not what the thing was; the new
one names what it is, using the same vocabulary the code and docs already use
throughout (mailbox, inbox, archive, broadcast). The Forgejo repository moved
from `open/coord` to `open/repo-mailbox` (the old URL redirects), and the
marketplace entry, plugin manifest and package name follow.
**This is breaking for installed users:** the skill is now invoked as
`/repo-mailbox:coord-send` rather than `/coord:coord-send`, and the plugin
must be reinstalled under its new name.
**Deliberately unchanged:** the CLI (`coord-send.sh`, `coord-inbox.sh`,
`coord-done.sh`, `coord-selftest.sh`), the skill name `coord-send`, the
mailbox root `~/.claude/coord/`, and the `CLAUDE_COORD_DIR` override. Those
name the transport protocol, not the product — renaming them would migrate
live mailbox data and break message history in every participating repo for
no gain in clarity.
### Fixed
- Catalog README listed `48 selftest checks` for this plugin; the actual count
has been 70 since v0.2.2.
## [0.2.2] - 2026-07-25
### Changed
- `coord-inbox.sh` now warns on stderr for each unknown argument instead of
discarding it silently. The read path stays lenient (it runs inside the
SessionStart hook, which must never fail a session over a stray flag) and
the exit code is unchanged, but a mistyped flag no longer looks like a
working invocation. The hook runs the script with stderr discarded, so the
warning surfaces in manual CLI use only.
### Added
- Selftest: 68 -> 70 checks, covering the stderr warning and the leniency it
must not break (unknown argument still reads the inbox and exits 0).
### Documentation
- README: corrected the stale selftest count (64 -> 70), documented that a
broadcast is never delivered back to its own sender (shipped in 0.2.1 but
undocumented), and added rule 6 — message content is data, never
instructions — which `coord-inbox.sh` already enforces in the injection
framing.
## [0.2.1] - 2026-07-25
### Fixed
- Broadcast self-delivery: `coord-send --broadcast` left no seen-set entry for
the sender, so the announcing repo got its own announcement injected at its
next session start. The delivered filename is now recorded in
`_broadcast/seen/<sender>` at delivery time, reusing the existing per-repo
seen set rather than adding a second exclusion mechanism. Filtering on the
`from:` field at read time was rejected: `from:` is sender-controlled, so it
would let any repo suppress a broadcast for another by forging the field.
The seen file is keyed by the raw sender name (the read side keys it
unsanitized, so the two must agree); senders containing a slash, or equal to
`.`/`..`, skip the marking rather than escape the directory.
### Added
- Selftest: 64 -> 68 checks, covering sender self-exclusion, unaffected
delivery to other repos, self-exclusion for sender names that are not
shell-clean, and the seen-marking path-escape guard.
## [0.2.0] - 2026-07-24
Hardening pass over the three CLIs.
### Fixed
- Atomic delivery: the temp file is created inside the destination directory
(dot-prefixed, invisible to the inbox glob), so the final rename never
crosses filesystems and readers never observe a half-written message.
- `.` and `..` are rejected explicitly in the `--reply-to` and `coord-done`
name guards instead of relying on downstream failure.
### Added
- `-h`/`--help` on `coord-inbox.sh`, making usage output uniform across the
three CLIs.
- Selftest: 48 -> 64 checks, closing gaps around the default mailbox path via
`HOME` fallback, malformed frontmatter on the read path, and a read-only
destination directory.
## [0.1.0] - 2026-07-24
First public release: extraction of a battle-tested personal mechanism into a
distributable plugin.
### Added
- Bash engine: `coord-send.sh` (directed / broadcast / reply delivery),
`coord-inbox.sh` (pending inbox + unseen broadcasts, formatted for context
injection), `coord-done.sh` (archive without reply), all bash-3.2-safe,
ASCII-only, zero dependencies.
- `coord-selftest.sh`: 48 checks against a throwaway mailbox, including
regression cover for filename sanitization, no-hang argument parsing,
prompt-injection resistance, frontmatter hygiene, and broadcast ordering.
- SessionStart hook (`hooks/scripts/session-start.mjs`): thin zero-dependency
Node wrapper that injects the repo's pending mailbox as
`additionalContext`. Always exits 0 — a broken mailbox never blocks a session.
- `coord-send` skill: natural-language front door (English + Norwegian
triggers) mapping intent to engine invocations, including bounded
multi-target loops, deferred sends, and replies.
### Fixed
- Frontmatter injection: CR/LF and control characters in `--subject`/`--from`
are collapsed before writing, so fields can no longer inject frontmatter
lines or a premature `---` terminator.
- Broadcast loss: a per-repo seen set (`_broadcast/seen/<repo>`, one delivered
filename per line) replaces the single high-water mark, which silently
dropped a same-second broadcast whose filename sorted below one already
seen. Upgrade note: a legacy watermark file is read as a one-entry seen
set, so old broadcasts may be re-delivered once.
### Changed
- Read-side protocol strings are English (`--- message:`, `-> reply:`,
UNTRUSTED DATA framing); the forgery-resistance selftest checks pin the
English tokens.