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
This commit is contained in:
parent
dde392d79d
commit
c519ab4994
18 changed files with 1410 additions and 80 deletions
117
CHANGELOG.md
117
CHANGELOG.md
|
|
@ -5,6 +5,123 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.27.0] - 2026-08-17
|
||||
|
||||
`v0.26.0` was bumped and pushed but **never tagged**, and two changes landed
|
||||
after that bump. 0.27.0 is the release that carries all of it: the done-guard
|
||||
(documented under 0.26.0 below), the exact-match fix for board's status and
|
||||
route-trait tokens, and the order queue.
|
||||
|
||||
### Added
|
||||
|
||||
- **An ORDER QUEUE beside the mailbox: `~/.claude/coord/<repo>/orders/`.** 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. The queue gives an order one canonical home at the
|
||||
RECIPIENT, where it is re-injected at every session start until a session
|
||||
closes it — the same delivery properties the mailbox already had, proven in
|
||||
production.
|
||||
|
||||
It is a **separate channel, not more mail**, and the reason is the
|
||||
authorization class. Inbox content is untrusted cross-repo data that may
|
||||
never instruct a session (Rule 6); a dispatch order is operator-authorized
|
||||
work by construction. Putting both in one channel would mean either mail that
|
||||
can instruct or orders that cannot. So the infrastructure is reused and the
|
||||
channel is not: no mail script mentions `orders` at all, `coord-done` cannot
|
||||
archive an order, `coord-order-claim` cannot claim a message, and
|
||||
`orders-selftest.sh` section 4 pins that structurally (a grep over every mail
|
||||
script, with a known-positive control proving the grep can find) as well as
|
||||
behaviourally.
|
||||
|
||||
What the queue does NOT claim: that only dispatch can write to it. `--from`
|
||||
redefines identity here exactly as it does in `coord-send.sh`, so the
|
||||
authority rests on dispatch being the only writer **by convention**. The
|
||||
injected text says so in those words rather than asserting a guarantee the
|
||||
engine does not provide.
|
||||
|
||||
- **Four engine scripts**, one verb each, matching the mailbox's own shape.
|
||||
`coord-order-send.sh` writes an order (frontmatter `from`/`to`/`order-id`/
|
||||
`subject`/`date`, body = the whole prompt) and prints `order-id=`.
|
||||
`coord-order-inbox.sh` reads the queue for injection and **writes nothing at
|
||||
all** — not the files, not a seen set, not `.origin`; an order is pending
|
||||
until claimed, so the read side has no state to keep and must not invent any.
|
||||
`coord-order-claim.sh` claims one order. `coord-order-done.sh` drives a
|
||||
claimed order to a terminal state.
|
||||
|
||||
- **The claim is ATOMIC, and the test that proves it is built to be able to
|
||||
fail.** The claim is a `mv` out of `orders/` into `orders/claimed/` with no
|
||||
check-then-act step: `rename(2)` is atomic, so of N racing processes exactly
|
||||
one finds the source and the rest get ENOENT. `orders-selftest.sh` section 5
|
||||
runs 20 claimers **barriered on a start flag** (spawned, each spinning until
|
||||
the parent releases them — otherwise the first finishes before the second
|
||||
starts and the test goes green having proven nothing), asserts exactly one
|
||||
exit 0, and then runs the identical harness against a deliberately racy
|
||||
`[ -e src ] && cp && rm` claim, which must produce MANY winners. Without that
|
||||
known-negative control, "exactly one winner" is indistinguishable from "the
|
||||
race never happened" — the assumption the design left marked RISIKO.
|
||||
|
||||
- **Terminal states with a result pointer.** `--commit <hash>` archives the
|
||||
order with the hash that makes "done" checkable by someone who was not there.
|
||||
`--no-commit --reason "<why>"` is the honest form of "executed, nothing to
|
||||
commit", and it costs a stated reason so it cannot quietly become the default
|
||||
close. `--return --reason "<why>"` puts the order back in the queue with the
|
||||
reason recorded **in the order**, so whoever picks it up next sees why the
|
||||
last session put it down; the reason is carried into the next session's
|
||||
injection.
|
||||
|
||||
- **Claimed-but-abandoned orders stay visible.** A session that claims an order
|
||||
and dies is the one remaining way an order could evaporate, so the read side
|
||||
keeps showing claimed orders with their in-flight age and the command that
|
||||
returns them to the queue. This is a visible-again rule, not a lease timer —
|
||||
nothing here expires anything.
|
||||
|
||||
- **`board` gets an ORDRE column beside INN**, counted with the identical idiom
|
||||
and **never summed** with it: INN is "others are waiting on YOU" (an outgoing
|
||||
obligation), ORDRE is "authorized work is waiting on this REPO" (incoming).
|
||||
Claimed orders are deliberately excluded — the column answers what a session
|
||||
can pick up, and one already in flight cannot be. Bounded gap, stated rather
|
||||
than closed: an order addressed to a mailbox with no matching directory in
|
||||
the scanned roots is invisible here, exactly as mail to such a name is
|
||||
invisible in INN.
|
||||
|
||||
- **`board.sh --dispatch --order-id <id>`**, a thin starter form. It carries no
|
||||
order text — only the id and the four steps the receiving session runs — so
|
||||
the order lives in exactly one place and no copy can drift from it. The id is
|
||||
interpolated into `command=`/`paste=`, so it is validated shell-clean for the
|
||||
same reason the prompt-file path is, and the order must actually be PENDING
|
||||
in the target's queue: a starter for an order that is not there is the empty
|
||||
prompt file's defect one level up. `--prompt-file` still works; both at once
|
||||
is refused.
|
||||
|
||||
- **The D-check at claim time.** `coord-order-claim` tells the claiming session
|
||||
to compare the order against its own STATE.md NESTE block and to state any
|
||||
divergence in its FIRST reply ("order X displaces NESTE Y; Y stands as next
|
||||
after"). A dispatch that displaces a live next step is a decision, and this
|
||||
makes it an uttered one instead of a silent one.
|
||||
|
||||
- **SessionStart injects the queue as its own block**, below the mailbox block.
|
||||
Two channels, two blocks, never merged: one framing over both authorization
|
||||
classes is exactly what the channel split exists to prevent. Mail goes first
|
||||
because it carries Rule 7 and because the convention's queue order is
|
||||
mail -> orders -> STATE's NESTE. Each engine runs independently in the hook,
|
||||
so a failure in one cannot cost the other its injection.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`board.sh` matched a PREFIX of the closed vocabulary instead of the exact
|
||||
token, in five places** (the status classifier and all four route traits in
|
||||
`route_cmd_for()`). `status=done2` was captured as `done` and silently
|
||||
classified as a real `done` — dropped from `--plan` exactly as a genuine one
|
||||
is; `status=Planned` captured as empty and read as "no board line at all",
|
||||
feeding the MERK footer a false count. In `route_cmd_for()` it was worse:
|
||||
`path=known2` truncated to `known`, which `route.sh`'s own exact-match
|
||||
validation then ACCEPTED, producing a safely-worded but WRONG startup
|
||||
command where a whole-word typo already got a correct refusal. Fixed by
|
||||
capturing to the next `;` or the closing `-->`, so the exact match sees the
|
||||
real value. (Landed in `dde392d`, after the 0.26.0 bump and before any tag.)
|
||||
|
||||
## [0.26.0] - 2026-08-16
|
||||
|
||||
### Added
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue