@coreplane/switchboard 1.246.0 → 1.248.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 (144) hide show
  1. package/dist/assets/config/config.example.yaml +1 -1
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +14 -4
  3. package/dist/assets/deploy/cloudflare-resident/worker.ts +1 -1
  4. package/dist/assets/package-lock.json +3 -3
  5. package/dist/assets/package.json +5 -3
  6. package/dist/assets/project.json +10 -2
  7. package/dist/assets/source.json +3 -3
  8. package/dist/assets/src/agents/registry.ts +8 -3
  9. package/dist/assets/src/core/budgets.ts +19 -7
  10. package/dist/assets/src/core/runEvents.ts +5 -1
  11. package/dist/assets/src/core/ship/contract.ts +18 -1
  12. package/dist/assets/src/core/ship/coordinator.ts +1 -1
  13. package/dist/assets/src/core/trace/attrs.ts +9 -0
  14. package/dist/assets/web/dist/.vite/manifest.json +474 -485
  15. package/dist/assets/web/dist/assets/AppShell-DAtHiuI6.js +1 -0
  16. package/dist/assets/web/dist/assets/CostsPage-BoKzYa4B.js +2 -0
  17. package/dist/assets/web/dist/assets/DeliveryPage-NP4g6bQd.js +1 -0
  18. package/dist/assets/web/dist/assets/HomePage-DYxC0izY.js +2 -0
  19. package/dist/assets/web/dist/assets/InputMenu-C6aPpe30.js +1 -0
  20. package/dist/assets/web/dist/assets/NotFoundPage-DtE-GgTk.js +1 -0
  21. package/dist/assets/web/dist/assets/PendingTurnRow-BuRre8it.js +1 -0
  22. package/dist/assets/web/dist/assets/ResidentDetailPage-DLIpWYOc.js +1 -0
  23. package/dist/assets/web/dist/assets/ResidentsIndexPage-6LipuDjR.js +1 -0
  24. package/dist/assets/web/dist/assets/RunFoldRow-V-iSy64e.js +1 -0
  25. package/dist/assets/web/dist/assets/RunRoutePage-DUalB1u2.js +9 -0
  26. package/dist/assets/web/dist/assets/RunsIndexPage-B9Ba1KdD.js +1 -0
  27. package/dist/assets/web/dist/assets/RunsTabs-YSbUu5py.js +1 -0
  28. package/dist/assets/web/dist/assets/ScheduledPage-KdjLtD_7.js +1 -0
  29. package/dist/assets/web/dist/assets/SettingSelect-BIzAsLk1.js +1 -0
  30. package/dist/assets/web/dist/assets/SettingsPage-IT5l_NaL.js +1 -0
  31. package/dist/assets/web/dist/assets/StatusDot-DBHAl4Il.js +1 -0
  32. package/dist/assets/web/dist/assets/Tooltip-_LEjptLV.js +1 -0
  33. package/dist/assets/web/dist/assets/UnitRoutePage-DPBsvGPR.js +1 -0
  34. package/dist/assets/web/dist/assets/{angular-html-D_3slZqf.js → angular-html-BjeQZdCq.js} +1 -1
  35. package/dist/assets/web/dist/assets/{angular-ts-D1Fv2cpN.js → angular-ts-ZGdORrL2.js} +1 -1
  36. package/dist/assets/web/dist/assets/{apl-60Nm_trN.js → apl-DhvV_X93.js} +1 -1
  37. package/dist/assets/web/dist/assets/{astro-CvyaM3Q9.js → astro-DiRSE4Ug.js} +1 -1
  38. package/dist/assets/web/dist/assets/{blade-yan3VBls.js → blade-D8Fr5Wvq.js} +1 -1
  39. package/dist/assets/web/dist/assets/{seed-BhxEVasY.js → browser-B6T5hjfR.js} +1 -1
  40. package/dist/assets/web/dist/assets/{c-Ddgepap2.js → c-BwbC64D4.js} +1 -1
  41. package/dist/assets/web/dist/assets/{chapel-CgNY6dxC.js → chapel-DgmqRK97.js} +1 -1
  42. package/dist/assets/web/dist/assets/{cobol-C7q82cdP.js → cobol-DJod2RNK.js} +1 -1
  43. package/dist/assets/web/dist/assets/{coffee-DgRMF_ni.js → coffee-D1Y2CgBY.js} +1 -1
  44. package/dist/assets/web/dist/assets/{cpp-B8Lf-aeo.js → cpp-CsO97YOM.js} +1 -1
  45. package/dist/assets/web/dist/assets/{crystal-CkA_dhEO.js → crystal-CJ7m2tOc.js} +1 -1
  46. package/dist/assets/web/dist/assets/{css-Cyu-C_oA.js → css-B2M-NKoj.js} +1 -1
  47. package/dist/assets/web/dist/assets/{dist-DS-gMjLZ.js → dist-BcYPGOBL.js} +2 -2
  48. package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +1 -0
  49. package/dist/assets/web/dist/assets/{edge-BxV3rKNI.js → edge-CFVW-m9B.js} +1 -1
  50. package/dist/assets/web/dist/assets/{elixir-pNSEK39d.js → elixir-B2mtRgCr.js} +1 -1
  51. package/dist/assets/web/dist/assets/{elm-zgwWE33W.js → elm-2teVAwCi.js} +1 -1
  52. package/dist/assets/web/dist/assets/{erb-yzcYAHp_.js → erb-LAbeNqen.js} +1 -1
  53. package/dist/assets/web/dist/assets/eventSource-Bj_dZf6M.js +1 -0
  54. package/dist/assets/web/dist/assets/{git-rebase-Dg7Ous1-.js → git-rebase-DnhzH_sp.js} +1 -1
  55. package/dist/assets/web/dist/assets/{glimmer-js-CbZEwX27.js → glimmer-js-DPKjXf1-.js} +1 -1
  56. package/dist/assets/web/dist/assets/{glimmer-ts-D3NIqALy.js → glimmer-ts-CABXnt5z.js} +1 -1
  57. package/dist/assets/web/dist/assets/{glsl-y4-3WrI2.js → glsl-Dz4-3-gJ.js} +1 -1
  58. package/dist/assets/web/dist/assets/{graphql-D5d0al7K.js → graphql-ByWUD1DL.js} +1 -1
  59. package/dist/assets/web/dist/assets/{hack-D8fD6kEl.js → hack-CdmOK2K-.js} +1 -1
  60. package/dist/assets/web/dist/assets/{haml-BWeFFc-V.js → haml-DzegMpd2.js} +1 -1
  61. package/dist/assets/web/dist/assets/{handlebars-gIm5FeE_.js → handlebars-B5NeiP0e.js} +1 -1
  62. package/dist/assets/web/dist/assets/{html-fXJTVLSM.js → html-Zg_NHLv9.js} +1 -1
  63. package/dist/assets/web/dist/assets/{html-derivative-B3ySSBrQ.js → html-derivative-DZf9JNzh.js} +1 -1
  64. package/dist/assets/web/dist/assets/{http-DrJfQUmw.js → http-CVFrQZeN.js} +1 -1
  65. package/dist/assets/web/dist/assets/{hurl-Bvbnsf55.js → hurl-L5brWAJe.js} +1 -1
  66. package/dist/assets/web/dist/assets/{java-CUSpb-i-.js → java-hFphyZ4R.js} +1 -1
  67. package/dist/assets/web/dist/assets/{javascript-BWRH5fbY.js → javascript-CAJO1WKX.js} +1 -1
  68. package/dist/assets/web/dist/assets/{jinja-D3Ymci8y.js → jinja-N4A4srXI.js} +1 -1
  69. package/dist/assets/web/dist/assets/{jison-Gt9SO7ZC.js → jison-D3BZa_iU.js} +1 -1
  70. package/dist/assets/web/dist/assets/{json-Cfcblhgn.js → json-DxD1Qh8W.js} +1 -1
  71. package/dist/assets/web/dist/assets/{jsx-C0366fFM.js → jsx-CHhag69S.js} +1 -1
  72. package/dist/assets/web/dist/assets/{julia-CyBOTbxS.js → julia-CrhkZ6Tt.js} +1 -1
  73. package/dist/assets/web/dist/assets/{just-FPXZcIOd.js → just-D0LFOuhq.js} +1 -1
  74. package/dist/assets/web/dist/assets/{latex-Bd-c88v0.js → latex-BHAlwCXK.js} +1 -1
  75. package/dist/assets/web/dist/assets/{liquid-5tu9Hc0k.js → liquid-CkaLfCWh.js} +1 -1
  76. package/dist/assets/web/dist/assets/{lua-BZ5fdPP8.js → lua-B1h4aAzP.js} +1 -1
  77. package/dist/assets/web/dist/assets/main-DUfSE0dj.js +28 -0
  78. package/dist/assets/web/dist/assets/main-zbP_dTjR.css +1 -0
  79. package/dist/assets/web/dist/assets/{marko-C_mmj4p0.js → marko-D6CG4hvX.js} +1 -1
  80. package/dist/assets/web/dist/assets/{mdc-CtaNqWjY.js → mdc-CVQzimAU.js} +1 -1
  81. package/dist/assets/web/dist/assets/{nginx-Ddm5Mb8J.js → nginx-h-9Ir54h.js} +1 -1
  82. package/dist/assets/web/dist/assets/{nim-COhfroYB.js → nim-DHVI30sp.js} +1 -1
  83. package/dist/assets/web/dist/assets/{org-CCHFgmTb.js → org-AasAXfMF.js} +1 -1
  84. package/dist/assets/web/dist/assets/{perl-CXzOE-Z-.js → perl-BVi5aTEy.js} +1 -1
  85. package/dist/assets/web/dist/assets/{php-1V6jAIhU.js → php-qOkCeUKr.js} +1 -1
  86. package/dist/assets/web/dist/assets/{pug-uS37efts.js → pug-8TnUGvEX.js} +1 -1
  87. package/dist/assets/web/dist/assets/{qml-Bj1A0Ben.js → qml-CpJbhoxn.js} +1 -1
  88. package/dist/assets/web/dist/assets/{r-CMbKbLsS.js → r-JkUNcAJW.js} +1 -1
  89. package/dist/assets/web/dist/assets/{razor-DVRq_Ndk.js → razor-CtU4ws5a.js} +1 -1
  90. package/dist/assets/web/dist/assets/{regexp-AbaOr4oB.js → regexp-J2zA6ceN.js} +1 -1
  91. package/dist/assets/web/dist/assets/{rst-Dq_QWgOF.js → rst-CMCvf-yZ.js} +1 -1
  92. package/dist/assets/web/dist/assets/{ruby-CVSsAj7T.js → ruby-ChLBY_iR.js} +1 -1
  93. package/dist/assets/web/dist/assets/{sas-C-dGfxUi.js → sas-cwn4x2VY.js} +1 -1
  94. package/dist/assets/web/dist/assets/{scss-DxALxYPA.js → scss-jSrJlzLI.js} +1 -1
  95. package/dist/assets/web/dist/assets/{shellscript--qXcvQ5L.js → shellscript-D5nAd0gs.js} +1 -1
  96. package/dist/assets/web/dist/assets/{shellsession-DeqnW8Ux.js → shellsession-BZY4-Tdh.js} +1 -1
  97. package/dist/assets/web/dist/assets/{soy-DreoUIMt.js → soy-COppPOKr.js} +1 -1
  98. package/dist/assets/web/dist/assets/{sql-o3i2VTG6.js → sql-Blc_6dOA.js} +1 -1
  99. package/dist/assets/web/dist/assets/{sseReplay-qF-gpbAY.js → sseReplay-DXC7kGbN.js} +3 -3
  100. package/dist/assets/web/dist/assets/{stata-CGAT41H7.js → stata-DRv3OY-h.js} +1 -1
  101. package/dist/assets/web/dist/assets/{surrealql-CBU0QuGV.js → surrealql-qMv_eeJ6.js} +1 -1
  102. package/dist/assets/web/dist/assets/{svelte-CfHaLZiG.js → svelte-d1Edb6Rz.js} +1 -1
  103. package/dist/assets/web/dist/assets/{templ-D02MUcma.js → templ-CUMSIOXG.js} +1 -1
  104. package/dist/assets/web/dist/assets/{tex-DjprHYXl.js → tex-BGZQA-Ei.js} +1 -1
  105. package/dist/assets/web/dist/assets/{ts-tags-DftDOzP-.js → ts-tags-Z9ulxVsq.js} +1 -1
  106. package/dist/assets/web/dist/assets/{tsx-BOdvPohJ.js → tsx-oI4bI3Vf.js} +1 -1
  107. package/dist/assets/web/dist/assets/{twig-B_yLS56B.js → twig-D_RXwBIX.js} +1 -1
  108. package/dist/assets/web/dist/assets/{typescript-CdPCa4nU.js → typescript-BgqQDYoN.js} +1 -1
  109. package/dist/assets/web/dist/assets/{typst-C1oIFsUl.js → typst-5MW-KRDd.js} +1 -1
  110. package/dist/assets/web/dist/assets/{vue-CcszJEX8.js → vue-CZhHHsPm.js} +1 -1
  111. package/dist/assets/web/dist/assets/{vue-html-DcpxPcuT.js → vue-html-DlYy2d8v.js} +1 -1
  112. package/dist/assets/web/dist/assets/{vue-vine-D26bIKGj.js → vue-vine-CJDnx-qn.js} +1 -1
  113. package/dist/assets/web/dist/assets/wallClock-DmaC1sKA.js +1 -0
  114. package/dist/assets/web/dist/assets/{xml-D8zl6W5P.js → xml-BRBDk65Q.js} +1 -1
  115. package/dist/assets/web/dist/assets/{xsl-D86Bii28.js → xsl-BlCykPxo.js} +1 -1
  116. package/dist/assets/web/dist/assets/{yaml-g6Px4KFU.js → yaml-BqW48szf.js} +1 -1
  117. package/dist/cli.js +1443 -727
  118. package/package.json +1 -1
  119. package/dist/assets/web/dist/assets/AppShell-B4EUiSwP.js +0 -1
  120. package/dist/assets/web/dist/assets/CostsPage-D8VjAtuH.js +0 -2
  121. package/dist/assets/web/dist/assets/DeliveryPage-DpQi5XUI.js +0 -1
  122. package/dist/assets/web/dist/assets/HomePage-CvKuIGqg.js +0 -2
  123. package/dist/assets/web/dist/assets/InputMenu-DmqAxkn4.js +0 -1
  124. package/dist/assets/web/dist/assets/NotFoundPage-DDogyiXA.js +0 -1
  125. package/dist/assets/web/dist/assets/PendingTurnRow-BoSLlR6j.js +0 -1
  126. package/dist/assets/web/dist/assets/ResidentDetailPage-CiBdbzkE.js +0 -1
  127. package/dist/assets/web/dist/assets/ResidentsIndexPage-DgtHtd9K.js +0 -1
  128. package/dist/assets/web/dist/assets/RunFoldRow-IF4xbz-3.js +0 -1
  129. package/dist/assets/web/dist/assets/RunRoutePage-DHYa7rHK.js +0 -9
  130. package/dist/assets/web/dist/assets/RunsIndexPage-aWhNTv8r.js +0 -1
  131. package/dist/assets/web/dist/assets/RunsTabs-CY_tdGTU.js +0 -1
  132. package/dist/assets/web/dist/assets/ScheduledPage-gPEBZjcx.js +0 -1
  133. package/dist/assets/web/dist/assets/SettingSelect-VDanrTZi.js +0 -1
  134. package/dist/assets/web/dist/assets/SettingsPage-CFoltSRD.js +0 -1
  135. package/dist/assets/web/dist/assets/StatusDot-DA3eTVrT.js +0 -1
  136. package/dist/assets/web/dist/assets/Tooltip-BgbCkM12.js +0 -1
  137. package/dist/assets/web/dist/assets/UnitRoutePage-DibegaJJ.js +0 -1
  138. package/dist/assets/web/dist/assets/durationTone-l6aVrHb8.js +0 -1
  139. package/dist/assets/web/dist/assets/eventSource-Ce94ho6M.js +0 -1
  140. package/dist/assets/web/dist/assets/favicon-D1hkyC5j.js +0 -1
  141. package/dist/assets/web/dist/assets/main-B4A6x82d.js +0 -28
  142. package/dist/assets/web/dist/assets/main-CpkwzHX6.css +0 -1
  143. package/dist/assets/web/dist/assets/residentsModel-By1fThvk.js +0 -1
  144. package/dist/assets/web/dist/assets/wallClock-DPTuP2FD.js +0 -1
