@coreplane/switchboard 1.261.0 → 1.261.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/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/contract.ts +39 -0
- package/dist/assets/src/core/coordinator/driver.ts +17 -5
- package/dist/assets/src/core/reviewVerdict.ts +33 -6
- package/dist/assets/src/core/runEvents.ts +15 -1
- package/dist/assets/src/core/runLedger/types.ts +4 -0
- package/dist/assets/src/core/ship/coordinator.ts +333 -72
- package/dist/assets/src/core/ship/renewal.ts +1 -1
- package/dist/assets/src/core/types.ts +6 -2
- package/dist/cli.js +1264 -662
- 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 --------------------------------------------------------------------------------
|
|
@@ -507,6 +502,8 @@ export type Brief =
|
|
|
507
502
|
kind: "findings";
|
|
508
503
|
unit: string;
|
|
509
504
|
pr: number;
|
|
505
|
+
/** The exact reviewed head the fix round may replace. */
|
|
506
|
+
headSha?: string;
|
|
510
507
|
/** The review run normally supplies the findings. An adopted attempt
|
|
511
508
|
* can recover them from the last posted verdict marker instead. */
|
|
512
509
|
reviewRunId?: string;
|
|
@@ -704,7 +701,17 @@ export type PrCheck =
|
|
|
704
701
|
commentId: string;
|
|
705
702
|
};
|
|
706
703
|
}
|
|
707
|
-
| {
|
|
704
|
+
| {
|
|
705
|
+
state: "merged";
|
|
706
|
+
prNumber: number;
|
|
707
|
+
url: string;
|
|
708
|
+
/** The pull request's last source head, distinct from `sha`, the commit
|
|
709
|
+
* the merge put on the base. Required for exact findings reconciliation. */
|
|
710
|
+
headSha?: string;
|
|
711
|
+
sha: string;
|
|
712
|
+
mergedAt: string;
|
|
713
|
+
mergedBy?: string;
|
|
714
|
+
}
|
|
708
715
|
| { state: "closed"; prNumber: number; url: string; closedBy: string };
|
|
709
716
|
|
|
710
717
|
/** Why a live child is being drained before the unit moves on. The pull
|
|
@@ -952,6 +959,15 @@ export type UnitEnding =
|
|
|
952
959
|
round?: RoundRef;
|
|
953
960
|
reviewRounds: number;
|
|
954
961
|
finalReply?: string;
|
|
962
|
+
/** The mechanical WIP checkpoint recorded before this terminal abort. */
|
|
963
|
+
checkpoint?: { branch: string; sha: string };
|
|
964
|
+
/** A completed findings run could not safely advance to review. These
|
|
965
|
+
* causes render their own saved-work, failure and next-action facts,
|
|
966
|
+
* without the generic continuation promise. */
|
|
967
|
+
findingsStop?: "incomplete_outputs" | "unfinished" | "missing_remote" | "head_mismatch" | "remote_unreadable";
|
|
968
|
+
observedHead?: string;
|
|
969
|
+
remoteHead?: string;
|
|
970
|
+
missingOutputs?: string[];
|
|
955
971
|
/** A round-0 end without a pull request that was judged for renewal and
|
|
956
972
|
* refused: the decision and the card's sentence (decision 0046). */
|
|
957
973
|
renewal?: { decision: Extract<RenewalDecision, { renew: false }>; line: string };
|
|
@@ -1000,8 +1016,8 @@ export type UnitEnding =
|
|
|
1000
1016
|
/** The unit idles instead of ending (record 0051): with the resolved
|
|
1001
1017
|
* `ship.idleDays` above zero, `end()` wraps an idling kind — every kind but
|
|
1002
1018
|
* the four ended ones (`merged`, `already_landed`, `merge_ready`,
|
|
1003
|
-
* `refused`) — in this ending: the old kind as `why`,
|
|
1004
|
-
*
|
|
1019
|
+
* `refused`) — in this ending: the old kind as `why`, its outcome report
|
|
1020
|
+
* with the terminal restart replaced by the durable wake action, and what a continuation needs (the renewals the grant
|
|
1005
1021
|
* still holds — unspent: the wake spends one, this plan's fifth unit — the
|
|
1006
1022
|
* head to continue from, the last coding child's run, the spend and its
|
|
1007
1023
|
* handoff). `runId` is absent when the pipeline capped before any coding
|
|
@@ -1212,10 +1228,14 @@ type Phase =
|
|
|
1212
1228
|
childPushed?: PushedHeadFact[];
|
|
1213
1229
|
childLeaseStartedAt?: number;
|
|
1214
1230
|
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. */
|
|
1231
|
+
/** Open or recover a pull request from the branch. A completed round-zero
|
|
1232
|
+
* child whose same-head salvage merely republished ready work needs this
|
|
1233
|
+
* path without being classified as dead. */
|
|
1218
1234
|
recover?: true;
|
|
1235
|
+
/** A completed findings run supplied every typed result. The next step
|
|
1236
|
+
* must independently prove an open branch and pull request at exactly
|
|
1237
|
+
* `childHead`; neither a push record nor this flag is remote evidence. */
|
|
1238
|
+
findingsReady?: true;
|
|
1219
1239
|
/** The coding child died (`failed` or `interrupted`) after it may have
|
|
1220
1240
|
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
1221
1241
|
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
@@ -1448,6 +1468,7 @@ function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
|
|
|
1448
1468
|
kind: "findings",
|
|
1449
1469
|
unit,
|
|
1450
1470
|
pr,
|
|
1471
|
+
...(s.lastReviewHead !== undefined ? { headSha: s.lastReviewHead } : {}),
|
|
1451
1472
|
...(reviewRunId !== undefined ? { reviewRunId } : {}),
|
|
1452
1473
|
...(reviewRunId === undefined || (answers !== undefined && answers.length > 0) ? { findings: issued } : {}),
|
|
1453
1474
|
...(answers !== undefined && answers.length > 0 ? { answers } : {}),
|
|
@@ -1863,6 +1884,13 @@ function interruptedCheckpoint(
|
|
|
1863
1884
|
return { branch: found.ref, sha: found.sha };
|
|
1864
1885
|
}
|
|
1865
1886
|
|
|
1887
|
+
/** A full commit id for findings completion and remote reconciliation. A
|
|
1888
|
+
* prefix can identify a commit for display, but cannot prove exact equality. */
|
|
1889
|
+
function fullHead(value: unknown): string | undefined {
|
|
1890
|
+
const head = normalizeHead(value);
|
|
1891
|
+
return head?.length === 40 ? head : undefined;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1866
1894
|
/** A coding run's confirmed end: round 0's child, or the run a findings step dispatched. */
|
|
1867
1895
|
function settleCoding(
|
|
1868
1896
|
s: UnitPipelineState,
|
|
@@ -1890,8 +1918,8 @@ function settleCoding(
|
|
|
1890
1918
|
}
|
|
1891
1919
|
: {}),
|
|
1892
1920
|
};
|
|
1893
|
-
const readySalvage = completedSameHeadSalvage(next, facts.pushed, facts);
|
|
1894
|
-
const checkpoint = interruptedCheckpoint(next, facts.pushed, facts);
|
|
1921
|
+
const readySalvage = round.kind === "coding" && completedSameHeadSalvage(next, facts.pushed, facts);
|
|
1922
|
+
const checkpoint = interruptedCheckpoint(next, facts.pushed, round.kind === "findings" ? undefined : facts);
|
|
1895
1923
|
const mode = stopMode(facts.status);
|
|
1896
1924
|
if (mode !== undefined)
|
|
1897
1925
|
return end(
|
|
@@ -1906,6 +1934,64 @@ function settleCoding(
|
|
|
1906
1934
|
},
|
|
1907
1935
|
[roundNote(round, "stopped")],
|
|
1908
1936
|
);
|
|
1937
|
+
// Findings rounds have their own typed completion contract. They do not
|
|
1938
|
+
// require round zero's handoff, but they do require a completed run, one
|
|
1939
|
+
// matching disposition for every finding, an updated PR description and a
|
|
1940
|
+
// final observed head. Only then may a fresh remote read decide readiness.
|
|
1941
|
+
if (round.kind === "findings" && facts.status === "completed") {
|
|
1942
|
+
const issued = next.findingsByRound[round.index] ?? [];
|
|
1943
|
+
const matched = matchDispositions(issued, facts.dispositions ?? []).matched;
|
|
1944
|
+
const missingOutputs = issued
|
|
1945
|
+
.filter((finding) => !matched.some((disposition) => disposition.findingId === finding.id))
|
|
1946
|
+
.map((finding) => `missing disposition for ${finding.id}`);
|
|
1947
|
+
if (facts.description !== true) missingOutputs.push("missing updated pull request description");
|
|
1948
|
+
const observedHead = fullHead(facts.headSha);
|
|
1949
|
+
if (observedHead === undefined) missingOutputs.push("missing final observed commit");
|
|
1950
|
+
if (missingOutputs.length > 0)
|
|
1951
|
+
return end(
|
|
1952
|
+
next,
|
|
1953
|
+
{
|
|
1954
|
+
kind: "aborted",
|
|
1955
|
+
reason: "The completed findings work did not record every required result.",
|
|
1956
|
+
findingsStop: "incomplete_outputs",
|
|
1957
|
+
...(observedHead !== undefined ? { observedHead } : {}),
|
|
1958
|
+
missingOutputs,
|
|
1959
|
+
round,
|
|
1960
|
+
reviewRounds: next.reviewRounds,
|
|
1961
|
+
},
|
|
1962
|
+
[roundNote(round, "aborted")],
|
|
1963
|
+
);
|
|
1964
|
+
return {
|
|
1965
|
+
state: {
|
|
1966
|
+
...next,
|
|
1967
|
+
phase: {
|
|
1968
|
+
at: "pr-check",
|
|
1969
|
+
round,
|
|
1970
|
+
runId,
|
|
1971
|
+
childHead: observedHead,
|
|
1972
|
+
findingsReady: true,
|
|
1973
|
+
...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
|
|
1974
|
+
},
|
|
1975
|
+
},
|
|
1976
|
+
notes: [],
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
// A findings run must be completed before any remote state can make it
|
|
1980
|
+
// review-ready. A failed run stays resumable even when its record says a
|
|
1981
|
+
// push was attempted; no remote read upgrades an unfinished result.
|
|
1982
|
+
if (round.kind === "findings")
|
|
1983
|
+
return end(
|
|
1984
|
+
next,
|
|
1985
|
+
{
|
|
1986
|
+
kind: "aborted",
|
|
1987
|
+
reason: "The findings work did not complete, so no review was started.",
|
|
1988
|
+
findingsStop: "unfinished",
|
|
1989
|
+
...(fullHead(facts.headSha) !== undefined ? { observedHead: fullHead(facts.headSha) } : {}),
|
|
1990
|
+
round,
|
|
1991
|
+
reviewRounds: next.reviewRounds,
|
|
1992
|
+
},
|
|
1993
|
+
[roundNote(round, "aborted")],
|
|
1994
|
+
);
|
|
1909
1995
|
// A mechanical WIP push means the child did not declare the work ready for
|
|
1910
1996
|
// review, whatever terminal status its own answer produced. Keep the unit
|
|
1911
1997
|
// branch as the resumption point and abort this attempt; an ordinary push
|
|
@@ -1921,7 +2007,8 @@ function settleCoding(
|
|
|
1921
2007
|
next,
|
|
1922
2008
|
{
|
|
1923
2009
|
kind: "aborted",
|
|
1924
|
-
reason: `⚠️ The coding child ended because ${cause}
|
|
2010
|
+
reason: `⚠️ The coding child ended because ${cause}.`,
|
|
2011
|
+
checkpoint,
|
|
1925
2012
|
round,
|
|
1926
2013
|
reviewRounds: next.reviewRounds,
|
|
1927
2014
|
},
|
|
@@ -2457,9 +2544,27 @@ function childSupersession(
|
|
|
2457
2544
|
return undefined;
|
|
2458
2545
|
}
|
|
2459
2546
|
|
|
2460
|
-
function finishSupersededChild(
|
|
2547
|
+
function finishSupersededChild(
|
|
2548
|
+
s: UnitPipelineState,
|
|
2549
|
+
phase: Extract<Phase, { at: "superseded-read" }>,
|
|
2550
|
+
facts: Extract<ChildFacts, { finished: true }>,
|
|
2551
|
+
): Transition {
|
|
2461
2552
|
const supersession = phase.supersession;
|
|
2462
|
-
if (supersession.reason === "merged")
|
|
2553
|
+
if (supersession.reason === "merged") {
|
|
2554
|
+
// A findings child can observe a merge before its own record is read. Fold
|
|
2555
|
+
// that record through the same typed-output gate as an ordinary completion,
|
|
2556
|
+
// then reconcile the merged PR's source head with the completed child head.
|
|
2557
|
+
// Neither the earlier terminal observation nor steering the child proves
|
|
2558
|
+
// that its final commit reached the pull request before the merge.
|
|
2559
|
+
if (phase.round.kind === "findings") {
|
|
2560
|
+
const settled = settleCoding(s, phase.round, phase.runId, facts);
|
|
2561
|
+
const ready = settled.state.phase;
|
|
2562
|
+
if (ready.at === "pr-check" && ready.findingsReady === true)
|
|
2563
|
+
return settlePrCheck(settled.state, ready, supersession.pullRequest);
|
|
2564
|
+
return settled;
|
|
2565
|
+
}
|
|
2566
|
+
return foundMerged(s, supersession.pullRequest);
|
|
2567
|
+
}
|
|
2463
2568
|
if (supersession.reason === "closed") return foundClosed(s, supersession.pullRequest);
|
|
2464
2569
|
if (supersession.reason === "branch_deleted") return missingHeadBranch(s, supersession.pullRequest);
|
|
2465
2570
|
return {
|
|
@@ -2507,6 +2612,87 @@ export function reconcileTerminalPr(s: UnitPipelineState, pr: PrCheck): UnitPipe
|
|
|
2507
2612
|
/** The pull request heading the branch after round 0 or a findings step. */
|
|
2508
2613
|
function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-check" }>, pr: PrCheck): Transition {
|
|
2509
2614
|
const { round } = phase;
|
|
2615
|
+
// A completed findings run is not ready because it says it pushed. It is
|
|
2616
|
+
// ready only when this independent read sees the adopted pull request at the
|
|
2617
|
+
// exact full observed source head (and, while open, its live branch). Every
|
|
2618
|
+
// other remote fact is a resumable stop with no review or merged success.
|
|
2619
|
+
if (phase.findingsReady === true) {
|
|
2620
|
+
const observedHead = fullHead(phase.childHead)!;
|
|
2621
|
+
const withPr = pr.state !== "none" ? { ...s, pr: { number: pr.prNumber, url: pr.url } } : s;
|
|
2622
|
+
if (pr.state === "merged") {
|
|
2623
|
+
const remoteHead = fullHead(pr.headSha);
|
|
2624
|
+
if (remoteHead === undefined)
|
|
2625
|
+
return end(
|
|
2626
|
+
withPr,
|
|
2627
|
+
{
|
|
2628
|
+
kind: "aborted",
|
|
2629
|
+
reason: "The merged pull request's exact source head could not be verified.",
|
|
2630
|
+
findingsStop: "remote_unreadable",
|
|
2631
|
+
observedHead,
|
|
2632
|
+
round,
|
|
2633
|
+
reviewRounds: withPr.reviewRounds,
|
|
2634
|
+
},
|
|
2635
|
+
[roundNote(round, "aborted")],
|
|
2636
|
+
);
|
|
2637
|
+
if (remoteHead !== observedHead)
|
|
2638
|
+
return end(
|
|
2639
|
+
withPr,
|
|
2640
|
+
{
|
|
2641
|
+
kind: "aborted",
|
|
2642
|
+
reason: "The merged pull request does not hold the completed changes.",
|
|
2643
|
+
findingsStop: "head_mismatch",
|
|
2644
|
+
observedHead,
|
|
2645
|
+
remoteHead,
|
|
2646
|
+
round,
|
|
2647
|
+
reviewRounds: withPr.reviewRounds,
|
|
2648
|
+
},
|
|
2649
|
+
[roundNote(round, "aborted")],
|
|
2650
|
+
);
|
|
2651
|
+
return foundMerged(withPr, pr, [roundNote(round, "completed")]);
|
|
2652
|
+
}
|
|
2653
|
+
if (pr.state !== "open" || pr.headBranchExists === false)
|
|
2654
|
+
return end(
|
|
2655
|
+
withPr,
|
|
2656
|
+
{
|
|
2657
|
+
kind: "aborted",
|
|
2658
|
+
reason: "No open pull request and branch could be verified for the completed changes.",
|
|
2659
|
+
findingsStop: "missing_remote",
|
|
2660
|
+
observedHead,
|
|
2661
|
+
round,
|
|
2662
|
+
reviewRounds: withPr.reviewRounds,
|
|
2663
|
+
},
|
|
2664
|
+
[roundNote(round, "aborted")],
|
|
2665
|
+
);
|
|
2666
|
+
const remoteHead = fullHead(pr.headSha);
|
|
2667
|
+
if (pr.headBranchExists !== true || remoteHead === undefined)
|
|
2668
|
+
return end(
|
|
2669
|
+
withPr,
|
|
2670
|
+
{
|
|
2671
|
+
kind: "aborted",
|
|
2672
|
+
reason: "The open pull request's branch and exact head could not be verified.",
|
|
2673
|
+
findingsStop: "remote_unreadable",
|
|
2674
|
+
observedHead,
|
|
2675
|
+
round,
|
|
2676
|
+
reviewRounds: withPr.reviewRounds,
|
|
2677
|
+
},
|
|
2678
|
+
[roundNote(round, "aborted")],
|
|
2679
|
+
);
|
|
2680
|
+
if (remoteHead !== observedHead)
|
|
2681
|
+
return end(
|
|
2682
|
+
withPr,
|
|
2683
|
+
{
|
|
2684
|
+
kind: "aborted",
|
|
2685
|
+
reason: "The pull request does not hold the completed changes.",
|
|
2686
|
+
findingsStop: "head_mismatch",
|
|
2687
|
+
observedHead,
|
|
2688
|
+
remoteHead,
|
|
2689
|
+
round,
|
|
2690
|
+
reviewRounds: withPr.reviewRounds,
|
|
2691
|
+
},
|
|
2692
|
+
[roundNote(round, "aborted")],
|
|
2693
|
+
);
|
|
2694
|
+
return roundOnOpenPr(withPr, phase, pr);
|
|
2695
|
+
}
|
|
2510
2696
|
// The merge landed during the round: the child found nothing left to ship
|
|
2511
2697
|
// (or shipped into a pull request a person merged under it). The round
|
|
2512
2698
|
// completed without a pull request of its own, and the unit is done.
|
|
@@ -3000,7 +3186,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
3000
3186
|
supersession,
|
|
3001
3187
|
finishedObserved: p.finishedObserved,
|
|
3002
3188
|
};
|
|
3003
|
-
return finishSupersededChild(clocked, terminalPhase);
|
|
3189
|
+
return finishSupersededChild(clocked, terminalPhase, r.run);
|
|
3004
3190
|
}
|
|
3005
3191
|
return {
|
|
3006
3192
|
state: {
|
|
@@ -3066,10 +3252,26 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
3066
3252
|
},
|
|
3067
3253
|
[roundNote(p.round, "aborted")],
|
|
3068
3254
|
);
|
|
3069
|
-
// A
|
|
3070
|
-
//
|
|
3071
|
-
//
|
|
3072
|
-
if (p.round.kind
|
|
3255
|
+
// A findings run must complete before another review. An interrupted
|
|
3256
|
+
// one remains resumable even if it recorded an attempted push; round
|
|
3257
|
+
// zero keeps its established recovery check.
|
|
3258
|
+
if (p.round.kind === "findings")
|
|
3259
|
+
return end(
|
|
3260
|
+
clocked,
|
|
3261
|
+
{
|
|
3262
|
+
kind: "interrupted",
|
|
3263
|
+
round: p.round,
|
|
3264
|
+
runId: p.runId,
|
|
3265
|
+
reviewRounds: s.reviewRounds,
|
|
3266
|
+
...(cause !== undefined ? { cause } : {}),
|
|
3267
|
+
},
|
|
3268
|
+
[roundNote(p.round, "aborted")],
|
|
3269
|
+
);
|
|
3270
|
+
// A dead round-zero coding child may have pushed normally before the
|
|
3271
|
+
// ledger closed it: the pr-check recovers that branch. A review child
|
|
3272
|
+
// has nothing on the branch to recover, so its interruption ends the
|
|
3273
|
+
// unit at once.
|
|
3274
|
+
if (p.round.kind === "coding")
|
|
3073
3275
|
return {
|
|
3074
3276
|
state: {
|
|
3075
3277
|
...clocked,
|
|
@@ -3138,7 +3340,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
3138
3340
|
}
|
|
3139
3341
|
case "superseded-read": {
|
|
3140
3342
|
const r = ret as Extract<StepReturn, { type: "read-record" }>;
|
|
3141
|
-
if (r.run.finished) return finishSupersededChild(clocked, p);
|
|
3343
|
+
if (r.run.finished) return finishSupersededChild(clocked, p, r.run);
|
|
3142
3344
|
return {
|
|
3143
3345
|
state: {
|
|
3144
3346
|
...clocked,
|
|
@@ -3503,10 +3705,21 @@ export function renderUnitReport(
|
|
|
3503
3705
|
* after 2 review rounds: <url>`, `Held: <the human-gated row>`, `Stopped`,
|
|
3504
3706
|
* `Round cap reached: <counts>` — and the verdict, the findings left below
|
|
3505
3707
|
* 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. */
|
|
3708
|
+
* force, the grant, the write-up pointer, the budget split, the restart or
|
|
3709
|
+
* durable-wake action and a segment boundary are `verbose` asides; the full
|
|
3710
|
+
* detail stays on the pull request, where the round routes put it. */
|
|
3509
3711
|
verbosity: Verbosity = "verbose",
|
|
3712
|
+
): string {
|
|
3713
|
+
return renderUnitReportWithWake(s, facts, verbosity, false);
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
/** The internal idle render is the only caller allowed to claim an indexed
|
|
3717
|
+
* wake will durably record a reply as the continuation action. */
|
|
3718
|
+
function renderUnitReportWithWake(
|
|
3719
|
+
s: UnitPipelineState,
|
|
3720
|
+
facts: MergeReadyFacts | undefined,
|
|
3721
|
+
verbosity: Verbosity,
|
|
3722
|
+
durableWake: boolean,
|
|
3510
3723
|
): string {
|
|
3511
3724
|
const e = s.ending;
|
|
3512
3725
|
if (!e) return "";
|
|
@@ -3514,12 +3727,20 @@ export function renderUnitReport(
|
|
|
3514
3727
|
const rounds = `${e.reviewRounds} review round${e.reviewRounds === 1 ? "" : "s"}`;
|
|
3515
3728
|
const prUrl = s.pr?.url;
|
|
3516
3729
|
const prLine = prUrl ? ` PR: ${prUrl}` : "";
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3730
|
+
const nextAction = (checkpoint?: { branch: string; sha: string }): string => {
|
|
3731
|
+
if (durableWake)
|
|
3732
|
+
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"}.`;
|
|
3733
|
+
const reconcile = checkpoint
|
|
3734
|
+
? `verify \`${checkpoint.sha.slice(0, 7)}\` is on \`${checkpoint.branch}\`, then reconcile the remote branch and pull request if needed`
|
|
3735
|
+
: prUrl
|
|
3736
|
+
? `verify the remote branch and pull request (${prUrl}) agree, then reconcile them if needed`
|
|
3737
|
+
: "verify the remote branch and reconcile any saved checkpoint if needed";
|
|
3738
|
+
const restart = s.input.generated
|
|
3739
|
+
? `then start ship again with ${prUrl ?? "the original task"}`
|
|
3740
|
+
: "then start this plan again";
|
|
3741
|
+
const blocked = s.input.generated ? "" : "The unit's dependents in this plan stay blocked. ";
|
|
3742
|
+
return `${blocked}Next action: ${reconcile}, ${restart}.`;
|
|
3743
|
+
};
|
|
3523
3744
|
const declined = [...(s.dispositionsByRound[e.reviewRounds - 1] ?? [])].filter((d) => d.disposition === "declined");
|
|
3524
3745
|
const declinedLine = `Declined findings: ${declined.length > 0 ? declined.map((d) => `${d.findingId}${d.note ? ` — ${d.note}` : ""}`).join("; ") : "none"}`;
|
|
3525
3746
|
const verdictLine = `Verdict: LGTM${s.lastVerdictSummary ? ` — ${s.lastVerdictSummary}` : ""}`;
|
|
@@ -3544,7 +3765,7 @@ export function renderUnitReport(
|
|
|
3544
3765
|
const checkpointLine = (checkpoint: { branch: string; sha: string } | undefined) =>
|
|
3545
3766
|
checkpoint === undefined
|
|
3546
3767
|
? undefined
|
|
3547
|
-
: `The
|
|
3768
|
+
: `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
3769
|
switch (e.kind) {
|
|
3549
3770
|
case "merged":
|
|
3550
3771
|
if (e.by === "other")
|
|
@@ -3605,9 +3826,9 @@ export function renderUnitReport(
|
|
|
3605
3826
|
if (e.cause === "draft") {
|
|
3606
3827
|
const link = e.pr !== undefined ? ` — ${e.pr.url}` : "";
|
|
3607
3828
|
if (!shows(verbosity, "verbose")) return `⏸️ Held: draft — GitHub still marks the pull request as draft${link}`;
|
|
3608
|
-
const draftContinuation =
|
|
3609
|
-
|
|
3610
|
-
:
|
|
3829
|
+
const draftContinuation = `Next action: mark the pull request ready in GitHub, then ${nextAction()
|
|
3830
|
+
.replace(/^The unit's dependents in this plan stay blocked\. Next action: /, "")
|
|
3831
|
+
.replace(/^Next action: /, "")}`;
|
|
3611
3832
|
return join([
|
|
3612
3833
|
`⏸️ 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
3834
|
draftContinuation,
|
|
@@ -3623,7 +3844,9 @@ export function renderUnitReport(
|
|
|
3623
3844
|
return join([
|
|
3624
3845
|
`⏸️ 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
3846
|
writeUpPointer(s, e.round.kind, e.runId),
|
|
3626
|
-
|
|
3847
|
+
durableWake
|
|
3848
|
+
? `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()}`
|
|
3849
|
+
: `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
3850
|
]);
|
|
3628
3851
|
}
|
|
3629
3852
|
// A human-gated row is a question to a person. The enclosing idle keeps
|
|
@@ -3651,8 +3874,8 @@ export function renderUnitReport(
|
|
|
3651
3874
|
return join([
|
|
3652
3875
|
`⚠️ 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
3876
|
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
|
|
3877
|
+
? `Next action: merge or close the pull request, then start ship again with ${e.pr.url} so the result is reconciled.`
|
|
3878
|
+
: "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
3879
|
]);
|
|
3657
3880
|
case "round_cap": {
|
|
3658
3881
|
// The quiet copy counts the last review's open findings, only the
|
|
@@ -3671,7 +3894,7 @@ export function renderUnitReport(
|
|
|
3671
3894
|
failedChecks.length > 0
|
|
3672
3895
|
? `🧢 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
3896
|
: `🧢 Ship stopped at a cap: the ${e.maxRounds}-round cap — no approval after ${rounds}.${prLine}`;
|
|
3674
|
-
return join([headline, splitReport(s),
|
|
3897
|
+
return join([headline, splitReport(s), nextAction()]);
|
|
3675
3898
|
}
|
|
3676
3899
|
case "wall_clock_cap":
|
|
3677
3900
|
if (!shows(verbosity, "verbose")) return `🧢 Out of budget — no approval after ${rounds}.${prLine}`;
|
|
@@ -3679,13 +3902,13 @@ export function renderUnitReport(
|
|
|
3679
3902
|
`🧢 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
3903
|
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
3681
3904
|
splitReport(s),
|
|
3682
|
-
|
|
3905
|
+
nextAction(),
|
|
3683
3906
|
]);
|
|
3684
3907
|
case "review_pending":
|
|
3685
3908
|
return join([
|
|
3686
3909
|
`⏳ 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
3910
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
3688
|
-
aside(
|
|
3911
|
+
aside(nextAction()),
|
|
3689
3912
|
]);
|
|
3690
3913
|
case "stopped":
|
|
3691
3914
|
if (!shows(verbosity, "verbose"))
|
|
@@ -3701,12 +3924,50 @@ export function renderUnitReport(
|
|
|
3701
3924
|
e.postedReview
|
|
3702
3925
|
? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
|
|
3703
3926
|
: undefined,
|
|
3704
|
-
|
|
3927
|
+
nextAction(e.checkpoint),
|
|
3705
3928
|
]);
|
|
3706
3929
|
case "aborted":
|
|
3930
|
+
if (e.findingsStop !== undefined) {
|
|
3931
|
+
const observed = e.observedHead?.slice(0, 7);
|
|
3932
|
+
const branch = s.input.unit.branch;
|
|
3933
|
+
if (e.findingsStop === "incomplete_outputs")
|
|
3934
|
+
return join([
|
|
3935
|
+
`⚠️ Review did not restart: the completed findings work did not record every required result (${(e.missingOutputs ?? []).join("; ")}).`,
|
|
3936
|
+
observed !== undefined
|
|
3937
|
+
? `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.`
|
|
3938
|
+
: "Saved-work fact: The run did not record a final commit, and remote state was not checked.",
|
|
3939
|
+
"Next action: a new findings run must record every disposition, the updated pull request description and its final commit before remote reconciliation.",
|
|
3940
|
+
]);
|
|
3941
|
+
if (e.findingsStop === "unfinished")
|
|
3942
|
+
return join([
|
|
3943
|
+
"⚠️ Review did not restart: the findings work did not complete.",
|
|
3944
|
+
observed !== undefined
|
|
3945
|
+
? `Saved-work fact: the stopped run recorded \`${observed}\`, but Switchboard did not verify it on the remote branch or pull request.`
|
|
3946
|
+
: "Saved-work fact: no completed commit was recorded, and remote state was not checked.",
|
|
3947
|
+
"Next action: start a new findings run; review can begin only after it completes every required result and Switchboard verifies its exact remote head.",
|
|
3948
|
+
]);
|
|
3949
|
+
if (e.findingsStop === "missing_remote")
|
|
3950
|
+
return join([
|
|
3951
|
+
`⚠️ Review did not restart: The completed changes ended at \`${observed}\`, but no open pull request was found for \`${branch}\`.`,
|
|
3952
|
+
`Saved-work fact: The run recorded completed commit \`${observed}\`; Switchboard did not verify that commit on the remote branch or a pull request.`,
|
|
3953
|
+
"Next action: reconcile the branch and open pull request, then start ship again so it can verify the exact head before review.",
|
|
3954
|
+
]);
|
|
3955
|
+
if (e.findingsStop === "head_mismatch")
|
|
3956
|
+
return join([
|
|
3957
|
+
`⚠️ Review did not restart: The completed changes ended at \`${observed}\`, but the pull request is at \`${e.remoteHead?.slice(0, 7)}\` (${s.pr?.url}).`,
|
|
3958
|
+
"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.",
|
|
3959
|
+
"Next action: reconcile the pull request to the completed commit, then start ship again so it can verify that exact head before review.",
|
|
3960
|
+
]);
|
|
3961
|
+
return join([
|
|
3962
|
+
`⚠️ Review did not restart: The pull request exists (${s.pr?.url}), but its exact head could not be verified.`,
|
|
3963
|
+
`Saved-work fact: the run recorded completed commit \`${observed}\`; Switchboard did not start review for \`${observed}\` and cannot claim the pull request contains it.`,
|
|
3964
|
+
"Next action: retry ship when the pull request state is readable so it can verify the exact head before review.",
|
|
3965
|
+
]);
|
|
3966
|
+
}
|
|
3707
3967
|
if (!shows(verbosity, "verbose")) return `⚠️ Aborted after ${rounds}: ${e.reason}${prLine}`;
|
|
3708
3968
|
return join([
|
|
3709
3969
|
e.reason,
|
|
3970
|
+
checkpointLine(e.checkpoint),
|
|
3710
3971
|
writeUpPointer(
|
|
3711
3972
|
s,
|
|
3712
3973
|
e.round?.kind ?? "coding",
|
|
@@ -3714,7 +3975,7 @@ export function renderUnitReport(
|
|
|
3714
3975
|
),
|
|
3715
3976
|
e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
|
|
3716
3977
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
3717
|
-
|
|
3978
|
+
nextAction(e.checkpoint),
|
|
3718
3979
|
]);
|
|
3719
3980
|
case "continued":
|
|
3720
3981
|
// A segment boundary is the runner continuing — an acknowledgement, verbose
|
|
@@ -3723,8 +3984,8 @@ export function renderUnitReport(
|
|
|
3723
3984
|
if (!shows(verbosity, "verbose")) return "";
|
|
3724
3985
|
return join([
|
|
3725
3986
|
writeUpPointer(s, e.round.kind, e.runId),
|
|
3726
|
-
|
|
3727
|
-
? `🔁 The unit's budget ran out with the unit unfinished — ${e.line}.
|
|
3987
|
+
durableWake
|
|
3988
|
+
? `🔁 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
3989
|
: `🔁 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
3990
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
3730
3991
|
]);
|
|
@@ -3735,7 +3996,7 @@ export function renderUnitReport(
|
|
|
3735
3996
|
`⚠️ 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
3997
|
writeUpPointer(s, e.round.kind, s.lastCodingRunId),
|
|
3737
3998
|
`⚠️ This is a bug: ship ended after ${rounds} because its automatic provider retry was spent.`,
|
|
3738
|
-
|
|
3999
|
+
nextAction(),
|
|
3739
4000
|
]);
|
|
3740
4001
|
case "no_verdict":
|
|
3741
4002
|
if (!shows(verbosity, "verbose"))
|
|
@@ -3744,20 +4005,20 @@ export function renderUnitReport(
|
|
|
3744
4005
|
`⚠️ 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
4006
|
writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index]),
|
|
3746
4007
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
3747
|
-
|
|
4008
|
+
nextAction(),
|
|
3748
4009
|
]);
|
|
3749
4010
|
case "interrupted":
|
|
3750
|
-
return join([shipInterruptedNote(prUrl, e.cause,
|
|
4011
|
+
return join([shipInterruptedNote(prUrl, e.cause, durableWake), checkpointLine(e.checkpoint)]);
|
|
3751
4012
|
case "idle_expired":
|
|
3752
4013
|
return "⌛ Idle expired: no reply continued this unit before its idle window closed.";
|
|
3753
4014
|
case "refused":
|
|
3754
4015
|
return join([
|
|
3755
4016
|
`🚫 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(
|
|
4017
|
+
aside(nextAction()),
|
|
3757
4018
|
]);
|
|
3758
4019
|
case "idle":
|
|
3759
|
-
//
|
|
3760
|
-
//
|
|
3761
|
-
return
|
|
4020
|
+
// Keep the old kind's outcome at this copy's level, but render the
|
|
4021
|
+
// indexed wake's durable reply action instead of a terminal restart.
|
|
4022
|
+
return renderUnitReportWithWake({ ...s, ending: e.idled }, facts, verbosity, true);
|
|
3762
4023
|
}
|
|
3763
4024
|
}
|
|
@@ -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
|
: "";
|