@coreplane/switchboard 1.243.0 → 1.245.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.
Files changed (159) hide show
  1. package/dist/assets/config/config.example.yaml +6 -3
  2. package/dist/assets/deploy/cloudflare/worker.ts +2 -0
  3. package/dist/assets/deploy/cloudflare-memory/worker.ts +148 -0
  4. package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
  5. package/dist/assets/deploy/cloudflare-resident/threadErr.ts +337 -0
  6. package/dist/assets/deploy/cloudflare-resident/tsconfig.json +1 -1
  7. package/dist/assets/deploy/cloudflare-resident/worker.ts +258 -173
  8. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +18 -12
  9. package/dist/assets/deploy/secrets.manifest.json +12 -0
  10. package/dist/assets/package-lock.json +3 -3
  11. package/dist/assets/package.json +1 -1
  12. package/dist/assets/source.json +3 -3
  13. package/dist/assets/src/agents/registry.ts +5 -5
  14. package/dist/assets/src/config/profile.ts +17 -1
  15. package/dist/assets/src/core/authz/types.ts +18 -0
  16. package/dist/assets/src/core/budgets.ts +61 -0
  17. package/dist/assets/src/core/coordinator/contract.ts +32 -0
  18. package/dist/assets/src/core/coordinator/driver.ts +75 -12
  19. package/dist/assets/src/core/delivery.ts +1 -1
  20. package/dist/assets/src/core/reviewVerdict.ts +101 -24
  21. package/dist/assets/src/core/runEvents.ts +76 -8
  22. package/dist/assets/src/core/runFriction.ts +2 -1
  23. package/dist/assets/src/core/runRecord.ts +65 -0
  24. package/dist/assets/src/core/ship/contract.ts +2 -1
  25. package/dist/assets/src/core/ship/coordinator.ts +235 -33
  26. package/dist/assets/src/core/ship/renewal.ts +155 -0
  27. package/dist/assets/src/core/trace/attrs.ts +11 -0
  28. package/dist/assets/src/core/trace/types.ts +4 -3
  29. package/dist/assets/src/execution/residentRefresh.ts +46 -7
  30. package/dist/assets/src/execution/residentText.ts +8 -3
  31. package/dist/assets/src/mcp/registry.ts +3 -0
  32. package/dist/assets/web/dist/.vite/manifest.json +491 -472
  33. package/dist/assets/web/dist/assets/AppShell-qqs_p96-.js +1 -0
  34. package/dist/assets/web/dist/assets/CostsPage-Dd9aXxxI.js +2 -0
  35. package/dist/assets/web/dist/assets/DeliveryPage-D90FTjYq.js +1 -0
  36. package/dist/assets/web/dist/assets/HomePage-VLTFopnl.js +2 -0
  37. package/dist/assets/web/dist/assets/InputMenu-lQP3ia9D.js +1 -0
  38. package/dist/assets/web/dist/assets/NotFoundPage-8ViskpN3.js +1 -0
  39. package/dist/assets/web/dist/assets/PendingTurnRow-D2gHef5v.js +1 -0
  40. package/dist/assets/web/dist/assets/ResidentDetailPage-nWLN8BOd.js +1 -0
  41. package/dist/assets/web/dist/assets/ResidentsIndexPage-D3Q48CJW.js +1 -0
  42. package/dist/assets/web/dist/assets/RunFoldRow-CHy_IfZG.js +1 -0
  43. package/dist/assets/web/dist/assets/RunRoutePage-C6kX5om4.js +9 -0
  44. package/dist/assets/web/dist/assets/RunsIndexPage-DjxGj1Q5.js +1 -0
  45. package/dist/assets/web/dist/assets/RunsTabs-WyeG5tKb.js +1 -0
  46. package/dist/assets/web/dist/assets/ScheduledPage-B8D5AU4d.js +1 -0
  47. package/dist/assets/web/dist/assets/SettingSelect-UTHsqUUS.js +1 -0
  48. package/dist/assets/web/dist/assets/SettingsPage-BoW7Zjzt.js +1 -0
  49. package/dist/assets/web/dist/assets/StatusDot-BGHureCJ.js +1 -0
  50. package/dist/assets/web/dist/assets/Tooltip-CiP8qPd7.js +1 -0
  51. package/dist/assets/web/dist/assets/UnitRoutePage-2zEeRBP1.js +1 -0
  52. package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-qXhb9ti8.js} +1 -1
  53. package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-DzvdyoUZ.js} +1 -1
  54. package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-Ck7Ru2Sm.js} +1 -1
  55. package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-CwehvReb.js} +1 -1
  56. package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-WSyVbSFK.js} +1 -1
  57. package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DMHI1wDz.js} +1 -1
  58. package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-BF8AYK8_.js} +1 -1
  59. package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-UAfImuWu.js} +1 -1
  60. package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-D54hi96d.js} +1 -1
  61. package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-BRWfLgBc.js} +1 -1
  62. package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-CSqC5deT.js} +1 -1
  63. package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-D4ZuRFGk.js} +1 -1
  64. package/dist/assets/web/dist/assets/{dist-twkFmSUY.js → dist-73ZMn_Mg.js} +2 -2
  65. package/dist/assets/web/dist/assets/{durationTone-BobbycC-.js → durationTone-D51enaGR.js} +1 -1
  66. package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-DDPtNbZc.js} +1 -1
  67. package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-BTQkQfsJ.js} +1 -1
  68. package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-B_PkLzGZ.js} +1 -1
  69. package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-DfjIMVM_.js} +1 -1
  70. package/dist/assets/web/dist/assets/{eventSource-tRprhqm7.js → eventSource-By0B_w4S.js} +1 -1
  71. package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DP8wmn7K.js} +1 -1
  72. package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-G12VK2gK.js} +1 -1
  73. package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-D3XiSI8E.js} +1 -1
  74. package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DOKAhMj-.js} +1 -1
  75. package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CJB_fmON.js} +1 -1
  76. package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BHqtRWap.js} +1 -1
  77. package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-DwotyarS.js} +1 -1
  78. package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-B6fDaA3U.js} +1 -1
  79. package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-C5bpQmmI.js} +1 -1
  80. package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-s52taj36.js} +1 -1
  81. package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-99zC935r.js} +1 -1
  82. package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-Bgk-4-0x.js} +1 -1
  83. package/dist/assets/web/dist/assets/{indexRow-B_s5tKyq.js → indexRow-BmK74Vp1.js} +1 -1
  84. package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-CuV_c5I0.js} +1 -1
  85. package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-CZlOvSMQ.js} +1 -1
  86. package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-BuXeZ4Tu.js} +1 -1
  87. package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-DIuP0hkG.js} +1 -1
  88. package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-CZ74vfoD.js} +1 -1
  89. package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-BNjRs8Vj.js} +1 -1
  90. package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-OCE-fFOg.js} +1 -1
  91. package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-CtvD87WF.js} +1 -1
  92. package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-C3kf86OR.js} +1 -1
  93. package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-B1WI--Kp.js} +1 -1
  94. package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-DtoT0gKg.js} +1 -1
  95. package/dist/assets/web/dist/assets/main-ClluP-Qf.js +28 -0
  96. package/dist/assets/web/dist/assets/main-CpkwzHX6.css +1 -0
  97. package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-BE2MC6Yt.js} +1 -1
  98. package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-BwO4weXJ.js} +1 -1
  99. package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-BTuOgGVC.js} +1 -1
  100. package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-CiIUM-oV.js} +1 -1
  101. package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-L78DEWB-.js} +1 -1
  102. package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-KqHOm4Ft.js} +1 -1
  103. package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-C41XbEkn.js} +1 -1
  104. package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-B12-4i-k.js} +1 -1
  105. package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml--fw2Up5c.js} +1 -1
  106. package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-Db1JBXrC.js} +1 -1
  107. package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-BYpVBgbu.js} +1 -1
  108. package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-BMqGLSPc.js} +1 -1
  109. package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-B63n7-Sa.js} +1 -1
  110. package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-D3VI8UYv.js} +1 -1
  111. package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DxRf8ZAR.js} +1 -1
  112. package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DRRA6d6A.js} +1 -1
  113. package/dist/assets/web/dist/assets/seed-DFfT5S_N.js +6 -0
  114. package/dist/assets/web/dist/assets/{settingsApi-priOPiwu.js → settingsApi-Dm6eaQcu.js} +1 -1
  115. package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CoOBe3_y.js} +1 -1
  116. package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DOV1HYxf.js} +1 -1
  117. package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-Acpmpp1W.js} +1 -1
  118. package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-DeT9N-fA.js} +1 -1
  119. package/dist/assets/web/dist/assets/{sseReplay-C9m_EB8J.js → sseReplay-DPXVxhL5.js} +5 -5
  120. package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CbxXAMO4.js} +1 -1
  121. package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-BdKbi4hc.js} +1 -1
  122. package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-tAcjDM7R.js} +1 -1
  123. package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CObINNJj.js} +1 -1
  124. package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-BYwpZT1J.js} +1 -1
  125. package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags-BciREL5R.js} +1 -1
  126. package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-CRiv3Pr3.js} +1 -1
  127. package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-DuZZ1Ej0.js} +1 -1
  128. package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-jkHz_5HZ.js} +1 -1
  129. package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-DOac4Gal.js} +1 -1
  130. package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-CgOX5wXA.js} +1 -1
  131. package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-Duu99VNw.js} +1 -1
  132. package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-Bmj_wvRJ.js} +1 -1
  133. package/dist/assets/web/dist/assets/wallClock-DD_fPuSY.js +1 -0
  134. package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-BtUIVeWH.js} +1 -1
  135. package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-wX71n1E5.js} +1 -1
  136. package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-CdLdXvmz.js} +1 -1
  137. package/dist/cli.js +4330 -1256
  138. package/package.json +1 -1
  139. package/dist/assets/web/dist/assets/AppShell-IDMGG6yi.js +0 -1
  140. package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +0 -2
  141. package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
  142. package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +0 -2
  143. package/dist/assets/web/dist/assets/NotFoundPage-C97EhJcQ.js +0 -1
  144. package/dist/assets/web/dist/assets/PendingTurnRow-BT9RhFZ7.js +0 -1
  145. package/dist/assets/web/dist/assets/ResidentDetailPage-DACalNVF.js +0 -1
  146. package/dist/assets/web/dist/assets/ResidentsIndexPage-SHPdu6uZ.js +0 -1
  147. package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +0 -1
  148. package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +0 -9
  149. package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +0 -1
  150. package/dist/assets/web/dist/assets/RunsTabs-BUfdk0lH.js +0 -1
  151. package/dist/assets/web/dist/assets/ScheduledPage-DJ8HiCPt.js +0 -1
  152. package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +0 -1
  153. package/dist/assets/web/dist/assets/StatusDot-Dw0T1M-P.js +0 -1
  154. package/dist/assets/web/dist/assets/Tooltip-BbLuIAiS.js +0 -1
  155. package/dist/assets/web/dist/assets/UnitRoutePage-DicUG96U.js +0 -1
  156. package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +0 -1
  157. package/dist/assets/web/dist/assets/main-d-w-tIKt.js +0 -28
  158. package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +0 -6
  159. package/dist/assets/web/dist/assets/wallClock-61NpVkY6.js +0 -1
