@locusai/cli 0.19.2 → 0.20.1

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 -89
  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) {
@@ -4483,7 +4534,6 @@ class SandboxedClaudeRunner {
4483
4534
  const text = chunk.toString();
4484
4535
  errorOutput += text;
4485
4536
  log.debug("sandboxed claude stderr", { text: text.slice(0, 500) });
4486
- options.onOutput?.(text);
4487
4537
  });
4488
4538
  this.process.on("close", (code) => {
4489
4539
  this.process = null;
@@ -5126,6 +5176,9 @@ ${red("✗")} ${dim("Force exit.")}\r
5126
5176
  if (!hasOutput) {
5127
5177
  hasOutput = true;
5128
5178
  indicator.stop();
5179
+ if (!options.silent)
5180
+ process.stdout.write(`
5181
+ `);
5129
5182
  }
5130
5183
  renderer?.push(chunk);
5131
5184
  output += chunk;
@@ -5148,6 +5201,10 @@ ${red("✗")} ${dim("Force exit.")}\r
5148
5201
  });
5149
5202
  renderer?.stop();
5150
5203
  indicator.stop();
5204
+ if (hasOutput && !wasAborted && renderer) {
5205
+ process.stdout.write(`
5206
+ `);
5207
+ }
5151
5208
  if (wasAborted) {
5152
5209
  return {
5153
5210
  success: false,
@@ -7894,15 +7951,31 @@ ${red("✗")} ${aiResult.error}
7894
7951
  function printWelcome(session) {
7895
7952
  process.stderr.write(`
7896
7953
  `);
7897
- process.stderr.write(`${bold("Locus")} ${dim("REPL")} session ${dim(session.id)}
7954
+ const logoWidth = 10;
7955
+ const gap = " ";
7956
+ const infoLines = [
7957
+ `${bold("Locus")} ${dim("REPL")}`,
7958
+ `${dim("Provider:")} ${session.metadata.provider} ${dim("/")} ${session.metadata.model}`,
7959
+ `${dim("Session:")} ${dim(session.id)}`
7960
+ ];
7961
+ const textOffset = 1;
7962
+ const totalLines = Math.max(LOCUS_LOGO.length, infoLines.length + textOffset);
7963
+ for (let i = 0;i < totalLines; i++) {
7964
+ const logoLine = LOCUS_LOGO[i] ?? "";
7965
+ const paddedLogo = logoLine.padEnd(logoWidth);
7966
+ const infoIdx = i - textOffset;
7967
+ const infoLine = infoIdx >= 0 && infoIdx < infoLines.length ? infoLines[infoIdx] : "";
7968
+ process.stderr.write(`${bold(white(paddedLogo))}${gap}${infoLine}
7898
7969
  `);
7899
- process.stderr.write(`${dim(`Provider: ${session.metadata.provider} / ${session.metadata.model}`)}
7970
+ }
7971
+ process.stderr.write(`
7900
7972
  `);
7901
- process.stderr.write(`${dim("Type /help for commands, Shift+Enter for newline, Ctrl+C twice to exit")}
7973
+ process.stderr.write(` ${dim("Type /help for commands, Shift+Enter for newline, Ctrl+C twice to exit")}
7902
7974
  `);
7903
7975
  process.stderr.write(`
7904
7976
  `);
7905
7977
  }
7978
+ var LOCUS_LOGO;
7906
7979
  var init_repl = __esm(() => {
7907
7980
  init_run_ai();
7908
7981
  init_runner();
@@ -7917,6 +7990,18 @@ var init_repl = __esm(() => {
7917
7990
  init_input_history();
7918
7991
  init_model_config();
7919
7992
  init_session_manager();
7993
+ LOCUS_LOGO = [
7994
+ " ▄█ ",
7995
+ " ▄▄████▄▄▄▄ ",
7996
+ " ████▀ ████ ",
7997
+ " ████▄▄████ ",
7998
+ " ▀█▄█▀███▀▀ ▄▄██▄▄ ",
7999
+ " ████ ▄██████▀███▄ ",
8000
+ " ████▄██▀ ▀▀███ ▄████ ",
8001
+ " ▀▀██████▄ ██████▀ ",
8002
+ " ▀▀█████▄▄██▀▀ ",
8003
+ " ▀▀██▀▀ "
8004
+ ];
7920
8005
  });
7921
8006
 
7922
8007
  // src/commands/exec.ts
@@ -8590,15 +8675,6 @@ var init_run_state = __esm(() => {
8590
8675
  });
8591
8676
 
8592
8677
  // 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
8678
  function registerShutdownHandlers(ctx) {
8603
8679
  shutdownContext = ctx;
8604
8680
  interruptCount = 0;
@@ -8632,7 +8708,6 @@ Interrupted. Saving state...
8632
8708
  `);
8633
8709
  }
8634
8710
  }
8635
- cleanupActiveSandboxes();
8636
8711
  shutdownContext?.onShutdown?.();
8637
8712
  if (interruptTimer)
8638
8713
  clearTimeout(interruptTimer);
@@ -8660,18 +8735,17 @@ Interrupted. Saving state...
8660
8735
  }
