@coreplane/switchboard 1.244.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 (145) 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-resident/threadErr.ts +337 -0
  4. package/dist/assets/deploy/cloudflare-resident/tsconfig.json +1 -1
  5. package/dist/assets/deploy/cloudflare-resident/worker.ts +31 -262
  6. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +5 -10
  7. package/dist/assets/deploy/secrets.manifest.json +12 -0
  8. package/dist/assets/package-lock.json +3 -3
  9. package/dist/assets/package.json +1 -1
  10. package/dist/assets/source.json +3 -3
  11. package/dist/assets/src/agents/registry.ts +4 -4
  12. package/dist/assets/src/config/profile.ts +17 -1
  13. package/dist/assets/src/core/authz/types.ts +8 -0
  14. package/dist/assets/src/core/budgets.ts +25 -0
  15. package/dist/assets/src/core/reviewVerdict.ts +101 -24
  16. package/dist/assets/src/core/runEvents.ts +26 -1
  17. package/dist/assets/src/core/runRecord.ts +34 -0
  18. package/dist/assets/src/core/ship/coordinator.ts +25 -21
  19. package/dist/assets/src/execution/residentRefresh.ts +25 -6
  20. package/dist/assets/web/dist/.vite/manifest.json +481 -462
  21. package/dist/assets/web/dist/assets/AppShell-qqs_p96-.js +1 -0
  22. package/dist/assets/web/dist/assets/CostsPage-Dd9aXxxI.js +2 -0
  23. package/dist/assets/web/dist/assets/DeliveryPage-D90FTjYq.js +1 -0
  24. package/dist/assets/web/dist/assets/HomePage-VLTFopnl.js +2 -0
  25. package/dist/assets/web/dist/assets/InputMenu-lQP3ia9D.js +1 -0
  26. package/dist/assets/web/dist/assets/NotFoundPage-8ViskpN3.js +1 -0
  27. package/dist/assets/web/dist/assets/PendingTurnRow-D2gHef5v.js +1 -0
  28. package/dist/assets/web/dist/assets/ResidentDetailPage-nWLN8BOd.js +1 -0
  29. package/dist/assets/web/dist/assets/ResidentsIndexPage-D3Q48CJW.js +1 -0
  30. package/dist/assets/web/dist/assets/{RunFoldRow-BPMQxTcl.js → RunFoldRow-CHy_IfZG.js} +1 -1
  31. package/dist/assets/web/dist/assets/RunRoutePage-C6kX5om4.js +9 -0
  32. package/dist/assets/web/dist/assets/RunsIndexPage-DjxGj1Q5.js +1 -0
  33. package/dist/assets/web/dist/assets/RunsTabs-WyeG5tKb.js +1 -0
  34. package/dist/assets/web/dist/assets/ScheduledPage-B8D5AU4d.js +1 -0
  35. package/dist/assets/web/dist/assets/SettingSelect-UTHsqUUS.js +1 -0
  36. package/dist/assets/web/dist/assets/SettingsPage-BoW7Zjzt.js +1 -0
  37. package/dist/assets/web/dist/assets/StatusDot-BGHureCJ.js +1 -0
  38. package/dist/assets/web/dist/assets/Tooltip-CiP8qPd7.js +1 -0
  39. package/dist/assets/web/dist/assets/UnitRoutePage-2zEeRBP1.js +1 -0
  40. package/dist/assets/web/dist/assets/{angular-html-Dail07rr.js → angular-html-qXhb9ti8.js} +1 -1
  41. package/dist/assets/web/dist/assets/{angular-ts-BReExio8.js → angular-ts-DzvdyoUZ.js} +1 -1
  42. package/dist/assets/web/dist/assets/{apl-DhNO6Yyx.js → apl-Ck7Ru2Sm.js} +1 -1
  43. package/dist/assets/web/dist/assets/{astro-BYotTY4C.js → astro-CwehvReb.js} +1 -1
  44. package/dist/assets/web/dist/assets/{blade-mUxTtD1X.js → blade-WSyVbSFK.js} +1 -1
  45. package/dist/assets/web/dist/assets/{c-DhuiwcSQ.js → c-DMHI1wDz.js} +1 -1
  46. package/dist/assets/web/dist/assets/{chapel-GD2vFtRi.js → chapel-BF8AYK8_.js} +1 -1
  47. package/dist/assets/web/dist/assets/{cobol-BhFDwkes.js → cobol-UAfImuWu.js} +1 -1
  48. package/dist/assets/web/dist/assets/{coffee-DWUgbE0r.js → coffee-D54hi96d.js} +1 -1
  49. package/dist/assets/web/dist/assets/{cpp-CO9XFjGW.js → cpp-BRWfLgBc.js} +1 -1
  50. package/dist/assets/web/dist/assets/{crystal-B9pVurpn.js → crystal-CSqC5deT.js} +1 -1
  51. package/dist/assets/web/dist/assets/{css-CFVVV6lI.js → css-D4ZuRFGk.js} +1 -1
  52. package/dist/assets/web/dist/assets/{dist-BtPs2AkQ.js → dist-73ZMn_Mg.js} +2 -2
  53. package/dist/assets/web/dist/assets/{durationTone-BobbycC-.js → durationTone-D51enaGR.js} +1 -1
  54. package/dist/assets/web/dist/assets/{edge-BDhIDQIb.js → edge-DDPtNbZc.js} +1 -1
  55. package/dist/assets/web/dist/assets/{elixir-DG3J8WoD.js → elixir-BTQkQfsJ.js} +1 -1
  56. package/dist/assets/web/dist/assets/{elm-FiOdiaOE.js → elm-B_PkLzGZ.js} +1 -1
  57. package/dist/assets/web/dist/assets/{erb-vxbtW503.js → erb-DfjIMVM_.js} +1 -1
  58. package/dist/assets/web/dist/assets/{eventSource-tRprhqm7.js → eventSource-By0B_w4S.js} +1 -1
  59. package/dist/assets/web/dist/assets/{git-rebase-DINz8M3H.js → git-rebase-DP8wmn7K.js} +1 -1
  60. package/dist/assets/web/dist/assets/{glimmer-js-yann6r9i.js → glimmer-js-G12VK2gK.js} +1 -1
  61. package/dist/assets/web/dist/assets/{glimmer-ts-CXTbrZn_.js → glimmer-ts-D3XiSI8E.js} +1 -1
  62. package/dist/assets/web/dist/assets/{glsl-DNYC-hq0.js → glsl-DOKAhMj-.js} +1 -1
  63. package/dist/assets/web/dist/assets/{graphql-CwzhHzi9.js → graphql-CJB_fmON.js} +1 -1
  64. package/dist/assets/web/dist/assets/{hack-BBXF1cwR.js → hack-BHqtRWap.js} +1 -1
  65. package/dist/assets/web/dist/assets/{haml-oEqBcsTy.js → haml-DwotyarS.js} +1 -1
  66. package/dist/assets/web/dist/assets/{handlebars-YE5thkT1.js → handlebars-B6fDaA3U.js} +1 -1
  67. package/dist/assets/web/dist/assets/{html-derivative-DCT1GYtQ.js → html-derivative-C5bpQmmI.js} +1 -1
  68. package/dist/assets/web/dist/assets/{html-Dp4jRQ1j.js → html-s52taj36.js} +1 -1
  69. package/dist/assets/web/dist/assets/{http-BPcGbmzJ.js → http-99zC935r.js} +1 -1
  70. package/dist/assets/web/dist/assets/{hurl-CQnNHwyd.js → hurl-Bgk-4-0x.js} +1 -1
  71. package/dist/assets/web/dist/assets/{java-Dm8Ovj6W.js → java-CuV_c5I0.js} +1 -1
  72. package/dist/assets/web/dist/assets/{javascript-Btm9oJK9.js → javascript-CZlOvSMQ.js} +1 -1
  73. package/dist/assets/web/dist/assets/{jinja-VZYjWF_U.js → jinja-BuXeZ4Tu.js} +1 -1
  74. package/dist/assets/web/dist/assets/{jison-Dcmjxa9x.js → jison-DIuP0hkG.js} +1 -1
  75. package/dist/assets/web/dist/assets/{json-DoWiOGZ8.js → json-CZ74vfoD.js} +1 -1
  76. package/dist/assets/web/dist/assets/{jsx-Dvmo55ma.js → jsx-BNjRs8Vj.js} +1 -1
  77. package/dist/assets/web/dist/assets/{julia-d8AtfV26.js → julia-OCE-fFOg.js} +1 -1
  78. package/dist/assets/web/dist/assets/{just-Cc5EStA-.js → just-CtvD87WF.js} +1 -1
  79. package/dist/assets/web/dist/assets/{latex-B5fKKIH7.js → latex-C3kf86OR.js} +1 -1
  80. package/dist/assets/web/dist/assets/{liquid-DUYI9JND.js → liquid-B1WI--Kp.js} +1 -1
  81. package/dist/assets/web/dist/assets/{lua-BhMwKWpw.js → lua-DtoT0gKg.js} +1 -1
  82. package/dist/assets/web/dist/assets/main-ClluP-Qf.js +28 -0
  83. package/dist/assets/web/dist/assets/main-CpkwzHX6.css +1 -0
  84. package/dist/assets/web/dist/assets/{marko-Cnf2cPAi.js → marko-BE2MC6Yt.js} +1 -1
  85. package/dist/assets/web/dist/assets/{mdc-FDDXoir9.js → mdc-BwO4weXJ.js} +1 -1
  86. package/dist/assets/web/dist/assets/{nginx-q-8U_1jR.js → nginx-BTuOgGVC.js} +1 -1
  87. package/dist/assets/web/dist/assets/{nim-DIA0TUWs.js → nim-CiIUM-oV.js} +1 -1
  88. package/dist/assets/web/dist/assets/{org-CR88hy7S.js → org-L78DEWB-.js} +1 -1
  89. package/dist/assets/web/dist/assets/{perl-Bkd8N7ja.js → perl-KqHOm4Ft.js} +1 -1
  90. package/dist/assets/web/dist/assets/{php-BOs84mvy.js → php-C41XbEkn.js} +1 -1
  91. package/dist/assets/web/dist/assets/{pug-BbpZBb3w.js → pug-B12-4i-k.js} +1 -1
  92. package/dist/assets/web/dist/assets/{qml-D2jQpjFE.js → qml--fw2Up5c.js} +1 -1
  93. package/dist/assets/web/dist/assets/{r-DKzcAQ22.js → r-Db1JBXrC.js} +1 -1
  94. package/dist/assets/web/dist/assets/{razor-SOQMWcC9.js → razor-BYpVBgbu.js} +1 -1
  95. package/dist/assets/web/dist/assets/{regexp-B6KK4W8d.js → regexp-BMqGLSPc.js} +1 -1
  96. package/dist/assets/web/dist/assets/{rst-kPSg-NPf.js → rst-B63n7-Sa.js} +1 -1
  97. package/dist/assets/web/dist/assets/{ruby-cKS-JYLH.js → ruby-D3VI8UYv.js} +1 -1
  98. package/dist/assets/web/dist/assets/{sas-DNxzE5gs.js → sas-DxRf8ZAR.js} +1 -1
  99. package/dist/assets/web/dist/assets/{scss-DGcHQ5OO.js → scss-DRRA6d6A.js} +1 -1
  100. package/dist/assets/web/dist/assets/seed-DFfT5S_N.js +6 -0
  101. package/dist/assets/web/dist/assets/{settingsApi-priOPiwu.js → settingsApi-Dm6eaQcu.js} +1 -1
  102. package/dist/assets/web/dist/assets/{shellscript-CRZeZBa5.js → shellscript-CoOBe3_y.js} +1 -1
  103. package/dist/assets/web/dist/assets/{shellsession-DSNEL0HX.js → shellsession-DOV1HYxf.js} +1 -1
  104. package/dist/assets/web/dist/assets/{soy-zisNbkDO.js → soy-Acpmpp1W.js} +1 -1
  105. package/dist/assets/web/dist/assets/{sql-MG06KMMb.js → sql-DeT9N-fA.js} +1 -1
  106. package/dist/assets/web/dist/assets/{sseReplay-DaRtc7nI.js → sseReplay-DPXVxhL5.js} +3 -3
  107. package/dist/assets/web/dist/assets/{stata-CB_wKwv9.js → stata-CbxXAMO4.js} +1 -1
  108. package/dist/assets/web/dist/assets/{surrealql-DYdSt7re.js → surrealql-BdKbi4hc.js} +1 -1
  109. package/dist/assets/web/dist/assets/{svelte-DHO-F7c7.js → svelte-tAcjDM7R.js} +1 -1
  110. package/dist/assets/web/dist/assets/{templ-CUL5L4Lr.js → templ-CObINNJj.js} +1 -1
  111. package/dist/assets/web/dist/assets/{tex-_JNEKjVW.js → tex-BYwpZT1J.js} +1 -1
  112. package/dist/assets/web/dist/assets/{ts-tags--9EpEV_8.js → ts-tags-BciREL5R.js} +1 -1
  113. package/dist/assets/web/dist/assets/{tsx-BcNq2x2u.js → tsx-CRiv3Pr3.js} +1 -1
  114. package/dist/assets/web/dist/assets/{twig-EsY5Dk_D.js → twig-DuZZ1Ej0.js} +1 -1
  115. package/dist/assets/web/dist/assets/{typescript-CyA_HiUH.js → typescript-jkHz_5HZ.js} +1 -1
  116. package/dist/assets/web/dist/assets/{typst-SzaHlDt4.js → typst-DOac4Gal.js} +1 -1
  117. package/dist/assets/web/dist/assets/{vue-D3cn5acV.js → vue-CgOX5wXA.js} +1 -1
  118. package/dist/assets/web/dist/assets/{vue-html-6MxhUxOa.js → vue-html-Duu99VNw.js} +1 -1
  119. package/dist/assets/web/dist/assets/{vue-vine-qJnqhgo9.js → vue-vine-Bmj_wvRJ.js} +1 -1
  120. package/dist/assets/web/dist/assets/wallClock-DD_fPuSY.js +1 -0
  121. package/dist/assets/web/dist/assets/{xml-DyRpC7xv.js → xml-BtUIVeWH.js} +1 -1
  122. package/dist/assets/web/dist/assets/{xsl-BXMRPAsg.js → xsl-wX71n1E5.js} +1 -1
  123. package/dist/assets/web/dist/assets/{yaml-BNmPCtAN.js → yaml-CdLdXvmz.js} +1 -1
  124. package/dist/cli.js +1474 -684
  125. package/package.json +1 -1
  126. package/dist/assets/web/dist/assets/AppShell-Ck4hSZDi.js +0 -1
  127. package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +0 -2
  128. package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +0 -1
  129. package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +0 -2
  130. package/dist/assets/web/dist/assets/NotFoundPage-CiGrVOyv.js +0 -1
  131. package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +0 -1
  132. package/dist/assets/web/dist/assets/ResidentDetailPage-C5USV_8P.js +0 -1
  133. package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +0 -1
  134. package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +0 -9
  135. package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +0 -1
  136. package/dist/assets/web/dist/assets/RunsTabs-BWRHzhtT.js +0 -1
  137. package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +0 -1
  138. package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +0 -1
  139. package/dist/assets/web/dist/assets/StatusDot-_Q_bmReh.js +0 -1
  140. package/dist/assets/web/dist/assets/Tooltip-Cafsgd4m.js +0 -1
  141. package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +0 -1
  142. package/dist/assets/web/dist/assets/main-C4366PZF.css +0 -1
  143. package/dist/assets/web/dist/assets/main-DlTNAC6c.js +0 -28
  144. package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +0 -6
  145. 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) —
