repo-mailbox/scripts/coord-count.sh
Kjell Tore Guttormsen f425a11311 fix(coord-send): stop rejecting dot-prefixed repo names
coord-send.sh:154 guarded the --to target with a `.*` case arm, which
rejects every leading-dot name instead of just `.` and `..`. A
dot-prefixed name is a real repo identity (basename of a git toplevel
under a hidden directory, e.g. ~/.claude) and was reported unreachable
by morning-driver via coord message 20260809T103138Z. Narrowed the
guard to reject exactly `.` and `..`, matching the equivalent guards
already used elsewhere in this file and in coord-done.sh.

coord-count.sh and coord-sweep.sh both enumerate the mailbox root with
a bare "$COORD"/* glob, which by construction never matches a
dot-prefixed directory - confirmed empirically before this change.
Fixing only the send-side guard would have let a dot-prefixed mailbox
receive mail that neither script could ever report or close on its
grace window. Both now also glob "$COORD"/.[!.]* to reach hidden
mailboxes without matching "." or "..".

coord-selftest.sh: 183 -> 191 checks, section 30 covers the fix and
both enumeration paths. Doc counts (README badge, CLAUDE.md) updated
to match; the catalog's mirrored badge is untouched pending release.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHmf1VfCvaYXamJxe5y6Vt
2026-08-09 21:09:23 +02:00

95 lines
4.6 KiB
Bash
Executable file

#!/bin/bash
# coord-count.sh - count PENDING directed messages per mailbox WITHOUT
# delivering anything. Prints one "<mailbox>\t<pending>\t<debt>" line per mailbox
# that has unhandled mail, sorted by name; prints nothing when none do.
#
# TWO INTEGERS, NOT ONE. <pending> is every unhandled message; <debt> is the
# subset whose sender declared it expects a reply (frontmatter reply-expected,
# 0.11.0). Replacing the first with the second was the obvious reading of "count
# debt rather than unarchived messages", and it is wrong here: board.sh counts
# the same inbox files itself, so a debt-only count would put two different
# numbers under one name with nothing to reconcile them - and a mailbox holding
# only notices would read as empty while its messages keep being re-injected.
#
# WHY THIS IS NOT coord-inbox.sh --repo <x>: reading IS delivery. The read path
# prints a broadcast and then records it as seen, so asking it "what is pending
# for x" would consume x's broadcast backlog as a side effect - once, silently,
# and unrecoverably (the seen set is delivery history, and retraction
# deliberately leaves it alone). This script only counts files.
#
# It keys on MAILBOXES, not on repos: it enumerates $COORD/* and never scans a
# filesystem for checkouts. A repo without a mailbox has no pending messages by
# definition - it is not missing from the count, it is absent from the domain.
#
# Usage: coord-count.sh [--exclude <mailbox>]
# --exclude <mailbox> omit one mailbox (the caller's own, whose inbox is
# already injected in full).
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
# Exit: always 0 - this runs at session start and must never fail one.
# ASCII only, bash 3.2 safe.
set -u
export LC_ALL=C
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
EXCLUDE=""
while [ $# -gt 0 ]; do
case "$1" in
# bash 3.2: `shift 2` past the end of $# is a no-op -> would loop forever.
--exclude) [ $# -ge 2 ] || { echo "coord-count: --exclude requires a value" >&2; exit 2; }
EXCLUDE="$2"; shift 2 ;;
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
# Lenient but not silent, exactly as coord-inbox.sh: failing here would fail
# a SessionStart over a stray flag, and staying silent would make a typo
# look like a working invocation. The hook discards stderr.
*) echo "coord-count: unknown argument: $1 (ignored)" >&2; shift ;;
esac
done
[ -d "$COORD" ] || exit 0
# Does this message owe a reply? Absent field means YES: every message written
# before 0.11.0 lacks it, so absence has to keep 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 silence a real debt by claiming
# "reply-expected: no" at column 0. That is stricter than the grep -m1 the older
# fields use, where frontmatter-comes-first happens to save them. A file without
# two '---' terminators has no frontmatter to trust, so it counts as debt.
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
}
# Glob expansion under LC_ALL=C is already name-sorted. An unmatched glob
# expands to the literal pattern, which fails the -d test and is skipped.
# Two patterns, not dotglob: a bare "$COORD"/* never matches a dot-prefixed
# directory (e.g. .claude, a real repo's mailbox), and dotglob would also hand
# back "." and ".." plus stray dotfiles like .DS_Store - both filtered here
# only by luck of also failing -d. ".[!.]*" matches exactly the hidden
# directories, excluding "." and "..".
for d in "$COORD"/* "$COORD"/.[!.]*; do
[ -d "$d" ] || continue
name="$(basename "$d")"
# Reserved engine namespace (_broadcast): storage, not a correspondent.
case "$name" in _*) continue ;; esac
[ -n "$EXCLUDE" ] && [ "$name" = "$EXCLUDE" ] && continue
[ -d "$d/inbox" ] || continue
# *.md is the message grammar; a stray file must not inflate a total the
# operator reads as "replies owed".
n=0; owed=0
for m in "$d/inbox"/*.md; do
[ -e "$m" ] || continue
n=$((n + 1))
owes_reply "$m" && owed=$((owed + 1))
done
[ "$n" -gt 0 ] || continue
# Absent, not zero: the question is "who has unhandled mail", and a list of
# zeroes answers a different one at every reader's expense. A mailbox holding
# only notices IS listed, with a debt of 0 - it has mail that will be
# re-injected until someone closes it, which is the thing worth knowing.
printf '%s\t%s\t%s\n' "$name" "$n" "$owed"
done
exit 0