agent-coord-mcp 0.26.36 → 0.26.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/auth-faults.js +145 -0
  2. package/dist/auth-faults.js.map +1 -0
  3. package/dist/capabilities.js +113 -2
  4. package/dist/capabilities.js.map +1 -1
  5. package/dist/launch-unit.js +282 -0
  6. package/dist/launch-unit.js.map +1 -0
  7. package/dist/server.js +46 -7
  8. package/dist/server.js.map +1 -1
  9. package/dist/store.js +139 -1
  10. package/dist/store.js.map +1 -1
  11. package/dist/test-count-key.js +22 -0
  12. package/dist/test-count-key.js.map +1 -0
  13. package/dist/tools/away.js +80 -17
  14. package/dist/tools/away.js.map +1 -1
  15. package/dist/tools/events.js +77 -3
  16. package/dist/tools/events.js.map +1 -1
  17. package/dist/tools/herdr-delivery.js +51 -36
  18. package/dist/tools/herdr-delivery.js.map +1 -1
  19. package/dist/tools/herdr-tail.js +99 -77
  20. package/dist/tools/herdr-tail.js.map +1 -1
  21. package/dist/tools/registry.js +50 -3
  22. package/dist/tools/registry.js.map +1 -1
  23. package/dist/transports/herdr.js +92 -2
  24. package/dist/transports/herdr.js.map +1 -1
  25. package/package.json +1 -1
  26. package/scripts/check-test-count.mjs +80 -6
  27. package/scripts/coord-attention-clock.mjs +19 -21
  28. package/scripts/coord-stall-clock.mjs +23 -24
  29. package/scripts/lib/clock-unit-cli.mjs +134 -0
  30. package/src/auth-faults.ts +166 -0
  31. package/src/capabilities.ts +115 -2
  32. package/src/launch-unit.ts +350 -0
  33. package/src/server.ts +47 -6
  34. package/src/store.ts +146 -1
  35. package/src/test-count-key.ts +21 -0
  36. package/src/tools/away.ts +113 -28
  37. package/src/tools/events.ts +89 -4
  38. package/src/tools/herdr-delivery.ts +46 -31
  39. package/src/tools/herdr-tail.ts +84 -65
  40. package/src/tools/registry.ts +55 -3
  41. package/src/transports/herdr.ts +93 -2
