@coreplane/switchboard 1.250.0 → 1.252.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 (79) hide show
  1. package/dist/assets/config/config.example.yaml +13 -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/worker.ts +108 -12
  5. package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
  6. package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
  7. package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
  8. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
  9. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
  10. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
  11. package/dist/assets/package-lock.json +3 -3
  12. package/dist/assets/package.json +3 -2
  13. package/dist/assets/project.json +13 -9
  14. package/dist/assets/source.json +3 -3
  15. package/dist/assets/src/agents/registry.ts +5 -5
  16. package/dist/assets/src/core/authz/policy.ts +4 -0
  17. package/dist/assets/src/core/authz/resource.ts +6 -2
  18. package/dist/assets/src/core/authz/types.ts +2 -0
  19. package/dist/assets/src/core/budgets.ts +22 -0
  20. package/dist/assets/src/core/coordinator/contract.ts +42 -0
  21. package/dist/assets/src/core/coordinator/driver.ts +150 -12
  22. package/dist/assets/src/core/drain.ts +50 -0
  23. package/dist/assets/src/core/memory/engine.ts +98 -0
  24. package/dist/assets/src/core/memory/scorer.ts +12 -4
  25. package/dist/assets/src/core/memory/types.ts +69 -12
  26. package/dist/assets/src/core/modelCard.ts +51 -7
  27. package/dist/assets/src/core/modelPricing.ts +111 -1
  28. package/dist/assets/src/core/modelProxy/usage.ts +88 -0
  29. package/dist/assets/src/core/modelRegistry.ts +15 -1
  30. package/dist/assets/src/core/provider.ts +49 -0
  31. package/dist/assets/src/core/refusal.ts +6 -6
  32. package/dist/assets/src/core/reviewVerdict.ts +4 -0
  33. package/dist/assets/src/core/runEvents.ts +51 -2
  34. package/dist/assets/src/core/runFriction.ts +7 -2
  35. package/dist/assets/src/core/runLedger/types.ts +23 -4
  36. package/dist/assets/src/core/runRecord.ts +16 -0
  37. package/dist/assets/src/core/runUsage.ts +67 -13
  38. package/dist/assets/src/core/schedules.ts +3 -0
  39. package/dist/assets/src/core/ship/contract.ts +45 -5
  40. package/dist/assets/src/core/ship/coordinator.ts +441 -48
  41. package/dist/assets/src/core/ship/renewal.ts +10 -5
  42. package/dist/assets/src/core/trace/attrs.ts +24 -0
  43. package/dist/assets/src/core/types.ts +5 -5
  44. package/dist/assets/src/core/verbosity.ts +48 -0
  45. package/dist/assets/src/deploy/liveGate.ts +40 -13
  46. package/dist/assets/src/deploy/restart.ts +11 -12
  47. package/dist/assets/src/execution/residentDepCache.ts +50 -1
  48. package/dist/assets/src/execution/residentDepsStore.ts +40 -2
  49. package/dist/assets/src/execution/residentRefresh.ts +55 -3
  50. package/dist/assets/src/execution/residentSteps.ts +4 -0
  51. package/dist/assets/src/execution/sandboxErrors.ts +8 -0
  52. package/dist/assets/web/dist/.vite/manifest.json +58 -52
  53. package/dist/assets/web/dist/assets/DeliveryPage-3ELQWM0r.js +1 -0
  54. package/dist/assets/web/dist/assets/{HomePage-AnycA57D.js → HomePage-BG_ok-K2.js} +2 -2
  55. package/dist/assets/web/dist/assets/{PendingTurnRow-BuRre8it.js → PendingTurnRow-ChCQOLgZ.js} +1 -1
  56. package/dist/assets/web/dist/assets/{ResidentDetailPage-Cb3sFkfj.js → ResidentDetailPage-C9y3nbo8.js} +1 -1
  57. package/dist/assets/web/dist/assets/{ResidentsIndexPage-BZ6n6UxF.js → ResidentsIndexPage-i1RG9e7g.js} +1 -1
  58. package/dist/assets/web/dist/assets/RunFoldRow-D3wVpzBa.js +1 -0
  59. package/dist/assets/web/dist/assets/RunRoutePage-B3IirUVi.js +9 -0
  60. package/dist/assets/web/dist/assets/RunsIndexPage-DiFmtGaJ.js +1 -0
  61. package/dist/assets/web/dist/assets/{ScheduledPage-CBUxbeqN.js → ScheduledPage-DvYwM2TE.js} +1 -1
  62. package/dist/assets/web/dist/assets/{SettingsPage-CBTnZ9Qv.js → SettingsPage-Bo6yCyXZ.js} +1 -1
  63. package/dist/assets/web/dist/assets/{StatusDot-BnRjWzFN.js → StatusDot-CAfS1AUi.js} +1 -1
  64. package/dist/assets/web/dist/assets/{Tooltip-Brge0wnd.js → Tooltip-tZoum_T-.js} +1 -1
  65. package/dist/assets/web/dist/assets/{UnitRoutePage-o6sLju16.js → UnitRoutePage-BmdOHwNn.js} +1 -1
  66. package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
  67. package/dist/assets/web/dist/assets/{dist-rgAhsmE-.js → dist-DfbEpHXR.js} +1 -1
  68. package/dist/assets/web/dist/assets/indexRow-BT0cPVRw.js +1 -0
  69. package/dist/assets/web/dist/assets/{main-CeRuGONy.js → main-5Gm_1Gv8.js} +2 -2
  70. package/dist/assets/web/dist/assets/sseReplay-DmyMXfRC.js +11 -0
  71. package/dist/cli.js +3319 -1015
  72. package/package.json +1 -1
  73. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
  74. package/dist/assets/web/dist/assets/DeliveryPage-CIfBiINK.js +0 -1
  75. package/dist/assets/web/dist/assets/RunFoldRow-BRXkjkgO.js +0 -1
  76. package/dist/assets/web/dist/assets/RunRoutePage-psSMI3fN.js +0 -9
  77. package/dist/assets/web/dist/assets/RunsIndexPage-68YT_RWt.js +0 -1
  78. package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +0 -1
  79. package/dist/assets/web/dist/assets/sseReplay-DXC7kGbN.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,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.250.0",
