@aiden-ade/sandbox-agent 0.1.49 → 0.1.51

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 +2584 -787
  2. package/package.json +5 -5
package/dist/index.cjs CHANGED
@@ -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",
@@ -4810,6 +4810,351 @@ var require_websocket_server = __commonJS({
4810
4810
  }
4811
4811
  });
4812
4812
 
4813
+ // src/index.ts
4814
+ var import_node_child_process9 = require("child_process");
4815
+
4816
+ // src/cli-executable.ts
4817
+ var import_node_child_process = require("child_process");
4818
+ var import_node_fs = require("fs");
4819
+ var import_node_os = require("os");
4820
+ var import_node_path = require("path");
4821
+ var PROVIDER_CLI_COMMANDS = {
4822
+ claude_cli: { provider: "claude_cli", command: "claude", envVars: ["ALAN_CLAUDE_PATH"] },
4823
+ codex_app_server: {
4824
+ provider: "codex_app_server",
4825
+ command: "codex",
4826
+ envVars: ["ALAN_CODEX_PATH"]
4827
+ },
4828
+ copilot_cli: { provider: "copilot_cli", command: "copilot", envVars: ["ALAN_COPILOT_PATH"] },
4829
+ cursor_agent_cli: {
4830
+ provider: "cursor_agent_cli",
4831
+ command: "cursor-agent",
4832
+ envVars: ["ALAN_CURSOR_AGENT_PATH"]
4833
+ },
4834
+ antigravity_cli: {
4835
+ provider: "antigravity_cli",
4836
+ command: "agy",
4837
+ envVars: ["ALAN_ANTIGRAVITY_PATH"]
4838
+ },
4839
+ kimi_cli: { provider: "kimi_cli", command: "kimi-cli", envVars: ["ALAN_KIMI_PATH"] },
4840
+ grok_cli: { provider: "grok_cli", command: "grok", envVars: ["ALAN_GROK_PATH"] },
4841
+ opencode_cli: { provider: "opencode_cli", command: "opencode", envVars: ["ALAN_OPENCODE_PATH"] },
4842
+ droid_cli: { provider: "droid_cli", command: "droid", envVars: ["ALAN_DROID_PATH"] },
4843
+ supatest_cli: { provider: "supatest_cli", command: "supatest", envVars: ["ALAN_SUPATEST_PATH"] }
4844
+ };
4845
+ var DISCOVERABLE_PROVIDER_KINDS = Object.keys(PROVIDER_CLI_COMMANDS);
4846
+ var PROVIDER_ALIASES = {
4847
+ agy: "antigravity_cli",
4848
+ antigravity: "antigravity_cli",
4849
+ antigravity_cli: "antigravity_cli",
4850
+ codex: "codex_app_server",
4851
+ gemini: "antigravity_cli",
4852
+ gemini_cli: "antigravity_cli"
4853
+ };
4854
+ var BACKEND_CLI_COMMANDS = {
4855
+ claude_cli: "claude",
4856
+ codex: "codex",
4857
+ codex_app_server: "codex",
4858
+ copilot_cli: "copilot",
4859
+ cursor_agent_cli: "cursor-agent",
4860
+ agy: "agy",
4861
+ antigravity: "agy",
4862
+ antigravity_cli: "agy",
4863
+ gemini_cli: "agy",
4864
+ gemini: "agy",
4865
+ kimi_cli: "kimi-cli",
4866
+ grok_cli: "grok",
4867
+ opencode_cli: "opencode",
4868
+ droid_cli: "droid",
4869
+ supatest_cli: "supatest"
4870
+ };
4871
+ var CLI_COMMAND_ALIASES = {
4872
+ "kimi-cli": ["kimi"],
4873
+ grok: ["grok-build"]
4874
+ };
4875
+ var resolvedCliCache = /* @__PURE__ */ new Map();
4876
+ var NEGATIVE_RESOLUTION_TTL_MS = 15e3;
4877
+ var negativeResolutionExpiry = /* @__PURE__ */ new Map();
4878
+ var cachedLoginShellEnv = null;
4879
+ function parseEnvOutput(output) {
4880
+ const loginEnv = {};
4881
+ for (const line of output.split("\n")) {
4882
+ if (!line.trim()) continue;
4883
+ const eqIdx = line.indexOf("=");
4884
+ if (eqIdx <= 0) continue;
4885
+ const key = line.slice(0, eqIdx);
4886
+ if (/\s/.test(key)) continue;
4887
+ loginEnv[key] = line.slice(eqIdx + 1);
4888
+ }
4889
+ if (!loginEnv.HOME) loginEnv.HOME = process.env.HOME ?? (0, import_node_os.homedir)();
4890
+ if (!loginEnv.SHELL) loginEnv.SHELL = process.env.SHELL ?? "/bin/zsh";
4891
+ return loginEnv;
4892
+ }
4893
+ function loadLoginShellEnvironment() {
4894
+ if (cachedLoginShellEnv) return cachedLoginShellEnv;
4895
+ if ((0, import_node_os.platform)() === "win32") {
4896
+ cachedLoginShellEnv = process.env;
4897
+ return cachedLoginShellEnv;
4898
+ }
4899
+ try {
4900
+ const shell = (process.env.SHELL || "/bin/bash").replace(/'/g, "'\\''");
4901
+ const envOutput = (0, import_node_child_process.execSync)(`'${shell}' -ilc 'env'`, {
4902
+ encoding: "utf8",
4903
+ stdio: ["pipe", "pipe", "pipe"],
4904
+ timeout: 5e3
4905
+ });
4906
+ cachedLoginShellEnv = parseEnvOutput(envOutput);
4907
+ } catch {
4908
+ cachedLoginShellEnv = process.env;
4909
+ }
4910
+ return cachedLoginShellEnv;
4911
+ }
4912
+ function getDaemonCliEnvironment() {
4913
+ const base = (0, import_node_os.platform)() === "win32" ? process.env : loadLoginShellEnvironment();
4914
+ return augmentCliPath(base);
4915
+ }
4916
+ function cacheResolvedCli(command, resolvedPath) {
4917
+ resolvedCliCache.set(command, resolvedPath);
4918
+ if (resolvedPath === null) {
4919
+ negativeResolutionExpiry.set(command, Date.now() + NEGATIVE_RESOLUTION_TTL_MS);
4920
+ } else {
4921
+ negativeResolutionExpiry.delete(command);
4922
+ }
4923
+ }
4924
+ function getCachedResolvedCli(command) {
4925
+ const cached = resolvedCliCache.get(command);
4926
+ if (cached === null) {
4927
+ const expiresAt = negativeResolutionExpiry.get(command);
4928
+ if (expiresAt === void 0 || Date.now() >= expiresAt) {
4929
+ resolvedCliCache.delete(command);
4930
+ negativeResolutionExpiry.delete(command);
4931
+ return void 0;
4932
+ }
4933
+ }
4934
+ return cached;
4935
+ }
4936
+ function resolveViaWhere(command, env) {
4937
+ if ((0, import_node_os.platform)() !== "win32") return null;
4938
+ const whereExe = (0, import_node_path.join)(
4939
+ env.SystemRoot ?? process.env.SystemRoot ?? "C:\\Windows",
4940
+ "System32",
4941
+ "where.exe"
4942
+ );
4943
+ if (!(0, import_node_fs.existsSync)(whereExe)) return null;
4944
+ const result = (0, import_node_child_process.spawnSync)(whereExe, [command], {
4945
+ encoding: "utf8",
4946
+ env,
4947
+ windowsHide: true,
4948
+ timeout: 3e3
4949
+ });
4950
+ if (result.error || result.status !== 0) return null;
4951
+ for (const line of result.stdout.split(/\r?\n/)) {
4952
+ const trimmed = line.trim();
4953
+ if (!trimmed) continue;
4954
+ if (isRunnableFile(trimmed)) return trimmed;
4955
+ }
4956
+ return null;
4957
+ }
4958
+ function runCliProbeAsync(executable, env, args = ["--version"], timeoutMs = 3e3, spawnOptions) {
4959
+ return new Promise((resolve7) => {
4960
+ const isWin = (0, import_node_os.platform)() === "win32";
4961
+ let command = executable;
4962
+ let commandArgs = args;
4963
+ if (isWin && /\.(cmd|bat)$/i.test(executable)) {
4964
+ command = env.ComSpec ?? process.env.ComSpec ?? "cmd.exe";
4965
+ commandArgs = ["/d", "/s", "/c", executable, ...args];
4966
+ }
4967
+ const useShell = isWin && command === executable && !/[\\/]/.test(executable);
4968
+ const closeStdin = spawnOptions?.closeStdin === true;
4969
+ let child;
4970
+ try {
4971
+ child = (0, import_node_child_process.spawn)(command, commandArgs, {
4972
+ env,
4973
+ shell: useShell,
4974
+ windowsHide: isWin ? true : void 0,
4975
+ timeout: timeoutMs,
4976
+ stdio: closeStdin ? ["pipe", "pipe", "pipe"] : void 0
4977
+ });
4978
+ } catch (error) {
4979
+ resolve7({ status: null, stdout: "", stderr: "", error });
4980
+ return;
4981
+ }
4982
+ if (closeStdin) {
4983
+ child.stdin?.end();
4984
+ }
4985
+ let stdout = "";
4986
+ let stderr = "";
4987
+ child.stdout?.setEncoding("utf8").on("data", (chunk) => {
4988
+ stdout += chunk;
4989
+ });
4990
+ child.stderr?.setEncoding("utf8").on("data", (chunk) => {
4991
+ stderr += chunk;
4992
+ });
4993
+ child.once("error", (error) => resolve7({ status: null, stdout, stderr, error }));
4994
+ child.once(
4995
+ "close",
4996
+ (status, signal) => resolve7({ status, stdout, stderr, signal: signal ?? null })
4997
+ );
4998
+ });
4999
+ }
5000
+ function runCliVersionProbeAsync(executable, env, args = ["--version"], timeoutMs = 3e3) {
5001
+ return runCliProbeAsync(executable, env, args, timeoutMs);
5002
+ }
5003
+ function pathSeparator() {
5004
+ return (0, import_node_os.platform)() === "win32" ? ";" : ":";
5005
+ }
5006
+ function splitPath(pathValue) {
5007
+ if (!pathValue) return [];
5008
+ return pathValue.split(pathSeparator()).filter(Boolean);
5009
+ }
5010
+ function isRunnableFile(path) {
5011
+ if (!(0, import_node_fs.existsSync)(path)) return false;
5012
+ if ((0, import_node_os.platform)() === "win32") return true;
5013
+ try {
5014
+ (0, import_node_fs.accessSync)(path, import_node_fs.constants.X_OK);
5015
+ return true;
5016
+ } catch {
5017
+ return false;
5018
+ }
5019
+ }
5020
+ function windowsCommandCandidates(command, pathExt) {
5021
+ const trimmed = command.trim();
5022
+ if (!trimmed) return [];
5023
+ const hasExtension = /\.[a-z0-9]+$/i.test(trimmed);
5024
+ if (hasExtension) return [trimmed];
5025
+ const extensions = pathExt.split(";").map((ext) => ext.trim()).filter(Boolean);
5026
+ return [trimmed, ...extensions.map((ext) => `${trimmed}${ext}`)];
5027
+ }
5028
+ function augmentCliPath(env) {
5029
+ const home = env.HOME || env.USERPROFILE || (0, import_node_os.homedir)();
5030
+ const isWin = (0, import_node_os.platform)() === "win32";
5031
+ const extraPaths = isWin ? [
5032
+ (0, import_node_path.join)(home, "AppData", "Roaming", "npm"),
5033
+ (0, import_node_path.join)(home, "AppData", "Local", "Programs", "Microsoft", "WindowsApps"),
5034
+ (0, import_node_path.join)(home, ".local", "bin"),
5035
+ "C:\\Program Files\\nodejs",
5036
+ "C:\\Program Files\\Git\\cmd"
5037
+ ] : [
5038
+ (0, import_node_path.join)(home, ".local", "bin"),
5039
+ (0, import_node_path.join)(home, ".local", "node", "bin"),
5040
+ (0, import_node_path.join)(home, ".bun", "bin"),
5041
+ (0, import_node_path.join)(home, ".cargo", "bin"),
5042
+ "/opt/homebrew/bin",
5043
+ "/usr/local/bin"
5044
+ ];
5045
+ const basePath = env.PATH ?? "";
5046
+ const existing = new Set(splitPath(basePath));
5047
+ const missing = extraPaths.filter((dir) => !existing.has(dir));
5048
+ if (missing.length === 0) return env;
5049
+ return {
5050
+ ...env,
5051
+ PATH: missing.length > 0 ? `${missing.join(pathSeparator())}${pathSeparator()}${basePath}` : basePath
5052
+ };
5053
+ }
5054
+ function resolveCliExecutable(command, env = getDaemonCliEnvironment()) {
5055
+ const trimmed = command.trim();
5056
+ if (!trimmed) return null;
5057
+ const cached = getCachedResolvedCli(trimmed);
5058
+ if (cached === null) return null;
5059
+ if (cached && isRunnableFile(cached)) return cached;
5060
+ if ((trimmed.includes("/") || trimmed.includes("\\")) && isRunnableFile(trimmed)) {
5061
+ cacheResolvedCli(trimmed, trimmed);
5062
+ return trimmed;
5063
+ }
5064
+ const enriched = augmentCliPath(env);
5065
+ const home = enriched.HOME || enriched.USERPROFILE || (0, import_node_os.homedir)();
5066
+ const isWin = (0, import_node_os.platform)() === "win32";
5067
+ const pathExt = enriched.PATHEXT ?? (isWin ? ".EXE;.CMD;.BAT;.COM" : "");
5068
+ const commandNames = [trimmed, ...CLI_COMMAND_ALIASES[trimmed] ?? []];
5069
+ const names = commandNames.flatMap(
5070
+ (name) => isWin ? windowsCommandCandidates(name, pathExt) : [name]
5071
+ );
5072
+ const candidates = [];
5073
+ for (const name of names) {
5074
+ if (name.includes("/") || name.includes("\\")) {
5075
+ candidates.push(name);
5076
+ continue;
5077
+ }
5078
+ for (const dir of splitPath(enriched.PATH)) {
5079
+ candidates.push((0, import_node_path.join)(dir, name));
5080
+ }
5081
+ candidates.push((0, import_node_path.join)(home, ".local", "bin", name), (0, import_node_path.join)(home, ".local", "node", "bin", name));
5082
+ if (!isWin) {
5083
+ candidates.push((0, import_node_path.join)("/opt/homebrew/bin", name), (0, import_node_path.join)("/usr/local/bin", name));
5084
+ } else {
5085
+ candidates.push((0, import_node_path.join)(home, "AppData", "Roaming", "npm", name));
5086
+ }
5087
+ }
5088
+ const seen = /* @__PURE__ */ new Set();
5089
+ for (const candidate of candidates) {
5090
+ if (seen.has(candidate)) continue;
5091
+ seen.add(candidate);
5092
+ if (isRunnableFile(candidate)) {
5093
+ cacheResolvedCli(trimmed, candidate);
5094
+ return candidate;
5095
+ }
5096
+ }
5097
+ const viaWhere = resolveViaWhere(trimmed, enriched);
5098
+ if (viaWhere) {
5099
+ cacheResolvedCli(trimmed, viaWhere);
5100
+ return viaWhere;
5101
+ }
5102
+ cacheResolvedCli(trimmed, null);
5103
+ return null;
5104
+ }
5105
+ function normalizeDiscoverableProvider(provider) {
5106
+ const alias = PROVIDER_ALIASES[provider];
5107
+ if (alias) return alias;
5108
+ if (provider in PROVIDER_CLI_COMMANDS) {
5109
+ return provider;
5110
+ }
5111
+ return null;
5112
+ }
5113
+ function resolveProviderCliCommand(provider, env = getDaemonCliEnvironment()) {
5114
+ const normalized = normalizeDiscoverableProvider(provider);
5115
+ if (!normalized) return null;
5116
+ const spec = PROVIDER_CLI_COMMANDS[normalized];
5117
+ const override = spec.envVars.map((name) => process.env[name]?.trim() || env[name]?.trim()).find(Boolean);
5118
+ if (override) {
5119
+ const resolved2 = resolveCliExecutable(override, env) ?? (isRunnableFile(override) ? override : null);
5120
+ if (resolved2) cacheResolvedCli(spec.command, resolved2);
5121
+ return resolved2;
5122
+ }
5123
+ const resolved = resolveCliExecutable(spec.command, env);
5124
+ if (resolved) cacheResolvedCli(spec.command, resolved);
5125
+ return resolved;
5126
+ }
5127
+ function resolveBackendRuntimeCommand(backendKind, env = getDaemonCliEnvironment()) {
5128
+ if (!backendKind) return void 0;
5129
+ const command = BACKEND_CLI_COMMANDS[backendKind];
5130
+ if (!command) return void 0;
5131
+ const cached = getCachedResolvedCli(command);
5132
+ if (cached === null) return void 0;
5133
+ if (cached && isRunnableFile(cached)) return cached;
5134
+ const spec = Object.values(PROVIDER_CLI_COMMANDS).find((entry) => entry.command === command);
5135
+ const envOverride = spec ? spec.envVars.map((name) => env[name]?.trim()).find(Boolean) : void 0;
5136
+ const resolved = resolveCliExecutable(envOverride?.trim() || command, env);
5137
+ cacheResolvedCli(command, resolved);
5138
+ return resolved ?? void 0;
5139
+ }
5140
+ function invalidateResolvedCli(command) {
5141
+ const trimmed = command.trim();
5142
+ if (trimmed) {
5143
+ resolvedCliCache.delete(trimmed);
5144
+ negativeResolutionExpiry.delete(trimmed);
5145
+ }
5146
+ }
5147
+ function revalidateBackendRuntimeCommand(backendKind, env = getDaemonCliEnvironment()) {
5148
+ if (!backendKind) return void 0;
5149
+ const command = BACKEND_CLI_COMMANDS[backendKind];
5150
+ if (!command) return void 0;
5151
+ invalidateResolvedCli(command);
5152
+ const spec = Object.values(PROVIDER_CLI_COMMANDS).find((entry) => entry.command === command);
5153
+ const envOverride = spec ? spec.envVars.map((name) => env[name]?.trim()).find(Boolean) : void 0;
5154
+ if (envOverride) invalidateResolvedCli(envOverride);
5155
+ return resolveBackendRuntimeCommand(backendKind, env);
5156
+ }
5157
+
4813
5158
  // src/cli-help.ts
4814
5159
  var HELP_TEXT = `Alan agent CLI
4815
5160
 
@@ -4859,12 +5204,12 @@ function resolveCommand(args, env) {
4859
5204
  }
4860
5205
 
4861
5206
  // src/daemon.ts
4862
- var import_node_child_process6 = require("child_process");
4863
- var import_node_crypto3 = require("crypto");
4864
- var import_node_fs13 = require("fs");
5207
+ var import_node_child_process7 = require("child_process");
5208
+ var import_node_crypto5 = require("crypto");
5209
+ var import_node_fs14 = require("fs");
4865
5210
  var import_node_http = __toESM(require("http"), 1);
4866
- var import_node_os8 = require("os");
4867
- var import_node_path11 = require("path");
5211
+ var import_node_os9 = require("os");
5212
+ var import_node_path12 = require("path");
4868
5213
 
4869
5214
  // ../agent-core/dist/index.js
4870
5215
  var import_fs = require("fs");
@@ -5029,6 +5374,16 @@ Your job is to take the repository in this fresh cloud sandbox from an unknown s
5029
5374
  - You are inside an ephemeral cloud sandbox. Its filesystem can be captured as a reusable snapshot for future sessions.
5030
5375
  - Do not assume you are on the user's laptop or that tools outside this sandbox exist.
5031
5376
  - Install and build inside the workspace. Keep all setup repeatable from a fresh clone.
5377
+
5378
+ ### Sandbox privileges
5379
+
5380
+ - You are the non-root user \`alan\`; \`HOME=/home/alan\`, repos in \`/home/alan/workspace\`, which you fully own. Node/pnpm/CLIs are on \`PATH\`, and \`npm install -g\` needs no sudo.
5381
+ - **Installing system packages is allowed**, via sudo scoped to package managers: \`sudo apt-get\`, \`sudo apt\`, \`sudo dpkg\`, \`sudo add-apt-repository\`. Other sudo (\`su\`, \`sh\`, \`systemctl\`) is refused by design.
5382
+ - **Always \`sudo apt-get update\` first.** Package lists ship empty, so installing without it fails with "Unable to locate package" \u2014 which reads as "permission denied" but is not:
5383
+ \`sudo apt-get update && sudo apt-get install -y --no-install-recommends <pkgs>\`
5384
+ - Never hand-compile system packages into \`$HOME\` to route around a permission you already have. Use apt; fall back to a user-local install only if the package truly is not in apt.
5385
+ - No Docker daemon or socket. Ports below 1024 bind fine without elevation.
5386
+ - Read the real error before blaming permissions: \`ELIFECYCLE\` is a generic pnpm epilogue, not a diagnosis. Find the \`ERR_\`/\`gyp ERR!\` line above it. A native build failure means a missing system package, not a missing privilege.
5032
5387
  - Persist the final ordered commands with \`set_environment_setup_commands\`. Each command is replayed in a fresh shell, so no command may depend on shell state from a previous command.
5033
5388
  - The snapshot policy is \`block_if_dirty\`: the working tree must be clean before capture.
5034
5389
 
@@ -5038,8 +5393,8 @@ Your job is to take the repository in this fresh cloud sandbox from an unknown s
5038
5393
  2. **Set up** \u2014 install dependencies, select compatible runtimes, configure non-secret defaults, and build. Diagnose failures yourself before asking the user.
5039
5394
  3. **Run and verify** \u2014 start the real app or service. Prove it works using process stability, logs, HTTP probes, and \`agent-browser\` interaction when a UI exists. A process merely starting or dependencies merely installing is not success.
5040
5395
  4. **Persist** \u2014 call \`set_environment_setup_commands\` with only the ordered commands you actually verified from a fresh-shell model.
5041
- 5. **Prepare capture** \u2014 ensure every repository is clean. Verify that \`git status --porcelain --untracked-files=all\` is empty; generated untracked files count as dirty and must be removed or covered by an appropriate existing ignore rule. If setup requires tracked changes, create a focused branch, commit them, push it, and open a PR with \`github_pr_create\` explaining why the setup change is needed. Never commit secrets.
5042
- 6. **Human gate** \u2014 summarize the evidence and ask the user to reply exactly \`Capture snapshot\`. Do not call \`capture_environment_snapshot\` before that explicit confirmation.
5396
+ 5. **Prepare capture** \u2014 ensure every repository is clean. Verify that \`git status --porcelain --untracked-files=all\` is empty; generated untracked files count as dirty and must be removed or covered by an appropriate existing ignore rule. If setup requires tracked changes, create a focused branch, commit them, push it, and open a PR with \`github_pr_create\` explaining why the setup change is needed. When this setup session is attached to a task, link that PR with \`github_pr_link_task\`. Never commit secrets.
5397
+ 6. **Human gate** \u2014 summarize the evidence and ask the user to approve the snapshot. The chat shows them a **Capture snapshot** button, so invite them to use it rather than dictating an exact phrase to type. Do not call \`capture_environment_snapshot\` before that approval arrives.
5043
5398
  7. **Capture and re-verify** \u2014 after approval, call \`capture_environment_snapshot\` with \`afterSnapshot: "resume"\`. Poll \`get_cloud_machine\` until the snapshot is ready, then confirm the sandbox remains usable and repeat the key runtime probe before declaring completion.
5044
5399
 
5045
5400
  ## Autonomy
@@ -11023,7 +11378,7 @@ var EPIC_STATUS_OPTIONS = [
11023
11378
  value: "canceled",
11024
11379
  label: "Canceled",
11025
11380
  color: CATEGORICAL_COLORS.slateClosed,
11026
- group: "done"
11381
+ group: "closed"
11027
11382
  }
11028
11383
  ];
11029
11384
  var DEFAULT_EPIC_STATUSES = EPIC_STATUS_OPTIONS.map((o) => ({
@@ -11777,7 +12132,12 @@ function normalizeCodexCliErrorMessage(message) {
11777
12132
  if (lower.includes("reconnecting") && lower.includes("request timed out")) {
11778
12133
  return "Codex connection to OpenAI timed out while waiting for the turn to continue. Retry the message; if it repeats, check network/API latency or reduce slow MCP/tool calls in the turn.";
11779
12134
  }
11780
- if (lower.includes("access token could not be refreshed") || lower.includes("refresh_token_invalidated") || lower.includes("refresh token has been invalidated")) {
12135
+ if (lower.includes("access token could not be refreshed") || lower.includes("refresh_token_invalidated") || lower.includes("refresh token has been invalidated") || // OpenAI's ChatGPT backend rejects a stale bearer with this pair on both the
12136
+ // HTTP and WSS paths, e.g. "Reconnecting... 2/5 (unexpected status 401
12137
+ // Unauthorized: Provided authentication token is expired. ... auth error
12138
+ // code: token_expired)". Left unmapped these render as a raw retry banner
12139
+ // with a generic "check your API key" hint and no recovery path.
12140
+ lower.includes("token_expired") || lower.includes("provided authentication token is expired")) {
11781
12141
  return "Codex CLI connection expired. Re-authenticate the Codex CLI on this computer (run `codex login`) or reconnect it in Cloud settings, then retry.";
11782
12142
  }
11783
12143
  return message;
@@ -12014,12 +12374,29 @@ function formatCliSpawnError(command, error) {
12014
12374
  `CLI command not found: ${command}. Install it or configure this provider with the full executable path.`
12015
12375
  );
12016
12376
  }
12377
+ if (error.code === "ETXTBSY") {
12378
+ return new Error(
12379
+ `CLI "${command}" is still being written by an installer (ETXTBSY) and did not become runnable in time. Retry in a moment.`
12380
+ );
12381
+ }
12017
12382
  return error;
12018
12383
  }
12019
- function waitForSpawnOutcome(child) {
12384
+ var SPAWN_RETRY_DELAYS_BY_CODE = {
12385
+ ETXTBSY: [250, 750, 1500, 3e3, 5e3],
12386
+ ENOENT: [250, 750]
12387
+ };
12388
+ var delay = (ms) => new Promise((resolve22) => setTimeout(resolve22, ms));
12389
+ var SPAWN_OUTCOME_TIMEOUT_MS = 2e3;
12390
+ function waitForSpawnOutcome(child, timeoutMs = SPAWN_OUTCOME_TIMEOUT_MS) {
12020
12391
  return new Promise((resolve22) => {
12021
- child.once("spawn", () => resolve22(null));
12022
- child.once("error", (error) => resolve22(error));
12392
+ const timer = setTimeout(() => resolve22(null), timeoutMs);
12393
+ timer.unref?.();
12394
+ const settle = (value2) => {
12395
+ clearTimeout(timer);
12396
+ resolve22(value2);
12397
+ };
12398
+ child.once("spawn", () => settle(null));
12399
+ child.once("error", (error) => settle(error));
12023
12400
  });
12024
12401
  }
12025
12402
  function createGenericCliBackend(options) {
@@ -12037,28 +12414,35 @@ function createGenericCliBackend(options) {
12037
12414
  const args = options.buildArgs?.(context, prompt) ?? options.args.map((arg) => arg === "{{prompt}}" ? prompt : arg);
12038
12415
  let command = options.command;
12039
12416
  let child = spawnCli(command, args, context);
12040
- if (context.revalidateCommand) {
12041
- const spawnError = await waitForSpawnOutcome(child);
12042
- if (spawnError) {
12043
- const fresh = spawnError.code === "ENOENT" ? context.revalidateCommand()?.trim() || null : null;
12044
- if (fresh && fresh !== command) {
12045
- console.warn(
12046
- `[${options.kind}] Spawn failed ENOENT for "${command}" \u2014 re-resolved to "${fresh}", retrying once`
12047
- );
12048
- command = fresh;
12049
- child = spawnCli(command, args, context);
12050
- } else {
12051
- throw formatCliSpawnError(command, spawnError);
12052
- }
12053
- }
12054
- }
12055
12417
  state.process = child;
12056
12418
  state.lastRawOutputAtMs = Date.now();
12057
- context.onProcessSpawned?.(child);
12058
12419
  context.registerLivenessProbe?.(() => ({
12059
12420
  providerAlive: child.exitCode === null,
12060
12421
  lastRawOutputAgoMs: typeof state.lastRawOutputAtMs === "number" ? Date.now() - state.lastRawOutputAtMs : null
12061
12422
  }));
12423
+ for (let attempt = 0; ; attempt++) {
12424
+ if (child.pid !== void 0) break;
12425
+ const spawnError = await waitForSpawnOutcome(child);
12426
+ if (!spawnError) break;
12427
+ const delays = SPAWN_RETRY_DELAYS_BY_CODE[spawnError.code ?? ""] ?? [];
12428
+ if (attempt >= delays.length) {
12429
+ throw formatCliSpawnError(command, spawnError);
12430
+ }
12431
+ const waitMs = delays[attempt];
12432
+ console.warn(
12433
+ `[${options.kind}] Spawn of "${command}" failed ${spawnError.code} \u2014 a CLI installer is likely mid-flight; retrying in ${waitMs}ms (attempt ${attempt + 1}/${delays.length})`
12434
+ );
12435
+ await delay(waitMs);
12436
+ const fresh = context.revalidateCommand?.()?.trim() || null;
12437
+ if (fresh && fresh !== command) {
12438
+ console.warn(`[${options.kind}] Re-resolved "${command}" to "${fresh}"`);
12439
+ command = fresh;
12440
+ }
12441
+ child = spawnCli(command, args, context);
12442
+ }
12443
+ state.process = child;
12444
+ state.lastRawOutputAtMs = Date.now();
12445
+ context.onProcessSpawned?.(child);
12062
12446
  const safeArgs = args.map(
12063
12447
  (a, i) => i > 0 && args[i - 1] === "--system-prompt" ? `"<system-prompt ${a.length} chars>"` : a
12064
12448
  );
@@ -18625,18 +19009,18 @@ ${output.stderr}`;
18625
19009
  }
18626
19010
 
18627
19011
  // ../shared/dist/node/local-workspace.js
18628
- var import_node_fs = require("fs");
18629
- var import_node_os = require("os");
18630
- var import_node_path = require("path");
18631
- function resolveAlanDefaultWorkspacePath(homeDirectory = (0, import_node_os.homedir)()) {
18632
- return (0, import_node_path.join)(homeDirectory, ".alan", "workspace");
18633
- }
18634
-
18635
- // ../shared/dist/node/provider-limits.js
18636
- var import_node_child_process = require("child_process");
18637
19012
  var import_node_fs2 = require("fs");
18638
19013
  var import_node_os2 = require("os");
18639
19014
  var import_node_path2 = require("path");
19015
+ function resolveAlanDefaultWorkspacePath(homeDirectory = (0, import_node_os2.homedir)()) {
19016
+ return (0, import_node_path2.join)(homeDirectory, ".alan", "workspace");
19017
+ }
19018
+
19019
+ // ../shared/dist/node/provider-limits.js
19020
+ var import_node_child_process2 = require("child_process");
19021
+ var import_node_fs3 = require("fs");
19022
+ var import_node_os3 = require("os");
19023
+ var import_node_path3 = require("path");
18640
19024
  var CACHE_TTL_MS = 6e4;
18641
19025
  var cachedLimits = null;
18642
19026
  function numericValue(value2) {
@@ -18647,42 +19031,42 @@ function numericValue(value2) {
18647
19031
  }
18648
19032
  return null;
18649
19033
  }
18650
- function splitPath(value2) {
18651
- return (value2 ?? "").split(import_node_path2.delimiter).filter(Boolean);
19034
+ function splitPath2(value2) {
19035
+ return (value2 ?? "").split(import_node_path3.delimiter).filter(Boolean);
18652
19036
  }
18653
19037
  function isExecutableFile(path) {
18654
19038
  try {
18655
- (0, import_node_fs2.accessSync)(path, import_node_fs2.constants.X_OK);
19039
+ (0, import_node_fs3.accessSync)(path, import_node_fs3.constants.X_OK);
18656
19040
  return true;
18657
19041
  } catch {
18658
19042
  return false;
18659
19043
  }
18660
19044
  }
18661
- function augmentCliPath(env) {
18662
- const home = env.HOME || (0, import_node_os2.homedir)();
19045
+ function augmentCliPath2(env) {
19046
+ const home = env.HOME || (0, import_node_os3.homedir)();
18663
19047
  const extraPaths = [
18664
- (0, import_node_path2.join)(home, ".local", "bin"),
18665
- (0, import_node_path2.join)(home, ".local", "node", "bin"),
18666
- (0, import_node_path2.join)(home, ".bun", "bin"),
18667
- (0, import_node_path2.join)(home, ".cargo", "bin"),
19048
+ (0, import_node_path3.join)(home, ".local", "bin"),
19049
+ (0, import_node_path3.join)(home, ".local", "node", "bin"),
19050
+ (0, import_node_path3.join)(home, ".bun", "bin"),
19051
+ (0, import_node_path3.join)(home, ".cargo", "bin"),
18668
19052
  "/opt/homebrew/bin",
18669
19053
  "/usr/local/bin"
18670
19054
  ];
18671
- const existing = new Set(splitPath(env.PATH));
19055
+ const existing = new Set(splitPath2(env.PATH));
18672
19056
  const missing = extraPaths.filter((path) => !existing.has(path));
18673
19057
  if (missing.length === 0)
18674
19058
  return env;
18675
19059
  return {
18676
19060
  ...env,
18677
- PATH: `${missing.join(import_node_path2.delimiter)}${import_node_path2.delimiter}${env.PATH ?? ""}`
19061
+ PATH: `${missing.join(import_node_path3.delimiter)}${import_node_path3.delimiter}${env.PATH ?? ""}`
18678
19062
  };
18679
19063
  }
18680
19064
  function resolveExecutable(command, env, overrideEnvVar) {
18681
19065
  const override = env[overrideEnvVar]?.trim();
18682
19066
  if (override && isExecutableFile(override))
18683
19067
  return override;
18684
- for (const dir of splitPath(env.PATH)) {
18685
- const candidate = (0, import_node_path2.join)(dir, command);
19068
+ for (const dir of splitPath2(env.PATH)) {
19069
+ const candidate = (0, import_node_path3.join)(dir, command);
18686
19070
  if (isExecutableFile(candidate))
18687
19071
  return candidate;
18688
19072
  }
@@ -18744,8 +19128,8 @@ async function withTimeout(promise, timeoutMs, fallback) {
18744
19128
  try {
18745
19129
  return await Promise.race([
18746
19130
  promise,
18747
- new Promise((resolve6) => {
18748
- timer = setTimeout(() => resolve6(fallback), timeoutMs);
19131
+ new Promise((resolve7) => {
19132
+ timer = setTimeout(() => resolve7(fallback), timeoutMs);
18749
19133
  })
18750
19134
  ]);
18751
19135
  } finally {
@@ -18754,12 +19138,12 @@ async function withTimeout(promise, timeoutMs, fallback) {
18754
19138
  }
18755
19139
  }
18756
19140
  async function collectCodexLimits(env) {
18757
- const runtimeEnv = augmentCliPath(env);
19141
+ const runtimeEnv = augmentCliPath2(env);
18758
19142
  const codexCommand = resolveExecutable("codex", runtimeEnv, "ALAN_CODEX_PATH");
18759
19143
  if (!codexCommand)
18760
19144
  return [];
18761
- return withTimeout(new Promise((resolve6) => {
18762
- const child = (0, import_node_child_process.spawn)(codexCommand, ["-s", "read-only", "-a", "untrusted", "app-server"], {
19145
+ return withTimeout(new Promise((resolve7) => {
19146
+ const child = (0, import_node_child_process2.spawn)(codexCommand, ["-s", "read-only", "-a", "untrusted", "app-server"], {
18763
19147
  env: runtimeEnv,
18764
19148
  stdio: ["pipe", "pipe", "ignore"]
18765
19149
  });
@@ -18770,7 +19154,7 @@ async function collectCodexLimits(env) {
18770
19154
  pending.clear();
18771
19155
  if (!child.killed)
18772
19156
  child.kill();
18773
- resolve6(windows);
19157
+ resolve7(windows);
18774
19158
  };
18775
19159
  const request = (method, params = {}) => {
18776
19160
  const id = nextId++;
@@ -18826,10 +19210,10 @@ async function collectCodexLimits(env) {
18826
19210
  }), 4e3, []);
18827
19211
  }
18828
19212
  function readClaudeCredentialsPayload() {
18829
- const path = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".claude", ".credentials.json");
18830
- if ((0, import_node_fs2.existsSync)(path)) {
19213
+ const path = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".claude", ".credentials.json");
19214
+ if ((0, import_node_fs3.existsSync)(path)) {
18831
19215
  try {
18832
- return JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
19216
+ return JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
18833
19217
  } catch {
18834
19218
  return null;
18835
19219
  }
@@ -18837,7 +19221,7 @@ function readClaudeCredentialsPayload() {
18837
19221
  if (process.platform !== "darwin")
18838
19222
  return null;
18839
19223
  try {
18840
- const raw = (0, import_node_child_process.execFileSync)("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
19224
+ const raw = (0, import_node_child_process2.execFileSync)("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
18841
19225
  if (!raw)
18842
19226
  return null;
18843
19227
  return JSON.parse(raw);
@@ -20536,11 +20920,11 @@ var SocketWithoutUpgrade = class _SocketWithoutUpgrade extends Emitter {
20536
20920
  */
20537
20921
  _resetPingTimeout() {
20538
20922
  this.clearTimeoutFn(this._pingTimeoutTimer);
20539
- const delay = this._pingInterval + this._pingTimeout;
20540
- this._pingTimeoutTime = Date.now() + delay;
20923
+ const delay2 = this._pingInterval + this._pingTimeout;
20924
+ this._pingTimeoutTime = Date.now() + delay2;
20541
20925
  this._pingTimeoutTimer = this.setTimeoutFn(() => {
20542
20926
  this._onClose("ping timeout");
20543
- }, delay);
20927
+ }, delay2);
20544
20928
  if (this.opts.autoUnref) {
20545
20929
  this._pingTimeoutTimer.unref();
20546
20930
  }
@@ -21603,9 +21987,9 @@ var Socket2 = class extends Emitter {
21603
21987
  * @return a Promise that will be fulfilled when the server acknowledges the event
21604
21988
  */
21605
21989
  emitWithAck(ev, ...args) {
21606
- return new Promise((resolve6, reject) => {
21990
+ return new Promise((resolve7, reject) => {
21607
21991
  const fn = (arg1, arg2) => {
21608
- return arg1 ? reject(arg1) : resolve6(arg2);
21992
+ return arg1 ? reject(arg1) : resolve7(arg2);
21609
21993
  };
21610
21994
  fn.withError = true;
21611
21995
  args.push(fn);
@@ -22521,8 +22905,8 @@ var Manager = class extends Emitter {
22521
22905
  this.emitReserved("reconnect_failed");
22522
22906
  this._reconnecting = false;
22523
22907
  } else {
22524
- const delay = this.backoff.duration();
22525
- debug10("will wait %dms before reconnect attempt", delay);
22908
+ const delay2 = this.backoff.duration();
22909
+ debug10("will wait %dms before reconnect attempt", delay2);
22526
22910
  this._reconnecting = true;
22527
22911
  const timer = this.setTimeoutFn(() => {
22528
22912
  if (self.skipReconnect)
@@ -22542,7 +22926,7 @@ var Manager = class extends Emitter {
22542
22926
  self.onreconnect();
22543
22927
  }
22544
22928
  });
22545
- }, delay);
22929
+ }, delay2);
22546
22930
  if (this.opts.autoUnref) {
22547
22931
  timer.unref();
22548
22932
  }
@@ -22591,365 +22975,1139 @@ function lookup(uri, opts) {
22591
22975
  }
22592
22976
  io = cache[id];
22593
22977
  }
22594
- if (parsed.query && !opts.query) {
22595
- opts.query = parsed.queryKey;
22978
+ if (parsed.query && !opts.query) {
22979
+ opts.query = parsed.queryKey;
22980
+ }
22981
+ return io.socket(parsed.path, opts);
22982
+ }
22983
+ Object.assign(lookup, {
22984
+ Manager,
22985
+ Socket: Socket2,
22986
+ io: lookup,
22987
+ connect: lookup
22988
+ });
22989
+
22990
+ // src/binding-switch-git.ts
22991
+ var import_node_child_process3 = require("child_process");
22992
+ var import_node_crypto = require("crypto");
22993
+ var import_node_fs4 = require("fs");
22994
+ var import_node_os4 = require("os");
22995
+ var import_node_path4 = require("path");
22996
+ var import_node_url = require("url");
22997
+ var BINDING_SWITCH_GIT_METHODS = [
22998
+ "git.status",
22999
+ "git.checkpoint",
23000
+ "git.push",
23001
+ "git.fetch",
23002
+ "git.prepare_checkout",
23003
+ "git.prepare_worktree",
23004
+ "git.verify_head"
23005
+ ];
23006
+ function isBindingSwitchGitMethod(method) {
23007
+ return BINDING_SWITCH_GIT_METHODS.includes(method);
23008
+ }
23009
+ var BindingSwitchGitError = class extends Error {
23010
+ constructor(code, message, details) {
23011
+ super(message);
23012
+ this.code = code;
23013
+ this.details = details;
23014
+ this.name = "BindingSwitchGitError";
23015
+ }
23016
+ };
23017
+ var LOCAL_GIT_TIMEOUT_MS = 1e4;
23018
+ var NETWORK_GIT_TIMEOUT_MS = 3e4;
23019
+ var MAX_GIT_OUTPUT_BYTES = 10 * 1024 * 1024;
23020
+ function runGitResult(cwd, args, options = {}) {
23021
+ const result = (0, import_node_child_process3.spawnSync)("git", args, {
23022
+ cwd,
23023
+ env: { ...getDaemonCliEnvironment(), ...options.env },
23024
+ encoding: "utf8",
23025
+ input: options.input,
23026
+ maxBuffer: MAX_GIT_OUTPUT_BYTES,
23027
+ timeout: LOCAL_GIT_TIMEOUT_MS
23028
+ });
23029
+ if (result.error) {
23030
+ return { status: 1, stdout: "", stderr: result.error.message };
23031
+ }
23032
+ return {
23033
+ status: result.status ?? 1,
23034
+ stdout: result.stdout ?? "",
23035
+ stderr: result.stderr ?? ""
23036
+ };
23037
+ }
23038
+ function networkGitTimeoutMs() {
23039
+ if (process.env.NODE_ENV !== "test") return NETWORK_GIT_TIMEOUT_MS;
23040
+ const override = Number.parseInt(process.env.ALAN_TEST_GIT_NETWORK_TIMEOUT_MS ?? "", 10);
23041
+ return Number.isFinite(override) && override >= 10 ? Math.min(override, NETWORK_GIT_TIMEOUT_MS) : NETWORK_GIT_TIMEOUT_MS;
23042
+ }
23043
+ async function waitForCheckpointTestBarrier() {
23044
+ const barrierDir = process.env.NODE_ENV === "test" ? process.env.ALAN_TEST_GIT_CHECKPOINT_BARRIER_DIR : void 0;
23045
+ if (!barrierDir) return;
23046
+ (0, import_node_fs4.mkdirSync)(barrierDir, { recursive: true });
23047
+ const readyPath = (0, import_node_path4.join)(barrierDir, "ready");
23048
+ const releasePath = (0, import_node_path4.join)(barrierDir, "release");
23049
+ (0, import_node_fs4.writeFileSync)(readyPath, "ready\n");
23050
+ const deadline = Date.now() + 5e3;
23051
+ while (!(0, import_node_fs4.existsSync)(releasePath)) {
23052
+ if (Date.now() >= deadline) {
23053
+ throw new BindingSwitchGitError(
23054
+ "checkpoint_prepare_timeout",
23055
+ "Checkpoint preparation test barrier timed out"
23056
+ );
23057
+ }
23058
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
23059
+ }
23060
+ }
23061
+ async function waitForCheckpointFinalizeTestBarrier() {
23062
+ const barrierDir = process.env.NODE_ENV === "test" ? process.env.ALAN_TEST_GIT_CHECKPOINT_FINALIZE_BARRIER_DIR : void 0;
23063
+ if (!barrierDir) return;
23064
+ (0, import_node_fs4.mkdirSync)(barrierDir, { recursive: true });
23065
+ const readyPath = (0, import_node_path4.join)(barrierDir, "ready");
23066
+ const releasePath = (0, import_node_path4.join)(barrierDir, "release");
23067
+ (0, import_node_fs4.writeFileSync)(readyPath, "ready\n");
23068
+ const deadline = Date.now() + 5e3;
23069
+ while (!(0, import_node_fs4.existsSync)(releasePath)) {
23070
+ if (Date.now() >= deadline) {
23071
+ throw new BindingSwitchGitError(
23072
+ "checkpoint_finalize_timeout",
23073
+ "Checkpoint finalization test barrier timed out"
23074
+ );
23075
+ }
23076
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
23077
+ }
23078
+ }
23079
+ async function runNetworkGit(cwd, args, code) {
23080
+ return new Promise((resolvePromise, rejectPromise) => {
23081
+ const child = (0, import_node_child_process3.spawn)("git", args, {
23082
+ cwd,
23083
+ env: {
23084
+ ...getDaemonCliEnvironment(),
23085
+ GCM_INTERACTIVE: "Never",
23086
+ GIT_TERMINAL_PROMPT: "0",
23087
+ ...process.env.NODE_ENV === "test" && process.env.ALAN_TEST_GIT_SSH_COMMAND ? { GIT_SSH_COMMAND: process.env.ALAN_TEST_GIT_SSH_COMMAND } : {}
23088
+ },
23089
+ detached: process.platform !== "win32",
23090
+ stdio: ["ignore", "pipe", "pipe"]
23091
+ });
23092
+ let stdout = "";
23093
+ let stderr = "";
23094
+ let outputExceeded = false;
23095
+ let timedOut = false;
23096
+ let settled = false;
23097
+ const killTree = () => {
23098
+ if (child.pid && process.platform !== "win32") {
23099
+ try {
23100
+ process.kill(-child.pid, "SIGKILL");
23101
+ return;
23102
+ } catch {
23103
+ }
23104
+ }
23105
+ child.kill("SIGKILL");
23106
+ };
23107
+ const timer = setTimeout(() => {
23108
+ timedOut = true;
23109
+ killTree();
23110
+ }, networkGitTimeoutMs());
23111
+ timer.unref?.();
23112
+ const append = (current, chunk) => {
23113
+ if (Buffer.byteLength(current) + chunk.byteLength > MAX_GIT_OUTPUT_BYTES) {
23114
+ outputExceeded = true;
23115
+ killTree();
23116
+ return current;
23117
+ }
23118
+ return current + chunk.toString("utf8");
23119
+ };
23120
+ child.stdout.on("data", (chunk) => {
23121
+ stdout = append(stdout, chunk);
23122
+ });
23123
+ child.stderr.on("data", (chunk) => {
23124
+ stderr = append(stderr, chunk);
23125
+ });
23126
+ child.once("error", (error) => {
23127
+ if (settled) return;
23128
+ settled = true;
23129
+ clearTimeout(timer);
23130
+ rejectPromise(new BindingSwitchGitError(code, error.message));
23131
+ });
23132
+ child.once("close", (exitCode) => {
23133
+ if (settled) return;
23134
+ settled = true;
23135
+ clearTimeout(timer);
23136
+ if (timedOut) {
23137
+ rejectPromise(
23138
+ new BindingSwitchGitError(
23139
+ "git_network_timeout",
23140
+ "The Git network operation timed out and was cancelled"
23141
+ )
23142
+ );
23143
+ return;
23144
+ }
23145
+ if (outputExceeded) {
23146
+ rejectPromise(
23147
+ new BindingSwitchGitError(
23148
+ "git_output_limit_exceeded",
23149
+ "The Git network operation produced too much output and was cancelled"
23150
+ )
23151
+ );
23152
+ return;
23153
+ }
23154
+ if (exitCode !== 0) {
23155
+ rejectPromise(
23156
+ new BindingSwitchGitError(code, (stderr || stdout || "Git network command failed").trim())
23157
+ );
23158
+ return;
23159
+ }
23160
+ resolvePromise(stdout.trim());
23161
+ });
23162
+ });
23163
+ }
23164
+ function runGit(cwd, args, code = "git_operation_failed") {
23165
+ return runGitWithOptions(cwd, args, {}, code);
23166
+ }
23167
+ function runGitWithOptions(cwd, args, options, code = "git_operation_failed") {
23168
+ const result = runGitResult(cwd, args, options);
23169
+ if (result.status !== 0) {
23170
+ throw new BindingSwitchGitError(
23171
+ code,
23172
+ (result.stderr || result.stdout || "Git command failed").trim()
23173
+ );
23174
+ }
23175
+ return result.stdout.trim();
23176
+ }
23177
+ function sourceChangedDuringCheckpoint(message) {
23178
+ return new BindingSwitchGitError("source_changed_since_approval", message);
23179
+ }
23180
+ function acquireCheckpointHeadLock(locks) {
23181
+ try {
23182
+ (0, import_node_fs4.writeFileSync)(locks.headLockPath, "", { flag: "wx" });
23183
+ locks.headLockHeld = true;
23184
+ } catch {
23185
+ throw sourceChangedDuringCheckpoint(
23186
+ "Source repository is being changed by another Git operation"
23187
+ );
23188
+ }
23189
+ }
23190
+ function releaseCheckpointHeadLock(locks) {
23191
+ if (!locks.headLockHeld) return;
23192
+ (0, import_node_fs4.rmSync)(locks.headLockPath, { force: true });
23193
+ locks.headLockHeld = false;
23194
+ }
23195
+ function acquireCheckpointRepositoryLocks(repoRoot) {
23196
+ const gitDir = runGit(
23197
+ repoRoot,
23198
+ ["rev-parse", "--absolute-git-dir"],
23199
+ "checkpoint_lock_unavailable"
23200
+ );
23201
+ const indexPath = runGit(
23202
+ repoRoot,
23203
+ ["rev-parse", "--path-format=absolute", "--git-path", "index"],
23204
+ "checkpoint_lock_unavailable"
23205
+ );
23206
+ const locks = {
23207
+ headLockPath: (0, import_node_path4.join)(gitDir, "HEAD.lock"),
23208
+ headLockHeld: false,
23209
+ indexLockPath: `${indexPath}.lock`,
23210
+ indexLockHeld: false,
23211
+ indexPath
23212
+ };
23213
+ try {
23214
+ (0, import_node_fs4.writeFileSync)(locks.indexLockPath, "", { flag: "wx" });
23215
+ locks.indexLockHeld = true;
23216
+ acquireCheckpointHeadLock(locks);
23217
+ return locks;
23218
+ } catch {
23219
+ if (locks.indexLockHeld) {
23220
+ (0, import_node_fs4.rmSync)(locks.indexLockPath, { force: true });
23221
+ locks.indexLockHeld = false;
23222
+ }
23223
+ throw sourceChangedDuringCheckpoint(
23224
+ "Source repository is being changed by another Git operation"
23225
+ );
23226
+ }
23227
+ }
23228
+ function releaseCheckpointRepositoryLocks(locks) {
23229
+ releaseCheckpointHeadLock(locks);
23230
+ if (locks.indexLockHeld) {
23231
+ (0, import_node_fs4.rmSync)(locks.indexLockPath, { force: true });
23232
+ locks.indexLockHeld = false;
23233
+ }
23234
+ }
23235
+ function assertCheckpointHead(repoRoot, expectedRef, expectedHeadSha) {
23236
+ const currentRef = runGit(
23237
+ repoRoot,
23238
+ ["symbolic-ref", "-q", "HEAD"],
23239
+ "source_changed_since_approval"
23240
+ );
23241
+ const currentHead = runGit(repoRoot, ["rev-parse", "HEAD"], "source_changed_since_approval");
23242
+ if (currentRef !== expectedRef || currentHead !== expectedHeadSha) {
23243
+ throw sourceChangedDuringCheckpoint(
23244
+ "Source HEAD or branch changed during checkpoint finalization"
23245
+ );
23246
+ }
23247
+ }
23248
+ function createCheckpointIndexBaseline(repoRoot, checkpointSha) {
23249
+ const tempDir = (0, import_node_fs4.mkdtempSync)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "alan-checkpoint-index-"));
23250
+ const indexPath = (0, import_node_path4.join)(tempDir, "index");
23251
+ try {
23252
+ runGitWithOptions(
23253
+ repoRoot,
23254
+ ["read-tree", checkpointSha],
23255
+ { env: { GIT_INDEX_FILE: indexPath } },
23256
+ "checkpoint_index_sync_failed"
23257
+ );
23258
+ return { indexPath, tempDir };
23259
+ } catch (error) {
23260
+ (0, import_node_fs4.rmSync)(tempDir, { recursive: true, force: true });
23261
+ throw error;
23262
+ }
23263
+ }
23264
+ function installCheckpointIndexBaseline(locks, baselineIndexPath) {
23265
+ try {
23266
+ (0, import_node_fs4.copyFileSync)(baselineIndexPath, locks.indexLockPath);
23267
+ (0, import_node_fs4.renameSync)(locks.indexLockPath, locks.indexPath);
23268
+ locks.indexLockHeld = false;
23269
+ } catch (error) {
23270
+ throw new BindingSwitchGitError(
23271
+ "checkpoint_index_sync_failed",
23272
+ error instanceof Error ? error.message : "The checkpoint index could not be installed"
23273
+ );
23274
+ }
23275
+ }
23276
+ function requiredString(params, key) {
23277
+ const value2 = params[key];
23278
+ if (typeof value2 !== "string" || value2.trim() === "") {
23279
+ throw new BindingSwitchGitError("invalid_git_request", `${key} is required`);
23280
+ }
23281
+ if (/[\0\r\n]/.test(value2)) {
23282
+ throw new BindingSwitchGitError("invalid_git_request", `${key} contains unsafe characters`);
23283
+ }
23284
+ return value2;
23285
+ }
23286
+ function requiredCommitMessage(params) {
23287
+ const value2 = params.commitMessage;
23288
+ if (typeof value2 !== "string" || value2.trim() === "") {
23289
+ throw new BindingSwitchGitError("invalid_git_request", "commitMessage is required");
23290
+ }
23291
+ if (value2.includes("\0")) {
23292
+ throw new BindingSwitchGitError(
23293
+ "invalid_git_request",
23294
+ "commitMessage contains unsafe characters"
23295
+ );
23296
+ }
23297
+ return value2;
23298
+ }
23299
+ function optionalString(params, key) {
23300
+ const value2 = params[key];
23301
+ if (value2 === void 0 || value2 === null) return void 0;
23302
+ if (typeof value2 !== "string" || value2.trim() === "" || /[\0\r\n]/.test(value2)) {
23303
+ throw new BindingSwitchGitError("invalid_git_request", `${key} is invalid`);
23304
+ }
23305
+ return value2;
23306
+ }
23307
+ function commonRequest(params) {
23308
+ return {
23309
+ switchId: requiredString(params, "switchId"),
23310
+ repoKey: requiredString(params, "repoKey"),
23311
+ repoPath: (0, import_node_path4.resolve)(requiredString(params, "repoPath"))
23312
+ };
23313
+ }
23314
+ function assertCommitSha(sha, key) {
23315
+ if (!/^[0-9a-f]{40,64}$/i.test(sha)) {
23316
+ throw new BindingSwitchGitError("invalid_git_request", `${key} is not a commit SHA`);
22596
23317
  }
22597
- return io.socket(parsed.path, opts);
22598
23318
  }
22599
- Object.assign(lookup, {
22600
- Manager,
22601
- Socket: Socket2,
22602
- io: lookup,
22603
- connect: lookup
22604
- });
22605
-
22606
- // src/cli-ensure-install.ts
22607
- var import_node_child_process3 = require("child_process");
22608
- var import_node_os4 = require("os");
22609
-
22610
- // src/cli-executable.ts
22611
- var import_node_child_process2 = require("child_process");
22612
- var import_node_fs3 = require("fs");
22613
- var import_node_os3 = require("os");
22614
- var import_node_path3 = require("path");
22615
- var PROVIDER_CLI_COMMANDS = {
22616
- claude_cli: { provider: "claude_cli", command: "claude", envVars: ["ALAN_CLAUDE_PATH"] },
22617
- codex_app_server: {
22618
- provider: "codex_app_server",
22619
- command: "codex",
22620
- envVars: ["ALAN_CODEX_PATH"]
22621
- },
22622
- copilot_cli: { provider: "copilot_cli", command: "copilot", envVars: ["ALAN_COPILOT_PATH"] },
22623
- cursor_agent_cli: {
22624
- provider: "cursor_agent_cli",
22625
- command: "cursor-agent",
22626
- envVars: ["ALAN_CURSOR_AGENT_PATH"]
22627
- },
22628
- antigravity_cli: {
22629
- provider: "antigravity_cli",
22630
- command: "agy",
22631
- envVars: ["ALAN_ANTIGRAVITY_PATH"]
22632
- },
22633
- kimi_cli: { provider: "kimi_cli", command: "kimi-cli", envVars: ["ALAN_KIMI_PATH"] },
22634
- grok_cli: { provider: "grok_cli", command: "grok", envVars: ["ALAN_GROK_PATH"] },
22635
- opencode_cli: { provider: "opencode_cli", command: "opencode", envVars: ["ALAN_OPENCODE_PATH"] },
22636
- droid_cli: { provider: "droid_cli", command: "droid", envVars: ["ALAN_DROID_PATH"] },
22637
- supatest_cli: { provider: "supatest_cli", command: "supatest", envVars: ["ALAN_SUPATEST_PATH"] }
22638
- };
22639
- var DISCOVERABLE_PROVIDER_KINDS = Object.keys(PROVIDER_CLI_COMMANDS);
22640
- var PROVIDER_ALIASES = {
22641
- agy: "antigravity_cli",
22642
- antigravity: "antigravity_cli",
22643
- antigravity_cli: "antigravity_cli",
22644
- codex: "codex_app_server",
22645
- gemini: "antigravity_cli",
22646
- gemini_cli: "antigravity_cli"
22647
- };
22648
- var BACKEND_CLI_COMMANDS = {
22649
- claude_cli: "claude",
22650
- codex: "codex",
22651
- codex_app_server: "codex",
22652
- copilot_cli: "copilot",
22653
- cursor_agent_cli: "cursor-agent",
22654
- agy: "agy",
22655
- antigravity: "agy",
22656
- antigravity_cli: "agy",
22657
- gemini_cli: "agy",
22658
- gemini: "agy",
22659
- kimi_cli: "kimi-cli",
22660
- grok_cli: "grok",
22661
- opencode_cli: "opencode",
22662
- droid_cli: "droid",
22663
- supatest_cli: "supatest"
22664
- };
22665
- var CLI_COMMAND_ALIASES = {
22666
- "kimi-cli": ["kimi"],
22667
- grok: ["grok-build"]
22668
- };
22669
- var resolvedCliCache = /* @__PURE__ */ new Map();
22670
- var NEGATIVE_RESOLUTION_TTL_MS = 15e3;
22671
- var negativeResolutionExpiry = /* @__PURE__ */ new Map();
22672
- var cachedLoginShellEnv = null;
22673
- function parseEnvOutput(output) {
22674
- const loginEnv = {};
22675
- for (const line of output.split("\n")) {
22676
- if (!line.trim()) continue;
22677
- const eqIdx = line.indexOf("=");
22678
- if (eqIdx <= 0) continue;
22679
- const key = line.slice(0, eqIdx);
22680
- if (/\s/.test(key)) continue;
22681
- loginEnv[key] = line.slice(eqIdx + 1);
23319
+ function assertValidBranch(branch) {
23320
+ if (branch.startsWith("-")) {
23321
+ throw new BindingSwitchGitError("invalid_git_request", "branch is not a safe Git ref");
23322
+ }
23323
+ const result = (0, import_node_child_process3.spawnSync)("git", ["check-ref-format", "--branch", branch], {
23324
+ env: getDaemonCliEnvironment(),
23325
+ encoding: "utf8"
23326
+ });
23327
+ if (result.status !== 0) {
23328
+ throw new BindingSwitchGitError("invalid_git_request", "branch is not a valid Git ref");
22682
23329
  }
22683
- if (!loginEnv.HOME) loginEnv.HOME = process.env.HOME ?? (0, import_node_os3.homedir)();
22684
- if (!loginEnv.SHELL) loginEnv.SHELL = process.env.SHELL ?? "/bin/zsh";
22685
- return loginEnv;
22686
23330
  }
22687
- function loadLoginShellEnvironment() {
22688
- if (cachedLoginShellEnv) return cachedLoginShellEnv;
22689
- if ((0, import_node_os3.platform)() === "win32") {
22690
- cachedLoginShellEnv = process.env;
22691
- return cachedLoginShellEnv;
23331
+ function validatedRemote(params) {
23332
+ const remote = optionalString(params, "remote") ?? "origin";
23333
+ if (remote.startsWith("-") || remote.includes("/") || remote.includes("\\")) {
23334
+ throw new BindingSwitchGitError("invalid_git_request", "remote is not a safe remote name");
23335
+ }
23336
+ return remote;
23337
+ }
23338
+ function canonicalRemoteUrl(repoRoot, value2) {
23339
+ const trimmed = value2.trim();
23340
+ if (!trimmed || /[\0\r\n]/.test(trimmed)) {
23341
+ throw new BindingSwitchGitError("invalid_git_request", "Remote URL is invalid");
23342
+ }
23343
+ const scpStyle = trimmed.match(/^([^/@:\s]+)@([^/:\s]+):(.+)$/);
23344
+ const urlValue = scpStyle ? `ssh://${scpStyle[1]}@${scpStyle[2]}/${scpStyle[3]}` : trimmed;
23345
+ if ((0, import_node_path4.isAbsolute)(urlValue) || urlValue.startsWith("./") || urlValue.startsWith("../")) {
23346
+ const absolute = (0, import_node_path4.isAbsolute)(urlValue) ? urlValue : (0, import_node_path4.resolve)(repoRoot, urlValue);
23347
+ return `file://${canonicalPathWithMissingTail(absolute).replace(/\/+$/, "").replace(/\.git$/, "")}`;
22692
23348
  }
23349
+ let parsed;
22693
23350
  try {
22694
- const shell = (process.env.SHELL || "/bin/bash").replace(/'/g, "'\\''");
22695
- const envOutput = (0, import_node_child_process2.execSync)(`'${shell}' -ilc 'env'`, {
22696
- encoding: "utf8",
22697
- stdio: ["pipe", "pipe", "pipe"],
22698
- timeout: 5e3
22699
- });
22700
- cachedLoginShellEnv = parseEnvOutput(envOutput);
23351
+ parsed = new URL(urlValue);
22701
23352
  } catch {
22702
- cachedLoginShellEnv = process.env;
23353
+ throw new BindingSwitchGitError("invalid_git_request", "Remote URL is invalid");
22703
23354
  }
22704
- return cachedLoginShellEnv;
23355
+ if (parsed.protocol === "file:") {
23356
+ return `file://${canonicalPathWithMissingTail((0, import_node_url.fileURLToPath)(parsed)).replace(/\/+$/, "").replace(/\.git$/, "")}`;
23357
+ }
23358
+ if (parsed.search || parsed.hash) {
23359
+ throw new BindingSwitchGitError("invalid_git_request", "Remote URL is invalid");
23360
+ }
23361
+ if (!["https:", "http:", "ssh:", "git:"].includes(parsed.protocol)) {
23362
+ throw new BindingSwitchGitError(
23363
+ "invalid_git_request",
23364
+ "Remote URL uses an unsupported protocol"
23365
+ );
23366
+ }
23367
+ if (parsed.password || (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username) {
23368
+ throw new BindingSwitchGitError(
23369
+ "remote_url_credentials_rejected",
23370
+ "Credential-bearing remote URLs are not allowed for environment migration"
23371
+ );
23372
+ }
23373
+ const defaultPort = parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "http:" && parsed.port === "80" || parsed.protocol === "ssh:" && parsed.port === "22";
23374
+ const transport = parsed.protocol === "https:" || parsed.protocol === "ssh:" ? "secure:" : parsed.protocol;
23375
+ const authority = `${parsed.hostname.toLowerCase()}${parsed.port && !defaultPort ? `:${parsed.port}` : ""}`;
23376
+ const path = parsed.pathname.replace(/\/+/g, "/").replace(/\/+$/, "").replace(/\.git$/, "");
23377
+ return `${transport}//${authority}${path}`;
23378
+ }
23379
+ function verifiedRemote(repoRoot, params, includePushUrls) {
23380
+ const remote = validatedRemote(params);
23381
+ const expectedRemoteUrl = canonicalRemoteUrl(
23382
+ repoRoot,
23383
+ requiredString(params, "expectedRemoteUrl")
23384
+ );
23385
+ const fetchUrls = runGit(
23386
+ repoRoot,
23387
+ ["remote", "get-url", "--all", remote],
23388
+ "git_remote_unavailable"
23389
+ ).split("\n").filter(Boolean);
23390
+ const pushUrls = includePushUrls ? runGit(repoRoot, ["remote", "get-url", "--push", "--all", remote], "git_remote_unavailable").split("\n").filter(Boolean) : [];
23391
+ const urls = [...fetchUrls, ...pushUrls];
23392
+ if (urls.length === 0 || urls.some((url2) => canonicalRemoteUrl(repoRoot, url2) !== expectedRemoteUrl)) {
23393
+ throw new BindingSwitchGitError(
23394
+ "remote_url_mismatch",
23395
+ "The configured Git remote does not match the repository selected for migration"
23396
+ );
23397
+ }
23398
+ return { remote, expectedRemoteUrl };
22705
23399
  }
22706
- function getDaemonCliEnvironment() {
22707
- const base = (0, import_node_os3.platform)() === "win32" ? process.env : loadLoginShellEnvironment();
22708
- return augmentCliPath2(base);
23400
+ function isPathContained(childPath, parentPath) {
23401
+ const rel = (0, import_node_path4.relative)((0, import_node_path4.resolve)(parentPath), (0, import_node_path4.resolve)(childPath));
23402
+ return rel === "" || !(0, import_node_path4.isAbsolute)(rel) && rel !== ".." && !rel.startsWith(`..${import_node_path4.sep}`);
23403
+ }
23404
+ function canonicalPathWithMissingTail(path) {
23405
+ const missingSegments = [];
23406
+ let cursor = (0, import_node_path4.resolve)(path);
23407
+ while (!(0, import_node_fs4.existsSync)(cursor)) {
23408
+ const parent = (0, import_node_path4.dirname)(cursor);
23409
+ if (parent === cursor) {
23410
+ throw new BindingSwitchGitError(
23411
+ "path_canonicalization_failed",
23412
+ "The selected path has no existing canonical ancestor"
23413
+ );
23414
+ }
23415
+ missingSegments.unshift((0, import_node_path4.basename)(cursor));
23416
+ cursor = parent;
23417
+ }
23418
+ return (0, import_node_path4.join)(import_node_fs4.realpathSync.native(cursor), ...missingSegments);
22709
23419
  }
22710
- function cacheResolvedCli(command, resolvedPath) {
22711
- resolvedCliCache.set(command, resolvedPath);
22712
- if (resolvedPath === null) {
22713
- negativeResolutionExpiry.set(command, Date.now() + NEGATIVE_RESOLUTION_TTL_MS);
22714
- } else {
22715
- negativeResolutionExpiry.delete(command);
23420
+ function assertCanonicalPathContained(childPath, parentPath, code, message) {
23421
+ if (!isPathContained(
23422
+ canonicalPathWithMissingTail(childPath),
23423
+ canonicalPathWithMissingTail(parentPath)
23424
+ )) {
23425
+ throw new BindingSwitchGitError(code, message);
22716
23426
  }
22717
23427
  }
22718
- function getCachedResolvedCli(command) {
22719
- const cached = resolvedCliCache.get(command);
22720
- if (cached === null) {
22721
- const expiresAt = negativeResolutionExpiry.get(command);
22722
- if (expiresAt === void 0 || Date.now() >= expiresAt) {
22723
- resolvedCliCache.delete(command);
22724
- negativeResolutionExpiry.delete(command);
22725
- return void 0;
22726
- }
23428
+ function alanWorktreeRoot(projectRoot) {
23429
+ return (0, import_node_path4.join)((0, import_node_path4.dirname)(projectRoot), `${(0, import_node_path4.basename)(projectRoot)}-alan-worktrees`);
23430
+ }
23431
+ function resolveRepositoryRoot(requestedPath) {
23432
+ if (!(0, import_node_fs4.existsSync)(requestedPath)) {
23433
+ throw new BindingSwitchGitError(
23434
+ "repository_not_found",
23435
+ `The selected repository path does not exist: ${requestedPath}`
23436
+ );
22727
23437
  }
22728
- return cached;
23438
+ const root = runGitResult(requestedPath, ["rev-parse", "--show-toplevel"]);
23439
+ if (root.status !== 0 || !root.stdout.trim()) {
23440
+ throw new BindingSwitchGitError(
23441
+ "repository_not_found",
23442
+ "The selected folder is not inside a Git repository"
23443
+ );
23444
+ }
23445
+ return import_node_fs4.realpathSync.native((0, import_node_path4.resolve)(root.stdout.trim()));
22729
23446
  }
22730
- function resolveViaWhere(command, env) {
22731
- if ((0, import_node_os3.platform)() !== "win32") return null;
22732
- const whereExe = (0, import_node_path3.join)(
22733
- env.SystemRoot ?? process.env.SystemRoot ?? "C:\\Windows",
22734
- "System32",
22735
- "where.exe"
22736
- );
22737
- if (!(0, import_node_fs3.existsSync)(whereExe)) return null;
22738
- const result = (0, import_node_child_process2.spawnSync)(whereExe, [command], {
22739
- encoding: "utf8",
22740
- env,
22741
- windowsHide: true,
22742
- timeout: 3e3
23447
+ function listWorktrees(repoRoot) {
23448
+ return runGit(repoRoot, ["worktree", "list", "--porcelain"]).split("\n\n").flatMap((entry) => {
23449
+ const lines = entry.split("\n");
23450
+ const path = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
23451
+ if (!path) return [];
23452
+ return [
23453
+ {
23454
+ path: canonicalPathWithMissingTail(path),
23455
+ branch: lines.find((line) => line.startsWith("branch "))?.slice("branch ".length).replace("refs/heads/", "") ?? null
23456
+ }
23457
+ ];
22743
23458
  });
22744
- if (result.error || result.status !== 0) return null;
22745
- for (const line of result.stdout.split(/\r?\n/)) {
22746
- const trimmed = line.trim();
22747
- if (!trimmed) continue;
22748
- if (isRunnableFile(trimmed)) return trimmed;
23459
+ }
23460
+ function resolveAuthorizedRepository(params, requireTrustedContext = false) {
23461
+ const { repoPath } = commonRequest(params);
23462
+ const repoRoot = resolveRepositoryRoot(repoPath);
23463
+ const expectedRepoRoot = requireTrustedContext ? requiredString(params, "expectedRepoRoot") : optionalString(params, "expectedRepoRoot");
23464
+ if (expectedRepoRoot && resolveRepositoryRoot((0, import_node_path4.resolve)(expectedRepoRoot)) !== repoRoot) {
23465
+ throw new BindingSwitchGitError(
23466
+ "repository_path_changed",
23467
+ "The selected path now resolves to a different Git repository"
23468
+ );
22749
23469
  }
22750
- return null;
23470
+ const projectPath = requireTrustedContext ? requiredString(params, "projectPath") : optionalString(params, "projectPath");
23471
+ if (!projectPath) return repoRoot;
23472
+ const projectRoot = resolveRepositoryRoot((0, import_node_path4.resolve)(projectPath));
23473
+ const registrations = listWorktrees(projectRoot);
23474
+ const registered = registrations.find(({ path }) => path === repoRoot);
23475
+ if (!registered) {
23476
+ throw new BindingSwitchGitError(
23477
+ "repository_path_not_authorized",
23478
+ "The selected repository is not the Project checkout or one of its registered worktrees"
23479
+ );
23480
+ }
23481
+ const mainRoot = registrations[0]?.path;
23482
+ if (mainRoot && repoRoot !== mainRoot && !isPathContained(repoRoot, canonicalPathWithMissingTail(alanWorktreeRoot(mainRoot)))) {
23483
+ throw new BindingSwitchGitError(
23484
+ "repository_path_not_authorized",
23485
+ "The selected worktree is outside the Alan-managed worktree directory"
23486
+ );
23487
+ }
23488
+ return repoRoot;
22751
23489
  }
22752
- function runCliProbeAsync(executable, env, args = ["--version"], timeoutMs = 3e3, spawnOptions) {
22753
- return new Promise((resolve6) => {
22754
- const isWin = (0, import_node_os3.platform)() === "win32";
22755
- let command = executable;
22756
- let commandArgs = args;
22757
- if (isWin && /\.(cmd|bat)$/i.test(executable)) {
22758
- command = env.ComSpec ?? process.env.ComSpec ?? "cmd.exe";
22759
- commandArgs = ["/d", "/s", "/c", executable, ...args];
23490
+ function readRawGit(repoRoot, args, code) {
23491
+ const result = runGitResult(repoRoot, args);
23492
+ if (result.status !== 0) {
23493
+ throw new BindingSwitchGitError(
23494
+ code,
23495
+ (result.stderr || result.stdout || "Git inspection failed").trim()
23496
+ );
23497
+ }
23498
+ return result.stdout;
23499
+ }
23500
+ function updateLengthPrefixed(hash, value2) {
23501
+ const bytes = Buffer.from(value2, "utf8");
23502
+ hash.update(String(bytes.byteLength));
23503
+ hash.update("\0");
23504
+ hash.update(bytes);
23505
+ hash.update("\0");
23506
+ }
23507
+ function proposedTreeSha(repoRoot, headSha) {
23508
+ const tempDir = (0, import_node_fs4.mkdtempSync)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "alan-switch-index-"));
23509
+ const indexPath = (0, import_node_path4.join)(tempDir, "index");
23510
+ const options = { env: { GIT_INDEX_FILE: indexPath } };
23511
+ try {
23512
+ if (headSha) {
23513
+ runGitWithOptions(repoRoot, ["read-tree", headSha], options, "git_status_unreadable");
23514
+ } else {
23515
+ runGitWithOptions(repoRoot, ["read-tree", "--empty"], options, "git_status_unreadable");
22760
23516
  }
22761
- const useShell = isWin && command === executable && !/[\\/]/.test(executable);
22762
- const closeStdin = spawnOptions?.closeStdin === true;
22763
- let child;
23517
+ runGitWithOptions(repoRoot, ["add", "--all"], options, "git_status_unreadable");
23518
+ const treeSha = runGitWithOptions(repoRoot, ["write-tree"], options, "git_status_unreadable");
23519
+ assertCommitSha(treeSha, "proposedTreeSha");
23520
+ return treeSha;
23521
+ } finally {
23522
+ (0, import_node_fs4.rmSync)(tempDir, { recursive: true, force: true });
23523
+ }
23524
+ }
23525
+ function statusFingerprint(repoRoot, porcelain, proposedTree) {
23526
+ const unstagedPatch = readRawGit(
23527
+ repoRoot,
23528
+ ["diff", "--binary", "--full-index", "--no-ext-diff", "--submodule=log"],
23529
+ "git_status_unreadable"
23530
+ );
23531
+ const stagedPatch = readRawGit(
23532
+ repoRoot,
23533
+ ["diff", "--cached", "--binary", "--full-index", "--no-ext-diff", "--submodule=log"],
23534
+ "git_status_unreadable"
23535
+ );
23536
+ const untrackedRaw = readRawGit(
23537
+ repoRoot,
23538
+ ["ls-files", "--others", "--exclude-standard", "-z"],
23539
+ "git_status_unreadable"
23540
+ );
23541
+ const untrackedPaths = untrackedRaw.split("\0").filter(Boolean);
23542
+ const hash = (0, import_node_crypto.createHash)("sha256");
23543
+ updateLengthPrefixed(hash, porcelain);
23544
+ updateLengthPrefixed(hash, proposedTree);
23545
+ updateLengthPrefixed(hash, unstagedPatch);
23546
+ updateLengthPrefixed(hash, stagedPatch);
23547
+ for (let index = 0; index < untrackedPaths.length; index += 128) {
23548
+ const paths = untrackedPaths.slice(index, index + 128);
23549
+ const hashes = readRawGit(
23550
+ repoRoot,
23551
+ ["hash-object", "--no-filters", "--", ...paths],
23552
+ "git_status_unreadable"
23553
+ );
23554
+ updateLengthPrefixed(hash, paths.join("\0"));
23555
+ updateLengthPrefixed(hash, hashes);
23556
+ }
23557
+ return hash.digest("hex");
23558
+ }
23559
+ function readStatus(repoRoot) {
23560
+ const headResult = runGitResult(repoRoot, ["rev-parse", "HEAD"]);
23561
+ const headSha = headResult.status === 0 ? headResult.stdout.trim() || null : null;
23562
+ const branchResult = runGitResult(repoRoot, ["branch", "--show-current"]);
23563
+ const branch = branchResult.status === 0 ? branchResult.stdout.trim() || null : null;
23564
+ const proposedTree = proposedTreeSha(repoRoot, headSha);
23565
+ const porcelain = runGit(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
23566
+ const displayStatus = runGit(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
23567
+ const upstreamResult = runGitResult(repoRoot, [
23568
+ "rev-parse",
23569
+ "--abbrev-ref",
23570
+ "--symbolic-full-name",
23571
+ "@{upstream}"
23572
+ ]);
23573
+ const upstream = upstreamResult.status === 0 ? upstreamResult.stdout.trim() || null : null;
23574
+ let ahead = null;
23575
+ let behind = null;
23576
+ if (upstream) {
23577
+ const counts = runGit(repoRoot, [
23578
+ "rev-list",
23579
+ "--left-right",
23580
+ "--count",
23581
+ `${upstream}...HEAD`
23582
+ ]).split(/\s+/);
23583
+ const parsedBehind = Number.parseInt(counts[0] ?? "", 10);
23584
+ const parsedAhead = Number.parseInt(counts[1] ?? "", 10);
23585
+ behind = Number.isNaN(parsedBehind) ? null : parsedBehind;
23586
+ ahead = Number.isNaN(parsedAhead) ? null : parsedAhead;
23587
+ }
23588
+ return {
23589
+ repoRoot,
23590
+ branch,
23591
+ headSha,
23592
+ dirty: porcelain.length > 0,
23593
+ filesChanged: displayStatus ? displayStatus.split("\n").length : 0,
23594
+ ahead,
23595
+ behind,
23596
+ hasUpstream: upstream !== null,
23597
+ upstream,
23598
+ statusFingerprint: statusFingerprint(repoRoot, porcelain, proposedTree),
23599
+ proposedTreeSha: proposedTree
23600
+ };
23601
+ }
23602
+ function assertExpectedBranch(status, expectedBranch) {
23603
+ assertValidBranch(expectedBranch);
23604
+ if (status.branch !== expectedBranch) {
23605
+ throw new BindingSwitchGitError(
23606
+ "source_changed_since_approval",
23607
+ `Source branch changed from ${expectedBranch} to ${status.branch ?? "detached HEAD"}`
23608
+ );
23609
+ }
23610
+ }
23611
+ function assertSourcePreconditions(status, expectedHeadSha, expectedStatusFingerprint, expectedBranch) {
23612
+ assertCommitSha(expectedHeadSha, "expectedHeadSha");
23613
+ if (expectedBranch !== void 0) assertExpectedBranch(status, expectedBranch);
23614
+ if (status.headSha !== expectedHeadSha) {
23615
+ throw new BindingSwitchGitError(
23616
+ "source_changed_since_approval",
23617
+ `Source HEAD changed from ${expectedHeadSha} to ${status.headSha ?? "unborn"}`
23618
+ );
23619
+ }
23620
+ if (expectedStatusFingerprint !== void 0 && status.statusFingerprint !== expectedStatusFingerprint) {
23621
+ throw new BindingSwitchGitError(
23622
+ "source_changed_since_approval",
23623
+ "Source files changed after the migration inspection"
23624
+ );
23625
+ }
23626
+ }
23627
+ function commitHasSwitchTrailers(repoRoot, switchId, repoKey) {
23628
+ const message = runGitResult(repoRoot, ["show", "-s", "--format=%B", "HEAD"]);
23629
+ if (message.status !== 0) return false;
23630
+ const lines = message.stdout.split(/\r?\n/);
23631
+ return lines.includes(`Alan-Switch-Id: ${switchId}`) && lines.includes(`Alan-Repository-Id: ${repoKey}`);
23632
+ }
23633
+ async function checkpoint(params) {
23634
+ const { switchId, repoKey } = commonRequest(params);
23635
+ const repoRoot = resolveAuthorizedRepository(params, true);
23636
+ const expectedHeadSha = requiredString(params, "expectedHeadSha");
23637
+ const expectedStatusFingerprint = requiredString(params, "expectedStatusFingerprint");
23638
+ const expectedProposedTreeSha = requiredString(params, "expectedProposedTreeSha");
23639
+ assertCommitSha(expectedProposedTreeSha, "expectedProposedTreeSha");
23640
+ const expectedBranch = requiredString(params, "expectedBranch");
23641
+ const status = readStatus(repoRoot);
23642
+ assertExpectedBranch(status, expectedBranch);
23643
+ if (commitHasSwitchTrailers(repoRoot, switchId, repoKey)) {
23644
+ if (!status.headSha) {
23645
+ throw new BindingSwitchGitError("source_head_unreadable", "Checkpoint HEAD is unreadable");
23646
+ }
23647
+ const expectedRef2 = `refs/heads/${expectedBranch}`;
23648
+ const baseline2 = createCheckpointIndexBaseline(repoRoot, status.headSha);
23649
+ let locks2 = null;
22764
23650
  try {
22765
- child = (0, import_node_child_process2.spawn)(command, commandArgs, {
22766
- env,
22767
- shell: useShell,
22768
- windowsHide: isWin ? true : void 0,
22769
- timeout: timeoutMs,
22770
- stdio: closeStdin ? ["pipe", "pipe", "pipe"] : void 0
22771
- });
22772
- } catch (error) {
22773
- resolve6({ status: null, stdout: "", stderr: "", error });
22774
- return;
23651
+ locks2 = acquireCheckpointRepositoryLocks(repoRoot);
23652
+ assertCheckpointHead(repoRoot, expectedRef2, status.headSha);
23653
+ installCheckpointIndexBaseline(locks2, baseline2.indexPath);
23654
+ return { repoRoot, sha: status.headSha, replayed: true };
23655
+ } finally {
23656
+ if (locks2) releaseCheckpointRepositoryLocks(locks2);
23657
+ (0, import_node_fs4.rmSync)(baseline2.tempDir, { recursive: true, force: true });
22775
23658
  }
22776
- if (closeStdin) {
22777
- child.stdin?.end();
23659
+ }
23660
+ assertSourcePreconditions(status, expectedHeadSha, expectedStatusFingerprint, expectedBranch);
23661
+ if (status.proposedTreeSha !== expectedProposedTreeSha) {
23662
+ throw new BindingSwitchGitError(
23663
+ "source_changed_since_approval",
23664
+ "The proposed checkpoint tree changed after migration approval"
23665
+ );
23666
+ }
23667
+ if (!status.dirty) {
23668
+ if (!status.headSha) {
23669
+ throw new BindingSwitchGitError("source_head_unreadable", "Source HEAD is unreadable");
22778
23670
  }
22779
- let stdout = "";
22780
- let stderr = "";
22781
- child.stdout?.setEncoding("utf8").on("data", (chunk) => {
22782
- stdout += chunk;
22783
- });
22784
- child.stderr?.setEncoding("utf8").on("data", (chunk) => {
22785
- stderr += chunk;
22786
- });
22787
- child.once("error", (error) => resolve6({ status: null, stdout, stderr, error }));
22788
- child.once(
22789
- "close",
22790
- (status, signal) => resolve6({ status, stdout, stderr, signal: signal ?? null })
23671
+ return { repoRoot, sha: status.headSha, replayed: true };
23672
+ }
23673
+ const commitMessage = requiredCommitMessage(params).trim();
23674
+ const authorName = requiredString(params, "authorName");
23675
+ const authorEmail = requiredString(params, "authorEmail");
23676
+ const expectedHeadTree = runGit(
23677
+ repoRoot,
23678
+ ["rev-parse", `${expectedHeadSha}^{tree}`],
23679
+ "source_head_unreadable"
23680
+ );
23681
+ if (expectedHeadTree === expectedProposedTreeSha) {
23682
+ throw new BindingSwitchGitError(
23683
+ "checkpoint_empty",
23684
+ "The inspected changes did not produce a checkpoint commit"
23685
+ );
23686
+ }
23687
+ const proposedTreeObject = runGitResult(repoRoot, [
23688
+ "cat-file",
23689
+ "-e",
23690
+ `${expectedProposedTreeSha}^{tree}`
23691
+ ]);
23692
+ if (proposedTreeObject.status !== 0) {
23693
+ throw new BindingSwitchGitError(
23694
+ "checkpoint_tree_unavailable",
23695
+ "The approved checkpoint tree is no longer available"
23696
+ );
23697
+ }
23698
+ await waitForCheckpointTestBarrier();
23699
+ const currentBranch = runGit(repoRoot, ["branch", "--show-current"]);
23700
+ const currentHead = runGit(repoRoot, ["rev-parse", "HEAD"], "source_head_unreadable");
23701
+ if (currentBranch !== expectedBranch || currentHead !== expectedHeadSha) {
23702
+ throw new BindingSwitchGitError(
23703
+ "source_changed_since_approval",
23704
+ "Source HEAD or branch changed during checkpoint preparation"
23705
+ );
23706
+ }
23707
+ const message = `${commitMessage}
23708
+
23709
+ Alan-Switch-Id: ${switchId}
23710
+ Alan-Repository-Id: ${repoKey}`;
23711
+ const sha = runGitWithOptions(
23712
+ repoRoot,
23713
+ ["commit-tree", expectedProposedTreeSha, "-p", expectedHeadSha],
23714
+ {
23715
+ env: {
23716
+ GIT_AUTHOR_EMAIL: authorEmail,
23717
+ GIT_AUTHOR_NAME: authorName,
23718
+ GIT_COMMITTER_EMAIL: authorEmail,
23719
+ GIT_COMMITTER_NAME: authorName
23720
+ },
23721
+ input: `${message}
23722
+ `
23723
+ },
23724
+ "checkpoint_commit_failed"
23725
+ );
23726
+ assertCommitSha(sha, "checkpointSha");
23727
+ const expectedRef = `refs/heads/${expectedBranch}`;
23728
+ const currentRef = runGit(
23729
+ repoRoot,
23730
+ ["symbolic-ref", "-q", "HEAD"],
23731
+ "source_changed_since_approval"
23732
+ );
23733
+ if (currentRef !== expectedRef) {
23734
+ throw new BindingSwitchGitError(
23735
+ "source_changed_since_approval",
23736
+ "Source branch changed during checkpoint preparation"
23737
+ );
23738
+ }
23739
+ await waitForCheckpointFinalizeTestBarrier();
23740
+ const baseline = createCheckpointIndexBaseline(repoRoot, sha);
23741
+ let locks = null;
23742
+ let refAdvanced = false;
23743
+ let indexInstalled = false;
23744
+ try {
23745
+ locks = acquireCheckpointRepositoryLocks(repoRoot);
23746
+ assertCheckpointHead(repoRoot, expectedRef, expectedHeadSha);
23747
+ releaseCheckpointHeadLock(locks);
23748
+ runGit(
23749
+ repoRoot,
23750
+ ["update-ref", expectedRef, sha, expectedHeadSha],
23751
+ "source_changed_since_approval"
22791
23752
  );
22792
- });
23753
+ refAdvanced = true;
23754
+ acquireCheckpointHeadLock(locks);
23755
+ assertCheckpointHead(repoRoot, expectedRef, sha);
23756
+ installCheckpointIndexBaseline(locks, baseline.indexPath);
23757
+ indexInstalled = true;
23758
+ return { repoRoot, sha, replayed: false };
23759
+ } finally {
23760
+ if (refAdvanced && !indexInstalled) {
23761
+ if (locks) releaseCheckpointHeadLock(locks);
23762
+ runGitResult(repoRoot, ["update-ref", expectedRef, expectedHeadSha, sha]);
23763
+ }
23764
+ if (locks) releaseCheckpointRepositoryLocks(locks);
23765
+ (0, import_node_fs4.rmSync)(baseline.tempDir, { recursive: true, force: true });
23766
+ }
23767
+ }
23768
+ function resolveRemoteBranch(repoRoot, params) {
23769
+ const expectedBranch = requiredString(params, "expectedBranch");
23770
+ const requestedBranch = optionalString(params, "branch");
23771
+ if (requestedBranch && requestedBranch !== expectedBranch) {
23772
+ throw new BindingSwitchGitError("invalid_git_request", "branch must match expectedBranch");
23773
+ }
23774
+ const currentBranch = runGit(repoRoot, ["branch", "--show-current"]);
23775
+ const branch = expectedBranch;
23776
+ assertValidBranch(branch);
23777
+ if (currentBranch !== expectedBranch) {
23778
+ throw new BindingSwitchGitError(
23779
+ "source_changed_since_approval",
23780
+ `Source branch changed from ${expectedBranch} to ${currentBranch || "detached HEAD"}`
23781
+ );
23782
+ }
23783
+ const { remote } = verifiedRemote(repoRoot, params, true);
23784
+ return { branch, remote };
23785
+ }
23786
+ async function push(params) {
23787
+ const repoRoot = resolveAuthorizedRepository(params, true);
23788
+ const expectedHeadSha = requiredString(params, "expectedHeadSha");
23789
+ const expectedBranch = requiredString(params, "expectedBranch");
23790
+ const status = readStatus(repoRoot);
23791
+ assertSourcePreconditions(status, expectedHeadSha, void 0, expectedBranch);
23792
+ if (status.dirty) {
23793
+ throw new BindingSwitchGitError(
23794
+ "source_dirty_after_checkpoint",
23795
+ "The source has uncommitted changes and cannot be published"
23796
+ );
23797
+ }
23798
+ const { branch, remote } = resolveRemoteBranch(repoRoot, params);
23799
+ const latest = readStatus(repoRoot);
23800
+ assertSourcePreconditions(latest, expectedHeadSha, void 0, expectedBranch);
23801
+ if (latest.dirty) {
23802
+ throw new BindingSwitchGitError(
23803
+ "source_dirty_after_checkpoint",
23804
+ "The source has uncommitted changes and cannot be published"
23805
+ );
23806
+ }
23807
+ const before = await runNetworkGit(
23808
+ repoRoot,
23809
+ ["ls-remote", "--heads", remote, `refs/heads/${branch}`],
23810
+ "git_remote_unavailable"
23811
+ );
23812
+ const remoteBefore = before.trim().split(/\s+/)[0] || null;
23813
+ await runNetworkGit(
23814
+ repoRoot,
23815
+ ["push", remote, `${expectedHeadSha}:refs/heads/${expectedBranch}`],
23816
+ "git_push_failed"
23817
+ );
23818
+ const remoteHead = (await runNetworkGit(
23819
+ repoRoot,
23820
+ ["ls-remote", "--heads", remote, `refs/heads/${branch}`],
23821
+ "push_verification_failed"
23822
+ )).split(/\s+/)[0]?.trim();
23823
+ if (remoteHead !== expectedHeadSha) {
23824
+ throw new BindingSwitchGitError(
23825
+ "push_verification_failed",
23826
+ `Remote branch ${branch} is at ${remoteHead || "an unreadable SHA"}, expected ${expectedHeadSha}`
23827
+ );
23828
+ }
23829
+ const afterPush = readStatus(repoRoot);
23830
+ if (afterPush.headSha !== expectedHeadSha || afterPush.branch !== expectedBranch || afterPush.dirty || afterPush.statusFingerprint !== latest.statusFingerprint) {
23831
+ throw new BindingSwitchGitError(
23832
+ "source_changed_since_approval",
23833
+ "The source changed while the approved commit was being published",
23834
+ {
23835
+ published: {
23836
+ pushedSha: expectedHeadSha,
23837
+ branch,
23838
+ remote,
23839
+ replayed: remoteBefore === expectedHeadSha
23840
+ }
23841
+ }
23842
+ );
23843
+ }
23844
+ return {
23845
+ repoRoot,
23846
+ pushedSha: expectedHeadSha,
23847
+ branch,
23848
+ remote,
23849
+ replayed: remoteBefore === expectedHeadSha
23850
+ };
22793
23851
  }
22794
- function runCliVersionProbeAsync(executable, env, args = ["--version"], timeoutMs = 3e3) {
22795
- return runCliProbeAsync(executable, env, args, timeoutMs);
23852
+ async function fetchPublishedBranch(repoRoot, branch, expectedSha, params) {
23853
+ assertValidBranch(branch);
23854
+ assertCommitSha(expectedSha, "expectedSha");
23855
+ const { remote } = verifiedRemote(repoRoot, params, false);
23856
+ await runNetworkGit(
23857
+ repoRoot,
23858
+ ["fetch", "--no-tags", remote, `refs/heads/${branch}`],
23859
+ "git_fetch_failed"
23860
+ );
23861
+ const fetchedSha = runGit(repoRoot, ["rev-parse", "FETCH_HEAD"], "target_sha_unavailable");
23862
+ if (fetchedSha !== expectedSha) {
23863
+ throw new BindingSwitchGitError(
23864
+ "target_sha_mismatch",
23865
+ `Fetched ${branch} at ${fetchedSha}, expected the published SHA ${expectedSha}`
23866
+ );
23867
+ }
23868
+ const commit = runGitResult(repoRoot, ["cat-file", "-e", `${expectedSha}^{commit}`]);
23869
+ if (commit.status !== 0) {
23870
+ throw new BindingSwitchGitError(
23871
+ "target_sha_unavailable",
23872
+ `The published SHA ${expectedSha} is not available in the destination repository`
23873
+ );
23874
+ }
23875
+ return { fetchedSha, remote };
22796
23876
  }
22797
- function pathSeparator() {
22798
- return (0, import_node_os3.platform)() === "win32" ? ";" : ":";
23877
+ async function fetchBranch(params) {
23878
+ const repoRoot = resolveAuthorizedRepository(params, true);
23879
+ const branch = requiredString(params, "branch");
23880
+ const expectedSha = requiredString(params, "expectedSha");
23881
+ const { fetchedSha, remote } = await fetchPublishedBranch(repoRoot, branch, expectedSha, params);
23882
+ return { repoRoot, fetchedSha, branch, remote };
22799
23883
  }
22800
- function splitPath2(pathValue) {
22801
- if (!pathValue) return [];
22802
- return pathValue.split(pathSeparator()).filter(Boolean);
23884
+ function isAncestor(repoRoot, ancestor, descendant) {
23885
+ return runGitResult(repoRoot, ["merge-base", "--is-ancestor", ancestor, descendant]).status === 0;
22803
23886
  }
22804
- function isRunnableFile(path) {
22805
- if (!(0, import_node_fs3.existsSync)(path)) return false;
22806
- if ((0, import_node_os3.platform)() === "win32") return true;
22807
- try {
22808
- (0, import_node_fs3.accessSync)(path, import_node_fs3.constants.X_OK);
22809
- return true;
22810
- } catch {
22811
- return false;
23887
+ function assertCleanTarget(repoRoot) {
23888
+ if (readStatus(repoRoot).dirty) {
23889
+ throw new BindingSwitchGitError(
23890
+ "target_checkout_dirty",
23891
+ "The destination checkout has uncommitted changes"
23892
+ );
22812
23893
  }
22813
23894
  }
22814
- function windowsCommandCandidates(command, pathExt) {
22815
- const trimmed = command.trim();
22816
- if (!trimmed) return [];
22817
- const hasExtension = /\.[a-z0-9]+$/i.test(trimmed);
22818
- if (hasExtension) return [trimmed];
22819
- const extensions = pathExt.split(";").map((ext) => ext.trim()).filter(Boolean);
22820
- return [trimmed, ...extensions.map((ext) => `${trimmed}${ext}`)];
23895
+ function assertBranchAvailableAtPath(repoRoot, branch, allowedPath) {
23896
+ const canonicalAllowedPath = canonicalPathWithMissingTail(allowedPath);
23897
+ const conflicting = listWorktrees(repoRoot).find(
23898
+ (worktree) => worktree.branch === branch && worktree.path !== canonicalAllowedPath
23899
+ );
23900
+ if (conflicting) {
23901
+ throw new BindingSwitchGitError(
23902
+ "target_branch_in_use",
23903
+ `Branch ${branch} is already checked out at ${conflicting.path}`
23904
+ );
23905
+ }
22821
23906
  }
22822
- function augmentCliPath2(env) {
22823
- const home = env.HOME || env.USERPROFILE || (0, import_node_os3.homedir)();
22824
- const isWin = (0, import_node_os3.platform)() === "win32";
22825
- const extraPaths = isWin ? [
22826
- (0, import_node_path3.join)(home, "AppData", "Roaming", "npm"),
22827
- (0, import_node_path3.join)(home, "AppData", "Local", "Programs", "Microsoft", "WindowsApps"),
22828
- (0, import_node_path3.join)(home, ".local", "bin"),
22829
- "C:\\Program Files\\nodejs",
22830
- "C:\\Program Files\\Git\\cmd"
22831
- ] : [
22832
- (0, import_node_path3.join)(home, ".local", "bin"),
22833
- (0, import_node_path3.join)(home, ".local", "node", "bin"),
22834
- (0, import_node_path3.join)(home, ".bun", "bin"),
22835
- (0, import_node_path3.join)(home, ".cargo", "bin"),
22836
- "/opt/homebrew/bin",
22837
- "/usr/local/bin"
22838
- ];
22839
- const basePath = env.PATH ?? "";
22840
- const existing = new Set(splitPath2(basePath));
22841
- const missing = extraPaths.filter((dir) => !existing.has(dir));
22842
- if (missing.length === 0) return env;
23907
+ function advanceBranchAtCheckout(checkoutRoot, branch, targetSha) {
23908
+ const before = readStatus(checkoutRoot);
23909
+ if (before.branch === branch && before.headSha === targetSha) return true;
23910
+ assertBranchAvailableAtPath(checkoutRoot, branch, checkoutRoot);
23911
+ const branchExists = runGitResult(checkoutRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
23912
+ if (branchExists) {
23913
+ const branchHead = runGit(checkoutRoot, ["rev-parse", `refs/heads/${branch}`]);
23914
+ if (!isAncestor(checkoutRoot, branchHead, targetSha)) {
23915
+ throw new BindingSwitchGitError(
23916
+ "target_branch_diverged",
23917
+ `Local branch ${branch} cannot fast-forward to the published SHA`
23918
+ );
23919
+ }
23920
+ if (before.branch !== branch) {
23921
+ runGit(checkoutRoot, ["switch", branch], "target_checkout_failed");
23922
+ }
23923
+ } else {
23924
+ runGit(checkoutRoot, ["switch", "-c", branch, targetSha], "target_checkout_failed");
23925
+ }
23926
+ const currentHead = runGit(checkoutRoot, ["rev-parse", "HEAD"]);
23927
+ if (currentHead !== targetSha) {
23928
+ if (!isAncestor(checkoutRoot, currentHead, targetSha)) {
23929
+ throw new BindingSwitchGitError(
23930
+ "target_branch_diverged",
23931
+ `Local branch ${branch} cannot fast-forward to the published SHA`
23932
+ );
23933
+ }
23934
+ runGit(checkoutRoot, ["merge", "--ff-only", targetSha], "target_fast_forward_failed");
23935
+ }
23936
+ const verified = readStatus(checkoutRoot);
23937
+ if (verified.branch !== branch || verified.headSha !== targetSha) {
23938
+ throw new BindingSwitchGitError(
23939
+ "target_verification_failed",
23940
+ "The destination checkout did not land on the published branch and SHA"
23941
+ );
23942
+ }
23943
+ return false;
23944
+ }
23945
+ async function prepareCheckout(params) {
23946
+ const repoRoot = resolveAuthorizedRepository(params, true);
23947
+ const branch = requiredString(params, "branch");
23948
+ const targetSha = requiredString(params, "targetSha");
23949
+ await fetchPublishedBranch(repoRoot, branch, targetSha, params);
23950
+ assertCleanTarget(repoRoot);
23951
+ const replayed = advanceBranchAtCheckout(repoRoot, branch, targetSha);
22843
23952
  return {
22844
- ...env,
22845
- PATH: missing.length > 0 ? `${missing.join(pathSeparator())}${pathSeparator()}${basePath}` : basePath
23953
+ repoRoot,
23954
+ path: repoRoot,
23955
+ branch,
23956
+ headSha: targetSha,
23957
+ replayed
22846
23958
  };
22847
23959
  }
22848
- function resolveCliExecutable(command, env = getDaemonCliEnvironment()) {
22849
- const trimmed = command.trim();
22850
- if (!trimmed) return null;
22851
- const cached = getCachedResolvedCli(trimmed);
22852
- if (cached === null) return null;
22853
- if (cached && isRunnableFile(cached)) return cached;
22854
- if ((trimmed.includes("/") || trimmed.includes("\\")) && isRunnableFile(trimmed)) {
22855
- cacheResolvedCli(trimmed, trimmed);
22856
- return trimmed;
23960
+ async function prepareWorktree(params) {
23961
+ const repoRoot = resolveAuthorizedRepository(params, true);
23962
+ const registrations = listWorktrees(repoRoot);
23963
+ const projectRoot = registrations[0]?.path;
23964
+ if (!projectRoot || projectRoot !== repoRoot) {
23965
+ throw new BindingSwitchGitError(
23966
+ "project_checkout_required",
23967
+ "Worktree preparation requires the Project's main checkout"
23968
+ );
22857
23969
  }
22858
- const enriched = augmentCliPath2(env);
22859
- const home = enriched.HOME || enriched.USERPROFILE || (0, import_node_os3.homedir)();
22860
- const isWin = (0, import_node_os3.platform)() === "win32";
22861
- const pathExt = enriched.PATHEXT ?? (isWin ? ".EXE;.CMD;.BAT;.COM" : "");
22862
- const commandNames = [trimmed, ...CLI_COMMAND_ALIASES[trimmed] ?? []];
22863
- const names = commandNames.flatMap(
22864
- (name) => isWin ? windowsCommandCandidates(name, pathExt) : [name]
23970
+ const worktreePath = (0, import_node_path4.resolve)(requiredString(params, "worktreePath"));
23971
+ const managedRoot = alanWorktreeRoot(projectRoot);
23972
+ const pathError = "The destination worktree path is outside the Alan-managed worktree directory";
23973
+ const intendedManagedRoot = (0, import_node_path4.join)(
23974
+ import_node_fs4.realpathSync.native((0, import_node_path4.dirname)(managedRoot)),
23975
+ (0, import_node_path4.basename)(managedRoot)
22865
23976
  );
22866
- const candidates = [];
22867
- for (const name of names) {
22868
- if (name.includes("/") || name.includes("\\")) {
22869
- candidates.push(name);
22870
- continue;
22871
- }
22872
- for (const dir of splitPath2(enriched.PATH)) {
22873
- candidates.push((0, import_node_path3.join)(dir, name));
23977
+ if (canonicalPathWithMissingTail(managedRoot) !== intendedManagedRoot) {
23978
+ throw new BindingSwitchGitError("worktree_path_not_authorized", pathError);
23979
+ }
23980
+ assertCanonicalPathContained(
23981
+ worktreePath,
23982
+ managedRoot,
23983
+ "worktree_path_not_authorized",
23984
+ pathError
23985
+ );
23986
+ const canonicalWorktreePath = canonicalPathWithMissingTail(worktreePath);
23987
+ if (canonicalWorktreePath === canonicalPathWithMissingTail(managedRoot)) {
23988
+ throw new BindingSwitchGitError("worktree_path_not_authorized", pathError);
23989
+ }
23990
+ const branch = requiredString(params, "branch");
23991
+ const targetSha = requiredString(params, "targetSha");
23992
+ await fetchPublishedBranch(repoRoot, branch, targetSha, params);
23993
+ const refreshed = listWorktrees(repoRoot);
23994
+ const registeredAtPath = refreshed.find(({ path }) => path === canonicalWorktreePath);
23995
+ const registeredForBranch = refreshed.find(({ branch: value2 }) => value2 === branch);
23996
+ if (registeredAtPath) {
23997
+ if (registeredAtPath.branch !== branch) {
23998
+ throw new BindingSwitchGitError(
23999
+ "worktree_path_conflict",
24000
+ "The destination path is registered to a different branch"
24001
+ );
22874
24002
  }
22875
- candidates.push((0, import_node_path3.join)(home, ".local", "bin", name), (0, import_node_path3.join)(home, ".local", "node", "bin", name));
22876
- if (!isWin) {
22877
- candidates.push((0, import_node_path3.join)("/opt/homebrew/bin", name), (0, import_node_path3.join)("/usr/local/bin", name));
22878
- } else {
22879
- candidates.push((0, import_node_path3.join)(home, "AppData", "Roaming", "npm", name));
24003
+ assertCleanTarget(worktreePath);
24004
+ const replayed = advanceBranchAtCheckout(worktreePath, branch, targetSha);
24005
+ return {
24006
+ repoRoot: worktreePath,
24007
+ path: worktreePath,
24008
+ branch,
24009
+ headSha: targetSha,
24010
+ replayed
24011
+ };
24012
+ }
24013
+ if (registeredForBranch) {
24014
+ throw new BindingSwitchGitError(
24015
+ "target_branch_in_use",
24016
+ `Branch ${branch} is already checked out at ${registeredForBranch.path}`
24017
+ );
24018
+ }
24019
+ if ((0, import_node_fs4.existsSync)(worktreePath)) {
24020
+ throw new BindingSwitchGitError(
24021
+ "worktree_path_conflict",
24022
+ "The destination worktree path already exists and is not registered"
24023
+ );
24024
+ }
24025
+ const branchExists = runGitResult(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
24026
+ if (branchExists) {
24027
+ const branchHead = runGit(repoRoot, ["rev-parse", `refs/heads/${branch}`]);
24028
+ if (!isAncestor(repoRoot, branchHead, targetSha)) {
24029
+ throw new BindingSwitchGitError(
24030
+ "target_branch_diverged",
24031
+ `Local branch ${branch} cannot fast-forward to the published SHA`
24032
+ );
22880
24033
  }
22881
24034
  }
22882
- const seen = /* @__PURE__ */ new Set();
22883
- for (const candidate of candidates) {
22884
- if (seen.has(candidate)) continue;
22885
- seen.add(candidate);
22886
- if (isRunnableFile(candidate)) {
22887
- cacheResolvedCli(trimmed, candidate);
22888
- return candidate;
24035
+ (0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(worktreePath), { recursive: true });
24036
+ if (branchExists) {
24037
+ runGit(repoRoot, ["worktree", "add", worktreePath, branch], "worktree_prepare_failed");
24038
+ const currentHead = runGit(worktreePath, ["rev-parse", "HEAD"]);
24039
+ if (currentHead !== targetSha) {
24040
+ runGit(worktreePath, ["merge", "--ff-only", targetSha], "target_fast_forward_failed");
22889
24041
  }
24042
+ } else {
24043
+ runGit(
24044
+ repoRoot,
24045
+ ["worktree", "add", "-b", branch, worktreePath, targetSha],
24046
+ "worktree_prepare_failed"
24047
+ );
22890
24048
  }
22891
- const viaWhere = resolveViaWhere(trimmed, enriched);
22892
- if (viaWhere) {
22893
- cacheResolvedCli(trimmed, viaWhere);
22894
- return viaWhere;
24049
+ const createdRoot = resolveRepositoryRoot(worktreePath);
24050
+ assertCanonicalPathContained(createdRoot, managedRoot, "worktree_path_not_authorized", pathError);
24051
+ if (createdRoot !== import_node_fs4.realpathSync.native(worktreePath)) {
24052
+ throw new BindingSwitchGitError(
24053
+ "worktree_path_changed",
24054
+ "The destination worktree resolved to an unexpected path after creation"
24055
+ );
22895
24056
  }
22896
- cacheResolvedCli(trimmed, null);
22897
- return null;
22898
- }
22899
- function normalizeDiscoverableProvider(provider) {
22900
- const alias = PROVIDER_ALIASES[provider];
22901
- if (alias) return alias;
22902
- if (provider in PROVIDER_CLI_COMMANDS) {
22903
- return provider;
24057
+ const verified = readStatus(createdRoot);
24058
+ if (verified.branch !== branch || verified.headSha !== targetSha) {
24059
+ throw new BindingSwitchGitError(
24060
+ "target_verification_failed",
24061
+ "The destination worktree did not land on the published branch and SHA"
24062
+ );
22904
24063
  }
22905
- return null;
24064
+ return {
24065
+ repoRoot: worktreePath,
24066
+ path: worktreePath,
24067
+ branch,
24068
+ headSha: targetSha,
24069
+ replayed: false
24070
+ };
22906
24071
  }
22907
- function resolveProviderCliCommand(provider, env = getDaemonCliEnvironment()) {
22908
- const normalized = normalizeDiscoverableProvider(provider);
22909
- if (!normalized) return null;
22910
- const spec = PROVIDER_CLI_COMMANDS[normalized];
22911
- const override = spec.envVars.map((name) => process.env[name]?.trim() || env[name]?.trim()).find(Boolean);
22912
- if (override) {
22913
- const resolved2 = resolveCliExecutable(override, env) ?? (isRunnableFile(override) ? override : null);
22914
- if (resolved2) cacheResolvedCli(spec.command, resolved2);
22915
- return resolved2;
24072
+ function verifyHead(params) {
24073
+ const repoRoot = resolveAuthorizedRepository(params, true);
24074
+ const expectedSha = requiredString(params, "expectedSha");
24075
+ assertCommitSha(expectedSha, "expectedSha");
24076
+ assertCleanTarget(repoRoot);
24077
+ const headSha = runGit(repoRoot, ["rev-parse", "HEAD"], "target_head_unreadable");
24078
+ if (headSha !== expectedSha) {
24079
+ throw new BindingSwitchGitError(
24080
+ "verify_head_mismatch",
24081
+ `Destination HEAD is ${headSha}, expected ${expectedSha}`
24082
+ );
22916
24083
  }
22917
- const resolved = resolveCliExecutable(spec.command, env);
22918
- if (resolved) cacheResolvedCli(spec.command, resolved);
22919
- return resolved;
22920
- }
22921
- function resolveBackendRuntimeCommand(backendKind, env = getDaemonCliEnvironment()) {
22922
- if (!backendKind) return void 0;
22923
- const command = BACKEND_CLI_COMMANDS[backendKind];
22924
- if (!command) return void 0;
22925
- const cached = getCachedResolvedCli(command);
22926
- if (cached === null) return void 0;
22927
- if (cached && isRunnableFile(cached)) return cached;
22928
- const spec = Object.values(PROVIDER_CLI_COMMANDS).find((entry) => entry.command === command);
22929
- const envOverride = spec ? spec.envVars.map((name) => env[name]?.trim()).find(Boolean) : void 0;
22930
- const resolved = resolveCliExecutable(envOverride?.trim() || command, env);
22931
- cacheResolvedCli(command, resolved);
22932
- return resolved ?? void 0;
22933
- }
22934
- function invalidateResolvedCli(command) {
22935
- const trimmed = command.trim();
22936
- if (trimmed) {
22937
- resolvedCliCache.delete(trimmed);
22938
- negativeResolutionExpiry.delete(trimmed);
24084
+ return { repoRoot, headSha, verified: true };
24085
+ }
24086
+ function statusBindingSwitchGit(params) {
24087
+ return readStatus(resolveAuthorizedRepository(params));
24088
+ }
24089
+ async function executeBindingSwitchGitRpc(method, params) {
24090
+ switch (method) {
24091
+ case "git.status":
24092
+ return statusBindingSwitchGit(params);
24093
+ case "git.checkpoint":
24094
+ return checkpoint(params);
24095
+ case "git.push":
24096
+ return push(params);
24097
+ case "git.fetch":
24098
+ return fetchBranch(params);
24099
+ case "git.prepare_checkout":
24100
+ return prepareCheckout(params);
24101
+ case "git.prepare_worktree":
24102
+ return prepareWorktree(params);
24103
+ case "git.verify_head":
24104
+ return verifyHead(params);
22939
24105
  }
22940
24106
  }
22941
- function revalidateBackendRuntimeCommand(backendKind, env = getDaemonCliEnvironment()) {
22942
- if (!backendKind) return void 0;
22943
- const command = BACKEND_CLI_COMMANDS[backendKind];
22944
- if (!command) return void 0;
22945
- invalidateResolvedCli(command);
22946
- const spec = Object.values(PROVIDER_CLI_COMMANDS).find((entry) => entry.command === command);
22947
- const envOverride = spec ? spec.envVars.map((name) => env[name]?.trim()).find(Boolean) : void 0;
22948
- if (envOverride) invalidateResolvedCli(envOverride);
22949
- return resolveBackendRuntimeCommand(backendKind, env);
22950
- }
22951
24107
 
22952
24108
  // src/cli-ensure-install.ts
24109
+ var import_node_child_process4 = require("child_process");
24110
+ var import_node_os5 = require("os");
22953
24111
  var CLI_RUNTIME_VERSIONS_ENV = "ALAN_CLI_RUNTIME_VERSIONS";
22954
24112
  var INSTALLABLE_PROVIDER_PACKAGES = {
22955
24113
  claude_cli: { npmPackage: "@anthropic-ai/claude-code", command: "claude", label: "Claude Code" },
@@ -22974,11 +24132,11 @@ function pinnedVersion(npmPackage) {
22974
24132
  return "latest";
22975
24133
  }
22976
24134
  function runNpmInstall(installSpec, env, timeoutMs) {
22977
- return new Promise((resolve6) => {
22978
- const isWin = (0, import_node_os4.platform)() === "win32";
24135
+ return new Promise((resolve7) => {
24136
+ const isWin = (0, import_node_os5.platform)() === "win32";
22979
24137
  let child;
22980
24138
  try {
22981
- child = (0, import_node_child_process3.spawn)("npm", ["install", "-g", installSpec], {
24139
+ child = (0, import_node_child_process4.spawn)("npm", ["install", "-g", installSpec], {
22982
24140
  env,
22983
24141
  shell: isWin,
22984
24142
  windowsHide: isWin ? true : void 0,
@@ -22986,7 +24144,7 @@ function runNpmInstall(installSpec, env, timeoutMs) {
22986
24144
  stdio: ["ignore", "pipe", "pipe"]
22987
24145
  });
22988
24146
  } catch (error) {
22989
- resolve6({ status: null, stdout: "", stderr: "", error });
24147
+ resolve7({ status: null, stdout: "", stderr: "", error });
22990
24148
  return;
22991
24149
  }
22992
24150
  let stdout = "";
@@ -22997,10 +24155,10 @@ function runNpmInstall(installSpec, env, timeoutMs) {
22997
24155
  child.stderr?.setEncoding("utf8").on("data", (chunk) => {
22998
24156
  stderr += chunk;
22999
24157
  });
23000
- child.once("error", (error) => resolve6({ status: null, stdout, stderr, error }));
24158
+ child.once("error", (error) => resolve7({ status: null, stdout, stderr, error }));
23001
24159
  child.once(
23002
24160
  "close",
23003
- (status, signal) => resolve6({
24161
+ (status, signal) => resolve7({
23004
24162
  status,
23005
24163
  stdout,
23006
24164
  stderr,
@@ -23045,14 +24203,14 @@ function ensureProviderCliInstalled(backendKind, options) {
23045
24203
  }
23046
24204
 
23047
24205
  // src/computer-readiness.ts
23048
- var import_node_fs5 = require("fs");
24206
+ var import_node_fs6 = require("fs");
23049
24207
 
23050
24208
  // src/runtime-config.ts
23051
- var import_node_fs4 = require("fs");
23052
- var import_node_os5 = require("os");
23053
- var import_node_path4 = require("path");
23054
- var DEFAULT_AGENT_DIR = (0, import_node_path4.join)((0, import_node_os5.homedir)(), ".alan", "agent");
23055
- var LEGACY_AGENT_DIR = (0, import_node_path4.join)((0, import_node_os5.homedir)(), ".aiden", "agent");
24209
+ var import_node_fs5 = require("fs");
24210
+ var import_node_os6 = require("os");
24211
+ var import_node_path5 = require("path");
24212
+ var DEFAULT_AGENT_DIR = (0, import_node_path5.join)((0, import_node_os6.homedir)(), ".alan", "agent");
24213
+ var LEGACY_AGENT_DIR = (0, import_node_path5.join)((0, import_node_os6.homedir)(), ".aiden", "agent");
23056
24214
  var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Map([
23057
24215
  ["apiUrl", "string"],
23058
24216
  ["wsUrl", "string"],
@@ -23074,9 +24232,9 @@ function sanitizeAgentConfig(config) {
23074
24232
  }
23075
24233
  function isValidAgentConfig(configPath) {
23076
24234
  try {
23077
- const stat = (0, import_node_fs4.lstatSync)(configPath);
24235
+ const stat = (0, import_node_fs5.lstatSync)(configPath);
23078
24236
  if (!stat.isFile() || stat.isSymbolicLink()) return false;
23079
- const parsed = JSON.parse((0, import_node_fs4.readFileSync)(configPath, "utf8"));
24237
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)(configPath, "utf8"));
23080
24238
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
23081
24239
  const entries = Object.entries(parsed);
23082
24240
  return entries.some(([key]) => AGENT_CONFIG_KEYS.has(key)) && entries.every(([key, value2]) => {
@@ -23092,36 +24250,36 @@ function migrateLegacyAgentConfig(input) {
23092
24250
  const alanBaseDir = input?.alanBaseDir ?? DEFAULT_AGENT_DIR;
23093
24251
  const legacyBaseDir = input?.legacyBaseDir ?? LEGACY_AGENT_DIR;
23094
24252
  const fileName = resolveAgentConfigFileName(profile);
23095
- const legacyPath = (0, import_node_path4.join)(legacyBaseDir, fileName);
23096
- const alanPath = (0, import_node_path4.join)(alanBaseDir, fileName);
23097
- if ((0, import_node_fs4.existsSync)(alanPath) || !isValidAgentConfig(legacyPath)) return false;
24253
+ const legacyPath = (0, import_node_path5.join)(legacyBaseDir, fileName);
24254
+ const alanPath = (0, import_node_path5.join)(alanBaseDir, fileName);
24255
+ if ((0, import_node_fs5.existsSync)(alanPath) || !isValidAgentConfig(legacyPath)) return false;
23098
24256
  let copied = false;
23099
24257
  try {
23100
- const sourceDirectoryStat = (0, import_node_fs4.lstatSync)(legacyBaseDir);
23101
- const sourceFileStat = (0, import_node_fs4.lstatSync)(legacyPath);
24258
+ const sourceDirectoryStat = (0, import_node_fs5.lstatSync)(legacyBaseDir);
24259
+ const sourceFileStat = (0, import_node_fs5.lstatSync)(legacyPath);
23102
24260
  if (!sourceDirectoryStat.isDirectory() || sourceDirectoryStat.isSymbolicLink()) return false;
23103
- const alanDirectoryExisted = (0, import_node_fs4.existsSync)(alanBaseDir);
24261
+ const alanDirectoryExisted = (0, import_node_fs5.existsSync)(alanBaseDir);
23104
24262
  if (alanDirectoryExisted) {
23105
- const alanDirectoryStat = (0, import_node_fs4.lstatSync)(alanBaseDir);
24263
+ const alanDirectoryStat = (0, import_node_fs5.lstatSync)(alanBaseDir);
23106
24264
  if (!alanDirectoryStat.isDirectory() || alanDirectoryStat.isSymbolicLink()) return false;
23107
24265
  }
23108
- (0, import_node_fs4.mkdirSync)(alanBaseDir, { recursive: true, mode: sourceDirectoryStat.mode & 511 });
23109
- if (!alanDirectoryExisted) (0, import_node_fs4.chmodSync)(alanBaseDir, sourceDirectoryStat.mode & 511);
23110
- (0, import_node_fs4.copyFileSync)(legacyPath, alanPath, import_node_fs4.constants.COPYFILE_EXCL);
24266
+ (0, import_node_fs5.mkdirSync)(alanBaseDir, { recursive: true, mode: sourceDirectoryStat.mode & 511 });
24267
+ if (!alanDirectoryExisted) (0, import_node_fs5.chmodSync)(alanBaseDir, sourceDirectoryStat.mode & 511);
24268
+ (0, import_node_fs5.copyFileSync)(legacyPath, alanPath, import_node_fs5.constants.COPYFILE_EXCL);
23111
24269
  copied = true;
23112
24270
  const migrated = sanitizeAgentConfig(
23113
- JSON.parse((0, import_node_fs4.readFileSync)(alanPath, "utf8"))
24271
+ JSON.parse((0, import_node_fs5.readFileSync)(alanPath, "utf8"))
23114
24272
  );
23115
- (0, import_node_fs4.writeFileSync)(alanPath, JSON.stringify(migrated, null, 2));
23116
- (0, import_node_fs4.chmodSync)(alanPath, sourceFileStat.mode & 511);
23117
- (0, import_node_fs4.utimesSync)(alanPath, sourceFileStat.atime, sourceFileStat.mtime);
24273
+ (0, import_node_fs5.writeFileSync)(alanPath, JSON.stringify(migrated, null, 2));
24274
+ (0, import_node_fs5.chmodSync)(alanPath, sourceFileStat.mode & 511);
24275
+ (0, import_node_fs5.utimesSync)(alanPath, sourceFileStat.atime, sourceFileStat.mtime);
23118
24276
  if (!isValidAgentConfig(alanPath)) {
23119
- (0, import_node_fs4.unlinkSync)(alanPath);
24277
+ (0, import_node_fs5.unlinkSync)(alanPath);
23120
24278
  return false;
23121
24279
  }
23122
24280
  return true;
23123
24281
  } catch {
23124
- if (copied && (0, import_node_fs4.existsSync)(alanPath)) (0, import_node_fs4.unlinkSync)(alanPath);
24282
+ if (copied && (0, import_node_fs5.existsSync)(alanPath)) (0, import_node_fs5.unlinkSync)(alanPath);
23125
24283
  return false;
23126
24284
  }
23127
24285
  }
@@ -23132,10 +24290,10 @@ function resolveAgentConfigPath(input) {
23132
24290
  ) ?? "production";
23133
24291
  const baseDir = input?.baseDir ?? DEFAULT_AGENT_DIR;
23134
24292
  if (input?.baseDir === void 0) migrateLegacyAgentConfig({ profile });
23135
- return (0, import_node_path4.join)(baseDir, resolveAgentConfigFileName(profile));
24293
+ return (0, import_node_path5.join)(baseDir, resolveAgentConfigFileName(profile));
23136
24294
  }
23137
24295
  function configExistsAtPath(configPath) {
23138
- return (0, import_node_fs4.existsSync)(configPath);
24296
+ return (0, import_node_fs5.existsSync)(configPath);
23139
24297
  }
23140
24298
  function resolveEndpointProfileFromArgs(args) {
23141
24299
  const index = args.indexOf("--profile");
@@ -23182,9 +24340,9 @@ var WorkspaceAccessTracker = class {
23182
24340
  }
23183
24341
  };
23184
24342
  var DEFAULT_WORKSPACE_PROBE = {
23185
- stat: (path) => (0, import_node_fs5.statSync)(path),
23186
- access: (path, mode) => (0, import_node_fs5.accessSync)(path, mode),
23187
- exists: (path) => (0, import_node_fs5.existsSync)(path)
24343
+ stat: (path) => (0, import_node_fs6.statSync)(path),
24344
+ access: (path, mode) => (0, import_node_fs6.accessSync)(path, mode),
24345
+ exists: (path) => (0, import_node_fs6.existsSync)(path)
23188
24346
  };
23189
24347
  function workspaceVolumeRoot(path) {
23190
24348
  const windowsDrive = path.match(/^([A-Za-z]):[\\/]/);
@@ -23248,7 +24406,7 @@ function inspectWorkspaceReadiness(workspacePath, probe = DEFAULT_WORKSPACE_PROB
23248
24406
  probe
23249
24407
  );
23250
24408
  }
23251
- probe.access(workspacePath, import_node_fs5.constants.R_OK | import_node_fs5.constants.W_OK | import_node_fs5.constants.X_OK);
24409
+ probe.access(workspacePath, import_node_fs6.constants.R_OK | import_node_fs6.constants.W_OK | import_node_fs6.constants.X_OK);
23252
24410
  return {
23253
24411
  ready: true,
23254
24412
  message: "Workspace path is configured and accessible.",
@@ -23360,7 +24518,7 @@ function buildComputerReadiness(input) {
23360
24518
  }
23361
24519
 
23362
24520
  // src/core-agent.ts
23363
- var import_node_os6 = require("os");
24521
+ var import_node_os7 = require("os");
23364
24522
  var CoreAgent = class _CoreAgent extends BaseMachineAgent {
23365
24523
  childProcess = null;
23366
24524
  _aborted = false;
@@ -23510,7 +24668,7 @@ var CoreAgent = class _CoreAgent extends BaseMachineAgent {
23510
24668
  if (this.runtime.backendKind === "grok_cli" && this.providerApiKey) {
23511
24669
  augmented.XAI_API_KEY = this.providerApiKey;
23512
24670
  }
23513
- if ((0, import_node_os6.platform)() !== "win32") {
24671
+ if ((0, import_node_os7.platform)() !== "win32") {
23514
24672
  const home = augmented.HOME ?? "/home/user";
23515
24673
  const extraPaths = [`${home}/.local/node/bin`, `${home}/.local/bin`];
23516
24674
  const separator = ":";
@@ -23674,9 +24832,9 @@ ${meta.description}`);
23674
24832
  };
23675
24833
 
23676
24834
  // src/event-outbox.ts
23677
- var import_node_crypto = require("crypto");
23678
- var import_node_fs6 = require("fs");
23679
- var import_node_path5 = require("path");
24835
+ var import_node_crypto2 = require("crypto");
24836
+ var import_node_fs7 = require("fs");
24837
+ var import_node_path6 = require("path");
23680
24838
  var OUTBOX_VERSION = 1;
23681
24839
  var MAX_INTERMEDIATE_EVENTS_PER_RUN = 200;
23682
24840
  var MAX_ENCRYPTED_OUTBOX_BYTES = 64 * 1024 * 1024;
@@ -23709,7 +24867,7 @@ function truncateUtf8(value2, maxBytes) {
23709
24867
  return `${encoded.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "")}\u2026`;
23710
24868
  }
23711
24869
  function terminalFingerprint(event) {
23712
- return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(event)).digest("hex").slice(0, 16);
24870
+ return (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(event)).digest("hex").slice(0, 16);
23713
24871
  }
23714
24872
  function condenseTerminalEvent(event) {
23715
24873
  const fingerprint = terminalFingerprint(event);
@@ -23787,7 +24945,7 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
23787
24945
  throw new Error("event outbox maxIntermediateEventsPerRun must be a non-negative integer");
23788
24946
  }
23789
24947
  this.entries = this.read();
23790
- if (!(0, import_node_fs6.existsSync)(this.path)) {
24948
+ if (!(0, import_node_fs7.existsSync)(this.path)) {
23791
24949
  this.lastPersistedSignature = _EncryptedEventOutbox.EMPTY_SIGNATURE;
23792
24950
  } else if (this.entries.length > 0) {
23793
24951
  this.lastPersistedSignature = this.serializeForPersist().signature;
@@ -23817,7 +24975,7 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
23817
24975
  maxIntermediateEventsPerRun;
23818
24976
  enqueue(conversationId, runId, event) {
23819
24977
  const entry = {
23820
- eventId: (0, import_node_crypto.randomUUID)(),
24978
+ eventId: (0, import_node_crypto2.randomUUID)(),
23821
24979
  conversationId,
23822
24980
  runId,
23823
24981
  event,
@@ -23903,7 +25061,7 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
23903
25061
  const markerConversationId = runEntry?.conversationId ?? conversationId;
23904
25062
  if (!markerConversationId) return;
23905
25063
  const marker = {
23906
- eventId: (0, import_node_crypto.randomUUID)(),
25064
+ eventId: (0, import_node_crypto2.randomUUID)(),
23907
25065
  conversationId: markerConversationId,
23908
25066
  runId,
23909
25067
  event: { type: "text", text: CONDENSATION_NOTICE, ts: Date.now() },
@@ -23989,14 +25147,14 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
23989
25147
  this.retryTimer = null;
23990
25148
  }
23991
25149
  read() {
23992
- if (!(0, import_node_fs6.existsSync)(this.path)) return [];
25150
+ if (!(0, import_node_fs7.existsSync)(this.path)) return [];
23993
25151
  let envelope;
23994
25152
  try {
23995
- envelope = JSON.parse((0, import_node_fs6.readFileSync)(this.path, "utf8"));
25153
+ envelope = JSON.parse((0, import_node_fs7.readFileSync)(this.path, "utf8"));
23996
25154
  if (envelope.version !== OUTBOX_VERSION) {
23997
25155
  throw new Error(`unsupported event outbox version ${String(envelope.version)}`);
23998
25156
  }
23999
- const decipher = (0, import_node_crypto.createDecipheriv)(
25157
+ const decipher = (0, import_node_crypto2.createDecipheriv)(
24000
25158
  "aes-256-gcm",
24001
25159
  this.key,
24002
25160
  Buffer.from(envelope.iv, "base64url")
@@ -24020,21 +25178,21 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
24020
25178
  return { plaintext: "", signature: _EncryptedEventOutbox.EMPTY_SIGNATURE };
24021
25179
  }
24022
25180
  const plaintext = JSON.stringify({ entries: this.entries });
24023
- return { plaintext, signature: (0, import_node_crypto.createHash)("sha256").update(plaintext).digest("base64url") };
25181
+ return { plaintext, signature: (0, import_node_crypto2.createHash)("sha256").update(plaintext).digest("base64url") };
24024
25182
  }
24025
25183
  persist() {
24026
25184
  const { plaintext, signature } = this.serializeForPersist();
24027
25185
  if (signature === this.lastPersistedSignature) return;
24028
25186
  try {
24029
25187
  if (this.entries.length === 0) {
24030
- (0, import_node_fs6.rmSync)(this.path, { force: true });
25188
+ (0, import_node_fs7.rmSync)(this.path, { force: true });
24031
25189
  this.lastPersistedSignature = signature;
24032
25190
  this.persistFailureLogged = false;
24033
25191
  return;
24034
25192
  }
24035
- (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(this.path), { recursive: true, mode: 448 });
24036
- const iv = (0, import_node_crypto.randomBytes)(12);
24037
- const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", this.key, iv);
25193
+ (0, import_node_fs7.mkdirSync)((0, import_node_path6.dirname)(this.path), { recursive: true, mode: 448 });
25194
+ const iv = (0, import_node_crypto2.randomBytes)(12);
25195
+ const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", this.key, iv);
24038
25196
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
24039
25197
  const envelope = {
24040
25198
  version: OUTBOX_VERSION,
@@ -24042,13 +25200,13 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
24042
25200
  authTag: cipher.getAuthTag().toString("base64url"),
24043
25201
  ciphertext: ciphertext.toString("base64url")
24044
25202
  };
24045
- const pendingPath = `${this.path}.pending-${process.pid}-${(0, import_node_crypto.randomBytes)(6).toString("hex")}`;
25203
+ const pendingPath = `${this.path}.pending-${process.pid}-${(0, import_node_crypto2.randomBytes)(6).toString("hex")}`;
24046
25204
  try {
24047
- (0, import_node_fs6.writeFileSync)(pendingPath, JSON.stringify(envelope), { mode: 384 });
24048
- (0, import_node_fs6.chmodSync)(pendingPath, 384);
24049
- (0, import_node_fs6.renameSync)(pendingPath, this.path);
25205
+ (0, import_node_fs7.writeFileSync)(pendingPath, JSON.stringify(envelope), { mode: 384 });
25206
+ (0, import_node_fs7.chmodSync)(pendingPath, 384);
25207
+ (0, import_node_fs7.renameSync)(pendingPath, this.path);
24050
25208
  } finally {
24051
- (0, import_node_fs6.rmSync)(pendingPath, { force: true });
25209
+ (0, import_node_fs7.rmSync)(pendingPath, { force: true });
24052
25210
  }
24053
25211
  this.lastPersistedSignature = signature;
24054
25212
  this.persistFailureLogged = false;
@@ -24066,38 +25224,38 @@ var EncryptedEventOutbox = class _EncryptedEventOutbox {
24066
25224
  };
24067
25225
 
24068
25226
  // src/local-skill-scan.ts
24069
- var import_node_fs7 = require("fs");
24070
- var import_node_path6 = require("path");
25227
+ var import_node_fs8 = require("fs");
25228
+ var import_node_path7 = require("path");
24071
25229
  var SKILL_READ_LIMIT_BYTES = 8192;
24072
25230
  var SKILL_CACHE_TTL_MS = 3e4;
24073
25231
  function readSkillHead(path) {
24074
- const fd = (0, import_node_fs7.openSync)(path, "r");
25232
+ const fd = (0, import_node_fs8.openSync)(path, "r");
24075
25233
  try {
24076
25234
  const buffer = Buffer.alloc(SKILL_READ_LIMIT_BYTES);
24077
- const bytesRead = (0, import_node_fs7.readSync)(fd, buffer, 0, SKILL_READ_LIMIT_BYTES, 0);
25235
+ const bytesRead = (0, import_node_fs8.readSync)(fd, buffer, 0, SKILL_READ_LIMIT_BYTES, 0);
24078
25236
  return buffer.subarray(0, bytesRead).toString("utf-8");
24079
25237
  } finally {
24080
- (0, import_node_fs7.closeSync)(fd);
25238
+ (0, import_node_fs8.closeSync)(fd);
24081
25239
  }
24082
25240
  }
24083
25241
  function scanSkillDir(dirPath, source, repoRoot, entries, errors) {
24084
- if (!(0, import_node_fs7.existsSync)(dirPath)) return;
25242
+ if (!(0, import_node_fs8.existsSync)(dirPath)) return;
24085
25243
  let names;
24086
25244
  try {
24087
- names = (0, import_node_fs7.readdirSync)(dirPath);
25245
+ names = (0, import_node_fs8.readdirSync)(dirPath);
24088
25246
  } catch (error) {
24089
25247
  errors.push(`Failed to scan ${dirPath}: ${error instanceof Error ? error.message : error}`);
24090
25248
  return;
24091
25249
  }
24092
25250
  for (const name of names) {
24093
- const skillPath = (0, import_node_path6.join)(dirPath, name, "SKILL.md");
25251
+ const skillPath = (0, import_node_path7.join)(dirPath, name, "SKILL.md");
24094
25252
  let isDirectory = false;
24095
25253
  try {
24096
- isDirectory = (0, import_node_fs7.statSync)((0, import_node_path6.join)(dirPath, name)).isDirectory();
25254
+ isDirectory = (0, import_node_fs8.statSync)((0, import_node_path7.join)(dirPath, name)).isDirectory();
24097
25255
  } catch {
24098
25256
  continue;
24099
25257
  }
24100
- if (!isDirectory || !(0, import_node_fs7.existsSync)(skillPath)) continue;
25258
+ if (!isDirectory || !(0, import_node_fs8.existsSync)(skillPath)) continue;
24101
25259
  try {
24102
25260
  entries.push({ content: readSkillHead(skillPath), source, sourcePath: skillPath, repoRoot });
24103
25261
  } catch (error) {
@@ -24108,54 +25266,194 @@ function scanSkillDir(dirPath, source, repoRoot, entries, errors) {
24108
25266
  function discoverChildRepos(parentPath) {
24109
25267
  let names;
24110
25268
  try {
24111
- names = (0, import_node_fs7.readdirSync)(parentPath);
25269
+ names = (0, import_node_fs8.readdirSync)(parentPath);
24112
25270
  } catch {
24113
25271
  return [];
24114
25272
  }
24115
- const children = [];
24116
- for (const name of names) {
24117
- if (name.startsWith(".")) continue;
24118
- const childPath = (0, import_node_path6.join)(parentPath, name);
24119
- try {
24120
- if ((0, import_node_fs7.statSync)(childPath).isDirectory()) children.push(childPath);
24121
- } catch {
24122
- }
25273
+ const children = [];
25274
+ for (const name of names) {
25275
+ if (name.startsWith(".")) continue;
25276
+ const childPath = (0, import_node_path7.join)(parentPath, name);
25277
+ try {
25278
+ if ((0, import_node_fs8.statSync)(childPath).isDirectory()) children.push(childPath);
25279
+ } catch {
25280
+ }
25281
+ }
25282
+ return children;
25283
+ }
25284
+ function scanLocalSkills(homeDir, projectPath) {
25285
+ const rawEntries = [];
25286
+ const errors = [];
25287
+ for (const root of SKILL_DIRECTORY_ROOTS) {
25288
+ scanSkillDir((0, import_node_path7.join)(homeDir, root), "global", void 0, rawEntries, errors);
25289
+ }
25290
+ if (projectPath) {
25291
+ for (const dir of [projectPath, ...discoverChildRepos(projectPath)]) {
25292
+ for (const root of SKILL_DIRECTORY_ROOTS) {
25293
+ scanSkillDir((0, import_node_path7.join)(dir, root), "team", dir, rawEntries, errors);
25294
+ }
25295
+ }
25296
+ }
25297
+ return { skills: normalizeSkillEntries(rawEntries, "team"), errors };
25298
+ }
25299
+ var skillScanCache = /* @__PURE__ */ new Map();
25300
+ function scanLocalSkillsCached(homeDir, projectPath, options) {
25301
+ const key = projectPath ?? "";
25302
+ const now = Date.now();
25303
+ if (!options?.refresh) {
25304
+ const cached = skillScanCache.get(key);
25305
+ if (cached && cached.expiresAt > now) return cached.result;
25306
+ }
25307
+ const result = scanLocalSkills(homeDir, projectPath);
25308
+ skillScanCache.set(key, { expiresAt: now + SKILL_CACHE_TTL_MS, result });
25309
+ return result;
25310
+ }
25311
+
25312
+ // src/local-workspace-migration.ts
25313
+ var import_node_crypto3 = require("crypto");
25314
+ var MigrationStashApplyError = class extends Error {
25315
+ stash;
25316
+ stashPreserved = true;
25317
+ constructor(stash, cause) {
25318
+ super(`Could not apply migration stash ${stash.oid}; the stash was preserved`, { cause });
25319
+ this.name = "MigrationStashApplyError";
25320
+ this.stash = stash;
25321
+ }
25322
+ };
25323
+ var BRANCH_LABEL_MAX_LENGTH = 256;
25324
+ function stripControlCharacters(value2) {
25325
+ return Array.from(value2).filter((character) => {
25326
+ const code = character.charCodeAt(0);
25327
+ return code >= 32 && code !== 127;
25328
+ }).join("");
25329
+ }
25330
+ function sanitizeLabel(value2, fallback) {
25331
+ const sanitized = stripControlCharacters(value2).replace(/[^a-zA-Z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, BRANCH_LABEL_MAX_LENGTH);
25332
+ return sanitized || fallback;
25333
+ }
25334
+ function sanitizeBranch(value2) {
25335
+ const sanitized = stripControlCharacters(value2.trim()).slice(0, BRANCH_LABEL_MAX_LENGTH);
25336
+ return sanitized || null;
25337
+ }
25338
+ function countPorcelainEntries(output) {
25339
+ const records = output.split("\0");
25340
+ let count = 0;
25341
+ for (let index = 0; index < records.length; index += 1) {
25342
+ const record = records[index];
25343
+ if (!record) continue;
25344
+ count += 1;
25345
+ const status = record.slice(0, 2);
25346
+ if (status.includes("R") || status.includes("C")) {
25347
+ index += 1;
25348
+ }
25349
+ }
25350
+ return count;
25351
+ }
25352
+ function preflightGitTree(cwd, git2) {
25353
+ const currentBranch = sanitizeBranch(git2(cwd, ["branch", "--show-current"]));
25354
+ const dirtyFileCount = countPorcelainEntries(
25355
+ git2(cwd, ["status", "--porcelain=v1", "-z", "--untracked-files=all"])
25356
+ );
25357
+ return {
25358
+ currentBranch,
25359
+ dirtyFileCount,
25360
+ isDirty: dirtyFileCount > 0
25361
+ };
25362
+ }
25363
+ function preflightWorkspaceMigration(input, git2) {
25364
+ return {
25365
+ direction: input.direction,
25366
+ source: preflightGitTree(input.sourcePath, git2),
25367
+ destination: input.destinationPath ? preflightGitTree(input.destinationPath, git2) : null
25368
+ };
25369
+ }
25370
+ function uniqueOperationLabel(operation, dependencies) {
25371
+ const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
25372
+ const createdAt = now.toISOString();
25373
+ const id = (dependencies.createId ?? import_node_crypto3.randomUUID)();
25374
+ return {
25375
+ label: [
25376
+ "alan-migration",
25377
+ sanitizeLabel(operation, "workspace"),
25378
+ createdAt.replace(/[:.]/g, "-"),
25379
+ sanitizeLabel(id, "operation")
25380
+ ].join("/"),
25381
+ createdAt
25382
+ };
25383
+ }
25384
+ function createMigrationStash(input, dependencies) {
25385
+ const before = preflightGitTree(input.cwd, dependencies.git);
25386
+ if (!before.isDirty) return null;
25387
+ const { label, createdAt } = uniqueOperationLabel(input.operation, dependencies);
25388
+ dependencies.git(input.cwd, ["stash", "push", "--include-untracked", "--message", label]);
25389
+ const oid = dependencies.git(input.cwd, ["rev-parse", "--verify", "refs/stash"]).trim();
25390
+ if (!oid) {
25391
+ throw new Error("Git did not return the created migration stash");
25392
+ }
25393
+ return { oid, label, createdAt };
25394
+ }
25395
+ function findStashRef(cwd, oid, git2) {
25396
+ const matchingLine = git2(cwd, ["stash", "list", "--format=%H %gd"]).split("\n").find((line) => line.startsWith(`${oid} `));
25397
+ return matchingLine?.slice(oid.length + 1).trim() || null;
25398
+ }
25399
+ function dropMigrationStash(cwd, stash, git2) {
25400
+ const stashRef = findStashRef(cwd, stash.oid, git2);
25401
+ if (!stashRef) return false;
25402
+ git2(cwd, ["stash", "drop", stashRef]);
25403
+ return true;
25404
+ }
25405
+ function applyMigrationStash(input, git2) {
25406
+ try {
25407
+ git2(input.cwd, ["stash", "apply", "--index", input.stash.oid]);
25408
+ } catch (cause) {
25409
+ throw new MigrationStashApplyError(input.stash, cause);
25410
+ }
25411
+ if (input.dropOnSuccess !== false) {
25412
+ dropMigrationStash(input.cwd, input.stash, git2);
24123
25413
  }
24124
- return children;
25414
+ return preflightGitTree(input.cwd, git2);
24125
25415
  }
24126
- function scanLocalSkills(homeDir, projectPath) {
24127
- const rawEntries = [];
24128
- const errors = [];
24129
- for (const root of SKILL_DIRECTORY_ROOTS) {
24130
- scanSkillDir((0, import_node_path6.join)(homeDir, root), "global", void 0, rawEntries, errors);
25416
+ function resolveDirtyTree(input, dependencies) {
25417
+ if (input.action === "discard" && input.confirmDiscard !== true) {
25418
+ throw new Error("Discarding workspace changes requires explicit confirmation");
24131
25419
  }
24132
- if (projectPath) {
24133
- for (const dir of [projectPath, ...discoverChildRepos(projectPath)]) {
24134
- for (const root of SKILL_DIRECTORY_ROOTS) {
24135
- scanSkillDir((0, import_node_path6.join)(dir, root), "team", dir, rawEntries, errors);
25420
+ const before = preflightGitTree(input.cwd, dependencies.git);
25421
+ let stash = null;
25422
+ let commitOid = null;
25423
+ if (before.isDirty) {
25424
+ switch (input.action) {
25425
+ case "leave":
25426
+ break;
25427
+ case "move_via_stash":
25428
+ stash = createMigrationStash({ cwd: input.cwd, operation: input.operation }, dependencies);
25429
+ break;
25430
+ case "commit_wip": {
25431
+ const { label } = uniqueOperationLabel(input.operation, dependencies);
25432
+ dependencies.git(input.cwd, ["add", "--all"]);
25433
+ dependencies.git(input.cwd, ["commit", "--message", `WIP: ${label}`]);
25434
+ commitOid = dependencies.git(input.cwd, ["rev-parse", "HEAD"]).trim() || null;
25435
+ break;
24136
25436
  }
25437
+ case "discard":
25438
+ dependencies.git(input.cwd, ["reset", "--hard", "HEAD"]);
25439
+ dependencies.git(input.cwd, ["clean", "-fd"]);
25440
+ break;
24137
25441
  }
24138
25442
  }
24139
- return { skills: normalizeSkillEntries(rawEntries, "team"), errors };
24140
- }
24141
- var skillScanCache = /* @__PURE__ */ new Map();
24142
- function scanLocalSkillsCached(homeDir, projectPath, options) {
24143
- const key = projectPath ?? "";
24144
- const now = Date.now();
24145
- if (!options?.refresh) {
24146
- const cached = skillScanCache.get(key);
24147
- if (cached && cached.expiresAt > now) return cached.result;
24148
- }
24149
- const result = scanLocalSkills(homeDir, projectPath);
24150
- skillScanCache.set(key, { expiresAt: now + SKILL_CACHE_TTL_MS, result });
24151
- return result;
25443
+ return {
25444
+ action: input.action,
25445
+ before,
25446
+ after: preflightGitTree(input.cwd, dependencies.git),
25447
+ stash,
25448
+ commitOid
25449
+ };
24152
25450
  }
24153
25451
 
24154
25452
  // src/mcp-registration.ts
24155
- var import_node_crypto2 = require("crypto");
24156
- var import_node_fs8 = require("fs");
24157
- var import_node_os7 = require("os");
24158
- var import_node_path7 = require("path");
25453
+ var import_node_crypto4 = require("crypto");
25454
+ var import_node_fs9 = require("fs");
25455
+ var import_node_os8 = require("os");
25456
+ var import_node_path8 = require("path");
24159
25457
 
24160
25458
  // ../../node_modules/smol-toml/dist/error.js
24161
25459
  function getLineColFromPtr(string, ptr) {
@@ -24237,7 +25535,7 @@ function skipVoid(str, ptr, banNewLines, banComments) {
24237
25535
  }
24238
25536
  return ptr;
24239
25537
  }
24240
- function skipUntil(str, ptr, sep2, end, banNewLines = false) {
25538
+ function skipUntil(str, ptr, sep3, end, banNewLines = false) {
24241
25539
  if (!end) {
24242
25540
  ptr = indexOfNewline(str, ptr);
24243
25541
  return ptr < 0 ? str.length : ptr;
@@ -24246,7 +25544,7 @@ function skipUntil(str, ptr, sep2, end, banNewLines = false) {
24246
25544
  let c = str[i];
24247
25545
  if (c === "#") {
24248
25546
  i = indexOfNewline(str, i);
24249
- } else if (c === sep2) {
25547
+ } else if (c === sep3) {
24250
25548
  return i + 1;
24251
25549
  } else if (c === end || banNewLines && (c === "\n" || c === "\r" && str[i + 1] === "\n")) {
24252
25550
  return i;
@@ -24952,18 +26250,18 @@ var inspectAlanMcpThroughProviderCli = async (backendKind, expectedUrl, home) =>
24952
26250
  return result;
24953
26251
  };
24954
26252
  async function waitForConfigLock(lockPath) {
24955
- (0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(lockPath), { recursive: true });
26253
+ (0, import_node_fs9.mkdirSync)((0, import_node_path8.dirname)(lockPath), { recursive: true });
24956
26254
  const startedAt = Date.now();
24957
26255
  while (true) {
24958
26256
  try {
24959
- (0, import_node_fs8.mkdirSync)(lockPath);
24960
- return () => (0, import_node_fs8.rmSync)(lockPath, { recursive: true, force: true });
26257
+ (0, import_node_fs9.mkdirSync)(lockPath);
26258
+ return () => (0, import_node_fs9.rmSync)(lockPath, { recursive: true, force: true });
24961
26259
  } catch (error) {
24962
26260
  const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
24963
26261
  if (code !== "EEXIST") throw error;
24964
26262
  try {
24965
- if (Date.now() - (0, import_node_fs8.statSync)(lockPath).mtimeMs > CONFIG_LOCK_STALE_MS) {
24966
- (0, import_node_fs8.rmSync)(lockPath, { recursive: true, force: true });
26263
+ if (Date.now() - (0, import_node_fs9.statSync)(lockPath).mtimeMs > CONFIG_LOCK_STALE_MS) {
26264
+ (0, import_node_fs9.rmSync)(lockPath, { recursive: true, force: true });
24967
26265
  continue;
24968
26266
  }
24969
26267
  } catch {
@@ -24972,14 +26270,14 @@ async function waitForConfigLock(lockPath) {
24972
26270
  if (Date.now() - startedAt >= CONFIG_LOCK_WAIT_MS) {
24973
26271
  throw new Error(`Alan could not safely update ${lockPath.slice(0, -".alan-lock".length)}`);
24974
26272
  }
24975
- await new Promise((resolve6) => setTimeout(resolve6, CONFIG_LOCK_POLL_MS));
26273
+ await new Promise((resolve7) => setTimeout(resolve7, CONFIG_LOCK_POLL_MS));
24976
26274
  }
24977
26275
  }
24978
26276
  }
24979
26277
  function saveMalformedBackup(path, content) {
24980
- const hash = (0, import_node_crypto2.createHash)("sha256").update(content).digest("hex").slice(0, 12);
26278
+ const hash = (0, import_node_crypto4.createHash)("sha256").update(content).digest("hex").slice(0, 12);
24981
26279
  const backupPath = `${path}.alan-backup-${hash}`;
24982
- if (!(0, import_node_fs8.existsSync)(backupPath)) (0, import_node_fs8.copyFileSync)(path, backupPath);
26280
+ if (!(0, import_node_fs9.existsSync)(backupPath)) (0, import_node_fs9.copyFileSync)(path, backupPath);
24983
26281
  return backupPath;
24984
26282
  }
24985
26283
  function parseJsonObject2(path, content) {
@@ -25024,24 +26322,24 @@ function mergeCodexAlanSection(path, existingContent, desiredContent) {
25024
26322
  return merged;
25025
26323
  }
25026
26324
  function atomicWriteManagedConfig(path, content) {
25027
- const pendingPath = `${path}.alan-pending-${process.pid}-${(0, import_node_crypto2.randomBytes)(6).toString("hex")}`;
26325
+ const pendingPath = `${path}.alan-pending-${process.pid}-${(0, import_node_crypto4.randomBytes)(6).toString("hex")}`;
25028
26326
  try {
25029
- (0, import_node_fs8.writeFileSync)(pendingPath, content, { encoding: "utf8", mode: 384, flag: "wx" });
25030
- (0, import_node_fs8.renameSync)(pendingPath, path);
26327
+ (0, import_node_fs9.writeFileSync)(pendingPath, content, { encoding: "utf8", mode: 384, flag: "wx" });
26328
+ (0, import_node_fs9.renameSync)(pendingPath, path);
25031
26329
  } finally {
25032
- (0, import_node_fs8.rmSync)(pendingPath, { force: true });
26330
+ (0, import_node_fs9.rmSync)(pendingPath, { force: true });
25033
26331
  }
25034
26332
  }
25035
- async function ensureAlanMcpRegistered(backendKind, registration, home = (0, import_node_os7.homedir)(), inspectCli = inspectAlanMcpThroughProviderCli) {
26333
+ async function ensureAlanMcpRegistered(backendKind, registration, home = (0, import_node_os8.homedir)(), inspectCli = inspectAlanMcpThroughProviderCli) {
25036
26334
  try {
25037
26335
  const mcpServers = buildAlanMcpServers(registration);
25038
26336
  const files = alanMcpConfigFilesForBackend(backendKind, mcpServers, home);
25039
26337
  const paths = [];
25040
26338
  for (const file of files) {
25041
- (0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(file.path), { recursive: true });
26339
+ (0, import_node_fs9.mkdirSync)((0, import_node_path8.dirname)(file.path), { recursive: true });
25042
26340
  const releaseLock = await waitForConfigLock(`${file.path}.alan-lock`);
25043
26341
  try {
25044
- const current = (0, import_node_fs8.existsSync)(file.path) ? (0, import_node_fs8.readFileSync)(file.path, "utf8") : null;
26342
+ const current = (0, import_node_fs9.existsSync)(file.path) ? (0, import_node_fs9.readFileSync)(file.path, "utf8") : null;
25045
26343
  const content = file.path.endsWith(".toml") ? mergeCodexAlanSection(file.path, current, file.content) : mergeJsonConfig(file.path, current, file.content);
25046
26344
  if (current !== content) atomicWriteManagedConfig(file.path, content);
25047
26345
  paths.push(file.path);
@@ -25092,13 +26390,13 @@ async function ensureAlanMcpRegistered(backendKind, registration, home = (0, imp
25092
26390
  return { ok: false, paths: [], error: err instanceof Error ? err.message : String(err) };
25093
26391
  }
25094
26392
  }
25095
- async function verifyAlanMcpRegistered(backendKind, home = (0, import_node_os7.homedir)(), inspectCli = inspectAlanMcpThroughProviderCli) {
26393
+ async function verifyAlanMcpRegistered(backendKind, home = (0, import_node_os8.homedir)(), inspectCli = inspectAlanMcpThroughProviderCli) {
25096
26394
  const files = alanMcpConfigFilesForBackend(backendKind, {}, home);
25097
- const missing = files.map((f) => f.path).filter((p) => !(0, import_node_fs8.existsSync)(p));
26395
+ const missing = files.map((f) => f.path).filter((p) => !(0, import_node_fs9.existsSync)(p));
25098
26396
  const presentEntries = files.map((file) => ({
25099
26397
  path: file.path,
25100
- url: (0, import_node_fs8.existsSync)(file.path) ? readUsableAlanMcpUrl(file.path) : null
25101
- })).filter((entry) => (0, import_node_fs8.existsSync)(entry.path));
26398
+ url: (0, import_node_fs9.existsSync)(file.path) ? readUsableAlanMcpUrl(file.path) : null
26399
+ })).filter((entry) => (0, import_node_fs9.existsSync)(entry.path));
25102
26400
  let invalid = presentEntries.filter((entry) => !entry.url).map((entry) => entry.path);
25103
26401
  const urls = new Set(presentEntries.flatMap((entry) => entry.url ? [entry.url] : []));
25104
26402
  if (urls.size > 1) invalid = presentEntries.map((entry) => entry.path);
@@ -25154,15 +26452,15 @@ function removeAlanFromJsonConfig(path, existingContent) {
25154
26452
  }
25155
26453
  return JSON.stringify(config);
25156
26454
  }
25157
- async function unregisterAlanMcp(backendKind, home = (0, import_node_os7.homedir)()) {
26455
+ async function unregisterAlanMcp(backendKind, home = (0, import_node_os8.homedir)()) {
25158
26456
  const removedPaths = [];
25159
26457
  const errors = [];
25160
26458
  const files = alanMcpConfigFilesForBackend(backendKind, {}, home);
25161
26459
  for (const file of files) {
25162
- if (!(0, import_node_fs8.existsSync)(file.path)) continue;
26460
+ if (!(0, import_node_fs9.existsSync)(file.path)) continue;
25163
26461
  const releaseLock = await waitForConfigLock(`${file.path}.alan-lock`);
25164
26462
  try {
25165
- const current = (0, import_node_fs8.readFileSync)(file.path, "utf8");
26463
+ const current = (0, import_node_fs9.readFileSync)(file.path, "utf8");
25166
26464
  let next;
25167
26465
  if (file.path.endsWith(".toml")) {
25168
26466
  const stripped = stripCodexAlanMcpSection(current);
@@ -25193,7 +26491,7 @@ function isHttpUrl(value2) {
25193
26491
  }
25194
26492
  }
25195
26493
  function readUsableAlanMcpUrl(path) {
25196
- const content = (0, import_node_fs8.readFileSync)(path, "utf8");
26494
+ const content = (0, import_node_fs9.readFileSync)(path, "utf8");
25197
26495
  if (path.endsWith(".toml")) {
25198
26496
  const lines = content.split("\n");
25199
26497
  const start = lines.findIndex((line) => line.trim() === "[mcp_servers.alan]");
@@ -25484,7 +26782,7 @@ ${result.stderr}`.trim();
25484
26782
  }
25485
26783
 
25486
26784
  // src/provider-model-discovery.ts
25487
- var import_node_fs9 = require("fs");
26785
+ var import_node_fs10 = require("fs");
25488
26786
  var MODEL_DISCOVERY_TIMEOUT_MS = 8e3;
25489
26787
  var ANTIGRAVITY_MODEL_DISCOVERY_TIMEOUT_MS = 12e3;
25490
26788
  var OPENCODE_ZEN_MODEL_IDS = OPENCODE_ZEN_MODELS.map((model) => model.id);
@@ -25698,7 +26996,7 @@ function filterClaudeDatedModelIds(ids) {
25698
26996
  }
25699
26997
  function resolveExecutableForModelScan(executable) {
25700
26998
  try {
25701
- return (0, import_node_fs9.realpathSync)(executable);
26999
+ return (0, import_node_fs10.realpathSync)(executable);
25702
27000
  } catch {
25703
27001
  return executable;
25704
27002
  }
@@ -25706,7 +27004,7 @@ function resolveExecutableForModelScan(executable) {
25706
27004
  function extractClaudeModelIdsFromExecutable(executable) {
25707
27005
  try {
25708
27006
  const resolved = resolveExecutableForModelScan(executable);
25709
- const text = (0, import_node_fs9.readFileSync)(resolved).toString("latin1");
27007
+ const text = (0, import_node_fs10.readFileSync)(resolved).toString("latin1");
25710
27008
  const matches = text.match(CLAUDE_MODEL_ID_PATTERN) ?? [];
25711
27009
  const unique = [
25712
27010
  ...new Set(matches.map((id) => id.trim()).filter((id) => isValidClaudeModelId(id))),
@@ -25833,8 +27131,8 @@ async function discoverProviderModels(provider, executable, env) {
25833
27131
  }
25834
27132
 
25835
27133
  // src/run-journal.ts
25836
- var import_node_fs10 = require("fs");
25837
- var import_node_path8 = require("path");
27134
+ var import_node_fs11 = require("fs");
27135
+ var import_node_path9 = require("path");
25838
27136
  var JOURNAL_VERSION = 1;
25839
27137
  var INTERRUPTED_ENTRY_TTL_MS = 24 * 60 * 6e4;
25840
27138
  function isEntry(value2) {
@@ -25853,9 +27151,9 @@ function isEntry(value2) {
25853
27151
  var RuntimeRunJournal = class {
25854
27152
  constructor(path, nowMs = Date.now()) {
25855
27153
  this.path = path;
25856
- if (!(0, import_node_fs10.existsSync)(path)) return;
27154
+ if (!(0, import_node_fs11.existsSync)(path)) return;
25857
27155
  try {
25858
- const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
27156
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
25859
27157
  if (parsed.version !== JOURNAL_VERSION || !Array.isArray(parsed.entries)) {
25860
27158
  throw new Error("unsupported run journal format");
25861
27159
  }
@@ -25978,18 +27276,18 @@ var RuntimeRunJournal = class {
25978
27276
  }
25979
27277
  persist() {
25980
27278
  const pendingPath = `${this.path}.pending`;
25981
- (0, import_node_fs10.mkdirSync)((0, import_node_path8.dirname)(this.path), { recursive: true });
27279
+ (0, import_node_fs11.mkdirSync)((0, import_node_path9.dirname)(this.path), { recursive: true });
25982
27280
  try {
25983
- (0, import_node_fs10.writeFileSync)(
27281
+ (0, import_node_fs11.writeFileSync)(
25984
27282
  pendingPath,
25985
27283
  `${JSON.stringify({ version: JOURNAL_VERSION, entries: [...this.entries.values()] })}
25986
27284
  `,
25987
27285
  { mode: 384 }
25988
27286
  );
25989
- (0, import_node_fs10.chmodSync)(pendingPath, 384);
25990
- (0, import_node_fs10.renameSync)(pendingPath, this.path);
27287
+ (0, import_node_fs11.chmodSync)(pendingPath, 384);
27288
+ (0, import_node_fs11.renameSync)(pendingPath, this.path);
25991
27289
  } catch (error) {
25992
- (0, import_node_fs10.rmSync)(pendingPath, { force: true });
27290
+ (0, import_node_fs11.rmSync)(pendingPath, { force: true });
25993
27291
  throw error;
25994
27292
  }
25995
27293
  }
@@ -26001,8 +27299,8 @@ var RunStartGate = class {
26001
27299
  async acquire() {
26002
27300
  const previous = this.tail;
26003
27301
  let releaseCurrent;
26004
- const current = new Promise((resolve6) => {
26005
- releaseCurrent = resolve6;
27302
+ const current = new Promise((resolve7) => {
27303
+ releaseCurrent = resolve7;
26006
27304
  });
26007
27305
  this.tail = previous.then(() => current);
26008
27306
  await previous;
@@ -26016,17 +27314,17 @@ var RunStartGate = class {
26016
27314
  };
26017
27315
 
26018
27316
  // src/version.ts
26019
- var AGENT_VERSION = "0.1.49";
27317
+ var AGENT_VERSION = "0.1.51";
26020
27318
 
26021
27319
  // src/workspace-relocation.ts
26022
- var import_node_child_process4 = require("child_process");
26023
- var import_node_fs11 = require("fs");
26024
- var import_node_path9 = require("path");
27320
+ var import_node_child_process5 = require("child_process");
27321
+ var import_node_fs12 = require("fs");
27322
+ var import_node_path10 = require("path");
26025
27323
  var MAX_RELOCATION_CANDIDATES = 200;
26026
27324
  var DEFAULT_PROBE = {
26027
- 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)),
27325
+ 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)),
26028
27326
  gitRemoteUrl: (candidatePath) => {
26029
- const result = (0, import_node_child_process4.spawnSync)("git", ["-C", candidatePath, "config", "--get", "remote.origin.url"], {
27327
+ const result = (0, import_node_child_process5.spawnSync)("git", ["-C", candidatePath, "config", "--get", "remote.origin.url"], {
26030
27328
  encoding: "utf8",
26031
27329
  env: getDaemonCliEnvironment(),
26032
27330
  timeout: 2e3
@@ -26055,7 +27353,7 @@ function workspaceMatchesExpectedRepositories(input) {
26055
27353
  input.expectedRepoUrls.map(normalizeRepositoryIdentity).filter((identity2) => Boolean(identity2))
26056
27354
  );
26057
27355
  if (identities.size === 0) return false;
26058
- const remote = (input.probe ?? DEFAULT_PROBE).gitRemoteUrl((0, import_node_path9.resolve)(input.workspacePath));
27356
+ const remote = (input.probe ?? DEFAULT_PROBE).gitRemoteUrl((0, import_node_path10.resolve)(input.workspacePath));
26059
27357
  const identity = remote ? normalizeRepositoryIdentity(remote) : null;
26060
27358
  return Boolean(identity && identities.has(identity));
26061
27359
  }
@@ -26065,8 +27363,8 @@ function discoverRelocatedWorkspace(input) {
26065
27363
  );
26066
27364
  if (identities.size === 0) return null;
26067
27365
  const searchRoots = [
26068
- (0, import_node_path9.dirname)((0, import_node_path9.resolve)(input.missingPath)),
26069
- ...(input.approvedWorkspacePaths ?? []).map((path) => (0, import_node_path9.dirname)((0, import_node_path9.resolve)(path)))
27366
+ (0, import_node_path10.dirname)((0, import_node_path10.resolve)(input.missingPath)),
27367
+ ...(input.approvedWorkspacePaths ?? []).map((path) => (0, import_node_path10.dirname)((0, import_node_path10.resolve)(path)))
26070
27368
  ].filter((path, index, paths) => paths.indexOf(path) === index);
26071
27369
  const candidates = [];
26072
27370
  for (const root of searchRoots) {
@@ -26078,19 +27376,19 @@ function discoverRelocatedWorkspace(input) {
26078
27376
  }
26079
27377
  }
26080
27378
  const matches = [];
26081
- for (const candidate of new Set(candidates.map((path) => (0, import_node_path9.resolve)(path)))) {
27379
+ for (const candidate of new Set(candidates.map((path) => (0, import_node_path10.resolve)(path)))) {
26082
27380
  const remote = (input.probe ?? DEFAULT_PROBE).gitRemoteUrl(candidate);
26083
27381
  const identity = remote ? normalizeRepositoryIdentity(remote) : null;
26084
- if (identity && identities.has(identity)) matches.push((0, import_node_path9.resolve)(candidate));
27382
+ if (identity && identities.has(identity)) matches.push((0, import_node_path10.resolve)(candidate));
26085
27383
  if (matches.length > 1) return null;
26086
27384
  }
26087
27385
  return matches[0] ?? null;
26088
27386
  }
26089
27387
 
26090
27388
  // src/workspace-run-lease.ts
26091
- var import_node_child_process5 = require("child_process");
26092
- var import_node_fs12 = require("fs");
26093
- var import_node_path10 = require("path");
27389
+ var import_node_child_process6 = require("child_process");
27390
+ var import_node_fs13 = require("fs");
27391
+ var import_node_path11 = require("path");
26094
27392
  var WORKSPACE_LEASE_CHECK_INTERVAL_MS = 1e3;
26095
27393
  function parsePositiveMs(value2, fallback) {
26096
27394
  const parsed = value2 ? Number(value2) : Number.NaN;
@@ -26101,7 +27399,7 @@ var WORKSPACE_LEASE_GIT_TIMEOUT_MS = parsePositiveMs(
26101
27399
  2e3
26102
27400
  );
26103
27401
  function gitValue(cwd, args) {
26104
- const result = (0, import_node_child_process5.spawnSync)("git", ["-C", cwd, ...args], {
27402
+ const result = (0, import_node_child_process6.spawnSync)("git", ["-C", cwd, ...args], {
26105
27403
  encoding: "utf8",
26106
27404
  env: getDaemonCliEnvironment(),
26107
27405
  timeout: WORKSPACE_LEASE_GIT_TIMEOUT_MS
@@ -26110,17 +27408,17 @@ function gitValue(cwd, args) {
26110
27408
  return result.stdout.trim() || null;
26111
27409
  }
26112
27410
  function filesystemIdentity(path) {
26113
- const canonicalPath = import_node_fs12.realpathSync.native(path);
26114
- const stat = (0, import_node_fs12.statSync)(canonicalPath);
27411
+ const canonicalPath = import_node_fs13.realpathSync.native(path);
27412
+ const stat = (0, import_node_fs13.statSync)(canonicalPath);
26115
27413
  return `${canonicalPath}:${stat.dev}:${stat.ino}`;
26116
27414
  }
26117
27415
  function resolveGitPath(workspacePath, value2) {
26118
- return (0, import_node_path10.isAbsolute)(value2) ? value2 : (0, import_node_path10.resolve)(workspacePath, value2);
27416
+ return (0, import_node_path11.isAbsolute)(value2) ? value2 : (0, import_node_path11.resolve)(workspacePath, value2);
26119
27417
  }
26120
27418
  var DEFAULT_PROBE2 = {
26121
27419
  snapshot: (workspacePath) => {
26122
27420
  try {
26123
- const canonicalWorkspacePath = import_node_fs12.realpathSync.native((0, import_node_path10.resolve)(workspacePath));
27421
+ const canonicalWorkspacePath = import_node_fs13.realpathSync.native((0, import_node_path11.resolve)(workspacePath));
26124
27422
  const repositoryRoot = gitValue(canonicalWorkspacePath, ["rev-parse", "--show-toplevel"]);
26125
27423
  const gitDirectory = gitValue(canonicalWorkspacePath, ["rev-parse", "--git-dir"]);
26126
27424
  const commonDirectory = gitValue(canonicalWorkspacePath, ["rev-parse", "--git-common-dir"]);
@@ -26129,7 +27427,7 @@ var DEFAULT_PROBE2 = {
26129
27427
  const remote = gitValue(canonicalWorkspacePath, ["config", "--get", "remote.origin.url"]);
26130
27428
  const remoteIdentity = remote ? normalizeRepositoryIdentity(remote) : null;
26131
27429
  return {
26132
- workspacePath: (0, import_node_path10.resolve)(workspacePath),
27430
+ workspacePath: (0, import_node_path11.resolve)(workspacePath),
26133
27431
  workspaceIdentity: filesystemIdentity(canonicalWorkspacePath),
26134
27432
  repositoryIdentity: `${filesystemIdentity(
26135
27433
  resolveGitPath(canonicalWorkspacePath, commonDirectory)
@@ -26143,7 +27441,7 @@ var DEFAULT_PROBE2 = {
26143
27441
  },
26144
27442
  pathExists: (workspacePath) => {
26145
27443
  try {
26146
- return (0, import_node_fs12.existsSync)((0, import_node_path10.resolve)(workspacePath));
27444
+ return (0, import_node_fs13.existsSync)((0, import_node_path11.resolve)(workspacePath));
26147
27445
  } catch {
26148
27446
  return true;
26149
27447
  }
@@ -26174,7 +27472,7 @@ var WorkspaceRunLease = class _WorkspaceRunLease {
26174
27472
  static capture(targets, probe = DEFAULT_PROBE2) {
26175
27473
  const captured = [];
26176
27474
  for (const target of targets) {
26177
- const normalizedTarget = { ...target, workspacePath: (0, import_node_path10.resolve)(target.workspacePath) };
27475
+ const normalizedTarget = { ...target, workspacePath: (0, import_node_path11.resolve)(target.workspacePath) };
26178
27476
  const identity = probe.snapshot(normalizedTarget.workspacePath);
26179
27477
  if (!identity) return { lease: null, failure: unavailableFailure(normalizedTarget) };
26180
27478
  if (normalizedTarget.expectedBranch && identity.branch !== normalizedTarget.expectedBranch) {
@@ -26316,34 +27614,62 @@ var PRODUCTION_API_URL = "https://api.tryalan.ai";
26316
27614
  var PRODUCTION_WS_URL = "wss://ws.tryalan.ai";
26317
27615
  var LOCAL_API_URL = "http://localhost:8400";
26318
27616
  var LOCAL_WS_URL = "ws://localhost:8401";
26319
- function requiredString(params, key) {
27617
+ function requiredString2(params, key) {
26320
27618
  const value2 = params[key];
26321
27619
  if (typeof value2 !== "string" || value2.trim() === "") {
26322
27620
  throw new Error(`${key} is required`);
26323
27621
  }
26324
27622
  return value2;
26325
27623
  }
27624
+ var DIRTY_WORKSPACE_ACTIONS = /* @__PURE__ */ new Set([
27625
+ "move_via_stash",
27626
+ "commit_wip",
27627
+ "discard",
27628
+ "leave"
27629
+ ]);
27630
+ function parseDirtyWorkspaceResolution(value2) {
27631
+ if (value2 === void 0) return void 0;
27632
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) {
27633
+ throw new Error("dirtyResolution must be an object");
27634
+ }
27635
+ const raw = value2;
27636
+ const parseAction = (key) => {
27637
+ const action = raw[key];
27638
+ if (action === void 0) return void 0;
27639
+ if (typeof action !== "string" || !DIRTY_WORKSPACE_ACTIONS.has(action)) {
27640
+ throw new Error(`${key} is not a supported dirty workspace action`);
27641
+ }
27642
+ return action;
27643
+ };
27644
+ return {
27645
+ sourceAction: parseAction("sourceAction"),
27646
+ destinationAction: parseAction("destinationAction"),
27647
+ sourceCommitMessage: typeof raw.sourceCommitMessage === "string" ? raw.sourceCommitMessage : void 0,
27648
+ destinationCommitMessage: typeof raw.destinationCommitMessage === "string" ? raw.destinationCommitMessage : void 0,
27649
+ confirmDiscard: raw.confirmDiscard === true
27650
+ };
27651
+ }
26326
27652
  function assertValidGitRef(ref, label) {
26327
27653
  if (!ref || ref.startsWith("-") || ref.includes("\0")) {
26328
27654
  throw new Error(`${label} is not a safe git ref`);
26329
27655
  }
26330
- const result = (0, import_node_child_process6.spawnSync)("git", ["check-ref-format", "--branch", ref], {
27656
+ const result = (0, import_node_child_process7.spawnSync)("git", ["check-ref-format", "--branch", ref], {
26331
27657
  env: getDaemonCliEnvironment(),
26332
27658
  encoding: "utf8"
26333
27659
  });
26334
27660
  if (result.status !== 0) throw new Error(`${label} is not a valid git ref`);
26335
27661
  }
26336
- function isPathContained(childPath, parentPath) {
26337
- const rel = (0, import_node_path11.relative)((0, import_node_path11.resolve)(parentPath), (0, import_node_path11.resolve)(childPath));
26338
- return rel === "" || !(0, import_node_path11.isAbsolute)(rel) && rel !== ".." && !rel.startsWith(`..${import_node_path11.sep}`);
27662
+ function isPathContained2(childPath, parentPath) {
27663
+ const rel = (0, import_node_path12.relative)((0, import_node_path12.resolve)(parentPath), (0, import_node_path12.resolve)(childPath));
27664
+ return rel === "" || !(0, import_node_path12.isAbsolute)(rel) && rel !== ".." && !rel.startsWith(`..${import_node_path12.sep}`);
26339
27665
  }
26340
27666
  function assertPathContained(childPath, parentPath, label) {
26341
- if (!isPathContained(childPath, parentPath)) {
27667
+ if (!isPathContained2(childPath, parentPath)) {
26342
27668
  throw new Error(`${label} escapes allowed directory`);
26343
27669
  }
26344
27670
  }
26345
27671
  function git(cwd, args) {
26346
- const result = (0, import_node_child_process6.spawnSync)("git", args, {
27672
+ const result = (0, import_node_child_process7.spawnSync)("git", args, {
26347
27673
  cwd,
26348
27674
  env: getDaemonCliEnvironment(),
26349
27675
  encoding: "utf8",
@@ -26362,14 +27688,23 @@ function gitSafe(cwd, args) {
26362
27688
  }
26363
27689
  }
26364
27690
  function worktreeDir(projectPath) {
26365
- const resolved = (0, import_node_path11.resolve)(projectPath);
26366
- return (0, import_node_path11.join)((0, import_node_path11.dirname)(resolved), `${(0, import_node_path11.basename)(resolved)}-alan-worktrees`);
27691
+ const resolved = (0, import_node_path12.resolve)(projectPath);
27692
+ return (0, import_node_path12.join)((0, import_node_path12.dirname)(resolved), `${(0, import_node_path12.basename)(resolved)}-alan-worktrees`);
27693
+ }
27694
+ function hasAlanWorktreeAncestor(targetPath) {
27695
+ let cursor = (0, import_node_path12.resolve)(targetPath);
27696
+ while (true) {
27697
+ if ((0, import_node_path12.basename)(cursor).endsWith("-alan-worktrees")) return true;
27698
+ const parent = (0, import_node_path12.dirname)(cursor);
27699
+ if (parent === cursor) return false;
27700
+ cursor = parent;
27701
+ }
26367
27702
  }
26368
27703
  function pathSegment(value2) {
26369
27704
  return value2.toLowerCase().replace(/^alan\//, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "task";
26370
27705
  }
26371
27706
  function repoName(repoUrl, localPath) {
26372
- return repoUrl.replace(/\.git$/, "").split("/").pop() || (0, import_node_path11.basename)(localPath) || "repo";
27707
+ return repoUrl.replace(/\.git$/, "").split("/").pop() || (0, import_node_path12.basename)(localPath) || "repo";
26373
27708
  }
26374
27709
  function findWorktreeByBranch(repoPath, branchName) {
26375
27710
  const output = gitSafe(repoPath, ["worktree", "list", "--porcelain"]);
@@ -26385,21 +27720,21 @@ function findWorktreeByBranch(repoPath, branchName) {
26385
27720
  function ensureGitWorktree(input) {
26386
27721
  assertValidGitRef(input.branchName, "branchName");
26387
27722
  assertValidGitRef(input.baseBranch, "baseBranch");
26388
- const repoPath = (0, import_node_path11.resolve)(input.repoPath);
27723
+ const repoPath = (0, import_node_path12.resolve)(input.repoPath);
26389
27724
  const root = input.allowedRoot ?? worktreeDir(repoPath);
26390
27725
  const existingPath = findWorktreeByBranch(input.repoPath, input.branchName);
26391
27726
  if (existingPath) {
26392
- if ((0, import_node_path11.resolve)(existingPath) === repoPath) {
27727
+ if ((0, import_node_path12.resolve)(existingPath) === repoPath) {
26393
27728
  throw new Error(`${input.branchName} is checked out in the shared checkout`);
26394
27729
  }
26395
27730
  return existingPath;
26396
27731
  }
26397
27732
  assertPathContained(input.worktreePath, root, "worktreePath");
26398
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(input.worktreePath), { recursive: true });
27733
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(input.worktreePath), { recursive: true });
26399
27734
  if (!gitSafe(input.repoPath, ["branch", "--list", input.branchName])) {
26400
27735
  git(input.repoPath, ["branch", input.branchName, input.baseBranch]);
26401
27736
  }
26402
- if ((0, import_node_fs13.existsSync)(input.worktreePath)) {
27737
+ if ((0, import_node_fs14.existsSync)(input.worktreePath)) {
26403
27738
  if (gitSafe(input.worktreePath, ["branch", "--show-current"]) === input.branchName) {
26404
27739
  return input.worktreePath;
26405
27740
  }
@@ -26409,40 +27744,172 @@ function ensureGitWorktree(input) {
26409
27744
  return input.worktreePath;
26410
27745
  }
26411
27746
  function resolveProjectWorktreePaths(projectPath, gitRoot, gitWorktreePath) {
26412
- const resolvedProjectPath = (0, import_node_path11.resolve)(projectPath);
26413
- const resolvedGitRoot = (0, import_node_path11.resolve)(gitRoot);
26414
- const projectRelativePath = (0, import_node_path11.relative)(resolvedGitRoot, resolvedProjectPath);
26415
- if (projectRelativePath === ".." || projectRelativePath.startsWith(`..${import_node_path11.sep}`) || (0, import_node_path11.isAbsolute)(projectRelativePath)) {
27747
+ const resolvedProjectPath = (0, import_node_path12.resolve)(projectPath);
27748
+ const resolvedGitRoot = (0, import_node_path12.resolve)(gitRoot);
27749
+ const projectRelativePath = (0, import_node_path12.relative)(resolvedGitRoot, resolvedProjectPath);
27750
+ if (projectRelativePath === ".." || projectRelativePath.startsWith(`..${import_node_path12.sep}`) || (0, import_node_path12.isAbsolute)(projectRelativePath)) {
26416
27751
  throw new Error("The selected Project folder is outside its Git repository");
26417
27752
  }
26418
27753
  return {
26419
27754
  repoPath: resolvedGitRoot,
26420
- projectPath: projectRelativePath ? (0, import_node_path11.join)((0, import_node_path11.resolve)(gitWorktreePath), projectRelativePath) : (0, import_node_path11.resolve)(gitWorktreePath)
27755
+ projectPath: projectRelativePath ? (0, import_node_path12.join)((0, import_node_path12.resolve)(gitWorktreePath), projectRelativePath) : (0, import_node_path12.resolve)(gitWorktreePath)
27756
+ };
27757
+ }
27758
+ function preflightLocalWorkspaceMigration(params) {
27759
+ const direction = params.direction;
27760
+ if (direction !== "to-checkout" && direction !== "to-worktree") {
27761
+ throw new Error("direction must be to-checkout or to-worktree");
27762
+ }
27763
+ const targets = Array.isArray(params.targets) ? params.targets : [];
27764
+ if (targets.length !== 1) {
27765
+ throw new Error("Dirty workspace migration currently supports exactly one repository");
27766
+ }
27767
+ const rawTarget = targets[0];
27768
+ const projectPath = (0, import_node_path12.resolve)(requiredString2(rawTarget, "projectPath"));
27769
+ const projectRootOutput = gitSafe(projectPath, ["rev-parse", "--show-toplevel"]);
27770
+ if (!projectRootOutput) {
27771
+ throw new Error("The selected Project folder is not inside a Git repository");
27772
+ }
27773
+ const projectRoot = (0, import_node_path12.resolve)(projectRootOutput);
27774
+ if (direction === "to-worktree") {
27775
+ const result2 = preflightWorkspaceMigration(
27776
+ {
27777
+ direction: "checkout_to_worktree",
27778
+ sourcePath: projectRoot
27779
+ },
27780
+ git
27781
+ );
27782
+ return {
27783
+ direction,
27784
+ source: result2.source,
27785
+ destination: null,
27786
+ requiresResolution: result2.source.isDirty
27787
+ };
27788
+ }
27789
+ const requestedWorktreePath = (0, import_node_path12.resolve)(requiredString2(rawTarget, "worktreePath"));
27790
+ const worktreeRootOutput = gitSafe(requestedWorktreePath, ["rev-parse", "--show-toplevel"]);
27791
+ if (!worktreeRootOutput) {
27792
+ throw new Error("The task worktree is not a registered Git worktree");
27793
+ }
27794
+ const result = preflightWorkspaceMigration(
27795
+ {
27796
+ direction: "worktree_to_checkout",
27797
+ sourcePath: (0, import_node_path12.resolve)(worktreeRootOutput),
27798
+ destinationPath: projectRoot
27799
+ },
27800
+ git
27801
+ );
27802
+ return {
27803
+ direction,
27804
+ source: result.source,
27805
+ destination: result.destination,
27806
+ requiresResolution: result.source.isDirty || Boolean(result.destination?.isDirty)
27807
+ };
27808
+ }
27809
+ function statusLocalWorkspace(params) {
27810
+ const requestedPath = (0, import_node_path12.resolve)(requiredString2(params, "worktreePath"));
27811
+ const rootOutput = gitSafe(requestedPath, ["rev-parse", "--show-toplevel"]);
27812
+ if (!rootOutput) {
27813
+ throw new Error("The selected folder is not inside a Git repository");
27814
+ }
27815
+ const root = (0, import_node_path12.resolve)(rootOutput);
27816
+ const tree = preflightGitTree(root, git);
27817
+ const headSha = gitSafe(root, ["rev-parse", "HEAD"]) || null;
27818
+ const upstream = gitSafe(root, ["rev-parse", "--abbrev-ref", "@{upstream}"]);
27819
+ const hasUpstream = upstream !== "";
27820
+ let ahead = null;
27821
+ let behind = null;
27822
+ if (hasUpstream) {
27823
+ const counts = gitSafe(root, ["rev-list", "--left-right", "--count", "@{upstream}...HEAD"]);
27824
+ const [behindRaw, aheadRaw] = counts.split(/\s+/);
27825
+ const parsedBehind = Number.parseInt(behindRaw ?? "", 10);
27826
+ const parsedAhead = Number.parseInt(aheadRaw ?? "", 10);
27827
+ behind = Number.isNaN(parsedBehind) ? null : parsedBehind;
27828
+ ahead = Number.isNaN(parsedAhead) ? null : parsedAhead;
27829
+ }
27830
+ return {
27831
+ branch: tree.currentBranch,
27832
+ headSha,
27833
+ dirty: tree.isDirty,
27834
+ filesChanged: tree.dirtyFileCount,
27835
+ ahead,
27836
+ behind,
27837
+ hasUpstream
26421
27838
  };
26422
27839
  }
27840
+ function ensureGitWorktreeWithDirtyResolution(input, resolution) {
27841
+ const repoPath = (0, import_node_path12.resolve)(input.repoPath);
27842
+ const before = preflightGitTree(repoPath, git);
27843
+ let sourceStash = null;
27844
+ if (before.isDirty) {
27845
+ const action = resolution?.sourceAction;
27846
+ if (!action) {
27847
+ throw new Error(
27848
+ `The Project checkout has ${before.dirtyFileCount} uncommitted file${before.dirtyFileCount === 1 ? "" : "s"}. Choose how to preserve them before moving into a worktree.`
27849
+ );
27850
+ }
27851
+ if (action === "commit_wip" && before.currentBranch !== input.baseBranch) {
27852
+ throw new Error(
27853
+ `Commit as WIP would stay on "${before.currentBranch ?? "the current checkout"}", not the selected base "${input.baseBranch}". Choose Move changes with task instead.`
27854
+ );
27855
+ }
27856
+ const resolved = resolveDirtyTree(
27857
+ {
27858
+ cwd: repoPath,
27859
+ action,
27860
+ operation: "checkout-to-worktree",
27861
+ confirmDiscard: resolution?.confirmDiscard
27862
+ },
27863
+ { git }
27864
+ );
27865
+ sourceStash = resolved.stash;
27866
+ }
27867
+ const existingPath = findWorktreeByBranch(repoPath, input.branchName);
27868
+ const worktreePath = ensureGitWorktree(input);
27869
+ if (!sourceStash) return worktreePath;
27870
+ try {
27871
+ applyMigrationStash({ cwd: worktreePath, stash: sourceStash }, git);
27872
+ return worktreePath;
27873
+ } catch (error) {
27874
+ if (!existingPath) {
27875
+ gitSafe(worktreePath, ["reset", "--hard", "HEAD"]);
27876
+ gitSafe(worktreePath, ["clean", "-fd"]);
27877
+ gitSafe(repoPath, ["worktree", "remove", worktreePath]);
27878
+ gitSafe(repoPath, ["worktree", "prune"]);
27879
+ }
27880
+ throw error;
27881
+ }
27882
+ }
26423
27883
  function ensureWorktree(params) {
26424
27884
  const input = {
26425
- projectPath: requiredString(params, "projectPath"),
26426
- taskId: requiredString(params, "taskId"),
26427
- branchName: requiredString(params, "branchName"),
27885
+ projectPath: requiredString2(params, "projectPath"),
27886
+ taskId: requiredString2(params, "taskId"),
27887
+ branchName: requiredString2(params, "branchName"),
26428
27888
  baseBranch: typeof params.baseBranch === "string" ? params.baseBranch : void 0,
26429
- repoLocalPaths: Array.isArray(params.repoLocalPaths) ? params.repoLocalPaths : []
27889
+ repoLocalPaths: Array.isArray(params.repoLocalPaths) ? params.repoLocalPaths : [],
27890
+ dirtyResolution: parseDirtyWorkspaceResolution(params.dirtyResolution)
26430
27891
  };
27892
+ if ((input.repoLocalPaths?.length ?? 0) > 1) {
27893
+ throw new Error("A task worktree requires exactly one Git repository.");
27894
+ }
26431
27895
  if (input.repoLocalPaths?.length) {
26432
- const taskRoot2 = (0, import_node_path11.join)(
27896
+ const taskRoot2 = (0, import_node_path12.join)(
26433
27897
  worktreeDir(input.projectPath),
26434
27898
  `${pathSegment(input.branchName)}-${input.taskId.slice(0, 8)}`
26435
27899
  );
26436
27900
  return {
26437
27901
  worktrees: input.repoLocalPaths.map((repo) => {
26438
27902
  const baseBranch2 = repo.branch || input.baseBranch || "main";
26439
- const worktreePath2 = ensureGitWorktree({
26440
- repoPath: repo.localPath,
26441
- worktreePath: (0, import_node_path11.join)(taskRoot2, pathSegment(repoName(repo.repoUrl, repo.localPath))),
26442
- branchName: input.branchName,
26443
- baseBranch: baseBranch2,
26444
- allowedRoot: worktreeDir(input.projectPath)
26445
- });
27903
+ const worktreePath2 = ensureGitWorktreeWithDirtyResolution(
27904
+ {
27905
+ repoPath: repo.localPath,
27906
+ worktreePath: (0, import_node_path12.join)(taskRoot2, pathSegment(repoName(repo.repoUrl, repo.localPath))),
27907
+ branchName: input.branchName,
27908
+ baseBranch: baseBranch2,
27909
+ allowedRoot: worktreeDir(input.projectPath)
27910
+ },
27911
+ input.dirtyResolution
27912
+ );
26446
27913
  return {
26447
27914
  repoId: repo.repoId ?? null,
26448
27915
  repoUrl: repo.repoUrl,
@@ -26450,26 +27917,30 @@ function ensureWorktree(params) {
26450
27917
  branchName: input.branchName,
26451
27918
  baseBranch: baseBranch2
26452
27919
  };
26453
- })
27920
+ }),
27921
+ ...input.dirtyResolution ? { dirtyResolution: input.dirtyResolution } : {}
26454
27922
  };
26455
27923
  }
26456
- const gitRoot = gitSafe((0, import_node_path11.resolve)(input.projectPath), ["rev-parse", "--show-toplevel"]);
27924
+ const gitRoot = gitSafe((0, import_node_path12.resolve)(input.projectPath), ["rev-parse", "--show-toplevel"]);
26457
27925
  if (!gitRoot) {
26458
27926
  throw new Error("The selected Project folder is not inside a Git repository");
26459
27927
  }
26460
- const repoPath = (0, import_node_path11.resolve)(gitRoot);
27928
+ const repoPath = (0, import_node_path12.resolve)(gitRoot);
26461
27929
  const baseBranch = input.baseBranch || gitSafe(repoPath, ["branch", "--show-current"]) || "HEAD";
26462
- const taskRoot = (0, import_node_path11.join)(
27930
+ const taskRoot = (0, import_node_path12.join)(
26463
27931
  worktreeDir(repoPath),
26464
27932
  `${pathSegment(input.branchName)}-${input.taskId.slice(0, 8)}`
26465
27933
  );
26466
27934
  resolveProjectWorktreePaths(input.projectPath, repoPath, taskRoot);
26467
- const worktreePath = ensureGitWorktree({
26468
- repoPath,
26469
- worktreePath: taskRoot,
26470
- branchName: input.branchName,
26471
- baseBranch
26472
- });
27935
+ const worktreePath = ensureGitWorktreeWithDirtyResolution(
27936
+ {
27937
+ repoPath,
27938
+ worktreePath: taskRoot,
27939
+ branchName: input.branchName,
27940
+ baseBranch
27941
+ },
27942
+ input.dirtyResolution
27943
+ );
26473
27944
  const projectWorktreePath = resolveProjectWorktreePaths(
26474
27945
  input.projectPath,
26475
27946
  repoPath,
@@ -26482,13 +27953,286 @@ function ensureWorktree(params) {
26482
27953
  branchName: input.branchName,
26483
27954
  baseBranch
26484
27955
  }
26485
- ]
27956
+ ],
27957
+ ...input.dirtyResolution ? { dirtyResolution: input.dirtyResolution } : {}
27958
+ };
27959
+ }
27960
+ function listRegisteredWorktrees(cwd) {
27961
+ return git(cwd, ["worktree", "list", "--porcelain"]).split("\n\n").flatMap((entry) => {
27962
+ const lines = entry.split("\n");
27963
+ const path = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
27964
+ if (!path) return [];
27965
+ return [
27966
+ {
27967
+ path: (0, import_node_path12.resolve)(path),
27968
+ branch: lines.find((line) => line.startsWith("branch "))?.slice("branch ".length).replace("refs/heads/", "")
27969
+ }
27970
+ ];
27971
+ });
27972
+ }
27973
+ function removeWorktreesIfClean(params, removeGit = git) {
27974
+ const projectPath = (0, import_node_path12.resolve)(requiredString2(params, "projectPath"));
27975
+ const targets = Array.isArray(params.targets) ? params.targets : [];
27976
+ if (targets.length === 0) throw new Error("At least one worktree target is required");
27977
+ const projectRootOutput = gitSafe(projectPath, ["rev-parse", "--show-toplevel"]);
27978
+ const projectRoot = projectRootOutput ? (0, import_node_path12.resolve)(projectRootOutput) : null;
27979
+ const projectRegistrations = projectRoot ? listRegisteredWorktrees(projectRoot) : [];
27980
+ const planned = targets.map((target) => {
27981
+ const rawTarget = target;
27982
+ const requestedPath = (0, import_node_path12.resolve)(requiredString2(rawTarget, "worktreePath"));
27983
+ const branchName = requiredString2(rawTarget, "branchName");
27984
+ assertValidGitRef(branchName, "branchName");
27985
+ const registrationSets = [];
27986
+ if (projectRegistrations.length > 0) registrationSets.push(projectRegistrations);
27987
+ if ((0, import_node_fs14.existsSync)(requestedPath)) {
27988
+ const requestedRoot = gitSafe(requestedPath, ["rev-parse", "--show-toplevel"]);
27989
+ if (!requestedRoot) {
27990
+ throw new Error(
27991
+ `The earlier task worktree is not a registered Git worktree: ${requestedPath}`
27992
+ );
27993
+ }
27994
+ const requestedRegistrations = listRegisteredWorktrees((0, import_node_path12.resolve)(requestedRoot));
27995
+ if (!registrationSets.some(
27996
+ (entries) => entries.length === requestedRegistrations.length && entries.every((entry, index) => entry.path === requestedRegistrations[index]?.path)
27997
+ )) {
27998
+ registrationSets.push(requestedRegistrations);
27999
+ }
28000
+ }
28001
+ for (const registrations of registrationSets) {
28002
+ const mainRoot = registrations[0]?.path;
28003
+ const registeredTarget = registrations.find(
28004
+ (entry) => requestedPath === entry.path || isPathContained2(requestedPath, entry.path)
28005
+ );
28006
+ if (!registeredTarget || !mainRoot) continue;
28007
+ if (registeredTarget.path === mainRoot) {
28008
+ throw new Error("Refusing to remove the Project's main checkout");
28009
+ }
28010
+ if (registeredTarget.branch !== branchName) {
28011
+ throw new Error(
28012
+ `The registered worktree branch changed from "${branchName}" to "${registeredTarget.branch ?? "detached HEAD"}".`
28013
+ );
28014
+ }
28015
+ const allowedRoots = [
28016
+ worktreeDir(mainRoot),
28017
+ worktreeDir(projectPath),
28018
+ ...projectRoot ? [worktreeDir(projectRoot)] : []
28019
+ ];
28020
+ if (!allowedRoots.some((root) => isPathContained2(registeredTarget.path, root))) {
28021
+ throw new Error("The registered worktree is outside an Alan-managed worktree directory");
28022
+ }
28023
+ if (git(registeredTarget.path, ["status", "--porcelain"]).trim()) {
28024
+ throw new Error(
28025
+ `The earlier task worktree has uncommitted changes at ${requestedPath}. Commit or discard them before changing Project, branch, or worktree mode.`
28026
+ );
28027
+ }
28028
+ return {
28029
+ requestedPath,
28030
+ worktreeRoot: registeredTarget.path,
28031
+ mainRoot,
28032
+ branchName,
28033
+ replayed: false
28034
+ };
28035
+ }
28036
+ if (!(0, import_node_fs14.existsSync)(requestedPath) && hasAlanWorktreeAncestor(requestedPath)) {
28037
+ return {
28038
+ requestedPath,
28039
+ worktreeRoot: null,
28040
+ mainRoot: null,
28041
+ branchName,
28042
+ replayed: true
28043
+ };
28044
+ }
28045
+ throw new Error(
28046
+ `The earlier task worktree is not safely registered and was not changed: ${requestedPath}`
28047
+ );
28048
+ });
28049
+ const removalRoots = planned.flatMap(
28050
+ (target) => target.worktreeRoot && target.mainRoot ? [{ worktreeRoot: target.worktreeRoot, mainRoot: target.mainRoot }] : []
28051
+ );
28052
+ if (new Set(removalRoots.map(({ worktreeRoot }) => worktreeRoot)).size !== removalRoots.length) {
28053
+ throw new Error("Duplicate worktree targets were not changed");
28054
+ }
28055
+ for (const target of removalRoots) {
28056
+ removeGit(target.mainRoot, ["worktree", "remove", target.worktreeRoot]);
28057
+ gitSafe(target.mainRoot, ["worktree", "prune"]);
28058
+ }
28059
+ return {
28060
+ ok: true,
28061
+ removed: planned.map(({ requestedPath, branchName, replayed }) => ({
28062
+ worktreePath: requestedPath,
28063
+ branchName,
28064
+ replayed
28065
+ }))
28066
+ };
28067
+ }
28068
+ function migrateWorktreeToCheckout(params, removeGit = git) {
28069
+ const dirtyResolution = parseDirtyWorkspaceResolution(params.dirtyResolution);
28070
+ const targets = Array.isArray(params.targets) ? params.targets : [];
28071
+ if (targets.length !== 1) {
28072
+ throw new Error(
28073
+ "Multi-repository worktree migration is not supported; no repositories were changed."
28074
+ );
28075
+ }
28076
+ const target = targets[0];
28077
+ const rawTarget = target;
28078
+ const projectPath = (0, import_node_path12.resolve)(requiredString2(rawTarget, "projectPath"));
28079
+ const requestedWorktreePath = (0, import_node_path12.resolve)(requiredString2(rawTarget, "worktreePath"));
28080
+ const branch = requiredString2(rawTarget, "branch");
28081
+ assertValidGitRef(branch, "branch");
28082
+ const projectRootOutput = gitSafe(projectPath, ["rev-parse", "--show-toplevel"]);
28083
+ if (!projectRootOutput) {
28084
+ throw new Error("The selected Project folder is not inside a Git repository");
28085
+ }
28086
+ const projectRoot = (0, import_node_path12.resolve)(projectRootOutput);
28087
+ const projectRelativePath = (0, import_node_path12.relative)(projectRoot, projectPath);
28088
+ if (projectRelativePath === ".." || projectRelativePath.startsWith(`..${import_node_path12.sep}`) || (0, import_node_path12.isAbsolute)(projectRelativePath)) {
28089
+ throw new Error("The selected Project folder is outside its Git repository");
28090
+ }
28091
+ const registered = git(projectRoot, ["worktree", "list", "--porcelain"]).split("\n\n").map((entry) => {
28092
+ const lines = entry.split("\n");
28093
+ return {
28094
+ path: lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length),
28095
+ branch: lines.find((line) => line.startsWith("branch "))?.slice("branch ".length).replace("refs/heads/", "")
28096
+ };
28097
+ });
28098
+ const registeredTarget = registered.find(({ path, branch: registeredBranch }) => {
28099
+ if (!path || registeredBranch !== branch) return false;
28100
+ const root = (0, import_node_path12.resolve)(path);
28101
+ return requestedWorktreePath === root || isPathContained2(requestedWorktreePath, root);
28102
+ });
28103
+ const currentBranch = gitSafe(projectRoot, ["branch", "--show-current"]);
28104
+ if (!registeredTarget?.path) {
28105
+ if (currentBranch === branch) {
28106
+ return {
28107
+ ok: true,
28108
+ replayed: true,
28109
+ migrated: [{ ...target, projectPath, worktreePath: requestedWorktreePath, branch }],
28110
+ ...dirtyResolution ? { dirtyResolution } : {}
28111
+ };
28112
+ }
28113
+ throw new Error(
28114
+ `The task worktree is no longer registered and the Project is not on branch "${branch}".`
28115
+ );
28116
+ }
28117
+ const worktreeRoot = (0, import_node_path12.resolve)(registeredTarget.path);
28118
+ const worktreeRelativePath = (0, import_node_path12.relative)(worktreeRoot, requestedWorktreePath);
28119
+ if (worktreeRelativePath !== projectRelativePath) {
28120
+ throw new Error(
28121
+ "The configured Project and task worktree do not point at the same repository subdirectory."
28122
+ );
28123
+ }
28124
+ const allowedRoots = [worktreeDir(projectRoot), worktreeDir(projectPath)];
28125
+ if (!allowedRoots.some((allowedRoot) => isPathContained2(worktreeRoot, allowedRoot))) {
28126
+ throw new Error("The task worktree escapes the Project worktree directory.");
28127
+ }
28128
+ if (!(0, import_node_fs14.existsSync)(worktreeRoot)) {
28129
+ throw new Error(`Worktree directory not found: ${worktreeRoot}`);
28130
+ }
28131
+ const sourceBefore = preflightGitTree(worktreeRoot, git);
28132
+ const destinationBefore = preflightGitTree(projectRoot, git);
28133
+ if (sourceBefore.isDirty && !dirtyResolution?.sourceAction) {
28134
+ throw new Error(
28135
+ `The task worktree has ${sourceBefore.dirtyFileCount} uncommitted file${sourceBefore.dirtyFileCount === 1 ? "" : "s"}. Choose how to preserve them before migrating.`
28136
+ );
28137
+ }
28138
+ if (destinationBefore.isDirty && !dirtyResolution?.destinationAction) {
28139
+ throw new Error(
28140
+ `The Project checkout has ${destinationBefore.dirtyFileCount} uncommitted file${destinationBefore.dirtyFileCount === 1 ? "" : "s"}. Choose how to preserve them before migrating.`
28141
+ );
28142
+ }
28143
+ if (sourceBefore.isDirty && dirtyResolution?.sourceAction === "leave") {
28144
+ throw new Error("Task worktree changes cannot be left behind when releasing the worktree.");
28145
+ }
28146
+ if (destinationBefore.isDirty && dirtyResolution?.destinationAction === "leave") {
28147
+ throw new Error("Project checkout changes cannot stay in place while switching branches.");
28148
+ }
28149
+ const destinationResolved = destinationBefore.isDirty ? resolveDirtyTree(
28150
+ {
28151
+ cwd: projectRoot,
28152
+ action: dirtyResolution?.destinationAction ?? "leave",
28153
+ operation: "worktree-to-checkout-destination",
28154
+ confirmDiscard: dirtyResolution?.confirmDiscard
28155
+ },
28156
+ { git }
28157
+ ) : null;
28158
+ const sourceResolved = sourceBefore.isDirty ? resolveDirtyTree(
28159
+ {
28160
+ cwd: worktreeRoot,
28161
+ action: dirtyResolution?.sourceAction ?? "leave",
28162
+ operation: "worktree-to-checkout-source",
28163
+ confirmDiscard: dirtyResolution?.confirmDiscard
28164
+ },
28165
+ { git }
28166
+ ) : null;
28167
+ try {
28168
+ git(projectRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
28169
+ } catch {
28170
+ throw new Error(`The target branch "${branch}" is not available in the Project.`);
28171
+ }
28172
+ removeGit(projectRoot, ["worktree", "remove", worktreeRoot]);
28173
+ gitSafe(projectRoot, ["worktree", "prune"]);
28174
+ try {
28175
+ git(projectRoot, ["checkout", branch]);
28176
+ } catch (checkoutError) {
28177
+ try {
28178
+ git(projectRoot, ["worktree", "add", worktreeRoot, branch]);
28179
+ if (sourceResolved?.stash) {
28180
+ applyMigrationStash({ cwd: worktreeRoot, stash: sourceResolved.stash }, git);
28181
+ }
28182
+ if (destinationResolved?.stash) {
28183
+ applyMigrationStash({ cwd: projectRoot, stash: destinationResolved.stash }, git);
28184
+ }
28185
+ } catch (restoreError) {
28186
+ const checkoutMessage2 = checkoutError instanceof Error ? checkoutError.message : String(checkoutError);
28187
+ const restoreMessage = restoreError instanceof Error ? restoreError.message : String(restoreError);
28188
+ throw new Error(
28189
+ `Project checkout failed (${checkoutMessage2}) and the task worktree could not be restored (${restoreMessage}).`
28190
+ );
28191
+ }
28192
+ const checkoutMessage = checkoutError instanceof Error ? checkoutError.message : String(checkoutError);
28193
+ throw new Error(
28194
+ `Project checkout failed (${checkoutMessage}); restored the task worktree at ${worktreeRoot}.`
28195
+ );
28196
+ }
28197
+ if (sourceResolved?.stash) {
28198
+ try {
28199
+ applyMigrationStash({ cwd: projectRoot, stash: sourceResolved.stash }, git);
28200
+ } catch (applyError) {
28201
+ gitSafe(projectRoot, ["reset", "--hard", "HEAD"]);
28202
+ gitSafe(projectRoot, ["clean", "-fd"]);
28203
+ try {
28204
+ if (destinationBefore.currentBranch && destinationBefore.currentBranch !== branch) {
28205
+ git(projectRoot, ["checkout", destinationBefore.currentBranch]);
28206
+ }
28207
+ git(projectRoot, ["worktree", "add", worktreeRoot, branch]);
28208
+ applyMigrationStash({ cwd: worktreeRoot, stash: sourceResolved.stash }, git);
28209
+ if (destinationResolved?.stash) {
28210
+ applyMigrationStash({ cwd: projectRoot, stash: destinationResolved.stash }, git);
28211
+ }
28212
+ } catch (restoreError) {
28213
+ const applyMessage2 = applyError instanceof Error ? applyError.message : String(applyError);
28214
+ const restoreMessage = restoreError instanceof Error ? restoreError.message : String(restoreError);
28215
+ throw new Error(
28216
+ `Moving uncommitted changes failed (${applyMessage2}); automatic workspace restoration also failed (${restoreMessage}). The named migration stash was preserved.`
28217
+ );
28218
+ }
28219
+ const applyMessage = applyError instanceof Error ? applyError.message : String(applyError);
28220
+ throw new Error(
28221
+ `Moving uncommitted changes failed (${applyMessage}); the original worktree and checkout were restored.`
28222
+ );
28223
+ }
28224
+ }
28225
+ return {
28226
+ ok: true,
28227
+ replayed: false,
28228
+ migrated: [{ ...target, projectPath, worktreePath: requestedWorktreePath, branch }],
28229
+ ...dirtyResolution ? { dirtyResolution } : {}
26486
28230
  };
26487
28231
  }
26488
28232
  function ensureSharedCheckoutBranch(input) {
26489
28233
  assertValidGitRef(input.branchName, "branchName");
26490
28234
  assertValidGitRef(input.baseBranch, "baseBranch");
26491
- const repoPath = (0, import_node_path11.resolve)(input.repoPath);
28235
+ const repoPath = (0, import_node_path12.resolve)(input.repoPath);
26492
28236
  if (gitSafe(repoPath, ["rev-parse", "--is-inside-work-tree"]) !== "true") {
26493
28237
  return { checkedOut: false, reason: `${repoPath} is not a git work tree` };
26494
28238
  }
@@ -26533,7 +28277,7 @@ function resolveSharedCheckoutTargets(input) {
26533
28277
  }
26534
28278
  function resolveRuntimeCwd(payload) {
26535
28279
  const multiRepoWorktree = payload.worktreeInfo?.allWorktrees?.[0]?.worktreePath;
26536
- if (multiRepoWorktree) return (0, import_node_path11.dirname)(multiRepoWorktree);
28280
+ if (multiRepoWorktree) return (0, import_node_path12.dirname)(multiRepoWorktree);
26537
28281
  if (isAlanDefaultWorkspace(payload.projectPath)) return resolveAlanDefaultWorkspacePath();
26538
28282
  return payload.worktreeInfo?.worktreePath ?? payload.projectPath ?? process.cwd();
26539
28283
  }
@@ -26646,14 +28390,14 @@ function localServiceOwnerPayload(owner) {
26646
28390
  ].join("\0");
26647
28391
  }
26648
28392
  function signLocalServiceOwner(secret, owner) {
26649
- return (0, import_node_crypto3.createHmac)("sha256", secret).update(localServiceOwnerPayload(owner)).digest("hex");
28393
+ return (0, import_node_crypto5.createHmac)("sha256", secret).update(localServiceOwnerPayload(owner)).digest("hex");
26650
28394
  }
26651
28395
  function signLocalServiceAdoption(secret, serviceId, challenge) {
26652
- return (0, import_node_crypto3.createHmac)("sha256", secret).update(["adopt", serviceId, challenge].join("\0")).digest("hex");
28396
+ return (0, import_node_crypto5.createHmac)("sha256", secret).update(["adopt", serviceId, challenge].join("\0")).digest("hex");
26653
28397
  }
26654
28398
  function equalHexDigest(actual, expected) {
26655
28399
  if (!/^[a-f0-9]{64}$/i.test(actual) || !/^[a-f0-9]{64}$/i.test(expected)) return false;
26656
- return (0, import_node_crypto3.timingSafeEqual)(Buffer.from(actual, "hex"), Buffer.from(expected, "hex"));
28400
+ return (0, import_node_crypto5.timingSafeEqual)(Buffer.from(actual, "hex"), Buffer.from(expected, "hex"));
26657
28401
  }
26658
28402
  function parseLocalServiceOwner(value2) {
26659
28403
  if (!value2 || typeof value2 !== "object") return null;
@@ -26755,12 +28499,12 @@ function getConfigPath() {
26755
28499
  return process.env.ALAN_AGENT_CONFIG_PATH ?? boundAgentConfigPath ?? resolveAgentConfigPath({ profile: "production" });
26756
28500
  }
26757
28501
  function getEndpointDefaultsPath() {
26758
- return process.env.ALAN_AGENT_ENDPOINTS_PATH ?? (0, import_node_path11.join)((0, import_node_path11.dirname)(getConfigPath()), "endpoints.json");
28502
+ return process.env.ALAN_AGENT_ENDPOINTS_PATH ?? (0, import_node_path12.join)((0, import_node_path12.dirname)(getConfigPath()), "endpoints.json");
26759
28503
  }
26760
28504
  function readConfig() {
26761
28505
  const configPath = getConfigPath();
26762
- if (!(0, import_node_fs13.existsSync)(configPath)) return {};
26763
- const parsed = JSON.parse((0, import_node_fs13.readFileSync)(configPath, "utf8"));
28506
+ if (!(0, import_node_fs14.existsSync)(configPath)) return {};
28507
+ const parsed = JSON.parse((0, import_node_fs14.readFileSync)(configPath, "utf8"));
26764
28508
  const sanitized = sanitizeAgentConfig(parsed);
26765
28509
  if (Object.keys(parsed).some((key) => !Object.hasOwn(sanitized, key))) {
26766
28510
  writeConfig(sanitized);
@@ -26776,27 +28520,27 @@ function readConfigForRegistration() {
26776
28520
  }
26777
28521
  function readEndpointDefaults() {
26778
28522
  const endpointsPath = getEndpointDefaultsPath();
26779
- if (!(0, import_node_fs13.existsSync)(endpointsPath)) return {};
28523
+ if (!(0, import_node_fs14.existsSync)(endpointsPath)) return {};
26780
28524
  try {
26781
- return JSON.parse((0, import_node_fs13.readFileSync)(endpointsPath, "utf8"));
28525
+ return JSON.parse((0, import_node_fs14.readFileSync)(endpointsPath, "utf8"));
26782
28526
  } catch {
26783
28527
  return {};
26784
28528
  }
26785
28529
  }
26786
28530
  function writeConfig(config) {
26787
28531
  const configPath = getConfigPath();
26788
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(configPath), { recursive: true, mode: 448 });
26789
- const pendingPath = `${configPath}.pending-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
28532
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true, mode: 448 });
28533
+ const pendingPath = `${configPath}.pending-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
26790
28534
  try {
26791
- (0, import_node_fs13.writeFileSync)(
28535
+ (0, import_node_fs14.writeFileSync)(
26792
28536
  pendingPath,
26793
28537
  JSON.stringify(sanitizeAgentConfig(config), null, 2),
26794
28538
  { mode: 384 }
26795
28539
  );
26796
- (0, import_node_fs13.chmodSync)(pendingPath, 384);
26797
- (0, import_node_fs13.renameSync)(pendingPath, configPath);
28540
+ (0, import_node_fs14.chmodSync)(pendingPath, 384);
28541
+ (0, import_node_fs14.renameSync)(pendingPath, configPath);
26798
28542
  } finally {
26799
- (0, import_node_fs13.rmSync)(pendingPath, { force: true });
28543
+ (0, import_node_fs14.rmSync)(pendingPath, { force: true });
26800
28544
  }
26801
28545
  }
26802
28546
  function maintenanceLeasePath(configPath) {
@@ -26804,9 +28548,9 @@ function maintenanceLeasePath(configPath) {
26804
28548
  }
26805
28549
  function readLocalDaemonMaintenanceLease(configPath, nowMs = Date.now()) {
26806
28550
  const path = maintenanceLeasePath(configPath);
26807
- if (!(0, import_node_fs13.existsSync)(path)) return null;
28551
+ if (!(0, import_node_fs14.existsSync)(path)) return null;
26808
28552
  try {
26809
- const lease = JSON.parse((0, import_node_fs13.readFileSync)(path, "utf8"));
28553
+ const lease = JSON.parse((0, import_node_fs14.readFileSync)(path, "utf8"));
26810
28554
  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))) {
26811
28555
  throw new Error("invalid maintenance lease");
26812
28556
  }
@@ -26823,8 +28567,8 @@ function readLocalDaemonMaintenanceLease(configPath, nowMs = Date.now()) {
26823
28567
  expiresAtMs: nowMs + 5e3
26824
28568
  };
26825
28569
  try {
26826
- (0, import_node_fs13.writeFileSync)(path, JSON.stringify(fallback), { mode: 384 });
26827
- (0, import_node_fs13.chmodSync)(path, 384);
28570
+ (0, import_node_fs14.writeFileSync)(path, JSON.stringify(fallback), { mode: 384 });
28571
+ (0, import_node_fs14.chmodSync)(path, 384);
26828
28572
  } catch {
26829
28573
  }
26830
28574
  return fallback;
@@ -26835,15 +28579,15 @@ function acquireLocalDaemonMaintenanceLease(configPath, reason, ttlMs = 5e3) {
26835
28579
  if (existing) return { acquired: false, lease: existing };
26836
28580
  const lease = {
26837
28581
  version: 1,
26838
- id: (0, import_node_crypto3.randomUUID)(),
28582
+ id: (0, import_node_crypto5.randomUUID)(),
26839
28583
  reason,
26840
28584
  expiresAtMs: reason === "credential_cleanup" ? null : Date.now() + Math.max(1e3, Math.min(ttlMs, 6e4))
26841
28585
  };
26842
28586
  const path = maintenanceLeasePath(configPath);
26843
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
28587
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(path), { recursive: true, mode: 448 });
26844
28588
  try {
26845
- (0, import_node_fs13.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
26846
- (0, import_node_fs13.chmodSync)(path, 384);
28589
+ (0, import_node_fs14.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
28590
+ (0, import_node_fs14.chmodSync)(path, 384);
26847
28591
  return { acquired: true, lease };
26848
28592
  } catch (error) {
26849
28593
  const raced = readLocalDaemonMaintenanceLease(configPath);
@@ -26859,9 +28603,9 @@ function daemonOwnerLeasePath(configPath) {
26859
28603
  }
26860
28604
  function readLocalDaemonOwnerLease(configPath) {
26861
28605
  const path = daemonOwnerLeasePath(configPath);
26862
- if (!(0, import_node_fs13.existsSync)(path)) return null;
28606
+ if (!(0, import_node_fs14.existsSync)(path)) return null;
26863
28607
  try {
26864
- const lease = JSON.parse((0, import_node_fs13.readFileSync)(path, "utf8"));
28608
+ const lease = JSON.parse((0, import_node_fs14.readFileSync)(path, "utf8"));
26865
28609
  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) {
26866
28610
  throw new Error("invalid daemon owner lease");
26867
28611
  }
@@ -26903,7 +28647,7 @@ async function acquireLocalDaemonOwnerLease(input) {
26903
28647
  }
26904
28648
  const lease = {
26905
28649
  version: 1,
26906
- id: (0, import_node_crypto3.randomUUID)(),
28650
+ id: (0, import_node_crypto5.randomUUID)(),
26907
28651
  daemonSessionId: input.daemonSessionId,
26908
28652
  pid: process.pid,
26909
28653
  startedAtMs: Date.now(),
@@ -26913,10 +28657,10 @@ async function acquireLocalDaemonOwnerLease(input) {
26913
28657
  localApiToken: input.localApiToken
26914
28658
  };
26915
28659
  const path = daemonOwnerLeasePath(input.configPath);
26916
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
28660
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(path), { recursive: true, mode: 448 });
26917
28661
  try {
26918
- (0, import_node_fs13.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
26919
- (0, import_node_fs13.chmodSync)(path, 384);
28662
+ (0, import_node_fs14.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
28663
+ (0, import_node_fs14.chmodSync)(path, 384);
26920
28664
  return { acquired: true, lease };
26921
28665
  } catch (error) {
26922
28666
  const raced = readLocalDaemonOwnerLease(input.configPath);
@@ -26931,27 +28675,27 @@ function updateLocalDaemonOwnerEndpoint(configPath, lease, localApiPort) {
26931
28675
  const path = daemonOwnerLeasePath(configPath);
26932
28676
  const pendingPath = `${path}.pending-${lease.id}`;
26933
28677
  try {
26934
- (0, import_node_fs13.writeFileSync)(pendingPath, JSON.stringify(updated), { mode: 384 });
26935
- (0, import_node_fs13.chmodSync)(pendingPath, 384);
26936
- (0, import_node_fs13.renameSync)(pendingPath, path);
28678
+ (0, import_node_fs14.writeFileSync)(pendingPath, JSON.stringify(updated), { mode: 384 });
28679
+ (0, import_node_fs14.chmodSync)(pendingPath, 384);
28680
+ (0, import_node_fs14.renameSync)(pendingPath, path);
26937
28681
  return updated;
26938
28682
  } finally {
26939
- (0, import_node_fs13.rmSync)(pendingPath, { force: true });
28683
+ (0, import_node_fs14.rmSync)(pendingPath, { force: true });
26940
28684
  }
26941
28685
  }
26942
28686
  function releaseLocalDaemonOwnerLease(configPath, leaseId) {
26943
28687
  releaseOwnedJsonFile(daemonOwnerLeasePath(configPath), leaseId);
26944
28688
  }
26945
28689
  function releaseOwnedJsonFile(path, ownerId, ownerField = "id") {
26946
- const releasePath = `${path}.release-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
28690
+ const releasePath = `${path}.release-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
26947
28691
  try {
26948
- (0, import_node_fs13.renameSync)(path, releasePath);
28692
+ (0, import_node_fs14.renameSync)(path, releasePath);
26949
28693
  } catch (error) {
26950
28694
  if (error.code === "ENOENT") return;
26951
28695
  throw error;
26952
28696
  }
26953
28697
  try {
26954
- const content = (0, import_node_fs13.readFileSync)(releasePath, "utf8");
28698
+ const content = (0, import_node_fs14.readFileSync)(releasePath, "utf8");
26955
28699
  let capturedOwnerId = null;
26956
28700
  try {
26957
28701
  const captured = JSON.parse(content);
@@ -26960,13 +28704,13 @@ function releaseOwnedJsonFile(path, ownerId, ownerField = "id") {
26960
28704
  }
26961
28705
  if (capturedOwnerId === ownerId) return;
26962
28706
  try {
26963
- (0, import_node_fs13.writeFileSync)(path, content, { mode: 384, flag: "wx" });
26964
- (0, import_node_fs13.chmodSync)(path, 384);
28707
+ (0, import_node_fs14.writeFileSync)(path, content, { mode: 384, flag: "wx" });
28708
+ (0, import_node_fs14.chmodSync)(path, 384);
26965
28709
  } catch (error) {
26966
28710
  if (error.code !== "EEXIST") throw error;
26967
28711
  }
26968
28712
  } finally {
26969
- (0, import_node_fs13.rmSync)(releasePath, { force: true });
28713
+ (0, import_node_fs14.rmSync)(releasePath, { force: true });
26970
28714
  }
26971
28715
  }
26972
28716
  function inspectLocalDaemonRunJournal(configPath) {
@@ -26976,8 +28720,8 @@ function inspectLocalDaemonRunJournal(configPath) {
26976
28720
  try {
26977
28721
  let activeRunCount = 0;
26978
28722
  for (const artifact of [path, pendingPath]) {
26979
- if (!(0, import_node_fs13.existsSync)(artifact)) continue;
26980
- const parsed = JSON.parse((0, import_node_fs13.readFileSync)(artifact, "utf8"));
28723
+ if (!(0, import_node_fs14.existsSync)(artifact)) continue;
28724
+ const parsed = JSON.parse((0, import_node_fs14.readFileSync)(artifact, "utf8"));
26981
28725
  if (parsed.version !== 1 || !Array.isArray(parsed.entries))
26982
28726
  throw new Error("invalid journal");
26983
28727
  if (artifact === path) {
@@ -26996,7 +28740,7 @@ function inspectLocalDaemonRunJournal(configPath) {
26996
28740
  }).length;
26997
28741
  continue;
26998
28742
  }
26999
- const legacyStartedAtMs = (0, import_node_fs13.statSync)(pendingPath).mtimeMs;
28743
+ const legacyStartedAtMs = (0, import_node_fs14.statSync)(pendingPath).mtimeMs;
27000
28744
  activeRunCount += parsed.entries.filter((entry) => {
27001
28745
  if (!entry || typeof entry !== "object") throw new Error("invalid pending run entry");
27002
28746
  const startedAtMs = entry.startedAtMs;
@@ -27018,38 +28762,38 @@ function pendingSetupIntentPath() {
27018
28762
  return `${getConfigPath()}.setup-pending`;
27019
28763
  }
27020
28764
  function setupTokenHash(setupToken) {
27021
- return (0, import_node_crypto3.createHash)("sha256").update(setupToken).digest("hex");
28765
+ return (0, import_node_crypto5.createHash)("sha256").update(setupToken).digest("hex");
27022
28766
  }
27023
28767
  function getOrCreateSetupAttemptId(setupToken) {
27024
28768
  const intentPath = pendingSetupIntentPath();
27025
28769
  const tokenHash = setupTokenHash(setupToken);
27026
- if ((0, import_node_fs13.existsSync)(intentPath)) {
28770
+ if ((0, import_node_fs14.existsSync)(intentPath)) {
27027
28771
  try {
27028
- const stored = JSON.parse((0, import_node_fs13.readFileSync)(intentPath, "utf8"));
28772
+ const stored = JSON.parse((0, import_node_fs14.readFileSync)(intentPath, "utf8"));
27029
28773
  if (stored.setupTokenHash === tokenHash && typeof stored.registrationAttemptId === "string" && stored.registrationAttemptId.length > 0) {
27030
28774
  return stored.registrationAttemptId;
27031
28775
  }
27032
28776
  } catch {
27033
28777
  }
27034
28778
  }
27035
- const registrationAttemptId = (0, import_node_crypto3.randomUUID)();
27036
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(intentPath), { recursive: true, mode: 448 });
27037
- const writePath = `${intentPath}.pending-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
28779
+ const registrationAttemptId = (0, import_node_crypto5.randomUUID)();
28780
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(intentPath), { recursive: true, mode: 448 });
28781
+ const writePath = `${intentPath}.pending-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
27038
28782
  try {
27039
- (0, import_node_fs13.writeFileSync)(
28783
+ (0, import_node_fs14.writeFileSync)(
27040
28784
  writePath,
27041
28785
  JSON.stringify({ setupTokenHash: tokenHash, registrationAttemptId }, null, 2),
27042
28786
  { mode: 384 }
27043
28787
  );
27044
- (0, import_node_fs13.chmodSync)(writePath, 384);
27045
- (0, import_node_fs13.renameSync)(writePath, intentPath);
28788
+ (0, import_node_fs14.chmodSync)(writePath, 384);
28789
+ (0, import_node_fs14.renameSync)(writePath, intentPath);
27046
28790
  } finally {
27047
- (0, import_node_fs13.rmSync)(writePath, { force: true });
28791
+ (0, import_node_fs14.rmSync)(writePath, { force: true });
27048
28792
  }
27049
28793
  return registrationAttemptId;
27050
28794
  }
27051
28795
  function clearPendingSetupIntent() {
27052
- (0, import_node_fs13.rmSync)(pendingSetupIntentPath(), { force: true });
28796
+ (0, import_node_fs14.rmSync)(pendingSetupIntentPath(), { force: true });
27053
28797
  }
27054
28798
  function pendingDeviceAuthorizationPath() {
27055
28799
  return `${getConfigPath()}.device-auth-pending`;
@@ -27070,35 +28814,35 @@ function parseDeviceAuthorizationOperationContent(content) {
27070
28814
  }
27071
28815
  function restoreCapturedFileUnlessReplaced(path, content) {
27072
28816
  try {
27073
- (0, import_node_fs13.writeFileSync)(path, content, { mode: 384, flag: "wx" });
27074
- (0, import_node_fs13.chmodSync)(path, 384);
28817
+ (0, import_node_fs14.writeFileSync)(path, content, { mode: 384, flag: "wx" });
28818
+ (0, import_node_fs14.chmodSync)(path, 384);
27075
28819
  } catch (error) {
27076
28820
  if (error.code !== "EEXIST") throw error;
27077
28821
  }
27078
28822
  }
27079
28823
  function quarantineCorruptDeviceAuthorizationOperation(path) {
27080
- const quarantinePath = `${path}.corrupt-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
28824
+ const quarantinePath = `${path}.corrupt-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
27081
28825
  try {
27082
- (0, import_node_fs13.renameSync)(path, quarantinePath);
28826
+ (0, import_node_fs14.renameSync)(path, quarantinePath);
27083
28827
  } catch (error) {
27084
28828
  if (error.code === "ENOENT") return null;
27085
28829
  throw error;
27086
28830
  }
27087
28831
  try {
27088
- const capturedContent = (0, import_node_fs13.readFileSync)(quarantinePath, "utf8");
28832
+ const capturedContent = (0, import_node_fs14.readFileSync)(quarantinePath, "utf8");
27089
28833
  if (parseDeviceAuthorizationOperationContent(capturedContent)) {
27090
28834
  restoreCapturedFileUnlessReplaced(path, capturedContent);
27091
28835
  }
27092
28836
  } finally {
27093
- (0, import_node_fs13.rmSync)(quarantinePath, { force: true });
28837
+ (0, import_node_fs14.rmSync)(quarantinePath, { force: true });
27094
28838
  }
27095
- if (!(0, import_node_fs13.existsSync)(path)) return null;
27096
- return parseDeviceAuthorizationOperationContent((0, import_node_fs13.readFileSync)(path, "utf8"));
28839
+ if (!(0, import_node_fs14.existsSync)(path)) return null;
28840
+ return parseDeviceAuthorizationOperationContent((0, import_node_fs14.readFileSync)(path, "utf8"));
27097
28841
  }
27098
28842
  function readDeviceAuthorizationOperation() {
27099
28843
  const path = deviceAuthorizationOperationPath();
27100
- if (!(0, import_node_fs13.existsSync)(path)) return null;
27101
- const parsed = parseDeviceAuthorizationOperationContent((0, import_node_fs13.readFileSync)(path, "utf8"));
28844
+ if (!(0, import_node_fs14.existsSync)(path)) return null;
28845
+ const parsed = parseDeviceAuthorizationOperationContent((0, import_node_fs14.readFileSync)(path, "utf8"));
27102
28846
  const lease = parsed ?? quarantineCorruptDeviceAuthorizationOperation(path);
27103
28847
  if (!lease) return null;
27104
28848
  if (lease.expiresAtMs <= Date.now()) {
@@ -27112,14 +28856,14 @@ function acquireDeviceAuthorizationOperation() {
27112
28856
  if (existing) return null;
27113
28857
  const lease = {
27114
28858
  version: 1,
27115
- id: (0, import_node_crypto3.randomUUID)(),
28859
+ id: (0, import_node_crypto5.randomUUID)(),
27116
28860
  expiresAtMs: Date.now() + 6e4
27117
28861
  };
27118
28862
  const path = deviceAuthorizationOperationPath();
27119
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
28863
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(path), { recursive: true, mode: 448 });
27120
28864
  try {
27121
- (0, import_node_fs13.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
27122
- (0, import_node_fs13.chmodSync)(path, 384);
28865
+ (0, import_node_fs14.writeFileSync)(path, JSON.stringify(lease), { mode: 384, flag: "wx" });
28866
+ (0, import_node_fs14.chmodSync)(path, 384);
27123
28867
  return lease;
27124
28868
  } catch (error) {
27125
28869
  if (error.code === "EEXIST") return null;
@@ -27132,17 +28876,17 @@ function extendDeviceAuthorizationOperation(operationId, expiresAtMs) {
27132
28876
  throw new Error("browser authorization operation ownership was lost");
27133
28877
  }
27134
28878
  const path = deviceAuthorizationOperationPath();
27135
- const pendingPath = `${path}.pending-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
28879
+ const pendingPath = `${path}.pending-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
27136
28880
  try {
27137
- (0, import_node_fs13.writeFileSync)(
28881
+ (0, import_node_fs14.writeFileSync)(
27138
28882
  pendingPath,
27139
28883
  JSON.stringify({ ...current, expiresAtMs: Math.max(expiresAtMs, Date.now() + 6e4) }),
27140
28884
  { mode: 384 }
27141
28885
  );
27142
- (0, import_node_fs13.chmodSync)(pendingPath, 384);
27143
- (0, import_node_fs13.renameSync)(pendingPath, path);
28886
+ (0, import_node_fs14.chmodSync)(pendingPath, 384);
28887
+ (0, import_node_fs14.renameSync)(pendingPath, path);
27144
28888
  } finally {
27145
- (0, import_node_fs13.rmSync)(pendingPath, { force: true });
28889
+ (0, import_node_fs14.rmSync)(pendingPath, { force: true });
27146
28890
  }
27147
28891
  }
27148
28892
  function assertDeviceAuthorizationOperationOwner(operationId) {
@@ -27153,12 +28897,12 @@ function assertDeviceAuthorizationOperationOwner(operationId) {
27153
28897
  function clearPendingDeviceAuthorization(operationId) {
27154
28898
  const path = pendingDeviceAuthorizationPath();
27155
28899
  if (!operationId) {
27156
- (0, import_node_fs13.rmSync)(path, { force: true });
28900
+ (0, import_node_fs14.rmSync)(path, { force: true });
27157
28901
  return;
27158
28902
  }
27159
- if (!(0, import_node_fs13.existsSync)(path)) return;
28903
+ if (!(0, import_node_fs14.existsSync)(path)) return;
27160
28904
  try {
27161
- const pending = JSON.parse((0, import_node_fs13.readFileSync)(path, "utf8"));
28905
+ const pending = JSON.parse((0, import_node_fs14.readFileSync)(path, "utf8"));
27162
28906
  if (pending.operationId !== operationId) return;
27163
28907
  } catch {
27164
28908
  return;
@@ -27167,9 +28911,9 @@ function clearPendingDeviceAuthorization(operationId) {
27167
28911
  }
27168
28912
  function readPendingDeviceAuthorization(apiUrl) {
27169
28913
  const path = pendingDeviceAuthorizationPath();
27170
- if (!(0, import_node_fs13.existsSync)(path)) return null;
28914
+ if (!(0, import_node_fs14.existsSync)(path)) return null;
27171
28915
  try {
27172
- const pending = JSON.parse((0, import_node_fs13.readFileSync)(path, "utf8"));
28916
+ const pending = JSON.parse((0, import_node_fs14.readFileSync)(path, "utf8"));
27173
28917
  const expiresAtMs = Date.parse(pending.expiresAt ?? "");
27174
28918
  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()) {
27175
28919
  return null;
@@ -27181,20 +28925,20 @@ function readPendingDeviceAuthorization(apiUrl) {
27181
28925
  }
27182
28926
  function writePendingDeviceAuthorization(pending) {
27183
28927
  const path = pendingDeviceAuthorizationPath();
27184
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
27185
- const writePath = `${path}.pending-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
28928
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(path), { recursive: true, mode: 448 });
28929
+ const writePath = `${path}.pending-${process.pid}-${(0, import_node_crypto5.randomBytes)(6).toString("hex")}`;
27186
28930
  try {
27187
- (0, import_node_fs13.writeFileSync)(writePath, JSON.stringify(pending, null, 2), { mode: 384 });
27188
- (0, import_node_fs13.chmodSync)(writePath, 384);
27189
- (0, import_node_fs13.renameSync)(writePath, path);
28931
+ (0, import_node_fs14.writeFileSync)(writePath, JSON.stringify(pending, null, 2), { mode: 384 });
28932
+ (0, import_node_fs14.chmodSync)(writePath, 384);
28933
+ (0, import_node_fs14.renameSync)(writePath, path);
27190
28934
  } finally {
27191
- (0, import_node_fs13.rmSync)(writePath, { force: true });
28935
+ (0, import_node_fs14.rmSync)(writePath, { force: true });
27192
28936
  }
27193
28937
  }
27194
28938
  function removeAlanOwnedLocalArtifacts() {
27195
28939
  const configPath = getConfigPath();
27196
- const directory = (0, import_node_path11.dirname)(configPath);
27197
- const configName = (0, import_node_path11.basename)(configPath).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28940
+ const directory = (0, import_node_path12.dirname)(configPath);
28941
+ const configName = (0, import_node_path12.basename)(configPath).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27198
28942
  const alanAtomicSidecarPattern = new RegExp(
27199
28943
  `^${configName}(?:|\\.setup-pending|\\.device-auth-pending|\\.device-auth-operation\\.json|\\.maintenance-lease\\.json|\\.event-outbox\\.enc)\\.(?:pending|release|corrupt)-\\d+-[a-f0-9]{12}$`
27200
28944
  );
@@ -27211,13 +28955,13 @@ function removeAlanOwnedLocalArtifacts() {
27211
28955
  `${configPath}.pending-runs.json.pending`,
27212
28956
  maintenanceLeasePath(configPath)
27213
28957
  ]);
27214
- if ((0, import_node_fs13.existsSync)(directory)) {
27215
- for (const entry of (0, import_node_fs13.readdirSync)(directory)) {
27216
- if (alanAtomicSidecarPattern.test(entry)) artifacts.add((0, import_node_path11.join)(directory, entry));
28958
+ if ((0, import_node_fs14.existsSync)(directory)) {
28959
+ for (const entry of (0, import_node_fs14.readdirSync)(directory)) {
28960
+ if (alanAtomicSidecarPattern.test(entry)) artifacts.add((0, import_node_path12.join)(directory, entry));
27217
28961
  }
27218
28962
  }
27219
- for (const artifact of artifacts) (0, import_node_fs13.rmSync)(artifact, { force: true });
27220
- (0, import_node_fs13.rmSync)(configPath, { force: true });
28963
+ for (const artifact of artifacts) (0, import_node_fs14.rmSync)(artifact, { force: true });
28964
+ (0, import_node_fs14.rmSync)(configPath, { force: true });
27221
28965
  }
27222
28966
  function argValue(args, name) {
27223
28967
  const index = args.indexOf(name);
@@ -27273,7 +29017,7 @@ function getLocalApiPort(args, stored) {
27273
29017
  }
27274
29018
  function sleep(ms) {
27275
29019
  if (ms <= 0) return Promise.resolve();
27276
- return new Promise((resolve6) => setTimeout(resolve6, ms));
29020
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
27277
29021
  }
27278
29022
  function normalizeBackendKind2(backendKind) {
27279
29023
  if (backendKind === "codex") return "codex_app_server";
@@ -27439,13 +29183,13 @@ var DurablePendingRunStarts = class extends Map {
27439
29183
  }
27440
29184
  persist() {
27441
29185
  if (this.size === 0) {
27442
- (0, import_node_fs13.rmSync)(this.path, { force: true });
29186
+ (0, import_node_fs14.rmSync)(this.path, { force: true });
27443
29187
  return;
27444
29188
  }
27445
29189
  const pendingPath = `${this.path}.pending`;
27446
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(this.path), { recursive: true, mode: 448 });
29190
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(this.path), { recursive: true, mode: 448 });
27447
29191
  try {
27448
- (0, import_node_fs13.writeFileSync)(
29192
+ (0, import_node_fs14.writeFileSync)(
27449
29193
  pendingPath,
27450
29194
  JSON.stringify({
27451
29195
  version: 1,
@@ -27458,10 +29202,10 @@ var DurablePendingRunStarts = class extends Map {
27458
29202
  }),
27459
29203
  { mode: 384 }
27460
29204
  );
27461
- (0, import_node_fs13.chmodSync)(pendingPath, 384);
27462
- (0, import_node_fs13.renameSync)(pendingPath, this.path);
29205
+ (0, import_node_fs14.chmodSync)(pendingPath, 384);
29206
+ (0, import_node_fs14.renameSync)(pendingPath, this.path);
27463
29207
  } finally {
27464
- (0, import_node_fs13.rmSync)(pendingPath, { force: true });
29208
+ (0, import_node_fs14.rmSync)(pendingPath, { force: true });
27465
29209
  }
27466
29210
  }
27467
29211
  };
@@ -27601,7 +29345,7 @@ function buildApplicationRunFailure(input) {
27601
29345
  const problem = createAlanProblem({
27602
29346
  domain: "application",
27603
29347
  code: input.code,
27604
- id: (0, import_node_crypto3.randomUUID)(),
29348
+ id: (0, import_node_crypto5.randomUUID)(),
27605
29349
  title: "Alan could not complete the agent run",
27606
29350
  detail: input.detail,
27607
29351
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27696,21 +29440,21 @@ function reconcileActiveRuns(input) {
27696
29440
  }
27697
29441
  }
27698
29442
  function collectRuntimeMetadata() {
27699
- const cpuList = (0, import_node_os8.cpus)();
29443
+ const cpuList = (0, import_node_os9.cpus)();
27700
29444
  const metadata = {
27701
- platform: (0, import_node_os8.platform)(),
27702
- arch: (0, import_node_os8.arch)(),
27703
- osVersion: (0, import_node_os8.release)(),
29445
+ platform: (0, import_node_os9.platform)(),
29446
+ arch: (0, import_node_os9.arch)(),
29447
+ osVersion: (0, import_node_os9.release)(),
27704
29448
  agentVersion: AGENT_VERSION,
27705
- memoryBytes: (0, import_node_os8.totalmem)(),
27706
- memoryFreeBytes: (0, import_node_os8.freemem)()
29449
+ memoryBytes: (0, import_node_os9.totalmem)(),
29450
+ memoryFreeBytes: (0, import_node_os9.freemem)()
27707
29451
  };
27708
29452
  if (cpuList.length > 0) {
27709
29453
  metadata.cpuModel = cpuList[0]?.model;
27710
29454
  metadata.cpuCores = cpuList.length;
27711
29455
  }
27712
29456
  try {
27713
- const disk = (0, import_node_fs13.statfsSync)((0, import_node_os8.homedir)());
29457
+ const disk = (0, import_node_fs14.statfsSync)((0, import_node_os9.homedir)());
27714
29458
  metadata.diskFreeBytes = disk.bavail * disk.bsize;
27715
29459
  metadata.diskTotalBytes = disk.blocks * disk.bsize;
27716
29460
  } catch {
@@ -27928,7 +29672,7 @@ var RuntimePresenter = class {
27928
29672
  this.applicationProblem = createAlanProblem({
27929
29673
  domain: "application",
27930
29674
  code: /local path no longer exists/i.test(message) ? "app.workspace.missing" : "app.internal",
27931
- id: (0, import_node_crypto3.randomUUID)(),
29675
+ id: (0, import_node_crypto5.randomUUID)(),
27932
29676
  title: "Alan could not start the agent run",
27933
29677
  detail: message,
27934
29678
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27966,9 +29710,9 @@ async function setupDaemon(args) {
27966
29710
  const teamId = argValue(args, "--team") ?? process.env.ALAN_TEAM_ID;
27967
29711
  const setupToken = argValue(args, "--setup-token") ?? process.env.ALAN_RUNTIME_SETUP_TOKEN;
27968
29712
  const token = argValue(args, "--token") ?? process.env.ALAN_SETUP_TOKEN;
27969
- const displayName = argValue(args, "--name") ?? (0, import_node_os8.hostname)();
29713
+ const displayName = argValue(args, "--name") ?? (0, import_node_os9.hostname)();
27970
29714
  const scope = argValue(args, "--scope") ?? process.env.ALAN_RUNTIME_SCOPE ?? (setupToken ? "team" : "user");
27971
- const installationId = argValue(args, "--installation-id") ?? previousConfig.installationId ?? (0, import_node_crypto3.randomUUID)();
29715
+ const installationId = argValue(args, "--installation-id") ?? previousConfig.installationId ?? (0, import_node_crypto5.randomUUID)();
27972
29716
  if (scope !== "team" && scope !== "user") {
27973
29717
  throw new Error("setup --scope must be either 'team' or 'user'");
27974
29718
  }
@@ -27977,7 +29721,7 @@ async function setupDaemon(args) {
27977
29721
  "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"
27978
29722
  );
27979
29723
  }
27980
- const registrationAttemptId = setupToken ? getOrCreateSetupAttemptId(setupToken) : (0, import_node_crypto3.randomUUID)();
29724
+ const registrationAttemptId = setupToken ? getOrCreateSetupAttemptId(setupToken) : (0, import_node_crypto5.randomUUID)();
27981
29725
  const registrationUrl = `${apiUrl}/public/runtimes${setupToken ? "/register-with-setup-token" : ""}`;
27982
29726
  const registrationInit = {
27983
29727
  method: "POST",
@@ -27989,7 +29733,7 @@ async function setupDaemon(args) {
27989
29733
  ...teamId ? { teamId } : {},
27990
29734
  ...setupToken ? { setupToken, registrationAttemptId } : {},
27991
29735
  displayName,
27992
- hostname: (0, import_node_os8.hostname)(),
29736
+ hostname: (0, import_node_os9.hostname)(),
27993
29737
  runtimeKind: "machine",
27994
29738
  managementKind: "user_managed",
27995
29739
  hostKind: "daemon",
@@ -28012,7 +29756,7 @@ async function setupDaemon(args) {
28012
29756
  }
28013
29757
  const retriableStatus = response && (response.status === 408 || response.status === 429 || response.status >= 500);
28014
29758
  if (response && (!retriableStatus || response.ok) || attempt === 3) break;
28015
- await new Promise((resolve6) => setTimeout(resolve6, attempt * 100));
29759
+ await new Promise((resolve7) => setTimeout(resolve7, attempt * 100));
28016
29760
  }
28017
29761
  if (!response) {
28018
29762
  const detail = transportError instanceof Error ? transportError.message : String(transportError);
@@ -28043,10 +29787,10 @@ async function setupDaemon(args) {
28043
29787
  runtimeToken: body.runtimeToken,
28044
29788
  runtimeRenewalToken: body.runtimeRenewalToken,
28045
29789
  localApiPort: getLocalApiPort(args, {}),
28046
- localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto3.randomBytes)(24).toString("hex"),
28047
- localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto3.randomUUID)(),
28048
- localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url"),
28049
- eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url"),
29790
+ localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto5.randomBytes)(24).toString("hex"),
29791
+ localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto5.randomUUID)(),
29792
+ localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url"),
29793
+ eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url"),
28050
29794
  daemonEpoch: previousConfig.daemonEpoch,
28051
29795
  displayName: body.runtime.displayName ?? displayName,
28052
29796
  installationId
@@ -28073,10 +29817,10 @@ async function loginWithDeviceCode(args) {
28073
29817
  }
28074
29818
  const previousConfig = readConfigForRegistration();
28075
29819
  const { apiUrl, wsUrl, endpointProfile } = resolveEndpoints(args);
28076
- const displayName = argValue(args, "--name") ?? (0, import_node_os8.hostname)();
29820
+ const displayName = argValue(args, "--name") ?? (0, import_node_os9.hostname)();
28077
29821
  const maxPolls = Number.parseInt(argValue(args, "--max-polls") ?? "300", 10);
28078
29822
  const resumed = readPendingDeviceAuthorization(apiUrl);
28079
- const installationId = resumed?.installationId ?? previousConfig.installationId ?? (0, import_node_crypto3.randomUUID)();
29823
+ const installationId = resumed?.installationId ?? previousConfig.installationId ?? (0, import_node_crypto5.randomUUID)();
28080
29824
  let authorization = resumed ? { ...resumed, operationId: operation.id } : null;
28081
29825
  if (!authorization) {
28082
29826
  clearPendingDeviceAuthorization();
@@ -28085,7 +29829,7 @@ async function loginWithDeviceCode(args) {
28085
29829
  headers: { "content-type": "application/json" },
28086
29830
  body: JSON.stringify({
28087
29831
  displayName,
28088
- hostname: (0, import_node_os8.hostname)(),
29832
+ hostname: (0, import_node_os9.hostname)(),
28089
29833
  capabilities: discoverCapabilities(),
28090
29834
  metadata: {
28091
29835
  ...await collectRuntimeMetadataWithProviderLimits(),
@@ -28152,10 +29896,10 @@ async function loginWithDeviceCode(args) {
28152
29896
  runtimeToken: body.runtimeToken,
28153
29897
  runtimeRenewalToken: body.runtimeRenewalToken,
28154
29898
  localApiPort: getLocalApiPort(args, {}),
28155
- localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto3.randomBytes)(24).toString("hex"),
28156
- localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto3.randomUUID)(),
28157
- localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url"),
28158
- eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url"),
29899
+ localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto5.randomBytes)(24).toString("hex"),
29900
+ localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto5.randomUUID)(),
29901
+ localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url"),
29902
+ eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url"),
28159
29903
  daemonEpoch: previousConfig.daemonEpoch,
28160
29904
  displayName: body.runtime.displayName ?? displayName,
28161
29905
  installationId
@@ -28175,10 +29919,10 @@ async function loginWithDeviceCode(args) {
28175
29919
  async function startDaemon(args) {
28176
29920
  bindConfigPath(args);
28177
29921
  const stored = readConfig();
28178
- const installationId = stored.installationId ?? (0, import_node_crypto3.randomUUID)();
29922
+ const installationId = stored.installationId ?? (0, import_node_crypto5.randomUUID)();
28179
29923
  const configPath = getConfigPath();
28180
29924
  const endpointProfile = stored.endpointProfile ?? resolveEndpointProfile(args);
28181
- const daemonSessionId = (0, import_node_crypto3.randomUUID)();
29925
+ const daemonSessionId = (0, import_node_crypto5.randomUUID)();
28182
29926
  let heartbeatSeq = 0;
28183
29927
  const runtimeId = argValue(args, "--runtime-id") ?? process.env.ALAN_RUNTIME_ID ?? stored.runtimeId;
28184
29928
  const runtimeTokenOverride = argValue(args, "--token") ?? process.env.ALAN_RUNTIME_TOKEN;
@@ -28195,11 +29939,11 @@ async function startDaemon(args) {
28195
29939
  wsUrl = endpointMismatch.expectedWsUrl;
28196
29940
  }
28197
29941
  let localApiPort = getLocalApiPort(args, stored);
28198
- const localApiToken = stored.localApiToken ?? (0, import_node_crypto3.randomBytes)(24).toString("hex");
28199
- const localServiceId = stored.localServiceId ?? (0, import_node_crypto3.randomUUID)();
28200
- const localServiceSecret = stored.localServiceSecret ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url");
29942
+ const localApiToken = stored.localApiToken ?? (0, import_node_crypto5.randomBytes)(24).toString("hex");
29943
+ const localServiceId = stored.localServiceId ?? (0, import_node_crypto5.randomUUID)();
29944
+ const localServiceSecret = stored.localServiceSecret ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url");
28201
29945
  const localServiceStartedAtMs = Date.now();
28202
- const eventSpoolKey = stored.eventSpoolKey ?? (0, import_node_crypto3.randomBytes)(32).toString("base64url");
29946
+ const eventSpoolKey = stored.eventSpoolKey ?? (0, import_node_crypto5.randomBytes)(32).toString("base64url");
28203
29947
  if (stored.daemonEpoch !== void 0 && (!Number.isSafeInteger(stored.daemonEpoch) || stored.daemonEpoch < 0 || stored.daemonEpoch >= Number.MAX_SAFE_INTEGER)) {
28204
29948
  throw new Error("daemon config contains an invalid durable epoch; repair the runtime config");
28205
29949
  }
@@ -28319,7 +30063,7 @@ async function startDaemon(args) {
28319
30063
  const workspaceRelocations = /* @__PURE__ */ new Map();
28320
30064
  for (const relocation of stored.workspaceRelocations ?? []) {
28321
30065
  if (relocation?.conversationId && relocation.runId && relocation.previousPath && relocation.canonicalPath) {
28322
- workspaceRelocations.set((0, import_node_path11.resolve)(relocation.previousPath), relocation);
30066
+ workspaceRelocations.set((0, import_node_path12.resolve)(relocation.previousPath), relocation);
28323
30067
  }
28324
30068
  }
28325
30069
  if (stored.localApiPort !== localApiPort || stored.localApiToken !== localApiToken || stored.localServiceId !== localServiceId || stored.localServiceSecret !== localServiceSecret || stored.eventSpoolKey !== eventSpoolKey || stored.daemonEpoch !== daemonEpoch || stored.installationId !== installationId) {
@@ -28635,20 +30379,25 @@ async function startDaemon(args) {
28635
30379
  pendingDeliveryLost.push({ conversationId: entry.conversationId, runId });
28636
30380
  }
28637
30381
  });
28638
- socket.on("desktop:rpc", (message) => {
30382
+ socket.on("desktop:rpc", async (message) => {
28639
30383
  if (!message?.id || !message.method) return;
28640
30384
  try {
28641
30385
  const params = message.params ?? {};
28642
- if (message.method !== "worktree.ensure") {
30386
+ 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) : (() => {
28643
30387
  throw new Error(`Method not found: ${message.method}`);
28644
- }
28645
- const result = ensureWorktree(params);
30388
+ })();
28646
30389
  socket.emit("desktop:rpc", { jsonrpc: "2.0", id: message.id, result });
28647
30390
  } catch (error) {
30391
+ const stableCode = error instanceof BindingSwitchGitError ? error.code : "runtime_operation_failed";
30392
+ const stableDetails = error instanceof BindingSwitchGitError ? error.details : void 0;
28648
30393
  socket.emit("desktop:rpc", {
28649
30394
  jsonrpc: "2.0",
28650
30395
  id: message.id,
28651
- error: { code: -32e3, message: error.message }
30396
+ error: {
30397
+ code: -32e3,
30398
+ message: error.message,
30399
+ data: { ...stableDetails, code: stableCode }
30400
+ }
28652
30401
  });
28653
30402
  }
28654
30403
  });
@@ -28656,6 +30405,17 @@ async function startDaemon(args) {
28656
30405
  if (!payload?.runId || !payload.conversationId || typeof payload.content !== "string") {
28657
30406
  return;
28658
30407
  }
30408
+ const isTaskWorkspacePayload = Boolean(
30409
+ payload.worktreeInfo?.worktreePath || payload.worktreeInfo?.allWorktrees?.length
30410
+ );
30411
+ if ((payload.worktreeInfo?.allWorktrees?.length ?? 0) > 1 || isTaskWorkspacePayload && (payload.repoLocalPaths?.length ?? 0) > 1) {
30412
+ socket.emit("agent.rejected", {
30413
+ runId: payload.runId,
30414
+ message: "A task worktree requires exactly one Git repository.",
30415
+ code: "single_repository_worktree_required"
30416
+ });
30417
+ return;
30418
+ }
28659
30419
  const existingRun = activeAgents.get(payload.runId);
28660
30420
  if (existingRun) {
28661
30421
  if (existingRun.conversationId !== payload.conversationId) {
@@ -28748,31 +30508,31 @@ async function startDaemon(args) {
28748
30508
  const workspaceRequired = Boolean(explicitWorkspacePath);
28749
30509
  if (isAlanDefaultWorkspace(payload.projectPath)) {
28750
30510
  try {
28751
- (0, import_node_fs13.mkdirSync)(cwd, { recursive: true });
30511
+ (0, import_node_fs14.mkdirSync)(cwd, { recursive: true });
28752
30512
  } catch {
28753
30513
  }
28754
30514
  }
28755
30515
  let workspaceReadiness = inspectWorkspaceReadiness(workspaceRequired ? cwd : void 0);
28756
30516
  const expectedRepoUrls = [
28757
30517
  ...(payload.repoLocalPaths ?? []).filter(
28758
- (repo) => explicitWorkspacePath ? (0, import_node_path11.resolve)(repo.localPath) === (0, import_node_path11.resolve)(explicitWorkspacePath) : false
30518
+ (repo) => explicitWorkspacePath ? (0, import_node_path12.resolve)(repo.localPath) === (0, import_node_path12.resolve)(explicitWorkspacePath) : false
28759
30519
  ).map((repo) => repo.repoUrl),
28760
30520
  ...payload.localRepoUrls ?? []
28761
30521
  ];
28762
30522
  if (workspaceReadiness.code === "workspace_missing" && explicitWorkspacePath && !payload.worktreeInfo) {
28763
- const previousPath = (0, import_node_path11.resolve)(explicitWorkspacePath);
30523
+ const previousPath = (0, import_node_path12.resolve)(explicitWorkspacePath);
28764
30524
  const remembered = workspaceRelocations.get(previousPath);
28765
30525
  const rememberedPath = remembered?.canonicalPath;
28766
30526
  const relocated = rememberedPath && workspaceMatchesExpectedRepositories({
28767
30527
  workspacePath: rememberedPath,
28768
30528
  expectedRepoUrls
28769
- }) ? (0, import_node_path11.resolve)(rememberedPath) : discoverRelocatedWorkspace({
30529
+ }) ? (0, import_node_path12.resolve)(rememberedPath) : discoverRelocatedWorkspace({
28770
30530
  missingPath: explicitWorkspacePath,
28771
30531
  approvedWorkspacePaths: payload.approvedWorkspacePaths,
28772
30532
  expectedRepoUrls
28773
30533
  });
28774
30534
  if (relocated) {
28775
- if (!remembered || (0, import_node_path11.resolve)(remembered.canonicalPath) !== relocated) {
30535
+ if (!remembered || (0, import_node_path12.resolve)(remembered.canonicalPath) !== relocated) {
28776
30536
  workspaceRelocations.set(previousPath, {
28777
30537
  conversationId: payload.conversationId,
28778
30538
  runId: payload.runId,
@@ -29240,7 +31000,7 @@ async function startDaemon(args) {
29240
31000
  const requestId = payload.requestId;
29241
31001
  try {
29242
31002
  const projectPath = isAlanDefaultWorkspace(payload.projectPath) ? resolveAlanDefaultWorkspacePath() : payload.projectPath;
29243
- const result = scanLocalSkillsCached((0, import_node_os8.homedir)(), projectPath, {
31003
+ const result = scanLocalSkillsCached((0, import_node_os9.homedir)(), projectPath, {
29244
31004
  refresh: payload.refresh === true
29245
31005
  });
29246
31006
  socket.emit("runtime.skills.result", {
@@ -29268,7 +31028,7 @@ async function startDaemon(args) {
29268
31028
  if (!oldest) break;
29269
31029
  adoptionChallenges.delete(oldest);
29270
31030
  }
29271
- const challenge = (0, import_node_crypto3.randomBytes)(24).toString("base64url");
31031
+ const challenge = (0, import_node_crypto5.randomBytes)(24).toString("base64url");
29272
31032
  adoptionChallenges.set(challenge, now + LOCAL_SERVICE_CHALLENGE_TTL_MS);
29273
31033
  const unsignedOwner = {
29274
31034
  version: LOCAL_SERVICE_OWNER_VERSION,
@@ -29471,7 +31231,7 @@ async function startDaemon(args) {
29471
31231
  `${describeOccupiedLocalDaemon(localApiPort)} The token in ${getConfigPath()} no longer matches the running daemon; quit Alan desktop or run setup again.`
29472
31232
  );
29473
31233
  }
29474
- const localApiListening = new Promise((resolve6, reject) => {
31234
+ const localApiListening = new Promise((resolve7, reject) => {
29475
31235
  const onError = (error) => {
29476
31236
  clearInterval(heartbeat);
29477
31237
  clearInterval(versionRefresh);
@@ -29500,7 +31260,7 @@ async function startDaemon(args) {
29500
31260
  if (address && typeof address === "object") localApiPort = address.port;
29501
31261
  pushLog(`local_api listening port=${localApiPort}`);
29502
31262
  console.info("[alan-agent] Local daemon API listening", { port: localApiPort });
29503
- resolve6();
31263
+ resolve7();
29504
31264
  });
29505
31265
  });
29506
31266
  try {
@@ -29519,7 +31279,7 @@ async function startDaemon(args) {
29519
31279
  clearInterval(versionRefresh);
29520
31280
  clearInterval(workspaceLeaseMonitor);
29521
31281
  socket.disconnect();
29522
- await new Promise((resolve6) => localServer.close(() => resolve6()));
31282
+ await new Promise((resolve7) => localServer.close(() => resolve7()));
29523
31283
  releaseOwnerLease();
29524
31284
  releaseStartupLease();
29525
31285
  throw error;
@@ -29538,8 +31298,8 @@ async function startDaemon(args) {
29538
31298
  }
29539
31299
  activeAgents.clear();
29540
31300
  socket.disconnect();
29541
- await new Promise((resolve6) => {
29542
- localServer.close(() => resolve6());
31301
+ await new Promise((resolve7) => {
31302
+ localServer.close(() => resolve7());
29543
31303
  });
29544
31304
  } finally {
29545
31305
  releaseOwnerLease();
@@ -29976,7 +31736,7 @@ function decodeResumeFallbackContext(encoded) {
29976
31736
  }
29977
31737
 
29978
31738
  // src/web-presenter.ts
29979
- var import_node_crypto4 = require("crypto");
31739
+ var import_node_crypto6 = require("crypto");
29980
31740
  var WebPresenter = class {
29981
31741
  constructor(wsClient, conversationId, cwd) {
29982
31742
  this.conversationId = conversationId;
@@ -30153,7 +31913,7 @@ var WebPresenter = class {
30153
31913
  this.applicationProblem = createAlanProblem({
30154
31914
  domain: "application",
30155
31915
  code: /required tools|prepare the required tools/i.test(message) ? "app.service.unavailable" : "app.internal",
30156
- id: (0, import_node_crypto4.randomUUID)(),
31916
+ id: (0, import_node_crypto6.randomUUID)(),
30157
31917
  title: "Alan could not continue the agent run",
30158
31918
  detail: message,
30159
31919
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -30220,7 +31980,7 @@ var WebPresenter = class {
30220
31980
  };
30221
31981
 
30222
31982
  // src/ws-client.ts
30223
- var import_node_crypto6 = require("crypto");
31983
+ var import_node_crypto8 = require("crypto");
30224
31984
 
30225
31985
  // ../shared/dist/agent-liveness.js
30226
31986
  var AGENT_LIVENESS_PROTOCOL_VERSION = 1;
@@ -30248,8 +32008,8 @@ var agentProbeAckSchema = external_exports.object({
30248
32008
  });
30249
32009
 
30250
32010
  // src/sandbox-outbox.ts
30251
- var import_node_crypto5 = require("crypto");
30252
- var import_node_fs14 = require("fs");
32011
+ var import_node_crypto7 = require("crypto");
32012
+ var import_node_fs15 = require("fs");
30253
32013
  var SANDBOX_EVENT_OUTBOX_ENV = "ALAN_EVENT_OUTBOX_ENABLED";
30254
32014
  function isSandboxEventOutboxEnabled(env = process.env) {
30255
32015
  return env[SANDBOX_EVENT_OUTBOX_ENV] === "true";
@@ -30258,7 +32018,7 @@ function deriveSandboxOutboxKey(sessionToken) {
30258
32018
  if (!sessionToken) {
30259
32019
  throw new Error("sandbox event outbox requires ALAN_SESSION_TOKEN for key derivation");
30260
32020
  }
30261
- return (0, import_node_crypto5.createHash)("sha256").update(sessionToken, "utf8").digest("base64url");
32021
+ return (0, import_node_crypto7.createHash)("sha256").update(sessionToken, "utf8").digest("base64url");
30262
32022
  }
30263
32023
  function sandboxOutboxSpoolPath(conversationId) {
30264
32024
  return `/tmp/alan-agent-outbox-${conversationId}.enc`;
@@ -30272,7 +32032,7 @@ function createSandboxEventOutbox(input) {
30272
32032
  input.pushLog?.(
30273
32033
  `sandbox_outbox_spool_reset ${error instanceof Error ? error.message : String(error)}`
30274
32034
  );
30275
- (0, import_node_fs14.rmSync)(path, { force: true });
32035
+ (0, import_node_fs15.rmSync)(path, { force: true });
30276
32036
  return new EncryptedEventOutbox(path, key, input.pushLog, input.options);
30277
32037
  }
30278
32038
  }
@@ -30395,7 +32155,7 @@ var WSClient = class {
30395
32155
  }
30396
32156
  socket;
30397
32157
  /** Stable id for this agent process; fences stale heartbeats server-side. */
30398
- agentSessionId = (0, import_node_crypto6.randomUUID)();
32158
+ agentSessionId = (0, import_node_crypto8.randomUUID)();
30399
32159
  /** Monotonic heartbeat sequence — advances on every hello + heartbeat. */
30400
32160
  heartbeatSeq = 0;
30401
32161
  heartbeatTimer = null;
@@ -30475,9 +32235,9 @@ var WSClient = class {
30475
32235
  });
30476
32236
  }
30477
32237
  waitForConnection(timeoutMs = 15e3) {
30478
- return new Promise((resolve6, reject) => {
32238
+ return new Promise((resolve7, reject) => {
30479
32239
  if (this.socket.connected) {
30480
- resolve6();
32240
+ resolve7();
30481
32241
  return;
30482
32242
  }
30483
32243
  const timeout = setTimeout(
@@ -30486,7 +32246,7 @@ var WSClient = class {
30486
32246
  );
30487
32247
  this.socket.once("connect", () => {
30488
32248
  clearTimeout(timeout);
30489
- resolve6();
32249
+ resolve7();
30490
32250
  });
30491
32251
  this.socket.once("connect_error", (err) => {
30492
32252
  clearTimeout(timeout);
@@ -30667,9 +32427,9 @@ async function runSandbox(config) {
30667
32427
  onUserMessage: (payload) => {
30668
32428
  if (payload === null) {
30669
32429
  if (pendingMessageResolve) {
30670
- const resolve6 = pendingMessageResolve;
32430
+ const resolve7 = pendingMessageResolve;
30671
32431
  pendingMessageResolve = null;
30672
- resolve6(null);
32432
+ resolve7(null);
30673
32433
  }
30674
32434
  return;
30675
32435
  }
@@ -30691,9 +32451,9 @@ async function runSandbox(config) {
30691
32451
  fileCount: payload.files?.length ?? 0
30692
32452
  });
30693
32453
  if (pendingMessageResolve) {
30694
- const resolve6 = pendingMessageResolve;
32454
+ const resolve7 = pendingMessageResolve;
30695
32455
  pendingMessageResolve = null;
30696
- resolve6(payload);
32456
+ resolve7(payload);
30697
32457
  } else {
30698
32458
  queuedMessages.push(payload);
30699
32459
  }
@@ -30886,8 +32646,8 @@ async function runSandbox(config) {
30886
32646
  });
30887
32647
  if (stopped) break;
30888
32648
  }
30889
- const nextPayload = queuedMessages.shift() ?? await new Promise((resolve6) => {
30890
- pendingMessageResolve = resolve6;
32649
+ const nextPayload = queuedMessages.shift() ?? await new Promise((resolve7) => {
32650
+ pendingMessageResolve = resolve7;
30891
32651
  });
30892
32652
  if (nextPayload === null || stopped) {
30893
32653
  break;
@@ -30969,10 +32729,10 @@ async function runSandbox(config) {
30969
32729
  }
30970
32730
 
30971
32731
  // src/service-manager.ts
30972
- var import_node_child_process7 = require("child_process");
30973
- var import_node_fs15 = require("fs");
30974
- var import_node_os9 = require("os");
30975
- var import_node_path12 = require("path");
32732
+ var import_node_child_process8 = require("child_process");
32733
+ var import_node_fs16 = require("fs");
32734
+ var import_node_os10 = require("os");
32735
+ var import_node_path13 = require("path");
30976
32736
  var SERVICE_LABEL = "ai.tryalan.agent";
30977
32737
  var SYSTEMD_UNIT = "alan-agent.service";
30978
32738
  var WINDOWS_TASK = "Alan Agent";
@@ -30992,8 +32752,8 @@ function buildDaemonServicePlan(input) {
30992
32752
  if (userId === void 0) throw new Error("Cannot determine the current macOS user ID");
30993
32753
  const domain = `gui/${userId}`;
30994
32754
  const serviceTarget = `${domain}/${SERVICE_LABEL}`;
30995
- const manifestPath = (0, import_node_path12.join)(input.homeDir, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
30996
- const logDir = (0, import_node_path12.join)(input.homeDir, ".alan", "agent", "logs");
32755
+ const manifestPath = (0, import_node_path13.join)(input.homeDir, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
32756
+ const logDir = (0, import_node_path13.join)(input.homeDir, ".alan", "agent", "logs");
30997
32757
  const programArguments = daemonArgs.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
30998
32758
  const manifest = `<?xml version="1.0" encoding="UTF-8"?>
30999
32759
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -31015,9 +32775,9 @@ ${programArguments}
31015
32775
  <key>ThrottleInterval</key>
31016
32776
  <integer>5</integer>
31017
32777
  <key>StandardOutPath</key>
31018
- <string>${xml((0, import_node_path12.join)(logDir, "daemon.log"))}</string>
32778
+ <string>${xml((0, import_node_path13.join)(logDir, "daemon.log"))}</string>
31019
32779
  <key>StandardErrorPath</key>
31020
- <string>${xml((0, import_node_path12.join)(logDir, "daemon-error.log"))}</string>
32780
+ <string>${xml((0, import_node_path13.join)(logDir, "daemon-error.log"))}</string>
31021
32781
  </dict>
31022
32782
  </plist>
31023
32783
  `;
@@ -31056,7 +32816,7 @@ ${programArguments}
31056
32816
  };
31057
32817
  }
31058
32818
  if (input.platform === "linux") {
31059
- const manifestPath = (0, import_node_path12.join)(input.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
32819
+ const manifestPath = (0, import_node_path13.join)(input.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
31060
32820
  const manifest = `[Unit]
31061
32821
  Description=Alan local agent daemon
31062
32822
  After=network-online.target
@@ -31153,7 +32913,7 @@ WantedBy=default.target
31153
32913
  };
31154
32914
  }
31155
32915
  function currentServicePlan(args) {
31156
- const currentPlatform = (0, import_node_os9.platform)();
32916
+ const currentPlatform = (0, import_node_os10.platform)();
31157
32917
  if (currentPlatform !== "darwin" && currentPlatform !== "linux" && currentPlatform !== "win32") {
31158
32918
  throw new Error(`Daemon service installation is not supported on ${currentPlatform}`);
31159
32919
  }
@@ -31162,25 +32922,25 @@ function currentServicePlan(args) {
31162
32922
  if (!cliEntry) throw new Error("Cannot determine the alan-agent executable path");
31163
32923
  return buildDaemonServicePlan({
31164
32924
  platform: currentPlatform,
31165
- homeDir: (0, import_node_os9.homedir)(),
32925
+ homeDir: (0, import_node_os10.homedir)(),
31166
32926
  nodeExecutable: process.execPath,
31167
- cliEntry: (0, import_node_path12.resolve)(cliEntry),
32927
+ cliEntry: (0, import_node_path13.resolve)(cliEntry),
31168
32928
  profile
31169
32929
  });
31170
32930
  }
31171
32931
  function writeManifest(path, contents) {
31172
- (0, import_node_fs15.mkdirSync)((0, import_node_path12.dirname)(path), { recursive: true, mode: 448 });
32932
+ (0, import_node_fs16.mkdirSync)((0, import_node_path13.dirname)(path), { recursive: true, mode: 448 });
31173
32933
  const pendingPath = `${path}.pending-${process.pid}`;
31174
32934
  try {
31175
- (0, import_node_fs15.writeFileSync)(pendingPath, contents, { mode: 384 });
31176
- (0, import_node_fs15.chmodSync)(pendingPath, 384);
31177
- (0, import_node_fs15.renameSync)(pendingPath, path);
32935
+ (0, import_node_fs16.writeFileSync)(pendingPath, contents, { mode: 384 });
32936
+ (0, import_node_fs16.chmodSync)(pendingPath, 384);
32937
+ (0, import_node_fs16.renameSync)(pendingPath, path);
31178
32938
  } finally {
31179
- (0, import_node_fs15.rmSync)(pendingPath, { force: true });
32939
+ (0, import_node_fs16.rmSync)(pendingPath, { force: true });
31180
32940
  }
31181
32941
  }
31182
32942
  function runServiceCommand(command) {
31183
- const result = (0, import_node_child_process7.spawnSync)(command.command, command.args, { encoding: "utf8" });
32943
+ const result = (0, import_node_child_process8.spawnSync)(command.command, command.args, { encoding: "utf8" });
31184
32944
  const status = result.status ?? 1;
31185
32945
  const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
31186
32946
  if (status !== 0 && !command.tolerateFailure) {
@@ -31198,8 +32958,8 @@ function installDaemonService(args = []) {
31198
32958
  }
31199
32959
  const plan = currentServicePlan(args);
31200
32960
  if (plan.manifestPath && plan.manifest) {
31201
- if ((0, import_node_os9.platform)() === "darwin") {
31202
- (0, import_node_fs15.mkdirSync)((0, import_node_path12.join)((0, import_node_os9.homedir)(), ".alan", "agent", "logs"), { recursive: true, mode: 448 });
32961
+ if ((0, import_node_os10.platform)() === "darwin") {
32962
+ (0, import_node_fs16.mkdirSync)((0, import_node_path13.join)((0, import_node_os10.homedir)(), ".alan", "agent", "logs"), { recursive: true, mode: 448 });
31203
32963
  }
31204
32964
  writeManifest(plan.manifestPath, plan.manifest);
31205
32965
  }
@@ -31209,7 +32969,7 @@ function installDaemonService(args = []) {
31209
32969
  function uninstallDaemonService(args = []) {
31210
32970
  const plan = currentServicePlan(args);
31211
32971
  for (const command of plan.uninstallCommands) runServiceCommand(command);
31212
- if (plan.manifestPath) (0, import_node_fs15.rmSync)(plan.manifestPath, { force: true });
32972
+ if (plan.manifestPath) (0, import_node_fs16.rmSync)(plan.manifestPath, { force: true });
31213
32973
  console.info("[alan-agent] Per-user daemon service removed");
31214
32974
  }
31215
32975
  function printDaemonServiceStatus(args = []) {
@@ -31220,6 +32980,42 @@ function printDaemonServiceStatus(args = []) {
31220
32980
  }
31221
32981
 
31222
32982
  // src/index.ts
32983
+ function logEffectiveGitIdentity(lifecycle, projectPath) {
32984
+ const readConfig2 = (key) => {
32985
+ try {
32986
+ const result = (0, import_node_child_process9.spawnSync)("git", ["config", "--get", key], {
32987
+ cwd: projectPath,
32988
+ env: getDaemonCliEnvironment(),
32989
+ encoding: "utf8",
32990
+ timeout: 5e3
32991
+ });
32992
+ const value2 = result.status === 0 ? result.stdout.trim() : "";
32993
+ return value2 || null;
32994
+ } catch {
32995
+ return null;
32996
+ }
32997
+ };
32998
+ const envName = process.env.GIT_AUTHOR_NAME || null;
32999
+ const envEmail = process.env.GIT_AUTHOR_EMAIL || null;
33000
+ const configName = readConfig2("user.name");
33001
+ const configEmail = readConfig2("user.email");
33002
+ const payload = {
33003
+ projectPath,
33004
+ envAuthorName: envName,
33005
+ envAuthorEmail: envEmail,
33006
+ configUserName: configName,
33007
+ configUserEmail: configEmail
33008
+ };
33009
+ if (!envEmail && !configEmail) {
33010
+ lifecycle.error("sandbox_agent_git_identity_missing", payload);
33011
+ return;
33012
+ }
33013
+ if (envEmail && configEmail && envEmail !== configEmail) {
33014
+ lifecycle.error("sandbox_agent_git_identity_mismatch", payload);
33015
+ return;
33016
+ }
33017
+ lifecycle.info("sandbox_agent_git_identity", payload);
33018
+ }
31223
33019
  async function runSessionFromEnv() {
31224
33020
  const wsUrl = process.env.ALAN_WS_URL;
31225
33021
  const sessionId = process.env.ALAN_SESSION_ID;
@@ -31264,6 +33060,7 @@ async function runSessionFromEnv() {
31264
33060
  mode,
31265
33061
  model
31266
33062
  });
33063
+ logEffectiveGitIdentity(lifecycle, projectPath);
31267
33064
  await runSandbox({
31268
33065
  wsUrl,
31269
33066
  sessionId,