@coreplane/switchboard 1.261.3 → 1.261.4

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.
@@ -2732,12 +2732,12 @@ export class RunHistoryDO extends DurableObject<Env> {
2732
2732
  return { ok: true };
2733
2733
  }
2734
2734
 
2735
- /** One compare-and-replace transaction reserves a legacy continuation for
2736
- * exactly one caller. The whole expected JSON is the fence: any intervening
2737
- * unit write, including another recovery, makes this caller stale. */
2738
- async claimLegacyContinuation(
2735
+ /** One compare-and-replace transaction updates a unit for exactly one
2736
+ * caller. The whole expected JSON is the fence: any intervening unit write
2737
+ * makes this caller stale. */
2738
+ async compareAndReplaceUnit(
2739
2739
  expected: CoordinatorUnit,
2740
- recovered: CoordinatorUnit,
2740
+ replacement: CoordinatorUnit,
2741
2741
  now: number,
2742
2742
  ): Promise<{ ok: true } | { ok: false; reason: "stale" }> {
2743
2743
  let out: { ok: true } | { ok: false; reason: "stale" } = { ok: true };
@@ -2755,7 +2755,7 @@ export class RunHistoryDO extends DurableObject<Env> {
2755
2755
  }
2756
2756
  this.sql.exec(
2757
2757
  `UPDATE coordinator_units SET json = ?, updated_at = ? WHERE instance_id = ? AND unit = ?`,
2758
- JSON.stringify(recovered),
2758
+ JSON.stringify(replacement),
2759
2759
  now,
2760
2760
  expected.instanceId,
2761
2761
  expected.unit,
@@ -5924,9 +5924,9 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
5924
5924
  return json({ error: "expected and recovered must be coordinator unit rows" }, 400);
5925
5925
  if (b.expected.instanceId !== b.recovered.instanceId || b.expected.unit !== b.recovered.unit)
5926
5926
  return json({ error: "expected and recovered must name the same unit" }, 400);
5927
- const r = await stub.claimLegacyContinuation(b.expected, b.recovered, now);
5927
+ const r = await stub.compareAndReplaceUnit(b.expected, b.recovered, now);
5928
5928
  console.log(
5929
- `[runs/coordinator/units/claim-legacy-continuation] ${key.value} ${b.expected.instanceId}:${b.expected.unit} → ${r.ok ? "claimed" : r.reason}`,
5929
+ `[runs/coordinator/units/claim-legacy-continuation] ${key.value} ${b.expected.instanceId}:${b.expected.unit} → ${r.ok ? "replaced" : r.reason}`,
5930
5930
  );
5931
5931
  return r.ok ? json(r) : json(r, 409);
5932
5932
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.261.3",
3
+ "version": "1.261.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.261.3",
9
+ "version": "1.261.4",
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.261.3",
21063
+ "version": "1.261.4",
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.261.3",
3
+ "version": "1.261.4",
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.261.3",
3
- "commit": "b727e58725e3d234e0d34b43e04ca0dc9af17075",
4
- "builtAt": "2026-09-24T14:03:08.080Z"
2
+ "version": "1.261.4",
3
+ "commit": "9f4d0f4c9645ea67ecab3ede2455e658f38a7d4c",
4
+ "builtAt": "2026-09-24T16:28:36.099Z"
5
5
  }
package/dist/cli.js CHANGED
@@ -34687,7 +34687,7 @@ var init_instanceStore = __esm({
34687
34687
  async putUnits(_units) {
34688
34688
  return { ok: false, reason: "unavailable" };
34689
34689
  }
34690
- async claimLegacyContinuation(_expected, _recovered) {
34690
+ async compareAndReplaceUnit(_expected, _replacement) {
34691
34691
  return { ok: false, reason: "unavailable" };
34692
34692
  }
34693
34693
  async listUnits(_instanceId) {
@@ -34764,8 +34764,11 @@ var init_instanceStore = __esm({
34764
34764
  if (d.ok === true) return { ok: true };
34765
34765
  throw new Error(`coordinator store /runs/coordinator/units/put: unexpected answer (HTTP ${r.status})`);
34766
34766
  }
34767
- async claimLegacyContinuation(expected, recovered) {
34768
- const r = await this.post("/runs/coordinator/units/claim-legacy-continuation", { expected, recovered });
34767
+ async compareAndReplaceUnit(expected, replacement) {
34768
+ const r = await this.post("/runs/coordinator/units/claim-legacy-continuation", {
34769
+ expected,
34770
+ recovered: replacement
34771
+ });
34769
34772
  const d = r.data;
34770
34773
  if (r.status === 409 && d.reason === "stale") return { ok: false, reason: "stale" };
34771
34774
  if (d.ok === true) return { ok: true };
@@ -56132,16 +56135,22 @@ function continuationBudgetOf(instance, unit) {
56132
56135
  }
56133
56136
  async function legacyReviewedHeadOf(runs, instance, unit, prNumber) {
56134
56137
  const listed = await runs.listUnitRuns(unitKeyOf({ instanceId: instance.id, unit: unit.unit }), { kind: "all" }).catch(() => void 0);
56135
- if (listed === void 0 || !listed.ok || listed.value.unit !== unitKeyOf({ instanceId: instance.id, unit: unit.unit }) || listed.value.instanceId !== instance.id || listed.value.id !== unit.unit || listed.value.instance.id !== instance.id || listed.value.instance.repo.toLowerCase() !== instance.repo.toLowerCase())
56138
+ if (listed === void 0 || !listed.ok || listed.value.unit !== unitKeyOf({ instanceId: instance.id, unit: unit.unit }) || listed.value.instanceId !== instance.id || listed.value.id !== unit.unit || listed.value.branch !== unit.branch || listed.value.pr?.number !== prNumber || listed.value.instance.id !== instance.id || listed.value.instance.repo.toLowerCase() !== instance.repo.toLowerCase())
56136
56139
  return void 0;
56137
56140
  const prefix = `${instance.id}:${unit.unit}/`;
56138
56141
  const owned = listed.value.runs.filter(
56139
56142
  (run2) => run2.finished && run2.status === "completed" && run2.parentInstanceId === instance.id && run2.idempotencyKey?.startsWith(prefix) === true && run2.userId === instance.userId && run2.repo?.toLowerCase() === instance.repo.toLowerCase()
56140
56143
  );
56141
56144
  const coding = new Set(
56142
- owned.flatMap(
56143
- (run2) => run2.agent === "coding" && run2.pr?.number === prNumber && typeof run2.headSha === "string" && FULL_SHA2.test(run2.headSha) ? [run2.headSha.toLowerCase()] : []
56144
- )
56145
+ owned.flatMap((run2) => {
56146
+ if (run2.agent !== "coding" || run2.pr?.number !== prNumber) return [];
56147
+ return [
56148
+ ...typeof run2.headSha === "string" && FULL_SHA2.test(run2.headSha) ? [run2.headSha.toLowerCase()] : [],
56149
+ ...(run2.pushed ?? []).flatMap(
56150
+ (pushed) => pushed.ref === unit.branch && FULL_SHA2.test(pushed.sha) ? [pushed.sha.toLowerCase()] : []
56151
+ )
56152
+ ];
56153
+ })
56145
56154
  );
56146
56155
  const approved = new Set(
56147
56156
  owned.flatMap((run2) => {
@@ -56613,7 +56622,7 @@ async function dispatch(deps, msg, io, opts = {}) {
56613
56622
  `Unit ${legacyUnit.unit}'s ownership reservation was lost, so continuation did not start. Nothing else ran.`
56614
56623
  )
56615
56624
  };
56616
- const persisted = await coordinatorInstances.claimLegacyContinuation(legacyUnit, recoveredUnit).catch(() => void 0);
56625
+ const persisted = await coordinatorInstances.compareAndReplaceUnit(legacyUnit, recoveredUnit).catch(() => void 0);
56617
56626
  if (persisted?.ok !== true) {
56618
56627
  const released = releaseLegacyOwnership?.() ?? true;
56619
56628
  const reason2 = persisted?.reason === "stale" ? `Unit ${legacyUnit.unit} changed while its legacy continuation was being reserved, so continuation did not start.` : `Unit ${legacyUnit.unit}'s recovered continuation binding could not be stored, so continuation did not start.`;
@@ -56622,7 +56631,7 @@ async function dispatch(deps, msg, io, opts = {}) {
56622
56631
  }
56623
56632
  const reservedOwner = runnerOwnership.owner(instance.repo, recordedPr.number);
56624
56633
  if (reservedOwner?.instanceId !== reservationOwner.instanceId || reservedOwner.unit !== reservationOwner.unit) {
56625
- const restored = await coordinatorInstances.claimLegacyContinuation(recoveredUnit, legacyUnit).catch(() => void 0);
56634
+ const restored = await coordinatorInstances.compareAndReplaceUnit(recoveredUnit, legacyUnit).catch(() => void 0);
56626
56635
  const released = releaseLegacyOwnership?.() ?? true;
56627
56636
  const rollbackFailure = restored?.ok === true ? void 0 : restored?.reason ?? "unavailable";
56628
56637
  const cleanupFailure = released ? "" : " and its ownership reservation could not be released";
@@ -56648,7 +56657,7 @@ async function dispatch(deps, msg, io, opts = {}) {
56648
56657
  transferredOwner = void 0;
56649
56658
  },
56650
56659
  abort: async () => {
56651
- const restored = await coordinatorInstances.claimLegacyContinuation(recoveredUnit, legacyUnit).catch(() => void 0);
56660
+ const restored = await coordinatorInstances.compareAndReplaceUnit(recoveredUnit, legacyUnit).catch(() => void 0);
56652
56661
  const released = releaseLegacyOwnership?.() ?? true;
56653
56662
  owner.unit = legacyUnit;
56654
56663
  const failures = [
@@ -73363,6 +73372,68 @@ async function steerChild(body, deps) {
73363
73372
  const steered = await deps.steerChild?.(body.runId, text, instance) ?? false;
73364
73373
  return json3(200, { ok: true, outcome: steered ? "steered" : "not_live", at });
73365
73374
  }
73375
+ async function bindOpenPullRequest(deps, instance, row2, pr, facts2) {
73376
+ if (row2 === void 0) return;
73377
+ const verified = facts2.verifiedHead;
73378
+ if (instance.base === void 0 || facts2.state !== "open" || facts2.sameRepoHead !== true || facts2.headBranchExists !== true || facts2.headRef !== row2.branch || facts2.baseRef !== instance.base || verified === void 0 || verified.repo.toLowerCase() !== instance.repo.toLowerCase() || verified.ref !== row2.branch || !/^[0-9a-f]{40}$/i.test(verified.sha) || facts2.headSha !== verified.sha || row2.pr !== void 0 && row2.pr.number !== pr.number)
73379
+ throw new PublicationBindingRefusal("publication_facts_mismatch");
73380
+ const owner = { instanceId: instance.id, unit: row2.unit };
73381
+ const publication = {
73382
+ repo: instance.repo,
73383
+ pr: pr.number,
73384
+ headRef: row2.branch,
73385
+ baseRef: instance.base,
73386
+ expectedHeadSha: verified.sha,
73387
+ publicationRef: row2.branch,
73388
+ owner
73389
+ };
73390
+ const existing = row2.publication;
73391
+ if (existing !== void 0 && (existing.repo !== publication.repo || existing.pr !== publication.pr || existing.headRef !== publication.headRef || existing.baseRef !== publication.baseRef || existing.expectedHeadSha !== publication.expectedHeadSha || existing.publicationRef !== publication.publicationRef || !samePublicationOwner(existing.owner, owner)))
73392
+ throw new PublicationBindingRefusal("publication_facts_mismatch");
73393
+ const fence = deps.runnerOwnership;
73394
+ if (fence === void 0) throw new PublicationBindingRefusal("publication_ownership_unknown");
73395
+ let priorOwner;
73396
+ try {
73397
+ priorOwner = fence.owner(instance.repo, pr.number);
73398
+ } catch {
73399
+ throw new PublicationBindingRefusal("publication_ownership_unknown");
73400
+ }
73401
+ if (priorOwner !== void 0 && !samePublicationOwner(priorOwner, owner))
73402
+ throw new PublicationBindingRefusal("publication_ownership_changed");
73403
+ if (!fence.claim(instance.repo, pr.number, owner))
73404
+ throw new PublicationBindingRefusal("publication_ownership_changed");
73405
+ let claimedHere = priorOwner === void 0;
73406
+ const releaseClaim = () => {
73407
+ if (!claimedHere) return;
73408
+ fence.release(instance.repo, pr.number, owner);
73409
+ claimedHere = false;
73410
+ };
73411
+ try {
73412
+ if (!samePublicationOwner(fence.owner(instance.repo, pr.number), owner)) {
73413
+ releaseClaim();
73414
+ throw new PublicationBindingRefusal("publication_ownership_unknown");
73415
+ }
73416
+ } catch (err2) {
73417
+ releaseClaim();
73418
+ if (err2 instanceof PublicationBindingRefusal) throw err2;
73419
+ throw new PublicationBindingRefusal("publication_ownership_unknown");
73420
+ }
73421
+ const replacement = { ...row2, pr, publication };
73422
+ if (JSON.stringify(replacement) === JSON.stringify(row2)) return;
73423
+ let replaced;
73424
+ try {
73425
+ replaced = await deps.instances.compareAndReplaceUnit(row2, replacement);
73426
+ } catch {
73427
+ releaseClaim();
73428
+ throw new PublicationBindingRefusal("publication_store_unavailable");
73429
+ }
73430
+ if (!replaced.ok) {
73431
+ releaseClaim();
73432
+ throw new PublicationBindingRefusal(
73433
+ replaced.reason === "stale" ? "publication_binding_stale" : "publication_store_unavailable"
73434
+ );
73435
+ }
73436
+ }
73366
73437
  async function prCheck(body, deps) {
73367
73438
  const id = parseInstanceId(body.parentInstanceId);
73368
73439
  if (!id.ok) return json3(400, { ok: false, error: id.error });
@@ -73379,19 +73450,9 @@ async function prCheck(body, deps) {
73379
73450
  const unit = await unitRowOf(deps, instance, body.unit);
73380
73451
  if (!unit.ok) return unit.response;
73381
73452
  const branch2 = unit.row?.branch ?? instance.branch;
73382
- const remember = async (pr, facts2) => {
73383
- if (!unit.row) return;
73384
- const publication = unit.row.publication ?? (facts2?.state === "open" && facts2.sameRepoHead === true && facts2.headBranchExists === true && typeof facts2.headRef === "string" && typeof facts2.baseRef === "string" && typeof facts2.headSha === "string" && /^[0-9a-f]{40}$/i.test(facts2.headSha) ? {
73385
- repo: instance.repo,
73386
- pr: pr.number,
73387
- headRef: facts2.headRef,
73388
- baseRef: facts2.baseRef,
73389
- expectedHeadSha: facts2.headSha,
73390
- publicationRef: facts2.headRef,
73391
- owner: { instanceId: instance.id, unit: unit.row.unit }
73392
- } : void 0);
73393
- if (unit.row.pr?.number !== pr.number || unit.row.pr.url !== pr.url || unit.row.publication === void 0 && publication !== void 0)
73394
- await deps.instances.putUnits([{ ...unit.row, pr, ...publication !== void 0 ? { publication } : {} }]);
73453
+ const rememberTerminal = async (pr) => {
73454
+ if (!unit.row || unit.row.pr?.number === pr.number && unit.row.pr.url === pr.url) return;
73455
+ await deps.instances.putUnits([{ ...unit.row, pr }]);
73395
73456
  };
73396
73457
  try {
73397
73458
  if (follow !== void 0) {
@@ -73400,12 +73461,12 @@ async function prCheck(body, deps) {
73400
73461
  const fallbackUrl = unit.row?.pr?.url ?? `https://github.com/${instance.repo}/pull/${follow}`;
73401
73462
  const state2 = pullRequestState(follow, fallbackUrl, facts2);
73402
73463
  if (state2 === void 0) throw new Error(headBranchStateError(instance.repo, follow));
73403
- await remember({ number: follow, url: String(state2.url) }, facts2);
73404
- if (state2.state !== "open") return json3(200, { ok: true, ...state2, at });
73405
- deps.runnerOwnership?.claim(instance.repo, follow, {
73406
- instanceId: instance.id,
73407
- unit: unit.row?.unit ?? body.unit
73408
- });
73464
+ const followedPr = { number: follow, url: String(state2.url) };
73465
+ if (state2.state !== "open") {
73466
+ await rememberTerminal(followedPr);
73467
+ return json3(200, { ok: true, ...state2, at });
73468
+ }
73469
+ await bindOpenPullRequest(deps, instance, unit.row, followedPr, facts2);
73409
73470
  const prRef = { repo: instance.repo, number: follow };
73410
73471
  const headSha = facts2.headSha;
73411
73472
  const approved = entry && headSha !== void 0 ? await reviewPostedAt(deps, prRef, "approve", headSha) : void 0;
@@ -73436,16 +73497,16 @@ async function prCheck(body, deps) {
73436
73497
  }
73437
73498
  const open2 = await deps.findOpenPrByHead(instance.repo, branch2);
73438
73499
  if (open2) {
73439
- deps.runnerOwnership?.claim(instance.repo, open2.number, {
73440
- instanceId: instance.id,
73441
- unit: unit.row?.unit ?? body.unit
73442
- });
73443
73500
  const liveFacts = await deps.fetchPrFacts({ repo: instance.repo, number: open2.number });
73444
73501
  if (liveFacts === void 0) throw new Error(`could not read ${instance.repo}#${open2.number}`);
73445
- await remember({ number: open2.number, url: open2.htmlUrl }, liveFacts);
73446
73502
  const current = pullRequestState(open2.number, open2.htmlUrl, liveFacts);
73447
73503
  if (current === void 0) throw new Error(headBranchStateError(instance.repo, open2.number));
73448
- if (current.state !== "open") return json3(200, { ok: true, ...current, at });
73504
+ const discoveredPr = { number: open2.number, url: open2.htmlUrl };
73505
+ if (current.state !== "open") {
73506
+ await rememberTerminal(discoveredPr);
73507
+ return json3(200, { ok: true, ...current, at });
73508
+ }
73509
+ await bindOpenPullRequest(deps, instance, unit.row, discoveredPr, liveFacts);
73449
73510
  const entryFacts = entry ? liveFacts : void 0;
73450
73511
  const branchHead = entryFacts?.headSha;
73451
73512
  const entryHead = branchHead ?? open2.headSha;
@@ -73496,22 +73557,27 @@ async function prCheck(body, deps) {
73496
73557
  }
73497
73558
  const recovered = await recoverPushedBranch(deps, instance, unit.row, branch2, recover.runId);
73498
73559
  if (recovered.kind === "opened") {
73499
- await remember({ number: recovered.pr.number, url: recovered.pr.htmlUrl });
73500
73560
  const recoveredFacts = await deps.fetchPrFacts({ repo: instance.repo, number: recovered.pr.number });
73501
73561
  if (recoveredFacts === void 0) throw new Error(`could not read ${instance.repo}#${recovered.pr.number}`);
73502
73562
  const recoveredState = pullRequestState(recovered.pr.number, recovered.pr.htmlUrl, recoveredFacts);
73503
73563
  if (recoveredState === void 0) throw new Error(headBranchStateError(instance.repo, recovered.pr.number));
73564
+ const recoveredPr = { number: recovered.pr.number, url: recovered.pr.htmlUrl };
73565
+ if (recoveredState.state === "open")
73566
+ await bindOpenPullRequest(deps, instance, unit.row, recoveredPr, recoveredFacts);
73567
+ else await rememberTerminal(recoveredPr);
73504
73568
  return json3(200, { ok: true, ...recoveredState, at });
73505
73569
  }
73506
73570
  return json3(200, { ok: true, state: "none", unrecovered: recovered.why, at });
73507
73571
  }
73508
- await remember({ number: merged.number, url: merged.htmlUrl });
73509
73572
  const mergedFacts = await deps.fetchPrFacts({ repo: instance.repo, number: merged.number });
73510
73573
  if (mergedFacts === void 0) throw new Error(`could not read ${instance.repo}#${merged.number}`);
73511
73574
  const state = pullRequestState(merged.number, merged.htmlUrl, mergedFacts);
73512
73575
  if (state?.state !== "merged") throw new Error(`${instance.repo}#${merged.number} no longer reads merged`);
73576
+ await rememberTerminal({ number: merged.number, url: merged.htmlUrl });
73513
73577
  return json3(200, { ok: true, ...state, at });
73514
73578
  } catch (err2) {
73579
+ if (err2 instanceof PublicationBindingRefusal)
73580
+ return json3(409, { ok: false, error: err2.reason, message: "the open pull request was not durably bound", at });
73515
73581
  return json3(502, { ok: false, error: "github_unavailable", message: describe12(err2), at });
73516
73582
  }
73517
73583
  }
@@ -74734,7 +74800,7 @@ function createAdminCoordinatorHandler(deps) {
74734
74800
  })();
74735
74801
  };
74736
74802
  }
74737
- var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json3, describe12, isGenerated2, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, githubSecondStart, CHILD_SUPERSESSION_REASONS, ROUND_OUTCOMES, ENDING_ICON, STEPS;
74803
+ var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json3, describe12, isGenerated2, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, githubSecondStart, CHILD_SUPERSESSION_REASONS, PublicationBindingRefusal, samePublicationOwner, ROUND_OUTCOMES, ENDING_ICON, STEPS;
74738
74804
  var init_adminCoordinator = __esm({
74739
74805
  "../../src/channels/adminCoordinator.ts"() {
74740
74806
  "use strict";
@@ -74790,6 +74856,14 @@ var init_adminCoordinator = __esm({
74790
74856
  REVIEW_POSTED_RECHECK_MS = 2e3;
74791
74857
  githubSecondStart = (at) => Math.floor(at / SECOND_MS) * SECOND_MS;
74792
74858
  CHILD_SUPERSESSION_REASONS = /* @__PURE__ */ new Set(["merged", "closed", "head_moved", "branch_deleted"]);
74859
+ PublicationBindingRefusal = class extends Error {
74860
+ constructor(reason2) {
74861
+ super(reason2);
74862
+ this.reason = reason2;
74863
+ }
74864
+ reason;
74865
+ };
74866
+ samePublicationOwner = (left, right) => left?.instanceId === right.instanceId && left.unit === right.unit;
74793
74867
  ROUND_OUTCOMES = [
74794
74868
  "started",
74795
74869
  "pr_opened",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreplane/switchboard",
3
- "version": "1.261.3",
3
+ "version": "1.261.4",
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",