@aiden-ade/sandbox-agent 0.1.50 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +2714 -606
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -36,7 +36,7 @@ var require_XMLHttpRequest = __commonJS({
36
36
  "use strict";
37
37
  var fs = require("fs");
38
38
  var Url = require("url");
39
- var spawn5 = require("child_process").spawn;
39
+ var spawn7 = require("child_process").spawn;
40
40
  module2.exports = XMLHttpRequest3;
41
41
  XMLHttpRequest3.XMLHttpRequest = XMLHttpRequest3;
42
42
  function XMLHttpRequest3(opts) {
@@ -332,7 +332,7 @@ var require_XMLHttpRequest = __commonJS({
332
332
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
333
333
  fs.writeFileSync(syncFile, "", "utf8");
334
334
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
335
- var syncProc = spawn5(process.argv[0], ["-e", execString]);
335
+ var syncProc = spawn7(process.argv[0], ["-e", execString]);
336
336
  var statusText;
337
337
  while (fs.existsSync(syncFile)) {
338
338
  }
@@ -3247,7 +3247,7 @@ var require_extension = __commonJS({
3247
3247
  "../../node_modules/engine.io-client/node_modules/ws/lib/extension.js"(exports2, module2) {
3248
3248
  "use strict";
3249
3249
  var { tokenChars } = require_validation();
3250
- function push(dest, name, elem) {
3250
+ function push2(dest, name, elem) {
3251
3251
  if (dest[name] === void 0) dest[name] = [elem];
3252
3252
  else dest[name].push(elem);
3253
3253
  }
@@ -3277,7 +3277,7 @@ var require_extension = __commonJS({
3277
3277
  if (end === -1) end = i;
3278
3278
  const name = header.slice(start, end);
3279
3279
  if (code === 44) {
3280
- push(offers, name, params);
3280
+ push2(offers, name, params);
3281
3281
  params = /* @__PURE__ */ Object.create(null);
3282
3282
  } else {
3283
3283
  extensionName = name;
@@ -3296,9 +3296,9 @@ var require_extension = __commonJS({
3296
3296
  throw new SyntaxError(`Unexpected character at index ${i}`);
3297
3297
  }
3298
3298
  if (end === -1) end = i;
3299
- push(params, header.slice(start, end), true);
3299
+ push2(params, header.slice(start, end), true);
3300
3300
  if (code === 44) {
3301
- push(offers, extensionName, params);
3301
+ push2(offers, extensionName, params);
3302
3302
  params = /* @__PURE__ */ Object.create(null);
3303
3303
  extensionName = void 0;
3304
3304
  }
@@ -3344,9 +3344,9 @@ var require_extension = __commonJS({
3344
3344
  value2 = value2.replace(/\\/g, "");
3345
3345
  mustUnescape = false;
3346
3346
  }
3347
- push(params, paramName, value2);
3347
+ push2(params, paramName, value2);
3348
3348
  if (code === 44) {
3349
- push(offers, extensionName, params);
3349
+ push2(offers, extensionName, params);
3350
3350
  params = /* @__PURE__ */ Object.create(null);
3351
3351
  extensionName = void 0;
3352
3352
  }
@@ -3363,16 +3363,16 @@ var require_extension = __commonJS({
3363
3363
  if (end === -1) end = i;
3364
3364
  const token = header.slice(start, end);
3365
3365
  if (extensionName === void 0) {
3366
- push(offers, token, params);
3366
+ push2(offers, token, params);
3367
3367
  } else {
3368
3368
  if (paramName === void 0) {
3369
- push(params, token, true);
3369
+ push2(params, token, true);
3370
3370
  } else if (mustUnescape) {
3371
- push(params, paramName, token.replace(/\\/g, ""));
3371
+ push2(params, paramName, token.replace(/\\/g, ""));
3372
3372
  } else {
3373
- push(params, paramName, token);
3373
+ push2(params, paramName, token);
3374
3374
  }
3375
- push(offers, extensionName, params);
3375
+ push2(offers, extensionName, params);
3376
3376
  }
3377
3377
  return offers;
3378
3378
  }
@@ -3404,7 +3404,7 @@ var require_websocket = __commonJS({
3404
3404
  var http2 = require("http");
3405
3405
  var net = require("net");
3406
3406
  var tls = require("tls");
3407
- var { randomBytes: randomBytes5, createHash: createHash7 } = require("crypto");
3407
+ var { randomBytes: randomBytes5, createHash: createHash8 } = require("crypto");
3408
3408
  var { Duplex, Readable } = require("stream");
3409
3409
  var { URL: URL2 } = require("url");
3410
3410
  var PerMessageDeflate = require_permessage_deflate();
@@ -4061,7 +4061,7 @@ var require_websocket = __commonJS({
4061
4061
  abortHandshake(websocket, socket, "Invalid Upgrade header");
4062
4062
  return;
4063
4063
  }
4064
- const digest = createHash7("sha1").update(key + GUID).digest("base64");
4064
+ const digest = createHash8("sha1").update(key + GUID).digest("base64");
4065
4065
  if (res.headers["sec-websocket-accept"] !== digest) {
4066
4066
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
4067
4067
  return;
@@ -4428,7 +4428,7 @@ var require_websocket_server = __commonJS({
4428
4428
  var EventEmitter = require("events");
4429
4429
  var http2 = require("http");
4430
4430
  var { Duplex } = require("stream");
4431
- var { createHash: createHash7 } = require("crypto");
4431
+ var { createHash: createHash8 } = require("crypto");
4432
4432
  var extension = require_extension();
4433
4433
  var PerMessageDeflate = require_permessage_deflate();
4434
4434
  var subprotocol = require_subprotocol();
@@ -4725,7 +4725,7 @@ var require_websocket_server = __commonJS({
4725
4725
  );
4726
4726
  }
4727
4727
  if (this._state > RUNNING) return abortHandshake(socket, 503);
4728
- const digest = createHash7("sha1").update(key + GUID).digest("base64");
4728
+ const digest = createHash8("sha1").update(key + GUID).digest("base64");
4729
4729
  const headers = [
4730
4730
  "HTTP/1.1 101 Switching Protocols",
4731
4731
  "Upgrade: websocket",
@@ -4811,7 +4811,7 @@ var require_websocket_server = __commonJS({
4811
4811
  });
4812
4812
 
4813
4813
  // src/index.ts
4814
- var import_node_child_process8 = require("child_process");
4814
+ var import_node_child_process10 = require("child_process");
4815
4815
 
4816
4816
  // src/cli-executable.ts
4817
4817
  var import_node_child_process = require("child_process");
@@ -4956,7 +4956,7 @@ function resolveViaWhere(command, env) {
4956
4956
  return null;
4957
4957
  }
4958
4958
  function runCliProbeAsync(executable, env, args = ["--version"], timeoutMs = 3e3, spawnOptions) {
4959
- return new Promise((resolve6) => {
4959
+ return new Promise((resolve7) => {
4960
4960
  const isWin = (0, import_node_os.platform)() === "win32";
4961
4961
  let command = executable;
4962
4962
  let commandArgs = args;
@@ -4976,7 +4976,7 @@ function runCliProbeAsync(executable, env, args = ["--version"], timeoutMs = 3e3
4976
4976
  stdio: closeStdin ? ["pipe", "pipe", "pipe"] : void 0
4977
4977
  });
4978
4978
  } catch (error) {
4979
- resolve6({ status: null, stdout: "", stderr: "", error });
4979
+ resolve7({ status: null, stdout: "", stderr: "", error });
4980
4980
  return;
4981
4981
  }
4982
4982
  if (closeStdin) {
@@ -4990,10 +4990,10 @@ function runCliProbeAsync(executable, env, args = ["--version"], timeoutMs = 3e3
4990
4990
  child.stderr?.setEncoding("utf8").on("data", (chunk) => {
4991
4991
  stderr += chunk;
4992
4992
  });
4993
- child.once("error", (error) => resolve6({ status: null, stdout, stderr, error }));
4993
+ child.once("error", (error) => resolve7({ status: null, stdout, stderr, error }));
4994
4994
  child.once(
4995
4995
  "close",
4996
- (status, signal) => resolve6({ status, stdout, stderr, signal: signal ?? null })
4996
+ (status, signal) => resolve7({ status, stdout, stderr, signal: signal ?? null })
4997
4997
  );
4998
4998
  });
4999
4999
  }
@@ -5204,12 +5204,12 @@ function resolveCommand(args, env) {
5204
5204
  }
5205
5205
 
5206
5206
  // src/daemon.ts
5207
- var import_node_child_process6 = require("child_process");
5208
- var import_node_crypto3 = require("crypto");
5209
- var import_node_fs13 = require("fs");
5207
+ var import_node_child_process8 = require("child_process");
5208
+ var import_node_crypto5 = require("crypto");
5209
+ var import_node_fs14 = require("fs");
5210
5210
  var import_node_http = __toESM(require("http"), 1);
5211
- var import_node_os8 = require("os");
5212
- var import_node_path11 = require("path");
5211
+ var import_node_os9 = require("os");
5212
+ var import_node_path12 = require("path");
5213
5213
 
5214
5214
  // ../agent-core/dist/index.js
5215
5215
  var import_fs = require("fs");
@@ -5362,7 +5362,7 @@ Every \`start_task_session\` message must include all four of these:
5362
5362
  3. Write requirements as a team doc (\`create_document\`) before any delegation. Create an artifact only when explicitly requested.
5363
5363
  4. Break work into subtasks ordered by dependency. Delegate one at a time unless subtasks are truly independent.
5364
5364
  5. Track results and update task status as work completes.`;
5365
- var GENERAL_PROMPT = `You are Alan, the General Agent \u2014 a flexible general-purpose agent profile. When identifying yourself, say "I'm Alan, the General Agent."`;
5365
+ var GENERAL_PROMPT = "";
5366
5366
  var SETUP_PROMPT = `## You are the Setup Agent
5367
5367
 
5368
5368
  You are the **Setup Agent** for this cloud environment. When identifying yourself, say "I'm the Setup Agent (an Alan agent profile) for this environment."
@@ -10311,9 +10311,7 @@ Team
10311
10311
 
10312
10312
  ## Task Logging
10313
10313
 
10314
- For non-trivial tasks, log high-signal events as they happen: decisions, changed assumptions, validation results, known-good IDs, blockers, risks, and direction changes. Keep one current task-state summary updated with current state, known-good IDs, open risks, and next action. Do not log routine commands or file reads. Before stopping, ensure the timeline and current state are enough for a human to understand progress and for another agent to resume. If a prior decision is stale, write a newer event that supersedes it.
10315
-
10316
- The specific work you do \u2014 implementation, planning, review, coordination, verification \u2014 is determined by your agent role, described next.`;
10314
+ For non-trivial tasks, log high-signal events as they happen: decisions, changed assumptions, validation results, known-good IDs, blockers, risks, and direction changes. Keep one current task-state summary updated with current state, known-good IDs, open risks, and next action. Do not log routine commands or file reads. Before stopping, ensure the timeline and current state are enough for a human to understand progress and for another agent to resume. If a prior decision is stale, write a newer event that supersedes it.`;
10317
10315
  function buildAlanTeamScopePrompt(teamId) {
10318
10316
  return [
10319
10317
  `You are operating within team \`${teamId}\`.`,
@@ -10338,6 +10336,7 @@ Examples: "where is X configured", "which file handles Y", "how does Z work", "f
10338
10336
  - Do NOT start with \`grep\`, \`rg\`, \`find\`, or broad Bash filesystem search for codebase discovery or implementation-answer questions when \`search_code_context\` is available.
10339
10337
  - Do NOT repeat or rephrase index searches after the first result identifies useful paths. Continue locally instead.
10340
10338
  - Do NOT claim current behavior from an index excerpt without reading the cited local file.
10339
+ - When several repositories are available and repoId is omitted, code context routes automatically using the task workspace, repository-name hints, and indexed evidence. Pass repoId only when the user explicitly names a repository or an existing trusted task selection supplies it. Never invent repo IDs or call list_teams.
10341
10340
 
10342
10341
  ### Local follow-ups
10343
10342
  - Import/caller structure: use local \`code_graph_query\` or narrow symbol search after reading the cited files.
@@ -19128,8 +19127,8 @@ async function withTimeout(promise, timeoutMs, fallback) {
19128
19127
  try {
19129
19128
  return await Promise.race([
19130
19129
  promise,
19131
- new Promise((resolve6) => {
19132
- timer = setTimeout(() => resolve6(fallback), timeoutMs);
19130
+ new Promise((resolve7) => {
19131
+ timer = setTimeout(() => resolve7(fallback), timeoutMs);
19133
19132
  })
19134
19133
  ]);
19135
19134
  } finally {
@@ -19142,7 +19141,7 @@ async function collectCodexLimits(env) {
19142
19141
  const codexCommand = resolveExecutable("codex", runtimeEnv, "ALAN_CODEX_PATH");
19143
19142
  if (!codexCommand)
19144
19143
  return [];
19145
- return withTimeout(new Promise((resolve6) => {
19144
+ return withTimeout(new Promise((resolve7) => {
19146
19145
  const child = (0, import_node_child_process2.spawn)(codexCommand, ["-s", "read-only", "-a", "untrusted", "app-server"], {
19147
19146
  env: runtimeEnv,
19148
19147
  stdio: ["pipe", "pipe", "ignore"]
@@ -19154,7 +19153,7 @@ async function collectCodexLimits(env) {
19154
19153
  pending.clear();
19155
19154
  if (!child.killed)
19156
19155
  child.kill();
19157
- resolve6(windows);
19156
+ resolve7(windows);
19158
19157
  };
19159
19158
  const request = (method, params = {}) => {
19160
19159
  const id = nextId++;
@@ -21987,9 +21986,9 @@ var Socket2 = class extends Emitter {
21987
21986
  * @return a Promise that will be fulfilled when the server acknowledges the event
21988
21987
  */
21989
21988
  emitWithAck(ev, ...args) {
21990
- return new Promise((resolve6, reject) => {
21989
+ return new Promise((resolve7, reject) => {
21991
21990
  const fn = (arg1, arg2) => {
21992
- return arg1 ? reject(arg1) : resolve6(arg2);
21991
+ return arg1 ? reject(arg1) : resolve7(arg2);
21993
21992
  };
21994
21993
  fn.withError = true;
21995
21994
  args.push(fn);
@@ -22987,231 +22986,1365 @@ Object.assign(lookup, {
22987
22986
  connect: lookup
22988
22987
  });
22989
22988
 
22990
- // src/cli-ensure-install.ts
22989
+ // src/binding-switch-git.ts
22991
22990
  var import_node_child_process3 = require("child_process");
22991
+ var import_node_crypto = require("crypto");
22992
+ var import_node_fs4 = require("fs");
22992
22993
  var import_node_os4 = require("os");
22993
- var CLI_RUNTIME_VERSIONS_ENV = "ALAN_CLI_RUNTIME_VERSIONS";
22994
- var INSTALLABLE_PROVIDER_PACKAGES = {
22995
- claude_cli: { npmPackage: "@anthropic-ai/claude-code", command: "claude", label: "Claude Code" },
22996
- codex_app_server: { npmPackage: "@openai/codex", command: "codex", label: "Codex" },
22997
- copilot_cli: { npmPackage: "@github/copilot", command: "copilot", label: "Copilot" },
22998
- opencode_cli: { npmPackage: "opencode-ai", command: "opencode", label: "opencode" }
22999
- };
23000
- var INSTALL_TIMEOUT_MS = 12e4;
23001
- var installOutcomes = /* @__PURE__ */ new Map();
23002
- function canInstallProviderCli(backendKind) {
23003
- return Boolean(backendKind && INSTALLABLE_PROVIDER_PACKAGES[backendKind]);
22994
+ var import_node_path4 = require("path");
22995
+ var import_node_url = require("url");
22996
+ var BINDING_SWITCH_GIT_METHODS = [
22997
+ "git.status",
22998
+ "git.checkpoint",
22999
+ "git.push",
23000
+ "git.fetch",
23001
+ "git.prepare_checkout",
23002
+ "git.prepare_worktree",
23003
+ "git.verify_head"
23004
+ ];
23005
+ function isBindingSwitchGitMethod(method) {
23006
+ return BINDING_SWITCH_GIT_METHODS.includes(method);
23004
23007
  }
23005
- function pinnedVersion(npmPackage) {
23006
- const raw = process.env[CLI_RUNTIME_VERSIONS_ENV];
23007
- if (!raw) return "latest";
23008
- try {
23009
- const parsed = JSON.parse(raw);
23010
- const version = parsed[npmPackage];
23011
- if (typeof version === "string" && version.trim()) return version.trim();
23012
- } catch {
23008
+ var BindingSwitchGitError = class extends Error {
23009
+ constructor(code, message, details) {
23010
+ super(message);
23011
+ this.code = code;
23012
+ this.details = details;
23013
+ this.name = "BindingSwitchGitError";
23013
23014
  }
23014
- return "latest";
23015
+ };
23016
+ var LOCAL_GIT_TIMEOUT_MS = 1e4;
23017
+ var NETWORK_GIT_TIMEOUT_MS = 3e4;
23018
+ var MAX_GIT_OUTPUT_BYTES = 10 * 1024 * 1024;
23019
+ function runGitResult(cwd, args, options = {}) {
23020
+ const result = (0, import_node_child_process3.spawnSync)("git", args, {
23021
+ cwd,
23022
+ env: { ...getDaemonCliEnvironment(), ...options.env },
23023
+ encoding: "utf8",
23024
+ input: options.input,
23025
+ maxBuffer: MAX_GIT_OUTPUT_BYTES,
23026
+ timeout: LOCAL_GIT_TIMEOUT_MS
23027
+ });
23028
+ if (result.error) {
23029
+ return { status: 1, stdout: "", stderr: result.error.message };
23030
+ }
23031
+ return {
23032
+ status: result.status ?? 1,
23033
+ stdout: result.stdout ?? "",
23034
+ stderr: result.stderr ?? ""
23035
+ };
23015
23036
  }
23016
- function runNpmInstall(installSpec, env, timeoutMs) {
23017
- return new Promise((resolve6) => {
23018
- const isWin = (0, import_node_os4.platform)() === "win32";
23019
- let child;
23020
- try {
23021
- child = (0, import_node_child_process3.spawn)("npm", ["install", "-g", installSpec], {
23022
- env,
23023
- shell: isWin,
23024
- windowsHide: isWin ? true : void 0,
23025
- timeout: timeoutMs,
23026
- stdio: ["ignore", "pipe", "pipe"]
23027
- });
23028
- } catch (error) {
23029
- resolve6({ status: null, stdout: "", stderr: "", error });
23030
- return;
23037
+ function networkGitTimeoutMs() {
23038
+ if (process.env.NODE_ENV !== "test") return NETWORK_GIT_TIMEOUT_MS;
23039
+ const override = Number.parseInt(process.env.ALAN_TEST_GIT_NETWORK_TIMEOUT_MS ?? "", 10);
23040
+ return Number.isFinite(override) && override >= 10 ? Math.min(override, NETWORK_GIT_TIMEOUT_MS) : NETWORK_GIT_TIMEOUT_MS;
23041
+ }
23042
+ async function waitForCheckpointTestBarrier() {
23043
+ const barrierDir = process.env.NODE_ENV === "test" ? process.env.ALAN_TEST_GIT_CHECKPOINT_BARRIER_DIR : void 0;
23044
+ if (!barrierDir) return;
23045
+ (0, import_node_fs4.mkdirSync)(barrierDir, { recursive: true });
23046
+ const readyPath = (0, import_node_path4.join)(barrierDir, "ready");
23047
+ const releasePath = (0, import_node_path4.join)(barrierDir, "release");
23048
+ (0, import_node_fs4.writeFileSync)(readyPath, "ready\n");
23049
+ const deadline = Date.now() + 5e3;
23050
+ while (!(0, import_node_fs4.existsSync)(releasePath)) {
23051
+ if (Date.now() >= deadline) {
23052
+ throw new BindingSwitchGitError(
23053
+ "checkpoint_prepare_timeout",
23054
+ "Checkpoint preparation test barrier timed out"
23055
+ );
23056
+ }
23057
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
23058
+ }
23059
+ }
23060
+ async function waitForCheckpointFinalizeTestBarrier() {
23061
+ const barrierDir = process.env.NODE_ENV === "test" ? process.env.ALAN_TEST_GIT_CHECKPOINT_FINALIZE_BARRIER_DIR : void 0;
23062
+ if (!barrierDir) return;
23063
+ (0, import_node_fs4.mkdirSync)(barrierDir, { recursive: true });
23064
+ const readyPath = (0, import_node_path4.join)(barrierDir, "ready");
23065
+ const releasePath = (0, import_node_path4.join)(barrierDir, "release");
23066
+ (0, import_node_fs4.writeFileSync)(readyPath, "ready\n");
23067
+ const deadline = Date.now() + 5e3;
23068
+ while (!(0, import_node_fs4.existsSync)(releasePath)) {
23069
+ if (Date.now() >= deadline) {
23070
+ throw new BindingSwitchGitError(
23071
+ "checkpoint_finalize_timeout",
23072
+ "Checkpoint finalization test barrier timed out"
23073
+ );
23031
23074
  }
23075
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
23076
+ }
23077
+ }
23078
+ async function runNetworkGit(cwd, args, code) {
23079
+ return new Promise((resolvePromise, rejectPromise) => {
23080
+ const child = (0, import_node_child_process3.spawn)("git", args, {
23081
+ cwd,
23082
+ env: {
23083
+ ...getDaemonCliEnvironment(),
23084
+ GCM_INTERACTIVE: "Never",
23085
+ GIT_TERMINAL_PROMPT: "0",
23086
+ ...process.env.NODE_ENV === "test" && process.env.ALAN_TEST_GIT_SSH_COMMAND ? { GIT_SSH_COMMAND: process.env.ALAN_TEST_GIT_SSH_COMMAND } : {}
23087
+ },
23088
+ detached: process.platform !== "win32",
23089
+ stdio: ["ignore", "pipe", "pipe"]
23090
+ });
23032
23091
  let stdout = "";
23033
23092
  let stderr = "";
23034
- child.stdout?.setEncoding("utf8").on("data", (chunk) => {
23035
- stdout += chunk;
23093
+ let outputExceeded = false;
23094
+ let timedOut = false;
23095
+ let settled = false;
23096
+ const killTree = () => {
23097
+ if (child.pid && process.platform !== "win32") {
23098
+ try {
23099
+ process.kill(-child.pid, "SIGKILL");
23100
+ return;
23101
+ } catch {
23102
+ }
23103
+ }
23104
+ child.kill("SIGKILL");
23105
+ };
23106
+ const timer = setTimeout(() => {
23107
+ timedOut = true;
23108
+ killTree();
23109
+ }, networkGitTimeoutMs());
23110
+ timer.unref?.();
23111
+ const append = (current, chunk) => {
23112
+ if (Buffer.byteLength(current) + chunk.byteLength > MAX_GIT_OUTPUT_BYTES) {
23113
+ outputExceeded = true;
23114
+ killTree();
23115
+ return current;
23116
+ }
23117
+ return current + chunk.toString("utf8");
23118
+ };
23119
+ child.stdout.on("data", (chunk) => {
23120
+ stdout = append(stdout, chunk);
23036
23121
  });
23037
- child.stderr?.setEncoding("utf8").on("data", (chunk) => {
23038
- stderr += chunk;
23122
+ child.stderr.on("data", (chunk) => {
23123
+ stderr = append(stderr, chunk);
23124
+ });
23125
+ child.once("error", (error) => {
23126
+ if (settled) return;
23127
+ settled = true;
23128
+ clearTimeout(timer);
23129
+ rejectPromise(new BindingSwitchGitError(code, error.message));
23130
+ });
23131
+ child.once("close", (exitCode) => {
23132
+ if (settled) return;
23133
+ settled = true;
23134
+ clearTimeout(timer);
23135
+ if (timedOut) {
23136
+ rejectPromise(
23137
+ new BindingSwitchGitError(
23138
+ "git_network_timeout",
23139
+ "The Git network operation timed out and was cancelled"
23140
+ )
23141
+ );
23142
+ return;
23143
+ }
23144
+ if (outputExceeded) {
23145
+ rejectPromise(
23146
+ new BindingSwitchGitError(
23147
+ "git_output_limit_exceeded",
23148
+ "The Git network operation produced too much output and was cancelled"
23149
+ )
23150
+ );
23151
+ return;
23152
+ }
23153
+ if (exitCode !== 0) {
23154
+ rejectPromise(
23155
+ new BindingSwitchGitError(code, (stderr || stdout || "Git network command failed").trim())
23156
+ );
23157
+ return;
23158
+ }
23159
+ resolvePromise(stdout.trim());
23039
23160
  });
23040
- child.once("error", (error) => resolve6({ status: null, stdout, stderr, error }));
23041
- child.once(
23042
- "close",
23043
- (status, signal) => resolve6({
23044
- status,
23045
- stdout,
23046
- stderr,
23047
- // Spawn `timeout` kills by signal without an `error` event; surface it
23048
- // so a timed-out install is reported as a failure, not status null + ok.
23049
- ...signal ? { error: new Error(`npm install killed by ${signal}`) } : {}
23050
- })
23051
- );
23052
23161
  });
23053
23162
  }
23054
- async function attemptInstall(spec, options) {
23055
- const installSpec = `${spec.npmPackage}@${pinnedVersion(spec.npmPackage)}`;
23056
- const notify = options?.notify ?? ((message) => console.info(`[cli-ensure-install] ${message}`));
23057
- notify(`Installing ${spec.label} CLI (${installSpec})\u2026`);
23058
- const runner = options?.runner ?? runNpmInstall;
23059
- const result = await runner(installSpec, getDaemonCliEnvironment(), INSTALL_TIMEOUT_MS);
23060
- if (result.error || result.status !== 0) {
23061
- console.warn(`[cli-ensure-install] npm install -g ${installSpec} failed`, {
23062
- status: result.status,
23063
- error: result.error?.message,
23064
- output: `${result.stderr || result.stdout}`.trim().slice(-500)
23065
- });
23066
- notify(`Failed to install ${spec.label} CLI (${installSpec}).`);
23067
- return false;
23068
- }
23069
- invalidateResolvedCli(spec.command);
23070
- notify(`Installed ${spec.label} CLI (${installSpec}).`);
23071
- return true;
23163
+ function runGit(cwd, args, code = "git_operation_failed") {
23164
+ return runGitWithOptions(cwd, args, {}, code);
23072
23165
  }
23073
- function ensureProviderCliInstalled(backendKind, options) {
23074
- if (!backendKind) return Promise.resolve(false);
23075
- const spec = INSTALLABLE_PROVIDER_PACKAGES[backendKind];
23076
- if (!spec) return Promise.resolve(false);
23077
- const existing = installOutcomes.get(spec.npmPackage);
23078
- if (existing) return existing;
23079
- const attempt = attemptInstall(spec, options).catch((error) => {
23080
- console.warn(`[cli-ensure-install] install attempt for ${spec.npmPackage} threw`, error);
23081
- return false;
23082
- });
23083
- installOutcomes.set(spec.npmPackage, attempt);
23084
- return attempt;
23166
+ function runGitWithOptions(cwd, args, options, code = "git_operation_failed") {
23167
+ const result = runGitResult(cwd, args, options);
23168
+ if (result.status !== 0) {
23169
+ throw new BindingSwitchGitError(
23170
+ code,
23171
+ (result.stderr || result.stdout || "Git command failed").trim()
23172
+ );
23173
+ }
23174
+ return result.stdout.trim();
23085
23175
  }
23086
-
23087
- // src/computer-readiness.ts
23088
- var import_node_fs5 = require("fs");
23089
-
23090
- // src/runtime-config.ts
23091
- var import_node_fs4 = require("fs");
23092
- var import_node_os5 = require("os");
23093
- var import_node_path4 = require("path");
23094
- var DEFAULT_AGENT_DIR = (0, import_node_path4.join)((0, import_node_os5.homedir)(), ".alan", "agent");
23095
- var LEGACY_AGENT_DIR = (0, import_node_path4.join)((0, import_node_os5.homedir)(), ".aiden", "agent");
23096
- var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Map([
23097
- ["apiUrl", "string"],
23098
- ["wsUrl", "string"],
23099
- ["endpointProfile", "string"],
23100
- ["teamId", "string"],
23101
- ["runtimeId", "string"],
23102
- ["runtimeToken", "string"],
23103
- ["localApiPort", "number"],
23104
- ["localApiToken", "string"],
23105
- ["displayName", "string"]
23106
- ]);
23107
- function sanitizeAgentConfig(config) {
23108
- const sanitized = { ...config };
23109
- delete sanitized.teamId;
23110
- delete sanitized.organizationId;
23111
- delete sanitized.currentTeamId;
23112
- delete sanitized.currentOrganizationId;
23113
- return sanitized;
23176
+ function sourceChangedDuringCheckpoint(message) {
23177
+ return new BindingSwitchGitError("source_changed_since_approval", message);
23114
23178
  }
23115
- function isValidAgentConfig(configPath) {
23179
+ function acquireCheckpointHeadLock(locks) {
23116
23180
  try {
23117
- const stat = (0, import_node_fs4.lstatSync)(configPath);
23118
- if (!stat.isFile() || stat.isSymbolicLink()) return false;
23119
- const parsed = JSON.parse((0, import_node_fs4.readFileSync)(configPath, "utf8"));
23120
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
23121
- const entries = Object.entries(parsed);
23122
- return entries.some(([key]) => AGENT_CONFIG_KEYS.has(key)) && entries.every(([key, value2]) => {
23123
- const expectedType = AGENT_CONFIG_KEYS.get(key);
23124
- return expectedType === void 0 || typeof value2 === expectedType;
23125
- });
23181
+ (0, import_node_fs4.writeFileSync)(locks.headLockPath, "", { flag: "wx" });
23182
+ locks.headLockHeld = true;
23126
23183
  } catch {
23127
- return false;
23184
+ throw sourceChangedDuringCheckpoint(
23185
+ "Source repository is being changed by another Git operation"
23186
+ );
23128
23187
  }
23129
23188
  }
23130
- function migrateLegacyAgentConfig(input) {
23131
- const profile = normalizeAgentEndpointProfile(input?.profile ?? null) ?? "production";
23132
- const alanBaseDir = input?.alanBaseDir ?? DEFAULT_AGENT_DIR;
23133
- const legacyBaseDir = input?.legacyBaseDir ?? LEGACY_AGENT_DIR;
23134
- const fileName = resolveAgentConfigFileName(profile);
23135
- const legacyPath = (0, import_node_path4.join)(legacyBaseDir, fileName);
23136
- const alanPath = (0, import_node_path4.join)(alanBaseDir, fileName);
23137
- if ((0, import_node_fs4.existsSync)(alanPath) || !isValidAgentConfig(legacyPath)) return false;
23138
- let copied = false;
23189
+ function releaseCheckpointHeadLock(locks) {
23190
+ if (!locks.headLockHeld) return;
23191
+ (0, import_node_fs4.rmSync)(locks.headLockPath, { force: true });
23192
+ locks.headLockHeld = false;
23193
+ }
23194
+ function acquireCheckpointRepositoryLocks(repoRoot) {
23195
+ const gitDir = runGit(
23196
+ repoRoot,
23197
+ ["rev-parse", "--absolute-git-dir"],
23198
+ "checkpoint_lock_unavailable"
23199
+ );
23200
+ const indexPath = runGit(
23201
+ repoRoot,
23202
+ ["rev-parse", "--path-format=absolute", "--git-path", "index"],
23203
+ "checkpoint_lock_unavailable"
23204
+ );
23205
+ const locks = {
23206
+ headLockPath: (0, import_node_path4.join)(gitDir, "HEAD.lock"),
23207
+ headLockHeld: false,
23208
+ indexLockPath: `${indexPath}.lock`,
23209
+ indexLockHeld: false,
23210
+ indexPath
23211
+ };
23139
23212
  try {
23140
- const sourceDirectoryStat = (0, import_node_fs4.lstatSync)(legacyBaseDir);
23141
- const sourceFileStat = (0, import_node_fs4.lstatSync)(legacyPath);
23142
- if (!sourceDirectoryStat.isDirectory() || sourceDirectoryStat.isSymbolicLink()) return false;
23143
- const alanDirectoryExisted = (0, import_node_fs4.existsSync)(alanBaseDir);
23144
- if (alanDirectoryExisted) {
23145
- const alanDirectoryStat = (0, import_node_fs4.lstatSync)(alanBaseDir);
23146
- if (!alanDirectoryStat.isDirectory() || alanDirectoryStat.isSymbolicLink()) return false;
23213
+ (0, import_node_fs4.writeFileSync)(locks.indexLockPath, "", { flag: "wx" });
23214
+ locks.indexLockHeld = true;
23215
+ acquireCheckpointHeadLock(locks);
23216
+ return locks;
23217
+ } catch {
23218
+ if (locks.indexLockHeld) {
23219
+ (0, import_node_fs4.rmSync)(locks.indexLockPath, { force: true });
23220
+ locks.indexLockHeld = false;
23147
23221
  }
23148
- (0, import_node_fs4.mkdirSync)(alanBaseDir, { recursive: true, mode: sourceDirectoryStat.mode & 511 });
23149
- if (!alanDirectoryExisted) (0, import_node_fs4.chmodSync)(alanBaseDir, sourceDirectoryStat.mode & 511);
23150
- (0, import_node_fs4.copyFileSync)(legacyPath, alanPath, import_node_fs4.constants.COPYFILE_EXCL);
23151
- copied = true;
23152
- const migrated = sanitizeAgentConfig(
23153
- JSON.parse((0, import_node_fs4.readFileSync)(alanPath, "utf8"))
23222
+ throw sourceChangedDuringCheckpoint(
23223
+ "Source repository is being changed by another Git operation"
23154
23224
  );
23155
- (0, import_node_fs4.writeFileSync)(alanPath, JSON.stringify(migrated, null, 2));
23156
- (0, import_node_fs4.chmodSync)(alanPath, sourceFileStat.mode & 511);
23157
- (0, import_node_fs4.utimesSync)(alanPath, sourceFileStat.atime, sourceFileStat.mtime);
23158
- if (!isValidAgentConfig(alanPath)) {
23159
- (0, import_node_fs4.unlinkSync)(alanPath);
23160
- return false;
23161
- }
23162
- return true;
23163
- } catch {
23164
- if (copied && (0, import_node_fs4.existsSync)(alanPath)) (0, import_node_fs4.unlinkSync)(alanPath);
23165
- return false;
23166
23225
  }
23167
23226
  }
23168
- function resolveAgentConfigPath(input) {
23169
- if (input?.explicitPath?.trim()) return input.explicitPath.trim();
23170
- const profile = normalizeAgentEndpointProfile(
23171
- typeof input?.profile === "string" ? input.profile : input?.profile ?? null
23172
- ) ?? "production";
23173
- const baseDir = input?.baseDir ?? DEFAULT_AGENT_DIR;
23174
- if (input?.baseDir === void 0) migrateLegacyAgentConfig({ profile });
23175
- return (0, import_node_path4.join)(baseDir, resolveAgentConfigFileName(profile));
23227
+ function releaseCheckpointRepositoryLocks(locks) {
23228
+ releaseCheckpointHeadLock(locks);
23229
+ if (locks.indexLockHeld) {
23230
+ (0, import_node_fs4.rmSync)(locks.indexLockPath, { force: true });
23231
+ locks.indexLockHeld = false;
23232
+ }
23176
23233
  }
23177
- function configExistsAtPath(configPath) {
23178
- return (0, import_node_fs4.existsSync)(configPath);
23234
+ function assertCheckpointHead(repoRoot, expectedRef, expectedHeadSha) {
23235
+ const currentRef = runGit(
23236
+ repoRoot,
23237
+ ["symbolic-ref", "-q", "HEAD"],
23238
+ "source_changed_since_approval"
23239
+ );
23240
+ const currentHead = runGit(repoRoot, ["rev-parse", "HEAD"], "source_changed_since_approval");
23241
+ if (currentRef !== expectedRef || currentHead !== expectedHeadSha) {
23242
+ throw sourceChangedDuringCheckpoint(
23243
+ "Source HEAD or branch changed during checkpoint finalization"
23244
+ );
23245
+ }
23179
23246
  }
23180
- function resolveEndpointProfileFromArgs(args) {
23181
- const index = args.indexOf("--profile");
23182
- const raw = index === -1 ? process.env.ALAN_AGENT_PROFILE : args[index + 1];
23183
- return normalizeAgentEndpointProfile(raw) ?? "production";
23247
+ function createCheckpointIndexBaseline(repoRoot, checkpointSha) {
23248
+ const tempDir = (0, import_node_fs4.mkdtempSync)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "alan-checkpoint-index-"));
23249
+ const indexPath = (0, import_node_path4.join)(tempDir, "index");
23250
+ try {
23251
+ runGitWithOptions(
23252
+ repoRoot,
23253
+ ["read-tree", checkpointSha],
23254
+ { env: { GIT_INDEX_FILE: indexPath } },
23255
+ "checkpoint_index_sync_failed"
23256
+ );
23257
+ return { indexPath, tempDir };
23258
+ } catch (error) {
23259
+ (0, import_node_fs4.rmSync)(tempDir, { recursive: true, force: true });
23260
+ throw error;
23261
+ }
23184
23262
  }
23185
- function bindAgentConfigPathFromArgs(args) {
23186
- const explicit = process.env.ALAN_AGENT_CONFIG_PATH?.trim();
23187
- if (explicit) return explicit;
23188
- const profile = resolveEndpointProfileFromArgs(args);
23189
- const configPath = resolveAgentConfigPath({ profile });
23190
- process.env.ALAN_AGENT_CONFIG_PATH = configPath;
23191
- return configPath;
23263
+ function installCheckpointIndexBaseline(locks, baselineIndexPath) {
23264
+ try {
23265
+ (0, import_node_fs4.copyFileSync)(baselineIndexPath, locks.indexLockPath);
23266
+ (0, import_node_fs4.renameSync)(locks.indexLockPath, locks.indexPath);
23267
+ locks.indexLockHeld = false;
23268
+ } catch (error) {
23269
+ throw new BindingSwitchGitError(
23270
+ "checkpoint_index_sync_failed",
23271
+ error instanceof Error ? error.message : "The checkpoint index could not be installed"
23272
+ );
23273
+ }
23192
23274
  }
23193
-
23194
- // src/computer-readiness.ts
23195
- var WorkspaceAccessTracker = class {
23196
- constructor(inspect = (path) => inspectWorkspaceReadiness(path)) {
23197
- this.inspect = inspect;
23275
+ function requiredString(params, key) {
23276
+ const value2 = params[key];
23277
+ if (typeof value2 !== "string" || value2.trim() === "") {
23278
+ throw new BindingSwitchGitError("invalid_git_request", `${key} is required`);
23198
23279
  }
23199
- issue = null;
23200
- track(path, readiness) {
23201
- if (readiness.ready) {
23202
- this.issue = null;
23203
- return;
23204
- }
23205
- this.issue = { path, readiness };
23280
+ if (/[\0\r\n]/.test(value2)) {
23281
+ throw new BindingSwitchGitError("invalid_git_request", `${key} contains unsafe characters`);
23206
23282
  }
23207
- recheck() {
23208
- if (!this.issue) return null;
23209
- const readiness = this.inspect(this.issue.path);
23210
- if (readiness.ready) {
23211
- this.issue = null;
23212
- return readiness;
23213
- }
23214
- this.issue = { path: this.issue.path, readiness };
23283
+ return value2;
23284
+ }
23285
+ function requiredCommitMessage(params) {
23286
+ const value2 = params.commitMessage;
23287
+ if (typeof value2 !== "string" || value2.trim() === "") {
23288
+ throw new BindingSwitchGitError("invalid_git_request", "commitMessage is required");
23289
+ }
23290
+ if (value2.includes("\0")) {
23291
+ throw new BindingSwitchGitError(
23292
+ "invalid_git_request",
23293
+ "commitMessage contains unsafe characters"
23294
+ );
23295
+ }
23296
+ return value2;
23297
+ }
23298
+ function optionalString(params, key) {
23299
+ const value2 = params[key];
23300
+ if (value2 === void 0 || value2 === null) return void 0;
23301
+ if (typeof value2 !== "string" || value2.trim() === "" || /[\0\r\n]/.test(value2)) {
23302
+ throw new BindingSwitchGitError("invalid_git_request", `${key} is invalid`);
23303
+ }
23304
+ return value2;
23305
+ }
23306
+ function commonRequest(params) {
23307
+ return {
23308
+ switchId: requiredString(params, "switchId"),
23309
+ repoKey: requiredString(params, "repoKey"),
23310
+ repoPath: (0, import_node_path4.resolve)(requiredString(params, "repoPath"))
23311
+ };
23312
+ }
23313
+ function assertCommitSha(sha, key) {
23314
+ if (!/^[0-9a-f]{40,64}$/i.test(sha)) {
23315
+ throw new BindingSwitchGitError("invalid_git_request", `${key} is not a commit SHA`);
23316
+ }
23317
+ }
23318
+ function assertValidBranch(branch) {
23319
+ if (branch.startsWith("-")) {
23320
+ throw new BindingSwitchGitError("invalid_git_request", "branch is not a safe Git ref");
23321
+ }
23322
+ const result = (0, import_node_child_process3.spawnSync)("git", ["check-ref-format", "--branch", branch], {
23323
+ env: getDaemonCliEnvironment(),
23324
+ encoding: "utf8"
23325
+ });
23326
+ if (result.status !== 0) {
23327
+ throw new BindingSwitchGitError("invalid_git_request", "branch is not a valid Git ref");
23328
+ }
23329
+ }
23330
+ function validatedRemote(params) {
23331
+ const remote = optionalString(params, "remote") ?? "origin";
23332
+ if (remote.startsWith("-") || remote.includes("/") || remote.includes("\\")) {
23333
+ throw new BindingSwitchGitError("invalid_git_request", "remote is not a safe remote name");
23334
+ }
23335
+ return remote;
23336
+ }
23337
+ function canonicalRemoteUrl(repoRoot, value2) {
23338
+ const trimmed = value2.trim();
23339
+ if (!trimmed || /[\0\r\n]/.test(trimmed)) {
23340
+ throw new BindingSwitchGitError("invalid_git_request", "Remote URL is invalid");
23341
+ }
23342
+ const scpStyle = trimmed.match(/^([^/@:\s]+)@([^/:\s]+):(.+)$/);
23343
+ const urlValue = scpStyle ? `ssh://${scpStyle[1]}@${scpStyle[2]}/${scpStyle[3]}` : trimmed;
23344
+ if ((0, import_node_path4.isAbsolute)(urlValue) || urlValue.startsWith("./") || urlValue.startsWith("../")) {
23345
+ const absolute = (0, import_node_path4.isAbsolute)(urlValue) ? urlValue : (0, import_node_path4.resolve)(repoRoot, urlValue);
23346
+ return `file://${canonicalPathWithMissingTail(absolute).replace(/\/+$/, "").replace(/\.git$/, "")}`;
23347
+ }
23348
+ let parsed;
23349
+ try {
23350
+ parsed = new URL(urlValue);
23351
+ } catch {
23352
+ throw new BindingSwitchGitError("invalid_git_request", "Remote URL is invalid");
23353
+ }
23354
+ if (parsed.protocol === "file:") {
23355
+ return `file://${canonicalPathWithMissingTail((0, import_node_url.fileURLToPath)(parsed)).replace(/\/+$/, "").replace(/\.git$/, "")}`;
23356
+ }
23357
+ if (parsed.search || parsed.hash) {
23358
+ throw new BindingSwitchGitError("invalid_git_request", "Remote URL is invalid");
23359
+ }
23360
+ if (!["https:", "http:", "ssh:", "git:"].includes(parsed.protocol)) {
23361
+ throw new BindingSwitchGitError(
23362
+ "invalid_git_request",
23363
+ "Remote URL uses an unsupported protocol"
23364
+ );
23365
+ }
23366
+ if (parsed.password || (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username) {
23367
+ throw new BindingSwitchGitError(
23368
+ "remote_url_credentials_rejected",
23369
+ "Credential-bearing remote URLs are not allowed for environment migration"
23370
+ );
23371
+ }
23372
+ const defaultPort = parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "http:" && parsed.port === "80" || parsed.protocol === "ssh:" && parsed.port === "22";
23373
+ const transport = parsed.protocol === "https:" || parsed.protocol === "ssh:" ? "secure:" : parsed.protocol;
23374
+ const authority = `${parsed.hostname.toLowerCase()}${parsed.port && !defaultPort ? `:${parsed.port}` : ""}`;
23375
+ const path = parsed.pathname.replace(/\/+/g, "/").replace(/\/+$/, "").replace(/\.git$/, "");
23376
+ return `${transport}//${authority}${path}`;
23377
+ }
23378
+ function verifiedRemote(repoRoot, params, includePushUrls) {
23379
+ const remote = validatedRemote(params);
23380
+ const expectedRemoteUrl = canonicalRemoteUrl(
23381
+ repoRoot,
23382
+ requiredString(params, "expectedRemoteUrl")
23383
+ );
23384
+ const fetchUrls = runGit(
23385
+ repoRoot,
23386
+ ["remote", "get-url", "--all", remote],
23387
+ "git_remote_unavailable"
23388
+ ).split("\n").filter(Boolean);
23389
+ const pushUrls = includePushUrls ? runGit(repoRoot, ["remote", "get-url", "--push", "--all", remote], "git_remote_unavailable").split("\n").filter(Boolean) : [];
23390
+ const urls = [...fetchUrls, ...pushUrls];
23391
+ if (urls.length === 0 || urls.some((url2) => canonicalRemoteUrl(repoRoot, url2) !== expectedRemoteUrl)) {
23392
+ throw new BindingSwitchGitError(
23393
+ "remote_url_mismatch",
23394
+ "The configured Git remote does not match the repository selected for migration"
23395
+ );
23396
+ }
23397
+ return { remote, expectedRemoteUrl, remoteUrl: fetchUrls[0] };
23398
+ }
23399
+ function inspectedRemoteUrl(repoRoot, params) {
23400
+ if (optionalString(params, "expectedRemoteUrl")) {
23401
+ return verifiedRemote(repoRoot, params, false).remoteUrl;
23402
+ }
23403
+ const remote = validatedRemote(params);
23404
+ const result = runGitResult(repoRoot, ["remote", "get-url", "--all", remote]);
23405
+ const urls = result.status === 0 ? result.stdout.split("\n").filter(Boolean) : [];
23406
+ if (urls.length === 0) return null;
23407
+ for (const url2 of urls) canonicalRemoteUrl(repoRoot, url2);
23408
+ return urls[0];
23409
+ }
23410
+ function isPathContained(childPath, parentPath) {
23411
+ const rel = (0, import_node_path4.relative)((0, import_node_path4.resolve)(parentPath), (0, import_node_path4.resolve)(childPath));
23412
+ return rel === "" || !(0, import_node_path4.isAbsolute)(rel) && rel !== ".." && !rel.startsWith(`..${import_node_path4.sep}`);
23413
+ }
23414
+ function canonicalPathWithMissingTail(path) {
23415
+ const missingSegments = [];
23416
+ let cursor = (0, import_node_path4.resolve)(path);
23417
+ while (!(0, import_node_fs4.existsSync)(cursor)) {
23418
+ const parent = (0, import_node_path4.dirname)(cursor);
23419
+ if (parent === cursor) {
23420
+ throw new BindingSwitchGitError(
23421
+ "path_canonicalization_failed",
23422
+ "The selected path has no existing canonical ancestor"
23423
+ );
23424
+ }
23425
+ missingSegments.unshift((0, import_node_path4.basename)(cursor));
23426
+ cursor = parent;
23427
+ }
23428
+ return (0, import_node_path4.join)(import_node_fs4.realpathSync.native(cursor), ...missingSegments);
23429
+ }
23430
+ function assertCanonicalPathContained(childPath, parentPath, code, message) {
23431
+ if (!isPathContained(
23432
+ canonicalPathWithMissingTail(childPath),
23433
+ canonicalPathWithMissingTail(parentPath)
23434
+ )) {
23435
+ throw new BindingSwitchGitError(code, message);
23436
+ }
23437
+ }
23438
+ function alanWorktreeRoot(projectRoot) {
23439
+ return (0, import_node_path4.join)((0, import_node_path4.dirname)(projectRoot), `${(0, import_node_path4.basename)(projectRoot)}-alan-worktrees`);
23440
+ }
23441
+ function resolveRepositoryRoot(requestedPath) {
23442
+ if (!(0, import_node_fs4.existsSync)(requestedPath)) {
23443
+ throw new BindingSwitchGitError(
23444
+ "repository_not_found",
23445
+ `The selected repository path does not exist: ${requestedPath}`
23446
+ );
23447
+ }
23448
+ const root = runGitResult(requestedPath, ["rev-parse", "--show-toplevel"]);
23449
+ if (root.status !== 0 || !root.stdout.trim()) {
23450
+ throw new BindingSwitchGitError(
23451
+ "repository_not_found",
23452
+ "The selected folder is not inside a Git repository"
23453
+ );
23454
+ }
23455
+ return import_node_fs4.realpathSync.native((0, import_node_path4.resolve)(root.stdout.trim()));
23456
+ }
23457
+ function listWorktrees(repoRoot) {
23458
+ return runGit(repoRoot, ["worktree", "list", "--porcelain"]).split("\n\n").flatMap((entry) => {
23459
+ const lines = entry.split("\n");
23460
+ const path = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
23461
+ if (!path) return [];
23462
+ return [
23463
+ {
23464
+ path: canonicalPathWithMissingTail(path),
23465
+ branch: lines.find((line) => line.startsWith("branch "))?.slice("branch ".length).replace("refs/heads/", "") ?? null
23466
+ }
23467
+ ];
23468
+ });
23469
+ }
23470
+ function resolveAuthorizedRepository(params, requireTrustedContext = false) {
23471
+ const { repoPath } = commonRequest(params);
23472
+ const repoRoot = resolveRepositoryRoot(repoPath);
23473
+ const expectedRepoRoot = requireTrustedContext ? requiredString(params, "expectedRepoRoot") : optionalString(params, "expectedRepoRoot");
23474
+ if (expectedRepoRoot && resolveRepositoryRoot((0, import_node_path4.resolve)(expectedRepoRoot)) !== repoRoot) {
23475
+ throw new BindingSwitchGitError(
23476
+ "repository_path_changed",
23477
+ "The selected path now resolves to a different Git repository"
23478
+ );
23479
+ }
23480
+ const projectPath = requireTrustedContext ? requiredString(params, "projectPath") : optionalString(params, "projectPath");
23481
+ if (!projectPath) return repoRoot;
23482
+ const projectRoot = resolveRepositoryRoot((0, import_node_path4.resolve)(projectPath));
23483
+ const registrations = listWorktrees(projectRoot);
23484
+ const registered = registrations.find(({ path }) => path === repoRoot);
23485
+ if (!registered) {
23486
+ throw new BindingSwitchGitError(
23487
+ "repository_path_not_authorized",
23488
+ "The selected repository is not the Project checkout or one of its registered worktrees"
23489
+ );
23490
+ }
23491
+ const mainRoot = registrations[0]?.path;
23492
+ if (mainRoot && repoRoot !== mainRoot && !isPathContained(repoRoot, canonicalPathWithMissingTail(alanWorktreeRoot(mainRoot)))) {
23493
+ throw new BindingSwitchGitError(
23494
+ "repository_path_not_authorized",
23495
+ "The selected worktree is outside the Alan-managed worktree directory"
23496
+ );
23497
+ }
23498
+ return repoRoot;
23499
+ }
23500
+ function readRawGit(repoRoot, args, code) {
23501
+ const result = runGitResult(repoRoot, args);
23502
+ if (result.status !== 0) {
23503
+ throw new BindingSwitchGitError(
23504
+ code,
23505
+ (result.stderr || result.stdout || "Git inspection failed").trim()
23506
+ );
23507
+ }
23508
+ return result.stdout;
23509
+ }
23510
+ function updateLengthPrefixed(hash, value2) {
23511
+ const bytes = Buffer.from(value2, "utf8");
23512
+ hash.update(String(bytes.byteLength));
23513
+ hash.update("\0");
23514
+ hash.update(bytes);
23515
+ hash.update("\0");
23516
+ }
23517
+ function proposedTreeSha(repoRoot, headSha) {
23518
+ const tempDir = (0, import_node_fs4.mkdtempSync)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "alan-switch-index-"));
23519
+ const indexPath = (0, import_node_path4.join)(tempDir, "index");
23520
+ const options = { env: { GIT_INDEX_FILE: indexPath } };
23521
+ try {
23522
+ if (headSha) {
23523
+ runGitWithOptions(repoRoot, ["read-tree", headSha], options, "git_status_unreadable");
23524
+ } else {
23525
+ runGitWithOptions(repoRoot, ["read-tree", "--empty"], options, "git_status_unreadable");
23526
+ }
23527
+ runGitWithOptions(repoRoot, ["add", "--all"], options, "git_status_unreadable");
23528
+ const treeSha = runGitWithOptions(repoRoot, ["write-tree"], options, "git_status_unreadable");
23529
+ assertCommitSha(treeSha, "proposedTreeSha");
23530
+ return treeSha;
23531
+ } finally {
23532
+ (0, import_node_fs4.rmSync)(tempDir, { recursive: true, force: true });
23533
+ }
23534
+ }
23535
+ function statusFingerprint(repoRoot, porcelain, proposedTree) {
23536
+ const unstagedPatch = readRawGit(
23537
+ repoRoot,
23538
+ ["diff", "--binary", "--full-index", "--no-ext-diff", "--submodule=log"],
23539
+ "git_status_unreadable"
23540
+ );
23541
+ const stagedPatch = readRawGit(
23542
+ repoRoot,
23543
+ ["diff", "--cached", "--binary", "--full-index", "--no-ext-diff", "--submodule=log"],
23544
+ "git_status_unreadable"
23545
+ );
23546
+ const untrackedRaw = readRawGit(
23547
+ repoRoot,
23548
+ ["ls-files", "--others", "--exclude-standard", "-z"],
23549
+ "git_status_unreadable"
23550
+ );
23551
+ const untrackedPaths = untrackedRaw.split("\0").filter(Boolean);
23552
+ const hash = (0, import_node_crypto.createHash)("sha256");
23553
+ updateLengthPrefixed(hash, porcelain);
23554
+ updateLengthPrefixed(hash, proposedTree);
23555
+ updateLengthPrefixed(hash, unstagedPatch);
23556
+ updateLengthPrefixed(hash, stagedPatch);
23557
+ for (let index = 0; index < untrackedPaths.length; index += 128) {
23558
+ const paths = untrackedPaths.slice(index, index + 128);
23559
+ const hashes = readRawGit(
23560
+ repoRoot,
23561
+ ["hash-object", "--no-filters", "--", ...paths],
23562
+ "git_status_unreadable"
23563
+ );
23564
+ updateLengthPrefixed(hash, paths.join("\0"));
23565
+ updateLengthPrefixed(hash, hashes);
23566
+ }
23567
+ return hash.digest("hex");
23568
+ }
23569
+ function readStatus(repoRoot) {
23570
+ const headResult = runGitResult(repoRoot, ["rev-parse", "HEAD"]);
23571
+ const headSha = headResult.status === 0 ? headResult.stdout.trim() || null : null;
23572
+ const branchResult = runGitResult(repoRoot, ["branch", "--show-current"]);
23573
+ const branch = branchResult.status === 0 ? branchResult.stdout.trim() || null : null;
23574
+ const proposedTree = proposedTreeSha(repoRoot, headSha);
23575
+ const porcelain = runGit(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
23576
+ const displayStatus = runGit(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
23577
+ const upstreamResult = runGitResult(repoRoot, [
23578
+ "rev-parse",
23579
+ "--abbrev-ref",
23580
+ "--symbolic-full-name",
23581
+ "@{upstream}"
23582
+ ]);
23583
+ const upstream = upstreamResult.status === 0 ? upstreamResult.stdout.trim() || null : null;
23584
+ let ahead = null;
23585
+ let behind = null;
23586
+ if (upstream) {
23587
+ const counts = runGit(repoRoot, [
23588
+ "rev-list",
23589
+ "--left-right",
23590
+ "--count",
23591
+ `${upstream}...HEAD`
23592
+ ]).split(/\s+/);
23593
+ const parsedBehind = Number.parseInt(counts[0] ?? "", 10);
23594
+ const parsedAhead = Number.parseInt(counts[1] ?? "", 10);
23595
+ behind = Number.isNaN(parsedBehind) ? null : parsedBehind;
23596
+ ahead = Number.isNaN(parsedAhead) ? null : parsedAhead;
23597
+ }
23598
+ return {
23599
+ repoRoot,
23600
+ remoteUrl: null,
23601
+ branch,
23602
+ headSha,
23603
+ dirty: porcelain.length > 0,
23604
+ filesChanged: displayStatus ? displayStatus.split("\n").length : 0,
23605
+ ahead,
23606
+ behind,
23607
+ hasUpstream: upstream !== null,
23608
+ upstream,
23609
+ statusFingerprint: statusFingerprint(repoRoot, porcelain, proposedTree),
23610
+ proposedTreeSha: proposedTree
23611
+ };
23612
+ }
23613
+ function assertExpectedBranch(status, expectedBranch) {
23614
+ assertValidBranch(expectedBranch);
23615
+ if (status.branch !== expectedBranch) {
23616
+ throw new BindingSwitchGitError(
23617
+ "source_changed_since_approval",
23618
+ `Source branch changed from ${expectedBranch} to ${status.branch ?? "detached HEAD"}`
23619
+ );
23620
+ }
23621
+ }
23622
+ function assertSourcePreconditions(status, expectedHeadSha, expectedStatusFingerprint, expectedBranch) {
23623
+ assertCommitSha(expectedHeadSha, "expectedHeadSha");
23624
+ if (expectedBranch !== void 0) assertExpectedBranch(status, expectedBranch);
23625
+ if (status.headSha !== expectedHeadSha) {
23626
+ throw new BindingSwitchGitError(
23627
+ "source_changed_since_approval",
23628
+ `Source HEAD changed from ${expectedHeadSha} to ${status.headSha ?? "unborn"}`
23629
+ );
23630
+ }
23631
+ if (expectedStatusFingerprint !== void 0 && status.statusFingerprint !== expectedStatusFingerprint) {
23632
+ throw new BindingSwitchGitError(
23633
+ "source_changed_since_approval",
23634
+ "Source files changed after the migration inspection"
23635
+ );
23636
+ }
23637
+ }
23638
+ function commitHasSwitchTrailers(repoRoot, switchId, repoKey) {
23639
+ const message = runGitResult(repoRoot, ["show", "-s", "--format=%B", "HEAD"]);
23640
+ if (message.status !== 0) return false;
23641
+ const lines = message.stdout.split(/\r?\n/);
23642
+ return lines.includes(`Alan-Switch-Id: ${switchId}`) && lines.includes(`Alan-Repository-Id: ${repoKey}`);
23643
+ }
23644
+ async function checkpoint(params) {
23645
+ const { switchId, repoKey } = commonRequest(params);
23646
+ const repoRoot = resolveAuthorizedRepository(params, true);
23647
+ const expectedHeadSha = requiredString(params, "expectedHeadSha");
23648
+ const expectedStatusFingerprint = requiredString(params, "expectedStatusFingerprint");
23649
+ const expectedProposedTreeSha = requiredString(params, "expectedProposedTreeSha");
23650
+ assertCommitSha(expectedProposedTreeSha, "expectedProposedTreeSha");
23651
+ const expectedBranch = requiredString(params, "expectedBranch");
23652
+ const status = readStatus(repoRoot);
23653
+ assertExpectedBranch(status, expectedBranch);
23654
+ if (commitHasSwitchTrailers(repoRoot, switchId, repoKey)) {
23655
+ if (!status.headSha) {
23656
+ throw new BindingSwitchGitError("source_head_unreadable", "Checkpoint HEAD is unreadable");
23657
+ }
23658
+ const expectedRef2 = `refs/heads/${expectedBranch}`;
23659
+ const baseline2 = createCheckpointIndexBaseline(repoRoot, status.headSha);
23660
+ let locks2 = null;
23661
+ try {
23662
+ locks2 = acquireCheckpointRepositoryLocks(repoRoot);
23663
+ assertCheckpointHead(repoRoot, expectedRef2, status.headSha);
23664
+ installCheckpointIndexBaseline(locks2, baseline2.indexPath);
23665
+ return { repoRoot, sha: status.headSha, replayed: true };
23666
+ } finally {
23667
+ if (locks2) releaseCheckpointRepositoryLocks(locks2);
23668
+ (0, import_node_fs4.rmSync)(baseline2.tempDir, { recursive: true, force: true });
23669
+ }
23670
+ }
23671
+ assertSourcePreconditions(status, expectedHeadSha, expectedStatusFingerprint, expectedBranch);
23672
+ if (status.proposedTreeSha !== expectedProposedTreeSha) {
23673
+ throw new BindingSwitchGitError(
23674
+ "source_changed_since_approval",
23675
+ "The proposed checkpoint tree changed after migration approval"
23676
+ );
23677
+ }
23678
+ if (!status.dirty) {
23679
+ if (!status.headSha) {
23680
+ throw new BindingSwitchGitError("source_head_unreadable", "Source HEAD is unreadable");
23681
+ }
23682
+ return { repoRoot, sha: status.headSha, replayed: true };
23683
+ }
23684
+ const commitMessage = requiredCommitMessage(params).trim();
23685
+ const authorName = requiredString(params, "authorName");
23686
+ const authorEmail = requiredString(params, "authorEmail");
23687
+ const expectedHeadTree = runGit(
23688
+ repoRoot,
23689
+ ["rev-parse", `${expectedHeadSha}^{tree}`],
23690
+ "source_head_unreadable"
23691
+ );
23692
+ if (expectedHeadTree === expectedProposedTreeSha) {
23693
+ throw new BindingSwitchGitError(
23694
+ "checkpoint_empty",
23695
+ "The inspected changes did not produce a checkpoint commit"
23696
+ );
23697
+ }
23698
+ const proposedTreeObject = runGitResult(repoRoot, [
23699
+ "cat-file",
23700
+ "-e",
23701
+ `${expectedProposedTreeSha}^{tree}`
23702
+ ]);
23703
+ if (proposedTreeObject.status !== 0) {
23704
+ throw new BindingSwitchGitError(
23705
+ "checkpoint_tree_unavailable",
23706
+ "The approved checkpoint tree is no longer available"
23707
+ );
23708
+ }
23709
+ await waitForCheckpointTestBarrier();
23710
+ const currentBranch = runGit(repoRoot, ["branch", "--show-current"]);
23711
+ const currentHead = runGit(repoRoot, ["rev-parse", "HEAD"], "source_head_unreadable");
23712
+ if (currentBranch !== expectedBranch || currentHead !== expectedHeadSha) {
23713
+ throw new BindingSwitchGitError(
23714
+ "source_changed_since_approval",
23715
+ "Source HEAD or branch changed during checkpoint preparation"
23716
+ );
23717
+ }
23718
+ const message = `${commitMessage}
23719
+
23720
+ Alan-Switch-Id: ${switchId}
23721
+ Alan-Repository-Id: ${repoKey}`;
23722
+ const sha = runGitWithOptions(
23723
+ repoRoot,
23724
+ ["commit-tree", expectedProposedTreeSha, "-p", expectedHeadSha],
23725
+ {
23726
+ env: {
23727
+ GIT_AUTHOR_EMAIL: authorEmail,
23728
+ GIT_AUTHOR_NAME: authorName,
23729
+ GIT_COMMITTER_EMAIL: authorEmail,
23730
+ GIT_COMMITTER_NAME: authorName
23731
+ },
23732
+ input: `${message}
23733
+ `
23734
+ },
23735
+ "checkpoint_commit_failed"
23736
+ );
23737
+ assertCommitSha(sha, "checkpointSha");
23738
+ const expectedRef = `refs/heads/${expectedBranch}`;
23739
+ const currentRef = runGit(
23740
+ repoRoot,
23741
+ ["symbolic-ref", "-q", "HEAD"],
23742
+ "source_changed_since_approval"
23743
+ );
23744
+ if (currentRef !== expectedRef) {
23745
+ throw new BindingSwitchGitError(
23746
+ "source_changed_since_approval",
23747
+ "Source branch changed during checkpoint preparation"
23748
+ );
23749
+ }
23750
+ await waitForCheckpointFinalizeTestBarrier();
23751
+ const baseline = createCheckpointIndexBaseline(repoRoot, sha);
23752
+ let locks = null;
23753
+ let refAdvanced = false;
23754
+ let indexInstalled = false;
23755
+ try {
23756
+ locks = acquireCheckpointRepositoryLocks(repoRoot);
23757
+ assertCheckpointHead(repoRoot, expectedRef, expectedHeadSha);
23758
+ releaseCheckpointHeadLock(locks);
23759
+ runGit(
23760
+ repoRoot,
23761
+ ["update-ref", expectedRef, sha, expectedHeadSha],
23762
+ "source_changed_since_approval"
23763
+ );
23764
+ refAdvanced = true;
23765
+ acquireCheckpointHeadLock(locks);
23766
+ assertCheckpointHead(repoRoot, expectedRef, sha);
23767
+ installCheckpointIndexBaseline(locks, baseline.indexPath);
23768
+ indexInstalled = true;
23769
+ return { repoRoot, sha, replayed: false };
23770
+ } finally {
23771
+ if (refAdvanced && !indexInstalled) {
23772
+ if (locks) releaseCheckpointHeadLock(locks);
23773
+ runGitResult(repoRoot, ["update-ref", expectedRef, expectedHeadSha, sha]);
23774
+ }
23775
+ if (locks) releaseCheckpointRepositoryLocks(locks);
23776
+ (0, import_node_fs4.rmSync)(baseline.tempDir, { recursive: true, force: true });
23777
+ }
23778
+ }
23779
+ function resolveRemoteBranch(repoRoot, params) {
23780
+ const expectedBranch = requiredString(params, "expectedBranch");
23781
+ const requestedBranch = optionalString(params, "branch");
23782
+ if (requestedBranch && requestedBranch !== expectedBranch) {
23783
+ throw new BindingSwitchGitError("invalid_git_request", "branch must match expectedBranch");
23784
+ }
23785
+ const currentBranch = runGit(repoRoot, ["branch", "--show-current"]);
23786
+ const branch = expectedBranch;
23787
+ assertValidBranch(branch);
23788
+ if (currentBranch !== expectedBranch) {
23789
+ throw new BindingSwitchGitError(
23790
+ "source_changed_since_approval",
23791
+ `Source branch changed from ${expectedBranch} to ${currentBranch || "detached HEAD"}`
23792
+ );
23793
+ }
23794
+ const { remote } = verifiedRemote(repoRoot, params, true);
23795
+ return { branch, remote };
23796
+ }
23797
+ async function push(params) {
23798
+ const repoRoot = resolveAuthorizedRepository(params, true);
23799
+ const expectedHeadSha = requiredString(params, "expectedHeadSha");
23800
+ const expectedBranch = requiredString(params, "expectedBranch");
23801
+ const status = readStatus(repoRoot);
23802
+ assertSourcePreconditions(status, expectedHeadSha, void 0, expectedBranch);
23803
+ if (status.dirty) {
23804
+ throw new BindingSwitchGitError(
23805
+ "source_dirty_after_checkpoint",
23806
+ "The source has uncommitted changes and cannot be published"
23807
+ );
23808
+ }
23809
+ const { branch, remote } = resolveRemoteBranch(repoRoot, params);
23810
+ const latest = readStatus(repoRoot);
23811
+ assertSourcePreconditions(latest, expectedHeadSha, void 0, expectedBranch);
23812
+ if (latest.dirty) {
23813
+ throw new BindingSwitchGitError(
23814
+ "source_dirty_after_checkpoint",
23815
+ "The source has uncommitted changes and cannot be published"
23816
+ );
23817
+ }
23818
+ const before = await runNetworkGit(
23819
+ repoRoot,
23820
+ ["ls-remote", "--heads", remote, `refs/heads/${branch}`],
23821
+ "git_remote_unavailable"
23822
+ );
23823
+ const remoteBefore = before.trim().split(/\s+/)[0] || null;
23824
+ await runNetworkGit(
23825
+ repoRoot,
23826
+ ["push", remote, `${expectedHeadSha}:refs/heads/${expectedBranch}`],
23827
+ "git_push_failed"
23828
+ );
23829
+ const remoteHead = (await runNetworkGit(
23830
+ repoRoot,
23831
+ ["ls-remote", "--heads", remote, `refs/heads/${branch}`],
23832
+ "push_verification_failed"
23833
+ )).split(/\s+/)[0]?.trim();
23834
+ if (remoteHead !== expectedHeadSha) {
23835
+ throw new BindingSwitchGitError(
23836
+ "push_verification_failed",
23837
+ `Remote branch ${branch} is at ${remoteHead || "an unreadable SHA"}, expected ${expectedHeadSha}`
23838
+ );
23839
+ }
23840
+ const afterPush = readStatus(repoRoot);
23841
+ if (afterPush.headSha !== expectedHeadSha || afterPush.branch !== expectedBranch || afterPush.dirty || afterPush.statusFingerprint !== latest.statusFingerprint) {
23842
+ throw new BindingSwitchGitError(
23843
+ "source_changed_since_approval",
23844
+ "The source changed while the approved commit was being published",
23845
+ {
23846
+ published: {
23847
+ pushedSha: expectedHeadSha,
23848
+ branch,
23849
+ remote,
23850
+ replayed: remoteBefore === expectedHeadSha
23851
+ }
23852
+ }
23853
+ );
23854
+ }
23855
+ return {
23856
+ repoRoot,
23857
+ pushedSha: expectedHeadSha,
23858
+ branch,
23859
+ remote,
23860
+ replayed: remoteBefore === expectedHeadSha
23861
+ };
23862
+ }
23863
+ async function fetchPublishedBranch(repoRoot, branch, expectedSha, params) {
23864
+ assertValidBranch(branch);
23865
+ assertCommitSha(expectedSha, "expectedSha");
23866
+ const { remote } = verifiedRemote(repoRoot, params, false);
23867
+ await runNetworkGit(
23868
+ repoRoot,
23869
+ ["fetch", "--no-tags", remote, `refs/heads/${branch}`],
23870
+ "git_fetch_failed"
23871
+ );
23872
+ const fetchedSha = runGit(repoRoot, ["rev-parse", "FETCH_HEAD"], "target_sha_unavailable");
23873
+ if (fetchedSha !== expectedSha) {
23874
+ throw new BindingSwitchGitError(
23875
+ "target_sha_mismatch",
23876
+ `Fetched ${branch} at ${fetchedSha}, expected the published SHA ${expectedSha}`
23877
+ );
23878
+ }
23879
+ const commit = runGitResult(repoRoot, ["cat-file", "-e", `${expectedSha}^{commit}`]);
23880
+ if (commit.status !== 0) {
23881
+ throw new BindingSwitchGitError(
23882
+ "target_sha_unavailable",
23883
+ `The published SHA ${expectedSha} is not available in the destination repository`
23884
+ );
23885
+ }
23886
+ return { fetchedSha, remote };
23887
+ }
23888
+ async function fetchBranch(params) {
23889
+ const repoRoot = resolveAuthorizedRepository(params, true);
23890
+ const branch = requiredString(params, "branch");
23891
+ const expectedSha = requiredString(params, "expectedSha");
23892
+ const { fetchedSha, remote } = await fetchPublishedBranch(repoRoot, branch, expectedSha, params);
23893
+ return { repoRoot, fetchedSha, branch, remote };
23894
+ }
23895
+ function isAncestor(repoRoot, ancestor, descendant) {
23896
+ return runGitResult(repoRoot, ["merge-base", "--is-ancestor", ancestor, descendant]).status === 0;
23897
+ }
23898
+ function assertCleanTarget(repoRoot) {
23899
+ if (readStatus(repoRoot).dirty) {
23900
+ throw new BindingSwitchGitError(
23901
+ "target_checkout_dirty",
23902
+ "The destination checkout has uncommitted changes"
23903
+ );
23904
+ }
23905
+ }
23906
+ function assertBranchAvailableAtPath(repoRoot, branch, allowedPath) {
23907
+ const canonicalAllowedPath = canonicalPathWithMissingTail(allowedPath);
23908
+ const conflicting = listWorktrees(repoRoot).find(
23909
+ (worktree) => worktree.branch === branch && worktree.path !== canonicalAllowedPath
23910
+ );
23911
+ if (conflicting) {
23912
+ throw new BindingSwitchGitError(
23913
+ "target_branch_in_use",
23914
+ `Branch ${branch} is already checked out at ${conflicting.path}`
23915
+ );
23916
+ }
23917
+ }
23918
+ function advanceBranchAtCheckout(checkoutRoot, branch, targetSha) {
23919
+ const before = readStatus(checkoutRoot);
23920
+ if (before.branch === branch && before.headSha === targetSha) return true;
23921
+ assertBranchAvailableAtPath(checkoutRoot, branch, checkoutRoot);
23922
+ const branchExists = runGitResult(checkoutRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
23923
+ if (branchExists) {
23924
+ const branchHead = runGit(checkoutRoot, ["rev-parse", `refs/heads/${branch}`]);
23925
+ if (!isAncestor(checkoutRoot, branchHead, targetSha)) {
23926
+ throw new BindingSwitchGitError(
23927
+ "target_branch_diverged",
23928
+ `Local branch ${branch} cannot fast-forward to the published SHA`
23929
+ );
23930
+ }
23931
+ if (before.branch !== branch) {
23932
+ runGit(checkoutRoot, ["switch", branch], "target_checkout_failed");
23933
+ }
23934
+ } else {
23935
+ runGit(checkoutRoot, ["switch", "-c", branch, targetSha], "target_checkout_failed");
23936
+ }
23937
+ const currentHead = runGit(checkoutRoot, ["rev-parse", "HEAD"]);
23938
+ if (currentHead !== targetSha) {
23939
+ if (!isAncestor(checkoutRoot, currentHead, targetSha)) {
23940
+ throw new BindingSwitchGitError(
23941
+ "target_branch_diverged",
23942
+ `Local branch ${branch} cannot fast-forward to the published SHA`
23943
+ );
23944
+ }
23945
+ runGit(checkoutRoot, ["merge", "--ff-only", targetSha], "target_fast_forward_failed");
23946
+ }
23947
+ const verified = readStatus(checkoutRoot);
23948
+ if (verified.branch !== branch || verified.headSha !== targetSha) {
23949
+ throw new BindingSwitchGitError(
23950
+ "target_verification_failed",
23951
+ "The destination checkout did not land on the published branch and SHA"
23952
+ );
23953
+ }
23954
+ return false;
23955
+ }
23956
+ async function prepareCheckout(params) {
23957
+ const repoRoot = resolveAuthorizedRepository(params, true);
23958
+ const branch = requiredString(params, "branch");
23959
+ const targetSha = requiredString(params, "targetSha");
23960
+ await fetchPublishedBranch(repoRoot, branch, targetSha, params);
23961
+ assertCleanTarget(repoRoot);
23962
+ const replayed = advanceBranchAtCheckout(repoRoot, branch, targetSha);
23963
+ return {
23964
+ repoRoot,
23965
+ path: repoRoot,
23966
+ branch,
23967
+ headSha: targetSha,
23968
+ replayed
23969
+ };
23970
+ }
23971
+ async function prepareWorktree(params) {
23972
+ const repoRoot = resolveAuthorizedRepository(params, true);
23973
+ const registrations = listWorktrees(repoRoot);
23974
+ const projectRoot = registrations[0]?.path;
23975
+ if (!projectRoot || projectRoot !== repoRoot) {
23976
+ throw new BindingSwitchGitError(
23977
+ "project_checkout_required",
23978
+ "Worktree preparation requires the Project's main checkout"
23979
+ );
23980
+ }
23981
+ const worktreePath = (0, import_node_path4.resolve)(requiredString(params, "worktreePath"));
23982
+ const managedRoot = alanWorktreeRoot(projectRoot);
23983
+ const pathError = "The destination worktree path is outside the Alan-managed worktree directory";
23984
+ const intendedManagedRoot = (0, import_node_path4.join)(
23985
+ import_node_fs4.realpathSync.native((0, import_node_path4.dirname)(managedRoot)),
23986
+ (0, import_node_path4.basename)(managedRoot)
23987
+ );
23988
+ if (canonicalPathWithMissingTail(managedRoot) !== intendedManagedRoot) {
23989
+ throw new BindingSwitchGitError("worktree_path_not_authorized", pathError);
23990
+ }
23991
+ assertCanonicalPathContained(
23992
+ worktreePath,
23993
+ managedRoot,
23994
+ "worktree_path_not_authorized",
23995
+ pathError
23996
+ );
23997
+ const canonicalWorktreePath = canonicalPathWithMissingTail(worktreePath);
23998
+ if (canonicalWorktreePath === canonicalPathWithMissingTail(managedRoot)) {
23999
+ throw new BindingSwitchGitError("worktree_path_not_authorized", pathError);
24000
+ }
24001
+ const branch = requiredString(params, "branch");
24002
+ const targetSha = requiredString(params, "targetSha");
24003
+ await fetchPublishedBranch(repoRoot, branch, targetSha, params);
24004
+ const refreshed = listWorktrees(repoRoot);
24005
+ const registeredAtPath = refreshed.find(({ path }) => path === canonicalWorktreePath);
24006
+ const registeredForBranch = refreshed.find(({ branch: value2 }) => value2 === branch);
24007
+ if (registeredAtPath) {
24008
+ if (registeredAtPath.branch !== branch) {
24009
+ throw new BindingSwitchGitError(
24010
+ "worktree_path_conflict",
24011
+ "The destination path is registered to a different branch"
24012
+ );
24013
+ }
24014
+ assertCleanTarget(worktreePath);
24015
+ const replayed = advanceBranchAtCheckout(worktreePath, branch, targetSha);
24016
+ return {
24017
+ repoRoot: worktreePath,
24018
+ path: worktreePath,
24019
+ branch,
24020
+ headSha: targetSha,
24021
+ replayed
24022
+ };
24023
+ }
24024
+ if (registeredForBranch) {
24025
+ throw new BindingSwitchGitError(
24026
+ "target_branch_in_use",
24027
+ `Branch ${branch} is already checked out at ${registeredForBranch.path}`
24028
+ );
24029
+ }
24030
+ if ((0, import_node_fs4.existsSync)(worktreePath)) {
24031
+ throw new BindingSwitchGitError(
24032
+ "worktree_path_conflict",
24033
+ "The destination worktree path already exists and is not registered"
24034
+ );
24035
+ }
24036
+ const branchExists = runGitResult(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
24037
+ if (branchExists) {
24038
+ const branchHead = runGit(repoRoot, ["rev-parse", `refs/heads/${branch}`]);
24039
+ if (!isAncestor(repoRoot, branchHead, targetSha)) {
24040
+ throw new BindingSwitchGitError(
24041
+ "target_branch_diverged",
24042
+ `Local branch ${branch} cannot fast-forward to the published SHA`
24043
+ );
24044
+ }
24045
+ }
24046
+ (0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(worktreePath), { recursive: true });
24047
+ if (branchExists) {
24048
+ runGit(repoRoot, ["worktree", "add", worktreePath, branch], "worktree_prepare_failed");
24049
+ const currentHead = runGit(worktreePath, ["rev-parse", "HEAD"]);
24050
+ if (currentHead !== targetSha) {
24051
+ runGit(worktreePath, ["merge", "--ff-only", targetSha], "target_fast_forward_failed");
24052
+ }
24053
+ } else {
24054
+ runGit(
24055
+ repoRoot,
24056
+ ["worktree", "add", "-b", branch, worktreePath, targetSha],
24057
+ "worktree_prepare_failed"
24058
+ );
24059
+ }
24060
+ const createdRoot = resolveRepositoryRoot(worktreePath);
24061
+ assertCanonicalPathContained(createdRoot, managedRoot, "worktree_path_not_authorized", pathError);
24062
+ if (createdRoot !== import_node_fs4.realpathSync.native(worktreePath)) {
24063
+ throw new BindingSwitchGitError(
24064
+ "worktree_path_changed",
24065
+ "The destination worktree resolved to an unexpected path after creation"
24066
+ );
24067
+ }
24068
+ const verified = readStatus(createdRoot);
24069
+ if (verified.branch !== branch || verified.headSha !== targetSha) {
24070
+ throw new BindingSwitchGitError(
24071
+ "target_verification_failed",
24072
+ "The destination worktree did not land on the published branch and SHA"
24073
+ );
24074
+ }
24075
+ return {
24076
+ repoRoot: worktreePath,
24077
+ path: worktreePath,
24078
+ branch,
24079
+ headSha: targetSha,
24080
+ replayed: false
24081
+ };
24082
+ }
24083
+ function verifyHead(params) {
24084
+ const repoRoot = resolveAuthorizedRepository(params, true);
24085
+ const expectedSha = requiredString(params, "expectedSha");
24086
+ assertCommitSha(expectedSha, "expectedSha");
24087
+ assertCleanTarget(repoRoot);
24088
+ const headSha = runGit(repoRoot, ["rev-parse", "HEAD"], "target_head_unreadable");
24089
+ if (headSha !== expectedSha) {
24090
+ throw new BindingSwitchGitError(
24091
+ "verify_head_mismatch",
24092
+ `Destination HEAD is ${headSha}, expected ${expectedSha}`
24093
+ );
24094
+ }
24095
+ return { repoRoot, headSha, verified: true };
24096
+ }
24097
+ function statusBindingSwitchGit(params) {
24098
+ const repoRoot = resolveAuthorizedRepository(params);
24099
+ return {
24100
+ ...readStatus(repoRoot),
24101
+ remoteUrl: inspectedRemoteUrl(repoRoot, params)
24102
+ };
24103
+ }
24104
+ async function executeBindingSwitchGitRpc(method, params) {
24105
+ switch (method) {
24106
+ case "git.status":
24107
+ return statusBindingSwitchGit(params);
24108
+ case "git.checkpoint":
24109
+ return checkpoint(params);
24110
+ case "git.push":
24111
+ return push(params);
24112
+ case "git.fetch":
24113
+ return fetchBranch(params);
24114
+ case "git.prepare_checkout":
24115
+ return prepareCheckout(params);
24116
+ case "git.prepare_worktree":
24117
+ return prepareWorktree(params);
24118
+ case "git.verify_head":
24119
+ return verifyHead(params);
24120
+ }
24121
+ }
24122
+
24123
+ // src/cli-ensure-install.ts
24124
+ var import_node_child_process4 = require("child_process");
24125
+ var import_node_os5 = require("os");
24126
+ var CLI_RUNTIME_VERSIONS_ENV = "ALAN_CLI_RUNTIME_VERSIONS";
24127
+ var INSTALLABLE_PROVIDER_PACKAGES = {
24128
+ claude_cli: { npmPackage: "@anthropic-ai/claude-code", command: "claude", label: "Claude Code" },
24129
+ codex_app_server: { npmPackage: "@openai/codex", command: "codex", label: "Codex" },
24130
+ copilot_cli: { npmPackage: "@github/copilot", command: "copilot", label: "Copilot" },
24131
+ opencode_cli: { npmPackage: "opencode-ai", command: "opencode", label: "opencode" }
24132
+ };
24133
+ var INSTALL_TIMEOUT_MS = 12e4;
24134
+ var installOutcomes = /* @__PURE__ */ new Map();
24135
+ function canInstallProviderCli(backendKind) {
24136
+ return Boolean(backendKind && INSTALLABLE_PROVIDER_PACKAGES[backendKind]);
24137
+ }
24138
+ function pinnedVersion(npmPackage) {
24139
+ const raw = process.env[CLI_RUNTIME_VERSIONS_ENV];
24140
+ if (!raw) return "latest";
24141
+ try {
24142
+ const parsed = JSON.parse(raw);
24143
+ const version = parsed[npmPackage];
24144
+ if (typeof version === "string" && version.trim()) return version.trim();
24145
+ } catch {
24146
+ }
24147
+ return "latest";
24148
+ }
24149
+ function runNpmInstall(installSpec, env, timeoutMs) {
24150
+ return new Promise((resolve7) => {
24151
+ const isWin = (0, import_node_os5.platform)() === "win32";
24152
+ let child;
24153
+ try {
24154
+ child = (0, import_node_child_process4.spawn)("npm", ["install", "-g", installSpec], {
24155
+ env,
24156
+ shell: isWin,
24157
+ windowsHide: isWin ? true : void 0,
24158
+ timeout: timeoutMs,
24159
+ stdio: ["ignore", "pipe", "pipe"]
24160
+ });
24161
+ } catch (error) {
24162
+ resolve7({ status: null, stdout: "", stderr: "", error });
24163
+ return;
24164
+ }
24165
+ let stdout = "";
24166
+ let stderr = "";
24167
+ child.stdout?.setEncoding("utf8").on("data", (chunk) => {
24168
+ stdout += chunk;
24169
+ });
24170
+ child.stderr?.setEncoding("utf8").on("data", (chunk) => {
24171
+ stderr += chunk;
24172
+ });
24173
+ child.once("error", (error) => resolve7({ status: null, stdout, stderr, error }));
24174
+ child.once(
24175
+ "close",
24176
+ (status, signal) => resolve7({
24177
+ status,
24178
+ stdout,
24179
+ stderr,
24180
+ // Spawn `timeout` kills by signal without an `error` event; surface it
24181
+ // so a timed-out install is reported as a failure, not status null + ok.
24182
+ ...signal ? { error: new Error(`npm install killed by ${signal}`) } : {}
24183
+ })
24184
+ );
24185
+ });
24186
+ }
24187
+ async function attemptInstall(spec, options) {
24188
+ const installSpec = `${spec.npmPackage}@${pinnedVersion(spec.npmPackage)}`;
24189
+ const notify = options?.notify ?? ((message) => console.info(`[cli-ensure-install] ${message}`));
24190
+ notify(`Installing ${spec.label} CLI (${installSpec})\u2026`);
24191
+ const runner = options?.runner ?? runNpmInstall;
24192
+ const result = await runner(installSpec, getDaemonCliEnvironment(), INSTALL_TIMEOUT_MS);
24193
+ if (result.error || result.status !== 0) {
24194
+ console.warn(`[cli-ensure-install] npm install -g ${installSpec} failed`, {
24195
+ status: result.status,
24196
+ error: result.error?.message,
24197
+ output: `${result.stderr || result.stdout}`.trim().slice(-500)
24198
+ });
24199
+ notify(`Failed to install ${spec.label} CLI (${installSpec}).`);
24200
+ return false;
24201
+ }
24202
+ invalidateResolvedCli(spec.command);
24203
+ notify(`Installed ${spec.label} CLI (${installSpec}).`);
24204
+ return true;
24205
+ }
24206
+ function ensureProviderCliInstalled(backendKind, options) {
24207
+ if (!backendKind) return Promise.resolve(false);
24208
+ const spec = INSTALLABLE_PROVIDER_PACKAGES[backendKind];
24209
+ if (!spec) return Promise.resolve(false);
24210
+ const existing = installOutcomes.get(spec.npmPackage);
24211
+ if (existing) return existing;
24212
+ const attempt = attemptInstall(spec, options).catch((error) => {
24213
+ console.warn(`[cli-ensure-install] install attempt for ${spec.npmPackage} threw`, error);
24214
+ return false;
24215
+ });
24216
+ installOutcomes.set(spec.npmPackage, attempt);
24217
+ return attempt;
24218
+ }
24219
+
24220
+ // src/computer-readiness.ts
24221
+ var import_node_fs6 = require("fs");
24222
+
24223
+ // src/runtime-config.ts
24224
+ var import_node_fs5 = require("fs");
24225
+ var import_node_os6 = require("os");
24226
+ var import_node_path5 = require("path");
24227
+ var DEFAULT_AGENT_DIR = (0, import_node_path5.join)((0, import_node_os6.homedir)(), ".alan", "agent");
24228
+ var LEGACY_AGENT_DIR = (0, import_node_path5.join)((0, import_node_os6.homedir)(), ".aiden", "agent");
24229
+ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Map([
24230
+ ["apiUrl", "string"],
24231
+ ["wsUrl", "string"],
24232
+ ["endpointProfile", "string"],
24233
+ ["teamId", "string"],
24234
+ ["runtimeId", "string"],
24235
+ ["runtimeToken", "string"],
24236
+ ["localApiPort", "number"],
24237
+ ["localApiToken", "string"],
24238
+ ["displayName", "string"]
24239
+ ]);
24240
+ function sanitizeAgentConfig(config) {
24241
+ const sanitized = { ...config };
24242
+ delete sanitized.teamId;
24243
+ delete sanitized.organizationId;
24244
+ delete sanitized.currentTeamId;
24245
+ delete sanitized.currentOrganizationId;
24246
+ return sanitized;
24247
+ }
24248
+ function isValidAgentConfig(configPath) {
24249
+ try {
24250
+ const stat = (0, import_node_fs5.lstatSync)(configPath);
24251
+ if (!stat.isFile() || stat.isSymbolicLink()) return false;
24252
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)(configPath, "utf8"));
24253
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
24254
+ const entries = Object.entries(parsed);
24255
+ return entries.some(([key]) => AGENT_CONFIG_KEYS.has(key)) && entries.every(([key, value2]) => {
24256
+ const expectedType = AGENT_CONFIG_KEYS.get(key);
24257
+ return expectedType === void 0 || typeof value2 === expectedType;
24258
+ });
24259
+ } catch {
24260
+ return false;
24261
+ }
24262
+ }
24263
+ function migrateLegacyAgentConfig(input) {
24264
+ const profile = normalizeAgentEndpointProfile(input?.profile ?? null) ?? "production";
24265
+ const alanBaseDir = input?.alanBaseDir ?? DEFAULT_AGENT_DIR;
24266
+ const legacyBaseDir = input?.legacyBaseDir ?? LEGACY_AGENT_DIR;
24267
+ const fileName = resolveAgentConfigFileName(profile);
24268
+ const legacyPath = (0, import_node_path5.join)(legacyBaseDir, fileName);
24269
+ const alanPath = (0, import_node_path5.join)(alanBaseDir, fileName);
24270
+ if ((0, import_node_fs5.existsSync)(alanPath) || !isValidAgentConfig(legacyPath)) return false;
24271
+ let copied = false;
24272
+ try {
24273
+ const sourceDirectoryStat = (0, import_node_fs5.lstatSync)(legacyBaseDir);
24274
+ const sourceFileStat = (0, import_node_fs5.lstatSync)(legacyPath);
24275
+ if (!sourceDirectoryStat.isDirectory() || sourceDirectoryStat.isSymbolicLink()) return false;
24276
+ const alanDirectoryExisted = (0, import_node_fs5.existsSync)(alanBaseDir);
24277
+ if (alanDirectoryExisted) {
24278
+ const alanDirectoryStat = (0, import_node_fs5.lstatSync)(alanBaseDir);
24279
+ if (!alanDirectoryStat.isDirectory() || alanDirectoryStat.isSymbolicLink()) return false;
24280
+ }
24281
+ (0, import_node_fs5.mkdirSync)(alanBaseDir, { recursive: true, mode: sourceDirectoryStat.mode & 511 });
24282
+ if (!alanDirectoryExisted) (0, import_node_fs5.chmodSync)(alanBaseDir, sourceDirectoryStat.mode & 511);
24283
+ (0, import_node_fs5.copyFileSync)(legacyPath, alanPath, import_node_fs5.constants.COPYFILE_EXCL);
24284
+ copied = true;
24285
+ const migrated = sanitizeAgentConfig(
24286
+ JSON.parse((0, import_node_fs5.readFileSync)(alanPath, "utf8"))
24287
+ );
24288
+ (0, import_node_fs5.writeFileSync)(alanPath, JSON.stringify(migrated, null, 2));
24289
+ (0, import_node_fs5.chmodSync)(alanPath, sourceFileStat.mode & 511);
24290
+ (0, import_node_fs5.utimesSync)(alanPath, sourceFileStat.atime, sourceFileStat.mtime);
24291
+ if (!isValidAgentConfig(alanPath)) {
24292
+ (0, import_node_fs5.unlinkSync)(alanPath);
24293
+ return false;
24294
+ }
24295
+ return true;
24296
+ } catch {
24297
+ if (copied && (0, import_node_fs5.existsSync)(alanPath)) (0, import_node_fs5.unlinkSync)(alanPath);
24298
+ return false;
24299
+ }
24300
+ }
24301
+ function resolveAgentConfigPath(input) {
24302
+ if (input?.explicitPath?.trim()) return input.explicitPath.trim();
24303
+ const profile = normalizeAgentEndpointProfile(
24304
+ typeof input?.profile === "string" ? input.profile : input?.profile ?? null
24305
+ ) ?? "production";
24306
+ const baseDir = input?.baseDir ?? DEFAULT_AGENT_DIR;
24307
+ if (input?.baseDir === void 0) migrateLegacyAgentConfig({ profile });
24308
+ return (0, import_node_path5.join)(baseDir, resolveAgentConfigFileName(profile));
24309
+ }
24310
+ function configExistsAtPath(configPath) {
24311
+ return (0, import_node_fs5.existsSync)(configPath);
24312
+ }
24313
+ function resolveEndpointProfileFromArgs(args) {
24314
+ const index = args.indexOf("--profile");
24315
+ const raw = index === -1 ? process.env.ALAN_AGENT_PROFILE : args[index + 1];
24316
+ return normalizeAgentEndpointProfile(raw) ?? "production";
24317
+ }
24318
+ function bindAgentConfigPathFromArgs(args) {
24319
+ const explicit = process.env.ALAN_AGENT_CONFIG_PATH?.trim();
24320
+ if (explicit) return explicit;
24321
+ const profile = resolveEndpointProfileFromArgs(args);
24322
+ const configPath = resolveAgentConfigPath({ profile });
24323
+ process.env.ALAN_AGENT_CONFIG_PATH = configPath;
24324
+ return configPath;
24325
+ }
24326
+
24327
+ // src/computer-readiness.ts
24328
+ var WorkspaceAccessTracker = class {
24329
+ constructor(inspect = (path) => inspectWorkspaceReadiness(path)) {
24330
+ this.inspect = inspect;
24331
+ }
24332
+ issue = null;
24333
+ track(path, readiness) {
24334
+ if (readiness.ready) {
24335
+ this.issue = null;
24336
+ return;
24337
+ }
24338
+ this.issue = { path, readiness };
24339
+ }
24340
+ recheck() {
24341
+ if (!this.issue) return null;
24342
+ const readiness = this.inspect(this.issue.path);
24343
+ if (readiness.ready) {
24344
+ this.issue = null;
24345
+ return readiness;
24346
+ }
24347
+ this.issue = { path: this.issue.path, readiness };
23215
24348
  return readiness;
23216
24349
  }
23217
24350
  recoveryPath() {
@@ -23222,9 +24355,9 @@ var WorkspaceAccessTracker = class {
23222
24355
  }
23223
24356
  };
23224
24357
  var DEFAULT_WORKSPACE_PROBE = {
23225
- stat: (path) => (0, import_node_fs5.statSync)(path),
23226
- access: (path, mode) => (0, import_node_fs5.accessSync)(path, mode),
23227
- exists: (path) => (0, import_node_fs5.existsSync)(path)
24358
+ stat: (path) => (0, import_node_fs6.statSync)(path),
24359
+ access: (path, mode) => (0, import_node_fs6.accessSync)(path, mode),
24360
+ exists: (path) => (0, import_node_fs6.existsSync)(path)
23228
24361
  };
23229
24362
  function workspaceVolumeRoot(path) {
23230
24363
  const windowsDrive = path.match(/^([A-Za-z]):[\\/]/);
@@ -23288,7 +24421,7 @@ function inspectWorkspaceReadiness(workspacePath, probe = DEFAULT_WORKSPACE_PROB
23288
24421
  probe
23289
24422
  );
23290
24423
  }
23291
- probe.access(workspacePath, import_node_fs5.constants.R_OK | import_node_fs5.constants.W_OK | import_node_fs5.constants.X_OK);
24424
+ probe.access(workspacePath, import_node_fs6.constants.R_OK | import_node_fs6.constants.W_OK | import_node_fs6.constants.X_OK);
23292
24425
  return {
23293
24426
  ready: true,
23294
24427
  message: "Workspace path is configured and accessible.",
@@ -23400,7 +24533,7 @@ function buildComputerReadiness(input) {
23400
24533
  }
23401
24534
 
23402
24535
  // src/core-agent.ts
23403
- var import_node_os6 = require("os");
24536
+ var import_node_os7 = require("os");
23404
24537
  var CoreAgent = class _CoreAgent extends BaseMachineAgent {
23405
24538
  childProcess = null;
23406
24539
  _aborted = false;
@@ -23550,7 +24683,7 @@ var CoreAgent = class _CoreAgent extends BaseMachineAgent {
23550
24683
  if (this.runtime.backendKind === "grok_cli" && this.providerApiKey) {
23551
24684
  augmented.XAI_API_KEY = this.providerApiKey;
23552
24685
  }
23553
- if ((0, import_node_os6.platform)() !== "win32") {
24686
+ if ((0, import_node_os7.platform)() !== "win32") {
23554
24687
  const home = augmented.HOME ?? "/home/user";
23555
24688
  const extraPaths = [`${home}/.local/node/bin`, `${home}/.local/bin`];
23556
24689
  const separator = ":";
@@ -23714,9 +24847,9 @@ ${meta.description}`);
23714
24847
  };
23715
24848
 
23716
24849
  // src/event-outbox.ts
23717
- var import_node_crypto = require("crypto");
23718
- var import_node_fs6 = require("fs");
23719
- var import_node_path5 = require("path");
24850
+ var import_node_crypto2 = require("crypto");
24851
+ var import_node_fs7 = require("fs");
24852
+ var import_node_path6 = require("path");
23720
24853
  var OUTBOX_VERSION = 1;
23721
24854
  var MAX_INTERMEDIATE_EVENTS_PER_RUN = 200;
23722
24855
  var MAX_ENCRYPTED_OUTBOX_BYTES = 64 * 1024 * 1024;
@@ -23749,7 +24882,7 @@ function truncateUtf8(value2, maxBytes) {
23749
24882
  return `${encoded.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "")}\u2026`;
23750
24883
  }
23751
24884
  function terminalFingerprint(event) {
23752
- return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(event)).digest("hex").slice(0, 16);
24885
+ return (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(event)).digest("hex").slice(0, 16);
23753
24886
  }
23754
24887
  function condenseTerminalEvent(event) {
23755
24888
  const fingerprint = terminalFingerprint(event);
@@ -23827,7 +24960,7 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
23827
24960
  throw new Error("event outbox maxIntermediateEventsPerRun must be a non-negative integer");
23828
24961
  }
23829
24962
  this.entries = this.read();
23830
- if (!(0, import_node_fs6.existsSync)(this.path)) {
24963
+ if (!(0, import_node_fs7.existsSync)(this.path)) {
23831
24964
  this.lastPersistedSignature = _EncryptedEventOutbox.EMPTY_SIGNATURE;
23832
24965
  } else if (this.entries.length > 0) {
23833
24966
  this.lastPersistedSignature = this.serializeForPersist().signature;
@@ -23857,7 +24990,7 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
23857
24990
  maxIntermediateEventsPerRun;
23858
24991
  enqueue(conversationId, runId, event) {
23859
24992
  const entry = {
23860
- eventId: (0, import_node_crypto.randomUUID)(),
24993
+ eventId: (0, import_node_crypto2.randomUUID)(),
23861
24994
  conversationId,
23862
24995
  runId,
23863
24996
  event,
@@ -23943,7 +25076,7 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
23943
25076
  const markerConversationId = runEntry?.conversationId ?? conversationId;
23944
25077
  if (!markerConversationId) return;
23945
25078
  const marker = {
23946
- eventId: (0, import_node_crypto.randomUUID)(),
25079
+ eventId: (0, import_node_crypto2.randomUUID)(),
23947
25080
  conversationId: markerConversationId,
23948
25081
  runId,
23949
25082
  event: { type: "text", text: CONDENSATION_NOTICE, ts: Date.now() },
@@ -24029,14 +25162,14 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
24029
25162
  this.retryTimer = null;
24030
25163
  }
24031
25164
  read() {
24032
- if (!(0, import_node_fs6.existsSync)(this.path)) return [];
25165
+ if (!(0, import_node_fs7.existsSync)(this.path)) return [];
24033
25166
  let envelope;
24034
25167
  try {
24035
- envelope = JSON.parse((0, import_node_fs6.readFileSync)(this.path, "utf8"));
25168
+ envelope = JSON.parse((0, import_node_fs7.readFileSync)(this.path, "utf8"));
24036
25169
  if (envelope.version !== OUTBOX_VERSION) {
24037
25170
  throw new Error(`unsupported event outbox version ${String(envelope.version)}`);
24038
25171
  }
24039
- const decipher = (0, import_node_crypto.createDecipheriv)(
25172
+ const decipher = (0, import_node_crypto2.createDecipheriv)(
24040
25173
  "aes-256-gcm",
24041
25174
  this.key,
24042
25175
  Buffer.from(envelope.iv, "base64url")
@@ -24060,21 +25193,21 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
24060
25193
  return { plaintext: "", signature: _EncryptedEventOutbox.EMPTY_SIGNATURE };
24061
25194
  }
24062
25195
  const plaintext = JSON.stringify({ entries: this.entries });
24063
- return { plaintext, signature: (0, import_node_crypto.createHash)("sha256").update(plaintext).digest("base64url") };
25196
+ return { plaintext, signature: (0, import_node_crypto2.createHash)("sha256").update(plaintext).digest("base64url") };
24064
25197
  }
24065
25198
  persist() {
24066
25199
  const { plaintext, signature } = this.serializeForPersist();
24067
25200
  if (signature === this.lastPersistedSignature) return;
24068
25201
  try {
24069
25202
  if (this.entries.length === 0) {
24070
- (0, import_node_fs6.rmSync)(this.path, { force: true });
25203
+ (0, import_node_fs7.rmSync)(this.path, { force: true });
24071
25204
  this.lastPersistedSignature = signature;
24072
25205
  this.persistFailureLogged = false;
24073
25206
  return;
24074
25207
  }
24075
- (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(this.path), { recursive: true, mode: 448 });
24076
- const iv = (0, import_node_crypto.randomBytes)(12);
24077
- const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", this.key, iv);
25208
+ (0, import_node_fs7.mkdirSync)((0, import_node_path6.dirname)(this.path), { recursive: true, mode: 448 });
25209
+ const iv = (0, import_node_crypto2.randomBytes)(12);
25210
+ const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", this.key, iv);
24078
25211
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
24079
25212
  const envelope = {
24080
25213
  version: OUTBOX_VERSION,
@@ -24082,13 +25215,13 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
24082
25215
  authTag: cipher.getAuthTag().toString("base64url"),
24083
25216
  ciphertext: ciphertext.toString("base64url")
24084
25217
  };
24085
- const pendingPath = `${this.path}.pending-${process.pid}-${(0, import_node_crypto.randomBytes)(6).toString("hex")}`;
25218
+ const pendingPath = `${this.path}.pending-${process.pid}-${(0, import_node_crypto2.randomBytes)(6).toString("hex")}`;
24086
25219
  try {
24087
- (0, import_node_fs6.writeFileSync)(pendingPath, JSON.stringify(envelope), { mode: 384 });
24088
- (0, import_node_fs6.chmodSync)(pendingPath, 384);
24089
- (0, import_node_fs6.renameSync)(pendingPath, this.path);
25220
+ (0, import_node_fs7.writeFileSync)(pendingPath, JSON.stringify(envelope), { mode: 384 });
25221
+ (0, import_node_fs7.chmodSync)(pendingPath, 384);
25222
+ (0, import_node_fs7.renameSync)(pendingPath, this.path);
24090
25223
  } finally {
24091
- (0, import_node_fs6.rmSync)(pendingPath, { force: true });
25224
+ (0, import_node_fs7.rmSync)(pendingPath, { force: true });
24092
25225
  }
24093
25226
  this.lastPersistedSignature = signature;
24094
25227
  this.persistFailureLogged = false;
@@ -24106,38 +25239,38 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
24106
25239
  };
24107
25240
 
24108
25241
  // src/local-skill-scan.ts
24109
- var import_node_fs7 = require("fs");
24110
- var import_node_path6 = require("path");
25242
+ var import_node_fs8 = require("fs");
25243
+ var import_node_path7 = require("path");
24111
25244
  var SKILL_READ_LIMIT_BYTES = 8192;
24112
25245
  var SKILL_CACHE_TTL_MS = 3e4;
24113
25246
  function readSkillHead(path) {
24114
- const fd = (0, import_node_fs7.openSync)(path, "r");
25247
+ const fd = (0, import_node_fs8.openSync)(path, "r");
24115
25248
  try {
24116
25249
  const buffer = Buffer.alloc(SKILL_READ_LIMIT_BYTES);
24117
- const bytesRead = (0, import_node_fs7.readSync)(fd, buffer, 0, SKILL_READ_LIMIT_BYTES, 0);
25250
+ const bytesRead = (0, import_node_fs8.readSync)(fd, buffer, 0, SKILL_READ_LIMIT_BYTES, 0);
24118
25251
  return buffer.subarray(0, bytesRead).toString("utf-8");
24119
25252
  } finally {
24120
- (0, import_node_fs7.closeSync)(fd);
25253
+ (0, import_node_fs8.closeSync)(fd);
24121
25254
  }
24122
25255
  }
24123
25256
  function scanSkillDir(dirPath, source, repoRoot, entries, errors) {
24124
- if (!(0, import_node_fs7.existsSync)(dirPath)) return;
25257
+ if (!(0, import_node_fs8.existsSync)(dirPath)) return;
24125
25258
  let names;
24126
25259
  try {
24127
- names = (0, import_node_fs7.readdirSync)(dirPath);
25260
+ names = (0, import_node_fs8.readdirSync)(dirPath);
24128
25261
  } catch (error) {
24129
25262
  errors.push(`Failed to scan ${dirPath}: ${error instanceof Error ? error.message : error}`);
24130
25263
  return;
24131
25264
  }
24132
25265
  for (const name of names) {
24133
- const skillPath = (0, import_node_path6.join)(dirPath, name, "SKILL.md");
25266
+ const skillPath = (0, import_node_path7.join)(dirPath, name, "SKILL.md");
24134
25267
  let isDirectory = false;
24135
25268
  try {
24136
- isDirectory = (0, import_node_fs7.statSync)((0, import_node_path6.join)(dirPath, name)).isDirectory();
25269
+ isDirectory = (0, import_node_fs8.statSync)((0, import_node_path7.join)(dirPath, name)).isDirectory();
24137
25270
  } catch {
24138
25271
  continue;
24139
25272
  }
24140
- if (!isDirectory || !(0, import_node_fs7.existsSync)(skillPath)) continue;
25273
+ if (!isDirectory || !(0, import_node_fs8.existsSync)(skillPath)) continue;
24141
25274
  try {
24142
25275
  entries.push({ content: readSkillHead(skillPath), source, sourcePath: skillPath, repoRoot });
24143
25276
  } catch (error) {
@@ -24148,16 +25281,16 @@ function scanSkillDir(dirPath, source, repoRoot, entries, errors) {
24148
25281
  function discoverChildRepos(parentPath) {
24149
25282
  let names;
24150
25283
  try {
24151
- names = (0, import_node_fs7.readdirSync)(parentPath);
25284
+ names = (0, import_node_fs8.readdirSync)(parentPath);
24152
25285
  } catch {
24153
25286
  return [];
24154
25287
  }
24155
25288
  const children = [];
24156
25289
  for (const name of names) {
24157
25290
  if (name.startsWith(".")) continue;
24158
- const childPath = (0, import_node_path6.join)(parentPath, name);
25291
+ const childPath = (0, import_node_path7.join)(parentPath, name);
24159
25292
  try {
24160
- if ((0, import_node_fs7.statSync)(childPath).isDirectory()) children.push(childPath);
25293
+ if ((0, import_node_fs8.statSync)(childPath).isDirectory()) children.push(childPath);
24161
25294
  } catch {
24162
25295
  }
24163
25296
  }
@@ -24167,12 +25300,12 @@ function scanLocalSkills(homeDir, projectPath) {
24167
25300
  const rawEntries = [];
24168
25301
  const errors = [];
24169
25302
  for (const root of SKILL_DIRECTORY_ROOTS) {
24170
- scanSkillDir((0, import_node_path6.join)(homeDir, root), "global", void 0, rawEntries, errors);
25303
+ scanSkillDir((0, import_node_path7.join)(homeDir, root), "global", void 0, rawEntries, errors);
24171
25304
  }
24172
25305
  if (projectPath) {
24173
25306
  for (const dir of [projectPath, ...discoverChildRepos(projectPath)]) {
24174
25307
  for (const root of SKILL_DIRECTORY_ROOTS) {
24175
- scanSkillDir((0, import_node_path6.join)(dir, root), "team", dir, rawEntries, errors);
25308
+ scanSkillDir((0, import_node_path7.join)(dir, root), "team", dir, rawEntries, errors);
24176
25309
  }
24177
25310
  }
24178
25311
  }
@@ -24191,11 +25324,151 @@ function scanLocalSkillsCached(homeDir, projectPath, options) {
24191
25324
  return result;
24192
25325
  }
24193
25326
 
25327
+ // src/local-workspace-migration.ts
25328
+ var import_node_crypto3 = require("crypto");
25329
+ var MigrationStashApplyError = class extends Error {
25330
+ stash;
25331
+ stashPreserved = true;
25332
+ constructor(stash, cause) {
25333
+ super(`Could not apply migration stash ${stash.oid}; the stash was preserved`, { cause });
25334
+ this.name = "MigrationStashApplyError";
25335
+ this.stash = stash;
25336
+ }
25337
+ };
25338
+ var BRANCH_LABEL_MAX_LENGTH = 256;
25339
+ function stripControlCharacters(value2) {
25340
+ return Array.from(value2).filter((character) => {
25341
+ const code = character.charCodeAt(0);
25342
+ return code >= 32 && code !== 127;
25343
+ }).join("");
25344
+ }
25345
+ function sanitizeLabel(value2, fallback) {
25346
+ const sanitized = stripControlCharacters(value2).replace(/[^a-zA-Z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, BRANCH_LABEL_MAX_LENGTH);
25347
+ return sanitized || fallback;
25348
+ }
25349
+ function sanitizeBranch(value2) {
25350
+ const sanitized = stripControlCharacters(value2.trim()).slice(0, BRANCH_LABEL_MAX_LENGTH);
25351
+ return sanitized || null;
25352
+ }
25353
+ function countPorcelainEntries(output) {
25354
+ const records = output.split("\0");
25355
+ let count = 0;
25356
+ for (let index = 0; index < records.length; index += 1) {
25357
+ const record = records[index];
25358
+ if (!record) continue;
25359
+ count += 1;
25360
+ const status = record.slice(0, 2);
25361
+ if (status.includes("R") || status.includes("C")) {
25362
+ index += 1;
25363
+ }
25364
+ }
25365
+ return count;
25366
+ }
25367
+ function preflightGitTree(cwd, git2) {
25368
+ const currentBranch = sanitizeBranch(git2(cwd, ["branch", "--show-current"]));
25369
+ const dirtyFileCount = countPorcelainEntries(
25370
+ git2(cwd, ["status", "--porcelain=v1", "-z", "--untracked-files=all"])
25371
+ );
25372
+ return {
25373
+ currentBranch,
25374
+ dirtyFileCount,
25375
+ isDirty: dirtyFileCount > 0
25376
+ };
25377
+ }
25378
+ function preflightWorkspaceMigration(input, git2) {
25379
+ return {
25380
+ direction: input.direction,
25381
+ source: preflightGitTree(input.sourcePath, git2),
25382
+ destination: input.destinationPath ? preflightGitTree(input.destinationPath, git2) : null
25383
+ };
25384
+ }
25385
+ function uniqueOperationLabel(operation, dependencies) {
25386
+ const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
25387
+ const createdAt = now.toISOString();
25388
+ const id = (dependencies.createId ?? import_node_crypto3.randomUUID)();
25389
+ return {
25390
+ label: [
25391
+ "alan-migration",
25392
+ sanitizeLabel(operation, "workspace"),
25393
+ createdAt.replace(/[:.]/g, "-"),
25394
+ sanitizeLabel(id, "operation")
25395
+ ].join("/"),
25396
+ createdAt
25397
+ };
25398
+ }
25399
+ function createMigrationStash(input, dependencies) {
25400
+ const before = preflightGitTree(input.cwd, dependencies.git);
25401
+ if (!before.isDirty) return null;
25402
+ const { label, createdAt } = uniqueOperationLabel(input.operation, dependencies);
25403
+ dependencies.git(input.cwd, ["stash", "push", "--include-untracked", "--message", label]);
25404
+ const oid = dependencies.git(input.cwd, ["rev-parse", "--verify", "refs/stash"]).trim();
25405
+ if (!oid) {
25406
+ throw new Error("Git did not return the created migration stash");
25407
+ }
25408
+ return { oid, label, createdAt };
25409
+ }
25410
+ function findStashRef(cwd, oid, git2) {
25411
+ const matchingLine = git2(cwd, ["stash", "list", "--format=%H %gd"]).split("\n").find((line) => line.startsWith(`${oid} `));
25412
+ return matchingLine?.slice(oid.length + 1).trim() || null;
25413
+ }
25414
+ function dropMigrationStash(cwd, stash, git2) {
25415
+ const stashRef = findStashRef(cwd, stash.oid, git2);
25416
+ if (!stashRef) return false;
25417
+ git2(cwd, ["stash", "drop", stashRef]);
25418
+ return true;
25419
+ }
25420
+ function applyMigrationStash(input, git2) {
25421
+ try {
25422
+ git2(input.cwd, ["stash", "apply", "--index", input.stash.oid]);
25423
+ } catch (cause) {
25424
+ throw new MigrationStashApplyError(input.stash, cause);
25425
+ }
25426
+ if (input.dropOnSuccess !== false) {
25427
+ dropMigrationStash(input.cwd, input.stash, git2);
25428
+ }
25429
+ return preflightGitTree(input.cwd, git2);
25430
+ }
25431
+ function resolveDirtyTree(input, dependencies) {
25432
+ if (input.action === "discard" && input.confirmDiscard !== true) {
25433
+ throw new Error("Discarding workspace changes requires explicit confirmation");
25434
+ }
25435
+ const before = preflightGitTree(input.cwd, dependencies.git);
25436
+ let stash = null;
25437
+ let commitOid = null;
25438
+ if (before.isDirty) {
25439
+ switch (input.action) {
25440
+ case "leave":
25441
+ break;
25442
+ case "move_via_stash":
25443
+ stash = createMigrationStash({ cwd: input.cwd, operation: input.operation }, dependencies);
25444
+ break;
25445
+ case "commit_wip": {
25446
+ const { label } = uniqueOperationLabel(input.operation, dependencies);
25447
+ dependencies.git(input.cwd, ["add", "--all"]);
25448
+ dependencies.git(input.cwd, ["commit", "--message", `WIP: ${label}`]);
25449
+ commitOid = dependencies.git(input.cwd, ["rev-parse", "HEAD"]).trim() || null;
25450
+ break;
25451
+ }
25452
+ case "discard":
25453
+ dependencies.git(input.cwd, ["reset", "--hard", "HEAD"]);
25454
+ dependencies.git(input.cwd, ["clean", "-fd"]);
25455
+ break;
25456
+ }
25457
+ }
25458
+ return {
25459
+ action: input.action,
25460
+ before,
25461
+ after: preflightGitTree(input.cwd, dependencies.git),
25462
+ stash,
25463
+ commitOid
25464
+ };
25465
+ }
25466
+
24194
25467
  // src/mcp-registration.ts
24195
- var import_node_crypto2 = require("crypto");
24196
- var import_node_fs8 = require("fs");
24197
- var import_node_os7 = require("os");
24198
- var import_node_path7 = require("path");
25468
+ var import_node_crypto4 = require("crypto");
25469
+ var import_node_fs9 = require("fs");
25470
+ var import_node_os8 = require("os");
25471
+ var import_node_path8 = require("path");
24199
25472
 
24200
25473
  // ../../node_modules/smol-toml/dist/error.js
24201
25474
  function getLineColFromPtr(string, ptr) {
@@ -24277,7 +25550,7 @@ function skipVoid(str, ptr, banNewLines, banComments) {
24277
25550
  }
24278
25551
  return ptr;
24279
25552
  }
24280
- function skipUntil(str, ptr, sep2, end, banNewLines = false) {
25553
+ function skipUntil(str, ptr, sep3, end, banNewLines = false) {
24281
25554
  if (!end) {
24282
25555
  ptr = indexOfNewline(str, ptr);
24283
25556
  return ptr < 0 ? str.length : ptr;
@@ -24286,7 +25559,7 @@ function skipUntil(str, ptr, sep2, end, banNewLines = false) {
24286
25559
  let c = str[i];
24287
25560
  if (c === "#") {
24288
25561
  i = indexOfNewline(str, i);
24289
- } else if (c === sep2) {
25562
+ } else if (c === sep3) {
24290
25563
  return i + 1;
24291
25564
  } else if (c === end || banNewLines && (c === "\n" || c === "\r" && str[i + 1] === "\n")) {
24292
25565
  return i;
@@ -24992,18 +26265,18 @@ var inspectAlanMcpThroughProviderCli = async (backendKind, expectedUrl, home) =>
24992
26265
  return result;
24993
26266
  };
24994
26267
  async function waitForConfigLock(lockPath) {
24995
- (0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(lockPath), { recursive: true });
26268
+ (0, import_node_fs9.mkdirSync)((0, import_node_path8.dirname)(lockPath), { recursive: true });
24996
26269
  const startedAt = Date.now();
24997
26270
  while (true) {
24998
26271
  try {
24999
- (0, import_node_fs8.mkdirSync)(lockPath);
25000
- return () => (0, import_node_fs8.rmSync)(lockPath, { recursive: true, force: true });
26272
+ (0, import_node_fs9.mkdirSync)(lockPath);
26273
+ return () => (0, import_node_fs9.rmSync)(lockPath, { recursive: true, force: true });
25001
26274
  } catch (error) {
25002
26275
  const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
25003
26276
  if (code !== "EEXIST") throw error;
25004
26277
  try {
25005
- if (Date.now() - (0, import_node_fs8.statSync)(lockPath).mtimeMs > CONFIG_LOCK_STALE_MS) {
25006
- (0, import_node_fs8.rmSync)(lockPath, { recursive: true, force: true });
26278
+ if (Date.now() - (0, import_node_fs9.statSync)(lockPath).mtimeMs > CONFIG_LOCK_STALE_MS) {
26279
+ (0, import_node_fs9.rmSync)(lockPath, { recursive: true, force: true });
25007
26280
  continue;
25008
26281
  }
25009
26282
  } catch {
@@ -25012,14 +26285,14 @@ async function waitForConfigLock(lockPath) {
25012
26285
  if (Date.now() - startedAt >= CONFIG_LOCK_WAIT_MS) {
25013
26286
  throw new Error(`Alan could not safely update ${lockPath.slice(0, -".alan-lock".length)}`);
25014
26287
  }
25015
- await new Promise((resolve6) => setTimeout(resolve6, CONFIG_LOCK_POLL_MS));
26288
+ await new Promise((resolve7) => setTimeout(resolve7, CONFIG_LOCK_POLL_MS));
25016
26289
  }
25017
26290
  }
25018
26291
  }
25019
26292
  function saveMalformedBackup(path, content) {
25020
- const hash = (0, import_node_crypto2.createHash)("sha256").update(content).digest("hex").slice(0, 12);
26293
+ const hash = (0, import_node_crypto4.createHash)("sha256").update(content).digest("hex").slice(0, 12);
25021
26294
  const backupPath = `${path}.alan-backup-${hash}`;
25022
- if (!(0, import_node_fs8.existsSync)(backupPath)) (0, import_node_fs8.copyFileSync)(path, backupPath);
26295
+ if (!(0, import_node_fs9.existsSync)(backupPath)) (0, import_node_fs9.copyFileSync)(path, backupPath);
25023
26296
  return backupPath;
25024
26297
  }
25025
26298
  function parseJsonObject2(path, content) {
@@ -25064,24 +26337,24 @@ function mergeCodexAlanSection(path, existingContent, desiredContent) {
25064
26337
  return merged;
25065
26338
  }
25066
26339
  function atomicWriteManagedConfig(path, content) {
25067
- const pendingPath = `${path}.alan-pending-${process.pid}-${(0, import_node_crypto2.randomBytes)(6).toString("hex")}`;
26340
+ const pendingPath = `${path}.alan-pending-${process.pid}-${(0, import_node_crypto4.randomBytes)(6).toString("hex")}`;
25068
26341
  try {
25069
- (0, import_node_fs8.writeFileSync)(pendingPath, content, { encoding: "utf8", mode: 384, flag: "wx" });
25070
- (0, import_node_fs8.renameSync)(pendingPath, path);
26342
+ (0, import_node_fs9.writeFileSync)(pendingPath, content, { encoding: "utf8", mode: 384, flag: "wx" });
26343
+ (0, import_node_fs9.renameSync)(pendingPath, path);
25071
26344
  } finally {
25072
- (0, import_node_fs8.rmSync)(pendingPath, { force: true });
26345
+ (0, import_node_fs9.rmSync)(pendingPath, { force: true });
25073
26346
  }
25074
26347
  }
25075
- async function ensureAlanMcpRegistered(backendKind, registration, home = (0, import_node_os7.homedir)(), inspectCli = inspectAlanMcpThroughProviderCli) {
26348
+ async function ensureAlanMcpRegistered(backendKind, registration, home = (0, import_node_os8.homedir)(), inspectCli = inspectAlanMcpThroughProviderCli) {
25076
26349
  try {
25077
26350
  const mcpServers = buildAlanMcpServers(registration);
25078
26351
  const files = alanMcpConfigFilesForBackend(backendKind, mcpServers, home);
25079
26352
  const paths = [];
25080
26353
  for (const file of files) {
25081
- (0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(file.path), { recursive: true });
26354
+ (0, import_node_fs9.mkdirSync)((0, import_node_path8.dirname)(file.path), { recursive: true });
25082
26355
  const releaseLock = await waitForConfigLock(`${file.path}.alan-lock`);
25083
26356
  try {
25084
- const current = (0, import_node_fs8.existsSync)(file.path) ? (0, import_node_fs8.readFileSync)(file.path, "utf8") : null;
26357
+ const current = (0, import_node_fs9.existsSync)(file.path) ? (0, import_node_fs9.readFileSync)(file.path, "utf8") : null;
25085
26358
  const content = file.path.endsWith(".toml") ? mergeCodexAlanSection(file.path, current, file.content) : mergeJsonConfig(file.path, current, file.content);
25086
26359
  if (current !== content) atomicWriteManagedConfig(file.path, content);
25087
26360
  paths.push(file.path);
@@ -25132,13 +26405,13 @@ async function ensureAlanMcpRegistered(backendKind, registration, home = (0, imp
25132
26405
  return { ok: false, paths: [], error: err instanceof Error ? err.message : String(err) };
25133
26406
  }
25134
26407
  }
25135
- async function verifyAlanMcpRegistered(backendKind, home = (0, import_node_os7.homedir)(), inspectCli = inspectAlanMcpThroughProviderCli) {
26408
+ async function verifyAlanMcpRegistered(backendKind, home = (0, import_node_os8.homedir)(), inspectCli = inspectAlanMcpThroughProviderCli) {
25136
26409
  const files = alanMcpConfigFilesForBackend(backendKind, {}, home);
25137
- const missing = files.map((f) => f.path).filter((p) => !(0, import_node_fs8.existsSync)(p));
26410
+ const missing = files.map((f) => f.path).filter((p) => !(0, import_node_fs9.existsSync)(p));
25138
26411
  const presentEntries = files.map((file) => ({
25139
26412
  path: file.path,
25140
- url: (0, import_node_fs8.existsSync)(file.path) ? readUsableAlanMcpUrl(file.path) : null
25141
- })).filter((entry) => (0, import_node_fs8.existsSync)(entry.path));
26413
+ url: (0, import_node_fs9.existsSync)(file.path) ? readUsableAlanMcpUrl(file.path) : null
26414
+ })).filter((entry) => (0, import_node_fs9.existsSync)(entry.path));
25142
26415
  let invalid = presentEntries.filter((entry) => !entry.url).map((entry) => entry.path);
25143
26416
  const urls = new Set(presentEntries.flatMap((entry) => entry.url ? [entry.url] : []));
25144
26417
  if (urls.size > 1) invalid = presentEntries.map((entry) => entry.path);
@@ -25194,15 +26467,15 @@ function removeAlanFromJsonConfig(path, existingContent) {
25194
26467
  }
25195
26468
  return JSON.stringify(config);
25196
26469
  }
25197
- async function unregisterAlanMcp(backendKind, home = (0, import_node_os7.homedir)()) {
26470
+ async function unregisterAlanMcp(backendKind, home = (0, import_node_os8.homedir)()) {
25198
26471
  const removedPaths = [];
25199
26472
  const errors = [];
25200
26473
  const files = alanMcpConfigFilesForBackend(backendKind, {}, home);
25201
26474
  for (const file of files) {
25202
- if (!(0, import_node_fs8.existsSync)(file.path)) continue;
26475
+ if (!(0, import_node_fs9.existsSync)(file.path)) continue;
25203
26476
  const releaseLock = await waitForConfigLock(`${file.path}.alan-lock`);
25204
26477
  try {
25205
- const current = (0, import_node_fs8.readFileSync)(file.path, "utf8");
26478
+ const current = (0, import_node_fs9.readFileSync)(file.path, "utf8");
25206
26479
  let next;
25207
26480
  if (file.path.endsWith(".toml")) {
25208
26481
  const stripped = stripCodexAlanMcpSection(current);
@@ -25233,7 +26506,7 @@ function isHttpUrl(value2) {
25233
26506
  }
25234
26507
  }
25235
26508
  function readUsableAlanMcpUrl(path) {
25236
- const content = (0, import_node_fs8.readFileSync)(path, "utf8");
26509
+ const content = (0, import_node_fs9.readFileSync)(path, "utf8");
25237
26510
  if (path.endsWith(".toml")) {
25238
26511
  const lines = content.split("\n");
25239
26512
  const start = lines.findIndex((line) => line.trim() === "[mcp_servers.alan]");
@@ -25429,6 +26702,33 @@ function classifyTransportFailure(error) {
25429
26702
  return "backend_unreachable";
25430
26703
  }
25431
26704
 
26705
+ // src/observable-map.ts
26706
+ var ObservableMap = class extends Map {
26707
+ /**
26708
+ * Invoked after every membership change.
26709
+ *
26710
+ * Assigned after construction rather than passed to the constructor because
26711
+ * `Map`'s constructor may populate entries before subclass fields exist.
26712
+ */
26713
+ onChange = null;
26714
+ set(key, value2) {
26715
+ const existed = super.has(key);
26716
+ const result = super.set(key, value2);
26717
+ if (!existed) this.onChange?.();
26718
+ return result;
26719
+ }
26720
+ delete(key) {
26721
+ const removed = super.delete(key);
26722
+ if (removed) this.onChange?.();
26723
+ return removed;
26724
+ }
26725
+ clear() {
26726
+ const had = this.size > 0;
26727
+ super.clear();
26728
+ if (had) this.onChange?.();
26729
+ }
26730
+ };
26731
+
25432
26732
  // src/provider-auth-preflight.ts
25433
26733
  var AUTH_PROBES = {
25434
26734
  claude_cli: {
@@ -25524,7 +26824,7 @@ ${result.stderr}`.trim();
25524
26824
  }
25525
26825
 
25526
26826
  // src/provider-model-discovery.ts
25527
- var import_node_fs9 = require("fs");
26827
+ var import_node_fs10 = require("fs");
25528
26828
  var MODEL_DISCOVERY_TIMEOUT_MS = 8e3;
25529
26829
  var ANTIGRAVITY_MODEL_DISCOVERY_TIMEOUT_MS = 12e3;
25530
26830
  var OPENCODE_ZEN_MODEL_IDS = OPENCODE_ZEN_MODELS.map((model) => model.id);
@@ -25738,7 +27038,7 @@ function filterClaudeDatedModelIds(ids) {
25738
27038
  }
25739
27039
  function resolveExecutableForModelScan(executable) {
25740
27040
  try {
25741
- return (0, import_node_fs9.realpathSync)(executable);
27041
+ return (0, import_node_fs10.realpathSync)(executable);
25742
27042
  } catch {
25743
27043
  return executable;
25744
27044
  }
@@ -25746,7 +27046,7 @@ function resolveExecutableForModelScan(executable) {
25746
27046
  function extractClaudeModelIdsFromExecutable(executable) {
25747
27047
  try {
25748
27048
  const resolved = resolveExecutableForModelScan(executable);
25749
- const text = (0, import_node_fs9.readFileSync)(resolved).toString("latin1");
27049
+ const text = (0, import_node_fs10.readFileSync)(resolved).toString("latin1");
25750
27050
  const matches = text.match(CLAUDE_MODEL_ID_PATTERN) ?? [];
25751
27051
  const unique = [
25752
27052
  ...new Set(matches.map((id) => id.trim()).filter((id) => isValidClaudeModelId(id))),
@@ -25873,8 +27173,8 @@ async function discoverProviderModels(provider, executable, env) {
25873
27173
  }
25874
27174
 
25875
27175
  // src/run-journal.ts
25876
- var import_node_fs10 = require("fs");
25877
- var import_node_path8 = require("path");
27176
+ var import_node_fs11 = require("fs");
27177
+ var import_node_path9 = require("path");
25878
27178
  var JOURNAL_VERSION = 1;
25879
27179
  var INTERRUPTED_ENTRY_TTL_MS = 24 * 60 * 6e4;
25880
27180
  function isEntry(value2) {
@@ -25893,9 +27193,9 @@ function isEntry(value2) {
25893
27193
  var RuntimeRunJournal = class {
25894
27194
  constructor(path, nowMs = Date.now()) {
25895
27195
  this.path = path;
25896
- if (!(0, import_node_fs10.existsSync)(path)) return;
27196
+ if (!(0, import_node_fs11.existsSync)(path)) return;
25897
27197
  try {
25898
- const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
27198
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
25899
27199
  if (parsed.version !== JOURNAL_VERSION || !Array.isArray(parsed.entries)) {
25900
27200
  throw new Error("unsupported run journal format");
25901
27201
  }
@@ -26002,71 +27302,384 @@ var RuntimeRunJournal = class {
26002
27302
  providerPid: entry.providerPid
26003
27303
  };
26004
27304
  }
26005
- remove(runId) {
26006
- const previous = this.entries.get(runId);
26007
- if (!previous) return;
26008
- const wasRecovered = this.recoveredIds.has(runId);
26009
- this.entries.delete(runId);
26010
- this.recoveredIds.delete(runId);
26011
- try {
26012
- this.persist();
26013
- } catch (error) {
26014
- this.entries.set(runId, previous);
26015
- if (wasRecovered) this.recoveredIds.add(runId);
26016
- throw error;
27305
+ remove(runId) {
27306
+ const previous = this.entries.get(runId);
27307
+ if (!previous) return;
27308
+ const wasRecovered = this.recoveredIds.has(runId);
27309
+ this.entries.delete(runId);
27310
+ this.recoveredIds.delete(runId);
27311
+ try {
27312
+ this.persist();
27313
+ } catch (error) {
27314
+ this.entries.set(runId, previous);
27315
+ if (wasRecovered) this.recoveredIds.add(runId);
27316
+ throw error;
27317
+ }
27318
+ }
27319
+ persist() {
27320
+ const pendingPath = `${this.path}.pending`;
27321
+ (0, import_node_fs11.mkdirSync)((0, import_node_path9.dirname)(this.path), { recursive: true });
27322
+ try {
27323
+ (0, import_node_fs11.writeFileSync)(
27324
+ pendingPath,
27325
+ `${JSON.stringify({ version: JOURNAL_VERSION, entries: [...this.entries.values()] })}
27326
+ `,
27327
+ { mode: 384 }
27328
+ );
27329
+ (0, import_node_fs11.chmodSync)(pendingPath, 384);
27330
+ (0, import_node_fs11.renameSync)(pendingPath, this.path);
27331
+ } catch (error) {
27332
+ (0, import_node_fs11.rmSync)(pendingPath, { force: true });
27333
+ throw error;
27334
+ }
27335
+ }
27336
+ };
27337
+
27338
+ // src/run-start-gate.ts
27339
+ var RunStartGate = class {
27340
+ tail = Promise.resolve();
27341
+ async acquire() {
27342
+ const previous = this.tail;
27343
+ let releaseCurrent;
27344
+ const current = new Promise((resolve7) => {
27345
+ releaseCurrent = resolve7;
27346
+ });
27347
+ this.tail = previous.then(() => current);
27348
+ await previous;
27349
+ let released = false;
27350
+ return () => {
27351
+ if (released) return;
27352
+ released = true;
27353
+ releaseCurrent();
27354
+ };
27355
+ }
27356
+ };
27357
+
27358
+ // src/sleep-inhibitor.ts
27359
+ var import_node_child_process5 = require("child_process");
27360
+ var SLEEP_PREVENTION_DISABLED_ENV = "ALAN_DISABLE_SLEEP_PREVENTION";
27361
+ var HEALTH_CHECK_INTERVAL_MS = 3e4;
27362
+ var noopLog = () => {
27363
+ };
27364
+ function isSleepPreventionDisabled(env = process.env) {
27365
+ const raw = env[SLEEP_PREVENTION_DISABLED_ENV];
27366
+ if (raw === void 0) return false;
27367
+ const normalized = raw.trim().toLowerCase();
27368
+ return normalized === "1" || normalized === "true" || normalized === "yes";
27369
+ }
27370
+ var ChildProcessBackend = class {
27371
+ child = null;
27372
+ /**
27373
+ * Set once a candidate command is proven absent (ENOENT). Prevents a spawn
27374
+ * storm from the health watchdog on machines that simply lack the binary.
27375
+ */
27376
+ unavailable = false;
27377
+ log;
27378
+ constructor(log) {
27379
+ this.log = log;
27380
+ }
27381
+ /**
27382
+ * Let a multi-candidate backend react when its active helper terminates.
27383
+ * This is not called for an intentional release because `release()` clears
27384
+ * the active child before signalling it.
27385
+ */
27386
+ onUnexpectedTermination() {
27387
+ }
27388
+ isHealthy() {
27389
+ if (this.unavailable) return true;
27390
+ const child = this.child;
27391
+ return child !== null && child.exitCode === null && child.signalCode === null;
27392
+ }
27393
+ acquire() {
27394
+ if (this.unavailable) return;
27395
+ if (this.isHealthy() && this.child !== null) return;
27396
+ this.child = null;
27397
+ const command = this.command();
27398
+ if (command === null) {
27399
+ this.unavailable = true;
27400
+ this.log(`sleep-inhibitor ${this.name} unavailable: no supported command`);
27401
+ return;
27402
+ }
27403
+ let child;
27404
+ try {
27405
+ child = (0, import_node_child_process5.spawn)(command.file, command.args, { stdio: "ignore", detached: false });
27406
+ } catch (error) {
27407
+ this.unavailable = true;
27408
+ this.log(`sleep-inhibitor ${this.name} spawn threw: ${describeError(error)}`);
27409
+ return;
27410
+ }
27411
+ child.on("error", (error) => {
27412
+ const wasActiveChild = this.child === child;
27413
+ if (wasActiveChild) this.child = null;
27414
+ if (error.code === "ENOENT") this.unavailable = true;
27415
+ this.log(`sleep-inhibitor ${this.name} failed: ${describeError(error)}`);
27416
+ if (wasActiveChild) this.onUnexpectedTermination();
27417
+ });
27418
+ child.on("exit", (code, signal) => {
27419
+ const wasActiveChild = this.child === child;
27420
+ if (wasActiveChild) this.child = null;
27421
+ this.log(`sleep-inhibitor ${this.name} exited code=${code} signal=${signal}`);
27422
+ if (wasActiveChild) this.onUnexpectedTermination();
27423
+ });
27424
+ this.child = child;
27425
+ this.log(`sleep-inhibitor ${this.name} acquired pid=${child.pid}`);
27426
+ }
27427
+ release() {
27428
+ const child = this.child;
27429
+ if (child === null) return;
27430
+ this.child = null;
27431
+ try {
27432
+ child.kill("SIGTERM");
27433
+ } catch (error) {
27434
+ this.log(`sleep-inhibitor ${this.name} release failed: ${describeError(error)}`);
27435
+ }
27436
+ }
27437
+ };
27438
+ var MacCaffeinateBackend = class extends ChildProcessBackend {
27439
+ name = "caffeinate";
27440
+ command() {
27441
+ return { file: "/usr/bin/caffeinate", args: ["-i", "-s", "-w", String(process.pid)] };
27442
+ }
27443
+ };
27444
+ var LinuxIdleInhibitBackend = class extends ChildProcessBackend {
27445
+ name = "systemd-inhibit";
27446
+ candidateIndex = 0;
27447
+ candidateCount = 2;
27448
+ command() {
27449
+ const guard = `while kill -0 ${process.pid} 2>/dev/null; do sleep 5; done`;
27450
+ const candidates = [
27451
+ {
27452
+ file: "systemd-inhibit",
27453
+ args: [
27454
+ "--what=idle:sleep",
27455
+ "--mode=block",
27456
+ "--who=alan-agent",
27457
+ "--why=Alan is running an active agent run",
27458
+ "--",
27459
+ "sh",
27460
+ "-c",
27461
+ guard
27462
+ ]
27463
+ },
27464
+ {
27465
+ file: "gnome-session-inhibit",
27466
+ args: [
27467
+ "--inhibit",
27468
+ "idle",
27469
+ "--reason",
27470
+ "Alan is running an active agent run",
27471
+ "sh",
27472
+ "-c",
27473
+ guard
27474
+ ]
27475
+ }
27476
+ ];
27477
+ return candidates[this.candidateIndex] ?? null;
27478
+ }
27479
+ acquire() {
27480
+ const before = this.candidateIndex;
27481
+ super.acquire();
27482
+ if (this.unavailable && this.candidateIndex === before) {
27483
+ this.tryNextCandidate();
27484
+ }
27485
+ }
27486
+ onUnexpectedTermination() {
27487
+ this.tryNextCandidate();
27488
+ }
27489
+ tryNextCandidate() {
27490
+ this.candidateIndex += 1;
27491
+ if (this.candidateIndex >= this.candidateCount) return;
27492
+ this.unavailable = false;
27493
+ super.acquire();
27494
+ }
27495
+ };
27496
+ var WindowsPowerRequestBackend = class extends ChildProcessBackend {
27497
+ name = "windows-power-request";
27498
+ command() {
27499
+ const guard = `while (Get-Process -Id ${process.pid} -ErrorAction SilentlyContinue) { Start-Sleep -Seconds 5 }`;
27500
+ const script = [
27501
+ `Add-Type -Namespace Alan -Name Power -MemberDefinition '[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint esFlags);';`,
27502
+ "[Alan.Power]::SetThreadExecutionState(0x80000001) | Out-Null;",
27503
+ guard
27504
+ ].join(" ");
27505
+ return {
27506
+ file: "powershell.exe",
27507
+ args: ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", script]
27508
+ };
27509
+ }
27510
+ };
27511
+ var externalBackends = /* @__PURE__ */ new Set();
27512
+ function createPlatformBackends(log, hasExternalBackend) {
27513
+ switch (process.platform) {
27514
+ case "darwin":
27515
+ return [new MacCaffeinateBackend(log)];
27516
+ case "linux":
27517
+ return [new LinuxIdleInhibitBackend(log)];
27518
+ case "win32":
27519
+ return hasExternalBackend ? [] : [new WindowsPowerRequestBackend(log)];
27520
+ default:
27521
+ return [];
27522
+ }
27523
+ }
27524
+ var SleepInhibitor = class {
27525
+ backends;
27526
+ log;
27527
+ enabled;
27528
+ active = false;
27529
+ healthTimer = null;
27530
+ exitHandler = null;
27531
+ disposed = false;
27532
+ constructor(options = {}) {
27533
+ this.log = options.log ?? noopLog;
27534
+ this.enabled = !isSleepPreventionDisabled(options.env ?? process.env);
27535
+ this.backends = this.enabled ? [...createPlatformBackends(this.log, externalBackends.size > 0), ...externalBackends] : [];
27536
+ if (!this.enabled) {
27537
+ this.log(`sleep-inhibitor disabled via ${SLEEP_PREVENTION_DISABLED_ENV}`);
27538
+ }
27539
+ }
27540
+ isActive() {
27541
+ return this.active;
27542
+ }
27543
+ /** Names of the backends this inhibitor will drive. Exposed for diagnostics. */
27544
+ backendNames() {
27545
+ return this.backends.map((backend) => backend.name);
27546
+ }
27547
+ /**
27548
+ * Turn sleep prevention on or off. Safe to call repeatedly with the same
27549
+ * value; only edges do work.
27550
+ */
27551
+ setActive(active) {
27552
+ if (this.disposed) return;
27553
+ if (this.active === active) {
27554
+ if (active) this.healBackends();
27555
+ return;
27556
+ }
27557
+ this.active = active;
27558
+ if (active) this.start();
27559
+ else this.stop();
27560
+ }
27561
+ /** Release everything and detach timers/handlers. Idempotent. */
27562
+ dispose() {
27563
+ if (this.disposed) return;
27564
+ this.disposed = true;
27565
+ this.active = false;
27566
+ this.stop();
27567
+ }
27568
+ start() {
27569
+ if (this.backends.length === 0) return;
27570
+ this.healBackends();
27571
+ if (this.healthTimer === null) {
27572
+ this.healthTimer = setInterval(() => this.healBackends(), HEALTH_CHECK_INTERVAL_MS);
27573
+ this.healthTimer.unref?.();
27574
+ }
27575
+ if (this.exitHandler === null) {
27576
+ this.exitHandler = () => {
27577
+ for (const backend of this.backends) backend.release();
27578
+ };
27579
+ process.on("exit", this.exitHandler);
27580
+ }
27581
+ }
27582
+ stop() {
27583
+ if (this.healthTimer !== null) {
27584
+ clearInterval(this.healthTimer);
27585
+ this.healthTimer = null;
27586
+ }
27587
+ if (this.exitHandler !== null) {
27588
+ process.removeListener("exit", this.exitHandler);
27589
+ this.exitHandler = null;
27590
+ }
27591
+ for (const backend of this.backends) {
27592
+ try {
27593
+ backend.release();
27594
+ } catch (error) {
27595
+ this.log(`sleep-inhibitor release error: ${describeError(error)}`);
27596
+ }
26017
27597
  }
26018
27598
  }
26019
- persist() {
26020
- const pendingPath = `${this.path}.pending`;
26021
- (0, import_node_fs10.mkdirSync)((0, import_node_path8.dirname)(this.path), { recursive: true });
26022
- try {
26023
- (0, import_node_fs10.writeFileSync)(
26024
- pendingPath,
26025
- `${JSON.stringify({ version: JOURNAL_VERSION, entries: [...this.entries.values()] })}
26026
- `,
26027
- { mode: 384 }
26028
- );
26029
- (0, import_node_fs10.chmodSync)(pendingPath, 384);
26030
- (0, import_node_fs10.renameSync)(pendingPath, this.path);
26031
- } catch (error) {
26032
- (0, import_node_fs10.rmSync)(pendingPath, { force: true });
26033
- throw error;
27599
+ /**
27600
+ * Acquire anything not currently held. This is the self-healing step: a
27601
+ * backend whose helper died is respawned here.
27602
+ *
27603
+ * Healthy backends are skipped rather than re-acquired, so concurrent runs
27604
+ * can never stack assertions even if a backend's own `acquire()` is not
27605
+ * defensively idempotent.
27606
+ */
27607
+ healBackends() {
27608
+ for (const backend of this.backends) {
27609
+ try {
27610
+ if (backend.isHealthy()) continue;
27611
+ backend.acquire();
27612
+ } catch (error) {
27613
+ this.log(`sleep-inhibitor ${backend.name} acquire error: ${describeError(error)}`);
27614
+ }
26034
27615
  }
26035
27616
  }
26036
27617
  };
27618
+ function describeError(error) {
27619
+ return error instanceof Error ? error.message : String(error);
27620
+ }
26037
27621
 
26038
- // src/run-start-gate.ts
26039
- var RunStartGate = class {
26040
- tail = Promise.resolve();
26041
- async acquire() {
26042
- const previous = this.tail;
26043
- let releaseCurrent;
26044
- const current = new Promise((resolve6) => {
26045
- releaseCurrent = resolve6;
26046
- });
26047
- this.tail = previous.then(() => current);
26048
- await previous;
26049
- let released = false;
26050
- return () => {
26051
- if (released) return;
26052
- released = true;
26053
- releaseCurrent();
26054
- };
27622
+ // src/suspension-detector.ts
27623
+ var DEFAULT_CHECK_INTERVAL_MS = 15e3;
27624
+ var DEFAULT_THRESHOLD_MS = 6e4;
27625
+ var SuspensionDetector = class {
27626
+ onSuspensionDetected;
27627
+ checkIntervalMs;
27628
+ thresholdMs;
27629
+ now;
27630
+ setIntervalFn;
27631
+ clearIntervalFn;
27632
+ timer = null;
27633
+ lastTickAtMs = null;
27634
+ constructor(options) {
27635
+ this.onSuspensionDetected = options.onSuspensionDetected;
27636
+ this.checkIntervalMs = options.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
27637
+ this.thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
27638
+ this.now = options.now ?? Date.now;
27639
+ this.setIntervalFn = options.setIntervalFn ?? setInterval;
27640
+ this.clearIntervalFn = options.clearIntervalFn ?? clearInterval;
27641
+ }
27642
+ start() {
27643
+ if (this.timer !== null) return;
27644
+ this.lastTickAtMs = this.now();
27645
+ this.timer = this.setIntervalFn(() => this.tick(), this.checkIntervalMs);
27646
+ this.timer.unref?.();
27647
+ }
27648
+ stop() {
27649
+ if (this.timer === null) return;
27650
+ this.clearIntervalFn(this.timer);
27651
+ this.timer = null;
27652
+ this.lastTickAtMs = null;
27653
+ }
27654
+ isRunning() {
27655
+ return this.timer !== null;
27656
+ }
27657
+ tick() {
27658
+ const nowMs = this.now();
27659
+ const previousMs = this.lastTickAtMs;
27660
+ this.lastTickAtMs = nowMs;
27661
+ if (previousMs === null) return;
27662
+ const overshootMs = nowMs - previousMs - this.checkIntervalMs;
27663
+ if (overshootMs < this.thresholdMs) return;
27664
+ try {
27665
+ this.onSuspensionDetected(overshootMs);
27666
+ } catch {
27667
+ }
26055
27668
  }
26056
27669
  };
26057
27670
 
26058
27671
  // src/version.ts
26059
- var AGENT_VERSION = "0.1.50";
27672
+ var AGENT_VERSION = "0.1.52";
26060
27673
 
26061
27674
  // src/workspace-relocation.ts
26062
- var import_node_child_process4 = require("child_process");
26063
- var import_node_fs11 = require("fs");
26064
- var import_node_path9 = require("path");
27675
+ var import_node_child_process6 = require("child_process");
27676
+ var import_node_fs12 = require("fs");
27677
+ var import_node_path10 = require("path");
26065
27678
  var MAX_RELOCATION_CANDIDATES = 200;
26066
27679
  var DEFAULT_PROBE = {
26067
- childDirectories: (parentPath, limit) => (0, import_node_fs11.readdirSync)(parentPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).slice(0, limit).map((entry) => (0, import_node_path9.join)(parentPath, entry.name)),
27680
+ childDirectories: (parentPath, limit) => (0, import_node_fs12.readdirSync)(parentPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).slice(0, limit).map((entry) => (0, import_node_path10.join)(parentPath, entry.name)),
26068
27681
  gitRemoteUrl: (candidatePath) => {
26069
- const result = (0, import_node_child_process4.spawnSync)("git", ["-C", candidatePath, "config", "--get", "remote.origin.url"], {
27682
+ const result = (0, import_node_child_process6.spawnSync)("git", ["-C", candidatePath, "config", "--get", "remote.origin.url"], {
26070
27683
  encoding: "utf8",
26071
27684
  env: getDaemonCliEnvironment(),
26072
27685
  timeout: 2e3
@@ -26095,7 +27708,7 @@ function workspaceMatchesExpectedRepositories(input) {
26095
27708
  input.expectedRepoUrls.map(normalizeRepositoryIdentity).filter((identity2) => Boolean(identity2))
26096
27709
  );
26097
27710
  if (identities.size === 0) return false;
26098
- const remote = (input.probe ?? DEFAULT_PROBE).gitRemoteUrl((0, import_node_path9.resolve)(input.workspacePath));
27711
+ const remote = (input.probe ?? DEFAULT_PROBE).gitRemoteUrl((0, import_node_path10.resolve)(input.workspacePath));
26099
27712
  const identity = remote ? normalizeRepositoryIdentity(remote) : null;
26100
27713
  return Boolean(identity && identities.has(identity));
26101
27714
  }
@@ -26105,8 +27718,8 @@ function discoverRelocatedWorkspace(input) {
26105
27718
  );
26106
27719
  if (identities.size === 0) return null;
26107
27720
  const searchRoots = [
26108
- (0, import_node_path9.dirname)((0, import_node_path9.resolve)(input.missingPath)),
26109
- ...(input.approvedWorkspacePaths ?? []).map((path) => (0, import_node_path9.dirname)((0, import_node_path9.resolve)(path)))
27721
+ (0, import_node_path10.dirname)((0, import_node_path10.resolve)(input.missingPath)),
27722
+ ...(input.approvedWorkspacePaths ?? []).map((path) => (0, import_node_path10.dirname)((0, import_node_path10.resolve)(path)))
26110
27723
  ].filter((path, index, paths) => paths.indexOf(path) === index);
26111
27724
  const candidates = [];
26112
27725
  for (const root of searchRoots) {
@@ -26118,19 +27731,19 @@ function discoverRelocatedWorkspace(input) {
26118
27731
  }
26119
27732
  }
26120
27733
  const matches = [];
26121
- for (const candidate of new Set(candidates.map((path) => (0, import_node_path9.resolve)(path)))) {
27734
+ for (const candidate of new Set(candidates.map((path) => (0, import_node_path10.resolve)(path)))) {
26122
27735
  const remote = (input.probe ?? DEFAULT_PROBE).gitRemoteUrl(candidate);
26123
27736
  const identity = remote ? normalizeRepositoryIdentity(remote) : null;
26124
- if (identity && identities.has(identity)) matches.push((0, import_node_path9.resolve)(candidate));
27737
+ if (identity && identities.has(identity)) matches.push((0, import_node_path10.resolve)(candidate));
26125
27738
  if (matches.length > 1) return null;
26126
27739
  }
26127
27740
  return matches[0] ?? null;
26128
27741
  }
26129
27742
 
26130
27743
  // src/workspace-run-lease.ts
26131
- var import_node_child_process5 = require("child_process");
26132
- var import_node_fs12 = require("fs");
26133
- var import_node_path10 = require("path");
27744
+ var import_node_child_process7 = require("child_process");
27745
+ var import_node_fs13 = require("fs");
27746
+ var import_node_path11 = require("path");
26134
27747
  var WORKSPACE_LEASE_CHECK_INTERVAL_MS = 1e3;
26135
27748
  function parsePositiveMs(value2, fallback) {
26136
27749
  const parsed = value2 ? Number(value2) : Number.NaN;
@@ -26141,7 +27754,7 @@ var WORKSPACE_LEASE_GIT_TIMEOUT_MS = parsePositiveMs(
26141
27754
  2e3
26142
27755
  );
26143
27756
  function gitValue(cwd, args) {
26144
- const result = (0, import_node_child_process5.spawnSync)("git", ["-C", cwd, ...args], {
27757
+ const result = (0, import_node_child_process7.spawnSync)("git", ["-C", cwd, ...args], {
26145
27758
  encoding: "utf8",
26146
27759
  env: getDaemonCliEnvironment(),
26147
27760
  timeout: WORKSPACE_LEASE_GIT_TIMEOUT_MS
@@ -26150,17 +27763,17 @@ function gitValue(cwd, args) {
26150
27763
  return result.stdout.trim() || null;
26151
27764
  }
26152
27765
  function filesystemIdentity(path) {
26153
- const canonicalPath = import_node_fs12.realpathSync.native(path);
26154
- const stat = (0, import_node_fs12.statSync)(canonicalPath);
27766
+ const canonicalPath = import_node_fs13.realpathSync.native(path);
27767
+ const stat = (0, import_node_fs13.statSync)(canonicalPath);
26155
27768
  return `${canonicalPath}:${stat.dev}:${stat.ino}`;
26156
27769
  }
26157
27770
  function resolveGitPath(workspacePath, value2) {
26158
- return (0, import_node_path10.isAbsolute)(value2) ? value2 : (0, import_node_path10.resolve)(workspacePath, value2);
27771
+ return (0, import_node_path11.isAbsolute)(value2) ? value2 : (0, import_node_path11.resolve)(workspacePath, value2);
26159
27772
  }
26160
27773
  var DEFAULT_PROBE2 = {
26161
27774
  snapshot: (workspacePath) => {
26162
27775
  try {
26163
- const canonicalWorkspacePath = import_node_fs12.realpathSync.native((0, import_node_path10.resolve)(workspacePath));
27776
+ const canonicalWorkspacePath = import_node_fs13.realpathSync.native((0, import_node_path11.resolve)(workspacePath));
26164
27777
  const repositoryRoot = gitValue(canonicalWorkspacePath, ["rev-parse", "--show-toplevel"]);
26165
27778
  const gitDirectory = gitValue(canonicalWorkspacePath, ["rev-parse", "--git-dir"]);
26166
27779
  const commonDirectory = gitValue(canonicalWorkspacePath, ["rev-parse", "--git-common-dir"]);
@@ -26169,7 +27782,7 @@ var DEFAULT_PROBE2 = {
26169
27782
  const remote = gitValue(canonicalWorkspacePath, ["config", "--get", "remote.origin.url"]);
26170
27783
  const remoteIdentity = remote ? normalizeRepositoryIdentity(remote) : null;
26171
27784
  return {
26172
- workspacePath: (0, import_node_path10.resolve)(workspacePath),
27785
+ workspacePath: (0, import_node_path11.resolve)(workspacePath),
26173
27786
  workspaceIdentity: filesystemIdentity(canonicalWorkspacePath),
26174
27787
  repositoryIdentity: `${filesystemIdentity(
26175
27788
  resolveGitPath(canonicalWorkspacePath, commonDirectory)
@@ -26183,7 +27796,7 @@ var DEFAULT_PROBE2 = {
26183
27796
  },
26184
27797
  pathExists: (workspacePath) => {
26185
27798
  try {
26186
- return (0, import_node_fs12.existsSync)((0, import_node_path10.resolve)(workspacePath));
27799
+ return (0, import_node_fs13.existsSync)((0, import_node_path11.resolve)(workspacePath));
26187
27800
  } catch {
26188
27801
  return true;
26189
27802
  }
@@ -26214,7 +27827,7 @@ var WorkspaceRunLease = class _WorkspaceRunLease {
26214
27827
  static capture(targets, probe = DEFAULT_PROBE2) {
26215
27828
  const captured = [];
26216
27829
  for (const target of targets) {
26217
- const normalizedTarget = { ...target, workspacePath: (0, import_node_path10.resolve)(target.workspacePath) };
27830
+ const normalizedTarget = { ...target, workspacePath: (0, import_node_path11.resolve)(target.workspacePath) };
26218
27831
  const identity = probe.snapshot(normalizedTarget.workspacePath);
26219
27832
  if (!identity) return { lease: null, failure: unavailableFailure(normalizedTarget) };
26220
27833
  if (normalizedTarget.expectedBranch && identity.branch !== normalizedTarget.expectedBranch) {
@@ -26356,34 +27969,62 @@ var PRODUCTION_API_URL = "https://api.tryalan.ai";
26356
27969
  var PRODUCTION_WS_URL = "wss://ws.tryalan.ai";
26357
27970
  var LOCAL_API_URL = "http://localhost:8400";
26358
27971
  var LOCAL_WS_URL = "ws://localhost:8401";
26359
- function requiredString(params, key) {
27972
+ function requiredString2(params, key) {
26360
27973
  const value2 = params[key];
26361
27974
  if (typeof value2 !== "string" || value2.trim() === "") {
26362
27975
  throw new Error(`${key} is required`);
26363
27976
  }
26364
27977
  return value2;
26365
27978
  }
27979
+ var DIRTY_WORKSPACE_ACTIONS = /* @__PURE__ */ new Set([
27980
+ "move_via_stash",
27981
+ "commit_wip",
27982
+ "discard",
27983
+ "leave"
27984
+ ]);
27985
+ function parseDirtyWorkspaceResolution(value2) {
27986
+ if (value2 === void 0) return void 0;
27987
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) {
27988
+ throw new Error("dirtyResolution must be an object");
27989
+ }
27990
+ const raw = value2;
27991
+ const parseAction = (key) => {
27992
+ const action = raw[key];
27993
+ if (action === void 0) return void 0;
27994
+ if (typeof action !== "string" || !DIRTY_WORKSPACE_ACTIONS.has(action)) {
27995
+ throw new Error(`${key} is not a supported dirty workspace action`);
27996
+ }
27997
+ return action;
27998
+ };
27999
+ return {
28000
+ sourceAction: parseAction("sourceAction"),
28001
+ destinationAction: parseAction("destinationAction"),
28002
+ sourceCommitMessage: typeof raw.sourceCommitMessage === "string" ? raw.sourceCommitMessage : void 0,
28003
+ destinationCommitMessage: typeof raw.destinationCommitMessage === "string" ? raw.destinationCommitMessage : void 0,
28004
+ confirmDiscard: raw.confirmDiscard === true
28005
+ };
28006
+ }
26366
28007
  function assertValidGitRef(ref, label) {
26367
28008
  if (!ref || ref.startsWith("-") || ref.includes("\0")) {
26368
28009
  throw new Error(`${label} is not a safe git ref`);
26369
28010
  }
26370
- const result = (0, import_node_child_process6.spawnSync)("git", ["check-ref-format", "--branch", ref], {
28011
+ const result = (0, import_node_child_process8.spawnSync)("git", ["check-ref-format", "--branch", ref], {
26371
28012
  env: getDaemonCliEnvironment(),
26372
28013
  encoding: "utf8"
26373
28014
  });
26374
28015
  if (result.status !== 0) throw new Error(`${label} is not a valid git ref`);
26375
28016
  }
26376
- function isPathContained(childPath, parentPath) {
26377
- const rel = (0, import_node_path11.relative)((0, import_node_path11.resolve)(parentPath), (0, import_node_path11.resolve)(childPath));
26378
- return rel === "" || !(0, import_node_path11.isAbsolute)(rel) && rel !== ".." && !rel.startsWith(`..${import_node_path11.sep}`);
28017
+ function isPathContained2(childPath, parentPath) {
28018
+ const rel = (0, import_node_path12.relative)((0, import_node_path12.resolve)(parentPath), (0, import_node_path12.resolve)(childPath));
28019
+ return rel === "" || !(0, import_node_path12.isAbsolute)(rel) && rel !== ".." && !rel.startsWith(`..${import_node_path12.sep}`);
26379
28020
  }
26380
28021
  function assertPathContained(childPath, parentPath, label) {
26381
- if (!isPathContained(childPath, parentPath)) {
28022
+ if (!isPathContained2(childPath, parentPath)) {
26382
28023
  throw new Error(`${label} escapes allowed directory`);
26383
28024
  }
26384
28025
  }
26385
28026
  function git(cwd, args) {
26386
- const result = (0, import_node_child_process6.spawnSync)("git", args, {
28027
+ const result = (0, import_node_child_process8.spawnSync)("git", args, {
26387
28028
  cwd,
26388
28029
  env: getDaemonCliEnvironment(),
26389
28030
  encoding: "utf8",
@@ -26402,14 +28043,23 @@ function gitSafe(cwd, args) {
26402
28043
  }
26403
28044
  }
26404
28045
  function worktreeDir(projectPath) {
26405
- const resolved = (0, import_node_path11.resolve)(projectPath);
26406
- return (0, import_node_path11.join)((0, import_node_path11.dirname)(resolved), `${(0, import_node_path11.basename)(resolved)}-alan-worktrees`);
28046
+ const resolved = (0, import_node_path12.resolve)(projectPath);
28047
+ return (0, import_node_path12.join)((0, import_node_path12.dirname)(resolved), `${(0, import_node_path12.basename)(resolved)}-alan-worktrees`);
28048
+ }
28049
+ function hasAlanWorktreeAncestor(targetPath) {
28050
+ let cursor = (0, import_node_path12.resolve)(targetPath);
28051
+ while (true) {
28052
+ if ((0, import_node_path12.basename)(cursor).endsWith("-alan-worktrees")) return true;
28053
+ const parent = (0, import_node_path12.dirname)(cursor);
28054
+ if (parent === cursor) return false;
28055
+ cursor = parent;
28056
+ }
26407
28057
  }
26408
28058
  function pathSegment(value2) {
26409
28059
  return value2.toLowerCase().replace(/^alan\//, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "task";
26410
28060
  }
26411
28061
  function repoName(repoUrl, localPath) {
26412
- return repoUrl.replace(/\.git$/, "").split("/").pop() || (0, import_node_path11.basename)(localPath) || "repo";
28062
+ return repoUrl.replace(/\.git$/, "").split("/").pop() || (0, import_node_path12.basename)(localPath) || "repo";
26413
28063
  }
26414
28064
  function findWorktreeByBranch(repoPath, branchName) {
26415
28065
  const output = gitSafe(repoPath, ["worktree", "list", "--porcelain"]);
@@ -26425,21 +28075,21 @@ function findWorktreeByBranch(repoPath, branchName) {
26425
28075
  function ensureGitWorktree(input) {
26426
28076
  assertValidGitRef(input.branchName, "branchName");
26427
28077
  assertValidGitRef(input.baseBranch, "baseBranch");
26428
- const repoPath = (0, import_node_path11.resolve)(input.repoPath);
28078
+ const repoPath = (0, import_node_path12.resolve)(input.repoPath);
26429
28079
  const root = input.allowedRoot ?? worktreeDir(repoPath);
26430
28080
  const existingPath = findWorktreeByBranch(input.repoPath, input.branchName);
26431
28081
  if (existingPath) {
26432
- if ((0, import_node_path11.resolve)(existingPath) === repoPath) {
28082
+ if ((0, import_node_path12.resolve)(existingPath) === repoPath) {
26433
28083
  throw new Error(`${input.branchName} is checked out in the shared checkout`);
26434
28084
  }
26435
28085
  return existingPath;
26436
28086
  }
26437
28087
  assertPathContained(input.worktreePath, root, "worktreePath");
26438
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(input.worktreePath), { recursive: true });
28088
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(input.worktreePath), { recursive: true });
26439
28089
  if (!gitSafe(input.repoPath, ["branch", "--list", input.branchName])) {
26440
28090
  git(input.repoPath, ["branch", input.branchName, input.baseBranch]);
26441
28091
  }
26442
- if ((0, import_node_fs13.existsSync)(input.worktreePath)) {
28092
+ if ((0, import_node_fs14.existsSync)(input.worktreePath)) {
26443
28093
  if (gitSafe(input.worktreePath, ["branch", "--show-current"]) === input.branchName) {
26444
28094
  return input.worktreePath;
26445
28095
  }
@@ -26449,40 +28099,172 @@ function ensureGitWorktree(input) {
26449
28099
  return input.worktreePath;
26450
28100
  }
26451
28101
  function resolveProjectWorktreePaths(projectPath, gitRoot, gitWorktreePath) {
26452
- const resolvedProjectPath = (0, import_node_path11.resolve)(projectPath);
26453
- const resolvedGitRoot = (0, import_node_path11.resolve)(gitRoot);
26454
- const projectRelativePath = (0, import_node_path11.relative)(resolvedGitRoot, resolvedProjectPath);
26455
- if (projectRelativePath === ".." || projectRelativePath.startsWith(`..${import_node_path11.sep}`) || (0, import_node_path11.isAbsolute)(projectRelativePath)) {
28102
+ const resolvedProjectPath = (0, import_node_path12.resolve)(projectPath);
28103
+ const resolvedGitRoot = (0, import_node_path12.resolve)(gitRoot);
28104
+ const projectRelativePath = (0, import_node_path12.relative)(resolvedGitRoot, resolvedProjectPath);
28105
+ if (projectRelativePath === ".." || projectRelativePath.startsWith(`..${import_node_path12.sep}`) || (0, import_node_path12.isAbsolute)(projectRelativePath)) {
26456
28106
  throw new Error("The selected Project folder is outside its Git repository");
26457
28107
  }
26458
28108
  return {
26459
28109
  repoPath: resolvedGitRoot,
26460
- projectPath: projectRelativePath ? (0, import_node_path11.join)((0, import_node_path11.resolve)(gitWorktreePath), projectRelativePath) : (0, import_node_path11.resolve)(gitWorktreePath)
28110
+ projectPath: projectRelativePath ? (0, import_node_path12.join)((0, import_node_path12.resolve)(gitWorktreePath), projectRelativePath) : (0, import_node_path12.resolve)(gitWorktreePath)
28111
+ };
28112
+ }
28113
+ function preflightLocalWorkspaceMigration(params) {
28114
+ const direction = params.direction;
28115
+ if (direction !== "to-checkout" && direction !== "to-worktree") {
28116
+ throw new Error("direction must be to-checkout or to-worktree");
28117
+ }
28118
+ const targets = Array.isArray(params.targets) ? params.targets : [];
28119
+ if (targets.length !== 1) {
28120
+ throw new Error("Dirty workspace migration currently supports exactly one repository");
28121
+ }
28122
+ const rawTarget = targets[0];
28123
+ const projectPath = (0, import_node_path12.resolve)(requiredString2(rawTarget, "projectPath"));
28124
+ const projectRootOutput = gitSafe(projectPath, ["rev-parse", "--show-toplevel"]);
28125
+ if (!projectRootOutput) {
28126
+ throw new Error("The selected Project folder is not inside a Git repository");
28127
+ }
28128
+ const projectRoot = (0, import_node_path12.resolve)(projectRootOutput);
28129
+ if (direction === "to-worktree") {
28130
+ const result2 = preflightWorkspaceMigration(
28131
+ {
28132
+ direction: "checkout_to_worktree",
28133
+ sourcePath: projectRoot
28134
+ },
28135
+ git
28136
+ );
28137
+ return {
28138
+ direction,
28139
+ source: result2.source,
28140
+ destination: null,
28141
+ requiresResolution: result2.source.isDirty
28142
+ };
28143
+ }
28144
+ const requestedWorktreePath = (0, import_node_path12.resolve)(requiredString2(rawTarget, "worktreePath"));
28145
+ const worktreeRootOutput = gitSafe(requestedWorktreePath, ["rev-parse", "--show-toplevel"]);
28146
+ if (!worktreeRootOutput) {
28147
+ throw new Error("The task worktree is not a registered Git worktree");
28148
+ }
28149
+ const result = preflightWorkspaceMigration(
28150
+ {
28151
+ direction: "worktree_to_checkout",
28152
+ sourcePath: (0, import_node_path12.resolve)(worktreeRootOutput),
28153
+ destinationPath: projectRoot
28154
+ },
28155
+ git
28156
+ );
28157
+ return {
28158
+ direction,
28159
+ source: result.source,
28160
+ destination: result.destination,
28161
+ requiresResolution: result.source.isDirty || Boolean(result.destination?.isDirty)
28162
+ };
28163
+ }
28164
+ function statusLocalWorkspace(params) {
28165
+ const requestedPath = (0, import_node_path12.resolve)(requiredString2(params, "worktreePath"));
28166
+ const rootOutput = gitSafe(requestedPath, ["rev-parse", "--show-toplevel"]);
28167
+ if (!rootOutput) {
28168
+ throw new Error("The selected folder is not inside a Git repository");
28169
+ }
28170
+ const root = (0, import_node_path12.resolve)(rootOutput);
28171
+ const tree = preflightGitTree(root, git);
28172
+ const headSha = gitSafe(root, ["rev-parse", "HEAD"]) || null;
28173
+ const upstream = gitSafe(root, ["rev-parse", "--abbrev-ref", "@{upstream}"]);
28174
+ const hasUpstream = upstream !== "";
28175
+ let ahead = null;
28176
+ let behind = null;
28177
+ if (hasUpstream) {
28178
+ const counts = gitSafe(root, ["rev-list", "--left-right", "--count", "@{upstream}...HEAD"]);
28179
+ const [behindRaw, aheadRaw] = counts.split(/\s+/);
28180
+ const parsedBehind = Number.parseInt(behindRaw ?? "", 10);
28181
+ const parsedAhead = Number.parseInt(aheadRaw ?? "", 10);
28182
+ behind = Number.isNaN(parsedBehind) ? null : parsedBehind;
28183
+ ahead = Number.isNaN(parsedAhead) ? null : parsedAhead;
28184
+ }
28185
+ return {
28186
+ branch: tree.currentBranch,
28187
+ headSha,
28188
+ dirty: tree.isDirty,
28189
+ filesChanged: tree.dirtyFileCount,
28190
+ ahead,
28191
+ behind,
28192
+ hasUpstream
26461
28193
  };
26462
28194
  }
28195
+ function ensureGitWorktreeWithDirtyResolution(input, resolution) {
28196
+ const repoPath = (0, import_node_path12.resolve)(input.repoPath);
28197
+ const before = preflightGitTree(repoPath, git);
28198
+ let sourceStash = null;
28199
+ if (before.isDirty) {
28200
+ const action = resolution?.sourceAction;
28201
+ if (!action) {
28202
+ throw new Error(
28203
+ `The Project checkout has ${before.dirtyFileCount} uncommitted file${before.dirtyFileCount === 1 ? "" : "s"}. Choose how to preserve them before moving into a worktree.`
28204
+ );
28205
+ }
28206
+ if (action === "commit_wip" && before.currentBranch !== input.baseBranch) {
28207
+ throw new Error(
28208
+ `Commit as WIP would stay on "${before.currentBranch ?? "the current checkout"}", not the selected base "${input.baseBranch}". Choose Move changes with task instead.`
28209
+ );
28210
+ }
28211
+ const resolved = resolveDirtyTree(
28212
+ {
28213
+ cwd: repoPath,
28214
+ action,
28215
+ operation: "checkout-to-worktree",
28216
+ confirmDiscard: resolution?.confirmDiscard
28217
+ },
28218
+ { git }
28219
+ );
28220
+ sourceStash = resolved.stash;
28221
+ }
28222
+ const existingPath = findWorktreeByBranch(repoPath, input.branchName);
28223
+ const worktreePath = ensureGitWorktree(input);
28224
+ if (!sourceStash) return worktreePath;
28225
+ try {
28226
+ applyMigrationStash({ cwd: worktreePath, stash: sourceStash }, git);
28227
+ return worktreePath;
28228
+ } catch (error) {
28229
+ if (!existingPath) {
28230
+ gitSafe(worktreePath, ["reset", "--hard", "HEAD"]);
28231
+ gitSafe(worktreePath, ["clean", "-fd"]);
28232
+ gitSafe(repoPath, ["worktree", "remove", worktreePath]);
28233
+ gitSafe(repoPath, ["worktree", "prune"]);
28234
+ }
28235
+ throw error;
28236
+ }
28237
+ }
26463
28238
  function ensureWorktree(params) {
26464
28239
  const input = {
26465
- projectPath: requiredString(params, "projectPath"),
26466
- taskId: requiredString(params, "taskId"),
26467
- branchName: requiredString(params, "branchName"),
28240
+ projectPath: requiredString2(params, "projectPath"),
28241
+ taskId: requiredString2(params, "taskId"),
28242
+ branchName: requiredString2(params, "branchName"),
26468
28243
  baseBranch: typeof params.baseBranch === "string" ? params.baseBranch : void 0,
26469
- repoLocalPaths: Array.isArray(params.repoLocalPaths) ? params.repoLocalPaths : []
28244
+ repoLocalPaths: Array.isArray(params.repoLocalPaths) ? params.repoLocalPaths : [],
28245
+ dirtyResolution: parseDirtyWorkspaceResolution(params.dirtyResolution)
26470
28246
  };
28247
+ if ((input.repoLocalPaths?.length ?? 0) > 1) {
28248
+ throw new Error("A task worktree requires exactly one Git repository.");
28249
+ }
26471
28250
  if (input.repoLocalPaths?.length) {
26472
- const taskRoot2 = (0, import_node_path11.join)(
28251
+ const taskRoot2 = (0, import_node_path12.join)(
26473
28252
  worktreeDir(input.projectPath),
26474
28253
  `${pathSegment(input.branchName)}-${input.taskId.slice(0, 8)}`
26475
28254
  );
26476
28255
  return {
26477
28256
  worktrees: input.repoLocalPaths.map((repo) => {
26478
28257
  const baseBranch2 = repo.branch || input.baseBranch || "main";
26479
- const worktreePath2 = ensureGitWorktree({
26480
- repoPath: repo.localPath,
26481
- worktreePath: (0, import_node_path11.join)(taskRoot2, pathSegment(repoName(repo.repoUrl, repo.localPath))),
26482
- branchName: input.branchName,
26483
- baseBranch: baseBranch2,
26484
- allowedRoot: worktreeDir(input.projectPath)
26485
- });
28258
+ const worktreePath2 = ensureGitWorktreeWithDirtyResolution(
28259
+ {
28260
+ repoPath: repo.localPath,
28261
+ worktreePath: (0, import_node_path12.join)(taskRoot2, pathSegment(repoName(repo.repoUrl, repo.localPath))),
28262
+ branchName: input.branchName,
28263
+ baseBranch: baseBranch2,
28264
+ allowedRoot: worktreeDir(input.projectPath)
28265
+ },
28266
+ input.dirtyResolution
28267
+ );
26486
28268
  return {
26487
28269
  repoId: repo.repoId ?? null,
26488
28270
  repoUrl: repo.repoUrl,
@@ -26490,26 +28272,30 @@ function ensureWorktree(params) {
26490
28272
  branchName: input.branchName,
26491
28273
  baseBranch: baseBranch2
26492
28274
  };
26493
- })
28275
+ }),
28276
+ ...input.dirtyResolution ? { dirtyResolution: input.dirtyResolution } : {}
26494
28277
  };
26495
28278
  }
26496
- const gitRoot = gitSafe((0, import_node_path11.resolve)(input.projectPath), ["rev-parse", "--show-toplevel"]);
28279
+ const gitRoot = gitSafe((0, import_node_path12.resolve)(input.projectPath), ["rev-parse", "--show-toplevel"]);
26497
28280
  if (!gitRoot) {
26498
28281
  throw new Error("The selected Project folder is not inside a Git repository");
26499
28282
  }
26500
- const repoPath = (0, import_node_path11.resolve)(gitRoot);
28283
+ const repoPath = (0, import_node_path12.resolve)(gitRoot);
26501
28284
  const baseBranch = input.baseBranch || gitSafe(repoPath, ["branch", "--show-current"]) || "HEAD";
26502
- const taskRoot = (0, import_node_path11.join)(
28285
+ const taskRoot = (0, import_node_path12.join)(
26503
28286
  worktreeDir(repoPath),
26504
28287
  `${pathSegment(input.branchName)}-${input.taskId.slice(0, 8)}`
26505
28288
  );
26506
28289
  resolveProjectWorktreePaths(input.projectPath, repoPath, taskRoot);
26507
- const worktreePath = ensureGitWorktree({
26508
- repoPath,
26509
- worktreePath: taskRoot,
26510
- branchName: input.branchName,
26511
- baseBranch
26512
- });
28290
+ const worktreePath = ensureGitWorktreeWithDirtyResolution(
28291
+ {
28292
+ repoPath,
28293
+ worktreePath: taskRoot,
28294
+ branchName: input.branchName,
28295
+ baseBranch
28296
+ },
28297
+ input.dirtyResolution
28298
+ );
26513
28299
  const projectWorktreePath = resolveProjectWorktreePaths(
26514
28300
  input.projectPath,
26515
28301
  repoPath,
@@ -26522,13 +28308,286 @@ function ensureWorktree(params) {
26522
28308
  branchName: input.branchName,
26523
28309
  baseBranch
26524
28310
  }
26525
- ]
28311
+ ],
28312
+ ...input.dirtyResolution ? { dirtyResolution: input.dirtyResolution } : {}
28313
+ };
28314
+ }
28315
+ function listRegisteredWorktrees(cwd) {
28316
+ return git(cwd, ["worktree", "list", "--porcelain"]).split("\n\n").flatMap((entry) => {
28317
+ const lines = entry.split("\n");
28318
+ const path = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
28319
+ if (!path) return [];
28320
+ return [
28321
+ {
28322
+ path: (0, import_node_path12.resolve)(path),
28323
+ branch: lines.find((line) => line.startsWith("branch "))?.slice("branch ".length).replace("refs/heads/", "")
28324
+ }
28325
+ ];
28326
+ });
28327
+ }
28328
+ function removeWorktreesIfClean(params, removeGit = git) {
28329
+ const projectPath = (0, import_node_path12.resolve)(requiredString2(params, "projectPath"));
28330
+ const targets = Array.isArray(params.targets) ? params.targets : [];
28331
+ if (targets.length === 0) throw new Error("At least one worktree target is required");
28332
+ const projectRootOutput = gitSafe(projectPath, ["rev-parse", "--show-toplevel"]);
28333
+ const projectRoot = projectRootOutput ? (0, import_node_path12.resolve)(projectRootOutput) : null;
28334
+ const projectRegistrations = projectRoot ? listRegisteredWorktrees(projectRoot) : [];
28335
+ const planned = targets.map((target) => {
28336
+ const rawTarget = target;
28337
+ const requestedPath = (0, import_node_path12.resolve)(requiredString2(rawTarget, "worktreePath"));
28338
+ const branchName = requiredString2(rawTarget, "branchName");
28339
+ assertValidGitRef(branchName, "branchName");
28340
+ const registrationSets = [];
28341
+ if (projectRegistrations.length > 0) registrationSets.push(projectRegistrations);
28342
+ if ((0, import_node_fs14.existsSync)(requestedPath)) {
28343
+ const requestedRoot = gitSafe(requestedPath, ["rev-parse", "--show-toplevel"]);
28344
+ if (!requestedRoot) {
28345
+ throw new Error(
28346
+ `The earlier task worktree is not a registered Git worktree: ${requestedPath}`
28347
+ );
28348
+ }
28349
+ const requestedRegistrations = listRegisteredWorktrees((0, import_node_path12.resolve)(requestedRoot));
28350
+ if (!registrationSets.some(
28351
+ (entries) => entries.length === requestedRegistrations.length && entries.every((entry, index) => entry.path === requestedRegistrations[index]?.path)
28352
+ )) {
28353
+ registrationSets.push(requestedRegistrations);
28354
+ }
28355
+ }
28356
+ for (const registrations of registrationSets) {
28357
+ const mainRoot = registrations[0]?.path;
28358
+ const registeredTarget = registrations.find(
28359
+ (entry) => requestedPath === entry.path || isPathContained2(requestedPath, entry.path)
28360
+ );
28361
+ if (!registeredTarget || !mainRoot) continue;
28362
+ if (registeredTarget.path === mainRoot) {
28363
+ throw new Error("Refusing to remove the Project's main checkout");
28364
+ }
28365
+ if (registeredTarget.branch !== branchName) {
28366
+ throw new Error(
28367
+ `The registered worktree branch changed from "${branchName}" to "${registeredTarget.branch ?? "detached HEAD"}".`
28368
+ );
28369
+ }
28370
+ const allowedRoots = [
28371
+ worktreeDir(mainRoot),
28372
+ worktreeDir(projectPath),
28373
+ ...projectRoot ? [worktreeDir(projectRoot)] : []
28374
+ ];
28375
+ if (!allowedRoots.some((root) => isPathContained2(registeredTarget.path, root))) {
28376
+ throw new Error("The registered worktree is outside an Alan-managed worktree directory");
28377
+ }
28378
+ if (git(registeredTarget.path, ["status", "--porcelain"]).trim()) {
28379
+ throw new Error(
28380
+ `The earlier task worktree has uncommitted changes at ${requestedPath}. Commit or discard them before changing Project, branch, or worktree mode.`
28381
+ );
28382
+ }
28383
+ return {
28384
+ requestedPath,
28385
+ worktreeRoot: registeredTarget.path,
28386
+ mainRoot,
28387
+ branchName,
28388
+ replayed: false
28389
+ };
28390
+ }
28391
+ if (!(0, import_node_fs14.existsSync)(requestedPath) && hasAlanWorktreeAncestor(requestedPath)) {
28392
+ return {
28393
+ requestedPath,
28394
+ worktreeRoot: null,
28395
+ mainRoot: null,
28396
+ branchName,
28397
+ replayed: true
28398
+ };
28399
+ }
28400
+ throw new Error(
28401
+ `The earlier task worktree is not safely registered and was not changed: ${requestedPath}`
28402
+ );
28403
+ });
28404
+ const removalRoots = planned.flatMap(
28405
+ (target) => target.worktreeRoot && target.mainRoot ? [{ worktreeRoot: target.worktreeRoot, mainRoot: target.mainRoot }] : []
28406
+ );
28407
+ if (new Set(removalRoots.map(({ worktreeRoot }) => worktreeRoot)).size !== removalRoots.length) {
28408
+ throw new Error("Duplicate worktree targets were not changed");
28409
+ }
28410
+ for (const target of removalRoots) {
28411
+ removeGit(target.mainRoot, ["worktree", "remove", target.worktreeRoot]);
28412
+ gitSafe(target.mainRoot, ["worktree", "prune"]);
28413
+ }
28414
+ return {
28415
+ ok: true,
28416
+ removed: planned.map(({ requestedPath, branchName, replayed }) => ({
28417
+ worktreePath: requestedPath,
28418
+ branchName,
28419
+ replayed
28420
+ }))
28421
+ };
28422
+ }
28423
+ function migrateWorktreeToCheckout(params, removeGit = git) {
28424
+ const dirtyResolution = parseDirtyWorkspaceResolution(params.dirtyResolution);
28425
+ const targets = Array.isArray(params.targets) ? params.targets : [];
28426
+ if (targets.length !== 1) {
28427
+ throw new Error(
28428
+ "Multi-repository worktree migration is not supported; no repositories were changed."
28429
+ );
28430
+ }
28431
+ const target = targets[0];
28432
+ const rawTarget = target;
28433
+ const projectPath = (0, import_node_path12.resolve)(requiredString2(rawTarget, "projectPath"));
28434
+ const requestedWorktreePath = (0, import_node_path12.resolve)(requiredString2(rawTarget, "worktreePath"));
28435
+ const branch = requiredString2(rawTarget, "branch");
28436
+ assertValidGitRef(branch, "branch");
28437
+ const projectRootOutput = gitSafe(projectPath, ["rev-parse", "--show-toplevel"]);
28438
+ if (!projectRootOutput) {
28439
+ throw new Error("The selected Project folder is not inside a Git repository");
28440
+ }
28441
+ const projectRoot = (0, import_node_path12.resolve)(projectRootOutput);
28442
+ const projectRelativePath = (0, import_node_path12.relative)(projectRoot, projectPath);
28443
+ if (projectRelativePath === ".." || projectRelativePath.startsWith(`..${import_node_path12.sep}`) || (0, import_node_path12.isAbsolute)(projectRelativePath)) {
28444
+ throw new Error("The selected Project folder is outside its Git repository");
28445
+ }
28446
+ const registered = git(projectRoot, ["worktree", "list", "--porcelain"]).split("\n\n").map((entry) => {
28447
+ const lines = entry.split("\n");
28448
+ return {
28449
+ path: lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length),
28450
+ branch: lines.find((line) => line.startsWith("branch "))?.slice("branch ".length).replace("refs/heads/", "")
28451
+ };
28452
+ });
28453
+ const registeredTarget = registered.find(({ path, branch: registeredBranch }) => {
28454
+ if (!path || registeredBranch !== branch) return false;
28455
+ const root = (0, import_node_path12.resolve)(path);
28456
+ return requestedWorktreePath === root || isPathContained2(requestedWorktreePath, root);
28457
+ });
28458
+ const currentBranch = gitSafe(projectRoot, ["branch", "--show-current"]);
28459
+ if (!registeredTarget?.path) {
28460
+ if (currentBranch === branch) {
28461
+ return {
28462
+ ok: true,
28463
+ replayed: true,
28464
+ migrated: [{ ...target, projectPath, worktreePath: requestedWorktreePath, branch }],
28465
+ ...dirtyResolution ? { dirtyResolution } : {}
28466
+ };
28467
+ }
28468
+ throw new Error(
28469
+ `The task worktree is no longer registered and the Project is not on branch "${branch}".`
28470
+ );
28471
+ }
28472
+ const worktreeRoot = (0, import_node_path12.resolve)(registeredTarget.path);
28473
+ const worktreeRelativePath = (0, import_node_path12.relative)(worktreeRoot, requestedWorktreePath);
28474
+ if (worktreeRelativePath !== projectRelativePath) {
28475
+ throw new Error(
28476
+ "The configured Project and task worktree do not point at the same repository subdirectory."
28477
+ );
28478
+ }
28479
+ const allowedRoots = [worktreeDir(projectRoot), worktreeDir(projectPath)];
28480
+ if (!allowedRoots.some((allowedRoot) => isPathContained2(worktreeRoot, allowedRoot))) {
28481
+ throw new Error("The task worktree escapes the Project worktree directory.");
28482
+ }
28483
+ if (!(0, import_node_fs14.existsSync)(worktreeRoot)) {
28484
+ throw new Error(`Worktree directory not found: ${worktreeRoot}`);
28485
+ }
28486
+ const sourceBefore = preflightGitTree(worktreeRoot, git);
28487
+ const destinationBefore = preflightGitTree(projectRoot, git);
28488
+ if (sourceBefore.isDirty && !dirtyResolution?.sourceAction) {
28489
+ throw new Error(
28490
+ `The task worktree has ${sourceBefore.dirtyFileCount} uncommitted file${sourceBefore.dirtyFileCount === 1 ? "" : "s"}. Choose how to preserve them before migrating.`
28491
+ );
28492
+ }
28493
+ if (destinationBefore.isDirty && !dirtyResolution?.destinationAction) {
28494
+ throw new Error(
28495
+ `The Project checkout has ${destinationBefore.dirtyFileCount} uncommitted file${destinationBefore.dirtyFileCount === 1 ? "" : "s"}. Choose how to preserve them before migrating.`
28496
+ );
28497
+ }
28498
+ if (sourceBefore.isDirty && dirtyResolution?.sourceAction === "leave") {
28499
+ throw new Error("Task worktree changes cannot be left behind when releasing the worktree.");
28500
+ }
28501
+ if (destinationBefore.isDirty && dirtyResolution?.destinationAction === "leave") {
28502
+ throw new Error("Project checkout changes cannot stay in place while switching branches.");
28503
+ }
28504
+ const destinationResolved = destinationBefore.isDirty ? resolveDirtyTree(
28505
+ {
28506
+ cwd: projectRoot,
28507
+ action: dirtyResolution?.destinationAction ?? "leave",
28508
+ operation: "worktree-to-checkout-destination",
28509
+ confirmDiscard: dirtyResolution?.confirmDiscard
28510
+ },
28511
+ { git }
28512
+ ) : null;
28513
+ const sourceResolved = sourceBefore.isDirty ? resolveDirtyTree(
28514
+ {
28515
+ cwd: worktreeRoot,
28516
+ action: dirtyResolution?.sourceAction ?? "leave",
28517
+ operation: "worktree-to-checkout-source",
28518
+ confirmDiscard: dirtyResolution?.confirmDiscard
28519
+ },
28520
+ { git }
28521
+ ) : null;
28522
+ try {
28523
+ git(projectRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
28524
+ } catch {
28525
+ throw new Error(`The target branch "${branch}" is not available in the Project.`);
28526
+ }
28527
+ removeGit(projectRoot, ["worktree", "remove", worktreeRoot]);
28528
+ gitSafe(projectRoot, ["worktree", "prune"]);
28529
+ try {
28530
+ git(projectRoot, ["checkout", branch]);
28531
+ } catch (checkoutError) {
28532
+ try {
28533
+ git(projectRoot, ["worktree", "add", worktreeRoot, branch]);
28534
+ if (sourceResolved?.stash) {
28535
+ applyMigrationStash({ cwd: worktreeRoot, stash: sourceResolved.stash }, git);
28536
+ }
28537
+ if (destinationResolved?.stash) {
28538
+ applyMigrationStash({ cwd: projectRoot, stash: destinationResolved.stash }, git);
28539
+ }
28540
+ } catch (restoreError) {
28541
+ const checkoutMessage2 = checkoutError instanceof Error ? checkoutError.message : String(checkoutError);
28542
+ const restoreMessage = restoreError instanceof Error ? restoreError.message : String(restoreError);
28543
+ throw new Error(
28544
+ `Project checkout failed (${checkoutMessage2}) and the task worktree could not be restored (${restoreMessage}).`
28545
+ );
28546
+ }
28547
+ const checkoutMessage = checkoutError instanceof Error ? checkoutError.message : String(checkoutError);
28548
+ throw new Error(
28549
+ `Project checkout failed (${checkoutMessage}); restored the task worktree at ${worktreeRoot}.`
28550
+ );
28551
+ }
28552
+ if (sourceResolved?.stash) {
28553
+ try {
28554
+ applyMigrationStash({ cwd: projectRoot, stash: sourceResolved.stash }, git);
28555
+ } catch (applyError) {
28556
+ gitSafe(projectRoot, ["reset", "--hard", "HEAD"]);
28557
+ gitSafe(projectRoot, ["clean", "-fd"]);
28558
+ try {
28559
+ if (destinationBefore.currentBranch && destinationBefore.currentBranch !== branch) {
28560
+ git(projectRoot, ["checkout", destinationBefore.currentBranch]);
28561
+ }
28562
+ git(projectRoot, ["worktree", "add", worktreeRoot, branch]);
28563
+ applyMigrationStash({ cwd: worktreeRoot, stash: sourceResolved.stash }, git);
28564
+ if (destinationResolved?.stash) {
28565
+ applyMigrationStash({ cwd: projectRoot, stash: destinationResolved.stash }, git);
28566
+ }
28567
+ } catch (restoreError) {
28568
+ const applyMessage2 = applyError instanceof Error ? applyError.message : String(applyError);
28569
+ const restoreMessage = restoreError instanceof Error ? restoreError.message : String(restoreError);
28570
+ throw new Error(
28571
+ `Moving uncommitted changes failed (${applyMessage2}); automatic workspace restoration also failed (${restoreMessage}). The named migration stash was preserved.`
28572
+ );
28573
+ }
28574
+ const applyMessage = applyError instanceof Error ? applyError.message : String(applyError);
28575
+ throw new Error(
28576
+ `Moving uncommitted changes failed (${applyMessage}); the original worktree and checkout were restored.`
28577
+ );
28578
+ }
28579
+ }
28580
+ return {
28581
+ ok: true,
28582
+ replayed: false,
28583
+ migrated: [{ ...target, projectPath, worktreePath: requestedWorktreePath, branch }],
28584
+ ...dirtyResolution ? { dirtyResolution } : {}
26526
28585
  };
26527
28586
  }
26528
28587
  function ensureSharedCheckoutBranch(input) {
26529
28588
  assertValidGitRef(input.branchName, "branchName");
26530
28589
  assertValidGitRef(input.baseBranch, "baseBranch");
26531
- const repoPath = (0, import_node_path11.resolve)(input.repoPath);
28590
+ const repoPath = (0, import_node_path12.resolve)(input.repoPath);
26532
28591
  if (gitSafe(repoPath, ["rev-parse", "--is-inside-work-tree"]) !== "true") {
26533
28592
  return { checkedOut: false, reason: `${repoPath} is not a git work tree` };
26534
28593
  }
@@ -26573,7 +28632,7 @@ function resolveSharedCheckoutTargets(input) {
26573
28632
  }
26574
28633
  function resolveRuntimeCwd(payload) {
26575
28634
  const multiRepoWorktree = payload.worktreeInfo?.allWorktrees?.[0]?.worktreePath;
26576
- if (multiRepoWorktree) return (0, import_node_path11.dirname)(multiRepoWorktree);
28635
+ if (multiRepoWorktree) return (0, import_node_path12.dirname)(multiRepoWorktree);
26577
28636
  if (isAlanDefaultWorkspace(payload.projectPath)) return resolveAlanDefaultWorkspacePath();
26578
28637
  return payload.worktreeInfo?.worktreePath ?? payload.projectPath ?? process.cwd();
26579
28638
  }
@@ -26686,14 +28745,14 @@ function localServiceOwnerPayload(owner) {
26686
28745
  ].join("\0");
26687
28746
  }
26688
28747
  function signLocalServiceOwner(secret, owner) {
26689
- return (0, import_node_crypto3.createHmac)("sha256", secret).update(localServiceOwnerPayload(owner)).digest("hex");
28748
+ return (0, import_node_crypto5.createHmac)("sha256", secret).update(localServiceOwnerPayload(owner)).digest("hex");
26690
28749
  }
26691
28750
  function signLocalServiceAdoption(secret, serviceId, challenge) {
26692
- return (0, import_node_crypto3.createHmac)("sha256", secret).update(["adopt", serviceId, challenge].join("\0")).digest("hex");
28751
+ return (0, import_node_crypto5.createHmac)("sha256", secret).update(["adopt", serviceId, challenge].join("\0")).digest("hex");
26693
28752
  }
26694
28753
  function equalHexDigest(actual, expected) {
26695
28754
  if (!/^[a-f0-9]{64}$/i.test(actual) || !/^[a-f0-9]{64}$/i.test(expected)) return false;
26696
- return (0, import_node_crypto3.timingSafeEqual)(Buffer.from(actual, "hex"), Buffer.from(expected, "hex"));
28755
+ return (0, import_node_crypto5.timingSafeEqual)(Buffer.from(actual, "hex"), Buffer.from(expected, "hex"));
26697
28756
  }
26698
28757
  function parseLocalServiceOwner(value2) {
26699
28758
  if (!value2 || typeof value2 !== "object") return null;
@@ -26795,12 +28854,12 @@ function getConfigPath() {
26795
28854
  return process.env.ALAN_AGENT_CONFIG_PATH ?? boundAgentConfigPath ?? resolveAgentConfigPath({ profile: "production" });
26796
28855
  }
26797
28856
  function getEndpointDefaultsPath() {
26798
- return process.env.ALAN_AGENT_ENDPOINTS_PATH ?? (0, import_node_path11.join)((0, import_node_path11.dirname)(getConfigPath()), "endpoints.json");
28857
+ return process.env.ALAN_AGENT_ENDPOINTS_PATH ?? (0, import_node_path12.join)((0, import_node_path12.dirname)(getConfigPath()), "endpoints.json");
26799
28858
  }
26800
28859
  function readConfig() {
26801
28860
  const configPath = getConfigPath();
26802
- if (!(0, import_node_fs13.existsSync)(configPath)) return {};
26803
- const parsed = JSON.parse((0, import_node_fs13.readFileSync)(configPath, "utf8"));
28861
+ if (!(0, import_node_fs14.existsSync)(configPath)) return {};
28862
+ const parsed = JSON.parse((0, import_node_fs14.readFileSync)(configPath, "utf8"));
26804
28863
  const sanitized = sanitizeAgentConfig(parsed);
26805
28864
  if (Object.keys(parsed).some((key) => !Object.hasOwn(sanitized, key))) {
26806
28865
  writeConfig(sanitized);
@@ -26816,27 +28875,27 @@ function readConfigForRegistration() {
26816
28875
  }
26817
28876
  function readEndpointDefaults() {
26818
28877
  const endpointsPath = getEndpointDefaultsPath();
26819
- if (!(0, import_node_fs13.existsSync)(endpointsPath)) return {};
28878
+ if (!(0, import_node_fs14.existsSync)(endpointsPath)) return {};
26820
28879
  try {
26821
- return JSON.parse((0, import_node_fs13.readFileSync)(endpointsPath, "utf8"));
28880
+ return JSON.parse((0, import_node_fs14.readFileSync)(endpointsPath, "utf8"));
26822
28881
  } catch {
26823
28882
  return {};
26824
28883
  }
26825
28884
  }
26826
28885
  function writeConfig(config) {
26827
28886
  const configPath = getConfigPath();
26828
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(configPath), { recursive: true, mode: 448 });
26829
- const pendingPath = `${configPath}.pending-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
28887
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true, mode: 448 });
28888
+ const pendingPath = `${configPath}.pending-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
26830
28889
  try {
26831
- (0, import_node_fs13.writeFileSync)(
28890
+ (0, import_node_fs14.writeFileSync)(
26832
28891
  pendingPath,
26833
28892
  JSON.stringify(sanitizeAgentConfig(config), null, 2),
26834
28893
  { mode: 384 }
26835
28894
  );
26836
- (0, import_node_fs13.chmodSync)(pendingPath, 384);
26837
- (0, import_node_fs13.renameSync)(pendingPath, configPath);
28895
+ (0, import_node_fs14.chmodSync)(pendingPath, 384);
28896
+ (0, import_node_fs14.renameSync)(pendingPath, configPath);
26838
28897
  } finally {
26839
- (0, import_node_fs13.rmSync)(pendingPath, { force: true });
28898
+ (0, import_node_fs14.rmSync)(pendingPath, { force: true });
26840
28899
  }
26841
28900
  }
26842
28901
  function maintenanceLeasePath(configPath) {
@@ -26844,9 +28903,9 @@ function maintenanceLeasePath(configPath) {
26844
28903
  }
26845
28904
  function readLocalDaemonMaintenanceLease(configPath, nowMs = Date.now()) {
26846
28905
  const path = maintenanceLeasePath(configPath);
26847
- if (!(0, import_node_fs13.existsSync)(path)) return null;
28906
+ if (!(0, import_node_fs14.existsSync)(path)) return null;
26848
28907
  try {
26849
- const lease = JSON.parse((0, import_node_fs13.readFileSync)(path, "utf8"));
28908
+ const lease = JSON.parse((0, import_node_fs14.readFileSync)(path, "utf8"));
26850
28909
  if (lease.version !== 1 || typeof lease.id !== "string" || lease.reason !== "credential_cleanup" && lease.reason !== "daemon_startup" && lease.reason !== "watchdog" || !(lease.reason === "credential_cleanup" && lease.expiresAtMs === null || typeof lease.expiresAtMs === "number" && Number.isFinite(lease.expiresAtMs))) {
26851
28910
  throw new Error("invalid maintenance lease");
26852
28911
  }
@@ -26863,8 +28922,8 @@ function readLocalDaemonMaintenanceLease(configPath, nowMs = Date.now()) {
26863
28922
  expiresAtMs: nowMs + 5e3
26864
28923
  };
26865
28924
  try {
26866
- (0, import_node_fs13.writeFileSync)(path, JSON.stringify(fallback), { mode: 384 });
26867
- (0, import_node_fs13.chmodSync)(path, 384);
28925
+ (0, import_node_fs14.writeFileSync)(path, JSON.stringify(fallback), { mode: 384 });
28926
+ (0, import_node_fs14.chmodSync)(path, 384);
26868
28927
  } catch {
26869
28928
  }
26870
28929
  return fallback;
@@ -26875,15 +28934,15 @@ function acquireLocalDaemonMaintenanceLease(configPath, reason, ttlMs = 5e3) {
26875
28934
  if (existing) return { acquired: false, lease: existing };
26876
28935
  const lease = {
26877
28936
  version: 1,
26878
- id: (0, import_node_crypto3.randomUUID)(),
28937
+ id: (0, import_node_crypto5.randomUUID)(),
26879
28938
  reason,
26880
28939
  expiresAtMs: reason === "credential_cleanup" ? null : Date.now() + Math.max(1e3, Math.min(ttlMs, 6e4))
26881
28940
  };
26882
28941
  const path = maintenanceLeasePath(configPath);
26883
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
28942
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(path), { recursive: true, mode: 448 });
26884
28943
  try {
26885
- (0, import_node_fs13.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
26886
- (0, import_node_fs13.chmodSync)(path, 384);
28944
+ (0, import_node_fs14.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
28945
+ (0, import_node_fs14.chmodSync)(path, 384);
26887
28946
  return { acquired: true, lease };
26888
28947
  } catch (error) {
26889
28948
  const raced = readLocalDaemonMaintenanceLease(configPath);
@@ -26899,9 +28958,9 @@ function daemonOwnerLeasePath(configPath) {
26899
28958
  }
26900
28959
  function readLocalDaemonOwnerLease(configPath) {
26901
28960
  const path = daemonOwnerLeasePath(configPath);
26902
- if (!(0, import_node_fs13.existsSync)(path)) return null;
28961
+ if (!(0, import_node_fs14.existsSync)(path)) return null;
26903
28962
  try {
26904
- const lease = JSON.parse((0, import_node_fs13.readFileSync)(path, "utf8"));
28963
+ const lease = JSON.parse((0, import_node_fs14.readFileSync)(path, "utf8"));
26905
28964
  if (lease.version !== 1 || typeof lease.id !== "string" || typeof lease.daemonSessionId !== "string" || !Number.isSafeInteger(lease.pid) || lease.pid <= 0 || typeof lease.startedAtMs !== "number" || !Number.isFinite(lease.startedAtMs) || typeof lease.runtimeId !== "string" || typeof lease.localServiceId !== "string" || !Number.isSafeInteger(lease.localApiPort) || lease.localApiPort < 0 || lease.localApiPort > 65535 || typeof lease.localApiToken !== "string" || lease.localApiToken.length === 0) {
26906
28965
  throw new Error("invalid daemon owner lease");
26907
28966
  }
@@ -26923,10 +28982,13 @@ async function acquireLocalDaemonOwnerLease(input) {
26923
28982
  }
26924
28983
  return { acquired: false, lease: existing };
26925
28984
  }
26926
- if (endpoint.state === "listening_unauthorized" || isProviderProcessAlive(existing.pid)) {
28985
+ if (endpoint.state === "listening_unauthorized") {
28986
+ return { acquired: false, lease: existing };
28987
+ }
28988
+ const journal = inspectLocalDaemonRunJournal(input.configPath);
28989
+ if (isProviderProcessAlive(existing.pid) && !journal.safeToRestart) {
26927
28990
  return { acquired: false, lease: existing };
26928
28991
  }
26929
- inspectLocalDaemonRunJournal(input.configPath);
26930
28992
  releaseOwnedJsonFile(daemonOwnerLeasePath(input.configPath), existing.id);
26931
28993
  }
26932
28994
  if (!existing && input.storedLocalApiPort && input.storedLocalApiToken) {
@@ -26943,7 +29005,7 @@ async function acquireLocalDaemonOwnerLease(input) {
26943
29005
  }
26944
29006
  const lease = {
26945
29007
  version: 1,
26946
- id: (0, import_node_crypto3.randomUUID)(),
29008
+ id: (0, import_node_crypto5.randomUUID)(),
26947
29009
  daemonSessionId: input.daemonSessionId,
26948
29010
  pid: process.pid,
26949
29011
  startedAtMs: Date.now(),
@@ -26953,10 +29015,10 @@ async function acquireLocalDaemonOwnerLease(input) {
26953
29015
  localApiToken: input.localApiToken
26954
29016
  };
26955
29017
  const path = daemonOwnerLeasePath(input.configPath);
26956
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
29018
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(path), { recursive: true, mode: 448 });
26957
29019
  try {
26958
- (0, import_node_fs13.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
26959
- (0, import_node_fs13.chmodSync)(path, 384);
29020
+ (0, import_node_fs14.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
29021
+ (0, import_node_fs14.chmodSync)(path, 384);
26960
29022
  return { acquired: true, lease };
26961
29023
  } catch (error) {
26962
29024
  const raced = readLocalDaemonOwnerLease(input.configPath);
@@ -26971,27 +29033,27 @@ function updateLocalDaemonOwnerEndpoint(configPath, lease, localApiPort) {
26971
29033
  const path = daemonOwnerLeasePath(configPath);
26972
29034
  const pendingPath = `${path}.pending-${lease.id}`;
26973
29035
  try {
26974
- (0, import_node_fs13.writeFileSync)(pendingPath, JSON.stringify(updated), { mode: 384 });
26975
- (0, import_node_fs13.chmodSync)(pendingPath, 384);
26976
- (0, import_node_fs13.renameSync)(pendingPath, path);
29036
+ (0, import_node_fs14.writeFileSync)(pendingPath, JSON.stringify(updated), { mode: 384 });
29037
+ (0, import_node_fs14.chmodSync)(pendingPath, 384);
29038
+ (0, import_node_fs14.renameSync)(pendingPath, path);
26977
29039
  return updated;
26978
29040
  } finally {
26979
- (0, import_node_fs13.rmSync)(pendingPath, { force: true });
29041
+ (0, import_node_fs14.rmSync)(pendingPath, { force: true });
26980
29042
  }
26981
29043
  }
26982
29044
  function releaseLocalDaemonOwnerLease(configPath, leaseId) {
26983
29045
  releaseOwnedJsonFile(daemonOwnerLeasePath(configPath), leaseId);
26984
29046
  }
26985
29047
  function releaseOwnedJsonFile(path, ownerId, ownerField = "id") {
26986
- const releasePath = `${path}.release-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
29048
+ const releasePath = `${path}.release-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
26987
29049
  try {
26988
- (0, import_node_fs13.renameSync)(path, releasePath);
29050
+ (0, import_node_fs14.renameSync)(path, releasePath);
26989
29051
  } catch (error) {
26990
29052
  if (error.code === "ENOENT") return;
26991
29053
  throw error;
26992
29054
  }
26993
29055
  try {
26994
- const content = (0, import_node_fs13.readFileSync)(releasePath, "utf8");
29056
+ const content = (0, import_node_fs14.readFileSync)(releasePath, "utf8");
26995
29057
  let capturedOwnerId = null;
26996
29058
  try {
26997
29059
  const captured = JSON.parse(content);
@@ -27000,13 +29062,13 @@ function releaseOwnedJsonFile(path, ownerId, ownerField = "id") {
27000
29062
  }
27001
29063
  if (capturedOwnerId === ownerId) return;
27002
29064
  try {
27003
- (0, import_node_fs13.writeFileSync)(path, content, { mode: 384, flag: "wx" });
27004
- (0, import_node_fs13.chmodSync)(path, 384);
29065
+ (0, import_node_fs14.writeFileSync)(path, content, { mode: 384, flag: "wx" });
29066
+ (0, import_node_fs14.chmodSync)(path, 384);
27005
29067
  } catch (error) {
27006
29068
  if (error.code !== "EEXIST") throw error;
27007
29069
  }
27008
29070
  } finally {
27009
- (0, import_node_fs13.rmSync)(releasePath, { force: true });
29071
+ (0, import_node_fs14.rmSync)(releasePath, { force: true });
27010
29072
  }
27011
29073
  }
27012
29074
  function inspectLocalDaemonRunJournal(configPath) {
@@ -27016,8 +29078,8 @@ function inspectLocalDaemonRunJournal(configPath) {
27016
29078
  try {
27017
29079
  let activeRunCount = 0;
27018
29080
  for (const artifact of [path, pendingPath]) {
27019
- if (!(0, import_node_fs13.existsSync)(artifact)) continue;
27020
- const parsed = JSON.parse((0, import_node_fs13.readFileSync)(artifact, "utf8"));
29081
+ if (!(0, import_node_fs14.existsSync)(artifact)) continue;
29082
+ const parsed = JSON.parse((0, import_node_fs14.readFileSync)(artifact, "utf8"));
27021
29083
  if (parsed.version !== 1 || !Array.isArray(parsed.entries))
27022
29084
  throw new Error("invalid journal");
27023
29085
  if (artifact === path) {
@@ -27036,7 +29098,7 @@ function inspectLocalDaemonRunJournal(configPath) {
27036
29098
  }).length;
27037
29099
  continue;
27038
29100
  }
27039
- const legacyStartedAtMs = (0, import_node_fs13.statSync)(pendingPath).mtimeMs;
29101
+ const legacyStartedAtMs = (0, import_node_fs14.statSync)(pendingPath).mtimeMs;
27040
29102
  activeRunCount += parsed.entries.filter((entry) => {
27041
29103
  if (!entry || typeof entry !== "object") throw new Error("invalid pending run entry");
27042
29104
  const startedAtMs = entry.startedAtMs;
@@ -27058,38 +29120,38 @@ function pendingSetupIntentPath() {
27058
29120
  return `${getConfigPath()}.setup-pending`;
27059
29121
  }
27060
29122
  function setupTokenHash(setupToken) {
27061
- return (0, import_node_crypto3.createHash)("sha256").update(setupToken).digest("hex");
29123
+ return (0, import_node_crypto5.createHash)("sha256").update(setupToken).digest("hex");
27062
29124
  }
27063
29125
  function getOrCreateSetupAttemptId(setupToken) {
27064
29126
  const intentPath = pendingSetupIntentPath();
27065
29127
  const tokenHash = setupTokenHash(setupToken);
27066
- if ((0, import_node_fs13.existsSync)(intentPath)) {
29128
+ if ((0, import_node_fs14.existsSync)(intentPath)) {
27067
29129
  try {
27068
- const stored = JSON.parse((0, import_node_fs13.readFileSync)(intentPath, "utf8"));
29130
+ const stored = JSON.parse((0, import_node_fs14.readFileSync)(intentPath, "utf8"));
27069
29131
  if (stored.setupTokenHash === tokenHash && typeof stored.registrationAttemptId === "string" && stored.registrationAttemptId.length > 0) {
27070
29132
  return stored.registrationAttemptId;
27071
29133
  }
27072
29134
  } catch {
27073
29135
  }
27074
29136
  }
27075
- const registrationAttemptId = (0, import_node_crypto3.randomUUID)();
27076
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(intentPath), { recursive: true, mode: 448 });
27077
- const writePath = `${intentPath}.pending-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
29137
+ const registrationAttemptId = (0, import_node_crypto5.randomUUID)();
29138
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(intentPath), { recursive: true, mode: 448 });
29139
+ const writePath = `${intentPath}.pending-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
27078
29140
  try {
27079
- (0, import_node_fs13.writeFileSync)(
29141
+ (0, import_node_fs14.writeFileSync)(
27080
29142
  writePath,
27081
29143
  JSON.stringify({ setupTokenHash: tokenHash, registrationAttemptId }, null, 2),
27082
29144
  { mode: 384 }
27083
29145
  );
27084
- (0, import_node_fs13.chmodSync)(writePath, 384);
27085
- (0, import_node_fs13.renameSync)(writePath, intentPath);
29146
+ (0, import_node_fs14.chmodSync)(writePath, 384);
29147
+ (0, import_node_fs14.renameSync)(writePath, intentPath);
27086
29148
  } finally {
27087
- (0, import_node_fs13.rmSync)(writePath, { force: true });
29149
+ (0, import_node_fs14.rmSync)(writePath, { force: true });
27088
29150
  }
27089
29151
  return registrationAttemptId;
27090
29152
  }
27091
29153
  function clearPendingSetupIntent() {
27092
- (0, import_node_fs13.rmSync)(pendingSetupIntentPath(), { force: true });
29154
+ (0, import_node_fs14.rmSync)(pendingSetupIntentPath(), { force: true });
27093
29155
  }
27094
29156
  function pendingDeviceAuthorizationPath() {
27095
29157
  return `${getConfigPath()}.device-auth-pending`;
@@ -27110,35 +29172,35 @@ function parseDeviceAuthorizationOperationContent(content) {
27110
29172
  }
27111
29173
  function restoreCapturedFileUnlessReplaced(path, content) {
27112
29174
  try {
27113
- (0, import_node_fs13.writeFileSync)(path, content, { mode: 384, flag: "wx" });
27114
- (0, import_node_fs13.chmodSync)(path, 384);
29175
+ (0, import_node_fs14.writeFileSync)(path, content, { mode: 384, flag: "wx" });
29176
+ (0, import_node_fs14.chmodSync)(path, 384);
27115
29177
  } catch (error) {
27116
29178
  if (error.code !== "EEXIST") throw error;
27117
29179
  }
27118
29180
  }
27119
29181
  function quarantineCorruptDeviceAuthorizationOperation(path) {
27120
- const quarantinePath = `${path}.corrupt-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
29182
+ const quarantinePath = `${path}.corrupt-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
27121
29183
  try {
27122
- (0, import_node_fs13.renameSync)(path, quarantinePath);
29184
+ (0, import_node_fs14.renameSync)(path, quarantinePath);
27123
29185
  } catch (error) {
27124
29186
  if (error.code === "ENOENT") return null;
27125
29187
  throw error;
27126
29188
  }
27127
29189
  try {
27128
- const capturedContent = (0, import_node_fs13.readFileSync)(quarantinePath, "utf8");
29190
+ const capturedContent = (0, import_node_fs14.readFileSync)(quarantinePath, "utf8");
27129
29191
  if (parseDeviceAuthorizationOperationContent(capturedContent)) {
27130
29192
  restoreCapturedFileUnlessReplaced(path, capturedContent);
27131
29193
  }
27132
29194
  } finally {
27133
- (0, import_node_fs13.rmSync)(quarantinePath, { force: true });
29195
+ (0, import_node_fs14.rmSync)(quarantinePath, { force: true });
27134
29196
  }
27135
- if (!(0, import_node_fs13.existsSync)(path)) return null;
27136
- return parseDeviceAuthorizationOperationContent((0, import_node_fs13.readFileSync)(path, "utf8"));
29197
+ if (!(0, import_node_fs14.existsSync)(path)) return null;
29198
+ return parseDeviceAuthorizationOperationContent((0, import_node_fs14.readFileSync)(path, "utf8"));
27137
29199
  }
27138
29200
  function readDeviceAuthorizationOperation() {
27139
29201
  const path = deviceAuthorizationOperationPath();
27140
- if (!(0, import_node_fs13.existsSync)(path)) return null;
27141
- const parsed = parseDeviceAuthorizationOperationContent((0, import_node_fs13.readFileSync)(path, "utf8"));
29202
+ if (!(0, import_node_fs14.existsSync)(path)) return null;
29203
+ const parsed = parseDeviceAuthorizationOperationContent((0, import_node_fs14.readFileSync)(path, "utf8"));
27142
29204
  const lease = parsed ?? quarantineCorruptDeviceAuthorizationOperation(path);
27143
29205
  if (!lease) return null;
27144
29206
  if (lease.expiresAtMs <= Date.now()) {
@@ -27152,14 +29214,14 @@ function acquireDeviceAuthorizationOperation() {
27152
29214
  if (existing) return null;
27153
29215
  const lease = {
27154
29216
  version: 1,
27155
- id: (0, import_node_crypto3.randomUUID)(),
29217
+ id: (0, import_node_crypto5.randomUUID)(),
27156
29218
  expiresAtMs: Date.now() + 6e4
27157
29219
  };
27158
29220
  const path = deviceAuthorizationOperationPath();
27159
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
29221
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(path), { recursive: true, mode: 448 });
27160
29222
  try {
27161
- (0, import_node_fs13.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
27162
- (0, import_node_fs13.chmodSync)(path, 384);
29223
+ (0, import_node_fs14.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
29224
+ (0, import_node_fs14.chmodSync)(path, 384);
27163
29225
  return lease;
27164
29226
  } catch (error) {
27165
29227
  if (error.code === "EEXIST") return null;
@@ -27172,17 +29234,17 @@ function extendDeviceAuthorizationOperation(operationId, expiresAtMs) {
27172
29234
  throw new Error("browser authorization operation ownership was lost");
27173
29235
  }
27174
29236
  const path = deviceAuthorizationOperationPath();
27175
- const pendingPath = `${path}.pending-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
29237
+ const pendingPath = `${path}.pending-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
27176
29238
  try {
27177
- (0, import_node_fs13.writeFileSync)(
29239
+ (0, import_node_fs14.writeFileSync)(
27178
29240
  pendingPath,
27179
29241
  JSON.stringify({ ...current, expiresAtMs: Math.max(expiresAtMs, Date.now() + 6e4) }),
27180
29242
  { mode: 384 }
27181
29243
  );
27182
- (0, import_node_fs13.chmodSync)(pendingPath, 384);
27183
- (0, import_node_fs13.renameSync)(pendingPath, path);
29244
+ (0, import_node_fs14.chmodSync)(pendingPath, 384);
29245
+ (0, import_node_fs14.renameSync)(pendingPath, path);
27184
29246
  } finally {
27185
- (0, import_node_fs13.rmSync)(pendingPath, { force: true });
29247
+ (0, import_node_fs14.rmSync)(pendingPath, { force: true });
27186
29248
  }
27187
29249
  }
27188
29250
  function assertDeviceAuthorizationOperationOwner(operationId) {
@@ -27193,12 +29255,12 @@ function assertDeviceAuthorizationOperationOwner(operationId) {
27193
29255
  function clearPendingDeviceAuthorization(operationId) {
27194
29256
  const path = pendingDeviceAuthorizationPath();
27195
29257
  if (!operationId) {
27196
- (0, import_node_fs13.rmSync)(path, { force: true });
29258
+ (0, import_node_fs14.rmSync)(path, { force: true });
27197
29259
  return;
27198
29260
  }
27199
- if (!(0, import_node_fs13.existsSync)(path)) return;
29261
+ if (!(0, import_node_fs14.existsSync)(path)) return;
27200
29262
  try {
27201
- const pending = JSON.parse((0, import_node_fs13.readFileSync)(path, "utf8"));
29263
+ const pending = JSON.parse((0, import_node_fs14.readFileSync)(path, "utf8"));
27202
29264
  if (pending.operationId !== operationId) return;
27203
29265
  } catch {
27204
29266
  return;
@@ -27207,9 +29269,9 @@ function clearPendingDeviceAuthorization(operationId) {
27207
29269
  }
27208
29270
  function readPendingDeviceAuthorization(apiUrl) {
27209
29271
  const path = pendingDeviceAuthorizationPath();
27210
- if (!(0, import_node_fs13.existsSync)(path)) return null;
29272
+ if (!(0, import_node_fs14.existsSync)(path)) return null;
27211
29273
  try {
27212
- const pending = JSON.parse((0, import_node_fs13.readFileSync)(path, "utf8"));
29274
+ const pending = JSON.parse((0, import_node_fs14.readFileSync)(path, "utf8"));
27213
29275
  const expiresAtMs = Date.parse(pending.expiresAt ?? "");
27214
29276
  if (pending.apiUrl !== apiUrl || typeof pending.deviceCode !== "string" || typeof pending.userCode !== "string" || typeof pending.verificationUri !== "string" || typeof pending.installationId !== "string" || pending.operationId !== void 0 && typeof pending.operationId !== "string" || !Number.isFinite(pending.intervalSeconds) || !Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
27215
29277
  return null;
@@ -27221,20 +29283,20 @@ function readPendingDeviceAuthorization(apiUrl) {
27221
29283
  }
27222
29284
  function writePendingDeviceAuthorization(pending) {
27223
29285
  const path = pendingDeviceAuthorizationPath();
27224
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
27225
- const writePath = `${path}.pending-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
29286
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(path), { recursive: true, mode: 448 });
29287
+ const writePath = `${path}.pending-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
27226
29288
  try {
27227
- (0, import_node_fs13.writeFileSync)(writePath, JSON.stringify(pending, null, 2), { mode: 384 });
27228
- (0, import_node_fs13.chmodSync)(writePath, 384);
27229
- (0, import_node_fs13.renameSync)(writePath, path);
29289
+ (0, import_node_fs14.writeFileSync)(writePath, JSON.stringify(pending, null, 2), { mode: 384 });
29290
+ (0, import_node_fs14.chmodSync)(writePath, 384);
29291
+ (0, import_node_fs14.renameSync)(writePath, path);
27230
29292
  } finally {
27231
- (0, import_node_fs13.rmSync)(writePath, { force: true });
29293
+ (0, import_node_fs14.rmSync)(writePath, { force: true });
27232
29294
  }
27233
29295
  }
27234
29296
  function removeAlanOwnedLocalArtifacts() {
27235
29297
  const configPath = getConfigPath();
27236
- const directory = (0, import_node_path11.dirname)(configPath);
27237
- const configName = (0, import_node_path11.basename)(configPath).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29298
+ const directory = (0, import_node_path12.dirname)(configPath);
29299
+ const configName = (0, import_node_path12.basename)(configPath).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27238
29300
  const alanAtomicSidecarPattern = new RegExp(
27239
29301
  `^${configName}(?:|\\.setup-pending|\\.device-auth-pending|\\.device-auth-operation\\.json|\\.maintenance-lease\\.json|\\.event-outbox\\.enc)\\.(?:pending|release|corrupt)-\\d+-[a-f0-9]{12}$`
27240
29302
  );
@@ -27251,13 +29313,13 @@ function removeAlanOwnedLocalArtifacts() {
27251
29313
  `${configPath}.pending-runs.json.pending`,
27252
29314
  maintenanceLeasePath(configPath)
27253
29315
  ]);
27254
- if ((0, import_node_fs13.existsSync)(directory)) {
27255
- for (const entry of (0, import_node_fs13.readdirSync)(directory)) {
27256
- if (alanAtomicSidecarPattern.test(entry)) artifacts.add((0, import_node_path11.join)(directory, entry));
29316
+ if ((0, import_node_fs14.existsSync)(directory)) {
29317
+ for (const entry of (0, import_node_fs14.readdirSync)(directory)) {
29318
+ if (alanAtomicSidecarPattern.test(entry)) artifacts.add((0, import_node_path12.join)(directory, entry));
27257
29319
  }
27258
29320
  }
27259
- for (const artifact of artifacts) (0, import_node_fs13.rmSync)(artifact, { force: true });
27260
- (0, import_node_fs13.rmSync)(configPath, { force: true });
29321
+ for (const artifact of artifacts) (0, import_node_fs14.rmSync)(artifact, { force: true });
29322
+ (0, import_node_fs14.rmSync)(configPath, { force: true });
27261
29323
  }
27262
29324
  function argValue(args, name) {
27263
29325
  const index = args.indexOf(name);
@@ -27313,7 +29375,7 @@ function getLocalApiPort(args, stored) {
27313
29375
  }
27314
29376
  function sleep(ms) {
27315
29377
  if (ms <= 0) return Promise.resolve();
27316
- return new Promise((resolve6) => setTimeout(resolve6, ms));
29378
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
27317
29379
  }
27318
29380
  function normalizeBackendKind2(backendKind) {
27319
29381
  if (backendKind === "codex") return "codex_app_server";
@@ -27479,13 +29541,13 @@ var DurablePendingRunStarts = class extends Map {
27479
29541
  }
27480
29542
  persist() {
27481
29543
  if (this.size === 0) {
27482
- (0, import_node_fs13.rmSync)(this.path, { force: true });
29544
+ (0, import_node_fs14.rmSync)(this.path, { force: true });
27483
29545
  return;
27484
29546
  }
27485
29547
  const pendingPath = `${this.path}.pending`;
27486
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(this.path), { recursive: true, mode: 448 });
29548
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(this.path), { recursive: true, mode: 448 });
27487
29549
  try {
27488
- (0, import_node_fs13.writeFileSync)(
29550
+ (0, import_node_fs14.writeFileSync)(
27489
29551
  pendingPath,
27490
29552
  JSON.stringify({
27491
29553
  version: 1,
@@ -27498,10 +29560,10 @@ var DurablePendingRunStarts = class extends Map {
27498
29560
  }),
27499
29561
  { mode: 384 }
27500
29562
  );
27501
- (0, import_node_fs13.chmodSync)(pendingPath, 384);
27502
- (0, import_node_fs13.renameSync)(pendingPath, this.path);
29563
+ (0, import_node_fs14.chmodSync)(pendingPath, 384);
29564
+ (0, import_node_fs14.renameSync)(pendingPath, this.path);
27503
29565
  } finally {
27504
- (0, import_node_fs13.rmSync)(pendingPath, { force: true });
29566
+ (0, import_node_fs14.rmSync)(pendingPath, { force: true });
27505
29567
  }
27506
29568
  }
27507
29569
  };
@@ -27641,7 +29703,7 @@ function buildApplicationRunFailure(input) {
27641
29703
  const problem = createAlanProblem({
27642
29704
  domain: "application",
27643
29705
  code: input.code,
27644
- id: (0, import_node_crypto3.randomUUID)(),
29706
+ id: (0, import_node_crypto5.randomUUID)(),
27645
29707
  title: "Alan could not complete the agent run",
27646
29708
  detail: input.detail,
27647
29709
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27736,21 +29798,21 @@ function reconcileActiveRuns(input) {
27736
29798
  }
27737
29799
  }
27738
29800
  function collectRuntimeMetadata() {
27739
- const cpuList = (0, import_node_os8.cpus)();
29801
+ const cpuList = (0, import_node_os9.cpus)();
27740
29802
  const metadata = {
27741
- platform: (0, import_node_os8.platform)(),
27742
- arch: (0, import_node_os8.arch)(),
27743
- osVersion: (0, import_node_os8.release)(),
29803
+ platform: (0, import_node_os9.platform)(),
29804
+ arch: (0, import_node_os9.arch)(),
29805
+ osVersion: (0, import_node_os9.release)(),
27744
29806
  agentVersion: AGENT_VERSION,
27745
- memoryBytes: (0, import_node_os8.totalmem)(),
27746
- memoryFreeBytes: (0, import_node_os8.freemem)()
29807
+ memoryBytes: (0, import_node_os9.totalmem)(),
29808
+ memoryFreeBytes: (0, import_node_os9.freemem)()
27747
29809
  };
27748
29810
  if (cpuList.length > 0) {
27749
29811
  metadata.cpuModel = cpuList[0]?.model;
27750
29812
  metadata.cpuCores = cpuList.length;
27751
29813
  }
27752
29814
  try {
27753
- const disk = (0, import_node_fs13.statfsSync)((0, import_node_os8.homedir)());
29815
+ const disk = (0, import_node_fs14.statfsSync)((0, import_node_os9.homedir)());
27754
29816
  metadata.diskFreeBytes = disk.bavail * disk.bsize;
27755
29817
  metadata.diskTotalBytes = disk.blocks * disk.bsize;
27756
29818
  } catch {
@@ -27968,7 +30030,7 @@ var RuntimePresenter = class {
27968
30030
  this.applicationProblem = createAlanProblem({
27969
30031
  domain: "application",
27970
30032
  code: /local path no longer exists/i.test(message) ? "app.workspace.missing" : "app.internal",
27971
- id: (0, import_node_crypto3.randomUUID)(),
30033
+ id: (0, import_node_crypto5.randomUUID)(),
27972
30034
  title: "Alan could not start the agent run",
27973
30035
  detail: message,
27974
30036
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -28006,9 +30068,9 @@ async function setupDaemon(args) {
28006
30068
  const teamId = argValue(args, "--team") ?? process.env.ALAN_TEAM_ID;
28007
30069
  const setupToken = argValue(args, "--setup-token") ?? process.env.ALAN_RUNTIME_SETUP_TOKEN;
28008
30070
  const token = argValue(args, "--token") ?? process.env.ALAN_SETUP_TOKEN;
28009
- const displayName = argValue(args, "--name") ?? (0, import_node_os8.hostname)();
30071
+ const displayName = argValue(args, "--name") ?? (0, import_node_os9.hostname)();
28010
30072
  const scope = argValue(args, "--scope") ?? process.env.ALAN_RUNTIME_SCOPE ?? (setupToken ? "team" : "user");
28011
- const installationId = argValue(args, "--installation-id") ?? previousConfig.installationId ?? (0, import_node_crypto3.randomUUID)();
30073
+ const installationId = argValue(args, "--installation-id") ?? previousConfig.installationId ?? (0, import_node_crypto5.randomUUID)();
28012
30074
  if (scope !== "team" && scope !== "user") {
28013
30075
  throw new Error("setup --scope must be either 'team' or 'user'");
28014
30076
  }
@@ -28017,7 +30079,7 @@ async function setupDaemon(args) {
28017
30079
  "setup requires --setup-token <token> or --token <Alan access token>.\nFor normal setup, copy the setup token from Alan and run: alan-agent setup --setup-token <token>\nFor browser authorization, run: alan-agent login"
28018
30080
  );
28019
30081
  }
28020
- const registrationAttemptId = setupToken ? getOrCreateSetupAttemptId(setupToken) : (0, import_node_crypto3.randomUUID)();
30082
+ const registrationAttemptId = setupToken ? getOrCreateSetupAttemptId(setupToken) : (0, import_node_crypto5.randomUUID)();
28021
30083
  const registrationUrl = `${apiUrl}/public/runtimes${setupToken ? "/register-with-setup-token" : ""}`;
28022
30084
  const registrationInit = {
28023
30085
  method: "POST",
@@ -28029,7 +30091,7 @@ async function setupDaemon(args) {
28029
30091
  ...teamId ? { teamId } : {},
28030
30092
  ...setupToken ? { setupToken, registrationAttemptId } : {},
28031
30093
  displayName,
28032
- hostname: (0, import_node_os8.hostname)(),
30094
+ hostname: (0, import_node_os9.hostname)(),
28033
30095
  runtimeKind: "machine",
28034
30096
  managementKind: "user_managed",
28035
30097
  hostKind: "daemon",
@@ -28052,7 +30114,7 @@ async function setupDaemon(args) {
28052
30114
  }
28053
30115
  const retriableStatus = response && (response.status === 408 || response.status === 429 || response.status >= 500);
28054
30116
  if (response && (!retriableStatus || response.ok) || attempt === 3) break;
28055
- await new Promise((resolve6) => setTimeout(resolve6, attempt * 100));
30117
+ await new Promise((resolve7) => setTimeout(resolve7, attempt * 100));
28056
30118
  }
28057
30119
  if (!response) {
28058
30120
  const detail = transportError instanceof Error ? transportError.message : String(transportError);
@@ -28083,10 +30145,10 @@ async function setupDaemon(args) {
28083
30145
  runtimeToken: body.runtimeToken,
28084
30146
  runtimeRenewalToken: body.runtimeRenewalToken,
28085
30147
  localApiPort: getLocalApiPort(args, {}),
28086
- localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto3.randomBytes)(24).toString("hex"),
28087
- localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto3.randomUUID)(),
28088
- localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url"),
28089
- eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url"),
30148
+ localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto5.randomBytes)(24).toString("hex"),
30149
+ localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto5.randomUUID)(),
30150
+ localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url"),
30151
+ eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url"),
28090
30152
  daemonEpoch: previousConfig.daemonEpoch,
28091
30153
  displayName: body.runtime.displayName ?? displayName,
28092
30154
  installationId
@@ -28113,10 +30175,10 @@ async function loginWithDeviceCode(args) {
28113
30175
  }
28114
30176
  const previousConfig = readConfigForRegistration();
28115
30177
  const { apiUrl, wsUrl, endpointProfile } = resolveEndpoints(args);
28116
- const displayName = argValue(args, "--name") ?? (0, import_node_os8.hostname)();
30178
+ const displayName = argValue(args, "--name") ?? (0, import_node_os9.hostname)();
28117
30179
  const maxPolls = Number.parseInt(argValue(args, "--max-polls") ?? "300", 10);
28118
30180
  const resumed = readPendingDeviceAuthorization(apiUrl);
28119
- const installationId = resumed?.installationId ?? previousConfig.installationId ?? (0, import_node_crypto3.randomUUID)();
30181
+ const installationId = resumed?.installationId ?? previousConfig.installationId ?? (0, import_node_crypto5.randomUUID)();
28120
30182
  let authorization = resumed ? { ...resumed, operationId: operation.id } : null;
28121
30183
  if (!authorization) {
28122
30184
  clearPendingDeviceAuthorization();
@@ -28125,7 +30187,7 @@ async function loginWithDeviceCode(args) {
28125
30187
  headers: { "content-type": "application/json" },
28126
30188
  body: JSON.stringify({
28127
30189
  displayName,
28128
- hostname: (0, import_node_os8.hostname)(),
30190
+ hostname: (0, import_node_os9.hostname)(),
28129
30191
  capabilities: discoverCapabilities(),
28130
30192
  metadata: {
28131
30193
  ...await collectRuntimeMetadataWithProviderLimits(),
@@ -28192,10 +30254,10 @@ async function loginWithDeviceCode(args) {
28192
30254
  runtimeToken: body.runtimeToken,
28193
30255
  runtimeRenewalToken: body.runtimeRenewalToken,
28194
30256
  localApiPort: getLocalApiPort(args, {}),
28195
- localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto3.randomBytes)(24).toString("hex"),
28196
- localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto3.randomUUID)(),
28197
- localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url"),
28198
- eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url"),
30257
+ localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto5.randomBytes)(24).toString("hex"),
30258
+ localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto5.randomUUID)(),
30259
+ localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url"),
30260
+ eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url"),
28199
30261
  daemonEpoch: previousConfig.daemonEpoch,
28200
30262
  displayName: body.runtime.displayName ?? displayName,
28201
30263
  installationId
@@ -28215,10 +30277,10 @@ async function loginWithDeviceCode(args) {
28215
30277
  async function startDaemon(args) {
28216
30278
  bindConfigPath(args);
28217
30279
  const stored = readConfig();
28218
- const installationId = stored.installationId ?? (0, import_node_crypto3.randomUUID)();
30280
+ const installationId = stored.installationId ?? (0, import_node_crypto5.randomUUID)();
28219
30281
  const configPath = getConfigPath();
28220
30282
  const endpointProfile = stored.endpointProfile ?? resolveEndpointProfile(args);
28221
- const daemonSessionId = (0, import_node_crypto3.randomUUID)();
30283
+ const daemonSessionId = (0, import_node_crypto5.randomUUID)();
28222
30284
  let heartbeatSeq = 0;
28223
30285
  const runtimeId = argValue(args, "--runtime-id") ?? process.env.ALAN_RUNTIME_ID ?? stored.runtimeId;
28224
30286
  const runtimeTokenOverride = argValue(args, "--token") ?? process.env.ALAN_RUNTIME_TOKEN;
@@ -28235,11 +30297,11 @@ async function startDaemon(args) {
28235
30297
  wsUrl = endpointMismatch.expectedWsUrl;
28236
30298
  }
28237
30299
  let localApiPort = getLocalApiPort(args, stored);
28238
- const localApiToken = stored.localApiToken ?? (0, import_node_crypto3.randomBytes)(24).toString("hex");
28239
- const localServiceId = stored.localServiceId ?? (0, import_node_crypto3.randomUUID)();
28240
- const localServiceSecret = stored.localServiceSecret ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url");
30300
+ const localApiToken = stored.localApiToken ?? (0, import_node_crypto5.randomBytes)(24).toString("hex");
30301
+ const localServiceId = stored.localServiceId ?? (0, import_node_crypto5.randomUUID)();
30302
+ const localServiceSecret = stored.localServiceSecret ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url");
28241
30303
  const localServiceStartedAtMs = Date.now();
28242
- const eventSpoolKey = stored.eventSpoolKey ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url");
30304
+ const eventSpoolKey = stored.eventSpoolKey ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url");
28243
30305
  if (stored.daemonEpoch !== void 0 && (!Number.isSafeInteger(stored.daemonEpoch) || stored.daemonEpoch < 0 || stored.daemonEpoch >= Number.MAX_SAFE_INTEGER)) {
28244
30306
  throw new Error("daemon config contains an invalid durable epoch; repair the runtime config");
28245
30307
  }
@@ -28294,7 +30356,7 @@ async function startDaemon(args) {
28294
30356
  ownerLeaseHeld = false;
28295
30357
  releaseLocalDaemonOwnerLease(configPath, ownerLease.id);
28296
30358
  };
28297
- const activeAgents = /* @__PURE__ */ new Map();
30359
+ const activeAgents = new ObservableMap();
28298
30360
  const pendingRunStarts = new DurablePendingRunStarts(
28299
30361
  `${configPath}.pending-runs.json`,
28300
30362
  daemonSessionId
@@ -28315,6 +30377,13 @@ async function startDaemon(args) {
28315
30377
  recentLogs.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${message}`);
28316
30378
  if (recentLogs.length > 200) recentLogs.splice(0, recentLogs.length - 200);
28317
30379
  };
30380
+ const sleepInhibitor = new SleepInhibitor({ log: pushLog });
30381
+ activeAgents.onChange = () => {
30382
+ sleepInhibitor.setActive(activeAgents.size > 0);
30383
+ };
30384
+ if (sleepInhibitor.backendNames().length > 0) {
30385
+ pushLog(`sleep-inhibitor backends=${sleepInhibitor.backendNames().join(",")}`);
30386
+ }
28318
30387
  const alanMcpRegisteredRuns = /* @__PURE__ */ new Set();
28319
30388
  const alanMcpRegisteredBackends = /* @__PURE__ */ new Set();
28320
30389
  const finalizeAlanMcpForRun = (runId) => {
@@ -28359,7 +30428,7 @@ async function startDaemon(args) {
28359
30428
  const workspaceRelocations = /* @__PURE__ */ new Map();
28360
30429
  for (const relocation of stored.workspaceRelocations ?? []) {
28361
30430
  if (relocation?.conversationId && relocation.runId && relocation.previousPath && relocation.canonicalPath) {
28362
- workspaceRelocations.set((0, import_node_path11.resolve)(relocation.previousPath), relocation);
30431
+ workspaceRelocations.set((0, import_node_path12.resolve)(relocation.previousPath), relocation);
28363
30432
  }
28364
30433
  }
28365
30434
  if (stored.localApiPort !== localApiPort || stored.localApiToken !== localApiToken || stored.localServiceId !== localServiceId || stored.localServiceSecret !== localServiceSecret || stored.eventSpoolKey !== eventSpoolKey || stored.daemonEpoch !== daemonEpoch || stored.installationId !== installationId) {
@@ -28543,6 +30612,22 @@ async function startDaemon(args) {
28543
30612
  emitRunnerPresence();
28544
30613
  flushWorkspaceRelocations();
28545
30614
  }, 15e3);
30615
+ let pendingWakeGapMs = null;
30616
+ const flushWakeGap = () => {
30617
+ if (pendingWakeGapMs === null || !socket.connected) return;
30618
+ const gapMs = pendingWakeGapMs;
30619
+ pendingWakeGapMs = null;
30620
+ socket.emit("runtime.wake", { gapMs });
30621
+ pushLog(`runtime-wake reported gapMs=${gapMs}`);
30622
+ };
30623
+ const suspensionDetector = new SuspensionDetector({
30624
+ onSuspensionDetected: (gapMs) => {
30625
+ pendingWakeGapMs = Math.max(pendingWakeGapMs ?? 0, gapMs);
30626
+ pushLog(`host-suspension detected gapMs=${gapMs}`);
30627
+ flushWakeGap();
30628
+ }
30629
+ });
30630
+ suspensionDetector.start();
28546
30631
  void refreshProviderVersions();
28547
30632
  const versionRefresh = setInterval(() => {
28548
30633
  void refreshProviderVersions();
@@ -28634,6 +30719,7 @@ async function startDaemon(args) {
28634
30719
  runnerStatus: runnerSummary.runnerStatus
28635
30720
  }
28636
30721
  });
30722
+ flushWakeGap();
28637
30723
  scheduleRuntimeModelScan(() => Promise.resolve(metadata));
28638
30724
  });
28639
30725
  });
@@ -28675,20 +30761,25 @@ async function startDaemon(args) {
28675
30761
  pendingDeliveryLost.push({ conversationId: entry.conversationId, runId });
28676
30762
  }
28677
30763
  });
28678
- socket.on("desktop:rpc", (message) => {
30764
+ socket.on("desktop:rpc", async (message) => {
28679
30765
  if (!message?.id || !message.method) return;
28680
30766
  try {
28681
30767
  const params = message.params ?? {};
28682
- if (message.method !== "worktree.ensure") {
30768
+ const result = message.method === "worktree.ensure" ? ensureWorktree(params) : message.method === "worktree.remove_if_clean" ? removeWorktreesIfClean(params) : message.method === "worktree.migrate_to_checkout" ? migrateWorktreeToCheckout(params) : message.method === "worktree.migration_preflight" ? preflightLocalWorkspaceMigration(params) : message.method === "worktree.status" ? statusLocalWorkspace(params) : isBindingSwitchGitMethod(message.method) ? await executeBindingSwitchGitRpc(message.method, params) : (() => {
28683
30769
  throw new Error(`Method not found: ${message.method}`);
28684
- }
28685
- const result = ensureWorktree(params);
30770
+ })();
28686
30771
  socket.emit("desktop:rpc", { jsonrpc: "2.0", id: message.id, result });
28687
30772
  } catch (error) {
30773
+ const stableCode = error instanceof BindingSwitchGitError ? error.code : "runtime_operation_failed";
30774
+ const stableDetails = error instanceof BindingSwitchGitError ? error.details : void 0;
28688
30775
  socket.emit("desktop:rpc", {
28689
30776
  jsonrpc: "2.0",
28690
30777
  id: message.id,
28691
- error: { code: -32e3, message: error.message }
30778
+ error: {
30779
+ code: -32e3,
30780
+ message: error.message,
30781
+ data: { ...stableDetails, code: stableCode }
30782
+ }
28692
30783
  });
28693
30784
  }
28694
30785
  });
@@ -28696,6 +30787,17 @@ async function startDaemon(args) {
28696
30787
  if (!payload?.runId || !payload.conversationId || typeof payload.content !== "string") {
28697
30788
  return;
28698
30789
  }
30790
+ const isTaskWorkspacePayload = Boolean(
30791
+ payload.worktreeInfo?.worktreePath || payload.worktreeInfo?.allWorktrees?.length
30792
+ );
30793
+ if ((payload.worktreeInfo?.allWorktrees?.length ?? 0) > 1 || isTaskWorkspacePayload && (payload.repoLocalPaths?.length ?? 0) > 1) {
30794
+ socket.emit("agent.rejected", {
30795
+ runId: payload.runId,
30796
+ message: "A task worktree requires exactly one Git repository.",
30797
+ code: "single_repository_worktree_required"
30798
+ });
30799
+ return;
30800
+ }
28699
30801
  const existingRun = activeAgents.get(payload.runId);
28700
30802
  if (existingRun) {
28701
30803
  if (existingRun.conversationId !== payload.conversationId) {
@@ -28788,31 +30890,31 @@ async function startDaemon(args) {
28788
30890
  const workspaceRequired = Boolean(explicitWorkspacePath);
28789
30891
  if (isAlanDefaultWorkspace(payload.projectPath)) {
28790
30892
  try {
28791
- (0, import_node_fs13.mkdirSync)(cwd, { recursive: true });
30893
+ (0, import_node_fs14.mkdirSync)(cwd, { recursive: true });
28792
30894
  } catch {
28793
30895
  }
28794
30896
  }
28795
30897
  let workspaceReadiness = inspectWorkspaceReadiness(workspaceRequired ? cwd : void 0);
28796
30898
  const expectedRepoUrls = [
28797
30899
  ...(payload.repoLocalPaths ?? []).filter(
28798
- (repo) => explicitWorkspacePath ? (0, import_node_path11.resolve)(repo.localPath) === (0, import_node_path11.resolve)(explicitWorkspacePath) : false
30900
+ (repo) => explicitWorkspacePath ? (0, import_node_path12.resolve)(repo.localPath) === (0, import_node_path12.resolve)(explicitWorkspacePath) : false
28799
30901
  ).map((repo) => repo.repoUrl),
28800
30902
  ...payload.localRepoUrls ?? []
28801
30903
  ];
28802
30904
  if (workspaceReadiness.code === "workspace_missing" && explicitWorkspacePath && !payload.worktreeInfo) {
28803
- const previousPath = (0, import_node_path11.resolve)(explicitWorkspacePath);
30905
+ const previousPath = (0, import_node_path12.resolve)(explicitWorkspacePath);
28804
30906
  const remembered = workspaceRelocations.get(previousPath);
28805
30907
  const rememberedPath = remembered?.canonicalPath;
28806
30908
  const relocated = rememberedPath && workspaceMatchesExpectedRepositories({
28807
30909
  workspacePath: rememberedPath,
28808
30910
  expectedRepoUrls
28809
- }) ? (0, import_node_path11.resolve)(rememberedPath) : discoverRelocatedWorkspace({
30911
+ }) ? (0, import_node_path12.resolve)(rememberedPath) : discoverRelocatedWorkspace({
28810
30912
  missingPath: explicitWorkspacePath,
28811
30913
  approvedWorkspacePaths: payload.approvedWorkspacePaths,
28812
30914
  expectedRepoUrls
28813
30915
  });
28814
30916
  if (relocated) {
28815
- if (!remembered || (0, import_node_path11.resolve)(remembered.canonicalPath) !== relocated) {
30917
+ if (!remembered || (0, import_node_path12.resolve)(remembered.canonicalPath) !== relocated) {
28816
30918
  workspaceRelocations.set(previousPath, {
28817
30919
  conversationId: payload.conversationId,
28818
30920
  runId: payload.runId,
@@ -29280,7 +31382,7 @@ async function startDaemon(args) {
29280
31382
  const requestId = payload.requestId;
29281
31383
  try {
29282
31384
  const projectPath = isAlanDefaultWorkspace(payload.projectPath) ? resolveAlanDefaultWorkspacePath() : payload.projectPath;
29283
- const result = scanLocalSkillsCached((0, import_node_os8.homedir)(), projectPath, {
31385
+ const result = scanLocalSkillsCached((0, import_node_os9.homedir)(), projectPath, {
29284
31386
  refresh: payload.refresh === true
29285
31387
  });
29286
31388
  socket.emit("runtime.skills.result", {
@@ -29308,7 +31410,7 @@ async function startDaemon(args) {
29308
31410
  if (!oldest) break;
29309
31411
  adoptionChallenges.delete(oldest);
29310
31412
  }
29311
- const challenge = (0, import_node_crypto3.randomBytes)(24).toString("base64url");
31413
+ const challenge = (0, import_node_crypto5.randomBytes)(24).toString("base64url");
29312
31414
  adoptionChallenges.set(challenge, now + LOCAL_SERVICE_CHALLENGE_TTL_MS);
29313
31415
  const unsignedOwner = {
29314
31416
  version: LOCAL_SERVICE_OWNER_VERSION,
@@ -29493,6 +31595,7 @@ async function startDaemon(args) {
29493
31595
  clearInterval(heartbeat);
29494
31596
  clearInterval(versionRefresh);
29495
31597
  clearInterval(workspaceLeaseMonitor);
31598
+ suspensionDetector.stop();
29496
31599
  socket.disconnect();
29497
31600
  releaseOwnerLease();
29498
31601
  releaseStartupLease();
@@ -29504,6 +31607,7 @@ async function startDaemon(args) {
29504
31607
  clearInterval(heartbeat);
29505
31608
  clearInterval(versionRefresh);
29506
31609
  clearInterval(workspaceLeaseMonitor);
31610
+ suspensionDetector.stop();
29507
31611
  socket.disconnect();
29508
31612
  releaseOwnerLease();
29509
31613
  releaseStartupLease();
@@ -29511,11 +31615,12 @@ async function startDaemon(args) {
29511
31615
  `${describeOccupiedLocalDaemon(localApiPort)} The token in ${getConfigPath()} no longer matches the running daemon; quit Alan desktop or run setup again.`
29512
31616
  );
29513
31617
  }
29514
- const localApiListening = new Promise((resolve6, reject) => {
31618
+ const localApiListening = new Promise((resolve7, reject) => {
29515
31619
  const onError = (error) => {
29516
31620
  clearInterval(heartbeat);
29517
31621
  clearInterval(versionRefresh);
29518
31622
  clearInterval(workspaceLeaseMonitor);
31623
+ suspensionDetector.stop();
29519
31624
  socket.disconnect();
29520
31625
  if (error.code === "EADDRINUSE") {
29521
31626
  void probeLocalDaemonEndpoint(localApiPort).then((probe) => {
@@ -29540,7 +31645,7 @@ async function startDaemon(args) {
29540
31645
  if (address && typeof address === "object") localApiPort = address.port;
29541
31646
  pushLog(`local_api listening port=${localApiPort}`);
29542
31647
  console.info("[alan-agent] Local daemon API listening", { port: localApiPort });
29543
- resolve6();
31648
+ resolve7();
29544
31649
  });
29545
31650
  });
29546
31651
  try {
@@ -29558,8 +31663,9 @@ async function startDaemon(args) {
29558
31663
  clearInterval(heartbeat);
29559
31664
  clearInterval(versionRefresh);
29560
31665
  clearInterval(workspaceLeaseMonitor);
31666
+ suspensionDetector.stop();
29561
31667
  socket.disconnect();
29562
- await new Promise((resolve6) => localServer.close(() => resolve6()));
31668
+ await new Promise((resolve7) => localServer.close(() => resolve7()));
29563
31669
  releaseOwnerLease();
29564
31670
  releaseStartupLease();
29565
31671
  throw error;
@@ -29573,15 +31679,17 @@ async function startDaemon(args) {
29573
31679
  clearInterval(heartbeat);
29574
31680
  clearInterval(versionRefresh);
29575
31681
  clearInterval(workspaceLeaseMonitor);
31682
+ suspensionDetector.stop();
29576
31683
  for (const entry of activeAgents.values()) {
29577
31684
  abortActiveAgent(entry, { userInitiated: true });
29578
31685
  }
29579
31686
  activeAgents.clear();
29580
31687
  socket.disconnect();
29581
- await new Promise((resolve6) => {
29582
- localServer.close(() => resolve6());
31688
+ await new Promise((resolve7) => {
31689
+ localServer.close(() => resolve7());
29583
31690
  });
29584
31691
  } finally {
31692
+ sleepInhibitor.dispose();
29585
31693
  releaseOwnerLease();
29586
31694
  }
29587
31695
  };
@@ -30016,7 +32124,7 @@ function decodeResumeFallbackContext(encoded) {
30016
32124
  }
30017
32125
 
30018
32126
  // src/web-presenter.ts
30019
- var import_node_crypto4 = require("crypto");
32127
+ var import_node_crypto6 = require("crypto");
30020
32128
  var WebPresenter = class {
30021
32129
  constructor(wsClient, conversationId, cwd) {
30022
32130
  this.conversationId = conversationId;
@@ -30193,7 +32301,7 @@ var WebPresenter = class {
30193
32301
  this.applicationProblem = createAlanProblem({
30194
32302
  domain: "application",
30195
32303
  code: /required tools|prepare the required tools/i.test(message) ? "app.service.unavailable" : "app.internal",
30196
- id: (0, import_node_crypto4.randomUUID)(),
32304
+ id: (0, import_node_crypto6.randomUUID)(),
30197
32305
  title: "Alan could not continue the agent run",
30198
32306
  detail: message,
30199
32307
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -30260,7 +32368,7 @@ var WebPresenter = class {
30260
32368
  };
30261
32369
 
30262
32370
  // src/ws-client.ts
30263
- var import_node_crypto6 = require("crypto");
32371
+ var import_node_crypto8 = require("crypto");
30264
32372
 
30265
32373
  // ../shared/dist/agent-liveness.js
30266
32374
  var AGENT_LIVENESS_PROTOCOL_VERSION = 1;
@@ -30288,8 +32396,8 @@ var agentProbeAckSchema = external_exports.object({
30288
32396
  });
30289
32397
 
30290
32398
  // src/sandbox-outbox.ts
30291
- var import_node_crypto5 = require("crypto");
30292
- var import_node_fs14 = require("fs");
32399
+ var import_node_crypto7 = require("crypto");
32400
+ var import_node_fs15 = require("fs");
30293
32401
  var SANDBOX_EVENT_OUTBOX_ENV = "ALAN_EVENT_OUTBOX_ENABLED";
30294
32402
  function isSandboxEventOutboxEnabled(env = process.env) {
30295
32403
  return env[SANDBOX_EVENT_OUTBOX_ENV] === "true";
@@ -30298,7 +32406,7 @@ function deriveSandboxOutboxKey(sessionToken) {
30298
32406
  if (!sessionToken) {
30299
32407
  throw new Error("sandbox event outbox requires ALAN_SESSION_TOKEN for key derivation");
30300
32408
  }
30301
- return (0, import_node_crypto5.createHash)("sha256").update(sessionToken, "utf8").digest("base64url");
32409
+ return (0, import_node_crypto7.createHash)("sha256").update(sessionToken, "utf8").digest("base64url");
30302
32410
  }
30303
32411
  function sandboxOutboxSpoolPath(conversationId) {
30304
32412
  return `/tmp/alan-agent-outbox-${conversationId}.enc`;
@@ -30312,7 +32420,7 @@ function createSandboxEventOutbox(input) {
30312
32420
  input.pushLog?.(
30313
32421
  `sandbox_outbox_spool_reset ${error instanceof Error ? error.message : String(error)}`
30314
32422
  );
30315
- (0, import_node_fs14.rmSync)(path, { force: true });
32423
+ (0, import_node_fs15.rmSync)(path, { force: true });
30316
32424
  return new EncryptedEventOutbox(path, key, input.pushLog, input.options);
30317
32425
  }
30318
32426
  }
@@ -30435,7 +32543,7 @@ var WSClient = class {
30435
32543
  }
30436
32544
  socket;
30437
32545
  /** Stable id for this agent process; fences stale heartbeats server-side. */
30438
- agentSessionId = (0, import_node_crypto6.randomUUID)();
32546
+ agentSessionId = (0, import_node_crypto8.randomUUID)();
30439
32547
  /** Monotonic heartbeat sequence — advances on every hello + heartbeat. */
30440
32548
  heartbeatSeq = 0;
30441
32549
  heartbeatTimer = null;
@@ -30515,9 +32623,9 @@ var WSClient = class {
30515
32623
  });
30516
32624
  }
30517
32625
  waitForConnection(timeoutMs = 15e3) {
30518
- return new Promise((resolve6, reject) => {
32626
+ return new Promise((resolve7, reject) => {
30519
32627
  if (this.socket.connected) {
30520
- resolve6();
32628
+ resolve7();
30521
32629
  return;
30522
32630
  }
30523
32631
  const timeout = setTimeout(
@@ -30526,7 +32634,7 @@ var WSClient = class {
30526
32634
  );
30527
32635
  this.socket.once("connect", () => {
30528
32636
  clearTimeout(timeout);
30529
- resolve6();
32637
+ resolve7();
30530
32638
  });
30531
32639
  this.socket.once("connect_error", (err) => {
30532
32640
  clearTimeout(timeout);
@@ -30707,9 +32815,9 @@ async function runSandbox(config) {
30707
32815
  onUserMessage: (payload) => {
30708
32816
  if (payload === null) {
30709
32817
  if (pendingMessageResolve) {
30710
- const resolve6 = pendingMessageResolve;
32818
+ const resolve7 = pendingMessageResolve;
30711
32819
  pendingMessageResolve = null;
30712
- resolve6(null);
32820
+ resolve7(null);
30713
32821
  }
30714
32822
  return;
30715
32823
  }
@@ -30731,9 +32839,9 @@ async function runSandbox(config) {
30731
32839
  fileCount: payload.files?.length ?? 0
30732
32840
  });
30733
32841
  if (pendingMessageResolve) {
30734
- const resolve6 = pendingMessageResolve;
32842
+ const resolve7 = pendingMessageResolve;
30735
32843
  pendingMessageResolve = null;
30736
- resolve6(payload);
32844
+ resolve7(payload);
30737
32845
  } else {
30738
32846
  queuedMessages.push(payload);
30739
32847
  }
@@ -30926,8 +33034,8 @@ async function runSandbox(config) {
30926
33034
  });
30927
33035
  if (stopped) break;
30928
33036
  }
30929
- const nextPayload = queuedMessages.shift() ?? await new Promise((resolve6) => {
30930
- pendingMessageResolve = resolve6;
33037
+ const nextPayload = queuedMessages.shift() ?? await new Promise((resolve7) => {
33038
+ pendingMessageResolve = resolve7;
30931
33039
  });
30932
33040
  if (nextPayload === null || stopped) {
30933
33041
  break;
@@ -31009,10 +33117,10 @@ async function runSandbox(config) {
31009
33117
  }
31010
33118
 
31011
33119
  // src/service-manager.ts
31012
- var import_node_child_process7 = require("child_process");
31013
- var import_node_fs15 = require("fs");
31014
- var import_node_os9 = require("os");
31015
- var import_node_path12 = require("path");
33120
+ var import_node_child_process9 = require("child_process");
33121
+ var import_node_fs16 = require("fs");
33122
+ var import_node_os10 = require("os");
33123
+ var import_node_path13 = require("path");
31016
33124
  var SERVICE_LABEL = "ai.tryalan.agent";
31017
33125
  var SYSTEMD_UNIT = "alan-agent.service";
31018
33126
  var WINDOWS_TASK = "Alan Agent";
@@ -31032,8 +33140,8 @@ function buildDaemonServicePlan(input) {
31032
33140
  if (userId === void 0) throw new Error("Cannot determine the current macOS user ID");
31033
33141
  const domain = `gui/${userId}`;
31034
33142
  const serviceTarget = `${domain}/${SERVICE_LABEL}`;
31035
- const manifestPath = (0, import_node_path12.join)(input.homeDir, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
31036
- const logDir = (0, import_node_path12.join)(input.homeDir, ".alan", "agent", "logs");
33143
+ const manifestPath = (0, import_node_path13.join)(input.homeDir, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
33144
+ const logDir = (0, import_node_path13.join)(input.homeDir, ".alan", "agent", "logs");
31037
33145
  const programArguments = daemonArgs.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
31038
33146
  const manifest = `<?xml version="1.0" encoding="UTF-8"?>
31039
33147
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -31055,9 +33163,9 @@ ${programArguments}
31055
33163
  <key>ThrottleInterval</key>
31056
33164
  <integer>5</integer>
31057
33165
  <key>StandardOutPath</key>
31058
- <string>${xml((0, import_node_path12.join)(logDir, "daemon.log"))}</string>
33166
+ <string>${xml((0, import_node_path13.join)(logDir, "daemon.log"))}</string>
31059
33167
  <key>StandardErrorPath</key>
31060
- <string>${xml((0, import_node_path12.join)(logDir, "daemon-error.log"))}</string>
33168
+ <string>${xml((0, import_node_path13.join)(logDir, "daemon-error.log"))}</string>
31061
33169
  </dict>
31062
33170
  </plist>
31063
33171
  `;
@@ -31096,7 +33204,7 @@ ${programArguments}
31096
33204
  };
31097
33205
  }
31098
33206
  if (input.platform === "linux") {
31099
- const manifestPath = (0, import_node_path12.join)(input.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
33207
+ const manifestPath = (0, import_node_path13.join)(input.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
31100
33208
  const manifest = `[Unit]
31101
33209
  Description=Alan local agent daemon
31102
33210
  After=network-online.target
@@ -31193,7 +33301,7 @@ WantedBy=default.target
31193
33301
  };
31194
33302
  }
31195
33303
  function currentServicePlan(args) {
31196
- const currentPlatform = (0, import_node_os9.platform)();
33304
+ const currentPlatform = (0, import_node_os10.platform)();
31197
33305
  if (currentPlatform !== "darwin" && currentPlatform !== "linux" && currentPlatform !== "win32") {
31198
33306
  throw new Error(`Daemon service installation is not supported on ${currentPlatform}`);
31199
33307
  }
@@ -31202,25 +33310,25 @@ function currentServicePlan(args) {
31202
33310
  if (!cliEntry) throw new Error("Cannot determine the alan-agent executable path");
31203
33311
  return buildDaemonServicePlan({
31204
33312
  platform: currentPlatform,
31205
- homeDir: (0, import_node_os9.homedir)(),
33313
+ homeDir: (0, import_node_os10.homedir)(),
31206
33314
  nodeExecutable: process.execPath,
31207
- cliEntry: (0, import_node_path12.resolve)(cliEntry),
33315
+ cliEntry: (0, import_node_path13.resolve)(cliEntry),
31208
33316
  profile
31209
33317
  });
31210
33318
  }
31211
33319
  function writeManifest(path, contents) {
31212
- (0, import_node_fs15.mkdirSync)((0, import_node_path12.dirname)(path), { recursive: true, mode: 448 });
33320
+ (0, import_node_fs16.mkdirSync)((0, import_node_path13.dirname)(path), { recursive: true, mode: 448 });
31213
33321
  const pendingPath = `${path}.pending-${process.pid}`;
31214
33322
  try {
31215
- (0, import_node_fs15.writeFileSync)(pendingPath, contents, { mode: 384 });
31216
- (0, import_node_fs15.chmodSync)(pendingPath, 384);
31217
- (0, import_node_fs15.renameSync)(pendingPath, path);
33323
+ (0, import_node_fs16.writeFileSync)(pendingPath, contents, { mode: 384 });
33324
+ (0, import_node_fs16.chmodSync)(pendingPath, 384);
33325
+ (0, import_node_fs16.renameSync)(pendingPath, path);
31218
33326
  } finally {
31219
- (0, import_node_fs15.rmSync)(pendingPath, { force: true });
33327
+ (0, import_node_fs16.rmSync)(pendingPath, { force: true });
31220
33328
  }
31221
33329
  }
31222
33330
  function runServiceCommand(command) {
31223
- const result = (0, import_node_child_process7.spawnSync)(command.command, command.args, { encoding: "utf8" });
33331
+ const result = (0, import_node_child_process9.spawnSync)(command.command, command.args, { encoding: "utf8" });
31224
33332
  const status = result.status ?? 1;
31225
33333
  const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
31226
33334
  if (status !== 0 && !command.tolerateFailure) {
@@ -31238,8 +33346,8 @@ function installDaemonService(args = []) {
31238
33346
  }
31239
33347
  const plan = currentServicePlan(args);
31240
33348
  if (plan.manifestPath && plan.manifest) {
31241
- if ((0, import_node_os9.platform)() === "darwin") {
31242
- (0, import_node_fs15.mkdirSync)((0, import_node_path12.join)((0, import_node_os9.homedir)(), ".alan", "agent", "logs"), { recursive: true, mode: 448 });
33349
+ if ((0, import_node_os10.platform)() === "darwin") {
33350
+ (0, import_node_fs16.mkdirSync)((0, import_node_path13.join)((0, import_node_os10.homedir)(), ".alan", "agent", "logs"), { recursive: true, mode: 448 });
31243
33351
  }
31244
33352
  writeManifest(plan.manifestPath, plan.manifest);
31245
33353
  }
@@ -31249,7 +33357,7 @@ function installDaemonService(args = []) {
31249
33357
  function uninstallDaemonService(args = []) {
31250
33358
  const plan = currentServicePlan(args);
31251
33359
  for (const command of plan.uninstallCommands) runServiceCommand(command);
31252
- if (plan.manifestPath) (0, import_node_fs15.rmSync)(plan.manifestPath, { force: true });
33360
+ if (plan.manifestPath) (0, import_node_fs16.rmSync)(plan.manifestPath, { force: true });
31253
33361
  console.info("[alan-agent] Per-user daemon service removed");
31254
33362
  }
31255
33363
  function printDaemonServiceStatus(args = []) {
@@ -31263,7 +33371,7 @@ function printDaemonServiceStatus(args = []) {
31263
33371
  function logEffectiveGitIdentity(lifecycle, projectPath) {
31264
33372
  const readConfig2 = (key) => {
31265
33373
  try {
31266
- const result = (0, import_node_child_process8.spawnSync)("git", ["config", "--get", key], {
33374
+ const result = (0, import_node_child_process10.spawnSync)("git", ["config", "--get", key], {
31267
33375
  cwd: projectPath,
31268
33376
  env: getDaemonCliEnvironment(),
31269
33377
  encoding: "utf8",