@locusai/cli 0.7.7 → 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/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) {
@@ -20414,11 +20483,15 @@ var init_enums = __esm(() => {
20414
20483
  EventType2["SPRINT_STATUS_CHANGED"] = "SPRINT_STATUS_CHANGED";
20415
20484
  EventType2["SPRINT_DELETED"] = "SPRINT_DELETED";
20416
20485
  EventType2["CHECKLIST_INITIALIZED"] = "CHECKLIST_INITIALIZED";
20486
+ EventType2["INTERVIEW_STARTED"] = "INTERVIEW_STARTED";
20487
+ EventType2["INTERVIEW_FIELD_COMPLETED"] = "INTERVIEW_FIELD_COMPLETED";
20488
+ EventType2["INTERVIEW_COMPLETED"] = "INTERVIEW_COMPLETED";
20489
+ EventType2["INTERVIEW_ABANDONED"] = "INTERVIEW_ABANDONED";
20417
20490
  })(EventType ||= {});
20418
20491
  });
20419
20492
 
20420
20493
  // ../shared/src/models/activity.ts
20421
- var CommentSchema, ArtifactSchema, TaskCreatedPayloadSchema, TaskDeletedPayloadSchema, StatusChangedPayloadSchema, CommentAddedPayloadSchema, WorkspaceCreatedPayloadSchema, MemberAddedPayloadSchema, MemberInvitedPayloadSchema, SprintCreatedPayloadSchema, SprintStatusChangedPayloadSchema, ChecklistInitializedPayloadSchema, CiRanPayloadSchema, EventPayloadSchema, EventSchema, ArtifactParamSchema, TaskIdOnlyParamSchema, EventQuerySchema, EventResponseSchema, EventsResponseSchema, ActivityResponseSchema, CommentResponseSchema, ArtifactResponseSchema, ArtifactsResponseSchema, CreateArtifactSchema, ReportCiResultSchema;
20494
+ var CommentSchema, ArtifactSchema, TaskCreatedPayloadSchema, TaskDeletedPayloadSchema, StatusChangedPayloadSchema, CommentAddedPayloadSchema, WorkspaceCreatedPayloadSchema, MemberAddedPayloadSchema, MemberInvitedPayloadSchema, SprintCreatedPayloadSchema, SprintStatusChangedPayloadSchema, ChecklistInitializedPayloadSchema, CiRanPayloadSchema, InterviewStartedPayloadSchema, InterviewFieldCompletedPayloadSchema, InterviewCompletedPayloadSchema, InterviewAbandonedPayloadSchema, EventPayloadSchema, EventSchema, ArtifactParamSchema, TaskIdOnlyParamSchema, EventQuerySchema, EventResponseSchema, EventsResponseSchema, ActivityResponseSchema, CommentResponseSchema, ArtifactResponseSchema, ArtifactsResponseSchema, CreateArtifactSchema, ReportCiResultSchema;
20422
20495
  var init_activity = __esm(() => {
20423
20496
  init_zod();
20424
20497
  init_common();
@@ -20486,6 +20559,25 @@ var init_activity = __esm(() => {
20486
20559
  processed: exports_external.boolean(),
20487
20560
  commands: exports_external.array(exports_external.object({ cmd: exports_external.string(), exitCode: exports_external.number() }))
20488
20561
  });
20562
+ InterviewStartedPayloadSchema = exports_external.object({
20563
+ workspaceName: exports_external.string(),
20564
+ firstFieldName: exports_external.string()
20565
+ });
20566
+ InterviewFieldCompletedPayloadSchema = exports_external.object({
20567
+ fieldName: exports_external.string(),
20568
+ completionPercentage: exports_external.number()
20569
+ });
20570
+ InterviewCompletedPayloadSchema = exports_external.object({
20571
+ workspaceName: exports_external.string(),
20572
+ timeToCompleteMs: exports_external.number(),
20573
+ completedVia: exports_external.enum(["interview", "manual_bypass"])
20574
+ });
20575
+ InterviewAbandonedPayloadSchema = exports_external.object({
20576
+ workspaceName: exports_external.string(),
20577
+ lastActiveAt: exports_external.union([exports_external.date(), exports_external.string()]),
20578
+ completionPercentage: exports_external.number(),
20579
+ daysInactive: exports_external.number()
20580
+ });
20489
20581
  EventPayloadSchema = exports_external.discriminatedUnion("type", [
20490
20582
  exports_external.object({
20491
20583
  type: exports_external.literal("TASK_CREATED" /* TASK_CREATED */),
@@ -20530,6 +20622,22 @@ var init_activity = __esm(() => {
20530
20622
  exports_external.object({
20531
20623
  type: exports_external.literal("CI_RAN" /* CI_RAN */),
20532
20624
  payload: CiRanPayloadSchema
20625
+ }),
20626
+ exports_external.object({
20627
+ type: exports_external.literal("INTERVIEW_STARTED" /* INTERVIEW_STARTED */),
20628
+ payload: InterviewStartedPayloadSchema
20629
+ }),
20630
+ exports_external.object({
20631
+ type: exports_external.literal("INTERVIEW_FIELD_COMPLETED" /* INTERVIEW_FIELD_COMPLETED */),
20632
+ payload: InterviewFieldCompletedPayloadSchema
20633
+ }),
20634
+ exports_external.object({
20635
+ type: exports_external.literal("INTERVIEW_COMPLETED" /* INTERVIEW_COMPLETED */),
20636
+ payload: InterviewCompletedPayloadSchema
20637
+ }),
20638
+ exports_external.object({
20639
+ type: exports_external.literal("INTERVIEW_ABANDONED" /* INTERVIEW_ABANDONED */),
20640
+ payload: InterviewAbandonedPayloadSchema
20533
20641
  })
20534
20642
  ]);
20535
20643
  EventSchema = exports_external.object({
@@ -20609,7 +20717,7 @@ var init_ai = __esm(() => {
20609
20717
  AIRoleSchema = exports_external.enum(["user", "assistant", "system"]);
20610
20718
  AIArtifactSchema = exports_external.object({
20611
20719
  id: exports_external.string(),
20612
- type: exports_external.enum(["code", "document", "sprint", "task_list", "task"]),
20720
+ type: exports_external.enum(["code", "document"]),
20613
20721
  title: exports_external.string(),
20614
20722
  content: exports_external.string(),
20615
20723
  language: exports_external.string().optional(),
@@ -20617,13 +20725,7 @@ var init_ai = __esm(() => {
20617
20725
  });
20618
20726
  SuggestedActionSchema = exports_external.object({
20619
20727
  label: exports_external.string(),
20620
- type: exports_external.enum([
20621
- "create_task",
20622
- "create_doc",
20623
- "chat_suggestion",
20624
- "start_sprint",
20625
- "plan_sprint"
20626
- ]),
20728
+ type: exports_external.enum(["chat_suggestion", "create_doc"]),
20627
20729
  payload: exports_external.any()
20628
20730
  });
20629
20731
  AIMessageSchema = exports_external.object({
@@ -20924,6 +21026,58 @@ var init_invitation = __esm(() => {
20924
21026
  });
20925
21027
  });
20926
21028
 
21029
+ // ../shared/src/models/manifest.ts
21030
+ var ProjectPhaseSchema, SprintStatusSchema, ProjectSprintSchema, MilestoneStatusSchema, ProjectMilestoneSchema, ProjectTimelineSchema, RepositoryContextSchema, ProjectManifestSchema, PartialProjectManifestSchema;
21031
+ var init_manifest = __esm(() => {
21032
+ init_zod();
21033
+ ProjectPhaseSchema = exports_external.enum([
21034
+ "PLANNING",
21035
+ "MVP_BUILD",
21036
+ "SCALING",
21037
+ "MAINTENANCE"
21038
+ ]);
21039
+ SprintStatusSchema = exports_external.enum(["PLANNED", "ACTIVE", "COMPLETED"]);
21040
+ ProjectSprintSchema = exports_external.object({
21041
+ id: exports_external.string(),
21042
+ goal: exports_external.string(),
21043
+ tasks: exports_external.array(exports_external.string()),
21044
+ status: SprintStatusSchema
21045
+ });
21046
+ MilestoneStatusSchema = exports_external.enum(["PENDING", "COMPLETED"]);
21047
+ ProjectMilestoneSchema = exports_external.object({
21048
+ title: exports_external.string(),
21049
+ date: exports_external.string().optional(),
21050
+ status: MilestoneStatusSchema
21051
+ });
21052
+ ProjectTimelineSchema = exports_external.object({
21053
+ sprints: exports_external.array(ProjectSprintSchema),
21054
+ milestones: exports_external.array(ProjectMilestoneSchema)
21055
+ });
21056
+ RepositoryContextSchema = exports_external.object({
21057
+ summary: exports_external.string(),
21058
+ fileStructure: exports_external.string(),
21059
+ dependencies: exports_external.record(exports_external.string(), exports_external.string()),
21060
+ frameworks: exports_external.array(exports_external.string()),
21061
+ configFiles: exports_external.array(exports_external.string()),
21062
+ lastAnalysis: exports_external.string()
21063
+ });
21064
+ ProjectManifestSchema = exports_external.object({
21065
+ name: exports_external.string(),
21066
+ mission: exports_external.string(),
21067
+ targetUsers: exports_external.array(exports_external.string()),
21068
+ techStack: exports_external.array(exports_external.string()),
21069
+ phase: ProjectPhaseSchema,
21070
+ features: exports_external.array(exports_external.string()),
21071
+ competitors: exports_external.array(exports_external.string()),
21072
+ brandVoice: exports_external.string().optional(),
21073
+ successMetrics: exports_external.array(exports_external.string()).optional(),
21074
+ completenessScore: exports_external.number().min(0).max(100),
21075
+ timeline: ProjectTimelineSchema.optional(),
21076
+ repositoryState: RepositoryContextSchema.optional()
21077
+ });
21078
+ PartialProjectManifestSchema = ProjectManifestSchema.partial();
21079
+ });
21080
+
20927
21081
  // ../shared/src/models/organization.ts
20928
21082
  var OrganizationSchema, CreateOrganizationSchema, UpdateOrganizationSchema, AddMemberSchema, MembershipWithUserSchema, OrgIdParamSchema, MembershipIdParamSchema, OrganizationResponseSchema, OrganizationsResponseSchema, MembersResponseSchema, MembershipResponseSchema;
20929
21083
  var init_organization = __esm(() => {
@@ -21105,7 +21259,7 @@ var init_task = __esm(() => {
21105
21259
  });
21106
21260
 
21107
21261
  // ../shared/src/models/workspace.ts
21108
- var ChecklistItemSchema, WorkspaceSchema, CreateWorkspaceSchema, UpdateWorkspaceSchema, AddWorkspaceMemberSchema, WorkspaceIdParamSchema, WorkspaceAndUserParamSchema, WorkspaceResponseSchema, WorkspacesResponseSchema, WorkspaceStatsSchema, WorkspaceStatsResponseSchema;
21262
+ var ChecklistItemSchema, WorkspaceSchema, CreateWorkspaceSchema, UpdateWorkspaceSchema, AddWorkspaceMemberSchema, WorkspaceIdParamSchema, WorkspaceAndUserParamSchema, WorkspaceWithManifestInfoSchema, WorkspaceResponseSchema, WorkspacesResponseSchema, WorkspaceStatsSchema, WorkspaceStatsResponseSchema, ManifestStatusResponseSchema;
21109
21263
  var init_workspace = __esm(() => {
21110
21264
  init_zod();
21111
21265
  init_common();
@@ -21139,11 +21293,15 @@ var init_workspace = __esm(() => {
21139
21293
  workspaceId: exports_external.string().uuid("Invalid Workspace ID"),
21140
21294
  userId: exports_external.string().uuid("Invalid User ID")
21141
21295
  });
21296
+ WorkspaceWithManifestInfoSchema = WorkspaceSchema.extend({
21297
+ isManifestComplete: exports_external.boolean(),
21298
+ manifestCompletionPercentage: exports_external.number().min(0).max(100)
21299
+ });
21142
21300
  WorkspaceResponseSchema = exports_external.object({
21143
- workspace: WorkspaceSchema
21301
+ workspace: WorkspaceWithManifestInfoSchema
21144
21302
  });
21145
21303
  WorkspacesResponseSchema = exports_external.object({
21146
- workspaces: exports_external.array(WorkspaceSchema)
21304
+ workspaces: exports_external.array(WorkspaceWithManifestInfoSchema)
21147
21305
  });
21148
21306
  WorkspaceStatsSchema = exports_external.object({
21149
21307
  workspaceName: exports_external.string(),
@@ -21153,6 +21311,13 @@ var init_workspace = __esm(() => {
21153
21311
  WorkspaceStatsResponseSchema = exports_external.object({
21154
21312
  stats: WorkspaceStatsSchema
21155
21313
  });
21314
+ ManifestStatusResponseSchema = exports_external.object({
21315
+ isComplete: exports_external.boolean(),
21316
+ percentage: exports_external.number().min(0).max(100),
21317
+ missingFields: exports_external.array(exports_external.string()),
21318
+ filledFields: exports_external.array(exports_external.string()),
21319
+ completenessScore: exports_external.number().min(0).max(100).nullable()
21320
+ });
21156
21321
  });
21157
21322
 
21158
21323
  // ../shared/src/models/index.ts
@@ -21165,6 +21330,7 @@ var init_models = __esm(() => {
21165
21330
  init_doc();
21166
21331
  init_doc_group();
21167
21332
  init_invitation();
21333
+ init_manifest();
21168
21334
  init_organization();
21169
21335
  init_sprint();
21170
21336
  init_task();
@@ -21548,29 +21714,8 @@ class TaskExecutor {
21548
21714
  taskContext: context
21549
21715
  });
21550
21716
  try {
21551
- let plan = null;
21552
- this.deps.log("Phase 1: Planning (CLI)...", "info");
21553
- const planningPrompt = `${basePrompt}
21554
-
21555
- ## Phase 1: Planning
21556
- Analyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`;
21557
- plan = await this.deps.aiRunner.run(planningPrompt, true);
21558
21717
  this.deps.log("Starting Execution...", "info");
21559
- let executionPrompt = basePrompt;
21560
- if (plan != null) {
21561
- executionPrompt += `
21562
-
21563
- ## Phase 2: Execution
21564
- Based on the plan, execute the task:
21565
-
21566
- ${plan}`;
21567
- } else {
21568
- executionPrompt += `
21569
-
21570
- ## Execution
21571
- Execute the task directly.`;
21572
- }
21573
- executionPrompt += `
21718
+ const executionPrompt = `${basePrompt}
21574
21719
 
21575
21720
  When finished, output: <promise>COMPLETE</promise>`;
21576
21721
  const output = await this.deps.aiRunner.run(executionPrompt);
@@ -21671,7 +21816,7 @@ class ClaudeRunner {
21671
21816
  setEventEmitter(emitter) {
21672
21817
  this.eventEmitter = emitter;
21673
21818
  }
21674
- async run(prompt, _isPlanning = false) {
21819
+ async run(prompt) {
21675
21820
  const maxRetries = 3;
21676
21821
  let lastError = null;
21677
21822
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
@@ -37612,6 +37757,10 @@ var init_workspaces = __esm(() => {
37612
37757
  const { data } = await this.api.get(`/workspaces/${id}/stats`);
37613
37758
  return data;
37614
37759
  }
37760
+ async getManifestStatus(workspaceId) {
37761
+ const { data } = await this.api.get(`/workspaces/${workspaceId}/manifest-status`);
37762
+ return data;
37763
+ }
37615
37764
  async getActivity(id, limit) {
37616
37765
  const { data } = await this.api.get(`/workspaces/${id}/activity`, {
37617
37766
  params: { limit }
@@ -37753,6 +37902,8 @@ var init_src2 = __esm(() => {
37753
37902
  });
37754
37903
 
37755
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";
37756
37907
  function resolveProvider(value) {
37757
37908
  if (!value || value.startsWith("--")) {
37758
37909
  console.warn("Warning: --provider requires a value. Falling back to 'claude'.");
@@ -37771,11 +37922,9 @@ class AgentWorker {
37771
37922
  indexerService;
37772
37923
  documentFetcher;
37773
37924
  taskExecutor;
37774
- consecutiveEmpty = 0;
37775
- maxEmpty = 60;
37925
+ reviewService;
37776
37926
  maxTasks = 50;
37777
37927
  tasksCompleted = 0;
37778
- pollInterval = 1e4;
37779
37928
  constructor(config2) {
37780
37929
  this.config = config2;
37781
37930
  const projectPath = config2.projectPath || process.cwd();
@@ -37812,6 +37961,11 @@ class AgentWorker {
37812
37961
  projectPath,
37813
37962
  log
37814
37963
  });
37964
+ this.reviewService = new ReviewService({
37965
+ aiRunner: this.aiRunner,
37966
+ projectPath,
37967
+ log
37968
+ });
37815
37969
  const providerLabel = provider === "codex" ? "Codex" : "Claude";
37816
37970
  this.log(`Using ${providerLabel} CLI for all phases`, "info");
37817
37971
  }
@@ -37857,33 +38011,42 @@ class AgentWorker {
37857
38011
  await this.indexerService.reindex();
37858
38012
  return result;
37859
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
+ }
37860
38035
  async run() {
37861
38036
  this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
37862
38037
  const sprint2 = await this.getActiveSprint();
37863
38038
  if (sprint2) {
37864
- this.log(`Active sprint found: ${sprint2.name}. Ensuring plan is up to date...`, "info");
37865
- try {
37866
- await this.client.sprints.triggerAIPlanning(sprint2.id, this.config.workspaceId);
37867
- this.log(`Sprint plan sync checked on server.`, "success");
37868
- } catch (err) {
37869
- this.log(`Sprint planning sync failed (non-critical): ${err instanceof Error ? err.message : String(err)}`, "warn");
37870
- }
38039
+ this.log(`Active sprint found: ${sprint2.name}`, "info");
37871
38040
  } else {
37872
- this.log("No active sprint found for planning.", "warn");
38041
+ this.log("No active sprint found.", "warn");
37873
38042
  }
37874
- while (this.tasksCompleted < this.maxTasks && this.consecutiveEmpty < this.maxEmpty) {
38043
+ while (this.tasksCompleted < this.maxTasks) {
37875
38044
  const task2 = await this.getNextTask();
37876
38045
  if (!task2) {
37877
- if (this.consecutiveEmpty === 0) {
37878
- this.log("Queue empty, waiting for tasks...", "info");
37879
- }
37880
- this.consecutiveEmpty++;
37881
- if (this.consecutiveEmpty >= this.maxEmpty)
37882
- break;
37883
- await new Promise((r) => setTimeout(r, this.pollInterval));
37884
- continue;
38046
+ this.log("No tasks remaining. Running review on staged changes...", "info");
38047
+ await this.runStagedChangesReview(sprint2);
38048
+ break;
37885
38049
  }
37886
- this.consecutiveEmpty = 0;
37887
38050
  this.log(`Claimed: ${task2.title}`, "success");
37888
38051
  const result = await this.executeTask(task2);
37889
38052
  try {
@@ -37923,6 +38086,7 @@ var init_worker = __esm(() => {
37923
38086
  init_colors();
37924
38087
  init_codebase_indexer_service();
37925
38088
  init_document_fetcher();
38089
+ init_review_service();
37926
38090
  init_task_executor();
37927
38091
  if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("worker")) {
37928
38092
  process.title = "locus-worker";
@@ -37967,6 +38131,7 @@ var init_worker = __esm(() => {
37967
38131
  var init_agent2 = __esm(() => {
37968
38132
  init_codebase_indexer_service();
37969
38133
  init_document_fetcher();
38134
+ init_review_service();
37970
38135
  init_task_executor();
37971
38136
  init_worker();
37972
38137
  });
@@ -38392,14 +38557,14 @@ var init_event_emitter = __esm(() => {
38392
38557
 
38393
38558
  // ../sdk/src/exec/history-manager.ts
38394
38559
  import {
38395
- existsSync as existsSync5,
38396
- mkdirSync as mkdirSync3,
38560
+ existsSync as existsSync6,
38561
+ mkdirSync as mkdirSync4,
38397
38562
  readdirSync as readdirSync2,
38398
38563
  readFileSync as readFileSync5,
38399
38564
  rmSync,
38400
- writeFileSync as writeFileSync3
38565
+ writeFileSync as writeFileSync4
38401
38566
  } from "node:fs";
38402
- import { join as join6 } from "node:path";
38567
+ import { join as join7 } from "node:path";
38403
38568
  function generateSessionId2() {
38404
38569
  const timestamp = Date.now().toString(36);
38405
38570
  const random = Math.random().toString(36).substring(2, 9);
@@ -38410,26 +38575,26 @@ class HistoryManager {
38410
38575
  historyDir;
38411
38576
  maxSessions;
38412
38577
  constructor(projectPath, options) {
38413
- 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);
38414
38579
  this.maxSessions = options?.maxSessions ?? DEFAULT_MAX_SESSIONS;
38415
38580
  this.ensureHistoryDir();
38416
38581
  }
38417
38582
  ensureHistoryDir() {
38418
- if (!existsSync5(this.historyDir)) {
38419
- mkdirSync3(this.historyDir, { recursive: true });
38583
+ if (!existsSync6(this.historyDir)) {
38584
+ mkdirSync4(this.historyDir, { recursive: true });
38420
38585
  }
38421
38586
  }
38422
38587
  getSessionPath(sessionId) {
38423
- return join6(this.historyDir, `${sessionId}.json`);
38588
+ return join7(this.historyDir, `${sessionId}.json`);
38424
38589
  }
38425
38590
  saveSession(session) {
38426
38591
  const filePath = this.getSessionPath(session.id);
38427
38592
  session.updatedAt = Date.now();
38428
- writeFileSync3(filePath, JSON.stringify(session, null, 2), "utf-8");
38593
+ writeFileSync4(filePath, JSON.stringify(session, null, 2), "utf-8");
38429
38594
  }
38430
38595
  loadSession(sessionId) {
38431
38596
  const filePath = this.getSessionPath(sessionId);
38432
- if (!existsSync5(filePath)) {
38597
+ if (!existsSync6(filePath)) {
38433
38598
  return null;
38434
38599
  }
38435
38600
  try {
@@ -38441,7 +38606,7 @@ class HistoryManager {
38441
38606
  }
38442
38607
  deleteSession(sessionId) {
38443
38608
  const filePath = this.getSessionPath(sessionId);
38444
- if (!existsSync5(filePath)) {
38609
+ if (!existsSync6(filePath)) {
38445
38610
  return false;
38446
38611
  }
38447
38612
  try {
@@ -38529,7 +38694,7 @@ class HistoryManager {
38529
38694
  return files.filter((f) => f.endsWith(".json")).length;
38530
38695
  }
38531
38696
  sessionExists(sessionId) {
38532
- return existsSync5(this.getSessionPath(sessionId));
38697
+ return existsSync6(this.getSessionPath(sessionId));
38533
38698
  }
38534
38699
  findSessionByPartialId(partialId) {
38535
38700
  const sessions = this.listSessions();
@@ -38548,7 +38713,7 @@ class HistoryManager {
38548
38713
  for (const file2 of files) {
38549
38714
  if (file2.endsWith(".json")) {
38550
38715
  try {
38551
- rmSync(join6(this.historyDir, file2));
38716
+ rmSync(join7(this.historyDir, file2));
38552
38717
  deleted++;
38553
38718
  } catch {}
38554
38719
  }
@@ -38831,8 +38996,8 @@ var init_exec = __esm(() => {
38831
38996
 
38832
38997
  // ../sdk/src/orchestrator.ts
38833
38998
  import { spawn as spawn3 } from "node:child_process";
38834
- import { existsSync as existsSync6 } from "node:fs";
38835
- 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";
38836
39001
  import { fileURLToPath as fileURLToPath2 } from "node:url";
38837
39002
  import { EventEmitter as EventEmitter4 } from "events";
38838
39003
  var AgentOrchestrator;
@@ -38933,8 +39098,8 @@ ${c.success("✅ Orchestrator finished")}`);
38933
39098
  const potentialPaths = [];
38934
39099
  const currentModulePath = fileURLToPath2(import.meta.url);
38935
39100
  const currentModuleDir = dirname2(currentModulePath);
38936
- potentialPaths.push(join7(currentModuleDir, "agent", "worker.js"), join7(currentModuleDir, "worker.js"), join7(currentModuleDir, "agent", "worker.ts"));
38937
- 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));
38938
39103
  if (!workerPath) {
38939
39104
  throw new Error(`Worker file not found. Checked: ${potentialPaths.join(", ")}. ` + `Make sure the SDK is properly built and installed.`);
38940
39105
  }
@@ -39949,6 +40114,9 @@ class InteractiveSession {
39949
40114
  projectPath;
39950
40115
  model;
39951
40116
  provider;
40117
+ inputBuffer = [];
40118
+ inputDebounceTimer = null;
40119
+ static PASTE_DEBOUNCE_MS = 50;
39952
40120
  constructor(options) {
39953
40121
  this.aiRunner = createAiRunner(options.provider, {
39954
40122
  projectPath: options.projectPath,
@@ -39988,6 +40156,11 @@ class InteractiveSession {
39988
40156
  this.readline.on("line", (input) => this.handleLine(input));
39989
40157
  this.readline.on("close", () => this.shutdown());
39990
40158
  process.on("SIGINT", () => {
40159
+ if (this.inputDebounceTimer) {
40160
+ clearTimeout(this.inputDebounceTimer);
40161
+ this.inputDebounceTimer = null;
40162
+ }
40163
+ this.inputBuffer = [];
39991
40164
  if (this.isProcessing) {
39992
40165
  this.renderer.stopThinkingAnimation();
39993
40166
  console.log(c.dim(`
@@ -39999,18 +40172,34 @@ class InteractiveSession {
39999
40172
  }
40000
40173
  });
40001
40174
  }
40002
- async handleLine(input) {
40003
- const trimmed = input.trim();
40175
+ handleLine(input) {
40176
+ this.inputBuffer.push(input);
40177
+ if (this.inputDebounceTimer) {
40178
+ clearTimeout(this.inputDebounceTimer);
40179
+ }
40180
+ this.inputDebounceTimer = setTimeout(() => {
40181
+ this.processBufferedInput();
40182
+ }, InteractiveSession.PASTE_DEBOUNCE_MS);
40183
+ }
40184
+ async processBufferedInput() {
40185
+ const fullInput = this.inputBuffer.join(`
40186
+ `);
40187
+ this.inputBuffer = [];
40188
+ this.inputDebounceTimer = null;
40189
+ const trimmed = fullInput.trim();
40004
40190
  if (trimmed === "") {
40005
40191
  this.readline?.prompt();
40006
40192
  return;
40007
40193
  }
40008
- const command = parseCommand(trimmed);
40009
- if (command) {
40010
- await command.execute(this, trimmed.slice(command.name.length).trim());
40011
- if (this.readline)
40012
- this.readline.prompt();
40013
- return;
40194
+ if (!trimmed.includes(`
40195
+ `)) {
40196
+ const command = parseCommand(trimmed);
40197
+ if (command) {
40198
+ await command.execute(this, trimmed.slice(command.name.length).trim());
40199
+ if (this.readline)
40200
+ this.readline.prompt();
40201
+ return;
40202
+ }
40014
40203
  }
40015
40204
  await this.executePrompt(trimmed);
40016
40205
  this.readline?.prompt();
@@ -40117,6 +40306,10 @@ ${userInput}`;
40117
40306
  this.historyManager.pruneSessions();
40118
40307
  }
40119
40308
  shutdown() {
40309
+ if (this.inputDebounceTimer) {
40310
+ clearTimeout(this.inputDebounceTimer);
40311
+ this.inputDebounceTimer = null;
40312
+ }
40120
40313
  this.renderer.stopThinkingAnimation();
40121
40314
  console.log(c.dim(`
40122
40315
  Goodbye!`));
@@ -40152,22 +40345,22 @@ import { parseArgs } from "node:util";
40152
40345
  init_index_node();
40153
40346
 
40154
40347
  // src/utils/version.ts
40155
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
40156
- 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";
40157
40350
  import { fileURLToPath as fileURLToPath3 } from "node:url";
40158
40351
  function getVersion() {
40159
40352
  try {
40160
40353
  const __filename2 = fileURLToPath3(import.meta.url);
40161
40354
  const __dirname2 = dirname3(__filename2);
40162
- const bundledPath = join8(__dirname2, "..", "package.json");
40163
- const sourcePath = join8(__dirname2, "..", "..", "package.json");
40164
- if (existsSync7(bundledPath)) {
40355
+ const bundledPath = join9(__dirname2, "..", "package.json");
40356
+ const sourcePath = join9(__dirname2, "..", "..", "package.json");
40357
+ if (existsSync8(bundledPath)) {
40165
40358
  const pkg = JSON.parse(readFileSync6(bundledPath, "utf-8"));
40166
40359
  if (pkg.name === "@locusai/cli") {
40167
40360
  return pkg.version || "0.0.0";
40168
40361
  }
40169
40362
  }
40170
- if (existsSync7(sourcePath)) {
40363
+ if (existsSync8(sourcePath)) {
40171
40364
  const pkg = JSON.parse(readFileSync6(sourcePath, "utf-8"));
40172
40365
  if (pkg.name === "@locusai/cli") {
40173
40366
  return pkg.version || "0.0.0";
@@ -40190,12 +40383,12 @@ function printBanner() {
40190
40383
  }
40191
40384
  // src/utils/helpers.ts
40192
40385
  init_index_node();
40193
- import { existsSync as existsSync8 } from "node:fs";
40194
- import { join as join9 } from "node:path";
40386
+ import { existsSync as existsSync9 } from "node:fs";
40387
+ import { join as join10 } from "node:path";
40195
40388
  function isProjectInitialized(projectPath) {
40196
- const locusDir = join9(projectPath, LOCUS_CONFIG.dir);
40197
- const configPath = join9(locusDir, LOCUS_CONFIG.configFile);
40198
- 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);
40199
40392
  }
40200
40393
  function requireInitialization(projectPath, command) {
40201
40394
  if (!isProjectInitialized(projectPath)) {
@@ -40520,6 +40713,7 @@ function showHelp2() {
40520
40713
  ${c.success("init")} Initialize Locus in the current directory
40521
40714
  ${c.success("index")} Index the codebase for AI context
40522
40715
  ${c.success("run")} Start an agent to work on tasks
40716
+ ${c.success("review")} Review staged changes with AI
40523
40717
  ${c.success("exec")} Run a prompt with repository context
40524
40718
  ${c.dim("--interactive, -i Start interactive REPL mode")}
40525
40719
  ${c.dim("--session, -s <id> Resume a previous session")}
@@ -40536,6 +40730,7 @@ function showHelp2() {
40536
40730
  ${c.dim("$")} ${c.primary("locus init")}
40537
40731
  ${c.dim("$")} ${c.primary("locus index")}
40538
40732
  ${c.dim("$")} ${c.primary("locus run --api-key YOUR_KEY")}
40733
+ ${c.dim("$")} ${c.primary("locus review")}
40539
40734
  ${c.dim("$")} ${c.primary("locus exec sessions list")}
40540
40735
 
40541
40736
  For more information, visit: ${c.underline("https://locusai.dev/docs")}
@@ -40547,8 +40742,8 @@ import { parseArgs as parseArgs2 } from "node:util";
40547
40742
 
40548
40743
  // src/config-manager.ts
40549
40744
  init_index_node();
40550
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
40551
- 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";
40552
40747
 
40553
40748
  // src/templates/skills.ts
40554
40749
  var DEFAULT_SKILLS = [
@@ -40837,12 +41032,53 @@ Guidance for understanding and maintaining the overall project architecture.
40837
41032
 
40838
41033
  // src/config-manager.ts
40839
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
+ `;
40840
41058
  function updateGitignore(projectPath) {
40841
- const gitignorePath = join10(projectPath, ".gitignore");
41059
+ const gitignorePath = join11(projectPath, ".gitignore");
40842
41060
  let content = "";
40843
- if (existsSync9(gitignorePath)) {
41061
+ const locusBlock = LOCUS_GITIGNORE_PATTERNS.join(`
41062
+ `);
41063
+ if (existsSync10(gitignorePath)) {
40844
41064
  content = readFileSync7(gitignorePath, "utf-8");
40845
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);
40846
41082
  return;
40847
41083
  }
40848
41084
  if (content.length > 0 && !content.endsWith(`
@@ -40855,10 +41091,9 @@ function updateGitignore(projectPath) {
40855
41091
  `;
40856
41092
  }
40857
41093
  }
40858
- content += `${LOCUS_GITIGNORE_PATTERNS.join(`
40859
- `)}
41094
+ content += `${locusBlock}
40860
41095
  `;
40861
- writeFileSync4(gitignorePath, content);
41096
+ writeFileSync5(gitignorePath, content);
40862
41097
  }
40863
41098
 
40864
41099
  class ConfigManager {
@@ -40867,27 +41102,35 @@ class ConfigManager {
40867
41102
  this.projectPath = projectPath;
40868
41103
  }
40869
41104
  async init(version2) {
40870
- const locusConfigDir = join10(this.projectPath, LOCUS_CONFIG.dir);
41105
+ const locusConfigDir = join11(this.projectPath, LOCUS_CONFIG.dir);
40871
41106
  const locusConfigPath = getLocusPath(this.projectPath, "configFile");
40872
41107
  const claudeMdPath = getLocusPath(this.projectPath, "contextFile");
40873
- if (!existsSync9(claudeMdPath)) {
40874
- const template = `# Locus Project Context
40875
-
40876
- # Workflow
40877
- - Run lint and typecheck before completion
40878
- `;
40879
- writeFileSync4(claudeMdPath, template);
41108
+ if (!existsSync10(claudeMdPath)) {
41109
+ writeFileSync5(claudeMdPath, CLAUDE_MD_TEMPLATE);
40880
41110
  }
40881
- if (!existsSync9(locusConfigDir)) {
40882
- mkdirSync4(locusConfigDir, { recursive: true });
41111
+ if (!existsSync10(locusConfigDir)) {
41112
+ mkdirSync5(locusConfigDir, { recursive: true });
40883
41113
  }
40884
- if (!existsSync9(locusConfigPath)) {
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
+ }
41126
+ }
41127
+ if (!existsSync10(locusConfigPath)) {
40885
41128
  const config2 = {
40886
41129
  version: version2,
40887
41130
  createdAt: new Date().toISOString(),
40888
41131
  projectPath: "."
40889
41132
  };
40890
- writeFileSync4(locusConfigPath, JSON.stringify(config2, null, 2));
41133
+ writeFileSync5(locusConfigPath, JSON.stringify(config2, null, 2));
40891
41134
  }
40892
41135
  const skillLocations = [
40893
41136
  LOCUS_CONFIG.agentSkillsDir,
@@ -40897,15 +41140,15 @@ class ConfigManager {
40897
41140
  ".gemini/skills"
40898
41141
  ];
40899
41142
  for (const location of skillLocations) {
40900
- const skillsDir = join10(this.projectPath, location);
40901
- if (!existsSync9(skillsDir)) {
40902
- mkdirSync4(skillsDir, { recursive: true });
41143
+ const skillsDir = join11(this.projectPath, location);
41144
+ if (!existsSync10(skillsDir)) {
41145
+ mkdirSync5(skillsDir, { recursive: true });
40903
41146
  }
40904
41147
  for (const skill of DEFAULT_SKILLS) {
40905
- const skillPath = join10(skillsDir, skill.name);
40906
- if (!existsSync9(skillPath)) {
40907
- mkdirSync4(skillPath, { recursive: true });
40908
- 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);
40909
41152
  }
40910
41153
  }
40911
41154
  }
@@ -40913,7 +41156,7 @@ class ConfigManager {
40913
41156
  }
40914
41157
  loadConfig() {
40915
41158
  const path3 = getLocusPath(this.projectPath, "configFile");
40916
- if (existsSync9(path3)) {
41159
+ if (existsSync10(path3)) {
40917
41160
  return JSON.parse(readFileSync7(path3, "utf-8"));
40918
41161
  }
40919
41162
  return null;
@@ -40933,7 +41176,7 @@ class ConfigManager {
40933
41176
  skillsCreated: [],
40934
41177
  gitignoreUpdated: false
40935
41178
  };
40936
- const locusConfigDir = join10(this.projectPath, LOCUS_CONFIG.dir);
41179
+ const locusConfigDir = join11(this.projectPath, LOCUS_CONFIG.dir);
40937
41180
  const claudeMdPath = getLocusPath(this.projectPath, "contextFile");
40938
41181
  const config2 = this.loadConfig();
40939
41182
  if (config2) {
@@ -40944,24 +41187,21 @@ class ConfigManager {
40944
41187
  result.versionUpdated = true;
40945
41188
  }
40946
41189
  }
40947
- if (!existsSync9(claudeMdPath)) {
40948
- const template = `# Locus Project Context
40949
-
40950
- # Workflow
40951
- - Run lint and typecheck before completion
40952
- `;
40953
- writeFileSync4(claudeMdPath, template);
41190
+ if (!existsSync10(claudeMdPath)) {
41191
+ writeFileSync5(claudeMdPath, CLAUDE_MD_TEMPLATE);
40954
41192
  result.directoriesCreated.push("CLAUDE.md");
40955
41193
  }
40956
41194
  const locusSubdirs = [
40957
41195
  LOCUS_CONFIG.artifactsDir,
40958
41196
  LOCUS_CONFIG.documentsDir,
40959
- LOCUS_CONFIG.sessionsDir
41197
+ LOCUS_CONFIG.sessionsDir,
41198
+ LOCUS_CONFIG.reviewsDir,
41199
+ LOCUS_CONFIG.plansDir
40960
41200
  ];
40961
41201
  for (const subdir of locusSubdirs) {
40962
- const subdirPath = join10(locusConfigDir, subdir);
40963
- if (!existsSync9(subdirPath)) {
40964
- mkdirSync4(subdirPath, { recursive: true });
41202
+ const subdirPath = join11(locusConfigDir, subdir);
41203
+ if (!existsSync10(subdirPath)) {
41204
+ mkdirSync5(subdirPath, { recursive: true });
40965
41205
  result.directoriesCreated.push(`.locus/${subdir}`);
40966
41206
  }
40967
41207
  }
@@ -40973,24 +41213,25 @@ class ConfigManager {
40973
41213
  ".gemini/skills"
40974
41214
  ];
40975
41215
  for (const location of skillLocations) {
40976
- const skillsDir = join10(this.projectPath, location);
40977
- if (!existsSync9(skillsDir)) {
40978
- mkdirSync4(skillsDir, { recursive: true });
41216
+ const skillsDir = join11(this.projectPath, location);
41217
+ if (!existsSync10(skillsDir)) {
41218
+ mkdirSync5(skillsDir, { recursive: true });
40979
41219
  result.directoriesCreated.push(location);
40980
41220
  }
40981
41221
  for (const skill of DEFAULT_SKILLS) {
40982
- const skillPath = join10(skillsDir, skill.name);
40983
- if (!existsSync9(skillPath)) {
40984
- mkdirSync4(skillPath, { recursive: true });
40985
- 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);
40986
41226
  result.skillsCreated.push(`${location}/${skill.name}`);
40987
41227
  }
40988
41228
  }
40989
41229
  }
40990
- const gitignorePath = join10(this.projectPath, ".gitignore");
40991
- 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") : "";
40992
41232
  updateGitignore(this.projectPath);
40993
- if (!hadLocusPatterns) {
41233
+ const gitignoreAfter = readFileSync7(gitignorePath, "utf-8");
41234
+ if (gitignoreBefore !== gitignoreAfter) {
40994
41235
  result.gitignoreUpdated = true;
40995
41236
  }
40996
41237
  return result;
@@ -41007,7 +41248,7 @@ class ConfigManager {
41007
41248
  }
41008
41249
  saveConfig(config2) {
41009
41250
  const path3 = getLocusPath(this.projectPath, "configFile");
41010
- writeFileSync4(path3, JSON.stringify(config2, null, 2));
41251
+ writeFileSync5(path3, JSON.stringify(config2, null, 2));
41011
41252
  }
41012
41253
  }
41013
41254
 
@@ -41027,7 +41268,7 @@ Return ONLY a JSON object with this structure:
41027
41268
 
41028
41269
  File Tree:
41029
41270
  ${tree}`;
41030
- const output = await this.aiRunner.run(prompt, true);
41271
+ const output = await this.aiRunner.run(prompt);
41031
41272
  const jsonMatch = output.match(/\{[\s\S]*\}/);
41032
41273
  if (jsonMatch)
41033
41274
  return JSON.parse(jsonMatch[0]);
@@ -41135,9 +41376,69 @@ async function initCommand() {
41135
41376
  For more information, visit: ${c.underline("https://locusai.dev/docs")}
41136
41377
  `);
41137
41378
  }
41138
- // src/commands/run.ts
41379
+ // src/commands/review.ts
41139
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";
41140
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";
41141
41442
 
41142
41443
  // src/workspace-resolver.ts
41143
41444
  init_index_node();
@@ -41178,7 +41479,7 @@ class WorkspaceResolver {
41178
41479
 
41179
41480
  // src/commands/run.ts
41180
41481
  async function runCommand(args) {
41181
- const { values } = parseArgs3({
41482
+ const { values } = parseArgs4({
41182
41483
  args,
41183
41484
  options: {
41184
41485
  "api-key": { type: "string" },
@@ -41262,6 +41563,9 @@ async function main() {
41262
41563
  case "exec":
41263
41564
  await execCommand(args);
41264
41565
  break;
41566
+ case "review":
41567
+ await reviewCommand(args);
41568
+ break;
41265
41569
  default:
41266
41570
  showHelp2();
41267
41571
  }