@locusai/cli 0.20.0 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/locus.js +667 -265
  2. package/package.json +1 -1
package/bin/locus.js CHANGED
@@ -1280,6 +1280,177 @@ var init_version_check = __esm(() => {
1280
1280
  CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
1281
1281
  });
1282
1282
 
1283
+ // src/core/ecosystem.ts
1284
+ import { existsSync as existsSync5 } from "node:fs";
1285
+ import { join as join5 } from "node:path";
1286
+ function detectProjectEcosystem(projectRoot) {
1287
+ for (const signal of ECOSYSTEM_SIGNALS) {
1288
+ for (const marker of signal.markers) {
1289
+ if (marker.startsWith("*")) {
1290
+ continue;
1291
+ }
1292
+ if (existsSync5(join5(projectRoot, marker))) {
1293
+ return signal.ecosystem;
1294
+ }
1295
+ }
1296
+ }
1297
+ return "unknown";
1298
+ }
1299
+ function isJavaScriptEcosystem(ecosystem) {
1300
+ return ecosystem === "javascript";
1301
+ }
1302
+ function generateSandboxSetupTemplate(ecosystem) {
1303
+ switch (ecosystem) {
1304
+ case "javascript":
1305
+ return null;
1306
+ case "rust":
1307
+ return `#!/bin/sh
1308
+ # Sandbox setup for Rust projects
1309
+ # This script runs inside the Docker sandbox after creation.
1310
+ # Uncomment or modify the commands below for your project.
1311
+
1312
+ # Install Rust toolchain (if not pre-installed)
1313
+ # curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
1314
+ # . "$HOME/.cargo/env"
1315
+
1316
+ # Build the project
1317
+ # cargo build
1318
+
1319
+ echo "Rust sandbox setup complete"
1320
+ `;
1321
+ case "go":
1322
+ return `#!/bin/sh
1323
+ # Sandbox setup for Go projects
1324
+ # This script runs inside the Docker sandbox after creation.
1325
+
1326
+ # Download dependencies
1327
+ # go mod download
1328
+
1329
+ # Build the project
1330
+ # go build ./...
1331
+
1332
+ echo "Go sandbox setup complete"
1333
+ `;
1334
+ case "python":
1335
+ return `#!/bin/sh
1336
+ # Sandbox setup for Python projects
1337
+ # This script runs inside the Docker sandbox after creation.
1338
+
1339
+ # Create virtual environment and install dependencies
1340
+ # python3 -m venv .venv
1341
+ # . .venv/bin/activate
1342
+ # pip install -r requirements.txt
1343
+ # OR: pip install -e .
1344
+
1345
+ echo "Python sandbox setup complete"
1346
+ `;
1347
+ case "java":
1348
+ return `#!/bin/sh
1349
+ # Sandbox setup for Java/JVM projects
1350
+ # This script runs inside the Docker sandbox after creation.
1351
+
1352
+ # Maven
1353
+ # mvn install -DskipTests
1354
+
1355
+ # Gradle
1356
+ # ./gradlew build -x test
1357
+
1358
+ echo "Java sandbox setup complete"
1359
+ `;
1360
+ case "ruby":
1361
+ return `#!/bin/sh
1362
+ # Sandbox setup for Ruby projects
1363
+ # This script runs inside the Docker sandbox after creation.
1364
+
1365
+ # Install dependencies
1366
+ # bundle install
1367
+
1368
+ echo "Ruby sandbox setup complete"
1369
+ `;
1370
+ case "elixir":
1371
+ return `#!/bin/sh
1372
+ # Sandbox setup for Elixir projects
1373
+ # This script runs inside the Docker sandbox after creation.
1374
+
1375
+ # Install dependencies
1376
+ # mix deps.get
1377
+ # mix compile
1378
+
1379
+ echo "Elixir sandbox setup complete"
1380
+ `;
1381
+ case "dotnet":
1382
+ return `#!/bin/sh
1383
+ # Sandbox setup for .NET projects
1384
+ # This script runs inside the Docker sandbox after creation.
1385
+
1386
+ # Restore packages
1387
+ # dotnet restore
1388
+
1389
+ # Build
1390
+ # dotnet build
1391
+
1392
+ echo ".NET sandbox setup complete"
1393
+ `;
1394
+ case "unknown":
1395
+ return `#!/bin/sh
1396
+ # Sandbox setup script
1397
+ # This script runs inside the Docker sandbox after creation.
1398
+ # Add any commands needed to prepare the build environment.
1399
+
1400
+ # Example: install system dependencies
1401
+ # apt-get update && apt-get install -y <packages>
1402
+
1403
+ # Example: install project dependencies
1404
+ # <your-package-manager> install
1405
+
1406
+ echo "Sandbox setup complete"
1407
+ `;
1408
+ }
1409
+ }
1410
+ var ECOSYSTEM_SIGNALS;
1411
+ var init_ecosystem = __esm(() => {
1412
+ ECOSYSTEM_SIGNALS = [
1413
+ {
1414
+ ecosystem: "javascript",
1415
+ markers: ["package.json"]
1416
+ },
1417
+ {
1418
+ ecosystem: "rust",
1419
+ markers: ["Cargo.toml"]
1420
+ },
1421
+ {
1422
+ ecosystem: "go",
1423
+ markers: ["go.mod"]
1424
+ },
1425
+ {
1426
+ ecosystem: "python",
1427
+ markers: [
1428
+ "pyproject.toml",
1429
+ "setup.py",
1430
+ "setup.cfg",
1431
+ "Pipfile",
1432
+ "requirements.txt"
1433
+ ]
1434
+ },
1435
+ {
1436
+ ecosystem: "java",
1437
+ markers: ["pom.xml", "build.gradle", "build.gradle.kts"]
1438
+ },
1439
+ {
1440
+ ecosystem: "ruby",
1441
+ markers: ["Gemfile"]
1442
+ },
1443
+ {
1444
+ ecosystem: "elixir",
1445
+ markers: ["mix.exs"]
1446
+ },
1447
+ {
1448
+ ecosystem: "dotnet",
1449
+ markers: ["*.csproj", "*.fsproj", "*.sln"]
1450
+ }
1451
+ ];
1452
+ });
1453
+
1283
1454
  // src/core/github.ts
