@coreplane/switchboard 1.264.0 → 1.265.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.264.0",
3
+ "version": "1.265.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.264.0",
9
+ "version": "1.265.0",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -21060,7 +21060,7 @@
21060
21060
  },
21061
21061
  "packages/switchboard": {
21062
21062
  "name": "@coreplane/switchboard",
21063
- "version": "1.264.0",
21063
+ "version": "1.265.0",
21064
21064
  "license": "Apache-2.0",
21065
21065
  "dependencies": {
21066
21066
  "@earendil-works/pi-ai": "0.85.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.264.0",
3
+ "version": "1.265.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",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.264.0",
3
- "commit": "3f965ac7812604e62bd3e7df613ccf72ee665615",
4
- "builtAt": "2026-09-26T02:38:42.181Z"
2
+ "version": "1.265.0",
3
+ "commit": "39607a27b4b5751330311c08f9068a6bc007ca80",
4
+ "builtAt": "2026-09-26T04:13:47.389Z"
5
5
  }
@@ -1998,10 +1998,12 @@ function settleCoding(
1998
1998
  // final observed head. Only then may a fresh remote read decide readiness.
1999
1999
  if (round.kind === "findings" && facts.status === "completed") {
2000
2000
  const issued = next.findingsByRound[round.index] ?? [];
2001
- const matched = matchDispositions(issued, facts.dispositions ?? []).matched;
2001
+ const { matched, dropped } = matchDispositions(issued, facts.dispositions ?? []);
2002
2002
  const missingOutputs = issued
2003
2003
  .filter((finding) => !matched.some((disposition) => disposition.findingId === finding.id))
2004
2004
  .map((finding) => `missing disposition for ${finding.id}`);
2005
+ if (missingOutputs.length > 0 && dropped.length > 0)
2006
+ missingOutputs.push(`submitted IDs with no matching finding: ${dropped.join(", ")}`);
2005
2007
  if (facts.description !== true) missingOutputs.push("missing updated pull request description");
2006
2008
  const observedHead = fullHead(facts.headSha);
2007
2009
  if (observedHead === undefined) missingOutputs.push("missing final observed commit");
package/dist/cli.js CHANGED
@@ -36722,13 +36722,16 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs, recor
36722
36722
  rejected = void 0;
36723
36723
  unverified = void 0;
36724
36724
  }
36725
+ const bareNumberText = !s.pr && repo === records?.pr?.repo ? /\b(?:PR|pull request)\s*#(\d+)\b/i.exec(unwrapSlack(msg.text).replace(CODE_SPAN, " "))?.[1] : void 0;
36726
+ const bareNumber = bareNumberText === void 0 ? void 0 : Number(bareNumberText);
36727
+ const namedPr = s.pr ?? (repo && bareNumber !== void 0 && Number.isSafeInteger(bareNumber) && bareNumber > 0 ? { repo, number: bareNumber } : void 0);
36725
36728
  let headSha;
36726
36729
  let baseRef;
36727
36730
  let prSize;
36728
36731
  let facts2;
36729
36732
  let closedPr;
36730
- if (s.pr && repo === s.pr.repo) {
36731
- const head = await prHead(s.pr).catch(() => void 0);
36733
+ if (namedPr && repo === namedPr.repo) {
36734
+ const head = await prHead(namedPr).catch(() => void 0);
36732
36735
  if (head?.ref && head.state === "open") {
36733
36736
  ref = head.ref;
36734
36737
  refFromPr = true;
@@ -36739,7 +36742,7 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs, recor
36739
36742
  facts2 = head?.facts;
36740
36743
  if (head?.state === "closed")
36741
36744
  closedPr = {
36742
- number: s.pr.number,
36745
+ number: namedPr.number,
36743
36746
  merged: head.merged,
36744
36747
  ...head.mergedAt !== void 0 ? { mergedAt: head.mergedAt } : {}
36745
36748
  };
@@ -36750,16 +36753,16 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs, recor
36750
36753
  else if (rejected) out.rejectedRepo = rejected;
36751
36754
  if (ref) out.ref = ref;
36752
36755
  if (refFromPr) out.refFromPr = true;
36753
- if (repo && s.pr && repo === s.pr.repo) {
36754
- out.pr = s.pr.number;
36756
+ if (repo && namedPr && repo === namedPr.repo) {
36757
+ out.pr = namedPr.number;
36755
36758
  out.prFromMessage = true;
36756
- if (isThreadOwnPr(records, repo, s.pr.number)) out.prIsThreadOwn = true;
36759
+ if (isThreadOwnPr(records, repo, namedPr.number)) out.prIsThreadOwn = true;
36757
36760
  if (headSha) out.headSha = headSha;
36758
36761
  if (baseRef) out.baseRef = baseRef;
36759
36762
  if (prSize) out.prSize = prSize;
36760
36763
  if (facts2) out.prDescription = facts2;
36761
36764
  if (closedPr) out.closedPr = closedPr;
36762
- } else if (repo && !s.pr) {
36765
+ } else if (repo && !namedPr) {
36763
36766
  const inherited = inheritedPr(thread, records, repo);
36764
36767
  if (inherited) {
36765
36768
  const head = await openPrHeadSha(inherited);
@@ -40793,6 +40796,7 @@ async function ownerOf2(runs, unitsOf, threadKey) {
40793
40796
  if (live !== void 0) return { kind: "live", run: live };
40794
40797
  const hosted = runs.find((r) => !r.finished && r.hosted === true);
40795
40798
  const instanceId = instanceOf(runs);
40799
+ let releasedPr;
40796
40800
  if (instanceId !== void 0) {
40797
40801
  const units = await unitsOf(instanceId).catch(() => []);
40798
40802
  const unit = units.find((u) => u.threadKey === threadKey && u.ending === void 0);
@@ -40807,11 +40811,19 @@ async function ownerOf2(runs, unitsOf, threadKey) {
40807
40811
  if (ended.length === 1 && ship !== void 0) return { kind: "pipeline", instanceId, run: ship, unit: ended[0] };
40808
40812
  if (ended.length > 1 && ship !== void 0)
40809
40813
  return { kind: "pipeline_ambiguous", instanceId, run: ship, units: ended };
40814
+ const released = units.filter((u) => {
40815
+ const binding = u.publication;
40816
+ return u.threadKey === threadKey && u.instanceId === instanceId && u.ending?.kind === "merge_ready" && binding !== void 0 && u.pr?.number === binding.pr && u.branch === binding.headRef && u.branch === binding.publicationRef && binding.owner.instanceId === instanceId && binding.owner.unit === u.unit && (u.lastPush === void 0 || u.lastPush === binding.expectedHeadSha);
40817
+ });
40818
+ if (released.length === 1) {
40819
+ const unit2 = released[0];
40820
+ releasedPr = { repo: unit2.publication.repo, number: unit2.publication.pr, at: unit2.ending.at };
40821
+ }
40810
40822
  }
40811
40823
  if (hosted !== void 0) return { kind: "live", run: hosted };
40812
40824
  const agent = stickyAgentOf(runs);
40813
- if (agent !== void 0) return { kind: "session", agent };
40814
- return { kind: "none" };
40825
+ if (agent !== void 0) return { kind: "session", agent, ...releasedPr ? { releasedPr } : {} };
40826
+ return { kind: "none", ...releasedPr ? { releasedPr } : {} };
40815
40827
  }
40816
40828
  async function shipRequestOf(service, runId3) {
40817
40829
  const result = await service.getRun(runId3, { include: "messages" }).catch(() => void 0);
@@ -46062,7 +46074,7 @@ var init_submit = __esm({
46062
46074
  submitDispositionsTool = {
46063
46075
  name: "submit_dispositions",
46064
46076
  failsInText: true,
46065
- description: "Record one disposition per review finding after addressing them: `fixed` (the finding is addressed in your pushed code) or `declined` (deliberately not doing it \u2014 the note says why). `findingId` is the finding's stable id from the review (F1, F2, \u2026) \u2014 use exactly those ids; an id the review never issued answers nothing and is dropped when the plan runner reads your record. Every finding gets exactly one entry, every severity included (nits too). Call it once with the complete set after your last push; a later call replaces the earlier one. The set rides this run's record, where the plan runner reads it for the re-review.",
46077
+ description: "Record one disposition per review finding after addressing them: `fixed` (the finding is addressed in your pushed code) or `declined` (deliberately not doing it \u2014 the note says why). `findingId` is the finding's stable id printed in the findings. Copy it exactly, including check ids such as `check:CI / checks (test)`; do not replace a check id with `F1`. An id the review never issued answers nothing and is dropped when the plan runner reads your record. Every finding gets exactly one entry, every severity included (nits too). Call it once with the complete set after your last push; a later call replaces the earlier one. The set rides this run's record, where the plan runner reads it for the re-review.",
46066
46078
  inputSchema: {
46067
46079
  type: "object",
46068
46080
  properties: {
@@ -46072,7 +46084,10 @@ var init_submit = __esm({
46072
46084
  items: {
46073
46085
  type: "object",
46074
46086
  properties: {
46075
- findingId: { type: "string", description: `The finding's stable id from the review (e.g. "F1")` },
46087
+ findingId: {
46088
+ type: "string",
46089
+ description: 'Copy the exact id in the findings (e.g. "F1" or "check:CI / checks (test)")'
46090
+ },
46076
46091
  disposition: { type: "string", enum: ["fixed", "declined"], description: "fixed | declined" },
46077
46092
  note: { type: "string", description: "One line: what was done, or why it was declined" }
46078
46093
  },
@@ -46088,7 +46103,7 @@ var init_submit = __esm({
46088
46103
  if (!ctx.onDispositions) return "no run is recording dispositions here";
46089
46104
  ctx.onDispositions(parsed.dispositions);
46090
46105
  const drops = parsed.dropped.length ? ` (dropped: ${parsed.dropped.join("; ")})` : "";
46091
- return `dispositions recorded: ${parsed.dispositions.length}${drops}; a later call replaces this one`;
46106
+ return `dispositions recorded: ${parsed.dispositions.length}${drops}; IDs are checked against the findings by the plan runner; a later call replaces this one`;
46092
46107
  }
46093
46108
  };
46094
46109
  submitPrDescriptionTool = {
@@ -50278,19 +50293,36 @@ function judgeBashCommand(command2, ctx) {
50278
50293
  if (MERGE_OR_APPROVE.test(command2)) {
50279
50294
  return refused4("merge/approve \u2014 a coding run never merges or approves a pull request");
50280
50295
  }
50296
+ const pushCommand = withoutShellContinuations(command2);
50281
50297
  if (ctx.identity !== "write") {
50282
- if (ANY_PUSH.test(command2)) return refused4("read-only \u2014 a read-identity run never pushes");
50298
+ if (ANY_PUSH.test(pushCommand)) return refused4("read-only \u2014 a read-identity run never pushes");
50283
50299
  if (GH_WRITE_VERB.test(command2) || GH_API_WRITE.test(command2) || CURL_GITHUB_WRITE.test(command2)) {
50284
50300
  return refused4("read-only \u2014 a read-identity run never writes to GitHub");
50285
50301
  }
50286
50302
  return allowed;
50287
50303
  }
50288
- for (const match of command2.matchAll(GIT_PUSH)) {
50304
+ for (const match of pushCommand.matchAll(GIT_PUSH)) {
50289
50305
  const verdict = judgePush(match[1], ctx);
50290
50306
  if (verdict.verdict !== "allowed") return verdict;
50291
50307
  }
50292
50308
  return allowed;
50293
50309
  }
50310
+ function withoutShellContinuations(command2) {
50311
+ let result = "";
50312
+ let quote3;
50313
+ for (let i = 0; i < command2.length; i++) {
50314
+ const char = command2[i];
50315
+ if (char === "\\" && quote3 !== "'" && i + 1 < command2.length) {
50316
+ const next = command2[++i];
50317
+ if (next !== "\n") result += char + next;
50318
+ continue;
50319
+ }
50320
+ if (char === quote3) quote3 = void 0;
50321
+ else if (quote3 === void 0 && (char === "'" || char === '"')) quote3 = char;
50322
+ result += char;
50323
+ }
50324
+ return result;
50325
+ }
50294
50326
  function judgeBashTimeout(timeout, ctx) {
50295
50327
  if (!ctx.loopEndsIn || typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) return allowed;
50296
50328
  const leftMs = ctx.loopEndsIn();
@@ -50401,8 +50433,11 @@ var init_toolRules = __esm({
50401
50433
  ENV_DUMP = /(^|[;&|(]\s*)((printenv|env)(\s+-[A-Za-z0-9-]+)*|export\s+-p|declare\s+-[a-zA-Z]*[px][a-zA-Z]*|set)\s*($|[|;&>)])|\/proc\/(self|\d+)\/environ|process\.env(?![.[\w])/;
50402
50434
  CREDENTIAL_VAR = /\$\{?[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b|printenv\s+[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b/;
50403
50435
  MERGE_OR_APPROVE = /\bgh\s+pr\s+(merge|review)\b|\/pulls\/\d+\/(merge|reviews)\b/;
50404
- GIT_THEN_PUSH = String.raw`\bgit(?:\s+(?:-C\s+\S+|--git-dir=\S+|--work-tree=\S+|-c\s+\S+|--no-pager))*\s+push\b`;
50405
- GIT_PUSH = new RegExp(`${GIT_THEN_PUSH}((?:[^;&|]|(?<=[<>])&|&(?=>))*)`, "g");
50436
+ GIT_THEN_PUSH = String.raw`\bgit(?:[ \t]+(?:-C[ \t]+\S+|--git-dir=\S+|--work-tree=\S+|-c[ \t]+\S+|--no-pager))*[ \t]+push\b`;
50437
+ GIT_PUSH = new RegExp(
50438
+ GIT_THEN_PUSH + String.raw`((?:"(?:\\[\s\S]|[^"\\])*"|'[^']*'|\\[^\n]|[^\n;&|'"\\]|(?<=[<>])&|&(?=>))*)`,
50439
+ "g"
50440
+ );
50406
50441
  ANY_PUSH = new RegExp(GIT_THEN_PUSH);
50407
50442
  GH_WRITE_VERB = /\bgh\s+(pr|issue)\s+(comment|create|edit|close|reopen|ready|lock|unlock|review|merge|delete|transfer|pin|unpin)\b/;
50408
50443
  WRITE_METHOD = "(POST|post|PATCH|patch|PUT|put|DELETE|delete)";
@@ -56782,7 +56817,8 @@ async function dispatch(deps, msg, io, opts = {}) {
56782
56817
  );
56783
56818
  };
56784
56819
  const stickyAgent = thread ? stickyAgentOf(thread) : void 0;
56785
- const threadPr = thread ? threadPrOf(thread) : void 0;
56820
+ const laterPr = (runPr, releasedPr) => releasedPr !== void 0 && (runPr === void 0 || releasedPr.at > runPr.at) ? releasedPr : runPr;
56821
+ let threadPr = laterPr(thread ? threadPrOf(thread) : void 0, pageOwner?.releasedPr);
56786
56822
  const inheritedRepo = operatorRepo ?? (thread ? newestFinishedRunOf(thread)?.repo : void 0) ?? deps.config.scopes(msg.channelId, msg.userId).channel.repo;
56787
56823
  const historicalRoutePreset = restart?.row.meta.route?.preset;
56788
56824
  const resolveCurrent = () => resolveRun(deps, {
@@ -56803,6 +56839,7 @@ async function dispatch(deps, msg, io, opts = {}) {
56803
56839
  const threadLive = admission.get(msg.threadKey) !== void 0 || !resume && !restart && deps.threadsElsewhere.get(msg.threadKey) !== void 0;
56804
56840
  if (thread && !threadLive && deps.coordinatorInstances !== void 0) {
56805
56841
  const owner = pageOwner ?? await ownerOf2(thread, (id) => deps.coordinatorInstances.listUnits(id), msg.threadKey);
56842
+ threadPr = laterPr(threadPr, owner.releasedPr);
56806
56843
  if (owner.kind === "live" && owner.run.hosted === true) {
56807
56844
  const units = owner.run.instanceId === void 0 ? [] : await deps.coordinatorInstances.listUnits(owner.run.instanceId).catch(() => []);
56808
56845
  const open2 = units.filter((u) => u.ending === void 0);
@@ -72943,7 +72980,7 @@ function findingsRequest(input) {
72943
72980
 
72944
72981
  A person answered the human-gated finding. Treat the finding and this answer together as the fix brief:
72945
72982
  ${input.answers.join("\n\n")}` : "";
72946
- return `The review of ${input.where} requested changes. Load the \`address-review-findings\` skill and address every finding below, nits included: record one disposition per finding with submit_dispositions (fixed or declined, with a note), squash to coherent commits, resubmit the pull request description with submit_pr_description, and push the branch. Never merge and never approve.
72983
+ return `The review of ${input.where} requested changes. Load the \`address-review-findings\` skill and address every finding below, nits included: record one disposition per finding with submit_dispositions (fixed or declined, with a note). Copy each finding ID exactly, including any \`check:\u2026\` ID; never substitute an \`F1\` ID for a check. Squash to coherent commits, resubmit the pull request description with submit_pr_description, and push the branch. Never merge and never approve.
72947
72984
 
72948
72985
  Findings:
72949
72986
  ${findings}${answers}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreplane/switchboard",
3
- "version": "1.264.0",
3
+ "version": "1.265.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",