@@ -347,7 +347,7 @@ workspaceDir: ./workspaces
347
347
  # `ship` fails the load by name.
348
348
  # ship:
349
349
  # maxRounds: 3 # review rounds per unit
350
- # maxMinutes: 120 # the ship preset's wall-clock budget in minutes (the registry's default)
350
+ # maxMinutes: 240 # the ship preset's wall-clock budget in minutes (the registry's default)
351
351
 
352
352
  # Which harness each preset's runs are driven by, deployment-wide
353
353
  # (docs/reference/specs/harness.md item 8): a mapping of preset to a harness's
@@ -641,8 +641,18 @@ interface ConfirmationRow extends ConfirmationInput {
641
641
  }
642
642
  /** Why a consume or a cancel refused: the row is gone (`used`), past its expiry (`expired`), or someone else's (`foreign`). */
643
643
  type ConfirmationRefusal = "used" | "expired" | "foreign";
644
- type ConfirmationOutcome = { row: ConfirmationRow } | { refused: ConfirmationRefusal };
644
+ /** A refusal names the row where one still exists — `expired` (deleted here)
645
+ * and `foreign` (kept) — so the bot can record the click's refusal against
646
+ * the command that was bound (record 0054); `used` has no row to name. */
647
+ type ConfirmationOutcome = { row: ConfirmationRow } | { refused: ConfirmationRefusal; row?: ConfirmationRow };
645
648
  type ConfirmationCancelOutcome = { ok: true } | { refused: Exclude<ConfirmationRefusal, "expired"> };
