agent-coord-mcp 0.26.21 → 0.26.22

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 (62) hide show
  1. package/dist/capabilities.js +250 -2
  2. package/dist/capabilities.js.map +1 -1
  3. package/dist/closing-line.js +83 -0
  4. package/dist/closing-line.js.map +1 -0
  5. package/dist/commit-cite.js +55 -0
  6. package/dist/commit-cite.js.map +1 -0
  7. package/dist/gated-head.js +67 -20
  8. package/dist/gated-head.js.map +1 -1
  9. package/dist/server-spread.js +195 -0
  10. package/dist/server-spread.js.map +1 -0
  11. package/dist/server.js +2 -2
  12. package/dist/server.js.map +1 -1
  13. package/dist/store.js +32 -0
  14. package/dist/store.js.map +1 -1
  15. package/dist/tools/away.js +67 -7
  16. package/dist/tools/away.js.map +1 -1
  17. package/dist/tools/board-ref.js +44 -4
  18. package/dist/tools/board-ref.js.map +1 -1
  19. package/dist/tools/event-kinds.js +5 -1
  20. package/dist/tools/event-kinds.js.map +1 -1
  21. package/dist/tools/events.js +31 -2
  22. package/dist/tools/events.js.map +1 -1
  23. package/dist/tools/messaging.js +64 -6
  24. package/dist/tools/messaging.js.map +1 -1
  25. package/dist/tools/record-events.js +85 -5
  26. package/dist/tools/record-events.js.map +1 -1
  27. package/dist/tools/records.js +231 -38
  28. package/dist/tools/records.js.map +1 -1
  29. package/dist/tools/registry.js +52 -2
  30. package/dist/tools/registry.js.map +1 -1
  31. package/dist/tools/seat-build.js +173 -0
  32. package/dist/tools/seat-build.js.map +1 -0
  33. package/dist/tools/shared.js.map +1 -1
  34. package/dist/tools/stall.js +1095 -18
  35. package/dist/tools/stall.js.map +1 -1
  36. package/dist/tools/transport.js +21 -2
  37. package/dist/tools/transport.js.map +1 -1
  38. package/dist/tools/worktrees.js +14 -0
  39. package/dist/tools/worktrees.js.map +1 -1
  40. package/package.json +1 -1
  41. package/scripts/coord-attention-clock.mjs +2 -0
  42. package/scripts/coord-stall-clock.mjs +52 -11
  43. package/src/capabilities.ts +264 -2
  44. package/src/closing-line.ts +85 -0
  45. package/src/commit-cite.ts +58 -0
  46. package/src/gated-head.ts +128 -26
  47. package/src/server-spread.ts +233 -0
  48. package/src/server.ts +2 -2
  49. package/src/store.ts +32 -0
  50. package/src/tools/away.ts +82 -9
  51. package/src/tools/board-ref.ts +70 -3
  52. package/src/tools/event-kinds.ts +17 -1
  53. package/src/tools/events.ts +33 -2
  54. package/src/tools/messaging.ts +63 -6
  55. package/src/tools/record-events.ts +78 -5
  56. package/src/tools/records.ts +248 -38
  57. package/src/tools/registry.ts +54 -3
  58. package/src/tools/seat-build.ts +194 -0
  59. package/src/tools/shared.ts +22 -0
  60. package/src/tools/stall.ts +1266 -23
  61. package/src/tools/transport.ts +21 -2
  62. package/src/tools/worktrees.ts +13 -0
