@coreplane/switchboard 1.255.0 → 1.256.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/deploy/cloudflare-resident/drain.ts +55 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +177 -32
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +31 -0
- package/dist/assets/src/core/coordinator/driver.ts +11 -1
- package/dist/assets/src/core/pipelineStanding.ts +57 -0
- package/dist/assets/src/core/reviewVerdict.ts +49 -0
- package/dist/assets/src/core/ship/contract.ts +20 -30
- package/dist/assets/src/core/ship/coordinator.ts +195 -68
- package/dist/assets/web/dist/.vite/manifest.json +31 -31
- package/dist/assets/web/dist/assets/{HomePage-Be7jLLnU.js → HomePage-BEiStFwW.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-D_RD6wLo.js → ResidentDetailPage-jYhEeeyu.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BLkSuCxo.js → ResidentsIndexPage-DvFmlV4U.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DvWzR1JQ.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-CvZ-TOT3.js +9 -0
- package/dist/assets/web/dist/assets/{RunsIndexPage-DW-HHuZa.js → RunsIndexPage-JqVSDOok.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-3aYsDf-q.js → ScheduledPage-Viim-bus.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-DG-p5Xy1.js → SettingsPage-CYUy8McC.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-CEnGlyAL.js → StatusDot-Dv6UMaPy.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CiunVowT.js → Tooltip-Bd-5Rypv.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-B81wEnKN.js +1 -0
- package/dist/assets/web/dist/assets/{dist-luhv3YSo.js → dist-Nl3uaxrP.js} +1 -1
- package/dist/assets/web/dist/assets/{main-mAKx_zo9.js → main-DRxWlffc.js} +2 -2
- package/dist/cli.js +319 -64
- package/package.json +1 -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/UnitRoutePage-iOYnmTcQ.js +0 -1
|
@@ -439,42 +439,32 @@ export const TIMEOUT_ON_LONG_COMMANDS =
|
|
|
439
439
|
"State a timeout on any command you expect to run longer than a minute: a timeout that reaches past the loop's " +
|
|
440
440
|
"end is refused before the command runs, never cut midway.";
|
|
441
441
|
|
|
442
|
-
/** The fast gates a plan child runs before every push
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
export const FAST_GATES_BEFORE_PUSH =
|
|
462
|
-
"The fast gates, before every push — each scoped to the changed set, never the whole project: " +
|
|
463
|
-
`${TOUCHED_TESTS_COMMAND} once (never \`--changed\`, never a directory: on a moving base that is most of the suite), ` +
|
|
464
|
-
"`tsc --noEmit -p` the touched tsconfig under `NODE_OPTIONS=--max-old-space-size=6144`, " +
|
|
465
|
-
"`npx prettier --check` on the changed files, `npm run hygiene:check` and `npm run specs:check` — " +
|
|
466
|
-
"then your judgement on what else this change needs, not a longer checklist. Every CI pipeline runs the " +
|
|
467
|
-
"tests, the types, the formatting and the full verification on your push, so you never run them again: " +
|
|
468
|
-
"you validate and fix your own change before pushing, at the changed-set scope. Passing the full test suite " +
|
|
469
|
-
"and the full typecheck is NOT part of your criteria: CI is that gate and the only place they run — on a " +
|
|
470
|
-
"shared resident they cost minutes that every other run pays for. " +
|
|
442
|
+
/** The fast gates a plan child runs before every push live in the coding
|
|
443
|
+
* preset's own instructions (`FAST_GATES_BEFORE_PUSH`,
|
|
444
|
+
* src/agents/registry.ts — agent-coding item 13; issue 1796): the changed-set
|
|
445
|
+
* forms with their commands named, the full verification named as CI's gate.
|
|
446
|
+
* The contract used to restate the whole paragraph, so every ask that was not
|
|
447
|
+
* a plan unit had to repeat it by hand; now the first instruction points at
|
|
448
|
+
* the preset's paragraph — the child reads the sentence once — and keeps only
|
|
449
|
+
* what is the contract's own: the receipts. Each gate is a receipt — the exit
|
|
450
|
+
* line goes into the PR description's validation table as the row's proof
|
|
451
|
+
* (the handoff has no verified list; parseHandoff carries deviations,
|
|
452
|
+
* followUps, unproven and landed), and a gate the child could not run goes
|
|
453
|
+
* under the handoff's unproven list, never claimed clean — because only a
|
|
454
|
+
* plan child has a handoff to route them to. */
|
|
455
|
+
export const FAST_GATES_POINTER =
|
|
456
|
+
"The fast gates are the ones your preset instructions name (THE FAST GATES): the changed-set forms, " +
|
|
457
|
+
"never the whole project — the full suite, the full typecheck and the full verification are CI's, " +
|
|
458
|
+
"never yours to run.";
|
|
459
|
+
|
|
460
|
+
export const GATE_RECEIPTS =
|
|
471
461
|
"Paste each command's exit line into the PR description's validation table as the row's proof; a gate you " +
|
|
472
462
|
"could not run goes under the handoff's unproven list and is never claimed clean.";
|
|
473
463
|
|
|
474
464
|
function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
|
|
475
465
|
const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
|
|
476
466
|
const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
|
|
477
|
-
const gates =
|
|
467
|
+
const gates = `${FAST_GATES_POINTER} ${GATE_RECEIPTS}`;
|
|
478
468
|
return (
|
|
479
469
|
`Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
|
|
480
470
|
`the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
|
|
@@ -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
|
|
@@ -766,7 +824,7 @@ export type UnitEnding =
|
|
|
766
824
|
* `transient` so it reads as a condition beside `checks_failed` and `held`
|
|
767
825
|
* in the plane's table, never as the child failing on its task. */
|
|
768
826
|
| { kind: "transient"; round: RoundRef; runId: string; reviewRounds: number }
|
|
769
|
-
| { kind: "interrupted"; round: RoundRef; runId: string; reviewRounds: number }
|
|
827
|
+
| { kind: "interrupted"; round: RoundRef; runId: string; reviewRounds: number; cause?: InterruptionCause }
|
|
770
828
|
| { kind: "refused"; refusal: string; message?: string; round: RoundRef; reviewRounds: number }
|
|
771
829
|
/** The unit idles instead of ending (record 0051): with the resolved
|
|
772
830
|
* `ship.idleDays` above zero, `end()` wraps an idling kind — every kind but
|
|
@@ -940,6 +998,8 @@ type Phase =
|
|
|
940
998
|
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
941
999
|
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
942
1000
|
dead?: "failed" | "interrupted";
|
|
1001
|
+
/** What ended the dead child (issue 1876), for the `interrupted` ending's sentence. */
|
|
1002
|
+
cause?: InterruptionCause;
|
|
943
1003
|
/** The dead child's record names a provider transient (`failure:
|
|
944
1004
|
* provider_transient`, issue 1932): with nothing pushed, round 0 is
|
|
945
1005
|
* re-run once instead of the unit aborting; a second transient in the
|
|
@@ -1812,9 +1872,17 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
1812
1872
|
// A dead child left nothing on the branch to recover: the unit ends with
|
|
1813
1873
|
// the child's own reason — never the budget clip.
|
|
1814
1874
|
if (phase.dead === "interrupted")
|
|
1815
|
-
return end(
|
|
1816
|
-
|
|
1817
|
-
|
|
1875
|
+
return end(
|
|
1876
|
+
s,
|
|
1877
|
+
{
|
|
1878
|
+
kind: "interrupted",
|
|
1879
|
+
round,
|
|
1880
|
+
runId: phase.runId,
|
|
1881
|
+
reviewRounds: s.reviewRounds,
|
|
1882
|
+
...(phase.cause !== undefined ? { cause: phase.cause } : {}),
|
|
1883
|
+
},
|
|
1884
|
+
[roundNote(round, "aborted")],
|
|
1885
|
+
);
|
|
1818
1886
|
if (phase.dead === "failed") {
|
|
1819
1887
|
// A provider transient with nothing pushed is not the child's failure
|
|
1820
1888
|
// (issue 1932): the ledger row and the branch are untouched, so round 0
|
|
@@ -1971,9 +2039,17 @@ function roundOnOpenPr(
|
|
|
1971
2039
|
// ends with the child's own reason — the ship-restart note for a bot
|
|
1972
2040
|
// roll, the failure for a failed run — never as the round's inaction.
|
|
1973
2041
|
if (phase.dead === "interrupted")
|
|
1974
|
-
return end(
|
|
1975
|
-
|
|
1976
|
-
|
|
2042
|
+
return end(
|
|
2043
|
+
next,
|
|
2044
|
+
{
|
|
2045
|
+
kind: "interrupted",
|
|
2046
|
+
round,
|
|
2047
|
+
runId: phase.runId,
|
|
2048
|
+
reviewRounds: next.reviewRounds,
|
|
2049
|
+
...(phase.cause !== undefined ? { cause: phase.cause } : {}),
|
|
2050
|
+
},
|
|
2051
|
+
[roundNote(round, "aborted")],
|
|
2052
|
+
);
|
|
1977
2053
|
if (phase.dead === "failed")
|
|
1978
2054
|
return end(
|
|
1979
2055
|
next,
|
|
@@ -2169,10 +2245,14 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2169
2245
|
case "read": {
|
|
2170
2246
|
const r = ret as Extract<StepReturn, { type: "read-record" }>;
|
|
2171
2247
|
if (!r.run.finished)
|
|
2248
|
+
// `restartedAs` (issues 1903/1876): the child's container was replaced
|
|
2249
|
+
// and it restarted from its request as a new run — the round carries on
|
|
2250
|
+
// waiting on the successor, and the unit never ends over a resume that
|
|
2251
|
+
// succeeded. The next wait and read follow the successor's id.
|
|
2172
2252
|
return {
|
|
2173
2253
|
state: {
|
|
2174
2254
|
...clocked,
|
|
2175
|
-
phase: { at: "wait", round: p.round, runId: p.runId, n: p.n + 1, until: p.until },
|
|
2255
|
+
phase: { at: "wait", round: p.round, runId: r.restartedAs ?? p.runId, n: p.n + 1, until: p.until },
|
|
2176
2256
|
},
|
|
2177
2257
|
notes: [],
|
|
2178
2258
|
};
|
|
@@ -2192,17 +2272,35 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2192
2272
|
[roundNote(p.round, "stopped")],
|
|
2193
2273
|
);
|
|
2194
2274
|
if (r.run.status === "interrupted") {
|
|
2275
|
+
const cause = r.run.interruption;
|
|
2195
2276
|
// A dead CODING child may have pushed before the ledger closed it: the
|
|
2196
2277
|
// pr-check recovers the branch. A review child has nothing on the
|
|
2197
2278
|
// branch to recover, so its interruption still ends the unit at once.
|
|
2198
2279
|
if (p.round.kind !== "review")
|
|
2199
2280
|
return {
|
|
2200
|
-
state: {
|
|
2281
|
+
state: {
|
|
2282
|
+
...clocked,
|
|
2283
|
+
phase: {
|
|
2284
|
+
at: "pr-check",
|
|
2285
|
+
round: p.round,
|
|
2286
|
+
runId: p.runId,
|
|
2287
|
+
dead: "interrupted",
|
|
2288
|
+
...(cause !== undefined ? { cause } : {}),
|
|
2289
|
+
},
|
|
2290
|
+
},
|
|
2201
2291
|
notes: [],
|
|
2202
2292
|
};
|
|
2203
|
-
return end(
|
|
2204
|
-
|
|
2205
|
-
|
|
2293
|
+
return end(
|
|
2294
|
+
clocked,
|
|
2295
|
+
{
|
|
2296
|
+
kind: "interrupted",
|
|
2297
|
+
round: p.round,
|
|
2298
|
+
runId: p.runId,
|
|
2299
|
+
reviewRounds: s.reviewRounds,
|
|
2300
|
+
...(cause !== undefined ? { cause } : {}),
|
|
2301
|
+
},
|
|
2302
|
+
[roundNote(p.round, "aborted")],
|
|
2303
|
+
);
|
|
2206
2304
|
}
|
|
2207
2305
|
return p.round.kind === "review"
|
|
2208
2306
|
? settleReview(clocked, p.round, r.run)
|
|
@@ -2450,13 +2548,16 @@ function mergeReadyHeadline(rounds: string, url: string, base: string, facts: Me
|
|
|
2450
2548
|
export function renderUnitReport(
|
|
2451
2549
|
s: UnitPipelineState,
|
|
2452
2550
|
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
|
-
*
|
|
2551
|
+
/** The level the report speaks at (routing-and-config item 28, record
|
|
2552
|
+
* 0066). The default is the full report — the row's and the board's copy;
|
|
2553
|
+
* the thread's copy is rendered at the request's level, where only the
|
|
2554
|
+
* outcome is everyone's: ONE line in the user's words — `✅ Merge-ready
|
|
2555
|
+
* after 2 review rounds: <url>`, `Held: <the human-gated row>`, `Stopped`,
|
|
2556
|
+
* `Round cap reached: <counts>` — and the verdict, the findings left below
|
|
2557
|
+
* the gate, the declined ones, the remaining-gate sentence, the level in
|
|
2558
|
+
* force, the grant, the write-up pointer, the budget split, the re-issue
|
|
2559
|
+
* prompt and a segment boundary are `verbose` asides; the full detail
|
|
2560
|
+
* stays on the pull request, where the round routes put it. */
|
|
2460
2561
|
verbosity: Verbosity = "verbose",
|
|
2461
2562
|
): string {
|
|
2462
2563
|
const e = s.ending;
|
|
@@ -2498,11 +2599,11 @@ export function renderUnitReport(
|
|
|
2498
2599
|
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 attempt reached it, by a person or by an earlier attempt of this plan; the runner merged nothing. The unit is done and its dependents start on a base that carries it.`;
|
|
2499
2600
|
return [
|
|
2500
2601
|
`✅ Merged after ${rounds}: ${e.pr.url} (squash \`${e.sha.slice(0, 7)}\`) — merged by the plan runner under \`plan:merge\`: the review approved at this head and the guards were green.`,
|
|
2501
|
-
verdictLine,
|
|
2602
|
+
aside(verdictLine),
|
|
2502
2603
|
aside(levelLine),
|
|
2503
2604
|
aside(grantLine),
|
|
2504
|
-
skippedLine,
|
|
2505
|
-
declinedLine,
|
|
2605
|
+
aside(skippedLine),
|
|
2606
|
+
aside(declinedLine),
|
|
2506
2607
|
]
|
|
2507
2608
|
.filter(Boolean)
|
|
2508
2609
|
.join("\n");
|
|
@@ -2516,19 +2617,23 @@ export function renderUnitReport(
|
|
|
2516
2617
|
facts?.merged
|
|
2517
2618
|
? `✅ Merge-ready after ${rounds}: ${e.pr.url}`
|
|
2518
2619
|
: mergeReadyHeadline(rounds, e.pr.url, s.input.base, facts),
|
|
2519
|
-
verdictLine,
|
|
2620
|
+
aside(verdictLine),
|
|
2520
2621
|
aside(levelLine),
|
|
2521
2622
|
aside(grantLine),
|
|
2522
|
-
skippedLine,
|
|
2523
|
-
declinedLine,
|
|
2623
|
+
aside(skippedLine),
|
|
2624
|
+
aside(declinedLine),
|
|
2524
2625
|
// What the driver read at the approved head when it composed this
|
|
2525
2626
|
// ending (agent-ship item 9): a merge that already happened is named
|
|
2526
2627
|
// as such, else the pull request's own auto-merge fact, else the gate.
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2628
|
+
// A `verbose` aside like the rest — the quiet thread copy is the
|
|
2629
|
+
// headline alone (record 0066).
|
|
2630
|
+
aside(
|
|
2631
|
+
facts?.merged
|
|
2632
|
+
? `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 runner merged nothing.`
|
|
2633
|
+
: facts?.autoMergeEnabled
|
|
2634
|
+
? "Auto-merge is on for this pull request: the approval merges it once checks pass."
|
|
2635
|
+
: "Remaining gate: a person's merge — the runner merges only when the instance's `merge` field says runner, and ship never approves.",
|
|
2636
|
+
),
|
|
2532
2637
|
]
|
|
2533
2638
|
.filter(Boolean)
|
|
2534
2639
|
.join("\n");
|
|
@@ -2542,12 +2647,15 @@ export function renderUnitReport(
|
|
|
2542
2647
|
// first (item 10), the very round this ending exists to avoid — so the
|
|
2543
2648
|
// held case renders its own re-issue line instead of the shared one.
|
|
2544
2649
|
const rows = e.findings.map((f) => `${f.id} (${f.severity}) — ${f.title}`).join("; ");
|
|
2650
|
+
// The quiet copy is the ending in the user's words (record 0066): the
|
|
2651
|
+
// held row named, the pull request linked, nothing about the machinery.
|
|
2652
|
+
if (!shows(verbosity, "verbose")) return `⏸️ Held: ${rows}${e.pr !== undefined ? ` — ${e.pr.url}` : ""}`;
|
|
2545
2653
|
const heldReissue = s.input.generated
|
|
2546
2654
|
? `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
2655
|
: reissue;
|
|
2548
2656
|
return join([
|
|
2549
2657
|
`⏸️ ${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
|
-
|
|
2658
|
+
levelLine,
|
|
2551
2659
|
`Next step: produce the receipt each finding names and post it on the pull request. ${heldReissue} The re-issued attempt resumes at the review round — no coding round runs first.`,
|
|
2552
2660
|
]);
|
|
2553
2661
|
}
|
|
@@ -2558,22 +2666,39 @@ export function renderUnitReport(
|
|
|
2558
2666
|
// `already_landed`) and moves on to the dependents. The generated
|
|
2559
2667
|
// plan's line already says to re-issue with the PR URL, which takes the
|
|
2560
2668
|
// same recognition path.
|
|
2669
|
+
// The quiet copy: the outcome in the user's words with the person's
|
|
2670
|
+
// remedy — the hand merge is what a person must act on (record 0066).
|
|
2671
|
+
if (!shows(verbosity, "verbose")) return `⚠️ Not merged: ${e.reason} — ${e.pr.url}`;
|
|
2561
2672
|
return join([
|
|
2562
2673
|
`⚠️ The review approved ${e.pr.url} but the runner did not merge it: ${e.reason}. A person decides what becomes of the pull request.`,
|
|
2563
2674
|
s.input.generated
|
|
2564
2675
|
? reissue
|
|
2565
2676
|
: `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
2677
|
]);
|
|
2567
|
-
case "round_cap":
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2678
|
+
case "round_cap": {
|
|
2679
|
+
// The quiet copy counts the last review's open findings, only the
|
|
2680
|
+
// non-zero severities (record 0066): `Round cap reached: 2 blockers, 1 major`.
|
|
2681
|
+
if (!shows(verbosity, "verbose")) {
|
|
2682
|
+
const counts = severityCounts(s.findingsByRound[e.reviewRounds] ?? []);
|
|
2683
|
+
return `🧢 Round cap reached${counts ? `: ${counts}` : ""}${prUrl !== undefined ? ` — ${prUrl}` : ""}`;
|
|
2684
|
+
}
|
|
2685
|
+
// The cap bounds fix rounds, never the terminal steps (issue 2023): an
|
|
2686
|
+
// approval in the last allowed round still runs the checks step and the
|
|
2687
|
+
// merge, so a round_cap after an approve means the checks (or the merge
|
|
2688
|
+
// queue) failed at the approved head with no fix round left — and the
|
|
2689
|
+
// report names those findings instead of claiming no approval landed.
|
|
2690
|
+
const failedChecks = checkFindingsOf(s.findingsByRound[s.reviewRounds]);
|
|
2691
|
+
const headline =
|
|
2692
|
+
failedChecks.length > 0
|
|
2693
|
+
? `🧢 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}`
|
|
2694
|
+
: `🧢 Ship stopped at a cap: the ${e.maxRounds}-round cap — no approval after ${rounds}.${prLine}`;
|
|
2695
|
+
return join([headline, splitReport(s), reissue]);
|
|
2696
|
+
}
|
|
2573
2697
|
case "wall_clock_cap":
|
|
2698
|
+
if (!shows(verbosity, "verbose")) return `🧢 Out of budget — no approval after ${rounds}.${prLine}`;
|
|
2574
2699
|
return join([
|
|
2575
2700
|
`🧢 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
|
-
|
|
2701
|
+
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
2577
2702
|
splitReport(s),
|
|
2578
2703
|
reissue,
|
|
2579
2704
|
]);
|
|
@@ -2581,17 +2706,16 @@ export function renderUnitReport(
|
|
|
2581
2706
|
return join([
|
|
2582
2707
|
`⏳ 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 attempt starts at the review round while the pull request still heads at the child's own last push.`,
|
|
2583
2708
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
2584
|
-
reissue,
|
|
2709
|
+
aside(reissue),
|
|
2585
2710
|
]);
|
|
2586
2711
|
case "stopped":
|
|
2712
|
+
if (!shows(verbosity, "verbose")) return `${e.mode === "hard" ? "⛔" : "⏹"} Stopped.${prLine}`;
|
|
2587
2713
|
return join([
|
|
2588
2714
|
`${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
|
-
),
|
|
2715
|
+
writeUpPointer(
|
|
2716
|
+
s,
|
|
2717
|
+
e.round.kind,
|
|
2718
|
+
e.round.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
|
|
2595
2719
|
),
|
|
2596
2720
|
e.postedReview
|
|
2597
2721
|
? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
|
|
@@ -2599,14 +2723,13 @@ export function renderUnitReport(
|
|
|
2599
2723
|
reissue,
|
|
2600
2724
|
]);
|
|
2601
2725
|
case "aborted":
|
|
2726
|
+
if (!shows(verbosity, "verbose")) return `⚠️ Aborted after ${rounds}: ${e.reason}${prLine}`;
|
|
2602
2727
|
return join([
|
|
2603
2728
|
e.reason,
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
e.round?.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
|
|
2609
|
-
),
|
|
2729
|
+
writeUpPointer(
|
|
2730
|
+
s,
|
|
2731
|
+
e.round?.kind ?? "coding",
|
|
2732
|
+
e.round?.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
|
|
2610
2733
|
),
|
|
2611
2734
|
e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
|
|
2612
2735
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
@@ -2623,25 +2746,29 @@ export function renderUnitReport(
|
|
|
2623
2746
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
2624
2747
|
]);
|
|
2625
2748
|
case "transient":
|
|
2749
|
+
if (!shows(verbosity, "verbose"))
|
|
2750
|
+
return `⚠️ Aborted after ${rounds}: the model provider failed twice; re-issue once it settles.${prLine}`;
|
|
2626
2751
|
return join([
|
|
2627
2752
|
`⚠️ 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
|
-
|
|
2753
|
+
writeUpPointer(s, e.round.kind, s.lastCodingRunId),
|
|
2629
2754
|
`⚠️ Ship ended after ${rounds}; re-issue once the provider settles.`,
|
|
2630
2755
|
reissue,
|
|
2631
2756
|
]);
|
|
2632
2757
|
case "no_verdict":
|
|
2758
|
+
if (!shows(verbosity, "verbose"))
|
|
2759
|
+
return `⚠️ No verdict from review round ${e.round.index} — aborted after ${rounds}.${prLine}`;
|
|
2633
2760
|
return join([
|
|
2634
2761
|
`⚠️ 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
|
-
|
|
2762
|
+
writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index]),
|
|
2636
2763
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
2637
2764
|
reissue,
|
|
2638
2765
|
]);
|
|
2639
2766
|
case "interrupted":
|
|
2640
|
-
return shipInterruptedNote(prUrl);
|
|
2767
|
+
return shipInterruptedNote(prUrl, e.cause);
|
|
2641
2768
|
case "refused":
|
|
2642
2769
|
return join([
|
|
2643
2770
|
`🚫 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,
|
|
2771
|
+
aside(reissue),
|
|
2645
2772
|
]);
|
|
2646
2773
|
case "idle":
|
|
2647
2774
|
// The old kind's sentence at this copy's level — the report is unchanged
|