649
+ /** The consume log's word: a refusal may carry the row it names (expired,
650
+ * foreign), so the refusal is the discriminant, never the row's presence. The
651
+ * parameter is the declared union, where the narrowing holds; the stub's
652
+ * return type narrows to `never` across `in`. */
653
+ function consumeWord(outcome: ConfirmationOutcome): string {
654
+ return "refused" in outcome ? outcome.refused : "consumed";
655
+ }
646
656
 
647
657
  function isJsonObject(v: unknown): v is Record<string, unknown> {
648
658
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -718,9 +728,9 @@ export class ConfigDO extends DurableObject<Env> {
718
728
  if (!stored) return { refused: "used" };
719
729
  if (stored.expiresAt <= now) {
720
730
  this.sql.exec(`DELETE FROM confirmations WHERE id = ?`, id);
721
- return { refused: "expired" };
731
+ return { refused: "expired", row: stored };
722
732
  }
723
- if (!actorIds.includes(stored.requester)) return { refused: "foreign" };
733
+ if (!actorIds.includes(stored.requester)) return { refused: "foreign", row: stored };
724
734
  this.sql.exec(`DELETE FROM confirmations WHERE id = ?`, id);
725
735
  return { row: stored };
726
736
  });
@@ -1233,7 +1243,7 @@ async function handleConfig(pathname: string, body: unknown, env: Env): Promise<
1233
1243
  const click = confirmationClickOf(b);
