@coreplane/switchboard 1.267.1 → 1.267.3

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 (153) hide show
  1. package/dist/assets/deploy/cloudflare-memory/testDiagnostics.ts +44 -3
  2. package/dist/assets/deploy/cloudflare-memory/testDiagnosticsProtocol.ts +7 -0
  3. package/dist/assets/deploy/cloudflare-memory/testDiagnosticsReporter.ts +22 -0
  4. package/dist/assets/deploy/cloudflare-memory/testFetch.ts +21 -0
  5. package/dist/assets/deploy/cloudflare-memory/worker.ts +43 -0
  6. package/dist/assets/deploy/cloudflare-resident/runRegistration.ts +68 -0
  7. package/dist/assets/deploy/cloudflare-resident/worker.ts +379 -165
  8. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +17 -2
  9. package/dist/assets/package-lock.json +51 -4
  10. package/dist/assets/package.json +1 -1
  11. package/dist/assets/source.json +3 -3
  12. package/dist/assets/src/agents/registry.ts +2 -2
  13. package/dist/assets/src/core/budgets.ts +2 -0
  14. package/dist/assets/src/core/publicationPush.ts +31 -9
  15. package/dist/assets/src/core/runEvents.ts +17 -1
  16. package/dist/assets/src/core/runRecord.ts +17 -1
  17. package/dist/assets/src/core/ship/coordinator.ts +14 -1
  18. package/dist/assets/src/core/trace/attrs.ts +22 -0
  19. package/dist/assets/src/core/trace/tracer.ts +10 -2
  20. package/dist/assets/src/core/trace/types.ts +12 -1
  21. package/dist/assets/src/core/trace/workerTrace.ts +2 -2
  22. package/dist/assets/src/execution/seedPlan.ts +13 -4
  23. package/dist/assets/web/dist/.vite/manifest.json +488 -488
  24. package/dist/assets/web/dist/assets/AppShell-CCPwPqvc.js +1 -0
  25. package/dist/assets/web/dist/assets/{CostChart-4TeDdbvJ.js → CostChart-Y5BqJKLA.js} +2 -2
  26. package/dist/assets/web/dist/assets/CostsPage-RRggVvIx.js +1 -0
  27. package/dist/assets/web/dist/assets/DeliveryPage-CuIBLfup.js +1 -0
  28. package/dist/assets/web/dist/assets/HomePage-BQmun9kS.js +1 -0
  29. package/dist/assets/web/dist/assets/InputMenu-ymuqElZB.js +1 -0
  30. package/dist/assets/web/dist/assets/{MetricsPage-BQBuIuYU.js → MetricsPage-D2YGsI8C.js} +1 -1
  31. package/dist/assets/web/dist/assets/NotFoundPage-DZ6ysTXd.js +1 -0
  32. package/dist/assets/web/dist/assets/PendingTurnRow-BPyJ2f-i.js +1 -0
  33. package/dist/assets/web/dist/assets/PlanePage-C_gVx0Dg.js +1 -0
  34. package/dist/assets/web/dist/assets/ResidentDetailPage-CT6v9INM.js +1 -0
  35. package/dist/assets/web/dist/assets/ResidentsIndexPage-CWqPFf40.js +1 -0
  36. package/dist/assets/web/dist/assets/{RunFoldRow-BOWvSoIv.js → RunFoldRow-C1w-mwch.js} +1 -1
  37. package/dist/assets/web/dist/assets/RunRoutePage-zWMHTzla.js +9 -0
  38. package/dist/assets/web/dist/assets/RunsIndexPage-DF16D2Gg.js +1 -0
  39. package/dist/assets/web/dist/assets/RunsTabs-D7rsC4GR.js +1 -0
  40. package/dist/assets/web/dist/assets/ScheduledPage-CyRs0fXN.js +1 -0
  41. package/dist/assets/web/dist/assets/SettingSelect-CEkCLVWd.js +1 -0
  42. package/dist/assets/web/dist/assets/SettingsPage-BRuMCHSj.js +1 -0
  43. package/dist/assets/web/dist/assets/SilentTurn-D4MMDjWO.js +2 -0
  44. package/dist/assets/web/dist/assets/SlackMark-DjJTK2tD.js +1 -0
  45. package/dist/assets/web/dist/assets/StatusDot-BIAQN7iD.js +1 -0
  46. package/dist/assets/web/dist/assets/Tooltip-CxiQ7luL.js +1 -0
  47. package/dist/assets/web/dist/assets/UnitRoutePage-BQB5mAAl.js +1 -0
  48. package/dist/assets/web/dist/assets/{angular-html-BRRhsdQE.js → angular-html-DR_aiq_z.js} +1 -1
  49. package/dist/assets/web/dist/assets/{angular-ts-BFj-mqCm.js → angular-ts-DdAdPC99.js} +1 -1
  50. package/dist/assets/web/dist/assets/{apl-CsBuy1IL.js → apl-CFsVwJXV.js} +1 -1
  51. package/dist/assets/web/dist/assets/{astro-cVaqxBZq.js → astro-CV6XNX3T.js} +1 -1
  52. package/dist/assets/web/dist/assets/{blade-MEP5mWha.js → blade-BGvEqTv7.js} +1 -1
  53. package/dist/assets/web/dist/assets/browser-BWnzWwaT.js +6 -0
  54. package/dist/assets/web/dist/assets/{c-D4NGQ_Pd.js → c-ViUYuFAx.js} +1 -1
  55. package/dist/assets/web/dist/assets/{chapel-BkWImsIy.js → chapel-CZ7_YbfP.js} +1 -1
  56. package/dist/assets/web/dist/assets/{cobol-RPDSPSET.js → cobol-Dc8kYvS6.js} +1 -1
  57. package/dist/assets/web/dist/assets/{coffee-DZ5z7oVX.js → coffee-C0musOvn.js} +1 -1
  58. package/dist/assets/web/dist/assets/{cpp-CrBtIaRZ.js → cpp-ToPQGcGd.js} +1 -1
  59. package/dist/assets/web/dist/assets/{crystal-DJesYi6f.js → crystal-DUJIaO8W.js} +1 -1
  60. package/dist/assets/web/dist/assets/{css-DO9mjau-.js → css-BTcEgWyQ.js} +1 -1
  61. package/dist/assets/web/dist/assets/{dist-XRrc0hhm.js → dist-I2OEmPqz.js} +2 -2
  62. package/dist/assets/web/dist/assets/{edge-C3VzbSW0.js → edge-D8sh1uAj.js} +1 -1
  63. package/dist/assets/web/dist/assets/{elixir-BkD_Vhfe.js → elixir-D5vOtHrB.js} +1 -1
  64. package/dist/assets/web/dist/assets/{elm-KIM0mFPk.js → elm-B_5xqTuz.js} +1 -1
  65. package/dist/assets/web/dist/assets/{erb-D2ruCqb2.js → erb-DBEct7_l.js} +1 -1
  66. package/dist/assets/web/dist/assets/{eventSource-Cv0AFlMi.js → eventSource-Ds6_gjPD.js} +1 -1
  67. package/dist/assets/web/dist/assets/{git-rebase-CnfbQTxA.js → git-rebase-lWoTL0Qh.js} +1 -1
  68. package/dist/assets/web/dist/assets/{glimmer-js-CbiwFXWH.js → glimmer-js-DNfSP4GT.js} +1 -1
  69. package/dist/assets/web/dist/assets/{glimmer-ts-DV8ii0Pv.js → glimmer-ts-DpPeWMLs.js} +1 -1
  70. package/dist/assets/web/dist/assets/{glsl-CgmznWkA.js → glsl-m6Ziz8q0.js} +1 -1
  71. package/dist/assets/web/dist/assets/{graphql-BK-bBYfH.js → graphql-shD9521e.js} +1 -1
  72. package/dist/assets/web/dist/assets/{hack-DnwTTP5n.js → hack-D_Mf_2gv.js} +1 -1
  73. package/dist/assets/web/dist/assets/{haml-DAVJsWwT.js → haml-BuEAF47S.js} +1 -1
  74. package/dist/assets/web/dist/assets/{handlebars-voOVfxFL.js → handlebars-D2aiSYtv.js} +1 -1
  75. package/dist/assets/web/dist/assets/{html-3FTqwaPk.js → html-CRjXJZot.js} +1 -1
  76. package/dist/assets/web/dist/assets/{html-derivative-CSlhLDQn.js → html-derivative-CYpQX_KK.js} +1 -1
  77. package/dist/assets/web/dist/assets/{http-Vx4ilPfS.js → http-D6vXMpc_.js} +1 -1
  78. package/dist/assets/web/dist/assets/{hurl-DxyZDE3R.js → hurl-B634cMJT.js} +1 -1
  79. package/dist/assets/web/dist/assets/{java-nWqdjNJG.js → java-BlINdAve.js} +1 -1
  80. package/dist/assets/web/dist/assets/{javascript-Dr82HOwp.js → javascript-BBEMmUOu.js} +1 -1
  81. package/dist/assets/web/dist/assets/{jinja-BhjLrPOt.js → jinja-BIA8TYHJ.js} +1 -1
  82. package/dist/assets/web/dist/assets/{jison-DilGIUa8.js → jison-aepfTbzH.js} +1 -1
  83. package/dist/assets/web/dist/assets/{json-CrOcvmhJ.js → json-DmYZ1boG.js} +1 -1
  84. package/dist/assets/web/dist/assets/{jsx-BW_RX-fo.js → jsx-GkaaF4_W.js} +1 -1
  85. package/dist/assets/web/dist/assets/{julia-CWH071DB.js → julia-DRtMztOz.js} +1 -1
  86. package/dist/assets/web/dist/assets/{just-DkhhoWBy.js → just-PvoeEWoy.js} +1 -1
  87. package/dist/assets/web/dist/assets/{latex-cuHJ4TUT.js → latex-Bujuo3AZ.js} +1 -1
  88. package/dist/assets/web/dist/assets/{liquid-DG6afYQN.js → liquid-B0h0HkS3.js} +1 -1
  89. package/dist/assets/web/dist/assets/{lua-C5CMnCPS.js → lua-6O-O1CSO.js} +1 -1
  90. package/dist/assets/web/dist/assets/main-DYwKyLY5.js +28 -0
  91. package/dist/assets/web/dist/assets/{marko-Dy2_DuC4.js → marko-XS_tpV0N.js} +1 -1
  92. package/dist/assets/web/dist/assets/{mdc-BMKb_EeA.js → mdc-DxULuTS1.js} +1 -1
  93. package/dist/assets/web/dist/assets/{nginx-og0RgMKf.js → nginx-n2RAfCKE.js} +1 -1
  94. package/dist/assets/web/dist/assets/{nim-CQxTQQP0.js → nim-DJxdqD8U.js} +1 -1
  95. package/dist/assets/web/dist/assets/{org-b5lkq_Pl.js → org-R9yxwZ2v.js} +1 -1
  96. package/dist/assets/web/dist/assets/{perl-D9X5_i_J.js → perl-zKAUtl73.js} +1 -1
  97. package/dist/assets/web/dist/assets/{php-CwUxvLX0.js → php-Dk50x1WE.js} +1 -1
  98. package/dist/assets/web/dist/assets/{pug-uOIOExr0.js → pug-C3TSg8JJ.js} +1 -1
  99. package/dist/assets/web/dist/assets/{qml-g5RoADf-.js → qml-CL1nqvCt.js} +1 -1
  100. package/dist/assets/web/dist/assets/{r-5LdAopD3.js → r-Bo44Yl7y.js} +1 -1
  101. package/dist/assets/web/dist/assets/{razor-DwrvFLYQ.js → razor-k2Dt2qXz.js} +1 -1
  102. package/dist/assets/web/dist/assets/{regexp-dGtAaxC-.js → regexp-Uy_HNe6G.js} +1 -1
  103. package/dist/assets/web/dist/assets/{rst-wFdW0brr.js → rst-U0zcRJ60.js} +1 -1
  104. package/dist/assets/web/dist/assets/{ruby-DPglN6I9.js → ruby-DUyqwP37.js} +1 -1
  105. package/dist/assets/web/dist/assets/{sas-CUbCs40d.js → sas-CxC8bRy1.js} +1 -1
  106. package/dist/assets/web/dist/assets/{scss-B-DUiI1c.js → scss-DaqXj6RQ.js} +1 -1
  107. package/dist/assets/web/dist/assets/{shellscript-lGOV7Ram.js → shellscript-CI49IoFh.js} +1 -1
  108. package/dist/assets/web/dist/assets/{shellsession-BDWtAAjr.js → shellsession-DJxotIOK.js} +1 -1
  109. package/dist/assets/web/dist/assets/{soy-zod_dcOy.js → soy-BjVAVuCU.js} +1 -1
  110. package/dist/assets/web/dist/assets/{sql-BG0HSSoM.js → sql-CzxBKQd1.js} +1 -1
  111. package/dist/assets/web/dist/assets/{sseReplay-BsNQPLmJ.js → sseReplay-BDVyc8HX.js} +3 -3
  112. package/dist/assets/web/dist/assets/{stata-BVUMLp1W.js → stata-DLMv_9_X.js} +1 -1
  113. package/dist/assets/web/dist/assets/{surrealql-DiSUbHKF.js → surrealql-BzbVEZbO.js} +1 -1
  114. package/dist/assets/web/dist/assets/{svelte-DZenT45Y.js → svelte-DTSfopUt.js} +1 -1
  115. package/dist/assets/web/dist/assets/{templ-BWKRNM0-.js → templ-BPxFlKQj.js} +1 -1
  116. package/dist/assets/web/dist/assets/{tex-CbMzBlkK.js → tex-B_KndQwK.js} +1 -1
  117. package/dist/assets/web/dist/assets/{ts-tags-CZsVorDN.js → ts-tags-CG7tgvBl.js} +1 -1
  118. package/dist/assets/web/dist/assets/{tsx-R1r3mH4W.js → tsx-CN9eqIdJ.js} +1 -1
  119. package/dist/assets/web/dist/assets/{twig-BjPMyjKH.js → twig-CKPbB5ca.js} +1 -1
  120. package/dist/assets/web/dist/assets/{typescript-Bx-66VN_.js → typescript-BQAXVjHJ.js} +1 -1
  121. package/dist/assets/web/dist/assets/{typst-fW037Eo6.js → typst-D3EaP9Ui.js} +1 -1
  122. package/dist/assets/web/dist/assets/{vue-D1V_GNxW.js → vue-DuQwQ__D.js} +1 -1
  123. package/dist/assets/web/dist/assets/{vue-html-C-4pTZ-s.js → vue-html-UEJn5cLD.js} +1 -1
  124. package/dist/assets/web/dist/assets/{vue-vine-C-QsX3hR.js → vue-vine-DDdijZY2.js} +1 -1
  125. package/dist/assets/web/dist/assets/{wallClock-x0hjr0J_.js → wallClock-DaewxNMY.js} +1 -1
  126. package/dist/assets/web/dist/assets/{xml-BJTJXPgC.js → xml-BezNDynk.js} +1 -1
  127. package/dist/assets/web/dist/assets/{xsl-Xc9lukqe.js → xsl-Bzb2zIr8.js} +1 -1
  128. package/dist/assets/web/dist/assets/{yaml-Dw-FMsQa.js → yaml-DOccVRRG.js} +1 -1
  129. package/dist/cli.js +868 -226
  130. package/package.json +1 -1
  131. package/dist/assets/web/dist/assets/AppShell-DPJVhUGP.js +0 -1
  132. package/dist/assets/web/dist/assets/CostsPage-8HDgvuWB.js +0 -1
  133. package/dist/assets/web/dist/assets/DeliveryPage-QyS6_Q0m.js +0 -1
  134. package/dist/assets/web/dist/assets/HomePage-Bm40r0wF.js +0 -1
  135. package/dist/assets/web/dist/assets/InputMenu-BRV35UWP.js +0 -1
  136. package/dist/assets/web/dist/assets/NotFoundPage-CDjMfCcj.js +0 -1
  137. package/dist/assets/web/dist/assets/PendingTurnRow-BqY-EaZR.js +0 -1
  138. package/dist/assets/web/dist/assets/PlanePage-VmzjNLAz.js +0 -1
  139. package/dist/assets/web/dist/assets/ResidentDetailPage-hjX3tRCh.js +0 -1
  140. package/dist/assets/web/dist/assets/ResidentsIndexPage-BKGnlCEh.js +0 -1
  141. package/dist/assets/web/dist/assets/RunRoutePage-D9ztf-uE.js +0 -9
  142. package/dist/assets/web/dist/assets/RunsIndexPage-DTYQbhtr.js +0 -1
  143. package/dist/assets/web/dist/assets/RunsTabs-C7Ua3XX5.js +0 -1
  144. package/dist/assets/web/dist/assets/ScheduledPage-BAYR0tuv.js +0 -1
  145. package/dist/assets/web/dist/assets/SettingSelect-CEeA4iuC.js +0 -1
  146. package/dist/assets/web/dist/assets/SettingsPage-BlYBmCG9.js +0 -1
  147. package/dist/assets/web/dist/assets/SilentTurn-CHfzRQW8.js +0 -2
  148. package/dist/assets/web/dist/assets/SlackMark-DB0MGiF5.js +0 -1
  149. package/dist/assets/web/dist/assets/StatusDot-BMkUFVE8.js +0 -1
  150. package/dist/assets/web/dist/assets/Tooltip-Dfot16IA.js +0 -1
  151. package/dist/assets/web/dist/assets/UnitRoutePage-BdZoT-4d.js +0 -1
  152. package/dist/assets/web/dist/assets/browser-CCImen5O.js +0 -6
  153. package/dist/assets/web/dist/assets/main-B32N1vUh.js +0 -28
