agent-coord-mcp 0.26.25 → 0.26.27

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.
@@ -1,6 +1,5 @@
1
1
  import { loadLiveTransports, isMarkerLive, isPidAlive, markerHoldsLiveProcess } from "./registry.js";
2
2
  import {
3
- TMUX_PUSH,
4
3
  registerTmuxHost,
5
4
  activeTransport,
6
5
  HERDR,
@@ -8,7 +7,6 @@ import {
8
7
  isLocallyProbeable,
9
8
  isTmuxKind,
10
9
  paneExists,
11
- probePane,
12
10
  tmuxVersion,
13
11
  targetOf,
14
12
  tmuxAvailable,
@@ -30,9 +28,9 @@ import { resolveServerIdentity } from "../server-identity.js";
30
28
  import { capabilitiesTool } from "../capabilities.js";
31
29
  import { sendMessageTool, readMessagesTool } from "./messaging.js";
32
30
  import { randomUUID } from "node:crypto";
33
- import { existsSync, openSync, readFileSync, watch } from "node:fs";
31
+ import { existsSync, readFileSync, watch } from "node:fs";
34
32
  import { promises as fsp } from "node:fs";
35
- import { spawn, spawnSync } from "node:child_process";
33
+ import { spawnSync } from "node:child_process";
36
34
  import { fileURLToPath } from "node:url";
37
35
  import { z } from "zod";
38
36
  import { seatBuildOf, installedFrom, psReader } from "./seat-build.js";
@@ -691,158 +689,22 @@ export async function attachAgentTool(args: {
691
689
  note: "herdr socket transport: no pusher process — delivery is made in-process by this server through herdr's socket API; liveness is herdr's own pane status",
692
690
  };
693
691
  }
694
- const target = args.tmuxTarget ?? process.env.TMUX_PANE;
695
- if (!target) {
696
- return {
697
- ok: false,
698
- error:
699
- "tmuxTarget not provided and the MCP server is not running inside tmux (no $TMUX_PANE). Pass tmuxTarget explicitly (e.g. '%42' or 'session:window.pane').",
700
- };
701
- }
702
-
703
- // Validate target exists. The probe's discriminating power, and the control
704
- // that proves it, are documented once on `paneExists`.
705
- const targetProbe = probePane(target);
706
- if (!targetProbe.exists) {
707
- return {
708
- ok: false,
709
- error: `tmux target '${target}' not found: ${targetProbe.stderr}`,
710
- };
711
- }
712
-
713
- // If something's already attached, refuse rather than spawn a second pusher.
714
- const existing = await readJson<TransportMarker | null>(transportFile(args.agentId), null);
715
- if (markerHoldsLiveProcess(existing)) {
716
- return {
717
- ok: false,
718
- error: `agent '${args.agentId}' already has a live ${existing!.transport} attached (pid ${existing!.pid}). Call detach_agent first.`,
719
- existing,
720
- };
721
- }
722
- // Clean up dead marker, if any.
723
- if (existing) await deleteFile(transportFile(args.agentId));
724
-
725
- const pusher = resolvePusherPath();
726
- if (!existsSync(pusher)) {
727
- return { ok: false, error: `tmux-pusher not found at ${pusher}` };
728
- }
729
-
730
- // Detached spawn so the pusher outlives this MCP request/process.
731
- const log = logFile(args.agentId, "pusher");
732
- await fsp.mkdir(path.dirname(log), { recursive: true });
733
- await fsp.mkdir(path.dirname(pidFile(args.agentId, "pusher")), { recursive: true });
734
- await fsp.mkdir(path.dirname(transportFile(args.agentId)), { recursive: true });
735
- const logFd = openSync(log, "a");
736
- // Default: deliver room broadcasts too. The bus is chat-first — silence on
737
- // a room post is a worse failure mode than a slightly noisier pane. Callers
738
- // who want DM-only can pass includeRoom:false explicitly.
739
- const includeRoom = args.includeRoom !== false;
740
- // Use the exact node binary running this server, not bare "node" — the MCP
741
- // server is often launched via an absolute path (nvm/Homebrew/bundled
742
- // runtime) that isn't on the spawned child's PATH, which would silently fail
743
- // the pusher launch ("attached but nothing arrives").
744
- // `--agent <id>` is inert to the pusher (env stays authoritative) but puts
745
- // the agentId in argv, so a pattern kill can be scoped to ONE pusher
746
- // (`pkill -f "tmux-pusher.mjs --agent <id>"`). Without it the only matchable
747
- // pattern was the script path, and a `pkill -f tmux-pusher.mjs` during one
748
- // agent's cleanup silently detached every live agent on the bus (2026-07-28).
749
- const child = spawn(process.execPath, [pusher, "--agent", args.agentId], {
750
- detached: true,
751
- stdio: ["ignore", logFd, logFd],
752
- env: {
753
- ...process.env,
754
- AGENT_COORD_ID: args.agentId,
755
- AGENT_COORD_TMUX_TARGET: target,
756
- ...(includeRoom ? { AGENT_COORD_INCLUDE_ROOM: "1" } : {}),
757
- ...(args.allowlist && args.allowlist.length > 0
758
- ? { AGENT_COORD_ALLOWLIST: args.allowlist.join(",") }
759
- : {}),
760
- ...(args.debounceMs ? { AGENT_COORD_DEBOUNCE_MS: String(args.debounceMs) } : {}),
761
- },
762
- });
763
- child.unref();
764
- const pid = child.pid;
765
- if (!pid) return { ok: false, error: "spawn returned no pid" };
766
-
767
- // Write pid file (for scripts) and transport marker (for list_agents).
768
- await fsp.writeFile(pidFile(args.agentId, "pusher"), String(pid), "utf8");
769
- // Stamp the pusher source's freshness so doctor() can flag a stale daemon if
770
- // it outlives a later upgrade of the on-disk code (see v0.8.1 → v0.8.2 bug
771
- // report: control commands silently dropped by pre-v0.8 in-memory code).
772
- const scriptMtime = newestPusherSourceMtime();
773
- const marker: TransportMarker = {
774
- agentId: args.agentId,
775
- transport: TMUX_PUSH,
776
- pid,
777
- // DUAL-WRITTEN, and the duplication is the point. `target` is what every
778
- // consumer now reads (`targetOf`); `tmuxTarget` is what the code a merge
779
- // revert restores reads. Writing only the new field would leave markers the
780
- // old server cannot parse, and that failure does not degrade gracefully —
781
- // it silences every attached lane at once.
782
- target,
783
- tmuxTarget: target,
784
- since: Date.now(),
785
- scriptMtime,
786
- // Provenance: the build identity THIS server loaded at startup — not a
787
- // fresh stat of dist/, because the code doing the stamping is the loaded
788
- // code, and after an in-place rebuild the two differ (that difference is
789
- // exactly what doctor's provenance check exists to surface).
790
- serverBuildMtime: SERVER_BUILD_MTIME,
791
- // WHAT this transport carries, recorded by the code that decides it.
792
- // `includeRoom` is what the pusher is actually spawned with a few lines
793
- // above, so the marker cannot claim a capability the process was not
794
- // given — the marker and the spawn come from one value, not two.
795
- rooms: includeRoom,
796
- };
797
- // Use updateJson so it lockfile-protects and creates the file atomically.
798
- await updateJson<TransportMarker>(transportFile(args.agentId), marker, () => marker);
799
-
800
- // Best-effort scan for a peek-coord.mjs hook wired to the same agentId —
801
- // both consumers share the cursor file and would race / double-deliver.
802
- const conflictingHook = await detectPeekCoordHook(args.agentId);
803
-
692
+ // ⟨q-ec020f6a⟩ slice C — local tmux-push DELETED outright: 0 of 13 live seats used it,
693
+ // and keeping it meant a second delivery mechanism (hooks/tmux-pusher.mjs, now removed)
694
+ // plus a role-card precondition that halted every live herdr seat on a false requirement.
695
+ // attach_agent handles exactly one kind now. A remote seat never came through here in
696
+ // the first place — it registers via report_transport (scripts/coord-pusher.mjs) — so
697
+ // this refuses rather than silently trying (and failing) to spawn a script that no
698
+ // longer exists.
804
699
  return {
805
- ok: true,
806
- agentId: args.agentId,
807
- transport: TMUX_PUSH,
808
- tmuxTarget: target,
809
- pid,
810
- log,
811
- ...(conflictingHook
812
- ? {
813
- warnings: [
814
- `peek-coord.mjs hook for agentId='${args.agentId}' detected in ${conflictingHook}. ` +
815
- `Running both transports causes double-delivery — disable one. ` +
816
- `Recommend removing the peek-coord hook entry since tmux-push supersedes it.`,
817
- ],
818
- }
819
- : {}),
700
+ ok: false,
701
+ error:
702
+ `attach_agent only attaches the herdr transport now (this fleet's active transport is ` +
703
+ `${activeT ? `'${activeT.kind}'` : "unconfigured"}) — local tmux-push was removed (⟨q-ec020f6a⟩). ` +
704
+ `A remote seat registers via report_transport, not attach_agent.`,
820
705
  };
821
706
  }
822
707
 
823
- async function detectPeekCoordHook(agentId: string): Promise<string | undefined> {
824
- const home = process.env.HOME ?? "";
825
- const cwd = process.cwd();
826
- const candidates = [
827
- path.join(home, ".claude", "settings.json"),
828
- path.join(home, ".claude", "settings.local.json"),
829
- path.join(cwd, ".claude", "settings.json"),
830
- path.join(cwd, ".claude", "settings.local.json"),
831
- ];
832
- for (const file of candidates) {
833
- if (!existsSync(file)) continue;
834
- try {
835
- const raw = await fsp.readFile(file, "utf8");
836
- if (raw.includes("peek-coord.mjs") && raw.includes(`AGENT_COORD_ID=${agentId}`)) {
837
- return file;
838
- }
839
- } catch {
840
- // unreadable, skip
841
- }
842
- }
843
- return undefined;
844
- }
845
-
846
708
  export const detachAgentSchema = {
847
709
  agentId: z.string().min(1),
848
710
  };
@@ -1772,6 +1634,16 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1772
1634
  if (!counts.has(file)) counts.set(file, await scanJsonl(file));
1773
1635
  return counts.get(file)!;
1774
1636
  };