1234
1244
  if (click instanceof Response) return click;
1235
1245
  const outcome = await dO.consumeConfirmation(click.id, click.actorIds, systemClock());
1236
- console.log(`[config/confirmations/consume] ${click.id} ${"row" in outcome ? "consumed" : outcome.refused}`);
1246
+ console.log(`[config/confirmations/consume] ${click.id} ${consumeWord(outcome)}`);
1237
1247
  return json(outcome);
1238
1248
  }
1239
1249
  case "/config/confirmations/cancel": {
@@ -628,7 +628,7 @@ const ATTACH_MUTEX_WAIT_MS = 60_000;
628
628
  * 20-minute ceiling (`BASH_TIMEOUT_MAX_MS`) — clamped server-side by
629
629
  * `clampBashTimeout` in handleExec, never trusting the client's number. The
630
630
  * heartbeat keeps every HTTP hop alive for the whole budget; the ceiling
631
- * stays far inside the bot's 45-min run budget. Work beyond 20 minutes
631
+ * stays far inside the bot's 90-min coding budget. Work beyond 20 minutes
632
632
  * belongs in background jobs.
633
633
  *
634
634
  * /op runs (test/build) get the flat 20-minute ceiling — the deterministic op
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.246.0",
3
+ "version": "1.248.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.246.0",
9
+ "version": "1.248.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.246.0",
20448
+ "version": "1.248.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.246.0",
3
+ "version": "1.248.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",
@@ -39,14 +39,16 @@
39
39
  "cli": "tsx src/cli.ts",
40
40
  "verify": "npm run verify:root && npm run verify --workspaces --if-present && npm run check:site",
41
41
  "verify:root": "npm run check:consistency && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:dist",
42
- "check:consistency": "npm run check:lockfile && npm run check:sandbox-pair && npm run skills:check && npm run licenses:check && npm run docs:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run check:project-facts && npm run agents:check && npm run clock:check && npm run screenshots:check",
42
+ "check:consistency": "npm run check:lockfile && npm run check:sandbox-pair && npm run skills:check && npm run licenses:check && npm run docs:check && npm run pr-title:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run check:project-facts && npm run agents:check && npm run clock:check && npm run screenshots:check",
43
43
  "ci:gate": "node scripts/ci-gate.mjs",
44
- "fix": "npm run docs:gen && npm run agents:gen && npm run clock:gen && npm run hygiene:gen && npm run deploy:gen && npm run skills:sync && npm run lint:fix && npm run format",
44
+ "fix": "npm run docs:gen && npm run pr-title:gen && npm run agents:gen && npm run clock:gen && npm run hygiene:gen && npm run deploy:gen && npm run skills:sync && npm run lint:fix && npm run format",
45
45
  "deploy:gen": "npm run --silent cli -- deploy init",
46
46
  "deploy:check": "npm run --silent cli -- deploy init --check",
47
47
  "check:lockfile": "node scripts/check-lockfile.mjs",
48
48
  "check:sandbox-pair": "node scripts/check-sandbox-pair.mjs",
49
49
  "check:pr-title": "node scripts/check-pr-title.mjs",
50
+ "pr-title:gen": "node scripts/pr-title-vocabulary.mjs --write",
51
+ "pr-title:check": "node scripts/pr-title-vocabulary.mjs",
50
52
  "check:project-facts": "node scripts/check-project-facts.mjs",
51
53
  "agents:gen": "tsx scripts/agents-gen.ts",
52
54
  "clock:gen": "tsx scripts/clock-allowlist.mts --write",
@@ -100,8 +100,16 @@
100
100
  "when": "After bumping either half of a pair."
101
101
  },
