agent-coord-mcp 0.26.39 → 0.26.41

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/src/server.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // @ts-expect-error — untyped .mjs sibling, deliberately not duplicated in TS
3
3
  import { readyProfileStartupLine } from "../hooks/submit.mjs";
4
4
  import { randomUUID } from "node:crypto";
5
+ import { bindingDecision } from "./binding-decision.js";
5
6
  import { HERDR, initTransportFromConfig } from "./transports/index.js";
6
7
  import { startTailOnBind, stopHerdrTail } from "./tools/herdr-tail.js";
7
8
  import { createServer, IncomingMessage, ServerResponse } from "node:http";
@@ -168,6 +169,11 @@ function jsonResult(data: unknown) {
168
169
  // make pid-liveness meaningless.
169
170
  function buildServer(initialBound?: string, opts: { trackSession?: boolean } = {}): McpServer {
170
171
  let bound = initialBound;
172
+ // ⟨q-bf7fd8f3⟩ HOW the binding was acquired, not just what it is. A binding a
173
+ // gated tool TOFU'd on the caller's behalf is PROVISIONAL: the session never
174
+ // said "I am this", it merely named an id while unbound. An ASSERTED binding
175
+ // (env/bearer pre-bind, or an explicit `join`) is sticky and may not be replaced.
176
+ let boundAsserted = initialBound !== undefined;
171
177
  const trackSession = opts.trackSession ?? false;
172
178
  let sessionMarker: string | undefined;
173
179
  let exitHooksInstalled = false;
@@ -344,13 +350,16 @@ function buildServer(initialBound?: string, opts: { trackSession?: boolean } = {
344
350
  if (field) {
345
351
  const claimed = args[field];
346
352
  if (typeof claimed === "string") {
353
+ const decision = bindingDecision({ bound, boundAsserted, claimed, asserting: false, bindOnClaim });
347
354
  if (bound === undefined) {
348
- if (bindOnClaim) {
355
+ if (decision === "bind") {
349
356
  // TOFU: first claim wins, then sticky — but only after the
350
357
  // first-claim guard agrees the id isn't someone else's live
351
358
  // session (see guardFirstClaim).
352
359
  const via = await guardFirstClaim(claimed, args);
353
360
  bound = claimed;
361
+ // provisional: no one asserted this identity, a tool just named it.
362
+ boundAsserted = false;
354
363
  recordSessionBinding(claimed, via);
355
364
  // ⛔ THE TAIL STARTS HERE, AT THE BINDING — not from an env var no seat sets.
356
365
  startTailOnBind(claimed);
@@ -404,16 +413,30 @@ function buildServer(initialBound?: string, opts: { trackSession?: boolean } = {
404
413
  async (args: Record<string, unknown>) => {
405
414
  const claimed = args["agentId"];
406
415
  if (typeof claimed === "string") {
407
- if (bound === undefined) {
416
+ // ⟨q-bf7fd8f3⟩ `join` is the one place a session SAYS who it is, so it is the
417
+ // one place a PROVISIONAL binding may be corrected. Measured 2026-09-28: a
418
+ // resumer's fresh server was TOFU-bound to another seat by `attach_agent`,
419
+ // after which every verb — including read-only `status` — refused it as
420
+ // itself, and the only documented remedy was relaunching the process. A
421
+ // session that can never say "no, I am this" has to be killed to be fixed.
422
+ const jd = bindingDecision({ bound, boundAsserted, claimed, asserting: true, bindOnClaim: true });
423
+ if (jd === "bind" || jd === "rebind") {
408
424
  const via = await guardFirstClaim(claimed, args);
425
+ const rebound = bound !== undefined && bound !== claimed;
409
426
  bound = claimed;
410
- recordSessionBinding(claimed, via);
427
+ boundAsserted = true;
428
+ recordSessionBinding(claimed, rebound ? `${via}+rebind` : via);
411
429
  // ⛔ THE TAIL STARTS HERE, AT THE BINDING — not from an env var no seat sets.
412
430
  startTailOnBind(claimed);
413
431
  } else if (bound !== claimed) {
414
432
  throw new Error(
415
- `identity bound to '${bound}'; rejected attempt to act as '${claimed}'`,
433
+ `identity bound to '${bound}'; rejected attempt to act as '${claimed}'. ` +
434
+ `That binding was ASSERTED (join/token/env), so it is not provisional and ` +
435
+ `cannot be replaced from inside this session.`,
416
436
  );
437
+ } else {
438
+ // re-joining as itself asserts the binding it already holds
439
+ boundAsserted = true;
417
440
  }
418
441
  }
419
442
  return jsonResult(await (joinTool as (a: Record<string, unknown>) => Promise<unknown>)(args));
@@ -611,28 +634,28 @@ function buildServer(initialBound?: string, opts: { trackSession?: boolean } = {
611
634
  "attach_agent",
612
635
  "Start the tmux-push transport for an agent: spawns hooks/tmux-pusher.mjs as a background process so peer DMs (and optionally room messages) get typed into the agent's tmux pane in real time. tmuxTarget defaults to the MCP server's own $TMUX_PANE if this server is running inside tmux. allowlist restricts which peer agentIds can push. Updates list_agents to show transport=tmux-push.",
613
636
  attachAgentSchema,
614
- gate("agentId", attachAgentTool as (a: Record<string, unknown>) => Promise<unknown>),
637
+ gate("agentId", attachAgentTool as (a: Record<string, unknown>) => Promise<unknown>, { bindOnClaim: false }),
615
638
  );
616
639
 
617
640
  addTool(
618
641
  "detach_agent",
619
642
  "Stop the tmux-push transport for an agent: kills the pusher process and clears the transport marker.",
620
643
  detachAgentSchema,
621
- gate("agentId", detachAgentTool as (a: Record<string, unknown>) => Promise<unknown>),
644
+ gate("agentId", detachAgentTool as (a: Record<string, unknown>) => Promise<unknown>, { bindOnClaim: false }),
622
645
  );
623
646
 
624
647
  addTool(
625
648
  "report_transport",
626
649
  "Publish a transport marker for an agent (used by the remote tmux pusher, scripts/coord-pusher.mjs, to surface itself in list_agents). Set transport='tmux-push-remote' and optionally host/tmuxTarget. Liveness for remote markers is heartbeat-based — keep calling heartbeat or this marker gets GC'd after staleness.",
627
650
  reportTransportSchema,
628
- gate("agentId", reportTransportTool as (a: Record<string, unknown>) => Promise<unknown>),
651
+ gate("agentId", reportTransportTool as (a: Record<string, unknown>) => Promise<unknown>, { bindOnClaim: false }),
629
652
  );
630
653
 
631
654
  addTool(
632
655
  "clear_transport",
633
656
  "Idempotent delete of an agent's transport marker. The wire-callable counterpart to detach_agent for remote pushers: it only removes the marker — there's no local process to kill.",
634
657
  clearTransportSchema,
635
- gate("agentId", clearTransportTool as (a: Record<string, unknown>) => Promise<unknown>),
658
+ gate("agentId", clearTransportTool as (a: Record<string, unknown>) => Promise<unknown>, { bindOnClaim: false }),
636
659
  );
637
660
 
638
661
  addTool(
package/src/tools/away.ts CHANGED
@@ -112,8 +112,26 @@ export function coverageAgeOf(coverage: { at: string }, now = Date.now()): { age
112
112
  const stale = ageMs > COVERAGE_STALE_MINUTES * 60_000;
113
113
  return { ageMs, stale, label: `measured ${fmtAge(ageMs)} ago${stale ? ` — STALE (older than ${COVERAGE_STALE_MINUTES}m; re-arm or run stall_check for a current reading)` : ""}` };
114
114
  }
115
- /** Met when nothing scored is unseen — an empty board (0 of 0) is idle, not blind, and needs no special case. */
116
- export const meetsCoverageThreshold = (c: { checked: number; measurable: number }): boolean => c.measurable >= c.checked;
115
+ /*
116
+ * ⛔⛆ ⟨q-b0d4e45a⟩ THE DENOMINATOR IS THE IN-FLIGHT POPULATION, NOT THE SCORED SUBSET. This armed a
117
+ * fleet on `checked: 0, measurable: 0, blind: []` while its own population said `rows: 4, inFlight: 2`:
118
+ * both in-flight lanes carried a PROSE cell, so each one hit `deliberate` and `continue`d out of the
119
+ * scoring loop before `scored.push`, and `checked` counted only what survived that filter. `0 >= 0` armed.
120
+ *
121
+ * ⛔ SO THE GUARD INVERTED AT ITS OWN LIMIT: `2 of 5` REFUSED while `0 of 2` ARMED SILENTLY — the
122
+ * partially blind fleet was stopped and the totally blind one waved through. An EMPTY blind-list and a
123
+ * blind-list THAT WAS NEVER POPULATED are indistinguishable, and this read the second as the first.
124
+ *
125
+ * ⭐ `deliberate` STAYS a category — a deliberately unwatched lane is real. The bug was that it was
126
+ * subtracted from the POPULATION rather than reported within it: a classification is a REASON a lane is
127
+ * unscored, never an exemption from being counted.
128
+ *
129
+ * ⚠ `inFlight === 0` is the ONE case that still arms, and it is a different sentence: nothing is in
130
+ * flight, so there is nothing to see — idle, not blind. That distinction is why the old `checked > 0`
131
+ * reasoning existed; it was right about idle and wrong about what `checked` measured.
132
+ */
133
+ export const meetsCoverageThreshold = (c: { measurable: number; inFlight: number }): boolean =>
134
+ c.inFlight === 0 ? true : c.measurable >= c.inFlight;
117
135
 
118
136
  /**
119
137
  * ⟨q-1c95f7d4⟩ Task 5.4 — WHAT THE EXTERNAL TICK CONTRIBUTES TO THE AWAY PRECONDITION.
@@ -139,8 +157,19 @@ export type AwayCoverage = {
139
157
  tick?: AwayTick;
140
158
  /** ⟨q-5d1c8e04⟩ — standing seats on the board: present, not scored, and therefore not blind. */
141
159
  roles?: number;
142
- /** Lanes that declare no per-agent branch: out of the population by their own statement. */
160
+ /**
161
+ * ⛔ ⟨q-b0d4e45a⟩ THE IN-FLIGHT POPULATION — the denominator. `checked` is the SCORED SUBSET and is
162
+ * reported beside it, never as the divisor: a lane filtered out before scoring still has to be counted,
163
+ * or the reading congratulates itself for the rows it dropped.
164
+ */
165
+ inFlight: number;
166
+ /**
167
+ * Lanes in flight that declare no per-agent branch. COUNTED IN `inFlight`, unscored, and named with the
168
+ * offending cell — a reason, not an exemption ⟨q-b0d4e45a⟩.
169
+ */
143
170
  deliberate?: number;
171
+ /** The `deliberate` lanes themselves, so a refusal can name the ROW and the CELL rather than a count. */
172
+ deliberateRows?: { agentId: string; stream: string; cell: string }[];
144
173
  /** ⟨q-3d82f1a9⟩ — the board could not be read: rows present vs parsed, as stall_check stated it. */
145
174
  unreadable?: { rowsPresent: number; rowsParsed: number; why: string };
146
175
  at: string;
@@ -245,11 +274,20 @@ export const coordAwaySchema = {
245
274
  agentId: z.string().optional(),
246
275
  };
247
276
 
248
- async function measureCoverage(repo?: string): Promise<AwayCoverage | null> {
277
+ /**
278
+ * ⛔ THE SEAM EXISTS TO FEED THE GUARD BELOW, NOT TO BYPASS IT ⟨q-b0d4e45a⟩. `checkStall` replaces only
279
+ * the CLOCK'S REPLY; every line of the arithmetic and every refusal still runs on it. A seam that stood
280
+ * in for `measureCoverage` itself could not test the missing-population guard, because the guard is the
281
+ * thing under test — so the injection point is deliberately UPSTREAM of it.
282
+ */
283
+ export type StallReplyForCoverage = { ok?: boolean; checked?: number; measurable?: number; blind?: string[]; roles?: unknown[]; deliberate?: unknown[]; population?: { rows?: number; inFlight?: number }; tick?: unknown; boardParse?: unknown };
284
+ async function measureCoverage(repo?: string, checkStall: (a: { repo: string }) => Promise<unknown> = stallCheckTool): Promise<AwayCoverage | null> {
249
285
  if (!repo) return null;
250
286
  try {
251
- const r = (await stallCheckTool({ repo })) as unknown as {
252
- ok?: boolean; checked?: number; measurable?: number; blind?: string[]; roles?: unknown[]; deliberate?: unknown[];
287
+ const r = (await checkStall({ repo })) as unknown as {
288
+ ok?: boolean; checked?: number; measurable?: number; blind?: string[]; roles?: unknown[];
289
+ deliberate?: { agentId?: string; stream?: string; cell?: string }[];
290
+ population?: { rows?: number; inFlight?: number };
253
291
  tick?: { seats?: unknown[]; readable?: number; states?: Record<string, number>; note?: string };
254
292
  boardParse?: { readable: boolean; rowsPresent: number; rowsParsed: number; why: string };
255
293
  };
@@ -258,21 +296,42 @@ async function measureCoverage(repo?: string): Promise<AwayCoverage | null> {
258
296
  // promise a watched fleet over a table nobody can read.
259
297
  if (r?.boardParse && !r.boardParse.readable) {
260
298
  return {
261
- checked: 0, measurable: 0, blind: [], roles: 0, deliberate: 0,
299
+ checked: 0, measurable: 0, inFlight: 0, blind: [], roles: 0, deliberate: 0,
262
300
  unreadable: { rowsPresent: r.boardParse.rowsPresent, rowsParsed: r.boardParse.rowsParsed, why: r.boardParse.why },
263
301
  at: new Date().toISOString(),
264
302
  };
265
303
  }
266
304
  if (!r?.ok) return null;
305
+ // ⛔⛆ ⟨q-b0d4e45a⟩ A MISSING POPULATION IS NOT A POPULATION OF ZERO, and `?? 0` here would rebuild the
306
+ // defect one layer up: `inFlight: 0` is the one value that passes the threshold unconditionally. If the
307
+ // clock cannot state its own denominator the fleet is UNMEASURED, and `null` sends it to the
308
+ // "cannot measure" refusal.
309
+ // ⭐ MEASURED, by removing this line in the COMPILED dist (tsc refuses the mutation, so the matrix runs
310
+ // against dist — qa's technique) — and it is worth stating exactly, because the danger is NOT uniform:
311
+ // • plain call → refuses, but INCOHERENTLY: "covers 0 of undefined … (NaN short)"
312
+ // • acknowledgeBlindFleet → ⛔ ARMS, recording `coverage.inFlight: undefined` and announcing
313
+ // "Stall coverage 0/undefined — ACKNOWLEDGED BLIND"
314
+ // ⚠ So the acknowledged path is the one that actually goes away on an unmeasured fleet, and an earlier
315
+ // draft of this comment said "would ARM" without that split. Both outcomes are wrong; only one ships a
316
+ // fleet unwatched. The test covers BOTH, including that the flag cannot buy past it.
317
+ const inFlight = r.population?.inFlight;
318
+ if (typeof inFlight !== "number") return null;
319
+ const deliberateRows = (r.deliberate ?? []).map((d) => ({
320
+ agentId: String(d?.agentId ?? "-"),
321
+ stream: String(d?.stream ?? "-"),
322
+ cell: String(d?.cell ?? "").trim(),
323
+ }));
267
324
  return {
268
325
  checked: r.checked ?? 0,
269
326
  measurable: r.measurable ?? 0,
327
+ inFlight,
270
328
  blind: r.blind ?? [],
271
329
  tick: r.tick
272
330
  ? { seats: r.tick.seats?.length ?? 0, readable: r.tick.readable ?? 0, states: (r.tick.states ?? {}) as Record<string, number>, note: r.tick.note ?? "" }
273
331
  : undefined,
274
332
  roles: r.roles?.length ?? 0,
275
- deliberate: r.deliberate?.length ?? 0,
333
+ deliberate: deliberateRows.length,
334
+ deliberateRows,
276
335
  at: new Date().toISOString(),
277
336
  };
278
337
  } catch {
@@ -280,16 +339,23 @@ async function measureCoverage(repo?: string): Promise<AwayCoverage | null> {
280
339
  }
281
340
  }
282
341
 
283
- export async function coordAwayTool(args: {
284
- project: string;
285
- on?: boolean;
286
- leadId?: string;
287
- decisionLog?: string;
288
- until?: string;
289
- repo?: string;
290
- acknowledgeBlindFleet?: boolean;
291
- agentId?: string;
292
- }) {
342
+ export async function coordAwayTool(
343
+ args: {
344
+ project: string;
345
+ on?: boolean;
346
+ leadId?: string;
347
+ decisionLog?: string;
348
+ until?: string;
349
+ repo?: string;
350
+ acknowledgeBlindFleet?: boolean;
351
+ agentId?: string;
352
+ },
353
+ /**
354
+ * ⚠ A SECOND PARAMETER RATHER THAN A FIELD ON `args`, so nothing here can reach the MCP tool schema:
355
+ * a caller on the bus cannot supply its own clock. Tests only.
356
+ */
357
+ deps: { checkStall?: (a: { repo: string }) => Promise<unknown> } = {},
358
+ ) {
293
359
  const state = readAway();
294
360
  const prior = state[args.project];
295
361
 
@@ -320,7 +386,10 @@ export async function coordAwayTool(args: {
320
386
  summary:
321
387
  `'${args.project}' is ARMED — '${prior.leadId}' leads since ${prior.at}` +
322
388
  `${prior.until ? ` until ${prior.until}` : ""}. ` +
323
- `Coverage ${prior.coverage.measurable}/${prior.coverage.checked} lane(s) ${coverageAgeOf(prior.coverage).label}` +
389
+ // ⚠ A RECORD WRITTEN BEFORE ⟨q-b0d4e45a⟩ HAS NO POPULATION, and a query must not invent one.
390
+ // Printing `/${checked}` for it would quote the scored subset as the denominator — the very
391
+ // reading this row fixed — so an unrecorded population says so in words.
392
+ `Coverage ${prior.coverage.measurable}/${typeof prior.coverage.inFlight === "number" ? prior.coverage.inFlight : `?? (population not recorded at arm time; ${prior.coverage.checked} scored)`} lane(s) ${coverageAgeOf(prior.coverage).label}` +
324
393
  `${prior.coverage.roles ? ` · ${prior.coverage.roles} role(s) present, not scored` : ""}` +
325
394
  `${prior.blindAcknowledged ? " (ACKNOWLEDGED BLIND)" : ""}. ` +
326
395
  `Decisions logged to ${prior.decisionLog}.`,
@@ -347,7 +416,7 @@ export async function coordAwayTool(args: {
347
416
  }
348
417
 
349
418
  // THE PRECONDITION: what could the clock actually SEE?
350
- const coverage = await measureCoverage(args.repo);
419
+ const coverage = await measureCoverage(args.repo, deps.checkStall);
351
420
  if (!coverage) {
352
421
  return {
353
422
  ok: false as const,
@@ -370,18 +439,29 @@ export async function coordAwayTool(args: {
370
439
  `Away mode promises a watched fleet; a board the clock cannot read is not a quiet fleet, it is an unwatched one. Fix the row and re-arm.`,
371
440
  };
372
441
  }
373
- // `checked > 0` MATTERS: an empty board is an IDLE fleet, not a blind one.
374
- // Without it, arming while nothing is in flight refuses with "the clock
375
- // covers 0 of 0" — a refusal that reads as a fleet nobody can see when in
376
- // fact there is nothing to see, and the only way past it would be to
377
- // acknowledge a blindness that does not exist.
378
442
  // ⟨q-5a71fc3b⟩ — below the threshold, not only at zero: "2 of 5" refuses.
443
+ // ⛔⛆ ⟨q-b0d4e45a⟩ — AND "0 of 2" REFUSES THE SAME WAY. The denominator is the in-flight
444
+ // population, so a lane filtered out of `checked` before scoring cannot buy silence. An empty
445
+ // board still arms, because `inFlight === 0` is idle rather than blind — the one thing the old
446
+ // `checked > 0` reasoning got right, now expressed against the population instead of against
447
+ // the scored subset it was mistaken for.
379
448
  if (!meetsCoverageThreshold(coverage) && !args.acknowledgeBlindFleet) {
449
+ // ⭐ NAME THE ROW AND THE CELL, not a count. "1 deliberate" tells a reader nothing they can act
450
+ // on; the stream and the offending cell text tell them which line to fix — or that the lane is
451
+ // legitimately unscoreable because nothing has been pushed yet.
452
+ const unscored = coverage.deliberateRows ?? [];
453
+ const unscoredSentence = unscored.length
454
+ ? ` UNSCORED BUT COUNTED — ${unscored.length} in-flight lane(s) declare no branch ref, and each is counted in the ${coverage.inFlight}: ` +
455
+ unscored.map((d) => `${d.agentId} (${d.stream}) cell reads ${JSON.stringify(d.cell)}`).join(" · ") +
456
+ `. A cell like this is a REASON a lane is unscored, never an exemption from being counted: if the lane has genuinely pushed nothing there is no ref to write, and that is UNSCOREABLE-AND-COUNTED, not covered.`
457
+ : "";
380
458
  return {
381
459
  ok: false as const,
382
460
  error:
383
- `REFUSING: the stall clock covers ${coverage.measurable} of ${coverage.checked} in-flight row(s); the threshold is that ${COVERAGE_THRESHOLD} ` +
384
- `(${coverage.checked - coverage.measurable} short)${coverage.blind.length ? ` — blind on: ${coverage.blind.join(", ")}` : ""}. ` +
461
+ `REFUSING: the stall clock covers ${coverage.measurable} of ${coverage.inFlight} in-flight row(s) ` +
462
+ `(${coverage.checked} scored); the threshold is that ${COVERAGE_THRESHOLD} ` +
463
+ `(${coverage.inFlight - coverage.measurable} short)${coverage.blind.length ? ` — blind on: ${coverage.blind.join(", ")}` : ""}.` +
464
+ unscoredSentence + " " +
385
465
  `${coverage.measurable === 0 ? "It would RUN and see nothing. " : "The seats it cannot see are exactly the ones an absence leaves unattended. "}` +
386
466
  `"The clock ran" and "the fleet is observed" are different facts, and this verb must not treat the first as the second. ` +
387
467
  `The usual cause is a board 'Branch · Worktree' cell holding a PATH rather than a branch ref: a path resolves for ` +
@@ -417,7 +497,12 @@ export async function coordAwayTool(args: {
417
497
  `Decides: planning · priority · curation · roadmap · canon · releases under standing authorisation. ` +
418
498
  `Never: merges, gates, or takes a code lane. Parks for David: ${PARKED_CATEGORIES.join(" · ")}. ` +
419
499
  `Decisions logged to ${args.decisionLog}. ` +
420
- `Stall coverage ${coverage.measurable}/${coverage.checked}${!meetsCoverageThreshold(coverage) ? ` — ACKNOWLEDGED BLIND${coverage.measurable === 0 ? ": the clock runs and sees nothing" : ""}${coverage.blind.length ? ` on: ${coverage.blind.join(", ")}` : ""}` : ""}.` +
500
+ // ⭐ ⟨q-b0d4e45a⟩ TWO DIFFERENT SENTENCES, AND ONLY ONE OF THEM IS REASSURING. An idle fleet says
501
+ // "0 in flight"; it must never render as "0/0 covered", which reads as a measured fleet and is
502
+ // exactly how a never-populated reading passed for a clean one.
503
+ (coverage.inFlight === 0
504
+ ? `Stall coverage: 0 in flight — nothing to watch, so nothing was measured (idle, not blind; ${coverage.checked} scored).`
505
+ : `Stall coverage ${coverage.measurable}/${coverage.inFlight}${!meetsCoverageThreshold(coverage) ? ` — ACKNOWLEDGED BLIND${coverage.measurable === 0 ? ": the clock runs and sees nothing" : ""}${coverage.blind.length ? ` on: ${coverage.blind.join(", ")}` : ""}` : ""}.`) +
421
506
  tickSentence(coverage.tick),
422
507
  };
423
508
  }
@@ -292,7 +292,9 @@ export const heartbeatSchema = { agentId: z.string().min(1) };
292
292
  * ⟨q-18a719c5⟩ THE ANSWERING SERVER'S IDENTITY — what `serverSpread` places a seat by. It exists
293
293
  * only inside the process that serves the seat, so every path on which a session binds an
294
294
  * identity stamps it: `register` (which `join` calls), the first-claim binding in server.ts
295
- * (a server that restarts and re-binds through any gated tool, `attach_agent` included),
295
+ * (a server that restarts and re-binds through a gated tool — ⟨q-bf7fd8f3⟩: NO LONGER
296
+ * `attach_agent`, `detach_agent`, `report_transport` or `clear_transport`, which pass
297
+ * `bindOnClaim: false` because administering a seat is not a claim to BE it),
296
298
  * `rename_agent` (the renaming session serves the new id), and `heartbeat`. Before this,
297
299
  * only `heartbeat` stamped, no code path called it, and `register` rebuilt the entry and
298
300
  * dropped whatever it had written.
@@ -655,6 +657,22 @@ export async function liveClaimEvidence(agentId: string, now: number): Promise<C
655
657
  reasons.push(
656
658
  `live ${marker.transport} transport (pid ${marker.pid}${targetOf(marker) ? `, pane ${targetOf(marker)}` : ""})`,
657
659
  );
660
+ }
661
+ // ⛔⛆ ⟨q-f995c3c7⟩ THE OWN-PANE DETERMINATION IS NOT GATED ON THE MARKER READING LIVE, AND THE NESTING
662
+ // THAT GATED IT WAS THE DEFECT. Everything below used to sit inside the `isMarkerLive` branch above, so a
663
+ // marker herdr no longer recognises — `paneExists` false, which `isMarkerLive` correctly calls dead —
664
+ // skipped the escape entirely. `live` then stayed true from the REGISTRY HEARTBEAT ALONE and the seat was
665
+ // refused with no way out but a token.
666
+ // ⚠ That is this function's own documented case (1): "own pane + a marker NO PROCESS HOLDS -> ACCEPTED".
667
+ // A DEAD MARKER IS A MARKER NO PROCESS HOLDS, so the gate contradicted the requirement it implements.
668
+ // ⭐ REAL TRIGGER, not hypothetical: any seat whose pane id herdr stops recognising — a recreated pane, a
669
+ // renamed workspace, a herdr restart that renumbers. 4 of 14 seats moved panes across the 2026-09-28
670
+ // standdown and nobody hit this only because herdr keeps an old id RESOLVING after a move; that is luck,
671
+ // and a renumbering restart removes it.
672
+ // ⛔ AND IT WIDENS NOTHING: `samePane` still requires the marker to name THIS process's pane, and the
673
+ // permit at `server.ts` pairs it with `boundElsewhere === 0`, so a genuinely live SESSION still refuses.
674
+ // Being in one's own pane has never been the whole permit and is not now.
675
+ if (marker) {
658
676
  // ⟨q-f995c3c7⟩ THE SAME SEAT RESTARTING INTO ITS OWN PANE, for herdr as well as tmux. This read
659
677
  // `process.env.TMUX_PANE` alone, which a herdr seat never has, so the exception could not fire
660
678
  // for the fleet that runs on herdr: every seat was refused by its own leftover marker (6 of 6 on