@coreplane/switchboard 1.245.0 → 1.247.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 +1 -1
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +14 -4
  3. package/dist/assets/deploy/cloudflare-resident/worker.ts +10 -11
  4. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +17 -1
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +5 -3
  7. package/dist/assets/project.json +10 -2
  8. package/dist/assets/source.json +3 -3
  9. package/dist/assets/src/agents/registry.ts +32 -8
  10. package/dist/assets/src/core/budgets.ts +37 -7
  11. package/dist/assets/src/core/coordinator/contract.ts +29 -4
  12. package/dist/assets/src/core/coordinator/driver.ts +7 -1
  13. package/dist/assets/src/core/reviewVerdict.ts +138 -6
  14. package/dist/assets/src/core/runEvents.ts +34 -2
  15. package/dist/assets/src/core/runRecord.ts +38 -16
  16. package/dist/assets/src/core/ship/contract.ts +7 -0
  17. package/dist/assets/src/core/ship/coordinator.ts +24 -3
  18. package/dist/assets/src/core/ship/renewal.ts +10 -3
  19. package/dist/assets/src/core/trace/attrs.ts +9 -0
  20. package/dist/assets/src/execution/bashTimeout.ts +46 -2
  21. package/dist/assets/src/execution/residentRefresh.ts +9 -5
  22. package/dist/assets/src/execution/sandboxErrors.ts +9 -4
  23. package/dist/assets/web/dist/.vite/manifest.json +474 -485
  24. package/dist/assets/web/dist/assets/AppShell-DAtHiuI6.js +1 -0
  25. package/dist/assets/web/dist/assets/CostsPage-BoKzYa4B.js +2 -0
  26. package/dist/assets/web/dist/assets/DeliveryPage-NP4g6bQd.js +1 -0
  27. package/dist/assets/web/dist/assets/HomePage-DYxC0izY.js +2 -0
  28. package/dist/assets/web/dist/assets/InputMenu-C6aPpe30.js +1 -0
  29. package/dist/assets/web/dist/assets/NotFoundPage-DtE-GgTk.js +1 -0
  30. package/dist/assets/web/dist/assets/PendingTurnRow-BuRre8it.js +1 -0
  31. package/dist/assets/web/dist/assets/ResidentDetailPage-DLIpWYOc.js +1 -0
  32. package/dist/assets/web/dist/assets/ResidentsIndexPage-6LipuDjR.js +1 -0
  33. package/dist/assets/web/dist/assets/RunFoldRow-V-iSy64e.js +1 -0
  34. package/dist/assets/web/dist/assets/RunRoutePage-DUalB1u2.js +9 -0
  35. package/dist/assets/web/dist/assets/RunsIndexPage-B9Ba1KdD.js +1 -0
  36. package/dist/assets/web/dist/assets/RunsTabs-YSbUu5py.js +1 -0
  37. package/dist/assets/web/dist/assets/ScheduledPage-KdjLtD_7.js +1 -0
  38. package/dist/assets/web/dist/assets/SettingSelect-BIzAsLk1.js +1 -0
  39. package/dist/assets/web/dist/assets/SettingsPage-IT5l_NaL.js +1 -0
  40. package/dist/assets/web/dist/assets/StatusDot-DBHAl4Il.js +1 -0
  41. package/dist/assets/web/dist/assets/Tooltip-_LEjptLV.js +1 -0
  42. package/dist/assets/web/dist/assets/UnitRoutePage-DPBsvGPR.js +1 -0
  43. package/dist/assets/web/dist/assets/{angular-html-qXhb9ti8.js → angular-html-BjeQZdCq.js} +1 -1
  44. package/dist/assets/web/dist/assets/{angular-ts-DzvdyoUZ.js → angular-ts-ZGdORrL2.js} +1 -1
  45. package/dist/assets/web/dist/assets/{apl-Ck7Ru2Sm.js → apl-DhvV_X93.js} +1 -1
  46. package/dist/assets/web/dist/assets/{astro-CwehvReb.js → astro-DiRSE4Ug.js} +1 -1
  47. package/dist/assets/web/dist/assets/{blade-WSyVbSFK.js → blade-D8Fr5Wvq.js} +1 -1
  48. package/dist/assets/web/dist/assets/{seed-DFfT5S_N.js → browser-B6T5hjfR.js} +2 -2
  49. package/dist/assets/web/dist/assets/{c-DMHI1wDz.js → c-BwbC64D4.js} +1 -1
  50. package/dist/assets/web/dist/assets/{chapel-BF8AYK8_.js → chapel-DgmqRK97.js} +1 -1
  51. package/dist/assets/web/dist/assets/{cobol-UAfImuWu.js → cobol-DJod2RNK.js} +1 -1
  52. package/dist/assets/web/dist/assets/{coffee-D54hi96d.js → coffee-D1Y2CgBY.js} +1 -1
  53. package/dist/assets/web/dist/assets/{cpp-BRWfLgBc.js → cpp-CsO97YOM.js} +1 -1
  54. package/dist/assets/web/dist/assets/{crystal-CSqC5deT.js → crystal-CJ7m2tOc.js} +1 -1
  55. package/dist/assets/web/dist/assets/{css-D4ZuRFGk.js → css-B2M-NKoj.js} +1 -1
  56. package/dist/assets/web/dist/assets/{dist-73ZMn_Mg.js → dist-BcYPGOBL.js} +2 -2
  57. package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +1 -0
  58. package/dist/assets/web/dist/assets/{edge-DDPtNbZc.js → edge-CFVW-m9B.js} +1 -1
  59. package/dist/assets/web/dist/assets/{elixir-BTQkQfsJ.js → elixir-B2mtRgCr.js} +1 -1
  60. package/dist/assets/web/dist/assets/{elm-B_PkLzGZ.js → elm-2teVAwCi.js} +1 -1
  61. package/dist/assets/web/dist/assets/{erb-DfjIMVM_.js → erb-LAbeNqen.js} +1 -1
  62. package/dist/assets/web/dist/assets/eventSource-Bj_dZf6M.js +1 -0
  63. package/dist/assets/web/dist/assets/{git-rebase-DP8wmn7K.js → git-rebase-DnhzH_sp.js} +1 -1
  64. package/dist/assets/web/dist/assets/{glimmer-js-G12VK2gK.js → glimmer-js-DPKjXf1-.js} +1 -1
  65. package/dist/assets/web/dist/assets/{glimmer-ts-D3XiSI8E.js → glimmer-ts-CABXnt5z.js} +1 -1
  66. package/dist/assets/web/dist/assets/{glsl-DOKAhMj-.js → glsl-Dz4-3-gJ.js} +1 -1
  67. package/dist/assets/web/dist/assets/{graphql-CJB_fmON.js → graphql-ByWUD1DL.js} +1 -1
  68. package/dist/assets/web/dist/assets/{hack-BHqtRWap.js → hack-CdmOK2K-.js} +1 -1
  69. package/dist/assets/web/dist/assets/{haml-DwotyarS.js → haml-DzegMpd2.js} +1 -1
  70. package/dist/assets/web/dist/assets/{handlebars-B6fDaA3U.js → handlebars-B5NeiP0e.js} +1 -1
  71. package/dist/assets/web/dist/assets/{html-s52taj36.js → html-Zg_NHLv9.js} +1 -1
  72. package/dist/assets/web/dist/assets/{html-derivative-C5bpQmmI.js → html-derivative-DZf9JNzh.js} +1 -1
  73. package/dist/assets/web/dist/assets/{http-99zC935r.js → http-CVFrQZeN.js} +1 -1
  74. package/dist/assets/web/dist/assets/{hurl-Bgk-4-0x.js → hurl-L5brWAJe.js} +1 -1
  75. package/dist/assets/web/dist/assets/{java-CuV_c5I0.js → java-hFphyZ4R.js} +1 -1
  76. package/dist/assets/web/dist/assets/{javascript-CZlOvSMQ.js → javascript-CAJO1WKX.js} +1 -1
  77. package/dist/assets/web/dist/assets/{jinja-BuXeZ4Tu.js → jinja-N4A4srXI.js} +1 -1
  78. package/dist/assets/web/dist/assets/{jison-DIuP0hkG.js → jison-D3BZa_iU.js} +1 -1
  79. package/dist/assets/web/dist/assets/{json-CZ74vfoD.js → json-DxD1Qh8W.js} +1 -1
  80. package/dist/assets/web/dist/assets/{jsx-BNjRs8Vj.js → jsx-CHhag69S.js} +1 -1
  81. package/dist/assets/web/dist/assets/{julia-OCE-fFOg.js → julia-CrhkZ6Tt.js} +1 -1
  82. package/dist/assets/web/dist/assets/{just-CtvD87WF.js → just-D0LFOuhq.js} +1 -1
  83. package/dist/assets/web/dist/assets/{latex-C3kf86OR.js → latex-BHAlwCXK.js} +1 -1
  84. package/dist/assets/web/dist/assets/{liquid-B1WI--Kp.js → liquid-CkaLfCWh.js} +1 -1
  85. package/dist/assets/web/dist/assets/{lua-DtoT0gKg.js → lua-B1h4aAzP.js} +1 -1
  86. package/dist/assets/web/dist/assets/main-DUfSE0dj.js +28 -0
  87. package/dist/assets/web/dist/assets/main-zbP_dTjR.css +1 -0
  88. package/dist/assets/web/dist/assets/{marko-BE2MC6Yt.js → marko-D6CG4hvX.js} +1 -1
  89. package/dist/assets/web/dist/assets/{mdc-BwO4weXJ.js → mdc-CVQzimAU.js} +1 -1
  90. package/dist/assets/web/dist/assets/{nginx-BTuOgGVC.js → nginx-h-9Ir54h.js} +1 -1
  91. package/dist/assets/web/dist/assets/{nim-CiIUM-oV.js → nim-DHVI30sp.js} +1 -1
  92. package/dist/assets/web/dist/assets/{org-L78DEWB-.js → org-AasAXfMF.js} +1 -1
  93. package/dist/assets/web/dist/assets/{perl-KqHOm4Ft.js → perl-BVi5aTEy.js} +1 -1
  94. package/dist/assets/web/dist/assets/{php-C41XbEkn.js → php-qOkCeUKr.js} +1 -1
  95. package/dist/assets/web/dist/assets/{pug-B12-4i-k.js → pug-8TnUGvEX.js} +1 -1
  96. package/dist/assets/web/dist/assets/{qml--fw2Up5c.js → qml-CpJbhoxn.js} +1 -1
  97. package/dist/assets/web/dist/assets/{r-Db1JBXrC.js → r-JkUNcAJW.js} +1 -1
  98. package/dist/assets/web/dist/assets/{razor-BYpVBgbu.js → razor-CtU4ws5a.js} +1 -1
  99. package/dist/assets/web/dist/assets/{regexp-BMqGLSPc.js → regexp-J2zA6ceN.js} +1 -1
  100. package/dist/assets/web/dist/assets/{rst-B63n7-Sa.js → rst-CMCvf-yZ.js} +1 -1
  101. package/dist/assets/web/dist/assets/{ruby-D3VI8UYv.js → ruby-ChLBY_iR.js} +1 -1
  102. package/dist/assets/web/dist/assets/{sas-DxRf8ZAR.js → sas-cwn4x2VY.js} +1 -1
  103. package/dist/assets/web/dist/assets/{scss-DRRA6d6A.js → scss-jSrJlzLI.js} +1 -1
  104. package/dist/assets/web/dist/assets/{shellscript-CoOBe3_y.js → shellscript-D5nAd0gs.js} +1 -1
  105. package/dist/assets/web/dist/assets/{shellsession-DOV1HYxf.js → shellsession-BZY4-Tdh.js} +1 -1
  106. package/dist/assets/web/dist/assets/{soy-Acpmpp1W.js → soy-COppPOKr.js} +1 -1
  107. package/dist/assets/web/dist/assets/{sql-DeT9N-fA.js → sql-Blc_6dOA.js} +1 -1
  108. package/dist/assets/web/dist/assets/{sseReplay-DPXVxhL5.js → sseReplay-DXC7kGbN.js} +3 -3
  109. package/dist/assets/web/dist/assets/{stata-CbxXAMO4.js → stata-DRv3OY-h.js} +1 -1
  110. package/dist/assets/web/dist/assets/{surrealql-BdKbi4hc.js → surrealql-qMv_eeJ6.js} +1 -1
  111. package/dist/assets/web/dist/assets/{svelte-tAcjDM7R.js → svelte-d1Edb6Rz.js} +1 -1
  112. package/dist/assets/web/dist/assets/{templ-CObINNJj.js → templ-CUMSIOXG.js} +1 -1
  113. package/dist/assets/web/dist/assets/{tex-BYwpZT1J.js → tex-BGZQA-Ei.js} +1 -1
  114. package/dist/assets/web/dist/assets/{ts-tags-BciREL5R.js → ts-tags-Z9ulxVsq.js} +1 -1
  115. package/dist/assets/web/dist/assets/{tsx-CRiv3Pr3.js → tsx-oI4bI3Vf.js} +1 -1
  116. package/dist/assets/web/dist/assets/{twig-DuZZ1Ej0.js → twig-D_RXwBIX.js} +1 -1
  117. package/dist/assets/web/dist/assets/{typescript-jkHz_5HZ.js → typescript-BgqQDYoN.js} +1 -1
  118. package/dist/assets/web/dist/assets/{typst-DOac4Gal.js → typst-5MW-KRDd.js} +1 -1
  119. package/dist/assets/web/dist/assets/{vue-CgOX5wXA.js → vue-CZhHHsPm.js} +1 -1
  120. package/dist/assets/web/dist/assets/{vue-html-Duu99VNw.js → vue-html-DlYy2d8v.js} +1 -1
  121. package/dist/assets/web/dist/assets/{vue-vine-Bmj_wvRJ.js → vue-vine-CJDnx-qn.js} +1 -1
  122. package/dist/assets/web/dist/assets/wallClock-DmaC1sKA.js +1 -0
  123. package/dist/assets/web/dist/assets/{xml-BtUIVeWH.js → xml-BRBDk65Q.js} +1 -1
  124. package/dist/assets/web/dist/assets/{xsl-wX71n1E5.js → xsl-BlCykPxo.js} +1 -1
  125. package/dist/assets/web/dist/assets/{yaml-CdLdXvmz.js → yaml-BqW48szf.js} +1 -1
  126. package/dist/cli.js +2856 -1224
  127. package/package.json +1 -1
  128. package/dist/assets/web/dist/assets/AppShell-qqs_p96-.js +0 -1
  129. package/dist/assets/web/dist/assets/CostsPage-Dd9aXxxI.js +0 -2
  130. package/dist/assets/web/dist/assets/DeliveryPage-D90FTjYq.js +0 -1
  131. package/dist/assets/web/dist/assets/HomePage-VLTFopnl.js +0 -2
  132. package/dist/assets/web/dist/assets/InputMenu-lQP3ia9D.js +0 -1
  133. package/dist/assets/web/dist/assets/NotFoundPage-8ViskpN3.js +0 -1
  134. package/dist/assets/web/dist/assets/PendingTurnRow-D2gHef5v.js +0 -1
  135. package/dist/assets/web/dist/assets/ResidentDetailPage-nWLN8BOd.js +0 -1
  136. package/dist/assets/web/dist/assets/ResidentsIndexPage-D3Q48CJW.js +0 -1
  137. package/dist/assets/web/dist/assets/RunFoldRow-CHy_IfZG.js +0 -1
  138. package/dist/assets/web/dist/assets/RunRoutePage-C6kX5om4.js +0 -9
  139. package/dist/assets/web/dist/assets/RunsIndexPage-DjxGj1Q5.js +0 -1
  140. package/dist/assets/web/dist/assets/RunsTabs-WyeG5tKb.js +0 -1
  141. package/dist/assets/web/dist/assets/ScheduledPage-B8D5AU4d.js +0 -1
  142. package/dist/assets/web/dist/assets/SettingSelect-UTHsqUUS.js +0 -1
  143. package/dist/assets/web/dist/assets/SettingsPage-BoW7Zjzt.js +0 -1
  144. package/dist/assets/web/dist/assets/StatusDot-BGHureCJ.js +0 -1
  145. package/dist/assets/web/dist/assets/Tooltip-CiP8qPd7.js +0 -1
  146. package/dist/assets/web/dist/assets/UnitRoutePage-2zEeRBP1.js +0 -1
  147. package/dist/assets/web/dist/assets/durationTone-D51enaGR.js +0 -1
  148. package/dist/assets/web/dist/assets/eventSource-By0B_w4S.js +0 -1
  149. package/dist/assets/web/dist/assets/favicon-D1hkyC5j.js +0 -1
  150. package/dist/assets/web/dist/assets/main-ClluP-Qf.js +0 -28
  151. package/dist/assets/web/dist/assets/main-CpkwzHX6.css +0 -1
  152. package/dist/assets/web/dist/assets/residentsModel-By1fThvk.js +0 -1
  153. package/dist/assets/web/dist/assets/wallClock-DD_fPuSY.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
