@locusai/cli 0.22.1 → 0.22.3

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 +193 -131
  2. package/package.json +2 -2
package/bin/locus.js CHANGED
@@ -896,9 +896,10 @@ __export(exports_sandbox, {
896
896
  getModelSandboxName: () => getModelSandboxName,
897
897
  displaySandboxWarning: () => displaySandboxWarning,
898
898
  detectSandboxSupport: () => detectSandboxSupport,
899
+ detectContainerWorkdir: () => detectContainerWorkdir,
899
900
  checkProviderSandboxMismatch: () => checkProviderSandboxMismatch
900
901
  });
901
- import { execFile } from "node:child_process";
902
+ import { execFile, execSync as execSync2 } from "node:child_process";
902
903
  import { createInterface } from "node:readline";
903
904
  function getProviderSandboxName(config, provider) {
904
905
  return config.providers[provider];
@@ -1033,6 +1034,30 @@ ${yellow2("⚠")} Docker sandbox not available. Install Docker Desktop 4.58+ fo
1033
1034
  }
1034
1035
  return true;
1035
1036
  }
1037
+ function detectContainerWorkdir(sandboxName, hostProjectRoot) {
1038
+ const log = getLogger();
1039
+ try {
1040
+ const containerPath = execSync2(`docker sandbox exec ${sandboxName} pwd`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1041
+ if (!containerPath) {
1042
+ log.debug("Container pwd returned empty result");
1043
+ return null;
1044
+ }
1045
+ if (containerPath === hostProjectRoot) {
1046
+ log.debug("Container workdir matches host path", { hostProjectRoot });
1047
+ return null;
1048
+ }
1049
+ log.debug("Detected container workdir differs from host", {
1050
+ hostProjectRoot,
1051
+ containerWorkdir: containerPath
1052
+ });
1053
+ return containerPath;
1054
+ } catch (err) {
1055
+ log.debug("Container workdir detection via pwd failed", {
1056
+ error: err instanceof Error ? err.message : String(err)
1057
+ });
1058
+ }
1059
+ return null;
1060
+ }
1036
1061
  function waitForEnter() {
1037
1062
  return new Promise((resolve) => {
1038
1063
  const rl = createInterface({
@@ -1062,7 +1087,7 @@ __export(exports_upgrade, {
1062
1087
  fetchLatestVersion: () => fetchLatestVersion,
1063
1088
  compareSemver: () => compareSemver
1064
1089
  });
1065
- import { execSync as execSync2 } from "node:child_process";
1090
+ import { execSync as execSync3 } from "node:child_process";
1066
1091
  function compareSemver(a, b) {
1067
1092
  const partsA = a.replace(/^v/, "").split(".").map(Number);
1068
1093
  const partsB = b.replace(/^v/, "").split(".").map(Number);
@@ -1078,7 +1103,7 @@ function compareSemver(a, b) {
1078
1103
  }
1079
1104
  function fetchLatestVersion() {
1080
1105
  try {
1081
- const result = execSync2(`npm view ${PACKAGE_NAME} version`, {
1106
+ const result = execSync3(`npm view ${PACKAGE_NAME} version`, {
1082
1107
  encoding: "utf-8",
1083
1108
  stdio: ["pipe", "pipe", "pipe"],
1084
1109
  timeout: 15000
@@ -1090,14 +1115,14 @@ function fetchLatestVersion() {
1090
1115
  }
1091
1116
  function installVersion(version) {
1092
1117
  try {
1093
- execSync2("npm cache clean --force", {
1118
+ execSync3("npm cache clean --force", {
1094
1119
  encoding: "utf-8",
1095
1120
  stdio: ["pipe", "pipe", "pipe"],
1096
1121
  timeout: 30000
1097
1122
  });
1098
1123
  } catch {}
1099
1124
  try {
1100
- execSync2(`npm install -g ${PACKAGE_NAME}@${version}`, {
1125
+ execSync3(`npm install -g ${PACKAGE_NAME}@${version}`, {
1101
1126
  encoding: "utf-8",
1102
1127
  stdio: ["pipe", "pipe", "pipe"],
1103
1128
  timeout: 120000
@@ -1109,7 +1134,7 @@ function installVersion(version) {
1109
1134
  }
1110
1135
  function verifyInstalled(expectedVersion) {
1111
1136
  try {
1112
- const installed = execSync2(`npm list -g ${PACKAGE_NAME} --depth=0 --json`, {
1137
+ const installed = execSync3(`npm list -g ${PACKAGE_NAME} --depth=0 --json`, {
1113
1138
  encoding: "utf-8",
1114
1139
  stdio: ["pipe", "pipe", "pipe"],
1115
1140
  timeout: 1e4
@@ -1473,7 +1498,7 @@ var init_ecosystem = __esm(() => {
1473
1498
  // src/core/github.ts
1474
1499
  import {
1475
1500
  execFileSync,
1476
- execSync as execSync3
1501
+ execSync as execSync4
1477
1502
  } from "node:child_process";
1478
1503
  function gh(args, options = {}) {
1479
1504
  const log = getLogger();
@@ -1482,7 +1507,7 @@ function gh(args, options = {}) {
1482
1507
  log.debug(`gh ${args}`, { cwd });
1483
1508
  const startTime = Date.now();
1484
1509
  try {
1485
- const result = execSync3(`gh ${args}`, {
1510
+ const result = execSync4(`gh ${args}`, {
1486
1511
  cwd,
1487
1512
  encoding: "utf-8",
1488
1513
  stdio: ["pipe", "pipe", "pipe"],
@@ -2231,7 +2256,7 @@ var exports_create = {};
2231
2256
  __export(exports_create, {
2232
2257
  createCommand: () => createCommand
2233
2258
  });
2234
- import { execSync as execSync4 } from "node:child_process";
2259
+ import { execSync as execSync5 } from "node:child_process";
2235
2260
  import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
2236
2261
  import { join as join7 } from "node:path";
2237
2262
  function validateName(name) {
@@ -2250,7 +2275,7 @@ function capitalize(str) {
2250
2275
  }
2251
2276
  function checkNpmExists(fullName) {
2252
2277
  try {
2253
- execSync4(`npm view ${fullName} version 2>/dev/null`, {
2278
+ execSync5(`npm view ${fullName} version 2>/dev/null`, {
2254
2279
  stdio: "pipe",
2255
2280
  timeout: 1e4
2256
2281
  });
@@ -2390,7 +2415,7 @@ function printHelp(): void {
2390
2415
  }
2391
2416
  `;
2392
2417
  }
2393
- function generateReadme(name, displayName, description) {
2418
+ function generateReadme(name, description) {
2394
2419
  return `# @locusai/locus-${name}
2395
2420
 
2396
2421
  ${description}
@@ -2545,7 +2570,7 @@ ${bold2("Creating package:")} ${cyan2(fullNpmName)}
2545
2570
  writeFileSync5(join7(packagesDir, "src", "index.ts"), generateIndexTs(name), "utf-8");
2546
2571
  process.stderr.write(`${green("✓")} Generated src/index.ts
2547
2572
  `);
2548
- writeFileSync5(join7(packagesDir, "README.md"), generateReadme(name, displayName, description), "utf-8");
2573
+ writeFileSync5(join7(packagesDir, "README.md"), generateReadme(name, description), "utf-8");
2549
2574
  process.stderr.write(`${green("✓")} Generated README.md
2550
2575
  `);
2551
2576
  process.stderr.write(`
@@ -3690,7 +3715,7 @@ var init_stream_renderer = __esm(() => {
3690
3715
  });
3691
3716
 
3692
3717
  // src/repl/clipboard.ts
3693
- import { execSync as execSync5 } from "node:child_process";
3718
+ import { execSync as execSync6 } from "node:child_process";
3694
3719
  import { existsSync as existsSync12, mkdirSync as mkdirSync8 } from "node:fs";
3695
3720
  import { tmpdir } from "node:os";
3696
3721
  import { join as join11 } from "node:path";
@@ -3729,7 +3754,7 @@ function readMacOSClipboardImage() {
3729
3754
  `return "ok"`
3730
3755
  ].join(`
3731
3756
  `);
3732
- const result = execSync5("osascript", {
3757
+ const result = execSync6("osascript", {
3733
3758
  input: script,
3734
3759
  encoding: "utf-8",
3735
3760
  timeout: 5000,
@@ -3743,13 +3768,13 @@ function readMacOSClipboardImage() {
3743
3768
  }
3744
3769
  function readLinuxClipboardImage() {
3745
3770
  try {
3746
- const targets = execSync5("xclip -selection clipboard -t TARGETS -o 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
3771
+ const targets = execSync6("xclip -selection clipboard -t TARGETS -o 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
3747
3772
  if (!targets.includes("image/png")) {
3748
3773
  return null;
3749
3774
  }
3750
3775
  ensureStableDir();
3751
3776
  const destPath = join11(STABLE_DIR, `clipboard-${Date.now()}.png`);
3752
- execSync5(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
3777
+ execSync6(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
3753
3778
  if (existsSync12(destPath)) {
3754
3779
  return destPath;
3755
3780
  }
@@ -4770,7 +4795,7 @@ __export(exports_claude, {
4770
4795
  buildClaudeArgs: () => buildClaudeArgs,
4771
4796
  ClaudeRunner: () => ClaudeRunner
4772
4797
  });
4773
- import { execSync as execSync6, spawn as spawn2 } from "node:child_process";
4798
+ import { execSync as execSync7, spawn as spawn2 } from "node:child_process";
4774
4799
  function buildClaudeArgs(options) {
4775
4800
  const args = ["--dangerously-skip-permissions", "--no-session-persistence"];
4776
4801
  if (options.model) {
@@ -4788,7 +4813,7 @@ class ClaudeRunner {
4788
4813
  aborted = false;
4789
4814
  async isAvailable() {
4790
4815
  try {
4791
- execSync6("claude --version", {
4816
+ execSync7("claude --version", {
4792
4817
  encoding: "utf-8",
4793
4818
  stdio: ["pipe", "pipe", "pipe"]
4794
4819
  });
@@ -4799,7 +4824,7 @@ class ClaudeRunner {
4799
4824
  }
4800
4825
  async getVersion() {
4801
4826
  try {
4802
- const output = execSync6("claude --version", {
4827
+ const output = execSync7("claude --version", {
4803
4828
  encoding: "utf-8",
4804
4829
  stdio: ["pipe", "pipe", "pipe"]
4805
4830
  }).trim();
@@ -5141,13 +5166,13 @@ function backupIgnoredFiles(projectRoot) {
5141
5166
  }
5142
5167
  };
5143
5168
  }
5144
- async function enforceSandboxIgnore(sandboxName, projectRoot) {
5169
+ async function enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir) {
5145
5170
  const log = getLogger();
5146
5171
  const ignorePath = join13(projectRoot, ".sandboxignore");
5147
5172
  const rules = parseIgnoreFile(ignorePath);
5148
5173
  if (rules.length === 0)
5149
5174
  return;
5150
- const script = buildCleanupScript(rules, projectRoot);
5175
+ const script = buildCleanupScript(rules, containerWorkdir ?? projectRoot);
5151
5176
  if (!script)
5152
5177
  return;
5153
5178
  log.debug("Enforcing .sandboxignore", {
@@ -5187,11 +5212,13 @@ import { spawn as spawn3 } from "node:child_process";
5187
5212
 
5188
5213
  class SandboxedClaudeRunner {
5189
5214
  sandboxName;
5215
+ containerWorkdir;
5190
5216
  name = "claude-sandboxed";
5191
5217
  process = null;
5192
5218
  aborted = false;
5193
- constructor(sandboxName) {
5219
+ constructor(sandboxName, containerWorkdir) {
5194
5220
  this.sandboxName = sandboxName;
5221
+ this.containerWorkdir = containerWorkdir;
5195
5222
  }
5196
5223
  async isAvailable() {
5197
5224
  const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
@@ -5217,13 +5244,14 @@ class SandboxedClaudeRunner {
5217
5244
  const claudeArgs = ["-p", options.prompt, ...buildClaudeArgs(options)];
5218
5245
  options.onStatusChange?.("Syncing sandbox...");
5219
5246
  const backup = backupIgnoredFiles(options.cwd);
5220
- await enforceSandboxIgnore(this.sandboxName, options.cwd);
5247
+ await enforceSandboxIgnore(this.sandboxName, options.cwd, this.containerWorkdir);
5221
5248
  options.onStatusChange?.("Thinking...");
5249
+ const workdir = this.containerWorkdir ?? options.cwd;
5222
5250
  const dockerArgs = [
5223
5251
  "sandbox",
5224
5252
  "exec",
5225
5253
  "-w",
5226
- options.cwd,
5254
+ workdir,
5227
5255
  this.sandboxName,
5228
5256
  "claude",
5229
5257
  ...claudeArgs
@@ -5388,7 +5416,7 @@ var init_claude_sandbox = __esm(() => {
5388
5416
  });
5389
5417
 
5390
5418
  // src/ai/codex.ts
5391
- import { execSync as execSync7, spawn as spawn4 } from "node:child_process";
5419
+ import { execSync as execSync8, spawn as spawn4 } from "node:child_process";
5392
5420
  function buildCodexArgs(model) {
5393
5421
  const args = ["exec", "--full-auto", "--skip-git-repo-check", "--json"];
5394
5422
  if (model) {
@@ -5404,7 +5432,7 @@ class CodexRunner {
5404
5432
  aborted = false;
5405
5433
  async isAvailable() {
5406
5434
  try {
5407
- execSync7("codex --version", {
5435
+ execSync8("codex --version", {
5408
5436
  encoding: "utf-8",
5409
5437
  stdio: ["pipe", "pipe", "pipe"]
5410
5438
  });
@@ -5415,7 +5443,7 @@ class CodexRunner {
5415
5443
  }
5416
5444
  async getVersion() {
5417
5445
  try {
5418
- const output = execSync7("codex --version", {
5446
+ const output = execSync8("codex --version", {
5419
5447
  encoding: "utf-8",
5420
5448
  stdio: ["pipe", "pipe", "pipe"]
5421
5449
  }).trim();
@@ -5567,12 +5595,14 @@ import { spawn as spawn5 } from "node:child_process";
5567
5595
 
5568
5596
  class SandboxedCodexRunner {
5569
5597
  sandboxName;
5598
+ containerWorkdir;
5570
5599
  name = "codex-sandboxed";
5571
5600
  process = null;
5572
5601
  aborted = false;
5573
5602
  codexInstalled = false;
5574
- constructor(sandboxName) {
5603
+ constructor(sandboxName, containerWorkdir) {
5575
5604
  this.sandboxName = sandboxName;
5605
+ this.containerWorkdir = containerWorkdir;
5576
5606
  }
5577
5607
  async isAvailable() {
5578
5608
  const delegate = new CodexRunner;
@@ -5596,19 +5626,20 @@ class SandboxedCodexRunner {
5596
5626
  const codexArgs = buildCodexArgs(options.model);
5597
5627
  options.onStatusChange?.("Syncing sandbox...");
5598
5628
  const backup = backupIgnoredFiles(options.cwd);
5599
- await enforceSandboxIgnore(this.sandboxName, options.cwd);
5629
+ await enforceSandboxIgnore(this.sandboxName, options.cwd, this.containerWorkdir);
5600
5630
  if (!this.codexInstalled) {
5601
5631
  options.onStatusChange?.("Checking codex...");
5602
5632
  await this.ensureCodexInstalled(this.sandboxName);
5603
5633
  this.codexInstalled = true;
5604
5634
  }
5605
5635
  options.onStatusChange?.("Thinking...");
5636
+ const workdir = this.containerWorkdir ?? options.cwd;
5606
5637
  const dockerArgs = [
5607
5638
  "sandbox",
5608
5639
  "exec",
5609
5640
  "-i",
5610
5641
  "-w",
5611
- options.cwd,
5642
+ workdir,
5612
5643
  this.sandboxName,
5613
5644
  "codex",
5614
5645
  ...codexArgs
@@ -5789,12 +5820,12 @@ async function createRunnerAsync(provider, sandboxed) {
5789
5820
  throw new Error(`Unknown AI provider: ${provider}`);
5790
5821
  }
5791
5822
  }
5792
- function createUserManagedSandboxRunner(provider, sandboxName) {
5823
+ function createUserManagedSandboxRunner(provider, sandboxName, containerWorkdir) {
5793
5824
  switch (provider) {
5794
5825
  case "claude":
5795
- return new SandboxedClaudeRunner(sandboxName);
5826
+ return new SandboxedClaudeRunner(sandboxName, containerWorkdir);
5796
5827
  case "codex":
5797
- return new SandboxedCodexRunner(sandboxName);
5828
+ return new SandboxedCodexRunner(sandboxName, containerWorkdir);
5798
5829
  default:
5799
5830
  throw new Error(`Unknown AI provider: ${provider}`);
5800
5831
  }
@@ -5905,7 +5936,7 @@ ${red2("✗")} ${dim2("Force exit.")}\r
5905
5936
  exitCode: 1
5906
5937
  };
5907
5938
  }
5908
- runner = createUserManagedSandboxRunner(resolvedProvider, options.sandboxName);
5939
+ runner = createUserManagedSandboxRunner(resolvedProvider, options.sandboxName, options.containerWorkdir);
5909
5940
  } else {
5910
5941
  runner = await createRunnerAsync(resolvedProvider, false);
5911
5942
  }
@@ -6240,7 +6271,8 @@ async function issueCreate(projectRoot, parsed) {
6240
6271
  silent: true,
6241
6272
  activity: "generating issue",
6242
6273
  sandboxed: config.sandbox.enabled,
6243
- sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider)
6274
+ sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider),
6275
+ containerWorkdir: config.sandbox.containerWorkdir
6244
6276
  });
6245
6277
  if (!aiResult.success && !aiResult.interrupted) {
6246
6278
  process.stderr.write(`${red2("✗")} Failed to generate issue: ${aiResult.error}
@@ -7443,7 +7475,7 @@ var init_sprint = __esm(() => {
7443
7475
  });
7444
7476
 
7445
7477
  // src/core/prompt-builder.ts
7446
- import { execSync as execSync8 } from "node:child_process";
7478
+ import { execSync as execSync9 } from "node:child_process";
7447
7479
  import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
7448
7480
  import { join as join14 } from "node:path";
7449
7481
  function buildExecutionPrompt(ctx) {
@@ -7592,7 +7624,7 @@ ${parts.join(`
7592
7624
  function buildRepoContext(projectRoot) {
7593
7625
  const parts = [];
7594
7626
  try {
7595
- const tree = execSync8("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();
7627
+ 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();
7596
7628
  if (tree) {
7597
7629
  parts.push(`<file-tree>
7598
7630
  \`\`\`
@@ -7602,7 +7634,7 @@ ${tree}
7602
7634
  }
7603
7635
  } catch {}
7604
7636
  try {
7605
- const gitLog = execSync8("git log --oneline -10", {
7637
+ const gitLog = execSync9("git log --oneline -10", {
7606
7638
  cwd: projectRoot,
7607
7639
  encoding: "utf-8",
7608
7640
  stdio: ["pipe", "pipe", "pipe"]
@@ -7616,7 +7648,7 @@ ${gitLog}
7616
7648
  }
7617
7649
  } catch {}
7618
7650
  try {
7619
- const branch = execSync8("git rev-parse --abbrev-ref HEAD", {
7651
+ const branch = execSync9("git rev-parse --abbrev-ref HEAD", {
7620
7652
  cwd: projectRoot,
7621
7653
  encoding: "utf-8",
7622
7654
  stdio: ["pipe", "pipe", "pipe"]
@@ -7872,7 +7904,7 @@ var init_diff_renderer = __esm(() => {
7872
7904
  });
7873
7905
 
7874
7906
  // src/repl/commands.ts
7875
- import { execSync as execSync9 } from "node:child_process";
7907
+ import { execSync as execSync10 } from "node:child_process";
7876
7908
  function getSlashCommands() {
7877
7909
  return [
7878
7910
  {
@@ -8070,7 +8102,7 @@ function cmdModel(args, ctx) {
8070
8102
  }
8071
8103
  function cmdDiff(_args, ctx) {
8072
8104
  try {
8073
- const diff = execSync9("git diff", {
8105
+ const diff = execSync10("git diff", {
8074
8106
  cwd: ctx.projectRoot,
8075
8107
  encoding: "utf-8",
8076
8108
  stdio: ["pipe", "pipe", "pipe"]
@@ -8106,7 +8138,7 @@ function cmdDiff(_args, ctx) {
8106
8138
  }
8107
8139
  function cmdUndo(_args, ctx) {
8108
8140
  try {
8109
- const status = execSync9("git status --porcelain", {
8141
+ const status = execSync10("git status --porcelain", {
8110
8142
  cwd: ctx.projectRoot,
8111
8143
  encoding: "utf-8",
8112
8144
  stdio: ["pipe", "pipe", "pipe"]
@@ -8116,7 +8148,7 @@ function cmdUndo(_args, ctx) {
8116
8148
  `);
8117
8149
  return;
8118
8150
  }
8119
- execSync9("git checkout .", {
8151
+ execSync10("git checkout .", {
8120
8152
  cwd: ctx.projectRoot,
8121
8153
  encoding: "utf-8",
8122
8154
  stdio: ["pipe", "pipe", "pipe"]
@@ -8494,7 +8526,7 @@ var init_session_manager = __esm(() => {
8494
8526
  });
8495
8527
 
8496
8528
  // src/repl/voice.ts
8497
- import { execSync as execSync10, spawn as spawn6 } from "node:child_process";
8529
+ import { execSync as execSync11, spawn as spawn6 } from "node:child_process";
8498
8530
  import { existsSync as existsSync18, mkdirSync as mkdirSync13, unlinkSync as unlinkSync4 } from "node:fs";
8499
8531
  import { cpus, homedir as homedir4, platform, tmpdir as tmpdir4 } from "node:os";
8500
8532
  import { join as join18 } from "node:path";
@@ -8504,7 +8536,7 @@ function getWhisperModelPath() {
8504
8536
  function commandExists(cmd) {
8505
8537
  try {
8506
8538
  const which = platform() === "win32" ? "where" : "which";
8507
- execSync10(`${which} ${cmd}`, { stdio: "pipe" });
8539
+ execSync11(`${which} ${cmd}`, { stdio: "pipe" });
8508
8540
  return true;
8509
8541
  } catch {
8510
8542
  return false;
@@ -8630,22 +8662,22 @@ function installSox(pm) {
8630
8662
  try {
8631
8663
  switch (pm) {
8632
8664
  case "brew":
8633
- execSync10("brew install sox", { stdio: "inherit", timeout: 300000 });
8665
+ execSync11("brew install sox", { stdio: "inherit", timeout: 300000 });
8634
8666
  break;
8635
8667
  case "apt":
8636
- execSync10("sudo apt-get install -y sox", {
8668
+ execSync11("sudo apt-get install -y sox", {
8637
8669
  stdio: "inherit",
8638
8670
  timeout: 300000
8639
8671
  });
8640
8672
  break;
8641
8673
  case "dnf":
8642
- execSync10("sudo dnf install -y sox", {
8674
+ execSync11("sudo dnf install -y sox", {
8643
8675
  stdio: "inherit",
8644
8676
  timeout: 300000
8645
8677
  });
8646
8678
  break;
8647
8679
  case "pacman":
8648
- execSync10("sudo pacman -S --noconfirm sox", {
8680
+ execSync11("sudo pacman -S --noconfirm sox", {
8649
8681
  stdio: "inherit",
8650
8682
  timeout: 300000
8651
8683
  });
@@ -8659,7 +8691,7 @@ function installSox(pm) {
8659
8691
  function installWhisperCpp(pm) {
8660
8692
  if (pm === "brew") {
8661
8693
  try {
8662
- execSync10("brew install whisper-cpp", {
8694
+ execSync11("brew install whisper-cpp", {
8663
8695
  stdio: "inherit",
8664
8696
  timeout: 300000
8665
8697
  });
@@ -8681,19 +8713,19 @@ function ensureBuildDeps(pm) {
8681
8713
  try {
8682
8714
  switch (pm) {
8683
8715
  case "apt":
8684
- execSync10("sudo apt-get install -y cmake g++ make git", {
8716
+ execSync11("sudo apt-get install -y cmake g++ make git", {
8685
8717
  stdio: "inherit",
8686
8718
  timeout: 300000
8687
8719
  });
8688
8720
  break;
8689
8721
  case "dnf":
8690
- execSync10("sudo dnf install -y cmake gcc-c++ make git", {
8722
+ execSync11("sudo dnf install -y cmake gcc-c++ make git", {
8691
8723
  stdio: "inherit",
8692
8724
  timeout: 300000
8693
8725
  });
8694
8726
  break;
8695
8727
  case "pacman":
8696
- execSync10("sudo pacman -S --noconfirm cmake gcc make git", {
8728
+ execSync11("sudo pacman -S --noconfirm cmake gcc make git", {
8697
8729
  stdio: "inherit",
8698
8730
  timeout: 300000
8699
8731
  });
@@ -8719,17 +8751,17 @@ function buildWhisperFromSource(pm) {
8719
8751
  mkdirSync13(LOCUS_BIN_DIR, { recursive: true });
8720
8752
  out.write(` ${dim2("Cloning whisper.cpp...")}
8721
8753
  `);
8722
- execSync10(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join18(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
8754
+ execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join18(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
8723
8755
  const srcDir = join18(buildDir, "whisper.cpp");
8724
8756
  const numCpus = cpus().length || 2;
8725
8757
  out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
8726
8758
  `);
8727
- execSync10("cmake -B build -DCMAKE_BUILD_TYPE=Release", {
8759
+ execSync11("cmake -B build -DCMAKE_BUILD_TYPE=Release", {
8728
8760
  cwd: srcDir,
8729
8761
  stdio: ["pipe", "pipe", "pipe"],
8730
8762
  timeout: 120000
8731
8763
  });
8732
- execSync10(`cmake --build build --config Release -j${numCpus}`, {
8764
+ execSync11(`cmake --build build --config Release -j${numCpus}`, {
8733
8765
  cwd: srcDir,
8734
8766
  stdio: ["pipe", "pipe", "pipe"],
8735
8767
  timeout: 600000
@@ -8741,7 +8773,7 @@ function buildWhisperFromSource(pm) {
8741
8773
  ];
8742
8774
  for (const candidate of binaryCandidates) {
8743
8775
  if (existsSync18(candidate)) {
8744
- execSync10(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
8776
+ execSync11(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
8745
8777
  stdio: "pipe"
8746
8778
  });
8747
8779
  return true;
@@ -8756,7 +8788,7 @@ function buildWhisperFromSource(pm) {
8756
8788
  return false;
8757
8789
  } finally {
8758
8790
  try {
8759
- execSync10(`rm -rf "${buildDir}"`, { stdio: "pipe" });
8791
+ execSync11(`rm -rf "${buildDir}"`, { stdio: "pipe" });
8760
8792
  } catch {}
8761
8793
  }
8762
8794
  }
@@ -8827,12 +8859,12 @@ function downloadModel() {
8827
8859
  `);
8828
8860
  try {
8829
8861
  if (commandExists("curl")) {
8830
- execSync10(`curl -L -o "${modelPath}" "${url}"`, {
8862
+ execSync11(`curl -L -o "${modelPath}" "${url}"`, {
8831
8863
  stdio: ["pipe", "pipe", "pipe"],
8832
8864
  timeout: 300000
8833
8865
  });
8834
8866
  } else if (commandExists("wget")) {
8835
- execSync10(`wget -O "${modelPath}" "${url}"`, {
8867
+ execSync11(`wget -O "${modelPath}" "${url}"`, {
8836
8868
  stdio: ["pipe", "pipe", "pipe"],
8837
8869
  timeout: 300000
8838
8870
  });
@@ -9030,7 +9062,7 @@ var init_voice = __esm(() => {
9030
9062
  });
9031
9063
 
9032
9064
  // src/repl/repl.ts
9033
- import { execSync as execSync11 } from "node:child_process";
9065
+ import { execSync as execSync12 } from "node:child_process";
9034
9066
  async function startRepl(options) {
9035
9067
  const { projectRoot, config } = options;
9036
9068
  const sessionManager = new SessionManager(projectRoot);
@@ -9048,7 +9080,7 @@ async function startRepl(options) {
9048
9080
  } else {
9049
9081
  let branch = "main";
9050
9082
  try {
9051
- branch = execSync11("git rev-parse --abbrev-ref HEAD", {
9083
+ branch = execSync12("git rev-parse --abbrev-ref HEAD", {
9052
9084
  cwd: projectRoot,
9053
9085
  encoding: "utf-8",
9054
9086
  stdio: ["pipe", "pipe", "pipe"]
@@ -9100,7 +9132,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
9100
9132
  const provider = inferProviderFromModel(config.ai.model) || config.ai.provider;
9101
9133
  const sandboxName = getProviderSandboxName(config.sandbox, provider);
9102
9134
  if (sandboxName) {
9103
- sandboxRunner = createUserManagedSandboxRunner(provider, sandboxName);
9135
+ sandboxRunner = createUserManagedSandboxRunner(provider, sandboxName, config.sandbox.containerWorkdir);
9104
9136
  process.stderr.write(`${dim2("Using")} ${dim2(provider)} ${dim2("sandbox")} ${dim2(sandboxName)}
9105
9137
  `);
9106
9138
  } else {
@@ -9151,7 +9183,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
9151
9183
  if (providerChanged && config.sandbox.enabled) {
9152
9184
  const sandboxName = getProviderSandboxName(config.sandbox, inferredProvider);
9153
9185
  if (sandboxName) {
9154
- sandboxRunner = createUserManagedSandboxRunner(inferredProvider, sandboxName);
9186
+ sandboxRunner = createUserManagedSandboxRunner(inferredProvider, sandboxName, config.sandbox.containerWorkdir);
9155
9187
  process.stderr.write(`${dim2("Switched sandbox agent to")} ${dim2(inferredProvider)} ${dim2(`(${sandboxName})`)}
9156
9188
  `);
9157
9189
  } else {
@@ -9279,6 +9311,7 @@ async function executeAITurn(prompt, session, options, verbose = false, runner)
9279
9311
  verbose,
9280
9312
  sandboxed: config.sandbox.enabled,
9281
9313
  sandboxName,
9314
+ containerWorkdir: config.sandbox.containerWorkdir,
9282
9315
  runner
9283
9316
  });
9284
9317
  if (aiResult.interrupted) {
@@ -9483,7 +9516,7 @@ async function handleJsonStream(projectRoot, config, args, sessionId) {
9483
9516
  try {
9484
9517
  const fullPrompt = buildReplPrompt(prompt, projectRoot, config);
9485
9518
  const sandboxName = getProviderSandboxName(config.sandbox, config.ai.provider);
9486
- const runner = config.sandbox.enabled ? sandboxName ? createUserManagedSandboxRunner(config.ai.provider, sandboxName) : null : await createRunnerAsync(config.ai.provider, false);
9519
+ const runner = config.sandbox.enabled ? sandboxName ? createUserManagedSandboxRunner(config.ai.provider, sandboxName, config.sandbox.containerWorkdir) : null : await createRunnerAsync(config.ai.provider, false);
9487
9520
  if (!runner) {
9488
9521
  const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
9489
9522
  stream.emitError(mismatch ?? `No sandbox configured for "${config.ai.provider}". Run "locus sandbox" to create one.`, false);
@@ -9536,11 +9569,11 @@ var init_exec = __esm(() => {
9536
9569
  });
9537
9570
 
9538
9571
  // src/core/submodule.ts
9539
- import { execSync as execSync12 } from "node:child_process";
9572
+ import { execSync as execSync13 } from "node:child_process";
9540
9573
  import { existsSync as existsSync19 } from "node:fs";
9541
9574
  import { join as join19 } from "node:path";
9542
9575
  function git2(args, cwd) {
9543
- return execSync12(`git ${args}`, {
9576
+ return execSync13(`git ${args}`, {
9544
9577
  cwd,
9545
9578
  encoding: "utf-8",
9546
9579
  stdio: ["pipe", "pipe", "pipe"]
@@ -9608,7 +9641,7 @@ function commitDirtySubmodules(cwd, issueNumber, issueTitle) {
9608
9641
  const message = `chore: complete #${issueNumber} - ${issueTitle}
9609
9642
 
9610
9643
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
9611
- execSync12("git commit -F -", {
9644
+ execSync13("git commit -F -", {
9612
9645
  input: message,
9613
9646
  cwd: sub.absolutePath,
9614
9647
  encoding: "utf-8",
@@ -9695,7 +9728,7 @@ var init_submodule = __esm(() => {
9695
9728
  });
9696
9729
 
9697
9730
  // src/core/agent.ts
9698
- import { execSync as execSync13 } from "node:child_process";
9731
+ import { execSync as execSync14 } from "node:child_process";
9699
9732
  async function executeIssue(projectRoot, options) {
9700
9733
  const log = getLogger();
9701
9734
  const timer = createTimer();
@@ -9724,7 +9757,7 @@ ${cyan2("●")} ${bold2(`#${issueNumber}`)} ${issue.title}
9724
9757
  }
9725
9758
  let issueComments = [];
9726
9759
  try {
9727
- const commentsRaw = execSync13(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
9760
+ const commentsRaw = execSync14(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
9728
9761
  if (commentsRaw) {
9729
9762
  issueComments = commentsRaw.split(`
9730
9763
  `).filter(Boolean);
@@ -9761,7 +9794,8 @@ ${yellow2("⚠")} ${bold2("Dry run")} — would execute with:
9761
9794
  cwd: options.worktreePath ?? projectRoot,
9762
9795
  activity: `issue #${issueNumber}`,
9763
9796
  sandboxed: options.sandboxed,
9764
- sandboxName: options.sandboxName
9797
+ sandboxName: options.sandboxName,
9798
+ containerWorkdir: options.containerWorkdir
9765
9799
  });
9766
9800
  const output = aiResult.output;
9767
9801
  if (aiResult.interrupted) {
@@ -9867,7 +9901,8 @@ ${c.body}`),
9867
9901
  cwd: projectRoot,
9868
9902
  activity: `iterating on PR #${prNumber}`,
9869
9903
  sandboxed: config.sandbox.enabled,
9870
- sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider)
9904
+ sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider),
9905
+ containerWorkdir: config.sandbox.containerWorkdir
9871
9906
  });
9872
9907
  if (aiResult.interrupted) {
9873
9908
  process.stderr.write(`
@@ -9888,12 +9923,12 @@ ${aiResult.success ? green("✓") : red2("✗")} Iteration ${aiResult.success ?
9888
9923
  }
9889
9924
  async function createIssuePR(projectRoot, config, issue) {
9890
9925
  try {
9891
- const currentBranch = execSync13("git rev-parse --abbrev-ref HEAD", {
9926
+ const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
9892
9927
  cwd: projectRoot,
9893
9928
  encoding: "utf-8",
9894
9929
  stdio: ["pipe", "pipe", "pipe"]
9895
9930
  }).trim();
9896
- const diff = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9931
+ const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9897
9932
  cwd: projectRoot,
9898
9933
  encoding: "utf-8",
9899
9934
  stdio: ["pipe", "pipe", "pipe"]
@@ -9903,7 +9938,7 @@ async function createIssuePR(projectRoot, config, issue) {
9903
9938
  return;
9904
9939
  }
9905
9940
  pushSubmoduleBranches(projectRoot);
9906
- execSync13(`git push -u origin ${currentBranch}`, {
9941
+ execSync14(`git push -u origin ${currentBranch}`, {
9907
9942
  cwd: projectRoot,
9908
9943
  encoding: "utf-8",
9909
9944
  stdio: ["pipe", "pipe", "pipe"]
@@ -9958,9 +9993,9 @@ var init_agent = __esm(() => {
9958
9993
  });
9959
9994
 
9960
9995
  // src/core/conflict.ts
9961
- import { execSync as execSync14 } from "node:child_process";
9996
+ import { execSync as execSync15 } from "node:child_process";
9962
9997
  function git3(args, cwd) {
9963
- return execSync14(`git ${args}`, {
9998
+ return execSync15(`git ${args}`, {
9964
9999
  cwd,
9965
10000
  encoding: "utf-8",
9966
10001
  stdio: ["pipe", "pipe", "pipe"]
@@ -10261,11 +10296,11 @@ var init_shutdown = __esm(() => {
10261
10296
  });
10262
10297
 
10263
10298
  // src/core/worktree.ts
10264
- import { execSync as execSync15 } from "node:child_process";
10299
+ import { execSync as execSync16 } from "node:child_process";
10265
10300
  import { existsSync as existsSync21, readdirSync as readdirSync7, realpathSync, statSync as statSync4 } from "node:fs";
10266
10301
  import { join as join21 } from "node:path";
10267
10302
  function git4(args, cwd) {
10268
- return execSync15(`git ${args}`, {
10303
+ return execSync16(`git ${args}`, {
10269
10304
  cwd,
10270
10305
  encoding: "utf-8",
10271
10306
  stdio: ["pipe", "pipe", "pipe"]
@@ -10290,7 +10325,7 @@ function generateBranchName(issueNumber) {
10290
10325
  }
10291
10326
  function getWorktreeBranch(worktreePath) {
10292
10327
  try {
10293
- return execSync15("git branch --show-current", {
10328
+ return execSync16("git branch --show-current", {
10294
10329
  cwd: worktreePath,
10295
10330
  encoding: "utf-8",
10296
10331
  stdio: ["pipe", "pipe", "pipe"]
@@ -10417,12 +10452,12 @@ var exports_run = {};
10417
10452
  __export(exports_run, {
10418
10453
  runCommand: () => runCommand
10419
10454
  });
10420
- import { execSync as execSync16 } from "node:child_process";
10455
+ import { execSync as execSync17 } from "node:child_process";
10421
10456
  function resolveExecutionContext(config, modelOverride) {
10422
10457
  const model = modelOverride ?? config.ai.model;
10423
10458
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
10424
10459
  const sandboxName = getModelSandboxName(config.sandbox, model, provider);
10425
- return { provider, model, sandboxName };
10460
+ return { provider, model, sandboxName, containerWorkdir: config.sandbox.containerWorkdir };
10426
10461
  }
10427
10462
  function printRunHelp() {
10428
10463
  process.stderr.write(`
@@ -10577,7 +10612,7 @@ ${yellow2("⚠")} A sprint run is already in progress.
10577
10612
  }
10578
10613
  if (!flags.dryRun) {
10579
10614
  try {
10580
- execSync16(`git checkout -B ${branchName}`, {
10615
+ execSync17(`git checkout -B ${branchName}`, {
10581
10616
  cwd: projectRoot,
10582
10617
  encoding: "utf-8",
10583
10618
  stdio: ["pipe", "pipe", "pipe"]
@@ -10627,7 +10662,7 @@ ${red2("✗")} Auto-rebase failed. Resolve manually.
10627
10662
  let sprintContext;
10628
10663
  if (i > 0 && !flags.dryRun) {
10629
10664
  try {
10630
- sprintContext = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD`, {
10665
+ sprintContext = execSync17(`git diff origin/${config.agent.baseBranch}..HEAD`, {
10631
10666
  cwd: projectRoot,
10632
10667
  encoding: "utf-8",
10633
10668
  stdio: ["pipe", "pipe", "pipe"]
@@ -10648,7 +10683,8 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
10648
10683
  sprintContext,
10649
10684
  skipPR: true,
10650
10685
  sandboxed,
10651
- sandboxName: execution.sandboxName
10686
+ sandboxName: execution.sandboxName,
10687
+ containerWorkdir: execution.containerWorkdir
10652
10688
  });
10653
10689
  if (result.success) {
10654
10690
  if (!flags.dryRun) {
@@ -10692,7 +10728,7 @@ ${bold2("Summary:")}
10692
10728
  const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
10693
10729
  if (prNumber !== undefined) {
10694
10730
  try {
10695
- execSync16(`git checkout ${config.agent.baseBranch}`, {
10731
+ execSync17(`git checkout ${config.agent.baseBranch}`, {
10696
10732
  cwd: projectRoot,
10697
10733
  encoding: "utf-8",
10698
10734
  stdio: ["pipe", "pipe", "pipe"]
@@ -10725,7 +10761,8 @@ ${bold2("Running sprint issue")} ${cyan2(`#${issueNumber}`)} ${dim2("(sequential
10725
10761
  model: execution.model,
10726
10762
  dryRun: flags.dryRun,
10727
10763
  sandboxed,
10728
- sandboxName: execution.sandboxName
10764
+ sandboxName: execution.sandboxName,
10765
+ containerWorkdir: execution.containerWorkdir
10729
10766
  });
10730
10767
  return;
10731
10768
  }
@@ -10737,7 +10774,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
10737
10774
  `);
10738
10775
  if (!flags.dryRun) {
10739
10776
  try {
10740
- execSync16(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
10777
+ execSync17(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
10741
10778
  cwd: projectRoot,
10742
10779
  encoding: "utf-8",
10743
10780
  stdio: ["pipe", "pipe", "pipe"]
@@ -10762,7 +10799,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
10762
10799
  if (!flags.dryRun) {
10763
10800
  if (result.success) {
10764
10801
  try {
10765
- execSync16(`git checkout ${config.agent.baseBranch}`, {
10802
+ execSync17(`git checkout ${config.agent.baseBranch}`, {
10766
10803
  cwd: projectRoot,
10767
10804
  encoding: "utf-8",
10768
10805
  stdio: ["pipe", "pipe", "pipe"]
@@ -10833,7 +10870,8 @@ ${bold2("Running")} ${cyan2(`${issueNumbers.length} issues`)} ${dim2(`(max ${max
10833
10870
  model: execution.model,
10834
10871
  dryRun: flags.dryRun,
10835
10872
  sandboxed,
10836
- sandboxName: execution.sandboxName
10873
+ sandboxName: execution.sandboxName,
10874
+ containerWorkdir: execution.containerWorkdir
10837
10875
  });
10838
10876
  if (result.success) {
10839
10877
  markTaskDone(state, issueNumber, result.prNumber);
@@ -10899,13 +10937,13 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
10899
10937
  `);
10900
10938
  if (state.type === "sprint" && state.branch) {
10901
10939
  try {
10902
- const currentBranch = execSync16("git rev-parse --abbrev-ref HEAD", {
10940
+ const currentBranch = execSync17("git rev-parse --abbrev-ref HEAD", {
10903
10941
  cwd: projectRoot,
10904
10942
  encoding: "utf-8",
10905
10943
  stdio: ["pipe", "pipe", "pipe"]
10906
10944
  }).trim();
10907
10945
  if (currentBranch !== state.branch) {
10908
- execSync16(`git checkout ${state.branch}`, {
10946
+ execSync17(`git checkout ${state.branch}`, {
10909
10947
  cwd: projectRoot,
10910
10948
  encoding: "utf-8",
10911
10949
  stdio: ["pipe", "pipe", "pipe"]
@@ -10933,7 +10971,8 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
10933
10971
  model: execution.model,
10934
10972
  skipPR: isSprintRun,
10935
10973
  sandboxed,
10936
- sandboxName: execution.sandboxName
10974
+ sandboxName: execution.sandboxName,
10975
+ containerWorkdir: execution.containerWorkdir
10937
10976
  });
10938
10977
  if (result.success) {
10939
10978
  if (isSprintRun) {
@@ -10972,7 +11011,7 @@ ${bold2("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fai
10972
11011
  const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
10973
11012
  if (prNumber !== undefined) {
10974
11013
  try {
10975
- execSync16(`git checkout ${config.agent.baseBranch}`, {
11014
+ execSync17(`git checkout ${config.agent.baseBranch}`, {
10976
11015
  cwd: projectRoot,
10977
11016
  encoding: "utf-8",
10978
11017
  stdio: ["pipe", "pipe", "pipe"]
@@ -11008,14 +11047,14 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
11008
11047
  process.stderr.write(` ${dim2(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
11009
11048
  `);
11010
11049
  }
11011
- const status = execSync16("git status --porcelain", {
11050
+ const status = execSync17("git status --porcelain", {
11012
11051
  cwd: projectRoot,
11013
11052
  encoding: "utf-8",
11014
11053
  stdio: ["pipe", "pipe", "pipe"]
11015
11054
  }).trim();
11016
11055
  if (!status)
11017
11056
  return;
11018
- execSync16("git add -A", {
11057
+ execSync17("git add -A", {
11019
11058
  cwd: projectRoot,
11020
11059
  encoding: "utf-8",
11021
11060
  stdio: ["pipe", "pipe", "pipe"]
@@ -11023,7 +11062,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
11023
11062
  const message = `chore: complete #${issueNumber} - ${issueTitle}
11024
11063
 
11025
11064
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
11026
- execSync16(`git commit -F -`, {
11065
+ execSync17(`git commit -F -`, {
11027
11066
  input: message,
11028
11067
  cwd: projectRoot,
11029
11068
  encoding: "utf-8",
@@ -11037,7 +11076,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
11037
11076
  if (!config.agent.autoPR)
11038
11077
  return;
11039
11078
  try {
11040
- const diff = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
11079
+ const diff = execSync17(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
11041
11080
  cwd: projectRoot,
11042
11081
  encoding: "utf-8",
11043
11082
  stdio: ["pipe", "pipe", "pipe"]
@@ -11048,7 +11087,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
11048
11087
  return;
11049
11088
  }
11050
11089
  pushSubmoduleBranches(projectRoot);
11051
- execSync16(`git push -u origin ${branchName}`, {
11090
+ execSync17(`git push -u origin ${branchName}`, {
11052
11091
  cwd: projectRoot,
11053
11092
  encoding: "utf-8",
11054
11093
  stdio: ["pipe", "pipe", "pipe"]
@@ -11467,7 +11506,8 @@ ${bold2("Planning:")} ${cyan2(displayDirective)}
11467
11506
  cwd: projectRoot,
11468
11507
  activity: "planning",
11469
11508
  sandboxed: config.sandbox.enabled,
11470
- sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
11509
+ sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
11510
+ containerWorkdir: config.sandbox.containerWorkdir
11471
11511
  });
11472
11512
  if (aiResult.interrupted) {
11473
11513
  process.stderr.write(`
@@ -11590,7 +11630,8 @@ Start with foundational/setup tasks, then core features, then integration/testin
11590
11630
  activity: "issue ordering",
11591
11631
  silent: true,
11592
11632
  sandboxed: config.sandbox.enabled,
11593
- sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
11633
+ sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
11634
+ containerWorkdir: config.sandbox.containerWorkdir
11594
11635
  });
11595
11636
  if (aiResult.interrupted) {
11596
11637
  process.stderr.write(`
@@ -11850,7 +11891,7 @@ var exports_review = {};
11850
11891
  __export(exports_review, {
11851
11892
  reviewCommand: () => reviewCommand
11852
11893
  });
11853
- import { execFileSync as execFileSync2, execSync as execSync17 } from "node:child_process";
11894
+ import { execFileSync as execFileSync2, execSync as execSync18 } from "node:child_process";
11854
11895
  import { existsSync as existsSync23, readFileSync as readFileSync14 } from "node:fs";
11855
11896
  import { join as join23 } from "node:path";
11856
11897
  function printHelp3() {
@@ -11936,7 +11977,7 @@ ${bold2("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red2(
11936
11977
  async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
11937
11978
  let prInfo;
11938
11979
  try {
11939
- 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"] });
11980
+ const result = execSync18(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11940
11981
  const raw = JSON.parse(result);
11941
11982
  prInfo = {
11942
11983
  number: raw.number,
@@ -11981,7 +12022,8 @@ async function reviewPR(projectRoot, config, pr, focus, flags) {
11981
12022
  cwd: projectRoot,
11982
12023
  activity: `PR #${pr.number}`,
11983
12024
  sandboxed: config.sandbox.enabled,
11984
- sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
12025
+ sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
12026
+ containerWorkdir: config.sandbox.containerWorkdir
11985
12027
  });
11986
12028
  if (aiResult.interrupted) {
11987
12029
  process.stderr.write(` ${yellow2("⚡")} Review interrupted.
@@ -12083,7 +12125,7 @@ var exports_iterate = {};
12083
12125
  __export(exports_iterate, {
12084
12126
  iterateCommand: () => iterateCommand
12085
12127
  });
12086
- import { execSync as execSync18 } from "node:child_process";
12128
+ import { execSync as execSync19 } from "node:child_process";
12087
12129
  function printHelp4() {
12088
12130
  process.stderr.write(`
12089
12131
  ${bold2("locus iterate")} — Re-execute tasks with PR feedback
@@ -12301,12 +12343,12 @@ ${bold2("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red2(`✗ ${
12301
12343
  }
12302
12344
  function findPRForIssue(projectRoot, issueNumber) {
12303
12345
  try {
12304
- const result = execSync18(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12346
+ const result = execSync19(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12305
12347
  const parsed = JSON.parse(result);
12306
12348
  if (parsed.length > 0) {
12307
12349
  return parsed[0].number;
12308
12350
  }
12309
- const branchResult = execSync18(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12351
+ const branchResult = execSync19(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12310
12352
  const branchParsed = JSON.parse(branchResult);
12311
12353
  if (branchParsed.length > 0) {
12312
12354
  return branchParsed[0].number;
@@ -12563,7 +12605,8 @@ ${bold2("Discussion:")} ${cyan2(topic)}
12563
12605
  cwd: projectRoot,
12564
12606
  activity: "discussion",
12565
12607
  sandboxed: config.sandbox.enabled,
12566
- sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
12608
+ sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
12609
+ containerWorkdir: config.sandbox.containerWorkdir
12567
12610
  });
12568
12611
  if (aiResult.interrupted) {
12569
12612
  process.stderr.write(`
@@ -12936,7 +12979,7 @@ __export(exports_sandbox2, {
12936
12979
  parseSandboxLogsArgs: () => parseSandboxLogsArgs,
12937
12980
  parseSandboxInstallArgs: () => parseSandboxInstallArgs
12938
12981
  });
12939
- import { execSync as execSync19, spawn as spawn7 } from "node:child_process";
12982
+ import { execSync as execSync20, spawn as spawn7 } from "node:child_process";
12940
12983
  import { createHash } from "node:crypto";
12941
12984
  import { existsSync as existsSync26, readFileSync as readFileSync17 } from "node:fs";
12942
12985
  import { basename as basename4, join as join26 } from "node:path";
@@ -13070,11 +13113,26 @@ async function handleCreate(projectRoot) {
13070
13113
  }
13071
13114
  process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold2(name)}
13072
13115
  `);
13116
+ if (!config.sandbox.containerWorkdir) {
13117
+ const containerWorkdir = detectContainerWorkdir(name, projectRoot);
13118
+ if (containerWorkdir) {
13119
+ config.sandbox.containerWorkdir = containerWorkdir;
13120
+ process.stderr.write(` ${dim2(`Container workdir: ${containerWorkdir}`)}
13121
+ `);
13122
+ }
13123
+ }
13124
+ const workdir = config.sandbox.containerWorkdir ?? projectRoot;
13125
+ const backup = backupIgnoredFiles(projectRoot);
13126
+ try {
13127
+ await enforceSandboxIgnore(name, projectRoot, config.sandbox.containerWorkdir);
13128
+ } finally {
13129
+ backup.restore();
13130
+ }
13073
13131
  readySandboxes[provider] = name;
13074
13132
  config.sandbox.enabled = true;
13075
13133
  config.sandbox.providers = readySandboxes;
13076
13134
  saveConfig(projectRoot, config);
13077
- await runSandboxSetup(name, projectRoot);
13135
+ await runSandboxSetup(name, projectRoot, workdir);
13078
13136
  process.stderr.write(`
13079
13137
  ${green("✓")} Sandbox mode enabled for ${bold2(provider)}.
13080
13138
  `);
@@ -13099,19 +13157,20 @@ async function handleAgentLogin(projectRoot, agent) {
13099
13157
  if (agent === "codex") {
13100
13158
  await ensureCodexInSandbox(sandboxName);
13101
13159
  }
13160
+ const workdir = config.sandbox.containerWorkdir ?? projectRoot;
13102
13161
  process.stderr.write(`Connecting to ${agent} sandbox ${dim2(sandboxName)}...
13103
13162
  `);
13104
13163
  process.stderr.write(`${dim2("Login and then exit when ready.")}
13105
13164
 
13106
13165
  `);
13107
- const child = spawn7("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
13166
+ const child = spawn7("docker", ["sandbox", "exec", "-it", "-w", workdir, sandboxName, agent], {
13108
13167
  stdio: "inherit"
13109
13168
  });
13110
13169
  await new Promise((resolve2) => {
13111
13170
  child.on("close", async (code) => {
13112
13171
  const backup = backupIgnoredFiles(projectRoot);
13113
13172
  try {
13114
- await enforceSandboxIgnore(sandboxName, projectRoot);
13173
+ await enforceSandboxIgnore(sandboxName, projectRoot, config.sandbox.containerWorkdir);
13115
13174
  } finally {
13116
13175
  backup.restore();
13117
13176
  }
@@ -13148,7 +13207,7 @@ function handleRemove(projectRoot) {
13148
13207
  process.stderr.write(`Removing sandbox ${bold2(sandboxName)}...
13149
13208
  `);
13150
13209
  try {
13151
- execSync19(`docker sandbox rm ${sandboxName}`, {
13210
+ execSync20(`docker sandbox rm ${sandboxName}`, {
13152
13211
  encoding: "utf-8",
13153
13212
  stdio: ["pipe", "pipe", "pipe"],
13154
13213
  timeout: 15000
@@ -13157,6 +13216,7 @@ function handleRemove(projectRoot) {
13157
13216
  }
13158
13217
  config.sandbox.providers = {};
13159
13218
  config.sandbox.enabled = false;
13219
+ delete config.sandbox.containerWorkdir;
13160
13220
  saveConfig(projectRoot, config);
13161
13221
  process.stderr.write(`${green("✓")} Provider sandboxes removed. Sandbox mode disabled.
13162
13222
  `);
@@ -13169,6 +13229,10 @@ ${bold2("Sandbox Status")}
13169
13229
  `);
13170
13230
  process.stderr.write(` ${dim2("Enabled:")} ${config.sandbox.enabled ? green("yes") : red2("no")}
13171
13231
  `);
13232
+ if (config.sandbox.containerWorkdir) {
13233
+ process.stderr.write(` ${dim2("Container workdir:")} ${config.sandbox.containerWorkdir}
13234
+ `);
13235
+ }
13172
13236
  for (const provider of PROVIDERS) {
13173
13237
  const name = config.sandbox.providers[provider];
13174
13238
  process.stderr.write(` ${dim2(`${provider}:`).padEnd(15)}${name ? bold2(name) : dim2("(not configured)")}
@@ -13305,10 +13369,12 @@ async function handleShell(projectRoot, args) {
13305
13369
  `);
13306
13370
  return;
13307
13371
  }
13372
+ const config = loadConfig(projectRoot);
13308
13373
  const sandboxName = getActiveProviderSandbox(projectRoot, provider);
13309
13374
  if (!sandboxName) {
13310
13375
  return;
13311
13376
  }
13377
+ const workdir = config.sandbox.containerWorkdir ?? projectRoot;
13312
13378
  process.stderr.write(`Opening shell in ${provider} sandbox ${dim2(sandboxName)}...
13313
13379
  `);
13314
13380
  await runInteractiveCommand("docker", [
@@ -13316,7 +13382,7 @@ async function handleShell(projectRoot, args) {
13316
13382
  "exec",
13317
13383
  "-it",
13318
13384
  "-w",
13319
- projectRoot,
13385
+ workdir,
13320
13386
  sandboxName,
13321
13387
  "sh"
13322
13388
  ]);
@@ -13429,7 +13495,8 @@ function getInstallCommand(pm) {
13429
13495
  return ["npm", "install"];
13430
13496
  }
13431
13497
  }
13432
- async function runSandboxSetup(sandboxName, projectRoot) {
13498
+ async function runSandboxSetup(sandboxName, projectRoot, containerWorkdir) {
13499
+ const workdir = containerWorkdir ?? projectRoot;
13433
13500
  const ecosystem = detectProjectEcosystem(projectRoot);
13434
13501
  const isJS = isJavaScriptEcosystem(ecosystem);
13435
13502
  if (isJS) {
@@ -13445,7 +13512,7 @@ Installing dependencies (${bold2(installCmd.join(" "))}) in sandbox ${dim2(sandb
13445
13512
  "sandbox",
13446
13513
  "exec",
13447
13514
  "-w",
13448
- projectRoot,
13515
+ workdir,
13449
13516
  sandboxName,
13450
13517
  ...installCmd
13451
13518
  ]);
@@ -13462,6 +13529,7 @@ ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
13462
13529
  `);
13463
13530
  }
13464
13531
  const setupScript = join26(projectRoot, ".locus", "sandbox-setup.sh");
13532
+ const containerSetupScript = containerWorkdir ? join26(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
13465
13533
  if (existsSync26(setupScript)) {
13466
13534
  process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
13467
13535
  `);
@@ -13469,10 +13537,10 @@ ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
13469
13537
  "sandbox",
13470
13538
  "exec",
13471
13539
  "-w",
13472
- projectRoot,
13540
+ workdir,
13473
13541
  sandboxName,
13474
13542
  "sh",
13475
- setupScript
13543
+ containerSetupScript
13476
13544
  ]);
13477
13545
  if (!hookOk) {
13478
13546
  process.stderr.write(`${yellow2("⚠")} Setup hook failed in sandbox ${dim2(sandboxName)}.
@@ -13503,7 +13571,7 @@ async function handleSetup(projectRoot) {
13503
13571
  `);
13504
13572
  continue;
13505
13573
  }
13506
- await runSandboxSetup(sandboxName, projectRoot);
13574
+ await runSandboxSetup(sandboxName, projectRoot, config.sandbox.containerWorkdir);
13507
13575
  }
13508
13576
  }
13509
13577
  function buildProviderSandboxNames(projectRoot) {
@@ -13550,7 +13618,7 @@ function runInteractiveCommand(command, args) {
13550
13618
  }
13551
13619
  async function createProviderSandbox(provider, sandboxName, projectRoot) {
13552
13620
  try {
13553
- execSync19(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
13621
+ execSync20(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
13554
13622
  stdio: ["pipe", "pipe", "pipe"],
13555
13623
  timeout: 120000
13556
13624
  });
@@ -13561,17 +13629,11 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
13561
13629
  if (provider === "codex") {
13562
13630
  await ensureCodexInSandbox(sandboxName);
13563
13631
  }
13564
- const backup = backupIgnoredFiles(projectRoot);
13565
- try {
13566
- await enforceSandboxIgnore(sandboxName, projectRoot);
13567
- } finally {
13568
- backup.restore();
13569
- }
13570
13632
  return true;
13571
13633
  }
13572
13634
  async function ensurePackageManagerInSandbox(sandboxName, pm) {
13573
13635
  try {
13574
- execSync19(`docker sandbox exec ${sandboxName} which ${pm}`, {
13636
+ execSync20(`docker sandbox exec ${sandboxName} which ${pm}`, {
13575
13637
  stdio: ["pipe", "pipe", "pipe"],
13576
13638
  timeout: 5000
13577
13639
  });
@@ -13580,7 +13642,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
13580
13642
  process.stderr.write(`Installing ${bold2(pm)} in sandbox...
13581
13643
  `);
13582
13644
  try {
13583
- execSync19(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
13645
+ execSync20(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
13584
13646
  stdio: "inherit",
13585
13647
  timeout: 120000
13586
13648
  });
@@ -13592,7 +13654,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
13592
13654
  }
13593
13655
  async function ensureCodexInSandbox(sandboxName) {
13594
13656
  try {
13595
- execSync19(`docker sandbox exec ${sandboxName} which codex`, {
13657
+ execSync20(`docker sandbox exec ${sandboxName} which codex`, {
13596
13658
  stdio: ["pipe", "pipe", "pipe"],
13597
13659
  timeout: 5000
13598
13660
  });
@@ -13600,7 +13662,7 @@ async function ensureCodexInSandbox(sandboxName) {
13600
13662
  process.stderr.write(`Installing codex in sandbox...
13601
13663
  `);
13602
13664
  try {
13603
- execSync19(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
13665
+ execSync20(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
13604
13666
  } catch {
13605
13667
  process.stderr.write(`${red2("✗")} Failed to install codex in sandbox.
13606
13668
  `);
@@ -13609,7 +13671,7 @@ async function ensureCodexInSandbox(sandboxName) {
13609
13671
  }
13610
13672
  function isSandboxAlive(name) {
13611
13673
  try {
13612
- const output = execSync19("docker sandbox ls", {
13674
+ const output = execSync20("docker sandbox ls", {
13613
13675
  encoding: "utf-8",
13614
13676
  stdio: ["pipe", "pipe", "pipe"],
13615
13677
  timeout: 5000
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.22.1",
3
+ "version": "0.22.3",
4
4
  "description": "GitHub-native AI engineering assistant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "license": "MIT",
37
37
  "dependencies": {},
38
38
  "devDependencies": {
39
- "@locusai/sdk": "^0.22.1",
39
+ "@locusai/sdk": "^0.22.3",
40
40
  "@types/bun": "latest",
41
41
  "typescript": "^5.8.3"
42
42
  },