@@ -0,0 +1,194 @@
1
+ /*
2
+ * ⟨q-8a3f1c05⟩ — A HALF-RESTARTED SEAT IS A STATEMENT, NOT AN INFERENCE.
3
+ *
4
+ * Each seat runs TWO long-lived processes from the installed tree: the MCP server
5
+ * (answers `capabilities`) and the tmux pusher (`hooks/tmux-pusher.mjs --agent <id>`,
6
+ * pastes into the pane). A restart cycles the server and leaves the pusher, so a seat
7
+ * comes back running new server code behind an old pusher — fully responsive, `online:
8
+ * true`, and invisible to every instrument that reads the server. Measured 2026-09-14
9
+ * during the staged restart: stage 1's pusher was three days older than its server and
10
+ * was found only because that seat happened to look; a stale pusher still delivers.
11
+ *
12
+ * THE INSTRUMENT ALREADY EXISTED AND NOBODY CALLED IT: pushers self-identify on the
13
+ * command line (`--agent <id>`), so `ps` + `--agent` + the installed HOOK's mtime is a
14
+ * per-seat pusher-staleness probe. This module states it, per seat, beside the server
15
+ * half — in words, not a pair of timestamps a reader has to subtract.
16
+ *
17
+ * THE HOOK'S OWN MTIME, NEVER A SIBLING'S: `hooks/tmux-pusher.mjs` is the file the
18
+ * pusher loads. `package.json`, `dist/…` and the hook all carry the same instant under
19
+ * a whole-tree install, but an install that rewrites some files and not others breaks
20
+ * that, and nothing checks it. So the pusher half reads the hook file directly.
21
+ *
22
+ * THE LIMIT THIS CARRIES RATHER THAN SOLVES: servers carry no `--agent`, so the server
23
+ * side cannot be keyed to a seat from `ps`. Only the ANSWERING process knows its own
24
+ * start time (`answeredBy.pid`). For every other seat the server half is reported as
25
+ * UNOBSERVABLE from here — said, not inferred from the marker's attach-time stamp.
26
+ */
27
+ import { execFileSync } from "node:child_process";
28
+ import { statSync } from "node:fs";
29
+ import path from "node:path";
30
+ import type { TransportMarker } from "../transports/types.js";
31
+
32
+ export type ProcessFacts = { pid: number; startedAt: number | null; command: string | null; alive: boolean };
33
+ export type PsReader = (pid: number) => ProcessFacts;
34
+ export type StatReader = (p: string) => { mtimeMs: number };
35
+ export type Installed = { module: string; hookPath: string; hookMtime: number | null; buildMtime: number | null };
36
+ export type ServerFacts = { pid: number; startedAt: number; buildMtime: number | null } | null;
37
+
38
+ export type SeatBuild = {
39
+ agentId: string;
40
+ verdict: "current" | "half-restarted" | "stale" | "unknown";
41
+ statement: string;
42
+ server: { observable: boolean; pid: number | null; startedAt: string | null; buildMtime: string | null; current: boolean | null; note: string };
43
+ pusher: {
44
+ pid: number | null;
45
+ alive: boolean | null;
46
+ keyedByAgentArg: boolean | null;
47
+ startedAt: string | null;
48
+ /** The hook file THIS pusher loads, read off its own command line — never a sibling, never the server's tree. */
49
+ hookPath: string | null;
50
+ hookMtime: string | null;
51
+ /** false when the pusher runs from a different tree than the answering server (a dev checkout vs the global install, or two installs). */
52
+ sameTreeAsServer: boolean | null;
53
+ current: boolean | null;
54
+ note: string;
55
+ };
56
+ };
57
+
58
+ const iso = (ms: number | null | undefined) => (typeof ms === "number" && Number.isFinite(ms) ? new Date(ms).toISOString() : null);
59
+
60
+ /** `ps` for one pid: start time and command line. `lstart` is portable across macOS and Linux. */
61
+ export const psReader: PsReader = (pid) => {
62
+ try {
63
+ const out = execFileSync("ps", ["-o", "lstart=,command=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
64
+ if (!out) return { pid, startedAt: null, command: null, alive: false };
65
+ // lstart is 24 chars ("Mon Sep 14 14:38:24 2026"), then the command.
66
+ const m = /^(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(.*)$/.exec(out);
67
+ const startedAt = m ? Date.parse(m[1]) : NaN;
68
+ return { pid, startedAt: Number.isFinite(startedAt) ? startedAt : null, command: m ? m[2] : out, alive: true };
69
+ } catch {
70
+ return { pid, startedAt: null, command: null, alive: false };
71
+ }
72
+ };
73
+
74
+ /** The installed tree's hook and build stamps, read from the files themselves. */
75
+ export function installedFrom(moduleRoot: string, stat: (p: string) => { mtimeMs: number } = statSync): Installed {
76
+ const hookPath = path.join(moduleRoot, "hooks", "tmux-pusher.mjs");
77
+ const read = (p: string) => {
78
+ try {
79
+ return stat(p).mtimeMs;
80
+ } catch {
81
+ return null;
82
+ }
83
+ };
84
+ return { module: moduleRoot, hookPath, hookMtime: read(hookPath), buildMtime: read(path.join(moduleRoot, "package.json")) };
85
+ }
86
+
87
+ const day = (ms: number) => new Date(ms).toISOString().slice(0, 10);
88
+
89
+ /**
90
+ * One seat's build state: the pusher half from its marker's pid via `ps`, keyed by the
91
+ * `--agent` argument on that process's own command line; the server half from the
92
+ * answering process when the seat IS the answering process, else unobservable.
93
+ */
94
+ /** The hook file a pusher loads, read off its own command line: `node <path>/hooks/tmux-pusher.mjs --agent <id>`. */
95
+ export function hookPathOf(command: string): string | null {
96
+ const m = /(\S*\/hooks\/tmux-pusher\.mjs)(?=\s|$)/.exec(command);
97
+ return m ? m[1] : null;
98
+ }
99
+
100
+ export function seatBuildOf(input: { agentId: string; marker: TransportMarker | undefined; installed: Installed; ps: PsReader; server: ServerFacts; stat?: StatReader }): SeatBuild {
101
+ const { agentId, marker, installed, ps, server } = input;
102
+ const stat = input.stat ?? statSync;
103
+
104
+ // ---- pusher half
105
+ let pusher: SeatBuild["pusher"];
106
+ const empty = { pid: null, alive: null, keyedByAgentArg: null, startedAt: null, hookPath: null, hookMtime: null, sameTreeAsServer: null, current: null };
107
+ if (!marker || typeof marker.pid !== "number") {
108
+ pusher = { ...empty, note: "no transport marker for this seat — no pusher pid to read" };
109
+ } else {
110
+ const facts = ps(marker.pid);
111
+ if (!facts.alive) {
112
+ pusher = { ...empty, pid: marker.pid, alive: false, note: `the marker names pid ${marker.pid} and no such process is running — the pusher is dead, not stale` };
113
+ } else {
114
+ const cmd = facts.command ?? "";
115
+ const keyed = /tmux-pusher\.mjs/.test(cmd) && new RegExp(`--agent\\s+${agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`).test(cmd);
116
+ if (!keyed) {
117
+ pusher = { ...empty, pid: marker.pid, alive: true, keyedByAgentArg: false, startedAt: iso(facts.startedAt), note: `pid ${marker.pid} is alive but its command line does not say \`tmux-pusher.mjs --agent ${agentId}\` — the marker's pid is not this seat's pusher, so nothing is claimed about it` };
118
+ } else {
119
+ // THE VERY FILE THIS PUSHER LOADS. Its path is on the command line; the mtime is
120
+ // read from that path, not from the answering server's tree — a dev checkout
121
+ // answering for a fleet of global-install pushers would otherwise call every
122
+ // pusher stale against a file none of them ever loaded (measured while building
123
+ // this: 6 of 6 read stale against a worktree's hook, all 6 current against their own).
124
+ const hookPath = hookPathOf(cmd) ?? installed.hookPath;
125
+ const fromOwnCmd = hookPathOf(cmd) !== null;
126
+ let hookMtime: number | null = null;
127
+ try {
128
+ hookMtime = stat(hookPath).mtimeMs;
129
+ } catch {
130
+ hookMtime = null;
131
+ }
132
+ const sameTree = path.resolve(path.dirname(path.dirname(hookPath))) === path.resolve(installed.module);
133
+ if (facts.startedAt === null || hookMtime === null) {
134
+ pusher = { ...empty, pid: marker.pid, alive: true, keyedByAgentArg: true, startedAt: iso(facts.startedAt), hookPath, sameTreeAsServer: sameTree, note: facts.startedAt === null ? "ps gave no start time for the pusher" : `the hook ${hookPath} could not be read` };
135
+ } else {
136
+ const current = facts.startedAt >= hookMtime;
137
+ pusher = {
138
+ pid: marker.pid,
139
+ alive: true,
140
+ keyedByAgentArg: true,
141
+ startedAt: iso(facts.startedAt),
142
+ hookPath,
143
+ hookMtime: iso(hookMtime),
144
+ sameTreeAsServer: sameTree,
145
+ current,
146
+ note:
147
+ (current
148
+ ? `pusher started ${iso(facts.startedAt)}, after its hook ${hookPath} (${iso(hookMtime)}) — it loaded that installed code`
149
+ : `pusher started ${iso(facts.startedAt)}, BEFORE its hook ${hookPath} (${iso(hookMtime)}) — it runs the code it loaded then, and still delivers`) +
150
+ (fromOwnCmd ? "" : " (hook path not on the command line; the answering server's tree was used)") +
151
+ (sameTree ? "" : `; NOTE the pusher's tree differs from the answering server's (${installed.module})`),
152
+ };
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ // ---- server half
159
+ let srv: SeatBuild["server"];
160
+ if (!server) {
161
+ srv = { observable: false, pid: null, startedAt: null, buildMtime: iso(installed.buildMtime), current: null, note: "the server side cannot be keyed to a seat from ps (servers carry no --agent); only the answering process knows its own start time (answeredBy.pid). Ask this seat's own `capabilities`." };
162
+ } else if (server.buildMtime === null) {
163
+ srv = { observable: true, pid: server.pid, startedAt: iso(server.startedAt), buildMtime: null, current: null, note: "the installed package.json could not be read, so the server's build cannot be placed" };
164
+ } else {
165
+ const current = server.startedAt >= server.buildMtime;
166
+ srv = { observable: true, pid: server.pid, startedAt: iso(server.startedAt), buildMtime: iso(server.buildMtime), current, note: current ? `server started ${iso(server.startedAt)}, on the ${day(server.buildMtime)} install` : `server started ${iso(server.startedAt)}, BEFORE the ${day(server.buildMtime)} install` };
167
+ }
168
+
169
+ // ---- the statement
170
+ let verdict: SeatBuild["verdict"];
171
+ let statement: string;
172
+ const p = pusher.current;
173
+ const s = srv.current;
174
+ if (s === true && p === true) {
175
+ verdict = "current";
176
+ statement = `${agentId}: current — server and pusher both started after the ${day(server!.buildMtime!)} install.`;
177
+ } else if (s === false && p === false) {
178
+ verdict = "stale";
179
+ statement = `${agentId}: stale — server and pusher both predate the install; nothing on this seat runs the installed code.`;
180
+ } else if (s === true && p === false) {
181
+ verdict = "half-restarted";
182
+ statement = `${agentId}: HALF-RESTARTED — server on the ${day(server!.buildMtime!)} install, pusher started before it (${pusher.startedAt}). The seat sends through new code and receives through old; it looks healthy. Cycle the pusher: detach_agent, then attach_agent, from this seat.`;
183
+ } else if (s === false && p === true) {
184
+ verdict = "half-restarted";
185
+ statement = `${agentId}: HALF-RESTARTED the other way — pusher on the installed hook, server started before the ${day(server!.buildMtime!)} install. Reload the server (relaunch the MCP client for this seat).`;
186
+ } else if (s === null && p !== null) {
187
+ verdict = "unknown";
188
+ statement = `${agentId}: pusher ${p ? "current" : "STALE — started before the installed hook"}; server UNOBSERVABLE from here (${srv.note}).`;
189
+ } else {
190
+ verdict = "unknown";
191
+ statement = `${agentId}: unknown — ${pusher.note}${srv.observable ? "" : `; ${srv.note}`}`;
192
+ }
193
+ return { agentId, verdict, statement, server: srv, pusher };
194
+ }
@@ -86,6 +86,28 @@ export type AgentEntry = {
86
86
  // OTHER agents' context on every multi-line send. That is why it is stamped,
87
87
  // counted, and reviewed rather than self-service.
88
88
  proseOnly?: { since: number; reason?: string };
89
+
90
+ /*
91
+ * ⛔⛆ THE ANSWERING PROCESS, PUBLISHED — `⟨q-cec42e20⟩`. ADDITIVE AND OPTIONAL:
92
+ * absent on every pre-existing entry, and no reader that ignores unknown keys changes
93
+ * behaviour.
94
+ *
95
+ * Two seats ran the same verb, got `153` and `138`, and both servers honestly reported
96
+ * `versionLabel 0.26.20`. Nothing detected it — each seat had to VOLUNTEER it.
97
+ *
98
+ * ⭐ WHY THESE TWO FIELDS AND NOT A VERSION: a server loads its code once, at spawn,
99
+ * so START TIME against the installed build is the axis that separates two processes
100
+ * running different code. A label cannot: it AGREED while the behaviour differed, and
101
+ * `serverBuildMtime` cannot either — it is stamped at ATTACH and read IDENTICAL across
102
+ * all six live transports while the spread was live.
103
+ *
104
+ * ⚠ AND IT IS THE SERVER, NEVER THE PUSHER. `transports/<agent>.json` already carries a
105
+ * pid and it is the pusher's — a different process, on different code, which is the
106
+ * distinction this fleet paid for twice on 2026-09-12.
107
+ */
108
+ serverPid?: number;
109
+ serverStartedAt?: number;
110
+ serverModule?: string;
89
111
  };
90
112
 
91
113
  /**