@locusai/cli 0.19.1 → 0.20.0

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 +307 -88
  2. package/package.json +1 -1
package/bin/locus.js CHANGED
@@ -280,7 +280,7 @@ function drawBox(lines, options) {
280
280
  return parts.join(`
281
281
  `);
282
282
  }
283
- var cachedCapabilities = null, enabled = () => getCapabilities().colorBasic, bold, dim, italic, underline, strikethrough, red, green, yellow, blue, magenta, cyan, white, gray, redBright, greenBright, yellowBright, blueBright, magentaBright, cyanBright, bgRed, bgGreen, bgYellow, bgBlue, box;
283
+ var cachedCapabilities = null, enabled = () => getCapabilities().colorBasic, bold, dim, italic, underline, strikethrough, red, green, yellow, blue, magenta, cyan, white, gray, redBright, greenBright, yellowBright, blueBright, magentaBright, cyanBright, bgRed, bgGreen, bgYellow, bgBlue, bgGray, box;
284
284
  var init_terminal = __esm(() => {
285
285
  bold = wrap("\x1B[1m", "\x1B[22m");
286
286
  dim = wrap("\x1B[2m", "\x1B[22m");
@@ -305,6 +305,7 @@ var init_terminal = __esm(() => {
305
305
  bgGreen = wrap("\x1B[42m", "\x1B[49m");
306
306
  bgYellow = wrap("\x1B[43m", "\x1B[49m");
307
307
  bgBlue = wrap("\x1B[44m", "\x1B[49m");
308
+ bgGray = wrap("\x1B[100m", "\x1B[49m");
308
309
  box = {
309
310
  topLeft: "┌",
310
311
  topRight: "┐",
@@ -1714,6 +1715,8 @@ ${bold("Initializing Locus...")}
1714
1715
  config.sprint = { ...config.sprint, ...existing.sprint };
1715
1716
  if (existing.logging)
1716
1717
  config.logging = { ...config.logging, ...existing.logging };
1718
+ if (existing.sandbox)
1719
+ config.sandbox = { ...config.sandbox, ...existing.sandbox };
1717
1720
  } catch {}
1718
1721
  process.stderr.write(`${green("✓")} Updated config.json (preserved existing settings)
1719
1722
  `);
@@ -1817,14 +1820,27 @@ ${bold("Sandbox mode")} ${dim("(recommended)")}
1817
1820
  }
1818
1821
  var LOCUS_MD_TEMPLATE = `## Planning First
1819
1822
 
1820
- Complex tasks must be planned before writing code. Create ".locus/plans/<task-name>.md" with:
1823
+ **Before writing any code** for complex or multi-step tasks, you **must** create a plan file at \`.locus/plans/<task-name>.md\`. Do NOT skip this step — write the plan file to disk first, then execute.
1824
+
1825
+ **Plan file structure:**
1821
1826
  - **Goal**: What we're trying to achieve and why
1822
1827
  - **Approach**: Step-by-step strategy with technical decisions
1823
1828
  - **Affected files**: List of files to create/modify/delete
1824
1829
  - **Acceptance criteria**: Specific, testable conditions for completion
1825
1830
  - **Dependencies**: Required packages, APIs, or external services
1826
1831
 
1827
- Delete the planning .md files after successful execution.
1832
+ **When to plan:**
1833
+ - Tasks that touch 3+ files
1834
+ - New features or architectural changes
1835
+ - Tasks with ambiguous requirements that need decomposition
1836
+ - Any task where multiple approaches exist
1837
+
1838
+ **When you can skip planning:**
1839
+ - Single-file bug fixes with obvious root cause
1840
+ - Typo corrections, comment updates, or trivial changes
1841
+ - Tasks with very specific, step-by-step instructions already provided
1842
+
1843
+ Delete the planning \`.md\` files after successful execution.
1828
1844
 
1829
1845
  ## Code Quality
1830
1846
 
@@ -1834,6 +1850,26 @@ Delete the planning .md files after successful execution.
1834
1850
  - **Test as you go**: If tests exist, run relevant ones. If breaking changes occur, update tests accordingly
1835
1851
  - **Comment complex logic**: Explain *why*, not *what*. Focus on business logic and non-obvious decisions
1836
1852
 
1853
+ ## Parallel Execution with Subagents
1854
+
1855
+ Use the **Task tool** to launch subagents for parallelizing independent work. Subagents run autonomously and return results when done.
1856
+
1857
+ **When to use subagents:**
1858
+ - **Codebase exploration**: Use \`subagent_type: "Explore"\` to search for files, patterns, or understand architecture across multiple locations simultaneously
1859
+ - **Independent research**: Launch multiple explore agents in parallel when you need to understand different parts of the codebase at once
1860
+ - **Complex multi-area changes**: When a task touches unrelated areas, use explore agents to gather context from each area in parallel before making changes
1861
+
1862
+ **How to use:**
1863
+ - Specify \`subagent_type\` — use \`"Explore"\` for codebase research, \`"general-purpose"\` for multi-step autonomous tasks
1864
+ - Launch multiple agents in a **single message** to run them concurrently
1865
+ - Provide clear, detailed prompts so agents can work autonomously
1866
+ - Do NOT duplicate work — if you delegate research to a subagent, wait for results instead of searching yourself
1867
+
1868
+ **When NOT to use subagents:**
1869
+ - Simple, directed searches (use Glob or Grep directly)
1870
+ - Reading a specific known file (use Read directly)
1871
+ - Tasks that require sequential steps where each depends on the previous
1872
+
1837
1873
  ## Artifacts
1838
1874
 
1839
1875
  When a task produces knowledge, analysis, or research output rather than (or in addition to) code changes, you **must** save results as Markdown in ".locus/artifacts/<descriptive-name>.md":
@@ -1990,7 +2026,8 @@ import {
1990
2026
  import { homedir as homedir2 } from "node:os";
1991
2027
  import { join as join6 } from "node:path";
1992
2028
  function getPackagesDir() {
1993
- const dir = join6(homedir2(), ".locus", "packages");
2029
+ const home = process.env.HOME || homedir2();
2030
+ const dir = join6(home, ".locus", "packages");
1994
2031
  if (!existsSync6(dir)) {
1995
2032
  mkdirSync6(dir, { recursive: true });
1996
2033
  }
@@ -2903,6 +2940,8 @@ class StreamRenderer {
2903
2940
  renderTimer = null;
2904
2941
  catchUpMode = false;
2905
2942
  totalLinesRendered = 0;
2943
+ isFirstLine = true;
2944
+ hasContent = false;
2906
2945
  onRender;
2907
2946
  constructor(onRender) {
2908
2947
  this.onRender = onRender ?? ((line) => process.stdout.write(`${line}\r
@@ -2918,6 +2957,12 @@ class StreamRenderer {
2918
2957
  }
2919
2958
  push(text) {
2920
2959
  this.buffer += text;
2960
+ if (!this.hasContent) {
2961
+ this.buffer = this.buffer.replace(/^\n+/, "");
2962
+ if (this.buffer.length === 0)
2963
+ return;
2964
+ this.hasContent = true;
2965
+ }
2921
2966
  const lines = this.buffer.split(`
2922
2967
  `);
2923
2968
  this.buffer = lines.pop() ?? "";
@@ -2930,6 +2975,9 @@ class StreamRenderer {
2930
2975
  clearInterval(this.renderTimer);
2931
2976
  this.renderTimer = null;
2932
2977
  }
2978
+ while (this.lineQueue.length > 0 && this.lineQueue[this.lineQueue.length - 1]?.trim() === "") {
2979
+ this.lineQueue.pop();
2980
+ }
2933
2981
  while (this.lineQueue.length > 0) {
2934
2982
  const line = this.lineQueue.shift();
2935
2983
  if (line !== undefined)
@@ -2937,8 +2985,8 @@ class StreamRenderer {
2937
2985
  }
2938
2986
  if (this.buffer.trim()) {
2939
2987
  this.renderLine(this.buffer);
2940
- this.buffer = "";
2941
2988
  }
2989
+ this.buffer = "";
2942
2990
  this.inTable = false;
2943
2991
  }
2944
2992
  getLinesRendered() {
@@ -2965,7 +3013,10 @@ class StreamRenderer {
2965
3013
  }
2966
3014
  }
2967
3015
  renderLine(raw) {
2968
- const formatted = this.formatMarkdown(raw);
3016
+ const prefix = this.isFirstLine ? `${dim("●")} ` : " ";
3017
+ if (this.isFirstLine)
3018
+ this.isFirstLine = false;
3019
+ const formatted = `${prefix}${this.formatMarkdown(raw)}`;
2969
3020
  beginSync();
2970
3021
  this.onRender(formatted);
2971
3022
  endSync();
@@ -3016,7 +3067,7 @@ class StreamRenderer {
3016
3067
  result = result.replace(/\*\*([^*]+)\*\*/g, (_, content) => bold(content));
3017
3068
  result = result.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, content) => italic(content));
3018
3069
  result = result.replace(/~~([^~]+)~~/g, (_, content) => dim(content));
3019
- result = result.replace(/`([^`]+)`/g, (_, content) => cyan(content));
3070
+ result = result.replace(/`([^`]+)`/g, (_, content) => bold(cyan(content)));
3020
3071
  return result;
3021
3072
  }
3022
3073
  formatTableLine(line) {
@@ -5126,6 +5177,9 @@ ${red("✗")} ${dim("Force exit.")}\r
5126
5177
  if (!hasOutput) {
5127
5178
  hasOutput = true;
5128
5179
  indicator.stop();
5180
+ if (!options.silent)
5181
+ process.stdout.write(`
5182
+ `);
5129
5183
  }
5130
5184
  renderer?.push(chunk);
5131
5185
  output += chunk;
@@ -5148,6 +5202,10 @@ ${red("✗")} ${dim("Force exit.")}\r
5148
5202
  });
5149
5203
  renderer?.stop();
5150
5204
  indicator.stop();
5205
+ if (hasOutput && !wasAborted && renderer) {
5206
+ process.stdout.write(`
5207
+ `);
5208
+ }
5151
5209
  if (wasAborted) {
5152
5210
  return {
5153
5211
  success: false,
@@ -7894,15 +7952,31 @@ ${red("✗")} ${aiResult.error}
7894
7952
  function printWelcome(session) {
7895
7953
  process.stderr.write(`
7896
7954
  `);
7897
- process.stderr.write(`${bold("Locus")} ${dim("REPL")} session ${dim(session.id)}
7955
+ const logoWidth = 10;
7956
+ const gap = " ";
7957
+ const infoLines = [
7958
+ `${bold("Locus")} ${dim("REPL")}`,
7959
+ `${dim("Provider:")} ${session.metadata.provider} ${dim("/")} ${session.metadata.model}`,
7960
+ `${dim("Session:")} ${dim(session.id)}`
7961
+ ];
7962
+ const textOffset = 1;
7963
+ const totalLines = Math.max(LOCUS_LOGO.length, infoLines.length + textOffset);
7964
+ for (let i = 0;i < totalLines; i++) {
7965
+ const logoLine = LOCUS_LOGO[i] ?? "";
7966
+ const paddedLogo = logoLine.padEnd(logoWidth);
7967
+ const infoIdx = i - textOffset;
7968
+ const infoLine = infoIdx >= 0 && infoIdx < infoLines.length ? infoLines[infoIdx] : "";
7969
+ process.stderr.write(`${bold(white(paddedLogo))}${gap}${infoLine}
7898
7970
  `);
7899
- process.stderr.write(`${dim(`Provider: ${session.metadata.provider} / ${session.metadata.model}`)}
7971
+ }
7972
+ process.stderr.write(`
7900
7973
  `);
7901
- process.stderr.write(`${dim("Type /help for commands, Shift+Enter for newline, Ctrl+C twice to exit")}
7974
+ process.stderr.write(` ${dim("Type /help for commands, Shift+Enter for newline, Ctrl+C twice to exit")}
7902
7975
  `);
7903
7976
  process.stderr.write(`
7904
7977
  `);
7905
7978
  }
7979
+ var LOCUS_LOGO;
7906
7980
  var init_repl = __esm(() => {
7907
7981
  init_run_ai();
7908
7982
  init_runner();
@@ -7917,6 +7991,18 @@ var init_repl = __esm(() => {
7917
7991
  init_input_history();
7918
7992
  init_model_config();
7919
7993
  init_session_manager();
7994
+ LOCUS_LOGO = [
7995
+ " ▄█ ",
7996
+ " ▄▄████▄▄▄▄ ",
7997
+ " ████▀ ████ ",
7998
+ " ████▄▄████ ",
7999
+ " ▀█▄█▀███▀▀ ▄▄██▄▄ ",
8000
+ " ████ ▄██████▀███▄ ",
8001
+ " ████▄██▀ ▀▀███ ▄████ ",
8002
+ " ▀▀██████▄ ██████▀ ",
8003
+ " ▀▀█████▄▄██▀▀ ",
8004
+ " ▀▀██▀▀ "
8005
+ ];
7920
8006
  });
7921
8007
 
7922
8008
  // src/commands/exec.ts
@@ -8590,15 +8676,6 @@ var init_run_state = __esm(() => {
8590
8676
  });
8591
8677
 
8592
8678
  // src/core/shutdown.ts
8593
- import { execSync as execSync12 } from "node:child_process";
8594
- function cleanupActiveSandboxes() {
8595
- for (const name of activeSandboxes) {
8596
- try {
8597
- execSync12(`docker sandbox rm ${name}`, { timeout: 1e4 });
8598
- } catch {}
8599
- }
8600
- activeSandboxes.clear();
8601
- }
8602
8679
  function registerShutdownHandlers(ctx) {
8603
8680
  shutdownContext = ctx;
8604
8681
  interruptCount = 0;
@@ -8632,7 +8709,6 @@ Interrupted. Saving state...
8632
8709
  `);
8633
8710
  }
8634
8711
  }
8635
- cleanupActiveSandboxes();
8636
8712
  shutdownContext?.onShutdown?.();
8637
8713
  if (interruptTimer)
8638
8714
  clearTimeout(interruptTimer);
@@ -8660,18 +8736,17 @@ Interrupted. Saving state...
8660
8736
  }
8661
8737
  };
8662
8738
  }
