@lesscap/baton-cli 0.8.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/baton.mjs CHANGED
@@ -14,7 +14,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
14
14
  throw Error('Dynamic require of "' + x + '" is not supported');
15
15
  });
16
16
  var __commonJS = (cb, mod) => function __require2() {
17
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ try {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ } catch (e) {
20
+ throw mod = 0, e;
21
+ }
18
22
  };
19
23
  var __copyProps = (to, from, except, desc) => {
20
24
  if (from && typeof from === "object" || typeof from === "function") {
@@ -2281,7 +2285,7 @@ var require_websocket = __commonJS({
2281
2285
  var http = __require("http");
2282
2286
  var net = __require("net");
2283
2287
  var tls = __require("tls");
2284
- var { randomBytes, createHash } = __require("crypto");
2288
+ var { randomBytes, createHash: createHash2 } = __require("crypto");
2285
2289
  var { Duplex, Readable: Readable2 } = __require("stream");
2286
2290
  var { URL: URL2 } = __require("url");
2287
2291
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2949,7 +2953,7 @@ var require_websocket = __commonJS({
2949
2953
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2950
2954
  return;
2951
2955
  }
2952
- const digest = createHash("sha1").update(key + GUID).digest("base64");
2956
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
2953
2957
  if (res.headers["sec-websocket-accept"] !== digest) {
2954
2958
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
2955
2959
  return;
@@ -3230,7 +3234,7 @@ var require_stream = __commonJS({
3230
3234
  };
3231
3235
  duplex._final = function(callback) {
3232
3236
  if (ws.readyState === ws.CONNECTING) {
3233
- ws.once("open", function open() {
3237
+ ws.once("open", function open3() {
3234
3238
  duplex._final(callback);
3235
3239
  });
3236
3240
  return;
@@ -3251,7 +3255,7 @@ var require_stream = __commonJS({
3251
3255
  };
3252
3256
  duplex._write = function(chunk, encoding, callback) {
3253
3257
  if (ws.readyState === ws.CONNECTING) {
3254
- ws.once("open", function open() {
3258
+ ws.once("open", function open3() {
3255
3259
  duplex._write(chunk, encoding, callback);
3256
3260
  });
3257
3261
  return;
@@ -3318,7 +3322,7 @@ var require_websocket_server = __commonJS({
3318
3322
  var EventEmitter = __require("events");
3319
3323
  var http = __require("http");
3320
3324
  var { Duplex } = __require("stream");
3321
- var { createHash } = __require("crypto");
3325
+ var { createHash: createHash2 } = __require("crypto");
3322
3326
  var extension2 = require_extension();
3323
3327
  var PerMessageDeflate2 = require_permessage_deflate();
3324
3328
  var subprotocol2 = require_subprotocol();
@@ -3625,7 +3629,7 @@ var require_websocket_server = __commonJS({
3625
3629
  );
3626
3630
  }
3627
3631
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3628
- const digest = createHash("sha1").update(key + GUID).digest("base64");
3632
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
3629
3633
  const headers = [
3630
3634
  "HTTP/1.1 101 Switching Protocols",
3631
3635
  "Upgrade: websocket",
@@ -3724,14 +3728,14 @@ function isUppercase(char = "") {
3724
3728
  if (NUMBER_CHAR_RE.test(char)) return;
3725
3729
  return char !== char.toLowerCase();
3726
3730
  }
3727
- function splitByCase(str3, separators) {
3731
+ function splitByCase(str4, separators) {
3728
3732
  const splitters = separators ?? STR_SPLITTERS;
3729
3733
  const parts = [];
3730
- if (!str3 || typeof str3 !== "string") return parts;
3734
+ if (!str4 || typeof str4 !== "string") return parts;
3731
3735
  let buff = "";
3732
3736
  let previousUpper;
3733
3737
  let previousSplitter;
3734
- for (const char of str3) {
3738
+ for (const char of str4) {
3735
3739
  const isSplitter = splitters.includes(char);
3736
3740
  if (isSplitter === true) {
3737
3741
  parts.push(buff);
@@ -3762,23 +3766,23 @@ function splitByCase(str3, separators) {
3762
3766
  parts.push(buff);
3763
3767
  return parts;
3764
3768
  }
3765
- function upperFirst(str3) {
3766
- return str3 ? str3[0].toUpperCase() + str3.slice(1) : "";
3769
+ function upperFirst(str4) {
3770
+ return str4 ? str4[0].toUpperCase() + str4.slice(1) : "";
3767
3771
  }
3768
- function lowerFirst(str3) {
3769
- return str3 ? str3[0].toLowerCase() + str3.slice(1) : "";
3772
+ function lowerFirst(str4) {
3773
+ return str4 ? str4[0].toLowerCase() + str4.slice(1) : "";
3770
3774
  }
3771
- function pascalCase(str3, opts) {
3772
- return str3 ? (Array.isArray(str3) ? str3 : splitByCase(str3)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
3775
+ function pascalCase(str4, opts) {
3776
+ return str4 ? (Array.isArray(str4) ? str4 : splitByCase(str4)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
3773
3777
  }
3774
- function camelCase(str3, opts) {
3775
- return lowerFirst(pascalCase(str3 || "", opts));
3778
+ function camelCase(str4, opts) {
3779
+ return lowerFirst(pascalCase(str4 || "", opts));
3776
3780
  }
3777
- function kebabCase(str3, joiner) {
3778
- return str3 ? (Array.isArray(str3) ? str3 : splitByCase(str3)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
3781
+ function kebabCase(str4, joiner) {
3782
+ return str4 ? (Array.isArray(str4) ? str4 : splitByCase(str4)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
3779
3783
  }
3780
- function snakeCase(str3) {
3781
- return kebabCase(str3 || "", "_");
3784
+ function snakeCase(str4) {
3785
+ return kebabCase(str4 || "", "_");
3782
3786
  }
3783
3787
 
3784
3788
  // ../../node_modules/.pnpm/citty@0.2.2/node_modules/citty/dist/index.mjs
@@ -5370,11 +5374,13 @@ var contentTypeForPath = (path) => src_default.getType(path) ?? "application/oct
5370
5374
  // src/output.ts
5371
5375
  var toJson = (data) => JSON.stringify(data, null, 2);
5372
5376
  var fmtWorkspace = (w) => `${w.id} ${w.name}`;
5377
+ var fmtWorkerTemplate = (t) => `${t.id} ${t.name} [${t.available ? "available" : "unavailable"}] ${t.agentKinds.join(", ")}`;
5378
+ var fmtWorkerCreation = (c) => `${c.id} [${c.status}/${c.step ?? "pending"}] ${c.name} ${c.agentKind}${c.workerId === null ? "" : ` W-${c.workerId}`}${c.error ? ` ${c.error}` : ""}`;
5373
5379
  var fmtProject = (p) => `${p.id} ${p.name}`;
5374
5380
  var fmtRequirement = (r) => `${r.code} [${r.status}] ${r.title}`;
5375
5381
  var fmtTask = (t) => `${t.code} [${t.status}] ${t.title}`;
5376
5382
  var fmtComment = (c) => `${c.workerId !== void 0 ? `worker#${c.workerId}` : "you"} ${c.body}`;
5377
- var fmtSession = (s) => `${s.id} ${s.name} ${s.attached ? "[active]" : "[inactive]"} ${s.worktreePath ?? "(pending)"}`;
5383
+ var fmtSession = (s) => `${s.id} ${s.name} ${s.attached ? "[active]" : "[inactive]"} ${s.cwd ?? "(pending)"}`;
5378
5384
  var fmtWorker = (w) => {
5379
5385
  const offline = w.connected === false ? " [offline]" : "";
5380
5386
  return `W-${w.id} ${w.name} ${w.hostname} ${w.agentKind}${offline}`;
@@ -5486,6 +5492,11 @@ var primeLogin = (baseUrl) => {
5486
5492
  );
5487
5493
  };
5488
5494
 
5495
+ // src/client/files.ts
5496
+ var filesClient = (baseUrl) => ({
5497
+ report: (result) => request(`${baseUrl}/workers/me/files/results`, { method: "PUT", body: result })
5498
+ });
5499
+
5489
5500
  // src/client/loops.ts
5490
5501
  var loopsClient = (baseUrl) => {
5491
5502
  const u = (p) => `${baseUrl}${p}`;
@@ -5550,7 +5561,7 @@ var servicesClient = (baseUrl) => {
5550
5561
  };
5551
5562
 
5552
5563
  // src/client/retry.ts
5553
- var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
5564
+ var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
5554
5565
  var withRetry = async (fn, opts = {}) => {
5555
5566
  const tries = Math.max(1, opts.tries ?? 4);
5556
5567
  const baseMs = opts.baseMs ?? 300;
@@ -5665,7 +5676,8 @@ var sessionsClient = (baseUrl) => {
5665
5676
  },
5666
5677
  destroy: async (id) => {
5667
5678
  await request(u(`/sessions/${id}`), { method: "DELETE" });
5668
- }
5679
+ },
5680
+ notify: (id, text) => request(u(`/sessions/${id}/notify`), { method: "POST", body: { text } })
5669
5681
  };
5670
5682
  };
5671
5683
 
@@ -5685,6 +5697,14 @@ var taskClient = (baseUrl) => {
5685
5697
  };
5686
5698
  };
5687
5699
 
5700
+ // src/client/worker-creations.ts
5701
+ var workerCreationsClient = (baseUrl) => ({
5702
+ templates: (projectId) => request(`${baseUrl}/projects/${projectId}/worker-templates`, { method: "GET" }),
5703
+ create: (projectId, input) => request(`${baseUrl}/projects/${projectId}/worker-creations`, { method: "POST", body: input }),
5704
+ listByProject: (projectId) => request(`${baseUrl}/projects/${projectId}/worker-creations`, { method: "GET" }),
5705
+ get: (id) => request(`${baseUrl}/worker-creations/${id}`, { method: "GET" })
5706
+ });
5707
+
5688
5708
  // src/client/workers.ts
5689
5709
  var workersClient = (baseUrl) => {
5690
5710
  const u = (p) => `${baseUrl}${p}`;
@@ -5725,6 +5745,7 @@ var createClient = (baseUrl, opts) => {
5725
5745
  return clientFromBase(baseUrl);
5726
5746
  };
5727
5747
  var clientFromBase = (baseUrl) => ({
5748
+ workerCreations: workerCreationsClient(baseUrl),
5728
5749
  artifacts: artifactsClient(baseUrl),
5729
5750
  workspaces: workspaceClient(baseUrl),
5730
5751
  projects: projectClient(baseUrl),
@@ -5732,6 +5753,7 @@ var clientFromBase = (baseUrl) => ({
5732
5753
  tasks: taskClient(baseUrl),
5733
5754
  sessions: sessionsClient(baseUrl),
5734
5755
  services: servicesClient(baseUrl),
5756
+ files: filesClient(baseUrl),
5735
5757
  loops: loopsClient(baseUrl),
5736
5758
  workers: workersClient(baseUrl)
5737
5759
  });
@@ -5783,7 +5805,6 @@ var viewWorker = (cfg) => {
5783
5805
  return {
5784
5806
  server,
5785
5807
  projectId: project2,
5786
- baseBranch: cfg.baseBranch,
5787
5808
  workerId: worker2.id,
5788
5809
  agentKind: worker2.agentKind ?? "claude-code",
5789
5810
  name: worker2.name,
@@ -5791,29 +5812,40 @@ var viewWorker = (cfg) => {
5791
5812
  apiToken: worker2.apiToken
5792
5813
  };
5793
5814
  };
5794
- var worktreeConfig = (cfg, session2) => ({
5815
+ var workerProjectConfig = (cfg) => ({
5795
5816
  server: cfg.server,
5796
5817
  project: cfg.projectId,
5797
- baseBranch: cfg.baseBranch,
5798
5818
  worker: {
5799
5819
  id: cfg.workerId,
5800
5820
  name: cfg.name,
5801
5821
  agentKind: cfg.agentKind,
5802
5822
  machineId: cfg.machineId,
5803
5823
  apiToken: cfg.apiToken
5804
- },
5805
- ...session2 === void 0 ? {} : { session: session2 }
5824
+ }
5825
+ });
5826
+ var workerEnv = (cfg) => ({
5827
+ ...Object.fromEntries(
5828
+ Object.entries(process.env).filter(
5829
+ (entry) => entry[1] !== void 0
5830
+ )
5831
+ ),
5832
+ BATON_SERVER: cfg.server,
5833
+ BATON_PROJECT_ID: String(cfg.projectId),
5834
+ BATON_WORKER_ID: String(cfg.workerId),
5835
+ BATON_WORKER_TOKEN: cfg.apiToken
5806
5836
  });
5807
5837
 
5808
5838
  // src/config.ts
5809
5839
  var resolveBaseUrl = (urlArg, env = process.env) => {
5810
5840
  if (urlArg) return urlArg;
5841
+ if (env.BATON_SERVER) return env.BATON_SERVER;
5811
5842
  const cfg = loadProjectConfigOrNull(projectConfigPath());
5812
5843
  if (cfg?.server) return cfg.server;
5813
5844
  return env.BATON_URL ?? "http://localhost:3280";
5814
5845
  };
5815
5846
  var resolveBaseUrlOrNull = (urlArg, env = process.env) => {
5816
5847
  if (urlArg) return urlArg;
5848
+ if (env.BATON_SERVER) return env.BATON_SERVER;
5817
5849
  const cfg = loadProjectConfigOrNull(projectConfigPath());
5818
5850
  if (cfg?.server) return cfg.server;
5819
5851
  return env.BATON_URL ?? null;
@@ -5823,7 +5855,7 @@ var resolveBaseUrlOrNull = (urlArg, env = process.env) => {
5823
5855
  var common = {
5824
5856
  url: {
5825
5857
  type: "string",
5826
- description: "baton server url (--url > .baton.json > BATON_URL > http://localhost:3280)"
5858
+ description: "baton server url (--url > BATON_SERVER > .baton.json > BATON_URL > http://localhost:3280)"
5827
5859
  },
5828
5860
  json: { type: "boolean", description: "output JSON" }
5829
5861
  };
@@ -5846,6 +5878,8 @@ var resolveBearer = () => {
5846
5878
  };
5847
5879
  var resolveProjectId = (args) => {
5848
5880
  if (args.project !== void 0 && args.project !== "") return Number(args.project);
5881
+ if (process.env.BATON_PROJECT_ID !== void 0)
5882
+ return contextId(process.env.BATON_PROJECT_ID, "BATON_PROJECT_ID");
5849
5883
  const cfg = loadProjectConfigOrNull(projectConfigPath());
5850
5884
  if (cfg?.project !== void 0) return cfg.project;
5851
5885
  throw new Error("no project in scope. pass --project <id> or run `baton init` in this directory.");
@@ -5870,6 +5904,18 @@ var parseDuration = (s) => {
5870
5904
  const mult = m[2] === "d" ? 86400 : m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1;
5871
5905
  return n * mult;
5872
5906
  };
5907
+ var contextId = (value, source) => {
5908
+ const id = Number(value);
5909
+ if (!Number.isSafeInteger(id) || id <= 0) throw new Error(`invalid ${source}`);
5910
+ return id;
5911
+ };
5912
+ var resolveWorkerId = () => {
5913
+ if (process.env.BATON_WORKER_ID !== void 0)
5914
+ return contextId(process.env.BATON_WORKER_ID, "BATON_WORKER_ID");
5915
+ const id = loadProjectConfigOrNull(projectConfigPath())?.worker?.id;
5916
+ if (id === void 0) throw new Error("no worker in scope");
5917
+ return id;
5918
+ };
5873
5919
 
5874
5920
  // src/commands/artifact.ts
5875
5921
  var formatArtifact = (artifact2) => `${artifact2.id} ${artifact2.size} B ${artifact2.filename} ${artifact2.url}`;
@@ -6227,11 +6273,11 @@ var EventSource = class extends EventTarget {
6227
6273
  return;
6228
6274
  }
6229
6275
  const decoder = new TextDecoder(), reader = body.getReader();
6230
- let open = true;
6276
+ let open3 = true;
6231
6277
  do {
6232
6278
  const { done, value } = await reader.read();
6233
- value && __privateGet(this, _parser).feed(decoder.decode(value, { stream: !done })), done && (open = false, __privateGet(this, _parser).reset(), __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this));
6234
- } while (open);
6279
+ value && __privateGet(this, _parser).feed(decoder.decode(value, { stream: !done })), done && (open3 = false, __privateGet(this, _parser).reset(), __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this));
6280
+ } while (open3);
6235
6281
  }), __privateAdd(this, _onFetchError, (err) => {
6236
6282
  __privateSet(this, _controller, void 0), !(err.name === "AbortError" || err.type === "aborted") && __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this, flattenError(err));
6237
6283
  }), __privateAdd(this, _onEvent, (event) => {
@@ -6686,11 +6732,10 @@ import { tmpdir } from "node:os";
6686
6732
  import { join as join2 } from "node:path";
6687
6733
 
6688
6734
  // ../shared/src/domain/attachment.ts
6689
- var isImageAttachment = (att) => att.contentType.startsWith("image/");
6690
6735
  var labelAttachments = (attachments) => {
6691
6736
  const counters = { image: 0, file: 0 };
6692
6737
  return attachments.map((att) => {
6693
- const kind = isImageAttachment(att) ? "image" : "file";
6738
+ const kind = att.contentType.startsWith("image/") ? "image" : "file";
6694
6739
  counters[kind] += 1;
6695
6740
  return `${kind}-${counters[kind]}`;
6696
6741
  });
@@ -6699,6 +6744,77 @@ var labelAttachments = (attachments) => {
6699
6744
  // ../shared/src/domain/channel.ts
6700
6745
  var isMessageFor = (msg, name) => !msg.to || msg.to.length === 0 || msg.to.includes(name);
6701
6746
 
6747
+ // ../shared/src/domain/file.ts
6748
+ var FILE_TEXT_MAX_BYTES = 5 * 1024 * 1024;
6749
+ var FILE_DIRECTORY_MAX_ENTRIES = 2e3;
6750
+ var FILE_SEARCH_MAX_RESULTS = 500;
6751
+ var FILE_SEARCH_DEADLINE_MS = 5e3;
6752
+ var FILE_SEARCH_QUERY_MAX_LENGTH = 200;
6753
+ var FILE_RAW_MAX_BYTES = 5 * 1024 * 1024;
6754
+ var IMAGE_MEDIA_TYPES = {
6755
+ png: "image/png",
6756
+ jpg: "image/jpeg",
6757
+ jpeg: "image/jpeg",
6758
+ gif: "image/gif",
6759
+ webp: "image/webp",
6760
+ avif: "image/avif",
6761
+ bmp: "image/bmp",
6762
+ ico: "image/x-icon",
6763
+ svg: "image/svg+xml"
6764
+ };
6765
+ var RAW_MEDIA_TYPES = {
6766
+ ...IMAGE_MEDIA_TYPES,
6767
+ css: "text/css",
6768
+ woff: "font/woff",
6769
+ woff2: "font/woff2",
6770
+ ttf: "font/ttf",
6771
+ otf: "font/otf"
6772
+ };
6773
+ var mediaTypeIn = (types3) => (path) => {
6774
+ const extension2 = /\.([a-z\d]+)$/i.exec(path)?.[1]?.toLowerCase();
6775
+ return extension2 && types3[extension2] || null;
6776
+ };
6777
+ var imageMediaType = mediaTypeIn(IMAGE_MEDIA_TYPES);
6778
+ var rawMediaType = mediaTypeIn(RAW_MEDIA_TYPES);
6779
+ var normalizeFilePath = (value, allowRoot = false) => {
6780
+ if (value === "") return allowRoot ? "" : null;
6781
+ if (value.includes("\0") || value.includes("\\") || value.startsWith("/")) return null;
6782
+ if (/^[A-Za-z]:/.test(value)) return null;
6783
+ return value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..") ? value : null;
6784
+ };
6785
+ var SENSITIVE_SEGMENTS = /* @__PURE__ */ new Set([".git", ".ssh", ".aws", ".kube", ".gnupg"]);
6786
+ var SENSITIVE_NAMES = /* @__PURE__ */ new Set([
6787
+ ".baton.json",
6788
+ ".envrc",
6789
+ ".npmrc",
6790
+ ".netrc",
6791
+ ".pypirc",
6792
+ ".git-credentials",
6793
+ ".pgpass",
6794
+ ".htpasswd",
6795
+ ".credentials.json"
6796
+ ]);
6797
+ var SENSITIVE_EXTENSIONS = [".pem", ".key", ".p12", ".pfx", ".jks", ".keystore"];
6798
+ var isSensitiveName = (name) => SENSITIVE_NAMES.has(name) || /^\.env(\..+)?$/.test(name) && !/\.(example|sample|template)$/.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || SENSITIVE_EXTENSIONS.some((extension2) => name.endsWith(extension2));
6799
+ var isSensitiveFilePath = (path) => {
6800
+ const segments = path.toLowerCase().split("/");
6801
+ return segments.some((segment) => SENSITIVE_SEGMENTS.has(segment)) || isSensitiveName(segments.at(-1) ?? "");
6802
+ };
6803
+ var EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set([
6804
+ "node_modules",
6805
+ ".pnpm-store",
6806
+ "dist",
6807
+ "build",
6808
+ "coverage",
6809
+ ".next",
6810
+ ".turbo",
6811
+ ".cache",
6812
+ ".venv",
6813
+ "__pycache__",
6814
+ "target"
6815
+ ]);
6816
+ var isExcludedDirectoryName = (name) => EXCLUDED_DIRECTORIES.has(name);
6817
+
6702
6818
  // ../shared/src/domain/service.ts
6703
6819
  var SERVICE_PUBLIC_URL_MAX_LENGTH = 2048;
6704
6820
  var SERVICE_NOTE_MAX_LENGTH = 500;
@@ -6713,6 +6829,12 @@ var isServicePublicUrl = (value) => {
6713
6829
  };
6714
6830
  var isServiceNote = (value) => value.length > 0 && value.length <= SERVICE_NOTE_MAX_LENGTH;
6715
6831
 
6832
+ // ../shared/src/domain/session.ts
6833
+ var AGENT_KINDS = ["claude-code", "codex", "pi"];
6834
+ var isAgentKind = (v) => typeof v === "string" && AGENT_KINDS.includes(v);
6835
+ var EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
6836
+ var isAgentEffort = (v) => typeof v === "string" && EFFORTS.includes(v);
6837
+
6716
6838
  // ../shared/src/domain/session-event.ts
6717
6839
  var agentMessageText = (payload) => {
6718
6840
  if (typeof payload !== "object" || payload === null) return null;
@@ -6726,6 +6848,15 @@ var agentMessageText = (payload) => {
6726
6848
  return trimmed ? { id, text: trimmed } : null;
6727
6849
  };
6728
6850
 
6851
+ // ../shared/src/domain/worker-creation.ts
6852
+ var WORKER_TEMPLATES = [
6853
+ { id: "mini-glm-codeplan", name: "GLM Code Plan", agentKinds: ["claude-code", "pi"] },
6854
+ { id: "mini-dashscope-codeplan", name: "DashScope Code Plan", agentKinds: ["claude-code", "pi"] },
6855
+ { id: "mini-codex", name: "\u672C\u673A Codex", agentKinds: ["codex"] }
6856
+ ];
6857
+ var workerTemplateAgentKinds = (id) => id === "mini-local" ? AGENT_KINDS : WORKER_TEMPLATES.find((template) => template.id === id)?.agentKinds ?? [];
6858
+ var isWorkerTemplateId = (value) => typeof value === "string" && workerTemplateAgentKinds(value).length > 0;
6859
+
6729
6860
  // src/commands/channel/listen.ts
6730
6861
  var PREVIEW = 280;
6731
6862
  var describe = (e) => {
@@ -6776,10 +6907,10 @@ var runListen = async (o) => {
6776
6907
  onError: (e) => emit({ type: "channel.error", error: describe(e) })
6777
6908
  });
6778
6909
  emit({ type: "channel.listening", channel: o.channel, from: o.from, url: o.url });
6779
- await new Promise((resolve3) => {
6910
+ await new Promise((resolve4) => {
6780
6911
  const stop = () => {
6781
6912
  close();
6782
- resolve3();
6913
+ resolve4();
6783
6914
  };
6784
6915
  process.on("SIGINT", stop);
6785
6916
  process.on("SIGTERM", stop);
@@ -7034,10 +7165,6 @@ var init = defineCommand({
7034
7165
  });
7035
7166
 
7036
7167
  // src/commands/session/shared.ts
7037
- import { homedir } from "node:os";
7038
- import { join as join3 } from "node:path";
7039
- var defaultWorktreeDir = (env = process.env) => env.BATON_WORKTREE_DIR ?? join3(env.XDG_DATA_HOME ?? join3(env.HOME ?? homedir(), ".local/share"), "baton", "worktrees");
7040
- var slug = (s) => s.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
7041
7168
  var resolveSession = async (client, projectId, handle) => {
7042
7169
  const asInt = Number(handle);
7043
7170
  if (Number.isInteger(asInt) && asInt > 0) {
@@ -7190,6 +7317,34 @@ var loop = defineCommand({
7190
7317
  }
7191
7318
  });
7192
7319
 
7320
+ // src/commands/notify.ts
7321
+ var describe2 = (result) => {
7322
+ if (result.status === "sent") return `notified ${result.devices} device(s)`;
7323
+ if (result.status === "throttled")
7324
+ return `throttled: this session can notify again in ${result.retryAfterSec}s`;
7325
+ return "nobody to notify: no human has messaged this session, or they have not enabled notifications";
7326
+ };
7327
+ var notify = defineCommand({
7328
+ meta: {
7329
+ name: "notify",
7330
+ description: "Push a phone notification to the person working with this session. Use it only when a human needs to act now: something is wrong, a decision is needed, or the work is blocked. Do not use it for routine progress, and not to announce that a turn finished (that is already pushed). At most one notification per session every 10 minutes; text is cut at 200 characters."
7331
+ },
7332
+ args: {
7333
+ text: { type: "positional", required: true, description: "one short line for the lock screen" },
7334
+ ...common
7335
+ },
7336
+ run: async ({ args }) => {
7337
+ const raw = process.env.BATON_SESSION_ID;
7338
+ if (raw === void 0)
7339
+ throw new Error("baton notify only works inside a baton session (BATON_SESSION_ID is unset)");
7340
+ const sessionId = Number(raw);
7341
+ if (!Number.isSafeInteger(sessionId) || sessionId <= 0)
7342
+ throw new Error("invalid BATON_SESSION_ID");
7343
+ const result = await clientFor(args).sessions.notify(sessionId, args.text);
7344
+ console.log(args.json ? toJson(result) : describe2(result));
7345
+ }
7346
+ });
7347
+
7193
7348
  // src/commands/project.ts
7194
7349
  var createProject = (c, input, json) => c.projects.create(input).then((p) => renderOne(p, fmtProject, json));
7195
7350
  var listProjects = (c, workspaceId, json) => c.projects.listByWorkspace(workspaceId).then((ps) => renderList(ps, fmtProject, json));
@@ -7264,10 +7419,519 @@ var project = defineCommand({
7264
7419
  }
7265
7420
  });
7266
7421
 
7422
+ // src/provisioner/config.ts
7423
+ import { execFile } from "node:child_process";
7424
+ import { constants } from "node:fs";
7425
+ import { access, readFile, stat as stat2 } from "node:fs/promises";
7426
+ import { isAbsolute } from "node:path";
7427
+ import { promisify } from "node:util";
7428
+ var forbiddenEnv = /* @__PURE__ */ new Set([
7429
+ "BATON_PROVISIONER_TOKEN",
7430
+ "BATON_TOKEN",
7431
+ "BATON_WORKER_TOKEN",
7432
+ "BATON_USER",
7433
+ "BATON_PASS",
7434
+ "HOME",
7435
+ "XDG_DATA_HOME"
7436
+ ]);
7437
+ var object = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7438
+ var absolutePath = (value, field) => {
7439
+ if (typeof value !== "string" || !isAbsolute(value) || value.includes("\0"))
7440
+ throw new Error(`${field} must be an absolute path`);
7441
+ return value;
7442
+ };
7443
+ var environment = (value) => {
7444
+ if (!object(value)) throw new Error("workerEnv must be an object");
7445
+ return Object.fromEntries(
7446
+ Object.entries(value).map(([key, entry]) => {
7447
+ if (forbiddenEnv.has(key) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof entry !== "string" || entry.includes("\0"))
7448
+ throw new Error(`invalid worker environment key: ${key}`);
7449
+ return [key, entry];
7450
+ })
7451
+ );
7452
+ };
7453
+ var templates = (value) => {
7454
+ if (!object(value)) throw new Error("templates must be an object");
7455
+ return Object.fromEntries(
7456
+ WORKER_TEMPLATES.map((template) => {
7457
+ const raw = value[template.id];
7458
+ if (!object(raw)) throw new Error(`template ${template.id} must be configured`);
7459
+ const workerEnv2 = environment(raw.workerEnv);
7460
+ if (template.id === "mini-codex") {
7461
+ if (raw.pi !== void 0) throw new Error("mini-codex does not support pi");
7462
+ return [template.id, { workerEnv: workerEnv2 }];
7463
+ }
7464
+ const provider = template.id === "mini-glm-codeplan" ? "zai-coding-cn" : "qwen-token-plan-cn";
7465
+ const apiKey = template.id === "mini-glm-codeplan" ? "ZAI_CODING_CN_API_KEY" : "QWEN_TOKEN_PLAN_CN_API_KEY";
7466
+ if (!workerEnv2.ANTHROPIC_BASE_URL || !(workerEnv2.ANTHROPIC_AUTH_TOKEN || workerEnv2.ANTHROPIC_API_KEY) || !workerEnv2[apiKey])
7467
+ throw new Error(`template ${template.id} requires Claude and Pi credentials`);
7468
+ if (!object(raw.pi) || raw.pi.provider !== provider || typeof raw.pi.model !== "string" || !raw.pi.model.trim())
7469
+ throw new Error(`template ${template.id} requires its Pi provider and model`);
7470
+ return [template.id, { workerEnv: workerEnv2, pi: { provider, model: raw.pi.model.trim() } }];
7471
+ })
7472
+ );
7473
+ };
7474
+ var commandEnvironment = (config) => ({
7475
+ ...process.env,
7476
+ PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin"
7477
+ });
7478
+ var loadProvisionerConfig = async (path) => {
7479
+ if (process.platform !== "darwin") throw new Error("provisioner requires macOS");
7480
+ absolutePath(path, "config");
7481
+ if ((await stat2(path)).mode & 63) throw new Error("config must be private: chmod 600 <config>");
7482
+ const text = await readFile(path, "utf8");
7483
+ let raw;
7484
+ try {
7485
+ raw = JSON.parse(text);
7486
+ } catch (error) {
7487
+ if (!(error instanceof SyntaxError)) throw error;
7488
+ throw new Error("invalid provisioner JSON config");
7489
+ }
7490
+ if (!object(raw) || typeof raw.server !== "string" || typeof raw.token !== "string")
7491
+ throw new Error("server and token required");
7492
+ let server;
7493
+ try {
7494
+ server = new URL(raw.server);
7495
+ } catch (error) {
7496
+ if (!(error instanceof TypeError)) throw error;
7497
+ throw new Error("server must be a valid HTTP(S) URL");
7498
+ }
7499
+ if (!["http:", "https:"].includes(server.protocol) || server.username || server.password || server.search || server.hash)
7500
+ throw new Error("server must be an HTTP(S) URL without credentials, query, or fragment");
7501
+ if (!raw.token.trim() || /[\r\n]/.test(raw.token))
7502
+ throw new Error("valid provisioner token required");
7503
+ const config = {
7504
+ server: server.toString().replace(/\/$/, ""),
7505
+ token: raw.token.trim(),
7506
+ projectsRoot: absolutePath(raw.projectsRoot, "projectsRoot"),
7507
+ cliPath: absolutePath(raw.cliPath, "cliPath"),
7508
+ nodePath: absolutePath(raw.nodePath ?? process.execPath, "nodePath"),
7509
+ workerEnv: environment(raw.workerEnv),
7510
+ templates: templates(raw.templates)
7511
+ };
7512
+ await access(config.cliPath, constants.R_OK);
7513
+ await access(config.nodePath ?? process.execPath, constants.X_OK);
7514
+ const env = commandEnvironment(config);
7515
+ await promisify(execFile)("git", ["--version"], { env, timeout: 1e4 });
7516
+ await promisify(execFile)("launchctl", ["help"], { env, timeout: 1e4 });
7517
+ await promisify(execFile)("/usr/bin/plutil", ["-help"], { timeout: 1e4 });
7518
+ return config;
7519
+ };
7520
+
7521
+ // src/provisioner/client.ts
7522
+ var createProvisionerClient = (config) => {
7523
+ const request2 = async (path, token, body, signal) => {
7524
+ const timeout = AbortSignal.timeout(1e4);
7525
+ const response = await fetch(`${config.server}${path}`, {
7526
+ method: body === void 0 ? "GET" : "POST",
7527
+ headers: {
7528
+ authorization: `Bearer ${token}`,
7529
+ ...body === void 0 ? {} : { "content-type": "application/json" }
7530
+ },
7531
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
7532
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
7533
+ });
7534
+ if (!response.ok) {
7535
+ await response.body?.cancel();
7536
+ throw new HttpError(response.status, `provisioner request ${path}: HTTP ${response.status}`);
7537
+ }
7538
+ return await response.json();
7539
+ };
7540
+ return {
7541
+ get: (id, signal) => request2(`/provisioner/worker-creations/${id}`, config.token, void 0, signal),
7542
+ report: (id, report, signal) => request2(`/provisioner/worker-creations/${id}/progress`, config.token, report, signal),
7543
+ register: (id, hostname2, signal) => request2(`/provisioner/worker-creations/${id}/worker`, config.token, { hostname: hostname2 }, signal),
7544
+ worker: (id, token, signal) => request2(`/workers/${id}`, token, void 0, signal)
7545
+ };
7546
+ };
7547
+
7548
+ // src/provisioner/executor.ts
7549
+ import { execFile as execFile3 } from "node:child_process";
7550
+ import { lstat, mkdir as mkdir3, realpath, writeFile as writeFile2 } from "node:fs/promises";
7551
+ import { hostname } from "node:os";
7552
+ import { join as join5 } from "node:path";
7553
+ import { setTimeout as setTimeout2 } from "node:timers/promises";
7554
+ import { promisify as promisify3 } from "node:util";
7555
+
7556
+ // src/provisioner/launchd.ts
7557
+ import { execFile as execFile2 } from "node:child_process";
7558
+ import { mkdir, open } from "node:fs/promises";
7559
+ import { homedir } from "node:os";
7560
+ import { dirname, join as join3 } from "node:path";
7561
+ import { promisify as promisify2 } from "node:util";
7562
+ var workerStatePath = (config, creation) => join3(
7563
+ config.projectsRoot,
7564
+ ".worker-state",
7565
+ `project-${creation.projectId}`,
7566
+ `creation-${creation.id}`
7567
+ );
7568
+ var workerLogPath = (creation) => join3(homedir(), "Library/Logs", `baton-worker-creation-${creation.id}.log`);
7569
+ var startAgent = async (config, agent, signal) => {
7570
+ signal?.throwIfAborted();
7571
+ const plist = join3(homedir(), "Library/LaunchAgents", `${agent.label}.plist`);
7572
+ await mkdir(dirname(plist), { recursive: true });
7573
+ await mkdir(dirname(agent.log), { recursive: true });
7574
+ const domain = `gui/${process.getuid?.()}`;
7575
+ const env = commandEnvironment(config);
7576
+ try {
7577
+ await promisify2(execFile2)("launchctl", ["print", `${domain}/${agent.label}`], {
7578
+ env,
7579
+ timeout: 1e4,
7580
+ signal
7581
+ });
7582
+ throw new Error(
7583
+ `launchd service already exists: ${agent.label}; stop it manually before installing`
7584
+ );
7585
+ } catch (error) {
7586
+ if (!(error instanceof Error && "code" in error && error.code === 113 && "stderr" in error && typeof error.stderr === "string" && error.stderr.includes("Could not find service")))
7587
+ throw error;
7588
+ }
7589
+ const handle = await open(plist, "wx", 384);
7590
+ try {
7591
+ const payload = {
7592
+ Label: agent.label,
7593
+ ProgramArguments: agent.args,
7594
+ WorkingDirectory: agent.cwd,
7595
+ EnvironmentVariables: agent.env,
7596
+ RunAtLoad: true,
7597
+ KeepAlive: true,
7598
+ ThrottleInterval: 10,
7599
+ StandardOutPath: agent.log,
7600
+ StandardErrorPath: agent.log
7601
+ };
7602
+ await handle.writeFile(JSON.stringify(payload));
7603
+ } finally {
7604
+ await handle.close();
7605
+ }
7606
+ const log = await open(agent.log, "a", 384);
7607
+ await log.close();
7608
+ try {
7609
+ await promisify2(execFile2)("/usr/bin/plutil", ["-convert", "xml1", plist], {
7610
+ timeout: 1e4,
7611
+ signal
7612
+ });
7613
+ await promisify2(execFile2)("/usr/bin/plutil", ["-lint", plist], { timeout: 1e4, signal });
7614
+ await promisify2(execFile2)("launchctl", ["bootstrap", domain, plist], {
7615
+ env,
7616
+ timeout: 1e4,
7617
+ signal
7618
+ });
7619
+ } catch {
7620
+ throw new Error(`launchd start failed; inspect ${plist} and ${agent.log}`);
7621
+ }
7622
+ };
7623
+ var launchWorkerAgent = async (config, creation, repoPath, workerEnv2, signal) => startAgent(
7624
+ config,
7625
+ {
7626
+ label: `dev.fmap.baton-worker-creation-${creation.id}`,
7627
+ args: [config.nodePath ?? process.execPath, config.cliPath, "worker", "run"],
7628
+ cwd: repoPath,
7629
+ env: {
7630
+ PATH: workerEnv2.PATH ?? process.env.PATH ?? "/usr/bin:/bin",
7631
+ ...workerEnv2,
7632
+ HOME: homedir(),
7633
+ XDG_DATA_HOME: workerStatePath(config, creation)
7634
+ },
7635
+ log: workerLogPath(creation)
7636
+ },
7637
+ signal
7638
+ );
7639
+ var installProvisionerAgent = async (config, configPath) => startAgent(config, {
7640
+ label: "dev.fmap.baton-provisioner",
7641
+ args: [
7642
+ config.nodePath ?? process.execPath,
7643
+ config.cliPath,
7644
+ "provisioner",
7645
+ "run",
7646
+ "--config",
7647
+ configPath
7648
+ ],
7649
+ cwd: dirname(configPath),
7650
+ env: { HOME: homedir(), PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin" },
7651
+ log: join3(homedir(), "Library/Logs/baton-provisioner.log")
7652
+ });
7653
+
7654
+ // src/provisioner/worker-template.ts
7655
+ import { mkdir as mkdir2, writeFile } from "node:fs/promises";
7656
+ import { join as join4 } from "node:path";
7657
+ var workerTemplateEnvironment = async (config, creation, state) => {
7658
+ if (!workerTemplateAgentKinds(creation.templateId).includes(creation.agentKind))
7659
+ throw new Error("agentKind is not supported by this template");
7660
+ if (creation.templateId === "mini-local") return config.workerEnv;
7661
+ const profile = config.templates[creation.templateId];
7662
+ if (!profile) throw new Error(`template ${creation.templateId} is not configured`);
7663
+ if (creation.agentKind !== "pi") return profile.workerEnv;
7664
+ if (!profile.pi) throw new Error("Pi provider and model are not configured");
7665
+ const agentDir = join4(state, "pi");
7666
+ await mkdir2(agentDir, { mode: 448 });
7667
+ await writeFile(
7668
+ join4(agentDir, "settings.json"),
7669
+ JSON.stringify({
7670
+ defaultProvider: profile.pi.provider,
7671
+ defaultModel: profile.pi.model
7672
+ }),
7673
+ { flag: "wx", mode: 384 }
7674
+ );
7675
+ return { ...profile.workerEnv, PI_CODING_AGENT_DIR: agentDir };
7676
+ };
7677
+
7678
+ // src/provisioner/executor.ts
7679
+ var directory = async (path) => {
7680
+ try {
7681
+ await mkdir3(path, { mode: 448 });
7682
+ } catch (error) {
7683
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
7684
+ }
7685
+ const info = await lstat(path);
7686
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`unsafe directory: ${path}`);
7687
+ };
7688
+ var provisionWorker = async (config, creation, client, signal) => {
7689
+ const repoPath = join5(
7690
+ config.projectsRoot,
7691
+ `project-${creation.projectId}`,
7692
+ `creation-${creation.id}`
7693
+ );
7694
+ const progress = (step) => client.report(creation.id, { status: "running", step, repoPath }, signal);
7695
+ await progress("directory");
7696
+ signal.throwIfAborted();
7697
+ await mkdir3(config.projectsRoot, { recursive: true, mode: 448 });
7698
+ const root = await realpath(config.projectsRoot);
7699
+ await directory(join5(root, `project-${creation.projectId}`));
7700
+ await mkdir3(join5(root, `project-${creation.projectId}`, `creation-${creation.id}`), {
7701
+ mode: 448
7702
+ });
7703
+ await progress("git");
7704
+ const git2 = async (args) => {
7705
+ try {
7706
+ await promisify3(execFile3)("git", ["-C", repoPath, ...args], {
7707
+ env: commandEnvironment(config),
7708
+ signal,
7709
+ timeout: 1e4
7710
+ });
7711
+ } catch (error) {
7712
+ signal.throwIfAborted();
7713
+ throw new Error("Git repository initialization failed", { cause: error });
7714
+ }
7715
+ };
7716
+ await git2(["init", "-b", "main"]);
7717
+ await writeFile2(join5(repoPath, ".git/info/exclude"), "\n.baton.json\n", { flag: "a" });
7718
+ await git2([
7719
+ "-c",
7720
+ "user.name=Baton",
7721
+ "-c",
7722
+ "user.email=baton@localhost",
7723
+ "commit",
7724
+ "--allow-empty",
7725
+ "-m",
7726
+ "chore: initialize repository"
7727
+ ]);
7728
+ await progress("register");
7729
+ const result = await client.register(creation.id, hostname(), signal);
7730
+ const state = workerStatePath(config, creation);
7731
+ await directory(join5(root, ".worker-state"));
7732
+ await directory(join5(root, ".worker-state", `project-${creation.projectId}`));
7733
+ await mkdir3(state, { mode: 448 });
7734
+ await mkdir3(join5(state, "baton"), { mode: 448 });
7735
+ await writeFile2(join5(state, "baton/machine-id"), `${creation.machineId}
7736
+ `, {
7737
+ flag: "wx",
7738
+ mode: 384
7739
+ });
7740
+ const cfg = workerProjectConfig({
7741
+ server: config.server,
7742
+ projectId: creation.projectId,
7743
+ workerId: result.worker.id,
7744
+ name: result.worker.name,
7745
+ agentKind: result.worker.agentKind,
7746
+ machineId: result.worker.machineId,
7747
+ apiToken: result.apiToken
7748
+ });
7749
+ await writeFile2(join5(repoPath, ".baton.json"), `${JSON.stringify(cfg, null, 2)}
7750
+ `, {
7751
+ flag: "wx",
7752
+ mode: 384
7753
+ });
7754
+ await progress("start");
7755
+ signal.throwIfAborted();
7756
+ const workerEnv2 = await workerTemplateEnvironment(config, creation, state);
7757
+ await launchWorkerAgent(config, creation, repoPath, workerEnv2, signal);
7758
+ await progress("connect");
7759
+ const deadline = Date.now() + 6e4;
7760
+ while (Date.now() < deadline) {
7761
+ const worker2 = await client.worker(result.worker.id, result.apiToken, signal);
7762
+ if (worker2.connected) {
7763
+ await client.report(creation.id, { status: "succeeded" }, signal);
7764
+ return;
7765
+ }
7766
+ await setTimeout2(1e3, void 0, { signal });
7767
+ }
7768
+ throw new Error("Worker did not connect within 60 seconds");
7769
+ };
7770
+
7771
+ // src/provisioner/daemon.ts
7772
+ var taskError = (error, config) => {
7773
+ const message = error instanceof Error ? error.message : "unknown creation error";
7774
+ return [
7775
+ config.token,
7776
+ ...Object.values(config.workerEnv),
7777
+ ...Object.values(config.templates).flatMap((template) => Object.values(template.workerEnv))
7778
+ ].filter((value) => value.length > 0).reduce((safe, secret) => safe.replaceAll(secret, "[redacted]"), message);
7779
+ };
7780
+ var processCreation = async (id, config, client, signal) => {
7781
+ let creation;
7782
+ try {
7783
+ creation = await client.get(id, signal);
7784
+ } catch (error) {
7785
+ if (error instanceof HttpError && error.status === 404) {
7786
+ console.error(`[provisioner] creation ${id} no longer exists`);
7787
+ return;
7788
+ }
7789
+ throw error;
7790
+ }
7791
+ if (creation.status === "succeeded" || creation.status === "failed") return;
7792
+ if (creation.status === "running") {
7793
+ await client.report(
7794
+ id,
7795
+ {
7796
+ status: "failed",
7797
+ error: "Provisioner interrupted; inspect retained resources before creating again"
7798
+ },
7799
+ signal
7800
+ );
7801
+ return;
7802
+ }
7803
+ try {
7804
+ await provisionWorker(config, creation, client, signal);
7805
+ } catch (error) {
7806
+ if (signal.aborted) {
7807
+ console.error(`[provisioner] creation ${id} interrupted: ${taskError(error, config)}`);
7808
+ return;
7809
+ }
7810
+ if (!(error instanceof HttpError) || ![401, 403].includes(error.status)) {
7811
+ const message = `${taskError(error, config)}; worker log: ${workerLogPath(creation)}`;
7812
+ console.error(`[provisioner] creation ${id}: ${message}`);
7813
+ await client.report(id, { status: "failed", error: message }, signal);
7814
+ return;
7815
+ }
7816
+ throw error;
7817
+ }
7818
+ };
7819
+ var runProvisioner = async (config, signal) => {
7820
+ const client = createProvisionerClient(config);
7821
+ const abort = new AbortController();
7822
+ const queued = /* @__PURE__ */ new Set();
7823
+ let tail = Promise.resolve();
7824
+ let fatal;
7825
+ let finish = () => {
7826
+ };
7827
+ const ended = new Promise((resolve4) => {
7828
+ finish = resolve4;
7829
+ });
7830
+ const stream = new EventSource(`${config.server}/provisioner/stream`, {
7831
+ fetch: (url, init2) => fetch(url, {
7832
+ ...init2,
7833
+ headers: { ...init2?.headers, authorization: `Bearer ${config.token}` },
7834
+ signal: abort.signal
7835
+ })
7836
+ });
7837
+ const stop = () => {
7838
+ stream.close();
7839
+ abort.abort();
7840
+ finish();
7841
+ };
7842
+ const fail = (error) => {
7843
+ fatal = error;
7844
+ console.error(`[provisioner] ${taskError(error, config)}`);
7845
+ stop();
7846
+ };
7847
+ stream.onmessage = (event) => {
7848
+ try {
7849
+ const message = JSON.parse(event.data);
7850
+ if (!message || !Number.isSafeInteger(message.creationId) || message.creationId <= 0 || !["create", "inspect"].includes(message.action))
7851
+ throw new Error("invalid provisioner signal");
7852
+ if (queued.has(message.creationId)) return;
7853
+ queued.add(message.creationId);
7854
+ tail = tail.then(async () => {
7855
+ try {
7856
+ if (!abort.signal.aborted)
7857
+ await processCreation(message.creationId, config, client, abort.signal);
7858
+ } finally {
7859
+ queued.delete(message.creationId);
7860
+ }
7861
+ }).catch((error) => {
7862
+ if (!abort.signal.aborted) fail(error);
7863
+ else console.error(`[provisioner] stopped: ${taskError(error, config)}`);
7864
+ });
7865
+ } catch (error) {
7866
+ fail(error);
7867
+ }
7868
+ };
7869
+ stream.onerror = (event) => {
7870
+ if (stream.readyState === EventSource.CLOSED || [401, 403, 409].includes(event.code ?? 0))
7871
+ fail(new Error(`provisioner stream rejected: HTTP ${event.code}`));
7872
+ else
7873
+ console.error(`[provisioner] stream disconnected (${event.code ?? "network"}); reconnecting`);
7874
+ };
7875
+ signal.addEventListener("abort", stop, { once: true });
7876
+ if (signal.aborted) stop();
7877
+ console.log("[provisioner] waiting for worker creation requests");
7878
+ try {
7879
+ await ended;
7880
+ await tail;
7881
+ } finally {
7882
+ stop();
7883
+ signal.removeEventListener("abort", stop);
7884
+ }
7885
+ if (fatal !== void 0) throw new Error(taskError(fatal, config));
7886
+ };
7887
+
7888
+ // src/commands/provisioner.ts
7889
+ var configArg = {
7890
+ config: {
7891
+ type: "string",
7892
+ required: true,
7893
+ description: "absolute path to private provisioner JSON config"
7894
+ }
7895
+ };
7896
+ var provisioner = defineCommand({
7897
+ meta: {
7898
+ name: "provisioner",
7899
+ description: "prepare local repositories and launch workers on macOS"
7900
+ },
7901
+ subCommands: {
7902
+ run: defineCommand({
7903
+ args: configArg,
7904
+ run: async ({ args }) => {
7905
+ const config = await loadProvisionerConfig(args.config);
7906
+ const abort = new AbortController();
7907
+ const stop = () => abort.abort();
7908
+ process.on("SIGINT", stop);
7909
+ process.on("SIGTERM", stop);
7910
+ try {
7911
+ await runProvisioner(config, abort.signal);
7912
+ } finally {
7913
+ process.off("SIGINT", stop);
7914
+ process.off("SIGTERM", stop);
7915
+ }
7916
+ }
7917
+ }),
7918
+ install: defineCommand({
7919
+ args: configArg,
7920
+ run: async ({ args }) => {
7921
+ const config = await loadProvisionerConfig(args.config);
7922
+ await installProvisionerAgent(config, args.config);
7923
+ console.log(
7924
+ "installed dev.fmap.baton-provisioner; inspect ~/Library/Logs/baton-provisioner.log"
7925
+ );
7926
+ }
7927
+ })
7928
+ }
7929
+ });
7930
+
7267
7931
  // src/commands/relay.ts
7268
7932
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
7269
7933
  import { tmpdir as tmpdir2 } from "node:os";
7270
- import { join as join4 } from "node:path";
7934
+ import { join as join6 } from "node:path";
7271
7935
 
7272
7936
  // src/client/relay.ts
7273
7937
  var relayClient = (baseUrl) => {
@@ -7395,11 +8059,11 @@ var listenCommand2 = defineCommand({
7395
8059
  process.stdout.write(`${JSON.stringify(o)}
7396
8060
  `);
7397
8061
  };
7398
- const spillDir = join4(tmpdir2(), "hotline", args.channel);
8062
+ const spillDir = join6(tmpdir2(), "hotline", args.channel);
7399
8063
  mkdirSync2(spillDir, { recursive: true });
7400
8064
  const surface = (m) => {
7401
8065
  const text = typeof m.text === "string" ? m.text : "";
7402
- const file = join4(spillDir, `${m.seq}.txt`);
8066
+ const file = join6(spillDir, `${m.seq}.txt`);
7403
8067
  writeFileSync4(file, text);
7404
8068
  const truncated = text.length > PREVIEW2;
7405
8069
  emit({
@@ -7413,7 +8077,7 @@ var listenCommand2 = defineCommand({
7413
8077
  file
7414
8078
  });
7415
8079
  };
7416
- const describe2 = (e) => {
8080
+ const describe3 = (e) => {
7417
8081
  const ev = e;
7418
8082
  return ev?.code ? `${ev.code}${ev.message ? ` ${ev.message}` : ""}` : String(e);
7419
8083
  };
@@ -7423,13 +8087,13 @@ var listenCommand2 = defineCommand({
7423
8087
  onMessage: (m) => {
7424
8088
  if (m.from !== me) surface(m);
7425
8089
  },
7426
- onError: (e) => emit({ type: "relay.error", error: describe2(e) })
8090
+ onError: (e) => emit({ type: "relay.error", error: describe3(e) })
7427
8091
  });
7428
8092
  emit({ type: "relay.listening", channel: args.channel, from: me, url });
7429
- await new Promise((resolve3) => {
8093
+ await new Promise((resolve4) => {
7430
8094
  const stop = () => {
7431
8095
  close();
7432
- resolve3();
8096
+ resolve4();
7433
8097
  };
7434
8098
  process.on("SIGINT", stop);
7435
8099
  process.on("SIGTERM", stop);
@@ -7553,6 +8217,9 @@ var requirement = defineCommand({
7553
8217
  }
7554
8218
  });
7555
8219
 
8220
+ // src/commands/service.ts
8221
+ import { resolve as resolve2 } from "node:path";
8222
+
7556
8223
  // src/worker/service-child.ts
7557
8224
  import { spawn } from "node:child_process";
7558
8225
 
@@ -7633,25 +8300,15 @@ var formatUptime = (startedAt, now = Date.now()) => {
7633
8300
  if (seconds >= 60) return `${Math.floor(seconds / 60)}m`;
7634
8301
  return `${seconds}s`;
7635
8302
  };
7636
- var currentContext = () => {
7637
- const config = loadProjectConfig(projectConfigPath());
7638
- if (!config.worker || !config.session)
7639
- throw new Error("service run requires a Baton session worktree");
7640
- return { workerId: config.worker.id, sessionId: config.session };
7641
- };
7642
- var currentWorker = () => {
7643
- const worker2 = loadProjectConfig(projectConfigPath()).worker;
7644
- if (!worker2) throw new Error("no worker in scope");
7645
- return worker2.id;
7646
- };
7647
8303
  var parseTarget = (target) => {
7648
8304
  const explicit = target.match(/^W-(\d+)\/(.+)$/i);
7649
- return explicit ? { workerId: Number(explicit[1]), name: explicit[2] ?? "" } : { workerId: currentWorker(), name: target };
8305
+ return explicit ? { workerId: Number(explicit[1]), name: explicit[2] ?? "" } : { workerId: resolveWorkerId(), name: target };
7650
8306
  };
7651
8307
  var runCommand2 = defineCommand({
7652
8308
  meta: { name: "run", description: "run a service on the current worker" },
7653
8309
  args: {
7654
8310
  name: { type: "positional", required: true, description: "service name" },
8311
+ cwd: { type: "string", required: true, description: "service working directory on the Worker" },
7655
8312
  "public-url": { type: "string", description: "public HTTP(S) URL for this service" },
7656
8313
  note: { type: "string", description: "short note describing this service" },
7657
8314
  ...common
@@ -7666,9 +8323,9 @@ var runCommand2 = defineCommand({
7666
8323
  throw new Error("--public-url must be an absolute HTTP(S) URL");
7667
8324
  if (note && !isServiceNote(note))
7668
8325
  throw new Error("service note must be 500 characters or fewer");
7669
- const context = currentContext();
7670
- const result = await clientFor(args).services.run(context.workerId, {
7671
- sessionId: context.sessionId,
8326
+ const workerId = resolveWorkerId();
8327
+ const result = await clientFor(args).services.run(workerId, {
8328
+ cwd: resolve2(args.cwd),
7672
8329
  name: args.name,
7673
8330
  argv,
7674
8331
  ...publicUrl ? { publicUrl } : {},
@@ -7676,7 +8333,7 @@ var runCommand2 = defineCommand({
7676
8333
  });
7677
8334
  if (!result.ok) throw new Error(result.error);
7678
8335
  if (args.json) return console.log(toJson(result));
7679
- console.log(`running W-${context.workerId}/${args.name}`);
8336
+ console.log(`running W-${workerId}/${args.name}`);
7680
8337
  if (publicUrl) console.log(`public: ${publicUrl}`);
7681
8338
  if (result.logPath) console.log(`logs: ${result.logPath}`);
7682
8339
  }
@@ -7693,7 +8350,7 @@ var lsCommand = defineCommand({
7693
8350
  const workers = await client.workers.listByProject(projectId);
7694
8351
  const names = new Map(workers.map((worker2) => [worker2.id, worker2.name]));
7695
8352
  services.sort((a, b) => a.workerId - b.workerId || a.name.localeCompare(b.name)).map(
7696
- (item) => `${serviceHandle(item)} ${names.get(item.workerId) ?? "unknown"} session=${item.sessionId} uptime=${formatUptime(item.startedAt)}`
8353
+ (item) => `${serviceHandle(item)} ${names.get(item.workerId) ?? "unknown"} cwd=${item.cwd} uptime=${formatUptime(item.startedAt)}`
7697
8354
  ).forEach((line) => {
7698
8355
  console.log(line);
7699
8356
  });
@@ -7743,7 +8400,7 @@ var sessionAbortCommand = defineCommand({
7743
8400
  var sessionClearCommand = defineCommand({
7744
8401
  meta: {
7745
8402
  name: "clear",
7746
- description: "reset the conversation context (keeps the session + worktree)"
8403
+ description: "reset the conversation context (keeps the session + directory)"
7747
8404
  },
7748
8405
  args: {
7749
8406
  session: { type: "positional", required: true, description: "session int id or name" },
@@ -7852,7 +8509,7 @@ var sessionResumeCommand = defineCommand({
7852
8509
 
7853
8510
  // src/commands/session/rm.ts
7854
8511
  var sessionRmCommand = defineCommand({
7855
- meta: { name: "rm", description: "delete a session (irreversible; removes its worktree)" },
8512
+ meta: { name: "rm", description: "delete a session (irreversible; removes its runtime files)" },
7856
8513
  args: {
7857
8514
  session: { type: "positional", required: true, description: "session int id or name" },
7858
8515
  project: { type: "string", description: "project id (overrides .baton.json)" },
@@ -7863,7 +8520,9 @@ var sessionRmCommand = defineCommand({
7863
8520
  const c = clientFor(args);
7864
8521
  const handle = await resolveSession(c, resolveProjectId(args), args.session);
7865
8522
  if (!args.confirm) {
7866
- console.log(`[dry-run] would delete session ${handle.name} (#${handle.id}) + its worktree`);
8523
+ console.log(
8524
+ `[dry-run] would delete session ${handle.name} (#${handle.id}) + its runtime files`
8525
+ );
7867
8526
  console.log("re-run with --confirm to proceed.");
7868
8527
  return;
7869
8528
  }
@@ -7879,12 +8538,12 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
7879
8538
  // src/session/runner/transcript.ts
7880
8539
  import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
7881
8540
  import { homedir as homedir2 } from "node:os";
7882
- import { join as join5 } from "node:path";
8541
+ import { join as join7 } from "node:path";
7883
8542
  var findTranscriptPath = (agentSessionId) => {
7884
- const root = join5(homedir2(), ".claude", "projects");
8543
+ const root = join7(homedir2(), ".claude", "projects");
7885
8544
  try {
7886
8545
  for (const dir of readdirSync(root)) {
7887
- const candidate = join5(root, dir, `${agentSessionId}.jsonl`);
8546
+ const candidate = join7(root, dir, `${agentSessionId}.jsonl`);
7888
8547
  try {
7889
8548
  if (statSync(candidate).isFile()) return candidate;
7890
8549
  } catch {
@@ -7962,16 +8621,15 @@ var readFirstExchange = (agentSessionId) => {
7962
8621
  };
7963
8622
 
7964
8623
  // src/session/runner/turn.ts
7965
- import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
7966
- import { join as join7 } from "node:path";
8624
+ import { mkdir as mkdir5, writeFile as writeFile3 } from "node:fs/promises";
8625
+ import { join as join12 } from "node:path";
7967
8626
 
7968
8627
  // src/session/runner/attachments.ts
7969
8628
  import { createWriteStream } from "node:fs";
7970
- import { mkdir, writeFile } from "node:fs/promises";
7971
- import { join as join6 } from "node:path";
8629
+ import { mkdir as mkdir4 } from "node:fs/promises";
8630
+ import { join as join8 } from "node:path";
7972
8631
  import { Readable } from "node:stream";
7973
8632
  import { pipeline } from "node:stream/promises";
7974
- var ATTACH_DIR = "attachments";
7975
8633
  var dedupe = (name, taken) => {
7976
8634
  if (!taken.has(name)) {
7977
8635
  taken.add(name);
@@ -7991,25 +8649,24 @@ var dedupe = (name, taken) => {
7991
8649
  };
7992
8650
  var materializeAttachments = async (opts) => {
7993
8651
  const fetchImpl = opts.fetchImpl ?? fetch;
7994
- const dir = join6(opts.worktreePath, ATTACH_DIR);
7995
- await mkdir(dir, { recursive: true });
7996
- await writeFile(join6(dir, ".gitignore"), "*\n", "utf8");
7997
- const taken = /* @__PURE__ */ new Set([".gitignore"]);
7998
- const relPaths = [];
8652
+ const dir = opts.directory;
8653
+ await mkdir4(dir, { recursive: true });
8654
+ const taken = /* @__PURE__ */ new Set();
8655
+ const paths = [];
7999
8656
  for (const att of opts.attachments) {
8000
8657
  const name = dedupe(att.filename, taken);
8001
8658
  const res = await fetchImpl(`${opts.serverBase}${att.url}`);
8002
8659
  if (!res.ok || !res.body) throw new Error(`attachment ${att.id} \u2192 ${res.status}`);
8003
- await pipeline(Readable.fromWeb(res.body), createWriteStream(join6(dir, name)));
8004
- relPaths.push(`${ATTACH_DIR}/${name}`);
8660
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(join8(dir, name)));
8661
+ paths.push(join8(dir, name));
8005
8662
  }
8006
- return relPaths;
8663
+ return paths;
8007
8664
  };
8008
- var augmentPrompt = (text, relPaths, labels) => {
8009
- if (relPaths.length === 0) return text;
8665
+ var augmentPrompt = (text, paths, labels) => {
8666
+ if (paths.length === 0) return text;
8010
8667
  const header = [
8011
- "[Attached files \u2014 already saved in the working directory. Reference them by label:]",
8012
- ...relPaths.map((p, i) => `- {${labels[i]}} ${p}`)
8668
+ "[Attached files \u2014 already saved locally. Reference them by label:]",
8669
+ ...paths.map((p, i) => `- {${labels[i]}} ${p}`)
8013
8670
  ].join("\n");
8014
8671
  return text.length > 0 ? `${header}
8015
8672
 
@@ -8017,19 +8674,20 @@ ${text}` : header;
8017
8674
  };
8018
8675
 
8019
8676
  // src/session/runner/codex.ts
8677
+ import { join as join9 } from "node:path";
8020
8678
  import { Codex } from "@openai/codex-sdk";
8021
8679
 
8022
8680
  // src/session/runner/sdk-env.ts
8023
8681
  import { existsSync as existsSync3 } from "node:fs";
8024
- import { isAbsolute } from "node:path";
8682
+ import { isAbsolute as isAbsolute2 } from "node:path";
8025
8683
  var buildSdkEnv = (overlay) => overlay ? { ...process.env, ...overlay } : void 0;
8026
8684
  var claudeExecutable = () => process.env.BATON_CLAUDE_BIN;
8027
- var additionalDirs = () => {
8685
+ var additionalDirs = (required = []) => {
8028
8686
  const raw = process.env.BATON_ADD_DIRS;
8029
- if (!raw) return void 0;
8030
- const dirs = raw.split(":").map((s) => s.trim()).filter((s) => s !== "" && isAbsolute(s) && existsSync3(s));
8031
- return dirs.length > 0 ? dirs : void 0;
8687
+ const dirs = [...required, ...(raw ?? "").split(":")].map((s) => s.trim()).filter((s) => s !== "" && isAbsolute2(s) && existsSync3(s));
8688
+ return dirs.length > 0 ? [...new Set(dirs)] : void 0;
8032
8689
  };
8690
+ var PLAN_MODE_PREFIX = "Plan mode: inspect and propose a plan only. Do not edit files or run mutating commands.\n\n";
8033
8691
  var truthy = (value) => {
8034
8692
  if (value === void 0) return void 0;
8035
8693
  const normalized = value.trim().toLowerCase();
@@ -8050,17 +8708,8 @@ var codexApprovalPolicy = () => {
8050
8708
  return raw;
8051
8709
  return "never";
8052
8710
  };
8053
- var claudeEffort = (raw) => {
8054
- if (raw === "minimal") return "low";
8055
- if (raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max")
8056
- return raw;
8057
- return void 0;
8058
- };
8059
- var codexEffort = (raw) => {
8060
- if (raw === "minimal" || raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max")
8061
- return raw;
8062
- return void 0;
8063
- };
8711
+ var claudeEffort = (raw) => raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max" ? raw : void 0;
8712
+ var codexEffort = (raw) => isAgentEffort(raw) ? raw : void 0;
8064
8713
 
8065
8714
  // src/session/runner/codex.ts
8066
8715
  var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -8178,17 +8827,16 @@ var toAgentEvent = (raw) => {
8178
8827
  return { type: "raw", raw };
8179
8828
  };
8180
8829
  var withPlanPrefix = (input) => {
8181
- const prefix = "Plan mode: inspect and propose a plan only. Do not edit files or run mutating commands.\n\n";
8182
- if (typeof input === "string") return `${prefix}${input}`;
8830
+ if (typeof input === "string") return `${PLAN_MODE_PREFIX}${input}`;
8183
8831
  return input.map(
8184
- (entry, i) => i === 0 && entry.type === "text" ? { ...entry, text: `${prefix}${entry.text}` } : entry
8832
+ (entry, i) => i === 0 && entry.type === "text" ? { ...entry, text: `${PLAN_MODE_PREFIX}${entry.text}` } : entry
8185
8833
  );
8186
8834
  };
8187
8835
  async function* startCodexEvents(config, input, worker2, opts) {
8188
8836
  const env = compactEnv(buildSdkEnv(opts.envOverlay));
8189
8837
  const codex = new Codex(env ? { env } : void 0);
8190
8838
  const resuming = !config.agentSessionId.startsWith("pending:");
8191
- const addDirs = additionalDirs();
8839
+ const addDirs = additionalDirs([join9(config.runtimeDir, "attachments")]);
8192
8840
  const sandboxMode = codexSandboxMode(opts.planMode ?? false);
8193
8841
  const approvalPolicy = codexApprovalPolicy();
8194
8842
  const networkAccessEnabled = codexNetworkAccess();
@@ -8199,7 +8847,7 @@ async function* startCodexEvents(config, input, worker2, opts) {
8199
8847
  if (opts.model) opts.log(`[model] ${opts.model}`);
8200
8848
  if (reasoningEffort) opts.log(`[effort] ${reasoningEffort}`);
8201
8849
  const threadOptions = {
8202
- workingDirectory: config.worktreePath,
8850
+ workingDirectory: config.cwd,
8203
8851
  skipGitRepoCheck: true,
8204
8852
  sandboxMode,
8205
8853
  approvalPolicy,
@@ -8218,8 +8866,7 @@ async function* startCodexEvents(config, input, worker2, opts) {
8218
8866
  if (event.type === "thread.started" && !resuming && event.sessionId) {
8219
8867
  config.agentSessionId = event.sessionId;
8220
8868
  await worker2.materialize({
8221
- agentSessionId: event.sessionId,
8222
- worktreePath: config.worktreePath
8869
+ agentSessionId: event.sessionId
8223
8870
  });
8224
8871
  opts.log(`[codex] thread ${event.sessionId}`);
8225
8872
  }
@@ -8227,18 +8874,241 @@ async function* startCodexEvents(config, input, worker2, opts) {
8227
8874
  }
8228
8875
  }
8229
8876
 
8877
+ // src/session/runner/pi.ts
8878
+ import { readFile as readFile2 } from "node:fs/promises";
8879
+ import { homedir as homedir3 } from "node:os";
8880
+ import { join as join10 } from "node:path";
8881
+ var newPiTurnState = (startedAt = Date.now()) => ({
8882
+ startedAt,
8883
+ usage: {},
8884
+ toolArgs: {}
8885
+ });
8886
+ var READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
8887
+ var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8888
+ var str2 = (v) => typeof v === "string" ? v : void 0;
8889
+ var resultText = (result) => isRecord3(result) && Array.isArray(result.content) ? result.content.filter(isRecord3).map((block) => str2(block.text) ?? "").join("") : "";
8890
+ var startedItem = (id, name, args) => {
8891
+ const input = isRecord3(args) ? args : {};
8892
+ if (name === "bash") {
8893
+ return {
8894
+ type: "command_execution",
8895
+ id,
8896
+ status: "in_progress",
8897
+ command: str2(input.command) ?? "",
8898
+ output: ""
8899
+ };
8900
+ }
8901
+ if (name === "write" || name === "edit") {
8902
+ return {
8903
+ type: "file_change",
8904
+ id,
8905
+ status: "in_progress",
8906
+ changes: [{ path: str2(input.path) ?? "", kind: "update" }]
8907
+ };
8908
+ }
8909
+ return { type: "tool_call", id, status: "in_progress", name, input: args ?? null };
8910
+ };
8911
+ var completedItem = (id, name, args, result, isError) => {
8912
+ const status = isError ? "failed" : "completed";
8913
+ const started = startedItem(id, name, args);
8914
+ if (started.type === "command_execution") {
8915
+ const structured = isRecord3(result) && isRecord3(result.structuredContent) ? result.structuredContent : {};
8916
+ return {
8917
+ ...started,
8918
+ status,
8919
+ output: str2(structured.output) ?? resultText(result),
8920
+ exitCode: typeof structured.exit_code === "number" ? structured.exit_code : void 0
8921
+ };
8922
+ }
8923
+ if (started.type === "tool_call") {
8924
+ return { ...started, status, output: resultText(result), isError };
8925
+ }
8926
+ return { ...started, status };
8927
+ };
8928
+ var messageItems = (message) => message.content.flatMap((block, index) => {
8929
+ const id = `${message.responseId ?? message.timestamp}:${index}`;
8930
+ if (block.type === "text" && block.text.trim()) {
8931
+ return [{ type: "agent_message", id, status: "completed", text: block.text }];
8932
+ }
8933
+ if (block.type === "thinking" && block.thinking.trim()) {
8934
+ return [{ type: "reasoning", id, status: "completed", text: block.thinking }];
8935
+ }
8936
+ return [];
8937
+ });
8938
+ var addUsage = (total, usage) => ({
8939
+ inputTokens: (total.inputTokens ?? 0) + usage.input,
8940
+ cachedInputTokens: (total.cachedInputTokens ?? 0) + usage.cacheRead,
8941
+ outputTokens: (total.outputTokens ?? 0) + usage.output,
8942
+ reasoningOutputTokens: (total.reasoningOutputTokens ?? 0) + (usage.reasoning ?? 0),
8943
+ totalCostUsd: (total.totalCostUsd ?? 0) + usage.cost.total,
8944
+ numTurns: (total.numTurns ?? 0) + 1
8945
+ });
8946
+ var piToAgentEvents = (event, state, now = Date.now()) => {
8947
+ if (event.type === "tool_execution_start") {
8948
+ return {
8949
+ events: [
8950
+ { type: "item.started", item: startedItem(event.toolCallId, event.toolName, event.args) }
8951
+ ],
8952
+ state: { ...state, toolArgs: { ...state.toolArgs, [event.toolCallId]: event.args } }
8953
+ };
8954
+ }
8955
+ if (event.type === "tool_execution_end") {
8956
+ const args = state.toolArgs[event.toolCallId];
8957
+ return {
8958
+ events: [
8959
+ {
8960
+ type: "item.completed",
8961
+ item: completedItem(event.toolCallId, event.toolName, args, event.result, event.isError)
8962
+ }
8963
+ ],
8964
+ state
8965
+ };
8966
+ }
8967
+ if (event.type === "message_end" && event.message.role === "assistant") {
8968
+ const message = event.message;
8969
+ return {
8970
+ events: messageItems(message).map((item) => ({ type: "item.completed", item })),
8971
+ // Reset on every assistant message: pi auto-retries, and only the last
8972
+ // response decides whether the turn failed.
8973
+ state: {
8974
+ ...state,
8975
+ usage: addUsage(state.usage, message.usage),
8976
+ error: message.stopReason === "error" ? message.errorMessage ?? "pi turn failed" : void 0
8977
+ }
8978
+ };
8979
+ }
8980
+ if (event.type === "agent_settled") {
8981
+ const usage = { ...state.usage, durationMs: now - state.startedAt };
8982
+ return {
8983
+ events: [
8984
+ state.error ? { type: "turn.failed", error: { message: state.error }, usage } : { type: "turn.completed", usage }
8985
+ ],
8986
+ state
8987
+ };
8988
+ }
8989
+ return { events: [], state };
8990
+ };
8991
+ var resolveModel = (runtime, raw) => {
8992
+ const slash = raw.indexOf("/");
8993
+ const model = slash > 0 ? runtime.getModel(raw.slice(0, slash), raw.slice(slash + 1)) : void 0;
8994
+ if (!model) throw new Error(`unknown pi model "${raw}" (expected provider/model)`);
8995
+ return model;
8996
+ };
8997
+ var promptParts = async (input) => {
8998
+ if (typeof input === "string") return { text: input, images: [] };
8999
+ const text = input.flatMap((entry) => entry.type === "text" ? [entry.text] : []).join("\n");
9000
+ const images = await Promise.all(
9001
+ input.flatMap(
9002
+ (entry) => entry.type === "local_image" ? [
9003
+ readFile2(entry.path).then((data) => ({
9004
+ type: "image",
9005
+ data: data.toString("base64"),
9006
+ mimeType: contentTypeForPath(entry.path)
9007
+ }))
9008
+ ] : []
9009
+ )
9010
+ );
9011
+ return { text, images };
9012
+ };
9013
+ async function* startPiEvents(config, input, opts) {
9014
+ const pi = await import("@earendil-works/pi-coding-agent");
9015
+ const modelRuntime = await pi.ModelRuntime.create();
9016
+ const model = opts.model ? resolveModel(modelRuntime, opts.model) : void 0;
9017
+ const effort = isAgentEffort(opts.effort) ? opts.effort : void 0;
9018
+ const thinkingLevel = effort === "ultra" ? void 0 : effort;
9019
+ const agentDir = pi.getAgentDir();
9020
+ const resourceLoader = new pi.DefaultResourceLoader({
9021
+ cwd: config.cwd,
9022
+ agentDir,
9023
+ // pi discovers .pi/ and .agents/ skills natively; Baton workers deploy theirs
9024
+ // under .claude/skills (project and user level).
9025
+ additionalSkillPaths: [join10(config.cwd, ".claude/skills"), join10(homedir3(), ".claude/skills")]
9026
+ });
9027
+ await resourceLoader.reload();
9028
+ const existing = pi.SessionManager.findById(config.cwd, config.agentSessionId);
9029
+ const sessionManager = existing ? pi.SessionManager.open(existing) : pi.SessionManager.create(config.cwd, void 0, { id: config.agentSessionId });
9030
+ const { session: session2 } = await pi.createAgentSession({
9031
+ cwd: config.cwd,
9032
+ agentDir,
9033
+ modelRuntime,
9034
+ resourceLoader,
9035
+ sessionManager,
9036
+ ...model ? { model } : {},
9037
+ ...thinkingLevel ? { thinkingLevel } : {},
9038
+ ...opts.planMode ? { tools: READ_ONLY_TOOLS } : {}
9039
+ });
9040
+ const modelId = session2.model ? `${session2.model.provider}/${session2.model.id}` : void 0;
9041
+ opts.log(`[pi] ${existing ? "resume" : "new"} session ${session2.sessionId}`);
9042
+ if (modelId) opts.log(`[model] ${modelId}`);
9043
+ if (thinkingLevel) opts.log(`[effort] ${thinkingLevel}`);
9044
+ if (opts.planMode) opts.log("[plan] read-only tools");
9045
+ const queue = [];
9046
+ let wake = () => {
9047
+ };
9048
+ let failure;
9049
+ let finished = false;
9050
+ let settled = false;
9051
+ const unsubscribe = session2.subscribe((event) => {
9052
+ queue.push(event);
9053
+ wake();
9054
+ });
9055
+ const abort = () => {
9056
+ void session2.abort().catch((error) => opts.log(`[pi] abort: ${String(error)}`));
9057
+ };
9058
+ opts.signal?.addEventListener("abort", abort, { once: true });
9059
+ try {
9060
+ yield { type: "thread.started", sessionId: session2.sessionId, model: modelId };
9061
+ yield { type: "turn.started" };
9062
+ const { text, images } = await promptParts(input);
9063
+ if (opts.signal?.aborted) return;
9064
+ const running = session2.prompt(opts.planMode ? `${PLAN_MODE_PREFIX}${text}` : text, images?.length ? { images } : {}).then(
9065
+ () => {
9066
+ finished = true;
9067
+ wake();
9068
+ },
9069
+ (error) => {
9070
+ failure = error;
9071
+ wake();
9072
+ }
9073
+ );
9074
+ let state = newPiTurnState();
9075
+ while (!settled) {
9076
+ const event = queue.shift();
9077
+ if (!event) {
9078
+ if (failure !== void 0) throw failure;
9079
+ if (finished) break;
9080
+ await new Promise((resolve4) => {
9081
+ wake = resolve4;
9082
+ });
9083
+ continue;
9084
+ }
9085
+ const folded = piToAgentEvents(event, state);
9086
+ state = folded.state;
9087
+ settled = event.type === "agent_settled";
9088
+ yield* folded.events;
9089
+ }
9090
+ await running;
9091
+ if (failure !== void 0) throw failure;
9092
+ } finally {
9093
+ opts.signal?.removeEventListener("abort", abort);
9094
+ unsubscribe();
9095
+ session2.dispose();
9096
+ }
9097
+ }
9098
+
8230
9099
  // src/session/runner/query.ts
9100
+ import { join as join11 } from "node:path";
8231
9101
  var startQuery = (config, text, resuming, queryFn, abortController, log, { envOverlay, planMode = false, model, effort } = {}) => {
8232
9102
  const env = buildSdkEnv(envOverlay);
8233
9103
  const exe = claudeExecutable();
8234
- const addDirs = additionalDirs();
9104
+ const addDirs = additionalDirs([join11(config.runtimeDir, "attachments")]);
8235
9105
  const level = claudeEffort(effort);
8236
9106
  if (addDirs) log(`[add-dir] ${addDirs.join(", ")}`);
8237
9107
  if (planMode) log("[plan] read-only planning turn");
8238
9108
  if (model) log(`[model] ${model}`);
8239
9109
  if (level) log(`[effort] ${level}`);
8240
9110
  const options = {
8241
- cwd: config.worktreePath,
9111
+ cwd: config.cwd,
8242
9112
  permissionMode: planMode ? "plan" : "bypassPermissions",
8243
9113
  // Headless relay: no TTY/TUI can answer an interactive ask, so AskUserQuestion
8244
9114
  // would fail the whole turn. Block it — the model asks in plain text instead,
@@ -8279,34 +9149,34 @@ var streamAgentEvents = async (events, worker2) => {
8279
9149
  return result;
8280
9150
  };
8281
9151
  var streamClaudeSdkEvents = (messages, worker2) => streamAgentEvents(claudeToAgentEvents(messages), worker2);
8282
- var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8283
- var str2 = (v) => typeof v === "string" ? v : void 0;
9152
+ var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
9153
+ var str3 = (v) => typeof v === "string" ? v : void 0;
8284
9154
  var messageContent = (payload) => {
8285
9155
  const msg = payload.message;
8286
- if (!isRecord3(msg) || !Array.isArray(msg.content)) return [];
8287
- return msg.content.filter(isRecord3);
9156
+ if (!isRecord4(msg) || !Array.isArray(msg.content)) return [];
9157
+ return msg.content.filter(isRecord4);
8288
9158
  };
8289
9159
  var textFromToolResult = (raw) => {
8290
9160
  const c = raw.content;
8291
9161
  if (typeof c === "string") return c;
8292
9162
  if (Array.isArray(c)) {
8293
- return c.map((b) => isRecord3(b) && typeof b.text === "string" ? b.text : JSON.stringify(b)).join("\n");
9163
+ return c.map((b) => isRecord4(b) && typeof b.text === "string" ? b.text : JSON.stringify(b)).join("\n");
8294
9164
  }
8295
9165
  return JSON.stringify(c ?? "");
8296
9166
  };
8297
9167
  var commandOf = (input) => {
8298
- if (!isRecord3(input)) return "";
8299
- return str2(input.command) ?? str2(input.cmd) ?? "";
9168
+ if (!isRecord4(input)) return "";
9169
+ return str3(input.command) ?? str3(input.cmd) ?? "";
8300
9170
  };
8301
9171
  var pathOf = (input) => {
8302
- if (!isRecord3(input)) return void 0;
8303
- return str2(input.file_path) ?? str2(input.path);
9172
+ if (!isRecord4(input)) return void 0;
9173
+ return str3(input.file_path) ?? str3(input.path);
8304
9174
  };
8305
9175
  var fileKindOf = (name) => name === "Delete" ? "delete" : "update";
8306
9176
  var todoItems2 = (input) => {
8307
- if (!isRecord3(input) || !Array.isArray(input.todos)) return [];
8308
- return input.todos.filter(isRecord3).map((todo) => ({
8309
- text: str2(todo.content) ?? str2(todo.text) ?? "",
9177
+ if (!isRecord4(input) || !Array.isArray(input.todos)) return [];
9178
+ return input.todos.filter(isRecord4).map((todo) => ({
9179
+ text: str3(todo.content) ?? str3(todo.text) ?? "",
8310
9180
  completed: todo.status === "completed" || todo.completed === true
8311
9181
  }));
8312
9182
  };
@@ -8315,8 +9185,8 @@ var mcpParts = (name) => {
8315
9185
  return match ? { server: match[1] ?? "", tool: match[2] ?? "" } : null;
8316
9186
  };
8317
9187
  var startedItemFromToolUse = (block) => {
8318
- const id = str2(block.id) ?? `tool-${Math.random().toString(36).slice(2)}`;
8319
- const name = str2(block.name) ?? "tool";
9188
+ const id = str3(block.id) ?? `tool-${Math.random().toString(36).slice(2)}`;
9189
+ const name = str3(block.name) ?? "tool";
8320
9190
  const input = block.input ?? null;
8321
9191
  if (name === "Bash") {
8322
9192
  return {
@@ -8352,7 +9222,7 @@ var startedItemFromToolUse = (block) => {
8352
9222
  type: "web_search",
8353
9223
  id,
8354
9224
  status: "in_progress",
8355
- query: isRecord3(input) ? str2(input.query) ?? "" : ""
9225
+ query: isRecord4(input) ? str3(input.query) ?? "" : ""
8356
9226
  };
8357
9227
  }
8358
9228
  if (name === "TodoWrite") {
@@ -8371,9 +9241,9 @@ var completeItem = (item, output, isError) => {
8371
9241
  };
8372
9242
  var resultEvent = (message) => {
8373
9243
  const rec = message;
8374
- const subtype = str2(rec.subtype) ?? "unknown";
9244
+ const subtype = str3(rec.subtype) ?? "unknown";
8375
9245
  const isError = rec.is_error === true || subtype !== "success";
8376
- const resultText = str2(rec.result) ?? "";
9246
+ const resultText2 = str3(rec.result) ?? "";
8377
9247
  const usage = {
8378
9248
  numTurns: typeof rec.num_turns === "number" ? rec.num_turns : void 0,
8379
9249
  totalCostUsd: typeof rec.total_cost_usd === "number" ? rec.total_cost_usd : void 0,
@@ -8381,7 +9251,7 @@ var resultEvent = (message) => {
8381
9251
  };
8382
9252
  return isError ? {
8383
9253
  type: "turn.failed",
8384
- error: { message: resultText || `claude result: ${subtype}`, subtype },
9254
+ error: { message: resultText2 || `claude result: ${subtype}`, subtype },
8385
9255
  usage,
8386
9256
  raw: message
8387
9257
  } : { type: "turn.completed", subtype, usage, raw: message };
@@ -8398,8 +9268,8 @@ async function* claudeToAgentEvents(messages) {
8398
9268
  systemEmitted = true;
8399
9269
  yield {
8400
9270
  type: "thread.started",
8401
- sessionId: str2(p.session_id) ?? "",
8402
- model: str2(p.model) ?? (isRecord3(p.model_info) ? str2(p.model_info.id) : void 0),
9271
+ sessionId: str3(p.session_id) ?? "",
9272
+ model: str3(p.model) ?? (isRecord4(p.model_info) ? str3(p.model_info.id) : void 0),
8403
9273
  raw: message
8404
9274
  };
8405
9275
  yield { type: "turn.started", raw: message };
@@ -8438,7 +9308,7 @@ async function* claudeToAgentEvents(messages) {
8438
9308
  pendingTools.set(item.id, item);
8439
9309
  yield { type: "item.started", item, raw: block };
8440
9310
  } else if (block.type === "tool_result") {
8441
- const id = str2(block.tool_use_id) ?? "";
9311
+ const id = str3(block.tool_use_id) ?? "";
8442
9312
  const item = pendingTools.get(id);
8443
9313
  if (item) {
8444
9314
  const completed = completeItem(item, textFromToolResult(block), block.is_error === true);
@@ -8456,7 +9326,7 @@ async function* claudeToAgentEvents(messages) {
8456
9326
  if (message.type === "user") {
8457
9327
  for (const block of messageContent(p)) {
8458
9328
  if (block.type !== "tool_result") continue;
8459
- const id = str2(block.tool_use_id) ?? "";
9329
+ const id = str3(block.tool_use_id) ?? "";
8460
9330
  const item = pendingTools.get(id);
8461
9331
  if (!item) {
8462
9332
  yield { type: "raw", raw: block };
@@ -8482,15 +9352,19 @@ var UnsettledExecution = class extends Error {
8482
9352
  var materializeImages = async (config, execution) => {
8483
9353
  const images = execution.message.payload.images ?? [];
8484
9354
  if (!images.length) return [];
8485
- const directory = join7(config.worktreePath, "attachments");
8486
- await mkdir2(directory, { recursive: true });
8487
- await writeFile2(join7(directory, ".gitignore"), "*\n");
9355
+ const directory2 = join12(
9356
+ config.runtimeDir,
9357
+ "attachments",
9358
+ `message-${execution.message.id}`,
9359
+ "pasted"
9360
+ );
9361
+ await mkdir5(directory2, { recursive: true });
8488
9362
  return Promise.all(
8489
9363
  images.map(async (image, index) => {
8490
9364
  const match = /^data:image\/(png|jpeg|webp|gif);base64,([A-Za-z0-9+/=\r\n]+)$/.exec(image);
8491
9365
  if (!match) throw new Error("unsupported pasted image");
8492
- const path = `attachments/pasted-${execution.message.id}-${index + 1}.${match[1]}`;
8493
- await writeFile2(join7(config.worktreePath, path), Buffer.from(match[2] ?? "", "base64"));
9366
+ const path = join12(directory2, `pasted-${index + 1}.${match[1]}`);
9367
+ await writeFile3(path, Buffer.from(match[2] ?? "", "base64"));
8494
9368
  return path;
8495
9369
  })
8496
9370
  );
@@ -8498,7 +9372,12 @@ var materializeImages = async (config, execution) => {
8498
9372
  var buildPrompt = async (config, execution, fetchImpl) => {
8499
9373
  const { text: rawText, attachments = [] } = execution.message.payload;
8500
9374
  const paths = attachments.length ? await materializeAttachments({
8501
- worktreePath: config.worktreePath,
9375
+ directory: join12(
9376
+ config.runtimeDir,
9377
+ "attachments",
9378
+ `message-${execution.message.id}`,
9379
+ "files"
9380
+ ),
8502
9381
  serverBase: config.server,
8503
9382
  attachments,
8504
9383
  fetchImpl
@@ -8515,7 +9394,7 @@ var buildPrompt = async (config, execution, fetchImpl) => {
8515
9394
  ];
8516
9395
  const images = imagePaths.map((path) => ({
8517
9396
  type: "local_image",
8518
- path: join7(config.worktreePath, path)
9397
+ path
8519
9398
  }));
8520
9399
  return { text, input: images.length ? [{ type: "text", text }, ...images] : text };
8521
9400
  };
@@ -8576,25 +9455,27 @@ var runTurn = async (config, worker2, execution, resuming, queryFn, log, envOver
8576
9455
  const built = await buildPrompt(config, execution, fetchImpl);
8577
9456
  if (abort.signal.aborted) throw new Error("interrupted before SDK start");
8578
9457
  const { planMode, model, effort } = execution.message.payload;
8579
- return config.agentKind === "codex" ? streamAgentEvents(
8580
- startCodexEvents(config, built.input, worker2, {
8581
- envOverlay,
8582
- planMode,
8583
- model,
8584
- effort,
8585
- signal: abort.signal,
8586
- log
8587
- }),
8588
- output
8589
- ) : streamClaudeSdkEvents(
8590
- startQuery(config, built.text, resuming, queryFn, abort, log, {
8591
- envOverlay,
8592
- planMode,
8593
- model,
8594
- effort
8595
- }),
8596
- output
8597
- );
9458
+ const turn = { planMode, model, effort };
9459
+ const run4 = {
9460
+ "claude-code": () => streamClaudeSdkEvents(
9461
+ startQuery(config, built.text, resuming, queryFn, abort, log, { envOverlay, ...turn }),
9462
+ output
9463
+ ),
9464
+ codex: () => streamAgentEvents(
9465
+ startCodexEvents(config, built.input, worker2, {
9466
+ envOverlay,
9467
+ ...turn,
9468
+ signal: abort.signal,
9469
+ log
9470
+ }),
9471
+ output
9472
+ ),
9473
+ pi: () => streamAgentEvents(
9474
+ startPiEvents(config, built.input, { ...turn, signal: abort.signal, log }),
9475
+ output
9476
+ )
9477
+ };
9478
+ return run4[config.agentKind]();
8598
9479
  };
8599
9480
  const consuming = consume().then(
8600
9481
  (value) => ({ kind: "result", value }),
@@ -8618,7 +9499,7 @@ var runTurn = async (config, worker2, execution, resuming, queryFn, log, envOver
8618
9499
  };
8619
9500
 
8620
9501
  // src/session/runner.ts
8621
- var isAgentConversationResumable = (config, transcriptPath = findTranscriptPath) => config.agentKind === "codex" ? !config.agentSessionId.startsWith("pending:") : transcriptPath(config.agentSessionId) !== null;
9502
+ var isAgentConversationResumable = (config, transcriptPath = findTranscriptPath) => config.agentKind === "claude-code" ? transcriptPath(config.agentSessionId) !== null : !config.agentSessionId.startsWith("pending:");
8622
9503
  var shouldReap = (lastActivity, now, busy, queueLen, idleMs) => !busy && queueLen === 0 && now - lastActivity >= idleMs;
8623
9504
  var runDaemon = async (config, deps, signal) => {
8624
9505
  const log = deps.log ?? console.log;
@@ -8632,12 +9513,12 @@ var runDaemon = async (config, deps, signal) => {
8632
9513
  let lastActivity = Date.now();
8633
9514
  let stop = () => {
8634
9515
  };
8635
- const stopped = new Promise((resolve3) => {
8636
- stop = resolve3;
9516
+ const stopped = new Promise((resolve4) => {
9517
+ stop = resolve4;
8637
9518
  });
8638
9519
  const drain = async () => {
8639
9520
  if (draining || closed || stranded) return;
8640
- const run2 = async () => {
9521
+ const run4 = async () => {
8641
9522
  while (!closed && !stranded && !signal.aborted) {
8642
9523
  let claimed;
8643
9524
  try {
@@ -8680,7 +9561,7 @@ var runDaemon = async (config, deps, signal) => {
8680
9561
  }
8681
9562
  }
8682
9563
  };
8683
- draining = run2();
9564
+ draining = run4();
8684
9565
  try {
8685
9566
  await draining;
8686
9567
  } finally {
@@ -8745,6 +9626,19 @@ var runDaemon = async (config, deps, signal) => {
8745
9626
  }
8746
9627
  };
8747
9628
 
9629
+ // src/worker/runtime-paths.ts
9630
+ import { createHash } from "node:crypto";
9631
+ import { homedir as homedir4 } from "node:os";
9632
+ import { join as join13 } from "node:path";
9633
+ var workerRuntimeDir = (owner, env = process.env) => join13(
9634
+ env.XDG_DATA_HOME ?? join13(env.HOME ?? homedir4(), ".local/share"),
9635
+ "baton",
9636
+ "runtime",
9637
+ createHash("sha256").update(owner.server).digest("hex"),
9638
+ `worker-${owner.workerId}`
9639
+ );
9640
+ var sessionRuntimeDir = (owner, sessionId, env = process.env) => join13(workerRuntimeDir(owner, env), "sessions", String(sessionId));
9641
+
8748
9642
  // src/commands/session/run.ts
8749
9643
  var bearerFetch = (token) => (input, init2) => fetch(input, {
8750
9644
  ...init2,
@@ -8774,13 +9668,13 @@ var sessionRunCommand = defineCommand({
8774
9668
  },
8775
9669
  run: async ({ args }) => {
8776
9670
  if (!process.connected) throw new Error("session runner requires supervisor IPC");
8777
- await new Promise((resolve3, reject) => {
9671
+ await new Promise((resolve4, reject) => {
8778
9672
  const onMessage = (message) => {
8779
9673
  if (typeof message !== "object" || message === null || !("type" in message) || message.type !== "execute")
8780
9674
  return;
8781
9675
  process.off("message", onMessage);
8782
9676
  process.off("disconnect", onDisconnect);
8783
- resolve3();
9677
+ resolve4();
8784
9678
  };
8785
9679
  const onDisconnect = () => {
8786
9680
  process.off("message", onMessage);
@@ -8797,9 +9691,10 @@ var sessionRunCommand = defineCommand({
8797
9691
  const sessionId = Number(args.session);
8798
9692
  if (!Number.isInteger(sessionId) || sessionId <= 0)
8799
9693
  throw new Error(`invalid session id: ${args.session}`);
9694
+ process.env.BATON_SESSION_ID = String(sessionId);
8800
9695
  const client = createClient(server, { bearer: workerToken });
8801
9696
  const s = await client.sessions.get(sessionId);
8802
- if (!s.agentSessionId || !s.worktreePath)
9697
+ if (!s.agentSessionId || !s.cwd)
8803
9698
  throw new Error(`session #${sessionId} is not materialized yet`);
8804
9699
  const config = {
8805
9700
  server,
@@ -8807,7 +9702,8 @@ var sessionRunCommand = defineCommand({
8807
9702
  name: s.name,
8808
9703
  agentKind: s.agentKind,
8809
9704
  agentSessionId: s.agentSessionId,
8810
- worktreePath: s.worktreePath
9705
+ cwd: s.cwd,
9706
+ runtimeDir: sessionRuntimeDir({ server, workerId: s.workerId }, sessionId)
8811
9707
  };
8812
9708
  const worker2 = createWorkerClient(server, workerToken, sessionId);
8813
9709
  const ac = new AbortController();
@@ -9100,185 +9996,480 @@ var task = defineCommand({
9100
9996
  // src/commands/worker/index.ts
9101
9997
  import { hostname as osHostname } from "node:os";
9102
9998
 
9103
- // src/session/worktree.ts
9104
- import { execFile, execFileSync, spawnSync } from "node:child_process";
9999
+ // src/worker/bundled-skills.ts
10000
+ import { spawnSync as spawnSync2 } from "node:child_process";
10001
+ import { cpSync, existsSync as existsSync5 } from "node:fs";
10002
+ import { dirname as dirname3, join as join14 } from "node:path";
10003
+ import { fileURLToPath } from "node:url";
10004
+
10005
+ // src/worker/git-exclude.ts
10006
+ import { spawnSync } from "node:child_process";
9105
10007
  import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "node:fs";
9106
- import { dirname, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
9107
- import { promisify } from "node:util";
9108
- var git = (repo, args) => spawnSync("git", ["-C", repo, ...args], {
9109
- stdio: "pipe",
9110
- encoding: "utf8"
9111
- });
9112
- var commandError = (result) => {
9113
- const raw = result.stderr || result.stdout || result.error?.message || `exit ${result.status}`;
9114
- return raw.trim().replace(/(https?:\/\/)[^/@\s]+@/g, "$1***@");
9115
- };
9116
- var execFileAsync = promisify(execFile);
9117
- var DEFAULT_SYNC_TIMEOUT_MS = 6e4;
9118
- var asyncCommandError = (error, timeoutMs) => {
9119
- const failure = error;
9120
- if (failure.killed || failure.code === "ETIMEDOUT") return `timed out after ${timeoutMs}ms`;
9121
- const raw = failure.stderr || failure.stdout || failure.message || String(error);
9122
- return raw.trim().replace(/(https?:\/\/)[^/@\s]+@/g, "$1***@");
9123
- };
9124
- var validateBaseBranch = (branch) => {
9125
- const normalized = branch.trim();
9126
- const valid = spawnSync("git", ["check-ref-format", "--branch", normalized], {
9127
- stdio: "pipe",
9128
- encoding: "utf8"
9129
- });
9130
- if (valid.status !== 0)
9131
- throw new Error(`invalid base branch "${normalized}": ${commandError(valid)}`);
9132
- return normalized;
9133
- };
9134
- var createWorktree = (input) => {
9135
- if (existsSync4(input.worktreePath))
9136
- throw new Error(`worktree path already exists: ${input.worktreePath}`);
9137
- const head = spawnSync("git", ["-C", input.repo, "rev-parse", "--verify", "-q", "HEAD"], {
9138
- stdio: "pipe",
9139
- encoding: "utf8"
9140
- });
9141
- if (head.status !== 0)
9142
- throw new Error(
9143
- `repo has no commits (unborn HEAD) \u2014 create an initial commit before starting sessions`
9144
- );
9145
- mkdirSync3(dirname(input.worktreePath), { recursive: true });
9146
- const branch = `baton/${input.sessionCode}`;
9147
- const r = spawnSync(
9148
- "git",
9149
- ["-C", input.repo, "worktree", "add", "-b", branch, input.worktreePath, input.base],
9150
- { stdio: "pipe", encoding: "utf8" }
9151
- );
9152
- if (r.status !== 0) {
9153
- throw new Error(`git worktree add failed: ${r.stderr || r.stdout || `exit ${r.status}`}`);
9154
- }
9155
- };
9156
- var repoHeadBranch = (repo) => {
9157
- const r = git(repo, ["symbolic-ref", "--short", "HEAD"]);
9158
- const branch = r.status === 0 ? r.stdout.trim() : "";
9159
- return branch || "main";
9160
- };
9161
- var syncBaseBranch = async (repo, branch, options = {}) => {
9162
- const checkedBranch = validateBaseBranch(branch);
9163
- const env = options.env ?? process.env;
9164
- const timeoutMs = options.timeoutMs ?? DEFAULT_SYNC_TIMEOUT_MS;
9165
- const origin = git(repo, ["remote", "get-url", "origin"]);
9166
- if (origin.status !== 0) {
9167
- const localRef = `refs/heads/${checkedBranch}`;
9168
- const verified2 = git(repo, ["rev-parse", "--verify", "-q", localRef]);
9169
- if (verified2.status !== 0) throw new Error(`local base branch does not exist: ${checkedBranch}`);
9170
- return localRef;
9171
- }
9172
- const helper = env.BATON_GIT_SYNC_BIN?.trim();
9173
- const command = helper ?? "git";
9174
- const args = helper ? [repo, checkedBranch] : [
9175
- "-C",
9176
- repo,
9177
- "fetch",
9178
- "--no-tags",
9179
- "origin",
9180
- `+refs/heads/${checkedBranch}:refs/remotes/origin/${checkedBranch}`
9181
- ];
9182
- let synced;
9183
- try {
9184
- synced = await execFileAsync(command, args, {
9185
- encoding: "utf8",
9186
- env: { ...env, GIT_TERMINAL_PROMPT: "0" },
9187
- timeout: timeoutMs
9188
- });
9189
- } catch (error) {
9190
- const via = helper ? ` via ${helper}` : "";
9191
- throw new Error(
9192
- `git sync failed for origin/${checkedBranch}${via}: ${asyncCommandError(error, timeoutMs)}`
9193
- );
9194
- }
9195
- const remoteRef = `refs/remotes/origin/${checkedBranch}`;
9196
- const verified = git(repo, ["rev-parse", "--verify", "-q", remoteRef]);
9197
- if (verified.status !== 0) throw new Error(`git sync succeeded but ${remoteRef} does not exist`);
9198
- if (helper) {
9199
- const reportedCommit = synced.stdout.trim();
9200
- const actualCommit = verified.stdout.trim();
9201
- if (!/^[0-9a-f]{40,64}$/i.test(reportedCommit))
9202
- throw new Error("git sync helper did not report the synced commit on stdout");
9203
- if (reportedCommit !== actualCommit)
9204
- throw new Error(`git sync helper reported a commit that does not match ${remoteRef}`);
9205
- }
9206
- return remoteRef;
9207
- };
9208
- var ensureExcluded = (repo, pattern) => {
9209
- try {
9210
- const r = spawnSync("git", ["-C", repo, "rev-parse", "--git-path", "info/exclude"], {
9211
- stdio: "pipe",
9212
- encoding: "utf8"
9213
- });
9214
- if (r.status !== 0) return;
9215
- const rel = r.stdout.trim();
9216
- const path = isAbsolute2(rel) ? rel : resolve2(repo, rel);
9217
- const current = existsSync4(path) ? readFileSync5(path, "utf8") : "";
9218
- if (current.split("\n").includes(pattern)) return;
9219
- mkdirSync3(dirname(path), { recursive: true });
9220
- const sep = current === "" || current.endsWith("\n") ? "" : "\n";
9221
- appendFileSync(path, `${sep}${pattern}
9222
- `, "utf8");
9223
- } catch {
9224
- }
9225
- };
9226
- var removeWorktree = (repo, worktreePath) => {
9227
- if (!existsSync4(worktreePath)) return;
9228
- try {
9229
- execFileSync("git", ["-C", repo, "worktree", "remove", "--force", worktreePath], {
9230
- stdio: "pipe"
9231
- });
9232
- } catch {
9233
- }
9234
- };
9235
- var restoreWorktree = async (repo, worktreePath, sessionCode, baseBranch = repoHeadBranch(repo)) => {
9236
- spawnSync("git", ["-C", repo, "worktree", "prune"], { stdio: "pipe" });
9237
- mkdirSync3(dirname(worktreePath), { recursive: true });
9238
- const branch = `baton/${sessionCode}`;
9239
- const reuse = spawnSync("git", ["-C", repo, "worktree", "add", worktreePath, branch], {
9240
- stdio: "pipe",
9241
- encoding: "utf8"
9242
- });
9243
- if (reuse.status === 0) return;
9244
- createWorktree({
9245
- repo,
9246
- worktreePath,
9247
- sessionCode,
9248
- base: await syncBaseBranch(repo, baseBranch)
10008
+ import { dirname as dirname2, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
10009
+ var ensureExcluded = (cwd, pattern) => {
10010
+ const result = spawnSync("git", ["-C", cwd, "rev-parse", "--git-path", "info/exclude"], {
10011
+ encoding: "utf8",
10012
+ env: { ...process.env, LC_ALL: "C" }
9249
10013
  });
10014
+ if (result.error) throw result.error;
10015
+ if (result.status !== 0) {
10016
+ if (result.stderr.includes("not a git repository")) return;
10017
+ throw new Error(`cannot locate Git exclude file: ${result.stderr.trim()}`);
10018
+ }
10019
+ const location = result.stdout.trim();
10020
+ const path = isAbsolute3(location) ? location : resolve3(cwd, location);
10021
+ const current = existsSync4(path) ? readFileSync5(path, "utf8") : "";
10022
+ if (current.split("\n").includes(pattern)) return;
10023
+ mkdirSync3(dirname2(path), { recursive: true });
10024
+ appendFileSync(path, `${current && !current.endsWith("\n") ? "\n" : ""}${pattern}
10025
+ `);
9250
10026
  };
9251
10027
 
9252
10028
  // src/worker/bundled-skills.ts
9253
- import { spawnSync as spawnSync2 } from "node:child_process";
9254
- import { cpSync, existsSync as existsSync5 } from "node:fs";
9255
- import { dirname as dirname2, join as join8 } from "node:path";
9256
- import { fileURLToPath } from "node:url";
9257
10029
  var SKILL_NAME = "baton-publish-artifact";
9258
10030
  var TARGETS = [`.claude/skills/${SKILL_NAME}`, `.agents/skills/${SKILL_NAME}`];
9259
10031
  var bundledSkillPath = () => {
9260
- const here = dirname2(fileURLToPath(import.meta.url));
10032
+ const here = dirname3(fileURLToPath(import.meta.url));
9261
10033
  const candidates = [
9262
- join8(here, "..", "skills", SKILL_NAME),
9263
- join8(here, "..", "..", "skills", SKILL_NAME)
10034
+ join14(here, "..", "skills", SKILL_NAME),
10035
+ join14(here, "..", "..", "skills", SKILL_NAME)
9264
10036
  ];
9265
10037
  const found = candidates.find(existsSync5);
9266
10038
  if (!found) throw new Error(`bundled skill missing: ${SKILL_NAME}`);
9267
10039
  return found;
9268
10040
  };
9269
- var isTracked = (worktreePath, relativePath) => {
9270
- const result = spawnSync2("git", ["-C", worktreePath, "ls-files", "--", relativePath], {
10041
+ var isTracked = (cwd, relativePath) => {
10042
+ const result = spawnSync2("git", ["-C", cwd, "ls-files", "--", relativePath], {
9271
10043
  encoding: "utf8",
9272
10044
  stdio: "pipe"
9273
10045
  });
9274
10046
  return result.status === 0 && result.stdout.trim().length > 0;
9275
10047
  };
9276
- var syncBundledArtifactSkill = (repo, worktreePath) => {
10048
+ var syncBundledArtifactSkill = (cwd) => {
9277
10049
  const source = bundledSkillPath();
9278
- TARGETS.filter((target) => !isTracked(worktreePath, target)).forEach((target) => {
9279
- ensureExcluded(repo, `${target}/`);
9280
- cpSync(source, join8(worktreePath, target), { recursive: true, force: true });
10050
+ TARGETS.filter((target) => !isTracked(cwd, target)).forEach((target) => {
10051
+ ensureExcluded(cwd, `${target}/`);
10052
+ cpSync(source, join14(cwd, target), { recursive: true, force: true });
10053
+ });
10054
+ };
10055
+
10056
+ // src/worker/files/branches.ts
10057
+ import { realpath as realpath2 } from "node:fs/promises";
10058
+
10059
+ // src/worker/files/git.ts
10060
+ import { execFile as execFile4 } from "node:child_process";
10061
+ import { promisify as promisify4 } from "node:util";
10062
+ var run = promisify4(execFile4);
10063
+ var GIT_TIMEOUT_MS = 3e3;
10064
+ var runGit = async (cwd, args, maxBuffer = 4 * 1024 * 1024) => {
10065
+ const { stdout } = await run("git", ["--literal-pathspecs", "-C", cwd, ...args], {
10066
+ encoding: "buffer",
10067
+ timeout: GIT_TIMEOUT_MS,
10068
+ maxBuffer,
10069
+ env: { ...process.env, LC_ALL: "C", GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
9281
10070
  });
10071
+ return stdout;
10072
+ };
10073
+
10074
+ // src/worker/files/branches.ts
10075
+ var NOT_A_WORK_TREE_EXIT = 128;
10076
+ var git = async (cwd, args) => (await runGit(cwd, args)).toString("utf8");
10077
+ var workTreeTop = async (dir) => {
10078
+ try {
10079
+ return await realpath2((await git(dir, ["rev-parse", "--show-toplevel"])).trim());
10080
+ } catch (error) {
10081
+ if (error.code !== NOT_A_WORK_TREE_EXIT)
10082
+ console.warn("[files] git rev-parse failed; no branches to browse", error);
10083
+ return null;
10084
+ }
10085
+ };
10086
+ var parseWorktrees = (stdout) => stdout.split("\0\0").map((record) => record.split("\0").filter(Boolean)).flatMap((fields) => {
10087
+ const path = fields.find((field) => field.startsWith("worktree "))?.slice("worktree ".length);
10088
+ const ref = fields.find((field) => field.startsWith("branch "))?.slice("branch ".length);
10089
+ const skipped = fields.some((field) => field === "bare" || field.startsWith("prunable"));
10090
+ return path && ref && !skipped ? [{ path, name: ref.replace(/^refs\/heads\//, "") }] : [];
10091
+ });
10092
+ var parseTips = (stdout) => new Map(
10093
+ stdout.split("\n").map((line) => line.split("\0")).flatMap(
10094
+ ([ref, commit]) => ref && commit ? [[ref.replace(/^refs\/heads\//, ""), commit]] : []
10095
+ )
10096
+ );
10097
+ var existingPath = async (path) => {
10098
+ try {
10099
+ return await realpath2(path);
10100
+ } catch (error) {
10101
+ if (error.code !== "ENOENT")
10102
+ console.warn(`[files] cannot resolve worktree ${path}`, error);
10103
+ return null;
10104
+ }
10105
+ };
10106
+ var rank = (branch) => branch.current ? 0 : branch.worktree ? 1 : 2;
10107
+ var discoverBranches = async (baseDir) => {
10108
+ const base = await realpath2(baseDir);
10109
+ if (await workTreeTop(base) !== base) return [];
10110
+ let worktrees;
10111
+ let tips;
10112
+ try {
10113
+ const [listed, refs] = await Promise.all([
10114
+ git(base, ["worktree", "list", "--porcelain", "-z"]),
10115
+ git(base, ["for-each-ref", "--format=%(refname)%00%(objectname)", "refs/heads"])
10116
+ ]);
10117
+ worktrees = parseWorktrees(listed);
10118
+ tips = parseTips(refs);
10119
+ } catch (error) {
10120
+ console.warn("[files] listing git branches failed; no branches to browse", error);
10121
+ return [];
10122
+ }
10123
+ const checkedOut = new Map(
10124
+ (await Promise.all(
10125
+ worktrees.map(async ({ path, name }) => [name, await existingPath(path)])
10126
+ )).filter((entry) => entry[1] !== null)
10127
+ );
10128
+ return [.../* @__PURE__ */ new Set([...checkedOut.keys(), ...tips.keys()])].map((name) => {
10129
+ const worktree = checkedOut.get(name) ?? null;
10130
+ return { name, worktree, commit: tips.get(name) ?? null, current: worktree === base };
10131
+ }).sort((a, b) => rank(a) - rank(b) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
10132
+ };
10133
+
10134
+ // src/worker/files/common.ts
10135
+ var FileError = class extends Error {
10136
+ constructor(code, message) {
10137
+ super(message);
10138
+ this.code = code;
10139
+ }
10140
+ code;
10141
+ };
10142
+ var notFound = (path) => new FileError("file_not_found", `not found: ${path}`);
10143
+ var checkedPath = (path, allowRoot) => {
10144
+ const rel = normalizeFilePath(path, allowRoot);
10145
+ if (rel === null) throw new FileError("invalid_request", `invalid path: ${path}`);
10146
+ if (isSensitiveFilePath(rel)) throw new FileError("forbidden_path", `protected file: ${rel}`);
10147
+ return rel;
10148
+ };
10149
+ var searchNeedle = (query3) => {
10150
+ const trimmed = query3.trim();
10151
+ if (trimmed === "" || trimmed.length > FILE_SEARCH_QUERY_MAX_LENGTH)
10152
+ throw new FileError(
10153
+ "invalid_request",
10154
+ `search query must be 1-${FILE_SEARCH_QUERY_MAX_LENGTH} characters`
10155
+ );
10156
+ return trimmed.toLowerCase();
10157
+ };
10158
+ var byKindThenName = (a, b) => {
10159
+ if (a.kind !== b.kind) return a.kind === "directory" ? -1 : 1;
10160
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
10161
+ };
10162
+ var decodeText = (bytes) => {
10163
+ if (bytes.includes(0)) return null;
10164
+ try {
10165
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
10166
+ } catch (error) {
10167
+ if (error instanceof TypeError) return null;
10168
+ throw error;
10169
+ }
10170
+ };
10171
+ var hasBytes = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
10172
+ var hasText = (bytes, text, offset = 0) => bytes.subarray(offset, offset + text.length).toString("latin1") === text;
10173
+ var SVG_START = /^\s*(?:<\?xml[^>]*>\s*)?(?:<!--[\s\S]*?-->\s*)*(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>]/i;
10174
+ var RAW_SIGNATURES = {
10175
+ "image/png": (bytes) => hasBytes(bytes, [137, 80, 78, 71, 13, 10, 26, 10]),
10176
+ "image/jpeg": (bytes) => hasBytes(bytes, [255, 216, 255]),
10177
+ "image/gif": (bytes) => hasText(bytes, "GIF8"),
10178
+ "image/webp": (bytes) => hasText(bytes, "RIFF") && hasText(bytes, "WEBP", 8),
10179
+ "image/avif": (bytes) => hasText(bytes, "ftyp", 4) && bytes.subarray(8, 32).toString("latin1").includes("avif"),
10180
+ "image/bmp": (bytes) => hasText(bytes, "BM"),
10181
+ "image/x-icon": (bytes) => hasBytes(bytes, [0, 0, 1, 0]),
10182
+ "image/svg+xml": (bytes) => {
10183
+ const text = decodeText(bytes);
10184
+ return text !== null && SVG_START.test(text.slice(0, 4096));
10185
+ },
10186
+ "text/css": (bytes) => decodeText(bytes) !== null,
10187
+ "font/woff": (bytes) => hasText(bytes, "wOFF"),
10188
+ "font/woff2": (bytes) => hasText(bytes, "wOF2"),
10189
+ "font/ttf": (bytes) => hasBytes(bytes, [0, 1, 0, 0]) || hasText(bytes, "true"),
10190
+ "font/otf": (bytes) => hasText(bytes, "OTTO")
10191
+ };
10192
+ var rawPayload = (path, mediaType, bytes) => {
10193
+ if (!RAW_SIGNATURES[mediaType]?.(bytes))
10194
+ throw new FileError("unsupported", `not a valid ${mediaType} file: ${path}`);
10195
+ return { mediaType, base64: bytes.toString("base64") };
10196
+ };
10197
+
10198
+ // src/worker/files/commit.ts
10199
+ var MISSING_OBJECT_EXIT = 128;
10200
+ var SEARCH_MAX_OUTPUT = 64 * 1024 * 1024;
10201
+ var REGULAR_FILE_MODES = /* @__PURE__ */ new Set(["100644", "100755"]);
10202
+ var parseTree = (stdout) => stdout.toString("utf8").split("\0").flatMap((record) => {
10203
+ const tab = record.indexOf(" ");
10204
+ if (tab < 0) return [];
10205
+ const [mode = "", type = "", oid = "", size = ""] = record.slice(0, tab).split(/ +/);
10206
+ const bytes = /^\d+$/.test(size) ? Number(size) : null;
10207
+ return [{ mode, type, oid, size: bytes, path: record.slice(tab + 1) }];
10208
+ });
10209
+ var isRegularFile = (entry) => entry.type === "blob" && REGULAR_FILE_MODES.has(entry.mode);
10210
+ var lsTree = async (repo, args) => {
10211
+ try {
10212
+ return parseTree(await runGit(repo, ["ls-tree", "-z", ...args]));
10213
+ } catch (error) {
10214
+ if (error.code === MISSING_OBJECT_EXIT) return null;
10215
+ throw error;
10216
+ }
10217
+ };
10218
+ var listCommitDirectory = async (repo, commit, path) => {
10219
+ const rel = checkedPath(path, true);
10220
+ const children = await lsTree(repo, [`${commit}:${rel}`]);
10221
+ if (children === null) throw notFound(rel || ".");
10222
+ const entries = children.flatMap((entry) => {
10223
+ const child = rel ? `${rel}/${entry.path}` : entry.path;
10224
+ if (isSensitiveFilePath(child)) return [];
10225
+ if (entry.type === "tree")
10226
+ return isExcludedDirectoryName(entry.path) ? [] : [{ name: entry.path, path: child, kind: "directory" }];
10227
+ return isRegularFile(entry) ? [{ name: entry.path, path: child, kind: "file" }] : [];
10228
+ }).sort(byKindThenName);
10229
+ return {
10230
+ path: rel,
10231
+ entries: entries.slice(0, FILE_DIRECTORY_MAX_ENTRIES),
10232
+ truncated: entries.length > FILE_DIRECTORY_MAX_ENTRIES
10233
+ };
10234
+ };
10235
+ var searchCommit = async (repo, commit, query3) => {
10236
+ const needle = searchNeedle(query3);
10237
+ let stdout;
10238
+ try {
10239
+ stdout = await runGit(repo, ["ls-tree", "-r", "-z", commit], SEARCH_MAX_OUTPUT);
10240
+ } catch (error) {
10241
+ if (error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
10242
+ throw new FileError("unavailable", "the branch has too many files to search");
10243
+ throw error;
10244
+ }
10245
+ const matches = parseTree(stdout).filter(
10246
+ (entry) => isRegularFile(entry) && !entry.path.split("/").slice(0, -1).some(isExcludedDirectoryName) && !isSensitiveFilePath(entry.path) && entry.path.toLowerCase().includes(needle)
10247
+ );
10248
+ return {
10249
+ entries: matches.slice(0, FILE_SEARCH_MAX_RESULTS).map((entry) => ({
10250
+ name: entry.path.split("/").at(-1) ?? entry.path,
10251
+ path: entry.path,
10252
+ kind: "file"
10253
+ })),
10254
+ truncated: matches.length > FILE_SEARCH_MAX_RESULTS
10255
+ };
10256
+ };
10257
+ var regularEntry = async (repo, commit, rel) => {
10258
+ const entry = (await lsTree(repo, ["-l", commit, "--", rel]))?.find((item) => item.path === rel);
10259
+ if (!entry || !isRegularFile(entry) || entry.size === null) throw notFound(rel);
10260
+ return { oid: entry.oid, size: entry.size };
10261
+ };
10262
+ var viewCommitFile = async (repo, commit, path) => {
10263
+ const rel = checkedPath(path, false);
10264
+ const entry = await regularEntry(repo, commit, rel);
10265
+ const meta = { path: rel, name: rel.split("/").at(-1) ?? rel, commit };
10266
+ const mediaType = imageMediaType(rel);
10267
+ if (mediaType)
10268
+ return entry.size > FILE_RAW_MAX_BYTES ? { ...meta, size: entry.size, kind: "too_large", limit: FILE_RAW_MAX_BYTES } : { ...meta, size: entry.size, kind: "image", mediaType };
10269
+ if (entry.size > FILE_TEXT_MAX_BYTES)
10270
+ return { ...meta, size: entry.size, kind: "too_large", limit: FILE_TEXT_MAX_BYTES };
10271
+ const bytes = await runGit(repo, ["cat-file", "blob", entry.oid], FILE_TEXT_MAX_BYTES + 1);
10272
+ const content = decodeText(bytes);
10273
+ return content === null ? { ...meta, size: bytes.length, kind: "binary" } : { ...meta, size: bytes.length, kind: "text", content };
10274
+ };
10275
+ var readRawCommitFile = async (repo, commit, path) => {
10276
+ const rel = checkedPath(path, false);
10277
+ const mediaType = rawMediaType(rel);
10278
+ if (!mediaType) throw new FileError("unsupported", `not served raw: ${rel}`);
10279
+ const entry = await regularEntry(repo, commit, rel);
10280
+ if (entry.size > FILE_RAW_MAX_BYTES)
10281
+ throw new FileError("unsupported", `too large to serve raw: ${rel}`);
10282
+ const bytes = await runGit(repo, ["cat-file", "blob", entry.oid], FILE_RAW_MAX_BYTES + 1);
10283
+ return rawPayload(rel, mediaType, bytes);
10284
+ };
10285
+
10286
+ // src/worker/files/disk.ts
10287
+ import { constants as constants2 } from "node:fs";
10288
+ import { open as open2, readdir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
10289
+ import { isAbsolute as isAbsolute4, join as join15, relative, sep } from "node:path";
10290
+ var isMissing = (error) => ["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code ?? "");
10291
+ var childPath = (parent, name) => parent ? `${parent}/${name}` : name;
10292
+ var inside = (root, real) => {
10293
+ const rel = relative(root, real);
10294
+ if (isAbsolute4(rel) || rel === ".." || rel.startsWith(`..${sep}`)) return null;
10295
+ return rel.split(sep).join("/");
10296
+ };
10297
+ var resolveInside = async (root, path, allowRoot) => {
10298
+ const rel = checkedPath(path, allowRoot);
10299
+ let real;
10300
+ try {
10301
+ real = await realpath3(join15(root, rel));
10302
+ } catch (error) {
10303
+ if (isMissing(error)) throw notFound(rel);
10304
+ throw error;
10305
+ }
10306
+ const target = inside(root, real);
10307
+ if (target === null || isSensitiveFilePath(target)) throw notFound(rel);
10308
+ return { path: rel, real };
10309
+ };
10310
+ var symlinkEntry = async (root, linkPath, name, path) => {
10311
+ let real;
10312
+ try {
10313
+ real = await realpath3(linkPath);
10314
+ } catch (error) {
10315
+ if (!isMissing(error)) console.warn(`[files] cannot resolve symlink ${linkPath}`, error);
10316
+ return null;
10317
+ }
10318
+ const target = inside(root, real);
10319
+ if (target === null || isSensitiveFilePath(target)) return null;
10320
+ const info = await stat3(real);
10321
+ if (info.isDirectory())
10322
+ return isExcludedDirectoryName(name) ? null : { name, path, kind: "directory" };
10323
+ return info.isFile() ? { name, path, kind: "file" } : null;
10324
+ };
10325
+ var toEntry = async (root, dir, dirent) => {
10326
+ const path = childPath(dir.path, dirent.name);
10327
+ if (isSensitiveFilePath(path)) return null;
10328
+ if (dirent.isDirectory())
10329
+ return isExcludedDirectoryName(dirent.name) ? null : { name: dirent.name, path, kind: "directory" };
10330
+ if (dirent.isFile()) return { name: dirent.name, path, kind: "file" };
10331
+ return dirent.isSymbolicLink() ? symlinkEntry(root, join15(dir.real, dirent.name), dirent.name, path) : null;
10332
+ };
10333
+ var listDirectory = async (root, path) => {
10334
+ const dir = await resolveInside(root, path, true);
10335
+ if (!(await stat3(dir.real)).isDirectory()) throw notFound(dir.path);
10336
+ const dirents = await readdir(dir.real, { withFileTypes: true });
10337
+ const entries = (await Promise.all(dirents.map((dirent) => toEntry(root, dir, dirent)))).filter((entry) => entry !== null).sort(byKindThenName);
10338
+ return {
10339
+ path: dir.path,
10340
+ entries: entries.slice(0, FILE_DIRECTORY_MAX_ENTRIES),
10341
+ truncated: entries.length > FILE_DIRECTORY_MAX_ENTRIES
10342
+ };
10343
+ };
10344
+ var searchFiles = async (root, query3, now = Date.now) => {
10345
+ const needle = searchNeedle(query3);
10346
+ const deadline = now() + FILE_SEARCH_DEADLINE_MS;
10347
+ const entries = [];
10348
+ const pending = [""];
10349
+ for (let index = 0; index < pending.length; index += 1) {
10350
+ if (now() > deadline) return { entries, truncated: true };
10351
+ const dir = pending[index] ?? "";
10352
+ let dirents;
10353
+ try {
10354
+ dirents = await readdir(join15(root, dir), { withFileTypes: true });
10355
+ } catch (error) {
10356
+ console.warn(`[files] search skipped unreadable directory ${dir || "."}`, error);
10357
+ continue;
10358
+ }
10359
+ for (const dirent of dirents) {
10360
+ const path = childPath(dir, dirent.name);
10361
+ if (isSensitiveFilePath(path)) continue;
10362
+ if (dirent.isDirectory()) {
10363
+ if (!isExcludedDirectoryName(dirent.name)) pending.push(path);
10364
+ } else if (dirent.isFile() && path.toLowerCase().includes(needle)) {
10365
+ if (entries.length === FILE_SEARCH_MAX_RESULTS) return { entries, truncated: true };
10366
+ entries.push({ name: dirent.name, path, kind: "file" });
10367
+ }
10368
+ }
10369
+ }
10370
+ return { entries, truncated: false };
10371
+ };
10372
+ var readWithin = async (file, limit) => {
10373
+ const info = await stat3(file.real);
10374
+ if (!info.isFile()) throw notFound(file.path);
10375
+ if (info.size > limit) return { oversize: info.size };
10376
+ const handle = await open2(file.real, constants2.O_RDONLY | constants2.O_NONBLOCK);
10377
+ try {
10378
+ const opened = await handle.stat();
10379
+ if (!opened.isFile()) throw notFound(file.path);
10380
+ if (opened.size > limit) return { oversize: opened.size };
10381
+ const bytes = await handle.readFile();
10382
+ return bytes.length > limit ? { oversize: bytes.length } : { bytes };
10383
+ } finally {
10384
+ await handle.close();
10385
+ }
10386
+ };
10387
+ var viewFile = async (root, path) => {
10388
+ const file = await resolveInside(root, path, false);
10389
+ const meta = { path: file.path, name: file.path.split("/").at(-1) ?? file.path, commit: null };
10390
+ const mediaType = imageMediaType(file.path);
10391
+ if (mediaType) {
10392
+ const info = await stat3(file.real);
10393
+ if (!info.isFile()) throw notFound(file.path);
10394
+ return info.size > FILE_RAW_MAX_BYTES ? { ...meta, size: info.size, kind: "too_large", limit: FILE_RAW_MAX_BYTES } : { ...meta, size: info.size, kind: "image", mediaType };
10395
+ }
10396
+ const read = await readWithin(file, FILE_TEXT_MAX_BYTES);
10397
+ if ("oversize" in read)
10398
+ return { ...meta, size: read.oversize, kind: "too_large", limit: FILE_TEXT_MAX_BYTES };
10399
+ const content = decodeText(read.bytes);
10400
+ return content === null ? { ...meta, size: read.bytes.length, kind: "binary" } : { ...meta, size: read.bytes.length, kind: "text", content };
10401
+ };
10402
+ var readRawFile = async (root, path) => {
10403
+ const file = await resolveInside(root, path, false);
10404
+ const mediaType = rawMediaType(file.path);
10405
+ if (!mediaType) throw new FileError("unsupported", `not served raw: ${file.path}`);
10406
+ const read = await readWithin(file, FILE_RAW_MAX_BYTES);
10407
+ if ("oversize" in read) throw new FileError("unsupported", `too large to serve raw: ${file.path}`);
10408
+ return rawPayload(file.path, mediaType, read.bytes);
10409
+ };
10410
+
10411
+ // src/worker/files/handler.ts
10412
+ var DEFAULT_CONCURRENCY = 4;
10413
+ var readerFor = (baseDir, branch) => {
10414
+ const { worktree, commit } = branch;
10415
+ if (worktree !== null)
10416
+ return {
10417
+ list: (path) => listDirectory(worktree, path),
10418
+ search: (query3) => searchFiles(worktree, query3),
10419
+ view: (path) => viewFile(worktree, path),
10420
+ raw: (path) => readRawFile(worktree, path)
10421
+ };
10422
+ if (commit === null) throw new FileError("branch_not_found", `branch ${branch.name} is empty`);
10423
+ return {
10424
+ list: (path) => listCommitDirectory(baseDir, commit, path),
10425
+ search: (query3) => searchCommit(baseDir, commit, query3),
10426
+ view: (path) => viewCommitFile(baseDir, commit, path),
10427
+ raw: (path) => readRawCommitFile(baseDir, commit, path)
10428
+ };
10429
+ };
10430
+ var run2 = async (baseDir, query3) => {
10431
+ const branches = await discoverBranches(baseDir);
10432
+ if (query3.op === "branches") return { ok: true, data: branches };
10433
+ const branch = branches.find((candidate) => candidate.name === query3.branch);
10434
+ if (!branch) throw new FileError("branch_not_found", `branch ${query3.branch} does not exist`);
10435
+ const reader = readerFor(baseDir, branch);
10436
+ if (query3.op === "list") return { ok: true, data: await reader.list(query3.path) };
10437
+ if (query3.op === "search") return { ok: true, data: await reader.search(query3.query) };
10438
+ if (query3.op === "raw") return { ok: true, data: await reader.raw(query3.path) };
10439
+ return { ok: true, data: await reader.view(query3.path) };
10440
+ };
10441
+ var handleFileQuery = async (baseDir, query3) => {
10442
+ try {
10443
+ return await run2(baseDir, query3);
10444
+ } catch (error) {
10445
+ if (error instanceof FileError) return { ok: false, code: error.code, message: error.message };
10446
+ console.error(`[files] ${query3.op} query failed`, error);
10447
+ return { ok: false, code: "unavailable", message: "the worker could not complete the read" };
10448
+ }
10449
+ };
10450
+ var createFileQueryRunner = (baseDir, limit = DEFAULT_CONCURRENCY) => {
10451
+ let active = 0;
10452
+ const waiting = [];
10453
+ const acquire = async () => {
10454
+ if (active < limit) {
10455
+ active += 1;
10456
+ return;
10457
+ }
10458
+ await new Promise((resolve4) => waiting.push(resolve4));
10459
+ };
10460
+ const release = () => {
10461
+ const next = waiting.shift();
10462
+ if (next) next();
10463
+ else active -= 1;
10464
+ };
10465
+ return async (query3) => {
10466
+ await acquire();
10467
+ try {
10468
+ return await handleFileQuery(baseDir, query3);
10469
+ } finally {
10470
+ release();
10471
+ }
10472
+ };
9282
10473
  };
9283
10474
 
9284
10475
  // src/worker/watchdog.ts
@@ -9339,12 +10530,13 @@ var createLiveness = (deps) => {
9339
10530
 
9340
10531
  // src/worker/service-supervisor.ts
9341
10532
  import { mkdirSync as mkdirSync4 } from "node:fs";
9342
- import { join as join10 } from "node:path";
10533
+ import { stat as stat4 } from "node:fs/promises";
10534
+ import { join as join17 } from "node:path";
9343
10535
 
9344
10536
  // src/worker/service-process.ts
9345
10537
  import { spawn as spawn2 } from "node:child_process";
9346
10538
  import { closeSync, existsSync as existsSync6, openSync } from "node:fs";
9347
- import { dirname as dirname3, join as join9 } from "node:path";
10539
+ import { dirname as dirname4, join as join16 } from "node:path";
9348
10540
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9349
10541
  var START_TIMEOUT_MS = 1e4;
9350
10542
  var isWrapperMessage = (message) => {
@@ -9354,8 +10546,8 @@ var isWrapperMessage = (message) => {
9354
10546
  return value.type === "error" && typeof value.error === "string";
9355
10547
  };
9356
10548
  var binPath = () => {
9357
- const here = dirname3(fileURLToPath2(import.meta.url));
9358
- const devShim = join9(here, "..", "..", "bin", "baton.mjs");
10549
+ const here = dirname4(fileURLToPath2(import.meta.url));
10550
+ const devShim = join16(here, "..", "..", "bin", "baton.mjs");
9359
10551
  return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
9360
10552
  };
9361
10553
  var startServiceProcess = (input) => {
@@ -9370,7 +10562,7 @@ var startServiceProcess = (input) => {
9370
10562
  } finally {
9371
10563
  closeSync(logFd);
9372
10564
  }
9373
- const started = new Promise((resolve3, reject) => {
10565
+ const started = new Promise((resolve4, reject) => {
9374
10566
  let settled = false;
9375
10567
  let timer;
9376
10568
  const cleanup = () => {
@@ -9384,7 +10576,7 @@ var startServiceProcess = (input) => {
9384
10576
  settled = true;
9385
10577
  cleanup();
9386
10578
  if (result instanceof Error) reject(result);
9387
- else resolve3(result);
10579
+ else resolve4(result);
9388
10580
  };
9389
10581
  const onMessage = (message) => {
9390
10582
  if (!isWrapperMessage(message)) return;
@@ -9409,13 +10601,13 @@ var startServiceProcess = (input) => {
9409
10601
  };
9410
10602
 
9411
10603
  // src/worker/service-supervisor.ts
9412
- var waitForExit = (child) => child.exitCode !== null ? Promise.resolve() : new Promise((resolve3) => child.once("exit", () => resolve3()));
10604
+ var waitForExit = (child) => child.exitCode !== null ? Promise.resolve() : new Promise((resolve4) => child.once("exit", () => resolve4()));
9413
10605
  var createServiceSupervisor = (deps) => {
9414
- const { getSession, sendReport, cfg, log } = deps;
10606
+ const { sendReport, cfg, log } = deps;
9415
10607
  const entries = /* @__PURE__ */ new Map();
9416
10608
  let reportQueue = Promise.resolve();
9417
- const snapshot = () => [...entries.values()].filter((entry) => entry.appPid !== void 0).map(({ sessionId, name, startedAt, publicUrl, note }) => ({
9418
- sessionId,
10609
+ const snapshot = () => [...entries.values()].filter((entry) => entry.appPid !== void 0).map(({ cwd, name, startedAt, publicUrl, note }) => ({
10610
+ cwd,
9419
10611
  name,
9420
10612
  startedAt,
9421
10613
  ...publicUrl ? { publicUrl } : {},
@@ -9425,15 +10617,13 @@ var createServiceSupervisor = (deps) => {
9425
10617
  const body = { services: snapshot(), ...result ? { result } : {} };
9426
10618
  const request2 = reportQueue.then(() => sendReport(body)).then(() => {
9427
10619
  });
9428
- reportQueue = request2.catch(() => {
9429
- });
10620
+ reportQueue = request2.catch((error) => log(`service report failed: ${String(error)}`));
9430
10621
  return request2;
9431
10622
  };
9432
10623
  const remove = (entry) => {
9433
10624
  if (entries.get(entry.name) !== entry) return;
9434
10625
  entries.delete(entry.name);
9435
- void report().catch(() => {
9436
- });
10626
+ void report().catch((error) => log(`service removal report failed: ${String(error)}`));
9437
10627
  };
9438
10628
  const stopEntry = (entry) => {
9439
10629
  if (entry.wrapper.connected) {
@@ -9442,7 +10632,18 @@ var createServiceSupervisor = (deps) => {
9442
10632
  }
9443
10633
  if (entry.appPid) killPidGroup(entry.appPid, "SIGKILL");
9444
10634
  };
9445
- const runOne = async (requestId, sessionId, name, argv, publicUrl, note) => {
10635
+ const runOne = async (requestId, cwd, name, argv, publicUrl, note) => {
10636
+ try {
10637
+ if (!(await stat4(cwd)).isDirectory())
10638
+ return report({ requestId, ok: false, error: `not a directory: ${cwd}`, status: 400 });
10639
+ } catch (error) {
10640
+ const code = error.code;
10641
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
10642
+ return report({ requestId, ok: false, error: `directory not found: ${cwd}`, status: 400 });
10643
+ }
10644
+ const logDir = join17(workerRuntimeDir(cfg), "services");
10645
+ const logPath = join17(logDir, `${name}.log`);
10646
+ mkdirSync4(logDir, { recursive: true, mode: 448 });
9446
10647
  if (entries.has(name))
9447
10648
  return report({
9448
10649
  requestId,
@@ -9450,21 +10651,10 @@ var createServiceSupervisor = (deps) => {
9450
10651
  error: `service "${name}" is already running`,
9451
10652
  status: 409
9452
10653
  });
9453
- const session2 = await getSession(sessionId);
9454
- if (session2.workerId !== cfg.workerId || !session2.worktreePath)
9455
- return report({
9456
- requestId,
9457
- ok: false,
9458
- error: "session is not materialized on this worker",
9459
- status: 400
9460
- });
9461
- const logDir = join10(session2.worktreePath, ".baton-services");
9462
- const logPath = join10(logDir, `${name}.log`);
9463
- mkdirSync4(logDir, { recursive: true });
9464
- const { wrapper, started } = startServiceProcess({ argv, cwd: session2.worktreePath, logPath });
10654
+ const { wrapper, started } = startServiceProcess({ argv, cwd, logPath });
9465
10655
  const entry = {
9466
10656
  wrapper,
9467
- sessionId,
10657
+ cwd,
9468
10658
  name,
9469
10659
  startedAt: Date.now(),
9470
10660
  publicUrl,
@@ -9483,19 +10673,20 @@ var createServiceSupervisor = (deps) => {
9483
10673
  }
9484
10674
  const service2 = {
9485
10675
  workerId: cfg.workerId,
9486
- sessionId,
10676
+ cwd,
9487
10677
  name,
9488
10678
  startedAt: entry.startedAt,
9489
10679
  ...publicUrl ? { publicUrl } : {},
9490
10680
  ...note ? { note } : {}
9491
10681
  };
9492
- log(`service ${name} started in ${session2.worktreePath}`);
10682
+ log(`service ${name} started in ${cwd}`);
9493
10683
  await report({ requestId, ok: true, service: service2, logPath });
9494
10684
  };
9495
- const run2 = async (requestId, sessionId, name, argv, publicUrl, note) => {
10685
+ const run4 = async (requestId, cwd, name, argv, publicUrl, note) => {
9496
10686
  try {
9497
- await runOne(requestId, sessionId, name, argv, publicUrl, note);
10687
+ await runOne(requestId, cwd, name, argv, publicUrl, note);
9498
10688
  } catch (error) {
10689
+ log(`service ${name} failed: ${String(error)}`);
9499
10690
  await report({ requestId, ok: false, error: String(error), status: 500 });
9500
10691
  }
9501
10692
  };
@@ -9508,21 +10699,16 @@ var createServiceSupervisor = (deps) => {
9508
10699
  remove(entry);
9509
10700
  await report({ requestId, ok: true });
9510
10701
  };
9511
- const stopSession = async (sessionId) => {
9512
- const matches = [...entries.values()].filter((entry) => entry.sessionId === sessionId);
9513
- matches.forEach(stopEntry);
9514
- await Promise.all(matches.map((entry) => waitForExit(entry.wrapper)));
9515
- matches.forEach(remove);
9516
- };
9517
10702
  const killAll = () => entries.forEach(stopEntry);
9518
- return { run: run2, stop, stopSession, report, killAll };
10703
+ return { run: run4, stop, report, killAll };
9519
10704
  };
9520
10705
 
9521
10706
  // src/worker/session-supervisor.ts
9522
10707
  import { spawn as spawn3 } from "node:child_process";
9523
10708
  import { randomUUID as randomUUID2 } from "node:crypto";
9524
10709
  import { existsSync as existsSync7 } from "node:fs";
9525
- import { dirname as dirname4, join as join13 } from "node:path";
10710
+ import { mkdir as mkdir6, rm as rm3, stat as stat5 } from "node:fs/promises";
10711
+ import { dirname as dirname5, join as join21 } from "node:path";
9526
10712
  import { fileURLToPath as fileURLToPath3 } from "node:url";
9527
10713
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
9528
10714
 
@@ -9559,7 +10745,7 @@ var buildTitlePrompt = (userText, assistantText) => {
9559
10745
  ].join("\n");
9560
10746
  };
9561
10747
  var generateTitle = async (input) => {
9562
- const { worktreePath, userText, assistantText, queryFn } = input;
10748
+ const { cwd, userText, assistantText, queryFn } = input;
9563
10749
  if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
9564
10750
  const exe = claudeExecutable();
9565
10751
  const abort = new AbortController();
@@ -9572,7 +10758,7 @@ var generateTitle = async (input) => {
9572
10758
  const messages = queryFn({
9573
10759
  prompt: buildTitlePrompt(userText, assistantText),
9574
10760
  options: {
9575
- cwd: worktreePath,
10761
+ cwd,
9576
10762
  // A one-shot summarizer, not an agent: replace the ~18k-token Claude
9577
10763
  // Code preset system prompt. Inside the agent harness the model
9578
10764
  // routinely answers NONE to this ask (verified against reclaude);
@@ -9616,7 +10802,7 @@ var generateTitle = async (input) => {
9616
10802
  // src/session/runner/title-codex.ts
9617
10803
  import { mkdtemp, rm } from "node:fs/promises";
9618
10804
  import { tmpdir as tmpdir3 } from "node:os";
9619
- import { join as join11 } from "node:path";
10805
+ import { join as join18 } from "node:path";
9620
10806
  import { Codex as Codex2 } from "@openai/codex-sdk";
9621
10807
  var generateTitleWithCodex = async (input) => {
9622
10808
  const { userText, assistantText } = input;
@@ -9625,7 +10811,7 @@ var generateTitleWithCodex = async (input) => {
9625
10811
  const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
9626
10812
  let cwd = "";
9627
10813
  try {
9628
- cwd = await mkdtemp(join11(tmpdir3(), "baton-title-"));
10814
+ cwd = await mkdtemp(join18(tmpdir3(), "baton-title-"));
9629
10815
  const client = input.client ?? new Codex2();
9630
10816
  const thread = client.startThread({
9631
10817
  workingDirectory: cwd,
@@ -9651,19 +10837,79 @@ var generateTitleWithCodex = async (input) => {
9651
10837
  }
9652
10838
  };
9653
10839
 
10840
+ // src/session/runner/title-pi.ts
10841
+ import { mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
10842
+ import { tmpdir as tmpdir4 } from "node:os";
10843
+ import { join as join19 } from "node:path";
10844
+ var generateTitleWithPi = async (input) => {
10845
+ const { userText, assistantText } = input;
10846
+ if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
10847
+ let cwd = "";
10848
+ let timedOut = false;
10849
+ let timer;
10850
+ try {
10851
+ cwd = await mkdtemp2(join19(tmpdir4(), "baton-title-"));
10852
+ const pi = await import("@earendil-works/pi-coding-agent");
10853
+ const agentDir = pi.getAgentDir();
10854
+ const resourceLoader = new pi.DefaultResourceLoader({
10855
+ cwd,
10856
+ agentDir,
10857
+ noExtensions: true,
10858
+ noSkills: true,
10859
+ noPromptTemplates: true,
10860
+ noThemes: true,
10861
+ noContextFiles: true,
10862
+ systemPrompt: "You title chat sessions."
10863
+ });
10864
+ await resourceLoader.reload();
10865
+ const { session: session2 } = await pi.createAgentSession({
10866
+ cwd,
10867
+ agentDir,
10868
+ resourceLoader,
10869
+ sessionManager: pi.SessionManager.inMemory(cwd),
10870
+ noTools: "all"
10871
+ });
10872
+ let error;
10873
+ session2.subscribe((event) => {
10874
+ if (event.type === "message_end" && event.message.role === "assistant")
10875
+ error = event.message.stopReason === "error" ? event.message.errorMessage : void 0;
10876
+ });
10877
+ timer = setTimeout(() => {
10878
+ timedOut = true;
10879
+ void session2.abort().catch((e) => console.error(`[title] pi abort: ${String(e)}`));
10880
+ }, TITLE_TIMEOUT_MS);
10881
+ try {
10882
+ await session2.prompt(buildTitlePrompt(userText, assistantText));
10883
+ if (timedOut) return { kind: "error", reason: `timeout after ${TITLE_TIMEOUT_MS / 1e3}s` };
10884
+ if (error) return { kind: "error", reason: error.slice(0, 300) };
10885
+ return titleOutcome(session2.getLastAssistantText() ?? "");
10886
+ } finally {
10887
+ session2.dispose();
10888
+ }
10889
+ } catch (error) {
10890
+ return { kind: "error", reason: String(error).slice(0, 300) };
10891
+ } finally {
10892
+ clearTimeout(timer);
10893
+ if (cwd)
10894
+ await rm2(cwd, { recursive: true, force: true }).catch(
10895
+ (e) => console.error(`[title] cleanup ${cwd}: ${String(e)}`)
10896
+ );
10897
+ }
10898
+ };
10899
+
9654
10900
  // src/worker/session-process.ts
9655
- import { execFile as execFile2 } from "node:child_process";
9656
- import { readFile, rename, writeFile as writeFile3 } from "node:fs/promises";
9657
- import { join as join12 } from "node:path";
9658
- import { promisify as promisify2 } from "node:util";
9659
- var run = promisify2(execFile2);
10901
+ import { execFile as execFile5 } from "node:child_process";
10902
+ import { readFile as readFile3, rename, writeFile as writeFile4 } from "node:fs/promises";
10903
+ import { join as join20 } from "node:path";
10904
+ import { promisify as promisify5 } from "node:util";
10905
+ var run3 = promisify5(execFile5);
9660
10906
  var RUNNER_RECORD = ".baton-runner.json";
9661
- var recordPath = (worktreePath) => join12(worktreePath, RUNNER_RECORD);
10907
+ var recordPath = (cwd) => join20(cwd, RUNNER_RECORD);
9662
10908
  var codeOf = (error) => typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
9663
- var readRunnerRecord = async (worktreePath) => {
10909
+ var readRunnerRecord = async (cwd) => {
9664
10910
  let raw;
9665
10911
  try {
9666
- raw = await readFile(recordPath(worktreePath), "utf8");
10912
+ raw = await readFile3(recordPath(cwd), "utf8");
9667
10913
  } catch (error) {
9668
10914
  if (codeOf(error) === "ENOENT") return null;
9669
10915
  throw error;
@@ -9673,10 +10919,10 @@ var readRunnerRecord = async (worktreePath) => {
9673
10919
  throw new Error("invalid session process record");
9674
10920
  return value;
9675
10921
  };
9676
- var writeRunnerRecord = async (worktreePath, record) => {
9677
- const path = recordPath(worktreePath);
10922
+ var writeRunnerRecord = async (cwd, record) => {
10923
+ const path = recordPath(cwd);
9678
10924
  const temporary = `${path}.${record.runnerToken}.tmp`;
9679
- await writeFile3(temporary, JSON.stringify(record), { mode: 384, flag: "wx" });
10925
+ await writeFile4(temporary, JSON.stringify(record), { mode: 384, flag: "wx" });
9680
10926
  await rename(temporary, path);
9681
10927
  };
9682
10928
  var groupExists = (pgid) => {
@@ -9701,7 +10947,7 @@ var stopRecordedRunner = async (record, expected, graceMs = 5e3) => {
9701
10947
  if (!groupExists(record.pgid)) return;
9702
10948
  let stdout;
9703
10949
  try {
9704
- const result = await run("ps", ["-ww", "-p", String(record.pid), "-o", "pgid=", "-o", "args="]);
10950
+ const result = await run3("ps", ["-ww", "-p", String(record.pid), "-o", "pgid=", "-o", "args="]);
9705
10951
  stdout = result.stdout;
9706
10952
  } catch (error) {
9707
10953
  if (!groupExists(record.pgid)) return;
@@ -9714,37 +10960,23 @@ var stopRecordedRunner = async (record, expected, graceMs = 5e3) => {
9714
10960
  signalGroup(record.pgid, "SIGTERM");
9715
10961
  const deadline = Date.now() + graceMs;
9716
10962
  while (groupExists(record.pgid) && Date.now() < deadline)
9717
- await new Promise((resolve3) => setTimeout(resolve3, 50));
10963
+ await new Promise((resolve4) => setTimeout(resolve4, 50));
9718
10964
  if (!groupExists(record.pgid)) return;
9719
10965
  signalGroup(record.pgid, "SIGKILL");
9720
10966
  const killDeadline = Date.now() + graceMs;
9721
10967
  while (groupExists(record.pgid) && Date.now() < killDeadline)
9722
- await new Promise((resolve3) => setTimeout(resolve3, 50));
10968
+ await new Promise((resolve4) => setTimeout(resolve4, 50));
9723
10969
  if (groupExists(record.pgid)) throw new Error("session process group has not exited");
9724
10970
  };
9725
10971
 
9726
10972
  // src/worker/session-supervisor.ts
9727
10973
  var binPath2 = () => {
9728
- const here = dirname4(fileURLToPath3(import.meta.url));
9729
- const devShim = join13(here, "..", "..", "bin", "baton.mjs");
10974
+ const here = dirname5(fileURLToPath3(import.meta.url));
10975
+ const devShim = join21(here, "..", "..", "bin", "baton.mjs");
9730
10976
  return existsSync7(devShim) ? devShim : fileURLToPath3(import.meta.url);
9731
10977
  };
9732
10978
  var createSessionSupervisor = (deps) => {
9733
- const { client, cfg, repo, log, hasTerminal, closeTerminal } = deps;
9734
- const worktreeDir = defaultWorktreeDir();
9735
- const baseBranch = cfg.baseBranch ?? repoHeadBranch(repo);
9736
- const syncBase = deps.syncBase ?? syncBaseBranch;
9737
- let syncInFlight = null;
9738
- const syncedBase = () => {
9739
- if (syncInFlight) return syncInFlight;
9740
- const pending = syncBase(repo, baseBranch);
9741
- syncInFlight = pending;
9742
- const clear = () => {
9743
- if (syncInFlight === pending) syncInFlight = null;
9744
- };
9745
- void pending.then(clear, clear);
9746
- return pending;
9747
- };
10979
+ const { client, cfg, cwd, log, hasTerminal, closeTerminal } = deps;
9748
10980
  const children = /* @__PURE__ */ new Map();
9749
10981
  const lifecycle = /* @__PURE__ */ new Map();
9750
10982
  const enqueue = (id, action) => {
@@ -9763,8 +10995,8 @@ var createSessionSupervisor = (deps) => {
9763
10995
  const cancelStart = (sessionId) => {
9764
10996
  startEpochs.set(sessionId, currentStartEpoch(sessionId) + 1);
9765
10997
  };
9766
- const stopRecord = async (sessionId, worktreePath, token) => {
9767
- const record = await readRunnerRecord(worktreePath);
10998
+ const stopRecord = async (sessionId, token) => {
10999
+ const record = await readRunnerRecord(sessionRuntimeDir(cfg, sessionId));
9768
11000
  if (!record) {
9769
11001
  if (token) throw new Error("missing record for an unfinished execution");
9770
11002
  return;
@@ -9773,9 +11005,9 @@ var createSessionSupervisor = (deps) => {
9773
11005
  throw new Error("runner identity does not match the attempt");
9774
11006
  await stopRecordedRunner(record, { server: cfg.server, workerId: cfg.workerId, sessionId });
9775
11007
  };
9776
- const spawnChild = async (sessionId, worktreePath, wasCanceled) => {
11008
+ const spawnChild = async (sessionId, cwd2, wasCanceled) => {
9777
11009
  if (children.has(sessionId)) return;
9778
- await stopRecord(sessionId, worktreePath);
11010
+ await stopRecord(sessionId);
9779
11011
  const runnerToken = randomUUID2();
9780
11012
  const child = spawn3(
9781
11013
  process.execPath,
@@ -9783,16 +11015,17 @@ var createSessionSupervisor = (deps) => {
9783
11015
  {
9784
11016
  detached: true,
9785
11017
  stdio: ["inherit", "inherit", "inherit", "ipc"],
9786
- env: { ...process.env, BATON_SERVER: cfg.server, BATON_WORKER_TOKEN: cfg.apiToken }
11018
+ cwd: cwd2,
11019
+ env: workerEnv(cfg)
9787
11020
  }
9788
11021
  );
9789
- const entry = { child, worktreePath };
11022
+ const entry = { child };
9790
11023
  children.set(sessionId, entry);
9791
11024
  let readyTimer;
9792
- const ready = new Promise((resolve3, reject) => {
11025
+ const ready = new Promise((resolve4, reject) => {
9793
11026
  child.on("message", (message) => {
9794
11027
  if (typeof message === "object" && message !== null && "type" in message && message.type === "ready")
9795
- resolve3();
11028
+ resolve4();
9796
11029
  });
9797
11030
  child.once("error", reject);
9798
11031
  child.once("exit", () => reject(new Error("session child exited before ready")));
@@ -9812,8 +11045,8 @@ var createSessionSupervisor = (deps) => {
9812
11045
  void stop(sessionId).catch((error) => log(`stuck execution cleanup failed: ${String(error)}`));
9813
11046
  });
9814
11047
  try {
9815
- await new Promise((resolve3, reject) => {
9816
- child.once("spawn", resolve3);
11048
+ await new Promise((resolve4, reject) => {
11049
+ child.once("spawn", resolve4);
9817
11050
  child.once("error", reject);
9818
11051
  });
9819
11052
  if (!child.pid) throw new Error("session child has no PID");
@@ -9825,11 +11058,11 @@ var createSessionSupervisor = (deps) => {
9825
11058
  pid: child.pid,
9826
11059
  pgid: child.pid
9827
11060
  };
9828
- await writeRunnerRecord(worktreePath, record);
11061
+ await writeRunnerRecord(sessionRuntimeDir(cfg, sessionId), record);
9829
11062
  await readiness;
9830
11063
  if (wasCanceled()) return;
9831
11064
  await new Promise(
9832
- (resolve3, reject) => child.send({ type: "execute" }, (error) => error ? reject(error) : resolve3())
11065
+ (resolve4, reject) => child.send({ type: "execute" }, (error) => error ? reject(error) : resolve4())
9833
11066
  );
9834
11067
  } catch (error) {
9835
11068
  killProcessGroup(child);
@@ -9847,37 +11080,20 @@ var createSessionSupervisor = (deps) => {
9847
11080
  if (execution.paused || execution.contextResetRequested || execution.attempt?.status === "running" || execution.attempt?.status === "stopping")
9848
11081
  return;
9849
11082
  if (wasCanceled()) return log(`session #${sessionId} start canceled before materializing`);
9850
- let worktreePath = session2.worktreePath;
9851
- if (!session2.agentSessionId || !worktreePath) {
9852
- const sessionCode = randomUUID2();
9853
- const agentSessionId = session2.agentKind === "codex" ? `pending:${sessionCode}` : sessionCode;
9854
- worktreePath = join13(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
9855
- const base = await syncedBase();
9856
- if (wasCanceled()) return log(`session #${sessionId} start canceled during git sync`);
9857
- createWorktree({
9858
- repo,
9859
- worktreePath,
9860
- sessionCode: sessionCode.slice(0, 8),
9861
- base
9862
- });
9863
- await client.sessions.materialize(sessionId, { agentSessionId, worktreePath }, cfg.apiToken);
9864
- if (wasCanceled()) return log(`session #${sessionId} start canceled after materializing`);
9865
- log(`materialized session #${sessionId} \u2192 ${worktreePath}`);
9866
- } else if (!existsSync7(worktreePath)) {
9867
- await restoreWorktree(repo, worktreePath, session2.agentSessionId.slice(0, 8), baseBranch);
9868
- if (wasCanceled()) return log(`session #${sessionId} start canceled during worktree restore`);
9869
- log(`recreated session #${sessionId} worktree (was missing) \u2192 ${worktreePath}`);
9870
- }
9871
- ensureExcluded(repo, PROJECT_CONFIG_NAME);
9872
- ensureExcluded(repo, ".baton-services/");
9873
- ensureExcluded(repo, RUNNER_RECORD);
9874
- ensureExcluded(repo, ".baton-runner.json.*.tmp");
9875
- saveProjectConfig(join13(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg, sessionId));
9876
- syncBundledArtifactSkill(repo, worktreePath);
11083
+ const directory2 = session2.cwd ?? cwd;
11084
+ if (!(await stat5(directory2)).isDirectory()) throw new Error(`not a directory: ${directory2}`);
11085
+ if (wasCanceled()) return;
11086
+ if (!session2.agentSessionId || !session2.cwd) {
11087
+ const id = randomUUID2();
11088
+ const agentSessionId = session2.agentKind === "codex" ? `pending:${id}` : id;
11089
+ await client.sessions.materialize(sessionId, { agentSessionId, cwd: directory2 }, cfg.apiToken);
11090
+ log(`materialized session #${sessionId} (${name}) \u2192 ${directory2}`);
11091
+ }
11092
+ await mkdir6(sessionRuntimeDir(cfg, sessionId), { recursive: true, mode: 448 });
9877
11093
  if (hasTerminal(sessionId))
9878
11094
  return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
9879
11095
  if (wasCanceled()) return;
9880
- await spawnChild(sessionId, worktreePath, wasCanceled);
11096
+ await spawnChild(sessionId, directory2, wasCanceled);
9881
11097
  };
9882
11098
  const start = (sessionId, name) => {
9883
11099
  const epoch = currentStartEpoch(sessionId);
@@ -9895,38 +11111,31 @@ var createSessionSupervisor = (deps) => {
9895
11111
  cancelStart(sessionId);
9896
11112
  return enqueue(sessionId, async () => {
9897
11113
  const entry = children.get(sessionId);
9898
- const session2 = await client.sessions.get(sessionId);
9899
- if (session2.worktreePath) await stopRecord(sessionId, session2.worktreePath);
11114
+ await stopRecord(sessionId);
9900
11115
  if (entry && children.get(sessionId) === entry) children.delete(sessionId);
9901
11116
  });
9902
11117
  };
9903
- const remove = (sessionId, worktreePath) => {
11118
+ const remove = (sessionId) => {
9904
11119
  cancelStart(sessionId);
9905
11120
  closeTerminal(sessionId);
9906
11121
  return enqueue(sessionId, async () => {
9907
11122
  const entry = children.get(sessionId);
9908
- const path = entry?.worktreePath ?? worktreePath;
9909
- if (path) {
9910
- await stopRecord(sessionId, path);
9911
- removeWorktree(repo, path);
9912
- }
11123
+ await stopRecord(sessionId);
11124
+ await rm3(sessionRuntimeDir(cfg, sessionId), { recursive: true, force: true });
9913
11125
  if (children.get(sessionId) === entry) children.delete(sessionId);
9914
11126
  });
9915
11127
  };
9916
- const title = async (sessionId, agentSessionId, worktreePath) => {
11128
+ const title = async (sessionId, agentSessionId, cwd2) => {
9917
11129
  const session2 = await client.sessions.get(sessionId);
9918
11130
  const events = await client.sessions.listEvents(sessionId);
9919
11131
  const exchange = parseFirstExchangeFromEvents(events) ?? (session2.agentKind === "claude-code" ? readFirstExchange(agentSessionId) : null);
9920
11132
  if (!exchange) return log(`title #${sessionId}: no transcript yet, skipping`);
9921
- const outcome = session2.agentKind === "codex" ? await generateTitleWithCodex({
9922
- userText: exchange.userText,
9923
- assistantText: exchange.assistantText
9924
- }) : await generateTitle({
9925
- worktreePath,
9926
- userText: exchange.userText,
9927
- assistantText: exchange.assistantText,
9928
- queryFn: query2
9929
- });
11133
+ const titlers = {
11134
+ "claude-code": (e) => generateTitle({ cwd: cwd2, ...e, queryFn: query2 }),
11135
+ codex: generateTitleWithCodex,
11136
+ pi: generateTitleWithPi
11137
+ };
11138
+ const outcome = await titlers[session2.agentKind](exchange);
9930
11139
  if (outcome.kind === "error") return log(`title #${sessionId} failed: ${outcome.reason}`);
9931
11140
  if (outcome.kind === "declined")
9932
11141
  return log(`title #${sessionId}: not enough to title yet, skipping`);
@@ -9942,10 +11151,10 @@ var createSessionSupervisor = (deps) => {
9942
11151
  const session2 = await client.sessions.get(candidate.id);
9943
11152
  let execution = await client.sessions.execution(session2.id);
9944
11153
  if (execution.attempt?.status === "stopping") {
9945
- if (!session2.worktreePath) throw new Error("unfinished execution without worktree");
11154
+ if (!session2.cwd) throw new Error("unfinished execution without cwd");
9946
11155
  if (children.has(session2.id) && Date.now() - (execution.attempt.stopRequestedAt ?? 0) < 1e4)
9947
11156
  return;
9948
- await stopRecord(session2.id, session2.worktreePath, execution.attempt.runnerToken);
11157
+ await stopRecord(session2.id, execution.attempt.runnerToken);
9949
11158
  children.delete(session2.id);
9950
11159
  await client.sessions.stopped(
9951
11160
  session2.id,
@@ -9955,12 +11164,12 @@ var createSessionSupervisor = (deps) => {
9955
11164
  execution = await client.sessions.execution(session2.id);
9956
11165
  }
9957
11166
  if (execution.paused) {
9958
- if (session2.worktreePath) await stopRecord(session2.id, session2.worktreePath);
11167
+ if (session2.cwd) await stopRecord(session2.id);
9959
11168
  return;
9960
11169
  }
9961
11170
  if (execution.attempt?.status === "running") {
9962
- if (!children.has(session2.id) && session2.worktreePath)
9963
- await stopRecord(session2.id, session2.worktreePath, execution.attempt.runnerToken);
11171
+ if (!children.has(session2.id) && session2.cwd)
11172
+ await stopRecord(session2.id, execution.attempt.runnerToken);
9964
11173
  return;
9965
11174
  }
9966
11175
  if (execution.pendingCount || execution.turn?.status === "recovering")
@@ -9971,7 +11180,7 @@ var createSessionSupervisor = (deps) => {
9971
11180
  for (const [id, entry] of children) {
9972
11181
  if (!live.has(id))
9973
11182
  await enqueue(id, async () => {
9974
- await stopRecord(id, entry.worktreePath);
11183
+ await stopRecord(id);
9975
11184
  if (children.get(id) === entry) children.delete(id);
9976
11185
  });
9977
11186
  }
@@ -9991,7 +11200,7 @@ var createSessionSupervisor = (deps) => {
9991
11200
  };
9992
11201
 
9993
11202
  // src/worker/terminal-manager.ts
9994
- import { existsSync as existsSync9 } from "node:fs";
11203
+ import { statSync as statSync2 } from "node:fs";
9995
11204
  import { spawn as spawn4 } from "node-pty";
9996
11205
 
9997
11206
  // ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
@@ -10007,8 +11216,9 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
10007
11216
  // src/worker/pty.ts
10008
11217
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
10009
11218
  import { createRequire as createModuleRequire } from "node:module";
10010
- import { homedir as homedir3 } from "node:os";
10011
- import { join as join14 } from "node:path";
11219
+ import { homedir as homedir5 } from "node:os";
11220
+ import { dirname as dirname6, join as join22 } from "node:path";
11221
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
10012
11222
  var MAX_TERMINALS = 10;
10013
11223
  var claudePtyCommand = (agentSessionId, hasJsonl, env = process.env) => ({
10014
11224
  file: env.BATON_CLAUDE_BIN?.trim() || "claude",
@@ -10023,39 +11233,45 @@ var codexPtyCommand = (agentSessionId, env = process.env) => {
10023
11233
  args: [moduleRequire.resolve("@openai/codex/bin/codex.js"), ...args]
10024
11234
  };
10025
11235
  };
11236
+ var piPtyCommand = (agentSessionId, env = process.env) => {
11237
+ const args = ["--session-id", agentSessionId];
11238
+ const override = env.BATON_PI_BIN?.trim();
11239
+ if (override) return { file: override, args };
11240
+ const entry = fileURLToPath4(import.meta.resolve("@earendil-works/pi-coding-agent"));
11241
+ return { file: process.execPath, args: [join22(dirname6(entry), "bundle", "cli.js"), ...args] };
11242
+ };
10026
11243
  var hasSessionJsonl = (agentSessionId) => {
10027
- const root = join14(homedir3(), ".claude", "projects");
11244
+ const root = join22(homedir5(), ".claude", "projects");
10028
11245
  if (!existsSync8(root)) return false;
10029
11246
  for (const dir of readdirSync2(root))
10030
- if (existsSync8(join14(root, dir, `${agentSessionId}.jsonl`))) return true;
11247
+ if (existsSync8(join22(root, dir, `${agentSessionId}.jsonl`))) return true;
10031
11248
  return false;
10032
11249
  };
11250
+ var ptyCommand = (kind, agentSessionId) => ({
11251
+ "claude-code": () => claudePtyCommand(agentSessionId, hasSessionJsonl(agentSessionId)),
11252
+ codex: () => codexPtyCommand(agentSessionId),
11253
+ pi: () => piPtyCommand(agentSessionId)
11254
+ })[kind]();
10033
11255
  var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
10034
11256
  var isPositiveDim = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
10035
11257
 
10036
11258
  // src/worker/terminal-manager.ts
10037
11259
  var createTerminalManager = (deps) => {
10038
- const { cfg, log, hasChild, prepareWorktree } = deps;
11260
+ const { cfg, log, hasChild } = deps;
10039
11261
  const terminals = /* @__PURE__ */ new Map();
10040
- const open = (sessionId, agentSessionId, worktreePath) => {
10041
- const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !existsSync9(worktreePath) ? `worktree missing (${worktreePath})` : terminals.size >= MAX_TERMINALS ? `cap ${MAX_TERMINALS} reached` : null;
11262
+ const open3 = (sessionId, agentSessionId, cwd) => {
11263
+ const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !statSync2(cwd, { throwIfNoEntry: false })?.isDirectory() ? `directory missing (${cwd})` : terminals.size >= MAX_TERMINALS ? `cap ${MAX_TERMINALS} reached` : null;
10042
11264
  if (why) {
10043
11265
  log(`terminal #${sessionId}: ${why} \u2014 refusing`);
10044
11266
  return;
10045
11267
  }
10046
- try {
10047
- prepareWorktree(worktreePath);
10048
- } catch (error) {
10049
- log(`terminal #${sessionId}: worktree preparation failed: ${String(error)}`);
10050
- return;
10051
- }
10052
- const command = cfg.agentKind === "codex" ? codexPtyCommand(agentSessionId) : claudePtyCommand(agentSessionId, hasSessionJsonl(agentSessionId));
11268
+ const command = ptyCommand(cfg.agentKind, agentSessionId);
10053
11269
  const term = spawn4(command.file, command.args, {
10054
11270
  name: "xterm-color",
10055
11271
  cols: 80,
10056
11272
  rows: 24,
10057
- cwd: worktreePath,
10058
- env: process.env
11273
+ cwd,
11274
+ env: workerEnv(cfg)
10059
11275
  });
10060
11276
  const ws = new import_websocket.default(serverTerminalWsUrl(cfg.server, sessionId), {
10061
11277
  headers: { authorization: `Bearer ${cfg.apiToken}` }
@@ -10101,7 +11317,7 @@ var createTerminalManager = (deps) => {
10101
11317
  }
10102
11318
  };
10103
11319
  return {
10104
- open,
11320
+ open: open3,
10105
11321
  close,
10106
11322
  has: (sessionId) => terminals.has(sessionId),
10107
11323
  killAll: () => {
@@ -10122,12 +11338,14 @@ var createTerminalManager = (deps) => {
10122
11338
  // src/worker/daemon.ts
10123
11339
  var ES_OPEN = 1;
10124
11340
  var runWorkerDaemon = async (cfg, client, signal) => {
10125
- const repo = process.cwd();
11341
+ const cwd = process.cwd();
11342
+ ensureExcluded(cwd, PROJECT_CONFIG_NAME);
11343
+ syncBundledArtifactSkill(cwd);
10126
11344
  const log = (m) => console.log(`[worker #${cfg.workerId} ${cfg.name}] ${m}`);
10127
11345
  const supervisor = createSessionSupervisor({
10128
11346
  client,
10129
11347
  cfg,
10130
- repo,
11348
+ cwd,
10131
11349
  log,
10132
11350
  hasTerminal: (id) => terminals.has(id),
10133
11351
  closeTerminal: (id) => terminals.close(id)
@@ -10135,11 +11353,10 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10135
11353
  const terminals = createTerminalManager({
10136
11354
  cfg,
10137
11355
  log,
10138
- hasChild: (id) => supervisor.has(id),
10139
- prepareWorktree: (worktreePath) => syncBundledArtifactSkill(repo, worktreePath)
11356
+ hasChild: (id) => supervisor.has(id)
10140
11357
  });
11358
+ const files = createFileQueryRunner(cwd);
10141
11359
  const services = createServiceSupervisor({
10142
- getSession: (sessionId) => client.sessions.get(sessionId),
10143
11360
  sendReport: (report) => client.services.report(report),
10144
11361
  cfg,
10145
11362
  log
@@ -10184,28 +11401,29 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10184
11401
  else if (cmd.cmd === "session.reconcile")
10185
11402
  void supervisor.reconcile(cmd.sessionId).catch((error) => log(`reconcile failed: ${String(error)}`));
10186
11403
  else if (cmd.cmd === "session.delete")
10187
- void services.stopSession(cmd.sessionId).then(() => supervisor.remove(cmd.sessionId, cmd.worktreePath)).catch((err) => log(`service cleanup failed: ${String(err)}`));
11404
+ void supervisor.remove(cmd.sessionId).catch((err) => log(`session cleanup failed: ${String(err)}`));
10188
11405
  else if (cmd.cmd === "session.title")
10189
- void supervisor.title(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath).catch((err) => log(`title failed: ${String(err)}`));
11406
+ void supervisor.title(cmd.sessionId, cmd.agentSessionId, cmd.cwd).catch((err) => log(`title failed: ${String(err)}`));
10190
11407
  else if (cmd.cmd === "session.terminal") {
10191
- if (cmd.action === "open")
10192
- terminals.open(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath);
11408
+ if (cmd.action === "open") terminals.open(cmd.sessionId, cmd.agentSessionId, cmd.cwd);
10193
11409
  else terminals.close(cmd.sessionId);
10194
11410
  } else if (cmd.cmd === "service.run")
10195
- void services.run(cmd.requestId, cmd.sessionId, cmd.name, cmd.argv, cmd.publicUrl, cmd.note).catch((err) => log(`service run failed: ${String(err)}`));
11411
+ void services.run(cmd.requestId, cmd.cwd, cmd.name, cmd.argv, cmd.publicUrl, cmd.note).catch((err) => log(`service run failed: ${String(err)}`));
10196
11412
  else if (cmd.cmd === "service.stop")
10197
11413
  void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
11414
+ else if (cmd.cmd === "files.query")
11415
+ void files(cmd.query).then((reply) => client.files.report({ requestId: cmd.requestId, ...reply })).catch((err) => log(`files query failed: ${String(err)}`));
10198
11416
  } catch (error) {
10199
11417
  log(`invalid command: ${String(error)}`);
10200
11418
  }
10201
11419
  };
10202
11420
  es.onerror = () => log("command stream error (eventsource will retry)");
10203
- log(`listening for commands (server ${cfg.server}, repo ${repo})`);
11421
+ log(`listening for commands (server ${cfg.server}, cwd ${cwd})`);
10204
11422
  liveness.start();
10205
- await new Promise((resolve3) => {
10206
- stop = resolve3;
10207
- if (signal.aborted) return resolve3();
10208
- signal.addEventListener("abort", () => resolve3(), { once: true });
11423
+ await new Promise((resolve4) => {
11424
+ stop = resolve4;
11425
+ if (signal.aborted) return resolve4();
11426
+ signal.addEventListener("abort", () => resolve4(), { once: true });
10209
11427
  });
10210
11428
  es.close();
10211
11429
  liveness.stop();
@@ -10217,22 +11435,93 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10217
11435
 
10218
11436
  // src/worker/machine-id.ts
10219
11437
  import { randomUUID as randomUUID3 } from "node:crypto";
10220
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
10221
- import { homedir as homedir4 } from "node:os";
10222
- import { dirname as dirname5, join as join15 } from "node:path";
10223
- var machineIdPath = (env = process.env) => join15(env.XDG_DATA_HOME ?? join15(env.HOME ?? homedir4(), ".local/share"), "baton", "machine-id");
11438
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
11439
+ import { homedir as homedir6 } from "node:os";
11440
+ import { dirname as dirname7, join as join23 } from "node:path";
11441
+ var machineIdPath = (env = process.env) => join23(env.XDG_DATA_HOME ?? join23(env.HOME ?? homedir6(), ".local/share"), "baton", "machine-id");
10224
11442
  var readOrCreateMachineId = (path = machineIdPath()) => {
10225
- if (existsSync10(path)) {
11443
+ if (existsSync9(path)) {
10226
11444
  const s = readFileSync6(path, "utf8").trim();
10227
11445
  if (s) return s;
10228
11446
  }
10229
11447
  const id = randomUUID3();
10230
- mkdirSync5(dirname5(path), { recursive: true });
11448
+ mkdirSync5(dirname7(path), { recursive: true });
10231
11449
  writeFileSync5(path, `${id}
10232
11450
  `, "utf8");
10233
11451
  return id;
10234
11452
  };
10235
11453
 
11454
+ // src/commands/worker/provision.ts
11455
+ var projectArg = {
11456
+ project: { type: "string", description: "project id (overrides .baton.json)" }
11457
+ };
11458
+ var positiveId = (raw) => {
11459
+ const id = Number(raw);
11460
+ if (!Number.isSafeInteger(id) || id <= 0) throw new Error("positive id required");
11461
+ return id;
11462
+ };
11463
+ var workerTemplatesCommand = defineCommand({
11464
+ meta: { name: "templates", description: "list worker creation templates for a project" },
11465
+ args: { ...projectArg, ...common },
11466
+ run: async ({ args }) => {
11467
+ const list = await clientFor(args).workerCreations.templates(positiveId(resolveProjectId(args)));
11468
+ console.log(renderList(list, fmtWorkerTemplate, Boolean(args.json)));
11469
+ }
11470
+ });
11471
+ var workerCreateCommand = defineCommand({
11472
+ meta: { name: "create", description: "request a worker from the mini template (asynchronous)" },
11473
+ args: {
11474
+ ...projectArg,
11475
+ ...common,
11476
+ template: {
11477
+ type: "string",
11478
+ required: true,
11479
+ description: "mini-glm-codeplan | mini-dashscope-codeplan | mini-codex"
11480
+ },
11481
+ name: { type: "string", required: true, description: "worker name" },
11482
+ agentKind: { type: "string", description: AGENT_KINDS.join(", ") }
11483
+ },
11484
+ run: async ({ args }) => {
11485
+ if (!isWorkerTemplateId(args.template)) throw new Error("invalid worker template");
11486
+ const agentKind = args.agentKind ?? workerTemplateAgentKinds(args.template)[0];
11487
+ if (!isAgentKind(agentKind)) throw new Error("invalid agentKind");
11488
+ if (!workerTemplateAgentKinds(args.template).includes(agentKind))
11489
+ throw new Error("agentKind is not supported by this template");
11490
+ const name = args.name.trim();
11491
+ if (!name) throw new Error("worker name required");
11492
+ const creation = await clientFor(args).workerCreations.create(
11493
+ positiveId(resolveProjectId(args)),
11494
+ {
11495
+ templateId: args.template,
11496
+ name,
11497
+ agentKind
11498
+ }
11499
+ );
11500
+ console.log(renderOne(creation, fmtWorkerCreation, Boolean(args.json)));
11501
+ }
11502
+ });
11503
+ var workerCreationCommand = defineCommand({
11504
+ meta: { name: "creation", description: "inspect worker creation records" },
11505
+ subCommands: {
11506
+ ls: defineCommand({
11507
+ args: { ...projectArg, ...common },
11508
+ run: async ({ args }) => {
11509
+ const list = await clientFor(args).workerCreations.listByProject(
11510
+ positiveId(resolveProjectId(args))
11511
+ );
11512
+ console.log(renderList(list, fmtWorkerCreation, Boolean(args.json)));
11513
+ }
11514
+ }),
11515
+ get: defineCommand({
11516
+ args: { ...common, id: { type: "positional", required: true } },
11517
+ run: async ({ args }) => {
11518
+ const creation = await clientFor(args).workerCreations.get(positiveId(args.id));
11519
+ console.log(renderOne(creation, fmtWorkerCreation, Boolean(args.json)));
11520
+ }
11521
+ })
11522
+ }
11523
+ });
11524
+
10236
11525
  // src/commands/worker/index.ts
10237
11526
  var resolveWorker = async (client, projectId, handle) => {
10238
11527
  const asId = parseWorkerHandle(handle);
@@ -10246,13 +11535,8 @@ var resolveWorker = async (client, projectId, handle) => {
10246
11535
  };
10247
11536
  var parseAgentKind = (value) => {
10248
11537
  const raw = typeof value === "string" && value.trim() ? value.trim() : "claude-code";
10249
- if (raw === "claude-code" || raw === "codex") return raw;
10250
- throw new Error(`invalid agent kind "${raw}" (expected claude-code or codex)`);
10251
- };
10252
- var nonEmptyString = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
10253
- var resolveBaseBranch = (input) => {
10254
- const branch = nonEmptyString(input.flag) ?? nonEmptyString(input.env) ?? nonEmptyString(input.saved) ?? repoHeadBranch(input.repo ?? process.cwd());
10255
- return validateBaseBranch(branch);
11538
+ if (isAgentKind(raw)) return raw;
11539
+ throw new Error(`invalid agent kind "${raw}" (expected ${AGENT_KINDS.join(", ")})`);
10256
11540
  };
10257
11541
  var registerWorker = async (client, input, cfgPath = projectConfigPath()) => {
10258
11542
  const out = await client.workers.register({
@@ -10267,7 +11551,6 @@ var registerWorker = async (client, input, cfgPath = projectConfigPath()) => {
10267
11551
  ...existing,
10268
11552
  server: input.server,
10269
11553
  project: input.projectId,
10270
- baseBranch: input.baseBranch,
10271
11554
  worker: {
10272
11555
  id: out.worker.id,
10273
11556
  name: out.worker.name,
@@ -10281,6 +11564,9 @@ var registerWorker = async (client, input, cfgPath = projectConfigPath()) => {
10281
11564
  var worker = defineCommand({
10282
11565
  meta: { name: "worker", description: "register / inspect / close workers for a project" },
10283
11566
  subCommands: {
11567
+ create: workerCreateCommand,
11568
+ creation: workerCreationCommand,
11569
+ templates: workerTemplatesCommand,
10284
11570
  register: defineCommand({
10285
11571
  meta: {
10286
11572
  name: "register",
@@ -10295,11 +11581,7 @@ var worker = defineCommand({
10295
11581
  },
10296
11582
  agentKind: {
10297
11583
  type: "string",
10298
- description: "agent engine for new sessions: claude-code or codex"
10299
- },
10300
- baseBranch: {
10301
- type: "string",
10302
- description: "branch new session worktrees start from (default: current branch, then main)"
11584
+ description: `agent engine for new sessions: ${AGENT_KINDS.join(", ")}`
10303
11585
  },
10304
11586
  ...common
10305
11587
  },
@@ -10315,14 +11597,9 @@ var worker = defineCommand({
10315
11597
  const fileToken = savedConfig?.worker?.apiToken;
10316
11598
  const auth = resolveAuth(process.env, fileToken);
10317
11599
  const c = createClient(server, auth && auth !== "cookie" ? auth : void 0);
10318
- const baseBranch = resolveBaseBranch({
10319
- flag: args.baseBranch,
10320
- env: process.env.BATON_BASE_BRANCH,
10321
- saved: savedConfig?.baseBranch
10322
- });
10323
- const hostname = osHostname();
11600
+ const hostname2 = osHostname();
10324
11601
  const machineId = readOrCreateMachineId();
10325
- const name = args.name ?? hostname;
11602
+ const name = args.name ?? hostname2;
10326
11603
  const agentKind = parseAgentKind(args.agentKind ?? process.env.BATON_AGENT_KIND);
10327
11604
  const { out, configPath } = await registerWorker(
10328
11605
  c,
@@ -10330,20 +11607,18 @@ var worker = defineCommand({
10330
11607
  projectId: resolveProjectId(args),
10331
11608
  name,
10332
11609
  server,
10333
- hostname,
11610
+ hostname: hostname2,
10334
11611
  machineId,
10335
- agentKind,
10336
- baseBranch
11612
+ agentKind
10337
11613
  },
10338
11614
  cfgPath
10339
11615
  );
10340
11616
  if (args.json) {
10341
- console.log(toJson({ ...out, baseBranch, configPath, machineIdPath: machineIdPath() }));
11617
+ console.log(toJson({ ...out, configPath, machineIdPath: machineIdPath() }));
10342
11618
  return;
10343
11619
  }
10344
11620
  console.log(`worker #${out.worker.id} (${out.worker.name}) \u2014 ${out.outcome}`);
10345
11621
  console.log(` agentKind: ${out.worker.agentKind}`);
10346
- console.log(` baseBranch: ${baseBranch}`);
10347
11622
  console.log(` hostname: ${out.worker.hostname}`);
10348
11623
  console.log(` machineId: ${out.worker.machineId}`);
10349
11624
  console.log(` machineId file: ${machineIdPath()}`);
@@ -10452,14 +11727,12 @@ var worker = defineCommand({
10452
11727
  console.log("(no worker registered for this project \u2014 run `baton worker register`)");
10453
11728
  return;
10454
11729
  }
10455
- const baseBranch = resolveBaseBranch({ saved: cfg.baseBranch });
10456
11730
  if (args.json) {
10457
11731
  console.log(
10458
11732
  toJson({
10459
11733
  ...worker2,
10460
11734
  server: cfg.server,
10461
- projectId: cfg.project,
10462
- baseBranch
11735
+ projectId: cfg.project
10463
11736
  })
10464
11737
  );
10465
11738
  return;
@@ -10467,7 +11740,6 @@ var worker = defineCommand({
10467
11740
  console.log(`worker #${worker2.id} (${worker2.name})`);
10468
11741
  console.log(` machineId: ${worker2.machineId}`);
10469
11742
  console.log(` agentKind: ${worker2.agentKind ?? "claude-code"}`);
10470
- console.log(` baseBranch: ${baseBranch}`);
10471
11743
  console.log(` server: ${cfg?.server}`);
10472
11744
  }
10473
11745
  })
@@ -10541,10 +11813,12 @@ var main = defineCommand({
10541
11813
  subCommands: {
10542
11814
  init,
10543
11815
  worker,
11816
+ provisioner,
10544
11817
  session,
10545
11818
  service,
10546
11819
  artifact,
10547
11820
  loop,
11821
+ notify,
10548
11822
  relay,
10549
11823
  channel,
10550
11824
  workspace,