@@ -8906,19 +8906,18 @@ async function handleOp(env: Env, body: Record<string, unknown>, traceparent?: s
8906
8906
  return streamOp(stub.runOp(op as "test" | "build", ref, traceparent));
8907
8907
  }
8908
8908
 
8909
- /** /op's payload mapping: results pass through; a named error sheds its
8910
- * transport-only `status` field (the body is the contract, never the code); a
8911
- * pending op that rejected is the typed 500 (`catchAllErr`, so `transient`
8912
- * rides beside the words), its status shed the same way. */
8909
+ /** /op's payload mapping: results pass through; a failure streams as the typed
8910
+ * document it is — the named error with its `status` IN the body, a pending
8911
+ * op that rejected as the typed 500 (`catchAllErr`, so `transient` rides
8912
+ * beside the words) — exactly as `/exec`, `/attach` and `/await-restore`
8913
+ * stream theirs, so the client reads the status off the document by the one
8914
+ * rule (`answeredStatus`) and judges `transient` by the one rule too
8915
+ * (`isTransientRefusal`: a 5xx carrying it), never by the field on a 200. */
8913
8916
  function streamOp(pending: Promise<Awaited<ReturnType<ResidentDO["runOp"]>>>): Response {
8914
- const shedStatus = (failure: ThreadErr): object => {
8915
- const { status: _status, ...rest } = failure;
8916
- return rest;
8917
- };
8918
8917
  return streamHeartbeatJson(
8919
8918
  pending,
8920
- (result) => ("error" in result ? shedStatus(result) : result),
8921
- (err) => shedStatus(catchAllErr(err, "op-failed")),
8919
+ (result) => result,
8920
+ (err) => catchAllErr(err, "op-failed"),
8922
8921
  );
8923
8922
  }
