#!/bin/bash # coord-done.sh - mark directed coordination message(s) as handled by moving # them from this repo's inbox to archive (kept, never deleted). Idempotent and # fail-safe. ASCII only, bash 3.2 safe. # # Usage: # coord-done.sh ... 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 ] ... # Env: CLAUDE_COORD_DIR overrides the mailbox root. set -u export LC_ALL=C COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}" REPO="" ALL=0 # Indexed array, never a space-joined string: basenames may contain any # character except "/", and re-splitting would make them un-archivable. NAMES=() while [ $# -gt 0 ]; do case "$1" in # bash 3.2: `shift 2` past the end of $# is a no-op -> would loop forever. --repo) [ $# -ge 2 ] || { echo "coord-done: --repo requires a value" >&2; exit 2; } REPO="$2"; shift 2 ;; --all) ALL=1; shift ;; -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) NAMES+=("$1"); shift ;; esac done # git toplevel or an explicit --repo, never basename(pwd): see the identity # note in coord-send.sh. Guessing here archives messages out of a mailbox the # caller does not own. if [ -z "$REPO" ]; then REPO="$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null)" fi [ -z "$REPO" ] && { echo "coord-done: cannot resolve repo (not inside a git repo); pass --repo " >&2; exit 2; } # _broadcast is not a repo, and this is the door the sender check in # coord-send.sh does not cover: archiving out of _broadcast/inbox retires an # announcement for every repo that has not read it yet - an unauthenticated # retract. Retiring a broadcast is coord-send --retract, which checks the sender. case "$REPO" in _*) echo "coord-done: $REPO is a reserved engine namespace, not a repo; retire a broadcast with coord-send --retract" >&2; exit 2 ;; esac INBOX="$COORD/$REPO/inbox" 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 mkdir -p "$ARCHIVE" 2>/dev/null && mv "$INBOX/$1" "$ARCHIVE/" 2>/dev/null && moved=$((moved + 1)) fi } 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 # bash 3.2 + set -u: expanding an empty array errors, so guard on length. [ "${#NAMES[@]}" -eq 0 ] && { echo "coord-done: name(s) or --all required" >&2; exit 2; } for b in "${NAMES[@]}"; do archive_one "$b"; done fi # 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