@coreplane/switchboard 1.260.2 → 1.260.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/deploy/cloudflare-memory/backgroundTasks.ts +27 -3
- package/dist/assets/deploy/cloudflare-memory/testDiagnostics.ts +227 -0
- package/dist/assets/deploy/cloudflare-memory/testDiagnosticsProtocol.ts +22 -0
- package/dist/assets/deploy/cloudflare-memory/testDiagnosticsReporter.ts +88 -0
- package/dist/assets/deploy/cloudflare-memory/tsconfig.json +14 -2
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +11 -2
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +4 -3
- package/dist/assets/project.json +4 -0
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +6 -5
- package/dist/assets/src/core/coordinator/contract.ts +48 -2
- package/dist/assets/src/core/coordinator/driver.ts +40 -5
- package/dist/assets/src/core/reviewVerdict.ts +15 -5
- package/dist/assets/src/core/runFriction.ts +1 -1
- package/dist/assets/src/core/ship/contract.ts +2 -2
- package/dist/assets/src/core/ship/coordinator.ts +236 -108
- package/dist/assets/src/core/ship/renewal.ts +3 -1
- package/dist/assets/src/execution/bashTimeout.ts +1 -1
- package/dist/assets/src/execution/sandboxErrors.ts +6 -6
- package/dist/assets/web/dist/.vite/manifest.json +488 -488
- 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-BaemWKW8.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-Dk3HRBc3.js → HomePage-fgKQHLaO.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-vuWiDzvz.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-RD0M6O6W.js → PlanePage-DXMJf8FB.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Dl2KId95.js → ResidentDetailPage-DdC8yU_S.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-DBy43I1R.js → ResidentsIndexPage-CSM-KODv.js} +1 -1
- package/dist/assets/web/dist/assets/{RunFoldRow-CwHDL-Mo.js → RunFoldRow-Csik3wD6.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-Du1HqLZs.js → RunRoutePage-jo6NZxsP.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-BZphy3Hr.js → RunsIndexPage-Dx1Y4sSq.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-BuB_NZyR.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-Cn96jB6Q.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-CKxSxnQC.js → SilentTurn-D2cJfpEq.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-DDHUCPVJ.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-wVXCWhMp.js → Tooltip-B4VIHkAI.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-V5BXK7FH.js → UnitRoutePage-C-2DfjLI.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/{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-Cf1CmCSK.js} +2 -2
- 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/{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-DxPqaFu9.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-BzoeVxZB.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 +505 -253
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +0 -1
|
@@ -59,6 +59,7 @@ import { parsePlanUnit, planUnitIds } from "./contract.js";
|
|
|
59
59
|
import {
|
|
60
60
|
carve,
|
|
61
61
|
DEFAULT_GRANT,
|
|
62
|
+
IDLE_DAYS_MAX,
|
|
62
63
|
loopPosition,
|
|
63
64
|
MERGE_WAIT_ASK_MINUTES,
|
|
64
65
|
MINUTE_MS,
|
|
@@ -136,11 +137,11 @@ export function shipInterruptedNote(prUrl?: string, cause?: InterruptionCause, i
|
|
|
136
137
|
const stands = prUrl
|
|
137
138
|
? `Its work stands on GitHub: ${prUrl}.`
|
|
138
139
|
: "Whatever it pushed stands on its pipeline branch; no PR was opened yet.";
|
|
139
|
-
const
|
|
140
|
-
? "
|
|
140
|
+
const continuation = idle
|
|
141
|
+
? "The next reply in this thread continues the unit."
|
|
141
142
|
: prUrl
|
|
142
|
-
? `
|
|
143
|
-
: "
|
|
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.";
|
|
144
145
|
const opening =
|
|
145
146
|
cause === "container_replaced"
|
|
146
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."
|
|
@@ -149,7 +150,7 @@ export function shipInterruptedNote(prUrl?: string, cause?: InterruptionCause, i
|
|
|
149
150
|
: cause === "bot_restart"
|
|
150
151
|
? "⚠️ The bot restarted while this ship pipeline was running, so the pipeline stopped."
|
|
151
152
|
: "⚠️ This ship pipeline's child was interrupted and could not resume, so the pipeline stopped.";
|
|
152
|
-
return `${opening} ${stands} ${
|
|
153
|
+
return `${opening} ${stands} ${continuation}`;
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
// ---- the plan graph --------------------------------------------------------------------------------
|
|
@@ -494,8 +495,10 @@ export type Brief =
|
|
|
494
495
|
pr: number;
|
|
495
496
|
headSha?: string;
|
|
496
497
|
round: number;
|
|
497
|
-
/** The previous review round
|
|
498
|
-
|
|
498
|
+
/** The previous review round and the coding run that answered it. A
|
|
499
|
+
* recovered posted verdict carries findings by value when no run row
|
|
500
|
+
* belongs to this attempt. */
|
|
501
|
+
prior?: { reviewRunId?: string; codingRunId?: string; findings?: Finding[] };
|
|
499
502
|
/** The previous round's check findings (record 0055): they sit on no
|
|
500
503
|
* run's record, so the brief carries them beside `prior` by value. */
|
|
501
504
|
checks?: Finding[];
|
|
@@ -504,7 +507,12 @@ export type Brief =
|
|
|
504
507
|
kind: "findings";
|
|
505
508
|
unit: string;
|
|
506
509
|
pr: number;
|
|
507
|
-
|
|
510
|
+
/** The review run normally supplies the findings. An adopted attempt
|
|
511
|
+
* can recover them from the last posted verdict marker instead. */
|
|
512
|
+
reviewRunId?: string;
|
|
513
|
+
findings?: Finding[];
|
|
514
|
+
/** Human replies that answered a parked finding, attributed at intake. */
|
|
515
|
+
answers?: string[];
|
|
508
516
|
/** The round's check findings (record 0055), carried by value — the
|
|
509
517
|
* review run's record holds only the reviewer's own findings. */
|
|
510
518
|
checks?: Finding[];
|
|
@@ -522,6 +530,20 @@ export interface PrRef {
|
|
|
522
530
|
url: string;
|
|
523
531
|
}
|
|
524
532
|
|
|
533
|
+
/** A human-gated review question parked on its live unit. The review's typed
|
|
534
|
+
* findings and the pull request survive the wait; `reviewRunId` is absent when
|
|
535
|
+
* an adopted attempt recovered the question from GitHub's verdict marker. */
|
|
536
|
+
export interface HumanGatePending {
|
|
537
|
+
pr: PrRef;
|
|
538
|
+
round: number;
|
|
539
|
+
findings: Finding[];
|
|
540
|
+
verdict: "approve" | "request_changes";
|
|
541
|
+
reviewRunId?: string;
|
|
542
|
+
headSha?: string;
|
|
543
|
+
/** When the verdict asked the question, for fencing older unit events. */
|
|
544
|
+
askedAt?: number;
|
|
545
|
+
}
|
|
546
|
+
|
|
525
547
|
/** What the coordinator asks for next. `step` is the Workflow step's name, and
|
|
526
548
|
* for a spawn the second half of the child's idempotency key. */
|
|
527
549
|
export type CoordinatorAction =
|
|
@@ -577,6 +599,10 @@ export type ChildFacts =
|
|
|
577
599
|
| {
|
|
578
600
|
finished: true;
|
|
579
601
|
status: RunStatus;
|
|
602
|
+
/** The terminal record time; fallback fence for an older review record. */
|
|
603
|
+
finishedAt?: number;
|
|
604
|
+
/** When the posted verdict became the person's question. */
|
|
605
|
+
reviewAskedAt?: number;
|
|
580
606
|
finalReply?: string;
|
|
581
607
|
/** A coding child's `pr_opened`. */
|
|
582
608
|
pr?: { number: number; url: string; created: boolean };
|
|
@@ -659,6 +685,16 @@ export type PrCheck =
|
|
|
659
685
|
* read beside the checks on the ending's facts read, so a `merge_ready`
|
|
660
686
|
* report tells the person their merge is queued, never a direct one. */
|
|
661
687
|
baseHasMergeQueue?: boolean;
|
|
688
|
+
/** A person's newest pull-request comment answered the last posted
|
|
689
|
+
* human-gated verdict. Entry starts its fix round before any review. */
|
|
690
|
+
humanGate?: {
|
|
691
|
+
round: number;
|
|
692
|
+
findings: Finding[];
|
|
693
|
+
verdict: "approve" | "request_changes";
|
|
694
|
+
answer: string;
|
|
695
|
+
author: string;
|
|
696
|
+
commentId: string;
|
|
697
|
+
};
|
|
662
698
|
}
|
|
663
699
|
| { state: "merged"; prNumber: number; url: string; sha: string; mergedAt: string };
|
|
664
700
|
|
|
@@ -793,12 +829,9 @@ export type UnitEnding =
|
|
|
793
829
|
* The unit is done and its dependents start on a base that carries it. */
|
|
794
830
|
| { kind: "already_landed"; landed: HandoffLanded[]; round: RoundRef; runId: string; reviewRounds: number }
|
|
795
831
|
| { kind: "merge_ready"; pr: PrRef; reviewRounds: number }
|
|
796
|
-
/**
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
* nothing: the unit ends held for a person, the ending carrying the
|
|
800
|
-
* human-gated rows, and a re-issue with the pull request resumes at the
|
|
801
|
-
* review round once the receipt is posted (item 10's resume path). */
|
|
832
|
+
/** The report nested inside a parked human-gated question. The unit never
|
|
833
|
+
* publishes this as its terminal kind: `parkHumanGate` carries it under an
|
|
834
|
+
* idle ending until a person's answer opens the fix round. */
|
|
802
835
|
| {
|
|
803
836
|
kind: "held";
|
|
804
837
|
cause?: undefined;
|
|
@@ -938,6 +971,10 @@ export type UnitEnding =
|
|
|
938
971
|
handoff?: Handoff;
|
|
939
972
|
round?: RoundRef;
|
|
940
973
|
reviewRounds: number;
|
|
974
|
+
/** A parked human-gated review is always an idle, independent of the
|
|
975
|
+
* optional idle policy. Its question is the unit's pending state. */
|
|
976
|
+
humanGate?: HumanGatePending;
|
|
977
|
+
parkDays?: number;
|
|
941
978
|
};
|
|
942
979
|
|
|
943
980
|
/** The kinds that idle: every ending but the ended ones — the unit is
|
|
@@ -945,10 +982,9 @@ export type UnitEnding =
|
|
|
945
982
|
* a reply could continue it. `merged`, `already_landed`, `merge_ready` and
|
|
946
983
|
* `refused` never idle: the first two are done, merge-ready waits only for a
|
|
947
984
|
* person's merge, and a refused child would be refused again. A gated or
|
|
948
|
-
* draft `held` never idles either
|
|
949
|
-
*
|
|
950
|
-
*
|
|
951
|
-
* so it alone idles; `idleEnding` draws that line by cause. */
|
|
985
|
+
* draft `held` never idles either. A blocked hold (issue 2086) follows the
|
|
986
|
+
* configured idle policy; a human-gated hold takes `parkHumanGate` instead,
|
|
987
|
+
* which always creates the pending idle question. */
|
|
952
988
|
export type IdleWhy =
|
|
953
989
|
Exclude<UnitEnding["kind"], "idle" | "merged" | "already_landed" | "merge_ready" | "refused"> | RenewalWhy;
|
|
954
990
|
|
|
@@ -1014,6 +1050,9 @@ export interface LeaseSegmentProgress {
|
|
|
1014
1050
|
previousHandoff?: Handoff;
|
|
1015
1051
|
/** Words that woke an idle segment, already attributed in arrival order. */
|
|
1016
1052
|
texts?: string[];
|
|
1053
|
+
/** The human-gated question those words answer. It resumes as a findings
|
|
1054
|
+
* round before another review, without spending a renewal. */
|
|
1055
|
+
humanGate?: HumanGatePending;
|
|
1017
1056
|
/** A stopped segment reopens under the remainder of its cut lease. Each
|
|
1018
1057
|
* reopen gets a fresh attempt so its Workflow steps cannot replay the
|
|
1019
1058
|
* stopped attempt's cached answers. */
|
|
@@ -1165,6 +1204,8 @@ export interface UnitPipelineState {
|
|
|
1165
1204
|
* ids are unique within one round only. A disposition naming an id the review
|
|
1166
1205
|
* never issued is dropped at the match (`matchDispositions`). */
|
|
1167
1206
|
readonly findingsByRound: Readonly<Record<number, Finding[]>>;
|
|
1207
|
+
/** Human answers folded into a recovered/parked findings brief. */
|
|
1208
|
+
readonly humanAnswersByRound: Readonly<Record<number, string[]>>;
|
|
1168
1209
|
readonly dispositionsByRound: Readonly<Record<number, FindingDisposition[]>>;
|
|
1169
1210
|
readonly reviewRunByRound: Readonly<Record<number, string>>;
|
|
1170
1211
|
/** The coding run each round's findings step dispatched. */
|
|
@@ -1208,20 +1249,29 @@ export const BUSY_RETRY_MS = SHIP_WAIT.busyRetryMinutes * MIN;
|
|
|
1208
1249
|
// ---- the unit pipeline: opening and the next action -----------------------------------------------
|
|
1209
1250
|
|
|
1210
1251
|
export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipelineState {
|
|
1252
|
+
const pending = input.session?.humanGate;
|
|
1211
1253
|
const base: UnitPipelineState = {
|
|
1212
1254
|
input,
|
|
1213
1255
|
startedAt: at,
|
|
1214
1256
|
clock: at,
|
|
1215
1257
|
phase: { at: "pre-check" },
|
|
1216
|
-
reviewRounds: 0,
|
|
1258
|
+
reviewRounds: pending?.round ?? 0,
|
|
1217
1259
|
rebaseAttempts: 0,
|
|
1218
1260
|
spentMs: { coding: 0, review: 0, waiting: 0 },
|
|
1219
1261
|
spendUsd: input.session?.spendUsd ?? 0,
|
|
1220
|
-
findingsByRound: {},
|
|
1262
|
+
findingsByRound: pending ? { [pending.round]: pending.findings } : {},
|
|
1263
|
+
humanAnswersByRound: pending && input.session?.texts !== undefined ? { [pending.round]: input.session.texts } : {},
|
|
1221
1264
|
dispositionsByRound: {},
|
|
1222
|
-
reviewRunByRound: {},
|
|
1265
|
+
reviewRunByRound: pending?.reviewRunId !== undefined ? { [pending.round]: pending.reviewRunId } : {},
|
|
1223
1266
|
findingsRunByRound: {},
|
|
1267
|
+
...(pending !== undefined
|
|
1268
|
+
? {
|
|
1269
|
+
pr: pending.pr,
|
|
1270
|
+
...(pending.headSha !== undefined ? { lastReviewHead: pending.headSha } : {}),
|
|
1271
|
+
}
|
|
1272
|
+
: {}),
|
|
1224
1273
|
};
|
|
1274
|
+
if (pending !== undefined) return enterRound(base, { index: pending.round, kind: "findings" }).state;
|
|
1225
1275
|
if (!input.resume) return base;
|
|
1226
1276
|
const url = input.resume.url ?? `https://github.com/${input.repo}/pull/${input.resume.pr}`;
|
|
1227
1277
|
const resumed: UnitPipelineState = {
|
|
@@ -1294,26 +1344,40 @@ function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
|
|
|
1294
1344
|
return { kind: "rebase", unit, pr, headSha: s.lastReviewHead!, base: s.input.base };
|
|
1295
1345
|
}
|
|
1296
1346
|
if (round.kind === "findings") {
|
|
1297
|
-
const
|
|
1347
|
+
const issued = s.findingsByRound[round.index] ?? [];
|
|
1348
|
+
const checks = checkFindingsOf(issued);
|
|
1349
|
+
const reviewRunId = s.reviewRunByRound[round.index];
|
|
1350
|
+
const answers = s.humanAnswersByRound[round.index];
|
|
1298
1351
|
return {
|
|
1299
1352
|
kind: "findings",
|
|
1300
1353
|
unit,
|
|
1301
1354
|
pr,
|
|
1302
|
-
reviewRunId:
|
|
1355
|
+
...(reviewRunId !== undefined ? { reviewRunId } : {}),
|
|
1356
|
+
...(reviewRunId === undefined || (answers !== undefined && answers.length > 0) ? { findings: issued } : {}),
|
|
1357
|
+
...(answers !== undefined && answers.length > 0 ? { answers } : {}),
|
|
1303
1358
|
...(checks.length > 0 ? { checks } : {}),
|
|
1304
1359
|
};
|
|
1305
1360
|
}
|
|
1361
|
+
const priorFindings = s.findingsByRound[round.index - 1] ?? [];
|
|
1306
1362
|
const priorReview = s.reviewRunByRound[round.index - 1];
|
|
1307
1363
|
const priorCoding = s.findingsRunByRound[round.index - 1];
|
|
1308
|
-
const priorChecks = checkFindingsOf(
|
|
1364
|
+
const priorChecks = checkFindingsOf(priorFindings);
|
|
1309
1365
|
return {
|
|
1310
1366
|
kind: "review",
|
|
1311
1367
|
unit,
|
|
1312
1368
|
pr,
|
|
1313
1369
|
...(s.lastReviewHead !== undefined ? { headSha: s.lastReviewHead } : {}),
|
|
1314
1370
|
round: round.index,
|
|
1315
|
-
...(priorReview !== undefined
|
|
1316
|
-
? {
|
|
1371
|
+
...(priorReview !== undefined || (priorCoding !== undefined && priorFindings.length > 0)
|
|
1372
|
+
? {
|
|
1373
|
+
prior: {
|
|
1374
|
+
...(priorReview !== undefined ? { reviewRunId: priorReview } : {}),
|
|
1375
|
+
...(priorReview === undefined || s.humanAnswersByRound[round.index - 1] !== undefined
|
|
1376
|
+
? { findings: priorFindings }
|
|
1377
|
+
: {}),
|
|
1378
|
+
...(priorCoding !== undefined ? { codingRunId: priorCoding } : {}),
|
|
1379
|
+
},
|
|
1380
|
+
}
|
|
1317
1381
|
: {}),
|
|
1318
1382
|
...(priorChecks.length > 0 ? { checks: priorChecks } : {}),
|
|
1319
1383
|
};
|
|
@@ -1454,9 +1518,9 @@ function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[]
|
|
|
1454
1518
|
function idleEnding(s: UnitPipelineState, ending: UnitEnding): Extract<UnitEnding, { kind: "idle" }> | undefined {
|
|
1455
1519
|
if ((s.input.idleDays ?? 0) <= 0) return undefined;
|
|
1456
1520
|
if (NEVER_IDLES.has(ending.kind)) return undefined;
|
|
1457
|
-
// A
|
|
1458
|
-
//
|
|
1459
|
-
//
|
|
1521
|
+
// A draft hold is not continued by a reply. A blocked hold (issue 2086)
|
|
1522
|
+
// waits for the person's word. Human-gated questions bypass this mapping:
|
|
1523
|
+
// `parkHumanGate` always parks them with their typed pending state.
|
|
1460
1524
|
if (ending.kind === "held" && ending.cause !== "blocked") return undefined;
|
|
1461
1525
|
const old = ending as Exclude<UnitEnding, { kind: "idle" | "merged" | "already_landed" | "merge_ready" | "refused" }>;
|
|
1462
1526
|
const grant = s.input.grant ?? DEFAULT_GRANT;
|
|
@@ -1512,7 +1576,7 @@ const heldEnding = (
|
|
|
1512
1576
|
round: RoundRef,
|
|
1513
1577
|
findings: Finding[],
|
|
1514
1578
|
verdict: "approve" | "request_changes",
|
|
1515
|
-
): UnitEnding => ({
|
|
1579
|
+
): Extract<UnitEnding, { kind: "held" }> => ({
|
|
1516
1580
|
kind: "held",
|
|
1517
1581
|
...(s.pr !== undefined ? { pr: s.pr } : {}),
|
|
1518
1582
|
round,
|
|
@@ -1521,6 +1585,48 @@ const heldEnding = (
|
|
|
1521
1585
|
reviewRounds: s.reviewRounds,
|
|
1522
1586
|
});
|
|
1523
1587
|
|
|
1588
|
+
/** A human-gated finding is a question, not a terminal hold. Park the live
|
|
1589
|
+
* unit in record 0051's idle state regardless of the optional idle policy, so
|
|
1590
|
+
* either a thread reply or a pull-request comment can become its fix brief. */
|
|
1591
|
+
function parkHumanGate(
|
|
1592
|
+
s: UnitPipelineState,
|
|
1593
|
+
round: RoundRef,
|
|
1594
|
+
findings: Finding[],
|
|
1595
|
+
verdict: "approve" | "request_changes",
|
|
1596
|
+
askedAt: number,
|
|
1597
|
+
notes: CoordinatorNote[],
|
|
1598
|
+
): Transition {
|
|
1599
|
+
const held = heldEnding(s, round, findings, verdict);
|
|
1600
|
+
const humanGate: HumanGatePending = {
|
|
1601
|
+
pr: s.pr!,
|
|
1602
|
+
round: round.index,
|
|
1603
|
+
findings,
|
|
1604
|
+
verdict,
|
|
1605
|
+
...(s.reviewRunByRound[round.index] !== undefined ? { reviewRunId: s.reviewRunByRound[round.index] } : {}),
|
|
1606
|
+
...(s.lastReviewHead !== undefined ? { headSha: s.lastReviewHead } : {}),
|
|
1607
|
+
askedAt,
|
|
1608
|
+
};
|
|
1609
|
+
const grant = s.input.grant ?? DEFAULT_GRANT;
|
|
1610
|
+
const ending: Extract<UnitEnding, { kind: "idle" }> = {
|
|
1611
|
+
kind: "idle",
|
|
1612
|
+
why: "held",
|
|
1613
|
+
idled: held,
|
|
1614
|
+
renewalsLeft: Math.max(0, grant.renewals - (s.input.session?.renewalsSpent ?? 0)),
|
|
1615
|
+
...(s.lastReviewHead !== undefined ? { from: s.lastReviewHead } : {}),
|
|
1616
|
+
...(s.lastCodingRunId !== undefined ? { runId: s.lastCodingRunId } : {}),
|
|
1617
|
+
spendUsd: s.spendUsd,
|
|
1618
|
+
...(s.lastCodingHandoff !== undefined ? { handoff: s.lastCodingHandoff } : {}),
|
|
1619
|
+
round,
|
|
1620
|
+
reviewRounds: s.reviewRounds,
|
|
1621
|
+
humanGate,
|
|
1622
|
+
parkDays: IDLE_DAYS_MAX,
|
|
1623
|
+
};
|
|
1624
|
+
return {
|
|
1625
|
+
state: { ...s, phase: ENDED, ending },
|
|
1626
|
+
notes: [...notes, { type: "ended", ending }],
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1524
1630
|
const roundNote = (round: RoundRef, outcome: ShipRoundOutcome): RoundNote => ({
|
|
1525
1631
|
type: "round",
|
|
1526
1632
|
index: round.index,
|
|
@@ -1564,10 +1670,11 @@ function enterRound(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNot
|
|
|
1564
1670
|
};
|
|
1565
1671
|
}
|
|
1566
1672
|
|
|
1567
|
-
/** A rebase changed the approved patch,
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
|
|
1673
|
+
/** A rebase changed the approved patch, or a person's answer enabled a
|
|
1674
|
+
* human-gated fix, so review is mandatory even when the ordinary findings-
|
|
1675
|
+
* round count is spent. The run's remaining lease is the bound: `enterRound`
|
|
1676
|
+
* refuses when another review no longer fits. */
|
|
1677
|
+
function mandatoryReview(s: UnitPipelineState, notes: CoordinatorNote[] = []): Transition {
|
|
1571
1678
|
return enterRound(s, { index: s.reviewRounds + 1, kind: "review" }, notes);
|
|
1572
1679
|
}
|
|
1573
1680
|
|
|
@@ -1634,18 +1741,21 @@ function settleCoding(
|
|
|
1634
1741
|
[roundNote(round, "stopped")],
|
|
1635
1742
|
);
|
|
1636
1743
|
// A mechanical WIP push means the child did not declare the work ready for
|
|
1637
|
-
// review
|
|
1638
|
-
//
|
|
1639
|
-
|
|
1744
|
+
// review, whatever terminal status its own answer produced. Keep the unit
|
|
1745
|
+
// branch as the resumption point and abort this attempt; an ordinary push
|
|
1746
|
+
// still takes the established recover-PR path.
|
|
1747
|
+
if (checkpoint !== undefined) {
|
|
1640
1748
|
const cause =
|
|
1641
1749
|
facts.failure?.kind === "provider_transient"
|
|
1642
1750
|
? "the model provider's transport retry budget was spent"
|
|
1643
|
-
:
|
|
1751
|
+
: facts.status === "failed"
|
|
1752
|
+
? "it failed before finishing"
|
|
1753
|
+
: "it ended before its work was ready for review";
|
|
1644
1754
|
return end(
|
|
1645
1755
|
next,
|
|
1646
1756
|
{
|
|
1647
1757
|
kind: "aborted",
|
|
1648
|
-
reason: `⚠️ The coding child ended because ${cause}; the branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`.
|
|
1758
|
+
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.`,
|
|
1649
1759
|
round,
|
|
1650
1760
|
reviewRounds: next.reviewRounds,
|
|
1651
1761
|
},
|
|
@@ -1766,9 +1876,10 @@ function settleReview(
|
|
|
1766
1876
|
},
|
|
1767
1877
|
notes,
|
|
1768
1878
|
);
|
|
1769
|
-
// Every gated finding is human-gated
|
|
1770
|
-
//
|
|
1771
|
-
if (allHumanGated(gated))
|
|
1879
|
+
// Every gated finding is human-gated: ask a person and park the live
|
|
1880
|
+
// unit; an answer later opens the fix round with the question attached.
|
|
1881
|
+
if (allHumanGated(gated))
|
|
1882
|
+
return parkHumanGate(next, round, gated, "approve", facts.reviewAskedAt ?? facts.finishedAt ?? s.clock, notes);
|
|
1772
1883
|
if (next.reviewRounds >= next.input.caps.maxRounds)
|
|
1773
1884
|
return end(
|
|
1774
1885
|
next,
|
|
@@ -1797,11 +1908,18 @@ function settleReview(
|
|
|
1797
1908
|
},
|
|
1798
1909
|
notes,
|
|
1799
1910
|
);
|
|
1800
|
-
// Every finding of the round is human-gated
|
|
1801
|
-
//
|
|
1802
|
-
//
|
|
1911
|
+
// Every finding of the round is human-gated: no fix round can change
|
|
1912
|
+
// anything yet, so park the live unit for a person's answer before asking
|
|
1913
|
+
// the round cap.
|
|
1803
1914
|
if (allHumanGated(verdict.findings ?? []))
|
|
1804
|
-
return
|
|
1915
|
+
return parkHumanGate(
|
|
1916
|
+
next,
|
|
1917
|
+
round,
|
|
1918
|
+
verdict.findings ?? [],
|
|
1919
|
+
"request_changes",
|
|
1920
|
+
facts.reviewAskedAt ?? facts.finishedAt ?? s.clock,
|
|
1921
|
+
notes,
|
|
1922
|
+
);
|
|
1805
1923
|
if (next.reviewRounds >= next.input.caps.maxRounds)
|
|
1806
1924
|
return end(
|
|
1807
1925
|
next,
|
|
@@ -2303,12 +2421,14 @@ function roundOnOpenPr(
|
|
|
2303
2421
|
},
|
|
2304
2422
|
[roundNote(round, "aborted")],
|
|
2305
2423
|
);
|
|
2306
|
-
return
|
|
2424
|
+
return mandatoryReview({ ...next, lastReviewHead: pushed }, [roundNote(round, "pr_opened")]);
|
|
2307
2425
|
}
|
|
2426
|
+
const hasHumanAnswer = round.kind === "findings" && (s.humanAnswersByRound[round.index]?.length ?? 0) > 0;
|
|
2308
2427
|
if (round.kind === "findings") {
|
|
2309
2428
|
// Nothing repushed → nothing to re-review, unless every finding of the
|
|
2310
2429
|
// last review was declined on the record: that re-review verifies the
|
|
2311
|
-
// arguments and may concede.
|
|
2430
|
+
// arguments and may concede. A person's answer is itself new evidence for
|
|
2431
|
+
// the mandatory re-review, even when accepting it changes no code.
|
|
2312
2432
|
const codingHead = normalizeHead(head);
|
|
2313
2433
|
const reviewedAt = normalizeHead(s.lastReviewHead);
|
|
2314
2434
|
if (codingHead !== undefined && reviewedAt !== undefined && sameCommit(codingHead, reviewedAt)) {
|
|
@@ -2339,27 +2459,30 @@ function roundOnOpenPr(
|
|
|
2339
2459
|
},
|
|
2340
2460
|
[roundNote(round, "aborted")],
|
|
2341
2461
|
);
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2462
|
+
if (!hasHumanAnswer) {
|
|
2463
|
+
const findings = s.findingsByRound[round.index] ?? [];
|
|
2464
|
+
const dispositions = s.dispositionsByRound[round.index] ?? [];
|
|
2465
|
+
const allDeclined =
|
|
2466
|
+
findings.length > 0 &&
|
|
2467
|
+
findings.every((f) => dispositions.find((d) => d.findingId === f.id)?.disposition === "declined");
|
|
2468
|
+
if (!allDeclined)
|
|
2469
|
+
return end(
|
|
2470
|
+
next,
|
|
2471
|
+
{
|
|
2472
|
+
kind: "aborted",
|
|
2473
|
+
reason: `⚠️ Round ${round.index}'s findings step produced no new head — the branch still sits at \`${codingHead.slice(0, 7)}\`, the commit the review already read, and not every finding was declined on the record, so there is nothing new to re-review.`,
|
|
2474
|
+
round,
|
|
2475
|
+
reviewRounds: next.reviewRounds,
|
|
2476
|
+
},
|
|
2477
|
+
[roundNote(round, "aborted")],
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2358
2480
|
}
|
|
2359
2481
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2482
|
+
const reviewed: UnitPipelineState = { ...next, ...(head !== undefined ? { lastReviewHead: head } : {}) };
|
|
2483
|
+
const notes = [roundNote(round, "pr_opened")];
|
|
2484
|
+
if (hasHumanAnswer) return mandatoryReview(reviewed, notes);
|
|
2485
|
+
return nextReview(reviewed, notes);
|
|
2363
2486
|
}
|
|
2364
2487
|
|
|
2365
2488
|
/** Move the clock and charge the elapsed time to the budget's bucket: a phase
|
|
@@ -2421,6 +2544,20 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2421
2544
|
pr: { number: r.pr.prNumber, url: r.pr.url },
|
|
2422
2545
|
lastReviewHead: entryHead,
|
|
2423
2546
|
};
|
|
2547
|
+
const answered = r.pr.humanGate;
|
|
2548
|
+
if (answered !== undefined) {
|
|
2549
|
+
const round = Math.max(1, answered.round);
|
|
2550
|
+
const withAnswer: UnitPipelineState = {
|
|
2551
|
+
...adopted,
|
|
2552
|
+
reviewRounds: round,
|
|
2553
|
+
findingsByRound: { ...adopted.findingsByRound, [round]: answered.findings },
|
|
2554
|
+
humanAnswersByRound: {
|
|
2555
|
+
...adopted.humanAnswersByRound,
|
|
2556
|
+
[round]: [`${answered.author}: ${answered.answer}`],
|
|
2557
|
+
},
|
|
2558
|
+
};
|
|
2559
|
+
return enterRound(withAnswer, { index: round, kind: "findings" });
|
|
2560
|
+
}
|
|
2424
2561
|
const checks = r.pr.checks;
|
|
2425
2562
|
const green =
|
|
2426
2563
|
checks !== undefined && checks.total > 0 && checks.failed.length === 0 && checks.pending.length === 0;
|
|
@@ -2733,7 +2870,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
2733
2870
|
reviewRounds: s.reviewRounds,
|
|
2734
2871
|
});
|
|
2735
2872
|
const rebased: UnitPipelineState = { ...clocked, lastReviewHead: r.headSha };
|
|
2736
|
-
if (r.outcome === "changed") return
|
|
2873
|
+
if (r.outcome === "changed") return mandatoryReview(rebased);
|
|
2737
2874
|
return enterChecks(rebased, { index: s.reviewRounds, kind: "review" }, []);
|
|
2738
2875
|
}
|
|
2739
2876
|
case "merge-wait":
|
|
@@ -2785,7 +2922,7 @@ function dispositionFor(s: UnitPipelineState, finding: Finding, round: number):
|
|
|
2785
2922
|
function writeUpPointer(s: UnitPipelineState, kind: RoundKind, runId: string | undefined): string | undefined {
|
|
2786
2923
|
if (runId === undefined) return undefined;
|
|
2787
2924
|
const base = s.input.runPageBase;
|
|
2788
|
-
const at = base !== undefined ? `${base.replace(/\/+$/, "")}/${encodeURIComponent(runId)}` : `run ${runId}`;
|
|
2925
|
+
const at = base !== undefined ? `${base.replace(/\/+$/, "")}/${encodeURIComponent(runId)}` : `the run ${runId}`;
|
|
2789
2926
|
return `The ${presetOf(kind)} child's write-up is its own message above in this thread — the single copy of the detail (${at}).`;
|
|
2790
2927
|
}
|
|
2791
2928
|
|
|
@@ -2899,9 +3036,9 @@ export function renderUnitReport(
|
|
|
2899
3036
|
// The re-issue line keys on the instance's mark, never on who merges: a
|
|
2900
3037
|
// generated plan is re-issued with the request's own text, a seeded one by
|
|
2901
3038
|
// its plan path — and a seeded plan can be a person's merge too.
|
|
2902
|
-
const
|
|
2903
|
-
? `
|
|
2904
|
-
: `The unit's dependents in this plan stay blocked; the
|
|
3039
|
+
const continuation = s.input.generated
|
|
3040
|
+
? `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"}.`
|
|
3041
|
+
: `The unit's dependents in this plan stay blocked; the next run of this plan recognizes the unit's branch and pull request.`;
|
|
2905
3042
|
const declined = [...(s.dispositionsByRound[e.reviewRounds - 1] ?? [])].filter((d) => d.disposition === "declined");
|
|
2906
3043
|
const declinedLine = `Declined findings: ${declined.length > 0 ? declined.map((d) => `${d.findingId}${d.note ? ` — ${d.note}` : ""}`).join("; ") : "none"}`;
|
|
2907
3044
|
const verdictLine = `Verdict: LGTM${s.lastVerdictSummary ? ` — ${s.lastVerdictSummary}` : ""}`;
|
|
@@ -2926,7 +3063,7 @@ export function renderUnitReport(
|
|
|
2926
3063
|
const checkpointLine = (checkpoint: { branch: string; sha: string } | undefined) =>
|
|
2927
3064
|
checkpoint === undefined
|
|
2928
3065
|
? undefined
|
|
2929
|
-
: `The branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`;
|
|
3066
|
+
: `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.`;
|
|
2930
3067
|
switch (e.kind) {
|
|
2931
3068
|
case "merged":
|
|
2932
3069
|
if (e.by === "other")
|
|
@@ -2982,13 +3119,13 @@ export function renderUnitReport(
|
|
|
2982
3119
|
// the cause and the person's exact next step.
|
|
2983
3120
|
if (e.cause === "draft") {
|
|
2984
3121
|
const link = e.pr !== undefined ? ` — ${e.pr.url}` : "";
|
|
2985
|
-
if (!shows(verbosity, "verbose")) return `⏸️ Held: draft —
|
|
2986
|
-
const
|
|
2987
|
-
? `
|
|
2988
|
-
: `
|
|
3122
|
+
if (!shows(verbosity, "verbose")) return `⏸️ Held: draft — GitHub still marks the pull request as draft${link}`;
|
|
3123
|
+
const draftContinuation = s.input.generated
|
|
3124
|
+
? `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.`
|
|
3125
|
+
: `The unit stays held while GitHub marks the pull request as a draft; ${continuation.charAt(0).toLowerCase()}${continuation.slice(1)}`;
|
|
2989
3126
|
return join([
|
|
2990
3127
|
`⏸️ 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.`,
|
|
2991
|
-
|
|
3128
|
+
draftContinuation,
|
|
2992
3129
|
]);
|
|
2993
3130
|
}
|
|
2994
3131
|
// The blocked hold (issue 2086): the coding child concluded its round
|
|
@@ -3001,28 +3138,19 @@ export function renderUnitReport(
|
|
|
3001
3138
|
return join([
|
|
3002
3139
|
`⏸️ Held after ${rounds}: the coding child of round ${e.round.index} concluded its round blocked instead of opening a pull request — ${e.reason}.${prLine}`,
|
|
3003
3140
|
writeUpPointer(s, e.round.kind, e.runId),
|
|
3004
|
-
`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).
|
|
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). The child's answer remains the thread's open question; the next reply is treated as its answer. ${continuation}`,
|
|
3005
3142
|
]);
|
|
3006
3143
|
}
|
|
3007
|
-
//
|
|
3008
|
-
//
|
|
3009
|
-
//
|
|
3010
|
-
// resume path — once the receipt stands on the pull request. That path
|
|
3011
|
-
// fires only when the invocation carries NO new task text: re-issuing
|
|
3012
|
-
// with the task would ADOPT the pull request and run a coding round
|
|
3013
|
-
// first (item 10), the very round this ending exists to avoid — so the
|
|
3014
|
-
// held case renders its own re-issue line instead of the shared one.
|
|
3144
|
+
// A human-gated row is a question to a person. The enclosing idle keeps
|
|
3145
|
+
// this unit live; either answer surface wakes a fix round carrying both
|
|
3146
|
+
// the typed finding and the attributed answer.
|
|
3015
3147
|
const rows = e.findings.map((f) => `${f.id} (${f.severity}) — ${f.title}`).join("; ");
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
if (!shows(verbosity, "verbose")) return `⏸️ Held: ${rows}${e.pr !== undefined ? ` — ${e.pr.url}` : ""}`;
|
|
3019
|
-
const heldReissue = s.input.generated
|
|
3020
|
-
? `To continue, re-issue \`agent:ship\` in this thread with only the PR URL${e.pr !== undefined ? ` (${e.pr.url})` : ""} — no new task text.`
|
|
3021
|
-
: reissue;
|
|
3148
|
+
if (!shows(verbosity, "verbose"))
|
|
3149
|
+
return `⏸️ Waiting for you: ${rows}${e.pr !== undefined ? ` — ${e.pr.url}` : ""}`;
|
|
3022
3150
|
return join([
|
|
3023
|
-
`⏸️
|
|
3151
|
+
`⏸️ 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.`,
|
|
3024
3152
|
levelLine,
|
|
3025
|
-
`
|
|
3153
|
+
`The next reply in this unit thread or authorized pull request comment becomes the fix round's answer; review then runs again.`,
|
|
3026
3154
|
]);
|
|
3027
3155
|
}
|
|
3028
3156
|
case "merge_refused":
|
|
@@ -3038,8 +3166,8 @@ export function renderUnitReport(
|
|
|
3038
3166
|
return join([
|
|
3039
3167
|
`⚠️ The review approved ${e.pr.url} but the pipeline did not merge it: ${e.reason}. A person decides what becomes of the pull request.`,
|
|
3040
3168
|
s.input.generated
|
|
3041
|
-
?
|
|
3042
|
-
:
|
|
3169
|
+
? continuation
|
|
3170
|
+
: "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.",
|
|
3043
3171
|
]);
|
|
3044
3172
|
case "round_cap": {
|
|
3045
3173
|
// The quiet copy counts the last review's open findings, only the
|
|
@@ -3058,7 +3186,7 @@ export function renderUnitReport(
|
|
|
3058
3186
|
failedChecks.length > 0
|
|
3059
3187
|
? `🧢 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}`
|
|
3060
3188
|
: `🧢 Ship stopped at a cap: the ${e.maxRounds}-round cap — no approval after ${rounds}.${prLine}`;
|
|
3061
|
-
return join([headline, splitReport(s),
|
|
3189
|
+
return join([headline, splitReport(s), continuation]);
|
|
3062
3190
|
}
|
|
3063
3191
|
case "wall_clock_cap":
|
|
3064
3192
|
if (!shows(verbosity, "verbose")) return `🧢 Out of budget — no approval after ${rounds}.${prLine}`;
|
|
@@ -3066,13 +3194,13 @@ export function renderUnitReport(
|
|
|
3066
3194
|
`🧢 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}`,
|
|
3067
3195
|
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
3068
3196
|
splitReport(s),
|
|
3069
|
-
|
|
3197
|
+
continuation,
|
|
3070
3198
|
]);
|
|
3071
3199
|
case "review_pending":
|
|
3072
3200
|
return join([
|
|
3073
3201
|
`⏳ 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.`,
|
|
3074
3202
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
3075
|
-
aside(
|
|
3203
|
+
aside(continuation),
|
|
3076
3204
|
]);
|
|
3077
3205
|
case "stopped":
|
|
3078
3206
|
if (!shows(verbosity, "verbose"))
|
|
@@ -3088,7 +3216,7 @@ export function renderUnitReport(
|
|
|
3088
3216
|
e.postedReview
|
|
3089
3217
|
? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
|
|
3090
3218
|
: undefined,
|
|
3091
|
-
|
|
3219
|
+
continuation,
|
|
3092
3220
|
]);
|
|
3093
3221
|
case "aborted":
|
|
3094
3222
|
if (!shows(verbosity, "verbose")) return `⚠️ Aborted after ${rounds}: ${e.reason}${prLine}`;
|
|
@@ -3101,7 +3229,7 @@ export function renderUnitReport(
|
|
|
3101
3229
|
),
|
|
3102
3230
|
e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
|
|
3103
3231
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
3104
|
-
|
|
3232
|
+
continuation,
|
|
3105
3233
|
]);
|
|
3106
3234
|
case "continued":
|
|
3107
3235
|
// A segment boundary is the runner continuing — an acknowledgement, verbose
|
|
@@ -3111,18 +3239,18 @@ export function renderUnitReport(
|
|
|
3111
3239
|
return join([
|
|
3112
3240
|
writeUpPointer(s, e.round.kind, e.runId),
|
|
3113
3241
|
s.input.idleDays && s.input.idleDays > 0
|
|
3114
|
-
? `🔁 The unit's budget ran out with the unit unfinished — ${e.line}.
|
|
3242
|
+
? `🔁 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` : ""}.`
|
|
3115
3243
|
: `🔁 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` : ""}.`,
|
|
3116
3244
|
aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
|
|
3117
3245
|
]);
|
|
3118
3246
|
case "transient":
|
|
3119
3247
|
if (!shows(verbosity, "verbose"))
|
|
3120
|
-
return `⚠️ Aborted after ${rounds}: the model provider failed twice
|
|
3248
|
+
return `⚠️ Aborted after ${rounds}: this is a bug — the model provider failed twice and no automatic retry remains.${prLine}`;
|
|
3121
3249
|
return join([
|
|
3122
3250
|
`⚠️ 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.`,
|
|
3123
3251
|
writeUpPointer(s, e.round.kind, s.lastCodingRunId),
|
|
3124
|
-
`⚠️
|
|
3125
|
-
|
|
3252
|
+
`⚠️ This is a bug: ship ended after ${rounds} because its automatic provider retry was spent.`,
|
|
3253
|
+
continuation,
|
|
3126
3254
|
]);
|
|
3127
3255
|
case "no_verdict":
|
|
3128
3256
|
if (!shows(verbosity, "verbose"))
|
|
@@ -3131,7 +3259,7 @@ export function renderUnitReport(
|
|
|
3131
3259
|
`⚠️ 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.`,
|
|
3132
3260
|
writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index]),
|
|
3133
3261
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
3134
|
-
|
|
3262
|
+
continuation,
|
|
3135
3263
|
]);
|
|
3136
3264
|
case "interrupted":
|
|
3137
3265
|
return join([shipInterruptedNote(prUrl, e.cause, (s.input.idleDays ?? 0) > 0), checkpointLine(e.checkpoint)]);
|
|
@@ -3140,7 +3268,7 @@ export function renderUnitReport(
|
|
|
3140
3268
|
case "refused":
|
|
3141
3269
|
return join([
|
|
3142
3270
|
`🚫 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.`,
|
|
3143
|
-
aside(
|
|
3271
|
+
aside(continuation),
|
|
3144
3272
|
]);
|
|
3145
3273
|
case "idle":
|
|
3146
3274
|
// The old kind's sentence at this copy's level — the report is unchanged
|