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

@ -1250,6 +1250,144 @@ check "F14: control - a correct call over a real root still exits 0" $?
/bin/rm -rf "$F5DIR" 2>/dev/null
# 38. The launchd templates. A wrong program path in a plist is the one defect
# in this repo that NOTHING catches at runtime: the agent simply never runs, in
# silence, and `launchctl list` confirms only that it is LOADED, never that it
# does anything right. There is no output to be wrong, no exit status to read -
# the failure looks exactly like a quiet machine. So the path is asserted here,
# statically, against the file it actually names.
#
# This section covers EVERY plist in launchd/, not only the sweep agent that
# 0.33.0 adds, and that is deliberate: the plist grammar has one reader here
# rather than one per agent. Two half-checks in two suites would drift, which is
# the two-copies-of-one-policy defect this repo names repeatedly. board-selftest
# still owns brief-nightly.sh's BEHAVIOUR (section 9); this owns the templates.
#
# Deliberately NOT checked here: XML well-formedness. `plutil` is not coreutils,
# and malformed XML is the one plist defect that already fails LOUDLY - launchctl
# load rejects it on the spot. This section is for the defect that does not: a
# path that is merely wrong. Both files were linted by hand at 0.33.0.
LAUNCHD="$DIR/../launchd"
REPOROOT="$(cd "$DIR/.." && pwd)"
# One reader, shared by the real files below AND by the control at the end. A
# control that runs different code from the case it certifies proves nothing
# about it. The program path is the <string> carrying the checkout placeholder;
# the install instructions in the header comment name __CHECKOUT__ too, which is
# why <string> has to match first.
plist_program_path() {
grep '<string>' "$1" 2>/dev/null | grep '__CHECKOUT__' | head -1 \
| sed -e 's/.*<string>//' -e 's|</string>.*||'
}
plist_label() {
grep -A1 '<key>Label</key>' "$1" 2>/dev/null | grep '<string>' | head -1 \
| sed -e 's/.*<string>//' -e 's|</string>.*||'
}
plist_hour() {
grep -A1 '<key>Hour</key>' "$1" 2>/dev/null | grep '<integer>' | head -1 \
| sed -e 's/.*<integer>//' -e 's|</integer>.*||'
}
plist_n=0
for p in "$LAUNCHD"/*.plist; do
[ -e "$p" ] || continue
plist_n=$((plist_n + 1))
pb="$(basename "$p")"
# launchctl addresses an agent by Label, the operator by filename. When they
# disagree, load/start/unload silently act on a different agent than the one
# being edited.
lbl="$(plist_label "$p")"
[ -n "$lbl" ] && [ "$lbl" = "${pb%.plist}" ]
check "launchd $pb: Label matches the filename" $?
# The check this section exists for.
prog="$(plist_program_path "$p")"
[ -n "$prog" ] && [ -s "$REPOROOT/${prog#__CHECKOUT__/}" ]
check "launchd $pb: ProgramArguments names a script that exists here" $?
# The repo is mirrored publicly and a plist is the one file that would
# otherwise carry an absolute home path. It stays a TEMPLATE.
grep -q '__HOME__' "$p"
check "launchd $pb: log paths stay a __HOME__ placeholder (public mirror)" $?
# The cache path is version-pinned, so an agent pointing there breaks silently
# on the next bump - and a second copy of these scripts on disk is the exact
# defect class that produced the 0.12.1 stale-fallback bug. Asserted on the
# EXTRACTED PATH, never on the whole file: 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. Same shape as the board line, where
# prose saying status=done must never trigger the done-guard.
case "$prog" in *plugins/cache*) false ;; *) true ;; esac
check "launchd $pb: the program path is not the version-pinned plugin cache" $?
done
[ "$plist_n" -ge 2 ]
check "launchd: both agent templates are present (brief + sweep)" $?
# The grace window is the OPERATOR's policy constant (14 days, decided
# 2026-09-03), not the script's default wearing a schedule. An agent quietly
# running a different window would close a different population every night with
# nothing reporting the change.
SWEEPPL="$LAUNCHD/com.ktg.repo-mailbox-sweep.plist"
grep -q '<string>--write</string>' "$SWEEPPL" 2>/dev/null \
&& grep -q '<string>--days</string>' "$SWEEPPL" 2>/dev/null \
&& grep -q '<string>14</string>' "$SWEEPPL" 2>/dev/null
check "launchd sweep: the agent runs --write --days 14, the authorized window" $?
# The briefing READS the mailbox the sweep MUTATES, so the two must not fire in
# the same minute: a briefing rendered mid-sweep counts messages that are being
# closed underneath it.
hb="$(plist_hour "$LAUNCHD/com.ktg.repo-mailbox-brief.plist")"
hs="$(plist_hour "$SWEEPPL")"
[ -n "$hb" ] && [ -n "$hs" ] && [ "$hb" != "$hs" ]
check "launchd: the two agents run at different hours (the brief reads what the sweep mutates)" $?
# Mandatory controls. A path check with no negative case is a check that cannot
# go red, which this repo has shipped once already (section 11's vacuous first
# cut) and will not ship again.
BADPL="$CLAUDE_COORD_DIR/bad.plist"
{
echo '<plist version="1.0"><dict>'
echo '<key>Label</key>'
echo '<string>com.ktg.repo-mailbox-bad</string>'
echo '<key>ProgramArguments</key>'
echo '<array>'
echo '<string>/bin/bash</string>'
echo '<string>__CHECKOUT__/scripts/no-such-script.sh</string>'
echo '</array>'
echo '</dict></plist>'
} > "$BADPL"
[ "$(plist_program_path "$BADPL")" = "__CHECKOUT__/scripts/no-such-script.sh" ]
check "launchd control: the extraction really does read a program path" $?
badprog="$(plist_program_path "$BADPL")"
[ -s "$REPOROOT/${badprog#__CHECKOUT__/}" ]; [ $? -ne 0 ]
check "launchd control: a plist naming a missing script is judged missing" $?
[ "$(plist_label "$BADPL")" = "bad" ]; [ $? -ne 0 ]
check "launchd control: a Label disagreeing with the filename is caught" $?
# The cache check needs its own control, because narrowing it from the whole file
# to the extracted path is exactly the kind of narrowing that can quietly stop
# catching anything.
CACHEPL="$CLAUDE_COORD_DIR/cache.plist"
{
echo '<plist version="1.0"><dict>'
echo '<key>ProgramArguments</key>'
echo '<array>'
echo '<string>/bin/bash</string>'
echo '<string>__CHECKOUT__/.claude/plugins/cache/repo-mailbox/0.33.0/scripts/coord-sweep.sh</string>'
echo '</array>'
echo '</dict></plist>'
} > "$CACHEPL"
cprog="$(plist_program_path "$CACHEPL")"
case "$cprog" in *plugins/cache*) true ;; *) false ;; esac
check "launchd control: a program path INSIDE the plugin cache is caught" $?
/bin/rm -f "$BADPL" "$CACHEPL" 2>/dev/null
echo "----"
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]