@@ -210,11 +217,20 @@ export type RunNoteKind =
210
217
  * names the call and the step. Information, not a failure: the tool ran
211
218
  * under the gate's decision either way. Published by the OpenCode bridge. */
212
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"
213
225
  /** A ship coding child's budget ended with work still in the tree: the run
214
226
  * loop committed and pushed it to the unit's branch (or says plainly that
215
227
  * there was nothing to push), so a re-issue starts from the partial work
216
228
  * (docs/reference/specs/agent-ship.md item 8). */
217
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"
218
234
  /** The native loop's stuck-loop guard: the same tool call failed identically
219
235
  * six times in a row and the run was forced into its write-up. Written by
220
236
  * no loop since that loop's deletion; a record from before it may carry it. */
@@ -225,6 +241,7 @@ export type RunNoteKind =
225
241
  export const RUN_NOTE_KINDS = [
226
242
  "wrap_up",
227
243
  "time_budget_exhausted",
244
+ "tool_cut",
228
245
  "turn_budget_exhausted",
229
246
  "sandbox_dead",
230
247
  "fleet_busy",
@@ -243,6 +260,7 @@ export const RUN_NOTE_KINDS = [
243
260
  "cold_sandbox",
244
261
  "rebind_refused",
245
262
  "work_left_behind",
263
+ "workspace_torn_down",
246
264
  "pr_not_opened",
247
265
  "review_not_posted",
248
266
  "compacted",
@@ -251,6 +269,7 @@ export const RUN_NOTE_KINDS = [
251
269
  "tool_refused",
252
270
  "settle_set_aside",
253
271
  "tool_unnamed",
272
+ "directory_reached",
254
273
  "budget_salvage",
255
274
  "stuck_loop",
256
275
  ] as const satisfies readonly RunNoteKind[];
@@ -460,7 +479,12 @@ export type RunEvent =
460
479
  * executors' shared `exit N:` prefix, 0 for a clean run; absent when the code
461
480
  * was not numeric). `output` is the tool's text — control-stripped, redacted,
462
481
  * capped at TOOL_OUTPUT_CAP — for the run page's expandable card; the status
463
- * 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). */
464
488
  | {
465
489
  type: "tool_result";
466
490
  tool: string;
@@ -470,6 +494,7 @@ export type RunEvent =
470
494
  exitCode?: number;
471
495
  output?: string;
472
496
  infra?: true;
497
+ cut?: true;
473
498
  spanId?: string;
474
499
  logIndex?: number;
475
500
  seq?: number;
@@ -291,6 +291,40 @@ export function pushedBranchesOf(events: readonly RunEvent[]): PushedBranch[] {
291
291
  return [...byRef].map(([ref, pr]) => ({ ref, pr }));
292
292
  }
293
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
+
294
328
  /** The router's decision as a record carries it — the same fields the
295
329
  * `route` event and the ledger row's `meta.route` carry (routing-and-config
296
330
  * item 21). */
@@ -34,7 +34,18 @@ import type { Handoff } from "./handoff.js";
34
34
  import { progressOf, renderRenewal, renewalDecision, type PushedHeadFact, type RenewalDecision } from "./renewal.js";
35
35
  import type { RunStatus } from "../runRecord.js";
36
36
  import { normalizeHead, sameCommit } from "../reviewedHead.js";
37
- 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";
38
49
  import { parsePlanUnit, planUnitIds } from "./contract.js";
39
50
  import {
40
51
  carve,
@@ -598,26 +609,19 @@ export type CoordinatorNote =
598
609
  * person can see whether the cap or the child is the problem. */
599
610
  export type ShipBudgetSpent = Readonly<Record<"coding" | "review" | "waiting", number>>;
600
611
 
601
- /** The severity ship must address before an approve stands (agent-ship item
602
- * 9's gate): an approve carrying a finding at or above this level continues
603
- * into the findings step exactly as a request_changes does. Ordered most to
604
- * least severe. */
605
- export const ADDRESS_SEVERITIES = ["blocking", "major", "minor", "nit"] as const;
606
- export type AddressSeverity = (typeof ADDRESS_SEVERITIES)[number];
607
- /** 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). */
608
- export type AddressSeveritySource = "org" | "channel" | "user" | "run";
609
- export const DEFAULT_ADDRESS_SEVERITY: AddressSeverity = "minor";
610
- export const isAddressSeverity = (v: unknown): v is AddressSeverity =>
611
- (ADDRESS_SEVERITIES as readonly unknown[]).includes(v);
612
-
613
- const severityRank = (s: AddressSeverity): number => ADDRESS_SEVERITIES.indexOf(s);
614
- /** The findings the gate acts on at `level`: at or above it. Every parsed
615
- * finding carries one of the ladder's four levels — the verdict parser
616
- * (`parseVerdictInput`) drops an entry with any other severity, `fyi` or none,
617
- * with a note — so nothing outside the ladder ever reaches the gate. */
618
- export function findingsAtOrAbove(findings: readonly Finding[], level: AddressSeverity): Finding[] {
619
- return findings.filter((f) => severityRank(f.severity) <= severityRank(level));
620
- }
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
+ };
621
625
 
622
626
  export interface UnitSession {
623
627
  segment: number;
@@ -241,14 +241,17 @@ export function fetchFailureIsMirrors(message: string): boolean {
241
241
  export const RUNTIME_MOVED_WORDING =
242
242
  /previous runtime incarnation|interrupted because the runtime changed|runtime identity is no longer active|sandbox lifetime is no longer current|platform was updating the sandbox runtime|no longer identifies pid/i;
243
243
  /** The platform's words for a container that is down under a command (see
244
- * `RUNTIME_MOVED_WORDING`): the spawn refusal of a stopped container and the
245
- * binding's not-running refusal, anchored to the full phrase so a genuine
244
+ * `RUNTIME_MOVED_WORDING`): the spawn refusal of a stopped container, the
245
+ * binding's not-running refusal, and the SDK's "The container just exited"
246
+ * for a container that died under a live exec (a roll landing mid-step; the
247
+ * bot side reads the same words as a roll, residentWake's
248
+ * CONTAINER_GONE_WORDING), each anchored to the full phrase so a genuine
246
249
  * crash ("container exited with unexpected exit code") and the readiness
247
250
  * probe ("the container is not listening") never match. The harness's
248
251
  * container seam reads the same two as a container down under its one more
249
252
  * command — a wait, never a judgement (harness.md item 6). */
250
253
  export const STOPPED_CONTAINER_WORDING =
251
- /process supervisor is closed|container is not running, consider calling start/i;
254
+ /process supervisor is closed|container is not running, consider calling start|the container just exited/i;
252
255
  export const RUNTIME_REPLACEMENT_WORDING = new RegExp(
253
256
  `${RUNTIME_MOVED_WORDING.source}|${STOPPED_CONTAINER_WORDING.source}`,
254
257
  "i",
@@ -358,12 +361,28 @@ export const SDK_RUNTIME_RECORD_KEY = "currentRuntimeIdentity";
358
361
  /** The DOMException the SDK's connect abort raises — `fetchUpgradeAttempt`
359
362
  * calls `controller.abort()` with no reason when `DEFAULT_CONNECT_TIMEOUT_MS`
360
363
  * passes, so the runtime's own `AbortError` with the message exactly `The
361
- * operation was aborted`. Anchored on the WHOLE message, so a command's own
362
- * `Aborted (core dumped)` never matches and neither does a `TimeoutError`'s
364
+ * operation was aborted`. Anchored on the WHOLE message: a command's own
365
+ * `Aborted (core dumped)` never matches, and neither does a `TimeoutError`'s
363
366
  * `The operation was aborted due to timeout` — the text `AbortSignal.timeout`
364
- * raises, a route's own bound on a slow GitHub, not a silent runtime. */
367
+ * raises, a route's own bound on a slow GitHub, not a silent runtime. A
368
+ * wrapper is read by its `cause`, where the DOMException rides whole; a
369
+ * wrapper that only pasted the sentence into a longer message is not the
370
+ * signal. */
365
371
  export const RUNTIME_UNREACHABLE_WORDING = /^The operation was aborted\.?$/;
366
372
 
373
+ /** `err` and its `cause` chain, bounded like the SDK's own `selfAndCauses`
374
+ * walker: the SDK wraps platform errors, so the telling message can sit one or
375
+ * two links down. The one walker for every reader of a throw's chain — the
376
+ * resident Worker's classifiers and its thread-error builders, the sandbox
377
+ * Worker's — so the depth bound and the shape are decided once. */
378
+ export function* selfAndCauses(err: unknown): Generator<unknown> {
379
+ let current = err;
380
+ for (let depth = 0; depth < 8 && current != null; depth++) {
381
+ yield current;
382
+ current = typeof current === "object" ? (current as { cause?: unknown }).cause : undefined;
383
+ }
384
+ }
385
+
367
386
  /** Is this one link of an error's cause chain the SDK's connect abort? By
368
387
  * name first (the DOMException's `AbortError`), by the DOMException's message
369
388
  * when a wrapper copied only that. Applied by the Worker to the SPAWN-phase