@coreplane/switchboard 1.260.4 → 1.260.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/deploy/cloudflare/package.json +2 -2
- package/dist/assets/deploy/cloudflare-docs/package.json +1 -1
- package/dist/assets/deploy/cloudflare-memory/package.json +2 -2
- package/dist/assets/deploy/cloudflare-memory/worker.ts +222 -11
- package/dist/assets/deploy/cloudflare-resident/package.json +2 -2
- package/dist/assets/deploy/cloudflare-resident/worker.ts +124 -40
- package/dist/assets/deploy/cloudflare-sandbox/package.json +2 -2
- package/dist/assets/package-lock.json +712 -97
- package/dist/assets/package.json +4 -4
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/budgets.ts +10 -0
- package/dist/assets/src/core/coordinator/contract.ts +5 -0
- package/dist/assets/src/core/coordinator/driver.ts +155 -29
- package/dist/assets/src/core/pipelineStanding.ts +2 -0
- package/dist/assets/src/core/plane/decide.ts +19 -2
- package/dist/assets/src/core/provider.ts +380 -0
- package/dist/assets/src/core/refusal.ts +2 -1
- package/dist/assets/src/core/runEvents.ts +8 -0
- package/dist/assets/src/core/runLedger/decisions.ts +10 -1
- package/dist/assets/src/core/runLedger/types.ts +13 -1
- package/dist/assets/src/core/runRecord.ts +31 -0
- package/dist/assets/src/core/ship/contract.ts +2 -0
- package/dist/assets/src/core/ship/coordinator.ts +515 -30
- package/dist/assets/src/core/types.ts +6 -0
- package/dist/assets/src/execution/residentRebind.ts +15 -0
- package/dist/assets/src/execution/residentReuse.ts +23 -2
- package/dist/assets/web/dist/.vite/manifest.json +67 -67
- package/dist/assets/web/dist/assets/{DeliveryPage-BaemWKW8.js → DeliveryPage-BUJckSuW.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-fgKQHLaO.js → HomePage-CBQpcnaW.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-vuWiDzvz.js → PendingTurnRow-DIC86uWA.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-DXMJf8FB.js → PlanePage-CfrbMh8t.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DdC8yU_S.js → ResidentDetailPage-C-SgOUTl.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-CSM-KODv.js → ResidentsIndexPage-CiDhvXok.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DZ8huwUU.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-jo6NZxsP.js → RunRoutePage-Cp6wicYc.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-Dx1Y4sSq.js → RunsIndexPage-BPXtfwwx.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-BuB_NZyR.js → ScheduledPage-DbvP48HI.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-Cn96jB6Q.js → SettingsPage-B87lZaiN.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-D2cJfpEq.js → SilentTurn-CPED_XCV.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-DDHUCPVJ.js → StatusDot-CFLn-DZ2.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-B4VIHkAI.js → Tooltip-Dx9YBVEx.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-C-2DfjLI.js → UnitRoutePage-d4A2X_o7.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-BQgcCyvb.js +1 -0
- package/dist/assets/web/dist/assets/{dist-Cf1CmCSK.js → dist-Cn51vWpw.js} +11 -11
- package/dist/assets/web/dist/assets/{indexRow-Dxg7C8s9.js → indexRow-BsWcHHVh.js} +1 -1
- package/dist/assets/web/dist/assets/{main-DxPqaFu9.js → main-DYwRHh0F.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-BzoeVxZB.js → sseReplay-DGRDRLua.js} +1 -1
- package/dist/cli.js +2550 -924
- package/package.json +4 -4
- package/dist/assets/web/dist/assets/RunFoldRow-Csik3wD6.js +0 -1
- package/dist/assets/web/dist/assets/budgets-KNNkT4PZ.js +0 -1
|
@@ -550,7 +550,11 @@ export type CoordinatorAction =
|
|
|
550
550
|
| { type: "branch"; step: string; branch: string; from: string }
|
|
551
551
|
| { type: "spawn"; step: string; preset: ChildPreset; round: RoundRef; budgetMinutes: number; brief: Brief }
|
|
552
552
|
| { type: "wait"; step: string; runId: string; timeoutMs: number }
|
|
553
|
-
| { type: "read-record"; step: string; runId: string }
|
|
553
|
+
| { type: "read-record"; step: string; runId: string; finishedObserved?: true }
|
|
554
|
+
/** Tell a child whose pull request became terminal or moved to end at its
|
|
555
|
+
* next turn boundary without publishing anything. This is a steer through
|
|
556
|
+
* the child's inbox, never a stop that tears down its workspace. */
|
|
557
|
+
| { type: "steer"; step: string; runId: string; reason: ChildSupersession["reason"] }
|
|
554
558
|
| {
|
|
555
559
|
type: "pr-check";
|
|
556
560
|
step: string;
|
|
@@ -663,6 +667,10 @@ export type PrCheck =
|
|
|
663
667
|
prNumber: number;
|
|
664
668
|
url: string;
|
|
665
669
|
headSha?: string;
|
|
670
|
+
/** Whether the same-repository head ref still exists. False is decisive:
|
|
671
|
+
* no child may be dispatched onto a deleted branch. Undefined means the
|
|
672
|
+
* ref could not be asked, so the machine makes no claim from it. */
|
|
673
|
+
headBranchExists?: boolean;
|
|
666
674
|
/** The unit branch's own tip, read beside the listing on the entry check
|
|
667
675
|
* (issue 1689): the listing's `headSha` can lag a force-push, so the
|
|
668
676
|
* machine resumes at review only when the pull request heads the
|
|
@@ -696,7 +704,17 @@ export type PrCheck =
|
|
|
696
704
|
commentId: string;
|
|
697
705
|
};
|
|
698
706
|
}
|
|
699
|
-
| { state: "merged"; prNumber: number; url: string; sha: string; mergedAt: string }
|
|
707
|
+
| { state: "merged"; prNumber: number; url: string; sha: string; mergedAt: string; mergedBy?: string }
|
|
708
|
+
| { state: "closed"; prNumber: number; url: string; closedBy: string };
|
|
709
|
+
|
|
710
|
+
/** Why a live child is being drained before the unit moves on. The pull
|
|
711
|
+
* request fact is the fresh read that decided it; a late child result remains
|
|
712
|
+
* on that child's record but is never folded back into the unit. */
|
|
713
|
+
type ChildSupersession =
|
|
714
|
+
| { reason: "merged"; pullRequest: Extract<PrCheck, { state: "merged" }> }
|
|
715
|
+
| { reason: "closed"; pullRequest: Extract<PrCheck, { state: "closed" }> }
|
|
716
|
+
| { reason: "head_moved"; pullRequest: Extract<PrCheck, { state: "open" }> }
|
|
717
|
+
| { reason: "branch_deleted"; pullRequest: Extract<PrCheck, { state: "open" }> };
|
|
700
718
|
|
|
701
719
|
/** What a step answered. Every bot answer carries `at`, the bot's clock — the machine's time. */
|
|
702
720
|
export type StepReturn =
|
|
@@ -716,12 +734,32 @@ export type StepReturn =
|
|
|
716
734
|
/** `restartedAs`: the interrupted child restarted from its request as this
|
|
717
735
|
* run (issue 1903 — a replaced container's child resumes by itself), so the
|
|
718
736
|
* machine keeps waiting on the successor instead of ending the unit. */
|
|
719
|
-
| {
|
|
737
|
+
| {
|
|
738
|
+
type: "read-record";
|
|
739
|
+
step: string;
|
|
740
|
+
run: ChildFacts;
|
|
741
|
+
/** The unit's pull request read fresh in the same bot transition. */
|
|
742
|
+
pullRequest?: PrCheck;
|
|
743
|
+
stopped?: true;
|
|
744
|
+
restartedAs?: string;
|
|
745
|
+
at: number;
|
|
746
|
+
}
|
|
747
|
+
| { type: "steer"; step: string; outcome: "steered" | "not_live"; at: number }
|
|
720
748
|
| { type: "pr-check"; step: string; pr: PrCheck; at: number }
|
|
721
749
|
| { type: "merge"; step: string; outcome: "merged"; sha: string; at: number }
|
|
750
|
+
| { type: "merge"; step: string; outcome: "recheck"; pullRequest: PrCheck; at: number }
|
|
722
751
|
// The door found the pull request already merged after the approval — auto-merge
|
|
723
752
|
// fired, or a person merged — so the runner merged nothing (`by: other`).
|
|
724
|
-
| {
|
|
753
|
+
| {
|
|
754
|
+
type: "merge";
|
|
755
|
+
step: string;
|
|
756
|
+
outcome: "merged";
|
|
757
|
+
by: "other";
|
|
758
|
+
sha: string;
|
|
759
|
+
mergedAt: string;
|
|
760
|
+
mergedBy?: string;
|
|
761
|
+
at: number;
|
|
762
|
+
}
|
|
725
763
|
/** The door enqueued the pull request (or found it still queued), or the
|
|
726
764
|
* queue removed it — the reason is the queue's own (issue 2011). */
|
|
727
765
|
| {
|
|
@@ -741,6 +779,8 @@ export type StepReturn =
|
|
|
741
779
|
type: "checks";
|
|
742
780
|
step: string;
|
|
743
781
|
checks?: RoundChecks;
|
|
782
|
+
/** The pull request read in the same checks transition. */
|
|
783
|
+
pullRequest?: PrCheck;
|
|
744
784
|
draft?: boolean;
|
|
745
785
|
retried?: boolean;
|
|
746
786
|
refired?: boolean;
|
|
@@ -822,7 +862,16 @@ export const checkFindingsOf = (findings: readonly Finding[] | undefined): Findi
|
|
|
822
862
|
* authorize stage never started. */
|
|
823
863
|
export type UnitEnding =
|
|
824
864
|
| { kind: "merged"; by: "runner"; pr: PrRef; sha: string; reviewRounds: number }
|
|
825
|
-
| {
|
|
865
|
+
| {
|
|
866
|
+
kind: "merged";
|
|
867
|
+
by: "other";
|
|
868
|
+
pr: PrRef;
|
|
869
|
+
sha: string;
|
|
870
|
+
mergedAt: string;
|
|
871
|
+
mergedBy?: string;
|
|
872
|
+
reviewRounds: number;
|
|
873
|
+
}
|
|
874
|
+
| { kind: "closed"; pr: PrRef; closedBy: string; reviewRounds: number }
|
|
826
875
|
/** Round 0 found the unit's scope already on the base (agent-ship item 12):
|
|
827
876
|
* the coding child's handoff names where it landed and the branch has no
|
|
828
877
|
* commits over the base, so there is no pull request to open or review.
|
|
@@ -986,11 +1035,12 @@ export type UnitEnding =
|
|
|
986
1035
|
* configured idle policy; a human-gated hold takes `parkHumanGate` instead,
|
|
987
1036
|
* which always creates the pending idle question. */
|
|
988
1037
|
export type IdleWhy =
|
|
989
|
-
Exclude<UnitEnding["kind"], "idle" | "merged" | "already_landed" | "merge_ready" | "refused"> | RenewalWhy;
|
|
1038
|
+
Exclude<UnitEnding["kind"], "idle" | "merged" | "closed" | "already_landed" | "merge_ready" | "refused"> | RenewalWhy;
|
|
990
1039
|
|
|
991
1040
|
const NEVER_IDLES: ReadonlySet<UnitEnding["kind"]> = new Set([
|
|
992
1041
|
"idle",
|
|
993
1042
|
"merged",
|
|
1043
|
+
"closed",
|
|
994
1044
|
"already_landed",
|
|
995
1045
|
"merge_ready",
|
|
996
1046
|
"refused",
|
|
@@ -1120,7 +1170,38 @@ type Phase =
|
|
|
1120
1170
|
| { at: "busy-wait"; round: RoundRef; runId?: string; n: number }
|
|
1121
1171
|
/** `until`: when the child's budget plus the margin runs out, counted from the spawn's answer — the wait's last slice ends there. */
|
|
1122
1172
|
| { at: "wait"; round: RoundRef; runId: string; n: number; until: number }
|
|
1123
|
-
| { at: "read"; round: RoundRef; runId: string; n: number; until: number }
|
|
1173
|
+
| { at: "read"; round: RoundRef; runId: string; n: number; until: number; finishedObserved: boolean }
|
|
1174
|
+
/** A live child lost authority because its pull request became terminal,
|
|
1175
|
+
* its reviewed head moved, or its branch disappeared. Steer once, then
|
|
1176
|
+
* drain the child to its recorded end while ignoring every late artifact. */
|
|
1177
|
+
| {
|
|
1178
|
+
at: "steer";
|
|
1179
|
+
round: RoundRef;
|
|
1180
|
+
runId: string;
|
|
1181
|
+
n: number;
|
|
1182
|
+
until: number;
|
|
1183
|
+
supersession: ChildSupersession;
|
|
1184
|
+
}
|
|
1185
|
+
| {
|
|
1186
|
+
at: "superseded-wait";
|
|
1187
|
+
round: RoundRef;
|
|
1188
|
+
runId: string;
|
|
1189
|
+
n: number;
|
|
1190
|
+
until: number;
|
|
1191
|
+
supersession: ChildSupersession;
|
|
1192
|
+
}
|
|
1193
|
+
| {
|
|
1194
|
+
at: "superseded-read";
|
|
1195
|
+
round: RoundRef;
|
|
1196
|
+
runId: string;
|
|
1197
|
+
n: number;
|
|
1198
|
+
until: number;
|
|
1199
|
+
supersession: ChildSupersession;
|
|
1200
|
+
finishedObserved: boolean;
|
|
1201
|
+
}
|
|
1202
|
+
/** A moved-head child has drained. Re-read the adopted pull request before
|
|
1203
|
+
* restarting review because it may have merged, closed or moved again. */
|
|
1204
|
+
| { at: "superseded-pr-check"; round: RoundRef }
|
|
1124
1205
|
| {
|
|
1125
1206
|
at: "pr-check";
|
|
1126
1207
|
round: RoundRef;
|
|
@@ -1131,6 +1212,10 @@ type Phase =
|
|
|
1131
1212
|
childPushed?: PushedHeadFact[];
|
|
1132
1213
|
childLeaseStartedAt?: number;
|
|
1133
1214
|
childHandoff?: Handoff;
|
|
1215
|
+
/** Open or recover a pull request from the branch. A completed child
|
|
1216
|
+
* whose same-head salvage merely republished ready work needs this path
|
|
1217
|
+
* without being classified as dead. */
|
|
1218
|
+
recover?: true;
|
|
1134
1219
|
/** The coding child died (`failed` or `interrupted`) after it may have
|
|
1135
1220
|
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
1136
1221
|
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
@@ -1148,6 +1233,9 @@ type Phase =
|
|
|
1148
1233
|
* queue's outcome instead of attempting the squash again. */
|
|
1149
1234
|
| { at: "merge"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number; queued?: true }
|
|
1150
1235
|
| { at: "rebase"; pr: PrRef; headSha: string; n: number }
|
|
1236
|
+
/** A rebase can run long enough for the pull request to change again. Both
|
|
1237
|
+
* dispatching outcomes stop here for one fresh state/head/ref read. */
|
|
1238
|
+
| { at: "rebase-pr-check"; pr: PrRef; n: number; outcome: "conflict" | "changed" }
|
|
1151
1239
|
| { at: "merge-wait"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number; queued?: true }
|
|
1152
1240
|
/** The round's checks step (record 0055): the check runs at the reviewed head
|
|
1153
1241
|
* are read after an approve settles, before merge_ready or the merge door.
|
|
@@ -1196,6 +1284,9 @@ export interface UnitPipelineState {
|
|
|
1196
1284
|
/** Approved-head rebases attempted in this run. Each fresh conflict may buy
|
|
1197
1285
|
* one fix round; the run's remaining lease, not pull-request lifetime, is the bound. */
|
|
1198
1286
|
readonly rebaseAttempts: number;
|
|
1287
|
+
/** Review dispatches superseded by an external head move. The count gives
|
|
1288
|
+
* each restarted round a fresh durable `/a<n>` step identity. */
|
|
1289
|
+
readonly reviewRestarts: number;
|
|
1199
1290
|
readonly pr?: PrRef;
|
|
1200
1291
|
readonly lastReviewHead?: string;
|
|
1201
1292
|
readonly lastVerdictSummary?: string;
|
|
@@ -1234,6 +1325,10 @@ export const WAIT_MARGIN_MS = SHIP_WAIT.marginMinutes * MIN;
|
|
|
1234
1325
|
* margin and the merge poll are the same number), and it keeps a round to a
|
|
1235
1326
|
* few steps: a coding child's 90 minutes are eighteen waits and eighteen reads. */
|
|
1236
1327
|
export const WAIT_CHUNK_MS = SHIP_WAIT.chunkMinutes * MIN;
|
|
1328
|
+
/** A shipped unit's child is polled more often than an unshipped coding round:
|
|
1329
|
+
* a person's merge/close must reach its inbox before a minutes-long review or
|
|
1330
|
+
* fix can publish against a terminal pull request. */
|
|
1331
|
+
export const PR_TRANSITION_GUARD_MS = MIN;
|
|
1237
1332
|
/** The merge wait is a round of its own: its minutes are carved from the
|
|
1238
1333
|
* pipeline's remainder when the door is first asked (the merge wait's ask and
|
|
1239
1334
|
* floor are rows of `src/core/budgets.ts`), and the wait below is sliced
|
|
@@ -1257,6 +1352,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
1257
1352
|
phase: { at: "pre-check" },
|
|
1258
1353
|
reviewRounds: pending?.round ?? 0,
|
|
1259
1354
|
rebaseAttempts: 0,
|
|
1355
|
+
reviewRestarts: 0,
|
|
1260
1356
|
spentMs: { coding: 0, review: 0, waiting: 0 },
|
|
1261
1357
|
spendUsd: input.session?.spendUsd ?? 0,
|
|
1262
1358
|
findingsByRound: pending ? { [pending.round]: pending.findings } : {},
|
|
@@ -1271,7 +1367,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
1271
1367
|
}
|
|
1272
1368
|
: {}),
|
|
1273
1369
|
};
|
|
1274
|
-
if (pending !== undefined) return
|
|
1370
|
+
if (pending !== undefined) return base;
|
|
1275
1371
|
if (!input.resume) return base;
|
|
1276
1372
|
const url = input.resume.url ?? `https://github.com/${input.repo}/pull/${input.resume.pr}`;
|
|
1277
1373
|
const resumed: UnitPipelineState = {
|
|
@@ -1279,7 +1375,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
1279
1375
|
pr: { number: input.resume.pr, url },
|
|
1280
1376
|
...(input.resume.headSha !== undefined ? { lastReviewHead: input.resume.headSha } : {}),
|
|
1281
1377
|
};
|
|
1282
|
-
return
|
|
1378
|
+
return resumed;
|
|
1283
1379
|
}
|
|
1284
1380
|
|
|
1285
1381
|
const deadlineAt = (s: UnitPipelineState) =>
|
|
@@ -1406,7 +1502,12 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
1406
1502
|
const p = s.phase;
|
|
1407
1503
|
switch (p.at) {
|
|
1408
1504
|
case "pre-check":
|
|
1409
|
-
return {
|
|
1505
|
+
return {
|
|
1506
|
+
type: "pr-check",
|
|
1507
|
+
step: `${unit}/pr-check`,
|
|
1508
|
+
entry: true,
|
|
1509
|
+
...(s.pr !== undefined ? { pr: s.pr.number } : {}),
|
|
1510
|
+
};
|
|
1410
1511
|
case "branch":
|
|
1411
1512
|
return { type: "branch", step: `${unit}/branch`, branch: s.input.unit.branch, from: s.input.base };
|
|
1412
1513
|
case "spawn": {
|
|
@@ -1430,15 +1531,47 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
1430
1531
|
type: "wait",
|
|
1431
1532
|
step: `${roundStep(s, p.round)}/wait/${p.n}`,
|
|
1432
1533
|
runId: p.runId,
|
|
1433
|
-
timeoutMs: waitSliceMs(s.clock, p.until),
|
|
1534
|
+
timeoutMs: Math.min(waitSliceMs(s.clock, p.until), s.pr !== undefined ? PR_TRANSITION_GUARD_MS : WAIT_CHUNK_MS),
|
|
1434
1535
|
};
|
|
1435
1536
|
case "read":
|
|
1436
|
-
return {
|
|
1537
|
+
return {
|
|
1538
|
+
type: "read-record",
|
|
1539
|
+
step: `${roundStep(s, p.round)}/read/${p.n}`,
|
|
1540
|
+
runId: p.runId,
|
|
1541
|
+
...(p.finishedObserved ? { finishedObserved: true as const } : {}),
|
|
1542
|
+
};
|
|
1543
|
+
case "steer":
|
|
1544
|
+
return {
|
|
1545
|
+
type: "steer",
|
|
1546
|
+
step: `${roundStep(s, p.round)}/steer/${p.n}`,
|
|
1547
|
+
runId: p.runId,
|
|
1548
|
+
reason: p.supersession.reason,
|
|
1549
|
+
};
|
|
1550
|
+
case "superseded-wait":
|
|
1551
|
+
return {
|
|
1552
|
+
type: "wait",
|
|
1553
|
+
step: `${roundStep(s, p.round)}/superseded/wait/${p.n}`,
|
|
1554
|
+
runId: p.runId,
|
|
1555
|
+
timeoutMs: waitSliceMs(s.clock, p.until),
|
|
1556
|
+
};
|
|
1557
|
+
case "superseded-read":
|
|
1558
|
+
return {
|
|
1559
|
+
type: "read-record",
|
|
1560
|
+
step: `${roundStep(s, p.round)}/superseded/read/${p.n}`,
|
|
1561
|
+
runId: p.runId,
|
|
1562
|
+
...(p.finishedObserved ? { finishedObserved: true as const } : {}),
|
|
1563
|
+
};
|
|
1564
|
+
case "superseded-pr-check":
|
|
1565
|
+
return {
|
|
1566
|
+
type: "pr-check",
|
|
1567
|
+
step: `${roundStep(s, p.round)}/superseded/pr-check`,
|
|
1568
|
+
...(s.pr !== undefined ? { pr: s.pr.number } : {}),
|
|
1569
|
+
};
|
|
1437
1570
|
case "pr-check":
|
|
1438
1571
|
return {
|
|
1439
1572
|
type: "pr-check",
|
|
1440
1573
|
step: `${roundStep(s, p.round)}/pr-check`,
|
|
1441
|
-
...(p.dead !== undefined ? { recover: { runId: p.runId } } : {}),
|
|
1574
|
+
...(p.dead !== undefined || p.recover === true ? { recover: { runId: p.runId } } : {}),
|
|
1442
1575
|
// The adopted pull request rides the check so the bot can follow it
|
|
1443
1576
|
// when nothing heads the unit's branch (issue 1799).
|
|
1444
1577
|
...(s.pr !== undefined ? { pr: s.pr.number } : {}),
|
|
@@ -1474,6 +1607,12 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
1474
1607
|
prNumber: p.pr.number,
|
|
1475
1608
|
headSha: p.headSha,
|
|
1476
1609
|
};
|
|
1610
|
+
case "rebase-pr-check":
|
|
1611
|
+
return {
|
|
1612
|
+
type: "pr-check",
|
|
1613
|
+
step: `${unit}/rebase/${p.n}/pr-check`,
|
|
1614
|
+
pr: p.pr.number,
|
|
1615
|
+
};
|
|
1477
1616
|
case "merge-wait":
|
|
1478
1617
|
return {
|
|
1479
1618
|
type: "wait-checks",
|
|
@@ -1522,7 +1661,10 @@ function idleEnding(s: UnitPipelineState, ending: UnitEnding): Extract<UnitEndin
|
|
|
1522
1661
|
// waits for the person's word. Human-gated questions bypass this mapping:
|
|
1523
1662
|
// `parkHumanGate` always parks them with their typed pending state.
|
|
1524
1663
|
if (ending.kind === "held" && ending.cause !== "blocked") return undefined;
|
|
1525
|
-
const old = ending as Exclude<
|
|
1664
|
+
const old = ending as Exclude<
|
|
1665
|
+
UnitEnding,
|
|
1666
|
+
{ kind: "idle" | "merged" | "closed" | "already_landed" | "merge_ready" | "refused" }
|
|
1667
|
+
>;
|
|
1526
1668
|
const grant = s.input.grant ?? DEFAULT_GRANT;
|
|
1527
1669
|
// Unspent: an idle spends no renewal — the wake's segment does (this plan's
|
|
1528
1670
|
// fifth unit) — so the row says what the grant still holds.
|
|
@@ -1688,14 +1830,37 @@ function nextReview(s: UnitPipelineState, notes: CoordinatorNote[] = []): Transi
|
|
|
1688
1830
|
const stopMode = (status: RunStatus): "soft" | "hard" | undefined =>
|
|
1689
1831
|
status === "stopped_soft" ? "soft" : status === "stopped_hard" ? "hard" : undefined;
|
|
1690
1832
|
|
|
1833
|
+
/** The last mechanical salvage push to this unit branch. */
|
|
1834
|
+
function salvagePush(s: UnitPipelineState, pushed: readonly PushedHeadFact[] | undefined): PushedHeadFact | undefined {
|
|
1835
|
+
return pushed?.filter((p) => p.ref === s.input.unit.branch && p.by === "salvage").at(-1);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/** A completed child independently witnessed at the same head and carrying its
|
|
1839
|
+
* handoff already declared the work ready; salvage merely republished its commit. */
|
|
1840
|
+
function completedSameHeadSalvage(
|
|
1841
|
+
s: UnitPipelineState,
|
|
1842
|
+
pushed: readonly PushedHeadFact[] | undefined,
|
|
1843
|
+
child?: { status: RunStatus; handoff?: boolean; headSha?: string },
|
|
1844
|
+
): boolean {
|
|
1845
|
+
if (child === undefined) return false;
|
|
1846
|
+
const found = salvagePush(s, pushed);
|
|
1847
|
+
const salvageHead = normalizeHead(found?.sha);
|
|
1848
|
+
const childHead = normalizeHead(child.headSha);
|
|
1849
|
+
return (
|
|
1850
|
+
child.status === "completed" && child.handoff === true && salvageHead !== undefined && salvageHead === childHead
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1691
1854
|
/** The last mechanical WIP push to this unit branch. Unlike an ordinary push,
|
|
1692
|
-
*
|
|
1855
|
+
* it says the child ended before its work was ready for review. */
|
|
1693
1856
|
function interruptedCheckpoint(
|
|
1694
1857
|
s: UnitPipelineState,
|
|
1695
1858
|
pushed: readonly PushedHeadFact[] | undefined,
|
|
1859
|
+
child?: { status: RunStatus; handoff?: boolean; headSha?: string },
|
|
1696
1860
|
): { branch: string; sha: string } | undefined {
|
|
1697
|
-
const found =
|
|
1698
|
-
|
|
1861
|
+
const found = salvagePush(s, pushed);
|
|
1862
|
+
if (found === undefined || completedSameHeadSalvage(s, pushed, child)) return undefined;
|
|
1863
|
+
return { branch: found.ref, sha: found.sha };
|
|
1699
1864
|
}
|
|
1700
1865
|
|
|
1701
1866
|
/** A coding run's confirmed end: round 0's child, or the run a findings step dispatched. */
|
|
@@ -1725,7 +1890,8 @@ function settleCoding(
|
|
|
1725
1890
|
}
|
|
1726
1891
|
: {}),
|
|
1727
1892
|
};
|
|
1728
|
-
const
|
|
1893
|
+
const readySalvage = completedSameHeadSalvage(next, facts.pushed, facts);
|
|
1894
|
+
const checkpoint = interruptedCheckpoint(next, facts.pushed, facts);
|
|
1729
1895
|
const mode = stopMode(facts.status);
|
|
1730
1896
|
if (mode !== undefined)
|
|
1731
1897
|
return end(
|
|
@@ -1787,6 +1953,7 @@ function settleCoding(
|
|
|
1787
1953
|
round,
|
|
1788
1954
|
runId,
|
|
1789
1955
|
...(facts.headSha !== undefined ? { childHead: facts.headSha } : {}),
|
|
1956
|
+
...(readySalvage ? { recover: true as const } : {}),
|
|
1790
1957
|
...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
|
|
1791
1958
|
...(facts.pushed !== undefined ? { childPushed: facts.pushed } : {}),
|
|
1792
1959
|
...(facts.leaseStartedAt !== undefined ? { childLeaseStartedAt: facts.leaseStartedAt } : {}),
|
|
@@ -2038,8 +2205,25 @@ function settleChecks(
|
|
|
2038
2205
|
retried?: boolean,
|
|
2039
2206
|
draft?: boolean,
|
|
2040
2207
|
refired?: boolean,
|
|
2208
|
+
pullRequest?: PrCheck,
|
|
2041
2209
|
): Transition {
|
|
2042
2210
|
const { round } = p;
|
|
2211
|
+
if (pullRequest?.state === "merged") return foundMerged(s, pullRequest);
|
|
2212
|
+
if (pullRequest?.state === "closed") return foundClosed(s, pullRequest);
|
|
2213
|
+
if (pullRequest?.state === "open" && pullRequest.headBranchExists === false) return missingHeadBranch(s, pullRequest);
|
|
2214
|
+
if (pullRequest?.state === "open") {
|
|
2215
|
+
const actual = normalizeHead(pullRequest.headSha);
|
|
2216
|
+
if (actual !== undefined && !sameCommit(actual, p.headSha)) {
|
|
2217
|
+
const reviewRestarts = s.reviewRestarts + 1;
|
|
2218
|
+
const next: UnitPipelineState = {
|
|
2219
|
+
...s,
|
|
2220
|
+
reviewRestarts,
|
|
2221
|
+
pr: { number: pullRequest.prNumber, url: pullRequest.url },
|
|
2222
|
+
lastReviewHead: actual,
|
|
2223
|
+
};
|
|
2224
|
+
return enterRound(next, { ...round, attempt: reviewRestarts + 1 });
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2043
2227
|
const wait = (over: Partial<Extract<Phase, { at: "checks-wait" }>>): Transition => ({
|
|
2044
2228
|
state: {
|
|
2045
2229
|
...s,
|
|
@@ -2208,11 +2392,118 @@ function foundMerged(
|
|
|
2208
2392
|
const ref: PrRef = { number: pr.prNumber, url: pr.url };
|
|
2209
2393
|
return end(
|
|
2210
2394
|
{ ...s, pr: ref },
|
|
2211
|
-
{
|
|
2395
|
+
{
|
|
2396
|
+
kind: "merged",
|
|
2397
|
+
by: "other",
|
|
2398
|
+
pr: ref,
|
|
2399
|
+
sha: pr.sha,
|
|
2400
|
+
mergedAt: pr.mergedAt,
|
|
2401
|
+
...(pr.mergedBy !== undefined ? { mergedBy: pr.mergedBy } : {}),
|
|
2402
|
+
reviewRounds: s.reviewRounds,
|
|
2403
|
+
},
|
|
2404
|
+
notes,
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
/** A person closed the unit's pull request without merging it. Closed is a
|
|
2409
|
+
* terminal unit state: no child, check, or merge step may follow. */
|
|
2410
|
+
function foundClosed(
|
|
2411
|
+
s: UnitPipelineState,
|
|
2412
|
+
pr: Extract<PrCheck, { state: "closed" }>,
|
|
2413
|
+
notes: CoordinatorNote[] = [],
|
|
2414
|
+
): Transition {
|
|
2415
|
+
const ref: PrRef = { number: pr.prNumber, url: pr.url };
|
|
2416
|
+
return end(
|
|
2417
|
+
{ ...s, pr: ref },
|
|
2418
|
+
{ kind: "closed", pr: ref, closedBy: pr.closedBy, reviewRounds: s.reviewRounds },
|
|
2419
|
+
notes,
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
function missingHeadBranch(
|
|
2424
|
+
s: UnitPipelineState,
|
|
2425
|
+
pr: Extract<PrCheck, { state: "open" }>,
|
|
2426
|
+
notes: CoordinatorNote[] = [],
|
|
2427
|
+
): Transition {
|
|
2428
|
+
const ref: PrRef = { number: pr.prNumber, url: pr.url };
|
|
2429
|
+
return end(
|
|
2430
|
+
{ ...s, pr: ref },
|
|
2431
|
+
{
|
|
2432
|
+
kind: "aborted",
|
|
2433
|
+
reason: `⚠️ ${pr.url} is still open, but its head branch no longer exists; no child was dispatched onto the deleted branch.`,
|
|
2434
|
+
reviewRounds: s.reviewRounds,
|
|
2435
|
+
},
|
|
2212
2436
|
notes,
|
|
2213
2437
|
);
|
|
2214
2438
|
}
|
|
2215
2439
|
|
|
2440
|
+
/** What a fresh pull-request read says about a child already in flight. A
|
|
2441
|
+
* review is pinned, so a moved head supersedes it; a coding child may itself
|
|
2442
|
+
* be moving the head and is superseded only by a terminal or deleted ref. */
|
|
2443
|
+
function childSupersession(
|
|
2444
|
+
s: UnitPipelineState,
|
|
2445
|
+
round: RoundRef,
|
|
2446
|
+
pr: PrCheck | undefined,
|
|
2447
|
+
): ChildSupersession | undefined {
|
|
2448
|
+
if (pr?.state === "merged") return { reason: "merged", pullRequest: pr };
|
|
2449
|
+
if (pr?.state === "closed") return { reason: "closed", pullRequest: pr };
|
|
2450
|
+
if (pr?.state !== "open") return undefined;
|
|
2451
|
+
if (pr.headBranchExists === false) return { reason: "branch_deleted", pullRequest: pr };
|
|
2452
|
+
if (round.kind !== "review") return undefined;
|
|
2453
|
+
const expected = normalizeHead(s.lastReviewHead);
|
|
2454
|
+
const actual = normalizeHead(pr.headSha);
|
|
2455
|
+
if (expected !== undefined && actual !== undefined && !sameCommit(expected, actual))
|
|
2456
|
+
return { reason: "head_moved", pullRequest: pr };
|
|
2457
|
+
return undefined;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
function finishSupersededChild(s: UnitPipelineState, phase: Extract<Phase, { at: "superseded-read" }>): Transition {
|
|
2461
|
+
const supersession = phase.supersession;
|
|
2462
|
+
if (supersession.reason === "merged") return foundMerged(s, supersession.pullRequest);
|
|
2463
|
+
if (supersession.reason === "closed") return foundClosed(s, supersession.pullRequest);
|
|
2464
|
+
if (supersession.reason === "branch_deleted") return missingHeadBranch(s, supersession.pullRequest);
|
|
2465
|
+
return {
|
|
2466
|
+
state: {
|
|
2467
|
+
...s,
|
|
2468
|
+
pr: { number: supersession.pullRequest.prNumber, url: supersession.pullRequest.url },
|
|
2469
|
+
phase: { at: "superseded-pr-check", round: phase.round },
|
|
2470
|
+
},
|
|
2471
|
+
notes: [],
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
function restartSupersededReview(
|
|
2476
|
+
s: UnitPipelineState,
|
|
2477
|
+
round: RoundRef,
|
|
2478
|
+
pr: Extract<PrCheck, { state: "open" }>,
|
|
2479
|
+
): Transition {
|
|
2480
|
+
if (pr.headBranchExists === false) return missingHeadBranch(s, pr);
|
|
2481
|
+
const head = normalizeHead(pr.headSha);
|
|
2482
|
+
if (head === undefined)
|
|
2483
|
+
return end(s, {
|
|
2484
|
+
kind: "aborted",
|
|
2485
|
+
reason: `⚠️ ${pr.url} moved while review was running, but its fresh head could not be read; no child was dispatched on a guessed head.`,
|
|
2486
|
+
reviewRounds: s.reviewRounds,
|
|
2487
|
+
});
|
|
2488
|
+
const reviewRestarts = s.reviewRestarts + 1;
|
|
2489
|
+
const next: UnitPipelineState = {
|
|
2490
|
+
...s,
|
|
2491
|
+
reviewRestarts,
|
|
2492
|
+
pr: { number: pr.prNumber, url: pr.url },
|
|
2493
|
+
lastReviewHead: head,
|
|
2494
|
+
};
|
|
2495
|
+
return enterRound(next, { ...round, attempt: reviewRestarts + 1 });
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
/** Reconcile the ending-time facts read. A person can merge or close after
|
|
2499
|
+
* the machine chose merge-ready but before the driver publishes it; terminal
|
|
2500
|
+
* state wins and becomes the unit's actual ending, not merely report prose. */
|
|
2501
|
+
export function reconcileTerminalPr(s: UnitPipelineState, pr: PrCheck): UnitPipelineState {
|
|
2502
|
+
if (pr.state === "merged") return foundMerged(s, pr).state;
|
|
2503
|
+
if (pr.state === "closed") return foundClosed(s, pr).state;
|
|
2504
|
+
return s;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2216
2507
|
/** The pull request heading the branch after round 0 or a findings step. */
|
|
2217
2508
|
function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-check" }>, pr: PrCheck): Transition {
|
|
2218
2509
|
const { round } = phase;
|
|
@@ -2220,6 +2511,9 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
2220
2511
|
// (or shipped into a pull request a person merged under it). The round
|
|
2221
2512
|
// completed without a pull request of its own, and the unit is done.
|
|
2222
2513
|
if (pr.state === "merged") return foundMerged(s, pr, [roundNote(round, "completed")]);
|
|
2514
|
+
if (pr.state === "closed") return foundClosed(s, pr, [roundNote(round, "completed")]);
|
|
2515
|
+
if (pr.state === "open" && pr.headBranchExists === false)
|
|
2516
|
+
return missingHeadBranch(s, pr, [roundNote(round, "aborted")]);
|
|
2223
2517
|
if (pr.state === "none") {
|
|
2224
2518
|
// A dead child left nothing on the branch to recover: the unit ends with
|
|
2225
2519
|
// the child's own reason — never the budget clip.
|
|
@@ -2516,6 +2810,8 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2516
2810
|
// by the coding child and adopted at the round's own pr-check.
|
|
2517
2811
|
const r = ret as Extract<StepReturn, { type: "pr-check" }>;
|
|
2518
2812
|
if (r.pr.state === "merged") return foundMerged(clocked, r.pr);
|
|
2813
|
+
if (r.pr.state === "closed") return foundClosed(clocked, r.pr);
|
|
2814
|
+
if (r.pr.state === "open" && r.pr.headBranchExists === false) return missingHeadBranch(clocked, r.pr);
|
|
2519
2815
|
// The open pull request still heads the branch — its head is the
|
|
2520
2816
|
// branch's own tip (the entry facts, issue 1689) or the child's own last
|
|
2521
2817
|
// push (the previous attempt ended `review_pending`, the row's
|
|
@@ -2527,6 +2823,25 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2527
2823
|
// `merge: runner`, which re-verifies the approval and the checks itself.
|
|
2528
2824
|
if (r.pr.state === "open") {
|
|
2529
2825
|
const head = normalizeHead(r.pr.headSha);
|
|
2826
|
+
const pending = s.input.session?.humanGate;
|
|
2827
|
+
if (pending !== undefined) {
|
|
2828
|
+
const adopted: UnitPipelineState = {
|
|
2829
|
+
...clocked,
|
|
2830
|
+
pr: { number: r.pr.prNumber, url: r.pr.url },
|
|
2831
|
+
...(head !== undefined ? { lastReviewHead: head } : {}),
|
|
2832
|
+
};
|
|
2833
|
+
const expected = normalizeHead(pending.headSha);
|
|
2834
|
+
if (head !== undefined && expected !== undefined && !sameCommit(head, expected)) return nextReview(adopted);
|
|
2835
|
+
return enterRound(adopted, { index: pending.round, kind: "findings" });
|
|
2836
|
+
}
|
|
2837
|
+
if (s.input.resume !== undefined) {
|
|
2838
|
+
const adopted: UnitPipelineState = {
|
|
2839
|
+
...clocked,
|
|
2840
|
+
pr: { number: r.pr.prNumber, url: r.pr.url },
|
|
2841
|
+
...(head !== undefined ? { lastReviewHead: head } : {}),
|
|
2842
|
+
};
|
|
2843
|
+
return nextReview(adopted);
|
|
2844
|
+
}
|
|
2530
2845
|
const branchHead = normalizeHead(r.pr.branchHead);
|
|
2531
2846
|
const lastPush = normalizeHead(s.input.lastPush);
|
|
2532
2847
|
const atBranchHead = head !== undefined && branchHead !== undefined && sameCommit(head, branchHead);
|
|
@@ -2652,13 +2967,56 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2652
2967
|
notes: [],
|
|
2653
2968
|
};
|
|
2654
2969
|
}
|
|
2655
|
-
case "wait":
|
|
2970
|
+
case "wait": {
|
|
2971
|
+
const r = ret as Extract<StepReturn, { type: "wait" }>;
|
|
2656
2972
|
return {
|
|
2657
|
-
state: {
|
|
2973
|
+
state: {
|
|
2974
|
+
...s,
|
|
2975
|
+
phase: {
|
|
2976
|
+
at: "read",
|
|
2977
|
+
round: p.round,
|
|
2978
|
+
runId: p.runId,
|
|
2979
|
+
n: p.n,
|
|
2980
|
+
until: p.until,
|
|
2981
|
+
finishedObserved: r.outcome === "event",
|
|
2982
|
+
},
|
|
2983
|
+
},
|
|
2658
2984
|
notes: [],
|
|
2659
2985
|
};
|
|
2986
|
+
}
|
|
2660
2987
|
case "read": {
|
|
2661
2988
|
const r = ret as Extract<StepReturn, { type: "read-record" }>;
|
|
2989
|
+
const supersession = childSupersession(clocked, p.round, r.pullRequest);
|
|
2990
|
+
if (supersession !== undefined) {
|
|
2991
|
+
// A finished child needs no steer; its verdict, dispositions or push
|
|
2992
|
+
// remain on its own record but cannot change the terminal/moved PR.
|
|
2993
|
+
if (r.run.finished) {
|
|
2994
|
+
const terminalPhase: Extract<Phase, { at: "superseded-read" }> = {
|
|
2995
|
+
at: "superseded-read",
|
|
2996
|
+
round: p.round,
|
|
2997
|
+
runId: p.runId,
|
|
2998
|
+
n: p.n,
|
|
2999
|
+
until: p.until,
|
|
3000
|
+
supersession,
|
|
3001
|
+
finishedObserved: p.finishedObserved,
|
|
3002
|
+
};
|
|
3003
|
+
return finishSupersededChild(clocked, terminalPhase);
|
|
3004
|
+
}
|
|
3005
|
+
return {
|
|
3006
|
+
state: {
|
|
3007
|
+
...clocked,
|
|
3008
|
+
phase: {
|
|
3009
|
+
at: "steer",
|
|
3010
|
+
round: p.round,
|
|
3011
|
+
runId: r.restartedAs ?? p.runId,
|
|
3012
|
+
n: p.n,
|
|
3013
|
+
until: p.until,
|
|
3014
|
+
supersession,
|
|
3015
|
+
},
|
|
3016
|
+
},
|
|
3017
|
+
notes: [],
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
2662
3020
|
if (!r.run.finished)
|
|
2663
3021
|
// `restartedAs` (issues 1903/1876): the child's container was replaced
|
|
2664
3022
|
// and it restarted from its request as a new run — the round carries on
|
|
@@ -2741,11 +3099,77 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2741
3099
|
? settleReview(clocked, p.round, r.run)
|
|
2742
3100
|
: settleCoding(clocked, p.round, p.runId, r.run);
|
|
2743
3101
|
}
|
|
3102
|
+
case "steer": {
|
|
3103
|
+
// Delivery is best effort with respect to a child that may have ended
|
|
3104
|
+
// between the fresh PR read and this effect. Either answer is followed
|
|
3105
|
+
// by a record read; a late artifact is recorded there and ignored.
|
|
3106
|
+
return {
|
|
3107
|
+
state: {
|
|
3108
|
+
...clocked,
|
|
3109
|
+
phase: {
|
|
3110
|
+
at: "superseded-wait",
|
|
3111
|
+
round: p.round,
|
|
3112
|
+
runId: p.runId,
|
|
3113
|
+
n: p.n,
|
|
3114
|
+
until: p.until,
|
|
3115
|
+
supersession: p.supersession,
|
|
3116
|
+
},
|
|
3117
|
+
},
|
|
3118
|
+
notes: [],
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
3121
|
+
case "superseded-wait": {
|
|
3122
|
+
const r = ret as Extract<StepReturn, { type: "wait" }>;
|
|
3123
|
+
return {
|
|
3124
|
+
state: {
|
|
3125
|
+
...s,
|
|
3126
|
+
phase: {
|
|
3127
|
+
at: "superseded-read",
|
|
3128
|
+
round: p.round,
|
|
3129
|
+
runId: p.runId,
|
|
3130
|
+
n: p.n,
|
|
3131
|
+
until: p.until,
|
|
3132
|
+
supersession: p.supersession,
|
|
3133
|
+
finishedObserved: r.outcome === "event",
|
|
3134
|
+
},
|
|
3135
|
+
},
|
|
3136
|
+
notes: [],
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
case "superseded-read": {
|
|
3140
|
+
const r = ret as Extract<StepReturn, { type: "read-record" }>;
|
|
3141
|
+
if (r.run.finished) return finishSupersededChild(clocked, p);
|
|
3142
|
+
return {
|
|
3143
|
+
state: {
|
|
3144
|
+
...clocked,
|
|
3145
|
+
phase: {
|
|
3146
|
+
at: "superseded-wait",
|
|
3147
|
+
round: p.round,
|
|
3148
|
+
runId: r.restartedAs ?? p.runId,
|
|
3149
|
+
n: p.n + 1,
|
|
3150
|
+
until: p.until,
|
|
3151
|
+
supersession: p.supersession,
|
|
3152
|
+
},
|
|
3153
|
+
},
|
|
3154
|
+
notes: [],
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
case "superseded-pr-check": {
|
|
3158
|
+
const r = ret as Extract<StepReturn, { type: "pr-check" }>;
|
|
3159
|
+
if (r.pr.state === "merged") return foundMerged(clocked, r.pr);
|
|
3160
|
+
if (r.pr.state === "closed") return foundClosed(clocked, r.pr);
|
|
3161
|
+
if (r.pr.state === "open") return restartSupersededReview(clocked, p.round, r.pr);
|
|
3162
|
+
return end(clocked, {
|
|
3163
|
+
kind: "aborted",
|
|
3164
|
+
reason: `⚠️ The adopted pull request disappeared after the superseded review child drained; no replacement review was dispatched.`,
|
|
3165
|
+
reviewRounds: clocked.reviewRounds,
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
2744
3168
|
case "pr-check":
|
|
2745
3169
|
return settlePrCheck(clocked, p, (ret as Extract<StepReturn, { type: "pr-check" }>).pr);
|
|
2746
3170
|
case "checks": {
|
|
2747
3171
|
const r = ret as Extract<StepReturn, { type: "checks" }>;
|
|
2748
|
-
return settleChecks(clocked, p, r.checks, r.retried, r.draft, r.refired);
|
|
3172
|
+
return settleChecks(clocked, p, r.checks, r.retried, r.draft, r.refired, r.pullRequest);
|
|
2749
3173
|
}
|
|
2750
3174
|
case "checks-wait":
|
|
2751
3175
|
// The event fired or the chunk elapsed either way the head is read again.
|
|
@@ -2769,6 +3193,35 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2769
3193
|
};
|
|
2770
3194
|
case "merge": {
|
|
2771
3195
|
const r = ret as Extract<StepReturn, { type: "merge" }>;
|
|
3196
|
+
if (r.outcome === "recheck") {
|
|
3197
|
+
if (r.pullRequest.state === "merged") return foundMerged(clocked, r.pullRequest);
|
|
3198
|
+
if (r.pullRequest.state === "closed") return foundClosed(clocked, r.pullRequest);
|
|
3199
|
+
if (r.pullRequest.state === "open" && r.pullRequest.headBranchExists === false)
|
|
3200
|
+
return missingHeadBranch(clocked, r.pullRequest);
|
|
3201
|
+
if (r.pullRequest.state === "open") {
|
|
3202
|
+
const actual = normalizeHead(r.pullRequest.headSha);
|
|
3203
|
+
if (actual !== undefined && !sameCommit(actual, p.headSha)) {
|
|
3204
|
+
const reviewRestarts = s.reviewRestarts + 1;
|
|
3205
|
+
const next: UnitPipelineState = {
|
|
3206
|
+
...clocked,
|
|
3207
|
+
reviewRestarts,
|
|
3208
|
+
pr: { number: r.pullRequest.prNumber, url: r.pullRequest.url },
|
|
3209
|
+
lastReviewHead: actual,
|
|
3210
|
+
};
|
|
3211
|
+
return enterRound(next, {
|
|
3212
|
+
index: Math.max(1, s.reviewRounds),
|
|
3213
|
+
kind: "review",
|
|
3214
|
+
attempt: reviewRestarts + 1,
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
return end(clocked, {
|
|
3219
|
+
kind: "merge_refused",
|
|
3220
|
+
pr: p.pr,
|
|
3221
|
+
reason: "the pull request changed before the merge and could not be reconciled",
|
|
3222
|
+
reviewRounds: s.reviewRounds,
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
2772
3225
|
if (r.outcome === "merged")
|
|
2773
3226
|
// Found already merged at the door — auto-merge or a person, after the
|
|
2774
3227
|
// approval: the unit is done, the runner merged nothing (`by: other`).
|
|
@@ -2779,6 +3232,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2779
3232
|
pr: p.pr,
|
|
2780
3233
|
sha: r.sha,
|
|
2781
3234
|
mergedAt: r.mergedAt,
|
|
3235
|
+
...(r.mergedBy !== undefined ? { mergedBy: r.mergedBy } : {}),
|
|
2782
3236
|
reviewRounds: s.reviewRounds,
|
|
2783
3237
|
})
|
|
2784
3238
|
: end(clocked, { kind: "merged", by: "runner", pr: p.pr, sha: r.sha, reviewRounds: s.reviewRounds });
|
|
@@ -2856,12 +3310,11 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2856
3310
|
reason: r.reason,
|
|
2857
3311
|
reviewRounds: s.reviewRounds,
|
|
2858
3312
|
});
|
|
2859
|
-
if (r.outcome === "conflict")
|
|
2860
|
-
return
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
});
|
|
3313
|
+
if (r.outcome === "conflict" || r.outcome === "changed")
|
|
3314
|
+
return {
|
|
3315
|
+
state: { ...clocked, phase: { at: "rebase-pr-check", pr: p.pr, n: p.n, outcome: r.outcome } },
|
|
3316
|
+
notes: [],
|
|
3317
|
+
};
|
|
2865
3318
|
if (!("headSha" in r))
|
|
2866
3319
|
return end(clocked, {
|
|
2867
3320
|
kind: "merge_refused",
|
|
@@ -2870,9 +3323,37 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2870
3323
|
reviewRounds: s.reviewRounds,
|
|
2871
3324
|
});
|
|
2872
3325
|
const rebased: UnitPipelineState = { ...clocked, lastReviewHead: r.headSha };
|
|
2873
|
-
if (r.outcome === "changed") return mandatoryReview(rebased);
|
|
2874
3326
|
return enterChecks(rebased, { index: s.reviewRounds, kind: "review" }, []);
|
|
2875
3327
|
}
|
|
3328
|
+
case "rebase-pr-check": {
|
|
3329
|
+
const pr = (ret as Extract<StepReturn, { type: "pr-check" }>).pr;
|
|
3330
|
+
if (pr.state === "merged") return foundMerged(clocked, pr);
|
|
3331
|
+
if (pr.state === "closed") return foundClosed(clocked, pr);
|
|
3332
|
+
if (pr.state === "open" && pr.headBranchExists === false) return missingHeadBranch(clocked, pr);
|
|
3333
|
+
if (pr.state !== "open")
|
|
3334
|
+
return end(clocked, {
|
|
3335
|
+
kind: "merge_refused",
|
|
3336
|
+
pr: p.pr,
|
|
3337
|
+
reason: "the pull request disappeared after the rebase resolver finished",
|
|
3338
|
+
reviewRounds: s.reviewRounds,
|
|
3339
|
+
});
|
|
3340
|
+
const head = normalizeHead(pr.headSha);
|
|
3341
|
+
if (pr.headBranchExists !== true || head === undefined)
|
|
3342
|
+
return end(clocked, {
|
|
3343
|
+
kind: "merge_refused",
|
|
3344
|
+
pr: { number: pr.prNumber, url: pr.url },
|
|
3345
|
+
reason: "the pull request's head branch could not be verified after the rebase resolver finished",
|
|
3346
|
+
reviewRounds: s.reviewRounds,
|
|
3347
|
+
});
|
|
3348
|
+
const refreshed: UnitPipelineState = {
|
|
3349
|
+
...clocked,
|
|
3350
|
+
pr: { number: pr.prNumber, url: pr.url },
|
|
3351
|
+
lastReviewHead: head,
|
|
3352
|
+
};
|
|
3353
|
+
return p.outcome === "conflict"
|
|
3354
|
+
? enterRound(refreshed, { index: s.reviewRounds, kind: "rebase", attempt: p.n })
|
|
3355
|
+
: mandatoryReview(refreshed);
|
|
3356
|
+
}
|
|
2876
3357
|
case "merge-wait":
|
|
2877
3358
|
return {
|
|
2878
3359
|
state: {
|
|
@@ -3067,7 +3548,9 @@ export function renderUnitReport(
|
|
|
3067
3548
|
switch (e.kind) {
|
|
3068
3549
|
case "merged":
|
|
3069
3550
|
if (e.by === "other")
|
|
3070
|
-
return
|
|
3551
|
+
return e.mergedBy !== undefined
|
|
3552
|
+
? `✅ Merged: ${e.pr.url} — merged by ${e.mergedBy} at \`${e.sha.slice(0, 7)}\` (${e.mergedAt}); the pipeline observed that merge and ran nothing further. The unit is done and its dependents start on a base that carries it.`
|
|
3553
|
+
: `✅ Already merged: ${e.pr.url} (merge commit \`${e.sha.slice(0, 7)}\`, merged ${e.mergedAt}) — the pull request heading \`${s.input.unit.branch}\` was merged before this pipeline reached it, by a person or by an earlier pipeline of this plan; the pipeline merged nothing. The unit is done and its dependents start on a base that carries it.`;
|
|
3071
3554
|
return [
|
|
3072
3555
|
`✅ Merged after ${rounds}: ${e.pr.url} (squash \`${e.sha.slice(0, 7)}\`) — merged by the pipeline under \`plan:merge\`: the review approved at this head and the guards were green.`,
|
|
3073
3556
|
aside(verdictLine),
|
|
@@ -3078,6 +3561,8 @@ export function renderUnitReport(
|
|
|
3078
3561
|
]
|
|
3079
3562
|
.filter(Boolean)
|
|
3080
3563
|
.join("\n");
|
|
3564
|
+
case "closed":
|
|
3565
|
+
return `⛔ Closed: ${e.pr.url} — closed by ${e.closedBy} without merging; the unit is terminal and no review, findings, checks or merge step follows.`;
|
|
3081
3566
|
case "already_landed":
|
|
3082
3567
|
// No compare link, no renewal line, no re-issue prompt: there was
|
|
3083
3568
|
// nothing to ship, so none of them has a question to answer.
|