@@ -30,11 +30,34 @@
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
- import { formatFinding, type Finding, type FindingDisposition, type ReviewVerdictKind } from "../reviewVerdict.js";
37
+ import {
38
+ ADDRESS_SEVERITIES,
39
+ DEFAULT_ADDRESS_SEVERITY,
40
+ findingsAtOrAbove,
41
+ formatFinding,
42
+ isAddressSeverity,
43
+ type AddressSeverity,
44
+ type AddressSeveritySource,
45
+ type Finding,
46
+ type FindingDisposition,
47
+ type ReviewVerdictKind,
48
+ } from "../reviewVerdict.js";
36
49
  import { parsePlanUnit, planUnitIds } from "./contract.js";
37
- import { carve, loopPosition, MINUTE_MS, SHIP_WAIT, type Carve, type Loop } from "../budgets.js";
50
+ import {
51
+ carve,
52
+ DEFAULT_GRANT,
53
+ loopPosition,
54
+ MINUTE_MS,
55
+ SHIP_WAIT,
56
+ type Carve,
57
+ type Grant,
58
+ type GrantSource,
59
+ type Loop,
60
+ } from "../budgets.js";
38
61
 
39
62
  const MIN = MINUTE_MS;
40
63
 
@@ -391,7 +414,14 @@ export function presetOf(kind: RoundKind): ChildPreset {
391
414
  * turn: the unit's contract, or the review turn from the pull request and the
392
415
  * prior rounds' records, or the findings message from the review run's verdict. */
393
416
  export type Brief =
394
- | { kind: "contract"; unit: string; rebase: { branch: string; onto: string } }
417
+ | {
418
+ kind: "contract";
419
+ unit: string;
420
+ rebase: { branch: string; onto: string };
421
+ /** A renewal's segment (decision 0046): the child continues the previous
422
+ * segment's work from `from`, briefed with that run's write-up and handoff. */
423
+ continue?: { segment: number; from?: string; previousRunId?: string };
424
+ }
395
425
  | {
396
426
  kind: "review";
397
427
  unit: string;
@@ -455,6 +485,13 @@ export type ChildFacts =
455
485
  /** The dispositions a coding run recorded, as it submitted them: the machine matches them to the round's findings. */
456
486
  dispositions?: FindingDisposition[];
457
487
  handoff?: boolean;
488
+ /** The renewal's facts (decision 0046): the heads the run pushed, when
489
+ * its lease began, what it cost (null when a model had no price) and
490
+ * the handoff's lists — progress is read off these, never asked. */
491
+ pushed?: PushedHeadFact[];
492
+ leaseStartedAt?: number;
493
+ costUsd?: number | null;
494
+ handoffLists?: Handoff;
458
495
  };
459
496
 
460
497
  /** What heads the unit's branch on GitHub: nothing, an open pull request, or —
@@ -524,7 +561,38 @@ export type UnitEnding =
524
561
  /** A changes-requested review posted this round before the stop. */
525
562
  postedReview?: boolean;
526
563
  }
