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

@ -5,6 +5,58 @@ 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.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