#!/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 ] --commit # coord-order-done.sh [--repo ] --no-commit --reason "" # coord-order-done.sh [--repo ] --return --reason "" # # Three modes, mutually exclusive, one of them required: # --commit 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 " >&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 / --no-commit --reason / --return --reason 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 \"\"" >&2; exit 2; } ;; returned) [ -n "$REASON" ] || { echo "coord-order-done: --return requires --reason \"\"" >&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. REASON="$(printf '%s' "$REASON" | sed 's/--*>/-->/g; 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\n' "$STAMP" "$REPO" "$REASON" >> "$SRC" DEST="$ORDERS/$ORDER_ID.md" WORD="returned to the queue" else if [ "$MODE" = "executed" ]; then printf '\n\n' "$COMMIT" "$STAMP" >> "$SRC" else printf '\n\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