8661
8736
  };
8662
8737
  }
8663
- var shutdownRegistered = false, shutdownContext = null, interruptCount = 0, interruptTimer = null, activeSandboxes;
8738
+ var shutdownRegistered = false, shutdownContext = null, interruptCount = 0, interruptTimer = null;
8664
8739
  var init_shutdown = __esm(() => {
8665
8740
  init_run_state();
8666
- activeSandboxes = new Set;
8667
8741
  });
8668
8742
 
8669
8743
  // src/core/worktree.ts
8670
- import { execSync as execSync13 } from "node:child_process";
8744
+ import { execSync as execSync12 } from "node:child_process";
8671
8745
  import { existsSync as existsSync17, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
8672
8746
  import { join as join17 } from "node:path";
8673
8747
  function git3(args, cwd) {
8674
- return execSync13(`git ${args}`, {
8748
+ return execSync12(`git ${args}`, {
8675
8749
  cwd,
8676
8750
  encoding: "utf-8",
8677
8751
  stdio: ["pipe", "pipe", "pipe"]
@@ -8696,7 +8770,7 @@ function generateBranchName(issueNumber) {
8696
8770
  }
8697
8771
  function getWorktreeBranch(worktreePath) {
8698
8772
  try {
8699
- return execSync13("git branch --show-current", {
8773
+ return execSync12("git branch --show-current", {
8700
8774
  cwd: worktreePath,
8701
8775
  encoding: "utf-8",
8702
8776
  stdio: ["pipe", "pipe", "pipe"]
@@ -8821,7 +8895,7 @@ var exports_run = {};
8821
8895
  __export(exports_run, {
8822
8896
  runCommand: () => runCommand
8823
8897
  });
8824
- import { execSync as execSync14 } from "node:child_process";
8898
+ import { execSync as execSync13 } from "node:child_process";
8825
8899
  function resolveExecutionContext(config, modelOverride) {
8826
8900
  const model = modelOverride ?? config.ai.model;
8827
8901
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
@@ -8972,7 +9046,7 @@ ${yellow("⚠")} A sprint run is already in progress.
8972
9046
  }
8973
9047
  if (!flags.dryRun) {
8974
9048
  try {
8975
- execSync14(`git checkout -B ${branchName}`, {
9049
+ execSync13(`git checkout -B ${branchName}`, {
8976
9050
  cwd: projectRoot,
8977
9051
  encoding: "utf-8",
8978
9052
  stdio: ["pipe", "pipe", "pipe"]
@@ -9022,7 +9096,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
9022
9096
  let sprintContext;
9023
9097
  if (i > 0 && !flags.dryRun) {
9024
9098
  try {
9025
- sprintContext = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9099
+ sprintContext = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9026
9100
  cwd: projectRoot,
9027
9101
  encoding: "utf-8",
9028
9102
  stdio: ["pipe", "pipe", "pipe"]
@@ -9087,7 +9161,7 @@ ${bold("Summary:")}
9087
9161
  const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
9088
9162
  if (prNumber !== undefined) {
9089
9163
  try {
9090
- execSync14(`git checkout ${config.agent.baseBranch}`, {
9164
+ execSync13(`git checkout ${config.agent.baseBranch}`, {
9091
9165
  cwd: projectRoot,
9092
9166
  encoding: "utf-8",
9093
9167
  stdio: ["pipe", "pipe", "pipe"]
@@ -9287,13 +9361,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
9287
9361
  `);
9288
9362
  if (state.type === "sprint" && state.branch) {
9289
9363
  try {
9290
- const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
9364
+ const currentBranch = execSync13("git rev-parse --abbrev-ref HEAD", {
9291
9365
  cwd: projectRoot,
9292
9366
  encoding: "utf-8",
9293
9367
  stdio: ["pipe", "pipe", "pipe"]
9294
9368
  }).trim();
9295
9369
  if (currentBranch !== state.branch) {
9296
- execSync14(`git checkout ${state.branch}`, {
9370
+ execSync13(`git checkout ${state.branch}`, {
9297
9371
  cwd: projectRoot,
9298
9372
  encoding: "utf-8",
9299
9373
  stdio: ["pipe", "pipe", "pipe"]
@@ -9360,7 +9434,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
9360
9434
  const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
9361
9435
  if (prNumber !== undefined) {
9362
9436
  try {
9363
- execSync14(`git checkout ${config.agent.baseBranch}`, {
9437
+ execSync13(`git checkout ${config.agent.baseBranch}`, {
9364
9438
  cwd: projectRoot,
9365
9439
  encoding: "utf-8",
9366
9440
  stdio: ["pipe", "pipe", "pipe"]
@@ -9391,14 +9465,14 @@ function getOrder2(issue) {
9391
9465
  }
9392
9466
  function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
9393
9467
  try {
9394
- const status = execSync14("git status --porcelain", {
9468
+ const status = execSync13("git status --porcelain", {
9395
9469
  cwd: projectRoot,
9396
9470
  encoding: "utf-8",
9397
9471
  stdio: ["pipe", "pipe", "pipe"]
9398
9472
  }).trim();
9399
9473
  if (!status)
9400
9474
  return;
9401
- execSync14("git add -A", {
9475
+ execSync13("git add -A", {
9402
9476
  cwd: projectRoot,
9403
9477
  encoding: "utf-8",
9404
9478
  stdio: ["pipe", "pipe", "pipe"]
@@ -9406,7 +9480,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
9406
9480
  const message = `chore: complete #${issueNumber} - ${issueTitle}
9407
9481
 
9408
9482
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
9409
- execSync14(`git commit -F -`, {
9483
+ execSync13(`git commit -F -`, {
9410
9484
  input: message,
9411
9485
  cwd: projectRoot,
9412
9486
  encoding: "utf-8",
@@ -9420,7 +9494,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
9420
9494
  if (!config.agent.autoPR)
9421
9495
  return;
9422
9496
  try {
9423
- const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9497
+ const diff = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9424
9498
  cwd: projectRoot,
9425
9499
  encoding: "utf-8",
9426
9500
  stdio: ["pipe", "pipe", "pipe"]
@@ -9430,7 +9504,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
9430
9504
  `);
9431
9505
  return;
9432
9506
  }
9433
- execSync14(`git push -u origin ${branchName}`, {
9507
+ execSync13(`git push -u origin ${branchName}`, {
9434
9508
  cwd: projectRoot,
9435
9509
  encoding: "utf-8",
9436
9510
  stdio: ["pipe", "pipe", "pipe"]
@@ -10208,7 +10282,7 @@ var exports_review = {};
10208
10282
  __export(exports_review, {
10209
10283
  reviewCommand: () => reviewCommand
10210
10284
  });
10211
- import { execSync as execSync15 } from "node:child_process";
10285
+ import { execSync as execSync14 } from "node:child_process";
10212
10286
  import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
10213
10287
  import { join as join19 } from "node:path";
10214
10288
  function printHelp2() {
@@ -10286,7 +10360,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
10286
10360
  async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
10287
10361
  let prInfo;
10288
10362
  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"] });
10363
+ const result = execSync14(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10290
10364
  const raw = JSON.parse(result);
10291
10365
  prInfo = {
10292
10366
  number: raw.number,
@@ -10352,7 +10426,7 @@ ${output.slice(0, 60000)}
10352
10426
 
10353
10427
  ---
10354
10428
  _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"] });
10429
+ execSync14(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10356
10430
  process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
10357
10431
  `);
10358
10432
  } catch (e) {
@@ -10432,7 +10506,7 @@ var exports_iterate = {};
10432
10506
  __export(exports_iterate, {
10433
10507
  iterateCommand: () => iterateCommand
10434
10508
  });
10435
- import { execSync as execSync16 } from "node:child_process";
10509
+ import { execSync as execSync15 } from "node:child_process";
10436
10510
  function printHelp3() {
10437
10511
  process.stderr.write(`
10438
10512
  ${bold("locus iterate")} — Re-execute tasks with PR feedback
@@ -10642,12 +10716,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
10642
10716
  }
10643
10717
  function findPRForIssue(projectRoot, issueNumber) {
10644
10718
  try {
10645
- const result = execSync16(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10719
+ const result = execSync15(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10646
10720
  const parsed = JSON.parse(result);
10647
10721
  if (parsed.length > 0) {
10648
10722
  return parsed[0].number;
10649
10723
  }
10650
- const branchResult = execSync16(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10724
+ const branchResult = execSync15(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10651
10725
  const branchParsed = JSON.parse(branchResult);
10652
10726
  if (branchParsed.length > 0) {
10653
10727
  return branchParsed[0].number;
@@ -11266,9 +11340,10 @@ __export(exports_sandbox2, {
11266
11340
  parseSandboxInstallArgs: () => parseSandboxInstallArgs,
11267
11341
  parseSandboxExecArgs: () => parseSandboxExecArgs
11268
11342
  });
11269
- import { execSync as execSync17, spawn as spawn6 } from "node:child_process";
11343
+ import { execSync as execSync16, spawn as spawn6 } from "node:child_process";
11270
11344
  import { createHash } from "node:crypto";
11271
- import { basename as basename4 } from "node:path";
11345
+ import { existsSync as existsSync22, readFileSync as readFileSync17 } from "node:fs";
11346
+ import { basename as basename4, join as join22 } from "node:path";
11272
11347
  function printSandboxHelp() {
11273
11348
  process.stderr.write(`
11274
11349
  ${bold("locus sandbox")} — Manage Docker sandbox lifecycle
@@ -11277,15 +11352,15 @@ ${bold("Usage:")}
11277
11352
  locus sandbox ${dim("# Create claude/codex sandboxes and enable sandbox mode")}
11278
11353
  locus sandbox claude ${dim("# Run claude interactively (for login)")}
11279
11354
  locus sandbox codex ${dim("# Run codex interactively (for login)")}
11355
+ locus sandbox setup ${dim("# Re-run dependency install in sandbox(es)")}
11280
11356
  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
11357
  locus sandbox shell <provider> ${dim("# Open interactive shell in provider sandbox")}
11283
11358
  locus sandbox logs <provider> ${dim("# Show provider sandbox logs")}
11284
11359
  locus sandbox rm ${dim("# Destroy all provider sandboxes and disable sandbox mode")}
11285
11360
  locus sandbox status ${dim("# Show current sandbox state")}
11286
11361
 
11287
11362
  ${bold("Flow:")}
11288
- 1. ${cyan("locus sandbox")} Create provider sandboxes
11363
+ 1. ${cyan("locus sandbox")} Create sandboxes (auto-installs dependencies)
11289
11364
  2. ${cyan("locus sandbox claude")} Login Claude inside its sandbox
11290
11365
  3. ${cyan("locus sandbox codex")} Login Codex inside its sandbox
11291
11366
  4. ${cyan("locus sandbox install bun")} Install extra tools (optional)
@@ -11301,10 +11376,10 @@ async function sandboxCommand(projectRoot, args) {
11301
11376
  case "claude":
11302
11377
  case "codex":
11303
11378
  return handleAgentLogin(projectRoot, subcommand);
11379
+ case "setup":
11380
+ return handleSetup(projectRoot);
11304
11381
  case "install":
11305
11382
  return handleInstall(projectRoot, args.slice(1));
11306
- case "exec":
11307
- return handleExec(projectRoot, args.slice(1));
11308
11383
  case "shell":
11309
11384
  return handleShell(projectRoot, args.slice(1));
11310
11385
  case "logs":
@@ -11318,7 +11393,7 @@ async function sandboxCommand(projectRoot, args) {
11318
11393
  default:
11319
11394
  process.stderr.write(`${red("✗")} Unknown sandbox subcommand: ${bold(subcommand)}
11320
11395
  `);
11321
- process.stderr.write(` Available: ${cyan("claude")}, ${cyan("codex")}, ${cyan("install")}, ${cyan("exec")}, ${cyan("shell")}, ${cyan("logs")}, ${cyan("rm")}, ${cyan("status")}
11396
+ process.stderr.write(` Available: ${cyan("claude")}, ${cyan("codex")}, ${cyan("setup")}, ${cyan("install")}, ${cyan("exec")}, ${cyan("shell")}, ${cyan("logs")}, ${cyan("rm")}, ${cyan("status")}
11322
11397
  `);
11323
11398
  }
11324
11399
  }
@@ -11334,14 +11409,14 @@ async function handleCreate(projectRoot) {
11334
11409
  }
11335
11410
  const sandboxNames = buildProviderSandboxNames(projectRoot);
11336
11411
  const readySandboxes = {};
11412
+ const newlyCreated = new Set;
11337
11413
  let failed = false;
11338
- for (const provider of PROVIDERS) {
11414
+ const createResults = await Promise.all(PROVIDERS.map(async (provider) => {
11339
11415
  const name = sandboxNames[provider];
11340
11416
  if (isSandboxAlive(name)) {
11341
11417
  process.stderr.write(`${green("✓")} ${provider} sandbox ready: ${bold(name)}
11342
11418
  `);
11343
- readySandboxes[provider] = name;
11344
- continue;
11419
+ return { provider, name, created: false, existed: true };
11345
11420
  }
11346
11421
  process.stderr.write(`Creating ${bold(provider)} sandbox ${dim(name)} with workspace ${dim(projectRoot)}...
11347
11422
  `);
@@ -11349,16 +11424,28 @@ async function handleCreate(projectRoot) {
11349
11424
  if (!created) {
11350
11425
  process.stderr.write(`${red("✗")} Failed to create ${provider} sandbox (${name}).
11351
11426
  `);
11352
- failed = true;
11353
- continue;
11427
+ return { provider, name, created: false, existed: false };
11354
11428
  }
11355
11429
  process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold(name)}
11356
11430
  `);
11357
- readySandboxes[provider] = name;
11431
+ return { provider, name, created: true, existed: false };
11432
+ }));
11433
+ for (const result of createResults) {
11434
+ if (result.created || result.existed) {
11435
+ readySandboxes[result.provider] = result.name;
11436
+ if (result.created)
11437
+ newlyCreated.add(result.name);
11438
+ } else {
11439
+ failed = true;
11440
+ }
11358
11441
  }
11359
11442
  config.sandbox.enabled = true;
11360
11443
  config.sandbox.providers = readySandboxes;
11361
11444
  saveConfig(projectRoot, config);
11445
+ await Promise.all(PROVIDERS.filter((provider) => {
11446
+ const sandboxName = readySandboxes[provider];
11447
+ return sandboxName && newlyCreated.has(sandboxName);
11448
+ }).map((provider) => runSandboxSetup(readySandboxes[provider], projectRoot)));
11362
11449
  if (failed) {
11363
11450
  process.stderr.write(`
11364
11451
  ${yellow("⚠")} Some sandboxes failed to create. Re-run ${cyan("locus sandbox")} after resolving Docker issues.
@@ -11432,7 +11519,7 @@ function handleRemove(projectRoot) {
11432
11519
  process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
11433
11520
  `);
11434
11521
  try {
11435
- execSync17(`docker sandbox rm ${sandboxName}`, {
11522
+ execSync16(`docker sandbox rm ${sandboxName}`, {
11436
11523
  encoding: "utf-8",
11437
11524
  stdio: ["pipe", "pipe", "pipe"],
11438
11525
  timeout: 15000
@@ -11607,26 +11694,6 @@ function parseSandboxExecArgs(args) {
11607
11694
  }
11608
11695
  return { provider, command };
11609
11696
  }
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
11697
  async function handleShell(projectRoot, args) {
11631
11698
  const provider = args[0];
11632
11699
  if (provider !== "claude" && provider !== "codex") {
@@ -11724,6 +11791,104 @@ async function handleLogs(projectRoot, args) {
11724
11791
  dockerArgs.push(sandboxName);
11725
11792
  await runInteractiveCommand("docker", dockerArgs);
11726
11793
  }
11794
+ function detectPackageManager(projectRoot) {
11795
+ try {
11796
+ const raw = readFileSync17(join22(projectRoot, "package.json"), "utf-8");
11797
+ const pkgJson = JSON.parse(raw);
11798
+ if (typeof pkgJson.packageManager === "string") {
11799
+ const name = pkgJson.packageManager.split("@")[0];
11800
+ if (name === "bun" || name === "npm" || name === "yarn" || name === "pnpm") {
11801
+ return name;
11802
+ }
11803
+ }
11804
+ } catch {}
11805
+ if (existsSync22(join22(projectRoot, "bun.lock")) || existsSync22(join22(projectRoot, "bun.lockb"))) {
11806
+ return "bun";
11807
+ }
11808
+ if (existsSync22(join22(projectRoot, "yarn.lock"))) {
11809
+ return "yarn";
11810
+ }
11811
+ if (existsSync22(join22(projectRoot, "pnpm-lock.yaml"))) {
11812
+ return "pnpm";
11813
+ }
11814
+ return "npm";
11815
+ }
11816
+ function getInstallCommand(pm) {
11817
+ switch (pm) {
11818
+ case "bun":
11819
+ return ["bun", "install"];
11820
+ case "yarn":
11821
+ return ["yarn", "install"];
11822
+ case "pnpm":
11823
+ return ["pnpm", "install"];
11824
+ case "npm":
11825
+ return ["npm", "install"];
11826
+ }
11827
+ }
11828
+ async function runSandboxSetup(sandboxName, projectRoot) {
11829
+ const pm = detectPackageManager(projectRoot);
11830
+ if (pm !== "npm") {
11831
+ await ensurePackageManagerInSandbox(sandboxName, pm);
11832
+ }
11833
+ const installCmd = getInstallCommand(pm);
11834
+ process.stderr.write(`
11835
+ Installing dependencies (${bold(installCmd.join(" "))}) in sandbox ${dim(sandboxName)}...
11836
+ `);
11837
+ const installOk = await runInteractiveCommand("docker", [
11838
+ "sandbox",
11839
+ "exec",
11840
+ "-w",
11841
+ projectRoot,
11842
+ sandboxName,
11843
+ ...installCmd
11844
+ ]);
11845
+ if (!installOk) {
11846
+ process.stderr.write(`${red("✗")} Dependency install failed in sandbox ${dim(sandboxName)}.
11847
+ `);
11848
+ return false;
11849
+ }
11850
+ process.stderr.write(`${green("✓")} Dependencies installed in sandbox ${dim(sandboxName)}.
11851
+ `);
11852
+ const setupScript = join22(projectRoot, ".locus", "sandbox-setup.sh");
11853
+ if (existsSync22(setupScript)) {
11854
+ process.stderr.write(`Running ${bold(".locus/sandbox-setup.sh")} in sandbox ${dim(sandboxName)}...
11855
+ `);
11856
+ const hookOk = await runInteractiveCommand("docker", [
11857
+ "sandbox",
11858
+ "exec",
11859
+ "-w",
11860
+ projectRoot,
11861
+ sandboxName,
11862
+ "sh",
11863
+ setupScript
11864
+ ]);
11865
+ if (!hookOk) {
11866
+ process.stderr.write(`${yellow("⚠")} Setup hook failed in sandbox ${dim(sandboxName)}.
11867
+ `);
11868
+ }
11869
+ }
11870
+ return true;
11871
+ }
11872
+ async function handleSetup(projectRoot) {
11873
+ const config = loadConfig(projectRoot);
11874
+ const providers = config.sandbox.providers;
11875
+ if (!providers.claude && !providers.codex) {
11876
+ process.stderr.write(`${red("✗")} No sandboxes configured. Run ${cyan("locus sandbox")} first.
11877
+ `);
11878
+ return;
11879
+ }
11880
+ for (const provider of PROVIDERS) {
11881
+ const sandboxName = providers[provider];
11882
+ if (!sandboxName)
11883
+ continue;
11884
+ if (!isSandboxAlive(sandboxName)) {
11885
+ process.stderr.write(`${yellow("⚠")} ${provider} sandbox is not running: ${dim(sandboxName)}
11886
+ `);
11887
+ continue;
11888
+ }
11889
+ await runSandboxSetup(sandboxName, projectRoot);
11890
+ }
11891
+ }
11727
11892
  function buildProviderSandboxNames(projectRoot) {
11728
11893
  const segment = sanitizeSegment(basename4(projectRoot));
11729
11894
  const hash = createHash("sha1").update(projectRoot).digest("hex").slice(0, 8);
@@ -11768,7 +11933,7 @@ function runInteractiveCommand(command, args) {
11768
11933
  }
11769
11934
  async function createProviderSandbox(provider, sandboxName, projectRoot) {
11770
11935
  try {
11771
- execSync17(`docker sandbox run --name ${sandboxName} claude ${projectRoot} -- --version`, {
11936
+ execSync16(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
11772
11937
  stdio: ["pipe", "pipe", "pipe"],
11773
11938
  timeout: 120000
11774
11939
  });
@@ -11782,9 +11947,30 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
11782
11947
  await enforceSandboxIgnore(sandboxName, projectRoot);
11783
11948
  return true;
11784
11949
  }
11950
+ async function ensurePackageManagerInSandbox(sandboxName, pm) {
11951
+ try {
11952
+ execSync16(`docker sandbox exec ${sandboxName} which ${pm}`, {
11953
+ stdio: ["pipe", "pipe", "pipe"],
11954
+ timeout: 5000
11955
+ });
11956
+ } catch {
11957
+ const npmPkg = pm === "bun" ? "bun" : pm === "yarn" ? "yarn" : "pnpm";
11958
+ process.stderr.write(`Installing ${bold(pm)} in sandbox...
11959
+ `);
11960
+ try {
11961
+ execSync16(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
11962
+ stdio: "inherit",
11963
+ timeout: 120000
11964
+ });
11965
+ } catch {
11966
+ process.stderr.write(`${yellow("⚠")} Failed to install ${pm} in sandbox. Dependency install may fail.
11967
+ `);
11968
+ }
11969
+ }
11970
+ }
11785
11971
  async function ensureCodexInSandbox(sandboxName) {
11786
11972
  try {
11787
- execSync17(`docker sandbox exec ${sandboxName} which codex`, {
11973
+ execSync16(`docker sandbox exec ${sandboxName} which codex`, {
11788
11974
  stdio: ["pipe", "pipe", "pipe"],
11789
11975
  timeout: 5000
11790
11976
  });
@@ -11792,7 +11978,7 @@ async function ensureCodexInSandbox(sandboxName) {
11792
11978
  process.stderr.write(`Installing codex in sandbox...
11793
11979
  `);
11794
11980
  try {
11795
- execSync17(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
11981
+ execSync16(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
11796
11982
  } catch {
11797
11983
  process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
11798
11984
  `);
@@ -11801,7 +11987,7 @@ async function ensureCodexInSandbox(sandboxName) {
11801
11987
  }
11802
11988
  function isSandboxAlive(name) {
11803
11989
  try {
11804
- const output = execSync17("docker sandbox ls", {
11990
+ const output = execSync16("docker sandbox ls", {
11805
11991
  encoding: "utf-8",
11806
11992
  stdio: ["pipe", "pipe", "pipe"],
11807
11993
  timeout: 5000
@@ -11826,17 +12012,17 @@ init_context();
11826
12012
  init_logger();
11827
12013
  init_rate_limiter();
11828
12014
  init_terminal();
11829
- import { existsSync as existsSync22, readFileSync as readFileSync17 } from "node:fs";
11830
- import { join as join22 } from "node:path";
12015
+ import { existsSync as existsSync23, readFileSync as readFileSync18 } from "node:fs";
12016
+ import { join as join23 } from "node:path";
11831
12017
  import { fileURLToPath } from "node:url";
11832
12018
  function getCliVersion() {
11833
12019
  const fallbackVersion = "0.0.0";
11834
- const packageJsonPath = join22(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
11835
- if (!existsSync22(packageJsonPath)) {
12020
+ const packageJsonPath = join23(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
12021
+ if (!existsSync23(packageJsonPath)) {
11836
12022
  return fallbackVersion;
11837
12023
  }
11838
12024
  try {
11839
- const parsed = JSON.parse(readFileSync17(packageJsonPath, "utf-8"));
12025
+ const parsed = JSON.parse(readFileSync18(packageJsonPath, "utf-8"));
11840
12026
  return parsed.version ?? fallbackVersion;
11841
12027
  } catch {
11842
12028
  return fallbackVersion;
@@ -11950,9 +12136,41 @@ function parseArgs(argv) {
11950
12136
  const args = positional.slice(1);
11951
12137
  return { command, args, flags };
11952
12138
  }
12139
+ var LOCUS_LOGO2 = [
12140
+ " ▄█ ",
12141
+ "▄▄████▄▄▄▄ ",
12142
+ "████▀ ████ ",
12143
+ "████▄▄████ ",
12144
+ "▀█▄█▀███▀▀ ▄▄██▄▄ ",
12145
+ "████ ▄██████▀███▄ ",
12146
+ "████▄██▀ ▀▀███ ▄████ ",
12147
+ "▀▀██████▄ ██████▀ ",
12148
+ " ▀▀█████▄▄██▀▀ ",
12149
+ " ▀▀██▀▀ "
12150
+ ];
12151
+ function printLogo() {
12152
+ process.stderr.write(`
12153
+ `);
12154
+ const logoWidth = 10;
12155
+ const gap = " ";
12156
+ const infoLines = [
12157
+ `${bold("Locus")} ${dim(`v${VERSION}`)}`,
12158
+ `${dim("GitHub-native AI engineering assistant")}`
12159
+ ];
12160
+ const textOffset = 1;
12161
+ const totalLines = Math.max(LOCUS_LOGO2.length, infoLines.length + textOffset);
12162
+ for (let i = 0;i < totalLines; i++) {
12163
+ const logoLine = LOCUS_LOGO2[i] ?? "";
12164
+ const paddedLogo = logoLine.padEnd(logoWidth);
12165
+ const infoIdx = i - textOffset;
12166
+ const infoLine = infoIdx >= 0 && infoIdx < infoLines.length ? infoLines[infoIdx] : "";
12167
+ process.stderr.write(`${bold(white(paddedLogo))}${gap}${infoLine}
12168
+ `);
12169
+ }
12170
+ }
11953
12171
  function printHelp6() {
12172
+ printLogo();
11954
12173
  process.stderr.write(`
11955
- ${bold("Locus")} ${dim(`v${VERSION}`)} — GitHub-native AI engineering assistant
11956
12174
 
11957
12175
  ${bold("Usage:")}
11958
12176
  locus <command> [options]
@@ -12065,7 +12283,7 @@ async function main() {
12065
12283
  try {
12066
12284
  const root = getGitRoot(cwd);
12067
12285
  if (isInitialized(root)) {
12068
- logDir = join22(root, ".locus", "logs");
12286
+ logDir = join23(root, ".locus", "logs");
12069
12287
  getRateLimiter(root);
12070
12288
  }
12071
12289
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.19.2",
3
+ "version": "0.20.1",
4
4
  "description": "GitHub-native AI engineering assistant",
5
5
  "type": "module",
6
6
  "bin": {