agent-coord-mcp 0.26.19 → 0.26.21

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 (56) hide show
  1. package/README.md +82 -0
  2. package/dist/capabilities.js +57 -1
  3. package/dist/capabilities.js.map +1 -1
  4. package/dist/gated-head.js +130 -0
  5. package/dist/gated-head.js.map +1 -0
  6. package/dist/server.js +23 -0
  7. package/dist/server.js.map +1 -1
  8. package/dist/tools/queue-write.js +431 -0
  9. package/dist/tools/queue-write.js.map +1 -0
  10. package/dist/tools/records.js +406 -70
  11. package/dist/tools/records.js.map +1 -1
  12. package/dist/tools/registry.js +34 -5
  13. package/dist/tools/registry.js.map +1 -1
  14. package/dist/tools/shared.js.map +1 -1
  15. package/dist/tools/stall.js +2 -1
  16. package/dist/tools/stall.js.map +1 -1
  17. package/dist/tools/transport.js +82 -42
  18. package/dist/tools/transport.js.map +1 -1
  19. package/dist/tools/tree-provenance.js +107 -0
  20. package/dist/tools/tree-provenance.js.map +1 -0
  21. package/dist/tools/work.js +95 -3
  22. package/dist/tools/work.js.map +1 -1
  23. package/dist/transports/config.js +82 -0
  24. package/dist/transports/config.js.map +1 -0
  25. package/dist/transports/index.js +113 -0
  26. package/dist/transports/index.js.map +1 -0
  27. package/dist/transports/tmux.js +140 -0
  28. package/dist/transports/tmux.js.map +1 -0
  29. package/dist/transports/types.js +86 -0
  30. package/dist/transports/types.js.map +1 -0
  31. package/hooks/peek-coord.mjs +0 -0
  32. package/hooks/tmux-pusher.mjs +33 -3
  33. package/package.json +14 -11
  34. package/scripts/coord-attention-clock.mjs +0 -0
  35. package/scripts/coord-node.sh +0 -0
  36. package/scripts/coord-stall-clock.mjs +0 -0
  37. package/scripts/coord-token.mjs +0 -0
  38. package/scripts/probe-tmux-liveness.sh +0 -0
  39. package/scripts/spawn-agent.sh +0 -0
  40. package/scripts/stop-agent.sh +0 -0
  41. package/scripts/typed-record-stats.mjs +0 -0
  42. package/src/capabilities.ts +104 -1
  43. package/src/gated-head.ts +134 -0
  44. package/src/server.ts +29 -0
  45. package/src/tools/queue-write.ts +485 -0
  46. package/src/tools/records.ts +409 -40
  47. package/src/tools/registry.ts +36 -5
  48. package/src/tools/shared.ts +12 -36
  49. package/src/tools/stall.ts +2 -1
  50. package/src/tools/transport.ts +96 -43
  51. package/src/tools/tree-provenance.ts +136 -0
  52. package/src/tools/work.ts +95 -3
  53. package/src/transports/config.ts +110 -0
  54. package/src/transports/index.ts +126 -0
  55. package/src/transports/tmux.ts +177 -0
  56. package/src/transports/types.ts +201 -0
@@ -1,4 +1,5 @@
1
1
  import { detachAgentTool } from "./transport.js";
2
+ import { isLocallyProbeable, isRemoteTmuxKind, targetOf } from "../transports/index.js";
2
3
  import { readAway, secondCoordinatorRefusal } from "./away.js";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import { existsSync, openSync, watch } from "node:fs";
@@ -419,6 +420,36 @@ export async function listAgentsTool() {
419
420
  return { agents, evicted, proseOnly };
420
421
  }
421
422
 