527
- | { kind: "aborted"; reason: string; round?: RoundRef; reviewRounds: number; finalReply?: string }
564
+ | {
565
+ kind: "aborted";
566
+ reason: string;
567
+ round?: RoundRef;
568
+ reviewRounds: number;
569
+ finalReply?: string;
570
+ /** A round-0 end without a pull request that was judged for renewal and
571
+ * refused: the decision and the card's sentence (decision 0046). */
572
+ renewal?: { decision: Extract<RenewalDecision, { renew: false }>; line: string };
573
+ }
574
+ /** The coding round ended at its lease with the unit unfinished, the row
575
+ * showed progress and the grant renewed: this segment is over and the next
576
+ * opens in the same thread from `from` under a fresh lease, the coding
577
+ * child's write-up as its request (decision 0046, Renewal). */
578
+ | {
579
+ kind: "continued";
580
+ round: RoundRef;
581
+ /** The coding child whose write-up and handoff the continuation is briefed with. */
582
+ runId: string;
583
+ /** The segment the renewal opens (the first segment is 1). */
584
+ segment: number;
585
+ from?: string;
586
+ renewalsLeft: number;
587
+ /** The session's spend so far, this segment's children included; null when a run's model had no price. */
588
+ spendUsd: number | null;
589
+ handoff?: Handoff;
590
+ /** The coding child's write-up: the checkpoint the continuation is briefed with. */
591
+ finalReply?: string;
592
+ line: string;
593
+ reviewRounds: number;
594
+ spent: ShipBudgetSpent;
595
+ }
528
596
  | { kind: "no_verdict"; round: RoundRef; reviewRounds: number; finalReply?: string }
529
597
  | { kind: "interrupted"; round: RoundRef; runId: string; reviewRounds: number }
530
598
  | { kind: "refused"; refusal: string; message?: string; round: RoundRef; reviewRounds: number };