102
102
  "check:pr-title": {
103
- "does": "Judges one PR title as the changelog line it becomes: grammar, type, scope, the migration note behind `!`.",
104
- "when": "`-- \"feat(scope): …\"` before opening a PR; CI's `title` check runs it."
103
+ "does": "Judges one PR title as the changelog line: grammar, type, scope, the `!` migration note.",
104
+ "when": "`-- \"feat(scope): …\"` before opening a PR; CI's `title` check."
105
+ },
106
+ "pr-title:gen": {
107
+ "does": "Writes the title gate's types and scopes from the release config and the code map.",
108
+ "when": "Part of `fix`."
109
+ },
110
+ "pr-title:check": {
111
+ "does": "The committed title vocabulary equals its two sources.",
112
+ "when": "Part of `check:consistency`."
105
113
  },
106
114
  "check:dist": {
107
115
  "does": "Builds, then proves the compiled entry points and the web bundle the image needs exist.",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.246.0",
3
- "commit": "62e4e9ad464820900b07bed176e140af6682c598",
4
- "builtAt": "2026-09-18T01:06:13.947Z"
2
+ "version": "1.248.0",
3
+ "commit": "e2ba9e4c1b52ac15c841be8ad086b017d4a14ced",
4
+ "builtAt": "2026-09-18T04:34:16.576Z"
5
5
  }
