@coreplane/switchboard 1.256.0 → 1.258.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 (63) hide show
  1. package/dist/assets/config/config.example.yaml +14 -0
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +340 -7
  3. package/dist/assets/deploy/cloudflare-resident/drain.ts +49 -4
  4. package/dist/assets/deploy/cloudflare-resident/worker.ts +52 -4
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +5 -3
  7. package/dist/assets/project.json +17 -9
  8. package/dist/assets/source.json +3 -3
  9. package/dist/assets/src/agents/registry.ts +65 -2
  10. package/dist/assets/src/core/authz/grants.ts +12 -0
  11. package/dist/assets/src/core/authz/policy.ts +15 -0
  12. package/dist/assets/src/core/budgets.ts +66 -18
  13. package/dist/assets/src/core/coordinator/contract.ts +45 -5
  14. package/dist/assets/src/core/coordinator/driver.ts +6 -3
  15. package/dist/assets/src/core/delivery.ts +8 -5
  16. package/dist/assets/src/core/pipelineStanding.ts +2 -0
  17. package/dist/assets/src/core/plane/decide.ts +477 -12
  18. package/dist/assets/src/core/plane/findings.ts +120 -0
  19. package/dist/assets/src/core/runEvents.ts +54 -14
  20. package/dist/assets/src/core/runFriction.ts +16 -0
  21. package/dist/assets/src/core/runLedger/types.ts +16 -3
  22. package/dist/assets/src/core/runRecord.ts +12 -0
  23. package/dist/assets/src/core/ship/contract.ts +14 -2
  24. package/dist/assets/src/core/ship/coordinator.ts +179 -51
  25. package/dist/assets/src/core/ship/renewal.ts +23 -20
  26. package/dist/assets/src/core/trace/attrs.ts +1 -1
  27. package/dist/assets/src/core/untrusted.ts +35 -0
  28. package/dist/assets/src/execution/residentCredentials.ts +7 -4
  29. package/dist/assets/web/dist/.vite/manifest.json +97 -72
  30. package/dist/assets/web/dist/assets/{DeliveryPage-BBAyLwPq.js → DeliveryPage-DadntjSp.js} +1 -1
  31. package/dist/assets/web/dist/assets/HomePage-BEEpTnVT.js +1 -0
  32. package/dist/assets/web/dist/assets/{PendingTurnRow-DGINv9XT.js → PendingTurnRow-DBPMLVsm.js} +1 -1
  33. package/dist/assets/web/dist/assets/PlanePage-DLusMGQ0.js +1 -0
  34. package/dist/assets/web/dist/assets/{ResidentDetailPage-jYhEeeyu.js → ResidentDetailPage-CPnHnrxy.js} +1 -1
  35. package/dist/assets/web/dist/assets/{ResidentsIndexPage-DvFmlV4U.js → ResidentsIndexPage-C9hASwHL.js} +1 -1
  36. package/dist/assets/web/dist/assets/RunFoldRow-OPe7Z0Cz.js +1 -0
  37. package/dist/assets/web/dist/assets/{RunRoutePage-CvZ-TOT3.js → RunRoutePage-o1xzkTGZ.js} +4 -4
  38. package/dist/assets/web/dist/assets/RunsIndexPage-0YAClYQh.js +1 -0
  39. package/dist/assets/web/dist/assets/{ScheduledPage-Viim-bus.js → ScheduledPage-BIdfC0V3.js} +1 -1
  40. package/dist/assets/web/dist/assets/{SettingsPage-CYUy8McC.js → SettingsPage-CnqWAjl3.js} +1 -1
  41. package/dist/assets/web/dist/assets/SilentTurn-DGNg0sIZ.js +2 -0
  42. package/dist/assets/web/dist/assets/SlackMark-CtiHrNjr.js +1 -0
  43. package/dist/assets/web/dist/assets/{StatusDot-Dv6UMaPy.js → StatusDot-D_D_iJS8.js} +1 -1
  44. package/dist/assets/web/dist/assets/{Tooltip-Bd-5Rypv.js → Tooltip-DVFmM7on.js} +1 -1
  45. package/dist/assets/web/dist/assets/UnitRoutePage-7fAXFoB_.js +1 -0
  46. package/dist/assets/web/dist/assets/budgets-BRLm6oID.js +1 -0
  47. package/dist/assets/web/dist/assets/{dist-Nl3uaxrP.js → dist-C1VX-_mm.js} +1 -1
  48. package/dist/assets/web/dist/assets/durationTone-CVpX_yIk.js +1 -0
  49. package/dist/assets/web/dist/assets/{indexRow-DborJPFp.js → indexRow-BO_GIdF6.js} +1 -1
  50. package/dist/assets/web/dist/assets/main-Cl7xHG34.js +28 -0
  51. package/dist/assets/web/dist/assets/main-DGC6WFqS.css +1 -0
  52. package/dist/assets/web/dist/assets/{sseReplay-DE6wv1Ua.js → sseReplay-BjlKmQsY.js} +1 -1
  53. package/dist/cli.js +3096 -1136
  54. package/package.json +1 -1
  55. package/dist/assets/web/dist/assets/HomePage-BEiStFwW.js +0 -3
  56. package/dist/assets/web/dist/assets/PlanePage-JEj-lqgz.js +0 -1
  57. package/dist/assets/web/dist/assets/RunFoldRow-DvWzR1JQ.js +0 -1
  58. package/dist/assets/web/dist/assets/RunsIndexPage-JqVSDOok.js +0 -1
  59. package/dist/assets/web/dist/assets/UnitRoutePage-B81wEnKN.js +0 -1
  60. package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +0 -1
  61. package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +0 -1
  62. package/dist/assets/web/dist/assets/main-C4QfwybO.css +0 -1
  63. package/dist/assets/web/dist/assets/main-DRxWlffc.js +0 -28
