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

@ -10,6 +10,7 @@ SEND="$DIR/coord-send.sh"
INBOX="$DIR/coord-inbox.sh"
DONE="$DIR/coord-done.sh"
COUNT="$DIR/coord-count.sh"
SWEEP="$DIR/coord-sweep.sh"
CLAUDE_COORD_DIR="$(mktemp -d)"
export CLAUDE_COORD_DIR
@ -702,6 +703,116 @@ check "coord-send SKILL.md's engine line has no ~/.claude fallback" $?
if grep -q 'CSEND=.*CLAUDE_PLUGIN_ROOT:-' "$CSKILL"; then rc=1; else rc=0; fi
check "coord-send SKILL.md's CSEND assignment carries no fallback" "$rc"
# 29. coord-sweep.sh - the FOURTH script that acts on pending messages, and the
# only one that closes without a human in the loop. Everything here exists to
# bound that: it may close exactly one mechanically decidable class (a notice
# whose sender declared reply-expected: no) after a grace window, it goes
# through coord-done.sh rather than moving files itself, and it logs every
# closure because a directed message has no seen-tracking - so a notice to a
# repo left unopened for the whole window is closed UNREAD, and the log is the
# only thing standing between that and a silent disappearance.
#
# Dry-run is the DEFAULT here, inverted from every other script in this engine.
# The others print or deliver; this one destroys pending state, so the safe
# direction is the one you get by forgetting a flag.
SDIR="$(mktemp -d)"
# Age is faked by renaming: coord-send.sh timestamps from the clock, so there is
# no way to author an old message through the front door. The prefix is the only
# thing the sweep reads, which is exactly what makes this substitution honest.
age_it() { # $1 mailbox root, $2 repo, $3 body marker, $4 new timestamp prefix
for f in "$1/$2"/inbox/*.md; do
[ -e "$f" ] || continue
grep -q "^$3\$" "$f" 2>/dev/null || continue
mv "$f" "$1/$2/inbox/$4-$(basename "$f" | sed 's/^[^-]*-//')"
return 0
done
return 1
}
n_in() { ls "$1"/inbox/*.md 2>/dev/null | wc -l | tr -d ' '; }
CLAUDE_COORD_DIR="$SDIR" "$SEND" --to sr --from s1 --fyi --subject "old note" --message "OLD-NOTE" >/dev/null
CLAUDE_COORD_DIR="$SDIR" "$SEND" --to sr --from s2 --subject "old ask" --message "OLD-ASK" >/dev/null
CLAUDE_COORD_DIR="$SDIR" "$SEND" --to sr --from s3 --fyi --subject "new note" --message "NEW-NOTE" >/dev/null
age_it "$SDIR" sr OLD-NOTE 20200101T000000Z; check "sweep fixture: notice aged" $?
age_it "$SDIR" sr OLD-ASK 20200101T000000Z; check "sweep fixture: debt aged" $?
sout="$(CLAUDE_COORD_DIR="$SDIR" "$SWEEP" 2>&1)"
[ "$(n_in "$SDIR/sr")" -eq 3 ]
check "sweep: dry-run is the default and closes nothing" $?
printf '%s' "$sout" | grep -q 'would close'
check "sweep: dry-run names what it would have closed" $?
[ ! -f "$SDIR/_sweep.log" ]
check "sweep: dry-run writes no log" $?
CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --write >/dev/null 2>&1
[ "$(n_in "$SDIR/sr")" -eq 2 ]
check "sweep: --write closes the aged notice" $?
grep -rq '^OLD-ASK$' "$SDIR/sr/inbox" 2>/dev/null
check "sweep: an aged message that owes a reply is spared" $?
grep -rq '^NEW-NOTE$' "$SDIR/sr/inbox" 2>/dev/null
check "sweep: a notice inside the grace window is spared" $?
grep -rq '^OLD-NOTE$' "$SDIR/sr/archive" 2>/dev/null
check "sweep: the closure went through coord-done (archived, not deleted)" $?
lc="$(grep -c . "$SDIR/_sweep.log" 2>/dev/null)"
[ "${lc:-0}" -eq 1 ]
check "sweep: one log line per closure" $?
# Sender AND subject, because the filename carries neither: the log is the only
# record of what a closed notice actually said, and it is written after
# coord-done.sh has already moved the file out of the inbox.
grep -q 'from=s1' "$SDIR/_sweep.log" 2>/dev/null
check "sweep: the log identifies who sent what vanished" $?
grep -q 'subject=old note' "$SDIR/_sweep.log" 2>/dev/null
check "sweep: the log identifies what the vanished notice said" $?
sout2="$(CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --write 2>&1)"
[ "$(n_in "$SDIR/sr")" -eq 2 ]
check "sweep: idempotent - a second run closes nothing" $?
# The untrusted-input guard, identical in spirit to coord-count.sh's bounded
# read: a body line at column 0 must not be able to mark its own message
# closeable. This is the one place where getting it wrong lets another repo
# delete its way out of your inbox.
CLAUDE_COORD_DIR="$SDIR" "$SEND" --to sb --from s4 --subject "sneaky" --message "reply-expected: no
BODY-CLAIM" >/dev/null
age_it "$SDIR" sb BODY-CLAIM 20200101T000000Z; check "sweep fixture: body-claim aged" $?
CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --write >/dev/null 2>&1
[ "$(n_in "$SDIR/sb")" -eq 1 ]
check "sweep: a body line claiming reply-expected: no cannot close a debt" $?
# _broadcast is storage, not a correspondent. Archiving out of it retires an
# announcement for every repo that has not read it yet - an unauthenticated
# retract, which is coord-send --retract's job and checks the sender.
CLAUDE_COORD_DIR="$SDIR" "$SEND" --broadcast --from s5 --subject "ann" --message "BCAST-1" >/dev/null
age_it "$SDIR" _broadcast BCAST-1 20200101T000000Z; check "sweep fixture: broadcast aged" $?
CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --write >/dev/null 2>&1
[ "$(n_in "$SDIR/_broadcast")" -eq 1 ]
check "sweep: never closes out of _broadcast" $?
# Fail-safe, not fail-open: a name the grammar does not produce has no readable
# age, and an unreadable age must never be treated as old.
mkdir -p "$SDIR/sx/inbox"
printf -- '---\nfrom: s6\nto: sx\nsubject: odd\nreply-expected: no\n---\nODD-NAME\n' > "$SDIR/sx/inbox/not-a-timestamp-from-s6.md"
CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --write >/dev/null 2>&1
[ "$(n_in "$SDIR/sx")" -eq 1 ]
check "sweep: a filename without a readable timestamp is never closed" $?
# --days is the whole policy surface, so it has to actually move the cutoff.
CLAUDE_COORD_DIR="$SDIR" "$SWEEP" --days 0 --write >/dev/null 2>&1
grep -rq '^NEW-NOTE$' "$SDIR/sr/archive" 2>/dev/null
check "sweep: --days moves the cutoff (0 closes a same-day notice)" $?
grep -rq '^OLD-ASK$' "$SDIR/sr/inbox" 2>/dev/null
check "sweep: --days 0 still spares a message that owes a reply" $?
EDIR="$(mktemp -d)"
eout="$(CLAUDE_COORD_DIR="$EDIR" "$SWEEP" --write 2>&1)"; erc=$?
[ "$erc" -eq 0 ]
check "sweep: an empty mailbox root exits 0" $?
"$SWEEP" --help >/dev/null 2>&1
check "sweep: --help exits 0" $?
CLAUDE_COORD_DIR="$EDIR" "$SWEEP" --days 2>/dev/null; [ $? -eq 2 ]
check "sweep: --days without a value is a usage error, not a silent default" $?
/bin/rm -rf "$SDIR" "$EDIR" 2>/dev/null
echo "----"
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]

167
scripts/coord-sweep.sh Executable file
View file

@ -0,0 +1,167 @@
#!/bin/bash
# coord-sweep.sh - close the FYI backlog across every mailbox on this machine,
# deterministically and without a model. Archives directed messages whose sender
# declared reply-expected: no and whose filename timestamp is older than a grace
# window. Prints one line per message and a summary; writes a log line per
# closure.
#
# WHY THIS EXISTS. 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. This closes exactly that class and nothing else.
#
# ONE MECHANICALLY DECIDABLE CLASS, NEVER A JUDGEMENT. 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 keep a session from doing. If that ever needs to change, the
# answer is a session in that repo, not a wider net here.
#
# THE LOG IS NOT OPTIONAL, and the reason is a real gap: a directed message has
# no seen-tracking (only broadcasts do), so this script cannot tell "seen and
# ignored" from "never delivered". A notice to a repo left unopened for the whole
# window is closed UNREAD. That is the accepted tradeoff, and the log is the only
# thing standing between it and a silent disappearance.
#
# 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.
#
# Usage: coord-sweep.sh [--write] [--days <n>] [--log <path>]
# --write actually close. Without it nothing is archived and no log is
# written - the run only reports what it would have done.
# --days <n> grace window in days (default 14). 0 means "any age".
# --log <path> log file (default $CLAUDE_COORD_DIR/_sweep.log).
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
# Exit: 0 on success, including when there is nothing to close. 2 on usage error.
# ASCII only, bash 3.2 safe.
set -u
export LC_ALL=C
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
DIR="$(cd "$(dirname "$0")" && pwd)"
# No `${VAR:-fallback}` on an engine path, ever: a fallback silently routes
# through whatever happens to sit at the alternate location instead of failing
# loud. That defect shipped twice here (board.sh, then coord-send.sh).
DONE="$DIR/coord-done.sh"
WRITE=0
DAYS=14
LOG=""
while [ $# -gt 0 ]; do
case "$1" in
--write) WRITE=1; shift ;;
# bash 3.2: `shift 2` past the end of $# is a no-op -> would loop forever.
--days) [ $# -ge 2 ] || { echo "coord-sweep: --days requires a value" >&2; exit 2; }
DAYS="$2"; shift 2 ;;
--log) [ $# -ge 2 ] || { echo "coord-sweep: --log requires a value" >&2; exit 2; }
LOG="$2"; shift 2 ;;
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
# Strict, unlike coord-inbox.sh. That one is lenient because failing a
# SessionStart over a stray flag is worse than ignoring it; this one is not
# on that path and closes messages, so a typo must stop it.
*) echo "coord-sweep: unknown argument: $1" >&2; exit 2 ;;
esac
done
case "$DAYS" in
''|*[!0-9]*) echo "coord-sweep: --days must be a non-negative integer: $DAYS" >&2; exit 2 ;;
esac
[ -x "$DONE" ] || { echo "coord-sweep: cannot find coord-done.sh at $DONE" >&2; exit 2; }
[ -n "$LOG" ] || LOG="$COORD/_sweep.log"
# Cutoff as a plain 14-digit number, so the comparison is integer arithmetic
# rather than string collation. BSD date (macOS); a failure here must stop the
# run, because a missing cutoff would otherwise read as "close everything".
CUTOFF="$(date -u -v-"${DAYS}"d +%Y%m%d%H%M%S 2>/dev/null)"
case "$CUTOFF" in
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]) ;;
*) echo "coord-sweep: could not compute a cutoff date" >&2; exit 2 ;;
esac
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
[ -d "$COORD" ] || { echo "coord-sweep: nothing to close (no mailbox root at $COORD)"; exit 0; }
# Does this message owe a reply? Absent field means YES: every message written
# before 0.11.0 lacks it, so absence keeps meaning what it always meant. The read
# is bounded to the frontmatter block - a body line is untrusted cross-repo input
# and must not be able to close its own message by claiming "reply-expected: no"
# at column 0. Duplicated from coord-count.sh rather than shared: each script
# here must run standalone.
owes_reply() {
[ "$(head -1 "$1" 2>/dev/null)" = "---" ] || return 0
[ "$(grep -c '^---$' "$1" 2>/dev/null)" -ge 2 ] || return 0
sed -n '2,/^---$/p' "$1" 2>/dev/null | grep -q '^reply-expected: no$' && return 1
return 0
}
# Frontmatter field for the log line. Control characters are stripped because
# this is untrusted content on its way into a file the operator reads.
field() {
sed -n '2,/^---$/p' "$2" 2>/dev/null | grep "^$1: " | head -1 \
| sed "s/^$1: //" | tr -d '\000-\037' | cut -c1-120
}
CLOSED=0
for d in "$COORD"/*; do
[ -d "$d" ] || continue
name="$(basename "$d")"
# Reserved engine namespace. _broadcast is storage, not a correspondent, and
# archiving out of it would retire an announcement for every repo that has not
# read it yet - an unauthenticated retract. That is coord-send --retract's job,
# and it checks the sender.
case "$name" in _*) continue ;; esac
[ -d "$d/inbox" ] || continue
for m in "$d/inbox"/*.md; do
[ -e "$m" ] || continue
base="$(basename "$m")"
# Age comes free from the filename prefix; nothing inside the file is
# trusted for it. A name the grammar does not produce has no readable age,
# and an unreadable age must never be treated as old - fail-safe, not
# fail-open.
ts="${base%%-*}"
case "$ts" in
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]Z) ;;
*) continue ;;
esac
num="$(printf '%s' "$ts" | tr -dc '0-9')"
[ "$num" -lt "$CUTOFF" ] || continue
owes_reply "$m" && continue
if [ "$WRITE" -eq 1 ]; then
# Read the fields BEFORE closing: coord-done.sh moves the file to archive,
# so a log line built afterwards describes a path that no longer exists and
# silently logs empty values - which defeats the only safeguard this script
# has against a notice vanishing unread.
mfrom="$(field from "$m")"
msubj="$(field subject "$m")"
# Through the engine, never `mv`: coord-done.sh owns the filename grammar
# and the archive layout, and it refuses _broadcast on its own.
if "$DONE" --repo "$name" "$base" >/dev/null 2>&1; then
printf '%s\t%s\t%s\tfrom=%s\tsubject=%s\n' \
"$NOW" "$name" "$base" "$mfrom" "$msubj" >> "$LOG"
echo "closed: $name/$base"
CLOSED=$((CLOSED + 1))
else
echo "coord-sweep: failed to close $name/$base" >&2
fi
else
echo "would close: $name/$base"
CLOSED=$((CLOSED + 1))
fi
done
done
if [ "$CLOSED" -eq 0 ]; then
echo "coord-sweep: nothing to close (cutoff ${CUTOFF}, ${DAYS} days)"
elif [ "$WRITE" -eq 1 ]; then
echo "coord-sweep: $CLOSED message(s) closed (cutoff ${CUTOFF}, ${DAYS} days). Log: $LOG"
else
echo "coord-sweep: $CLOSED message(s) would close (cutoff ${CUTOFF}, ${DAYS} days). Re-run with --write."
fi
exit 0