@@ -95,6 +95,7 @@ import {
95
95
  SEED_MARKER,
96
96
  SEED_RESTORE_MAX_MS,
97
97
  SEED_ABANDONED_RESTORE_WAIT_MS,
98
+ seedDoorRemote,
98
99
  seedFixupScript,
99
100
  seedMarkerText,
100
101
  type RestorePhases,
@@ -451,12 +452,25 @@ export class SwitchboardSandbox extends Sandbox<Env> {
451
452
  detail: `presigned R2 transfer needs ${transfer.missing.join(", ")}`,
452
453
  };
453
454
  }
455
+ let doorRemote: string;
456
+ try {
457
+ doorRemote = seedDoorRemote(envVars.GIT_DOOR_ORIGIN ?? "", seed.slug);
458
+ } catch {
459
+ return { seeded: false, reason: "seed-failed", detail: "Git door origin unavailable", step: "fixup" };
460
+ }
454
461
  // The marker names the seed this container carries — the handle and the
455
462
  // ref and head checked out: the same seed again is the run's second
456
463
  // request (a retry, a re-attach), and a restore over the live tree would
457
464
  // destroy the run's work; a seed naming another ref is a new seed.
458
465
  const marker = await this.runRoot(["cat", SEED_MARKER], 30_000);
459
466
  if (marker.exitCode === 0 && marker.stdout.trim() === seedMarkerText(seed)) {
467
+ const origin = await this.runRoot(
468
+ ["git", "-C", SEED_CHECKOUT_DIR, "remote", "set-url", "origin", doorRemote],
469
+ 30_000,
470
+ envVars,
471
+ );
472
+ if (origin.exitCode !== 0)
473
+ return { seeded: false, reason: "seed-failed", detail: "seed origin refresh failed", step: "fixup" };
460
474
  const head = await this.runRoot(["git", "-C", SEED_CHECKOUT_DIR, "rev-parse", "HEAD"], 30_000);
461
475
  return {
462
476
  seeded: true,
@@ -490,14 +504,15 @@ export class SwitchboardSandbox extends Sandbox<Env> {
490
504
  t = systemClock();
491
505
  const script = seedFixupScript({
492
506
  slug: seed.slug,
507
+ doorOrigin: envVars.GIT_DOOR_ORIGIN ?? "",
493
508
  ref: seed.ref,
494
509
  ...(seed.fetchRef ? { fetchRef: seed.fetchRef } : {}),
495
510
  ...(seed.fetchSha ? { fetchSha: seed.fetchSha } : {}),
496
511
  checkoutDir: SEED_CHECKOUT_DIR,
497
512
  ...(seed.depsBackupId ? { depsDir: SEED_DEPS_STAGING_DIR } : {}),
498
513
  });
499
- // The fix-up's fetch authenticates through the image's credential helper
500
- // with the exec env's GH_TOKEN — the same channel every command uses.
514
+ // The fix-up's fetch authenticates through the door-scoped helper using
515
+ // the run bearer in the command environment.
501
516
  const fix = await this.runRoot(["bash", "-c", script], SEED_FIXUP_TIMEOUT_MS, envVars);
502
517
  if (fix.exitCode !== 0) throw new Error(`fix-up exited ${fix.exitCode}: ${tail(fix.stderr || fix.stdout)}`);
503
518
  steps.fixup = systemClock() - t;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.267.1",
3
+ "version": "1.267.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.267.1",
9
+ "version": "1.267.3",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -21023,7 +21023,7 @@
21023
21023
  },
21024
21024
  "packages/switchboard": {
21025
21025
  "name": "@coreplane/switchboard",
21026
- "version": "1.267.1",
21026
+ "version": "1.267.3",
21027
21027
  "license": "Apache-2.0",
21028
21028
  "dependencies": {
21029
21029
  "@earendil-works/pi-ai": "0.85.1",
@@ -21053,7 +21053,7 @@
21053
21053
  "dependencies": {
21054
21054
  "@nuxt/ui": "^4.11.0",
21055
21055
  "@pierre/diffs": "1.4.2",
21056
- "@vueuse/core": "^14.4.0",
21056
+ "@vueuse/core": "^15.0.0",
21057
21057
  "vue": "^3.5.42",
21058
21058
  "vue-router": "^5.3.1"
21059
21059
  },
@@ -21076,6 +21076,53 @@
21076
21076
  "engines": {
21077
21077
  "node": ">=22"
21078
21078
  }
21079
+ },
21080
+ "web/node_modules/@vueuse/core": {
21081
+ "version": "15.0.0",
21082
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-15.0.0.tgz",
21083
+ "integrity": "sha512-Zv67h5mrk7YHDJsRObx+DXaCBz6RKBUpxqLesJ+3xZNPG6yuQs7ja7kg3veFrrouYLWAqydQ7CwGEbLVxB43Ng==",
21084
+ "license": "MIT",
21085
+ "dependencies": {
21086
+ "@types/web-bluetooth": "^0.0.21",
21087
+ "@vueuse/metadata": "15.0.0",
21088
+ "@vueuse/shared": "15.0.0"
21089
+ },
21090
+ "engines": {
21091
+ "node": ">=22"
21092
+ },
21093
+ "funding": {
21094
+ "url": "https://github.com/sponsors/antfu"
21095
+ },
21096
+ "peerDependencies": {
21097
+ "vue": "^3.5.0"
21098
+ }
21099
+ },
21100
+ "web/node_modules/@vueuse/core/node_modules/@vueuse/shared": {
21101
+ "version": "15.0.0",
21102
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-15.0.0.tgz",
21103
+ "integrity": "sha512-vbNXhGrbV53xPOMSyOzMHQRvhMgZf7oNDw2RChUetVxwTIEkInQ1joZ9JbT0G3z2n2AJNjvmCWeM9ySYrnvogA==",
21104
+ "license": "MIT",
21105
+ "engines": {
21106
+ "node": ">=22"
21107
+ },
21108
+ "funding": {
21109
+ "url": "https://github.com/sponsors/antfu"
21110
+ },
21111
+ "peerDependencies": {
21112
+ "vue": "^3.5.0"
21113
+ }
21114
+ },
21115
+ "web/node_modules/@vueuse/metadata": {
21116
+ "version": "15.0.0",
21117
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-15.0.0.tgz",
21118
+ "integrity": "sha512-sfWE5LADAKJ0KnjtGliMdSN8Ij/L7uP6rXIZlgPloAI4zvvefbw6H5Lt7KJA6RqQOYgVqWF5Guj50HHMIzygBg==",
21119
+ "license": "MIT",
21120
+ "engines": {
21121
+ "node": ">=22"
21122
+ },
21123
+ "funding": {
21124
+ "url": "https://github.com/sponsors/antfu"
21125
+ }
21079
21126
  }
21080
21127
  }
21081
21128
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.267.1",
3
+ "version": "1.267.3",
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.267.1",
3
- "commit": "6382de7f03c465e18b040fd93103afb1dc44d543",
4
- "builtAt": "2026-09-28T23:55:40.901Z"
2
+ "version": "1.267.3",
3
+ "commit": "deb60c5f49012fb663a5797168b9e02d2ba394a7",
4
+ "builtAt": "2026-09-29T04:50:34.175Z"
5
5
  }
@@ -306,7 +306,7 @@ SCOPE FIRST — a hard rule, at most 5 tool calls: identify the target repositor
306
306
  - Use \`gh search code\` / \`gh api\` for cross-repo lookups; clone at most ONE repo per task.
307
307
 
308
308
  Workflow for shipping a PR:
309
- 1. Clone the repo into the workspace if it's not already there (use gh or git; both are authenticated on this host). Orient with a few BATCHED commands (tree + the relevant files in one call), not file-by-file exploration.
309
+ 1. Clone the repo into the workspace if it is not already there: \`gh repo clone <owner/name>\` or \`git clone "$GIT_DOOR_REMOTE"\` for the bound repository. A private \`github.com\` clone URL cannot use the run bearer. Orient with a few BATCHED commands (tree + the relevant files in one call), not file-by-file exploration.
310
310
  2. Create a branch with a descriptive name.
311
311
  3. Implement the change. Match the surrounding code's style and conventions.
312
312
  4. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change.
@@ -628,7 +628,7 @@ You cannot run commands, clone repositories, edit code, or review pull requests,
628
628
  // preset, not a directive.
629
629
  const EXPLORE_SYSTEM = `You are Switchboard's explore agent: a long, read-only investigation of a repository, answering a request from Slack.
630
630
 
631
- You work in a fresh sandbox with a shell (bash), read_file, and a read-scoped GitHub credential: git and gh are authenticated for reads, so clone the target repository into your workspace first (\`gh repo clone <owner/name>\` or \`git clone\`; check out the ref the request names), install what you need and run whatever the investigation calls for — builds, test suites, benchmarks, \`act\`. ${SANDBOX_TOOLCHAIN} You cannot push. Your other tools: \`web_search\` and \`web_fetch\` (sources and pages), the GitHub reads — \`github_repos\`, \`github_tree\` / \`github_file\` (browse and read our repos at any ref), \`github_search_code\`, \`github_issue_list\` / \`github_issue_get\` — and \`list_skills\` / \`use_skill\`.
631
+ You work in a fresh sandbox with a shell (bash), read_file, and a read-scoped GitHub credential: clone the bound repository first with \`gh repo clone <owner/name>\` or \`git clone "$GIT_DOOR_REMOTE"\`, then check out the ref the request names. A private \`github.com\` clone URL cannot use the run bearer. Install what you need and run whatever the investigation calls for — builds, test suites, benchmarks, \`act\`. ${SANDBOX_TOOLCHAIN} You cannot push. Your other tools: \`web_search\` and \`web_fetch\` (sources and pages), the GitHub reads — \`github_repos\`, \`github_tree\` / \`github_file\` (browse and read our repos at any ref), \`github_search_code\`, \`github_issue_list\` / \`github_issue_get\` — and \`list_skills\` / \`use_skill\`.
632
632
 
633
633
  THE DELIVERABLE IS A CLAIM TABLE. Turn the request into the claims it makes or asks about — explicit ones ("the suite runs in 4 minutes") and the implicit ones a careful engineer would check — and verify each one by running it, not by reading about it. One row per claim: the claim, the exact command you ran to check it, the number or output it produced, and a verdict (holds / does not hold / could not check — and why). Numbers over adjectives: measure a duration, count the failures, quote the version. Say what you did not get to.
634
634
 
@@ -21,6 +21,8 @@ export const SECOND_MS = 1_000;
21
21
  export const secondsToMs = (seconds: number): number => seconds * SECOND_MS;
22
22
 
23
23
  export const MINUTE_MS = 60_000;
24
+ /** Allow the run's ending to release its resident workspace after its budget ends. */
25
+ export const RUN_REGISTRATION_GRACE_MS = MINUTE_MS;
24
26
 
25
27
  /** Minutes → milliseconds, for a duration a request names in minutes (a drain's
26
28
  * length): the multiplication lives here so no other file holds it. */
@@ -15,14 +15,19 @@ const refName = (s: string): boolean =>
15
15
  /^[a-zA-Z0-9_/-]+(?:\.[a-zA-Z0-9_/-]+)*$/.test(s) && !s.includes("..") && !s.startsWith("-");
16
16
  const shortRef = (s: string): string => s.replace(/^refs\/heads\//, "");
17
17
 
18
+ /** Broad enough to count competing Git config pushes; leasedPushCommand
19
+ * separately proves whether one selected call is authorized evidence. */
20
+ export const mentionsGitPush = (command: string): boolean => /\bgit(?:\s+-c\s+\S+)*\s+push\b/.test(command);
21
+
18
22
  /** Deliberately not a shell interpreter. Only a standalone literal push can
19
23
  * attest its result: pipelines, scripts, substitutions and compound commands
20
24
  * cannot prove which operation produced stdout or the successful exit. */
21
25
  export function leasedPushCommand(command: string): { ref: string; expectedHeadSha: string } | undefined {
22
26
  if (command.length >= COMMAND_CAP) return;
23
27
  const literal = command.replace(/[ \t]+2>&1[ \t]*$/, "");
24
- if (!/^git push\s/.test(literal) || /[\n\r'"`$;|<>\\&]/.test(literal)) return;
25
- const words = literal.trim().split(/\s+/).slice(2);
28
+ const prefix = /^git(?: -c http\.postBuffer=[0-9]+)? push\s/.exec(literal);
29
+ if (!prefix || /[\n\r'"`$;|<>\\&]/.test(literal)) return;
30
+ const words = literal.slice(prefix[0].length).trim().split(/\s+/);
26
31
  const leases = words.filter((w) => w.startsWith("--force-with-lease="));
27
32
  if (leases.length !== 1) return;
28
33
  const lease = /^--force-with-lease=refs\/heads\/([^:]+):([a-f0-9]{40})$/.exec(leases[0]!);
@@ -42,13 +47,14 @@ export function pairedPublicationPush(
42
47
  binding: ExistingPrPublicationBinding,
43
48
  head: string,
44
49
  callId?: string,
50
+ gitDoorBaseUrl?: string,
45
51
  ): PushEvent | undefined {
46
52
  if (!fullSha(head) || !fullSha(binding.expectedHeadSha) || head === binding.expectedHeadSha) return;
47
53
  const calls = events.filter(
48
54
  (e): e is Extract<RunEvent, { type: "tool_call" }> =>
49
55
  e.type === "tool_call" &&
50
56
  e.tool === "bash" &&
51
- (callId === undefined ? /\bgit\s+push\b/.test(e.command ?? e.summary) : e.callId === callId),
57
+ (callId === undefined ? mentionsGitPush(e.command ?? e.summary) : e.callId === callId),
52
58
  );
53
59
  if (calls.length !== 1) return;
54
60
  const call = calls[0]!;
@@ -76,20 +82,36 @@ export function pairedPublicationPush(
76
82
  )
77
83
  return;
78
84
  let lines = result.output.trim().split(/\r?\n/);
79
- const tracking = `branch '${binding.publicationRef}' set up to track 'origin/${binding.publicationRef}'.`;
85
+ const tracking = [
86
+ `branch '${binding.publicationRef}' set up to track 'origin/${binding.publicationRef}'.`,
87
+ `Branch '${binding.publicationRef}' set up to track remote branch '${binding.publicationRef}' from 'origin'.`,
88
+ ];
80
89
  if (
81
90
  /(?:^|\s)(?:-u|--set-upstream)(?:\s|$)/.test(call.command) &&
82
- lines.filter((line) => line === tracking).length === 1
91
+ lines.filter((line) => tracking.includes(line)).length === 1
83
92
  )
84
- lines = lines.filter((line) => line !== tracking);
93
+ lines = lines.filter((line) => !tracking.includes(line));
85
94
  if (lines.length !== 2) return;
86
- const remote = /^To (?:https:\/\/github\.com\/|git@github\.com:|github\.com:)([^\s]+?)(?:\.git)?$/.exec(lines[0]!);
95
+ const githubRemote = /^To (?:https:\/\/github\.com\/|git@github\.com:|github\.com:)([^\s]+?)(?:\.git)?$/.exec(
96
+ lines[0]!,
97
+ );
98
+ let doorRemote: string | undefined;
99
+ if (gitDoorBaseUrl !== undefined) {
100
+ try {
101
+ doorRemote = `${new URL(gitDoorBaseUrl).origin}/git/${binding.repo}`;
102
+ } catch {
103
+ return;
104
+ }
105
+ }
106
+ const remoteMatches =
107
+ doorRemote === undefined
108
+ ? githubRemote?.[1]?.toLowerCase() === binding.repo.toLowerCase()
109
+ : lines[0] === `To ${doorRemote}` || lines[0] === `To ${doorRemote}.git`;
87
110
  const update = /^\s*\+?\s*([a-f0-9]{7,40})(\.{2,3})([a-f0-9]{7,40})\s+(\S+) -> (\S+)( \(forced update\))?$/.exec(
88
111
  lines[1]!,
89
112
  );
90
113
  if (
91
- !remote ||
92
- remote[1]!.toLowerCase() !== binding.repo.toLowerCase() ||
114
+ !remoteMatches ||
93
115
  !update ||
94
116
  !binding.expectedHeadSha.startsWith(update[1]!) ||
95
117
  !head.startsWith(update[3]!) ||
@@ -1142,7 +1142,15 @@ export type RunEvent =
1142
1142
  * `model` is the ref the run uses, resolved from a model the person
1143
1143
  * named in plain words (the plain-words model unit) — applied at
1144
1144
  * directive precedence. Additive: unknown → no model. */
1145
- binds?: ReadonlyArray<{ line: string; reason: string; model?: string; repo?: string; confirmed?: true }>;
1145
+ binds?: ReadonlyArray<{
1146
+ line: string;
1147
+ reason: string;
1148
+ model?: string;
1149
+ repo?: string;
1150
+ /** The admissible fact behind the model's typed repo choice. */
1151
+ repoSource?: "request" | "attachment" | "thread" | "channel";
1152
+ confirmed?: true;
1153
+ }>;
1146
1154
  question?: string;
1147
1155
  /** A question's proposed line, redacted and cut like the receipt — what
1148
1156
  * the next turn's "yes" binds (`bindFromAnswer`). */
@@ -1160,6 +1168,14 @@ export type RunEvent =
1160
1168
  * boundary. Its refusal text is always the cause-owned renderer. */
1161
1169
  providerFailure?: ProviderFailureCause;
1162
1170
  attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
1171
+ /** Snapshot of the factual repository context the operator received. */
1172
+ repoContext?: {
1173
+ organization: string;
1174
+ threadRepo?: string;
1175
+ channelRepo?: string;
1176
+ candidateStatus: "skipped" | "available" | "truncated" | "empty" | "unavailable";
1177
+ candidateCount: number;
1178
+ };
1163
1179
  intake?: { verdict: string; reason: string };
1164
1180
  latencyMs?: number;
1165
1181
  outputTokens?: number;
@@ -484,7 +484,14 @@ export interface RunOperatorDecision {
484
484
  mode: "shadow" | "on";
485
485
  outcome: "binds" | "question" | "refusal" | "non_decision";
486
486
  reason: string;
487
- binds?: { line: string; reason: string; model?: string; repo?: string; confirmed?: true }[];
487
+ binds?: {
488
+ line: string;
489
+ reason: string;
490
+ model?: string;
491
+ repo?: string;
492
+ repoSource?: "request" | "attachment" | "thread" | "channel";
493
+ confirmed?: true;
494
+ }[];
488
495
  question?: string;
489
496
  /** A question's proposed line — what the next turn's "yes" binds. */
490
497
  proposal?: string;
@@ -497,6 +504,13 @@ export interface RunOperatorDecision {
497
504
  /** The structured seam's attempts (record 0067): what each answer violated,
498
505
  * or that it was accepted. */
499
506
  attempts?: { outcome: "accepted" | "violation"; violation?: string }[];
507
+ repoContext?: {
508
+ organization: string;
509
+ threadRepo?: string;
510
+ channelRepo?: string;
511
+ candidateStatus: "skipped" | "available" | "truncated" | "empty" | "unavailable";
512
+ candidateCount: number;
513
+ };
500
514
  intake?: { verdict: string; reason: string };
501
515
  latencyMs?: number;
502
516
  outputTokens?: number;
@@ -520,6 +534,7 @@ export function operatorOfEvents(events: readonly RunEvent[]): RunOperatorDecisi
520
534
  reason: b.reason,
521
535
  ...(b.model !== undefined ? { model: b.model } : {}),
522
536
  ...(b.repo !== undefined ? { repo: b.repo } : {}),
537
+ ...(b.repoSource !== undefined ? { repoSource: b.repoSource } : {}),
523
538
  ...(b.confirmed ? { confirmed: true as const } : {}),
524
539
  })),
525
540
  }
@@ -538,6 +553,7 @@ export function operatorOfEvents(events: readonly RunEvent[]): RunOperatorDecisi
538
553
  })),