@@ -0,0 +1,350 @@
1
+ /*
2
+ * ⟨q-6d37c349⟩ A NAMED SCHEDULER UNIT THAT EXISTS, RATHER THAN ONE THAT IS DOCUMENTED.
3
+ *
4
+ * Both clocks in this package already argued the right thing in their own headers — a timer, not a
5
+ * trigger, because "a mechanism without a schedule measures inbound traffic rather than elapsed time
6
+ * and cannot fire during exactly the quiet it exists to cover". Both then named a launchd label in a
7
+ * comment and made `--install` PRINT a plist to stdout with instructions for a human.
8
+ *
9
+ * MEASURED 2026-09-26, and this is the whole reason this file exists: `launchctl print
10
+ * gui/501/<label>` exits 113 for BOTH `com.davidbalzan.coord-stall-clock` and
11
+ * `com.davidbalzan.coord-attention-clock`, and `~/Library/LaunchAgents` holds no plist for either.
12
+ * Neither was ever installed. So every one of the stall clock's 200 recorded runs was a HAND run by
13
+ * whoever last polled it, and `stall_clock_status` read "THE CLOCK IS STOPPED" at 9860m on 09-23 and
14
+ * again at 2523m on 09-26. ⛔ THE UNIT WAS NAMED IN DOCUMENTATION, NOT CREATED — the
15
+ * instrument-for-property substitution, one level above the code it was written to prevent.
16
+ *
17
+ * ⛔ SO "INSTALLED" MEANS LOADED, AND IT IS READ BACK FROM launchd. Writing the plist is not
18
+ * installing it; bootstrapping without checking is not either. `installUnit` writes, bootstraps, then
19
+ * ASKS launchctl and REFUSES unless the label answers. The instrument is validated in both directions
20
+ * before it is trusted: exit 0 for `com.davidbalzan.agent-coord-http`, which everyone agrees is
21
+ * loaded, and 113 for a label nobody has installed.
22
+ *
23
+ * Everything here takes its `run` and its filesystem as arguments so a test can drive the refusal
24
+ * paths without touching this machine's launchd, and one live test can prove the real thing.
25
+ */
26
+ import { homedir } from "node:os";
27
+ import path from "node:path";
28
+
29
+ /** What a process runner must answer. Injected everywhere, so nothing here reaches launchd by itself. */
30
+ export type RunResult = { status: number | null; stdout: string; stderr: string };
31
+ export type Runner = (cmd: string, args: string[]) => RunResult;
32
+
33
+ export type UnitFs = {
34
+ writeFileSync: (p: string, data: string) => void;
35
+ mkdirSync: (p: string, opts: { recursive: true }) => void;
36
+ existsSync: (p: string) => boolean;
37
+ readFileSync: (p: string, enc: "utf8") => string;
38
+ rmSync: (p: string, opts: { force: true }) => void;
39
+ };
40
+
41
+ /** `~/Library/LaunchAgents/<label>.plist` — where a per-user agent lives on macOS. */
42
+ export function unitPath(label: string, home: string = homedir()): string {
43
+ return path.join(home, "Library", "LaunchAgents", `${label}.plist`);
44
+ }
45
+
46
+ /**
47
+ * The calling user's uid, which names the `gui/<uid>` domain a per-user agent is filed under.
48
+ *
49
+ * ⛔ A DIRECT call, deliberately NOT an optional call on `getuid` with a `?? 0` fallback (LESSONS #41,
50
+ * and note the guard is line-based so this sentence must not spell the banned form). `getuid` is POSIX-only
51
+ * and @types/node therefore declares it optional, which is what invites the optional call — but every
52
+ * caller in this module refuses on non-darwin before reaching here, so its absence is not a case to
53
+ * tolerate. And the fallback the optional form wants is `0`, which is ROOT's domain: the one uid a
54
+ * per-user LaunchAgent must never be installed into. Throwing beats installing into the wrong domain.
55
+ */
56
+ export function ownUid(): number {
57
+ if (typeof process.getuid !== "function") {
58
+ throw new Error("launch-unit: process.getuid is unavailable — this module is darwin-only and must not guess a uid");
59
+ }
60
+ return process.getuid();
61
+ }
62
+
63
+ /** launchd's own name for a per-user service. `gui/<uid>/<label>` is what `launchctl print` takes. */
64
+ export function serviceTarget(label: string, uid: number = ownUid()): string {
65
+ return `gui/${uid}/${label}`;
66
+ }
67
+
68
+ const esc = (s: string) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
69
+
70
+ /**
71
+ * The plist text. `StartInterval` is what makes it a timer; `RunAtLoad` makes the first run happen at
72
+ * boot rather than one interval later.
73
+ *
74
+ * ⛔ NO `KeepAlive`. The bus daemon uses it because it is a long-lived server that should be restarted
75
+ * when it dies; a clock is a SHORT-LIVED command that exits after each tick, and `KeepAlive` on one
76
+ * tells launchd to relaunch it immediately, forever. The precedent is cited for the fact that a
77
+ * LaunchAgent is the thing that runs unattended here, not for its restart policy.
78
+ */
79
+ export function plistFor(args: {
80
+ label: string;
81
+ program: string;
82
+ programArgs: string[];
83
+ startIntervalSec: number;
84
+ stderrPath?: string;
85
+ stdoutPath?: string;
86
+ }): string {
87
+ const items = [args.program, ...args.programArgs].map((a) => ` <string>${esc(a)}</string>`).join("\n");
88
+ const err = args.stderrPath ? `\n <key>StandardErrorPath</key><string>${esc(args.stderrPath)}</string>` : "";
89
+ const out = args.stdoutPath ? `\n <key>StandardOutPath</key><string>${esc(args.stdoutPath)}</string>` : "";
90
+ return `<?xml version="1.0" encoding="UTF-8"?>
91
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
92
+ <plist version="1.0"><dict>
93
+ <key>Label</key><string>${esc(args.label)}</string>
94
+ <key>ProgramArguments</key><array>
95
+ ${items}
96
+ </array>
97
+ <key>StartInterval</key><integer>${args.startIntervalSec}</integer>
98
+ <key>RunAtLoad</key><true/>${err}${out}
99
+ </dict></plist>
100
+ `;
101
+ }
102
+
103
+ export type LoadState = { loaded: boolean; evidence: string };
104
+
105
+ /**
106
+ * IS THIS LABEL LOADED? Asked of launchd, never inferred from a file on disk.
107
+ *
108
+ * A plist in `~/Library/LaunchAgents` that nobody bootstrapped is exactly as inert as no plist at all,
109
+ * and it looks more installed. Measured on this machine: exit 0 for a loaded label, 113 for an absent
110
+ * one — both directions, before this was trusted.
111
+ */
112
+ export function isLoaded(label: string, run: Runner, uid?: number): LoadState {
113
+ const target = serviceTarget(label, uid);
114
+ const r = run("launchctl", ["print", target]);
115
+ if (r.status === 0) return { loaded: true, evidence: `launchctl print ${target} exited 0` };
116
+ const why = (r.stderr || r.stdout || "").trim().split("\n")[0] ?? "";
117
+ return { loaded: false, evidence: `launchctl print ${target} exited ${r.status}${why ? ` — ${why}` : ""}` };
118
+ }
119
+
120
+ export type InstallResult = {
121
+ ok: boolean;
122
+ label: string;
123
+ plistPath: string;
124
+ action: "bootstrapped" | "replaced" | "already-loaded" | "refused";
125
+ loaded: boolean;
126
+ evidence: string;
127
+ why?: string;
128
+ };
129
+
130
+ /**
131
+ * WRITE, BOOTSTRAP, THEN READ LAUNCHD BACK — and refuse if the label does not answer.
132
+ *
133
+ * `bootstrap` is not idempotent: bootstrapping an already-loaded label errors, so an existing unit is
134
+ * booted out first when its plist content differs, and left alone when it does not. The return says
135
+ * which of those happened, because "already-loaded" and "bootstrapped" are different facts and a
136
+ * caller that cannot tell them apart will report a no-op as an install.
137
+ */
138
+ export function installUnit(
139
+ spec: { label: string; program: string; programArgs: string[]; startIntervalSec: number; stderrPath?: string; stdoutPath?: string },
140
+ io: { run: Runner; fs: UnitFs; home?: string; uid?: number; store?: string },
141
+ ): InstallResult {
142
+ const { run, fs } = io;
143
+ const label = spec.label;
144
+ const plistPath = unitPath(label, io.home ?? homedir());
145
+ const target = serviceTarget(label, io.uid);
146
+ const wanted = plistFor(spec);
147
+ const before = isLoaded(label, run, io.uid);
148
+
149
+ const current = fs.existsSync(plistPath) ? fs.readFileSync(plistPath, "utf8") : null;
150
+ if (before.loaded && current === wanted) {
151
+ // The receipt is (re)written here too: a machine whose unit is loaded but whose receipt was lost
152
+ // would otherwise read as never-installed the moment the unit went away.
153
+ if (io.store) writeReceipt(io.store, { label, plistPath, at: new Date().toISOString(), program: spec.program }, fs);
154
+ return { ok: true, label, plistPath, action: "already-loaded", loaded: true, evidence: before.evidence };
155
+ }
156
+
157
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
158
+ fs.writeFileSync(plistPath, wanted);
159
+
160
+ let action: InstallResult["action"] = "bootstrapped";
161
+ if (before.loaded) {
162
+ run("launchctl", ["bootout", target]);
163
+ action = "replaced";
164
+ }
165
+ const boot = run("launchctl", ["bootstrap", `gui/${io.uid ?? ownUid()}`, plistPath]);
166
+
167
+ // ⛔ THE READBACK. A zero exit from `bootstrap` is not evidence the unit is loaded, and a non-zero
168
+ // one is not evidence it is absent — so the verdict comes from asking, either way.
169
+ const after = isLoaded(label, run, io.uid);
170
+ if (!after.loaded) {
171
+ return {
172
+ ok: false,
173
+ label,
174
+ plistPath,
175
+ action: "refused",
176
+ loaded: false,
177
+ evidence: after.evidence,
178
+ why:
179
+ `wrote ${plistPath} and ran launchctl bootstrap (exit ${boot.status}${(boot.stderr || "").trim() ? `: ${(boot.stderr || "").trim().split("\n")[0]}` : ""}), ` +
180
+ `but launchd does not report the label as loaded. NOTHING IS SCHEDULED. A plist on disk that was never ` +
181
+ `bootstrapped is exactly as inert as no plist at all, and it looks more installed.`,
182
+ };
183
+ }
184
+ if (io.store) {
185
+ writeReceipt(io.store, { label, plistPath, at: new Date().toISOString(), program: spec.program }, fs);
186
+ }
187
+ return { ok: true, label, plistPath, action, loaded: true, evidence: after.evidence };
188
+ }
189
+
190
+ /** Boot the label out and remove its plist. Reports what was actually there, not what was asked for. */
191
+ export function uninstallUnit(label: string, io: { run: Runner; fs: UnitFs; home?: string; uid?: number; store?: string }): {
192
+ ok: boolean;
193
+ label: string;
194
+ wasLoaded: boolean;
195
+ stillLoaded: boolean;
196
+ plistRemoved: boolean;
197
+ receiptDropped: boolean;
198
+ evidence: string;
199
+ } {
200
+ const plistPath = unitPath(label, io.home ?? homedir());
201
+ const before = isLoaded(label, io.run, io.uid);
202
+ if (before.loaded) io.run("launchctl", ["bootout", serviceTarget(label, io.uid)]);
203
+ const removed = io.fs.existsSync(plistPath);
204
+ if (removed) io.fs.rmSync(plistPath, { force: true });
205
+ const after = isLoaded(label, io.run, io.uid);
206
+ const receiptDropped = io.store ? dropReceipt(io.store, label, io.fs) : false;
207
+ return { ok: !after.loaded, label, wasLoaded: before.loaded, stillLoaded: after.loaded, plistRemoved: removed, receiptDropped, evidence: after.evidence };
208
+ }
209
+
210
+ /**
211
+ * EVERY UNIT THIS PACKAGE DECLARES, DERIVED FROM THE SCRIPTS THAT DECLARE THEM.
212
+ *
213
+ * ⛔ NOT A LIST IN THIS FILE. A guard whose population is a hardcoded list stops finding things — the
214
+ * same defect ⟨q-68b91bf7⟩ is open for in `packaging.mjs`, where a 14-name literal meant a new skill
215
+ * shipped unverified. A third clock added next month is covered here without anyone remembering to
216
+ * add it, and a clock whose label is renamed is followed automatically.
217
+ */
218
+ export function declaredUnits(scriptsDir: string, fs: Pick<UnitFs, "readFileSync"> & { readdirSync: (p: string) => string[] }): {
219
+ label: string;
220
+ script: string;
221
+ }[] {
222
+ const out: { label: string; script: string }[] = [];
223
+ let names: string[] = [];
224
+ try {
225
+ names = fs.readdirSync(scriptsDir);
226
+ } catch {
227
+ return out;
228
+ }
229
+ for (const name of names.sort()) {
230
+ if (!name.endsWith(".mjs")) continue;
231
+ let src = "";
232
+ try {
233
+ src = fs.readFileSync(path.join(scriptsDir, name), "utf8");
234
+ } catch {
235
+ continue;
236
+ }
237
+ // `const LABEL = "com.…"` is how both clocks declare theirs, and a declaration is a string
238
+ // literal assigned to LABEL — not any mention of a label, which comments also contain.
239
+ const m = /^\s*const\s+LABEL\s*=\s*["'`]([A-Za-z0-9.\-_]+)["'`]/m.exec(src);
240
+ if (m) out.push({ label: m[1], script: name });
241
+ }
242
+ return out;
243
+ }
244
+
245
+ /* ── the receipt: what THIS MACHINE has ever had installed ───────────────────────────────────────── */
246
+
247
+ /**
248
+ * ⛔ WHY A RECEIPT EXISTS AT ALL, because it is the difference between two states that look identical on
249
+ * disk and mean opposite things: a machine that NEVER installed a clock, and a machine where one was
250
+ * installed and has since stopped existing. The first is a decision nobody has taken yet — installing a
251
+ * LaunchAgent on someone's machine is theirs to authorise, and the last hop of an idle-fleet alarm is
252
+ * still David's open question. The second is a REGRESSION, and it is the case ⟨q-6d37c349⟩ was opened
253
+ * for: something that used to run stopped running and nothing said so for two days.
254
+ *
255
+ * So `installUnit` leaves a receipt and `uninstallUnit` removes it — an intentional uninstall is not a
256
+ * regression — and the check refuses only where a receipt and an absent unit disagree.
257
+ */
258
+ export const receiptPath = (store: string) => path.join(store, "clock-units.json");
259
+
260
+ export type Receipt = { label: string; plistPath: string; at: string; program: string };
261
+
262
+ /**
263
+ * ⛔ AN UNREADABLE RECEIPT IS NOT "NO RECEIPT", AND THE RETURN SHAPE IS WHAT MAKES THAT UNMISSABLE.
264
+ *
265
+ * The first version of this returned a bare `{}` on a parse failure with a comment claiming the caller
266
+ * would know — it would not have. Silently downgrading a corrupt receipt to never-installed turns the
267
+ * one case this check refuses (a clock that used to run and stopped) into the case it merely warns
268
+ * about. So `unreadable` is a field, and a caller that cannot answer says so instead of guessing.
269
+ */
270
+ export function readReceipts(
271
+ store: string,
272
+ fs: Pick<UnitFs, "existsSync" | "readFileSync">,
273
+ ): { receipts: Record<string, Receipt>; unreadable: string | null } {
274
+ const f = receiptPath(store);
275
+ if (!fs.existsSync(f)) return { receipts: {}, unreadable: null };
276
+ try {
277
+ const parsed = JSON.parse(fs.readFileSync(f, "utf8"));
278
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
279
+ return { receipts: {}, unreadable: `${f} does not hold a JSON object` };
280
+ }
281
+ return { receipts: parsed as Record<string, Receipt>, unreadable: null };
282
+ } catch (e) {
283
+ return { receipts: {}, unreadable: `${f} could not be parsed: ${(e as Error).message}` };
284
+ }
285
+ }
286
+
287
+ export function writeReceipt(store: string, r: Receipt, fs: Pick<UnitFs, "existsSync" | "readFileSync" | "writeFileSync" | "mkdirSync">): void {
288
+ const { receipts: all } = readReceipts(store, fs);
289
+ all[r.label] = r;
290
+ fs.mkdirSync(store, { recursive: true });
291
+ fs.writeFileSync(receiptPath(store), `${JSON.stringify(all, null, 2)}\n`);
292
+ }
293
+
294
+ export function dropReceipt(store: string, label: string, fs: Pick<UnitFs, "existsSync" | "readFileSync" | "writeFileSync" | "mkdirSync">): boolean {
295
+ const { receipts: all } = readReceipts(store, fs);
296
+ if (!(label in all)) return false;
297
+ delete all[label];
298
+ fs.mkdirSync(store, { recursive: true });
299
+ fs.writeFileSync(receiptPath(store), `${JSON.stringify(all, null, 2)}\n`);
300
+ return true;
301
+ }
302
+
303
+ /* ── loaded is not working ───────────────────────────────────────────────────────────────────────── */
304
+
305
+ export type LastRun = {
306
+ runs: number | null;
307
+ lastExitCode: number | null;
308
+ evidence: string;
309
+ unmeasured: string | null;
310
+ };
311
+
312
+ /**
313
+ * ⛔ DID ITS LAST RUN SUCCEED? A UNIT CAN BE LOADED AND FAILING EVERY TICK.
314
+ *
315
+ * MEASURED 2026-09-26, on my own mistake: an `--install` test of mine put this package's stall clock
316
+ * into launchd pointed at `--repo=/tmp/x`, and it fired 9 times over four hours, every 30 minutes, and
317
+ * FAILED every one. `check-clock-units-loaded` passed it the whole time, because it asked whether the
318
+ * unit was LOADED and the unit was loaded. The verb that caught it was `stall_clock_status`: "it is
319
+ * firing on schedule and measuring nothing, which reads as fresh and is not."
320
+ *
321
+ * ⛔ AND THE INSTRUMENT IS launchd's OWN RECORD, not a per-clock mark file. `launchctl print` reports
322
+ * `runs = <n>` and `last exit code = <n>` for every unit it has ever started — so this works for a
323
+ * clock that keeps no marks, and needs no map from label to log. A map would be the hardcoded-population
324
+ * defect again, one layer down.
325
+ *
326
+ * Three answers, and the third is not the second: never run yet (`runs = 0`, nothing to judge), ran and
327
+ * succeeded, ran and failed. A field that cannot be parsed is UNMEASURED and says so.
328
+ */
329
+ export function lastRun(label: string, run: Runner, uid?: number): LastRun {
330
+ const target = serviceTarget(label, uid);
331
+ const r = run("launchctl", ["print", target]);
332
+ if (r.status !== 0) {
333
+ return { runs: null, lastExitCode: null, evidence: `launchctl print ${target} exited ${r.status}`, unmeasured: "the unit is not loaded, so it has no run record here" };
334
+ }
335
+ const out = r.stdout ?? "";
336
+ // The FIRST match of each: `launchctl print` nests dictionaries, and the unit's own counters are at
337
+ // the top level. Taking the first is deliberate rather than incidental.
338
+ const runsM = /^\s*runs\s*=\s*(\d+)\s*$/m.exec(out);
339
+ const exitM = /^\s*last exit code\s*=\s*(-?\d+)\s*$/m.exec(out);
340
+ if (!runsM) {
341
+ return { runs: null, lastExitCode: null, evidence: `launchctl print ${target} exited 0 but reported no \`runs\` field`, unmeasured: "launchd did not report a run count — UNMEASURED, never assumed healthy" };
342
+ }
343
+ const runs = Number(runsM[1]);
344
+ const lastExitCode = exitM ? Number(exitM[1]) : null;
345
+ const evidence =
346
+ runs === 0
347
+ ? `launchctl print ${target}: runs = 0 — loaded and not yet fired`
348
+ : `launchctl print ${target}: runs = ${runs}, last exit code = ${lastExitCode === null ? "not reported" : lastExitCode}`;
349
+ return { runs, lastExitCode, evidence, unmeasured: runs > 0 && lastExitCode === null ? "launchd reported runs but no exit code — UNMEASURED" : null };
350
+ }
package/src/server.ts CHANGED
@@ -15,10 +15,12 @@ import { rotateSchema, rotateTool, rotateReconcileSchema, rotateReconcileTool }
15
15
  import { subscribeSchema, subscribeTool, unsubscribeSchema, unsubscribeTool, listSubscriptionsSchema, listSubscriptionsTool } from "./tools/events.js";
