@feigi/fleet-ctl 3.21.13 → 3.22.1
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/package.json +1 -1
- package/scripts/ambient-git-vars-mjs-prose.test.mjs +7 -3
- package/scripts/cell-readout.mjs +148 -0
- package/scripts/cell-readout.test.mjs +341 -0
- package/scripts/fleet-tick.mjs +46 -5
- package/scripts/fleet-tick.test.mjs +132 -4
- package/scripts/member-outcomes-header.test.mjs +45 -85
- package/scripts/member-outcomes.mjs +2 -2
- package/scripts/member-outcomes.test.mjs +3 -3
- package/scripts/tier-outcomes-header.test.mjs +37 -62
- package/skills/run-team/SKILL.md +39 -34
|
@@ -452,7 +452,7 @@ const pr = (number, labels = [], closes = [number + 1000], headRefOid = HEAD_B)
|
|
|
452
452
|
closingIssuesReferences: closes.map((n) => ({ number: n })),
|
|
453
453
|
headRefOid,
|
|
454
454
|
});
|
|
455
|
-
const run = (ledger, prs = [], closed) => deriveRun({ rows: [], dispatched: [], drain: null, ...ledger }, prs, closed);
|
|
455
|
+
const run = (ledger, prs = [], closed, finished) => deriveRun({ rows: [], dispatched: [], drain: null, ...ledger }, prs, closed, finished);
|
|
456
456
|
|
|
457
457
|
test("deriveRun: live implementers are the unsettled impl- tokens — the #1692 shape, read rather than recited", () => {
|
|
458
458
|
// #1692 was two sources disagreeing about the same fleet. There is one
|
|
@@ -506,6 +506,44 @@ test("deriveRun: live names every unsettled member and each in-flight review by
|
|
|
506
506
|
assert.deepEqual(r.live, ["fix-pr-21", "impl-15", "review:PR#20"], "run2 returned and run3 is dead, so neither is live");
|
|
507
507
|
});
|
|
508
508
|
|
|
509
|
+
// A review whose own `reviewed=` write never landed stays in flight in the
|
|
510
|
+
// ledger forever. The ledger cannot say the PR has since merged; the caller's
|
|
511
|
+
// probe can, and hands the merged or closed PR numbers to the fold.
|
|
512
|
+
const ZOMBIE_ROWS = [20, 21, 22].map((n) => `#${n - 10} impl-${n - 10}=PR#${n} → PR#${n} · review=member:review-pr-${n}`);
|
|
513
|
+
|
|
514
|
+
test("deriveRun: an in-flight review whose PR has left the open list is named for a probe, and still counts until the probe answers", () => {
|
|
515
|
+
const r = run({ rows: ZOMBIE_ROWS }, [pr(21)]);
|
|
516
|
+
assert.deepEqual(r.reviewsOffList, [20, 22], "only the in-flight reviews of PRs not on the open list");
|
|
517
|
+
assert.equal(r.reviewsLive, 3, "no probe answer: fail closed, every one still reads as live");
|
|
518
|
+
assert.deepEqual(r.live, ["review:PR#20", "review:PR#21", "review:PR#22"]);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("deriveRun: reviewsOffList is ascending whatever order the ledger rows arrive in", () => {
|
|
522
|
+
const r = run({ rows: [...ZOMBIE_ROWS].reverse() }, [pr(21)]);
|
|
523
|
+
assert.deepEqual(r.reviewsOffList, [20, 22], "rows arrive 22, 21, 20: the sort, not ledger order, makes it ascending");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("deriveRun: a review in flight on a PR the caller found merged or closed is not live", () => {
|
|
527
|
+
const r = run({ rows: ZOMBIE_ROWS }, [pr(21)], undefined, new Set([20, 22]));
|
|
528
|
+
assert.equal(r.reviewsLive, 1, "only PR 21's review, on the open list, is live");
|
|
529
|
+
assert.deepEqual(r.live, ["review:PR#21"]);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("deriveRun: a review in flight on an OPEN PR is live whatever the finished set says", () => {
|
|
533
|
+
// A probe answer that contradicts the open list is not trusted to retire a
|
|
534
|
+
// review: the open list is the fold's own read of which PRs are open.
|
|
535
|
+
const r = run({ rows: ZOMBIE_ROWS }, [pr(20), pr(21), pr(22)], undefined, new Set([20, 21, 22]));
|
|
536
|
+
assert.equal(r.reviewsLive, 3);
|
|
537
|
+
assert.deepEqual(r.reviewsOffList, []);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("deriveRun: a finished PR's returned review and settled members read as before", () => {
|
|
541
|
+
const rows = [...ZOMBIE_ROWS, "#40 impl-40=PR#50 → PR#50 · review=member:review-pr-50 reviewed=abc1234:0/0/0"];
|
|
542
|
+
const r = run({ rows }, [], undefined, new Set([20, 21, 22, 50]));
|
|
543
|
+
assert.deepEqual([r.reviewsLive, r.reviewsOffList, r.live], [0, [20, 21, 22], []]);
|
|
544
|
+
assert.deepEqual(r.reviewed.map((x) => x.pr), [50], "the returned review is still on record");
|
|
545
|
+
});
|
|
546
|
+
|
|
509
547
|
test("deriveRun: fix-pr is due on a returned review with survivors and no fix-applier since", () => {
|
|
510
548
|
const r = run({
|
|
511
549
|
rows: [
|
|
@@ -1267,7 +1305,9 @@ test("CLI: every synchronous spawn in this file goes through spawnBounded (#2320
|
|
|
1267
1305
|
});
|
|
1268
1306
|
|
|
1269
1307
|
// `pr list` for the open PRs, `issue view` for a behind-issue premise's state,
|
|
1270
|
-
// `
|
|
1308
|
+
// `pr view` for the state of a PR an in-flight review names that is no longer
|
|
1309
|
+
// on the open list, `issue list --label in-progress` for the stall report's
|
|
1310
|
+
// claimed count.
|
|
1271
1311
|
const GH_STUB = `#!/bin/sh
|
|
1272
1312
|
case "$1 $2" in
|
|
1273
1313
|
"pr list") [ -n "$PR_FAIL" ] && { echo "boom" >&2; exit 1; }; cat "$FIXTURE_PRS" ;;
|
|
@@ -1280,6 +1320,15 @@ case "$1 $2" in
|
|
|
1280
1320
|
# scrub them lifts an exclusion the case's own repository still holds.
|
|
1281
1321
|
[ -n "$GIT_DIR$GIT_WORK_TREE$GH_REPO" ] && { echo '{"state":"CLOSED"}'; exit 0; }
|
|
1282
1322
|
exec jq -c --arg n "$3" '{state: (.[$n] // error("no such issue"))}' "$FIXTURE_ISSUE_STATES" ;;
|
|
1323
|
+
"pr view")
|
|
1324
|
+
echo "$3" >> "$PR_VIEW_LOG"
|
|
1325
|
+
[ -n "$PR_VIEW_FAIL" ] && { echo "gh: pr view failed" >&2; exit 1; }
|
|
1326
|
+
[ -n "$PR_VIEW_BODY" ] && { printf '%s\n' "$PR_VIEW_BODY"; exit 0; }
|
|
1327
|
+
# Real gh answers for the repository an inherited GIT_DIR or GH_REPO names;
|
|
1328
|
+
# this one answers MERGED for every PR there, so a probe that forgot to
|
|
1329
|
+
# scrub them retires a review the case's own repository still holds.
|
|
1330
|
+
[ -n "$GIT_DIR$GIT_WORK_TREE$GH_REPO" ] && { echo '{"state":"MERGED"}'; exit 0; }
|
|
1331
|
+
exec jq -c --arg n "$3" '{state: (.[$n] // error("no such pull request"))}' "$FIXTURE_PR_STATES" ;;
|
|
1283
1332
|
"issue list")
|
|
1284
1333
|
[ -n "$CLAIMED_FAIL" ] && { echo "boom" >&2; exit 1; }
|
|
1285
1334
|
expr=""
|
|
@@ -1320,7 +1369,7 @@ const shortlistText = (ns, scanned = ns.length) => JSON.stringify({ scanned, sho
|
|
|
1320
1369
|
// through info/exclude so no tracked file says so.
|
|
1321
1370
|
function runCli(args = [], {
|
|
1322
1371
|
prs = [], ledger, shortlist, refresh = shortlistText([]), refreshFail = false,
|
|
1323
|
-
issueStates = {}, claimed = [], env: extraEnv = {}, defaultState = false, keep = false, beforeRun = () => {},
|
|
1372
|
+
issueStates = {}, prStates = {}, claimed = [], env: extraEnv = {}, defaultState = false, keep = false, beforeRun = () => {},
|
|
1324
1373
|
baseline = true, afterBaseline = () => {},
|
|
1325
1374
|
} = {}) {
|
|
1326
1375
|
// On macOS tmpdir() is /var -> /private/var; resolving it up front makes
|
|
@@ -1355,6 +1404,7 @@ function runCli(args = [], {
|
|
|
1355
1404
|
afterBaseline(repo);
|
|
1356
1405
|
const refreshLog = fx("refresh.log", "");
|
|
1357
1406
|
const issueViewLog = fx("issue-view.log", "");
|
|
1407
|
+
const prViewLog = fx("pr-view.log", "");
|
|
1358
1408
|
// Every case gets its own state file unless it names one: the default path
|
|
1359
1409
|
// resolves against the git common dir, and a shared streak and digest would
|
|
1360
1410
|
// make the fold cases order-dependent. `defaultState` opts out for the case
|
|
@@ -1367,14 +1417,16 @@ function runCli(args = [], {
|
|
|
1367
1417
|
FIXTURE_PRS: fx("prs.json", JSON.stringify(prs)),
|
|
1368
1418
|
FIXTURE_CLAIMED: fx("claimed.json", JSON.stringify(claimed)),
|
|
1369
1419
|
FIXTURE_ISSUE_STATES: fx("issue-states.json", JSON.stringify(issueStates)),
|
|
1420
|
+
FIXTURE_PR_STATES: fx("pr-states.json", JSON.stringify(prStates)),
|
|
1370
1421
|
FIXTURE_REFRESH: fx("refresh.json", refresh),
|
|
1371
|
-
REFRESH_LOG: refreshLog, ISSUE_VIEW_LOG: issueViewLog,
|
|
1422
|
+
REFRESH_LOG: refreshLog, ISSUE_VIEW_LOG: issueViewLog, PR_VIEW_LOG: prViewLog,
|
|
1372
1423
|
...(refreshFail ? { REFRESH_FAIL: "1" } : {}),
|
|
1373
1424
|
...extraEnv,
|
|
1374
1425
|
},
|
|
1375
1426
|
});
|
|
1376
1427
|
r.refreshed = readFileSync(refreshLog, "utf8").split("\n").filter(Boolean).length;
|
|
1377
1428
|
r.issueViews = readFileSync(issueViewLog, "utf8").split("\n").filter(Boolean);
|
|
1429
|
+
r.prViews = readFileSync(prViewLog, "utf8").split("\n").filter(Boolean);
|
|
1378
1430
|
r.repo = repo;
|
|
1379
1431
|
r.dir = dir;
|
|
1380
1432
|
returned = true;
|
|
@@ -1843,6 +1895,82 @@ test("CLI: an inherited GIT_DIR cannot retarget the behind-issue premise probe",
|
|
|
1843
1895
|
assert.equal(r.refreshed, 0, "#9 is OPEN in the case's own repository — the exclusion stands");
|
|
1844
1896
|
});
|
|
1845
1897
|
|
|
1898
|
+
// A review whose `reviewed=` write never landed stays in flight in the ledger
|
|
1899
|
+
// after its PR merges. Each such row holds one reviewer slot unless the tick
|
|
1900
|
+
// asks gh about the PR and finds it finished.
|
|
1901
|
+
const zombieRows = (numbers) => numbers.map((n) => `#${n - 90} impl-${n - 90}=PR#${n} → PR#${n} · review=member:review-pr-${n}`);
|
|
1902
|
+
const ZOMBIES = Array.from({ length: 10 }, (_, i) => 101 + i);
|
|
1903
|
+
const REVIEW_DUE = pr(500, [], [1500], HEAD_B);
|
|
1904
|
+
const CAPS = ["--reviewer-cap", "10", "--max-reviews", "10"];
|
|
1905
|
+
|
|
1906
|
+
test("CLI: dangling in-flight reviews of merged PRs do not starve review dispatch", () => {
|
|
1907
|
+
const r = runCli(CAPS, {
|
|
1908
|
+
shortlist: shortlistText([]), prs: [REVIEW_DUE], ledger: { rows: zombieRows(ZOMBIES) },
|
|
1909
|
+
prStates: Object.fromEntries(ZOMBIES.map((n) => [n, "MERGED"])),
|
|
1910
|
+
});
|
|
1911
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1912
|
+
assert.deepEqual(r.prViews.map(Number).sort((a, b) => a - b), ZOMBIES, "each off-list PR is asked about");
|
|
1913
|
+
assert.match(r.stdout, /^reviewers +0\/10 → DISPATCH review PR#500/m);
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
test("CLI: a CLOSED PR's dangling review is released too", () => {
|
|
1917
|
+
const r = runCli(["--reviewer-cap", "1", "--max-reviews", "1"], {
|
|
1918
|
+
shortlist: shortlistText([]), prs: [REVIEW_DUE], ledger: { rows: zombieRows([101]) }, prStates: { 101: "CLOSED" },
|
|
1919
|
+
});
|
|
1920
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1921
|
+
assert.match(r.stdout, /^reviewers +0\/1 → DISPATCH review PR#500/m);
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
test("CLI: an in-flight review on an open PR keeps its slot and is never probed", () => {
|
|
1925
|
+
const r = runCli(["--reviewer-cap", "1", "--max-reviews", "1"], {
|
|
1926
|
+
shortlist: shortlistText([]), prs: [pr(101, [], [1101]), REVIEW_DUE], ledger: { rows: zombieRows([101]) },
|
|
1927
|
+
});
|
|
1928
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1929
|
+
assert.deepEqual(r.prViews, [], "an open PR needs no probe");
|
|
1930
|
+
assert.match(r.stdout, /^reviewers +1\/1 → AT CAP/m);
|
|
1931
|
+
assert.doesNotMatch(r.stdout, /DISPATCH review/);
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
test("CLI: a PR off the open list that gh still calls OPEN keeps its review's slot", () => {
|
|
1935
|
+
const r = runCli(["--reviewer-cap", "1", "--max-reviews", "1"], {
|
|
1936
|
+
shortlist: shortlistText([]), prs: [REVIEW_DUE], ledger: { rows: zombieRows([101]) }, prStates: { 101: "OPEN" },
|
|
1937
|
+
});
|
|
1938
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1939
|
+
assert.deepEqual(r.prViews, ["101"]);
|
|
1940
|
+
assert.match(r.stdout, /^reviewers +1\/1 → AT CAP/m);
|
|
1941
|
+
assert.doesNotMatch(r.stdout, /DISPATCH review/);
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
test("CLI: a PR gh cannot answer for is not read as merged - the slot is held and the failure disclosed", () => {
|
|
1945
|
+
const cases = [
|
|
1946
|
+
["a nonzero exit", { PR_VIEW_FAIL: "1" }, /gh pr view 101 exited 1: gh: pr view failed/],
|
|
1947
|
+
["a reply with no state", { PR_VIEW_BODY: "<html>proxy error</html>" }, /gh pr view 101 printed no PR state/],
|
|
1948
|
+
["a state that is not MERGED or CLOSED", { PR_VIEW_BODY: JSON.stringify({ state: "merged" }) }, /gh pr view 101 printed no PR state/],
|
|
1949
|
+
];
|
|
1950
|
+
for (const [what, env, shown] of cases) {
|
|
1951
|
+
const r = runCli(["--reviewer-cap", "1", "--max-reviews", "1"], {
|
|
1952
|
+
shortlist: shortlistText([]), prs: [REVIEW_DUE], ledger: { rows: zombieRows([101]) }, prStates: { 101: "MERGED" }, env,
|
|
1953
|
+
});
|
|
1954
|
+
assert.equal(r.status, 0, `${what}: ${r.stderr}`);
|
|
1955
|
+
assert.match(r.stderr, shown, what);
|
|
1956
|
+
assert.match(r.stdout, /^reviewers +1\/1 → AT CAP/m, what);
|
|
1957
|
+
assert.doesNotMatch(r.stdout, /DISPATCH review/, what);
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
test("CLI: an inherited GIT_DIR cannot retarget the merged-PR review probe", (t) => {
|
|
1962
|
+
const decoy = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-decoy-pr-")));
|
|
1963
|
+
t.after(() => rmSync(decoy, { recursive: true, force: true }));
|
|
1964
|
+
spawnBounded("git", ["init", "-q", decoy]);
|
|
1965
|
+
const r = runCli(["--reviewer-cap", "1", "--max-reviews", "1"], {
|
|
1966
|
+
shortlist: shortlistText([]), prs: [REVIEW_DUE], ledger: { rows: zombieRows([101]) }, prStates: { 101: "OPEN" },
|
|
1967
|
+
env: { GIT_DIR: join(decoy, ".git"), GH_REPO: "someone/else" },
|
|
1968
|
+
});
|
|
1969
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1970
|
+
assert.deepEqual(r.prViews, ["101"], "the probe must still run");
|
|
1971
|
+
assert.match(r.stdout, /^reviewers +1\/1 → AT CAP/m, "101 is OPEN in the case's own repository - its review keeps the slot");
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1846
1974
|
// #2210, end to end: a real repo, a real baseline, a real stray write.
|
|
1847
1975
|
const LIVE = {
|
|
1848
1976
|
ledger: { rows: ["#412 impl-412 · tier-ok=impl-412:fleet-implementer"], dispatched: ["impl-412"] },
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
4
|
-
import { tempDir } from "./temp-dir.mjs";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
4
|
import { spawnSync } from "node:child_process";
|
|
6
5
|
import { join } from "node:path";
|
|
7
6
|
import { COLUMNS } from "./member-outcomes.mjs";
|
|
7
|
+
import { GATE } from "./cell-readout.mjs";
|
|
8
8
|
|
|
9
9
|
const REPO = join(import.meta.dirname, "..", "..");
|
|
10
10
|
const TSV = readFileSync(join(REPO, "docs", "metrics", "member-outcomes.tsv"), "utf8");
|
|
@@ -55,94 +55,54 @@ test("the header states the blank, hand-edit, and superseded-generation rules",
|
|
|
55
55
|
assert.match(HEADER, /workflows\/wf_/);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
test("the header's awk read-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// and distinguish by effort with nothing to catch it.
|
|
64
|
-
//
|
|
65
|
-
// So read the `$n` references out of the header's own awk lines and resolve
|
|
66
|
-
// each against COLUMNS. Mutation this must survive: changing any `$n` in the
|
|
67
|
-
// header without changing COLUMNS.
|
|
68
|
-
const awkLines = headerLines.filter((l) => l.includes("awk -F"));
|
|
69
|
-
assert.ok(awkLines.length >= 2, "header lost its awk read-outs");
|
|
70
|
-
|
|
71
|
-
const pair = pairQuery();
|
|
72
|
-
// BOTH arms are identified by the dispatch record (#1066), never by the role
|
|
73
|
-
// classifier and never by "the models differ" — so both `$n ~ /…/` tests
|
|
74
|
-
// must land on subagent_type, and there must be two of them.
|
|
75
|
-
const arms = [...pair.matchAll(/\$(\d+) ~ \/\(\^\|:\)fleet-implementer(-alt)?\$\//g)];
|
|
76
|
-
assert.equal(arms.length, 2, "the pair query no longer tests BOTH implementer definitions");
|
|
77
|
-
assert.deepEqual(arms.map((m) => Boolean(m[2])), [true, false], "the -alt arm must be tested FIRST — /fleet-implementer$/ is checked in the else branch");
|
|
78
|
-
for (const [, n] of arms) assert.equal(COLUMNS[n - 1], "subagent_type", "deliberateness comes from the dispatch record");
|
|
79
|
-
|
|
80
|
-
const keys = [...pair.matchAll(/[at]\[\$(\d+) FS \$(\d+)\]/g)];
|
|
81
|
-
assert.equal(keys.length, 2, "the pair query no longer keys both arms");
|
|
82
|
-
for (const [, session, model] of keys) {
|
|
83
|
-
assert.equal(COLUMNS[session - 1], "session", "a pair is WITHIN one session");
|
|
84
|
-
assert.equal(COLUMNS[model - 1], "model", "the two arms must have run different MODELS");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const [, dateKey, dateValue] = /d\[\$(\d+)\]=\$(\d+)/.exec(pair);
|
|
88
|
-
assert.equal(COLUMNS[dateKey - 1], "session");
|
|
89
|
-
assert.equal(COLUMNS[dateValue - 1], "run_date", "the gate's second number is distinct run_dateS");
|
|
90
|
-
|
|
58
|
+
test("the header's awk read-out indexes the column it names", () => {
|
|
59
|
+
// Read the `$n` reference out of the header's own awk line and resolve it
|
|
60
|
+
// against COLUMNS — a fact about the CODE alone (COLUMNS[4]) stays green when
|
|
61
|
+
// the header is repointed. Mutation this must survive: changing the `$n` in
|
|
62
|
+
// the header without changing COLUMNS.
|
|
91
63
|
const byModel = headerLines.find((l) => /\{n\[\$\d+\]\+\+\}/.test(l));
|
|
92
64
|
assert.ok(byModel, "header lost its rows-by-model read-out");
|
|
93
65
|
assert.equal(COLUMNS[/\{n\[\$(\d+)\]\+\+\}/.exec(byModel)[1] - 1], "model");
|
|
94
66
|
});
|
|
95
67
|
|
|
96
|
-
// The
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
assert.ok(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
assert.ok(
|
|
106
|
-
|
|
107
|
-
|
|
68
|
+
// The comparison count is cell-readout.mjs's, and the header only points at it:
|
|
69
|
+
// a second copy of the definition as a runnable query is two countings free to
|
|
70
|
+
// drift apart, which is how the pre-cell pair query came to count pairs no cell
|
|
71
|
+
// owns once the cell grid replaced the two definitions it keyed on.
|
|
72
|
+
test("the header points at cell-readout.mjs for a cell's comparisons and carries no query of its own", () => {
|
|
73
|
+
const invocation = headerLines.find((l) => /^#\s+node \S*cell-readout\.mjs\s*$/.test(l));
|
|
74
|
+
assert.ok(invocation, "header lost its `node plugin/scripts/cell-readout.mjs` pointer");
|
|
75
|
+
const script = invocation.replace(/^#\s+node\s+/, "").trim();
|
|
76
|
+
assert.equal(script, "plugin/scripts/cell-readout.mjs");
|
|
77
|
+
assert.ok(existsSync(join(REPO, script)), `the header points at ${script}, which does not exist`);
|
|
78
|
+
// The pointer is runnable as written, from the repo root, against this
|
|
79
|
+
// repo's own metrics files.
|
|
80
|
+
const r = spawnSync(process.execPath, [script], { cwd: REPO, encoding: "utf8" });
|
|
81
|
+
assert.equal(r.status, 0, r.stderr);
|
|
108
82
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// which leave the column indexes (and therefore the test above) intact:
|
|
116
|
-
// dropping the `x[2]!=y[2]` model test reads 4 2, dropping the `-alt` anchor
|
|
117
|
-
// so both arms match one definition reads 0 0, and going back to #1066's own
|
|
118
|
-
// query — arms identified by `$3=="implementer"` rather than by the dispatch
|
|
119
|
-
// — reads 4 2, counting the untyped session and missing the omp one.
|
|
120
|
-
const rows = [
|
|
121
|
-
// a real pair: alt at sonnet against the top definition at opus
|
|
122
|
-
["sA", "2026-09-01", "implementer", "impl-1", "claude-sonnet-5", "high", "1", "", "0", "0", "0", "1", "agent-a1", "claude", "fleet-implementer-alt"],
|
|
123
|
-
["sA", "2026-09-01", "implementer", "impl-2", "claude-opus-5", "high", "2", "", "0", "0", "0", "1", "agent-a2", "claude", "fleet-implementer"],
|
|
124
|
-
// deliberate dispatch, empty comparison: both arms resolved to one model
|
|
125
|
-
["sB", "2026-09-01", "other", "impl-3", "claude-sonnet-5", "high", "3", "", "0", "0", "0", "1", "agent-a3", "omp", "fleet-implementer-alt"],
|
|
126
|
-
["sB", "2026-09-01", "other", "impl-4", "claude-sonnet-5", "high", "4", "", "0", "0", "0", "1", "agent-a4", "omp", "fleet-implementer"],
|
|
127
|
-
// two models, no deliberate dispatch: the accidental pair #1066 counted
|
|
128
|
-
["sC", "2026-09-01", "implementer", "impl-5", "claude-sonnet-5", "high", "5", "", "0", "0", "0", "1", "agent-a5", "claude", ""],
|
|
129
|
-
["sC", "2026-09-01", "implementer", "impl-6", "claude-opus-5", "high", "6", "", "0", "0", "0", "1", "agent-a6", "claude", ""],
|
|
130
|
-
// a second pair on the SAME day — two pairs, one run_date
|
|
131
|
-
["sD", "2026-09-01", "implementer", "impl-7", "claude-sonnet-5", "high", "7", "", "0", "0", "0", "1", "agent-a7", "claude", "fleet-implementer-alt"],
|
|
132
|
-
["sD", "2026-09-01", "implementer", "impl-8", "claude-opus-5", "high", "8", "", "0", "0", "0", "1", "agent-a8", "claude", "fleet-implementer"],
|
|
133
|
-
// and one on another day, so the second number is not the first
|
|
134
|
-
["sE", "2026-09-02", "implementer", "impl-9", "claude-sonnet-5", "high", "9", "", "0", "0", "0", "1", "agent-a9", "claude", "fleet-implementer-alt"],
|
|
135
|
-
["sE", "2026-09-02", "implementer", "impl-10", "claude-opus-5", "high", "10", "", "0", "0", "0", "1", "agent-a10", "claude", "fleet-implementer"],
|
|
136
|
-
// a typed non-implementer at a third model inside a pair session: the
|
|
137
|
-
// review fan-out runs one on every session and must not inflate anything
|
|
138
|
-
["sE", "2026-09-02", "specialist", "rev", "claude-haiku-4-5", "", "", "", "0", "0", "0", "1", "agent-a11", "claude", "fleet-review-tests"],
|
|
139
|
-
];
|
|
140
|
-
const corpus = join(tempDir("mo-hdr-"), "member-outcomes.tsv");
|
|
141
|
-
writeFileSync(corpus, "# header line, skipped by the query\n" + rows.map((r) => r.join("\t")).join("\n") + "\n");
|
|
83
|
+
assert.doesNotMatch(HEADER, /fleet-implementer-alt\$\//, "the header still carries the pre-cell pair query");
|
|
84
|
+
const subagentType = new RegExp(`\\$${COLUMNS.indexOf("subagent_type") + 1}(?!\\d)`);
|
|
85
|
+
assert.ok(
|
|
86
|
+
!headerLines.some((l) => subagentType.test(l)),
|
|
87
|
+
"an awk read-out over `subagent_type` is a second counting of comparisons",
|
|
88
|
+
);
|
|
142
89
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
assert.
|
|
147
|
-
assert.
|
|
90
|
+
// The definition the header states is the one the script applies.
|
|
91
|
+
const block = HEADER.replace(/^#\s?/gm, "").replace(/\s+/g, " ");
|
|
92
|
+
assert.match(block, /a comparison is one session holding an ADMISSIBLE row at X and an admissible slow-high row whose resolved \(`model`, `effort`\) differ from the X row's/);
|
|
93
|
+
assert.match(block, /admissible only when its `subagent_type` is `fleet-implementer-<chosen_cell>` and its `effort` is the cell's level/);
|
|
94
|
+
assert.match(block, /joined to its row here on `session`\+`agent`/);
|
|
95
|
+
const gate = /at least (\w+) comparisons across (\w+) or more distinct `run_date`s/.exec(block);
|
|
96
|
+
assert.ok(gate, "the header no longer states the gate");
|
|
97
|
+
const words = { five: 5, ten: 10 };
|
|
98
|
+
assert.deepEqual({ comparisons: words[gate[1]], runDates: words[gate[2]] }, { ...GATE }, "the header's gate is not cell-readout.mjs's GATE");
|
|
99
|
+
assert.match(block, /`<cell> <comparisons> <run_dates> <resolved models>`/);
|
|
100
|
+
assert.match(block, /`mixed \(<model-a> n=…, <model-b> n=…\)`/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("the header names a producer script that exists", () => {
|
|
104
|
+
const producer = /produced by (\S+\.mjs)/.exec(HEADER);
|
|
105
|
+
assert.ok(producer, "the header no longer names the script that produces its rows");
|
|
106
|
+
assert.ok(existsSync(join(REPO, producer[1])), `the header names ${producer[1]} as its producer, which does not exist`);
|
|
107
|
+
assert.equal(producer[1], "plugin/scripts/member-outcomes.mjs");
|
|
148
108
|
});
|
|
@@ -35,8 +35,8 @@ export { normalizeModel, parseMemberName };
|
|
|
35
35
|
//
|
|
36
36
|
// `subagentType` is the record's `subagent_type` unchanged — `""` stays
|
|
37
37
|
// `""` here, because unlike `effort` it does NOT mean unknown: it means the
|
|
38
|
-
// dispatch named no agent definition, which is the reading
|
|
39
|
-
//
|
|
38
|
+
// dispatch named no agent definition, which is the reading cell-readout.mjs's
|
|
39
|
+
// admissibility test depends on: a blank is never any cell's definition.
|
|
40
40
|
//
|
|
41
41
|
// A member is never torn in the sense this file historically tracked (no
|
|
42
42
|
// fold-back to tear mid-turn); `false` says so plainly rather than leaving
|
|
@@ -269,9 +269,9 @@ test("a nested member's fan-out is scraped too, keyed on its path-relative stem"
|
|
|
269
269
|
});
|
|
270
270
|
|
|
271
271
|
test("role comes from classifyRole and is not invented here", () => {
|
|
272
|
-
// `role` had no assertion anywhere
|
|
273
|
-
// pair query, so a classifyRole regression moved every
|
|
274
|
-
// while the suite stayed green.
|
|
272
|
+
// `role` had no assertion anywhere while it was the GROUPING column of the
|
|
273
|
+
// header's former pair query, so a classifyRole regression moved every
|
|
274
|
+
// bucket in that read-out while the suite stayed green.
|
|
275
275
|
const dir = fixture([
|
|
276
276
|
["impl-580", [sessionEvt("/x"), thinkingEvt("xhigh"), assistantEvt("claude-opus-5")]],
|
|
277
277
|
["merge-bot-3", [sessionEvt("/x"), thinkingEvt("xhigh"), sessionInitEvt("merge pass 3", "fleet-merge-bot"), assistantEvt("claude-opus-5")]],
|
|
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { between, phrase, sentences, stripHashGutter, unemphasized } from "./prose-pin.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { GATE, readout } from "./cell-readout.mjs";
|
|
7
7
|
import { COLUMNS } from "./tier-outcomes.mjs";
|
|
8
8
|
|
|
9
9
|
// #472: this file's header has never been pinned, while SKILL.md's guard tells
|
|
@@ -381,26 +381,20 @@ test("the ruling step refuses to record a sizing verdict it cannot date", () =>
|
|
|
381
381
|
});
|
|
382
382
|
|
|
383
383
|
// `run_date` is DERIVED in `member-outcomes.tsv` (a transcript mtime) and
|
|
384
|
-
// STAMPED AT RULING here, and phase 2's
|
|
385
|
-
// `run_date`s — ten
|
|
386
|
-
// one. Read as a count over THIS file, it answers a different question,
|
|
387
|
-
// nothing in this header used to say so.
|
|
384
|
+
// STAMPED AT RULING here, and phase 2's per-cell gate counts distinct
|
|
385
|
+
// `run_date`s — ten comparisons across five dates — over the other file, never
|
|
386
|
+
// this one. Read as a count over THIS file, it answers a different question,
|
|
387
|
+
// and nothing in this header used to say so.
|
|
388
388
|
//
|
|
389
389
|
// The paragraph that now says it makes claims about files this one does not
|
|
390
390
|
// contain, which is the shape that rots silently: it stays true only while
|
|
391
|
-
//
|
|
392
|
-
// keeps stamping dates off an mtime, and SKILL.md keeps
|
|
393
|
-
// So the pins below DERIVE each claim from its real
|
|
394
|
-
// the `profile` pin above derives the enum instead of
|
|
395
|
-
|
|
391
|
+
// cell-readout.mjs keeps dating comparisons off member-outcomes.tsv,
|
|
392
|
+
// `member-outcomes.mjs` keeps stamping dates off an mtime, and SKILL.md keeps
|
|
393
|
+
// the floor's numbers. So the pins below DERIVE each claim from its real
|
|
394
|
+
// source — the same reason the `profile` pin above derives the enum instead of
|
|
395
|
+
// restating it.
|
|
396
396
|
const MEMBER_SRC = readFileSync(join(REPO, "plugin", "scripts", "member-outcomes.mjs"), "utf8");
|
|
397
397
|
|
|
398
|
-
const memberHeaderLines = [];
|
|
399
|
-
for (const l of MEMBER_TSV.split("\n")) {
|
|
400
|
-
if (!l.startsWith("#") && l.trim()) break;
|
|
401
|
-
memberHeaderLines.push(l);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
398
|
// Bounded at both ends, for `between`'s own stated reason: this header is 140
|
|
405
399
|
// lines about exactly these words, so an unbounded match is satisfiable from
|
|
406
400
|
// the provenance block above the paragraph or the column legend below it. The
|
|
@@ -418,9 +412,9 @@ test("the header says which file each distinct-`run_date` count is read from, an
|
|
|
418
412
|
assert.match(
|
|
419
413
|
block,
|
|
420
414
|
phrase(
|
|
421
|
-
"at least ten
|
|
415
|
+
"at least ten comparisons across five or more distinct `run_date`s, the floor that forbids reading a cell early — is NOT read here at all. It comes from `plugin/scripts/cell-readout.mjs`, which dates each comparison by its session's `run_date` in `docs/metrics/member-outcomes.tsv`",
|
|
422
416
|
),
|
|
423
|
-
"the header no longer says the
|
|
417
|
+
"the header no longer says the per-cell gate's distinct-date count is read from member-outcomes.tsv through cell-readout.mjs, not here",
|
|
424
418
|
);
|
|
425
419
|
|
|
426
420
|
// The definitional halves, each joined to what it makes the column MEAN. A
|
|
@@ -457,47 +451,27 @@ test("the header says which file each distinct-`run_date` count is read from, an
|
|
|
457
451
|
);
|
|
458
452
|
});
|
|
459
453
|
|
|
460
|
-
test("the header's `run_date` source claims still hold against the
|
|
454
|
+
test("the header's `run_date` source claims still hold against the readout, the scraper and the guard", () => {
|
|
461
455
|
const block = runDateSources();
|
|
462
456
|
|
|
463
|
-
// The
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
assert.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
assert.ok(pairKey, "member-outcomes.tsv's header no longer carries the pairing query this header points at");
|
|
482
|
-
assert.equal(
|
|
483
|
-
MEMBER_COLUMNS[pairKey[2] - 1],
|
|
484
|
-
"run_date",
|
|
485
|
-
"the pairing query's distinct-date count is not member-outcomes.tsv's `run_date`",
|
|
486
|
-
);
|
|
487
|
-
assert.ok(
|
|
488
|
-
block.includes(pairKey[0]),
|
|
489
|
-
`the header cites an idiom for the pairing query's date key that member-outcomes.tsv no longer uses (\`${pairKey[0]}\`)`,
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
// The header also cites the query's distinct-date ACCUMULATOR
|
|
493
|
-
// (`length(r)`) — resolve the variable name against the query's own
|
|
494
|
-
// `length(...)` call rather than trusting the header's word for it.
|
|
495
|
-
const pairAccumulator = /print n\+0, length\((\w+)\)/.exec(pairQueryBlock);
|
|
496
|
-
assert.ok(pairAccumulator, "member-outcomes.tsv's pair query no longer prints its distinct-date count via `length(...)`");
|
|
497
|
-
assert.ok(
|
|
498
|
-
block.includes(`length(${pairAccumulator[1]})`),
|
|
499
|
-
`the header cites the wrong accumulator for the pairing query's distinct-date count (expected \`length(${pairAccumulator[1]})\`)`,
|
|
500
|
-
);
|
|
457
|
+
// The gate half: the count lives in cell-readout.mjs and dates each
|
|
458
|
+
// comparison off the OTHER file's `run_date`. Run, not read: one comparison
|
|
459
|
+
// whose ticket-features date and member-outcomes date differ, and the date
|
|
460
|
+
// the readout counts must be the member-outcomes one. If the readout ever
|
|
461
|
+
// keys its dates elsewhere, "it comes from there" stops being true.
|
|
462
|
+
const session = "s-tier-hdr";
|
|
463
|
+
const pull = (agent, chosen_cell) => ({ run_date: "2026-01-01", session, agent, chosen_cell });
|
|
464
|
+
const member = (agent, cell, model) => ({
|
|
465
|
+
session, agent, run_date: "2026-02-02", model, effort: "high", subagentType: `fleet-implementer-${cell}`,
|
|
466
|
+
});
|
|
467
|
+
const { cells: [task] } = readout({
|
|
468
|
+
features: [pull("impl-1", "task-high"), pull("impl-2", "slow-high")],
|
|
469
|
+
members: [member("impl-1", "task-high", "m-task"), member("impl-2", "slow-high", "m-slow")],
|
|
470
|
+
});
|
|
471
|
+
assert.deepEqual([task.cell, task.comparisons, task.runDates], ["task-high", 1, 1]);
|
|
472
|
+
assert.deepEqual([...task.dates], ["2026-02-02"], "cell-readout.mjs no longer dates a comparison by member-outcomes.tsv's `run_date`");
|
|
473
|
+
assert.match(block, /`plugin\/scripts\/cell-readout\.mjs`/);
|
|
474
|
+
assert.match(block, /`docs\/metrics\/member-outcomes\.tsv`/);
|
|
501
475
|
|
|
502
476
|
// The DERIVED half as code, not prose: the date comes off a transcript
|
|
503
477
|
// mtime. A scraper switched to a clock read would make this header's
|
|
@@ -528,16 +502,17 @@ test("the header's `run_date` source claims still hold against the query, the sc
|
|
|
528
502
|
// writes to the day it ran.
|
|
529
503
|
|
|
530
504
|
// The floor's numbers, as the paragraph quotes them. It names figures it
|
|
531
|
-
// does not own; a threshold changed in SKILL.md
|
|
532
|
-
// quietly citing the old one.
|
|
505
|
+
// does not own; a threshold changed in SKILL.md or in the readout alone
|
|
506
|
+
// leaves this header quietly citing the old one.
|
|
533
507
|
assert.match(
|
|
534
508
|
unemphasized(RUN_TEAM),
|
|
535
509
|
phrase("there are at least ten of them across five or more distinct `run_date`s"),
|
|
536
|
-
"SKILL.md's
|
|
510
|
+
"SKILL.md's per-cell floor is no longer ten comparisons across five dates — the header quotes those numbers",
|
|
537
511
|
);
|
|
512
|
+
assert.deepEqual({ ...GATE }, { comparisons: 10, runDates: 5 }, "cell-readout.mjs's gate is no longer ten across five — the header quotes those numbers");
|
|
538
513
|
assert.match(
|
|
539
514
|
block,
|
|
540
|
-
phrase("at least ten
|
|
541
|
-
"the header no longer states the
|
|
515
|
+
phrase("at least ten comparisons across five or more"),
|
|
516
|
+
"the header no longer states the per-cell floor it attributes to the other file",
|
|
542
517
|
);
|
|
543
518
|
});
|