423
+ /**
424
+ * EVERY marker on disk, READ-ONLY.
425
+ *
426
+ * `loadLiveTransports` below deletes any marker it judges not-live, which is
427
+ * right for the delivery paths that own that garbage collection and WRONG for a
428
+ * diagnostic: `capabilities` must be able to describe the fleet without changing
429
+ * it. Using the reaping loader from a read-only verb made a mixed-fleet check
430
+ * delete the very marker it was reporting (caught by its own test — the second
431
+ * disagreeing agent vanished between write and read).
432
+ *
433
+ * Liveness is reported per marker rather than filtered on, because a STALE marker
434
+ * naming a different transport is more alarming than a live one, not less: it is
435
+ * a seat that may come back on the wrong transport. Filtering it out would make
436
+ * the remote kind — whose liveness is heartbeat-based and therefore absent
437
+ * without a registry entry — systematically invisible to this check.
438
+ */
439
+ export async function readAllTransportMarkers(): Promise<
440
+ { marker: TransportMarker; live: boolean }[]
441
+ > {
442
+ const out: { marker: TransportMarker; live: boolean }[] = [];
443
+ const reg = await readJson<AgentRegistry>(AGENTS_FILE, {});
444
+ const now = Date.now();
445
+ for (const fname of await listTransportFiles()) {
446
+ const marker = await readJson<TransportMarker | null>(path.join(TRANSPORT_DIR, fname), null);
447
+ if (!marker) continue; // unparseable: nothing to attribute, and not ours to delete
448
+ out.push({ marker, live: isMarkerLive(marker, reg, now) });
449
+ }
450
+ return out;
451
+ }
452
+
422
453
  export async function loadLiveTransports(): Promise<Map<string, TransportMarker>> {
423
454
  const out = new Map<string, TransportMarker>();
424
455
  const reg = await readJson<AgentRegistry>(AGENTS_FILE, {});
@@ -439,7 +470,7 @@ export async function loadLiveTransports(): Promise<Map<string, TransportMarker>
439
470
  // remote markers (tmux-push-remote, pid 0 on a foreign host) can't be — so we
440
471
  // trust the registry heartbeat the remote pusher refreshes (within STALE_MS).
441
472
  export function isMarkerLive(marker: TransportMarker, reg: AgentRegistry, now: number): boolean {
442
- if (marker.transport === "tmux-push-remote") {
473
+ if (isRemoteTmuxKind(marker.transport)) {
443
474
  const entry = reg[marker.agentId];
444
475
  return !!entry && now - entry.lastHeartbeat < STALE_MS;
445
476
  }
@@ -523,13 +554,13 @@ export async function liveClaimEvidence(agentId: string, now: number): Promise<C
523
554
  if (marker && isMarkerLive(marker, reg, now)) {
524
555
  markerLive = true;
525
556
  reasons.push(
526
- `live ${marker.transport} transport (pid ${marker.pid}${marker.tmuxTarget ? `, pane ${marker.tmuxTarget}` : ""})`,
557
+ `live ${marker.transport} transport (pid ${marker.pid}${targetOf(marker) ? `, pane ${targetOf(marker)}` : ""})`,
527
558
  );
528
559
  if (
529
- marker.transport === "tmux-push" &&
530
- marker.tmuxTarget &&
560
+ isLocallyProbeable(marker.transport) &&
561
+ targetOf(marker) &&
531
562
  process.env.TMUX_PANE &&
532
- marker.tmuxTarget === process.env.TMUX_PANE
563
+ targetOf(marker) === process.env.TMUX_PANE
533
564
  ) {
534
565
  samePane = true;
535
566
  }
@@ -88,42 +88,18 @@ export type AgentEntry = {
88
88
  proseOnly?: { since: number; reason?: string };
89
89
  };
90
90
 
91
- export type TransportMarker = {
92
- agentId: string;
93
- transport: string;
94
- pid: number;
95
- tmuxTarget?: string;
96
- since: number;
97
- // Remote pushers run on a different machine; the local pid is meaningless,
98
- // so we tag the host and use heartbeat-based liveness instead of pidAlive.
99
- host?: string;
100
- // mtime of the pusher script the daemon loaded into memory at spawn time
101
- // (epoch ms). When the on-disk script is upgraded but the daemon isn't
102
- // restarted, doctor() compares this to the current mtime to flag a stale
103
- // pusher — the class of bug that silently dropped /clear /compact in v0.8.1.
104
- // Absent on markers written by older versions (treated as "unknown, skip").
105
- scriptMtime?: number;
106
- // Build identity (newest dist mtime, sampled at that server's module load)
107
- // of the MCP server whose attach_agent stamped this marker. A marker
108
- // stamped by a server predating the current on-disk build was written by
109
- // attach/stamp logic the rebuild replaced — doctor's provenance check flags
110
- // it for a session restart + re-attach. Absent on markers written by older
111
- // versions (treated as "unknown, skip", deliberately mirroring scriptMtime).
112
- serverBuildMtime?: number;
113
- // Does this transport carry ROOM traffic, or DMs only?
114
- //
115
- // A pusher started with `--no-room` delivers inbox messages and nothing
116
- // else, and until this field existed the marker looked identical to a full
117
- // one — so `status` said `attached: true` and an agent sat with its room
118
- // feed off while every reading said healthy. That is how worker-2 missed
119
- // its channel traffic.
120
- //
121
- // ABSENT MEANS UNKNOWN, NEVER "ON" — deliberately mirroring scriptMtime and
122
- // serverBuildMtime above. A marker from an older pusher cannot tell us, and
123
- // reporting an unasked question as full capability is the defect this field
124
- // exists to remove.
125
- rooms?: boolean;
126
- };
91
+ /**
92
+ * THE MARKER TYPE NOW LIVES IN THE SEAM, and is re-exported here so the dozens
93
+ * of `from "./shared.js"` imports keep working.
94
+ *
95
+ * It was defined here, and the transport extraction gave it a second definition
96
+ * in `transports/types.ts` — two structurally-similar types that a consumer
97
+ * could satisfy while missing a field, which is the duplication this task exists
98
+ * to remove rather than double. One definition, one home: the transport layer,
99
+ * which is what the field describes. The nine fields and their comments moved
100
+ * with it verbatim, including `target`'s dual-write rule.
101
+ */
102
+ export type { TransportMarker } from "../transports/types.js";
127
103
 
128
104
  /**
129
105
  * How to REPORT a transport's room capability.
@@ -15,6 +15,7 @@
15
15
  * from "nothing ran". A check that only speaks when it fires cannot be told from
16
16
  * a broken one.
17
17
  */
18
+ import { isLocallyProbeable } from "../transports/index.js";
18
19
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
19
20
  import { execFileSync } from "node:child_process";
20
21
  import path from "node:path";
@@ -311,7 +312,7 @@ export async function stallCheckTool(args: { repo?: string; stallMinutes?: numbe
311
312
  // agent with no transport, or a REMOTE one where the heartbeat genuinely
312
313
  // is the liveness mechanism, still HITs on a dead heartbeat.
313
314
  const marker = liveTransports.get(agentId);
314
- if (age > limit && marker && marker.transport === "tmux-push") {
315
+ if (age > limit && marker && isLocallyProbeable(marker.transport)) {
315
316
  unmeasurable.push({
316
317
  agentId,
317
318
  value: marker.transport,
@@ -1,4 +1,16 @@
1
1
  import { loadLiveTransports, isMarkerLive, isPidAlive } from "./registry.js";
2
+ import {
3
+ TMUX_PUSH,
4
+ registerTmuxHost,
5
+ type TmuxHost,
6
+ isLocallyProbeable,
7
+ isTmuxKind,
8
+ paneExists,
9
+ probePane,
10
+ tmuxVersion,
11
+ targetOf,
12
+ tmuxAvailable,
13
+ } from "../transports/index.js";
2
14
  import { newestMtimeUnder, onDiskBuildMtime, onDiskSourceMtime, SERVER_BUILD_MTIME, SERVER_BUILD_SHA, BUILD_DIR } from "../build.js";
3
15
  import { prefixOf, prefixVerdict } from "../prefix.js";
4
16
  import { execFileSync } from "node:child_process";
@@ -119,17 +131,11 @@ export async function pingTool(args: { from: string; to: string; echo?: boolean
119
131
  let paneAlive: boolean | undefined;
120
132
  if (marker) {
121
133
  transportLive = isMarkerLive(marker, reg, now);
122
- if (transportLive && marker.transport === "tmux-push" && marker.tmuxTarget) {
134
+ if (transportLive && isLocallyProbeable(marker.transport) && targetOf(marker)) {
123
135
  // The pusher can outlive its pane (agent window closed) — probe the pane.
124
- // `has-session` VALIDATES THE TARGET; `display-message -p -t <target> "ok"`
125
- // DOES NOT — tmux exits 0 for any target, including a pane killed a moment
126
- // ago, so the probe had ZERO discriminating power and reported every dead
127
- // pane alive. Pinned to the BEHAVIOUR, not a version: measured identical on
128
- // tmux 3.6b and 3.7b, and a version-pinned claim rots on the next upgrade.
129
- // Positive control, both directions: bogus target -> has-session exit 1,
130
- // display-message exit 0; live pane -> both exit 0.
131
- const probe = spawnSync("tmux", ["has-session", "-t", marker.tmuxTarget]);
132
- paneAlive = probe.status === 0;
136
+ // Why `has-session` and not `display-message`, with the positive control
137
+ // both ways, is documented once on `paneExists`.
138
+ paneAlive = paneExists(targetOf(marker)!);
133
139
  }
134
140
  }
135
141
 
@@ -146,7 +152,7 @@ export async function pingTool(args: { from: string; to: string; echo?: boolean
146
152
  // same shape as `stall_clock_status` before Task 13.1. A REMOTE pusher, or
147
153
  // an agent with no probeable marker at all, has no pid to fall back on —
148
154
  // there heartbeat genuinely IS the liveness mechanism, unchanged.
149
- const heartbeatIsValidSignal = !marker || marker.transport !== "tmux-push";
155
+ const heartbeatIsValidSignal = !marker || !isLocallyProbeable(marker.transport);
150
156
  const alive = reachable || (heartbeatIsValidSignal && heartbeatFresh);
151
157
 
152
158
  let echoSent = false;
@@ -196,7 +202,7 @@ export const CONTROL_COMMANDS = ["clear", "compact", "reload-skills"] as const;
196
202
  // Transports whose pusher can actually TYPE a slash command into a live CLI.
197
203
  // A control command is meaningless to a plain MCP poller, so send_command is
198
204
  // gated to agents currently attached over one of these.
199
- const TMUX_TRANSPORTS = new Set(["tmux-push", "tmux-push-remote"]);
205
+
200
206
 
201
207
  // Normalize "clear" / "/clear" / " /Clear " → "clear"; null if not allowlisted.
202
208
  function normalizeControlCommand(raw: string): string | null {
@@ -208,7 +214,7 @@ function normalizeControlCommand(raw: string): string | null {
208
214
  async function liveTmuxTargets(): Promise<Map<string, TransportMarker>> {
209
215
  const all = await loadLiveTransports();
210
216
  const out = new Map<string, TransportMarker>();
211
- for (const [id, m] of all) if (TMUX_TRANSPORTS.has(m.transport)) out.set(id, m);
217
+ for (const [id, m] of all) if (isTmuxKind(m.transport)) out.set(id, m);
212
218
  return out;
213
219
  }
214
220
 
@@ -645,19 +651,13 @@ export async function attachAgentTool(args: {
645
651
  };
646
652
  }
647
653
 
648
- // Validate target exists.
649
- // `has-session` VALIDATES THE TARGET; `display-message -p -t <target> "ok"`
650
- // DOES NOT — tmux exits 0 for any target, including a pane killed a moment
651
- // ago, so the probe had ZERO discriminating power and reported every dead
652
- // pane alive. Pinned to the BEHAVIOUR, not a version: measured identical on
653
- // tmux 3.6b and 3.7b, and a version-pinned claim rots on the next upgrade.
654
- // Positive control, both directions: bogus target -> has-session exit 1,
655
- // display-message exit 0; live pane -> both exit 0.
656
- const probe = spawnSync("tmux", ["has-session", "-t", target]);
657
- if (probe.status !== 0) {
654
+ // Validate target exists. The probe's discriminating power, and the control
655
+ // that proves it, are documented once on `paneExists`.
656
+ const targetProbe = probePane(target);
657
+ if (!targetProbe.exists) {
658
658
  return {
659
659
  ok: false,
660
- error: `tmux target '${target}' not found: ${(probe.stderr ?? "").toString().trim()}`,
660
+ error: `tmux target '${target}' not found: ${targetProbe.stderr}`,
661
661
  };
662
662
  }
663
663
 
@@ -723,8 +723,14 @@ export async function attachAgentTool(args: {
723
723
  const scriptMtime = newestPusherSourceMtime();
724
724
  const marker: TransportMarker = {
725
725
  agentId: args.agentId,
726
- transport: "tmux-push",
726
+ transport: TMUX_PUSH,
727
727
  pid,
728
+ // DUAL-WRITTEN, and the duplication is the point. `target` is what every
729
+ // consumer now reads (`targetOf`); `tmuxTarget` is what the code a merge
730
+ // revert restores reads. Writing only the new field would leave markers the
731
+ // old server cannot parse, and that failure does not degrade gracefully —
732
+ // it silences every attached lane at once.
733
+ target,
728
734
  tmuxTarget: target,
729
735
  since: Date.now(),
730
736
  scriptMtime,
@@ -749,7 +755,7 @@ export async function attachAgentTool(args: {
749
755
  return {
750
756
  ok: true,
751
757
  agentId: args.agentId,
752
- transport: "tmux-push",
758
+ transport: TMUX_PUSH,
753
759
  tmuxTarget: target,
754
760
  pid,
755
761
  log,
@@ -1171,6 +1177,53 @@ async function scanStaleLocks(olderThanMs: number, now: number): Promise<{ path:
1171
1177
  return out;
1172
1178
  }
1173
1179
 
1180
+ /* ── wiring the seam's TMUX HOST (Phase 5.4 Task 3) ─────────────────────────── */
1181
+
1182
+ /**
1183
+ * The process-layer half of `TmuxTransport`, supplied by the module that owns
1184
+ * pusher spawn, receipts and marker files.
1185
+ *
1186
+ * ⚠ SCOPE, STATED RATHER THAN IMPLIED: Task 3 puts the seam in the IDENTITY and
1187
+ * DIAGNOSIS path — `capabilities` asks the live transport what it is, and the
1188
+ * mixed-fleet check reads markers through it. DELIVERY STILL RUNS THROUGH THE
1189
+ * EXISTING CODE PATHS. Task 2's gate was that nothing changes, and rerouting
1190
+ * every push through a new object would change the thing most likely to break
1191
+ * quietly. So the four delivery methods below THROW rather than no-op: a host
1192
+ * that silently accepted a push and dropped it would be the one failure this
1193
+ * fleet cannot observe, and an explicit throw is reachable only from code that
1194
+ * has not been written yet.
1195
+ */
1196
+ const TMUX_HOST: TmuxHost = {
1197
+ attach: async () => {
1198
+ throw new Error("TmuxTransport.attach is not the delivery path yet — call attachAgentTool (Phase 5.4 Task 3 wires identity only)");
1199
+ },
1200
+ detach: async () => {
1201
+ throw new Error("TmuxTransport.detach is not the delivery path yet — call detachAgentTool");
1202
+ },
1203
+ push: async () => {
1204
+ throw new Error("TmuxTransport.push is not the delivery path yet — delivery runs through the pusher process");
1205
+ },
1206
+ sendControl: async () => {
1207
+ throw new Error("TmuxTransport.sendControl is not the delivery path yet — call sendCommandTool");
1208
+ },
1209
+ /**
1210
+ * Is the pusher behind this marker still running? Reuses `isPusherProcess`,
1211
+ * which checks the COMMAND of the pid rather than merely that a pid exists —
1212
+ * a recycled pid belonging to something else is not a live pusher.
1213
+ */
1214
+ pusherAlive: (marker) => isPusherProcess(marker.pid),
1215
+ killPusher: (marker) => {
1216
+ try {
1217
+ process.kill(marker.pid, "SIGTERM");
1218
+ return true;
1219
+ } catch {
1220
+ return false;
1221
+ }
1222
+ },
1223
+ };
1224
+
1225
+ registerTmuxHost(TMUX_HOST);
1226
+
1174
1227
  export const doctorSchema = {
1175
1228
  fix: z.boolean().optional(),
1176
1229
  maxFileBytes: z.number().int().positive().optional(),
@@ -1231,7 +1284,7 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1231
1284
  const file = path.join(TRANSPORT_DIR, fname);
1232
1285
  const marker = await readJson<TransportMarker | null>(file, null);
1233
1286
  if (!marker || !isMarkerLive(marker, reg, now)) continue;
1234
- if (marker.transport !== "tmux-push") continue; // remote = can't verify (documented limit: can't stat another host)
1287
+ if (!isLocallyProbeable(marker.transport)) continue; // remote = can't verify (documented limit: can't stat another host)
1235
1288
  if (marker.scriptMtime === undefined) {
1236
1289
  // ABSENCE IS NOT EXEMPTION. The field's own writer once dropped it,
1237
1290
  // and the silent skip here meant the check was disabled by the very
@@ -1339,7 +1392,7 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1339
1392
  const file = path.join(TRANSPORT_DIR, fname);
1340
1393
  const marker = await readJson<TransportMarker | null>(file, null);
1341
1394
  if (!marker || !isMarkerLive(marker, reg, now)) continue;
1342
- if (marker.transport !== "tmux-push") continue; // remote = can't verify (documented limit: can't stat another host)
1395
+ if (!isLocallyProbeable(marker.transport)) continue; // remote = can't verify (documented limit: can't stat another host)
1343
1396
  if (marker.serverBuildMtime === undefined) {
1344
1397
  // ABSENCE IS NOT EXEMPTION — flipped in the same commit as the
1345
1398
  // scriptMtime absence above, so the two checks can never disagree
@@ -1434,7 +1487,7 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1434
1487
  const file = path.join(TRANSPORT_DIR, fname);
1435
1488
  const marker = await readJson<TransportMarker | null>(file, null);
1436
1489
  if (!marker || !isMarkerLive(marker, reg, now)) continue;
1437
- if (marker.transport !== "tmux-push") continue; // remote: the script lives on another host
1490
+ if (!isLocallyProbeable(marker.transport)) continue; // remote: the script lives on another host
1438
1491
  const { scriptMtime, serverBuildMtime, agentId } = marker;
1439
1492
  if (scriptMtime === undefined || serverBuildMtime === undefined || onDiskScript === undefined || onDiskServer === undefined) {
1440
1493
  // A pane missing either stamp cannot be classified. Saying so is the
@@ -1475,20 +1528,20 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1475
1528
  {
1476
1529
  // Without a tmux binary we can't tell "wedged" from "can't probe" — skip
1477
1530
  // rather than flag every local marker as dead.
1478
- const tmuxAvailable = spawnSync("tmux", ["-V"]).status === 0;
1531
+ const tmuxIsAvailable = tmuxAvailable();
1479
1532
  const wedged: { agentId: string; pid: number; file: string; target: string; isPusher: boolean }[] = [];
1480
- if (tmuxAvailable) {
1533
+ if (tmuxIsAvailable) {
1481
1534
  for (const fname of await listTransportFiles()) {
1482
1535
  const file = path.join(TRANSPORT_DIR, fname);
1483
1536
  const marker = await readJson<TransportMarker | null>(file, null);
1484
1537
  if (!marker || !isMarkerLive(marker, reg, now)) continue;
1485
- if (marker.transport !== "tmux-push") continue; // remote = no local pane to probe
1538
+ if (!isLocallyProbeable(marker.transport)) continue; // remote = no local pane to probe
1486
1539
  if (!marker.tmuxTarget) continue; // no target recorded, can't probe
1487
1540
  // has-session actually validates the target and fails on a dead
1488
1541
  // pane/session; `display-message -p -t <target> <literal>` does NOT
1489
1542
  // (tmux 3.6b exits 0 for any target, even a just-killed one, when
1490
1543
  // the format string has no #{...} needing that target resolved).
1491
- const probe = spawnSync("tmux", ["has-session", "-t", marker.tmuxTarget]);
1544
+ const probe = { status: paneExists(targetOf(marker)!) ? 0 : 1 };
1492
1545
  if (probe.status === 0) continue; // pane alive
1493
1546
  // The marker's pid being alive does not make it OUR pid — see
1494
1547
  // isPusherProcess. Record the verdict now so `fix` only ever signals
@@ -1523,7 +1576,7 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1523
1576
  level: wedged.length ? "warn" : "ok",
1524
1577
  detail: wedged.length
1525
1578
  ? `${wedged.length} local pusher(s) alive (pid) but their tmux pane is gone — looks attached, delivers nothing. ${fix ? "Reaped (SIGTERM + marker cleared)." : "Run doctor with fix:true to SIGTERM and clear the marker."}`
1526
- : tmuxAvailable
1579
+ : tmuxIsAvailable
1527
1580
  ? "no wedged local pushers (pid-alive, pane-dead)"
1528
1581
  : "tmux not available — skipped wedged-pusher pane probe",
1529
1582
  fixable: true,
@@ -1768,9 +1821,8 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1768
1821
  // Without a tmux binary we cannot probe a pane at all — every stale
1769
1822
  // agent is reported as an orphan candidate rather than silently split,
1770
1823
  // same posture `wedged-local-pushers` takes.
1771
- const tmuxAvailable = spawnSync("tmux", ["-V"]).status === 0;
1772
- const paneAlive = (target: string): boolean =>
1773
- tmuxAvailable && spawnSync("tmux", ["has-session", "-t", target]).status === 0;
1824
+ const tmuxIsAvailable = tmuxAvailable();
1825
+ const paneAlive = (target: string): boolean => tmuxIsAvailable && paneExists(target);
1774
1826
 
1775
1827
  const paneConfirmed: string[] = [];
1776
1828
  const orphans: string[] = [];
@@ -1779,8 +1831,9 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1779
1831
  if (now - a.lastHeartbeat <= EVICT_MS) continue;
1780
1832
  const age = `${Math.floor((now - a.lastHeartbeat) / 3600000)}h`;
1781
1833
  const marker = markerByAgent.get(id);
1782
- if (marker?.transport === "tmux-push" && marker.tmuxTarget && paneAlive(marker.tmuxTarget)) {
1783
- paneConfirmed.push(`${id} (${age}, pane '${marker.tmuxTarget}' still current)`);
1834
+ const markerTarget = marker ? targetOf(marker) : undefined;
1835
+ if (isLocallyProbeable(marker?.transport) && markerTarget && paneAlive(markerTarget)) {
1836
+ paneConfirmed.push(`${id} (${age}, pane '${markerTarget}' still current)`);
1784
1837
  } else {
1785
1838
  orphans.push(`${id} (${age})`);
1786
1839
  }
@@ -1808,7 +1861,7 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1808
1861
  level: orphans.length ? "warn" : "ok",
1809
1862
  detail: orphans.length
1810
1863
  ? `${orphans.length} stale agent(s) have no live tmux pane behind them — permanent, will not clear on their own (unregister or let eviction drop them)`
1811
- : tmuxAvailable
1864
+ : tmuxIsAvailable
1812
1865
  ? `no orphans among ${stale.length} stale agent(s)${stale.length ? " — all pane-confirmed current" : ""}`
1813
1866
  : "tmux not available — could not distinguish orphans from pane-confirmed stale agents",
1814
1867
  fixable: false,
@@ -2026,8 +2079,8 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
2026
2079
  // identity is walk-up-to-.git + rev-parse (kit monorepo root); omitted
2027
2080
  // entirely when there is no checkout — never invented.
2028
2081
  {
2029
- const tmuxProbe = spawnSync("tmux", ["-V"]);
2030
- const tmuxOk = tmuxProbe.status === 0;
2082
+ const tmuxReported = tmuxVersion();
2083
+ const tmuxOk = tmuxReported !== undefined;
2031
2084
  const ident = resolveServerIdentity();
2032
2085
  const loc = ident.branch && ident.sha
2033
2086
  ? `path=${ident.path} version=${ident.version} ${ident.branch}@${ident.sha.slice(0, 12)}`
@@ -2045,7 +2098,7 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
2045
2098
  check: "environment",
2046
2099
  level: tmuxOk ? "ok" : "warn",
2047
2100
  detail: tmuxOk
2048
- ? `root=${ROOT}; node=${process.execPath}; ${loc}; tmux=${(tmuxProbe.stdout ?? "").toString().trim() || "present"}`
2101
+ ? `root=${ROOT}; node=${process.execPath}; ${loc}; tmux=${tmuxReported || "present"}`
2049
2102
  : `root=${ROOT}; node=${process.execPath}; ${loc}; tmux NOT on PATH — the tmux-push transport will not work`,
2050
2103
  fixable: false,
2051
2104
  items,
@@ -0,0 +1,136 @@
1
+ /**
2
+ * ⟨q-c1af2db3⟩ — WHICH TREE DID THIS VERB JUST READ, AND HOW OLD IS IT?
3
+ *
4
+ * ⛔ THE BUG THIS EXISTS FOR ROUTED THE FLEET. `next_unblocked` reads
5
+ * `docs/QUEUE.md` out of whatever working tree it is handed, and that tree
6
+ * belongs to nobody: its freshness is a property of whoever last worked in it.
7
+ * Measured 2026-09-12 16:36 — the primary checkout sat FOUR commits behind
8
+ * `origin/main`, its `QUEUE.md` had zero hits for a tag committed six minutes
9
+ * earlier, and the verb returned the PRE-EDIT text as `next`.
10
+ *
11
+ * ⛆ AND THE HARM LANDED IN BOTH DIRECTIONS INSIDE TEN MINUTES: a row was ruled
12
+ * un-claimable from an observation that was actually a stale file, and a worker
13
+ * was told to hold on a row that was already split. ⭐ ***A true conclusion
14
+ * reached through an untrue reading is the shape, and here the INSTRUMENT
15
+ * supplied the untrue reading — the answer was well-formed, internally
16
+ * consistent, `accounting.reconciles` was TRUE, and nothing in it said which
17
+ * tree it read.***
18
+ *
19
+ * ⛔⛆⛆ THE TRAP THAT BREAKS THE OBVIOUS FIX, AND IT IS WHY THIS FETCHES:
20
+ *
21
+ * a checkout that has not fetched CANNOT REPORT that it has not fetched.
22
+ *
23
+ * `git rev-list --left-right --count origin/main...HEAD` in an unfetched clone
24
+ * compares HEAD against a STALE `origin/main` ref and returns `0 0` — *"I am
25
+ * perfectly current"* — while the real remote has moved. Measured: HEAD
26
+ * `1cab5c2`, its own `origin/main` also `1cab5c2`, true `origin/main` `558365d`,
27
+ * three commits behind, reported as ZERO. ⭐⭐ ***A distance of `0` from an
28
+ * unfetched ref and a distance of `0` from a fetched one are BYTE-IDENTICAL and
29
+ * mean opposite things. That distinction is the whole point of this module: the
30
+ * ref is fetched IN THIS CALL, and when the fetch fails the answer is
31
+ * `"unknown"` and never `0`.***
32
+ *
33
+ * ⚠ AND FETCHING IS NOT THE FIX — that is the row's named anti-control.
34
+ * Fetching updates REFS; the verb reads the TREE, which is still stale
35
+ * afterwards. So a fetch only makes the staleness *legible*; the caller must
36
+ * still refuse or flag. This module reports; it never silently repairs, because
37
+ * making someone else's working tree current is a destructive write into a
38
+ * checkout a seat may be mid-work in.
39
+ */
40
+ import { execFileSync } from "node:child_process";
41
+ import path from "node:path";
42
+
43
+ export type TreeProvenance = {
44
+ /** The tree actually read, resolved — not the string the caller passed. */
45
+ path: string;
46
+ head: string | null;
47
+ /** The ref the distance is measured against, e.g. `origin/main`. */
48
+ base: string;
49
+ baseSha: string | null;
50
+ /**
51
+ * Commits the tree is behind `base`.
52
+ *
53
+ * ⛔ `"unknown"` WHEN THE REF COULD NOT BE FRESHENED IN THIS CALL. Never `0`
54
+ * in that case: `0` is a claim of currency and an unfetched ref cannot make it.
55
+ */
56
+ behind: number | "unknown";
57
+ /** Whether the ref this distance rests on was freshened in THIS call. */
58
+ fetched: boolean;
59
+ fetchError?: string;
60
+ dirty: boolean | "unknown";
61
+ /** True only when the tree is MEASURABLY behind a ref fetched in this call. */
62
+ stale: boolean;
63
+ /** Present whenever the answer cannot be trusted as current. */
64
+ warning?: string;
65
+ };
66
+
67
+ const run = (repo: string, args: string[]): string | null => {
68
+ try {
69
+ return execFileSync("git", args, {
70
+ cwd: repo,
71
+ encoding: "utf8",
72
+ stdio: ["ignore", "pipe", "ignore"],
73
+ timeout: 20_000,
74
+ }).trim();
75
+ } catch {
76
+ return null;
77
+ }
78
+ };
79
+
80
+ /**
81
+ * What the caller needs to say out loud about the tree it just read.
82
+ *
83
+ * @param repo the working tree a verb was handed
84
+ * @param baseBranch the branch the tree should be current with
85
+ */
86
+ export function treeProvenance(repo: string, baseBranch = "main"): TreeProvenance {
87
+ const resolved = run(repo, ["rev-parse", "--show-toplevel"]) ?? path.resolve(repo);
88
+ const base = `origin/${baseBranch}`;
89
+ const head = run(resolved, ["rev-parse", "HEAD"]);
90
+ const status = run(resolved, ["status", "--porcelain"]);
91
+
92
+ // ⛔ FETCH FIRST, AND THE ORDER IS THE POINT. Every number below is worthless
93
+ // if it rests on a ref the tree last updated at some unknown past moment.
94
+ const fetched = run(resolved, ["fetch", "--quiet", "origin", baseBranch]) !== null;
95
+
96
+ const out: TreeProvenance = {
97
+ path: resolved,
98
+ head,
99
+ base,
100
+ baseSha: null,
101
+ behind: "unknown",
102
+ fetched,
103
+ dirty: status === null ? "unknown" : status.length > 0,
104
+ stale: false,
105
+ };
106
+
107
+ if (!fetched) {
108
+ out.fetchError = `could not fetch origin/${baseBranch} — the distance cannot be computed`;
109
+ out.warning =
110
+ `TREE FRESHNESS UNKNOWN: ${resolved} could not reach origin/${baseBranch} in this call, so its ` +
111
+ `distance is UNKNOWN rather than 0. A checkout that has not fetched cannot report that it has not ` +
112
+ `fetched — an unfetched ref answers "0 behind" and means nothing.`;
113
+ return out;
114
+ }
115
+
116
+ out.baseSha = run(resolved, ["rev-parse", base]);
117
+ const counts = run(resolved, ["rev-list", "--left-right", "--count", `${base}...HEAD`]);
118
+ const behind = counts ? Number(counts.split(/\s+/)[0]) : NaN;
119
+ if (!Number.isFinite(behind)) {
120
+ out.warning = `TREE FRESHNESS UNKNOWN: could not count ${base}...HEAD in ${resolved}.`;
121
+ return out;
122
+ }
123
+
124
+ out.behind = behind;
125
+ out.stale = behind > 0;
126
+ if (out.stale) {
127
+ // ⚠ FETCHING DID NOT FIX THE TREE — it only made this sentence possible.
128
+ // The file the verb read is still the old one.
129
+ out.warning =
130
+ `STALE TREE: ${resolved} is ${behind} commit(s) behind ${base} (HEAD ${head?.slice(0, 7)}, ` +
131
+ `${base} ${out.baseSha?.slice(0, 7)}). The answer above was read from THAT tree's files, so a row ` +
132
+ `filed or amended on ${base} since is invisible here and a superseded row can be offered as current. ` +
133
+ `Fetching refreshed the REF, not the working tree.`;
134
+ }
135
+ return out;
136
+ }