@locusai/cli 0.20.1 → 0.20.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 +809 -313
  2. package/package.json +1 -1
package/bin/locus.js CHANGED
@@ -890,7 +890,8 @@ __export(exports_sandbox, {
890
890
  getProviderSandboxName: () => getProviderSandboxName,
891
891
  getModelSandboxName: () => getModelSandboxName,
892
892
  displaySandboxWarning: () => displaySandboxWarning,
893
- detectSandboxSupport: () => detectSandboxSupport
893
+ detectSandboxSupport: () => detectSandboxSupport,
894
+ checkProviderSandboxMismatch: () => checkProviderSandboxMismatch
894
895
  });
895
896
  import { execFile } from "node:child_process";
896
897
  import { createInterface } from "node:readline";
@@ -901,6 +902,19 @@ function getModelSandboxName(config, model, fallbackProvider) {
901
902
  const provider = inferProviderFromModel(model) ?? fallbackProvider;
902
903
  return getProviderSandboxName(config, provider);
903
904
  }
905
+ function checkProviderSandboxMismatch(config, model, fallbackProvider) {
906
+ if (!config.enabled)
907
+ return null;
908
+ const targetProvider = inferProviderFromModel(model) ?? fallbackProvider;
909
+ const sandboxName = getProviderSandboxName(config, targetProvider);
910
+ if (sandboxName)
911
+ return null;
912
+ const configured = ["claude", "codex"].filter((p) => config.providers[p]);
913
+ if (configured.length > 0) {
914
+ return `Sandbox is configured for ${configured.join(", ")} but not for ${targetProvider}. ` + `Run "locus sandbox" and select ${targetProvider} to create its sandbox.`;
915
+ }
916
+ return `No sandbox is configured for ${targetProvider}. ` + `Run "locus sandbox" to create one.`;
917
+ }
904
918
  async function detectSandboxSupport() {
905
919
  if (cachedStatus)
906
920
  return cachedStatus;
@@ -1280,6 +1294,177 @@ var init_version_check = __esm(() => {
1280
1294
  CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
1281
1295
  });
1282
1296
 
1297
+ // src/core/ecosystem.ts
1298
+ import { existsSync as existsSync5 } from "node:fs";
1299
+ import { join as join5 } from "node:path";
1300
+ function detectProjectEcosystem(projectRoot) {
1301
+ for (const signal of ECOSYSTEM_SIGNALS) {
1302
+ for (const marker of signal.markers) {
1303
+ if (marker.startsWith("*")) {
1304
+ continue;
1305
+ }
1306
+ if (existsSync5(join5(projectRoot, marker))) {
1307
+ return signal.ecosystem;
1308
+ }
1309
+ }
1310
+ }
1311
+ return "unknown";
1312
+ }
1313
+ function isJavaScriptEcosystem(ecosystem) {
1314
+ return ecosystem === "javascript";
1315
+ }
1316
+ function generateSandboxSetupTemplate(ecosystem) {
1317
+ switch (ecosystem) {
1318
+ case "javascript":
1319
+ return null;
1320
+ case "rust":
1321
+ return `#!/bin/sh
1322
+ # Sandbox setup for Rust projects
1323
+ # This script runs inside the Docker sandbox after creation.
1324
+ # Uncomment or modify the commands below for your project.
1325
+
1326
+ # Install Rust toolchain (if not pre-installed)
1327
+ # curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
1328
+ # . "$HOME/.cargo/env"
1329
+
1330
+ # Build the project
1331
+ # cargo build
1332
+
1333
+ echo "Rust sandbox setup complete"
1334
+ `;
1335
+ case "go":
1336
+ return `#!/bin/sh
1337
+ # Sandbox setup for Go projects
1338
+ # This script runs inside the Docker sandbox after creation.
1339
+
1340
+ # Download dependencies
1341
+ # go mod download
1342
+
1343
+ # Build the project
1344
+ # go build ./...
1345
+
1346
+ echo "Go sandbox setup complete"
1347
+ `;
1348
+ case "python":
1349
+ return `#!/bin/sh
1350
+ # Sandbox setup for Python projects
1351
+ # This script runs inside the Docker sandbox after creation.
1352
+
1353
+ # Create virtual environment and install dependencies
1354
+ # python3 -m venv .venv
1355
+ # . .venv/bin/activate
1356
+ # pip install -r requirements.txt
1357
+ # OR: pip install -e .
1358
+
1359
+ echo "Python sandbox setup complete"
1360
+ `;
1361
+ case "java":
1362
+ return `#!/bin/sh
1363
+ # Sandbox setup for Java/JVM projects
1364
+ # This script runs inside the Docker sandbox after creation.
1365
+
1366
+ # Maven
1367
+ # mvn install -DskipTests
1368
+
1369
+ # Gradle
1370
+ # ./gradlew build -x test
1371
+
1372
+ echo "Java sandbox setup complete"
1373
+ `;
1374
+ case "ruby":
1375
+ return `#!/bin/sh
1376
+ # Sandbox setup for Ruby projects
1377
+ # This script runs inside the Docker sandbox after creation.
1378
+
1379
+ # Install dependencies
1380
+ # bundle install
1381
+
1382
+ echo "Ruby sandbox setup complete"
1383
+ `;
1384
+ case "elixir":
1385
+ return `#!/bin/sh
1386
+ # Sandbox setup for Elixir projects
1387
+ # This script runs inside the Docker sandbox after creation.
1388
+
1389
+ # Install dependencies
1390
+ # mix deps.get
1391
+ # mix compile
1392
+
1393
+ echo "Elixir sandbox setup complete"
1394
+ `;
1395
+ case "dotnet":
1396
+ return `#!/bin/sh
1397
+ # Sandbox setup for .NET projects
1398
+ # This script runs inside the Docker sandbox after creation.
1399
+
1400
+ # Restore packages
1401
+ # dotnet restore
1402
+
1403
+ # Build
1404
+ # dotnet build
1405
+
1406
+ echo ".NET sandbox setup complete"
1407
+ `;
1408
+ case "unknown":
1409
+ return `#!/bin/sh
1410
+ # Sandbox setup script
1411
+ # This script runs inside the Docker sandbox after creation.
1412
+ # Add any commands needed to prepare the build environment.
1413
+
1414
+ # Example: install system dependencies
1415
+ # apt-get update && apt-get install -y <packages>
1416
+
1417
+ # Example: install project dependencies
1418
+ # <your-package-manager> install
1419
+
1420
+ echo "Sandbox setup complete"
1421
+ `;
1422
+ }
1423
+ }
1424
+ var ECOSYSTEM_SIGNALS;
1425
+ var init_ecosystem = __esm(() => {
1426
+ ECOSYSTEM_SIGNALS = [
1427
+ {
1428
+ ecosystem: "javascript",
1429
+ markers: ["package.json"]
1430
+ },
1431
+ {
1432
+ ecosystem: "rust",
1433
+ markers: ["Cargo.toml"]
1434
+ },
1435
+ {
1436
+ ecosystem: "go",
1437
+ markers: ["go.mod"]
1438
+ },
1439
+ {
1440
+ ecosystem: "python",
1441
+ markers: [
1442
+ "pyproject.toml",
1443
+ "setup.py",
1444
+ "setup.cfg",
1445
+ "Pipfile",
1446
+ "requirements.txt"
1447
+ ]
1448
+ },
1449
+ {
1450
+ ecosystem: "java",
1451
+ markers: ["pom.xml", "build.gradle", "build.gradle.kts"]
1452
+ },
1453
+ {
1454
+ ecosystem: "ruby",
1455
+ markers: ["Gemfile"]
1456
+ },
1457
+ {
1458
+ ecosystem: "elixir",
1459
+ markers: ["mix.exs"]
1460
+ },
1461
+ {
1462
+ ecosystem: "dotnet",
1463
+ markers: ["*.csproj", "*.fsproj", "*.sln"]
1464
+ }
1465
+ ];
1466
+ });
1467
+
1283
1468
  // src/core/github.ts
1284
1469
  import {
1285
1470
  execFileSync,
@@ -1476,7 +1661,18 @@ function reopenMilestone(owner, repo, milestoneNumber, options = {}) {
1476
1661
  gh(`api repos/${owner}/${repo}/milestones/${milestoneNumber} -X PATCH -f state=open`, options);
1477
1662
  }
1478
1663
  function createPR(title, body, head, base, options = {}) {
1479
- const result = ghExec(["pr", "create", "--title", title, "--body", body, "--head", head, "--base", base], options);
1664
+ const result = ghExec([
1665
+ "pr",
1666
+ "create",
1667
+ "--title",
1668
+ title,
1669
+ "--body",
1670
+ body,
1671
+ "--head",
1672
+ head,
1673
+ "--base",
1674
+ base
1675
+ ], options);
1480
1676
  const match = result.match(/\/pull\/(\d+)/);
1481
1677
  if (!match) {
1482
1678
  throw new Error(`Could not extract PR number from: ${result}`);
@@ -1630,8 +1826,8 @@ var exports_init = {};
1630
1826
  __export(exports_init, {
1631
1827
  initCommand: () => initCommand
1632
1828
  });
1633
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1634
- import { join as join5 } from "node:path";
1829
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1830
+ import { join as join6 } from "node:path";
1635
1831
  async function initCommand(cwd) {
1636
1832
  const log = getLogger();
1637
1833
  process.stderr.write(`
@@ -1675,17 +1871,17 @@ ${bold("Initializing Locus...")}
1675
1871
  }
1676
1872
  process.stderr.write(`${green("✓")} Repository: ${bold(`${context.owner}/${context.repo}`)} (branch: ${context.defaultBranch})
1677
1873
  `);
1678
- const locusDir = join5(cwd, ".locus");
1874
+ const locusDir = join6(cwd, ".locus");
1679
1875
  const dirs = [
1680
1876
  locusDir,
1681
- join5(locusDir, "sessions"),
1682
- join5(locusDir, "discussions"),
1683
- join5(locusDir, "artifacts"),
1684
- join5(locusDir, "plans"),
1685
- join5(locusDir, "logs")
1877
+ join6(locusDir, "sessions"),
1878
+ join6(locusDir, "discussions"),
1879
+ join6(locusDir, "artifacts"),
1880
+ join6(locusDir, "plans"),
1881
+ join6(locusDir, "logs")
1686
1882
  ];
1687
1883
  for (const dir of dirs) {
1688
- if (!existsSync5(dir)) {
1884
+ if (!existsSync6(dir)) {
1689
1885
  mkdirSync5(dir, { recursive: true });
1690
1886
  }
1691
1887
  }
@@ -1706,7 +1902,7 @@ ${bold("Initializing Locus...")}
1706
1902
  };
1707
1903
  if (isReInit) {
1708
1904
  try {
1709
- const existing = JSON.parse(readFileSync4(join5(locusDir, "config.json"), "utf-8"));
1905
+ const existing = JSON.parse(readFileSync4(join6(locusDir, "config.json"), "utf-8"));
1710
1906
  if (existing.ai)
1711
1907
  config.ai = { ...config.ai, ...existing.ai };
1712
1908
  if (existing.agent)
@@ -1725,8 +1921,8 @@ ${bold("Initializing Locus...")}
1725
1921
  `);
1726
1922
  }
1727
1923
  saveConfig(cwd, config);
1728
- const locusMdPath = join5(locusDir, "LOCUS.md");
1729
- if (!existsSync5(locusMdPath)) {
1924
+ const locusMdPath = join6(locusDir, "LOCUS.md");
1925
+ if (!existsSync6(locusMdPath)) {
1730
1926
  writeFileSync4(locusMdPath, LOCUS_MD_TEMPLATE, "utf-8");
1731
1927
  process.stderr.write(`${green("✓")} Generated LOCUS.md (edit to add project context)
1732
1928
  `);
@@ -1734,8 +1930,8 @@ ${bold("Initializing Locus...")}
1734
1930
  process.stderr.write(`${dim("○")} LOCUS.md already exists (preserved)
1735
1931
  `);
1736
1932
  }
1737
- const learningsMdPath = join5(locusDir, "LEARNINGS.md");
1738
- if (!existsSync5(learningsMdPath)) {
1933
+ const learningsMdPath = join6(locusDir, "LEARNINGS.md");
1934
+ if (!existsSync6(learningsMdPath)) {
1739
1935
  writeFileSync4(learningsMdPath, LEARNINGS_MD_TEMPLATE, "utf-8");
1740
1936
  process.stderr.write(`${green("✓")} Generated LEARNINGS.md
1741
1937
  `);
@@ -1743,13 +1939,29 @@ ${bold("Initializing Locus...")}
1743
1939
  process.stderr.write(`${dim("○")} LEARNINGS.md already exists (preserved)
1744
1940
  `);
1745
1941
  }
1746
- const sandboxIgnorePath = join5(cwd, ".sandboxignore");
1747
- if (!existsSync5(sandboxIgnorePath)) {
1942
+ const sandboxIgnorePath = join6(cwd, ".sandboxignore");
1943
+ if (!existsSync6(sandboxIgnorePath)) {
1748
1944
  writeFileSync4(sandboxIgnorePath, SANDBOXIGNORE_TEMPLATE, "utf-8");
1749
1945
  process.stderr.write(`${green("✓")} Generated .sandboxignore
1750
1946
  `);
1751
1947
  } else {
1752
1948
  process.stderr.write(`${dim("○")} .sandboxignore already exists (preserved)
1949
+ `);
1950
+ }
1951
+ const ecosystem = detectProjectEcosystem(cwd);
1952
+ const sandboxSetupPath = join6(locusDir, "sandbox-setup.sh");
1953
+ if (!existsSync6(sandboxSetupPath)) {
1954
+ const template = generateSandboxSetupTemplate(ecosystem);
1955
+ if (template) {
1956
+ writeFileSync4(sandboxSetupPath, template, {
1957
+ encoding: "utf-8",
1958
+ mode: 493
1959
+ });
1960
+ process.stderr.write(`${green("✓")} Generated .locus/sandbox-setup.sh (${ecosystem} project detected)
1961
+ `);
1962
+ }
1963
+ } else {
1964
+ process.stderr.write(`${dim("○")} sandbox-setup.sh already exists (preserved)
1753
1965
  `);
1754
1966
  }
1755
1967
  process.stderr.write(`${cyan("●")} Creating GitHub labels...`);
@@ -1761,9 +1973,9 @@ ${bold("Initializing Locus...")}
1761
1973
  process.stderr.write(`\r${yellow("⚠")} Some labels could not be created: ${e.message}
1762
1974
  `);
1763
1975
  }
1764
- const gitignorePath = join5(cwd, ".gitignore");
1976
+ const gitignorePath = join6(cwd, ".gitignore");
1765
1977
  let gitignoreContent = "";
1766
- if (existsSync5(gitignorePath)) {
1978
+ if (existsSync6(gitignorePath)) {
1767
1979
  gitignoreContent = readFileSync4(gitignorePath, "utf-8");
1768
1980
  }
1769
1981
  const entriesToAdd = GITIGNORE_ENTRIES.filter((entry) => entry && !gitignoreContent.includes(entry.trim()));
@@ -1996,6 +2208,7 @@ It is read by AI agents before every task to avoid repeating mistakes and to fol
1996
2208
  var init_init = __esm(() => {
1997
2209
  init_config();
1998
2210
  init_context();
2211
+ init_ecosystem();
1999
2212
  init_github();
2000
2213
  init_logger();
2001
2214
  init_terminal();
@@ -2017,33 +2230,33 @@ var init_init = __esm(() => {
2017
2230
 
2018
2231
  // src/packages/registry.ts
2019
2232
  import {
2020
- existsSync as existsSync6,
2233
+ existsSync as existsSync7,
2021
2234
  mkdirSync as mkdirSync6,
2022
2235
  readFileSync as readFileSync5,
2023
2236
  renameSync,
2024
2237
  writeFileSync as writeFileSync5
2025
2238
  } from "node:fs";
2026
2239
  import { homedir as homedir2 } from "node:os";
2027
- import { join as join6 } from "node:path";
2240
+ import { join as join7 } from "node:path";
2028
2241
  function getPackagesDir() {
2029
2242
  const home = process.env.HOME || homedir2();
2030
- const dir = join6(home, ".locus", "packages");
2031
- if (!existsSync6(dir)) {
2243
+ const dir = join7(home, ".locus", "packages");
2244
+ if (!existsSync7(dir)) {
2032
2245
  mkdirSync6(dir, { recursive: true });
2033
2246
  }
2034
- const pkgJson = join6(dir, "package.json");
2035
- if (!existsSync6(pkgJson)) {
2247
+ const pkgJson = join7(dir, "package.json");
2248
+ if (!existsSync7(pkgJson)) {
2036
2249
  writeFileSync5(pkgJson, `${JSON.stringify({ private: true }, null, 2)}
2037
2250
  `, "utf-8");
2038
2251
  }
2039
2252
  return dir;
2040
2253
  }
2041
2254
  function getRegistryPath() {
2042
- return join6(getPackagesDir(), "registry.json");
2255
+ return join7(getPackagesDir(), "registry.json");
2043
2256
  }
2044
2257
  function loadRegistry() {
2045
2258
  const registryPath = getRegistryPath();
2046
- if (!existsSync6(registryPath)) {
2259
+ if (!existsSync7(registryPath)) {
2047
2260
  return { packages: {} };
2048
2261
  }
2049
2262
  try {
@@ -2066,8 +2279,8 @@ function saveRegistry(registry) {
2066
2279
  }
2067
2280
  function resolvePackageBinary(packageName) {
2068
2281
  const fullName = normalizePackageName(packageName);
2069
- const binPath = join6(getPackagesDir(), "node_modules", ".bin", fullName);
2070
- return existsSync6(binPath) ? binPath : null;
2282
+ const binPath = join7(getPackagesDir(), "node_modules", ".bin", fullName);
2283
+ return existsSync7(binPath) ? binPath : null;
2071
2284
  }
2072
2285
  function normalizePackageName(input) {
2073
2286
  if (input.startsWith("@")) {
@@ -2087,7 +2300,7 @@ __export(exports_pkg, {
2087
2300
  listInstalledPackages: () => listInstalledPackages
2088
2301
  });
2089
2302
  import { spawn } from "node:child_process";
2090
- import { existsSync as existsSync7 } from "node:fs";
2303
+ import { existsSync as existsSync8 } from "node:fs";
2091
2304
  function listInstalledPackages() {
2092
2305
  const registry = loadRegistry();
2093
2306
  const entries = Object.values(registry.packages);
@@ -2165,7 +2378,7 @@ async function pkgCommand(args, _flags) {
2165
2378
  return;
2166
2379
  }
2167
2380
  const binaryPath = entry.binaryPath;
2168
- if (!binaryPath || !existsSync7(binaryPath)) {
2381
+ if (!binaryPath || !existsSync8(binaryPath)) {
2169
2382
  process.stderr.write(`${red("✗")} Binary for ${bold(packageName)} not found on disk.
2170
2383
  `);
2171
2384
  if (binaryPath) {
@@ -2291,8 +2504,8 @@ __export(exports_install, {
2291
2504
  installCommand: () => installCommand
2292
2505
  });
2293
2506
  import { spawnSync as spawnSync2 } from "node:child_process";
2294
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
2295
- import { join as join7 } from "node:path";
2507
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
2508
+ import { join as join8 } from "node:path";
2296
2509
  function parsePackageArg(raw) {
2297
2510
  if (raw.startsWith("@")) {
2298
2511
  const slashIdx = raw.indexOf("/");
@@ -2369,8 +2582,8 @@ ${red("✗")} Failed to install ${bold(packageSpec)}.
2369
2582
  process.exit(1);
2370
2583
  return;
2371
2584
  }
2372
- const installedPkgJsonPath = join7(packagesDir, "node_modules", packageName, "package.json");
2373
- if (!existsSync8(installedPkgJsonPath)) {
2585
+ const installedPkgJsonPath = join8(packagesDir, "node_modules", packageName, "package.json");
2586
+ if (!existsSync9(installedPkgJsonPath)) {
2374
2587
  process.stderr.write(`
2375
2588
  ${red("✗")} Package installed but package.json not found at:
2376
2589
  `);
@@ -2645,16 +2858,16 @@ __export(exports_logs, {
2645
2858
  logsCommand: () => logsCommand
2646
2859
  });
2647
2860
  import {
2648
- existsSync as existsSync9,
2861
+ existsSync as existsSync10,
2649
2862
  readdirSync as readdirSync2,
2650
2863
  readFileSync as readFileSync7,
2651
2864
  statSync as statSync2,
2652
2865
  unlinkSync as unlinkSync2
2653
2866
  } from "node:fs";
2654
- import { join as join8 } from "node:path";
2867
+ import { join as join9 } from "node:path";
2655
2868
  async function logsCommand(cwd, options) {
2656
- const logsDir = join8(cwd, ".locus", "logs");
2657
- if (!existsSync9(logsDir)) {
2869
+ const logsDir = join9(cwd, ".locus", "logs");
2870
+ if (!existsSync10(logsDir)) {
2658
2871
  process.stderr.write(`${dim("No logs found.")}
2659
2872
  `);
2660
2873
  return;
@@ -2709,8 +2922,8 @@ async function tailLog(logFile, levelFilter) {
2709
2922
  process.stderr.write(`${bold("Tailing:")} ${dim(logFile)} ${dim("(Ctrl+C to stop)")}
2710
2923
 
2711
2924
  `);
2712
- let lastSize = existsSync9(logFile) ? statSync2(logFile).size : 0;
2713
- if (existsSync9(logFile)) {
2925
+ let lastSize = existsSync10(logFile) ? statSync2(logFile).size : 0;
2926
+ if (existsSync10(logFile)) {
2714
2927
  const content = readFileSync7(logFile, "utf-8");
2715
2928
  const lines = content.trim().split(`
2716
2929
  `).filter(Boolean);
@@ -2729,7 +2942,7 @@ async function tailLog(logFile, levelFilter) {
2729
2942
  }
2730
2943
  return new Promise((resolve) => {
2731
2944
  const interval = setInterval(() => {
2732
- if (!existsSync9(logFile))
2945
+ if (!existsSync10(logFile))
2733
2946
  return;
2734
2947
  const currentSize = statSync2(logFile).size;
2735
2948
  if (currentSize <= lastSize)
@@ -2789,7 +3002,7 @@ function cleanLogs(logsDir) {
2789
3002
  `);
2790
3003
  }
2791
3004
  function getLogFiles(logsDir) {
2792
- return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join8(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
3005
+ return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join9(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
2793
3006
  }
2794
3007
  function formatEntry(entry) {
2795
3008
  const time = dim(new Date(entry.ts).toLocaleTimeString());
@@ -3099,9 +3312,9 @@ var init_stream_renderer = __esm(() => {
3099
3312
 
3100
3313
  // src/repl/clipboard.ts
3101
3314
  import { execSync as execSync4 } from "node:child_process";
3102
- import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "node:fs";
3315
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7 } from "node:fs";
3103
3316
  import { tmpdir } from "node:os";
3104
- import { join as join9 } from "node:path";
3317
+ import { join as join10 } from "node:path";
3105
3318
  function readClipboardImage() {
3106
3319
  if (process.platform === "darwin") {
3107
3320
  return readMacOSClipboardImage();
@@ -3112,14 +3325,14 @@ function readClipboardImage() {
3112
3325
  return null;
3113
3326
  }
3114
3327
  function ensureStableDir() {
3115
- if (!existsSync10(STABLE_DIR)) {
3328
+ if (!existsSync11(STABLE_DIR)) {
3116
3329
  mkdirSync7(STABLE_DIR, { recursive: true });
3117
3330
  }
3118
3331
  }
3119
3332
  function readMacOSClipboardImage() {
3120
3333
  try {
3121
3334
  ensureStableDir();
3122
- const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
3335
+ const destPath = join10(STABLE_DIR, `clipboard-${Date.now()}.png`);
3123
3336
  const script = [
3124
3337
  `set destPath to POSIX file "${destPath}"`,
3125
3338
  "try",
@@ -3143,7 +3356,7 @@ function readMacOSClipboardImage() {
3143
3356
  timeout: 5000,
3144
3357
  stdio: ["pipe", "pipe", "pipe"]
3145
3358
  }).trim();
3146
- if (result === "ok" && existsSync10(destPath)) {
3359
+ if (result === "ok" && existsSync11(destPath)) {
3147
3360
  return destPath;
3148
3361
  }
3149
3362
  } catch {}
@@ -3156,9 +3369,9 @@ function readLinuxClipboardImage() {
3156
3369
  return null;
3157
3370
  }
3158
3371
  ensureStableDir();
3159
- const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
3372
+ const destPath = join10(STABLE_DIR, `clipboard-${Date.now()}.png`);
3160
3373
  execSync4(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
3161
- if (existsSync10(destPath)) {
3374
+ if (existsSync11(destPath)) {
3162
3375
  return destPath;
3163
3376
  }
3164
3377
  } catch {}
@@ -3166,13 +3379,13 @@ function readLinuxClipboardImage() {
3166
3379
  }
3167
3380
  var STABLE_DIR;
3168
3381
  var init_clipboard = __esm(() => {
3169
- STABLE_DIR = join9(tmpdir(), "locus-images");
3382
+ STABLE_DIR = join10(tmpdir(), "locus-images");
3170
3383
  });
3171
3384
 
3172
3385
  // src/repl/image-detect.ts
3173
- import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "node:fs";
3386
+ import { copyFileSync, existsSync as existsSync12, mkdirSync as mkdirSync8 } from "node:fs";
3174
3387
  import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
3175
- import { basename, extname, join as join10, resolve } from "node:path";
3388
+ import { basename, extname, join as join11, resolve } from "node:path";
3176
3389
  function detectImages(input) {
3177
3390
  const detected = [];
3178
3391
  const byResolved = new Map;
@@ -3266,15 +3479,15 @@ function collectReferencedAttachments(input, attachments) {
3266
3479
  return dedupeByResolvedPath(selected);
3267
3480
  }
3268
3481
  function relocateImages(images, projectRoot) {
3269
- const targetDir = join10(projectRoot, ".locus", "tmp", "images");
3482
+ const targetDir = join11(projectRoot, ".locus", "tmp", "images");
3270
3483
  for (const img of images) {
3271
3484
  if (!img.exists)
3272
3485
  continue;
3273
3486
  try {
3274
- if (!existsSync11(targetDir)) {
3487
+ if (!existsSync12(targetDir)) {
3275
3488
  mkdirSync8(targetDir, { recursive: true });
3276
3489
  }
3277
- const dest = join10(targetDir, basename(img.stablePath));
3490
+ const dest = join11(targetDir, basename(img.stablePath));
3278
3491
  copyFileSync(img.stablePath, dest);
3279
3492
  img.stablePath = dest;
3280
3493
  } catch {}
@@ -3286,7 +3499,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
3286
3499
  return;
3287
3500
  let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
3288
3501
  if (resolved.startsWith("~/")) {
3289
- resolved = join10(homedir3(), resolved.slice(2));
3502
+ resolved = join11(homedir3(), resolved.slice(2));
3290
3503
  }
3291
3504
  resolved = resolve(resolved);
3292
3505
  const existing = byResolved.get(resolved);
@@ -3299,7 +3512,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
3299
3512
  ]);
3300
3513
  return;
3301
3514
  }
3302
- const exists = existsSync11(resolved);
3515
+ const exists = existsSync12(resolved);
3303
3516
  let stablePath = resolved;
3304
3517
  if (exists) {
3305
3518
  stablePath = copyToStable(resolved);
@@ -3353,10 +3566,10 @@ function dedupeByResolvedPath(images) {
3353
3566
  }
3354
3567
  function copyToStable(sourcePath) {
3355
3568
  try {
3356
- if (!existsSync11(STABLE_DIR2)) {
3569
+ if (!existsSync12(STABLE_DIR2)) {
3357
3570
  mkdirSync8(STABLE_DIR2, { recursive: true });
3358
3571
  }
3359
- const dest = join10(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
3572
+ const dest = join11(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
3360
3573
  copyFileSync(sourcePath, dest);
3361
3574
  return dest;
3362
3575
  } catch {
@@ -3376,7 +3589,7 @@ var init_image_detect = __esm(() => {
3376
3589
  ".tif",
3377
3590
  ".tiff"
3378
3591
  ]);
3379
- STABLE_DIR2 = join10(tmpdir2(), "locus-images");
3592
+ STABLE_DIR2 = join11(tmpdir2(), "locus-images");
3380
3593
  PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
3381
3594
  });
3382
3595
 
@@ -4160,10 +4373,7 @@ __export(exports_claude, {
4160
4373
  });
4161
4374
  import { execSync as execSync5, spawn as spawn2 } from "node:child_process";
4162
4375
  function buildClaudeArgs(options) {
4163
- const args = [
4164
- "--dangerously-skip-permissions",
4165
- "--no-session-persistence"
4166
- ];
4376
+ const args = ["--dangerously-skip-permissions", "--no-session-persistence"];
4167
4377
  if (options.model) {
4168
4378
  args.push("--model", options.model);
4169
4379
  }
@@ -4355,11 +4565,11 @@ var init_claude = __esm(() => {
4355
4565
 
4356
4566
  // src/core/sandbox-ignore.ts
4357
4567
  import { exec } from "node:child_process";
4358
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
4359
- import { join as join11 } from "node:path";
4568
+ import { existsSync as existsSync13, readFileSync as readFileSync8 } from "node:fs";
4569
+ import { join as join12 } from "node:path";
4360
4570
  import { promisify } from "node:util";
4361
4571
  function parseIgnoreFile(filePath) {
4362
- if (!existsSync12(filePath))
4572
+ if (!existsSync13(filePath))
4363
4573
  return [];
4364
4574
  const content = readFileSync8(filePath, "utf-8");
4365
4575
  const rules = [];
@@ -4406,7 +4616,7 @@ function buildCleanupScript(rules, workspacePath) {
4406
4616
  }
4407
4617
  async function enforceSandboxIgnore(sandboxName, projectRoot) {
4408
4618
  const log = getLogger();
4409
- const ignorePath = join11(projectRoot, ".sandboxignore");
4619
+ const ignorePath = join12(projectRoot, ".sandboxignore");
4410
4620
  const rules = parseIgnoreFile(ignorePath);
4411
4621
  if (rules.length === 0)
4412
4622
  return;
@@ -5141,7 +5351,7 @@ ${red("✗")} ${dim("Force exit.")}\r
5141
5351
  return {
5142
5352
  success: false,
5143
5353
  output: "",
5144
- error: `Sandbox for provider "${resolvedProvider}" is not configured. ` + `Run "locus sandbox" and authenticate via "locus sandbox ${resolvedProvider}".`,
5354
+ error: `No sandbox configured for "${resolvedProvider}". ` + `Run "locus sandbox" and select "${resolvedProvider}" to create its sandbox, ` + `then authenticate via "locus sandbox ${resolvedProvider}".`,
5145
5355
  interrupted: false,
5146
5356
  exitCode: 1
5147
5357
  };
@@ -6685,8 +6895,8 @@ var init_sprint = __esm(() => {
6685
6895
 
6686
6896
  // src/core/prompt-builder.ts
6687
6897
  import { execSync as execSync7 } from "node:child_process";
6688
- import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
6689
- import { join as join12 } from "node:path";
6898
+ import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
6899
+ import { join as join13 } from "node:path";
6690
6900
  function buildExecutionPrompt(ctx) {
6691
6901
  const sections = [];
6692
6902
  sections.push(buildSystemContext(ctx.projectRoot));
@@ -6716,13 +6926,13 @@ function buildFeedbackPrompt(ctx) {
6716
6926
  }
6717
6927
  function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
6718
6928
  const sections = [];
6719
- const locusmd = readFileSafe(join12(projectRoot, ".locus", "LOCUS.md"));
6929
+ const locusmd = readFileSafe(join13(projectRoot, ".locus", "LOCUS.md"));
6720
6930
  if (locusmd) {
6721
6931
  sections.push(`<project-instructions>
6722
6932
  ${locusmd}
6723
6933
  </project-instructions>`);
6724
6934
  }
6725
- const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
6935
+ const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
6726
6936
  if (learnings) {
6727
6937
  sections.push(`<past-learnings>
6728
6938
  ${learnings}
@@ -6748,24 +6958,24 @@ ${userMessage}
6748
6958
  }
6749
6959
  function buildSystemContext(projectRoot) {
6750
6960
  const parts = [];
6751
- const locusmd = readFileSafe(join12(projectRoot, ".locus", "LOCUS.md"));
6961
+ const locusmd = readFileSafe(join13(projectRoot, ".locus", "LOCUS.md"));
6752
6962
  if (locusmd) {
6753
6963
  parts.push(`<project-instructions>
6754
6964
  ${locusmd}
6755
6965
  </project-instructions>`);
6756
6966
  }
6757
- const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
6967
+ const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
6758
6968
  if (learnings) {
6759
6969
  parts.push(`<past-learnings>
6760
6970
  ${learnings}
6761
6971
  </past-learnings>`);
6762
6972
  }
6763
- const discussionsDir = join12(projectRoot, ".locus", "discussions");
6764
- if (existsSync13(discussionsDir)) {
6973
+ const discussionsDir = join13(projectRoot, ".locus", "discussions");
6974
+ if (existsSync14(discussionsDir)) {
6765
6975
  try {
6766
6976
  const files = readdirSync3(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
6767
6977
  for (const file of files) {
6768
- const content = readFileSafe(join12(discussionsDir, file));
6978
+ const content = readFileSafe(join13(discussionsDir, file));
6769
6979
  if (content) {
6770
6980
  const name = file.replace(".md", "");
6771
6981
  parts.push(`<discussion name="${name}">
@@ -6916,7 +7126,7 @@ function buildFeedbackInstructions() {
6916
7126
  }
6917
7127
  function readFileSafe(path) {
6918
7128
  try {
6919
- if (!existsSync13(path))
7129
+ if (!existsSync14(path))
6920
7130
  return null;
6921
7131
  return readFileSync9(path, "utf-8");
6922
7132
  } catch {
@@ -7382,7 +7592,7 @@ var init_commands = __esm(() => {
7382
7592
 
7383
7593
  // src/repl/completions.ts
7384
7594
  import { readdirSync as readdirSync4 } from "node:fs";
7385
- import { basename as basename2, dirname as dirname3, join as join13 } from "node:path";
7595
+ import { basename as basename2, dirname as dirname3, join as join14 } from "node:path";
7386
7596
 
7387
7597
  class SlashCommandCompletion {
7388
7598
  commands;
@@ -7437,7 +7647,7 @@ class FilePathCompletion {
7437
7647
  }
7438
7648
  findMatches(partial) {
7439
7649
  try {
7440
- const dir = partial.includes("/") ? join13(this.projectRoot, dirname3(partial)) : this.projectRoot;
7650
+ const dir = partial.includes("/") ? join14(this.projectRoot, dirname3(partial)) : this.projectRoot;
7441
7651
  const prefix = basename2(partial);
7442
7652
  const entries = readdirSync4(dir, { withFileTypes: true });
7443
7653
  return entries.filter((e) => {
@@ -7473,14 +7683,14 @@ class CombinedCompletion {
7473
7683
  var init_completions = () => {};
7474
7684
 
7475
7685
  // src/repl/input-history.ts
7476
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
7477
- import { dirname as dirname4, join as join14 } from "node:path";
7686
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
7687
+ import { dirname as dirname4, join as join15 } from "node:path";
7478
7688
 
7479
7689
  class InputHistory {
7480
7690
  entries = [];
7481
7691
  filePath;
7482
7692
  constructor(projectRoot) {
7483
- this.filePath = join14(projectRoot, ".locus", "sessions", ".input-history");
7693
+ this.filePath = join15(projectRoot, ".locus", "sessions", ".input-history");
7484
7694
  this.load();
7485
7695
  }
7486
7696
  add(text) {
@@ -7519,7 +7729,7 @@ class InputHistory {
7519
7729
  }
7520
7730
  load() {
7521
7731
  try {
7522
- if (!existsSync14(this.filePath))
7732
+ if (!existsSync15(this.filePath))
7523
7733
  return;
7524
7734
  const content = readFileSync10(this.filePath, "utf-8");
7525
7735
  this.entries = content.split(`
@@ -7529,7 +7739,7 @@ class InputHistory {
7529
7739
  save() {
7530
7740
  try {
7531
7741
  const dir = dirname4(this.filePath);
7532
- if (!existsSync14(dir)) {
7742
+ if (!existsSync15(dir)) {
7533
7743
  mkdirSync9(dir, { recursive: true });
7534
7744
  }
7535
7745
  const content = this.entries.map((e) => this.escape(e)).join(`
@@ -7560,20 +7770,20 @@ var init_model_config = __esm(() => {
7560
7770
 
7561
7771
  // src/repl/session-manager.ts
7562
7772
  import {
7563
- existsSync as existsSync15,
7773
+ existsSync as existsSync16,
7564
7774
  mkdirSync as mkdirSync10,
7565
7775
  readdirSync as readdirSync5,
7566
7776
  readFileSync as readFileSync11,
7567
7777
  unlinkSync as unlinkSync3,
7568
7778
  writeFileSync as writeFileSync7
7569
7779
  } from "node:fs";
7570
- import { basename as basename3, join as join15 } from "node:path";
7780
+ import { basename as basename3, join as join16 } from "node:path";
7571
7781
 
7572
7782
  class SessionManager {
7573
7783
  sessionsDir;
7574
7784
  constructor(projectRoot) {
7575
- this.sessionsDir = join15(projectRoot, ".locus", "sessions");
7576
- if (!existsSync15(this.sessionsDir)) {
7785
+ this.sessionsDir = join16(projectRoot, ".locus", "sessions");
7786
+ if (!existsSync16(this.sessionsDir)) {
7577
7787
  mkdirSync10(this.sessionsDir, { recursive: true });
7578
7788
  }
7579
7789
  }
@@ -7599,12 +7809,12 @@ class SessionManager {
7599
7809
  }
7600
7810
  isPersisted(sessionOrId) {
7601
7811
  const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
7602
- return existsSync15(this.getSessionPath(sessionId));
7812
+ return existsSync16(this.getSessionPath(sessionId));
7603
7813
  }
7604
7814
  load(idOrPrefix) {
7605
7815
  const files = this.listSessionFiles();
7606
7816
  const exactPath = this.getSessionPath(idOrPrefix);
7607
- if (existsSync15(exactPath)) {
7817
+ if (existsSync16(exactPath)) {
7608
7818
  try {
7609
7819
  return JSON.parse(readFileSync11(exactPath, "utf-8"));
7610
7820
  } catch {
@@ -7654,7 +7864,7 @@ class SessionManager {
7654
7864
  }
7655
7865
  delete(sessionId) {
7656
7866
  const path = this.getSessionPath(sessionId);
7657
- if (existsSync15(path)) {
7867
+ if (existsSync16(path)) {
7658
7868
  unlinkSync3(path);
7659
7869
  return true;
7660
7870
  }
@@ -7684,7 +7894,7 @@ class SessionManager {
7684
7894
  const remaining = withStats.length - pruned;
7685
7895
  if (remaining > MAX_SESSIONS) {
7686
7896
  const toRemove = remaining - MAX_SESSIONS;
7687
- const alive = withStats.filter((e) => existsSync15(e.path));
7897
+ const alive = withStats.filter((e) => existsSync16(e.path));
7688
7898
  for (let i = 0;i < toRemove && i < alive.length; i++) {
7689
7899
  try {
7690
7900
  unlinkSync3(alive[i].path);
@@ -7699,7 +7909,7 @@ class SessionManager {
7699
7909
  }
7700
7910
  listSessionFiles() {
7701
7911
  try {
7702
- return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join15(this.sessionsDir, f));
7912
+ return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join16(this.sessionsDir, f));
7703
7913
  } catch {
7704
7914
  return [];
7705
7915
  }
@@ -7708,7 +7918,7 @@ class SessionManager {
7708
7918
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
7709
7919
  }
7710
7920
  getSessionPath(sessionId) {
7711
- return join15(this.sessionsDir, `${sessionId}.json`);
7921
+ return join16(this.sessionsDir, `${sessionId}.json`);
7712
7922
  }
7713
7923
  }
7714
7924
  var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
@@ -7787,7 +7997,8 @@ async function runInteractiveRepl(session, sessionManager, options) {
7787
7997
  process.stderr.write(`${dim("Using")} ${dim(provider)} ${dim("sandbox")} ${dim(sandboxName)}
7788
7998
  `);
7789
7999
  } else {
7790
- process.stderr.write(`${yellow("⚠")} ${dim(`No sandbox configured for ${provider}. Run locus sandbox.`)}
8000
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
8001
+ process.stderr.write(`${yellow("⚠")} ${dim(mismatch ?? `No sandbox configured for ${provider}. Run locus sandbox.`)}
7791
8002
  `);
7792
8003
  }
7793
8004
  }
@@ -7831,7 +8042,8 @@ async function runInteractiveRepl(session, sessionManager, options) {
7831
8042
  `);
7832
8043
  } else {
7833
8044
  sandboxRunner = null;
7834
- process.stderr.write(`${yellow("⚠")} ${dim(`No sandbox configured for ${inferredProvider}. Run locus sandbox.`)}
8045
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, model, config.ai.provider);
8046
+ process.stderr.write(`${yellow("⚠")} ${dim(mismatch ?? `No sandbox configured for ${inferredProvider}. Run locus sandbox.`)}
7835
8047
  `);
7836
8048
  }
7837
8049
  }
@@ -8136,7 +8348,8 @@ async function handleJsonStream(projectRoot, config, args, sessionId) {
8136
8348
  const sandboxName = getProviderSandboxName(config.sandbox, config.ai.provider);
8137
8349
  const runner = config.sandbox.enabled ? sandboxName ? createUserManagedSandboxRunner(config.ai.provider, sandboxName) : null : await createRunnerAsync(config.ai.provider, false);
8138
8350
  if (!runner) {
8139
- stream.emitError(`Sandbox for provider "${config.ai.provider}" is not configured. Run locus sandbox.`, false);
8351
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
8352
+ stream.emitError(mismatch ?? `No sandbox configured for "${config.ai.provider}". Run "locus sandbox" to create one.`, false);
8140
8353
  return;
8141
8354
  }
8142
8355
  const available = await runner.isAvailable();
@@ -8185,8 +8398,167 @@ var init_exec = __esm(() => {
8185
8398
  init_session_manager();
8186
8399
  });
8187
8400
 
8188
- // src/core/agent.ts
8401
+ // src/core/submodule.ts
8189
8402
  import { execSync as execSync10 } from "node:child_process";
8403
+ import { existsSync as existsSync17 } from "node:fs";
8404
+ import { join as join17 } from "node:path";
8405
+ function git2(args, cwd) {
8406
+ return execSync10(`git ${args}`, {
8407
+ cwd,
8408
+ encoding: "utf-8",
8409
+ stdio: ["pipe", "pipe", "pipe"]
8410
+ });
8411
+ }
8412
+ function gitSafe(args, cwd) {
8413
+ try {
8414
+ return git2(args, cwd);
8415
+ } catch {
8416
+ return null;
8417
+ }
8418
+ }
8419
+ function hasSubmodules(cwd) {
8420
+ return existsSync17(join17(cwd, ".gitmodules"));
8421
+ }
8422
+ function listSubmodules(cwd) {
8423
+ if (!hasSubmodules(cwd))
8424
+ return [];
8425
+ const log = getLogger();
8426
+ const submodules = [];
8427
+ try {
8428
+ const output = git2("submodule status", cwd);
8429
+ for (const line of output.trim().split(`
8430
+ `)) {
8431
+ if (!line.trim())
8432
+ continue;
8433
+ const dirty = line.startsWith("+");
8434
+ const parts = line.trim().replace(/^[+-]/, "").split(/\s+/);
8435
+ const path = parts[1];
8436
+ if (!path)
8437
+ continue;
8438
+ submodules.push({
8439
+ path,
8440
+ absolutePath: join17(cwd, path),
8441
+ dirty
8442
+ });
8443
+ }
8444
+ } catch (e) {
8445
+ log.warn(`Failed to list submodules: ${e}`);
8446
+ }
8447
+ return submodules;
8448
+ }
8449
+ function getDirtySubmodules(cwd) {
8450
+ const submodules = listSubmodules(cwd);
8451
+ const dirty = [];
8452
+ for (const sub of submodules) {
8453
+ if (!existsSync17(sub.absolutePath))
8454
+ continue;
8455
+ const status = gitSafe("status --porcelain", sub.absolutePath);
8456
+ if (status && status.trim().length > 0) {
8457
+ dirty.push({ ...sub, dirty: true });
8458
+ }
8459
+ }
8460
+ return dirty;
8461
+ }
8462
+ function commitDirtySubmodules(cwd, issueNumber, issueTitle) {
8463
+ const log = getLogger();
8464
+ const dirtySubmodules = getDirtySubmodules(cwd);
8465
+ if (dirtySubmodules.length === 0)
8466
+ return [];
8467
+ const committed = [];
8468
+ for (const sub of dirtySubmodules) {
8469
+ try {
8470
+ git2("add -A", sub.absolutePath);
8471
+ const message = `chore: complete #${issueNumber} - ${issueTitle}
8472
+
8473
+ Co-Authored-By: LocusAgent <agent@locusai.team>`;
8474
+ execSync10("git commit -F -", {
8475
+ input: message,
8476
+ cwd: sub.absolutePath,
8477
+ encoding: "utf-8",
8478
+ stdio: ["pipe", "pipe", "pipe"]
8479
+ });
8480
+ committed.push(sub.path);
8481
+ log.info(`Committed submodule changes: ${sub.path} for #${issueNumber}`);
8482
+ } catch {
8483
+ log.verbose(`No committable changes in submodule ${sub.path}`);
8484
+ }
8485
+ }
8486
+ if (committed.length > 0) {
8487
+ for (const subPath of committed) {
8488
+ gitSafe(`add ${subPath}`, cwd);
8489
+ }
8490
+ }
8491
+ return committed;
8492
+ }
8493
+ function initSubmodules(cwd) {
8494
+ if (!hasSubmodules(cwd))
8495
+ return;
8496
+ const log = getLogger();
8497
+ try {
8498
+ git2("submodule update --init --recursive", cwd);
8499
+ log.info("Initialized submodules");
8500
+ } catch (e) {
8501
+ log.warn(`Failed to initialize submodules: ${e}`);
8502
+ }
8503
+ }
8504
+ function updateSubmodulesAfterRebase(cwd) {
8505
+ if (!hasSubmodules(cwd))
8506
+ return;
8507
+ const log = getLogger();
8508
+ try {
8509
+ git2("submodule update --recursive", cwd);
8510
+ log.info("Updated submodules after rebase");
8511
+ } catch (e) {
8512
+ log.warn(`Failed to update submodules after rebase: ${e}`);
8513
+ }
8514
+ }
8515
+ function getSubmoduleChangeSummary(cwd, baseBranch) {
8516
+ if (!hasSubmodules(cwd))
8517
+ return null;
8518
+ const diff = gitSafe(`diff origin/${baseBranch}..HEAD --submodule=short`, cwd);
8519
+ if (!diff || !diff.trim())
8520
+ return null;
8521
+ const submoduleChanges = [];
8522
+ for (const line of diff.split(`
8523
+ `)) {
8524
+ if (line.startsWith("Submodule ")) {
8525
+ submoduleChanges.push(line.trim());
8526
+ }
8527
+ }
8528
+ if (submoduleChanges.length === 0)
8529
+ return null;
8530
+ return `### Submodule Changes
8531
+ ${submoduleChanges.map((c) => `- ${c}`).join(`
8532
+ `)}`;
8533
+ }
8534
+ function pushSubmoduleBranches(cwd) {
8535
+ if (!hasSubmodules(cwd))
8536
+ return;
8537
+ const log = getLogger();
8538
+ const submodules = listSubmodules(cwd);
8539
+ for (const sub of submodules) {
8540
+ if (!existsSync17(sub.absolutePath))
8541
+ continue;
8542
+ const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
8543
+ if (!branch || branch === "HEAD")
8544
+ continue;
8545
+ const unpushed = gitSafe(`log origin/${branch}..HEAD --oneline`, sub.absolutePath)?.trim();
8546
+ if (unpushed) {
8547
+ try {
8548
+ git2(`push origin ${branch}`, sub.absolutePath);
8549
+ log.info(`Pushed submodule ${sub.path} branch ${branch}`);
8550
+ } catch (e) {
8551
+ log.warn(`Failed to push submodule ${sub.path}: ${e}`);
8552
+ }
8553
+ }
8554
+ }
8555
+ }
8556
+ var init_submodule = __esm(() => {
8557
+ init_logger();
8558
+ });
8559
+
8560
+ // src/core/agent.ts
8561
+ import { execSync as execSync11 } from "node:child_process";
8190
8562
  async function executeIssue(projectRoot, options) {
8191
8563
  const log = getLogger();
8192
8564
  const timer = createTimer();
@@ -8215,7 +8587,7 @@ ${cyan("●")} ${bold(`#${issueNumber}`)} ${issue.title}
8215
8587
  }
8216
8588
  let issueComments = [];
8217
8589
  try {
8218
- const commentsRaw = execSync10(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8590
+ const commentsRaw = execSync11(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8219
8591
  if (commentsRaw) {
8220
8592
  issueComments = commentsRaw.split(`
8221
8593
  `).filter(Boolean);
@@ -8379,12 +8751,12 @@ ${aiResult.success ? green("✓") : red("✗")} Iteration ${aiResult.success ? "
8379
8751
  }
8380
8752
  async function createIssuePR(projectRoot, config, issue) {
8381
8753
  try {
8382
- const currentBranch = execSync10("git rev-parse --abbrev-ref HEAD", {
8754
+ const currentBranch = execSync11("git rev-parse --abbrev-ref HEAD", {
8383
8755
  cwd: projectRoot,
8384
8756
  encoding: "utf-8",
8385
8757
  stdio: ["pipe", "pipe", "pipe"]
8386
8758
  }).trim();
8387
- const diff = execSync10(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
8759
+ const diff = execSync11(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
8388
8760
  cwd: projectRoot,
8389
8761
  encoding: "utf-8",
8390
8762
  stdio: ["pipe", "pipe", "pipe"]
@@ -8393,17 +8765,25 @@ async function createIssuePR(projectRoot, config, issue) {
8393
8765
  getLogger().verbose("No changes to create PR for");
8394
8766
  return;
8395
8767
  }
8396
- execSync10(`git push -u origin ${currentBranch}`, {
8768
+ pushSubmoduleBranches(projectRoot);
8769
+ execSync11(`git push -u origin ${currentBranch}`, {
8397
8770
  cwd: projectRoot,
8398
8771
  encoding: "utf-8",
8399
8772
  stdio: ["pipe", "pipe", "pipe"]
8400
8773
  });
8401
- const prTitle = `${issue.title} (#${issue.number})`;
8402
- const prBody = `Closes #${issue.number}
8774
+ const submoduleSummary = getSubmoduleChangeSummary(projectRoot, config.agent.baseBranch);
8775
+ let prBody = `Closes #${issue.number}`;
8776
+ if (submoduleSummary) {
8777
+ prBody += `
8778
+
8779
+ ${submoduleSummary}`;
8780
+ }
8781
+ prBody += `
8403
8782
 
8404
8783
  ---
8405
8784
 
8406
8785
  \uD83E\uDD16 Automated by [Locus](https://github.com/locusai/locus)`;
8786
+ const prTitle = `${issue.title} (#${issue.number})`;
8407
8787
  const prNumber = createPR(prTitle, prBody, currentBranch, config.agent.baseBranch, { cwd: projectRoot });
8408
8788
  process.stderr.write(` ${green("✓")} Created PR #${prNumber}
8409
8789
  `);
@@ -8437,20 +8817,21 @@ var init_agent = __esm(() => {
8437
8817
  init_logger();
8438
8818
  init_prompt_builder();
8439
8819
  init_sandbox();
8820
+ init_submodule();
8440
8821
  });
8441
8822
 
8442
8823
  // src/core/conflict.ts
8443
- import { execSync as execSync11 } from "node:child_process";
8444
- function git2(args, cwd) {
8445
- return execSync11(`git ${args}`, {
8824
+ import { execSync as execSync12 } from "node:child_process";
8825
+ function git3(args, cwd) {
8826
+ return execSync12(`git ${args}`, {
8446
8827
  cwd,
8447
8828
  encoding: "utf-8",
8448
8829
  stdio: ["pipe", "pipe", "pipe"]
8449
8830
  });
8450
8831
  }
8451
- function gitSafe(args, cwd) {
8832
+ function gitSafe2(args, cwd) {
8452
8833
  try {
8453
- return git2(args, cwd);
8834
+ return git3(args, cwd);
8454
8835
  } catch {
8455
8836
  return null;
8456
8837
  }
@@ -8458,7 +8839,7 @@ function gitSafe(args, cwd) {
8458
8839
  function checkForConflicts(cwd, baseBranch) {
8459
8840
  const log = getLogger();
8460
8841
  try {
8461
- git2(`fetch origin ${baseBranch}`, cwd);
8842
+ git3(`fetch origin ${baseBranch}`, cwd);
8462
8843
  log.debug("Fetched latest from origin", { baseBranch });
8463
8844
  } catch (e) {
8464
8845
  log.warn(`Could not fetch origin/${baseBranch}: ${e}`);
@@ -8469,8 +8850,8 @@ function checkForConflicts(cwd, baseBranch) {
8469
8850
  newCommits: 0
8470
8851
  };
8471
8852
  }
8472
- const currentBranch = gitSafe("rev-parse --abbrev-ref HEAD", cwd)?.trim() ?? "";
8473
- const mergeBase = gitSafe(`merge-base ${currentBranch} origin/${baseBranch}`, cwd)?.trim();
8853
+ const currentBranch = gitSafe2("rev-parse --abbrev-ref HEAD", cwd)?.trim() ?? "";
8854
+ const mergeBase = gitSafe2(`merge-base ${currentBranch} origin/${baseBranch}`, cwd)?.trim();
8474
8855
  if (!mergeBase) {
8475
8856
  log.debug("Could not find merge base — branches may be unrelated");
8476
8857
  return {
@@ -8480,7 +8861,7 @@ function checkForConflicts(cwd, baseBranch) {
8480
8861
  newCommits: 0
8481
8862
  };
8482
8863
  }
8483
- const remoteTip = gitSafe(`rev-parse origin/${baseBranch}`, cwd)?.trim();
8864
+ const remoteTip = gitSafe2(`rev-parse origin/${baseBranch}`, cwd)?.trim();
8484
8865
  if (!remoteTip || remoteTip === mergeBase) {
8485
8866
  log.debug("Base branch has not advanced");
8486
8867
  return {
@@ -8490,16 +8871,16 @@ function checkForConflicts(cwd, baseBranch) {
8490
8871
  newCommits: 0
8491
8872
  };
8492
8873
  }
8493
- const newCommitsOutput = gitSafe(`rev-list --count ${mergeBase}..origin/${baseBranch}`, cwd)?.trim() ?? "0";
8874
+ const newCommitsOutput = gitSafe2(`rev-list --count ${mergeBase}..origin/${baseBranch}`, cwd)?.trim() ?? "0";
8494
8875
  const newCommits = Number.parseInt(newCommitsOutput, 10);
8495
8876
  log.verbose(`Base branch has ${newCommits} new commits`, {
8496
8877
  baseBranch,
8497
8878
  mergeBase: mergeBase.slice(0, 8),
8498
8879
  remoteTip: remoteTip.slice(0, 8)
8499
8880
  });
8500
- const ourChanges = gitSafe(`diff --name-only ${mergeBase}..HEAD`, cwd)?.trim().split(`
8881
+ const ourChanges = gitSafe2(`diff --name-only ${mergeBase}..HEAD`, cwd)?.trim().split(`
8501
8882
  `).filter(Boolean) ?? [];
8502
- const theirChanges = gitSafe(`diff --name-only ${mergeBase}..origin/${baseBranch}`, cwd)?.trim().split(`
8883
+ const theirChanges = gitSafe2(`diff --name-only ${mergeBase}..origin/${baseBranch}`, cwd)?.trim().split(`
8503
8884
  `).filter(Boolean) ?? [];
8504
8885
  const overlapping = ourChanges.filter((f) => theirChanges.includes(f));
8505
8886
  return {
@@ -8512,18 +8893,19 @@ function checkForConflicts(cwd, baseBranch) {
8512
8893
  function attemptRebase(cwd, baseBranch) {
8513
8894
  const log = getLogger();
8514
8895
  try {
8515
- git2(`rebase origin/${baseBranch}`, cwd);
8896
+ git3(`rebase origin/${baseBranch}`, cwd);
8516
8897
  log.info(`Successfully rebased onto origin/${baseBranch}`);
8898
+ updateSubmodulesAfterRebase(cwd);
8517
8899
  return { success: true };
8518
8900
  } catch (_e) {
8519
8901
  log.warn("Rebase failed, aborting");
8520
8902
  const conflicts = [];
8521
8903
  try {
8522
- const status = git2("diff --name-only --diff-filter=U", cwd);
8904
+ const status = git3("diff --name-only --diff-filter=U", cwd);
8523
8905
  conflicts.push(...status.trim().split(`
8524
8906
  `).filter(Boolean));
8525
8907
  } catch {}
8526
- gitSafe("rebase --abort", cwd);
8908
+ gitSafe2("rebase --abort", cwd);
8527
8909
  return { success: false, conflicts };
8528
8910
  }
8529
8911
  }
@@ -8565,23 +8947,24 @@ ${bold(yellow("⚠"))} Base branch has ${result.newCommits} new commit${result.n
8565
8947
  var init_conflict = __esm(() => {
8566
8948
  init_terminal();
8567
8949
  init_logger();
8950
+ init_submodule();
8568
8951
  });
8569
8952
 
8570
8953
  // src/core/run-state.ts
8571
8954
  import {
8572
- existsSync as existsSync16,
8955
+ existsSync as existsSync18,
8573
8956
  mkdirSync as mkdirSync11,
8574
8957
  readFileSync as readFileSync12,
8575
8958
  unlinkSync as unlinkSync4,
8576
8959
  writeFileSync as writeFileSync8
8577
8960
  } from "node:fs";
8578
- import { dirname as dirname5, join as join16 } from "node:path";
8961
+ import { dirname as dirname5, join as join18 } from "node:path";
8579
8962
  function getRunStatePath(projectRoot) {
8580
- return join16(projectRoot, ".locus", "run-state.json");
8963
+ return join18(projectRoot, ".locus", "run-state.json");
8581
8964
  }
8582
8965
  function loadRunState(projectRoot) {
8583
8966
  const path = getRunStatePath(projectRoot);
8584
- if (!existsSync16(path))
8967
+ if (!existsSync18(path))
8585
8968
  return null;
8586
8969
  try {
8587
8970
  return JSON.parse(readFileSync12(path, "utf-8"));
@@ -8593,7 +8976,7 @@ function loadRunState(projectRoot) {
8593
8976
  function saveRunState(projectRoot, state) {
8594
8977
  const path = getRunStatePath(projectRoot);
8595
8978
  const dir = dirname5(path);
8596
- if (!existsSync16(dir)) {
8979
+ if (!existsSync18(dir)) {
8597
8980
  mkdirSync11(dir, { recursive: true });
8598
8981
  }
8599
8982
  writeFileSync8(path, `${JSON.stringify(state, null, 2)}
@@ -8601,7 +8984,7 @@ function saveRunState(projectRoot, state) {
8601
8984
  }
8602
8985
  function clearRunState(projectRoot) {
8603
8986
  const path = getRunStatePath(projectRoot);
8604
- if (existsSync16(path)) {
8987
+ if (existsSync18(path)) {
8605
8988
  unlinkSync4(path);
8606
8989
  }
8607
8990
  }
@@ -8741,28 +9124,28 @@ var init_shutdown = __esm(() => {
8741
9124
  });
8742
9125
 
8743
9126
  // src/core/worktree.ts
8744
- import { execSync as execSync12 } from "node:child_process";
8745
- import { existsSync as existsSync17, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
8746
- import { join as join17 } from "node:path";
8747
- function git3(args, cwd) {
8748
- return execSync12(`git ${args}`, {
9127
+ import { execSync as execSync13 } from "node:child_process";
9128
+ import { existsSync as existsSync19, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
9129
+ import { join as join19 } from "node:path";
9130
+ function git4(args, cwd) {
9131
+ return execSync13(`git ${args}`, {
8749
9132
  cwd,
8750
9133
  encoding: "utf-8",
8751
9134
  stdio: ["pipe", "pipe", "pipe"]
8752
9135
  });
8753
9136
  }
8754
- function gitSafe2(args, cwd) {
9137
+ function gitSafe3(args, cwd) {
8755
9138
  try {
8756
- return git3(args, cwd);
9139
+ return git4(args, cwd);
8757
9140
  } catch {
8758
9141
  return null;
8759
9142
  }
8760
9143
  }
8761
9144
  function getWorktreeDir(projectRoot) {
8762
- return join17(projectRoot, ".locus", "worktrees");
9145
+ return join19(projectRoot, ".locus", "worktrees");
8763
9146
  }
8764
9147
  function getWorktreePath(projectRoot, issueNumber) {
8765
- return join17(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
9148
+ return join19(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
8766
9149
  }
8767
9150
  function generateBranchName(issueNumber) {
8768
9151
  const randomSuffix = Math.random().toString(36).slice(2, 8);
@@ -8770,7 +9153,7 @@ function generateBranchName(issueNumber) {
8770
9153
  }
8771
9154
  function getWorktreeBranch(worktreePath) {
8772
9155
  try {
8773
- return execSync12("git branch --show-current", {
9156
+ return execSync13("git branch --show-current", {
8774
9157
  cwd: worktreePath,
8775
9158
  encoding: "utf-8",
8776
9159
  stdio: ["pipe", "pipe", "pipe"]
@@ -8782,7 +9165,7 @@ function getWorktreeBranch(worktreePath) {
8782
9165
  function createWorktree(projectRoot, issueNumber, baseBranch) {
8783
9166
  const log = getLogger();
8784
9167
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
8785
- if (existsSync17(worktreePath)) {
9168
+ if (existsSync19(worktreePath)) {
8786
9169
  log.verbose(`Worktree already exists for issue #${issueNumber}`);
8787
9170
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
8788
9171
  return {
@@ -8793,7 +9176,8 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
8793
9176
  };
8794
9177
  }
8795
9178
  const branch = generateBranchName(issueNumber);
8796
- git3(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
9179
+ git4(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
9180
+ initSubmodules(worktreePath);
8797
9181
  log.info(`Created worktree for issue #${issueNumber}`, {
8798
9182
  path: worktreePath,
8799
9183
  branch,
@@ -8809,30 +9193,30 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
8809
9193
  function removeWorktree(projectRoot, issueNumber) {
8810
9194
  const log = getLogger();
8811
9195
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
8812
- if (!existsSync17(worktreePath)) {
9196
+ if (!existsSync19(worktreePath)) {
8813
9197
  log.verbose(`Worktree for issue #${issueNumber} does not exist`);
8814
9198
  return;
8815
9199
  }
8816
9200
  const branch = getWorktreeBranch(worktreePath);
8817
9201
  try {
8818
- git3(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
9202
+ git4(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
8819
9203
  log.info(`Removed worktree for issue #${issueNumber}`);
8820
9204
  } catch (e) {
8821
9205
  log.warn(`Failed to remove worktree: ${e}`);
8822
- gitSafe2(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
9206
+ gitSafe3(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
8823
9207
  }
8824
9208
  if (branch) {
8825
- gitSafe2(`branch -D ${branch}`, projectRoot);
9209
+ gitSafe3(`branch -D ${branch}`, projectRoot);
8826
9210
  }
8827
9211
  }
8828
9212
  function listWorktrees(projectRoot) {
8829
9213
  const log = getLogger();
8830
9214
  const worktreeDir = getWorktreeDir(projectRoot);
8831
- if (!existsSync17(worktreeDir)) {
9215
+ if (!existsSync19(worktreeDir)) {
8832
9216
  return [];
8833
9217
  }
8834
9218
  const entries = readdirSync6(worktreeDir).filter((entry) => entry.startsWith("issue-"));
8835
- const gitWorktreeList = gitSafe2("worktree list --porcelain", projectRoot);
9219
+ const gitWorktreeList = gitSafe3("worktree list --porcelain", projectRoot);
8836
9220
  const activeWorktrees = new Set;
8837
9221
  if (gitWorktreeList) {
8838
9222
  for (const line of gitWorktreeList.split(`
@@ -8848,7 +9232,7 @@ function listWorktrees(projectRoot) {
8848
9232
  if (!match)
8849
9233
  continue;
8850
9234
  const issueNumber = Number.parseInt(match[1], 10);
8851
- const path = join17(worktreeDir, entry);
9235
+ const path = join19(worktreeDir, entry);
8852
9236
  const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
8853
9237
  let resolvedPath;
8854
9238
  try {
@@ -8882,12 +9266,13 @@ function cleanupStaleWorktrees(projectRoot) {
8882
9266
  }
8883
9267
  }
8884
9268
  if (cleaned > 0) {
8885
- gitSafe2("worktree prune", projectRoot);
9269
+ gitSafe3("worktree prune", projectRoot);
8886
9270
  }
8887
9271
  return cleaned;
8888
9272
  }
8889
9273
  var init_worktree = __esm(() => {
8890
9274
  init_logger();
9275
+ init_submodule();
8891
9276
  });
8892
9277
 
8893
9278
  // src/commands/run.ts
@@ -8895,7 +9280,7 @@ var exports_run = {};
8895
9280
  __export(exports_run, {
8896
9281
  runCommand: () => runCommand
8897
9282
  });
8898
- import { execSync as execSync13 } from "node:child_process";
9283
+ import { execSync as execSync14 } from "node:child_process";
8899
9284
  function resolveExecutionContext(config, modelOverride) {
8900
9285
  const model = modelOverride ?? config.ai.model;
8901
9286
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
@@ -8967,6 +9352,15 @@ async function runCommand(projectRoot, args, flags = {}) {
8967
9352
  process.stderr.write(`${yellow("⚠")} Running without sandbox. The AI agent will have unrestricted access to your filesystem, network, and environment variables.
8968
9353
  `);
8969
9354
  }
9355
+ if (sandboxed) {
9356
+ const model = flags.model ?? config.ai.model;
9357
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, model, config.ai.provider);
9358
+ if (mismatch) {
9359
+ process.stderr.write(`${red("✗")} ${mismatch}
9360
+ `);
9361
+ return;
9362
+ }
9363
+ }
8970
9364
  if (flags.resume) {
8971
9365
  return handleResume(projectRoot, config, sandboxed);
8972
9366
  }
@@ -9046,7 +9440,7 @@ ${yellow("⚠")} A sprint run is already in progress.
9046
9440
  }
9047
9441
  if (!flags.dryRun) {
9048
9442
  try {
9049
- execSync13(`git checkout -B ${branchName}`, {
9443
+ execSync14(`git checkout -B ${branchName}`, {
9050
9444
  cwd: projectRoot,
9051
9445
  encoding: "utf-8",
9052
9446
  stdio: ["pipe", "pipe", "pipe"]
@@ -9096,7 +9490,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
9096
9490
  let sprintContext;
9097
9491
  if (i > 0 && !flags.dryRun) {
9098
9492
  try {
9099
- sprintContext = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9493
+ sprintContext = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9100
9494
  cwd: projectRoot,
9101
9495
  encoding: "utf-8",
9102
9496
  stdio: ["pipe", "pipe", "pipe"]
@@ -9161,7 +9555,7 @@ ${bold("Summary:")}
9161
9555
  const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
9162
9556
  if (prNumber !== undefined) {
9163
9557
  try {
9164
- execSync13(`git checkout ${config.agent.baseBranch}`, {
9558
+ execSync14(`git checkout ${config.agent.baseBranch}`, {
9165
9559
  cwd: projectRoot,
9166
9560
  encoding: "utf-8",
9167
9561
  stdio: ["pipe", "pipe", "pipe"]
@@ -9176,6 +9570,7 @@ ${bold("Summary:")}
9176
9570
  }
9177
9571
  }
9178
9572
  async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandboxed) {
9573
+ const log = getLogger();
9179
9574
  const execution = resolveExecutionContext(config, flags.model);
9180
9575
  let isSprintIssue = false;
9181
9576
  try {
@@ -9184,7 +9579,7 @@ async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandbo
9184
9579
  } catch {}
9185
9580
  if (isSprintIssue) {
9186
9581
  process.stderr.write(`
9187
- ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential, no worktree)")}
9582
+ ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential)")}
9188
9583
 
9189
9584
  `);
9190
9585
  await executeIssue(projectRoot, {
@@ -9197,42 +9592,48 @@ ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential, n
9197
9592
  });
9198
9593
  return;
9199
9594
  }
9595
+ const randomSuffix = Math.random().toString(36).slice(2, 8);
9596
+ const branchName = `locus/issue-${issueNumber}-${randomSuffix}`;
9200
9597
  process.stderr.write(`
9201
- ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
9598
+ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim(`(branch: ${branchName})`)}
9202
9599
 
9203
9600
  `);
9204
- let worktreePath;
9205
9601
  if (!flags.dryRun) {
9206
9602
  try {
9207
- const wt = createWorktree(projectRoot, issueNumber, config.agent.baseBranch);
9208
- worktreePath = wt.path;
9209
- process.stderr.write(` ${dim(`Worktree: ${wt.branch}`)}
9210
-
9211
- `);
9603
+ execSync14(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
9604
+ cwd: projectRoot,
9605
+ encoding: "utf-8",
9606
+ stdio: ["pipe", "pipe", "pipe"]
9607
+ });
9608
+ log.info(`Checked out branch ${branchName}`);
9212
9609
  } catch (e) {
9213
- process.stderr.write(`${yellow("⚠")} Could not create worktree: ${e}
9610
+ process.stderr.write(`${yellow("⚠")} Could not create branch: ${e}
9214
9611
  `);
9215
- process.stderr.write(` ${dim("Falling back to running in project root.")}
9612
+ process.stderr.write(` ${dim("Running on current branch instead.")}
9216
9613
 
9217
9614
  `);
9218
9615
  }
9219
9616
  }
9220
9617
  const result = await executeIssue(projectRoot, {
9221
9618
  issueNumber,
9222
- worktreePath,
9223
9619
  provider: execution.provider,
9224
9620
  model: execution.model,
9225
9621
  dryRun: flags.dryRun,
9226
9622
  sandboxed,
9227
9623
  sandboxName: execution.sandboxName
9228
9624
  });
9229
- if (worktreePath && !flags.dryRun) {
9625
+ if (!flags.dryRun) {
9230
9626
  if (result.success) {
9231
- removeWorktree(projectRoot, issueNumber);
9232
- process.stderr.write(` ${dim("Worktree cleaned up.")}
9233
- `);
9627
+ try {
9628
+ execSync14(`git checkout ${config.agent.baseBranch}`, {
9629
+ cwd: projectRoot,
9630
+ encoding: "utf-8",
9631
+ stdio: ["pipe", "pipe", "pipe"]
9632
+ });
9633
+ log.info(`Checked out ${config.agent.baseBranch}`);
9634
+ } catch {}
9234
9635
  } else {
9235
- process.stderr.write(` ${yellow("⚠")} Worktree preserved for debugging: ${dim(worktreePath)}
9636
+ process.stderr.write(` ${yellow("⚠")} Branch ${dim(branchName)} preserved for debugging.
9236
9637
  `);
9237
9638
  }
9238
9639
  }
@@ -9361,13 +9762,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
9361
9762
  `);
9362
9763
  if (state.type === "sprint" && state.branch) {
9363
9764
  try {
9364
- const currentBranch = execSync13("git rev-parse --abbrev-ref HEAD", {
9765
+ const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
9365
9766
  cwd: projectRoot,
9366
9767
  encoding: "utf-8",
9367
9768
  stdio: ["pipe", "pipe", "pipe"]
9368
9769
  }).trim();
9369
9770
  if (currentBranch !== state.branch) {
9370
- execSync13(`git checkout ${state.branch}`, {
9771
+ execSync14(`git checkout ${state.branch}`, {
9371
9772
  cwd: projectRoot,
9372
9773
  encoding: "utf-8",
9373
9774
  stdio: ["pipe", "pipe", "pipe"]
@@ -9434,7 +9835,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
9434
9835
  const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
9435
9836
  if (prNumber !== undefined) {
9436
9837
  try {
9437
- execSync13(`git checkout ${config.agent.baseBranch}`, {
9838
+ execSync14(`git checkout ${config.agent.baseBranch}`, {
9438
9839
  cwd: projectRoot,
9439
9840
  encoding: "utf-8",
9440
9841
  stdio: ["pipe", "pipe", "pipe"]
@@ -9465,14 +9866,19 @@ function getOrder2(issue) {
9465
9866
  }
9466
9867
  function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
9467
9868
  try {
9468
- const status = execSync13("git status --porcelain", {
9869
+ const committedSubmodules = commitDirtySubmodules(projectRoot, issueNumber, issueTitle);
9870
+ if (committedSubmodules.length > 0) {
9871
+ process.stderr.write(` ${dim(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
9872
+ `);
9873
+ }
9874
+ const status = execSync14("git status --porcelain", {
9469
9875
  cwd: projectRoot,
9470
9876
  encoding: "utf-8",
9471
9877
  stdio: ["pipe", "pipe", "pipe"]
9472
9878
  }).trim();
9473
9879
  if (!status)
9474
9880
  return;
9475
- execSync13("git add -A", {
9881
+ execSync14("git add -A", {
9476
9882
  cwd: projectRoot,
9477
9883
  encoding: "utf-8",
9478
9884
  stdio: ["pipe", "pipe", "pipe"]
@@ -9480,7 +9886,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
9480
9886
  const message = `chore: complete #${issueNumber} - ${issueTitle}
9481
9887
 
9482
9888
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
9483
- execSync13(`git commit -F -`, {
9889
+ execSync14(`git commit -F -`, {
9484
9890
  input: message,
9485
9891
  cwd: projectRoot,
9486
9892
  encoding: "utf-8",
@@ -9494,7 +9900,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
9494
9900
  if (!config.agent.autoPR)
9495
9901
  return;
9496
9902
  try {
9497
- const diff = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9903
+ const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9498
9904
  cwd: projectRoot,
9499
9905
  encoding: "utf-8",
9500
9906
  stdio: ["pipe", "pipe", "pipe"]
@@ -9504,16 +9910,24 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
9504
9910
  `);
9505
9911
  return;
9506
9912
  }
9507
- execSync13(`git push -u origin ${branchName}`, {
9913
+ pushSubmoduleBranches(projectRoot);
9914
+ execSync14(`git push -u origin ${branchName}`, {
9508
9915
  cwd: projectRoot,
9509
9916
  encoding: "utf-8",
9510
9917
  stdio: ["pipe", "pipe", "pipe"]
9511
9918
  });
9512
9919
  const taskLines = tasks.map((t) => `- Closes #${t.issue}${t.title ? `: ${t.title}` : ""}`).join(`
9513
9920
  `);
9514
- const prBody = `## Sprint: ${sprintName}
9921
+ const submoduleSummary = getSubmoduleChangeSummary(projectRoot, config.agent.baseBranch);
9922
+ let prBody = `## Sprint: ${sprintName}
9923
+
9924
+ ${taskLines}`;
9925
+ if (submoduleSummary) {
9926
+ prBody += `
9515
9927
 
9516
- ${taskLines}
9928
+ ${submoduleSummary}`;
9929
+ }
9930
+ prBody += `
9517
9931
 
9518
9932
  ---
9519
9933
 
@@ -9530,8 +9944,8 @@ ${taskLines}
9530
9944
  }
9531
9945
  }
9532
9946
  var init_run = __esm(() => {
9533
- init_ai_models();
9534
9947
  init_agent();
9948
+ init_ai_models();
9535
9949
  init_config();
9536
9950
  init_conflict();
9537
9951
  init_github();
@@ -9540,6 +9954,7 @@ var init_run = __esm(() => {
9540
9954
  init_run_state();
9541
9955
  init_sandbox();
9542
9956
  init_shutdown();
9957
+ init_submodule();
9543
9958
  init_worktree();
9544
9959
  init_progress();
9545
9960
  init_terminal();
@@ -9654,13 +10069,13 @@ __export(exports_plan, {
9654
10069
  parsePlanArgs: () => parsePlanArgs
9655
10070
  });
9656
10071
  import {
9657
- existsSync as existsSync18,
10072
+ existsSync as existsSync20,
9658
10073
  mkdirSync as mkdirSync12,
9659
10074
  readdirSync as readdirSync7,
9660
10075
  readFileSync as readFileSync13,
9661
10076
  writeFileSync as writeFileSync9
9662
10077
  } from "node:fs";
9663
- import { join as join18 } from "node:path";
10078
+ import { join as join20 } from "node:path";
9664
10079
  function printHelp() {
9665
10080
  process.stderr.write(`
9666
10081
  ${bold("locus plan")} — AI-powered sprint planning
@@ -9691,11 +10106,11 @@ function normalizeSprintName(name) {
9691
10106
  return name.trim().toLowerCase();
9692
10107
  }
9693
10108
  function getPlansDir(projectRoot) {
9694
- return join18(projectRoot, ".locus", "plans");
10109
+ return join20(projectRoot, ".locus", "plans");
9695
10110
  }
9696
10111
  function ensurePlansDir(projectRoot) {
9697
10112
  const dir = getPlansDir(projectRoot);
9698
- if (!existsSync18(dir)) {
10113
+ if (!existsSync20(dir)) {
9699
10114
  mkdirSync12(dir, { recursive: true });
9700
10115
  }
9701
10116
  return dir;
@@ -9705,14 +10120,14 @@ function generateId() {
9705
10120
  }
9706
10121
  function loadPlanFile(projectRoot, id) {
9707
10122
  const dir = getPlansDir(projectRoot);
9708
- if (!existsSync18(dir))
10123
+ if (!existsSync20(dir))
9709
10124
  return null;
9710
10125
  const files = readdirSync7(dir).filter((f) => f.endsWith(".json"));
9711
10126
  const match = files.find((f) => f.startsWith(id));
9712
10127
  if (!match)
9713
10128
  return null;
9714
10129
  try {
9715
- const content = readFileSync13(join18(dir, match), "utf-8");
10130
+ const content = readFileSync13(join20(dir, match), "utf-8");
9716
10131
  return JSON.parse(content);
9717
10132
  } catch {
9718
10133
  return null;
@@ -9758,7 +10173,7 @@ async function planCommand(projectRoot, args, flags = {}) {
9758
10173
  }
9759
10174
  function handleListPlans(projectRoot) {
9760
10175
  const dir = getPlansDir(projectRoot);
9761
- if (!existsSync18(dir)) {
10176
+ if (!existsSync20(dir)) {
9762
10177
  process.stderr.write(`${dim("No saved plans yet.")}
9763
10178
  `);
9764
10179
  return;
@@ -9776,7 +10191,7 @@ ${bold("Saved Plans:")}
9776
10191
  for (const file of files) {
9777
10192
  const id = file.replace(".json", "");
9778
10193
  try {
9779
- const content = readFileSync13(join18(dir, file), "utf-8");
10194
+ const content = readFileSync13(join20(dir, file), "utf-8");
9780
10195
  const plan = JSON.parse(content);
9781
10196
  const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
9782
10197
  const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
@@ -9887,7 +10302,7 @@ ${bold("Approving plan:")}
9887
10302
  async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
9888
10303
  const id = generateId();
9889
10304
  const plansDir = ensurePlansDir(projectRoot);
9890
- const planPath = join18(plansDir, `${id}.json`);
10305
+ const planPath = join20(plansDir, `${id}.json`);
9891
10306
  const planPathRelative = `.locus/plans/${id}.json`;
9892
10307
  const displayDirective = directive;
9893
10308
  process.stderr.write(`
@@ -9899,6 +10314,14 @@ ${bold("Planning:")} ${cyan(displayDirective)}
9899
10314
  }
9900
10315
  process.stderr.write(`
9901
10316
  `);
10317
+ if (config.sandbox.enabled) {
10318
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
10319
+ if (mismatch) {
10320
+ process.stderr.write(`${red("✗")} ${mismatch}
10321
+ `);
10322
+ return;
10323
+ }
10324
+ }
9902
10325
  const prompt = buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative);
9903
10326
  const aiResult = await runAI({
9904
10327
  prompt,
@@ -9921,7 +10344,7 @@ ${red("✗")} Planning failed: ${aiResult.error}
9921
10344
  `);
9922
10345
  return;
9923
10346
  }
9924
- if (!existsSync18(planPath)) {
10347
+ if (!existsSync20(planPath)) {
9925
10348
  process.stderr.write(`
9926
10349
  ${yellow("⚠")} Plan file was not created at ${bold(planPathRelative)}.
9927
10350
  `);
@@ -9994,6 +10417,14 @@ ${bold("Organizing issues for:")} ${cyan(sprintName)}
9994
10417
  `);
9995
10418
  return;
9996
10419
  }
10420
+ if (config.sandbox.enabled) {
10421
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
10422
+ if (mismatch) {
10423
+ process.stderr.write(`${red("✗")} ${mismatch}
10424
+ `);
10425
+ return;
10426
+ }
10427
+ }
9997
10428
  const issueDescriptions = issues.map((i) => `#${i.number}: ${i.title}
9998
10429
  ${i.body?.slice(0, 300) ?? ""}`).join(`
9999
10430
 
@@ -10094,15 +10525,15 @@ ${directive}${sprintName ? `
10094
10525
 
10095
10526
  **Sprint:** ${sprintName}` : ""}
10096
10527
  </directive>`);
10097
- const locusPath = join18(projectRoot, ".locus", "LOCUS.md");
10098
- if (existsSync18(locusPath)) {
10528
+ const locusPath = join20(projectRoot, ".locus", "LOCUS.md");
10529
+ if (existsSync20(locusPath)) {
10099
10530
  const content = readFileSync13(locusPath, "utf-8");
10100
10531
  parts.push(`<project-context>
10101
10532
  ${content.slice(0, 3000)}
10102
10533
  </project-context>`);
10103
10534
  }
10104
- const learningsPath = join18(projectRoot, ".locus", "LEARNINGS.md");
10105
- if (existsSync18(learningsPath)) {
10535
+ const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
10536
+ if (existsSync20(learningsPath)) {
10106
10537
  const content = readFileSync13(learningsPath, "utf-8");
10107
10538
  parts.push(`<past-learnings>
10108
10539
  ${content.slice(0, 2000)}
@@ -10273,8 +10704,8 @@ var init_plan = __esm(() => {
10273
10704
  init_run_ai();
10274
10705
  init_config();
10275
10706
  init_github();
10276
- init_terminal();
10277
10707
  init_sandbox();
10708
+ init_terminal();
10278
10709
  });
10279
10710
 
10280
10711
  // src/commands/review.ts
@@ -10282,9 +10713,9 @@ var exports_review = {};
10282
10713
  __export(exports_review, {
10283
10714
  reviewCommand: () => reviewCommand
10284
10715
  });
10285
- import { execSync as execSync14 } from "node:child_process";
10286
- import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
10287
- import { join as join19 } from "node:path";
10716
+ import { execSync as execSync15 } from "node:child_process";
10717
+ import { existsSync as existsSync21, readFileSync as readFileSync14 } from "node:fs";
10718
+ import { join as join21 } from "node:path";
10288
10719
  function printHelp2() {
10289
10720
  process.stderr.write(`
10290
10721
  ${bold("locus review")} — AI-powered code review
@@ -10323,6 +10754,14 @@ async function reviewCommand(projectRoot, args, flags = {}) {
10323
10754
  prNumber = Number.parseInt(args[i], 10);
10324
10755
  }
10325
10756
  }
10757
+ if (config.sandbox.enabled) {
10758
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
10759
+ if (mismatch) {
10760
+ process.stderr.write(`${red("✗")} ${mismatch}
10761
+ `);
10762
+ return;
10763
+ }
10764
+ }
10326
10765
  if (prNumber) {
10327
10766
  return reviewSinglePR(projectRoot, config, prNumber, focus, flags);
10328
10767
  }
@@ -10360,7 +10799,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
10360
10799
  async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
10361
10800
  let prInfo;
10362
10801
  try {
10363
- const result = execSync14(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10802
+ const result = execSync15(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10364
10803
  const raw = JSON.parse(result);
10365
10804
  prInfo = {
10366
10805
  number: raw.number,
@@ -10426,7 +10865,7 @@ ${output.slice(0, 60000)}
10426
10865
 
10427
10866
  ---
10428
10867
  _Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
10429
- execSync14(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10868
+ execSync15(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10430
10869
  process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
10431
10870
  `);
10432
10871
  } catch (e) {
@@ -10444,8 +10883,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
10444
10883
  parts.push(`<role>
10445
10884
  You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
10446
10885
  </role>`);
10447
- const locusPath = join19(projectRoot, ".locus", "LOCUS.md");
10448
- if (existsSync19(locusPath)) {
10886
+ const locusPath = join21(projectRoot, ".locus", "LOCUS.md");
10887
+ if (existsSync21(locusPath)) {
10449
10888
  const content = readFileSync14(locusPath, "utf-8");
10450
10889
  parts.push(`<project-context>
10451
10890
  ${content.slice(0, 2000)}
@@ -10506,7 +10945,7 @@ var exports_iterate = {};
10506
10945
  __export(exports_iterate, {
10507
10946
  iterateCommand: () => iterateCommand
10508
10947
  });
10509
- import { execSync as execSync15 } from "node:child_process";
10948
+ import { execSync as execSync16 } from "node:child_process";
10510
10949
  function printHelp3() {
10511
10950
  process.stderr.write(`
10512
10951
  ${bold("locus iterate")} — Re-execute tasks with PR feedback
@@ -10550,6 +10989,14 @@ async function iterateCommand(projectRoot, args, flags = {}) {
10550
10989
  issueNumber = Number.parseInt(args[i], 10);
10551
10990
  }
10552
10991
  }
10992
+ if (config.sandbox.enabled) {
10993
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
10994
+ if (mismatch) {
10995
+ process.stderr.write(`${red("✗")} ${mismatch}
10996
+ `);
10997
+ return;
10998
+ }
10999
+ }
10553
11000
  if (prNumber) {
10554
11001
  return handleSinglePR(projectRoot, config, prNumber, flags);
10555
11002
  }
@@ -10716,12 +11163,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
10716
11163
  }
10717
11164
  function findPRForIssue(projectRoot, issueNumber) {
10718
11165
  try {
10719
- const result = execSync15(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11166
+ const result = execSync16(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10720
11167
  const parsed = JSON.parse(result);
10721
11168
  if (parsed.length > 0) {
10722
11169
  return parsed[0].number;
10723
11170
  }
10724
- const branchResult = execSync15(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11171
+ const branchResult = execSync16(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10725
11172
  const branchParsed = JSON.parse(branchResult);
10726
11173
  if (branchParsed.length > 0) {
10727
11174
  return branchParsed[0].number;
@@ -10746,6 +11193,7 @@ var init_iterate = __esm(() => {
10746
11193
  init_agent();
10747
11194
  init_config();
10748
11195
  init_github();
11196
+ init_sandbox();
10749
11197
  init_progress();
10750
11198
  init_terminal();
10751
11199
  });
@@ -10756,14 +11204,14 @@ __export(exports_discuss, {
10756
11204
  discussCommand: () => discussCommand
10757
11205
  });
10758
11206
  import {
10759
- existsSync as existsSync20,
11207
+ existsSync as existsSync22,
10760
11208
  mkdirSync as mkdirSync13,
10761
11209
  readdirSync as readdirSync8,
10762
11210
  readFileSync as readFileSync15,
10763
11211
  unlinkSync as unlinkSync5,
10764
11212
  writeFileSync as writeFileSync10
10765
11213
  } from "node:fs";
10766
- import { join as join20 } from "node:path";
11214
+ import { join as join22 } from "node:path";
10767
11215
  function printHelp4() {
10768
11216
  process.stderr.write(`
10769
11217
  ${bold("locus discuss")} — AI-powered architectural discussions
@@ -10785,11 +11233,11 @@ ${bold("Examples:")}
10785
11233
  `);
10786
11234
  }
10787
11235
  function getDiscussionsDir(projectRoot) {
10788
- return join20(projectRoot, ".locus", "discussions");
11236
+ return join22(projectRoot, ".locus", "discussions");
10789
11237
  }
10790
11238
  function ensureDiscussionsDir(projectRoot) {
10791
11239
  const dir = getDiscussionsDir(projectRoot);
10792
- if (!existsSync20(dir)) {
11240
+ if (!existsSync22(dir)) {
10793
11241
  mkdirSync13(dir, { recursive: true });
10794
11242
  }
10795
11243
  return dir;
@@ -10824,7 +11272,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
10824
11272
  }
10825
11273
  function listDiscussions(projectRoot) {
10826
11274
  const dir = getDiscussionsDir(projectRoot);
10827
- if (!existsSync20(dir)) {
11275
+ if (!existsSync22(dir)) {
10828
11276
  process.stderr.write(`${dim("No discussions yet.")}
10829
11277
  `);
10830
11278
  return;
@@ -10841,7 +11289,7 @@ ${bold("Discussions:")}
10841
11289
  `);
10842
11290
  for (const file of files) {
10843
11291
  const id = file.replace(".md", "");
10844
- const content = readFileSync15(join20(dir, file), "utf-8");
11292
+ const content = readFileSync15(join22(dir, file), "utf-8");
10845
11293
  const titleMatch = content.match(/^#\s+(.+)/m);
10846
11294
  const title = titleMatch ? titleMatch[1] : id;
10847
11295
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
@@ -10859,7 +11307,7 @@ function showDiscussion(projectRoot, id) {
10859
11307
  return;
10860
11308
  }
10861
11309
  const dir = getDiscussionsDir(projectRoot);
10862
- if (!existsSync20(dir)) {
11310
+ if (!existsSync22(dir)) {
10863
11311
  process.stderr.write(`${red("✗")} No discussions found.
10864
11312
  `);
10865
11313
  return;
@@ -10871,7 +11319,7 @@ function showDiscussion(projectRoot, id) {
10871
11319
  `);
10872
11320
  return;
10873
11321
  }
10874
- const content = readFileSync15(join20(dir, match), "utf-8");
11322
+ const content = readFileSync15(join22(dir, match), "utf-8");
10875
11323
  process.stdout.write(`${content}
10876
11324
  `);
10877
11325
  }
@@ -10882,7 +11330,7 @@ function deleteDiscussion(projectRoot, id) {
10882
11330
  return;
10883
11331
  }
10884
11332
  const dir = getDiscussionsDir(projectRoot);
10885
- if (!existsSync20(dir)) {
11333
+ if (!existsSync22(dir)) {
10886
11334
  process.stderr.write(`${red("✗")} No discussions found.
10887
11335
  `);
10888
11336
  return;
@@ -10894,7 +11342,7 @@ function deleteDiscussion(projectRoot, id) {
10894
11342
  `);
10895
11343
  return;
10896
11344
  }
10897
- unlinkSync5(join20(dir, match));
11345
+ unlinkSync5(join22(dir, match));
10898
11346
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
10899
11347
  `);
10900
11348
  }
@@ -10907,7 +11355,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
10907
11355
  return;
10908
11356
  }
10909
11357
  const dir = getDiscussionsDir(projectRoot);
10910
- if (!existsSync20(dir)) {
11358
+ if (!existsSync22(dir)) {
10911
11359
  process.stderr.write(`${red("✗")} No discussions found.
10912
11360
  `);
10913
11361
  return;
@@ -10919,7 +11367,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
10919
11367
  `);
10920
11368
  return;
10921
11369
  }
10922
- const content = readFileSync15(join20(dir, match), "utf-8");
11370
+ const content = readFileSync15(join22(dir, match), "utf-8");
10923
11371
  const titleMatch = content.match(/^#\s+(.+)/m);
10924
11372
  const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
10925
11373
  await planCommand(projectRoot, [
@@ -10950,6 +11398,14 @@ async function startDiscussion(projectRoot, topic, flags) {
10950
11398
  const config = loadConfig(projectRoot);
10951
11399
  const timer = createTimer();
10952
11400
  const id = generateId2();
11401
+ if (config.sandbox.enabled) {
11402
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
11403
+ if (mismatch) {
11404
+ process.stderr.write(`${red("✗")} ${mismatch}
11405
+ `);
11406
+ return;
11407
+ }
11408
+ }
10953
11409
  process.stderr.write(`
10954
11410
  ${bold("Discussion:")} ${cyan(topic)}
10955
11411
 
@@ -11033,7 +11489,7 @@ ${turn.content}`;
11033
11489
  ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
11034
11490
  ].join(`
11035
11491
  `);
11036
- writeFileSync10(join20(dir, `${id}.md`), markdown, "utf-8");
11492
+ writeFileSync10(join22(dir, `${id}.md`), markdown, "utf-8");
11037
11493
  process.stderr.write(`
11038
11494
  ${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
11039
11495
  `);
@@ -11048,15 +11504,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
11048
11504
  parts.push(`<role>
11049
11505
  You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
11050
11506
  </role>`);
11051
- const locusPath = join20(projectRoot, ".locus", "LOCUS.md");
11052
- if (existsSync20(locusPath)) {
11507
+ const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
11508
+ if (existsSync22(locusPath)) {
11053
11509
  const content = readFileSync15(locusPath, "utf-8");
11054
11510
  parts.push(`<project-context>
11055
11511
  ${content.slice(0, 3000)}
11056
11512
  </project-context>`);
11057
11513
  }
11058
- const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
11059
- if (existsSync20(learningsPath)) {
11514
+ const learningsPath = join22(projectRoot, ".locus", "LEARNINGS.md");
11515
+ if (existsSync22(learningsPath)) {
11060
11516
  const content = readFileSync15(learningsPath, "utf-8");
11061
11517
  parts.push(`<past-learnings>
11062
11518
  ${content.slice(0, 2000)}
@@ -11128,8 +11584,8 @@ __export(exports_artifacts, {
11128
11584
  formatDate: () => formatDate2,
11129
11585
  artifactsCommand: () => artifactsCommand
11130
11586
  });
11131
- import { existsSync as existsSync21, readdirSync as readdirSync9, readFileSync as readFileSync16, statSync as statSync4 } from "node:fs";
11132
- import { join as join21 } from "node:path";
11587
+ import { existsSync as existsSync23, readdirSync as readdirSync9, readFileSync as readFileSync16, statSync as statSync4 } from "node:fs";
11588
+ import { join as join23 } from "node:path";
11133
11589
  function printHelp5() {
11134
11590
  process.stderr.write(`
11135
11591
  ${bold("locus artifacts")} — View and manage AI-generated artifacts
@@ -11149,14 +11605,14 @@ ${dim("Artifact names support partial matching.")}
11149
11605
  `);
11150
11606
  }
11151
11607
  function getArtifactsDir(projectRoot) {
11152
- return join21(projectRoot, ".locus", "artifacts");
11608
+ return join23(projectRoot, ".locus", "artifacts");
11153
11609
  }
11154
11610
  function listArtifacts(projectRoot) {
11155
11611
  const dir = getArtifactsDir(projectRoot);
11156
- if (!existsSync21(dir))
11612
+ if (!existsSync23(dir))
11157
11613
  return [];
11158
11614
  return readdirSync9(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
11159
- const filePath = join21(dir, fileName);
11615
+ const filePath = join23(dir, fileName);
11160
11616
  const stat = statSync4(filePath);
11161
11617
  return {
11162
11618
  name: fileName.replace(/\.md$/, ""),
@@ -11169,8 +11625,8 @@ function listArtifacts(projectRoot) {
11169
11625
  function readArtifact(projectRoot, name) {
11170
11626
  const dir = getArtifactsDir(projectRoot);
11171
11627
  const fileName = name.endsWith(".md") ? name : `${name}.md`;
11172
- const filePath = join21(dir, fileName);
11173
- if (!existsSync21(filePath))
11628
+ const filePath = join23(dir, fileName);
11629
+ if (!existsSync23(filePath))
11174
11630
  return null;
11175
11631
  const stat = statSync4(filePath);
11176
11632
  return {
@@ -11340,16 +11796,17 @@ __export(exports_sandbox2, {
11340
11796
  parseSandboxInstallArgs: () => parseSandboxInstallArgs,
11341
11797
  parseSandboxExecArgs: () => parseSandboxExecArgs
11342
11798
  });
11343
- import { execSync as execSync16, spawn as spawn6 } from "node:child_process";
11799
+ import { execSync as execSync17, spawn as spawn6 } from "node:child_process";
11344
11800
  import { createHash } from "node:crypto";
11345
- import { existsSync as existsSync22, readFileSync as readFileSync17 } from "node:fs";
11346
- import { basename as basename4, join as join22 } from "node:path";
11801
+ import { existsSync as existsSync24, readFileSync as readFileSync17 } from "node:fs";
11802
+ import { basename as basename4, join as join24 } from "node:path";
11803
+ import { createInterface as createInterface3 } from "node:readline";
11347
11804
  function printSandboxHelp() {
11348
11805
  process.stderr.write(`
11349
11806
  ${bold("locus sandbox")} — Manage Docker sandbox lifecycle
11350
11807
 
11351
11808
  ${bold("Usage:")}
11352
- locus sandbox ${dim("# Create claude/codex sandboxes and enable sandbox mode")}
11809
+ locus sandbox ${dim("# Select a provider and create its sandbox")}
11353
11810
  locus sandbox claude ${dim("# Run claude interactively (for login)")}
11354
11811
  locus sandbox codex ${dim("# Run codex interactively (for login)")}
11355
11812
  locus sandbox setup ${dim("# Re-run dependency install in sandbox(es)")}
@@ -11360,10 +11817,10 @@ ${bold("Usage:")}
11360
11817
  locus sandbox status ${dim("# Show current sandbox state")}
11361
11818
 
11362
11819
  ${bold("Flow:")}
11363
- 1. ${cyan("locus sandbox")} Create sandboxes (auto-installs dependencies)
11364
- 2. ${cyan("locus sandbox claude")} Login Claude inside its sandbox
11365
- 3. ${cyan("locus sandbox codex")} Login Codex inside its sandbox
11366
- 4. ${cyan("locus sandbox install bun")} Install extra tools (optional)
11820
+ 1. ${cyan("locus sandbox")} Select a provider and create its sandbox
11821
+ 2. ${cyan("locus sandbox <provider>")} Login to the provider inside its sandbox
11822
+ 3. ${cyan("locus sandbox install bun")} Install extra tools (optional)
11823
+ 4. Run ${cyan("locus sandbox")} again to add another provider (optional)
11367
11824
 
11368
11825
  `);
11369
11826
  }
@@ -11397,6 +11854,41 @@ async function sandboxCommand(projectRoot, args) {
11397
11854
  `);
11398
11855
  }
11399
11856
  }
11857
+ async function promptProviderSelection() {
11858
+ process.stderr.write(`
11859
+ ${bold("Select a provider to create a sandbox for:")}
11860
+
11861
+ `);
11862
+ for (let i = 0;i < PROVIDERS.length; i++) {
11863
+ process.stderr.write(` ${bold(String(i + 1))}. ${PROVIDERS[i]}
11864
+ `);
11865
+ }
11866
+ process.stderr.write(`
11867
+ `);
11868
+ const rl = createInterface3({
11869
+ input: process.stdin,
11870
+ output: process.stderr
11871
+ });
11872
+ return new Promise((resolve2) => {
11873
+ rl.question("Enter choice (1-2): ", (answer) => {
11874
+ rl.close();
11875
+ const trimmed = answer.trim().toLowerCase();
11876
+ if (trimmed === "claude" || trimmed === "codex") {
11877
+ resolve2(trimmed);
11878
+ return;
11879
+ }
11880
+ const num = Number.parseInt(trimmed, 10);
11881
+ if (num >= 1 && num <= PROVIDERS.length) {
11882
+ resolve2(PROVIDERS[num - 1]);
11883
+ return;
11884
+ }
11885
+ process.stderr.write(`${red("✗")} Invalid selection.
11886
+ `);
11887
+ resolve2(null);
11888
+ });
11889
+ rl.on("close", () => resolve2(null));
11890
+ });
11891
+ }
11400
11892
  async function handleCreate(projectRoot) {
11401
11893
  const config = loadConfig(projectRoot);
11402
11894
  const status = await detectSandboxSupport();
@@ -11407,54 +11899,44 @@ async function handleCreate(projectRoot) {
11407
11899
  `);
11408
11900
  return;
11409
11901
  }
11902
+ const provider = await promptProviderSelection();
11903
+ if (!provider)
11904
+ return;
11410
11905
  const sandboxNames = buildProviderSandboxNames(projectRoot);
11411
- const readySandboxes = {};
11412
- const newlyCreated = new Set;
11413
- let failed = false;
11414
- const createResults = await Promise.all(PROVIDERS.map(async (provider) => {
11415
- const name = sandboxNames[provider];
11416
- if (isSandboxAlive(name)) {
11417
- process.stderr.write(`${green("✓")} ${provider} sandbox ready: ${bold(name)}
11906
+ const name = sandboxNames[provider];
11907
+ const readySandboxes = { ...config.sandbox.providers };
11908
+ if (isSandboxAlive(name)) {
11909
+ process.stderr.write(`${green("✓")} ${provider} sandbox already exists: ${bold(name)}
11910
+ `);
11911
+ readySandboxes[provider] = name;
11912
+ config.sandbox.enabled = true;
11913
+ config.sandbox.providers = readySandboxes;
11914
+ saveConfig(projectRoot, config);
11915
+ process.stderr.write(` Next: run ${cyan(`locus sandbox ${provider}`)} to authenticate.
11418
11916
  `);
11419
- return { provider, name, created: false, existed: true };
11420
- }
11421
- process.stderr.write(`Creating ${bold(provider)} sandbox ${dim(name)} with workspace ${dim(projectRoot)}...
11917
+ return;
11918
+ }
11919
+ process.stderr.write(`Creating ${bold(provider)} sandbox ${dim(name)} with workspace ${dim(projectRoot)}...
11422
11920
  `);
11423
- const created = await createProviderSandbox(provider, name, projectRoot);
11424
- if (!created) {
11425
- process.stderr.write(`${red("✗")} Failed to create ${provider} sandbox (${name}).
11921
+ const created = await createProviderSandbox(provider, name, projectRoot);
11922
+ if (!created) {
11923
+ process.stderr.write(`${red("✗")} Failed to create ${provider} sandbox (${name}).
11426
11924
  `);
11427
- return { provider, name, created: false, existed: false };
11428
- }
11429
- process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold(name)}
11925
+ process.stderr.write(` Re-run ${cyan("locus sandbox")} after resolving Docker issues.
11430
11926
  `);
11431
- return { provider, name, created: true, existed: false };
11432
- }));
11433
- for (const result of createResults) {
11434
- if (result.created || result.existed) {
11435
- readySandboxes[result.provider] = result.name;
11436
- if (result.created)
11437
- newlyCreated.add(result.name);
11438
- } else {
11439
- failed = true;
11440
- }
11927
+ return;
11441
11928
  }
11929
+ process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold(name)}
11930
+ `);
11931
+ readySandboxes[provider] = name;
11442
11932
  config.sandbox.enabled = true;
11443
11933
  config.sandbox.providers = readySandboxes;
11444
11934
  saveConfig(projectRoot, config);
11445
- await Promise.all(PROVIDERS.filter((provider) => {
11446
- const sandboxName = readySandboxes[provider];
11447
- return sandboxName && newlyCreated.has(sandboxName);
11448
- }).map((provider) => runSandboxSetup(readySandboxes[provider], projectRoot)));
11449
- if (failed) {
11450
- process.stderr.write(`
11451
- ${yellow("⚠")} Some sandboxes failed to create. Re-run ${cyan("locus sandbox")} after resolving Docker issues.
11452
- `);
11453
- }
11935
+ await runSandboxSetup(name, projectRoot);
11454
11936
  process.stderr.write(`
11455
- ${green("✓")} Sandbox mode enabled with provider-specific sandboxes.
11937
+ ${green("✓")} Sandbox mode enabled for ${bold(provider)}.
11456
11938
  `);
11457
- process.stderr.write(` Next: run ${cyan("locus sandbox claude")} and ${cyan("locus sandbox codex")} to authenticate both providers.
11939
+ process.stderr.write(` Next: run ${cyan(`locus sandbox ${provider}`)} to authenticate.
11458
11940
  `);
11459
11941
  }
11460
11942
  async function handleAgentLogin(projectRoot, agent) {
@@ -11519,7 +12001,7 @@ function handleRemove(projectRoot) {
11519
12001
  process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
11520
12002
  `);
11521
12003
  try {
11522
- execSync16(`docker sandbox rm ${sandboxName}`, {
12004
+ execSync17(`docker sandbox rm ${sandboxName}`, {
11523
12005
  encoding: "utf-8",
11524
12006
  stdio: ["pipe", "pipe", "pipe"],
11525
12007
  timeout: 15000
@@ -11550,9 +12032,9 @@ ${bold("Sandbox Status")}
11550
12032
  `);
11551
12033
  }
11552
12034
  }
11553
- if (!config.sandbox.providers.claude || !config.sandbox.providers.codex) {
12035
+ if (!config.sandbox.providers.claude && !config.sandbox.providers.codex) {
11554
12036
  process.stderr.write(`
11555
- ${yellow("⚠")} Provider sandboxes are incomplete. Run ${bold("locus sandbox")}.
12037
+ ${yellow("⚠")} No provider sandboxes configured. Run ${bold("locus sandbox")} to create one.
11556
12038
  `);
11557
12039
  }
11558
12040
  process.stderr.write(`
@@ -11793,7 +12275,7 @@ async function handleLogs(projectRoot, args) {
11793
12275
  }
11794
12276
  function detectPackageManager(projectRoot) {
11795
12277
  try {
11796
- const raw = readFileSync17(join22(projectRoot, "package.json"), "utf-8");
12278
+ const raw = readFileSync17(join24(projectRoot, "package.json"), "utf-8");
11797
12279
  const pkgJson = JSON.parse(raw);
11798
12280
  if (typeof pkgJson.packageManager === "string") {
11799
12281
  const name = pkgJson.packageManager.split("@")[0];
@@ -11802,13 +12284,13 @@ function detectPackageManager(projectRoot) {
11802
12284
  }
11803
12285
  }
11804
12286
  } catch {}
11805
- if (existsSync22(join22(projectRoot, "bun.lock")) || existsSync22(join22(projectRoot, "bun.lockb"))) {
12287
+ if (existsSync24(join24(projectRoot, "bun.lock")) || existsSync24(join24(projectRoot, "bun.lockb"))) {
11806
12288
  return "bun";
11807
12289
  }
11808
- if (existsSync22(join22(projectRoot, "yarn.lock"))) {
12290
+ if (existsSync24(join24(projectRoot, "yarn.lock"))) {
11809
12291
  return "yarn";
11810
12292
  }
11811
- if (existsSync22(join22(projectRoot, "pnpm-lock.yaml"))) {
12293
+ if (existsSync24(join24(projectRoot, "pnpm-lock.yaml"))) {
11812
12294
  return "pnpm";
11813
12295
  }
11814
12296
  return "npm";
@@ -11826,31 +12308,39 @@ function getInstallCommand(pm) {
11826
12308
  }
11827
12309
  }
11828
12310
  async function runSandboxSetup(sandboxName, projectRoot) {
11829
- const pm = detectPackageManager(projectRoot);
11830
- if (pm !== "npm") {
11831
- await ensurePackageManagerInSandbox(sandboxName, pm);
11832
- }
11833
- const installCmd = getInstallCommand(pm);
11834
- process.stderr.write(`
12311
+ const ecosystem = detectProjectEcosystem(projectRoot);
12312
+ const isJS = isJavaScriptEcosystem(ecosystem);
12313
+ if (isJS) {
12314
+ const pm = detectPackageManager(projectRoot);
12315
+ if (pm !== "npm") {
12316
+ await ensurePackageManagerInSandbox(sandboxName, pm);
12317
+ }
12318
+ const installCmd = getInstallCommand(pm);
12319
+ process.stderr.write(`
11835
12320
  Installing dependencies (${bold(installCmd.join(" "))}) in sandbox ${dim(sandboxName)}...
11836
12321
  `);
11837
- const installOk = await runInteractiveCommand("docker", [
11838
- "sandbox",
11839
- "exec",
11840
- "-w",
11841
- projectRoot,
11842
- sandboxName,
11843
- ...installCmd
11844
- ]);
11845
- if (!installOk) {
11846
- process.stderr.write(`${red("✗")} Dependency install failed in sandbox ${dim(sandboxName)}.
12322
+ const installOk = await runInteractiveCommand("docker", [
12323
+ "sandbox",
12324
+ "exec",
12325
+ "-w",
12326
+ projectRoot,
12327
+ sandboxName,
12328
+ ...installCmd
12329
+ ]);
12330
+ if (!installOk) {
12331
+ process.stderr.write(`${red("✗")} Dependency install failed in sandbox ${dim(sandboxName)}.
11847
12332
  `);
11848
- return false;
11849
- }
11850
- process.stderr.write(`${green("✓")} Dependencies installed in sandbox ${dim(sandboxName)}.
12333
+ return false;
12334
+ }
12335
+ process.stderr.write(`${green("✓")} Dependencies installed in sandbox ${dim(sandboxName)}.
12336
+ `);
12337
+ } else {
12338
+ process.stderr.write(`
12339
+ ${dim(`Detected ${ecosystem} project — skipping JS package install.`)}
11851
12340
  `);
11852
- const setupScript = join22(projectRoot, ".locus", "sandbox-setup.sh");
11853
- if (existsSync22(setupScript)) {
12341
+ }
12342
+ const setupScript = join24(projectRoot, ".locus", "sandbox-setup.sh");
12343
+ if (existsSync24(setupScript)) {
11854
12344
  process.stderr.write(`Running ${bold(".locus/sandbox-setup.sh")} in sandbox ${dim(sandboxName)}...
11855
12345
  `);
11856
12346
  const hookOk = await runInteractiveCommand("docker", [
@@ -11866,6 +12356,11 @@ Installing dependencies (${bold(installCmd.join(" "))}) in sandbox ${dim(sandbox
11866
12356
  process.stderr.write(`${yellow("⚠")} Setup hook failed in sandbox ${dim(sandboxName)}.
11867
12357
  `);
11868
12358
  }
12359
+ } else if (!isJS) {
12360
+ process.stderr.write(`${yellow("⚠")} No ${bold(".locus/sandbox-setup.sh")} found. Create one to install ${ecosystem} toolchain in the sandbox.
12361
+ `);
12362
+ process.stderr.write(` Re-run ${cyan("locus init")} to auto-generate a template, or create it manually.
12363
+ `);
11869
12364
  }
11870
12365
  return true;
11871
12366
  }
@@ -11933,7 +12428,7 @@ function runInteractiveCommand(command, args) {
11933
12428
  }
11934
12429
  async function createProviderSandbox(provider, sandboxName, projectRoot) {
11935
12430
  try {
11936
- execSync16(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
12431
+ execSync17(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
11937
12432
  stdio: ["pipe", "pipe", "pipe"],
11938
12433
  timeout: 120000
11939
12434
  });
@@ -11949,7 +12444,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
11949
12444
  }
11950
12445
  async function ensurePackageManagerInSandbox(sandboxName, pm) {
11951
12446
  try {
11952
- execSync16(`docker sandbox exec ${sandboxName} which ${pm}`, {
12447
+ execSync17(`docker sandbox exec ${sandboxName} which ${pm}`, {
11953
12448
  stdio: ["pipe", "pipe", "pipe"],
11954
12449
  timeout: 5000
11955
12450
  });
@@ -11958,7 +12453,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
11958
12453
  process.stderr.write(`Installing ${bold(pm)} in sandbox...
11959
12454
  `);
11960
12455
  try {
11961
- execSync16(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
12456
+ execSync17(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
11962
12457
  stdio: "inherit",
11963
12458
  timeout: 120000
11964
12459
  });
@@ -11970,7 +12465,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
11970
12465
  }
11971
12466
  async function ensureCodexInSandbox(sandboxName) {
11972
12467
  try {
11973
- execSync16(`docker sandbox exec ${sandboxName} which codex`, {
12468
+ execSync17(`docker sandbox exec ${sandboxName} which codex`, {
11974
12469
  stdio: ["pipe", "pipe", "pipe"],
11975
12470
  timeout: 5000
11976
12471
  });
@@ -11978,7 +12473,7 @@ async function ensureCodexInSandbox(sandboxName) {
11978
12473
  process.stderr.write(`Installing codex in sandbox...
11979
12474
  `);
11980
12475
  try {
11981
- execSync16(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
12476
+ execSync17(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
11982
12477
  } catch {
11983
12478
  process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
11984
12479
  `);
@@ -11987,7 +12482,7 @@ async function ensureCodexInSandbox(sandboxName) {
11987
12482
  }
11988
12483
  function isSandboxAlive(name) {
11989
12484
  try {
11990
- const output = execSync16("docker sandbox ls", {
12485
+ const output = execSync17("docker sandbox ls", {
11991
12486
  encoding: "utf-8",
11992
12487
  stdio: ["pipe", "pipe", "pipe"],
11993
12488
  timeout: 5000
@@ -12000,6 +12495,7 @@ function isSandboxAlive(name) {
12000
12495
  var PROVIDERS;
12001
12496
  var init_sandbox2 = __esm(() => {
12002
12497
  init_config();
12498
+ init_ecosystem();
12003
12499
  init_sandbox();
12004
12500
  init_sandbox_ignore();
12005
12501
  init_terminal();
@@ -12012,13 +12508,13 @@ init_context();
12012
12508
  init_logger();
12013
12509
  init_rate_limiter();
12014
12510
  init_terminal();
12015
- import { existsSync as existsSync23, readFileSync as readFileSync18 } from "node:fs";
12016
- import { join as join23 } from "node:path";
12511
+ import { existsSync as existsSync25, readFileSync as readFileSync18 } from "node:fs";
12512
+ import { join as join25 } from "node:path";
12017
12513
  import { fileURLToPath } from "node:url";
12018
12514
  function getCliVersion() {
12019
12515
  const fallbackVersion = "0.0.0";
12020
- const packageJsonPath = join23(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
12021
- if (!existsSync23(packageJsonPath)) {
12516
+ const packageJsonPath = join25(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
12517
+ if (!existsSync25(packageJsonPath)) {
12022
12518
  return fallbackVersion;
12023
12519
  }
12024
12520
  try {
@@ -12283,7 +12779,7 @@ async function main() {
12283
12779
  try {
12284
12780
  const root = getGitRoot(cwd);
12285
12781
  if (isInitialized(root)) {
12286
- logDir = join23(root, ".locus", "logs");
12782
+ logDir = join25(root, ".locus", "logs");
12287
12783
  getRateLimiter(root);
12288
12784
  }
12289
12785
  } catch {}