@@ -2,7 +2,12 @@
2
2
  import type { Effort } from "../effort.js";
3
3
  import { BASH_TIMEOUT_MAX_MS } from "../execution/bashTimeout.js";
4
4
  import { ASKS, RUNAWAY_TURNS_PER_MINUTE, runawayTurnCap, type LoopPreset } from "../core/budgets.js";
5
- import { CONTRACT_HEADING, CONTRACT_SECTION_HEADINGS, PR_TITLE_GUARD } from "../core/ship/contract.js";
5
+ import {
6
+ CONTRACT_HEADING,
7
+ CONTRACT_SECTION_HEADINGS,
8
+ PR_TITLE_GUARD,
9
+ TIMEOUT_ON_LONG_COMMANDS,
10
+ } from "../core/ship/contract.js";
6
11
  // Which model runs it is resolved separately by the config layers, so any
7
12
  // agent can run on any configured provider/model.
8
13
 
@@ -143,7 +148,7 @@ export function statusCardRule(examples = '"Implement the fix", "Run the test su
143
148
  );
144
149
  }
145
150
 
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.
151
+ 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, and on Switchboard's own repository the tool refuses the same titles with the gate's own sentence, so fix the title and resubmit rather than pushing on. 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
152
  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.
148
153
  - **title** (≤72 characters in all): the changelog line — \`type(scope): what a reader can now do or expect\`, one change, one clause, present tense. The type and the scope spend the same budget as the description, so the description is short; the body carries the rest. The tool refuses a longer title naming the count, and so does the gate in CI.
149
154
  - **TL;DR** (\`tldr\`, rendered first, ≤300): two sentences for a reader with zero context — what this PR does and why it matters.
@@ -233,7 +238,7 @@ Keep notes with the \`notes\` tool: one short document, replaced whole each time
233
238
  // project's most expensive checks first; the rule is the runner's to hold, not
234
239
  // a line every requester remembers to paste. Stack-agnostic on purpose — the
235
240
  // classes are by duration, the project's own scripts and CI say which is which.
236
- 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.`;
241
+ 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. ${TIMEOUT_ON_LONG_COMMANDS} The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
237
242
 
238
243
  const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
239
244
 
@@ -68,12 +68,18 @@ export type Preset = LoopPreset | "ship";
68
68
 
69
69
  /** What each preset asks for when nothing above it is tighter, in minutes. The
70
70
  * pipeline's (`ship`) is the wall clock one segment of its loop runs under;
71
- * a deployment's `ship.maxMinutes` replaces it, held to `fit` below. */
71
+ * a deployment's `ship.maxMinutes` replaces it, held to `fit` below. Coding's
72
+ * is sized by the ledger's ship children, not its standalone runs: a child
73
+ * that pushes at minute 29 and then runs the repo's whole gate (seven to ten
74
+ * minutes on a resident) was cut at 45 one run in twelve, so the ask is
75
+ * double the 90th percentile (31.5) with the gate inside it. Ship's holds
76
+ * its own loop at the default rounds with a fix round at that ask, not at
77
+ * its floor (`fit` proves the floor case; the ask leaves room above it). */
72
78
  export const ASKS: Readonly<Record<Preset, number>> = {
73
79
  general: 5,
74
- coding: 45,
80
+ coding: 90,
75
81
  review: 25,
76
- ship: 120,
82
+ ship: 240,
77
83
  research: 8,
78
84
  explore: 120,
79
85
  conductor: 120,
@@ -87,10 +93,13 @@ export type RoundKind = "coding" | "review" | "fix" | "merge";
87
93
  * falls under the floor is refused rather than dispatched: a two-minute
88
94
  * review or fix costs an attach and a model turn and finishes nothing.
89
95
  * Review's is the ledger's 90th percentile of completed reviews (5.1 min over
90
- * 181); coding's and the merge wait's are guesses until the ledger says. */
96
+ * 181); coding's is the least a fix round can run the repo's gate in — the
97
+ * gate alone takes seven to ten minutes on a resident, and a fix that cannot
98
+ * run it finishes nothing its contract asks; the merge wait's is a guess
99
+ * until the ledger says. */
91
100
  export const PRESET_FLOORS: Readonly<Record<LoopPreset, number>> = {
92
101
  general: 2,
93
- coding: 10,
102
+ coding: 15,
94
103
  review: 5,
95
104
  research: 3,
96
105
  explore: 15,
@@ -387,6 +396,9 @@ export type GrantSource = "org" | "channel" | "user" | "run";
387
396
  export const DEFAULT_GRANT: Grant = { renewals: 0 };
388
397
 
389
398
  /** The most renewals one request may carry: thirteen segments of the ship
390
- * preset's ask are a day, the longest problem a person hands over in one
391
- * message before a plan should carry it. */
399
+ * preset's ask are two days. The cap bounds a COUNT a person types
400
+ * (`renewals:`) or a scope holds; it did not halve when the ask doubled,
401
+ * since every grant already configured is a count of segments and a day's
402
+ * hand-over is now `renewals: 5` — the longest problem a person hands over
403
+ * in one message before a plan should carry it. */
392
404
  export const GRANT_RENEWALS_MAX = 12;
@@ -462,7 +462,7 @@ export type RouteInputValue = RouteInputLeafOrList | RouteInputObject3;
462
462
  * `pasted` — the typed line that followed a hand-back in its thread, with the
463
463
  * same receipt, ran. A routed read carries no outcome: it is not a decision
464
464
  * about a state change. */
465
- export type RouteOutcome = "hand_back" | "offered" | "confirmed" | "pasted";
465
+ export type RouteOutcome = "hand_back" | "offered" | "confirmed" | "pasted" | "refused";
466
466
 
467
467
  export type RunEvent =
468
468
  /** `callId` is the provider's tool_use id — the explicit pair key between a
@@ -864,6 +864,10 @@ export type RunEvent =
864
864
  input?: { readonly [key: string]: RouteInputValue };
865
865
  receipt?: string;
866
866
  outcome?: RouteOutcome;
867
+ /** The refusal's code (src/core/refusal.ts) when `outcome` is `refused`
868
+ * (record 0054): a refusal after a command was bound is a run
869
+ * record, and the door report counts it by cause and code. */
870
+ refusalCode?: string;
867
871
  handBackRunId?: string;
868
872
  seq?: number;
869
873
  at?: number;
@@ -129,6 +129,13 @@ export const CONTRACT_SECTION_HEADINGS = {
129
129
  * repository's rules file (~15k characters) and a long unit both inside it. */
130
130
  export const DEFAULT_CONTRACT_MAX_CHARS = 48_000;
131
131
 
132
+ /** The most of a plan a reader hands the parsers: ten times the tool clip
133
+ * (`MAX_FILE_CHARS`, `src/execution/githubApi.ts`), which a plan that grows by
134
+ * amendment passes — the program plan crossed 400k characters and the seed
135
+ * then saw its first 25 units alone. A plan past this bound is refused by
136
+ * name, never parsed short. */
137
+ export const PLAN_MAX_CHARS = 2_000_000;
138
+
132
139
  const UNIT_HEADING = /^### (U\d+)\. (.+?)\s*$/;
133
140
  const ANY_HEADING = /^#{2,3} /;
134
141
  const BULLET = /^- \*\*([^*]+)\*\*:(?:\s?(.*))?$/;
@@ -422,6 +429,16 @@ function hasApproach(unit: ContractUnit): boolean {
422
429
  return "Approach" in unit.bullets;
423
430
  }
424
431
 
432
+ /** The timeout clause every coding contract carries after the wind-down
433
+ * sentence (agent-coding item 13; harness-pi items 7 and 15): the harness
434
+ * refuses a bash call whose explicit timeout reaches past the loop's end
435
+ * before it runs, so a stated timeout is what turns a command that could
436
+ * never finish into a refusal the model reads at once, instead of minutes
437
+ * spent on it and a cut at the end. One sentence, stack-agnostic. */
438
+ export const TIMEOUT_ON_LONG_COMMANDS =
439
+ "State a timeout on any command you expect to run longer than a minute: a timeout that reaches past the loop's " +
440
+ "end is refused before the command runs, never cut midway.";
441
+
425
442
  function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
426
443
  const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
427
444
  const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
@@ -432,7 +449,7 @@ function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
432
449
  `full verification, which runs after that push with any fix as a further commit; an unpushed tree does not ` +
433
450
  `survive the run's end. Right before each push, fetch ${onto} again and rebase once more if it moved while ` +
434
451
  `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.`
452
+ `say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`
436
453
  );
437
454
  }
