Reading IS delivery in this engine: coord-inbox.sh prints a broadcast and then records it as seen. That made "what is pending elsewhere" unanswerable -- asking the read path per repo would have consumed every repo's broadcast backlog as a side effect, once, silently, and unrecoverably, since the seen set is delivery history that retraction deliberately leaves alone. coord-count.sh answers it by counting files and writing nothing: no seen set, no .origin. It keys on MAILBOXES rather than repos -- it enumerates $COORD/* and never scans a filesystem for checkouts -- so a repo without a mailbox is not missing from the count, it is absent from the domain. Drained mailboxes are omitted rather than reported as zero, because the question is "who is owed a reply" and a list of zeroes answers a different one at every reader's expense. The read path now closes with one aggregate line built from it. The case that motivated this is the session whose own inbox is empty: it saw silence and concluded "all clear" while mail sat unanswered everywhere else. BREAKING (injection contract): the read path is no longer silent whenever THIS repo has nothing pending. It is a silent no-op only when the whole mailbox is empty. Coupling the line to having your own mail would have hidden it from its only real audience. Three selftest assertions that used "no output at all" as a proxy for "nothing was delivered" now assert the absence of the content itself, which is what they always meant. The line is an AGGREGATE of two integers, never a roster. A list of names would reproduce other repos' situation inside this repo's injection -- the state boundary the mailbox exists to respect -- and mailbox names are cross-repo input. Two integers cannot carry anything that escapes the framing. Its disclaimer is engine behavior, not politeness (Rule 7): the line lands directly beneath "handle this inbox FIRST", and without it the numbers read as an extension of that obligation and a session starts answering other repos' mail. Pinned in selftest section 26 exactly as section 20 pins the priority text. The hook's header drops "(unread messages)" for the same reason -- it would now announce mail that does not exist. Selftest 116 -> 136. Every new negative check is anchored to a positive assertion in the same output, because a missing script makes "X is absent" true by vacuity and would have gone green proving nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
62 lines
2.7 KiB
Bash
Executable file
62 lines
2.7 KiB
Bash
Executable file
#!/bin/bash
|
|
# coord-count.sh - count PENDING directed messages per mailbox WITHOUT
|
|
# delivering anything. Prints one "<mailbox>\t<count>" line per mailbox that
|
|
# has unhandled mail, sorted by name; prints nothing when none do.
|
|
#
|
|
# 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
|
|
|
|
# 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.
|
|
for d in "$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="$(ls "$d/inbox"/*.md 2>/dev/null | wc -l | tr -d ' ')"
|
|
[ -n "$n" ] || n=0
|
|
[ "$n" -gt 0 ] || continue
|
|
# Absent, not zero: the question is "who is owed a reply", and a list of
|
|
# zeroes answers a different one at every reader's expense.
|
|
printf '%s\t%s\n' "$name" "$n"
|
|
done
|
|
|
|
exit 0
|