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
81 lines
3.9 KiB
JavaScript
81 lines
3.9 KiB
JavaScript
#!/usr/bin/env node
|
|
// coord - SessionStart hook: inject this repo's pending coordination inbox
|
|
// (directed messages + unseen broadcasts) AND its pending order queue as
|
|
// additionalContext.
|
|
//
|
|
// TWO CHANNELS, TWO BLOCKS, never merged. The mailbox is untrusted cross-repo
|
|
// data that may never instruct a session; the order queue is operator-
|
|
// authorized work delivered by dispatch. Each engine script owns the words its
|
|
// own block is read under - concatenating them into one block, or letting this
|
|
// wrapper write a shared header, would put the two authorization classes under
|
|
// one framing, which is the exact thing the channel split exists to prevent.
|
|
//
|
|
// Thin Node wrapper (marketplace convention: hooks are .mjs) around the bash
|
|
// engine scripts/coord-inbox.sh, which owns the mailbox semantics and is
|
|
// covered by scripts/coord-selftest.sh. Zero dependencies. Always exits 0 -
|
|
// a broken mailbox must never block a session.
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
function emit(context) {
|
|
const out = { continue: true };
|
|
if (context) {
|
|
out.hookSpecificOutput = { hookEventName: 'SessionStart', additionalContext: context };
|
|
}
|
|
process.stdout.write(JSON.stringify(out) + '\n');
|
|
}
|
|
|
|
try {
|
|
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT
|
|
|| join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
|
|
// No identity resolution here. This used to resolve the repo itself and fall
|
|
// back to process.cwd(), which made it a fourth independent copy of the
|
|
// identity rule - and the only one that runs in production, so the engine's
|
|
// guards were bypassed exactly where they mattered. The engine runs in this
|
|
// same cwd and derives the identity from git alone.
|
|
//
|
|
// CLAUDE_COORD_REPO is the one exception, and it is a DECLARATION rather than
|
|
// a derivation: a working surface that is not a git repo (~/repos, $HOME) has
|
|
// nothing to derive from, so the read path declines silently and the surface
|
|
// loses its injection with no error - loss wearing the shape of normal. The
|
|
// operator sets this in that directory's settings to say which mailbox the
|
|
// surface owns. It is not the pwd fallback returning: the fallback guessed,
|
|
// this is written down, readable back, and deletable. Forwarded verbatim as
|
|
// --repo, so it inherits the engine's rules - including that an explicit
|
|
// override never claims .origin. Boundary rule holds: no mailbox logic here.
|
|
const declared = process.env.CLAUDE_COORD_REPO;
|
|
const run = (name) => {
|
|
const script = join(pluginRoot, 'scripts', name);
|
|
// Each engine is run on its own, and a failure in one must not cost the
|
|
// other its injection: an order queue that stayed invisible because the
|
|
// mailbox threw would be exactly the silent evaporation the queue exists
|
|
// to stop.
|
|
try {
|
|
return execFileSync('bash',
|
|
declared ? [script, '--repo', declared] : [script],
|
|
{ stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
|
|
} catch { return ''; }
|
|
};
|
|
|
|
const inbox = run('coord-inbox.sh');
|
|
const orders = run('coord-order-inbox.sh');
|
|
|
|
// Headers stay neutral on purpose. Since 0.8.0 the mailbox engine also emits
|
|
// a cross-repo line when THIS repo has nothing pending, so "(unread
|
|
// messages)" would announce mail that does not exist. Each engine's own text
|
|
// says what its block is; the wrapper must not restate it and get it wrong.
|
|
//
|
|
// Orders go LAST. The inbox block carries Rule 7 ("handle this inbox FIRST"),
|
|
// and the queue order the convention defines is mail -> orders -> STATE's
|
|
// NESTE; printing the queue above the rule that outranks it would put the two
|
|
// in the opposite order on the page from the order they are to be worked in.
|
|
let out = '';
|
|
if (inbox.trim()) out += '== Repo coordination ==\n' + inbox;
|
|
if (orders.trim()) out += (out ? '\n' : '') + '== Repo order queue ==\n' + orders;
|
|
emit(out);
|
|
} catch {
|
|
emit('');
|
|
}
|