3
+ "version": "1.252.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.250.0",
9
+ "version": "1.252.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.250.0",
20448
+ "version": "1.252.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.250.0",
3
+ "version": "1.252.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.250.0",
3
- "commit": "4ac4b778bce59eddec48e214def56ad7aa7c62d2",
4
- "builtAt": "2026-09-18T19:28:39.901Z"
2
+ "version": "1.252.0",
3
+ "commit": "0c2a0a9cdd4afd1a0def0d43ed7a14928c32101e",
4
+ "builtAt": "2026-09-19T03:40:43.236Z"
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
@@ -135,6 +135,10 @@ export const POLICY: readonly Rule[] = [
135
135
  // scope from anywhere, a private one only from inside it, `unknown` never.
136
136
  { action: "config:read", resource: "config-scope", resourceKind: "channel", when: [grant("config:write")] },
137
137
  { action: "config:read", resource: "config-scope", resourceKind: "channel", when: [MEMBER_OF] },
138
+ // A THREAD's scope (`config set thread`, routing-and-config item 27): the
139
+ // channel-config right — whoever may set the channel may set a thread in it;
140
+ // never a baseline, so membership alone admits nobody.
141
+ { action: "config:write", resource: "config-scope", resourceKind: "thread", when: [grant("config:write")] },
138
142
  // A user edits only their own scope.
139
143
  { action: "config:write", resource: "config-scope", resourceKind: "user", when: [IS_SELF] },
140
144
 
@@ -31,14 +31,14 @@ export type AttributeName = Exclude<keyof ResourceAttributes, "visibility">;
31
31
  /** The kinds each kinded resource type takes. Types absent here are not kinded. */
32
32
  export const RESOURCE_KINDS: { readonly [T in ResourceType]?: readonly KindOf<T>[] } = {
33
33
  "memory-scope": ["org", "user", "repo", "channel"],
34
- "config-scope": ["channel", "user", "org"],
34
+ "config-scope": ["channel", "user", "thread", "org"],
35
35
  };
36
36
 
37
37
  /** A resource type, or `type/kind` for the kinded ones — the unit a rule row targets. */
38
38
  export type Target =
39
39
  | Exclude<ResourceType, "memory-scope" | "config-scope">
40
40
  | `memory-scope/${"org" | "user" | "repo" | "channel"}`
41
- | `config-scope/${"channel" | "user" | "org"}`;
41
+ | `config-scope/${"channel" | "user" | "thread" | "org"}`;
42
42
 
43
43
  export const TARGET_ATTRIBUTES: Readonly<Record<Target, readonly AttributeName[]>> = {
44
44
  run: ["channelId", "userId", "repo"],
@@ -50,6 +50,9 @@ export const TARGET_ATTRIBUTES: Readonly<Record<Target, readonly AttributeName[]
50
50
  repo: ["repo"],
51
51
  "config-scope/channel": ["channelId"],
52
52
  "config-scope/user": ["userId"],
53
+ // A thread key carries no attribute a condition reads: the one row on it is
54
+ // a bare grant check (the channel-config right, `config set thread`).
55
+ "config-scope/thread": [],
53
56
  "config-scope/org": [],
54
57
  agent: ["name"],
55
58
  command: [],
@@ -136,6 +139,7 @@ export function attributesOf(resource: Resource): ResourceAttributes {
136
139
  return { channelId: resource.id, visibility: "unknown", channelVisibility: resource.visibility ?? "unknown" };
137
140
  case "user":
138
141
  return { userId: resource.id, visibility: "unknown" };
142
+ case "thread":
139
143
  case "org":
140
144
  return { visibility: "unknown" };
141
145
  }
@@ -114,6 +114,8 @@ export type Resource =
114
114
  readonly visibility?: ChannelVisibility;
115
115
  }
116
116
  | { readonly type: "config-scope"; readonly kind: "user"; readonly id: string }
117
+ /** A thread's runtime scope (`config set thread`, routing-and-config item 27); `id` is the thread key. */
118
+ | { readonly type: "config-scope"; readonly kind: "thread"; readonly id: string }
117
119
  | { readonly type: "config-scope"; readonly kind: "org" }
118
120
  | { readonly type: "agent"; readonly name: string }
119
121
  /** List-shaped actions with no single resource (`runs.list`, `friction.report`). */
@@ -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,6 +164,12 @@ 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. */
@@ -164,6 +181,11 @@ export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes:
164
181
  * `commandWriteUp` is what the last command leaves for that answer; `execCall`
165
182
  * is the exec client's wait past a command's own budget; `bearerGrace` is how
166
183
  * far past the lease the model bearer stays valid for the last call's tail. */
184
+ /** The stall signal's window in minutes (docs/reference/specs/live-view.md
185
+ * item 32): a run's pace is counted over the last this-many minutes, and a
186
+ * run with no tool call for a whole window reads as stalled. */
187
+ export const PACE_WINDOW_MINUTES = 5;
188
+
167
189
  export const ALLOWANCES = {
168
190
  provision: 3,
169
191
  writeUp: 3,
@@ -9,6 +9,7 @@
9
9
  // routes decide on.
10
10
  //
11
11
  import type { Grant, GrantSource } from "../budgets.js";
12
+ import { isVerbosity, type Verbosity } from "../verbosity.js";
12
13
  import { isAddressSeverity, type AddressSeverity, type AddressSeveritySource } from "../ship/coordinator.js";
13
14
 
14
15
  // A coordinator is a Workflow instance in the shim Worker whose children are
@@ -81,6 +82,22 @@ export function runFinishedEventType(runId: string): string {
81
82
  return `${RUN_FINISHED_EVENT_PREFIX}${runId}`;
82
83
  }
83
84
 
85
+ /** The deploy-roll signals a child's reattach path sends its parent
86
+ * (run-history item 47a): `child-interrupted-<runId>` — the child closed
87
+ * `interrupted` for a restart from its request, so the wait settles at once
88
+ * beside `run-finished-<runId>` and the round ends with the child's own
89
+ * reason; `child-resumed-<runId>` — the same run carries on after a roll, so
90
+ * the wait keeps waiting. Same alphabet as the finish event; the parent
91
+ * confirms either by `read-record` before it acts. */
92
+ export const CHILD_INTERRUPTED_EVENT_PREFIX = "child-interrupted-";
93
+ export function childInterruptedEventType(runId: string): string {
94
+ return `${CHILD_INTERRUPTED_EVENT_PREFIX}${runId}`;
95
+ }
96
+ export const CHILD_RESUMED_EVENT_PREFIX = "child-resumed-";
97
+ export function childResumedEventType(runId: string): string {
98
+ return `${CHILD_RESUMED_EVENT_PREFIX}${runId}`;
99
+ }
100
+
84
101
  /** A message into a thread an unfinished unit owns (record 0051's reply-as-event rule): one row
85
102
  * of the unit's event list, appended by the dispatcher, folded into the
86
103
  * unit's next coding spawn — or run as one fresh turn at the unit's end.
@@ -307,6 +324,11 @@ export interface CoordinatorInstance {
307
324
  * decision reads it. */
308
325
  grant?: Grant;
309
326
  grantSource?: GrantSource;
327
+ /** The request's verbosity (routing-and-config item 28), resolved once by
328
+ * the ship fork and written here beside `merge`: what the runner says in
329
+ * the unit threads it owns — the unit-ending report's asides and the
330
+ * segment lines are `verbose` material. Absent reads as `quiet`. */
331
+ verbosity?: Verbosity;
310
332
  /** The pipeline's caps as the profile gate clipped them: the rounds cap and the wall clock per unit. */
311
333
  caps?: { maxRounds: number; maxMinutes: number };
312
334
  /** The status card in the requesting thread, when the channel has one — what
@@ -429,6 +451,7 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
429
451
  return false;
430
452
  if (r.card !== undefined && !(isObject(r.card) && isText(r.card.channel) && isText(r.card.ts))) return false;
431
453
  if (!isOptionalText(r.runId) || !isOptionalText(r.label)) return false;
454
+ if (r.verbosity !== undefined && !isVerbosity(r.verbosity)) return false;
432
455
  if (r.attempt !== undefined && !(Number.isInteger(r.attempt) && (r.attempt as number) >= 2)) return false;
433
456
  return true;
434
457
  }
@@ -527,6 +550,25 @@ export async function sendChecksSettled(
527
550
  }
528
551
  }
529
552
 
553
+ /** One deploy-roll signal to the child's parent (run-history item 47a): best
554
+ * effort like `sendRunFinished` — a refusal is answered, never thrown; a lost
555
+ * send costs the wait a chunk, never the round. */
556
+ export async function sendChildSignal(
557
+ workflow: WorkflowSender | undefined,
558
+ signal: { runId: string; parentInstanceId: string; kind: "interrupted" | "resumed"; reason: string; at: number },
559
+ ): Promise<RunFinishedSend> {
560
+ const { runId, parentInstanceId: instance, kind, reason, at } = signal;
561
+ if (!workflow) return { kind: "no-binding", instance };
562
+ const type = kind === "interrupted" ? childInterruptedEventType(runId) : childResumedEventType(runId);
563
+ try {
564
+ const handle = await workflow.get(instance);
565
+ await handle.sendEvent({ type, payload: { runId, kind, reason, at, parentInstanceId: instance } });
566
+ return { kind: "sent", instance, type };
567
+ } catch (err) {
568
+ return { kind: "failed", instance, type, reason: err instanceof Error ? err.message : String(err) };
569
+ }
570
+ }
571
+
530
572
  /** The one send per committed terminal record (run-history item 47). */
531
573
  export async function sendRunFinished(
532
574
  workflow: WorkflowSender | undefined,