@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
@@ -19,19 +19,83 @@
19
19
  // a fix round can reference each finding by its stable id and answer it with
20
20
  // a typed disposition (`parseDispositionsInput` below). Findings validate
21
21
  // fail-closed PER finding — a malformed finding drops with a note, a
22
- // malformed array drops the whole field — and an `approve` carrying a
23
- // self-declared `blocking` finding is downgraded to `request_changes`, so the
24
- // posted body can never begin with `LGTM:` over a defect the reviewer itself
25
- // called blocking.
22
+ // malformed array drops the whole field.
23
+ //
24
+ // THE SEVERITY GATE (docs/reference/specs/agent-review.md item 5a): an
25
+ // `approve` carrying a finding at or above the severity to address — the
26
+ // level in force for the run, `minor` by default — is downgraded to
27
+ // `request_changes` HERE, where the verdict is parsed, so the posted body can
28
+ // never begin with `LGTM:` over a finding the loop would have to act on. The
29
+ // gate is keyed on the RAW entries' declared severity, so a finding that
30
+ // fails validation for another reason still poisons the approve. The same
31
+ // ladder is what ship's coordinator holds a posted approve to (agent-ship.md
32
+ // item 9) — that check is now defense in depth behind this one.
26
33
 
27
34
  import { normalizeHead } from "./reviewedHead.js";
28
35
  import { redactSecrets } from "./redact.js";
29
36
 
30
37
  export type ReviewVerdictKind = "approve" | "request_changes";
31
38
 
39
+ /** The severity ladder, most to least severe. One list serves the findings a
40
+ * reviewer submits and the level the loop addresses: a finding's severity is
41
+ * compared to the level in force on this ladder, so nothing outside it can
42
+ * reach a gate — the parser drops an entry with any other severity (`fyi`,
43
+ * none) with a note. */
32
44
  export const FINDING_SEVERITIES = ["blocking", "major", "minor", "nit"] as const;
33
45
  export type FindingSeverity = (typeof FINDING_SEVERITIES)[number];
34
46
 
