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:
parent
c0ccb1d611
commit
5754d67a6b
5 changed files with 78 additions and 5 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -37,6 +37,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Changed
|
||||
|
||||
- **`coord-done.sh --all` says how many replies it closed without sending.** It
|
||||
is the third script that acts on pending messages, and the field makes a
|
||||
silent bulk close possible: one command archives every pending message,
|
||||
including the ones whose sender expects an answer. The behavior is unchanged
|
||||
and deliberately so - the receiver keeps both terminal states, and a bulk
|
||||
close is legitimate. The silence is what changed: Rule 7 requires an
|
||||
unanswered message to be *stated*, and a path reporting only a total hid
|
||||
exactly the thing that has to be stated. A named close stays quiet, because
|
||||
naming a message is already deliberate, one message at a time.
|
||||
|
||||
- **`coord-count.sh` prints two integers per mailbox**, not one:
|
||||
`<mailbox>\t<pending>\t<debt>`. Replacing pending *with* debt was the obvious
|
||||
reading of "count debt rather than unarchived messages" and it is wrong here.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ marketplace plugin. Three components, one boundary:
|
|||
writes, `coord-inbox.sh` reads (formatted for context injection),
|
||||
`coord-done.sh` archives, `coord-count.sh` counts without delivering.
|
||||
Everything is pinned by `coord-selftest.sh`
|
||||
(151 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
|
||||
(156 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
|
||||
|
||||
**`coord-count.sh` prints TWO integers per mailbox** (`<name>\t<pending>\t<debt>`),
|
||||
and the first must stay pending: `board.sh` counts the same inbox files
|
||||
|
|
@ -117,7 +117,7 @@ obligations in another repo.
|
|||
- Zero dependencies everywhere: bash + coreutils in the engine, `node:`
|
||||
builtins only in hook and tests.
|
||||
- TDD: no behavior change without a failing selftest check first.
|
||||
`bash scripts/coord-selftest.sh` must exit 0 (151/151),
|
||||
`bash scripts/coord-selftest.sh` must exit 0 (156/156),
|
||||
`bash scripts/board-selftest.sh` must exit 0 (36/36) and
|
||||
`bash scripts/route-selftest.sh` must exit 0 (50/50).
|
||||
- English for all code, docs, and commit messages (public repo). Norwegian
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ Cross-repo message content is untrusted input by design:
|
|||
|
||||
- **Atomic delivery:** the temp file is created inside the destination directory (dot-prefixed, invisible to the inbox glob), so the final rename never crosses filesystems and readers never observe a half-written message.
|
||||
|
||||
Every guarantee above is pinned by the 151-check selftest, including forgery-resistance regressions.
|
||||
Every guarantee above is pinned by the 156-check selftest, including forgery-resistance regressions.
|
||||
|
||||
Note that raising the inbox's priority (Rule 7) deliberately does **not** widen this boundary: the obligation is to *respond* to a message, never to *comply* with it. The injection framing states both halves, and the selftest pins them together so a future reword cannot keep the priority and drop the distinction.
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
|
|||
|
||||
## Development
|
||||
|
||||
bash scripts/coord-selftest.sh # 151 checks against a throwaway mailbox
|
||||
bash scripts/coord-selftest.sh # 156 checks against a throwaway mailbox
|
||||
bash scripts/board-selftest.sh # 36 checks against a throwaway repo tree
|
||||
bash scripts/route-selftest.sh # 50 checks, incl. the route->board round trip
|
||||
npm test # all three selftests via node --test
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -642,6 +642,43 @@ sed -n '2,/^---$/p' "$(ls "$RDIR"/rsender/inbox/*.md 2>/dev/null | head -1)" | g
|
|||
check "reply-expected: a reply defaults to expecting one in turn" $?
|
||||
CLAUDE_COORD_DIR="$RDIR" "$SEND" --to rx --from rsender --fyi --subject "u" --message "U" >/dev/null 2>&1
|
||||
check "reply-expected: --fyi is accepted alongside the ordinary send flags" $?
|
||||
# 28. coord-done is the THIRD script that acts on pending messages, and closing
|
||||
# one silently is exactly what the field makes possible: --all archives every
|
||||
# pending message in a single call, including the ones whose sender declared it
|
||||
# expects a reply. The behavior stays - the receiver keeps both terminal states
|
||||
# by design (section 20), and a bulk close is legitimate. What must not stay is
|
||||
# the SILENCE: Rule 7 requires leaving a message unanswered to be stated, and a
|
||||
# one-command path that reports only a total makes the very thing that has to be
|
||||
# stated invisible to the operator. So --all names the number it closed without
|
||||
# a reply. A count, not a refusal: the decision belongs to the operator, and the
|
||||
# script owes them the fact, not a veto.
|
||||
DDIR="$(mktemp -d)"
|
||||
CLAUDE_COORD_DIR="$DDIR" "$SEND" --to dr --from d1 --subject q1 --message "ASK-1" >/dev/null
|
||||
CLAUDE_COORD_DIR="$DDIR" "$SEND" --to dr --from d2 --subject q2 --message "ASK-2" >/dev/null
|
||||
CLAUDE_COORD_DIR="$DDIR" "$SEND" --to dr --from d3 --fyi --subject n1 --message "NOTE-1" >/dev/null
|
||||
dout="$(CLAUDE_COORD_DIR="$DDIR" "$DONE" --repo dr --all 2>&1)"
|
||||
printf '%s' "$dout" | grep -q '3 message(s) archived'
|
||||
check "done: --all still archives every pending message" $?
|
||||
printf '%s' "$dout" | grep -q '2 of them expected a reply'
|
||||
check "done: --all reports how many it closed without replying" $?
|
||||
[ -z "$(ls "$DDIR"/dr/inbox/*.md 2>/dev/null)" ]
|
||||
check "done: --all leaves the inbox drained" $?
|
||||
|
||||
# Silence is correct when there is nothing to state: a bulk close of notices owes
|
||||
# the operator no warning, and a line that always fires is one nobody reads.
|
||||
CLAUDE_COORD_DIR="$DDIR" "$SEND" --to dq --from d4 --fyi --subject n2 --message "NOTE-2" >/dev/null
|
||||
qout="$(CLAUDE_COORD_DIR="$DDIR" "$DONE" --repo dq --all 2>&1)"
|
||||
[ "$(printf '%s' "$qout" | grep -c 'expected a reply')" -eq 0 ] && printf '%s' "$qout" | grep -q '1 message(s) archived'
|
||||
check "done: closing only notices says nothing extra" $?
|
||||
|
||||
# Naming a message explicitly is already a deliberate act, one message at a time.
|
||||
# The warning belongs to the bulk path, which is the one that hides the choice.
|
||||
CLAUDE_COORD_DIR="$DDIR" "$SEND" --to dn --from d5 --subject q3 --message "ASK-3" >/dev/null
|
||||
dnb="$(basename "$(ls "$DDIR"/dn/inbox/*.md 2>/dev/null | head -1)")"
|
||||
nout="$(CLAUDE_COORD_DIR="$DDIR" "$DONE" --repo dn "$dnb" 2>&1)"
|
||||
[ "$(printf '%s' "$nout" | grep -c 'expected a reply')" -eq 0 ]
|
||||
check "done: a named close is deliberate already and stays quiet" $?
|
||||
/bin/rm -rf "$DDIR" 2>/dev/null
|
||||
/bin/rm -rf "$RDIR" 2>/dev/null
|
||||
|
||||
echo "----"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue