@coreplane/switchboard 1.242.0 → 1.244.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/assets/config/config.example.yaml +20 -1
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +234 -29
  3. package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
  4. package/dist/assets/deploy/cloudflare-resident/worker.ts +483 -75
  5. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +13 -2
  6. package/dist/assets/package-lock.json +3 -3
  7. package/dist/assets/package.json +1 -1
  8. package/dist/assets/project.json +3 -3
  9. package/dist/assets/source.json +3 -3
  10. package/dist/assets/src/agents/registry.ts +31 -35
  11. package/dist/assets/src/config/profile.ts +68 -3
  12. package/dist/assets/src/core/authz/policy.ts +4 -0
  13. package/dist/assets/src/core/authz/types.ts +10 -0
  14. package/dist/assets/src/core/budgets.ts +349 -0
  15. package/dist/assets/src/core/coordinator/contract.ts +32 -0
  16. package/dist/assets/src/core/coordinator/driver.ts +75 -22
  17. package/dist/assets/src/core/costs.ts +9 -76
  18. package/dist/assets/src/core/delivery.ts +1 -1
  19. package/dist/assets/src/core/modelPricing.ts +212 -0
  20. package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
  21. package/dist/assets/src/core/reviewVerdict.ts +7 -0
  22. package/dist/assets/src/core/runEvents.ts +72 -5
  23. package/dist/assets/src/core/runFriction.ts +3 -1
  24. package/dist/assets/src/core/runRecord.ts +78 -0
  25. package/dist/assets/src/core/runUsage.ts +158 -47
  26. package/dist/assets/src/core/schedules.ts +3 -0
  27. package/dist/assets/src/core/ship/contract.ts +2 -1
  28. package/dist/assets/src/core/ship/coordinator.ts +312 -97
  29. package/dist/assets/src/core/ship/handoff.ts +9 -0
  30. package/dist/assets/src/core/ship/renewal.ts +155 -0
  31. package/dist/assets/src/core/trace/attrs.ts +11 -0
  32. package/dist/assets/src/core/trace/types.ts +4 -3
  33. package/dist/assets/src/execution/bashTimeout.ts +8 -5
  34. package/dist/assets/src/execution/residentRefresh.ts +52 -7
  35. package/dist/assets/src/execution/residentSteps.ts +1 -1
  36. package/dist/assets/src/execution/residentText.ts +8 -3
  37. package/dist/assets/src/mcp/registry.ts +3 -0
  38. package/dist/assets/web/dist/.vite/manifest.json +432 -429
  39. package/dist/assets/web/dist/assets/{AppShell-IDMGG6yi.js → AppShell-Ck4hSZDi.js} +1 -1
  40. package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +2 -0
  41. package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +1 -0
  42. package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +2 -0
  43. package/dist/assets/web/dist/assets/{NotFoundPage-C97EhJcQ.js → NotFoundPage-CiGrVOyv.js} +1 -1
  44. package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +1 -0
  45. package/dist/assets/web/dist/assets/{ResidentDetailPage-DTMBgnIW.js → ResidentDetailPage-C5USV_8P.js} +1 -1
  46. package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +1 -0
  47. package/dist/assets/web/dist/assets/RunFoldRow-BPMQxTcl.js +1 -0
  48. package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +9 -0
  49. package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +1 -0
  50. package/dist/assets/web/dist/assets/{RunsTabs-BUfdk0lH.js → RunsTabs-BWRHzhtT.js} +1 -1
  51. package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +1 -0
  52. package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +1 -0
  53. package/dist/assets/web/dist/assets/{StatusDot-ELoXHlFt.js → StatusDot-_Q_bmReh.js} +1 -1
  54. package/dist/assets/web/dist/assets/{Tooltip-BoeFwYP2.js → Tooltip-Cafsgd4m.js} +1 -1
  55. package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +1 -0
  56. package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-Dail07rr.js} +1 -1
  57. package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-BReExio8.js} +1 -1
  58. package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-DhNO6Yyx.js} +1 -1
  59. package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-BYotTY4C.js} +1 -1
  60. package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-mUxTtD1X.js} +1 -1
  61. package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DhuiwcSQ.js} +1 -1
  62. package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-GD2vFtRi.js} +1 -1
  63. package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-BhFDwkes.js} +1 -1
  64. package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-DWUgbE0r.js} +1 -1
  65. package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-CO9XFjGW.js} +1 -1
  66. package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-B9pVurpn.js} +1 -1
  67. package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-CFVVV6lI.js} +1 -1
  68. package/dist/assets/web/dist/assets/{dist-BU5UivXC.js → dist-BtPs2AkQ.js} +2 -2
  69. package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-BDhIDQIb.js} +1 -1
  70. package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-DG3J8WoD.js} +1 -1
  71. package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-FiOdiaOE.js} +1 -1
  72. package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-vxbtW503.js} +1 -1
  73. package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
  74. package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DINz8M3H.js} +1 -1
  75. package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-yann6r9i.js} +1 -1
  76. package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-CXTbrZn_.js} +1 -1
  77. package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DNYC-hq0.js} +1 -1
  78. package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CwzhHzi9.js} +1 -1
  79. package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BBXF1cwR.js} +1 -1
  80. package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-oEqBcsTy.js} +1 -1
  81. package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-YE5thkT1.js} +1 -1
  82. package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-Dp4jRQ1j.js} +1 -1
  83. package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-DCT1GYtQ.js} +1 -1
  84. package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-BPcGbmzJ.js} +1 -1
  85. package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-CQnNHwyd.js} +1 -1
  86. package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +1 -0
  87. package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-Dm8Ovj6W.js} +1 -1
  88. package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-Btm9oJK9.js} +1 -1
  89. package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-VZYjWF_U.js} +1 -1
  90. package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-Dcmjxa9x.js} +1 -1
  91. package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-DoWiOGZ8.js} +1 -1
  92. package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-Dvmo55ma.js} +1 -1
  93. package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-d8AtfV26.js} +1 -1
  94. package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-Cc5EStA-.js} +1 -1
  95. package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-B5fKKIH7.js} +1 -1
  96. package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-DUYI9JND.js} +1 -1
  97. package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-BhMwKWpw.js} +1 -1
  98. package/dist/assets/web/dist/assets/main-C4366PZF.css +1 -0
  99. package/dist/assets/web/dist/assets/main-DlTNAC6c.js +28 -0
  100. package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-Cnf2cPAi.js} +1 -1
  101. package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-FDDXoir9.js} +1 -1
  102. package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-q-8U_1jR.js} +1 -1
  103. package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-DIA0TUWs.js} +1 -1
  104. package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-CR88hy7S.js} +1 -1
  105. package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-Bkd8N7ja.js} +1 -1
  106. package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-BOs84mvy.js} +1 -1
  107. package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-BbpZBb3w.js} +1 -1
  108. package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml-D2jQpjFE.js} +1 -1
  109. package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-DKzcAQ22.js} +1 -1
  110. package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-SOQMWcC9.js} +1 -1
  111. package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-B6KK4W8d.js} +1 -1
  112. package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-kPSg-NPf.js} +1 -1
  113. package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-cKS-JYLH.js} +1 -1
  114. package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DNxzE5gs.js} +1 -1
  115. package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DGcHQ5OO.js} +1 -1
  116. package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CRZeZBa5.js} +1 -1
  117. package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DSNEL0HX.js} +1 -1
  118. package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-zisNbkDO.js} +1 -1
  119. package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-MG06KMMb.js} +1 -1
  120. package/dist/assets/web/dist/assets/{sseReplay-g7ml86LM.js → sseReplay-DaRtc7nI.js} +5 -5
  121. package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CB_wKwv9.js} +1 -1
  122. package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-DYdSt7re.js} +1 -1
  123. package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-DHO-F7c7.js} +1 -1
  124. package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CUL5L4Lr.js} +1 -1
  125. package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-_JNEKjVW.js} +1 -1
  126. package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags--9EpEV_8.js} +1 -1
  127. package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-BcNq2x2u.js} +1 -1
  128. package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-EsY5Dk_D.js} +1 -1
  129. package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-CyA_HiUH.js} +1 -1
  130. package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-SzaHlDt4.js} +1 -1
  131. package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-D3cn5acV.js} +1 -1
  132. package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-6MxhUxOa.js} +1 -1
  133. package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-qJnqhgo9.js} +1 -1
  134. package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-DyRpC7xv.js} +1 -1
  135. package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-BXMRPAsg.js} +1 -1
  136. package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-BNmPCtAN.js} +1 -1
  137. package/dist/cli.js +5843 -2246
  138. package/package.json +1 -1
  139. package/dist/assets/web/dist/assets/CostsPage-CtmKhhOF.js +0 -2
  140. package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
  141. package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +0 -2
  142. package/dist/assets/web/dist/assets/PendingTurnRow-BZA_vQt3.js +0 -1
  143. package/dist/assets/web/dist/assets/ResidentsIndexPage-CaDvXhzJ.js +0 -1
  144. package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +0 -1
  145. package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +0 -6
  146. package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +0 -1
  147. package/dist/assets/web/dist/assets/ScheduledPage-DXD2gLJk.js +0 -1
  148. package/dist/assets/web/dist/assets/SettingsPage-BIGio8Y0.js +0 -1
  149. package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.js +0 -1
  150. package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
  151. package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
  152. package/dist/assets/web/dist/assets/main-B2fX10aW.css +0 -1
  153. package/dist/assets/web/dist/assets/main-D4EA1g6n.js +0 -28
@@ -588,9 +588,20 @@ export class SwitchboardSandbox extends Sandbox<Env> {
588
588
  await Promise.race([settled, new Promise<void>((r) => setTimeout(r, maxMs))]);
589
589
  }
590
590
 
591
- /** `du -sk` over the paths, summed, in KiB; null when nothing could be measured. */
591
+ /** `du -sk` over the paths, summed, in KiB; null when nothing could be
592
+ * measured — including a `du` that did not answer inside its own limit
593
+ * (a mount mid-restore under load): the judge treats a sample the
594
+ * container could not take as no evidence either way, never as the
595
+ * restore's failure. The restore's own promise still reports a runtime
596
+ * that went away. */
592
597
  private async duKiB(paths: string[]): Promise<number | null> {
593
- const r = await this.runRoot(["du", "-sk", ...paths], 30_000);
598
+ let r: { stdout: string; exitCode: number };
599
+ try {
600
+ r = await this.runRoot(["du", "-sk", ...paths], 30_000);
601
+ } catch {
602
+ return null;
603
+ }
604
+ if (r.exitCode === 124) return null;
594
605
  let total = 0;
595
606
  let seen = false;
596
607
  for (const line of r.stdout.split("\n")) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.242.0",
3
+ "version": "1.244.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.242.0",
9
+ "version": "1.244.0",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -20445,7 +20445,7 @@
20445
20445
  },
20446
20446
  "packages/switchboard": {
20447
20447
  "name": "@coreplane/switchboard",
20448
- "version": "1.242.0",
20448
+ "version": "1.244.0",
20449
20449
  "license": "Apache-2.0",
20450
20450
  "dependencies": {
20451
20451
  "@earendil-works/pi-ai": "0.85.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.242.0",
3
+ "version": "1.244.0",
4
4
  "private": true,
5
5
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
6
6
  "license": "Apache-2.0",
@@ -168,11 +168,11 @@
168
168
  "when": "Part of `check:consistency`."
169
169
  },
170
170
  "clock:gen": {
171
- "does": "Regenerates the clock-read allowlist (`src/core/trace/clockAllowlist.json`) from the tree — empty since the ratchet reached zero; a result that is not `{}` names a new direct read.",
171
+ "does": "Regenerates both clock allowlists from the tree: wall-clock reads (empty) and duration literals outside `src/core/budgets.ts`.",
172
172
  "when": "Part of `fix`."
173
173
  },
174
174
  "clock:check": {
175
- "does": "No production file reads the wall clock directly: the allowlist is empty and the tree agrees.",
175
+ "does": "No production file reads the wall clock directly or gained a duration literal outside `src/core/budgets.ts`; both allowlists match the tree.",
176
176
  "when": "Part of `check:consistency`."
177
177
  },
178
178
  "docs:changed": {
@@ -212,7 +212,7 @@
212
212
  "when": "Part of `check:consistency`."
213
213
  },
214
214
  "load": {
215
- "does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider|pi|route`.",
215
+ "does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider|pi|route|door`.",
216
216
  "when": "Capacity receipts (docs/reference/specs/load-harness.md)."
217
217
  }
218
218
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.242.0",
3
- "commit": "c0dc26a1590c854d944c451829e9b21f6ce7d721",
4
- "builtAt": "2026-09-17T05:42:17.412Z"
2
+ "version": "1.244.0",
3
+ "commit": "de7fddf53659568639ad877e935a8b9c0177debd",
4
+ "builtAt": "2026-09-17T15:03:14.350Z"
5
5
  }
@@ -1,6 +1,7 @@
1
1
  // Agent definitions. An agent is a system prompt + toolset + machine class + wall-clock budget.
2
2
  import type { Effort } from "../effort.js";
3
3
  import { BASH_TIMEOUT_MAX_MS } from "../execution/bashTimeout.js";
4
+ import { ASKS, RUNAWAY_TURNS_PER_MINUTE, runawayTurnCap, type LoopPreset } from "../core/budgets.js";
4
5
  import { CONTRACT_HEADING, CONTRACT_SECTION_HEADINGS, PR_TITLE_GUARD } from "../core/ship/contract.js";
5
6
  // Which model runs it is resolved separately by the config layers, so any
6
7
  // agent can run on any configured provider/model.
@@ -41,24 +42,16 @@ export function machineNeedsRepo(machine: MachineClass): boolean {
41
42
  export const IDENTITIES = ["none", "read", "write"] as const;
42
43
  export type Identity = (typeof IDENTITIES)[number];
43
44
 
44
- /** The pace that marks a run as looping rather than working: a model turn
45
- * every ten seconds, sustained for the whole wall clock. A busy run takes
46
- * 20–40 s a turn (a model think plus a tool call), so a run that averages six
47
- * a minute from start to end is re-issuing calls, not making progress — and
48
- * its turn cap ends it before the wall clock would, with a write-up that
49
- * says so (docs/reference/specs/harness-pi.md item 15). */
50
- export const RUNAWAY_TURNS_PER_MINUTE = 6;
51
-
52
- /** The turn cap a wall clock implies: `maxMinutes × RUNAWAY_TURNS_PER_MINUTE`.
53
- * Every preset that runs the loop derives its `maxTurns` from this, so the
54
- * cap is never a number a good run reaches — the minutes are the budget. */
55
- export function runawayTurnCap(maxMinutes: number): number {
56
- return maxMinutes * RUNAWAY_TURNS_PER_MINUTE;
57
- }
45
+ /** The wall clocks live in `src/core/budgets.ts` (docs/decisions/0046): a
46
+ * preset's ask, the turn cap derived from it and every allowance are rows
47
+ * there, and this registry reads them. Re-exported for the readers that
48
+ * learned them here. */
49
+ export { RUNAWAY_TURNS_PER_MINUTE, runawayTurnCap };
58
50
 
59
- /** A loop-running preset's budget as one fact: the wall clock, and the runaway
60
- * guard derived from it. */
61
- function loopBudget(maxMinutes: number): Pick<AgentDef, "maxMinutes" | "maxTurns"> {
51
+ /** A loop-running preset's budget as one fact read from the module: the wall
52
+ * clock it asks for, and the runaway guard derived from it. */
53
+ function loopBudget(preset: LoopPreset): Pick<AgentDef, "maxMinutes" | "maxTurns"> {
54
+ const maxMinutes = ASKS[preset];
62
55
  return { maxMinutes, maxTurns: runawayTurnCap(maxMinutes) };
63
56
  }
64
57
 
@@ -150,15 +143,18 @@ export function statusCardRule(examples = '"Implement the fix", "Run the test su
150
143
  );
151
144
  }
152
145
 
153
- const PR_DESCRIPTION_TEMPLATE = `PR description — submit it with the submit_pr_description tool for EVERY PR (this is the default, not something to wait to be asked for). Switchboard renders the GitHub body from the object you submit, so never author PR-body markdown yourself. Before submitting, judge your title with the ${PR_TITLE_GUARD} gate — \`npm run check:pr-title -- "<title>"\` — and submit only a title it accepts; the same gate refuses the PR in CI. Content contract per field (each renders as its own section): prose is unwrapped — no hard line breaks inside a paragraph. Always hyperlink the triggering issue/request. Never fabricate validation — state exactly what you ran and the real result. Keep each field concise, not padded.
154
- EVERY PR includes one that already exists when you push — opened by a person, by dependabot, or by an earlier run. After EVERY push to such a PR: read its current title and body (\`github_issue_get\` with the PR number works for pull requests; \`gh pr view\` where gh exists), judge them against the change as it now stands at the pushed head, and submit the object that describes the PR as it is NOW — carry forward what the existing body says that is still true (a dependency bump's release notes belong in whatWhy), add what you changed, and anchor the Tour at the new head. Switchboard replaces the PR's title and body with your rendering. A description that describes an earlier state of its branch is a bug; "it is someone else's PR" is never a reason to leave it.
146
+ const PR_DESCRIPTION_TEMPLATE = `PR description — submit it with the submit_pr_description tool for EVERY PR (this is the default, not something to wait to be asked for). Switchboard renders the GitHub body from the object you submit, so never author PR-body markdown yourself. Before submitting, judge your title with the ${PR_TITLE_GUARD} gate — \`npm run check:pr-title -- "<title>"\` — and submit only a title it accepts; the same gate refuses the PR in CI. The body is a fixed-size MAP for the reader with everything for agents collapsed under it; every field is capped in visible characters (a link's URL is not counted) and the tool refuses an object over a cap naming the field and the count — cut and resubmit. Prose is unwrapped — no hard line breaks inside a paragraph. Always hyperlink the triggering issue/request. Never fabricate validation — state exactly what you ran and the real result. BEFORE authoring the pointers, load the \`pr-description\` skill with use_skill — it defines how to choose at most seven pointers, the mechanical anchor rules and what goes below the fold; follow it for every PR.
147
+ EVERY PR includes one that already exists when you push — opened by a person, by dependabot, or by an earlier run. After EVERY push to such a PR: read its current title and body (\`github_issue_get\` with the PR number works for pull requests; \`gh pr view\` where gh exists), judge them against the change as it now stands at the pushed head, and submit the object that describes the PR as it is NOW — carry forward what the existing body says that is still true (a dependency bump's release notes belong in why), add what you changed, and anchor the pointers at the new head. Switchboard replaces the PR's title and body with your rendering. A description that describes an earlier state of its branch is a bug; "it is someone else's PR" is never a reason to leave it.
155
148
  - **title**: the PR title — one line naming the change, specific enough to pick out of a PR list.
156
- - **TL;DR** (\`tldr\`, rendered first): two sentences for a naive reader with zero context — what this PR does and why it matters.
157
- - **What & why** (\`whatWhy\`): the change and its motivation, linked to the triggering issue/request.
158
- - **Tour** (\`tour\` + \`remaining\`): the guided walkthrough of the change, replacing any prose list of changes — ordered steps of { title, description, optional lookFor, anchor }, each anchor a { path, from, to } line range at your pushed head; every touched file no step covers goes in \`remaining\` as { path, note }. BEFORE authoring the Tour steps, load the \`pr-tour\` skill with use_skill — it defines the reader-first step shape, the anchor rules, and the Remaining-changes catch-all. Follow it for every PR; if a later push changes what the steps point at, resubmit the description with corrected anchors.
159
- - **Decisions** (\`decisions\`): non-obvious choices as { title, rationale } — alternatives considered and rejected, trade-offs.
160
- - **Risks & implications** (\`risks\`): what could break, the blast radius, and any migration/rollout/compatibility concerns (or "none" — and why).
161
- - **Validation** (\`validation\`): what you tested and the actual results as { criterion, proof } rows (commands run, pass/fail), plus how the reviewer can verify it themselves; the optional summary line carries the overall result.`;
149
+ - **TL;DR** (\`tldr\`, rendered first, ≤300): two sentences for a reader with zero context — what this PR does and why it matters.
150
+ - **Why** (\`why\`, ≤400): the problem and the motivation, with the triggering issue/request, the record and the stack position hyperlinked. Why, never what: the diff shows what.
151
+ - **Where to look** (\`pointers\`, 1 to 7): the files a reviewer would open first, in reading order, each { label ≤60, text ≤160, optional risk ≤100, anchor } with the anchor a { path, from, to } line range at your pushed head, rendered as a link (never embedded code). One pointer per idea, never per file; when the change has more ideas than seven, keep the seven whose mistake would cost most.
152
+ - **Feedback wanted** (\`feedbackWanted\`, ≤200): the one or two things you want the reviewer's judgement on.
153
+ - **Risk** (\`risk\`, ≤300): what breaks if this is wrong, the blast radius, the rollback; over 400 changed lines, say so and name the split you considered.
154
+ - **Verified** (\`verified\`, ≤200): one line for a person — which suites ran and passed, what is still human-gated.
155
+ - **Decisions** (\`decisions\`, 0 to 10, collapsed): non-obvious choices as { title, rationale ≤400 } — the alternative rejected and the fact that decided it.
156
+ - **Validation** (\`validation\`, 1 to 30 criteria, collapsed): what you tested and the actual results as { criterion ≤200, proof ≤300 } rows (test ids, commands run, pass/fail), plus how the reviewer can verify it.
157
+ - **For agents** (\`agentNotes\`, optional, ≤2000, collapsed): what a reviewing agent needs that a person does not — the rebase you did, generated files to skip, the command that reproduces the bug.`;
162
158
 
163
159
  // Both coding prompts carry this verbatim: coding runs hold a write-scoped
164
160
  // token where a merge is one command away, so the boundary is spelled out the
@@ -229,7 +225,7 @@ Keep notes with the \`notes\` tool: one short document, replaced whole each time
229
225
  // project's most expensive checks first; the rule is the runner's to hold, not
230
226
  // a line every requester remembers to paste. Stack-agnostic on purpose — the
231
227
  // classes are by duration, the project's own scripts and CI say which is which.
232
- export const CHECKS_BY_COST = `CHECKS BY COST — push before the expensive ones. Every check you might run has a cost class: seconds (a formatter or a linter on the files you touched, one test file, a docs, link or spec check, the typecheck of one package) or minutes (the whole test suite, a build, a dependency install, an end-to-end or full verification). Know a command's class before you run it — from the project's own scripts and CI configuration, from how long it took last time, or by the class above when you have nothing better. Prove each change with the cheapest check that can prove it, matched to the change's scope: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. As soon as the change exists and those checks pass, commit and push — the pushed branch is the deliverable, and an unpushed tree does not survive the run's end. Only then run the expensive checks, once, and fix forward with further commits and pushes. Never start an operation whose expected duration does not fit the time you have left minus what a commit, a push and the description need: push what there is and say plainly what is unverified instead. The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
228
+ export const CHECKS_BY_COST = `CHECKS BY COST — push before the expensive ones. Every check you might run has a cost class: seconds (a formatter or a linter on the files you touched, one test file, a docs, link or spec check, the typecheck of one package) or minutes (the whole test suite, a build, a dependency install, an end-to-end or full verification). Know a command's class before you run it — from the project's own scripts and CI configuration, from how long it took last time, or by the class above when you have nothing better. Prove each change with the cheapest check that can prove it, matched to the change's scope: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. As soon as the change exists and those checks pass, commit and push — the pushed branch is the deliverable, and an unpushed tree does not survive the run's end. Only then run the expensive checks, once, and fix forward with further commits and pushes. Never start an operation whose expected duration does not fit the time you have left minus what a commit, a push and the description need: push what there is and say plainly what is unverified instead. At the wind-down note, commit and push what compiles, say what does not, then answer. The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
233
229
 
234
230
  const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
235
231
 
@@ -521,7 +517,7 @@ Your tools work without a workspace: the GitHub tools — \`github_repos\` (the
521
517
 
522
518
  ${statusCardRule('"Read the issue and its thread", "Post the comment"')} A one-step answer needs no checklist; post one when the request has steps the person would wait on.
523
519
 
524
- You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web. Other Switchboard agents can: for code changes or PRs tell the user to re-send with \`agent:coding\`; for a PR review, \`agent:review\`; for a web-research question, \`agent:research\` (e.g. "\`agent:coding fix the failing login test in acme/api\`", "\`agent:research compare X and Y\`"). Delete an issue only when the user explicitly asked to delete it (closing is an update).`;
520
+ You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web. Other Switchboard agents can: for a code change or a pull request tell the user to re-send with \`agent:ship\` (it makes the change, opens the PR and loops review); for a PR review, \`agent:review\`; for a web-research question, \`agent:research\` (e.g. "\`agent:ship in acme/api: fix the failing login test\`", "\`agent:research compare X and Y\`"). Delete an issue only when the user explicitly asked to delete it (closing is an update).`;
525
521
 
526
522
  // The explore agent (docs/reference/specs/agent-explore.md): a long, read-only
527
523
  // investigation — "run our CI locally and validate the claims", "how long does
@@ -544,7 +540,7 @@ THE DELIVERABLE IS A CLAIM TABLE. Turn the request into the claims it makes or a
544
540
 
545
541
  TIME. Your budget is up to two hours — less when a boundary or the request's \`budget:\` directive clipped it, which the runtime-config block above says — and the wrap-up warning tells you when to stop starting new checks. A single command is capped at ${BASH_TIMEOUT_MAX_MS / 60_000} minutes (pass the bash tool's \`timeoutMs\`, up to ${BASH_TIMEOUT_MAX_MS} ms, for a long one). A job that needs longer — a full suite, a build, a pipeline run — is started detached and polled across tool calls: \`setsid -f sh -c '<command> > /tmp/job.log 2>&1; echo $? > /tmp/job.exit'\`, then \`tail -n 40 /tmp/job.log\` and \`cat /tmp/job.exit\` on later calls (a plain background job dies with the command that started it; a \`setsid -f\` job outlives it). Batch commands into few tool calls; never explore file by file.
546
542
 
547
- READ-ONLY: NEVER open a pull request, and never commit or push — no branch, no \`gh pr create\`, no PR or issue write of any kind. You hold a read credential and your job is to find out, not to change. If the investigation shows a change is needed, say exactly what and where in your write-up and point the user at \`agent:coding\`.
543
+ READ-ONLY: NEVER open a pull request, and never commit or push — no branch, no \`gh pr create\`, no PR or issue write of any kind. You hold a read credential and your job is to find out, not to change. If the investigation shows a change is needed, say exactly what and where in your write-up and point the user at \`agent:ship\` (it makes the change, opens the PR and loops review).
548
544
 
549
545
  You cannot attach or post files: your whole answer is text. Never say a file is attached or below — name its path in the workspace and describe it (what it shows, its size) instead; a person who needs the file itself asks \`agent:coding\`, which can attach.
550
546
 
@@ -638,7 +634,7 @@ const WORK_PRESETS = {
638
634
  machine: "none",
639
635
  identity: "none",
640
636
  maxTokens: 16000,
641
- ...loopBudget(5),
637
+ ...loopBudget("general"),
642
638
  },
643
639
  coding: {
644
640
  name: "coding",
@@ -648,7 +644,7 @@ const WORK_PRESETS = {
648
644
  seededSystem: CODING_SYSTEM_SEEDED,
649
645
  toolset: "full",
650
646
  maxTokens: 64000,
651
- ...loopBudget(45),
647
+ ...loopBudget("coding"),
652
648
  // No built-in effort: the deployment decides (`defaults.efforts.coding`,
653
649
  // `config set channel efforts.coding=…`, or `effort:` per request).
654
650
  machine: "repo-resident",
@@ -669,7 +665,7 @@ const WORK_PRESETS = {
669
665
  machine: "repo-resident",
670
666
  identity: "read", // a read-scoped token and a read-only worktree: it cannot post or push from inside
671
667
  maxTokens: 64000,
672
- ...loopBudget(25), // a safety net — typical reviews land in ~5 minutes
668
+ ...loopBudget("review"), // a safety net — typical reviews land in ~5 minutes
673
669
  effort: "medium", // fast turns; one big-context pass does the deep work
674
670
  },
675
671
  ship: {
@@ -698,7 +694,7 @@ const WORK_PRESETS = {
698
694
  // reviewed pull request, never code landing on main.
699
695
  maxTurns: 1,
700
696
  maxTokens: 16000,
701
- maxMinutes: 120,
697
+ maxMinutes: ASKS.ship,
702
698
  },
703
699
  research: {
704
700
  name: "research",
@@ -709,7 +705,7 @@ const WORK_PRESETS = {
709
705
  machine: "none", // web I/O only; no workspace is provisioned
710
706
  identity: "none",
711
707
  maxTokens: 24000,
712
- ...loopBudget(8),
708
+ ...loopBudget("research"),
713
709
  effort: "medium",
714
710
  },
715
711
  explore: {
@@ -723,7 +719,7 @@ const WORK_PRESETS = {
723
719
  machine: "repo-cold",
724
720
  identity: "read", // a read-scoped token: it can clone and read, never push — whatever the caller holds
725
721
  maxTokens: 64000,
726
- ...loopBudget(120),
722
+ ...loopBudget("explore"),
727
723
  // No built-in effort: the deployment decides, as for coding.
728
724
  },
729
725
  } satisfies Record<string, AgentDef>;
@@ -748,7 +744,7 @@ export const AGENTS: Record<string, AgentDef> = {
748
744
  // spawn exactly those — so no child runs that the record did not name.
749
745
  routable: false,
750
746
  maxTokens: 32000,
751
- ...loopBudget(120), // long enough to outlast a coding child; every child is capped by what remains of it
747
+ ...loopBudget("conductor"), // long enough to outlast a coding child; every child is capped by what remains of it
752
748
  // No built-in effort: the deployment decides, as for coding.
753
749
  },
754
750
  };
@@ -73,10 +73,40 @@ export function budgetedAgent(agent: AgentDef, profile: RunProfile): AgentDef {
73
73
 
74
74
  // ---- boundaries: a scope caps, never grants -----------------------------------
75
75
 
76
+ /** The blast-radius classes a scope may name as its `confirm`
77
+ * (docs/decisions/0044-a-routed-write-is-confirmed-in-proportion-to-its-blast-radius.md):
78
+ * the first class on the ladder the door hands back instead of running.
79
+ * `exec` is on the ladder for the comparison but not settable — a test or
80
+ * build never asks — and `never` is refused until the door's write misbind
81
+ * rate has been measured; the validator names both reasons. */
82
+ export type ConfirmClass = "write" | "destructive";
83
+ export const CONFIRM_CLASSES: readonly ConfirmClass[] = ["write", "destructive"];
84
+
85
+ /** The classes on the door's ladder — the command registry's `BlastRadius`,
86
+ * spelled here rather than imported: a type import still drags the registry's
87
+ * whole graph into every program that compiles this near-leaf module, the
88
+ * Workers included. The door indexes the ladder with the registry's type
89
+ * (`routedRunsAtOnce`), so a class added there without a rung here fails to
90
+ * compile at the one place the two vocabularies meet. */
91
+ export type ConfirmLadderClass = "read" | "exec" | "write" | "destructive";
92
+
93
+ /** The ladder the door compares on, `read < exec < write < destructive`: among
94
+ * the last three, from asking most to asking least — a `confirm` of `write`
95
+ * hands back every write and every destructive write, `destructive` only the
96
+ * destructive ones. A read is on the order so the comparison is total, but the
97
+ * door never asks for one. */
98
+ export const CONFIRM_ORDER: Record<ConfirmLadderClass, number> = { read: 0, exec: 1, write: 2, destructive: 3 };
99
+
100
+ /** The door's confirm class when no scope sets one: every routed write is
101
+ * handed back, a read or a test run at once — the door as it was before the
102
+ * axis existed. Attributed to `built-in`, a word that appears in no config. */
103
+ export const BUILT_IN_CONFIRM: ConfirmClass = "write";
104
+
76
105
  /** A cap on the three axes that any scope may set (`defaults`, `channels.<id>`,
77
- * `users.<id>`; docs/reference/specs/routing-and-config.md item 2). An absent
78
- * axis caps nothing. A boundary never grants: it is not a fourth grants axis,
79
- * and the policy table's one question (who may run a preset) is unchanged. */
106
+ * `users.<id>`; docs/reference/specs/routing-and-config.md item 2), and the
107
+ * door's `confirm` beside them. An absent axis caps nothing. A boundary never
108
+ * grants: it is not a fourth grants axis, and the policy table's one question
109
+ * (who may run a preset) is unchanged. */
80
110
  export interface Boundary {
81
111
  /** The most a run may have, in minutes; at least 2 (the bash tool keeps a 60 s reserve). */
82
112
  maxMinutes?: number;
@@ -84,6 +114,11 @@ export interface Boundary {
84
114
  maxIdentity?: Identity;
85
115
  /** The machine classes a run may execute on; a preset's class must be in every layer's set. */
86
116
  machines?: MachineClass[];
117
+ /** The first blast-radius class a command the router bound is handed back
118
+ * at instead of run (record 0044). Not a run cap: it rides the boundary for
119
+ * its scopes and its intersection-toward-caution, is read by the door alone
120
+ * (`effectiveConfirm`), and never enters `intersectBoundaries` or a profile. */
121
+ confirm?: ConfirmClass;
87
122
  }
88
123
 
89
124
  /** One layer's boundary with the scope it came from, in resolution order:
@@ -143,6 +178,36 @@ export function intersectBoundaries(layers: readonly ScopedBoundary[]): Effectiv
143
178
  return out.maxMinutes || out.maxIdentity || out.machines ? out : undefined;
144
179
  }
145
180
 
181
+ /** Where the door's confirm class came from: a scope that set it, or the
182
+ * built-in default when none did. Local to the confirm axis — `BoundaryScope`
183
+ * itself is unchanged, since no run cap is ever attributed to `built-in`. */
184
+ export type ConfirmScope = BoundaryScope | "built-in";
185
+
186
+ /** The door's decision on the confirm axis: the class and the scope it names. */
187
+ export interface EffectiveConfirm {
188
+ value: ConfirmClass;
189
+ scope: ConfirmScope;
190
+ }
191
+
192
+ /**
193
+ * The confirm axis intersected over a request's path, apart from the run caps:
194
+ * the earliest class on `CONFIRM_ORDER` any layer named — the most cautious,
195
+ * since a scope's value is the most permissive answer it allows and the org's
196
+ * is therefore a floor no layer below it can loosen — attributed to the layer
197
+ * that set it (on a tie the first layer named keeps it, the least specific
198
+ * scope, as `intersectBoundaries` does). No layer set one: the built-in
199
+ * `write`, attributed to `built-in`. Pure over the same layers `intersectBoundaries` takes.
200
+ */
201
+ export function effectiveConfirm(layers: readonly ScopedBoundary[]): EffectiveConfirm {
202
+ let out: EffectiveConfirm | undefined;
203
+ for (const { scope, boundary } of layers) {
204
+ if (boundary.confirm !== undefined && (!out || CONFIRM_ORDER[boundary.confirm] < CONFIRM_ORDER[out.value])) {
205
+ out = { value: boundary.confirm, scope };
206
+ }
207
+ }
208
+ return out ?? { value: BUILT_IN_CONFIRM, scope: "built-in" };
209
+ }
210
+
146
211
  /**
147
212
  * The boundaries on a child's path with its parent's remaining wall clock as
148
213
  * one more layer (docs/reference/specs/routing-and-config.md item 20): the
@@ -82,6 +82,10 @@ export const POLICY: readonly Rule[] = [
82
82
  { action: "friction:write", resource: "command", when: [grant("friction:write")] },
83
83
 
84
84
  // ── costs ────────────────────────────────────────────────────────────────
85
+ // `costs by` reads the snapshot's arithmetic: what every browser session
86
+ // holds (every group's read) and what a Slack user or a token is granted —
87
+ // never a chat baseline, since a by-user table names who spent what.
88
+ { action: "costs:read", resource: "command", when: [grant("costs:read")] },
85
89
  // `costs snapshot` reads both billing providers and replaces what every
86
90
  // viewer of the costs page sees: the grant, never a baseline (the admins'
87
91
  // `all` and a named `grants` entry hold it).
@@ -196,4 +196,14 @@ export interface ChannelDirectory {
196
196
  /** Every channel the actor is in, platform-namespaced — what the resolver puts on
197
197
  * `Actor.memberOf`; `unknown` when the adapter cannot say (fail-closed). */
198
198
  channelsOf(actorId: string): Promise<ReadonlySet<string> | "unknown">;
199
+ /** Every channel the bot itself is in, with its visibility — the channels a person can
200
+ * pick a scope or an MCP tier for (settings-page.md item 7); `unknown` when the adapter
201
+ * cannot list them, and the surfaces fall back to the channels that already carry a scope. */
202
+ channels(): Promise<readonly ListedChannel[] | "unknown">;
203
+ }
204
+
205
+ /** One channel the bot is in, as the directory lists it. */
206
+ export interface ListedChannel {
207
+ id: string;
208
+ visibility: ChannelVisibility;
199
209
  }