@band-app/server 0.42.1 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/client/_shell.html +0 -0
  2. package/dist/client/assets/{TerminalPanel-BdNt_1-v.js → TerminalPanel-B4mOqlhb.js} +1 -1
  3. package/dist/client/assets/{_basePickBy-Dt5aurJR.js → _basePickBy-CHfQ38My.js} +1 -1
  4. package/dist/client/assets/{_baseUniq-eGBE_G8J.js → _baseUniq-C0VxYqbf.js} +1 -1
  5. package/dist/client/assets/{arc-C2TAcIr0.js → arc-oM0HaiTF.js} +1 -1
  6. package/dist/client/assets/{architectureDiagram-VXUJARFQ-CXYQ0Bl3.js → architectureDiagram-VXUJARFQ-BHJF6rDN.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-VD42YOAC-QNGIe7jl.js → blockDiagram-VD42YOAC-DfoHWvF-.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-YG6GDRKO-DNBB986h.js → c4Diagram-YG6GDRKO-ofRUHXOe.js} +1 -1
  9. package/dist/client/assets/channel-Bpwh056d.js +1 -0
  10. package/dist/client/assets/{chunk-4BX2VUAB-BiYeCu61.js → chunk-4BX2VUAB-CXpLCpos.js} +1 -1
  11. package/dist/client/assets/{chunk-55IACEB6-DetvZt2e.js → chunk-55IACEB6-D9t-7fJU.js} +1 -1
  12. package/dist/client/assets/{chunk-B4BG7PRW-C6yj2phu.js → chunk-B4BG7PRW-BsG1-s4X.js} +1 -1
  13. package/dist/client/assets/{chunk-DI55MBZ5-C3tA1HuX.js → chunk-DI55MBZ5-8gxNuHjG.js} +1 -1
  14. package/dist/client/assets/{chunk-FMBD7UC4-BQgPG9nP.js → chunk-FMBD7UC4-FeEHb48m.js} +1 -1
  15. package/dist/client/assets/{chunk-QN33PNHL-BEe46vEx.js → chunk-QN33PNHL-z8mbW7ph.js} +1 -1
  16. package/dist/client/assets/{chunk-QZHKN3VN-S1hRRppa.js → chunk-QZHKN3VN-CvLhsLHt.js} +1 -1
  17. package/dist/client/assets/{chunk-TZMSLE5B-DzuJYhii.js → chunk-TZMSLE5B-s9nFI-vX.js} +1 -1
  18. package/dist/client/assets/classDiagram-2ON5EDUG-DwcD-A80.js +1 -0
  19. package/dist/client/assets/classDiagram-v2-WZHVMYZB-DwcD-A80.js +1 -0
  20. package/dist/client/assets/clone-DbV2PV1D.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-S5V4N54A-CUxpzpxe.js → cose-bilkent-S5V4N54A-onixR54D.js} +1 -1
  22. package/dist/client/assets/{dagre-6UL2VRFP-CxQr_vx3.js → dagre-6UL2VRFP-CmeoFjCN.js} +1 -1
  23. package/dist/client/assets/{diagram-PSM6KHXK-BM9S3pcM.js → diagram-PSM6KHXK-Dv0xhzCj.js} +1 -1
  24. package/dist/client/assets/{diagram-QEK2KX5R-00WgBgwT.js → diagram-QEK2KX5R-Z_UWRAUZ.js} +1 -1
  25. package/dist/client/assets/{diagram-S2PKOQOG-CkydGteg.js → diagram-S2PKOQOG-BAib5-Ox.js} +1 -1
  26. package/dist/client/assets/{erDiagram-Q2GNP2WA-DDbgp90K.js → erDiagram-Q2GNP2WA-BhsQblX6.js} +1 -1
  27. package/dist/client/assets/{flowDiagram-NV44I4VS-1sWwkovK.js → flowDiagram-NV44I4VS-CAmBrRtm.js} +1 -1
  28. package/dist/client/assets/{ganttDiagram-JELNMOA3-z9MMcnjj.js → ganttDiagram-JELNMOA3-Bt9yOXqD.js} +1 -1
  29. package/dist/client/assets/{gitGraphDiagram-V2S2FVAM-CEeNZDag.js → gitGraphDiagram-V2S2FVAM-CjP86OLx.js} +1 -1
  30. package/dist/client/assets/{graph-8-N2XIVg.js → graph-DjlnSlEi.js} +1 -1
  31. package/dist/client/assets/{highlighted-body-B3W2YXNL-oqBJo7Lw.js → highlighted-body-B3W2YXNL-BC26yCeV.js} +1 -1
  32. package/dist/client/assets/{index-D7GkBMm7.js → index-BRLLIA0E.js} +1 -1
  33. package/dist/client/assets/{index-BjeVPwqv.js → index-BqHRp8MJ.js} +1 -1
  34. package/dist/client/assets/{index-B10E2JKM.js → index-BzKNDxvN.js} +1 -1
  35. package/dist/client/assets/{index-BfpCIunK.js → index-C0J9o47g.js} +1 -1
  36. package/dist/client/assets/{index-BW8m1RE2.js → index-CL8ph-cw.js} +1 -1
  37. package/dist/client/assets/{index-Ci4aLxZE.js → index-D1ndeAzk.js} +1 -1
  38. package/dist/client/assets/{index-UwPr1Bxu.js → index-D7os2dUh.js} +1 -1
  39. package/dist/client/assets/{index-BSMwXa_n.js → index-DBYA5XXI.js} +1 -1
  40. package/dist/client/assets/{index-B0DR0_5t.js → index-DS5EEpw2.js} +1 -1
  41. package/dist/client/assets/{index-DyktM7iC.js → index-Dhjz23HU.js} +1 -1
  42. package/dist/client/assets/{index-BYyaVskX.js → index-SJc6A-mo.js} +1 -1
  43. package/dist/client/assets/{index-CWgE6xNM.js → index-csfelsYg.js} +1 -1
  44. package/dist/client/assets/{index-DgfP7WcS.js → index-sTelM5W4.js} +1 -1
  45. package/dist/client/assets/{infoDiagram-HS3SLOUP-0xlIsMeE.js → infoDiagram-HS3SLOUP-DANcwC0l.js} +1 -1
  46. package/dist/client/assets/{journeyDiagram-XKPGCS4Q-URjejIwO.js → journeyDiagram-XKPGCS4Q-Bq7L8SS6.js} +1 -1
  47. package/dist/client/assets/{kanban-definition-3W4ZIXB7-iMd66xJM.js → kanban-definition-3W4ZIXB7-Rdco6x9s.js} +1 -1
  48. package/dist/client/assets/{layout-CZKMoUju.js → layout-BZ5I6v7b.js} +1 -1
  49. package/dist/client/assets/{main-Bf7Cqkhh.js → main-D_sbU-8F.js} +258 -258
  50. package/dist/client/assets/main-vXnu-Zdi.css +1 -0
  51. package/dist/client/assets/{mindmap-definition-VGOIOE7T-BgBLqjDc.js → mindmap-definition-VGOIOE7T-Daxduia_.js} +1 -1
  52. package/dist/client/assets/{pieDiagram-ADFJNKIX-CZlj_xhf.js → pieDiagram-ADFJNKIX-CfQaiffR.js} +1 -1
  53. package/dist/client/assets/{project._projectId-DkEX_rhg.js → project._projectId-DbodKP8r.js} +1 -1
  54. package/dist/client/assets/{quadrantDiagram-AYHSOK5B-egWT3ySU.js → quadrantDiagram-AYHSOK5B-Dp_MXprC.js} +1 -1
  55. package/dist/client/assets/{requirementDiagram-UZGBJVZJ-C1f4-YqC.js → requirementDiagram-UZGBJVZJ-qLyV9NbO.js} +1 -1
  56. package/dist/client/assets/{sankeyDiagram-TZEHDZUN-BIBQCdMV.js → sankeyDiagram-TZEHDZUN-mYTnU11w.js} +1 -1
  57. package/dist/client/assets/{sequenceDiagram-WL72ISMW-CwcAaqPr.js → sequenceDiagram-WL72ISMW-C59cPqQq.js} +1 -1
  58. package/dist/client/assets/{stateDiagram-FKZM4ZOC-BR0ZE0jR.js → stateDiagram-FKZM4ZOC-DyxRCj8E.js} +1 -1
  59. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-CSNSj8Vh.js +1 -0
  60. package/dist/client/assets/{timeline-definition-IT6M3QCI-Br4v6z5c.js → timeline-definition-IT6M3QCI-D71_ooP7.js} +1 -1
  61. package/dist/client/assets/{treemap-GDKQZRPO-BknlJ8NC.js → treemap-GDKQZRPO-Ctq0P5nh.js} +1 -1
  62. package/dist/client/assets/{use-worktree-route-BKyPkuvd.js → use-worktree-route-B4VSufa4.js} +1 -1
  63. package/dist/client/assets/{worktree._worktreeId-C4rABxUB.js → worktree._worktreeId-CvHNSUat.js} +1 -1
  64. package/dist/client/assets/{worktree._worktreeId-DmBaR5MI.js → worktree._worktreeId-DQRHrz5g.js} +1 -1
  65. package/dist/client/assets/{xychartDiagram-PRI3JC2R-CUQFNqNp.js → xychartDiagram-PRI3JC2R-TzranwDg.js} +1 -1
  66. package/dist/openapi.json +196 -76
  67. package/dist/start-server.mjs +1339 -892
  68. package/package.json +7 -7
  69. package/dist/client/assets/channel-C-DZN68q.js +0 -1
  70. package/dist/client/assets/classDiagram-2ON5EDUG-CnfpJOrh.js +0 -1
  71. package/dist/client/assets/classDiagram-v2-WZHVMYZB-CnfpJOrh.js +0 -1
  72. package/dist/client/assets/clone-HEWjuVIQ.js +0 -1
  73. package/dist/client/assets/main-D4L0ZncB.css +0 -1
  74. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-Bl6SncpV.js +0 -1
