Replaces the weighted score shipped in 0.19.0 with five lookups: chain-root credit, unhandled inbox, planned, in-progress, undeclared status. Within a group: that group's own quantity, then a Sonnet next-cost, then oldest plan. The score's objection is accepted, not forgotten, and is written into board.sh and CLAUDE.md so a later session reads it as decided rather than as an unfixed defect: a group order cannot express "owes one message AND releases two others" as one quantity. What the score could not do was hold still for the format's second consumer - re-tuning one weight against another silently reorders a parser in another repo, and no test here can catch that. planned now ranks above in-progress, inverted by the same decision: converting a decision into motion is the slow step; live work is already moving. Debt stays uncapped and never excluded. One group below chain-root credit is not the cap declined at 0.19.0 - the debtor keeps its tab, its most-owed-first position, and its why=inbox:N. Pinned by a discriminating fixture the score would fail: a root releasing one repo outranks a repo owing four. board-selftest 134 -> 138. Suite 183 + 138 + 73 = 394. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6ULuFCPMNYAPNN3pAjsXQ
905 lines
47 KiB
Markdown
905 lines
47 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.20.0] - 2026-08-03
|
|
|
|
### Changed
|
|
|
|
- **`--plan` ranks on FIVE ORDERED GROUPS, replacing the weighted score shipped
|
|
one version earlier.** The groups are chain-root credit, unhandled inbox,
|
|
`planned`, `in-progress`, and undeclared status — each a lookup over a field
|
|
the scan already read, so a selftest pins an outcome rather than a
|
|
coefficient. Within a group: the quantity that group is about, then a Sonnet
|
|
`next-cost`, then oldest plan first.
|
|
|
|
The objection the score answered is **accepted, not forgotten**, and is
|
|
recorded in `board.sh` and `CLAUDE.md` so it is read as decided rather than as
|
|
an unfixed defect: a group order genuinely cannot express "owes one message
|
|
**and** releases two others" as a single quantity. What the score could not do
|
|
was hold still for the format's second consumer — re-tuning 40 against 15
|
|
silently reorders a parser living in another repo, and no test here can catch
|
|
that. Operator decision, 2026-08-03.
|
|
|
|
- **`planned` now ranks ABOVE `in-progress`**, inverted from every earlier
|
|
version by the same decision. A decided but unstarted next step is the step
|
|
that converts a decision into motion; live work is already moving. Flipping it
|
|
back is a policy change, not a sort fix.
|
|
|
|
- **`why=` names the group that admitted the repo**, not the dominant scoring
|
|
term. The emitted values are unchanged (`unblocks:N`, `inbox:N`, `planned`,
|
|
`in-progress`, `uavklart`), so neither consumer sees a new token — but the
|
|
chain-root group is tested first, so `why=unblocks:N` now wins for **any**
|
|
root, not only where its weight happened to dominate.
|
|
|
|
- **The Sonnet `next-cost` tiebreak introduced with the score is carried
|
|
forward deliberately**, demoted to the last key below group and magnitude. It
|
|
is a lookup, and removing a shipped property nobody asked to remove would be
|
|
the larger surprise. It can now only separate repos that are otherwise equal.
|
|
|
|
### Notes
|
|
|
|
- **Debt is still neither capped nor excluded.** Being one group below
|
|
chain-root credit is not the cap that was proposed and declined at 0.19.0: a
|
|
debtor keeps its tab, its most-owed-first position among the other debtors,
|
|
and its `why=inbox:N`. Selftest section 12 pins both halves on a
|
|
discriminating fixture — a root releasing **one** repo outranks a repo owing
|
|
**four** (which the score would have ordered the other way), and that debtor
|
|
keeps everything it had.
|
|
- `board-selftest.sh` is 134 → **138 checks**. Suite total 183 + 138 + 73 =
|
|
**394**.
|
|
|
|
## [0.19.0] - 2026-08-02
|
|
|
|
### Added
|
|
|
|
- **Chain-root credit in `board.sh --plan`.** `blocked-on` was scanned and then
|
|
used for one display string (`blocked>X`); it never reached the ordering. For
|
|
every `blocked` repo the edge is now followed **transitively** to the root of
|
|
the chain — the first repo not itself blocked — and only that root is
|
|
credited with the repos it holds up. Opening a blocked repo releases nobody;
|
|
opening the root releases everything behind it. Measured on the real tree: the
|
|
root of the one live chain moved from tab 14 to tab 1, ahead of the two repos
|
|
waiting on it.
|
|
- **`why=unblocks:N`.** `why=` now names the *dominant* scoring term rather than
|
|
the first rule that matched, so a block says what opening it would release.
|
|
Previously every planned block read `inbox:N` — the same sentence repeated.
|
|
- **`blocked-on` is a record field**, inserted before `neste` so no existing
|
|
field index moves. `neste` is free prose lifted from a STATE.md and may carry
|
|
a literal `|` from a markdown table, so it stays last.
|
|
|
|
### Changed
|
|
|
|
- **`--plan` ranks on one score, not four hard buckets.** Four groups could not
|
|
express "this repo owes one message and releases two others", which is exactly
|
|
how a chain's root ended up below the repos waiting on it:
|
|
|
|
40 x repos released transitively + 15 x inbox
|
|
+10 in-progress / +5 planned / +2 undeclared, +3 for a Sonnet row
|
|
|
|
Every term is a lookup over a field the scan already read, so the order stays
|
|
reproducible and costs no model call.
|
|
- **`--plan` header lines** now state the score and that a blocked repo never
|
|
receives credit. The format is otherwise **unchanged**: `tab=`, `repo=`,
|
|
`dir=`, `why=`, `status=`, `neste=`, `command=`/`command_missing=`, `paste=`
|
|
and the `fokus_*` header all keep their names and positions.
|
|
|
|
### Not done, deliberately
|
|
|
|
- **No cap on inbox debt.** Proposed alongside the credit and declined by the
|
|
operator: owing a reply is the other axis from a repo's own next step, and
|
|
answering is often precisely what unblocks a chain. The debt weight is 15 —
|
|
above the sum of every other bonus (13) — so one message more still outranks
|
|
any combination of status and cost, and the bonuses only separate repos that
|
|
owe the same. At the proposed 12 they could have flipped it, which would have
|
|
weakened the debt order through a tiebreak.
|
|
|
|
## [0.18.0] - 2026-08-02
|
|
|
|
### Added
|
|
|
|
- **`--focus "<prose>"` — the plan's first cutoff, shipped with the disclosure
|
|
that was its precondition.** `--plan` narrows to the repos whose `STATE.md`
|
|
*declares* a topic marker (`<slug>: <status>`) matching the operator's prose.
|
|
Measured on the real tree: 26 blocks to 6.
|
|
|
|
The filter alone was never the feature. `--plan` documents that it takes one
|
|
position (the order), hides nothing, and labels what it cannot rank rather
|
|
than dropping it — so a silent filter would break a property the format had
|
|
already written down. The same run therefore prints `fokus=` (the slugs the
|
|
prose resolved to), `fokus_droppet=` (blocks removed), `fokus_utenfor=` (the
|
|
repos that *mention* a resolved slug with no marker line, **named**) and
|
|
`fokus_rekkevidde=` (how many `STATE.md` were searched). Every surviving
|
|
block carries `fokus_treff=`, the declaration it survived on — the per-block
|
|
evidence that closed `topics=`.
|
|
|
|
Four constraints are load-bearing rather than stylistic. The held-back class
|
|
is **enumerated, not counted**, because in the measurement behind this
|
|
feature the decisive find — a heavy consumer pinning the library in its build
|
|
file — was invisible to every string measurement and appeared only once the
|
|
population was listed. The wording says **"nevner", never "dekker"**: this
|
|
reports text found in a file, and `board.sh` has no grounds for a claim about
|
|
relevance. The reach line states **which files were searched**, because one
|
|
repo's strongest evidence lives in a `README` this scan never opens. And the
|
|
slug vocabulary comes from the scanned `STATE.md` themselves, so the
|
|
"reads `STATE.md` and no other file" invariant survives — `register.md` owns
|
|
the grammar, this only ever reads a declaration written in it.
|
|
|
|
The reader accepts the marker as operators actually write it (bulleted, bold,
|
|
backticked), not only the strict form the register's own grep looks for —
|
|
that grep losing a bold declaration is precisely the class that hid the
|
|
decisive find.
|
|
|
|
Prose matching no declared slug prints the **full** plan plus
|
|
`fokus_ikke_brukt=`. The driver forwards operator prose verbatim, so a typo
|
|
must not silently produce a morning with no tabs. Ambiguous prose widens to
|
|
the union and names every slug, since picking one silently would make the
|
|
cutoff lie about its own size.
|
|
|
|
Emitted as `key=value` rather than `#` comments deliberately: the format's
|
|
second consumer drops comment lines by rule, so a disclosure written as a
|
|
comment would reach the operator on the terminal path and vanish on the
|
|
driver path — the one case where the cutoff would be applied unseen.
|
|
|
|
`board-selftest.sh` 89 -> 114 checks. The narrowing check is comparative
|
|
(focused block count strictly below unfocused, and non-zero): every
|
|
presence-only assertion passes against a no-op filter, which is exactly the
|
|
state the flag was in while it was being swallowed by the unknown-argument
|
|
fallback.
|
|
|
|
## [0.17.0] - 2026-08-02
|
|
|
|
### Added
|
|
|
|
- **`paste=` in every `--plan` block — the operator's line.** `--plan` shipped
|
|
with `dir=` and `command=` as separate fields, which is correct for a driver
|
|
(it moves the pane itself, then types the command) and unusable for a person.
|
|
The first operator to read a block could not act on it: joining an absolute
|
|
path from one field with a command from another is not a saved line of
|
|
output, it is the step where a session gets started in the wrong repository.
|
|
|
|
`paste=` is the two already joined — `cd <dir> && <command>` — one thing to
|
|
select and paste. Both forms stay, because the two consumers want the same
|
|
fact shaped differently and neither shape serves both.
|
|
|
|
It is emitted **only alongside `command=`**, never once per block: a
|
|
`paste=cd X && ` with nothing after it would run the `cd` and then a bare
|
|
newline, which fails silently by leaving the operator in the right directory
|
|
with no session started and no error to notice.
|
|
|
|
- `board-selftest.sh`: 86 -> 89 checks.
|
|
|
|
## [0.16.0] - 2026-08-02
|
|
|
|
### Added
|
|
|
|
- **`board.sh --plan` — the day plan, a THIRD rendering of the same scan.** The
|
|
table says what the state of every repo is; the briefing says who is waiting;
|
|
the plan says which repos to open a tab for today, in what order, with which
|
|
command to start each. Built on exactly the argument `--brief` was: it is a
|
|
lookup over data the scan already holds, it costs zero model calls, and
|
|
`route.sh` already derives the per-repo command.
|
|
|
|
**The order is the position it takes, and the only one.** There is no cutoff,
|
|
so nothing is hidden, and four deterministic rules decide the ranking:
|
|
|
|
1. **Repos owing mail come first, most-owed first — whatever their status.**
|
|
This is the rule most likely to be "fixed" into a defect. Excluding
|
|
`blocked` or `done` is a claim about a repo's *own next step*, which by
|
|
definition cannot be moved; owing an answer is a different axis entirely,
|
|
and answering is frequently what unblocks it. Measured on the real tree at
|
|
this version, two of 26 planned repos were `done` with an unhandled inbox,
|
|
which is why the fixture tree now pins both `done`-with-debt and
|
|
`blocked`-with-debt rather than the general rule alone.
|
|
2. `in-progress` — live work, oldest plan first.
|
|
3. `planned`.
|
|
4. Repos with no board line, ranked last and **labelled** `why=uavklart`. The
|
|
table already prints a note about these; a plan that dropped them silently
|
|
would repeat exactly that defect.
|
|
|
|
Excluded, and only when the repo owes nothing: `done`, `deferred`, `blocked`,
|
|
and repos with no STATE.md. A tab that cannot be moved is not a plan entry.
|
|
|
|
- **`key=value` blocks rather than prose, because the plan has two consumers:**
|
|
the operator pasting a command, and a separate repo driving a terminal from
|
|
it. Prose would make the rendered format an API that no test in this repo
|
|
could hold stable for a consumer living in another one. Comment lines all
|
|
start with `#`, so a consumer drops them with one rule.
|
|
|
|
Two fields carry measured constraints rather than preferences. `dir=` is
|
|
absolute because a new terminal pane inherits its anchor's working directory,
|
|
so a plan that omitted it would look right and point at the wrong repo. And a
|
|
repo whose route line is missing or unparseable gets `command_missing=` with
|
|
the reason, never an empty `command=`: a driver reading `^command=` would type
|
|
a blank line into whatever that pane is running.
|
|
|
|
### Changed
|
|
|
|
- `route_cmd_for()` is now the single reader of the route-line grammar, shared
|
|
by `--brief` and `--plan`. It distinguishes "no route line" from "a route line
|
|
`route.sh` rejects" by **exit code** rather than by an empty string, because
|
|
both must degrade to a marker and neither may become a guess. `--brief` output
|
|
is unchanged.
|
|
- `board-selftest.sh`: 51 -> 86 checks.
|
|
|
|
### Notes
|
|
|
|
- **Driving a terminal from this plan deliberately does not live here.** The
|
|
measurement in `docs/ghostty-orchestration-measurement.md` is the argument:
|
|
it would be a version-pinned, undocumented composition over a preview API
|
|
whose documented path is already broken upstream and whose regression was
|
|
closed as *not planned*, with a blast radius reaching into other repos' live
|
|
sessions. The dependency runs one way — the driver consumes the plan, the plan
|
|
never knows a terminal exists — so if that API breaks, the plan still prints.
|
|
|
|
## [0.15.0] - 2026-08-01
|
|
|
|
### Added
|
|
|
|
- **`board.sh --brief` — the nightly cross-repo briefing, a second RENDERING of
|
|
the scan `board.sh` already does, never a second scan.** The operator has
|
|
more repos than fits in memory, and the question that actually costs them is
|
|
"who is waiting on me, and what does answering cost" — `board.sh` already
|
|
scans for it, but nothing rendered it into a form an unattended job could
|
|
leave behind. `--brief` prints NESTE **uncut**: the 38-character cut is the
|
|
table column's property, not the record's, and the value used to be
|
|
truncated at record-build time, leaving the cut string as the only copy.
|
|
Each startup command is derived by **calling** `route.sh` with that repo's
|
|
own four traits — `next-cost` alone cannot produce it, since the advisor
|
|
flag is a property of the ROW and two rows can share a model/effort pair
|
|
while differing on it. A repo with no route line is told so rather than
|
|
handed a guess.
|
|
|
|
- **It cross-checks itself against `coord-count.sh`**, and that is the
|
|
substance of the change, not a nicety. The repo scan and the mailbox are two
|
|
different populations: a mailbox can carry a name no scan will ever produce
|
|
— a declared non-git surface (`CLAUDE_COORD_REPO`, e.g. `~/repos`) or a
|
|
checkout outside the roots. Measured on the real mailbox: 11 repos / 21
|
|
messages in the briefing against `coord-count`'s 12 / 22, the missing one
|
|
being the declared surface `repos`. A briefing that only walked the scan
|
|
would answer "who is waiting on you" with a number it quietly knew was
|
|
short.
|
|
|
|
- **Zero model calls**, the deciding property. Measured against 2.1.220 under
|
|
subscription auth: `--max-budget-usd` does bite (`terminal_reason:
|
|
budget_exhausted`, exit 1), but only *after* turn one — floor ~0.25
|
|
USD-equivalent per turn on `claude-opus-5[1m]`. It is a runaway brake, not a
|
|
pre-flight gate, so a nightly `claude -p` job would draw on the same quota
|
|
pool as interactive work every night. Determinism removes the question
|
|
entirely.
|
|
|
|
- **`scripts/brief-nightly.sh`** — the one writer in this path. `board.sh`
|
|
stays read-only: `brief-nightly.sh` renders to a temp file and renames it
|
|
into place, and treats an empty render as a *failed* one, since `board.sh`
|
|
prints nothing when its scan roots don't exist — what a mistyped path or a
|
|
moved home directory looks like. A plain `> file` redirect would destroy
|
|
yesterday's briefing on a bad launchd environment.
|
|
|
|
- **`launchd/com.ktg.repo-mailbox-brief.plist`** — a template carrying
|
|
placeholders, not absolute paths, since this repo is mirrored publicly. It
|
|
points at a checkout rather than the plugin cache, which is version-pinned
|
|
and would break silently on the next bump.
|
|
|
|
- `board-selftest.sh`: 36 -> 49 checks. One check pins a defect caught only by
|
|
eye against the real tree: `fold` copies its input's missing trailing
|
|
newline, which ran the command onto the tail of the NESTE prose and produced
|
|
a briefing whose commands could not be copied. Two more checks landed six
|
|
minutes after this tag, in an untagged follow-up commit that shipped no
|
|
version bump of its own — `board-selftest.sh` had already reached 51 by the
|
|
time `0.16.0`'s own work began, which is why that entry's count starts there.
|
|
|
|
## [0.14.0] - 2026-08-01
|
|
|
|
### Added
|
|
|
|
- **`scripts/coord-sweep.sh` — the FYI backlog closes itself, deterministically
|
|
and without a model.** A notice needs no answer, but it is still re-injected at
|
|
every session start until someone closes it by hand. Across enough
|
|
repositories that hand-closing *is* the manual work the mailbox was supposed to
|
|
remove, and the pending count — the operator's only signal — drowns in messages
|
|
that were never going to be acted on. At the time of writing, 9 of 22 pending
|
|
messages across 12 mailboxes were pure notices.
|
|
|
|
The sweep closes exactly one mechanically decidable class: a directed message
|
|
whose sender declared `reply-expected: no`, older than a grace window (default
|
|
14 days, `--days`). **A message that owes a reply is never touched, at any age,
|
|
with any flag** — answering it would mean deciding something on the receiving
|
|
repo's behalf, which is the one thing this system exists to prevent a session
|
|
from doing.
|
|
|
|
Four properties are load-bearing rather than incidental:
|
|
|
|
- **Dry-run is the default**, inverted from every other script here. The others
|
|
print or deliver; this one destroys pending state, so the safe direction has
|
|
to be the one you get by forgetting a flag.
|
|
- **Closing goes through `coord-done.sh --repo`, never `mv`.** The engine owns
|
|
the filename grammar and the archive layout, and it already refuses
|
|
`_broadcast` on its own — archiving out of there would retire an announcement
|
|
for every repo that has not read it yet, an unauthenticated retract.
|
|
- **Age is read from the filename prefix, never from the file.** A name the
|
|
grammar does not produce has no readable age, and an unreadable age is never
|
|
treated as old: fail-safe, not fail-open.
|
|
- **Every closure is logged** (`$CLAUDE_COORD_DIR/_sweep.log` by default) with
|
|
sender and subject. This is not bookkeeping. A directed message has no
|
|
seen-tracking — only broadcasts do — so the sweep cannot distinguish "seen and
|
|
ignored" from "never delivered", and a notice to a repo left unopened for the
|
|
whole window is closed **unread**. That is an accepted tradeoff; the log is
|
|
the only thing standing between it and a silent disappearance.
|
|
|
|
The `reply-expected` read is bounded to the frontmatter block, matching
|
|
`coord-count.sh`: a body line claiming `reply-expected: no` at column 0 is
|
|
untrusted cross-repo input and must not be able to close its own message. That
|
|
is the one place where getting it wrong would let another repo delete its way
|
|
out of your inbox.
|
|
|
|
Scoped deliberately: no scheduler, no launchd unit, no skill front door. The
|
|
script does nothing until invoked.
|
|
|
|
- Selftest 159 → 182 checks (new section 29). The log check caught a real defect
|
|
during development — the first implementation read `from`/`subject` *after*
|
|
`coord-done.sh` had already moved the file, logging empty values and quietly
|
|
defeating the only safeguard the design has.
|
|
|
|
## [0.13.0] - 2026-07-31
|
|
|
|
### Added
|
|
|
|
- **`route.sh` now emits `--advisor opus` into the startup command, per row and
|
|
on a need.** The advisor is a second, stronger model consulted at key moments
|
|
during a session; Claude Code exposes it both as a launch flag and as the
|
|
global `advisorModel` setting that `/advisor` writes. The setting was tried
|
|
and abandoned here because it applies to every session in every repo and
|
|
burned quota accordingly — but nothing replaced it, so the flag went
|
|
effectively unused and `route.sh` never mentioned it. Two independent
|
|
triggers now decide it:
|
|
|
|
- **rows 1-2, always.** The main model is Sonnet, so `opus` is a capability
|
|
*lift* rather than a peer — Opus judgement at Sonnet cost. This half is
|
|
load-bearing: every `fallback-command` is one row cheaper and the cheapest
|
|
rows are the Sonnet ones, so this is what makes the quota fallback safe to
|
|
take.
|
|
- **rows 3-4, only at `reversibility=costly|one-way`.** The main model is
|
|
already Opus, so the advisor buys peer review — worth paying for when a
|
|
mistake is not cheap to undo.
|
|
- **rows 5-6, never.** Not a judgement call: the CLI rejects every advisor
|
|
for a Fable main model, and refuses `opus` as under-capable for one.
|
|
|
|
The two triggers are almost disjoint by construction, since `costly` forces
|
|
row 3 and `one-way` forces row 4 — so a Sonnet row always has
|
|
`reversibility=cheap`, and neither rule can reach the other's rows.
|
|
`verification=none` is deliberately **not** a third trigger: beyond the stakes
|
|
rule the only cases it would add are mistakes that are cheap to reverse, docs
|
|
sessions (`known/none/cheap/local`) among them. The decision is applied per
|
|
ROW rather than once, so `fallback-command` carries its own correct answer.
|
|
|
|
`route-selftest.sh` grows 17 checks (56 -> 73). Section 14 pins the rule and
|
|
gates the three CLI facts it rests on against the installed `claude` without
|
|
spending a token: advisor validation runs before the empty-prompt check, so
|
|
`-p ""` reaches the validator and stops there. `--help` cannot serve as that
|
|
gate — it short-circuits before option validation, so even an unknown flag
|
|
exits 0 and the gate would pass without testing anything.
|
|
|
|
`skills/route/SKILL.md` gains the two failure modes worth naming: paste
|
|
`command` verbatim rather than dropping the advisor as noise, never add one to
|
|
a command that came back without it, and remember that `--advisor` is a launch
|
|
flag — `/clear` reuses the process and keeps whatever advisor the session
|
|
started with, so it is not enough when the advisor changes.
|
|
|
|
## [0.12.1] - 2026-07-31
|
|
|
|
### Fixed
|
|
|
|
- **`coord-send`'s own script-path expression fell back to a stale deployed
|
|
copy.** A Bash tool call never has `CLAUDE_PLUGIN_ROOT` set as a real shell
|
|
variable — only the skill's own markdown rendering resolves that bare
|
|
token — so the `${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}` fallback silently won
|
|
every time the engine line was actually executed, routing through whatever
|
|
the operator's personal `~/.claude/scripts/coord-send.sh` happened to be
|
|
instead of the plugin's own bundled script. Same defect class `board.sh`
|
|
was already fixed for. Reported live by org-ops: a `--fyi` send hit an old
|
|
deployed copy lacking the flag. `coord-selftest.sh` grows three checks
|
|
(156 -> 159) pinning the fix.
|
|
|
|
## [0.12.0] - 2026-07-31
|
|
|
|
### Fixed
|
|
|
|
- **The last-session record asked the operator for the one field it could
|
|
measure.** `route-last` (0.10.0) exists to make the routing policy
|
|
falsifiable — a policy that always answers "Opus" is not a policy — and it
|
|
only is if `--last-effort` reports what the session actually ran with.
|
|
`route.sh` documented the opposite as settled fact: that a session cannot
|
|
observe its own effort from the inside. That was true when written and is not
|
|
now. Claude Code exports `CLAUDE_EFFORT` into every tool-use context as the
|
|
session's current effort, so a single Bash call reads it.
|
|
|
|
**The premise had a cost, and it was the record's weakest field.** With effort
|
|
unobservable, the record could only be completed by asking the operator at
|
|
session end, which made all four fields block on their presence. It was also
|
|
the weaker measurement, in exactly the way the previous board line is: the
|
|
operator reads the effort off the startup command they typed, so both sources
|
|
report what was *prescribed* rather than what was *run*. Those come apart
|
|
precisely when the record would be worth having — as a session that silently
|
|
ran `xhigh` under a board line saying `high` already showed. Reading the
|
|
variable makes all four fields knowable from inside the ending session, so the
|
|
record no longer waits on anyone.
|
|
|
|
**`route.sh` still does not read the variable, deliberately.** The skill
|
|
measures and passes `--last-effort`; the calculator only computes. A
|
|
calculator that consults its environment is no longer deterministic from its
|
|
arguments, and the route → board round trip in selftest section 6 rests on
|
|
that determinism.
|
|
|
|
- **The reading had a trap that would have hidden itself.** Skill frontmatter
|
|
overrides the session effort while that skill is active, so an `effort:` field
|
|
in `skills/route/SKILL.md` would make the reading report the skill instead of
|
|
the session — a measurement quietly measuring itself, with nothing in the
|
|
output to show it happened. Section 13 pins the absence of that field.
|
|
|
|
- **The neighbouring claim about the model was also wrong, in the other
|
|
direction.** There is no `CLAUDE_MODEL` to read: `model` is a SessionStart
|
|
field and documented as not guaranteed. The session takes the model from what
|
|
it knows itself to be running as, and only the effort is measured.
|
|
|
|
`route-selftest.sh`: 50 → 56 checks.
|
|
|
|
## [0.11.0] - 2026-07-31
|
|
|
|
### Added
|
|
|
|
- **`reply-expected` — the format can finally say "this one needs no answer".**
|
|
Rule 7 (0.5.0) shipped an obligation on a format with four fields, none of
|
|
which could tell a question from a notice. Two consequences fell out of that
|
|
gap and both are now closed: the injection had to name both terminal states
|
|
and prefer neither, and `coord-count.sh` had to treat every unarchived file as
|
|
a reply owed. `coord-send.sh --fyi` writes `reply-expected: no`; omitting it
|
|
writes `yes`.
|
|
|
|
**Absent means expected.** Every message already on disk lacks the field, and
|
|
so does every caller not yet updated, so absence keeps meaning what it always
|
|
meant. The failure mode of a forgotten flag is then over-counting debt —
|
|
visible and harmless — rather than debt that never surfaces. A reply is not a
|
|
special case: it expects one back unless it says otherwise, because a hidden
|
|
exception is the kind of implicit rule this engine refuses.
|
|
|
|
**A broadcast is always `reply-expected: no`**, with or without `--fyi`. Not a
|
|
defaulted value: `--reply-to` resolves inside the recipient's own mailbox and
|
|
a broadcast never lands there, so there is no reply path to promise.
|
|
|
|
**The field is frontmatter, and only frontmatter.** Bodies are untrusted
|
|
cross-repo input, so the read is bounded to the block between the two `---`
|
|
terminators — a body line reading `reply-expected: no` at column 0 cannot
|
|
silence a real debt, and a file without valid frontmatter counts as owing a
|
|
reply. That is stricter than the `grep -m1` the older fields use, where
|
|
frontmatter-comes-first happens to save them.
|
|
|
|
### Changed
|
|
|
|
- **`coord-done.sh --all` says how many replies it closed without sending.** It
|
|
is the third script that acts on pending messages, and the field makes a
|
|
silent bulk close possible: one command archives every pending message,
|
|
including the ones whose sender expects an answer. The behavior is unchanged
|
|
and deliberately so - the receiver keeps both terminal states, and a bulk
|
|
close is legitimate. The silence is what changed: Rule 7 requires an
|
|
unanswered message to be *stated*, and a path reporting only a total hid
|
|
exactly the thing that has to be stated. A named close stays quiet, because
|
|
naming a message is already deliberate, one message at a time.
|
|
|
|
- **`coord-count.sh` prints two integers per mailbox**, not one:
|
|
`<mailbox>\t<pending>\t<debt>`. Replacing pending *with* debt was the obvious
|
|
reading of "count debt rather than unarchived messages" and it is wrong here.
|
|
`board.sh` counts the same inbox files itself, so a debt-only count would put
|
|
two different numbers under one name with nothing to reconcile them; and a
|
|
mailbox holding only notices would read as empty while its messages keep being
|
|
re-injected until someone closes them. The cross-repo line follows:
|
|
*"N unhandled messages (M awaiting a reply) across K other mailboxes"*.
|
|
|
|
- **Rule 7's wording, because its stated reason expired.** The injection said
|
|
"Neither is the default" *because* the format could not distinguish the two
|
|
kinds. It now can, so each message is marked with the terminal state its
|
|
sender expects — and the text states, load-bearingly, that the marking is a
|
|
**declaration, not an instruction**: the receiver keeps both terminal states
|
|
and may close a reply-expected message with `coord-done`, stating the reason.
|
|
Without that clause the new field would be a lever any sender could pull to
|
|
mint obligations in another repo, which is precisely the procedural/substantive
|
|
split Rule 6 exists to hold. Selftest section 20 pins both halves together, as
|
|
it has since 0.5.0.
|
|
|
|
## [0.10.0] - 2026-07-31
|
|
|
|
### Added
|
|
|
|
- **`route.sh` + the `route` skill - the writer for the board's cost field.**
|
|
`next-cost` had a reader and no writer, so it was retyped by hand every
|
|
session; measured across the operator's tree, the existing board lines carried
|
|
eight distinct spellings of a field with one documented form. `route.sh` is a
|
|
pure calculator: four scored traits of the next task (`path`, `verification`,
|
|
`reversibility`, `scope`) plus a required rationale, and it prints the rubric
|
|
row, the `next-cost` value, a pasteable startup command and the one-row-cheaper
|
|
fallback. Escalation is asymmetric - any single trait escalates, the cheapest
|
|
row needs all four at the cheap end. The row table is a closed set of six, so a
|
|
seventh spelling cannot enter circulation, and selftest section 6 runs the
|
|
round trip (route emits, board parses) inside one repo instead of across two.
|
|
|
|
The table is the operator's global model rubric, moved here as the single
|
|
copy: two copies of one policy drift, and a table in a prose file cannot be
|
|
tested. Scoring is judgement and stays in the skill; turning scores into a row
|
|
is a lookup and costs no model calls.
|
|
|
|
### 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.
|