feat(sweep): schedule the FYI sweep - invocation was the gap, not the mechanism

coord-sweep.sh shipped in 0.10.0 and had never run once against the real
mailbox. Measured 2026-09-03 with a denominator (docs/2026-09-03-coordination-
debt-measurement.md): 55 mailbox directories, 52 with an inbox/, 27 pending
directed messages - 23 of them pure notices, re-injected at every session start
in repos nobody had opened. The script was correct and unreachable.

WP5 (order 20260902T113745Z-1254925290) asked for a mechanism and named two
candidates. The measurement chose neither, and the first session returned the
order saying so: bulk-ack for pure notices was already built - it is this
script - so the second candidate would have been two copies of one policy, and
the broadcast class converges on its own (reading sets seen), with 34 of 263
unread pairs belonging to two mailboxes no session can hold, so a TTL would
have closed those rather than reduced them. The operator then chose the window
and authorized the schedule.

launchd/com.ktg.repo-mailbox-sweep.plist runs --write --days 14 daily at 05:30.
That is the entire behavioural change. The window is written out in the plist
rather than inherited from the script's default: it is a policy constant chosen
on a measured distribution (30d -> 0 messages, 14d -> 7, 7d -> 13), so a later
change to DAYS=14 must not silently change what an unattended job closes across
51 other repos. It runs BEFORE the 06:00 briefing agent, which scans the same
mailbox this mutates, so the morning briefing reports the debt that remains
rather than counting notices being closed underneath it.

coord-selftest.sh section 38 pins the launchd templates (242 -> 257 checks). A
wrong program path is the one defect here that nothing catches at runtime: the
agent loads cleanly and then silently never runs, with no output to be wrong
and no exit status to read. launchctl list proves an agent is LOADED, never
that it is RIGHT. The section covers every plist in launchd/, not only the new
one - the plist grammar gets one reader rather than one per agent - while
board-selftest.sh section 9 keeps owning brief-nightly.sh's behaviour. Each
plist must name a script that exists here, carry a Label matching its filename,
keep its __CHECKOUT__/__HOME__ placeholders (public mirror), and never point
into the version-pinned plugin cache.

The cache assertion runs on the extracted path, never the whole file - caught
by the check itself on its first run: the brief plist's header explains in
prose why it does not point at the cache, and a file-wide grep read that
explanation as the defect it warns about, the same shape as prose saying
status=done triggering the board's done-guard. Four controls present; mutation-
verified against the real file, where a one-letter typo (coord-sweeep.sh) turns
exactly that check red. XML well-formedness is deliberately not checked:
plutil is not coreutils, and malformed XML already fails loudly at launchctl
load - the opposite of the silent failure this section exists for.

Also fixes the README selftest-checks badge, stale at 529 since 0.25.0; the
real total is 868 (257 + 368 + 73 + 116 + 54).

Suites: coord 257, board 368, route 73, orders 116, guard 54. npm test 11/11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-04 08:12:32 +02:00
commit 2f8ceb3f97
12 changed files with 451 additions and 11 deletions

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
Daily FYI sweep. Closes directed messages whose sender declared
reply-expected: no and whose filename timestamp is older than 14 days, across
every mailbox on this machine, through coord-done.sh.
INVOCATION WAS THE GAP, NOT THE MECHANISM. coord-sweep.sh shipped in 0.10.0
and had never run once against the real mailbox - measured 2026-09-03: 27
pending directed messages, 23 of them pure notices nobody was ever going to
act on, re-injected at every session start in repos nobody had opened. The
script was correct and unreachable. This file is the whole fix; no new
mechanism was built, and building a second one would have been two copies of
a policy that already existed.
THE 14-DAY WINDOW IS THE OPERATOR'S CONSTANT, NOT A DEFAULT WEARING A
SCHEDULE (decided 2026-09-03, on a measured distribution: 30d -> 0 messages,
14d -> 7, 7d -> 13). It is written out explicitly here rather than left to
coord-sweep.sh's own default, so that changing the script's default can never
silently change what this agent closes every night.
WHAT IT CAN NEVER DO. A message that owes a reply is untouched at any age -
the script's own rule, not this file's. This agent only supplies the
invocation; every bound on what gets closed lives in coord-sweep.sh, and the
closure log ($CLAUDE_COORD_DIR/_sweep.log, NOT the launchd log below) is the
only record that a notice closed unread ever existed.
THE TWO AGENTS MUST NEVER SHARE AN HOUR. com.ktg.repo-mailbox-brief renders
at 06:00 from a scan of the same mailbox this mutates, so a briefing rendered
mid-sweep reads a mailbox changing underneath it. This runs at 05:30, clear of
it; coord-selftest.sh section 38 asserts the two hours differ.
What it does NOT change is the briefing's DEBT figure. Since 0.22.0 that is
computed from coord-count.sh's `owed` column, and this sweep closes only
messages that owe nothing - so the debt listing is identical before and after.
What moves is the raw pending count (the table's INN column, the FYI-only
naming in --brief, and the volume every repo gets injected at session start).
Claiming the briefing reports "the debt that remains" because of this agent
would be an overclaim; the ordering exists for the read/write overlap alone.
ZERO MODEL CALLS, same as the briefing and for the same reason: the operator
authenticates by subscription, so a headless `claude -p` job would draw from
the same quota pool as interactive work. This runs one shell script.
PATH: every binary this touches (bash, date, grep, sed, basename, tr, cut)
lives in /usr/bin or /bin, so launchd's minimal default PATH is sufficient
and no EnvironmentVariables block is needed.
The program path points at the SOURCE REPO, deliberately. The alternative is
version-pinned (~/.claude/plugins/cache/.../repo-mailbox/<version>/...), so
an agent pointing there would break silently on the next version bump - and a
second copy of these scripts on disk is the exact defect class that produced
the 0.12.1 stale-fallback bug.
This file is a TEMPLATE. It carries no absolute home path on purpose: the
repo is mirrored publicly, and a plist is the one file here that would need
one. Substitute both placeholders at install time.
Install: sed -e "s|__CHECKOUT__|$PWD|g" -e "s|__HOME__|$HOME|g" \
launchd/com.ktg.repo-mailbox-sweep.plist \
> ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
launchctl load ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
Verify: launchctl list | grep com.ktg.repo-mailbox-sweep # loaded only
launchctl start com.ktg.repo-mailbox-sweep # proves the path
tail ~/Library/Logs/repo-mailbox-sweep.log # the actual proof
Remove: launchctl unload ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
`launchctl list` proves the agent is LOADED, never that it does anything
right: a wrong program path produces a loaded agent that silently never runs.
Only `launchctl start` plus a line in the log below proves the path resolves.
-->
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.ktg.repo-mailbox-sweep</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>__CHECKOUT__/scripts/coord-sweep.sh</string>
<string>--write</string>
<string>--days</string>
<string>14</string>
</array>
<key>StandardErrorPath</key>
<string>__HOME__/Library/Logs/repo-mailbox-sweep.log</string>
<key>StandardOutPath</key>
<string>__HOME__/Library/Logs/repo-mailbox-sweep.log</string>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>5</integer>
<key>Minute</key>
<integer>30</integer>
</dict>
</dict>
</plist>