Reading IS delivery in this engine: coord-inbox.sh prints a broadcast and then records it as seen. That made "what is pending elsewhere" unanswerable -- asking the read path per repo would have consumed every repo's broadcast backlog as a side effect, once, silently, and unrecoverably, since the seen set is delivery history that retraction deliberately leaves alone. coord-count.sh answers it by counting files and writing nothing: no seen set, no .origin. It keys on MAILBOXES rather than repos -- it enumerates $COORD/* and never scans a filesystem for checkouts -- so a repo without a mailbox is not missing from the count, it is absent from the domain. Drained mailboxes are omitted rather than reported as zero, because the question is "who is owed a reply" and a list of zeroes answers a different one at every reader's expense. The read path now closes with one aggregate line built from it. The case that motivated this is the session whose own inbox is empty: it saw silence and concluded "all clear" while mail sat unanswered everywhere else. BREAKING (injection contract): the read path is no longer silent whenever THIS repo has nothing pending. It is a silent no-op only when the whole mailbox is empty. Coupling the line to having your own mail would have hidden it from its only real audience. Three selftest assertions that used "no output at all" as a proxy for "nothing was delivered" now assert the absence of the content itself, which is what they always meant. The line is an AGGREGATE of two integers, never a roster. A list of names would reproduce other repos' situation inside this repo's injection -- the state boundary the mailbox exists to respect -- and mailbox names are cross-repo input. Two integers cannot carry anything that escapes the framing. Its disclaimer is engine behavior, not politeness (Rule 7): the line lands directly beneath "handle this inbox FIRST", and without it the numbers read as an extension of that obligation and a session starts answering other repos' mail. Pinned in selftest section 26 exactly as section 20 pins the priority text. The hook's header drops "(unread messages)" for the same reason -- it would now announce mail that does not exist. Selftest 116 -> 136. Every new negative check is anchored to a positive assertion in the same output, because a missing script makes "X is absent" true by vacuity and would have gone green proving nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
54 lines
2.6 KiB
JavaScript
54 lines
2.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// coord - SessionStart hook: inject this repo's pending coordination inbox
|
|
// (directed messages + unseen broadcasts) as additionalContext.
|
|
//
|
|
// 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 script = join(pluginRoot, 'scripts', 'coord-inbox.sh');
|
|
const inbox = execFileSync('bash',
|
|
declared ? [script, '--repo', declared] : [script],
|
|
{ stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
|
|
|
|
// Header stays neutral on purpose. Since 0.8.0 the engine also emits a
|
|
// cross-repo line when THIS repo has nothing pending, so "(unread messages)"
|
|
// would announce mail that does not exist. The engine's own text says what
|
|
// each block is; the wrapper must not restate it and get it wrong.
|
|
emit(inbox.trim() ? '== Repo coordination ==\n' + inbox : '');
|
|
} catch {
|
|
emit('');
|
|
}
|