@coreplane/switchboard 1.260.1 → 1.260.2
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/bin/switchboard.js +15 -11
- package/dist/assets/deploy/cloudflare-memory/backgroundTasks.ts +25 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +36 -16
- package/dist/assets/deploy/cloudflare-resident/worker.ts +39 -18
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +2 -2
- package/dist/assets/src/core/budgets.ts +7 -0
- package/dist/assets/src/core/coordinator/driver.ts +38 -5
- package/dist/assets/src/core/reviewedHead.ts +16 -2
- package/dist/assets/src/core/ship/coordinator.ts +103 -8
- package/dist/cli.js +549 -149
- package/package.json +1 -1
|
@@ -458,7 +458,7 @@ export function cursorFinished(cursor: PlanCursor): boolean {
|
|
|
458
458
|
* findings dispatched into the unit thread as `agent:coding`, so the coding
|
|
459
459
|
* session there continues with them (record 0034). Never a `fix` child briefed
|
|
460
460
|
* from the review. */
|
|
461
|
-
export type RoundKind = "coding" | "review" | "findings";
|
|
461
|
+
export type RoundKind = "coding" | "review" | "findings" | "rebase";
|
|
462
462
|
export interface RoundRef {
|
|
463
463
|
/** Round 0 is the coding round; review round n and its findings step share n. */
|
|
464
464
|
index: number;
|
|
@@ -508,6 +508,13 @@ export type Brief =
|
|
|
508
508
|
/** The round's check findings (record 0055), carried by value — the
|
|
509
509
|
* review run's record holds only the reviewer's own findings. */
|
|
510
510
|
checks?: Finding[];
|
|
511
|
+
}
|
|
512
|
+
| {
|
|
513
|
+
kind: "rebase";
|
|
514
|
+
unit: string;
|
|
515
|
+
pr: number;
|
|
516
|
+
headSha: string;
|
|
517
|
+
base: string;
|
|
511
518
|
};
|
|
512
519
|
|
|
513
520
|
export interface PrRef {
|
|
@@ -549,6 +556,9 @@ export type CoordinatorAction =
|
|
|
549
556
|
* the queue's outcome (merged, still queued, or removed with the reason)
|
|
550
557
|
* instead of attempting the squash. */
|
|
551
558
|
| { type: "merge"; step: string; prNumber: number; headSha: string; queued?: true }
|
|
559
|
+
/** Rung one of an approved head's rebase: git first. A conflict alone buys
|
|
560
|
+
* the runner's coding round; a clean changed patch returns to review. */
|
|
561
|
+
| { type: "rebase"; step: string; prNumber: number; headSha: string }
|
|
552
562
|
/** Read the check runs at the reviewed head (record 0055, the round verdict):
|
|
553
563
|
* the merge door's own reading, folded into the round. `retry` names the
|
|
554
564
|
* failed checks whose one flake re-run the machine is spending; `refire`
|
|
@@ -678,7 +688,15 @@ export type StepReturn =
|
|
|
678
688
|
| { type: "merge"; step: string; outcome: "merged"; by: "other"; sha: string; mergedAt: string; at: number }
|
|
679
689
|
/** The door enqueued the pull request (or found it still queued), or the
|
|
680
690
|
* queue removed it — the reason is the queue's own (issue 2011). */
|
|
681
|
-
| {
|
|
691
|
+
| {
|
|
692
|
+
type: "merge";
|
|
693
|
+
step: string;
|
|
694
|
+
outcome: "pending" | "refused" | "conflict" | "enqueued" | "removed";
|
|
695
|
+
reason: string;
|
|
696
|
+
at: number;
|
|
697
|
+
}
|
|
698
|
+
| { type: "rebase"; step: string; outcome: "carried" | "changed"; headSha: string; at: number }
|
|
699
|
+
| { type: "rebase"; step: string; outcome: "conflict" | "refused"; reason: string; at: number }
|
|
682
700
|
/** The checks read at the reviewed head; `checks` absent means GitHub could
|
|
683
701
|
* not be read, which the machine treats as pending (record 0055). Effect
|
|
684
702
|
* asks answer `retried` or `refired` instead; false still spends that one
|
|
@@ -1090,6 +1108,7 @@ type Phase =
|
|
|
1090
1108
|
* only through a merge queue (issue 2011) — so every later ask reads the
|
|
1091
1109
|
* queue's outcome instead of attempting the squash again. */
|
|
1092
1110
|
| { at: "merge"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number; queued?: true }
|
|
1111
|
+
| { at: "rebase"; pr: PrRef; headSha: string; n: number }
|
|
1093
1112
|
| { at: "merge-wait"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number; queued?: true }
|
|
1094
1113
|
/** The round's checks step (record 0055): the check runs at the reviewed head
|
|
1095
1114
|
* are read after an approve settles, before merge_ready or the merge door.
|
|
@@ -1135,6 +1154,9 @@ export interface UnitPipelineState {
|
|
|
1135
1154
|
readonly phase: Phase;
|
|
1136
1155
|
/** Review rounds started so far. */
|
|
1137
1156
|
readonly reviewRounds: number;
|
|
1157
|
+
/** Approved-head rebases attempted in this run. Each fresh conflict may buy
|
|
1158
|
+
* one fix round; the run's remaining lease, not pull-request lifetime, is the bound. */
|
|
1159
|
+
readonly rebaseAttempts: number;
|
|
1138
1160
|
readonly pr?: PrRef;
|
|
1139
1161
|
readonly lastReviewHead?: string;
|
|
1140
1162
|
readonly lastVerdictSummary?: string;
|
|
@@ -1192,6 +1214,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
1192
1214
|
clock: at,
|
|
1193
1215
|
phase: { at: "pre-check" },
|
|
1194
1216
|
reviewRounds: 0,
|
|
1217
|
+
rebaseAttempts: 0,
|
|
1195
1218
|
spentMs: { coding: 0, review: 0, waiting: 0 },
|
|
1196
1219
|
spendUsd: input.session?.spendUsd ?? 0,
|
|
1197
1220
|
findingsByRound: {},
|
|
@@ -1230,11 +1253,8 @@ const loopOf = (s: UnitPipelineState): Loop => ({ maxRounds: s.input.caps.maxRou
|
|
|
1230
1253
|
* A review round `n` and the findings step that follows it share `n`; the
|
|
1231
1254
|
* module's positions are the loop's own. */
|
|
1232
1255
|
function roundCarve(s: UnitPipelineState, round: RoundRef): Carve {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
{ kind: round.kind, index: loopPosition(loopOf(s), round.kind, round.index) },
|
|
1236
|
-
loopOf(s),
|
|
1237
|
-
);
|
|
1256
|
+
const kind = round.kind === "rebase" ? "findings" : round.kind;
|
|
1257
|
+
return carve(remainingMs(s), { kind, index: loopPosition(loopOf(s), kind, round.index) }, loopOf(s));
|
|
1238
1258
|
}
|
|
1239
1259
|
|
|
1240
1260
|
/** The prefix every step of this pipeline is named under: the unit id, a
|
|
@@ -1270,6 +1290,9 @@ function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
|
|
|
1270
1290
|
};
|
|
1271
1291
|
}
|
|
1272
1292
|
const pr = s.pr!.number;
|
|
1293
|
+
if (round.kind === "rebase") {
|
|
1294
|
+
return { kind: "rebase", unit, pr, headSha: s.lastReviewHead!, base: s.input.base };
|
|
1295
|
+
}
|
|
1273
1296
|
if (round.kind === "findings") {
|
|
1274
1297
|
const checks = checkFindingsOf(s.findingsByRound[round.index]);
|
|
1275
1298
|
return {
|
|
@@ -1380,6 +1403,13 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
1380
1403
|
headSha: p.headSha,
|
|
1381
1404
|
...(p.queued === true ? { queued: true as const } : {}),
|
|
1382
1405
|
};
|
|
1406
|
+
case "rebase":
|
|
1407
|
+
return {
|
|
1408
|
+
type: "rebase",
|
|
1409
|
+
step: `${unit}/rebase/${p.n}`,
|
|
1410
|
+
prNumber: p.pr.number,
|
|
1411
|
+
headSha: p.headSha,
|
|
1412
|
+
};
|
|
1383
1413
|
case "merge-wait":
|
|
1384
1414
|
return {
|
|
1385
1415
|
type: "wait-checks",
|
|
@@ -1534,6 +1564,13 @@ function enterRound(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNot
|
|
|
1534
1564
|
};
|
|
1535
1565
|
}
|
|
1536
1566
|
|
|
1567
|
+
/** A rebase changed the approved patch, so review is mandatory even when
|
|
1568
|
+
* the ordinary findings-round count is spent. The run's remaining lease is
|
|
1569
|
+
* the bound: `enterRound` refuses when another review no longer fits. */
|
|
1570
|
+
function reviewAfterRebase(s: UnitPipelineState, notes: CoordinatorNote[] = []): Transition {
|
|
1571
|
+
return enterRound(s, { index: s.reviewRounds + 1, kind: "review" }, notes);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1537
1574
|
/** The next review round, or the round cap. */
|
|
1538
1575
|
function nextReview(s: UnitPipelineState, notes: CoordinatorNote[] = []): Transition {
|
|
1539
1576
|
if (s.reviewRounds >= s.input.caps.maxRounds)
|
|
@@ -1987,6 +2024,21 @@ function settleChecks(
|
|
|
1987
2024
|
}
|
|
1988
2025
|
}
|
|
1989
2026
|
|
|
2027
|
+
/** Re-enter the approved head's rebase step. The attempt count belongs to
|
|
2028
|
+
* this run, not to the pull request: every later base move is a fresh
|
|
2029
|
+
* conflict, while the one pipeline lease remains the hard bound. */
|
|
2030
|
+
export function reenterApprovedRebase(s: UnitPipelineState): UnitPipelineState {
|
|
2031
|
+
if (s.pr === undefined || s.lastReviewHead === undefined)
|
|
2032
|
+
throw new Error("an approved rebase needs the pull request and reviewed head");
|
|
2033
|
+
const { ending: _ending, ...open } = s;
|
|
2034
|
+
const n = s.rebaseAttempts + 1;
|
|
2035
|
+
return {
|
|
2036
|
+
...open,
|
|
2037
|
+
rebaseAttempts: n,
|
|
2038
|
+
phase: { at: "rebase", pr: s.pr, headSha: s.lastReviewHead, n },
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1990
2042
|
/** An approve past the checks read: merge_ready for a person, the merge door
|
|
1991
2043
|
* under `merge: runner` — the tail the round verdict guards (record 0055). */
|
|
1992
2044
|
function approveOutcome(next: UnitPipelineState, notes: CoordinatorNote[]): Transition {
|
|
@@ -2237,6 +2289,22 @@ function roundOnOpenPr(
|
|
|
2237
2289
|
const { round } = phase;
|
|
2238
2290
|
const head = pr.headSha ?? phase.childHead;
|
|
2239
2291
|
const next: UnitPipelineState = { ...s, pr: { number: pr.prNumber, url: pr.url } };
|
|
2292
|
+
if (round.kind === "rebase") {
|
|
2293
|
+
const pushed = normalizeHead(head);
|
|
2294
|
+
const approved = normalizeHead(s.lastReviewHead);
|
|
2295
|
+
if (pushed === undefined || (approved !== undefined && sameCommit(pushed, approved)))
|
|
2296
|
+
return end(
|
|
2297
|
+
next,
|
|
2298
|
+
{
|
|
2299
|
+
kind: "aborted",
|
|
2300
|
+
reason: `⚠️ Rebase round ${round.attempt ?? 1} left the approved head unchanged, so the conflict still has no resolution to review.`,
|
|
2301
|
+
round,
|
|
2302
|
+
reviewRounds: next.reviewRounds,
|
|
2303
|
+
},
|
|
2304
|
+
[roundNote(round, "aborted")],
|
|
2305
|
+
);
|
|
2306
|
+
return reviewAfterRebase({ ...next, lastReviewHead: pushed }, [roundNote(round, "pr_opened")]);
|
|
2307
|
+
}
|
|
2240
2308
|
if (round.kind === "findings") {
|
|
2241
2309
|
// Nothing repushed → nothing to re-review, unless every finding of the
|
|
2242
2310
|
// last review was declined on the record: that re-review verifies the
|
|
@@ -2577,6 +2645,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2577
2645
|
reviewRounds: s.reviewRounds,
|
|
2578
2646
|
})
|
|
2579
2647
|
: end(clocked, { kind: "merged", by: "runner", pr: p.pr, sha: r.sha, reviewRounds: s.reviewRounds });
|
|
2648
|
+
if (r.outcome === "conflict") return { state: reenterApprovedRebase(clocked), notes: [] };
|
|
2580
2649
|
if (r.outcome === "refused")
|
|
2581
2650
|
return end(clocked, { kind: "merge_refused", pr: p.pr, reason: r.reason, reviewRounds: s.reviewRounds });
|
|
2582
2651
|
if (r.outcome === "removed") {
|
|
@@ -2641,6 +2710,32 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2641
2710
|
: [],
|
|
2642
2711
|
};
|
|
2643
2712
|
}
|
|
2713
|
+
case "rebase": {
|
|
2714
|
+
const r = ret as Extract<StepReturn, { type: "rebase" }>;
|
|
2715
|
+
if (r.outcome === "refused")
|
|
2716
|
+
return end(clocked, {
|
|
2717
|
+
kind: "merge_refused",
|
|
2718
|
+
pr: p.pr,
|
|
2719
|
+
reason: r.reason,
|
|
2720
|
+
reviewRounds: s.reviewRounds,
|
|
2721
|
+
});
|
|
2722
|
+
if (r.outcome === "conflict")
|
|
2723
|
+
return enterRound(clocked, {
|
|
2724
|
+
index: s.reviewRounds,
|
|
2725
|
+
kind: "rebase",
|
|
2726
|
+
attempt: p.n,
|
|
2727
|
+
});
|
|
2728
|
+
if (!("headSha" in r))
|
|
2729
|
+
return end(clocked, {
|
|
2730
|
+
kind: "merge_refused",
|
|
2731
|
+
pr: p.pr,
|
|
2732
|
+
reason: "the rebase resolver returned no head",
|
|
2733
|
+
reviewRounds: s.reviewRounds,
|
|
2734
|
+
});
|
|
2735
|
+
const rebased: UnitPipelineState = { ...clocked, lastReviewHead: r.headSha };
|
|
2736
|
+
if (r.outcome === "changed") return reviewAfterRebase(rebased);
|
|
2737
|
+
return enterChecks(rebased, { index: s.reviewRounds, kind: "review" }, []);
|
|
2738
|
+
}
|
|
2644
2739
|
case "merge-wait":
|
|
2645
2740
|
return {
|
|
2646
2741
|
state: {
|
|
@@ -2764,7 +2859,7 @@ export interface CommitChecksFacts {
|
|
|
2764
2859
|
* the line is unchanged. */
|
|
2765
2860
|
function mergeReadyHeadline(rounds: string, url: string, base: string, facts: MergeReadyFacts | undefined): string {
|
|
2766
2861
|
if (facts?.mergeableState === "dirty")
|
|
2767
|
-
return `⚠️ Approved but not merge-ready after ${rounds}: ${url} — the head conflicts with \`${base}
|
|
2862
|
+
return `⚠️ Approved but not merge-ready after ${rounds}: ${url} — the head conflicts with \`${base}\`; the pipeline runner owns the rebase (an unchanged patch carries the approval). The approved work stands.`;
|
|
2768
2863
|
const fixups = facts?.fixupCommits ?? [];
|
|
2769
2864
|
if (fixups.length > 0)
|
|
2770
2865
|
return `⚠️ Approved but not merge-ready after ${rounds}: ${url} — ${fixups.length} unsquashed fix-up commit${fixups.length === 1 ? "" : "s"} on the head (${fixups.join("; ")}): squash into the unit's commit, push, and re-review.`;
|