Tier 3, all five the same defect class (Verifiseringsloven ansikt 4): a
broken or uninstrumented query returning a positive-looking null, consumed
as a fact about the world.
F5 coord-count.sh: a mailbox root that does not exist was byte-identical
to one where nobody has pending mail - zero lines, exit 0, silent
stderr. Now exit 3 + a named stderr line; an existing-but-empty root
stays a silent, clean 0. 3 rather than 2 because 2 already means "you
called me wrong" and this means "the world you named is not there".
F14 coord-count.sh: the header promised exit 0 unconditionally while
--exclude with no value already exited 2. Contract restated as
0/2/3 and pinned as a check on the help TEXT.
F6 board.sh: `git status | wc -l` yields 0 lines whether the tree is
clean or git refused to answer, so a failure printed DRT=0. Now "?",
and BOTH awk consumers handle it - --plan's free-capacity test
compares the field as a string against "0" (a "?" coerces to 0 in
arithmetic and would certify an unmeasured tree as free), and the SUM
roll-up names what it could not add.
F10 board.sh: a scan root that does not exist was skipped in silence and
the empty scan exited 0. Bad roots are now named on stderr; exit 3
only when NO root was scanned. A mix still exits 0 and prints the
board. Replaces an assertion that encoded this defect as a pass.
F13 pre-state-line-guard.mjs: MAX_LINES is overridable via
CLAUDE_STATE_MAX_LINES so the boundary is testable without hardcoding
120 twice. An unusable value denies by name rather than falling back
to the default - a limit that silently did not take effect is the
same defect one layer up.
Every design choice mutation-tested; every negative check carries a
known-positive control. Section 11's first cut was vacuously green (wrong
basename + unexported fixture path) - recorded in CLAUDE.md rather than
quietly fixed, and the section now asserts its own ground truth.
Denominator measured, not estimated: coord-inbox.sh:57 and
coord-order-inbox.sh:60/64 carry the same `|| exit 0` shape and are
deliberately left alone (injection path, prose output, must never fail a
SessionStart) - stated in CLAUDE.md as a bounded gap.
Suites: coord 230->242, board 281->300, guard 40->54, route 69, orders
110, npm 11/11. Verified under system bash 3.2, not just Homebrew 5.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
182 lines
9.3 KiB
Bash
Executable file
182 lines
9.3 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>\t<origin_age>"
|
|
# 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.
|
|
#
|
|
# <origin_age> (WP1d, .claude 2026-08-14): "-" when the mailbox has a .origin
|
|
# file, otherwise the age in whole days of its OLDEST pending message.
|
|
# coord-inbox.sh writes .origin only from a REAL session's own SessionStart
|
|
# (REPO_PATH resolved via git rev-parse, never when --repo is passed
|
|
# explicitly), so a mailbox with no .origin has NEVER been reached by the
|
|
# normal per-repo injection - pending mail there is a dead letter, not merely
|
|
# slow. This script only reports the raw age; judging it against a threshold
|
|
# is board.sh's job, the same split as <pending> vs <debt> above.
|
|
#
|
|
# 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: 0 = counted (zero or more mailboxes have pending mail)
|
|
# 2 = usage error, nothing counted
|
|
# 3 = mailbox root does not exist, nothing counted
|
|
#
|
|
# The header used to promise exit 0 unconditionally, on the grounds that this
|
|
# runs at session start and must never fail one - and that was false in both
|
|
# directions (F14). It exited 2 on a usage error already, and - worse - it exited 0 with zero lines when the mailbox root
|
|
# was ABSENT, which is byte-identical to "no mailbox has pending mail" on every
|
|
# channel a consumer can read (F5). board.sh consumes this TSV. That is
|
|
# Verifiseringsloven ansikt 4: a broken query returning a positive-looking null.
|
|
# What the old claim was protecting is kept and made precise: no state OF THE
|
|
# MAILBOX can ever produce a nonzero exit - not an empty root, not a malformed
|
|
# message, not an unreadable date. Only the caller (2) or a missing root (3)
|
|
# can, and both print nothing on stdout, so neither can be mistaken for a count.
|
|
# 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
|
|
|
|
# Not `|| exit 0`: see the F5 paragraph in the header. Status 3 rather than 2
|
|
# because 2 is already "you called me wrong" and this is "the world you named
|
|
# is not there" - two different repairs, and a consumer that only ever sees one
|
|
# integer cannot tell them apart. stdout stays empty on purpose: 3 is not a
|
|
# count of zero, it is the absence of a count.
|
|
if [ ! -d "$COORD" ]; then
|
|
echo "coord-count: mailbox root does not exist: $COORD (not counted, not zero)" >&2
|
|
exit 3
|
|
fi
|
|
|
|
# GNU/BSD date flavor, detected once per run (not per mailbox): BSD date
|
|
# rejects --version outright (exit nonzero, "illegal option" - measured on
|
|
# this machine); GNU date supports it and prints a version banner (exit 0 -
|
|
# measured directly against Ubuntu 24.04 / GNU coreutils 9.4). The origin_age
|
|
# column below needs this because BSD's `date -j -f` and GNU's `date -d`
|
|
# share no common invocation - GNU date has no -j at all (measured: "date:
|
|
# invalid option -- 'j'", exit 1), which is why every mailbox printed "-"
|
|
# (now "?", see the F11a comment below) on Linux before this branch existed.
|
|
# coord-selftest.sh section 32 pins the GNU branch via a PATH shim that
|
|
# replays these measured facts.
|
|
DATE_IS_GNU=0
|
|
date --version >/dev/null 2>&1 && DATE_IS_GNU=1
|
|
|
|
# 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".
|
|
# oldest_ts captures only the FIRST message whose filename matches the
|
|
# timestamp grammar. That is safe because the glob above is already
|
|
# name-sorted under LC_ALL=C (see the comment on it), and the grammar's
|
|
# timestamp prefix sorts identically to chronological order - so the first
|
|
# match encountered is the oldest, without a second pass or a full sort.
|
|
n=0; owed=0; oldest_ts=""
|
|
for m in "$d/inbox"/*.md; do
|
|
[ -e "$m" ] || continue
|
|
n=$((n + 1))
|
|
owes_reply "$m" && owed=$((owed + 1))
|
|
if [ -z "$oldest_ts" ]; then
|
|
mts="${m##*/}"
|
|
mts="${mts%%-*}"
|
|
case "$mts" 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)
|
|
oldest_ts="$mts" ;;
|
|
esac
|
|
fi
|
|
done
|
|
[ "$n" -gt 0 ] || continue
|
|
# "-" means .origin exists (claimed, never a dead-letter candidate
|
|
# regardless of age). "?" means unclaimed but the age could not be read -
|
|
# fail-safe, not fail-open, an unreadable age must never be treated as old,
|
|
# matching coord-sweep.sh's identical rule for the same filename grammar -
|
|
# and, critically, must never be reported as the SAME token as claimed
|
|
# (review finding 11, 2026-08-14: both used to print "-", collapsing "not a
|
|
# dead-letter candidate" and "not measured" into one token a consumer could
|
|
# not tell apart). Only a real computed age is neither.
|
|
origin_age="-"
|
|
if [ ! -f "$d/.origin" ]; then
|
|
origin_age="?"
|
|
if [ -n "$oldest_ts" ]; then
|
|
if [ "$DATE_IS_GNU" -eq 1 ]; then
|
|
# Compact grammar (YYYYMMDDTHHMMSSZ) expanded to the RFC 3339 form
|
|
# GNU date documents as always parseable by -d regardless of locale.
|
|
# Bash 3.2 substring expansion, no external command needed.
|
|
oldest_iso="${oldest_ts:0:4}-${oldest_ts:4:2}-${oldest_ts:6:2}T${oldest_ts:9:2}:${oldest_ts:11:2}:${oldest_ts:13:2}Z"
|
|
oldest_epoch="$(date -u -d "$oldest_iso" '+%s' 2>/dev/null)"
|
|
else
|
|
oldest_epoch="$(date -u -j -f '%Y%m%dT%H%M%SZ' "$oldest_ts" '+%s' 2>/dev/null)"
|
|
fi
|
|
case "$oldest_epoch" in
|
|
[0-9]*)
|
|
now_epoch="$(date -u +%s)"
|
|
age_days=$(( (now_epoch - oldest_epoch) / 86400 ))
|
|
[ "$age_days" -ge 0 ] && origin_age="$age_days"
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
# 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\t%s\n' "$name" "$n" "$owed" "$origin_age"
|
|
done
|
|
|
|
exit 0
|