@lesscap/baton-cli 0.8.3 → 0.8.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
@@ -2281,7 +2281,7 @@ var require_websocket = __commonJS({
2281
2281
  var http = __require("http");
2282
2282
  var net = __require("net");
2283
2283
  var tls = __require("tls");
2284
- var { randomBytes, createHash } = __require("crypto");
2284
+ var { randomBytes, createHash: createHash2 } = __require("crypto");
2285
2285
  var { Duplex, Readable: Readable2 } = __require("stream");
2286
2286
  var { URL: URL2 } = __require("url");
2287
2287
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2949,7 +2949,7 @@ var require_websocket = __commonJS({
2949
2949
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2950
2950
  return;
2951
2951
  }
2952
- const digest = createHash("sha1").update(key + GUID).digest("base64");
2952
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
2953
2953
  if (res.headers["sec-websocket-accept"] !== digest) {
2954
2954
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
2955
2955
  return;
@@ -3230,7 +3230,7 @@ var require_stream = __commonJS({
3230
3230
  };
3231
3231
  duplex._final = function(callback) {
3232
3232
  if (ws.readyState === ws.CONNECTING) {
3233
- ws.once("open", function open() {
3233
+ ws.once("open", function open2() {
3234
3234
  duplex._final(callback);
3235
3235
  });
3236
3236
  return;
@@ -3251,7 +3251,7 @@ var require_stream = __commonJS({
3251
3251
  };
3252
3252
  duplex._write = function(chunk, encoding, callback) {
3253
3253
  if (ws.readyState === ws.CONNECTING) {
3254
- ws.once("open", function open() {
3254
+ ws.once("open", function open2() {
3255
3255
  duplex._write(chunk, encoding, callback);
3256
3256
  });
3257
3257
  return;
@@ -3318,7 +3318,7 @@ var require_websocket_server = __commonJS({
3318
3318
  var EventEmitter = __require("events");
3319
3319
  var http = __require("http");
3320
3320
  var { Duplex } = __require("stream");
3321
- var { createHash } = __require("crypto");
3321
+ var { createHash: createHash2 } = __require("crypto");
3322
3322
  var extension2 = require_extension();
3323
3323
  var PerMessageDeflate2 = require_permessage_deflate();
3324
3324
  var subprotocol2 = require_subprotocol();
@@ -3625,7 +3625,7 @@ var require_websocket_server = __commonJS({
3625
3625
  );
3626
3626
  }
3627
3627
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3628
- const digest = createHash("sha1").update(key + GUID).digest("base64");
3628
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
3629
3629
  const headers = [
3630
3630
  "HTTP/1.1 101 Switching Protocols",
3631
3631
  "Upgrade: websocket",
@@ -5370,11 +5370,13 @@ var contentTypeForPath = (path) => src_default.getType(path) ?? "application/oct
5370
5370
  // src/output.ts
5371
5371
  var toJson = (data) => JSON.stringify(data, null, 2);
5372
5372
  var fmtWorkspace = (w) => `${w.id} ${w.name}`;
5373
+ var fmtWorkerTemplate = (t) => `${t.id} ${t.name} [${t.available ? "available" : "unavailable"}] ${t.agentKinds.join(", ")}`;
5374
+ 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
5375
  var fmtProject = (p) => `${p.id} ${p.name}`;
5374
5376
  var fmtRequirement = (r) => `${r.code} [${r.status}] ${r.title}`;
5375
5377
  var fmtTask = (t) => `${t.code} [${t.status}] ${t.title}`;
5376
5378
  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)"}`;
5379
+ var fmtSession = (s) => `${s.id} ${s.name} ${s.attached ? "[active]" : "[inactive]"} ${s.cwd ?? "(pending)"}`;
5378
5380
  var fmtWorker = (w) => {
5379
5381
  const offline = w.connected === false ? " [offline]" : "";
5380
5382
  return `W-${w.id} ${w.name} ${w.hostname} ${w.agentKind}${offline}`;
@@ -5550,7 +5552,7 @@ var servicesClient = (baseUrl) => {
5550
5552
  };
5551
5553
 
5552
5554
  // src/client/retry.ts
5553
- var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
5555
+ var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
5554
5556
  var withRetry = async (fn, opts = {}) => {
5555
5557
  const tries = Math.max(1, opts.tries ?? 4);
5556
5558
  const baseMs = opts.baseMs ?? 300;
@@ -5685,6 +5687,14 @@ var taskClient = (baseUrl) => {
5685
5687
  };
5686
5688
  };
5687
5689
 
5690
+ // src/client/worker-creations.ts
5691
+ var workerCreationsClient = (baseUrl) => ({
5692
+ templates: (projectId) => request(`${baseUrl}/projects/${projectId}/worker-templates`, { method: "GET" }),
5693
+ create: (projectId, input) => request(`${baseUrl}/projects/${projectId}/worker-creations`, { method: "POST", body: input }),
5694
+ listByProject: (projectId) => request(`${baseUrl}/projects/${projectId}/worker-creations`, { method: "GET" }),
5695
+ get: (id) => request(`${baseUrl}/worker-creations/${id}`, { method: "GET" })
5696
+ });
5697
+
5688
5698
  // src/client/workers.ts
5689
5699
  var workersClient = (baseUrl) => {
5690
5700
  const u = (p) => `${baseUrl}${p}`;
@@ -5725,6 +5735,7 @@ var createClient = (baseUrl, opts) => {
5725
5735
  return clientFromBase(baseUrl);
5726
5736
  };
5727
5737
  var clientFromBase = (baseUrl) => ({
5738
+ workerCreations: workerCreationsClient(baseUrl),
5728
5739
  artifacts: artifactsClient(baseUrl),
5729
5740
  workspaces: workspaceClient(baseUrl),
5730
5741
  projects: projectClient(baseUrl),
@@ -5783,7 +5794,6 @@ var viewWorker = (cfg) => {
5783
5794
  return {
5784
5795
  server,
5785
5796
  projectId: project2,
5786
- baseBranch: cfg.baseBranch,
5787
5797
  workerId: worker2.id,
5788
5798
  agentKind: worker2.agentKind ?? "claude-code",
5789
5799
  name: worker2.name,
@@ -5791,29 +5801,40 @@ var viewWorker = (cfg) => {
5791
5801
  apiToken: worker2.apiToken
5792
5802
  };
5793
5803
  };
5794
- var worktreeConfig = (cfg, session2) => ({
5804
+ var workerProjectConfig = (cfg) => ({
5795
5805
  server: cfg.server,
5796
5806
  project: cfg.projectId,
5797
- baseBranch: cfg.baseBranch,
5798
5807
  worker: {
5799
5808
  id: cfg.workerId,
5800
5809
  name: cfg.name,
5801
5810
  agentKind: cfg.agentKind,
5802
5811
  machineId: cfg.machineId,
5803
5812
  apiToken: cfg.apiToken
5804
- },
5805
- ...session2 === void 0 ? {} : { session: session2 }
5813
+ }
5814
+ });
5815
+ var workerEnv = (cfg) => ({
5816
+ ...Object.fromEntries(
5817
+ Object.entries(process.env).filter(
5818
+ (entry) => entry[1] !== void 0
5819
+ )
5820
+ ),
5821
+ BATON_SERVER: cfg.server,
5822
+ BATON_PROJECT_ID: String(cfg.projectId),
5823
+ BATON_WORKER_ID: String(cfg.workerId),
5824
+ BATON_WORKER_TOKEN: cfg.apiToken
5806
5825
  });
5807
5826
 
5808
5827
  // src/config.ts
5809
5828
  var resolveBaseUrl = (urlArg, env = process.env) => {
5810
5829
  if (urlArg) return urlArg;
5830
+ if (env.BATON_SERVER) return env.BATON_SERVER;
5811
5831
  const cfg = loadProjectConfigOrNull(projectConfigPath());
5812
5832
  if (cfg?.server) return cfg.server;
5813
5833
  return env.BATON_URL ?? "http://localhost:3280";
5814
5834
  };
5815
5835
  var resolveBaseUrlOrNull = (urlArg, env = process.env) => {
5816
5836
  if (urlArg) return urlArg;
5837
+ if (env.BATON_SERVER) return env.BATON_SERVER;
5817
5838
  const cfg = loadProjectConfigOrNull(projectConfigPath());
5818
5839
  if (cfg?.server) return cfg.server;
5819
5840
  return env.BATON_URL ?? null;
@@ -5823,7 +5844,7 @@ var resolveBaseUrlOrNull = (urlArg, env = process.env) => {
5823
5844
  var common = {
5824
5845
  url: {
5825
5846
  type: "string",
5826
- description: "baton server url (--url > .baton.json > BATON_URL > http://localhost:3280)"
5847
+ description: "baton server url (--url > BATON_SERVER > .baton.json > BATON_URL > http://localhost:3280)"
5827
5848
  },
5828
5849
  json: { type: "boolean", description: "output JSON" }
5829
5850
  };
@@ -5846,6 +5867,8 @@ var resolveBearer = () => {
5846
5867
  };
5847
5868
  var resolveProjectId = (args) => {
5848
5869
  if (args.project !== void 0 && args.project !== "") return Number(args.project);
5870
+ if (process.env.BATON_PROJECT_ID !== void 0)
5871
+ return contextId(process.env.BATON_PROJECT_ID, "BATON_PROJECT_ID");
5849
5872
  const cfg = loadProjectConfigOrNull(projectConfigPath());
5850
5873
  if (cfg?.project !== void 0) return cfg.project;
5851
5874
  throw new Error("no project in scope. pass --project <id> or run `baton init` in this directory.");
@@ -5870,6 +5893,18 @@ var parseDuration = (s) => {
5870
5893
  const mult = m[2] === "d" ? 86400 : m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1;
5871
5894
  return n * mult;
5872
5895
  };
5896
+ var contextId = (value, source) => {
5897
+ const id = Number(value);
5898
+ if (!Number.isSafeInteger(id) || id <= 0) throw new Error(`invalid ${source}`);
5899
+ return id;
5900
+ };
5901
+ var resolveWorkerId = () => {
5902
+ if (process.env.BATON_WORKER_ID !== void 0)
5903
+ return contextId(process.env.BATON_WORKER_ID, "BATON_WORKER_ID");
5904
+ const id = loadProjectConfigOrNull(projectConfigPath())?.worker?.id;
5905
+ if (id === void 0) throw new Error("no worker in scope");
5906
+ return id;
5907
+ };
5873
5908
 
5874
5909
  // src/commands/artifact.ts
5875
5910
  var formatArtifact = (artifact2) => `${artifact2.id} ${artifact2.size} B ${artifact2.filename} ${artifact2.url}`;
@@ -6227,11 +6262,11 @@ var EventSource = class extends EventTarget {
6227
6262
  return;
6228
6263
  }
6229
6264
  const decoder = new TextDecoder(), reader = body.getReader();
6230
- let open = true;
6265
+ let open2 = true;
6231
6266
  do {
6232
6267
  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);
6268
+ value && __privateGet(this, _parser).feed(decoder.decode(value, { stream: !done })), done && (open2 = false, __privateGet(this, _parser).reset(), __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this));
6269
+ } while (open2);
6235
6270
  }), __privateAdd(this, _onFetchError, (err) => {
6236
6271
  __privateSet(this, _controller, void 0), !(err.name === "AbortError" || err.type === "aborted") && __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this, flattenError(err));
6237
6272
  }), __privateAdd(this, _onEvent, (event) => {
@@ -6776,10 +6811,10 @@ var runListen = async (o) => {
6776
6811
  onError: (e) => emit({ type: "channel.error", error: describe(e) })
6777
6812
  });
6778
6813
  emit({ type: "channel.listening", channel: o.channel, from: o.from, url: o.url });
6779
- await new Promise((resolve3) => {
6814
+ await new Promise((resolve4) => {
6780
6815
  const stop = () => {
6781
6816
  close();
6782
- resolve3();
6817
+ resolve4();
6783
6818
  };
6784
6819
  process.on("SIGINT", stop);
6785
6820
  process.on("SIGTERM", stop);
@@ -7034,10 +7069,6 @@ var init = defineCommand({
7034
7069
  });
7035
7070
 
7036
7071
  // 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
7072
  var resolveSession = async (client, projectId, handle) => {
7042
7073
  const asInt = Number(handle);
7043
7074
  if (Number.isInteger(asInt) && asInt > 0) {
@@ -7264,10 +7295,466 @@ var project = defineCommand({
7264
7295
  }
7265
7296
  });
7266
7297
 
7298
+ // src/provisioner/config.ts
7299
+ import { execFile } from "node:child_process";
7300
+ import { constants } from "node:fs";
7301
+ import { access, readFile, stat as stat2 } from "node:fs/promises";
7302
+ import { isAbsolute } from "node:path";
7303
+ import { promisify } from "node:util";
7304
+ var forbiddenEnv = /* @__PURE__ */ new Set([
7305
+ "BATON_PROVISIONER_TOKEN",
7306
+ "BATON_TOKEN",
7307
+ "BATON_WORKER_TOKEN",
7308
+ "BATON_USER",
7309
+ "BATON_PASS",
7310
+ "HOME",
7311
+ "XDG_DATA_HOME"
7312
+ ]);
7313
+ var object = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7314
+ var absolutePath = (value, field) => {
7315
+ if (typeof value !== "string" || !isAbsolute(value) || value.includes("\0"))
7316
+ throw new Error(`${field} must be an absolute path`);
7317
+ return value;
7318
+ };
7319
+ var commandEnvironment = (config) => ({
7320
+ ...process.env,
7321
+ PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin"
7322
+ });
7323
+ var loadProvisionerConfig = async (path) => {
7324
+ if (process.platform !== "darwin") throw new Error("provisioner requires macOS");
7325
+ absolutePath(path, "config");
7326
+ if ((await stat2(path)).mode & 63) throw new Error("config must be private: chmod 600 <config>");
7327
+ const text = await readFile(path, "utf8");
7328
+ let raw;
7329
+ try {
7330
+ raw = JSON.parse(text);
7331
+ } catch (error) {
7332
+ if (!(error instanceof SyntaxError)) throw error;
7333
+ throw new Error("invalid provisioner JSON config");
7334
+ }
7335
+ if (!object(raw) || typeof raw.server !== "string" || typeof raw.token !== "string")
7336
+ throw new Error("server and token required");
7337
+ let server;
7338
+ try {
7339
+ server = new URL(raw.server);
7340
+ } catch (error) {
7341
+ if (!(error instanceof TypeError)) throw error;
7342
+ throw new Error("server must be a valid HTTP(S) URL");
7343
+ }
7344
+ if (!["http:", "https:"].includes(server.protocol) || server.username || server.password || server.search || server.hash)
7345
+ throw new Error("server must be an HTTP(S) URL without credentials, query, or fragment");
7346
+ if (!raw.token.trim() || /[\r\n]/.test(raw.token))
7347
+ throw new Error("valid provisioner token required");
7348
+ if (!object(raw.workerEnv)) throw new Error("workerEnv must be an object");
7349
+ const workerEnv2 = Object.fromEntries(
7350
+ Object.entries(raw.workerEnv).map(([key, value]) => {
7351
+ if (forbiddenEnv.has(key) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof value !== "string" || value.includes("\0"))
7352
+ throw new Error(`invalid worker environment key: ${key}`);
7353
+ return [key, value];
7354
+ })
7355
+ );
7356
+ const config = {
7357
+ server: server.toString().replace(/\/$/, ""),
7358
+ token: raw.token.trim(),
7359
+ projectsRoot: absolutePath(raw.projectsRoot, "projectsRoot"),
7360
+ cliPath: absolutePath(raw.cliPath, "cliPath"),
7361
+ nodePath: absolutePath(raw.nodePath ?? process.execPath, "nodePath"),
7362
+ workerEnv: workerEnv2
7363
+ };
7364
+ await access(config.cliPath, constants.R_OK);
7365
+ await access(config.nodePath ?? process.execPath, constants.X_OK);
7366
+ const env = commandEnvironment(config);
7367
+ await promisify(execFile)("git", ["--version"], { env, timeout: 1e4 });
7368
+ await promisify(execFile)("launchctl", ["help"], { env, timeout: 1e4 });
7369
+ await promisify(execFile)("/usr/bin/plutil", ["-help"], { timeout: 1e4 });
7370
+ return config;
7371
+ };
7372
+
7373
+ // src/provisioner/client.ts
7374
+ var createProvisionerClient = (config) => {
7375
+ const request2 = async (path, token, body, signal) => {
7376
+ const timeout = AbortSignal.timeout(1e4);
7377
+ const response = await fetch(`${config.server}${path}`, {
7378
+ method: body === void 0 ? "GET" : "POST",
7379
+ headers: {
7380
+ authorization: `Bearer ${token}`,
7381
+ ...body === void 0 ? {} : { "content-type": "application/json" }
7382
+ },
7383
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
7384
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
7385
+ });
7386
+ if (!response.ok) {
7387
+ await response.body?.cancel();
7388
+ throw new HttpError(response.status, `provisioner request ${path}: HTTP ${response.status}`);
7389
+ }
7390
+ return await response.json();
7391
+ };
7392
+ return {
7393
+ get: (id, signal) => request2(`/provisioner/worker-creations/${id}`, config.token, void 0, signal),
7394
+ report: (id, report, signal) => request2(`/provisioner/worker-creations/${id}/progress`, config.token, report, signal),
7395
+ register: (id, hostname2, signal) => request2(`/provisioner/worker-creations/${id}/worker`, config.token, { hostname: hostname2 }, signal),
7396
+ worker: (id, token, signal) => request2(`/workers/${id}`, token, void 0, signal)
7397
+ };
7398
+ };
7399
+
7400
+ // src/provisioner/executor.ts
7401
+ import { execFile as execFile3 } from "node:child_process";
7402
+ import { lstat, mkdir as mkdir2, realpath, writeFile } from "node:fs/promises";
7403
+ import { hostname } from "node:os";
7404
+ import { join as join4 } from "node:path";
7405
+ import { setTimeout as setTimeout2 } from "node:timers/promises";
7406
+ import { promisify as promisify3 } from "node:util";
7407
+
7408
+ // src/provisioner/launchd.ts
7409
+ import { execFile as execFile2 } from "node:child_process";
7410
+ import { mkdir, open } from "node:fs/promises";
7411
+ import { homedir } from "node:os";
7412
+ import { dirname, join as join3 } from "node:path";
7413
+ import { promisify as promisify2 } from "node:util";
7414
+ var workerStatePath = (config, creation) => join3(
7415
+ config.projectsRoot,
7416
+ ".worker-state",
7417
+ `project-${creation.projectId}`,
7418
+ `creation-${creation.id}`
7419
+ );
7420
+ var workerLogPath = (creation) => join3(homedir(), "Library/Logs", `baton-worker-creation-${creation.id}.log`);
7421
+ var startAgent = async (config, agent, signal) => {
7422
+ signal?.throwIfAborted();
7423
+ const plist = join3(homedir(), "Library/LaunchAgents", `${agent.label}.plist`);
7424
+ await mkdir(dirname(plist), { recursive: true });
7425
+ await mkdir(dirname(agent.log), { recursive: true });
7426
+ const domain = `gui/${process.getuid?.()}`;
7427
+ const env = commandEnvironment(config);
7428
+ try {
7429
+ await promisify2(execFile2)("launchctl", ["print", `${domain}/${agent.label}`], {
7430
+ env,
7431
+ timeout: 1e4,
7432
+ signal
7433
+ });
7434
+ throw new Error(
7435
+ `launchd service already exists: ${agent.label}; stop it manually before installing`
7436
+ );
7437
+ } catch (error) {
7438
+ if (!(error instanceof Error && "code" in error && error.code === 113 && "stderr" in error && typeof error.stderr === "string" && error.stderr.includes("Could not find service")))
7439
+ throw error;
7440
+ }
7441
+ const handle = await open(plist, "wx", 384);
7442
+ try {
7443
+ const payload = {
7444
+ Label: agent.label,
7445
+ ProgramArguments: agent.args,
7446
+ WorkingDirectory: agent.cwd,
7447
+ EnvironmentVariables: agent.env,
7448
+ RunAtLoad: true,
7449
+ KeepAlive: true,
7450
+ ThrottleInterval: 10,
7451
+ StandardOutPath: agent.log,
7452
+ StandardErrorPath: agent.log
7453
+ };
7454
+ await handle.writeFile(JSON.stringify(payload));
7455
+ } finally {
7456
+ await handle.close();
7457
+ }
7458
+ const log = await open(agent.log, "a", 384);
7459
+ await log.close();
7460
+ try {
7461
+ await promisify2(execFile2)("/usr/bin/plutil", ["-convert", "xml1", plist], {
7462
+ timeout: 1e4,
7463
+ signal
7464
+ });
7465
+ await promisify2(execFile2)("/usr/bin/plutil", ["-lint", plist], { timeout: 1e4, signal });
7466
+ await promisify2(execFile2)("launchctl", ["bootstrap", domain, plist], {
7467
+ env,
7468
+ timeout: 1e4,
7469
+ signal
7470
+ });
7471
+ } catch {
7472
+ throw new Error(`launchd start failed; inspect ${plist} and ${agent.log}`);
7473
+ }
7474
+ };
7475
+ var launchWorkerAgent = async (config, creation, repoPath, signal) => startAgent(
7476
+ config,
7477
+ {
7478
+ label: `dev.fmap.baton-worker-creation-${creation.id}`,
7479
+ args: [config.nodePath ?? process.execPath, config.cliPath, "worker", "run"],
7480
+ cwd: repoPath,
7481
+ env: {
7482
+ PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin",
7483
+ ...config.workerEnv,
7484
+ HOME: homedir(),
7485
+ XDG_DATA_HOME: workerStatePath(config, creation)
7486
+ },
7487
+ log: workerLogPath(creation)
7488
+ },
7489
+ signal
7490
+ );
7491
+ var installProvisionerAgent = async (config, configPath) => startAgent(config, {
7492
+ label: "dev.fmap.baton-provisioner",
7493
+ args: [
7494
+ config.nodePath ?? process.execPath,
7495
+ config.cliPath,
7496
+ "provisioner",
7497
+ "run",
7498
+ "--config",
7499
+ configPath
7500
+ ],
7501
+ cwd: dirname(configPath),
7502
+ env: { HOME: homedir(), PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin" },
7503
+ log: join3(homedir(), "Library/Logs/baton-provisioner.log")
7504
+ });
7505
+
7506
+ // src/provisioner/executor.ts
7507
+ var directory = async (path) => {
7508
+ try {
7509
+ await mkdir2(path, { mode: 448 });
7510
+ } catch (error) {
7511
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
7512
+ }
7513
+ const info = await lstat(path);
7514
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`unsafe directory: ${path}`);
7515
+ };
7516
+ var provisionWorker = async (config, creation, client, signal) => {
7517
+ const repoPath = join4(
7518
+ config.projectsRoot,
7519
+ `project-${creation.projectId}`,
7520
+ `creation-${creation.id}`
7521
+ );
7522
+ const progress = (step) => client.report(creation.id, { status: "running", step, repoPath }, signal);
7523
+ await progress("directory");
7524
+ signal.throwIfAborted();
7525
+ await mkdir2(config.projectsRoot, { recursive: true, mode: 448 });
7526
+ const root = await realpath(config.projectsRoot);
7527
+ await directory(join4(root, `project-${creation.projectId}`));
7528
+ await mkdir2(join4(root, `project-${creation.projectId}`, `creation-${creation.id}`), {
7529
+ mode: 448
7530
+ });
7531
+ await progress("git");
7532
+ const git = async (args) => {
7533
+ try {
7534
+ await promisify3(execFile3)("git", ["-C", repoPath, ...args], {
7535
+ env: commandEnvironment(config),
7536
+ signal,
7537
+ timeout: 1e4
7538
+ });
7539
+ } catch (error) {
7540
+ signal.throwIfAborted();
7541
+ throw new Error("Git repository initialization failed", { cause: error });
7542
+ }
7543
+ };
7544
+ await git(["init", "-b", "main"]);
7545
+ await writeFile(join4(repoPath, ".git/info/exclude"), "\n.baton.json\n", { flag: "a" });
7546
+ await git([
7547
+ "-c",
7548
+ "user.name=Baton",
7549
+ "-c",
7550
+ "user.email=baton@localhost",
7551
+ "commit",
7552
+ "--allow-empty",
7553
+ "-m",
7554
+ "chore: initialize repository"
7555
+ ]);
7556
+ await progress("register");
7557
+ const result = await client.register(creation.id, hostname(), signal);
7558
+ const state = workerStatePath(config, creation);
7559
+ await directory(join4(root, ".worker-state"));
7560
+ await directory(join4(root, ".worker-state", `project-${creation.projectId}`));
7561
+ await mkdir2(state, { mode: 448 });
7562
+ await mkdir2(join4(state, "baton"), { mode: 448 });
7563
+ await writeFile(join4(state, "baton/machine-id"), `${creation.machineId}
7564
+ `, {
7565
+ flag: "wx",
7566
+ mode: 384
7567
+ });
7568
+ const cfg = workerProjectConfig({
7569
+ server: config.server,
7570
+ projectId: creation.projectId,
7571
+ workerId: result.worker.id,
7572
+ name: result.worker.name,
7573
+ agentKind: result.worker.agentKind,
7574
+ machineId: result.worker.machineId,
7575
+ apiToken: result.apiToken
7576
+ });
7577
+ await writeFile(join4(repoPath, ".baton.json"), `${JSON.stringify(cfg, null, 2)}
7578
+ `, {
7579
+ flag: "wx",
7580
+ mode: 384
7581
+ });
7582
+ await progress("start");
7583
+ signal.throwIfAborted();
7584
+ await launchWorkerAgent(config, creation, repoPath, signal);
7585
+ await progress("connect");
7586
+ const deadline = Date.now() + 6e4;
7587
+ while (Date.now() < deadline) {
7588
+ const worker2 = await client.worker(result.worker.id, result.apiToken, signal);
7589
+ if (worker2.connected) {
7590
+ await client.report(creation.id, { status: "succeeded" }, signal);
7591
+ return;
7592
+ }
7593
+ await setTimeout2(1e3, void 0, { signal });
7594
+ }
7595
+ throw new Error("Worker did not connect within 60 seconds");
7596
+ };
7597
+
7598
+ // src/provisioner/daemon.ts
7599
+ var taskError = (error, config) => {
7600
+ const message = error instanceof Error ? error.message : "unknown creation error";
7601
+ return [config.token, ...Object.values(config.workerEnv)].filter((value) => value.length > 0).reduce((safe, secret) => safe.replaceAll(secret, "[redacted]"), message);
7602
+ };
7603
+ var processCreation = async (id, config, client, signal) => {
7604
+ let creation;
7605
+ try {
7606
+ creation = await client.get(id, signal);
7607
+ } catch (error) {
7608
+ if (error instanceof HttpError && error.status === 404) {
7609
+ console.error(`[provisioner] creation ${id} no longer exists`);
7610
+ return;
7611
+ }
7612
+ throw error;
7613
+ }
7614
+ if (creation.status === "succeeded" || creation.status === "failed") return;
7615
+ if (creation.status === "running") {
7616
+ await client.report(
7617
+ id,
7618
+ {
7619
+ status: "failed",
7620
+ error: "Provisioner interrupted; inspect retained resources before creating again"
7621
+ },
7622
+ signal
7623
+ );
7624
+ return;
7625
+ }
7626
+ try {
7627
+ await provisionWorker(config, creation, client, signal);
7628
+ } catch (error) {
7629
+ if (signal.aborted) {
7630
+ console.error(`[provisioner] creation ${id} interrupted: ${taskError(error, config)}`);
7631
+ return;
7632
+ }
7633
+ if (!(error instanceof HttpError) || ![401, 403].includes(error.status)) {
7634
+ const message = `${taskError(error, config)}; worker log: ${workerLogPath(creation)}`;
7635
+ console.error(`[provisioner] creation ${id}: ${message}`);
7636
+ await client.report(id, { status: "failed", error: message }, signal);
7637
+ return;
7638
+ }
7639
+ throw error;
7640
+ }
7641
+ };
7642
+ var runProvisioner = async (config, signal) => {
7643
+ const client = createProvisionerClient(config);
7644
+ const abort = new AbortController();
7645
+ const queued = /* @__PURE__ */ new Set();
7646
+ let tail = Promise.resolve();
7647
+ let fatal;
7648
+ let finish = () => {
7649
+ };
7650
+ const ended = new Promise((resolve4) => {
7651
+ finish = resolve4;
7652
+ });
7653
+ const stream = new EventSource(`${config.server}/provisioner/stream`, {
7654
+ fetch: (url, init2) => fetch(url, {
7655
+ ...init2,
7656
+ headers: { ...init2?.headers, authorization: `Bearer ${config.token}` },
7657
+ signal: abort.signal
7658
+ })
7659
+ });
7660
+ const stop = () => {
7661
+ stream.close();
7662
+ abort.abort();
7663
+ finish();
7664
+ };
7665
+ const fail = (error) => {
7666
+ fatal = error;
7667
+ console.error(`[provisioner] ${taskError(error, config)}`);
7668
+ stop();
7669
+ };
7670
+ stream.onmessage = (event) => {
7671
+ try {
7672
+ const message = JSON.parse(event.data);
7673
+ if (!message || !Number.isSafeInteger(message.creationId) || message.creationId <= 0 || !["create", "inspect"].includes(message.action))
7674
+ throw new Error("invalid provisioner signal");
7675
+ if (queued.has(message.creationId)) return;
7676
+ queued.add(message.creationId);
7677
+ tail = tail.then(async () => {
7678
+ try {
7679
+ if (!abort.signal.aborted)
7680
+ await processCreation(message.creationId, config, client, abort.signal);
7681
+ } finally {
7682
+ queued.delete(message.creationId);
7683
+ }
7684
+ }).catch((error) => {
7685
+ if (!abort.signal.aborted) fail(error);
7686
+ else console.error(`[provisioner] stopped: ${taskError(error, config)}`);
7687
+ });
7688
+ } catch (error) {
7689
+ fail(error);
7690
+ }
7691
+ };
7692
+ stream.onerror = (event) => {
7693
+ if (stream.readyState === EventSource.CLOSED || [401, 403, 409].includes(event.code ?? 0))
7694
+ fail(new Error(`provisioner stream rejected: HTTP ${event.code}`));
7695
+ else
7696
+ console.error(`[provisioner] stream disconnected (${event.code ?? "network"}); reconnecting`);
7697
+ };
7698
+ signal.addEventListener("abort", stop, { once: true });
7699
+ if (signal.aborted) stop();
7700
+ console.log("[provisioner] waiting for worker creation requests");
7701
+ try {
7702
+ await ended;
7703
+ await tail;
7704
+ } finally {
7705
+ stop();
7706
+ signal.removeEventListener("abort", stop);
7707
+ }
7708
+ if (fatal !== void 0) throw new Error(taskError(fatal, config));
7709
+ };
7710
+
7711
+ // src/commands/provisioner.ts
7712
+ var configArg = {
7713
+ config: {
7714
+ type: "string",
7715
+ required: true,
7716
+ description: "absolute path to private provisioner JSON config"
7717
+ }
7718
+ };
7719
+ var provisioner = defineCommand({
7720
+ meta: {
7721
+ name: "provisioner",
7722
+ description: "prepare local repositories and launch workers on macOS"
7723
+ },
7724
+ subCommands: {
7725
+ run: defineCommand({
7726
+ args: configArg,
7727
+ run: async ({ args }) => {
7728
+ const config = await loadProvisionerConfig(args.config);
7729
+ const abort = new AbortController();
7730
+ const stop = () => abort.abort();
7731
+ process.on("SIGINT", stop);
7732
+ process.on("SIGTERM", stop);
7733
+ try {
7734
+ await runProvisioner(config, abort.signal);
7735
+ } finally {
7736
+ process.off("SIGINT", stop);
7737
+ process.off("SIGTERM", stop);
7738
+ }
7739
+ }
7740
+ }),
7741
+ install: defineCommand({
7742
+ args: configArg,
7743
+ run: async ({ args }) => {
7744
+ const config = await loadProvisionerConfig(args.config);
7745
+ await installProvisionerAgent(config, args.config);
7746
+ console.log(
7747
+ "installed dev.fmap.baton-provisioner; inspect ~/Library/Logs/baton-provisioner.log"
7748
+ );
7749
+ }
7750
+ })
7751
+ }
7752
+ });
7753
+
7267
7754
  // src/commands/relay.ts
7268
7755
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
7269
7756
  import { tmpdir as tmpdir2 } from "node:os";
7270
- import { join as join4 } from "node:path";
7757
+ import { join as join5 } from "node:path";
7271
7758
 
7272
7759
  // src/client/relay.ts
7273
7760
  var relayClient = (baseUrl) => {
@@ -7395,11 +7882,11 @@ var listenCommand2 = defineCommand({
7395
7882
  process.stdout.write(`${JSON.stringify(o)}
7396
7883
  `);
7397
7884
  };
7398
- const spillDir = join4(tmpdir2(), "hotline", args.channel);
7885
+ const spillDir = join5(tmpdir2(), "hotline", args.channel);
7399
7886
  mkdirSync2(spillDir, { recursive: true });
7400
7887
  const surface = (m) => {
7401
7888
  const text = typeof m.text === "string" ? m.text : "";
7402
- const file = join4(spillDir, `${m.seq}.txt`);
7889
+ const file = join5(spillDir, `${m.seq}.txt`);
7403
7890
  writeFileSync4(file, text);
7404
7891
  const truncated = text.length > PREVIEW2;
7405
7892
  emit({
@@ -7426,10 +7913,10 @@ var listenCommand2 = defineCommand({
7426
7913
  onError: (e) => emit({ type: "relay.error", error: describe2(e) })
7427
7914
  });
7428
7915
  emit({ type: "relay.listening", channel: args.channel, from: me, url });
7429
- await new Promise((resolve3) => {
7916
+ await new Promise((resolve4) => {
7430
7917
  const stop = () => {
7431
7918
  close();
7432
- resolve3();
7919
+ resolve4();
7433
7920
  };
7434
7921
  process.on("SIGINT", stop);
7435
7922
  process.on("SIGTERM", stop);
@@ -7553,6 +8040,9 @@ var requirement = defineCommand({
7553
8040
  }
7554
8041
  });
7555
8042
 
8043
+ // src/commands/service.ts
8044
+ import { resolve as resolve2 } from "node:path";
8045
+
7556
8046
  // src/worker/service-child.ts
7557
8047
  import { spawn } from "node:child_process";
7558
8048
 
@@ -7633,25 +8123,15 @@ var formatUptime = (startedAt, now = Date.now()) => {
7633
8123
  if (seconds >= 60) return `${Math.floor(seconds / 60)}m`;
7634
8124
  return `${seconds}s`;
7635
8125
  };
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
8126
  var parseTarget = (target) => {
7648
8127
  const explicit = target.match(/^W-(\d+)\/(.+)$/i);
7649
- return explicit ? { workerId: Number(explicit[1]), name: explicit[2] ?? "" } : { workerId: currentWorker(), name: target };
8128
+ return explicit ? { workerId: Number(explicit[1]), name: explicit[2] ?? "" } : { workerId: resolveWorkerId(), name: target };
7650
8129
  };
7651
8130
  var runCommand2 = defineCommand({
7652
8131
  meta: { name: "run", description: "run a service on the current worker" },
7653
8132
  args: {
7654
8133
  name: { type: "positional", required: true, description: "service name" },
8134
+ cwd: { type: "string", required: true, description: "service working directory on the Worker" },
7655
8135
  "public-url": { type: "string", description: "public HTTP(S) URL for this service" },
7656
8136
  note: { type: "string", description: "short note describing this service" },
7657
8137
  ...common
@@ -7666,9 +8146,9 @@ var runCommand2 = defineCommand({
7666
8146
  throw new Error("--public-url must be an absolute HTTP(S) URL");
7667
8147
  if (note && !isServiceNote(note))
7668
8148
  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,
8149
+ const workerId = resolveWorkerId();
8150
+ const result = await clientFor(args).services.run(workerId, {
8151
+ cwd: resolve2(args.cwd),
7672
8152
  name: args.name,
7673
8153
  argv,
7674
8154
  ...publicUrl ? { publicUrl } : {},
@@ -7676,7 +8156,7 @@ var runCommand2 = defineCommand({
7676
8156
  });
7677
8157
  if (!result.ok) throw new Error(result.error);
7678
8158
  if (args.json) return console.log(toJson(result));
7679
- console.log(`running W-${context.workerId}/${args.name}`);
8159
+ console.log(`running W-${workerId}/${args.name}`);
7680
8160
  if (publicUrl) console.log(`public: ${publicUrl}`);
7681
8161
  if (result.logPath) console.log(`logs: ${result.logPath}`);
7682
8162
  }
@@ -7693,7 +8173,7 @@ var lsCommand = defineCommand({
7693
8173
  const workers = await client.workers.listByProject(projectId);
7694
8174
  const names = new Map(workers.map((worker2) => [worker2.id, worker2.name]));
7695
8175
  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)}`
8176
+ (item) => `${serviceHandle(item)} ${names.get(item.workerId) ?? "unknown"} cwd=${item.cwd} uptime=${formatUptime(item.startedAt)}`
7697
8177
  ).forEach((line) => {
7698
8178
  console.log(line);
7699
8179
  });
@@ -7743,7 +8223,7 @@ var sessionAbortCommand = defineCommand({
7743
8223
  var sessionClearCommand = defineCommand({
7744
8224
  meta: {
7745
8225
  name: "clear",
7746
- description: "reset the conversation context (keeps the session + worktree)"
8226
+ description: "reset the conversation context (keeps the session + directory)"
7747
8227
  },
7748
8228
  args: {
7749
8229
  session: { type: "positional", required: true, description: "session int id or name" },
@@ -7852,7 +8332,7 @@ var sessionResumeCommand = defineCommand({
7852
8332
 
7853
8333
  // src/commands/session/rm.ts
7854
8334
  var sessionRmCommand = defineCommand({
7855
- meta: { name: "rm", description: "delete a session (irreversible; removes its worktree)" },
8335
+ meta: { name: "rm", description: "delete a session (irreversible; removes its runtime files)" },
7856
8336
  args: {
7857
8337
  session: { type: "positional", required: true, description: "session int id or name" },
7858
8338
  project: { type: "string", description: "project id (overrides .baton.json)" },
@@ -7863,7 +8343,9 @@ var sessionRmCommand = defineCommand({
7863
8343
  const c = clientFor(args);
7864
8344
  const handle = await resolveSession(c, resolveProjectId(args), args.session);
7865
8345
  if (!args.confirm) {
7866
- console.log(`[dry-run] would delete session ${handle.name} (#${handle.id}) + its worktree`);
8346
+ console.log(
8347
+ `[dry-run] would delete session ${handle.name} (#${handle.id}) + its runtime files`
8348
+ );
7867
8349
  console.log("re-run with --confirm to proceed.");
7868
8350
  return;
7869
8351
  }
@@ -7879,12 +8361,12 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
7879
8361
  // src/session/runner/transcript.ts
7880
8362
  import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
7881
8363
  import { homedir as homedir2 } from "node:os";
7882
- import { join as join5 } from "node:path";
8364
+ import { join as join6 } from "node:path";
7883
8365
  var findTranscriptPath = (agentSessionId) => {
7884
- const root = join5(homedir2(), ".claude", "projects");
8366
+ const root = join6(homedir2(), ".claude", "projects");
7885
8367
  try {
7886
8368
  for (const dir of readdirSync(root)) {
7887
- const candidate = join5(root, dir, `${agentSessionId}.jsonl`);
8369
+ const candidate = join6(root, dir, `${agentSessionId}.jsonl`);
7888
8370
  try {
7889
8371
  if (statSync(candidate).isFile()) return candidate;
7890
8372
  } catch {
@@ -7962,16 +8444,15 @@ var readFirstExchange = (agentSessionId) => {
7962
8444
  };
7963
8445
 
7964
8446
  // 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";
8447
+ import { mkdir as mkdir4, writeFile as writeFile2 } from "node:fs/promises";
8448
+ import { join as join10 } from "node:path";
7967
8449
 
7968
8450
  // src/session/runner/attachments.ts
7969
8451
  import { createWriteStream } from "node:fs";
7970
- import { mkdir, writeFile } from "node:fs/promises";
7971
- import { join as join6 } from "node:path";
8452
+ import { mkdir as mkdir3 } from "node:fs/promises";
8453
+ import { join as join7 } from "node:path";
7972
8454
  import { Readable } from "node:stream";
7973
8455
  import { pipeline } from "node:stream/promises";
7974
- var ATTACH_DIR = "attachments";
7975
8456
  var dedupe = (name, taken) => {
7976
8457
  if (!taken.has(name)) {
7977
8458
  taken.add(name);
@@ -7991,25 +8472,24 @@ var dedupe = (name, taken) => {
7991
8472
  };
7992
8473
  var materializeAttachments = async (opts) => {
7993
8474
  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 = [];
8475
+ const dir = opts.directory;
8476
+ await mkdir3(dir, { recursive: true });
8477
+ const taken = /* @__PURE__ */ new Set();
8478
+ const paths = [];
7999
8479
  for (const att of opts.attachments) {
8000
8480
  const name = dedupe(att.filename, taken);
8001
8481
  const res = await fetchImpl(`${opts.serverBase}${att.url}`);
8002
8482
  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}`);
8483
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(join7(dir, name)));
8484
+ paths.push(join7(dir, name));
8005
8485
  }
8006
- return relPaths;
8486
+ return paths;
8007
8487
  };
8008
- var augmentPrompt = (text, relPaths, labels) => {
8009
- if (relPaths.length === 0) return text;
8488
+ var augmentPrompt = (text, paths, labels) => {
8489
+ if (paths.length === 0) return text;
8010
8490
  const header = [
8011
- "[Attached files \u2014 already saved in the working directory. Reference them by label:]",
8012
- ...relPaths.map((p, i) => `- {${labels[i]}} ${p}`)
8491
+ "[Attached files \u2014 already saved locally. Reference them by label:]",
8492
+ ...paths.map((p, i) => `- {${labels[i]}} ${p}`)
8013
8493
  ].join("\n");
8014
8494
  return text.length > 0 ? `${header}
8015
8495
 
@@ -8017,18 +8497,18 @@ ${text}` : header;
8017
8497
  };
8018
8498
 
8019
8499
  // src/session/runner/codex.ts
8500
+ import { join as join8 } from "node:path";
8020
8501
  import { Codex } from "@openai/codex-sdk";
8021
8502
 
8022
8503
  // src/session/runner/sdk-env.ts
8023
8504
  import { existsSync as existsSync3 } from "node:fs";
8024
- import { isAbsolute } from "node:path";
8505
+ import { isAbsolute as isAbsolute2 } from "node:path";
8025
8506
  var buildSdkEnv = (overlay) => overlay ? { ...process.env, ...overlay } : void 0;
8026
8507
  var claudeExecutable = () => process.env.BATON_CLAUDE_BIN;
8027
- var additionalDirs = () => {
8508
+ var additionalDirs = (required = []) => {
8028
8509
  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;
8510
+ const dirs = [...required, ...(raw ?? "").split(":")].map((s) => s.trim()).filter((s) => s !== "" && isAbsolute2(s) && existsSync3(s));
8511
+ return dirs.length > 0 ? [...new Set(dirs)] : void 0;
8032
8512
  };
8033
8513
  var truthy = (value) => {
8034
8514
  if (value === void 0) return void 0;
@@ -8188,7 +8668,7 @@ async function* startCodexEvents(config, input, worker2, opts) {
8188
8668
  const env = compactEnv(buildSdkEnv(opts.envOverlay));
8189
8669
  const codex = new Codex(env ? { env } : void 0);
8190
8670
  const resuming = !config.agentSessionId.startsWith("pending:");
8191
- const addDirs = additionalDirs();
8671
+ const addDirs = additionalDirs([join8(config.runtimeDir, "attachments")]);
8192
8672
  const sandboxMode = codexSandboxMode(opts.planMode ?? false);
8193
8673
  const approvalPolicy = codexApprovalPolicy();
8194
8674
  const networkAccessEnabled = codexNetworkAccess();
@@ -8199,7 +8679,7 @@ async function* startCodexEvents(config, input, worker2, opts) {
8199
8679
  if (opts.model) opts.log(`[model] ${opts.model}`);
8200
8680
  if (reasoningEffort) opts.log(`[effort] ${reasoningEffort}`);
8201
8681
  const threadOptions = {
8202
- workingDirectory: config.worktreePath,
8682
+ workingDirectory: config.cwd,
8203
8683
  skipGitRepoCheck: true,
8204
8684
  sandboxMode,
8205
8685
  approvalPolicy,
@@ -8218,8 +8698,7 @@ async function* startCodexEvents(config, input, worker2, opts) {
8218
8698
  if (event.type === "thread.started" && !resuming && event.sessionId) {
8219
8699
  config.agentSessionId = event.sessionId;
8220
8700
  await worker2.materialize({
8221
- agentSessionId: event.sessionId,
8222
- worktreePath: config.worktreePath
8701
+ agentSessionId: event.sessionId
8223
8702
  });
8224
8703
  opts.log(`[codex] thread ${event.sessionId}`);
8225
8704
  }
@@ -8228,17 +8707,18 @@ async function* startCodexEvents(config, input, worker2, opts) {
8228
8707
  }
8229
8708
 
8230
8709
  // src/session/runner/query.ts
8710
+ import { join as join9 } from "node:path";
8231
8711
  var startQuery = (config, text, resuming, queryFn, abortController, log, { envOverlay, planMode = false, model, effort } = {}) => {
8232
8712
  const env = buildSdkEnv(envOverlay);
8233
8713
  const exe = claudeExecutable();
8234
- const addDirs = additionalDirs();
8714
+ const addDirs = additionalDirs([join9(config.runtimeDir, "attachments")]);
8235
8715
  const level = claudeEffort(effort);
8236
8716
  if (addDirs) log(`[add-dir] ${addDirs.join(", ")}`);
8237
8717
  if (planMode) log("[plan] read-only planning turn");
8238
8718
  if (model) log(`[model] ${model}`);
8239
8719
  if (level) log(`[effort] ${level}`);
8240
8720
  const options = {
8241
- cwd: config.worktreePath,
8721
+ cwd: config.cwd,
8242
8722
  permissionMode: planMode ? "plan" : "bypassPermissions",
8243
8723
  // Headless relay: no TTY/TUI can answer an interactive ask, so AskUserQuestion
8244
8724
  // would fail the whole turn. Block it — the model asks in plain text instead,
@@ -8482,15 +8962,19 @@ var UnsettledExecution = class extends Error {
8482
8962
  var materializeImages = async (config, execution) => {
8483
8963
  const images = execution.message.payload.images ?? [];
8484
8964
  if (!images.length) return [];
8485
- const directory = join7(config.worktreePath, "attachments");
8486
- await mkdir2(directory, { recursive: true });
8487
- await writeFile2(join7(directory, ".gitignore"), "*\n");
8965
+ const directory2 = join10(
8966
+ config.runtimeDir,
8967
+ "attachments",
8968
+ `message-${execution.message.id}`,
8969
+ "pasted"
8970
+ );
8971
+ await mkdir4(directory2, { recursive: true });
8488
8972
  return Promise.all(
8489
8973
  images.map(async (image, index) => {
8490
8974
  const match = /^data:image\/(png|jpeg|webp|gif);base64,([A-Za-z0-9+/=\r\n]+)$/.exec(image);
8491
8975
  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"));
8976
+ const path = join10(directory2, `pasted-${index + 1}.${match[1]}`);
8977
+ await writeFile2(path, Buffer.from(match[2] ?? "", "base64"));
8494
8978
  return path;
8495
8979
  })
8496
8980
  );
@@ -8498,7 +8982,12 @@ var materializeImages = async (config, execution) => {
8498
8982
  var buildPrompt = async (config, execution, fetchImpl) => {
8499
8983
  const { text: rawText, attachments = [] } = execution.message.payload;
8500
8984
  const paths = attachments.length ? await materializeAttachments({
8501
- worktreePath: config.worktreePath,
8985
+ directory: join10(
8986
+ config.runtimeDir,
8987
+ "attachments",
8988
+ `message-${execution.message.id}`,
8989
+ "files"
8990
+ ),
8502
8991
  serverBase: config.server,
8503
8992
  attachments,
8504
8993
  fetchImpl
@@ -8515,7 +9004,7 @@ var buildPrompt = async (config, execution, fetchImpl) => {
8515
9004
  ];
8516
9005
  const images = imagePaths.map((path) => ({
8517
9006
  type: "local_image",
8518
- path: join7(config.worktreePath, path)
9007
+ path
8519
9008
  }));
8520
9009
  return { text, input: images.length ? [{ type: "text", text }, ...images] : text };
8521
9010
  };
@@ -8632,8 +9121,8 @@ var runDaemon = async (config, deps, signal) => {
8632
9121
  let lastActivity = Date.now();
8633
9122
  let stop = () => {
8634
9123
  };
8635
- const stopped = new Promise((resolve3) => {
8636
- stop = resolve3;
9124
+ const stopped = new Promise((resolve4) => {
9125
+ stop = resolve4;
8637
9126
  });
8638
9127
  const drain = async () => {
8639
9128
  if (draining || closed || stranded) return;
@@ -8745,6 +9234,19 @@ var runDaemon = async (config, deps, signal) => {
8745
9234
  }
8746
9235
  };
8747
9236
 
9237
+ // src/worker/runtime-paths.ts
9238
+ import { createHash } from "node:crypto";
9239
+ import { homedir as homedir3 } from "node:os";
9240
+ import { join as join11 } from "node:path";
9241
+ var workerRuntimeDir = (owner, env = process.env) => join11(
9242
+ env.XDG_DATA_HOME ?? join11(env.HOME ?? homedir3(), ".local/share"),
9243
+ "baton",
9244
+ "runtime",
9245
+ createHash("sha256").update(owner.server).digest("hex"),
9246
+ `worker-${owner.workerId}`
9247
+ );
9248
+ var sessionRuntimeDir = (owner, sessionId, env = process.env) => join11(workerRuntimeDir(owner, env), "sessions", String(sessionId));
9249
+
8748
9250
  // src/commands/session/run.ts
8749
9251
  var bearerFetch = (token) => (input, init2) => fetch(input, {
8750
9252
  ...init2,
@@ -8774,13 +9276,13 @@ var sessionRunCommand = defineCommand({
8774
9276
  },
8775
9277
  run: async ({ args }) => {
8776
9278
  if (!process.connected) throw new Error("session runner requires supervisor IPC");
8777
- await new Promise((resolve3, reject) => {
9279
+ await new Promise((resolve4, reject) => {
8778
9280
  const onMessage = (message) => {
8779
9281
  if (typeof message !== "object" || message === null || !("type" in message) || message.type !== "execute")
8780
9282
  return;
8781
9283
  process.off("message", onMessage);
8782
9284
  process.off("disconnect", onDisconnect);
8783
- resolve3();
9285
+ resolve4();
8784
9286
  };
8785
9287
  const onDisconnect = () => {
8786
9288
  process.off("message", onMessage);
@@ -8799,7 +9301,7 @@ var sessionRunCommand = defineCommand({
8799
9301
  throw new Error(`invalid session id: ${args.session}`);
8800
9302
  const client = createClient(server, { bearer: workerToken });
8801
9303
  const s = await client.sessions.get(sessionId);
8802
- if (!s.agentSessionId || !s.worktreePath)
9304
+ if (!s.agentSessionId || !s.cwd)
8803
9305
  throw new Error(`session #${sessionId} is not materialized yet`);
8804
9306
  const config = {
8805
9307
  server,
@@ -8807,7 +9309,8 @@ var sessionRunCommand = defineCommand({
8807
9309
  name: s.name,
8808
9310
  agentKind: s.agentKind,
8809
9311
  agentSessionId: s.agentSessionId,
8810
- worktreePath: s.worktreePath
9312
+ cwd: s.cwd,
9313
+ runtimeDir: sessionRuntimeDir({ server, workerId: s.workerId }, sessionId)
8811
9314
  };
8812
9315
  const worker2 = createWorkerClient(server, workerToken, sessionId);
8813
9316
  const ac = new AbortController();
@@ -9100,184 +9603,60 @@ var task = defineCommand({
9100
9603
  // src/commands/worker/index.ts
9101
9604
  import { hostname as osHostname } from "node:os";
9102
9605
 
9103
- // src/session/worktree.ts
9104
- import { execFile, execFileSync, spawnSync } from "node:child_process";
9606
+ // src/worker/bundled-skills.ts
9607
+ import { spawnSync as spawnSync2 } from "node:child_process";
9608
+ import { cpSync, existsSync as existsSync5 } from "node:fs";
9609
+ import { dirname as dirname3, join as join12 } from "node:path";
9610
+ import { fileURLToPath } from "node:url";
9611
+
9612
+ // src/worker/git-exclude.ts
9613
+ import { spawnSync } from "node:child_process";
9105
9614
  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)
9615
+ import { dirname as dirname2, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
9616
+ var ensureExcluded = (cwd, pattern) => {
9617
+ const result = spawnSync("git", ["-C", cwd, "rev-parse", "--git-path", "info/exclude"], {
9618
+ encoding: "utf8",
9619
+ env: { ...process.env, LC_ALL: "C" }
9249
9620
  });
9621
+ if (result.error) throw result.error;
9622
+ if (result.status !== 0) {
9623
+ if (result.stderr.includes("not a git repository")) return;
9624
+ throw new Error(`cannot locate Git exclude file: ${result.stderr.trim()}`);
9625
+ }
9626
+ const location = result.stdout.trim();
9627
+ const path = isAbsolute3(location) ? location : resolve3(cwd, location);
9628
+ const current = existsSync4(path) ? readFileSync5(path, "utf8") : "";
9629
+ if (current.split("\n").includes(pattern)) return;
9630
+ mkdirSync3(dirname2(path), { recursive: true });
9631
+ appendFileSync(path, `${current && !current.endsWith("\n") ? "\n" : ""}${pattern}
9632
+ `);
9250
9633
  };
9251
9634
 
9252
9635
  // 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
9636
  var SKILL_NAME = "baton-publish-artifact";
9258
9637
  var TARGETS = [`.claude/skills/${SKILL_NAME}`, `.agents/skills/${SKILL_NAME}`];
9259
9638
  var bundledSkillPath = () => {
9260
- const here = dirname2(fileURLToPath(import.meta.url));
9639
+ const here = dirname3(fileURLToPath(import.meta.url));
9261
9640
  const candidates = [
9262
- join8(here, "..", "skills", SKILL_NAME),
9263
- join8(here, "..", "..", "skills", SKILL_NAME)
9641
+ join12(here, "..", "skills", SKILL_NAME),
9642
+ join12(here, "..", "..", "skills", SKILL_NAME)
9264
9643
  ];
9265
9644
  const found = candidates.find(existsSync5);
9266
9645
  if (!found) throw new Error(`bundled skill missing: ${SKILL_NAME}`);
9267
9646
  return found;
9268
9647
  };
9269
- var isTracked = (worktreePath, relativePath) => {
9270
- const result = spawnSync2("git", ["-C", worktreePath, "ls-files", "--", relativePath], {
9648
+ var isTracked = (cwd, relativePath) => {
9649
+ const result = spawnSync2("git", ["-C", cwd, "ls-files", "--", relativePath], {
9271
9650
  encoding: "utf8",
9272
9651
  stdio: "pipe"
9273
9652
  });
9274
9653
  return result.status === 0 && result.stdout.trim().length > 0;
9275
9654
  };
9276
- var syncBundledArtifactSkill = (repo, worktreePath) => {
9655
+ var syncBundledArtifactSkill = (cwd) => {
9277
9656
  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 });
9657
+ TARGETS.filter((target) => !isTracked(cwd, target)).forEach((target) => {
9658
+ ensureExcluded(cwd, `${target}/`);
9659
+ cpSync(source, join12(cwd, target), { recursive: true, force: true });
9281
9660
  });
9282
9661
  };
9283
9662
 
@@ -9339,12 +9718,13 @@ var createLiveness = (deps) => {
9339
9718
 
9340
9719
  // src/worker/service-supervisor.ts
9341
9720
  import { mkdirSync as mkdirSync4 } from "node:fs";
9342
- import { join as join10 } from "node:path";
9721
+ import { stat as stat3 } from "node:fs/promises";
9722
+ import { join as join14 } from "node:path";
9343
9723
 
9344
9724
  // src/worker/service-process.ts
9345
9725
  import { spawn as spawn2 } from "node:child_process";
9346
9726
  import { closeSync, existsSync as existsSync6, openSync } from "node:fs";
9347
- import { dirname as dirname3, join as join9 } from "node:path";
9727
+ import { dirname as dirname4, join as join13 } from "node:path";
9348
9728
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9349
9729
  var START_TIMEOUT_MS = 1e4;
9350
9730
  var isWrapperMessage = (message) => {
@@ -9354,8 +9734,8 @@ var isWrapperMessage = (message) => {
9354
9734
  return value.type === "error" && typeof value.error === "string";
9355
9735
  };
9356
9736
  var binPath = () => {
9357
- const here = dirname3(fileURLToPath2(import.meta.url));
9358
- const devShim = join9(here, "..", "..", "bin", "baton.mjs");
9737
+ const here = dirname4(fileURLToPath2(import.meta.url));
9738
+ const devShim = join13(here, "..", "..", "bin", "baton.mjs");
9359
9739
  return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
9360
9740
  };
9361
9741
  var startServiceProcess = (input) => {
@@ -9370,7 +9750,7 @@ var startServiceProcess = (input) => {
9370
9750
  } finally {
9371
9751
  closeSync(logFd);
9372
9752
  }
9373
- const started = new Promise((resolve3, reject) => {
9753
+ const started = new Promise((resolve4, reject) => {
9374
9754
  let settled = false;
9375
9755
  let timer;
9376
9756
  const cleanup = () => {
@@ -9384,7 +9764,7 @@ var startServiceProcess = (input) => {
9384
9764
  settled = true;
9385
9765
  cleanup();
9386
9766
  if (result instanceof Error) reject(result);
9387
- else resolve3(result);
9767
+ else resolve4(result);
9388
9768
  };
9389
9769
  const onMessage = (message) => {
9390
9770
  if (!isWrapperMessage(message)) return;
@@ -9409,13 +9789,13 @@ var startServiceProcess = (input) => {
9409
9789
  };
9410
9790
 
9411
9791
  // src/worker/service-supervisor.ts
9412
- var waitForExit = (child) => child.exitCode !== null ? Promise.resolve() : new Promise((resolve3) => child.once("exit", () => resolve3()));
9792
+ var waitForExit = (child) => child.exitCode !== null ? Promise.resolve() : new Promise((resolve4) => child.once("exit", () => resolve4()));
9413
9793
  var createServiceSupervisor = (deps) => {
9414
- const { getSession, sendReport, cfg, log } = deps;
9794
+ const { sendReport, cfg, log } = deps;
9415
9795
  const entries = /* @__PURE__ */ new Map();
9416
9796
  let reportQueue = Promise.resolve();
9417
- const snapshot = () => [...entries.values()].filter((entry) => entry.appPid !== void 0).map(({ sessionId, name, startedAt, publicUrl, note }) => ({
9418
- sessionId,
9797
+ const snapshot = () => [...entries.values()].filter((entry) => entry.appPid !== void 0).map(({ cwd, name, startedAt, publicUrl, note }) => ({
9798
+ cwd,
9419
9799
  name,
9420
9800
  startedAt,
9421
9801
  ...publicUrl ? { publicUrl } : {},
@@ -9425,15 +9805,13 @@ var createServiceSupervisor = (deps) => {
9425
9805
  const body = { services: snapshot(), ...result ? { result } : {} };
9426
9806
  const request2 = reportQueue.then(() => sendReport(body)).then(() => {
9427
9807
  });
9428
- reportQueue = request2.catch(() => {
9429
- });
9808
+ reportQueue = request2.catch((error) => log(`service report failed: ${String(error)}`));
9430
9809
  return request2;
9431
9810
  };
9432
9811
  const remove = (entry) => {
9433
9812
  if (entries.get(entry.name) !== entry) return;
9434
9813
  entries.delete(entry.name);
9435
- void report().catch(() => {
9436
- });
9814
+ void report().catch((error) => log(`service removal report failed: ${String(error)}`));
9437
9815
  };
9438
9816
  const stopEntry = (entry) => {
9439
9817
  if (entry.wrapper.connected) {
@@ -9442,7 +9820,18 @@ var createServiceSupervisor = (deps) => {
9442
9820
  }
9443
9821
  if (entry.appPid) killPidGroup(entry.appPid, "SIGKILL");
9444
9822
  };
9445
- const runOne = async (requestId, sessionId, name, argv, publicUrl, note) => {
9823
+ const runOne = async (requestId, cwd, name, argv, publicUrl, note) => {
9824
+ try {
9825
+ if (!(await stat3(cwd)).isDirectory())
9826
+ return report({ requestId, ok: false, error: `not a directory: ${cwd}`, status: 400 });
9827
+ } catch (error) {
9828
+ const code = error.code;
9829
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
9830
+ return report({ requestId, ok: false, error: `directory not found: ${cwd}`, status: 400 });
9831
+ }
9832
+ const logDir = join14(workerRuntimeDir(cfg), "services");
9833
+ const logPath = join14(logDir, `${name}.log`);
9834
+ mkdirSync4(logDir, { recursive: true, mode: 448 });
9446
9835
  if (entries.has(name))
9447
9836
  return report({
9448
9837
  requestId,
@@ -9450,21 +9839,10 @@ var createServiceSupervisor = (deps) => {
9450
9839
  error: `service "${name}" is already running`,
9451
9840
  status: 409
9452
9841
  });
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 });
9842
+ const { wrapper, started } = startServiceProcess({ argv, cwd, logPath });
9465
9843
  const entry = {
9466
9844
  wrapper,
9467
- sessionId,
9845
+ cwd,
9468
9846
  name,
9469
9847
  startedAt: Date.now(),
9470
9848
  publicUrl,
@@ -9483,19 +9861,20 @@ var createServiceSupervisor = (deps) => {
9483
9861
  }
9484
9862
  const service2 = {
9485
9863
  workerId: cfg.workerId,
9486
- sessionId,
9864
+ cwd,
9487
9865
  name,
9488
9866
  startedAt: entry.startedAt,
9489
9867
  ...publicUrl ? { publicUrl } : {},
9490
9868
  ...note ? { note } : {}
9491
9869
  };
9492
- log(`service ${name} started in ${session2.worktreePath}`);
9870
+ log(`service ${name} started in ${cwd}`);
9493
9871
  await report({ requestId, ok: true, service: service2, logPath });
9494
9872
  };
9495
- const run2 = async (requestId, sessionId, name, argv, publicUrl, note) => {
9873
+ const run2 = async (requestId, cwd, name, argv, publicUrl, note) => {
9496
9874
  try {
9497
- await runOne(requestId, sessionId, name, argv, publicUrl, note);
9875
+ await runOne(requestId, cwd, name, argv, publicUrl, note);
9498
9876
  } catch (error) {
9877
+ log(`service ${name} failed: ${String(error)}`);
9499
9878
  await report({ requestId, ok: false, error: String(error), status: 500 });
9500
9879
  }
9501
9880
  };
@@ -9508,21 +9887,16 @@ var createServiceSupervisor = (deps) => {
9508
9887
  remove(entry);
9509
9888
  await report({ requestId, ok: true });
9510
9889
  };
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
9890
  const killAll = () => entries.forEach(stopEntry);
9518
- return { run: run2, stop, stopSession, report, killAll };
9891
+ return { run: run2, stop, report, killAll };
9519
9892
  };
9520
9893
 
9521
9894
  // src/worker/session-supervisor.ts
9522
9895
  import { spawn as spawn3 } from "node:child_process";
9523
9896
  import { randomUUID as randomUUID2 } from "node:crypto";
9524
9897
  import { existsSync as existsSync7 } from "node:fs";
9525
- import { dirname as dirname4, join as join13 } from "node:path";
9898
+ import { mkdir as mkdir5, rm as rm2, stat as stat4 } from "node:fs/promises";
9899
+ import { dirname as dirname5, join as join17 } from "node:path";
9526
9900
  import { fileURLToPath as fileURLToPath3 } from "node:url";
9527
9901
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
9528
9902
 
@@ -9559,7 +9933,7 @@ var buildTitlePrompt = (userText, assistantText) => {
9559
9933
  ].join("\n");
9560
9934
  };
9561
9935
  var generateTitle = async (input) => {
9562
- const { worktreePath, userText, assistantText, queryFn } = input;
9936
+ const { cwd, userText, assistantText, queryFn } = input;
9563
9937
  if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
9564
9938
  const exe = claudeExecutable();
9565
9939
  const abort = new AbortController();
@@ -9572,7 +9946,7 @@ var generateTitle = async (input) => {
9572
9946
  const messages = queryFn({
9573
9947
  prompt: buildTitlePrompt(userText, assistantText),
9574
9948
  options: {
9575
- cwd: worktreePath,
9949
+ cwd,
9576
9950
  // A one-shot summarizer, not an agent: replace the ~18k-token Claude
9577
9951
  // Code preset system prompt. Inside the agent harness the model
9578
9952
  // routinely answers NONE to this ask (verified against reclaude);
@@ -9616,7 +9990,7 @@ var generateTitle = async (input) => {
9616
9990
  // src/session/runner/title-codex.ts
9617
9991
  import { mkdtemp, rm } from "node:fs/promises";
9618
9992
  import { tmpdir as tmpdir3 } from "node:os";
9619
- import { join as join11 } from "node:path";
9993
+ import { join as join15 } from "node:path";
9620
9994
  import { Codex as Codex2 } from "@openai/codex-sdk";
9621
9995
  var generateTitleWithCodex = async (input) => {
9622
9996
  const { userText, assistantText } = input;
@@ -9625,7 +9999,7 @@ var generateTitleWithCodex = async (input) => {
9625
9999
  const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
9626
10000
  let cwd = "";
9627
10001
  try {
9628
- cwd = await mkdtemp(join11(tmpdir3(), "baton-title-"));
10002
+ cwd = await mkdtemp(join15(tmpdir3(), "baton-title-"));
9629
10003
  const client = input.client ?? new Codex2();
9630
10004
  const thread = client.startThread({
9631
10005
  workingDirectory: cwd,
@@ -9652,18 +10026,18 @@ var generateTitleWithCodex = async (input) => {
9652
10026
  };
9653
10027
 
9654
10028
  // 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);
10029
+ import { execFile as execFile4 } from "node:child_process";
10030
+ import { readFile as readFile2, rename, writeFile as writeFile3 } from "node:fs/promises";
10031
+ import { join as join16 } from "node:path";
10032
+ import { promisify as promisify4 } from "node:util";
10033
+ var run = promisify4(execFile4);
9660
10034
  var RUNNER_RECORD = ".baton-runner.json";
9661
- var recordPath = (worktreePath) => join12(worktreePath, RUNNER_RECORD);
10035
+ var recordPath = (cwd) => join16(cwd, RUNNER_RECORD);
9662
10036
  var codeOf = (error) => typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
9663
- var readRunnerRecord = async (worktreePath) => {
10037
+ var readRunnerRecord = async (cwd) => {
9664
10038
  let raw;
9665
10039
  try {
9666
- raw = await readFile(recordPath(worktreePath), "utf8");
10040
+ raw = await readFile2(recordPath(cwd), "utf8");
9667
10041
  } catch (error) {
9668
10042
  if (codeOf(error) === "ENOENT") return null;
9669
10043
  throw error;
@@ -9673,8 +10047,8 @@ var readRunnerRecord = async (worktreePath) => {
9673
10047
  throw new Error("invalid session process record");
9674
10048
  return value;
9675
10049
  };
9676
- var writeRunnerRecord = async (worktreePath, record) => {
9677
- const path = recordPath(worktreePath);
10050
+ var writeRunnerRecord = async (cwd, record) => {
10051
+ const path = recordPath(cwd);
9678
10052
  const temporary = `${path}.${record.runnerToken}.tmp`;
9679
10053
  await writeFile3(temporary, JSON.stringify(record), { mode: 384, flag: "wx" });
9680
10054
  await rename(temporary, path);
@@ -9714,37 +10088,23 @@ var stopRecordedRunner = async (record, expected, graceMs = 5e3) => {
9714
10088
  signalGroup(record.pgid, "SIGTERM");
9715
10089
  const deadline = Date.now() + graceMs;
9716
10090
  while (groupExists(record.pgid) && Date.now() < deadline)
9717
- await new Promise((resolve3) => setTimeout(resolve3, 50));
10091
+ await new Promise((resolve4) => setTimeout(resolve4, 50));
9718
10092
  if (!groupExists(record.pgid)) return;
9719
10093
  signalGroup(record.pgid, "SIGKILL");
9720
10094
  const killDeadline = Date.now() + graceMs;
9721
10095
  while (groupExists(record.pgid) && Date.now() < killDeadline)
9722
- await new Promise((resolve3) => setTimeout(resolve3, 50));
10096
+ await new Promise((resolve4) => setTimeout(resolve4, 50));
9723
10097
  if (groupExists(record.pgid)) throw new Error("session process group has not exited");
9724
10098
  };
9725
10099
 
9726
10100
  // src/worker/session-supervisor.ts
9727
10101
  var binPath2 = () => {
9728
- const here = dirname4(fileURLToPath3(import.meta.url));
9729
- const devShim = join13(here, "..", "..", "bin", "baton.mjs");
10102
+ const here = dirname5(fileURLToPath3(import.meta.url));
10103
+ const devShim = join17(here, "..", "..", "bin", "baton.mjs");
9730
10104
  return existsSync7(devShim) ? devShim : fileURLToPath3(import.meta.url);
9731
10105
  };
9732
10106
  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
- };
10107
+ const { client, cfg, cwd, log, hasTerminal, closeTerminal } = deps;
9748
10108
  const children = /* @__PURE__ */ new Map();
9749
10109
  const lifecycle = /* @__PURE__ */ new Map();
9750
10110
  const enqueue = (id, action) => {
@@ -9763,8 +10123,8 @@ var createSessionSupervisor = (deps) => {
9763
10123
  const cancelStart = (sessionId) => {
9764
10124
  startEpochs.set(sessionId, currentStartEpoch(sessionId) + 1);
9765
10125
  };
9766
- const stopRecord = async (sessionId, worktreePath, token) => {
9767
- const record = await readRunnerRecord(worktreePath);
10126
+ const stopRecord = async (sessionId, token) => {
10127
+ const record = await readRunnerRecord(sessionRuntimeDir(cfg, sessionId));
9768
10128
  if (!record) {
9769
10129
  if (token) throw new Error("missing record for an unfinished execution");
9770
10130
  return;
@@ -9773,9 +10133,9 @@ var createSessionSupervisor = (deps) => {
9773
10133
  throw new Error("runner identity does not match the attempt");
9774
10134
  await stopRecordedRunner(record, { server: cfg.server, workerId: cfg.workerId, sessionId });
9775
10135
  };
9776
- const spawnChild = async (sessionId, worktreePath, wasCanceled) => {
10136
+ const spawnChild = async (sessionId, cwd2, wasCanceled) => {
9777
10137
  if (children.has(sessionId)) return;
9778
- await stopRecord(sessionId, worktreePath);
10138
+ await stopRecord(sessionId);
9779
10139
  const runnerToken = randomUUID2();
9780
10140
  const child = spawn3(
9781
10141
  process.execPath,
@@ -9783,16 +10143,17 @@ var createSessionSupervisor = (deps) => {
9783
10143
  {
9784
10144
  detached: true,
9785
10145
  stdio: ["inherit", "inherit", "inherit", "ipc"],
9786
- env: { ...process.env, BATON_SERVER: cfg.server, BATON_WORKER_TOKEN: cfg.apiToken }
10146
+ cwd: cwd2,
10147
+ env: workerEnv(cfg)
9787
10148
  }
9788
10149
  );
9789
- const entry = { child, worktreePath };
10150
+ const entry = { child };
9790
10151
  children.set(sessionId, entry);
9791
10152
  let readyTimer;
9792
- const ready = new Promise((resolve3, reject) => {
10153
+ const ready = new Promise((resolve4, reject) => {
9793
10154
  child.on("message", (message) => {
9794
10155
  if (typeof message === "object" && message !== null && "type" in message && message.type === "ready")
9795
- resolve3();
10156
+ resolve4();
9796
10157
  });
9797
10158
  child.once("error", reject);
9798
10159
  child.once("exit", () => reject(new Error("session child exited before ready")));
@@ -9812,8 +10173,8 @@ var createSessionSupervisor = (deps) => {
9812
10173
  void stop(sessionId).catch((error) => log(`stuck execution cleanup failed: ${String(error)}`));
9813
10174
  });
9814
10175
  try {
9815
- await new Promise((resolve3, reject) => {
9816
- child.once("spawn", resolve3);
10176
+ await new Promise((resolve4, reject) => {
10177
+ child.once("spawn", resolve4);
9817
10178
  child.once("error", reject);
9818
10179
  });
9819
10180
  if (!child.pid) throw new Error("session child has no PID");
@@ -9825,11 +10186,11 @@ var createSessionSupervisor = (deps) => {
9825
10186
  pid: child.pid,
9826
10187
  pgid: child.pid
9827
10188
  };
9828
- await writeRunnerRecord(worktreePath, record);
10189
+ await writeRunnerRecord(sessionRuntimeDir(cfg, sessionId), record);
9829
10190
  await readiness;
9830
10191
  if (wasCanceled()) return;
9831
10192
  await new Promise(
9832
- (resolve3, reject) => child.send({ type: "execute" }, (error) => error ? reject(error) : resolve3())
10193
+ (resolve4, reject) => child.send({ type: "execute" }, (error) => error ? reject(error) : resolve4())
9833
10194
  );
9834
10195
  } catch (error) {
9835
10196
  killProcessGroup(child);
@@ -9847,37 +10208,20 @@ var createSessionSupervisor = (deps) => {
9847
10208
  if (execution.paused || execution.contextResetRequested || execution.attempt?.status === "running" || execution.attempt?.status === "stopping")
9848
10209
  return;
9849
10210
  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);
10211
+ const directory2 = session2.cwd ?? cwd;
10212
+ if (!(await stat4(directory2)).isDirectory()) throw new Error(`not a directory: ${directory2}`);
10213
+ if (wasCanceled()) return;
10214
+ if (!session2.agentSessionId || !session2.cwd) {
10215
+ const id = randomUUID2();
10216
+ const agentSessionId = session2.agentKind === "codex" ? `pending:${id}` : id;
10217
+ await client.sessions.materialize(sessionId, { agentSessionId, cwd: directory2 }, cfg.apiToken);
10218
+ log(`materialized session #${sessionId} (${name}) \u2192 ${directory2}`);
10219
+ }
10220
+ await mkdir5(sessionRuntimeDir(cfg, sessionId), { recursive: true, mode: 448 });
9877
10221
  if (hasTerminal(sessionId))
9878
10222
  return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
9879
10223
  if (wasCanceled()) return;
9880
- await spawnChild(sessionId, worktreePath, wasCanceled);
10224
+ await spawnChild(sessionId, directory2, wasCanceled);
9881
10225
  };
9882
10226
  const start = (sessionId, name) => {
9883
10227
  const epoch = currentStartEpoch(sessionId);
@@ -9895,25 +10239,21 @@ var createSessionSupervisor = (deps) => {
9895
10239
  cancelStart(sessionId);
9896
10240
  return enqueue(sessionId, async () => {
9897
10241
  const entry = children.get(sessionId);
9898
- const session2 = await client.sessions.get(sessionId);
9899
- if (session2.worktreePath) await stopRecord(sessionId, session2.worktreePath);
10242
+ await stopRecord(sessionId);
9900
10243
  if (entry && children.get(sessionId) === entry) children.delete(sessionId);
9901
10244
  });
9902
10245
  };
9903
- const remove = (sessionId, worktreePath) => {
10246
+ const remove = (sessionId) => {
9904
10247
  cancelStart(sessionId);
9905
10248
  closeTerminal(sessionId);
9906
10249
  return enqueue(sessionId, async () => {
9907
10250
  const entry = children.get(sessionId);
9908
- const path = entry?.worktreePath ?? worktreePath;
9909
- if (path) {
9910
- await stopRecord(sessionId, path);
9911
- removeWorktree(repo, path);
9912
- }
10251
+ await stopRecord(sessionId);
10252
+ await rm2(sessionRuntimeDir(cfg, sessionId), { recursive: true, force: true });
9913
10253
  if (children.get(sessionId) === entry) children.delete(sessionId);
9914
10254
  });
9915
10255
  };
9916
- const title = async (sessionId, agentSessionId, worktreePath) => {
10256
+ const title = async (sessionId, agentSessionId, cwd2) => {
9917
10257
  const session2 = await client.sessions.get(sessionId);
9918
10258
  const events = await client.sessions.listEvents(sessionId);
9919
10259
  const exchange = parseFirstExchangeFromEvents(events) ?? (session2.agentKind === "claude-code" ? readFirstExchange(agentSessionId) : null);
@@ -9922,7 +10262,7 @@ var createSessionSupervisor = (deps) => {
9922
10262
  userText: exchange.userText,
9923
10263
  assistantText: exchange.assistantText
9924
10264
  }) : await generateTitle({
9925
- worktreePath,
10265
+ cwd: cwd2,
9926
10266
  userText: exchange.userText,
9927
10267
  assistantText: exchange.assistantText,
9928
10268
  queryFn: query2
@@ -9942,10 +10282,10 @@ var createSessionSupervisor = (deps) => {
9942
10282
  const session2 = await client.sessions.get(candidate.id);
9943
10283
  let execution = await client.sessions.execution(session2.id);
9944
10284
  if (execution.attempt?.status === "stopping") {
9945
- if (!session2.worktreePath) throw new Error("unfinished execution without worktree");
10285
+ if (!session2.cwd) throw new Error("unfinished execution without cwd");
9946
10286
  if (children.has(session2.id) && Date.now() - (execution.attempt.stopRequestedAt ?? 0) < 1e4)
9947
10287
  return;
9948
- await stopRecord(session2.id, session2.worktreePath, execution.attempt.runnerToken);
10288
+ await stopRecord(session2.id, execution.attempt.runnerToken);
9949
10289
  children.delete(session2.id);
9950
10290
  await client.sessions.stopped(
9951
10291
  session2.id,
@@ -9955,12 +10295,12 @@ var createSessionSupervisor = (deps) => {
9955
10295
  execution = await client.sessions.execution(session2.id);
9956
10296
  }
9957
10297
  if (execution.paused) {
9958
- if (session2.worktreePath) await stopRecord(session2.id, session2.worktreePath);
10298
+ if (session2.cwd) await stopRecord(session2.id);
9959
10299
  return;
9960
10300
  }
9961
10301
  if (execution.attempt?.status === "running") {
9962
- if (!children.has(session2.id) && session2.worktreePath)
9963
- await stopRecord(session2.id, session2.worktreePath, execution.attempt.runnerToken);
10302
+ if (!children.has(session2.id) && session2.cwd)
10303
+ await stopRecord(session2.id, execution.attempt.runnerToken);
9964
10304
  return;
9965
10305
  }
9966
10306
  if (execution.pendingCount || execution.turn?.status === "recovering")
@@ -9971,7 +10311,7 @@ var createSessionSupervisor = (deps) => {
9971
10311
  for (const [id, entry] of children) {
9972
10312
  if (!live.has(id))
9973
10313
  await enqueue(id, async () => {
9974
- await stopRecord(id, entry.worktreePath);
10314
+ await stopRecord(id);
9975
10315
  if (children.get(id) === entry) children.delete(id);
9976
10316
  });
9977
10317
  }
@@ -9991,7 +10331,7 @@ var createSessionSupervisor = (deps) => {
9991
10331
  };
9992
10332
 
9993
10333
  // src/worker/terminal-manager.ts
9994
- import { existsSync as existsSync9 } from "node:fs";
10334
+ import { statSync as statSync2 } from "node:fs";
9995
10335
  import { spawn as spawn4 } from "node-pty";
9996
10336
 
9997
10337
  // ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
@@ -10007,8 +10347,8 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
10007
10347
  // src/worker/pty.ts
10008
10348
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
10009
10349
  import { createRequire as createModuleRequire } from "node:module";
10010
- import { homedir as homedir3 } from "node:os";
10011
- import { join as join14 } from "node:path";
10350
+ import { homedir as homedir4 } from "node:os";
10351
+ import { join as join18 } from "node:path";
10012
10352
  var MAX_TERMINALS = 10;
10013
10353
  var claudePtyCommand = (agentSessionId, hasJsonl, env = process.env) => ({
10014
10354
  file: env.BATON_CLAUDE_BIN?.trim() || "claude",
@@ -10024,10 +10364,10 @@ var codexPtyCommand = (agentSessionId, env = process.env) => {
10024
10364
  };
10025
10365
  };
10026
10366
  var hasSessionJsonl = (agentSessionId) => {
10027
- const root = join14(homedir3(), ".claude", "projects");
10367
+ const root = join18(homedir4(), ".claude", "projects");
10028
10368
  if (!existsSync8(root)) return false;
10029
10369
  for (const dir of readdirSync2(root))
10030
- if (existsSync8(join14(root, dir, `${agentSessionId}.jsonl`))) return true;
10370
+ if (existsSync8(join18(root, dir, `${agentSessionId}.jsonl`))) return true;
10031
10371
  return false;
10032
10372
  };
10033
10373
  var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
@@ -10035,27 +10375,21 @@ var isPositiveDim = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
10035
10375
 
10036
10376
  // src/worker/terminal-manager.ts
10037
10377
  var createTerminalManager = (deps) => {
10038
- const { cfg, log, hasChild, prepareWorktree } = deps;
10378
+ const { cfg, log, hasChild } = deps;
10039
10379
  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;
10380
+ const open2 = (sessionId, agentSessionId, cwd) => {
10381
+ 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
10382
  if (why) {
10043
10383
  log(`terminal #${sessionId}: ${why} \u2014 refusing`);
10044
10384
  return;
10045
10385
  }
10046
- try {
10047
- prepareWorktree(worktreePath);
10048
- } catch (error) {
10049
- log(`terminal #${sessionId}: worktree preparation failed: ${String(error)}`);
10050
- return;
10051
- }
10052
10386
  const command = cfg.agentKind === "codex" ? codexPtyCommand(agentSessionId) : claudePtyCommand(agentSessionId, hasSessionJsonl(agentSessionId));
10053
10387
  const term = spawn4(command.file, command.args, {
10054
10388
  name: "xterm-color",
10055
10389
  cols: 80,
10056
10390
  rows: 24,
10057
- cwd: worktreePath,
10058
- env: process.env
10391
+ cwd,
10392
+ env: workerEnv(cfg)
10059
10393
  });
10060
10394
  const ws = new import_websocket.default(serverTerminalWsUrl(cfg.server, sessionId), {
10061
10395
  headers: { authorization: `Bearer ${cfg.apiToken}` }
@@ -10101,7 +10435,7 @@ var createTerminalManager = (deps) => {
10101
10435
  }
10102
10436
  };
10103
10437
  return {
10104
- open,
10438
+ open: open2,
10105
10439
  close,
10106
10440
  has: (sessionId) => terminals.has(sessionId),
10107
10441
  killAll: () => {
@@ -10122,12 +10456,14 @@ var createTerminalManager = (deps) => {
10122
10456
  // src/worker/daemon.ts
10123
10457
  var ES_OPEN = 1;
10124
10458
  var runWorkerDaemon = async (cfg, client, signal) => {
10125
- const repo = process.cwd();
10459
+ const cwd = process.cwd();
10460
+ ensureExcluded(cwd, PROJECT_CONFIG_NAME);
10461
+ syncBundledArtifactSkill(cwd);
10126
10462
  const log = (m) => console.log(`[worker #${cfg.workerId} ${cfg.name}] ${m}`);
10127
10463
  const supervisor = createSessionSupervisor({
10128
10464
  client,
10129
10465
  cfg,
10130
- repo,
10466
+ cwd,
10131
10467
  log,
10132
10468
  hasTerminal: (id) => terminals.has(id),
10133
10469
  closeTerminal: (id) => terminals.close(id)
@@ -10135,11 +10471,9 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10135
10471
  const terminals = createTerminalManager({
10136
10472
  cfg,
10137
10473
  log,
10138
- hasChild: (id) => supervisor.has(id),
10139
- prepareWorktree: (worktreePath) => syncBundledArtifactSkill(repo, worktreePath)
10474
+ hasChild: (id) => supervisor.has(id)
10140
10475
  });
10141
10476
  const services = createServiceSupervisor({
10142
- getSession: (sessionId) => client.sessions.get(sessionId),
10143
10477
  sendReport: (report) => client.services.report(report),
10144
10478
  cfg,
10145
10479
  log
@@ -10184,15 +10518,14 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10184
10518
  else if (cmd.cmd === "session.reconcile")
10185
10519
  void supervisor.reconcile(cmd.sessionId).catch((error) => log(`reconcile failed: ${String(error)}`));
10186
10520
  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)}`));
10521
+ void supervisor.remove(cmd.sessionId).catch((err) => log(`session cleanup failed: ${String(err)}`));
10188
10522
  else if (cmd.cmd === "session.title")
10189
- void supervisor.title(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath).catch((err) => log(`title failed: ${String(err)}`));
10523
+ void supervisor.title(cmd.sessionId, cmd.agentSessionId, cmd.cwd).catch((err) => log(`title failed: ${String(err)}`));
10190
10524
  else if (cmd.cmd === "session.terminal") {
10191
- if (cmd.action === "open")
10192
- terminals.open(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath);
10525
+ if (cmd.action === "open") terminals.open(cmd.sessionId, cmd.agentSessionId, cmd.cwd);
10193
10526
  else terminals.close(cmd.sessionId);
10194
10527
  } 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)}`));
10528
+ void services.run(cmd.requestId, cmd.cwd, cmd.name, cmd.argv, cmd.publicUrl, cmd.note).catch((err) => log(`service run failed: ${String(err)}`));
10196
10529
  else if (cmd.cmd === "service.stop")
10197
10530
  void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
10198
10531
  } catch (error) {
@@ -10200,12 +10533,12 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10200
10533
  }
10201
10534
  };
10202
10535
  es.onerror = () => log("command stream error (eventsource will retry)");
10203
- log(`listening for commands (server ${cfg.server}, repo ${repo})`);
10536
+ log(`listening for commands (server ${cfg.server}, cwd ${cwd})`);
10204
10537
  liveness.start();
10205
- await new Promise((resolve3) => {
10206
- stop = resolve3;
10207
- if (signal.aborted) return resolve3();
10208
- signal.addEventListener("abort", () => resolve3(), { once: true });
10538
+ await new Promise((resolve4) => {
10539
+ stop = resolve4;
10540
+ if (signal.aborted) return resolve4();
10541
+ signal.addEventListener("abort", () => resolve4(), { once: true });
10209
10542
  });
10210
10543
  es.close();
10211
10544
  liveness.stop();
@@ -10217,22 +10550,87 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10217
10550
 
10218
10551
  // src/worker/machine-id.ts
10219
10552
  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");
10553
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
10554
+ import { homedir as homedir5 } from "node:os";
10555
+ import { dirname as dirname6, join as join19 } from "node:path";
10556
+ var machineIdPath = (env = process.env) => join19(env.XDG_DATA_HOME ?? join19(env.HOME ?? homedir5(), ".local/share"), "baton", "machine-id");
10224
10557
  var readOrCreateMachineId = (path = machineIdPath()) => {
10225
- if (existsSync10(path)) {
10558
+ if (existsSync9(path)) {
10226
10559
  const s = readFileSync6(path, "utf8").trim();
10227
10560
  if (s) return s;
10228
10561
  }
10229
10562
  const id = randomUUID3();
10230
- mkdirSync5(dirname5(path), { recursive: true });
10563
+ mkdirSync5(dirname6(path), { recursive: true });
10231
10564
  writeFileSync5(path, `${id}
10232
10565
  `, "utf8");
10233
10566
  return id;
10234
10567
  };
10235
10568
 
10569
+ // src/commands/worker/provision.ts
10570
+ var projectArg = {
10571
+ project: { type: "string", description: "project id (overrides .baton.json)" }
10572
+ };
10573
+ var positiveId = (raw) => {
10574
+ const id = Number(raw);
10575
+ if (!Number.isSafeInteger(id) || id <= 0) throw new Error("positive id required");
10576
+ return id;
10577
+ };
10578
+ var workerTemplatesCommand = defineCommand({
10579
+ meta: { name: "templates", description: "list worker creation templates for a project" },
10580
+ args: { ...projectArg, ...common },
10581
+ run: async ({ args }) => {
10582
+ const list = await clientFor(args).workerCreations.templates(positiveId(resolveProjectId(args)));
10583
+ console.log(renderList(list, fmtWorkerTemplate, Boolean(args.json)));
10584
+ }
10585
+ });
10586
+ var workerCreateCommand = defineCommand({
10587
+ meta: { name: "create", description: "request a worker from the mini template (asynchronous)" },
10588
+ args: {
10589
+ ...projectArg,
10590
+ ...common,
10591
+ template: { type: "string", required: true, description: "mini-local" },
10592
+ name: { type: "string", required: true, description: "worker name" },
10593
+ agentKind: { type: "string", default: "claude-code", description: "claude-code or codex" }
10594
+ },
10595
+ run: async ({ args }) => {
10596
+ if (args.template !== "mini-local") throw new Error("template must be mini-local");
10597
+ if (args.agentKind !== "claude-code" && args.agentKind !== "codex")
10598
+ throw new Error("invalid agentKind");
10599
+ const name = args.name.trim();
10600
+ if (!name) throw new Error("worker name required");
10601
+ const creation = await clientFor(args).workerCreations.create(
10602
+ positiveId(resolveProjectId(args)),
10603
+ {
10604
+ templateId: "mini-local",
10605
+ name,
10606
+ agentKind: args.agentKind
10607
+ }
10608
+ );
10609
+ console.log(renderOne(creation, fmtWorkerCreation, Boolean(args.json)));
10610
+ }
10611
+ });
10612
+ var workerCreationCommand = defineCommand({
10613
+ meta: { name: "creation", description: "inspect worker creation records" },
10614
+ subCommands: {
10615
+ ls: defineCommand({
10616
+ args: { ...projectArg, ...common },
10617
+ run: async ({ args }) => {
10618
+ const list = await clientFor(args).workerCreations.listByProject(
10619
+ positiveId(resolveProjectId(args))
10620
+ );
10621
+ console.log(renderList(list, fmtWorkerCreation, Boolean(args.json)));
10622
+ }
10623
+ }),
10624
+ get: defineCommand({
10625
+ args: { ...common, id: { type: "positional", required: true } },
10626
+ run: async ({ args }) => {
10627
+ const creation = await clientFor(args).workerCreations.get(positiveId(args.id));
10628
+ console.log(renderOne(creation, fmtWorkerCreation, Boolean(args.json)));
10629
+ }
10630
+ })
10631
+ }
10632
+ });
10633
+
10236
10634
  // src/commands/worker/index.ts
10237
10635
  var resolveWorker = async (client, projectId, handle) => {
10238
10636
  const asId = parseWorkerHandle(handle);
@@ -10249,11 +10647,6 @@ var parseAgentKind = (value) => {
10249
10647
  if (raw === "claude-code" || raw === "codex") return raw;
10250
10648
  throw new Error(`invalid agent kind "${raw}" (expected claude-code or codex)`);
10251
10649
  };
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);
10256
- };
10257
10650
  var registerWorker = async (client, input, cfgPath = projectConfigPath()) => {
10258
10651
  const out = await client.workers.register({
10259
10652
  projectId: input.projectId,
@@ -10267,7 +10660,6 @@ var registerWorker = async (client, input, cfgPath = projectConfigPath()) => {
10267
10660
  ...existing,
10268
10661
  server: input.server,
10269
10662
  project: input.projectId,
10270
- baseBranch: input.baseBranch,
10271
10663
  worker: {
10272
10664
  id: out.worker.id,
10273
10665
  name: out.worker.name,
@@ -10281,6 +10673,9 @@ var registerWorker = async (client, input, cfgPath = projectConfigPath()) => {
10281
10673
  var worker = defineCommand({
10282
10674
  meta: { name: "worker", description: "register / inspect / close workers for a project" },
10283
10675
  subCommands: {
10676
+ create: workerCreateCommand,
10677
+ creation: workerCreationCommand,
10678
+ templates: workerTemplatesCommand,
10284
10679
  register: defineCommand({
10285
10680
  meta: {
10286
10681
  name: "register",
@@ -10297,10 +10692,6 @@ var worker = defineCommand({
10297
10692
  type: "string",
10298
10693
  description: "agent engine for new sessions: claude-code or codex"
10299
10694
  },
10300
- baseBranch: {
10301
- type: "string",
10302
- description: "branch new session worktrees start from (default: current branch, then main)"
10303
- },
10304
10695
  ...common
10305
10696
  },
10306
10697
  run: async ({ args }) => {
@@ -10315,14 +10706,9 @@ var worker = defineCommand({
10315
10706
  const fileToken = savedConfig?.worker?.apiToken;
10316
10707
  const auth = resolveAuth(process.env, fileToken);
10317
10708
  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();
10709
+ const hostname2 = osHostname();
10324
10710
  const machineId = readOrCreateMachineId();
10325
- const name = args.name ?? hostname;
10711
+ const name = args.name ?? hostname2;
10326
10712
  const agentKind = parseAgentKind(args.agentKind ?? process.env.BATON_AGENT_KIND);
10327
10713
  const { out, configPath } = await registerWorker(
10328
10714
  c,
@@ -10330,20 +10716,18 @@ var worker = defineCommand({
10330
10716
  projectId: resolveProjectId(args),
10331
10717
  name,
10332
10718
  server,
10333
- hostname,
10719
+ hostname: hostname2,
10334
10720
  machineId,
10335
- agentKind,
10336
- baseBranch
10721
+ agentKind
10337
10722
  },
10338
10723
  cfgPath
10339
10724
  );
10340
10725
  if (args.json) {
10341
- console.log(toJson({ ...out, baseBranch, configPath, machineIdPath: machineIdPath() }));
10726
+ console.log(toJson({ ...out, configPath, machineIdPath: machineIdPath() }));
10342
10727
  return;
10343
10728
  }
10344
10729
  console.log(`worker #${out.worker.id} (${out.worker.name}) \u2014 ${out.outcome}`);
10345
10730
  console.log(` agentKind: ${out.worker.agentKind}`);
10346
- console.log(` baseBranch: ${baseBranch}`);
10347
10731
  console.log(` hostname: ${out.worker.hostname}`);
10348
10732
  console.log(` machineId: ${out.worker.machineId}`);
10349
10733
  console.log(` machineId file: ${machineIdPath()}`);
@@ -10452,14 +10836,12 @@ var worker = defineCommand({
10452
10836
  console.log("(no worker registered for this project \u2014 run `baton worker register`)");
10453
10837
  return;
10454
10838
  }
10455
- const baseBranch = resolveBaseBranch({ saved: cfg.baseBranch });
10456
10839
  if (args.json) {
10457
10840
  console.log(
10458
10841
  toJson({
10459
10842
  ...worker2,
10460
10843
  server: cfg.server,
10461
- projectId: cfg.project,
10462
- baseBranch
10844
+ projectId: cfg.project
10463
10845
  })
10464
10846
  );
10465
10847
  return;
@@ -10467,7 +10849,6 @@ var worker = defineCommand({
10467
10849
  console.log(`worker #${worker2.id} (${worker2.name})`);
10468
10850
  console.log(` machineId: ${worker2.machineId}`);
10469
10851
  console.log(` agentKind: ${worker2.agentKind ?? "claude-code"}`);
10470
- console.log(` baseBranch: ${baseBranch}`);
10471
10852
  console.log(` server: ${cfg?.server}`);
10472
10853
  }
10473
10854
  })
@@ -10541,6 +10922,7 @@ var main = defineCommand({
10541
10922
  subCommands: {
10542
10923
  init,
10543
10924
  worker,
10925
+ provisioner,
10544
10926
  session,
10545
10927
  service,
10546
10928
  artifact,