438
455
 
@@ -754,7 +754,7 @@ export const WAIT_MARGIN_MS = SHIP_WAIT.marginMinutes * MIN;
754
754
  * one chunk of the runner's time, not the child's whole budget. Five minutes
755
755
  * is the machine's one cadence for asking the bot what it cannot be told (the
756
756
  * margin and the merge poll are the same number), and it keeps a round to a
757
- * few steps: a coding child's 45 minutes are ten waits and ten reads. */
757
+ * few steps: a coding child's 90 minutes are eighteen waits and eighteen reads. */
758
758
  export const WAIT_CHUNK_MS = SHIP_WAIT.chunkMinutes * MIN;
759
759
  /** The merge wait is a round of its own: its minutes are carved from the
760
760
  * pipeline's remainder when the door is first asked (the merge wait's ask and
@@ -26,6 +26,11 @@ export interface AttrDomain {
26
26
  requester: "message" | "relay-footer" | "thread-parent" | "bot";
27
27
  // dispatch.* / run.* / post.*
28
28
  outcome: string;
29
+ /** The refusal's code (src/core/refusal.ts) — on the `dispatch.refuse` span
30
+ * and the request's root, so refusals are countable from the trace alone. */
31
+ refusal: string;
32
+ /** The refusal's cause, from the one code→cause table in src/core/refusal.ts. */
33
+ cause: "request" | "policy" | "system";
29
34
  count: number;
30
35
  backend: Backend;
31
36
  // run.command
@@ -114,6 +119,8 @@ export type SpanAttrs = { readonly [K in SpanAttrKey]?: AttrDomain[K] };
114
119
  const IDENTIFIER_KEYS: ReadonlySet<SpanAttrKey> = new Set<SpanAttrKey>([
115
120
  "runId",
116
121
  "outcome",
122
+ "refusal",
123
+ "cause",
117
124
  "command",
118
125
  "route",
119
126
  "host",
@@ -163,6 +170,8 @@ const ATTR_TYPE: Record<SpanAttrKey, "string" | "number" | "boolean"> = {
163
170
  dedupe: "string",
164
171
  requester: "string",
165
172
  outcome: "string",
173
+ refusal: "string",
174
+ cause: "string",
166
175
  count: "number",
167
176
  backend: "string",
168
177
  command: "string",