@feigi/fleet-ctl 3.22.0 → 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
CHANGED
|
@@ -201,8 +201,8 @@ const COVERED_MJS = {
|
|
|
201
201
|
// children (`node candidates.mjs`, `node ledger.mjs`, `sh inflight.sh`)
|
|
202
202
|
// name no git.
|
|
203
203
|
"shortlist.mjs": 2,
|
|
204
|
-
//
|
|
205
|
-
// third is below (#2485). `shortlistPath()`'s
|
|
204
|
+
// FOUR. The first two are the pair shortlist.mjs carries (#1803), the
|
|
205
|
+
// third is below (#2485), the fourth after it. `shortlistPath()`'s
|
|
206
206
|
// `--git-common-dir` probe names the `.fleet/shortlist.json` the tick PULLs
|
|
207
207
|
// from; an ambient GIT_DIR would read another repository's shortlist and
|
|
208
208
|
// name its tickets. Measured in fleet-tick.test.mjs, "an ambient GIT_DIR
|
|
@@ -216,7 +216,11 @@ const COVERED_MJS = {
|
|
|
216
216
|
// implementer's ticket is already closed, and resolves the repository the
|
|
217
217
|
// same way; measured in fleet-tick.test.mjs, "an inherited GIT_DIR cannot
|
|
218
218
|
// retarget the tier-mismatch closed-ticket probe".
|
|
219
|
-
|
|
219
|
+
// `finishedReviewPrs()`'s `gh pr view` asks whether a PR with a dangling
|
|
220
|
+
// in-flight review is already merged or closed, and resolves the repository
|
|
221
|
+
// the same way; measured in fleet-tick.test.mjs, "an inherited GIT_DIR
|
|
222
|
+
// cannot retarget the merged-PR review probe".
|
|
223
|
+
"fleet-tick.mjs": 4,
|
|
220
224
|
// ONE spawn primitive, `git(args, cwd)`, behind both of the file's git calls
|
|
221
225
|
// — `resolveMainRoot()`'s `rev-parse --git-common-dir` and `checkIgnored()`'s
|
|
222
226
|
// `check-ignore` (#1411). An ambient GIT_DIR would answer the first for
|
package/scripts/fleet-tick.mjs
CHANGED
|
@@ -39,7 +39,10 @@
|
|
|
39
39
|
// merge queue, and which PRs are owed a review. Also, for each ticket
|
|
40
40
|
// holding the implementer row on a tier mismatch, whether its issue
|
|
41
41
|
// is CLOSED (`gh issue view`, #2485): a closed ticket's mismatch
|
|
42
|
-
// holds nothing.
|
|
42
|
+
// holds nothing. And, for each in-flight `review=` token whose PR
|
|
43
|
+
// the open list does not carry, whether that PR is MERGED or
|
|
44
|
+
// CLOSED (`gh pr view`): a review cannot be running against a
|
|
45
|
+
// finished PR, so its dangling token holds no reviewer slot.
|
|
43
46
|
// main The main checkout against the run-start baseline
|
|
44
47
|
// `.fleet/main-checkout.sha`, through main-checkout.mjs (#2210).
|
|
45
48
|
// Anything but `clean` — dirty, unknown, no baseline — prints one
|
|
@@ -496,7 +499,7 @@ export function unlabelledFinishers(ledger, unqueued) {
|
|
|
496
499
|
.map((u) => ({ pr: u.pr, labelled: u.attempts.filter((a) => a.outcome === "labelled").map((a) => a.name) }));
|
|
497
500
|
}
|
|
498
501
|
|
|
499
|
-
export function deriveRun({ rows, dispatched, drain }, prs, closed = new Set()) {
|
|
502
|
+
export function deriveRun({ rows, dispatched, drain }, prs, closed = new Set(), finished = new Set()) {
|
|
500
503
|
// One entry per member name across `## Dispatched` and every row. A member
|
|
501
504
|
// settled ANYWHERE is settled: `settle` is the only writer of an outcome, and
|
|
502
505
|
// a bare copy beside it is what a whole-line `row` rewrite leaves behind.
|
|
@@ -752,11 +755,23 @@ export function deriveRun({ rows, dispatched, drain }, prs, closed = new Set())
|
|
|
752
755
|
// head that review read, and there is nothing more to review.
|
|
753
756
|
const pastPinDue = (st, head) => st !== undefined && st.pastPinHalt && !st.inFlight && st.reviewedHead !== null
|
|
754
757
|
&& !String(head).toLowerCase().startsWith(st.reviewedHead);
|
|
758
|
+
// A `review=` token with no `reviewed=` after it reads as in flight, and the
|
|
759
|
+
// ledger alone never says otherwise: a review whose own `reviewed=` write
|
|
760
|
+
// never landed stays in flight after its PR merges, holding a reviewer slot
|
|
761
|
+
// for good. `finished` is the PR numbers the caller has found MERGED or
|
|
762
|
+
// CLOSED, and a finished PR has no review running against it. A PR on the
|
|
763
|
+
// open list is open whatever `finished` says, and none is the default, so a
|
|
764
|
+
// PR nobody has asked about keeps its review live.
|
|
765
|
+
const reviewing = [...byPr.entries()].filter(([, st]) => st.inFlight);
|
|
766
|
+
const liveReviews = reviewing.filter(([n]) => open.has(n) || !finished.has(n));
|
|
755
767
|
return {
|
|
756
768
|
implLive: live("impl"),
|
|
757
769
|
fixLive: live("fix-pr"),
|
|
758
770
|
mergeBotLive: live("merge-bot"),
|
|
759
|
-
reviewsLive:
|
|
771
|
+
reviewsLive: liveReviews.length,
|
|
772
|
+
// The in-flight reviews of PRs the open list does not carry: the ones
|
|
773
|
+
// `finished` could retire, so the caller asks gh about these and no others.
|
|
774
|
+
reviewsOffList: reviewing.filter(([n]) => !open.has(n)).map(([n]) => n).sort(asc),
|
|
760
775
|
// A returned review whose survivors no review fix-applier has answered,
|
|
761
776
|
// a conflict hold no fix-applier has cleared, or a dispositions mismatch
|
|
762
777
|
// no retry has answered — on a PR still open, with no fix-applier working
|
|
@@ -824,7 +839,7 @@ export function deriveRun({ rows, dispatched, drain }, prs, closed = new Set())
|
|
|
824
839
|
// run carries no member token of its own.
|
|
825
840
|
live: [
|
|
826
841
|
...all.filter((m) => m.outcome === null).map((m) => m.name),
|
|
827
|
-
...
|
|
842
|
+
...liveReviews.map(([n]) => `review:PR#${n}`),
|
|
828
843
|
],
|
|
829
844
|
};
|
|
830
845
|
}
|
|
@@ -1106,6 +1121,31 @@ function closedTickets(mismatched) {
|
|
|
1106
1121
|
return closed;
|
|
1107
1122
|
}
|
|
1108
1123
|
|
|
1124
|
+
// The PRs, of those whose review the ledger reads as in flight yet the open
|
|
1125
|
+
// list does not carry, that gh says are MERGED or CLOSED. A review cannot be
|
|
1126
|
+
// running against a finished PR, so the ledger's dangling `review=` token must
|
|
1127
|
+
// not hold a reviewer slot for good. A probe that cannot answer — a nonzero
|
|
1128
|
+
// exit, a reply carrying no state, a state that is none of the three — is
|
|
1129
|
+
// disclosed and the review keeps its slot: an unreadable state is never read
|
|
1130
|
+
// as finished. A PR gh calls OPEN keeps it quietly.
|
|
1131
|
+
function finishedReviewPrs(offList) {
|
|
1132
|
+
const finished = new Set();
|
|
1133
|
+
for (const number of offList) {
|
|
1134
|
+
const r = spawnSync("gh", ["pr", "view", String(number), "--json", "state"], { encoding: "utf8", env: gitEnv({ GH_REPO: "" }) });
|
|
1135
|
+
if (r.error || r.status !== 0) {
|
|
1136
|
+
console.error(`${NAME}: ${failure(r, `gh pr view ${number}`)} — review on PR#${number} unconfirmed finished, its slot stands`);
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
let st = null;
|
|
1140
|
+
try { st = JSON.parse(r.stdout).state; } catch { /* no state: disclosed below */ }
|
|
1141
|
+
if (st === "MERGED" || st === "CLOSED") finished.add(number);
|
|
1142
|
+
else if (st !== "OPEN") {
|
|
1143
|
+
console.error(`${NAME}: gh pr view ${number} printed no PR state — review on PR#${number} unconfirmed finished, its slot stands`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return finished;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1109
1149
|
// shortlist.mjs, run by the tick itself (§ 6 §3). Its per-ticket stderr is
|
|
1110
1150
|
// captured and dropped rather than billed to the controller's context; only a
|
|
1111
1151
|
// failure's last line rides along.
|
|
@@ -1166,7 +1206,8 @@ function main() {
|
|
|
1166
1206
|
const ledger = readLedger();
|
|
1167
1207
|
run = deriveRun(ledger, prs);
|
|
1168
1208
|
const closed = closedTickets(run.tierMismatch);
|
|
1169
|
-
|
|
1209
|
+
const finished = finishedReviewPrs(run.reviewsOffList);
|
|
1210
|
+
if (closed.size || finished.size) run = deriveRun(ledger, prs, closed, finished);
|
|
1170
1211
|
} catch (e) {
|
|
1171
1212
|
if (e instanceof LedgerError) die(e.message);
|
|
1172
1213
|
throw e;
|
|
@@ -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"] },
|
package/skills/run-team/SKILL.md
CHANGED
|
@@ -2032,9 +2032,12 @@ is never something to assume.
|
|
|
2032
2032
|
`fix-pr-<pr#>` is one; a finisher and a CI wait are zero; and a PR holds at most
|
|
2033
2033
|
one slot at a time. In-flight reviews are derived, never remembered: a row with
|
|
2034
2034
|
`review=` and no `reviewed=` is one, which is why the token goes on before
|
|
2035
|
-
anything else.
|
|
2036
|
-
|
|
2037
|
-
|
|
2035
|
+
anything else. A review whose PR has left the open list is probed: when
|
|
2036
|
+
`gh pr view` reports that PR MERGED or CLOSED, the tick drops the review,
|
|
2037
|
+
since none runs against a finished PR, and a probe that
|
|
2038
|
+
cannot answer keeps the slot. The defaults are 2 implementers and 6 reviewers,
|
|
2039
|
+
and neither is a hard cap. The cap bounds units, not the agents a review fans
|
|
2040
|
+
out to — 1 snapshot + up to 6 specialists + 2 refuters per critical/important finding — and
|
|
2038
2041
|
how many reviews may be in flight at once, inside it, is `--max-reviews`. The
|
|
2039
2042
|
slots a review does not hold still serve fix-appliers.
|
|
2040
2043
|
|
|
@@ -3704,7 +3707,8 @@ written with `row` (**Reviewers**): `review=wf:<runId>` |
|
|
|
3704
3707
|
`review=member:review-pr-<n>` | `review=fallback:review-pr-<n>[-b]` at launch,
|
|
3705
3708
|
settled dead as `…=failed`, then `reviewed=<head>:<survived>/<refuted>/<unverified>`
|
|
3706
3709
|
when the result lands. A row with `review=` and no `reviewed=` is a review in
|
|
3707
|
-
flight, and the tick counts it against the reviewer cap
|
|
3710
|
+
flight, and the tick counts it against the reviewer cap unless gh reports its PR
|
|
3711
|
+
MERGED or CLOSED. `ci=<run-id>:<attempt>:<conclusion>`
|
|
3708
3712
|
and `held-behind:#<lower>` are row tokens the same way (Phase 3), and so is
|
|
3709
3713
|
`conflict-hold:#<pr>` — the one row token the merge bot writes itself, naming
|
|
3710
3714
|
the row's own PR (`run-merge-bot.md` step 1). It is not an Exclusion: that
|