feat(sweep): close the aged FYI backlog without a human in the loop

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 meant to remove, and the
pending count -- the operator's only signal -- drowns in messages that
were never going to be acted on: 9 of 22 pending messages across 12
mailboxes were pure notices when this was written.

coord-sweep.sh closes exactly one mechanically decidable class:
reply-expected: no, older than a grace window (default 14 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.

Four properties are load-bearing, not incidental:

- Dry-run is the default, inverted from the rest of the engine. The
  others print or deliver; this one destroys pending state, so the safe
  direction has to be what you get by forgetting a flag.
- Closing goes through coord-done.sh --repo, never mv, so the archive
  layout and the _broadcast refusal stay in one place.
- Age is read from the filename prefix, never the file. An unreadable
  age is never treated as old: fail-safe, not fail-open.
- Every closure is logged with sender and subject. Directed messages
  have no seen-tracking, so the sweep cannot tell "seen and ignored"
  from "never delivered" -- a notice can be closed unread, and the log
  is the only thing standing between that and silent data loss.

The reply-expected read is bounded to the frontmatter block, matching
coord-count.sh: a body line claiming it at column 0 is untrusted
cross-repo input and must not close its own message.

No scheduler, no launchd unit, no skill front door -- the script does
nothing until invoked.

Selftest 159 -> 182. The log check caught a real defect during
development: the first implementation read from/subject AFTER
coord-done.sh had moved the file, logging empty values and quietly
defeating the only safeguard the design has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uwcak9j4m9JijDKeFmptww
This commit is contained in:
Kjell Tore Guttormsen 2026-08-01 22:00:42 +02:00
commit 459c9feec0
10 changed files with 362 additions and 11 deletions

View file

@ -13,9 +13,24 @@ 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, `coord-count.sh` counts without delivering.
`coord-done.sh` archives, `coord-count.sh` counts without delivering,
`coord-sweep.sh` closes the aged FYI backlog machine-wide.
Everything is pinned by `coord-selftest.sh`
(159 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
(182 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
**`coord-sweep.sh` is the only path that closes a message with no human in
the loop, and every constraint on it follows from that.** It may close exactly
one mechanically decidable class - `reply-expected: no`, older than the grace
window - because a message that owes a reply can only be answered by a session
in the repo that owes it. Dry-run is the default, inverted from the rest of the
engine, since this is the one script that destroys pending state. It closes
through `coord-done.sh --repo` rather than moving files, so the archive layout
and the `_broadcast` refusal stay in one place. And it logs every closure with
sender and subject, because directed messages have no seen-tracking: the sweep
genuinely cannot tell "seen and ignored" from "never delivered", so a notice
can be closed unread and the log is the only record that it existed. Widening
the class, defaulting to `--write`, or dropping the log each independently
turn this from a bounded cleanup into silent data loss.
**`coord-count.sh` prints TWO integers per mailbox** (`<name>\t<pending>\t<debt>`),
and the first must stay pending: `board.sh` counts the same inbox files
@ -153,7 +168,7 @@ obligations in another repo.
- 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 (159/159),
`bash scripts/coord-selftest.sh` must exit 0 (182/182),
`bash scripts/board-selftest.sh` must exit 0 (36/36) and
`bash scripts/route-selftest.sh` must exit 0 (73/73).
- English for all code, docs, and commit messages (public repo). Norwegian
@ -168,6 +183,9 @@ obligations in another repo.
- Board smoke test: `bash scripts/board.sh` (read-only, ~3s over the real tree)
- Route smoke test: `bash scripts/route.sh --path known --verification strong
--reversibility cheap --scope local --rationale x` (writes nothing, instant)
- Sweep smoke test: `bash scripts/coord-sweep.sh` (dry-run is the default, so
this writes nothing; never add `--write` to a smoke test against the real
mailbox)
## Release