feat(engine): make a bulk close state what Rule 7 says must be stated

coord-done.sh was the third script that acts on pending messages and the only
one that did not learn the field. --all archives every pending message in one
call, including the ones whose sender declared it expects a reply - the exact
outcome Rule 7 exists to prevent, now reachable with no friction and no trace.

The behavior stays. The receiver keeps both terminal states by design (section
20), and a bulk close is legitimate; refusing would move a decision that belongs
to the operator into the script. What was wrong was the silence: Rule 7 requires
leaving a message unanswered to be STATED, and a command reporting only a total
made the thing that has to be stated invisible.

So --all now names the number it closed without a reply, and only then - a line
that always fires is one nobody reads. A named close stays quiet, because naming
a message is already deliberate, one message at a time.

coord-selftest 151 -> 156.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016iJoZVmU2guTEZcMghk88z
This commit is contained in:
Kjell Tore Guttormsen 2026-07-31 15:48:42 +02:00
commit 5754d67a6b
5 changed files with 78 additions and 5 deletions

View file

@ -6,6 +6,12 @@
# Usage:
# coord-done.sh <basename>... mark the named message(s) handled
# coord-done.sh --all mark all pending directed messages handled
# --all reports how many of the messages it closed had a sender expecting a
# reply (frontmatter reply-expected, 0.11.0). It does not refuse: the receiver
# keeps both terminal states by design, and a bulk close is legitimate. But
# Rule 7 requires leaving a message unanswered to be STATED, and a one-command
# path reporting only a total makes the thing that has to be stated invisible.
# The script owes the operator the fact, not a veto.
# coord-done.sh [--repo <name>] <basename>...
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
set -u
@ -48,6 +54,17 @@ ARCHIVE="$COORD/$REPO/archive"
[ -d "$INBOX" ] || { echo "coord-done: no inbox for $REPO"; exit 0; }
moved=0
owed=0
# Same rule as coord-count.sh: absent means a reply IS expected, and the read is
# bounded to the frontmatter block so an untrusted body cannot mark itself
# closeable. Duplicated rather than shared - each script 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
}
archive_one() {
case "$1" in */*|.|..|"") echo "coord-done: invalid name: $1" >&2; return 1 ;; esac
if [ -e "$INBOX/$1" ]; then
@ -58,6 +75,8 @@ archive_one() {
if [ "$ALL" -eq 1 ]; then
for f in "$INBOX"/*.md; do
[ -e "$f" ] || continue
# Read BEFORE the move: after it the file is no longer at this path.
owes_reply "$f" && owed=$((owed + 1))
archive_one "$(basename "$f")"
done
else
@ -66,5 +85,12 @@ else
for b in "${NAMES[@]}"; do archive_one "$b"; done
fi
echo "coord-done: $moved message(s) archived for $REPO"
# Only on the bulk path, and only when there is something to state: naming a
# message is a deliberate act one message at a time, and a line that always
# fires is one nobody reads.
if [ "$ALL" -eq 1 ] && [ "$owed" -gt 0 ]; then
echo "coord-done: $moved message(s) archived for $REPO ($owed of them expected a reply, and got none - state that to the operator)"
else
echo "coord-done: $moved message(s) archived for $REPO"
fi
exit 0