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/dist/binding-decision.js +16 -0
- package/dist/binding-decision.js.map +1 -0
- package/dist/capabilities.js +89 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/server.js +32 -8
- package/dist/server.js.map +1 -1
- package/dist/tools/away.js +80 -17
- package/dist/tools/away.js.map +1 -1
- package/dist/tools/registry.js +19 -1
- package/dist/tools/registry.js.map +1 -1
- package/package.json +1 -1
- package/src/binding-decision.ts +33 -0
- package/src/capabilities.ts +90 -1
- package/src/server.ts +31 -8
- package/src/tools/away.ts +113 -28
- package/src/tools/registry.ts +19 -1
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
252
|
-
ok?: boolean; checked?: number; measurable?: number; blind?: string[]; roles?: 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:
|
|
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(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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.
|
|
384
|
-
`(${coverage.checked
|
|
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
|
-
|
|
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
|
}
|
package/src/tools/registry.ts
CHANGED
|
@@ -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
|
|
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
|