@@ -350,6 +350,7 @@ import {
350
350
  liveDrain,
351
351
  parseDrainRequest,
352
352
  reportImageCurrent,
353
+ staleHolds,
353
354
  type DrainRecord,
354
355
  } from "./drain";
355
356
  import {
@@ -1604,9 +1605,15 @@ export class ResidentRegistryDO extends DurableObject<Env> {
1604
1605
  * the new image (issue 1931): hold the drain for each — the fleet must not
1605
1606
  * reopen onto them until they report. No live drain, nothing to hold. */
1606
1607
  async holdDrainFor(resources: string[]): Promise<void> {
1607
- const record = liveDrain(await this.ctx.storage.get(DRAIN_KEY), systemClock());
1608
+ const now = systemClock();
1609
+ const record = liveDrain(await this.ctx.storage.get(DRAIN_KEY), now);
1608
1610
  if (record === null || resources.length === 0) return;
1609
- await this.ctx.storage.put(DRAIN_KEY, holdDrain(record, resources));
1611
+ const held = holdDrain(record, resources, now);
1612
+ await this.ctx.storage.put(DRAIN_KEY, held);
1613
+ // The hold's liveness alarm (issue 2044): the cycle bound is the earlier
1614
+ // end, so the alarm fires there — reopening the fleet with the stale
1615
+ // containers named — instead of at `until`, the last resort.
1616
+ if (held.holdsUntil !== undefined) await this.ctx.storage.setAlarm(Date.parse(held.holdsUntil));
1610
1617
  }
1611
1618
 
1612
1619
  /** One resident's word that its running container is on the deploy's image:
@@ -1635,11 +1642,25 @@ export class ResidentRegistryDO extends DurableObject<Env> {
1635
1642
  const stored = await this.ctx.storage.get(DRAIN_KEY);
1636
1643
  if (stored === undefined) return;
1637
1644
  if (liveDrain(stored, now) === null) {
1645
+ // The hold's liveness (issue 2044): a reopen past the cycle bound names
1646
+ // the containers whose post-deploy cycle never landed — a warning, never
1647
+ // a silence; each restarts on its own next quiet attach or refresh.
1648
+ const stale = staleHolds(stored, now);
1649
+ if (stale !== null)
1650
+ console.log(
1651
+ `[drain] fleet reopened with ${stale.join(", ")} still on the pre-deploy image — the post-deploy cycle did not land within its bound; a stale container restarts on its next quiet attach or refresh`,
1652
+ );
1638
1653
  await this.ctx.storage.delete(DRAIN_KEY);
1639
1654
  await this.pushDrainPost("below");
1640
1655
  } else {
1641
- // Replaced with a later end under an already-armed alarm: re-arm at it.
1642
- await this.ctx.storage.setAlarm(Date.parse((stored as DrainRecord).until));
1656
+ // Replaced with a later end under an already-armed alarm: re-arm at the
1657
+ // record's own earlier end — the hold's cycle bound when one stands.
1658
+ const record = stored as DrainRecord;
1659
+ const ends = [
1660
+ Date.parse(record.until),
1661
+ ...(record.holdsUntil !== undefined ? [Date.parse(record.holdsUntil)] : []),
1662
+ ];
1663
+ await this.ctx.storage.setAlarm(Math.min(...ends.filter(Number.isFinite)));
1643
1664
  }
1644
1665
  }
1645
1666
 
@@ -3261,6 +3282,14 @@ export class ResidentDO extends Sandbox<Env> {
3261
3282
  await this.writeDiskMarkers({ ready: sha, depsKey: lockfileHash, builtSha: sha });
3262
3283
  this.deleteSchedules(PROVISIONING_CALLBACK);
3263
3284
  await this.setResidentState("warm");
3285
+ // A fresh provision runs on the image the deployed Worker pins, so it IS
3286
+ // the cycle a held drain waits for (issue 2044): report it — a rebuild
3287
+ // mid-drain reopens the fleet instead of leaving the hold standing on a
3288
+ // report `reconcileImage` would never send for a container it never saw
3289
+ // as stale. The marker is cleared with the report; a failure keeps both
3290
+ // for the next reconcile, and the hold's cycle bound is the backstop.
3291
+ await this.ctx.storage.put(IMAGE_REPORT_PENDING_KEY, { resource });
3292
+ await this.reportPendingImageCurrent("provision");
3264
3293
  // The first refresh instance is the cron's: the row now reads `warm`
3265
3294
  // with no instance recorded, so the next firing creates it (item 9).
3266
3295
  } catch (err) {
@@ -7597,6 +7626,19 @@ export class ResidentDO extends Sandbox<Env> {
7597
7626
  }
7598
7627
 
7599
7628
  private async runOpTraced(op: "test" | "build", refArg: string | null, t0: number): Promise<OpRunOk | ThreadErr> {
7629
+ // The fleet drain (item 69; issue 2044): a typed op is a new piece of work
7630
+ // like a new run's attach, and during the incident one waited silently at
7631
+ // the drain — so it is answered with the drain's own record at once, a
7632
+ // deterministic refusal the command surfaces as its reason, never a wait.
7633
+ const drain = await this.fleetDrain();
7634
+ if (drain !== null) {
7635
+ return {
7636
+ error: `op-refused: the resident fleet is drained for ${drain.reason} (asked by ${drain.by}, ends by ${drain.until}) — re-run the command when the fleet reopens`,
7637
+ status: 503,
7638
+ reason: "draining",
7639
+ cause: "system",
7640
+ };
7641
+ }
7600
7642
  try {
7601
7643
  await this.ensureHydrated();
7602
7644
  } catch (err) {
@@ -8070,6 +8112,7 @@ export class ResidentDO extends Sandbox<Env> {
8070
8112
  AUTO_REBUILDS_KEY,
8071
8113
  INFRA_STREAK_KEY,
8072
8114
  MEMORY_KEY,
8115
+ IMAGE_REPORT_PENDING_KEY,
8073
8116
  ]);
8074
8117
  const facts = map.get(FACTS_KEY) as RepoFacts | undefined;
8075
8118
  const snap = map.get(SNAPSHOT_KEY) as SnapshotRecord | undefined;
@@ -8147,6 +8190,11 @@ export class ResidentDO extends Sandbox<Env> {
8147
8190
  inFlight: this.inFlightCount() + registeredRuns,
8148
8191
  // The runs alone (no refresh cycle): what the deploy preflight refuses on.
8149
8192
  runsInFlight: this.runsInFlightCount() + registeredRuns,
8193
+ // Issue 2044: whether this container's new-image report is still owed —
8194
+ // `pending` names a resident a held drain waits on, so `repo list` and
8195
+ // the deploy's readers never print a bare "warm" for a repository whose
8196
+ // running container the last deploy could not verify.
8197
+ imageReport: map.get(IMAGE_REPORT_PENDING_KEY) !== undefined ? "pending" : "current",
8150
8198
  // Item 22: who holds what, as the rows say — the mirror mutex and the
8151
8199
  // cycle/hydration leases, each judged against this incarnation.
8152
8200
  incarnation: this.incarnation,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.256.0",
3
+ "version": "1.258.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.256.0",
9
+ "version": "1.258.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.256.0",
20448
+ "version": "1.258.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.256.0",
3
+ "version": "1.258.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,9 +39,9 @@
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 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",
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 vocabulary: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
- "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",
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 vocabulary:gen && npm run deploy:gen && npm run skills:sync && npm run lint:fix && npm run format",
45
45
  "deploy:gen": "npm run --silent cli -- deploy init",
46
46
  "deploy:check": "npm run --silent cli -- deploy init --check",
47
47
  "check:lockfile": "node scripts/check-lockfile.mjs",
@@ -73,6 +73,8 @@
73
73
  "decisions:check": "tsx scripts/decisions-check.ts",
74
74
  "hygiene:check": "node scripts/public-hygiene.mjs",
75
75
  "hygiene:gen": "node scripts/public-hygiene.mjs --write",
76
+ "vocabulary:check": "node scripts/vocabulary-check.mjs",
77
+ "vocabulary:gen": "node scripts/vocabulary-check.mjs --write",
76
78
  "docs:changed": "node scripts/docs-changed.mjs",
77
79
  "deploy:targets": "node scripts/deploy-targets.mjs",
78
80
  "docs:dev": "npm run dev -w docs --",
@@ -32,7 +32,7 @@
32
32
  "commands": {
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
- "does": "The whole gate: every root check, every workspace's verify, the site check — exactly what CI runs.",
35
+ "does": "The whole gate: every check, every workspace — exactly what CI runs.",
36
36
  "when": "Local sessions, before requesting review. ~4 min. A pipeline agent leaves it to CI."
37
37
  },
38
38
  "verify:root": {
@@ -40,11 +40,11 @@
40
40
  "when": "When only the bot changed."
41
41
  },
42
42
  "check:consistency": {
43
- "does": "The sub-second checks that generated and declared things equal the code (lockfile, sandbox pairs, skills, licenses, docs tables, specs, project facts, this table).",
43
+ "does": "The sub-second checks that generated and declared things equal the code, this table included.",
44
44
  "when": "After touching a generated or declared artifact; one CI leg."
45
45
  },
46
46
  "test": {
47
- "does": "The whole vitest suite (bot, web, the plain-Node Worker tests) from one entry, after `deploy:gen`.",
47
+ "does": "The whole vitest suite from one entry, after `deploy:gen`.",
48
48
  "when": "Local sessions, before pushing; a pipeline agent runs the changed set — the full suite is CI's."
49
49
  },
50
50
  "typecheck": {
@@ -160,21 +160,29 @@
160
160
  "when": "After renaming a test or editing a spec; `-- --fix` makes truncated titles explicit."
161
161
  },
162
162
  "specs:coverage": {
163
- "does": "Maps a change's paths to the specs whose `Code`/`Tests` headers cover them, then lists changed source paths no spec covers.",
163
+ "does": "Maps a change's paths to the specs whose headers cover them, then lists paths no spec covers.",
164
164
  "when": "`-- --changed origin/main...HEAD [--test-guard]` before review; `-- --require` fails on an uncovered path; `-- --json` for machines."
165
165
  },
166
166
  "decisions:check": {
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`.",
168
- "when": "Part of `check:consistency`; a failing record is superseded or amended by appending, never edited."
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 `## Amended`.",
168
+ "when": "Part of `check:consistency`; a failing record is superseded or amended, never edited."
169
169
  },
170
170
  "hygiene:check": {
171
171
  "does": "The public tree's imprint (company, people, trackers, plan ids, ids, dates) equals the recorded list, which only shrinks.",
172
- "when": "Part of `check:consistency`. New hit: rewrite the line or allow it in `scripts/public-hygiene.allow`."
172
+ "when": "Part of `check:consistency`. New hit: rewrite the line or allow it by name."
173
173
  },
174
174
  "hygiene:gen": {
175
175
  "does": "Records the tree's remaining imprint after a scrub; refuses growth unless `-- --force`.",
176
176
  "when": "Part of `fix`; new imprint fails it like `hygiene:check`."
177
177
  },
178
+ "vocabulary:check": {
179
+ "does": "No internal word prints on a user surface; the baseline only shrinks.",
180
+ "when": "Part of `check:consistency`; a hit is rewritten in the user's nouns."
181
+ },
182
+ "vocabulary:gen": {
183
+ "does": "Records the remaining internal words; refuses growth unless `-- --force`.",
184
+ "when": "Part of `fix`."
185
+ },
178
186
  "agents:gen": {
179
187
  "does": "Writes the Commands table in AGENTS.md from `package.json` and this file.",
180
188
  "when": "After adding or changing a script; part of `fix`."
@@ -220,8 +228,8 @@
220
228
  "when": "After a `web/` change."
221
229
  },
222
230
  "screenshots:gen": {
223
- "does": "Renders the dashboard screenshots whose inputs changed, both themes, recording each surface's input hashes in `docs/public/screenshots/manifest/` (`--force`: all).",
224
- "when": "After a `web/` or fixture change, once `screenshots:check` names it; needs `npx playwright-core install chromium`, so not in `fix`."
231
+ "does": "Renders the dashboard screenshots whose inputs changed, both themes, recording input hashes in the manifest (`--force`: all).",
232
+ "when": "After a `web/` or fixture change; needs `npx playwright-core install chromium`, so not in `fix`."
225
233
  },
226
234
  "screenshots:check": {
227
235
  "does": "Each surface's inputs still hash to what its screenshots were rendered from — no browser.",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.256.0",
3
- "commit": "fd814ee9ad2421f38178b15103d49ff4d9c5eb7c",
4
- "builtAt": "2026-09-20T04:37:22.536Z"
2
+ "version": "1.258.0",
3
+ "commit": "05d8b84a387c7b7c15ed8dd2dc30f08a5982a981",
4
+ "builtAt": "2026-09-20T17:05:48.184Z"
5
5
  }
@@ -74,7 +74,7 @@ export interface AgentDef {
74
74
  system: string;
75
75
  /** key into TOOLSETS (src/tools/toolsets.ts): the tools the bot relays to
76
76
  * the preset's pi; pi's own workspace tools follow `identity`. */
77
- toolset: "full" | "readonly" | "web" | "assistant" | "explore" | "conductor" | "none";
77
+ toolset: "full" | "readonly" | "web" | "assistant" | "explore" | "conductor" | "orchestrator" | "none";
78
78
  /** The runaway guard, not a budget: `runawayTurnCap(maxMinutes)` for every
79
79
  * preset that runs the loop (`loopBudget`). The wall clock below is the
80
80
  * budget; a run that reaches this cap first was pacing like a loop, and its
@@ -279,6 +279,23 @@ export const FAST_GATES_BEFORE_PUSH =
279
279
  "CI's gate — `npm run verify` runs there on your push, never here: push a head early and let CI judge it, " +
280
280
  "fixing forward with further commits and pushes.";
281
281
 
282
+ // Every coding prompt carries this verbatim, right after the fast gates
283
+ // (agent-coding item 13; record 0071 mechanism one, issue 1747): the rebase
284
+ // before every push, always and never configurable. Approved pull requests
285
+ // went stale behind sibling merges and each needed a hand-posted rebase; a
286
+ // rebase against a base the run just fetched, with the change's context still
287
+ // in its window, is the cheapest rebase the system will ever run, so it is
288
+ // unconditional. One constant, one text: the three coding variants carry the
289
+ // same bytes — a ship fix round's child runs these same prompts — and the ship
290
+ // contract's first instruction points here instead of re-stating it
291
+ // (src/core/ship/contract.ts).
292
+ export const REBASE_BEFORE_PUSH =
293
+ "REBASE BEFORE EVERY PUSH — always, not configurable. Immediately before each push: fetch your base branch, " +
294
+ "rebase your branch onto it, resolve any conflict with the context you already have (the repository's " +
295
+ "AGENTS.md says how a generated file is regenerated — regenerate it, never hand-merge it), re-run THE FAST " +
296
+ "GATES on the rebased tree, and only then push. Every head that reaches review is then current with its " +
297
+ "base when it lands, and no unit ends merge-ready behind a sibling that merged first.";
298
+
282
299
  const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
283
300
 
284
301
  You work inside a dedicated workspace directory with bash, read_file, and write_file tools. ${SANDBOX_TOOLCHAIN}
@@ -303,6 +320,8 @@ ${CHECKS_BY_COST}
303
320
 
304
321
  ${FAST_GATES_BEFORE_PUSH}
305
322
 
323
+ ${REBASE_BEFORE_PUSH}
324
+
306
325
  ${NEVER_MERGE}
307
326
 
308
327
  ${UNIT_CONTRACT}
@@ -352,6 +371,8 @@ ${CHECKS_BY_COST}
352
371
 
353
372
  ${FAST_GATES_BEFORE_PUSH}
354
373
 
374
+ ${REBASE_BEFORE_PUSH}
375
+
355
376
  ${NEVER_MERGE}
356
377
 
357
378
  ${UNIT_CONTRACT}
@@ -397,6 +418,8 @@ ${CHECKS_BY_COST}
397
418
 
398
419
  ${FAST_GATES_BEFORE_PUSH}
399
420
 
421
+ ${REBASE_BEFORE_PUSH}
422
+
400
423
  ${NEVER_MERGE}
401
424
 
402
425
  ${UNIT_CONTRACT}
@@ -589,7 +612,7 @@ Your tools work without a workspace: the GitHub tools — \`github_repos\` (the
589
612
 
590
613
  ${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.
591
614
 
592
- 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).`;
615
+ 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: Switchboard's door reads every message — a reply in this thread included — and starts the right agent from the person's own words. 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 — right here in the thread or anywhere in the channel, 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 tell the person to post a new top-level message, re-post, re-ask elsewhere or retype their request — their words have already been said and the door reads thread replies; and never hand back a command or an \`agent:…\` line to type: describe the ask in their words. Delete an issue only when the user explicitly asked to delete it (closing is an update).`;
593
616
 
594
617
  // The explore agent (docs/reference/specs/agent-explore.md): a long, read-only
595
618
  // investigation — "run our CI locally and validate the claims", "how long does
@@ -624,6 +647,32 @@ ${BREVITY_RULE}
624
647
  ${FENCED_CONTENT_RULE}
625
648
  Report outcomes faithfully: a check you could not run is "could not check", never a guess. Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks — render the claim table as aligned rows inside a code block). Your final message is posted to Slack: lead with the overall verdict in one line, then the claim table, then what a follow-up should do.`;
626
649
 
650
+ // The orchestrator (record 0070; docs/reference/specs/orchestration-plane.md
651
+ // item 11): the plane's chat preset — one long-lived thread per person beside
652
+ // the panels, and `agent:orchestrator` from any channel a grant admits. It
653
+ // reads the fleet from the plane's projections (the `plane_show` tool answers
654
+ // the SAME rows the panels paint) and never from its own context: every fleet
655
+ // fact cites the row it read, and a question the tables cannot answer says so
656
+ // instead of recalling. Machine `none`, identity `none`: no workspace, no
657
+ // shell, no credential — the read tool set is its whole reach, and its writes
658
+ // are the registry's fenced commands through the door (record 0070's later
659
+ // units), never a tool here. The prose speaks record 0066's twelve nouns:
660
+ // thread, run, agent, pipeline, unit, round, budget, follow-up, verdict,
661
+ // outcome, card, pull request — no thirteenth noun rides in.
662
+ const ORCHESTRATOR_SYSTEM = `You are Switchboard's orchestrator: the control plane's chat, answering a person about their fleet — every thread, run, agent, pipeline, unit, round, budget, follow-up, verdict, outcome, card and pull request the plane's tables carry.
663
+
664
+ You have no workspace and no shell. Your tools: \`plane_show\` (the plane's live tables — every live and recently ended run, every unit and every tracked pull request, each with its owner and its health: the same rows the /plane panels paint, seen as the person who asked may see them), \`recall\` and \`notes\` (this thread's log and your notepad), and \`update_status\`.
665
+
666
+ EVERY FLEET FACT COMES FROM THE TABLES, NEVER FROM MEMORY. Before answering any question about the fleet — what is running, which unit is idle, which pull request is merge-ready and nobody's — read the tables with \`plane_show\` and cite the row you read: name the run id, the unit or the pull request the answer stands on, so the person can check it against the panel beside you. Never answer a standing question from this conversation's earlier turns or your own context: an earlier turn is history, the table is now. A question the tables cannot answer — a why the rows do not carry, a fact outside the fleet — is answered "the tables do not say", plainly, instead of recalling or guessing; say what the rows do show and where the answer would live.
667
+
668
+ You cannot run commands, edit code, merge or stop anything from here: acting on the fleet goes through Switchboard's own commands, which the person's own words reach through the door under their own grants — say what you found and let their next sentence act.
669
+
670
+ ${statusCardRule('"Read the plane tables", "Check the run\'s standing"')} A one-read answer needs no checklist.
671
+
672
+ ${BREVITY_RULE}
673
+ ${FENCED_CONTENT_RULE}
674
+ Lead with the answer, then the cited rows — short lines, the row's own words. Your reply renders in the plane's chat column or the channel the question came from.`;
675
+
627
676
  // The conductor (docs/reference/specs/agent-conductor.md): a run that starts
628
677
  // other runs instead of doing the work — the spawn/await substrate's first
629
678
  // preset. A child is a `dispatch()` run as the requesting user, in a thread of
@@ -802,6 +851,20 @@ const WORK_PRESETS = {
802
851
  ...loopBudget("explore"),
803
852
  // No built-in effort: the deployment decides, as for coding.
804
853
  },
854
+ orchestrator: {
855
+ name: "orchestrator",
856
+ description:
857
+ "Answers fleet questions from the plane's live tables — runs, units, pull requests — with the row cited. No workspace or shell.",
858
+ system: ORCHESTRATOR_SYSTEM,
859
+ toolset: "orchestrator",
860
+ // The plane read is a call in the bot process: nothing is provisioned
861
+ // and no credential is minted (record 0070, criterion 3).
862
+ machine: "none",
863
+ identity: "none",
864
+ tiers: ["fast", "strong"], // reads and cites rows: may run fast
865
+ maxTokens: 16000,
866
+ ...loopBudget("orchestrator"),
867
+ },
805
868
  } satisfies Record<string, AgentDef>;
806
869
 
807
870
  export const AGENTS: Record<string, AgentDef> = {
@@ -13,6 +13,18 @@ import { NO_GRANTS, type Grants, type GrantSet } from "./types.js";
13
13
  /** Every grant, on every axis. What admins and the local CLI hold. */
14
14
  export const ALL_GRANTS: Grants = Object.freeze({ actions: "all", channels: "all", repos: "all" });
15
15
 
16
+ /** The plane's system actor (record 0064; record 0057 as amended): the run
17
+ * ledger object steers write-preset runs itself — the checkpoint steer and the
18
+ * held turn's reissue land as `run_inbox` rows with sender `plane` — under
19
+ * this one standing grant. Recorded here so the table stays the whole story:
20
+ * the write happens on the object, which holds no other authority. */
21
+ export const PLANE_ACTOR_ID = "plane";
22
+ export const PLANE_GRANTS: Grants = Object.freeze({
23
+ actions: new Set(["steer:write"]),
24
+ channels: "all",
25
+ repos: "all",
26
+ });
27
+
16
28
  /** The namespaces a native `grants` key may use (invariant 4). `cli:` is not
17
29
  * configurable (the local CLI always holds everything) and `agent:` actors
18
30
  * derive their grants from their principal, so neither is listed. */
@@ -63,6 +63,12 @@ export const POLICY: readonly Rule[] = [
63
63
  // NAMED run (`authorizeSteerOwner`, authorization item 16a) — the requester,
64
64
  // a linked id, or `runs:write` — which no command-door row can see.
65
65
  { action: "steer:write", resource: "command", when: [grant("steer:write")] },
66
+ // The plane's checkpoint steer (record 0064): the system actor `plane`
67
+ // (`PLANE_ACTOR_ID`, a `service` actor holding `PLANE_GRANTS`' one standing
68
+ // `steer:write`) writes the steer into a write-preset run's inbox on the
69
+ // ledger object itself, sender `plane`; this row records that authority in
70
+ // the one table even though the object, not the bot, executes the write.
71
+ { action: "steer:write", resource: "run", actorKinds: ["service"], when: [grant("steer:write")] },
66
72
 
67
73
  // ── conversations ────────────────────────────────────────────────────────
68
74
  // Who may point the bot at another thread (record 0037). Asked for a
@@ -108,6 +114,11 @@ export const POLICY: readonly Rule[] = [
108
114
  // read is never a chat baseline, like the costs reads.
109
115
  { action: "providers:read", resource: "command", when: [grant("providers:read")] },
110
116
 
117
+ // ── pull requests ──────────────────────────────────────────────────
118
+ // `pulls rebase` force-pushes the pipeline's branches and can start a paid
119
+ // fix round (record 0071): a grant, never a chat baseline.
120
+ { action: "pulls:write", resource: "command", when: [grant("pulls:write")] },
121
+
111
122
  // ── repos ────────────────────────────────────────────────────────────────
112
123
  { action: "repo:read", resource: "command", when: [grant("repo:read")] },
113
124
  { action: "repo:write", resource: "repo", when: [grant("repo:write")] },
@@ -150,6 +161,10 @@ export const POLICY: readonly Rule[] = [
150
161
  // channel-config right — whoever may set the channel may set a thread in it;
151
162
  // never a baseline, so membership alone admits nobody.
152
163
  { action: "config:write", resource: "config-scope", resourceKind: "thread", when: [grant("config:write")] },
164
+ // The ORG scope (`config set org --pulls.…`, record 0071) — and a repository
165
+ // scope, the org setting's per-repository slice — affects every requester:
166
+ // the same `config:write` grant, never a baseline.
167
+ { action: "config:write", resource: "config-scope", resourceKind: "org", when: [grant("config:write")] },
153
168
  // A user edits only their own scope.
154
169
  { action: "config:write", resource: "config-scope", resourceKind: "user", when: [IS_SELF] },
155
170
  // The author binding's one trusted write (record 0062, authorization.md item
@@ -16,6 +16,10 @@
16
16
  // Workflow-driven coordinator and the deploy Workers can bundle it — the
17
17
  // dependency runs registry → budgets, never the reverse.
18
18
 
19
+ export const SECOND_MS = 1_000;
20
+
21
+ export const secondsToMs = (seconds: number): number => seconds * SECOND_MS;
22
+
19
23
  export const MINUTE_MS = 60_000;
20
24
 
21
25
  /** Minutes → milliseconds, for a duration a request names in minutes (a drain's
@@ -109,6 +113,14 @@ export const DRAIN = {
109
113
  marginMinutes: 5,
110
114
  maxMinutes: 90,
111
115
  defaultMinutes: 60,
116
+ /** How long a held drain waits for a container's post-deploy cycle before
117
+ * reopening the fleet anyway with the stale container named (issue 2044):
118
+ * the platform replaces container processes three to ten minutes after a
119
+ * deploy (docs/reference/specs/resident-repos.md item 65), so a cycle that
120
+ * has not landed within this bound is not coming on its own — the gate
121
+ * asked for the cycle, so it owns the outcome and never holds the fleet
122
+ * closed to the drain's whole `until` on a report nothing will send. */
123
+ cycleBoundMinutes: 10,
112
124
  } as const;
113
125
 
114
126
  /** How long an intake receipt row is kept on the run history object
@@ -132,7 +144,15 @@ export function intakeReceiptRetentionMs(catchUpWindowMs: number): number {
132
144
  export const INTAKE_RECOVERY_WINDOW_MS = 10 * MINUTE_MS;
133
145
 
134
146
  /** The presets that run the tool loop, and the one pipeline preset. */
135
- export const LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"] as const;
147
+ export const LOOP_PRESETS = [
148
+ "general",
149
+ "coding",
150
+ "review",
151
+ "research",
152
+ "explore",
153
+ "conductor",
154
+ "orchestrator",
155
+ ] as const;
136
156
  export type LoopPreset = (typeof LOOP_PRESETS)[number];
137
157
  export type Preset = LoopPreset | "ship";
138
158
 
@@ -143,7 +163,7 @@ export type Preset = LoopPreset | "ship";
143
163
  * that pushes at minute 29 and then runs the repo's whole gate (seven to ten
144
164
  * minutes on a resident) was cut at 45 one run in twelve, so the ask is
145
165
  * double the 90th percentile (31.5) with the gate inside it. Ship's holds
146
- * its own loop at the default rounds with a fix round at that ask, not at
166
+ * its own loop at the default rounds with a findings round at that ask, not at
147
167
  * its floor (`fit` proves the floor case; the ask leaves room above it). */
148
168
  export const ASKS: Readonly<Record<Preset, number>> = {
149
169
  general: 5,
@@ -153,19 +173,24 @@ export const ASKS: Readonly<Record<Preset, number>> = {
153
173
  research: 8,
154
174
  explore: 120,
155
175
  conductor: 120,
176
+ // The plane's chat preset (record 0070): reads the fleet's tables and
177
+ // answers; a turn is a projection read plus a write-up, so its ask sits
178
+ // between general's and research's kind of work, with room for a few reads.
179
+ orchestrator: 10,
156
180
  };
157
181
 
158
- /** The rounds a ship loop is made of. `fix` is a coding child handed the
182
+ /** The rounds a ship loop is made of. `findings` is a coding child handed the
159
183
  * review's findings; `merge` is the runner's wait on the guards. */
160
- export type RoundKind = "coding" | "review" | "fix" | "merge";
184
+ export type RoundKind = "coding" | "review" | "findings" | "merge";
161
185
 
162
186
  /** The least lease in which a round does useful work, in minutes. A carve that
163
187
  * falls under the floor is refused rather than dispatched: a two-minute
164
- * review or fix costs an attach and a model turn and finishes nothing.
165
- * Review's is the ledger's 90th percentile of completed reviews (5.1 min over
166
- * 181); coding's is the least a fix round can run the repo's gate in — the
167
- * gate alone takes seven to ten minutes on a resident, and a fix that cannot
168
- * run it finishes nothing its contract asks; the merge wait's is a guess
188
+ * review or findings round costs an attach and a model turn and finishes
189
+ * nothing. Review's is the ledger's 90th percentile of completed reviews (5.1
190
+ * min over 181); coding's is the least a findings round can run the repo's
191
+ * gate in — the gate alone takes seven to ten minutes on a resident, and a
192
+ * round that cannot run it finishes nothing its contract asks; the merge
193
+ * wait's is a guess
169
194
  * until the ledger says. */
170
195
  export const PRESET_FLOORS: Readonly<Record<LoopPreset, number>> = {
171
196
  general: 2,
@@ -174,10 +199,11 @@ export const PRESET_FLOORS: Readonly<Record<LoopPreset, number>> = {
174
199
  research: 3,
175
200
  explore: 15,
176
201
  conductor: 15,
202
+ orchestrator: 2,
177
203
  };
178
204
  export const FLOORS: Readonly<Record<RoundKind, number>> = {
179
205
  coding: PRESET_FLOORS.coding,
180
- fix: PRESET_FLOORS.coding,
206
+ findings: PRESET_FLOORS.coding,
181
207
  review: PRESET_FLOORS.review,
182
208
  merge: 10,
183
209
  };
@@ -186,6 +212,12 @@ export const FLOORS: Readonly<Record<RoundKind, number>> = {
186
212
  * when the remainder allows it. */
187
213
  export const MERGE_WAIT_ASK_MINUTES = 60;
188
214
 
215
+ /** The sweep's bounds (record 0071, mechanism two): the short lease of the one
216
+ * model round a conflicting pull request may buy, and the spend that round is
217
+ * capped at per pull request — a round refused at either bound ends the pull
218
+ * request's line with the conflict named, never a retry loop. */
219
+ export const PULL_SWEEP = { leaseMinutes: 15, spendCapUsd: 5 } as const;
220
+
189
221
  /** The provider retry ladder (issue 1932): the backoff before each retry of a
190
222
  * transient model-call failure — a gateway 5xx, a stream cut before
191
223
  * `message_stop`, a gateway timeout. Three attempts with growing waits,
@@ -208,7 +240,15 @@ export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes:
208
240
  * it, a reservation's window, and the default re-ask cadence — how often a
209
241
  * silent resident something waits on is probed (`plane.reaskMinutes`
210
242
  * overrides it per deployment). */
211
- export const PLANE = { recentMinutes: 60, reservationMinutes: 2, reaskMinutes: 2 } as const;
243
+ export const PLANE = {
244
+ recentMinutes: 60,
245
+ reservationMinutes: 2,
246
+ reaskMinutes: 2,
247
+ /** A coding round with no pushed head past this is steered once (record 0064, "The backpressure contract"). */
248
+ noPushMinutes: 15,
249
+ /** The line an in-flight call with NO declared bound is judged against for the same steer. */
250
+ noBoundMinutes: 20,
251
+ } as const;
212
252
 
213
253
  /** The named amounts a lease holds back, in minutes. Each stands for a step
214
254
  * every run or round pays: `provision` is attach and restore before the
@@ -238,6 +278,7 @@ export const POST_STEP_MINUTES: Readonly<Record<Preset, number>> = {
238
278
  research: 0,
239
279
  explore: 0,
240
280
  conductor: 0,
281
+ orchestrator: 0,
241
282
  ship: 0,
242
283
  };
243
284
 
@@ -328,6 +369,12 @@ export function provisionalBearerExpiresAt(now: number, leaseMinutes: number): n
328
369
  return now + (ALLOWANCES.provision + leaseMinutes + ALLOWANCES.bearerGrace) * MINUTE_MS;
329
370
  }
330
371
 
372
+ /** The e2b credential-write exec's own timeout: the store-file write is one
373
+ * `printf` plus a `git config` reset, so a minute is generous headroom while
374
+ * still failing fast when the micro-VM is wedged
375
+ * (docs/reference/specs/execution.md item 5). */
376
+ export const SANDBOX_CREDENTIAL_WRITE_TIMEOUT_MS = MINUTE_MS;
377
+
331
378
  /** The per-command bash budget's rows: the default when a call names none, the
332
379
  * ceiling a call may raise it to, and the floor under which a number is a
333
380
  * typo (docs/reference/specs/execution.md item 11). */
@@ -361,19 +408,20 @@ export interface RoundPosition {
361
408
  }
362
409
 
363
410
  /** The rounds of a loop in order: the coding round, then a review and, after
364
- * every review but the last, a fix, then the merge wait. */
411
+ * every review but the last, a findings round, then the merge wait. */
365
412
  export function loopRounds(loop: Loop): RoundKind[] {
366
413
  const rounds: RoundKind[] = ["coding"];
367
414
  for (let i = 0; i < loop.maxRounds; i++) {
368
415
  rounds.push("review");
369
- if (i < loop.maxRounds - 1) rounds.push("fix");
416
+ if (i < loop.maxRounds - 1) rounds.push("findings");
370
417
  }
371
418
  rounds.push("merge");
372
419
  return rounds;
373
420
  }
374
421
 
375
422
  /** Where a round sits in `loopRounds`: the coding round first; review round
376
- * `n` (counted from 1) and the fix that follows it at `2n − 1` and `2n`; the
423
+ * `n` (counted from 1) and the findings round that follows it at `2n − 1`
424
+ * and `2n`; the
377
425
  * merge wait last. The ship coordinator numbers its rounds this way, so the
378
426
  * reserve it carves with is the one the fit assumed. */
379
427
  export function loopPosition(loop: Loop, kind: RoundKind, n = 0): number {
@@ -382,7 +430,7 @@ export function loopPosition(loop: Loop, kind: RoundKind, n = 0): number {
382
430
  return 0;
383
431
  case "review":
384
432
  return 2 * n - 1;
385
- case "fix":
433
+ case "findings":
386
434
  return 2 * n;
387
435
  case "merge":
388
436
  return 2 * loop.maxRounds;
@@ -403,7 +451,7 @@ export function carveChildOfParent(
403
451
  }
404
452
 
405
453
  /** The minutes a round holds back for what must follow it in the loop: the
406
- * floor plus provisioning of every later review and fix, and the merge
454
+ * floor plus provisioning of every later review and findings round, and the merge
407
455
  * wait's floor. A merge holds nothing back; a round with no loop (a
408
456
  * conductor's child) holds nothing back either. */
409
457
  export function reserveMinutes(
@@ -419,12 +467,12 @@ export function reserveMinutes(
419
467
  return reserve;
420
468
  }
421
469
 
422
- /** What a round asks for: the preset's ask for a coding or fix round, the
470
+ /** What a round asks for: the preset's ask for a coding or findings round, the
423
471
  * review's for a review, the merge wait's own for the merge. */
424
472
  export function roundAskMinutes(kind: RoundKind): number {
425
473
  switch (kind) {
426
474
  case "coding":
427
- case "fix":
475
+ case "findings":
428
476
  return ASKS.coding;
429
477
  case "review":
430
478
  return ASKS.review;