47
+ /** The severity to address (the loop's gate on a round): an approve carrying a
48
+ * finding at or above this level is not an approve. Resolved once per run —
49
+ * the request's `severity:` directive over the user's scope over the
50
+ * channel's over the org's `review.addressSeverity` — and handed to the
51
+ * verdict parser and ship's coordinator alike. */
52
+ export const ADDRESS_SEVERITIES = FINDING_SEVERITIES;
53
+ export type AddressSeverity = FindingSeverity;
54
+ /** Where the level in force came from, most specific wins: a `severity:`
55
+ * directive on the request (`run`), the user's or the channel's config scope,
56
+ * the org's `review.addressSeverity` (or its default). */
57
+ export type AddressSeveritySource = "org" | "channel" | "user" | "run";
58
+ export const DEFAULT_ADDRESS_SEVERITY: AddressSeverity = "minor";
59
+ export const isAddressSeverity = (v: unknown): v is AddressSeverity =>
60
+ (ADDRESS_SEVERITIES as readonly unknown[]).includes(v);
61
+
62
+ const severityRank = (s: AddressSeverity): number => ADDRESS_SEVERITIES.indexOf(s);
63
+ /** Whether a declared severity sits at or above the level in force on the
64
+ * ladder — the one comparison every gate makes. A value outside the ladder is
65
+ * never at or above anything. */
66
+ export function severityAtOrAbove(severity: unknown, level: AddressSeverity): severity is AddressSeverity {
67
+ return isAddressSeverity(severity) && severityRank(severity) <= severityRank(level);
68
+ }
69
+ /** The findings a gate acts on at `level`: at or above it. */
70
+ export function findingsAtOrAbove(findings: readonly Finding[], level: AddressSeverity): Finding[] {
71
+ return findings.filter((f) => severityAtOrAbove(f.severity, level));
72
+ }
73
+
74
+ /** The level in force and the layer that set it: the request's
75
+ * `severity:` directive wins, then the user's scope, the channel's, the org's
76
+ * `review.addressSeverity` — the default counts as the org's. Resolved once
77
+ * per run by the dispatcher (every review's verdict parser reads it) and once
78
+ * per ship request by the hand-off, which writes it on the instance beside
79
+ * `merge` and hands it to each review child as its `severity:` directive. */
80
+ export function resolveAddressSeverity(layers: {
81
+ org?: AddressSeverity;
82
+ channel?: AddressSeverity;
83
+ user?: AddressSeverity;
84
+ run?: AddressSeverity;
85
+ }): { level: AddressSeverity; source: AddressSeveritySource } {
86
+ if (layers.run !== undefined) return { level: layers.run, source: "run" };
87
+ if (layers.user !== undefined) return { level: layers.user, source: "user" };
88
+ if (layers.channel !== undefined) return { level: layers.channel, source: "channel" };
89
+ return { level: layers.org ?? DEFAULT_ADDRESS_SEVERITY, source: "org" };
90
+ }
91
+
92
+ /** The gate's one sentence — `finding(s) <id> (<severity>)[, …] at or above
93
+ * <level>, the severity to address` — as the downgraded summary carries it into
94
+ * the posted body and the tool's ack repeats it to the model. */
95
+ export function downgradeNote(d: { findings: readonly string[]; level: AddressSeverity }): string {
96
+ return `finding${d.findings.length === 1 ? "" : "s"} ${d.findings.join(", ")} at or above ${d.level}, the severity to address`;
97
+ }
98
+
35
99
  export interface Finding {
36
100
  /** Stable id the reviewer assigned in order ("F1", "F2", …) — dispositions
37
101
  * reference findings by this id. */
@@ -63,50 +127,63 @@ export interface ReviewVerdict {
63
127
  * reasons) — surfaced in the tool ack so the model can resubmit, never
64
128
  * rendered into the posted body. */
65
129
  droppedFindings?: string[];
130
+ /** Parse note: the approve was downgraded by the severity gate — the gated
131
+ * findings as `id (severity)` and the level in force. Surfaced in the tool
132
+ * ack so the model knows its verdict changed; the summary carries the same
133
+ * words into the posted body, so this never rides a record. */
134
+ downgraded?: { findings: string[]; level: AddressSeverity };
66
135
  }
67
136
 
68
137
  export const LGTM_TOKEN = "LGTM:";
69
138
  export const CHANGES_TOKEN = "Changes requested:";
70
139
  export const NO_VERDICT_LINE = "No verdict submitted — not approving.";
71
140
 