8924
8923
 
@@ -111,6 +111,13 @@ const SDK_PIN: string = sandboxPkg.dependencies["@cloudflare/sandbox"];
111
111
 
112
112
  const WORKDIR = "/workspace";
113
113
 
114
+ /** How long the SDK waits for the platform to grant a container instance
115
+ * before its start fails (docs/reference/specs/execution.md item 23). The
116
+ * SDK's default is 30 s and it retries, so a burst that the platform would
117
+ * not admit took 63 s to say so; at 10 s (the SDK allows 5 s to 300 s) the
118
+ * refusal reaches the start gate — and the executor's wait — in seconds. */
119
+ const INSTANCE_GET_TIMEOUT_MS = 10_000;
120
+
114
121
  // Default per-command time limit, enforced by coreutils `timeout` inside the
115
122
  // sandbox. The tuned 280s applies when the body carries no timeoutMs (an older
116
123
  // bot); a caller-supplied timeoutMs is clamped server-side to the shared
@@ -806,7 +813,16 @@ export default {
806
813
  // One sandbox per thread; the DO name is the thread key. The stub's own
807
814
  // methods (`runCommand`, `readText`, …) are what the routes call — the
808
815
  // work happens in the Durable Object, the data comes back over RPC.
809
- const sandbox = getSandbox(env.Sandbox, threadKey);
816
+ const sandbox = getSandbox(env.Sandbox, threadKey, {
817
+ containerTimeouts: {
818
+ // A refused instance grant is learned in seconds, not after the SDK's
819
+ // 30 s default and its retries (63 s to "no container instance" under
820
+ // a burst, docs/reference/specs/execution.md item 23): the gate turns
821
+ // it into the wait token and the executor re-sends. A grant that is
822
+ // merely slow is re-asked on the next send, which finds it made.
823
+ instanceGetTimeoutMS: INSTANCE_GET_TIMEOUT_MS,
824
+ },
825
+ });
810
826
 
811
827
  const url = new URL(request.url);
812
828
  const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.245.0",
3
+ "version": "1.247.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.245.0",
9
+ "version": "1.247.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.245.0",
20448
+ "version": "1.247.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.245.0",
3
+ "version": "1.247.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.245.0",
3
- "commit": "5cc6e0f8a25dfb2406f9248a2245c7412e381da6",
4
- "builtAt": "2026-09-17T19:32:54.666Z"
2
+ "version": "1.247.0",
3
+ "commit": "713b83c9db0b6f3f10ef6d2cc1a46714b9a23c3c",
4
+ "builtAt": "2026-09-18T03:41:18.440Z"
5
5
  }
