@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/README.md +155 -0
- package/bin/agent/worker.js +208 -56
- package/bin/locus.js +452 -148
- package/package.json +2 -2
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
|
|
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"
|
|
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:
|
|
21301
|
+
workspace: WorkspaceWithManifestInfoSchema
|
|
21144
21302
|
});
|
|
21145
21303
|
WorkspacesResponseSchema = exports_external.object({
|
|
21146
|
-
workspaces: exports_external.array(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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}
|
|
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
|
|
38041
|
+
this.log("No active sprint found.", "warn");
|
|
37873
38042
|
}
|
|
37874
|
-
while (this.tasksCompleted < this.maxTasks
|
|
38043
|
+
while (this.tasksCompleted < this.maxTasks) {
|
|
37875
38044
|
const task2 = await this.getNextTask();
|
|
37876
38045
|
if (!task2) {
|
|
37877
|
-
|
|
37878
|
-
|
|
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
|
|
38396
|
-
mkdirSync as
|
|
38560
|
+
existsSync as existsSync6,
|
|
38561
|
+
mkdirSync as mkdirSync4,
|
|
38397
38562
|
readdirSync as readdirSync2,
|
|
38398
38563
|
readFileSync as readFileSync5,
|
|
38399
38564
|
rmSync,
|
|
38400
|
-
writeFileSync as
|
|
38565
|
+
writeFileSync as writeFileSync4
|
|
38401
38566
|
} from "node:fs";
|
|
38402
|
-
import { join as
|
|
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 ??
|
|
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 (!
|
|
38419
|
-
|
|
38583
|
+
if (!existsSync6(this.historyDir)) {
|
|
38584
|
+
mkdirSync4(this.historyDir, { recursive: true });
|
|
38420
38585
|
}
|
|
38421
38586
|
}
|
|
38422
38587
|
getSessionPath(sessionId) {
|
|
38423
|
-
return
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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(
|
|
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
|
|
38835
|
-
import { dirname as dirname2, join as
|
|
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(
|
|
38937
|
-
const workerPath = potentialPaths.find((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
|
-
|
|
40003
|
-
|
|
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
|
-
|
|
40009
|
-
|
|
40010
|
-
|
|
40011
|
-
if (
|
|
40012
|
-
this.
|
|
40013
|
-
|
|
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
|
|
40156
|
-
import { dirname as dirname3, join as
|
|
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 =
|
|
40163
|
-
const sourcePath =
|
|
40164
|
-
if (
|
|
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 (
|
|
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
|
|
40194
|
-
import { join as
|
|
40386
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
40387
|
+
import { join as join10 } from "node:path";
|
|
40195
40388
|
function isProjectInitialized(projectPath) {
|
|
40196
|
-
const locusDir =
|
|
40197
|
-
const configPath =
|
|
40198
|
-
return
|
|
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
|
|
40551
|
-
import { join as
|
|
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 =
|
|
41059
|
+
const gitignorePath = join11(projectPath, ".gitignore");
|
|
40842
41060
|
let content = "";
|
|
40843
|
-
|
|
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 += `${
|
|
40859
|
-
`)}
|
|
41094
|
+
content += `${locusBlock}
|
|
40860
41095
|
`;
|
|
40861
|
-
|
|
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 =
|
|
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 (!
|
|
40874
|
-
|
|
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 (!
|
|
40882
|
-
|
|
41111
|
+
if (!existsSync10(locusConfigDir)) {
|
|
41112
|
+
mkdirSync5(locusConfigDir, { recursive: true });
|
|
40883
41113
|
}
|
|
40884
|
-
|
|
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
|
-
|
|
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 =
|
|
40901
|
-
if (!
|
|
40902
|
-
|
|
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 =
|
|
40906
|
-
if (!
|
|
40907
|
-
|
|
40908
|
-
|
|
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 (
|
|
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 =
|
|
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 (!
|
|
40948
|
-
|
|
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 =
|
|
40963
|
-
if (!
|
|
40964
|
-
|
|
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 =
|
|
40977
|
-
if (!
|
|
40978
|
-
|
|
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 =
|
|
40983
|
-
if (!
|
|
40984
|
-
|
|
40985
|
-
|
|
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 =
|
|
40991
|
-
const
|
|
41230
|
+
const gitignorePath = join11(this.projectPath, ".gitignore");
|
|
41231
|
+
const gitignoreBefore = existsSync10(gitignorePath) ? readFileSync7(gitignorePath, "utf-8") : "";
|
|
40992
41232
|
updateGitignore(this.projectPath);
|
|
40993
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
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 } =
|
|
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
|
}
|