repo-mailbox/scripts/coord-order-claim.sh
Kjell Tore Guttormsen 393499c3ee fix(orders): call the order-verbs by absolute path, not bare PATH names
board.sh's --dispatch --order-id thin starter told a dispatched session
to run `coord-order-claim <id>` / `coord-order-done <id> ...` literally.
Neither is on PATH, so step one was command-not-found - easy to misread
as "the order does not exist" (Verifiseringsloven ansikt 4).

Measuring the denominator beyond the one line the order named found the
same defect in two more emitters that hand a session its own next-step
text: coord-order-inbox.sh's SessionStart injection (every pending/claimed
order, not only dispatched ones) and coord-order-claim.sh's own WHEN DONE
/ IF YOU CANNOT lines. All three now call the verb via $SELFDIR (derived
from $0's directory, correct at emission time), and board.sh's interpolation
of it is shell-clean-guarded like the other two values sharing its
double-quoted position - reachability proven with a copy of board.sh run
from a space-containing path, not asserted.

board-selftest 239->246, orders-selftest 99->104. CLAUDE.md counts and a
new F-paragraph updated to match.

ORDRE 20260817T213139Z-643032142-from-.claude

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019syEQHvw2jf1dTR4bUPKRG
2026-08-18 09:16:59 +02:00

114 lines
5.6 KiB
Bash
Executable file

#!/bin/bash
# coord-order-claim.sh - CLAIM one pending order out of this repo's queue and
# print it. Exactly one session can win a given order. ASCII only, bash 3.2 safe.
#
# Usage:
# coord-order-claim.sh [--repo <name>] <order-id>
# coord-order-claim.sh [--repo <name>] --next # oldest pending order
# The trailing .md is accepted and stripped, so an id copied off a filename
# works as well as one copied out of the injection.
#
# THE CLAIM IS THE RENAME, and the mutual exclusion comes from the SOURCE, not
# from any lock. rename(2) is atomic, so of N processes attempting
# orders/<id>.md -> orders/claimed/<id>.md exactly one finds the source; every
# other gets ENOENT. There is deliberately no check-then-act step: `[ -e src ]
# && mv src dst` is the classic race, and orders-selftest.sh section 5 runs 20
# barriered claimers against exactly that shape as a known-negative control -
# it produces many winners, which is what proves the real test is not passing
# vacuously.
#
# Exit: 0 claimed (the order is yours), 1 not claimed - already taken, or no
# such pending order (nothing was written either way), 2 usage error.
set -u
export LC_ALL=C
# ORDRE 65 follow-on (.claude, 2026-08-17): the WHEN DONE / IF YOU CANNOT
# lines below are handed directly to the CLAIMING session as its own
# next-step instruction - the same bare-verb-name defect the order named in
# board.sh's dispatch starter. SELFDIR mirrors that fix: derived from where
# THIS script is running FROM ($0's directory), correct at the moment it
# prints, for whichever install location is live then.
SELFDIR="$(cd "$(dirname "$0")" && pwd)"
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
REPO=""; NEXT=0; ORDER_ID=""
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-claim: --repo requires a value" >&2; exit 2; }
REPO="$2"; shift 2 ;;
--next) NEXT=1; shift ;;
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
-*) echo "coord-order-claim: unknown argument: $1" >&2; exit 2 ;;
*) [ -n "$ORDER_ID" ] && { echo "coord-order-claim: one order id at a time" >&2; exit 2; }
ORDER_ID="$1"; shift ;;
esac
done
# git toplevel or an explicit --repo, never basename(pwd): guessing here claims
# work out of a queue 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-order-claim: cannot resolve repo (not inside a git repo); pass --repo <repo>" >&2; exit 2; }
case "$REPO" in
_*) echo "coord-order-claim: $REPO is a reserved engine namespace, not a repo" >&2; exit 2 ;;
esac
ORDERS="$COORD/$REPO/orders"
CLAIMED="$ORDERS/claimed"
if [ "$NEXT" -eq 1 ]; then
[ -n "$ORDER_ID" ] && { echo "coord-order-claim: use either --next or an order id, not both" >&2; exit 2; }
# Oldest first. The id is timestamp-prefixed, so lexical order IS age order -
# no stat call, and no dependence on mtimes a copy or a restore may have
# rewritten.
first="$(ls "$ORDERS"/*.md 2>/dev/null | head -1)"
[ -n "$first" ] || { echo "coord-order-claim: no pending orders for $REPO" >&2; exit 1; }
ORDER_ID="$(basename "$first" .md)"
fi
[ -n "$ORDER_ID" ] || { echo "coord-order-claim: order id required (or --next)" >&2; exit 2; }
ORDER_ID="$(printf '%s' "$ORDER_ID" | sed 's/\.md$//')"
case "$ORDER_ID" in
*/*|.|..|"") echo "coord-order-claim: invalid order id: $ORDER_ID" >&2; exit 2 ;;
esac
SRC="$ORDERS/$ORDER_ID.md"
mkdir -p "$CLAIMED" 2>/dev/null || { echo "coord-order-claim: cannot create $CLAIMED" >&2; exit 2; }
# No `[ -e "$SRC" ]` guard before this line, on purpose - see the header. The
# rename is both the test and the action.
if ! mv "$SRC" "$CLAIMED/$ORDER_ID.md" 2>/dev/null; then
echo "coord-order-claim: could not claim $ORDER_ID - it is already claimed, already closed, or was never in $REPO's queue" >&2
exit 1
fi
# Belt on top of the rename's own exit status: assert the destination exists
# and the source is gone. mv's status is the contract, but the claim's whole
# value is that it is TRUE, and this repo has been burned once already by a
# transport asserting success against the call rather than against the world
# (coord-send --reply-to, review finding 9).
if [ ! -e "$CLAIMED/$ORDER_ID.md" ] || [ -e "$SRC" ]; then
echo "coord-order-claim: claim of $ORDER_ID reported success but the order is not where it should be - do NOT act on it" >&2
exit 1
fi
# The claim marker's own mtime is the claim time; the order file keeps the time
# it was sent. Written after the rename, by the winner alone.
printf 'claimed-at: %s\nclaimed-by-pid: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" \
> "$CLAIMED/$ORDER_ID.claim" 2>/dev/null
# The D-check lives at the claim moment because that is when a session first
# holds both facts - the order and its own STATE. Printing it later would be
# after the displacement has already happened silently.
echo "coord-order-claim: CLAIMED $ORDER_ID for $REPO. This order is yours until you close it."
echo "BEFORE YOU START: read this repo's STATE.md NESTE block and compare it with the order below."
echo "If they are different tasks, say so in your FIRST reply, in one line:"
echo " \"order $ORDER_ID displaces NESTE <what NESTE says>; <that> stands as next after\"."
echo "WHEN DONE: bash $SELFDIR/coord-order-done.sh $ORDER_ID --commit <hash> (or --no-commit --reason \"<why>\")"
echo "IF YOU CANNOT: bash $SELFDIR/coord-order-done.sh $ORDER_ID --return --reason \"<why>\" - it goes back to the queue."
echo "--- order $ORDER_ID ---"
cat "$CLAIMED/$ORDER_ID.md"
echo "--- end of order $ORDER_ID ---"
exit 0