539
554
  }
540
555
  : {}),
556
+ ...(e.repoContext ? { repoContext: { ...e.repoContext } } : {}),
541
557
  ...(e.intake ? { intake: { verdict: e.intake.verdict, reason: e.intake.reason } } : {}),
542
558
  ...(e.latencyMs !== undefined ? { latencyMs: e.latencyMs } : {}),
543
559
  ...(e.outputTokens !== undefined ? { outputTokens: e.outputTokens } : {}),
@@ -3883,9 +3883,19 @@ function renderUnitReportWithWake(
3883
3883
  const rounds = `${e.reviewRounds} review round${e.reviewRounds === 1 ? "" : "s"}`;
3884
3884
  const prUrl = s.pr?.url;
3885
3885
  const prLine = prUrl ? ` PR: ${prUrl}` : "";
3886
+ const spentCapAction = (): string | undefined => {
3887
+ const cap = s.input.grant?.costCapUsd;
3888
+ if (cap === undefined || s.spendUsd === null || s.spendUsd < cap) return undefined;
3889
+ const next = prUrl
3890
+ ? `If the pull request (${prUrl}) is still open, continue it only under separate authorization, then request review of its exact head. If it has merged, no recovery is needed.`
3891
+ : "If work remains on the saved branch, continue it only under separate authorization, then request review of its exact head.";
3892
+ return `Next action: $${s.spendUsd.toFixed(2)} spent against the original $${cap.toFixed(2)} cost cap; this unit cannot start another child. ${next}`;
3893
+ };
3886
3894
  const nextAction = (checkpoint?: { branch: string; sha: string }): string => {
3887
3895
  if (durableWake)
3888
3896
  return `The unit remains live; a reply in this thread is recorded as its continuation action${prUrl ? ` from the open pull request (${prUrl})` : " from the saved branch state"}.`;
3897
+ const cappedAction = spentCapAction();
3898
+ if (cappedAction !== undefined) return cappedAction;
3889
3899
  const reconcile = checkpoint
3890
3900
  ? `verify \`${checkpoint.sha.slice(0, 7)}\` is on \`${checkpoint.branch}\`, then reconcile the remote branch and pull request if needed`
3891
3901
  : prUrl
@@ -4167,7 +4177,10 @@ function renderUnitReportWithWake(
4167
4177
  ]);
4168
4178
  case "no_verdict":
4169
4179
  if (!shows(verbosity, "verbose"))
4170
- return `⚠️ No verdict from review round ${e.round.index} — aborted after ${rounds}.${prLine}`;
4180
+ return join([
4181
+ `⚠️ No verdict from review round ${e.round.index} — aborted after ${rounds}.${prLine}`,
4182
+ spentCapAction(),
4183
+ ]);
4171
4184
  return join([
4172
4185
  `⚠️ Review round ${e.round.index} ended without a submitted verdict (budget, refusal, or stop) — ship never converts that into a request for changes, so no findings step ran.`,
4173
4186
  writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index]),
@@ -45,6 +45,16 @@ export interface AttrDomain {
45
45
  refusal: string;
46
46
  /** The refusal's cause, from the one code→cause table in src/core/refusal.ts. */
47
47
  cause: "request" | "policy" | "system";
48
+ /** The operator's decision, separate from the request's final outcome. */
49
+ operatorOutcome: "binds" | "question" | "refusal" | "non_decision";
50
+ /** Provenance of an accepted typed repository; the slug stays on the run event. */
51
+ operatorRepoSource: "request" | "attachment" | "thread" | "channel";
52
+ operatorAttempts: number;
53
+ operatorRepoCatalog: "skipped" | "available" | "truncated" | "empty" | "unavailable";
54
+ operatorRepoCandidates: number;
55
+ /** Final target resolution, separate from the operator's proposed repo. */
56
+ repoResolution: "skipped" | "resolved" | "rejected" | "unverified" | "conflict" | "unresolved";
57
+ repoTargetRelation: "not_proposed" | "matched" | "overridden" | "unresolved";
48
58
  count: number;
49
59
  backend: Backend;
50
60
  // run.command
@@ -146,6 +156,11 @@ const IDENTIFIER_KEYS: ReadonlySet<SpanAttrKey> = new Set<SpanAttrKey>([
146
156
  "outcome",
147
157
  "refusal",
148
158
  "cause",
159
+ "operatorOutcome",
160
+ "operatorRepoSource",
161
+ "operatorRepoCatalog",
162
+ "repoResolution",
163
+ "repoTargetRelation",
149
164
  "command",
150
165
  "route",
151
166
  "host",
@@ -204,6 +219,13 @@ const ATTR_TYPE: Record<SpanAttrKey, "string" | "number" | "boolean"> = {
204
219
  outcome: "string",
205
220
  refusal: "string",
206
221
  cause: "string",
222
+ operatorOutcome: "string",
223
+ operatorRepoSource: "string",
224
+ operatorAttempts: "number",
225
+ operatorRepoCatalog: "string",
226
+ operatorRepoCandidates: "number",
227
+ repoResolution: "string",
228
+ repoTargetRelation: "string",
207
229
  count: "number",
208
230
  backend: "string",
209
231
  command: "string",
@@ -99,6 +99,7 @@ interface SpanInit {
99
99
  sinks: SpanSink[];
100
100
  startedAt: number | undefined;
101
101
  attrs: SpanAttrs | undefined;
102
+ rootIdentity?: { name: string; runId?: string };
102
103
  }
103
104
 
104
105
  class SpanImpl implements Span {
@@ -109,6 +110,8 @@ class SpanImpl implements Span {
109
110
  ended = false;
110
111
  private readonly sinks: SpanSink[];
111
112
  private readonly rec: SpanRecord;
113
+ private readonly rootIdentity: { name: string; runId?: string };
114
+ private readonly isLocalRoot: boolean;
112
115
 
113
116
  constructor(
114
117
  private readonly shared: Shared,
@@ -118,6 +121,8 @@ class SpanImpl implements Span {
118
121
  this.parentId = init.parentId;
119
122
  this.name = sanitizeSpanName(init.name);
120
123
  this.sinks = init.sinks;
124
+ this.isLocalRoot = init.rootIdentity === undefined;
125
+ this.rootIdentity = init.rootIdentity ?? { name: this.name, runId: init.attrs?.runId };
121
126
  this.rec = {
122
127
  traceId: this.traceId,
123
128
  spanId: this.id,
@@ -164,6 +169,7 @@ class SpanImpl implements Span {
164
169
  sinks: this.sinks,
165
170
  startedAt: opts?.startedAt,
166
171
  attrs: opts?.attrs,
172
+ rootIdentity: this.rootIdentity,
167
173
  });
168
174
  child.emitStart();
169
175
  return child;
@@ -177,6 +183,7 @@ class SpanImpl implements Span {
177
183
  sinks: this.sinks,
178
184
  startedAt: opts.startedAt,
179
185
  attrs: opts.attrs,
186
+ rootIdentity: this.rootIdentity,
180
187
  });
181
188
  child.emitStart();
182
189
  child.endAt(Math.max(opts.startedAt, opts.endedAt), opts.status ?? "ok", opts.errorKind, opts.errorCode);
@@ -196,7 +203,7 @@ class SpanImpl implements Span {
196
203
  }
197
204
  for (const s of this.sinks) {
198
205
  try {
199
- s.onEnd(this.record());
206
+ s.onEnd(this.record(), this.rootIdentity);
200
207
  } catch (err) {
201
208
  this.shared.warn(
202
209
  `[trace] sink onEnd threw for ${this.name}: ${err instanceof Error ? err.message : String(err)}`,
@@ -215,7 +222,7 @@ class SpanImpl implements Span {
215
222
  this.rec.status = status ?? this.rec.status ?? "ok";
216
223
  for (const s of this.sinks) {
217
224
  try {
218
- s.onEnd(this.record());
225
+ s.onEnd(this.record(), this.rootIdentity);
219
226
  } catch (err) {
220
227
  this.shared.warn(
221
228
  `[trace] sink onEnd threw for ${this.name}: ${err instanceof Error ? err.message : String(err)}`,
@@ -239,6 +246,7 @@ class SpanImpl implements Span {
239
246
 
240
247
  setAttrs(attrs: SpanAttrs): void {
241
248
  (this.rec as { attrs: SpanAttrs }).attrs = { ...this.rec.attrs, ...attrs };
249
+ if (this.isLocalRoot) this.rootIdentity.runId = this.rec.attrs.runId;
242
250
  }
243
251
 
244
252
  record(): SpanRecord {
@@ -40,12 +40,23 @@ export interface SpanRecord {
40
40
  attrs: SpanAttrs;
41
41
  }
42
42
 
43
+ /** A live, read-only view of the local root's run identity. Shared by its
44
+ * descendants, including late children; changing the root's runId updates
45
+ * the view without changing any span record. No span, sink or payload is
46
+ * retained here, so a bounded sink can keep it after the root ends. */
47
+ export interface SpanRootIdentity {
48
+ readonly name: string;
49
+ readonly runId?: string;
50
+ }
51
+
43
52
  /** Observer of span starts and ends (Observer). Sinks are attached to a root
44
53
  * and inherited by its whole subtree; a throwing sink never reaches traced
45
54
  * code. */
46
55
  export interface SpanSink {
47
56
  onStart?(span: SpanRecord): void;
48
- onEnd(span: SpanRecord): void;
57
+ /** The tracer supplies root identity separately from the record snapshot;
58
+ * synthetic records supplied directly by a caller may omit it. */
59
+ onEnd(span: SpanRecord, root?: SpanRootIdentity): void;
49
60
  }
50
61
 
51
62
  /** The seam the no-gaps test enters through (Strategy): production is the
@@ -82,9 +82,9 @@ export function refusalFilter(sink: SpanSink): SpanSink {
82
82
  const refused = (status: unknown) => status === 401 || status === 403;
83
83
  return {
84
84
  ...(sink.onStart ? { onStart: (rec) => sink.onStart!(rec) } : {}),
85
- onEnd: (rec) => {
85
+ onEnd: (rec, root) => {
86
86
  if (refused(rec.attrs.httpStatus)) return;
87
- sink.onEnd(rec);
87
+ sink.onEnd(rec, root);
88
88
  },
89
89
  };
90
90
  }
@@ -106,7 +106,7 @@ export type SeedAnswer =
106
106
  /** A backup id as the SDK mints it (a UUID: hex and hyphens) — it becomes a
107
107
  * path and a glob on the container, so nothing else may. */
108
108
  const BACKUP_ID_RE = /^(?=.*[0-9a-fA-F])[0-9a-fA-F-]{8,64}$/;
109
- const SLUG_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
109
+ const SLUG_RE = /^(?!\.{1,2}\/)[A-Za-z0-9_.-]+\/(?!\.{1,2}$)[A-Za-z0-9_.-]+$/;
110
110
  const SHA_RE = /^[0-9a-f]{40}$/;
111
111
  /** A branch name git accepts (`check-ref-format --branch`, in the shape a
112
112
  * Slack thread binds): printable, no leading `-`/`/`/`.`, no `..`, no
@@ -148,8 +148,8 @@ export function parseSeed(v: unknown): { ok: true; seed: SandboxSeed } | { ok: f
148
148
  * 1. the tree becomes root's — the archive came from the resident's build
149
149
  * user, and a sandbox runs everything as root, so git would otherwise
150
150
  * refuse the "dubious ownership" and every write would need a chown;
151
- * 2. origin points at GitHub — the resident's checkout fetched from its
152
- * local mirror;
151
+ * 2. origin points at this run's exact repository on the Git door — the
152
+ * resident's checkout fetched from its local mirror;
153
153
  * 3. the deps view, when one was restored, replaces whatever `node_modules`
154
154
  * the checkout carries (older snapshots still hold one);
155
155
  * 4. the thread's ref is fetched from origin (the credential is the exec
@@ -158,8 +158,17 @@ export function parseSeed(v: unknown): { ok: true; seed: SandboxSeed } | { ok: f
158
158
  * fetch holds it, else at the fetched tip; without a thread ref the
159
159
  * checkout stays on the snapshot's branch;
160
160
  * 5. the head is printed last: the answer's `sha`. */
161
+ export function seedDoorRemote(baseUrl: string, slug: string): string {
162
+ if (!SLUG_RE.test(slug)) throw new Error("seed door remote needs owner/name");
163
+ const base = new URL(baseUrl);
164
+ if ((base.protocol !== "https:" && base.protocol !== "http:") || base.username || base.password)
165
+ throw new Error("seed door remote needs an HTTP origin");
166
+ return `${base.origin}/git/${slug}.git`;
167
+ }
168
+
161
169
  export function seedFixupScript(input: {
162
170
  slug: string;
171
+ doorOrigin: string;
163
172
  ref: string;
164
173
  fetchRef?: string;
165
174
  fetchSha?: string;
@@ -170,7 +179,7 @@ export function seedFixupScript(input: {
170
179
  "set -e",
171
180
  `cd ${shellQuote(input.checkoutDir)}`,
172
181
  "chown -R 0:0 .",
173
- `git remote set-url origin ${shellQuote(`https://github.com/${input.slug}.git`)}`,
182
+ `git remote set-url origin ${shellQuote(seedDoorRemote(input.doorOrigin, input.slug))}`,
174
183
  ];
175
184
  if (input.depsDir) {
176
185
  lines.push("rm -rf node_modules", `mv ${shellQuote(input.depsDir)} node_modules`);