@feigi/fleet-ctl 3.76.5 → 3.76.7
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
package/scripts/fleet-tick.mjs
CHANGED
|
@@ -104,16 +104,20 @@
|
|
|
104
104
|
// up; a member with no schema is never checked against one.
|
|
105
105
|
// pulls `<scratch>/impl-<N>/pull.json`, pull.mjs's record of a Pull, for
|
|
106
106
|
// every head on the shortlist, `<scratch>` as for the report
|
|
107
|
-
// files
|
|
108
|
-
//
|
|
109
|
-
// ` (
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
107
|
+
// files: read before the refresh is judged, and again for the
|
|
108
|
+
// heads a refresh replaced them with. A question standing prints
|
|
109
|
+
// `PULL #<N> (Q<n> pending — pull.mjs answer <N> <qid> …)` in
|
|
110
|
+
// place of the fresh row, with ` (stale)` once the file is older
|
|
111
|
+
// than `--pull-stale-minutes`; a Pull that dispatched with the
|
|
112
|
+
// head still holding no `impl-` row prints `(dispatched, row
|
|
113
|
+
// missing — ledger.mjs row …)`. A Pull that ended in this run's
|
|
114
|
+
// session (the ticket left, or its claim was released) names no
|
|
115
|
+
// row: `pull.mjs next` skips it, `next --head` would start the
|
|
116
|
+
// ticket over, so the next head fills the slot. Nor is it counted
|
|
117
|
+
// in `unclaimed=` or toward the short-queue refresh, so a
|
|
118
|
+
// shortlist of ended heads refreshes. A record from another
|
|
119
|
+
// phase-0 session, or one unreadable or absent, is the fresh row:
|
|
120
|
+
// `pull.mjs next` starts or resumes it.
|
|
117
121
|
// session The listing of the session directory omp writes one
|
|
118
122
|
// `<member>.jsonl` transcript into per dispatched member: this
|
|
119
123
|
// run's phase-0 receipt's `session`, else `--session`; with
|
|
@@ -278,12 +282,23 @@ function pullAction(n, p) {
|
|
|
278
282
|
return `PULL #${n} — pull.mjs next --head ${n}`;
|
|
279
283
|
}
|
|
280
284
|
|
|
285
|
+
// The heads a PULL row could name: a head whose Pull ended in this run takes
|
|
286
|
+
// no slot, and the next head takes it. Order kept, so the slots fill oldest first.
|
|
287
|
+
function pullable(heads, pulls) {
|
|
288
|
+
return heads.filter((n) => pulls?.get(n)?.kind !== "ended");
|
|
289
|
+
}
|
|
290
|
+
|
|
281
291
|
// The implementer rows: one per head a free slot Pulls, else the one row
|
|
282
|
-
// saying why none is pulled.
|
|
292
|
+
// saying why none is pulled. `unclaimed=` counts the heads a PULL row could
|
|
293
|
+
// name. A refresh whose trigger text names a count carries it as `unclaimed`,
|
|
294
|
+
// and the row prints that count, not the refreshed file's: one tick never
|
|
295
|
+
// prints two different counts.
|
|
283
296
|
function implementers(s) {
|
|
284
297
|
const dropped = specsDropped(s.dropped);
|
|
298
|
+
const heads = pullable(s.heads, s.pulls);
|
|
299
|
+
const counted = s.refresh?.unclaimed ?? heads.length;
|
|
285
300
|
const one = mkRow("implementers", s.inFlight, s.bound,
|
|
286
|
-
`implementers=${s.inFlightImpl} prs=${s.inFlightPrs} unclaimed=${
|
|
301
|
+
`implementers=${s.inFlightImpl} prs=${s.inFlightPrs} unclaimed=${counted} supply=${s.supply ?? "?"}`
|
|
287
302
|
+ (s.shortlistStatus === "ok" ? "" : ` shortlist=${s.shortlistStatus}`)
|
|
288
303
|
+ (dropped ? ` ${dropped}` : ""));
|
|
289
304
|
const row = (...a) => [one(...a)];
|
|
@@ -323,9 +338,7 @@ function implementers(s) {
|
|
|
323
338
|
return s.owedCounted.length ? row(hold, { acts: true, extra: replaceStep(s.owedCounted, s.implNames) }) : row(hold);
|
|
324
339
|
}
|
|
325
340
|
|
|
326
|
-
|
|
327
|
-
const pullable = s.heads.filter((n) => s.pulls?.get(n)?.kind !== "ended");
|
|
328
|
-
if (pullable.length) return pullable.slice(0, s.bound - s.inFlight).map((n) => one(pullAction(n, s.pulls?.get(n)), { acts: true }));
|
|
341
|
+
if (heads.length) return heads.slice(0, s.bound - s.inFlight).map((n) => one(pullAction(n, s.pulls?.get(n)), { acts: true }));
|
|
329
342
|
|
|
330
343
|
// Nothing to Pull even after a refresh: no unclaimed head, or only heads
|
|
331
344
|
// whose Pull ended in this run, which `pull.mjs next` skips. `SUGGEST /triage` asks a
|
|
@@ -1138,7 +1151,9 @@ export function parseShortlist(text) {
|
|
|
1138
1151
|
};
|
|
1139
1152
|
}
|
|
1140
1153
|
|
|
1141
|
-
// Why this tick refreshes the shortlist, or null.
|
|
1154
|
+
// Why this tick refreshes the shortlist, or null. `unclaimed` counts the
|
|
1155
|
+
// heads a PULL row could name, so a head whose Pull ended in this run is not
|
|
1156
|
+
// in it. The premise-lifted
|
|
1142
1157
|
// trigger costs gh calls, so main() asks it only when these answer null.
|
|
1143
1158
|
// Nothing refreshes while the bound holds (`held`, n ≥ B, whichever HOLD row
|
|
1144
1159
|
// prints): a refresh then cannot feed a Pull, and the first tick back under
|
|
@@ -1147,10 +1162,15 @@ export function refreshWhy({ draining, held, status, entries, unclaimed: k, boun
|
|
|
1147
1162
|
if (draining !== null || held) return null;
|
|
1148
1163
|
if (status !== "ok") return `shortlist ${status}`;
|
|
1149
1164
|
if (entries === 0) return "shortlist empty";
|
|
1150
|
-
if (k < bound) return
|
|
1165
|
+
if (k < bound) return shortQueue(k, bound);
|
|
1151
1166
|
return null;
|
|
1152
1167
|
}
|
|
1153
1168
|
|
|
1169
|
+
// The short-queue trigger's text, the one trigger that names a count.
|
|
1170
|
+
function shortQueue(k, bound) {
|
|
1171
|
+
return `unclaimed ${k} < in-flight bound ${bound}`;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1154
1174
|
// How long, in minutes, a live token of each family may run before the tick
|
|
1155
1175
|
// asks the controller to look at it. Each default is that family's p99
|
|
1156
1176
|
// run time: a token past it is rare among members that finished, so a line
|
|
@@ -2046,49 +2066,58 @@ function main() {
|
|
|
2046
2066
|
if (unchecked.length && applyTierChecks(unchecked, phase0.session)) run = readRun(prs);
|
|
2047
2067
|
}
|
|
2048
2068
|
|
|
2069
|
+
// The scratch root of the report files and the Pull records alike.
|
|
2070
|
+
const scratch = reportsScratch(receipt, scratchFlag);
|
|
2071
|
+
if (scratch === null) console.error(`${NAME}: report files and Pull records not checked — no scratch in the phase-0 receipt and no --scratch`);
|
|
2072
|
+
// Each head's Pull record read, not only the heads a free slot names: a head
|
|
2073
|
+
// whose Pull ended gives its slot to the next, so the heads past the slots
|
|
2074
|
+
// can fill one. The session it is judged against is the receipt's, as
|
|
2075
|
+
// pull.mjs judges it.
|
|
2076
|
+
const runSession = receipt.read.state === "ok" && receipt.read.receipt.session !== "" ? receipt.read.receipt.session : null;
|
|
2077
|
+
const pullsOf = (hs) => scratch === null ? new Map()
|
|
2078
|
+
: readPulls(scratch.root, hs, { session: runSession, now: Date.now(), staleMinutes: pullStaleMinutes });
|
|
2079
|
+
|
|
2049
2080
|
// Pull from the current file first; refresh when it is short, absent, an
|
|
2050
2081
|
// exclusion has lifted, or a branch-named or issue one still names a ticket on it.
|
|
2051
2082
|
// A refresh that succeeds replaces the heads outright: the PULL is read off
|
|
2052
2083
|
// what shortlist.mjs just probed, never merged with the file's own heads,
|
|
2053
2084
|
// or a ticket the refresh dropped would be pulled anyway.
|
|
2054
2085
|
let heads = unclaimed(current.entries, run);
|
|
2086
|
+
let pulls = pullsOf(heads);
|
|
2055
2087
|
let supply = current.scanned;
|
|
2056
2088
|
let dropped = current.dropped;
|
|
2057
2089
|
let fresh = null;
|
|
2090
|
+
// Short means fewer heads than B that a PULL row could name: `pull.mjs next`
|
|
2091
|
+
// skips a head whose Pull ended in this run, so counting it would hold back
|
|
2092
|
+
// the refresh that finds the head to take its slot.
|
|
2093
|
+
const counted = pullable(heads, pulls).length;
|
|
2058
2094
|
// While the bound holds a refresh cannot feed a Pull, so none of its
|
|
2059
2095
|
// triggers runs: the first tick back under the bound refreshes and Pulls in
|
|
2060
2096
|
// the same tick.
|
|
2061
2097
|
const held = run.inFlight >= caps.bound;
|
|
2062
2098
|
const trigger = refreshWhy({ draining: run.draining, held, status: current.status, entries: current.entries.length,
|
|
2063
|
-
unclaimed:
|
|
2099
|
+
unclaimed: counted, bound: caps.bound })
|
|
2064
2100
|
?? (run.draining === null && !held ? liftedPremise(run.excluded, current.entries, prs) : null);
|
|
2065
2101
|
if (trigger) {
|
|
2066
2102
|
const r = refresh();
|
|
2067
2103
|
if (r.ok) {
|
|
2068
2104
|
const excludedSet = new Set(run.excluded.map((x) => x.n));
|
|
2069
2105
|
heads = unclaimed(r.entries, run);
|
|
2106
|
+
pulls = pullsOf(heads);
|
|
2070
2107
|
supply = r.scanned;
|
|
2071
2108
|
dropped = r.dropped;
|
|
2072
2109
|
fresh = {
|
|
2073
2110
|
ok: true, trigger, entries: r.entries.length,
|
|
2074
2111
|
lifted: r.entries.filter((e) => excludedSet.has(e.n)).length,
|
|
2075
2112
|
changed: r.entries.map((e) => e.n).join(",") !== current.entries.map((e) => e.n).join(","),
|
|
2113
|
+
// The count this trigger's text names, which the implementer row prints in place of the refreshed file's.
|
|
2114
|
+
...(trigger === shortQueue(counted, caps.bound) ? { unclaimed: counted } : {}),
|
|
2076
2115
|
};
|
|
2077
2116
|
} else {
|
|
2078
2117
|
fresh = { ok: false, trigger, why: r.why };
|
|
2079
2118
|
}
|
|
2080
2119
|
}
|
|
2081
2120
|
|
|
2082
|
-
// The scratch root of the report files and the Pull records alike.
|
|
2083
|
-
const scratch = reportsScratch(receipt, scratchFlag);
|
|
2084
|
-
if (scratch === null) console.error(`${NAME}: report files and Pull records not checked — no scratch in the phase-0 receipt and no --scratch`);
|
|
2085
|
-
// Each head's Pull record read, not only the heads a free slot names: a head
|
|
2086
|
-
// whose Pull ended gives its slot to the next, so the heads past the slots
|
|
2087
|
-
// can fill one. The session it is judged against is the receipt's, as
|
|
2088
|
-
// pull.mjs judges it.
|
|
2089
|
-
const runSession = receipt.read.state === "ok" && receipt.read.receipt.session !== "" ? receipt.read.receipt.session : null;
|
|
2090
|
-
const pulls = scratch === null ? new Map()
|
|
2091
|
-
: readPulls(scratch.root, heads, { session: runSession, now: Date.now(), staleMinutes: pullStaleMinutes });
|
|
2092
2121
|
const rows = reconcile({
|
|
2093
2122
|
...caps, ...run, heads, supply, dropped, shortlistStatus: current.status, refresh: fresh, mainCheckout, phase0, router, pulls,
|
|
2094
2123
|
});
|
package/scripts/tier-check.mjs
CHANGED
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
// readers — a member with no assistant turn yet still resolves off its
|
|
23
23
|
// dispatch-time `session_init` record, never treated as absent). An
|
|
24
24
|
// entry that names neither a `session` nor a `transcript` takes the
|
|
25
|
-
// `session` the phase-0 receipt `.fleet/phase0.json` records
|
|
26
|
-
//
|
|
25
|
+
// `session` the phase-0 receipt `.fleet/phase0.json` records — the one
|
|
26
|
+
// in the workspace holding `--ledger`, or the cwd's when `--ledger` is
|
|
27
|
+
// omitted, resolved the same way the default ledger is — and is exit 2
|
|
28
|
+
// naming the receipt when it records none. That session is taken
|
|
27
29
|
// as the receipt stands: whether the receipt is this run's is judged by
|
|
28
30
|
// fleet-tick.mjs's phase-0 gate, not here, and a message about it names
|
|
29
31
|
// the receipt's session where it would name `--session`.
|
|
@@ -77,14 +79,16 @@
|
|
|
77
79
|
// LIVE member may yet write one; a member settled `PR#M`, `bailed`, `blocked`
|
|
78
80
|
// or `tier-mismatch` ran, so its transcript exists and the `session` is the
|
|
79
81
|
// wrong directory; a `session` that is empty, missing or not a directory,
|
|
80
|
-
// or omitted with none in the phase-0 receipt
|
|
81
|
-
//
|
|
82
|
+
// or omitted with none in the phase-0 receipt — a `--ledger` outside any
|
|
83
|
+
// repository or in a directory that does not exist among them, since no
|
|
84
|
+
// workspace holds it and so there is no receipt to read — is exit 2 whether
|
|
85
|
+
// the member is live or settled; and a session
|
|
82
86
|
// directory that cannot be searched (EACCES) is exit 2 naming the errno,
|
|
83
87
|
// never read as holding no transcript.
|
|
84
88
|
|
|
85
89
|
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
86
90
|
import { execFileSync } from "node:child_process";
|
|
87
|
-
import { join, dirname, isAbsolute } from "node:path";
|
|
91
|
+
import { join, dirname, isAbsolute, resolve } from "node:path";
|
|
88
92
|
import { fileURLToPath } from "node:url";
|
|
89
93
|
import { isCLI } from "./is-cli.mjs";
|
|
90
94
|
import { makeDie, defineFlags } from "./arg.mjs";
|
|
@@ -328,6 +332,19 @@ function sessionLacksTranscript(repoRoot, sessionPath, member) {
|
|
|
328
332
|
// tier can still be judged.
|
|
329
333
|
const NO_TRANSCRIPT_OUTCOMES = new Set(["killed", "released"]);
|
|
330
334
|
|
|
335
|
+
// Where the phase-0 receipt is read from: the workspace the ledger in use
|
|
336
|
+
// belongs to, so a `--ledger` naming another workspace's judges that
|
|
337
|
+
// workspace's transcripts, not the cwd's. `--ledger` takes any path, not
|
|
338
|
+
// always one under a `.fleet/` — so the workspace is the one git names for
|
|
339
|
+
// the ledger's directory, the same answer ledger.mjs's own default gives
|
|
340
|
+
// from the cwd. A ledger under no repository, or in a directory that does not
|
|
341
|
+
// exist, has no workspace and so no receipt: sessionFromReceipt refuses
|
|
342
|
+
// naming why, and the whole batch is exit 2 with nothing written. Omitted,
|
|
343
|
+
// the cwd's.
|
|
344
|
+
function receiptOpts(ledgerFile) {
|
|
345
|
+
return ledgerFile ? { cwd: dirname(resolve(ledgerFile)) } : undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
331
348
|
function runLedger(ledgerFile, args, what) {
|
|
332
349
|
try {
|
|
333
350
|
return execFileSync(process.execPath, [LEDGER_SCRIPT, ...(ledgerFile ? ["--file", ledgerFile] : []), ...args], { encoding: "utf8" });
|
|
@@ -480,7 +497,7 @@ function main() {
|
|
|
480
497
|
// session directory the phase-0 receipt records — read once per
|
|
481
498
|
// batch — and refuses naming the receipt when it records none. A
|
|
482
499
|
// given one always wins.
|
|
483
|
-
const session = raw.session ?? (raw.transcript ? undefined : (receiptSession ??= sessionFromReceipt()));
|
|
500
|
+
const session = raw.session ?? (raw.transcript ? undefined : (receiptSession ??= sessionFromReceipt(receiptOpts(ledgerFile))));
|
|
484
501
|
// Said as the caller's flag only when the caller gave it.
|
|
485
502
|
const where = raw.session ? `--session ${session}` : `the phase-0 receipt's session ${session}`;
|
|
486
503
|
if (session) {
|
package/skills/run-team/SKILL.md
CHANGED
|
@@ -147,7 +147,8 @@ phase, or in any later one, asks the maintainer which tickets to take.
|
|
|
147
147
|
lifted, and when a branch-named `behind-pr` exclusion whose PR is not yet
|
|
148
148
|
MERGED or CLOSED, or a `behind-issue` exclusion whose issue is not yet
|
|
149
149
|
closed, names a ticket the file still carries, and prints
|
|
150
|
-
`REFRESHED shortlist: <n> entries; <k> lifted`.
|
|
150
|
+
`REFRESHED shortlist: <n> entries; <k> lifted`. A head whose Pull ended
|
|
151
|
+
this run is not counted. Phase 0 runs it by hand
|
|
151
152
|
once because a dead run's shortlist is stale by definition.
|
|
152
153
|
None of those triggers runs while `HOLD (in-flight <n>/<B>)` stands
|
|
153
154
|
(**Queue depth**).
|
|
@@ -249,6 +250,7 @@ unclaimed count is below the In-flight bound, when the file is missing or
|
|
|
249
250
|
empty, when an Exclusion's premise has lifted, or when a branch-named
|
|
250
251
|
`behind-pr` or a `behind-issue` Exclusion still not lifted names a ticket the
|
|
251
252
|
file carries — never while the bound holds; the open PRs come off `gh`. A
|
|
253
|
+
head whose Pull ended this run is not counted. A
|
|
252
254
|
ledger token it
|
|
253
255
|
cannot read, it refuses rather than guessing — a member counted live or gone on
|
|
254
256
|
a guess is the over- or under-dispatch it exists to prevent — so correct that
|
|
@@ -738,10 +740,11 @@ dispatched under.
|
|
|
738
740
|
Pull, a batch of one, the member just dispatched: `{member}`.
|
|
739
741
|
`member` is the AgentId the dispatch returns (`impl-<N>`). An entry that
|
|
740
742
|
names neither `session` nor `transcript` takes the session directory phase
|
|
741
|
-
0's receipt `.fleet/phase0.json`
|
|
742
|
-
|
|
743
|
-
`<session>/<member>.jsonl
|
|
744
|
-
still resolves off its dispatch-time
|
|
743
|
+
0's receipt `.fleet/phase0.json` of `--ledger`'s workspace (else the cwd's,
|
|
744
|
+
resolved as the default ledger is) records, and is exit 2 naming the receipt
|
|
745
|
+
when it records none; the check reads `<session>/<member>.jsonl`, so a
|
|
746
|
+
member with no assistant turn yet still resolves off its dispatch-time
|
|
747
|
+
record. A `session` the entry gives
|
|
745
748
|
wins over the receipt's; a caller already holding the exact transcript path
|
|
746
749
|
may give `transcript` instead. The agent type an implementer was dispatched
|
|
747
750
|
as lives only in its transcript. `resolvedModel`
|
|
@@ -2110,6 +2113,8 @@ Read the depth off the tick and never derive it by hand: act on the row it print
|
|
|
2110
2113
|
|
|
2111
2114
|
- **shortlist** — `.fleet/shortlist.json`'s entries with no `impl-` row and no
|
|
2112
2115
|
live `excluded` row: admissible, not yet pulled. The tick's `unclaimed=`.
|
|
2116
|
+
Not after a refresh at unclaimed < B (below). A head whose Pull ended this
|
|
2117
|
+
run is not counted.
|
|
2113
2118
|
- **supply** — `scanned`, the open `ready-for-agent` tickets `shortlist.mjs`'s
|
|
2114
2119
|
candidate scan returned, before any filter. Only a Pull's full read judges
|
|
2115
2120
|
decided?, so a shortlist of undecided tickets is supply that every Pull
|
|
@@ -2149,18 +2154,12 @@ Read the depth off the tick and never derive it by hand: act on the row it print
|
|
|
2149
2154
|
|
|
2150
2155
|
**The review backlog never gates, and the merge-queue depth is still no
|
|
2151
2156
|
signal.** The only gate on a Pull is the In-flight count. A deep backlog is a
|
|
2152
|
-
`DISPATCH review` on the same tick, never a reason to idle an implementer.
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
non-empty" is a *level* condition — re-derive the deficit on **every** tick,
|
|
2159
|
-
whatever woke you: a member finishing, a member bailing before implementing, a
|
|
2160
|
-
merge landing. An edge-triggered loop that only Pulls on completion stalls
|
|
2161
|
-
silently the moment the shortlist empties, because 0 implementers emit no
|
|
2162
|
-
completion event. With the shortlist empty the table below governs — refresh,
|
|
2163
|
-
and never Pull a ticket the shortlist does not hold.
|
|
2157
|
+
`DISPATCH review` on the same tick, never a reason to idle an implementer.
|
|
2158
|
+
For the merge queue, see references/pull.md.
|
|
2159
|
+
|
|
2160
|
+
**Reconcile, do not wait for an event.** With the shortlist empty the table
|
|
2161
|
+
below governs — refresh, and never Pull a ticket the shortlist does not hold.
|
|
2162
|
+
See references/pull.md.
|
|
2164
2163
|
|
|
2165
2164
|
**Do not re-derive this by hand — `fleet-tick.mjs` computes it.** One invocation
|
|
2166
2165
|
prints per-role `actual/target` and an explicit ACTION with the backlog gate
|
|
@@ -2170,7 +2169,9 @@ the whole point: a table you must remember to consult is one you will not
|
|
|
2170
2169
|
consult under a merge-side event storm, and that is how implementers reached
|
|
2171
2170
|
0/target with one approved ticket waiting and ~57 `ready-for-agent` in supply while nobody
|
|
2172
2171
|
noticed (#3). What follows stays here as the explanation of what the script decides —
|
|
2173
|
-
never as a second, hand-run copy of it.
|
|
2172
|
+
never as a second, hand-run copy of it. The table's shortlist counts no head
|
|
2173
|
+
whose Pull ended this run. A refresh at unclaimed < B leaves `unclaimed=` at
|
|
2174
|
+
that count, so `SUGGEST /triage` can print it above 0.
|
|
2174
2175
|
|
|
2175
2176
|
| shortlist (unclaimed) | implementer row prints |
|
|
2176
2177
|
|---|---|
|
|
@@ -2186,7 +2187,8 @@ awaiting to-tickets, so an empty shortlist carrying one is not "no work".
|
|
|
2186
2187
|
|
|
2187
2188
|
The refresh runs at unclaimed < B and, with every other refresh trigger, never
|
|
2188
2189
|
while the bound holds: a refresh then cannot feed a Pull, and the first tick
|
|
2189
|
-
back under the bound refreshes and Pulls in the same tick.
|
|
2190
|
+
back under the bound refreshes and Pulls in the same tick. Unclaimed there
|
|
2191
|
+
counts no head whose Pull ended this run.
|
|
2190
2192
|
|
|
2191
2193
|
Four holds outrank every row: `HOLD (draining)` once `ledger.mjs drain` has
|
|
2192
2194
|
recorded the drain, `HOLD (tier mismatch impl-<N>)` until a replacement is
|
|
@@ -435,3 +435,16 @@ raising B past that buys nothing. 8 was rejected: peak review concurrency above
|
|
|
435
435
|
the former reviewer count, for a throughput gain the backlog does not show it
|
|
436
436
|
needs. Re-derive it from the map's guard once 20 or more PRs have merged under
|
|
437
437
|
it, never from one run's rows.
|
|
438
|
+
|
|
439
|
+
A deep `ready-to-merge` queue adds no rebases per PR — each PR rebases exactly
|
|
440
|
+
once, when it becomes the candidate — and those PRs count as In flight because
|
|
441
|
+
they sit between Claim and merge, not because the queue is deep.
|
|
442
|
+
|
|
443
|
+
## Reconcile, do not wait for an event
|
|
444
|
+
|
|
445
|
+
"A slot is free and the shortlist is
|
|
446
|
+
non-empty" is a *level* condition — re-derive the deficit on **every** tick,
|
|
447
|
+
whatever woke you: a member finishing, a member bailing before implementing, a
|
|
448
|
+
merge landing. An edge-triggered loop that only Pulls on completion stalls
|
|
449
|
+
silently the moment the shortlist empties, because 0 implementers emit no
|
|
450
|
+
completion event.
|
|
@@ -8,9 +8,14 @@ The full text of each row `fleet-tick.mjs` prints and what it asks of the contro
|
|
|
8
8
|
- `REFRESHED shortlist: <n> entries; <k> lifted` — the tick refreshed the
|
|
9
9
|
shortlist itself, and its `PULL` row already reads the result. `REFRESH
|
|
10
10
|
FAILED` is yours: `shortlist.mjs` could not run, and the implementer row is
|
|
11
|
-
blind until it can.
|
|
11
|
+
blind until it can. When the refresh ran because fewer than B heads were
|
|
12
|
+
unclaimed (`unclaimed <u> < in-flight bound <B>`), a head whose Pull ended
|
|
13
|
+
this run not counted, the implementer row's `unclaimed=` prints that same
|
|
14
|
+
`<u>`, not the refreshed file's count.
|
|
12
15
|
- `SUGGEST /triage, hold idle` — nothing is admissible after the refresh.
|
|
13
|
-
Suggest, never run (**Queue depth**).
|
|
16
|
+
Suggest, never run (**Queue depth**). A head whose Pull already ended in
|
|
17
|
+
this run is not counted toward the refresh, so a shortlist of only such
|
|
18
|
+
heads triggers the refresh before this row prints.
|
|
14
19
|
- `specs-dropped=#<N> … (to-spec: User Stories heading)` at the end of the
|
|
15
20
|
implementer row's counts, whatever its ACTION — the tickets the to-spec
|
|
16
21
|
filter kept out of `supply`, read off the shortlist's `dropped`. Never
|