agent-coord-mcp 0.26.23 → 0.26.25

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 (63) hide show
  1. package/dist/capabilities.js +180 -5
  2. package/dist/capabilities.js.map +1 -1
  3. package/dist/gated-head.js +172 -6
  4. package/dist/gated-head.js.map +1 -1
  5. package/dist/server-spread.js +60 -53
  6. package/dist/server-spread.js.map +1 -1
  7. package/dist/server.js +41 -2
  8. package/dist/server.js.map +1 -1
  9. package/dist/tools/away.js +20 -2
  10. package/dist/tools/away.js.map +1 -1
  11. package/dist/tools/herdr-delivery.js +86 -26
  12. package/dist/tools/herdr-delivery.js.map +1 -1
  13. package/dist/tools/herdr-tail.js +221 -0
  14. package/dist/tools/herdr-tail.js.map +1 -0
  15. package/dist/tools/jsonl-offsets.js +37 -0
  16. package/dist/tools/jsonl-offsets.js.map +1 -0
  17. package/dist/tools/messaging.js +18 -0
  18. package/dist/tools/messaging.js.map +1 -1
  19. package/dist/tools/queue-write.js +4 -1
  20. package/dist/tools/queue-write.js.map +1 -1
  21. package/dist/tools/record-events.js +42 -4
  22. package/dist/tools/record-events.js.map +1 -1
  23. package/dist/tools/records.js +24 -5
  24. package/dist/tools/records.js.map +1 -1
  25. package/dist/tools/registry.js +56 -21
  26. package/dist/tools/registry.js.map +1 -1
  27. package/dist/tools/stall.js +73 -9
  28. package/dist/tools/stall.js.map +1 -1
  29. package/dist/tools/tick.js +77 -0
  30. package/dist/tools/tick.js.map +1 -0
  31. package/dist/tools/transport.js +4 -3
  32. package/dist/tools/transport.js.map +1 -1
  33. package/dist/transports/herdr.js +203 -19
  34. package/dist/transports/herdr.js.map +1 -1
  35. package/dist/transports/index.js +1 -1
  36. package/dist/transports/index.js.map +1 -1
  37. package/dist/transports/types.js +41 -0
  38. package/dist/transports/types.js.map +1 -1
  39. package/hooks/control-bytes.mjs +69 -0
  40. package/hooks/submit.mjs +227 -9
  41. package/hooks/tier.mjs +7 -1
  42. package/hooks/tmux-pusher.mjs +5 -1
  43. package/package.json +1 -1
  44. package/scripts/coord-pusher.mjs +5 -1
  45. package/src/capabilities.ts +183 -4
  46. package/src/gated-head.ts +193 -6
  47. package/src/server-spread.ts +52 -4
  48. package/src/server.ts +38 -1
  49. package/src/tools/away.ts +38 -2
  50. package/src/tools/herdr-delivery.ts +92 -24
  51. package/src/tools/herdr-tail.ts +244 -0
  52. package/src/tools/jsonl-offsets.ts +29 -0
  53. package/src/tools/messaging.ts +20 -0
  54. package/src/tools/queue-write.ts +4 -1
  55. package/src/tools/record-events.ts +46 -4
  56. package/src/tools/records.ts +24 -4
  57. package/src/tools/registry.ts +56 -20
  58. package/src/tools/stall.ts +80 -12
  59. package/src/tools/tick.ts +117 -0
  60. package/src/tools/transport.ts +5 -4
  61. package/src/transports/herdr.ts +251 -19
  62. package/src/transports/index.ts +1 -1
  63. package/src/transports/types.ts +71 -0