16
16
  import { scanRecordEventsSchema, scanRecordEventsTool } from "./tools/record-events.js";
17
17
  import { capabilitiesSchema, capabilitiesTool } from "./capabilities.js";
18
+ import { type AuthFault, logText, responseText, detailInResponse } from "./auth-faults.js";
18
19
  import {
19
20
  ensureDirs,
20
21
  getTokenMap,
21
22
  agentForBearer,
23
+ retiredBearerFor,
22
24
  tokenMapCensusLine,
23
25
  TOKENS_FILE,
24
26
  reloadTokenMapSync,
@@ -767,7 +769,7 @@ function buildServer(initialBound?: string, opts: { trackSession?: boolean } = {
767
769
 
768
770
  addTool(
769
771
  "subscribe",
770
- "Register for a record event: `task` (a phase checkbox newly ticked), `phase` (the last open box in a phase ticked), `item` (a queue item closed), or `pr` (a PR recorded in DONE.md). Events are DERIVED from the record and refused if the ref is not in it, so the stream can never claim something the authoritative markdown does not \u2014 but the TRIGGER is the record's COMMITTED CHANGE (`scan_record_events`), not any one verb, so a hand-edited DONE.md fires exactly like `land` does. Every offered kind has an emitter: the wire enum is generated from the emitter registry, so an unsatisfiable subscription cannot be registered. Re-subscribing returns the existing registration rather than a duplicate.",
772
+ "Register for a record event: `task` (a phase checkbox newly ticked), `phase` (the last open box in a phase ticked), `item` (a queue item closed), or `pr` (a PR recorded in DONE.md). Events are DERIVED from the record and refused if the ref is not in it, so the stream can never claim something the authoritative markdown does not \u2014 but the TRIGGER is the record's COMMITTED CHANGE (`scan_record_events`), not any one verb, so a hand-edited DONE.md fires exactly like `land` does. Every offered kind has an emitter: the wire enum is generated from the emitter registry, so an unsatisfiable subscription cannot be registered. Re-subscribing returns the existing registration rather than a duplicate. SCOPE picks how widely you are asking and is an explicit field rather than a magic target: `exact` (the default, and what every subscription stored before this field meant) matches one target; `kind` matches every event of the kind and REFUSES a target, because a target that was accepted and ignored would read narrower than it behaves; `prefix` matches targets starting with yours — which is how you watch a class whose ids do not exist yet, e.g. `davidbalzan/groundwork-kit#` for one repo's PRs. A prefix is over whatever `target` holds FOR THAT KIND (`pr` the full ref, `item` the queue id, `task` the KEY rather than the ticked line, `phase` the phase), and an EMPTY prefix is refused because it would match everything — that is the wildcard this field exists to avoid spelling in-band; ask for `kind` and say so.",
771
773
  subscribeSchema,
772
774
  gate("agentId", subscribeTool as (a: Record<string, unknown>) => Promise<unknown>),
773
775
  );
@@ -854,6 +856,11 @@ function loadTokenMap(initial: boolean): void {
854
856
  }
855
857
  }
856
858
 
859
+ /** The coord dir, for the bind-time log line that tells an operator where refusals are recorded. */
860
+ function coordRootForLog(): string {
861
+ return process.env.AGENT_COORD_DIR ?? `${process.env.HOME ?? "~"}/agent-coord`;
862
+ }
863
+
857
864
  async function main() {
858
865
  ensureDirs();
859
866
  loadTokenMap(true);
@@ -1117,8 +1124,12 @@ async function startHttp(port: number): Promise<void> {
1117
1124
  // Reverse-lookup: extract bearer from header, map → bound agent. Returns
1118
1125
  // undefined if no map is configured (advisory mode); throws-like return of
1119
1126
  // null if the bearer doesn't match any known agent (caller responds 401).
1120
- function resolveBoundAgent(authHeader: string | undefined): { ok: boolean; agent?: string } {
1121
- if (!authHeader || !authHeader.startsWith("Bearer ")) return { ok: false };
1127
+ function resolveBoundAgent(authHeader: string | undefined): { ok: boolean; agent?: string; fault?: AuthFault } {
1128
+ // ⟨q-a441f1e1⟩ The refusal now carries WHICH fault it was. The caller is told only about the
1129
+ // SHAPE of its request; everything about a credential's validity is uniform to the caller and
1130
+ // detailed in the daemon log — see src/auth-faults.ts for why that split is where it is.
1131
+ if (!authHeader) return { ok: false, fault: { kind: "no-header" } };
1132
+ if (!authHeader.startsWith("Bearer ")) return { ok: false, fault: { kind: "not-bearer" } };
1122
1133
  const bearer = authHeader.slice("Bearer ".length);
1123
1134
  // ⟨q-54adf191⟩ Pick up a tokens.json that changed while this daemon ran. Rate-limited, so the
1124
1135
  // steady state is one stat; the file is read only when its stamp moved. Every outcome that is
@@ -1143,10 +1154,16 @@ async function startHttp(port: number): Promise<void> {
1143
1154
  // actually connects gets a tail, so a 20-agent map does not spawn 20 timers for seats that
1144
1155
  // may not exist. Idempotent — steady state is one Map lookup per request.
1145
1156
  if (agent) startTailOnBind(agent);
1146
- return agent ? { ok: true, agent } : { ok: false };
1157
+ if (agent) return { ok: true, agent };
1158
+ if (bearer === "") return { ok: false, fault: { kind: "empty-bearer" } };
1159
+ // Retired is asked only AFTER the forced re-read above: a bearer minted seconds ago must not
1160
+ // be reported as retired because this process had not read the file yet.
1161
+ const retired = retiredBearerFor(bearer);
1162
+ return { ok: false, fault: retired ? { kind: "retired-bearer", retired } : { kind: "unknown-bearer" } };
1147
1163
  }
1148
1164
  // Advisory mode: only check the shared bearer matches.
1149
- return sharedExpected && authHeader === sharedExpected ? { ok: true } : { ok: false };
1165
+ if (sharedExpected && authHeader === sharedExpected) return { ok: true };
1166
+ return { ok: false, fault: { kind: "advisory-mismatch" } };
1150
1167
  }
1151
1168
 
1152
1169
  // ⛔ ONE HANDLER, N SERVERS. Node binds a single address per `http.Server`, so a list of
@@ -1171,8 +1188,13 @@ async function startHttp(port: number): Promise<void> {
1171
1188
  // observe traffic; TLS termination is the answer to that.
1172
1189
  const resolved = resolveBoundAgent(req.headers.authorization);
1173
1190
  if (!resolved.ok) {
1191
+ // ⟨q-a441f1e1⟩ The LOG is where an operator diagnosing "my seat stopped working" looks, and
1192
+ // it distinguishes every fault. The RESPONSE names only what the caller already knows about
1193
+ // its own request. Neither ever contains any part of a bearer.
1194
+ const fault = resolved.fault ?? { kind: "unknown-bearer" as const };
1195
+ console.error(`[agent-coord-mcp] ${logText(fault)}`);
1174
1196
  res.writeHead(401, { "Content-Type": "text/plain", "WWW-Authenticate": "Bearer" });
1175
- res.end("unauthorized\n");
1197
+ res.end(`${responseText(fault)}\n`);
1176
1198
  return;
1177
1199
  }
1178
1200
 
@@ -1217,6 +1239,25 @@ async function startHttp(port: number): Promise<void> {
1217
1239
  {
1218
1240
  const startupCensus = tokenMapCensusLine();
1219
1241
  if (startupCensus) console.error(startupCensus);
1242
+ // ⛔ ⟨q-a441f1e1⟩ A BUS RUNNING WITH THE ORACLE ON SAYS SO, AT BIND. qa: the switch existed and
1243
+ // NOTHING announced it — no startup line, no warning — so a daemon could distinguish validity
1244
+ // faults to unauthenticated callers while neither its log nor its capabilities mentioned it.
1245
+ // An operator must not have to read the environment of a running process to learn this.
1246
+ // ⭐ ⟨q-a441f1e1⟩ WHERE THE WITHHELD REASONS GO, stated at bind so an operator has the exact
1247
+ // destination without it ever appearing in a 401 body. The response points at "this bus's daemon
1248
+ // log" by role; this line is what turns that role into a path for the person entitled to it.
1249
+ console.error(
1250
+ `[agent-coord-mcp] 401 reasons (unknown / retired / advisory-mismatch bearers) are written HERE, to this ` +
1251
+ `process's stderr — pid ${process.pid}, agent-coord dir ${coordRootForLog()}. A refused caller is told to ` +
1252
+ `look here and is told nothing else.`,
1253
+ );
1254
+ if (detailInResponse()) {
1255
+ console.error(
1256
+ `[agent-coord-mcp] ⛔ AGENT_COORD_401_DETAIL=response — this bus tells an UNAUTHENTICATED caller ` +
1257
+ `whether a bearer was never minted, or was genuine and retired, and when. That confirms a real ` +
1258
+ `credential to whoever just failed to authenticate. Unset it to keep the detail in this log only.`,
1259
+ );
1260
+ }
1220
1261
  }
1221
1262
  await Promise.all(
1222
1263
  bindAddrs.map(
package/src/store.ts CHANGED
@@ -392,8 +392,30 @@ export type TokenMapRefresh = {
392
392
  count: number | null;
393
393
  /** Present only when the reload was REFUSED; the previous map still serves. */
394
394
  refused?: string;
395
+ /**
396
+ * ⟨q-a441f1e1⟩ Bearers that were in the map before this reload and are not in it after — the only
397
+ * moment the daemon can ever observe a retirement, because nothing on disk records one.
398
+ * Hashes, never secrets. Empty unless `changed` is true.
399
+ */
400
+ retired?: RetiredHash[];
395
401
  };
396
402
 
403
+ /** A bearer hash that left the map, with the agent it belonged to. ⛔ A HASH, NEVER A BEARER. */
404
+ export type RetiredHash = { hash: string; agentId: string };
405
+
406
+ /**
407
+ * ⟨q-a441f1e1⟩ Which entries LEFT the map. Pure, and separate from the reload decision so it can be
408
+ * exercised on posed maps: a rotation (`coord-token add` on an existing agent) and a revoke
409
+ * (`coord-token revoke`) both present here as a hash that is gone, and they are told apart by
410
+ * whether the agent still holds some other key afterwards.
411
+ */
412
+ export function retiredSince(before: Map<string, string> | null, after: Map<string, string> | null): RetiredHash[] {
413
+ if (!before || !after) return [];
414
+ const out: RetiredHash[] = [];
415
+ for (const [hash, agentId] of before) if (!after.has(hash)) out.push({ hash, agentId });
416
+ return out;
417
+ }
418
+
397
419
  // The file identity we compare against: mtime, size AND inode.
398
420
  //
399
421
  // ⛔ THE INODE IS LOAD-BEARING, NOT BELT-AND-BRACES. `coord-token` rotates by writing a temp file and
@@ -463,7 +485,12 @@ export function decideTokenMapRefresh(
463
485
  }
464
486
  return {
465
487
  next: { map: next, stamp, checkedAt: now },
466
- result: { changed: true, reason: state.stamp === null ? "appeared" : "changed", count: next.size },
488
+ result: {
489
+ changed: true,
490
+ reason: state.stamp === null ? "appeared" : "changed",
491
+ count: next.size,
492
+ retired: retiredSince(state.map, next),
493
+ },
467
494
  };
468
495
  } catch (e) {
469
496
  // The stamp is deliberately NOT advanced: a half-written file that is fixed in place, with no
@@ -472,6 +499,58 @@ export function decideTokenMapRefresh(
472
499
  }
473
500
  }
474
501
 
502
+ /**
503
+ * ⟨q-a441f1e1⟩ — WHAT THIS PROCESS WATCHED LEAVE THE TOKEN MAP. Hash → who held it and when it went.
504
+ *
505
+ * ⛔ IN MEMORY, DELIBERATELY, AND IT FORGETS ON RESTART. Nothing on disk records a superseded token:
506
+ * `coord-token add` overwrites an agent's entry and `revoke` deletes it, so after either the old
507
+ * bearer is simply absent — byte-for-byte identical to one that never existed. The ONLY moment the
508
+ * difference is observable is the reload that removes it, which this process is already doing.
509
+ * Persisting it would mean a new file or a schema change (⟨q-4b9cdeab⟩'s `previous` is documented in
510
+ * a comment and written by NOTHING), and release A writes neither form of token. So:
511
+ * · a bearer retired while this daemon was RUNNING is reported as retired;
512
+ * · a bearer retired while it was DOWN reads as unknown, and that is the honest answer — this
513
+ * process never saw it valid. A test asserts that limit rather than only the happy path.
514
+ * Hashes only: a retirement record is never a place a secret appears.
515
+ */
516
+ const RETIRED_MAX = 512;
517
+ const retiredBearers = new Map<string, RetiredBearerRecord>();
518
+ export type RetiredBearerRecord = { agentId: string; at: number; cause: "replaced" | "revoked" };
519
+
520
+ /** Record what a reload removed. `cause` is derived, not guessed: see `retirementCause`. */
521
+ export function noteRetired(entries: RetiredHash[], after: Map<string, string> | null, now = Date.now()): void {
522
+ for (const { hash, agentId } of entries) {
523
+ retiredBearers.set(hash, { agentId, at: now, cause: retirementCause(agentId, after) });
524
+ // Bounded so a long-lived daemon rotating often cannot grow this without limit. Insertion order
525
+ // is iteration order for a Map, so the oldest record is the one that goes.
526
+ if (retiredBearers.size > RETIRED_MAX) {
527
+ const oldest = retiredBearers.keys().next();
528
+ if (!oldest.done) retiredBearers.delete(oldest.value);
529
+ }
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Replaced or revoked? The agent still holding a key after the reload was ROTATED; an agent with no
535
+ * key left was REVOKED. Derived from the map itself rather than from which command an operator ran,
536
+ * because the daemon never sees the command.
537
+ */
538
+ export function retirementCause(agentId: string, after: Map<string, string> | null): "replaced" | "revoked" {
539
+ if (!after) return "revoked";
540
+ for (const held of after.values()) if (held === agentId) return "replaced";
541
+ return "revoked";
542
+ }
543
+
544
+ /** What this process knows about a presented bearer that is no longer live, or undefined. */
545
+ export function retiredBearerFor(bearer: string): RetiredBearerRecord | undefined {
546
+ return retiredBearers.get(bearerHash(bearer));
547
+ }
548
+
549
+ /** Test seam only: forget every retirement, so a case can pose an empty process memory. */
550
+ export function clearRetiredBearers(): void {
551
+ retiredBearers.clear();
552
+ }
553
+
475
554
  export function refreshTokenMapIfChanged(opts: { force?: boolean; now?: number } = {}): TokenMapRefresh {
476
555
  if (!tokenMapInitialized) getTokenMap();
477
556
  const { next, result } = decideTokenMapRefresh(
@@ -479,6 +558,8 @@ export function refreshTokenMapIfChanged(opts: { force?: boolean; now?: number }
479
558
  { stamp: stampOf, read: readTokenMapSync },
480
559
  opts,
481
560
  );
561
+ // Before the cache moves: everything the old map held and the new one does not is retired.
562
+ if (result.retired?.length) noteRetired(result.retired, next.map, opts.now);
482
563
  tokenMapCache = next.map;
483
564
  tokenMapStamp = next.stamp;
484
565
  tokenMapCheckedAt = next.checkedAt;
@@ -729,6 +810,70 @@ function sanitize(id: string): string {
729
810
  return id.replace(/[^a-zA-Z0-9._-]/g, "_");
730
811
  }
731
812
 
813
+ /* ── ⟨q-524305dd⟩ ONE DELIVERER PER (SEAT, SOURCE) AT A TIME, ACROSS PROCESSES ─────────────────────
814
+ Two paths type a room message into a herdr pane: the SENDER's server at send time (herdr-delivery)
815
+ and the RECIPIENT's own tail (herdr-tail). Each read the push cursor, typed, waited seconds to
816
+ verify the screen, and only then advanced it; the other read the same not-yet-advanced cursor in
817
+ that window and typed the message again. Measured 2026-09-24: 6 of 39 room messages reached one
818
+ seat's pane twice, 2.7–3.5s apart. The only guard was a closure-local `inFlight` flag — per
819
+ PROCESS, against a race between processes.
820
+ So read → type → verify → advance runs under a lock BOTH paths can see: a directory lock
821
+ (proper-lockfile, the same one appendJsonl uses) per seat and source.
822
+ ⛔ A holder that DIES must not deafen the seat. The lock's mtime is refreshed while its owner's
823
+ event loop runs, and it is STALE after DELIVERY_LOCK_STALE_MS without a refresh, so a killed sender
824
+ costs a bounded stall, never a permanent one. It must outlast the slowest push: herdr's verify
825
+ sleeps by spinning, which blocks the refresh for the push's whole duration.
826
+ ⛔ A compromised lock is LOGGED, never thrown: the library's default throws asynchronously, which
827
+ would take the bus server down with it. */
828
+ const envMs = (name: string, fallback: number, min: number) => { const n = Number(process.env[name]); return Number.isFinite(n) && n >= min ? Math.floor(n) : fallback; };
829
+ export const DELIVERY_LOCK_STALE_MS = envMs("AGENT_COORD_DELIVERY_LOCK_STALE_MS", 60_000, 2_000);
830
+ export const DELIVERY_LOCK_WAIT_MS = envMs("AGENT_COORD_DELIVERY_LOCK_WAIT_MS", 30_000, 0);
831
+ export const DELIVERY_LOCK_DIR = path.join(ROOT, "delivery-locks");
832
+ export const deliveryLockPath = (agentId: string, sourceKey: string) => path.join(DELIVERY_LOCK_DIR, `${sanitize(agentId)}--${sanitize(sourceKey)}`);
833
+ export type DeliveryLockOutcome<T> = { ran: true; value: T } | { ran: false; why: string };
834
+
835
+ /**
836
+ * Run `fn` as the only deliverer to `agentId`'s `sourceKey` ("dm" or "room:<chan>") on this bus.
837
+ * `wait: false` (a tail tick) answers `ran: false` at once when another deliverer holds it — the next
838
+ * tick re-reads the cursor. `wait: true` (the send-time push) waits up to DELIVERY_LOCK_WAIT_MS rather
839
+ * than skipping, because the seat may have NO tail (⟨q-cdb5b007⟩) and a skipped send-time push would
840
+ * then be a silent drop. The lock is released on every path out of `fn`, including a throw.
841
+ */
842
+ export async function withDeliveryLock<T>(
843
+ agentId: string,
844
+ sourceKey: string,
845
+ fn: () => Promise<T>,
846
+ opts: { wait: boolean; log?: (line: string) => void },
847
+ ): Promise<DeliveryLockOutcome<T>> {
848
+ const target = deliveryLockPath(agentId, sourceKey);
849
+ // A compromise or a failed release is ALWAYS logged — to the caller's sink, or stderr when none is given.
850
+ // (Not an optional call: a lock event that is silently dropped is the invisible failure this lock exists to end.)
851
+ const log = opts.log ?? ((line: string) => console.error(line));
852
+ mkdirSync(DELIVERY_LOCK_DIR, { recursive: true });
853
+ const deadline = Date.now() + (opts.wait ? DELIVERY_LOCK_WAIT_MS : 0);
854
+ let release: () => Promise<void>;
855
+ for (;;) {
856
+ try {
857
+ release = await lockfile.lock(target, {
858
+ realpath: false,
859
+ stale: DELIVERY_LOCK_STALE_MS,
860
+ retries: 0,
861
+ onCompromised: (e: Error) => log(`[delivery-lock] ${sourceKey} for ${agentId} was COMPROMISED (${e.message}) — another deliverer may have taken it over; logged, not thrown`),
862
+ });
863
+ break;
864
+ } catch (e) {
865
+ if ((e as NodeJS.ErrnoException).code !== "ELOCKED") return { ran: false, why: `delivery lock unavailable: ${(e as Error).message}` };
866
+ if (Date.now() >= deadline) return { ran: false, why: `another deliverer holds ${sourceKey} for ${agentId}` };
867
+ await new Promise((r) => setTimeout(r, 40 + Math.floor(Math.random() * 60)));
868
+ }
869
+ }
870
+ try {
871
+ return { ran: true, value: await fn() };
872
+ } finally {
873
+ try { await release(); } catch (e) { log(`[delivery-lock] release of ${sourceKey} for ${agentId} failed: ${(e as Error).message}`); }
874
+ }
875
+ }
876
+
732
877
  export async function rewriteJsonl<T>(
733
878
  file: string,
734
879
  filter: (entry: T) => boolean
@@ -0,0 +1,21 @@
1
+ // ⟨q-f2cf9e0f⟩ — HOW A PASSING TEST IS ATTRIBUTED TO A FILE, in src/ so a probe can CALL it.
2
+ //
3
+ // `check-test-count` compares per-file pass counts against a committed record, and both the record's
4
+ // keys and the glob's selections are paths relative to the package. The attribution key has to be
5
+ // the SAME thing or the comparison is between two different namespaces.
6
+ //
7
+ // ⛔ IT WAS `test/${basename(file)}`, WHICH FLATTENS. Every test file sat directly in `test/`, so the
8
+ // flattened key and the real one were identical and the defect was invisible — until a suite moved to
9
+ // `test/live/`, whose passes then landed on the phantom key `test/<name>` while the real entry read
10
+ // zero. A per-file guard that mis-keys is worse than no guard: it reports drift that is not there and
11
+ // hides drift that is.
12
+ //
13
+ // Lives in src/ rather than beside the script for the reason measured in ⟨q-0c5476ec⟩: a probe in
14
+ // dist/ importing out of scripts/ breaks any install that ships dist without scripts.
15
+ import path from "node:path";
16
+
17
+ /** The record/glob key for a test file: its path relative to the package, POSIX-separated. */
18
+ export function testCountKey(pkgRoot: string, file: string | undefined): string {
19
+ if (!file) return "(unattributed)";
20
+ return path.relative(pkgRoot, file).split(path.sep).join("/");
21
+ }