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

@ -16,7 +16,7 @@ marketplace plugin. Three components, one boundary:
`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`
(242 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
(257 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
**`ktg-plugin-marketplace` is a RETIRED `--to` address (operator decision
2026-08-15), rejected rather than redirected.** It is a polyrepo directory,
@ -94,6 +94,62 @@ marketplace plugin. Three components, one boundary:
the class, defaulting to `--write`, or dropping the log each independently
turn this from a bounded cleanup into silent data loss.
**Order 20260902T113745Z-1254925290 (WP5, 2026-09-02, amended by the operator
2026-09-03): the sweep was CORRECT AND UNREACHABLE, and the fix is a schedule,
not a mechanism.** It 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, of which **23 were pure
notices** being re-injected at every session start in repos nobody had opened.
The order asked for a mechanism and named two candidates; the measurement
chose neither, and the session returned it saying so. Bulk-ack for pure
notices *was already built* - this script - so building the order's candidate
B 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, not
reduced them. `launchd/com.ktg.repo-mailbox-sweep.plist` is the entire
0.33.0 change. **Invocation was the gap.**
**The 14-day window is written OUT in the plist, never left to the script's
default.** It is a policy constant the operator chose on a measured
distribution (30d -> 0 messages, 14d -> 7, 7d -> 13), of the same class as the
STATE.md line limit - so a later change to `DAYS=14` in the script must not
silently change what an unattended nightly job closes across every other
mailbox on this machine. (A hard count belongs in the dated measurement doc,
not here: it was 52 with an `inbox/` on 2026-09-03 and 53 a day later.)
The agent runs at **05:30**, clear of the 06:00 briefing agent, because the
briefing scans the same mailbox this mutates; selftest section 38 asserts the
two hours differ. It does **not** change the briefing's DEBT figure - since
0.22.0 that is read from `coord-count.sh`'s `owed` column, and this sweep
closes only messages that owe nothing, so the debt listing is byte-identical
before and after. What moves is the raw pending count (the table's `INN`
column, `--brief`'s FYI-only naming, and the volume injected at every session
start). The first cut of this paragraph claimed the sweep made the briefing
report "the debt that remains" - an overclaim, caught in review, and recorded
here because it is the kind of plausible sentence that survives a reread.
**`launchctl list` proves an agent is LOADED, never that it is RIGHT, and a
wrong program path is the one defect in this repo that nothing catches at
runtime** - the agent simply never runs, in silence, with no output to be
wrong and no exit status to read. Two separate things close it, and neither
substitutes for the other: `launchctl start <label>` plus a line appearing in
the launchd log is the only *runtime* proof the path resolves, and selftest
**section 38** asserts *statically*, for every plist in `launchd/`, that the
path names a file that exists here, that `Label` matches the filename, that
the `__CHECKOUT__`/`__HOME__` placeholders survive (the repo is mirrored
publicly), and that no agent points into the version-pinned plugin cache.
Section 38 covers **every** plist rather than only the new one, deliberately:
the plist grammar gets one reader here instead of one per agent, and
`board-selftest.sh` section 9 keeps owning `brief-nightly.sh`'s *behaviour*.
The cache assertion runs on the **extracted path, never 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, exactly
as prose saying `status=done` must never trigger the board's done-guard.
XML well-formedness is deliberately NOT checked: `plutil` is not coreutils,
and malformed XML already fails loudly at `launchctl load`, which is the
opposite of the failure this section exists for. Mutation-verified: a
one-letter typo (`coord-sweeep.sh`) turns exactly that check red.
**`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
itself, so a debt-only count would put two different numbers under one name.
@ -1294,7 +1350,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 (242/242),
`bash scripts/coord-selftest.sh` must exit 0 (257/257),
`bash scripts/board-selftest.sh` must exit 0 (368/368),
`bash scripts/route-selftest.sh` must exit 0 (73/73),
`bash scripts/orders-selftest.sh` must exit 0 (116/116) and
@ -1349,6 +1405,13 @@ obligations in another repo.
- 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)
- launchd template check: `plutil -lint launchd/*.plist` (XML only). The paths,
labels, placeholders and schedule are pinned by coord-selftest section 38 —
run that, not `launchctl list`, to find a broken plist. Installing an agent
is `sed` + `launchctl load` (see the header of either plist); proving it
works is `launchctl start <label>` plus a new line in
`~/Library/Logs/repo-mailbox-<brief|sweep>.log`, which is a DIFFERENT file
from the sweep's own closure log at `$CLAUDE_COORD_DIR/_sweep.log`
## Release