8663
- var shutdownRegistered = false, shutdownContext = null, interruptCount = 0, interruptTimer = null, activeSandboxes;
8739
+ var shutdownRegistered = false, shutdownContext = null, interruptCount = 0, interruptTimer = null;
8664
8740
  var init_shutdown = __esm(() => {
8665
8741
  init_run_state();
8666
- activeSandboxes = new Set;
8667
8742
  });
8668
8743
 
8669
8744
  // src/core/worktree.ts
8670
- import { execSync as execSync13 } from "node:child_process";
8745
+ import { execSync as execSync12 } from "node:child_process";
8671
8746
  import { existsSync as existsSync17, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
8672
8747
  import { join as join17 } from "node:path";
8673
8748
  function git3(args, cwd) {
8674
- return execSync13(`git ${args}`, {
8749
+ return execSync12(`git ${args}`, {
8675
8750
  cwd,
8676
8751
  encoding: "utf-8",
8677
8752
  stdio: ["pipe", "pipe", "pipe"]
@@ -8696,7 +8771,7 @@ function generateBranchName(issueNumber) {
8696
8771
  }
8697
8772
  function getWorktreeBranch(worktreePath) {
8698
8773
  try {
8699
- return execSync13("git branch --show-current", {
8774
+ return execSync12("git branch --show-current", {
8700
8775
  cwd: worktreePath,
8701
8776
  encoding: "utf-8",
8702
8777
  stdio: ["pipe", "pipe", "pipe"]
@@ -8821,7 +8896,7 @@ var exports_run = {};
8821
8896
  __export(exports_run, {
8822
8897
  runCommand: () => runCommand
8823
8898
  });
8824
- import { execSync as execSync14 } from "node:child_process";
8899
+ import { execSync as execSync13 } from "node:child_process";
8825
8900
  function resolveExecutionContext(config, modelOverride) {
8826
8901
  const model = modelOverride ?? config.ai.model;
8827
8902
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
@@ -8972,7 +9047,7 @@ ${yellow("⚠")} A sprint run is already in progress.
8972
9047
  }
8973
9048
  if (!flags.dryRun) {
8974
9049
  try {
8975
- execSync14(`git checkout -B ${branchName}`, {
9050
+ execSync13(`git checkout -B ${branchName}`, {
8976
9051
  cwd: projectRoot,
8977
9052
  encoding: "utf-8",
8978
9053
  stdio: ["pipe", "pipe", "pipe"]
@@ -9022,7 +9097,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
9022
9097
  let sprintContext;
9023
9098
  if (i > 0 && !flags.dryRun) {
9024
9099
  try {
9025
- sprintContext = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9100
+ sprintContext = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9026
9101
  cwd: projectRoot,
9027
9102
  encoding: "utf-8",
9028
9103
  stdio: ["pipe", "pipe", "pipe"]
@@ -9087,7 +9162,7 @@ ${bold("Summary:")}
9087
9162
  const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
9088
9163
  if (prNumber !== undefined) {
9089
9164
  try {
9090
- execSync14(`git checkout ${config.agent.baseBranch}`, {
9165
+ execSync13(`git checkout ${config.agent.baseBranch}`, {
9091
9166
  cwd: projectRoot,
9092
9167
  encoding: "utf-8",
9093
9168
  stdio: ["pipe", "pipe", "pipe"]
@@ -9287,13 +9362,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
9287
9362
  `);
9288
9363
  if (state.type === "sprint" && state.branch) {
9289
9364
  try {
9290
- const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
9365
+ const currentBranch = execSync13("git rev-parse --abbrev-ref HEAD", {
9291
9366
  cwd: projectRoot,
9292
9367
  encoding: "utf-8",
9293
9368
  stdio: ["pipe", "pipe", "pipe"]
9294
9369
  }).trim();
9295
9370
  if (currentBranch !== state.branch) {
9296
- execSync14(`git checkout ${state.branch}`, {
9371
+ execSync13(`git checkout ${state.branch}`, {
9297
9372
  cwd: projectRoot,
9298
9373
  encoding: "utf-8",
9299
9374
  stdio: ["pipe", "pipe", "pipe"]
@@ -9360,7 +9435,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
9360
9435
  const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
9361
9436
  if (prNumber !== undefined) {
9362
9437
  try {
9363
- execSync14(`git checkout ${config.agent.baseBranch}`, {
9438
+ execSync13(`git checkout ${config.agent.baseBranch}`, {
9364
9439
  cwd: projectRoot,
9365
9440
  encoding: "utf-8",
9366
9441
  stdio: ["pipe", "pipe", "pipe"]
@@ -9391,14 +9466,14 @@ function getOrder2(issue) {
9391
9466
  }
9392
9467
  function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
9393
9468
  try {
9394
- const status = execSync14("git status --porcelain", {
9469
+ const status = execSync13("git status --porcelain", {
9395
9470
  cwd: projectRoot,
9396
9471
  encoding: "utf-8",
9397
9472
  stdio: ["pipe", "pipe", "pipe"]
9398
9473
  }).trim();
9399
9474
  if (!status)
9400
9475
  return;
9401
- execSync14("git add -A", {
9476
+ execSync13("git add -A", {
9402
9477
  cwd: projectRoot,
9403
9478
  encoding: "utf-8",
9404
9479
  stdio: ["pipe", "pipe", "pipe"]
@@ -9406,7 +9481,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
9406
9481
  const message = `chore: complete #${issueNumber} - ${issueTitle}
9407
9482
 
9408
9483
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
9409
- execSync14(`git commit -F -`, {
9484
+ execSync13(`git commit -F -`, {
9410
9485
  input: message,
9411
9486
  cwd: projectRoot,
9412
9487
  encoding: "utf-8",
@@ -9420,7 +9495,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
9420
9495
  if (!config.agent.autoPR)
9421
9496
  return;
9422
9497
  try {
9423
- const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9498
+ const diff = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9424
9499
  cwd: projectRoot,
9425
9500
  encoding: "utf-8",
9426
9501
  stdio: ["pipe", "pipe", "pipe"]
@@ -9430,7 +9505,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
9430
9505
  `);
9431
9506
  return;
9432
9507
  }
9433
- execSync14(`git push -u origin ${branchName}`, {
9508
+ execSync13(`git push -u origin ${branchName}`, {
9434
9509
  cwd: projectRoot,
9435
9510
  encoding: "utf-8",
9436
9511
  stdio: ["pipe", "pipe", "pipe"]
@@ -10208,7 +10283,7 @@ var exports_review = {};
10208
10283
  __export(exports_review, {
10209
10284
  reviewCommand: () => reviewCommand
10210
10285
  });
10211
- import { execSync as execSync15 } from "node:child_process";
10286
+ import { execSync as execSync14 } from "node:child_process";
10212
10287
  import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
10213
10288
  import { join as join19 } from "node:path";
10214
10289
  function printHelp2() {
@@ -10286,7 +10361,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
10286
10361
  async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
10287
10362
  let prInfo;
10288
10363
  try {
10289
- 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
+ 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"] });
10290
10365
  const raw = JSON.parse(result);
10291
10366
  prInfo = {
10292
10367
  number: raw.number,
@@ -10352,7 +10427,7 @@ ${output.slice(0, 60000)}
10352
10427
 
10353
10428
  ---
10354
10429
  _Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
10355
- execSync15(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10430
+ execSync14(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10356
10431
  process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
10357
10432
  `);
10358
10433
  } catch (e) {
@@ -10432,7 +10507,7 @@ var exports_iterate = {};
10432
10507
  __export(exports_iterate, {
10433
10508
  iterateCommand: () => iterateCommand
10434
10509
  });
10435
- import { execSync as execSync16 } from "node:child_process";
10510
+ import { execSync as execSync15 } from "node:child_process";
10436
10511
  function printHelp3() {
10437
10512
  process.stderr.write(`
10438
10513
  ${bold("locus iterate")} — Re-execute tasks with PR feedback
@@ -10642,12 +10717,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
10642
10717
  }
10643
10718
  function findPRForIssue(projectRoot, issueNumber) {
10644
10719
  try {
10645
- const result = execSync16(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10720
+ const result = execSync15(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10646
10721
  const parsed = JSON.parse(result);
10647
10722
  if (parsed.length > 0) {
10648
10723
  return parsed[0].number;
10649
10724
  }
10650
- const branchResult = execSync16(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10725
+ const branchResult = execSync15(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10651
10726
  const branchParsed = JSON.parse(branchResult);
10652
10727
  if (branchParsed.length > 0) {
10653
10728
  return branchParsed[0].number;
@@ -11266,9 +11341,10 @@ __export(exports_sandbox2, {
11266
11341
  parseSandboxInstallArgs: () => parseSandboxInstallArgs,
11267
11342
  parseSandboxExecArgs: () => parseSandboxExecArgs
11268
11343
  });
11269
- import { execSync as execSync17, spawn as spawn6 } from "node:child_process";
11344
+ import { execSync as execSync16, spawn as spawn6 } from "node:child_process";
11270
11345
  import { createHash } from "node:crypto";
11271
- import { basename as basename4 } from "node:path";
11346
+ import { existsSync as existsSync22, readFileSync as readFileSync17 } from "node:fs";
11347
+ import { basename as basename4, join as join22 } from "node:path";
11272
11348
  function printSandboxHelp() {
11273
11349
  process.stderr.write(`
11274
11350
  ${bold("locus sandbox")} — Manage Docker sandbox lifecycle
@@ -11277,15 +11353,15 @@ ${bold("Usage:")}
11277
11353
  locus sandbox ${dim("# Create claude/codex sandboxes and enable sandbox mode")}
11278
11354
  locus sandbox claude ${dim("# Run claude interactively (for login)")}
11279
11355
  locus sandbox codex ${dim("# Run codex interactively (for login)")}
11356
+ locus sandbox setup ${dim("# Re-run dependency install in sandbox(es)")}
11280
11357
  locus sandbox install <pkg> ${dim("# npm install -g package(s) in sandbox(es)")}
11281
- locus sandbox exec <provider> -- <cmd...> ${dim("# Run one command inside provider sandbox")}
11282
11358
  locus sandbox shell <provider> ${dim("# Open interactive shell in provider sandbox")}
11283
11359
  locus sandbox logs <provider> ${dim("# Show provider sandbox logs")}
11284
11360
  locus sandbox rm ${dim("# Destroy all provider sandboxes and disable sandbox mode")}
11285
11361
  locus sandbox status ${dim("# Show current sandbox state")}
11286
11362
 
11287
11363
  ${bold("Flow:")}
11288
- 1. ${cyan("locus sandbox")} Create provider sandboxes
11364
+ 1. ${cyan("locus sandbox")} Create sandboxes (auto-installs dependencies)
11289
11365
  2. ${cyan("locus sandbox claude")} Login Claude inside its sandbox
11290
11366
  3. ${cyan("locus sandbox codex")} Login Codex inside its sandbox
11291
11367
  4. ${cyan("locus sandbox install bun")} Install extra tools (optional)
@@ -11301,10 +11377,10 @@ async function sandboxCommand(projectRoot, args) {
11301
11377
  case "claude":
11302
11378
  case "codex":
11303
11379
  return handleAgentLogin(projectRoot, subcommand);
11380
+ case "setup":
11381
+ return handleSetup(projectRoot);
11304
11382
  case "install":
11305
11383
  return handleInstall(projectRoot, args.slice(1));
11306
- case "exec":
11307
- return handleExec(projectRoot, args.slice(1));
11308
11384
  case "shell":
11309
11385
  return handleShell(projectRoot, args.slice(1));
11310
11386
  case "logs":
@@ -11318,7 +11394,7 @@ async function sandboxCommand(projectRoot, args) {
11318
11394
  default:
11319
11395
  process.stderr.write(`${red("✗")} Unknown sandbox subcommand: ${bold(subcommand)}
11320
11396
  `);
11321
- process.stderr.write(` Available: ${cyan("claude")}, ${cyan("codex")}, ${cyan("install")}, ${cyan("exec")}, ${cyan("shell")}, ${cyan("logs")}, ${cyan("rm")}, ${cyan("status")}
11397
+ process.stderr.write(` Available: ${cyan("claude")}, ${cyan("codex")}, ${cyan("setup")}, ${cyan("install")}, ${cyan("exec")}, ${cyan("shell")}, ${cyan("logs")}, ${cyan("rm")}, ${cyan("status")}
11322
11398
  `);
11323
11399
  }
11324
11400
  }
@@ -11334,14 +11410,14 @@ async function handleCreate(projectRoot) {
11334
11410
  }
11335
11411
  const sandboxNames = buildProviderSandboxNames(projectRoot);
11336
11412
  const readySandboxes = {};
11413
+ const newlyCreated = new Set;
11337
11414
  let failed = false;
11338
- for (const provider of PROVIDERS) {
11415
+ const createResults = await Promise.all(PROVIDERS.map(async (provider) => {
11339
11416
  const name = sandboxNames[provider];
11340
11417
  if (isSandboxAlive(name)) {
11341
11418
  process.stderr.write(`${green("✓")} ${provider} sandbox ready: ${bold(name)}
11342
11419
  `);
11343
- readySandboxes[provider] = name;
11344
- continue;
11420
+ return { provider, name, created: false, existed: true };
11345
11421
  }
11346
11422
  process.stderr.write(`Creating ${bold(provider)} sandbox ${dim(name)} with workspace ${dim(projectRoot)}...
11347
11423
  `);
@@ -11349,16 +11425,28 @@ async function handleCreate(projectRoot) {
11349
11425
  if (!created) {
11350
11426
  process.stderr.write(`${red("✗")} Failed to create ${provider} sandbox (${name}).
11351
11427
  `);
11352
- failed = true;
11353
- continue;
11428
+ return { provider, name, created: false, existed: false };
11354
11429
  }
11355
11430
  process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold(name)}
11356
11431
  `);
11357
- readySandboxes[provider] = name;
11432
+ return { provider, name, created: true, existed: false };
11433
+ }));
11434
+ for (const result of createResults) {
11435
+ if (result.created || result.existed) {
11436
+ readySandboxes[result.provider] = result.name;
11437
+ if (result.created)
11438
+ newlyCreated.add(result.name);
11439
+ } else {
11440
+ failed = true;
11441
+ }
11358
11442
  }
11359
11443
  config.sandbox.enabled = true;
11360
11444
  config.sandbox.providers = readySandboxes;
11361
11445
  saveConfig(projectRoot, config);
11446
+ await Promise.all(PROVIDERS.filter((provider) => {
11447
+ const sandboxName = readySandboxes[provider];
11448
+ return sandboxName && newlyCreated.has(sandboxName);
11449
+ }).map((provider) => runSandboxSetup(readySandboxes[provider], projectRoot)));
11362
11450
  if (failed) {
11363
11451
  process.stderr.write(`
11364
11452
  ${yellow("⚠")} Some sandboxes failed to create. Re-run ${cyan("locus sandbox")} after resolving Docker issues.
@@ -11432,7 +11520,7 @@ function handleRemove(projectRoot) {
11432
11520
  process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
11433
11521
  `);
11434
11522
  try {
11435
- execSync17(`docker sandbox rm ${sandboxName}`, {
11523
+ execSync16(`docker sandbox rm ${sandboxName}`, {
11436
11524
  encoding: "utf-8",
11437
11525
  stdio: ["pipe", "pipe", "pipe"],
11438
11526
  timeout: 15000
@@ -11607,26 +11695,6 @@ function parseSandboxExecArgs(args) {
11607
11695
  }
11608
11696
  return { provider, command };
11609
11697
  }
11610
- async function handleExec(projectRoot, args) {
11611
- const parsed = parseSandboxExecArgs(args);
11612
- if (parsed.error || !parsed.provider) {
11613
- process.stderr.write(`${red("✗")} ${parsed.error}
11614
- `);
11615
- return;
11616
- }
11617
- const sandboxName = getActiveProviderSandbox(projectRoot, parsed.provider);
11618
- if (!sandboxName) {
11619
- return;
11620
- }
11621
- await runInteractiveCommand("docker", [
11622
- "sandbox",
11623
- "exec",
11624
- "-w",
11625
- projectRoot,
11626
- sandboxName,
11627
- ...parsed.command
11628
- ]);
11629
- }
11630
11698
  async function handleShell(projectRoot, args) {
11631
11699
  const provider = args[0];
11632
11700
  if (provider !== "claude" && provider !== "codex") {
@@ -11724,6 +11792,104 @@ async function handleLogs(projectRoot, args) {
11724
11792
  dockerArgs.push(sandboxName);
11725
11793
  await runInteractiveCommand("docker", dockerArgs);
11726
11794
  }
11795
+ function detectPackageManager(projectRoot) {
11796
+ try {
11797
+ const raw = readFileSync17(join22(projectRoot, "package.json"), "utf-8");
11798
+ const pkgJson = JSON.parse(raw);
11799
+ if (typeof pkgJson.packageManager === "string") {
11800
+ const name = pkgJson.packageManager.split("@")[0];
11801
+ if (name === "bun" || name === "npm" || name === "yarn" || name === "pnpm") {
11802
+ return name;
11803
+ }
11804
+ }
11805
+ } catch {}
11806
+ if (existsSync22(join22(projectRoot, "bun.lock")) || existsSync22(join22(projectRoot, "bun.lockb"))) {
11807
+ return "bun";
11808
+ }
11809
+ if (existsSync22(join22(projectRoot, "yarn.lock"))) {
11810
+ return "yarn";
11811
+ }
11812
+ if (existsSync22(join22(projectRoot, "pnpm-lock.yaml"))) {
11813
+ return "pnpm";
11814
+ }
11815
+ return "npm";
11816
+ }
11817
+ function getInstallCommand(pm) {
11818
+ switch (pm) {
11819
+ case "bun":
11820
+ return ["bun", "install"];
11821
+ case "yarn":
11822
+ return ["yarn", "install"];
11823
+ case "pnpm":
11824
+ return ["pnpm", "install"];
11825
+ case "npm":
11826
+ return ["npm", "install"];
11827
+ }
11828
+ }
11829
+ 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(`
11836
+ Installing dependencies (${bold(installCmd.join(" "))}) in sandbox ${dim(sandboxName)}...
11837
+ `);
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)}.
11848
+ `);
11849
+ return false;
11850
+ }
11851
+ process.stderr.write(`${green("✓")} Dependencies installed in sandbox ${dim(sandboxName)}.
11852
+ `);
11853
+ const setupScript = join22(projectRoot, ".locus", "sandbox-setup.sh");
11854
+ if (existsSync22(setupScript)) {
11855
+ process.stderr.write(`Running ${bold(".locus/sandbox-setup.sh")} in sandbox ${dim(sandboxName)}...
11856
+ `);
11857
+ const hookOk = await runInteractiveCommand("docker", [
11858
+ "sandbox",
11859
+ "exec",
11860
+ "-w",
11861
+ projectRoot,
11862
+ sandboxName,
11863
+ "sh",
11864
+ setupScript
11865
+ ]);
11866
+ if (!hookOk) {
11867
+ process.stderr.write(`${yellow("⚠")} Setup hook failed in sandbox ${dim(sandboxName)}.
11868
+ `);
11869
+ }
11870
+ }
11871
+ return true;
11872
+ }
11873
+ async function handleSetup(projectRoot) {
11874
+ const config = loadConfig(projectRoot);
11875
+ const providers = config.sandbox.providers;
11876
+ if (!providers.claude && !providers.codex) {
11877
+ process.stderr.write(`${red("✗")} No sandboxes configured. Run ${cyan("locus sandbox")} first.
11878
+ `);
11879
+ return;
11880
+ }
11881
+ for (const provider of PROVIDERS) {
11882
+ const sandboxName = providers[provider];
11883
+ if (!sandboxName)
11884
+ continue;
11885
+ if (!isSandboxAlive(sandboxName)) {
11886
+ process.stderr.write(`${yellow("⚠")} ${provider} sandbox is not running: ${dim(sandboxName)}
11887
+ `);
11888
+ continue;
11889
+ }
11890
+ await runSandboxSetup(sandboxName, projectRoot);
11891
+ }
11892
+ }
11727
11893
  function buildProviderSandboxNames(projectRoot) {
11728
11894
  const segment = sanitizeSegment(basename4(projectRoot));
11729
11895
  const hash = createHash("sha1").update(projectRoot).digest("hex").slice(0, 8);
@@ -11768,7 +11934,7 @@ function runInteractiveCommand(command, args) {
11768
11934
  }
11769
11935
  async function createProviderSandbox(provider, sandboxName, projectRoot) {
11770
11936
  try {
11771
- execSync17(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, {
11937
+ execSync16(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
11772
11938
  stdio: ["pipe", "pipe", "pipe"],
11773
11939
  timeout: 120000
11774
11940
  });
@@ -11782,9 +11948,30 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
11782
11948
  await enforceSandboxIgnore(sandboxName, projectRoot);
11783
11949
  return true;
11784
11950
  }
11951
+ async function ensurePackageManagerInSandbox(sandboxName, pm) {
11952
+ try {
11953
+ execSync16(`docker sandbox exec ${sandboxName} which ${pm}`, {
11954
+ stdio: ["pipe", "pipe", "pipe"],
11955
+ timeout: 5000
11956
+ });
11957
+ } catch {
11958
+ const npmPkg = pm === "bun" ? "bun" : pm === "yarn" ? "yarn" : "pnpm";
11959
+ process.stderr.write(`Installing ${bold(pm)} in sandbox...
11960
+ `);
11961
+ try {
11962
+ execSync16(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
11963
+ stdio: "inherit",
11964
+ timeout: 120000
11965
+ });
11966
+ } catch {
11967
+ process.stderr.write(`${yellow("⚠")} Failed to install ${pm} in sandbox. Dependency install may fail.
11968
+ `);
11969
+ }
11970
+ }
11971
+ }
11785
11972
  async function ensureCodexInSandbox(sandboxName) {
11786
11973
  try {
11787
- execSync17(`docker sandbox exec ${sandboxName} which codex`, {
11974
+ execSync16(`docker sandbox exec ${sandboxName} which codex`, {
11788
11975
  stdio: ["pipe", "pipe", "pipe"],
11789
11976
  timeout: 5000
11790
11977
  });
@@ -11792,7 +11979,7 @@ async function ensureCodexInSandbox(sandboxName) {
11792
11979
  process.stderr.write(`Installing codex in sandbox...
11793
11980
  `);
11794
11981
  try {
11795
- execSync17(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
11982
+ execSync16(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
11796
11983
  } catch {
11797
11984
  process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
11798
11985
  `);
@@ -11801,7 +11988,7 @@ async function ensureCodexInSandbox(sandboxName) {
11801
11988
  }
11802
11989
  function isSandboxAlive(name) {
11803
11990
  try {
11804
- const output = execSync17("docker sandbox ls", {
11991
+ const output = execSync16("docker sandbox ls", {
11805
11992
  encoding: "utf-8",
11806
11993
  stdio: ["pipe", "pipe", "pipe"],
11807
11994
  timeout: 5000
@@ -11826,17 +12013,17 @@ init_context();
11826
12013
  init_logger();
11827
12014
  init_rate_limiter();
11828
12015
  init_terminal();
11829
- import { existsSync as existsSync22, readFileSync as readFileSync17 } from "node:fs";
11830
- import { join as join22 } from "node:path";
12016
+ import { existsSync as existsSync23, readFileSync as readFileSync18 } from "node:fs";
12017
+ import { join as join23 } from "node:path";
11831
12018
  import { fileURLToPath } from "node:url";
11832
12019
  function getCliVersion() {
11833
12020
  const fallbackVersion = "0.0.0";
11834
- const packageJsonPath = join22(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
11835
- if (!existsSync22(packageJsonPath)) {
12021
+ const packageJsonPath = join23(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
12022
+ if (!existsSync23(packageJsonPath)) {
11836
12023
  return fallbackVersion;
11837
12024
  }
11838
12025
  try {
11839
- const parsed = JSON.parse(readFileSync17(packageJsonPath, "utf-8"));
12026
+ const parsed = JSON.parse(readFileSync18(packageJsonPath, "utf-8"));
11840
12027
  return parsed.version ?? fallbackVersion;
11841
12028
  } catch {
11842
12029
  return fallbackVersion;
@@ -11950,9 +12137,41 @@ function parseArgs(argv) {
11950
12137
  const args = positional.slice(1);
11951
12138
  return { command, args, flags };
11952
12139
  }
12140
+ var LOCUS_LOGO2 = [
12141
+ " ▄█ ",
12142
+ "▄▄████▄▄▄▄ ",
12143
+ "████▀ ████ ",
12144
+ "████▄▄████ ",
12145
+ "▀█▄█▀███▀▀ ▄▄██▄▄ ",
12146
+ "████ ▄██████▀███▄ ",
12147
+ "████▄██▀ ▀▀███ ▄████ ",
12148
+ "▀▀██████▄ ██████▀ ",
12149
+ " ▀▀█████▄▄██▀▀ ",
12150
+ " ▀▀██▀▀ "
12151
+ ];
12152
+ function printLogo() {
12153
+ process.stderr.write(`
12154
+ `);
12155
+ const logoWidth = 10;
12156
+ const gap = " ";
12157
+ const infoLines = [
12158
+ `${bold("Locus")} ${dim(`v${VERSION}`)}`,
12159
+ `${dim("GitHub-native AI engineering assistant")}`
12160
+ ];
12161
+ const textOffset = 1;
12162
+ const totalLines = Math.max(LOCUS_LOGO2.length, infoLines.length + textOffset);
12163
+ for (let i = 0;i < totalLines; i++) {
12164
+ const logoLine = LOCUS_LOGO2[i] ?? "";
12165
+ const paddedLogo = logoLine.padEnd(logoWidth);
12166
+ const infoIdx = i - textOffset;
12167
+ const infoLine = infoIdx >= 0 && infoIdx < infoLines.length ? infoLines[infoIdx] : "";
12168
+ process.stderr.write(`${bold(white(paddedLogo))}${gap}${infoLine}
12169
+ `);
12170
+ }
12171
+ }
11953
12172
  function printHelp6() {
12173
+ printLogo();
11954
12174
  process.stderr.write(`
11955
- ${bold("Locus")} ${dim(`v${VERSION}`)} — GitHub-native AI engineering assistant
11956
12175
 
11957
12176
  ${bold("Usage:")}
11958
12177
  locus <command> [options]
@@ -12065,7 +12284,7 @@ async function main() {
12065
12284
  try {
12066
12285
  const root = getGitRoot(cwd);
12067
12286
  if (isInitialized(root)) {
12068
- logDir = join22(root, ".locus", "logs");
12287
+ logDir = join23(root, ".locus", "logs");
12069
12288
  getRateLimiter(root);
12070
12289
  }
12071
12290
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "GitHub-native AI engineering assistant",
5
5
  "type": "module",
6
6
  "bin": {