repo-mailbox/scripts/coord-order-done.sh
Kjell Tore Guttormsen 6b26b8e94b fix(orders): narrow the return-reason escape so a plain arrow survives
`sed 's/--*>/-->/g; s/-->/ /g'` rewrote `->` (ONE dash) into `-->` and then
blanked it, so a reason written the way this repo writes prose - "premise ->
dead" - silently lost its arrow. The intent was only to stop a literal `-->`
from closing the trailer's HTML comment early. Replaced with a single
expression matching two-or-more dashes, `s/---*>/ /g`.

Same over-broad-escaping class as the `$`-pattern bug in
pre-state-line-guard.mjs, and invisible for the same reason: the fixture
reason had no arrow. Two checks added - a plain arrow must survive, a literal
`-->` must still be neutralised - so the escape now has both a known-positive
and a known-negative. orders 97 -> 99.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iB7ipXGgEpv9imYoVmr2
2026-08-17 21:24:37 +02:00

126 lines
6 KiB
Bash
Executable file

#!/bin/bash
# coord-order-done.sh - drive a CLAIMED order to a terminal state. ASCII only,
# bash 3.2 safe.
#
# Usage:
# coord-order-done.sh [--repo <name>] <order-id> --commit <hash>
# coord-order-done.sh [--repo <name>] <order-id> --no-commit --reason "<why>"
# coord-order-done.sh [--repo <name>] <order-id> --return --reason "<why>"
#
# Three modes, mutually exclusive, one of them required:
# --commit <hash> executed. Archived with a RESULT POINTER - the hash is
# what makes "done" checkable by someone who was not there.
# --no-commit executed with nothing to commit (a measurement, a
# verification). Costs a stated --reason precisely so it
# cannot quietly become the default way to close an order.
# --return not executed. Goes BACK to pending with the reason
# recorded IN the order, so whoever picks it up next sees
# why the last session put it down. Never a silent drop.
#
# Only ever looks in orders/claimed/. It cannot touch the coordination inbox,
# and coord-done.sh cannot touch an order: the two channels have separate
# verbs on purpose, and orders-selftest.sh section 4 pins both directions.
#
# Exit: 0 closed, 1 no such claimed order (nothing written), 2 usage error.
set -u
export LC_ALL=C
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
REPO=""; ORDER_ID=""; COMMIT=""; REASON=""; MODE=""
set_mode() {
if [ -n "$MODE" ] && [ "$MODE" != "$1" ]; then
echo "coord-order-done: --commit, --no-commit and --return are mutually exclusive" >&2; exit 2
fi
MODE="$1"
}
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-order-done: --repo requires a value" >&2; exit 2; }
REPO="$2"; shift 2 ;;
--commit) [ $# -ge 2 ] || { echo "coord-order-done: --commit requires a value" >&2; exit 2; }
set_mode executed; COMMIT="$2"; shift 2 ;;
--no-commit) set_mode no-commit; shift ;;
--return) set_mode returned; shift ;;
--reason) [ $# -ge 2 ] || { echo "coord-order-done: --reason requires a value" >&2; exit 2; }
REASON="$2"; shift 2 ;;
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
-*) echo "coord-order-done: unknown argument: $1" >&2; exit 2 ;;
*) [ -n "$ORDER_ID" ] && { echo "coord-order-done: one order id at a time" >&2; exit 2; }
ORDER_ID="$1"; shift ;;
esac
done
if [ -z "$REPO" ]; then
REPO="$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null)"
fi
[ -z "$REPO" ] && { echo "coord-order-done: cannot resolve repo (not inside a git repo); pass --repo <repo>" >&2; exit 2; }
case "$REPO" in
_*) echo "coord-order-done: $REPO is a reserved engine namespace, not a repo" >&2; exit 2 ;;
esac
[ -n "$ORDER_ID" ] || { echo "coord-order-done: order id required" >&2; exit 2; }
ORDER_ID="$(printf '%s' "$ORDER_ID" | sed 's/\.md$//')"
case "$ORDER_ID" in
*/*|.|..|"") echo "coord-order-done: invalid order id: $ORDER_ID" >&2; exit 2 ;;
esac
case "$MODE" in
"") echo "coord-order-done: one of --commit <hash> / --no-commit --reason <why> / --return --reason <why> is required" >&2; exit 2 ;;
executed) [ -n "$COMMIT" ] || { echo "coord-order-done: --commit requires a hash" >&2; exit 2; } ;;
# A reason is the whole content of these two states. Without it "returned"
# is a silent drop with extra steps, and --no-commit is "trust me".
no-commit) [ -n "$REASON" ] || { echo "coord-order-done: --no-commit requires --reason \"<why there is nothing to commit>\"" >&2; exit 2; } ;;
returned) [ -n "$REASON" ] || { echo "coord-order-done: --return requires --reason \"<why you are putting it back>\"" >&2; exit 2; } ;;
esac
# Same line-orientation rule as the send side: the trailer is one line, and a
# newline inside it would forge a second one.
sanitize_field() { printf '%s' "$1" | tr '\r\n' ' ' | tr -d '\000-\037'; }
COMMIT="$(sanitize_field "$COMMIT")"
REASON="$(sanitize_field "$REASON")"
# The trailer is an HTML comment, so a '-->' inside a reason would close it
# early and leave the rest as body prose. ONE expression, not a round trip
# through '-->': a `s/--*>/-->/g; s/-->/ /g` pair also rewrites a plain `->`
# into `-->` and then blanks it, so a reason written the way this repo writes
# prose ("premise -> dead") would silently lose its arrow. Same over-broad
# escaping class as the `$`-pattern bug the state-line guard already paid for.
# `---*>` is TWO-or-more dashes then '>', not `--*>` which is ONE-or-more and
# therefore eats a plain `->` as well - the same over-broad match, one character
# narrower, and the selftest carries an arrow fixture that catches it.
REASON="$(printf '%s' "$REASON" | sed 's/---*>/ /g')"
ORDERS="$COORD/$REPO/orders"
CLAIMED="$ORDERS/claimed"
ARCHIVE="$ORDERS/archive"
SRC="$CLAIMED/$ORDER_ID.md"
[ -e "$SRC" ] || { echo "coord-order-done: no claimed order $ORDER_ID for $REPO (already closed, never claimed, or the wrong id)" >&2; exit 1; }
STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
if [ "$MODE" = "returned" ]; then
printf '\n<!-- order-returned: at=%s; by=%s; reason=%s -->\n' "$STAMP" "$REPO" "$REASON" >> "$SRC"
DEST="$ORDERS/$ORDER_ID.md"
WORD="returned to the queue"
else
if [ "$MODE" = "executed" ]; then
printf '\n<!-- order-result: executed; commit=%s; at=%s -->\n' "$COMMIT" "$STAMP" >> "$SRC"
else
printf '\n<!-- order-result: executed; commit=none; at=%s; why=%s -->\n' "$STAMP" "$REASON" >> "$SRC"
fi
mkdir -p "$ARCHIVE" 2>/dev/null || { echo "coord-order-done: cannot create $ARCHIVE" >&2; exit 2; }
DEST="$ARCHIVE/$ORDER_ID.md"
WORD="archived"
fi
if ! mv "$SRC" "$DEST" 2>/dev/null; then
echo "coord-order-done: could not move $ORDER_ID to $DEST" >&2; exit 2
fi
# The claim marker is delivery state, not history: once the order has left
# orders/claimed/ a marker there would make a closed order look in flight.
/bin/rm -f "$CLAIMED/$ORDER_ID.claim" 2>/dev/null
echo "coord-order-done: $ORDER_ID $WORD for $REPO"
exit 0