#!/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 ] [--log ] # --write actually close. Without it nothing is archived and no log is # written - the run only reports what it would have done. # --days grace window in days (default 14). 0 means "any age". # --log 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