@@ -143,7 +143,7 @@ export function statusCardRule(examples = '"Implement the fix", "Run the test su
143
143
  );
144
144
  }
145
145
 
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.
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, 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
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.
148
148
  - **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
149
  - **TL;DR** (\`tldr\`, rendered first, ≤300): two sentences for a reader with zero context — what this PR does and why it matters.
@@ -209,6 +209,14 @@ Text stays in your message; do not attach what you can say.`;
209
209
  // belongs in the agent's notes for the thread, and why — the one thing sure to
210
210
  // survive a compaction and reach the next run there — beside the reach `recall`
211
211
  // gives into every earlier turn. Said once so the prompts cannot drift on it.
212
+ /** The one rule every preset carries about the length of what it says: the
213
+ * answer is read in a chat thread, so it leads with the outcome and stops
214
+ * when the outcome is said. Spelled the same in every prompt; the registry
215
+ * test pins it. A review's verdict and findings are rendered by code from the
216
+ * typed verdict, so there the rule governs the write-up alone. */
217
+ export const BREVITY_RULE =
218
+ "SAY LESS. Your answer is read in a chat thread, often on a phone: lead with the answer or the outcome in one sentence, then only what the reader needs to act on it. Never restate the question or recap what was asked, never describe your method or list what you checked (that is your notes' job), never list options you are not recommending, never end with an offer or a question about what to do next — when the answer is complete, stop. One idea per sentence; a bullet only for items that are truly parallel, one line each; no headings. A question gets a few sentences; a refusal gets one sentence and the nearest thing you can do; a report gets its findings and nothing around them. Length is right only when the person asked for depth or the content is the list they asked for (a table, a diff, a plan).";
219
+
212
220
  /** The one rule every preset carries about text it did not receive from the
213
221
  * person (record 0037): a linked thread, a stored record, a page someone
214
222
  * else wrote, arrives inside the untrusted fence and is quoted data. Spelled
@@ -265,6 +273,7 @@ ${statusCardRule('"Clone repo and read the diff", "Run the test suite"')}
265
273
 
266
274
  If the request doesn't name a repository and you can't infer it, ask for it instead of guessing.
267
275
  Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
276
+ ${BREVITY_RULE}
268
277
  ${FENCED_CONTENT_RULE}
269
278
  Your final message is posted to Slack — keep it readable, lead with the outcome.`;
270
279
 
@@ -310,6 +319,7 @@ ${NOTEPAD}
310
319
  ${statusCardRule()}
311
320
 
312
321
  Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
322
+ ${BREVITY_RULE}
313
323
  ${FENCED_CONTENT_RULE}
314
324
  Your final message is posted to Slack — keep it readable, lead with the outcome.`;
315
325
 
@@ -352,6 +362,7 @@ ${NOTEPAD}
352
362
  ${statusCardRule()}
353
363
 
354
364
  Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
365
+ ${BREVITY_RULE}
355
366
  ${FENCED_CONTENT_RULE}
356
367
  Your final message is posted to Slack — keep it readable, lead with the outcome.`;
357
368
 
@@ -359,7 +370,13 @@ Your final message is posted to Slack — keep it readable, lead with the outcom
359
370
  // (docs/reference/specs/agent-ship.md item 6) lives here once — stable ids, the severity
360
371
  // vocabulary, the severity gate's downgrade (agent-review.md item 5a) — so the
361
372
  // sandbox and resident variants can never drift apart on it.
362
- const REVIEW_VERDICT_INSTRUCTION = `VERDICT: before your final message, call the submit_verdict tool exactly once with \`approve\` (no finding at or above the severity to address remains — the level in force for this run, \`minor\` by default: a major or a minor finding means \`request_changes\`; nits alone never block) or \`request_changes\`, a one-line summary, \`head\` = the output of \`git rev-parse HEAD\` in the checkout you reviewed, and \`findings\` — every issue you report as a structured entry with a stable id you assign in order (F1, F2, …), a severity of exactly blocking|major|minor|nit, the file (plus line when it points at one), and a one-line title. The findings array is the index of your review: the full explanation of each finding stays in your prose, keyed by the same ids. Switchboard writes the verdict as the first line of the GitHub comment itself and lists the findings under it; a review with no submitted verdict is posted as not approving, so never skip it. An \`approve\` carrying a finding at or above the severity to address is downgraded to \`request_changes\` and the tool's ack says so — approve only when every finding sits below the level. Do not write "LGTM" in your own text — the verdict line carries it.`;
373
+ // The write-up is the review's text alone (docs/reference/specs/agent-review.md
374
+ // item 5b): the verdict line and the findings list are rendered by code from
375
+ // the submit_verdict call — on GitHub as the comment's head, in Slack as the
376
+ // whole reply — so the prose never repeats them and never pads around them.
377
+ const REVIEW_FINAL_MESSAGE = `YOUR FINAL MESSAGE IS THE REVIEW'S TEXT, NOTHING ELSE. Switchboard renders the verdict line and the findings list from your submit_verdict call — on GitHub as the head of the comment, in Slack as the whole reply — and folds your final message under them on GitHub as the full review. So write only what the list cannot carry: one short paragraph per finding, keyed by its id (what is wrong, the concrete failure, the fix). Do not restate the verdict or the findings, do not summarize what you read, do not list what you verified clean, do not describe your method — what you checked belongs in your notes, which the run page shows. A change with no findings needs one sentence, not a tour.`;
378
+
379
+ const REVIEW_VERDICT_INSTRUCTION = `VERDICT: before your final message, call the submit_verdict tool exactly once with \`approve\` (no finding at or above the severity to address remains — the level in force for this run, \`minor\` by default: a major or a minor finding means \`request_changes\`; nits alone never block) or \`request_changes\`, a one-line summary, \`head\` = the output of \`git rev-parse HEAD\` in the checkout you reviewed, and \`findings\` — every issue you report as a structured entry with a stable id you assign in order (F1, F2, …), a severity of exactly blocking|major|minor|nit, the file (plus line when it points at one), and a one-line title. The findings array is the index of your review: the full explanation of each finding stays in your prose, keyed by the same ids. Switchboard writes the verdict as the first line of the GitHub comment itself, lists the findings under it and folds your text below them as the full review; a review with no submitted verdict is posted as not approving, so never skip it. An \`approve\` carrying a finding at or above the severity to address is downgraded to \`request_changes\` and the tool's ack says so — approve only when every finding sits below the level. Do not write "LGTM" in your own text — the verdict line carries it.`;
363
380
 
364
381
  // The diff-gated spec review (docs/reference/specs/agent-review.md item 14) and
365
382
  // the test guard under it (item 16; specs-coverage.md item 6), one text for
@@ -416,8 +433,9 @@ ${NOTEPAD}
416
433
 
417
434
  ${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
418
435
 
436
+ ${BREVITY_RULE}
419
437
  ${FENCED_CONTENT_RULE}
420
- Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
438
+ ${REVIEW_FINAL_MESSAGE}`;
421
439
 
422
440
  // Resident-path variant for review (docs/reference/specs/resident-repos.md): same
423
441
  // gather-once discipline, but against the ready worktree with git — the
@@ -449,8 +467,9 @@ ${NOTEPAD}
449
467
 
450
468
  ${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
451
469
 
470
+ ${BREVITY_RULE}
452
471
  ${FENCED_CONTENT_RULE}
453
- Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
472
+ ${REVIEW_FINAL_MESSAGE}`;
454
473
 
455
474
  // Seeded-sandbox variant for review (docs/reference/specs/execution.md item 26):
456
475
  // the gather-once discipline against a checkout that is already at the PR
@@ -483,8 +502,9 @@ ${NOTEPAD}
483
502
 
484
503
  ${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
485
504
 
505
+ ${BREVITY_RULE}
486
506
  ${FENCED_CONTENT_RULE}
487
- Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
507
+ ${REVIEW_FINAL_MESSAGE}`;
488
508
 
489
509
  // Research agent: no repo, no workspace — just web search + URL
490
510
  // reading, so a user can drop a link or ask a research question and get an
@@ -492,7 +512,7 @@ Your final message is posted to Slack. Lead with a one-line verdict, then the fi
492
512
  // fast and tool-less.
493
513
  const RESEARCH_SYSTEM = `You are Switchboard's research agent, answering a request from Slack.
494
514
 
495
- You have no workspace and cannot run commands or clone repos. Your tools: \`web_search\` (find sources), \`web_fetch\` (read a public URL — pages as text; image and PDF links come back as the image/document itself), and the GitHub tools — \`github_repos\` (the org repositories you can reach, private ones included), \`github_tree\` / \`github_file\` (browse and read their files at any ref), \`github_search_code\`, and \`github_issue_list\` / \`github_issue_get\`. They use Switchboard's own GitHub credential, so a private repo of ours is readable — never conclude a repo is inaccessible from a public-web 404; use the GitHub tools.
515
+ You have no workspace and cannot run commands or clone repos. Your tools: \`web_search\` (find sources), \`web_fetch\` (read a public URL — pages as text; image and PDF links come back as the image/document itself), and the GitHub tools — \`github_repos\` (the org repositories you can reach, private ones included), \`github_tree\` / \`github_file\` (browse and read their files at any ref), \`github_search_code\`, \`github_issue_list\` / \`github_issue_get\`, and \`github_actions_run\` / \`github_actions_job_log\` (a GitHub Actions run, its jobs, a job's errors and log tail). They use Switchboard's own GitHub credential, so a private repo of ours is readable — never conclude a repo is inaccessible from a public-web 404; use the GitHub tools.
496
516
 
497
517
  How to work:
498
518
  1. If the user gave a URL, read it first — a github.com URL to one of our repos with github_file/github_tree (web_fetch cannot see private repos), anything else with web_fetch. If they asked about Switchboard or one of our repos, read the repo (README, AGENTS.md, \`docs/reference/specs/*.md\` specs, the code) with github_tree / github_file / github_search_code before answering. For a general question, web_search for good sources, then web_fetch the most promising 1-3 to read the actual content — don't answer from snippets alone when the page is readable.
@@ -501,6 +521,7 @@ How to work:
501
521
 
502
522
  ${statusCardRule('"Search the sources", "Write the answer"')}
503
523
 
524
+ ${BREVITY_RULE}
504
525
  ${FENCED_CONTENT_RULE}
505
526
  Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks). Your final message is posted to Slack — lead with the answer, then supporting detail and sources.`;
506
527
 
@@ -510,10 +531,11 @@ Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks
510
531
  // everyday asks ("open an issue on X", "what does our resident system do?",
511
532
  // "what's in that link?") are answered here instead of bounced to a directive.
512
533
  const GENERAL_SYSTEM = `You are Switchboard, a helpful assistant answering requests from Slack.
534
+ ${BREVITY_RULE}
513
535
  ${FENCED_CONTENT_RULE}
514
536
  Answer directly and concisely. Use Slack-friendly formatting (no markdown headers; use *bold*, bullets, and code blocks).
515
537
 
516
- Your tools work without a workspace: the GitHub tools — \`github_repos\` (the org repositories you can reach), \`github_tree\` / \`github_file\` / \`github_search_code\` (browse, read, search their code and docs, private repos included), \`github_issue_list\` / \`github_issue_get\` (read issues), \`github_issue_create\` / \`github_issue_update\` / \`github_issue_comment\` / \`github_issue_delete\` (act on issues) — and \`web_fetch\` (read a public URL). Use them: when the user names a repo loosely ("the switchboard app"), resolve it with github_repos (or the thread) rather than asking; when asked about one of our repos, read it before answering. Report exactly what a tool did (issue number + URL) — never claim an action you did not perform, and never fabricate file contents, URLs, or command output.
538
+ Your tools work without a workspace: the GitHub tools — \`github_repos\` (the org repositories you can reach), \`github_tree\` / \`github_file\` / \`github_search_code\` (browse, read, search their code and docs, private repos included), \`github_issue_list\` / \`github_issue_get\` (read issues), \`github_actions_run\` / \`github_actions_job_log\` (a GitHub Actions run, its jobs, and a failed job's errors and log tail — "why did this run fail?"), \`github_issue_create\` / \`github_issue_update\` / \`github_issue_comment\` / \`github_issue_delete\` (act on issues) — and \`web_fetch\` (read a public URL). Use them: when the user names a repo loosely ("the switchboard app"), resolve it with github_repos (or the thread) rather than asking; when asked about one of our repos, read it before answering. Report exactly what a tool did (issue number + URL) — never claim an action you did not perform, and never fabricate file contents, URLs, or command output.
517
539
 
518
540
  ${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.
519
541
 
@@ -548,6 +570,7 @@ ${statusCardRule('"Clone and install", "Time the full suite"')}
548
570
 
549
571
  ${NOTEPAD}
550
572
 
573
+ ${BREVITY_RULE}
551
574
  ${FENCED_CONTENT_RULE}
552
575
  Report outcomes faithfully: a check you could not run is "could not check", never a guess. Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks — render the claim table as aligned rows inside a code block). Your final message is posted to Slack: lead with the overall verdict in one line, then the claim table, then what a follow-up should do.`;
553
576
 
@@ -576,7 +599,7 @@ function conductorSystem(siblings: readonly AgentDef[]): string {
576
599
  );
577
600
  return `You are Switchboard's conductor: you coordinate other runs instead of doing the work yourself, answering a request from Slack.
578
601
 
579
- You have no workspace and no shell. Your tools: \`spawn_run\` (start a child run), \`send_to_run\` (steer a live child: your text reaches it as a follow-up at its next step), \`await_runs\` (wait for your children to end and get each end — its status and final reply — back as data), \`list_runs\` (the runs you may see — your own children by default), \`get_run_status\` (one run: whether it is running, what it is doing, and its final reply once it finished), the GitHub reads — \`github_repos\`, \`github_tree\` / \`github_file\` (browse and read our repositories), \`github_search_code\`, \`github_issue_list\` / \`github_issue_get\` — \`web_fetch\` (read a public URL), and \`update_status\`.
602
+ You have no workspace and no shell. Your tools: \`spawn_run\` (start a child run), \`send_to_run\` (steer a live child: your text reaches it as a follow-up at its next step), \`await_runs\` (wait for your children to end and get each end — its status and final reply — back as data), \`list_runs\` (the runs you may see — your own children by default), \`get_run_status\` (one run: whether it is running, what it is doing, and its final reply once it finished), the GitHub reads — \`github_repos\`, \`github_tree\` / \`github_file\` (browse and read our repositories), \`github_search_code\`, \`github_issue_list\` / \`github_issue_get\`, \`github_actions_run\` / \`github_actions_job_log\` (an Actions run, its jobs, a job's log) — \`web_fetch\` (read a public URL), and \`update_status\`.
580
603
 
581
604
  WHAT A CHILD IS. A child is an ordinary Switchboard run started as the person who asked you — exactly the run they could start by hand with \`agent:<preset>\` — in a thread of its own in this channel, visible to everyone there, with its own status card and run page, and under their permissions: a preset they may not run, a repository they may not use, or a profile a boundary caps is refused in the child's thread, and the refusal comes back to you as the tool result naming the gate. Children cannot spawn children. You may have a few live at once (the deployment's \`spawn.maxChildren\`, three by default); a spawn past the cap is refused until one finishes. A child's wall clock is capped by what is left of yours.
582
605
 
@@ -593,6 +616,7 @@ A CHILD IS ITS THREAD. People can reply in a child's thread. While the child run
593
616
 
594
617
  Maintain the user-facing status card with the update_status tool: one item per child (○ pending, ✱ running, ✓ finished — only once await_runs or get_run_status said so).
595
618
 
619
+ ${BREVITY_RULE}
596
620
  ${FENCED_CONTENT_RULE}
597
621
  Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks). Your final message is posted to Slack: lead with the outcome, then one line per child — its preset, its thread, its status and its result in a sentence — and what is still running, if anything.`;
598
622
  }
@@ -42,6 +42,24 @@ export const CONFIRMATION_TTL_MS = 10 * MINUTE_MS;
42
42
  * inside it runs to the attach's own timeout. The run's own stop ends any of
43
43
  * it at once. Read by the executor factory. */
44
44
  export const FIRST_ATTACH_WAIT_MS = MINUTE_MS;
45
+ /** The least an attach REQUEST is opened with (docs/reference/specs/execution.md
46
+ * item 9): a re-attach that recreates the worktree clones from the resident's
47
+ * local mirror and may install deps, so a bound under this could not finish
48
+ * and a request cut mid-clone is struck as a rollout — the strike this floor
49
+ * exists to stop counting. A run with less than this left past its write-up
50
+ * reserve opens no attach at all (`attachBoundWithinRun`: `exhausted`). Half
51
+ * a minute: the one-second floor a COMMAND keeps (`BASH_TIMEOUT_MIN_MS`) is
52
+ * the model's to choose; an attach is opened on the run's behalf. */
53
+ export const ATTACH_REQUEST_MIN_MS = 30_000;
54
+ /** How long a harness's one more command waits for a container that is down
55
+ * under a live run to answer (docs/reference/specs/harness-pi.md item 16):
56
+ * the platform rebuilt a replaced resident container in about a minute, and
57
+ * a run with work in flight should not hang on a container that is not coming
58
+ * back for as long as a fresh run may wait for its first container
59
+ * (`SANDBOX_START_WAIT_MAX_MS`, ten minutes, execution.md item 23 — a wait
60
+ * before anything ran). A wait that runs out decides nothing; the failure
61
+ * that opened the question stands. */
62
+ export const HARNESS_PROBE_WAIT_MS = 5 * MINUTE_MS;
45
63
 
46
64
  /** The presets that run the tool loop, and the one pipeline preset. */
47
65
  export const LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"] as const;
@@ -50,12 +68,18 @@ export type Preset = LoopPreset | "ship";
50
68
 
51
69
  /** What each preset asks for when nothing above it is tighter, in minutes. The
52
70
  * pipeline's (`ship`) is the wall clock one segment of its loop runs under;
53
- * 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). */
54
78
  export const ASKS: Readonly<Record<Preset, number>> = {
55
79
  general: 5,
56
- coding: 45,
80
+ coding: 90,
57
81
  review: 25,
58
- ship: 120,
82
+ ship: 240,
59
83
  research: 8,
60
84
  explore: 120,
61
85
  conductor: 120,
@@ -69,10 +93,13 @@ export type RoundKind = "coding" | "review" | "fix" | "merge";
69
93
  * falls under the floor is refused rather than dispatched: a two-minute
70
94
  * review or fix costs an attach and a model turn and finishes nothing.
71
95
  * Review's is the ledger's 90th percentile of completed reviews (5.1 min over
72
- * 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. */
73
100
  export const PRESET_FLOORS: Readonly<Record<LoopPreset, number>> = {
74
101
  general: 2,
75
- coding: 10,
102
+ coding: 15,
76
103
  review: 5,
77
104
  research: 3,
78
105
  explore: 15,
@@ -369,6 +396,9 @@ export type GrantSource = "org" | "channel" | "user" | "run";
369
396
  export const DEFAULT_GRANT: Grant = { renewals: 0 };
370
397
 
371
398
  /** The most renewals one request may carry: thirteen segments of the ship
372
- * preset's ask are a day, the longest problem a person hands over in one
373
- * 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. */
374
404
  export const GRANT_RENEWALS_MAX = 12;
@@ -9,7 +9,7 @@
9
9
  // routes decide on.
10
10
  //
11
11
  import type { Grant, GrantSource } from "../budgets.js";
12
- import type { AddressSeverity, AddressSeveritySource } from "../ship/coordinator.js";
12
+ import { isAddressSeverity, type AddressSeverity, type AddressSeveritySource } from "../ship/coordinator.js";
13
13
 
14
14
  // A coordinator is a Workflow instance in the shim Worker whose children are
15
15
  // ordinary `dispatch()` runs as the requesting user. It holds no credential of
@@ -215,6 +215,12 @@ export interface UnitSegment {
215
215
  * reaches it — its thread, its pull request, the round boundaries the card
216
216
  * drew and how it ended. One row a person can read for "what happened to this
217
217
  * unit". */
218
+ /** What the severity gate caught on a round: the level in force and the gated findings as `id (severity)`. */
219
+ export interface RoundGate {
220
+ level: AddressSeverity;
221
+ findings: string[];
222
+ }
223
+
218
224
  export interface CoordinatorUnit {
219
225
  instanceId: string;
220
226
  /** `U<n>` as the plan spells it. */
@@ -251,8 +257,12 @@ export interface CoordinatorUnit {
251
257
  * renews the same segment twice. `from` is the sha the segment continues
252
258
  * from, `runId` the coding run whose write-up briefs it. */
253
259
  segments?: UnitSegment[];
254
- /** The round boundaries the coordinator reported, oldest first (the `ship_round` vocabulary). */
255
- rounds: Array<{ index: number; agent: string; outcome: string; at: number }>;
260
+ /** The round boundaries the coordinator reported, oldest first (the `ship_round`
261
+ * vocabulary). `gate` rides an approve the machine's severity check caught
262
+ * carrying a finding at or above the level in force ([agent-ship](../../../docs/reference/specs/agent-ship.md)
263
+ * item 9) — a mismatch to be seen, since the child's parser holds an approve
264
+ * to the same level. */
265
+ rounds: Array<{ index: number; agent: string; outcome: string; at: number; gate?: RoundGate }>;
256
266
  /** How the unit ended: the ending's kind and the thread's report, when it has. */
257
267
  ending?: { kind: string; report: string; at: number };
258
268
  startedAt?: number;
@@ -303,6 +313,13 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
303
313
  }
304
314
 
305
315
  /** Structural check on a unit row from outside the process. */
316
+ /** A round's gate: a level on the ladder and string findings. */
317
+ const isRoundGate = (v: unknown): boolean =>
318
+ isObject(v) &&
319
+ isAddressSeverity(v.level) &&
320
+ Array.isArray(v.findings) &&
321
+ v.findings.every((f) => typeof f === "string");
322
+
306
323
  const isSegment = (v: unknown): boolean =>
307
324
  isObject(v) &&
308
325
  typeof v.index === "number" &&
@@ -328,7 +345,15 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
328
345
  if (
329
346
  !Array.isArray(r.rounds) ||
330
347
  r.rounds.length > MAX_ROUNDS ||
331
- !r.rounds.every((x) => isObject(x) && isFinite(x.index) && isText(x.agent) && isText(x.outcome) && isFinite(x.at))
348
+ !r.rounds.every(
349
+ (x) =>
350
+ isObject(x) &&
351
+ isFinite(x.index) &&
352
+ isText(x.agent) &&
353
+ isText(x.outcome) &&
354
+ isFinite(x.at) &&
355
+ (x.gate === undefined || isRoundGate(x.gate)),
356
+ )
332
357
  )
333
358
  return false;
334
359
  if (
@@ -539,7 +539,13 @@ async function runUnit(
539
539
  state = transition.state;
540
540
  for (const note of transition.notes) {
541
541
  if (note.type === "round") {
542
- const body = { ...tag, index: note.index, agent: note.agent, outcome: note.outcome };
542
+ const body = {
543
+ ...tag,
544
+ index: note.index,
545
+ agent: note.agent,
546
+ outcome: note.outcome,
547
+ ...(note.gate !== undefined ? { gate: note.gate } : {}),
548
+ };
543
549
  await step.do(`${prefix}/note/${++notes}`, STEP_CONFIG, () => call(bot, "round", body));
544
550
  } else {
545
551
  // A merge_ready ending names the pull request as it is at the APPROVED