@coreplane/switchboard 1.255.0 → 1.257.0
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/config/config.example.yaml +14 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +340 -7
- package/dist/assets/deploy/cloudflare-resident/drain.ts +100 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +227 -34
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +5 -3
- package/dist/assets/project.json +17 -9
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +55 -1
- package/dist/assets/src/core/authz/grants.ts +12 -0
- package/dist/assets/src/core/authz/policy.ts +15 -0
- package/dist/assets/src/core/budgets.ts +51 -17
- package/dist/assets/src/core/coordinator/contract.ts +45 -5
- package/dist/assets/src/core/coordinator/driver.ts +17 -4
- package/dist/assets/src/core/delivery.ts +8 -5
- package/dist/assets/src/core/pipelineStanding.ts +57 -0
- package/dist/assets/src/core/plane/decide.ts +468 -12
- package/dist/assets/src/core/plane/findings.ts +120 -0
- package/dist/assets/src/core/reviewVerdict.ts +49 -0
- package/dist/assets/src/core/runEvents.ts +41 -14
- package/dist/assets/src/core/runFriction.ts +16 -0
- package/dist/assets/src/core/runLedger/types.ts +16 -3
- package/dist/assets/src/core/runRecord.ts +12 -0
- package/dist/assets/src/core/ship/contract.ts +34 -32
- package/dist/assets/src/core/ship/coordinator.ts +291 -95
- package/dist/assets/src/core/ship/renewal.ts +19 -17
- package/dist/assets/src/core/trace/attrs.ts +1 -1
- package/dist/assets/src/core/untrusted.ts +35 -0
- package/dist/assets/src/execution/residentCredentials.ts +7 -4
- package/dist/assets/web/dist/.vite/manifest.json +82 -57
- package/dist/assets/web/dist/assets/HomePage-DJqJQzrL.js +1 -0
- package/dist/assets/web/dist/assets/{PendingTurnRow-DGINv9XT.js → PendingTurnRow-fhKqKUVv.js} +1 -1
- package/dist/assets/web/dist/assets/PlanePage-Bood0hNO.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-D_RD6wLo.js → ResidentDetailPage-DOe1HtUC.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BLkSuCxo.js → ResidentsIndexPage-TyCGjXM4.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-C_4t-sit.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BNepPMRX.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-jZnMw1mM.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-3aYsDf-q.js → ScheduledPage-BkNoudDP.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-DG-p5Xy1.js → SettingsPage-COvWGXl-.js} +1 -1
- package/dist/assets/web/dist/assets/SilentTurn-DNAANKgN.js +2 -0
- package/dist/assets/web/dist/assets/SlackMark-CtiHrNjr.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-CEnGlyAL.js → StatusDot-BgmkSV0S.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CiunVowT.js → Tooltip-DEUPuCPW.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DNhdzxvY.js +1 -0
- package/dist/assets/web/dist/assets/{dist-luhv3YSo.js → dist-CF3jz9LM.js} +1 -1
- package/dist/assets/web/dist/assets/durationTone-CVpX_yIk.js +1 -0
- package/dist/assets/web/dist/assets/main-7SAujY_s.js +28 -0
- package/dist/assets/web/dist/assets/main-DGC6WFqS.css +1 -0
- package/dist/assets/web/dist/assets/{sseReplay-DE6wv1Ua.js → sseReplay-BRJEIh83.js} +1 -1
- package/dist/cli.js +2941 -1116
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-Be7jLLnU.js +0 -3
- package/dist/assets/web/dist/assets/PlanePage-JEj-lqgz.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CmWYGR36.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-DW-HHuZa.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +0 -1
- package/dist/assets/web/dist/assets/main-C4QfwybO.css +0 -1
- package/dist/assets/web/dist/assets/main-mAKx_zo9.js +0 -28
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
findingsAtOrAbove,
|
|
42
42
|
formatFinding,
|
|
43
43
|
isAddressSeverity,
|
|
44
|
+
severityCounts,
|
|
44
45
|
type AddressSeverity,
|
|
45
46
|
type AddressSeveritySource,
|
|
46
47
|
type Finding,
|
|
@@ -76,21 +77,70 @@ export interface ShipCaps {
|
|
|
76
77
|
* rounds that must still follow, capped at the preset's ask, refused under the
|
|
77
78
|
* round's floor. Nothing here holds a reserve of its own. */
|
|
78
79
|
|
|
79
|
-
/** What
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
export
|
|
80
|
+
/** What actually ended an interrupted child, from the ledger that saw it
|
|
81
|
+
* (issue 1876): the ending's sentence names the cause in the user's nouns —
|
|
82
|
+
* the bot restarted, the resident container was replaced, the sandbox failed
|
|
83
|
+
* — never "the bot restarted" for a site three causes reach. Read off the
|
|
84
|
+
* child's record by the bot's `read-record` (`child_interrupted` / the resume
|
|
85
|
+
* notes) and carried on the `interrupted` ending; absent when the record
|
|
86
|
+
* named none. */
|
|
87
|
+
export type InterruptionCause = "bot_restart" | "container_replaced" | "sandbox_fault";
|
|
88
|
+
|
|
89
|
+
/** What a ship pipeline's thread and card say when its child died under it
|
|
90
|
+
* (run-history item 36): the work it did stands on GitHub with nobody driving
|
|
91
|
+
* it, so the note names the actual cause (issue 1876 — the site is reached by
|
|
92
|
+
* a bot restart, a resident container replacement and a sandbox fault, and
|
|
93
|
+
* only the ledger's word picks one; with none the sentence claims no cause),
|
|
94
|
+
* the PR when one was opened and the exact re-issue that continues the loop —
|
|
95
|
+
* the same entry the preflight's resume-at-review takes (agent-ship item 10).
|
|
96
|
+
* Without a PR the task itself is the re-issue: round 0 runs again on the
|
|
97
|
+
* pipeline's own deterministic branch. A child whose container was replaced
|
|
98
|
+
* resumes from its request by itself (issue 1903) and the pipeline keeps
|
|
99
|
+
* waiting — this note is written only when that resume failed or no resume
|
|
100
|
+
* applied. */
|
|
101
|
+
/** The cause an interruption's recorded words name (issue 1876): the ledger
|
|
102
|
+
* that saw the roll wrote them — the relaunch refusals and the lost-workspace
|
|
103
|
+
* notes name the replaced container, the sandbox executor's wordings the
|
|
104
|
+
* sandbox, the boot gap names the restart — and the ending's sentence repeats
|
|
105
|
+
* the cause, never "the bot restarted" for words that say otherwise. Each
|
|
106
|
+
* pattern is anchored on the exact phrases those ledgers write —
|
|
107
|
+
* `src/core/harness/contract.ts` and `src/core/dispatch/relaunch.ts`
|
|
108
|
+
* ("container replaced under the run", "replacement container"),
|
|
109
|
+
* `src/core/dispatch/reattach.ts` ("workspace lost", "could not be
|
|
110
|
+
* re-attached"), `src/execution/sandboxLifecycle.ts` and the sandbox worker's
|
|
111
|
+
* failures ("sandbox recycled", "sandbox worker", "sandbox exec"),
|
|
112
|
+
* `src/core/boot.ts` and the resume notes ("bot restarted", the deploy
|
|
113
|
+
* hand-off's "next generation") — so an unrelated word ("regenerating…", a
|
|
114
|
+
* reason merely mentioning a sandbox path) cannot classify. Unrecognized
|
|
115
|
+
* words are no cause: the sentence then claims none. */
|
|
116
|
+
export function interruptionCauseOfWords(words: string): InterruptionCause | undefined {
|
|
117
|
+
if (
|
|
118
|
+
/\bcontainer (?:was )?replaced\b|\breplac(?:ed|ement) container\b|\bworkspace lost\b|\bcould not be re-attached\b/i.test(
|
|
119
|
+
words,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
return "container_replaced";
|
|
123
|
+
if (/\bsandbox (?:recycled|worker|exec|runtime)\b/i.test(words)) return "sandbox_fault";
|
|
124
|
+
if (/\bbot restart(?:ed|s)?\b|\b(?:next|previous|another) generation\b/i.test(words)) return "bot_restart";
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function shipInterruptedNote(prUrl?: string, cause?: InterruptionCause): string {
|
|
87
129
|
const stands = prUrl
|
|
88
130
|
? `Its work stands on GitHub: ${prUrl}.`
|
|
89
131
|
: "Whatever it pushed stands on its pipeline branch; no PR was opened yet.";
|
|
90
132
|
const reissue = prUrl
|
|
91
133
|
? `To continue the review loop, re-issue \`agent:ship\` in this thread with only the PR URL (${prUrl}).`
|
|
92
134
|
: "To continue, re-issue `agent:ship` in this thread with the task — round 0 runs again on the same branch.";
|
|
93
|
-
|
|
135
|
+
const opening =
|
|
136
|
+
cause === "container_replaced"
|
|
137
|
+
? "⚠️ 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."
|
|
138
|
+
: cause === "sandbox_fault"
|
|
139
|
+
? "⚠️ The sandbox running this pipeline's child failed and the child could not resume, so the pipeline stopped."
|
|
140
|
+
: cause === "bot_restart"
|
|
141
|
+
? "⚠️ The bot restarted while this ship pipeline was running, so the pipeline stopped."
|
|
142
|
+
: "⚠️ This ship pipeline's child was interrupted and could not resume, so the pipeline stopped.";
|
|
143
|
+
return `${opening} ${stands} ${reissue}`;
|
|
94
144
|
}
|
|
95
145
|
|
|
96
146
|
// ---- the plan graph --------------------------------------------------------------------------------
|
|
@@ -473,9 +523,10 @@ export type CoordinatorAction =
|
|
|
473
523
|
* over an already-shipped pull request. */
|
|
474
524
|
entry?: true;
|
|
475
525
|
/** Set after a coding child died: the bot opens the pull request from the
|
|
476
|
-
* pushed branch itself (title from
|
|
477
|
-
*
|
|
478
|
-
*
|
|
526
|
+
* pushed branch itself (title from this run's submitted description when
|
|
527
|
+
* the record holds one, else a conventional fallback from the unit's
|
|
528
|
+
* title — issue 1877; body from the description when the record holds
|
|
529
|
+
* one) instead of answering `none` over stranded work. */
|
|
479
530
|
recover?: { runId: string };
|
|
480
531
|
/** The pull request the machine has adopted (`state.pr`), when it holds
|
|
481
532
|
* one: the child may have worked that pull request's own head branch,
|
|
@@ -536,6 +587,10 @@ export type ChildFacts =
|
|
|
536
587
|
* `provider_transient` marks a child a gateway 5xx, a cut stream or a
|
|
537
588
|
* gateway timeout ended past the harness's retry ladder (issue 1932). */
|
|
538
589
|
failure?: { kind: string };
|
|
590
|
+
/** What ended an `interrupted` child, off its record's own events (issue
|
|
591
|
+
* 1876): the ending's sentence names it instead of claiming a bot
|
|
592
|
+
* restart for every cause. */
|
|
593
|
+
interruption?: InterruptionCause;
|
|
539
594
|
};
|
|
540
595
|
|
|
541
596
|
/** What heads the unit's branch on GitHub: nothing, an open pull request, or —
|
|
@@ -599,7 +654,10 @@ export type StepReturn =
|
|
|
599
654
|
| { type: "wait"; step: string; outcome: "event" | "timeout" }
|
|
600
655
|
// `stopped` on a read: the instance row carries the hard stop's mark, so a
|
|
601
656
|
// finished child ends its unit `stopped` whatever the child's own status.
|
|
602
|
-
|
|
657
|
+
/** `restartedAs`: the interrupted child restarted from its request as this
|
|
658
|
+
* run (issue 1903 — a replaced container's child resumes by itself), so the
|
|
659
|
+
* machine keeps waiting on the successor instead of ending the unit. */
|
|
660
|
+
| { type: "read-record"; step: string; run: ChildFacts; stopped?: true; restartedAs?: string; at: number }
|
|
603
661
|
| { type: "pr-check"; step: string; pr: PrCheck; at: number }
|
|
604
662
|
| { type: "merge"; step: string; outcome: "merged"; sha: string; at: number }
|
|
605
663
|
// The door found the pull request already merged after the approval — auto-merge
|
|
@@ -612,7 +670,7 @@ export type StepReturn =
|
|
|
612
670
|
* not be read, which the machine treats as pending (record 0055). A retry
|
|
613
671
|
* ask answers `retried` instead: whether the bot dispatched the re-run —
|
|
614
672
|
* false means there is nothing to wait for at the unchanged head. */
|
|
615
|
-
| { type: "checks"; step: string; checks?: RoundChecks; retried?: boolean; at: number }
|
|
673
|
+
| { type: "checks"; step: string; checks?: RoundChecks; draft?: boolean; retried?: boolean; at: number }
|
|
616
674
|
| { type: "wait-checks"; step: string; outcome: "event" | "timeout" }
|
|
617
675
|
| { type: "sleep"; step: string };
|
|
618
676
|
|
|
@@ -627,11 +685,16 @@ export interface CheckFailure {
|
|
|
627
685
|
flakeSuspect?: boolean;
|
|
628
686
|
}
|
|
629
687
|
|
|
630
|
-
/** The check runs at the reviewed head as the round's checks step reads them.
|
|
688
|
+
/** The check runs at the reviewed head as the round's checks step reads them.
|
|
689
|
+
* `expected` names checks the head must still gain — a required check, or the
|
|
690
|
+
* repository's approve workflow whose run does not exist yet (issue 2063):
|
|
691
|
+
* the base's required contexts not among the reported runs. The table reads
|
|
692
|
+
* an expected-but-unreported check exactly as a pending one. */
|
|
631
693
|
export interface RoundChecks {
|
|
632
694
|
total: number;
|
|
633
695
|
pending: string[];
|
|
634
696
|
failed: CheckFailure[];
|
|
697
|
+
expected?: string[];
|
|
635
698
|
}
|
|
636
699
|
|
|
637
700
|
/** A failed check as a finding of the round (record 0055): a finding row like
|
|
@@ -695,12 +758,24 @@ export type UnitEnding =
|
|
|
695
758
|
* review round once the receipt is posted (item 10's resume path). */
|
|
696
759
|
| {
|
|
697
760
|
kind: "held";
|
|
761
|
+
cause?: undefined;
|
|
698
762
|
pr?: PrRef;
|
|
699
763
|
round: RoundRef;
|
|
700
764
|
findings: Finding[];
|
|
701
765
|
verdict: "approve" | "request_changes";
|
|
702
766
|
reviewRounds: number;
|
|
703
767
|
}
|
|
768
|
+
/** The pull request is a draft (issue 2063): nothing can merge and a fix
|
|
769
|
+
* round would change nothing, so the checks step waits for the ready event
|
|
770
|
+
* inside its ask and, still a draft at the ask's end, the unit ends held —
|
|
771
|
+
* `held: draft — mark it ready to continue` — never an unnamed exit. */
|
|
772
|
+
| {
|
|
773
|
+
kind: "held";
|
|
774
|
+
cause: "draft";
|
|
775
|
+
pr?: PrRef;
|
|
776
|
+
round: RoundRef;
|
|
777
|
+
reviewRounds: number;
|
|
778
|
+
}
|
|
704
779
|
| { kind: "merge_refused"; pr: PrRef; reason: string; reviewRounds: number }
|
|
705
780
|
| { kind: "round_cap"; maxRounds: number; reviewRounds: number }
|
|
706
781
|
| {
|
|
@@ -766,7 +841,7 @@ export type UnitEnding =
|
|
|
766
841
|
* `transient` so it reads as a condition beside `checks_failed` and `held`
|
|
767
842
|
* in the plane's table, never as the child failing on its task. */
|
|
768
843
|
| { kind: "transient"; round: RoundRef; runId: string; reviewRounds: number }
|
|
769
|
-
| { kind: "interrupted"; round: RoundRef; runId: string; reviewRounds: number }
|
|
844
|
+
| { kind: "interrupted"; round: RoundRef; runId: string; reviewRounds: number; cause?: InterruptionCause }
|
|
770
845
|
| { kind: "refused"; refusal: string; message?: string; round: RoundRef; reviewRounds: number }
|
|
771
846
|
/** The unit idles instead of ending (record 0051): with the resolved
|
|
772
847
|
* `ship.idleDays` above zero, `end()` wraps an idling kind — every kind but
|
|
@@ -854,7 +929,10 @@ export {
|
|
|
854
929
|
type AddressSeveritySource,
|
|
855
930
|
};
|
|
856
931
|
|
|
857
|
-
|
|
932
|
+
/** How far the unit's lease segments have got (decision 0046, the renewable
|
|
933
|
+
* lease): the segment's number, the renewals and dollars spent before it and
|
|
934
|
+
* the continuation facts — a progress counter, nothing conversational. */
|
|
935
|
+
export interface LeaseSegmentProgress {
|
|
858
936
|
segment: number;
|
|
859
937
|
renewalsSpent: number;
|
|
860
938
|
spendUsd: number | null;
|
|
@@ -894,7 +972,7 @@ export interface UnitPipelineInput {
|
|
|
894
972
|
* session's spend so far, the sha it continues from and the previous
|
|
895
973
|
* segment's handoff. Every step name of a later segment is prefixed with
|
|
896
974
|
* it, so the Workflow's durable steps never collide across segments. */
|
|
897
|
-
session?:
|
|
975
|
+
session?: LeaseSegmentProgress;
|
|
898
976
|
/** The instance's mark (agent-ship item 16): a generated one-unit plan — a
|
|
899
977
|
* `plan` with an id and no `path` — whose unit runs in the requesting
|
|
900
978
|
* thread and is re-issued with the request's own text, never a plan path. */
|
|
@@ -940,6 +1018,8 @@ type Phase =
|
|
|
940
1018
|
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
941
1019
|
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
942
1020
|
dead?: "failed" | "interrupted";
|
|
1021
|
+
/** What ended the dead child (issue 1876), for the `interrupted` ending's sentence. */
|
|
1022
|
+
cause?: InterruptionCause;
|
|
943
1023
|
/** The dead child's record names a provider transient (`failure:
|
|
944
1024
|
* provider_transient`, issue 1932): with nothing pushed, round 0 is
|
|
945
1025
|
* re-run once instead of the unit aborting; a second transient in the
|
|
@@ -1085,14 +1165,17 @@ const loopOf = (s: UnitPipelineState): Loop => ({ maxRounds: s.input.caps.maxRou
|
|
|
1085
1165
|
* A review round `n` and the findings step that follows it share `n`; the
|
|
1086
1166
|
* module's positions are the loop's own. */
|
|
1087
1167
|
function roundCarve(s: UnitPipelineState, round: RoundRef): Carve {
|
|
1088
|
-
|
|
1089
|
-
|
|
1168
|
+
return carve(
|
|
1169
|
+
remainingMs(s),
|
|
1170
|
+
{ kind: round.kind, index: loopPosition(loopOf(s), round.kind, round.index) },
|
|
1171
|
+
loopOf(s),
|
|
1172
|
+
);
|
|
1090
1173
|
}
|
|
1091
1174
|
|
|
1092
1175
|
/** The prefix every step of this pipeline is named under: the unit id, and for
|
|
1093
1176
|
* a renewal's segment the segment too (`U10/s2/…`), so the Workflow's durable
|
|
1094
1177
|
* step cache never hands segment two the answers of segment one. */
|
|
1095
|
-
export const stepPrefixOf = (unit: string, session:
|
|
1178
|
+
export const stepPrefixOf = (unit: string, session: LeaseSegmentProgress | undefined): string =>
|
|
1096
1179
|
session !== undefined && session.segment > 1 ? `${unit}/s${session.segment}` : unit;
|
|
1097
1180
|
const stepPrefix = (s: UnitPipelineState) => stepPrefixOf(s.input.unit.id, s.input.session);
|
|
1098
1181
|
const roundStep = (s: UnitPipelineState, round: RoundRef) =>
|
|
@@ -1623,19 +1706,29 @@ function enterChecks(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNo
|
|
|
1623
1706
|
};
|
|
1624
1707
|
}
|
|
1625
1708
|
|
|
1626
|
-
/** The
|
|
1627
|
-
*
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
1632
|
-
*
|
|
1709
|
+
/** The checks step's decision table over the reviewed head's facts (record
|
|
1710
|
+
* 0055; issues 1991 and 2063) — one function, read top to bottom, so every
|
|
1711
|
+
* sibling that folds more outcomes into the step (a held ending, a transient
|
|
1712
|
+
* round re-run) reads the same order:
|
|
1713
|
+
* (a) any failed check is the round's answer as soon as it is read, whatever
|
|
1714
|
+
* else is still pending — with the flake rule spending its one re-run
|
|
1715
|
+
* first when every failure is a suspect;
|
|
1716
|
+
* (d) no failure and the pull request a draft: the step waits for the ready
|
|
1717
|
+
* event — nothing can merge a draft and a fix round would change nothing;
|
|
1718
|
+
* (b) no failure and a check pending, queued, expected but not yet reported
|
|
1719
|
+
* (`expected`: a required check, or the repository's approve workflow
|
|
1720
|
+
* whose run does not exist yet), or GitHub unreadable: the head waits on
|
|
1721
|
+
* the settled event and is read again;
|
|
1722
|
+
* (—) no check reported at all: one chunk of grace, never more;
|
|
1723
|
+
* (c) every expected check reported green: the round proceeds with no wait. */
|
|
1633
1724
|
function checksVerdict(
|
|
1634
1725
|
checks: RoundChecks | undefined,
|
|
1635
1726
|
p: { retried: boolean; graced: boolean },
|
|
1727
|
+
draft?: boolean,
|
|
1636
1728
|
):
|
|
1637
1729
|
| { kind: "retry"; names: string[] }
|
|
1638
1730
|
| { kind: "failed"; failed: CheckFailure[] }
|
|
1731
|
+
| { kind: "draft" }
|
|
1639
1732
|
| { kind: "pending" }
|
|
1640
1733
|
| { kind: "grace" }
|
|
1641
1734
|
| { kind: "green" } {
|
|
@@ -1649,8 +1742,14 @@ function checksVerdict(
|
|
|
1649
1742
|
return { kind: "retry", names: checks.failed.map((f) => f.name) };
|
|
1650
1743
|
return { kind: "failed", failed: checks.failed };
|
|
1651
1744
|
}
|
|
1652
|
-
//
|
|
1653
|
-
|
|
1745
|
+
// A draft outranks the waits: its checks may sit green forever, and only a
|
|
1746
|
+
// person's "ready" changes anything — a red check above still gets its fix
|
|
1747
|
+
// round, since that work stands whether or not the pull request is a draft.
|
|
1748
|
+
if (draft === true) return { kind: "draft" };
|
|
1749
|
+
// GitHub unreadable answers as pending and is re-read at the chunk's end;
|
|
1750
|
+
// an expected check not yet reported (issue 2063) is pending the same way.
|
|
1751
|
+
if (checks === undefined || checks.pending.length > 0 || (checks.expected?.length ?? 0) > 0)
|
|
1752
|
+
return { kind: "pending" };
|
|
1654
1753
|
// No check reported at the head: one chunk of grace — the first check starts
|
|
1655
1754
|
// within minutes where CI exists — then the round proceeds, so a repository
|
|
1656
1755
|
// without CI costs one chunk per round and never idles (record 0055).
|
|
@@ -1673,6 +1772,7 @@ function settleChecks(
|
|
|
1673
1772
|
p: Extract<Phase, { at: "checks" }>,
|
|
1674
1773
|
checks: RoundChecks | undefined,
|
|
1675
1774
|
retried?: boolean,
|
|
1775
|
+
draft?: boolean,
|
|
1676
1776
|
): Transition {
|
|
1677
1777
|
const { round } = p;
|
|
1678
1778
|
const wait = (over: Partial<Extract<Phase, { at: "checks-wait" }>>): Transition => ({
|
|
@@ -1706,7 +1806,7 @@ function settleChecks(
|
|
|
1706
1806
|
}
|
|
1707
1807
|
return wait({ retried: true });
|
|
1708
1808
|
}
|
|
1709
|
-
const verdict = checksVerdict(checks, p);
|
|
1809
|
+
const verdict = checksVerdict(checks, p, draft);
|
|
1710
1810
|
switch (verdict.kind) {
|
|
1711
1811
|
case "retry":
|
|
1712
1812
|
return {
|
|
@@ -1731,6 +1831,21 @@ function settleChecks(
|
|
|
1731
1831
|
);
|
|
1732
1832
|
return enterRound(next, { index: round.index, kind: "findings" }, notes);
|
|
1733
1833
|
}
|
|
1834
|
+
case "draft":
|
|
1835
|
+
// A draft pull request (issue 2063): the unit idles on the settled event
|
|
1836
|
+
// — marking it ready starts the head's check suites, whose completion
|
|
1837
|
+
// fires `checks-settled-<head>` — and the re-read continues through the
|
|
1838
|
+
// table. Still a draft at the ask's end, the unit ends held naming the
|
|
1839
|
+
// draft and the person's next step, never an exit with no cause.
|
|
1840
|
+
if (s.clock - p.since >= p.waitMs)
|
|
1841
|
+
return end(s, {
|
|
1842
|
+
kind: "held",
|
|
1843
|
+
cause: "draft",
|
|
1844
|
+
...(s.pr !== undefined ? { pr: s.pr } : {}),
|
|
1845
|
+
round: p.round,
|
|
1846
|
+
reviewRounds: s.reviewRounds,
|
|
1847
|
+
});
|
|
1848
|
+
return wait({});
|
|
1734
1849
|
case "pending":
|
|
1735
1850
|
// A head still pending or unreadable at the ask's end proceeds — the
|
|
1736
1851
|
// ending's facts read names what is still pending, and the merge door
|
|
@@ -1812,9 +1927,17 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
1812
1927
|
// A dead child left nothing on the branch to recover: the unit ends with
|
|
1813
1928
|
// the child's own reason — never the budget clip.
|
|
1814
1929
|
if (phase.dead === "interrupted")
|
|
1815
|
-
return end(
|
|
1816
|
-
|
|
1817
|
-
|
|
1930
|
+
return end(
|
|
1931
|
+
s,
|
|
1932
|
+
{
|
|
1933
|
+
kind: "interrupted",
|
|
1934
|
+
round,
|
|
1935
|
+
runId: phase.runId,
|
|
1936
|
+
reviewRounds: s.reviewRounds,
|
|
1937
|
+
...(phase.cause !== undefined ? { cause: phase.cause } : {}),
|
|
1938
|
+
},
|
|
1939
|
+
[roundNote(round, "aborted")],
|
|
1940
|
+
);
|
|
1818
1941
|
if (phase.dead === "failed") {
|
|
1819
1942
|
// A provider transient with nothing pushed is not the child's failure
|
|
1820
1943
|
// (issue 1932): the ledger row and the branch are untouched, so round 0
|
|
@@ -1832,7 +1955,7 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
1832
1955
|
pr.unrecovered === "no_commits"
|
|
1833
1956
|
? `nothing heads \`${s.input.unit.branch}\`: no commits were pushed, so there was no work to recover`
|
|
1834
1957
|
: pr.unrecovered === "no_base"
|
|
1835
|
-
? `\`${s.input.unit.branch}\` could not be given a pull request: the
|
|
1958
|
+
? `\`${s.input.unit.branch}\` could not be given a pull request: the pipeline names no base branch to open it against, so whatever was pushed stays on the branch`
|
|
1836
1959
|
: `the pr-check found no pull request heading \`${s.input.unit.branch}\`, so nothing was recovered`;
|
|
1837
1960
|
return end(
|
|
1838
1961
|
s,
|
|
@@ -1971,9 +2094,17 @@ function roundOnOpenPr(
|
|
|
1971
2094
|
// ends with the child's own reason — the ship-restart note for a bot
|
|
1972
2095
|
// roll, the failure for a failed run — never as the round's inaction.
|
|
1973
2096
|
if (phase.dead === "interrupted")
|
|
1974
|
-
return end(
|
|
1975
|
-
|
|
1976
|
-
|
|
2097
|
+
return end(
|
|
2098
|
+
next,
|
|
2099
|
+
{
|
|
2100
|
+
kind: "interrupted",
|
|
2101
|
+
round,
|
|
2102
|
+
runId: phase.runId,
|
|
2103
|
+
reviewRounds: next.reviewRounds,
|
|
2104
|
+
...(phase.cause !== undefined ? { cause: phase.cause } : {}),
|
|
2105
|
+
},
|
|
2106
|
+
[roundNote(round, "aborted")],
|
|
2107
|
+
);
|
|
1977
2108
|
if (phase.dead === "failed")
|
|
1978
2109
|
return end(
|
|
1979
2110
|
next,
|
|
@@ -2169,10 +2300,14 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2169
2300
|
case "read": {
|
|
2170
2301
|
const r = ret as Extract<StepReturn, { type: "read-record" }>;
|
|
2171
2302
|
if (!r.run.finished)
|
|
2303
|
+
// `restartedAs` (issues 1903/1876): the child's container was replaced
|
|
2304
|
+
// and it restarted from its request as a new run — the round carries on
|
|
2305
|
+
// waiting on the successor, and the unit never ends over a resume that
|
|
2306
|
+
// succeeded. The next wait and read follow the successor's id.
|
|
2172
2307
|
return {
|
|
2173
2308
|
state: {
|
|
2174
2309
|
...clocked,
|
|
2175
|
-
phase: { at: "wait", round: p.round, runId: p.runId, n: p.n + 1, until: p.until },
|
|
2310
|
+
phase: { at: "wait", round: p.round, runId: r.restartedAs ?? p.runId, n: p.n + 1, until: p.until },
|
|
2176
2311
|
},
|
|
2177
2312
|
notes: [],
|
|
2178
2313
|
};
|
|
@@ -2192,17 +2327,35 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2192
2327
|
[roundNote(p.round, "stopped")],
|
|
2193
2328
|
);
|
|
2194
2329
|
if (r.run.status === "interrupted") {
|
|
2330
|
+
const cause = r.run.interruption;
|
|
2195
2331
|
// A dead CODING child may have pushed before the ledger closed it: the
|
|
2196
2332
|
// pr-check recovers the branch. A review child has nothing on the
|
|
2197
2333
|
// branch to recover, so its interruption still ends the unit at once.
|
|
2198
2334
|
if (p.round.kind !== "review")
|
|
2199
2335
|
return {
|
|
2200
|
-
state: {
|
|
2336
|
+
state: {
|
|
2337
|
+
...clocked,
|
|
2338
|
+
phase: {
|
|
2339
|
+
at: "pr-check",
|
|
2340
|
+
round: p.round,
|
|
2341
|
+
runId: p.runId,
|
|
2342
|
+
dead: "interrupted",
|
|
2343
|
+
...(cause !== undefined ? { cause } : {}),
|
|
2344
|
+
},
|
|
2345
|
+
},
|
|
2201
2346
|
notes: [],
|
|
2202
2347
|
};
|
|
2203
|
-
return end(
|
|
2204
|
-
|
|
2205
|
-
|
|
2348
|
+
return end(
|
|
2349
|
+
clocked,
|
|
2350
|
+
{
|
|
2351
|
+
kind: "interrupted",
|
|
2352
|
+
round: p.round,
|
|
2353
|
+
runId: p.runId,
|
|
2354
|
+
reviewRounds: s.reviewRounds,
|
|
2355
|
+
...(cause !== undefined ? { cause } : {}),
|
|
2356
|
+
},
|
|
2357
|
+
[roundNote(p.round, "aborted")],
|
|
2358
|
+
);
|
|
2206
2359
|
}
|
|
2207
2360
|
return p.round.kind === "review"
|
|
2208
2361
|
? settleReview(clocked, p.round, r.run)
|
|
@@ -2212,7 +2365,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2212
2365
|
return settlePrCheck(clocked, p, (ret as Extract<StepReturn, { type: "pr-check" }>).pr);
|
|
2213
2366
|
case "checks": {
|
|
2214
2367
|
const r = ret as Extract<StepReturn, { type: "checks" }>;
|
|
2215
|
-
return settleChecks(clocked, p, r.checks, r.retried);
|
|
2368
|
+
return settleChecks(clocked, p, r.checks, r.retried, r.draft);
|
|
2216
2369
|
}
|
|
2217
2370
|
case "checks-wait":
|
|
2218
2371
|
// The event fired or the chunk elapsed either way the head is read again.
|
|
@@ -2431,7 +2584,7 @@ export interface CommitChecksFacts {
|
|
|
2431
2584
|
* the line is unchanged. */
|
|
2432
2585
|
function mergeReadyHeadline(rounds: string, url: string, base: string, facts: MergeReadyFacts | undefined): string {
|
|
2433
2586
|
if (facts?.mergeableState === "dirty")
|
|
2434
|
-
return `⚠️ Approved but not merge-ready after ${rounds}: ${url} — the head conflicts with \`${base}\`: rebase onto \`${base}
|
|
2587
|
+
return `⚠️ Approved but not merge-ready after ${rounds}: ${url} — the head conflicts with \`${base}\`: \`pulls rebase ${url}\` rebases it onto \`${base}\` (an unchanged patch carries the approval). The approved work stands.`;
|
|
2435
2588
|
const fixups = facts?.fixupCommits ?? [];
|
|
2436
2589
|
if (fixups.length > 0)
|
|
2437
2590
|
return `⚠️ Approved but not merge-ready after ${rounds}: ${url} — ${fixups.length} unsquashed fix-up commit${fixups.length === 1 ? "" : "s"} on the head (${fixups.join("; ")}): squash into the unit's commit, push, and re-review.`;
|
|
@@ -2450,13 +2603,16 @@ function mergeReadyHeadline(rounds: string, url: string, base: string, facts: Me
|
|
|
2450
2603
|
export function renderUnitReport(
|
|
2451
2604
|
s: UnitPipelineState,
|
|
2452
2605
|
facts?: MergeReadyFacts,
|
|
2453
|
-
/** The level the report speaks at (routing-and-config item 28
|
|
2454
|
-
* is the full report — the row's and the board's copy;
|
|
2455
|
-
* rendered at the request's level, where
|
|
2456
|
-
*
|
|
2457
|
-
*
|
|
2458
|
-
*
|
|
2459
|
-
*
|
|
2606
|
+
/** The level the report speaks at (routing-and-config item 28, record
|
|
2607
|
+
* 0066). The default is the full report — the row's and the board's copy;
|
|
2608
|
+
* the thread's copy is rendered at the request's level, where only the
|
|
2609
|
+
* outcome is everyone's: ONE line in the user's words — `✅ Merge-ready
|
|
2610
|
+
* after 2 review rounds: <url>`, `Held: <the human-gated row>`, `Stopped`,
|
|
2611
|
+
* `Round cap reached: <counts>` — and the verdict, the findings left below
|
|
2612
|
+
* the gate, the declined ones, the remaining-gate sentence, the level in
|
|
2613
|
+
* force, the grant, the write-up pointer, the budget split, the re-issue
|
|
2614
|
+
* prompt and a segment boundary are `verbose` asides; the full detail
|
|
2615
|
+
* stays on the pull request, where the round routes put it. */
|
|
2460
2616
|
verbosity: Verbosity = "verbose",
|
|
2461
2617
|
): string {
|
|
2462
2618
|
const e = s.ending;
|
|
@@ -2495,44 +2651,62 @@ export function renderUnitReport(
|
|
|
2495
2651
|
switch (e.kind) {
|
|
2496
2652
|
case "merged":
|
|
2497
2653
|
if (e.by === "other")
|
|
2498
|
-
return `✅ 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
|
|
2654
|
+
return `✅ 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.`;
|
|
2499
2655
|
return [
|
|
2500
|
-
`✅ Merged after ${rounds}: ${e.pr.url} (squash \`${e.sha.slice(0, 7)}\`) — merged by the
|
|
2501
|
-
verdictLine,
|
|
2656
|
+
`✅ 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.`,
|
|
2657
|
+
aside(verdictLine),
|
|
2502
2658
|
aside(levelLine),
|
|
2503
2659
|
aside(grantLine),
|
|
2504
|
-
skippedLine,
|
|
2505
|
-
declinedLine,
|
|
2660
|
+
aside(skippedLine),
|
|
2661
|
+
aside(declinedLine),
|
|
2506
2662
|
]
|
|
2507
2663
|
.filter(Boolean)
|
|
2508
2664
|
.join("\n");
|
|
2509
2665
|
case "already_landed":
|
|
2510
2666
|
// No compare link, no renewal line, no re-issue prompt: there was
|
|
2511
2667
|
// nothing to ship, so none of them has a question to answer.
|
|
2512
|
-
return `✅ Already on \`${s.input.base}\`: the unit's scope landed before this
|
|
2668
|
+
return `✅ Already on \`${s.input.base}\`: the unit's scope landed before this pipeline — ${e.landed.map((l) => `${l.what} (${l.where})`).join("; ")}. The coding child (run ${e.runId}) found it there and pushed nothing of its own: \`${s.input.unit.branch}\` has no commits over \`${s.input.base}\`, so there is no pull request to open or review. The unit is done and its dependents start on a base that carries it.`;
|
|
2513
2669
|
case "merge_ready":
|
|
2514
2670
|
return [
|
|
2515
2671
|
// A merge that already happened outranks the checks: there is no head left to gate.
|
|
2516
2672
|
facts?.merged
|
|
2517
2673
|
? `✅ Merge-ready after ${rounds}: ${e.pr.url}`
|
|
2518
2674
|
: mergeReadyHeadline(rounds, e.pr.url, s.input.base, facts),
|
|
2519
|
-
verdictLine,
|
|
2675
|
+
aside(verdictLine),
|
|
2520
2676
|
aside(levelLine),
|
|
2521
2677
|
aside(grantLine),
|
|
2522
|
-
skippedLine,
|
|
2523
|
-
declinedLine,
|
|
2678
|
+
aside(skippedLine),
|
|
2679
|
+
aside(declinedLine),
|
|
2524
2680
|
// What the driver read at the approved head when it composed this
|
|
2525
2681
|
// ending (agent-ship item 9): a merge that already happened is named
|
|
2526
2682
|
// as such, else the pull request's own auto-merge fact, else the gate.
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2683
|
+
// A `verbose` aside like the rest — the quiet thread copy is the
|
|
2684
|
+
// headline alone (record 0066).
|
|
2685
|
+
aside(
|
|
2686
|
+
facts?.merged
|
|
2687
|
+
? `Already merged: ${e.pr.url} (merge commit \`${facts.merged.sha.slice(0, 7)}\`, merged ${facts.merged.mergedAt}) — auto-merge or a person merged it after the approval; the pipeline merged nothing.`
|
|
2688
|
+
: facts?.autoMergeEnabled
|
|
2689
|
+
? "Auto-merge is on for this pull request: the approval merges it once checks pass."
|
|
2690
|
+
: "Remaining gate: a person's merge — the pipeline merges only when the plan's `merge` setting says so, and ship never approves.",
|
|
2691
|
+
),
|
|
2532
2692
|
]
|
|
2533
2693
|
.filter(Boolean)
|
|
2534
2694
|
.join("\n");
|
|
2535
2695
|
case "held": {
|
|
2696
|
+
// The draft hold (issue 2063): the pull request is a draft, so nothing
|
|
2697
|
+
// can merge and no fix round would change anything — the one line names
|
|
2698
|
+
// the cause and the person's exact next step.
|
|
2699
|
+
if (e.cause === "draft") {
|
|
2700
|
+
const link = e.pr !== undefined ? ` — ${e.pr.url}` : "";
|
|
2701
|
+
if (!shows(verbosity, "verbose")) return `⏸️ Held: draft — mark it ready to continue${link}`;
|
|
2702
|
+
const draftReissue = s.input.generated
|
|
2703
|
+
? `To continue, mark it ready and re-issue \`agent:ship\` in this thread with only the PR URL${e.pr !== undefined ? ` (${e.pr.url})` : ""} — no new task text; the re-issued pipeline resumes at the review round.`
|
|
2704
|
+
: `To continue, mark it ready; ${reissue.charAt(0).toLowerCase()}${reissue.slice(1)}`;
|
|
2705
|
+
return join([
|
|
2706
|
+
`⏸️ 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.`,
|
|
2707
|
+
draftReissue,
|
|
2708
|
+
]);
|
|
2709
|
+
}
|
|
2536
2710
|
// The person's next step is the report's whole point (issue 1990): the
|
|
2537
2711
|
// human-gated rows are named with the reviewer's own words, and the
|
|
2538
2712
|
// re-issue line says the attempt resumes at the review round — item 10's
|
|
@@ -2542,13 +2716,16 @@ export function renderUnitReport(
|
|
|
2542
2716
|
// first (item 10), the very round this ending exists to avoid — so the
|
|
2543
2717
|
// held case renders its own re-issue line instead of the shared one.
|
|
2544
2718
|
const rows = e.findings.map((f) => `${f.id} (${f.severity}) — ${f.title}`).join("; ");
|
|
2719
|
+
// The quiet copy is the ending in the user's words (record 0066): the
|
|
2720
|
+
// held row named, the pull request linked, nothing about the machinery.
|
|
2721
|
+
if (!shows(verbosity, "verbose")) return `⏸️ Held: ${rows}${e.pr !== undefined ? ` — ${e.pr.url}` : ""}`;
|
|
2545
2722
|
const heldReissue = s.input.generated
|
|
2546
2723
|
? `To continue, re-issue \`agent:ship\` in this thread with only the PR URL${e.pr !== undefined ? ` (${e.pr.url})` : ""} — no new task text.`
|
|
2547
2724
|
: reissue;
|
|
2548
2725
|
return join([
|
|
2549
2726
|
`⏸️ ${e.verdict === "approve" ? "Approved but held" : "Changes requested but held"} 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}. No fix round was opened: a coding child cannot produce the receipt.`,
|
|
2550
|
-
|
|
2551
|
-
`Next step: produce the receipt each finding names and post it on the pull request. ${heldReissue} The re-issued
|
|
2727
|
+
levelLine,
|
|
2728
|
+
`Next step: produce the receipt each finding names and post it on the pull request. ${heldReissue} The re-issued pipeline resumes at the review round — no coding round runs first.`,
|
|
2552
2729
|
]);
|
|
2553
2730
|
}
|
|
2554
2731
|
case "merge_refused":
|
|
@@ -2558,40 +2735,56 @@ export function renderUnitReport(
|
|
|
2558
2735
|
// `already_landed`) and moves on to the dependents. The generated
|
|
2559
2736
|
// plan's line already says to re-issue with the PR URL, which takes the
|
|
2560
2737
|
// same recognition path.
|
|
2738
|
+
// The quiet copy: the outcome in the user's words with the person's
|
|
2739
|
+
// remedy — the hand merge is what a person must act on (record 0066).
|
|
2740
|
+
if (!shows(verbosity, "verbose")) return `⚠️ Not merged: ${e.reason} — ${e.pr.url}`;
|
|
2561
2741
|
return join([
|
|
2562
|
-
`⚠️ The review approved ${e.pr.url} but the
|
|
2742
|
+
`⚠️ The review approved ${e.pr.url} but the pipeline did not merge it: ${e.reason}. A person decides what becomes of the pull request.`,
|
|
2563
2743
|
s.input.generated
|
|
2564
2744
|
? reissue
|
|
2565
2745
|
: `The approved work is on the branch: rebase or fix it, push, and merge it by hand. Then re-issue the plan naming the remaining units — a unit whose pull request has merged is recognized and not run again, and its dependents start from there.`,
|
|
2566
2746
|
]);
|
|
2567
|
-
case "round_cap":
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2747
|
+
case "round_cap": {
|
|
2748
|
+
// The quiet copy counts the last review's open findings, only the
|
|
2749
|
+
// non-zero severities (record 0066): `Round cap reached: 2 blockers, 1 major`.
|
|
2750
|
+
if (!shows(verbosity, "verbose")) {
|
|
2751
|
+
const counts = severityCounts(s.findingsByRound[e.reviewRounds] ?? []);
|
|
2752
|
+
return `🧢 Round cap reached${counts ? `: ${counts}` : ""}${prUrl !== undefined ? ` — ${prUrl}` : ""}`;
|
|
2753
|
+
}
|
|
2754
|
+
// The cap bounds fix rounds, never the terminal steps (issue 2023): an
|
|
2755
|
+
// approval in the last allowed round still runs the checks step and the
|
|
2756
|
+
// merge, so a round_cap after an approve means the checks (or the merge
|
|
2757
|
+
// queue) failed at the approved head with no fix round left — and the
|
|
2758
|
+
// report names those findings instead of claiming no approval landed.
|
|
2759
|
+
const failedChecks = checkFindingsOf(s.findingsByRound[s.reviewRounds]);
|
|
2760
|
+
const headline =
|
|
2761
|
+
failedChecks.length > 0
|
|
2762
|
+
? `🧢 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}`
|
|
2763
|
+
: `🧢 Ship stopped at a cap: the ${e.maxRounds}-round cap — no approval after ${rounds}.${prLine}`;
|
|
2764
|
+
return join([headline, splitReport(s), reissue]);
|
|
2765
|
+
}
|
|
2573
2766
|
case "wall_clock_cap":
|
|
2767
|
+
if (!shows(verbosity, "verbose")) return `🧢 Out of budget — no approval after ${rounds}.${prLine}`;
|
|
2574
2768
|
return join([
|
|
2575
2769
|
`🧢 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}`,
|
|
2576
|
-
|
|
2770
|
+
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
2577
2771
|
splitReport(s),
|
|
2578
2772
|
reissue,
|
|
2579
2773
|
]);
|
|
2580
2774
|
case "review_pending":
|
|
2581
2775
|
return join([
|
|
2582
|
-
`⏳ 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
|
|
2776
|
+
`⏳ 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.`,
|
|
2583
2777
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
2584
|
-
reissue,
|
|
2778
|
+
aside(reissue),
|
|
2585
2779
|
]);
|
|
2586
2780
|
case "stopped":
|
|
2781
|
+
if (!shows(verbosity, "verbose")) return `${e.mode === "hard" ? "⛔" : "⏹"} Stopped.${prLine}`;
|
|
2587
2782
|
return join([
|
|
2588
2783
|
`${e.mode === "hard" ? "⛔" : "⏹"} Ship stopped by operator (${e.mode} stop) after ${rounds}.${prLine}`,
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
e.round.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
|
|
2594
|
-
),
|
|
2784
|
+
writeUpPointer(
|
|
2785
|
+
s,
|
|
2786
|
+
e.round.kind,
|
|
2787
|
+
e.round.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
|
|
2595
2788
|
),
|
|
2596
2789
|
e.postedReview
|
|
2597
2790
|
? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
|
|
@@ -2599,14 +2792,13 @@ export function renderUnitReport(
|
|
|
2599
2792
|
reissue,
|
|
2600
2793
|
]);
|
|
2601
2794
|
case "aborted":
|
|
2795
|
+
if (!shows(verbosity, "verbose")) return `⚠️ Aborted after ${rounds}: ${e.reason}${prLine}`;
|
|
2602
2796
|
return join([
|
|
2603
2797
|
e.reason,
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
e.round?.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
|
|
2609
|
-
),
|
|
2798
|
+
writeUpPointer(
|
|
2799
|
+
s,
|
|
2800
|
+
e.round?.kind ?? "coding",
|
|
2801
|
+
e.round?.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
|
|
2610
2802
|
),
|
|
2611
2803
|
e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
|
|
2612
2804
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
@@ -2619,29 +2811,33 @@ export function renderUnitReport(
|
|
|
2619
2811
|
if (!shows(verbosity, "verbose")) return "";
|
|
2620
2812
|
return join([
|
|
2621
2813
|
writeUpPointer(s, e.round.kind, e.runId),
|
|
2622
|
-
`🔁
|
|
2814
|
+
`🔁 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` : ""}.`,
|
|
2623
2815
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
2624
2816
|
]);
|
|
2625
2817
|
case "transient":
|
|
2818
|
+
if (!shows(verbosity, "verbose"))
|
|
2819
|
+
return `⚠️ Aborted after ${rounds}: the model provider failed twice; re-issue once it settles.${prLine}`;
|
|
2626
2820
|
return join([
|
|
2627
2821
|
`⚠️ 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.`,
|
|
2628
|
-
|
|
2822
|
+
writeUpPointer(s, e.round.kind, s.lastCodingRunId),
|
|
2629
2823
|
`⚠️ Ship ended after ${rounds}; re-issue once the provider settles.`,
|
|
2630
2824
|
reissue,
|
|
2631
2825
|
]);
|
|
2632
2826
|
case "no_verdict":
|
|
2827
|
+
if (!shows(verbosity, "verbose"))
|
|
2828
|
+
return `⚠️ No verdict from review round ${e.round.index} — aborted after ${rounds}.${prLine}`;
|
|
2633
2829
|
return join([
|
|
2634
2830
|
`⚠️ 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.`,
|
|
2635
|
-
|
|
2831
|
+
writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index]),
|
|
2636
2832
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
2637
2833
|
reissue,
|
|
2638
2834
|
]);
|
|
2639
2835
|
case "interrupted":
|
|
2640
|
-
return shipInterruptedNote(prUrl);
|
|
2836
|
+
return shipInterruptedNote(prUrl, e.cause);
|
|
2641
2837
|
case "refused":
|
|
2642
2838
|
return join([
|
|
2643
2839
|
`🚫 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.`,
|
|
2644
|
-
reissue,
|
|
2840
|
+
aside(reissue),
|
|
2645
2841
|
]);
|
|
2646
2842
|
case "idle":
|
|
2647
2843
|
// The old kind's sentence at this copy's level — the report is unchanged
|