`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
126 lines
6 KiB
Bash
Executable file
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
|