repo-mailbox/scripts/coord-order-claim.sh
Kjell Tore Guttormsen c519ab4994 feat(orders): order queue channel with atomic claim, board ORDRE column
ORDRE 59. A dispatched order used to live only in a scratch prompt file
passed through argv, so it died with the pane it was typed into. Measured
2026-08-17: one order was dispatched three times over 90 minutes before it
was worked, because the first two tabs ran something else and the order
left no trace in the receiving repo at all.

New channel `~/.claude/coord/<repo>/orders/`, beside `inbox/` and never
merged with it. The axis is authorization: inbox content is untrusted
cross-repo data that may never instruct a session (Rule 6), a dispatch
order is operator-authorized work by construction. One channel carrying
both classes would mean either mail that can instruct or orders that
cannot, so the infrastructure is reused and the channel is not.

Four one-verb engines: coord-order-send.sh (write), coord-order-inbox.sh
(read, writes nothing at all), coord-order-claim.sh (atomic claim),
coord-order-done.sh (executed with a commit pointer / --no-commit with a
reason / --return with a reason).

The claim is a rename with no check-then-act step, so of N racing sessions
exactly one finds the source and the rest get ENOENT. The test that proves
it spawns 20 claimers BARRIERED on a start flag - unbarriered children do
not race at all - and runs the identical harness against a deliberately
racy `[ -e src ] && cp && rm` as a known-negative control, which must
produce many winners. Without that control, "exactly one winner" is
indistinguishable from "the race never happened".

Channel separation is pinned structurally, not only behaviourally: no mail
script may contain the string `orders`, with a known-positive control
proving the grep can find. coord-done cannot archive an order and
coord-order-claim cannot claim a message.

board gains an ORDRE column beside INN, counted with the identical idiom
and never summed with it: INN is "others are waiting on YOU", ORDRE is
"work is waiting on this REPO". Claimed orders are excluded - the column
answers what a session can pick up. board.sh --dispatch --order-id emits a
thin starter carrying only the id and the four steps, so the order text has
exactly one home; the id is validated shell-clean and must be pending in
the target's queue.

SessionStart injects the queue as its own block below the mailbox block.
Two channels, two blocks, mail first: it carries Rule 7, and the queue
order is mail -> orders -> STATE's NESTE.

Also folds in dde392d (board prefix-match fix), which landed after the
0.26.0 bump and before any tag. v0.26.0 was never tagged, so 0.27.0 is the
release that carries all of it.

Suites: coord 220, board 237, route 69, orders 97, guard 40; npm test 11/11.
Antakelse 4 (atomic claim) and antakelse 6 (morning --plan-file --dry-run
reports 1 of 1 for the thin starter) both measured, not assumed.

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

106 lines
5.1 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
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: coord-order-done $ORDER_ID --commit <hash> (or --no-commit --reason \"<why>\")"
echo "IF YOU CANNOT: coord-order-done $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