@coreplane/switchboard 1.203.0 → 1.204.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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.203.0",
3
+ "version": "1.204.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.203.0",
9
+ "version": "1.204.0",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -18806,7 +18806,7 @@
18806
18806
  },
18807
18807
  "packages/switchboard": {
18808
18808
  "name": "@coreplane/switchboard",
18809
- "version": "1.203.0",
18809
+ "version": "1.204.0",
18810
18810
  "license": "Apache-2.0",
18811
18811
  "dependencies": {
18812
18812
  "@anthropic-ai/sdk": "^0.124.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.203.0",
3
+ "version": "1.204.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",
@@ -212,7 +212,7 @@
212
212
  "when": "Part of `check:consistency`."
213
213
  },
214
214
  "load": {
215
- "does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider`.",
215
+ "does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider|pi`.",
216
216
  "when": "Capacity receipts (docs/reference/specs/load-harness.md)."
217
217
  }
218
218
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.203.0",
3
- "commit": "e3f155d00ca4b1131eb3bbe4d9274a825afa4e9e",
4
- "builtAt": "2026-09-11T23:34:59.316Z"
2
+ "version": "1.204.0",
3
+ "commit": "7182e104a894723453ad1c0ea5005ffea44b80e0",
4
+ "builtAt": "2026-09-12T01:46:35.879Z"
5
5
  }
package/dist/cli.js CHANGED
@@ -1485,6 +1485,9 @@ var init_contract = __esm({
1485
1485
  });
1486
1486
 
1487
1487
  // ../../src/agents/registry.ts
