@locusai/cli 0.22.1 → 0.22.2

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 -127
  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,34 @@ ${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
+ execSync2(`docker sandbox exec ${sandboxName} test -d ${JSON.stringify(hostProjectRoot)}`, { stdio: ["pipe", "pipe", "pipe"], timeout: 5000 });
1041
+ log.debug("Container workdir matches host path", { hostProjectRoot });
1042
+ return null;
1043
+ } catch {}
1044
+ try {
1045
+ const result = execSync2(`docker sandbox exec ${sandboxName} find / -maxdepth 5 -path '*/.locus/config.json' -type f 2>/dev/null`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 }).trim();
1046
+ if (result) {
1047
+ const configPath = result.split(`
1048
+ `)[0].trim();
1049
+ const workdir = configPath.replace(/\/.locus\/config\.json$/, "");
1050
+ if (workdir) {
1051
+ log.debug("Detected container workdir", {
1052
+ hostProjectRoot,
1053
+ containerWorkdir: workdir
1054
+ });
1055
+ return workdir;
1056
+ }
1057
+ }
1058
+ } catch (err) {
1059
+ log.debug("Container workdir probe failed", {
1060
+ error: err instanceof Error ? err.message : String(err)
1061
+ });
1062
+ }
1063
+ return null;
1064
+ }
1036
1065
  function waitForEnter() {
1037
1066
  return new Promise((resolve) => {
1038
1067
  const rl = createInterface({
@@ -1062,7 +1091,7 @@ __export(exports_upgrade, {
1062
1091
  fetchLatestVersion: () => fetchLatestVersion,
1063
1092
  compareSemver: () => compareSemver
1064
1093
  });
1065
- import { execSync as execSync2 } from "node:child_process";
1094
+ import { execSync as execSync3 } from "node:child_process";
1066
1095
  function compareSemver(a, b) {
1067
1096
  const partsA = a.replace(/^v/, "").split(".").map(Number);
1068
1097
  const partsB = b.replace(/^v/, "").split(".").map(Number);
@@ -1078,7 +1107,7 @@ function compareSemver(a, b) {
1078
1107
  }
1079
1108
  function fetchLatestVersion() {
1080
1109
  try {
1081
- const result = execSync2(`npm view ${PACKAGE_NAME} version`, {
1110
+ const result = execSync3(`npm view ${PACKAGE_NAME} version`, {
1082
1111
  encoding: "utf-8",
1083
1112
  stdio: ["pipe", "pipe", "pipe"],
1084
1113
  timeout: 15000
@@ -1090,14 +1119,14 @@ function fetchLatestVersion() {
1090
1119
  }
1091
1120
  function installVersion(version) {
1092
1121
  try {
1093
- execSync2("npm cache clean --force", {
1122
+ execSync3("npm cache clean --force", {
1094
1123
  encoding: "utf-8",
1095
1124
  stdio: ["pipe", "pipe", "pipe"],
1096
1125
  timeout: 30000
1097
1126
  });
1098
1127
  } catch {}
1099
1128
  try {
1100
- execSync2(`npm install -g ${PACKAGE_NAME}@${version}`, {
1129
+ execSync3(`npm install -g ${PACKAGE_NAME}@${version}`, {
1101
1130
  encoding: "utf-8",
1102
1131
  stdio: ["pipe", "pipe", "pipe"],
1103
1132
  timeout: 120000
@@ -1109,7 +1138,7 @@ function installVersion(version) {
1109
1138
  }
1110
1139
  function verifyInstalled(expectedVersion) {
1111
1140
  try {
1112
- const installed = execSync2(`npm list -g ${PACKAGE_NAME} --depth=0 --json`, {
1141
+ const installed = execSync3(`npm list -g ${PACKAGE_NAME} --depth=0 --json`, {
1113
1142
  encoding: "utf-8",
1114
1143
  stdio: ["pipe", "pipe", "pipe"],
1115
1144
  timeout: 1e4
@@ -1473,7 +1502,7 @@ var init_ecosystem = __esm(() => {
1473
1502
  // src/core/github.ts
1474
1503
  import {
1475
1504
  execFileSync,
1476
- execSync as execSync3
1505
+ execSync as execSync4
1477
1506
  } from "node:child_process";
1478
1507
  function gh(args, options = {}) {
1479
1508
  const log = getLogger();
@@ -1482,7 +1511,7 @@ function gh(args, options = {}) {
1482
1511
  log.debug(`gh ${args}`, { cwd });
1483
1512
  const startTime = Date.now();
1484
1513
  try {
1485
- const result = execSync3(`gh ${args}`, {
1514
+ const result = execSync4(`gh ${args}`, {
1486
1515
  cwd,
1487
1516
  encoding: "utf-8",
1488
1517
  stdio: ["pipe", "pipe", "pipe"],
@@ -2231,7 +2260,7 @@ var exports_create = {};
2231
2260
  __export(exports_create, {
2232
2261
  createCommand: () => createCommand
2233
2262
  });
2234
- import { execSync as execSync4 } from "node:child_process";
2263
+ import { execSync as execSync5 } from "node:child_process";
2235
2264
  import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
2236
2265
  import { join as join7 } from "node:path";
2237
2266
  function validateName(name) {
@@ -2250,7 +2279,7 @@ function capitalize(str) {
2250
2279
  }
2251
2280
  function checkNpmExists(fullName) {
2252
2281
  try {
2253
- execSync4(`npm view ${fullName} version 2>/dev/null`, {
2282
+ execSync5(`npm view ${fullName} version 2>/dev/null`, {
2254
2283
  stdio: "pipe",
2255
2284
  timeout: 1e4
2256
2285
  });
@@ -2390,7 +2419,7 @@ function printHelp(): void {
2390
2419
  }
2391
2420
  `;
2392
2421
  }
2393
- function generateReadme(name, displayName, description) {
2422
+ function generateReadme(name, description) {
2394
2423
  return `# @locusai/locus-${name}
2395
2424
 
2396
2425
  ${description}
@@ -2545,7 +2574,7 @@ ${bold2("Creating package:")} ${cyan2(fullNpmName)}
2545
2574
  writeFileSync5(join7(packagesDir, "src", "index.ts"), generateIndexTs(name), "utf-8");
2546
2575
  process.stderr.write(`${green("✓")} Generated src/index.ts
2547
2576
  `);
2548
- writeFileSync5(join7(packagesDir, "README.md"), generateReadme(name, displayName, description), "utf-8");
2577
+ writeFileSync5(join7(packagesDir, "README.md"), generateReadme(name, description), "utf-8");
2549
2578
  process.stderr.write(`${green("✓")} Generated README.md
2550
2579
  `);
2551
2580
  process.stderr.write(`
@@ -3690,7 +3719,7 @@ var init_stream_renderer = __esm(() => {
3690
3719
  });
3691
3720
 
3692
3721
  // src/repl/clipboard.ts
3693
- import { execSync as execSync5 } from "node:child_process";
3722
+ import { execSync as execSync6 } from "node:child_process";
3694
3723
  import { existsSync as existsSync12, mkdirSync as mkdirSync8 } from "node:fs";
3695
3724
  import { tmpdir } from "node:os";
3696
3725
  import { join as join11 } from "node:path";
@@ -3729,7 +3758,7 @@ function readMacOSClipboardImage() {
3729
3758
  `return "ok"`
3730
3759
  ].join(`
3731
3760
  `);
3732
- const result = execSync5("osascript", {
3761
+ const result = execSync6("osascript", {
3733
3762
  input: script,
3734
3763
  encoding: "utf-8",
3735
3764
  timeout: 5000,
@@ -3743,13 +3772,13 @@ function readMacOSClipboardImage() {
3743
3772
  }
3744
3773
  function readLinuxClipboardImage() {
3745
3774
  try {
3746
- const targets = execSync5("xclip -selection clipboard -t TARGETS -o 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
3775
+ const targets = execSync6("xclip -selection clipboard -t TARGETS -o 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
3747
3776
  if (!targets.includes("image/png")) {
3748
3777
  return null;
3749
3778
  }
3750
3779
  ensureStableDir();
3751
3780
  const destPath = join11(STABLE_DIR, `clipboard-${Date.now()}.png`);
3752
- execSync5(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
3781
+ execSync6(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
3753
3782
  if (existsSync12(destPath)) {
3754
3783
  return destPath;
3755
3784
  }
@@ -4770,7 +4799,7 @@ __export(exports_claude, {
4770
4799
  buildClaudeArgs: () => buildClaudeArgs,
4771
4800
  ClaudeRunner: () => ClaudeRunner
4772
4801
  });
4773
- import { execSync as execSync6, spawn as spawn2 } from "node:child_process";
4802
+ import { execSync as execSync7, spawn as spawn2 } from "node:child_process";
4774
4803
  function buildClaudeArgs(options) {
4775
4804
  const args = ["--dangerously-skip-permissions", "--no-session-persistence"];
4776
4805
  if (options.model) {
@@ -4788,7 +4817,7 @@ class ClaudeRunner {
4788
4817
  aborted = false;
4789
4818
  async isAvailable() {
4790
4819
  try {
4791
- execSync6("claude --version", {
4820
+ execSync7("claude --version", {
4792
4821
  encoding: "utf-8",
4793
4822
  stdio: ["pipe", "pipe", "pipe"]
4794
4823
  });
@@ -4799,7 +4828,7 @@ class ClaudeRunner {
4799
4828
  }
4800
4829
  async getVersion() {
4801
4830
  try {
4802
- const output = execSync6("claude --version", {
4831
+ const output = execSync7("claude --version", {
4803
4832
  encoding: "utf-8",
4804
4833
  stdio: ["pipe", "pipe", "pipe"]
4805
4834
  }).trim();
@@ -5141,13 +5170,13 @@ function backupIgnoredFiles(projectRoot) {
5141
5170
  }
5142
5171
  };
5143
5172
  }
5144
- async function enforceSandboxIgnore(sandboxName, projectRoot) {
5173
+ async function enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir) {
5145
5174
  const log = getLogger();
5146
5175
  const ignorePath = join13(projectRoot, ".sandboxignore");
5147
5176
  const rules = parseIgnoreFile(ignorePath);
5148
5177
  if (rules.length === 0)
5149
5178
  return;
5150
- const script = buildCleanupScript(rules, projectRoot);
5179
+ const script = buildCleanupScript(rules, containerWorkdir ?? projectRoot);
5151
5180
  if (!script)
5152
5181
  return;
5153
5182
  log.debug("Enforcing .sandboxignore", {
@@ -5187,11 +5216,13 @@ import { spawn as spawn3 } from "node:child_process";
5187
5216
 
5188
5217
  class SandboxedClaudeRunner {
5189
5218
  sandboxName;
5219
+ containerWorkdir;
5190
5220
  name = "claude-sandboxed";
5191
5221
  process = null;
5192
5222
  aborted = false;
5193
- constructor(sandboxName) {
5223
+ constructor(sandboxName, containerWorkdir) {
5194
5224
  this.sandboxName = sandboxName;
5225
+ this.containerWorkdir = containerWorkdir;
5195
5226
  }
5196
5227
  async isAvailable() {
5197
5228
  const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
@@ -5217,13 +5248,14 @@ class SandboxedClaudeRunner {
5217
5248
  const claudeArgs = ["-p", options.prompt, ...buildClaudeArgs(options)];
5218
5249
  options.onStatusChange?.("Syncing sandbox...");
5219
5250
  const backup = backupIgnoredFiles(options.cwd);
5220
- await enforceSandboxIgnore(this.sandboxName, options.cwd);
5251
+ await enforceSandboxIgnore(this.sandboxName, options.cwd, this.containerWorkdir);
5221
5252
  options.onStatusChange?.("Thinking...");
5253
+ const workdir = this.containerWorkdir ?? options.cwd;
5222
5254
  const dockerArgs = [
5223
5255
  "sandbox",
5224
5256
  "exec",
5225
5257
  "-w",
5226
- options.cwd,
5258
+ workdir,
5227
5259
  this.sandboxName,
5228
5260
  "claude",
5229
5261
  ...claudeArgs
@@ -5388,7 +5420,7 @@ var init_claude_sandbox = __esm(() => {
5388
5420
  });
5389
5421
 
5390
5422
  // src/ai/codex.ts
5391
- import { execSync as execSync7, spawn as spawn4 } from "node:child_process";
5423
+ import { execSync as execSync8, spawn as spawn4 } from "node:child_process";
5392
5424
  function buildCodexArgs(model) {
5393
5425
  const args = ["exec", "--full-auto", "--skip-git-repo-check", "--json"];
5394
5426
  if (model) {
@@ -5404,7 +5436,7 @@ class CodexRunner {
5404
5436
  aborted = false;
5405
5437
  async isAvailable() {
5406
5438
  try {
5407
- execSync7("codex --version", {
5439
+ execSync8("codex --version", {
5408
5440
  encoding: "utf-8",
5409
5441
  stdio: ["pipe", "pipe", "pipe"]
5410
5442
  });
@@ -5415,7 +5447,7 @@ class CodexRunner {
5415
5447
  }
5416
5448
  async getVersion() {
5417
5449
  try {
5418
- const output = execSync7("codex --version", {
5450
+ const output = execSync8("codex --version", {
5419
5451
  encoding: "utf-8",
5420
5452
  stdio: ["pipe", "pipe", "pipe"]
5421
5453
  }).trim();
@@ -5567,12 +5599,14 @@ import { spawn as spawn5 } from "node:child_process";
5567
5599
 
5568
5600
  class SandboxedCodexRunner {
5569
5601
  sandboxName;
5602
+ containerWorkdir;
5570
5603
  name = "codex-sandboxed";
5571
5604
  process = null;
5572
5605
  aborted = false;
5573
5606
  codexInstalled = false;
5574
- constructor(sandboxName) {
5607
+ constructor(sandboxName, containerWorkdir) {
5575
5608
  this.sandboxName = sandboxName;
5609
+ this.containerWorkdir = containerWorkdir;
5576
5610
  }
5577
5611
  async isAvailable() {
5578
5612
  const delegate = new CodexRunner;
@@ -5596,19 +5630,20 @@ class SandboxedCodexRunner {
5596
5630
  const codexArgs = buildCodexArgs(options.model);
5597
5631
  options.onStatusChange?.("Syncing sandbox...");
5598
5632
  const backup = backupIgnoredFiles(options.cwd);
5599
- await enforceSandboxIgnore(this.sandboxName, options.cwd);
5633
+ await enforceSandboxIgnore(this.sandboxName, options.cwd, this.containerWorkdir);
5600
5634
  if (!this.codexInstalled) {
5601
5635
  options.onStatusChange?.("Checking codex...");
5602
5636
  await this.ensureCodexInstalled(this.sandboxName);
5603
5637
  this.codexInstalled = true;
5604
5638
  }
5605
5639
  options.onStatusChange?.("Thinking...");
5640
+ const workdir = this.containerWorkdir ?? options.cwd;
5606
5641
  const dockerArgs = [
5607
5642
  "sandbox",
5608
5643
  "exec",
5609
5644
  "-i",
5610
5645
  "-w",
5611
- options.cwd,
5646
+ workdir,
5612
5647
  this.sandboxName,
5613
5648
  "codex",
5614
5649
  ...codexArgs
@@ -5789,12 +5824,12 @@ async function createRunnerAsync(provider, sandboxed) {
5789
5824
  throw new Error(`Unknown AI provider: ${provider}`);
5790
5825
  }
5791
5826
  }
5792
- function createUserManagedSandboxRunner(provider, sandboxName) {
5827
+ function createUserManagedSandboxRunner(provider, sandboxName, containerWorkdir) {
5793
5828
  switch (provider) {
5794
5829
  case "claude":
5795
- return new SandboxedClaudeRunner(sandboxName);
5830
+ return new SandboxedClaudeRunner(sandboxName, containerWorkdir);
5796
5831
  case "codex":
5797
- return new SandboxedCodexRunner(sandboxName);
5832
+ return new SandboxedCodexRunner(sandboxName, containerWorkdir);
5798
5833
  default:
5799
5834
  throw new Error(`Unknown AI provider: ${provider}`);
5800
5835
  }
@@ -5905,7 +5940,7 @@ ${red2("✗")} ${dim2("Force exit.")}\r
5905
5940
  exitCode: 1
5906
5941
  };
5907
5942
  }
5908
- runner = createUserManagedSandboxRunner(resolvedProvider, options.sandboxName);
5943
+ runner = createUserManagedSandboxRunner(resolvedProvider, options.sandboxName, options.containerWorkdir);
5909
5944
  } else {
5910
5945
  runner = await createRunnerAsync(resolvedProvider, false);
5911
5946
  }
@@ -6240,7 +6275,8 @@ async function issueCreate(projectRoot, parsed) {
6240
6275
  silent: true,
6241
6276
  activity: "generating issue",
6242
6277
  sandboxed: config.sandbox.enabled,
6243
- sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider)
6278
+ sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider),
6279
+ containerWorkdir: config.sandbox.containerWorkdir
6244
6280
  });
6245
6281
  if (!aiResult.success && !aiResult.interrupted) {
6246
6282
  process.stderr.write(`${red2("✗")} Failed to generate issue: ${aiResult.error}
@@ -7443,7 +7479,7 @@ var init_sprint = __esm(() => {
7443
7479
  });
7444
7480
 
7445
7481
  // src/core/prompt-builder.ts
7446
- import { execSync as execSync8 } from "node:child_process";
7482
+ import { execSync as execSync9 } from "node:child_process";
7447
7483
  import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
7448
7484
  import { join as join14 } from "node:path";
7449
7485
  function buildExecutionPrompt(ctx) {
@@ -7592,7 +7628,7 @@ ${parts.join(`
7592
7628
  function buildRepoContext(projectRoot) {
7593
7629
  const parts = [];
7594
7630
  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();
7631
+ 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
7632
  if (tree) {
7597
7633
  parts.push(`<file-tree>
7598
7634
  \`\`\`
@@ -7602,7 +7638,7 @@ ${tree}
7602
7638
  }
7603
7639
  } catch {}
7604
7640
  try {
7605
- const gitLog = execSync8("git log --oneline -10", {
7641
+ const gitLog = execSync9("git log --oneline -10", {
7606
7642
  cwd: projectRoot,
7607
7643
  encoding: "utf-8",
7608
7644
  stdio: ["pipe", "pipe", "pipe"]
@@ -7616,7 +7652,7 @@ ${gitLog}
7616
7652
  }
7617
7653
  } catch {}
7618
7654
  try {
7619
- const branch = execSync8("git rev-parse --abbrev-ref HEAD", {
7655
+ const branch = execSync9("git rev-parse --abbrev-ref HEAD", {
7620
7656
  cwd: projectRoot,
7621
7657
  encoding: "utf-8",
7622
7658
  stdio: ["pipe", "pipe", "pipe"]
@@ -7872,7 +7908,7 @@ var init_diff_renderer = __esm(() => {
7872
7908
  });
7873
7909
 
7874
7910
  // src/repl/commands.ts
7875
- import { execSync as execSync9 } from "node:child_process";
7911
+ import { execSync as execSync10 } from "node:child_process";
7876
7912
  function getSlashCommands() {
7877
7913
  return [
7878
7914
  {
@@ -8070,7 +8106,7 @@ function cmdModel(args, ctx) {
8070
8106
  }
8071
8107
  function cmdDiff(_args, ctx) {
8072
8108
  try {
8073
- const diff = execSync9("git diff", {
8109
+ const diff = execSync10("git diff", {
8074
8110
  cwd: ctx.projectRoot,
8075
8111
  encoding: "utf-8",
8076
8112
  stdio: ["pipe", "pipe", "pipe"]
@@ -8106,7 +8142,7 @@ function cmdDiff(_args, ctx) {
8106
8142
  }
8107
8143
  function cmdUndo(_args, ctx) {
8108
8144
  try {
8109
- const status = execSync9("git status --porcelain", {
8145
+ const status = execSync10("git status --porcelain", {
8110
8146
  cwd: ctx.projectRoot,
8111
8147
  encoding: "utf-8",
8112
8148
  stdio: ["pipe", "pipe", "pipe"]
@@ -8116,7 +8152,7 @@ function cmdUndo(_args, ctx) {
8116
8152
  `);
8117
8153
  return;
8118
8154
  }
8119
- execSync9("git checkout .", {
8155
+ execSync10("git checkout .", {
8120
8156
  cwd: ctx.projectRoot,
8121
8157
  encoding: "utf-8",
8122
8158
  stdio: ["pipe", "pipe", "pipe"]
@@ -8494,7 +8530,7 @@ var init_session_manager = __esm(() => {
8494
8530
  });
8495
8531
 
8496
8532
  // src/repl/voice.ts
8497
- import { execSync as execSync10, spawn as spawn6 } from "node:child_process";
8533
+ import { execSync as execSync11, spawn as spawn6 } from "node:child_process";
8498
8534
  import { existsSync as existsSync18, mkdirSync as mkdirSync13, unlinkSync as unlinkSync4 } from "node:fs";
8499
8535
  import { cpus, homedir as homedir4, platform, tmpdir as tmpdir4 } from "node:os";
8500
8536
  import { join as join18 } from "node:path";
@@ -8504,7 +8540,7 @@ function getWhisperModelPath() {
8504
8540
  function commandExists(cmd) {
8505
8541
  try {
8506
8542
  const which = platform() === "win32" ? "where" : "which";
8507
- execSync10(`${which} ${cmd}`, { stdio: "pipe" });
8543
+ execSync11(`${which} ${cmd}`, { stdio: "pipe" });
8508
8544
  return true;
8509
8545
  } catch {
8510
8546
  return false;
@@ -8630,22 +8666,22 @@ function installSox(pm) {
8630
8666
  try {
8631
8667
  switch (pm) {
8632
8668
  case "brew":
8633
- execSync10("brew install sox", { stdio: "inherit", timeout: 300000 });
8669
+ execSync11("brew install sox", { stdio: "inherit", timeout: 300000 });
8634
8670
  break;
8635
8671
  case "apt":
8636
- execSync10("sudo apt-get install -y sox", {
8672
+ execSync11("sudo apt-get install -y sox", {
8637
8673
  stdio: "inherit",
8638
8674
  timeout: 300000
8639
8675
  });
8640
8676
  break;
8641
8677
  case "dnf":
8642
- execSync10("sudo dnf install -y sox", {
8678
+ execSync11("sudo dnf install -y sox", {
8643
8679
  stdio: "inherit",
8644
8680
  timeout: 300000
8645
8681
  });
8646
8682
  break;
8647
8683
  case "pacman":
8648
- execSync10("sudo pacman -S --noconfirm sox", {
8684
+ execSync11("sudo pacman -S --noconfirm sox", {
8649
8685
  stdio: "inherit",
8650
8686
  timeout: 300000
8651
8687
  });
@@ -8659,7 +8695,7 @@ function installSox(pm) {
8659
8695
  function installWhisperCpp(pm) {
8660
8696
  if (pm === "brew") {
8661
8697
  try {
8662
- execSync10("brew install whisper-cpp", {
8698
+ execSync11("brew install whisper-cpp", {
8663
8699
  stdio: "inherit",
8664
8700
  timeout: 300000
8665
8701
  });
@@ -8681,19 +8717,19 @@ function ensureBuildDeps(pm) {
8681
8717
  try {
8682
8718
  switch (pm) {
8683
8719
  case "apt":
8684
- execSync10("sudo apt-get install -y cmake g++ make git", {
8720
+ execSync11("sudo apt-get install -y cmake g++ make git", {
8685
8721
  stdio: "inherit",
8686
8722
  timeout: 300000
8687
8723
  });
8688
8724
  break;
8689
8725
  case "dnf":
8690
- execSync10("sudo dnf install -y cmake gcc-c++ make git", {
8726
+ execSync11("sudo dnf install -y cmake gcc-c++ make git", {
8691
8727
  stdio: "inherit",
8692
8728
  timeout: 300000
8693
8729
  });
8694
8730
  break;
8695
8731
  case "pacman":
8696
- execSync10("sudo pacman -S --noconfirm cmake gcc make git", {
8732
+ execSync11("sudo pacman -S --noconfirm cmake gcc make git", {
8697
8733
  stdio: "inherit",
8698
8734
  timeout: 300000
8699
8735
  });
@@ -8719,17 +8755,17 @@ function buildWhisperFromSource(pm) {
8719
8755
  mkdirSync13(LOCUS_BIN_DIR, { recursive: true });
8720
8756
  out.write(` ${dim2("Cloning whisper.cpp...")}
8721
8757
  `);
8722
- execSync10(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join18(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
8758
+ execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join18(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
8723
8759
  const srcDir = join18(buildDir, "whisper.cpp");
8724
8760
  const numCpus = cpus().length || 2;
8725
8761
  out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
8726
8762
  `);
8727
- execSync10("cmake -B build -DCMAKE_BUILD_TYPE=Release", {
8763
+ execSync11("cmake -B build -DCMAKE_BUILD_TYPE=Release", {
8728
8764
  cwd: srcDir,
8729
8765
  stdio: ["pipe", "pipe", "pipe"],
8730
8766
  timeout: 120000
8731
8767
  });
8732
- execSync10(`cmake --build build --config Release -j${numCpus}`, {
8768
+ execSync11(`cmake --build build --config Release -j${numCpus}`, {
8733
8769
  cwd: srcDir,
8734
8770
  stdio: ["pipe", "pipe", "pipe"],
8735
8771
  timeout: 600000
@@ -8741,7 +8777,7 @@ function buildWhisperFromSource(pm) {
8741
8777
  ];
8742
8778
  for (const candidate of binaryCandidates) {
8743
8779
  if (existsSync18(candidate)) {
8744
- execSync10(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
8780
+ execSync11(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
8745
8781
  stdio: "pipe"
8746
8782
  });
8747
8783
  return true;
@@ -8756,7 +8792,7 @@ function buildWhisperFromSource(pm) {
8756
8792
  return false;
8757
8793
  } finally {
8758
8794
  try {
8759
- execSync10(`rm -rf "${buildDir}"`, { stdio: "pipe" });
8795
+ execSync11(`rm -rf "${buildDir}"`, { stdio: "pipe" });
8760
8796
  } catch {}
8761
8797
  }
8762
8798
  }
@@ -8827,12 +8863,12 @@ function downloadModel() {
8827
8863
  `);
8828
8864
  try {
8829
8865
  if (commandExists("curl")) {
8830
- execSync10(`curl -L -o "${modelPath}" "${url}"`, {
8866
+ execSync11(`curl -L -o "${modelPath}" "${url}"`, {
8831
8867
  stdio: ["pipe", "pipe", "pipe"],
8832
8868
  timeout: 300000
8833
8869
  });
8834
8870
  } else if (commandExists("wget")) {
8835
- execSync10(`wget -O "${modelPath}" "${url}"`, {
8871
+ execSync11(`wget -O "${modelPath}" "${url}"`, {
8836
8872
  stdio: ["pipe", "pipe", "pipe"],
8837
8873
  timeout: 300000
8838
8874
  });
@@ -9030,7 +9066,7 @@ var init_voice = __esm(() => {
9030
9066
  });
9031
9067
 
9032
9068
  // src/repl/repl.ts
9033
- import { execSync as execSync11 } from "node:child_process";
9069
+ import { execSync as execSync12 } from "node:child_process";
9034
9070
  async function startRepl(options) {
9035
9071
  const { projectRoot, config } = options;
9036
9072
  const sessionManager = new SessionManager(projectRoot);
@@ -9048,7 +9084,7 @@ async function startRepl(options) {
9048
9084
  } else {
9049
9085
  let branch = "main";
9050
9086
  try {
9051
- branch = execSync11("git rev-parse --abbrev-ref HEAD", {
9087
+ branch = execSync12("git rev-parse --abbrev-ref HEAD", {
9052
9088
  cwd: projectRoot,
9053
9089
  encoding: "utf-8",
9054
9090
  stdio: ["pipe", "pipe", "pipe"]
@@ -9100,7 +9136,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
9100
9136
  const provider = inferProviderFromModel(config.ai.model) || config.ai.provider;
9101
9137
  const sandboxName = getProviderSandboxName(config.sandbox, provider);
9102
9138
  if (sandboxName) {
9103
- sandboxRunner = createUserManagedSandboxRunner(provider, sandboxName);
9139
+ sandboxRunner = createUserManagedSandboxRunner(provider, sandboxName, config.sandbox.containerWorkdir);
9104
9140
  process.stderr.write(`${dim2("Using")} ${dim2(provider)} ${dim2("sandbox")} ${dim2(sandboxName)}
9105
9141
  `);
9106
9142
  } else {
@@ -9151,7 +9187,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
9151
9187
  if (providerChanged && config.sandbox.enabled) {
9152
9188
  const sandboxName = getProviderSandboxName(config.sandbox, inferredProvider);
9153
9189
  if (sandboxName) {
9154
- sandboxRunner = createUserManagedSandboxRunner(inferredProvider, sandboxName);
9190
+ sandboxRunner = createUserManagedSandboxRunner(inferredProvider, sandboxName, config.sandbox.containerWorkdir);
9155
9191
  process.stderr.write(`${dim2("Switched sandbox agent to")} ${dim2(inferredProvider)} ${dim2(`(${sandboxName})`)}
9156
9192
  `);
9157
9193
  } else {
@@ -9279,6 +9315,7 @@ async function executeAITurn(prompt, session, options, verbose = false, runner)
9279
9315
  verbose,
9280
9316
  sandboxed: config.sandbox.enabled,
9281
9317
  sandboxName,
9318
+ containerWorkdir: config.sandbox.containerWorkdir,
9282
9319
  runner
9283
9320
  });
9284
9321
  if (aiResult.interrupted) {
@@ -9483,7 +9520,7 @@ async function handleJsonStream(projectRoot, config, args, sessionId) {
9483
9520
  try {
9484
9521
  const fullPrompt = buildReplPrompt(prompt, projectRoot, config);
9485
9522
  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);
9523
+ const runner = config.sandbox.enabled ? sandboxName ? createUserManagedSandboxRunner(config.ai.provider, sandboxName, config.sandbox.containerWorkdir) : null : await createRunnerAsync(config.ai.provider, false);
9487
9524
  if (!runner) {
9488
9525
  const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
9489
9526
  stream.emitError(mismatch ?? `No sandbox configured for "${config.ai.provider}". Run "locus sandbox" to create one.`, false);
@@ -9536,11 +9573,11 @@ var init_exec = __esm(() => {
9536
9573
  });
9537
9574
 
9538
9575
  // src/core/submodule.ts
9539
- import { execSync as execSync12 } from "node:child_process";
9576
+ import { execSync as execSync13 } from "node:child_process";
9540
9577
  import { existsSync as existsSync19 } from "node:fs";
9541
9578
  import { join as join19 } from "node:path";
9542
9579
  function git2(args, cwd) {
9543
- return execSync12(`git ${args}`, {
9580
+ return execSync13(`git ${args}`, {
9544
9581
  cwd,
9545
9582
  encoding: "utf-8",
9546
9583
  stdio: ["pipe", "pipe", "pipe"]
@@ -9608,7 +9645,7 @@ function commitDirtySubmodules(cwd, issueNumber, issueTitle) {
9608
9645
  const message = `chore: complete #${issueNumber} - ${issueTitle}
9609
9646
 
9610
9647
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
9611
- execSync12("git commit -F -", {
9648
+ execSync13("git commit -F -", {
9612
9649
  input: message,
9613
9650
  cwd: sub.absolutePath,
9614
9651
  encoding: "utf-8",
@@ -9695,7 +9732,7 @@ var init_submodule = __esm(() => {
9695
9732
  });
9696
9733
 
9697
9734
  // src/core/agent.ts
9698
- import { execSync as execSync13 } from "node:child_process";
9735
+ import { execSync as execSync14 } from "node:child_process";
9699
9736
  async function executeIssue(projectRoot, options) {
9700
9737
  const log = getLogger();
9701
9738
  const timer = createTimer();
@@ -9724,7 +9761,7 @@ ${cyan2("●")} ${bold2(`#${issueNumber}`)} ${issue.title}
9724
9761
  }
9725
9762
  let issueComments = [];
9726
9763
  try {
9727
- const commentsRaw = execSync13(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
9764
+ const commentsRaw = execSync14(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
9728
9765
  if (commentsRaw) {
9729
9766
  issueComments = commentsRaw.split(`
9730
9767
  `).filter(Boolean);
@@ -9761,7 +9798,8 @@ ${yellow2("⚠")} ${bold2("Dry run")} — would execute with:
9761
9798
  cwd: options.worktreePath ?? projectRoot,
9762
9799
  activity: `issue #${issueNumber}`,
9763
9800
  sandboxed: options.sandboxed,
9764
- sandboxName: options.sandboxName
9801
+ sandboxName: options.sandboxName,
9802
+ containerWorkdir: options.containerWorkdir
9765
9803
  });
9766
9804
  const output = aiResult.output;
9767
9805
  if (aiResult.interrupted) {
@@ -9867,7 +9905,8 @@ ${c.body}`),
9867
9905
  cwd: projectRoot,
9868
9906
  activity: `iterating on PR #${prNumber}`,
9869
9907
  sandboxed: config.sandbox.enabled,
9870
- sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider)
9908
+ sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider),
9909
+ containerWorkdir: config.sandbox.containerWorkdir
9871
9910
  });
9872
9911
  if (aiResult.interrupted) {
9873
9912
  process.stderr.write(`
@@ -9888,12 +9927,12 @@ ${aiResult.success ? green("✓") : red2("✗")} Iteration ${aiResult.success ?
9888
9927
  }
9889
9928
  async function createIssuePR(projectRoot, config, issue) {
9890
9929
  try {
9891
- const currentBranch = execSync13("git rev-parse --abbrev-ref HEAD", {
9930
+ const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
9892
9931
  cwd: projectRoot,
9893
9932
  encoding: "utf-8",
9894
9933
  stdio: ["pipe", "pipe", "pipe"]
9895
9934
  }).trim();
9896
- const diff = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9935
+ const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9897
9936
  cwd: projectRoot,
9898
9937
  encoding: "utf-8",
9899
9938
  stdio: ["pipe", "pipe", "pipe"]
@@ -9903,7 +9942,7 @@ async function createIssuePR(projectRoot, config, issue) {
9903
9942
  return;
9904
9943
  }
9905
9944
  pushSubmoduleBranches(projectRoot);
9906
- execSync13(`git push -u origin ${currentBranch}`, {
9945
+ execSync14(`git push -u origin ${currentBranch}`, {
9907
9946
  cwd: projectRoot,
9908
9947
  encoding: "utf-8",
9909
9948
  stdio: ["pipe", "pipe", "pipe"]
@@ -9958,9 +9997,9 @@ var init_agent = __esm(() => {
9958
9997
  });
9959
9998
 
9960
9999
  // src/core/conflict.ts
9961
- import { execSync as execSync14 } from "node:child_process";
10000
+ import { execSync as execSync15 } from "node:child_process";
9962
10001
  function git3(args, cwd) {
9963
- return execSync14(`git ${args}`, {
10002
+ return execSync15(`git ${args}`, {
9964
10003
  cwd,
9965
10004
  encoding: "utf-8",
9966
10005
  stdio: ["pipe", "pipe", "pipe"]
@@ -10261,11 +10300,11 @@ var init_shutdown = __esm(() => {
10261
10300
  });
10262
10301
 
10263
10302
  // src/core/worktree.ts
10264
- import { execSync as execSync15 } from "node:child_process";
10303
+ import { execSync as execSync16 } from "node:child_process";
10265
10304
  import { existsSync as existsSync21, readdirSync as readdirSync7, realpathSync, statSync as statSync4 } from "node:fs";
10266
10305
  import { join as join21 } from "node:path";
10267
10306
  function git4(args, cwd) {
10268
- return execSync15(`git ${args}`, {
10307
+ return execSync16(`git ${args}`, {
10269
10308
  cwd,
10270
10309
  encoding: "utf-8",
10271
10310
  stdio: ["pipe", "pipe", "pipe"]
@@ -10290,7 +10329,7 @@ function generateBranchName(issueNumber) {
10290
10329
  }
10291
10330
  function getWorktreeBranch(worktreePath) {
10292
10331
  try {
10293
- return execSync15("git branch --show-current", {
10332
+ return execSync16("git branch --show-current", {
10294
10333
  cwd: worktreePath,
10295
10334
  encoding: "utf-8",
10296
10335
  stdio: ["pipe", "pipe", "pipe"]
@@ -10417,12 +10456,12 @@ var exports_run = {};
10417
10456
  __export(exports_run, {
10418
10457
  runCommand: () => runCommand
10419
10458
  });
10420
- import { execSync as execSync16 } from "node:child_process";
10459
+ import { execSync as execSync17 } from "node:child_process";
10421
10460
  function resolveExecutionContext(config, modelOverride) {
10422
10461
  const model = modelOverride ?? config.ai.model;
10423
10462
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
10424
10463
  const sandboxName = getModelSandboxName(config.sandbox, model, provider);
10425
- return { provider, model, sandboxName };
10464
+ return { provider, model, sandboxName, containerWorkdir: config.sandbox.containerWorkdir };
10426
10465
  }
10427
10466
  function printRunHelp() {
10428
10467
  process.stderr.write(`
@@ -10577,7 +10616,7 @@ ${yellow2("⚠")} A sprint run is already in progress.
10577
10616
  }
10578
10617
  if (!flags.dryRun) {
10579
10618
  try {
10580
- execSync16(`git checkout -B ${branchName}`, {
10619
+ execSync17(`git checkout -B ${branchName}`, {
10581
10620
  cwd: projectRoot,
10582
10621
  encoding: "utf-8",
10583
10622
  stdio: ["pipe", "pipe", "pipe"]
@@ -10627,7 +10666,7 @@ ${red2("✗")} Auto-rebase failed. Resolve manually.
10627
10666
  let sprintContext;
10628
10667
  if (i > 0 && !flags.dryRun) {
10629
10668
  try {
10630
- sprintContext = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD`, {
10669
+ sprintContext = execSync17(`git diff origin/${config.agent.baseBranch}..HEAD`, {
10631
10670
  cwd: projectRoot,
10632
10671
  encoding: "utf-8",
10633
10672
  stdio: ["pipe", "pipe", "pipe"]
@@ -10648,7 +10687,8 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
10648
10687
  sprintContext,
10649
10688
  skipPR: true,
10650
10689
  sandboxed,
10651
- sandboxName: execution.sandboxName
10690
+ sandboxName: execution.sandboxName,
10691
+ containerWorkdir: execution.containerWorkdir
10652
10692
  });
10653
10693
  if (result.success) {
10654
10694
  if (!flags.dryRun) {
@@ -10692,7 +10732,7 @@ ${bold2("Summary:")}
10692
10732
  const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
10693
10733
  if (prNumber !== undefined) {
10694
10734
  try {
10695
- execSync16(`git checkout ${config.agent.baseBranch}`, {
10735
+ execSync17(`git checkout ${config.agent.baseBranch}`, {
10696
10736
  cwd: projectRoot,
10697
10737
  encoding: "utf-8",
10698
10738
  stdio: ["pipe", "pipe", "pipe"]
@@ -10725,7 +10765,8 @@ ${bold2("Running sprint issue")} ${cyan2(`#${issueNumber}`)} ${dim2("(sequential
10725
10765
  model: execution.model,
10726
10766
  dryRun: flags.dryRun,
10727
10767
  sandboxed,
10728
- sandboxName: execution.sandboxName
10768
+ sandboxName: execution.sandboxName,
10769
+ containerWorkdir: execution.containerWorkdir
10729
10770
  });
10730
10771
  return;
10731
10772
  }
@@ -10737,7 +10778,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
10737
10778
  `);
10738
10779
  if (!flags.dryRun) {
10739
10780
  try {
10740
- execSync16(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
10781
+ execSync17(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
10741
10782
  cwd: projectRoot,
10742
10783
  encoding: "utf-8",
10743
10784
  stdio: ["pipe", "pipe", "pipe"]
@@ -10762,7 +10803,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
10762
10803
  if (!flags.dryRun) {
10763
10804
  if (result.success) {
10764
10805
  try {
10765
- execSync16(`git checkout ${config.agent.baseBranch}`, {
10806
+ execSync17(`git checkout ${config.agent.baseBranch}`, {
10766
10807
  cwd: projectRoot,
10767
10808
  encoding: "utf-8",
10768
10809
  stdio: ["pipe", "pipe", "pipe"]
@@ -10833,7 +10874,8 @@ ${bold2("Running")} ${cyan2(`${issueNumbers.length} issues`)} ${dim2(`(max ${max
10833
10874
  model: execution.model,
10834
10875
  dryRun: flags.dryRun,
10835
10876
  sandboxed,
10836
- sandboxName: execution.sandboxName
10877
+ sandboxName: execution.sandboxName,
10878
+ containerWorkdir: execution.containerWorkdir
10837
10879
  });
10838
10880
  if (result.success) {
10839
10881
  markTaskDone(state, issueNumber, result.prNumber);
@@ -10899,13 +10941,13 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
10899
10941
  `);
10900
10942
  if (state.type === "sprint" && state.branch) {
10901
10943
  try {
10902
- const currentBranch = execSync16("git rev-parse --abbrev-ref HEAD", {
10944
+ const currentBranch = execSync17("git rev-parse --abbrev-ref HEAD", {
10903
10945
  cwd: projectRoot,
10904
10946
  encoding: "utf-8",
10905
10947
  stdio: ["pipe", "pipe", "pipe"]
10906
10948
  }).trim();
10907
10949
  if (currentBranch !== state.branch) {
10908
- execSync16(`git checkout ${state.branch}`, {
10950
+ execSync17(`git checkout ${state.branch}`, {
10909
10951
  cwd: projectRoot,
10910
10952
  encoding: "utf-8",
10911
10953
  stdio: ["pipe", "pipe", "pipe"]
@@ -10933,7 +10975,8 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
10933
10975
  model: execution.model,
10934
10976
  skipPR: isSprintRun,
10935
10977
  sandboxed,
10936
- sandboxName: execution.sandboxName
10978
+ sandboxName: execution.sandboxName,
10979
+ containerWorkdir: execution.containerWorkdir
10937
10980
  });
10938
10981
  if (result.success) {
10939
10982
  if (isSprintRun) {
@@ -10972,7 +11015,7 @@ ${bold2("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fai
10972
11015
  const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
10973
11016
  if (prNumber !== undefined) {
10974
11017
  try {
10975
- execSync16(`git checkout ${config.agent.baseBranch}`, {
11018
+ execSync17(`git checkout ${config.agent.baseBranch}`, {
10976
11019
  cwd: projectRoot,
10977
11020
  encoding: "utf-8",
10978
11021
  stdio: ["pipe", "pipe", "pipe"]
@@ -11008,14 +11051,14 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
11008
11051
  process.stderr.write(` ${dim2(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
11009
11052
  `);
11010
11053
  }
11011
- const status = execSync16("git status --porcelain", {
11054
+ const status = execSync17("git status --porcelain", {
11012
11055
  cwd: projectRoot,
11013
11056
  encoding: "utf-8",
11014
11057
  stdio: ["pipe", "pipe", "pipe"]
11015
11058
  }).trim();
11016
11059
  if (!status)
11017
11060
  return;
11018
- execSync16("git add -A", {
11061
+ execSync17("git add -A", {
11019
11062
  cwd: projectRoot,
11020
11063
  encoding: "utf-8",
11021
11064
  stdio: ["pipe", "pipe", "pipe"]
@@ -11023,7 +11066,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
11023
11066
  const message = `chore: complete #${issueNumber} - ${issueTitle}
11024
11067
 
11025
11068
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
11026
- execSync16(`git commit -F -`, {
11069
+ execSync17(`git commit -F -`, {
11027
11070
  input: message,
11028
11071
  cwd: projectRoot,
11029
11072
  encoding: "utf-8",
@@ -11037,7 +11080,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
11037
11080
  if (!config.agent.autoPR)
11038
11081
  return;
11039
11082
  try {
11040
- const diff = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
11083
+ const diff = execSync17(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
11041
11084
  cwd: projectRoot,
11042
11085
  encoding: "utf-8",
11043
11086
  stdio: ["pipe", "pipe", "pipe"]
@@ -11048,7 +11091,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
11048
11091
  return;
11049
11092
  }
11050
11093
  pushSubmoduleBranches(projectRoot);
11051
- execSync16(`git push -u origin ${branchName}`, {
11094
+ execSync17(`git push -u origin ${branchName}`, {
11052
11095
  cwd: projectRoot,
11053
11096
  encoding: "utf-8",
11054
11097
  stdio: ["pipe", "pipe", "pipe"]
@@ -11467,7 +11510,8 @@ ${bold2("Planning:")} ${cyan2(displayDirective)}
11467
11510
  cwd: projectRoot,
11468
11511
  activity: "planning",
11469
11512
  sandboxed: config.sandbox.enabled,
11470
- sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
11513
+ sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
11514
+ containerWorkdir: config.sandbox.containerWorkdir
11471
11515
  });
11472
11516
  if (aiResult.interrupted) {
11473
11517
  process.stderr.write(`
@@ -11590,7 +11634,8 @@ Start with foundational/setup tasks, then core features, then integration/testin
11590
11634
  activity: "issue ordering",
11591
11635
  silent: true,
11592
11636
  sandboxed: config.sandbox.enabled,
11593
- sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
11637
+ sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
11638
+ containerWorkdir: config.sandbox.containerWorkdir
11594
11639
  });
11595
11640
  if (aiResult.interrupted) {
11596
11641
  process.stderr.write(`
@@ -11850,7 +11895,7 @@ var exports_review = {};
11850
11895
  __export(exports_review, {
11851
11896
  reviewCommand: () => reviewCommand
11852
11897
  });
11853
- import { execFileSync as execFileSync2, execSync as execSync17 } from "node:child_process";
11898
+ import { execFileSync as execFileSync2, execSync as execSync18 } from "node:child_process";
11854
11899
  import { existsSync as existsSync23, readFileSync as readFileSync14 } from "node:fs";
11855
11900
  import { join as join23 } from "node:path";
11856
11901
  function printHelp3() {
@@ -11936,7 +11981,7 @@ ${bold2("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red2(
11936
11981
  async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
11937
11982
  let prInfo;
11938
11983
  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"] });
11984
+ 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
11985
  const raw = JSON.parse(result);
11941
11986
  prInfo = {
11942
11987
  number: raw.number,
@@ -11981,7 +12026,8 @@ async function reviewPR(projectRoot, config, pr, focus, flags) {
11981
12026
  cwd: projectRoot,
11982
12027
  activity: `PR #${pr.number}`,
11983
12028
  sandboxed: config.sandbox.enabled,
11984
- sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
12029
+ sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
12030
+ containerWorkdir: config.sandbox.containerWorkdir
11985
12031
  });
11986
12032
  if (aiResult.interrupted) {
11987
12033
  process.stderr.write(` ${yellow2("⚡")} Review interrupted.
@@ -12083,7 +12129,7 @@ var exports_iterate = {};
12083
12129
  __export(exports_iterate, {
12084
12130
  iterateCommand: () => iterateCommand
12085
12131
  });
12086
- import { execSync as execSync18 } from "node:child_process";
12132
+ import { execSync as execSync19 } from "node:child_process";
12087
12133
  function printHelp4() {
12088
12134
  process.stderr.write(`
12089
12135
  ${bold2("locus iterate")} — Re-execute tasks with PR feedback
@@ -12301,12 +12347,12 @@ ${bold2("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red2(`✗ ${
12301
12347
  }
12302
12348
  function findPRForIssue(projectRoot, issueNumber) {
12303
12349
  try {
12304
- const result = execSync18(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12350
+ const result = execSync19(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12305
12351
  const parsed = JSON.parse(result);
12306
12352
  if (parsed.length > 0) {
12307
12353
  return parsed[0].number;
12308
12354
  }
12309
- const branchResult = execSync18(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12355
+ const branchResult = execSync19(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12310
12356
  const branchParsed = JSON.parse(branchResult);
12311
12357
  if (branchParsed.length > 0) {
12312
12358
  return branchParsed[0].number;
@@ -12563,7 +12609,8 @@ ${bold2("Discussion:")} ${cyan2(topic)}
12563
12609
  cwd: projectRoot,
12564
12610
  activity: "discussion",
12565
12611
  sandboxed: config.sandbox.enabled,
12566
- sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
12612
+ sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
12613
+ containerWorkdir: config.sandbox.containerWorkdir
12567
12614
  });
12568
12615
  if (aiResult.interrupted) {
12569
12616
  process.stderr.write(`
@@ -12936,7 +12983,7 @@ __export(exports_sandbox2, {
12936
12983
  parseSandboxLogsArgs: () => parseSandboxLogsArgs,
12937
12984
  parseSandboxInstallArgs: () => parseSandboxInstallArgs
12938
12985
  });
12939
- import { execSync as execSync19, spawn as spawn7 } from "node:child_process";
12986
+ import { execSync as execSync20, spawn as spawn7 } from "node:child_process";
12940
12987
  import { createHash } from "node:crypto";
12941
12988
  import { existsSync as existsSync26, readFileSync as readFileSync17 } from "node:fs";
12942
12989
  import { basename as basename4, join as join26 } from "node:path";
@@ -13070,11 +13117,20 @@ async function handleCreate(projectRoot) {
13070
13117
  }
13071
13118
  process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold2(name)}
13072
13119
  `);
13120
+ if (!config.sandbox.containerWorkdir) {
13121
+ const containerWorkdir = detectContainerWorkdir(name, projectRoot);
13122
+ if (containerWorkdir) {
13123
+ config.sandbox.containerWorkdir = containerWorkdir;
13124
+ process.stderr.write(` ${dim2(`Container workdir: ${containerWorkdir}`)}
13125
+ `);
13126
+ }
13127
+ }
13073
13128
  readySandboxes[provider] = name;
13074
13129
  config.sandbox.enabled = true;
13075
13130
  config.sandbox.providers = readySandboxes;
13076
13131
  saveConfig(projectRoot, config);
13077
- await runSandboxSetup(name, projectRoot);
13132
+ const workdir = config.sandbox.containerWorkdir ?? projectRoot;
13133
+ await runSandboxSetup(name, projectRoot, workdir);
13078
13134
  process.stderr.write(`
13079
13135
  ${green("✓")} Sandbox mode enabled for ${bold2(provider)}.
13080
13136
  `);
@@ -13099,19 +13155,20 @@ async function handleAgentLogin(projectRoot, agent) {
13099
13155
  if (agent === "codex") {
13100
13156
  await ensureCodexInSandbox(sandboxName);
13101
13157
  }
13158
+ const workdir = config.sandbox.containerWorkdir ?? projectRoot;
13102
13159
  process.stderr.write(`Connecting to ${agent} sandbox ${dim2(sandboxName)}...
13103
13160
  `);
13104
13161
  process.stderr.write(`${dim2("Login and then exit when ready.")}
13105
13162
 
13106
13163
  `);
13107
- const child = spawn7("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
13164
+ const child = spawn7("docker", ["sandbox", "exec", "-it", "-w", workdir, sandboxName, agent], {
13108
13165
  stdio: "inherit"
13109
13166
  });
13110
13167
  await new Promise((resolve2) => {
13111
13168
  child.on("close", async (code) => {
13112
13169
  const backup = backupIgnoredFiles(projectRoot);
13113
13170
  try {
13114
- await enforceSandboxIgnore(sandboxName, projectRoot);
13171
+ await enforceSandboxIgnore(sandboxName, projectRoot, config.sandbox.containerWorkdir);
13115
13172
  } finally {
13116
13173
  backup.restore();
13117
13174
  }
@@ -13148,7 +13205,7 @@ function handleRemove(projectRoot) {
13148
13205
  process.stderr.write(`Removing sandbox ${bold2(sandboxName)}...
13149
13206
  `);
13150
13207
  try {
13151
- execSync19(`docker sandbox rm ${sandboxName}`, {
13208
+ execSync20(`docker sandbox rm ${sandboxName}`, {
13152
13209
  encoding: "utf-8",
13153
13210
  stdio: ["pipe", "pipe", "pipe"],
13154
13211
  timeout: 15000
@@ -13157,6 +13214,7 @@ function handleRemove(projectRoot) {
13157
13214
  }
13158
13215
  config.sandbox.providers = {};
13159
13216
  config.sandbox.enabled = false;
13217
+ delete config.sandbox.containerWorkdir;
13160
13218
  saveConfig(projectRoot, config);
13161
13219
  process.stderr.write(`${green("✓")} Provider sandboxes removed. Sandbox mode disabled.
13162
13220
  `);
@@ -13169,6 +13227,10 @@ ${bold2("Sandbox Status")}
13169
13227
  `);
13170
13228
  process.stderr.write(` ${dim2("Enabled:")} ${config.sandbox.enabled ? green("yes") : red2("no")}
13171
13229
  `);
13230
+ if (config.sandbox.containerWorkdir) {
13231
+ process.stderr.write(` ${dim2("Container workdir:")} ${config.sandbox.containerWorkdir}
13232
+ `);
13233
+ }
13172
13234
  for (const provider of PROVIDERS) {
13173
13235
  const name = config.sandbox.providers[provider];
13174
13236
  process.stderr.write(` ${dim2(`${provider}:`).padEnd(15)}${name ? bold2(name) : dim2("(not configured)")}
@@ -13305,10 +13367,12 @@ async function handleShell(projectRoot, args) {
13305
13367
  `);
13306
13368
  return;
13307
13369
  }
13370
+ const config = loadConfig(projectRoot);
13308
13371
  const sandboxName = getActiveProviderSandbox(projectRoot, provider);
13309
13372
  if (!sandboxName) {
13310
13373
  return;
13311
13374
  }
13375
+ const workdir = config.sandbox.containerWorkdir ?? projectRoot;
13312
13376
  process.stderr.write(`Opening shell in ${provider} sandbox ${dim2(sandboxName)}...
13313
13377
  `);
13314
13378
  await runInteractiveCommand("docker", [
@@ -13316,7 +13380,7 @@ async function handleShell(projectRoot, args) {
13316
13380
  "exec",
13317
13381
  "-it",
13318
13382
  "-w",
13319
- projectRoot,
13383
+ workdir,
13320
13384
  sandboxName,
13321
13385
  "sh"
13322
13386
  ]);
@@ -13429,7 +13493,8 @@ function getInstallCommand(pm) {
13429
13493
  return ["npm", "install"];
13430
13494
  }
13431
13495
  }
13432
- async function runSandboxSetup(sandboxName, projectRoot) {
13496
+ async function runSandboxSetup(sandboxName, projectRoot, containerWorkdir) {
13497
+ const workdir = containerWorkdir ?? projectRoot;
13433
13498
  const ecosystem = detectProjectEcosystem(projectRoot);
13434
13499
  const isJS = isJavaScriptEcosystem(ecosystem);
13435
13500
  if (isJS) {
@@ -13445,7 +13510,7 @@ Installing dependencies (${bold2(installCmd.join(" "))}) in sandbox ${dim2(sandb
13445
13510
  "sandbox",
13446
13511
  "exec",
13447
13512
  "-w",
13448
- projectRoot,
13513
+ workdir,
13449
13514
  sandboxName,
13450
13515
  ...installCmd
13451
13516
  ]);
@@ -13462,6 +13527,7 @@ ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
13462
13527
  `);
13463
13528
  }
13464
13529
  const setupScript = join26(projectRoot, ".locus", "sandbox-setup.sh");
13530
+ const containerSetupScript = containerWorkdir ? join26(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
13465
13531
  if (existsSync26(setupScript)) {
13466
13532
  process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
13467
13533
  `);
@@ -13469,10 +13535,10 @@ ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
13469
13535
  "sandbox",
13470
13536
  "exec",
13471
13537
  "-w",
13472
- projectRoot,
13538
+ workdir,
13473
13539
  sandboxName,
13474
13540
  "sh",
13475
- setupScript
13541
+ containerSetupScript
13476
13542
  ]);
13477
13543
  if (!hookOk) {
13478
13544
  process.stderr.write(`${yellow2("⚠")} Setup hook failed in sandbox ${dim2(sandboxName)}.
@@ -13503,7 +13569,7 @@ async function handleSetup(projectRoot) {
13503
13569
  `);
13504
13570
  continue;
13505
13571
  }
13506
- await runSandboxSetup(sandboxName, projectRoot);
13572
+ await runSandboxSetup(sandboxName, projectRoot, config.sandbox.containerWorkdir);
13507
13573
  }
13508
13574
  }
13509
13575
  function buildProviderSandboxNames(projectRoot) {
@@ -13548,9 +13614,9 @@ function runInteractiveCommand(command, args) {
13548
13614
  child.on("error", () => resolve2(false));
13549
13615
  });
13550
13616
  }
13551
- async function createProviderSandbox(provider, sandboxName, projectRoot) {
13617
+ async function createProviderSandbox(provider, sandboxName, projectRoot, containerWorkdir) {
13552
13618
  try {
13553
- execSync19(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
13619
+ execSync20(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
13554
13620
  stdio: ["pipe", "pipe", "pipe"],
13555
13621
  timeout: 120000
13556
13622
  });
@@ -13563,7 +13629,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
13563
13629
  }
13564
13630
  const backup = backupIgnoredFiles(projectRoot);
13565
13631
  try {
13566
- await enforceSandboxIgnore(sandboxName, projectRoot);
13632
+ await enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir);
13567
13633
  } finally {
13568
13634
  backup.restore();
13569
13635
  }
@@ -13571,7 +13637,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
13571
13637
  }
13572
13638
  async function ensurePackageManagerInSandbox(sandboxName, pm) {
13573
13639
  try {
13574
- execSync19(`docker sandbox exec ${sandboxName} which ${pm}`, {
13640
+ execSync20(`docker sandbox exec ${sandboxName} which ${pm}`, {
13575
13641
  stdio: ["pipe", "pipe", "pipe"],
13576
13642
  timeout: 5000
13577
13643
  });
@@ -13580,7 +13646,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
13580
13646
  process.stderr.write(`Installing ${bold2(pm)} in sandbox...
13581
13647
  `);
13582
13648
  try {
13583
- execSync19(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
13649
+ execSync20(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
13584
13650
  stdio: "inherit",
13585
13651
  timeout: 120000
13586
13652
  });
@@ -13592,7 +13658,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
13592
13658
  }
13593
13659
  async function ensureCodexInSandbox(sandboxName) {
13594
13660
  try {
13595
- execSync19(`docker sandbox exec ${sandboxName} which codex`, {
13661
+ execSync20(`docker sandbox exec ${sandboxName} which codex`, {
13596
13662
  stdio: ["pipe", "pipe", "pipe"],
13597
13663
  timeout: 5000
13598
13664
  });
@@ -13600,7 +13666,7 @@ async function ensureCodexInSandbox(sandboxName) {
13600
13666
  process.stderr.write(`Installing codex in sandbox...
13601
13667
  `);
13602
13668
  try {
13603
- execSync19(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
13669
+ execSync20(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
13604
13670
  } catch {
13605
13671
  process.stderr.write(`${red2("✗")} Failed to install codex in sandbox.
13606
13672
  `);
@@ -13609,7 +13675,7 @@ async function ensureCodexInSandbox(sandboxName) {
13609
13675
  }
13610
13676
  function isSandboxAlive(name) {
13611
13677
  try {
13612
- const output = execSync19("docker sandbox ls", {
13678
+ const output = execSync20("docker sandbox ls", {
13613
13679
  encoding: "utf-8",
13614
13680
  stdio: ["pipe", "pipe", "pipe"],
13615
13681
  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.2",
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.2",
40
40
  "@types/bun": "latest",
41
41
  "typescript": "^5.8.3"
42
42
  },