@feigi/fleet-ctl 3.80.2 → 3.81.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
package/scripts/fleet-tick.mjs
CHANGED
|
@@ -51,14 +51,17 @@
|
|
|
51
51
|
// GitHub's `mergeable` — the merge queue, which PRs are owed a
|
|
52
52
|
// review, which conflict, and the PR half of the In-flight count:
|
|
53
53
|
// every open PR that closes an issue, whoever opened it and
|
|
54
|
-
// whatever its state. Also, for each
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
54
|
+
// whatever its state. Also, for each in-flight `review=` token
|
|
55
|
+
// whose PR the open list does not carry, and for each ticket's
|
|
56
|
+
// latest settled `impl-*=PR#<M>` PR the open list does not carry,
|
|
57
|
+
// that PR's state (`gh pr view`, once per PR): a review cannot
|
|
58
|
+
// be running against a finished PR, so its dangling token is not
|
|
59
|
+
// a review unit, and a PR CLOSED unmerged prints an ESCALATE line
|
|
60
|
+
// for its ticket. And, for each ticket holding the implementer
|
|
61
|
+
// row on a tier mismatch, owed a replacement, or whose keyed PR
|
|
62
|
+
// read CLOSED or unread, whether its issue is CLOSED (`gh issue
|
|
63
|
+
// view`): a closed ticket's mismatch holds nothing, its owed
|
|
64
|
+
// replacement is not in flight, and its PR prints no line.
|
|
62
65
|
// main The main checkout against the run-start baseline
|
|
63
66
|
// `.fleet/main-checkout.sha`, through main-checkout.mjs.
|
|
64
67
|
// Anything but `clean` — dirty, unknown, no baseline — prints one
|
|
@@ -146,16 +149,38 @@ export function reconcile(s) {
|
|
|
146
149
|
const rev = reviewers(s);
|
|
147
150
|
// The conflict rows only ask the controller to write a record — never to
|
|
148
151
|
// dispatch — so they stand outside the dispatch hold: the fix-applier a
|
|
149
|
-
// conflict hold leads to obeys the hold through its own role's row.
|
|
152
|
+
// conflict hold leads to obeys the hold through its own role's row. The
|
|
153
|
+
// closed-PR rows dispatch nothing either.
|
|
150
154
|
return [
|
|
151
155
|
...dispatchHold(s, [...implementers(s), ...rev.rows]),
|
|
152
156
|
...rev.conflictRows,
|
|
157
|
+
...prClosedRows(s),
|
|
153
158
|
...dispatchHold(s, [mergeBot(s)]),
|
|
154
159
|
...shortlistRows(s),
|
|
155
160
|
...routerRows(s),
|
|
156
161
|
];
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
// A ticket whose PR closed unmerged while its issue stays open: unknown
|
|
165
|
+
// intent, so the line is the maintainer's and not actionable. One line per
|
|
166
|
+
// ticket, beside the implementer row and never in it — it takes no slot and
|
|
167
|
+
// counts toward no bound. While a member on PR M is live the line names it:
|
|
168
|
+
// nothing resolves the ticket until that member settles. A state probe that
|
|
169
|
+
// could not answer prints its own line, and the next tick probes again.
|
|
170
|
+
function prClosedRows(s) {
|
|
171
|
+
const live = (k) => (k.live.length ? ` — ${k.live.join(", ")} live` : "");
|
|
172
|
+
return [
|
|
173
|
+
...s.prClosed.map((k) => ({
|
|
174
|
+
role: "tickets", action: `ESCALATE pr-closed #${k.n} PR#${k.pr}${live(k)}`, acts: false,
|
|
175
|
+
detail: `PR#${k.pr} closed unmerged and #${k.n} is open: the maintainer's — dispatch nothing for #${k.n}`,
|
|
176
|
+
})),
|
|
177
|
+
...s.prUnchecked.map((k) => ({
|
|
178
|
+
role: "tickets", action: `ESCALATE pr-unchecked #${k.n} PR#${k.pr}`, acts: false,
|
|
179
|
+
detail: `gh could not read PR#${k.pr}'s state; read again next tick`,
|
|
180
|
+
})),
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
159
184
|
// A main checkout changed since the run's baseline — or one the tick
|
|
160
185
|
// could not compare — holds every DISPATCHING row: one HOLD per role,
|
|
161
186
|
// keeping its counts, in place of whatever it would have dispatched. Placed
|
|
@@ -919,7 +944,10 @@ export function foldLedger({ rows, dispatched }, open, { lenient = false } = {})
|
|
|
919
944
|
export const pastPinDue = (st, head) => st !== undefined && st.pastPinHalt && !st.inFlight && st.reviewedHead !== null
|
|
920
945
|
&& !String(head).toLowerCase().startsWith(st.reviewedHead);
|
|
921
946
|
|
|
922
|
-
|
|
947
|
+
// The states `gh pr view --json state` answers: anything else is no answer.
|
|
948
|
+
const PR_STATES = ["OPEN", "MERGED", "CLOSED"];
|
|
949
|
+
|
|
950
|
+
export function deriveRun({ rows, dispatched, drain, retired = [] }, prs, closed = new Set(), prStates = new Map()) {
|
|
923
951
|
const open = new Set(prs.map((p) => p.number));
|
|
924
952
|
const { members, byPr, verdicts, claimed, excluded } = foldLedger({ rows, dispatched }, open);
|
|
925
953
|
// `## Retired` lines as every reader reads them: a line's name before its
|
|
@@ -951,6 +979,41 @@ export function deriveRun({ rows, dispatched, drain, retired = [] }, prs, closed
|
|
|
951
979
|
// A still-open ticket keeps its hold.
|
|
952
980
|
const mismatched = (m) => m.outcome === "tier-mismatch" || verdicts.mismatch.has(m.name);
|
|
953
981
|
const tierMismatch = newest.filter((m) => mismatched(m) && !closed.has(m.number)).map((m) => m.name);
|
|
982
|
+
// A ticket whose PR closed unmerged. Each ticket is keyed on its LATEST impl
|
|
983
|
+
// member settled `PR#<M>`, walked the way `newest` is — never on rowPr(),
|
|
984
|
+
// which stays first-wins for the per-PR fold — and left alone while an impl
|
|
985
|
+
// member after that one is live (a replacement is running) or once one
|
|
986
|
+
// after it settled `released` (the claim was released). A later `bailed`,
|
|
987
|
+
// `blocked` or `killed` leaves the key on M. So does a later
|
|
988
|
+
// `tier-mismatch`, but the ticket says nothing while the implementer row
|
|
989
|
+
// holds on that mismatch: the row owes the replacement at its tier — the
|
|
990
|
+
// maintainer's `replace` carried on — and holds every Pull until it is
|
|
991
|
+
// dispatched, so a line saying to dispatch nothing for the ticket would
|
|
992
|
+
// stall it. Once that replacement settles the walk reads it like any other.
|
|
993
|
+
// A mismatch on the keyed member itself is no replacement of M and still
|
|
994
|
+
// prints. A keyed PR the open list carries is normal flow; one it does not
|
|
995
|
+
// is named in `prKeyed` for the caller's state probe, `prStates` its
|
|
996
|
+
// answers (null for a probe that could not answer, none by default).
|
|
997
|
+
// CLOSED escalates, an unanswered probe escalates as unchecked, OPEN and
|
|
998
|
+
// MERGED say nothing, and a ticket whose issue the caller found CLOSED
|
|
999
|
+
// (`closed`) says nothing either way.
|
|
1000
|
+
const settledPr = (m) => (m.outcome?.startsWith("PR#") ? Number(m.outcome.slice("PR#".length)) : null);
|
|
1001
|
+
const supersedes = (o) => settledPr(o) !== null || o.outcome === null || o.outcome === "released";
|
|
1002
|
+
const latestPr = impls.filter((m, i) => settledPr(m) !== null
|
|
1003
|
+
&& !impls.some((o, j) => j > i && o.number === m.number && supersedes(o)));
|
|
1004
|
+
const keyed = new Set(latestPr.map((m) => m.name));
|
|
1005
|
+
const replacementHeld = new Set(newest.filter((m) => tierMismatch.includes(m.name) && !keyed.has(m.name)).map((m) => m.number));
|
|
1006
|
+
const prKeyed = latestPr.filter((m) => !replacementHeld.has(m.number)).map((m) => ({ n: m.number, pr: settledPr(m) }))
|
|
1007
|
+
.filter((k) => !open.has(k.pr)).sort((a, b) => a.n - b.n);
|
|
1008
|
+
const prSurfaced = prKeyed.filter((k) => !closed.has(k.n));
|
|
1009
|
+
// Who on PR M is live: an unsettled fix-applier or finisher bound to it, and
|
|
1010
|
+
// a live merge bot when M's latest finisher attempt settled `labelled` with
|
|
1011
|
+
// no `label-off=` after it — the ledger's reading of M in the merge queue.
|
|
1012
|
+
const queuedOnLedger = new Set(latestFinisherAttempts({ rows, dispatched }, new Set(prSurfaced.map((k) => k.pr)))
|
|
1013
|
+
.filter((u) => u.outcome === "labelled").map((u) => u.pr));
|
|
1014
|
+
const liveOn = (pr) => all.filter((m) => m.outcome === null
|
|
1015
|
+
&& (((m.family === "fix-pr" || m.family === "finisher-pr") && m.number === pr) || (m.family === "merge-bot" && queuedOnLedger.has(pr))))
|
|
1016
|
+
.map((m) => m.name).sort();
|
|
954
1017
|
const cleared = (m) => verdicts.ok.has(m.name) || verdicts.unverifiable.has(m.name);
|
|
955
1018
|
const tierUnchecked = newest.filter((m) => !mismatched(m) && !cleared(m)).map((m) => m.name);
|
|
956
1019
|
// The In-flight count: tickets between a Claim and their PR's merge. Read off
|
|
@@ -1005,12 +1068,13 @@ export function deriveRun({ rows, dispatched, drain, retired = [] }, prs, closed
|
|
|
1005
1068
|
// A `review=` token with no `reviewed=` after it reads as in flight, and the
|
|
1006
1069
|
// ledger alone never says otherwise: a review whose own `reviewed=` write
|
|
1007
1070
|
// never landed stays in flight after its PR merges, counting as a review unit
|
|
1008
|
-
// for good.
|
|
1009
|
-
//
|
|
1010
|
-
// open list is open whatever
|
|
1071
|
+
// for good. A PR the caller's probe found MERGED or CLOSED (`prStates`) is
|
|
1072
|
+
// finished, and a finished PR has no review running against it. A PR on the
|
|
1073
|
+
// open list is open whatever the probe says, and none is the default, so a
|
|
1011
1074
|
// PR nobody has asked about keeps its review live.
|
|
1012
1075
|
const reviewing = [...byPr.entries()].filter(([, st]) => st.inFlight);
|
|
1013
|
-
const
|
|
1076
|
+
const finished = (n) => prStates.get(n) === "MERGED" || prStates.get(n) === "CLOSED";
|
|
1077
|
+
const liveReviews = reviewing.filter(([n]) => open.has(n) || !finished(n));
|
|
1014
1078
|
const unsettled = all.filter((m) => m.outcome === null).map((m) => m.name);
|
|
1015
1079
|
return {
|
|
1016
1080
|
implLive: live("impl"),
|
|
@@ -1019,6 +1083,15 @@ export function deriveRun({ rows, dispatched, drain, retired = [] }, prs, closed
|
|
|
1019
1083
|
inFlightPrs: prsIn.length,
|
|
1020
1084
|
owedReplacement,
|
|
1021
1085
|
owedCounted,
|
|
1086
|
+
// Each ticket's keyed PR the open list does not carry, `{n, pr}` ascending
|
|
1087
|
+
// by ticket: the PRs the caller probes the state of.
|
|
1088
|
+
prKeyed,
|
|
1089
|
+
// The tickets whose keyed PR the probe read CLOSED, each with `live`, the
|
|
1090
|
+
// members on PR M still unsettled; and those whose probe did not answer.
|
|
1091
|
+
// Additive lines beside the implementer row: neither counts toward
|
|
1092
|
+
// anything above.
|
|
1093
|
+
prClosed: prSurfaced.filter((k) => prStates.get(k.pr) === "CLOSED").map((k) => ({ ...k, live: liveOn(k.pr) })),
|
|
1094
|
+
prUnchecked: prSurfaced.filter((k) => !PR_STATES.includes(prStates.get(k.pr))),
|
|
1022
1095
|
fixLive: live("fix-pr"),
|
|
1023
1096
|
mergeBotLive,
|
|
1024
1097
|
// The number of every merge bot settled `done`: each is a merge pass
|
|
@@ -1026,7 +1099,7 @@ export function deriveRun({ rows, dispatched, drain, retired = [] }, prs, closed
|
|
|
1026
1099
|
mergeBotDone: all.filter((m) => m.family === "merge-bot" && m.outcome === "done").map((m) => m.number).sort(asc),
|
|
1027
1100
|
reviewsLive: liveReviews.length,
|
|
1028
1101
|
// The in-flight reviews of PRs the open list does not carry: the ones
|
|
1029
|
-
//
|
|
1102
|
+
// the probe could retire, so the caller asks gh about these and no others.
|
|
1030
1103
|
reviewsOffList: reviewing.filter(([n]) => !open.has(n)).map(([n]) => n).sort(asc),
|
|
1031
1104
|
// A returned review whose survived or unverified findings no review
|
|
1032
1105
|
// fix-applier has answered — `ledger.mjs dispatch` refuses a finisher
|
|
@@ -1687,21 +1760,27 @@ function liftedPremise(excluded, entries, prs) {
|
|
|
1687
1760
|
}
|
|
1688
1761
|
|
|
1689
1762
|
// The tickets whose issue is CLOSED, of those the tick would otherwise keep
|
|
1690
|
-
// counting
|
|
1691
|
-
// mismatch,
|
|
1692
|
-
//
|
|
1693
|
-
//
|
|
1763
|
+
// counting, holding on or escalating: every ticket holding the implementer row
|
|
1764
|
+
// on a tier mismatch, every ticket owed a replacement, and every ticket whose
|
|
1765
|
+
// keyed PR the state probe read CLOSED or could not read (`surfacing`). One
|
|
1766
|
+
// live read deriveRun needs to lift a hold, uncount a ticket whose replacement
|
|
1767
|
+
// has nothing left to replace, and drop a closed-PR line for a ticket that is
|
|
1768
|
+
// done. One `gh issue view` per ticket, deduped (a tier-mismatch outcome is
|
|
1694
1769
|
// both), and none when nothing is owed. A probe that cannot answer — a nonzero
|
|
1695
1770
|
// exit, or a reply carrying no issue state — is disclosed, and the ticket
|
|
1696
|
-
// stays counted and its
|
|
1697
|
-
|
|
1771
|
+
// stays counted, its hold stands and its line prints: an unreadable state
|
|
1772
|
+
// never uncounts.
|
|
1773
|
+
function closedTickets(mismatched, owed, surfacing) {
|
|
1698
1774
|
const closed = new Set();
|
|
1699
1775
|
const mismatchedNumbers = new Set(mismatched.map((name) => parseMember(name).number));
|
|
1700
|
-
const
|
|
1776
|
+
const owedNumbers = new Set(owed.map((name) => parseMember(name).number));
|
|
1777
|
+
const numbers = new Set([...mismatchedNumbers, ...owedNumbers, ...surfacing]);
|
|
1701
1778
|
for (const number of numbers) {
|
|
1702
1779
|
const unconfirmed = mismatchedNumbers.has(number)
|
|
1703
1780
|
? `tier mismatch on #${number} unconfirmed closed, hold stands`
|
|
1704
|
-
:
|
|
1781
|
+
: owedNumbers.has(number)
|
|
1782
|
+
? `replacement owed on #${number} unconfirmed closed, ticket stays counted`
|
|
1783
|
+
: `#${number} unconfirmed closed, its closed-PR line stands`;
|
|
1705
1784
|
const r = spawnSync("gh", ["issue", "view", String(number), "--json", "state"], { encoding: "utf8", timeout: GH_TIMEOUT_MS, env: gitEnv({ GH_REPO: "" }) });
|
|
1706
1785
|
if (r.error || r.status !== 0) {
|
|
1707
1786
|
console.error(`${NAME}: ${failure(r, `gh issue view ${number}`)} — ${unconfirmed}`);
|
|
@@ -1719,29 +1798,36 @@ function closedTickets(mismatched, owed) {
|
|
|
1719
1798
|
return closed;
|
|
1720
1799
|
}
|
|
1721
1800
|
|
|
1722
|
-
// The
|
|
1723
|
-
//
|
|
1724
|
-
//
|
|
1725
|
-
//
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
function
|
|
1730
|
-
const
|
|
1731
|
-
|
|
1801
|
+
// The state of each PR the tick asks gh about, one `gh pr view --json state`
|
|
1802
|
+
// per PR however many readings need it: `reviews`, the PRs whose review the
|
|
1803
|
+
// ledger reads as in flight yet the open list does not carry, and `keyed`,
|
|
1804
|
+
// each ticket's keyed PR the open list does not carry (deriveRun's
|
|
1805
|
+
// `prKeyed`). The answer is OPEN, MERGED or CLOSED, or null for a probe that
|
|
1806
|
+
// could not answer — a nonzero exit, a reply carrying no state, a state that
|
|
1807
|
+
// is none of the three — which is disclosed with what it leaves standing.
|
|
1808
|
+
function prStates(reviews, keyed) {
|
|
1809
|
+
const states = new Map();
|
|
1810
|
+
const reviewed = new Set(reviews);
|
|
1811
|
+
// Two tickets can be keyed on one PR: each is named, on its one probe.
|
|
1812
|
+
const ticketsOf = new Map();
|
|
1813
|
+
for (const k of keyed) ticketsOf.set(k.pr, [...(ticketsOf.get(k.pr) ?? []), k.n]);
|
|
1814
|
+
for (const number of [...new Set([...reviews, ...ticketsOf.keys()])].sort((a, b) => a - b)) {
|
|
1815
|
+
const unconfirmed = [
|
|
1816
|
+
...(reviewed.has(number) ? [`review on PR#${number} unconfirmed finished, its unit stands`] : []),
|
|
1817
|
+
...(ticketsOf.get(number) ?? []).map((n) => `#${n}'s PR#${number} unconfirmed closed, printed as pr-unchecked`),
|
|
1818
|
+
].join("; ");
|
|
1819
|
+
states.set(number, null);
|
|
1732
1820
|
const r = spawnSync("gh", ["pr", "view", String(number), "--json", "state"], { encoding: "utf8", timeout: GH_TIMEOUT_MS, env: gitEnv({ GH_REPO: "" }) });
|
|
1733
1821
|
if (r.error || r.status !== 0) {
|
|
1734
|
-
console.error(`${NAME}: ${failure(r, `gh pr view ${number}`)} —
|
|
1822
|
+
console.error(`${NAME}: ${failure(r, `gh pr view ${number}`)} — ${unconfirmed}`);
|
|
1735
1823
|
continue;
|
|
1736
1824
|
}
|
|
1737
1825
|
let st = null;
|
|
1738
1826
|
try { st = JSON.parse(r.stdout).state; } catch { /* no state: disclosed below */ }
|
|
1739
|
-
if (st
|
|
1740
|
-
else
|
|
1741
|
-
console.error(`${NAME}: gh pr view ${number} printed no PR state — review on PR#${number} unconfirmed finished, its unit stands`);
|
|
1742
|
-
}
|
|
1827
|
+
if (PR_STATES.includes(st)) states.set(number, st);
|
|
1828
|
+
else console.error(`${NAME}: gh pr view ${number} printed no PR state — ${unconfirmed}`);
|
|
1743
1829
|
}
|
|
1744
|
-
return
|
|
1830
|
+
return states;
|
|
1745
1831
|
}
|
|
1746
1832
|
|
|
1747
1833
|
// shortlist.mjs, run by the tick itself. Its per-ticket stderr is
|
|
@@ -1885,15 +1971,21 @@ export function controllerState(controller, name = NAME) {
|
|
|
1885
1971
|
}
|
|
1886
1972
|
}
|
|
1887
1973
|
|
|
1888
|
-
// The run off the ledger, with the closed tickets
|
|
1889
|
-
// says the ledger cannot. A
|
|
1974
|
+
// The run off the ledger, with the closed tickets, finished reviews and keyed
|
|
1975
|
+
// PR states gh says the ledger cannot. A review cannot be running against a
|
|
1976
|
+
// MERGED or CLOSED PR, so its dangling `review=` token stops counting as a
|
|
1977
|
+
// review unit; an unread state is never read as finished. The issue probe asks
|
|
1978
|
+
// only about the tickets whose keyed PR read CLOSED or could not be read: OPEN
|
|
1979
|
+
// and MERGED print nothing whatever the issue says. A ledger it cannot read
|
|
1980
|
+
// refuses the tick.
|
|
1890
1981
|
function readRun(prs) {
|
|
1891
1982
|
try {
|
|
1892
1983
|
const ledger = readLedger();
|
|
1893
1984
|
const run = deriveRun(ledger, prs);
|
|
1894
|
-
const
|
|
1895
|
-
const
|
|
1896
|
-
|
|
1985
|
+
const states = prStates(run.reviewsOffList, run.prKeyed);
|
|
1986
|
+
const surfacing = run.prKeyed.filter((k) => states.get(k.pr) !== "OPEN" && states.get(k.pr) !== "MERGED").map((k) => k.n);
|
|
1987
|
+
const closed = closedTickets(run.tierMismatch, run.owedReplacement, surfacing);
|
|
1988
|
+
return deriveRun(ledger, prs, closed, states);
|
|
1897
1989
|
} catch (e) {
|
|
1898
1990
|
if (e instanceof LedgerError) die(e.message);
|
|
1899
1991
|
throw e;
|
package/scripts/stranded.mjs
CHANGED
|
@@ -18,7 +18,13 @@
|
|
|
18
18
|
// be read, is `none`.
|
|
19
19
|
// dead every open in-progress issue with no open PR: `resume` when
|
|
20
20
|
// exactly one worktree for it sits under this checkout's
|
|
21
|
-
// `.worktrees/` and its directory is there, `report` otherwise
|
|
21
|
+
// `.worktrees/` and its directory is there, `report` otherwise.
|
|
22
|
+
// That worktree's branch is listed first, every PR state: every
|
|
23
|
+
// one CLOSED unmerged — none OPEN, none MERGED — is `report` with
|
|
24
|
+
// `why` `PR #<M> closed unmerged` — an unmerged close is the
|
|
25
|
+
// maintainer's to rule on — and a list that does not answer, or a
|
|
26
|
+
// detached worktree whose branch cannot be asked about, is
|
|
27
|
+
// `report` too, never `resume`.
|
|
22
28
|
// ancestor this session's earlier run: no claim listed — its members may
|
|
23
29
|
// still be live, and rotate carried their tokens forward;
|
|
24
30
|
// `carried` lists them instead, for the controller to reconcile
|
|
@@ -95,6 +101,30 @@ function ghList(what, args) {
|
|
|
95
101
|
return rows;
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
// The PRs a claim's branch opened, every state: `{closed}`, the highest PR
|
|
105
|
+
// number when every one of them is CLOSED unmerged — none OPEN, none MERGED,
|
|
106
|
+
// at least one — else null; or `{failed}`, why the list did not answer. Never
|
|
107
|
+
// a refusal of the step: an unanswered list leaves this one claim unresumed,
|
|
108
|
+
// and the others classify.
|
|
109
|
+
function closedHeadPr(branch) {
|
|
110
|
+
const r = spawnSync("gh", ["pr", "list", "--state", "all", "--head", branch, "--json", "number,state", "--limit", String(LIMIT)], {
|
|
111
|
+
encoding: "utf8", maxBuffer: MAX_BUFFER, timeout: GH_TIMEOUT_MS, env: gitEnv({ GH_REPO: "" }),
|
|
112
|
+
});
|
|
113
|
+
if (r.error || r.status !== 0) return { failed: how(r) };
|
|
114
|
+
let rows;
|
|
115
|
+
try {
|
|
116
|
+
rows = JSON.parse(r.stdout);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return { failed: `unparsable: ${e.message}` };
|
|
119
|
+
}
|
|
120
|
+
if (!Array.isArray(rows) || !rows.every((p) => Number.isInteger(p?.number) && ["OPEN", "CLOSED", "MERGED"].includes(p.state))) {
|
|
121
|
+
return { failed: "not a list of {number, state}" };
|
|
122
|
+
}
|
|
123
|
+
if (rows.length >= LIMIT) return { failed: `at its cap of ${LIMIT}` };
|
|
124
|
+
const closed = rows.length > 0 && rows.every((p) => p.state === "CLOSED");
|
|
125
|
+
return { closed: closed ? Math.max(...rows.map((p) => p.number)) : null };
|
|
126
|
+
}
|
|
127
|
+
|
|
98
128
|
// Every registered worktree: its path, branch (null when detached) and
|
|
99
129
|
// whether git calls it prunable — its directory gone.
|
|
100
130
|
function worktrees(cwd) {
|
|
@@ -205,13 +235,21 @@ function main() {
|
|
|
205
235
|
const inside = mine.filter((w) => real(dirname(w.path)) === home);
|
|
206
236
|
const outside = mine.filter((w) => !inside.includes(w));
|
|
207
237
|
const one = inside.length === 1 ? inside[0] : null;
|
|
238
|
+
// A dead claim's branch that opened a PR: every one CLOSED unmerged is
|
|
239
|
+
// reported in place of the resume, and so is a branch whose PRs cannot be
|
|
240
|
+
// listed — or cannot be asked about at all, its worktree detached. A
|
|
241
|
+
// branch that never opened one resumes as before.
|
|
242
|
+
const head = prior === "dead" && one?.branch ? closedHeadPr(one.branch) : null;
|
|
208
243
|
let why;
|
|
209
244
|
if (prior !== "dead") why = "no record proves the previous run dead, so nothing is resumed";
|
|
245
|
+
else if (one && !one.branch) why = "its worktree is detached, so whether its branch opened a PR cannot be told — nothing is resumed";
|
|
246
|
+
else if (head?.failed) why = `gh pr list --head ${one.branch} did not answer (${head.failed}) — whether its branch opened a PR is unknown, so nothing is resumed`;
|
|
247
|
+
else if (head?.closed) why = `PR #${head.closed} closed unmerged`;
|
|
210
248
|
else if (one) why = "its worktree is in this checkout";
|
|
211
249
|
else if (inside.length > 1) why = `several worktrees for it in this checkout: ${inside.map((w) => w.path).join(", ")}`;
|
|
212
250
|
else if (outside.length > 0) why = `its worktree is outside this checkout's .worktrees/: ${outside.map((w) => w.path).join(", ")}`;
|
|
213
251
|
else why = "no worktree for it in this checkout";
|
|
214
|
-
const action = prior === "dead" && one && mentioned.length === 0 ? "resume" : "report";
|
|
252
|
+
const action = prior === "dead" && one?.branch && mentioned.length === 0 && !head?.failed && !head?.closed ? "resume" : "report";
|
|
215
253
|
// Not taken for its PR — but never resumed past one that might be.
|
|
216
254
|
if (mentioned.length > 0) why += `; ${mentioned.join(", ")} carries the number outside the ticket position, so check whether it is this claim's`;
|
|
217
255
|
claims.push({ n: issue.number, t: issue.title, action, worktree: one?.path ?? null, branch: one?.branch ?? null, why });
|
package/skills/run-team/SKILL.md
CHANGED
|
@@ -74,13 +74,14 @@ phase, or in any later one, asks the maintainer which tickets to take.
|
|
|
74
74
|
run `phase0.mjs` again. `RECIPE NOT PROVEN` → stall: report `STALL —
|
|
75
75
|
Recipe not proven: <cause>` to the maintainer with the deriver's reason
|
|
76
76
|
verbatim, and claim, Pull and review nothing.
|
|
77
|
-
- `NEED resume #<n> <worktree> <branch>` — a dead run's claim
|
|
77
|
+
- `NEED resume #<n> <worktree> <branch>` — a dead run's claim: no PR, or not only `CLOSED` ones.
|
|
78
78
|
Dispatch a stranded `impl-<n>-b` the ordinary way (record it with
|
|
79
79
|
`ledger.mjs row`, render its brief, `ledger.mjs dispatch`, the `task`
|
|
80
80
|
call, the tier check), in that worktree and branch, and never with
|
|
81
81
|
`claim-ticket.sh`: the claim stands. A re-run finds the need met once its
|
|
82
82
|
row is written. A `report #<n> — <why>` line is the maintainer's and
|
|
83
83
|
nothing else, and a `carried` line is a live token to reconcile first.
|
|
84
|
+
A closed-PR `report` takes `replace`/`release #<n>`.
|
|
84
85
|
- `NEED review PR#<pr> (closes no issue)` — an open PR a prior run left
|
|
85
86
|
that no row can queue. Advice, never an exit status: hand-dispatch its
|
|
86
87
|
review exactly as ticket work, or leave it.
|
|
@@ -273,6 +274,7 @@ depth** guard table applied in code. Act on each line as it reads:
|
|
|
273
274
|
- `ESCALATE unlabelled PR#<M> …` — a second finisher settled `labelled` and the label is still off: not actionable, the maintainer's.
|
|
274
275
|
- `CONFLICT PR#<M> …` — record the hold with `fleet-event.mjs conflict <M>`, never with the `ledger.mjs row` form; the tick then prints `DISPATCH fix-pr`.
|
|
275
276
|
- `ESCALATE conflict PR#<M> …` — PR M reads CONFLICTING again after two conflict fix-appliers: comment on the PR and flag it for a human, write no hold, dispatch no fix-applier.
|
|
277
|
+
- `ESCALATE pr-closed #<N> PR#<M>`, `… pr-unchecked …` — the maintainer's: tell them once per ticket per run; no dispatch or comment for #N. Reopening M or closing #N clears it. Only `replace #N` (`NEED resume`, same row) or `release #N` (closed-PR `release-ticket.sh --apply`, settle `released`) acts, once no live member names M.
|
|
276
278
|
- `HOLD (…)` — the row is held and its detail names the clearing step: dispatch nothing on that row until it clears; a tier mismatch is yours to clear by dispatching the replacement.
|
|
277
279
|
- `AT CAP`, `IDLE OK` — nothing to do on that row.
|
|
278
280
|
- `MAIN-CHECKOUT-DIRTY …` — dispatch nothing new and tell the maintainer; once they have resolved the paths, `main-checkout.mjs --record`; never revert the paths or re-baseline over them yourself.
|
|
@@ -1218,13 +1220,8 @@ Names follow the unit of work: `impl-<issue#>`, `fix-pr-<pr#>`,
|
|
|
1218
1220
|
`review-pr-<pr#>`, `finisher-pr-<pr#>`, `merge-bot-<n>`. See
|
|
1219
1221
|
references/member-lifecycle.md.
|
|
1220
1222
|
|
|
1221
|
-
**Fresh context per member.** One member, one unit of work, gone.
|
|
1222
|
-
|
|
1223
|
-
**new** member, **new** name. Sending is still right for pinging a live
|
|
1224
|
-
member for a report it owes, or resuming a truncated reply — never for
|
|
1225
|
-
handing a finished member the next ticket.
|
|
1226
|
-
|
|
1227
|
-
`hub send` to an idle peer wakes it into its old transcript the same way; re-dispatching under the same name does not reset it — omp auto-suffixes a fresh peer (`name-2`) instead.
|
|
1223
|
+
**Fresh context per member.** One member, one unit of work, gone. Refill =
|
|
1224
|
+
**new** member, **new** name.
|
|
1228
1225
|
|
|
1229
1226
|
See references/member-lifecycle.md.
|
|
1230
1227
|
|
|
@@ -389,7 +389,12 @@ token, row}]}`:
|
|
|
389
389
|
|
|
390
390
|
- `prior: dead` — every open `in-progress` issue with no open PR (one with
|
|
391
391
|
an open PR is the fold-in's): `resume` when exactly one worktree for it
|
|
392
|
-
sits under this checkout's `.worktrees/`, `report` otherwise.
|
|
392
|
+
sits under this checkout's `.worktrees/`, `report` otherwise. That
|
|
393
|
+
worktree's branch is asked `gh pr list --state all --head <branch>` first:
|
|
394
|
+
PRs that all read CLOSED unmerged — none OPEN, none MERGED — make it
|
|
395
|
+
`report` with `why` `PR #<M> closed unmerged`, and so does a list that
|
|
396
|
+
does not answer, or a detached worktree whose branch cannot be asked
|
|
397
|
+
about, never `resume`.
|
|
393
398
|
- `prior: ancestor` — this session's own earlier run: no claim is listed.
|
|
394
399
|
`carried` lists every live token rotate carried — its member or runner
|
|
395
400
|
`name` (a `review=wf:` launch's runId), the `token` as the ledger holds
|
|
@@ -428,7 +433,10 @@ impl-<N>-b …`, then `ledger.mjs dispatch <N> impl-<N>-b`, then the `task`
|
|
|
428
433
|
call naming the `agent` it printed, then the tier check — and never
|
|
429
434
|
`claim-ticket.sh`: the claim already stands. A `report` goes to the
|
|
430
435
|
maintainer as `#<N> — <why>` and nothing else: no dispatch, no label change,
|
|
431
|
-
no release.
|
|
436
|
+
no release. One whose `why` reads `PR #<M> closed unmerged` waits on the
|
|
437
|
+
maintainer's `replace #<N>` or `release #<N>`, the two words the tick's
|
|
438
|
+
`ESCALATE pr-closed` line takes (references/tick-rows.md), and on nothing
|
|
439
|
+
else. Exit 2 is a list that did not answer — a `gh` or `git`
|
|
432
440
|
failure, a list at its cap, a worktree whose presence cannot be told, or,
|
|
433
441
|
after `ancestor`, a ledger that cannot be read — and is never nothing
|
|
434
442
|
stranded.
|
|
@@ -60,6 +60,41 @@ The full text of each row `fleet-tick.mjs` prints and what it asks of the contro
|
|
|
60
60
|
and flag it for a human; write no hold and dispatch no fix-applier. Not
|
|
61
61
|
actionable; it repeats while the PR reads CONFLICTING and clears once it
|
|
62
62
|
stops, closes or merges.
|
|
63
|
+
- `ESCALATE pr-closed #<N> PR#<M>` — PR M, ticket N's latest PR (the latest
|
|
64
|
+
`impl-<N>…=PR#<M>` on the row), is CLOSED unmerged while issue #N is not
|
|
65
|
+
read CLOSED (an issue gh cannot read counts as open), and no
|
|
66
|
+
`impl-<N>-<x>` after it is live or settled `released`. An unmerged
|
|
67
|
+
close is unknown intent, so the line is the maintainer's and not
|
|
68
|
+
actionable: tell the maintainer once per ticket per run, dispatch nothing
|
|
69
|
+
for #N, and post no GitHub comment. It prints beside the implementer row,
|
|
70
|
+
never in it, takes no implementer slot and counts toward no bound. While a
|
|
71
|
+
member on PR M is live — `fix-pr-<M>…`, `finisher-pr-<M>…`, or a live
|
|
72
|
+
`merge-bot-<n>` while M's latest finisher settled `labelled` with no
|
|
73
|
+
`label-off=` after it — the line ends ` — <member> live`. Reopening PR M,
|
|
74
|
+
or closing #N, clears it by itself on the next tick. Act only on the
|
|
75
|
+
maintainer's `replace #N` or `release #N`, and only once the line names no
|
|
76
|
+
live member:
|
|
77
|
+
- `replace #N` — the `NEED resume` procedure (phase 0): `impl-<N>-b`, or
|
|
78
|
+
one letter past the highest `impl-<N>-<x>` the ticket has used, in the
|
|
79
|
+
existing worktree and branch, never `claim-ticket.sh`. Its new PR settles
|
|
80
|
+
onto the same row, and that two-PR row is a legitimate shape: the tick
|
|
81
|
+
keys the row on the newer PR from then on.
|
|
82
|
+
- `release #N` — `release-ticket.sh <N> impl-<N> implementer --apply`,
|
|
83
|
+
which runs its closed-PR mode on a branch whose PRs all read CLOSED, then
|
|
84
|
+
settle `released`: `ledger.mjs row <N> "<text> · impl-<N>-<x>"`, the next
|
|
85
|
+
letter, and `ledger.mjs settle impl-<N>-<x> released`, which ends the
|
|
86
|
+
line.
|
|
87
|
+
|
|
88
|
+
A replacement that settles `bailed`, `blocked` or `killed` brings the line
|
|
89
|
+
back on M, and one whose own PR M′ closes unmerged prints it on M′. One the
|
|
90
|
+
implementer row holds on a tier mismatch (`HOLD (tier mismatch …)`) keeps
|
|
91
|
+
the line off while that row asks for the next replacement — the `replace`
|
|
92
|
+
carried on at the right tier — and once that one settles, the line reads it
|
|
93
|
+
the same way. A tier mismatch on the member that settled `PR#<M>` itself
|
|
94
|
+
replaces nothing, so the line stands beside that hold.
|
|
95
|
+
- `ESCALATE pr-unchecked #<N> PR#<M>` — the same ticket, but `gh pr view
|
|
96
|
+
<M>` could not answer, so whether M closed is unknown. The maintainer's on
|
|
97
|
+
the same terms as `pr-closed`; the next tick probes again.
|
|
63
98
|
- `HOLD (…)` — the row is held and says why: draining, a tier mismatch, an
|
|
64
99
|
unchecked tier, the In-flight bound reached (`HOLD (in-flight <n>/<B>)` —
|
|
65
100
|
nothing to do unless its detail names a replacement to dispatch, else a
|