@coreplane/switchboard 1.242.0 → 1.244.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 +20 -1
- package/dist/assets/deploy/cloudflare-memory/worker.ts +234 -29
- package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +483 -75
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +13 -2
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +3 -3
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +31 -35
- package/dist/assets/src/config/profile.ts +68 -3
- package/dist/assets/src/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/authz/types.ts +10 -0
- package/dist/assets/src/core/budgets.ts +349 -0
- package/dist/assets/src/core/coordinator/contract.ts +32 -0
- package/dist/assets/src/core/coordinator/driver.ts +75 -22
- package/dist/assets/src/core/costs.ts +9 -76
- package/dist/assets/src/core/delivery.ts +1 -1
- package/dist/assets/src/core/modelPricing.ts +212 -0
- package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
- package/dist/assets/src/core/reviewVerdict.ts +7 -0
- package/dist/assets/src/core/runEvents.ts +72 -5
- package/dist/assets/src/core/runFriction.ts +3 -1
- package/dist/assets/src/core/runRecord.ts +78 -0
- package/dist/assets/src/core/runUsage.ts +158 -47
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +2 -1
- package/dist/assets/src/core/ship/coordinator.ts +312 -97
- package/dist/assets/src/core/ship/handoff.ts +9 -0
- package/dist/assets/src/core/ship/renewal.ts +155 -0
- package/dist/assets/src/core/trace/attrs.ts +11 -0
- package/dist/assets/src/core/trace/types.ts +4 -3
- package/dist/assets/src/execution/bashTimeout.ts +8 -5
- package/dist/assets/src/execution/residentRefresh.ts +52 -7
- package/dist/assets/src/execution/residentSteps.ts +1 -1
- package/dist/assets/src/execution/residentText.ts +8 -3
- package/dist/assets/src/mcp/registry.ts +3 -0
- package/dist/assets/web/dist/.vite/manifest.json +432 -429
- package/dist/assets/web/dist/assets/{AppShell-IDMGG6yi.js → AppShell-Ck4hSZDi.js} +1 -1
- package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-C97EhJcQ.js → NotFoundPage-CiGrVOyv.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DTMBgnIW.js → ResidentDetailPage-C5USV_8P.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-BPMQxTcl.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-BUfdk0lH.js → RunsTabs-BWRHzhtT.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-ELoXHlFt.js → StatusDot-_Q_bmReh.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BoeFwYP2.js → Tooltip-Cafsgd4m.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-Dail07rr.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-BReExio8.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-DhNO6Yyx.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-BYotTY4C.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-mUxTtD1X.js} +1 -1
- package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DhuiwcSQ.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-GD2vFtRi.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-BhFDwkes.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-DWUgbE0r.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-CO9XFjGW.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-B9pVurpn.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-CFVVV6lI.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BU5UivXC.js → dist-BtPs2AkQ.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-BDhIDQIb.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-DG3J8WoD.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-FiOdiaOE.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-vxbtW503.js} +1 -1
- package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
- package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DINz8M3H.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-yann6r9i.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-CXTbrZn_.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DNYC-hq0.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CwzhHzi9.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BBXF1cwR.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-oEqBcsTy.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-YE5thkT1.js} +1 -1
- package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-Dp4jRQ1j.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-DCT1GYtQ.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-BPcGbmzJ.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-CQnNHwyd.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +1 -0
- package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-Dm8Ovj6W.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-Btm9oJK9.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-VZYjWF_U.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-Dcmjxa9x.js} +1 -1
- package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-DoWiOGZ8.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-Dvmo55ma.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-d8AtfV26.js} +1 -1
- package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-Cc5EStA-.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-B5fKKIH7.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-DUYI9JND.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-BhMwKWpw.js} +1 -1
- package/dist/assets/web/dist/assets/main-C4366PZF.css +1 -0
- package/dist/assets/web/dist/assets/main-DlTNAC6c.js +28 -0
- package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-Cnf2cPAi.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-FDDXoir9.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-q-8U_1jR.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-DIA0TUWs.js} +1 -1
- package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-CR88hy7S.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-Bkd8N7ja.js} +1 -1
- package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-BOs84mvy.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-BbpZBb3w.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml-D2jQpjFE.js} +1 -1
- package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-DKzcAQ22.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-SOQMWcC9.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-B6KK4W8d.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-kPSg-NPf.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-cKS-JYLH.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DNxzE5gs.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DGcHQ5OO.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CRZeZBa5.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DSNEL0HX.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-zisNbkDO.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-MG06KMMb.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-g7ml86LM.js → sseReplay-DaRtc7nI.js} +5 -5
- package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CB_wKwv9.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-DYdSt7re.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-DHO-F7c7.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CUL5L4Lr.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-_JNEKjVW.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags--9EpEV_8.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-BcNq2x2u.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-EsY5Dk_D.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-CyA_HiUH.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-SzaHlDt4.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-D3cn5acV.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-6MxhUxOa.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-qJnqhgo9.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-DyRpC7xv.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-BXMRPAsg.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-BNmPCtAN.js} +1 -1
- package/dist/cli.js +5843 -2246
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-CtmKhhOF.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +0 -2
- package/dist/assets/web/dist/assets/PendingTurnRow-BZA_vQt3.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CaDvXhzJ.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DXD2gLJk.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-BIGio8Y0.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
- package/dist/assets/web/dist/assets/main-B2fX10aW.css +0 -1
- package/dist/assets/web/dist/assets/main-D4EA1g6n.js +0 -28
|
@@ -30,12 +30,25 @@
|
|
|
30
30
|
// arrive in the input rather than from the agent registry.
|
|
31
31
|
|
|
32
32
|
import type { ShipRoundOutcome } from "../runEvents.js";
|
|
33
|
+
import type { Handoff } from "./handoff.js";
|
|
34
|
+
import { progressOf, renderRenewal, renewalDecision, type PushedHeadFact, type RenewalDecision } from "./renewal.js";
|
|
33
35
|
import type { RunStatus } from "../runRecord.js";
|
|
34
36
|
import { normalizeHead, sameCommit } from "../reviewedHead.js";
|
|
35
37
|
import { formatFinding, type Finding, type FindingDisposition, type ReviewVerdictKind } from "../reviewVerdict.js";
|
|
36
38
|
import { parsePlanUnit, planUnitIds } from "./contract.js";
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
import {
|
|
40
|
+
carve,
|
|
41
|
+
DEFAULT_GRANT,
|
|
42
|
+
loopPosition,
|
|
43
|
+
MINUTE_MS,
|
|
44
|
+
SHIP_WAIT,
|
|
45
|
+
type Carve,
|
|
46
|
+
type Grant,
|
|
47
|
+
type GrantSource,
|
|
48
|
+
type Loop,
|
|
49
|
+
} from "../budgets.js";
|
|
50
|
+
|
|
51
|
+
const MIN = MINUTE_MS;
|
|
39
52
|
|
|
40
53
|
/** What a pipeline runs under: the rounds cap from the `ship` config block, and
|
|
41
54
|
* the wall clock from the parent's EFFECTIVE profile — the ship preset's
|
|
@@ -45,34 +58,10 @@ export interface ShipCaps {
|
|
|
45
58
|
maxMinutes: number;
|
|
46
59
|
}
|
|
47
60
|
|
|
48
|
-
/** A round
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
export const SHIP_ROUND_RESERVE_MS = 3 * MIN;
|
|
53
|
-
|
|
54
|
-
/** What a round-0 coding child must leave on the pipeline's clock: two review
|
|
55
|
-
* rounds (a review and, after a fix, its re-review) and one merge poll — the
|
|
56
|
-
* loop the pipeline exists to run. The coding child's budget is clipped to
|
|
57
|
-
* the remaining wall clock MINUS this reserve (never under the two minutes a
|
|
58
|
-
* spawn accepts), so a child that uses its whole directive still hands the
|
|
59
|
-
* pipeline a clock that holds the review; without the clip a 40-minute
|
|
60
|
-
* pipeline hands 39 minutes to the child and caps out with the pull request
|
|
61
|
-
* shipped and unreviewed. */
|
|
62
|
-
export const SHIP_LOOP_RESERVE_MS = 2 * SHIP_ROUND_RESERVE_MS + 5 * MIN;
|
|
63
|
-
|
|
64
|
-
/** What a findings child (the fix after a review asked for changes) must leave
|
|
65
|
-
* on the clock: the re-review and one merge poll — one round's reserve and
|
|
66
|
-
* five minutes. Never the whole loop's: by the time a fix round runs, the
|
|
67
|
-
* first review has already happened, and holding two rounds back from a late
|
|
68
|
-
* fix would cap a pipeline that still has the time. */
|
|
69
|
-
export const SHIP_FIX_RESERVE_MS = SHIP_ROUND_RESERVE_MS + 5 * MIN;
|
|
70
|
-
|
|
71
|
-
/** The floor `validateShip` holds `ship.maxMinutes` to: the loop's reserve
|
|
72
|
-
* plus one round's — under it no coding child can both work and leave the
|
|
73
|
-
* review its time, so the config is refused at load rather than left to cap
|
|
74
|
-
* out on every unit. */
|
|
75
|
-
export const SHIP_MIN_MAX_MINUTES = (SHIP_LOOP_RESERVE_MS + SHIP_ROUND_RESERVE_MS) / MIN;
|
|
61
|
+
/** A round's minutes come from `carve` in `src/core/budgets.ts` (agent-ship
|
|
62
|
+
* item 8, decision 0046): the remainder minus the reserve derived over the
|
|
63
|
+
* rounds that must still follow, capped at the preset's ask, refused under the
|
|
64
|
+
* round's floor. Nothing here holds a reserve of its own. */
|
|
76
65
|
|
|
77
66
|
/** What a ship pipeline's thread and card say when the bot died under it (run-
|
|
78
67
|
* history item 36): the work it did stands on GitHub with nobody driving it,
|
|
@@ -414,7 +403,14 @@ export function presetOf(kind: RoundKind): ChildPreset {
|
|
|
414
403
|
* turn: the unit's contract, or the review turn from the pull request and the
|
|
415
404
|
* prior rounds' records, or the findings message from the review run's verdict. */
|
|
416
405
|
export type Brief =
|
|
417
|
-
| {
|
|
406
|
+
| {
|
|
407
|
+
kind: "contract";
|
|
408
|
+
unit: string;
|
|
409
|
+
rebase: { branch: string; onto: string };
|
|
410
|
+
/** A renewal's segment (decision 0046): the child continues the previous
|
|
411
|
+
* segment's work from `from`, briefed with that run's write-up and handoff. */
|
|
412
|
+
continue?: { segment: number; from?: string; previousRunId?: string };
|
|
413
|
+
}
|
|
418
414
|
| {
|
|
419
415
|
kind: "review";
|
|
420
416
|
unit: string;
|
|
@@ -478,6 +474,13 @@ export type ChildFacts =
|
|
|
478
474
|
/** The dispositions a coding run recorded, as it submitted them: the machine matches them to the round's findings. */
|
|
479
475
|
dispositions?: FindingDisposition[];
|
|
480
476
|
handoff?: boolean;
|
|
477
|
+
/** The renewal's facts (decision 0046): the heads the run pushed, when
|
|
478
|
+
* its lease began, what it cost (null when a model had no price) and
|
|
479
|
+
* the handoff's lists — progress is read off these, never asked. */
|
|
480
|
+
pushed?: PushedHeadFact[];
|
|
481
|
+
leaseStartedAt?: number;
|
|
482
|
+
costUsd?: number | null;
|
|
483
|
+
handoffLists?: Handoff;
|
|
481
484
|
};
|
|
482
485
|
|
|
483
486
|
/** What heads the unit's branch on GitHub: nothing, an open pull request, or —
|
|
@@ -525,7 +528,14 @@ export type UnitEnding =
|
|
|
525
528
|
| { kind: "merge_ready"; pr: PrRef; reviewRounds: number }
|
|
526
529
|
| { kind: "merge_refused"; pr: PrRef; reason: string; reviewRounds: number }
|
|
527
530
|
| { kind: "round_cap"; maxRounds: number; reviewRounds: number }
|
|
528
|
-
| {
|
|
531
|
+
| {
|
|
532
|
+
kind: "wall_clock_cap";
|
|
533
|
+
remainingMs: number;
|
|
534
|
+
reviewRounds: number;
|
|
535
|
+
spent: ShipBudgetSpent;
|
|
536
|
+
/** The round the remainder could not hold: what it would have got and the floor it fell under. */
|
|
537
|
+
refused?: { round: RoundKind; minutes: number; floor: number };
|
|
538
|
+
}
|
|
529
539
|
/** The wall clock capped AFTER the coding child opened or updated the pull
|
|
530
540
|
* request: the work stands and only the review is missing, so the ending
|
|
531
541
|
* names the pull request and "review pending" instead of calling the unit a
|
|
@@ -540,7 +550,38 @@ export type UnitEnding =
|
|
|
540
550
|
/** A changes-requested review posted this round before the stop. */
|
|
541
551
|
postedReview?: boolean;
|
|
542
552
|
}
|
|
543
|
-
| {
|
|
553
|
+
| {
|
|
554
|
+
kind: "aborted";
|
|
555
|
+
reason: string;
|
|
556
|
+
round?: RoundRef;
|
|
557
|
+
reviewRounds: number;
|
|
558
|
+
finalReply?: string;
|
|
559
|
+
/** A round-0 end without a pull request that was judged for renewal and
|
|
560
|
+
* refused: the decision and the card's sentence (decision 0046). */
|
|
561
|
+
renewal?: { decision: Extract<RenewalDecision, { renew: false }>; line: string };
|
|
562
|
+
}
|
|
563
|
+
/** The coding round ended at its lease with the unit unfinished, the row
|
|
564
|
+
* showed progress and the grant renewed: this segment is over and the next
|
|
565
|
+
* opens in the same thread from `from` under a fresh lease, the coding
|
|
566
|
+
* child's write-up as its request (decision 0046, Renewal). */
|
|
567
|
+
| {
|
|
568
|
+
kind: "continued";
|
|
569
|
+
round: RoundRef;
|
|
570
|
+
/** The coding child whose write-up and handoff the continuation is briefed with. */
|
|
571
|
+
runId: string;
|
|
572
|
+
/** The segment the renewal opens (the first segment is 1). */
|
|
573
|
+
segment: number;
|
|
574
|
+
from?: string;
|
|
575
|
+
renewalsLeft: number;
|
|
576
|
+
/** The session's spend so far, this segment's children included; null when a run's model had no price. */
|
|
577
|
+
spendUsd: number | null;
|
|
578
|
+
handoff?: Handoff;
|
|
579
|
+
/** The coding child's write-up: the checkpoint the continuation is briefed with. */
|
|
580
|
+
finalReply?: string;
|
|
581
|
+
line: string;
|
|
582
|
+
reviewRounds: number;
|
|
583
|
+
spent: ShipBudgetSpent;
|
|
584
|
+
}
|
|
544
585
|
| { kind: "no_verdict"; round: RoundRef; reviewRounds: number; finalReply?: string }
|
|
545
586
|
| { kind: "interrupted"; round: RoundRef; runId: string; reviewRounds: number }
|
|
546
587
|
| { kind: "refused"; refusal: string; message?: string; round: RoundRef; reviewRounds: number };
|
|
@@ -578,15 +619,22 @@ export function findingsAtOrAbove(findings: readonly Finding[], level: AddressSe
|
|
|
578
619
|
return findings.filter((f) => severityRank(f.severity) <= severityRank(level));
|
|
579
620
|
}
|
|
580
621
|
|
|
622
|
+
export interface UnitSession {
|
|
623
|
+
segment: number;
|
|
624
|
+
renewalsSpent: number;
|
|
625
|
+
spendUsd: number | null;
|
|
626
|
+
continueFrom?: string;
|
|
627
|
+
/** The previous segment's coding run: its write-up and handoff brief the continuation. */
|
|
628
|
+
previousRunId?: string;
|
|
629
|
+
previousHandoff?: Handoff;
|
|
630
|
+
}
|
|
631
|
+
|
|
581
632
|
export interface UnitPipelineInput {
|
|
582
633
|
unit: { id: string; branch: string };
|
|
583
634
|
repo: string;
|
|
584
635
|
/** The pull request's base — the branch the unit is created from and rebased onto. */
|
|
585
636
|
base: string;
|
|
586
637
|
caps: ShipCaps;
|
|
587
|
-
/** Each child preset's own wall-clock budget (its `maxMinutes`), the number a
|
|
588
|
-
* round's budget is clipped from — supplied by the bot, which holds the registry. */
|
|
589
|
-
childMinutes: Readonly<Record<ChildPreset, number>>;
|
|
590
638
|
/** Who merges: the instance's `merge` field as the plan route answers it —
|
|
591
639
|
* `runner` (a seeded plan, under its grant) or `person` (a task, or a
|
|
592
640
|
* record without the field). */
|
|
@@ -597,6 +645,18 @@ export interface UnitPipelineInput {
|
|
|
597
645
|
addressSeverity?: AddressSeverity;
|
|
598
646
|
/** Which layer set the level in force; named in the round header and the ending. */
|
|
599
647
|
addressSeveritySource?: AddressSeveritySource;
|
|
648
|
+
/** The grant the request carried (decision 0046, the renewable lease):
|
|
649
|
+
* renewals and a cost cap, and which layer granted it — named in the report;
|
|
650
|
+
* absent reads as zero renewals and no cap, the org's. Nothing here spends
|
|
651
|
+
* it yet: the renewal decision is the segment's end, not this unit's. */
|
|
652
|
+
grant?: Grant;
|
|
653
|
+
grantSource?: GrantSource;
|
|
654
|
+
/** The segment this pipeline runs (decision 0046, Renewal): absent for the
|
|
655
|
+
* first; a renewal's carries its number, the renewals spent before it, the
|
|
656
|
+
* session's spend so far, the sha it continues from and the previous
|
|
657
|
+
* segment's handoff. Every step name of a later segment is prefixed with
|
|
658
|
+
* it, so the Workflow's durable steps never collide across segments. */
|
|
659
|
+
session?: UnitSession;
|
|
600
660
|
/** The instance's mark (agent-ship item 16): a generated one-unit plan — a
|
|
601
661
|
* `plan` with an id and no `path` — whose unit runs in the requesting
|
|
602
662
|
* thread and is re-issued with the request's own text, never a plan path. */
|
|
@@ -614,7 +674,7 @@ type Phase =
|
|
|
614
674
|
/** Before anything is created: what already heads the branch — a merged pull request ends the unit here. */
|
|
615
675
|
| { at: "pre-check" }
|
|
616
676
|
| { at: "branch" }
|
|
617
|
-
| { at: "spawn"; round: RoundRef; busy: number }
|
|
677
|
+
| { at: "spawn"; round: RoundRef; busy: number; minutes: number; holds: number }
|
|
618
678
|
| { at: "busy-wait"; round: RoundRef; runId?: string; n: number }
|
|
619
679
|
/** `until`: when the child's budget plus the margin runs out, counted from the spawn's answer — the wait's last slice ends there. */
|
|
620
680
|
| { at: "wait"; round: RoundRef; runId: string; n: number; until: number }
|
|
@@ -625,13 +685,17 @@ type Phase =
|
|
|
625
685
|
runId: string;
|
|
626
686
|
childHead?: string;
|
|
627
687
|
finalReply?: string;
|
|
688
|
+
/** The renewal's facts off the child's record (decision 0046): its pushed heads, its lease's start, its handoff. */
|
|
689
|
+
childPushed?: PushedHeadFact[];
|
|
690
|
+
childLeaseStartedAt?: number;
|
|
691
|
+
childHandoff?: Handoff;
|
|
628
692
|
/** The coding child died (`failed` or `interrupted`) after it may have
|
|
629
693
|
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
630
694
|
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
631
695
|
dead?: "failed" | "interrupted";
|
|
632
696
|
}
|
|
633
|
-
| { at: "merge"; pr: PrRef; headSha: string; n: number; since: number }
|
|
634
|
-
| { at: "merge-wait"; pr: PrRef; headSha: string; n: number; since: number }
|
|
697
|
+
| { at: "merge"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number }
|
|
698
|
+
| { at: "merge-wait"; pr: PrRef; headSha: string; n: number; since: number; waitMs: number }
|
|
635
699
|
| { at: "ended" };
|
|
636
700
|
|
|
637
701
|
export interface UnitPipelineState {
|
|
@@ -658,11 +722,14 @@ export interface UnitPipelineState {
|
|
|
658
722
|
readonly lastCodingRunId?: string;
|
|
659
723
|
/** How the budget went so far, accrued as each answer moves the clock. */
|
|
660
724
|
readonly spentMs: ShipBudgetSpent;
|
|
725
|
+
/** The session's dollars so far: the input's from earlier segments plus each
|
|
726
|
+
* child's cost as its record is read; null once any run's cost is unknown. */
|
|
727
|
+
readonly spendUsd: number | null;
|
|
661
728
|
readonly ending?: UnitEnding;
|
|
662
729
|
}
|
|
663
730
|
|
|
664
731
|
/** Past a child's budget, the parent asks the bot instead of waiting on. */
|
|
665
|
-
export const WAIT_MARGIN_MS =
|
|
732
|
+
export const WAIT_MARGIN_MS = SHIP_WAIT.marginMinutes * MIN;
|
|
666
733
|
/** One slice of a wait on a child. The child's budget plus the margin is
|
|
667
734
|
* walked in chunks with a `read-record` between them, so an event the engine
|
|
668
735
|
* never delivered — refused, lost, sent to an instance that had ended — costs
|
|
@@ -670,19 +737,18 @@ export const WAIT_MARGIN_MS = 5 * MIN;
|
|
|
670
737
|
* is the machine's one cadence for asking the bot what it cannot be told (the
|
|
671
738
|
* margin and the merge poll are the same number), and it keeps a round to a
|
|
672
739
|
* few steps: a coding child's 45 minutes are ten waits and ten reads. */
|
|
673
|
-
export const WAIT_CHUNK_MS =
|
|
674
|
-
/**
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
export const MERGE_WAIT_MAX_MS = 60 * MIN;
|
|
740
|
+
export const WAIT_CHUNK_MS = SHIP_WAIT.chunkMinutes * MIN;
|
|
741
|
+
/** The merge wait is a round of its own: its minutes are carved from the
|
|
742
|
+
* pipeline's remainder when the door is first asked (the merge wait's ask and
|
|
743
|
+
* floor are rows of `src/core/budgets.ts`), and the wait below is sliced
|
|
744
|
+
* from that carve. */
|
|
679
745
|
/** One merge wait's fallback timeout: the old poll's cadence. The event wakes
|
|
680
746
|
* the machine at once when the intake delivers it; without one (the webhook
|
|
681
747
|
* not configured, a delivery lost) the door is still re-asked every chunk, so
|
|
682
748
|
* a merge is never slower than the poll it replaced. */
|
|
683
|
-
export const MERGE_WAIT_CHUNK_MS =
|
|
749
|
+
export const MERGE_WAIT_CHUNK_MS = SHIP_WAIT.mergeChunkMinutes * MIN;
|
|
684
750
|
/** A `busy` without the live run's id: nothing to wait on, so a short sleep before the spawn is asked again. */
|
|
685
|
-
export const BUSY_RETRY_MS =
|
|
751
|
+
export const BUSY_RETRY_MS = SHIP_WAIT.busyRetryMinutes * MIN;
|
|
686
752
|
|
|
687
753
|
// ---- the unit pipeline: opening and the next action -----------------------------------------------
|
|
688
754
|
|
|
@@ -694,6 +760,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
694
760
|
phase: { at: "pre-check" },
|
|
695
761
|
reviewRounds: 0,
|
|
696
762
|
spentMs: { coding: 0, review: 0, waiting: 0 },
|
|
763
|
+
spendUsd: input.session?.spendUsd ?? 0,
|
|
697
764
|
findingsByRound: {},
|
|
698
765
|
dispositionsByRound: {},
|
|
699
766
|
reviewRunByRound: {},
|
|
@@ -721,36 +788,45 @@ function waitSliceMs(clock: number, until: number): number {
|
|
|
721
788
|
return remaining > 0 ? Math.min(WAIT_CHUNK_MS, remaining) : WAIT_CHUNK_MS;
|
|
722
789
|
}
|
|
723
790
|
|
|
724
|
-
/** The
|
|
725
|
-
|
|
726
|
-
* spawn accepts. */
|
|
727
|
-
function budgetMinutesFor(s: UnitPipelineState, round: RoundRef): number {
|
|
728
|
-
const headroom = remainingMs(s) - reserveFor(round.kind);
|
|
729
|
-
return Math.max(2, Math.min(s.input.childMinutes[presetOf(round.kind)], Math.floor(headroom / MIN)));
|
|
730
|
-
}
|
|
791
|
+
/** The pipeline's loop as the config allows it. */
|
|
792
|
+
const loopOf = (s: UnitPipelineState): Loop => ({ maxRounds: s.input.caps.maxRounds });
|
|
731
793
|
|
|
732
|
-
/**
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
case "coding":
|
|
740
|
-
return SHIP_LOOP_RESERVE_MS;
|
|
741
|
-
case "findings":
|
|
742
|
-
return SHIP_FIX_RESERVE_MS;
|
|
743
|
-
case "review":
|
|
744
|
-
return 0;
|
|
745
|
-
}
|
|
794
|
+
/** A round's carve (agent-ship item 8): the remainder minus the reserve for
|
|
795
|
+
* the rounds after it, capped at its preset's ask, refused under its floor.
|
|
796
|
+
* A review round `n` and the findings step that follows it share `n`; the
|
|
797
|
+
* module's positions are the loop's own. */
|
|
798
|
+
function roundCarve(s: UnitPipelineState, round: RoundRef): Carve {
|
|
799
|
+
const kind = round.kind === "findings" ? "fix" : round.kind;
|
|
800
|
+
return carve(remainingMs(s), { kind, index: loopPosition(loopOf(s), kind, round.index) }, loopOf(s));
|
|
746
801
|
}
|
|
747
802
|
|
|
748
|
-
|
|
803
|
+
/** The prefix every step of this pipeline is named under: the unit id, and for
|
|
804
|
+
* a renewal's segment the segment too (`U10/s2/…`), so the Workflow's durable
|
|
805
|
+
* step cache never hands segment two the answers of segment one. */
|
|
806
|
+
export const stepPrefixOf = (unit: string, session: UnitSession | undefined): string =>
|
|
807
|
+
session !== undefined && session.segment > 1 ? `${unit}/s${session.segment}` : unit;
|
|
808
|
+
const stepPrefix = (s: UnitPipelineState) => stepPrefixOf(s.input.unit.id, s.input.session);
|
|
809
|
+
const roundStep = (s: UnitPipelineState, round: RoundRef) => `${stepPrefix(s)}/${round.index}/${round.kind}`;
|
|
749
810
|
|
|
750
811
|
function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
|
|
751
812
|
const unit = s.input.unit.id;
|
|
752
|
-
if (round.kind === "coding")
|
|
753
|
-
|
|
813
|
+
if (round.kind === "coding") {
|
|
814
|
+
const session = s.input.session;
|
|
815
|
+
return {
|
|
816
|
+
kind: "contract",
|
|
817
|
+
unit,
|
|
818
|
+
rebase: { branch: s.input.unit.branch, onto: s.input.base },
|
|
819
|
+
...(session !== undefined && session.segment > 1
|
|
820
|
+
? {
|
|
821
|
+
continue: {
|
|
822
|
+
segment: session.segment,
|
|
823
|
+
...(session.continueFrom !== undefined ? { from: session.continueFrom } : {}),
|
|
824
|
+
...(session.previousRunId !== undefined ? { previousRunId: session.previousRunId } : {}),
|
|
825
|
+
},
|
|
826
|
+
}
|
|
827
|
+
: {}),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
754
830
|
const pr = s.pr!.number;
|
|
755
831
|
if (round.kind === "findings") return { kind: "findings", unit, pr, reviewRunId: s.reviewRunByRound[round.index]! };
|
|
756
832
|
const priorReview = s.reviewRunByRound[round.index - 1];
|
|
@@ -786,7 +862,7 @@ export function matchDispositions(
|
|
|
786
862
|
/** The step the machine is at. Pure over the state: asked before every step
|
|
787
863
|
* and again after a replay, it names the same step for the same state. */
|
|
788
864
|
export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
789
|
-
const unit = s
|
|
865
|
+
const unit = stepPrefix(s);
|
|
790
866
|
const p = s.phase;
|
|
791
867
|
switch (p.at) {
|
|
792
868
|
case "pre-check":
|
|
@@ -800,7 +876,7 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
800
876
|
step: roundStep(s, p.round),
|
|
801
877
|
preset,
|
|
802
878
|
round: p.round,
|
|
803
|
-
budgetMinutes:
|
|
879
|
+
budgetMinutes: p.minutes,
|
|
804
880
|
brief: briefFor(s, p.round),
|
|
805
881
|
};
|
|
806
882
|
}
|
|
@@ -831,7 +907,7 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
831
907
|
type: "wait-checks",
|
|
832
908
|
step: `${unit}/merge/wait/${p.n}`,
|
|
833
909
|
headSha: p.headSha,
|
|
834
|
-
timeoutMs: Math.max(MIN, Math.min(MERGE_WAIT_CHUNK_MS,
|
|
910
|
+
timeoutMs: Math.max(MIN, Math.min(MERGE_WAIT_CHUNK_MS, p.waitMs - (s.clock - p.since))),
|
|
835
911
|
};
|
|
836
912
|
case "ended":
|
|
837
913
|
return { type: "end", step: `${unit}/end`, ending: s.ending! };
|
|
@@ -847,6 +923,14 @@ interface Transition {
|
|
|
847
923
|
|
|
848
924
|
const ENDED: Phase = { at: "ended" };
|
|
849
925
|
|
|
926
|
+
/** The session's dollars after one more child: unknown (null) once any run's
|
|
927
|
+
* cost is — a total that left a run out would understate the spend a cap
|
|
928
|
+
* judges — and a record without the field counts as unknown too. */
|
|
929
|
+
function addSpend(sum: number | null, cost: number | null | undefined): number | null {
|
|
930
|
+
if (sum === null || cost === null || cost === undefined) return null;
|
|
931
|
+
return sum + cost;
|
|
932
|
+
}
|
|
933
|
+
|
|
850
934
|
function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[] = []): Transition {
|
|
851
935
|
return { state: { ...s, phase: ENDED, ending }, notes: [...notes, { type: "ended", ending }] };
|
|
852
936
|
}
|
|
@@ -861,7 +945,7 @@ const roundNote = (round: RoundRef, outcome: ShipRoundOutcome): CoordinatorNote
|
|
|
861
945
|
/** The cap's ending: `review_pending` when the clock ran out entering a
|
|
862
946
|
* review round with the child's pull request standing — the work shipped and
|
|
863
947
|
* only the review is missing — else the wall-clock cap. Both carry the split. */
|
|
864
|
-
function capEnding(s: UnitPipelineState, round?: RoundRef): UnitEnding {
|
|
948
|
+
function capEnding(s: UnitPipelineState, round?: RoundRef, refused?: Carve): UnitEnding {
|
|
865
949
|
if (round?.kind === "review" && s.pr !== undefined)
|
|
866
950
|
return {
|
|
867
951
|
kind: "review_pending",
|
|
@@ -870,16 +954,28 @@ function capEnding(s: UnitPipelineState, round?: RoundRef): UnitEnding {
|
|
|
870
954
|
reviewRounds: s.reviewRounds,
|
|
871
955
|
spent: s.spentMs,
|
|
872
956
|
};
|
|
873
|
-
return {
|
|
957
|
+
return {
|
|
958
|
+
kind: "wall_clock_cap",
|
|
959
|
+
remainingMs: remainingMs(s),
|
|
960
|
+
reviewRounds: s.reviewRounds,
|
|
961
|
+
spent: s.spentMs,
|
|
962
|
+
...(round !== undefined && refused?.kind === "refused"
|
|
963
|
+
? { refused: { round: round.kind, minutes: refused.minutes, floor: refused.floor } }
|
|
964
|
+
: {}),
|
|
965
|
+
};
|
|
874
966
|
}
|
|
875
967
|
|
|
876
|
-
/** Start a round if
|
|
877
|
-
*
|
|
968
|
+
/** Start a round if its carve holds (agent-ship item 8): a round the
|
|
969
|
+
* remainder cannot carve above its floor is not dispatched, and the unit ends
|
|
970
|
+
* at the cap naming the round, what it would have got and the floor. */
|
|
878
971
|
function enterRound(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNote[] = []): Transition {
|
|
879
|
-
const
|
|
880
|
-
if (
|
|
972
|
+
const carved = roundCarve(s, round);
|
|
973
|
+
if (carved.kind === "refused") return end(s, capEnding(s, round, carved), notes);
|
|
881
974
|
const reviewRounds = round.kind === "review" ? round.index : s.reviewRounds;
|
|
882
|
-
return {
|
|
975
|
+
return {
|
|
976
|
+
state: { ...s, reviewRounds, phase: { at: "spawn", round, busy: 0, minutes: carved.minutes, holds: carved.holds } },
|
|
977
|
+
notes,
|
|
978
|
+
};
|
|
883
979
|
}
|
|
884
980
|
|
|
885
981
|
/** The next review round, or the round cap. */
|
|
@@ -905,6 +1001,7 @@ function settleCoding(
|
|
|
905
1001
|
// round's findings enter the state (agent-ship item 6).
|
|
906
1002
|
let next: UnitPipelineState = {
|
|
907
1003
|
...s,
|
|
1004
|
+
spendUsd: addSpend(s.spendUsd, facts.costUsd),
|
|
908
1005
|
...(facts.pr !== undefined ? { pr: { number: facts.pr.number, url: facts.pr.url } } : {}),
|
|
909
1006
|
...(round.kind === "findings" && facts.dispositions !== undefined
|
|
910
1007
|
? {
|
|
@@ -942,6 +1039,9 @@ function settleCoding(
|
|
|
942
1039
|
runId,
|
|
943
1040
|
...(facts.headSha !== undefined ? { childHead: facts.headSha } : {}),
|
|
944
1041
|
...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
|
|
1042
|
+
...(facts.pushed !== undefined ? { childPushed: facts.pushed } : {}),
|
|
1043
|
+
...(facts.leaseStartedAt !== undefined ? { childLeaseStartedAt: facts.leaseStartedAt } : {}),
|
|
1044
|
+
...(facts.handoffLists !== undefined ? { childHandoff: facts.handoffLists } : {}),
|
|
945
1045
|
},
|
|
946
1046
|
};
|
|
947
1047
|
return { state: next, notes: [] };
|
|
@@ -981,6 +1081,7 @@ function settleReview(
|
|
|
981
1081
|
const verdict = facts.verdict;
|
|
982
1082
|
const next: UnitPipelineState = {
|
|
983
1083
|
...s,
|
|
1084
|
+
spendUsd: addSpend(s.spendUsd, facts.costUsd),
|
|
984
1085
|
findingsByRound: { ...s.findingsByRound, [round.index]: verdict.findings },
|
|
985
1086
|
...(facts.reviewHead !== undefined ? { lastReviewHead: facts.reviewHead } : {}),
|
|
986
1087
|
...(verdict.summary !== undefined ? { lastVerdictSummary: verdict.summary } : {}),
|
|
@@ -1042,7 +1143,26 @@ function settleReview(
|
|
|
1042
1143
|
{ kind: "merge_refused", pr, reason: "no approved head is known to merge at", reviewRounds: next.reviewRounds },
|
|
1043
1144
|
notes,
|
|
1044
1145
|
);
|
|
1045
|
-
|
|
1146
|
+
const mergeWait = carve(
|
|
1147
|
+
remainingMs(next),
|
|
1148
|
+
{ kind: "merge", index: loopPosition(loopOf(next), "merge") },
|
|
1149
|
+
loopOf(next),
|
|
1150
|
+
);
|
|
1151
|
+
if (mergeWait.kind === "refused")
|
|
1152
|
+
return end(
|
|
1153
|
+
next,
|
|
1154
|
+
{
|
|
1155
|
+
kind: "merge_refused",
|
|
1156
|
+
pr,
|
|
1157
|
+
reason: `the remaining ${mergeWait.minutes} minutes of the pipeline are under the merge wait's floor of ${mergeWait.floor}`,
|
|
1158
|
+
reviewRounds: next.reviewRounds,
|
|
1159
|
+
},
|
|
1160
|
+
notes,
|
|
1161
|
+
);
|
|
1162
|
+
return {
|
|
1163
|
+
state: { ...next, phase: { at: "merge", pr, headSha, n: 1, since: next.clock, waitMs: mergeWait.minutes * MIN } },
|
|
1164
|
+
notes,
|
|
1165
|
+
};
|
|
1046
1166
|
}
|
|
1047
1167
|
// request_changes: the verdict settled (and posted) — now a stop
|
|
1048
1168
|
// short-circuits the findings step, naming the review standing on the pull request.
|
|
@@ -1123,15 +1243,72 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
1123
1243
|
[roundNote(round, "aborted")],
|
|
1124
1244
|
);
|
|
1125
1245
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1246
|
+
// Round 0 ended without a pull request: the segment is over with the unit
|
|
1247
|
+
// unfinished, and the grant decides whether the next opens (decision
|
|
1248
|
+
// 0046, Renewal). Progress is read off the child's record — a head pushed
|
|
1249
|
+
// to the unit's branch since the segment started, or a handoff that moved —
|
|
1250
|
+
// never off its words; the decision then asks the grant's count, the cap
|
|
1251
|
+
// and the fit, in that order, and a refusal names the clause. A plain
|
|
1252
|
+
// abort keeps its old shape when nothing was pushed under a grant of zero:
|
|
1253
|
+
// a clarifying question is not a stop to explain.
|
|
1254
|
+
if (round.index === 0) {
|
|
1255
|
+
const grant = s.input.grant ?? DEFAULT_GRANT;
|
|
1256
|
+
const session = s.input.session;
|
|
1257
|
+
const progress = progressOf({
|
|
1258
|
+
branch: s.input.unit.branch,
|
|
1259
|
+
pushed: phase.childPushed ?? [],
|
|
1260
|
+
...(session?.continueFrom !== undefined ? { startHead: session.continueFrom } : {}),
|
|
1261
|
+
...(phase.childLeaseStartedAt !== undefined ? { leaseStartedAt: phase.childLeaseStartedAt } : {}),
|
|
1262
|
+
...(session?.previousHandoff !== undefined && phase.childHandoff !== undefined
|
|
1263
|
+
? { handoff: { previous: session.previousHandoff, current: phase.childHandoff } }
|
|
1264
|
+
: {}),
|
|
1265
|
+
});
|
|
1266
|
+
const decision = renewalDecision({
|
|
1267
|
+
grant,
|
|
1268
|
+
renewalsSpent: session?.renewalsSpent ?? 0,
|
|
1269
|
+
spendUsd: s.spendUsd,
|
|
1270
|
+
progress,
|
|
1271
|
+
pipeline: s.input.caps,
|
|
1272
|
+
});
|
|
1273
|
+
const line = renderRenewal(decision, grant);
|
|
1274
|
+
if (decision.renew)
|
|
1275
|
+
return end(
|
|
1276
|
+
s,
|
|
1277
|
+
{
|
|
1278
|
+
kind: "continued",
|
|
1279
|
+
round,
|
|
1280
|
+
runId: phase.runId,
|
|
1281
|
+
segment: decision.segment,
|
|
1282
|
+
...(decision.from !== undefined ? { from: decision.from } : {}),
|
|
1283
|
+
renewalsLeft: decision.renewalsLeft,
|
|
1284
|
+
spendUsd: s.spendUsd,
|
|
1285
|
+
...(phase.childHandoff !== undefined ? { handoff: phase.childHandoff } : {}),
|
|
1286
|
+
...(phase.finalReply !== undefined ? { finalReply: phase.finalReply } : {}),
|
|
1287
|
+
line,
|
|
1288
|
+
reviewRounds: s.reviewRounds,
|
|
1289
|
+
spent: s.spentMs,
|
|
1290
|
+
},
|
|
1291
|
+
[roundNote(round, "continued")],
|
|
1292
|
+
);
|
|
1293
|
+
const judged = grant.renewals > 0 || progress.progressed;
|
|
1294
|
+
return end(
|
|
1295
|
+
s,
|
|
1296
|
+
{
|
|
1297
|
+
kind: "aborted",
|
|
1298
|
+
reason: `⚠️ Ship ended at round 0: the coding round ended without opening a pull request (a clarifying question, a budget write-up, an unproven push or a description-less push ends the pipeline here). No review round ran.`,
|
|
1299
|
+
round,
|
|
1300
|
+
reviewRounds: s.reviewRounds,
|
|
1301
|
+
...(phase.finalReply !== undefined ? { finalReply: phase.finalReply } : {}),
|
|
1302
|
+
...(judged ? { renewal: { decision, line } } : {}),
|
|
1303
|
+
},
|
|
1304
|
+
[roundNote(round, "aborted")],
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1130
1307
|
return end(
|
|
1131
1308
|
s,
|
|
1132
1309
|
{
|
|
1133
1310
|
kind: "aborted",
|
|
1134
|
-
reason,
|
|
1311
|
+
reason: `⚠️ Round ${round.index}'s findings step left no open pull request heading \`${s.input.unit.branch}\` — the pull request was closed out from under the pipeline and none was reopened, so there is nothing to re-review.`,
|
|
1135
1312
|
round,
|
|
1136
1313
|
reviewRounds: s.reviewRounds,
|
|
1137
1314
|
...(phase.finalReply !== undefined ? { finalReply: phase.finalReply } : {}),
|
|
@@ -1252,7 +1429,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1252
1429
|
case "spawned":
|
|
1253
1430
|
case "alreadySpawned": {
|
|
1254
1431
|
// The child's budget runs from the spawn's answer; the wait walks it, plus the margin, in chunks.
|
|
1255
|
-
const until = clocked.clock +
|
|
1432
|
+
const until = clocked.clock + p.minutes * MIN + WAIT_MARGIN_MS;
|
|
1256
1433
|
const runs =
|
|
1257
1434
|
p.round.kind === "review"
|
|
1258
1435
|
? { reviewRunByRound: { ...s.reviewRunByRound, [p.round.index]: r.runId } }
|
|
@@ -1270,7 +1447,8 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1270
1447
|
case "busy": {
|
|
1271
1448
|
// Another run holds the unit's thread: wait for its end, then ask
|
|
1272
1449
|
// again — unless the pipeline's wall clock ran out meanwhile.
|
|
1273
|
-
|
|
1450
|
+
const recarved = roundCarve(clocked, p.round);
|
|
1451
|
+
if (recarved.kind === "refused") return end(clocked, capEnding(clocked, p.round, recarved));
|
|
1274
1452
|
return {
|
|
1275
1453
|
state: {
|
|
1276
1454
|
...clocked,
|
|
@@ -1302,8 +1480,19 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1302
1480
|
}
|
|
1303
1481
|
break;
|
|
1304
1482
|
}
|
|
1305
|
-
case "busy-wait":
|
|
1306
|
-
|
|
1483
|
+
case "busy-wait": {
|
|
1484
|
+
// The clock moved while the spawn was busy: the round is carved again
|
|
1485
|
+
// from what remains, and a carve now under the floor ends at the cap.
|
|
1486
|
+
const carved = roundCarve(s, p.round);
|
|
1487
|
+
if (carved.kind === "refused") return end(s, capEnding(s, p.round, carved));
|
|
1488
|
+
return {
|
|
1489
|
+
state: {
|
|
1490
|
+
...s,
|
|
1491
|
+
phase: { at: "spawn", round: p.round, busy: p.n, minutes: carved.minutes, holds: carved.holds },
|
|
1492
|
+
},
|
|
1493
|
+
notes: [],
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1307
1496
|
case "wait":
|
|
1308
1497
|
return {
|
|
1309
1498
|
state: { ...s, phase: { at: "read", round: p.round, runId: p.runId, n: p.n, until: p.until } },
|
|
@@ -1356,7 +1545,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1356
1545
|
if (r.outcome === "refused")
|
|
1357
1546
|
return end(clocked, { kind: "merge_refused", pr: p.pr, reason: r.reason, reviewRounds: s.reviewRounds });
|
|
1358
1547
|
const waited = r.at - p.since;
|
|
1359
|
-
if (waited >=
|
|
1548
|
+
if (waited >= p.waitMs)
|
|
1360
1549
|
return end(clocked, {
|
|
1361
1550
|
kind: "merge_refused",
|
|
1362
1551
|
pr: p.pr,
|
|
@@ -1364,13 +1553,19 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1364
1553
|
reviewRounds: s.reviewRounds,
|
|
1365
1554
|
});
|
|
1366
1555
|
return {
|
|
1367
|
-
state: {
|
|
1556
|
+
state: {
|
|
1557
|
+
...clocked,
|
|
1558
|
+
phase: { at: "merge-wait", pr: p.pr, headSha: p.headSha, n: p.n, since: p.since, waitMs: p.waitMs },
|
|
1559
|
+
},
|
|
1368
1560
|
notes: [],
|
|
1369
1561
|
};
|
|
1370
1562
|
}
|
|
1371
1563
|
case "merge-wait":
|
|
1372
1564
|
return {
|
|
1373
|
-
state: {
|
|
1565
|
+
state: {
|
|
1566
|
+
...s,
|
|
1567
|
+
phase: { at: "merge", pr: p.pr, headSha: p.headSha, n: p.n + 1, since: p.since, waitMs: p.waitMs },
|
|
1568
|
+
},
|
|
1374
1569
|
notes: [],
|
|
1375
1570
|
};
|
|
1376
1571
|
case "ended":
|
|
@@ -1458,6 +1653,12 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1458
1653
|
// stated decision, never a silent one.
|
|
1459
1654
|
const level = s.input.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
|
|
1460
1655
|
const levelLine = `Severity addressed: ${level} and above (set by ${s.input.addressSeveritySource ?? "org"}).`;
|
|
1656
|
+
// The grant as the instance carries it (decision 0046): spent of granted,
|
|
1657
|
+
// the cap when one is set, and who granted it — zero of zero until a scope
|
|
1658
|
+
// or a directive says otherwise, and nothing spends it before the renewal
|
|
1659
|
+
// decision exists.
|
|
1660
|
+
const grant = s.input.grant ?? DEFAULT_GRANT;
|
|
1661
|
+
const grantLine = `Renewals: 0 of ${grant.renewals} spent${grant.costCapUsd !== undefined ? `, cost cap $${grant.costCapUsd}` : ""} (granted by ${s.input.grantSource ?? "org"}).`;
|
|
1461
1662
|
const lastFindings = s.findingsByRound[e.reviewRounds] ?? [];
|
|
1462
1663
|
const skipped = lastFindings.filter((f) => !findingsAtOrAbove([f], level).length);
|
|
1463
1664
|
const skippedLine =
|
|
@@ -1473,6 +1674,7 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1473
1674
|
`✅ 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.`,
|
|
1474
1675
|
verdictLine,
|
|
1475
1676
|
levelLine,
|
|
1677
|
+
grantLine,
|
|
1476
1678
|
...(skippedLine ? [skippedLine] : []),
|
|
1477
1679
|
declinedLine,
|
|
1478
1680
|
].join("\n");
|
|
@@ -1481,6 +1683,7 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1481
1683
|
`✅ Merge-ready after ${rounds}: ${e.pr.url}`,
|
|
1482
1684
|
verdictLine,
|
|
1483
1685
|
levelLine,
|
|
1686
|
+
grantLine,
|
|
1484
1687
|
...(skippedLine ? [skippedLine] : []),
|
|
1485
1688
|
declinedLine,
|
|
1486
1689
|
// What the driver read at the approved head when it composed this
|
|
@@ -1505,7 +1708,7 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1505
1708
|
]);
|
|
1506
1709
|
case "wall_clock_cap":
|
|
1507
1710
|
return join([
|
|
1508
|
-
`🧢 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 — no approval after ${rounds}.${prLine}`,
|
|
1711
|
+
`🧢 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}`,
|
|
1509
1712
|
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
1510
1713
|
splitReport(s),
|
|
1511
1714
|
reissue,
|
|
@@ -1526,7 +1729,19 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1526
1729
|
reissue,
|
|
1527
1730
|
]);
|
|
1528
1731
|
case "aborted":
|
|
1529
|
-
return join([
|
|
1732
|
+
return join([
|
|
1733
|
+
e.finalReply,
|
|
1734
|
+
e.reason,
|
|
1735
|
+
e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
|
|
1736
|
+
`⚠️ Ship aborted after ${rounds}.`,
|
|
1737
|
+
reissue,
|
|
1738
|
+
]);
|
|
1739
|
+
case "continued":
|
|
1740
|
+
return join([
|
|
1741
|
+
e.finalReply,
|
|
1742
|
+
`🔁 Segment ${e.segment - 1} ended at its lease with the unit unfinished — ${e.line}. Segment ${e.segment} opens in this thread${e.from !== undefined ? ` from \`${e.from.slice(0, 7)}\`` : ""} under a fresh ${s.input.caps.maxMinutes}-minute lease, with this segment's write-up as its request; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`,
|
|
1743
|
+
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
1744
|
+
]);
|
|
1530
1745
|
case "no_verdict":
|
|
1531
1746
|
return join([
|
|
1532
1747
|
`⚠️ 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.`,
|