1637
+ /** A PUSH cursor's bound: bytes on disk, the same figure `advancePushCursor` writes. ⟨q-f018dd51⟩ */
1638
+ const sizes = new Map<string, number>();
1639
+ const byteSizeOf = async (file: string) => {
1640
+ if (!sizes.has(file)) {
1641
+ let n = 0;
1642
+ try { n = (await fsp.stat(file)).size; } catch { n = 0; }
1643
+ sizes.set(file, n);
1644
+ }
1645
+ return sizes.get(file)!;
1646
+ };
1775
1647
 
1776
1648
  // 4. Cursor offsets past end-of-file (would return [] forever).
1777
1649
  //
@@ -1783,20 +1655,46 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1783
1655
  // agent's inbox in the first place.
1784
1656
  {
1785
1657
  const broken: string[] = [];
1658
+ const lagging: string[] = [];
1786
1659
  for (const fname of await listCursorFiles()) {
1787
1660
  const { id, kind } = agentIdFromCursorFilename(fname);
1788
1661
  const cursorPath = path.join(CURSOR_DIR, fname);
1789
1662
  const cursor = await readJson<Cursor>(cursorPath, {});
1790
1663
  const overflow: string[] = [];
1791
- const inboxMax = (await countFor(inboxFile(id))).parsed;
1792
- if ((cursor.inboxOffset ?? 0) > inboxMax) overflow.push(`inboxOffset ${cursor.inboxOffset}>${inboxMax}`);
1793
- const roomMax = (await countFor(ROOM_FILE)).parsed;
1794
- if ((cursor.roomOffset ?? 0) > roomMax) overflow.push(`roomOffset ${cursor.roomOffset}>${roomMax}`);
1795
- const statusMax = (await countFor(STATUS_FILE)).parsed;
1796
- if ((cursor.statusOffset ?? 0) > statusMax) overflow.push(`statusOffset ${cursor.statusOffset}>${statusMax}`);
1664
+ // ⛔⛔ THE TWO CURSOR KINDS ARE IN DIFFERENT UNITS, AND CONFLATING THEM ARMED A DESTRUCTIVE FIX.
1665
+ // A READ cursor (`<agent>.json`) indexes PARSED ENTRIES — a message count. A PUSH cursor
1666
+ // (`<agent>.push.json`) is a BYTE OFFSET: `advancePushCursor` writes `toOffset ?? statSync(file).size`,
1667
+ // "the byte just past the line that was delivered". Measuring a byte offset against a message
1668
+ // count makes EVERY push cursor on a non-trivial inbox read as past EOF — and because this
1669
+ // check is `fixable`, the "clamp" then rewinds it to the count. ⟨q-f018dd51⟩: on 2026-09-17 a
1670
+ // `doctor {fix:true}` did exactly that to 11 of 13 seats across two fleets in one call —
1671
+ // cass-poc-coordinator went from byte 3,413,506 to byte 992, i.e. to the START of its inbox.
1672
+ // Nothing replayed only because the running tails held an in-process high-water mark; the
1673
+ // damage was LATENT and would have fired on the next restart, when that mark is gone.
1674
+ const boundOf = async (file: string) => (kind === "push" ? await byteSizeOf(file) : (await countFor(file)).parsed);
1675
+ const unit = kind === "push" ? "bytes" : "entries";
1676
+ const inboxMax = await boundOf(inboxFile(id));
1677
+ if ((cursor.inboxOffset ?? 0) > inboxMax) overflow.push(`inboxOffset ${cursor.inboxOffset}>${inboxMax} ${unit}`);
1678
+ const roomMax = await boundOf(ROOM_FILE);
1679
+ if ((cursor.roomOffset ?? 0) > roomMax) overflow.push(`roomOffset ${cursor.roomOffset}>${roomMax} ${unit}`);
1680
+ const statusMax = await boundOf(STATUS_FILE);
1681
+ if ((cursor.statusOffset ?? 0) > statusMax) overflow.push(`statusOffset ${cursor.statusOffset}>${statusMax} ${unit}`);
1797
1682
  for (const [chan, off] of Object.entries(cursor.roomOffsets ?? {})) {
1798
- const max = (await countFor(roomFile(chan))).parsed;
1799
- if (off > max) overflow.push(`roomOffsets[${chan}] ${off}>${max}`);
1683
+ const max = await boundOf(roomFile(chan));
1684
+ if (off > max) overflow.push(`roomOffsets[${chan}] ${off}>${max} ${unit}`);
1685
+ }
1686
+ // ⭐ THE DIRECTION THAT WAS NEVER TESTED, AND THE ONE THAT ACTUALLY BIT. Everything above asks
1687
+ // "is the cursor PAST the end" — an over-run. A seat goes silently deaf from the opposite
1688
+ // shape: its push cursor sits BEHIND a grown inbox, so mail is owed and never typed
1689
+ // (⟨q-cdb5b007⟩). doctor reported "all cursor offsets are within bounds" for seats 10KB
1690
+ // behind, all day, while a worker missed its GO and idled 40 minutes.
1691
+ // ⛔ REPORTED, NEVER "FIXED": advancing a lagging cursor to EOF SKIPS the backlog rather than
1692
+ // delivering it. The remedy is a working tail; a clamp here would destroy the evidence and
1693
+ // the mail. This is why lag is deliberately absent from the `fix` branch below.
1694
+ if (kind === "push") {
1695
+ const size = await byteSizeOf(inboxFile(id));
1696
+ const behind = size - Number(cursor.inboxOffset ?? 0);
1697
+ if (behind > 0) lagging.push(`${id}: ${behind} byte(s) owed (push cursor ${cursor.inboxOffset ?? 0}/${size})`);
1800
1698
  }
1801
1699
  if (overflow.length) {
1802
1700
  broken.push(`${id}${kind === "push" ? " (push)" : ""}: ${overflow.join(", ")}`);
@@ -1807,7 +1705,9 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1807
1705
  if ((c.statusOffset ?? 0) > statusMax) c.statusOffset = statusMax;
1808
1706
  if (c.roomOffsets) {
1809
1707
  for (const chan of Object.keys(c.roomOffsets)) {
1810
- const max = counts.get(roomFile(chan))?.parsed ?? 0;
1708
+ // Same per-kind bound as the detection above — a clamp that used the other unit
1709
+ // would be the very rewind this row exists to stop.
1710
+ const max = kind === "push" ? (sizes.get(roomFile(chan)) ?? 0) : (counts.get(roomFile(chan))?.parsed ?? 0);
1811
1711
  if (c.roomOffsets[chan] > max) c.roomOffsets[chan] = max;
1812
1712
  }
1813
1713
  }
@@ -1820,10 +1720,24 @@ export async function doctorTool(args: { fix?: boolean; maxFileBytes?: number })
1820
1720
  findings.push({
1821
1721
  check: "cursor-past-eof",
1822
1722
  level: broken.length ? "error" : "ok",
1823
- detail: broken.length ? `${broken.length} cursor(s) with an offset past EOF — delivery stalled` : "all cursor offsets are within bounds",
1723
+ detail: broken.length
1724
+ ? `${broken.length} cursor(s) with an offset past EOF — delivery stalled`
1725
+ : "all cursor offsets are within bounds (push cursors measured in BYTES, read cursors in parsed entries)",
1824
1726
  fixable: true,
1825
1727
  items: broken.length ? broken : undefined,
1826
1728
  });
1729
+ // A SEPARATE FINDING, AND DELIBERATELY `fixable: false`. Lag is the ⟨q-cdb5b007⟩ deafness shape:
1730
+ // mail owed and never typed. It is a symptom of a tail that is not running or not typing, and
1731
+ // the remedy is that tail — advancing the cursor would SKIP the backlog, not deliver it.
1732
+ findings.push({
1733
+ check: "push-cursor-lag",
1734
+ level: lagging.length ? "warn" : "ok",
1735
+ detail: lagging.length
1736
+ ? `${lagging.length} seat(s) with mail owed but not yet typed into their pane — a tail that is not running, or running and holding. Check herdrTail: delivered/held tells you which, and they have opposite fixes. NOT auto-repairable: advancing a lagging cursor SKIPS the backlog instead of delivering it.`
1737
+ : "no push cursor is behind its inbox",
1738
+ fixable: false,
1739
+ items: lagging.length ? lagging : undefined,
1740
+ });
1827
1741
  }
1828
1742
 
1829
1743
  // 5. Malformed JSONL lines (silently desync offset math between server + hooks).
@@ -7,18 +7,27 @@
7
7
  * mid-process, and then two calls in one session disagree about what the fleet
8
8
  * is doing.
9
9
  *
10
- * ⛔ AN UNKNOWN VALUE REFUSES AT STARTUP. It does not fall back to `tmux-push`.
10
+ * ⛔ AN UNKNOWN VALUE REFUSES AT STARTUP. It does not fall back to anything.
11
11
  *
12
12
  * The reason is not tidiness. A SILENT FALLBACK AND A CORRECT DEFAULT PRODUCE
13
- * IDENTICAL EVIDENCE: both give you a fleet on tmux with nothing in any log, so
14
- * a typo in the config reads exactly like a deliberate default, and the person
15
- * who typed `heardr` spends the afternoon asking why their transport change did
16
- * nothing. Refusing is louder than the bug it prevents.
13
+ * IDENTICAL EVIDENCE: both give you a fleet on some transport with nothing in any
14
+ * log, so a typo in the config reads exactly like a deliberate default, and the
15
+ * person who typed `heardr` spends the afternoon asking why their transport
16
+ * change did nothing. Refusing is louder than the bug it prevents.
17
+ *
18
+ * ⟨q-ec020f6a⟩ THERE IS NO BUILT-IN DEFAULT ANY MORE, for the identical reason.
19
+ * `tmux-push` (0 of 13 live seats) used to be it; deleting that kind without
20
+ * removing the fallback would have meant every unconfigured session refused
21
+ * with "unknown transport 'tmux-push'" — a config-file bug wearing a runtime
22
+ * bug's clothes. Picking a new implicit default (herdr, say) would be correct
23
+ * on THIS fleet today and wrong on the next machine that doesn't run herdr —
24
+ * the exact silent-guess failure ⟨q-e439e4ad⟩ spent a day fixing, moved one
25
+ * layer up. So: zero configuration REFUSES, naming the kinds that exist.
17
26
  */
18
27
  import { existsSync, readFileSync } from "node:fs";
19
28
  import path from "node:path";
20
29
  import { ROOT } from "../store.js";
21
- import { TMUX_PUSH, TRANSPORT_KINDS, type TransportKind } from "./types.js";
30
+ import { TRANSPORT_KINDS, type TransportKind } from "./types.js";
22
31
 
23
32
  /** `$AGENT_COORD_DIR/config.json`, the fleet-wide file. */
24
33
  export const TRANSPORT_CONFIG_FILE = path.join(ROOT, "config.json");
@@ -39,7 +48,7 @@ export const TRANSPORT_ENV_VAR = "AGENT_COORD_TRANSPORT";
39
48
  */
40
49
  export type ConfiguredTransport = {
41
50
  kind: TransportKind;
42
- source: "config" | "env" | "default";
51
+ source: "config" | "env";
43
52
  /** Where the value came from, for an error message a human can act on. */
44
53
  origin: string;
45
54
  };
@@ -48,9 +57,9 @@ function refuse(value: string, origin: string): never {
48
57
  throw new Error(
49
58
  `[agent-coord-mcp] unknown transport ${JSON.stringify(value)} from ${origin}. ` +
50
59
  `Valid: ${TRANSPORT_KINDS.join(", ")}. ` +
51
- `REFUSING AT STARTUP rather than falling back to "${TMUX_PUSH}" — a silent fallback and a correct ` +
52
- `default leave identical evidence, so a typo here would look exactly like a working default and the ` +
53
- `transport change would appear to do nothing. Fix the value or remove it to get the default.`,
60
+ `REFUSING AT STARTUP rather than falling back to anything — a silent fallback and a correct ` +
61
+ `default leave identical evidence, so a typo here would look exactly like a working value and the ` +
62
+ `transport change would appear to do nothing. Fix the value or remove it (⟨q-ec020f6a⟩: there is no default to fall back to).`,
54
63
  );
55
64
  }
56
65
 
@@ -97,8 +106,12 @@ export function configuredTransport(): ConfiguredTransport {
97
106
  return cached;
98
107
  }
99
108
 
100
- cached = { kind: TMUX_PUSH, source: "default", origin: `built-in default (${TMUX_PUSH})` };
101
- return cached;
109
+ throw new Error(
110
+ `[agent-coord-mcp] no transport configured — set $${TRANSPORT_ENV_VAR} or ${TRANSPORT_CONFIG_FILE} ` +
111
+ `("transport") to one of: ${TRANSPORT_KINDS.join(", ")}. ` +
112
+ `REFUSING rather than picking one: a default that is right for this fleet today is wrong for the ` +
113
+ `next machine that doesn't have it, and the two failures leave identical evidence.`,
114
+ );
102
115
  }
103
116
 
104
117
  /**
@@ -257,6 +257,24 @@ export class HerdrTransport implements Transport {
257
257
  if (!avail.available) throw new Error(`herdr transport cannot attach '${args.agentId}': ${avail.reason}`);
258
258
  let target = args.target ?? this.#env.HERDR_PANE_ID;
259
259
  if (!target) {
260
+ // ⟨q-e439e4ad⟩ `herdr pane current` asks HERDR FOR WHATEVER PANE IS CURRENTLY
261
+ // FOCUSED ON THIS HOST — under stdio that is correct, because the server process
262
+ // itself lives inside the caller's own pane (the same relationship $TMUX_PANE has
263
+ // to a tmux server). AGENT_COORD_HTTP_PORT set means this process is instead the
264
+ // shared bus DAEMON: one long-lived process with no pane of its own, answering
265
+ // every seat on the host. "Current" there names whichever terminal a human last
266
+ // focused — measured attaching two different seats onto ANOTHER FLEET's panes,
267
+ // both calls returning `ok`. Same rule as transports/config.ts's unknown-transport
268
+ // refusal: a silent fallback and a correct default produce identical evidence, so
269
+ // this refuses instead of guessing.
270
+ if (this.#env.AGENT_COORD_HTTP_PORT) {
271
+ throw new Error(
272
+ `herdr target not provided for '${args.agentId}': this server is the shared HTTP daemon ` +
273
+ `(AGENT_COORD_HTTP_PORT set) and has no pane of its own, so \`herdr pane current\` would name ` +
274
+ `whichever pane last had focus on this host, not '${args.agentId}''s. Pass target explicitly ` +
275
+ `(e.g. 'w2:p1').`,
276
+ );
277
+ }
260
278
  const cur = this.#run(["pane", "current"]);
261
279
  target = paneOf(cur)?.pane_id;
262
280
  }
@@ -6,7 +6,7 @@
6
6
  * fall-through at a call site. Until Task 3, tmux is the only registered
7
7
  * implementation and that is the rollback plan — the seam lands behind no config.
8
8
  */
9
- import { HERDR, TMUX_PUSH, TMUX_PUSH_REMOTE, TRANSPORT_KINDS, type Transport, type TransportKind } from "./types.js";
9
+ import { HERDR, TMUX_PUSH_REMOTE, TRANSPORT_KINDS, type Transport, type TransportKind } from "./types.js";
10
10
  import { TmuxTransport, type TmuxHost } from "./tmux.js";
11
11
  import { HerdrTransport } from "./herdr.js";
12
12
  import { configuredTransport } from "./config.js";
@@ -31,7 +31,8 @@ export function resolveTransport(kind: TransportKind): Transport {
31
31
  );
32
32
  }
