@coreplane/switchboard 1.261.0 → 1.261.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/dist/assets/deploy/cloudflare-memory/worker.ts +1 -1
- 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/core/coordinator/driver.ts +17 -5
- package/dist/assets/src/core/reviewVerdict.ts +33 -6
- package/dist/assets/src/core/ship/coordinator.ts +330 -72
- package/dist/assets/src/core/ship/renewal.ts +1 -1
- package/dist/cli.js +842 -510
- package/package.json +1 -1
|
@@ -94,18 +94,15 @@ export interface ShipCaps {
|
|
|
94
94
|
* named none. */
|
|
95
95
|
export type InterruptionCause = "bot_restart" | "container_replaced" | "sandbox_fault";
|
|
96
96
|
|
|
97
|
-
/** What a ship pipeline's thread and card say when
|
|
98
|
-
* (run-history item 36): the
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* resumes from its request by itself (issue 1903) and the pipeline keeps
|
|
107
|
-
* waiting — this note is written only when that resume failed or no resume
|
|
108
|
-
* applied. */
|
|
97
|
+
/** What a ship pipeline's thread and card say when a run died under it
|
|
98
|
+
* (run-history item 36): the note names the recorded cause and only remote
|
|
99
|
+
* facts that were proved. A recorded pull request is not proof that the
|
|
100
|
+
* stopped run's latest commit reached it. A continuation is named only for
|
|
101
|
+
* an idle unit, whose reply wake is durable; otherwise the note names a new
|
|
102
|
+
* ship run as the next action without promising that a reply resumes work.
|
|
103
|
+
* A run whose container was replaced first resumes from its request by itself
|
|
104
|
+
* (issue 1903); this note is written only when that resume failed or did not
|
|
105
|
+
* apply. */
|
|
109
106
|
/** The cause an interruption's recorded words name (issue 1876): the ledger
|
|
110
107
|
* that saw the roll wrote them — the relaunch refusals and the lost-workspace
|
|
111
108
|
* notes name the replaced container, the sandbox executor's wordings the
|
|
@@ -134,23 +131,21 @@ export function interruptionCauseOfWords(words: string): InterruptionCause | und
|
|
|
134
131
|
}
|
|
135
132
|
|
|
136
133
|
export function shipInterruptedNote(prUrl?: string, cause?: InterruptionCause, idle = false): string {
|
|
137
|
-
const stands = prUrl
|
|
138
|
-
? `Its work stands on GitHub: ${prUrl}.`
|
|
139
|
-
: "Whatever it pushed stands on its pipeline branch; no PR was opened yet.";
|
|
140
|
-
const continuation = idle
|
|
141
|
-
? "The next reply in this thread continues the unit."
|
|
142
|
-
: prUrl
|
|
143
|
-
? `The pipeline kept its task, branch and pull request; the next reply in this thread continues the review loop at ${prUrl}.`
|
|
144
|
-
: "The pipeline kept its task and branch; the next reply in this thread starts round 0 again on that branch.";
|
|
145
134
|
const opening =
|
|
146
135
|
cause === "container_replaced"
|
|
147
|
-
? "⚠️
|
|
136
|
+
? "⚠️ Work stopped before the next review because the repository container was replaced and the run could not continue."
|
|
148
137
|
: cause === "sandbox_fault"
|
|
149
|
-
? "⚠️
|
|
138
|
+
? "⚠️ Work stopped before the next review because the sandbox failed and the run could not continue."
|
|
150
139
|
: cause === "bot_restart"
|
|
151
|
-
? "⚠️
|
|
152
|
-
: "⚠️
|
|
153
|
-
|
|
140
|
+
? "⚠️ Work stopped before the next review because Switchboard restarted and the run could not continue."
|
|
141
|
+
: "⚠️ Work stopped before the next review because the run could not continue.";
|
|
142
|
+
const saved = prUrl
|
|
143
|
+
? `The existing pull request is ${prUrl}; Switchboard did not verify the stopped run's latest commit there.`
|
|
144
|
+
: "No open pull request was verified for the stopped run's latest commit.";
|
|
145
|
+
const next = idle
|
|
146
|
+
? "The unit remains live; a reply in this thread is recorded as its continuation action."
|
|
147
|
+
: "No review was started. Next action: start ship again after the remote branch and pull request state can be verified.";
|
|
148
|
+
return `${opening} ${saved} ${next}`;
|
|
154
149
|
}
|
|
155
150
|
|
|
156
151
|
// ---- the plan graph --------------------------------------------------------------------------------
|
|
@@ -704,7 +699,17 @@ export type PrCheck =
|
|
|
704
699
|
commentId: string;
|
|
705
700
|
};
|
|
706
701
|
}
|
|
707
|
-
| {
|
|
702
|
+
| {
|
|
703
|
+
state: "merged";
|
|
704
|
+
prNumber: number;
|
|
705
|
+
url: string;
|
|
706
|
+
/** The pull request's last source head, distinct from `sha`, the commit
|
|
707
|
+
* the merge put on the base. Required for exact findings reconciliation. */
|
|
708
|
+
headSha?: string;
|
|
709
|
+
sha: string;
|
|
710
|
+
mergedAt: string;
|
|
711
|
+
mergedBy?: string;
|
|
712
|
+
}
|
|
708
713
|
| { state: "closed"; prNumber: number; url: string; closedBy: string };
|
|
709
714
|
|
|
710
715
|
/** Why a live child is being drained before the unit moves on. The pull
|
|
@@ -952,6 +957,15 @@ export type UnitEnding =
|
|
|
952
957
|
round?: RoundRef;
|
|
953
958
|
reviewRounds: number;
|
|
954
959
|
finalReply?: string;
|
|
960
|
+
/** The mechanical WIP checkpoint recorded before this terminal abort. */
|
|
961
|
+
checkpoint?: { branch: string; sha: string };
|
|
962
|
+
/** A completed findings run could not safely advance to review. These
|
|
963
|
+
* causes render their own saved-work, failure and next-action facts,
|
|
964
|
+
* without the generic continuation promise. */
|
|
965
|
+
findingsStop?: "incomplete_outputs" | "unfinished" | "missing_remote" | "head_mismatch" | "remote_unreadable";
|
|
966
|
+
observedHead?: string;
|
|
967
|
+
remoteHead?: string;
|
|
968
|
+
missingOutputs?: string[];
|
|
955
969
|
/** A round-0 end without a pull request that was judged for renewal and
|
|
956
970
|
* refused: the decision and the card's sentence (decision 0046). */
|
|
957
971
|
renewal?: { decision: Extract<RenewalDecision, { renew: false }>; line: string };
|
|
@@ -1000,8 +1014,8 @@ export type UnitEnding =
|
|
|
1000
1014
|
/** The unit idles instead of ending (record 0051): with the resolved
|
|
1001
1015
|
* `ship.idleDays` above zero, `end()` wraps an idling kind — every kind but
|
|
1002
1016
|
* the four ended ones (`merged`, `already_landed`, `merge_ready`,
|
|
1003
|
-
* `refused`) — in this ending: the old kind as `why`,
|
|
1004
|
-
*
|
|
1017
|
+
* `refused`) — in this ending: the old kind as `why`, its outcome report
|
|
1018
|
+
* with the terminal restart replaced by the durable wake action, and what a continuation needs (the renewals the grant
|
|
1005
1019
|
* still holds — unspent: the wake spends one, this plan's fifth unit — the
|
|
1006
1020
|
* head to continue from, the last coding child's run, the spend and its
|
|
1007
1021
|
* handoff). `runId` is absent when the pipeline capped before any coding
|
|
@@ -1212,10 +1226,14 @@ type Phase =
|
|
|
1212
1226
|
childPushed?: PushedHeadFact[];
|
|
1213
1227
|
childLeaseStartedAt?: number;
|
|
1214
1228
|
childHandoff?: Handoff;
|
|
1215
|
-
/** Open or recover a pull request from the branch. A completed
|
|
1216
|
-
* whose same-head salvage merely republished ready work needs this
|
|
1217
|
-
* without being classified as dead. */
|
|
1229
|
+
/** Open or recover a pull request from the branch. A completed round-zero
|
|
1230
|
+
* child whose same-head salvage merely republished ready work needs this
|
|
1231
|
+
* path without being classified as dead. */
|
|
1218
1232
|
recover?: true;
|
|
1233
|
+
/** A completed findings run supplied every typed result. The next step
|
|
1234
|
+
* must independently prove an open branch and pull request at exactly
|
|
1235
|
+
* `childHead`; neither a push record nor this flag is remote evidence. */
|
|
1236
|
+
findingsReady?: true;
|
|
1219
1237
|
/** The coding child died (`failed` or `interrupted`) after it may have
|
|
1220
1238
|
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
1221
1239
|
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
@@ -1863,6 +1881,13 @@ function interruptedCheckpoint(
|
|
|
1863
1881
|
return { branch: found.ref, sha: found.sha };
|
|
1864
1882
|
}
|
|
1865
1883
|
|
|
1884
|
+
/** A full commit id for findings completion and remote reconciliation. A
|
|
1885
|
+
* prefix can identify a commit for display, but cannot prove exact equality. */
|
|
1886
|
+
function fullHead(value: unknown): string | undefined {
|
|
1887
|
+
const head = normalizeHead(value);
|
|
1888
|
+
return head?.length === 40 ? head : undefined;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1866
1891
|
/** A coding run's confirmed end: round 0's child, or the run a findings step dispatched. */
|
|
1867
1892
|
function settleCoding(
|
|
1868
1893
|
s: UnitPipelineState,
|
|
@@ -1890,8 +1915,8 @@ function settleCoding(
|
|
|
1890
1915
|
}
|
|
1891
1916
|
: {}),
|
|
1892
1917
|
};
|
|
1893
|
-
const readySalvage = completedSameHeadSalvage(next, facts.pushed, facts);
|
|
1894
|
-
const checkpoint = interruptedCheckpoint(next, facts.pushed, facts);
|
|
1918
|
+
const readySalvage = round.kind === "coding" && completedSameHeadSalvage(next, facts.pushed, facts);
|
|
1919
|
+
const checkpoint = interruptedCheckpoint(next, facts.pushed, round.kind === "findings" ? undefined : facts);
|
|
1895
1920
|
const mode = stopMode(facts.status);
|
|
1896
1921
|
if (mode !== undefined)
|
|
1897
1922
|
return end(
|
|
@@ -1906,6 +1931,64 @@ function settleCoding(
|
|
|
1906
1931
|
},
|
|
1907
1932
|
[roundNote(round, "stopped")],
|
|
1908
1933
|
);
|
|
1934
|
+
// Findings rounds have their own typed completion contract. They do not
|
|
1935
|
+
// require round zero's handoff, but they do require a completed run, one
|
|
1936
|
+
// matching disposition for every finding, an updated PR description and a
|
|
1937
|
+
// final observed head. Only then may a fresh remote read decide readiness.
|
|
1938
|
+
if (round.kind === "findings" && facts.status === "completed") {
|
|
1939
|
+
const issued = next.findingsByRound[round.index] ?? [];
|
|
1940
|
+
const matched = matchDispositions(issued, facts.dispositions ?? []).matched;
|
|
1941
|
+
const missingOutputs = issued
|
|
1942
|
+
.filter((finding) => !matched.some((disposition) => disposition.findingId === finding.id))
|
|
1943
|
+
.map((finding) => `missing disposition for ${finding.id}`);
|
|
1944
|
+
if (facts.description !== true) missingOutputs.push("missing updated pull request description");
|
|
1945
|
+
const observedHead = fullHead(facts.headSha);
|
|
1946
|
+
if (observedHead === undefined) missingOutputs.push("missing final observed commit");
|
|
1947
|
+
if (missingOutputs.length > 0)
|
|
1948
|
+
return end(
|
|
1949
|
+
next,
|
|
1950
|
+
{
|
|
1951
|
+
kind: "aborted",
|
|
1952
|
+
reason: "The completed findings work did not record every required result.",
|
|
1953
|
+
findingsStop: "incomplete_outputs",
|
|
1954
|
+
...(observedHead !== undefined ? { observedHead } : {}),
|
|
1955
|
+
missingOutputs,
|
|
1956
|
+
round,
|
|
1957
|
+
reviewRounds: next.reviewRounds,
|
|
1958
|
+
},
|
|
1959
|
+
[roundNote(round, "aborted")],
|
|
1960
|
+
);
|
|
1961
|
+
return {
|
|
1962
|
+
state: {
|
|
1963
|
+
...next,
|
|
1964
|
+
phase: {
|
|
1965
|
+
at: "pr-check",
|
|
1966
|
+
round,
|
|
1967
|
+
runId,
|
|
1968
|
+
childHead: observedHead,
|
|
1969
|
+
findingsReady: true,
|
|
1970
|
+
...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
|
|
1971
|
+
},
|
|
1972
|
+
},
|
|
1973
|
+
notes: [],
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
// A findings run must be completed before any remote state can make it
|
|
1977
|
+
// review-ready. A failed run stays resumable even when its record says a
|
|
1978
|
+
// push was attempted; no remote read upgrades an unfinished result.
|
|
1979
|
+
if (round.kind === "findings")
|
|
1980
|
+
return end(
|
|
1981
|
+
next,
|
|
1982
|
+
{
|
|
1983
|
+
kind: "aborted",
|
|
1984
|
+
reason: "The findings work did not complete, so no review was started.",
|
|
1985
|
+
findingsStop: "unfinished",
|
|
1986
|
+
...(fullHead(facts.headSha) !== undefined ? { observedHead: fullHead(facts.headSha) } : {}),
|
|
1987
|
+
round,
|
|
1988
|
+
reviewRounds: next.reviewRounds,
|
|
1989
|
+
},
|
|
1990
|
+
[roundNote(round, "aborted")],
|
|
1991
|
+
);
|
|
1909
1992
|
// A mechanical WIP push means the child did not declare the work ready for
|
|
1910
1993
|
// review, whatever terminal status its own answer produced. Keep the unit
|
|
1911
1994
|
// branch as the resumption point and abort this attempt; an ordinary push
|
|
@@ -1921,7 +2004,8 @@ function settleCoding(
|
|
|
1921
2004
|
next,
|
|
1922
2005
|
{
|
|
1923
2006
|
kind: "aborted",
|
|
1924
|
-
reason: `⚠️ The coding child ended because ${cause}
|
|
2007
|
+
reason: `⚠️ The coding child ended because ${cause}.`,
|
|
2008
|
+
checkpoint,
|
|
1925
2009
|
round,
|
|
1926
2010
|
reviewRounds: next.reviewRounds,
|
|
1927
2011
|
},
|
|
@@ -2457,9 +2541,27 @@ function childSupersession(
|
|
|
2457
2541
|
return undefined;
|
|
2458
2542
|
}
|
|
2459
2543
|
|
|
2460
|
-
function finishSupersededChild(
|
|
2544
|
+
function finishSupersededChild(
|
|
2545
|
+
s: UnitPipelineState,
|
|
2546
|
+
phase: Extract<Phase, { at: "superseded-read" }>,
|
|
2547
|
+
facts: Extract<ChildFacts, { finished: true }>,
|
|
2548
|
+
): Transition {
|
|
2461
2549
|
const supersession = phase.supersession;
|
|
2462
|
-
if (supersession.reason === "merged")
|
|
2550
|
+
if (supersession.reason === "merged") {
|
|
2551
|
+
// A findings child can observe a merge before its own record is read. Fold
|
|
2552
|
+
// that record through the same typed-output gate as an ordinary completion,
|
|
2553
|
+
// then reconcile the merged PR's source head with the completed child head.
|
|
2554
|
+
// Neither the earlier terminal observation nor steering the child proves
|
|
2555
|
+
// that its final commit reached the pull request before the merge.
|
|
2556
|
+
if (phase.round.kind === "findings") {
|
|
2557
|
+
const settled = settleCoding(s, phase.round, phase.runId, facts);
|
|
2558
|
+
const ready = settled.state.phase;
|
|
2559
|
+
if (ready.at === "pr-check" && ready.findingsReady === true)
|
|
2560
|
+
return settlePrCheck(settled.state, ready, supersession.pullRequest);
|
|
2561
|
+
return settled;
|
|
2562
|
+
}
|
|
2563
|
+
return foundMerged(s, supersession.pullRequest);
|
|
2564
|
+
}
|
|
2463
2565
|
if (supersession.reason === "closed") return foundClosed(s, supersession.pullRequest);
|
|
2464
2566
|
if (supersession.reason === "branch_deleted") return missingHeadBranch(s, supersession.pullRequest);
|
|
2465
2567
|
return {
|
|
@@ -2507,6 +2609,87 @@ export function reconcileTerminalPr(s: UnitPipelineState, pr: PrCheck): UnitPipe
|
|
|
2507
2609
|
/** The pull request heading the branch after round 0 or a findings step. */
|
|
2508
2610
|
function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-check" }>, pr: PrCheck): Transition {
|
|
2509
2611
|
const { round } = phase;
|
|
2612
|
+
// A completed findings run is not ready because it says it pushed. It is
|
|
2613
|
+
// ready only when this independent read sees the adopted pull request at the
|
|
2614
|
+
// exact full observed source head (and, while open, its live branch). Every
|
|
2615
|
+
// other remote fact is a resumable stop with no review or merged success.
|
|
2616
|
+
if (phase.findingsReady === true) {
|
|
2617
|
+
const observedHead = fullHead(phase.childHead)!;
|
|
2618
|
+
const withPr = pr.state !== "none" ? { ...s, pr: { number: pr.prNumber, url: pr.url } } : s;
|
|
2619
|
+
if (pr.state === "merged") {
|
|
2620
|
+
const remoteHead = fullHead(pr.headSha);
|
|
2621
|
+
if (remoteHead === undefined)
|
|
2622
|
+
return end(
|
|
2623
|
+
withPr,
|
|
2624
|
+
{
|
|
2625
|
+
kind: "aborted",
|
|
2626
|
+
reason: "The merged pull request's exact source head could not be verified.",
|
|
2627
|
+
findingsStop: "remote_unreadable",
|
|
2628
|
+
observedHead,
|
|
2629
|
+
round,
|
|
2630
|
+
reviewRounds: withPr.reviewRounds,
|
|
2631
|
+
},
|
|
2632
|
+
[roundNote(round, "aborted")],
|
|
2633
|
+
);
|
|
2634
|
+
if (remoteHead !== observedHead)
|
|
2635
|
+
return end(
|
|
2636
|
+
withPr,
|
|
2637
|
+
{
|
|
2638
|
+
kind: "aborted",
|
|
2639
|
+
reason: "The merged pull request does not hold the completed changes.",
|
|
2640
|
+
findingsStop: "head_mismatch",
|
|
2641
|
+
observedHead,
|
|
2642
|
+
remoteHead,
|
|
2643
|
+
round,
|
|
2644
|
+
reviewRounds: withPr.reviewRounds,
|
|
2645
|
+
},
|
|
2646
|
+
[roundNote(round, "aborted")],
|
|
2647
|
+
);
|
|
2648
|
+
return foundMerged(withPr, pr, [roundNote(round, "completed")]);
|
|
2649
|
+
}
|
|
2650
|
+
if (pr.state !== "open" || pr.headBranchExists === false)
|
|
2651
|
+
return end(
|
|
2652
|
+
withPr,
|
|
2653
|
+
{
|
|
2654
|
+
kind: "aborted",
|
|
2655
|
+
reason: "No open pull request and branch could be verified for the completed changes.",
|
|
2656
|
+
findingsStop: "missing_remote",
|
|
2657
|
+
observedHead,
|
|
2658
|
+
round,
|
|
2659
|
+
reviewRounds: withPr.reviewRounds,
|
|
2660
|
+
},
|
|
2661
|
+
[roundNote(round, "aborted")],
|
|
2662
|
+
);
|
|
2663
|
+
const remoteHead = fullHead(pr.headSha);
|
|
2664
|
+
if (pr.headBranchExists !== true || remoteHead === undefined)
|
|
2665
|
+
return end(
|
|
2666
|
+
withPr,
|
|
2667
|
+
{
|
|
2668
|
+
kind: "aborted",
|
|
2669
|
+
reason: "The open pull request's branch and exact head could not be verified.",
|
|
2670
|
+
findingsStop: "remote_unreadable",
|
|
2671
|
+
observedHead,
|
|
2672
|
+
round,
|
|
2673
|
+
reviewRounds: withPr.reviewRounds,
|
|
2674
|
+
},
|
|
2675
|
+
[roundNote(round, "aborted")],
|
|
2676
|
+
);
|
|
2677
|
+
if (remoteHead !== observedHead)
|
|
2678
|
+
return end(
|
|
2679
|
+
withPr,
|
|
2680
|
+
{
|
|
2681
|
+
kind: "aborted",
|
|
2682
|
+
reason: "The pull request does not hold the completed changes.",
|
|
2683
|
+
findingsStop: "head_mismatch",
|
|
2684
|
+
observedHead,
|
|
2685
|
+
remoteHead,
|
|
2686
|
+
round,
|
|
2687
|
+
reviewRounds: withPr.reviewRounds,
|
|
2688
|
+
},
|
|
2689
|
+
[roundNote(round, "aborted")],
|
|
2690
|
+
);
|
|
2691
|
+
return roundOnOpenPr(withPr, phase, pr);
|
|
2692
|
+
}
|
|
2510
2693
|
// The merge landed during the round: the child found nothing left to ship
|
|
2511
2694
|
// (or shipped into a pull request a person merged under it). The round
|
|
2512
2695
|
// completed without a pull request of its own, and the unit is done.
|
|
@@ -3000,7 +3183,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
3000
3183
|
supersession,
|
|
3001
3184
|
finishedObserved: p.finishedObserved,
|
|
3002
3185
|
};
|
|
3003
|
-
return finishSupersededChild(clocked, terminalPhase);
|
|
3186
|
+
return finishSupersededChild(clocked, terminalPhase, r.run);
|
|
3004
3187
|
}
|
|
3005
3188
|
return {
|
|
3006
3189
|
state: {
|
|
@@ -3066,10 +3249,26 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
3066
3249
|
},
|
|
3067
3250
|
[roundNote(p.round, "aborted")],
|
|
3068
3251
|
);
|
|
3069
|
-
// A
|
|
3070
|
-
//
|
|
3071
|
-
//
|
|
3072
|
-
if (p.round.kind
|
|
3252
|
+
// A findings run must complete before another review. An interrupted
|
|
3253
|
+
// one remains resumable even if it recorded an attempted push; round
|
|
3254
|
+
// zero keeps its established recovery check.
|
|
3255
|
+
if (p.round.kind === "findings")
|
|
3256
|
+
return end(
|
|
3257
|
+
clocked,
|
|
3258
|
+
{
|
|
3259
|
+
kind: "interrupted",
|
|
3260
|
+
round: p.round,
|
|
3261
|
+
runId: p.runId,
|
|
3262
|
+
reviewRounds: s.reviewRounds,
|
|
3263
|
+
...(cause !== undefined ? { cause } : {}),
|
|
3264
|
+
},
|
|
3265
|
+
[roundNote(p.round, "aborted")],
|
|
3266
|
+
);
|
|
3267
|
+
// A dead round-zero coding child may have pushed normally before the
|
|
3268
|
+
// ledger closed it: the pr-check recovers that branch. A review child
|
|
3269
|
+
// has nothing on the branch to recover, so its interruption ends the
|
|
3270
|
+
// unit at once.
|
|
3271
|
+
if (p.round.kind === "coding")
|
|
3073
3272
|
return {
|
|
3074
3273
|
state: {
|
|
3075
3274
|
...clocked,
|
|
@@ -3138,7 +3337,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
3138
3337
|
}
|
|
3139
3338
|
case "superseded-read": {
|
|
3140
3339
|
const r = ret as Extract<StepReturn, { type: "read-record" }>;
|
|
3141
|
-
if (r.run.finished) return finishSupersededChild(clocked, p);
|
|
3340
|
+
if (r.run.finished) return finishSupersededChild(clocked, p, r.run);
|
|
3142
3341
|
return {
|
|
3143
3342
|
state: {
|
|
3144
3343
|
...clocked,
|
|
@@ -3503,10 +3702,21 @@ export function renderUnitReport(
|
|
|
3503
3702
|
* after 2 review rounds: <url>`, `Held: <the human-gated row>`, `Stopped`,
|
|
3504
3703
|
* `Round cap reached: <counts>` — and the verdict, the findings left below
|
|
3505
3704
|
* the gate, the declined ones, the remaining-gate sentence, the level in
|
|
3506
|
-
* force, the grant, the write-up pointer, the budget split, the
|
|
3507
|
-
*
|
|
3508
|
-
* stays on the pull request, where the round routes put it. */
|
|
3705
|
+
* force, the grant, the write-up pointer, the budget split, the restart or
|
|
3706
|
+
* durable-wake action and a segment boundary are `verbose` asides; the full
|
|
3707
|
+
* detail stays on the pull request, where the round routes put it. */
|
|
3509
3708
|
verbosity: Verbosity = "verbose",
|
|
3709
|
+
): string {
|
|
3710
|
+
return renderUnitReportWithWake(s, facts, verbosity, false);
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
/** The internal idle render is the only caller allowed to claim an indexed
|
|
3714
|
+
* wake will durably record a reply as the continuation action. */
|
|
3715
|
+
function renderUnitReportWithWake(
|
|
3716
|
+
s: UnitPipelineState,
|
|
3717
|
+
facts: MergeReadyFacts | undefined,
|
|
3718
|
+
verbosity: Verbosity,
|
|
3719
|
+
durableWake: boolean,
|
|
3510
3720
|
): string {
|
|
3511
3721
|
const e = s.ending;
|
|
3512
3722
|
if (!e) return "";
|
|
@@ -3514,12 +3724,20 @@ export function renderUnitReport(
|
|
|
3514
3724
|
const rounds = `${e.reviewRounds} review round${e.reviewRounds === 1 ? "" : "s"}`;
|
|
3515
3725
|
const prUrl = s.pr?.url;
|
|
3516
3726
|
const prLine = prUrl ? ` PR: ${prUrl}` : "";
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3727
|
+
const nextAction = (checkpoint?: { branch: string; sha: string }): string => {
|
|
3728
|
+
if (durableWake)
|
|
3729
|
+
return `The unit remains live; a reply in this thread is recorded as its continuation action${prUrl ? ` from the open pull request (${prUrl})` : " from the saved branch state"}.`;
|
|
3730
|
+
const reconcile = checkpoint
|
|
3731
|
+
? `verify \`${checkpoint.sha.slice(0, 7)}\` is on \`${checkpoint.branch}\`, then reconcile the remote branch and pull request if needed`
|
|
3732
|
+
: prUrl
|
|
3733
|
+
? `verify the remote branch and pull request (${prUrl}) agree, then reconcile them if needed`
|
|
3734
|
+
: "verify the remote branch and reconcile any saved checkpoint if needed";
|
|
3735
|
+
const restart = s.input.generated
|
|
3736
|
+
? `then start ship again with ${prUrl ?? "the original task"}`
|
|
3737
|
+
: "then start this plan again";
|
|
3738
|
+
const blocked = s.input.generated ? "" : "The unit's dependents in this plan stay blocked. ";
|
|
3739
|
+
return `${blocked}Next action: ${reconcile}, ${restart}.`;
|
|
3740
|
+
};
|
|
3523
3741
|
const declined = [...(s.dispositionsByRound[e.reviewRounds - 1] ?? [])].filter((d) => d.disposition === "declined");
|
|
3524
3742
|
const declinedLine = `Declined findings: ${declined.length > 0 ? declined.map((d) => `${d.findingId}${d.note ? ` — ${d.note}` : ""}`).join("; ") : "none"}`;
|
|
3525
3743
|
const verdictLine = `Verdict: LGTM${s.lastVerdictSummary ? ` — ${s.lastVerdictSummary}` : ""}`;
|
|
@@ -3544,7 +3762,7 @@ export function renderUnitReport(
|
|
|
3544
3762
|
const checkpointLine = (checkpoint: { branch: string; sha: string } | undefined) =>
|
|
3545
3763
|
checkpoint === undefined
|
|
3546
3764
|
? undefined
|
|
3547
|
-
: `The
|
|
3765
|
+
: `The run recorded \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\` as pushed, but Switchboard did not independently verify that commit on the remote branch or an open pull request.`;
|
|
3548
3766
|
switch (e.kind) {
|
|
3549
3767
|
case "merged":
|
|
3550
3768
|
if (e.by === "other")
|
|
@@ -3605,9 +3823,9 @@ export function renderUnitReport(
|
|
|
3605
3823
|
if (e.cause === "draft") {
|
|
3606
3824
|
const link = e.pr !== undefined ? ` — ${e.pr.url}` : "";
|
|
3607
3825
|
if (!shows(verbosity, "verbose")) return `⏸️ Held: draft — GitHub still marks the pull request as draft${link}`;
|
|
3608
|
-
const draftContinuation =
|
|
3609
|
-
|
|
3610
|
-
:
|
|
3826
|
+
const draftContinuation = `Next action: mark the pull request ready in GitHub, then ${nextAction()
|
|
3827
|
+
.replace(/^The unit's dependents in this plan stay blocked\. Next action: /, "")
|
|
3828
|
+
.replace(/^Next action: /, "")}`;
|
|
3611
3829
|
return join([
|
|
3612
3830
|
`⏸️ Held after ${rounds}${e.pr !== undefined ? `: ${e.pr.url}` : ""} — the pull request is a draft, so nothing can merge and no fix round would change anything.`,
|
|
3613
3831
|
draftContinuation,
|
|
@@ -3623,7 +3841,9 @@ export function renderUnitReport(
|
|
|
3623
3841
|
return join([
|
|
3624
3842
|
`⏸️ Held after ${rounds}: the coding child of round ${e.round.index} concluded its round blocked instead of opening a pull request — ${e.reason}.${prLine}`,
|
|
3625
3843
|
writeUpPointer(s, e.round.kind, e.runId),
|
|
3626
|
-
|
|
3844
|
+
durableWake
|
|
3845
|
+
? `No renewal was spent and no second coding child ran — a concluded round is not renewed (a renewal continues a budget that ran out mid-work). The child's answer remains the thread's open question. ${nextAction()}`
|
|
3846
|
+
: `No renewal was spent and no second coding child ran — a concluded round is not renewed (a renewal continues a budget that ran out mid-work). The child's answer remains unresolved. ${nextAction()}`,
|
|
3627
3847
|
]);
|
|
3628
3848
|
}
|
|
3629
3849
|
// A human-gated row is a question to a person. The enclosing idle keeps
|
|
@@ -3651,8 +3871,8 @@ export function renderUnitReport(
|
|
|
3651
3871
|
return join([
|
|
3652
3872
|
`⚠️ The review approved ${e.pr.url} but the pipeline did not merge it: ${e.reason}. A person decides what becomes of the pull request.`,
|
|
3653
3873
|
s.input.generated
|
|
3654
|
-
?
|
|
3655
|
-
: "The approved work remains on the branch. This is a bug: the pipeline has no automatic recovery for this refused merge, so
|
|
3874
|
+
? `Next action: merge or close the pull request, then start ship again with ${e.pr.url} so the result is reconciled.`
|
|
3875
|
+
: "The approved work remains on the branch. This is a bug: the pipeline has no automatic recovery for this refused merge. Next action: merge or close the pull request, then start this plan again so its dependents can be reconciled.",
|
|
3656
3876
|
]);
|
|
3657
3877
|
case "round_cap": {
|
|
3658
3878
|
// The quiet copy counts the last review's open findings, only the
|
|
@@ -3671,7 +3891,7 @@ export function renderUnitReport(
|
|
|
3671
3891
|
failedChecks.length > 0
|
|
3672
3892
|
? `🧢 Ship stopped at a cap: the ${e.maxRounds}-round cap — the review of round ${e.reviewRounds} approved, but ${failedChecks.map((f) => `\`${f.id}\``).join(", ")} failed at the approved head and no fix round remains.${prLine}`
|
|
3673
3893
|
: `🧢 Ship stopped at a cap: the ${e.maxRounds}-round cap — no approval after ${rounds}.${prLine}`;
|
|
3674
|
-
return join([headline, splitReport(s),
|
|
3894
|
+
return join([headline, splitReport(s), nextAction()]);
|
|
3675
3895
|
}
|
|
3676
3896
|
case "wall_clock_cap":
|
|
3677
3897
|
if (!shows(verbosity, "verbose")) return `🧢 Out of budget — no approval after ${rounds}.${prLine}`;
|
|
@@ -3679,13 +3899,13 @@ export function renderUnitReport(
|
|
|
3679
3899
|
`🧢 Ship stopped at a cap: the remaining pipeline time (~${Math.max(0, Math.round(e.remainingMs / MIN))} min of the ${s.input.caps.maxMinutes}-minute budget) cannot hold another round${e.refused ? ` (the ${e.refused.round} round would get ${e.refused.minutes} min, under its floor of ${e.refused.floor})` : ""} — no approval after ${rounds}.${prLine}`,
|
|
3680
3900
|
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
3681
3901
|
splitReport(s),
|
|
3682
|
-
|
|
3902
|
+
nextAction(),
|
|
3683
3903
|
]);
|
|
3684
3904
|
case "review_pending":
|
|
3685
3905
|
return join([
|
|
3686
3906
|
`⏳ Review pending: the coding child shipped ${e.pr.url}${e.headSha !== undefined ? ` (head \`${e.headSha.slice(0, 7)}\`)` : ""} but the remaining pipeline time cannot hold the review round — the work stands, only the review is missing. The next pipeline starts at the review round while the pull request still heads at the child's own last push.`,
|
|
3687
3907
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
3688
|
-
aside(
|
|
3908
|
+
aside(nextAction()),
|
|
3689
3909
|
]);
|
|
3690
3910
|
case "stopped":
|
|
3691
3911
|
if (!shows(verbosity, "verbose"))
|
|
@@ -3701,12 +3921,50 @@ export function renderUnitReport(
|
|
|
3701
3921
|
e.postedReview
|
|
3702
3922
|
? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
|
|
3703
3923
|
: undefined,
|
|
3704
|
-
|
|
3924
|
+
nextAction(e.checkpoint),
|
|
3705
3925
|
]);
|
|
3706
3926
|
case "aborted":
|
|
3927
|
+
if (e.findingsStop !== undefined) {
|
|
3928
|
+
const observed = e.observedHead?.slice(0, 7);
|
|
3929
|
+
const branch = s.input.unit.branch;
|
|
3930
|
+
if (e.findingsStop === "incomplete_outputs")
|
|
3931
|
+
return join([
|
|
3932
|
+
`⚠️ Review did not restart: the completed findings work did not record every required result (${(e.missingOutputs ?? []).join("; ")}).`,
|
|
3933
|
+
observed !== undefined
|
|
3934
|
+
? `Saved-work fact: The run ended at \`${observed}\`, but remote state was not checked; Switchboard cannot claim that commit is on the branch or pull request.`
|
|
3935
|
+
: "Saved-work fact: The run did not record a final commit, and remote state was not checked.",
|
|
3936
|
+
"Next action: a new findings run must record every disposition, the updated pull request description and its final commit before remote reconciliation.",
|
|
3937
|
+
]);
|
|
3938
|
+
if (e.findingsStop === "unfinished")
|
|
3939
|
+
return join([
|
|
3940
|
+
"⚠️ Review did not restart: the findings work did not complete.",
|
|
3941
|
+
observed !== undefined
|
|
3942
|
+
? `Saved-work fact: the stopped run recorded \`${observed}\`, but Switchboard did not verify it on the remote branch or pull request.`
|
|
3943
|
+
: "Saved-work fact: no completed commit was recorded, and remote state was not checked.",
|
|
3944
|
+
"Next action: start a new findings run; review can begin only after it completes every required result and Switchboard verifies its exact remote head.",
|
|
3945
|
+
]);
|
|
3946
|
+
if (e.findingsStop === "missing_remote")
|
|
3947
|
+
return join([
|
|
3948
|
+
`⚠️ Review did not restart: The completed changes ended at \`${observed}\`, but no open pull request was found for \`${branch}\`.`,
|
|
3949
|
+
`Saved-work fact: The run recorded completed commit \`${observed}\`; Switchboard did not verify that commit on the remote branch or a pull request.`,
|
|
3950
|
+
"Next action: reconcile the branch and open pull request, then start ship again so it can verify the exact head before review.",
|
|
3951
|
+
]);
|
|
3952
|
+
if (e.findingsStop === "head_mismatch")
|
|
3953
|
+
return join([
|
|
3954
|
+
`⚠️ Review did not restart: The completed changes ended at \`${observed}\`, but the pull request is at \`${e.remoteHead?.slice(0, 7)}\` (${s.pr?.url}).`,
|
|
3955
|
+
"Saved-work fact: the pull request's current commit is verified, but the completed changes are not verified there. No review ran on either commit.",
|
|
3956
|
+
"Next action: reconcile the pull request to the completed commit, then start ship again so it can verify that exact head before review.",
|
|
3957
|
+
]);
|
|
3958
|
+
return join([
|
|
3959
|
+
`⚠️ Review did not restart: The pull request exists (${s.pr?.url}), but its exact head could not be verified.`,
|
|
3960
|
+
`Saved-work fact: the run recorded completed commit \`${observed}\`; Switchboard did not start review for \`${observed}\` and cannot claim the pull request contains it.`,
|
|
3961
|
+
"Next action: retry ship when the pull request state is readable so it can verify the exact head before review.",
|
|
3962
|
+
]);
|
|
3963
|
+
}
|
|
3707
3964
|
if (!shows(verbosity, "verbose")) return `⚠️ Aborted after ${rounds}: ${e.reason}${prLine}`;
|
|
3708
3965
|
return join([
|
|
3709
3966
|
e.reason,
|
|
3967
|
+
checkpointLine(e.checkpoint),
|
|
3710
3968
|
writeUpPointer(
|
|
3711
3969
|
s,
|
|
3712
3970
|
e.round?.kind ?? "coding",
|
|
@@ -3714,7 +3972,7 @@ export function renderUnitReport(
|
|
|
3714
3972
|
),
|
|
3715
3973
|
e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
|
|
3716
3974
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
3717
|
-
|
|
3975
|
+
nextAction(e.checkpoint),
|
|
3718
3976
|
]);
|
|
3719
3977
|
case "continued":
|
|
3720
3978
|
// A segment boundary is the runner continuing — an acknowledgement, verbose
|
|
@@ -3723,8 +3981,8 @@ export function renderUnitReport(
|
|
|
3723
3981
|
if (!shows(verbosity, "verbose")) return "";
|
|
3724
3982
|
return join([
|
|
3725
3983
|
writeUpPointer(s, e.round.kind, e.runId),
|
|
3726
|
-
|
|
3727
|
-
? `🔁 The unit's budget ran out with the unit unfinished — ${e.line}.
|
|
3984
|
+
durableWake
|
|
3985
|
+
? `🔁 The unit's budget ran out with the unit unfinished — ${e.line}. A reply in this thread is recorded as its continuation action; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`
|
|
3728
3986
|
: `🔁 The unit's budget ran out with the unit unfinished — ${e.line}. A fresh ${s.input.caps.maxMinutes}-minute budget opens in this thread${e.from !== undefined ? ` from \`${e.from.slice(0, 7)}\`` : ""}, with the last run's write-up as its request; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`,
|
|
3729
3987
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
3730
3988
|
]);
|
|
@@ -3735,7 +3993,7 @@ export function renderUnitReport(
|
|
|
3735
3993
|
`⚠️ The coding child of round ${e.round.index} (run ${e.runId}) died on a provider transient — a model-gateway 5xx, a cut stream or a gateway timeout past the harness's retry ladder — with nothing pushed, after the round was already re-run once for the same reason. The task itself was never the problem.`,
|
|
3736
3994
|
writeUpPointer(s, e.round.kind, s.lastCodingRunId),
|
|
3737
3995
|
`⚠️ This is a bug: ship ended after ${rounds} because its automatic provider retry was spent.`,
|
|
3738
|
-
|
|
3996
|
+
nextAction(),
|
|
3739
3997
|
]);
|
|
3740
3998
|
case "no_verdict":
|
|
3741
3999
|
if (!shows(verbosity, "verbose"))
|
|
@@ -3744,20 +4002,20 @@ export function renderUnitReport(
|
|
|
3744
4002
|
`⚠️ Review round ${e.round.index} ended without a submitted verdict (budget, refusal, or stop) — ship never converts that into a request for changes, so no findings step ran.`,
|
|
3745
4003
|
writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index]),
|
|
3746
4004
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
3747
|
-
|
|
4005
|
+
nextAction(),
|
|
3748
4006
|
]);
|
|
3749
4007
|
case "interrupted":
|
|
3750
|
-
return join([shipInterruptedNote(prUrl, e.cause,
|
|
4008
|
+
return join([shipInterruptedNote(prUrl, e.cause, durableWake), checkpointLine(e.checkpoint)]);
|
|
3751
4009
|
case "idle_expired":
|
|
3752
4010
|
return "⌛ Idle expired: no reply continued this unit before its idle window closed.";
|
|
3753
4011
|
case "refused":
|
|
3754
4012
|
return join([
|
|
3755
4013
|
`🚫 The ${presetOf(e.round.kind)} child of round ${e.round.index} was refused by the authorize stage (${e.refusal})${e.message ? `: ${e.message}` : ""} — every child is authorized as the requesting user, so the pipeline ends here.`,
|
|
3756
|
-
aside(
|
|
4014
|
+
aside(nextAction()),
|
|
3757
4015
|
]);
|
|
3758
4016
|
case "idle":
|
|
3759
|
-
//
|
|
3760
|
-
//
|
|
3761
|
-
return
|
|
4017
|
+
// Keep the old kind's outcome at this copy's level, but render the
|
|
4018
|
+
// indexed wake's durable reply action instead of a terminal restart.
|
|
4019
|
+
return renderUnitReportWithWake({ ...s, ending: e.idled }, facts, verbosity, true);
|
|
3762
4020
|
}
|
|
3763
4021
|
}
|
|
@@ -162,7 +162,7 @@ export function renderRenewal(
|
|
|
162
162
|
): string {
|
|
163
163
|
const recourse = options.idle
|
|
164
164
|
? "the next reply in this thread continues the unit"
|
|
165
|
-
: "
|
|
165
|
+
: "start ship again with the original task";
|
|
166
166
|
const senders = options.senders?.length
|
|
167
167
|
? `, with ${options.senders.length} message${options.senders.length === 1 ? "" : "s"} from ${options.senders.join(", ")}`
|
|
168
168
|
: "";
|