@coreplane/switchboard 1.260.3 → 1.260.5
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 +205 -8
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +11 -2
- 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 +7 -6
- package/dist/assets/project.json +4 -0
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/budgets.ts +5 -0
- package/dist/assets/src/core/coordinator/contract.ts +5 -0
- package/dist/assets/src/core/coordinator/driver.ts +118 -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 +1 -0
- package/dist/assets/src/core/runEvents.ts +8 -0
- package/dist/assets/src/core/runFriction.ts +1 -1
- package/dist/assets/src/core/runLedger/types.ts +10 -1
- package/dist/assets/src/core/runRecord.ts +31 -0
- package/dist/assets/src/core/ship/contract.ts +3 -1
- package/dist/assets/src/core/ship/coordinator.ts +518 -56
- package/dist/assets/src/core/ship/renewal.ts +3 -1
- package/dist/assets/src/core/types.ts +6 -0
- package/dist/assets/src/execution/bashTimeout.ts +1 -1
- package/dist/assets/src/execution/residentRebind.ts +15 -0
- package/dist/assets/src/execution/residentReuse.ts +23 -2
- package/dist/assets/src/execution/sandboxErrors.ts +6 -6
- package/dist/assets/web/dist/.vite/manifest.json +505 -505
- package/dist/assets/web/dist/assets/{AppShell-CSXDCV4x.js → AppShell-DPJVhUGP.js} +1 -1
- package/dist/assets/web/dist/assets/{CostChart-DsuPS2g7.js → CostChart-4TeDdbvJ.js} +1 -1
- package/dist/assets/web/dist/assets/CostsPage-8HDgvuWB.js +1 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-62qCSLvk.js → DeliveryPage-Cf1joUyA.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-Dk3HRBc3.js → HomePage-DZEAnCCv.js} +1 -1
- package/dist/assets/web/dist/assets/{InputMenu-BdZXnibG.js → InputMenu-BRV35UWP.js} +1 -1
- package/dist/assets/web/dist/assets/{MetricsPage-o5fSaCTe.js → MetricsPage-BQBuIuYU.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-DHuWyjlq.js → NotFoundPage-CDjMfCcj.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-CeTGOXUy.js → PendingTurnRow-4B8JwETU.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-RD0M6O6W.js → PlanePage-CHU2szQs.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Dl2KId95.js → ResidentDetailPage-L5SRcN_O.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-DBy43I1R.js → ResidentsIndexPage-BIFf8qyE.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DzPIshJx.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-Du1HqLZs.js → RunRoutePage-Cru5IVux.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-BZphy3Hr.js → RunsIndexPage-CBkdPAdj.js} +1 -1
- package/dist/assets/web/dist/assets/{RunsTabs-kHbbxVHx.js → RunsTabs-C7Ua3XX5.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-B7jh898t.js → ScheduledPage-D_R55BDk.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingSelect-BfID5nF4.js → SettingSelect-CEeA4iuC.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-3uLEsiiE.js → SettingsPage-DlmZvAvf.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-CKxSxnQC.js → SilentTurn-PWMDMbMG.js} +1 -1
- package/dist/assets/web/dist/assets/{SlackMark-CtiHrNjr.js → SlackMark-DB0MGiF5.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-x7vcK0iE.js → StatusDot-Bq0mYMFo.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-wVXCWhMp.js → Tooltip-Bs6zd7yE.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-V5BXK7FH.js → UnitRoutePage-BMGEZCvn.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-C8b5YTHf.js → angular-html-BRRhsdQE.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-C3f4WYNe.js → angular-ts-BFj-mqCm.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-DSHxUJc3.js → apl-CsBuy1IL.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-BSGCCBP6.js → astro-cVaqxBZq.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-DFeYrMKk.js → blade-MEP5mWha.js} +1 -1
- package/dist/assets/web/dist/assets/{browser-B6T5hjfR.js → browser-CCImen5O.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-0szUBIMv.js +1 -0
- package/dist/assets/web/dist/assets/{c-L_QOnFVv.js → c-D4NGQ_Pd.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DLQWUwhu.js → chapel-BkWImsIy.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-1DlMmrEO.js → cobol-RPDSPSET.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-BTB6os6N.js → coffee-DZ5z7oVX.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-BvJI2z3u.js → cpp-CrBtIaRZ.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-B11wGW_h.js → crystal-DJesYi6f.js} +1 -1
- package/dist/assets/web/dist/assets/{css-BiIWx5Pw.js → css-DO9mjau-.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-C-XGnvM4.js → dist-EkKVbIM-.js} +12 -12
- package/dist/assets/web/dist/assets/{edge-nSglEXaD.js → edge-C3VzbSW0.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-x7o8qMl8.js → elixir-BkD_Vhfe.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DGrRyqTQ.js → elm-KIM0mFPk.js} +1 -1
- package/dist/assets/web/dist/assets/{erb--_dh58Nw.js → erb-D2ruCqb2.js} +1 -1
- package/dist/assets/web/dist/assets/{eventSource-Bj_dZf6M.js → eventSource-Cv0AFlMi.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-DbpUXwyg.js → git-rebase-CnfbQTxA.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-BG0CuZG5.js → glimmer-js-CbiwFXWH.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CQIl5qyR.js → glimmer-ts-DV8ii0Pv.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-9PJNQTcw.js → glsl-CgmznWkA.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-CzVTzjEI.js → graphql-BK-bBYfH.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BhCmAz3Q.js → hack-DnwTTP5n.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-BmI12COR.js → haml-DAVJsWwT.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-Cib-ISTS.js → handlebars-voOVfxFL.js} +1 -1
- package/dist/assets/web/dist/assets/{html-D3lDapvI.js → html-3FTqwaPk.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DRPY8vOK.js → html-derivative-CSlhLDQn.js} +1 -1
- package/dist/assets/web/dist/assets/{http-B9VttK6m.js → http-Vx4ilPfS.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-C0saNJYj.js → hurl-DxyZDE3R.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-Dxg7C8s9.js → indexRow-5s3hNzZ9.js} +1 -1
- package/dist/assets/web/dist/assets/{java-BCFbtYAj.js → java-nWqdjNJG.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-nwBjnrhh.js → javascript-Dr82HOwp.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-CMYO9byN.js → jinja-BhjLrPOt.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-Cko8HnE_.js → jison-DilGIUa8.js} +1 -1
- package/dist/assets/web/dist/assets/{json-Dzv__YsX.js → json-CrOcvmhJ.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CTFxagih.js → jsx-BW_RX-fo.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-_Xvten1-.js → julia-CWH071DB.js} +1 -1
- package/dist/assets/web/dist/assets/{just-bLps4qOu.js → just-DkhhoWBy.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-D6UaYsUP.js → latex-cuHJ4TUT.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-CoDIeULa.js → liquid-DG6afYQN.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-D1KFRnVf.js → lua-C5CMnCPS.js} +1 -1
- package/dist/assets/web/dist/assets/{main-DAL--hyj.js → main-Disot0n_.js} +3 -3
- package/dist/assets/web/dist/assets/{marko-B5N5XcOw.js → marko-Dy2_DuC4.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-uxzjQO3q.js → mdc-BMKb_EeA.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-BMJxs2TK.js → nginx-og0RgMKf.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-DqQ3-8p1.js → nim-CQxTQQP0.js} +1 -1
- package/dist/assets/web/dist/assets/{org-DsY6LK59.js → org-b5lkq_Pl.js} +1 -1
- package/dist/assets/web/dist/assets/{perl--pq9iJFp.js → perl-D9X5_i_J.js} +1 -1
- package/dist/assets/web/dist/assets/{php-RHCMbsM2.js → php-CwUxvLX0.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-CFNq_FCp.js → pug-uOIOExr0.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-C9Acx9E8.js → qml-g5RoADf-.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DxlU8FBd.js → r-5LdAopD3.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-CLJ2kKih.js → razor-DwrvFLYQ.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CzmdGoEp.js → regexp-dGtAaxC-.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-D9yuxBET.js → rst-wFdW0brr.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-CIRcEvnD.js → ruby-DPglN6I9.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-LOl-uUPh.js → sas-CUbCs40d.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-BX2_sZHY.js → scss-B-DUiI1c.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-4d-QvKkY.js → shellscript-lGOV7Ram.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-DhB16UGy.js → shellsession-BDWtAAjr.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-Cm1xTUBr.js → soy-zod_dcOy.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-BsfcBJdu.js → sql-BG0HSSoM.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-BqgOggyQ.js → sseReplay-9gcMpylx.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-Dwat3BSw.js → stata-BVUMLp1W.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-P4rtxzAW.js → surrealql-DiSUbHKF.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DTcsi8S8.js → svelte-DZenT45Y.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-D-kP0Tg5.js → templ-BWKRNM0-.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-D-4vuedK.js → tex-CbMzBlkK.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-jEEWYxsu.js → ts-tags-CZsVorDN.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-CFnlrVzT.js → tsx-R1r3mH4W.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-D5rGrPjs.js → twig-BjPMyjKH.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-nYIQ35or.js → typescript-Bx-66VN_.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-3-UX8dna.js → typst-fW037Eo6.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CLIPIX03.js → vue-D1V_GNxW.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-Ci9CpkHU.js → vue-html-C-4pTZ-s.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-DL5pEYYk.js → vue-vine-C-QsX3hR.js} +1 -1
- package/dist/assets/web/dist/assets/{wallClock-DmaC1sKA.js → wallClock-x0hjr0J_.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-bO80qikC.js → xml-BJTJXPgC.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-BPze60Ej.js → xsl-Xc9lukqe.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BQUSon_o.js → yaml-Dw-FMsQa.js} +1 -1
- package/dist/cli.js +1988 -546
- package/package.json +4 -4
- package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CwHDL-Mo.js +0 -1
- package/dist/assets/web/dist/assets/budgets-KNNkT4PZ.js +0 -1
|
@@ -137,11 +137,11 @@ export function shipInterruptedNote(prUrl?: string, cause?: InterruptionCause, i
|
|
|
137
137
|
const stands = prUrl
|
|
138
138
|
? `Its work stands on GitHub: ${prUrl}.`
|
|
139
139
|
: "Whatever it pushed stands on its pipeline branch; no PR was opened yet.";
|
|
140
|
-
const
|
|
141
|
-
? "
|
|
140
|
+
const continuation = idle
|
|
141
|
+
? "The next reply in this thread continues the unit."
|
|
142
142
|
: prUrl
|
|
143
|
-
? `
|
|
144
|
-
: "
|
|
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
145
|
const opening =
|
|
146
146
|
cause === "container_replaced"
|
|
147
147
|
? "⚠️ The resident container running this pipeline's child was replaced (a deploy's image swap) and the child could not resume, so the pipeline stopped."
|
|
@@ -150,7 +150,7 @@ export function shipInterruptedNote(prUrl?: string, cause?: InterruptionCause, i
|
|
|
150
150
|
: cause === "bot_restart"
|
|
151
151
|
? "⚠️ The bot restarted while this ship pipeline was running, so the pipeline stopped."
|
|
152
152
|
: "⚠️ This ship pipeline's child was interrupted and could not resume, so the pipeline stopped.";
|
|
153
|
-
return `${opening} ${stands} ${
|
|
153
|
+
return `${opening} ${stands} ${continuation}`;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// ---- the plan graph --------------------------------------------------------------------------------
|
|
@@ -551,6 +551,10 @@ export type CoordinatorAction =
|
|
|
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
553
|
| { type: "read-record"; step: string; runId: string }
|
|
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",
|
|
@@ -1121,6 +1171,36 @@ type Phase =
|
|
|
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
1173
|
| { at: "read"; round: RoundRef; runId: string; n: number; until: number }
|
|
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
|
+
}
|
|
1201
|
+
/** A moved-head child has drained. Re-read the adopted pull request before
|
|
1202
|
+
* restarting review because it may have merged, closed or moved again. */
|
|
1203
|
+
| { at: "superseded-pr-check"; round: RoundRef }
|
|
1124
1204
|
| {
|
|
1125
1205
|
at: "pr-check";
|
|
1126
1206
|
round: RoundRef;
|
|
@@ -1131,6 +1211,10 @@ type Phase =
|
|
|
1131
1211
|
childPushed?: PushedHeadFact[];
|
|
1132
1212
|
childLeaseStartedAt?: number;
|
|
1133
1213
|
childHandoff?: Handoff;
|
|
1214
|
+
/** Open or recover a pull request from the branch. A completed child
|
|
1215
|
+
* whose same-head salvage merely republished ready work needs this path
|
|
1216
|
+
* without being classified as dead. */
|
|
1217
|
+
recover?: true;
|
|
1134
1218
|
/** The coding child died (`failed` or `interrupted`) after it may have
|
|
1135
1219
|
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
1136
1220
|
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
@@ -1148,6 +1232,9 @@ type Phase =
|
|
|
1148
1232
|
* queue's outcome instead of attempting the squash again. */
|
|
1149
1233
|
| { at: "merge"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number; queued?: true }
|
|
1150
1234
|
| { at: "rebase"; pr: PrRef; headSha: string; n: number }
|
|
1235
|
+
/** A rebase can run long enough for the pull request to change again. Both
|
|
1236
|
+
* dispatching outcomes stop here for one fresh state/head/ref read. */
|
|
1237
|
+
| { at: "rebase-pr-check"; pr: PrRef; n: number; outcome: "conflict" | "changed" }
|
|
1151
1238
|
| { at: "merge-wait"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number; queued?: true }
|
|
1152
1239
|
/** The round's checks step (record 0055): the check runs at the reviewed head
|
|
1153
1240
|
* are read after an approve settles, before merge_ready or the merge door.
|
|
@@ -1196,6 +1283,9 @@ export interface UnitPipelineState {
|
|
|
1196
1283
|
/** Approved-head rebases attempted in this run. Each fresh conflict may buy
|
|
1197
1284
|
* one fix round; the run's remaining lease, not pull-request lifetime, is the bound. */
|
|
1198
1285
|
readonly rebaseAttempts: number;
|
|
1286
|
+
/** Review dispatches superseded by an external head move. The count gives
|
|
1287
|
+
* each restarted round a fresh durable `/a<n>` step identity. */
|
|
1288
|
+
readonly reviewRestarts: number;
|
|
1199
1289
|
readonly pr?: PrRef;
|
|
1200
1290
|
readonly lastReviewHead?: string;
|
|
1201
1291
|
readonly lastVerdictSummary?: string;
|
|
@@ -1234,6 +1324,10 @@ export const WAIT_MARGIN_MS = SHIP_WAIT.marginMinutes * MIN;
|
|
|
1234
1324
|
* margin and the merge poll are the same number), and it keeps a round to a
|
|
1235
1325
|
* few steps: a coding child's 90 minutes are eighteen waits and eighteen reads. */
|
|
1236
1326
|
export const WAIT_CHUNK_MS = SHIP_WAIT.chunkMinutes * MIN;
|
|
1327
|
+
/** A shipped unit's child is polled more often than an unshipped coding round:
|
|
1328
|
+
* a person's merge/close must reach its inbox before a minutes-long review or
|
|
1329
|
+
* fix can publish against a terminal pull request. */
|
|
1330
|
+
export const PR_TRANSITION_GUARD_MS = MIN;
|
|
1237
1331
|
/** The merge wait is a round of its own: its minutes are carved from the
|
|
1238
1332
|
* pipeline's remainder when the door is first asked (the merge wait's ask and
|
|
1239
1333
|
* floor are rows of `src/core/budgets.ts`), and the wait below is sliced
|
|
@@ -1257,6 +1351,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
1257
1351
|
phase: { at: "pre-check" },
|
|
1258
1352
|
reviewRounds: pending?.round ?? 0,
|
|
1259
1353
|
rebaseAttempts: 0,
|
|
1354
|
+
reviewRestarts: 0,
|
|
1260
1355
|
spentMs: { coding: 0, review: 0, waiting: 0 },
|
|
1261
1356
|
spendUsd: input.session?.spendUsd ?? 0,
|
|
1262
1357
|
findingsByRound: pending ? { [pending.round]: pending.findings } : {},
|
|
@@ -1271,7 +1366,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
1271
1366
|
}
|
|
1272
1367
|
: {}),
|
|
1273
1368
|
};
|
|
1274
|
-
if (pending !== undefined) return
|
|
1369
|
+
if (pending !== undefined) return base;
|
|
1275
1370
|
if (!input.resume) return base;
|
|
1276
1371
|
const url = input.resume.url ?? `https://github.com/${input.repo}/pull/${input.resume.pr}`;
|
|
1277
1372
|
const resumed: UnitPipelineState = {
|
|
@@ -1279,7 +1374,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
1279
1374
|
pr: { number: input.resume.pr, url },
|
|
1280
1375
|
...(input.resume.headSha !== undefined ? { lastReviewHead: input.resume.headSha } : {}),
|
|
1281
1376
|
};
|
|
1282
|
-
return
|
|
1377
|
+
return resumed;
|
|
1283
1378
|
}
|
|
1284
1379
|
|
|
1285
1380
|
const deadlineAt = (s: UnitPipelineState) =>
|
|
@@ -1406,7 +1501,12 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
1406
1501
|
const p = s.phase;
|
|
1407
1502
|
switch (p.at) {
|
|
1408
1503
|
case "pre-check":
|
|
1409
|
-
return {
|
|
1504
|
+
return {
|
|
1505
|
+
type: "pr-check",
|
|
1506
|
+
step: `${unit}/pr-check`,
|
|
1507
|
+
entry: true,
|
|
1508
|
+
...(s.pr !== undefined ? { pr: s.pr.number } : {}),
|
|
1509
|
+
};
|
|
1410
1510
|
case "branch":
|
|
1411
1511
|
return { type: "branch", step: `${unit}/branch`, branch: s.input.unit.branch, from: s.input.base };
|
|
1412
1512
|
case "spawn": {
|
|
@@ -1430,15 +1530,41 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
1430
1530
|
type: "wait",
|
|
1431
1531
|
step: `${roundStep(s, p.round)}/wait/${p.n}`,
|
|
1432
1532
|
runId: p.runId,
|
|
1433
|
-
timeoutMs: waitSliceMs(s.clock, p.until),
|
|
1533
|
+
timeoutMs: Math.min(waitSliceMs(s.clock, p.until), s.pr !== undefined ? PR_TRANSITION_GUARD_MS : WAIT_CHUNK_MS),
|
|
1434
1534
|
};
|
|
1435
1535
|
case "read":
|
|
1436
1536
|
return { type: "read-record", step: `${roundStep(s, p.round)}/read/${p.n}`, runId: p.runId };
|
|
1537
|
+
case "steer":
|
|
1538
|
+
return {
|
|
1539
|
+
type: "steer",
|
|
1540
|
+
step: `${roundStep(s, p.round)}/steer/${p.n}`,
|
|
1541
|
+
runId: p.runId,
|
|
1542
|
+
reason: p.supersession.reason,
|
|
1543
|
+
};
|
|
1544
|
+
case "superseded-wait":
|
|
1545
|
+
return {
|
|
1546
|
+
type: "wait",
|
|
1547
|
+
step: `${roundStep(s, p.round)}/superseded/wait/${p.n}`,
|
|
1548
|
+
runId: p.runId,
|
|
1549
|
+
timeoutMs: waitSliceMs(s.clock, p.until),
|
|
1550
|
+
};
|
|
1551
|
+
case "superseded-read":
|
|
1552
|
+
return {
|
|
1553
|
+
type: "read-record",
|
|
1554
|
+
step: `${roundStep(s, p.round)}/superseded/read/${p.n}`,
|
|
1555
|
+
runId: p.runId,
|
|
1556
|
+
};
|
|
1557
|
+
case "superseded-pr-check":
|
|
1558
|
+
return {
|
|
1559
|
+
type: "pr-check",
|
|
1560
|
+
step: `${roundStep(s, p.round)}/superseded/pr-check`,
|
|
1561
|
+
...(s.pr !== undefined ? { pr: s.pr.number } : {}),
|
|
1562
|
+
};
|
|
1437
1563
|
case "pr-check":
|
|
1438
1564
|
return {
|
|
1439
1565
|
type: "pr-check",
|
|
1440
1566
|
step: `${roundStep(s, p.round)}/pr-check`,
|
|
1441
|
-
...(p.dead !== undefined ? { recover: { runId: p.runId } } : {}),
|
|
1567
|
+
...(p.dead !== undefined || p.recover === true ? { recover: { runId: p.runId } } : {}),
|
|
1442
1568
|
// The adopted pull request rides the check so the bot can follow it
|
|
1443
1569
|
// when nothing heads the unit's branch (issue 1799).
|
|
1444
1570
|
...(s.pr !== undefined ? { pr: s.pr.number } : {}),
|
|
@@ -1474,6 +1600,12 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
1474
1600
|
prNumber: p.pr.number,
|
|
1475
1601
|
headSha: p.headSha,
|
|
1476
1602
|
};
|
|
1603
|
+
case "rebase-pr-check":
|
|
1604
|
+
return {
|
|
1605
|
+
type: "pr-check",
|
|
1606
|
+
step: `${unit}/rebase/${p.n}/pr-check`,
|
|
1607
|
+
pr: p.pr.number,
|
|
1608
|
+
};
|
|
1477
1609
|
case "merge-wait":
|
|
1478
1610
|
return {
|
|
1479
1611
|
type: "wait-checks",
|
|
@@ -1522,7 +1654,10 @@ function idleEnding(s: UnitPipelineState, ending: UnitEnding): Extract<UnitEndin
|
|
|
1522
1654
|
// waits for the person's word. Human-gated questions bypass this mapping:
|
|
1523
1655
|
// `parkHumanGate` always parks them with their typed pending state.
|
|
1524
1656
|
if (ending.kind === "held" && ending.cause !== "blocked") return undefined;
|
|
1525
|
-
const old = ending as Exclude<
|
|
1657
|
+
const old = ending as Exclude<
|
|
1658
|
+
UnitEnding,
|
|
1659
|
+
{ kind: "idle" | "merged" | "closed" | "already_landed" | "merge_ready" | "refused" }
|
|
1660
|
+
>;
|
|
1526
1661
|
const grant = s.input.grant ?? DEFAULT_GRANT;
|
|
1527
1662
|
// Unspent: an idle spends no renewal — the wake's segment does (this plan's
|
|
1528
1663
|
// fifth unit) — so the row says what the grant still holds.
|
|
@@ -1688,14 +1823,37 @@ function nextReview(s: UnitPipelineState, notes: CoordinatorNote[] = []): Transi
|
|
|
1688
1823
|
const stopMode = (status: RunStatus): "soft" | "hard" | undefined =>
|
|
1689
1824
|
status === "stopped_soft" ? "soft" : status === "stopped_hard" ? "hard" : undefined;
|
|
1690
1825
|
|
|
1826
|
+
/** The last mechanical salvage push to this unit branch. */
|
|
1827
|
+
function salvagePush(s: UnitPipelineState, pushed: readonly PushedHeadFact[] | undefined): PushedHeadFact | undefined {
|
|
1828
|
+
return pushed?.filter((p) => p.ref === s.input.unit.branch && p.by === "salvage").at(-1);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/** A completed child independently witnessed at the same head and carrying its
|
|
1832
|
+
* handoff already declared the work ready; salvage merely republished its commit. */
|
|
1833
|
+
function completedSameHeadSalvage(
|
|
1834
|
+
s: UnitPipelineState,
|
|
1835
|
+
pushed: readonly PushedHeadFact[] | undefined,
|
|
1836
|
+
child?: { status: RunStatus; handoff?: boolean; headSha?: string },
|
|
1837
|
+
): boolean {
|
|
1838
|
+
if (child === undefined) return false;
|
|
1839
|
+
const found = salvagePush(s, pushed);
|
|
1840
|
+
const salvageHead = normalizeHead(found?.sha);
|
|
1841
|
+
const childHead = normalizeHead(child.headSha);
|
|
1842
|
+
return (
|
|
1843
|
+
child.status === "completed" && child.handoff === true && salvageHead !== undefined && salvageHead === childHead
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1691
1847
|
/** The last mechanical WIP push to this unit branch. Unlike an ordinary push,
|
|
1692
|
-
*
|
|
1848
|
+
* it says the child ended before its work was ready for review. */
|
|
1693
1849
|
function interruptedCheckpoint(
|
|
1694
1850
|
s: UnitPipelineState,
|
|
1695
1851
|
pushed: readonly PushedHeadFact[] | undefined,
|
|
1852
|
+
child?: { status: RunStatus; handoff?: boolean; headSha?: string },
|
|
1696
1853
|
): { branch: string; sha: string } | undefined {
|
|
1697
|
-
const found =
|
|
1698
|
-
|
|
1854
|
+
const found = salvagePush(s, pushed);
|
|
1855
|
+
if (found === undefined || completedSameHeadSalvage(s, pushed, child)) return undefined;
|
|
1856
|
+
return { branch: found.ref, sha: found.sha };
|
|
1699
1857
|
}
|
|
1700
1858
|
|
|
1701
1859
|
/** A coding run's confirmed end: round 0's child, or the run a findings step dispatched. */
|
|
@@ -1725,7 +1883,8 @@ function settleCoding(
|
|
|
1725
1883
|
}
|
|
1726
1884
|
: {}),
|
|
1727
1885
|
};
|
|
1728
|
-
const
|
|
1886
|
+
const readySalvage = completedSameHeadSalvage(next, facts.pushed, facts);
|
|
1887
|
+
const checkpoint = interruptedCheckpoint(next, facts.pushed, facts);
|
|
1729
1888
|
const mode = stopMode(facts.status);
|
|
1730
1889
|
if (mode !== undefined)
|
|
1731
1890
|
return end(
|
|
@@ -1755,7 +1914,7 @@ function settleCoding(
|
|
|
1755
1914
|
next,
|
|
1756
1915
|
{
|
|
1757
1916
|
kind: "aborted",
|
|
1758
|
-
reason: `⚠️ The coding child ended because ${cause}; the branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`.
|
|
1917
|
+
reason: `⚠️ The coding child ended because ${cause}; the branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`. The next reply in this thread resumes from that checkpoint instead of starting over.`,
|
|
1759
1918
|
round,
|
|
1760
1919
|
reviewRounds: next.reviewRounds,
|
|
1761
1920
|
},
|
|
@@ -1787,6 +1946,7 @@ function settleCoding(
|
|
|
1787
1946
|
round,
|
|
1788
1947
|
runId,
|
|
1789
1948
|
...(facts.headSha !== undefined ? { childHead: facts.headSha } : {}),
|
|
1949
|
+
...(readySalvage ? { recover: true as const } : {}),
|
|
1790
1950
|
...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
|
|
1791
1951
|
...(facts.pushed !== undefined ? { childPushed: facts.pushed } : {}),
|
|
1792
1952
|
...(facts.leaseStartedAt !== undefined ? { childLeaseStartedAt: facts.leaseStartedAt } : {}),
|
|
@@ -2038,8 +2198,25 @@ function settleChecks(
|
|
|
2038
2198
|
retried?: boolean,
|
|
2039
2199
|
draft?: boolean,
|
|
2040
2200
|
refired?: boolean,
|
|
2201
|
+
pullRequest?: PrCheck,
|
|
2041
2202
|
): Transition {
|
|
2042
2203
|
const { round } = p;
|
|
2204
|
+
if (pullRequest?.state === "merged") return foundMerged(s, pullRequest);
|
|
2205
|
+
if (pullRequest?.state === "closed") return foundClosed(s, pullRequest);
|
|
2206
|
+
if (pullRequest?.state === "open" && pullRequest.headBranchExists === false) return missingHeadBranch(s, pullRequest);
|
|
2207
|
+
if (pullRequest?.state === "open") {
|
|
2208
|
+
const actual = normalizeHead(pullRequest.headSha);
|
|
2209
|
+
if (actual !== undefined && !sameCommit(actual, p.headSha)) {
|
|
2210
|
+
const reviewRestarts = s.reviewRestarts + 1;
|
|
2211
|
+
const next: UnitPipelineState = {
|
|
2212
|
+
...s,
|
|
2213
|
+
reviewRestarts,
|
|
2214
|
+
pr: { number: pullRequest.prNumber, url: pullRequest.url },
|
|
2215
|
+
lastReviewHead: actual,
|
|
2216
|
+
};
|
|
2217
|
+
return enterRound(next, { ...round, attempt: reviewRestarts + 1 });
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2043
2220
|
const wait = (over: Partial<Extract<Phase, { at: "checks-wait" }>>): Transition => ({
|
|
2044
2221
|
state: {
|
|
2045
2222
|
...s,
|
|
@@ -2208,11 +2385,118 @@ function foundMerged(
|
|
|
2208
2385
|
const ref: PrRef = { number: pr.prNumber, url: pr.url };
|
|
2209
2386
|
return end(
|
|
2210
2387
|
{ ...s, pr: ref },
|
|
2211
|
-
{
|
|
2388
|
+
{
|
|
2389
|
+
kind: "merged",
|
|
2390
|
+
by: "other",
|
|
2391
|
+
pr: ref,
|
|
2392
|
+
sha: pr.sha,
|
|
2393
|
+
mergedAt: pr.mergedAt,
|
|
2394
|
+
...(pr.mergedBy !== undefined ? { mergedBy: pr.mergedBy } : {}),
|
|
2395
|
+
reviewRounds: s.reviewRounds,
|
|
2396
|
+
},
|
|
2397
|
+
notes,
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
/** A person closed the unit's pull request without merging it. Closed is a
|
|
2402
|
+
* terminal unit state: no child, check, or merge step may follow. */
|
|
2403
|
+
function foundClosed(
|
|
2404
|
+
s: UnitPipelineState,
|
|
2405
|
+
pr: Extract<PrCheck, { state: "closed" }>,
|
|
2406
|
+
notes: CoordinatorNote[] = [],
|
|
2407
|
+
): Transition {
|
|
2408
|
+
const ref: PrRef = { number: pr.prNumber, url: pr.url };
|
|
2409
|
+
return end(
|
|
2410
|
+
{ ...s, pr: ref },
|
|
2411
|
+
{ kind: "closed", pr: ref, closedBy: pr.closedBy, reviewRounds: s.reviewRounds },
|
|
2412
|
+
notes,
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function missingHeadBranch(
|
|
2417
|
+
s: UnitPipelineState,
|
|
2418
|
+
pr: Extract<PrCheck, { state: "open" }>,
|
|
2419
|
+
notes: CoordinatorNote[] = [],
|
|
2420
|
+
): Transition {
|
|
2421
|
+
const ref: PrRef = { number: pr.prNumber, url: pr.url };
|
|
2422
|
+
return end(
|
|
2423
|
+
{ ...s, pr: ref },
|
|
2424
|
+
{
|
|
2425
|
+
kind: "aborted",
|
|
2426
|
+
reason: `⚠️ ${pr.url} is still open, but its head branch no longer exists; no child was dispatched onto the deleted branch.`,
|
|
2427
|
+
reviewRounds: s.reviewRounds,
|
|
2428
|
+
},
|
|
2212
2429
|
notes,
|
|
2213
2430
|
);
|
|
2214
2431
|
}
|
|
2215
2432
|
|
|
2433
|
+
/** What a fresh pull-request read says about a child already in flight. A
|
|
2434
|
+
* review is pinned, so a moved head supersedes it; a coding child may itself
|
|
2435
|
+
* be moving the head and is superseded only by a terminal or deleted ref. */
|
|
2436
|
+
function childSupersession(
|
|
2437
|
+
s: UnitPipelineState,
|
|
2438
|
+
round: RoundRef,
|
|
2439
|
+
pr: PrCheck | undefined,
|
|
2440
|
+
): ChildSupersession | undefined {
|
|
2441
|
+
if (pr?.state === "merged") return { reason: "merged", pullRequest: pr };
|
|
2442
|
+
if (pr?.state === "closed") return { reason: "closed", pullRequest: pr };
|
|
2443
|
+
if (pr?.state !== "open") return undefined;
|
|
2444
|
+
if (pr.headBranchExists === false) return { reason: "branch_deleted", pullRequest: pr };
|
|
2445
|
+
if (round.kind !== "review") return undefined;
|
|
2446
|
+
const expected = normalizeHead(s.lastReviewHead);
|
|
2447
|
+
const actual = normalizeHead(pr.headSha);
|
|
2448
|
+
if (expected !== undefined && actual !== undefined && !sameCommit(expected, actual))
|
|
2449
|
+
return { reason: "head_moved", pullRequest: pr };
|
|
2450
|
+
return undefined;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
function finishSupersededChild(s: UnitPipelineState, phase: Extract<Phase, { at: "superseded-read" }>): Transition {
|
|
2454
|
+
const supersession = phase.supersession;
|
|
2455
|
+
if (supersession.reason === "merged") return foundMerged(s, supersession.pullRequest);
|
|
2456
|
+
if (supersession.reason === "closed") return foundClosed(s, supersession.pullRequest);
|
|
2457
|
+
if (supersession.reason === "branch_deleted") return missingHeadBranch(s, supersession.pullRequest);
|
|
2458
|
+
return {
|
|
2459
|
+
state: {
|
|
2460
|
+
...s,
|
|
2461
|
+
pr: { number: supersession.pullRequest.prNumber, url: supersession.pullRequest.url },
|
|
2462
|
+
phase: { at: "superseded-pr-check", round: phase.round },
|
|
2463
|
+
},
|
|
2464
|
+
notes: [],
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
function restartSupersededReview(
|
|
2469
|
+
s: UnitPipelineState,
|
|
2470
|
+
round: RoundRef,
|
|
2471
|
+
pr: Extract<PrCheck, { state: "open" }>,
|
|
2472
|
+
): Transition {
|
|
2473
|
+
if (pr.headBranchExists === false) return missingHeadBranch(s, pr);
|
|
2474
|
+
const head = normalizeHead(pr.headSha);
|
|
2475
|
+
if (head === undefined)
|
|
2476
|
+
return end(s, {
|
|
2477
|
+
kind: "aborted",
|
|
2478
|
+
reason: `⚠️ ${pr.url} moved while review was running, but its fresh head could not be read; no child was dispatched on a guessed head.`,
|
|
2479
|
+
reviewRounds: s.reviewRounds,
|
|
2480
|
+
});
|
|
2481
|
+
const reviewRestarts = s.reviewRestarts + 1;
|
|
2482
|
+
const next: UnitPipelineState = {
|
|
2483
|
+
...s,
|
|
2484
|
+
reviewRestarts,
|
|
2485
|
+
pr: { number: pr.prNumber, url: pr.url },
|
|
2486
|
+
lastReviewHead: head,
|
|
2487
|
+
};
|
|
2488
|
+
return enterRound(next, { ...round, attempt: reviewRestarts + 1 });
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
/** Reconcile the ending-time facts read. A person can merge or close after
|
|
2492
|
+
* the machine chose merge-ready but before the driver publishes it; terminal
|
|
2493
|
+
* state wins and becomes the unit's actual ending, not merely report prose. */
|
|
2494
|
+
export function reconcileTerminalPr(s: UnitPipelineState, pr: PrCheck): UnitPipelineState {
|
|
2495
|
+
if (pr.state === "merged") return foundMerged(s, pr).state;
|
|
2496
|
+
if (pr.state === "closed") return foundClosed(s, pr).state;
|
|
2497
|
+
return s;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2216
2500
|
/** The pull request heading the branch after round 0 or a findings step. */
|
|
2217
2501
|
function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-check" }>, pr: PrCheck): Transition {
|
|
2218
2502
|
const { round } = phase;
|
|
@@ -2220,6 +2504,9 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
2220
2504
|
// (or shipped into a pull request a person merged under it). The round
|
|
2221
2505
|
// completed without a pull request of its own, and the unit is done.
|
|
2222
2506
|
if (pr.state === "merged") return foundMerged(s, pr, [roundNote(round, "completed")]);
|
|
2507
|
+
if (pr.state === "closed") return foundClosed(s, pr, [roundNote(round, "completed")]);
|
|
2508
|
+
if (pr.state === "open" && pr.headBranchExists === false)
|
|
2509
|
+
return missingHeadBranch(s, pr, [roundNote(round, "aborted")]);
|
|
2223
2510
|
if (pr.state === "none") {
|
|
2224
2511
|
// A dead child left nothing on the branch to recover: the unit ends with
|
|
2225
2512
|
// the child's own reason — never the budget clip.
|
|
@@ -2516,6 +2803,8 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2516
2803
|
// by the coding child and adopted at the round's own pr-check.
|
|
2517
2804
|
const r = ret as Extract<StepReturn, { type: "pr-check" }>;
|
|
2518
2805
|
if (r.pr.state === "merged") return foundMerged(clocked, r.pr);
|
|
2806
|
+
if (r.pr.state === "closed") return foundClosed(clocked, r.pr);
|
|
2807
|
+
if (r.pr.state === "open" && r.pr.headBranchExists === false) return missingHeadBranch(clocked, r.pr);
|
|
2519
2808
|
// The open pull request still heads the branch — its head is the
|
|
2520
2809
|
// branch's own tip (the entry facts, issue 1689) or the child's own last
|
|
2521
2810
|
// push (the previous attempt ended `review_pending`, the row's
|
|
@@ -2527,6 +2816,25 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2527
2816
|
// `merge: runner`, which re-verifies the approval and the checks itself.
|
|
2528
2817
|
if (r.pr.state === "open") {
|
|
2529
2818
|
const head = normalizeHead(r.pr.headSha);
|
|
2819
|
+
const pending = s.input.session?.humanGate;
|
|
2820
|
+
if (pending !== undefined) {
|
|
2821
|
+
const adopted: UnitPipelineState = {
|
|
2822
|
+
...clocked,
|
|
2823
|
+
pr: { number: r.pr.prNumber, url: r.pr.url },
|
|
2824
|
+
...(head !== undefined ? { lastReviewHead: head } : {}),
|
|
2825
|
+
};
|
|
2826
|
+
const expected = normalizeHead(pending.headSha);
|
|
2827
|
+
if (head !== undefined && expected !== undefined && !sameCommit(head, expected)) return nextReview(adopted);
|
|
2828
|
+
return enterRound(adopted, { index: pending.round, kind: "findings" });
|
|
2829
|
+
}
|
|
2830
|
+
if (s.input.resume !== undefined) {
|
|
2831
|
+
const adopted: UnitPipelineState = {
|
|
2832
|
+
...clocked,
|
|
2833
|
+
pr: { number: r.pr.prNumber, url: r.pr.url },
|
|
2834
|
+
...(head !== undefined ? { lastReviewHead: head } : {}),
|
|
2835
|
+
};
|
|
2836
|
+
return nextReview(adopted);
|
|
2837
|
+
}
|
|
2530
2838
|
const branchHead = normalizeHead(r.pr.branchHead);
|
|
2531
2839
|
const lastPush = normalizeHead(s.input.lastPush);
|
|
2532
2840
|
const atBranchHead = head !== undefined && branchHead !== undefined && sameCommit(head, branchHead);
|
|
@@ -2659,6 +2967,36 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2659
2967
|
};
|
|
2660
2968
|
case "read": {
|
|
2661
2969
|
const r = ret as Extract<StepReturn, { type: "read-record" }>;
|
|
2970
|
+
const supersession = childSupersession(clocked, p.round, r.pullRequest);
|
|
2971
|
+
if (supersession !== undefined) {
|
|
2972
|
+
// A finished child needs no steer; its verdict, dispositions or push
|
|
2973
|
+
// remain on its own record but cannot change the terminal/moved PR.
|
|
2974
|
+
if (r.run.finished) {
|
|
2975
|
+
const terminalPhase: Extract<Phase, { at: "superseded-read" }> = {
|
|
2976
|
+
at: "superseded-read",
|
|
2977
|
+
round: p.round,
|
|
2978
|
+
runId: p.runId,
|
|
2979
|
+
n: p.n,
|
|
2980
|
+
until: p.until,
|
|
2981
|
+
supersession,
|
|
2982
|
+
};
|
|
2983
|
+
return finishSupersededChild(clocked, terminalPhase);
|
|
2984
|
+
}
|
|
2985
|
+
return {
|
|
2986
|
+
state: {
|
|
2987
|
+
...clocked,
|
|
2988
|
+
phase: {
|
|
2989
|
+
at: "steer",
|
|
2990
|
+
round: p.round,
|
|
2991
|
+
runId: r.restartedAs ?? p.runId,
|
|
2992
|
+
n: p.n,
|
|
2993
|
+
until: p.until,
|
|
2994
|
+
supersession,
|
|
2995
|
+
},
|
|
2996
|
+
},
|
|
2997
|
+
notes: [],
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
2662
3000
|
if (!r.run.finished)
|
|
2663
3001
|
// `restartedAs` (issues 1903/1876): the child's container was replaced
|
|
2664
3002
|
// and it restarted from its request as a new run — the round carries on
|
|
@@ -2741,11 +3079,74 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2741
3079
|
? settleReview(clocked, p.round, r.run)
|
|
2742
3080
|
: settleCoding(clocked, p.round, p.runId, r.run);
|
|
2743
3081
|
}
|
|
3082
|
+
case "steer": {
|
|
3083
|
+
// Delivery is best effort with respect to a child that may have ended
|
|
3084
|
+
// between the fresh PR read and this effect. Either answer is followed
|
|
3085
|
+
// by a record read; a late artifact is recorded there and ignored.
|
|
3086
|
+
return {
|
|
3087
|
+
state: {
|
|
3088
|
+
...clocked,
|
|
3089
|
+
phase: {
|
|
3090
|
+
at: "superseded-wait",
|
|
3091
|
+
round: p.round,
|
|
3092
|
+
runId: p.runId,
|
|
3093
|
+
n: p.n,
|
|
3094
|
+
until: p.until,
|
|
3095
|
+
supersession: p.supersession,
|
|
3096
|
+
},
|
|
3097
|
+
},
|
|
3098
|
+
notes: [],
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
case "superseded-wait":
|
|
3102
|
+
return {
|
|
3103
|
+
state: {
|
|
3104
|
+
...s,
|
|
3105
|
+
phase: {
|
|
3106
|
+
at: "superseded-read",
|
|
3107
|
+
round: p.round,
|
|
3108
|
+
runId: p.runId,
|
|
3109
|
+
n: p.n,
|
|
3110
|
+
until: p.until,
|
|
3111
|
+
supersession: p.supersession,
|
|
3112
|
+
},
|
|
3113
|
+
},
|
|
3114
|
+
notes: [],
|
|
3115
|
+
};
|
|
3116
|
+
case "superseded-read": {
|
|
3117
|
+
const r = ret as Extract<StepReturn, { type: "read-record" }>;
|
|
3118
|
+
if (r.run.finished) return finishSupersededChild(clocked, p);
|
|
3119
|
+
return {
|
|
3120
|
+
state: {
|
|
3121
|
+
...clocked,
|
|
3122
|
+
phase: {
|
|
3123
|
+
at: "superseded-wait",
|
|
3124
|
+
round: p.round,
|
|
3125
|
+
runId: r.restartedAs ?? p.runId,
|
|
3126
|
+
n: p.n + 1,
|
|
3127
|
+
until: p.until,
|
|
3128
|
+
supersession: p.supersession,
|
|
3129
|
+
},
|
|
3130
|
+
},
|
|
3131
|
+
notes: [],
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
case "superseded-pr-check": {
|
|
3135
|
+
const r = ret as Extract<StepReturn, { type: "pr-check" }>;
|
|
3136
|
+
if (r.pr.state === "merged") return foundMerged(clocked, r.pr);
|
|
3137
|
+
if (r.pr.state === "closed") return foundClosed(clocked, r.pr);
|
|
3138
|
+
if (r.pr.state === "open") return restartSupersededReview(clocked, p.round, r.pr);
|
|
3139
|
+
return end(clocked, {
|
|
3140
|
+
kind: "aborted",
|
|
3141
|
+
reason: `⚠️ The adopted pull request disappeared after the superseded review child drained; no replacement review was dispatched.`,
|
|
3142
|
+
reviewRounds: clocked.reviewRounds,
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
2744
3145
|
case "pr-check":
|
|
2745
3146
|
return settlePrCheck(clocked, p, (ret as Extract<StepReturn, { type: "pr-check" }>).pr);
|
|
2746
3147
|
case "checks": {
|
|
2747
3148
|
const r = ret as Extract<StepReturn, { type: "checks" }>;
|
|
2748
|
-
return settleChecks(clocked, p, r.checks, r.retried, r.draft, r.refired);
|
|
3149
|
+
return settleChecks(clocked, p, r.checks, r.retried, r.draft, r.refired, r.pullRequest);
|
|
2749
3150
|
}
|
|
2750
3151
|
case "checks-wait":
|
|
2751
3152
|
// The event fired or the chunk elapsed either way the head is read again.
|
|
@@ -2769,6 +3170,35 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2769
3170
|
};
|
|
2770
3171
|
case "merge": {
|
|
2771
3172
|
const r = ret as Extract<StepReturn, { type: "merge" }>;
|
|
3173
|
+
if (r.outcome === "recheck") {
|
|
3174
|
+
if (r.pullRequest.state === "merged") return foundMerged(clocked, r.pullRequest);
|
|
3175
|
+
if (r.pullRequest.state === "closed") return foundClosed(clocked, r.pullRequest);
|
|
3176
|
+
if (r.pullRequest.state === "open" && r.pullRequest.headBranchExists === false)
|
|
3177
|
+
return missingHeadBranch(clocked, r.pullRequest);
|
|
3178
|
+
if (r.pullRequest.state === "open") {
|
|
3179
|
+
const actual = normalizeHead(r.pullRequest.headSha);
|
|
3180
|
+
if (actual !== undefined && !sameCommit(actual, p.headSha)) {
|
|
3181
|
+
const reviewRestarts = s.reviewRestarts + 1;
|
|
3182
|
+
const next: UnitPipelineState = {
|
|
3183
|
+
...clocked,
|
|
3184
|
+
reviewRestarts,
|
|
3185
|
+
pr: { number: r.pullRequest.prNumber, url: r.pullRequest.url },
|
|
3186
|
+
lastReviewHead: actual,
|
|
3187
|
+
};
|
|
3188
|
+
return enterRound(next, {
|
|
3189
|
+
index: Math.max(1, s.reviewRounds),
|
|
3190
|
+
kind: "review",
|
|
3191
|
+
attempt: reviewRestarts + 1,
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
return end(clocked, {
|
|
3196
|
+
kind: "merge_refused",
|
|
3197
|
+
pr: p.pr,
|
|
3198
|
+
reason: "the pull request changed before the merge and could not be reconciled",
|
|
3199
|
+
reviewRounds: s.reviewRounds,
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
2772
3202
|
if (r.outcome === "merged")
|
|
2773
3203
|
// Found already merged at the door — auto-merge or a person, after the
|
|
2774
3204
|
// approval: the unit is done, the runner merged nothing (`by: other`).
|
|
@@ -2779,6 +3209,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2779
3209
|
pr: p.pr,
|
|
2780
3210
|
sha: r.sha,
|
|
2781
3211
|
mergedAt: r.mergedAt,
|
|
3212
|
+
...(r.mergedBy !== undefined ? { mergedBy: r.mergedBy } : {}),
|
|
2782
3213
|
reviewRounds: s.reviewRounds,
|
|
2783
3214
|
})
|
|
2784
3215
|
: end(clocked, { kind: "merged", by: "runner", pr: p.pr, sha: r.sha, reviewRounds: s.reviewRounds });
|
|
@@ -2856,12 +3287,11 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2856
3287
|
reason: r.reason,
|
|
2857
3288
|
reviewRounds: s.reviewRounds,
|
|
2858
3289
|
});
|
|
2859
|
-
if (r.outcome === "conflict")
|
|
2860
|
-
return
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
});
|
|
3290
|
+
if (r.outcome === "conflict" || r.outcome === "changed")
|
|
3291
|
+
return {
|
|
3292
|
+
state: { ...clocked, phase: { at: "rebase-pr-check", pr: p.pr, n: p.n, outcome: r.outcome } },
|
|
3293
|
+
notes: [],
|
|
3294
|
+
};
|
|
2865
3295
|
if (!("headSha" in r))
|
|
2866
3296
|
return end(clocked, {
|
|
2867
3297
|
kind: "merge_refused",
|
|
@@ -2870,9 +3300,37 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2870
3300
|
reviewRounds: s.reviewRounds,
|
|
2871
3301
|
});
|
|
2872
3302
|
const rebased: UnitPipelineState = { ...clocked, lastReviewHead: r.headSha };
|
|
2873
|
-
if (r.outcome === "changed") return mandatoryReview(rebased);
|
|
2874
3303
|
return enterChecks(rebased, { index: s.reviewRounds, kind: "review" }, []);
|
|
2875
3304
|
}
|
|
3305
|
+
case "rebase-pr-check": {
|
|
3306
|
+
const pr = (ret as Extract<StepReturn, { type: "pr-check" }>).pr;
|
|
3307
|
+
if (pr.state === "merged") return foundMerged(clocked, pr);
|
|
3308
|
+
if (pr.state === "closed") return foundClosed(clocked, pr);
|
|
3309
|
+
if (pr.state === "open" && pr.headBranchExists === false) return missingHeadBranch(clocked, pr);
|
|
3310
|
+
if (pr.state !== "open")
|
|
3311
|
+
return end(clocked, {
|
|
3312
|
+
kind: "merge_refused",
|
|
3313
|
+
pr: p.pr,
|
|
3314
|
+
reason: "the pull request disappeared after the rebase resolver finished",
|
|
3315
|
+
reviewRounds: s.reviewRounds,
|
|
3316
|
+
});
|
|
3317
|
+
const head = normalizeHead(pr.headSha);
|
|
3318
|
+
if (pr.headBranchExists !== true || head === undefined)
|
|
3319
|
+
return end(clocked, {
|
|
3320
|
+
kind: "merge_refused",
|
|
3321
|
+
pr: { number: pr.prNumber, url: pr.url },
|
|
3322
|
+
reason: "the pull request's head branch could not be verified after the rebase resolver finished",
|
|
3323
|
+
reviewRounds: s.reviewRounds,
|
|
3324
|
+
});
|
|
3325
|
+
const refreshed: UnitPipelineState = {
|
|
3326
|
+
...clocked,
|
|
3327
|
+
pr: { number: pr.prNumber, url: pr.url },
|
|
3328
|
+
lastReviewHead: head,
|
|
3329
|
+
};
|
|
3330
|
+
return p.outcome === "conflict"
|
|
3331
|
+
? enterRound(refreshed, { index: s.reviewRounds, kind: "rebase", attempt: p.n })
|
|
3332
|
+
: mandatoryReview(refreshed);
|
|
3333
|
+
}
|
|
2876
3334
|
case "merge-wait":
|
|
2877
3335
|
return {
|
|
2878
3336
|
state: {
|
|
@@ -2922,7 +3380,7 @@ function dispositionFor(s: UnitPipelineState, finding: Finding, round: number):
|
|
|
2922
3380
|
function writeUpPointer(s: UnitPipelineState, kind: RoundKind, runId: string | undefined): string | undefined {
|
|
2923
3381
|
if (runId === undefined) return undefined;
|
|
2924
3382
|
const base = s.input.runPageBase;
|
|
2925
|
-
const at = base !== undefined ? `${base.replace(/\/+$/, "")}/${encodeURIComponent(runId)}` : `run ${runId}`;
|
|
3383
|
+
const at = base !== undefined ? `${base.replace(/\/+$/, "")}/${encodeURIComponent(runId)}` : `the run ${runId}`;
|
|
2926
3384
|
return `The ${presetOf(kind)} child's write-up is its own message above in this thread — the single copy of the detail (${at}).`;
|
|
2927
3385
|
}
|
|
2928
3386
|
|
|
@@ -3036,9 +3494,9 @@ export function renderUnitReport(
|
|
|
3036
3494
|
// The re-issue line keys on the instance's mark, never on who merges: a
|
|
3037
3495
|
// generated plan is re-issued with the request's own text, a seeded one by
|
|
3038
3496
|
// its plan path — and a seeded plan can be a person's merge too.
|
|
3039
|
-
const
|
|
3040
|
-
? `
|
|
3041
|
-
: `The unit's dependents in this plan stay blocked; the
|
|
3497
|
+
const continuation = s.input.generated
|
|
3498
|
+
? `The pipeline kept this unit's task and branch; the next reply in this thread continues it${prUrl ? ` from the open pull request (${prUrl})` : " from the branch head"}.`
|
|
3499
|
+
: `The unit's dependents in this plan stay blocked; the next run of this plan recognizes the unit's branch and pull request.`;
|
|
3042
3500
|
const declined = [...(s.dispositionsByRound[e.reviewRounds - 1] ?? [])].filter((d) => d.disposition === "declined");
|
|
3043
3501
|
const declinedLine = `Declined findings: ${declined.length > 0 ? declined.map((d) => `${d.findingId}${d.note ? ` — ${d.note}` : ""}`).join("; ") : "none"}`;
|
|
3044
3502
|
const verdictLine = `Verdict: LGTM${s.lastVerdictSummary ? ` — ${s.lastVerdictSummary}` : ""}`;
|
|
@@ -3063,11 +3521,13 @@ export function renderUnitReport(
|
|
|
3063
3521
|
const checkpointLine = (checkpoint: { branch: string; sha: string } | undefined) =>
|
|
3064
3522
|
checkpoint === undefined
|
|
3065
3523
|
? undefined
|
|
3066
|
-
: `The branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`;
|
|
3524
|
+
: `The branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`; the next reply in this thread resumes from it instead of starting over.`;
|
|
3067
3525
|
switch (e.kind) {
|
|
3068
3526
|
case "merged":
|
|
3069
3527
|
if (e.by === "other")
|
|
3070
|
-
return
|
|
3528
|
+
return e.mergedBy !== undefined
|
|
3529
|
+
? `✅ 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.`
|
|
3530
|
+
: `✅ 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
3531
|
return [
|
|
3072
3532
|
`✅ 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
3533
|
aside(verdictLine),
|
|
@@ -3078,6 +3538,8 @@ export function renderUnitReport(
|
|
|
3078
3538
|
]
|
|
3079
3539
|
.filter(Boolean)
|
|
3080
3540
|
.join("\n");
|
|
3541
|
+
case "closed":
|
|
3542
|
+
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
3543
|
case "already_landed":
|
|
3082
3544
|
// No compare link, no renewal line, no re-issue prompt: there was
|
|
3083
3545
|
// nothing to ship, so none of them has a question to answer.
|
|
@@ -3119,13 +3581,13 @@ export function renderUnitReport(
|
|
|
3119
3581
|
// the cause and the person's exact next step.
|
|
3120
3582
|
if (e.cause === "draft") {
|
|
3121
3583
|
const link = e.pr !== undefined ? ` — ${e.pr.url}` : "";
|
|
3122
|
-
if (!shows(verbosity, "verbose")) return `⏸️ Held: draft —
|
|
3123
|
-
const
|
|
3124
|
-
? `
|
|
3125
|
-
: `
|
|
3584
|
+
if (!shows(verbosity, "verbose")) return `⏸️ Held: draft — GitHub still marks the pull request as draft${link}`;
|
|
3585
|
+
const draftContinuation = s.input.generated
|
|
3586
|
+
? `The pipeline kept the task, branch and pull request${e.pr !== undefined ? ` (${e.pr.url})` : ""}; after GitHub marks it ready, the next reply in this thread resumes at the review round.`
|
|
3587
|
+
: `The unit stays held while GitHub marks the pull request as a draft; ${continuation.charAt(0).toLowerCase()}${continuation.slice(1)}`;
|
|
3126
3588
|
return join([
|
|
3127
3589
|
`⏸️ 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.`,
|
|
3128
|
-
|
|
3590
|
+
draftContinuation,
|
|
3129
3591
|
]);
|
|
3130
3592
|
}
|
|
3131
3593
|
// The blocked hold (issue 2086): the coding child concluded its round
|
|
@@ -3138,7 +3600,7 @@ export function renderUnitReport(
|
|
|
3138
3600
|
return join([
|
|
3139
3601
|
`⏸️ Held after ${rounds}: the coding child of round ${e.round.index} concluded its round blocked instead of opening a pull request — ${e.reason}.${prLine}`,
|
|
3140
3602
|
writeUpPointer(s, e.round.kind, e.runId),
|
|
3141
|
-
`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).
|
|
3603
|
+
`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; the next reply is treated as its answer. ${continuation}`,
|
|
3142
3604
|
]);
|
|
3143
3605
|
}
|
|
3144
3606
|
// A human-gated row is a question to a person. The enclosing idle keeps
|
|
@@ -3150,7 +3612,7 @@ export function renderUnitReport(
|
|
|
3150
3612
|
return join([
|
|
3151
3613
|
`⏸️ Waiting for a person after ${rounds}${e.pr !== undefined ? `: ${e.pr.url}` : ""} — every finding of review round ${e.round.index} is human-gated, a receipt only a person can produce: ${rows}. The unit stays live; no unanswered fix round was opened.`,
|
|
3152
3614
|
levelLine,
|
|
3153
|
-
`
|
|
3615
|
+
`The next reply in this unit thread or authorized pull request comment becomes the fix round's answer; review then runs again.`,
|
|
3154
3616
|
]);
|
|
3155
3617
|
}
|
|
3156
3618
|
case "merge_refused":
|
|
@@ -3166,8 +3628,8 @@ export function renderUnitReport(
|
|
|
3166
3628
|
return join([
|
|
3167
3629
|
`⚠️ The review approved ${e.pr.url} but the pipeline did not merge it: ${e.reason}. A person decides what becomes of the pull request.`,
|
|
3168
3630
|
s.input.generated
|
|
3169
|
-
?
|
|
3170
|
-
:
|
|
3631
|
+
? continuation
|
|
3632
|
+
: "The approved work remains on the branch. This is a bug: the pipeline has no automatic recovery for this refused merge, so the unit's dependents remain blocked.",
|
|
3171
3633
|
]);
|
|
3172
3634
|
case "round_cap": {
|
|
3173
3635
|
// The quiet copy counts the last review's open findings, only the
|
|
@@ -3186,7 +3648,7 @@ export function renderUnitReport(
|
|
|
3186
3648
|
failedChecks.length > 0
|
|
3187
3649
|
? `🧢 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}`
|
|
3188
3650
|
: `🧢 Ship stopped at a cap: the ${e.maxRounds}-round cap — no approval after ${rounds}.${prLine}`;
|
|
3189
|
-
return join([headline, splitReport(s),
|
|
3651
|
+
return join([headline, splitReport(s), continuation]);
|
|
3190
3652
|
}
|
|
3191
3653
|
case "wall_clock_cap":
|
|
3192
3654
|
if (!shows(verbosity, "verbose")) return `🧢 Out of budget — no approval after ${rounds}.${prLine}`;
|
|
@@ -3194,13 +3656,13 @@ export function renderUnitReport(
|
|
|
3194
3656
|
`🧢 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}`,
|
|
3195
3657
|
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
3196
3658
|
splitReport(s),
|
|
3197
|
-
|
|
3659
|
+
continuation,
|
|
3198
3660
|
]);
|
|
3199
3661
|
case "review_pending":
|
|
3200
3662
|
return join([
|
|
3201
3663
|
`⏳ 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.`,
|
|
3202
3664
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
3203
|
-
aside(
|
|
3665
|
+
aside(continuation),
|
|
3204
3666
|
]);
|
|
3205
3667
|
case "stopped":
|
|
3206
3668
|
if (!shows(verbosity, "verbose"))
|
|
@@ -3216,7 +3678,7 @@ export function renderUnitReport(
|
|
|
3216
3678
|
e.postedReview
|
|
3217
3679
|
? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
|
|
3218
3680
|
: undefined,
|
|
3219
|
-
|
|
3681
|
+
continuation,
|
|
3220
3682
|
]);
|
|
3221
3683
|
case "aborted":
|
|
3222
3684
|
if (!shows(verbosity, "verbose")) return `⚠️ Aborted after ${rounds}: ${e.reason}${prLine}`;
|
|
@@ -3229,7 +3691,7 @@ export function renderUnitReport(
|
|
|
3229
3691
|
),
|
|
3230
3692
|
e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
|
|
3231
3693
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
3232
|
-
|
|
3694
|
+
continuation,
|
|
3233
3695
|
]);
|
|
3234
3696
|
case "continued":
|
|
3235
3697
|
// A segment boundary is the runner continuing — an acknowledgement, verbose
|
|
@@ -3239,18 +3701,18 @@ export function renderUnitReport(
|
|
|
3239
3701
|
return join([
|
|
3240
3702
|
writeUpPointer(s, e.round.kind, e.runId),
|
|
3241
3703
|
s.input.idleDays && s.input.idleDays > 0
|
|
3242
|
-
? `🔁 The unit's budget ran out with the unit unfinished — ${e.line}.
|
|
3704
|
+
? `🔁 The unit's budget ran out with the unit unfinished — ${e.line}. The next reply in this thread continues it; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`
|
|
3243
3705
|
: `🔁 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` : ""}.`,
|
|
3244
3706
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
3245
3707
|
]);
|
|
3246
3708
|
case "transient":
|
|
3247
3709
|
if (!shows(verbosity, "verbose"))
|
|
3248
|
-
return `⚠️ Aborted after ${rounds}: the model provider failed twice
|
|
3710
|
+
return `⚠️ Aborted after ${rounds}: this is a bug — the model provider failed twice and no automatic retry remains.${prLine}`;
|
|
3249
3711
|
return join([
|
|
3250
3712
|
`⚠️ 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.`,
|
|
3251
3713
|
writeUpPointer(s, e.round.kind, s.lastCodingRunId),
|
|
3252
|
-
`⚠️
|
|
3253
|
-
|
|
3714
|
+
`⚠️ This is a bug: ship ended after ${rounds} because its automatic provider retry was spent.`,
|
|
3715
|
+
continuation,
|
|
3254
3716
|
]);
|
|
3255
3717
|
case "no_verdict":
|
|
3256
3718
|
if (!shows(verbosity, "verbose"))
|
|
@@ -3259,7 +3721,7 @@ export function renderUnitReport(
|
|
|
3259
3721
|
`⚠️ 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.`,
|
|
3260
3722
|
writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index]),
|
|
3261
3723
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
3262
|
-
|
|
3724
|
+
continuation,
|
|
3263
3725
|
]);
|
|
3264
3726
|
case "interrupted":
|
|
3265
3727
|
return join([shipInterruptedNote(prUrl, e.cause, (s.input.idleDays ?? 0) > 0), checkpointLine(e.checkpoint)]);
|
|
@@ -3268,7 +3730,7 @@ export function renderUnitReport(
|
|
|
3268
3730
|
case "refused":
|
|
3269
3731
|
return join([
|
|
3270
3732
|
`🚫 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.`,
|
|
3271
|
-
aside(
|
|
3733
|
+
aside(continuation),
|
|
3272
3734
|
]);
|
|
3273
3735
|
case "idle":
|
|
3274
3736
|
// The old kind's sentence at this copy's level — the report is unchanged
|