@@ -2031,7 +2031,7 @@ var require_thread_stream = __commonJS({
2031
2031
  var { version: version2 } = require_package();
2032
2032
  var { EventEmitter: EventEmitter3 } = __require("events");
2033
2033
  var { Worker: Worker2 } = __require("worker_threads");
2034
- var { join: join63 } = __require("path");
2034
+ var { join: join64 } = __require("path");
2035
2035
  var { pathToFileURL } = __require("url");
2036
2036
  var { wait } = require_wait();
2037
2037
  var {
@@ -2067,7 +2067,7 @@ var require_thread_stream = __commonJS({
2067
2067
  function createWorker(stream, opts) {
2068
2068
  const { filename, workerData } = opts;
2069
2069
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
2070
- const toExecute = bundlerOverrides["thread-stream-worker"] || join63(__dirname, "lib", "worker.js");
2070
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join64(__dirname, "lib", "worker.js");
2071
2071
  const worker2 = new Worker2(toExecute, {
2072
2072
  ...opts.workerOpts,
2073
2073
  trackUnmanagedFds: false,
@@ -2453,7 +2453,7 @@ var require_transport = __commonJS({
2453
2453
  "use strict";
2454
2454
  var { createRequire: createRequire2 } = __require("module");
2455
2455
  var getCallers = require_caller();
2456
- var { join: join63, isAbsolute: isAbsolute7, sep: sep13 } = __require("node:path");
2456
+ var { join: join64, isAbsolute: isAbsolute7, sep: sep13 } = __require("node:path");
2457
2457
  var sleep3 = require_atomic_sleep();
2458
2458
  var onExit = require_on_exit_leak_free();
2459
2459
  var ThreadStream = require_thread_stream();
@@ -2516,7 +2516,7 @@ var require_transport = __commonJS({
2516
2516
  throw new Error("only one of target or targets can be specified");
2517
2517
  }
2518
2518
  if (targets) {
2519
- target = bundlerOverrides["pino-worker"] || join63(__dirname, "worker.js");
2519
+ target = bundlerOverrides["pino-worker"] || join64(__dirname, "worker.js");
2520
2520
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
2521
2521
  return {
2522
2522
  ...dest,
@@ -2534,7 +2534,7 @@ var require_transport = __commonJS({
2534
2534
  });
2535
2535
  });
2536
2536
  } else if (pipeline2) {
2537
- target = bundlerOverrides["pino-worker"] || join63(__dirname, "worker.js");
2537
+ target = bundlerOverrides["pino-worker"] || join64(__dirname, "worker.js");
2538
2538
  options.pipelines = [pipeline2.map((dest) => {
2539
2539
  return {
2540
2540
  ...dest,
@@ -2556,7 +2556,7 @@ var require_transport = __commonJS({
2556
2556
  return origin;
2557
2557
  }
2558
2558
  if (origin === "pino/file") {
2559
- return join63(__dirname, "..", "file.js");
2559
+ return join64(__dirname, "..", "file.js");
2560
2560
  }
2561
2561
  let fixTarget2;
2562
2562
  for (const filePath of callers) {
@@ -3545,7 +3545,7 @@ var require_safe_stable_stringify = __commonJS({
3545
3545
  return circularValue;
3546
3546
  }
3547
3547
  let res = "";
3548
- let join63 = ",";
3548
+ let join64 = ",";
3549
3549
  const originalIndentation = indentation;
3550
3550
  if (Array.isArray(value)) {
3551
3551
  if (value.length === 0) {
@@ -3559,7 +3559,7 @@ var require_safe_stable_stringify = __commonJS({
3559
3559
  indentation += spacer;
3560
3560
  res += `
3561
3561
  ${indentation}`;
3562
- join63 = `,
3562
+ join64 = `,
3563
3563
  ${indentation}`;
3564
3564
  }
3565
3565
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -3567,13 +3567,13 @@ ${indentation}`;
3567
3567
  for (; i < maximumValuesToStringify - 1; i++) {
3568
3568
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
3569
3569
  res += tmp2 !== void 0 ? tmp2 : "null";
3570
- res += join63;
3570
+ res += join64;
3571
3571
  }
3572
3572
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
3573
3573
  res += tmp !== void 0 ? tmp : "null";
3574
3574
  if (value.length - 1 > maximumBreadth) {
3575
3575
  const removedKeys = value.length - maximumBreadth - 1;
3576
- res += `${join63}"... ${getItemCount(removedKeys)} not stringified"`;
3576
+ res += `${join64}"... ${getItemCount(removedKeys)} not stringified"`;
3577
3577
  }
3578
3578
  if (spacer !== "") {
3579
3579
  res += `
@@ -3594,7 +3594,7 @@ ${originalIndentation}`;
3594
3594
  let separator = "";
3595
3595
  if (spacer !== "") {
3596
3596
  indentation += spacer;
3597
- join63 = `,
3597
+ join64 = `,
3598
3598
  ${indentation}`;
3599
3599
  whitespace = " ";
3600
3600
  }
@@ -3608,13 +3608,13 @@ ${indentation}`;
3608
3608
  const tmp = stringifyFnReplacer(key3, value, stack, replacer, spacer, indentation);
3609
3609
  if (tmp !== void 0) {
3610
3610
  res += `${separator}${strEscape(key3)}:${whitespace}${tmp}`;
3611
- separator = join63;
3611
+ separator = join64;
3612
3612
  }
3613
3613
  }
3614
3614
  if (keyLength > maximumBreadth) {
3615
3615
  const removedKeys = keyLength - maximumBreadth;
3616
3616
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
3617
- separator = join63;
3617
+ separator = join64;
3618
3618
  }
3619
3619
  if (spacer !== "" && separator.length > 1) {
3620
3620
  res = `
@@ -3655,7 +3655,7 @@ ${originalIndentation}`;
3655
3655
  }
3656
3656
  const originalIndentation = indentation;
3657
3657
  let res = "";
3658
- let join63 = ",";
3658
+ let join64 = ",";
3659
3659
  if (Array.isArray(value)) {
3660
3660
  if (value.length === 0) {
3661
3661
  return "[]";
@@ -3668,7 +3668,7 @@ ${originalIndentation}`;
3668
3668
  indentation += spacer;
3669
3669
  res += `
3670
3670
  ${indentation}`;
3671
- join63 = `,
3671
+ join64 = `,
3672
3672
  ${indentation}`;
3673
3673
  }
3674
3674
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -3676,13 +3676,13 @@ ${indentation}`;
3676
3676
  for (; i < maximumValuesToStringify - 1; i++) {
3677
3677
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
3678
3678
  res += tmp2 !== void 0 ? tmp2 : "null";
3679
- res += join63;
3679
+ res += join64;
3680
3680
  }
3681
3681
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
3682
3682
  res += tmp !== void 0 ? tmp : "null";
3683
3683
  if (value.length - 1 > maximumBreadth) {
3684
3684
  const removedKeys = value.length - maximumBreadth - 1;
3685
- res += `${join63}"... ${getItemCount(removedKeys)} not stringified"`;
3685
+ res += `${join64}"... ${getItemCount(removedKeys)} not stringified"`;
3686
3686
  }
3687
3687
  if (spacer !== "") {
3688
3688
  res += `
@@ -3695,7 +3695,7 @@ ${originalIndentation}`;
3695
3695
  let whitespace = "";
3696
3696
  if (spacer !== "") {
3697
3697
  indentation += spacer;
3698
- join63 = `,
3698
+ join64 = `,
3699
3699
  ${indentation}`;
3700
3700
  whitespace = " ";
3701
3701
  }
@@ -3704,7 +3704,7 @@ ${indentation}`;
3704
3704
  const tmp = stringifyArrayReplacer(key3, value[key3], stack, replacer, spacer, indentation);
3705
3705
  if (tmp !== void 0) {
3706
3706
  res += `${separator}${strEscape(key3)}:${whitespace}${tmp}`;
3707
- separator = join63;
3707
+ separator = join64;
3708
3708
  }
3709
3709
  }
3710
3710
  if (spacer !== "" && separator.length > 1) {
@@ -3762,20 +3762,20 @@ ${originalIndentation}`;
3762
3762
  indentation += spacer;
3763
3763
  let res2 = `
3764
3764
  ${indentation}`;
3765
- const join64 = `,
3765
+ const join65 = `,
3766
3766
  ${indentation}`;
3767
3767
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
3768
3768
  let i = 0;
3769
3769
  for (; i < maximumValuesToStringify - 1; i++) {
3770
3770
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
3771
3771
  res2 += tmp2 !== void 0 ? tmp2 : "null";
3772
- res2 += join64;
3772
+ res2 += join65;
3773
3773
  }
3774
3774
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
3775
3775
  res2 += tmp !== void 0 ? tmp : "null";
3776
3776
  if (value.length - 1 > maximumBreadth) {
3777
3777
  const removedKeys = value.length - maximumBreadth - 1;
3778
- res2 += `${join64}"... ${getItemCount(removedKeys)} not stringified"`;
3778
+ res2 += `${join65}"... ${getItemCount(removedKeys)} not stringified"`;
3779
3779
  }
3780
3780
  res2 += `
3781
3781
  ${originalIndentation}`;
@@ -3791,16 +3791,16 @@ ${originalIndentation}`;
3791
3791
  return '"[Object]"';
3792
3792
  }
3793
3793
  indentation += spacer;
3794
- const join63 = `,
3794
+ const join64 = `,
3795
3795
  ${indentation}`;
3796
3796
  let res = "";
3797
3797
  let separator = "";
3798
3798
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
3799
3799
  if (isTypedArrayWithEntries(value)) {
3800
- res += stringifyTypedArray(value, join63, maximumBreadth);
3800
+ res += stringifyTypedArray(value, join64, maximumBreadth);
3801
3801
  keys = keys.slice(value.length);
3802
3802
  maximumPropertiesToStringify -= value.length;
3803
- separator = join63;
3803
+ separator = join64;
3804
3804
  }
3805
3805
  if (deterministic) {
3806
3806
  keys = sort(keys, comparator2);
@@ -3811,13 +3811,13 @@ ${indentation}`;
3811
3811
  const tmp = stringifyIndent(key3, value[key3], stack, spacer, indentation);
3812
3812
  if (tmp !== void 0) {
3813
3813
  res += `${separator}${strEscape(key3)}: ${tmp}`;
3814
- separator = join63;
3814
+ separator = join64;
3815
3815
  }
3816
3816
  }
3817
3817
  if (keyLength > maximumBreadth) {
3818
3818
  const removedKeys = keyLength - maximumBreadth;
3819
3819
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
3820
- separator = join63;
3820
+ separator = join64;
3821
3821
  }
3822
3822
  if (separator !== "") {
3823
3823
  res = `
@@ -13105,7 +13105,7 @@ var init_sql = __esm({
13105
13105
  return new SQL([new StringChunk(str4)]);
13106
13106
  }
13107
13107
  _sql.raw = raw;
13108
- function join63(chunks, separator) {
13108
+ function join64(chunks, separator) {
13109
13109
  const result = [];
13110
13110
  for (const [i, chunk] of chunks.entries()) {
13111
13111
  if (i > 0 && separator !== void 0) result.push(separator);
@@ -13113,7 +13113,7 @@ var init_sql = __esm({
13113
13113
  }
13114
13114
  return new SQL(result);
13115
13115
  }
13116
- _sql.join = join63;
13116
+ _sql.join = join64;
13117
13117
  function identifier(value) {
13118
13118
  return new Name(value);
13119
13119
  }
@@ -18267,7 +18267,7 @@ var init_select2 = __esm({
18267
18267
  const baseTableName = this.tableName;
18268
18268
  const tableName = getTableLikeName(table);
18269
18269
  for (const item of extractUsedTable(table)) this.usedTables.add(item);
18270
- if (typeof tableName === "string" && this.config.joins?.some((join63) => join63.alias === tableName)) throw new Error(`Alias "${tableName}" is already used in this query`);
18270
+ if (typeof tableName === "string" && this.config.joins?.some((join64) => join64.alias === tableName)) throw new Error(`Alias "${tableName}" is already used in this query`);
18271
18271
  if (!this.isPartialSelect) {
18272
18272
  if (Object.keys(this.joinsNotNullableMap).length === 1 && typeof baseTableName === "string") this.config.fields = { [baseTableName]: this.config.fields };
18273
18273
  if (typeof tableName === "string" && !is(table, SQL)) {
@@ -19517,7 +19517,7 @@ var init_dialect = __esm({
19517
19517
  if (!joins2) return;
19518
19518
  const withEntries = Object.entries(joins2).filter(([_2, v2]) => v2);
19519
19519
  if (!withEntries.length) return;
19520
- return sql.join(withEntries.map(([k2, join63]) => {
19520
+ return sql.join(withEntries.map(([k2, join64]) => {
19521
19521
  const relation = tableConfig.relations[k2];
19522
19522
  const isSingle2 = is(relation, One);
19523
19523
  const targetTable = aliasedTable(relation.targetTable, `d${currentDepth + 1}`);
@@ -19528,7 +19528,7 @@ var init_dialect = __esm({
19528
19528
  table: targetTable,
19529
19529
  mode: isSingle2 ? "first" : "many",
19530
19530
  schema,
19531
- queryConfig: join63,
19531
+ queryConfig: join64,
19532
19532
  tableConfig: schema[relation.targetTableName],
19533
19533
  relationWhere: filter,
19534
19534
  isNested: true,
@@ -19542,7 +19542,7 @@ var init_dialect = __esm({
19542
19542
  key: k2,
19543
19543
  selection: innerQuery.selection,
19544
19544
  isArray: !isSingle2,
19545
- isOptional: (relation.optional ?? false) || join63 !== true && !!join63.where
19545
+ isOptional: (relation.optional ?? false) || join64 !== true && !!join64.where
19546
19546
  });
19547
19547
  const jsonColumns = sql.join(innerQuery.selection.map((s2) => {
19548
19548
  return sql`${sql.raw(this.escapeString(s2.key))}, ${s2.selection ? sql`${jsonb2}(${sql.identifier(s2.key)})` : sql.identifier(s2.key)}`;
@@ -20061,7 +20061,7 @@ var init_update = __esm({
20061
20061
  createJoin(joinType) {
20062
20062
  return ((table, on) => {
20063
20063
  const tableName = getTableLikeName(table);
20064
- if (typeof tableName === "string" && this.config.joins.some((join63) => join63.alias === tableName)) throw new Error(`Alias "${tableName}" is already used in this query`);
20064
+ if (typeof tableName === "string" && this.config.joins.some((join64) => join64.alias === tableName)) throw new Error(`Alias "${tableName}" is already used in this query`);
20065
20065
  if (typeof on === "function") {
20066
20066
  const from = this.config.from ? is(table, SQLiteTable) ? table[Table.Symbol.Columns] : is(table, Subquery) ? table._.selectedFields : is(table, SQLiteViewBase) ? table[ViewBaseConfig].selectedFields : void 0 : void 0;
20067
20067
  on = on(new Proxy(this.config.table[Table.Symbol.Columns], new SelectionProxyHandler({
@@ -26784,10 +26784,10 @@ var init_zod = __esm({
26784
26784
 
26785
26785
  // src/server/infra/db/queries/repos.ts
26786
26786
  import { existsSync as existsSync10 } from "node:fs";
26787
- import { join as join34 } from "node:path";
26787
+ import { join as join35 } from "node:path";
26788
26788
  function reconcileKindForRepo(repo) {
26789
26789
  if (!repo.path || !existsSync10(repo.path)) return false;
26790
- const detectedKind = existsSync10(join34(repo.path, ".git")) ? "git" : "plain";
26790
+ const detectedKind = existsSync10(join35(repo.path, ".git")) ? "git" : "plain";
26791
26791
  if (detectedKind === repo.kind) return false;
26792
26792
  repo.kind = detectedKind;
26793
26793
  if (detectedKind === "plain") {
@@ -27338,7 +27338,7 @@ var init_isolation = __esm({
27338
27338
 
27339
27339
  // src/server/services/_utils/runner-config.ts
27340
27340
  import { existsSync as existsSync11 } from "node:fs";
27341
- import { dirname as dirname10, isAbsolute as isAbsolute2, join as join35, resolve as resolve8 } from "node:path";
27341
+ import { dirname as dirname10, isAbsolute as isAbsolute2, join as join36, resolve as resolve8 } from "node:path";
27342
27342
  function parseRunners(raw) {
27343
27343
  if (raw === void 0 || raw === null) return { runners: [], errors: [] };
27344
27344
  if (!Array.isArray(raw)) return { runners: [], errors: ["runners must be a list"] };
@@ -27363,8 +27363,8 @@ function bundledRunnersDir() {
27363
27363
  if (fromEnv) return resolve8(fromEnv);
27364
27364
  let dir = import.meta.dirname;
27365
27365
  for (let i = 0; i < 10; i++) {
27366
- const candidate = join35(dir, "runners");
27367
- if (existsSync11(join35(candidate, "local", "spawn.sh"))) return candidate;
27366
+ const candidate = join36(dir, "runners");
27367
+ if (existsSync11(join36(candidate, "local", "spawn.sh"))) return candidate;
27368
27368
  const parent = dirname10(dir);
27369
27369
  if (parent === dir) break;
27370
27370
  dir = parent;
@@ -27375,7 +27375,7 @@ function resolveHookPath(spec, script) {
27375
27375
  if (spec.startsWith(BUNDLED_PREFIX)) {
27376
27376
  const name5 = spec.slice(BUNDLED_PREFIX.length);
27377
27377
  if (!/^[a-z][a-z0-9-]*$/.test(name5)) throw new Error(`invalid bundled hook "${spec}"`);
27378
- return join35(bundledRunnersDir(), name5, `${script}.sh`);
27378
+ return join36(bundledRunnersDir(), name5, `${script}.sh`);
27379
27379
  }
27380
27380
  return isAbsolute2(spec) ? spec : resolve8(bandHome(), spec);
27381
27381
  }
@@ -27474,7 +27474,7 @@ var init_runner_config = __esm({
27474
27474
  });
27475
27475
 
27476
27476
  // src/server/services/settings-service.ts
27477
- import { join as join36 } from "node:path";
27477
+ import { join as join37 } from "node:path";
27478
27478
  function agentModeFromVia(via) {
27479
27479
  if (via === "chat") return "gui";
27480
27480
  if (via === "terminal") return "tui";
@@ -27668,7 +27668,7 @@ var init_settings_service = __esm({
27668
27668
  */
27669
27669
  worktreesDir() {
27670
27670
  const raw = this.rawWorktreesDir();
27671
- return raw ?? join36(bandHome(), "worktrees");
27671
+ return raw ?? join37(bandHome(), "worktrees");
27672
27672
  }
27673
27673
  /**
27674
27674
  * Return the raw `worktreesDir` value from settings without applying the
@@ -34771,7 +34771,7 @@ import {
34771
34771
  statSync as statSync14
34772
34772
  } from "node:fs";
34773
34773
  import { createServer as createServer3 } from "node:http";
34774
- import { basename as basename7, join as join62, resolve as resolve15, sep as sep12 } from "node:path";
34774
+ import { basename as basename7, join as join63, resolve as resolve15, sep as sep12 } from "node:path";
34775
34775
  import { pipeline, Readable as Readable2 } from "node:stream";
34776
34776
  import { parseArgs } from "node:util";
34777
34777
 
@@ -41327,6 +41327,11 @@ function openSseStream(res) {
41327
41327
  // src/server/services/agent-session-service.ts
41328
41328
  import { randomUUID as randomUUID17 } from "node:crypto";
41329
41329
 
41330
+ // ../../packages/host-api/src/capabilities.ts
41331
+ function agentIsUsable(a) {
41332
+ return Boolean(a?.installed) && a?.loggedIn !== false;
41333
+ }
41334
+
41330
41335
  // ../../packages/host-api/src/errors.ts
41331
41336
  var HostPathDeniedError = class extends Error {
41332
41337
  path;
@@ -56856,11 +56861,11 @@ var ChatEventQueries = class {
56856
56861
  init_settings();
56857
56862
 
56858
56863
  // src/server/infra/host/registry.ts
56859
- import { join as join37 } from "node:path";
56864
+ import { join as join38 } from "node:path";
56860
56865
 
56861
56866
  // ../../packages/host-local/src/local-host.ts
56862
56867
  init_src2();
56863
- import { execFile as execFile11, spawn as spawn7 } from "node:child_process";
56868
+ import { execFile as execFile12, spawn as spawn7 } from "node:child_process";
56864
56869
  import { createReadStream as createReadStream3, watch as fsWatch } from "node:fs";
56865
56870
  import {
56866
56871
  cp,
@@ -56872,12 +56877,12 @@ import {
56872
56877
  readFile as readFile7,
56873
56878
  realpath as realpath4,
56874
56879
  rename as rename3,
56875
- rm as rm3,
56880
+ rm as rm4,
56876
56881
  stat as stat7,
56877
56882
  writeFile as writeFile4
56878
56883
  } from "node:fs/promises";
56879
- import { homedir as homedir13, hostname as hostname3, tmpdir as tmpdir2 } from "node:os";
56880
- import { join as join33 } from "node:path";
56884
+ import { homedir as homedir14, hostname as hostname3, tmpdir as tmpdir2 } from "node:os";
56885
+ import { join as join34 } from "node:path";
56881
56886
 
56882
56887
  // ../../packages/host-local/src/agents/acp-launch.ts
56883
56888
  init_src2();
@@ -57959,7 +57964,7 @@ var NETWORK_TIMEOUT_MS = 3e4;
57959
57964
  var LOCAL_TIMEOUT_MS = 3e4;
57960
57965
  var MAX_PUSH_ATTEMPTS = 5;
57961
57966
  var APPEND_ONLY = ["learnings/**", "inbox/**"];
57962
- var LOCAL_ONLY = ["/tasks/", "/repos/", "/AGENTS.md", "/CLAUDE.md"];
57967
+ var LOCAL_ONLY = ["/tasks/", "/repos/"];
57963
57968
  function gitEnv(extra = {}) {
57964
57969
  const env = {};
57965
57970
  for (const [k2, v2] of Object.entries(process.env)) {
@@ -58296,6 +58301,32 @@ var ContextSync = class {
58296
58301
  `);
58297
58302
  await this.git(dir, ["config", "rebase.autoStash", "false"]);
58298
58303
  await this.git(dir, ["config", "commit.gpgsign", "false"]);
58304
+ await this.dropLegacyInstructions(dir);
58305
+ }
58306
+ /**
58307
+ * Older Band wrote a generated AGENTS.md and CLAUDE.md into each host's copy and kept them out of
58308
+ * the context repo. They are synced files now, so an untracked leftover would conflict with the
58309
+ * hub's seeded file or be pushed as the user's own. Remove an untracked one that still carries
58310
+ * Band's generated header.
58311
+ */
58312
+ async dropLegacyInstructions(dir) {
58313
+ const markers = {
58314
+ "AGENTS.md": "<!-- Written by Band from the project's settings",
58315
+ "CLAUDE.md": "<!-- Written by Band. Claude Code reads this file"
58316
+ };
58317
+ for (const [file2, marker] of Object.entries(markers)) {
58318
+ try {
58319
+ const text3 = await readFile4(join23(dir, file2), "utf8");
58320
+ if (!text3.startsWith(marker)) continue;
58321
+ const tracked2 = await run2(["ls-files", "--error-unmatch", "--", file2], {
58322
+ cwd: dir,
58323
+ timeoutMs: LOCAL_TIMEOUT_MS
58324
+ });
58325
+ if (tracked2.code === 0) continue;
58326
+ await rm2(join23(dir, file2), { force: true });
58327
+ } catch {
58328
+ }
58329
+ }
58299
58330
  }
58300
58331
  async branch(dir) {
58301
58332
  const r = await this.git(dir, ["symbolic-ref", "--short", "HEAD"]);
@@ -58646,9 +58677,9 @@ async function listWorktrees(repoPath) {
58646
58677
  if (currentPath) await pushCurrent();
58647
58678
  return worktrees2;
58648
58679
  }
58649
- async function getRepoInfo(worktreePath, run4 = execGit) {
58680
+ async function getRepoInfo(worktreePath, run5 = execGit) {
58650
58681
  try {
58651
- const remoteUrl = (await run4(["remote", "get-url", "origin"], worktreePath)).trim();
58682
+ const remoteUrl = (await run5(["remote", "get-url", "origin"], worktreePath)).trim();
58652
58683
  const parsed = parseGitRemoteUrl(remoteUrl);
58653
58684
  if (!parsed) {
58654
58685
  log10.debug('getRepoInfo: failed to parse remote URL "%s" for %s', remoteUrl, worktreePath);
@@ -58827,76 +58858,10 @@ function killAllServers() {
58827
58858
  worktreeServers.clear();
58828
58859
  }
58829
58860
 
58830
- // ../../packages/host-local/src/process/du.ts
58861
+ // ../../packages/host-local/src/process/capabilities.ts
58831
58862
  import { execFile as execFile7 } from "node:child_process";
58832
- import { readdir as readdir4, stat as stat4 } from "node:fs/promises";
58833
- import { join as join27 } from "node:path";
58834
- var MAX_DU_BUFFER = 10 * 1024 * 1024;
58835
- var DU_TIMEOUT_MS = 3e4;
58836
- var STAT_CHUNK = 256;
58837
- async function duBytesNodeWalk(path2) {
58838
- const deadline = Date.now() + DU_TIMEOUT_MS;
58839
- let total = 0;
58840
- const stack = [path2];
58841
- while (stack.length > 0) {
58842
- if (Date.now() > deadline) break;
58843
- const dir = stack.pop();
58844
- try {
58845
- const entries = await readdir4(dir, { withFileTypes: true });
58846
- const filePaths = [];
58847
- for (const entry of entries) {
58848
- if (entry.isDirectory()) {
58849
- stack.push(join27(dir, entry.name));
58850
- } else if (entry.isFile()) {
58851
- filePaths.push(join27(dir, entry.name));
58852
- }
58853
- }
58854
- for (let i = 0; i < filePaths.length; i += STAT_CHUNK) {
58855
- if (Date.now() > deadline) break;
58856
- const sizes = await Promise.all(
58857
- filePaths.slice(i, i + STAT_CHUNK).map(
58858
- (p2) => stat4(p2).then(
58859
- (s2) => s2.size,
58860
- () => 0
58861
- // vanished between readdir and stat — count as 0
58862
- )
58863
- )
58864
- );
58865
- for (const size of sizes) total += size;
58866
- }
58867
- } catch {
58868
- }
58869
- }
58870
- return total;
58871
- }
58872
- async function duBytes(path2) {
58873
- if (process.platform === "win32") {
58874
- return duBytesNodeWalk(path2);
58875
- }
58876
- const stdout = await new Promise((resolve16, reject) => {
58877
- execFile7(
58878
- "du",
58879
- ["-sk", path2],
58880
- { maxBuffer: MAX_DU_BUFFER, timeout: DU_TIMEOUT_MS },
58881
- (err, out) => {
58882
- if (err && !out) {
58883
- reject(err);
58884
- return;
58885
- }
58886
- resolve16(out);
58887
- }
58888
- );
58889
- });
58890
- const kbStr = stdout.trim().split(/\s+/)[0];
58891
- const kb = Number.parseInt(kbStr, 10);
58892
- if (!Number.isFinite(kb)) {
58893
- throw new Error(`du output not numeric: ${JSON.stringify(stdout)}`);
58894
- }
58895
- return kb * 1024;
58896
- }
58897
-
58898
- // ../../packages/host-local/src/process/tools.ts
58899
- import { execFile as execFile8 } from "node:child_process";
58863
+ import { homedir as homedir12 } from "node:os";
58864
+ import { delimiter as delimiter5, join as join27 } from "node:path";
58900
58865
 
58901
58866
  // ../../packages/environment/src/image.ts
58902
58867
  import { createHash as createHash3 } from "node:crypto";
@@ -59319,7 +59284,213 @@ ${start}
59319
59284
  return install ?? start ?? null;
59320
59285
  }
59321
59286
 
59287
+ // ../../packages/host-local/src/process/capabilities.ts
59288
+ var AGENT_PROBES = [
59289
+ {
59290
+ type: "claude-code",
59291
+ bin: "claude",
59292
+ login: ["auth", "status"],
59293
+ envKeys: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
59294
+ install: "npm install -g @anthropic-ai/claude-code",
59295
+ loginFix: "claude auth login"
59296
+ },
59297
+ {
59298
+ type: "codex",
59299
+ bin: "codex",
59300
+ login: ["login", "status"],
59301
+ envKeys: ["OPENAI_API_KEY"],
59302
+ install: "npm install -g @openai/codex",
59303
+ loginFix: "codex login"
59304
+ },
59305
+ {
59306
+ type: "opencode",
59307
+ bin: "opencode",
59308
+ login: null,
59309
+ envKeys: [],
59310
+ install: "npm install -g opencode-ai",
59311
+ loginFix: "opencode auth login"
59312
+ },
59313
+ {
59314
+ type: "gemini-cli",
59315
+ bin: "gemini",
59316
+ login: null,
59317
+ envKeys: ["GEMINI_API_KEY"],
59318
+ install: "npm install -g @google/gemini-cli",
59319
+ loginFix: "gemini"
59320
+ },
59321
+ {
59322
+ type: "cursor-cli",
59323
+ bin: "cursor-agent",
59324
+ login: null,
59325
+ envKeys: ["CURSOR_API_KEY"],
59326
+ install: "curl https://cursor.com/install -fsS | bash",
59327
+ loginFix: "cursor-agent login"
59328
+ }
59329
+ ];
59330
+ var PROBE_TIMEOUT_MS = 8e3;
59331
+ function searchPath() {
59332
+ const extra = process.env.BAND_AGENT_BIN_DIRS?.trim();
59333
+ const home = homedir12();
59334
+ const installDirs = [
59335
+ ".local/bin",
59336
+ ".opencode/bin",
59337
+ ".claude/local",
59338
+ ".bun/bin",
59339
+ ".npm-global/bin"
59340
+ ].map((d) => join27(home, d));
59341
+ return [...extra ? [extra] : [], prependBinDirs(process.env.PATH), ...installDirs].join(
59342
+ delimiter5
59343
+ );
59344
+ }
59345
+ function run3(bin, args) {
59346
+ return new Promise((resolve16) => {
59347
+ execFile7(
59348
+ bin,
59349
+ args,
59350
+ {
59351
+ env: { ...process.env, PATH: searchPath() },
59352
+ timeout: PROBE_TIMEOUT_MS
59353
+ },
59354
+ (err, stdout, stderr) => {
59355
+ const notFound = err?.code === "ENOENT";
59356
+ resolve16({ ok: !err, found: !notFound, out: `${stdout}
59357
+ ${stderr}` });
59358
+ }
59359
+ );
59360
+ });
59361
+ }
59362
+ async function probeAgent(probe) {
59363
+ const version2 = await run3(probe.bin, ["--version"]);
59364
+ if (!version2.found) {
59365
+ return {
59366
+ type: probe.type,
59367
+ installed: false,
59368
+ version: null,
59369
+ loggedIn: null,
59370
+ fix: probe.install
59371
+ };
59372
+ }
59373
+ let loggedIn = null;
59374
+ if (probe.envKeys.some((k2) => Boolean(process.env[k2]))) loggedIn = true;
59375
+ else if (probe.login) loggedIn = (await run3(probe.bin, probe.login)).ok;
59376
+ return {
59377
+ type: probe.type,
59378
+ installed: true,
59379
+ version: extractVersion(version2.out),
59380
+ loggedIn,
59381
+ fix: loggedIn === false ? probe.loginFix : ""
59382
+ };
59383
+ }
59384
+ async function probeGh() {
59385
+ const v2 = await run3("gh", ["--version"]);
59386
+ if (!v2.found) {
59387
+ return {
59388
+ tool: "gh",
59389
+ installed: false,
59390
+ version: null,
59391
+ loggedIn: null,
59392
+ fix: "install the GitHub CLI from https://cli.github.com"
59393
+ };
59394
+ }
59395
+ const auth2 = process.env.GH_TOKEN || process.env.GITHUB_TOKEN ? { ok: true } : await run3("gh", ["auth", "status"]);
59396
+ return {
59397
+ tool: "gh",
59398
+ installed: true,
59399
+ version: extractVersion(v2.out),
59400
+ loggedIn: auth2.ok,
59401
+ fix: auth2.ok ? "" : "gh auth login"
59402
+ };
59403
+ }
59404
+ async function probeGit() {
59405
+ const v2 = await run3("git", ["--version"]);
59406
+ return {
59407
+ tool: "git",
59408
+ installed: v2.found && v2.ok,
59409
+ version: v2.found ? extractVersion(v2.out) : null,
59410
+ loggedIn: null,
59411
+ fix: v2.found ? "" : "install git"
59412
+ };
59413
+ }
59414
+ async function probeCapabilities(agentTypes) {
59415
+ const probes = agentTypes ? AGENT_PROBES.filter((p2) => agentTypes.includes(p2.type)) : AGENT_PROBES;
59416
+ const [agents, gh, git3] = await Promise.all([
59417
+ Promise.all(probes.map(probeAgent)),
59418
+ probeGh(),
59419
+ probeGit()
59420
+ ]);
59421
+ return { agents, tools: [git3, gh], checkedAt: Date.now() };
59422
+ }
59423
+
59424
+ // ../../packages/host-local/src/process/du.ts
59425
+ import { execFile as execFile8 } from "node:child_process";
59426
+ import { readdir as readdir4, stat as stat4 } from "node:fs/promises";
59427
+ import { join as join28 } from "node:path";
59428
+ var MAX_DU_BUFFER = 10 * 1024 * 1024;
59429
+ var DU_TIMEOUT_MS = 3e4;
59430
+ var STAT_CHUNK = 256;
59431
+ async function duBytesNodeWalk(path2) {
59432
+ const deadline = Date.now() + DU_TIMEOUT_MS;
59433
+ let total = 0;
59434
+ const stack = [path2];
59435
+ while (stack.length > 0) {
59436
+ if (Date.now() > deadline) break;
59437
+ const dir = stack.pop();
59438
+ try {
59439
+ const entries = await readdir4(dir, { withFileTypes: true });
59440
+ const filePaths = [];
59441
+ for (const entry of entries) {
59442
+ if (entry.isDirectory()) {
59443
+ stack.push(join28(dir, entry.name));
59444
+ } else if (entry.isFile()) {
59445
+ filePaths.push(join28(dir, entry.name));
59446
+ }
59447
+ }
59448
+ for (let i = 0; i < filePaths.length; i += STAT_CHUNK) {
59449
+ if (Date.now() > deadline) break;
59450
+ const sizes = await Promise.all(
59451
+ filePaths.slice(i, i + STAT_CHUNK).map(
59452
+ (p2) => stat4(p2).then(
59453
+ (s2) => s2.size,
59454
+ () => 0
59455
+ // vanished between readdir and stat — count as 0
59456
+ )
59457
+ )
59458
+ );
59459
+ for (const size of sizes) total += size;
59460
+ }
59461
+ } catch {
59462
+ }
59463
+ }
59464
+ return total;
59465
+ }
59466
+ async function duBytes(path2) {
59467
+ if (process.platform === "win32") {
59468
+ return duBytesNodeWalk(path2);
59469
+ }
59470
+ const stdout = await new Promise((resolve16, reject) => {
59471
+ execFile8(
59472
+ "du",
59473
+ ["-sk", path2],
59474
+ { maxBuffer: MAX_DU_BUFFER, timeout: DU_TIMEOUT_MS },
59475
+ (err, out) => {
59476
+ if (err && !out) {
59477
+ reject(err);
59478
+ return;
59479
+ }
59480
+ resolve16(out);
59481
+ }
59482
+ );
59483
+ });
59484
+ const kbStr = stdout.trim().split(/\s+/)[0];
59485
+ const kb = Number.parseInt(kbStr, 10);
59486
+ if (!Number.isFinite(kb)) {
59487
+ throw new Error(`du output not numeric: ${JSON.stringify(stdout)}`);
59488
+ }
59489
+ return kb * 1024;
59490
+ }
59491
+
59322
59492
  // ../../packages/host-local/src/process/tools.ts
59493
+ import { execFile as execFile9 } from "node:child_process";
59323
59494
  var PROBES = [
59324
59495
  { tool: "node", bin: "node", args: ["--version"] },
59325
59496
  { tool: "python", bin: "python3", args: ["--version"] },
@@ -59329,15 +59500,15 @@ var PROBES = [
59329
59500
  { tool: "docker", bin: "docker", args: ["--version"] },
59330
59501
  { tool: "git", bin: "git", args: ["--version"] }
59331
59502
  ];
59332
- var PROBE_TIMEOUT_MS = 5e3;
59503
+ var PROBE_TIMEOUT_MS2 = 5e3;
59333
59504
  function versionOf(bin, args) {
59334
59505
  return new Promise((resolve16) => {
59335
- execFile8(
59506
+ execFile9(
59336
59507
  bin,
59337
59508
  args,
59338
59509
  {
59339
59510
  env: { ...process.env, PATH: prependBinDirs(process.env.PATH) },
59340
- timeout: PROBE_TIMEOUT_MS
59511
+ timeout: PROBE_TIMEOUT_MS2
59341
59512
  },
59342
59513
  (err, stdout, stderr) => resolve16(err ? null : extractVersion(`${stdout}
59343
59514
  ${stderr}`))
@@ -59355,13 +59526,14 @@ async function probeTools() {
59355
59526
 
59356
59527
  // ../../packages/host-local/src/project/project-folder.ts
59357
59528
  import { spawn as spawn5 } from "node:child_process";
59358
- import { mkdir as mkdir4, open as open2, readdir as readdir5, realpath as realpath2, stat as stat5 } from "node:fs/promises";
59359
- import { join as join28, resolve as resolve6, sep as sep3 } from "node:path";
59529
+ import { mkdir as mkdir4, open as open2, readdir as readdir5, realpath as realpath2, rm as rm3, stat as stat5 } from "node:fs/promises";
59530
+ import { join as join29, resolve as resolve6, sep as sep3 } from "node:path";
59360
59531
  var PROJECT_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
59361
59532
  var REPO_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
59362
59533
  var BRANCH_NAME = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/;
59363
59534
  var DEFAULT_FETCH_THROTTLE_MS = 6e4;
59364
59535
  var FETCH_TIMEOUT_MS = 3e4;
59536
+ var CLONE_TIMEOUT_MS = 10 * 6e4;
59365
59537
  var LOCAL_TIMEOUT_MS2 = 3e4;
59366
59538
  var DEFAULT_READ_BYTES = 2e5;
59367
59539
  var MAX_READ_BYTES = 1e6;
@@ -59454,11 +59626,11 @@ var ProjectFolder = class {
59454
59626
  locks = /* @__PURE__ */ new Map();
59455
59627
  folderOf(project) {
59456
59628
  checkProject(project);
59457
- return join28(this.bandHome(), "projects", project);
59629
+ return join29(this.bandHome(), "projects", project);
59458
59630
  }
59459
59631
  checkoutOf(project, repo) {
59460
59632
  checkRepo({ name: repo });
59461
- return join28(this.folderOf(project), "repos", repo);
59633
+ return join29(this.folderOf(project), "repos", repo);
59462
59634
  }
59463
59635
  // ---- ensure ------------------------------------------------------------------
59464
59636
  async ensure(req) {
@@ -59483,7 +59655,8 @@ var ProjectFolder = class {
59483
59655
  });
59484
59656
  }
59485
59657
  async ensureCheckout(project, repo, fetchMode) {
59486
- const branch = (() => {
59658
+ const branch = repo.defaultBranch;
59659
+ const legacyBranch = (() => {
59487
59660
  try {
59488
59661
  checkRepo(repo);
59489
59662
  return projectBranch(project, repo.defaultBranch);
@@ -59491,7 +59664,7 @@ var ProjectFolder = class {
59491
59664
  return projectBranch(project, "?");
59492
59665
  }
59493
59666
  })();
59494
- const path2 = join28(this.folderOf(project), "repos", repo.name);
59667
+ const path2 = join29(this.folderOf(project), "repos", repo.name);
59495
59668
  const base = {
59496
59669
  repo: repo.name,
59497
59670
  path: path2,
@@ -59506,8 +59679,20 @@ var ProjectFolder = class {
59506
59679
  checkRepo(repo);
59507
59680
  let fetchError;
59508
59681
  if (fetchMode !== "never") fetchError = await this.fetchClone(repo.clonePath, fetchMode);
59509
- if (!await exists2(join28(path2, ".git"))) {
59510
- await this.createCheckout(project, repo, path2, branch);
59682
+ let legacyWorktree = false;
59683
+ let created = false;
59684
+ if (!await exists2(join29(path2, ".git"))) {
59685
+ await this.createCheckout(project, repo, path2);
59686
+ created = true;
59687
+ } else if (await isWorktreeCheckout(path2)) {
59688
+ legacyWorktree = !await this.convertWorktree(repo, path2, legacyBranch);
59689
+ if (!legacyWorktree) {
59690
+ await this.createCheckout(project, repo, path2);
59691
+ created = true;
59692
+ }
59693
+ }
59694
+ if (fetchMode !== "never" && !created && !legacyWorktree) {
59695
+ fetchError ??= await this.fetchClone(path2, fetchMode);
59511
59696
  }
59512
59697
  let state2 = await this.state(path2);
59513
59698
  let updated = false;
@@ -59520,10 +59705,12 @@ var ProjectFolder = class {
59520
59705
  const status = updated ? "updated" : state2.behind > 0 ? "behind" : state2.ahead > 0 || state2.dirty ? "ahead" : "current";
59521
59706
  return {
59522
59707
  ...base,
59708
+ branch: legacyWorktree ? (await repoGit(path2, ["symbolic-ref", "--short", "HEAD"])).stdout.trim() || legacyBranch : branch,
59523
59709
  status,
59524
59710
  ahead: state2.ahead,
59525
59711
  behind: state2.behind,
59526
59712
  dirty: state2.dirty,
59713
+ ...legacyWorktree ? { legacyWorktree } : {},
59527
59714
  ...fetchError ? { fetchError } : {}
59528
59715
  };
59529
59716
  } catch (err) {
@@ -59550,9 +59737,25 @@ var ProjectFolder = class {
59550
59737
  this.lastFetchError.delete(clonePath);
59551
59738
  return void 0;
59552
59739
  }
59553
- async createCheckout(project, repo, path2, branch) {
59554
- await mkdir4(join28(this.folderOf(project), "repos"), { recursive: true });
59555
- await repoGit(repo.clonePath, ["worktree", "prune"]);
59740
+ /**
59741
+ * Removes a worktree an older Band made, with its local branch, when it has nothing the remote
59742
+ * lacks. Returns false and changes nothing when it is dirty or ahead.
59743
+ */
59744
+ async convertWorktree(repo, path2, legacyBranch) {
59745
+ const state2 = await this.state(path2);
59746
+ if (state2.dirty || state2.ahead > 0) return false;
59747
+ const removed = await repoGit(repo.clonePath, ["worktree", "remove", path2]);
59748
+ if (removed.code !== 0) throw new Error(brief(removed));
59749
+ await repoGit(repo.clonePath, ["branch", "-D", legacyBranch]);
59750
+ return true;
59751
+ }
59752
+ /**
59753
+ * Clones the repo's origin into `path`, borrowing the objects of the repo's own clone and then
59754
+ * copying them (`--reference --dissociate`), so the checkout depends on nothing in that clone.
59755
+ * Without a usable reference it is a plain clone.
59756
+ */
59757
+ async createCheckout(project, repo, path2) {
59758
+ await mkdir4(join29(this.folderOf(project), "repos"), { recursive: true });
59556
59759
  const upstream = `origin/${repo.defaultBranch}`;
59557
59760
  const hasUpstream = await repoGit(repo.clonePath, [
59558
59761
  "rev-parse",
@@ -59563,16 +59766,31 @@ var ProjectFolder = class {
59563
59766
  if (hasUpstream.code !== 0) {
59564
59767
  throw new Error(`${repo.name} has no ${upstream}. Fetch the repository first.`);
59565
59768
  }
59566
- const hasBranch = (await repoGit(repo.clonePath, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`])).code === 0;
59567
- const added = hasBranch ? await repoGit(repo.clonePath, ["worktree", "add", path2, branch]) : await repoGit(repo.clonePath, ["worktree", "add", "--track", "-b", branch, path2, upstream]);
59568
- if (added.code !== 0) throw new Error(brief(added));
59569
- const ext = await repoGit(repo.clonePath, ["config", "extensions.worktreeConfig", "true"]);
59570
- if (ext.code !== 0) throw new Error(brief(ext));
59571
- const pushDefault = await repoGit(path2, ["config", "--worktree", "push.default", "upstream"]);
59572
- if (pushDefault.code !== 0) throw new Error(brief(pushDefault));
59573
- if (hasBranch) {
59574
- await repoGit(path2, ["branch", "--set-upstream-to", upstream, branch]);
59769
+ const origin = (await repoGit(repo.clonePath, ["remote", "get-url", "origin"])).stdout.trim();
59770
+ if (!origin) throw new Error(`${repo.name} has no origin remote`);
59771
+ if (origin.startsWith("-")) throw new Error(`${repo.name} has an invalid origin remote`);
59772
+ const existedBefore = await stat5(path2).then(
59773
+ () => true,
59774
+ () => false
59775
+ );
59776
+ const common2 = ["clone", "--quiet", "--branch", repo.defaultBranch, "--no-tags"];
59777
+ let cloned = await repoGit(
59778
+ join29(this.folderOf(project), "repos"),
59779
+ [...common2, "--reference", repo.clonePath, "--dissociate", "--", origin, path2],
59780
+ CLONE_TIMEOUT_MS
59781
+ );
59782
+ if (cloned.code !== 0) {
59783
+ if (existedBefore) throw new Error(brief(cloned));
59784
+ await rm3(path2, { recursive: true, force: true });
59785
+ cloned = await repoGit(
59786
+ join29(this.folderOf(project), "repos"),
59787
+ [...common2, "--", origin, path2],
59788
+ CLONE_TIMEOUT_MS
59789
+ );
59575
59790
  }
59791
+ if (cloned.code !== 0) throw new Error(brief(cloned));
59792
+ const pushDefault = await repoGit(path2, ["config", "push.default", "upstream"]);
59793
+ if (pushDefault.code !== 0) throw new Error(brief(pushDefault));
59576
59794
  }
59577
59795
  async state(path2) {
59578
59796
  const counts = await repoGit(path2, ["rev-list", "--left-right", "--count", "HEAD...@{u}"]);
@@ -59586,7 +59804,7 @@ var ProjectFolder = class {
59586
59804
  /** The checkout of a repo, which must exist. */
59587
59805
  async existingCheckout(project, repo) {
59588
59806
  const path2 = this.checkoutOf(project, repo);
59589
- if (!await exists2(join28(path2, ".git"))) {
59807
+ if (!await exists2(join29(path2, ".git"))) {
59590
59808
  throw new Error(`Repo "${repo}" has no checkout in project "${project}" on this host`);
59591
59809
  }
59592
59810
  return path2;
@@ -59885,7 +60103,7 @@ var ProjectFolder = class {
59885
60103
  async removeRepo(req) {
59886
60104
  const path2 = this.checkoutOf(req.project, req.repo);
59887
60105
  await this.exclusive(req.project, async () => {
59888
- if (!await exists2(join28(path2, ".git"))) {
60106
+ if (!await exists2(join29(path2, ".git"))) {
59889
60107
  await repoGit(req.clonePath, ["worktree", "prune"]);
59890
60108
  return;
59891
60109
  }
@@ -59900,10 +60118,14 @@ var ProjectFolder = class {
59900
60118
  `The checkout of "${req.repo}" in ${path2} has ${reasons.join(" and ")}. Commit and push or discard them first.`
59901
60119
  );
59902
60120
  }
59903
- const removed = await repoGit(req.clonePath, ["worktree", "remove", path2]);
59904
- if (removed.code !== 0) throw new Error(brief(removed));
59905
- if (branch.startsWith(`band/${req.project}/`)) {
59906
- await repoGit(req.clonePath, ["branch", "-D", branch]);
60121
+ if (await isWorktreeCheckout(path2)) {
60122
+ const removed = await repoGit(req.clonePath, ["worktree", "remove", path2]);
60123
+ if (removed.code !== 0) throw new Error(brief(removed));
60124
+ if (branch.startsWith(`band/${req.project}/`)) {
60125
+ await repoGit(req.clonePath, ["branch", "-D", branch]);
60126
+ }
60127
+ } else {
60128
+ await rm3(path2, { recursive: true, force: true });
59907
60129
  }
59908
60130
  });
59909
60131
  }
@@ -59919,6 +60141,13 @@ var ProjectFolder = class {
59919
60141
  }
59920
60142
  }
59921
60143
  };
60144
+ async function isWorktreeCheckout(path2) {
60145
+ try {
60146
+ return (await stat5(join29(path2, ".git"))).isFile();
60147
+ } catch {
60148
+ return false;
60149
+ }
60150
+ }
59922
60151
  async function exists2(path2) {
59923
60152
  try {
59924
60153
  await stat5(path2);
@@ -59929,12 +60158,12 @@ async function exists2(path2) {
59929
60158
  }
59930
60159
 
59931
60160
  // ../../packages/host-local/src/repos/host-repos.ts
59932
- import { execFile as execFile9 } from "node:child_process";
60161
+ import { execFile as execFile10 } from "node:child_process";
59933
60162
  import { createHash as createHash4, randomBytes as randomBytes2 } from "node:crypto";
59934
60163
  import { existsSync as existsSync9 } from "node:fs";
59935
60164
  import { mkdir as mkdir5, readdir as readdir6, readFile as readFile6, realpath as realpath3, rename as rename2, stat as stat6, writeFile as writeFile3 } from "node:fs/promises";
59936
- import { homedir as homedir12 } from "node:os";
59937
- import { dirname as dirname8, join as join29, sep as sep4 } from "node:path";
60165
+ import { homedir as homedir13 } from "node:os";
60166
+ import { dirname as dirname8, join as join30, sep as sep4 } from "node:path";
59938
60167
 
59939
60168
  // ../../packages/shared/src/remote-url.ts
59940
60169
  function stripGitSuffix(path2) {
@@ -60012,9 +60241,9 @@ function stripUrlCredentials(input) {
60012
60241
  }
60013
60242
 
60014
60243
  // ../../packages/host-local/src/repos/host-repos.ts
60015
- var CLONE_TIMEOUT_MS = 10 * 60 * 1e3;
60244
+ var CLONE_TIMEOUT_MS2 = 10 * 60 * 1e3;
60016
60245
  function defaultReposDir() {
60017
- return process.env.BAND_REPOS_DIR?.trim() || join29(homedir12(), "band", "repos");
60246
+ return process.env.BAND_REPOS_DIR?.trim() || join30(homedir13(), "band", "repos");
60018
60247
  }
60019
60248
  async function git(args, cwd) {
60020
60249
  try {
@@ -60027,10 +60256,10 @@ function clone2(url2, dest) {
60027
60256
  const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
60028
60257
  env.PATH = prependBinDirs(env.PATH);
60029
60258
  return new Promise((resolve16, reject) => {
60030
- execFile9(
60259
+ execFile10(
60031
60260
  "git",
60032
60261
  ["clone", "--", url2, dest],
60033
- { env, timeout: CLONE_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 },
60262
+ { env, timeout: CLONE_TIMEOUT_MS2, maxBuffer: 10 * 1024 * 1024 },
60034
60263
  (err, _stdout, stderr) => {
60035
60264
  if (err)
60036
60265
  reject(
@@ -60123,11 +60352,11 @@ var LocalRepos = class {
60123
60352
  }
60124
60353
  const running = this.ensuring.get(id.key);
60125
60354
  if (running) return running;
60126
- const run4 = this.doEnsure(id.key, id.url, id.owner, id.name).finally(
60355
+ const run5 = this.doEnsure(id.key, id.url, id.owner, id.name).finally(
60127
60356
  () => this.ensuring.delete(id.key)
60128
60357
  );
60129
- this.ensuring.set(id.key, run4);
60130
- return run4;
60358
+ this.ensuring.set(id.key, run5);
60359
+ return run5;
60131
60360
  }
60132
60361
  async doEnsure(key2, url2, owner, name5) {
60133
60362
  const mappings = await this.load();
@@ -60135,7 +60364,7 @@ var LocalRepos = class {
60135
60364
  if (mapped && existsSync9(mapped.path) && await isDirectory(mapped.path)) {
60136
60365
  return { path: mapped.path, cloned: false };
60137
60366
  }
60138
- const base = join29(this.options.reposDir(), owner, name5);
60367
+ const base = join30(this.options.reposDir(), owner, name5);
60139
60368
  let dest = base;
60140
60369
  if (existsSync9(dest)) {
60141
60370
  const origin = await git(["remote", "get-url", "origin"], dest);
@@ -60155,7 +60384,7 @@ var LocalRepos = class {
60155
60384
  }
60156
60385
  };
60157
60386
  async function browseFolder(path2) {
60158
- const home = homedir12();
60387
+ const home = homedir13();
60159
60388
  const target = await realpath3(path2?.trim() ? path2 : home).catch(() => {
60160
60389
  throw new Error(`${path2} does not exist`);
60161
60390
  });
@@ -60163,11 +60392,11 @@ async function browseFolder(path2) {
60163
60392
  const dirents = await readdir6(target, { withFileTypes: true });
60164
60393
  const entries = [];
60165
60394
  for (const d of dirents) {
60166
- const full = join29(target, d.name);
60395
+ const full = join30(target, d.name);
60167
60396
  let isDir = d.isDirectory();
60168
60397
  if (d.isSymbolicLink()) isDir = await isDirectory(full);
60169
60398
  if (!isDir) continue;
60170
- entries.push({ name: d.name, path: full, isGit: existsSync9(join29(full, ".git")) });
60399
+ entries.push({ name: d.name, path: full, isGit: existsSync9(join30(full, ".git")) });
60171
60400
  }
60172
60401
  entries.sort((a, b2) => a.name.localeCompare(b2.name));
60173
60402
  const parent = dirname8(target);
@@ -60181,7 +60410,7 @@ async function browseFolder(path2) {
60181
60410
  }
60182
60411
 
60183
60412
  // ../../packages/host-local/src/search/ripgrep-client.ts
60184
- import { execFile as execFile10, spawn as spawn6 } from "node:child_process";
60413
+ import { execFile as execFile11, spawn as spawn6 } from "node:child_process";
60185
60414
  import { rgPath } from "@vscode/ripgrep";
60186
60415
  function streamMatches(options) {
60187
60416
  return {
@@ -60303,7 +60532,7 @@ function listFiles(cwd) {
60303
60532
  "!**/.DS_Store"
60304
60533
  ];
60305
60534
  return new Promise((resolve16, reject) => {
60306
- execFile10(
60535
+ execFile11(
60307
60536
  rgPath,
60308
60537
  args,
60309
60538
  { cwd, maxBuffer: LIST_FILES_MAX_BUFFER, encoding: "utf-8" },
@@ -60320,13 +60549,13 @@ function listFiles(cwd) {
60320
60549
  }
60321
60550
 
60322
60551
  // ../../packages/host-local/src/setup/repo-config.ts
60323
- import { join as join30 } from "node:path";
60552
+ import { join as join31 } from "node:path";
60324
60553
  init_src();
60325
60554
  init_zod();
60326
60555
  var log12 = createLogger("repo-config");
60327
60556
  async function loadRepoConfig(host, worktreePath, repoPath) {
60328
60557
  for (const base of [worktreePath, repoPath]) {
60329
- const text3 = await readConfig(host, join30(base, ".band", "config.json"));
60558
+ const text3 = await readConfig(host, join31(base, ".band", "config.json"));
60330
60559
  if (text3 === null) continue;
60331
60560
  try {
60332
60561
  return JSON.parse(text3);
@@ -60337,10 +60566,10 @@ async function loadRepoConfig(host, worktreePath, repoPath) {
60337
60566
  }
60338
60567
  async function loadEnvironment(host, worktreePath, repoPath) {
60339
60568
  for (const base of [worktreePath, repoPath]) {
60340
- const file2 = join30(base, ENVIRONMENT_FILE);
60569
+ const file2 = join31(base, ENVIRONMENT_FILE);
60341
60570
  const text3 = await readConfig(host, file2);
60342
60571
  if (text3 === null) continue;
60343
- const exists4 = (relative3) => host.fs.stat(join30(base, relative3)).then(
60572
+ const exists4 = (relative3) => host.fs.stat(join31(base, relative3)).then(
60344
60573
  () => true,
60345
60574
  () => false
60346
60575
  );
@@ -60372,7 +60601,7 @@ async function readConfig(host, configPath) {
60372
60601
  }
60373
60602
  var CopyFilesSchema = external_exports2.array(external_exports2.string()).optional();
60374
60603
  async function getCopyFiles(host, repoPath) {
60375
- const configPath = join30(repoPath, ".band", "config.json");
60604
+ const configPath = join31(repoPath, ".band", "config.json");
60376
60605
  const text3 = await readConfig(host, configPath);
60377
60606
  if (text3 === null) return null;
60378
60607
  let raw;
@@ -60397,7 +60626,7 @@ async function getCopyFiles(host, repoPath) {
60397
60626
 
60398
60627
  // ../../packages/host-local/src/setup/script-run.ts
60399
60628
  init_src();
60400
- import { join as join31 } from "node:path";
60629
+ import { join as join32 } from "node:path";
60401
60630
  var log13 = createLogger("script-run");
60402
60631
  var POLL_MS = 1e3;
60403
60632
  var decoder = new TextDecoder();
@@ -60406,8 +60635,8 @@ function quote(value) {
60406
60635
  }
60407
60636
  async function prepareScriptRun(host, script, label2) {
60408
60637
  const dir = await host.fs.mkdtemp("band-script-");
60409
- const scriptFile = join31(dir, `${label2}.sh`);
60410
- const exitFile = join31(dir, "exit-code");
60638
+ const scriptFile = join32(dir, `${label2}.sh`);
60639
+ const exitFile = join32(dir, "exit-code");
60411
60640
  const partialFile = `${exitFile}.partial`;
60412
60641
  const onExit = [
60413
60642
  "rc=$?",
@@ -60467,7 +60696,7 @@ async function prepareScriptRun(host, script, label2) {
60467
60696
 
60468
60697
  // ../../packages/host-local/src/setup/worktree-files.ts
60469
60698
  init_src();
60470
- import { dirname as dirname9, isAbsolute, join as join32, relative, resolve as resolve7, sep as sep5 } from "node:path";
60699
+ import { dirname as dirname9, isAbsolute, join as join33, relative, resolve as resolve7, sep as sep5 } from "node:path";
60471
60700
  var log14 = createLogger("worktree-files");
60472
60701
  var MAX_COPY_FILES_ENTRIES = 100;
60473
60702
  var MAX_COPY_FILES_MATCHES = 500;
@@ -60498,7 +60727,7 @@ async function copyWorktreeFiles(host, repoPath, worktreePath) {
60498
60727
  }
60499
60728
  for (const [absSource, source] of byAbs) {
60500
60729
  const rel = relative(repoPath, absSource);
60501
- const dest = join32(worktreePath, rel);
60730
+ const dest = join33(worktreePath, rel);
60502
60731
  if (rel.startsWith("..") || isAbsolute(rel)) {
60503
60732
  log14.warn({ source, entry: absSource }, "refusing to copy file outside repo root \u2014 skipping");
60504
60733
  continue;
@@ -60587,7 +60816,7 @@ async function resolveFromConfig(host, repoPath, missing) {
60587
60816
  return out;
60588
60817
  }
60589
60818
  async function resolveFromWorktreeInclude(host, repoPath) {
60590
- const includePath = join32(repoPath, ".worktreeinclude");
60819
+ const includePath = join33(repoPath, ".worktreeinclude");
60591
60820
  let includeContent;
60592
60821
  try {
60593
60822
  includeContent = new TextDecoder().decode(await host.fs.readFile(includePath));
@@ -60629,14 +60858,15 @@ function hasGlobMeta(pattern) {
60629
60858
 
60630
60859
  // ../../packages/host-local/src/local-host.ts
60631
60860
  var TOOLS_TTL_MS = 6e4;
60861
+ var REPORT_TTL_MS = 2 * 6e4;
60632
60862
  var EXEC_MAX_BUFFER = 50 * 1024 * 1024;
60633
60863
  var LOCAL_HOST_ID = "local";
60634
60864
  var LocalHost = class {
60635
60865
  constructor(options) {
60636
60866
  this.options = options;
60637
- const bandHome2 = () => process.env.BAND_HOME ?? join33(homedir13(), ".band");
60867
+ const bandHome2 = () => process.env.BAND_HOME ?? join34(homedir14(), ".band");
60638
60868
  this.repos = new LocalRepos({
60639
- mappingsFile: options.repos?.mappingsFile ?? (() => join33(bandHome2(), "repo-mappings.json")),
60869
+ mappingsFile: options.repos?.mappingsFile ?? (() => join34(bandHome2(), "repo-mappings.json")),
60640
60870
  reposDir: options.repos?.reposDir ?? defaultReposDir
60641
60871
  });
60642
60872
  const source = options.context;
@@ -60700,7 +60930,7 @@ var LocalHost = class {
60700
60930
  openStdio: (spec) => openMcpStdio(spec)
60701
60931
  };
60702
60932
  chromium = new ChromiumManager(
60703
- () => join33(process.env.BAND_HOME ?? join33(homedir13(), ".band"), "browser-profiles")
60933
+ () => join34(process.env.BAND_HOME ?? join34(homedir14(), ".band"), "browser-profiles")
60704
60934
  );
60705
60935
  browser = {
60706
60936
  open: (spec) => this.chromium.open(spec),
@@ -60787,7 +61017,31 @@ var LocalHost = class {
60787
61017
  })();
60788
61018
  return this.cliFacts;
60789
61019
  }
61020
+ reportValue;
61021
+ reportAt = 0;
61022
+ reportProbe = null;
61023
+ /**
61024
+ * Probing the agent CLIs runs several processes and `claude auth status` can take seconds, so
61025
+ * `info()` never waits for it: it returns the last report and refreshes it in the background once
61026
+ * it is older than `REPORT_TTL_MS`. The first `info()` has no report yet.
61027
+ */
61028
+ refreshReport() {
61029
+ if (this.reportProbe) return this.reportProbe;
61030
+ if (this.reportValue && Date.now() - this.reportAt <= REPORT_TTL_MS) return Promise.resolve();
61031
+ this.reportProbe = probeCapabilities().then((value) => {
61032
+ this.reportValue = value;
61033
+ this.reportAt = Date.now();
61034
+ }).catch(() => void 0).finally(() => {
61035
+ this.reportProbe = null;
61036
+ });
61037
+ return this.reportProbe;
61038
+ }
61039
+ /** Waits for the capability report to exist. A worker calls it once before its hello, which carries it. */
61040
+ async awaitReport() {
61041
+ await this.refreshReport();
61042
+ }
60790
61043
  async info() {
61044
+ void this.refreshReport();
60791
61045
  const { versions: probed, gh } = await this.probeCliFacts();
60792
61046
  const versions = { ...probed };
60793
61047
  return {
@@ -60795,11 +61049,12 @@ var LocalHost = class {
60795
61049
  os: process.platform,
60796
61050
  arch: process.arch,
60797
61051
  hostname: hostname3(),
60798
- home: homedir13(),
61052
+ home: homedir14(),
60799
61053
  labels: [],
60800
61054
  roots: [],
60801
61055
  versions,
60802
61056
  tools: await this.toolVersions(),
61057
+ report: this.reportValue,
60803
61058
  repoMappings: await this.repos.list(),
60804
61059
  capabilities: {
60805
61060
  git: "git" in versions,
@@ -60817,7 +61072,7 @@ var LocalHost = class {
60817
61072
  return new Promise((resolve16, reject) => {
60818
61073
  const env = { ...process.env, ...options.env };
60819
61074
  env.PATH = prependBinDirs(env.PATH);
60820
- execFile11(
61075
+ execFile12(
60821
61076
  bin,
60822
61077
  args,
60823
61078
  { cwd: options.cwd, env, maxBuffer: EXEC_MAX_BUFFER, timeout: options.timeoutMs },
@@ -60872,7 +61127,7 @@ var localFs = {
60872
61127
  for await (const match of fsGlob(pattern, { cwd })) matches2.push(match);
60873
61128
  return matches2;
60874
61129
  },
60875
- mkdtemp: (prefix) => mkdtemp(join33(tmpdir2(), prefix)),
61130
+ mkdtemp: (prefix) => mkdtemp(join34(tmpdir2(), prefix)),
60876
61131
  async list(path2) {
60877
61132
  const entries = await readdir7(path2, { withFileTypes: true });
60878
61133
  return entries.map((entry) => ({ name: entry.name, kind: kindOf(entry) }));
@@ -60880,7 +61135,7 @@ var localFs = {
60880
61135
  async mkdir(path2, options) {
60881
61136
  await mkdir6(path2, { recursive: options?.recursive });
60882
61137
  },
60883
- rm: (path2, options) => rm3(path2, options),
61138
+ rm: (path2, options) => rm4(path2, options),
60884
61139
  rename: (from, to) => rename3(from, to),
60885
61140
  // dereference: copy a symlink's target bytes, as copyFileSync did, so the copy never links back into the source tree.
60886
61141
  copy: (from, to, options) => cp(from, to, {
@@ -61086,6 +61341,12 @@ function chatScope(chat) {
61086
61341
  throw new Error("A chat has neither a worktree nor a project");
61087
61342
  }
61088
61343
 
61344
+ // src/server/infra/host/local-host-enabled.ts
61345
+ function isLocalHostEnabled() {
61346
+ const value = process.env.BAND_LOCAL_HOST?.trim().toLowerCase();
61347
+ return !(value === "off" || value === "false" || value === "0");
61348
+ }
61349
+
61089
61350
  // src/server/infra/host/registry.ts
61090
61351
  var worktreeQueries = new WorktreeQueries();
61091
61352
  var repoQueries2 = new RepoQueries();
@@ -61110,13 +61371,22 @@ var HostRegistry = class {
61110
61371
  return host;
61111
61372
  }
61112
61373
  hostFor(worktreeId2) {
61374
+ if (projectIdOfScope(worktreeId2)) {
61375
+ const id = this.hostIdOfScope(worktreeId2);
61376
+ if (!id) throw new Error("The project has no host yet: it is waiting for a worker");
61377
+ return this.hostById(id);
61378
+ }
61113
61379
  if (this.hosts.size === 1) return this.local;
61114
61380
  return this.hostById(this.hostIdOfScope(worktreeId2) ?? this.local.id);
61115
61381
  }
61116
61382
  /** The host id of a worktree, or of a project chat's scope (the project's coordinator host). */
61117
61383
  hostIdOfScope(scopeId) {
61118
61384
  const projectId = projectIdOfScope(scopeId);
61119
- if (projectId) return projectQueries.find(projectId)?.coordinatorHostId ?? this.local.id;
61385
+ if (projectId) {
61386
+ const placed = projectQueries.find(projectId)?.coordinatorHostId;
61387
+ if (placed && (placed !== this.local.id || isLocalHostEnabled())) return placed;
61388
+ return isLocalHostEnabled() ? this.local.id : null;
61389
+ }
61120
61390
  return worktreeQueries.findHostId(scopeId);
61121
61391
  }
61122
61392
  /**
@@ -61156,7 +61426,7 @@ var hostRegistry = new HostRegistry(
61156
61426
  // The hub is a host too. Its working copies sit in its own BAND_HOME and read the bare repos by path.
61157
61427
  context: {
61158
61428
  bandHome: () => bandHome(),
61159
- remote: async (name5) => ({ url: join37(bandHome(), "context", `${name5}.git`) })
61429
+ remote: async (name5) => ({ url: join38(bandHome(), "context", `${name5}.git`) })
61160
61430
  },
61161
61431
  // `gh` is the GitHub plugin's tool: with that plugin disabled the host never runs it. The
61162
61432
  // import is dynamic because the services import this registry.
@@ -61278,27 +61548,27 @@ function textChunk(row) {
61278
61548
  }
61279
61549
  function rowsToEvents(rows) {
61280
61550
  const out = [];
61281
- let run4 = null;
61551
+ let run5 = null;
61282
61552
  const flush = () => {
61283
- if (!run4) return;
61553
+ if (!run5) return;
61284
61554
  out.push({
61285
61555
  type: "update",
61286
- update: { ...run4.update, content: { type: "text", text: run4.text } },
61287
- eventId: run4.lastId,
61288
- createdAt: run4.createdAt
61556
+ update: { ...run5.update, content: { type: "text", text: run5.text } },
61557
+ eventId: run5.lastId,
61558
+ createdAt: run5.createdAt
61289
61559
  });
61290
- run4 = null;
61560
+ run5 = null;
61291
61561
  };
61292
61562
  for (const row of rows) {
61293
61563
  const chunk = textChunk(row);
61294
- if (chunk && run4 && run4.update.sessionUpdate === chunk.sessionUpdate && (run4.update.messageId ?? null) === (chunk.messageId ?? null)) {
61295
- if (chunk.content.type === "text") run4.text += chunk.content.text;
61296
- run4.lastId = row.id;
61564
+ if (chunk && run5 && run5.update.sessionUpdate === chunk.sessionUpdate && (run5.update.messageId ?? null) === (chunk.messageId ?? null)) {
61565
+ if (chunk.content.type === "text") run5.text += chunk.content.text;
61566
+ run5.lastId = row.id;
61297
61567
  continue;
61298
61568
  }
61299
61569
  flush();
61300
61570
  if (chunk) {
61301
- run4 = {
61571
+ run5 = {
61302
61572
  update: chunk,
61303
61573
  text: chunk.content.type === "text" ? chunk.content.text : "",
61304
61574
  lastId: row.id,
@@ -61415,12 +61685,12 @@ function resolvePolicy(policy, coordinatorModel) {
61415
61685
  // src/server/services/_utils/shared-dir.ts
61416
61686
  import { readdirSync as readdirSync8 } from "node:fs";
61417
61687
  import { mkdir as mkdir7 } from "node:fs/promises";
61418
- import { join as join38 } from "node:path";
61688
+ import { join as join39 } from "node:path";
61419
61689
  init_state();
61420
61690
  async function openSharedDir(worktreeId2) {
61421
61691
  const host = hostRegistry.hostFor(worktreeId2);
61422
61692
  if (host.id === hostRegistry.local.id) {
61423
- const path3 = join38(bandHome(), "shared", worktreeId2);
61693
+ const path3 = join39(bandHome(), "shared", worktreeId2);
61424
61694
  await mkdir7(path3, { recursive: true });
61425
61695
  return {
61426
61696
  path: path3,
@@ -61435,7 +61705,7 @@ async function openSharedDir(worktreeId2) {
61435
61705
  }
61436
61706
  const dirs = (await host.info()).dirs;
61437
61707
  if (!dirs) throw new Error(`Host ${host.id} has no directory for shared files`);
61438
- const path2 = join38(dirs.shared, worktreeId2);
61708
+ const path2 = join39(dirs.shared, worktreeId2);
61439
61709
  await host.fs.mkdir(path2, { recursive: true });
61440
61710
  return {
61441
61711
  path: path2,
@@ -61451,7 +61721,7 @@ async function openSharedDir(worktreeId2) {
61451
61721
  async function agentExtraDirs(worktreeId2) {
61452
61722
  const host = hostRegistry.hostFor(worktreeId2);
61453
61723
  if (host.id === hostRegistry.local.id) {
61454
- return [join38(bandHome(), "uploads"), join38(bandHome(), "shared")];
61724
+ return [join39(bandHome(), "uploads"), join39(bandHome(), "shared")];
61455
61725
  }
61456
61726
  const dirs = (await host.info()).dirs;
61457
61727
  return dirs ? [dirs.uploads, dirs.shared] : [];
@@ -62013,13 +62283,13 @@ function parseBatchedCIResponse(data, aliases, defaultBranches) {
62013
62283
  }
62014
62284
  let aggregatedState = "success";
62015
62285
  let aggregatedUrl = null;
62016
- for (const run4 of latestByWorkflow.values()) {
62286
+ for (const run5 of latestByWorkflow.values()) {
62017
62287
  let runState;
62018
- if (run4.status === "IN_PROGRESS" || run4.status === "QUEUED") {
62019
- runState = run4.status === "QUEUED" ? "pending" : "running";
62020
- } else if (run4.conclusion === "FAILURE") {
62288
+ if (run5.status === "IN_PROGRESS" || run5.status === "QUEUED") {
62289
+ runState = run5.status === "QUEUED" ? "pending" : "running";
62290
+ } else if (run5.conclusion === "FAILURE") {
62021
62291
  runState = "failure";
62022
- } else if (run4.conclusion === "CANCELLED") {
62292
+ } else if (run5.conclusion === "CANCELLED") {
62023
62293
  runState = "cancelled";
62024
62294
  } else {
62025
62295
  runState = "success";
@@ -62027,7 +62297,7 @@ function parseBatchedCIResponse(data, aliases, defaultBranches) {
62027
62297
  const priority = statePriority(runState);
62028
62298
  if (priority >= statePriority(aggregatedState)) {
62029
62299
  aggregatedState = runState;
62030
- aggregatedUrl = run4.url;
62300
+ aggregatedUrl = run5.url;
62031
62301
  }
62032
62302
  }
62033
62303
  results.set(alias, {
@@ -62055,14 +62325,14 @@ async function mapLimited(items, limit, fn) {
62055
62325
  }
62056
62326
 
62057
62327
  // src/server/services/github-poll-service.ts
62058
- import { tmpdir as tmpdir5 } from "node:os";
62328
+ import { tmpdir as tmpdir6 } from "node:os";
62059
62329
  init_src();
62060
62330
 
62061
62331
  // src/server/infra/subscriptions/github.ts
62062
62332
  init_settings();
62063
62333
  import { createHmac, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
62064
62334
  import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
62065
- import { dirname as dirname11, join as join39 } from "node:path";
62335
+ import { dirname as dirname11, join as join40 } from "node:path";
62066
62336
  var GITHUB_HOOK_EVENTS = [
62067
62337
  "pull_request",
62068
62338
  "pull_request_review",
@@ -62077,7 +62347,7 @@ var SNIPPET_LIMIT = 200;
62077
62347
  var githubPrKey = (repo, number4) => `github:pr:${repo}#${number4}`;
62078
62348
  var githubCiKey = (repo, branch) => `github:ci:${repo}@${branch}`;
62079
62349
  function secretPath() {
62080
- return join39(bandHome(), "github-webhook-secret");
62350
+ return join40(bandHome(), "github-webhook-secret");
62081
62351
  }
62082
62352
  function readWebhookSecret() {
62083
62353
  const fromEnv = process.env.BAND_GITHUB_WEBHOOK_SECRET;
@@ -62213,10 +62483,10 @@ function parseGithubDelivery(eventType, delivery, payload, now = Date.now()) {
62213
62483
  case "check_suite":
62214
62484
  case "workflow_run": {
62215
62485
  if (action !== "completed") return { type: "ignored" };
62216
- const run4 = obj(body[eventType]);
62217
- const suite = obj(run4?.check_suite);
62218
- const branch = str(run4?.head_branch) || str(suite?.head_branch);
62219
- const sha = str(run4?.head_sha) || str(suite?.head_sha);
62486
+ const run5 = obj(body[eventType]);
62487
+ const suite = obj(run5?.check_suite);
62488
+ const branch = str(run5?.head_branch) || str(suite?.head_branch);
62489
+ const sha = str(run5?.head_sha) || str(suite?.head_sha);
62220
62490
  if (!branch || !/^[0-9a-f]{7,64}$/i.test(sha)) return { type: "ignored" };
62221
62491
  return { type: "check", trigger: { repo, branch, sha } };
62222
62492
  }
@@ -62265,15 +62535,15 @@ function parseCheckRuns(output) {
62265
62535
  const runs = parsed?.check_runs;
62266
62536
  if (!Array.isArray(runs)) throw new Error("Unexpected check-runs response");
62267
62537
  return runs.flatMap((r) => {
62268
- const run4 = obj(r);
62269
- if (!run4) return [];
62538
+ const run5 = obj(r);
62539
+ if (!run5) return [];
62270
62540
  return [
62271
62541
  {
62272
- name: str(run4.name) || "unnamed check",
62273
- status: str(run4.status),
62274
- conclusion: typeof run4.conclusion === "string" ? run4.conclusion : null,
62275
- url: str(run4.html_url) || void 0,
62276
- headSha: str(run4.head_sha) || void 0
62542
+ name: str(run5.name) || "unnamed check",
62543
+ status: str(run5.status),
62544
+ conclusion: typeof run5.conclusion === "string" ? run5.conclusion : null,
62545
+ url: str(run5.html_url) || void 0,
62546
+ headSha: str(run5.head_sha) || void 0
62277
62547
  }
62278
62548
  ];
62279
62549
  });
@@ -62379,7 +62649,7 @@ function initialCursor(createdAt) {
62379
62649
  }
62380
62650
 
62381
62651
  // src/server/services/_utils/hub-gh-auth.ts
62382
- import { tmpdir as tmpdir4 } from "node:os";
62652
+ import { tmpdir as tmpdir5 } from "node:os";
62383
62653
 
62384
62654
  // src/server/services/vault-service.ts
62385
62655
  import { createHash as createHash9, randomUUID as randomUUID13, timingSafeEqual as timingSafeEqual6 } from "node:crypto";
@@ -62786,14 +63056,14 @@ function accountFromIdToken(idToken) {
62786
63056
  // src/server/services/_utils/vault-crypto.ts
62787
63057
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes5 } from "node:crypto";
62788
63058
  import { chmodSync as chmodSync3, existsSync as existsSync13, readFileSync as readFileSync9, renameSync as renameSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
62789
- import { join as join40 } from "node:path";
63059
+ import { join as join41 } from "node:path";
62790
63060
  init_settings();
62791
63061
  var VERSION = "v1:";
62792
63062
  var KEY_BYTES = 32;
62793
63063
  var NONCE_BYTES = 12;
62794
63064
  var TAG_BYTES = 16;
62795
63065
  function vaultKeyFile() {
62796
- return join40(bandHome(), "vault.key");
63066
+ return join41(bandHome(), "vault.key");
62797
63067
  }
62798
63068
  function keySource() {
62799
63069
  return process.env.BAND_VAULT_KEY?.trim() ? "env" : "file";
@@ -62853,7 +63123,7 @@ function decrypt(key2, aad, blob2) {
62853
63123
  }
62854
63124
 
62855
63125
  // src/server/services/github-webhook-service.ts
62856
- import { tmpdir as tmpdir3 } from "node:os";
63126
+ import { tmpdir as tmpdir4 } from "node:os";
62857
63127
  init_src();
62858
63128
 
62859
63129
  // src/server/services/pushed-sha-service.ts
@@ -64034,7 +64304,7 @@ init_state();
64034
64304
  init_src2();
64035
64305
  import { randomUUID as randomUUID11 } from "node:crypto";
64036
64306
  import { unlinkSync as unlinkSync4 } from "node:fs";
64037
- import { join as join46, posix as posix6 } from "node:path";
64307
+ import { join as join48, posix as posix6 } from "node:path";
64038
64308
 
64039
64309
  // ../../packages/host-local/src/git/git-refusals.ts
64040
64310
  var MAX_LISTED_FILES = 3;
@@ -64439,7 +64709,7 @@ var placementInput = external_exports2.object({
64439
64709
  });
64440
64710
 
64441
64711
  // src/server/services/_utils/write-brief.ts
64442
- import { dirname as dirname12, join as join41, posix as posix3 } from "node:path";
64712
+ import { dirname as dirname12, join as join42, posix as posix3 } from "node:path";
64443
64713
 
64444
64714
  // src/server/services/_utils/dispatch-brief.ts
64445
64715
  var BRIEF_DIR = ".am";
@@ -64478,7 +64748,7 @@ ${input.scenarios.map((s2, i) => `- S${i + 1}: ${s2.trim()}`).join("\n")}`
64478
64748
 
64479
64749
  // src/server/services/_utils/write-brief.ts
64480
64750
  async function writeBrief(host, worktreePath, brief2, remote) {
64481
- const p2 = remote ? posix3 : { join: join41 };
64751
+ const p2 = remote ? posix3 : { join: join42 };
64482
64752
  const dir = p2.join(worktreePath, BRIEF_DIR);
64483
64753
  await host.fs.mkdir(dir, { recursive: true });
64484
64754
  await host.fs.writeFile(p2.join(dir, BRIEF_FILE), brief2);
@@ -64588,7 +64858,7 @@ function removeTerminalFromLayout(worktreeId2, terminalId) {
64588
64858
  // src/server/services/ephemeral-lifecycle-service.ts
64589
64859
  import { rmSync as rmSync4 } from "node:fs";
64590
64860
  import { mkdir as mkdir8, readFile as readFile8, writeFile as writeFile5 } from "node:fs/promises";
64591
- import { join as join43, posix as posix5 } from "node:path";
64861
+ import { join as join44, posix as posix5 } from "node:path";
64592
64862
 
64593
64863
  // ../../packages/host-remote/src/pty.ts
64594
64864
  import { StringDecoder } from "node:string_decoder";
@@ -66299,6 +66569,7 @@ init_repos();
66299
66569
  init_isolation();
66300
66570
  init_runner_config();
66301
66571
  init_settings_service();
66572
+ init_state();
66302
66573
 
66303
66574
  // src/server/services/token-service.ts
66304
66575
  init_src();
@@ -66390,6 +66661,10 @@ var TokenQueries = class _TokenQueries {
66390
66661
  markHostOnline(id, at, fields) {
66391
66662
  this.db().update(hosts).set({ status: "online", lastSeenAt: at, info: fields.info, version: fields.version }).where(eq(hosts.id, id)).run();
66392
66663
  }
66664
+ /** Replaces what a host reported about itself, without touching its status. */
66665
+ setHostInfo(id, info) {
66666
+ this.db().update(hosts).set({ info }).where(eq(hosts.id, id)).run();
66667
+ }
66393
66668
  /** Sets a host's status and, when the worker was last heard from, its last-seen time. */
66394
66669
  setHostStatus(id, status, lastSeenAt) {
66395
66670
  this.db().update(hosts).set(lastSeenAt === void 0 ? { status } : { status, lastSeenAt }).where(eq(hosts.id, id)).run();
@@ -66412,12 +66687,6 @@ var TokenQueries = class _TokenQueries {
66412
66687
  }
66413
66688
  };
66414
66689
 
66415
- // src/server/infra/host/local-host-enabled.ts
66416
- function isLocalHostEnabled() {
66417
- const value = process.env.BAND_LOCAL_HOST?.trim().toLowerCase();
66418
- return !(value === "off" || value === "false" || value === "0");
66419
- }
66420
-
66421
66690
  // src/server/services/token-service.ts
66422
66691
  var log19 = createLogger("token-service");
66423
66692
  var SHARED_TOKEN_ID = "shared";
@@ -66444,6 +66713,16 @@ function stringMap(value) {
66444
66713
  Object.entries(value).filter((e) => typeof e[1] === "string")
66445
66714
  );
66446
66715
  }
66716
+ function reportOf(info) {
66717
+ const report2 = info?.report;
66718
+ return report2 && Array.isArray(report2.agents) && Array.isArray(report2.tools) ? report2 : null;
66719
+ }
66720
+ function usableAgents(info) {
66721
+ const report2 = reportOf(info);
66722
+ if (report2)
66723
+ return report2.agents.filter((a) => a.installed && a.loggedIn !== false).map((a) => a.type);
66724
+ return stringList(info?.agents);
66725
+ }
66447
66726
  function stringList(value) {
66448
66727
  return Array.isArray(value) ? value.filter((v2) => typeof v2 === "string") : [];
66449
66728
  }
@@ -66707,7 +66986,8 @@ var TokenService = class {
66707
66986
  version: h.version,
66708
66987
  createdAt: h.createdAt,
66709
66988
  info: h.info,
66710
- agents: stringList(h.info?.agents),
66989
+ agents: usableAgents(h.info),
66990
+ report: reportOf(h.info),
66711
66991
  roots: stringList(h.info?.roots),
66712
66992
  capabilities: capabilityNames(h.info?.capabilities),
66713
66993
  home: typeof h.info?.home === "string" ? h.info.home : null,
@@ -66889,6 +67169,7 @@ var PlacementService = class {
66889
67169
  const tools = stringRecord(info?.tools ?? stored.tools);
66890
67170
  out.push({
66891
67171
  id: row.id,
67172
+ agents: row.agents,
66892
67173
  labels: [
66893
67174
  .../* @__PURE__ */ new Set([...stringList2(row.labels), ...stringList2(info?.labels ?? stored.labels)])
66894
67175
  ],
@@ -66909,10 +67190,12 @@ var PlacementService = class {
66909
67190
  * a host, and a `worktree` worktree never lands on a host that was started
66910
67191
  * for one of those.
66911
67192
  */
66912
- async place(placement, onlyHosts) {
67193
+ async place(placement, onlyHosts, agent) {
66913
67194
  if (requestedIsolation(placement.environment ?? null) !== "worktree") return null;
66914
67195
  const fits = (await this.candidates()).filter(
66915
- (c) => !isExclusiveHost(c.labels) && (onlyHosts === void 0 || onlyHosts.includes(c.id)) && matches(c, placement)
67196
+ (c) => !isExclusiveHost(c.labels) && (onlyHosts === void 0 || onlyHosts.includes(c.id)) && // A worker must report the agent installed and logged in. The hub's own machine is
67197
+ // described live elsewhere, so it is not held to a report here.
67198
+ (agent === void 0 || c.id === LOCAL_HOST_ID2 || c.agents.includes(agent)) && matches(c, placement)
66916
67199
  );
66917
67200
  if (fits.length === 0) return null;
66918
67201
  const load = this.queries.worktreeCounts();
@@ -66938,7 +67221,8 @@ var PlacementService = class {
66938
67221
  const open4 = this.queries.findOpenForWorktree(worktreeId2);
66939
67222
  if (open4) return { kind: "request", requestId: open4.id };
66940
67223
  const holders = this.holdersOfRemotelessRepo(input.repo);
66941
- const hostId2 = await this.place(placement, holders ?? void 0);
67224
+ const agent = input.codingAgentId ? loadSettings().codingAgents?.find((a) => a.id === input.codingAgentId)?.type ?? input.codingAgentId : void 0;
67225
+ const hostId2 = await this.place(placement, holders ?? void 0, agent);
66942
67226
  if (hostId2) return { kind: "host", hostId: hostId2 };
66943
67227
  if (holders) {
66944
67228
  throw new Error(
@@ -67195,7 +67479,7 @@ var placementService = new PlacementService();
67195
67479
  import { spawn as spawn8 } from "node:child_process";
67196
67480
  import { randomBytes as randomBytes10 } from "node:crypto";
67197
67481
  import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync10, rmSync as rmSync3, statSync as statSync11 } from "node:fs";
67198
- import { dirname as dirname13, join as join42 } from "node:path";
67482
+ import { dirname as dirname13, join as join43 } from "node:path";
67199
67483
  init_src();
67200
67484
 
67201
67485
  // src/server/infra/db/queries/runner-machines.ts
@@ -68189,7 +68473,7 @@ var RunnerService = class {
68189
68473
  }
68190
68474
  }
68191
68475
  logFile(requestId2) {
68192
- return join42(bandHome(), "runners", "logs", `${requestId2}.log`);
68476
+ return join43(bandHome(), "runners", "logs", `${requestId2}.log`);
68193
68477
  }
68194
68478
  /** Every valid runner in settings. */
68195
68479
  configs() {
@@ -68205,12 +68489,12 @@ var RunnerService = class {
68205
68489
  }
68206
68490
  /** Whether the worker id belongs to an attempt in flight. */
68207
68491
  isAttempting(workerId) {
68208
- for (const run4 of this.running.values()) if (run4.workerId === workerId) return true;
68492
+ for (const run5 of this.running.values()) if (run5.workerId === workerId) return true;
68209
68493
  return false;
68210
68494
  }
68211
68495
  runningCount(runnerId2) {
68212
68496
  let n = 0;
68213
- for (const run4 of this.running.values()) if (run4.runnerId === runnerId2) n++;
68497
+ for (const run5 of this.running.values()) if (run5.runnerId === runnerId2) n++;
68214
68498
  return n;
68215
68499
  }
68216
68500
  // ---- leasing -------------------------------------------------------------
@@ -68246,7 +68530,7 @@ var RunnerService = class {
68246
68530
  }
68247
68531
  }
68248
68532
  begin(runner, row) {
68249
- const run4 = {
68533
+ const run5 = {
68250
68534
  requestId: row.id,
68251
68535
  runnerId: runner.id,
68252
68536
  worktreeId: row.worktreeId,
@@ -68257,27 +68541,27 @@ var RunnerService = class {
68257
68541
  endedAt: null,
68258
68542
  error: null
68259
68543
  };
68260
- this.running.set(row.id, run4);
68261
- const done = this.run(runner, row, run4).catch(
68544
+ this.running.set(row.id, run5);
68545
+ const done = this.run(runner, row, run5).catch(
68262
68546
  (err) => log22.error(`run of ${row.id} crashed: ${err instanceof Error ? err.stack : err}`)
68263
68547
  ).finally(() => {
68264
68548
  this.running.delete(row.id);
68265
- run4.endedAt = Date.now();
68266
- this.history.push(run4);
68549
+ run5.endedAt = Date.now();
68550
+ this.history.push(run5);
68267
68551
  if (this.history.length > HISTORY_LIMIT) this.history.shift();
68268
68552
  this.settled.delete(done);
68269
68553
  });
68270
68554
  this.settled.add(done);
68271
68555
  }
68272
68556
  // ---- one request ---------------------------------------------------------
68273
- async run(runner, row, run4) {
68557
+ async run(runner, row, run5) {
68274
68558
  const runLog = new RunLog(this.logFile(row.id));
68275
68559
  runLog.write("hub", `runner ${runner.id} took request ${row.id} for ${row.worktreeId}`);
68276
68560
  const environment = parseRequestEnvironment(row);
68277
68561
  if (!environment.ok) {
68278
68562
  runLog.write("hub", environment.error);
68279
- run4.status = "failed";
68280
- run4.error = environment.error;
68563
+ run5.status = "failed";
68564
+ run5.error = environment.error;
68281
68565
  try {
68282
68566
  placementService.fail(row.id, environment.error);
68283
68567
  } catch (err) {
@@ -68287,19 +68571,19 @@ var RunnerService = class {
68287
68571
  }
68288
68572
  let lastError = "";
68289
68573
  for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
68290
- run4.attempt = attempt;
68574
+ run5.attempt = attempt;
68291
68575
  runLog.write("hub", `attempt ${attempt} of ${ATTEMPTS}`);
68292
68576
  const attemptState = { hostId: null, token: null };
68293
68577
  try {
68294
- const hostId2 = await this.attempt(runner, row, run4, runLog, attemptState);
68295
- run4.workerId = hostId2;
68296
- run4.status = "ready";
68578
+ const hostId2 = await this.attempt(runner, row, run5, runLog, attemptState);
68579
+ run5.workerId = hostId2;
68580
+ run5.status = "ready";
68297
68581
  runLog.write("hub", `worker ${hostId2} said hello; request fulfilled`);
68298
68582
  return;
68299
68583
  } catch (err) {
68300
68584
  if (err instanceof Aborted) {
68301
- run4.status = "aborted";
68302
- run4.error = err.message;
68585
+ run5.status = "aborted";
68586
+ run5.error = err.message;
68303
68587
  runLog.write("hub", `stopped: ${err.message}`);
68304
68588
  await this.cleanup(runner, row, attemptState, runLog);
68305
68589
  return;
@@ -68310,8 +68594,8 @@ var RunnerService = class {
68310
68594
  }
68311
68595
  }
68312
68596
  const reason = this.failureReason(runner, lastError, runLog);
68313
- run4.status = "failed";
68314
- run4.error = lastError;
68597
+ run5.status = "failed";
68598
+ run5.error = lastError;
68315
68599
  try {
68316
68600
  placementService.fail(row.id, reason);
68317
68601
  } catch (err) {
@@ -68384,13 +68668,13 @@ ${tail}` : head;
68384
68668
  * runner holds a snapshot for tries `restore` first, and starts a fresh worker with `spawn` when
68385
68669
  * that fails, so the git and session state the sleep stored is what brings the worktree back.
68386
68670
  */
68387
- async attempt(runner, row, run4, runLog, state2) {
68671
+ async attempt(runner, row, run5, runLog, state2) {
68388
68672
  const wake = wakeOf(row);
68389
68673
  const snapshot = wake ? this.restorableSnapshot(runner, wake.hostId) : void 0;
68390
68674
  if (snapshot) {
68391
68675
  this.restoring.add(snapshot.id);
68392
68676
  try {
68393
- return await this.launch(runner, row, run4, runLog, state2, "restore", snapshot);
68677
+ return await this.launch(runner, row, run5, runLog, state2, "restore", snapshot);
68394
68678
  } catch (err) {
68395
68679
  if (err instanceof Aborted) throw err;
68396
68680
  runLog.write(
@@ -68403,7 +68687,7 @@ ${tail}` : head;
68403
68687
  this.restoring.delete(snapshot.id);
68404
68688
  }
68405
68689
  }
68406
- return this.launch(runner, row, run4, runLog, state2, "spawn");
68690
+ return this.launch(runner, row, run5, runLog, state2, "spawn");
68407
68691
  }
68408
68692
  /** The snapshot a wake can restore from: this runner's, still alive, and the runner has a `restore` hook. */
68409
68693
  restorableSnapshot(runner, hostId2) {
@@ -68413,7 +68697,7 @@ ${tail}` : head;
68413
68697
  return void 0;
68414
68698
  return latest;
68415
68699
  }
68416
- async launch(runner, row, run4, runLog, state2, kind, snapshot) {
68700
+ async launch(runner, row, run5, runLog, state2, kind, snapshot) {
68417
68701
  const deadline = Date.now() + runner.timeoutSec * 1e3;
68418
68702
  const labelList = Object.entries(row.labels).map(([k2, v2]) => `${k2}=${v2}`);
68419
68703
  const wanted = requestedIsolation(row.environment);
@@ -68425,7 +68709,7 @@ ${tail}` : head;
68425
68709
  state2.reused = wake !== null;
68426
68710
  state2.token = issued.token;
68427
68711
  runLog.addSecret(issued.token);
68428
- run4.workerId = issued.hostId;
68712
+ run5.workerId = issued.hostId;
68429
68713
  runLog.write("hub", `issued worker ${issued.hostId}`);
68430
68714
  const machineId = this.recordMachine(runner, row, issued.hostId);
68431
68715
  state2.machineId = machineId;
@@ -68555,11 +68839,11 @@ ${tail}` : head;
68555
68839
  destroyMachine(machineId, reason) {
68556
68840
  const inflight = this.destroying.get(machineId);
68557
68841
  if (inflight) return inflight;
68558
- const run4 = this.doDestroyMachine(machineId, reason).finally(
68842
+ const run5 = this.doDestroyMachine(machineId, reason).finally(
68559
68843
  () => this.destroying.delete(machineId)
68560
68844
  );
68561
- this.destroying.set(machineId, run4);
68562
- return run4;
68845
+ this.destroying.set(machineId, run5);
68846
+ return run5;
68563
68847
  }
68564
68848
  async doDestroyMachine(machineId, reason) {
68565
68849
  const machine = this.machines.get(machineId);
@@ -68568,7 +68852,7 @@ ${tail}` : head;
68568
68852
  const runner = this.findRunner(machine.runnerId);
68569
68853
  const request = machine.requestId ? placementService.get(machine.requestId) : void 0;
68570
68854
  const runLog = new RunLog(
68571
- machine.requestId ? this.logFile(machine.requestId) : join42(bandHome(), "runners", "logs", `${machine.id}.log`)
68855
+ machine.requestId ? this.logFile(machine.requestId) : join43(bandHome(), "runners", "logs", `${machine.id}.log`)
68572
68856
  );
68573
68857
  runLog.write(
68574
68858
  "hub",
@@ -68616,7 +68900,7 @@ ${tail}` : head;
68616
68900
  /** Runs `destroy` for a machine the hub has no record of, named only by the handle `status` printed. */
68617
68901
  async destroyHandle(runner, handle, reason) {
68618
68902
  if (!runner.destroy) return false;
68619
- const file2 = join42(bandHome(), "runners", "logs", "reaper.log");
68903
+ const file2 = join43(bandHome(), "runners", "logs", "reaper.log");
68620
68904
  try {
68621
68905
  if (statSync11(file2).size > LOG_FILE_LIMIT) rmSync3(file2, { force: true });
68622
68906
  } catch {
@@ -68838,7 +69122,7 @@ ${tail}` : head;
68838
69122
  // The repo's current environment image (plan step 3.2), empty before its first ready build.
68839
69123
  BAND_REPO_IMAGE: row ? this.repoImage(row.repo) : "",
68840
69124
  BAND_RUNNER_ID: runner.id,
68841
- BAND_RUNNER_DIR: join42(bandHome(), "runners", runner.id),
69125
+ BAND_RUNNER_DIR: join43(bandHome(), "runners", runner.id),
68842
69126
  BAND_NODE: process.execPath
68843
69127
  });
68844
69128
  if (runner.desktop) env.BAND_DESKTOP = "1";
@@ -68864,7 +69148,7 @@ ${tail}` : head;
68864
69148
  */
68865
69149
  runHook(opts) {
68866
69150
  const { runner, name: name5, path: path2, env, timeoutMs, runLog, watch, onStdout, onStderr } = opts;
68867
- const cwd = join42(bandHome(), "runners", runner.id);
69151
+ const cwd = join43(bandHome(), "runners", runner.id);
68868
69152
  mkdirSync7(cwd, { recursive: true, mode: 448 });
68869
69153
  return new Promise((resolve16, reject) => {
68870
69154
  let child;
@@ -68965,7 +69249,7 @@ function idleTimeoutMs() {
68965
69249
  return Number.isFinite(raw) && raw > 0 ? raw : void 0;
68966
69250
  }
68967
69251
  function sleepDir(worktreeId2) {
68968
- return join43(bandHome(), "sleep", worktreeId2);
69252
+ return join44(bandHome(), "sleep", worktreeId2);
68969
69253
  }
68970
69254
  var errorText2 = (err) => err instanceof Error ? err.message : String(err);
68971
69255
  var EphemeralLifecycleService = class {
@@ -69266,7 +69550,7 @@ var EphemeralLifecycleService = class {
69266
69550
  const dir = sleepDir(worktreeId2);
69267
69551
  try {
69268
69552
  await mkdir8(dir, { recursive: true, mode: 448 });
69269
- await writeFile5(join43(dir, "snapshot.bundle"), await host.fs.readFile(bundle), {
69553
+ await writeFile5(join44(dir, "snapshot.bundle"), await host.fs.readFile(bundle), {
69270
69554
  mode: 384
69271
69555
  });
69272
69556
  } catch (err) {
@@ -69290,7 +69574,7 @@ var EphemeralLifecycleService = class {
69290
69574
  try {
69291
69575
  const dir = sleepDir(worktreeId2);
69292
69576
  await mkdir8(dir, { recursive: true, mode: 448 });
69293
- await writeFile5(join43(dir, "sessions.json"), JSON.stringify({ files }), { mode: 384 });
69577
+ await writeFile5(join44(dir, "sessions.json"), JSON.stringify({ files }), { mode: 384 });
69294
69578
  } catch (err) {
69295
69579
  log23.warn(`could not keep the agent sessions of ${worktreeId2}: ${errorText2(err)}`);
69296
69580
  return [];
@@ -69416,7 +69700,7 @@ var EphemeralLifecycleService = class {
69416
69700
  const bundle = posix5.join(wipDir, `${row.worktreeId}.bundle`);
69417
69701
  await host.fs.writeFile(
69418
69702
  bundle,
69419
- await readFile8(join43(sleepDir(row.worktreeId), "snapshot.bundle"))
69703
+ await readFile8(join44(sleepDir(row.worktreeId), "snapshot.bundle"))
69420
69704
  );
69421
69705
  try {
69422
69706
  await git3(["fetch", "--force", bundle, `${local}:${local}`]);
@@ -69473,7 +69757,7 @@ var EphemeralLifecycleService = class {
69473
69757
  if (row.sessionIds.length === 0) return;
69474
69758
  let files;
69475
69759
  try {
69476
- files = JSON.parse(await readFile8(join43(sleepDir(row.worktreeId), "sessions.json"), "utf8")).files;
69760
+ files = JSON.parse(await readFile8(join44(sleepDir(row.worktreeId), "sessions.json"), "utf8")).files;
69477
69761
  } catch (err) {
69478
69762
  log23.warn(`${row.worktreeId}: no saved agent sessions: ${errorText2(err)}`);
69479
69763
  return;
@@ -69509,12 +69793,100 @@ init_worktree_id();
69509
69793
  import { randomBytes as randomBytes12 } from "node:crypto";
69510
69794
  init_worktrees();
69511
69795
 
69796
+ // src/server/services/_utils/project-instructions.ts
69797
+ var INSTRUCTIONS_FILE = "AGENTS.md";
69798
+ var CLAUDE_FILE = "CLAUDE.md";
69799
+ var CLAUDE_TEXT = `@${INSTRUCTIONS_FILE}
69800
+ `;
69801
+ function defaultInstructions(view4) {
69802
+ const p2 = view4.policy;
69803
+ const repos2 = view4.repos.map((r) => `- \`${r.repo}\`${r.role ? ` (role: ${r.role})` : ""}`).join("\n");
69804
+ const limits = [
69805
+ p2.maxConcurrent ? `at most ${p2.maxConcurrent} worker agents run at once` : "no limit on concurrent worker agents",
69806
+ p2.budgetUsd ? `a soft budget of $${p2.budgetUsd} for the project` : "no budget limit",
69807
+ `worker agents run at isolation "${p2.isolationFloor}" or stronger`,
69808
+ p2.labels.length ? `workers go only on hosts labeled ${p2.labels.join(", ")}` : "workers may go on any host"
69809
+ ].join("; ");
69810
+ const autonomy = p2.autonomy === "observe" ? "Autonomy is observe. You may read project state and worker chats. You may not message, stop or dispatch anything. Report what you find and recommend actions to the user." : p2.autoMerge ? "Autonomy is autonomous with auto-merge on. You may dispatch workers within the limits and merge pull requests whose CI passed." : "Autonomy is autonomous. You may dispatch workers within the limits. Merging still needs the user's confirmation.";
69811
+ const folderEdits = p2.autonomy === "autonomous" ? "When the user asks for such an edit, you may commit it, and you may push it because autonomy is autonomous." : `When the user asks for such an edit, commit it but do not push without the user's confirmation, because autonomy is ${p2.autonomy}.`;
69812
+ return `${[
69813
+ `# Coordinator of the Band project "${view4.name}"`,
69814
+ view4.description ? `Project description: ${view4.description}` : "",
69815
+ "You coordinate this project. Plan the work across the project's repos, hand it to worker agents, check on them and keep the user informed. Each worker agent works in one git worktree of one repo, with its own chat. Work that spans repos takes one worker per repo, and you keep them in step.",
69816
+ "This file is yours and the user's to edit. It started from the project's settings at creation. The Band hub enforces the limits below in its tools whatever this file says, so a refused call names the reason and you should tell the user what blocked you instead of retrying.",
69817
+ `## Working directory
69818
+
69819
+ You run in the project folder on the coordinator host. It is the working copy of the project context (notes.md, docs/, learnings/, inbox/, handoffs/) and has a checkout of every project repo's default branch under \`repos/<repo>/\` (a clone of the repo's default branch that tracks \`origin/<default>\`). Read the current code there, or through repo_read, repo_search and repo_log. Band fetches before your turns and fast-forwards a checkout only when it is clean, so a checkout with local changes can be behind origin. Code changes go through worker agents (worktree_create). Do not edit a default branch checkout unless the user explicitly asks you to. ${folderEdits} The folder \`repos/\` is never synced to the context repo. Everything else in the project folder (notes, docs, inbox, handoffs, learnings and any file you or the user add) syncs to the hub and to every worker of the project on its own, within seconds: write a file and the others see it, with no commit or push.`,
69820
+ `## Repos
69821
+
69822
+ ${repos2 || "- none yet"}`,
69823
+ `## Models and limits
69824
+
69825
+ You run on ${p2.models.coordinator}, worker agents on ${p2.models.worker}, reviewers on ${p2.models.reviewer}. Limits: ${limits}.
69826
+
69827
+ ${autonomy}`,
69828
+ `## Context
69829
+
69830
+ The project context repo "${view4.contextName}" is shared by every agent in the project, and the user context holds the user's preferences. Read them before you plan. Layout of the project context: notes.md (running notes), docs/ (design and contracts), media/ (screenshots, recordings), inbox/<agent>.md (pointers to handoffs for an agent), handoffs/ (one file per handoff), learnings/ (what agents learned). Use context_search to find things, context_append_learning to record what future agents should know, and context_handoff to pass work on. Write contracts between repos (API shapes, event formats) to docs/ so the agent on the other side can read them.`,
69831
+ `## Tools
69832
+
69833
+ You act on the project only through the ${COORDINATOR_SERVER} tools: project_status (worktrees, agents, pull requests, spend), worktrees_list, worktree_create, chats_read, chats_send and worktree_stop, plus repo_read {repo, path}, repo_search {repo, query} and repo_log {repo, n} for the default branch checkouts. They are limited to this project's worktrees and repos.`,
69834
+ "## Dispatching\n\nworktree_create starts a worker agent in a new worktree of one repo of the project, on the branch you name, with your brief in .am/BRIEF.md. Write the brief so the worker can act on it alone: the goal, the constraints, the contracts with other repos and what is out of scope, plus acceptance scenarios it can check. For work across repos, start one worker per repo, usually on the same branch name, put the contract between them in docs/ of the project folder first, and decide the order their pull requests merge in.",
69835
+ `## Wake-ups
69836
+
69837
+ Band wakes you with a "Subscription update" message when something needs you: a worker chat ended its turn with an error, is waiting for a permission or an answer, or finished; a pull request of a project worktree has a review comment, a CI result or was merged or closed; or a new file appeared in inbox/ or handoffs/ of the project context. The message names the chat id or the file path. Read the source before you act on it. Events are batched, so one message can carry several. A pull request's subscriptions end when it merges or closes.`,
69838
+ `## Notes and the inbox
69839
+
69840
+ You are the only writer of notes.md in the project context: workers do not edit it, so keep it current with decisions and status. learnings/ is append-only: add entries, never rewrite or delete them. Handoffs from workers land in handoffs/<stamp>-<from>-to-<to>.md and a pointer line in inbox/<to>.md. When you have dealt with an item in inbox/ or handoffs/, move the file into inbox/done/ (a plain move in the project folder, which syncs on its own). A file in inbox/done/ does not wake you again.`,
69841
+ "## Merging\n\nNever merge a pull request unless the user confirmed it in this conversation or auto-merge is on."
69842
+ ].filter(Boolean).join("\n\n")}
69843
+ `;
69844
+ }
69845
+
69846
+ // src/server/services/context-browser-service.ts
69847
+ import { mkdtempSync, rmSync as rmSync6 } from "node:fs";
69848
+ import { tmpdir as tmpdir3 } from "node:os";
69849
+ import { join as join46 } from "node:path";
69850
+
69851
+ // src/server/services/_utils/context-redaction.ts
69852
+ function scanForSecrets(text3) {
69853
+ return scanText("", text3, [], { looseAssignments: true }).map((f2) => ({
69854
+ kind: RULE_LABELS[f2.rule] ?? f2.rule,
69855
+ line: f2.line
69856
+ }));
69857
+ }
69858
+ var PLACEHOLDER = "[redacted]";
69859
+ var EXTRA_PATTERNS = [
69860
+ /\bglpat-[A-Za-z0-9_-]{16,}/g,
69861
+ /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/g,
69862
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
69863
+ // Credentials inside URLs: scheme://user:password@host.
69864
+ /(?<=:\/\/[^\s/:@]+:)[^\s/@]+(?=@)/g
69865
+ ];
69866
+ var ASSIGNMENT2 = /\b([A-Za-z0-9_.-]*(?:password|passwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|credential)[A-Za-z0-9_.-]*)(["']?\s*[:=]\s*["']?)([^\s"',;]{6,})/gi;
69867
+ var globally = (re) => new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
69868
+ function redactSecrets(text3) {
69869
+ let out = text3;
69870
+ for (const [kind, pattern] of SECRET_PATTERNS) {
69871
+ const re = globally(pattern);
69872
+ out = kind === "private key" ? out.replace(
69873
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g,
69874
+ PLACEHOLDER
69875
+ ) : kind === "bearer token" ? out.replace(re, `Bearer ${PLACEHOLDER}`) : out.replace(re, PLACEHOLDER);
69876
+ }
69877
+ for (const pattern of EXTRA_PATTERNS) out = out.replace(pattern, PLACEHOLDER);
69878
+ return out.replace(
69879
+ ASSIGNMENT2,
69880
+ (match, name5, sep13, value) => value.startsWith("[redacted") ? match : `${name5}${sep13}${PLACEHOLDER}`
69881
+ );
69882
+ }
69883
+
69512
69884
  // src/server/services/context-service.ts
69513
69885
  init_src();
69514
69886
  import { spawn as spawn9 } from "node:child_process";
69515
69887
  import { randomBytes as randomBytes11 } from "node:crypto";
69516
69888
  import { mkdirSync as mkdirSync8, rmSync as rmSync5 } from "node:fs";
69517
- import { join as join44 } from "node:path";
69889
+ import { join as join45 } from "node:path";
69518
69890
 
69519
69891
  // src/server/infra/db/queries/contexts.ts
69520
69892
  init_drizzle_orm();
@@ -69581,10 +69953,10 @@ function toView2(row) {
69581
69953
  };
69582
69954
  }
69583
69955
  function contextRoot() {
69584
- return join44(bandHome(), "context");
69956
+ return join45(bandHome(), "context");
69585
69957
  }
69586
69958
  function contextRepoPath(name5) {
69587
- return join44(contextRoot(), `${name5}.git`);
69959
+ return join45(contextRoot(), `${name5}.git`);
69588
69960
  }
69589
69961
  function contextGitEnv(extra = {}) {
69590
69962
  return {
@@ -70084,6 +70456,323 @@ var ContextService = class {
70084
70456
  };
70085
70457
  var contextService = new ContextService();
70086
70458
 
70459
+ // src/server/services/context-browser-service.ts
70460
+ var MAX_FILE_BYTES2 = 1024 * 1024;
70461
+ var MAX_DIFF_BYTES2 = 200 * 1024;
70462
+ var MAX_MESSAGE = 200;
70463
+ var LOG_LIMIT = 200;
70464
+ var CONFLICT = /^(.*?)\.conflict-([A-Za-z0-9]+)((?:\.[^/.]+)?)$/;
70465
+ var ContextConflictError = class extends Error {
70466
+ constructor(message) {
70467
+ super(message);
70468
+ this.name = "ContextConflictError";
70469
+ }
70470
+ };
70471
+ function conflictOriginal(path2) {
70472
+ const m2 = CONFLICT.exec(path2);
70473
+ return m2 ? `${m2[1]}${m2[3]}` : null;
70474
+ }
70475
+ function hasControl(text3, allowWhitespace = false) {
70476
+ for (let i = 0; i < text3.length; i++) {
70477
+ const c = text3.charCodeAt(i);
70478
+ if (allowWhitespace && (c === 9 || c === 10 || c === 13)) continue;
70479
+ if (c < 32 || c === 127) return true;
70480
+ }
70481
+ return false;
70482
+ }
70483
+ function checkPath(path2) {
70484
+ const bad = !path2 || path2.length > 500 || path2.startsWith("/") || path2.endsWith("/") || path2.startsWith(":") || hasControl(path2) || path2.includes("\\") || path2.split("/").some((seg) => seg === "" || seg === "." || seg === ".." || seg === ".git");
70485
+ if (bad) throw new ContextInputError(`"${path2}" is not a valid path inside a context`);
70486
+ return path2;
70487
+ }
70488
+ function checkMessage(message) {
70489
+ const trimmed = message.trim();
70490
+ if (!trimmed) throw new ContextInputError("A commit message is required");
70491
+ if (hasControl(trimmed, true)) {
70492
+ throw new ContextInputError("The commit message holds control characters");
70493
+ }
70494
+ if (scanForSecrets(trimmed).length > 0) {
70495
+ throw new ContextInputError("Not saved: the commit message looks like it holds a credential.");
70496
+ }
70497
+ if (trimmed.length > MAX_MESSAGE) {
70498
+ throw new ContextInputError(`The commit message is longer than ${MAX_MESSAGE} characters`);
70499
+ }
70500
+ return trimmed;
70501
+ }
70502
+ var ContextBrowserService = class {
70503
+ repo(name5) {
70504
+ contextService.require(name5);
70505
+ return contextRepoPath(name5);
70506
+ }
70507
+ /** The commit HEAD points at, or null in a repo with no commits. */
70508
+ async head(repo) {
70509
+ const r = await runGit(["rev-parse", "--verify", "-q", "HEAD^{commit}"], { cwd: repo });
70510
+ return r.code === 0 ? r.stdout.trim() : null;
70511
+ }
70512
+ async tree(name5) {
70513
+ const repo = this.repo(name5);
70514
+ const head = await this.head(repo);
70515
+ if (!head) return { head, entries: [] };
70516
+ const out = await git2(["ls-tree", "-r", "-z", "--long", head], { cwd: repo });
70517
+ const entries = [];
70518
+ for (const rec of out.split("\0")) {
70519
+ if (!rec) continue;
70520
+ const tab = rec.indexOf(" ");
70521
+ const meta = rec.slice(0, tab).split(/\s+/);
70522
+ if (meta[1] !== "blob") continue;
70523
+ const path2 = rec.slice(tab + 1);
70524
+ entries.push({ path: path2, size: Number(meta[3]) || 0, conflictOf: conflictOriginal(path2) });
70525
+ }
70526
+ entries.sort((a, b2) => a.path.localeCompare(b2.path));
70527
+ return { head, entries };
70528
+ }
70529
+ async file(name5, path2) {
70530
+ const repo = this.repo(name5);
70531
+ checkPath(path2);
70532
+ const head = await this.head(repo);
70533
+ if (!head) throw new ContextInputError(`"${path2}" does not exist`);
70534
+ const size = await runGit(["cat-file", "-s", `${head}:${path2}`], { cwd: repo });
70535
+ if (size.code !== 0) throw new ContextInputError(`"${path2}" does not exist`);
70536
+ const bytes = Number(size.stdout.trim());
70537
+ const type = await git2(["cat-file", "-t", `${head}:${path2}`], { cwd: repo });
70538
+ if (type.trim() !== "blob") throw new ContextInputError(`"${path2}" is a folder`);
70539
+ if (bytes > MAX_FILE_BYTES2) {
70540
+ return { path: path2, binary: true, content: null, size: bytes, commit: head };
70541
+ }
70542
+ const content = await git2(["cat-file", "blob", `${head}:${path2}`], { cwd: repo });
70543
+ if (content.includes("\0") || content.includes("\uFFFD")) {
70544
+ return { path: path2, binary: true, content: null, size: bytes, commit: head };
70545
+ }
70546
+ return { path: path2, binary: false, content, size: bytes, commit: head };
70547
+ }
70548
+ async log(name5, path2, limit = 50) {
70549
+ const repo = this.repo(name5);
70550
+ if (path2) checkPath(path2);
70551
+ if (!await this.head(repo)) return [];
70552
+ const args = [
70553
+ "--literal-pathspecs",
70554
+ "log",
70555
+ "--no-color",
70556
+ `-n${Math.min(Math.max(limit, 1), LOG_LIMIT)}`,
70557
+ "--format=%H%x1f%an%x1f%at%x1f%s%x1e"
70558
+ ];
70559
+ if (path2) args.push("--", path2);
70560
+ const out = await git2(args, { cwd: repo });
70561
+ return out.split("").map((rec) => rec.trim()).filter(Boolean).map((rec) => {
70562
+ const [sha = "", author = "", at = "0", subject = ""] = rec.split("");
70563
+ return { sha, author, at: Number(at) * 1e3, subject };
70564
+ });
70565
+ }
70566
+ async diff(name5, sha, path2) {
70567
+ const repo = this.repo(name5);
70568
+ if (!/^[0-9a-f]{40}$/.test(sha)) throw new ContextInputError("Not a commit id");
70569
+ if (path2) checkPath(path2);
70570
+ const exists4 = await runGit(["cat-file", "-e", `${sha}^{commit}`], { cwd: repo });
70571
+ if (exists4.code !== 0) throw new ContextInputError("No such commit");
70572
+ const scope3 = path2 ? ["--", path2] : [];
70573
+ const patch = await git2(
70574
+ [
70575
+ "--literal-pathspecs",
70576
+ "show",
70577
+ "--root",
70578
+ "--format=",
70579
+ "--patch",
70580
+ "--no-color",
70581
+ "-M",
70582
+ "--no-ext-diff",
70583
+ sha,
70584
+ ...scope3
70585
+ ],
70586
+ { cwd: repo }
70587
+ );
70588
+ const names = await git2(
70589
+ [
70590
+ "diff-tree",
70591
+ "--root",
70592
+ "-m",
70593
+ "--first-parent",
70594
+ "--no-commit-id",
70595
+ "--name-status",
70596
+ "-r",
70597
+ "-M",
70598
+ sha
70599
+ ],
70600
+ { cwd: repo }
70601
+ );
70602
+ const files = names.split("\n").filter(Boolean).map((line2) => {
70603
+ const parts = line2.split(" ");
70604
+ return { status: parts[0] ?? "M", path: parts[parts.length - 1] ?? "" };
70605
+ });
70606
+ const patchBytes = Buffer.from(patch);
70607
+ const truncated = patchBytes.length > MAX_DIFF_BYTES2;
70608
+ return {
70609
+ diff: truncated ? patchBytes.subarray(0, MAX_DIFF_BYTES2).toString("utf8") : patch,
70610
+ truncated,
70611
+ files
70612
+ };
70613
+ }
70614
+ /** The newest learnings and handoffs files, one row per file, newest commit first. */
70615
+ async recent(name5, limit = 20) {
70616
+ const repo = this.repo(name5);
70617
+ if (!await this.head(repo)) return [];
70618
+ const out = await git2(
70619
+ [
70620
+ "log",
70621
+ "--no-color",
70622
+ "-n300",
70623
+ "--name-only",
70624
+ "--format=%x1e%H%x1f%at%x1f%s",
70625
+ "--",
70626
+ "learnings",
70627
+ "handoffs"
70628
+ ],
70629
+ { cwd: repo }
70630
+ );
70631
+ const seen = /* @__PURE__ */ new Set();
70632
+ const rows = [];
70633
+ for (const block of out.split("")) {
70634
+ const [header = "", ...files] = block.split("\n").filter((l, i) => i === 0 || l.trim());
70635
+ const [sha = "", at = "0", subject = ""] = header.split("");
70636
+ if (!sha) continue;
70637
+ for (const path2 of files) {
70638
+ const kind = path2.startsWith("learnings/") ? "learnings" : path2.startsWith("handoffs/") ? "handoffs" : null;
70639
+ if (!kind || path2.endsWith(".gitkeep") || seen.has(path2)) continue;
70640
+ seen.add(path2);
70641
+ rows.push({ path: path2, kind, sha, at: Number(at) * 1e3, subject });
70642
+ }
70643
+ }
70644
+ const { entries } = await this.tree(name5);
70645
+ const live = new Set(entries.map((e) => e.path));
70646
+ return rows.filter((r) => live.has(r.path)).slice(0, limit);
70647
+ }
70648
+ /**
70649
+ * Saves `content` at `path` as one commit. `base` is the commit the editor
70650
+ * loaded: when the file changed since, the save is refused so an edit never
70651
+ * overwrites a newer version unseen.
70652
+ */
70653
+ async write(name5, path2, content, message, base) {
70654
+ checkPath(path2);
70655
+ if (Buffer.byteLength(content) > MAX_FILE_BYTES2) {
70656
+ throw new ContextInputError("The file is larger than 1 MiB");
70657
+ }
70658
+ const found = scanForSecrets(content);
70659
+ if (found.length > 0) {
70660
+ const list = found.map((f2) => `${f2.kind} on line ${f2.line}`).join(", ");
70661
+ throw new ContextInputError(
70662
+ `Not saved: the text looks like it holds a credential (${list}). Remove it first.`
70663
+ );
70664
+ }
70665
+ return this.commit(name5, [{ path: path2, content }], checkMessage(message), { base, guard: path2 });
70666
+ }
70667
+ /** Keeps one version of a conflicted file at its original path and deletes the conflict copy. */
70668
+ async resolveConflict(name5, conflictPath, keep, message) {
70669
+ checkPath(conflictPath);
70670
+ const original = conflictOriginal(conflictPath);
70671
+ if (!original) throw new ContextInputError(`"${conflictPath}" is not a conflict copy`);
70672
+ const msg = checkMessage(
70673
+ message ?? `Resolve conflict in ${original}: keep ${keep === "conflict" ? "the conflict copy" : "the original"}`
70674
+ );
70675
+ const repo = this.repo(name5);
70676
+ const result = await contextService.exclusive(name5, async () => {
70677
+ const head = await this.head(repo);
70678
+ if (!head) throw new ContextInputError(`"${conflictPath}" does not exist`);
70679
+ const blob2 = await runGit(["rev-parse", "--verify", "-q", `${head}:${conflictPath}`], {
70680
+ cwd: repo
70681
+ });
70682
+ if (blob2.code !== 0) throw new ContextInputError(`"${conflictPath}" does not exist`);
70683
+ if (keep === "original") {
70684
+ const orig = await runGit(["rev-parse", "--verify", "-q", `${head}:${original}`], {
70685
+ cwd: repo
70686
+ });
70687
+ if (orig.code !== 0) {
70688
+ throw new ContextInputError(
70689
+ `"${original}" does not exist, so "${conflictPath}" is not a conflict copy of anything`
70690
+ );
70691
+ }
70692
+ }
70693
+ const changes = [{ path: conflictPath }];
70694
+ if (keep === "conflict") changes.push({ path: original, blob: blob2.stdout.trim() });
70695
+ return this.commitLocked(repo, name5, head, changes, msg);
70696
+ });
70697
+ return { commit: result.commit, path: original };
70698
+ }
70699
+ commit(name5, changes, message, opts = {}) {
70700
+ const repo = this.repo(name5);
70701
+ return contextService.exclusive(name5, async () => {
70702
+ const head = await this.head(repo);
70703
+ if (head && opts.base && opts.guard && head !== opts.base) {
70704
+ const at = async (rev) => {
70705
+ const r = await runGit(["rev-parse", "--verify", "-q", `${rev}:${opts.guard}`], {
70706
+ cwd: repo
70707
+ });
70708
+ return r.code === 0 ? r.stdout.trim() : "";
70709
+ };
70710
+ if (await at(head) !== await at(opts.base)) {
70711
+ throw new ContextConflictError(
70712
+ `"${opts.guard}" changed since you opened it. Reload it to see the newer version.`
70713
+ );
70714
+ }
70715
+ }
70716
+ return this.commitLocked(repo, name5, head, changes, message, opts.author);
70717
+ });
70718
+ }
70719
+ /** Builds the commit in a private index. The caller holds the context's lock. */
70720
+ async commitLocked(repo, name5, head, changes, message, author) {
70721
+ const dir = mkdtempSync(join46(tmpdir3(), "band-context-index-"));
70722
+ try {
70723
+ const env = contextGitEnv({
70724
+ GIT_INDEX_FILE: join46(dir, "index"),
70725
+ ...author ? {
70726
+ GIT_AUTHOR_NAME: author,
70727
+ GIT_AUTHOR_EMAIL: `${author}@localhost`,
70728
+ GIT_COMMITTER_NAME: author,
70729
+ GIT_COMMITTER_EMAIL: `${author}@localhost`
70730
+ } : {}
70731
+ });
70732
+ const opts = { cwd: repo, env };
70733
+ if (head) await git2(["read-tree", head], opts);
70734
+ for (const change of changes) {
70735
+ if (change.content === void 0 && change.blob === void 0) {
70736
+ await git2(["update-index", "--index-info"], {
70737
+ ...opts,
70738
+ input: `0 ${"0".repeat(40)} ${change.path}
70739
+ `
70740
+ });
70741
+ continue;
70742
+ }
70743
+ const oid = change.blob ?? (await git2(["hash-object", "-w", "--stdin"], { ...opts, input: change.content })).trim();
70744
+ const r = await runGit(
70745
+ ["update-index", "--add", "--cacheinfo", `100644,${oid},${change.path}`],
70746
+ opts
70747
+ );
70748
+ if (r.code !== 0) {
70749
+ throw new ContextInputError(
70750
+ `Cannot save "${change.path}": ${r.stderr.trim().slice(0, 200)}`
70751
+ );
70752
+ }
70753
+ }
70754
+ const tree = (await git2(["write-tree"], opts)).trim();
70755
+ if (head) {
70756
+ const current3 = (await git2(["rev-parse", `${head}^{tree}`], opts)).trim();
70757
+ if (current3 === tree) return { commit: head, changed: false };
70758
+ }
70759
+ const commit = (await git2(["commit-tree", tree, ...head ? ["-p", head] : [], "-m", message], opts)).trim();
70760
+ const ref2 = (await git2(["symbolic-ref", "-q", "HEAD"], { cwd: repo })).trim();
70761
+ const moved = await runGit(["update-ref", "-m", "context browser", ref2, commit, head ?? ""], {
70762
+ cwd: repo
70763
+ });
70764
+ if (moved.code !== 0) {
70765
+ throw new ContextConflictError("The context changed while saving. Reload and try again.");
70766
+ }
70767
+ contextService.syncSoon(name5);
70768
+ return { commit, changed: true };
70769
+ } finally {
70770
+ rmSync6(dir, { recursive: true, force: true });
70771
+ }
70772
+ }
70773
+ };
70774
+ var contextBrowserService = new ContextBrowserService();
70775
+
70087
70776
  // src/server/services/project-service.ts
70088
70777
  init_state();
70089
70778
  var log25 = createLogger("project-service");
@@ -70230,8 +70919,41 @@ var ProjectService = class {
70230
70919
  throw err;
70231
70920
  }
70232
70921
  log25.info(`created project ${name5} with ${repos2.length} repos`);
70922
+ await this.seedInstructions(row.id);
70233
70923
  return this.get(row.id);
70234
70924
  }
70925
+ /**
70926
+ * Gives the project's context repo a default `AGENTS.md` and a `CLAUDE.md` that imports it, when
70927
+ * it has none. The files then belong to the user and are never rewritten. Never throws, so a
70928
+ * failure leaves a project without them and the coordinator without instructions.
70929
+ */
70930
+ async seedInstructions(ref2) {
70931
+ try {
70932
+ const view4 = this.get(ref2);
70933
+ const have = new Set(
70934
+ (await contextBrowserService.tree(view4.contextName)).entries.map((e) => e.path)
70935
+ );
70936
+ const files = [
70937
+ [
70938
+ INSTRUCTIONS_FILE,
70939
+ defaultInstructions({
70940
+ name: view4.name,
70941
+ description: view4.description,
70942
+ contextName: view4.contextName,
70943
+ repos: view4.repos,
70944
+ policy: view4.effectivePolicy
70945
+ })
70946
+ ],
70947
+ [CLAUDE_FILE, CLAUDE_TEXT]
70948
+ ];
70949
+ for (const [path2, content] of files) {
70950
+ if (have.has(path2)) continue;
70951
+ await contextBrowserService.write(view4.contextName, path2, content, `Add ${path2}`);
70952
+ }
70953
+ } catch (err) {
70954
+ log25.warn({ project: ref2, err }, "could not add the default AGENTS.md to the project context");
70955
+ }
70956
+ }
70235
70957
  update(ref2, patch) {
70236
70958
  const row = this.require(ref2);
70237
70959
  const set2 = {};
@@ -70468,59 +71190,49 @@ var projectService = new ProjectService();
70468
71190
 
70469
71191
  // src/server/services/project-folder-service.ts
70470
71192
  init_state();
71193
+ var LOCAL_HOST_ID4 = "local";
71194
+ var WaitingForWorkerError = class extends Error {
71195
+ constructor(agent) {
71196
+ super(`Waiting for a worker that can run ${agent}`);
71197
+ this.name = "WaitingForWorkerError";
71198
+ }
71199
+ };
70471
71200
  var log26 = createLogger("project-folder");
70472
- var INSTRUCTIONS_FILE = "AGENTS.md";
70473
- var CLAUDE_FILE = "CLAUDE.md";
70474
- var CLAUDE_TEXT = `<!-- Written by Band. Claude Code reads this file; it imports the project's instructions. -->
70475
- @${INSTRUCTIONS_FILE}
70476
- `;
70477
71201
  var ProjectFolderService = class {
70478
71202
  states = /* @__PURE__ */ new Map();
70479
71203
  inflight = /* @__PURE__ */ new Map();
70480
- /** The coordinator's charter of a project. Set by `ProjectCoordinatorService`, which imports this file. */
70481
- instructionsOf = null;
70482
- setInstructions(fn) {
70483
- this.instructionsOf = fn;
70484
- }
70485
- /**
70486
- * Writes the project's agent instructions into its folder: `AGENTS.md` holds the coordinator's
70487
- * charter (Codex, OpenCode and most agents read it) and `CLAUDE.md` imports it for Claude Code. So
70488
- * any agent started in the folder, in a chat or a terminal, works from the same text. Band
70489
- * rewrites both whenever the folder is prepared or the project changes, and the context sync
70490
- * leaves them out, so each host keeps its own copy and an edit never spreads. A file is only
70491
- * written when its text changed. Never throws.
70492
- */
70493
- async writeInstructions(row, folder) {
70494
- const charter = this.instructionsOf?.(row);
70495
- const dir = folder ?? this.states.get(row.id)?.folder;
70496
- if (!charter || !dir) return;
70497
- const host = this.hostOf(row);
70498
- const body = charter.replace(/(?<![`\w])([\w./-]*<[\w-]+>[\w./<>-]*)/g, "`$1`");
70499
- const agents = `<!-- Written by Band from the project's settings, repos and policy. Edits are overwritten: change the project in Settings > Projects instead. -->
70500
-
70501
- ${body}
70502
- `;
70503
- for (const [name5, text3] of [
70504
- [INSTRUCTIONS_FILE, agents],
70505
- [CLAUDE_FILE, CLAUDE_TEXT]
70506
- ]) {
70507
- const path2 = `${dir.replace(/\/+$/, "")}/${name5}`;
70508
- try {
70509
- const current3 = await host.fs.readFile(path2).then((b2) => new TextDecoder().decode(b2)).catch(() => null);
70510
- if (current3 !== text3) await host.fs.writeFile(path2, text3);
70511
- } catch (err) {
70512
- log26.warn(
70513
- { project: row.name, file: name5, err },
70514
- "could not write the project's agent instructions"
70515
- );
70516
- }
70517
- }
70518
- }
70519
71204
  /** `project:host` folders a view prepared without checkouts, so the next view asks nothing. */
70520
71205
  prepared = /* @__PURE__ */ new Set();
70521
- /** The host the project's folder is on. */
71206
+ /**
71207
+ * The id of the host the project's folder is on, or null while no host can take it. A hub with
71208
+ * `BAND_LOCAL_HOST=off` never uses its own machine: the coordinator goes on the first online
71209
+ * worker that reports the coordinator's agent installed and logged in, and that choice is saved.
71210
+ * With no such worker the project waits, and nothing is placed.
71211
+ */
71212
+ hostIdOf(row) {
71213
+ const localOn = isLocalHostEnabled();
71214
+ const chosen = row.coordinatorHostId;
71215
+ if (chosen && (localOn || chosen !== LOCAL_HOST_ID4)) return chosen;
71216
+ if (localOn) return LOCAL_HOST_ID4;
71217
+ const agent = this.coordinatorAgentType(row);
71218
+ const eligible = tokenService.listHosts().find(
71219
+ (h) => h.id !== LOCAL_HOST_ID4 && h.status === "online" && agentIsUsable(h.report?.agents.find((a) => a.type === agent))
71220
+ );
71221
+ if (!eligible) return null;
71222
+ projectService.update(row.id, { coordinatorHostId: eligible.id });
71223
+ log26.info({ project: row.name, host: eligible.id, agent }, "placed the coordinator on a worker");
71224
+ return eligible.id;
71225
+ }
71226
+ /** The agent type the project's coordinator runs. */
71227
+ coordinatorAgentType(row) {
71228
+ const id = row.coordinatorAgent ?? "claude-code";
71229
+ return loadSettings().codingAgents?.find((a) => a.id === id)?.type ?? id;
71230
+ }
71231
+ /** The host the project's folder is on. Throws while the project waits for a worker. */
70522
71232
  hostOf(row) {
70523
- return hostRegistry.hostById(row.coordinatorHostId ?? hostRegistry.local.id);
71233
+ const id = this.hostIdOf(row);
71234
+ if (!id) throw new WaitingForWorkerError(this.coordinatorAgentType(row));
71235
+ return hostRegistry.hostById(id);
70524
71236
  }
70525
71237
  /**
70526
71238
  * The project's git repos with their clone on `host`, and the repos that have none. A repo with a
@@ -70591,7 +71303,6 @@ ${body}
70591
71303
  skipped
70592
71304
  };
70593
71305
  this.states.set(row.id, state2);
70594
- await this.writeInstructions(row, state2.folder);
70595
71306
  return state2;
70596
71307
  })().finally(() => {
70597
71308
  if (this.inflight.get(row.id) === work) this.inflight.delete(row.id);
@@ -72721,7 +73432,7 @@ exit`;
72721
73432
  var cronjobService = new CronjobService();
72722
73433
 
72723
73434
  // src/server/services/local-host-policy.ts
72724
- var LOCAL_HOST_ID4 = "local";
73435
+ var LOCAL_HOST_ID5 = "local";
72725
73436
  var LocalHostDisabledError = class extends Error {
72726
73437
  constructor() {
72727
73438
  super(
@@ -72740,20 +73451,20 @@ var NoDefaultHostError = class extends Error {
72740
73451
  };
72741
73452
  function resolveWorktreeHostId(requested) {
72742
73453
  if (!isLocalHostEnabled()) {
72743
- if (requested === LOCAL_HOST_ID4) throw new LocalHostDisabledError();
73454
+ if (requested === LOCAL_HOST_ID5) throw new LocalHostDisabledError();
72744
73455
  if (requested) return requested;
72745
73456
  const configured = process.env.BAND_DEFAULT_HOST?.trim();
72746
73457
  if (configured) {
72747
- if (configured === LOCAL_HOST_ID4) throw new LocalHostDisabledError();
73458
+ if (configured === LOCAL_HOST_ID5) throw new LocalHostDisabledError();
72748
73459
  return configured;
72749
73460
  }
72750
- const online = tokenService.listHosts().filter((h) => h.id !== LOCAL_HOST_ID4 && h.status === "online");
73461
+ const online = tokenService.listHosts().filter((h) => h.id !== LOCAL_HOST_ID5 && h.status === "online");
72751
73462
  if (online.length === 1) return online[0].id;
72752
73463
  throw new NoDefaultHostError(
72753
73464
  online.length === 0 ? "No worker is online." : "More than one worker is online."
72754
73465
  );
72755
73466
  }
72756
- return requested ?? LOCAL_HOST_ID4;
73467
+ return requested ?? LOCAL_HOST_ID5;
72757
73468
  }
72758
73469
 
72759
73470
  // src/server/services/panel-focus-service.ts
@@ -72847,7 +73558,7 @@ init_settings_service();
72847
73558
  init_state();
72848
73559
 
72849
73560
  // src/server/services/sync-service.ts
72850
- import { join as join45, relative as relative2, sep as sep6 } from "node:path";
73561
+ import { join as join47, relative as relative2, sep as sep6 } from "node:path";
72851
73562
 
72852
73563
  // src/server/services/_utils/remote-worktrees.ts
72853
73564
  async function refreshRemoteWorktrees(repoName2, repoPath, worktrees2) {
@@ -72955,7 +73666,7 @@ function isProjectCheckout(path2, roots) {
72955
73666
  });
72956
73667
  }
72957
73668
  async function projectFolderRoots() {
72958
- const root = join45(bandHome(), "projects");
73669
+ const root = join47(bandHome(), "projects");
72959
73670
  let real3 = root;
72960
73671
  try {
72961
73672
  real3 = await hostRegistry.hostById("local").fs.realpath(root);
@@ -73375,7 +74086,7 @@ var WorktreeService = class {
73375
74086
  const project = projectService.find(projectId);
73376
74087
  if (!project) return null;
73377
74088
  const host = projectFolderService.hostOf(project);
73378
- const folder = projectFolderService.state(project.id)?.folder ?? (host.id === hostRegistry.local.id ? join46(bandHome(), "projects", project.name) : void 0);
74089
+ const folder = projectFolderService.state(project.id)?.folder ?? (host.id === hostRegistry.local.id ? join48(bandHome(), "projects", project.name) : void 0);
73379
74090
  if (!folder) return null;
73380
74091
  const worktree = {
73381
74092
  name: project.name,
@@ -73539,8 +74250,8 @@ var WorktreeService = class {
73539
74250
  const remote = hostId2 !== hostRegistry.local.id;
73540
74251
  const repoPath = await this.checkoutOn(repo, host, input.hostRepoPath);
73541
74252
  const wtDir = remote ? await this.remoteWorktreesDir(host) : worktreesDir();
73542
- const worktreePath = remote ? posix6.join(wtDir, input.repo, input.branch) : join46(wtDir, input.repo, input.branch);
73543
- await host.fs.mkdir(remote ? posix6.join(wtDir, input.repo) : join46(wtDir, input.repo), {
74253
+ const worktreePath = remote ? posix6.join(wtDir, input.repo, input.branch) : join48(wtDir, input.repo, input.branch);
74254
+ await host.fs.mkdir(remote ? posix6.join(wtDir, input.repo) : join48(wtDir, input.repo), {
73544
74255
  recursive: true
73545
74256
  });
73546
74257
  let base = input.base;
@@ -73798,7 +74509,7 @@ var WorktreeService = class {
73798
74509
  saveState(state2);
73799
74510
  removal.commit();
73800
74511
  try {
73801
- unlinkSync4(join46(bandHome(), "workspace-prompts", `${worktreeId2}.json`));
74512
+ unlinkSync4(join48(bandHome(), "workspace-prompts", `${worktreeId2}.json`));
73802
74513
  } catch {
73803
74514
  }
73804
74515
  deleteWorktreeStatus(worktreeId2);
@@ -74182,7 +74893,7 @@ var WorktreeService = class {
74182
74893
  if (!/^[a-zA-Z0-9._-]+$/.test(input.scriptType) || input.scriptType.includes("..")) {
74183
74894
  throw new Error(`Invalid script type "${input.scriptType}"`);
74184
74895
  }
74185
- const scriptPath = join46(input.path, ".band", input.scriptType);
74896
+ const scriptPath = join48(input.path, ".band", input.scriptType);
74186
74897
  const host = hostRegistry.local;
74187
74898
  try {
74188
74899
  await host.fs.stat(scriptPath);
@@ -75309,11 +76020,11 @@ function publicHubUrl() {
75309
76020
  }
75310
76021
  }
75311
76022
  function ghApi(args) {
75312
- return withHubGhCredential((env) => execGh(["api", ...args], tmpdir3(), env));
76023
+ return withHubGhCredential((env) => execGh(["api", ...args], tmpdir4(), env));
75313
76024
  }
75314
76025
  function ghApiJson(endpoint, body) {
75315
76026
  return withHubGhCredential(
75316
- (env) => execGhWithInput(["api", endpoint, "--input", "-"], tmpdir3(), JSON.stringify(body), env)
76027
+ (env) => execGhWithInput(["api", endpoint, "--input", "-"], tmpdir4(), JSON.stringify(body), env)
75317
76028
  );
75318
76029
  }
75319
76030
  var seenDeliveries = /* @__PURE__ */ new Set();
@@ -75384,11 +76095,11 @@ var GithubWebhookService = class {
75384
76095
  if (running) {
75385
76096
  await running;
75386
76097
  }
75387
- const run4 = this.fetchAndIngest(trigger).finally(() => {
75388
- if (checking.get(flight) === run4) checking.delete(flight);
76098
+ const run5 = this.fetchAndIngest(trigger).finally(() => {
76099
+ if (checking.get(flight) === run5) checking.delete(flight);
75389
76100
  });
75390
- checking.set(flight, run4);
75391
- await run4;
76101
+ checking.set(flight, run5);
76102
+ await run5;
75392
76103
  }
75393
76104
  async fetchAndIngest(trigger) {
75394
76105
  const checks = [];
@@ -75428,11 +76139,11 @@ var GithubWebhookService = class {
75428
76139
  const repo = sub.config.repo;
75429
76140
  if (sub.source !== "github" || !repo) return;
75430
76141
  const previous = registering.get(repo) ?? Promise.resolve();
75431
- const run4 = previous.then(() => this.register(sub.id, repo)).finally(() => {
75432
- if (registering.get(repo) === run4) registering.delete(repo);
76142
+ const run5 = previous.then(() => this.register(sub.id, repo)).finally(() => {
76143
+ if (registering.get(repo) === run5) registering.delete(repo);
75433
76144
  });
75434
- registering.set(repo, run4);
75435
- await run4;
76145
+ registering.set(repo, run5);
76146
+ await run5;
75436
76147
  }
75437
76148
  async register(subscriptionId, repo) {
75438
76149
  const set2 = (status, error40) => {
@@ -75990,7 +76701,7 @@ async function machineHasLogin() {
75990
76701
  if (loginCheck && now - loginCheck.at < LOGIN_CHECK_TTL_MS) return loginCheck.loggedIn;
75991
76702
  let loggedIn = false;
75992
76703
  try {
75993
- await execGh(["auth", "status", "--hostname", "github.com"], tmpdir4());
76704
+ await execGh(["auth", "status", "--hostname", "github.com"], tmpdir5());
75994
76705
  loggedIn = true;
75995
76706
  } catch {
75996
76707
  loggedIn = false;
@@ -76006,10 +76717,10 @@ async function hubGhEnv() {
76006
76717
  async function hubGhHasNoCredential() {
76007
76718
  return !await machineHasLogin() && vaultService.findGitHubToken() === void 0;
76008
76719
  }
76009
- async function withHubGhCredential(run4) {
76720
+ async function withHubGhCredential(run5) {
76010
76721
  const env = await hubGhEnv();
76011
76722
  try {
76012
- return await run4(env);
76723
+ return await run5(env);
76013
76724
  } catch (err) {
76014
76725
  const message = err instanceof Error ? err.message : String(err);
76015
76726
  if (env.GH_TOKEN === void 0 && UNAUTHENTICATED.test(message) && await hubGhHasNoCredential()) {
@@ -76172,11 +76883,11 @@ function parseBranchChecks(data) {
76172
76883
  if (!suite.workflowRun) continue;
76173
76884
  const workflowName = suite.workflowRun.workflow.name;
76174
76885
  for (const node of suite.checkRuns.nodes) {
76175
- const run4 = fromCheckRun(node, workflowName);
76176
- const key2 = `${workflowName}\0${run4.name}`;
76886
+ const run5 = fromCheckRun(node, workflowName);
76887
+ const key2 = `${workflowName}\0${run5.name}`;
76177
76888
  const existing = latest.get(key2);
76178
- if (!existing || (run4.startedAt ?? "") > (existing.startedAt ?? "")) {
76179
- latest.set(key2, run4);
76889
+ if (!existing || (run5.startedAt ?? "") > (existing.startedAt ?? "")) {
76890
+ latest.set(key2, run5);
76180
76891
  }
76181
76892
  }
76182
76893
  }
@@ -76588,7 +77299,7 @@ var GithubPollService = class {
76588
77299
  /** One `gh` call, counted against the poll's budget. */
76589
77300
  gh(args) {
76590
77301
  this.state.callsLeft--;
76591
- return withHubGhCredential((env) => execGh(args, tmpdir5(), env));
77302
+ return withHubGhCredential((env) => execGh(args, tmpdir6(), env));
76592
77303
  }
76593
77304
  /** Logs a failure once per distinct message, and skips the item for longer each time it repeats. */
76594
77305
  fail(item, poll, err) {
@@ -77120,7 +77831,7 @@ var WatcherService = class {
77120
77831
  this.poller.start();
77121
77832
  const statuses = loadCurrentStatuses();
77122
77833
  const runningScripts = worktreeScriptService.getRunning();
77123
- const runningSetups = [...new Set(runningScripts.map((run4) => run4.worktreeId))];
77834
+ const runningSetups = [...new Set(runningScripts.map((run5) => run5.worktreeId))];
77124
77835
  listener({ kind: "snapshot", statuses, runningSetups });
77125
77836
  for (const event of this.loadCurrentBranchStatuses()) {
77126
77837
  listener(event);
@@ -77250,47 +77961,14 @@ var agentSessionRegistry = new AgentSessionRegistryService();
77250
77961
  // src/server/services/context-capture-service.ts
77251
77962
  init_src();
77252
77963
 
77253
- // src/server/services/_utils/context-redaction.ts
77254
- function scanForSecrets(text3) {
77255
- return scanText("", text3, [], { looseAssignments: true }).map((f2) => ({
77256
- kind: RULE_LABELS[f2.rule] ?? f2.rule,
77257
- line: f2.line
77258
- }));
77259
- }
77260
- var PLACEHOLDER = "[redacted]";
77261
- var EXTRA_PATTERNS = [
77262
- /\bglpat-[A-Za-z0-9_-]{16,}/g,
77263
- /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/g,
77264
- /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
77265
- // Credentials inside URLs: scheme://user:password@host.
77266
- /(?<=:\/\/[^\s/:@]+:)[^\s/@]+(?=@)/g
77267
- ];
77268
- var ASSIGNMENT2 = /\b([A-Za-z0-9_.-]*(?:password|passwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|credential)[A-Za-z0-9_.-]*)(["']?\s*[:=]\s*["']?)([^\s"',;]{6,})/gi;
77269
- var globally = (re) => new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
77270
- function redactSecrets(text3) {
77271
- let out = text3;
77272
- for (const [kind, pattern] of SECRET_PATTERNS) {
77273
- const re = globally(pattern);
77274
- out = kind === "private key" ? out.replace(
77275
- /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g,
77276
- PLACEHOLDER
77277
- ) : kind === "bearer token" ? out.replace(re, `Bearer ${PLACEHOLDER}`) : out.replace(re, PLACEHOLDER);
77278
- }
77279
- for (const pattern of EXTRA_PATTERNS) out = out.replace(pattern, PLACEHOLDER);
77280
- return out.replace(
77281
- ASSIGNMENT2,
77282
- (match, name5, sep13, value) => value.startsWith("[redacted") ? match : `${name5}${sep13}${PLACEHOLDER}`
77283
- );
77284
- }
77285
-
77286
77964
  // src/server/services/context-tools-service.ts
77287
77965
  init_worktree_id();
77288
77966
  import { randomBytes as randomBytes14 } from "node:crypto";
77289
77967
 
77290
77968
  // src/server/services/context-store.ts
77291
- import { mkdtempSync, rmSync as rmSync6 } from "node:fs";
77292
- import { tmpdir as tmpdir6 } from "node:os";
77293
- import { join as join47 } from "node:path";
77969
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync7 } from "node:fs";
77970
+ import { tmpdir as tmpdir7 } from "node:os";
77971
+ import { join as join49 } from "node:path";
77294
77972
  var MAX_TERMS = 8;
77295
77973
  var MAX_FILES_SCANNED = 5e3;
77296
77974
  var SNIPPETS_PER_FILE = 3;
@@ -77349,11 +78027,11 @@ async function commitAppends(name5, edits, message) {
77349
78027
  const sha = await contextService.withLock(name5, async () => {
77350
78028
  let lastError = "";
77351
78029
  for (let attempt = 0; attempt < WRITE_ATTEMPTS; attempt++) {
77352
- const dir = mkdtempSync(join47(tmpdir6(), "band-context-"));
78030
+ const dir = mkdtempSync2(join49(tmpdir7(), "band-context-"));
77353
78031
  try {
77354
78032
  const headRef = (await git2(["symbolic-ref", "-q", "HEAD"], { cwd: repo })).trim();
77355
78033
  const parent = await headCommit(repo);
77356
- const env = contextGitEnv({ GIT_INDEX_FILE: join47(dir, "index") });
78034
+ const env = contextGitEnv({ GIT_INDEX_FILE: join49(dir, "index") });
77357
78035
  if (parent) await git2(["read-tree", parent], { cwd: repo, env });
77358
78036
  for (const edit of edits) {
77359
78037
  let current3 = "";
@@ -77375,7 +78053,7 @@ async function commitAppends(name5, edits, message) {
77375
78053
  if (update.code === 0) return commit;
77376
78054
  lastError = update.stderr.trim();
77377
78055
  } finally {
77378
- rmSync6(dir, { recursive: true, force: true });
78056
+ rmSync7(dir, { recursive: true, force: true });
77379
78057
  }
77380
78058
  }
77381
78059
  throw new Error(`The context moved while writing: ${lastError.slice(0, 200)}`);
@@ -78163,7 +78841,7 @@ init_src();
78163
78841
  // src/server/services/context-sync-service.ts
78164
78842
  init_src();
78165
78843
  var log47 = createLogger("context-sync");
78166
- var LOCAL_HOST_ID5 = "local";
78844
+ var LOCAL_HOST_ID6 = "local";
78167
78845
  var DEFAULT_PULL_TIMEOUT_MS2 = 1e4;
78168
78846
  function pullTimeoutMs() {
78169
78847
  return Number(process.env.BAND_CONTEXT_PULL_TIMEOUT_MS) || DEFAULT_PULL_TIMEOUT_MS2;
@@ -78179,8 +78857,10 @@ var ContextSyncService = class {
78179
78857
  const project = projectId ? projectService.find(projectId) : void 0;
78180
78858
  const worktree = projectId ? void 0 : worktreeService.resolve(scopeId);
78181
78859
  if (projectId ? !project : !worktree) return null;
78182
- const host = project ? hostRegistry.hostById(project.coordinatorHostId ?? LOCAL_HOST_ID5) : worktree.host;
78183
- const labels3 = host.id === LOCAL_HOST_ID5 ? (await host.info().catch(() => null))?.labels ?? [] : tokenService.hostLabels(host.id) ?? [];
78860
+ const projectHostId = project ? projectFolderService.hostIdOf(project) : null;
78861
+ if (project && !projectHostId) return null;
78862
+ const host = project ? hostRegistry.hostById(projectHostId ?? LOCAL_HOST_ID6) : worktree.host;
78863
+ const labels3 = host.id === LOCAL_HOST_ID6 ? (await host.info().catch(() => null))?.labels ?? [] : tokenService.hostLabels(host.id) ?? [];
78184
78864
  const rows = project ? contextService.forSession("", labels3, contextService.find(project.contextName)) : contextService.forSession(
78185
78865
  worktree.repo.name,
78186
78866
  labels3,
@@ -78228,7 +78908,7 @@ var ContextSyncService = class {
78228
78908
  const { readOnly } = target;
78229
78909
  const hostId2 = target.host.id;
78230
78910
  const writable = target.specs.filter(
78231
- (s2) => target.host.id !== LOCAL_HOST_ID5 || !readOnly.has(s2.name)
78911
+ (s2) => target.host.id !== LOCAL_HOST_ID6 || !readOnly.has(s2.name)
78232
78912
  );
78233
78913
  if (writable.length === 0) return [];
78234
78914
  const results = await target.host.context.push({
@@ -79152,6 +79832,7 @@ var mcpProxyService = new McpProxyService();
79152
79832
  // src/server/services/project-coordinator-service.ts
79153
79833
  init_src();
79154
79834
  init_worktree_id();
79835
+ init_status_event_bus();
79155
79836
 
79156
79837
  // src/server/services/project-subscription-service.ts
79157
79838
  init_src();
@@ -79345,11 +80026,11 @@ var ProjectSubscriptionService = class {
79345
80026
  /** Records the member's PR and subscribes or unsubscribes the coordinator for it. */
79346
80027
  syncMemberPr(worktreeId2, pr) {
79347
80028
  const previous = this.memberLanes.get(worktreeId2) ?? Promise.resolve();
79348
- const run4 = previous.catch(() => void 0).then(() => this.syncMemberPrNow(worktreeId2, pr)).finally(() => {
79349
- if (this.memberLanes.get(worktreeId2) === run4) this.memberLanes.delete(worktreeId2);
80029
+ const run5 = previous.catch(() => void 0).then(() => this.syncMemberPrNow(worktreeId2, pr)).finally(() => {
80030
+ if (this.memberLanes.get(worktreeId2) === run5) this.memberLanes.delete(worktreeId2);
79350
80031
  });
79351
- this.memberLanes.set(worktreeId2, run4);
79352
- return run4;
80032
+ this.memberLanes.set(worktreeId2, run5);
80033
+ return run5;
79353
80034
  }
79354
80035
  async syncMemberPrNow(worktreeId2, pr) {
79355
80036
  const row = projectService.projectOfWorker(worktreeId2);
@@ -79419,10 +80100,10 @@ var ProjectSubscriptionService = class {
79419
80100
  const row = projectService.findByContext(name5);
79420
80101
  if (!row?.coordinatorChatId) return;
79421
80102
  const previous = this.contextLanes.get(name5) ?? Promise.resolve();
79422
- const run4 = previous.catch(() => void 0).then(() => this.scanContext(row, name5)).catch((err) => log51.warn({ err, context: name5 }, "could not scan the context for new files")).finally(() => {
79423
- if (this.contextLanes.get(name5) === run4) this.contextLanes.delete(name5);
80103
+ const run5 = previous.catch(() => void 0).then(() => this.scanContext(row, name5)).catch((err) => log51.warn({ err, context: name5 }, "could not scan the context for new files")).finally(() => {
80104
+ if (this.contextLanes.get(name5) === run5) this.contextLanes.delete(name5);
79424
80105
  });
79425
- this.contextLanes.set(name5, run4);
80106
+ this.contextLanes.set(name5, run5);
79426
80107
  }
79427
80108
  async scanContext(row, name5) {
79428
80109
  const head = await this.headOf(name5);
@@ -79467,7 +80148,7 @@ var projectSubscriptionService = new ProjectSubscriptionService();
79467
80148
  // src/server/services/project-coordinator-service.ts
79468
80149
  var log52 = createLogger("project-coordinator");
79469
80150
  var DEFAULT_COORDINATOR_AGENT = "claude-code";
79470
- var MAX_MESSAGE = 2e4;
80151
+ var MAX_MESSAGE2 = 2e4;
79471
80152
  var MAX_READ_TURNS = 20;
79472
80153
  var MAX_TRANSCRIPT_CHARS = 3e4;
79473
80154
  var CoordinatorToolError = class extends Error {
@@ -79484,7 +80165,7 @@ var ProjectCoordinatorService = class {
79484
80165
  // ---- session start -----------------------------------------------------------------
79485
80166
  /**
79486
80167
  * Creates the coordinator's chat when the project has none, and starts the agent so it
79487
- * receives the charter. Needs at least one repo. The agent starts in the background (it
80168
+ * starts with it. Needs at least one repo. The agent starts in the background (it
79488
80169
  * prepares the project folder first), so a slow or failing agent never fails the caller.
79489
80170
  * Safe to call again: a running start is shared.
79490
80171
  */
@@ -79505,14 +80186,47 @@ var ProjectCoordinatorService = class {
79505
80186
  * the new host when `coordinatorHostId` changed.
79506
80187
  */
79507
80188
  async update(ref2, patch) {
79508
- const before = projectFolderService.hostOf(projectService.row(ref2)).id;
80189
+ const row = projectService.row(ref2);
80190
+ const before = projectFolderService.hostIdOf(row);
80191
+ const next = patch.coordinatorHostId?.trim() || null;
80192
+ if (before && next && next !== before) await this.assertMovable(row, before);
79509
80193
  const updated = projectService.update(ref2, patch);
79510
80194
  this.syncModel(updated.id);
79511
- if (projectFolderService.hostOf(projectService.row(updated.id)).id !== before) {
80195
+ if (projectFolderService.hostIdOf(projectService.row(updated.id)) !== before) {
79512
80196
  await this.moveHost(updated.id);
79513
80197
  }
79514
80198
  return projectService.get(updated.id);
79515
80199
  }
80200
+ /**
80201
+ * Refuses a move while a default-branch checkout on the old host has changes or commits that
80202
+ * exist nowhere else. A host that cannot be reached (offline, or no checkout there) cannot be
80203
+ * checked, so it does not block the move.
80204
+ */
80205
+ async assertMovable(row, hostId2) {
80206
+ const host = hostRegistry.hostById(hostId2);
80207
+ const blockers = [];
80208
+ for (const { repo } of projectService.get(row.id).repos) {
80209
+ try {
80210
+ const status = await host.project.status({ project: row.name, repo });
80211
+ const reasons = [];
80212
+ if (status.dirty) reasons.push("uncommitted changes");
80213
+ if (status.ahead > 0) {
80214
+ reasons.push(`${status.ahead} unpushed commit${status.ahead === 1 ? "" : "s"}`);
80215
+ }
80216
+ if (reasons.length > 0) blockers.push(`${repo} has ${reasons.join(" and ")}`);
80217
+ } catch (err) {
80218
+ log52.info(
80219
+ { project: row.name, repo, hostId: hostId2, err },
80220
+ "could not check a checkout before a move"
80221
+ );
80222
+ }
80223
+ }
80224
+ if (blockers.length > 0) {
80225
+ throw new ProjectConflictError(
80226
+ `Cannot move the coordinator off host "${hostId2}": ${blockers.join("; ")}. Commit and push or discard them first.`
80227
+ );
80228
+ }
80229
+ }
79516
80230
  /**
79517
80231
  * Moves the project's folder view to its current coordinator host after `coordinatorHostId`
79518
80232
  * changed. Every chat of the view stops its agent process on the old host and starts a fresh
@@ -79532,6 +80246,26 @@ var ProjectCoordinatorService = class {
79532
80246
  lastError(projectId) {
79533
80247
  return this.startErrors.get(projectId) ?? null;
79534
80248
  }
80249
+ /** Whether the project has no coordinator host yet, because no worker can run the agent. */
80250
+ isWaiting(projectId) {
80251
+ return this.waiting.has(projectId) && !this.projectById(projectId)?.coordinatorHostId;
80252
+ }
80253
+ waiting = /* @__PURE__ */ new Set();
80254
+ stopListening = null;
80255
+ /**
80256
+ * Starts the coordinators that wait for a worker when a worker comes online or reports new
80257
+ * capabilities. Called once the server is up.
80258
+ */
80259
+ start() {
80260
+ this.stopListening ??= subscribe((event) => {
80261
+ if (event.kind !== "host-status-changed" || event.hostStatus !== "online") return;
80262
+ for (const id of [...this.waiting]) {
80263
+ void this.ensureCoordinator(id).catch((err) => {
80264
+ log52.warn({ projectId: id, err }, "could not start a waiting coordinator");
80265
+ });
80266
+ }
80267
+ });
80268
+ }
79535
80269
  async create(projectId) {
79536
80270
  try {
79537
80271
  const row = projectService.row(projectId);
@@ -79547,11 +80281,14 @@ var ProjectCoordinatorService = class {
79547
80281
  projectService.setCoordinator(row.id, chat.id);
79548
80282
  }
79549
80283
  this.startErrors.delete(row.id);
80284
+ await projectService.seedInstructions(row.id);
79550
80285
  const chatId = chat.id;
79551
80286
  void projectSubscriptionService.reconcile(row.id).catch((err) => {
79552
80287
  log52.warn({ projectId: row.id, err }, "could not subscribe the coordinator");
79553
80288
  });
79554
- void agentSessionService.ensureSession(chatId, "prompt").catch((err) => {
80289
+ void agentSessionService.ensureSession(chatId, "prompt").then(() => this.waiting.delete(row.id)).catch((err) => {
80290
+ if (err instanceof WaitingForWorkerError) this.waiting.add(row.id);
80291
+ else this.waiting.delete(row.id);
79555
80292
  this.startErrors.set(row.id, err instanceof Error ? err.message : String(err));
79556
80293
  log52.warn({ projectId: row.id, chatId, err }, "coordinator agent did not start");
79557
80294
  });
@@ -79643,7 +80380,7 @@ var ProjectCoordinatorService = class {
79643
80380
  chatService.update(row.coordinatorChatId, { model: row.coordinatorModel });
79644
80381
  }
79645
80382
  }
79646
- // ---- charter and session scope --------------------------------------------------------
80383
+ // ---- session scope --------------------------------------------------------
79647
80384
  /** The project a chat coordinates, or undefined for any other chat. */
79648
80385
  projectOfChat(chatId) {
79649
80386
  const chat = chatService.get(chatId);
@@ -79658,53 +80395,6 @@ var ProjectCoordinatorService = class {
79658
80395
  return void 0;
79659
80396
  }
79660
80397
  }
79661
- /** The charter the project page shows. */
79662
- charterFor(ref2) {
79663
- return this.charter(projectService.row(ref2));
79664
- }
79665
- /** The system prompt appended to a coordinator session. */
79666
- charter(row) {
79667
- const view4 = projectService.get(row.id);
79668
- const p2 = view4.effectivePolicy;
79669
- const repos2 = view4.repos.map((r) => `- ${r.repo}${r.role ? ` (role: ${r.role})` : ""}`).join("\n");
79670
- const limits = [
79671
- p2.maxConcurrent ? `at most ${p2.maxConcurrent} worker agents run at once` : "no limit on concurrent worker agents",
79672
- p2.budgetUsd ? `a soft budget of $${p2.budgetUsd} for the project` : "no budget limit",
79673
- `worker agents run at isolation "${p2.isolationFloor}" or stronger`,
79674
- p2.labels.length ? `workers go only on hosts labeled ${p2.labels.join(", ")}` : "workers may go on any host"
79675
- ].join("; ");
79676
- const autonomy = {
79677
- observe: "Autonomy is observe. You may read project state and worker chats. You may not message, stop or dispatch anything. Report what you find and recommend actions to the user.",
79678
- autonomous: p2.autoMerge ? "Autonomy is autonomous with auto-merge on. You may dispatch workers within the limits and merge pull requests whose CI passed." : "Autonomy is autonomous. You may dispatch workers within the limits. Merging still needs the user's confirmation."
79679
- };
79680
- return [
79681
- `You are the coordinator of the Band project "${view4.name}".`,
79682
- view4.description ? `
79683
- Project description: ${view4.description}` : "",
79684
- "\nYour job is to plan the work across the project's repos, hand it to worker agents, check on them and keep the user informed. Each worker agent works in one git worktree of one repo, with its own chat. Work that spans repos takes one worker per repo, and you keep them in step.",
79685
- `
79686
- Working directory. You run in the project folder on the coordinator host. It is the working copy of the project context (notes.md, docs/, learnings/, inbox/, handoffs/) and has a checkout of every project repo's default branch under repos/<repo>/ (the local branch band/${view4.name}/<default>, which tracks origin/<default>). Read the current code there, or through repo_read, repo_search and repo_log. Band fetches before your turns and fast-forwards a checkout only when it is clean, so a checkout with local changes can be behind origin. Code changes go through worker agents (worktree_create). Do not edit a default branch checkout unless the user explicitly asks you to. ${p2.autonomy === "autonomous" ? "When the user asks for such an edit, you may commit it, and you may push it because autonomy is autonomous." : `When the user asks for such an edit, commit it but do not push without the user's confirmation, because autonomy is ${p2.autonomy}.`} The folder repos/ is never synced to the context repo. Everything else in the project folder (notes, docs, inbox, handoffs, learnings and any file you or the user add) syncs to the hub and to every worker of the project on its own, within seconds: write a file and the others see it, with no commit or push.`,
79687
- `
79688
- Repos in this project:
79689
- ${repos2 || "- none yet"}`,
79690
- `
79691
- Models: you run on ${p2.models.coordinator}, worker agents on ${p2.models.worker}, reviewers on ${p2.models.reviewer}.`,
79692
- `
79693
- Policy: ${limits}.`,
79694
- `
79695
- ${autonomy[p2.autonomy]}`,
79696
- `
79697
- Context. The project context repo "${view4.contextName}" is shared by every agent in the project, and the user context holds the user's preferences. Read them before you plan. Layout of the project context: notes.md (running notes), docs/ (design and contracts), media/ (screenshots, recordings), inbox/<agent>.md (pointers to handoffs for an agent), handoffs/ (one file per handoff), learnings/ (what agents learned). Use context_search to find things, context_append_learning to record what future agents should know, and context_handoff to pass work on. Write contracts between repos (API shapes, event formats) to docs/ so the agent on the other side can read them.`,
79698
- `
79699
- Tools. You act on the project only through the ${COORDINATOR_SERVER} tools: project_status (worktrees, agents, pull requests, spend), worktrees_list, worktree_create, chats_read, chats_send and worktree_stop, plus repo_read {repo, path}, repo_search {repo, query} and repo_log {repo, n} for the default branch checkouts. They are limited to this project's worktrees and repos. A refused call names the reason, so tell the user what blocked you instead of retrying.`,
79700
- "\nDispatching. worktree_create starts a worker agent in a new worktree of one repo of the project, on the branch you name, with your brief in .am/BRIEF.md. Write the brief so the worker can act on it alone: the goal, the constraints, the contracts with other repos and what is out of scope, plus acceptance scenarios it can check. For work across repos, start one worker per repo, usually on the same branch name, put the contract between them in docs/ of the project folder first, and decide the order their pull requests merge in.",
79701
- `
79702
- Wake-ups. Band wakes you with a "Subscription update" message when something needs you: a worker chat ended its turn with an error, is waiting for a permission or an answer, or finished; a pull request of a project worktree has a review comment, a CI result or was merged or closed; or a new file appeared in inbox/ or handoffs/ of the project context. The message names the chat id or the file path. Read the source before you act on it. Events are batched, so one message can carry several. A pull request's subscriptions end when it merges or closes.`,
79703
- `
79704
- Notes and the inbox. You are the only writer of notes.md in the project context: workers do not edit it, so keep it current with decisions and status. learnings/ is append-only: add entries, never rewrite or delete them. Handoffs from workers land in handoffs/<stamp>-<from>-to-<to>.md and a pointer line in inbox/<to>.md. When you have dealt with an item in inbox/ or handoffs/, move the file into inbox/done/ (a plain move in the project folder, which syncs on its own). A file in inbox/done/ does not wake you again.`,
79705
- "\nMerging. Never merge a pull request unless the user confirmed it in this conversation or auto-merge is on."
79706
- ].filter(Boolean).join("\n");
79707
- }
79708
80398
  // ---- tool logic ---------------------------------------------------------------------
79709
80399
  workersOf(row) {
79710
80400
  return projectService.workersOf(row).map((w2) => {
@@ -79846,8 +80536,8 @@ Notes and the inbox. You are the only writer of notes.md in the project context:
79846
80536
  const chat = this.workerChat(row, chatId);
79847
80537
  const text3 = message.trim();
79848
80538
  if (!text3) throw new CoordinatorToolError("The message is empty.");
79849
- if (text3.length > MAX_MESSAGE) {
79850
- throw new CoordinatorToolError(`The message is over ${MAX_MESSAGE} characters.`);
80539
+ if (text3.length > MAX_MESSAGE2) {
80540
+ throw new CoordinatorToolError(`The message is over ${MAX_MESSAGE2} characters.`);
79851
80541
  }
79852
80542
  this.checkDispatch(row, { newRun: !this.isRunning(chatId) });
79853
80543
  const result = submitOrQueueTask({ worktreeId: chatScope(chat), chatId, prompt: text3 });
@@ -79901,7 +80591,6 @@ Notes and the inbox. You are the only writer of notes.md in the project context:
79901
80591
  }
79902
80592
  };
79903
80593
  var projectCoordinatorService = new ProjectCoordinatorService();
79904
- projectFolderService.setInstructions((row) => projectCoordinatorService.charter(row));
79905
80594
 
79906
80595
  // src/server/services/agent-session-service.ts
79907
80596
  var log53 = createLogger("agent-sessions");
@@ -80003,6 +80692,7 @@ function stopRuntime(rt) {
80003
80692
  rt.process = null;
80004
80693
  rt.sessionId = null;
80005
80694
  runtimes.delete(rt.chatId);
80695
+ mcpProxyService.revokeSession(rt.chatId);
80006
80696
  }
80007
80697
  function idleTimeoutMs2() {
80008
80698
  const override = Number(process.env.BAND_AGENT_IDLE_TIMEOUT_MS);
@@ -80320,7 +81010,9 @@ async function ensureProcess(rt, def, cwd) {
80320
81010
  const exited = handlers2.onExit;
80321
81011
  handlers2.onExit = (code, stderr) => {
80322
81012
  void grant?.revoke();
80323
- mcpProxyService.revokeSession(rt.chatId);
81013
+ if (runtimes.get(rt.chatId) === rt && rt.generation === generation) {
81014
+ mcpProxyService.revokeSession(rt.chatId);
81015
+ }
80324
81016
  exited?.(code, stderr);
80325
81017
  };
80326
81018
  let proc;
@@ -80427,6 +81119,13 @@ function sessionMcpServers(rt, proc) {
80427
81119
  );
80428
81120
  return [];
80429
81121
  }
81122
+ if (!rt.relayEnv && hostRegistry.hostFor(rt.worktreeId).id !== hostRegistry.local.id) {
81123
+ log53.warn(
81124
+ { chatId: rt.chatId, scope: rt.worktreeId },
81125
+ "the agent runs on a worker but has no relay grant, so no MCP servers are passed"
81126
+ );
81127
+ return [];
81128
+ }
80430
81129
  mcpProxyService.revokeSession(rt.chatId);
80431
81130
  const { token } = mcpProxyService.issueSessionToken(
80432
81131
  rt.chatId,
@@ -80439,6 +81138,16 @@ function sessionMcpServers(rt, proc) {
80439
81138
  if (rt.relayEnv?.BAND_TOKEN) {
80440
81139
  headers.push({ name: "X-Band-Relay-Token", value: rt.relayEnv.BAND_TOKEN });
80441
81140
  }
81141
+ log53.info(
81142
+ {
81143
+ chatId: rt.chatId,
81144
+ scope: rt.worktreeId,
81145
+ servers: names,
81146
+ baseUrl: base,
81147
+ viaRelay: Boolean(rt.relayEnv?.BAND_TOKEN)
81148
+ },
81149
+ "MCP servers for the session"
81150
+ );
80442
81151
  return names.map((name5) => ({
80443
81152
  type: "http",
80444
81153
  name: name5,
@@ -81287,7 +81996,7 @@ var sessionIdSchema = external_exports2.string().regex(SESSION_ID_PATTERN3, "inv
81287
81996
 
81288
81997
  // src/server/services/_utils/upload-utils.ts
81289
81998
  import { mkdir as mkdir9, writeFile as writeFile6 } from "node:fs/promises";
81290
- import { join as join48, resolve as resolve9, sep as sep7 } from "node:path";
81999
+ import { join as join50, resolve as resolve9, sep as sep7 } from "node:path";
81291
82000
  init_state();
81292
82001
  function decodeParts(fileParts) {
81293
82002
  const baseTimestamp = Date.now();
@@ -81308,11 +82017,11 @@ function decodeParts(fileParts) {
81308
82017
  return decoded;
81309
82018
  }
81310
82019
  async function saveUploadedFilesDetailed(fileParts) {
81311
- const uploadDir = join48(bandHome(), "uploads");
82020
+ const uploadDir = join50(bandHome(), "uploads");
81312
82021
  await mkdir9(uploadDir, { recursive: true });
81313
82022
  const saved = [];
81314
82023
  for (const part of decodeParts(fileParts)) {
81315
- const filePath = join48(uploadDir, part.storedName);
82024
+ const filePath = join50(uploadDir, part.storedName);
81316
82025
  await writeFile6(filePath, part.buffer);
81317
82026
  saved.push({
81318
82027
  path: filePath,
@@ -81327,7 +82036,7 @@ async function saveUploadedFilesDetailed(fileParts) {
81327
82036
  async function remoteUploadDir(host, worktreeId2) {
81328
82037
  const dirs = (await host.info()).dirs;
81329
82038
  if (!dirs) throw new Error(`Host ${host.id} has no directory for uploads`);
81330
- return join48(dirs.uploads, worktreeId2);
82039
+ return join50(dirs.uploads, worktreeId2);
81331
82040
  }
81332
82041
  async function saveWorktreeUploads(worktreeId2, fileParts) {
81333
82042
  const host = hostRegistry.hostFor(worktreeId2);
@@ -81336,7 +82045,7 @@ async function saveWorktreeUploads(worktreeId2, fileParts) {
81336
82045
  await host.fs.mkdir(dir, { recursive: true });
81337
82046
  const saved = [];
81338
82047
  for (const part of decodeParts(fileParts)) {
81339
- const filePath = join48(dir, part.storedName);
82048
+ const filePath = join50(dir, part.storedName);
81340
82049
  await host.fs.writeFile(filePath, part.buffer, { exclusive: true, mode: 384 });
81341
82050
  saved.push({
81342
82051
  path: filePath,
@@ -81352,19 +82061,19 @@ async function uploadPathFromUrl(worktreeId2, url2) {
81352
82061
  const host = hostRegistry.hostFor(worktreeId2);
81353
82062
  const match = url2.match(/^\/api\/uploads\/(.+)$/);
81354
82063
  if (!match) return null;
81355
- if (host.id === hostRegistry.local.id) return join48(bandHome(), "uploads", match[1]);
82064
+ if (host.id === hostRegistry.local.id) return join50(bandHome(), "uploads", match[1]);
81356
82065
  const rest = match[1].split("/");
81357
82066
  if (rest.length !== 2) return null;
81358
82067
  try {
81359
82068
  if (decodeURIComponent(rest[0]) !== worktreeId2) return null;
81360
- return join48(await remoteUploadDir(host, worktreeId2), decodeURIComponent(rest[1]));
82069
+ return join50(await remoteUploadDir(host, worktreeId2), decodeURIComponent(rest[1]));
81361
82070
  } catch {
81362
82071
  return null;
81363
82072
  }
81364
82073
  }
81365
82074
  async function isWithinUploads(worktreeId2, path2) {
81366
82075
  const host = hostRegistry.hostFor(worktreeId2);
81367
- const dir = host.id === hostRegistry.local.id ? join48(bandHome(), "uploads") : await remoteUploadDir(host, worktreeId2).catch(() => null);
82076
+ const dir = host.id === hostRegistry.local.id ? join50(bandHome(), "uploads") : await remoteUploadDir(host, worktreeId2).catch(() => null);
81368
82077
  if (!dir) return false;
81369
82078
  const normalized2 = resolve9(path2);
81370
82079
  return normalized2 === dir || normalized2.startsWith(dir + sep7);
@@ -88353,7 +89062,8 @@ function createContext(opts) {
88353
89062
  return {
88354
89063
  chatId: readHeader(opts?.req?.headers, CHAT_ID_HEADER),
88355
89064
  worktreeId: readHeader(opts?.req?.headers, WORKTREE_ID_HEADER),
88356
- admin: opts?.admin === true
89065
+ admin: opts?.admin === true,
89066
+ tokenId: opts?.tokenId
88357
89067
  };
88358
89068
  }
88359
89069
 
@@ -89306,7 +90016,7 @@ var chatsRouter = t.router({
89306
90016
  init_zod();
89307
90017
 
89308
90018
  // src/server/services/cli-service.ts
89309
- import { execFile as execFile12 } from "node:child_process";
90019
+ import { execFile as execFile13 } from "node:child_process";
89310
90020
  import {
89311
90021
  accessSync as accessSync3,
89312
90022
  constants as constants3,
@@ -89319,13 +90029,13 @@ import {
89319
90029
  unlinkSync as unlinkSync5,
89320
90030
  writeFileSync as writeFileSync7
89321
90031
  } from "node:fs";
89322
- import { homedir as homedir14, platform as platform2 } from "node:os";
89323
- import { basename as basename2, dirname as dirname14, join as join49 } from "node:path";
90032
+ import { homedir as homedir15, platform as platform2 } from "node:os";
90033
+ import { basename as basename2, dirname as dirname14, join as join51 } from "node:path";
89324
90034
  function windowsBinDir() {
89325
- const localAppData = process.env.LOCALAPPDATA || join49(homedir14(), "AppData", "Local");
89326
- return join49(localAppData, "band", "bin");
90035
+ const localAppData = process.env.LOCALAPPDATA || join51(homedir15(), "AppData", "Local");
90036
+ return join51(localAppData, "band", "bin");
89327
90037
  }
89328
- var SYMLINK_PATH = platform2() === "win32" ? join49(windowsBinDir(), "band.cmd") : "/usr/local/bin/band";
90038
+ var SYMLINK_PATH = platform2() === "win32" ? join51(windowsBinDir(), "band.cmd") : "/usr/local/bin/band";
89329
90039
  function checkCliWindows() {
89330
90040
  let contents;
89331
90041
  try {
@@ -89356,8 +90066,8 @@ async function checkCli() {
89356
90066
  } catch {
89357
90067
  return "NotInstalled";
89358
90068
  }
89359
- const isCargoBuild = target.includes(join49("apps", "cli", "target"));
89360
- const isElectronSidecar = target.endsWith(join49("binaries", "band"));
90069
+ const isCargoBuild = target.includes(join51("apps", "cli", "target"));
90070
+ const isElectronSidecar = target.endsWith(join51("binaries", "band"));
89361
90071
  if (isCargoBuild || isElectronSidecar) {
89362
90072
  return "Installed";
89363
90073
  }
@@ -89400,7 +90110,7 @@ function noBinaryError(env = process.env) {
89400
90110
  }
89401
90111
  function runReg(args) {
89402
90112
  return new Promise((resolve16) => {
89403
- execFile12("reg", args, { windowsHide: true }, (err, stdout) => {
90113
+ execFile13("reg", args, { windowsHide: true }, (err, stdout) => {
89404
90114
  resolve16(err ? "" : stdout ?? "");
89405
90115
  });
89406
90116
  });
@@ -89530,328 +90240,6 @@ var clientStateRouter = t.router({
89530
90240
 
89531
90241
  // src/server/api/context/router.ts
89532
90242
  init_zod();
89533
-
89534
- // src/server/services/context-browser-service.ts
89535
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync7 } from "node:fs";
89536
- import { tmpdir as tmpdir7 } from "node:os";
89537
- import { join as join50 } from "node:path";
89538
- var MAX_FILE_BYTES2 = 1024 * 1024;
89539
- var MAX_DIFF_BYTES2 = 200 * 1024;
89540
- var MAX_MESSAGE2 = 200;
89541
- var LOG_LIMIT = 200;
89542
- var CONFLICT = /^(.*?)\.conflict-([A-Za-z0-9]+)((?:\.[^/.]+)?)$/;
89543
- var ContextConflictError = class extends Error {
89544
- constructor(message) {
89545
- super(message);
89546
- this.name = "ContextConflictError";
89547
- }
89548
- };
89549
- function conflictOriginal(path2) {
89550
- const m2 = CONFLICT.exec(path2);
89551
- return m2 ? `${m2[1]}${m2[3]}` : null;
89552
- }
89553
- function hasControl(text3, allowWhitespace = false) {
89554
- for (let i = 0; i < text3.length; i++) {
89555
- const c = text3.charCodeAt(i);
89556
- if (allowWhitespace && (c === 9 || c === 10 || c === 13)) continue;
89557
- if (c < 32 || c === 127) return true;
89558
- }
89559
- return false;
89560
- }
89561
- function checkPath(path2) {
89562
- const bad = !path2 || path2.length > 500 || path2.startsWith("/") || path2.endsWith("/") || path2.startsWith(":") || hasControl(path2) || path2.includes("\\") || path2.split("/").some((seg) => seg === "" || seg === "." || seg === ".." || seg === ".git");
89563
- if (bad) throw new ContextInputError(`"${path2}" is not a valid path inside a context`);
89564
- return path2;
89565
- }
89566
- function checkMessage(message) {
89567
- const trimmed = message.trim();
89568
- if (!trimmed) throw new ContextInputError("A commit message is required");
89569
- if (hasControl(trimmed, true)) {
89570
- throw new ContextInputError("The commit message holds control characters");
89571
- }
89572
- if (scanForSecrets(trimmed).length > 0) {
89573
- throw new ContextInputError("Not saved: the commit message looks like it holds a credential.");
89574
- }
89575
- if (trimmed.length > MAX_MESSAGE2) {
89576
- throw new ContextInputError(`The commit message is longer than ${MAX_MESSAGE2} characters`);
89577
- }
89578
- return trimmed;
89579
- }
89580
- var ContextBrowserService = class {
89581
- repo(name5) {
89582
- contextService.require(name5);
89583
- return contextRepoPath(name5);
89584
- }
89585
- /** The commit HEAD points at, or null in a repo with no commits. */
89586
- async head(repo) {
89587
- const r = await runGit(["rev-parse", "--verify", "-q", "HEAD^{commit}"], { cwd: repo });
89588
- return r.code === 0 ? r.stdout.trim() : null;
89589
- }
89590
- async tree(name5) {
89591
- const repo = this.repo(name5);
89592
- const head = await this.head(repo);
89593
- if (!head) return { head, entries: [] };
89594
- const out = await git2(["ls-tree", "-r", "-z", "--long", head], { cwd: repo });
89595
- const entries = [];
89596
- for (const rec of out.split("\0")) {
89597
- if (!rec) continue;
89598
- const tab = rec.indexOf(" ");
89599
- const meta = rec.slice(0, tab).split(/\s+/);
89600
- if (meta[1] !== "blob") continue;
89601
- const path2 = rec.slice(tab + 1);
89602
- entries.push({ path: path2, size: Number(meta[3]) || 0, conflictOf: conflictOriginal(path2) });
89603
- }
89604
- entries.sort((a, b2) => a.path.localeCompare(b2.path));
89605
- return { head, entries };
89606
- }
89607
- async file(name5, path2) {
89608
- const repo = this.repo(name5);
89609
- checkPath(path2);
89610
- const head = await this.head(repo);
89611
- if (!head) throw new ContextInputError(`"${path2}" does not exist`);
89612
- const size = await runGit(["cat-file", "-s", `${head}:${path2}`], { cwd: repo });
89613
- if (size.code !== 0) throw new ContextInputError(`"${path2}" does not exist`);
89614
- const bytes = Number(size.stdout.trim());
89615
- const type = await git2(["cat-file", "-t", `${head}:${path2}`], { cwd: repo });
89616
- if (type.trim() !== "blob") throw new ContextInputError(`"${path2}" is a folder`);
89617
- if (bytes > MAX_FILE_BYTES2) {
89618
- return { path: path2, binary: true, content: null, size: bytes, commit: head };
89619
- }
89620
- const content = await git2(["cat-file", "blob", `${head}:${path2}`], { cwd: repo });
89621
- if (content.includes("\0") || content.includes("\uFFFD")) {
89622
- return { path: path2, binary: true, content: null, size: bytes, commit: head };
89623
- }
89624
- return { path: path2, binary: false, content, size: bytes, commit: head };
89625
- }
89626
- async log(name5, path2, limit = 50) {
89627
- const repo = this.repo(name5);
89628
- if (path2) checkPath(path2);
89629
- if (!await this.head(repo)) return [];
89630
- const args = [
89631
- "--literal-pathspecs",
89632
- "log",
89633
- "--no-color",
89634
- `-n${Math.min(Math.max(limit, 1), LOG_LIMIT)}`,
89635
- "--format=%H%x1f%an%x1f%at%x1f%s%x1e"
89636
- ];
89637
- if (path2) args.push("--", path2);
89638
- const out = await git2(args, { cwd: repo });
89639
- return out.split("").map((rec) => rec.trim()).filter(Boolean).map((rec) => {
89640
- const [sha = "", author = "", at = "0", subject = ""] = rec.split("");
89641
- return { sha, author, at: Number(at) * 1e3, subject };
89642
- });
89643
- }
89644
- async diff(name5, sha, path2) {
89645
- const repo = this.repo(name5);
89646
- if (!/^[0-9a-f]{40}$/.test(sha)) throw new ContextInputError("Not a commit id");
89647
- if (path2) checkPath(path2);
89648
- const exists4 = await runGit(["cat-file", "-e", `${sha}^{commit}`], { cwd: repo });
89649
- if (exists4.code !== 0) throw new ContextInputError("No such commit");
89650
- const scope3 = path2 ? ["--", path2] : [];
89651
- const patch = await git2(
89652
- [
89653
- "--literal-pathspecs",
89654
- "show",
89655
- "--root",
89656
- "--format=",
89657
- "--patch",
89658
- "--no-color",
89659
- "-M",
89660
- "--no-ext-diff",
89661
- sha,
89662
- ...scope3
89663
- ],
89664
- { cwd: repo }
89665
- );
89666
- const names = await git2(
89667
- [
89668
- "diff-tree",
89669
- "--root",
89670
- "-m",
89671
- "--first-parent",
89672
- "--no-commit-id",
89673
- "--name-status",
89674
- "-r",
89675
- "-M",
89676
- sha
89677
- ],
89678
- { cwd: repo }
89679
- );
89680
- const files = names.split("\n").filter(Boolean).map((line2) => {
89681
- const parts = line2.split(" ");
89682
- return { status: parts[0] ?? "M", path: parts[parts.length - 1] ?? "" };
89683
- });
89684
- const patchBytes = Buffer.from(patch);
89685
- const truncated = patchBytes.length > MAX_DIFF_BYTES2;
89686
- return {
89687
- diff: truncated ? patchBytes.subarray(0, MAX_DIFF_BYTES2).toString("utf8") : patch,
89688
- truncated,
89689
- files
89690
- };
89691
- }
89692
- /** The newest learnings and handoffs files, one row per file, newest commit first. */
89693
- async recent(name5, limit = 20) {
89694
- const repo = this.repo(name5);
89695
- if (!await this.head(repo)) return [];
89696
- const out = await git2(
89697
- [
89698
- "log",
89699
- "--no-color",
89700
- "-n300",
89701
- "--name-only",
89702
- "--format=%x1e%H%x1f%at%x1f%s",
89703
- "--",
89704
- "learnings",
89705
- "handoffs"
89706
- ],
89707
- { cwd: repo }
89708
- );
89709
- const seen = /* @__PURE__ */ new Set();
89710
- const rows = [];
89711
- for (const block of out.split("")) {
89712
- const [header = "", ...files] = block.split("\n").filter((l, i) => i === 0 || l.trim());
89713
- const [sha = "", at = "0", subject = ""] = header.split("");
89714
- if (!sha) continue;
89715
- for (const path2 of files) {
89716
- const kind = path2.startsWith("learnings/") ? "learnings" : path2.startsWith("handoffs/") ? "handoffs" : null;
89717
- if (!kind || path2.endsWith(".gitkeep") || seen.has(path2)) continue;
89718
- seen.add(path2);
89719
- rows.push({ path: path2, kind, sha, at: Number(at) * 1e3, subject });
89720
- }
89721
- }
89722
- const { entries } = await this.tree(name5);
89723
- const live = new Set(entries.map((e) => e.path));
89724
- return rows.filter((r) => live.has(r.path)).slice(0, limit);
89725
- }
89726
- /**
89727
- * Saves `content` at `path` as one commit. `base` is the commit the editor
89728
- * loaded: when the file changed since, the save is refused so an edit never
89729
- * overwrites a newer version unseen.
89730
- */
89731
- async write(name5, path2, content, message, base) {
89732
- checkPath(path2);
89733
- if (Buffer.byteLength(content) > MAX_FILE_BYTES2) {
89734
- throw new ContextInputError("The file is larger than 1 MiB");
89735
- }
89736
- const found = scanForSecrets(content);
89737
- if (found.length > 0) {
89738
- const list = found.map((f2) => `${f2.kind} on line ${f2.line}`).join(", ");
89739
- throw new ContextInputError(
89740
- `Not saved: the text looks like it holds a credential (${list}). Remove it first.`
89741
- );
89742
- }
89743
- return this.commit(name5, [{ path: path2, content }], checkMessage(message), { base, guard: path2 });
89744
- }
89745
- /** Keeps one version of a conflicted file at its original path and deletes the conflict copy. */
89746
- async resolveConflict(name5, conflictPath, keep, message) {
89747
- checkPath(conflictPath);
89748
- const original = conflictOriginal(conflictPath);
89749
- if (!original) throw new ContextInputError(`"${conflictPath}" is not a conflict copy`);
89750
- const msg = checkMessage(
89751
- message ?? `Resolve conflict in ${original}: keep ${keep === "conflict" ? "the conflict copy" : "the original"}`
89752
- );
89753
- const repo = this.repo(name5);
89754
- const result = await contextService.exclusive(name5, async () => {
89755
- const head = await this.head(repo);
89756
- if (!head) throw new ContextInputError(`"${conflictPath}" does not exist`);
89757
- const blob2 = await runGit(["rev-parse", "--verify", "-q", `${head}:${conflictPath}`], {
89758
- cwd: repo
89759
- });
89760
- if (blob2.code !== 0) throw new ContextInputError(`"${conflictPath}" does not exist`);
89761
- if (keep === "original") {
89762
- const orig = await runGit(["rev-parse", "--verify", "-q", `${head}:${original}`], {
89763
- cwd: repo
89764
- });
89765
- if (orig.code !== 0) {
89766
- throw new ContextInputError(
89767
- `"${original}" does not exist, so "${conflictPath}" is not a conflict copy of anything`
89768
- );
89769
- }
89770
- }
89771
- const changes = [{ path: conflictPath }];
89772
- if (keep === "conflict") changes.push({ path: original, blob: blob2.stdout.trim() });
89773
- return this.commitLocked(repo, name5, head, changes, msg);
89774
- });
89775
- return { commit: result.commit, path: original };
89776
- }
89777
- commit(name5, changes, message, opts = {}) {
89778
- const repo = this.repo(name5);
89779
- return contextService.exclusive(name5, async () => {
89780
- const head = await this.head(repo);
89781
- if (head && opts.base && opts.guard && head !== opts.base) {
89782
- const at = async (rev) => {
89783
- const r = await runGit(["rev-parse", "--verify", "-q", `${rev}:${opts.guard}`], {
89784
- cwd: repo
89785
- });
89786
- return r.code === 0 ? r.stdout.trim() : "";
89787
- };
89788
- if (await at(head) !== await at(opts.base)) {
89789
- throw new ContextConflictError(
89790
- `"${opts.guard}" changed since you opened it. Reload it to see the newer version.`
89791
- );
89792
- }
89793
- }
89794
- return this.commitLocked(repo, name5, head, changes, message, opts.author);
89795
- });
89796
- }
89797
- /** Builds the commit in a private index. The caller holds the context's lock. */
89798
- async commitLocked(repo, name5, head, changes, message, author) {
89799
- const dir = mkdtempSync2(join50(tmpdir7(), "band-context-index-"));
89800
- try {
89801
- const env = contextGitEnv({
89802
- GIT_INDEX_FILE: join50(dir, "index"),
89803
- ...author ? {
89804
- GIT_AUTHOR_NAME: author,
89805
- GIT_AUTHOR_EMAIL: `${author}@localhost`,
89806
- GIT_COMMITTER_NAME: author,
89807
- GIT_COMMITTER_EMAIL: `${author}@localhost`
89808
- } : {}
89809
- });
89810
- const opts = { cwd: repo, env };
89811
- if (head) await git2(["read-tree", head], opts);
89812
- for (const change of changes) {
89813
- if (change.content === void 0 && change.blob === void 0) {
89814
- await git2(["update-index", "--index-info"], {
89815
- ...opts,
89816
- input: `0 ${"0".repeat(40)} ${change.path}
89817
- `
89818
- });
89819
- continue;
89820
- }
89821
- const oid = change.blob ?? (await git2(["hash-object", "-w", "--stdin"], { ...opts, input: change.content })).trim();
89822
- const r = await runGit(
89823
- ["update-index", "--add", "--cacheinfo", `100644,${oid},${change.path}`],
89824
- opts
89825
- );
89826
- if (r.code !== 0) {
89827
- throw new ContextInputError(
89828
- `Cannot save "${change.path}": ${r.stderr.trim().slice(0, 200)}`
89829
- );
89830
- }
89831
- }
89832
- const tree = (await git2(["write-tree"], opts)).trim();
89833
- if (head) {
89834
- const current3 = (await git2(["rev-parse", `${head}^{tree}`], opts)).trim();
89835
- if (current3 === tree) return { commit: head, changed: false };
89836
- }
89837
- const commit = (await git2(["commit-tree", tree, ...head ? ["-p", head] : [], "-m", message], opts)).trim();
89838
- const ref2 = (await git2(["symbolic-ref", "-q", "HEAD"], { cwd: repo })).trim();
89839
- const moved = await runGit(["update-ref", "-m", "context browser", ref2, commit, head ?? ""], {
89840
- cwd: repo
89841
- });
89842
- if (moved.code !== 0) {
89843
- throw new ContextConflictError("The context changed while saving. Reload and try again.");
89844
- }
89845
- contextService.syncSoon(name5);
89846
- return { commit, changed: true };
89847
- } finally {
89848
- rmSync7(dir, { recursive: true, force: true });
89849
- }
89850
- }
89851
- };
89852
- var contextBrowserService = new ContextBrowserService();
89853
-
89854
- // src/server/api/context/router.ts
89855
90243
  var name2 = external_exports2.string().trim().min(1).max(63);
89856
90244
  var labels = external_exports2.array(external_exports2.string().min(1).max(100)).max(20);
89857
90245
  var repoNames = external_exports2.array(external_exports2.string().trim().min(1).max(200)).max(100);
@@ -90048,7 +90436,7 @@ function throwAsTRPCError(err) {
90048
90436
  init_zod();
90049
90437
 
90050
90438
  // src/server/services/editor-service.ts
90051
- import { isAbsolute as isAbsolute5, join as join52, resolve as resolve10, sep as sep8 } from "node:path";
90439
+ import { isAbsolute as isAbsolute5, join as join53, resolve as resolve10, sep as sep8 } from "node:path";
90052
90440
 
90053
90441
  // ../../packages/shared/src/file-location.ts
90054
90442
  function formatFileLocation(filePath, line2, options) {
@@ -90148,7 +90536,7 @@ function subscribeToFileChanges(worktreeId2, listener) {
90148
90536
 
90149
90537
  // src/server/services/formatter.ts
90150
90538
  init_src();
90151
- import { basename as basename3, dirname as dirname15, isAbsolute as isAbsolute4, join as join51, resolve as resolvePath3 } from "node:path";
90539
+ import { basename as basename3, dirname as dirname15, isAbsolute as isAbsolute4, join as join52, resolve as resolvePath3 } from "node:path";
90152
90540
  import prettier from "prettier";
90153
90541
  var log59 = createLogger("formatter");
90154
90542
  var FormatterError = class extends Error {
@@ -90230,10 +90618,10 @@ async function resolveConfigOnHost(fs3, absFile, worktreePath) {
90230
90618
  let dir = dirname15(absFile);
90231
90619
  for (; ; ) {
90232
90620
  for (const name5 of JSON_CONFIGS) {
90233
- const parsed = await readJson2(fs3, join51(dir, name5));
90621
+ const parsed = await readJson2(fs3, join52(dir, name5));
90234
90622
  if (parsed && typeof parsed === "object") return withoutPlugins(parsed);
90235
90623
  }
90236
- const pkg = await readJson2(fs3, join51(dir, "package.json"));
90624
+ const pkg = await readJson2(fs3, join52(dir, "package.json"));
90237
90625
  if (pkg && typeof pkg.prettier === "object" && pkg.prettier !== null) {
90238
90626
  return withoutPlugins(pkg.prettier);
90239
90627
  }
@@ -90271,7 +90659,7 @@ async function isInsideWorktree(fs3, absFile, worktreePath) {
90271
90659
  realFile = await fs3.realpath(absFile);
90272
90660
  } catch {
90273
90661
  try {
90274
- realFile = join51(await fs3.realpath(dirname15(absFile)), basename3(absFile));
90662
+ realFile = join52(await fs3.realpath(dirname15(absFile)), basename3(absFile));
90275
90663
  } catch {
90276
90664
  realFile = absFile;
90277
90665
  }
@@ -90456,7 +90844,7 @@ async function canonicalizeMaybeMissing(fs3, p2) {
90456
90844
  const prefix = parts.slice(0, i).join(sep8) || sep8;
90457
90845
  try {
90458
90846
  const canonicalPrefix = await fs3.realpath(prefix);
90459
- return join52(canonicalPrefix, ...parts.slice(i));
90847
+ return join53(canonicalPrefix, ...parts.slice(i));
90460
90848
  } catch {
90461
90849
  }
90462
90850
  }
@@ -90531,14 +90919,14 @@ import { basename as basename5, dirname as dirname16, isAbsolute as isAbsolute6
90531
90919
 
90532
90920
  // src/server/services/repo-service.ts
90533
90921
  import { existsSync as existsSync15 } from "node:fs";
90534
- import { basename as basename4, join as join54, posix as posix7, resolve as resolve11 } from "node:path";
90922
+ import { basename as basename4, join as join55, posix as posix7, resolve as resolve11 } from "node:path";
90535
90923
  init_worktree_id();
90536
90924
  init_repos();
90537
90925
  init_worktree_statuses();
90538
90926
 
90539
90927
  // src/server/services/repo-avatar-service.ts
90540
90928
  import { mkdir as mkdir10, readFile as readFile9, rename as rename4, writeFile as writeFile7 } from "node:fs/promises";
90541
- import { join as join53 } from "node:path";
90929
+ import { join as join54 } from "node:path";
90542
90930
  init_src();
90543
90931
  init_repos();
90544
90932
  init_settings();
@@ -90690,7 +91078,7 @@ var RepoAvatarService = class {
90690
91078
  return null;
90691
91079
  }
90692
91080
  await mkdir10(cacheDir(), { recursive: true });
90693
- const imagePath = join53(cacheDir(), `${key2}.img`);
91081
+ const imagePath = join54(cacheDir(), `${key2}.img`);
90694
91082
  await writeFile7(`${imagePath}.tmp`, result.bytes);
90695
91083
  await rename4(`${imagePath}.tmp`, imagePath);
90696
91084
  await this.writeMeta(key2, entry, {
@@ -90708,7 +91096,7 @@ var RepoAvatarService = class {
90708
91096
  async readCached(key2, meta) {
90709
91097
  if (meta?.status !== "ok" || !meta.contentType) return null;
90710
91098
  try {
90711
- const bytes = await readFile9(join53(cacheDir(), `${key2}.img`));
91099
+ const bytes = await readFile9(join54(cacheDir(), `${key2}.img`));
90712
91100
  return { bytes, contentType: meta.contentType };
90713
91101
  } catch {
90714
91102
  return null;
@@ -90718,7 +91106,7 @@ var RepoAvatarService = class {
90718
91106
  entry.meta = meta;
90719
91107
  entry.retryAfter = 0;
90720
91108
  await mkdir10(cacheDir(), { recursive: true });
90721
- await writeFile7(join53(cacheDir(), `${key2}.json`), JSON.stringify(meta));
91109
+ await writeFile7(join54(cacheDir(), `${key2}.json`), JSON.stringify(meta));
90722
91110
  }
90723
91111
  /** In-memory view of one owner's cache sidecar, loaded from disk once. */
90724
91112
  async entry(ref2) {
@@ -90727,7 +91115,7 @@ var RepoAvatarService = class {
90727
91115
  if (entry) return entry;
90728
91116
  let meta = null;
90729
91117
  try {
90730
- meta = JSON.parse(await readFile9(join53(cacheDir(), `${key2}.json`), "utf-8"));
91118
+ meta = JSON.parse(await readFile9(join54(cacheDir(), `${key2}.json`), "utf-8"));
90731
91119
  } catch {
90732
91120
  }
90733
91121
  entry = this.entries.get(key2) ?? { meta, retryAfter: 0 };
@@ -90749,7 +91137,7 @@ var RepoAvatarService = class {
90749
91137
  }
90750
91138
  };
90751
91139
  function cacheDir() {
90752
- return join53(bandHome(), "cache", "github-avatars");
91140
+ return join54(bandHome(), "cache", "github-avatars");
90753
91141
  }
90754
91142
  function cacheKey2(ref2) {
90755
91143
  return `${ref2.host.replace(/[^a-z0-9.-]/g, "_")}__${ref2.owner.toLowerCase()}`;
@@ -90899,7 +91287,7 @@ var RepoService = class {
90899
91287
  }
90900
91288
  }
90901
91289
  const resolvedPath = resolve11(path2);
90902
- const kind = existsSync15(join54(resolvedPath, ".git")) ? "git" : "plain";
91290
+ const kind = existsSync15(join55(resolvedPath, ".git")) ? "git" : "plain";
90903
91291
  let defaultBranch = "main";
90904
91292
  let worktrees2 = [];
90905
91293
  if (kind === "git") {
@@ -91851,14 +92239,16 @@ var hostsRouter = t.router({
91851
92239
  list: publicProcedure.input(
91852
92240
  external_exports2.object({ limit: external_exports2.number().int().min(1).max(MAX_LIST_LIMIT).default(DEFAULT_LIST_LIMIT) }).default({ limit: DEFAULT_LIST_LIMIT })
91853
92241
  ).query(async ({ input }) => {
91854
- const hosts2 = tokenService.listHosts(input.limit);
92242
+ const all = tokenService.listHosts(input.limit);
92243
+ const hosts2 = all.filter((h) => h.id !== "local" || h.usable);
91855
92244
  const local = hosts2.find((h) => h.id === "local");
91856
92245
  if (local) {
91857
92246
  const info = await hostRegistry.local.info().catch(() => null);
91858
92247
  if (info) {
91859
92248
  local.home = info.home ?? null;
91860
92249
  local.capabilities = Object.entries(info.capabilities).filter(([, on]) => on).map(([name5]) => name5);
91861
- local.agents = await agentTypesOn(hostRegistry.local);
92250
+ local.report = info.report ?? null;
92251
+ local.agents = info.report ? info.report.agents.filter((a) => a.installed && a.loggedIn !== false).map((a) => a.type) : await agentTypesOn(hostRegistry.local);
91862
92252
  local.tools = info.tools;
91863
92253
  }
91864
92254
  }
@@ -94630,10 +95020,10 @@ var pluginsRouter = t.router({
94630
95020
  });
94631
95021
 
94632
95022
  // ../../packages/host-local/src/process/install.ts
94633
- import { execFile as execFile13 } from "node:child_process";
95023
+ import { execFile as execFile14 } from "node:child_process";
94634
95024
  async function brewInstall(pkg, resolvedPath) {
94635
95025
  await new Promise((resolve16, reject) => {
94636
- execFile13(
95026
+ execFile14(
94637
95027
  "brew",
94638
95028
  ["install", pkg],
94639
95029
  { env: { ...process.env, PATH: resolvedPath }, timeout: 12e4 },
@@ -94933,16 +95323,16 @@ async function checkout(fn) {
94933
95323
  });
94934
95324
  }
94935
95325
  function present(view4) {
94936
- return { ...view4, coordinatorError: projectCoordinatorService.lastError(view4.id) };
95326
+ return {
95327
+ ...view4,
95328
+ coordinatorError: projectCoordinatorService.lastError(view4.id),
95329
+ coordinatorWaiting: projectCoordinatorService.isWaiting(view4.id)
95330
+ };
94937
95331
  }
94938
95332
  var projectsRouter = t.router({
94939
95333
  list: publicProcedure.query(() => ({ projects: projectService.list().map(present) })),
94940
95334
  /** `project` is an id or a name. */
94941
95335
  get: publicProcedure.input(external_exports2.object({ project: ref })).query(({ input }) => guard3(() => ({ project: present(projectService.get(input.project)) }))),
94942
- /** The charter the coordinator starts with, for the project's view. */
94943
- charter: publicProcedure.input(external_exports2.object({ project: ref })).query(
94944
- ({ input }) => guard3(() => ({ charter: projectCoordinatorService.charterFor(input.project) }))
94945
- ),
94946
95336
  create: adminProcedure.input(
94947
95337
  external_exports2.object({
94948
95338
  name: external_exports2.string().trim().min(1).max(63),
@@ -94984,7 +95374,6 @@ var projectsRouter = t.router({
94984
95374
  ({ input }) => guard3(async () => {
94985
95375
  const { project, ...patch } = input;
94986
95376
  const updated = await projectCoordinatorService.update(project, patch);
94987
- void projectFolderService.writeInstructions(projectService.row(updated.id));
94988
95377
  return { project: present(updated) };
94989
95378
  })
94990
95379
  ),
@@ -95040,7 +95429,6 @@ var projectsRouter = t.router({
95040
95429
  throw new ProjectConflictError(err instanceof Error ? err.message : String(err));
95041
95430
  }
95042
95431
  const view4 = projectService.removeRepo(row.id, input.repo);
95043
- void projectFolderService.writeInstructions(projectService.row(row.id));
95044
95432
  return { project: view4 };
95045
95433
  })
95046
95434
  ),
@@ -95161,7 +95549,7 @@ var projectsRouter = t.router({
95161
95549
  init_src();
95162
95550
  init_zod();
95163
95551
  import { realpathSync as realpathSync5 } from "node:fs";
95164
- import { join as join55, resolve as resolve12, sep as sep9 } from "node:path";
95552
+ import { join as join56, resolve as resolve12, sep as sep9 } from "node:path";
95165
95553
  init_state();
95166
95554
  var log63 = createLogger("trpc.queue");
95167
95555
  var queuedFileSchema = external_exports2.object({
@@ -95171,7 +95559,7 @@ var queuedFileSchema = external_exports2.object({
95171
95559
  filename: external_exports2.string().optional()
95172
95560
  });
95173
95561
  function isPathWithinUploadDir(p2) {
95174
- const uploadDir = join55(bandHome(), "uploads");
95562
+ const uploadDir = join56(bandHome(), "uploads");
95175
95563
  const normalized2 = resolve12(p2);
95176
95564
  if (normalized2 !== uploadDir && !normalized2.startsWith(uploadDir + sep9)) {
95177
95565
  return false;
@@ -95911,12 +96299,12 @@ function allowed(args) {
95911
96299
  var UNAUTHENTICATED2 = /gh auth login|GH_TOKEN|GITHUB_TOKEN|authentication|HTTP 401|bad credentials/i;
95912
96300
  var MISSING_GH = /ENOENT|command not found|executable file not found|no such file/i;
95913
96301
  var GH_TIMEOUT_MS2 = 3e4;
95914
- function withTimeout2(run4) {
96302
+ function withTimeout2(run5) {
95915
96303
  let timer;
95916
96304
  const timeout = new Promise((_2, reject) => {
95917
96305
  timer = setTimeout(() => reject(new Error("gh timed out")), GH_TIMEOUT_MS2);
95918
96306
  });
95919
- return Promise.race([run4, timeout]).finally(() => clearTimeout(timer));
96307
+ return Promise.race([run5, timeout]).finally(() => clearTimeout(timer));
95920
96308
  }
95921
96309
  function firstLine2(err) {
95922
96310
  return (err instanceof Error ? err.message : String(err)).split("\n")[0];
@@ -97190,6 +97578,11 @@ var terminalsRouters = {
97190
97578
  init_zod();
97191
97579
  var label = external_exports2.string().trim().min(1).max(100);
97192
97580
  var tokensRouter = t.router({
97581
+ /** Which token this request uses. Any device token may ask; it returns no secret. */
97582
+ current: publicProcedure.query(({ ctx }) => ({
97583
+ tokenId: ctx.tokenId ?? null,
97584
+ admin: ctx.admin
97585
+ })),
97193
97586
  list: adminProcedure.input(
97194
97587
  external_exports2.object({ limit: external_exports2.number().int().min(1).max(MAX_LIST_LIMIT).default(DEFAULT_LIST_LIMIT) }).default({ limit: DEFAULT_LIST_LIMIT })
97195
97588
  ).query(({ input }) => ({ tokens: tokenService.list(input.limit) })),
@@ -97313,7 +97706,7 @@ var vaultRouter = t.router({
97313
97706
  init_zod();
97314
97707
 
97315
97708
  // src/server/services/diff-service.ts
97316
- import { join as join56, resolve as resolve13, sep as sep10 } from "node:path";
97709
+ import { join as join57, resolve as resolve13, sep as sep10 } from "node:path";
97317
97710
  init_src();
97318
97711
  var log71 = createLogger("diff-service");
97319
97712
  var EMPTY_TREE_ARGS = ["hash-object", "-t", "tree", "/dev/null"];
@@ -97411,7 +97804,7 @@ function parseFileStatuses(nameStatusOutput) {
97411
97804
  return fileStatuses;
97412
97805
  }
97413
97806
  function assertWorktreeRelative(cwd, filePath) {
97414
- const target = resolve13(join56(cwd, filePath));
97807
+ const target = resolve13(join57(cwd, filePath));
97415
97808
  if (target !== cwd && !target.startsWith(cwd + sep10)) {
97416
97809
  throw new Error("Invalid path");
97417
97810
  }
@@ -97426,7 +97819,7 @@ function assertWorktreeRelative(cwd, filePath) {
97426
97819
  }
97427
97820
  async function readUntrackedFileLines(host, cwd, file2) {
97428
97821
  try {
97429
- const content = new TextDecoder().decode(await host.fs.readFile(join56(cwd, file2)));
97822
+ const content = new TextDecoder().decode(await host.fs.readFile(join57(cwd, file2)));
97430
97823
  const lines = content.split("\n");
97431
97824
  if (lines.length > 0 && lines[lines.length - 1] === "") {
97432
97825
  lines.pop();
@@ -97773,7 +98166,7 @@ var DiffService = class {
97773
98166
  const compareBranch = options.compareBranch ?? defaultBranch;
97774
98167
  const cwd = worktree.worktree.path;
97775
98168
  const execGit2 = gitRunner(worktree.host);
97776
- const hasGit = await worktree.host.fs.stat(join56(cwd, ".git")).then(
98169
+ const hasGit = await worktree.host.fs.stat(join57(cwd, ".git")).then(
97777
98170
  () => true,
97778
98171
  (err) => {
97779
98172
  if (err instanceof HostOfflineError || err instanceof HostTimeoutError) throw err;
@@ -97811,7 +98204,7 @@ var DiffService = class {
97811
98204
  applyLineCounts(status.staged, parseNumstat(stagedNumstat));
97812
98205
  await Promise.all(
97813
98206
  status.untracked.map(async (entry) => {
97814
- const lines = await countUntrackedLines(worktree.host, join56(cwd, entry.path));
98207
+ const lines = await countUntrackedLines(worktree.host, join57(cwd, entry.path));
97815
98208
  if (lines !== null) {
97816
98209
  entry.additions = lines;
97817
98210
  entry.deletions = 0;
@@ -97959,7 +98352,7 @@ var DiffService = class {
97959
98352
  var diffService = new DiffService();
97960
98353
 
97961
98354
  // src/server/services/files-service.ts
97962
- import { dirname as dirname17, extname as extname3, join as join57, resolve as resolve14, sep as sep11 } from "node:path";
98355
+ import { dirname as dirname17, extname as extname3, join as join58, resolve as resolve14, sep as sep11 } from "node:path";
97963
98356
  var MAX_FILE_SIZE2 = 1024 * 1024;
97964
98357
  var LANG_MAP2 = {
97965
98358
  ".js": "javascript",
@@ -98023,7 +98416,7 @@ var FilesService = class {
98023
98416
  throw new WorktreeNotFoundError(worktreeId2);
98024
98417
  }
98025
98418
  const root = worktree.worktree.path;
98026
- const target = resolve14(join57(root, relative3));
98419
+ const target = resolve14(join58(root, relative3));
98027
98420
  const insideRoot = target === root || target.startsWith(root + sep11);
98028
98421
  if (!insideRoot) {
98029
98422
  throw new Error("Invalid path");
@@ -99902,7 +100295,7 @@ function createMcpServer(req) {
99902
100295
  return server;
99903
100296
  }
99904
100297
  function registerContextTools(server, caller) {
99905
- const run4 = async (fn) => {
100298
+ const run5 = async (fn) => {
99906
100299
  try {
99907
100300
  return { content: [{ type: "text", text: JSON.stringify(await fn(), null, 2) }] };
99908
100301
  } catch (err) {
@@ -99922,7 +100315,7 @@ function registerContextTools(server, caller) {
99922
100315
  // biome-ignore lint/suspicious/noExplicitAny: MCP SDK accepts Zod schemas as AnySchema
99923
100316
  },
99924
100317
  // biome-ignore lint/suspicious/noExplicitAny: handler args follow the schema above
99925
- (args) => run4(() => contextToolsService.search(caller, args.query, args.scope, args.limit))
100318
+ (args) => run5(() => contextToolsService.search(caller, args.query, args.scope, args.limit))
99926
100319
  );
99927
100320
  server.registerTool(
99928
100321
  "context_append_learning",
@@ -99935,7 +100328,7 @@ function registerContextTools(server, caller) {
99935
100328
  // biome-ignore lint/suspicious/noExplicitAny: MCP SDK accepts Zod schemas as AnySchema
99936
100329
  },
99937
100330
  // biome-ignore lint/suspicious/noExplicitAny: handler args follow the schema above
99938
- (args) => run4(() => contextToolsService.appendLearning(caller, args))
100331
+ (args) => run5(() => contextToolsService.appendLearning(caller, args))
99939
100332
  );
99940
100333
  server.registerTool(
99941
100334
  "context_handoff",
@@ -99949,7 +100342,7 @@ function registerContextTools(server, caller) {
99949
100342
  // biome-ignore lint/suspicious/noExplicitAny: MCP SDK accepts Zod schemas as AnySchema
99950
100343
  },
99951
100344
  // biome-ignore lint/suspicious/noExplicitAny: handler args follow the schema above
99952
- (args) => run4(() => contextToolsService.handoff(caller, args))
100345
+ (args) => run5(() => contextToolsService.handoff(caller, args))
99953
100346
  );
99954
100347
  }
99955
100348
  async function handleMcpRequest(req, res) {
@@ -100245,8 +100638,8 @@ init_isolation();
100245
100638
  var log73 = createLogger("project-dispatch");
100246
100639
  var DEFAULT_WORKER_AGENT = "claude-code";
100247
100640
  var lanes = /* @__PURE__ */ new Map();
100248
- function serialized(projectId, run4) {
100249
- const next = (lanes.get(projectId) ?? Promise.resolve()).then(run4, run4);
100641
+ function serialized(projectId, run5) {
100642
+ const next = (lanes.get(projectId) ?? Promise.resolve()).then(run5, run5);
100250
100643
  const tail = next.catch(() => void 0);
100251
100644
  lanes.set(projectId, tail);
100252
100645
  void tail.then(() => {
@@ -100332,7 +100725,9 @@ var ProjectDispatchService = class {
100332
100725
  repo: input.repo,
100333
100726
  branch,
100334
100727
  projectId: row.id,
100335
- ...Object.keys(placement).length > 0 ? { placement } : {},
100728
+ // With the hub's own machine off there is no default host, so a call that names no
100729
+ // placement still goes through placement and gets the least loaded capable worker.
100730
+ ...Object.keys(placement).length > 0 || !isLocalHostEnabled() ? { placement } : {},
100336
100731
  brief: renderBrief({
100337
100732
  title: input.title?.trim() || void 0,
100338
100733
  project: row.name,
@@ -100436,12 +100831,12 @@ function lookUpToolsOnce(server, listPage) {
100436
100831
  if (running) return running;
100437
100832
  const last = lookupMissedAt.get(server.id);
100438
100833
  if (last !== void 0 && Date.now() - last < LOOKUP_COOLDOWN_MS) return Promise.resolve();
100439
- const run4 = lookUpTools(server, listPage).catch(() => void 0).finally(() => {
100834
+ const run5 = lookUpTools(server, listPage).catch(() => void 0).finally(() => {
100440
100835
  lookupsInFlight.delete(server.id);
100441
100836
  lookupMissedAt.set(server.id, Date.now());
100442
100837
  });
100443
- lookupsInFlight.set(server.id, run4);
100444
- return run4;
100838
+ lookupsInFlight.set(server.id, run5);
100839
+ return run5;
100445
100840
  }
100446
100841
  async function lookUpTools(server, listPage) {
100447
100842
  const tools = /* @__PURE__ */ new Map();
@@ -100600,7 +100995,7 @@ async function readBody2(req, max2) {
100600
100995
 
100601
100996
  // src/server/api/mcp-proxy/coordinator.ts
100602
100997
  var log74 = createLogger("coordinator-mcp");
100603
- async function run3(fn) {
100998
+ async function run4(fn) {
100604
100999
  try {
100605
101000
  const value = await fn();
100606
101001
  return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
@@ -100623,7 +101018,7 @@ function createServer2(projectId) {
100623
101018
  server.registerTool(
100624
101019
  name5,
100625
101020
  { description, inputSchema },
100626
- (args) => run3(() => handler(args ?? {}))
101021
+ (args) => run4(() => handler(args ?? {}))
100627
101022
  );
100628
101023
  };
100629
101024
  tool(
@@ -101131,7 +101526,7 @@ import {
101131
101526
  statSync as statSync12,
101132
101527
  writeFileSync as writeFileSync8
101133
101528
  } from "node:fs";
101134
- import { join as join58 } from "node:path";
101529
+ import { join as join59 } from "node:path";
101135
101530
  var MEDIA_ID = /^[0-9a-f]{64}$/;
101136
101531
  var DEFAULT_MAX_MEDIA_BYTES = 50 * 1024 * 1024;
101137
101532
  var ALLOWED_MEDIA_TYPES = /* @__PURE__ */ new Set([
@@ -101159,7 +101554,7 @@ function maxMediaBytes() {
101159
101554
  return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAX_MEDIA_BYTES;
101160
101555
  }
101161
101556
  function mediaDir() {
101162
- return join58(bandHome(), "media");
101557
+ return join59(bandHome(), "media");
101163
101558
  }
101164
101559
  function allowedMediaType(header) {
101165
101560
  const type = (header ?? "").split(";")[0].trim().toLowerCase();
@@ -101179,7 +101574,7 @@ var mediaService = {
101179
101574
  }
101180
101575
  const dir = mediaDir();
101181
101576
  mkdirSync10(dir, { recursive: true });
101182
- const tmp = join58(dir, `.upload-${randomBytes17(8).toString("hex")}`);
101577
+ const tmp = join59(dir, `.upload-${randomBytes17(8).toString("hex")}`);
101183
101578
  const hash = createHash12("sha256");
101184
101579
  const limit = maxMediaBytes();
101185
101580
  let size = 0;
@@ -101200,11 +101595,11 @@ var mediaService = {
101200
101595
  if (expectedId && expectedId !== id) {
101201
101596
  throw new MediaRefusedError(400, "The id is not the SHA-256 of the content");
101202
101597
  }
101203
- const target = join58(dir, id);
101598
+ const target = join59(dir, id);
101204
101599
  const created = !existsSync16(target);
101205
- const typePath = join58(dir, `${id}.type`);
101600
+ const typePath = join59(dir, `${id}.type`);
101206
101601
  if (created || !existsSync16(typePath)) {
101207
- const typeTmp = join58(dir, `.type-${randomBytes17(8).toString("hex")}`);
101602
+ const typeTmp = join59(dir, `.type-${randomBytes17(8).toString("hex")}`);
101208
101603
  writeFileSync8(typeTmp, contentType);
101209
101604
  renameSync6(typeTmp, typePath);
101210
101605
  }
@@ -101225,15 +101620,15 @@ var mediaService = {
101225
101620
  meta(id) {
101226
101621
  if (!MEDIA_ID.test(id)) return null;
101227
101622
  try {
101228
- const size = statSync12(join58(mediaDir(), id)).size;
101229
- const contentType = readFileSync12(join58(mediaDir(), `${id}.type`), "utf8").trim();
101623
+ const size = statSync12(join59(mediaDir(), id)).size;
101624
+ const contentType = readFileSync12(join59(mediaDir(), `${id}.type`), "utf8").trim();
101230
101625
  return { size, contentType };
101231
101626
  } catch {
101232
101627
  return null;
101233
101628
  }
101234
101629
  },
101235
101630
  read(id, range) {
101236
- return createReadStream4(join58(mediaDir(), id), range);
101631
+ return createReadStream4(join59(mediaDir(), id), range);
101237
101632
  }
101238
101633
  };
101239
101634
 
@@ -101747,14 +102142,14 @@ async function handleOAuthCallback(req, res) {
101747
102142
 
101748
102143
  // src/server/api/web-app-manifest.ts
101749
102144
  import { createReadStream as createReadStream5, statSync as statSync13 } from "node:fs";
101750
- import { join as join59 } from "node:path";
102145
+ import { join as join60 } from "node:path";
101751
102146
  var WEB_APP_MANIFEST_PATH = "/manifest.webmanifest";
101752
102147
  var ICON_FILES = ["band-192.png", "band-512.png", "apple-touch-icon.png"];
101753
102148
  function handleWebAppManifest(req, res, publicDir) {
101754
102149
  if (req.method !== "GET" && req.method !== "HEAD") return false;
101755
102150
  const pathname = req.url?.split("?")[0];
101756
102151
  if (pathname === WEB_APP_MANIFEST_PATH) {
101757
- sendFile(req, res, join59(publicDir, "manifest.webmanifest"), {
102152
+ sendFile(req, res, join60(publicDir, "manifest.webmanifest"), {
101758
102153
  "Content-Type": "application/manifest+json",
101759
102154
  "Cache-Control": "no-cache"
101760
102155
  });
@@ -101762,7 +102157,7 @@ function handleWebAppManifest(req, res, publicDir) {
101762
102157
  }
101763
102158
  const icon = ICON_FILES.find((name5) => pathname === `/icons/${name5}`);
101764
102159
  if (!icon) return false;
101765
- sendFile(req, res, join59(publicDir, "icons", icon), {
102160
+ sendFile(req, res, join60(publicDir, "icons", icon), {
101766
102161
  "Content-Type": "image/png",
101767
102162
  "Cache-Control": "public, max-age=86400"
101768
102163
  });
@@ -102022,6 +102417,10 @@ var MSG_KEY_EVENT = 4;
102022
102417
  var MSG_POINTER_EVENT = 5;
102023
102418
  var MSG_CLIENT_CUT_TEXT = 6;
102024
102419
  var MAX_MESSAGE_BYTES2 = 1024 * 1024;
102420
+ function cutTextSize(payload) {
102421
+ if (8 + payload > MAX_MESSAGE_BYTES2) throw new RfbFilterError("RFB message too large");
102422
+ return 8 + payload;
102423
+ }
102025
102424
  var RfbFilterError = class extends Error {
102026
102425
  };
102027
102426
  var RfbClientFilter = class {
@@ -102072,7 +102471,7 @@ var RfbClientFilter = class {
102072
102471
  case MSG_POINTER_EVENT:
102073
102472
  return 6;
102074
102473
  case MSG_CLIENT_CUT_TEXT:
102075
- return buf.length < 8 ? null : 8 + buf.readUInt32BE(4);
102474
+ return buf.length < 8 ? null : cutTextSize(Math.abs(buf.readInt32BE(4)));
102076
102475
  default:
102077
102476
  throw new RfbFilterError(`Unsupported RFB message type ${buf[0]}`);
102078
102477
  }
@@ -102419,7 +102818,7 @@ async function handleLspConnection(ws, req) {
102419
102818
  }
102420
102819
 
102421
102820
  // src/server/services/_utils/hosted-file.ts
102422
- import { basename as basename6, join as join60 } from "node:path";
102821
+ import { basename as basename6, join as join61 } from "node:path";
102423
102822
  async function openHostedFile(kind, worktreeId2, rawName) {
102424
102823
  const host = hostRegistry.hostFor(worktreeId2);
102425
102824
  if (host.id === hostRegistry.local.id) return null;
@@ -102433,7 +102832,7 @@ async function openHostedFile(kind, worktreeId2, rawName) {
102433
102832
  try {
102434
102833
  const dirs = (await host.info()).dirs;
102435
102834
  if (!dirs) return null;
102436
- const path2 = join60(dirs[kind], worktreeId2, name5);
102835
+ const path2 = join61(dirs[kind], worktreeId2, name5);
102437
102836
  const stat8 = await host.fs.stat(path2, { followSymlinks: true });
102438
102837
  if (stat8.kind !== "file") return null;
102439
102838
  return { size: stat8.size, stream: host.fs.readStream(path2) };
@@ -102479,7 +102878,7 @@ async function listenWithFallback(server, startPort, attempts = 20) {
102479
102878
  init_src();
102480
102879
  init_worktree_id();
102481
102880
  var log82 = createLogger("context-autosync");
102482
- var LOCAL_HOST_ID6 = "local";
102881
+ var LOCAL_HOST_ID7 = "local";
102483
102882
  var DEFAULT_INTERVAL_MS2 = 5e3;
102484
102883
  var SOON_MS = 300;
102485
102884
  var MAX_STALE_WAIT = 12;
@@ -102620,18 +103019,20 @@ var ContextAutosyncService = class {
102620
103019
  const online = new Set(
102621
103020
  tokenService.listHosts().filter((h) => h.status === "online").map((h) => h.id)
102622
103021
  );
102623
- online.add(LOCAL_HOST_ID6);
103022
+ online.add(LOCAL_HOST_ID7);
102624
103023
  let localLabels;
102625
103024
  const lifecycle = ephemeralLifecycleService.states();
102626
103025
  const out = [];
102627
103026
  for (const project of projectService.rows()) {
102628
103027
  const context = contextService.find(project.contextName);
102629
103028
  if (!context) continue;
102630
- const hostIds = /* @__PURE__ */ new Set([projectFolderService.hostOf(project).id]);
103029
+ const hostIds = /* @__PURE__ */ new Set();
103030
+ const folderHost = projectFolderService.hostIdOf(project);
103031
+ if (folderHost) hostIds.add(folderHost);
102631
103032
  for (const w2 of projectService.allWorktreesOf(project.id)) {
102632
103033
  const id = toWorktreeId(w2.repoName, w2.name);
102633
103034
  if (lifecycle.has(id)) continue;
102634
- hostIds.add(w2.hostId ?? LOCAL_HOST_ID6);
103035
+ hostIds.add(w2.hostId ?? LOCAL_HOST_ID7);
102635
103036
  }
102636
103037
  for (const hostId2 of hostIds) {
102637
103038
  if (!online.has(hostId2)) continue;
@@ -102641,10 +103042,10 @@ var ContextAutosyncService = class {
102641
103042
  } catch {
102642
103043
  continue;
102643
103044
  }
102644
- if (hostId2 === LOCAL_HOST_ID6 && localLabels === void 0) {
103045
+ if (hostId2 === LOCAL_HOST_ID7 && localLabels === void 0) {
102645
103046
  localLabels = (await host.info().catch(() => null))?.labels ?? [];
102646
103047
  }
102647
- const labels3 = hostId2 === LOCAL_HOST_ID6 ? localLabels ?? [] : tokenService.hostLabels(hostId2) ?? [];
103048
+ const labels3 = hostId2 === LOCAL_HOST_ID7 ? localLabels ?? [] : tokenService.hostLabels(hostId2) ?? [];
102648
103049
  if (contextService.forSession("", labels3, context).every((r) => r.name !== context.name)) {
102649
103050
  continue;
102650
103051
  }
@@ -102652,7 +103053,7 @@ var ContextAutosyncService = class {
102652
103053
  project,
102653
103054
  host,
102654
103055
  spec: { name: context.name, kind: "project" },
102655
- readOnly: hostId2 !== LOCAL_HOST_ID6 && context.workerAccess === "read-only"
103056
+ readOnly: hostId2 !== LOCAL_HOST_ID7 && context.workerAccess === "read-only"
102656
103057
  });
102657
103058
  }
102658
103059
  }
@@ -102883,7 +103284,7 @@ init_repos();
102883
103284
  // src/server/services/worker-cli-service.ts
102884
103285
  import { createHash as createHash13 } from "node:crypto";
102885
103286
  import { readFile as readFile10 } from "node:fs/promises";
102886
- import { join as join61 } from "node:path";
103287
+ import { join as join62 } from "node:path";
102887
103288
  init_src();
102888
103289
  var log84 = createLogger("worker-cli");
102889
103290
  var RPC_INVALID_PARAMS2 = -32602;
@@ -102891,7 +103292,7 @@ var PLATFORM_NAME = /^[a-z0-9_]{1,16}$/;
102891
103292
  function candidatesFor(platform3, arch) {
102892
103293
  const out = [];
102893
103294
  const dir = process.env.BAND_CLI_BINARIES_DIR;
102894
- if (dir) out.push(join61(dir, `band-${platform3}-${arch}${platform3 === "win32" ? ".exe" : ""}`));
103295
+ if (dir) out.push(join62(dir, `band-${platform3}-${arch}${platform3 === "win32" ? ".exe" : ""}`));
102895
103296
  if (platform3 === process.platform && arch === process.arch) {
102896
103297
  const own = findCliBinary();
102897
103298
  if (own) out.push(own);
@@ -103441,7 +103842,8 @@ var workerRelayService = new WorkerRelayService();
103441
103842
  // src/server/services/worker-link-service.ts
103442
103843
  var log86 = createLogger("worker-link");
103443
103844
  var repoQueries4 = new RepoQueries();
103444
- var LOCAL_HOST_ID7 = "local";
103845
+ var LOCAL_HOST_ID8 = "local";
103846
+ var CAPABILITY_REFRESH_MS = 3 * 6e4;
103445
103847
  var WORKER_CONNECT_PATH = "/api/workers/connect";
103446
103848
  var WORKER_EXCHANGE_PATH = "/api/workers/exchange";
103447
103849
  var MAX_EXCHANGE_BODY_BYTES = 4 * 1024;
@@ -103457,6 +103859,7 @@ var WorkerLinkService = class {
103457
103859
  handleProtocols: selectWsProtocol
103458
103860
  });
103459
103861
  unsubscribeRevoke = null;
103862
+ refreshTimer = null;
103460
103863
  constructor(options = {}) {
103461
103864
  this.tokens = options.tokens ?? tokenService;
103462
103865
  this.queries = options.queries ?? new TokenQueries();
@@ -103482,20 +103885,59 @@ var WorkerLinkService = class {
103482
103885
  /** Makes every known worker resolvable, so a worktree on an offline host still finds its host. */
103483
103886
  start() {
103484
103887
  for (const row of this.queries.listHosts(1e3)) {
103485
- if (row.id !== LOCAL_HOST_ID7) this.remoteHost(row.id);
103888
+ if (row.id !== LOCAL_HOST_ID8) this.remoteHost(row.id);
103486
103889
  }
103487
103890
  for (const row of this.queries.listHosts(1e3)) {
103488
- if (row.id !== LOCAL_HOST_ID7 && row.status === "online") {
103891
+ if (row.id !== LOCAL_HOST_ID8 && row.status === "online") {
103489
103892
  this.queries.setHostStatus(row.id, "offline");
103490
103893
  }
103491
103894
  }
103895
+ const every = Number(process.env.BAND_CAPABILITY_REFRESH_MS);
103896
+ this.refreshTimer = setInterval(
103897
+ () => void this.refreshReports(),
103898
+ Number.isFinite(every) && every > 0 ? every : CAPABILITY_REFRESH_MS
103899
+ );
103900
+ this.refreshTimer.unref();
103492
103901
  this.unsubscribeRevoke = this.tokens.onRevoked((row) => {
103493
103902
  if (row.kind === "worker_session" && row.hostId) {
103494
103903
  if (this.server.drop(row.hostId, "token revoked")) log86.info(`cut link of ${row.hostId}`);
103495
103904
  }
103496
103905
  });
103497
103906
  }
103907
+ /**
103908
+ * Reads `host.info` again from every online worker and stores its capability report. The call is
103909
+ * passive, so it does not keep an ephemeral worker awake. A change goes out as a host status
103910
+ * event, which Settings > Hosts follows and a coordinator waiting for a worker listens to.
103911
+ */
103912
+ async refreshReports() {
103913
+ if (this.refreshing) return;
103914
+ this.refreshing = true;
103915
+ try {
103916
+ await Promise.allSettled(
103917
+ this.queries.listHosts(1e3).filter((row) => row.id !== LOCAL_HOST_ID8 && row.status === "online").map((row) => this.refreshReport(row))
103918
+ );
103919
+ } finally {
103920
+ this.refreshing = false;
103921
+ }
103922
+ }
103923
+ refreshing = false;
103924
+ async refreshReport(row) {
103925
+ try {
103926
+ const info = await this.remoteHost(row.id).info();
103927
+ const { repoMappings: _mappings, ...facts } = info;
103928
+ const stored = { ...row.info ?? {}, ...facts };
103929
+ const changed = reportKey(stored.report) !== reportKey(row.info?.report);
103930
+ this.queries.setHostInfo(row.id, stored);
103931
+ if (changed) this.publish(row.id, "online");
103932
+ } catch (err) {
103933
+ log86.debug(
103934
+ `could not refresh the report of ${row.id}: ${err instanceof Error ? err.message : err}`
103935
+ );
103936
+ }
103937
+ }
103498
103938
  async close() {
103939
+ if (this.refreshTimer) clearInterval(this.refreshTimer);
103940
+ this.refreshTimer = null;
103499
103941
  this.unsubscribeRevoke?.();
103500
103942
  for (const ws of this.wss.clients) ws.terminate();
103501
103943
  await this.server.close();
@@ -103618,12 +104060,16 @@ var WorkerLinkService = class {
103618
104060
  }
103619
104061
  };
103620
104062
  var workerLinkService = new WorkerLinkService();
104063
+ function reportKey(report2) {
104064
+ const { checkedAt: _at, ...rest } = report2 ?? {};
104065
+ return JSON.stringify(rest);
104066
+ }
103621
104067
 
103622
104068
  // start-server.ts
103623
104069
  function logCrash(message) {
103624
104070
  try {
103625
104071
  mkdirSync11(bandHome(), { recursive: true });
103626
- appendFileSync2(join62(bandHome(), "server.log"), message, "utf-8");
104072
+ appendFileSync2(join63(bandHome(), "server.log"), message, "utf-8");
103627
104073
  } catch {
103628
104074
  }
103629
104075
  }
@@ -103663,9 +104109,9 @@ function resolveUiDir() {
103663
104109
  if (explicit) return resolve15(explicit);
103664
104110
  const candidates2 = [
103665
104111
  resolve15(SERVER_ROOT, "..", "..", "web", "dist", "client"),
103666
- join62(SERVER_ROOT, "client")
104112
+ join63(SERVER_ROOT, "client")
103667
104113
  ];
103668
- return candidates2.find((dir) => existsSync17(join62(dir, "_shell.html"))) ?? candidates2[1];
104114
+ return candidates2.find((dir) => existsSync17(join63(dir, "_shell.html"))) ?? candidates2[1];
103669
104115
  }
103670
104116
  var clientDir = resolveUiDir();
103671
104117
  var uiRoot = process.env.BAND_UI_ROOT ? resolve15(process.env.BAND_UI_ROOT) : resolve15(SERVER_ROOT, "..", "web");
@@ -103701,12 +104147,12 @@ function allowedOrigins() {
103701
104147
  ];
103702
104148
  }
103703
104149
  var handleCors = createCorsMiddleware(allowedOrigins);
103704
- function requestIsAdmin(req) {
103705
- if (!expectedToken) return true;
104150
+ function requestDevice(req) {
104151
+ if (!expectedToken) return { admin: true, tokenId: void 0 };
103706
104152
  const token = authenticatedCredential(req, (c) => tokenService.resolveDevice(c), {
103707
104153
  allowCookie: req.headers.origin !== "null"
103708
104154
  });
103709
- return token?.admin === true;
104155
+ return { admin: token?.admin === true, tokenId: token?.id };
103710
104156
  }
103711
104157
  var assets = null;
103712
104158
  var _openApiSpec = null;
@@ -103716,7 +104162,7 @@ function getOpenApiSpec() {
103716
104162
  if (isDev) {
103717
104163
  const { generateOpenAPIDocument } = await import("@trpc/openapi");
103718
104164
  const doc = await generateOpenAPIDocument(
103719
- join62(SERVER_ROOT, "src", "server", "api", "router.ts"),
104165
+ join63(SERVER_ROOT, "src", "server", "api", "router.ts"),
103720
104166
  {
103721
104167
  title: "Band API",
103722
104168
  version: "1.0.0"
@@ -103725,7 +104171,7 @@ function getOpenApiSpec() {
103725
104171
  doc.servers = [{ url: "/trpc" }];
103726
104172
  return JSON.stringify(doc, null, 2);
103727
104173
  }
103728
- const openApiDoc = JSON.parse(readFileSync13(join62(SERVER_ROOT, "openapi.json"), "utf-8"));
104174
+ const openApiDoc = JSON.parse(readFileSync13(join63(SERVER_ROOT, "openapi.json"), "utf-8"));
103729
104175
  openApiDoc.servers = [{ url: "/trpc" }];
103730
104176
  return JSON.stringify(openApiDoc, null, 2);
103731
104177
  })().catch((err) => {
@@ -103748,7 +104194,7 @@ function serveStaticFile(res, root, subdir, rawFilename) {
103748
104194
  res.end("Bad request");
103749
104195
  return;
103750
104196
  }
103751
- const filePath = join62(root, subdir, filename);
104197
+ const filePath = join63(root, subdir, filename);
103752
104198
  try {
103753
104199
  const fileStat = statSync14(filePath);
103754
104200
  const contentType = mimeTypeFromFilename(filename);
@@ -103792,7 +104238,7 @@ async function serveWorktreeFile(res, worktreeId2, rawPath) {
103792
104238
  return;
103793
104239
  }
103794
104240
  const root = worktree.worktree.path;
103795
- const target = resolve15(join62(root, rawPath));
104241
+ const target = resolve15(join63(root, rawPath));
103796
104242
  if (!target.startsWith(root + sep12) && target !== root) {
103797
104243
  res.writeHead(400);
103798
104244
  res.end("Bad request");
@@ -104024,7 +104470,7 @@ async function main() {
104024
104470
  res.end(JSON.stringify({ status: "ok", app: "band-web-server" }));
104025
104471
  return;
104026
104472
  }
104027
- if (handleWebAppManifest(req, res, isDev ? join62(uiRoot, "public") : clientDir)) return;
104473
+ if (handleWebAppManifest(req, res, isDev ? join63(uiRoot, "public") : clientDir)) return;
104028
104474
  if (req.url?.split("?")[0] === WORKER_EXCHANGE_PATH) {
104029
104475
  await workerLinkService.handleExchange(req, res);
104030
104476
  return;
@@ -104125,7 +104571,7 @@ async function main() {
104125
104571
  return;
104126
104572
  }
104127
104573
  if (await serveHostedFile(res, "shared", partition, rest.slice(slashIdx + 1))) return;
104128
- serveStaticFile(res, bandHome(), join62("shared", partition), rest.slice(slashIdx + 1));
104574
+ serveStaticFile(res, bandHome(), join63("shared", partition), rest.slice(slashIdx + 1));
104129
104575
  return;
104130
104576
  }
104131
104577
  if (req.url?.startsWith("/api/worktree-file/")) {
@@ -104260,7 +104706,7 @@ async function main() {
104260
104706
  endpoint: "/trpc",
104261
104707
  req: request,
104262
104708
  router: appRouter,
104263
- createContext: ({ req: webRequest }) => createContext({ req: webRequest, admin: requestIsAdmin(req) })
104709
+ createContext: ({ req: webRequest }) => createContext({ req: webRequest, ...requestDevice(req) })
104264
104710
  });
104265
104711
  pipeWebResponseToNodeRes(response, res);
104266
104712
  return;
@@ -104333,7 +104779,7 @@ async function main() {
104333
104779
  if (pathname.startsWith("/_shell")) res.setHeader("Cache-Control", "no-cache");
104334
104780
  }
104335
104781
  });
104336
- const shellPath2 = join62(clientDir, "_shell.html");
104782
+ const shellPath2 = join63(clientDir, "_shell.html");
104337
104783
  if (!existsSync17(shellPath2)) {
104338
104784
  throw new Error(
104339
104785
  `UI build not found at ${clientDir} (no _shell.html). Run \`pnpm build\` or pass --ui-dir <path>.`
@@ -104345,7 +104791,7 @@ async function main() {
104345
104791
  const wssHandler = applyWSSHandler({
104346
104792
  wss,
104347
104793
  router: appRouter,
104348
- createContext: ({ req }) => createContext({ req, admin: requestIsAdmin(req) })
104794
+ createContext: ({ req }) => createContext({ req, ...requestDevice(req) })
104349
104795
  });
104350
104796
  const terminalWss = new import_websocket_server.default({ noServer: true, handleProtocols: selectWsProtocol });
104351
104797
  const lspWss = new import_websocket_server.default({ noServer: true, handleProtocols: selectWsProtocol });
@@ -104489,6 +104935,7 @@ Failed to bind any port in ${initialPort}..${initialPort + 19}:
104489
104935
  subscriptionService.start();
104490
104936
  void projectCoordinatorService.removeLegacyWorktrees().catch((err) => console.error("Failed to remove legacy coordinator worktrees:", err)).finally(() => {
104491
104937
  projectSubscriptionService.start();
104938
+ projectCoordinatorService.start();
104492
104939
  void projectFolderService.warmAll();
104493
104940
  contextAutosyncService.start();
104494
104941
  });