1284
1455
  import {
1285
1456
  execFileSync,
@@ -1476,7 +1647,18 @@ function reopenMilestone(owner, repo, milestoneNumber, options = {}) {
1476
1647
  gh(`api repos/${owner}/${repo}/milestones/${milestoneNumber} -X PATCH -f state=open`, options);
1477
1648
  }
1478
1649
  function createPR(title, body, head, base, options = {}) {
1479
- const result = ghExec(["pr", "create", "--title", title, "--body", body, "--head", head, "--base", base], options);
1650
+ const result = ghExec([
1651
+ "pr",
1652
+ "create",
1653
+ "--title",
1654
+ title,
1655
+ "--body",
1656
+ body,
1657
+ "--head",
1658
+ head,
1659
+ "--base",
1660
+ base
1661
+ ], options);
1480
1662
  const match = result.match(/\/pull\/(\d+)/);
1481
1663
  if (!match) {
1482
1664
  throw new Error(`Could not extract PR number from: ${result}`);
@@ -1630,8 +1812,8 @@ var exports_init = {};
1630
1812
  __export(exports_init, {
1631
1813
  initCommand: () => initCommand
1632
1814
  });
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";
1815
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1816
+ import { join as join6 } from "node:path";
1635
1817
  async function initCommand(cwd) {
1636
1818
  const log = getLogger();
1637
1819
  process.stderr.write(`
@@ -1675,17 +1857,17 @@ ${bold("Initializing Locus...")}
1675
1857
  }
1676
1858
  process.stderr.write(`${green("✓")} Repository: ${bold(`${context.owner}/${context.repo}`)} (branch: ${context.defaultBranch})
1677
1859
  `);
1678
- const locusDir = join5(cwd, ".locus");
1860
+ const locusDir = join6(cwd, ".locus");
1679
1861
  const dirs = [
1680
1862
  locusDir,
1681
- join5(locusDir, "sessions"),
1682
- join5(locusDir, "discussions"),
1683
- join5(locusDir, "artifacts"),
1684
- join5(locusDir, "plans"),
1685
- join5(locusDir, "logs")
1863
+ join6(locusDir, "sessions"),
1864
+ join6(locusDir, "discussions"),
1865
+ join6(locusDir, "artifacts"),
1866
+ join6(locusDir, "plans"),
1867
+ join6(locusDir, "logs")
1686
1868
  ];
1687
1869
  for (const dir of dirs) {
1688
- if (!existsSync5(dir)) {
1870
+ if (!existsSync6(dir)) {
1689
1871
  mkdirSync5(dir, { recursive: true });
1690
1872
  }
1691
1873
  }
@@ -1706,7 +1888,7 @@ ${bold("Initializing Locus...")}
1706
1888
  };
1707
1889
  if (isReInit) {
1708
1890
  try {
1709
- const existing = JSON.parse(readFileSync4(join5(locusDir, "config.json"), "utf-8"));
1891
+ const existing = JSON.parse(readFileSync4(join6(locusDir, "config.json"), "utf-8"));
1710
1892
  if (existing.ai)
1711
1893
  config.ai = { ...config.ai, ...existing.ai };
1712
1894
  if (existing.agent)
@@ -1725,8 +1907,8 @@ ${bold("Initializing Locus...")}
1725
1907
  `);
1726
1908
  }
1727
1909
  saveConfig(cwd, config);
1728
- const locusMdPath = join5(locusDir, "LOCUS.md");
1729
- if (!existsSync5(locusMdPath)) {
1910
+ const locusMdPath = join6(locusDir, "LOCUS.md");
1911
+ if (!existsSync6(locusMdPath)) {
1730
1912
  writeFileSync4(locusMdPath, LOCUS_MD_TEMPLATE, "utf-8");
1731
1913
  process.stderr.write(`${green("✓")} Generated LOCUS.md (edit to add project context)
1732
1914
  `);
@@ -1734,8 +1916,8 @@ ${bold("Initializing Locus...")}
1734
1916
  process.stderr.write(`${dim("○")} LOCUS.md already exists (preserved)
1735
1917
  `);
1736
1918
  }
1737
- const learningsMdPath = join5(locusDir, "LEARNINGS.md");
1738
- if (!existsSync5(learningsMdPath)) {
1919
+ const learningsMdPath = join6(locusDir, "LEARNINGS.md");
1920
+ if (!existsSync6(learningsMdPath)) {
1739
1921
  writeFileSync4(learningsMdPath, LEARNINGS_MD_TEMPLATE, "utf-8");
1740
1922
  process.stderr.write(`${green("✓")} Generated LEARNINGS.md
1741
1923
  `);
@@ -1743,13 +1925,29 @@ ${bold("Initializing Locus...")}
1743
1925
  process.stderr.write(`${dim("○")} LEARNINGS.md already exists (preserved)
1744
1926
  `);
1745
1927
  }
1746
- const sandboxIgnorePath = join5(cwd, ".sandboxignore");
1747
- if (!existsSync5(sandboxIgnorePath)) {
1928
+ const sandboxIgnorePath = join6(cwd, ".sandboxignore");
1929
+ if (!existsSync6(sandboxIgnorePath)) {
1748
1930
  writeFileSync4(sandboxIgnorePath, SANDBOXIGNORE_TEMPLATE, "utf-8");
1749
1931
  process.stderr.write(`${green("✓")} Generated .sandboxignore
1750
1932
  `);
1751
1933
  } else {
1752
1934
  process.stderr.write(`${dim("○")} .sandboxignore already exists (preserved)
1935
+ `);
1936
+ }
1937
+ const ecosystem = detectProjectEcosystem(cwd);
1938
+ const sandboxSetupPath = join6(locusDir, "sandbox-setup.sh");
1939
+ if (!existsSync6(sandboxSetupPath)) {
1940
+ const template = generateSandboxSetupTemplate(ecosystem);
1941
+ if (template) {
1942
+ writeFileSync4(sandboxSetupPath, template, {
1943
+ encoding: "utf-8",
1944
+ mode: 493
1945
+ });
1946
+ process.stderr.write(`${green("✓")} Generated .locus/sandbox-setup.sh (${ecosystem} project detected)
1947
+ `);
1948
+ }
1949
+ } else {
1950
+ process.stderr.write(`${dim("○")} sandbox-setup.sh already exists (preserved)
1753
1951
  `);
1754
1952
  }
1755
1953
  process.stderr.write(`${cyan("●")} Creating GitHub labels...`);
@@ -1761,9 +1959,9 @@ ${bold("Initializing Locus...")}
1761
1959
  process.stderr.write(`\r${yellow("⚠")} Some labels could not be created: ${e.message}
1762
1960
  `);
1763
1961
  }
1764
- const gitignorePath = join5(cwd, ".gitignore");
1962
+ const gitignorePath = join6(cwd, ".gitignore");
1765
1963
  let gitignoreContent = "";
1766
- if (existsSync5(gitignorePath)) {
1964
+ if (existsSync6(gitignorePath)) {
1767
1965
  gitignoreContent = readFileSync4(gitignorePath, "utf-8");
1768
1966
  }
1769
1967
  const entriesToAdd = GITIGNORE_ENTRIES.filter((entry) => entry && !gitignoreContent.includes(entry.trim()));
@@ -1996,6 +2194,7 @@ It is read by AI agents before every task to avoid repeating mistakes and to fol
1996
2194
  var init_init = __esm(() => {
1997
2195
  init_config();
1998
2196
  init_context();
2197
+ init_ecosystem();
1999
2198
  init_github();
2000
2199
  init_logger();
2001
2200
  init_terminal();
@@ -2017,33 +2216,33 @@ var init_init = __esm(() => {
2017
2216
 
2018
2217
  // src/packages/registry.ts
2019
2218
  import {
2020
- existsSync as existsSync6,
2219
+ existsSync as existsSync7,
2021
2220
  mkdirSync as mkdirSync6,
2022
2221
  readFileSync as readFileSync5,
2023
2222
  renameSync,
2024
2223
  writeFileSync as writeFileSync5
2025
2224
  } from "node:fs";
2026
2225
  import { homedir as homedir2 } from "node:os";
2027
- import { join as join6 } from "node:path";
2226
+ import { join as join7 } from "node:path";
2028
2227
  function getPackagesDir() {
2029
2228
  const home = process.env.HOME || homedir2();
2030
- const dir = join6(home, ".locus", "packages");
2031
- if (!existsSync6(dir)) {
2229
+ const dir = join7(home, ".locus", "packages");
2230
+ if (!existsSync7(dir)) {
2032
2231
  mkdirSync6(dir, { recursive: true });
2033
2232
  }
2034
- const pkgJson = join6(dir, "package.json");
2035
- if (!existsSync6(pkgJson)) {
2233
+ const pkgJson = join7(dir, "package.json");
2234
+ if (!existsSync7(pkgJson)) {
2036
2235
  writeFileSync5(pkgJson, `${JSON.stringify({ private: true }, null, 2)}
2037
2236
  `, "utf-8");
2038
2237
  }
2039
2238
  return dir;
2040
2239
  }
2041
2240
  function getRegistryPath() {
2042
- return join6(getPackagesDir(), "registry.json");
2241
+ return join7(getPackagesDir(), "registry.json");
2043
2242
  }
2044
2243
  function loadRegistry() {
2045
2244
  const registryPath = getRegistryPath();
2046
- if (!existsSync6(registryPath)) {
2245
+ if (!existsSync7(registryPath)) {
2047
2246
  return { packages: {} };
2048
2247
  }
2049
2248
  try {
@@ -2066,8 +2265,8 @@ function saveRegistry(registry) {
2066
2265
  }
2067
2266
  function resolvePackageBinary(packageName) {
2068
2267
  const fullName = normalizePackageName(packageName);
2069
- const binPath = join6(getPackagesDir(), "node_modules", ".bin", fullName);
2070
- return existsSync6(binPath) ? binPath : null;
2268
+ const binPath = join7(getPackagesDir(), "node_modules", ".bin", fullName);
2269
+ return existsSync7(binPath) ? binPath : null;
2071
2270
  }
2072
2271
  function normalizePackageName(input) {
2073
2272
  if (input.startsWith("@")) {
@@ -2087,7 +2286,7 @@ __export(exports_pkg, {
2087
2286
  listInstalledPackages: () => listInstalledPackages
2088
2287
  });
2089
2288
  import { spawn } from "node:child_process";
2090
- import { existsSync as existsSync7 } from "node:fs";
2289
+ import { existsSync as existsSync8 } from "node:fs";
2091
2290
  function listInstalledPackages() {
2092
2291
  const registry = loadRegistry();
2093
2292
  const entries = Object.values(registry.packages);
@@ -2165,7 +2364,7 @@ async function pkgCommand(args, _flags) {
2165
2364
  return;
2166
2365
  }
2167
2366
  const binaryPath = entry.binaryPath;
2168
- if (!binaryPath || !existsSync7(binaryPath)) {
2367
+ if (!binaryPath || !existsSync8(binaryPath)) {
2169
2368
  process.stderr.write(`${red("✗")} Binary for ${bold(packageName)} not found on disk.
2170
2369
  `);
2171
2370
  if (binaryPath) {
@@ -2291,8 +2490,8 @@ __export(exports_install, {
2291
2490
  installCommand: () => installCommand
2292
2491
  });
2293
2492
  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";
2493
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
2494
+ import { join as join8 } from "node:path";
2296
2495
  function parsePackageArg(raw) {
2297
2496
  if (raw.startsWith("@")) {
2298
2497
  const slashIdx = raw.indexOf("/");
@@ -2369,8 +2568,8 @@ ${red("✗")} Failed to install ${bold(packageSpec)}.
2369
2568
  process.exit(1);
2370
2569
  return;
2371
2570
  }
2372
- const installedPkgJsonPath = join7(packagesDir, "node_modules", packageName, "package.json");
2373
- if (!existsSync8(installedPkgJsonPath)) {
2571
+ const installedPkgJsonPath = join8(packagesDir, "node_modules", packageName, "package.json");
2572
+ if (!existsSync9(installedPkgJsonPath)) {
2374
2573
  process.stderr.write(`
2375
2574
  ${red("✗")} Package installed but package.json not found at:
2376
2575
  `);
@@ -2645,16 +2844,16 @@ __export(exports_logs, {
2645
2844
  logsCommand: () => logsCommand
2646
2845
  });
2647
2846
  import {
2648
- existsSync as existsSync9,
2847
+ existsSync as existsSync10,
2649
2848
  readdirSync as readdirSync2,
2650
2849
  readFileSync as readFileSync7,
2651
2850
  statSync as statSync2,
2652
2851
  unlinkSync as unlinkSync2
2653
2852
  } from "node:fs";
2654
- import { join as join8 } from "node:path";
2853
+ import { join as join9 } from "node:path";
2655
2854
  async function logsCommand(cwd, options) {
2656
- const logsDir = join8(cwd, ".locus", "logs");
2657
- if (!existsSync9(logsDir)) {
2855
+ const logsDir = join9(cwd, ".locus", "logs");
2856
+ if (!existsSync10(logsDir)) {
2658
2857
  process.stderr.write(`${dim("No logs found.")}
2659
2858
  `);
2660
2859
  return;
@@ -2709,8 +2908,8 @@ async function tailLog(logFile, levelFilter) {
2709
2908
  process.stderr.write(`${bold("Tailing:")} ${dim(logFile)} ${dim("(Ctrl+C to stop)")}
2710
2909
 
2711
2910
  `);
2712
- let lastSize = existsSync9(logFile) ? statSync2(logFile).size : 0;
2713
- if (existsSync9(logFile)) {
2911
+ let lastSize = existsSync10(logFile) ? statSync2(logFile).size : 0;
2912
+ if (existsSync10(logFile)) {
2714
2913
  const content = readFileSync7(logFile, "utf-8");
2715
2914
  const lines = content.trim().split(`
2716
2915
  `).filter(Boolean);
@@ -2729,7 +2928,7 @@ async function tailLog(logFile, levelFilter) {
2729
2928
  }
2730
2929
  return new Promise((resolve) => {
2731
2930
  const interval = setInterval(() => {
2732
- if (!existsSync9(logFile))
2931
+ if (!existsSync10(logFile))
2733
2932
  return;
2734
2933
  const currentSize = statSync2(logFile).size;
2735
2934
  if (currentSize <= lastSize)
@@ -2789,7 +2988,7 @@ function cleanLogs(logsDir) {
2789
2988
  `);
2790
2989
  }
2791
2990
  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);
2991
+ 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
2992
  }
2794
2993
  function formatEntry(entry) {
2795
2994
  const time = dim(new Date(entry.ts).toLocaleTimeString());
@@ -3099,9 +3298,9 @@ var init_stream_renderer = __esm(() => {
3099
3298
 
3100
3299
  // src/repl/clipboard.ts
3101
3300
  import { execSync as execSync4 } from "node:child_process";
3102
- import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "node:fs";
3301
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7 } from "node:fs";
3103
3302
  import { tmpdir } from "node:os";
3104
- import { join as join9 } from "node:path";
3303
+ import { join as join10 } from "node:path";
3105
3304
  function readClipboardImage() {
3106
3305
  if (process.platform === "darwin") {
3107
3306
  return readMacOSClipboardImage();
@@ -3112,14 +3311,14 @@ function readClipboardImage() {
3112
3311
  return null;
3113
3312
  }
3114
3313
  function ensureStableDir() {
3115
- if (!existsSync10(STABLE_DIR)) {
3314
+ if (!existsSync11(STABLE_DIR)) {
3116
3315
  mkdirSync7(STABLE_DIR, { recursive: true });
3117
3316
  }
3118
3317
  }
3119
3318
  function readMacOSClipboardImage() {
3120
3319
  try {
3121
3320
  ensureStableDir();
3122
- const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
3321
+ const destPath = join10(STABLE_DIR, `clipboard-${Date.now()}.png`);
3123
3322
  const script = [
3124
3323
  `set destPath to POSIX file "${destPath}"`,
3125
3324
  "try",
@@ -3143,7 +3342,7 @@ function readMacOSClipboardImage() {
3143
3342
  timeout: 5000,
3144
3343
  stdio: ["pipe", "pipe", "pipe"]
3145
3344
  }).trim();
3146
- if (result === "ok" && existsSync10(destPath)) {
3345
+ if (result === "ok" && existsSync11(destPath)) {
3147
3346
  return destPath;
3148
3347
  }
3149
3348
  } catch {}
@@ -3156,9 +3355,9 @@ function readLinuxClipboardImage() {
3156
3355
  return null;
3157
3356
  }
3158
3357
  ensureStableDir();
3159
- const destPath = join9(STABLE_DIR, `clipboard-${Date.now()}.png`);
3358
+ const destPath = join10(STABLE_DIR, `clipboard-${Date.now()}.png`);
3160
3359
  execSync4(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
3161
- if (existsSync10(destPath)) {
3360
+ if (existsSync11(destPath)) {
3162
3361
  return destPath;
3163
3362
  }
3164
3363
  } catch {}
@@ -3166,13 +3365,13 @@ function readLinuxClipboardImage() {
3166
3365
  }
3167
3366
  var STABLE_DIR;
3168
3367
  var init_clipboard = __esm(() => {
3169
- STABLE_DIR = join9(tmpdir(), "locus-images");
3368
+ STABLE_DIR = join10(tmpdir(), "locus-images");
3170
3369
  });
3171
3370
 
3172
3371
  // src/repl/image-detect.ts
3173
- import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "node:fs";
3372
+ import { copyFileSync, existsSync as existsSync12, mkdirSync as mkdirSync8 } from "node:fs";
3174
3373
  import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
3175
- import { basename, extname, join as join10, resolve } from "node:path";
3374
+ import { basename, extname, join as join11, resolve } from "node:path";
3176
3375
  function detectImages(input) {
3177
3376
  const detected = [];
3178
3377
  const byResolved = new Map;
@@ -3266,15 +3465,15 @@ function collectReferencedAttachments(input, attachments) {
3266
3465
  return dedupeByResolvedPath(selected);
3267
3466
  }
3268
3467
  function relocateImages(images, projectRoot) {
3269
- const targetDir = join10(projectRoot, ".locus", "tmp", "images");
3468
+ const targetDir = join11(projectRoot, ".locus", "tmp", "images");
3270
3469
  for (const img of images) {
3271
3470
  if (!img.exists)
3272
3471
  continue;
3273
3472
  try {
3274
- if (!existsSync11(targetDir)) {
3473
+ if (!existsSync12(targetDir)) {
3275
3474
  mkdirSync8(targetDir, { recursive: true });
3276
3475
  }
3277
- const dest = join10(targetDir, basename(img.stablePath));
3476
+ const dest = join11(targetDir, basename(img.stablePath));
3278
3477
  copyFileSync(img.stablePath, dest);
3279
3478
  img.stablePath = dest;
3280
3479
  } catch {}
@@ -3286,7 +3485,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
3286
3485
  return;
3287
3486
  let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
3288
3487
  if (resolved.startsWith("~/")) {
3289
- resolved = join10(homedir3(), resolved.slice(2));
3488
+ resolved = join11(homedir3(), resolved.slice(2));
3290
3489
  }
3291
3490
  resolved = resolve(resolved);
3292
3491
  const existing = byResolved.get(resolved);
@@ -3299,7 +3498,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
3299
3498
  ]);
3300
3499
  return;
3301
3500
  }
3302
- const exists = existsSync11(resolved);
3501
+ const exists = existsSync12(resolved);
3303
3502
  let stablePath = resolved;
3304
3503
  if (exists) {
3305
3504
  stablePath = copyToStable(resolved);
@@ -3353,10 +3552,10 @@ function dedupeByResolvedPath(images) {
3353
3552
  }
3354
3553
  function copyToStable(sourcePath) {
3355
3554
  try {
3356
- if (!existsSync11(STABLE_DIR2)) {
3555
+ if (!existsSync12(STABLE_DIR2)) {
3357
3556
  mkdirSync8(STABLE_DIR2, { recursive: true });
3358
3557
  }
3359
- const dest = join10(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
3558
+ const dest = join11(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
3360
3559
  copyFileSync(sourcePath, dest);
3361
3560
  return dest;
3362
3561
  } catch {
@@ -3376,7 +3575,7 @@ var init_image_detect = __esm(() => {
3376
3575
  ".tif",
3377
3576
  ".tiff"
3378
3577
  ]);
3379
- STABLE_DIR2 = join10(tmpdir2(), "locus-images");
3578
+ STABLE_DIR2 = join11(tmpdir2(), "locus-images");
3380
3579
  PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
3381
3580
  });
3382
3581
 
@@ -4160,10 +4359,7 @@ __export(exports_claude, {
4160
4359
  });
4161
4360
  import { execSync as execSync5, spawn as spawn2 } from "node:child_process";
4162
4361
  function buildClaudeArgs(options) {
4163
- const args = [
4164
- "--dangerously-skip-permissions",
4165
- "--no-session-persistence"
4166
- ];
4362
+ const args = ["--dangerously-skip-permissions", "--no-session-persistence"];
4167
4363
  if (options.model) {
4168
4364
  args.push("--model", options.model);
4169
4365
  }
@@ -4355,11 +4551,11 @@ var init_claude = __esm(() => {
4355
4551
 
4356
4552
  // src/core/sandbox-ignore.ts
4357
4553
  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";
4554
+ import { existsSync as existsSync13, readFileSync as readFileSync8 } from "node:fs";
4555
+ import { join as join12 } from "node:path";
4360
4556
  import { promisify } from "node:util";
4361
4557
  function parseIgnoreFile(filePath) {
4362
- if (!existsSync12(filePath))
4558
+ if (!existsSync13(filePath))
4363
4559
  return [];
4364
4560
  const content = readFileSync8(filePath, "utf-8");
4365
4561
  const rules = [];
@@ -4406,7 +4602,7 @@ function buildCleanupScript(rules, workspacePath) {
4406
4602
  }
4407
4603
  async function enforceSandboxIgnore(sandboxName, projectRoot) {
4408
4604
  const log = getLogger();
4409
- const ignorePath = join11(projectRoot, ".sandboxignore");
4605
+ const ignorePath = join12(projectRoot, ".sandboxignore");
4410
4606
  const rules = parseIgnoreFile(ignorePath);
4411
4607
  if (rules.length === 0)
4412
4608
  return;
@@ -4534,7 +4730,6 @@ class SandboxedClaudeRunner {
4534
4730
  const text = chunk.toString();
4535
4731
  errorOutput += text;
4536
4732
  log.debug("sandboxed claude stderr", { text: text.slice(0, 500) });
4537
- options.onOutput?.(text);
4538
4733
  });
4539
4734
  this.process.on("close", (code) => {
4540
4735
  this.process = null;
@@ -6686,8 +6881,8 @@ var init_sprint = __esm(() => {
6686
6881
 
6687
6882
  // src/core/prompt-builder.ts
6688
6883
  import { execSync as execSync7 } from "node:child_process";
6689
- import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
6690
- import { join as join12 } from "node:path";
6884
+ import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
6885
+ import { join as join13 } from "node:path";
6691
6886
  function buildExecutionPrompt(ctx) {
6692
6887
  const sections = [];
6693
6888
  sections.push(buildSystemContext(ctx.projectRoot));
@@ -6717,13 +6912,13 @@ function buildFeedbackPrompt(ctx) {
6717
6912
  }
6718
6913
  function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
6719
6914
  const sections = [];
6720
- const locusmd = readFileSafe(join12(projectRoot, ".locus", "LOCUS.md"));
6915
+ const locusmd = readFileSafe(join13(projectRoot, ".locus", "LOCUS.md"));
6721
6916
  if (locusmd) {
6722
6917
  sections.push(`<project-instructions>
6723
6918
  ${locusmd}
6724
6919
  </project-instructions>`);
6725
6920
  }
6726
- const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
6921
+ const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
6727
6922
  if (learnings) {
6728
6923
  sections.push(`<past-learnings>
6729
6924
  ${learnings}
@@ -6749,24 +6944,24 @@ ${userMessage}
6749
6944
  }
6750
6945
  function buildSystemContext(projectRoot) {
6751
6946
  const parts = [];
6752
- const locusmd = readFileSafe(join12(projectRoot, ".locus", "LOCUS.md"));
6947
+ const locusmd = readFileSafe(join13(projectRoot, ".locus", "LOCUS.md"));
6753
6948
  if (locusmd) {
6754
6949
  parts.push(`<project-instructions>
6755
6950
  ${locusmd}
6756
6951
  </project-instructions>`);
6757
6952
  }
6758
- const learnings = readFileSafe(join12(projectRoot, ".locus", "LEARNINGS.md"));
6953
+ const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
6759
6954
  if (learnings) {
6760
6955
  parts.push(`<past-learnings>
6761
6956
  ${learnings}
6762
6957
  </past-learnings>`);
6763
6958
  }
6764
- const discussionsDir = join12(projectRoot, ".locus", "discussions");
6765
- if (existsSync13(discussionsDir)) {
6959
+ const discussionsDir = join13(projectRoot, ".locus", "discussions");
6960
+ if (existsSync14(discussionsDir)) {
6766
6961
  try {
6767
6962
  const files = readdirSync3(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
6768
6963
  for (const file of files) {
6769
- const content = readFileSafe(join12(discussionsDir, file));
6964
+ const content = readFileSafe(join13(discussionsDir, file));
6770
6965
  if (content) {
6771
6966
  const name = file.replace(".md", "");
6772
6967
  parts.push(`<discussion name="${name}">
@@ -6917,7 +7112,7 @@ function buildFeedbackInstructions() {
6917
7112
  }
6918
7113
  function readFileSafe(path) {
6919
7114
  try {
6920
- if (!existsSync13(path))
7115
+ if (!existsSync14(path))
6921
7116
  return null;
6922
7117
  return readFileSync9(path, "utf-8");
6923
7118
  } catch {
@@ -7383,7 +7578,7 @@ var init_commands = __esm(() => {
7383
7578
 
7384
7579
  // src/repl/completions.ts
7385
7580
  import { readdirSync as readdirSync4 } from "node:fs";
7386
- import { basename as basename2, dirname as dirname3, join as join13 } from "node:path";
7581
+ import { basename as basename2, dirname as dirname3, join as join14 } from "node:path";
7387
7582
 
7388
7583
  class SlashCommandCompletion {
7389
7584
  commands;
@@ -7438,7 +7633,7 @@ class FilePathCompletion {
7438
7633
  }
7439
7634
  findMatches(partial) {
7440
7635
  try {
7441
- const dir = partial.includes("/") ? join13(this.projectRoot, dirname3(partial)) : this.projectRoot;
7636
+ const dir = partial.includes("/") ? join14(this.projectRoot, dirname3(partial)) : this.projectRoot;
7442
7637
  const prefix = basename2(partial);
7443
7638
  const entries = readdirSync4(dir, { withFileTypes: true });
7444
7639
  return entries.filter((e) => {
@@ -7474,14 +7669,14 @@ class CombinedCompletion {
7474
7669
  var init_completions = () => {};
7475
7670
 
7476
7671
  // src/repl/input-history.ts
7477
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
7478
- import { dirname as dirname4, join as join14 } from "node:path";
7672
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
7673
+ import { dirname as dirname4, join as join15 } from "node:path";
7479
7674
 
7480
7675
  class InputHistory {
7481
7676
  entries = [];
7482
7677
  filePath;
7483
7678
  constructor(projectRoot) {
7484
- this.filePath = join14(projectRoot, ".locus", "sessions", ".input-history");
7679
+ this.filePath = join15(projectRoot, ".locus", "sessions", ".input-history");
7485
7680
  this.load();
7486
7681
  }
7487
7682
  add(text) {
@@ -7520,7 +7715,7 @@ class InputHistory {
7520
7715
  }
7521
7716
  load() {
7522
7717
  try {
7523
- if (!existsSync14(this.filePath))
7718
+ if (!existsSync15(this.filePath))
7524
7719
  return;
7525
7720
  const content = readFileSync10(this.filePath, "utf-8");
7526
7721
  this.entries = content.split(`
@@ -7530,7 +7725,7 @@ class InputHistory {
7530
7725
  save() {
7531
7726
  try {
7532
7727
  const dir = dirname4(this.filePath);
7533
- if (!existsSync14(dir)) {
7728
+ if (!existsSync15(dir)) {
7534
7729
  mkdirSync9(dir, { recursive: true });
7535
7730
  }
7536
7731
  const content = this.entries.map((e) => this.escape(e)).join(`
@@ -7561,20 +7756,20 @@ var init_model_config = __esm(() => {
7561
7756
 
7562
7757
  // src/repl/session-manager.ts
7563
7758
  import {
7564
- existsSync as existsSync15,
7759
+ existsSync as existsSync16,
7565
7760
  mkdirSync as mkdirSync10,
7566
7761
  readdirSync as readdirSync5,
7567
7762
  readFileSync as readFileSync11,
7568
7763
  unlinkSync as unlinkSync3,
7569
7764
  writeFileSync as writeFileSync7
7570
7765
  } from "node:fs";
7571
- import { basename as basename3, join as join15 } from "node:path";
7766
+ import { basename as basename3, join as join16 } from "node:path";
7572
7767
 
7573
7768
  class SessionManager {
7574
7769
  sessionsDir;
7575
7770
  constructor(projectRoot) {
7576
- this.sessionsDir = join15(projectRoot, ".locus", "sessions");
7577
- if (!existsSync15(this.sessionsDir)) {
7771
+ this.sessionsDir = join16(projectRoot, ".locus", "sessions");
7772
+ if (!existsSync16(this.sessionsDir)) {
7578
7773
  mkdirSync10(this.sessionsDir, { recursive: true });
7579
7774
  }
7580
7775
  }
@@ -7600,12 +7795,12 @@ class SessionManager {
7600
7795
  }
7601
7796
  isPersisted(sessionOrId) {
7602
7797
  const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
7603
- return existsSync15(this.getSessionPath(sessionId));
7798
+ return existsSync16(this.getSessionPath(sessionId));
7604
7799
  }
7605
7800
  load(idOrPrefix) {
7606
7801
  const files = this.listSessionFiles();
7607
7802
  const exactPath = this.getSessionPath(idOrPrefix);
7608
- if (existsSync15(exactPath)) {
7803
+ if (existsSync16(exactPath)) {
7609
7804
  try {
7610
7805
  return JSON.parse(readFileSync11(exactPath, "utf-8"));
7611
7806
  } catch {
@@ -7655,7 +7850,7 @@ class SessionManager {
7655
7850
  }
7656
7851
  delete(sessionId) {
7657
7852
  const path = this.getSessionPath(sessionId);
7658
- if (existsSync15(path)) {
7853
+ if (existsSync16(path)) {
7659
7854
  unlinkSync3(path);
7660
7855
  return true;
7661
7856
  }
@@ -7685,7 +7880,7 @@ class SessionManager {
7685
7880
  const remaining = withStats.length - pruned;
7686
7881
  if (remaining > MAX_SESSIONS) {
7687
7882
  const toRemove = remaining - MAX_SESSIONS;
7688
- const alive = withStats.filter((e) => existsSync15(e.path));
7883
+ const alive = withStats.filter((e) => existsSync16(e.path));
7689
7884
  for (let i = 0;i < toRemove && i < alive.length; i++) {
7690
7885
  try {
7691
7886
  unlinkSync3(alive[i].path);
@@ -7700,7 +7895,7 @@ class SessionManager {
7700
7895
  }
7701
7896
  listSessionFiles() {
7702
7897
  try {
7703
- return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join15(this.sessionsDir, f));
7898
+ return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join16(this.sessionsDir, f));
7704
7899
  } catch {
7705
7900
  return [];
7706
7901
  }
@@ -7709,7 +7904,7 @@ class SessionManager {
7709
7904
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
7710
7905
  }
7711
7906
  getSessionPath(sessionId) {
7712
- return join15(this.sessionsDir, `${sessionId}.json`);
7907
+ return join16(this.sessionsDir, `${sessionId}.json`);
7713
7908
  }
7714
7909
  }
7715
7910
  var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
@@ -8186,8 +8381,167 @@ var init_exec = __esm(() => {
8186
8381
  init_session_manager();
8187
8382
  });
8188
8383
 
8189
- // src/core/agent.ts
8384
+ // src/core/submodule.ts
8190
8385
  import { execSync as execSync10 } from "node:child_process";
8386
+ import { existsSync as existsSync17 } from "node:fs";
8387
+ import { join as join17 } from "node:path";
8388
+ function git2(args, cwd) {
8389
+ return execSync10(`git ${args}`, {
8390
+ cwd,
8391
+ encoding: "utf-8",
8392
+ stdio: ["pipe", "pipe", "pipe"]
8393
+ });
8394
+ }
8395
+ function gitSafe(args, cwd) {
8396
+ try {
8397
+ return git2(args, cwd);
8398
+ } catch {
8399
+ return null;
8400
+ }
8401
+ }
8402
+ function hasSubmodules(cwd) {
8403
+ return existsSync17(join17(cwd, ".gitmodules"));
8404
+ }
8405
+ function listSubmodules(cwd) {
8406
+ if (!hasSubmodules(cwd))
8407
+ return [];
8408
+ const log = getLogger();
8409
+ const submodules = [];
8410
+ try {
8411
+ const output = git2("submodule status", cwd);
8412
+ for (const line of output.trim().split(`
8413
+ `)) {
8414
+ if (!line.trim())
8415
+ continue;
8416
+ const dirty = line.startsWith("+");
8417
+ const parts = line.trim().replace(/^[+-]/, "").split(/\s+/);
8418
+ const path = parts[1];
8419
+ if (!path)
8420
+ continue;
8421
+ submodules.push({
8422
+ path,
8423
+ absolutePath: join17(cwd, path),
8424
+ dirty
8425
+ });
8426
+ }
8427
+ } catch (e) {
8428
+ log.warn(`Failed to list submodules: ${e}`);
8429
+ }
8430
+ return submodules;
8431
+ }
8432
+ function getDirtySubmodules(cwd) {
8433
+ const submodules = listSubmodules(cwd);
8434
+ const dirty = [];
8435
+ for (const sub of submodules) {
8436
+ if (!existsSync17(sub.absolutePath))
8437
+ continue;
8438
+ const status = gitSafe("status --porcelain", sub.absolutePath);
8439
+ if (status && status.trim().length > 0) {
8440
+ dirty.push({ ...sub, dirty: true });
8441
+ }
8442
+ }
8443
+ return dirty;
8444
+ }
8445
+ function commitDirtySubmodules(cwd, issueNumber, issueTitle) {
8446
+ const log = getLogger();
8447
+ const dirtySubmodules = getDirtySubmodules(cwd);
8448
+ if (dirtySubmodules.length === 0)
8449
+ return [];
8450
+ const committed = [];
8451
+ for (const sub of dirtySubmodules) {
8452
+ try {
8453
+ git2("add -A", sub.absolutePath);
8454
+ const message = `chore: complete #${issueNumber} - ${issueTitle}
8455
+
8456
+ Co-Authored-By: LocusAgent <agent@locusai.team>`;
8457
+ execSync10("git commit -F -", {
8458
+ input: message,
8459
+ cwd: sub.absolutePath,
8460
+ encoding: "utf-8",
8461
+ stdio: ["pipe", "pipe", "pipe"]
8462
+ });
8463
+ committed.push(sub.path);
8464
+ log.info(`Committed submodule changes: ${sub.path} for #${issueNumber}`);
8465
+ } catch {
8466
+ log.verbose(`No committable changes in submodule ${sub.path}`);
8467
+ }
8468
+ }
8469
+ if (committed.length > 0) {
8470
+ for (const subPath of committed) {
8471
+ gitSafe(`add ${subPath}`, cwd);
8472
+ }
8473
+ }
8474
+ return committed;
8475
+ }
8476
+ function initSubmodules(cwd) {
8477
+ if (!hasSubmodules(cwd))
8478
+ return;
8479
+ const log = getLogger();
8480
+ try {
8481
+ git2("submodule update --init --recursive", cwd);
8482
+ log.info("Initialized submodules");
8483
+ } catch (e) {
8484
+ log.warn(`Failed to initialize submodules: ${e}`);
8485
+ }
8486
+ }
8487
+ function updateSubmodulesAfterRebase(cwd) {
8488
+ if (!hasSubmodules(cwd))
8489
+ return;
8490
+ const log = getLogger();
8491
+ try {
8492
+ git2("submodule update --recursive", cwd);
8493
+ log.info("Updated submodules after rebase");
8494
+ } catch (e) {
8495
+ log.warn(`Failed to update submodules after rebase: ${e}`);
8496
+ }
8497
+ }
8498
+ function getSubmoduleChangeSummary(cwd, baseBranch) {
8499
+ if (!hasSubmodules(cwd))
8500
+ return null;
8501
+ const diff = gitSafe(`diff origin/${baseBranch}..HEAD --submodule=short`, cwd);
8502
+ if (!diff || !diff.trim())
8503
+ return null;
8504
+ const submoduleChanges = [];
8505
+ for (const line of diff.split(`
8506
+ `)) {
8507
+ if (line.startsWith("Submodule ")) {
8508
+ submoduleChanges.push(line.trim());
8509
+ }
8510
+ }
8511
+ if (submoduleChanges.length === 0)
8512
+ return null;
8513
+ return `### Submodule Changes
8514
+ ${submoduleChanges.map((c) => `- ${c}`).join(`
8515
+ `)}`;
8516
+ }
8517
+ function pushSubmoduleBranches(cwd) {
8518
+ if (!hasSubmodules(cwd))
8519
+ return;
8520
+ const log = getLogger();
8521
+ const submodules = listSubmodules(cwd);
8522
+ for (const sub of submodules) {
8523
+ if (!existsSync17(sub.absolutePath))
8524
+ continue;
8525
+ const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
8526
+ if (!branch || branch === "HEAD")
8527
+ continue;
8528
+ const unpushed = gitSafe(`log origin/${branch}..HEAD --oneline`, sub.absolutePath)?.trim();
8529
+ if (unpushed) {
8530
+ try {
8531
+ git2(`push origin ${branch}`, sub.absolutePath);
8532
+ log.info(`Pushed submodule ${sub.path} branch ${branch}`);
8533
+ } catch (e) {
8534
+ log.warn(`Failed to push submodule ${sub.path}: ${e}`);
8535
+ }
8536
+ }
8537
+ }
8538
+ }
8539
+ var init_submodule = __esm(() => {
8540
+ init_logger();
8541
+ });
8542
+
8543
+ // src/core/agent.ts
8544
+ import { execSync as execSync11 } from "node:child_process";
8191
8545
  async function executeIssue(projectRoot, options) {
8192
8546
  const log = getLogger();
8193
8547
  const timer = createTimer();
@@ -8216,7 +8570,7 @@ ${cyan("●")} ${bold(`#${issueNumber}`)} ${issue.title}
8216
8570
  }
8217
8571
  let issueComments = [];
8218
8572
  try {
8219
- const commentsRaw = execSync10(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8573
+ const commentsRaw = execSync11(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8220
8574
  if (commentsRaw) {
8221
8575
  issueComments = commentsRaw.split(`
8222
8576
  `).filter(Boolean);
@@ -8380,12 +8734,12 @@ ${aiResult.success ? green("✓") : red("✗")} Iteration ${aiResult.success ? "
8380
8734
  }
8381
8735
  async function createIssuePR(projectRoot, config, issue) {
8382
8736
  try {
8383
- const currentBranch = execSync10("git rev-parse --abbrev-ref HEAD", {
8737
+ const currentBranch = execSync11("git rev-parse --abbrev-ref HEAD", {
8384
8738
  cwd: projectRoot,
8385
8739
  encoding: "utf-8",
8386
8740
  stdio: ["pipe", "pipe", "pipe"]
8387
8741
  }).trim();
8388
- const diff = execSync10(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
8742
+ const diff = execSync11(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
8389
8743
  cwd: projectRoot,
8390
8744
  encoding: "utf-8",
8391
8745
  stdio: ["pipe", "pipe", "pipe"]
@@ -8394,17 +8748,25 @@ async function createIssuePR(projectRoot, config, issue) {
8394
8748
  getLogger().verbose("No changes to create PR for");
8395
8749
  return;
8396
8750
  }
8397
- execSync10(`git push -u origin ${currentBranch}`, {
8751
+ pushSubmoduleBranches(projectRoot);
8752
+ execSync11(`git push -u origin ${currentBranch}`, {
8398
8753
  cwd: projectRoot,
8399
8754
  encoding: "utf-8",
8400
8755
  stdio: ["pipe", "pipe", "pipe"]
8401
8756
  });
8402
- const prTitle = `${issue.title} (#${issue.number})`;
8403
- const prBody = `Closes #${issue.number}
8757
+ const submoduleSummary = getSubmoduleChangeSummary(projectRoot, config.agent.baseBranch);
8758
+ let prBody = `Closes #${issue.number}`;
8759
+ if (submoduleSummary) {
8760
+ prBody += `
8761
+
8762
+ ${submoduleSummary}`;
8763
+ }
8764
+ prBody += `
8404
8765
 
8405
8766
  ---
8406
8767
 
8407
8768
  \uD83E\uDD16 Automated by [Locus](https://github.com/locusai/locus)`;
8769
+ const prTitle = `${issue.title} (#${issue.number})`;
8408
8770
  const prNumber = createPR(prTitle, prBody, currentBranch, config.agent.baseBranch, { cwd: projectRoot });
8409
8771
  process.stderr.write(` ${green("✓")} Created PR #${prNumber}
8410
8772
  `);
@@ -8438,20 +8800,21 @@ var init_agent = __esm(() => {
8438
8800
  init_logger();
8439
8801
  init_prompt_builder();
8440
8802
  init_sandbox();
8803
+ init_submodule();
8441
8804
  });
8442
8805
 
8443
8806
  // src/core/conflict.ts
8444
- import { execSync as execSync11 } from "node:child_process";
8445
- function git2(args, cwd) {
8446
- return execSync11(`git ${args}`, {
8807
+ import { execSync as execSync12 } from "node:child_process";
8808
+ function git3(args, cwd) {
8809
+ return execSync12(`git ${args}`, {
8447
8810
  cwd,
8448
8811
  encoding: "utf-8",
8449
8812
  stdio: ["pipe", "pipe", "pipe"]
8450
8813
  });
8451
8814
  }
8452
- function gitSafe(args, cwd) {
8815
+ function gitSafe2(args, cwd) {
8453
8816
  try {
8454
- return git2(args, cwd);
8817
+ return git3(args, cwd);
8455
8818
  } catch {
8456
8819
  return null;
8457
8820
  }
@@ -8459,7 +8822,7 @@ function gitSafe(args, cwd) {
8459
8822
  function checkForConflicts(cwd, baseBranch) {
8460
8823
  const log = getLogger();
8461
8824
  try {
8462
- git2(`fetch origin ${baseBranch}`, cwd);
8825
+ git3(`fetch origin ${baseBranch}`, cwd);
8463
8826
  log.debug("Fetched latest from origin", { baseBranch });
8464
8827
  } catch (e) {
8465
8828
  log.warn(`Could not fetch origin/${baseBranch}: ${e}`);
@@ -8470,8 +8833,8 @@ function checkForConflicts(cwd, baseBranch) {
8470
8833
  newCommits: 0
8471
8834
  };
8472
8835
  }
8473
- const currentBranch = gitSafe("rev-parse --abbrev-ref HEAD", cwd)?.trim() ?? "";
8474
- const mergeBase = gitSafe(`merge-base ${currentBranch} origin/${baseBranch}`, cwd)?.trim();
8836
+ const currentBranch = gitSafe2("rev-parse --abbrev-ref HEAD", cwd)?.trim() ?? "";
8837
+ const mergeBase = gitSafe2(`merge-base ${currentBranch} origin/${baseBranch}`, cwd)?.trim();
8475
8838
  if (!mergeBase) {
8476
8839
  log.debug("Could not find merge base — branches may be unrelated");
8477
8840
  return {
@@ -8481,7 +8844,7 @@ function checkForConflicts(cwd, baseBranch) {
8481
8844
  newCommits: 0
8482
8845
  };
8483
8846
  }
8484
- const remoteTip = gitSafe(`rev-parse origin/${baseBranch}`, cwd)?.trim();
8847
+ const remoteTip = gitSafe2(`rev-parse origin/${baseBranch}`, cwd)?.trim();
8485
8848
  if (!remoteTip || remoteTip === mergeBase) {
8486
8849
  log.debug("Base branch has not advanced");
8487
8850
  return {
@@ -8491,16 +8854,16 @@ function checkForConflicts(cwd, baseBranch) {
8491
8854
  newCommits: 0
8492
8855
  };
8493
8856
  }
8494
- const newCommitsOutput = gitSafe(`rev-list --count ${mergeBase}..origin/${baseBranch}`, cwd)?.trim() ?? "0";
8857
+ const newCommitsOutput = gitSafe2(`rev-list --count ${mergeBase}..origin/${baseBranch}`, cwd)?.trim() ?? "0";
8495
8858
  const newCommits = Number.parseInt(newCommitsOutput, 10);
8496
8859
  log.verbose(`Base branch has ${newCommits} new commits`, {
8497
8860
  baseBranch,
8498
8861
  mergeBase: mergeBase.slice(0, 8),
8499
8862
  remoteTip: remoteTip.slice(0, 8)
8500
8863
  });
8501
- const ourChanges = gitSafe(`diff --name-only ${mergeBase}..HEAD`, cwd)?.trim().split(`
8864
+ const ourChanges = gitSafe2(`diff --name-only ${mergeBase}..HEAD`, cwd)?.trim().split(`
8502
8865
  `).filter(Boolean) ?? [];
8503
- const theirChanges = gitSafe(`diff --name-only ${mergeBase}..origin/${baseBranch}`, cwd)?.trim().split(`
8866
+ const theirChanges = gitSafe2(`diff --name-only ${mergeBase}..origin/${baseBranch}`, cwd)?.trim().split(`
8504
8867
  `).filter(Boolean) ?? [];
8505
8868
  const overlapping = ourChanges.filter((f) => theirChanges.includes(f));
8506
8869
  return {
@@ -8513,18 +8876,19 @@ function checkForConflicts(cwd, baseBranch) {
8513
8876
  function attemptRebase(cwd, baseBranch) {
8514
8877
  const log = getLogger();
8515
8878
  try {
8516
- git2(`rebase origin/${baseBranch}`, cwd);
8879
+ git3(`rebase origin/${baseBranch}`, cwd);
8517
8880
  log.info(`Successfully rebased onto origin/${baseBranch}`);
8881
+ updateSubmodulesAfterRebase(cwd);
8518
8882
  return { success: true };
8519
8883
  } catch (_e) {
8520
8884
  log.warn("Rebase failed, aborting");
8521
8885
  const conflicts = [];
8522
8886
  try {
8523
- const status = git2("diff --name-only --diff-filter=U", cwd);
8887
+ const status = git3("diff --name-only --diff-filter=U", cwd);
8524
8888
  conflicts.push(...status.trim().split(`
8525
8889
  `).filter(Boolean));
8526
8890
  } catch {}
8527
- gitSafe("rebase --abort", cwd);
8891
+ gitSafe2("rebase --abort", cwd);
8528
8892
  return { success: false, conflicts };
8529
8893
  }
8530
8894
  }
@@ -8566,23 +8930,24 @@ ${bold(yellow("⚠"))} Base branch has ${result.newCommits} new commit${result.n
8566
8930
  var init_conflict = __esm(() => {
8567
8931
  init_terminal();
8568
8932
  init_logger();
8933
+ init_submodule();
8569
8934
  });
8570
8935
 
8571
8936
  // src/core/run-state.ts
8572
8937
  import {
8573
- existsSync as existsSync16,
8938
+ existsSync as existsSync18,
8574
8939
  mkdirSync as mkdirSync11,
8575
8940
  readFileSync as readFileSync12,
8576
8941
  unlinkSync as unlinkSync4,
8577
8942
  writeFileSync as writeFileSync8
8578
8943
  } from "node:fs";
8579
- import { dirname as dirname5, join as join16 } from "node:path";
8944
+ import { dirname as dirname5, join as join18 } from "node:path";
8580
8945
  function getRunStatePath(projectRoot) {
8581
- return join16(projectRoot, ".locus", "run-state.json");
8946
+ return join18(projectRoot, ".locus", "run-state.json");
8582
8947
  }
8583
8948
  function loadRunState(projectRoot) {
8584
8949
  const path = getRunStatePath(projectRoot);
8585
- if (!existsSync16(path))
8950
+ if (!existsSync18(path))
8586
8951
  return null;
8587
8952
  try {
8588
8953
  return JSON.parse(readFileSync12(path, "utf-8"));
@@ -8594,7 +8959,7 @@ function loadRunState(projectRoot) {
8594
8959
  function saveRunState(projectRoot, state) {
8595
8960
  const path = getRunStatePath(projectRoot);
8596
8961
  const dir = dirname5(path);
8597
- if (!existsSync16(dir)) {
8962
+ if (!existsSync18(dir)) {
8598
8963
  mkdirSync11(dir, { recursive: true });
8599
8964
  }
8600
8965
  writeFileSync8(path, `${JSON.stringify(state, null, 2)}
@@ -8602,7 +8967,7 @@ function saveRunState(projectRoot, state) {
8602
8967
  }
8603
8968
  function clearRunState(projectRoot) {
8604
8969
  const path = getRunStatePath(projectRoot);
8605
- if (existsSync16(path)) {
8970
+ if (existsSync18(path)) {
8606
8971
  unlinkSync4(path);
8607
8972
  }
8608
8973
  }
@@ -8742,28 +9107,28 @@ var init_shutdown = __esm(() => {
8742
9107
  });
8743
9108
 
8744
9109
  // src/core/worktree.ts
8745
- import { execSync as execSync12 } from "node:child_process";
8746
- import { existsSync as existsSync17, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
8747
- import { join as join17 } from "node:path";
8748
- function git3(args, cwd) {
8749
- return execSync12(`git ${args}`, {
9110
+ import { execSync as execSync13 } from "node:child_process";
9111
+ import { existsSync as existsSync19, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
9112
+ import { join as join19 } from "node:path";
9113
+ function git4(args, cwd) {
9114
+ return execSync13(`git ${args}`, {
8750
9115
  cwd,
8751
9116
  encoding: "utf-8",
8752
9117
  stdio: ["pipe", "pipe", "pipe"]
8753
9118
  });
8754
9119
  }
8755
- function gitSafe2(args, cwd) {
9120
+ function gitSafe3(args, cwd) {
8756
9121
  try {
8757
- return git3(args, cwd);
9122
+ return git4(args, cwd);
8758
9123
  } catch {
8759
9124
  return null;
8760
9125
  }
8761
9126
  }
8762
9127
  function getWorktreeDir(projectRoot) {
8763
- return join17(projectRoot, ".locus", "worktrees");
9128
+ return join19(projectRoot, ".locus", "worktrees");
8764
9129
  }
8765
9130
  function getWorktreePath(projectRoot, issueNumber) {
8766
- return join17(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
9131
+ return join19(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
8767
9132
  }
8768
9133
  function generateBranchName(issueNumber) {
8769
9134
  const randomSuffix = Math.random().toString(36).slice(2, 8);
@@ -8771,7 +9136,7 @@ function generateBranchName(issueNumber) {
8771
9136
  }
8772
9137
  function getWorktreeBranch(worktreePath) {
8773
9138
  try {
8774
- return execSync12("git branch --show-current", {
9139
+ return execSync13("git branch --show-current", {
8775
9140
  cwd: worktreePath,
8776
9141
  encoding: "utf-8",
8777
9142
  stdio: ["pipe", "pipe", "pipe"]
@@ -8783,7 +9148,7 @@ function getWorktreeBranch(worktreePath) {
8783
9148
  function createWorktree(projectRoot, issueNumber, baseBranch) {
8784
9149
  const log = getLogger();
8785
9150
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
8786
- if (existsSync17(worktreePath)) {
9151
+ if (existsSync19(worktreePath)) {
8787
9152
  log.verbose(`Worktree already exists for issue #${issueNumber}`);
8788
9153
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
8789
9154
  return {
@@ -8794,7 +9159,8 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
8794
9159
  };
8795
9160
  }
8796
9161
  const branch = generateBranchName(issueNumber);
8797
- git3(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
9162
+ git4(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
9163
+ initSubmodules(worktreePath);
8798
9164
  log.info(`Created worktree for issue #${issueNumber}`, {
8799
9165
  path: worktreePath,
8800
9166
  branch,
@@ -8810,30 +9176,30 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
8810
9176
  function removeWorktree(projectRoot, issueNumber) {
8811
9177
  const log = getLogger();
8812
9178
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
8813
- if (!existsSync17(worktreePath)) {
9179
+ if (!existsSync19(worktreePath)) {
8814
9180
  log.verbose(`Worktree for issue #${issueNumber} does not exist`);
8815
9181
  return;
8816
9182
  }
8817
9183
  const branch = getWorktreeBranch(worktreePath);
8818
9184
  try {
8819
- git3(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
9185
+ git4(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
8820
9186
  log.info(`Removed worktree for issue #${issueNumber}`);
8821
9187
  } catch (e) {
8822
9188
  log.warn(`Failed to remove worktree: ${e}`);
8823
- gitSafe2(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
9189
+ gitSafe3(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
8824
9190
  }
8825
9191
  if (branch) {
8826
- gitSafe2(`branch -D ${branch}`, projectRoot);
9192
+ gitSafe3(`branch -D ${branch}`, projectRoot);
8827
9193
  }
8828
9194
  }
8829
9195
  function listWorktrees(projectRoot) {
8830
9196
  const log = getLogger();
8831
9197
  const worktreeDir = getWorktreeDir(projectRoot);
8832
- if (!existsSync17(worktreeDir)) {
9198
+ if (!existsSync19(worktreeDir)) {
8833
9199
  return [];
8834
9200
  }
8835
9201
  const entries = readdirSync6(worktreeDir).filter((entry) => entry.startsWith("issue-"));
8836
- const gitWorktreeList = gitSafe2("worktree list --porcelain", projectRoot);
9202
+ const gitWorktreeList = gitSafe3("worktree list --porcelain", projectRoot);
8837
9203
  const activeWorktrees = new Set;
8838
9204
  if (gitWorktreeList) {
8839
9205
  for (const line of gitWorktreeList.split(`
@@ -8849,7 +9215,7 @@ function listWorktrees(projectRoot) {
8849
9215
  if (!match)
8850
9216
  continue;
8851
9217
  const issueNumber = Number.parseInt(match[1], 10);
8852
- const path = join17(worktreeDir, entry);
9218
+ const path = join19(worktreeDir, entry);
8853
9219
  const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
8854
9220
  let resolvedPath;
8855
9221
  try {
@@ -8883,12 +9249,13 @@ function cleanupStaleWorktrees(projectRoot) {
8883
9249
  }
8884
9250
  }
8885
9251
  if (cleaned > 0) {
8886
- gitSafe2("worktree prune", projectRoot);
9252
+ gitSafe3("worktree prune", projectRoot);
8887
9253
  }
8888
9254
  return cleaned;
8889
9255
  }
8890
9256
  var init_worktree = __esm(() => {
8891
9257
  init_logger();
9258
+ init_submodule();
8892
9259
  });
8893
9260
 
8894
9261
  // src/commands/run.ts
@@ -8896,7 +9263,7 @@ var exports_run = {};
8896
9263
  __export(exports_run, {
8897
9264
  runCommand: () => runCommand
8898
9265
  });
8899
- import { execSync as execSync13 } from "node:child_process";
9266
+ import { execSync as execSync14 } from "node:child_process";
8900
9267
  function resolveExecutionContext(config, modelOverride) {
8901
9268
  const model = modelOverride ?? config.ai.model;
8902
9269
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
@@ -9047,7 +9414,7 @@ ${yellow("⚠")} A sprint run is already in progress.
9047
9414
  }
9048
9415
  if (!flags.dryRun) {
9049
9416
  try {
9050
- execSync13(`git checkout -B ${branchName}`, {
9417
+ execSync14(`git checkout -B ${branchName}`, {
9051
9418
  cwd: projectRoot,
9052
9419
  encoding: "utf-8",
9053
9420
  stdio: ["pipe", "pipe", "pipe"]
@@ -9097,7 +9464,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
9097
9464
  let sprintContext;
9098
9465
  if (i > 0 && !flags.dryRun) {
9099
9466
  try {
9100
- sprintContext = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9467
+ sprintContext = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9101
9468
  cwd: projectRoot,
9102
9469
  encoding: "utf-8",
9103
9470
  stdio: ["pipe", "pipe", "pipe"]
@@ -9162,7 +9529,7 @@ ${bold("Summary:")}
9162
9529
  const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
9163
9530
  if (prNumber !== undefined) {
9164
9531
  try {
9165
- execSync13(`git checkout ${config.agent.baseBranch}`, {
9532
+ execSync14(`git checkout ${config.agent.baseBranch}`, {
9166
9533
  cwd: projectRoot,
9167
9534
  encoding: "utf-8",
9168
9535
  stdio: ["pipe", "pipe", "pipe"]
@@ -9177,6 +9544,7 @@ ${bold("Summary:")}
9177
9544
  }
9178
9545
  }
9179
9546
  async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandboxed) {
9547
+ const log = getLogger();
9180
9548
  const execution = resolveExecutionContext(config, flags.model);
9181
9549
  let isSprintIssue = false;
9182
9550
  try {
@@ -9185,7 +9553,7 @@ async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandbo
9185
9553
  } catch {}
9186
9554
  if (isSprintIssue) {
9187
9555
  process.stderr.write(`
9188
- ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential, no worktree)")}
9556
+ ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential)")}
9189
9557
 
9190
9558
  `);
9191
9559
  await executeIssue(projectRoot, {
@@ -9198,42 +9566,48 @@ ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential, n
9198
9566
  });
9199
9567
  return;
9200
9568
  }
9569
+ const randomSuffix = Math.random().toString(36).slice(2, 8);
9570
+ const branchName = `locus/issue-${issueNumber}-${randomSuffix}`;
9201
9571
  process.stderr.write(`
9202
- ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim("(worktree)")}
9572
+ ${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim(`(branch: ${branchName})`)}
9203
9573
 
9204
9574
  `);
9205
- let worktreePath;
9206
9575
  if (!flags.dryRun) {
9207
9576
  try {
9208
- const wt = createWorktree(projectRoot, issueNumber, config.agent.baseBranch);
9209
- worktreePath = wt.path;
9210
- process.stderr.write(` ${dim(`Worktree: ${wt.branch}`)}
9211
-
9212
- `);
9577
+ execSync14(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
9578
+ cwd: projectRoot,
9579
+ encoding: "utf-8",
9580
+ stdio: ["pipe", "pipe", "pipe"]
9581
+ });
9582
+ log.info(`Checked out branch ${branchName}`);
9213
9583
  } catch (e) {
9214
- process.stderr.write(`${yellow("⚠")} Could not create worktree: ${e}
9584
+ process.stderr.write(`${yellow("⚠")} Could not create branch: ${e}
9215
9585
  `);
9216
- process.stderr.write(` ${dim("Falling back to running in project root.")}
9586
+ process.stderr.write(` ${dim("Running on current branch instead.")}
9217
9587
 
9218
9588
  `);
9219
9589
  }
9220
9590
  }
9221
9591
  const result = await executeIssue(projectRoot, {
9222
9592
  issueNumber,
9223
- worktreePath,
9224
9593
  provider: execution.provider,
9225
9594
  model: execution.model,
9226
9595
  dryRun: flags.dryRun,
9227
9596
  sandboxed,
9228
9597
  sandboxName: execution.sandboxName
9229
9598
  });
9230
- if (worktreePath && !flags.dryRun) {
9599
+ if (!flags.dryRun) {
9231
9600
  if (result.success) {
9232
- removeWorktree(projectRoot, issueNumber);
9233
- process.stderr.write(` ${dim("Worktree cleaned up.")}
9234
- `);
9601
+ try {
9602
+ execSync14(`git checkout ${config.agent.baseBranch}`, {
9603
+ cwd: projectRoot,
9604
+ encoding: "utf-8",
9605
+ stdio: ["pipe", "pipe", "pipe"]
9606
+ });
9607
+ log.info(`Checked out ${config.agent.baseBranch}`);
9608
+ } catch {}
9235
9609
  } else {
9236
- process.stderr.write(` ${yellow("⚠")} Worktree preserved for debugging: ${dim(worktreePath)}
9610
+ process.stderr.write(` ${yellow("⚠")} Branch ${dim(branchName)} preserved for debugging.
9237
9611
  `);
9238
9612
  }
9239
9613
  }
@@ -9362,13 +9736,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
9362
9736
  `);
9363
9737
  if (state.type === "sprint" && state.branch) {
9364
9738
  try {
9365
- const currentBranch = execSync13("git rev-parse --abbrev-ref HEAD", {
9739
+ const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
9366
9740
  cwd: projectRoot,
9367
9741
  encoding: "utf-8",
9368
9742
  stdio: ["pipe", "pipe", "pipe"]
9369
9743
  }).trim();
9370
9744
  if (currentBranch !== state.branch) {
9371
- execSync13(`git checkout ${state.branch}`, {
9745
+ execSync14(`git checkout ${state.branch}`, {
9372
9746
  cwd: projectRoot,
9373
9747
  encoding: "utf-8",
9374
9748
  stdio: ["pipe", "pipe", "pipe"]
@@ -9435,7 +9809,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
9435
9809
  const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
9436
9810
  if (prNumber !== undefined) {
9437
9811
  try {
9438
- execSync13(`git checkout ${config.agent.baseBranch}`, {
9812
+ execSync14(`git checkout ${config.agent.baseBranch}`, {
9439
9813
  cwd: projectRoot,
9440
9814
  encoding: "utf-8",
9441
9815
  stdio: ["pipe", "pipe", "pipe"]
@@ -9466,14 +9840,19 @@ function getOrder2(issue) {
9466
9840
  }
9467
9841
  function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
9468
9842
  try {
9469
- const status = execSync13("git status --porcelain", {
9843
+ const committedSubmodules = commitDirtySubmodules(projectRoot, issueNumber, issueTitle);
9844
+ if (committedSubmodules.length > 0) {
9845
+ process.stderr.write(` ${dim(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
9846
+ `);
9847
+ }
9848
+ const status = execSync14("git status --porcelain", {
9470
9849
  cwd: projectRoot,
9471
9850
  encoding: "utf-8",
9472
9851
  stdio: ["pipe", "pipe", "pipe"]
9473
9852
  }).trim();
9474
9853
  if (!status)
9475
9854
  return;
9476
- execSync13("git add -A", {
9855
+ execSync14("git add -A", {
9477
9856
  cwd: projectRoot,
9478
9857
  encoding: "utf-8",
9479
9858
  stdio: ["pipe", "pipe", "pipe"]
@@ -9481,7 +9860,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
9481
9860
  const message = `chore: complete #${issueNumber} - ${issueTitle}
9482
9861
 
9483
9862
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
9484
- execSync13(`git commit -F -`, {
9863
+ execSync14(`git commit -F -`, {
9485
9864
  input: message,
9486
9865
  cwd: projectRoot,
9487
9866
  encoding: "utf-8",
@@ -9495,7 +9874,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
9495
9874
  if (!config.agent.autoPR)
9496
9875
  return;
9497
9876
  try {
9498
- const diff = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9877
+ const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9499
9878
  cwd: projectRoot,
9500
9879
  encoding: "utf-8",
9501
9880
  stdio: ["pipe", "pipe", "pipe"]
@@ -9505,16 +9884,24 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
9505
9884
  `);
9506
9885
  return;
9507
9886
  }
9508
- execSync13(`git push -u origin ${branchName}`, {
9887
+ pushSubmoduleBranches(projectRoot);
9888
+ execSync14(`git push -u origin ${branchName}`, {
9509
9889
  cwd: projectRoot,
9510
9890
  encoding: "utf-8",
9511
9891
  stdio: ["pipe", "pipe", "pipe"]
9512
9892
  });
9513
9893
  const taskLines = tasks.map((t) => `- Closes #${t.issue}${t.title ? `: ${t.title}` : ""}`).join(`
9514
9894
  `);
9515
- const prBody = `## Sprint: ${sprintName}
9895
+ const submoduleSummary = getSubmoduleChangeSummary(projectRoot, config.agent.baseBranch);
9896
+ let prBody = `## Sprint: ${sprintName}
9516
9897
 
9517
- ${taskLines}
9898
+ ${taskLines}`;
9899
+ if (submoduleSummary) {
9900
+ prBody += `
9901
+
9902
+ ${submoduleSummary}`;
9903
+ }
9904
+ prBody += `
9518
9905
 
9519
9906
  ---
9520
9907
 
@@ -9531,8 +9918,8 @@ ${taskLines}
9531
9918
  }
9532
9919
  }
9533
9920
  var init_run = __esm(() => {
9534
- init_ai_models();
9535
9921
  init_agent();
9922
+ init_ai_models();
9536
9923
  init_config();
9537
9924
  init_conflict();
9538
9925
  init_github();
@@ -9541,6 +9928,7 @@ var init_run = __esm(() => {
9541
9928
  init_run_state();
9542
9929
  init_sandbox();
9543
9930
  init_shutdown();
9931
+ init_submodule();
9544
9932
  init_worktree();
9545
9933
  init_progress();
9546
9934
  init_terminal();
@@ -9655,13 +10043,13 @@ __export(exports_plan, {
9655
10043
  parsePlanArgs: () => parsePlanArgs
9656
10044
  });
9657
10045
  import {
9658
- existsSync as existsSync18,
10046
+ existsSync as existsSync20,
9659
10047
  mkdirSync as mkdirSync12,
9660
10048
  readdirSync as readdirSync7,
9661
10049
  readFileSync as readFileSync13,
9662
10050
  writeFileSync as writeFileSync9
9663
10051
  } from "node:fs";
9664
- import { join as join18 } from "node:path";
10052
+ import { join as join20 } from "node:path";
9665
10053
  function printHelp() {
9666
10054
  process.stderr.write(`
9667
10055
  ${bold("locus plan")} — AI-powered sprint planning
@@ -9692,11 +10080,11 @@ function normalizeSprintName(name) {
9692
10080
  return name.trim().toLowerCase();
9693
10081
  }
9694
10082
  function getPlansDir(projectRoot) {
9695
- return join18(projectRoot, ".locus", "plans");
10083
+ return join20(projectRoot, ".locus", "plans");
9696
10084
  }
9697
10085
  function ensurePlansDir(projectRoot) {
9698
10086
  const dir = getPlansDir(projectRoot);
9699
- if (!existsSync18(dir)) {
10087
+ if (!existsSync20(dir)) {
9700
10088
  mkdirSync12(dir, { recursive: true });
9701
10089
  }
9702
10090
  return dir;
@@ -9706,14 +10094,14 @@ function generateId() {
9706
10094
  }
9707
10095
  function loadPlanFile(projectRoot, id) {
9708
10096
  const dir = getPlansDir(projectRoot);
9709
- if (!existsSync18(dir))
10097
+ if (!existsSync20(dir))
9710
10098
  return null;
9711
10099
  const files = readdirSync7(dir).filter((f) => f.endsWith(".json"));
9712
10100
  const match = files.find((f) => f.startsWith(id));
9713
10101
  if (!match)
9714
10102
  return null;
9715
10103
  try {
9716
- const content = readFileSync13(join18(dir, match), "utf-8");
10104
+ const content = readFileSync13(join20(dir, match), "utf-8");
9717
10105
  return JSON.parse(content);
9718
10106
  } catch {
9719
10107
  return null;
@@ -9759,7 +10147,7 @@ async function planCommand(projectRoot, args, flags = {}) {
9759
10147
  }
9760
10148
  function handleListPlans(projectRoot) {
9761
10149
  const dir = getPlansDir(projectRoot);
9762
- if (!existsSync18(dir)) {
10150
+ if (!existsSync20(dir)) {
9763
10151
  process.stderr.write(`${dim("No saved plans yet.")}
9764
10152
  `);
9765
10153
  return;
@@ -9777,7 +10165,7 @@ ${bold("Saved Plans:")}
9777
10165
  for (const file of files) {
9778
10166
  const id = file.replace(".json", "");
9779
10167
  try {
9780
- const content = readFileSync13(join18(dir, file), "utf-8");
10168
+ const content = readFileSync13(join20(dir, file), "utf-8");
9781
10169
  const plan = JSON.parse(content);
9782
10170
  const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
9783
10171
  const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
@@ -9888,7 +10276,7 @@ ${bold("Approving plan:")}
9888
10276
  async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
9889
10277
  const id = generateId();
9890
10278
  const plansDir = ensurePlansDir(projectRoot);
9891
- const planPath = join18(plansDir, `${id}.json`);
10279
+ const planPath = join20(plansDir, `${id}.json`);
9892
10280
  const planPathRelative = `.locus/plans/${id}.json`;
9893
10281
  const displayDirective = directive;
9894
10282
  process.stderr.write(`
@@ -9922,7 +10310,7 @@ ${red("✗")} Planning failed: ${aiResult.error}
9922
10310
  `);
9923
10311
  return;
9924
10312
  }
9925
- if (!existsSync18(planPath)) {
10313
+ if (!existsSync20(planPath)) {
9926
10314
  process.stderr.write(`
9927
10315
  ${yellow("⚠")} Plan file was not created at ${bold(planPathRelative)}.
9928
10316
  `);
@@ -10095,15 +10483,15 @@ ${directive}${sprintName ? `
10095
10483
 
10096
10484
  **Sprint:** ${sprintName}` : ""}
10097
10485
  </directive>`);
10098
- const locusPath = join18(projectRoot, ".locus", "LOCUS.md");
10099
- if (existsSync18(locusPath)) {
10486
+ const locusPath = join20(projectRoot, ".locus", "LOCUS.md");
10487
+ if (existsSync20(locusPath)) {
10100
10488
  const content = readFileSync13(locusPath, "utf-8");
10101
10489
  parts.push(`<project-context>
10102
10490
  ${content.slice(0, 3000)}
10103
10491
  </project-context>`);
10104
10492
  }
10105
- const learningsPath = join18(projectRoot, ".locus", "LEARNINGS.md");
10106
- if (existsSync18(learningsPath)) {
10493
+ const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
10494
+ if (existsSync20(learningsPath)) {
10107
10495
  const content = readFileSync13(learningsPath, "utf-8");
10108
10496
  parts.push(`<past-learnings>
10109
10497
  ${content.slice(0, 2000)}
@@ -10274,8 +10662,8 @@ var init_plan = __esm(() => {
10274
10662
  init_run_ai();
10275
10663
  init_config();
10276
10664
  init_github();
10277
- init_terminal();
10278
10665
  init_sandbox();
10666
+ init_terminal();
10279
10667
  });
10280
10668
 
10281
10669
  // src/commands/review.ts
@@ -10283,9 +10671,9 @@ var exports_review = {};
10283
10671
  __export(exports_review, {
10284
10672
  reviewCommand: () => reviewCommand
10285
10673
  });
10286
- import { execSync as execSync14 } from "node:child_process";
10287
- import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
10288
- import { join as join19 } from "node:path";
10674
+ import { execSync as execSync15 } from "node:child_process";
10675
+ import { existsSync as existsSync21, readFileSync as readFileSync14 } from "node:fs";
10676
+ import { join as join21 } from "node:path";
10289
10677
  function printHelp2() {
10290
10678
  process.stderr.write(`
10291
10679
  ${bold("locus review")} — AI-powered code review
@@ -10361,7 +10749,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
10361
10749
  async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
10362
10750
  let prInfo;
10363
10751
  try {
10364
- 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"] });
10752
+ 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"] });
10365
10753
  const raw = JSON.parse(result);
10366
10754
  prInfo = {
10367
10755
  number: raw.number,
@@ -10427,7 +10815,7 @@ ${output.slice(0, 60000)}
10427
10815
 
10428
10816
  ---
10429
10817
  _Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
10430
- execSync14(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10818
+ execSync15(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10431
10819
  process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
10432
10820
  `);
10433
10821
  } catch (e) {
@@ -10445,8 +10833,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
10445
10833
  parts.push(`<role>
10446
10834
  You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
10447
10835
  </role>`);
10448
- const locusPath = join19(projectRoot, ".locus", "LOCUS.md");
10449
- if (existsSync19(locusPath)) {
10836
+ const locusPath = join21(projectRoot, ".locus", "LOCUS.md");
10837
+ if (existsSync21(locusPath)) {
10450
10838
  const content = readFileSync14(locusPath, "utf-8");
10451
10839
  parts.push(`<project-context>
10452
10840
  ${content.slice(0, 2000)}
@@ -10507,7 +10895,7 @@ var exports_iterate = {};
10507
10895
  __export(exports_iterate, {
10508
10896
  iterateCommand: () => iterateCommand
10509
10897
  });
10510
- import { execSync as execSync15 } from "node:child_process";
10898
+ import { execSync as execSync16 } from "node:child_process";
10511
10899
  function printHelp3() {
10512
10900
  process.stderr.write(`
10513
10901
  ${bold("locus iterate")} — Re-execute tasks with PR feedback
@@ -10717,12 +11105,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
10717
11105
  }
10718
11106
  function findPRForIssue(projectRoot, issueNumber) {
10719
11107
  try {
10720
- const result = execSync15(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11108
+ const result = execSync16(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10721
11109
  const parsed = JSON.parse(result);
10722
11110
  if (parsed.length > 0) {
10723
11111
  return parsed[0].number;
10724
11112
  }
10725
- const branchResult = execSync15(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11113
+ const branchResult = execSync16(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10726
11114
  const branchParsed = JSON.parse(branchResult);
10727
11115
  if (branchParsed.length > 0) {
10728
11116
  return branchParsed[0].number;
@@ -10757,14 +11145,14 @@ __export(exports_discuss, {
10757
11145
  discussCommand: () => discussCommand
10758
11146
  });
10759
11147
  import {
10760
- existsSync as existsSync20,
11148
+ existsSync as existsSync22,
10761
11149
  mkdirSync as mkdirSync13,
10762
11150
  readdirSync as readdirSync8,
10763
11151
  readFileSync as readFileSync15,
10764
11152
  unlinkSync as unlinkSync5,
10765
11153
  writeFileSync as writeFileSync10
10766
11154
  } from "node:fs";
10767
- import { join as join20 } from "node:path";
11155
+ import { join as join22 } from "node:path";
10768
11156
  function printHelp4() {
10769
11157
  process.stderr.write(`
10770
11158
  ${bold("locus discuss")} — AI-powered architectural discussions
@@ -10786,11 +11174,11 @@ ${bold("Examples:")}
10786
11174
  `);
10787
11175
  }
10788
11176
  function getDiscussionsDir(projectRoot) {
10789
- return join20(projectRoot, ".locus", "discussions");
11177
+ return join22(projectRoot, ".locus", "discussions");
10790
11178
  }
10791
11179
  function ensureDiscussionsDir(projectRoot) {
10792
11180
  const dir = getDiscussionsDir(projectRoot);
10793
- if (!existsSync20(dir)) {
11181
+ if (!existsSync22(dir)) {
10794
11182
  mkdirSync13(dir, { recursive: true });
10795
11183
  }
10796
11184
  return dir;
@@ -10825,7 +11213,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
10825
11213
  }
10826
11214
  function listDiscussions(projectRoot) {
10827
11215
  const dir = getDiscussionsDir(projectRoot);
10828
- if (!existsSync20(dir)) {
11216
+ if (!existsSync22(dir)) {
10829
11217
  process.stderr.write(`${dim("No discussions yet.")}
10830
11218
  `);
10831
11219
  return;
@@ -10842,7 +11230,7 @@ ${bold("Discussions:")}
10842
11230
  `);
10843
11231
  for (const file of files) {
10844
11232
  const id = file.replace(".md", "");
10845
- const content = readFileSync15(join20(dir, file), "utf-8");
11233
+ const content = readFileSync15(join22(dir, file), "utf-8");
10846
11234
  const titleMatch = content.match(/^#\s+(.+)/m);
10847
11235
  const title = titleMatch ? titleMatch[1] : id;
10848
11236
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
@@ -10860,7 +11248,7 @@ function showDiscussion(projectRoot, id) {
10860
11248
  return;
10861
11249
  }
10862
11250
  const dir = getDiscussionsDir(projectRoot);
10863
- if (!existsSync20(dir)) {
11251
+ if (!existsSync22(dir)) {
10864
11252
  process.stderr.write(`${red("✗")} No discussions found.
10865
11253
  `);
10866
11254
  return;
@@ -10872,7 +11260,7 @@ function showDiscussion(projectRoot, id) {
10872
11260
  `);
10873
11261
  return;
10874
11262
  }
10875
- const content = readFileSync15(join20(dir, match), "utf-8");
11263
+ const content = readFileSync15(join22(dir, match), "utf-8");
10876
11264
  process.stdout.write(`${content}
10877
11265
  `);
10878
11266
  }
@@ -10883,7 +11271,7 @@ function deleteDiscussion(projectRoot, id) {
10883
11271
  return;
10884
11272
  }
10885
11273
  const dir = getDiscussionsDir(projectRoot);
10886
- if (!existsSync20(dir)) {
11274
+ if (!existsSync22(dir)) {
10887
11275
  process.stderr.write(`${red("✗")} No discussions found.
10888
11276
  `);
10889
11277
  return;
@@ -10895,7 +11283,7 @@ function deleteDiscussion(projectRoot, id) {
10895
11283
  `);
10896
11284
  return;
10897
11285
  }
10898
- unlinkSync5(join20(dir, match));
11286
+ unlinkSync5(join22(dir, match));
10899
11287
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
10900
11288
  `);
10901
11289
  }
@@ -10908,7 +11296,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
10908
11296
  return;
10909
11297
  }
10910
11298
  const dir = getDiscussionsDir(projectRoot);
10911
- if (!existsSync20(dir)) {
11299
+ if (!existsSync22(dir)) {
10912
11300
  process.stderr.write(`${red("✗")} No discussions found.
10913
11301
  `);
10914
11302
  return;
@@ -10920,7 +11308,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
10920
11308
  `);
10921
11309
  return;
10922
11310
  }
10923
- const content = readFileSync15(join20(dir, match), "utf-8");
11311
+ const content = readFileSync15(join22(dir, match), "utf-8");
10924
11312
  const titleMatch = content.match(/^#\s+(.+)/m);
10925
11313
  const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
10926
11314
  await planCommand(projectRoot, [
@@ -11034,7 +11422,7 @@ ${turn.content}`;
11034
11422
  ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
11035
11423
  ].join(`
11036
11424
  `);
11037
- writeFileSync10(join20(dir, `${id}.md`), markdown, "utf-8");
11425
+ writeFileSync10(join22(dir, `${id}.md`), markdown, "utf-8");
11038
11426
  process.stderr.write(`
11039
11427
  ${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
11040
11428
  `);
@@ -11049,15 +11437,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
11049
11437
  parts.push(`<role>
11050
11438
  You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
11051
11439
  </role>`);
11052
- const locusPath = join20(projectRoot, ".locus", "LOCUS.md");
11053
- if (existsSync20(locusPath)) {
11440
+ const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
11441
+ if (existsSync22(locusPath)) {
11054
11442
  const content = readFileSync15(locusPath, "utf-8");
11055
11443
  parts.push(`<project-context>
11056
11444
  ${content.slice(0, 3000)}
11057
11445
  </project-context>`);
11058
11446
  }
11059
- const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
11060
- if (existsSync20(learningsPath)) {
11447
+ const learningsPath = join22(projectRoot, ".locus", "LEARNINGS.md");
11448
+ if (existsSync22(learningsPath)) {
11061
11449
  const content = readFileSync15(learningsPath, "utf-8");
11062
11450
  parts.push(`<past-learnings>
11063
11451
  ${content.slice(0, 2000)}
@@ -11129,8 +11517,8 @@ __export(exports_artifacts, {
11129
11517
  formatDate: () => formatDate2,
11130
11518
  artifactsCommand: () => artifactsCommand
11131
11519
  });
11132
- import { existsSync as existsSync21, readdirSync as readdirSync9, readFileSync as readFileSync16, statSync as statSync4 } from "node:fs";
11133
- import { join as join21 } from "node:path";
11520
+ import { existsSync as existsSync23, readdirSync as readdirSync9, readFileSync as readFileSync16, statSync as statSync4 } from "node:fs";
11521
+ import { join as join23 } from "node:path";
11134
11522
  function printHelp5() {
11135
11523
  process.stderr.write(`
11136
11524
  ${bold("locus artifacts")} — View and manage AI-generated artifacts
@@ -11150,14 +11538,14 @@ ${dim("Artifact names support partial matching.")}
11150
11538
  `);
11151
11539
  }
11152
11540
  function getArtifactsDir(projectRoot) {
11153
- return join21(projectRoot, ".locus", "artifacts");
11541
+ return join23(projectRoot, ".locus", "artifacts");
11154
11542
  }
11155
11543
  function listArtifacts(projectRoot) {
11156
11544
  const dir = getArtifactsDir(projectRoot);
11157
- if (!existsSync21(dir))
11545
+ if (!existsSync23(dir))
11158
11546
  return [];
11159
11547
  return readdirSync9(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
11160
- const filePath = join21(dir, fileName);
11548
+ const filePath = join23(dir, fileName);
11161
11549
  const stat = statSync4(filePath);
11162
11550
  return {
11163
11551
  name: fileName.replace(/\.md$/, ""),
@@ -11170,8 +11558,8 @@ function listArtifacts(projectRoot) {
11170
11558
  function readArtifact(projectRoot, name) {
11171
11559
  const dir = getArtifactsDir(projectRoot);
11172
11560
  const fileName = name.endsWith(".md") ? name : `${name}.md`;
11173
- const filePath = join21(dir, fileName);
11174
- if (!existsSync21(filePath))
11561
+ const filePath = join23(dir, fileName);
11562
+ if (!existsSync23(filePath))
11175
11563
  return null;
11176
11564
  const stat = statSync4(filePath);
11177
11565
  return {
@@ -11341,10 +11729,10 @@ __export(exports_sandbox2, {
11341
11729
  parseSandboxInstallArgs: () => parseSandboxInstallArgs,
11342
11730
  parseSandboxExecArgs: () => parseSandboxExecArgs
11343
11731
  });
11344
- import { execSync as execSync16, spawn as spawn6 } from "node:child_process";
11732
+ import { execSync as execSync17, spawn as spawn6 } from "node:child_process";
11345
11733
  import { createHash } from "node:crypto";
11346
- import { existsSync as existsSync22, readFileSync as readFileSync17 } from "node:fs";
11347
- import { basename as basename4, join as join22 } from "node:path";
11734
+ import { existsSync as existsSync24, readFileSync as readFileSync17 } from "node:fs";
11735
+ import { basename as basename4, join as join24 } from "node:path";
11348
11736
  function printSandboxHelp() {
11349
11737
  process.stderr.write(`
11350
11738
  ${bold("locus sandbox")} — Manage Docker sandbox lifecycle
@@ -11520,7 +11908,7 @@ function handleRemove(projectRoot) {
11520
11908
  process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
11521
11909
  `);
11522
11910
  try {
11523
- execSync16(`docker sandbox rm ${sandboxName}`, {
11911
+ execSync17(`docker sandbox rm ${sandboxName}`, {
11524
11912
  encoding: "utf-8",
11525
11913
  stdio: ["pipe", "pipe", "pipe"],
11526
11914
  timeout: 15000
@@ -11794,7 +12182,7 @@ async function handleLogs(projectRoot, args) {
11794
12182
  }
11795
12183
  function detectPackageManager(projectRoot) {
11796
12184
  try {
11797
- const raw = readFileSync17(join22(projectRoot, "package.json"), "utf-8");
12185
+ const raw = readFileSync17(join24(projectRoot, "package.json"), "utf-8");
11798
12186
  const pkgJson = JSON.parse(raw);
11799
12187
  if (typeof pkgJson.packageManager === "string") {
11800
12188
  const name = pkgJson.packageManager.split("@")[0];
@@ -11803,13 +12191,13 @@ function detectPackageManager(projectRoot) {
11803
12191
  }
11804
12192
  }
11805
12193
  } catch {}
11806
- if (existsSync22(join22(projectRoot, "bun.lock")) || existsSync22(join22(projectRoot, "bun.lockb"))) {
12194
+ if (existsSync24(join24(projectRoot, "bun.lock")) || existsSync24(join24(projectRoot, "bun.lockb"))) {
11807
12195
  return "bun";
11808
12196
  }
11809
- if (existsSync22(join22(projectRoot, "yarn.lock"))) {
12197
+ if (existsSync24(join24(projectRoot, "yarn.lock"))) {
11810
12198
  return "yarn";
11811
12199
  }
11812
- if (existsSync22(join22(projectRoot, "pnpm-lock.yaml"))) {
12200
+ if (existsSync24(join24(projectRoot, "pnpm-lock.yaml"))) {
11813
12201
  return "pnpm";
11814
12202
  }
11815
12203
  return "npm";
@@ -11827,31 +12215,39 @@ function getInstallCommand(pm) {
11827
12215
  }
11828
12216
  }
11829
12217
  async function runSandboxSetup(sandboxName, projectRoot) {
11830
- const pm = detectPackageManager(projectRoot);
11831
- if (pm !== "npm") {
11832
- await ensurePackageManagerInSandbox(sandboxName, pm);
11833
- }
11834
- const installCmd = getInstallCommand(pm);
11835
- process.stderr.write(`
12218
+ const ecosystem = detectProjectEcosystem(projectRoot);
12219
+ const isJS = isJavaScriptEcosystem(ecosystem);
12220
+ if (isJS) {
12221
+ const pm = detectPackageManager(projectRoot);
12222
+ if (pm !== "npm") {
12223
+ await ensurePackageManagerInSandbox(sandboxName, pm);
12224
+ }
12225
+ const installCmd = getInstallCommand(pm);
12226
+ process.stderr.write(`
11836
12227
  Installing dependencies (${bold(installCmd.join(" "))}) in sandbox ${dim(sandboxName)}...
11837
12228
  `);
11838
- const installOk = await runInteractiveCommand("docker", [
11839
- "sandbox",
11840
- "exec",
11841
- "-w",
11842
- projectRoot,
11843
- sandboxName,
11844
- ...installCmd
11845
- ]);
11846
- if (!installOk) {
11847
- process.stderr.write(`${red("✗")} Dependency install failed in sandbox ${dim(sandboxName)}.
12229
+ const installOk = await runInteractiveCommand("docker", [
12230
+ "sandbox",
12231
+ "exec",
12232
+ "-w",
12233
+ projectRoot,
12234
+ sandboxName,
12235
+ ...installCmd
12236
+ ]);
12237
+ if (!installOk) {
12238
+ process.stderr.write(`${red("✗")} Dependency install failed in sandbox ${dim(sandboxName)}.
11848
12239
  `);
11849
- return false;
11850
- }
11851
- process.stderr.write(`${green("✓")} Dependencies installed in sandbox ${dim(sandboxName)}.
12240
+ return false;
12241
+ }
12242
+ process.stderr.write(`${green("✓")} Dependencies installed in sandbox ${dim(sandboxName)}.
12243
+ `);
12244
+ } else {
12245
+ process.stderr.write(`
12246
+ ${dim(`Detected ${ecosystem} project — skipping JS package install.`)}
11852
12247
  `);
11853
- const setupScript = join22(projectRoot, ".locus", "sandbox-setup.sh");
11854
- if (existsSync22(setupScript)) {
12248
+ }
12249
+ const setupScript = join24(projectRoot, ".locus", "sandbox-setup.sh");
12250
+ if (existsSync24(setupScript)) {
11855
12251
  process.stderr.write(`Running ${bold(".locus/sandbox-setup.sh")} in sandbox ${dim(sandboxName)}...
11856
12252
  `);
11857
12253
  const hookOk = await runInteractiveCommand("docker", [
@@ -11867,6 +12263,11 @@ Installing dependencies (${bold(installCmd.join(" "))}) in sandbox ${dim(sandbox
11867
12263
  process.stderr.write(`${yellow("⚠")} Setup hook failed in sandbox ${dim(sandboxName)}.
11868
12264
  `);
11869
12265
  }
12266
+ } else if (!isJS) {
12267
+ process.stderr.write(`${yellow("⚠")} No ${bold(".locus/sandbox-setup.sh")} found. Create one to install ${ecosystem} toolchain in the sandbox.
12268
+ `);
12269
+ process.stderr.write(` Re-run ${cyan("locus init")} to auto-generate a template, or create it manually.
12270
+ `);
11870
12271
  }
11871
12272
  return true;
11872
12273
  }
@@ -11934,7 +12335,7 @@ function runInteractiveCommand(command, args) {
11934
12335
  }
11935
12336
  async function createProviderSandbox(provider, sandboxName, projectRoot) {
11936
12337
  try {
11937
- execSync16(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
12338
+ execSync17(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
11938
12339
  stdio: ["pipe", "pipe", "pipe"],
11939
12340
  timeout: 120000
11940
12341
  });
@@ -11950,7 +12351,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
11950
12351
  }
11951
12352
  async function ensurePackageManagerInSandbox(sandboxName, pm) {
11952
12353
  try {
11953
- execSync16(`docker sandbox exec ${sandboxName} which ${pm}`, {
12354
+ execSync17(`docker sandbox exec ${sandboxName} which ${pm}`, {
11954
12355
  stdio: ["pipe", "pipe", "pipe"],
11955
12356
  timeout: 5000
11956
12357
  });
@@ -11959,7 +12360,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
11959
12360
  process.stderr.write(`Installing ${bold(pm)} in sandbox...
11960
12361
  `);
11961
12362
  try {
11962
- execSync16(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
12363
+ execSync17(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
11963
12364
  stdio: "inherit",
11964
12365
  timeout: 120000
11965
12366
  });
@@ -11971,7 +12372,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
11971
12372
  }
11972
12373
  async function ensureCodexInSandbox(sandboxName) {
11973
12374
  try {
11974
- execSync16(`docker sandbox exec ${sandboxName} which codex`, {
12375
+ execSync17(`docker sandbox exec ${sandboxName} which codex`, {
11975
12376
  stdio: ["pipe", "pipe", "pipe"],
11976
12377
  timeout: 5000
11977
12378
  });
@@ -11979,7 +12380,7 @@ async function ensureCodexInSandbox(sandboxName) {
11979
12380
  process.stderr.write(`Installing codex in sandbox...
11980
12381
  `);
11981
12382
  try {
11982
- execSync16(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
12383
+ execSync17(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
11983
12384
  } catch {
11984
12385
  process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
11985
12386
  `);
@@ -11988,7 +12389,7 @@ async function ensureCodexInSandbox(sandboxName) {
11988
12389
  }
11989
12390
  function isSandboxAlive(name) {
11990
12391
  try {
11991
- const output = execSync16("docker sandbox ls", {
12392
+ const output = execSync17("docker sandbox ls", {
11992
12393
  encoding: "utf-8",
11993
12394
  stdio: ["pipe", "pipe", "pipe"],
11994
12395
  timeout: 5000
@@ -12001,6 +12402,7 @@ function isSandboxAlive(name) {
12001
12402
  var PROVIDERS;
12002
12403
  var init_sandbox2 = __esm(() => {
12003
12404
  init_config();
12405
+ init_ecosystem();
12004
12406
  init_sandbox();
12005
12407
  init_sandbox_ignore();
12006
12408
  init_terminal();
@@ -12013,13 +12415,13 @@ init_context();
12013
12415
  init_logger();
12014
12416
  init_rate_limiter();
12015
12417
  init_terminal();
12016
- import { existsSync as existsSync23, readFileSync as readFileSync18 } from "node:fs";
12017
- import { join as join23 } from "node:path";
12418
+ import { existsSync as existsSync25, readFileSync as readFileSync18 } from "node:fs";
12419
+ import { join as join25 } from "node:path";
12018
12420
  import { fileURLToPath } from "node:url";
12019
12421
  function getCliVersion() {
12020
12422
  const fallbackVersion = "0.0.0";
12021
- const packageJsonPath = join23(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
12022
- if (!existsSync23(packageJsonPath)) {
12423
+ const packageJsonPath = join25(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
12424
+ if (!existsSync25(packageJsonPath)) {
12023
12425
  return fallbackVersion;
12024
12426
  }
12025
12427
  try {
@@ -12284,7 +12686,7 @@ async function main() {
12284
12686
  try {
12285
12687
  const root = getGitRoot(cwd);
12286
12688
  if (isInitialized(root)) {
12287
- logDir = join23(root, ".locus", "logs");
12689
+ logDir = join25(root, ".locus", "logs");
12288
12690
  getRateLimiter(root);
12289
12691
  }
12290
12692
  } catch {}