33
33
  switch (kind) {
34
- case TMUX_PUSH:
34
+ // ⟨q-ec020f6a⟩ local tmux-push deleted outright — TmuxTransport now only ever
35
+ // backs the remote kind. Kept fully intact: only this construction site changed.
35
36
  case TMUX_PUSH_REMOTE:
36
37
  return new TmuxTransport(host, kind);
37
38
  case HERDR:
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { spawnSync } from "node:child_process";
16
16
  import type { ControlCommand, Liveness, Transport, TransportMarker, TransportKind } from "./types.js";
17
- import { TMUX_PUSH, isLocallyProbeable, isTmuxKind, targetOf } from "./types.js";
17
+ import { TMUX_PUSH_REMOTE, isLocallyProbeable, isTmuxKind, targetOf } from "./types.js";
18
18
 
19
19
  /**
20
20
  * The parts of delivery that belong to the PROCESS layer, not to tmux.
@@ -87,7 +87,10 @@ export class TmuxTransport implements Transport {
87
87
  readonly kind: TransportKind;
88
88
  #host: TmuxHost;
89
89
 
90
- constructor(host: TmuxHost, kind: TransportKind = TMUX_PUSH) {
90
+ // ⟨q-ec020f6a⟩ local tmux-push deleted: this class now only ever backs the remote kind,
91
+ // so the default (used only by tests that omit `kind`) follows that — everything else
92
+ // in this file is unchanged, per the slice's "keep TmuxTransport fully intact" scope.
93
+ constructor(host: TmuxHost, kind: TransportKind = TMUX_PUSH_REMOTE) {
91
94
  this.#host = host;
92
95
  this.kind = kind;
93
96
  }
@@ -27,17 +27,32 @@
27
27
  * instead of reading the field, and a `string` discriminant cannot tell the
28
28
  * compiler that a branch was missed. Not a two-member union either — see (1).
29
29
  */
30
+ /**
31
+ * ⟨q-ec020f6a⟩ LOCAL tmux-push was DELETED outright (0 of 13 live seats used it; keeping
32
+ * it meant a second delivery mechanism — hooks/tmux-pusher.mjs, now removed — plus a
33
+ * role-card precondition that halted every live herdr seat on a false requirement).
34
+ *
35
+ * `TMUX_PUSH` stays DEFINED AND EXPORTED, but OUT of `TransportKind`/`TRANSPORT_KINDS`, for
36
+ * exactly one reason: a marker written to disk before this change can still literally read
37
+ * `"tmux-push"`, and `isLocallyProbeable`/`isTmuxKind` below must keep classifying that
38
+ * HISTORICAL value correctly (never `"dead"` where it used to read `"unknown"`) even
39
+ * though nothing can construct a NEW one — `resolveTransport` has no case for it and
40
+ * `config.ts` refuses it. Classifying old data and constructing new data are different
41
+ * questions; removing it from the union answers only the second.
42
+ */
30
43
  export const TMUX_PUSH = "tmux-push" as const;
31
44
  export const TMUX_PUSH_REMOTE = "tmux-push-remote" as const;
32
45
  export const HERDR = "herdr" as const;
33
46
 
34
- export type TransportKind = typeof TMUX_PUSH | typeof TMUX_PUSH_REMOTE | typeof HERDR;
47
+ export type TransportKind = typeof TMUX_PUSH_REMOTE | typeof HERDR;
35
48
 
36
- export const TRANSPORT_KINDS: readonly TransportKind[] = [TMUX_PUSH, TMUX_PUSH_REMOTE, HERDR] as const;
49
+ export const TRANSPORT_KINDS: readonly TransportKind[] = [TMUX_PUSH_REMOTE, HERDR] as const;
37
50
 
38
51
  /**
39
- * The tmux family. Both members are delivered by a pusher process typing into a
40
- * pane; they differ in WHERE that pane is, which is why liveness splits below.
52
+ * The tmux family, for LIVENESS CLASSIFICATION of whatever a marker on disk says —
53
+ * including the historical, no-longer-constructible local kind (`TMUX_PUSH`, above).
54
+ * Both members are delivered by a pusher process typing into a pane; they differed in
55
+ * WHERE that pane was, which is why liveness splits below.
41
56
  *
42
57
  * THIS IS THE ONE PLACE THE TMUX LITERALS LIVE. Twelve call sites used to spell
43
58
  * them; they now ask.