1488
+ function machineNeedsRepo(machine) {
1489
+ return machine === "repo-cold" || machine === "repo-resident";
1490
+ }
1488
1491
  function getAgent(name) {
1489
1492
  const a = AGENTS[name];
1490
1493
  if (!a) {
@@ -1654,12 +1657,13 @@ You cannot run commands, clone repositories, edit code, or review pull requests,
1654
1657
  description: "Default assistant on the configured model: answers directly, reads the org's repos and manages their issues over GitHub, reads URLs. No workspace or shell.",
1655
1658
  system: GENERAL_SYSTEM,
1656
1659
  toolset: "assistant",
1660
+ // The GitHub tools are REST in the bot process, so a general ask never
1661
+ // provisions a workspace or sandbox (docs/reference/specs/agent-general.md item 4).
1662
+ machine: "none",
1657
1663
  maxTurns: 8,
1658
1664
  // a repo read is 2-3 calls (repos → tree → file); an issue action 1-2; still fast
1659
1665
  maxTokens: 16e3,
1660
1666
  maxMinutes: 5
1661
- // No `resources`: the GitHub tools are REST in the bot process, so a
1662
- // general ask still never provisions a workspace or sandbox (item 4).
1663
1667
  },
1664
1668
  coding: {
1665
1669
  name: "coding",
@@ -1677,7 +1681,7 @@ You cannot run commands, clone repositories, edit code, or review pull requests,
1677
1681
  cacheTtl: "1h",
1678
1682
  // No built-in effort: the deployment decides (`defaults.efforts.coding`,
1679
1683
  // `config set channel efforts.coding=…`, or `effort:` per request).
1680
- resources: { repo: "required" }
1684
+ machine: "repo-resident"
1681
1685
  },
1682
1686
  review: {
1683
1687
  name: "review",
@@ -1685,7 +1689,7 @@ You cannot run commands, clone repositories, edit code, or review pull requests,
1685
1689
  system: REVIEW_SYSTEM,
1686
1690
  residentSystem: REVIEW_SYSTEM_RESIDENT,
1687
1691
  toolset: "readonly",
1688
- resources: { repo: "required" },
1692
+ machine: "repo-resident",
1689
1693
  maxTurns: 30,
1690
1694
  // backstop only; wall clock is the real budget (12 bound at ~4 min in practice)
1691
1695
  maxTokens: 64e3,
@@ -1701,22 +1705,22 @@ You cannot run commands, clone repositories, edit code, or review pull requests,
1701
1705
  // pipeline orchestrator (src/core/shipPipeline.ts), whose child rounds run
1702
1706
  // on the coding/review defs above — runAgent is never called with THIS def.
1703
1707
  system: "You are Switchboard's ship pipeline. This prompt is never sent to a model \u2014 the pipeline orchestrates coding and review child runs on their own definitions.",
1704
- // Full toolset so a ship thread provisions a writable workspace class like
1705
- // coding; placeholder budgets — the pipeline is bounded by the `ship` config
1706
- // caps and by each child's own budgets clipped to the remaining wall clock,
1707
- // never by these numbers.
1708
+ // Full toolset and the coding machine class, so repo and PR resolution
1709
+ // gate a ship thread like a coding one; placeholder budgets — the pipeline
1710
+ // is bounded by the `ship` config caps and by each child's own budgets
1711
+ // clipped to the remaining wall clock, never by these numbers.
1708
1712
  toolset: "full",
1713
+ machine: "repo-resident",
1709
1714
  maxTurns: 1,
1710
1715
  maxTokens: 16e3,
1711
- maxMinutes: 5,
1712
- resources: { repo: "required" }
1716
+ maxMinutes: 5
1713
1717
  },
1714
1718
  research: {
1715
1719
  name: "research",
1716
1720
  description: "Answers questions with web search, URL reading, and read access to the org's repos and issues over GitHub. No workspace.",
1717
1721
  system: RESEARCH_SYSTEM,
1718
1722
  toolset: "web",
1719
- resources: { repo: "none" },
1723
+ machine: "none",
1720
1724
  // web I/O only; no workspace is provisioned
1721
1725
  maxTurns: 12,
1722
1726
  maxTokens: 24e3,
@@ -8901,9 +8905,19 @@ var init_residentState = __esm({
8901
8905
  import { resolve as resolve7 } from "node:path";
8902
8906
  import { mkdirSync as mkdirSync7 } from "node:fs";
8903
8907
  async function makeExecutor(opts, ctx, span) {
8904
- if (ctx.agent.resources?.repo !== "required") {
8908
+ const machine = ctx.agent.machine;
8909
+ if (machine === "none") {
8905
8910
  return { executor: new NullExecutor(ctx.agent.name), backend: "local" };
8906
8911
  }
8912
+ if (machine === "blank") {
8913
+ return {
8914
+ executor: await makePerThreadExecutor(opts, { threadKey: ctx.threadKey, resolveEnvs: async () => ({}) }),
8915
+ backend: perThreadBackend(opts)
8916
+ };
8917
+ }
8918
+ if (machine === "repo-cold") {
8919
+ return { executor: await makePerThreadExecutor(opts, perThreadCheckout(ctx)), backend: perThreadBackend(opts) };
8920
+ }
8907
8921
  let note;
8908
8922
  let failedAttach;
8909
8923
  if (ctx.repo && opts.execution?.resident) {
@@ -8946,7 +8960,7 @@ async function makeExecutor(opts, ctx, span) {
8946
8960
  }
8947
8961
  }
8948
8962
  return {
8949
- executor: await makePerThreadExecutor(opts, ctx),
8963
+ executor: await makePerThreadExecutor(opts, perThreadCheckout(ctx)),
8950
8964
  note,
8951
8965
  backend: perThreadBackend(opts),
8952
8966
  ...failedAttach ? { trace: failedAttach.steps } : {}
@@ -9041,8 +9055,11 @@ function localWorkspaceDir(baseDir, threadKey) {
9041
9055
  const safe = threadKey.replace(/[^a-zA-Z0-9_.-]/g, "_");
9042
9056
  return resolve7(baseDir, safe);
9043
9057
  }
9044
- async function makePerThreadExecutor(opts, ctx) {
9045
- const { threadKey } = ctx;
9058
+ function perThreadCheckout(ctx) {
9059
+ return { threadKey: ctx.threadKey, repo: ctx.repo, ref: ctx.ref, resolveEnvs: () => githubEnvs(ctx.agent) };
9060
+ }
9061
+ async function makePerThreadExecutor(opts, input) {
9062
+ const { threadKey } = input;
9046
9063
  const type = opts.execution?.type ?? "local";
9047
9064
  if (type === "local") {
9048
9065
  const dir = localWorkspaceDir(opts.workspaceDir, threadKey);
@@ -9058,9 +9075,9 @@ async function makePerThreadExecutor(opts, ctx) {
9058
9075
  threadKey,
9059
9076
  timeoutMs: (opts.execution?.timeoutMinutes ?? 30) * 6e4,
9060
9077
  statePath: resolve7(opts.dataDir, "sandboxes.json"),
9061
- resolveEnvs: () => githubEnvs(ctx.agent),
9062
- repo: ctx.repo,
9063
- ref: ctx.ref
9078
+ resolveEnvs: input.resolveEnvs,
9079
+ repo: input.repo,
9080
+ ref: input.ref
9064
9081
  });
9065
9082
  }
9066
9083
  if (type === "cloudflare") {
@@ -9074,16 +9091,18 @@ async function makePerThreadExecutor(opts, ctx) {
9074
9091
  url: opts.execution.url,
9075
9092
  token: token.reveal(),
9076
9093
  threadKey,
9077
- resolveEnvs: () => githubEnvs(ctx.agent),
9078
- repo: ctx.repo,
9079
- ref: ctx.ref
9094
+ resolveEnvs: input.resolveEnvs,
9095
+ repo: input.repo,
9096
+ ref: input.ref
9080
9097
  });
9081
9098
  }
9082
9099
  throw new Error(`Unknown execution.type "${type}" (valid: local, e2b, cloudflare)`);
9083
9100
  }
9101
+ function githubTokenScopeFor(agent) {
9102
+ return agent.toolset === "readonly" ? "read" : "write";
9103
+ }
9084
9104
  async function githubEnvs(agent) {
9085
- const scope = agent.toolset === "readonly" ? "read" : "write";
9086
- const token = await resolveGithubToken(scope);
9105
+ const token = await resolveGithubToken(githubTokenScopeFor(agent));
9087
9106
  return token ? { GH_TOKEN: token } : {};
9088
9107
  }
9089
9108
  function perThreadBackend(opts) {
@@ -9113,7 +9132,7 @@ var init_factory = __esm({
9113
9132
  agentName;
9114
9133
  fail() {
9115
9134
  throw new Error(
9116
- `Agent "${this.agentName}" declares no repo resource, so it has no execution workspace. Declare resources: { repo: "required" } on the agent if its tools need one.`
9135
+ `Agent "${this.agentName}" runs on machine class "none", so it has no execution workspace. Declare a machine class that provisions one (\`repo-resident\`, \`repo-cold\` or \`blank\`) if its tools need one.`
9117
9136
  );
9118
9137
  }
9119
9138
  async exec() {
@@ -20442,10 +20461,10 @@ function safePredicate(isResident, slug) {
20442
20461
  return false;
20443
20462
  }
20444
20463
  }
20445
- async function safeProbe(isResident, slug) {
20446
- if (!isResident) return true;
20464
+ async function safeProbe(probe, slug) {
20465
+ if (!probe) return true;
20447
20466
  try {
20448
- const answer = await isResident(slug);
20467
+ const answer = await probe(slug);
20449
20468
  return answer === true || answer === "unreachable" ? answer : false;
20450
20469
  } catch {
20451
20470
  return "unreachable";
@@ -20454,7 +20473,7 @@ async function safeProbe(isResident, slug) {
20454
20473
  function repoFromThread(history, isResident) {
20455
20474
  return threadSignals(history, isResident).repo;
20456
20475
  }
20457
- async function resolveRepoContext(msg, history = [], isResident, residentSlugs) {
20476
+ async function resolveRepoContext(msg, history = [], probe, residentSlugs) {
20458
20477
  const s = extractSignals(msg.text);
20459
20478
  const thread = threadSignals(history);
20460
20479
  let rejected;
@@ -20463,7 +20482,7 @@ async function resolveRepoContext(msg, history = [], isResident, residentSlugs)
20463
20482
  const vet = async (cand) => {
20464
20483
  let p = vetted.get(cand);
20465
20484
  if (!p) {
20466
- p = safeProbe(isResident, cand);
20485
+ p = safeProbe(probe, cand);
20467
20486
  vetted.set(cand, p);
20468
20487
  }
20469
20488
  const answer = await p;
@@ -20479,7 +20498,7 @@ async function resolveRepoContext(msg, history = [], isResident, residentSlugs)
20479
20498
  return slugsOnce;
20480
20499
  };
20481
20500
  const resolveAddressed = async (a) => {
20482
- if (a.slug !== void 0) return isResident && await vet(a.slug) ? a.slug : void 0;
20501
+ if (a.slug !== void 0) return probe && await vet(a.slug) ? a.slug : void 0;
20483
20502
  const matches = (await listSlugs() ?? []).filter((slug) => slug.split("/")[1] === a.name);
20484
20503
  return matches.length === 1 ? matches[0] : void 0;
20485
20504
  };
@@ -25111,6 +25130,44 @@ var init_directives = __esm({
25111
25130
  }
25112
25131
  });
25113
25132
 
25133
+ // ../../src/execution/githubRepoProbe.ts
25134
+ function githubRepoProbe(opts) {
25135
+ const fetchImpl = opts.fetch ?? fetch;
25136
+ const token = opts.token ?? ((scope) => resolveGithubToken(scope));
25137
+ return async (slug) => {
25138
+ if (!SLUG.test(slug)) return false;
25139
+ let bearer;
25140
+ try {
25141
+ bearer = await token(opts.scope);
25142
+ } catch {
25143
+ return "unreachable";
25144
+ }
25145
+ const headers = { accept: "application/vnd.github+json", "user-agent": "switchboard" };
25146
+ if (bearer) headers.authorization = `Bearer ${bearer}`;
25147
+ let res;
25148
+ try {
25149
+ res = await fetchImpl(`https://api.github.com/repos/${slug}`, {
25150
+ headers,
25151
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS6)
25152
+ });
25153
+ } catch {
25154
+ return "unreachable";
25155
+ }
25156
+ if (res.ok) return true;
25157
+ if (res.status === 404) return false;
25158
+ return "unreachable";
25159
+ };
25160
+ }
25161
+ var REQUEST_TIMEOUT_MS6, SLUG;
25162
+ var init_githubRepoProbe = __esm({
25163
+ "../../src/execution/githubRepoProbe.ts"() {
25164
+ "use strict";
25165
+ init_githubApp();
25166
+ REQUEST_TIMEOUT_MS6 = 1e4;
25167
+ SLUG = /^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/;
25168
+ }
25169
+ });
25170
+
25114
25171
  // ../../src/core/dispatch/resolve.ts
25115
25172
  async function readRequest(ctx) {
25116
25173
  const { msg, io, root } = ctx;
@@ -25136,28 +25193,29 @@ function resolveTarget(deps, ctx) {
25136
25193
  const { msg, history, agent, resolved, resume, root } = ctx;
25137
25194
  const { provider: providerName, model } = parseModelRef(resolved.modelRef);
25138
25195
  const provider = deps.providers.get(providerName);
25139
- const needsRepo = agent.resources?.repo === "required";
25196
+ const needsRepo = machineNeedsRepo(agent.machine);
25140
25197
  const repoCtxP = root.span(
25141
25198
  "dispatch.repo_context",
25142
25199
  () => resume ? Promise.resolve(resume.repoCtx) : needsRepo ? Promise.resolve(
25143
- deps.resolveRepoContext ? deps.resolveRepoContext(msg, history) : resolveRepoContext(
25144
- msg,
25145
- history,
25146
- residentOnboardedProbe(deps.config.config.execution?.resident),
25147
- residentSlugsLister(deps.config.config.execution?.resident)
25148
- )
25200
+ deps.resolveRepoContext ? deps.resolveRepoContext(msg, history) : resolveRepoContext(msg, history, ...repoVetFor(agent, deps.config.config.execution?.resident))
25149
25201
  ).then((ctx2) => ctx2 ?? {}) : Promise.resolve({})
25150
25202
  );
25151
25203
  repoCtxP.catch(() => {
25152
25204
  });
25153
25205
  return { provider, model, needsRepo, repoCtxP };
25154
25206
  }
25207
+ function repoVetFor(agent, resident) {
25208
+ if (agent.machine === "repo-cold") return [githubRepoProbe({ scope: githubTokenScopeFor(agent) }), void 0];
25209
+ return [residentOnboardedProbe(resident), residentSlugsLister(resident)];
25210
+ }
25155
25211
  var init_resolve = __esm({
25156
25212
  "../../src/core/dispatch/resolve.ts"() {
25157
25213
  "use strict";
25214
+ init_registry();
25158
25215
  init_directives();
25159
25216
  init_types3();
25160
25217
  init_factory();
25218
+ init_githubRepoProbe();
25161
25219
  init_repoContext();
25162
25220
  }
25163
25221
  });
@@ -25178,6 +25236,41 @@ async function authorizeAgent(deps, ctx) {
25178
25236
  }
25179
25237
  async function authorizeRepo(deps, ctx) {
25180
25238
  const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock2, agent, needsRepo, repoCtx } = ctx;
25239
+ if (agent.machine === "repo-cold" && needsRepo && !repoCtx.repo) {
25240
+ if (repoCtx.rejectedRepo) {
25241
+ const slug = repoCtx.rejectedRepo;
25242
+ console.log(`[dispatch] ${msg.threadKey} not started: repo not visible (${slug}: GitHub answered 404)`);
25243
+ await refuse3("repo_not_visible", async () => {
25244
+ await card.done(
25245
+ shell.close({ kind: "not_started", icon: "\u{1F4E6}", reason: "repo not visible", ...closeLines(clock2(), false) })
25246
+ );
25247
+ await io.reply(
25248
+ `\u{1F4E6} \`${slug}\` is not a repository this installation can see \u2014 GitHub answered 404 \u2014 so I did not start ${aRun(agent.name)} for it. The repository is outside the Switchboard GitHub App installation (\`github_repos\` lists the reachable ones), or the name is wrong.`
25249
+ );
25250
+ });
25251
+ return { kind: "refused", reason: "repo_not_visible" };
25252
+ }
25253
+ if (repoCtx.unverifiedRepo) {
25254
+ const slug = repoCtx.unverifiedRepo;
25255
+ console.log(
25256
+ `[dispatch] ${msg.threadKey} not started: repo could not be verified (${slug}: GitHub did not answer)`
25257
+ );
25258
+ await refuse3("repo_unverified", async () => {
25259
+ await card.done(
25260
+ shell.close({
25261
+ kind: "not_started",
25262
+ icon: "\u{1F4E6}",
25263
+ reason: "repo could not be verified",
25264
+ ...closeLines(clock2(), false)
25265
+ })
25266
+ );
25267
+ await io.reply(
25268
+ `\u26A0\uFE0F I couldn't verify \`${slug}\` against GitHub \u2014 it didn't answer \u2014 so I did not start ${aRun(agent.name)} rather than guess which repository you meant. Try again in a minute.`
25269
+ );
25270
+ });
25271
+ return { kind: "refused", reason: "repo_unverified" };
25272
+ }
25273
+ }
25181
25274
  if (deps.capabilities.residents && needsRepo && !repoCtx.repo && repoCtx.rejectedRepo) {
25182
25275
  const slug = repoCtx.rejectedRepo;
25183
25276
  console.log(`[dispatch] ${msg.threadKey} not started: repo not onboarded (${slug})`);
@@ -25226,6 +25319,9 @@ async function authorizeRepo(deps, ctx) {
25226
25319
  }
25227
25320
  return { kind: "allowed" };
25228
25321
  }
25322
+ function aRun(agentName) {
25323
+ return `${/^[aeiou]/i.test(agentName) ? "an" : "a"} *${agentName}* run`;
25324
+ }
25229
25325
  async function authorizePrHead(ctx) {
25230
25326
  const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock2, agent, directives, repoCtx } = ctx;
25231
25327
  const preflight = checkPrHeadPreflight({ agent, requestText: directives.text, repoCtx });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreplane/switchboard",
3
- "version": "1.203.0",
3
+ "version": "1.204.0",
4
4
  "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.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://openswitchboard.dev",