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:
parent
5316688844
commit
2f8ceb3f97
12 changed files with 451 additions and 11 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-mailbox",
|
||||
"version": "0.32.1",
|
||||
"version": "0.33.0",
|
||||
"description": "Local mailbox for coordination between Claude Code sessions in different repositories. Directed messages and broadcasts as plain Markdown files on your own disk, injected as context at session start. Local, private, no network.",
|
||||
"author": {
|
||||
"name": "Kjell Tore Guttormsen"
|
||||
|
|
|
|||
78
CHANGELOG.md
78
CHANGELOG.md
|
|
@ -5,6 +5,84 @@ 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.33.0] - 2026-09-04
|
||||
|
||||
### Added
|
||||
|
||||
- **The FYI sweep is scheduled. 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 carrying an `inbox/`, 27 pending directed messages - of which **23 were
|
||||
pure notices**, re-injected at every session start in repos nobody had
|
||||
opened. The script was correct and unreachable.
|
||||
|
||||
The order behind this (WP5, `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 building the second candidate would
|
||||
have been two copies of one policy. The broadcast class converges on its own
|
||||
(reading sets `seen`), and 34 of 263 unread pairs belong 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; no new mechanism was built.
|
||||
|
||||
- **The 14-day window is written out in the plist, not inherited from the
|
||||
script's default.** It is a policy constant chosen on a measured distribution
|
||||
(30 days would have closed 0 messages, 14 closed 7, 7 would have closed 13),
|
||||
of the same class as the STATE.md line limit. Leaving it implicit would let a
|
||||
later change to `DAYS=14` silently change what an unattended job closes every
|
||||
night across every other mailbox on this machine.
|
||||
|
||||
It runs at 05:30, clear of the 06:00 briefing agent, because the briefing
|
||||
scans the same mailbox this mutates and the two must not overlap. It does
|
||||
**not** change what the briefing reports as debt: since 0.22.0 that figure
|
||||
comes from `coord-count.sh`'s `owed` column, and this sweep closes only
|
||||
messages that owe nothing. What moves is the raw pending count — the table's
|
||||
`INN` column and the volume injected at every session start. Selftest section
|
||||
38 asserts the two agents never share an hour.
|
||||
|
||||
- **coord-selftest.sh section 38 pins the launchd templates (+15 checks, 242 ->
|
||||
257).** A wrong program path in a plist is the one defect here that nothing
|
||||
catches at runtime: the agent loads cleanly and then silently never runs -
|
||||
no output to be wrong, no exit status to read, a failure indistinguishable
|
||||
from a quiet machine. `launchctl list` proves an agent is *loaded*, never
|
||||
that it is *right*.
|
||||
|
||||
The section covers **every** plist in `launchd/`, not just the new one: the
|
||||
plist grammar gets one reader here rather than one per agent, which is the
|
||||
two-copies-of-one-policy defect this repo has named repeatedly.
|
||||
`board-selftest.sh` section 9 still owns `brief-nightly.sh`'s behaviour. Each
|
||||
plist must name a script that exists in this checkout, carry a `Label`
|
||||
matching its filename, keep its `__CHECKOUT__`/`__HOME__` placeholders (the
|
||||
repo is mirrored publicly, and a plist is the one file that would otherwise
|
||||
need an absolute home path), and never point into the version-pinned plugin
|
||||
cache.
|
||||
|
||||
The cache assertion runs on the **extracted program 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. Same shape as the board
|
||||
line, where prose saying `status=done` must never trigger the done-guard.
|
||||
Four controls are mandatory here and present: the extractor really does read
|
||||
a path, a plist naming a missing script is judged missing, a `Label`
|
||||
disagreeing with its filename is caught, and a program path inside the plugin
|
||||
cache is caught. Mutation-verified against the real file: 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. Both files were
|
||||
linted by hand at 0.33.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The README's selftest-checks badge had read 529 since 0.25.0; the real
|
||||
total is 868.** A stale count in the one place a reader takes as the
|
||||
headline number, corrected while adding to it: 257 + 368 + 73 + 116 + 54.
|
||||
|
||||
## [0.32.1] - 2026-09-03
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
67
CLAUDE.md
67
CLAUDE.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
19
README.md
19
README.md
|
|
@ -10,11 +10,11 @@ Session A in repo X leaves a message for repo Y; the next session in repo Y gets
|
|||
|
||||
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
|
@ -130,9 +130,22 @@ The reply/resolve hints the hook injects (`-> reply: coord-send --reply-to …
|
|||
|
||||
**`coord-sweep.sh` is the only script that closes a message without a human in the loop**, and it is bounded to one mechanically decidable class: a directed message whose sender declared `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. Dry-run is the default, inverted from every other script here, because this is the one that destroys pending state. Every closure appends a line naming the sender and subject: a directed message has no seen-tracking, so the sweep cannot tell "seen and ignored" from "never delivered", and a notice to a repo left unopened for the whole window is closed *unread*. The log is what keeps that from being silent.
|
||||
|
||||
**Since 0.33.0 that sweep is scheduled, and the schedule is the whole feature — invocation was the gap, not the mechanism.** `coord-sweep.sh` shipped in 0.10.0 and had then never run once: measured 2026-09-03 across 52 mailboxes, 27 pending directed messages, of which 23 were pure notices being re-injected at every session start in repos nobody had opened. The script was correct and unreachable, so nothing new was built — a second mechanism would have been two copies of a policy that already existed. `launchd/com.ktg.repo-mailbox-sweep.plist` runs `--write --days 14` daily at 05:30, half an hour *before* the briefing agent — not because it changes what the briefing reports, but because the briefing scans the same mailbox this mutates, and the two must not overlap. The briefing's *debt* figure is in fact unaffected: since 0.22.0 it is computed from `coord-count.sh`'s `owed` column, and this sweep closes only messages that owe nothing. What the sweep moves is the raw pending count — the table's `INN` column, and the volume every repo gets injected at session start. The window is written out in the plist rather than left to the script's default: it is a policy constant decided on a measured distribution (30 days would have closed 0 messages, 14 closed 7, 7 would have closed 13), and changing a default must never silently change what an unattended job closes every night.
|
||||
|
||||
Both agents are **templates**, carrying `__CHECKOUT__`/`__HOME__` placeholders rather than absolute paths, because this repo is mirrored publicly. Substitute them at install time:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
**`launchctl list` proves an agent is *loaded*, never that it is *right*.** A plist naming a script that does not exist loads cleanly and then silently never runs — there is no output to be wrong and no exit status to read, so the failure looks exactly like a quiet machine. Two separate things close that: `launchctl start <label>` followed by a line appearing in `~/Library/Logs/repo-mailbox-sweep.log` is the only runtime proof the program path resolves, and `coord-selftest.sh` section 38 asserts statically, for *every* plist in `launchd/`, that the path it names is a file that exists in this repo, that the `Label` matches the filename, that the placeholders survive, and that no agent points into the version-pinned plugin cache. Note that the launchd log is not the closure log: `$CLAUDE_COORD_DIR/_sweep.log` is where the record of each closed notice lives.
|
||||
|
||||
**`board.sh --brief` renders the nightly briefing**, a second rendering of the scan the board already does rather than a second scan: the repos with an unhandled inbox, each one's next step *in full* (the 38-character cut belongs to the table column, not to the record), and the exact command to start a session there — derived by calling `route.sh` with that repo's own four traits, since `next-cost` alone cannot produce the advisor flag. A repo with no route line is told so rather than handed a guessed command. It also cross-checks itself against `coord-count.sh`, because the repo scan and the mailbox are different populations: a mailbox can carry a name no scan will ever produce, such as a declared non-git surface (`CLAUDE_COORD_REPO`) or a checkout outside the roots, and a briefing that only walked the scan would answer "who is waiting on you" with a number it quietly knew was short.
|
||||
|
||||
It makes **zero model calls**, which is the point rather than a detail. Under subscription auth a headless session draws from the same quota pool as interactive work, and `--max-budget-usd` is a runaway brake rather than a pre-flight gate — measured against 2.1.220, it aborts *after* the first turn, never before it. `board.sh --brief` writes nothing; the file write lives in `brief-nightly.sh`, which renders to a temp file and renames it into place, and refuses to replace a good briefing with an empty render. `launchd/` holds a sample agent that runs it nightly; it points at a checkout, never at the version-pinned plugin cache.
|
||||
It makes **zero model calls**, which is the point rather than a detail. Under subscription auth a headless session draws from the same quota pool as interactive work, and `--max-budget-usd` is a runaway brake rather than a pre-flight gate — measured against 2.1.220, it aborts *after* the first turn, never before it. `board.sh --brief` writes nothing; the file write lives in `brief-nightly.sh`, which renders to a temp file and renames it into place, and refuses to replace a good briefing with an empty render. `launchd/` holds a sample agent that runs it nightly at 06:00; like the sweep agent above it points at a checkout, never at the version-pinned plugin cache, and it is pinned by the same section 38 checks.
|
||||
|
||||
**`board.sh --voyage` reports the Voyage briefs in flight**, a sixth rendering of the same scan. `board` reads STATE lines, which say nothing about a brief, so a programme running Voyage across several repos had no shared surface: nobody could answer which briefs were running, in what phase, and who was waiting on whom. Detection is by **property, never by directory name** — a directory holding `brief.md` or `brief.md.draft` under any of the three planning locations the convention recognises (`.claude/projects/`, `docs/`, `features/<n>-<name>/`) — and it walks the **filesystem, never the git index**: a repo that gitignores `.claude/projects/` would otherwise report zero briefs while actually running one. The phase ladder measures *artifacts*, not sessions: a plan executed in a single session leaves no file behind, so `plan` is the last thing the filesystem can prove, and nothing here claims a session is alive. `brief_quality` is read out of the brief's frontmatter and an absent field reads `-`, never `complete`; a research directory that exists and holds nothing reads `0`, distinct from the `-` that means no research step was ever started. The table carries a matching `VOY` column beside `ORDRE` and `FLY` — the same class of durable filesystem fact, and never summed with them.
|
||||
|
||||
|
|
|
|||
|
|
@ -101,3 +101,52 @@ plugin's.
|
|||
The messages that owe a reply are untouched by any of this, at any age, with
|
||||
any flag. That is not a gap to close later - it is the rule that keeps a
|
||||
procedural duty from becoming a substantive one.
|
||||
|
||||
---
|
||||
|
||||
## Appendix: the before-state, captured 2026-09-04 08:0x UTC
|
||||
|
||||
The operator authorized the sweep on 2026-09-03 (14-day window, scheduled via
|
||||
launchd). This is `coord-count.sh` immediately before the authorized
|
||||
`--write --days 14` run, recorded here because a before-state stops existing
|
||||
the moment the write happens, and the order asks for debt before/after with a
|
||||
denominator.
|
||||
|
||||
mailbox pending owed origin-age
|
||||
app-creator 1 1 -
|
||||
claude-playlist-corpus 1 0 -
|
||||
graceful-handoff 3 0 -
|
||||
human-friendly-style 1 0 -
|
||||
ki-produktivitetsmodell 1 0 -
|
||||
llm-ingestion-guard 1 0 25
|
||||
llm-security 2 0 -
|
||||
mcp-servere 2 0 -
|
||||
okr 1 0 -
|
||||
org-ops 5 2 -
|
||||
portfolio-optimiser-commons 3 0 -
|
||||
repo-standard 1 1 -
|
||||
wiki-advise 2 0 -
|
||||
.claude 1 0 -
|
||||
.profile 1 0 -
|
||||
------------------------------------------------------
|
||||
15 mailboxes with pending mail 26 4
|
||||
|
||||
`$CLAUDE_COORD_DIR/_sweep.log` did not exist: the sweep had still never run.
|
||||
|
||||
**The control this file exists to make runnable:** after the write, the `owed`
|
||||
column must be BYTE-IDENTICAL (total 4), because the sweep spares every message
|
||||
that owes a reply at any age. A changed `owed` figure means the sweep closed
|
||||
something it must never touch, and is a defect, not a result.
|
||||
|
||||
**The dry-run said 11, not the 7 this document measured a day earlier, and the
|
||||
gap is entirely the moving cutoff.** Verified two ways rather than assumed.
|
||||
Yesterday's 14-day cutoff was ~20260820T184902; today's is 20260821T055442.
|
||||
Four notices timestamped 2026-08-20 between those two instants
|
||||
(`human-friendly-style` T210113Z, `ki-produktivitetsmodell` T205611Z, `org-ops`
|
||||
T210955Z and T211358Z) crossed the boundary in one calendar day: 7 + 4 = 11.
|
||||
Independently, `--days 15` today yields 6, and those 6 plus
|
||||
`mcp-servere/20260820T104644Z` - which sits between the 15-day cutoff and
|
||||
yesterday's 14-day one - reconstruct yesterday's 7 exactly. The engine is
|
||||
consistent; the 7 was a measurement of a moment, never a constant, and reading
|
||||
it as one would have been face 3 of the verification law pointed at our own
|
||||
report.
|
||||
|
|
|
|||
99
launchd/com.ktg.repo-mailbox-sweep.plist
Normal file
99
launchd/com.ktg.repo-mailbox-sweep.plist
Normal 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>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-mailbox",
|
||||
"version": "0.32.1",
|
||||
"version": "0.33.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -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 ]
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ description: >-
|
|||
user names no repo and no tool — choosing *between* repos is this skill. Not for
|
||||
"where were we" inside the current repo: that is this repo's own STATE.md,
|
||||
already injected at session start.
|
||||
version: "0.32.1"
|
||||
version: "0.33.0"
|
||||
---
|
||||
|
||||
# board — which repo deserves the next session
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ description: >-
|
|||
covers retiring a broadcast that has become wrong or obsolete: "retract that
|
||||
broadcast", "that announcement is outdated, pull it", "trekk tilbake kringkastingen",
|
||||
"den broadcasten er utdatert".
|
||||
version: "0.32.1"
|
||||
version: "0.33.0"
|
||||
---
|
||||
|
||||
# coord-send — natural-language front door for inter-repo messages
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ description: >-
|
|||
skill. Not for choosing WHICH repo deserves the next session (that is `board`),
|
||||
not for scoring model and effort alone (that is `route`), and not for sending a
|
||||
message to another repo (that is `coord-send`).
|
||||
version: "0.32.1"
|
||||
version: "0.33.0"
|
||||
---
|
||||
|
||||
# dispatch — hand a session a task it can actually start on
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ description: >-
|
|||
the operator names no model and no tool — choosing the model for the next
|
||||
session IS this skill. Not for choosing which REPO gets the next session:
|
||||
that is the `board` skill.
|
||||
version: "0.32.1"
|
||||
version: "0.33.0"
|
||||
---
|
||||
|
||||
# route — what the next session should run with
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue