@locusai/cli 0.18.0 → 0.18.1

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/bin/locus.js +492 -323
  2. package/package.json +1 -1
package/bin/locus.js CHANGED
@@ -807,6 +807,14 @@ var init_rate_limiter = __esm(() => {
807
807
  });
808
808
 
809
809
  // src/display/progress.ts
810
+ var exports_progress = {};
811
+ __export(exports_progress, {
812
+ renderTaskStatus: () => renderTaskStatus,
813
+ progressBar: () => progressBar,
814
+ formatDuration: () => formatDuration,
815
+ createTimer: () => createTimer,
816
+ Spinner: () => Spinner
817
+ });
810
818
  function progressBar(current, total, options = {}) {
811
819
  const { width = 30, showPercent = true, showCount = true, label } = options;
812
820
  const percent = total > 0 ? current / total : 0;
@@ -903,6 +911,149 @@ var init_progress = __esm(() => {
903
911
  SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
904
912
  });
905
913
 
914
+ // src/core/sandbox.ts
915
+ var exports_sandbox = {};
916
+ __export(exports_sandbox, {
917
+ resolveSandboxMode: () => resolveSandboxMode,
918
+ displaySandboxWarning: () => displaySandboxWarning,
919
+ detectSandboxSupport: () => detectSandboxSupport
920
+ });
921
+ import { execFile } from "node:child_process";
922
+ import { createInterface } from "node:readline";
923
+ async function detectSandboxSupport() {
924
+ if (cachedStatus)
925
+ return cachedStatus;
926
+ const log = getLogger();
927
+ log.debug("Detecting Docker sandbox support...");
928
+ const status = await runDetection();
929
+ cachedStatus = status;
930
+ if (status.available) {
931
+ log.verbose("Docker sandbox support detected");
932
+ } else {
933
+ log.verbose(`Docker sandbox not available: ${status.reason}`);
934
+ }
935
+ return status;
936
+ }
937
+ function runDetection() {
938
+ return new Promise((resolve) => {
939
+ let settled = false;
940
+ const child = execFile("docker", ["sandbox", "ls"], { timeout: TIMEOUT_MS }, (error, _stdout, stderr) => {
941
+ if (settled)
942
+ return;
943
+ settled = true;
944
+ if (!error) {
945
+ resolve({ available: true });
946
+ return;
947
+ }
948
+ const code = error.code;
949
+ if (code === "ENOENT") {
950
+ resolve({ available: false, reason: "Docker is not installed" });
951
+ return;
952
+ }
953
+ if (error.killed) {
954
+ resolve({ available: false, reason: "Docker is not responding" });
955
+ return;
956
+ }
957
+ const stderrStr = (stderr ?? "").toLowerCase();
958
+ if (stderrStr.includes("unknown") || stderrStr.includes("not a docker command") || stderrStr.includes("is not a docker command")) {
959
+ resolve({
960
+ available: false,
961
+ reason: "Docker Desktop 4.58+ with sandbox support required"
962
+ });
963
+ return;
964
+ }
965
+ resolve({
966
+ available: false,
967
+ reason: "Docker Desktop 4.58+ with sandbox support required"
968
+ });
969
+ });
970
+ child.on?.("error", (err) => {
971
+ if (settled)
972
+ return;
973
+ settled = true;
974
+ if (err.code === "ENOENT") {
975
+ resolve({ available: false, reason: "Docker is not installed" });
976
+ } else {
977
+ resolve({
978
+ available: false,
979
+ reason: "Docker Desktop 4.58+ with sandbox support required"
980
+ });
981
+ }
982
+ });
983
+ });
984
+ }
985
+ function resolveSandboxMode(config, flags) {
986
+ if (flags.noSandbox) {
987
+ return "disabled";
988
+ }
989
+ if (flags.sandbox !== undefined) {
990
+ if (flags.sandbox === "require") {
991
+ return "required";
992
+ }
993
+ throw new Error(`Invalid --sandbox value: "${flags.sandbox}". Valid values: require`);
994
+ }
995
+ if (!config.enabled) {
996
+ return "disabled";
997
+ }
998
+ return "auto";
999
+ }
1000
+ async function displaySandboxWarning(mode, status) {
1001
+ if (mode === "required" && !status.available) {
1002
+ process.stderr.write(`
1003
+ ${red("✖")} Docker sandbox required but not available: ${bold(status.reason ?? "Docker Desktop 4.58+ with sandbox support required")}
1004
+ `);
1005
+ process.stderr.write(` Install Docker Desktop 4.58+ or remove --sandbox=require to continue.
1006
+
1007
+ `);
1008
+ process.exit(1);
1009
+ }
1010
+ if (mode === "disabled") {
1011
+ process.stderr.write(`
1012
+ ${yellow("⚠")} ${bold("WARNING:")} Running without sandbox. The AI agent will have unrestricted
1013
+ `);
1014
+ process.stderr.write(` access to your filesystem, network, and environment variables.
1015
+ `);
1016
+ if (process.stdin.isTTY) {
1017
+ process.stderr.write(` Press ${bold("Enter")} to continue or ${bold("Ctrl+C")} to abort.
1018
+ `);
1019
+ await waitForEnter();
1020
+ }
1021
+ process.stderr.write(`
1022
+ `);
1023
+ return false;
1024
+ }
1025
+ if (mode === "auto" && !status.available) {
1026
+ process.stderr.write(`
1027
+ ${yellow("⚠")} Docker sandbox not available. Install Docker Desktop 4.58+ for secure execution.
1028
+ `);
1029
+ process.stderr.write(` Running without sandbox. Use ${dim("--no-sandbox")} to suppress this warning.
1030
+
1031
+ `);
1032
+ return false;
1033
+ }
1034
+ return true;
1035
+ }
1036
+ function waitForEnter() {
1037
+ return new Promise((resolve) => {
1038
+ const rl = createInterface({
1039
+ input: process.stdin,
1040
+ output: process.stderr
1041
+ });
1042
+ rl.once("line", () => {
1043
+ rl.close();
1044
+ resolve();
1045
+ });
1046
+ rl.once("close", () => {
1047
+ resolve();
1048
+ });
1049
+ });
1050
+ }
1051
+ var TIMEOUT_MS = 5000, cachedStatus = null;
1052
+ var init_sandbox = __esm(() => {
1053
+ init_terminal();
1054
+ init_logger();
1055
+ });
1056
+
906
1057
  // src/commands/upgrade.ts
907
1058
  var exports_upgrade = {};
908
1059
  __export(exports_upgrade, {
@@ -1840,7 +1991,8 @@ var init_init = __esm(() => {
1840
1991
  ".locus/logs/",
1841
1992
  ".locus/worktrees/",
1842
1993
  ".locus/artifacts/",
1843
- ".locus/discussions/"
1994
+ ".locus/discussions/",
1995
+ ".locus/tmp/"
1844
1996
  ];
1845
1997
  });
1846
1998
 
@@ -2911,14 +3063,87 @@ var init_stream_renderer = __esm(() => {
2911
3063
  init_terminal();
2912
3064
  });
2913
3065
 
3066
+ // src/repl/clipboard.ts
3067
+ import { execSync as execSync4 } from "node:child_process";
3068
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "node:fs";
3069
+ import { tmpdir } from "node:os";
3070
+ import { join as join9 } from "node:path";
3071
+ function readClipboardImage() {
3072
+ if (process.platform === "darwin") {
3073
+ return readMacOSClipboardImage();
3074
+ }
3075
+ if (process.platform === "linux") {
3076
+ return readLinuxClipboardImage();
3077
+ }
3078
+ return null;
3079
+ }
3080
+ function ensureStableDir() {
3081
+ if (!existsSync10(STABLE_DIR)) {
3082
+ mkdirSync7(STABLE_DIR, { recursive: true });
3083
+ }
3084
+ }
3085
+ function readMacOSClipboardImage() {
3086
+ try {
3087
+ ensureStableDir();
3088
+ const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
3089
+ const script = [
3090
+ `set destPath to POSIX file "${destPath}"`,
3091
+ "try",
3092
+ ` set imgData to the clipboard as «class PNGf»`,
3093
+ "on error",
3094
+ " try",
3095
+ ` set imgData to the clipboard as «class TIFF»`,
3096
+ " on error",
3097
+ ` return "no-image"`,
3098
+ " end try",
3099
+ "end try",
3100
+ "set fRef to open for access destPath with write permission",
3101
+ "write imgData to fRef",
3102
+ "close access fRef",
3103
+ `return "ok"`
3104
+ ].join(`
3105
+ `);
3106
+ const result = execSync4("osascript", {
3107
+ input: script,
3108
+ encoding: "utf-8",
3109
+ timeout: 5000,
3110
+ stdio: ["pipe", "pipe", "pipe"]
3111
+ }).trim();
3112
+ if (result === "ok" && existsSync10(destPath)) {
3113
+ return destPath;
3114
+ }
3115
+ } catch {}
3116
+ return null;
3117
+ }
3118
+ function readLinuxClipboardImage() {
3119
+ try {
3120
+ const targets = execSync4("xclip -selection clipboard -t TARGETS -o 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
3121
+ if (!targets.includes("image/png")) {
3122
+ return null;
3123
+ }
3124
+ ensureStableDir();
3125
+ const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
3126
+ execSync4(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
3127
+ if (existsSync10(destPath)) {
3128
+ return destPath;
3129
+ }
3130
+ } catch {}
3131
+ return null;
3132
+ }
3133
+ var STABLE_DIR;
3134
+ var init_clipboard = __esm(() => {
3135
+ STABLE_DIR = join9(tmpdir(), "locus-images");
3136
+ });
3137
+
2914
3138
  // src/repl/image-detect.ts
2915
- import { copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "node:fs";
2916
- import { homedir as homedir3, tmpdir } from "node:os";
2917
- import { basename, extname, join as join9, resolve } from "node:path";
3139
+ import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "node:fs";
3140
+ import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
3141
+ import { basename, extname, join as join10, resolve } from "node:path";
2918
3142
  function detectImages(input) {
2919
3143
  const detected = [];
2920
3144
  const byResolved = new Map;
2921
- for (const line of input.split(`
3145
+ const sanitized = input.replace(/!\[Screenshot:[^\]]*\]\(locus:\/\/screenshot-\d+\)/g, "");
3146
+ for (const line of sanitized.split(`
2922
3147
  `)) {
2923
3148
  const trimmed = line.trim();
2924
3149
  if (!trimmed)
@@ -2929,20 +3154,20 @@ function detectImages(input) {
2929
3154
  }
2930
3155
  }
2931
3156
  const quotedPattern = /["']([^"']+\.(?:png|jpg|jpeg|gif|webp|bmp|svg))["']/gi;
2932
- for (const match of input.matchAll(quotedPattern)) {
3157
+ for (const match of sanitized.matchAll(quotedPattern)) {
2933
3158
  if (!match[0] || !match[1])
2934
3159
  continue;
2935
3160
  addIfImage(match[1], match[0], detected, byResolved);
2936
3161
  }
2937
3162
  const escapedPattern = /(?:\/|~\/|\.\/)?(?:[^\s"'\\]|\\ )+\.(?:png|jpg|jpeg|gif|webp|bmp|svg|tiff?)/gi;
2938
- for (const match of input.matchAll(escapedPattern)) {
3163
+ for (const match of sanitized.matchAll(escapedPattern)) {
2939
3164
  if (!match[0])
2940
3165
  continue;
2941
3166
  const path = match[0].replace(/\\ /g, " ");
2942
3167
  addIfImage(path, match[0], detected, byResolved);
2943
3168
  }
2944
3169
  const plainPattern = /(?:\/|~\/|\.\/)[^\s"']+\.(?:png|jpg|jpeg|gif|webp|bmp|svg|tiff?)/gi;
2945
- for (const match of input.matchAll(plainPattern)) {
3170
+ for (const match of sanitized.matchAll(plainPattern)) {
2946
3171
  if (!match[0])
2947
3172
  continue;
2948
3173
  addIfImage(match[0], match[0], detected, byResolved);
@@ -3006,13 +3231,28 @@ function collectReferencedAttachments(input, attachments) {
3006
3231
  const selected = attachments.filter((attachment) => ids.has(attachment.id));
3007
3232
  return dedupeByResolvedPath(selected);
3008
3233
  }
3234
+ function relocateImages(images, projectRoot) {
3235
+ const targetDir = join10(projectRoot, ".locus", "tmp", "images");
3236
+ for (const img of images) {
3237
+ if (!img.exists)
3238
+ continue;
3239
+ try {
3240
+ if (!existsSync11(targetDir)) {
3241
+ mkdirSync8(targetDir, { recursive: true });
3242
+ }
3243
+ const dest = join10(targetDir, basename(img.stablePath));
3244
+ copyFileSync(img.stablePath, dest);
3245
+ img.stablePath = dest;
3246
+ } catch {}
3247
+ }
3248
+ }
3009
3249
  function addIfImage(rawPath, rawMatch, detected, byResolved) {
3010
3250
  const ext = extname(rawPath).toLowerCase();
3011
3251
  if (!IMAGE_EXTENSIONS.has(ext))
3012
3252
  return;
3013
3253
  let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
3014
3254
  if (resolved.startsWith("~/")) {
3015
- resolved = join9(homedir3(), resolved.slice(2));
3255
+ resolved = join10(homedir3(), resolved.slice(2));
3016
3256
  }
3017
3257
  resolved = resolve(resolved);
3018
3258
  const existing = byResolved.get(resolved);
@@ -3025,7 +3265,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
3025
3265
  ]);
3026
3266
  return;
3027
3267
  }
3028
- const exists = existsSync10(resolved);
3268
+ const exists = existsSync11(resolved);
3029
3269
  let stablePath = resolved;
3030
3270
  if (exists) {
3031
3271
  stablePath = copyToStable(resolved);
@@ -3079,17 +3319,17 @@ function dedupeByResolvedPath(images) {
3079
3319
  }
3080
3320
  function copyToStable(sourcePath) {
3081
3321
  try {
3082
- if (!existsSync10(STABLE_DIR)) {
3083
- mkdirSync7(STABLE_DIR, { recursive: true });
3322
+ if (!existsSync11(STABLE_DIR2)) {
3323
+ mkdirSync8(STABLE_DIR2, { recursive: true });
3084
3324
  }
3085
- const dest = join9(STABLE_DIR, `${Date.now()}-${basename(sourcePath)}`);
3325
+ const dest = join10(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
3086
3326
  copyFileSync(sourcePath, dest);
3087
3327
  return dest;
3088
3328
  } catch {
3089
3329
  return sourcePath;
3090
3330
  }
3091
3331
  }
3092
- var IMAGE_EXTENSIONS, STABLE_DIR, PLACEHOLDER_SCHEME = "locus://screenshot-", PLACEHOLDER_ID_PATTERN;
3332
+ var IMAGE_EXTENSIONS, STABLE_DIR2, PLACEHOLDER_SCHEME = "locus://screenshot-", PLACEHOLDER_ID_PATTERN;
3093
3333
  var init_image_detect = __esm(() => {
3094
3334
  IMAGE_EXTENSIONS = new Set([
3095
3335
  ".png",
@@ -3102,7 +3342,7 @@ var init_image_detect = __esm(() => {
3102
3342
  ".tif",
3103
3343
  ".tiff"
3104
3344
  ]);
3105
- STABLE_DIR = join9(tmpdir(), "locus-images");
3345
+ STABLE_DIR2 = join10(tmpdir2(), "locus-images");
3106
3346
  PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
3107
3347
  });
3108
3348
 
@@ -3521,6 +3761,15 @@ ${dim("Press Ctrl+C again to exit")}\r
3521
3761
  pasteBuffer += pending.slice(0, endIdx);
3522
3762
  pending = pending.slice(endIdx + PASTE_END.length);
3523
3763
  isPasting = false;
3764
+ if (pasteBuffer.trim() === "") {
3765
+ const clipboardImagePath = readClipboardImage();
3766
+ if (clipboardImagePath) {
3767
+ insertText(clipboardImagePath);
3768
+ pasteBuffer = "";
3769
+ render();
3770
+ continue;
3771
+ }
3772
+ }
3524
3773
  insertText(normalizeLineEndings(pasteBuffer));
3525
3774
  pasteBuffer = "";
3526
3775
  render();
@@ -3812,6 +4061,7 @@ var CSI = "\x1B[", SAVE_CURSOR = "\x1B7", RESTORE_CURSOR = "\x1B8", ENABLE_BRACK
3812
4061
  `, ESC = "\x1B", BACKSPACE = "", SEQ_LEFT, SEQ_RIGHT, SEQ_UP, SEQ_DOWN, SEQ_HOME, SEQ_END, SEQ_HOME_1, SEQ_END_4, SEQ_HOME_O = "\x1BOH", SEQ_END_O = "\x1BOF", SEQ_DELETE, SEQ_WORD_LEFT, SEQ_WORD_RIGHT, SEQ_SHIFT_LEFT, SEQ_SHIFT_RIGHT, SEQ_META_LEFT, SEQ_META_RIGHT, SEQ_META_SHIFT_LEFT, SEQ_META_SHIFT_RIGHT, SEQ_SHIFT_ENTER_CSI_U, SEQ_SHIFT_ENTER_MODIFY, SEQ_SHIFT_ENTER_TILDE, SEQ_ALT_ENTER, CONTROL_SEQUENCES;
3813
4062
  var init_input_handler = __esm(() => {
3814
4063
  init_terminal();
4064
+ init_clipboard();
3815
4065
  init_image_detect();
3816
4066
  ENABLE_BRACKETED_PASTE = `${CSI}?2004h`;
3817
4067
  DISABLE_BRACKETED_PASTE = `${CSI}?2004l`;
@@ -3874,7 +4124,7 @@ __export(exports_claude, {
3874
4124
  buildClaudeArgs: () => buildClaudeArgs,
3875
4125
  ClaudeRunner: () => ClaudeRunner
3876
4126
  });
3877
- import { execSync as execSync4, spawn as spawn2 } from "node:child_process";
4127
+ import { execSync as execSync5, spawn as spawn2 } from "node:child_process";
3878
4128
  function buildClaudeArgs(options) {
3879
4129
  const args = [
3880
4130
  "--dangerously-skip-permissions",
@@ -3895,7 +4145,7 @@ class ClaudeRunner {
3895
4145
  aborted = false;
3896
4146
  async isAvailable() {
3897
4147
  try {
3898
- execSync4("claude --version", {
4148
+ execSync5("claude --version", {
3899
4149
  encoding: "utf-8",
3900
4150
  stdio: ["pipe", "pipe", "pipe"]
3901
4151
  });
@@ -3906,7 +4156,7 @@ class ClaudeRunner {
3906
4156
  }
3907
4157
  async getVersion() {
3908
4158
  try {
3909
- const output = execSync4("claude --version", {
4159
+ const output = execSync5("claude --version", {
3910
4160
  encoding: "utf-8",
3911
4161
  stdio: ["pipe", "pipe", "pipe"]
3912
4162
  }).trim();
@@ -4071,11 +4321,11 @@ var init_claude = __esm(() => {
4071
4321
 
4072
4322
  // src/core/sandbox-ignore.ts
4073
4323
  import { exec } from "node:child_process";
4074
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
4075
- import { join as join10 } from "node:path";
4324
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
4325
+ import { join as join11 } from "node:path";
4076
4326
  import { promisify } from "node:util";
4077
4327
  function parseIgnoreFile(filePath) {
4078
- if (!existsSync11(filePath))
4328
+ if (!existsSync12(filePath))
4079
4329
  return [];
4080
4330
  const content = readFileSync8(filePath, "utf-8");
4081
4331
  const rules = [];
@@ -4122,7 +4372,7 @@ function buildCleanupScript(rules, workspacePath) {
4122
4372
  }
4123
4373
  async function enforceSandboxIgnore(sandboxName, projectRoot) {
4124
4374
  const log = getLogger();
4125
- const ignorePath = join10(projectRoot, ".sandboxignore");
4375
+ const ignorePath = join11(projectRoot, ".sandboxignore");
4126
4376
  const rules = parseIgnoreFile(ignorePath);
4127
4377
  if (rules.length === 0)
4128
4378
  return;
@@ -4151,19 +4401,19 @@ var init_sandbox_ignore = __esm(() => {
4151
4401
 
4152
4402
  // src/core/run-state.ts
4153
4403
  import {
4154
- existsSync as existsSync12,
4155
- mkdirSync as mkdirSync8,
4404
+ existsSync as existsSync13,
4405
+ mkdirSync as mkdirSync9,
4156
4406
  readFileSync as readFileSync9,
4157
4407
  unlinkSync as unlinkSync3,
4158
4408
  writeFileSync as writeFileSync6
4159
4409
  } from "node:fs";
4160
- import { dirname as dirname3, join as join11 } from "node:path";
4410
+ import { dirname as dirname3, join as join12 } from "node:path";
4161
4411
  function getRunStatePath(projectRoot) {
4162
- return join11(projectRoot, ".locus", "run-state.json");
4412
+ return join12(projectRoot, ".locus", "run-state.json");
4163
4413
  }
4164
4414
  function loadRunState(projectRoot) {
4165
4415
  const path = getRunStatePath(projectRoot);
4166
- if (!existsSync12(path))
4416
+ if (!existsSync13(path))
4167
4417
  return null;
4168
4418
  try {
4169
4419
  return JSON.parse(readFileSync9(path, "utf-8"));
@@ -4175,15 +4425,15 @@ function loadRunState(projectRoot) {
4175
4425
  function saveRunState(projectRoot, state) {
4176
4426
  const path = getRunStatePath(projectRoot);
4177
4427
  const dir = dirname3(path);
4178
- if (!existsSync12(dir)) {
4179
- mkdirSync8(dir, { recursive: true });
4428
+ if (!existsSync13(dir)) {
4429
+ mkdirSync9(dir, { recursive: true });
4180
4430
  }
4181
4431
  writeFileSync6(path, `${JSON.stringify(state, null, 2)}
4182
4432
  `, "utf-8");
4183
4433
  }
4184
4434
  function clearRunState(projectRoot) {
4185
4435
  const path = getRunStatePath(projectRoot);
4186
- if (existsSync12(path)) {
4436
+ if (existsSync13(path)) {
4187
4437
  unlinkSync3(path);
4188
4438
  }
4189
4439
  }
@@ -4257,7 +4507,7 @@ var init_run_state = __esm(() => {
4257
4507
  });
4258
4508
 
4259
4509
  // src/core/shutdown.ts
4260
- import { execSync as execSync5 } from "node:child_process";
4510
+ import { execSync as execSync6 } from "node:child_process";
4261
4511
  function registerActiveSandbox(name) {
4262
4512
  activeSandboxes.add(name);
4263
4513
  }
@@ -4267,7 +4517,7 @@ function unregisterActiveSandbox(name) {
4267
4517
  function cleanupActiveSandboxes() {
4268
4518
  for (const name of activeSandboxes) {
4269
4519
  try {
4270
- execSync5(`docker sandbox rm ${name}`, { timeout: 1e4 });
4520
+ execSync6(`docker sandbox rm ${name}`, { timeout: 1e4 });
4271
4521
  } catch {}
4272
4522
  }
4273
4523
  activeSandboxes.clear();
@@ -4340,7 +4590,7 @@ var init_shutdown = __esm(() => {
4340
4590
  });
4341
4591
 
4342
4592
  // src/ai/claude-sandbox.ts
4343
- import { execSync as execSync6, spawn as spawn3 } from "node:child_process";
4593
+ import { execSync as execSync7, spawn as spawn3 } from "node:child_process";
4344
4594
 
4345
4595
  class SandboxedClaudeRunner {
4346
4596
  name = "claude-sandboxed";
@@ -4556,7 +4806,7 @@ class SandboxedClaudeRunner {
4556
4806
  sandboxName: this.sandboxName
4557
4807
  });
4558
4808
  try {
4559
- execSync6(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
4809
+ execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
4560
4810
  } catch {}
4561
4811
  }
4562
4812
  }
@@ -4570,7 +4820,7 @@ class SandboxedClaudeRunner {
4570
4820
  const log = getLogger();
4571
4821
  log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
4572
4822
  try {
4573
- execSync6(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
4823
+ execSync7(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
4574
4824
  } catch {}
4575
4825
  unregisterActiveSandbox(this.sandboxName);
4576
4826
  this.sandboxName = null;
@@ -4582,7 +4832,7 @@ class SandboxedClaudeRunner {
4582
4832
  const log = getLogger();
4583
4833
  log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
4584
4834
  try {
4585
- execSync6(`docker sandbox rm ${this.sandboxName}`, {
4835
+ execSync7(`docker sandbox rm ${this.sandboxName}`, {
4586
4836
  timeout: 60000
4587
4837
  });
4588
4838
  } catch {}
@@ -4658,7 +4908,7 @@ var init_claude_sandbox = __esm(() => {
4658
4908
  });
4659
4909
 
4660
4910
  // src/ai/codex.ts
4661
- import { execSync as execSync7, spawn as spawn4 } from "node:child_process";
4911
+ import { execSync as execSync8, spawn as spawn4 } from "node:child_process";
4662
4912
  function buildCodexArgs(model) {
4663
4913
  const args = ["exec", "--full-auto", "--skip-git-repo-check", "--json"];
4664
4914
  if (model) {
@@ -4674,7 +4924,7 @@ class CodexRunner {
4674
4924
  aborted = false;
4675
4925
  async isAvailable() {
4676
4926
  try {
4677
- execSync7("codex --version", {
4927
+ execSync8("codex --version", {
4678
4928
  encoding: "utf-8",
4679
4929
  stdio: ["pipe", "pipe", "pipe"]
4680
4930
  });
@@ -4685,7 +4935,7 @@ class CodexRunner {
4685
4935
  }
4686
4936
  async getVersion() {
4687
4937
  try {
4688
- const output = execSync7("codex --version", {
4938
+ const output = execSync8("codex --version", {
4689
4939
  encoding: "utf-8",
4690
4940
  stdio: ["pipe", "pipe", "pipe"]
4691
4941
  }).trim();
@@ -4833,7 +5083,7 @@ var init_codex = __esm(() => {
4833
5083
  });
4834
5084
 
4835
5085
  // src/ai/codex-sandbox.ts
4836
- import { execSync as execSync8, spawn as spawn5 } from "node:child_process";
5086
+ import { execSync as execSync9, spawn as spawn5 } from "node:child_process";
4837
5087
 
4838
5088
  class SandboxedCodexRunner {
4839
5089
  name = "codex-sandboxed";
@@ -5076,7 +5326,7 @@ class SandboxedCodexRunner {
5076
5326
  sandboxName: this.sandboxName
5077
5327
  });
5078
5328
  try {
5079
- execSync8(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
5329
+ execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
5080
5330
  } catch {}
5081
5331
  }
5082
5332
  }
@@ -5090,7 +5340,7 @@ class SandboxedCodexRunner {
5090
5340
  const log = getLogger();
5091
5341
  log.debug("Destroying sandbox", { sandboxName: this.sandboxName });
5092
5342
  try {
5093
- execSync8(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
5343
+ execSync9(`docker sandbox rm ${this.sandboxName}`, { timeout: 60000 });
5094
5344
  } catch {}
5095
5345
  unregisterActiveSandbox(this.sandboxName);
5096
5346
  this.sandboxName = null;
@@ -5102,7 +5352,7 @@ class SandboxedCodexRunner {
5102
5352
  const log = getLogger();
5103
5353
  log.debug("Cleaning up sandbox", { sandboxName: this.sandboxName });
5104
5354
  try {
5105
- execSync8(`docker sandbox rm ${this.sandboxName}`, {
5355
+ execSync9(`docker sandbox rm ${this.sandboxName}`, {
5106
5356
  timeout: 60000
5107
5357
  });
5108
5358
  } catch {}
@@ -5493,7 +5743,7 @@ var exports_issue = {};
5493
5743
  __export(exports_issue, {
5494
5744
  issueCommand: () => issueCommand
5495
5745
  });
5496
- import { createInterface } from "node:readline";
5746
+ import { createInterface as createInterface2 } from "node:readline";
5497
5747
  function parseIssueArgs(args) {
5498
5748
  const flags = {};
5499
5749
  const positional = [];
@@ -5726,7 +5976,7 @@ function extractJSON(text) {
5726
5976
  }
5727
5977
  function askQuestion(question) {
5728
5978
  return new Promise((resolve2) => {
5729
- const rl = createInterface({
5979
+ const rl = createInterface2({
5730
5980
  input: process.stdin,
5731
5981
  output: process.stderr
5732
5982
  });
@@ -6765,9 +7015,9 @@ var init_sprint = __esm(() => {
6765
7015
  });
6766
7016
 
6767
7017
  // src/core/prompt-builder.ts
6768
- import { execSync as execSync9 } from "node:child_process";
6769
- import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "node:fs";
6770
- import { join as join12 } from "node:path";
7018
+ import { execSync as execSync10 } from "node:child_process";
7019
+ import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "node:fs";
7020
+ import { join as join13 } from "node:path";
6771
7021
  function buildExecutionPrompt(ctx) {
6772
7022
  const sections = [];
6773
7023
  sections.push(buildSystemContext(ctx.projectRoot));
@@ -6797,13 +7047,13 @@ function buildFeedbackPrompt(ctx) {
6797
7047
  }
6798
7048
  function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
6799
7049
  const sections = [];
6800
- const locusmd = readFileSafe(join12(projectRoot, "LOCUS.md"));
7050
+ const locusmd = readFileSafe(join13(projectRoot, "LOCUS.md"));
6801
7051
  if (locusmd) {
6802
7052
  sections.push(`# Project Instructions
6803
7053
 
6804
7054
  ${locusmd}`);
6805
7055
  }
6806
- const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
7056
+ const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
6807
7057
  if (learnings) {
6808
7058
  sections.push(`# Past Learnings
6809
7059
 
@@ -6829,24 +7079,24 @@ ${userMessage}`);
6829
7079
  }
6830
7080
  function buildSystemContext(projectRoot) {
6831
7081
  const parts = ["# System Context"];
6832
- const locusmd = readFileSafe(join12(projectRoot, "LOCUS.md"));
7082
+ const locusmd = readFileSafe(join13(projectRoot, "LOCUS.md"));
6833
7083
  if (locusmd) {
6834
7084
  parts.push(`## Project Instructions (LOCUS.md)
6835
7085
 
6836
7086
  ${locusmd}`);
6837
7087
  }
6838
- const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
7088
+ const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
6839
7089
  if (learnings) {
6840
7090
  parts.push(`## Past Learnings
6841
7091
 
6842
7092
  ${learnings}`);
6843
7093
  }
6844
- const discussionsDir = join12(projectRoot, ".locus", "discussions");
6845
- if (existsSync13(discussionsDir)) {
7094
+ const discussionsDir = join13(projectRoot, ".locus", "discussions");
7095
+ if (existsSync14(discussionsDir)) {
6846
7096
  try {
6847
7097
  const files = readdirSync3(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
6848
7098
  for (const file of files) {
6849
- const content = readFileSafe(join12(discussionsDir, file));
7099
+ const content = readFileSafe(join13(discussionsDir, file));
6850
7100
  if (content) {
6851
7101
  parts.push(`## Discussion: ${file.replace(".md", "")}
6852
7102
 
@@ -6909,7 +7159,7 @@ ${diffSummary}
6909
7159
  function buildRepoContext(projectRoot) {
6910
7160
  const parts = ["# Repository Context"];
6911
7161
  try {
6912
- const tree = execSync9("find . -maxdepth 2 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.locus/*' -not -path '*/dist/*' -not -path '*/build/*' | head -80", { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
7162
+ const tree = execSync10("find . -maxdepth 2 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.locus/*' -not -path '*/dist/*' -not -path '*/build/*' | head -80", { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
6913
7163
  if (tree) {
6914
7164
  parts.push(`## File Tree
6915
7165
 
@@ -6919,7 +7169,7 @@ ${tree}
6919
7169
  }
6920
7170
  } catch {}
6921
7171
  try {
6922
- const gitLog = execSync9("git log --oneline -10", {
7172
+ const gitLog = execSync10("git log --oneline -10", {
6923
7173
  cwd: projectRoot,
6924
7174
  encoding: "utf-8",
6925
7175
  stdio: ["pipe", "pipe", "pipe"]
@@ -6933,7 +7183,7 @@ ${gitLog}
6933
7183
  }
6934
7184
  } catch {}
6935
7185
  try {
6936
- const branch = execSync9("git rev-parse --abbrev-ref HEAD", {
7186
+ const branch = execSync10("git rev-parse --abbrev-ref HEAD", {
6937
7187
  cwd: projectRoot,
6938
7188
  encoding: "utf-8",
6939
7189
  stdio: ["pipe", "pipe", "pipe"]
@@ -6992,7 +7242,7 @@ function buildFeedbackInstructions() {
6992
7242
  }
6993
7243
  function readFileSafe(path) {
6994
7244
  try {
6995
- if (!existsSync13(path))
7245
+ if (!existsSync14(path))
6996
7246
  return null;
6997
7247
  return readFileSync10(path, "utf-8");
6998
7248
  } catch {
@@ -7186,7 +7436,7 @@ var init_diff_renderer = __esm(() => {
7186
7436
  });
7187
7437
 
7188
7438
  // src/repl/commands.ts
7189
- import { execSync as execSync10 } from "node:child_process";
7439
+ import { execSync as execSync11 } from "node:child_process";
7190
7440
  function getSlashCommands() {
7191
7441
  return [
7192
7442
  {
@@ -7378,7 +7628,7 @@ function cmdModel(args, ctx) {
7378
7628
  }
7379
7629
  function cmdDiff(_args, ctx) {
7380
7630
  try {
7381
- const diff = execSync10("git diff", {
7631
+ const diff = execSync11("git diff", {
7382
7632
  cwd: ctx.projectRoot,
7383
7633
  encoding: "utf-8",
7384
7634
  stdio: ["pipe", "pipe", "pipe"]
@@ -7414,7 +7664,7 @@ function cmdDiff(_args, ctx) {
7414
7664
  }
7415
7665
  function cmdUndo(_args, ctx) {
7416
7666
  try {
7417
- const status = execSync10("git status --porcelain", {
7667
+ const status = execSync11("git status --porcelain", {
7418
7668
  cwd: ctx.projectRoot,
7419
7669
  encoding: "utf-8",
7420
7670
  stdio: ["pipe", "pipe", "pipe"]
@@ -7424,7 +7674,7 @@ function cmdUndo(_args, ctx) {
7424
7674
  `);
7425
7675
  return;
7426
7676
  }
7427
- execSync10("git checkout .", {
7677
+ execSync11("git checkout .", {
7428
7678
  cwd: ctx.projectRoot,
7429
7679
  encoding: "utf-8",
7430
7680
  stdio: ["pipe", "pipe", "pipe"]
@@ -7458,7 +7708,7 @@ var init_commands = __esm(() => {
7458
7708
 
7459
7709
  // src/repl/completions.ts
7460
7710
  import { readdirSync as readdirSync4 } from "node:fs";
7461
- import { basename as basename2, dirname as dirname4, join as join13 } from "node:path";
7711
+ import { basename as basename2, dirname as dirname4, join as join14 } from "node:path";
7462
7712
 
7463
7713
  class SlashCommandCompletion {
7464
7714
  commands;
@@ -7513,7 +7763,7 @@ class FilePathCompletion {
7513
7763
  }
7514
7764
  findMatches(partial) {
7515
7765
  try {
7516
- const dir = partial.includes("/") ? join13(this.projectRoot, dirname4(partial)) : this.projectRoot;
7766
+ const dir = partial.includes("/") ? join14(this.projectRoot, dirname4(partial)) : this.projectRoot;
7517
7767
  const prefix = basename2(partial);
7518
7768
  const entries = readdirSync4(dir, { withFileTypes: true });
7519
7769
  return entries.filter((e) => {
@@ -7549,14 +7799,14 @@ class CombinedCompletion {
7549
7799
  var init_completions = () => {};
7550
7800
 
7551
7801
  // src/repl/input-history.ts
7552
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
7553
- import { dirname as dirname5, join as join14 } from "node:path";
7802
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
7803
+ import { dirname as dirname5, join as join15 } from "node:path";
7554
7804
 
7555
7805
  class InputHistory {
7556
7806
  entries = [];
7557
7807
  filePath;
7558
7808
  constructor(projectRoot) {
7559
- this.filePath = join14(projectRoot, ".locus", "sessions", ".input-history");
7809
+ this.filePath = join15(projectRoot, ".locus", "sessions", ".input-history");
7560
7810
  this.load();
7561
7811
  }
7562
7812
  add(text) {
@@ -7595,7 +7845,7 @@ class InputHistory {
7595
7845
  }
7596
7846
  load() {
7597
7847
  try {
7598
- if (!existsSync14(this.filePath))
7848
+ if (!existsSync15(this.filePath))
7599
7849
  return;
7600
7850
  const content = readFileSync11(this.filePath, "utf-8");
7601
7851
  this.entries = content.split(`
@@ -7605,8 +7855,8 @@ class InputHistory {
7605
7855
  save() {
7606
7856
  try {
7607
7857
  const dir = dirname5(this.filePath);
7608
- if (!existsSync14(dir)) {
7609
- mkdirSync9(dir, { recursive: true });
7858
+ if (!existsSync15(dir)) {
7859
+ mkdirSync10(dir, { recursive: true });
7610
7860
  }
7611
7861
  const content = this.entries.map((e) => this.escape(e)).join(`
7612
7862
  `);
@@ -7636,21 +7886,21 @@ var init_model_config = __esm(() => {
7636
7886
 
7637
7887
  // src/repl/session-manager.ts
7638
7888
  import {
7639
- existsSync as existsSync15,
7640
- mkdirSync as mkdirSync10,
7889
+ existsSync as existsSync16,
7890
+ mkdirSync as mkdirSync11,
7641
7891
  readdirSync as readdirSync5,
7642
7892
  readFileSync as readFileSync12,
7643
7893
  unlinkSync as unlinkSync4,
7644
7894
  writeFileSync as writeFileSync8
7645
7895
  } from "node:fs";
7646
- import { basename as basename3, join as join15 } from "node:path";
7896
+ import { basename as basename3, join as join16 } from "node:path";
7647
7897
 
7648
7898
  class SessionManager {
7649
7899
  sessionsDir;
7650
7900
  constructor(projectRoot) {
7651
- this.sessionsDir = join15(projectRoot, ".locus", "sessions");
7652
- if (!existsSync15(this.sessionsDir)) {
7653
- mkdirSync10(this.sessionsDir, { recursive: true });
7901
+ this.sessionsDir = join16(projectRoot, ".locus", "sessions");
7902
+ if (!existsSync16(this.sessionsDir)) {
7903
+ mkdirSync11(this.sessionsDir, { recursive: true });
7654
7904
  }
7655
7905
  }
7656
7906
  create(options) {
@@ -7675,12 +7925,12 @@ class SessionManager {
7675
7925
  }
7676
7926
  isPersisted(sessionOrId) {
7677
7927
  const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
7678
- return existsSync15(this.getSessionPath(sessionId));
7928
+ return existsSync16(this.getSessionPath(sessionId));
7679
7929
  }
7680
7930
  load(idOrPrefix) {
7681
7931
  const files = this.listSessionFiles();
7682
7932
  const exactPath = this.getSessionPath(idOrPrefix);
7683
- if (existsSync15(exactPath)) {
7933
+ if (existsSync16(exactPath)) {
7684
7934
  try {
7685
7935
  return JSON.parse(readFileSync12(exactPath, "utf-8"));
7686
7936
  } catch {
@@ -7730,7 +7980,7 @@ class SessionManager {
7730
7980
  }
7731
7981
  delete(sessionId) {
7732
7982
  const path = this.getSessionPath(sessionId);
7733
- if (existsSync15(path)) {
7983
+ if (existsSync16(path)) {
7734
7984
  unlinkSync4(path);
7735
7985
  return true;
7736
7986
  }
@@ -7760,7 +8010,7 @@ class SessionManager {
7760
8010
  const remaining = withStats.length - pruned;
7761
8011
  if (remaining > MAX_SESSIONS) {
7762
8012
  const toRemove = remaining - MAX_SESSIONS;
7763
- const alive = withStats.filter((e) => existsSync15(e.path));
8013
+ const alive = withStats.filter((e) => existsSync16(e.path));
7764
8014
  for (let i = 0;i < toRemove && i < alive.length; i++) {
7765
8015
  try {
7766
8016
  unlinkSync4(alive[i].path);
@@ -7775,7 +8025,7 @@ class SessionManager {
7775
8025
  }
7776
8026
  listSessionFiles() {
7777
8027
  try {
7778
- return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join15(this.sessionsDir, f));
8028
+ return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join16(this.sessionsDir, f));
7779
8029
  } catch {
7780
8030
  return [];
7781
8031
  }
@@ -7784,7 +8034,7 @@ class SessionManager {
7784
8034
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
7785
8035
  }
7786
8036
  getSessionPath(sessionId) {
7787
- return join15(this.sessionsDir, `${sessionId}.json`);
8037
+ return join16(this.sessionsDir, `${sessionId}.json`);
7788
8038
  }
7789
8039
  }
7790
8040
  var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
@@ -7794,7 +8044,7 @@ var init_session_manager = __esm(() => {
7794
8044
  });
7795
8045
 
7796
8046
  // src/repl/repl.ts
7797
- import { execSync as execSync11 } from "node:child_process";
8047
+ import { execSync as execSync12 } from "node:child_process";
7798
8048
  async function startRepl(options) {
7799
8049
  const { projectRoot, config } = options;
7800
8050
  const sessionManager = new SessionManager(projectRoot);
@@ -7812,7 +8062,7 @@ async function startRepl(options) {
7812
8062
  } else {
7813
8063
  let branch = "main";
7814
8064
  try {
7815
- branch = execSync11("git rev-parse --abbrev-ref HEAD", {
8065
+ branch = execSync12("git rev-parse --abbrev-ref HEAD", {
7816
8066
  cwd: projectRoot,
7817
8067
  encoding: "utf-8",
7818
8068
  stdio: ["pipe", "pipe", "pipe"]
@@ -7837,6 +8087,7 @@ async function executeOneShotPrompt(prompt, session, sessionManager, options) {
7837
8087
  const normalized = normalizeImagePlaceholders(prompt);
7838
8088
  const text = normalized.text;
7839
8089
  const images = collectReferencedAttachments(text, normalized.attachments);
8090
+ relocateImages(images, projectRoot);
7840
8091
  const imageContext = buildImageContext(images);
7841
8092
  const fullPrompt = buildReplPrompt(text + imageContext, projectRoot, config, session.messages);
7842
8093
  sessionManager.addMessage(session, {
@@ -7931,6 +8182,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
7931
8182
  continue;
7932
8183
  }
7933
8184
  history.add(text);
8185
+ relocateImages(result.images, projectRoot);
7934
8186
  const imageContext = buildImageContext(result.images);
7935
8187
  const fullPrompt = buildReplPrompt(text + imageContext, projectRoot, { ...config, ai: { provider: currentProvider, model: currentModel } }, session.messages);
7936
8188
  sessionManager.addMessage(session, {
@@ -8223,7 +8475,7 @@ var init_exec = __esm(() => {
8223
8475
  });
8224
8476
 
8225
8477
  // src/core/agent.ts
8226
- import { execSync as execSync12 } from "node:child_process";
8478
+ import { execSync as execSync13 } from "node:child_process";
8227
8479
  async function executeIssue(projectRoot, options) {
8228
8480
  const log = getLogger();
8229
8481
  const timer = createTimer();
@@ -8252,7 +8504,7 @@ ${cyan("●")} ${bold(`#${issueNumber}`)} ${issue.title}
8252
8504
  }
8253
8505
  let issueComments = [];
8254
8506
  try {
8255
- const commentsRaw = execSync12(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8507
+ const commentsRaw = execSync13(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8256
8508
  if (commentsRaw) {
8257
8509
  issueComments = commentsRaw.split(`
8258
8510
  `).filter(Boolean);
@@ -8416,12 +8668,12 @@ ${aiResult.success ? green("✓") : red("✗")} Iteration ${aiResult.success ? "
8416
8668
  }
8417
8669
  async function createIssuePR(projectRoot, config, issue) {
8418
8670
  try {
8419
- const currentBranch = execSync12("git rev-parse --abbrev-ref HEAD", {
8671
+ const currentBranch = execSync13("git rev-parse --abbrev-ref HEAD", {
8420
8672
  cwd: projectRoot,
8421
8673
  encoding: "utf-8",
8422
8674
  stdio: ["pipe", "pipe", "pipe"]
8423
8675
  }).trim();
8424
- const diff = execSync12(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
8676
+ const diff = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
8425
8677
  cwd: projectRoot,
8426
8678
  encoding: "utf-8",
8427
8679
  stdio: ["pipe", "pipe", "pipe"]
@@ -8430,7 +8682,7 @@ async function createIssuePR(projectRoot, config, issue) {
8430
8682
  getLogger().verbose("No changes to create PR for");
8431
8683
  return;
8432
8684
  }
8433
- execSync12(`git push -u origin ${currentBranch}`, {
8685
+ execSync13(`git push -u origin ${currentBranch}`, {
8434
8686
  cwd: projectRoot,
8435
8687
  encoding: "utf-8",
8436
8688
  stdio: ["pipe", "pipe", "pipe"]
@@ -8476,9 +8728,9 @@ var init_agent = __esm(() => {
8476
8728
  });
8477
8729
 
8478
8730
  // src/core/conflict.ts
8479
- import { execSync as execSync13 } from "node:child_process";
8731
+ import { execSync as execSync14 } from "node:child_process";
8480
8732
  function git2(args, cwd) {
8481
- return execSync13(`git ${args}`, {
8733
+ return execSync14(`git ${args}`, {
8482
8734
  cwd,
8483
8735
  encoding: "utf-8",
8484
8736
  stdio: ["pipe", "pipe", "pipe"]
@@ -8603,144 +8855,12 @@ var init_conflict = __esm(() => {
8603
8855
  init_logger();
8604
8856
  });
8605
8857
 
8606
- // src/core/sandbox.ts
8607
- import { execFile } from "node:child_process";
8608
- async function detectSandboxSupport() {
8609
- if (cachedStatus)
8610
- return cachedStatus;
8611
- const log = getLogger();
8612
- log.debug("Detecting Docker sandbox support...");
8613
- const status = await runDetection();
8614
- cachedStatus = status;
8615
- if (status.available) {
8616
- log.verbose("Docker sandbox support detected");
8617
- } else {
8618
- log.verbose(`Docker sandbox not available: ${status.reason}`);
8619
- }
8620
- return status;
8621
- }
8622
- function runDetection() {
8623
- return new Promise((resolve2) => {
8624
- let settled = false;
8625
- const child = execFile("docker", ["sandbox", "ls"], { timeout: TIMEOUT_MS }, (error, _stdout, stderr) => {
8626
- if (settled)
8627
- return;
8628
- settled = true;
8629
- if (!error) {
8630
- resolve2({ available: true });
8631
- return;
8632
- }
8633
- const code = error.code;
8634
- if (code === "ENOENT") {
8635
- resolve2({ available: false, reason: "Docker is not installed" });
8636
- return;
8637
- }
8638
- if (error.killed) {
8639
- resolve2({ available: false, reason: "Docker is not responding" });
8640
- return;
8641
- }
8642
- const stderrStr = (stderr ?? "").toLowerCase();
8643
- if (stderrStr.includes("unknown") || stderrStr.includes("not a docker command") || stderrStr.includes("is not a docker command")) {
8644
- resolve2({
8645
- available: false,
8646
- reason: "Docker Desktop 4.58+ with sandbox support required"
8647
- });
8648
- return;
8649
- }
8650
- resolve2({
8651
- available: false,
8652
- reason: "Docker Desktop 4.58+ with sandbox support required"
8653
- });
8654
- });
8655
- child.on?.("error", (err) => {
8656
- if (settled)
8657
- return;
8658
- settled = true;
8659
- if (err.code === "ENOENT") {
8660
- resolve2({ available: false, reason: "Docker is not installed" });
8661
- } else {
8662
- resolve2({
8663
- available: false,
8664
- reason: "Docker Desktop 4.58+ with sandbox support required"
8665
- });
8666
- }
8667
- });
8668
- });
8669
- }
8670
- async function cleanupStaleSandboxes() {
8671
- const log = getLogger();
8672
- try {
8673
- const { stdout } = await execFileAsync("docker", ["sandbox", "ls"], {
8674
- timeout: TIMEOUT_MS
8675
- });
8676
- const lines = stdout.trim().split(`
8677
- `);
8678
- if (lines.length <= 1)
8679
- return 0;
8680
- const staleNames = [];
8681
- for (const line of lines.slice(1)) {
8682
- const name = line.trim().split(/\s+/)[0];
8683
- if (name?.startsWith("locus-")) {
8684
- staleNames.push(name);
8685
- }
8686
- }
8687
- if (staleNames.length === 0)
8688
- return 0;
8689
- log.verbose(`Found ${staleNames.length} stale sandbox(es) to clean up`);
8690
- let cleaned = 0;
8691
- for (const name of staleNames) {
8692
- try {
8693
- await execFileAsync("docker", ["sandbox", "rm", name], {
8694
- timeout: 1e4
8695
- });
8696
- log.debug(`Removed stale sandbox: ${name}`);
8697
- cleaned++;
8698
- } catch {
8699
- log.debug(`Failed to remove stale sandbox: ${name}`);
8700
- }
8701
- }
8702
- return cleaned;
8703
- } catch {
8704
- return 0;
8705
- }
8706
- }
8707
- function execFileAsync(file, args, options) {
8708
- return new Promise((resolve2, reject) => {
8709
- execFile(file, args, options, (error, stdout, stderr) => {
8710
- if (error)
8711
- reject(error);
8712
- else
8713
- resolve2({ stdout: stdout ?? "", stderr: stderr ?? "" });
8714
- });
8715
- });
8716
- }
8717
- function resolveSandboxMode(config, flags) {
8718
- if (flags.noSandbox) {
8719
- return "disabled";
8720
- }
8721
- if (flags.sandbox !== undefined) {
8722
- if (flags.sandbox === "require") {
8723
- return "required";
8724
- }
8725
- throw new Error(`Invalid --sandbox value: "${flags.sandbox}". Valid values: require`);
8726
- }
8727
- if (!config.enabled) {
8728
- return "disabled";
8729
- }
8730
- return "auto";
8731
- }
8732
- var TIMEOUT_MS = 5000, cachedStatus = null;
8733
- var init_sandbox = __esm(() => {
8734
- init_terminal();
8735
- init_logger();
8736
- });
8737
-
8738
8858
  // src/core/worktree.ts
8739
- import { execSync as execSync14 } from "node:child_process";
8740
- import { existsSync as existsSync16, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
8741
- import { join as join16 } from "node:path";
8859
+ import { execSync as execSync15 } from "node:child_process";
8860
+ import { existsSync as existsSync17, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
8861
+ import { join as join17 } from "node:path";
8742
8862
  function git3(args, cwd) {
8743
- return execSync14(`git ${args}`, {
8863
+ return execSync15(`git ${args}`, {
8744
8864
  cwd,
8745
8865
  encoding: "utf-8",
8746
8866
  stdio: ["pipe", "pipe", "pipe"]
@@ -8754,10 +8874,10 @@ function gitSafe2(args, cwd) {
8754
8874
  }
8755
8875
  }
8756
8876
  function getWorktreeDir(projectRoot) {
8757
- return join16(projectRoot, ".locus", "worktrees");
8877
+ return join17(projectRoot, ".locus", "worktrees");
8758
8878
  }
8759
8879
  function getWorktreePath(projectRoot, issueNumber) {
8760
- return join16(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
8880
+ return join17(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
8761
8881
  }
8762
8882
  function generateBranchName(issueNumber) {
8763
8883
  const randomSuffix = Math.random().toString(36).slice(2, 8);
@@ -8765,7 +8885,7 @@ function generateBranchName(issueNumber) {
8765
8885
  }
8766
8886
  function getWorktreeBranch(worktreePath) {
8767
8887
  try {
8768
- return execSync14("git branch --show-current", {
8888
+ return execSync15("git branch --show-current", {
8769
8889
  cwd: worktreePath,
8770
8890
  encoding: "utf-8",
8771
8891
  stdio: ["pipe", "pipe", "pipe"]
@@ -8777,7 +8897,7 @@ function getWorktreeBranch(worktreePath) {
8777
8897
  function createWorktree(projectRoot, issueNumber, baseBranch) {
8778
8898
  const log = getLogger();
8779
8899
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
8780
- if (existsSync16(worktreePath)) {
8900
+ if (existsSync17(worktreePath)) {
8781
8901
  log.verbose(`Worktree already exists for issue #${issueNumber}`);
8782
8902
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
8783
8903
  return {
@@ -8804,7 +8924,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
8804
8924
  function removeWorktree(projectRoot, issueNumber) {
8805
8925
  const log = getLogger();
8806
8926
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
8807
- if (!existsSync16(worktreePath)) {
8927
+ if (!existsSync17(worktreePath)) {
8808
8928
  log.verbose(`Worktree for issue #${issueNumber} does not exist`);
8809
8929
  return;
8810
8930
  }
@@ -8823,7 +8943,7 @@ function removeWorktree(projectRoot, issueNumber) {
8823
8943
  function listWorktrees(projectRoot) {
8824
8944
  const log = getLogger();
8825
8945
  const worktreeDir = getWorktreeDir(projectRoot);
8826
- if (!existsSync16(worktreeDir)) {
8946
+ if (!existsSync17(worktreeDir)) {
8827
8947
  return [];
8828
8948
  }
8829
8949
  const entries = readdirSync6(worktreeDir).filter((entry) => entry.startsWith("issue-"));
@@ -8843,7 +8963,7 @@ function listWorktrees(projectRoot) {
8843
8963
  if (!match)
8844
8964
  continue;
8845
8965
  const issueNumber = Number.parseInt(match[1], 10);
8846
- const path = join16(worktreeDir, entry);
8966
+ const path = join17(worktreeDir, entry);
8847
8967
  const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
8848
8968
  let resolvedPath;
8849
8969
  try {
@@ -8890,7 +9010,7 @@ var exports_run = {};
8890
9010
  __export(exports_run, {
8891
9011
  runCommand: () => runCommand
8892
9012
  });
8893
- import { execSync as execSync15 } from "node:child_process";
9013
+ import { execSync as execSync16 } from "node:child_process";
8894
9014
  function printRunHelp() {
8895
9015
  process.stderr.write(`
8896
9016
  ${bold("locus run")} — Execute issues using AI agents
@@ -8956,13 +9076,6 @@ async function runCommand(projectRoot, args, flags = {}) {
8956
9076
  process.stderr.write(`${yellow("⚠")} Running without sandbox. The AI agent will have unrestricted access to your filesystem, network, and environment variables.
8957
9077
  `);
8958
9078
  }
8959
- if (sandboxed) {
8960
- const staleCleaned = await cleanupStaleSandboxes();
8961
- if (staleCleaned > 0) {
8962
- process.stderr.write(` ${dim(`Cleaned up ${staleCleaned} stale sandbox${staleCleaned === 1 ? "" : "es"}.`)}
8963
- `);
8964
- }
8965
- }
8966
9079
  if (flags.resume) {
8967
9080
  return handleResume(projectRoot, config, sandboxed);
8968
9081
  }
@@ -9041,7 +9154,7 @@ ${yellow("⚠")} A sprint run is already in progress.
9041
9154
  }
9042
9155
  if (!flags.dryRun) {
9043
9156
  try {
9044
- execSync15(`git checkout -B ${branchName}`, {
9157
+ execSync16(`git checkout -B ${branchName}`, {
9045
9158
  cwd: projectRoot,
9046
9159
  encoding: "utf-8",
9047
9160
  stdio: ["pipe", "pipe", "pipe"]
@@ -9091,7 +9204,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
9091
9204
  let sprintContext;
9092
9205
  if (i > 0 && !flags.dryRun) {
9093
9206
  try {
9094
- sprintContext = execSync15(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9207
+ sprintContext = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9095
9208
  cwd: projectRoot,
9096
9209
  encoding: "utf-8",
9097
9210
  stdio: ["pipe", "pipe", "pipe"]
@@ -9111,13 +9224,16 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
9111
9224
  dryRun: flags.dryRun,
9112
9225
  sprintContext,
9113
9226
  skipPR: true,
9114
- sandboxed,
9115
- sandboxName: config.sandbox.name
9227
+ sandboxed
9116
9228
  });
9117
9229
  if (result.success) {
9118
9230
  if (!flags.dryRun) {
9119
9231
  const issueTitle = issue?.title ?? "";
9120
9232
  ensureTaskCommit(projectRoot, task.issue, issueTitle);
9233
+ if (sandboxed && i < state.tasks.length - 1) {
9234
+ process.stderr.write(` ${dim("↻ Sandbox will resync on next task")}
9235
+ `);
9236
+ }
9121
9237
  }
9122
9238
  markTaskDone(state, task.issue, result.prNumber);
9123
9239
  } else {
@@ -9152,7 +9268,7 @@ ${bold("Summary:")}
9152
9268
  const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
9153
9269
  if (prNumber !== undefined) {
9154
9270
  try {
9155
- execSync15(`git checkout ${config.agent.baseBranch}`, {
9271
+ execSync16(`git checkout ${config.agent.baseBranch}`, {
9156
9272
  cwd: projectRoot,
9157
9273
  encoding: "utf-8",
9158
9274
  stdio: ["pipe", "pipe", "pipe"]
@@ -9349,13 +9465,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
9349
9465
  `);
9350
9466
  if (state.type === "sprint" && state.branch) {
9351
9467
  try {
9352
- const currentBranch = execSync15("git rev-parse --abbrev-ref HEAD", {
9468
+ const currentBranch = execSync16("git rev-parse --abbrev-ref HEAD", {
9353
9469
  cwd: projectRoot,
9354
9470
  encoding: "utf-8",
9355
9471
  stdio: ["pipe", "pipe", "pipe"]
9356
9472
  }).trim();
9357
9473
  if (currentBranch !== state.branch) {
9358
- execSync15(`git checkout ${state.branch}`, {
9474
+ execSync16(`git checkout ${state.branch}`, {
9359
9475
  cwd: projectRoot,
9360
9476
  encoding: "utf-8",
9361
9477
  stdio: ["pipe", "pipe", "pipe"]
@@ -9383,7 +9499,7 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
9383
9499
  model: config.ai.model,
9384
9500
  skipPR: isSprintRun,
9385
9501
  sandboxed,
9386
- sandboxName: config.sandbox.name
9502
+ sandboxName: isSprintRun ? undefined : config.sandbox.name
9387
9503
  });
9388
9504
  if (result.success) {
9389
9505
  if (isSprintRun) {
@@ -9393,6 +9509,10 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
9393
9509
  issueTitle = iss.title;
9394
9510
  } catch {}
9395
9511
  ensureTaskCommit(projectRoot, task.issue, issueTitle);
9512
+ if (sandboxed) {
9513
+ process.stderr.write(` ${dim("↻ Sandbox will resync on next task")}
9514
+ `);
9515
+ }
9396
9516
  }
9397
9517
  markTaskDone(state, task.issue, result.prNumber);
9398
9518
  } else {
@@ -9418,7 +9538,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
9418
9538
  const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
9419
9539
  if (prNumber !== undefined) {
9420
9540
  try {
9421
- execSync15(`git checkout ${config.agent.baseBranch}`, {
9541
+ execSync16(`git checkout ${config.agent.baseBranch}`, {
9422
9542
  cwd: projectRoot,
9423
9543
  encoding: "utf-8",
9424
9544
  stdio: ["pipe", "pipe", "pipe"]
@@ -9449,14 +9569,14 @@ function getOrder2(issue) {
9449
9569
  }
9450
9570
  function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
9451
9571
  try {
9452
- const status = execSync15("git status --porcelain", {
9572
+ const status = execSync16("git status --porcelain", {
9453
9573
  cwd: projectRoot,
9454
9574
  encoding: "utf-8",
9455
9575
  stdio: ["pipe", "pipe", "pipe"]
9456
9576
  }).trim();
9457
9577
  if (!status)
9458
9578
  return;
9459
- execSync15("git add -A", {
9579
+ execSync16("git add -A", {
9460
9580
  cwd: projectRoot,
9461
9581
  encoding: "utf-8",
9462
9582
  stdio: ["pipe", "pipe", "pipe"]
@@ -9464,7 +9584,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
9464
9584
  const message = `chore: complete #${issueNumber} - ${issueTitle}
9465
9585
 
9466
9586
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
9467
- execSync15(`git commit -F -`, {
9587
+ execSync16(`git commit -F -`, {
9468
9588
  input: message,
9469
9589
  cwd: projectRoot,
9470
9590
  encoding: "utf-8",
@@ -9478,7 +9598,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
9478
9598
  if (!config.agent.autoPR)
9479
9599
  return;
9480
9600
  try {
9481
- const diff = execSync15(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9601
+ const diff = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9482
9602
  cwd: projectRoot,
9483
9603
  encoding: "utf-8",
9484
9604
  stdio: ["pipe", "pipe", "pipe"]
@@ -9488,7 +9608,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
9488
9608
  `);
9489
9609
  return;
9490
9610
  }
9491
- execSync15(`git push -u origin ${branchName}`, {
9611
+ execSync16(`git push -u origin ${branchName}`, {
9492
9612
  cwd: projectRoot,
9493
9613
  encoding: "utf-8",
9494
9614
  stdio: ["pipe", "pipe", "pipe"]
@@ -9535,6 +9655,8 @@ __export(exports_status, {
9535
9655
  });
9536
9656
  async function statusCommand(projectRoot) {
9537
9657
  const config = loadConfig(projectRoot);
9658
+ const spinner = new Spinner;
9659
+ spinner.start("Fetching project status...");
9538
9660
  const lines = [];
9539
9661
  lines.push(` ${dim("Repo:")} ${cyan(`${config.github.owner}/${config.github.repo}`)}`);
9540
9662
  lines.push(` ${dim("Provider:")} ${config.ai.provider} / ${config.ai.model}`);
@@ -9611,6 +9733,7 @@ async function statusCommand(projectRoot) {
9611
9733
  }
9612
9734
  }
9613
9735
  } catch {}
9736
+ spinner.stop();
9614
9737
  lines.push("");
9615
9738
  process.stderr.write(`
9616
9739
  ${drawBox(lines, { title: "Locus Status" })}
@@ -9634,13 +9757,13 @@ __export(exports_plan, {
9634
9757
  parsePlanArgs: () => parsePlanArgs
9635
9758
  });
9636
9759
  import {
9637
- existsSync as existsSync17,
9638
- mkdirSync as mkdirSync11,
9760
+ existsSync as existsSync18,
9761
+ mkdirSync as mkdirSync12,
9639
9762
  readdirSync as readdirSync7,
9640
9763
  readFileSync as readFileSync13,
9641
9764
  writeFileSync as writeFileSync9
9642
9765
  } from "node:fs";
9643
- import { join as join17 } from "node:path";
9766
+ import { join as join18 } from "node:path";
9644
9767
  function printHelp() {
9645
9768
  process.stderr.write(`
9646
9769
  ${bold("locus plan")} — AI-powered sprint planning
@@ -9671,12 +9794,12 @@ function normalizeSprintName(name) {
9671
9794
  return name.trim().toLowerCase();
9672
9795
  }
9673
9796
  function getPlansDir(projectRoot) {
9674
- return join17(projectRoot, ".locus", "plans");
9797
+ return join18(projectRoot, ".locus", "plans");
9675
9798
  }
9676
9799
  function ensurePlansDir(projectRoot) {
9677
9800
  const dir = getPlansDir(projectRoot);
9678
- if (!existsSync17(dir)) {
9679
- mkdirSync11(dir, { recursive: true });
9801
+ if (!existsSync18(dir)) {
9802
+ mkdirSync12(dir, { recursive: true });
9680
9803
  }
9681
9804
  return dir;
9682
9805
  }
@@ -9685,14 +9808,14 @@ function generateId() {
9685
9808
  }
9686
9809
  function loadPlanFile(projectRoot, id) {
9687
9810
  const dir = getPlansDir(projectRoot);
9688
- if (!existsSync17(dir))
9811
+ if (!existsSync18(dir))
9689
9812
  return null;
9690
9813
  const files = readdirSync7(dir).filter((f) => f.endsWith(".json"));
9691
9814
  const match = files.find((f) => f.startsWith(id));
9692
9815
  if (!match)
9693
9816
  return null;
9694
9817
  try {
9695
- const content = readFileSync13(join17(dir, match), "utf-8");
9818
+ const content = readFileSync13(join18(dir, match), "utf-8");
9696
9819
  return JSON.parse(content);
9697
9820
  } catch {
9698
9821
  return null;
@@ -9738,7 +9861,7 @@ async function planCommand(projectRoot, args, flags = {}) {
9738
9861
  }
9739
9862
  function handleListPlans(projectRoot) {
9740
9863
  const dir = getPlansDir(projectRoot);
9741
- if (!existsSync17(dir)) {
9864
+ if (!existsSync18(dir)) {
9742
9865
  process.stderr.write(`${dim("No saved plans yet.")}
9743
9866
  `);
9744
9867
  return;
@@ -9756,7 +9879,7 @@ ${bold("Saved Plans:")}
9756
9879
  for (const file of files) {
9757
9880
  const id = file.replace(".json", "");
9758
9881
  try {
9759
- const content = readFileSync13(join17(dir, file), "utf-8");
9882
+ const content = readFileSync13(join18(dir, file), "utf-8");
9760
9883
  const plan = JSON.parse(content);
9761
9884
  const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
9762
9885
  const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
@@ -9867,7 +9990,7 @@ ${bold("Approving plan:")}
9867
9990
  async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
9868
9991
  const id = generateId();
9869
9992
  const plansDir = ensurePlansDir(projectRoot);
9870
- const planPath = join17(plansDir, `${id}.json`);
9993
+ const planPath = join18(plansDir, `${id}.json`);
9871
9994
  const planPathRelative = `.locus/plans/${id}.json`;
9872
9995
  const displayDirective = directive;
9873
9996
  process.stderr.write(`
@@ -9901,7 +10024,7 @@ ${red("✗")} Planning failed: ${aiResult.error}
9901
10024
  `);
9902
10025
  return;
9903
10026
  }
9904
- if (!existsSync17(planPath)) {
10027
+ if (!existsSync18(planPath)) {
9905
10028
  process.stderr.write(`
9906
10029
  ${yellow("⚠")} Plan file was not created at ${bold(planPathRelative)}.
9907
10030
  `);
@@ -10068,15 +10191,15 @@ function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, pla
10068
10191
  parts.push(`SPRINT: ${sprintName}`);
10069
10192
  }
10070
10193
  parts.push("");
10071
- const locusPath = join17(projectRoot, "LOCUS.md");
10072
- if (existsSync17(locusPath)) {
10194
+ const locusPath = join18(projectRoot, "LOCUS.md");
10195
+ if (existsSync18(locusPath)) {
10073
10196
  const content = readFileSync13(locusPath, "utf-8");
10074
10197
  parts.push("PROJECT CONTEXT (LOCUS.md):");
10075
10198
  parts.push(content.slice(0, 3000));
10076
10199
  parts.push("");
10077
10200
  }
10078
- const learningsPath = join17(projectRoot, ".locus", "LEARNINGS.md");
10079
- if (existsSync17(learningsPath)) {
10201
+ const learningsPath = join18(projectRoot, ".locus", "LEARNINGS.md");
10202
+ if (existsSync18(learningsPath)) {
10080
10203
  const content = readFileSync13(learningsPath, "utf-8");
10081
10204
  parts.push("PAST LEARNINGS:");
10082
10205
  parts.push(content.slice(0, 2000));
@@ -10253,9 +10376,9 @@ var exports_review = {};
10253
10376
  __export(exports_review, {
10254
10377
  reviewCommand: () => reviewCommand
10255
10378
  });
10256
- import { execSync as execSync16 } from "node:child_process";
10257
- import { existsSync as existsSync18, readFileSync as readFileSync14 } from "node:fs";
10258
- import { join as join18 } from "node:path";
10379
+ import { execSync as execSync17 } from "node:child_process";
10380
+ import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
10381
+ import { join as join19 } from "node:path";
10259
10382
  function printHelp2() {
10260
10383
  process.stderr.write(`
10261
10384
  ${bold("locus review")} — AI-powered code review
@@ -10331,7 +10454,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
10331
10454
  async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
10332
10455
  let prInfo;
10333
10456
  try {
10334
- const result = execSync16(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10457
+ const result = execSync17(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10335
10458
  const raw = JSON.parse(result);
10336
10459
  prInfo = {
10337
10460
  number: raw.number,
@@ -10397,7 +10520,7 @@ ${output.slice(0, 60000)}
10397
10520
 
10398
10521
  ---
10399
10522
  _Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
10400
- execSync16(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10523
+ execSync17(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10401
10524
  process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
10402
10525
  `);
10403
10526
  } catch (e) {
@@ -10414,8 +10537,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
10414
10537
  const parts = [];
10415
10538
  parts.push(`You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.`);
10416
10539
  parts.push("");
10417
- const locusPath = join18(projectRoot, "LOCUS.md");
10418
- if (existsSync18(locusPath)) {
10540
+ const locusPath = join19(projectRoot, "LOCUS.md");
10541
+ if (existsSync19(locusPath)) {
10419
10542
  const content = readFileSync14(locusPath, "utf-8");
10420
10543
  parts.push("PROJECT CONTEXT:");
10421
10544
  parts.push(content.slice(0, 2000));
@@ -10468,7 +10591,7 @@ var exports_iterate = {};
10468
10591
  __export(exports_iterate, {
10469
10592
  iterateCommand: () => iterateCommand
10470
10593
  });
10471
- import { execSync as execSync17 } from "node:child_process";
10594
+ import { execSync as execSync18 } from "node:child_process";
10472
10595
  function printHelp3() {
10473
10596
  process.stderr.write(`
10474
10597
  ${bold("locus iterate")} — Re-execute tasks with PR feedback
@@ -10678,12 +10801,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
10678
10801
  }
10679
10802
  function findPRForIssue(projectRoot, issueNumber) {
10680
10803
  try {
10681
- const result = execSync17(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10804
+ const result = execSync18(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10682
10805
  const parsed = JSON.parse(result);
10683
10806
  if (parsed.length > 0) {
10684
10807
  return parsed[0].number;
10685
10808
  }
10686
- const branchResult = execSync17(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10809
+ const branchResult = execSync18(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10687
10810
  const branchParsed = JSON.parse(branchResult);
10688
10811
  if (branchParsed.length > 0) {
10689
10812
  return branchParsed[0].number;
@@ -10718,14 +10841,14 @@ __export(exports_discuss, {
10718
10841
  discussCommand: () => discussCommand
10719
10842
  });
10720
10843
  import {
10721
- existsSync as existsSync19,
10722
- mkdirSync as mkdirSync12,
10844
+ existsSync as existsSync20,
10845
+ mkdirSync as mkdirSync13,
10723
10846
  readdirSync as readdirSync8,
10724
10847
  readFileSync as readFileSync15,
10725
10848
  unlinkSync as unlinkSync5,
10726
10849
  writeFileSync as writeFileSync10
10727
10850
  } from "node:fs";
10728
- import { join as join19 } from "node:path";
10851
+ import { join as join20 } from "node:path";
10729
10852
  function printHelp4() {
10730
10853
  process.stderr.write(`
10731
10854
  ${bold("locus discuss")} — AI-powered architectural discussions
@@ -10747,12 +10870,12 @@ ${bold("Examples:")}
10747
10870
  `);
10748
10871
  }
10749
10872
  function getDiscussionsDir(projectRoot) {
10750
- return join19(projectRoot, ".locus", "discussions");
10873
+ return join20(projectRoot, ".locus", "discussions");
10751
10874
  }
10752
10875
  function ensureDiscussionsDir(projectRoot) {
10753
10876
  const dir = getDiscussionsDir(projectRoot);
10754
- if (!existsSync19(dir)) {
10755
- mkdirSync12(dir, { recursive: true });
10877
+ if (!existsSync20(dir)) {
10878
+ mkdirSync13(dir, { recursive: true });
10756
10879
  }
10757
10880
  return dir;
10758
10881
  }
@@ -10786,7 +10909,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
10786
10909
  }
10787
10910
  function listDiscussions(projectRoot) {
10788
10911
  const dir = getDiscussionsDir(projectRoot);
10789
- if (!existsSync19(dir)) {
10912
+ if (!existsSync20(dir)) {
10790
10913
  process.stderr.write(`${dim("No discussions yet.")}
10791
10914
  `);
10792
10915
  return;
@@ -10803,7 +10926,7 @@ ${bold("Discussions:")}
10803
10926
  `);
10804
10927
  for (const file of files) {
10805
10928
  const id = file.replace(".md", "");
10806
- const content = readFileSync15(join19(dir, file), "utf-8");
10929
+ const content = readFileSync15(join20(dir, file), "utf-8");
10807
10930
  const titleMatch = content.match(/^#\s+(.+)/m);
10808
10931
  const title = titleMatch ? titleMatch[1] : id;
10809
10932
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
@@ -10821,7 +10944,7 @@ function showDiscussion(projectRoot, id) {
10821
10944
  return;
10822
10945
  }
10823
10946
  const dir = getDiscussionsDir(projectRoot);
10824
- if (!existsSync19(dir)) {
10947
+ if (!existsSync20(dir)) {
10825
10948
  process.stderr.write(`${red("✗")} No discussions found.
10826
10949
  `);
10827
10950
  return;
@@ -10833,7 +10956,7 @@ function showDiscussion(projectRoot, id) {
10833
10956
  `);
10834
10957
  return;
10835
10958
  }
10836
- const content = readFileSync15(join19(dir, match), "utf-8");
10959
+ const content = readFileSync15(join20(dir, match), "utf-8");
10837
10960
  process.stdout.write(`${content}
10838
10961
  `);
10839
10962
  }
@@ -10844,7 +10967,7 @@ function deleteDiscussion(projectRoot, id) {
10844
10967
  return;
10845
10968
  }
10846
10969
  const dir = getDiscussionsDir(projectRoot);
10847
- if (!existsSync19(dir)) {
10970
+ if (!existsSync20(dir)) {
10848
10971
  process.stderr.write(`${red("✗")} No discussions found.
10849
10972
  `);
10850
10973
  return;
@@ -10856,7 +10979,7 @@ function deleteDiscussion(projectRoot, id) {
10856
10979
  `);
10857
10980
  return;
10858
10981
  }
10859
- unlinkSync5(join19(dir, match));
10982
+ unlinkSync5(join20(dir, match));
10860
10983
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
10861
10984
  `);
10862
10985
  }
@@ -10869,7 +10992,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
10869
10992
  return;
10870
10993
  }
10871
10994
  const dir = getDiscussionsDir(projectRoot);
10872
- if (!existsSync19(dir)) {
10995
+ if (!existsSync20(dir)) {
10873
10996
  process.stderr.write(`${red("✗")} No discussions found.
10874
10997
  `);
10875
10998
  return;
@@ -10881,7 +11004,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
10881
11004
  `);
10882
11005
  return;
10883
11006
  }
10884
- const content = readFileSync15(join19(dir, match), "utf-8");
11007
+ const content = readFileSync15(join20(dir, match), "utf-8");
10885
11008
  const titleMatch = content.match(/^#\s+(.+)/m);
10886
11009
  const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
10887
11010
  await planCommand(projectRoot, [
@@ -10995,7 +11118,7 @@ ${turn.content}`;
10995
11118
  ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
10996
11119
  ].join(`
10997
11120
  `);
10998
- writeFileSync10(join19(dir, `${id}.md`), markdown, "utf-8");
11121
+ writeFileSync10(join20(dir, `${id}.md`), markdown, "utf-8");
10999
11122
  process.stderr.write(`
11000
11123
  ${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
11001
11124
  `);
@@ -11009,15 +11132,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
11009
11132
  const parts = [];
11010
11133
  parts.push(`You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.`);
11011
11134
  parts.push("");
11012
- const locusPath = join19(projectRoot, "LOCUS.md");
11013
- if (existsSync19(locusPath)) {
11135
+ const locusPath = join20(projectRoot, "LOCUS.md");
11136
+ if (existsSync20(locusPath)) {
11014
11137
  const content = readFileSync15(locusPath, "utf-8");
11015
11138
  parts.push("PROJECT CONTEXT:");
11016
11139
  parts.push(content.slice(0, 3000));
11017
11140
  parts.push("");
11018
11141
  }
11019
- const learningsPath = join19(projectRoot, ".locus", "LEARNINGS.md");
11020
- if (existsSync19(learningsPath)) {
11142
+ const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
11143
+ if (existsSync20(learningsPath)) {
11021
11144
  const content = readFileSync15(learningsPath, "utf-8");
11022
11145
  parts.push("PAST LEARNINGS:");
11023
11146
  parts.push(content.slice(0, 2000));
@@ -11077,8 +11200,8 @@ __export(exports_artifacts, {
11077
11200
  formatDate: () => formatDate2,
11078
11201
  artifactsCommand: () => artifactsCommand
11079
11202
  });
11080
- import { existsSync as existsSync20, readdirSync as readdirSync9, readFileSync as readFileSync16, statSync as statSync4 } from "node:fs";
11081
- import { join as join20 } from "node:path";
11203
+ import { existsSync as existsSync21, readdirSync as readdirSync9, readFileSync as readFileSync16, statSync as statSync4 } from "node:fs";
11204
+ import { join as join21 } from "node:path";
11082
11205
  function printHelp5() {
11083
11206
  process.stderr.write(`
11084
11207
  ${bold("locus artifacts")} — View and manage AI-generated artifacts
@@ -11098,14 +11221,14 @@ ${dim("Artifact names support partial matching.")}
11098
11221
  `);
11099
11222
  }
11100
11223
  function getArtifactsDir(projectRoot) {
11101
- return join20(projectRoot, ".locus", "artifacts");
11224
+ return join21(projectRoot, ".locus", "artifacts");
11102
11225
  }
11103
11226
  function listArtifacts(projectRoot) {
11104
11227
  const dir = getArtifactsDir(projectRoot);
11105
- if (!existsSync20(dir))
11228
+ if (!existsSync21(dir))
11106
11229
  return [];
11107
11230
  return readdirSync9(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
11108
- const filePath = join20(dir, fileName);
11231
+ const filePath = join21(dir, fileName);
11109
11232
  const stat = statSync4(filePath);
11110
11233
  return {
11111
11234
  name: fileName.replace(/\.md$/, ""),
@@ -11118,8 +11241,8 @@ function listArtifacts(projectRoot) {
11118
11241
  function readArtifact(projectRoot, name) {
11119
11242
  const dir = getArtifactsDir(projectRoot);
11120
11243
  const fileName = name.endsWith(".md") ? name : `${name}.md`;
11121
- const filePath = join20(dir, fileName);
11122
- if (!existsSync20(filePath))
11244
+ const filePath = join21(dir, fileName);
11245
+ if (!existsSync21(filePath))
11123
11246
  return null;
11124
11247
  const stat = statSync4(filePath);
11125
11248
  return {
@@ -11282,11 +11405,11 @@ var init_artifacts = __esm(() => {
11282
11405
  });
11283
11406
 
11284
11407
  // src/commands/sandbox.ts
11285
- var exports_sandbox = {};
11286
- __export(exports_sandbox, {
11408
+ var exports_sandbox2 = {};
11409
+ __export(exports_sandbox2, {
11287
11410
  sandboxCommand: () => sandboxCommand
11288
11411
  });
11289
- import { execSync as execSync18, spawn as spawn6 } from "node:child_process";
11412
+ import { execSync as execSync19, spawn as spawn6 } from "node:child_process";
11290
11413
  function printSandboxHelp() {
11291
11414
  process.stderr.write(`
11292
11415
  ${bold("locus sandbox")} — Manage Docker sandbox lifecycle
@@ -11400,7 +11523,7 @@ async function handleAgentLogin(projectRoot, agent) {
11400
11523
  process.stderr.write(`Creating sandbox ${bold(sandboxName)} with workspace ${dim(projectRoot)}...
11401
11524
  `);
11402
11525
  try {
11403
- execSync18(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
11526
+ execSync19(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
11404
11527
  } catch {}
11405
11528
  if (!isSandboxAlive(sandboxName)) {
11406
11529
  process.stderr.write(`${red("✗")} Failed to create sandbox.
@@ -11463,7 +11586,7 @@ function handleRemove(projectRoot) {
11463
11586
  process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
11464
11587
  `);
11465
11588
  try {
11466
- execSync18(`docker sandbox rm ${sandboxName}`, {
11589
+ execSync19(`docker sandbox rm ${sandboxName}`, {
11467
11590
  encoding: "utf-8",
11468
11591
  stdio: ["pipe", "pipe", "pipe"],
11469
11592
  timeout: 15000
@@ -11500,7 +11623,7 @@ ${bold("Sandbox Status")}
11500
11623
  }
11501
11624
  async function ensureCodexInSandbox(sandboxName) {
11502
11625
  try {
11503
- execSync18(`docker sandbox exec ${sandboxName} which codex`, {
11626
+ execSync19(`docker sandbox exec ${sandboxName} which codex`, {
11504
11627
  stdio: ["pipe", "pipe", "pipe"],
11505
11628
  timeout: 5000
11506
11629
  });
@@ -11508,7 +11631,7 @@ async function ensureCodexInSandbox(sandboxName) {
11508
11631
  process.stderr.write(`Installing codex in sandbox...
11509
11632
  `);
11510
11633
  try {
11511
- execSync18(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
11634
+ execSync19(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
11512
11635
  } catch {
11513
11636
  process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
11514
11637
  `);
@@ -11517,7 +11640,7 @@ async function ensureCodexInSandbox(sandboxName) {
11517
11640
  }
11518
11641
  function isSandboxAlive(name) {
11519
11642
  try {
11520
- const output = execSync18("docker sandbox ls", {
11643
+ const output = execSync19("docker sandbox ls", {
11521
11644
  encoding: "utf-8",
11522
11645
  stdio: ["pipe", "pipe", "pipe"],
11523
11646
  timeout: 5000
@@ -11540,13 +11663,13 @@ init_context();
11540
11663
  init_logger();
11541
11664
  init_rate_limiter();
11542
11665
  init_terminal();
11543
- import { existsSync as existsSync21, readFileSync as readFileSync17 } from "node:fs";
11544
- import { join as join21 } from "node:path";
11666
+ import { existsSync as existsSync22, readFileSync as readFileSync17 } from "node:fs";
11667
+ import { join as join22 } from "node:path";
11545
11668
  import { fileURLToPath } from "node:url";
11546
11669
  function getCliVersion() {
11547
11670
  const fallbackVersion = "0.0.0";
11548
- const packageJsonPath = join21(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
11549
- if (!existsSync21(packageJsonPath)) {
11671
+ const packageJsonPath = join22(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
11672
+ if (!existsSync22(packageJsonPath)) {
11550
11673
  return fallbackVersion;
11551
11674
  }
11552
11675
  try {
@@ -11720,6 +11843,46 @@ function resolveAlias(command) {
11720
11843
  };
11721
11844
  return aliases[command] ?? command;
11722
11845
  }
11846
+ function requiresSandboxSync(command, args, flags) {
11847
+ if (flags.noSandbox)
11848
+ return false;
11849
+ if (flags.help)
11850
+ return false;
11851
+ switch (command) {
11852
+ case "run":
11853
+ case "review":
11854
+ case "iterate":
11855
+ return true;
11856
+ case "exec":
11857
+ return args[0] !== "sessions" && args[0] !== "help";
11858
+ case "issue":
11859
+ return args[0] === "create";
11860
+ case "plan":
11861
+ if (args.length === 0)
11862
+ return false;
11863
+ return !["list", "show", "approve", "help"].includes(args[0]);
11864
+ case "discuss":
11865
+ if (args.length === 0)
11866
+ return false;
11867
+ return !["list", "show", "delete", "help"].includes(args[0]);
11868
+ case "config":
11869
+ return args[0] === "set";
11870
+ default:
11871
+ return false;
11872
+ }
11873
+ }
11874
+ async function prepareSandbox() {
11875
+ const { Spinner: Spinner2 } = await Promise.resolve().then(() => (init_progress(), exports_progress));
11876
+ const { detectSandboxSupport: detectSandboxSupport2 } = await Promise.resolve().then(() => (init_sandbox(), exports_sandbox));
11877
+ const spinner = new Spinner2;
11878
+ spinner.start("Preparing sandbox...");
11879
+ const status = await detectSandboxSupport2();
11880
+ if (status.available) {
11881
+ spinner.succeed("Sandbox ready");
11882
+ } else {
11883
+ spinner.warn(`Sandbox not available: ${status.reason}`);
11884
+ }
11885
+ }
11723
11886
  async function main() {
11724
11887
  const parsed = parseArgs(process.argv);
11725
11888
  if (parsed.flags.version) {
@@ -11739,7 +11902,7 @@ async function main() {
11739
11902
  try {
11740
11903
  const root = getGitRoot(cwd);
11741
11904
  if (isInitialized(root)) {
11742
- logDir = join21(root, ".locus", "logs");
11905
+ logDir = join22(root, ".locus", "logs");
11743
11906
  getRateLimiter(root);
11744
11907
  }
11745
11908
  } catch {}
@@ -11821,6 +11984,12 @@ async function main() {
11821
11984
  `);
11822
11985
  process.exit(1);
11823
11986
  }
11987
+ if (requiresSandboxSync(command, parsed.args, parsed.flags)) {
11988
+ const config = loadConfig(projectRoot);
11989
+ if (config.sandbox.enabled) {
11990
+ await prepareSandbox();
11991
+ }
11992
+ }
11824
11993
  switch (command) {
11825
11994
  case "config": {
11826
11995
  const { configCommand: configCommand2 } = await Promise.resolve().then(() => (init_config2(), exports_config));
@@ -11917,7 +12086,7 @@ async function main() {
11917
12086
  break;
11918
12087
  }
11919
12088
  case "sandbox": {
11920
- const { sandboxCommand: sandboxCommand2 } = await Promise.resolve().then(() => (init_sandbox2(), exports_sandbox));
12089
+ const { sandboxCommand: sandboxCommand2 } = await Promise.resolve().then(() => (init_sandbox2(), exports_sandbox2));
11921
12090
  const sandboxArgs = parsed.flags.help ? ["help"] : parsed.args;
11922
12091
  await sandboxCommand2(projectRoot, sandboxArgs);
11923
12092
  break;