agent-coord-mcp 0.26.15 → 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/server.js +59 -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/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/package.json +1 -1
- package/scripts/check-test-count.mjs +152 -91
- package/src/server.ts +62 -2
- package/src/tools/board-ref.ts +194 -0
- package/src/tools/records.ts +15 -1
- package/src/tools/registry.ts +17 -0
- package/src/tools/stall.ts +38 -24
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* IS THIS BOARD CELL'S REF A PER-AGENT ACTIVITY SIGNAL?
|
|
3
|
+
*
|
|
4
|
+
* One classifier, used by the CHECKER that reads the cell and the VERB that
|
|
5
|
+
* writes it. A rule enforced at the read and not the write is a rule the write
|
|
6
|
+
* defeats: `claim` writes these rows, so if a bad ref is refusable by
|
|
7
|
+
* `stall_check` and writable by `claim`, the verb the fleet is being pushed
|
|
8
|
+
* toward becomes the thing that manufactures the bad row.
|
|
9
|
+
*
|
|
10
|
+
* FOUR WAYS THIS ONE CELL HAS BEEN WRONG, all measured on this fleet:
|
|
11
|
+
*
|
|
12
|
+
* `docs/*` a PATH — refused today, correctly
|
|
13
|
+
* `main` a SHARED ref — resolved, and reported the
|
|
14
|
+
* whole fleet's activity as one agent's, so the
|
|
15
|
+
* row could never stall
|
|
16
|
+
* `worker-3/task13-…` LOCAL-ONLY — resolved for its author and for
|
|
17
|
+
* nobody else, so the same board read
|
|
18
|
+
* differently from two checkouts
|
|
19
|
+
* `worker-1/task16-3-…` MERGED — resolves everywhere and is FROZEN by
|
|
20
|
+
* construction, so it reports an ever-growing
|
|
21
|
+
* stall on a lane that is finished
|
|
22
|
+
*
|
|
23
|
+
* THREE OF THOSE FOUR RESOLVED CLEANLY. So the test cannot be "does it
|
|
24
|
+
* resolve" — that check passed on every bad case except the path. The property
|
|
25
|
+
* is narrower and it is the one thing all four violate:
|
|
26
|
+
*
|
|
27
|
+
* THE CELL MUST NAME A REF WHOSE MOVEMENT IS THIS AGENT'S WORK.
|
|
28
|
+
*
|
|
29
|
+
* Which decomposes into three questions git can actually answer:
|
|
30
|
+
*
|
|
31
|
+
* scoped does the ref name begin with this agent's id? That is the ONLY
|
|
32
|
+
* per-agent signal this repo carries. Author cannot serve: measured
|
|
33
|
+
* on 60 commits of `origin/main`, every one is authored "David
|
|
34
|
+
* Balzan", so an `--author` filter would match every agent on every
|
|
35
|
+
* ref forever WHILE LOOKING SCOPED.
|
|
36
|
+
* shared is it resolvable from ANY checkout, not just the one that happens
|
|
37
|
+
* to have fetched it? That means the remote-tracking form.
|
|
38
|
+
* live is it still ahead of the base? A merged branch's ref never moves
|
|
39
|
+
* again, so measuring it produces a number that only grows.
|
|
40
|
+
*
|
|
41
|
+
* WHAT THIS DELIBERATELY DOES NOT DO: guess. Every rejection is reported with
|
|
42
|
+
* the reason and the population it judged, and an UNPUSHED branch is called
|
|
43
|
+
* unpushed rather than stalled — a freshly claimed lane has no shared evidence
|
|
44
|
+
* yet, and saying "stalled" there would be the manufactured-narrowing error
|
|
45
|
+
* pointed at a new lane.
|
|
46
|
+
*/
|
|
47
|
+
import { execFileSync } from "node:child_process";
|
|
48
|
+
|
|
49
|
+
export type BoardRefVerdict =
|
|
50
|
+
| { kind: "measurable"; ref: string }
|
|
51
|
+
| { kind: "empty"; why: string }
|
|
52
|
+
| { kind: "path"; why: string }
|
|
53
|
+
| { kind: "shared"; why: string }
|
|
54
|
+
| { kind: "unscoped"; why: string }
|
|
55
|
+
| { kind: "local-only"; why: string }
|
|
56
|
+
| { kind: "unpushed"; why: string }
|
|
57
|
+
| { kind: "merged"; why: string };
|
|
58
|
+
|
|
59
|
+
const git = (repo: string, args: string[]): string | null => {
|
|
60
|
+
try {
|
|
61
|
+
return execFileSync("git", args, { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const resolves = (repo: string, ref: string): boolean => !!git(repo, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
67
|
+
|
|
68
|
+
/** The `\`ref\`` inside a `Branch · Worktree` cell, or "". */
|
|
69
|
+
export function refInCell(cell: string): string {
|
|
70
|
+
return (String(cell ?? "").match(/`([^`]+)`/)?.[1] ?? "").trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The remote-tracking form a board cell should name for `branch`. */
|
|
74
|
+
export const boardRefFor = (branch: string): string => (branch.startsWith("origin/") ? branch : `origin/${branch}`);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Classify one cell for one agent.
|
|
78
|
+
*
|
|
79
|
+
* `base` is the ref a merged branch would be contained in — `origin/main` by
|
|
80
|
+
* convention, passed rather than assumed so a consumer fleet on another default
|
|
81
|
+
* branch is not silently misjudged.
|
|
82
|
+
*/
|
|
83
|
+
export function classifyBoardRef(
|
|
84
|
+
repo: string,
|
|
85
|
+
agentId: string,
|
|
86
|
+
cell: string,
|
|
87
|
+
base = "origin/main",
|
|
88
|
+
): BoardRefVerdict {
|
|
89
|
+
const raw = refInCell(cell);
|
|
90
|
+
if (!raw) return { kind: "empty", why: "no ref in the Branch · Worktree cell" };
|
|
91
|
+
|
|
92
|
+
const bare = raw.replace(/^origin\//, "");
|
|
93
|
+
|
|
94
|
+
// A SHARED REF FIRST, because `main` resolves and would otherwise be measured.
|
|
95
|
+
// Named explicitly rather than inferred from scoping, so the reason a reader
|
|
96
|
+
// gets is the real one.
|
|
97
|
+
const shared = new Set(["main", "master", "develop", "trunk", "HEAD"]);
|
|
98
|
+
if (shared.has(bare)) {
|
|
99
|
+
return {
|
|
100
|
+
kind: "shared",
|
|
101
|
+
why:
|
|
102
|
+
`'${raw}' is a SHARED branch — every agent merges into it, so its movement is the fleet's activity and not '${agentId}'s. ` +
|
|
103
|
+
`A row pointing here can never stall, which is a manufactured green rather than a measurement.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// SCOPED BY NAME — but only refusing a ref that belongs to a DIFFERENT agent,
|
|
108
|
+
// never one that is merely unconventional.
|
|
109
|
+
//
|
|
110
|
+
// Authorship cannot carry ownership here (every commit in this repo is
|
|
111
|
+
// authored by the same person, so `--author` would match every agent on every
|
|
112
|
+
// ref while looking scoped), which leaves the branch-naming convention
|
|
113
|
+
// `<agentId>/<slice>`. But REQUIRING that convention would make a repo that
|
|
114
|
+
// does not use it universally blind — and a narrowing that disables the check
|
|
115
|
+
// everywhere is worse than the defect it fixes. So a plain `feature-x` is
|
|
116
|
+
// allowed and `<someone-else>/x` is refused: the second is positively
|
|
117
|
+
// someone else's work, the first is only unlabelled.
|
|
118
|
+
const owner = bare.includes("/") ? bare.slice(0, bare.indexOf("/")) : null;
|
|
119
|
+
if (owner && owner !== agentId && /^[\w.-]+-(worker|aide|qa|ci|coordinator)(-\d+)?$/.test(owner)) {
|
|
120
|
+
return {
|
|
121
|
+
kind: "unscoped",
|
|
122
|
+
why:
|
|
123
|
+
`'${raw}' is scoped to '${owner}', not '${agentId}' — its movement is that agent's work. ` +
|
|
124
|
+
`A per-agent signal needs a ref only this agent writes, and the branch name is the only ownership marker this repo carries.`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const remote = `origin/${bare}`;
|
|
129
|
+
const remoteResolves = resolves(repo, remote);
|
|
130
|
+
const localResolves = resolves(repo, bare);
|
|
131
|
+
|
|
132
|
+
// NO SHARED FORM CAN EXIST, so the local ref is the best evidence there is.
|
|
133
|
+
// Without this a repo with no `origin` remote would report every row
|
|
134
|
+
// unmeasurable forever — the check disabled everywhere by a rule meant to
|
|
135
|
+
// make it honest. Preferring the shared form is right; requiring one that
|
|
136
|
+
// cannot exist is not.
|
|
137
|
+
if (!remoteResolves && !resolves(repo, "origin/HEAD") && git(repo, ["remote", "get-url", "origin"]) === null) {
|
|
138
|
+
if (!localResolves) {
|
|
139
|
+
return { kind: "path", why: `'${raw}' does not resolve as a git ref in a repo with no remote — it is a path or glob` };
|
|
140
|
+
}
|
|
141
|
+
return { kind: "measurable", ref: bare };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!remoteResolves) {
|
|
145
|
+
// Local-only vs never-pushed are different facts and only one is a defect
|
|
146
|
+
// in the CELL. Both are unmeasurable, and neither is a stall.
|
|
147
|
+
if (localResolves) {
|
|
148
|
+
return {
|
|
149
|
+
kind: "local-only",
|
|
150
|
+
why:
|
|
151
|
+
`'${raw}' resolves in THIS checkout and not on the remote, so the same board reads differently from another checkout — ` +
|
|
152
|
+
`whether it is measurable depends on who last ran \`git fetch\`, which is a property of the reader rather than of the work.`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
kind: "unpushed",
|
|
157
|
+
why:
|
|
158
|
+
`'${remote}' does not exist — the branch has not been pushed, so there is no shared evidence of activity yet. ` +
|
|
159
|
+
`That is an ABSENCE of evidence on a new lane, not a stall.`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// MERGED IS FROZEN. Its ref can never move again, so any age computed from it
|
|
164
|
+
// only grows — an ever-worsening stall on a lane that is finished.
|
|
165
|
+
//
|
|
166
|
+
// `--is-ancestor` ALONE CANNOT SEE IT, and this fleet is the case that proves
|
|
167
|
+
// it: we SQUASH-merge, so the branch's commits never become ancestors of the
|
|
168
|
+
// base. Measured on a branch I knew was merged — `--is-ancestor` said NO
|
|
169
|
+
// while the work was plainly on main. A merged-detector that cannot detect
|
|
170
|
+
// this repo's own merge strategy is inert, which is worse than absent because
|
|
171
|
+
// it reads as covered.
|
|
172
|
+
//
|
|
173
|
+
// `git cherry` compares PATCH IDS, so a squashed single-commit branch reports
|
|
174
|
+
// `-` (already upstream). It is not complete either — squashing several
|
|
175
|
+
// commits into one changes the patch id — so BOTH tests run and either one
|
|
176
|
+
// answering yes is enough. Neither is asked to be sufficient alone.
|
|
177
|
+
if (resolves(repo, base)) {
|
|
178
|
+
const ancestor = git(repo, ["merge-base", "--is-ancestor", remote, base]) !== null;
|
|
179
|
+
const cherry = git(repo, ["cherry", base, remote]);
|
|
180
|
+
const allUpstream =
|
|
181
|
+
cherry !== null && cherry.length > 0 && cherry.split("\n").every((l) => l.trim().startsWith("-"));
|
|
182
|
+
if (ancestor || allUpstream) {
|
|
183
|
+
return {
|
|
184
|
+
kind: "merged",
|
|
185
|
+
why:
|
|
186
|
+
`'${remote}' has already landed in ${base} (${ancestor ? "an ancestor" : "every commit's patch is upstream — a squash merge"}) — ` +
|
|
187
|
+
`so this ref is FROZEN and can never move again. ` +
|
|
188
|
+
`Measuring it reports a stall that grows forever on a finished lane. The row needs the agent's current slice, or an idle marker.`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { kind: "measurable", ref: remote };
|
|
194
|
+
}
|
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);
|