72
- /** Parse an arbitrary tool input into a verdict, or null when it is not one. */
73
- export function parseVerdictInput(input: Record<string, unknown>): ReviewVerdict | null {
141
+ /** Parse an arbitrary tool input into a verdict, or null when it is not one.
142
+ * `addressSeverity` is the level in force for the run (default `minor`): an
143
+ * approve carrying a finding at or above it is parsed as `request_changes`. */
144
+ export function parseVerdictInput(
145
+ input: Record<string, unknown>,
146
+ opts: { addressSeverity?: AddressSeverity } = {},
147
+ ): ReviewVerdict | null {
74
148
  const verdict = input.verdict;
75
149
  if (verdict !== "approve" && verdict !== "request_changes") return null;
150
+ const level = opts.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
76
151
  const summary = typeof input.summary === "string" ? oneLine(input.summary) : "";
77
152
  const head = normalizeHead(input.head);
78
153
  const out: ReviewVerdict = { verdict, summary };
79
154
  if (head) out.head = head;
80
155
  if (input.findings !== undefined) {
81
- const parsed = parseFindings(input.findings);
156
+ const parsed = parseFindings(input.findings, level);
82
157
  if (parsed.findings) out.findings = parsed.findings;
83
158
  if (parsed.dropped.length) out.droppedFindings = parsed.dropped;
84
- // Verdict/severity consistency, fail-closed: an approve carrying a
85
- // blocking finding downgrades so the posted body can never start with
86
- // `LGTM:` over a self-declared blocking defect. Keyed on the RAW entries'
87
- // declared severity, not the validated survivors — a blocking entry that
88
- // itself failed validation and dropped still poisons the approve.
89
- if (out.verdict === "approve" && parsed.blocking.length > 0) {
159
+ // The severity gate, fail-closed: an approve carrying a finding at or
160
+ // above the level in force downgrades, so the posted body can never start
161
+ // with `LGTM:` over a finding the loop has to address. Keyed on the RAW
162
+ // entries' declared severity, not the validated survivors — a gated entry
163
+ // that itself failed validation and dropped still poisons the approve.
164
+ if (out.verdict === "approve" && parsed.gated.length > 0) {
90
165
  out.verdict = "request_changes";
91
- out.summary = oneLine(
92
- `${out.summary} [downgraded from approve: blocking finding${parsed.blocking.length === 1 ? "" : "s"} ${parsed.blocking.join(", ")}]`,
93
- );
166
+ out.downgraded = { findings: parsed.gated, level };
167
+ out.summary = oneLine(`${out.summary} [downgraded from approve: ${downgradeNote(out.downgraded)}]`);
94
168
  }
95
169
  }
96
170
  return out;
97
171
  }
98
172
 
99
173
  /** Fail-closed per finding: each malformed entry drops with a note naming its
100
- * index (and id when readable); a non-array drops the whole field.
101
- * `blocking` labels every raw entry that declared severity "blocking" —
102
- * valid or not — for the approve downgrade above. */
103
- function parseFindings(value: unknown): { findings?: Finding[]; dropped: string[]; blocking: string[] } {
174
+ * index (and id when readable); a non-array drops the whole field. `gated`
175
+ * labels every raw entry whose declared severity sits at or above `level` —
176
+ * valid or not — as `id (severity)`, for the approve downgrade above. */
177
+ function parseFindings(
178
+ value: unknown,
179
+ level: AddressSeverity,
180
+ ): { findings?: Finding[]; dropped: string[]; gated: string[] } {
104
181
  if (!Array.isArray(value)) {
105
- return { dropped: ["findings: dropped — not an array"], blocking: [] };
182
+ return { dropped: ["findings: dropped — not an array"], gated: [] };
106
183
  }
107
184
  const findings: Finding[] = [];
108
185
  const dropped: string[] = [];
109
- const blocking: string[] = [];
186
+ const gated: string[] = [];
110
187
  value.forEach((raw, i) => {
111
188
  const parsed = parseFinding(raw);
112
189
  if (parsed.finding) {
@@ -116,9 +193,9 @@ function parseFindings(value: unknown): { findings?: Finding[]; dropped: string[
116
193
  }
117
194
  const label = parsed.finding?.id ?? parsed.id ?? `findings[${i}]`;
118
195
  const r = typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : undefined;
119
- if (r?.severity === "blocking") blocking.push(label);
196
+ if (severityAtOrAbove(r?.severity, level)) gated.push(`${label} (${r?.severity})`);
120
197
  });
121
- return { findings, dropped, blocking };
198
+ return { findings, dropped, gated };
122
199
  }
123
200
 
124
201
  function parseFinding(
@@ -158,6 +158,13 @@ export type RunNoteKind =
158
158
  * after the model's last turn — the release itself runs after the record
159
159
  * is sealed — and set on the card's label too, so the loss is never silent. */
160
160
  | "work_left_behind"
161
+ /** The run's ending may have left a command running in its workspace — a
162
+ * call cut by the ending's abort or interrupt, or open when the run failed —
163
+ * so the release tears the workspace down rather than pair it for the
164
+ * thread's next run (harness.md item 13). The summary names the calls.
165
+ * Written by the run loop once the harness session has ended and before the
166
+ * record is sealed; the release itself runs after. */
167
+ | "workspace_torn_down"
161
168
  /** A coding run submitted a PR description but the post-step opened no
162
169
  * pull request because the branch it observed IS the base the pull
163
170
  * request would target (docs/reference/specs/pr-description.md item 5) —
@@ -196,11 +203,34 @@ export type RunNoteKind =
196
203
  * item 7): the summary names the tool and the rule; the model read the same
197
204
  * reason as the tool's result. Published by the bot's authorize route. */
198
205
  | "tool_refused"
206
+ /** An OpenCode tool settled under a step this loop never saw start and the
207
+ * settle was set aside (harness.md item 13): an earlier execution's late
208
+ * result — the pinned binary's ordinary shape after a hung call's interrupt
209
+ * — or a step lost with the tailer's stream. The summary names the call and
210
+ * the step. Information, not a failure of the harness: nothing ran that the
211
+ * gate did not decide. Published by the OpenCode bridge in its own mode. */
212
+ | "settle_set_aside"
213
+ /** OpenCode's store refill carried a permission over something other than a
214
+ * tool (a directory outside the project, a repeating session) for a call
215
+ * the record never saw, and the store held no part yet to name the call's
216
+ * tool by: the call was opened under the permission's own name. The summary
217
+ * names the call and the step. Information, not a failure: the tool ran
218
+ * under the gate's decision either way. Published by the OpenCode bridge. */
219
+ | "tool_unnamed"
220
+ /** An `external_directory` ask answered once its call's line was already on
221
+ * the record (harness.md item 13): the line cannot be amended, so the
222
+ * directory the call reached is said here, naming the call. Published by
223
+ * the OpenCode bridge. */
224
+ | "directory_reached"
199
225
  /** A ship coding child's budget ended with work still in the tree: the run
200
226
  * loop committed and pushed it to the unit's branch (or says plainly that
201
227
  * there was nothing to push), so a re-issue starts from the partial work
202
228
  * (docs/reference/specs/agent-ship.md item 8). */
203
229
  | "budget_salvage"
230
+ /** The loop's end found a tool call in flight and ended it, so the write-up
231
+ * keeps its allowance (decision 0046, unit seven; harness-pi item 6). The
232
+ * summary names the tools. Published by the harness beside the budget note. */
233
+ | "tool_cut"
204
234
  /** The native loop's stuck-loop guard: the same tool call failed identically
205
235
  * six times in a row and the run was forced into its write-up. Written by
206
236
  * no loop since that loop's deletion; a record from before it may carry it. */
@@ -211,6 +241,7 @@ export type RunNoteKind =
211
241
  export const RUN_NOTE_KINDS = [
212
242
  "wrap_up",
213
243
  "time_budget_exhausted",
244
+ "tool_cut",
214
245
  "turn_budget_exhausted",
215
246
  "sandbox_dead",
216
247
  "fleet_busy",
@@ -229,12 +260,16 @@ export const RUN_NOTE_KINDS = [
229
260
  "cold_sandbox",
230
261
  "rebind_refused",
231
262
  "work_left_behind",
263
+ "workspace_torn_down",
232
264
  "pr_not_opened",
233
265
  "review_not_posted",
234
266
  "compacted",
235
267
  "harness_error",
236
268
  "policy_refusal",
237
269
  "tool_refused",
270
+ "settle_set_aside",
271
+ "tool_unnamed",
272
+ "directory_reached",
238
273
  "budget_salvage",
239
274
  "stuck_loop",
240
275
  ] as const satisfies readonly RunNoteKind[];
@@ -293,7 +328,17 @@ export function sanitizeActor(actor: RunActor): RunActor {
293
328
  * settles a round: caps end the pipeline BETWEEN rounds, visible as the
294
329
  * absence of a next `started` boundary plus the answer's cap report. */
295
330
  export type ShipRoundOutcome =
296
- "started" | "pr_opened" | "completed" | "approve" | "request_changes" | "no_verdict" | "aborted" | "stopped";
331
+ | "started"
332
+ | "pr_opened"
333
+ | "completed"
334
+ | "approve"
335
+ | "request_changes"
336
+ | "no_verdict"
337
+ | "aborted"
338
+ | "stopped"
339
+ /** The coding round ended at its lease with the unit unfinished and the row
340
+ * showing progress, and the grant renewed: the next segment opens (decision 0046). */
341
+ | "continued";
297
342
 
298
343
  /**
299
344
  * One event in a run's stream. `seq` is stamped by `RunRegistry.publish` — a
@@ -393,10 +438,14 @@ export type RouteInputValue = RouteInputLeafOrList | RouteInputObject3;
393
438
 
394
439
  /** How a door decision about a state change ended (docs/decisions/0044-a-routed-write-is-confirmed-in-proportion-to-its-blast-radius.md):
395
440
  * `hand_back` — the router bound a state-changing command and the door answered
396
- * the line to paste, nothing invoked; `pasted` — the typed line that followed
397
- * a hand-back in its thread, with the same receipt, ran. A routed read
398
- * carries no outcome: it is not a decision about a state change. */
399
- export type RouteOutcome = "hand_back" | "pasted";
441
+ * the line to paste, nothing invoked; `offered` — the same decision on a
442
+ * channel that can show a confirmation: the line, its risk and a button, a
443
+ * row stored in the config object, nothing invoked; `confirmed` — the stored
444
+ * input ran at the click, as the requester, through the typed line's path;
445
+ * `pasted` — the typed line that followed a hand-back in its thread, with the
446
+ * same receipt, ran. A routed read carries no outcome: it is not a decision
447
+ * about a state change. */
448
+ export type RouteOutcome = "hand_back" | "offered" | "confirmed" | "pasted";
400
449
 
401
450
  export type RunEvent =
402
451
  /** `callId` is the provider's tool_use id — the explicit pair key between a
@@ -430,7 +479,12 @@ export type RunEvent =
430
479
  * executors' shared `exit N:` prefix, 0 for a clean run; absent when the code
431
480
  * was not numeric). `output` is the tool's text — control-stripped, redacted,
432
481
  * capped at TOOL_OUTPUT_CAP — for the run page's expandable card; the status
433
- * card and the friction analyzer keep reading `summary`. */
482
+ * card and the friction analyzer keep reading `summary`. `cut` marks the
483
+ * result of a call the run's ending cut rather than settled — pi's abort (the
484
+ * tool's own end after it), OpenCode's interrupt (a call still open when its
485
+ * loop left), a session's end (`closeOpenSpans`): the command behind it may
486
+ * still be running, and the workspace's release reads it so (`callsInFlight`,
487
+ * harness.md item 13). */
434
488
  | {
435
489
  type: "tool_result";
436
490
  tool: string;
@@ -440,6 +494,7 @@ export type RunEvent =
440
494
  exitCode?: number;
441
495
  output?: string;
442
496
  infra?: true;
497
+ cut?: true;
443
498
  spanId?: string;
444
499
  logIndex?: number;
445
500
  seq?: number;
@@ -561,6 +616,10 @@ export type RunEvent =
561
616
  * the instance's units. A child's instance rides the record's
562
617
  * `parentInstanceId`, never here. */
563
618
  instanceId?: string;
619
+ /** The grant the ship request carried (decision 0046, the renewable
620
+ * lease): renewals and a cost cap — what a renewal could spend. Written
621
+ * on the pipeline's own record beside `instanceId`; absent elsewhere. */
622
+ grant?: { renewals: number; costCapUsd?: number };
564
623
  seq?: number;
565
624
  at?: number;
566
625
  }
@@ -691,6 +750,12 @@ export type RunEvent =
691
750
  * start of the loop; a resumed run carries the original. Head material,
692
751
  * like `run_meta`. Additive: unknown → ignored. */
693
752
  | { type: "lease"; startedAt: number; endsAt: number; loopEndsAt: number; seq?: number; at?: number }
753
+ /** A head the run pushed (docs/reference/specs/run-history.md item 2; decision
754
+ * 0046): the branch and the sha the coding post-step observed on the remote
755
+ * (`by: "push"`), or the budget-end salvage pushed (`by: "salvage"`),
756
+ * whether or not a pull request follows — the fact renewal reads. Published
757
+ * straight to the registry like `pr_opened`. Additive: unknown → ignored. */
758
+ | { type: "pushed_head"; ref: string; sha: string; by: "push" | "salvage"; seq?: number; at?: number }
694
759
  /** The coordinator tag as a fact of the run (docs/reference/specs/run-history.md
695
760
  * item 48a): the instance the run is a child of, the unit its idempotency
696
761
  * key named, and the base branch its pull request targets — published by
@@ -755,8 +820,11 @@ export type RunEvent =
755
820
  * how the invoke ended is the run's own status and `answer`. A door
756
821
  * decision about a state change (record 0044) is a command run too, with
757
822
  * `outcome` saying which: a hand-back invoked nothing and its `answer` is
758
- * the line to paste; a paste is the typed line that followed, whatever its
759
- * command, `handBackRunId` naming the hand-back's record. */
823
+ * the line to paste; an offer invoked nothing and its `answer` is the offer
824
+ * as the channel shows it; a confirmation is the stored input run at the
825
+ * click, its `answer` the command's own text as a routed read's is; a paste is
826
+ * the typed line that followed a hand-back, whatever its command,
827
+ * `handBackRunId` naming the hand-back's record. */
760
828
  | {
761
829
  type: "route";
762
830
  preset: string;
@@ -426,7 +426,8 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
426
426
  ev.type === "ship_round" ||
427
427
  ev.type === "route" ||
428
428
  ev.type === "reference" ||
429
- ev.type === "lease"
429
+ ev.type === "lease" ||
430
+ ev.type === "pushed_head"
430
431
  ) {
431
432
  sideFactEvents++;
432
433
  return;
@@ -213,6 +213,10 @@ export interface RunRecord {
213
213
  * `lease` event, folded at the assembly. Present only on a run whose
214
214
  * harness started a loop; a record written before the event lacks it. */
215
215
  lease?: RunLease;
216
+ /** The heads the run pushed (item 2; decision 0046): one per branch, the
217
+ * last `pushed_head` event's sha winning — the fact a renewal reads to know
218
+ * the run made progress. Present only on a run that pushed. */
219
+ pushed?: PushedHead[];
216
220
  /** What the run cost in tokens, per model, summed from its `model.turn`
217
221
  * spans at finish (`usageOfEvents`; docs/reference/specs/costs.md, cost by user).
218
222
  * Every record written since carries it (zero turns included); one written
@@ -238,6 +242,21 @@ export interface RunLease {
238
242
  loopEndsAt: number;
239
243
  }
240
244
 
245
+ /** A head a run pushed, as the record names it (item 2): the branch and the sha. */
246
+ export interface PushedHead {
247
+ ref: string;
248
+ sha: string;
249
+ }
250
+
251
+ /** The heads a run's events say it pushed — one per branch, the last event's
252
+ * sha winning, in first-seen order — or nothing when it pushed none. */
253
+ export function pushedHeadsOf(events: readonly RunEvent[]): PushedHead[] | undefined {
254
+ const byRef = new Map<string, string>();
255
+ for (const e of events) if (e.type === "pushed_head") byRef.set(e.ref, e.sha);
256
+ if (byRef.size === 0) return undefined;
257
+ return [...byRef].map(([ref, sha]) => ({ ref, sha }));
258
+ }
259
+
241
260
  /** The lease a run's events say its harness started — the first `lease`
242
261
  * event; a resumed run's later generations publish none — or nothing. */
243
262
  export function leaseOfEvents(events: readonly RunEvent[]): RunLease | undefined {
@@ -272,6 +291,40 @@ export function pushedBranchesOf(events: readonly RunEvent[]): PushedBranch[] {
272
291
  return [...byRef].map(([ref, pr]) => ({ ref, pr }));
273
292
  }
274
293
 
294
+ /** The tool calls a run's ending may have left running in its workspace, in
295
+ * the record's order — what the workspace's release reads to tear the
296
+ * workspace down rather than wait for an idle the command would refuse, and
297
+ * what its note names (harness.md item 13). A call whose result is marked
298
+ * `cut` — pi's abort, OpenCode's interrupt, a session's end — was ended, not
299
+ * settled, whatever the run's status, and stays cut whatever lands for it
300
+ * later — an unmarked result (a post-turn's bridge reads the earlier
301
+ * execution's aborted settle as its own), or the call announced again under
302
+ * the same id; a call with no result at
303
+ * all was left open by a run that failed, was interrupted or hard-stopped,
304
+ * while on a run that completed or stopped softly it is a relayed tool that
305
+ * ran in the bot or a result lost to a transport gap, nothing running. Calls
306
+ * and results pair by id; a result for a call the record never opened counts
307
+ * for nothing. */
308
+ export function callsInFlight(
309
+ events: readonly RunEvent[],
310
+ status: RunStatus,
311
+ ): Extract<RunEvent, { type: "tool_call" }>[] {
312
+ const calls = new Map<
313
+ string,
314
+ { call: Extract<RunEvent, { type: "tool_call" }>; state: "open" | "cut" | "settled" }
315
+ >();
316
+ for (const e of events) {
317
+ if (e.type === "tool_call" && e.callId !== undefined) {
318
+ if (calls.get(e.callId)?.state !== "cut") calls.set(e.callId, { call: e, state: "open" });
319
+ } else if (e.type === "tool_result" && e.callId !== undefined) {
320
+ const seen = calls.get(e.callId);
321
+ if (seen !== undefined && seen.state !== "cut") seen.state = e.cut === true ? "cut" : "settled";
322
+ }
323
+ }
324
+ const orderly = status === "completed" || status === "stopped_soft";
325
+ return [...calls.values()].filter((c) => c.state === "cut" || (c.state === "open" && !orderly)).map((c) => c.call);
326
+ }
327
+
275
328
  /** The router's decision as a record carries it — the same fields the
276
329
  * `route` event and the ledger row's `meta.route` carry (routing-and-config
277
330
  * item 21). */
@@ -798,6 +851,18 @@ export function isRunRecord(v: unknown): v is RunRecord {
798
851
  if (r.profile !== undefined && !isRunProfileRecord(r.profile)) return false;
799
852
  // The pull request the run reached (item 2): a positive integer and a URL, or absent.
800
853
  if (r.pr !== undefined && !isRunPullRequestShape(r.pr)) return false;
854
+ if (
855
+ r.pushed !== undefined &&
856
+ !(
857
+ Array.isArray(r.pushed) &&
858
+ r.pushed.every((h) => {
859
+ if (typeof h !== "object" || h === null) return false;
860
+ const { ref, sha } = h as Record<string, unknown>;
861
+ return typeof ref === "string" && ref.length > 0 && typeof sha === "string" && /^[0-9a-f]{7,40}$/.test(sha);
862
+ })
863
+ )
864
+ )
865
+ return false;
801
866
  // A parent is named by a run id (item 46): the same shape as the record's own.
802
867
  if (r.parentRunId !== undefined && (typeof r.parentRunId !== "string" || !RUN_ID_PATTERN.test(r.parentRunId)))
803
868
  return false;
@@ -431,7 +431,8 @@ function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
431
431
  `Push the branch as soon as the change exists and its cheapest proving checks pass — before the project's ` +
432
432
  `full verification, which runs after that push with any fix as a further commit; an unpushed tree does not ` +
433
433
  `survive the run's end. Right before each push, fetch ${onto} again and rebase once more if it moved while ` +
434
- `you worked, so the pull request is not born conflicting.`
434
+ `you worked, so the pull request is not born conflicting. At the wind-down note, commit and push what compiles, ` +
435
+ `say what does not, then answer.`
435
436
  );
436
437
  }
437
438