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
383 lines
19 KiB
Markdown
383 lines
19 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).
|
|
|
|
## [Unreleased]
|
|
|
|
### Fixed
|
|
|
|
- **The board line's `next-cost` field had two specs, and one of them was
|
|
ours.** `board.sh --help` prescribed a versionless `sonnet/xhigh` while the
|
|
global model rubric it points at spells the model `Sonnet 5 / xhigh`. Neither
|
|
a session following the help nor one following the rubric was doing anything
|
|
wrong, so the field drifted: measured across a real 44-repo tree, the 14
|
|
existing board lines carried **eight** distinct strings — and not one used the
|
|
form documented here. The help now prescribes the rubric's spelling
|
|
(`Sonnet 5/xhigh`), which is also the one most lines already use.
|
|
|
|
**The parser is unchanged and stays lenient.** It still reads to `;` or
|
|
`-->` rather than stopping at the first non-lowercase character, precisely so
|
|
the rubric's spacing survives; the selftest fixtures still vary the spelling
|
|
on purpose to pin that. Leniency on the read side was never the defect — it
|
|
is what makes a documentation-only fix sufficient.
|
|
|
|
**No normalization was added, deliberately.** Board lines are rewritten every
|
|
session by convention, so an unambiguous spec self-heals the existing lines
|
|
within about one session per active repo. Mapping variants at read time would
|
|
be permanent code migrating data that migrates itself, and nothing compares
|
|
the field mechanically today: `KOST` is a display column, and the board sorts
|
|
on a numeric bucket key. Two new `board-selftest.sh` checks (30 total) pin the
|
|
documented form, because a spec that contradicts the rubric regenerates the
|
|
divergence no matter how often the data is cleaned up.
|
|
|
|
## [0.9.0] - 2026-07-28
|
|
|
|
### Added
|
|
|
|
- **`board.sh` + the `board` skill — the cross-repo attention board.** The
|
|
mailbox answers "who wrote to me"; it never answered "which repo deserves the
|
|
next session". `board.sh` scans every discovered repo and reads three sources
|
|
per repo: the STATE.md next-step block with its optional board line, `git
|
|
status`, and that repo's pending mailbox count. Read-only by construction — it
|
|
writes to no repo, no STATE.md and no mailbox — and pinned by
|
|
`board-selftest.sh` (28 checks against a throwaway repo tree and a throwaway
|
|
mailbox).
|
|
|
|
**Why it ships here rather than as a personal script.** The mailbox is one of
|
|
its three inputs, and the two carry the same axis distinction: a repo's pending
|
|
count means *others are waiting on it* (an obligation owed outward), while who
|
|
a repo waits *on* is only derivable from its own board line, because the
|
|
message format has no reply-to or thread field. Splitting the board from the
|
|
mailbox would put that distinction in two places, and a rule enforced in one of
|
|
two places is not a rule. It also means the engine resolves through
|
|
`CLAUDE_PLUGIN_ROOT` like every other script here, instead of existing only on
|
|
the author's machine.
|
|
|
|
The skill is a ranking, not a report: it re-runs the board on every invocation
|
|
(counts drift between turns), ranks by what unblocks the most and what is
|
|
cheapest to move, then names one repo, the rule that fired, and the real next
|
|
action read from that repo's STATE.md — never the table. Deliberately not wired
|
|
into session start: injecting the whole board into every session spends context
|
|
on repos the session is not in.
|
|
|
|
## [0.8.0] - 2026-07-27
|
|
|
|
### Added
|
|
|
|
- **`coord-count.sh` — counting without delivering.** Reading *is* delivery in
|
|
this engine: `coord-inbox.sh` prints a broadcast and then records it as seen,
|
|
so there was no way to ask "what is pending" without answering it. Asking per
|
|
repo would have consumed every repo's broadcast backlog as a side effect —
|
|
once, silently, and unrecoverably, since the seen set is delivery history that
|
|
retraction deliberately leaves alone. `coord-count.sh` counts files instead
|
|
and writes nothing: no seen set, no `.origin`.
|
|
|
|
It keys on **mailboxes**, not repos. It enumerates `$COORD/*` and never scans
|
|
a filesystem for checkouts, so a repo without a mailbox is not missing from
|
|
the count — it is absent from the domain. Drained mailboxes are omitted rather
|
|
than reported as zero, the reserved `_` namespace is skipped, and `--exclude`
|
|
drops one mailbox (the caller's own).
|
|
|
|
- **A cross-repo line in the SessionStart injection.** A session whose own inbox
|
|
was empty saw silence and concluded "all clear" while mail sat unanswered in
|
|
every other mailbox. The read path now appends one aggregate line:
|
|
|
|
Elsewhere in the mailbox: 3 unhandled messages across 2 other mailboxes.
|
|
Counted, not delivered -- none of it is yours to handle here. Run
|
|
coord-count for the per-mailbox breakdown.
|
|
|
|
Deliberately an **aggregate of two integers, not a roster**. A list of names
|
|
would reproduce every other repo's situation inside this repo's injection —
|
|
the state boundary the mailbox exists to respect — and mailbox names are
|
|
cross-repo input. Two integers cannot carry anything that escapes framing.
|
|
|
|
The disclaimer is engine behavior, not politeness (Rule 7): the line lands
|
|
directly beneath *"handle this inbox FIRST"*, and without it the numbers read
|
|
as an extension of that obligation. Pinned in selftest section 26, exactly as
|
|
section 20 pins the priority text.
|
|
|
|
### Changed
|
|
|
|
- **BREAKING (injection contract): the read path is no longer silent whenever
|
|
*this* repo has nothing pending.** It is now a silent no-op only when the
|
|
whole mailbox is empty. The empty-inbox session is precisely the one that
|
|
needed the cross-repo line, so coupling the line to having your own mail would
|
|
have hidden it from its only real audience. Three selftest assertions that
|
|
used "no output at all" as a proxy for "nothing was delivered" now assert the
|
|
absence of the content itself, which is what they always meant.
|
|
|
|
## [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.
|