@coreplane/switchboard 1.251.0 → 1.253.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 (167) hide show
  1. package/dist/assets/config/config.example.yaml +28 -2
  2. package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
  3. package/dist/assets/deploy/cloudflare/worker.ts +6 -3
  4. package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
  5. package/dist/assets/deploy/cloudflare-memory/worker.ts +223 -38
  6. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
  7. package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
  8. package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
  9. package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
  10. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
  11. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
  12. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
  13. package/dist/assets/deploy/profile.example.json +1 -1
  14. package/dist/assets/package-lock.json +3 -3
  15. package/dist/assets/package.json +3 -2
  16. package/dist/assets/project.json +13 -9
  17. package/dist/assets/source.json +3 -3
  18. package/dist/assets/src/agents/registry.ts +5 -5
  19. package/dist/assets/src/core/authz/policy.ts +8 -0
  20. package/dist/assets/src/core/budgets.ts +39 -0
  21. package/dist/assets/src/core/coordinator/contract.ts +93 -1
  22. package/dist/assets/src/core/coordinator/driver.ts +236 -21
  23. package/dist/assets/src/core/drain.ts +50 -0
  24. package/dist/assets/src/core/memory/engine.ts +123 -4
  25. package/dist/assets/src/core/memory/scorer.ts +12 -4
  26. package/dist/assets/src/core/memory/types.ts +73 -12
  27. package/dist/assets/src/core/modelCard.ts +32 -4
  28. package/dist/assets/src/core/modelPricing.ts +111 -1
  29. package/dist/assets/src/core/modelProxy/usage.ts +88 -0
  30. package/dist/assets/src/core/modelRegistry.ts +15 -1
  31. package/dist/assets/src/core/refusal.ts +4 -7
  32. package/dist/assets/src/core/reviewVerdict.ts +4 -0
  33. package/dist/assets/src/core/runEvents.ts +82 -3
  34. package/dist/assets/src/core/runFriction.ts +8 -2
  35. package/dist/assets/src/core/runLedger/types.ts +11 -0
  36. package/dist/assets/src/core/runMetrics.ts +237 -0
  37. package/dist/assets/src/core/runRecord.ts +46 -0
  38. package/dist/assets/src/core/runUsage.ts +67 -13
  39. package/dist/assets/src/core/schedules.ts +3 -0
  40. package/dist/assets/src/core/ship/contract.ts +34 -14
  41. package/dist/assets/src/core/ship/coordinator.ts +485 -55
  42. package/dist/assets/src/core/ship/renewal.ts +10 -5
  43. package/dist/assets/src/core/trace/attrs.ts +24 -0
  44. package/dist/assets/src/core/types.ts +5 -5
  45. package/dist/assets/src/core/verbosity.ts +48 -0
  46. package/dist/assets/src/deploy/liveGate.ts +40 -13
  47. package/dist/assets/src/deploy/profile.ts +16 -0
  48. package/dist/assets/src/deploy/restart.ts +11 -12
  49. package/dist/assets/src/execution/residentDepCache.ts +50 -1
  50. package/dist/assets/src/execution/residentDepsStore.ts +40 -2
  51. package/dist/assets/src/execution/residentRefresh.ts +55 -3
  52. package/dist/assets/src/execution/residentSteps.ts +4 -0
  53. package/dist/assets/src/execution/sandboxErrors.ts +8 -0
  54. package/dist/assets/web/dist/.vite/manifest.json +444 -430
  55. package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
  56. package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
  57. package/dist/assets/web/dist/assets/{DeliveryPage-DUXd-Sl-.js → DeliveryPage-C7jaEJip.js} +1 -1
  58. package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
  59. package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
  60. package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
  61. package/dist/assets/web/dist/assets/{PendingTurnRow-DDhMhrI7.js → PendingTurnRow-C-pUgyyX.js} +1 -1
  62. package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
  63. package/dist/assets/web/dist/assets/{ResidentDetailPage-BnEoOnGQ.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
  64. package/dist/assets/web/dist/assets/{ResidentsIndexPage-Dxpgf-l-.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
  65. package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
  66. package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
  67. package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
  68. package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
  69. package/dist/assets/web/dist/assets/{ScheduledPage-B_GgeJrb.js → ScheduledPage-C8FYoZy5.js} +1 -1
  70. package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
  71. package/dist/assets/web/dist/assets/{SettingsPage-BXX4R113.js → SettingsPage-CkYIhLUl.js} +1 -1
  72. package/dist/assets/web/dist/assets/{StatusDot-BOaw8le9.js → StatusDot-C0TUgnC1.js} +1 -1
  73. package/dist/assets/web/dist/assets/{Tooltip-DYZZ4l4V.js → Tooltip-CN5cZdl3.js} +1 -1
  74. package/dist/assets/web/dist/assets/{UnitRoutePage-BaSW5Odq.js → UnitRoutePage-D_EtAl8r.js} +1 -1
  75. package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
  76. package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
  77. package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
  78. package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
  79. package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
  80. package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
  81. package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
  82. package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
  83. package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
  84. package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
  85. package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
  86. package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
  87. package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
  88. package/dist/assets/web/dist/assets/{dist-BCVXeBJ9.js → dist-B1Mf2JbM.js} +2 -2
  89. package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
  90. package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
  91. package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
  92. package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
  93. package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
  94. package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
  95. package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
  96. package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
  97. package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
  98. package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
  99. package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
  100. package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
  101. package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
  102. package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
  103. package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
  104. package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
  105. package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
  106. package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
  107. package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
  108. package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
  109. package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
  110. package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
  111. package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
  112. package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
  113. package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
  114. package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
  115. package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
  116. package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
  117. package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
  118. package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
  119. package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
  120. package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
  121. package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
  122. package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
  123. package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
  124. package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
  125. package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
  126. package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
  127. package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
  128. package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
  129. package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
  130. package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
  131. package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
  132. package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
  133. package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
  134. package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
  135. package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
  136. package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
  137. package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
  138. package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
  139. package/dist/assets/web/dist/assets/sseReplay-BYo2EZKy.js +11 -0
  140. package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
  141. package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
  142. package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
  143. package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
  144. package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
  145. package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
  146. package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
  147. package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
  148. package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
  149. package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
  150. package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
  151. package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
  152. package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
  153. package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
  154. package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
  155. package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
  156. package/dist/cli.js +5623 -2455
  157. package/package.json +1 -1
  158. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
  159. package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +0 -2
  160. package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +0 -1
  161. package/dist/assets/web/dist/assets/RunRoutePage-9klVWhSF.js +0 -9
  162. package/dist/assets/web/dist/assets/RunsIndexPage-BplMIgaw.js +0 -1
  163. package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +0 -1
  164. package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +0 -1
  165. package/dist/assets/web/dist/assets/main-Dkcbtu3u.js +0 -28
  166. package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
  167. package/dist/assets/web/dist/assets/sseReplay-DPwdsaok.js +0 -9
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ // The cold sandbox's process supervisor, run by tini (PID 1): keeps the
3
+ // container alive across a crash of the SDK's container server
4
+ // (/container-server/sandbox). That server calls process.exit(1) on any
5
+ // uncaught exception, and the base image runs it as tini's one child — so one
6
+ // uncaught error ended the whole container: /workspace gone, a detached pi
7
+ // with it, every later /exec a failure until the platform noticed. Started
8
+ // here as a child instead, the server's exit is a restart a second later; the
9
+ // disk, the detached processes and the Durable Object's view of a running
10
+ // container all survive, and the next command starts a fresh process on the
11
+ // new server (docs/reference/specs/execution.md item 21).
12
+ //
13
+ // A stop from the platform is SIGTERM, which tini forwards here: forwarded to
14
+ // the server, and nothing starts again — a stop that lands during the pause
15
+ // between two starts still ends here — and the server's own exit status
16
+ // becomes this process's (the shell convention, 128 + the signal's number,
17
+ // when a signal ended it). A terminal's INT is forwarded as TERM too: one
18
+ // stop path, whatever the signal. A server that cannot stay up —
19
+ // QUICK_EXIT_LIMIT exits in a row, each within QUICK_EXIT_SECS of starting —
20
+ // ends the container with the last status instead of looping forever.
21
+ //
22
+ // Dependency-free Node ESM on the Node the image ships; `supervise` takes its
23
+ // process factory, clock, sleep and signal source as arguments, so
24
+ // src/deploy/sandboxRuntimeSupervisor.test.ts drives it with a fake child and
25
+ // a fake clock — no real process anywhere in the tests.
26
+ import { spawn as nodeSpawn } from "node:child_process";
27
+ import { constants } from "node:os";
28
+ import { pathToFileURL } from "node:url";
29
+ import { setTimeout as sleepFor } from "node:timers/promises";
30
+
31
+ /** The SDK's container server, unless SANDBOX_RUNTIME names another. */
32
+ export const DEFAULT_RUNTIME = "/container-server/sandbox";
33
+ /** An exit within this many seconds of starting counts as a quick exit. */
34
+ export const QUICK_EXIT_SECS = 10;
35
+ /** This many quick exits in a row end the supervisor with the last status. */
36
+ export const QUICK_EXIT_LIMIT = 5;
37
+ /** The pause between an exit and the next start. */
38
+ export const RESTART_PAUSE_MS = 1_000;
39
+
40
+ /** One child's end as a shell would report it: its own exit code; 128 plus
41
+ * the signal's number when a signal ended it; 127 when it never started. */
42
+ export function endCode(code, signal) {
43
+ if (typeof code === "number") return code;
44
+ if (signal) return 128 + (constants.signals[signal] ?? 0);
45
+ return 127;
46
+ }
47
+
48
+ /**
49
+ * Start the runtime and start it again whenever it exits, until a stop signal
50
+ * or the quick-exit limit ends the loop; resolves with the exit status the
51
+ * whole process should carry. Every effect is an injected dependency:
52
+ * `spawn(runtime, args)` returns the child (a `kill(signal)` method and
53
+ * `exit`/`error` events are all it needs), `now()` is the clock in
54
+ * milliseconds, `sleep(ms)` the pause, `log(line)` the stderr line, and
55
+ * `signals` the emitter whose SIGTERM/SIGINT mean stop.
56
+ */
57
+ export async function supervise({ runtime, args, spawn, now, sleep, log, signals }) {
58
+ let child;
59
+ let stopping = false;
60
+ let quick = 0;
61
+ let code;
62
+ const stop = () => {
63
+ stopping = true;
64
+ // Always TERM, whichever signal the stop arrived as: the server has one
65
+ // shutdown path and a terminal's INT should take it too.
66
+ if (child) child.kill("SIGTERM");
67
+ };
68
+ signals.on("SIGTERM", stop);
69
+ signals.on("SIGINT", stop);
70
+ try {
71
+ for (;;) {
72
+ const started = now();
73
+ child = spawn(runtime, args);
74
+ const ended = new Promise((resolve) => {
75
+ child.once("exit", (c, s) => resolve(endCode(c, s)));
76
+ // A runtime that cannot start (the path is wrong) never emits `exit`;
77
+ // 127 is what a shell would say, and the quick-exit limit ends the loop.
78
+ child.once("error", () => resolve(endCode(null, null)));
79
+ });
80
+ // A stop that landed before this start found no child to forward to;
81
+ // the one it missed gets it here.
82
+ if (stopping) child.kill("SIGTERM");
83
+ code = await ended;
84
+ child = undefined;
85
+ if (stopping) return code;
86
+ quick = now() - started < QUICK_EXIT_SECS * 1000 ? quick + 1 : 0;
87
+ if (quick >= QUICK_EXIT_LIMIT) {
88
+ log(
89
+ `sandbox-runtime-supervisor: the runtime exited with status ${code}, ` +
90
+ `${quick} times in a row within ${QUICK_EXIT_SECS}s of starting; giving up`,
91
+ );
92
+ return code;
93
+ }
94
+ log(`sandbox-runtime-supervisor: the runtime exited with status ${code}; starting it again`);
95
+ await sleep(RESTART_PAUSE_MS);
96
+ // A stop that landed during the pause: the last status is the answer,
97
+ // and nothing starts again.
98
+ if (stopping) return code;
99
+ }
100
+ } finally {
101
+ signals.removeListener("SIGTERM", stop);
102
+ signals.removeListener("SIGINT", stop);
103
+ }
104
+ }
105
+
106
+ // Run only when executed directly (tini runs this file), not when imported by
107
+ // the tests.
108
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
109
+ const code = await supervise({
110
+ runtime: process.env.SANDBOX_RUNTIME || DEFAULT_RUNTIME,
111
+ args: process.argv.slice(2),
112
+ spawn: (runtime, args) => nodeSpawn(runtime, args, { stdio: "inherit" }),
113
+ now: () => Date.now(),
114
+ sleep: sleepFor,
115
+ log: (line) => process.stderr.write(line + "\n"),
116
+ signals: process,
117
+ });
118
+ process.exit(code);
119
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "The deployment profile: where THIS installation runs. Copy to deploy/profile.json and fill it in (or run `switchboard deploy init`). The account is your Cloudflare account id; every hostname must be under `zone`, a zone in that account, unless the Worker names its own `zone` (also in the account); `workers.bot` is the one required Worker — leave `memory`, `resident` or `sandbox` out and `deploy plan` has no step for them (a bot-only profile is a one-step plan; without `memory` the config is not pushed anywhere). The project's docs site is not a Worker of an installation: it is the project's website, deployed by the project's own CI from project.json, so there is no `docs` entry. `configSource` is where `deploy all` reads the bot's runtime config from before building the image — a path, `github://owner/repo/path@ref` (needs CONFIG_REPO_TOKEN), or `op://Vault/Item/field` (needs OP_SERVICE_ACCOUNT_TOKEN); `secretsSource` is where `secrets put` reads values from — a directory of <NAME> files (the default when absent) or `op://Vault/Item`. `images` is where the bot, resident and sandbox container images come from: `registry` — the release's published images, copied once per version into your account registry by `deploy all` itself (or `deploy images` ahead of it) over HTTPS and referenced from there — no Docker anywhere, a Cloudflare API token with Containers Edit in CLOUDFLARE_API_TOKEN for the copy (an installation deploying published images; what `init` writes from the published package); or `build` (the default when absent) — each Worker's Dockerfile, built by wrangler where `deploy all` runs (a checkout; the project's own production). `artifacts` (optional) names the R2 bucket a run's files move through (docs/reference/specs/execution.md item 20): with it the bot Worker binds the bucket and `deploy` creates it before the upload (the credential then needs Workers R2 Storage: Edit); the bot's runtime config `artifacts.r2.bucket` must say the same name. `deploy plan` reads this example when no profile exists; `deploy all` refuses it.",
2
+ "$comment": "The deployment profile: where THIS installation runs. Copy to deploy/profile.json and fill it in (or run `switchboard deploy init`). The account is your Cloudflare account id; every hostname must be under `zone`, a zone in that account, unless the Worker names its own `zone` (also in the account); `workers.bot` is the one required Worker — leave `memory`, `resident` or `sandbox` out and `deploy plan` has no step for them (a bot-only profile is a one-step plan; without `memory` the config is not pushed anywhere). The project's docs site is not a Worker of an installation: it is the project's website, deployed by the project's own CI from project.json, so there is no `docs` entry. `configSource` is where `deploy all` reads the bot's runtime config from before building the image — a path, `github://owner/repo/path@ref` (needs CONFIG_REPO_TOKEN), or `op://Vault/Item/field` (needs OP_SERVICE_ACCOUNT_TOKEN); `secretsSource` is where `secrets put` reads values from — a directory of <NAME> files (the default when absent) or `op://Vault/Item`. `images` is where the bot, resident and sandbox container images come from: `registry` — the release's published images, copied once per version into your account registry by `deploy all` itself (or `deploy images` ahead of it) over HTTPS and referenced from there — no Docker anywhere, a Cloudflare API token with Containers Edit in CLOUDFLARE_API_TOKEN for the copy (an installation deploying published images; what `init` writes from the published package); or `build` (the default when absent) — each Worker's Dockerfile, built by wrangler where `deploy all` runs (a checkout; the project's own production). `artifacts` (optional) names the R2 bucket a run's files move through (docs/reference/specs/execution.md item 20): with it the bot Worker binds the bucket and `deploy` creates it before the upload (the credential then needs Workers R2 Storage: Edit); the bot's runtime config `artifacts.r2.bucket` must say the same name. `metrics` (optional) names the Analytics Engine dataset every finished run's point is written to (docs/reference/specs/run-metrics.md): the state Worker's template then binds it as `RUN_METRICS` with the name beside it in `RUN_METRICS_DATASET` — nothing is created ahead of the deploy, the platform creates the dataset on first write — and the bot's runtime config `metrics.dataset` must say the same name (the bot warns at boot when the two differ). `deploy plan` reads this example when no profile exists; `deploy all` refuses it.",
3
3
  "account": "00000000000000000000000000000000",
4
4
  "zone": "example.com",
5
5
  "workers": {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.251.0",
3
+ "version": "1.253.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.251.0",
9
+ "version": "1.253.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.251.0",
20448
+ "version": "1.253.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.251.0",
3
+ "version": "1.253.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,7 +39,7 @@
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 pr-title:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run check:project-facts && npm run check:registry-drift && 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 check:deps-drift && 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 check:registry-drift && npm run agents:check && npm run clock:check && npm run screenshots:check",
43
43
  "ci:gate": "node scripts/ci-gate.mjs",
44
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",
@@ -65,6 +65,7 @@
65
65
  "skills:sync": "tsx scripts/skills-sync.ts",
66
66
  "skills:check": "tsx scripts/skills-sync.ts --check",
67
67
  "licenses:check": "node scripts/licenses-check.mjs",
68
+ "check:deps-drift": "node scripts/deps-drift-check.mjs",
68
69
  "docs:gen": "tsx scripts/docs-gen.ts",
69
70
  "docs:check": "tsx scripts/docs-gen.ts --check",
70
71
  "specs:check": "node scripts/specs-check.mjs",
@@ -33,7 +33,7 @@
33
33
  "$comment": "What each root npm script does and when to run it — the source of the Commands table in AGENTS.md (npm run agents:gen). A script without an entry fails agents:check, so a new script cannot ship undocumented.",
34
34
  "verify": {
35
35
  "does": "The whole gate: every root check, every workspace's verify, the site check — exactly what CI runs.",
36
- "when": "Before requesting review. ~4 min."
36
+ "when": "Local sessions, before requesting review. ~4 min. A pipeline agent leaves it to CI."
37
37
  },
38
38
  "verify:root": {
39
39
  "does": "The bot package's gate: consistency checks, typecheck, lint, format, tests, dist.",
@@ -45,11 +45,11 @@
45
45
  },
46
46
  "test": {
47
47
  "does": "The whole vitest suite (bot, web, the plain-Node Worker tests) from one entry, after `deploy:gen`.",
48
- "when": "Before pushing."
48
+ "when": "Local sessions, before pushing; a pipeline agent runs the changed set — the full suite is CI's."
49
49
  },
50
50
  "typecheck": {
51
51
  "does": "TypeScript over the bot and its scripts, no emit.",
52
- "when": "After type-level changes; `verify:root` runs it."
52
+ "when": "After type-level changes; `verify:root` runs it. A pipeline agent scopes it to the touched tsconfig."
53
53
  },
54
54
  "lint": {
55
55
  "does": "ESLint over the whole tree.",
@@ -100,7 +100,7 @@
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: grammar, type, scope, the `!` migration note.",
103
+ "does": "Judges one PR title as the changelog line: grammar, type, scope, `!`.",
104
104
  "when": "`-- \"feat(scope): …\"` before opening a PR; CI's `title` check."
105
105
  },
106
106
  "pr-title:gen": {
@@ -143,6 +143,10 @@
143
143
  "does": "Every production dependency's license is on the allowlist.",
144
144
  "when": "After adding a dependency."
145
145
  },
146
+ "check:deps-drift": {
147
+ "does": "Installed node_modules match the lockfile: drift fails by name.",
148
+ "when": "Part of `check:consistency`."
149
+ },
146
150
  "docs:gen": {
147
151
  "does": "Writes the generated regions of the reference docs from the command registry.",
148
152
  "when": "After changing a command, flag, route, or config key; part of `fix`."
@@ -160,12 +164,12 @@
160
164
  "when": "`-- --changed origin/main...HEAD [--test-guard]` before review; `-- --require` fails on an uncovered path; `-- --json` for machines."
161
165
  },
162
166
  "decisions:check": {
163
- "does": "Every record under `docs/decisions/` and `docs/plans/` has a valid `status`, a superseded one names its successor, and an accepted body changes only by an appended `## Amended` re-evaluation.",
167
+ "does": "Every record under `docs/decisions/` and `docs/plans/` has a valid `status`, a superseded one names its successor, an accepted body only gains an appended `## Amended`.",
164
168
  "when": "Part of `check:consistency`; a failing record is superseded or amended by appending, never edited."
165
169
  },
166
170
  "hygiene:check": {
167
171
  "does": "The public tree's imprint (company, people, trackers, plan ids, ids, dates) equals the recorded list, which only shrinks.",
168
- "when": "Part of `check:consistency`. New hit: rewrite the line or allow it by name in `scripts/public-hygiene.allow`; `-- --list <prefix>` shows the rest."
172
+ "when": "Part of `check:consistency`. New hit: rewrite the line or allow it in `scripts/public-hygiene.allow`."
169
173
  },
170
174
  "hygiene:gen": {
171
175
  "does": "Records the tree's remaining imprint after a scrub; refuses growth unless `-- --force`.",
@@ -180,7 +184,7 @@
180
184
  "when": "Part of `check:consistency`."
181
185
  },
182
186
  "clock:gen": {
183
- "does": "Regenerates both clock allowlists from the tree: wall-clock reads (empty) and duration literals outside `src/core/budgets.ts`.",
187
+ "does": "Regenerates both clock allowlists: wall-clock reads (empty) and duration literals outside `src/core/budgets.ts`.",
184
188
  "when": "Part of `fix`."
185
189
  },
186
190
  "clock:check": {
@@ -217,14 +221,14 @@
217
221
  },
218
222
  "screenshots:gen": {
219
223
  "does": "Renders the dashboard screenshots whose inputs changed, both themes, recording each surface's input hashes in `docs/public/screenshots/manifest/` (`--force`: all).",
220
- "when": "After a `web/` or fixture change, once `screenshots:check` names it; needs `npx playwright-core install chromium`, so it is not part of `fix`."
224
+ "when": "After a `web/` or fixture change, once `screenshots:check` names it; needs `npx playwright-core install chromium`, so not in `fix`."
221
225
  },
222
226
  "screenshots:check": {
223
227
  "does": "Each surface's inputs still hash to what its screenshots were rendered from — no browser.",
224
228
  "when": "Part of `check:consistency`."
225
229
  },
226
230
  "load": {
227
- "does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider|pi|route|door`.",
231
+ "does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider|pi|route|intake|door`.",
228
232
  "when": "Capacity receipts (docs/reference/specs/load-harness.md)."
229
233
  }
230
234
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.251.0",
3
- "commit": "95a98b3da9996ee6194236ec9c6b453c18aa6bcf",
4
- "builtAt": "2026-09-18T22:15:18.238Z"
2
+ "version": "1.253.0",
3
+ "commit": "c96eda5a0bced3e6e6347180846163cc19b28c49",
4
+ "builtAt": "2026-09-19T15:36:27.120Z"
5
5
  }
@@ -238,7 +238,7 @@ Keep notes with the \`notes\` tool: one short document, replaced whole each time
238
238
  // project's most expensive checks first; the rule is the runner's to hold, not
239
239
  // a line every requester remembers to paste. Stack-agnostic on purpose — the
240
240
  // classes are by duration, the project's own scripts and CI say which is which.
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.`;
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 and scoped to the changed set — the tests nearest your change, the touched project's typecheck, the changed files' formatting, never the whole tree: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. Every CI pipeline runs the tests, the types, the formatting and the full verification on your push, so you never run them again: you validate and fix your own change before pushing, at the changed-set scope. Passing the full test suite and the full typecheck is NOT part of your criteria: CI is that gate and the only place they run — on a shared machine they cost minutes that every other run pays for. 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. Beyond the changed set, use judgement about what this change needs rather than a checklist, fixing 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.`;
242
242
 
243
243
  const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
244
244
 
@@ -256,7 +256,7 @@ Workflow for shipping a PR:
256
256
  3. Implement the change. Match the surrounding code's style and conventions.
257
257
  4. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change.
258
258
  5. Commit with a clear message and push the branch — before any full suite, build or full verification.
259
- 6. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
259
+ 6. CI runs the full suite, the typecheck and the full verification on that push — you never run them yourself; read CI's result if it lands within your budget and fix forward with further commits and pushes.
260
260
  7. Call the submit_pr_description tool with the typed description object (content contract below) — every time, bringing forward the context you gained while implementing. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with \`gh\` or any API call.
261
261
  8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
262
262
 
@@ -302,7 +302,7 @@ Workflow for shipping a change:
302
302
  2. Implement the change. Match the surrounding code's style and conventions.
303
303
  3. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change (dependencies are already present).
304
304
  4. Commit with a clear message and push the branch with \`git push -u origin <branch>\` — before any full suite, build or full verification.
305
- 5. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
305
+ 5. CI runs the full suite, the typecheck and the full verification on that push — you never run them yourself; read CI's result if it lands within your budget and fix forward with further commits and pushes.
306
306
  6. Call the \`diff_digest\` tool to get a distilled summary of your change — per-file churn, totals, and risky-file flags. It is a distilled summary, not the raw diff: use it to shape the description you submit next — which files the Tour must walk, what belongs in risks.
307
307
  7. Call the submit_pr_description tool with the typed description object (content contract below) — every time. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with any API call.
308
308
  8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
@@ -345,7 +345,7 @@ Workflow for shipping a change:
345
345
  2. Implement the change. Match the surrounding code's style and conventions.
346
346
  3. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change (dependencies are already present).
347
347
  4. Commit with a clear message and push the branch with \`git push -u origin <branch>\` — before any full suite, build or full verification.
348
- 5. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
348
+ 5. CI runs the full suite, the typecheck and the full verification on that push — you never run them yourself; read CI's result if it lands within your budget and fix forward with further commits and pushes.
349
349
  6. Call the \`diff_digest\` tool to get a distilled summary of your change — per-file churn, totals, and risky-file flags. It is a distilled summary, not the raw diff: use it to shape the description you submit next — which files the Tour must walk, what belongs in risks.
350
350
  7. Call the submit_pr_description tool with the typed description object (content contract below) — every time. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with \`gh\` or any API call.
351
351
  8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
@@ -544,7 +544,7 @@ Your tools work without a workspace: the GitHub tools — \`github_repos\` (the
544
544
 
545
545
  ${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.
546
546
 
547
- You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web. Other Switchboard agents can, and a plain message reaches them by itself: for a code change or a pull request, say what you found and that the change is not yours to make, and that asking for it in plain words in a new message — "in acme/api: fix the failing login test" — starts the agent that makes the change, opens the PR and loops review; the same for a PR review ("review <PR URL>") and a web-research question ("compare X and Y on the web"). Never hand back a command or an \`agent:…\` line for the person to type: describe the ask in their words. Delete an issue only when the user explicitly asked to delete it (closing is an update).`;
547
+ You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web — and you cannot hand off, route, forward or start another run: never say you will hand off, route, forward or start anything, because you cannot start another agent's run. Other Switchboard agents can do those things, and a plain message reaches them by itself — but only a new top-level message in the channel reaches the router; a reply in this thread comes back to you. For a code change or a pull request, say what you found and that the change is not yours to make, and that asking for it in plain words in a new top-level message in the channel — not a reply in this thread — like "in acme/api: fix the failing login test" starts the agent that makes the change, opens the PR and loops review; the same for a PR review ("review <PR URL>") and a web-research question ("compare X and Y on the web"). Never hand back a command or an \`agent:…\` line for the person to type: describe the ask in their words. Delete an issue only when the user explicitly asked to delete it (closing is an update).`;
548
548
 
549
549
  // The explore agent (docs/reference/specs/agent-explore.md): a long, read-only
550
550
  // investigation — "run our CI locally and validate the claims", "how long does
@@ -141,6 +141,14 @@ export const POLICY: readonly Rule[] = [
141
141
  { action: "config:write", resource: "config-scope", resourceKind: "thread", when: [grant("config:write")] },
142
142
  // A user edits only their own scope.
143
143
  { action: "config:write", resource: "config-scope", resourceKind: "user", when: [IS_SELF] },
144
+ // The author binding's one trusted write (record 0062, authorization.md item
145
+ // 18): `config set user --github` binds a person to a GitHub login, so it is
146
+ // its own action — `identity:write`, held by `all` and by a named grants
147
+ // entry, never a baseline — not the channel-config right: a `config:write`
148
+ // holder could otherwise author commits as any unbound person. The is-self
149
+ // row above does not carry it: a person never binds themself (`config set me
150
+ // --github` is refused at the registry door before this table is asked).
151
+ { action: "identity:write", resource: "config-scope", resourceKind: "user", when: [grant("identity:write")] },
144
152
 
145
153
  // ── agents ───────────────────────────────────────────────────────────────
146
154
  // `agent:run:*` covers every agent through wildcard coverage (grants.ts).
@@ -28,6 +28,10 @@ export const minutesToMs = (minutes: number): number => minutes * MINUTE_MS;
28
28
  * as a literal where it is used. */
29
29
  export const DAY_MS = 24 * 60 * MINUTE_MS;
30
30
 
31
+ /** One calendar week in milliseconds: the bucket the live false-silence ratio
32
+ * is printed per (docs/reference/specs/load-harness.md item 20). */
33
+ export const WEEK_MS = 7 * DAY_MS;
34
+
31
35
  /** How long the confirmation a routed write is offered as stays pending
32
36
  * (docs/decisions/0044; docs/reference/specs/routing-and-config.md item 25):
33
37
  * the connect ticket's ten minutes. The bot passes it to the config object,
@@ -98,6 +102,13 @@ export function intakeReceiptRetentionMs(catchUpWindowMs: number): number {
98
102
  return Math.max(DAY_MS, window + minutesToMs(DRAIN.maxMinutes));
99
103
  }
100
104
 
105
+ /** The live false-silence join's recovery window (docs/reference/specs/load-harness.md
106
+ * item 20; docs/decisions/0058): a `silent` intake receipt counts as a false
107
+ * silence when the same person mentions the bot in the same thread within
108
+ * this window — the mention is the ignored person's recovery move, so a
109
+ * prompt one bounds the ratio the gate is judged by. */
110
+ export const INTAKE_RECOVERY_WINDOW_MS = 10 * MINUTE_MS;
111
+
101
112
  /** The presets that run the tool loop, and the one pipeline preset. */
102
113
  export const LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"] as const;
103
114
  export type LoopPreset = (typeof LOOP_PRESETS)[number];
@@ -153,17 +164,31 @@ export const FLOORS: Readonly<Record<RoundKind, number>> = {
153
164
  * when the remainder allows it. */
154
165
  export const MERGE_WAIT_ASK_MINUTES = 60;
155
166
 
167
+ /** A hosted ship parent's deadline margin past the pipeline's wall clock
168
+ * (record 0060): the row's `state.hosting.until` is the hand-off time plus
169
+ * the instance's `caps.maxMinutes` plus this hour, absorbing the runner's own
170
+ * scheduling slack before a reclaim closes the row `interrupted`. */
171
+ export const HOSTED_DEADLINE_MARGIN_MINUTES = 60;
172
+
156
173
  /** The ship runner's waits, in minutes: the margin a child's wait allows past
157
174
  * its budget, the slice a wait is asked in, the merge door's re-ask cadence,
158
175
  * and the pause before a busy spawn is asked again. */
159
176
  export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes: 5, busyRetryMinutes: 2 } as const;
160
177
 
178
+ /** The plane's table (docs/decisions/0064): how long a finished run stays on it. */
179
+ export const PLANE = { recentMinutes: 60 } as const;
180
+
161
181
  /** The named amounts a lease holds back, in minutes. Each stands for a step
162
182
  * every run or round pays: `provision` is attach and restore before the
163
183
  * harness's clock starts; `writeUp` is the final answer after the loop ends;
164
184
  * `commandWriteUp` is what the last command leaves for that answer; `execCall`
165
185
  * is the exec client's wait past a command's own budget; `bearerGrace` is how
166
186
  * far past the lease the model bearer stays valid for the last call's tail. */
187
+ /** The stall signal's window in minutes (docs/reference/specs/live-view.md
188
+ * item 32): a run's pace is counted over the last this-many minutes, and a
189
+ * run with no tool call for a whole window reads as stalled. */
190
+ export const PACE_WINDOW_MINUTES = 5;
191
+
167
192
  export const ALLOWANCES = {
168
193
  provision: 3,
169
194
  writeUp: 3,
@@ -439,3 +464,17 @@ export const DEFAULT_GRANT: Grant = { renewals: 0 };
439
464
  * hand-over is now `renewals: 5` — the longest problem a person hands over
440
465
  * in one message before a plan should carry it. */
441
466
  export const GRANT_RENEWALS_MAX = 12;
467
+
468
+ // ---- the idle unit (record 0051: a pipeline idles instead of ending) ----
469
+
470
+ /** `ship.idleDays`' default: zero — today's endings byte for byte. It moves to
471
+ * seven only in the unit that lands after the baseline and the billing answer
472
+ * are recorded (record 0051's plan), never before. */
473
+ export const IDLE_DAYS_DEFAULT = 0;
474
+ /** The most days one idle may wait: the platform's own `waitForEvent` ceiling
475
+ * (a year), which the wait must pass explicitly — its default is 24 hours. */
476
+ export const IDLE_DAYS_MAX = 365;
477
+ /** The most wakes one idle answers before it expires: a bound on how long a
478
+ * busy thread can keep an instance parked (read by the wait, this plan's
479
+ * fifth unit). */
480
+ export const IDLE_WAKES_MAX = 100;
@@ -8,8 +8,10 @@
8
8
  // parent record the spawn route reads the requester from, and the names the
9
9
  // routes decide on.
10
10
  //
11
- import type { Grant, GrantSource } from "../budgets.js";
11
+ import { IDLE_DAYS_MAX, type Grant, type GrantSource } from "../budgets.js";
12
+ import { isVerbosity, type Verbosity } from "../verbosity.js";
12
13
  import { isAddressSeverity, type AddressSeverity, type AddressSeveritySource } from "../ship/coordinator.js";
14
+ import { isHandoffShape, type Handoff } from "../ship/handoff.js";
13
15
 
14
16
  // A coordinator is a Workflow instance in the shim Worker whose children are
15
17
  // ordinary `dispatch()` runs as the requesting user. It holds no credential of
@@ -81,6 +83,22 @@ export function runFinishedEventType(runId: string): string {
81
83
  return `${RUN_FINISHED_EVENT_PREFIX}${runId}`;
82
84
  }
83
85
 
86
+ /** The deploy-roll signals a child's reattach path sends its parent
87
+ * (run-history item 47a): `child-interrupted-<runId>` — the child closed
88
+ * `interrupted` for a restart from its request, so the wait settles at once
89
+ * beside `run-finished-<runId>` and the round ends with the child's own
90
+ * reason; `child-resumed-<runId>` — the same run carries on after a roll, so
91
+ * the wait keeps waiting. Same alphabet as the finish event; the parent
92
+ * confirms either by `read-record` before it acts. */
93
+ export const CHILD_INTERRUPTED_EVENT_PREFIX = "child-interrupted-";
94
+ export function childInterruptedEventType(runId: string): string {
95
+ return `${CHILD_INTERRUPTED_EVENT_PREFIX}${runId}`;
96
+ }
97
+ export const CHILD_RESUMED_EVENT_PREFIX = "child-resumed-";
98
+ export function childResumedEventType(runId: string): string {
99
+ return `${CHILD_RESUMED_EVENT_PREFIX}${runId}`;
100
+ }
101
+
84
102
  /** A message into a thread an unfinished unit owns (record 0051's reply-as-event rule): one row
85
103
  * of the unit's event list, appended by the dispatcher, folded into the
86
104
  * unit's next coding spawn — or run as one fresh turn at the unit's end.
@@ -307,6 +325,16 @@ export interface CoordinatorInstance {
307
325
  * decision reads it. */
308
326
  grant?: Grant;
309
327
  grantSource?: GrantSource;
328
+ /** The request's verbosity (routing-and-config item 28), resolved once by
329
+ * the ship fork and written here beside `merge`: what the runner says in
330
+ * the unit threads it owns — the unit-ending report's asides and the
331
+ * segment lines are `verbose` material. Absent reads as `quiet`. */
332
+ verbosity?: Verbosity;
333
+ /** The idle flag (record 0051; agent-ship item 8): `ship.idleDays` as the
334
+ * ship fork resolved it (user > channel > org), written here beside the
335
+ * grant and answered by the plan route — above zero, an idling ending
336
+ * becomes `idle`; absent reads as zero, today's endings. */
337
+ idleDays?: number;
310
338
  /** The pipeline's caps as the profile gate clipped them: the rounds cap and the wall clock per unit. */
311
339
  caps?: { maxRounds: number; maxMinutes: number };
312
340
  /** The status card in the requesting thread, when the channel has one — what
@@ -328,6 +356,24 @@ export interface UnitSegment {
328
356
  at: number;
329
357
  }
330
358
 
359
+ /** The longest `why` an idle carries: an ending kind's name, never prose —
360
+ * the route refuses a longer one at the door, the row validator at the store. */
361
+ export const IDLE_WHY_MAX = 64;
362
+
363
+ /** The idle on a unit's row (record 0051): what a continuation needs, and the
364
+ * wakes spent against `IDLE_WAKES_MAX`. */
365
+ export interface UnitIdle {
366
+ /** The old ending kind the idle stands in for (`wall_clock_cap`, `stopped`, `continued`, …). */
367
+ why: string;
368
+ at: number;
369
+ renewalsLeft: number;
370
+ from?: string;
371
+ runId?: string;
372
+ spendUsd: number | null;
373
+ handoff?: Handoff;
374
+ wakes: number;
375
+ }
376
+
331
377
  /** One unit of the plan an instance runs (a task string is a generated plan of
332
378
  * one unit, `U1`): its branch, the units it waits on, and — as the runner
333
379
  * reaches it — its thread, its pull request, the round boundaries the card
@@ -376,6 +422,15 @@ export interface CoordinatorUnit {
376
422
  * renews the same segment twice. `from` is the sha the segment continues
377
423
  * from, `runId` the coding run whose write-up briefs it. */
378
424
  segments?: UnitSegment[];
425
+ /** The unit idles (record 0051; run-history item 50): written by `unit-end`
426
+ * on an `idle` ending in place of `ending`, so the unit stays unfinished
427
+ * and keeps owning its thread. `why` is the old kind, `renewalsLeft` what
428
+ * the grant still holds (the wake spends one — this plan's fifth unit),
429
+ * `from` the head a continuation opens from, `runId` the last coding
430
+ * child's run (absent when none ran), `spendUsd` the session's dollars
431
+ * (null once any run's cost is unknown), `handoff` that child's lists, and
432
+ * `wakes` how many wakes this idle has answered — zero at the write. */
433
+ idle?: UnitIdle;
379
434
  /** The round boundaries the coordinator reported, oldest first (the `ship_round`
380
435
  * vocabulary). `gate` rides an approve the machine's severity check caught
381
436
  * carrying a finding at or above the level in force ([agent-ship](../../../docs/reference/specs/agent-ship.md)
@@ -428,7 +483,13 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
428
483
  if (r.caps !== undefined && !(isObject(r.caps) && isFinite(r.caps.maxRounds) && isFinite(r.caps.maxMinutes)))
429
484
  return false;
430
485
  if (r.card !== undefined && !(isObject(r.card) && isText(r.card.channel) && isText(r.card.ts))) return false;
486
+ if (
487
+ r.idleDays !== undefined &&
488
+ !(Number.isInteger(r.idleDays) && (r.idleDays as number) >= 0 && (r.idleDays as number) <= IDLE_DAYS_MAX)
489
+ )
490
+ return false;
431
491
  if (!isOptionalText(r.runId) || !isOptionalText(r.label)) return false;
492
+ if (r.verbosity !== undefined && !isVerbosity(r.verbosity)) return false;
432
493
  if (r.attempt !== undefined && !(Number.isInteger(r.attempt) && (r.attempt as number) >= 2)) return false;
433
494
  return true;
434
495
  }
@@ -441,6 +502,17 @@ const isRoundGate = (v: unknown): boolean =>
441
502
  Array.isArray(v.findings) &&
442
503
  v.findings.every((f) => typeof f === "string");
443
504
 
505
+ const isUnitIdle = (v: unknown): boolean =>
506
+ isObject(v) &&
507
+ isText(v.why, IDLE_WHY_MAX) &&
508
+ isFinite(v.at) &&
509
+ isCount(v.renewalsLeft) &&
510
+ (v.from === undefined || isText(v.from)) &&
511
+ (v.runId === undefined || isText(v.runId)) &&
512
+ (v.spendUsd === null || isFinite(v.spendUsd)) &&
513
+ (v.handoff === undefined || isHandoffShape(v.handoff)) &&
514
+ isCount(v.wakes);
515
+
444
516
  const isSegment = (v: unknown): boolean =>
445
517
  isObject(v) &&
446
518
  typeof v.index === "number" &&
@@ -463,6 +535,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
463
535
  if (r.resume !== undefined && !isResume(r.resume)) return false;
464
536
  if (r.lastPush !== undefined && !isText(r.lastPush)) return false;
465
537
  if (r.segments !== undefined && (!Array.isArray(r.segments) || !r.segments.every(isSegment))) return false;
538
+ if (r.idle !== undefined && !isUnitIdle(r.idle)) return false;
466
539
  if (
467
540
  !Array.isArray(r.rounds) ||
468
541
  r.rounds.length > MAX_ROUNDS ||
@@ -527,6 +600,25 @@ export async function sendChecksSettled(
527
600
  }
528
601
  }
529
602
 
603
+ /** One deploy-roll signal to the child's parent (run-history item 47a): best
604
+ * effort like `sendRunFinished` — a refusal is answered, never thrown; a lost
605
+ * send costs the wait a chunk, never the round. */
606
+ export async function sendChildSignal(
607
+ workflow: WorkflowSender | undefined,
608
+ signal: { runId: string; parentInstanceId: string; kind: "interrupted" | "resumed"; reason: string; at: number },
609
+ ): Promise<RunFinishedSend> {
610
+ const { runId, parentInstanceId: instance, kind, reason, at } = signal;
611
+ if (!workflow) return { kind: "no-binding", instance };
612
+ const type = kind === "interrupted" ? childInterruptedEventType(runId) : childResumedEventType(runId);
613
+ try {
614
+ const handle = await workflow.get(instance);
615
+ await handle.sendEvent({ type, payload: { runId, kind, reason, at, parentInstanceId: instance } });
616
+ return { kind: "sent", instance, type };
617
+ } catch (err) {
618
+ return { kind: "failed", instance, type, reason: err instanceof Error ? err.message : String(err) };
619
+ }
620
+ }
621
+
530
622
  /** The one send per committed terminal record (run-history item 47). */
531
623
  export async function sendRunFinished(
532
624
  workflow: WorkflowSender | undefined,