agent-coord-mcp 0.26.28 → 0.26.30

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.
@@ -93,18 +93,25 @@ function rel(file) {
93
93
  return file ? `test/${path.basename(file)}` : "(unattributed)";
94
94
  }
95
95
 
96
- if (failures.length) {
96
+ // ⟨q-4b2362a8⟩ — A FAILING TEST MUST NOT MASK STRUCTURAL DRIFT. This used to report the failures and
97
+ // exit, so an unrecorded test file stayed invisible behind any red: #393's `bind-list.test.mjs` was
98
+ // missing from the record for days, hidden behind the quiet-fleet failure of merge-window. The
99
+ // failures are still the headline — a count over a failing suite means little — but the drift that
100
+ // does NOT depend on the counts (a file nobody recorded, a record entry with no file) is reported in
101
+ // the same run, because one red hiding another is how both survive.
102
+ const failed = failures.length > 0;
103
+ if (failed) {
97
104
  console.error(`[check-test-count] ${failures.length} test(s) FAILED — the count is not the story here:`);
98
105
  for (const f of failures) console.error(` ${f}`);
99
- process.exit(1);
100
106
  }
101
107
 
102
108
  // `=0` disables; any other override asserts the old single global total.
103
- if (globalOverride === 0) {
109
+ // Both early exits are skipped on a failing suite: a green there would report a pass over a red run.
110
+ if (globalOverride === 0 && !failed) {
104
111
  console.log(`[check-test-count] disabled by AGENT_COORD_EXPECTED_TESTS=0 — ${total} test(s) passed, unchecked`);
105
112
  process.exit(0);
106
113
  }
107
- if (globalOverride !== null && Number.isFinite(globalOverride)) {
114
+ if (globalOverride !== null && Number.isFinite(globalOverride) && !failed) {
108
115
  if (total !== globalOverride) {
109
116
  console.error(`[check-test-count] FAIL — ${total} tests passed, override expected ${globalOverride}.`);
110
117
  process.exit(1);
@@ -115,11 +122,15 @@ if (globalOverride !== null && Number.isFinite(globalOverride)) {
115
122
 
116
123
  // ── the real comparison, per file ──────────────────────────────────────────
117
124
  const drifted = [];
125
+ // STRUCTURAL drift does not depend on the counts — which file is recorded at all, which record entry
126
+ // has no file. It is the half that still means something when the suite is red (⟨q-4b2362a8⟩).
127
+ const structural = [];
118
128
  for (const f of files) {
119
129
  const want = expected[f];
120
130
  const got = actual.get(f) ?? 0;
121
131
  if (want === undefined) {
122
- drifted.push(`${f}: ran ${got} test(s) and is NOT in the record — a new test file must be recorded in the same commit`);
132
+ const line = `${f}: ran ${got} test(s) and is NOT in the record — a new test file must be recorded in the same commit`;
133
+ drifted.push(line); structural.push(line);
123
134
  } else if (got !== want) {
124
135
  drifted.push(`${f}: ran ${got}, record says ${want}`);
125
136
  }
@@ -134,15 +145,30 @@ for (const f of files) {
134
145
  // exists to exercise — the guard's own test caught it.
135
146
  for (const f of Object.keys(expected)) {
136
147
  if (!existsSync(path.join(pkgRoot, f))) {
137
- drifted.push(`${f}: in the record but no such file exists — deleted, or renamed and its coverage lost`);
148
+ const line = `${f}: in the record but no such file exists — deleted, or renamed and its coverage lost`;
149
+ drifted.push(line); structural.push(line);
138
150
  } else if (files.includes(f) === false && GLOB === "test/*.test.mjs") {
139
- drifted.push(`${f}: exists but did not run — the glob no longer selects it`);
151
+ const line = `${f}: exists but did not run — the glob no longer selects it`;
152
+ drifted.push(line); structural.push(line);
140
153
  }
141
154
  }
142
155
  // An unattributed pass would make every per-file number meaningless.
143
156
  const unattributed = actual.get("(unattributed)") ?? 0;
144
157
  if (unattributed) drifted.push(`${unattributed} test(s) passed with no file attribution — the per-file record cannot be verified against this run`);
145
158
 
159
+ // On a red suite: report the structural half (never the counts, which a failing test legitimately
160
+ // changes) and exit on the failures.
161
+ if (failed) {
162
+ if (structural.length) {
163
+ console.error(`[check-test-count] AND, not masked by those failures — ${structural.length} structural drift(s) in the record:`);
164
+ for (const d of structural) console.error(` ${d}`);
165
+ console.error(` Record them with \`node scripts/check-test-count.mjs --write\` once the suite is green.`);
166
+ } else {
167
+ console.error(`[check-test-count] no structural record drift besides the failures above (per-file counts are not judged over a red suite)`);
168
+ }
169
+ process.exit(1);
170
+ }
171
+
146
172
  if (!drifted.length) {
147
173
  console.log(
148
174
  `[check-test-count] ${total} test(s) across ${files.length} file(s), each matching its recorded count — ` +
@@ -64,7 +64,7 @@ import { queueWriteSchema } from "./tools/queue-write.js";
64
64
  import { treeProvenance } from "./tools/tree-provenance.js";
65
65
  import { detectSpread } from "./server-spread.js";
66
66
  import { answeringServerIdentity } from "./tools/registry.js";
67
- import { HerdrTransport, herdrKeyName } from "./transports/herdr.js";
67
+ import { HerdrTransport, herdrKeyName, interpretHerdrReply, submitModeOf, SUBMIT_MODE_ENV_VAR } from "./transports/herdr.js";
68
68
  import { tickVerdict } from "./tools/tick.js";
69
69
  import { ensureHerdrTail, herdrTailState, newContext, newMessagesIn, stopHerdrTail } from "./tools/herdr-tail.js";
70
70
  import { HERDR, TICK_READS_AS, TICK_STORED_AS, type TickState } from "./transports/types.js";
@@ -461,6 +461,62 @@ const PROBES: Probe[] = [
461
461
  };
462
462
  },
463
463
  },
464
+ {
465
+ // ⟨q-f7e3c701⟩ 0.26.29 — ATOMIC SUBMIT. Two properties, and BOTH are here because each one
466
+ // alone would have shipped a defect that was actually shipped:
467
+ //
468
+ // 1. THE PARSE. #397 first read `json?.status ?? json?.result?.status`. The real herdr 0.9.1
469
+ // reply carries `result.type`, and there is no `status` anywhere — so on real hardware the
470
+ // success check NEVER fired and every genuine delivery fell through to unverified /
471
+ // safeToRetry:false, which is the DROP path. It would have reported 100% failure while
472
+ // succeeding, and the canary would have binned a working fix on false evidence.
473
+ // ⛔ THE UNIT TESTS COULD NOT CATCH IT: they mocked `{status:"agent_prompted"}` — the code's
474
+ // own assumption — so they asserted the belief rather than the world. The payloads below are
475
+ // VERBATIM CAPTURES from a real binary, which is the only fixture shape that can fail.
476
+ //
477
+ // 2. THE SAFE DEFAULT. Blast radius is both fleets and the failure mode is silence, so the
478
+ // canary must be opt-in by EXACT literal. A near-miss must NOT opt in.
479
+ //
480
+ // Both are pure predicates handed the REJECTED input, per this file's mutation-matrix rule:
481
+ // a probe that cannot be given the case it must refuse is decorative.
482
+ id: "atomic-submit-parses-real-herdr-replies",
483
+ since: "0.26.29",
484
+ run: () => {
485
+ // VERBATIM from `herdr agent prompt` on herdr 0.9.1 — success carries result.type.
486
+ const REAL_PROMPTED = '{"id":"cli:agent:prompt","result":{"agent":{"agent":"claude","agent_status":"idle","pane_id":"w16:pD"},"type":"agent_prompted"}}';
487
+ // VERBATIM from the same binary against a blocked agent — a TOP-LEVEL error.code, no result.
488
+ const REAL_BLOCKED = '{"error":{"code":"agent_blocked","message":"agent w16:pD is blocked and requires interactive input"},"id":"cli:agent:prompt"}';
489
+
490
+ const okReply = interpretHerdrReply({ status: 0, stdout: REAL_PROMPTED, stderr: "" });
491
+ const blockedReply = interpretHerdrReply({ status: 1, stdout: REAL_BLOCKED, stderr: "" });
492
+
493
+ const prompted = (okReply.json as { result?: { type?: string } } | undefined)?.result?.type === "agent_prompted";
494
+ // The two live in DIFFERENT PLACES — one field cannot serve both, which was the bug.
495
+ const blockedCode = blockedReply.ok === false && blockedReply.error?.code === "agent_blocked";
496
+ // ⛔ THE NEGATIVE CONTROL FOR THE PARSE: the old expression must NOT find anything, or this
497
+ // probe would pass on the broken code it exists to refuse.
498
+ const oldPathFindsNothing =
499
+ (okReply.json as { status?: string; result?: { status?: string } } | undefined)?.status === undefined &&
500
+ (okReply.json as { status?: string; result?: { status?: string } } | undefined)?.result?.status === undefined;
501
+
502
+ const defaultsSafe = submitModeOf({}) === "send-keys";
503
+ const optsIn = submitModeOf({ [SUBMIT_MODE_ENV_VAR]: "agent-prompt" }) === "agent-prompt";
504
+ // ⛔ NEGATIVE CONTROLS FOR THE FLAG: a trailing space and a near-miss must NOT flip a fleet.
505
+ const typoStaysSafe =
506
+ submitModeOf({ [SUBMIT_MODE_ENV_VAR]: "agent-prompt " }) === "send-keys" &&
507
+ submitModeOf({ [SUBMIT_MODE_ENV_VAR]: "AGENT-PROMPT" }) === "send-keys" &&
508
+ submitModeOf({ [SUBMIT_MODE_ENV_VAR]: "true" }) === "send-keys";
509
+
510
+ const present = prompted && blockedCode && oldPathFindsNothing && defaultsSafe && optsIn && typoStaysSafe;
511
+ return {
512
+ present,
513
+ evidence:
514
+ `real agent_prompted -> result.type read=${prompted} · real agent_blocked -> error.code read=${blockedCode} · ` +
515
+ `old \`status\` path finds nothing on the real payload=${oldPathFindsNothing} · ` +
516
+ `default=${JSON.stringify(submitModeOf({}))} safe=${defaultsSafe} · exact opt-in=${optsIn} · near-misses stay send-keys=${typoStaysSafe} -> present=${present}`,
517
+ };
518
+ },
519
+ },
464
520
  {
465
521
  // ⟨q-1c95f7d4⟩ Phase 5.4 Task 5 — the external tick, probed by CALLING the code that
466
522
  // decides what a reading MEANS (every probe here is synchronous, so the async read is
@@ -567,6 +623,35 @@ const PROBES: Probe[] = [
567
623
  return { present, evidence: `module ${me.serverModule ?? "UNRESOLVED"} · dead stamp -> unknown: ${staleUnknown} · split + unstamped seat -> ${split?.state ?? "n/a"} -> present=${present}` };
568
624
  },
569
625
  },
626
+ {
627
+ // ⟨q-caa2959e⟩ (0.26.30) — the merge-window live control's APPLICABILITY is own landings in the
628
+ // horizon, never its own window count. A quiet fleet (record commits, no landing) is NOT
629
+ // APPLICABLE and says so with both counts; a landing whose PR announced no window is a problem,
630
+ // which is the violation the control exists to catch and the one a count-based rule would skip
631
+ // green. Asked of the running function, so a server on the pre-fix build answers false.
632
+ id: "merge-window-applies-on-own-landings",
633
+ since: "0.26.30",
634
+ run: () => {
635
+ const T = Date.parse("2026-09-20T12:00:00Z");
636
+ const landing = { sha: "a".repeat(40), at: T, author: "seat", subject: "docs(done): close it (#900)", mergeOf: 900 };
637
+ const plain = { sha: "b".repeat(40), at: T, author: "seat", subject: "docs(board): claim a row", mergeOf: null };
638
+ const opts = { now: T + 60_000, floorMs: T - 60_000 };
639
+ const unannounced = mergeWindowLivePopulation({ logText: "", writes: [landing], ...opts });
640
+ const quiet = mergeWindowLivePopulation({ logText: "", writes: [plain], ...opts });
641
+ const windowsOnly = mergeWindowLivePopulation({ logText: `{"ts":${T},"from":"qa","to":"#k","text":"QA GATE: PASS #900\nMERGE WINDOW: #900 — hold queue/board writes"}`, writes: [], ...opts });
642
+ const present =
643
+ unannounced.applicable === true && unannounced.missingWindow.length === 1 &&
644
+ quiet.applicable === false && /landed nothing in the horizon/.test(quiet.reason) && /0 own landing\(s\)/.test(quiet.reason) &&
645
+ windowsOnly.applicable === false;
646
+ return {
647
+ present,
648
+ evidence:
649
+ `landing with no window -> applicable=${unannounced.applicable} missingWindow=${unannounced.missingWindow.length} · ` +
650
+ `record commit, no landing -> applicable=${quiet.applicable} (names both counts: ${/0 own landing\(s\)/.test(quiet.reason)}) · ` +
651
+ `windows but no landing -> applicable=${windowsOnly.applicable} -> present=${present}`,
652
+ };
653
+ },
654
+ },
570
655
  {
571
656
  id: "record-authority",
572
657
  since: "0.24.0",
@@ -750,7 +835,7 @@ export function probeCapabilities(context: { module: string; versionLabel: strin
750
835
 
751
836
  import { resolveServerIdentity } from "./server-identity.js";
752
837
  import { seatBuildOf, installedFrom, psReader, type SeatBuild } from "./tools/seat-build.js";
753
- import { isInFlightStatus, gateClaimsIn } from "./tools/stall.js";
838
+ import { isInFlightStatus, gateClaimsIn, mergeWindowLivePopulation } from "./tools/stall.js";
754
839
  import { parseWorkDoc, queueItemsOf, stampQueueIds } from "@davidbalzan/groundwork-seam";
755
840
 
756
841
  export const capabilitiesSchema = {} as const;
@@ -127,6 +127,19 @@ export async function tailOnce(
127
127
  // read its marker file on every tick while the stated idle cost said otherwise.
128
128
  if (refresh) {
129
129
  ctx.marker = (opts.markerOf ?? readOwnMarker)(agentId);
130
+ // ⟨q-d58ccdce⟩ ⛔⛆⛆ THIS BRANCH IS THE WHOLE DEFECT. `marker.rooms === false` makes
131
+ // `ctx.rooms` PERMANENTLY EMPTY — every room this agent belongs to is silently excluded
132
+ // from `sources` below, so the per-source loop never runs for a single one of them. That
133
+ // is a total, structural skip with no `continue` to log: #395's per-outcome push logging
134
+ // fires only for a source the loop actually reaches, and a room that is never a source
135
+ // produces zero log lines forever — indistinguishable from a room with nothing owed.
136
+ // Measured live 2026-09-19: every groundwork-kit-* seat's marker had `rooms:false` (a
137
+ // side effect of `attach_agent {..., includeRoom:false}`, called believing it only
138
+ // affected DM liveness), while every unaffected control seat had `rooms:true`. So THIS
139
+ // is the log line the class needed — not one more line inside a loop that never starts.
140
+ if (ctx.marker && ctx.marker.transport === HERDR && ctx.marker.rooms === false) {
141
+ log(`[herdr-tail] rooms DISABLED for ${agentId} — marker.rooms=false, so no room is ever tailed regardless of membership (⟨q-d58ccdce⟩); re-attach with includeRoom:true (or omit it) to restore room delivery`);
142
+ }
130
143
  if (ctx.marker && ctx.marker.transport === HERDR && ctx.marker.rooms !== false) {
131
144
  try {
132
145
  const all = await (opts.roomsOf ?? getRooms)();
@@ -150,13 +163,20 @@ export async function tailOnce(
150
163
  // ⛔ FIRST SIGHT OF A SOURCE UNDER seedEof: adopt EOF and type NOTHING. This is the only place
151
164
  // the starting offset is decided, and it is decided EXPLICITLY rather than inherited from a
152
165
  // cursor file that a restart may have reset. Per source, so a room joined later seeds on its
153
- // own first sight instead of replaying its history.
166
+ // own first sight instead of replaying its history. Logged (low-frequency: once per source
167
+ // per process) so a seat that never types into a newly-joined room is distinguishable from
168
+ // one that was never told the room existed.
154
169
  if (ctx.seedEof && !ctx.seeded.has(key)) {
155
170
  ctx.seeded.add(key);
156
171
  ctx.hw.set(key, size);
172
+ log(`[herdr-tail] seeded ${key} at EOF (${size} bytes) for ${agentId} — first sight this process, nothing predating the tail will be typed`);
157
173
  continue;
158
174
  }
159
175
  const from = await offsetOf(agentId, src, ctx);
176
+ // ⭐ NEGATIVE CONTROL FOR THE CLASS THIS ROW IS ABOUT: nothing is logged here when
177
+ // `size === from` — a source with nothing owed must stay silent, or every idle room on
178
+ // every tick becomes a line and the log that is supposed to make a real stall visible
179
+ // drowns in ones that are not.
160
180
  if (size <= from) continue;
161
181
 
162
182
  for (const { msg, end } of newMessagesIn(src.file, from)) {
@@ -512,6 +512,57 @@ export function mergeWindowChecks(input: { logText: string; writes: RecordWrite[
512
512
  return { hits: withAudience(hits), windows: out, unmeasurable };
513
513
  }
514
514
 
515
+ /**
516
+ * ⟨q-caa2959e⟩ — WHETHER THE MERGE-WINDOW CONVENTION CAN BE JUDGED ON THIS BUS RIGHT NOW,
517
+ * decided from OWN LANDINGS IN THE HORIZON and never from the window count.
518
+ *
519
+ * The live check used to assert `windows.length > 0`. A stand-down left the fleet quiet: 0 window
520
+ * announcements inside the horizon while record commits remained, so a control pinned to a live
521
+ * baseline failed for everyone — it expired when the fleet went quiet, not when anything broke.
522
+ *
523
+ * ⛔ AND APPLICABILITY IS NEVER READ OFF THE PREDICATE'S OWN COUNT. Deciding it from `windows.length`
524
+ * is the by-mention trap ⟨q-552b9912⟩ taught us, inverted: a fleet that landed PRs and announced NO
525
+ * window — precisely the violation this check exists to catch — would skip itself green. The
526
+ * population is what the fleet DID (its landings); the windows are what it is judged ON.
527
+ *
528
+ * Applicable when the horizon holds at least one landing of this fleet's (a squash carrying the
529
+ * forge's `(#N)` marker). Then every one of them MUST carry a window announcement, and a missing one
530
+ * is a problem. With no landing there is nothing to judge: NOT APPLICABLE, named with both counts.
531
+ */
532
+ export function mergeWindowLivePopulation(input: { logText: string; writes: RecordWrite[] | null; now: number; floorMs?: number; horizonMs?: number }): {
533
+ applicable: boolean;
534
+ reason: string;
535
+ population: string;
536
+ landings: RecordWrite[];
537
+ missingWindow: { sha: string; pr: number }[];
538
+ windows: ReturnType<typeof mergeWindowChecks>["windows"];
539
+ hits: ReturnType<typeof mergeWindowChecks>["hits"];
540
+ } {
541
+ const floor = input.floorMs ?? MERGE_WINDOW_ADOPTED_MS;
542
+ const horizon = input.horizonMs ?? CONVENTION_WINDOW_MS;
543
+ const r = mergeWindowChecks(input);
544
+ const since = new Date(input.now - horizon).toISOString();
545
+ const head = `horizon ${since} → ${new Date(input.now).toISOString()}, floor ${new Date(floor).toISOString()}`;
546
+ if (input.writes === null) {
547
+ const reason = `NOT APPLICABLE: record-document writes could not be read from the shared branch — ${head}. UNMEASURED is not the same as kept.`;
548
+ return { applicable: false, reason, population: reason, landings: [], missingWindow: [], windows: r.windows, hits: r.hits };
549
+ }
550
+ const inHorizon = input.writes.filter((c) => Number.isFinite(c.at) && c.at >= input.now - horizon);
551
+ const landings = inHorizon.filter((c) => c.mergeOf !== null);
552
+ const population = `${head}: ${inHorizon.length} record commit(s), ${landings.length} own landing(s), ${r.windows.length} window(s), ${r.hits.length} hit(s)`;
553
+ if (landings.length === 0) {
554
+ return {
555
+ applicable: false,
556
+ reason: `NOT APPLICABLE: this fleet landed nothing in the horizon, so there is no window to require — ${population}. (A quiet fleet, not a broken convention; the window count is never what decides this.)`,
557
+ population, landings, missingWindow: [], windows: r.windows, hits: r.hits,
558
+ };
559
+ }
560
+ const missingWindow = landings
561
+ .filter((c) => !r.windows.some((w) => w.pr === c.mergeOf))
562
+ .map((c) => ({ sha: c.sha, pr: c.mergeOf as number }));
563
+ return { applicable: true, reason: `APPLICABLE — ${population}`, population, landings, missingWindow, windows: r.windows, hits: r.hits };
564
+ }
565
+
515
566
  export function conventionChecks(input: {
516
567
  recentMerges: RecentMerge[] | null | undefined;
517
568
  /** PRs the caller knows merged express. Nothing populates this yet — see isDocsOnlyMerge. */
@@ -170,8 +170,28 @@ export type HerdrTransportOptions = {
170
170
  * variable at a time, six others left in place — was what turned it green.
171
171
  */
172
172
  env?: Record<string, string | undefined>;
173
+ /** Override the submit mode this instance uses; defaults to `submitModeOf(this.#env)`. Tests only. */
174
+ submitMode?: SubmitMode;
173
175
  };
174
176
 
177
+ /**
178
+ * ⟨q-f7e3c701⟩ — WHICH SUBMIT PATH `push` USES. `agent-prompt` is the atomic fix
179
+ * (`herdr agent prompt <target> <text>`, one call, no type→read-back→Enter window). `send-keys`
180
+ * is today's path (type, verify it landed, Enter, verify it submitted).
181
+ *
182
+ * DEFAULT IS `send-keys`, DELIBERATELY. The blast radius of switching is both fleets and the
183
+ * failure mode is silence — a wrong default ships broken delivery to every seat with no signal
184
+ * until someone notices nothing arrived. `agent-prompt` is enabled per-seat as a canary via
185
+ * `AGENT_COORD_SUBMIT=agent-prompt`, compared against control seats by `outcome=` rate in
186
+ * bus-daemon.err.log, and reverted by unsetting the variable — no republish required, which
187
+ * matters because 0.26.28 took two days to travel merged→observed.
188
+ */
189
+ export type SubmitMode = "agent-prompt" | "send-keys";
190
+ export const SUBMIT_MODE_ENV_VAR = "AGENT_COORD_SUBMIT";
191
+ export function submitModeOf(env: Record<string, string | undefined> = process.env): SubmitMode {
192
+ return env[SUBMIT_MODE_ENV_VAR] === "agent-prompt" ? "agent-prompt" : "send-keys";
193
+ }
194
+
175
195
  /**
176
196
  * ⟨q-abd88dd4⟩ — THE PID A HERDR MARKER CARRIES, chosen for the readers that CANNOT be patched.
177
197
  *
@@ -225,6 +245,7 @@ export class HerdrTransport implements Transport {
225
245
  #readLines: number;
226
246
  #markerPid: () => HerdrMarkerPid;
227
247
  #env: Record<string, string | undefined>;
248
+ #submitMode: SubmitMode;
228
249
 
229
250
  constructor(opts: HerdrTransportOptions = {}) {
230
251
  this.#run = opts.run ?? defaultHerdrRunner;
@@ -237,6 +258,7 @@ export class HerdrTransport implements Transport {
237
258
  this.#readLines = opts.readLines ?? 40;
238
259
  this.#markerPid = opts.markerPid ?? (() => herdrMarkerPid());
239
260
  this.#env = opts.env ?? process.env;
261
+ this.#submitMode = opts.submitMode ?? submitModeOf(this.#env);
240
262
  }
241
263
 
242
264
  /** Is herdr on this host AND is its server running? Both, or the reason. */
@@ -330,6 +352,9 @@ export class HerdrTransport implements Transport {
330
352
  if (!avail.available) return { delivered: false, outcome: "held", safeToRetry: true, error: avail.reason };
331
353
  const target = targetOf(marker);
332
354
  if (!target) return { delivered: false, outcome: "held", safeToRetry: true, error: "no target recorded on the marker" };
355
+ // ⟨q-f7e3c701⟩ ATOMIC SUBMIT, canaried per seat via AGENT_COORD_SUBMIT. Everything below this
356
+ // branch is the send-keys path (today's default); #pushAgentPrompt is the opt-in replacement.
357
+ if (this.#submitMode === "agent-prompt") return this.#pushAgentPrompt(target, text);
333
358
  const payload = opts.paste === false ? text : `${PASTE_START}${neutralizeControls(text)}${PASTE_END}`;
334
359
  const enterKey = herdrKeyName("enter");
335
360
  if (!enterKey.ok) return { delivered: false, outcome: "held", safeToRetry: true, error: enterKey.error };
@@ -363,6 +388,73 @@ export class HerdrTransport implements Transport {
363
388
  return { delivered: false, verified: true, outcome: "pending", safeToRetry: true, enters, error: `text still sitting unsubmitted in ${target}'s input after ${enters} enters` };
364
389
  }
365
390
 
391
+ /**
392
+ * ⟨q-f7e3c701⟩ ATOMIC SUBMIT, behind `AGENT_COORD_SUBMIT=agent-prompt` (default `send-keys` —
393
+ * see `submitModeOf`). `herdr agent prompt <target> <text> --wait --until working` types and
394
+ * submits in ONE call: no type→read-back→Enter window for a decoration to land in, which is
395
+ * the class the send-keys path above exists to survive. MEASURED (joint cross-fleet design,
396
+ * 2026-09-21): 4/4 across two hosts, including a LIVE WEDGE the send-keys path had already
397
+ * abandoned in the same minute.
398
+ *
399
+ * ⛔ `refusalBeforeKeys` IS NOT OPTIONAL — MEASURED ON A THROWAWAY PANE: `agent prompt` into a
400
+ * DIRTY composer returned `agent_prompted` (success) and CONCATENATED the stale draft with the
401
+ * new text, no separator, then submitted both. Atomic submit alone is strictly WORSE than
402
+ * today: a recoverable `held` becomes unrecoverable corruption carrying a success receipt.
403
+ * `agent_blocked` is a STATUS axis and is BLIND to a dirty composer (reads
404
+ * `status: idle · interactive_ready: true` regardless), so it cannot substitute for reading
405
+ * the box first — this method still reads it, exactly as the send-keys path does.
406
+ *
407
+ * Receipt is `--until working`, an OBSERVED state transition — strictly stronger than "I typed
408
+ * it and could read it back", the only receipt class send-keys has ever had. A stall/timeout is
409
+ * treated as UNCONFIRMED, never as safe to retry: unlike send-keys's `typed-unconfirmed` (where
410
+ * nothing had actually been typed yet), a stalled atomic submit may already have gone through,
411
+ * and retrying risks a genuine double-send.
412
+ *
413
+ * ⛔⛔ THE FIELD PATH BELOW WAS WRONG ONCE, SHIPPED, AND CAUGHT BY QA'S OWN LIVE SMOKE TEST
414
+ * BEFORE MERGE — recorded because the wrong version passed every unit test, having been
415
+ * written against a GUESSED shape (`json.status ?? json.result.status`) that the mocks then
416
+ * reproduced instead of measuring. Against the real CLI that path is always `undefined`, so
417
+ * every genuine success fell through to the catch-all and the feature would have reported
418
+ * 100% failure on real hardware. VERIFIED PAYLOADS, captured against herdr 0.9.1 by the aide
419
+ * and independently reproduced here on a disposable scratch pane (`herdr workspace create` /
420
+ * `agent start --kind claude` / closed after):
421
+ * success: {"result":{"agent":{...},"type":"agent_prompted"}} — result.type, not result.status
422
+ * blocked: {"error":{"code":"agent_blocked","message":"..."}} — TOP-LEVEL error, no `result` at all
423
+ * timeout: {"error":{"code":"timeout","message":"..."}} — same top-level shape as blocked
424
+ * So this is TWO PATHS, not one field read: `result.type` for success, `error.code` for every
425
+ * refusal. A single-location parse mishandles one of them regardless of which is picked — the
426
+ * exact shape of the bug that shipped. `agent_prompt_stalled` was NOT reproduced against a real
427
+ * binary despite attempting it (the 5000ms stall window did not fire in either kit's or my own
428
+ * attempts) — it is handled via the same `error.code` path as `agent_blocked`/`timeout` BY
429
+ * ANALOGY with the CLI's consistent refuse-via-top-level-error convention, not by measurement.
430
+ * If that analogy is wrong, the failure mode is `unverified/safeToRetry:false` (never a
431
+ * double-send) rather than a false "delivered", so the untested case fails safe.
432
+ */
433
+ async #pushAgentPrompt(target: string, text: string): Promise<HerdrPushResult> {
434
+ if (readyProfile().profile !== "none") {
435
+ const before = readReadyBox(this.#screen(target));
436
+ const refusal = refusalBeforeKeys(before);
437
+ if (refusal) return { delivered: false, verified: false, outcome: "held", safeToRetry: true, error: `held, nothing typed into ${target}: ${refusal}` };
438
+ }
439
+ const r = this.#run(["agent", "prompt", target, text, "--wait", "--until", "working"]);
440
+ const resultType = (r.json as { result?: { type?: string } } | undefined)?.result?.type;
441
+ if (r.ok && resultType === "agent_prompted") {
442
+ return { delivered: true, verified: true, outcome: "delivered", safeToRetry: false };
443
+ }
444
+ if (r.error?.code === "agent_blocked") {
445
+ // MEASURED: nothing is typed into the pane on agent_blocked — the agent itself refused the
446
+ // prompt before any input was sent. Safe to retry, same as a send-keys `held`.
447
+ return { delivered: false, verified: false, outcome: "held", safeToRetry: true, error: `agent_blocked at ${target}: ${r.error.message}` };
448
+ }
449
+ if (r.error?.code === "agent_prompt_stalled" || r.error?.code === "timeout") {
450
+ return {
451
+ delivered: false, verified: false, outcome: "unverified", safeToRetry: false,
452
+ error: `${r.error.code} at ${target}: submission unconfirmed by --wait --until working — not retried, to avoid a double-send on an atomic path`,
453
+ };
454
+ }
455
+ return { delivered: false, verified: false, outcome: "unverified", safeToRetry: false, error: `agent prompt to ${target} refused or unrecognised: ${r.error?.message ?? r.stderr ?? JSON.stringify(r.json)}` };
456
+ }
457
+
366
458
  /** The screen, styled, so the ready-box reader can tell a ghost suggestion from a draft. null = unreadable. */
367
459
  #screen(target: string): string | null {
368
460
  const read = this.#run(["pane", "read", target, "--source", "visible", "--lines", String(this.#readLines), "--format", "ansi"]);
@@ -14,7 +14,7 @@ import { configuredTransport } from "./config.js";
14
14
  export * from "./types.js";
15
15
  export * from "./config.js";
16
16
  export { TmuxTransport, tmuxAvailable, paneExists, probePane, tmuxVersion, type TmuxHost } from "./tmux.js";
17
- export { HerdrTransport, defaultHerdrRunner, interpretHerdrReply, herdrKeyName, herdrPaneExists, herdrMarkerPid, HERDR_KEYS, HERDR_ABSENT_MESSAGE, type HerdrRunner, type HerdrResult, type HerdrMarkerPid } from "./herdr.js";
17
+ export { HerdrTransport, defaultHerdrRunner, interpretHerdrReply, herdrKeyName, herdrPaneExists, herdrMarkerPid, submitModeOf, SUBMIT_MODE_ENV_VAR, HERDR_KEYS, HERDR_ABSENT_MESSAGE, type HerdrRunner, type HerdrResult, type HerdrMarkerPid, type SubmitMode } from "./herdr.js";
18
18
 
19
19
  let host: TmuxHost | undefined;
20
20