#!/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(''); }