agent-coord-mcp 0.25.3 → 0.25.5

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/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.3",
163
+ version: "0.25.5",
164
164
  });
165
165
  const addTool = (name, description, inputSchema, cb) => {
166
166
  server.registerTool(name, { description, inputSchema: z.object(inputSchema) }, async (args) => cb((args ?? {})));
File without changes
package/hooks/submit.mjs CHANGED
@@ -182,6 +182,20 @@ export function partitionStyledLine(styledLine) {
182
182
  // lesson is narrower than "test on a real TUI": a check that reads scrollback
183
183
  // has to be tested against a pane that HAS scrollback.
184
184
  //
185
+ // Claude Code collapses a long bracketed paste into a chip in the input:
186
+ // ❯ [Pasted text #14 +9 lines]
187
+ // That chip IS the landed paste — the full payload never appears as plain
188
+ // text. Treating it as "not landed" was the intermittent Enter miss David hit:
189
+ // landedInInput returned false → Enter was refused → chip sat forever. The
190
+ // opposite failure (chip still sitting after Enter, verifier saying "gone"
191
+ // because the payload string isn't in the chip label) is handled in
192
+ // stillInInput below.
193
+ export const PASTE_CHIP_RE = /\[Pasted text #\d+/i;
194
+
195
+ export function isPasteChip(draft) {
196
+ return PASTE_CHIP_RE.test(String(draft ?? ""));
197
+ }
198
+
185
199
  // Returns true = still waiting in the input, false = gone (submitted),
186
200
  // null = UNKNOWN. Unknown is never upgraded to a confirmation: no pane text,
187
201
  // or no line matching AGENT_COORD_PROMPT_PATTERN, means we cannot tell.
@@ -191,7 +205,12 @@ export function stillInInput(paneText, payload) {
191
205
  if (!needle) return false;
192
206
  const { draft } = readPaneState(paneText);
193
207
  if (draft === null) return null; // no recognizable input line — cannot tell
194
- return squash(draft).includes(needle);
208
+ const d = squash(draft);
209
+ if (!d) return false;
210
+ // A paste chip still in the input means Enter did not submit — regardless
211
+ // of whether the full payload string is visible inside the chip label.
212
+ if (isPasteChip(d)) return true;
213
+ return d.includes(needle);
195
214
  }
196
215
 
197
216
  // Has the PASTED payload appeared in the input line yet? Distinct from
@@ -209,6 +228,8 @@ export function landedInInput(paneText, payload) {
209
228
  if (draft === null) return null;
210
229
  const d = squash(draft);
211
230
  if (!d) return false;
231
+ // Claude Code paste chip = landed (collapsed). See PASTE_CHIP_RE.
232
+ if (isPasteChip(d)) return true;
212
233
  if (d.includes(needle.slice(0, 32))) return true; // draft carries the payload head
213
234
  return needle.includes(d) && d.length >= 8; // visible draft is a genuine fragment of the payload
214
235
  }
@@ -336,12 +357,17 @@ export async function submitControl(deps, payload) {
336
357
  // run(args) -> {status, stdout, stderr} // spawnSync-shaped
337
358
  // target, buffer // tmux -t target, buffer name
338
359
  //
339
- // Returns {submitted, verified, reason?, attempts}. For bracketed peer content
340
- // verification is skipped entirely (`verified:false`, `submitted:true`) — that
341
- // path pastes inert data on the hot path and has never been the problem; only
342
- // control commands are verified.
343
- export async function pasteAndSubmit(deps, payload, { bracketed = false, verify = false } = {}) {
360
+ // Returns {submitted, verified, reason?, attempts}.
361
+ //
362
+ // Bracketed peer content verifies by DEFAULT. The old hot-path shortcut
363
+ // (`verify:false` fire-and-forget Enter) left Claude Code paste chips
364
+ // (`[Pasted text #N +X lines]`) sitting unsubmitted whenever Enter was
365
+ // dropped or refused — no retry, no receipt of failure. Control commands
366
+ // already verified; peer traffic gets the same loop now. Pass
367
+ // `verify:false` explicitly to opt out (unknown-TUI / test escape hatch).
368
+ export async function pasteAndSubmit(deps, payload, { bracketed = false, verify } = {}) {
344
369
  const { run, runStdin, target, buffer } = deps;
370
+ const doVerify = verify === undefined ? bracketed : verify;
345
371
 
346
372
  await runStdin(["load-buffer", "-b", buffer, "-"], payload);
347
373
  const paste = run(["paste-buffer", ...(bracketed ? ["-p"] : []), "-b", buffer, "-t", target, "-d"]);
@@ -378,7 +404,7 @@ export async function pasteAndSubmit(deps, payload, { bracketed = false, verify
378
404
  await sleep(ENTER_GAP_MS());
379
405
  run(["send-keys", "-t", target, "Enter"]);
380
406
 
381
- if (!verify) return { submitted: true, verified: false, attempts: 1 };
407
+ if (!doVerify) return { submitted: true, verified: false, attempts: 1 };
382
408
 
383
409
  const retries = ENTER_RETRIES();
384
410
  for (let attempt = 1; ; attempt++) {
@@ -428,8 +428,12 @@ async function injectViaTmux(batch) {
428
428
  let run = [];
429
429
  const flushRun = async () => {
430
430
  if (run.length === 0) return;
431
- await pasteAndSubmit(formatBatch(run), true); // peer content: inert bracketed paste
432
- writeReceipts(run); // stamp only AFTER the paste+submit resolves
431
+ // bracketed=true submit.mjs verifies by default (paste-chip land + Enter retry).
432
+ const outcome = await pasteAndSubmit(formatBatch(run), true);
433
+ if (outcome && outcome.submitted === false) {
434
+ process.stderr.write(`[tmux-pusher] peer paste NOT submitted: ${outcome.reason}\n`);
435
+ }
436
+ writeReceipts(run, outcome); // stamp only AFTER the paste+submit resolves
433
437
  run = [];
434
438
  };
435
439
  for (const m of batch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-coord-mcp",
3
- "version": "0.25.3",
3
+ "version": "0.25.5",
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": {
@@ -8,17 +8,6 @@
8
8
  "coord-chat": "scripts/coord-chat.mjs",
9
9
  "coord-pusher": "scripts/coord-pusher.mjs"
10
10
  },
11
- "scripts": {
12
- "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
13
- "build": "pnpm run clean && tsc",
14
- "prepare": "node scripts/check-self-dependency.mjs && pnpm run clean && tsc",
15
- "prepack": "node scripts/check-self-dependency.mjs && pnpm run build",
16
- "start": "node dist/server.js",
17
- "dev": "tsx src/server.ts",
18
- "pretest": "node scripts/check-self-dependency.mjs && tsc",
19
- "test": "node scripts/check-test-count.mjs",
20
- "test:raw": "node --test \"test/*.test.mjs\""
21
- },
22
11
  "files": [
23
12
  "dist",
24
13
  "src",
@@ -54,15 +43,24 @@
54
43
  "node": ">=18"
55
44
  },
56
45
  "dependencies": {
57
- "@davidbalzan/groundwork-seam": "workspace:*",
58
46
  "@modelcontextprotocol/sdk": "^1.30.0",
59
47
  "proper-lockfile": "^4.1.2",
60
- "zod": "^4.4.3"
48
+ "zod": "^4.4.3",
49
+ "@davidbalzan/groundwork-seam": "0.1.2"
61
50
  },
62
51
  "devDependencies": {
63
52
  "@types/node": "^26.2.0",
64
53
  "@types/proper-lockfile": "^4.1.4",
65
54
  "tsx": "^4.23.12",
66
55
  "typescript": "^7.0.2"
56
+ },
57
+ "scripts": {
58
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
59
+ "build": "pnpm run clean && tsc",
60
+ "start": "node dist/server.js",
61
+ "dev": "tsx src/server.ts",
62
+ "pretest": "node scripts/check-self-dependency.mjs && tsc",
63
+ "test": "node scripts/check-test-count.mjs",
64
+ "test:raw": "node --test \"test/*.test.mjs\""
67
65
  }
68
- }
66
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ // prepublishOnly: refuse a publish that would ship a literal workspace:* dep.
3
+ //
4
+ // npm does NOT rewrite `workspace:` protocols at publish time; pnpm does.
5
+ // 0.25.3 shipped `"@davidbalzan/groundwork-seam": "workspace:*"` to the
6
+ // registry exactly this way (npm publish from the package dir, 2026-08-22) and
7
+ // every `npm i -g` failed with "unsupported URL type workspace:". The repo's
8
+ // pack gate checks pnpm's tarball, which is always clean — the npm-publish
9
+ // path had no witness. This is it.
10
+ import { readFileSync } from "node:fs";
11
+
12
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
13
+ const carries = JSON.stringify({ ...pkg.dependencies, ...pkg.optionalDependencies }).includes("workspace:");
14
+ const agent = process.env.npm_config_user_agent ?? "";
15
+ if (carries && !agent.startsWith("pnpm")) {
16
+ console.error(
17
+ "[check-publish-tool] REFUSED: package.json carries a workspace:* dependency and the publisher is " +
18
+ `'${agent.split("/")[0] || "unknown"}', which will NOT rewrite it — the published tarball would be uninstallable ` +
19
+ "(0.25.3 shipped exactly this). Publish with `pnpm publish --filter agent-coord-mcp --access public`, " +
20
+ "or `pnpm --filter agent-coord-mcp pack` and `npm publish <tarball>`.",
21
+ );
22
+ process.exit(1);
23
+ }
@@ -22,7 +22,7 @@
22
22
 
23
23
  import { spawn } from "node:child_process";
24
24
 
25
- const EXPECTED_TESTS = 286;
25
+ const EXPECTED_TESTS = 291;
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
File without changes
@@ -300,8 +300,9 @@ async function injectViaTmux(batch) {
300
300
  let run = [];
301
301
  const flushRun = async () => {
302
302
  if (run.length === 0) return;
303
- await pasteAndSubmit(formatBatch(run), true); // peer content: inert bracketed paste
304
- await reportReceipts(run); // stamp only AFTER the paste+submit resolves
303
+ // bracketed=true submit.mjs verifies by default (paste-chip land + Enter retry).
304
+ const outcome = await pasteAndSubmit(formatBatch(run), true);
305
+ await reportReceipts(run, outcome); // stamp only AFTER the paste+submit resolves
305
306
  run = [];
306
307
  };
307
308
  for (const m of batch) {
@@ -328,11 +329,9 @@ async function injectViaTmux(batch) {
328
329
  // Wire counterpart to tmux-pusher's writeReceipts: this pusher cannot append
329
330
  // to receipts/<id>.jsonl on the server's filesystem, so it reports each
330
331
  // delivery over MCP and the server writes the same receipt line the local
331
- // path does. `outcome` (control commands only) carries what submit
332
- // verification actually observed — submitted/verified/reason are forwarded
333
- // verbatim and OMITTED entirely for ordinary peer batches, because an absent
334
- // `submitted` means "typed but unverified" and must never be upgraded to a
335
- // claim of execution this pusher did not make.
332
+ // path does. `outcome` carries what submit verification observed
333
+ // submitted/verified/reason are forwarded for both control commands and
334
+ // (as of 0.25.5) bracketed peer pastes.
336
335
  //
337
336
  // Best-effort by design: a failed report must not break delivery or crash the
338
337
  // inject loop (the message IS in the pane by the time we get here). The
File without changes
File without changes
File without changes
package/src/server.ts CHANGED
@@ -241,7 +241,7 @@ function buildServer(initialBound?: string, opts: { trackSession?: boolean } = {
241
241
 
242
242
  const server = new McpServer({
243
243
  name: "agent-coord",
244
- version: "0.25.3",
244
+ version: "0.25.5",
245
245
  });
246
246
 
247
247
  const addTool = (