@@ -0,0 +1,244 @@
1
+ /**
2
+ * ⛔ A HERDR SEAT WAS DEAF TO EVERY TMUX SEAT, AND THE REASON IS A SINGLETON.
3
+ *
4
+ * `deliverToHerdrSeats` runs at SEND time and returns early unless `activeTransport().kind` is
5
+ * HERDR. `activeTransport()` returns a module-level singleton wired once at startup from the
6
+ * SENDER's own config — so that early return can only ever see the SENDER's transport, and a tmux
7
+ * seat's server could never type into a herdr pane. Measured both ways: `herdr → herdr` typed and
8
+ * woke the seat; `tmux → herdr` reached the inbox file and was never typed.
9
+ *
10
+ * ⚠ THE EASIER FIX IS THE WRONG ONE: letting the SENDER drive herdr per recipient passes on a
11
+ * one-machine fleet and fails silently and asymmetrically on two, because every sender would need
12
+ * the `herdr` binary on its own host. So the duty goes to the seat that needs waking — the only
13
+ * party guaranteed to be where it is.
14
+ *
15
+ * ⛔⛆ AND THE FIRST VERSION OF THIS FILE WAS INERT ON EVERY SEAT, INCLUDING ITS AUTHOR'S.
16
+ * `8363968` started the tail only when `AGENT_COORD_BOUND_AGENT` was set at startup. No seat on
17
+ * this fleet sets it — identity binds at `join` — so the coordinator measured "herdr tail NOT
18
+ * started" on the PR's own server and qa read the variable unset on all six live seat servers.
19
+ * Its eleven tests injected the agent id and so could not see it. The tail now starts from the
20
+ * IDENTITY BINDING (`startTailOnBind`), idempotently, one per agent per process.
21
+ *
22
+ * ⛔ AND IT MUST NOT RUN THE REAPER. That version called `loadLiveTransports()` on every tick —
23
+ * the loader that DELETES markers it judges dead, named an hour earlier as the vanishing-marker
24
+ * mechanism. A reaper in a once-a-second loop on every seat is not shippable on the argument that
25
+ * the current build usually keeps the marker. This file reads its own seat's marker FILE directly,
26
+ * read-only, and never asks about anyone else's.
27
+ *
28
+ * THE CURSOR RULE IS THE PUSHER'S (⟨q-7be94b5e⟩): advance the PUSH cursor only past what was
29
+ * VERIFIABLY typed. Two corrections from qa's read of `8363968`, both now driven by tests:
30
+ * · the cursor advances to the byte AFTER THE DELIVERED LINE, never to the file's size — sizing
31
+ * it skipped a later message in the same tick whose paste then failed;
32
+ * · a cursor write that FAILS is held, and an in-process high-water mark stops the same message
33
+ * being re-typed on the next tick — the directory fix closed one cause of a paste loop, this
34
+ * closes the class.
35
+ *
36
+ * IDLE COST, COUNTED BY TESTS RATHER THAN ASSERTED HERE: when no tailed file has grown, a tick is
37
+ * one `stat()` per tailed file and nothing else. The marker and room membership are re-read every
38
+ * `REFRESH_TICKS` ticks, and no herdr subprocess runs until there is something to type.
39
+ */
40
+ import { readFileSync, statSync } from "node:fs";
41
+ import { inboxFile, roomFile, getRooms, transportFile } from "../store.js";
42
+ import { activeTransport, HERDR, type Transport, type TransportMarker } from "../transports/index.js";
43
+ import { renderForPane, advancePushCursor, readPushCursorFor, type Message } from "./herdr-delivery.js";
44
+ import { newMessagesIn } from "./jsonl-offsets.js";
45
+
46
+ export const DEFAULT_POLL_MS = 1000;
47
+ /** Marker and room membership are re-read this often. A room joined mid-session is picked up within this many ticks. */
48
+ export const REFRESH_TICKS = 30;
49
+
50
+ type Source = { kind: "dm" | "room"; chan?: string; file: string };
51
+ export type TailOutcome = {
52
+ delivered: { kind: "dm" | "room"; chan?: string; id: string }[];
53
+ held: { kind: "dm" | "room"; chan?: string; id: string; why: string }[];
54
+ idle?: string;
55
+ };
56
+
57
+ /** Per-agent memory that survives between ticks — what makes the idle path cheap and the paste loop impossible. */
58
+ export type TailContext = {
59
+ tick: number;
60
+ marker: TransportMarker | null;
61
+ rooms: string[];
62
+ /** In-process high-water mark per source: never re-type below this, even if the persistent cursor could not be written. */
63
+ hw: Map<string, number>;
64
+ /** Last seen size per source: a source that has not grown costs one stat. */
65
+ sizes: Map<string, number>;
66
+ };
67
+ export const newContext = (): TailContext => ({ tick: 0, marker: null, rooms: [], hw: new Map(), sizes: new Map() });
68
+
69
+ const keyOf = (s: { kind: string; chan?: string }) => (s.kind === "dm" ? "dm" : `room:${s.chan}`);
70
+ const sizeOf = (file: string): number => {
71
+ try { return statSync(file).size; } catch { return 0; }
72
+ };
73
+
74
+ /** Read THIS seat's own marker, read-only. Never the reaping loader. */
75
+ export function readOwnMarker(agentId: string): TransportMarker | null {
76
+ try { return JSON.parse(readFileSync(transportFile(agentId), "utf8")) as TransportMarker; } catch { return null; }
77
+ }
78
+
79
+ /** The shared line reader — re-exported so callers of this module keep one import. */
80
+ export { newMessagesIn } from "./jsonl-offsets.js";
81
+
82
+ async function offsetOf(agentId: string, src: Source, ctx: TailContext): Promise<number> {
83
+ const c = await readPushCursorFor(agentId);
84
+ const persisted = src.kind === "dm" ? Number(c.inboxOffset ?? 0) : Number(((c.roomOffsets as Record<string, number> | undefined) ?? {})[src.chan!] ?? 0);
85
+ return Math.max(persisted, ctx.hw.get(keyOf(src)) ?? 0);
86
+ }
87
+
88
+ /** One pass. Everything that talks to the outside world is injectable, and nothing here schedules itself. */
89
+ export async function tailOnce(
90
+ agentId: string,
91
+ opts: {
92
+ transport?: Transport;
93
+ ctx?: TailContext;
94
+ markerOf?: (agentId: string) => TransportMarker | null;
95
+ roomsOf?: () => Promise<Record<string, { members?: string[] }>>;
96
+ } = {},
97
+ ): Promise<TailOutcome> {
98
+ const out: TailOutcome = { delivered: [], held: [] };
99
+ const t = opts.transport ?? activeTransport();
100
+ if (!t || t.kind !== HERDR) return { ...out, idle: "this server's transport is not herdr — the tail is for a herdr seat's own inbox" };
101
+ const ctx = opts.ctx ?? newContext();
102
+ const refresh = ctx.tick % REFRESH_TICKS === 0;
103
+ ctx.tick += 1;
104
+
105
+ // A seat with NO marker re-reads it only on the refresh cadence too. The coordinator's gate found
106
+ // `|| !ctx.marker` here, which made a markerless seat — the exact state a vanished marker leaves —
107
+ // read its marker file on every tick while the stated idle cost said otherwise.
108
+ if (refresh) {
109
+ ctx.marker = (opts.markerOf ?? readOwnMarker)(agentId);
110
+ if (ctx.marker && ctx.marker.transport === HERDR && ctx.marker.rooms !== false) {
111
+ try {
112
+ const all = await (opts.roomsOf ?? getRooms)();
113
+ ctx.rooms = Object.entries(all ?? {}).filter(([, r]) => (r?.members ?? []).includes(agentId)).map(([chan]) => chan);
114
+ } catch { ctx.rooms = []; }
115
+ } else {
116
+ ctx.rooms = [];
117
+ }
118
+ }
119
+ const marker = ctx.marker;
120
+ if (!marker || marker.transport !== HERDR) return { ...out, idle: `${agentId} has no herdr marker on this bus — nothing to type into (re-checked every ${REFRESH_TICKS} ticks)` };
121
+
122
+ const sources: Source[] = [{ kind: "dm", file: inboxFile(agentId) }, ...ctx.rooms.map((chan) => ({ kind: "room" as const, chan, file: roomFile(chan) }))];
123
+
124
+ for (const src of sources) {
125
+ const size = sizeOf(src.file);
126
+ const key = keyOf(src);
127
+ // ⭐ THE IDLE PATH: a source that has not grown since the last tick costs this one stat.
128
+ if (ctx.sizes.get(key) === size) continue;
129
+ ctx.sizes.set(key, size);
130
+ const from = await offsetOf(agentId, src, ctx);
131
+ if (size <= from) continue;
132
+
133
+ for (const { msg, end } of newMessagesIn(src.file, from)) {
134
+ if (msg.from === agentId) { ctx.hw.set(key, end); continue; }
135
+ const where = src.kind === "dm" ? ({ kind: "dm" } as const) : ({ kind: "room", chan: src.chan! } as const);
136
+ const rendered = await renderForPane(msg as unknown as Message, agentId, where);
137
+ let r: { delivered: boolean; error?: string; verified?: boolean; safeToRetry?: boolean; outcome?: string };
138
+ try { r = await t.push(marker, rendered); } catch (e) { r = { delivered: false, safeToRetry: false, error: (e as Error).message }; }
139
+ if (!r.delivered && r.safeToRetry === true) {
140
+ // HELD: the transport sent NO key (no ready input box, a draft, a dialog, an unreadable pane).
141
+ // Nothing moves and the message is retried next tick, which costs only a screen read. Stop
142
+ // this source so no later message is typed past one that did not land.
143
+ out.held.push({ kind: src.kind, chan: src.chan, id: String(msg.id ?? ""), why: r.error ?? "held — nothing was typed" });
144
+ ctx.sizes.delete(key); // force a re-read next tick
145
+ break;
146
+ }
147
+ // ⛔ ANYTHING SHORT OF A VERIFIED DELIVERY DOES NOT MOVE THE PERSISTENT CURSOR (⟨q-15d763dc⟩).
148
+ // Keys may have reached the pane (text typed, an Enter pressed) without the screen proving the
149
+ // message was submitted, so it stays owed on disk: read_messages still serves it and a restart
150
+ // re-offers it. The in-process high-water mark alone stops it being re-typed every tick, since
151
+ // re-typing a message that may already be sitting in the box is the paste loop in another coat.
152
+ // A throw lands here too: nothing says whether a key went out before it.
153
+ if (!r.delivered || r.verified !== true) {
154
+ ctx.hw.set(key, end);
155
+ out.held.push({ kind: src.kind, chan: src.chan, id: String(msg.id ?? ""), why: `${r.outcome ?? (r.delivered ? "unverified" : "not delivered")} — ${r.error ?? "not verified"}; the persistent cursor was not moved (q-15d763dc)` });
156
+ break;
157
+ }
158
+ // It landed and was verified. Record that in-process FIRST, so a cursor write that fails
159
+ // cannot turn one delivery into a paste loop, then persist exactly past this line.
160
+ ctx.hw.set(key, end);
161
+ const persisted = await advancePushCursor(agentId, where, end);
162
+ out.delivered.push({ kind: src.kind, chan: src.chan, id: String(msg.id ?? "") });
163
+ if (!persisted) {
164
+ out.held.push({ kind: src.kind, chan: src.chan, id: String(msg.id ?? ""), why: "delivered, but the push cursor could not be written — held in memory so it is not re-typed; a restart before the next successful write may type it once more" });
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ return out;
170
+ }
171
+
172
+ // ─── the per-process registry of running tails ─────────────────────────────
173
+
174
+ type TailRecord = {
175
+ agentId: string;
176
+ startedAt: string;
177
+ ticks: number;
178
+ delivered: number;
179
+ held: number;
180
+ lastTickAt: string | null;
181
+ lastWhy: string | null;
182
+ stop: () => void;
183
+ };
184
+ const running = new Map<string, TailRecord>();
185
+
186
+ /** What `capabilities` reports: the tails THIS PROCESS is actually running, not the ones it could run. */
187
+ export function herdrTailState(): { agentId: string; running: true; startedAt: string; ticks: number; delivered: number; held: number; lastTickAt: string | null; lastWhy: string | null }[] {
188
+ return [...running.values()].map(({ stop: _stop, ...r }) => ({ ...r, running: true as const }));
189
+ }
190
+
191
+ export function stopHerdrTail(agentId: string): boolean {
192
+ const r = running.get(agentId);
193
+ if (!r) return false;
194
+ r.stop();
195
+ running.delete(agentId);
196
+ return true;
197
+ }
198
+ export function stopAllHerdrTails(): void {
199
+ for (const id of [...running.keys()]) stopHerdrTail(id);
200
+ }
201
+
202
+ /**
203
+ * Start the tail for `agentId` if this server's transport is herdr. IDEMPOTENT: a second bind of
204
+ * the same agent in the same process returns `running: true, started: false`.
205
+ *
206
+ * ⛔ NEVER OVERLAPS ITSELF: a tick still typing when the timer fires would interleave two pastes
207
+ * into one pane; the guard is one in-flight flag, because the next tick re-reads the cursor.
208
+ */
209
+ export function ensureHerdrTail(
210
+ agentId: string,
211
+ opts: { transport?: Transport; pollMs?: number; tail?: (id: string, ctx: TailContext) => Promise<TailOutcome> } = {},
212
+ ): { started: boolean; running: boolean; why: string } {
213
+ const t = opts.transport ?? activeTransport();
214
+ if (!t || t.kind !== HERDR) return { started: false, running: false, why: `transport is ${t?.kind ?? "unwired"}, not herdr — a tmux seat is woken by its pusher` };
215
+ if (running.has(agentId)) return { started: false, running: true, why: "already tailing this agent in this process" };
216
+ const ctx = newContext();
217
+ const run = opts.tail ?? ((id: string, c: TailContext) => tailOnce(id, { transport: t, ctx: c }));
218
+ const rec: TailRecord = { agentId, startedAt: new Date().toISOString(), ticks: 0, delivered: 0, held: 0, lastTickAt: null, lastWhy: null, stop: () => {} };
219
+ let inFlight = false;
220
+ const timer = setInterval(() => {
221
+ if (inFlight) return;
222
+ inFlight = true;
223
+ void run(agentId, ctx)
224
+ .then((o) => {
225
+ rec.ticks += 1;
226
+ rec.lastTickAt = new Date().toISOString();
227
+ rec.delivered += o.delivered.length;
228
+ rec.held += o.held.length;
229
+ rec.lastWhy = o.held.at(-1)?.why ?? o.idle ?? null;
230
+ })
231
+ .catch((e) => { rec.lastWhy = `tick threw: ${(e as Error).message}`; })
232
+ .finally(() => { inFlight = false; });
233
+ }, opts.pollMs ?? DEFAULT_POLL_MS);
234
+ timer.unref?.(); // optional-call: `unref` is absent on some host timer shims, and a server that cannot unref its tail must still start it
235
+ rec.stop = () => clearInterval(timer);
236
+ running.set(agentId, rec);
237
+ return { started: true, running: true, why: `tailing ${agentId}'s inbox and rooms every ${opts.pollMs ?? DEFAULT_POLL_MS}ms` };
238
+ }
239
+
240
+ /** The hook the server calls at EVERY identity binding — `join`, a gated first claim, or the env var. */
241
+ export function startTailOnBind(agentId: string, log: (line: string) => void = (l) => console.error(l)): void {
242
+ const r = ensureHerdrTail(agentId);
243
+ if (r.started) log(`[agent-coord-mcp] herdr tail: ${r.why}`);
244
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Messages appended to a JSONL bus file after byte `from`, each with the absolute byte offset just
3
+ * past its own line. Shared by the send-time herdr path and the seat's own tail, so both agree on
4
+ * exactly which bytes a delivery covers — a second reader would be a second truth about where a
5
+ * seat's cursor may move.
6
+ *
7
+ * A partial trailing line is left for the next read rather than guessed at, and a line that is not
8
+ * JSON is skipped with the offset moving past it.
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+
12
+ export type OffsetMessage = { msg: Record<string, unknown>; end: number };
13
+
14
+ export function newMessagesIn(file: string, from: number): OffsetMessage[] {
15
+ let buf: Buffer;
16
+ try { buf = readFileSync(file); } catch { return []; }
17
+ const out: OffsetMessage[] = [];
18
+ let pos = Math.min(Math.max(0, from), buf.length);
19
+ while (pos < buf.length) {
20
+ const nl = buf.indexOf(0x0a, pos);
21
+ if (nl === -1) break;
22
+ const line = buf.subarray(pos, nl).toString("utf8");
23
+ const end = nl + 1;
24
+ pos = end;
25
+ if (!line.trim()) continue;
26
+ try { out.push({ msg: JSON.parse(line), end }); } catch { /* not a message; the offset moves past it */ }
27
+ }
28
+ return out;
29
+ }
@@ -20,6 +20,9 @@ import { readLog } from "./logwatch.js";
20
20
  // `dist/`, so this resolves the same in the built package as in source.
21
21
  // @ts-expect-error — untyped .mjs sibling, deliberately not duplicated in TS
22
22
  import { priorDeliveries, replayInfo } from "../../hooks/replay.mjs";
23
+ // ⟨q-e5cb3538⟩ The pane-unsafe byte class is single-sourced there too, shared with the renderer.
24
+ // @ts-expect-error — untyped .mjs sibling, deliberately not duplicated in TS
25
+ import { findControlByte } from "../../hooks/control-bytes.mjs";
23
26
  import { renderRecord } from "./render.js";
24
27
  import path from "node:path";
25
28
  import {
@@ -344,6 +347,23 @@ export async function sendMessageTool(args: {
344
347
  const scribeCheck = checkVerdictScribe(args.from, args.record);
345
348
  if (!scribeCheck.ok) return { ok: false as const, error: scribeCheck.error };
346
349
 
350
+ // ⟨q-e5cb3538⟩ — INGRESS POLICY: REFUSE, don't rewrite. A control byte in content is a
351
+ // keystroke in every pane the message is typed into (CR submits, ESC interrupts, ETX is
352
+ // Ctrl-C). Refused rather than silently escaped because the stored message is what gets
353
+ // cited and replayed; a rewrite would make it differ from what the sender wrote, and the
354
+ // sender is the only party who can say what the byte was for. Line feed and tab pass.
355
+ // Render-time escaping (hooks/tier.mjs) still covers history stored before this check.
356
+ const control = findControlByte({ from: args.from, to: args.to, room: args.room, text: args.text, record: args.record, repo: args.repo });
357
+ if (control) {
358
+ return {
359
+ ok: false as const,
360
+ error:
361
+ `refused: '${control.path}' carries a control character (${control.escape}) at offset ${control.offset}. ` +
362
+ "Delivered to a pane it would be typed as a keystroke in the recipient's session. " +
363
+ "Remove it, or write it out visibly (e.g. \\r); quoted external text (PR bodies, commit messages) is the usual carrier. Nothing was written.",
364
+ };
365
+ }
366
+
347
367
  // A `done` must cite the work it claims. Presence and shape only — resolving
348
368
  // the ref against gh/git is a consumer's job, and the send path makes no
349
369
  // network calls. Rejected as a value, not a throw, mirroring the
@@ -395,7 +395,10 @@ export async function queueWriteTool(args: {
395
395
  `item by hand once, or move the queue section last.`,
396
396
  );
397
397
  }
398
- const w0 = writeDoc(repo, QUEUE_DOC, reparsed, original);
398
+ // `own` names the row this call authored. It is already recorded above, so this
399
+ // changes nothing today — it is stated so the guarantee survives a refactor that
400
+ // stops pre-stamping, rather than depending on line 361 staying where it is.
401
+ const w0 = writeDoc(repo, QUEUE_DOC, reparsed, original, [], [target.id]);
399
402
  return {
400
403
  ok: true,
401
404
  op: args.op,
@@ -120,6 +120,48 @@ const norm = (s: string) => String(s).toLowerCase().replace(/[`*_]/g, "").replac
120
120
  * against them by the caller (`eventIsDerived`), so a diff that has since been
121
121
  * reverted cannot produce an event that outlives the record.
122
122
  */
123
+ /**
124
+ * ⟨q-…⟩ PHASE COMPLETENESS IS THE ABSENCE OF AN UNTICKED BOX, AND IT MUST NOT DEPEND ON WHAT
125
+ * A BOX IS CALLED.
126
+ *
127
+ * ⛔ MEASURED, NOT REASONED — a false "phase 5.4 complete" event was emitted and DELIVERED on
128
+ * 2026-09-16 at `579db9d`. The detector's box pattern required a label shaped `\d+\.\d+`, so
129
+ * against the real document it saw **31 of 36** checkboxes, every one of them ticked, and
130
+ * fired. The three it could not see were `3B.2`, `3B.3` and `3B.4` — labels with a LETTER
131
+ * inside the number — and `3B.3` is deliberately unticked and not recoverable.
132
+ *
133
+ * ⚠ SO THE CAUSE IS THE LABEL PATTERN, NOT THE POSITION OF THE LAST BOX. The event cited
134
+ * `- [x] 6.4`, the document's final line, which made it look like a last-line rule; the ref is
135
+ * only what it cites. Any doc whose remaining work carries an unanticipated label reported
136
+ * itself finished, wherever that work sat.
137
+ *
138
+ * ⭐ THE FIX IS A DIFFERENT QUESTION, not a wider regex for the same one: "is any checkbox in
139
+ * this document unticked" is asked over EVERY markdown checkbox, whatever follows it. A label
140
+ * shape nobody has invented yet cannot defeat it. The failure direction is deliberate — an
141
+ * unticked box anywhere blocks the event, so an unrecognised box costs a MISSED completion,
142
+ * never a FALSE one. This code's own comment already said which way to lean: *"A false
143
+ * completion is worse than a missing one: it closes a phase nobody finished."*
144
+ */
145
+ export const ANY_BOX = /^[ \t]*[-*+][ \t]*\[([ xX])\][ \t]*(.*)$/;
146
+ export const TICKED_BOX = /^[ \t]*[-*+][ \t]*\[[xX]\]/;
147
+
148
+ export type PhaseBox = { ticked: boolean; label: string | null; line: string };
149
+
150
+ /** Every markdown checkbox in a phase document, in file order, label-agnostic. */
151
+ export function checkboxesIn(text: string): PhaseBox[] {
152
+ const out: PhaseBox[] = [];
153
+ for (const raw of String(text).split("\n")) {
154
+ const m = ANY_BOX.exec(raw);
155
+ if (!m) continue;
156
+ const rest = m[2] ?? "";
157
+ out.push({ ticked: m[1]!.toLowerCase() === "x", label: (/^\*{0,2}([A-Za-z0-9][A-Za-z0-9.]*)\b/.exec(rest) ?? [])[1] ?? null, line: raw.trim() });
158
+ }
159
+ return out;
160
+ }
161
+
162
+ /** The unticked boxes, for a caller that wants to say WHY it did not fire. */
163
+ export const untickedIn = (text: string): PhaseBox[] => checkboxesIn(text).filter((b) => !b.ticked);
164
+
123
165
  export function eventsFromCommittedChange(
124
166
  diff: string,
125
167
  after: { done?: string; phases?: Record<string, string> } = {},
@@ -257,14 +299,14 @@ export function eventsFromCommittedChange(
257
299
  // finished.
258
300
  for (const [rel, text] of Object.entries(after.phases ?? {})) {
259
301
  if (!isPhaseDoc(rel)) continue;
260
- const boxes = [...String(text).matchAll(/^\s*-\s*\[( |x)\]\s*\*{0,2}(\d+\.\d+[a-z]?)\b/gim)];
261
- if (!boxes.length || boxes.some((m) => m[1] !== "x")) continue;
302
+ const boxes = checkboxesIn(String(text));
303
+ if (!boxes.length || boxes.some((b) => !b.ticked)) continue;
262
304
  // This commit must have ticked a box IN THIS FILE.
263
- const closedHere = (byFile.get(rel)?.added ?? []).some((l) => /^\s*-\s*\[x\]\s*\*{0,2}\d+\.\d+/.test(l));
305
+ const closedHere = (byFile.get(rel)?.added ?? []).some((l) => TICKED_BOX.test(l));
264
306
  if (!closedHere) continue;
265
307
  const phase = /phase(\d+(?:\.\d+)?)/i.exec(rel)?.[1];
266
308
  if (!phase) continue;
267
- events.push({ kind: "phase", target: phase, ref: boxes[boxes.length - 1]![0].trim(), summary: `phase ${phase} — every task box ticked` });
309
+ events.push({ kind: "phase", target: phase, ref: boxes[boxes.length - 1]!.line, summary: `phase ${phase} — every task box ticked` });
268
310
  }
269
311
 
270
312
  lastUnattributedItems = unattributedItems;
@@ -45,7 +45,7 @@ import { verdictsFor, gatedBy, prVerdictsIn } from "../gated-head.js";
45
45
  import { boardRefFor, classifyBoardRef } from "./board-ref.js";
46
46
  import { haltState } from "./stall.js";
47
47
  import { readSubs, evaluate, commitEvaluation, eventIsDerived, type RecordEvent } from "./events.js";
48
- import { prRefsIn } from "./record-events.js";
48
+ import { prRefsIn, leadingItemIdOf } from "./record-events.js";
49
49
 
50
50
  const QUEUE_DOC = "docs/QUEUE.md";
51
51
  const DONE_DOC = "docs/DONE.md";
@@ -149,8 +149,12 @@ export function writeDoc(
149
149
  doc: WorkDoc,
150
150
  original: string,
151
151
  alreadyWritten: string[] = [],
152
+ own: readonly string[] = [],
152
153
  ): { written: boolean; stamped: string[] } {
153
- const { text, stamped } = renderWorkDocForWrite(doc);
154
+ // `own` = the rows THIS CALL authored. Everything else is stamped only if the
155
+ // document already speaks the recorded-id grammar — see `stampQueueIds`, which
156
+ // holds the rule so both writers cannot drift apart on it.
157
+ const { text, stamped } = renderWorkDocForWrite(doc, { own });
154
158
  if (text === original) return { written: false, stamped: [] };
155
159
  const p = path.join(repo, rel);
156
160
  const current = existsSync(p) ? readFileSync(p, "utf8") : "";
@@ -1195,7 +1199,23 @@ export async function landTool(
1195
1199
  }
1196
1200
  }
1197
1201
 
1198
- const already = doneEntriesOf(d.doc).some((e) => new RegExp(`#${n}\\b`).test(String(e.ref ?? "")));
1202
+ /*
1203
+ * ⛔ ⟨q-552b9912⟩ "ALREADY LOGGED" IS A QUESTION ABOUT THE ITEM, NOT THE PR.
1204
+ *
1205
+ * It asked whether ANY DONE entry cited this PR, so the second item a PR closed found the first item's entry,
1206
+ * reported `alreadyLogged: true`, and wrote nothing: the row left the queue and never reached the delivery record,
1207
+ * so `scan_record_events` emitted no item event for it. Measured twice on 2026-09-16 — ⟨q-3b8e05af⟩ on #360 and
1208
+ * ⟨q-144c97a8⟩ on #366, both hand-repaired (07338a1, 00c8315).
1209
+ *
1210
+ * Keyed per item BY POSITION ONLY: an entry is this item's when it LEADS with `⟨id⟩` (the form this verb writes). A
1211
+ * by-mention fallback was tried and REMOVED (qa's FAIL on #370 @ fce3ae1): an id-less entry for this PR that merely
1212
+ * MENTIONS the id — B never landed — read as logged and wrote nothing, the same defect narrowed. A mention is not a
1213
+ * record. A land with no item keeps the PR key, since there is no item to key on.
1214
+ */
1215
+ const citesThisPr = (e: { ref?: string }) => new RegExp(`#${n}\\b`).test(String(e.ref ?? ""));
1216
+ const already = target
1217
+ ? doneEntriesOf(d.doc).some((e) => leadingItemIdOf(e.text) === target.id)
1218
+ : doneEntriesOf(d.doc).some(citesThisPr);
1199
1219
 
1200
1220
  // A DONE LINE A HUMAN WOULD NOT HAVE WRITTEN IS NOT A DONE LINE.
1201
1221
  //
@@ -1265,7 +1285,7 @@ export async function landTool(
1265
1285
  try {
1266
1286
  if (args.write) {
1267
1287
  if (queueChanged) {
1268
- const w = writeDoc(repo, QUEUE_DOC, q.doc, q.text, wrote);
1288
+ const w = writeDoc(repo, QUEUE_DOC, q.doc, q.text, wrote, args.queueItemId ? [args.queueItemId] : []);
1269
1289
  if (w.written) wrote.push(QUEUE_DOC);
1270
1290
  // Rows stamped that this call did not ask to touch. `queueItemId` is the
1271
1291
  // one item the caller supplied, so anything else here is a row that
@@ -6,7 +6,7 @@ import { existsSync, openSync, statSync, watch } from "node:fs";
6
6
  import { promises as fsp } from "node:fs";
7
7
  import { spawn, spawnSync } from "node:child_process";
8
8
  import { fileURLToPath } from "node:url";
9
- import { detectSpread, installedBuild } from "../server-spread.js";
9
+ import { detectSpread, installedBuild, pidRunning } from "../server-spread.js";
10
10
  import { z } from "zod";
11
11
  import path from "node:path";
12
12
  import {
@@ -161,6 +161,9 @@ export async function registerTool(args: {
161
161
  registeredAt: existing?.registeredAt ?? now,
162
162
  lastHeartbeat: now,
163
163
  capabilities: existing?.capabilities,
164
+ // ⟨q-18a719c5⟩ The entry is rebuilt here, so the stamp is rebuilt with it — from the
165
+ // server answering THIS register, which is the one serving the seat now.
166
+ ...answeringServerIdentity(),
164
167
  // Omitted → carried forward untouched. Only an explicit `false` revokes.
165
168
  ...(args.proseOnly === undefined
166
169
  ? existing?.proseOnly
@@ -285,6 +288,39 @@ export async function quitTool(args: { agentId: string }): Promise<never> {
285
288
 
286
289
  export const heartbeatSchema = { agentId: z.string().min(1) };
287
290
 
291
+ /**
292
+ * ⟨q-18a719c5⟩ THE ANSWERING SERVER'S IDENTITY — what `serverSpread` places a seat by. It exists
293
+ * only inside the process that serves the seat, so every path on which a session binds an
294
+ * identity stamps it: `register` (which `join` calls), the first-claim binding in server.ts
295
+ * (a server that restarts and re-binds through any gated tool, `attach_agent` included),
296
+ * `rename_agent` (the renaming session serves the new id), and `heartbeat`. Before this,
297
+ * only `heartbeat` stamped, no code path called it, and `register` rebuilt the entry and
298
+ * dropped whatever it had written.
299
+ */
300
+ export function answeringServerIdentity(): { serverPid: number; serverStartedAt: number; serverModule?: string } {
301
+ return {
302
+ serverPid: process.pid,
303
+ // Derived from uptime rather than read from a file: an mtime tracks writes (a reinstall of
304
+ // identical bytes moves it) while uptime is a fact about THIS process.
305
+ serverStartedAt: Date.now() - Math.round(process.uptime() * 1000),
306
+ // WHAT this process is executing, not what it is labelled. Resolved from this module's own
307
+ // URL, so a server running a dev `dist/` says so instead of inheriting the installed path.
308
+ serverModule: installedBuild(import.meta.url, statSync)?.module,
309
+ };
310
+ }
311
+
312
+ /** Stamp an EXISTING registry entry with the answering server's identity. False when absent. */
313
+ export async function stampServerIdentity(agentId: string): Promise<boolean> {
314
+ let stamped = false;
315
+ await updateJson<AgentRegistry>(AGENTS_FILE, {}, (current) => {
316
+ if (!current[agentId]) return current;
317
+ Object.assign(current[agentId], answeringServerIdentity());
318
+ stamped = true;
319
+ return current;
320
+ });
321
+ return stamped;
322
+ }
323
+
288
324
  export async function heartbeatTool(args: { agentId: string }) {
289
325
  let missing = false;
290
326
  await updateJson<AgentRegistry>(AGENTS_FILE, {}, (current) => {
@@ -293,23 +329,8 @@ export async function heartbeatTool(args: { agentId: string }) {
293
329
  return current;
294
330
  }
295
331
  current[args.agentId].lastHeartbeat = Date.now();
296
- /*
297
- * ⛔ STAMP THE ANSWERING PROCESS — `⟨q-cec42e20⟩`. This runs INSIDE the server that
298
- * is answering, which is the only place these two facts exist: `capabilities`
299
- * reports them solely to its own caller, and no seat can query another seat's
300
- * server at all. Publishing them here is what makes a cross-seat comparison
301
- * possible without every seat having to volunteer it in prose.
302
- *
303
- * `startedAt` is derived from uptime rather than read from a file: an mtime tracks
304
- * writes (a reinstall of identical bytes moves it) while uptime is a fact about
305
- * THIS process.
306
- */
307
- current[args.agentId].serverPid = process.pid;
308
- current[args.agentId].serverStartedAt = Date.now() - Math.round(process.uptime() * 1000);
309
- // ⛔ WHAT this process is executing, not what it is labelled. Resolved from this
310
- // module's own URL, so a server running a dev `dist/` says so instead of inheriting
311
- // the installed path — the case that made a 3-build fleet read AGREED.
312
- current[args.agentId].serverModule = installedBuild(import.meta.url, statSync)?.module;
332
+ // ⛔ STAMP THE ANSWERING PROCESS — `⟨q-cec42e20⟩`; see `answeringServerIdentity`.
333
+ Object.assign(current[args.agentId], answeringServerIdentity());
313
334
  return current;
314
335
  });
315
336
  if (missing) return { ok: false, error: `agent '${args.agentId}' not registered` };
@@ -464,6 +485,8 @@ export async function listAgentsTool() {
464
485
  serverModule: a.serverModule,
465
486
  })),
466
487
  installedBuild(import.meta.url, statSync),
488
+ // ⟨q-18a719c5⟩ a stamp whose server is gone reads unknown: ask the kernel, here, in production.
489
+ { isRunning: pidRunning },
467
490
  );
468
491
 
469
492
  // ⟨q-178878aa⟩ — the humans the bus knows: visible here, read at send time, never evicted.
@@ -541,6 +564,18 @@ export function isMarkerLive(marker: TransportMarker, reg: AgentRegistry, now: n
541
564
  return isPidAlive(marker.pid);
542
565
  }
543
566
 
567
+ /**
568
+ * ⟨q-abd88dd4⟩ Does this marker hold a LIVE LOCAL PROCESS — a pusher someone could signal, or
569
+ * wait on? A herdr marker never does: it has no pusher, and its `pid` is addressed to pre-herdr
570
+ * readers (pid 1, see `herdrMarkerPid`), so reading it as a process would make pid 1 look like a
571
+ * running pusher. Every pid-as-process decision about a marker goes through here; liveness of a
572
+ * herdr seat is `isMarkerLive`, which asks herdr for the pane.
573
+ */
574
+ export function markerHoldsLiveProcess(marker: TransportMarker | null | undefined): boolean {
575
+ if (!marker || marker.transport === HERDR) return false;
576
+ return isPidAlive(marker.pid);
577
+ }
578
+
544
579
  export function isPidAlive(pid: number): boolean {
545
580
  if (!pid || pid <= 0) return false;
546
581
  try {
@@ -682,7 +717,7 @@ export async function renameAgentTool(args: { agentId: string; newAgentId: strin
682
717
  // must re-attach under the new id (join/attach_agent) to restore push.
683
718
  const liveTransport = await readJson<TransportMarker | null>(transportFile(oldId), null);
684
719
  let detachedTransport = false;
685
- if (liveTransport && isPidAlive(liveTransport.pid)) {
720
+ if (markerHoldsLiveProcess(liveTransport)) {
686
721
  await detachAgentTool({ agentId: oldId });
687
722
  detachedTransport = true;
688
723
  }
@@ -690,7 +725,8 @@ export async function renameAgentTool(args: { agentId: string; newAgentId: strin
690
725
  // Registry: move the entry under the new key.
691
726
  await updateJson<AgentRegistry>(AGENTS_FILE, {}, (current) => {
692
727
  if (current[oldId]) {
693
- current[newId] = { ...current[oldId], agentId: newId };
728
+ // ⟨q-18a719c5⟩ The renaming session serves the new id, so the stamp is re-taken from it.
729
+ current[newId] = { ...current[oldId], agentId: newId, ...answeringServerIdentity() };
694
730
  delete current[oldId];
695
731
  }
696
732
  return current;