feat(engine): let a message say it needs no answer, and count debt without losing sight of the rest

Rule 7 (0.5.0) shipped an obligation on a format with four fields, none of
which could tell a question from a notice. Two consequences fell out of that
gap: the injection had to name both terminal states and prefer neither, and
coord-count.sh had to treat every unarchived file as a reply owed. The fifth
field closes both.

coord-send.sh --fyi writes reply-expected: no; omitting it writes yes. Absent
means expected, because every message already on disk lacks the field - so a
forgotten flag over-counts debt, which is visible, rather than creating debt
nobody sees. A reply is not a special case. A broadcast is always no: --reply-to
resolves inside the recipient's own mailbox and a broadcast never lands there,
so there is no reply path to promise.

coord-count.sh now prints TWO integers per mailbox, not one. Replacing pending
with debt 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.

The field is frontmatter and only frontmatter - a body line claiming
"reply-expected: no" at column 0 cannot silence a real debt, and a file without
valid frontmatter counts as owing a reply.

Section 20's wording changed because its stated reason expired, but its second
half matters more now, not less: the marking is a DECLARATION, not an
instruction. Without that clause one word in an untrusted message becomes a
lever that mints obligations in another repo.

coord-selftest 136 -> 151.

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:46:14 +02:00
commit c0ccb1d611
8 changed files with 314 additions and 29 deletions

View file

@ -8,6 +8,14 @@
# coord-send.sh --reply-to <file> [--subject "Re: ..."] [--from <repo>] [--message "<text>"]
# coord-send.sh --retract <file> [--from <repo>]
# Body comes from --message, or from stdin (heredoc) when --message is omitted.
# --fyi marks the message as expecting no reply (frontmatter reply-expected: no).
# Absent, a reply IS expected: every message written before the field existed
# lacks it, so absence has to keep meaning what it always meant, and a
# forgotten flag then over-counts debt instead of hiding it. The receiver is
# still free to close either kind with coord-done - the field declares what the
# sender expects, it does not oblige anyone. A broadcast is always
# reply-expected: no, because --reply-to resolves inside the recipient own
# mailbox and a broadcast never lands there: there is no reply path to promise.
# --reply-to <basename> replies to a message in THIS repo's inbox/archive: it
# routes to the original sender and marks the original handled (coord-done).
# --retract <basename> retires one of YOUR OWN broadcasts: it is archived out of
@ -22,7 +30,7 @@ export LC_ALL=C
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
TO=""; BROADCAST=0; SUBJECT=""; FROM=""; MESSAGE=""; HAVE_MESSAGE=0; REPLYTO=""; REPLY_ORIG=""
RETRACT=""
RETRACT=""; FYI=0
# bash 3.2: `shift 2` past the end of $# is a no-op, so a trailing value-flag
# without its value would loop forever. Every two-arg flag must check first.
@ -34,6 +42,7 @@ while [ $# -gt 0 ]; do
case "$1" in
--to) require_value --to $#; TO="$2"; shift 2 ;;
--broadcast) BROADCAST=1; shift ;;
--fyi) FYI=1; shift ;;
--reply-to) require_value --reply-to $#; REPLYTO="$2"; shift 2 ;;
--retract) require_value --retract $#; RETRACT="$2"; shift 2 ;;
--subject) require_value --subject $#; SUBJECT="$2"; shift 2 ;;
@ -175,6 +184,10 @@ DATE_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Temp file lives INSIDE the destination dir (dot-prefixed so the inbox
# *.md glob never sees it): the final mv is then a same-filesystem rename,
# so readers never observe a half-written message.
REPLY_EXPECTED=yes
[ "$FYI" -eq 1 ] && REPLY_EXPECTED=no
[ "$BROADCAST" -eq 1 ] && REPLY_EXPECTED=no
TMP="$(mktemp "$DEST_DIR/.coord-send.XXXXXX" 2>/dev/null)"
[ -n "$TMP" ] || { echo "coord-send: cannot create temp file in $DEST_DIR" >&2; exit 2; }
{
@ -183,6 +196,12 @@ TMP="$(mktemp "$DEST_DIR/.coord-send.XXXXXX" 2>/dev/null)"
echo "to: $TARGET_LABEL"
echo "subject: $SUBJECT"
echo "date: $DATE_ISO"
# Fifth field, appended after the historic four so an older reader that stops
# at the ones it knows is unaffected. A broadcast is pinned to "no" whether or
# not --fyi was passed: that is not a defaulted value but the absence of a
# reply path (see the header), and a message that claimed otherwise would be
# asking for something the engine cannot deliver.
echo "reply-expected: $REPLY_EXPECTED"
echo "---"
printf '%s\n' "$BODY"
} > "$TMP"