@@ -541,25 +609,28 @@ export type CoordinatorNote =
541
609
  * person can see whether the cap or the child is the problem. */
542
610
  export type ShipBudgetSpent = Readonly<Record<"coding" | "review" | "waiting", number>>;
543
611
 
544
- /** The severity ship must address before an approve stands (agent-ship item
545
- * 9's gate): an approve carrying a finding at or above this level continues
546
- * into the findings step exactly as a request_changes does. Ordered most to
547
- * least severe. */
548
- export const ADDRESS_SEVERITIES = ["blocking", "major", "minor", "nit"] as const;
549
- export type AddressSeverity = (typeof ADDRESS_SEVERITIES)[number];
550
- /** Where the level in force came from, most specific wins: a `severity:` directive on the request (`run`), the user's or the channel's config scope, the org's `ship.addressSeverity` (or its default). */
551
- export type AddressSeveritySource = "org" | "channel" | "user" | "run";
552
- export const DEFAULT_ADDRESS_SEVERITY: AddressSeverity = "minor";
553
- export const isAddressSeverity = (v: unknown): v is AddressSeverity =>
554
- (ADDRESS_SEVERITIES as readonly unknown[]).includes(v);
555
-
556
- const severityRank = (s: AddressSeverity): number => ADDRESS_SEVERITIES.indexOf(s);
557
- /** The findings the gate acts on at `level`: at or above it. Every parsed
558
- * finding carries one of the ladder's four levels — the verdict parser
559
- * (`parseVerdictInput`) drops an entry with any other severity, `fyi` or none,
560
- * with a note — so nothing outside the ladder ever reaches the gate. */
561
- export function findingsAtOrAbove(findings: readonly Finding[], level: AddressSeverity): Finding[] {
562
- return findings.filter((f) => severityRank(f.severity) <= severityRank(level));
612
+ // The severity to address (agent-ship item 9's gate) is the verdict parser's
613
+ // ladder (src/core/reviewVerdict.ts): the same level the parser holds a
614
+ // submitted approve to, so a posted approve can carry a gated finding only
615
+ // when it was parsed at a different level — the check below is defense in
616
+ // depth behind the parser's. Re-exported here for the runner's callers.
617
+ export {
618
+ ADDRESS_SEVERITIES,
619
+ DEFAULT_ADDRESS_SEVERITY,
620
+ findingsAtOrAbove,
621
+ isAddressSeverity,
622
+ type AddressSeverity,
623
+ type AddressSeveritySource,
624
+ };
625
+
626
+ export interface UnitSession {
627
+ segment: number;
628
+ renewalsSpent: number;
629
+ spendUsd: number | null;
630
+ continueFrom?: string;
631
+ /** The previous segment's coding run: its write-up and handoff brief the continuation. */
632
+ previousRunId?: string;
633
+ previousHandoff?: Handoff;
563
634
  }
564
635
 
565
636
  export interface UnitPipelineInput {
@@ -578,6 +649,18 @@ export interface UnitPipelineInput {
578
649
  addressSeverity?: AddressSeverity;
579
650
  /** Which layer set the level in force; named in the round header and the ending. */
580
651
  addressSeveritySource?: AddressSeveritySource;
652
+ /** The grant the request carried (decision 0046, the renewable lease):
653
+ * renewals and a cost cap, and which layer granted it — named in the report;
654
+ * absent reads as zero renewals and no cap, the org's. Nothing here spends
655
+ * it yet: the renewal decision is the segment's end, not this unit's. */
656
+ grant?: Grant;
657
+ grantSource?: GrantSource;
658
+ /** The segment this pipeline runs (decision 0046, Renewal): absent for the
659
+ * first; a renewal's carries its number, the renewals spent before it, the
660
+ * session's spend so far, the sha it continues from and the previous
661
+ * segment's handoff. Every step name of a later segment is prefixed with
662
+ * it, so the Workflow's durable steps never collide across segments. */
663
+ session?: UnitSession;
581
664
  /** The instance's mark (agent-ship item 16): a generated one-unit plan — a
582
665
  * `plan` with an id and no `path` — whose unit runs in the requesting
583
666
  * thread and is re-issued with the request's own text, never a plan path. */
@@ -606,6 +689,10 @@ type Phase =
606
689
  runId: string;
607
690
  childHead?: string;
608
691
  finalReply?: string;
692
+ /** The renewal's facts off the child's record (decision 0046): its pushed heads, its lease's start, its handoff. */
693
+ childPushed?: PushedHeadFact[];
694
+ childLeaseStartedAt?: number;
695
+ childHandoff?: Handoff;
609
696
  /** The coding child died (`failed` or `interrupted`) after it may have
610
697
  * pushed: the pr-check recovers a pushed branch by opening its pull
611
698
  * request; with nothing pushed the unit ends with the child's own reason. */
@@ -639,6 +726,9 @@ export interface UnitPipelineState {
639
726
  readonly lastCodingRunId?: string;
640
727
  /** How the budget went so far, accrued as each answer moves the clock. */
641
728
  readonly spentMs: ShipBudgetSpent;
729
+ /** The session's dollars so far: the input's from earlier segments plus each
730
+ * child's cost as its record is read; null once any run's cost is unknown. */
731
+ readonly spendUsd: number | null;
642
732
  readonly ending?: UnitEnding;
643
733
  }
644
734
 
@@ -674,6 +764,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
674
764
  phase: { at: "pre-check" },
675
765
  reviewRounds: 0,
676
766
  spentMs: { coding: 0, review: 0, waiting: 0 },
767
+ spendUsd: input.session?.spendUsd ?? 0,
677
768
  findingsByRound: {},
678
769
  dispositionsByRound: {},
679
770
  reviewRunByRound: {},
@@ -713,12 +804,33 @@ function roundCarve(s: UnitPipelineState, round: RoundRef): Carve {
713
804
  return carve(remainingMs(s), { kind, index: loopPosition(loopOf(s), kind, round.index) }, loopOf(s));
714
805
  }
715
806
 
716
- const roundStep = (s: UnitPipelineState, round: RoundRef) => `${s.input.unit.id}/${round.index}/${round.kind}`;
807
+ /** The prefix every step of this pipeline is named under: the unit id, and for
808
+ * a renewal's segment the segment too (`U10/s2/…`), so the Workflow's durable
809
+ * step cache never hands segment two the answers of segment one. */
810
+ export const stepPrefixOf = (unit: string, session: UnitSession | undefined): string =>
811
+ session !== undefined && session.segment > 1 ? `${unit}/s${session.segment}` : unit;
812
+ const stepPrefix = (s: UnitPipelineState) => stepPrefixOf(s.input.unit.id, s.input.session);
813
+ const roundStep = (s: UnitPipelineState, round: RoundRef) => `${stepPrefix(s)}/${round.index}/${round.kind}`;
717
814
 
718
815
  function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
719
816
  const unit = s.input.unit.id;
720
- if (round.kind === "coding")
721
- return { kind: "contract", unit, rebase: { branch: s.input.unit.branch, onto: s.input.base } };
817
+ if (round.kind === "coding") {
818
+ const session = s.input.session;
819
+ return {
820
+ kind: "contract",
821
+ unit,
822
+ rebase: { branch: s.input.unit.branch, onto: s.input.base },
823
+ ...(session !== undefined && session.segment > 1
824
+ ? {
825
+ continue: {
826
+ segment: session.segment,
827
+ ...(session.continueFrom !== undefined ? { from: session.continueFrom } : {}),
828
+ ...(session.previousRunId !== undefined ? { previousRunId: session.previousRunId } : {}),
829
+ },
830
+ }
831
+ : {}),
832
+ };
833
+ }
722
834
  const pr = s.pr!.number;
723
835
  if (round.kind === "findings") return { kind: "findings", unit, pr, reviewRunId: s.reviewRunByRound[round.index]! };
724
836
  const priorReview = s.reviewRunByRound[round.index - 1];
@@ -754,7 +866,7 @@ export function matchDispositions(
754
866
  /** The step the machine is at. Pure over the state: asked before every step
755
867
  * and again after a replay, it names the same step for the same state. */
756
868
  export function nextAction(s: UnitPipelineState): CoordinatorAction {
757
- const unit = s.input.unit.id;
869
+ const unit = stepPrefix(s);
758
870
  const p = s.phase;
759
871
  switch (p.at) {
760
872
  case "pre-check":
@@ -815,6 +927,14 @@ interface Transition {
815
927
 
816
928
  const ENDED: Phase = { at: "ended" };
817
929
 
930
+ /** The session's dollars after one more child: unknown (null) once any run's
931
+ * cost is — a total that left a run out would understate the spend a cap
932
+ * judges — and a record without the field counts as unknown too. */
933
+ function addSpend(sum: number | null, cost: number | null | undefined): number | null {
934
+ if (sum === null || cost === null || cost === undefined) return null;
935
+ return sum + cost;
936
+ }
937
+
818
938
  function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[] = []): Transition {
819
939
  return { state: { ...s, phase: ENDED, ending }, notes: [...notes, { type: "ended", ending }] };
820
940
  }
@@ -885,6 +1005,7 @@ function settleCoding(
885
1005
  // round's findings enter the state (agent-ship item 6).
886
1006
  let next: UnitPipelineState = {
887
1007
  ...s,
1008
+ spendUsd: addSpend(s.spendUsd, facts.costUsd),
888
1009
  ...(facts.pr !== undefined ? { pr: { number: facts.pr.number, url: facts.pr.url } } : {}),
889
1010
  ...(round.kind === "findings" && facts.dispositions !== undefined
890
1011
  ? {
@@ -922,6 +1043,9 @@ function settleCoding(
922
1043
  runId,
923
1044
  ...(facts.headSha !== undefined ? { childHead: facts.headSha } : {}),
924
1045
  ...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
1046
+ ...(facts.pushed !== undefined ? { childPushed: facts.pushed } : {}),
1047
+ ...(facts.leaseStartedAt !== undefined ? { childLeaseStartedAt: facts.leaseStartedAt } : {}),
1048
+ ...(facts.handoffLists !== undefined ? { childHandoff: facts.handoffLists } : {}),
925
1049
  },
926
1050
  };
927
1051
  return { state: next, notes: [] };
@@ -961,6 +1085,7 @@ function settleReview(
961
1085
  const verdict = facts.verdict;
962
1086
  const next: UnitPipelineState = {
963
1087
  ...s,
1088
+ spendUsd: addSpend(s.spendUsd, facts.costUsd),
964
1089
  findingsByRound: { ...s.findingsByRound, [round.index]: verdict.findings },
965
1090
  ...(facts.reviewHead !== undefined ? { lastReviewHead: facts.reviewHead } : {}),
966
1091
  ...(verdict.summary !== undefined ? { lastVerdictSummary: verdict.summary } : {}),
@@ -1122,15 +1247,72 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
1122
1247
  [roundNote(round, "aborted")],
1123
1248
  );
1124
1249
  }
1125
- const reason =
1126
- round.index === 0
1127
- ? `⚠️ 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.`
1128
- : `⚠️ 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.`;
1250
+ // Round 0 ended without a pull request: the segment is over with the unit
1251
+ // unfinished, and the grant decides whether the next opens (decision
1252
+ // 0046, Renewal). Progress is read off the child's record — a head pushed
1253
+ // to the unit's branch since the segment started, or a handoff that moved —
1254
+ // never off its words; the decision then asks the grant's count, the cap
1255
+ // and the fit, in that order, and a refusal names the clause. A plain
1256
+ // abort keeps its old shape when nothing was pushed under a grant of zero:
1257
+ // a clarifying question is not a stop to explain.
1258
+ if (round.index === 0) {
1259
+ const grant = s.input.grant ?? DEFAULT_GRANT;
1260
+ const session = s.input.session;
1261
+ const progress = progressOf({
1262
+ branch: s.input.unit.branch,
1263
+ pushed: phase.childPushed ?? [],
1264
+ ...(session?.continueFrom !== undefined ? { startHead: session.continueFrom } : {}),
1265
+ ...(phase.childLeaseStartedAt !== undefined ? { leaseStartedAt: phase.childLeaseStartedAt } : {}),
1266
+ ...(session?.previousHandoff !== undefined && phase.childHandoff !== undefined
1267
+ ? { handoff: { previous: session.previousHandoff, current: phase.childHandoff } }
1268
+ : {}),
1269
+ });
1270
+ const decision = renewalDecision({
1271
+ grant,
1272
+ renewalsSpent: session?.renewalsSpent ?? 0,
1273
+ spendUsd: s.spendUsd,
1274
+ progress,
1275
+ pipeline: s.input.caps,
1276
+ });
1277
+ const line = renderRenewal(decision, grant);
1278
+ if (decision.renew)
1279
+ return end(
1280
+ s,
1281
+ {
1282
+ kind: "continued",
1283
+ round,
1284
+ runId: phase.runId,
1285
+ segment: decision.segment,
1286
+ ...(decision.from !== undefined ? { from: decision.from } : {}),
1287
+ renewalsLeft: decision.renewalsLeft,
1288
+ spendUsd: s.spendUsd,
1289
+ ...(phase.childHandoff !== undefined ? { handoff: phase.childHandoff } : {}),
1290
+ ...(phase.finalReply !== undefined ? { finalReply: phase.finalReply } : {}),
1291
+ line,
1292
+ reviewRounds: s.reviewRounds,
1293
+ spent: s.spentMs,
1294
+ },
1295
+ [roundNote(round, "continued")],
1296
+ );
1297
+ const judged = grant.renewals > 0 || progress.progressed;
1298
+ return end(
1299
+ s,
1300
+ {
1301
+ kind: "aborted",
1302
+ 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.`,
1303
+ round,
1304
+ reviewRounds: s.reviewRounds,
1305
+ ...(phase.finalReply !== undefined ? { finalReply: phase.finalReply } : {}),
1306
+ ...(judged ? { renewal: { decision, line } } : {}),
1307
+ },
1308
+ [roundNote(round, "aborted")],
1309
+ );
1310
+ }
1129
1311
  return end(
1130
1312
  s,
1131
1313
  {
1132
1314
  kind: "aborted",
1133
- reason,
1315
+ 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.`,
1134
1316
  round,
1135
1317
  reviewRounds: s.reviewRounds,
1136
1318
  ...(phase.finalReply !== undefined ? { finalReply: phase.finalReply } : {}),
@@ -1475,6 +1657,12 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
1475
1657
  // stated decision, never a silent one.
1476
1658
  const level = s.input.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
1477
1659
  const levelLine = `Severity addressed: ${level} and above (set by ${s.input.addressSeveritySource ?? "org"}).`;
1660
+ // The grant as the instance carries it (decision 0046): spent of granted,
1661
+ // the cap when one is set, and who granted it — zero of zero until a scope
1662
+ // or a directive says otherwise, and nothing spends it before the renewal
1663
+ // decision exists.
1664
+ const grant = s.input.grant ?? DEFAULT_GRANT;
1665
+ const grantLine = `Renewals: 0 of ${grant.renewals} spent${grant.costCapUsd !== undefined ? `, cost cap $${grant.costCapUsd}` : ""} (granted by ${s.input.grantSource ?? "org"}).`;
1478
1666
  const lastFindings = s.findingsByRound[e.reviewRounds] ?? [];
1479
1667
  const skipped = lastFindings.filter((f) => !findingsAtOrAbove([f], level).length);
1480
1668
  const skippedLine =
@@ -1490,6 +1678,7 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
1490
1678
  `✅ 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.`,
1491
1679
  verdictLine,
1492
1680
  levelLine,
1681
+ grantLine,
1493
1682
  ...(skippedLine ? [skippedLine] : []),
1494
1683
  declinedLine,
1495
1684
  ].join("\n");
@@ -1498,6 +1687,7 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
1498
1687
  `✅ Merge-ready after ${rounds}: ${e.pr.url}`,
1499
1688
  verdictLine,
1500
1689
  levelLine,
1690
+ grantLine,
1501
1691
  ...(skippedLine ? [skippedLine] : []),
1502
1692
  declinedLine,
1503
1693
  // What the driver read at the approved head when it composed this
@@ -1543,7 +1733,19 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
1543
1733
  reissue,
1544
1734
  ]);
1545
1735
  case "aborted":
1546
- return join([e.finalReply, e.reason, `⚠️ Ship aborted after ${rounds}.`, reissue]);
1736
+ return join([
1737
+ e.finalReply,
1738
+ e.reason,
1739
+ e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
1740
+ `⚠️ Ship aborted after ${rounds}.`,
1741
+ reissue,
1742
+ ]);
1743
+ case "continued":
1744
+ return join([
1745
+ e.finalReply,
1746
+ `🔁 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` : ""}.`,
1747
+ budgetSplitLine(e.spent, s.input.caps.maxMinutes),
1748
+ ]);
1547
1749
  case "no_verdict":
1548
1750
  return join([
1549
1751
  `⚠️ 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.`,
@@ -0,0 +1,155 @@
1
+ // The renewal decision (decision 0046, Renewal: the grant decides, the lease
2
+ // continues). A lease is sized by the infrastructure; the problem is bounded by
3
+ // the grant. When a segment ends with its unit unfinished, the runner reads
4
+ // progress off the row — never off a model's word — and renews only when three
5
+ // things hold: the row shows progress, the grant has a renewal left and the
6
+ // spend is under its cap, and the pipeline's ask still passes the fit.
7
+ // Otherwise it stops and names the clause that failed; a person's reply spends
8
+ // a renewal by hand. Pure and node-free: the unit machine (a Worker) and the
9
+ // bot's routes compute the same decision from the same facts.
10
+
11
+ import { fit, type Grant, type Pipeline } from "../budgets.js";
12
+ import type { Handoff } from "./handoff.js";
13
+
14
+ /** A head the run pushed, as the record carries it (run-history item 2). */
15
+ export interface PushedHeadFact {
16
+ ref: string;
17
+ sha: string;
18
+ at?: number;
19
+ }
20
+
21
+ export interface ProgressInput {
22
+ /** The unit's branch: a push to any other ref is not the unit's progress. */
23
+ branch: string;
24
+ /** The heads the segment's coding run pushed (`RunRecord.pushed`). */
25
+ pushed: readonly PushedHeadFact[];
26
+ /** The head the segment started from — the previous segment's recorded sha.
27
+ * Absent on a fresh branch, where any push is progress. */
28
+ startHead?: string;
29
+ /** When the segment's lease began; a push stamped before it is not this segment's. */
30
+ leaseStartedAt?: number;
31
+ /** The previous segment's handoff and this one's, when both exist. */
32
+ handoff?: { previous: Handoff; current: Handoff };
33
+ }
34
+
35
+ export type Progress =
36
+ | { progressed: true; by: "push"; sha: string }
37
+ | { progressed: true; by: "handoff" }
38
+ | { progressed: false; why: string };
39
+
40
+ /** Two shas name one commit when the shorter is a prefix of the longer. */
41
+ const sameSha = (a: string, b: string): boolean => {
42
+ const x = a.toLowerCase();
43
+ const y = b.toLowerCase();
44
+ return x.length <= y.length ? y.startsWith(x) : x.startsWith(y);
45
+ };
46
+
47
+ /** Progress is a fact the row records after unit eight and the runner reads
48
+ * without a model: the last head pushed to the unit's branch that differs from
49
+ * the head the segment started at and is not older than the lease; failing
50
+ * that, a handoff whose follow-ups shrank or whose deviations grew. */
51
+ export function progressOf(input: ProgressInput): Progress {
52
+ const last = [...input.pushed].reverse().find((h) => h.ref === input.branch);
53
+ if (last !== undefined) {
54
+ const newerThanLease =
55
+ input.leaseStartedAt === undefined || last.at === undefined || last.at >= input.leaseStartedAt;
56
+ const moved = input.startHead === undefined || !sameSha(last.sha, input.startHead);
57
+ if (newerThanLease && moved) return { progressed: true, by: "push", sha: last.sha };
58
+ }
59
+ const pushWhy = `no head newer than the lease's start was pushed to \`${input.branch}\``;
60
+ if (input.handoff === undefined) return { progressed: false, why: pushWhy };
61
+ const { previous, current } = input.handoff;
62
+ if (current.followUps.length < previous.followUps.length || current.deviations.length > previous.deviations.length)
63
+ return { progressed: true, by: "handoff" };
64
+ return { progressed: false, why: `${pushWhy} and the handoff is unchanged` };
65
+ }
66
+
67
+ export interface RenewalInput {
68
+ grant: Grant;
69
+ /** Renewals already spent under this request: the segments beyond the first. */
70
+ renewalsSpent: number;
71
+ /** Dollars spent under this request so far; null when a run's model had no
72
+ * price, since a total that left a model's tokens out would understate it. */
73
+ spendUsd: number | null;
74
+ progress: Progress;
75
+ /** The pipeline the next segment would run: its fit is re-asserted before every segment. */
76
+ pipeline: Pipeline;
77
+ }
78
+
79
+ export type RenewalWhy = "no_progress" | "grant_exhausted" | "cost_cap" | "unfit";
80
+
81
+ export type RenewalDecision =
82
+ | { renew: true; segment: number; from?: string; renewalsLeft: number }
83
+ | { renew: false; why: RenewalWhy; detail: string; renewalsLeft: number };
84
+
85
+ const usd = (n: number): string => `$${n.toFixed(2)}`;
86
+
87
+ /** The decision, in the order the record states the clauses: progress, the
88
+ * grant's count, the cap, the fit. `segment` is the one the renewal opens
89
+ * (the first segment is 1); `from` the sha it continues from when a push made
90
+ * the progress. */
91
+ export function renewalDecision(input: RenewalInput): RenewalDecision {
92
+ const renewalsLeft = Math.max(0, input.grant.renewals - input.renewalsSpent);
93
+ if (!input.progress.progressed) return { renew: false, why: "no_progress", detail: input.progress.why, renewalsLeft };
94
+ if (renewalsLeft === 0) {
95
+ const detail =
96
+ input.grant.renewals === 0
97
+ ? "the grant holds no renewals"
98
+ : `the grant's ${input.grant.renewals} renewal${input.grant.renewals === 1 ? "" : "s"} are spent`;
99
+ return { renew: false, why: "grant_exhausted", detail, renewalsLeft };
100
+ }
101
+ const cap = input.grant.costCapUsd;
102
+ if (cap !== undefined) {
103
+ if (input.spendUsd === null)
104
+ return {
105
+ renew: false,
106
+ why: "cost_cap",
107
+ detail: `spend is unknown (a model had no price) under the grant's cap of $${cap}`,
108
+ renewalsLeft,
109
+ };
110
+ if (input.spendUsd >= cap)
111
+ return {
112
+ renew: false,
113
+ why: "cost_cap",
114
+ detail: `spend ${usd(input.spendUsd)} reached the grant's cap of $${cap}`,
115
+ renewalsLeft,
116
+ };
117
+ }
118
+ const held = fit(input.pipeline);
119
+ if (!held.ok)
120
+ return {
121
+ renew: false,
122
+ why: "unfit",
123
+ detail: `a ${held.have}-minute segment cannot hold the ship loop (${input.pipeline.maxRounds} review rounds need ${held.need} min)`,
124
+ renewalsLeft,
125
+ };
126
+ return {
127
+ renew: true,
128
+ segment: input.renewalsSpent + 2,
129
+ ...(input.progress.by === "push" ? { from: input.progress.sha } : {}),
130
+ renewalsLeft: renewalsLeft - 1,
131
+ };
132
+ }
133
+
134
+ /** The card's words (the record's trace, steps 4 and 9): a renewal names its
135
+ * number of the grant's and the sha it continues from; a stop names the
136
+ * clause and, when renewals remain, the reply that spends one by hand. */
137
+ export function renderRenewal(decision: RenewalDecision, grant: Grant): string {
138
+ if (decision.renew)
139
+ return `renewal ${decision.segment - 1} of ${grant.renewals}, continues ${decision.from !== undefined ? decision.from.slice(0, 7) : "the branch's head"}`;
140
+ const holds =
141
+ decision.renewalsLeft > 0
142
+ ? `grant holds ${decision.renewalsLeft} renewal${decision.renewalsLeft === 1 ? "" : "s"}`
143
+ : "the grant holds no renewals";
144
+ switch (decision.why) {
145
+ case "no_progress":
146
+ return decision.renewalsLeft > 0
147
+ ? `no progress in the last lease; ${holds}; reply continue to spend one`
148
+ : `no progress in the last lease; ${holds}`;
149
+ case "cost_cap":
150
+ return decision.renewalsLeft > 0 ? `${decision.detail}; ${holds} unspent` : decision.detail;
151
+ case "grant_exhausted":
152
+ case "unfit":
153
+ return decision.detail;
154
+ }
155
+ }
@@ -34,6 +34,14 @@ export interface AttrDomain {
34
34
  /** The `<provider>/<model>` that took the turn — a ship run's children answer
35
35
  * on different models, and the run page badges the switch per step. */
36
36
  model: string;
37
+ /** The tools a proxied call offered the model (model-proxy.md item 6): how
38
+ * many, their names sorted and comma-joined, and the request's `tool_choice`
39
+ * by its word — `auto` when tools came and nothing was said, `none` when no
40
+ * tools came or the request said so, `any` for OpenAI's `required`, `tool`
41
+ * for a named tool. */
42
+ tools: number;
43
+ toolNames: string;
44
+ toolChoice: "auto" | "none" | "any" | "tool";
37
45
  stopReason: "end_turn" | "tool_use" | "max_tokens" | "stop_sequence" | "other";
38
46
  inputTokens: number;
39
47
  outputTokens: number;
@@ -159,6 +167,9 @@ const ATTR_TYPE: Record<SpanAttrKey, "string" | "number" | "boolean"> = {
159
167
  backend: "string",
160
168
  command: "string",
161
169
  model: "string",
170
+ tools: "number",
171
+ toolNames: "string",
172
+ toolChoice: "string",
162
173
  stopReason: "string",
163
174
  inputTokens: "number",
164
175
  outputTokens: "number",
@@ -70,9 +70,10 @@ export interface TraceOptions {
70
70
  span?: Span;
71
71
  /** Which door the call came through when it was not the surface's own
72
72
  * grammar: `route` for a command the request router bound from prose
73
- * (record 0036). Copied onto the registry's audit line, never read by a
74
- * handler. Absent for a typed command. */
75
- source?: "route";
73
+ * (record 0036), `confirm` for a stored input run at a confirmation's
74
+ * click (record 0044). Copied onto the registry's audit line, never read
75
+ * by a handler. Absent for a typed command. */
76
+ source?: "route" | "confirm";
76
77
  }
77
78
 
78
79
  export interface Span {