agent-coord-mcp 0.25.0 → 0.25.2
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.
- package/README.md +1 -1
- package/dist/server.js +1 -1
- package/hooks/submit.mjs +66 -1
- package/hooks/tmux-pusher.mjs +2 -1
- package/package.json +4 -3
- package/scripts/check-test-count.mjs +9 -3
- package/scripts/coord-pusher.mjs +3 -2
- package/src/server.ts +1 -1
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ If you're building an agent with the official MCP SDKs (`@modelcontextprotocol/s
|
|
|
69
69
|
| `heartbeat({agentId})` | Manual heartbeat. Usually unnecessary — agents with a live transport get heartbeats auto-bumped on every `list_agents`. |
|
|
70
70
|
| `list_agents()` | See all known agents, who looks online, and which `transport` (if any) they have attached. Validates transport pid liveness on every call. |
|
|
71
71
|
| `send_message({from, to?, room?, text, kind?})` | If `to` set → that agent's inbox (DM). Else → a channel: `room` names it (`"seo"` / `"#seo"`), omit for the default `general` channel. Tag channel posts with `kind: "decision"` for GOs/verdicts (longer retention, quoted in digests), `"status"` for progress notes; absent = chatter. |
|
|
72
|
-
| `send_command({from, to?, command, reminderMs?, reminderText?})` / `send_command({from, room, command, ...})` | Inject a context-management slash command (`/clear`, `/compact` or `/reload-skills`) **directly into a sub-agent's CLI** — delivered raw (no banner/prefix) so the receiving TUI runs it as a real slash command. `to` targets one agent; `room` broadcasts to a channel's tmux-attached members (never the sender). **Gated to tmux:** returns `ok:false` unless the target has a live `tmux-push`(`-remote`) transport. Command allowlist is locked to `/clear` + `/compact`. After `/clear`, a follow-up identity-reminder DM is auto-scheduled (`reminderMs:0` to opt out, `reminderText` to override) so the freshly-cleared worker re-anchors on its agentId and bus attach state. **Blocks until an out-of-band delivery receipt confirms the command reached the pane** (`delivery:"confirmed"`+`deliveredAt`, or `"pending"`+`warning` on a stale pusher); `waitForDelivery:false` for fire-and-forget, `deliveryTimeoutMs` to tune. See [clearing sub-agent context](#clearing-sub-agent-context-send_command). |
|
|
72
|
+
| `send_command({from, to?, command, reminderMs?, reminderText?})` / `send_command({from, room, command, ...})` | Inject a context-management slash command (`/clear`, `/compact` or `/reload-skills`) **directly into a sub-agent's CLI** — delivered raw (no banner/prefix) so the receiving TUI runs it as a real slash command. `to` targets one agent; `room` broadcasts to a channel's tmux-attached members (never the sender). **Gated to tmux:** returns `ok:false` unless the target has a live `tmux-push`(`-remote`) transport. Command allowlist is locked to `/clear` + `/compact` + `/reload-skills`. After `/clear`, a follow-up identity-reminder DM is auto-scheduled (`reminderMs:0` to opt out, `reminderText` to override) so the freshly-cleared worker re-anchors on its agentId and bus attach state. **Blocks until an out-of-band delivery receipt confirms the command reached the pane** (`delivery:"confirmed"`+`deliveredAt`, or `"pending"`+`warning` on a stale pusher); `waitForDelivery:false` for fire-and-forget, `deliveryTimeoutMs` to tune. See [clearing sub-agent context](#clearing-sub-agent-context-send_command). |
|
|
73
73
|
| `read_messages({agentId, source, room?, limit?, peek?, sinceTs?})` | Read new messages. `source` is `inbox`/`room`/`status`; for `room`, `room` picks the channel (default `general`). Advances the per-channel cursor unless `peek:true`. Room reads return the most recent `limit` (default 50); any older overflow is replaced by a compact `history` digest carrying a retrieval hash. |
|
|
74
74
|
| `retrieve_room_history({agentId, hash, query?})` | Expand a `history` digest from `read_messages` into the original overflow messages. `query` filters to matching messages (case-insensitive substring). Scoped to the producing agent; entries expire after 30 min. |
|
|
75
75
|
| `post_status({agentId, status, detail?})` | Append to the shared status stream (separate from chat). |
|
package/dist/server.js
CHANGED
|
@@ -160,7 +160,7 @@ function buildServer(initialBound, opts = {}) {
|
|
|
160
160
|
}
|
|
161
161
|
const server = new McpServer({
|
|
162
162
|
name: "agent-coord",
|
|
163
|
-
version: "0.25.
|
|
163
|
+
version: "0.25.2",
|
|
164
164
|
});
|
|
165
165
|
const addTool = (name, description, inputSchema, cb) => {
|
|
166
166
|
server.registerTool(name, { description, inputSchema: z.object(inputSchema) }, async (args) => cb((args ?? {})));
|
package/hooks/submit.mjs
CHANGED
|
@@ -38,6 +38,13 @@ export const ENTER_GAP_MS = () => envInt("AGENT_COORD_ENTER_GAP_MS", 150);
|
|
|
38
38
|
// often. Bounded and finite — this must not spin, and it must not stall the
|
|
39
39
|
// delivery loop behind a pane that will never change.
|
|
40
40
|
export const VERIFY_TIMEOUT_MS = () => envInt("AGENT_COORD_SUBMIT_VERIFY_MS", 1500);
|
|
41
|
+
// Ceiling for the paste-landing check: how long after paste-buffer we keep
|
|
42
|
+
// polling for the pasted text to APPEAR in the input line before refusing to
|
|
43
|
+
// send Enter. ENTER_DELAY_MS stays the floor (menu-settle reasoning above) —
|
|
44
|
+
// landing early never shortens it. On a slow machine the paste can take
|
|
45
|
+
// longer than any fixed delay; a blind Enter then submits whatever WAS in the
|
|
46
|
+
// input (nothing, or someone's draft) while the real payload lands after it.
|
|
47
|
+
export const PASTE_VERIFY_MS = () => envInt("AGENT_COORD_PASTE_VERIFY_MS", 4000);
|
|
41
48
|
export const VERIFY_POLL_MS = () => envInt("AGENT_COORD_SUBMIT_POLL_MS", 100);
|
|
42
49
|
// Extra Enters to try when the first pair didn't submit. 0 disables retrying.
|
|
43
50
|
export const ENTER_RETRIES = () => envInt("AGENT_COORD_ENTER_RETRIES", 2);
|
|
@@ -187,6 +194,25 @@ export function stillInInput(paneText, payload) {
|
|
|
187
194
|
return squash(draft).includes(needle);
|
|
188
195
|
}
|
|
189
196
|
|
|
197
|
+
// Has the PASTED payload appeared in the input line yet? Distinct from
|
|
198
|
+
// stillInInput (verify-by-absence after Enter): before Enter, "gone" cannot
|
|
199
|
+
// mean submitted — it means not-landed-yet. The input box wraps long pastes
|
|
200
|
+
// across lines the prompt regex does not match, so the visible draft may be
|
|
201
|
+
// only the head of the payload; match on a shared prefix in either direction.
|
|
202
|
+
// true = payload (or its head) is visibly in the input; false = input readable
|
|
203
|
+
// and payload not there; null = no readable input line (unknown TUI/capture).
|
|
204
|
+
export function landedInInput(paneText, payload) {
|
|
205
|
+
if (paneText === null || paneText === undefined) return null;
|
|
206
|
+
const needle = squash(payload);
|
|
207
|
+
if (!needle) return false;
|
|
208
|
+
const { draft } = readPaneState(paneText);
|
|
209
|
+
if (draft === null) return null;
|
|
210
|
+
const d = squash(draft);
|
|
211
|
+
if (!d) return false;
|
|
212
|
+
if (d.includes(needle.slice(0, 32))) return true; // draft carries the payload head
|
|
213
|
+
return needle.includes(d) && d.length >= 8; // visible draft is a genuine fragment of the payload
|
|
214
|
+
}
|
|
215
|
+
|
|
190
216
|
// Read the pane's input state: is the TUI busy, and is there unsent text?
|
|
191
217
|
//
|
|
192
218
|
// Accepts BOTH plain and styled (`capture-pane -e`) text — on styled input the
|
|
@@ -323,7 +349,30 @@ export async function pasteAndSubmit(deps, payload, { bracketed = false, verify
|
|
|
323
349
|
throw new Error(`tmux paste-buffer: ${String(paste.stderr ?? "").trim()}`);
|
|
324
350
|
}
|
|
325
351
|
|
|
326
|
-
|
|
352
|
+
// Verify the paste LANDED before any Enter (recurring loss on slow machines:
|
|
353
|
+
// the blind delay elapsed before the paste rendered, and the Enter submitted
|
|
354
|
+
// an empty input while the payload arrived after it). ENTER_DELAY_MS is the
|
|
355
|
+
// floor; PASTE_VERIFY_MS is the ceiling. Three outcomes:
|
|
356
|
+
// true — payload seen in the input; proceed to Enter.
|
|
357
|
+
// false — input line readable the whole time and the payload never
|
|
358
|
+
// appeared; refuse the Enter and say so (honest timeout, not a
|
|
359
|
+
// blind CR into someone's session).
|
|
360
|
+
// null — no readable input line (unknown TUI / capture failed); fall back
|
|
361
|
+
// to the legacy fixed-delay path — unknown never blocks delivery.
|
|
362
|
+
const floor = sleep(ENTER_DELAY_MS());
|
|
363
|
+
const landed = await pollUntilLanded(deps, payload);
|
|
364
|
+
await floor;
|
|
365
|
+
if (landed === false) {
|
|
366
|
+
return {
|
|
367
|
+
submitted: false,
|
|
368
|
+
verified: true, // we DID observe the input line; what we observed is a paste that never arrived
|
|
369
|
+
attempts: 0,
|
|
370
|
+
reason:
|
|
371
|
+
`pasted text did not appear in the input line of pane '${target}' within ${PASTE_VERIFY_MS()}ms — ` +
|
|
372
|
+
`Enter NOT sent (the paste may still land later; raise AGENT_COORD_PASTE_VERIFY_MS on slow machines, ` +
|
|
373
|
+
`and check the pane for a stranded draft before resending)`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
327
376
|
const e1 = run(["send-keys", "-t", target, "Enter"]);
|
|
328
377
|
if (e1.status !== 0) throw new Error(`tmux send-keys: ${String(e1.stderr ?? "").trim()}`);
|
|
329
378
|
await sleep(ENTER_GAP_MS());
|
|
@@ -369,6 +418,22 @@ export async function pasteAndSubmit(deps, payload, { bracketed = false, verify
|
|
|
369
418
|
}
|
|
370
419
|
}
|
|
371
420
|
|
|
421
|
+
// Poll capture-pane until the pasted payload APPEARS in the input line, the
|
|
422
|
+
// PASTE_VERIFY_MS budget runs out, or the input line proves unreadable.
|
|
423
|
+
// true = landed, false = readable-but-never-landed, null = never readable.
|
|
424
|
+
async function pollUntilLanded(deps, payload) {
|
|
425
|
+
const { run, target } = deps;
|
|
426
|
+
const deadline = Date.now() + PASTE_VERIFY_MS();
|
|
427
|
+
let readable = false;
|
|
428
|
+
for (;;) {
|
|
429
|
+
const verdict = landedInInput(captureStyled(run, target), payload);
|
|
430
|
+
if (verdict === true) return true;
|
|
431
|
+
if (verdict === false) readable = true;
|
|
432
|
+
if (Date.now() >= deadline) return readable ? false : null;
|
|
433
|
+
await sleep(VERIFY_POLL_MS());
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
372
437
|
// Poll capture-pane until the payload leaves the input line, the budget runs
|
|
373
438
|
// out, or we run out of ways to tell. true = still there, false = gone,
|
|
374
439
|
// null = unknown (capture failed, or no input line we can read).
|
package/hooks/tmux-pusher.mjs
CHANGED
|
@@ -224,7 +224,8 @@ function readJsonl(file) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// Allowlisted control commands the bus may type into this CLI. Mirrors
|
|
227
|
-
// CONTROL_COMMANDS in src/tools.ts
|
|
227
|
+
// CONTROL_COMMANDS in src/tools/transport.ts and scripts/coord-pusher.mjs
|
|
228
|
+
// (test/allowlist-parity.test.mjs locks the three); kept tiny + literal so a bugged/compromised
|
|
228
229
|
// sender can't smuggle an arbitrary slash command past the guard below.
|
|
229
230
|
const CONTROL_COMMANDS = new Set(["/clear", "/compact", "/reload-skills"]);
|
|
230
231
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-coord-mcp",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
4
4
|
"description": "File-backed MCP server for coordinating multiple AI coding agents (Claude Code, Cursor, Cline, etc.). Local stdio or networked over Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
47
47
|
"proper-lockfile": "^4.1.2",
|
|
48
48
|
"zod": "^4.4.3",
|
|
49
|
-
"@davidbalzan/groundwork-seam": "0.1.
|
|
49
|
+
"@davidbalzan/groundwork-seam": "0.1.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^26.2.0",
|
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
"typescript": "^7.0.2"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
|
-
"
|
|
58
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
59
|
+
"build": "pnpm run clean && tsc",
|
|
59
60
|
"start": "node dist/server.js",
|
|
60
61
|
"dev": "tsx src/server.ts",
|
|
61
62
|
"pretest": "node scripts/check-self-dependency.mjs && tsc",
|
|
@@ -22,13 +22,18 @@
|
|
|
22
22
|
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
24
|
|
|
25
|
-
const EXPECTED_TESTS =
|
|
25
|
+
const EXPECTED_TESTS = 285;
|
|
26
26
|
|
|
27
27
|
const expected = Number(process.env.AGENT_COORD_EXPECTED_TESTS ?? EXPECTED_TESTS);
|
|
28
28
|
// Same glob the suite always used — `--test test/` would recurse differently
|
|
29
29
|
// on some node versions, and a runner that selects a different set of files
|
|
30
30
|
// is exactly the failure this script exists to catch.
|
|
31
|
-
|
|
31
|
+
// Reporter pinned to TAP: newer Node defaults to the spec reporter even when
|
|
32
|
+
// piped (and env like FORCE_COLOR can style its summary lines), so grepping
|
|
33
|
+
// the summary was machine-dependent — it broke David's publish run 2026-08-20.
|
|
34
|
+
// TAP is stable, uncolored, and the format the totals-check below was written
|
|
35
|
+
// against.
|
|
36
|
+
const child = spawn(process.execPath, ["--test", "--test-reporter=tap", "test/*.test.mjs"], {
|
|
32
37
|
stdio: ["inherit", "pipe", "inherit"],
|
|
33
38
|
shell: false,
|
|
34
39
|
});
|
|
@@ -41,7 +46,8 @@ child.stdout.on("data", (chunk) => {
|
|
|
41
46
|
|
|
42
47
|
child.on("exit", (code, signal) => {
|
|
43
48
|
if (signal) process.exit(1);
|
|
44
|
-
// TAP
|
|
49
|
+
// TAP is forced above, so `# pass N` is the contract; the `ℹ` alternative
|
|
50
|
+
// stays as a belt against a runner that ignores the flag.
|
|
45
51
|
const num = (label) => {
|
|
46
52
|
const m = out.match(new RegExp(`^(?:#|ℹ) ${label} (\\d+)\\s*$`, "m"));
|
|
47
53
|
return m ? Number(m[1]) : null;
|
package/scripts/coord-pusher.mjs
CHANGED
|
@@ -179,8 +179,9 @@ let pending = [];
|
|
|
179
179
|
let debounceTimer = null;
|
|
180
180
|
let sending = false;
|
|
181
181
|
|
|
182
|
-
// Allowlisted control commands (mirrors CONTROL_COMMANDS in src/tools.ts
|
|
183
|
-
|
|
182
|
+
// Allowlisted control commands (mirrors CONTROL_COMMANDS in src/tools/transport.ts and
|
|
183
|
+
// hooks/tmux-pusher.mjs — test/allowlist-parity.test.mjs locks the three together).
|
|
184
|
+
const CONTROL_COMMANDS = new Set(["/clear", "/compact", "/reload-skills"]);
|
|
184
185
|
|
|
185
186
|
// Shells we refuse to inject into: if the pane's foreground command is one of
|
|
186
187
|
// these, the agent CLI has exited and typing would run pasted text as commands.
|
package/src/server.ts
CHANGED