agent-coord-mcp 0.26.14 → 0.26.16
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/capabilities.js +158 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/server-identity.js +82 -0
- package/dist/server-identity.js.map +1 -0
- package/dist/server.js +61 -4
- package/dist/server.js.map +1 -1
- package/dist/tools/board-ref.js +164 -0
- package/dist/tools/board-ref.js.map +1 -0
- package/dist/tools/events.js +64 -7
- package/dist/tools/events.js.map +1 -1
- package/dist/tools/record-events.js +9 -2
- package/dist/tools/record-events.js.map +1 -1
- package/dist/tools/records.js +15 -1
- package/dist/tools/records.js.map +1 -1
- package/dist/tools/registry.js +3 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/stall.js +25 -24
- package/dist/tools/stall.js.map +1 -1
- package/dist/tools/transport.js +26 -56
- package/dist/tools/transport.js.map +1 -1
- package/hooks/peek-coord.mjs +0 -0
- package/hooks/tmux-pusher.mjs +0 -0
- package/package.json +14 -11
- package/scripts/check-test-count.mjs +152 -91
- package/scripts/coord-attention-clock.mjs +0 -0
- package/scripts/coord-node.sh +0 -0
- package/scripts/coord-stall-clock.mjs +0 -0
- package/scripts/coord-token.mjs +0 -0
- package/scripts/probe-tmux-liveness.sh +0 -0
- package/scripts/spawn-agent.sh +0 -0
- package/scripts/stop-agent.sh +0 -0
- package/scripts/typed-record-stats.mjs +0 -0
- package/src/capabilities.ts +188 -0
- package/src/server-identity.ts +83 -0
- package/src/server.ts +70 -2
- package/src/tools/board-ref.ts +194 -0
- package/src/tools/events.ts +89 -9
- package/src/tools/record-events.ts +9 -2
- package/src/tools/records.ts +15 -1
- package/src/tools/registry.ts +17 -0
- package/src/tools/stall.ts +38 -24
- package/src/tools/transport.ts +26 -57
package/src/tools/events.ts
CHANGED
|
@@ -29,8 +29,26 @@ export type Subscription = {
|
|
|
29
29
|
kind: SubKind;
|
|
30
30
|
target: string;
|
|
31
31
|
createdAt: number;
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* null until an event OF THIS KIND was evaluated against this subscription.
|
|
34
|
+
*
|
|
35
|
+
* NOT "the machinery ran" — see lastScannedAt. Those were one field, and the
|
|
36
|
+
* collapse is the defect: `evaluate()` only touches subscriptions whose kind
|
|
37
|
+
* matches the event, so an `item` subscriber sat at `never evaluated` for a
|
|
38
|
+
* week while `pr` events flowed past it. Its health said the wiring was dead;
|
|
39
|
+
* the wiring was fine and nothing of its kind had happened.
|
|
40
|
+
*/
|
|
33
41
|
lastEvaluatedAt: number | null;
|
|
42
|
+
/**
|
|
43
|
+
* null until a scan RAN with this subscription registered — regardless of
|
|
44
|
+
* kind. A scan that ran is evidence the wiring works; it says nothing about
|
|
45
|
+
* whether your kind fired, which is the other field.
|
|
46
|
+
*
|
|
47
|
+
* Optional on disk: subscriptions written before this existed have no value,
|
|
48
|
+
* and absent must read as UNKNOWN rather than as "never scanned". Fabricating
|
|
49
|
+
* a scan nobody observed is the failure this whole phase is about.
|
|
50
|
+
*/
|
|
51
|
+
lastScannedAt?: number | null;
|
|
34
52
|
lastEventAt: number | null;
|
|
35
53
|
/** Idempotency keys already delivered, for 6.4. */
|
|
36
54
|
delivered: string[];
|
|
@@ -60,17 +78,53 @@ function writeSubs(subs: Subscription[]): void {
|
|
|
60
78
|
* subscription with no last-evaluated mark has produced no evidence of
|
|
61
79
|
* anything, and "no events" is the same output a broken subscription gives.
|
|
62
80
|
*/
|
|
63
|
-
export function subscriptionHealth(s: Subscription): { level: "ok" | "error"; detail: string } {
|
|
64
|
-
|
|
81
|
+
export function subscriptionHealth(s: Subscription): { level: "ok" | "error" | "unknown"; detail: string } {
|
|
82
|
+
const iso = (n: number) => new Date(n).toISOString();
|
|
83
|
+
|
|
84
|
+
// THREE STATES, BECAUSE THERE ARE THREE FACTS. They were two, and the
|
|
85
|
+
// collapse cost a week: an `item` subscriber read `error — never evaluated`
|
|
86
|
+
// the whole time, because `evaluate()` only touches subscriptions whose kind
|
|
87
|
+
// matches the event and no `item` event had fired. The wiring was fine. The
|
|
88
|
+
// health field said it was dead, and the agent holding it believed the field.
|
|
89
|
+
//
|
|
90
|
+
// Same shape as `alive` swallowing `heartbeatFresh`, and as
|
|
91
|
+
// `stall_clock_status` reporting green while covering nothing: one field
|
|
92
|
+
// answering two questions always answers the easier one.
|
|
93
|
+
if (s.lastScannedAt === undefined && s.lastEvaluatedAt === null) {
|
|
94
|
+
// Written before this field existed AND never evaluated. Absent is UNKNOWN,
|
|
95
|
+
// never "never scanned" — inventing a scan nobody observed is the failure
|
|
96
|
+
// this phase is about, and asserting one never happened is the same error
|
|
97
|
+
// pointed the other way.
|
|
98
|
+
return {
|
|
99
|
+
level: "unknown",
|
|
100
|
+
detail:
|
|
101
|
+
"predates scan tracking and has never been evaluated — whether the machinery has run for it CANNOT be determined from this record. " +
|
|
102
|
+
"It will resolve to ok or error on the next scan; until then this is an absence of evidence, not evidence of absence.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (!s.lastScannedAt) {
|
|
65
106
|
return {
|
|
66
107
|
level: "error",
|
|
67
|
-
detail:
|
|
108
|
+
detail:
|
|
109
|
+
"NEVER SCANNED — no scan has run with this subscription registered, so it has produced no evidence of being wired to anything. " +
|
|
110
|
+
"This is the state that means broken.",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (s.lastEvaluatedAt === null) {
|
|
114
|
+
// THE STATE THAT USED TO READ AS BROKEN. The machinery ran and nothing of
|
|
115
|
+
// this kind happened, which is a healthy idle watch.
|
|
116
|
+
return {
|
|
117
|
+
level: "ok",
|
|
118
|
+
detail:
|
|
119
|
+
`scanned ${iso(s.lastScannedAt)}, never evaluated — the machinery RAN and no '${s.kind}' event has occurred yet. ` +
|
|
120
|
+
"Scanned-and-quiet is healthy; it is not the same as never run, and those shared a field until now.",
|
|
68
121
|
};
|
|
122
|
+
}
|
|
69
123
|
return {
|
|
70
124
|
level: "ok",
|
|
71
125
|
detail: s.lastEventAt
|
|
72
|
-
? `last evaluated ${
|
|
73
|
-
: `last evaluated ${
|
|
126
|
+
? `scanned ${iso(s.lastScannedAt)}, last evaluated ${iso(s.lastEvaluatedAt)}, last event ${iso(s.lastEventAt)}`
|
|
127
|
+
: `scanned ${iso(s.lastScannedAt)}, last evaluated ${iso(s.lastEvaluatedAt)}, no events yet`,
|
|
74
128
|
};
|
|
75
129
|
}
|
|
76
130
|
|
|
@@ -122,12 +176,26 @@ export type Delivery = { subscriptionId: string; agentId: string; key: string; s
|
|
|
122
176
|
* A subscription only learns it is alive by being evaluated, so the mark is
|
|
123
177
|
* written for every subscription of that kind, not only the ones that fired.
|
|
124
178
|
*/
|
|
179
|
+
/**
|
|
180
|
+
* Record that a scan RAN, for every live subscription regardless of kind.
|
|
181
|
+
*
|
|
182
|
+
* Called once per scan — including a scan that produced NO events, which is
|
|
183
|
+
* exactly the case that starved the old field: no events of your kind means
|
|
184
|
+
* `evaluate` never touches you, and a subscription that is never touched cannot
|
|
185
|
+
* be told from one that is not wired to anything.
|
|
186
|
+
*/
|
|
187
|
+
export function markScanned(subs: Subscription[], now: number): Subscription[] {
|
|
188
|
+
return subs.map((s) => ({ ...s, lastScannedAt: now }));
|
|
189
|
+
}
|
|
190
|
+
|
|
125
191
|
export function evaluate(subs: Subscription[], ev: RecordEvent, now: number): { subs: Subscription[]; deliveries: Delivery[] } {
|
|
126
192
|
const key = eventKey(ev.kind, ev.target, ev.ref);
|
|
127
193
|
const deliveries: Delivery[] = [];
|
|
128
194
|
const next = subs.map((s) => {
|
|
129
|
-
|
|
130
|
-
const
|
|
195
|
+
// Every subscription observed this scan, whatever its kind.
|
|
196
|
+
const scanned = { ...s, lastScannedAt: now };
|
|
197
|
+
if (s.kind !== ev.kind) return scanned;
|
|
198
|
+
const evaluated = { ...scanned, lastEvaluatedAt: now };
|
|
131
199
|
if (s.target !== ev.target) return evaluated;
|
|
132
200
|
if (s.delivered.includes(key)) {
|
|
133
201
|
deliveries.push({ subscriptionId: s.id, agentId: s.agentId, key, status: "duplicate-suppressed" });
|
|
@@ -157,6 +225,12 @@ export async function subscribeTool(args: { agentId: string; kind: SubKind; targ
|
|
|
157
225
|
kind: args.kind,
|
|
158
226
|
target: args.target,
|
|
159
227
|
createdAt: Date.now(),
|
|
228
|
+
// EXPLICIT null, never left absent. `null` means "we know no scan has run
|
|
229
|
+
// for this"; `undefined` means "this record predates the field and we
|
|
230
|
+
// cannot say". A subscription created now is the first case, and writing it
|
|
231
|
+
// explicitly is what keeps a brand-new registration from being reported as
|
|
232
|
+
// unknowable.
|
|
233
|
+
lastScannedAt: null,
|
|
160
234
|
lastEvaluatedAt: null,
|
|
161
235
|
lastEventAt: null,
|
|
162
236
|
delivered: [],
|
|
@@ -185,6 +259,7 @@ export async function listSubscriptionsTool(args: { agentId?: string }) {
|
|
|
185
259
|
const subs = args.agentId ? all.filter((s) => s.agentId === args.agentId) : all;
|
|
186
260
|
const rows = subs.map((s) => ({ ...s, health: subscriptionHealth(s) }));
|
|
187
261
|
const neverEvaluated = rows.filter((r) => r.health.level === "error");
|
|
262
|
+
const undetermined = rows.filter((r) => r.health.level === "unknown");
|
|
188
263
|
return {
|
|
189
264
|
ok: neverEvaluated.length === 0,
|
|
190
265
|
// Population beside the verdict, always: "no subscriptions" and "none
|
|
@@ -192,7 +267,12 @@ export async function listSubscriptionsTool(args: { agentId?: string }) {
|
|
|
192
267
|
population: { listed: rows.length, total: all.length },
|
|
193
268
|
subscriptions: rows,
|
|
194
269
|
...(neverEvaluated.length
|
|
195
|
-
? { error: `${neverEvaluated.length} of ${rows.length} subscription(s) have NEVER
|
|
270
|
+
? { error: `${neverEvaluated.length} of ${rows.length} subscription(s) have NEVER BEEN SCANNED — no scan has run with them registered, so they have produced no evidence of being wired to anything.` }
|
|
271
|
+
: {}),
|
|
272
|
+
...(undetermined.length
|
|
273
|
+
? {
|
|
274
|
+
undetermined: `${undetermined.length} of ${rows.length} subscription(s) predate scan tracking and cannot be judged yet — they resolve on the next scan. Reported rather than counted as either healthy or broken.`,
|
|
275
|
+
}
|
|
196
276
|
: {}),
|
|
197
277
|
};
|
|
198
278
|
}
|
|
@@ -257,7 +257,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
257
257
|
import path from "node:path";
|
|
258
258
|
import { z } from "zod";
|
|
259
259
|
import { ROOT } from "../store.js";
|
|
260
|
-
import { readSubs, evaluate, commitEvaluation, eventIsDerived } from "./events.js";
|
|
260
|
+
import { readSubs, evaluate, commitEvaluation, eventIsDerived, markScanned } from "./events.js";
|
|
261
261
|
|
|
262
262
|
/**
|
|
263
263
|
* The watermark: the last commit whose record change has been turned into
|
|
@@ -349,8 +349,15 @@ export async function scanRecordEventsTool(args: { repo: string; since?: string;
|
|
|
349
349
|
|
|
350
350
|
const deliveries: unknown[] = [];
|
|
351
351
|
if (args.write) {
|
|
352
|
-
|
|
352
|
+
// MARK THE SCAN BEFORE THE EVENTS, and unconditionally.
|
|
353
|
+
//
|
|
354
|
+
// A scan that produced NO events is exactly the case that starved the old
|
|
355
|
+
// health field: `evaluate` is per-event, so a quiet scan touched nothing
|
|
356
|
+
// and every subscription kept reading "never evaluated" — indistinguishable
|
|
357
|
+
// from unwired. The scan RAN; that is a fact about the machinery and it is
|
|
358
|
+
// recorded whether or not anything fired.
|
|
353
359
|
const now = Date.now();
|
|
360
|
+
let subs = markScanned(readSubs(), now);
|
|
354
361
|
for (const ev of emitted) {
|
|
355
362
|
const r = evaluate(subs, ev, now);
|
|
356
363
|
subs = r.subs;
|
package/src/tools/records.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
sweepTagOf,
|
|
28
28
|
} from "@davidbalzan/groundwork-seam";
|
|
29
29
|
import { ensureWorktreeTool } from "./worktrees.js";
|
|
30
|
+
import { boardRefFor } from "./board-ref.js";
|
|
30
31
|
import { haltState } from "./stall.js";
|
|
31
32
|
import { readSubs, evaluate, commitEvaluation, eventIsDerived, type RecordEvent } from "./events.js";
|
|
32
33
|
|
|
@@ -340,7 +341,20 @@ export async function claimTool(args: { project: string; agentId: string; itemId
|
|
|
340
341
|
};
|
|
341
342
|
}
|
|
342
343
|
|
|
343
|
-
|
|
344
|
+
// THE CELL NAMES THE REMOTE-TRACKING REF, not the bare local branch.
|
|
345
|
+
//
|
|
346
|
+
// A bare name resolves in the checkout that created it and nowhere else, so
|
|
347
|
+
// the same board read differently from two checkouts and whether a row was
|
|
348
|
+
// measurable depended on who last ran `git fetch` — a property of the reader
|
|
349
|
+
// rather than of the work. Writing `origin/<branch>` makes the cell mean the
|
|
350
|
+
// same thing everywhere.
|
|
351
|
+
//
|
|
352
|
+
// It does not resolve YET, because a freshly cut branch is unpushed. That is
|
|
353
|
+
// correct and `stall_check` says so in those words: an unpushed lane has no
|
|
354
|
+
// shared evidence of activity, which is an absence of evidence rather than a
|
|
355
|
+
// stall. Enforced at the WRITE as well as the read (board-ref.ts) — a rule the
|
|
356
|
+
// writer can defeat is a rule the writer defeats.
|
|
357
|
+
const boardHunk = `| ${keyOf(item)} | ${args.agentId} | \`${boardRefFor(wt.branch ?? "")}\` · ${wt.path} | 🚧 In Progress | — | claimed |`;
|
|
344
358
|
|
|
345
359
|
// 2.4b — THE VERB WRITES THE ROW. Reported by default, applied with
|
|
346
360
|
// write:true, matching `land`. A returned hunk that a human pastes is the
|
package/src/tools/registry.ts
CHANGED
|
@@ -472,10 +472,25 @@ export type ClaimEvidence = {
|
|
|
472
472
|
samePane: boolean;
|
|
473
473
|
boundElsewhere: number;
|
|
474
474
|
reasons: string[];
|
|
475
|
+
/**
|
|
476
|
+
* SESSION processes still holding this identity, structured rather than
|
|
477
|
+
* buried in `reasons` prose.
|
|
478
|
+
*
|
|
479
|
+
* A live SESSION binding is not the same fact as a live TRANSPORT marker, and
|
|
480
|
+
* conflating them is what let a refusal read as routine. The marker's pid is
|
|
481
|
+
* the pusher daemon — expected, and doing its job. A session binding for this
|
|
482
|
+
* id, alive, that is not this process, is a LEFTOVER PROCESS holding an
|
|
483
|
+
* identity: yesterday two sessions held worker-3's, and the refusal that
|
|
484
|
+
* reported it sent the reader to re-join.
|
|
485
|
+
*
|
|
486
|
+
* Named so the reader can look at the pid instead of routing around it.
|
|
487
|
+
*/
|
|
488
|
+
leakedSessions: { pid: number; via: string }[];
|
|
475
489
|
};
|
|
476
490
|
|
|
477
491
|
export async function liveClaimEvidence(agentId: string, now: number): Promise<ClaimEvidence> {
|
|
478
492
|
const reasons: string[] = [];
|
|
493
|
+
const leakedSessions: { pid: number; via: string }[] = [];
|
|
479
494
|
let verifiable = true;
|
|
480
495
|
let samePane = false;
|
|
481
496
|
let heartbeatFresh = false;
|
|
@@ -529,6 +544,7 @@ export async function liveClaimEvidence(agentId: string, now: number): Promise<C
|
|
|
529
544
|
if (!s || s.agentId !== agentId || s.pid === process.pid) continue;
|
|
530
545
|
if (isPidAlive(s.pid)) {
|
|
531
546
|
boundElsewhere++;
|
|
547
|
+
leakedSessions.push({ pid: s.pid, via: String(s.via ?? "unknown") });
|
|
532
548
|
reasons.push(`another live session (pid ${s.pid}, via ${s.via}) is already bound to this id`);
|
|
533
549
|
}
|
|
534
550
|
}
|
|
@@ -539,6 +555,7 @@ export async function liveClaimEvidence(agentId: string, now: number): Promise<C
|
|
|
539
555
|
samePane,
|
|
540
556
|
boundElsewhere,
|
|
541
557
|
reasons,
|
|
558
|
+
leakedSessions,
|
|
542
559
|
};
|
|
543
560
|
}
|
|
544
561
|
|
package/src/tools/stall.ts
CHANGED
|
@@ -21,6 +21,7 @@ import path from "node:path";
|
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { parseWorkDoc, workstreamsV1RowsOf } from "@davidbalzan/groundwork-seam";
|
|
23
23
|
import { ROOT, AGENTS_FILE, readJson } from "../store.js";
|
|
24
|
+
import { classifyBoardRef, refInCell } from "./board-ref.js";
|
|
24
25
|
import { loadLiveTransports } from "./registry.js";
|
|
25
26
|
|
|
26
27
|
const BOARD_DOC = "docs/WORKSTREAMS.md";
|
|
@@ -31,7 +32,19 @@ const haltFile = () => path.join(ROOT, "halt.json");
|
|
|
31
32
|
|
|
32
33
|
export type StallHit =
|
|
33
34
|
| { kind: "no-heartbeat"; agentId: string; stream: string; minutes: number }
|
|
34
|
-
| { kind: "no-vcs-activity"; agentId: string; branch: string; minutes: number }
|
|
35
|
+
| { kind: "no-vcs-activity"; agentId: string; branch: string; minutes: number }
|
|
36
|
+
/**
|
|
37
|
+
* A 🚧 row whose branch HAS ALREADY LANDED. Not a stall — the opposite: the
|
|
38
|
+
* work finished and nobody closed the row.
|
|
39
|
+
*
|
|
40
|
+
* It exists because the VCS axis was catching this BY ACCIDENT and is about to
|
|
41
|
+
* stop. A merged ref is frozen, so it reported an ever-growing "no activity"
|
|
42
|
+
* age, and that false stall was the only thing surfacing a real routing
|
|
43
|
+
* failure: a lane sitting In Progress with nothing routed to it. Making the
|
|
44
|
+
* stall honest would have silently removed the signal, so the signal gets its
|
|
45
|
+
* own name instead of inheriting a wrong one.
|
|
46
|
+
*/
|
|
47
|
+
| { kind: "stale-row"; agentId: string; branch: string; why: string };
|
|
35
48
|
|
|
36
49
|
// ---------- halt ----------
|
|
37
50
|
|
|
@@ -343,47 +356,48 @@ export async function stallCheckTool(args: { repo?: string; stallMinutes?: numbe
|
|
|
343
356
|
// heartbeats. An unresolvable value is REPORTED as unmeasurable rather than
|
|
344
357
|
// skipped in silence: a silent `continue` and a healthy agent produce the
|
|
345
358
|
// same output, which is the failure this whole verb exists to avoid.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
359
|
+
// ONE CLASSIFIER, SHARED WITH `claim` (board-ref.ts). The old block asked
|
|
360
|
+
// only "does it resolve", and three of the four ways this cell has been
|
|
361
|
+
// wrong resolved cleanly — a shared ref, a local-only ref, and a merged
|
|
362
|
+
// ref. Resolution was never the property; the property is whether the ref's
|
|
363
|
+
// movement is THIS AGENT'S WORK.
|
|
364
|
+
const verdict = classifyBoardRef(repo, agentId, row.branchWorktree);
|
|
365
|
+
if (verdict.kind === "merged") {
|
|
366
|
+
// A VERDICT, NOT AN ABSENCE — and it counts as coverage, because the check
|
|
367
|
+
// did learn something about this lane: its work landed and its row is
|
|
368
|
+
// stale. Reported as its own kind rather than as a stall, which is what
|
|
369
|
+
// the frozen ref was reporting it as.
|
|
370
|
+
hits.push({ kind: "stale-row", agentId, branch: refInCell(row.branchWorktree), why: verdict.why });
|
|
371
|
+
measured.add(agentId);
|
|
349
372
|
continue;
|
|
350
373
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
sha = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${raw}^{commit}`], {
|
|
354
|
-
cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"],
|
|
355
|
-
}).trim();
|
|
356
|
-
} catch {
|
|
357
|
-
sha = "";
|
|
358
|
-
}
|
|
359
|
-
if (!sha) {
|
|
360
|
-
unmeasurable.push({
|
|
361
|
-
agentId,
|
|
362
|
-
value: raw,
|
|
363
|
-
why: `'${raw}' does not resolve as a git ref — it is a path or glob, and \`git log <path>\` would silently report that PATH's last commit as this agent's activity`,
|
|
364
|
-
});
|
|
374
|
+
if (verdict.kind !== "measurable") {
|
|
375
|
+
unmeasurable.push({ agentId, value: refInCell(row.branchWorktree), why: verdict.why });
|
|
365
376
|
continue;
|
|
366
377
|
}
|
|
367
378
|
try {
|
|
368
379
|
// The resolved SHA, and `--`: a ref can then never be re-read as a
|
|
369
380
|
// pathspec, which is the ambiguity that produced the wrong number.
|
|
381
|
+
const sha = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${verdict.ref}^{commit}`], {
|
|
382
|
+
cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"],
|
|
383
|
+
}).trim();
|
|
370
384
|
const iso = execFileSync("git", ["log", "-1", "--format=%cI", sha, "--"], {
|
|
371
385
|
cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"],
|
|
372
386
|
}).trim();
|
|
373
387
|
const age = now - Date.parse(iso);
|
|
374
388
|
measured.add(agentId);
|
|
375
389
|
if (age > limit) {
|
|
376
|
-
hits.push({ kind: "no-vcs-activity", agentId, branch:
|
|
390
|
+
hits.push({ kind: "no-vcs-activity", agentId, branch: verdict.ref, minutes: Math.round(age / 60000) });
|
|
377
391
|
}
|
|
378
392
|
} catch {
|
|
379
|
-
unmeasurable.push({ agentId, value:
|
|
393
|
+
unmeasurable.push({ agentId, value: verdict.ref, why: "resolved as a ref but its log could not be read" });
|
|
380
394
|
}
|
|
381
395
|
}
|
|
382
396
|
|
|
383
|
-
// COVERAGE, not "did it run". A row is COVERED when
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
397
|
+
// COVERAGE, not "did it run". A row is COVERED when a predicate produced a
|
|
398
|
+
// verdict about it; a row where every predicate came back unmeasurable was
|
|
399
|
+
// looked at and not measured, and counting it as checked is how "the clock
|
|
400
|
+
// ran" gets mistaken for "the fleet is observed".
|
|
387
401
|
const owners = inFlight.map((r) => r.owner.replace(/[`*]/g, "").trim()).filter(Boolean);
|
|
388
402
|
const blind = [...new Set(owners)].filter((id) => !measured.has(id));
|
|
389
403
|
const measurable = Math.max(0, inFlight.length - blind.length);
|
package/src/tools/transport.ts
CHANGED
|
@@ -5,6 +5,15 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { registerTool } from "./registry.js";
|
|
6
6
|
import { roleInputSchema, type RoleArg } from "../roles.js";
|
|
7
7
|
import { attributeWriter, isGitRepo, lastWriterOf, loadScopes, ownsDocument } from "./scopes.js";
|
|
8
|
+
// Moved to a LEAF so `status` can report capabilities without a cycle — see
|
|
9
|
+
// src/server-identity.ts. Re-exported so every existing importer is untouched.
|
|
10
|
+
export { resolveServerIdentity } from "../server-identity.js";
|
|
11
|
+
import { resolveServerIdentity } from "../server-identity.js";
|
|
12
|
+
// ONE-WAY EDGE, and the direction is the point: `status` CALLS the capabilities
|
|
13
|
+
// verb rather than re-deriving its probes. Two probes of the same thing is the
|
|
14
|
+
// four-matchers defect committed deliberately, which is why 17.4 was deferred
|
|
15
|
+
// behind 13.7 instead of built beside it.
|
|
16
|
+
import { capabilitiesTool } from "../capabilities.js";
|
|
8
17
|
import { sendMessageTool, readMessagesTool } from "./messaging.js";
|
|
9
18
|
import { randomUUID } from "node:crypto";
|
|
10
19
|
import { existsSync, openSync, readFileSync, watch } from "node:fs";
|
|
@@ -835,63 +844,6 @@ function resolvePusherPath(): string {
|
|
|
835
844
|
return path.resolve(here, "..", "..", "hooks", "tmux-pusher.mjs");
|
|
836
845
|
}
|
|
837
846
|
|
|
838
|
-
/** Running server package + optional git identity. `fromDir` is a test seam. */
|
|
839
|
-
export function resolveServerIdentity(fromDir?: string): {
|
|
840
|
-
path: string;
|
|
841
|
-
version: string;
|
|
842
|
-
branch?: string;
|
|
843
|
-
sha?: string;
|
|
844
|
-
} {
|
|
845
|
-
const start = fromDir ?? path.dirname(fileURLToPath(import.meta.url));
|
|
846
|
-
let dir = start;
|
|
847
|
-
let pkgFile: string | undefined;
|
|
848
|
-
for (let i = 0; i < 8; i++) {
|
|
849
|
-
const candidate = path.join(dir, "package.json");
|
|
850
|
-
if (existsSync(candidate)) {
|
|
851
|
-
pkgFile = candidate;
|
|
852
|
-
break;
|
|
853
|
-
}
|
|
854
|
-
const parent = path.dirname(dir);
|
|
855
|
-
if (parent === dir) break;
|
|
856
|
-
dir = parent;
|
|
857
|
-
}
|
|
858
|
-
const pkgPath = pkgFile ?? path.resolve(start, "..", "..", "package.json");
|
|
859
|
-
const pkgDir = path.dirname(pkgPath);
|
|
860
|
-
let version = "unknown";
|
|
861
|
-
try {
|
|
862
|
-
const raw = JSON.parse(readFileSync(pkgPath, "utf8")) as { version?: string };
|
|
863
|
-
if (typeof raw.version === "string" && raw.version) version = raw.version;
|
|
864
|
-
} catch {
|
|
865
|
-
/* leave unknown — never invent a version */
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const out: { path: string; version: string; branch?: string; sha?: string } = {
|
|
869
|
-
path: pkgDir,
|
|
870
|
-
version,
|
|
871
|
-
};
|
|
872
|
-
|
|
873
|
-
dir = pkgDir;
|
|
874
|
-
let gitRoot: string | undefined;
|
|
875
|
-
for (let i = 0; i < 10; i++) {
|
|
876
|
-
if (existsSync(path.join(dir, ".git"))) {
|
|
877
|
-
gitRoot = dir;
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
const parent = path.dirname(dir);
|
|
881
|
-
if (parent === dir) break;
|
|
882
|
-
dir = parent;
|
|
883
|
-
}
|
|
884
|
-
if (gitRoot && isGitRepo(gitRoot)) {
|
|
885
|
-
const branch = spawnSync("git", ["-C", gitRoot, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" });
|
|
886
|
-
const sha = spawnSync("git", ["-C", gitRoot, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
887
|
-
const b = branch.status === 0 ? String(branch.stdout).trim() : "";
|
|
888
|
-
const s = sha.status === 0 ? String(sha.stdout).trim() : "";
|
|
889
|
-
if (b) out.branch = b;
|
|
890
|
-
if (s) out.sha = s;
|
|
891
|
-
}
|
|
892
|
-
return out;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
847
|
/** An epoch-ms stamp as ISO-8601 UTC, or an explicit absence — never a blank. */
|
|
896
848
|
function stamp(ms: number | undefined): string {
|
|
897
849
|
return ms === undefined ? "(no stamp)" : new Date(ms).toISOString();
|
|
@@ -925,6 +877,22 @@ export async function statusTool(args: { agentId: string }) {
|
|
|
925
877
|
const cursor = await readJson<Cursor>(cursorFile(args.agentId), {});
|
|
926
878
|
const inboxOffset = cursor.inboxOffset ?? 0;
|
|
927
879
|
const unread = Math.max(0, inbox.length - inboxOffset);
|
|
880
|
+
// WHAT THIS PROCESS CAN ACTUALLY DO, embedded WHOLE.
|
|
881
|
+
//
|
|
882
|
+
// A new tool otherwise lands silently: an agent reading an old listing
|
|
883
|
+
// asserts the tool does not exist, and it is right about its own listing and
|
|
884
|
+
// wrong about the world. Three wrong conclusions in two days came from
|
|
885
|
+
// exactly that, each about which build a pane was running.
|
|
886
|
+
//
|
|
887
|
+
// The full report travels, not a summary. Flattening it to a boolean would
|
|
888
|
+
// hand the caller a tidier answer and a worse one: `versionLabel` stays
|
|
889
|
+
// CONTEXT rather than evidence, a THROWN probe stays an absent capability
|
|
890
|
+
// rather than a broken check, and `answeredBy` keeps naming which pid
|
|
891
|
+
// answered — two servers disagreeing mid-rollout is the normal case, and it
|
|
892
|
+
// is the case every other instrument here reports as if the fleet were one
|
|
893
|
+
// thing.
|
|
894
|
+
const capabilities = await capabilitiesTool();
|
|
895
|
+
|
|
928
896
|
return {
|
|
929
897
|
agentId: args.agentId,
|
|
930
898
|
registered: !!entry,
|
|
@@ -935,6 +903,7 @@ export async function statusTool(args: { agentId: string }) {
|
|
|
935
903
|
inboxUnread: unread,
|
|
936
904
|
inTmux: !!process.env.TMUX_PANE,
|
|
937
905
|
tmuxPane: process.env.TMUX_PANE,
|
|
906
|
+
capabilities,
|
|
938
907
|
};
|
|
939
908
|
}
|
|
940
909
|
|