@locusai/cli 0.8.0 → 0.8.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.
package/README.md CHANGED
@@ -0,0 +1,155 @@
1
+ # @locusai/cli
2
+
3
+ Command-line interface for [Locus](https://locusai.dev) - an AI-native project management platform for engineering teams.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @locusai/cli
9
+ ```
10
+
11
+ Or with other package managers:
12
+
13
+ ```bash
14
+ # pnpm
15
+ pnpm add -g @locusai/cli
16
+
17
+ # yarn
18
+ yarn global add @locusai/cli
19
+
20
+ # bun
21
+ bun add -g @locusai/cli
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```bash
27
+ # Initialize Locus in your project
28
+ locus init
29
+
30
+ # Index your codebase for AI context
31
+ locus index
32
+
33
+ # Run an agent to work on tasks
34
+ locus run --api-key YOUR_API_KEY
35
+
36
+ # Execute a prompt with repository context
37
+ locus exec "Explain the authentication flow"
38
+ ```
39
+
40
+ ## Commands
41
+
42
+ ### `locus init`
43
+
44
+ Initialize Locus in the current directory. Creates the necessary configuration files and directory structure:
45
+
46
+ - `.locus/` - Configuration directory
47
+ - `.locus/config.json` - Project settings
48
+ - `CLAUDE.md` - AI instructions and context
49
+ - `.agent/skills/` - Domain-specific agent skills
50
+
51
+ Running `init` on an already initialized project will update the configuration to the latest version.
52
+
53
+ ### `locus index`
54
+
55
+ Index the codebase for AI context. This analyzes your project structure and creates a searchable index that helps AI agents understand your codebase.
56
+
57
+ ```bash
58
+ locus index [options]
59
+
60
+ Options:
61
+ --dir <path> Project directory (default: current directory)
62
+ --model <name> AI model to use
63
+ --provider <name> AI provider: claude or codex (default: claude)
64
+ ```
65
+
66
+ ### `locus run`
67
+
68
+ Start an agent to work on tasks from your Locus workspace.
69
+
70
+ ```bash
71
+ locus run [options]
72
+
73
+ Options:
74
+ --api-key <key> Your Locus API key (required)
75
+ --workspace <id> Workspace ID to connect to
76
+ --sprint <id> Sprint ID to work on
77
+ --model <name> AI model to use
78
+ --provider <name> AI provider: claude or codex (default: claude)
79
+ --api-url <url> Custom API URL
80
+ --dir <path> Project directory (default: current directory)
81
+ --skip-planning Skip the planning phase
82
+ ```
83
+
84
+ ### `locus exec`
85
+
86
+ Run a prompt with repository context. Supports both single execution and interactive REPL mode.
87
+
88
+ ```bash
89
+ locus exec "your prompt" [options]
90
+ locus exec --interactive [options]
91
+
92
+ Options:
93
+ --interactive, -i Start interactive REPL mode
94
+ --session, -s <id> Resume a previous session
95
+ --model <name> AI model to use
96
+ --provider <name> AI provider: claude or codex (default: claude)
97
+ --dir <path> Project directory (default: current directory)
98
+ --no-stream Disable streaming output
99
+ --no-status Disable status display
100
+ ```
101
+
102
+ #### Session Management
103
+
104
+ Manage your exec sessions with these subcommands:
105
+
106
+ ```bash
107
+ # List recent sessions
108
+ locus exec sessions list
109
+
110
+ # Show messages from a session
111
+ locus exec sessions show <session-id>
112
+
113
+ # Delete a session
114
+ locus exec sessions delete <session-id>
115
+
116
+ # Clear all sessions
117
+ locus exec sessions clear
118
+ ```
119
+
120
+ ## Configuration
121
+
122
+ Locus stores its configuration in the `.locus/` directory within your project:
123
+
124
+ - `config.json` - Project settings including workspace ID and version
125
+ - `codebase-index.json` - Indexed codebase structure
126
+
127
+ The `CLAUDE.md` file in your project root provides AI instructions and context that agents use when working on your codebase.
128
+
129
+ ## AI Providers
130
+
131
+ Locus supports multiple AI providers:
132
+
133
+ - **Claude** (default) - Anthropic's Claude models
134
+ - **Codex** - OpenAI Codex models
135
+
136
+ Specify the provider with the `--provider` flag:
137
+
138
+ ```bash
139
+ locus exec "your prompt" --provider codex
140
+ locus run --api-key YOUR_KEY --provider claude
141
+ ```
142
+
143
+ ## Requirements
144
+
145
+ - Node.js 18 or later
146
+ - A Locus API key (for `run` command)
147
+
148
+ ## Links
149
+
150
+ - [Documentation](https://locusai.dev/docs)
151
+ - [Website](https://locusai.dev)
152
+
153
+ ## License
154
+
155
+ MIT
@@ -17011,6 +17011,10 @@ var require_ignore = __commonJS((exports, module) => {
17011
17011
  define(module.exports, Symbol.for("setupWindows"), setupWindows);
17012
17012
  });
17013
17013
 
17014
+ // ../sdk/src/agent/worker.ts
17015
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
17016
+ import { join as join6 } from "node:path";
17017
+
17014
17018
  // ../sdk/src/core/config.ts
17015
17019
  import { join } from "node:path";
17016
17020
  var PROVIDER = {
@@ -17029,7 +17033,9 @@ var LOCUS_CONFIG = {
17029
17033
  artifactsDir: "artifacts",
17030
17034
  documentsDir: "documents",
17031
17035
  agentSkillsDir: ".agent/skills",
17032
- sessionsDir: "sessions"
17036
+ sessionsDir: "sessions",
17037
+ reviewsDir: "reviews",
17038
+ plansDir: "plans"
17033
17039
  };
17034
17040
  function getLocusPath(projectPath, fileName) {
17035
17041
  if (fileName === "contextFile") {
@@ -17120,7 +17126,7 @@ class ClaudeRunner {
17120
17126
  setEventEmitter(emitter) {
17121
17127
  this.eventEmitter = emitter;
17122
17128
  }
17123
- async run(prompt, _isPlanning = false) {
17129
+ async run(prompt) {
17124
17130
  const maxRetries = 3;
17125
17131
  let lastError = null;
17126
17132
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
@@ -36686,7 +36692,7 @@ File tree:
36686
36692
  ${tree}
36687
36693
 
36688
36694
  Return ONLY valid JSON, no markdown formatting.`;
36689
- const response = await this.deps.aiRunner.run(prompt, true);
36695
+ const response = await this.deps.aiRunner.run(prompt);
36690
36696
  const jsonMatch = response.match(/\{[\s\S]*\}/);
36691
36697
  if (jsonMatch) {
36692
36698
  return JSON.parse(jsonMatch[0]);
@@ -36749,6 +36755,66 @@ class DocumentFetcher {
36749
36755
  }
36750
36756
  }
36751
36757
 
36758
+ // ../sdk/src/agent/review-service.ts
36759
+ import { execSync } from "node:child_process";
36760
+
36761
+ class ReviewService {
36762
+ deps;
36763
+ constructor(deps) {
36764
+ this.deps = deps;
36765
+ }
36766
+ async reviewStagedChanges(sprint2) {
36767
+ const { projectPath, log } = this.deps;
36768
+ try {
36769
+ execSync("git add -A", { cwd: projectPath, stdio: "pipe" });
36770
+ log("Staged all changes for review.", "info");
36771
+ } catch (err) {
36772
+ log(`Failed to stage changes: ${err instanceof Error ? err.message : String(err)}`, "error");
36773
+ return null;
36774
+ }
36775
+ let diff;
36776
+ try {
36777
+ diff = execSync("git diff --cached --stat && echo '---' && git diff --cached", {
36778
+ cwd: projectPath,
36779
+ maxBuffer: 10 * 1024 * 1024
36780
+ }).toString();
36781
+ } catch (err) {
36782
+ log(`Failed to get staged diff: ${err instanceof Error ? err.message : String(err)}`, "error");
36783
+ return null;
36784
+ }
36785
+ if (!diff.trim()) {
36786
+ return null;
36787
+ }
36788
+ const sprintInfo = sprint2 ? `Sprint: ${sprint2.name} (${sprint2.id})` : "No active sprint";
36789
+ const reviewPrompt = `# Code Review Request
36790
+
36791
+ ## Context
36792
+ ${sprintInfo}
36793
+ Date: ${new Date().toISOString()}
36794
+
36795
+ ## Staged Changes (git diff)
36796
+ \`\`\`diff
36797
+ ${diff}
36798
+ \`\`\`
36799
+
36800
+ ## Instructions
36801
+ You are reviewing the staged changes at the end of a sprint. Produce a thorough markdown review report with the following sections:
36802
+
36803
+ 1. **Summary** — Brief overview of what changed and why.
36804
+ 2. **Files Changed** — List each file with a short description of changes.
36805
+ 3. **Code Quality** — Note any code quality concerns (naming, structure, complexity).
36806
+ 4. **Potential Issues** — Identify bugs, security issues, edge cases, or regressions.
36807
+ 5. **Recommendations** — Actionable suggestions for improvement.
36808
+ 6. **Overall Assessment** — A short verdict (e.g., "Looks good", "Needs attention", "Critical issues found").
36809
+
36810
+ Keep the review concise but thorough. Focus on substance over style.
36811
+ Do NOT output <promise>COMPLETE</promise> — just output the review report as markdown.`;
36812
+ log("Running AI review on staged changes...", "info");
36813
+ const report = await this.deps.aiRunner.run(reviewPrompt);
36814
+ return report;
36815
+ }
36816
+ }
36817
+
36752
36818
  // ../sdk/src/core/prompt-builder.ts
36753
36819
  import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
36754
36820
  import { homedir } from "node:os";
@@ -37113,29 +37179,8 @@ class TaskExecutor {
37113
37179
  taskContext: context
37114
37180
  });
37115
37181
  try {
37116
- let plan = null;
37117
- this.deps.log("Phase 1: Planning (CLI)...", "info");
37118
- const planningPrompt = `${basePrompt}
37119
-
37120
- ## Phase 1: Planning
37121
- Analyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`;
37122
- plan = await this.deps.aiRunner.run(planningPrompt, true);
37123
37182
  this.deps.log("Starting Execution...", "info");
37124
- let executionPrompt = basePrompt;
37125
- if (plan != null) {
37126
- executionPrompt += `
37127
-
37128
- ## Phase 2: Execution
37129
- Based on the plan, execute the task:
37130
-
37131
- ${plan}`;
37132
- } else {
37133
- executionPrompt += `
37134
-
37135
- ## Execution
37136
- Execute the task directly.`;
37137
- }
37138
- executionPrompt += `
37183
+ const executionPrompt = `${basePrompt}
37139
37184
 
37140
37185
  When finished, output: <promise>COMPLETE</promise>`;
37141
37186
  const output = await this.deps.aiRunner.run(executionPrompt);
@@ -37169,11 +37214,9 @@ class AgentWorker {
37169
37214
  indexerService;
37170
37215
  documentFetcher;
37171
37216
  taskExecutor;
37172
- consecutiveEmpty = 0;
37173
- maxEmpty = 60;
37217
+ reviewService;
37174
37218
  maxTasks = 50;
37175
37219
  tasksCompleted = 0;
37176
- pollInterval = 1e4;
37177
37220
  constructor(config2) {
37178
37221
  this.config = config2;
37179
37222
  const projectPath = config2.projectPath || process.cwd();
@@ -37210,6 +37253,11 @@ class AgentWorker {
37210
37253
  projectPath,
37211
37254
  log
37212
37255
  });
37256
+ this.reviewService = new ReviewService({
37257
+ aiRunner: this.aiRunner,
37258
+ projectPath,
37259
+ log
37260
+ });
37213
37261
  const providerLabel = provider === "codex" ? "Codex" : "Claude";
37214
37262
  this.log(`Using ${providerLabel} CLI for all phases`, "info");
37215
37263
  }
@@ -37255,33 +37303,42 @@ class AgentWorker {
37255
37303
  await this.indexerService.reindex();
37256
37304
  return result;
37257
37305
  }
37306
+ async runStagedChangesReview(sprint2) {
37307
+ try {
37308
+ const report = await this.reviewService.reviewStagedChanges(sprint2);
37309
+ if (report) {
37310
+ const reviewsDir = join6(this.config.projectPath, LOCUS_CONFIG.dir, "reviews");
37311
+ if (!existsSync5(reviewsDir)) {
37312
+ mkdirSync3(reviewsDir, { recursive: true });
37313
+ }
37314
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
37315
+ const sprintSlug = sprint2?.name ? sprint2.name.toLowerCase().replace(/\s+/g, "-").slice(0, 40) : "no-sprint";
37316
+ const fileName = `review-${sprintSlug}-${timestamp}.md`;
37317
+ const filePath = join6(reviewsDir, fileName);
37318
+ writeFileSync3(filePath, report);
37319
+ this.log(`Review report saved to .locus/reviews/${fileName}`, "success");
37320
+ } else {
37321
+ this.log("No staged changes to review.", "info");
37322
+ }
37323
+ } catch (err) {
37324
+ this.log(`Review failed: ${err instanceof Error ? err.message : String(err)}`, "error");
37325
+ }
37326
+ }
37258
37327
  async run() {
37259
37328
  this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
37260
37329
  const sprint2 = await this.getActiveSprint();
37261
37330
  if (sprint2) {
37262
- this.log(`Active sprint found: ${sprint2.name}. Ensuring plan is up to date...`, "info");
37263
- try {
37264
- await this.client.sprints.triggerAIPlanning(sprint2.id, this.config.workspaceId);
37265
- this.log(`Sprint plan sync checked on server.`, "success");
37266
- } catch (err) {
37267
- this.log(`Sprint planning sync failed (non-critical): ${err instanceof Error ? err.message : String(err)}`, "warn");
37268
- }
37331
+ this.log(`Active sprint found: ${sprint2.name}`, "info");
37269
37332
  } else {
37270
- this.log("No active sprint found for planning.", "warn");
37333
+ this.log("No active sprint found.", "warn");
37271
37334
  }
37272
- while (this.tasksCompleted < this.maxTasks && this.consecutiveEmpty < this.maxEmpty) {
37335
+ while (this.tasksCompleted < this.maxTasks) {
37273
37336
  const task2 = await this.getNextTask();
37274
37337
  if (!task2) {
37275
- if (this.consecutiveEmpty === 0) {
37276
- this.log("Queue empty, waiting for tasks...", "info");
37277
- }
37278
- this.consecutiveEmpty++;
37279
- if (this.consecutiveEmpty >= this.maxEmpty)
37280
- break;
37281
- await new Promise((r) => setTimeout(r, this.pollInterval));
37282
- continue;
37338
+ this.log("No tasks remaining. Running review on staged changes...", "info");
37339
+ await this.runStagedChangesReview(sprint2);
37340
+ break;
37283
37341
  }
37284
- this.consecutiveEmpty = 0;
37285
37342
  this.log(`Claimed: ${task2.title}`, "success");
37286
37343
  const result = await this.executeTask(task2);
37287
37344
  try {
package/bin/locus.js CHANGED
@@ -6341,7 +6341,7 @@ File tree:
6341
6341
  ${tree}
6342
6342
 
6343
6343
  Return ONLY valid JSON, no markdown formatting.`;
6344
- const response = await this.deps.aiRunner.run(prompt, true);
6344
+ const response = await this.deps.aiRunner.run(prompt);
6345
6345
  const jsonMatch = response.match(/\{[\s\S]*\}/);
6346
6346
  if (jsonMatch) {
6347
6347
  return JSON.parse(jsonMatch[0]);
@@ -6389,14 +6389,22 @@ var init_config = __esm(() => {
6389
6389
  artifactsDir: "artifacts",
6390
6390
  documentsDir: "documents",
6391
6391
  agentSkillsDir: ".agent/skills",
6392
- sessionsDir: "sessions"
6392
+ sessionsDir: "sessions",
6393
+ reviewsDir: "reviews",
6394
+ plansDir: "plans"
6393
6395
  };
6394
6396
  LOCUS_GITIGNORE_PATTERNS = [
6395
6397
  "# Locus AI - Session data (user-specific, can grow large)",
6396
6398
  ".locus/sessions/",
6397
6399
  "",
6398
6400
  "# Locus AI - Artifacts (local-only, user-specific)",
6399
- ".locus/artifacts/"
6401
+ ".locus/artifacts/",
6402
+ "",
6403
+ "# Locus AI - Review reports (generated per sprint)",
6404
+ ".locus/reviews/",
6405
+ "",
6406
+ "# Locus AI - Plans (generated per task)",
6407
+ ".locus/plans/"
6400
6408
  ];
6401
6409
  });
6402
6410
 
@@ -6448,6 +6456,67 @@ var init_document_fetcher = __esm(() => {
6448
6456
  init_config();
6449
6457
  });
6450
6458
 
6459
+ // ../sdk/src/agent/review-service.ts
6460
+ import { execSync } from "node:child_process";
6461
+
6462
+ class ReviewService {
6463
+ deps;
6464
+ constructor(deps) {
6465
+ this.deps = deps;
6466
+ }
6467
+ async reviewStagedChanges(sprint) {
6468
+ const { projectPath, log } = this.deps;
6469
+ try {
6470
+ execSync("git add -A", { cwd: projectPath, stdio: "pipe" });
6471
+ log("Staged all changes for review.", "info");
6472
+ } catch (err) {
6473
+ log(`Failed to stage changes: ${err instanceof Error ? err.message : String(err)}`, "error");
6474
+ return null;
6475
+ }
6476
+ let diff;
6477
+ try {
6478
+ diff = execSync("git diff --cached --stat && echo '---' && git diff --cached", {
6479
+ cwd: projectPath,
6480
+ maxBuffer: 10 * 1024 * 1024
6481
+ }).toString();
6482
+ } catch (err) {
6483
+ log(`Failed to get staged diff: ${err instanceof Error ? err.message : String(err)}`, "error");
6484
+ return null;
6485
+ }
6486
+ if (!diff.trim()) {
6487
+ return null;
6488
+ }
6489
+ const sprintInfo = sprint ? `Sprint: ${sprint.name} (${sprint.id})` : "No active sprint";
6490
+ const reviewPrompt = `# Code Review Request
6491
+
6492
+ ## Context
6493
+ ${sprintInfo}
6494
+ Date: ${new Date().toISOString()}
6495
+
6496
+ ## Staged Changes (git diff)
6497
+ \`\`\`diff
6498
+ ${diff}
6499
+ \`\`\`
6500
+
6501
+ ## Instructions
6502
+ You are reviewing the staged changes at the end of a sprint. Produce a thorough markdown review report with the following sections:
6503
+
6504
+ 1. **Summary** — Brief overview of what changed and why.
6505
+ 2. **Files Changed** — List each file with a short description of changes.
6506
+ 3. **Code Quality** — Note any code quality concerns (naming, structure, complexity).
6507
+ 4. **Potential Issues** — Identify bugs, security issues, edge cases, or regressions.
6508
+ 5. **Recommendations** — Actionable suggestions for improvement.
6509
+ 6. **Overall Assessment** — A short verdict (e.g., "Looks good", "Needs attention", "Critical issues found").
6510
+
6511
+ Keep the review concise but thorough. Focus on substance over style.
6512
+ Do NOT output <promise>COMPLETE</promise> — just output the review report as markdown.`;
6513
+ log("Running AI review on staged changes...", "info");
6514
+ const report = await this.deps.aiRunner.run(reviewPrompt);
6515
+ return report;
6516
+ }
6517
+ }
6518
+ var init_review_service = () => {};
6519
+
6451
6520
  // ../../node_modules/zod/v4/core/core.js
6452
6521
  function $constructor(name, initializer, params) {
6453
6522
  function init(inst, def) {
@@ -21645,29 +21714,8 @@ class TaskExecutor {
21645
21714
  taskContext: context
21646
21715
  });
21647
21716
  try {
21648
- let plan = null;
21649
- this.deps.log("Phase 1: Planning (CLI)...", "info");
21650
- const planningPrompt = `${basePrompt}
21651
-
21652
- ## Phase 1: Planning
21653
- Analyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`;
21654
- plan = await this.deps.aiRunner.run(planningPrompt, true);
21655
21717
  this.deps.log("Starting Execution...", "info");
21656
- let executionPrompt = basePrompt;
21657
- if (plan != null) {
21658
- executionPrompt += `
21659
-
21660
- ## Phase 2: Execution
21661
- Based on the plan, execute the task:
21662
-
21663
- ${plan}`;
21664
- } else {
21665
- executionPrompt += `
21666
-
21667
- ## Execution
21668
- Execute the task directly.`;
21669
- }
21670
- executionPrompt += `
21718
+ const executionPrompt = `${basePrompt}
21671
21719
 
21672
21720
  When finished, output: <promise>COMPLETE</promise>`;
21673
21721
  const output = await this.deps.aiRunner.run(executionPrompt);
@@ -21768,7 +21816,7 @@ class ClaudeRunner {
21768
21816
  setEventEmitter(emitter) {
21769
21817
  this.eventEmitter = emitter;
21770
21818
  }
21771
- async run(prompt, _isPlanning = false) {
21819
+ async run(prompt) {
21772
21820
  const maxRetries = 3;
21773
21821
  let lastError = null;
21774
21822
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
@@ -37854,6 +37902,8 @@ var init_src2 = __esm(() => {
37854
37902
  });
37855
37903
 
37856
37904
  // ../sdk/src/agent/worker.ts
37905
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
37906
+ import { join as join6 } from "node:path";
37857
37907
  function resolveProvider(value) {
37858
37908
  if (!value || value.startsWith("--")) {
37859
37909
  console.warn("Warning: --provider requires a value. Falling back to 'claude'.");
@@ -37872,11 +37922,9 @@ class AgentWorker {
37872
37922
  indexerService;
37873
37923
  documentFetcher;
37874
37924
  taskExecutor;
37875
- consecutiveEmpty = 0;
37876
- maxEmpty = 60;
37925
+ reviewService;
37877
37926
  maxTasks = 50;
37878
37927
  tasksCompleted = 0;
37879
- pollInterval = 1e4;
37880
37928
  constructor(config2) {
37881
37929
  this.config = config2;
37882
37930
  const projectPath = config2.projectPath || process.cwd();
@@ -37913,6 +37961,11 @@ class AgentWorker {
37913
37961
  projectPath,
37914
37962
  log
37915
37963
  });
37964
+ this.reviewService = new ReviewService({
37965
+ aiRunner: this.aiRunner,
37966
+ projectPath,
37967
+ log
37968
+ });
37916
37969
  const providerLabel = provider === "codex" ? "Codex" : "Claude";
37917
37970
  this.log(`Using ${providerLabel} CLI for all phases`, "info");
37918
37971
  }
@@ -37958,33 +38011,42 @@ class AgentWorker {
37958
38011
  await this.indexerService.reindex();
37959
38012
  return result;
37960
38013
  }
38014
+ async runStagedChangesReview(sprint2) {
38015
+ try {
38016
+ const report = await this.reviewService.reviewStagedChanges(sprint2);
38017
+ if (report) {
38018
+ const reviewsDir = join6(this.config.projectPath, LOCUS_CONFIG.dir, "reviews");
38019
+ if (!existsSync5(reviewsDir)) {
38020
+ mkdirSync3(reviewsDir, { recursive: true });
38021
+ }
38022
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
38023
+ const sprintSlug = sprint2?.name ? sprint2.name.toLowerCase().replace(/\s+/g, "-").slice(0, 40) : "no-sprint";
38024
+ const fileName = `review-${sprintSlug}-${timestamp}.md`;
38025
+ const filePath = join6(reviewsDir, fileName);
38026
+ writeFileSync3(filePath, report);
38027
+ this.log(`Review report saved to .locus/reviews/${fileName}`, "success");
38028
+ } else {
38029
+ this.log("No staged changes to review.", "info");
38030
+ }
38031
+ } catch (err) {
38032
+ this.log(`Review failed: ${err instanceof Error ? err.message : String(err)}`, "error");
38033
+ }
38034
+ }
37961
38035
  async run() {
37962
38036
  this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
37963
38037
  const sprint2 = await this.getActiveSprint();
37964
38038
  if (sprint2) {
37965
- this.log(`Active sprint found: ${sprint2.name}. Ensuring plan is up to date...`, "info");
37966
- try {
37967
- await this.client.sprints.triggerAIPlanning(sprint2.id, this.config.workspaceId);
37968
- this.log(`Sprint plan sync checked on server.`, "success");
37969
- } catch (err) {
37970
- this.log(`Sprint planning sync failed (non-critical): ${err instanceof Error ? err.message : String(err)}`, "warn");
37971
- }
38039
+ this.log(`Active sprint found: ${sprint2.name}`, "info");
37972
38040
  } else {
37973
- this.log("No active sprint found for planning.", "warn");
38041
+ this.log("No active sprint found.", "warn");
37974
38042
  }
37975
- while (this.tasksCompleted < this.maxTasks && this.consecutiveEmpty < this.maxEmpty) {
38043
+ while (this.tasksCompleted < this.maxTasks) {
37976
38044
  const task2 = await this.getNextTask();
37977
38045
  if (!task2) {
37978
- if (this.consecutiveEmpty === 0) {
37979
- this.log("Queue empty, waiting for tasks...", "info");
37980
- }
37981
- this.consecutiveEmpty++;
37982
- if (this.consecutiveEmpty >= this.maxEmpty)
37983
- break;
37984
- await new Promise((r) => setTimeout(r, this.pollInterval));
37985
- continue;
38046
+ this.log("No tasks remaining. Running review on staged changes...", "info");
38047
+ await this.runStagedChangesReview(sprint2);
38048
+ break;
37986
38049
  }
37987
- this.consecutiveEmpty = 0;
37988
38050
  this.log(`Claimed: ${task2.title}`, "success");
37989
38051
  const result = await this.executeTask(task2);
37990
38052
  try {
@@ -38024,6 +38086,7 @@ var init_worker = __esm(() => {
38024
38086
  init_colors();
38025
38087
  init_codebase_indexer_service();
38026
38088
  init_document_fetcher();
38089
+ init_review_service();
38027
38090
  init_task_executor();
38028
38091
  if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("worker")) {
38029
38092
  process.title = "locus-worker";
@@ -38068,6 +38131,7 @@ var init_worker = __esm(() => {
38068
38131
  var init_agent2 = __esm(() => {
38069
38132
  init_codebase_indexer_service();
38070
38133
  init_document_fetcher();
38134
+ init_review_service();
38071
38135
  init_task_executor();
38072
38136
  init_worker();
38073
38137
  });
@@ -38493,14 +38557,14 @@ var init_event_emitter = __esm(() => {
38493
38557
 
38494
38558
  // ../sdk/src/exec/history-manager.ts
38495
38559
  import {
38496
- existsSync as existsSync5,
38497
- mkdirSync as mkdirSync3,
38560
+ existsSync as existsSync6,
38561
+ mkdirSync as mkdirSync4,
38498
38562
  readdirSync as readdirSync2,
38499
38563
  readFileSync as readFileSync5,
38500
38564
  rmSync,
38501
- writeFileSync as writeFileSync3
38565
+ writeFileSync as writeFileSync4
38502
38566
  } from "node:fs";
38503
- import { join as join6 } from "node:path";
38567
+ import { join as join7 } from "node:path";
38504
38568
  function generateSessionId2() {
38505
38569
  const timestamp = Date.now().toString(36);
38506
38570
  const random = Math.random().toString(36).substring(2, 9);
@@ -38511,26 +38575,26 @@ class HistoryManager {
38511
38575
  historyDir;
38512
38576
  maxSessions;
38513
38577
  constructor(projectPath, options) {
38514
- this.historyDir = options?.historyDir ?? join6(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
38578
+ this.historyDir = options?.historyDir ?? join7(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
38515
38579
  this.maxSessions = options?.maxSessions ?? DEFAULT_MAX_SESSIONS;
38516
38580
  this.ensureHistoryDir();
38517
38581
  }
38518
38582
  ensureHistoryDir() {
38519
- if (!existsSync5(this.historyDir)) {
38520
- mkdirSync3(this.historyDir, { recursive: true });
38583
+ if (!existsSync6(this.historyDir)) {
38584
+ mkdirSync4(this.historyDir, { recursive: true });
38521
38585
  }
38522
38586
  }
38523
38587
  getSessionPath(sessionId) {
38524
- return join6(this.historyDir, `${sessionId}.json`);
38588
+ return join7(this.historyDir, `${sessionId}.json`);
38525
38589
  }
38526
38590
  saveSession(session) {
38527
38591
  const filePath = this.getSessionPath(session.id);
38528
38592
  session.updatedAt = Date.now();
38529
- writeFileSync3(filePath, JSON.stringify(session, null, 2), "utf-8");
38593
+ writeFileSync4(filePath, JSON.stringify(session, null, 2), "utf-8");
38530
38594
  }
38531
38595
  loadSession(sessionId) {
38532
38596
  const filePath = this.getSessionPath(sessionId);
38533
- if (!existsSync5(filePath)) {
38597
+ if (!existsSync6(filePath)) {
38534
38598
  return null;
38535
38599
  }
38536
38600
  try {
@@ -38542,7 +38606,7 @@ class HistoryManager {
38542
38606
  }
38543
38607
  deleteSession(sessionId) {
38544
38608
  const filePath = this.getSessionPath(sessionId);
38545
- if (!existsSync5(filePath)) {
38609
+ if (!existsSync6(filePath)) {
38546
38610
  return false;
38547
38611
  }
38548
38612
  try {
@@ -38630,7 +38694,7 @@ class HistoryManager {
38630
38694
  return files.filter((f) => f.endsWith(".json")).length;
38631
38695
  }
38632
38696
  sessionExists(sessionId) {
38633
- return existsSync5(this.getSessionPath(sessionId));
38697
+ return existsSync6(this.getSessionPath(sessionId));
38634
38698
  }
38635
38699
  findSessionByPartialId(partialId) {
38636
38700
  const sessions = this.listSessions();
@@ -38649,7 +38713,7 @@ class HistoryManager {
38649
38713
  for (const file2 of files) {
38650
38714
  if (file2.endsWith(".json")) {
38651
38715
  try {
38652
- rmSync(join6(this.historyDir, file2));
38716
+ rmSync(join7(this.historyDir, file2));
38653
38717
  deleted++;
38654
38718
  } catch {}
38655
38719
  }
@@ -38932,8 +38996,8 @@ var init_exec = __esm(() => {
38932
38996
 
38933
38997
  // ../sdk/src/orchestrator.ts
38934
38998
  import { spawn as spawn3 } from "node:child_process";
38935
- import { existsSync as existsSync6 } from "node:fs";
38936
- import { dirname as dirname2, join as join7 } from "node:path";
38999
+ import { existsSync as existsSync7 } from "node:fs";
39000
+ import { dirname as dirname2, join as join8 } from "node:path";
38937
39001
  import { fileURLToPath as fileURLToPath2 } from "node:url";
38938
39002
  import { EventEmitter as EventEmitter4 } from "events";
38939
39003
  var AgentOrchestrator;
@@ -39034,8 +39098,8 @@ ${c.success("✅ Orchestrator finished")}`);
39034
39098
  const potentialPaths = [];
39035
39099
  const currentModulePath = fileURLToPath2(import.meta.url);
39036
39100
  const currentModuleDir = dirname2(currentModulePath);
39037
- potentialPaths.push(join7(currentModuleDir, "agent", "worker.js"), join7(currentModuleDir, "worker.js"), join7(currentModuleDir, "agent", "worker.ts"));
39038
- const workerPath = potentialPaths.find((p) => existsSync6(p));
39101
+ potentialPaths.push(join8(currentModuleDir, "agent", "worker.js"), join8(currentModuleDir, "worker.js"), join8(currentModuleDir, "agent", "worker.ts"));
39102
+ const workerPath = potentialPaths.find((p) => existsSync7(p));
39039
39103
  if (!workerPath) {
39040
39104
  throw new Error(`Worker file not found. Checked: ${potentialPaths.join(", ")}. ` + `Make sure the SDK is properly built and installed.`);
39041
39105
  }
@@ -40281,22 +40345,22 @@ import { parseArgs } from "node:util";
40281
40345
  init_index_node();
40282
40346
 
40283
40347
  // src/utils/version.ts
40284
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
40285
- import { dirname as dirname3, join as join8 } from "node:path";
40348
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
40349
+ import { dirname as dirname3, join as join9 } from "node:path";
40286
40350
  import { fileURLToPath as fileURLToPath3 } from "node:url";
40287
40351
  function getVersion() {
40288
40352
  try {
40289
40353
  const __filename2 = fileURLToPath3(import.meta.url);
40290
40354
  const __dirname2 = dirname3(__filename2);
40291
- const bundledPath = join8(__dirname2, "..", "package.json");
40292
- const sourcePath = join8(__dirname2, "..", "..", "package.json");
40293
- if (existsSync7(bundledPath)) {
40355
+ const bundledPath = join9(__dirname2, "..", "package.json");
40356
+ const sourcePath = join9(__dirname2, "..", "..", "package.json");
40357
+ if (existsSync8(bundledPath)) {
40294
40358
  const pkg = JSON.parse(readFileSync6(bundledPath, "utf-8"));
40295
40359
  if (pkg.name === "@locusai/cli") {
40296
40360
  return pkg.version || "0.0.0";
40297
40361
  }
40298
40362
  }
40299
- if (existsSync7(sourcePath)) {
40363
+ if (existsSync8(sourcePath)) {
40300
40364
  const pkg = JSON.parse(readFileSync6(sourcePath, "utf-8"));
40301
40365
  if (pkg.name === "@locusai/cli") {
40302
40366
  return pkg.version || "0.0.0";
@@ -40319,12 +40383,12 @@ function printBanner() {
40319
40383
  }
40320
40384
  // src/utils/helpers.ts
40321
40385
  init_index_node();
40322
- import { existsSync as existsSync8 } from "node:fs";
40323
- import { join as join9 } from "node:path";
40386
+ import { existsSync as existsSync9 } from "node:fs";
40387
+ import { join as join10 } from "node:path";
40324
40388
  function isProjectInitialized(projectPath) {
40325
- const locusDir = join9(projectPath, LOCUS_CONFIG.dir);
40326
- const configPath = join9(locusDir, LOCUS_CONFIG.configFile);
40327
- return existsSync8(locusDir) && existsSync8(configPath);
40389
+ const locusDir = join10(projectPath, LOCUS_CONFIG.dir);
40390
+ const configPath = join10(locusDir, LOCUS_CONFIG.configFile);
40391
+ return existsSync9(locusDir) && existsSync9(configPath);
40328
40392
  }
40329
40393
  function requireInitialization(projectPath, command) {
40330
40394
  if (!isProjectInitialized(projectPath)) {
@@ -40649,6 +40713,7 @@ function showHelp2() {
40649
40713
  ${c.success("init")} Initialize Locus in the current directory
40650
40714
  ${c.success("index")} Index the codebase for AI context
40651
40715
  ${c.success("run")} Start an agent to work on tasks
40716
+ ${c.success("review")} Review staged changes with AI
40652
40717
  ${c.success("exec")} Run a prompt with repository context
40653
40718
  ${c.dim("--interactive, -i Start interactive REPL mode")}
40654
40719
  ${c.dim("--session, -s <id> Resume a previous session")}
@@ -40665,6 +40730,7 @@ function showHelp2() {
40665
40730
  ${c.dim("$")} ${c.primary("locus init")}
40666
40731
  ${c.dim("$")} ${c.primary("locus index")}
40667
40732
  ${c.dim("$")} ${c.primary("locus run --api-key YOUR_KEY")}
40733
+ ${c.dim("$")} ${c.primary("locus review")}
40668
40734
  ${c.dim("$")} ${c.primary("locus exec sessions list")}
40669
40735
 
40670
40736
  For more information, visit: ${c.underline("https://locusai.dev/docs")}
@@ -40676,8 +40742,8 @@ import { parseArgs as parseArgs2 } from "node:util";
40676
40742
 
40677
40743
  // src/config-manager.ts
40678
40744
  init_index_node();
40679
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
40680
- import { join as join10 } from "node:path";
40745
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
40746
+ import { join as join11 } from "node:path";
40681
40747
 
40682
40748
  // src/templates/skills.ts
40683
40749
  var DEFAULT_SKILLS = [
@@ -40966,12 +41032,53 @@ Guidance for understanding and maintaining the overall project architecture.
40966
41032
 
40967
41033
  // src/config-manager.ts
40968
41034
  var LOCUS_GITIGNORE_MARKER = "# Locus AI";
41035
+ var CLAUDE_MD_TEMPLATE = `# CLAUDE.md
41036
+
41037
+ ## Planning First
41038
+
41039
+ Every task must be planned before writing code. Create \`.locus/plans/<task-name>.md\` with: goal, approach, affected files, and acceptance criteria. Update the plan if the approach changes. Mark complete when done.
41040
+
41041
+ ## Code
41042
+
41043
+ - Follow the existing formatter, linter, and code style. Run them before finishing.
41044
+ - Keep changes minimal and atomic. Separate refactors from behavioral changes.
41045
+ - No new dependencies without explicit approval.
41046
+ - Never put raw secrets or credentials in the codebase.
41047
+
41048
+ ## Testing
41049
+
41050
+ - Every behavioral change needs a test. Bug fixes need a regression test.
41051
+ - Run the relevant test suite before marking work complete.
41052
+ - Don't modify tests just to make them pass — understand why they fail.
41053
+
41054
+ ## Communication
41055
+
41056
+ - If the plan needs to change, update it and explain why before continuing.
41057
+ `;
40969
41058
  function updateGitignore(projectPath) {
40970
- const gitignorePath = join10(projectPath, ".gitignore");
41059
+ const gitignorePath = join11(projectPath, ".gitignore");
40971
41060
  let content = "";
40972
- if (existsSync9(gitignorePath)) {
41061
+ const locusBlock = LOCUS_GITIGNORE_PATTERNS.join(`
41062
+ `);
41063
+ if (existsSync10(gitignorePath)) {
40973
41064
  content = readFileSync7(gitignorePath, "utf-8");
40974
41065
  if (content.includes(LOCUS_GITIGNORE_MARKER)) {
41066
+ const lines = content.split(`
41067
+ `);
41068
+ const startIdx = lines.findIndex((l) => l.includes(LOCUS_GITIGNORE_MARKER));
41069
+ let endIdx = startIdx;
41070
+ for (let i = startIdx;i < lines.length; i++) {
41071
+ if (lines[i].startsWith(LOCUS_GITIGNORE_MARKER) || lines[i].startsWith(".locus/") || lines[i].trim() === "") {
41072
+ endIdx = i;
41073
+ } else {
41074
+ break;
41075
+ }
41076
+ }
41077
+ const before = lines.slice(0, startIdx);
41078
+ const after = lines.slice(endIdx + 1);
41079
+ content = [...before, locusBlock, ...after].join(`
41080
+ `);
41081
+ writeFileSync5(gitignorePath, content);
40975
41082
  return;
40976
41083
  }
40977
41084
  if (content.length > 0 && !content.endsWith(`
@@ -40984,10 +41091,9 @@ function updateGitignore(projectPath) {
40984
41091
  `;
40985
41092
  }
40986
41093
  }
40987
- content += `${LOCUS_GITIGNORE_PATTERNS.join(`
40988
- `)}
41094
+ content += `${locusBlock}
40989
41095
  `;
40990
- writeFileSync4(gitignorePath, content);
41096
+ writeFileSync5(gitignorePath, content);
40991
41097
  }
40992
41098
 
40993
41099
  class ConfigManager {
@@ -40996,27 +41102,35 @@ class ConfigManager {
40996
41102
  this.projectPath = projectPath;
40997
41103
  }
40998
41104
  async init(version2) {
40999
- const locusConfigDir = join10(this.projectPath, LOCUS_CONFIG.dir);
41105
+ const locusConfigDir = join11(this.projectPath, LOCUS_CONFIG.dir);
41000
41106
  const locusConfigPath = getLocusPath(this.projectPath, "configFile");
41001
41107
  const claudeMdPath = getLocusPath(this.projectPath, "contextFile");
41002
- if (!existsSync9(claudeMdPath)) {
41003
- const template = `# Locus Project Context
41004
-
41005
- # Workflow
41006
- - Run lint and typecheck before completion
41007
- `;
41008
- writeFileSync4(claudeMdPath, template);
41108
+ if (!existsSync10(claudeMdPath)) {
41109
+ writeFileSync5(claudeMdPath, CLAUDE_MD_TEMPLATE);
41110
+ }
41111
+ if (!existsSync10(locusConfigDir)) {
41112
+ mkdirSync5(locusConfigDir, { recursive: true });
41009
41113
  }
41010
- if (!existsSync9(locusConfigDir)) {
41011
- mkdirSync4(locusConfigDir, { recursive: true });
41114
+ const locusSubdirs = [
41115
+ LOCUS_CONFIG.artifactsDir,
41116
+ LOCUS_CONFIG.documentsDir,
41117
+ LOCUS_CONFIG.sessionsDir,
41118
+ LOCUS_CONFIG.reviewsDir,
41119
+ LOCUS_CONFIG.plansDir
41120
+ ];
41121
+ for (const subdir of locusSubdirs) {
41122
+ const subdirPath = join11(locusConfigDir, subdir);
41123
+ if (!existsSync10(subdirPath)) {
41124
+ mkdirSync5(subdirPath, { recursive: true });
41125
+ }
41012
41126
  }
41013
- if (!existsSync9(locusConfigPath)) {
41127
+ if (!existsSync10(locusConfigPath)) {
41014
41128
  const config2 = {
41015
41129
  version: version2,
41016
41130
  createdAt: new Date().toISOString(),
41017
41131
  projectPath: "."
41018
41132
  };
41019
- writeFileSync4(locusConfigPath, JSON.stringify(config2, null, 2));
41133
+ writeFileSync5(locusConfigPath, JSON.stringify(config2, null, 2));
41020
41134
  }
41021
41135
  const skillLocations = [
41022
41136
  LOCUS_CONFIG.agentSkillsDir,
@@ -41026,15 +41140,15 @@ class ConfigManager {
41026
41140
  ".gemini/skills"
41027
41141
  ];
41028
41142
  for (const location of skillLocations) {
41029
- const skillsDir = join10(this.projectPath, location);
41030
- if (!existsSync9(skillsDir)) {
41031
- mkdirSync4(skillsDir, { recursive: true });
41143
+ const skillsDir = join11(this.projectPath, location);
41144
+ if (!existsSync10(skillsDir)) {
41145
+ mkdirSync5(skillsDir, { recursive: true });
41032
41146
  }
41033
41147
  for (const skill of DEFAULT_SKILLS) {
41034
- const skillPath = join10(skillsDir, skill.name);
41035
- if (!existsSync9(skillPath)) {
41036
- mkdirSync4(skillPath, { recursive: true });
41037
- writeFileSync4(join10(skillPath, "SKILL.md"), skill.content);
41148
+ const skillPath = join11(skillsDir, skill.name);
41149
+ if (!existsSync10(skillPath)) {
41150
+ mkdirSync5(skillPath, { recursive: true });
41151
+ writeFileSync5(join11(skillPath, "SKILL.md"), skill.content);
41038
41152
  }
41039
41153
  }
41040
41154
  }
@@ -41042,7 +41156,7 @@ class ConfigManager {
41042
41156
  }
41043
41157
  loadConfig() {
41044
41158
  const path3 = getLocusPath(this.projectPath, "configFile");
41045
- if (existsSync9(path3)) {
41159
+ if (existsSync10(path3)) {
41046
41160
  return JSON.parse(readFileSync7(path3, "utf-8"));
41047
41161
  }
41048
41162
  return null;
@@ -41062,7 +41176,7 @@ class ConfigManager {
41062
41176
  skillsCreated: [],
41063
41177
  gitignoreUpdated: false
41064
41178
  };
41065
- const locusConfigDir = join10(this.projectPath, LOCUS_CONFIG.dir);
41179
+ const locusConfigDir = join11(this.projectPath, LOCUS_CONFIG.dir);
41066
41180
  const claudeMdPath = getLocusPath(this.projectPath, "contextFile");
41067
41181
  const config2 = this.loadConfig();
41068
41182
  if (config2) {
@@ -41073,24 +41187,21 @@ class ConfigManager {
41073
41187
  result.versionUpdated = true;
41074
41188
  }
41075
41189
  }
41076
- if (!existsSync9(claudeMdPath)) {
41077
- const template = `# Locus Project Context
41078
-
41079
- # Workflow
41080
- - Run lint and typecheck before completion
41081
- `;
41082
- writeFileSync4(claudeMdPath, template);
41190
+ if (!existsSync10(claudeMdPath)) {
41191
+ writeFileSync5(claudeMdPath, CLAUDE_MD_TEMPLATE);
41083
41192
  result.directoriesCreated.push("CLAUDE.md");
41084
41193
  }
41085
41194
  const locusSubdirs = [
41086
41195
  LOCUS_CONFIG.artifactsDir,
41087
41196
  LOCUS_CONFIG.documentsDir,
41088
- LOCUS_CONFIG.sessionsDir
41197
+ LOCUS_CONFIG.sessionsDir,
41198
+ LOCUS_CONFIG.reviewsDir,
41199
+ LOCUS_CONFIG.plansDir
41089
41200
  ];
41090
41201
  for (const subdir of locusSubdirs) {
41091
- const subdirPath = join10(locusConfigDir, subdir);
41092
- if (!existsSync9(subdirPath)) {
41093
- mkdirSync4(subdirPath, { recursive: true });
41202
+ const subdirPath = join11(locusConfigDir, subdir);
41203
+ if (!existsSync10(subdirPath)) {
41204
+ mkdirSync5(subdirPath, { recursive: true });
41094
41205
  result.directoriesCreated.push(`.locus/${subdir}`);
41095
41206
  }
41096
41207
  }
@@ -41102,24 +41213,25 @@ class ConfigManager {
41102
41213
  ".gemini/skills"
41103
41214
  ];
41104
41215
  for (const location of skillLocations) {
41105
- const skillsDir = join10(this.projectPath, location);
41106
- if (!existsSync9(skillsDir)) {
41107
- mkdirSync4(skillsDir, { recursive: true });
41216
+ const skillsDir = join11(this.projectPath, location);
41217
+ if (!existsSync10(skillsDir)) {
41218
+ mkdirSync5(skillsDir, { recursive: true });
41108
41219
  result.directoriesCreated.push(location);
41109
41220
  }
41110
41221
  for (const skill of DEFAULT_SKILLS) {
41111
- const skillPath = join10(skillsDir, skill.name);
41112
- if (!existsSync9(skillPath)) {
41113
- mkdirSync4(skillPath, { recursive: true });
41114
- writeFileSync4(join10(skillPath, "SKILL.md"), skill.content);
41222
+ const skillPath = join11(skillsDir, skill.name);
41223
+ if (!existsSync10(skillPath)) {
41224
+ mkdirSync5(skillPath, { recursive: true });
41225
+ writeFileSync5(join11(skillPath, "SKILL.md"), skill.content);
41115
41226
  result.skillsCreated.push(`${location}/${skill.name}`);
41116
41227
  }
41117
41228
  }
41118
41229
  }
41119
- const gitignorePath = join10(this.projectPath, ".gitignore");
41120
- const hadLocusPatterns = existsSync9(gitignorePath) && readFileSync7(gitignorePath, "utf-8").includes(LOCUS_GITIGNORE_MARKER);
41230
+ const gitignorePath = join11(this.projectPath, ".gitignore");
41231
+ const gitignoreBefore = existsSync10(gitignorePath) ? readFileSync7(gitignorePath, "utf-8") : "";
41121
41232
  updateGitignore(this.projectPath);
41122
- if (!hadLocusPatterns) {
41233
+ const gitignoreAfter = readFileSync7(gitignorePath, "utf-8");
41234
+ if (gitignoreBefore !== gitignoreAfter) {
41123
41235
  result.gitignoreUpdated = true;
41124
41236
  }
41125
41237
  return result;
@@ -41136,7 +41248,7 @@ class ConfigManager {
41136
41248
  }
41137
41249
  saveConfig(config2) {
41138
41250
  const path3 = getLocusPath(this.projectPath, "configFile");
41139
- writeFileSync4(path3, JSON.stringify(config2, null, 2));
41251
+ writeFileSync5(path3, JSON.stringify(config2, null, 2));
41140
41252
  }
41141
41253
  }
41142
41254
 
@@ -41156,7 +41268,7 @@ Return ONLY a JSON object with this structure:
41156
41268
 
41157
41269
  File Tree:
41158
41270
  ${tree}`;
41159
- const output = await this.aiRunner.run(prompt, true);
41271
+ const output = await this.aiRunner.run(prompt);
41160
41272
  const jsonMatch = output.match(/\{[\s\S]*\}/);
41161
41273
  if (jsonMatch)
41162
41274
  return JSON.parse(jsonMatch[0]);
@@ -41264,9 +41376,69 @@ async function initCommand() {
41264
41376
  For more information, visit: ${c.underline("https://locusai.dev/docs")}
41265
41377
  `);
41266
41378
  }
41267
- // src/commands/run.ts
41379
+ // src/commands/review.ts
41268
41380
  init_index_node();
41381
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
41382
+ import { join as join12 } from "node:path";
41269
41383
  import { parseArgs as parseArgs3 } from "node:util";
41384
+ async function reviewCommand(args) {
41385
+ const { values } = parseArgs3({
41386
+ args,
41387
+ options: {
41388
+ model: { type: "string" },
41389
+ provider: { type: "string" },
41390
+ dir: { type: "string" }
41391
+ },
41392
+ strict: false
41393
+ });
41394
+ const projectPath = values.dir || process.cwd();
41395
+ requireInitialization(projectPath, "review");
41396
+ const provider = resolveProvider2(values.provider);
41397
+ const model = values.model || DEFAULT_MODEL[provider];
41398
+ const aiRunner = createAiRunner(provider, {
41399
+ projectPath,
41400
+ model
41401
+ });
41402
+ const reviewService = new ReviewService({
41403
+ aiRunner,
41404
+ projectPath,
41405
+ log: (msg, level) => {
41406
+ switch (level) {
41407
+ case "error":
41408
+ console.log(` ${c.error("✖")} ${msg}`);
41409
+ break;
41410
+ case "success":
41411
+ console.log(` ${c.success("✔")} ${msg}`);
41412
+ break;
41413
+ default:
41414
+ console.log(` ${c.dim(msg)}`);
41415
+ }
41416
+ }
41417
+ });
41418
+ console.log(`
41419
+ ${c.primary("\uD83D\uDD0D")} ${c.bold("Reviewing staged changes...")}
41420
+ `);
41421
+ const report = await reviewService.reviewStagedChanges(null);
41422
+ if (!report) {
41423
+ console.log(` ${c.dim("No changes to review.")}
41424
+ `);
41425
+ return;
41426
+ }
41427
+ const reviewsDir = join12(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.reviewsDir);
41428
+ if (!existsSync11(reviewsDir)) {
41429
+ mkdirSync6(reviewsDir, { recursive: true });
41430
+ }
41431
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
41432
+ const reportPath = join12(reviewsDir, `review-${timestamp}.md`);
41433
+ writeFileSync6(reportPath, report, "utf-8");
41434
+ console.log(`
41435
+ ${c.success("✔")} ${c.success("Review complete!")}`);
41436
+ console.log(` ${c.dim("Report saved to:")} ${c.primary(reportPath)}
41437
+ `);
41438
+ }
41439
+ // src/commands/run.ts
41440
+ init_index_node();
41441
+ import { parseArgs as parseArgs4 } from "node:util";
41270
41442
 
41271
41443
  // src/workspace-resolver.ts
41272
41444
  init_index_node();
@@ -41307,7 +41479,7 @@ class WorkspaceResolver {
41307
41479
 
41308
41480
  // src/commands/run.ts
41309
41481
  async function runCommand(args) {
41310
- const { values } = parseArgs3({
41482
+ const { values } = parseArgs4({
41311
41483
  args,
41312
41484
  options: {
41313
41485
  "api-key": { type: "string" },
@@ -41391,6 +41563,9 @@ async function main() {
41391
41563
  case "exec":
41392
41564
  await execCommand(args);
41393
41565
  break;
41566
+ case "review":
41567
+ await reviewCommand(args);
41568
+ break;
41394
41569
  default:
41395
41570
  showHelp2();
41396
41571
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "CLI for Locus - AI-native project management platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,7 +32,7 @@
32
32
  "author": "",
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
- "@locusai/sdk": "^0.8.0"
35
+ "@locusai/sdk": "^0.8.1"
36
36
  },
37
37
  "devDependencies": {}
38
38
  }