@locusai/sdk 0.8.1 → 0.9.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/dist/agent/__tests__/orchestrator.cleanup.test.d.ts +2 -0
- package/dist/agent/__tests__/orchestrator.cleanup.test.d.ts.map +1 -0
- package/dist/agent/__tests__/worker.no-changes.test.d.ts +2 -0
- package/dist/agent/__tests__/worker.no-changes.test.d.ts.map +1 -0
- package/dist/agent/document-fetcher.d.ts.map +1 -1
- package/dist/agent/index.d.ts +1 -0
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/review-service.d.ts.map +1 -1
- package/dist/agent/reviewer-worker.d.ts +42 -0
- package/dist/agent/reviewer-worker.d.ts.map +1 -0
- package/dist/agent/task-executor.d.ts +1 -1
- package/dist/agent/task-executor.d.ts.map +1 -1
- package/dist/agent/worker.d.ts +47 -4
- package/dist/agent/worker.d.ts.map +1 -1
- package/dist/agent/worker.js +1102 -506
- package/dist/ai/claude-runner.d.ts +5 -0
- package/dist/ai/claude-runner.d.ts.map +1 -1
- package/dist/ai/codex-runner.d.ts +5 -0
- package/dist/ai/codex-runner.d.ts.map +1 -1
- package/dist/ai/runner.d.ts +5 -0
- package/dist/ai/runner.d.ts.map +1 -1
- package/dist/core/config.d.ts +10 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/prompt-builder.d.ts +3 -6
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/git/git-utils.d.ts +31 -0
- package/dist/git/git-utils.d.ts.map +1 -0
- package/dist/git/index.d.ts +3 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/pr-service.d.ts +66 -0
- package/dist/git/pr-service.d.ts.map +1 -0
- package/dist/index-node.d.ts +5 -1
- package/dist/index-node.d.ts.map +1 -1
- package/dist/index-node.js +2560 -729
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -49
- package/dist/modules/auth.d.ts +3 -0
- package/dist/modules/auth.d.ts.map +1 -1
- package/dist/modules/tasks.d.ts +0 -5
- package/dist/modules/tasks.d.ts.map +1 -1
- package/dist/modules/workspaces.d.ts +10 -10
- package/dist/modules/workspaces.d.ts.map +1 -1
- package/dist/orchestrator.d.ts +38 -5
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/planning/agents/architect.d.ts +15 -0
- package/dist/planning/agents/architect.d.ts.map +1 -0
- package/dist/planning/agents/sprint-organizer.d.ts +14 -0
- package/dist/planning/agents/sprint-organizer.d.ts.map +1 -0
- package/dist/planning/agents/tech-lead.d.ts +15 -0
- package/dist/planning/agents/tech-lead.d.ts.map +1 -0
- package/dist/planning/index.d.ts +4 -0
- package/dist/planning/index.d.ts.map +1 -0
- package/dist/planning/plan-manager.d.ts +52 -0
- package/dist/planning/plan-manager.d.ts.map +1 -0
- package/dist/planning/planning-meeting.d.ts +36 -0
- package/dist/planning/planning-meeting.d.ts.map +1 -0
- package/dist/planning/sprint-plan.d.ts +47 -0
- package/dist/planning/sprint-plan.d.ts.map +1 -0
- package/dist/project/knowledge-base.d.ts +25 -0
- package/dist/project/knowledge-base.d.ts.map +1 -0
- package/dist/worktree/index.d.ts +3 -0
- package/dist/worktree/index.d.ts.map +1 -0
- package/dist/worktree/worktree-config.d.ts +58 -0
- package/dist/worktree/worktree-config.d.ts.map +1 -0
- package/dist/worktree/worktree-manager.d.ts +96 -0
- package/dist/worktree/worktree-manager.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/modules/ai.d.ts +0 -55
- package/dist/modules/ai.d.ts.map +0 -1
package/dist/index-node.js
CHANGED
|
@@ -52,8 +52,7 @@ __export(exports_src, {
|
|
|
52
52
|
InvitationsModule: () => InvitationsModule,
|
|
53
53
|
DocsModule: () => DocsModule,
|
|
54
54
|
CiModule: () => CiModule,
|
|
55
|
-
AuthModule: () => AuthModule
|
|
56
|
-
AIModule: () => AIModule
|
|
55
|
+
AuthModule: () => AuthModule
|
|
57
56
|
});
|
|
58
57
|
module.exports = __toCommonJS(exports_src);
|
|
59
58
|
var import_axios = __toESM(require("axios"));
|
|
@@ -86,43 +85,6 @@ class BaseModule {
|
|
|
86
85
|
}
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
// src/modules/ai.ts
|
|
90
|
-
class AIModule extends BaseModule {
|
|
91
|
-
async chat(workspaceId, body) {
|
|
92
|
-
const { data } = await this.api.post(`/ai/${workspaceId}/chat`, body, { timeout: 300000 });
|
|
93
|
-
return data;
|
|
94
|
-
}
|
|
95
|
-
async detectIntent(workspaceId, body) {
|
|
96
|
-
const { data } = await this.api.post(`/ai/${workspaceId}/chat/intent`, body, { timeout: 300000 });
|
|
97
|
-
return data;
|
|
98
|
-
}
|
|
99
|
-
async executeIntent(workspaceId, body) {
|
|
100
|
-
const { data } = await this.api.post(`/ai/${workspaceId}/chat/execute`, body, { timeout: 300000 });
|
|
101
|
-
return data;
|
|
102
|
-
}
|
|
103
|
-
async listSessions(workspaceId) {
|
|
104
|
-
const { data } = await this.api.get(`/ai/${workspaceId}/sessions`);
|
|
105
|
-
return data;
|
|
106
|
-
}
|
|
107
|
-
async getSession(workspaceId, sessionId) {
|
|
108
|
-
const { data } = await this.api.get(`/ai/${workspaceId}/session/${sessionId}`);
|
|
109
|
-
return data;
|
|
110
|
-
}
|
|
111
|
-
getChatStreamUrl(workspaceId, sessionId) {
|
|
112
|
-
return `${this.api.defaults.baseURL}/ai/${workspaceId}/chat/stream?sessionId=${sessionId}`;
|
|
113
|
-
}
|
|
114
|
-
async deleteSession(workspaceId, sessionId) {
|
|
115
|
-
await this.api.delete(`/ai/${workspaceId}/session/${sessionId}`);
|
|
116
|
-
}
|
|
117
|
-
async shareSession(workspaceId, sessionId, body) {
|
|
118
|
-
await this.api.post(`/ai/${workspaceId}/session/${sessionId}/share`, body);
|
|
119
|
-
}
|
|
120
|
-
async getSharedSession(sessionId) {
|
|
121
|
-
const { data } = await this.api.get(`/ai/shared/${sessionId}`);
|
|
122
|
-
return data;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
88
|
// src/modules/auth.ts
|
|
127
89
|
class AuthModule extends BaseModule {
|
|
128
90
|
async getProfile() {
|
|
@@ -149,6 +111,10 @@ class AuthModule extends BaseModule {
|
|
|
149
111
|
const { data } = await this.api.post("/auth/complete-registration", body);
|
|
150
112
|
return data;
|
|
151
113
|
}
|
|
114
|
+
async deleteAccount() {
|
|
115
|
+
const { data } = await this.api.delete("/auth/account");
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
152
118
|
}
|
|
153
119
|
|
|
154
120
|
// src/modules/ci.ts
|
|
@@ -346,10 +312,6 @@ class TasksModule extends BaseModule {
|
|
|
346
312
|
updates
|
|
347
313
|
});
|
|
348
314
|
}
|
|
349
|
-
async getContext(id, workspaceId) {
|
|
350
|
-
const { data } = await this.api.get(`/workspaces/${workspaceId}/tasks/${id}/context`);
|
|
351
|
-
return data;
|
|
352
|
-
}
|
|
353
315
|
}
|
|
354
316
|
|
|
355
317
|
// src/modules/workspaces.ts
|
|
@@ -386,10 +348,6 @@ class WorkspacesModule extends BaseModule {
|
|
|
386
348
|
const { data } = await this.api.get(`/workspaces/${id}/stats`);
|
|
387
349
|
return data;
|
|
388
350
|
}
|
|
389
|
-
async getManifestStatus(workspaceId) {
|
|
390
|
-
const { data } = await this.api.get(`/workspaces/${workspaceId}/manifest-status`);
|
|
391
|
-
return data;
|
|
392
|
-
}
|
|
393
351
|
async getActivity(id, limit) {
|
|
394
352
|
const { data } = await this.api.get(`/workspaces/${id}/activity`, {
|
|
395
353
|
params: { limit }
|
|
@@ -400,6 +358,18 @@ class WorkspacesModule extends BaseModule {
|
|
|
400
358
|
const { data } = await this.api.post(`/workspaces/${id}/dispatch`, { workerId, sprintId });
|
|
401
359
|
return data.task;
|
|
402
360
|
}
|
|
361
|
+
async heartbeat(workspaceId, agentId, currentTaskId, status) {
|
|
362
|
+
const { data } = await this.api.post(`/workspaces/${workspaceId}/agents/heartbeat`, {
|
|
363
|
+
agentId,
|
|
364
|
+
currentTaskId: currentTaskId ?? null,
|
|
365
|
+
status: status ?? "WORKING"
|
|
366
|
+
});
|
|
367
|
+
return data.agent;
|
|
368
|
+
}
|
|
369
|
+
async getAgents(workspaceId) {
|
|
370
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/agents`);
|
|
371
|
+
return data.agents;
|
|
372
|
+
}
|
|
403
373
|
async listApiKeys(workspaceId) {
|
|
404
374
|
const { data } = await this.api.get(`/workspaces/${workspaceId}/api-keys`);
|
|
405
375
|
return data.apiKeys;
|
|
@@ -418,7 +388,6 @@ class LocusClient {
|
|
|
418
388
|
api;
|
|
419
389
|
emitter;
|
|
420
390
|
auth;
|
|
421
|
-
ai;
|
|
422
391
|
tasks;
|
|
423
392
|
sprints;
|
|
424
393
|
workspaces;
|
|
@@ -438,7 +407,6 @@ class LocusClient {
|
|
|
438
407
|
});
|
|
439
408
|
this.setupInterceptors();
|
|
440
409
|
this.auth = new AuthModule(this.api, this.emitter);
|
|
441
|
-
this.ai = new AIModule(this.api, this.emitter);
|
|
442
410
|
this.tasks = new TasksModule(this.api, this.emitter);
|
|
443
411
|
this.sprints = new SprintsModule(this.api, this.emitter);
|
|
444
412
|
this.workspaces = new WorkspacesModule(this.api, this.emitter);
|
|
@@ -512,8 +480,7 @@ __export(exports_worker, {
|
|
|
512
480
|
AgentWorker: () => AgentWorker
|
|
513
481
|
});
|
|
514
482
|
module.exports = __toCommonJS(exports_worker);
|
|
515
|
-
var
|
|
516
|
-
var import_node_path7 = require("node:path");
|
|
483
|
+
var import_shared3 = require("@locusai/shared");
|
|
517
484
|
|
|
518
485
|
// src/core/config.ts
|
|
519
486
|
var import_node_path = require("node:path");
|
|
@@ -523,19 +490,27 @@ var PROVIDER = {
|
|
|
523
490
|
};
|
|
524
491
|
var DEFAULT_MODEL = {
|
|
525
492
|
[PROVIDER.CLAUDE]: "opus",
|
|
526
|
-
[PROVIDER.CODEX]: "gpt-5.
|
|
493
|
+
[PROVIDER.CODEX]: "gpt-5.3-codex"
|
|
494
|
+
};
|
|
495
|
+
var LOCUS_SCHEMA_BASE_URL = "https://locusai.dev/schemas";
|
|
496
|
+
var LOCUS_SCHEMAS = {
|
|
497
|
+
config: `${LOCUS_SCHEMA_BASE_URL}/config.schema.json`,
|
|
498
|
+
settings: `${LOCUS_SCHEMA_BASE_URL}/settings.schema.json`
|
|
527
499
|
};
|
|
528
500
|
var LOCUS_CONFIG = {
|
|
529
501
|
dir: ".locus",
|
|
530
502
|
configFile: "config.json",
|
|
503
|
+
settingsFile: "settings.json",
|
|
531
504
|
indexFile: "codebase-index.json",
|
|
532
|
-
contextFile: "
|
|
505
|
+
contextFile: "LOCUS.md",
|
|
533
506
|
artifactsDir: "artifacts",
|
|
534
507
|
documentsDir: "documents",
|
|
535
|
-
agentSkillsDir: ".agent/skills",
|
|
536
508
|
sessionsDir: "sessions",
|
|
537
509
|
reviewsDir: "reviews",
|
|
538
|
-
plansDir: "plans"
|
|
510
|
+
plansDir: "plans",
|
|
511
|
+
projectDir: "project",
|
|
512
|
+
projectContextFile: "context.md",
|
|
513
|
+
projectProgressFile: "progress.md"
|
|
539
514
|
};
|
|
540
515
|
var LOCUS_GITIGNORE_PATTERNS = [
|
|
541
516
|
"# Locus AI - Session data (user-specific, can grow large)",
|
|
@@ -548,11 +523,17 @@ var LOCUS_GITIGNORE_PATTERNS = [
|
|
|
548
523
|
".locus/reviews/",
|
|
549
524
|
"",
|
|
550
525
|
"# Locus AI - Plans (generated per task)",
|
|
551
|
-
".locus/plans/"
|
|
526
|
+
".locus/plans/",
|
|
527
|
+
"",
|
|
528
|
+
"# Locus AI - Agent worktrees (parallel execution)",
|
|
529
|
+
".locus-worktrees/",
|
|
530
|
+
"",
|
|
531
|
+
"# Locus AI - Settings (contains API key, telegram config, etc.)",
|
|
532
|
+
".locus/settings.json"
|
|
552
533
|
];
|
|
553
534
|
function getLocusPath(projectPath, fileName) {
|
|
554
|
-
if (fileName === "
|
|
555
|
-
return import_node_path.join(projectPath, LOCUS_CONFIG.
|
|
535
|
+
if (fileName === "projectContextFile" || fileName === "projectProgressFile") {
|
|
536
|
+
return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.projectDir, LOCUS_CONFIG[fileName]);
|
|
556
537
|
}
|
|
557
538
|
return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG[fileName]);
|
|
558
539
|
}
|
|
@@ -628,6 +609,14 @@ var c = {
|
|
|
628
609
|
};
|
|
629
610
|
|
|
630
611
|
// src/ai/claude-runner.ts
|
|
612
|
+
var SANDBOX_SETTINGS = JSON.stringify({
|
|
613
|
+
sandbox: {
|
|
614
|
+
enabled: true,
|
|
615
|
+
autoAllow: true,
|
|
616
|
+
allowUnsandboxedCommands: false
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
631
620
|
class ClaudeRunner {
|
|
632
621
|
model;
|
|
633
622
|
log;
|
|
@@ -635,6 +624,7 @@ class ClaudeRunner {
|
|
|
635
624
|
eventEmitter;
|
|
636
625
|
currentToolName;
|
|
637
626
|
activeTools = new Map;
|
|
627
|
+
activeProcess = null;
|
|
638
628
|
constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log) {
|
|
639
629
|
this.model = model;
|
|
640
630
|
this.log = log;
|
|
@@ -643,6 +633,12 @@ class ClaudeRunner {
|
|
|
643
633
|
setEventEmitter(emitter) {
|
|
644
634
|
this.eventEmitter = emitter;
|
|
645
635
|
}
|
|
636
|
+
abort() {
|
|
637
|
+
if (this.activeProcess && !this.activeProcess.killed) {
|
|
638
|
+
this.activeProcess.kill("SIGTERM");
|
|
639
|
+
this.activeProcess = null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
646
642
|
async run(prompt) {
|
|
647
643
|
const maxRetries = 3;
|
|
648
644
|
let lastError = null;
|
|
@@ -671,7 +667,9 @@ class ClaudeRunner {
|
|
|
671
667
|
"stream-json",
|
|
672
668
|
"--include-partial-messages",
|
|
673
669
|
"--model",
|
|
674
|
-
this.model
|
|
670
|
+
this.model,
|
|
671
|
+
"--settings",
|
|
672
|
+
SANDBOX_SETTINGS
|
|
675
673
|
];
|
|
676
674
|
const env = {
|
|
677
675
|
...process.env,
|
|
@@ -688,6 +686,7 @@ class ClaudeRunner {
|
|
|
688
686
|
stdio: ["pipe", "pipe", "pipe"],
|
|
689
687
|
env
|
|
690
688
|
});
|
|
689
|
+
this.activeProcess = claude;
|
|
691
690
|
let buffer = "";
|
|
692
691
|
let stderrBuffer = "";
|
|
693
692
|
let resolveChunk = null;
|
|
@@ -755,6 +754,7 @@ class ClaudeRunner {
|
|
|
755
754
|
signalEnd();
|
|
756
755
|
});
|
|
757
756
|
claude.on("close", (code) => {
|
|
757
|
+
this.activeProcess = null;
|
|
758
758
|
if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
|
|
759
759
|
process.stderr.write(`${stderrBuffer}
|
|
760
760
|
`);
|
|
@@ -912,7 +912,9 @@ class ClaudeRunner {
|
|
|
912
912
|
"stream-json",
|
|
913
913
|
"--include-partial-messages",
|
|
914
914
|
"--model",
|
|
915
|
-
this.model
|
|
915
|
+
this.model,
|
|
916
|
+
"--settings",
|
|
917
|
+
SANDBOX_SETTINGS
|
|
916
918
|
];
|
|
917
919
|
const env = {
|
|
918
920
|
...process.env,
|
|
@@ -924,6 +926,7 @@ class ClaudeRunner {
|
|
|
924
926
|
stdio: ["pipe", "pipe", "pipe"],
|
|
925
927
|
env
|
|
926
928
|
});
|
|
929
|
+
this.activeProcess = claude;
|
|
927
930
|
let finalResult = "";
|
|
928
931
|
let errorOutput = "";
|
|
929
932
|
let buffer = "";
|
|
@@ -957,6 +960,7 @@ class ClaudeRunner {
|
|
|
957
960
|
reject(new Error(`Failed to start Claude CLI: ${err.message}. Please ensure the 'claude' command is available in your PATH.`));
|
|
958
961
|
});
|
|
959
962
|
claude.on("close", (code) => {
|
|
963
|
+
this.activeProcess = null;
|
|
960
964
|
if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
|
|
961
965
|
process.stderr.write(`${stderrBuffer}
|
|
962
966
|
`);
|
|
@@ -1023,11 +1027,18 @@ class CodexRunner {
|
|
|
1023
1027
|
projectPath;
|
|
1024
1028
|
model;
|
|
1025
1029
|
log;
|
|
1030
|
+
activeProcess = null;
|
|
1026
1031
|
constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log) {
|
|
1027
1032
|
this.projectPath = projectPath;
|
|
1028
1033
|
this.model = model;
|
|
1029
1034
|
this.log = log;
|
|
1030
1035
|
}
|
|
1036
|
+
abort() {
|
|
1037
|
+
if (this.activeProcess && !this.activeProcess.killed) {
|
|
1038
|
+
this.activeProcess.kill("SIGTERM");
|
|
1039
|
+
this.activeProcess = null;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1031
1042
|
async run(prompt) {
|
|
1032
1043
|
const maxRetries = 3;
|
|
1033
1044
|
let lastError = null;
|
|
@@ -1054,6 +1065,7 @@ class CodexRunner {
|
|
|
1054
1065
|
env: process.env,
|
|
1055
1066
|
shell: false
|
|
1056
1067
|
});
|
|
1068
|
+
this.activeProcess = codex;
|
|
1057
1069
|
let resolveChunk = null;
|
|
1058
1070
|
const chunkQueue = [];
|
|
1059
1071
|
let processEnded = false;
|
|
@@ -1103,6 +1115,7 @@ class CodexRunner {
|
|
|
1103
1115
|
signalEnd();
|
|
1104
1116
|
});
|
|
1105
1117
|
codex.on("close", (code) => {
|
|
1118
|
+
this.activeProcess = null;
|
|
1106
1119
|
this.cleanupTempFile(outputPath);
|
|
1107
1120
|
if (code === 0) {
|
|
1108
1121
|
const result = this.readOutput(outputPath, finalOutput);
|
|
@@ -1148,6 +1161,7 @@ class CodexRunner {
|
|
|
1148
1161
|
env: process.env,
|
|
1149
1162
|
shell: false
|
|
1150
1163
|
});
|
|
1164
|
+
this.activeProcess = codex;
|
|
1151
1165
|
let output = "";
|
|
1152
1166
|
let errorOutput = "";
|
|
1153
1167
|
const handleOutput = (data) => {
|
|
@@ -1165,6 +1179,7 @@ class CodexRunner {
|
|
|
1165
1179
|
reject(new Error(`Failed to start Codex CLI: ${err.message}. ` + `Ensure 'codex' is installed and available in PATH.`));
|
|
1166
1180
|
});
|
|
1167
1181
|
codex.on("close", (code) => {
|
|
1182
|
+
this.activeProcess = null;
|
|
1168
1183
|
this.cleanupTempFile(outputPath);
|
|
1169
1184
|
if (code === 0) {
|
|
1170
1185
|
resolve2(this.readOutput(outputPath, output));
|
|
@@ -1179,7 +1194,8 @@ class CodexRunner {
|
|
|
1179
1194
|
buildArgs(outputPath) {
|
|
1180
1195
|
const args = [
|
|
1181
1196
|
"exec",
|
|
1182
|
-
"--
|
|
1197
|
+
"--sandbox",
|
|
1198
|
+
"workspace-write",
|
|
1183
1199
|
"--skip-git-repo-check",
|
|
1184
1200
|
"--output-last-message",
|
|
1185
1201
|
outputPath
|
|
@@ -1246,435 +1262,805 @@ function createAiRunner(provider, config) {
|
|
|
1246
1262
|
}
|
|
1247
1263
|
}
|
|
1248
1264
|
|
|
1249
|
-
// src/
|
|
1250
|
-
var
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1265
|
+
// src/git/git-utils.ts
|
|
1266
|
+
var import_node_child_process3 = require("node:child_process");
|
|
1267
|
+
function isGitAvailable() {
|
|
1268
|
+
try {
|
|
1269
|
+
import_node_child_process3.execFileSync("git", ["--version"], {
|
|
1270
|
+
encoding: "utf-8",
|
|
1271
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1272
|
+
});
|
|
1273
|
+
return true;
|
|
1274
|
+
} catch {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function isGhAvailable(projectPath) {
|
|
1279
|
+
try {
|
|
1280
|
+
import_node_child_process3.execFileSync("gh", ["auth", "status"], {
|
|
1281
|
+
cwd: projectPath,
|
|
1282
|
+
encoding: "utf-8",
|
|
1283
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1284
|
+
});
|
|
1285
|
+
return true;
|
|
1286
|
+
} catch {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function getGhUsername() {
|
|
1291
|
+
try {
|
|
1292
|
+
const output = import_node_child_process3.execFileSync("gh", ["api", "user", "--jq", ".login"], {
|
|
1293
|
+
encoding: "utf-8",
|
|
1294
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1295
|
+
}).trim();
|
|
1296
|
+
return output || null;
|
|
1297
|
+
} catch {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
function detectRemoteProvider(projectPath) {
|
|
1302
|
+
const url = getRemoteUrl(projectPath);
|
|
1303
|
+
if (!url)
|
|
1304
|
+
return "unknown";
|
|
1305
|
+
if (url.includes("github.com"))
|
|
1306
|
+
return "github";
|
|
1307
|
+
if (url.includes("gitlab.com") || url.includes("gitlab"))
|
|
1308
|
+
return "gitlab";
|
|
1309
|
+
if (url.includes("bitbucket.org"))
|
|
1310
|
+
return "bitbucket";
|
|
1311
|
+
return "unknown";
|
|
1312
|
+
}
|
|
1313
|
+
function getRemoteUrl(projectPath, remote = "origin") {
|
|
1314
|
+
try {
|
|
1315
|
+
return import_node_child_process3.execFileSync("git", ["remote", "get-url", remote], {
|
|
1316
|
+
cwd: projectPath,
|
|
1317
|
+
encoding: "utf-8",
|
|
1318
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1319
|
+
}).trim();
|
|
1320
|
+
} catch {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function getCurrentBranch(projectPath) {
|
|
1325
|
+
return import_node_child_process3.execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1326
|
+
cwd: projectPath,
|
|
1327
|
+
encoding: "utf-8",
|
|
1328
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1329
|
+
}).trim();
|
|
1330
|
+
}
|
|
1331
|
+
function getDefaultBranch(projectPath, remote = "origin") {
|
|
1332
|
+
try {
|
|
1333
|
+
const ref = import_node_child_process3.execFileSync("git", ["symbolic-ref", `refs/remotes/${remote}/HEAD`], {
|
|
1334
|
+
cwd: projectPath,
|
|
1335
|
+
encoding: "utf-8",
|
|
1336
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1337
|
+
}).trim();
|
|
1338
|
+
return ref.replace(`refs/remotes/${remote}/`, "");
|
|
1339
|
+
} catch {
|
|
1340
|
+
for (const candidate of ["main", "master"]) {
|
|
1341
|
+
try {
|
|
1342
|
+
import_node_child_process3.execFileSync("git", ["ls-remote", "--exit-code", "--heads", remote, candidate], {
|
|
1343
|
+
cwd: projectPath,
|
|
1344
|
+
encoding: "utf-8",
|
|
1345
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1346
|
+
});
|
|
1347
|
+
return candidate;
|
|
1348
|
+
} catch {}
|
|
1349
|
+
}
|
|
1350
|
+
try {
|
|
1351
|
+
return getCurrentBranch(projectPath);
|
|
1352
|
+
} catch {
|
|
1353
|
+
return "main";
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1254
1357
|
|
|
1255
|
-
|
|
1358
|
+
// src/git/pr-service.ts
|
|
1359
|
+
var import_node_child_process4 = require("node:child_process");
|
|
1360
|
+
class PrService {
|
|
1256
1361
|
projectPath;
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
constructor(projectPath) {
|
|
1362
|
+
log;
|
|
1363
|
+
constructor(projectPath, log) {
|
|
1260
1364
|
this.projectPath = projectPath;
|
|
1261
|
-
this.
|
|
1365
|
+
this.log = log;
|
|
1262
1366
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1367
|
+
createPr(options) {
|
|
1368
|
+
const {
|
|
1369
|
+
task,
|
|
1370
|
+
branch,
|
|
1371
|
+
baseBranch: requestedBaseBranch,
|
|
1372
|
+
agentId,
|
|
1373
|
+
summary
|
|
1374
|
+
} = options;
|
|
1375
|
+
const provider = detectRemoteProvider(this.projectPath);
|
|
1376
|
+
if (provider !== "github") {
|
|
1377
|
+
throw new Error(`PR creation is only supported for GitHub repositories (detected: ${provider})`);
|
|
1378
|
+
}
|
|
1379
|
+
if (!isGhAvailable(this.projectPath)) {
|
|
1380
|
+
throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
|
|
1381
|
+
}
|
|
1382
|
+
const title = `[Locus] ${task.title}`;
|
|
1383
|
+
const body = this.buildPrBody(task, agentId, summary);
|
|
1384
|
+
const baseBranch = requestedBaseBranch ?? getDefaultBranch(this.projectPath);
|
|
1385
|
+
this.validateCreatePrInputs(baseBranch, branch);
|
|
1386
|
+
this.log(`Creating PR: ${title} (${branch} → ${baseBranch})`, "info");
|
|
1387
|
+
const output = import_node_child_process4.execFileSync("gh", [
|
|
1388
|
+
"pr",
|
|
1389
|
+
"create",
|
|
1390
|
+
"--title",
|
|
1391
|
+
title,
|
|
1392
|
+
"--body",
|
|
1393
|
+
body,
|
|
1394
|
+
"--base",
|
|
1395
|
+
baseBranch,
|
|
1396
|
+
"--head",
|
|
1397
|
+
branch
|
|
1398
|
+
], {
|
|
1399
|
+
cwd: this.projectPath,
|
|
1400
|
+
encoding: "utf-8",
|
|
1401
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1402
|
+
}).trim();
|
|
1403
|
+
const url = output;
|
|
1404
|
+
const prNumber = this.extractPrNumber(url);
|
|
1405
|
+
this.log(`PR created: ${url}`, "success");
|
|
1406
|
+
return { url, number: prNumber };
|
|
1407
|
+
}
|
|
1408
|
+
validateCreatePrInputs(baseBranch, headBranch) {
|
|
1409
|
+
if (!this.hasRemoteBranch(baseBranch)) {
|
|
1410
|
+
throw new Error(`Base branch "${baseBranch}" does not exist on origin. Push/fetch refs and retry.`);
|
|
1266
1411
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
const treeString = currentFiles.join(`
|
|
1270
|
-
`);
|
|
1271
|
-
const newTreeHash = this.hashTree(treeString);
|
|
1272
|
-
const existingIndex = this.loadIndex();
|
|
1273
|
-
const currentHashes = this.computeFileHashes(currentFiles);
|
|
1274
|
-
const existingHashes = existingIndex?.fileHashes;
|
|
1275
|
-
const hasExistingContent = existingIndex && (Object.keys(existingIndex.symbols).length > 0 || Object.keys(existingIndex.responsibilities).length > 0);
|
|
1276
|
-
const canIncremental = !force && existingIndex && existingHashes && hasExistingContent;
|
|
1277
|
-
if (canIncremental) {
|
|
1278
|
-
onProgress?.("Performing incremental update");
|
|
1279
|
-
const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
|
|
1280
|
-
const changedFiles = [...added, ...modified];
|
|
1281
|
-
const totalChanges = changedFiles.length + deleted.length;
|
|
1282
|
-
const existingFileCount = Object.keys(existingHashes).length;
|
|
1283
|
-
onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
|
|
1284
|
-
if (existingFileCount > 0) {
|
|
1285
|
-
const changeRatio = totalChanges / existingFileCount;
|
|
1286
|
-
if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
|
|
1287
|
-
onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
|
|
1288
|
-
const incrementalIndex = await treeSummarizer(changedFiles.join(`
|
|
1289
|
-
`));
|
|
1290
|
-
const updatedIndex = this.cloneIndex(existingIndex);
|
|
1291
|
-
this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
|
|
1292
|
-
return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
|
|
1293
|
-
}
|
|
1294
|
-
if (changedFiles.length === 0 && deleted.length > 0) {
|
|
1295
|
-
onProgress?.(`Removing ${deleted.length} deleted files from index`);
|
|
1296
|
-
const updatedIndex = this.cloneIndex(existingIndex);
|
|
1297
|
-
this.removeFilesFromIndex(updatedIndex, deleted);
|
|
1298
|
-
return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
|
|
1299
|
-
}
|
|
1300
|
-
if (changedFiles.length === 0 && deleted.length === 0) {
|
|
1301
|
-
onProgress?.("No actual file changes, updating hashes only");
|
|
1302
|
-
const updatedIndex = this.cloneIndex(existingIndex);
|
|
1303
|
-
return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
|
|
1304
|
-
}
|
|
1305
|
-
onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
|
|
1306
|
-
}
|
|
1412
|
+
if (!this.hasRemoteBranch(headBranch)) {
|
|
1413
|
+
throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
|
|
1307
1414
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
} catch (error) {
|
|
1313
|
-
throw new Error(`AI analysis failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1415
|
+
const baseRef = this.resolveBranchRef(baseBranch);
|
|
1416
|
+
const headRef = this.resolveBranchRef(headBranch);
|
|
1417
|
+
if (!baseRef) {
|
|
1418
|
+
throw new Error(`Could not resolve base branch "${baseBranch}" locally.`);
|
|
1314
1419
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
const gitmodulesPath = import_node_path4.join(this.projectPath, ".gitmodules");
|
|
1318
|
-
const submoduleIgnores = [];
|
|
1319
|
-
if (import_node_fs2.existsSync(gitmodulesPath)) {
|
|
1320
|
-
try {
|
|
1321
|
-
const content = import_node_fs2.readFileSync(gitmodulesPath, "utf-8");
|
|
1322
|
-
const lines = content.split(`
|
|
1323
|
-
`);
|
|
1324
|
-
for (const line of lines) {
|
|
1325
|
-
const match = line.match(/^\s*path\s*=\s*(.*)$/);
|
|
1326
|
-
const path = match?.[1]?.trim();
|
|
1327
|
-
if (path) {
|
|
1328
|
-
submoduleIgnores.push(`${path}/**`);
|
|
1329
|
-
submoduleIgnores.push(`**/${path}/**`);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
} catch {}
|
|
1420
|
+
if (!headRef) {
|
|
1421
|
+
throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
|
|
1333
1422
|
}
|
|
1334
|
-
|
|
1423
|
+
const commitsAhead = this.countCommitsAhead(baseRef, headRef);
|
|
1424
|
+
if (commitsAhead <= 0) {
|
|
1425
|
+
throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
countCommitsAhead(baseRef, headRef) {
|
|
1429
|
+
const output = import_node_child_process4.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
|
|
1335
1430
|
cwd: this.projectPath,
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
"**/build/**",
|
|
1342
|
-
"**/target/**",
|
|
1343
|
-
"**/bin/**",
|
|
1344
|
-
"**/obj/**",
|
|
1345
|
-
"**/.next/**",
|
|
1346
|
-
"**/.svelte-kit/**",
|
|
1347
|
-
"**/.nuxt/**",
|
|
1348
|
-
"**/.cache/**",
|
|
1349
|
-
"**/out/**",
|
|
1350
|
-
"**/__tests__/**",
|
|
1351
|
-
"**/coverage/**",
|
|
1352
|
-
"**/*.test.*",
|
|
1353
|
-
"**/*.spec.*",
|
|
1354
|
-
"**/*.d.ts",
|
|
1355
|
-
"**/tsconfig.tsbuildinfo",
|
|
1356
|
-
"**/.locus/*.json",
|
|
1357
|
-
"**/.locus/*.md",
|
|
1358
|
-
"**/.locus/!(artifacts)/**",
|
|
1359
|
-
"**/.git/**",
|
|
1360
|
-
"**/.svn/**",
|
|
1361
|
-
"**/.hg/**",
|
|
1362
|
-
"**/.vscode/**",
|
|
1363
|
-
"**/.idea/**",
|
|
1364
|
-
"**/.DS_Store",
|
|
1365
|
-
"**/bun.lock",
|
|
1366
|
-
"**/package-lock.json",
|
|
1367
|
-
"**/yarn.lock",
|
|
1368
|
-
"**/pnpm-lock.yaml",
|
|
1369
|
-
"**/Cargo.lock",
|
|
1370
|
-
"**/go.sum",
|
|
1371
|
-
"**/poetry.lock",
|
|
1372
|
-
"**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
|
|
1373
|
-
]
|
|
1374
|
-
});
|
|
1431
|
+
encoding: "utf-8",
|
|
1432
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1433
|
+
}).trim();
|
|
1434
|
+
const value = Number.parseInt(output || "0", 10);
|
|
1435
|
+
return Number.isNaN(value) ? 0 : value;
|
|
1375
1436
|
}
|
|
1376
|
-
|
|
1377
|
-
if (
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
}
|
|
1437
|
+
resolveBranchRef(branch) {
|
|
1438
|
+
if (this.hasLocalBranch(branch)) {
|
|
1439
|
+
return branch;
|
|
1440
|
+
}
|
|
1441
|
+
if (this.hasRemoteTrackingBranch(branch)) {
|
|
1442
|
+
return `origin/${branch}`;
|
|
1383
1443
|
}
|
|
1384
1444
|
return null;
|
|
1385
1445
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1446
|
+
hasLocalBranch(branch) {
|
|
1447
|
+
try {
|
|
1448
|
+
import_node_child_process4.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
|
|
1449
|
+
cwd: this.projectPath,
|
|
1450
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1451
|
+
});
|
|
1452
|
+
return true;
|
|
1453
|
+
} catch {
|
|
1454
|
+
return false;
|
|
1390
1455
|
}
|
|
1391
|
-
import_node_fs2.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
|
|
1392
1456
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1457
|
+
hasRemoteTrackingBranch(branch) {
|
|
1458
|
+
try {
|
|
1459
|
+
import_node_child_process4.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`], {
|
|
1460
|
+
cwd: this.projectPath,
|
|
1461
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1462
|
+
});
|
|
1463
|
+
return true;
|
|
1464
|
+
} catch {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1395
1467
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1468
|
+
hasRemoteBranch(branch) {
|
|
1469
|
+
try {
|
|
1470
|
+
import_node_child_process4.execFileSync("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], {
|
|
1471
|
+
cwd: this.projectPath,
|
|
1472
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1473
|
+
});
|
|
1474
|
+
return true;
|
|
1475
|
+
} catch {
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1401
1478
|
}
|
|
1402
|
-
|
|
1403
|
-
return
|
|
1479
|
+
getPrDiff(branch) {
|
|
1480
|
+
return import_node_child_process4.execFileSync("gh", ["pr", "diff", branch], {
|
|
1481
|
+
cwd: this.projectPath,
|
|
1482
|
+
encoding: "utf-8",
|
|
1483
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1484
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1485
|
+
});
|
|
1404
1486
|
}
|
|
1405
|
-
|
|
1487
|
+
submitReview(prIdentifier, body, event) {
|
|
1406
1488
|
try {
|
|
1407
|
-
|
|
1408
|
-
|
|
1489
|
+
import_node_child_process4.execFileSync("gh", [
|
|
1490
|
+
"pr",
|
|
1491
|
+
"review",
|
|
1492
|
+
prIdentifier,
|
|
1493
|
+
"--body",
|
|
1494
|
+
body,
|
|
1495
|
+
`--${event.toLowerCase().replace("_", "-")}`
|
|
1496
|
+
], {
|
|
1497
|
+
cwd: this.projectPath,
|
|
1498
|
+
encoding: "utf-8",
|
|
1499
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1500
|
+
});
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1503
|
+
if (event === "REQUEST_CHANGES" && msg.includes("own pull request")) {
|
|
1504
|
+
import_node_child_process4.execFileSync("gh", ["pr", "review", prIdentifier, "--body", body, "--comment"], {
|
|
1505
|
+
cwd: this.projectPath,
|
|
1506
|
+
encoding: "utf-8",
|
|
1507
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1508
|
+
});
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
throw err;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
listLocusPrs() {
|
|
1515
|
+
try {
|
|
1516
|
+
const output = import_node_child_process4.execFileSync("gh", [
|
|
1517
|
+
"pr",
|
|
1518
|
+
"list",
|
|
1519
|
+
"--search",
|
|
1520
|
+
"[Locus] in:title",
|
|
1521
|
+
"--state",
|
|
1522
|
+
"open",
|
|
1523
|
+
"--json",
|
|
1524
|
+
"number,title,url,headRefName"
|
|
1525
|
+
], {
|
|
1526
|
+
cwd: this.projectPath,
|
|
1527
|
+
encoding: "utf-8",
|
|
1528
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1529
|
+
}).trim();
|
|
1530
|
+
const prs = JSON.parse(output || "[]");
|
|
1531
|
+
return prs.map((pr) => ({
|
|
1532
|
+
number: pr.number,
|
|
1533
|
+
title: pr.title,
|
|
1534
|
+
url: pr.url,
|
|
1535
|
+
branch: pr.headRefName
|
|
1536
|
+
}));
|
|
1409
1537
|
} catch {
|
|
1410
|
-
|
|
1538
|
+
this.log("Failed to list Locus PRs", "warn");
|
|
1539
|
+
return [];
|
|
1411
1540
|
}
|
|
1412
1541
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
}
|
|
1542
|
+
hasLocusReview(prNumber) {
|
|
1543
|
+
try {
|
|
1544
|
+
const output = import_node_child_process4.execFileSync("gh", ["pr", "view", prNumber, "--json", "reviews"], {
|
|
1545
|
+
cwd: this.projectPath,
|
|
1546
|
+
encoding: "utf-8",
|
|
1547
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1548
|
+
}).trim();
|
|
1549
|
+
const data = JSON.parse(output || "{}");
|
|
1550
|
+
return data.reviews?.some((r) => r.body?.includes("## Locus Agent Review")) ?? false;
|
|
1551
|
+
} catch {
|
|
1552
|
+
return false;
|
|
1420
1553
|
}
|
|
1421
|
-
return hashes;
|
|
1422
1554
|
}
|
|
1423
|
-
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
const existingSet = new Set(existingFiles);
|
|
1427
|
-
const currentSet = new Set(currentFiles);
|
|
1428
|
-
const added = currentFiles.filter((f) => !existingSet.has(f));
|
|
1429
|
-
const deleted = existingFiles.filter((f) => !currentSet.has(f));
|
|
1430
|
-
const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
|
|
1431
|
-
return { added, deleted, modified };
|
|
1555
|
+
listUnreviewedLocusPrs() {
|
|
1556
|
+
const allPrs = this.listLocusPrs();
|
|
1557
|
+
return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
|
|
1432
1558
|
}
|
|
1433
|
-
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1559
|
+
buildPrBody(task, agentId, summary) {
|
|
1560
|
+
const sections = [];
|
|
1561
|
+
sections.push(`## Task: ${task.title}`);
|
|
1562
|
+
sections.push("");
|
|
1563
|
+
if (task.description) {
|
|
1564
|
+
sections.push(task.description);
|
|
1565
|
+
sections.push("");
|
|
1437
1566
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1567
|
+
if (task.acceptanceChecklist?.length > 0) {
|
|
1568
|
+
sections.push("## Acceptance Criteria");
|
|
1569
|
+
for (const item of task.acceptanceChecklist) {
|
|
1570
|
+
sections.push(`- [ ] ${item.text}`);
|
|
1442
1571
|
}
|
|
1572
|
+
sections.push("");
|
|
1443
1573
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
if (mergedSymbols[symbol]) {
|
|
1449
|
-
mergedSymbols[symbol] = [
|
|
1450
|
-
...new Set([...mergedSymbols[symbol], ...paths])
|
|
1451
|
-
];
|
|
1452
|
-
} else {
|
|
1453
|
-
mergedSymbols[symbol] = paths;
|
|
1454
|
-
}
|
|
1574
|
+
if (summary) {
|
|
1575
|
+
sections.push("## Agent Summary");
|
|
1576
|
+
sections.push(summary);
|
|
1577
|
+
sections.push("");
|
|
1455
1578
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
return this.applyIndexMetadata(merged, newHashes, newTreeHash);
|
|
1579
|
+
sections.push("---");
|
|
1580
|
+
sections.push(`*Created by Locus Agent \`${agentId.slice(-8)}\`* | Task ID: \`${task.id}\``);
|
|
1581
|
+
return sections.join(`
|
|
1582
|
+
`);
|
|
1583
|
+
}
|
|
1584
|
+
extractPrNumber(url) {
|
|
1585
|
+
const match = url.match(/\/pull\/(\d+)/);
|
|
1586
|
+
return match ? Number.parseInt(match[1], 10) : 0;
|
|
1465
1587
|
}
|
|
1466
1588
|
}
|
|
1467
1589
|
|
|
1468
|
-
// src/
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1590
|
+
// src/project/knowledge-base.ts
|
|
1591
|
+
var import_node_fs2 = require("node:fs");
|
|
1592
|
+
var import_node_path4 = require("node:path");
|
|
1593
|
+
class KnowledgeBase {
|
|
1594
|
+
contextPath;
|
|
1595
|
+
progressPath;
|
|
1596
|
+
constructor(projectPath) {
|
|
1597
|
+
this.contextPath = getLocusPath(projectPath, "projectContextFile");
|
|
1598
|
+
this.progressPath = getLocusPath(projectPath, "projectProgressFile");
|
|
1475
1599
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
|
|
1480
|
-
1. Key symbols (classes, functions, types) and their locations
|
|
1481
|
-
2. Responsibilities of each directory/file
|
|
1482
|
-
3. Overall project structure
|
|
1483
|
-
|
|
1484
|
-
Analyze this file tree and provide a JSON response with:
|
|
1485
|
-
- "symbols": object mapping symbol names to file paths (array)
|
|
1486
|
-
- "responsibilities": object mapping paths to brief descriptions
|
|
1487
|
-
|
|
1488
|
-
File tree:
|
|
1489
|
-
${tree}
|
|
1490
|
-
|
|
1491
|
-
Return ONLY valid JSON, no markdown formatting.`;
|
|
1492
|
-
const response = await this.deps.aiRunner.run(prompt);
|
|
1493
|
-
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
1494
|
-
if (jsonMatch) {
|
|
1495
|
-
return JSON.parse(jsonMatch[0]);
|
|
1496
|
-
}
|
|
1497
|
-
return { symbols: {}, responsibilities: {}, lastIndexed: "" };
|
|
1498
|
-
}, force);
|
|
1499
|
-
if (index === null) {
|
|
1500
|
-
this.deps.log("No changes detected, skipping reindex", "info");
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
this.indexer.saveIndex(index);
|
|
1504
|
-
this.deps.log("Codebase reindexed successfully", "success");
|
|
1505
|
-
} catch (error) {
|
|
1506
|
-
this.deps.log(`Failed to reindex codebase: ${error}`, "error");
|
|
1600
|
+
readContext() {
|
|
1601
|
+
if (!import_node_fs2.existsSync(this.contextPath)) {
|
|
1602
|
+
return "";
|
|
1507
1603
|
}
|
|
1604
|
+
return import_node_fs2.readFileSync(this.contextPath, "utf-8");
|
|
1508
1605
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
class DocumentFetcher {
|
|
1515
|
-
deps;
|
|
1516
|
-
constructor(deps) {
|
|
1517
|
-
this.deps = deps;
|
|
1606
|
+
readProgress() {
|
|
1607
|
+
if (!import_node_fs2.existsSync(this.progressPath)) {
|
|
1608
|
+
return "";
|
|
1609
|
+
}
|
|
1610
|
+
return import_node_fs2.readFileSync(this.progressPath, "utf-8");
|
|
1518
1611
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1612
|
+
updateContext(content) {
|
|
1613
|
+
this.ensureDir(this.contextPath);
|
|
1614
|
+
import_node_fs2.writeFileSync(this.contextPath, content);
|
|
1615
|
+
}
|
|
1616
|
+
updateProgress(event) {
|
|
1617
|
+
this.ensureDir(this.progressPath);
|
|
1618
|
+
const existing = this.readProgress();
|
|
1619
|
+
const timestamp = (event.timestamp ?? new Date).toISOString();
|
|
1620
|
+
let entry = "";
|
|
1621
|
+
switch (event.type) {
|
|
1622
|
+
case "task_completed":
|
|
1623
|
+
entry = `- [x] ${event.title} — completed ${timestamp}`;
|
|
1624
|
+
break;
|
|
1625
|
+
case "sprint_started":
|
|
1626
|
+
entry = `
|
|
1627
|
+
## Current Sprint: ${event.title}
|
|
1628
|
+
**Status:** ACTIVE | Started: ${timestamp}
|
|
1629
|
+
`;
|
|
1630
|
+
break;
|
|
1631
|
+
case "sprint_completed":
|
|
1632
|
+
entry = `
|
|
1633
|
+
### Sprint Completed: ${event.title} — ${timestamp}
|
|
1634
|
+
`;
|
|
1635
|
+
break;
|
|
1636
|
+
case "blocker":
|
|
1637
|
+
entry = `- BLOCKER: ${event.title}`;
|
|
1638
|
+
break;
|
|
1639
|
+
case "pr_opened":
|
|
1640
|
+
entry = `- [ ] ${event.title} — PR opened ${timestamp}`;
|
|
1641
|
+
break;
|
|
1642
|
+
case "pr_reviewed":
|
|
1643
|
+
entry = `- ${event.title} — reviewed ${timestamp}`;
|
|
1644
|
+
break;
|
|
1645
|
+
case "pr_merged":
|
|
1646
|
+
entry = `- [x] ${event.title} — PR merged ${timestamp}`;
|
|
1647
|
+
break;
|
|
1648
|
+
case "exec_completed":
|
|
1649
|
+
entry = `- [x] ${event.title} — exec ${timestamp}`;
|
|
1650
|
+
break;
|
|
1523
1651
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
const docs2 = await this.deps.client.docs.list(this.deps.workspaceId);
|
|
1528
|
-
const artifactsGroupId = groups.find((g) => g.name === "Artifacts")?.id;
|
|
1529
|
-
let fetchedCount = 0;
|
|
1530
|
-
for (const doc of docs2) {
|
|
1531
|
-
if (doc.groupId === artifactsGroupId) {
|
|
1532
|
-
continue;
|
|
1533
|
-
}
|
|
1534
|
-
const groupName = groupMap.get(doc.groupId || "") || "General";
|
|
1535
|
-
const groupDir = import_node_path5.join(documentsDir, groupName);
|
|
1536
|
-
if (!import_node_fs3.existsSync(groupDir)) {
|
|
1537
|
-
import_node_fs3.mkdirSync(groupDir, { recursive: true });
|
|
1538
|
-
}
|
|
1539
|
-
const fileName = `${doc.title}.md`;
|
|
1540
|
-
const filePath = import_node_path5.join(groupDir, fileName);
|
|
1541
|
-
if (!import_node_fs3.existsSync(filePath) || import_node_fs3.readFileSync(filePath, "utf-8") !== doc.content) {
|
|
1542
|
-
import_node_fs3.writeFileSync(filePath, doc.content || "");
|
|
1543
|
-
fetchedCount++;
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
if (fetchedCount > 0) {
|
|
1547
|
-
this.deps.log(`Fetched ${fetchedCount} document(s) from server`, "info");
|
|
1548
|
-
}
|
|
1549
|
-
} catch (error) {
|
|
1550
|
-
this.deps.log(`Failed to fetch documents: ${error}`, "error");
|
|
1652
|
+
if (event.details) {
|
|
1653
|
+
entry += `
|
|
1654
|
+
${event.details}`;
|
|
1551
1655
|
}
|
|
1552
|
-
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
// src/agent/review-service.ts
|
|
1556
|
-
var import_node_child_process3 = require("node:child_process");
|
|
1656
|
+
const updated = existing ? `${existing}
|
|
1657
|
+
${entry}` : `# Project Progress
|
|
1557
1658
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
constructor(deps) {
|
|
1561
|
-
this.deps = deps;
|
|
1659
|
+
${entry}`;
|
|
1660
|
+
import_node_fs2.writeFileSync(this.progressPath, updated);
|
|
1562
1661
|
}
|
|
1563
|
-
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
log(`Failed to stage changes: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1570
|
-
return null;
|
|
1571
|
-
}
|
|
1572
|
-
let diff;
|
|
1573
|
-
try {
|
|
1574
|
-
diff = import_node_child_process3.execSync("git diff --cached --stat && echo '---' && git diff --cached", {
|
|
1575
|
-
cwd: projectPath,
|
|
1576
|
-
maxBuffer: 10 * 1024 * 1024
|
|
1577
|
-
}).toString();
|
|
1578
|
-
} catch (err) {
|
|
1579
|
-
log(`Failed to get staged diff: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1580
|
-
return null;
|
|
1662
|
+
getFullContext() {
|
|
1663
|
+
const context = this.readContext();
|
|
1664
|
+
const progress = this.readProgress();
|
|
1665
|
+
const parts = [];
|
|
1666
|
+
if (context.trim()) {
|
|
1667
|
+
parts.push(context.trim());
|
|
1581
1668
|
}
|
|
1582
|
-
if (
|
|
1583
|
-
|
|
1669
|
+
if (progress.trim()) {
|
|
1670
|
+
parts.push(progress.trim());
|
|
1584
1671
|
}
|
|
1585
|
-
|
|
1586
|
-
const reviewPrompt = `# Code Review Request
|
|
1672
|
+
return parts.join(`
|
|
1587
1673
|
|
|
1588
|
-
|
|
1589
|
-
${sprintInfo}
|
|
1590
|
-
Date: ${new Date().toISOString()}
|
|
1674
|
+
---
|
|
1591
1675
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1676
|
+
`);
|
|
1677
|
+
}
|
|
1678
|
+
initialize(info) {
|
|
1679
|
+
this.ensureDir(this.contextPath);
|
|
1680
|
+
this.ensureDir(this.progressPath);
|
|
1681
|
+
const techStackList = info.techStack.map((t) => `- ${t}`).join(`
|
|
1682
|
+
`);
|
|
1683
|
+
const contextContent = `# Project: ${info.name}
|
|
1596
1684
|
|
|
1597
|
-
##
|
|
1598
|
-
|
|
1685
|
+
## Mission
|
|
1686
|
+
${info.mission}
|
|
1599
1687
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
3. **Code Quality** — Note any code quality concerns (naming, structure, complexity).
|
|
1603
|
-
4. **Potential Issues** — Identify bugs, security issues, edge cases, or regressions.
|
|
1604
|
-
5. **Recommendations** — Actionable suggestions for improvement.
|
|
1605
|
-
6. **Overall Assessment** — A short verdict (e.g., "Looks good", "Needs attention", "Critical issues found").
|
|
1688
|
+
## Tech Stack
|
|
1689
|
+
${techStackList}
|
|
1606
1690
|
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
log("Running AI review on staged changes...", "info");
|
|
1610
|
-
const report = await this.deps.aiRunner.run(reviewPrompt);
|
|
1611
|
-
return report;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1691
|
+
## Architecture
|
|
1692
|
+
<!-- Describe your high-level architecture here -->
|
|
1614
1693
|
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
var import_node_os2 = require("node:os");
|
|
1618
|
-
var import_node_path6 = require("node:path");
|
|
1619
|
-
var import_shared2 = require("@locusai/shared");
|
|
1620
|
-
class PromptBuilder {
|
|
1621
|
-
projectPath;
|
|
1622
|
-
constructor(projectPath) {
|
|
1623
|
-
this.projectPath = projectPath;
|
|
1624
|
-
}
|
|
1625
|
-
async build(task, options = {}) {
|
|
1626
|
-
let prompt = `# Task: ${task.title}
|
|
1694
|
+
## Key Decisions
|
|
1695
|
+
<!-- Document important technical decisions and their rationale -->
|
|
1627
1696
|
|
|
1697
|
+
## Feature Areas
|
|
1698
|
+
<!-- List your main feature areas and their status -->
|
|
1628
1699
|
`;
|
|
1629
|
-
const
|
|
1630
|
-
if (roleText) {
|
|
1631
|
-
prompt += `## Role
|
|
1632
|
-
You are acting as a ${roleText}.
|
|
1700
|
+
const progressContent = `# Project Progress
|
|
1633
1701
|
|
|
1702
|
+
No sprints started yet.
|
|
1634
1703
|
`;
|
|
1704
|
+
import_node_fs2.writeFileSync(this.contextPath, contextContent);
|
|
1705
|
+
import_node_fs2.writeFileSync(this.progressPath, progressContent);
|
|
1706
|
+
}
|
|
1707
|
+
get exists() {
|
|
1708
|
+
return import_node_fs2.existsSync(this.contextPath) || import_node_fs2.existsSync(this.progressPath);
|
|
1709
|
+
}
|
|
1710
|
+
ensureDir(filePath) {
|
|
1711
|
+
const dir = import_node_path4.dirname(filePath);
|
|
1712
|
+
if (!import_node_fs2.existsSync(dir)) {
|
|
1713
|
+
import_node_fs2.mkdirSync(dir, { recursive: true });
|
|
1635
1714
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1638
1717
|
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
`;
|
|
1644
|
-
prompt += `- Version: ${projectConfig.version || "Unknown"}
|
|
1645
|
-
`;
|
|
1646
|
-
prompt += `- Created At: ${projectConfig.createdAt || "Unknown"}
|
|
1718
|
+
// src/worktree/worktree-manager.ts
|
|
1719
|
+
var import_node_child_process5 = require("node:child_process");
|
|
1720
|
+
var import_node_fs3 = require("node:fs");
|
|
1721
|
+
var import_node_path5 = require("node:path");
|
|
1647
1722
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1723
|
+
// src/worktree/worktree-config.ts
|
|
1724
|
+
var WORKTREE_ROOT_DIR = ".locus-worktrees";
|
|
1725
|
+
var WORKTREE_BRANCH_PREFIX = "agent";
|
|
1726
|
+
var DEFAULT_WORKTREE_CONFIG = {
|
|
1727
|
+
rootDir: WORKTREE_ROOT_DIR,
|
|
1728
|
+
branchPrefix: WORKTREE_BRANCH_PREFIX,
|
|
1729
|
+
cleanupPolicy: "retain-on-failure"
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
// src/worktree/worktree-manager.ts
|
|
1733
|
+
class WorktreeManager {
|
|
1734
|
+
config;
|
|
1735
|
+
projectPath;
|
|
1736
|
+
log;
|
|
1737
|
+
constructor(projectPath, config, log) {
|
|
1738
|
+
this.projectPath = import_node_path5.resolve(projectPath);
|
|
1739
|
+
this.config = { ...DEFAULT_WORKTREE_CONFIG, ...config };
|
|
1740
|
+
this.log = log ?? ((_msg) => {
|
|
1741
|
+
return;
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
get rootPath() {
|
|
1745
|
+
return import_node_path5.join(this.projectPath, this.config.rootDir);
|
|
1746
|
+
}
|
|
1747
|
+
buildBranchName(taskId, taskSlug) {
|
|
1748
|
+
const sanitized = taskSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
|
|
1749
|
+
return `${this.config.branchPrefix}/${taskId}-${sanitized}`;
|
|
1750
|
+
}
|
|
1751
|
+
create(options) {
|
|
1752
|
+
const branch = this.buildBranchName(options.taskId, options.taskSlug);
|
|
1753
|
+
const worktreeDir = `${options.agentId}-${options.taskId}`;
|
|
1754
|
+
const worktreePath = import_node_path5.join(this.rootPath, worktreeDir);
|
|
1755
|
+
this.ensureDirectory(this.rootPath, "Worktree root");
|
|
1756
|
+
const baseBranch = options.baseBranch ?? this.config.baseBranch ?? this.getCurrentBranch();
|
|
1757
|
+
this.log(`Creating worktree: ${worktreeDir} (branch: ${branch}, base: ${baseBranch})`, "info");
|
|
1758
|
+
if (import_node_fs3.existsSync(worktreePath)) {
|
|
1759
|
+
this.log(`Removing stale worktree directory: ${worktreePath}`, "warn");
|
|
1652
1760
|
try {
|
|
1653
|
-
|
|
1761
|
+
this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
|
|
1654
1762
|
} catch {
|
|
1655
|
-
|
|
1763
|
+
import_node_fs3.rmSync(worktreePath, { recursive: true, force: true });
|
|
1764
|
+
this.git("worktree prune", this.projectPath);
|
|
1656
1765
|
}
|
|
1657
1766
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1767
|
+
if (this.branchExists(branch)) {
|
|
1768
|
+
this.log(`Deleting existing branch: ${branch}`, "warn");
|
|
1769
|
+
const branchWorktrees = this.list().filter((wt) => wt.branch === branch);
|
|
1770
|
+
for (const wt of branchWorktrees) {
|
|
1771
|
+
const worktreePath2 = import_node_path5.resolve(wt.path);
|
|
1772
|
+
if (wt.isMain || !this.isManagedWorktreePath(worktreePath2)) {
|
|
1773
|
+
throw new Error(`Branch "${branch}" is checked out at "${worktreePath2}". Remove or detach that worktree before retrying.`);
|
|
1774
|
+
}
|
|
1775
|
+
this.log(`Removing existing worktree for branch: ${branch} (${worktreePath2})`, "warn");
|
|
1776
|
+
this.remove(worktreePath2, false);
|
|
1777
|
+
}
|
|
1661
1778
|
try {
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
${
|
|
1779
|
+
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
1780
|
+
} catch {
|
|
1781
|
+
this.git("worktree prune", this.projectPath);
|
|
1782
|
+
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
const addWorktree = () => this.git(`worktree add "${worktreePath}" -b "${branch}" "${baseBranch}"`, this.projectPath);
|
|
1786
|
+
try {
|
|
1787
|
+
addWorktree();
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
if (!this.isMissingDirectoryError(error)) {
|
|
1790
|
+
throw error;
|
|
1791
|
+
}
|
|
1792
|
+
this.log(`Worktree creation failed due to missing directories. Retrying after cleanup: ${worktreePath}`, "warn");
|
|
1793
|
+
this.cleanupFailedWorktree(worktreePath, branch);
|
|
1794
|
+
this.ensureDirectory(this.rootPath, "Worktree root");
|
|
1795
|
+
addWorktree();
|
|
1796
|
+
}
|
|
1797
|
+
this.log(`Worktree created at ${worktreePath}`, "success");
|
|
1798
|
+
return { worktreePath, branch, baseBranch };
|
|
1799
|
+
}
|
|
1800
|
+
list() {
|
|
1801
|
+
const output = this.git("worktree list --porcelain", this.projectPath);
|
|
1802
|
+
const worktrees = [];
|
|
1803
|
+
const blocks = output.trim().split(`
|
|
1666
1804
|
|
|
1667
|
-
|
|
1668
|
-
|
|
1805
|
+
`);
|
|
1806
|
+
for (const block of blocks) {
|
|
1807
|
+
if (!block.trim())
|
|
1808
|
+
continue;
|
|
1809
|
+
const lines = block.trim().split(`
|
|
1810
|
+
`);
|
|
1811
|
+
let path = "";
|
|
1812
|
+
let head = "";
|
|
1813
|
+
let branch = "";
|
|
1814
|
+
let isMain = false;
|
|
1815
|
+
let isPrunable = false;
|
|
1816
|
+
for (const line of lines) {
|
|
1817
|
+
if (line.startsWith("worktree ")) {
|
|
1818
|
+
path = line.slice("worktree ".length);
|
|
1819
|
+
} else if (line.startsWith("HEAD ")) {
|
|
1820
|
+
head = line.slice("HEAD ".length);
|
|
1821
|
+
} else if (line.startsWith("branch ")) {
|
|
1822
|
+
branch = line.slice("branch ".length).replace("refs/heads/", "");
|
|
1823
|
+
} else if (line === "bare" || path === this.projectPath) {
|
|
1824
|
+
isMain = true;
|
|
1825
|
+
} else if (line === "prunable") {
|
|
1826
|
+
isPrunable = true;
|
|
1827
|
+
} else if (line === "detached") {
|
|
1828
|
+
branch = "(detached)";
|
|
1669
1829
|
}
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1830
|
+
}
|
|
1831
|
+
if (import_node_path5.resolve(path) === this.projectPath) {
|
|
1832
|
+
isMain = true;
|
|
1833
|
+
}
|
|
1834
|
+
if (path) {
|
|
1835
|
+
worktrees.push({ path, branch, head, isMain, isPrunable });
|
|
1672
1836
|
}
|
|
1673
1837
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1838
|
+
return worktrees;
|
|
1839
|
+
}
|
|
1840
|
+
listAgentWorktrees() {
|
|
1841
|
+
return this.list().filter((wt) => !wt.isMain);
|
|
1842
|
+
}
|
|
1843
|
+
remove(worktreePath, deleteBranch = true) {
|
|
1844
|
+
const absolutePath = import_node_path5.resolve(worktreePath);
|
|
1845
|
+
const worktrees = this.list();
|
|
1846
|
+
const worktree = worktrees.find((wt) => import_node_path5.resolve(wt.path) === absolutePath);
|
|
1847
|
+
const branchToDelete = worktree?.branch;
|
|
1848
|
+
this.log(`Removing worktree: ${absolutePath}`, "info");
|
|
1849
|
+
try {
|
|
1850
|
+
this.git(`worktree remove "${absolutePath}" --force`, this.projectPath);
|
|
1851
|
+
} catch {
|
|
1852
|
+
if (import_node_fs3.existsSync(absolutePath)) {
|
|
1853
|
+
import_node_fs3.rmSync(absolutePath, { recursive: true, force: true });
|
|
1854
|
+
}
|
|
1855
|
+
this.git("worktree prune", this.projectPath);
|
|
1856
|
+
}
|
|
1857
|
+
if (deleteBranch && branchToDelete && !branchToDelete.startsWith("(")) {
|
|
1858
|
+
try {
|
|
1859
|
+
this.git(`branch -D "${branchToDelete}"`, this.projectPath);
|
|
1860
|
+
this.log(`Deleted branch: ${branchToDelete}`, "success");
|
|
1861
|
+
} catch {
|
|
1862
|
+
this.log(`Could not delete branch: ${branchToDelete} (may already be deleted)`, "warn");
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
this.log("Worktree removed", "success");
|
|
1866
|
+
}
|
|
1867
|
+
prune() {
|
|
1868
|
+
const before = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
1869
|
+
this.git("worktree prune", this.projectPath);
|
|
1870
|
+
const after = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
1871
|
+
const pruned = before - after;
|
|
1872
|
+
if (pruned > 0) {
|
|
1873
|
+
this.log(`Pruned ${pruned} stale worktree(s)`, "success");
|
|
1874
|
+
}
|
|
1875
|
+
return pruned;
|
|
1876
|
+
}
|
|
1877
|
+
removeAll() {
|
|
1878
|
+
const agentWorktrees = this.listAgentWorktrees();
|
|
1879
|
+
let removed = 0;
|
|
1880
|
+
for (const wt of agentWorktrees) {
|
|
1881
|
+
try {
|
|
1882
|
+
this.remove(wt.path, true);
|
|
1883
|
+
removed++;
|
|
1884
|
+
} catch {
|
|
1885
|
+
this.log(`Failed to remove worktree: ${wt.path}`, "warn");
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
if (import_node_fs3.existsSync(this.rootPath)) {
|
|
1889
|
+
try {
|
|
1890
|
+
import_node_fs3.rmSync(this.rootPath, { recursive: true, force: true });
|
|
1891
|
+
} catch {}
|
|
1892
|
+
}
|
|
1893
|
+
return removed;
|
|
1894
|
+
}
|
|
1895
|
+
hasChanges(worktreePath) {
|
|
1896
|
+
const status = this.git("status --porcelain", worktreePath).trim();
|
|
1897
|
+
return status.length > 0;
|
|
1898
|
+
}
|
|
1899
|
+
commitChanges(worktreePath, message) {
|
|
1900
|
+
if (!this.hasChanges(worktreePath)) {
|
|
1901
|
+
this.log("No changes to commit", "info");
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
this.git("add -A", worktreePath);
|
|
1905
|
+
this.gitExec(["commit", "-m", message], worktreePath);
|
|
1906
|
+
const hash = this.git("rev-parse HEAD", worktreePath).trim();
|
|
1907
|
+
this.log(`Committed: ${hash.slice(0, 8)}`, "success");
|
|
1908
|
+
return hash;
|
|
1909
|
+
}
|
|
1910
|
+
pushBranch(worktreePath, remote = "origin") {
|
|
1911
|
+
const branch = this.getBranch(worktreePath);
|
|
1912
|
+
this.log(`Pushing branch ${branch} to ${remote}`, "info");
|
|
1913
|
+
try {
|
|
1914
|
+
this.gitExec(["push", "-u", remote, branch], worktreePath);
|
|
1915
|
+
this.log(`Pushed ${branch} to ${remote}`, "success");
|
|
1916
|
+
return branch;
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
if (!this.isNonFastForwardPushError(error)) {
|
|
1919
|
+
throw error;
|
|
1920
|
+
}
|
|
1921
|
+
this.log(`Push rejected for ${branch} (non-fast-forward). Retrying with --force-with-lease.`, "warn");
|
|
1922
|
+
try {
|
|
1923
|
+
this.gitExec(["fetch", remote, branch], worktreePath);
|
|
1924
|
+
} catch {}
|
|
1925
|
+
this.gitExec(["push", "--force-with-lease", "-u", remote, branch], worktreePath);
|
|
1926
|
+
this.log(`Pushed ${branch} to ${remote} with --force-with-lease`, "success");
|
|
1927
|
+
}
|
|
1928
|
+
return branch;
|
|
1929
|
+
}
|
|
1930
|
+
getBranch(worktreePath) {
|
|
1931
|
+
return this.git("rev-parse --abbrev-ref HEAD", worktreePath).trim();
|
|
1932
|
+
}
|
|
1933
|
+
hasWorktreeForTask(taskId) {
|
|
1934
|
+
return this.listAgentWorktrees().some((wt) => wt.branch.includes(taskId) || wt.path.includes(taskId));
|
|
1935
|
+
}
|
|
1936
|
+
branchExists(branchName) {
|
|
1937
|
+
try {
|
|
1938
|
+
this.git(`rev-parse --verify "refs/heads/${branchName}"`, this.projectPath);
|
|
1939
|
+
return true;
|
|
1940
|
+
} catch {
|
|
1941
|
+
return false;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
getCurrentBranch() {
|
|
1945
|
+
return this.git("rev-parse --abbrev-ref HEAD", this.projectPath).trim();
|
|
1946
|
+
}
|
|
1947
|
+
isManagedWorktreePath(worktreePath) {
|
|
1948
|
+
const rootPath = import_node_path5.resolve(this.rootPath);
|
|
1949
|
+
const candidate = import_node_path5.resolve(worktreePath);
|
|
1950
|
+
const rootWithSep = rootPath.endsWith(import_node_path5.sep) ? rootPath : `${rootPath}${import_node_path5.sep}`;
|
|
1951
|
+
return candidate.startsWith(rootWithSep);
|
|
1952
|
+
}
|
|
1953
|
+
ensureDirectory(dirPath, label) {
|
|
1954
|
+
if (import_node_fs3.existsSync(dirPath)) {
|
|
1955
|
+
if (!import_node_fs3.statSync(dirPath).isDirectory()) {
|
|
1956
|
+
throw new Error(`${label} exists but is not a directory: ${dirPath}`);
|
|
1957
|
+
}
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
import_node_fs3.mkdirSync(dirPath, { recursive: true });
|
|
1961
|
+
}
|
|
1962
|
+
isMissingDirectoryError(error) {
|
|
1963
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1964
|
+
return message.includes("cannot create directory") || message.includes("No such file or directory");
|
|
1965
|
+
}
|
|
1966
|
+
cleanupFailedWorktree(worktreePath, branch) {
|
|
1967
|
+
try {
|
|
1968
|
+
this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
|
|
1969
|
+
} catch {}
|
|
1970
|
+
if (import_node_fs3.existsSync(worktreePath)) {
|
|
1971
|
+
import_node_fs3.rmSync(worktreePath, { recursive: true, force: true });
|
|
1972
|
+
}
|
|
1973
|
+
try {
|
|
1974
|
+
this.git("worktree prune", this.projectPath);
|
|
1975
|
+
} catch {}
|
|
1976
|
+
if (this.branchExists(branch)) {
|
|
1977
|
+
try {
|
|
1978
|
+
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
1979
|
+
} catch {}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
isNonFastForwardPushError(error) {
|
|
1983
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1984
|
+
return message.includes("non-fast-forward") || message.includes("[rejected]") || message.includes("fetch first");
|
|
1985
|
+
}
|
|
1986
|
+
git(args, cwd) {
|
|
1987
|
+
return import_node_child_process5.execSync(`git ${args}`, {
|
|
1988
|
+
cwd,
|
|
1989
|
+
encoding: "utf-8",
|
|
1990
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
gitExec(args, cwd) {
|
|
1994
|
+
return import_node_child_process5.execFileSync("git", args, {
|
|
1995
|
+
cwd,
|
|
1996
|
+
encoding: "utf-8",
|
|
1997
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// src/core/prompt-builder.ts
|
|
2003
|
+
var import_node_fs4 = require("node:fs");
|
|
2004
|
+
var import_node_path6 = require("node:path");
|
|
2005
|
+
var import_shared2 = require("@locusai/shared");
|
|
2006
|
+
class PromptBuilder {
|
|
2007
|
+
projectPath;
|
|
2008
|
+
constructor(projectPath) {
|
|
2009
|
+
this.projectPath = projectPath;
|
|
2010
|
+
}
|
|
2011
|
+
async build(task, options = {}) {
|
|
2012
|
+
let prompt = `# Task: ${task.title}
|
|
2013
|
+
|
|
2014
|
+
`;
|
|
2015
|
+
const roleText = this.roleToText(task.assigneeRole);
|
|
2016
|
+
if (roleText) {
|
|
2017
|
+
prompt += `## Role
|
|
2018
|
+
You are acting as a ${roleText}.
|
|
2019
|
+
|
|
2020
|
+
`;
|
|
2021
|
+
}
|
|
2022
|
+
prompt += `## Description
|
|
2023
|
+
${task.description || "No description provided."}
|
|
2024
|
+
|
|
2025
|
+
`;
|
|
2026
|
+
const projectConfig = this.getProjectConfig();
|
|
2027
|
+
if (projectConfig) {
|
|
2028
|
+
prompt += `## Project Metadata
|
|
2029
|
+
`;
|
|
2030
|
+
prompt += `- Version: ${projectConfig.version || "Unknown"}
|
|
2031
|
+
`;
|
|
2032
|
+
prompt += `- Created At: ${projectConfig.createdAt || "Unknown"}
|
|
2033
|
+
|
|
2034
|
+
`;
|
|
2035
|
+
}
|
|
2036
|
+
let serverContext = null;
|
|
2037
|
+
if (options.taskContext) {
|
|
2038
|
+
try {
|
|
2039
|
+
serverContext = JSON.parse(options.taskContext);
|
|
2040
|
+
} catch {
|
|
2041
|
+
serverContext = { context: options.taskContext };
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
const contextPath = getLocusPath(this.projectPath, "contextFile");
|
|
2045
|
+
let hasLocalContext = false;
|
|
2046
|
+
if (import_node_fs4.existsSync(contextPath)) {
|
|
2047
|
+
try {
|
|
2048
|
+
const context = import_node_fs4.readFileSync(contextPath, "utf-8");
|
|
2049
|
+
if (context.trim().length > 20) {
|
|
2050
|
+
prompt += `## Project Context (Local)
|
|
2051
|
+
${context}
|
|
2052
|
+
|
|
2053
|
+
`;
|
|
2054
|
+
hasLocalContext = true;
|
|
2055
|
+
}
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
console.warn(`Warning: Could not read context file: ${err}`);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
if (!hasLocalContext) {
|
|
2061
|
+
const fallback = this.getFallbackContext();
|
|
2062
|
+
if (fallback) {
|
|
2063
|
+
prompt += `## Project Context (README Fallback)
|
|
1678
2064
|
${fallback}
|
|
1679
2065
|
|
|
1680
2066
|
`;
|
|
@@ -1683,11 +2069,12 @@ ${fallback}
|
|
|
1683
2069
|
if (serverContext) {
|
|
1684
2070
|
prompt += `## Project Context (Server)
|
|
1685
2071
|
`;
|
|
1686
|
-
|
|
1687
|
-
|
|
2072
|
+
const project = serverContext.project;
|
|
2073
|
+
if (project) {
|
|
2074
|
+
prompt += `- Project: ${project.name || "Unknown"}
|
|
1688
2075
|
`;
|
|
1689
|
-
if (!hasLocalContext &&
|
|
1690
|
-
prompt += `- Tech Stack: ${
|
|
2076
|
+
if (!hasLocalContext && project.techStack?.length) {
|
|
2077
|
+
prompt += `- Tech Stack: ${project.techStack.join(", ")}
|
|
1691
2078
|
`;
|
|
1692
2079
|
}
|
|
1693
2080
|
}
|
|
@@ -1700,12 +2087,11 @@ ${serverContext.context}
|
|
|
1700
2087
|
`;
|
|
1701
2088
|
}
|
|
1702
2089
|
prompt += this.getProjectStructure();
|
|
1703
|
-
prompt += this.getSkillsInfo();
|
|
1704
2090
|
prompt += `## Project Knowledge Base
|
|
1705
2091
|
`;
|
|
1706
2092
|
prompt += `You have access to the following documentation directories for context:
|
|
1707
2093
|
`;
|
|
1708
|
-
prompt += `- Artifacts: \`.locus/artifacts\`
|
|
2094
|
+
prompt += `- Artifacts: \`.locus/artifacts\`
|
|
1709
2095
|
`;
|
|
1710
2096
|
prompt += `- Documents: \`.locus/documents\`
|
|
1711
2097
|
`;
|
|
@@ -1763,11 +2149,9 @@ ${comment.text}
|
|
|
1763
2149
|
}
|
|
1764
2150
|
}
|
|
1765
2151
|
prompt += `## Instructions
|
|
1766
|
-
1. Complete this task.
|
|
2152
|
+
1. Complete this task.
|
|
1767
2153
|
2. **Artifact Management**: If you create any high-level documentation (PRDs, technical drafts, architecture docs), you MUST save them in \`.locus/artifacts/\`. Do NOT create them in the root directory.
|
|
1768
|
-
3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...)
|
|
1769
|
-
4. When finished successfully, output: <promise>COMPLETE</promise>
|
|
1770
|
-
`;
|
|
2154
|
+
3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
|
|
1771
2155
|
return prompt;
|
|
1772
2156
|
}
|
|
1773
2157
|
async buildGenericPrompt(query) {
|
|
@@ -1814,7 +2198,6 @@ ${fallback}
|
|
|
1814
2198
|
}
|
|
1815
2199
|
}
|
|
1816
2200
|
prompt += this.getProjectStructure();
|
|
1817
|
-
prompt += this.getSkillsInfo();
|
|
1818
2201
|
prompt += `## Project Knowledge Base
|
|
1819
2202
|
`;
|
|
1820
2203
|
prompt += `You have access to the following documentation directories for context:
|
|
@@ -1835,9 +2218,7 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
1835
2218
|
}
|
|
1836
2219
|
prompt += `## Instructions
|
|
1837
2220
|
1. Execute the prompt based on the provided project context.
|
|
1838
|
-
2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...)
|
|
1839
|
-
3. When finished successfully, output: <promise>COMPLETE</promise>
|
|
1840
|
-
`;
|
|
2221
|
+
2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
|
|
1841
2222
|
return prompt;
|
|
1842
2223
|
}
|
|
1843
2224
|
getProjectConfig() {
|
|
@@ -1887,134 +2268,981 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
1887
2268
|
structure += `- \`${folder}/\`
|
|
1888
2269
|
`;
|
|
1889
2270
|
}
|
|
1890
|
-
return `${structure}
|
|
1891
|
-
`;
|
|
1892
|
-
} catch {
|
|
1893
|
-
return "";
|
|
2271
|
+
return `${structure}
|
|
2272
|
+
`;
|
|
2273
|
+
} catch {
|
|
2274
|
+
return "";
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
roleToText(role) {
|
|
2278
|
+
if (!role) {
|
|
2279
|
+
return null;
|
|
2280
|
+
}
|
|
2281
|
+
switch (role) {
|
|
2282
|
+
case import_shared2.AssigneeRole.BACKEND:
|
|
2283
|
+
return "Backend Engineer";
|
|
2284
|
+
case import_shared2.AssigneeRole.FRONTEND:
|
|
2285
|
+
return "Frontend Engineer";
|
|
2286
|
+
case import_shared2.AssigneeRole.PM:
|
|
2287
|
+
return "Product Manager";
|
|
2288
|
+
case import_shared2.AssigneeRole.QA:
|
|
2289
|
+
return "QA Engineer";
|
|
2290
|
+
case import_shared2.AssigneeRole.DESIGN:
|
|
2291
|
+
return "Product Designer";
|
|
2292
|
+
default:
|
|
2293
|
+
return "engineer";
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// src/agent/task-executor.ts
|
|
2299
|
+
class TaskExecutor {
|
|
2300
|
+
deps;
|
|
2301
|
+
promptBuilder;
|
|
2302
|
+
constructor(deps) {
|
|
2303
|
+
this.deps = deps;
|
|
2304
|
+
this.promptBuilder = new PromptBuilder(deps.projectPath);
|
|
2305
|
+
}
|
|
2306
|
+
async execute(task) {
|
|
2307
|
+
this.deps.log(`Executing: ${task.title}`, "info");
|
|
2308
|
+
const basePrompt = await this.promptBuilder.build(task);
|
|
2309
|
+
try {
|
|
2310
|
+
this.deps.log("Starting Execution...", "info");
|
|
2311
|
+
await this.deps.aiRunner.run(basePrompt);
|
|
2312
|
+
return {
|
|
2313
|
+
success: true,
|
|
2314
|
+
summary: "Task completed by the agent"
|
|
2315
|
+
};
|
|
2316
|
+
} catch (error) {
|
|
2317
|
+
return { success: false, summary: `Error: ${error}` };
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
// src/agent/worker.ts
|
|
2323
|
+
function resolveProvider(value) {
|
|
2324
|
+
if (!value || value.startsWith("--")) {
|
|
2325
|
+
console.warn("Warning: --provider requires a value. Falling back to 'claude'.");
|
|
2326
|
+
return PROVIDER.CLAUDE;
|
|
2327
|
+
}
|
|
2328
|
+
if (value === PROVIDER.CLAUDE || value === PROVIDER.CODEX)
|
|
2329
|
+
return value;
|
|
2330
|
+
console.warn(`Warning: invalid --provider value '${value}'. Falling back to 'claude'.`);
|
|
2331
|
+
return PROVIDER.CLAUDE;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
class AgentWorker {
|
|
2335
|
+
config;
|
|
2336
|
+
client;
|
|
2337
|
+
aiRunner;
|
|
2338
|
+
taskExecutor;
|
|
2339
|
+
knowledgeBase;
|
|
2340
|
+
worktreeManager = null;
|
|
2341
|
+
prService = null;
|
|
2342
|
+
maxTasks = 50;
|
|
2343
|
+
tasksCompleted = 0;
|
|
2344
|
+
heartbeatInterval = null;
|
|
2345
|
+
currentTaskId = null;
|
|
2346
|
+
currentWorktreePath = null;
|
|
2347
|
+
postCleanupDelayMs = 5000;
|
|
2348
|
+
ghUsername = null;
|
|
2349
|
+
constructor(config) {
|
|
2350
|
+
this.config = config;
|
|
2351
|
+
const projectPath = config.projectPath || process.cwd();
|
|
2352
|
+
this.client = new LocusClient({
|
|
2353
|
+
baseUrl: config.apiBase,
|
|
2354
|
+
token: config.apiKey,
|
|
2355
|
+
retryOptions: {
|
|
2356
|
+
maxRetries: 3,
|
|
2357
|
+
initialDelay: 1000,
|
|
2358
|
+
maxDelay: 5000,
|
|
2359
|
+
factor: 2
|
|
2360
|
+
}
|
|
2361
|
+
});
|
|
2362
|
+
const log = this.log.bind(this);
|
|
2363
|
+
if (config.useWorktrees && !isGitAvailable()) {
|
|
2364
|
+
this.log("git is not installed — worktree isolation will not work", "error");
|
|
2365
|
+
config.useWorktrees = false;
|
|
2366
|
+
}
|
|
2367
|
+
if (config.autoPush && !isGhAvailable(projectPath)) {
|
|
2368
|
+
this.log("GitHub CLI (gh) not available or not authenticated. Branch push can continue, but automatic PR creation may fail until gh is configured. Install from https://cli.github.com/", "warn");
|
|
2369
|
+
}
|
|
2370
|
+
if (config.autoPush) {
|
|
2371
|
+
this.ghUsername = getGhUsername();
|
|
2372
|
+
if (this.ghUsername) {
|
|
2373
|
+
this.log(`GitHub user: ${this.ghUsername}`, "info");
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
const provider = config.provider ?? PROVIDER.CLAUDE;
|
|
2377
|
+
this.aiRunner = createAiRunner(provider, {
|
|
2378
|
+
projectPath,
|
|
2379
|
+
model: config.model,
|
|
2380
|
+
log
|
|
2381
|
+
});
|
|
2382
|
+
this.taskExecutor = new TaskExecutor({
|
|
2383
|
+
aiRunner: this.aiRunner,
|
|
2384
|
+
projectPath,
|
|
2385
|
+
log
|
|
2386
|
+
});
|
|
2387
|
+
this.knowledgeBase = new KnowledgeBase(projectPath);
|
|
2388
|
+
if (config.useWorktrees) {
|
|
2389
|
+
this.worktreeManager = new WorktreeManager(projectPath, {
|
|
2390
|
+
cleanupPolicy: "auto"
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
if (config.autoPush) {
|
|
2394
|
+
this.prService = new PrService(projectPath, log);
|
|
2395
|
+
}
|
|
2396
|
+
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
2397
|
+
this.log(`Using ${providerLabel} CLI for all phases`, "info");
|
|
2398
|
+
if (config.useWorktrees) {
|
|
2399
|
+
this.log("Per-task worktree isolation enabled", "info");
|
|
2400
|
+
if (config.autoPush) {
|
|
2401
|
+
this.log("Auto-push enabled: branches will be pushed to remote", "info");
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
log(message, level = "info") {
|
|
2406
|
+
const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
2407
|
+
const colorFn = {
|
|
2408
|
+
info: c.cyan,
|
|
2409
|
+
success: c.green,
|
|
2410
|
+
warn: c.yellow,
|
|
2411
|
+
error: c.red
|
|
2412
|
+
}[level];
|
|
2413
|
+
const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
|
|
2414
|
+
console.log(`${c.dim(`[${timestamp}]`)} ${c.bold(`[${this.config.agentId.slice(-8)}]`)} ${colorFn(`${prefix} ${message}`)}`);
|
|
2415
|
+
}
|
|
2416
|
+
async getActiveSprint() {
|
|
2417
|
+
try {
|
|
2418
|
+
if (this.config.sprintId) {
|
|
2419
|
+
return await this.client.sprints.getById(this.config.sprintId, this.config.workspaceId);
|
|
2420
|
+
}
|
|
2421
|
+
return await this.client.sprints.getActive(this.config.workspaceId);
|
|
2422
|
+
} catch (_error) {
|
|
2423
|
+
return null;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
async getNextTask() {
|
|
2427
|
+
const maxRetries = 10;
|
|
2428
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
2429
|
+
try {
|
|
2430
|
+
const task = await this.client.workspaces.dispatch(this.config.workspaceId, this.config.agentId, this.config.sprintId);
|
|
2431
|
+
return task;
|
|
2432
|
+
} catch (error) {
|
|
2433
|
+
const isAxiosError = error != null && typeof error === "object" && "response" in error && typeof error.response?.status === "number";
|
|
2434
|
+
const status = isAxiosError ? error.response.status : 0;
|
|
2435
|
+
if (status === 404) {
|
|
2436
|
+
this.log("No tasks available in the backlog.", "info");
|
|
2437
|
+
return null;
|
|
2438
|
+
}
|
|
2439
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2440
|
+
if (attempt < maxRetries) {
|
|
2441
|
+
this.log(`Nothing dispatched (attempt ${attempt}/${maxRetries}): ${msg}. Retrying in 30s...`, "warn");
|
|
2442
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
2443
|
+
} else {
|
|
2444
|
+
this.log(`Nothing dispatched after ${maxRetries} attempts: ${msg}`, "warn");
|
|
2445
|
+
return null;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
return null;
|
|
2450
|
+
}
|
|
2451
|
+
createTaskWorktree(task) {
|
|
2452
|
+
if (!this.worktreeManager) {
|
|
2453
|
+
return {
|
|
2454
|
+
worktreePath: null,
|
|
2455
|
+
baseBranch: null,
|
|
2456
|
+
executor: this.taskExecutor
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
const slug = task.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
2460
|
+
const result = this.worktreeManager.create({
|
|
2461
|
+
taskId: task.id,
|
|
2462
|
+
taskSlug: slug,
|
|
2463
|
+
agentId: this.config.agentId
|
|
2464
|
+
});
|
|
2465
|
+
this.log(`Worktree created: ${result.worktreePath} (${result.branch})`, "info");
|
|
2466
|
+
const log = this.log.bind(this);
|
|
2467
|
+
const provider = this.config.provider ?? PROVIDER.CLAUDE;
|
|
2468
|
+
const taskAiRunner = createAiRunner(provider, {
|
|
2469
|
+
projectPath: result.worktreePath,
|
|
2470
|
+
model: this.config.model,
|
|
2471
|
+
log
|
|
2472
|
+
});
|
|
2473
|
+
const taskExecutor = new TaskExecutor({
|
|
2474
|
+
aiRunner: taskAiRunner,
|
|
2475
|
+
projectPath: result.worktreePath,
|
|
2476
|
+
log
|
|
2477
|
+
});
|
|
2478
|
+
return {
|
|
2479
|
+
worktreePath: result.worktreePath,
|
|
2480
|
+
baseBranch: result.baseBranch,
|
|
2481
|
+
executor: taskExecutor
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
commitAndPushWorktree(worktreePath, task) {
|
|
2485
|
+
if (!this.worktreeManager) {
|
|
2486
|
+
return { branch: null, pushed: false, pushFailed: false };
|
|
2487
|
+
}
|
|
2488
|
+
try {
|
|
2489
|
+
const trailers = [
|
|
2490
|
+
`Task-ID: ${task.id}`,
|
|
2491
|
+
`Agent: ${this.config.agentId}`,
|
|
2492
|
+
"Co-authored-by: LocusAI <noreply@locusai.dev>"
|
|
2493
|
+
];
|
|
2494
|
+
if (this.ghUsername) {
|
|
2495
|
+
trailers.push(`Co-authored-by: ${this.ghUsername} <${this.ghUsername}@users.noreply.github.com>`);
|
|
2496
|
+
}
|
|
2497
|
+
const commitMessage = `feat(agent): ${task.title}
|
|
2498
|
+
|
|
2499
|
+
${trailers.join(`
|
|
2500
|
+
`)}`;
|
|
2501
|
+
const hash = this.worktreeManager.commitChanges(worktreePath, commitMessage);
|
|
2502
|
+
if (!hash) {
|
|
2503
|
+
this.log("No changes to commit for this task", "info");
|
|
2504
|
+
return {
|
|
2505
|
+
branch: null,
|
|
2506
|
+
pushed: false,
|
|
2507
|
+
pushFailed: false,
|
|
2508
|
+
noChanges: true,
|
|
2509
|
+
skipReason: "No changes were committed, so no branch was pushed."
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
const localBranch = this.worktreeManager.getBranch(worktreePath);
|
|
2513
|
+
if (this.config.autoPush) {
|
|
2514
|
+
try {
|
|
2515
|
+
return {
|
|
2516
|
+
branch: this.worktreeManager.pushBranch(worktreePath),
|
|
2517
|
+
pushed: true,
|
|
2518
|
+
pushFailed: false
|
|
2519
|
+
};
|
|
2520
|
+
} catch (err) {
|
|
2521
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2522
|
+
this.log(`Git push failed: ${errorMessage}`, "error");
|
|
2523
|
+
return {
|
|
2524
|
+
branch: localBranch,
|
|
2525
|
+
pushed: false,
|
|
2526
|
+
pushFailed: true,
|
|
2527
|
+
pushError: errorMessage
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
this.log("Auto-push disabled; skipping branch push", "info");
|
|
2532
|
+
return {
|
|
2533
|
+
branch: localBranch,
|
|
2534
|
+
pushed: false,
|
|
2535
|
+
pushFailed: false,
|
|
2536
|
+
skipReason: "Auto-push is disabled, so PR creation was skipped."
|
|
2537
|
+
};
|
|
2538
|
+
} catch (err) {
|
|
2539
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2540
|
+
this.log(`Git commit failed: ${errorMessage}`, "error");
|
|
2541
|
+
return { branch: null, pushed: false, pushFailed: false };
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
createPullRequest(task, branch, summary, baseBranch) {
|
|
2545
|
+
if (!this.prService) {
|
|
2546
|
+
const errorMessage = "PR service is not initialized. Enable auto-push to allow PR creation.";
|
|
2547
|
+
this.log(`PR creation skipped: ${errorMessage}`, "warn");
|
|
2548
|
+
return { url: null, error: errorMessage };
|
|
2549
|
+
}
|
|
2550
|
+
this.log(`Attempting PR creation from branch: ${branch}`, "info");
|
|
2551
|
+
try {
|
|
2552
|
+
const result = this.prService.createPr({
|
|
2553
|
+
task,
|
|
2554
|
+
branch,
|
|
2555
|
+
baseBranch,
|
|
2556
|
+
agentId: this.config.agentId,
|
|
2557
|
+
summary
|
|
2558
|
+
});
|
|
2559
|
+
return { url: result.url };
|
|
2560
|
+
} catch (err) {
|
|
2561
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2562
|
+
this.log(`PR creation failed: ${errorMessage}`, "error");
|
|
2563
|
+
return { url: null, error: errorMessage };
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
cleanupTaskWorktree(worktreePath, keepBranch) {
|
|
2567
|
+
if (!this.worktreeManager || !worktreePath)
|
|
2568
|
+
return;
|
|
2569
|
+
try {
|
|
2570
|
+
this.worktreeManager.remove(worktreePath, !keepBranch);
|
|
2571
|
+
this.log(keepBranch ? "Worktree cleaned up (branch preserved)" : "Worktree cleaned up", "info");
|
|
2572
|
+
} catch {
|
|
2573
|
+
this.log(`Could not clean up worktree: ${worktreePath}`, "warn");
|
|
2574
|
+
}
|
|
2575
|
+
this.currentWorktreePath = null;
|
|
2576
|
+
}
|
|
2577
|
+
async executeTask(task) {
|
|
2578
|
+
const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
|
|
2579
|
+
const { worktreePath, baseBranch, executor } = this.createTaskWorktree(fullTask);
|
|
2580
|
+
this.currentWorktreePath = worktreePath;
|
|
2581
|
+
let branchPushed = false;
|
|
2582
|
+
let keepBranch = false;
|
|
2583
|
+
let preserveWorktree = false;
|
|
2584
|
+
try {
|
|
2585
|
+
const result = await executor.execute(fullTask);
|
|
2586
|
+
let taskBranch = null;
|
|
2587
|
+
let prUrl = null;
|
|
2588
|
+
let prError = null;
|
|
2589
|
+
let noChanges = false;
|
|
2590
|
+
if (result.success && worktreePath) {
|
|
2591
|
+
const commitResult = this.commitAndPushWorktree(worktreePath, fullTask);
|
|
2592
|
+
taskBranch = commitResult.branch;
|
|
2593
|
+
branchPushed = commitResult.pushed;
|
|
2594
|
+
keepBranch = taskBranch !== null;
|
|
2595
|
+
noChanges = Boolean(commitResult.noChanges);
|
|
2596
|
+
if (commitResult.pushFailed) {
|
|
2597
|
+
preserveWorktree = true;
|
|
2598
|
+
prError = commitResult.pushError ?? "Git push failed before PR creation. Please retry manually.";
|
|
2599
|
+
this.log(`Preserving worktree after push failure: ${worktreePath}`, "warn");
|
|
2600
|
+
}
|
|
2601
|
+
if (branchPushed && taskBranch) {
|
|
2602
|
+
const prResult = this.createPullRequest(fullTask, taskBranch, result.summary, baseBranch ?? undefined);
|
|
2603
|
+
prUrl = prResult.url;
|
|
2604
|
+
prError = prResult.error ?? null;
|
|
2605
|
+
if (!prUrl) {
|
|
2606
|
+
preserveWorktree = true;
|
|
2607
|
+
this.log(`Preserving worktree for manual follow-up: ${worktreePath}`, "warn");
|
|
2608
|
+
}
|
|
2609
|
+
} else if (commitResult.skipReason) {
|
|
2610
|
+
this.log(`Skipping PR creation: ${commitResult.skipReason}`, "info");
|
|
2611
|
+
}
|
|
2612
|
+
} else if (result.success && !worktreePath) {
|
|
2613
|
+
this.log("Skipping commit/push/PR flow because no task worktree is active.", "warn");
|
|
2614
|
+
}
|
|
2615
|
+
return {
|
|
2616
|
+
...result,
|
|
2617
|
+
branch: taskBranch ?? undefined,
|
|
2618
|
+
prUrl: prUrl ?? undefined,
|
|
2619
|
+
prError: prError ?? undefined,
|
|
2620
|
+
noChanges: noChanges || undefined
|
|
2621
|
+
};
|
|
2622
|
+
} finally {
|
|
2623
|
+
if (preserveWorktree || keepBranch) {
|
|
2624
|
+
this.currentWorktreePath = null;
|
|
2625
|
+
} else {
|
|
2626
|
+
this.cleanupTaskWorktree(worktreePath, keepBranch);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
updateProgress(task, success) {
|
|
2631
|
+
try {
|
|
2632
|
+
if (success) {
|
|
2633
|
+
this.knowledgeBase.updateProgress({
|
|
2634
|
+
type: "task_completed",
|
|
2635
|
+
title: task.title,
|
|
2636
|
+
details: `Agent: ${this.config.agentId.slice(-8)}`
|
|
2637
|
+
});
|
|
2638
|
+
this.log(`Updated progress.md: ${task.title}`, "info");
|
|
2639
|
+
}
|
|
2640
|
+
} catch (err) {
|
|
2641
|
+
this.log(`Failed to update progress: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
startHeartbeat() {
|
|
2645
|
+
this.sendHeartbeat();
|
|
2646
|
+
this.heartbeatInterval = setInterval(() => {
|
|
2647
|
+
this.sendHeartbeat();
|
|
2648
|
+
}, 60000);
|
|
2649
|
+
}
|
|
2650
|
+
stopHeartbeat() {
|
|
2651
|
+
if (this.heartbeatInterval) {
|
|
2652
|
+
clearInterval(this.heartbeatInterval);
|
|
2653
|
+
this.heartbeatInterval = null;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
sendHeartbeat() {
|
|
2657
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, this.currentTaskId, this.currentTaskId ? "WORKING" : "IDLE").catch((err) => {
|
|
2658
|
+
this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
async delayAfterCleanup() {
|
|
2662
|
+
if (!this.config.useWorktrees || this.postCleanupDelayMs <= 0)
|
|
2663
|
+
return;
|
|
2664
|
+
this.log(`Waiting ${Math.floor(this.postCleanupDelayMs / 1000)}s after worktree cleanup before next dispatch`, "info");
|
|
2665
|
+
await new Promise((resolve3) => setTimeout(resolve3, this.postCleanupDelayMs));
|
|
2666
|
+
}
|
|
2667
|
+
async run() {
|
|
2668
|
+
this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
|
|
2669
|
+
const handleShutdown = () => {
|
|
2670
|
+
this.log("Received shutdown signal. Aborting...", "warn");
|
|
2671
|
+
this.aiRunner.abort();
|
|
2672
|
+
this.stopHeartbeat();
|
|
2673
|
+
this.cleanupTaskWorktree(this.currentWorktreePath, false);
|
|
2674
|
+
process.exit(1);
|
|
2675
|
+
};
|
|
2676
|
+
process.on("SIGTERM", handleShutdown);
|
|
2677
|
+
process.on("SIGINT", handleShutdown);
|
|
2678
|
+
this.startHeartbeat();
|
|
2679
|
+
const sprint = await this.getActiveSprint();
|
|
2680
|
+
if (sprint) {
|
|
2681
|
+
this.log(`Active sprint found: ${sprint.name}`, "info");
|
|
2682
|
+
} else {
|
|
2683
|
+
this.log("No active sprint found.", "warn");
|
|
2684
|
+
}
|
|
2685
|
+
while (this.tasksCompleted < this.maxTasks) {
|
|
2686
|
+
const task = await this.getNextTask();
|
|
2687
|
+
if (!task) {
|
|
2688
|
+
this.log("No more tasks to process. Exiting.", "info");
|
|
2689
|
+
break;
|
|
2690
|
+
}
|
|
2691
|
+
this.log(`Claimed: ${task.title}`, "success");
|
|
2692
|
+
this.currentTaskId = task.id;
|
|
2693
|
+
this.sendHeartbeat();
|
|
2694
|
+
const result = await this.executeTask(task);
|
|
2695
|
+
if (result.success) {
|
|
2696
|
+
if (result.noChanges) {
|
|
2697
|
+
this.log(`Blocked: ${task.title} - execution produced no file changes`, "warn");
|
|
2698
|
+
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
2699
|
+
status: import_shared3.TaskStatus.BLOCKED,
|
|
2700
|
+
assignedTo: null
|
|
2701
|
+
});
|
|
2702
|
+
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
2703
|
+
author: this.config.agentId,
|
|
2704
|
+
text: `⚠️ Agent execution finished with no file changes, so no commit/branch/PR was created.
|
|
2705
|
+
|
|
2706
|
+
${result.summary}`
|
|
2707
|
+
});
|
|
2708
|
+
} else {
|
|
2709
|
+
this.log(`Completed: ${task.title}`, "success");
|
|
2710
|
+
const updatePayload = {
|
|
2711
|
+
status: import_shared3.TaskStatus.IN_REVIEW
|
|
2712
|
+
};
|
|
2713
|
+
if (result.prUrl) {
|
|
2714
|
+
updatePayload.prUrl = result.prUrl;
|
|
2715
|
+
}
|
|
2716
|
+
await this.client.tasks.update(task.id, this.config.workspaceId, updatePayload);
|
|
2717
|
+
const branchInfo = result.branch ? `
|
|
2718
|
+
|
|
2719
|
+
Branch: \`${result.branch}\`` : "";
|
|
2720
|
+
const prInfo = result.prUrl ? `
|
|
2721
|
+
PR: ${result.prUrl}` : "";
|
|
2722
|
+
const prErrorInfo = result.prError ? `
|
|
2723
|
+
PR automation error: ${result.prError}` : "";
|
|
2724
|
+
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
2725
|
+
author: this.config.agentId,
|
|
2726
|
+
text: `✅ ${result.summary}${branchInfo}${prInfo}${prErrorInfo}`
|
|
2727
|
+
});
|
|
2728
|
+
this.tasksCompleted++;
|
|
2729
|
+
this.updateProgress(task, true);
|
|
2730
|
+
if (result.prUrl) {
|
|
2731
|
+
try {
|
|
2732
|
+
this.knowledgeBase.updateProgress({
|
|
2733
|
+
type: "pr_opened",
|
|
2734
|
+
title: task.title,
|
|
2735
|
+
details: `PR: ${result.prUrl}`
|
|
2736
|
+
});
|
|
2737
|
+
} catch {}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
} else {
|
|
2741
|
+
this.log(`Failed: ${task.title} - ${result.summary}`, "error");
|
|
2742
|
+
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
2743
|
+
status: import_shared3.TaskStatus.BACKLOG,
|
|
2744
|
+
assignedTo: null
|
|
2745
|
+
});
|
|
2746
|
+
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
2747
|
+
author: this.config.agentId,
|
|
2748
|
+
text: `❌ ${result.summary}`
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
this.currentTaskId = null;
|
|
2752
|
+
this.sendHeartbeat();
|
|
2753
|
+
await this.delayAfterCleanup();
|
|
2754
|
+
}
|
|
2755
|
+
this.currentTaskId = null;
|
|
2756
|
+
this.stopHeartbeat();
|
|
2757
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
|
|
2758
|
+
process.exit(0);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
var workerEntrypoint = process.argv[1]?.split(/[\\/]/).pop();
|
|
2762
|
+
if (workerEntrypoint === "worker.js" || workerEntrypoint === "worker.ts") {
|
|
2763
|
+
process.title = "locus-worker";
|
|
2764
|
+
const args = process.argv.slice(2);
|
|
2765
|
+
const config = {};
|
|
2766
|
+
for (let i = 0;i < args.length; i++) {
|
|
2767
|
+
const arg = args[i];
|
|
2768
|
+
if (arg === "--agent-id")
|
|
2769
|
+
config.agentId = args[++i];
|
|
2770
|
+
else if (arg === "--workspace-id")
|
|
2771
|
+
config.workspaceId = args[++i];
|
|
2772
|
+
else if (arg === "--sprint-id")
|
|
2773
|
+
config.sprintId = args[++i];
|
|
2774
|
+
else if (arg === "--api-url")
|
|
2775
|
+
config.apiBase = args[++i];
|
|
2776
|
+
else if (arg === "--api-key")
|
|
2777
|
+
config.apiKey = args[++i];
|
|
2778
|
+
else if (arg === "--project-path")
|
|
2779
|
+
config.projectPath = args[++i];
|
|
2780
|
+
else if (arg === "--main-project-path")
|
|
2781
|
+
config.mainProjectPath = args[++i];
|
|
2782
|
+
else if (arg === "--model")
|
|
2783
|
+
config.model = args[++i];
|
|
2784
|
+
else if (arg === "--use-worktrees")
|
|
2785
|
+
config.useWorktrees = true;
|
|
2786
|
+
else if (arg === "--auto-push")
|
|
2787
|
+
config.autoPush = true;
|
|
2788
|
+
else if (arg === "--provider") {
|
|
2789
|
+
const value = args[i + 1];
|
|
2790
|
+
if (value && !value.startsWith("--"))
|
|
2791
|
+
i++;
|
|
2792
|
+
config.provider = resolveProvider(value);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
if (!config.agentId || !config.workspaceId || !config.apiBase || !config.apiKey || !config.projectPath) {
|
|
2796
|
+
console.error("Missing required arguments");
|
|
2797
|
+
process.exit(1);
|
|
2798
|
+
}
|
|
2799
|
+
const worker = new AgentWorker(config);
|
|
2800
|
+
worker.run().catch((err) => {
|
|
2801
|
+
console.error("Fatal worker error:", err);
|
|
2802
|
+
process.exit(1);
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// src/index-node.ts
|
|
2807
|
+
var exports_index_node = {};
|
|
2808
|
+
__export(exports_index_node, {
|
|
2809
|
+
sprintPlanToMarkdown: () => sprintPlanToMarkdown,
|
|
2810
|
+
plannedTasksToCreatePayloads: () => plannedTasksToCreatePayloads,
|
|
2811
|
+
parseSprintPlanFromAI: () => parseSprintPlanFromAI,
|
|
2812
|
+
getRemoteUrl: () => getRemoteUrl,
|
|
2813
|
+
getLocusPath: () => getLocusPath,
|
|
2814
|
+
getDefaultBranch: () => getDefaultBranch,
|
|
2815
|
+
getCurrentBranch: () => getCurrentBranch,
|
|
2816
|
+
getAgentArtifactsPath: () => getAgentArtifactsPath,
|
|
2817
|
+
detectRemoteProvider: () => detectRemoteProvider,
|
|
2818
|
+
createAiRunner: () => createAiRunner,
|
|
2819
|
+
c: () => c,
|
|
2820
|
+
WorktreeManager: () => WorktreeManager,
|
|
2821
|
+
WorkspacesModule: () => WorkspacesModule,
|
|
2822
|
+
WORKTREE_ROOT_DIR: () => WORKTREE_ROOT_DIR,
|
|
2823
|
+
WORKTREE_BRANCH_PREFIX: () => WORKTREE_BRANCH_PREFIX,
|
|
2824
|
+
TasksModule: () => TasksModule,
|
|
2825
|
+
TaskExecutor: () => TaskExecutor,
|
|
2826
|
+
SprintsModule: () => SprintsModule,
|
|
2827
|
+
ReviewerWorker: () => ReviewerWorker,
|
|
2828
|
+
ReviewService: () => ReviewService,
|
|
2829
|
+
PromptBuilder: () => PromptBuilder,
|
|
2830
|
+
PrService: () => PrService,
|
|
2831
|
+
PlanningMeeting: () => PlanningMeeting,
|
|
2832
|
+
PlanManager: () => PlanManager,
|
|
2833
|
+
PROVIDER: () => PROVIDER,
|
|
2834
|
+
OrganizationsModule: () => OrganizationsModule,
|
|
2835
|
+
LocusEvent: () => LocusEvent,
|
|
2836
|
+
LocusEmitter: () => LocusEmitter,
|
|
2837
|
+
LocusClient: () => LocusClient,
|
|
2838
|
+
LOCUS_SCHEMA_BASE_URL: () => LOCUS_SCHEMA_BASE_URL,
|
|
2839
|
+
LOCUS_SCHEMAS: () => LOCUS_SCHEMAS,
|
|
2840
|
+
LOCUS_GITIGNORE_PATTERNS: () => LOCUS_GITIGNORE_PATTERNS,
|
|
2841
|
+
LOCUS_CONFIG: () => LOCUS_CONFIG,
|
|
2842
|
+
KnowledgeBase: () => KnowledgeBase,
|
|
2843
|
+
InvitationsModule: () => InvitationsModule,
|
|
2844
|
+
HistoryManager: () => HistoryManager,
|
|
2845
|
+
ExecSession: () => ExecSession,
|
|
2846
|
+
ExecEventType: () => ExecEventType,
|
|
2847
|
+
ExecEventEmitter: () => ExecEventEmitter,
|
|
2848
|
+
DocumentFetcher: () => DocumentFetcher,
|
|
2849
|
+
DocsModule: () => DocsModule,
|
|
2850
|
+
DEFAULT_WORKTREE_CONFIG: () => DEFAULT_WORKTREE_CONFIG,
|
|
2851
|
+
DEFAULT_MODEL: () => DEFAULT_MODEL,
|
|
2852
|
+
ContextTracker: () => ContextTracker,
|
|
2853
|
+
CodexRunner: () => CodexRunner,
|
|
2854
|
+
CodebaseIndexerService: () => CodebaseIndexerService,
|
|
2855
|
+
CodebaseIndexer: () => CodebaseIndexer,
|
|
2856
|
+
ClaudeRunner: () => ClaudeRunner,
|
|
2857
|
+
CiModule: () => CiModule,
|
|
2858
|
+
AuthModule: () => AuthModule,
|
|
2859
|
+
AgentWorker: () => AgentWorker,
|
|
2860
|
+
AgentOrchestrator: () => AgentOrchestrator
|
|
2861
|
+
});
|
|
2862
|
+
module.exports = __toCommonJS(exports_index_node);
|
|
2863
|
+
|
|
2864
|
+
// src/core/indexer.ts
|
|
2865
|
+
var import_node_crypto2 = require("node:crypto");
|
|
2866
|
+
var import_node_fs5 = require("node:fs");
|
|
2867
|
+
var import_node_path7 = require("node:path");
|
|
2868
|
+
var import_globby = require("globby");
|
|
2869
|
+
|
|
2870
|
+
class CodebaseIndexer {
|
|
2871
|
+
projectPath;
|
|
2872
|
+
indexPath;
|
|
2873
|
+
fullReindexRatioThreshold = 0.2;
|
|
2874
|
+
constructor(projectPath) {
|
|
2875
|
+
this.projectPath = projectPath;
|
|
2876
|
+
this.indexPath = import_node_path7.join(projectPath, ".locus", "codebase-index.json");
|
|
2877
|
+
}
|
|
2878
|
+
async index(onProgress, treeSummarizer, force = false) {
|
|
2879
|
+
if (!treeSummarizer) {
|
|
2880
|
+
throw new Error("A treeSummarizer is required for this indexing method.");
|
|
2881
|
+
}
|
|
2882
|
+
onProgress?.("Generating file tree...");
|
|
2883
|
+
const currentFiles = await this.getFileTree();
|
|
2884
|
+
const treeString = currentFiles.join(`
|
|
2885
|
+
`);
|
|
2886
|
+
const newTreeHash = this.hashTree(treeString);
|
|
2887
|
+
const existingIndex = this.loadIndex();
|
|
2888
|
+
const currentHashes = this.computeFileHashes(currentFiles);
|
|
2889
|
+
const existingHashes = existingIndex?.fileHashes;
|
|
2890
|
+
const hasExistingContent = existingIndex && (Object.keys(existingIndex.symbols).length > 0 || Object.keys(existingIndex.responsibilities).length > 0);
|
|
2891
|
+
const canIncremental = !force && existingIndex && existingHashes && hasExistingContent;
|
|
2892
|
+
if (canIncremental) {
|
|
2893
|
+
onProgress?.("Performing incremental update");
|
|
2894
|
+
const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
|
|
2895
|
+
const changedFiles = [...added, ...modified];
|
|
2896
|
+
const totalChanges = changedFiles.length + deleted.length;
|
|
2897
|
+
const existingFileCount = Object.keys(existingHashes).length;
|
|
2898
|
+
onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
|
|
2899
|
+
if (existingFileCount > 0) {
|
|
2900
|
+
const changeRatio = totalChanges / existingFileCount;
|
|
2901
|
+
if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
|
|
2902
|
+
onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
|
|
2903
|
+
const incrementalIndex = await treeSummarizer(changedFiles.join(`
|
|
2904
|
+
`));
|
|
2905
|
+
const updatedIndex = this.cloneIndex(existingIndex);
|
|
2906
|
+
this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
|
|
2907
|
+
return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
|
|
2908
|
+
}
|
|
2909
|
+
if (changedFiles.length === 0 && deleted.length > 0) {
|
|
2910
|
+
onProgress?.(`Removing ${deleted.length} deleted files from index`);
|
|
2911
|
+
const updatedIndex = this.cloneIndex(existingIndex);
|
|
2912
|
+
this.removeFilesFromIndex(updatedIndex, deleted);
|
|
2913
|
+
return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
|
|
2914
|
+
}
|
|
2915
|
+
if (changedFiles.length === 0 && deleted.length === 0) {
|
|
2916
|
+
onProgress?.("No actual file changes, updating hashes only");
|
|
2917
|
+
const updatedIndex = this.cloneIndex(existingIndex);
|
|
2918
|
+
return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
|
|
2919
|
+
}
|
|
2920
|
+
onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
onProgress?.("AI is analyzing codebase structure...");
|
|
2924
|
+
try {
|
|
2925
|
+
const index = await treeSummarizer(treeString);
|
|
2926
|
+
return this.applyIndexMetadata(index, currentHashes, newTreeHash);
|
|
2927
|
+
} catch (error) {
|
|
2928
|
+
throw new Error(`AI analysis failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
async getFileTree() {
|
|
2932
|
+
const gitmodulesPath = import_node_path7.join(this.projectPath, ".gitmodules");
|
|
2933
|
+
const submoduleIgnores = [];
|
|
2934
|
+
if (import_node_fs5.existsSync(gitmodulesPath)) {
|
|
2935
|
+
try {
|
|
2936
|
+
const content = import_node_fs5.readFileSync(gitmodulesPath, "utf-8");
|
|
2937
|
+
const lines = content.split(`
|
|
2938
|
+
`);
|
|
2939
|
+
for (const line of lines) {
|
|
2940
|
+
const match = line.match(/^\s*path\s*=\s*(.*)$/);
|
|
2941
|
+
const path = match?.[1]?.trim();
|
|
2942
|
+
if (path) {
|
|
2943
|
+
submoduleIgnores.push(`${path}/**`);
|
|
2944
|
+
submoduleIgnores.push(`**/${path}/**`);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
} catch {}
|
|
2948
|
+
}
|
|
2949
|
+
return import_globby.globby(["**/*"], {
|
|
2950
|
+
cwd: this.projectPath,
|
|
2951
|
+
gitignore: true,
|
|
2952
|
+
ignore: [
|
|
2953
|
+
...submoduleIgnores,
|
|
2954
|
+
"**/node_modules/**",
|
|
2955
|
+
"**/dist/**",
|
|
2956
|
+
"**/build/**",
|
|
2957
|
+
"**/target/**",
|
|
2958
|
+
"**/bin/**",
|
|
2959
|
+
"**/obj/**",
|
|
2960
|
+
"**/.next/**",
|
|
2961
|
+
"**/.svelte-kit/**",
|
|
2962
|
+
"**/.nuxt/**",
|
|
2963
|
+
"**/.cache/**",
|
|
2964
|
+
"**/out/**",
|
|
2965
|
+
"**/__tests__/**",
|
|
2966
|
+
"**/coverage/**",
|
|
2967
|
+
"**/*.test.*",
|
|
2968
|
+
"**/*.spec.*",
|
|
2969
|
+
"**/*.d.ts",
|
|
2970
|
+
"**/tsconfig.tsbuildinfo",
|
|
2971
|
+
"**/.locus/*.json",
|
|
2972
|
+
"**/.locus/*.md",
|
|
2973
|
+
"**/.locus/!(artifacts)/**",
|
|
2974
|
+
"**/.git/**",
|
|
2975
|
+
"**/.svn/**",
|
|
2976
|
+
"**/.hg/**",
|
|
2977
|
+
"**/.vscode/**",
|
|
2978
|
+
"**/.idea/**",
|
|
2979
|
+
"**/.DS_Store",
|
|
2980
|
+
"**/bun.lock",
|
|
2981
|
+
"**/package-lock.json",
|
|
2982
|
+
"**/yarn.lock",
|
|
2983
|
+
"**/pnpm-lock.yaml",
|
|
2984
|
+
"**/Cargo.lock",
|
|
2985
|
+
"**/go.sum",
|
|
2986
|
+
"**/poetry.lock",
|
|
2987
|
+
"**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
|
|
2988
|
+
]
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
loadIndex() {
|
|
2992
|
+
if (import_node_fs5.existsSync(this.indexPath)) {
|
|
2993
|
+
try {
|
|
2994
|
+
return JSON.parse(import_node_fs5.readFileSync(this.indexPath, "utf-8"));
|
|
2995
|
+
} catch {
|
|
2996
|
+
return null;
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
return null;
|
|
3000
|
+
}
|
|
3001
|
+
saveIndex(index) {
|
|
3002
|
+
const dir = import_node_path7.dirname(this.indexPath);
|
|
3003
|
+
if (!import_node_fs5.existsSync(dir)) {
|
|
3004
|
+
import_node_fs5.mkdirSync(dir, { recursive: true });
|
|
3005
|
+
}
|
|
3006
|
+
import_node_fs5.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
|
|
3007
|
+
}
|
|
3008
|
+
cloneIndex(index) {
|
|
3009
|
+
return JSON.parse(JSON.stringify(index));
|
|
3010
|
+
}
|
|
3011
|
+
applyIndexMetadata(index, fileHashes, treeHash) {
|
|
3012
|
+
index.lastIndexed = new Date().toISOString();
|
|
3013
|
+
index.treeHash = treeHash;
|
|
3014
|
+
index.fileHashes = fileHashes;
|
|
3015
|
+
return index;
|
|
3016
|
+
}
|
|
3017
|
+
hashTree(tree) {
|
|
3018
|
+
return import_node_crypto2.createHash("sha256").update(tree).digest("hex");
|
|
3019
|
+
}
|
|
3020
|
+
hashFile(filePath) {
|
|
3021
|
+
try {
|
|
3022
|
+
const content = import_node_fs5.readFileSync(import_node_path7.join(this.projectPath, filePath), "utf-8");
|
|
3023
|
+
return import_node_crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
3024
|
+
} catch {
|
|
3025
|
+
return null;
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
computeFileHashes(files) {
|
|
3029
|
+
const hashes = {};
|
|
3030
|
+
for (const file of files) {
|
|
3031
|
+
const hash = this.hashFile(file);
|
|
3032
|
+
if (hash !== null) {
|
|
3033
|
+
hashes[file] = hash;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
return hashes;
|
|
3037
|
+
}
|
|
3038
|
+
diffFiles(currentHashes, existingHashes) {
|
|
3039
|
+
const currentFiles = Object.keys(currentHashes);
|
|
3040
|
+
const existingFiles = Object.keys(existingHashes);
|
|
3041
|
+
const existingSet = new Set(existingFiles);
|
|
3042
|
+
const currentSet = new Set(currentFiles);
|
|
3043
|
+
const added = currentFiles.filter((f) => !existingSet.has(f));
|
|
3044
|
+
const deleted = existingFiles.filter((f) => !currentSet.has(f));
|
|
3045
|
+
const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
|
|
3046
|
+
return { added, deleted, modified };
|
|
3047
|
+
}
|
|
3048
|
+
removeFilesFromIndex(index, files) {
|
|
3049
|
+
const fileSet = new Set(files);
|
|
3050
|
+
for (const file of files) {
|
|
3051
|
+
delete index.responsibilities[file];
|
|
3052
|
+
}
|
|
3053
|
+
for (const [symbol, paths] of Object.entries(index.symbols)) {
|
|
3054
|
+
index.symbols[symbol] = paths.filter((p) => !fileSet.has(p));
|
|
3055
|
+
if (index.symbols[symbol].length === 0) {
|
|
3056
|
+
delete index.symbols[symbol];
|
|
3057
|
+
}
|
|
1894
3058
|
}
|
|
1895
3059
|
}
|
|
1896
|
-
|
|
1897
|
-
const
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
import_node_path6.join(globalHome, ".cursor/skills"),
|
|
1907
|
-
import_node_path6.join(globalHome, ".claude/skills"),
|
|
1908
|
-
import_node_path6.join(globalHome, ".codex/skills"),
|
|
1909
|
-
import_node_path6.join(globalHome, ".gemini/skills")
|
|
1910
|
-
];
|
|
1911
|
-
const allSkillNames = new Set;
|
|
1912
|
-
for (const relativePath of projectSkillsDirs) {
|
|
1913
|
-
const fullPath = import_node_path6.join(this.projectPath, relativePath);
|
|
1914
|
-
this.scanSkillsInDirectory(fullPath, allSkillNames);
|
|
1915
|
-
}
|
|
1916
|
-
for (const fullPath of globalSkillsDirs) {
|
|
1917
|
-
this.scanSkillsInDirectory(fullPath, allSkillNames);
|
|
3060
|
+
mergeIndex(existing, incremental, newHashes, newTreeHash) {
|
|
3061
|
+
const mergedSymbols = { ...existing.symbols };
|
|
3062
|
+
for (const [symbol, paths] of Object.entries(incremental.symbols)) {
|
|
3063
|
+
if (mergedSymbols[symbol]) {
|
|
3064
|
+
mergedSymbols[symbol] = [
|
|
3065
|
+
...new Set([...mergedSymbols[symbol], ...paths])
|
|
3066
|
+
];
|
|
3067
|
+
} else {
|
|
3068
|
+
mergedSymbols[symbol] = paths;
|
|
3069
|
+
}
|
|
1918
3070
|
}
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
3071
|
+
const merged = {
|
|
3072
|
+
symbols: mergedSymbols,
|
|
3073
|
+
responsibilities: {
|
|
3074
|
+
...existing.responsibilities,
|
|
3075
|
+
...incremental.responsibilities
|
|
3076
|
+
},
|
|
3077
|
+
lastIndexed: ""
|
|
3078
|
+
};
|
|
3079
|
+
return this.applyIndexMetadata(merged, newHashes, newTreeHash);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
1926
3082
|
|
|
1927
|
-
|
|
3083
|
+
// src/agent/codebase-indexer-service.ts
|
|
3084
|
+
class CodebaseIndexerService {
|
|
3085
|
+
deps;
|
|
3086
|
+
indexer;
|
|
3087
|
+
constructor(deps) {
|
|
3088
|
+
this.deps = deps;
|
|
3089
|
+
this.indexer = new CodebaseIndexer(deps.projectPath);
|
|
1928
3090
|
}
|
|
1929
|
-
|
|
1930
|
-
if (!import_node_fs4.existsSync(dirPath))
|
|
1931
|
-
return;
|
|
3091
|
+
async reindex(force = false) {
|
|
1932
3092
|
try {
|
|
1933
|
-
const
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
3093
|
+
const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
|
|
3094
|
+
const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
|
|
3095
|
+
1. Key symbols (classes, functions, types) and their locations
|
|
3096
|
+
2. Responsibilities of each directory/file
|
|
3097
|
+
3. Overall project structure
|
|
3098
|
+
|
|
3099
|
+
Analyze this file tree and provide a JSON response with:
|
|
3100
|
+
- "symbols": object mapping symbol names to file paths (array)
|
|
3101
|
+
- "responsibilities": object mapping paths to brief descriptions
|
|
3102
|
+
|
|
3103
|
+
File tree:
|
|
3104
|
+
${tree}
|
|
3105
|
+
|
|
3106
|
+
Return ONLY valid JSON, no markdown formatting.`;
|
|
3107
|
+
const response = await this.deps.aiRunner.run(prompt);
|
|
3108
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
3109
|
+
if (jsonMatch) {
|
|
3110
|
+
return JSON.parse(jsonMatch[0]);
|
|
1938
3111
|
}
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
3112
|
+
return { symbols: {}, responsibilities: {}, lastIndexed: "" };
|
|
3113
|
+
}, force);
|
|
3114
|
+
if (index === null) {
|
|
3115
|
+
this.deps.log("No changes detected, skipping reindex", "info");
|
|
3116
|
+
return;
|
|
1942
3117
|
}
|
|
1943
|
-
|
|
3118
|
+
this.indexer.saveIndex(index);
|
|
3119
|
+
this.deps.log("Codebase reindexed successfully", "success");
|
|
3120
|
+
} catch (error) {
|
|
3121
|
+
this.deps.log(`Failed to reindex codebase: ${error}`, "error");
|
|
3122
|
+
}
|
|
1944
3123
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
3124
|
+
}
|
|
3125
|
+
// src/agent/document-fetcher.ts
|
|
3126
|
+
var import_node_fs6 = require("node:fs");
|
|
3127
|
+
var import_node_path8 = require("node:path");
|
|
3128
|
+
class DocumentFetcher {
|
|
3129
|
+
deps;
|
|
3130
|
+
constructor(deps) {
|
|
3131
|
+
this.deps = deps;
|
|
3132
|
+
}
|
|
3133
|
+
async fetch() {
|
|
3134
|
+
const documentsDir = getLocusPath(this.deps.projectPath, "documentsDir");
|
|
3135
|
+
if (!import_node_fs6.existsSync(documentsDir)) {
|
|
3136
|
+
import_node_fs6.mkdirSync(documentsDir, { recursive: true });
|
|
1948
3137
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
3138
|
+
try {
|
|
3139
|
+
const groups = await this.deps.client.docs.listGroups(this.deps.workspaceId);
|
|
3140
|
+
const groupMap = new Map(groups.map((g) => [g.id, g.name]));
|
|
3141
|
+
const docs2 = await this.deps.client.docs.list(this.deps.workspaceId);
|
|
3142
|
+
const artifactsGroupId = groups.find((g) => g.name === "Artifacts")?.id;
|
|
3143
|
+
let fetchedCount = 0;
|
|
3144
|
+
for (const doc of docs2) {
|
|
3145
|
+
if (doc.groupId === artifactsGroupId) {
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
const groupName = groupMap.get(doc.groupId || "") || "General";
|
|
3149
|
+
const groupDir = import_node_path8.join(documentsDir, groupName);
|
|
3150
|
+
if (!import_node_fs6.existsSync(groupDir)) {
|
|
3151
|
+
import_node_fs6.mkdirSync(groupDir, { recursive: true });
|
|
3152
|
+
}
|
|
3153
|
+
const fileName = `${doc.title}.md`;
|
|
3154
|
+
const filePath = import_node_path8.join(groupDir, fileName);
|
|
3155
|
+
if (!import_node_fs6.existsSync(filePath) || import_node_fs6.readFileSync(filePath, "utf-8") !== doc.content) {
|
|
3156
|
+
import_node_fs6.writeFileSync(filePath, doc.content || "");
|
|
3157
|
+
fetchedCount++;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
if (fetchedCount > 0) {
|
|
3161
|
+
this.deps.log(`Fetched ${fetchedCount} document(s) from server`, "info");
|
|
3162
|
+
}
|
|
3163
|
+
} catch (error) {
|
|
3164
|
+
this.deps.log(`Failed to fetch documents: ${error}`, "error");
|
|
3165
|
+
throw error;
|
|
1962
3166
|
}
|
|
1963
3167
|
}
|
|
1964
3168
|
}
|
|
3169
|
+
// src/agent/review-service.ts
|
|
3170
|
+
var import_node_child_process6 = require("node:child_process");
|
|
1965
3171
|
|
|
1966
|
-
|
|
1967
|
-
class TaskExecutor {
|
|
3172
|
+
class ReviewService {
|
|
1968
3173
|
deps;
|
|
1969
|
-
promptBuilder;
|
|
1970
3174
|
constructor(deps) {
|
|
1971
3175
|
this.deps = deps;
|
|
1972
|
-
this.promptBuilder = new PromptBuilder(deps.projectPath);
|
|
1973
3176
|
}
|
|
1974
|
-
async
|
|
1975
|
-
|
|
1976
|
-
const basePrompt = await this.promptBuilder.build(task, {
|
|
1977
|
-
taskContext: context
|
|
1978
|
-
});
|
|
3177
|
+
async reviewStagedChanges(sprint) {
|
|
3178
|
+
const { projectPath, log } = this.deps;
|
|
1979
3179
|
try {
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
3180
|
+
import_node_child_process6.execSync("git add -A", { cwd: projectPath, stdio: "pipe" });
|
|
3181
|
+
log("Staged all changes for review.", "info");
|
|
3182
|
+
} catch (err) {
|
|
3183
|
+
log(`Failed to stage changes: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
3184
|
+
return null;
|
|
3185
|
+
}
|
|
3186
|
+
let diff;
|
|
3187
|
+
try {
|
|
3188
|
+
diff = import_node_child_process6.execSync("git diff --cached --stat && echo '---' && git diff --cached", {
|
|
3189
|
+
cwd: projectPath,
|
|
3190
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3191
|
+
}).toString();
|
|
3192
|
+
} catch (err) {
|
|
3193
|
+
log(`Failed to get staged diff: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
3194
|
+
return null;
|
|
3195
|
+
}
|
|
3196
|
+
if (!diff.trim()) {
|
|
3197
|
+
return null;
|
|
1992
3198
|
}
|
|
3199
|
+
const sprintInfo = sprint ? `Sprint: ${sprint.name} (${sprint.id})` : "No active sprint";
|
|
3200
|
+
const reviewPrompt = `# Code Review Request
|
|
3201
|
+
|
|
3202
|
+
## Context
|
|
3203
|
+
${sprintInfo}
|
|
3204
|
+
Date: ${new Date().toISOString()}
|
|
3205
|
+
|
|
3206
|
+
## Staged Changes (git diff)
|
|
3207
|
+
\`\`\`diff
|
|
3208
|
+
${diff}
|
|
3209
|
+
\`\`\`
|
|
3210
|
+
|
|
3211
|
+
## Instructions
|
|
3212
|
+
You are reviewing the staged changes at the end of a sprint. Produce a thorough markdown review report with the following sections:
|
|
3213
|
+
|
|
3214
|
+
1. **Summary** — Brief overview of what changed and why.
|
|
3215
|
+
2. **Files Changed** — List each file with a short description of changes.
|
|
3216
|
+
3. **Code Quality** — Note any code quality concerns (naming, structure, complexity).
|
|
3217
|
+
4. **Potential Issues** — Identify bugs, security issues, edge cases, or regressions.
|
|
3218
|
+
5. **Recommendations** — Actionable suggestions for improvement.
|
|
3219
|
+
6. **Overall Assessment** — A short verdict (e.g., "Looks good", "Needs attention", "Critical issues found").
|
|
3220
|
+
|
|
3221
|
+
Keep the review concise but thorough. Focus on substance over style.`;
|
|
3222
|
+
log("Running AI review on staged changes...", "info");
|
|
3223
|
+
const report = await this.deps.aiRunner.run(reviewPrompt);
|
|
3224
|
+
return report;
|
|
1993
3225
|
}
|
|
1994
3226
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
if (!value || value.startsWith("--")) {
|
|
1999
|
-
console.warn("Warning: --provider requires a value. Falling back to 'claude'.");
|
|
3227
|
+
// src/agent/reviewer-worker.ts
|
|
3228
|
+
function resolveProvider2(value) {
|
|
3229
|
+
if (!value || value.startsWith("--"))
|
|
2000
3230
|
return PROVIDER.CLAUDE;
|
|
2001
|
-
}
|
|
2002
3231
|
if (value === PROVIDER.CLAUDE || value === PROVIDER.CODEX)
|
|
2003
3232
|
return value;
|
|
2004
|
-
console.warn(`Warning: invalid --provider value '${value}'. Falling back to 'claude'.`);
|
|
2005
3233
|
return PROVIDER.CLAUDE;
|
|
2006
3234
|
}
|
|
2007
3235
|
|
|
2008
|
-
class
|
|
3236
|
+
class ReviewerWorker {
|
|
2009
3237
|
config;
|
|
2010
3238
|
client;
|
|
2011
3239
|
aiRunner;
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
3240
|
+
prService;
|
|
3241
|
+
knowledgeBase;
|
|
3242
|
+
heartbeatInterval = null;
|
|
3243
|
+
currentTaskId = null;
|
|
3244
|
+
maxReviews = 50;
|
|
3245
|
+
reviewsCompleted = 0;
|
|
2018
3246
|
constructor(config) {
|
|
2019
3247
|
this.config = config;
|
|
2020
3248
|
const projectPath = config.projectPath || process.cwd();
|
|
@@ -2035,29 +3263,10 @@ class AgentWorker {
|
|
|
2035
3263
|
model: config.model,
|
|
2036
3264
|
log
|
|
2037
3265
|
});
|
|
2038
|
-
this.
|
|
2039
|
-
|
|
2040
|
-
projectPath,
|
|
2041
|
-
log
|
|
2042
|
-
});
|
|
2043
|
-
this.documentFetcher = new DocumentFetcher({
|
|
2044
|
-
client: this.client,
|
|
2045
|
-
workspaceId: config.workspaceId,
|
|
2046
|
-
projectPath,
|
|
2047
|
-
log
|
|
2048
|
-
});
|
|
2049
|
-
this.taskExecutor = new TaskExecutor({
|
|
2050
|
-
aiRunner: this.aiRunner,
|
|
2051
|
-
projectPath,
|
|
2052
|
-
log
|
|
2053
|
-
});
|
|
2054
|
-
this.reviewService = new ReviewService({
|
|
2055
|
-
aiRunner: this.aiRunner,
|
|
2056
|
-
projectPath,
|
|
2057
|
-
log
|
|
2058
|
-
});
|
|
3266
|
+
this.prService = new PrService(projectPath, log);
|
|
3267
|
+
this.knowledgeBase = new KnowledgeBase(projectPath);
|
|
2059
3268
|
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
2060
|
-
this.log(`
|
|
3269
|
+
this.log(`Reviewer agent using ${providerLabel} CLI`, "info");
|
|
2061
3270
|
}
|
|
2062
3271
|
log(message, level = "info") {
|
|
2063
3272
|
const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
@@ -2068,109 +3277,131 @@ class AgentWorker {
|
|
|
2068
3277
|
error: c.red
|
|
2069
3278
|
}[level];
|
|
2070
3279
|
const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
|
|
2071
|
-
console.log(`${c.dim(`[${timestamp}]`)} ${c.bold(`[
|
|
2072
|
-
}
|
|
2073
|
-
async getActiveSprint() {
|
|
2074
|
-
try {
|
|
2075
|
-
if (this.config.sprintId) {
|
|
2076
|
-
return await this.client.sprints.getById(this.config.sprintId, this.config.workspaceId);
|
|
2077
|
-
}
|
|
2078
|
-
return await this.client.sprints.getActive(this.config.workspaceId);
|
|
2079
|
-
} catch (_error) {
|
|
2080
|
-
return null;
|
|
2081
|
-
}
|
|
3280
|
+
console.log(`${c.dim(`[${timestamp}]`)} ${c.bold(`[R:${this.config.agentId.slice(-8)}]`)} ${colorFn(`${prefix} ${message}`)}`);
|
|
2082
3281
|
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
return task;
|
|
2087
|
-
} catch (error) {
|
|
2088
|
-
this.log(`No task dispatched: ${error instanceof Error ? error.message : String(error)}`, "info");
|
|
2089
|
-
return null;
|
|
2090
|
-
}
|
|
3282
|
+
getNextUnreviewedPr() {
|
|
3283
|
+
const prs = this.prService.listUnreviewedLocusPrs();
|
|
3284
|
+
return prs.length > 0 ? prs[0] : null;
|
|
2091
3285
|
}
|
|
2092
|
-
async
|
|
2093
|
-
const
|
|
2094
|
-
|
|
3286
|
+
async reviewPr(pr) {
|
|
3287
|
+
const prNumber = String(pr.number);
|
|
3288
|
+
this.log(`Reviewing PR #${prNumber}: ${pr.title}`, "info");
|
|
3289
|
+
let diff;
|
|
2095
3290
|
try {
|
|
2096
|
-
|
|
3291
|
+
diff = this.prService.getPrDiff(prNumber);
|
|
2097
3292
|
} catch (err) {
|
|
2098
|
-
|
|
3293
|
+
return {
|
|
3294
|
+
reviewed: false,
|
|
3295
|
+
approved: false,
|
|
3296
|
+
summary: `Failed to get PR diff: ${err instanceof Error ? err.message : String(err)}`
|
|
3297
|
+
};
|
|
2099
3298
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
3299
|
+
if (!diff.trim()) {
|
|
3300
|
+
return {
|
|
3301
|
+
reviewed: true,
|
|
3302
|
+
approved: true,
|
|
3303
|
+
summary: "PR has no changes (empty diff)"
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
const reviewPrompt = `# Code Review Request
|
|
3307
|
+
|
|
3308
|
+
## PR: ${pr.title}
|
|
3309
|
+
|
|
3310
|
+
## PR Diff
|
|
3311
|
+
\`\`\`diff
|
|
3312
|
+
${diff.slice(0, 1e5)}
|
|
3313
|
+
\`\`\`
|
|
3314
|
+
|
|
3315
|
+
## Instructions
|
|
3316
|
+
You are a code reviewer. Review the PR diff above for:
|
|
3317
|
+
|
|
3318
|
+
1. **Correctness** — Does the code do what the PR title suggests?
|
|
3319
|
+
2. **Code Quality** — Naming, structure, complexity, readability.
|
|
3320
|
+
3. **Potential Issues** — Bugs, security issues, edge cases, regressions.
|
|
3321
|
+
|
|
3322
|
+
Output your review in this exact format:
|
|
3323
|
+
|
|
3324
|
+
VERDICT: APPROVE or REQUEST_CHANGES
|
|
3325
|
+
|
|
3326
|
+
Then provide a concise review with specific findings. Keep it actionable and focused.`;
|
|
3327
|
+
const output = await this.aiRunner.run(reviewPrompt);
|
|
3328
|
+
const approved = output.includes("VERDICT: APPROVE");
|
|
3329
|
+
const summary = output.replace(/VERDICT:\s*(APPROVE|REQUEST_CHANGES)\n?/, "").trim();
|
|
2105
3330
|
try {
|
|
2106
|
-
const
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2113
|
-
const sprintSlug = sprint?.name ? sprint.name.toLowerCase().replace(/\s+/g, "-").slice(0, 40) : "no-sprint";
|
|
2114
|
-
const fileName = `review-${sprintSlug}-${timestamp}.md`;
|
|
2115
|
-
const filePath = import_node_path7.join(reviewsDir, fileName);
|
|
2116
|
-
import_node_fs5.writeFileSync(filePath, report);
|
|
2117
|
-
this.log(`Review report saved to .locus/reviews/${fileName}`, "success");
|
|
2118
|
-
} else {
|
|
2119
|
-
this.log("No staged changes to review.", "info");
|
|
2120
|
-
}
|
|
3331
|
+
const event = approved ? "APPROVE" : "REQUEST_CHANGES";
|
|
3332
|
+
const reviewBody = `## Locus Agent Review
|
|
3333
|
+
|
|
3334
|
+
${summary}`;
|
|
3335
|
+
this.prService.submitReview(prNumber, reviewBody, event);
|
|
3336
|
+
this.log(`Review posted on PR #${prNumber}: ${approved ? "APPROVED" : "CHANGES REQUESTED"}`, approved ? "success" : "warn");
|
|
2121
3337
|
} catch (err) {
|
|
2122
|
-
this.log(`
|
|
3338
|
+
this.log(`Failed to post PR review: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
2123
3339
|
}
|
|
3340
|
+
return { reviewed: true, approved, summary };
|
|
2124
3341
|
}
|
|
2125
|
-
|
|
2126
|
-
this.
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
this.
|
|
3342
|
+
startHeartbeat() {
|
|
3343
|
+
this.sendHeartbeat();
|
|
3344
|
+
this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), 60000);
|
|
3345
|
+
}
|
|
3346
|
+
stopHeartbeat() {
|
|
3347
|
+
if (this.heartbeatInterval) {
|
|
3348
|
+
clearInterval(this.heartbeatInterval);
|
|
3349
|
+
this.heartbeatInterval = null;
|
|
2132
3350
|
}
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
3351
|
+
}
|
|
3352
|
+
sendHeartbeat() {
|
|
3353
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, this.currentTaskId, this.currentTaskId ? "WORKING" : "IDLE").catch((err) => {
|
|
3354
|
+
this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
async run() {
|
|
3358
|
+
this.log(`Reviewer agent started in ${this.config.projectPath || process.cwd()}`, "success");
|
|
3359
|
+
if (!isGhAvailable(this.config.projectPath)) {
|
|
3360
|
+
this.log("GitHub CLI (gh) not available — reviewer agent cannot operate", "error");
|
|
3361
|
+
process.exit(1);
|
|
3362
|
+
}
|
|
3363
|
+
const handleShutdown = () => {
|
|
3364
|
+
this.log("Received shutdown signal. Aborting...", "warn");
|
|
3365
|
+
this.aiRunner.abort();
|
|
3366
|
+
this.stopHeartbeat();
|
|
3367
|
+
process.exit(1);
|
|
3368
|
+
};
|
|
3369
|
+
process.on("SIGTERM", handleShutdown);
|
|
3370
|
+
process.on("SIGINT", handleShutdown);
|
|
3371
|
+
this.startHeartbeat();
|
|
3372
|
+
while (this.reviewsCompleted < this.maxReviews) {
|
|
3373
|
+
const pr = this.getNextUnreviewedPr();
|
|
3374
|
+
if (!pr) {
|
|
3375
|
+
this.log("No unreviewed PRs found. Waiting 30s...", "info");
|
|
3376
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
3377
|
+
continue;
|
|
2146
3378
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
3379
|
+
this.log(`Reviewing: ${pr.title} (PR #${pr.number})`, "success");
|
|
3380
|
+
this.sendHeartbeat();
|
|
3381
|
+
const result = await this.reviewPr(pr);
|
|
3382
|
+
if (result.reviewed) {
|
|
3383
|
+
const status = result.approved ? "APPROVED" : "CHANGES REQUESTED";
|
|
3384
|
+
try {
|
|
3385
|
+
this.knowledgeBase.updateProgress({
|
|
3386
|
+
type: "pr_reviewed",
|
|
3387
|
+
title: pr.title,
|
|
3388
|
+
details: `Review: ${status}`
|
|
3389
|
+
});
|
|
3390
|
+
} catch {}
|
|
3391
|
+
this.reviewsCompleted++;
|
|
2157
3392
|
} else {
|
|
2158
|
-
this.log(`
|
|
2159
|
-
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
2160
|
-
status: "BACKLOG",
|
|
2161
|
-
assignedTo: null
|
|
2162
|
-
});
|
|
2163
|
-
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
2164
|
-
author: this.config.agentId,
|
|
2165
|
-
text: `❌ ${result.summary}`
|
|
2166
|
-
});
|
|
3393
|
+
this.log(`Review skipped: ${result.summary}`, "warn");
|
|
2167
3394
|
}
|
|
3395
|
+
this.currentTaskId = null;
|
|
2168
3396
|
}
|
|
3397
|
+
this.stopHeartbeat();
|
|
3398
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
|
|
2169
3399
|
process.exit(0);
|
|
2170
3400
|
}
|
|
2171
3401
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
3402
|
+
var reviewerEntrypoint = process.argv[1]?.split(/[\\/]/).pop();
|
|
3403
|
+
if (reviewerEntrypoint === "reviewer-worker.js" || reviewerEntrypoint === "reviewer-worker.ts") {
|
|
3404
|
+
process.title = "locus-reviewer";
|
|
2174
3405
|
const args = process.argv.slice(2);
|
|
2175
3406
|
const config = {};
|
|
2176
3407
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -2193,60 +3424,19 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
|
|
|
2193
3424
|
const value = args[i + 1];
|
|
2194
3425
|
if (value && !value.startsWith("--"))
|
|
2195
3426
|
i++;
|
|
2196
|
-
config.provider =
|
|
3427
|
+
config.provider = resolveProvider2(value);
|
|
2197
3428
|
}
|
|
2198
3429
|
}
|
|
2199
3430
|
if (!config.agentId || !config.workspaceId || !config.apiBase || !config.apiKey || !config.projectPath) {
|
|
2200
3431
|
console.error("Missing required arguments");
|
|
2201
3432
|
process.exit(1);
|
|
2202
3433
|
}
|
|
2203
|
-
const worker = new
|
|
3434
|
+
const worker = new ReviewerWorker(config);
|
|
2204
3435
|
worker.run().catch((err) => {
|
|
2205
|
-
console.error("Fatal
|
|
3436
|
+
console.error("Fatal reviewer error:", err);
|
|
2206
3437
|
process.exit(1);
|
|
2207
3438
|
});
|
|
2208
3439
|
}
|
|
2209
|
-
|
|
2210
|
-
// src/index-node.ts
|
|
2211
|
-
var exports_index_node = {};
|
|
2212
|
-
__export(exports_index_node, {
|
|
2213
|
-
getLocusPath: () => getLocusPath,
|
|
2214
|
-
getAgentArtifactsPath: () => getAgentArtifactsPath,
|
|
2215
|
-
createAiRunner: () => createAiRunner,
|
|
2216
|
-
c: () => c,
|
|
2217
|
-
WorkspacesModule: () => WorkspacesModule,
|
|
2218
|
-
TasksModule: () => TasksModule,
|
|
2219
|
-
TaskExecutor: () => TaskExecutor,
|
|
2220
|
-
SprintsModule: () => SprintsModule,
|
|
2221
|
-
ReviewService: () => ReviewService,
|
|
2222
|
-
PromptBuilder: () => PromptBuilder,
|
|
2223
|
-
PROVIDER: () => PROVIDER,
|
|
2224
|
-
OrganizationsModule: () => OrganizationsModule,
|
|
2225
|
-
LocusEvent: () => LocusEvent,
|
|
2226
|
-
LocusEmitter: () => LocusEmitter,
|
|
2227
|
-
LocusClient: () => LocusClient,
|
|
2228
|
-
LOCUS_GITIGNORE_PATTERNS: () => LOCUS_GITIGNORE_PATTERNS,
|
|
2229
|
-
LOCUS_CONFIG: () => LOCUS_CONFIG,
|
|
2230
|
-
InvitationsModule: () => InvitationsModule,
|
|
2231
|
-
HistoryManager: () => HistoryManager,
|
|
2232
|
-
ExecSession: () => ExecSession,
|
|
2233
|
-
ExecEventType: () => ExecEventType,
|
|
2234
|
-
ExecEventEmitter: () => ExecEventEmitter,
|
|
2235
|
-
DocumentFetcher: () => DocumentFetcher,
|
|
2236
|
-
DocsModule: () => DocsModule,
|
|
2237
|
-
DEFAULT_MODEL: () => DEFAULT_MODEL,
|
|
2238
|
-
ContextTracker: () => ContextTracker,
|
|
2239
|
-
CodexRunner: () => CodexRunner,
|
|
2240
|
-
CodebaseIndexerService: () => CodebaseIndexerService,
|
|
2241
|
-
CodebaseIndexer: () => CodebaseIndexer,
|
|
2242
|
-
ClaudeRunner: () => ClaudeRunner,
|
|
2243
|
-
CiModule: () => CiModule,
|
|
2244
|
-
AuthModule: () => AuthModule,
|
|
2245
|
-
AgentWorker: () => AgentWorker,
|
|
2246
|
-
AgentOrchestrator: () => AgentOrchestrator,
|
|
2247
|
-
AIModule: () => AIModule
|
|
2248
|
-
});
|
|
2249
|
-
module.exports = __toCommonJS(exports_index_node);
|
|
2250
3440
|
// src/exec/context-tracker.ts
|
|
2251
3441
|
var REFERENCE_ALIASES = {
|
|
2252
3442
|
plan: ["the plan", "sprint plan", "project plan", "implementation plan"],
|
|
@@ -2660,8 +3850,8 @@ class ExecEventEmitter {
|
|
|
2660
3850
|
}
|
|
2661
3851
|
}
|
|
2662
3852
|
// src/exec/history-manager.ts
|
|
2663
|
-
var
|
|
2664
|
-
var
|
|
3853
|
+
var import_node_fs7 = require("node:fs");
|
|
3854
|
+
var import_node_path9 = require("node:path");
|
|
2665
3855
|
var DEFAULT_MAX_SESSIONS = 30;
|
|
2666
3856
|
function generateSessionId2() {
|
|
2667
3857
|
const timestamp = Date.now().toString(36);
|
|
@@ -2673,30 +3863,30 @@ class HistoryManager {
|
|
|
2673
3863
|
historyDir;
|
|
2674
3864
|
maxSessions;
|
|
2675
3865
|
constructor(projectPath, options) {
|
|
2676
|
-
this.historyDir = options?.historyDir ??
|
|
3866
|
+
this.historyDir = options?.historyDir ?? import_node_path9.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
|
|
2677
3867
|
this.maxSessions = options?.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
2678
3868
|
this.ensureHistoryDir();
|
|
2679
3869
|
}
|
|
2680
3870
|
ensureHistoryDir() {
|
|
2681
|
-
if (!
|
|
2682
|
-
|
|
3871
|
+
if (!import_node_fs7.existsSync(this.historyDir)) {
|
|
3872
|
+
import_node_fs7.mkdirSync(this.historyDir, { recursive: true });
|
|
2683
3873
|
}
|
|
2684
3874
|
}
|
|
2685
3875
|
getSessionPath(sessionId) {
|
|
2686
|
-
return
|
|
3876
|
+
return import_node_path9.join(this.historyDir, `${sessionId}.json`);
|
|
2687
3877
|
}
|
|
2688
3878
|
saveSession(session) {
|
|
2689
3879
|
const filePath = this.getSessionPath(session.id);
|
|
2690
3880
|
session.updatedAt = Date.now();
|
|
2691
|
-
|
|
3881
|
+
import_node_fs7.writeFileSync(filePath, JSON.stringify(session, null, 2), "utf-8");
|
|
2692
3882
|
}
|
|
2693
3883
|
loadSession(sessionId) {
|
|
2694
3884
|
const filePath = this.getSessionPath(sessionId);
|
|
2695
|
-
if (!
|
|
3885
|
+
if (!import_node_fs7.existsSync(filePath)) {
|
|
2696
3886
|
return null;
|
|
2697
3887
|
}
|
|
2698
3888
|
try {
|
|
2699
|
-
const content =
|
|
3889
|
+
const content = import_node_fs7.readFileSync(filePath, "utf-8");
|
|
2700
3890
|
return JSON.parse(content);
|
|
2701
3891
|
} catch {
|
|
2702
3892
|
return null;
|
|
@@ -2704,18 +3894,18 @@ class HistoryManager {
|
|
|
2704
3894
|
}
|
|
2705
3895
|
deleteSession(sessionId) {
|
|
2706
3896
|
const filePath = this.getSessionPath(sessionId);
|
|
2707
|
-
if (!
|
|
3897
|
+
if (!import_node_fs7.existsSync(filePath)) {
|
|
2708
3898
|
return false;
|
|
2709
3899
|
}
|
|
2710
3900
|
try {
|
|
2711
|
-
|
|
3901
|
+
import_node_fs7.rmSync(filePath);
|
|
2712
3902
|
return true;
|
|
2713
3903
|
} catch {
|
|
2714
3904
|
return false;
|
|
2715
3905
|
}
|
|
2716
3906
|
}
|
|
2717
3907
|
listSessions(options) {
|
|
2718
|
-
const files =
|
|
3908
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
2719
3909
|
let sessions = [];
|
|
2720
3910
|
for (const file of files) {
|
|
2721
3911
|
if (file.endsWith(".json")) {
|
|
@@ -2788,11 +3978,11 @@ class HistoryManager {
|
|
|
2788
3978
|
return deleted;
|
|
2789
3979
|
}
|
|
2790
3980
|
getSessionCount() {
|
|
2791
|
-
const files =
|
|
3981
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
2792
3982
|
return files.filter((f) => f.endsWith(".json")).length;
|
|
2793
3983
|
}
|
|
2794
3984
|
sessionExists(sessionId) {
|
|
2795
|
-
return
|
|
3985
|
+
return import_node_fs7.existsSync(this.getSessionPath(sessionId));
|
|
2796
3986
|
}
|
|
2797
3987
|
findSessionByPartialId(partialId) {
|
|
2798
3988
|
const sessions = this.listSessions();
|
|
@@ -2806,12 +3996,12 @@ class HistoryManager {
|
|
|
2806
3996
|
return this.historyDir;
|
|
2807
3997
|
}
|
|
2808
3998
|
clearAllSessions() {
|
|
2809
|
-
const files =
|
|
3999
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
2810
4000
|
let deleted = 0;
|
|
2811
4001
|
for (const file of files) {
|
|
2812
4002
|
if (file.endsWith(".json")) {
|
|
2813
4003
|
try {
|
|
2814
|
-
|
|
4004
|
+
import_node_fs7.rmSync(import_node_path9.join(this.historyDir, file));
|
|
2815
4005
|
deleted++;
|
|
2816
4006
|
} catch {}
|
|
2817
4007
|
}
|
|
@@ -3076,12 +4266,14 @@ ${currentPrompt}`);
|
|
|
3076
4266
|
}
|
|
3077
4267
|
}
|
|
3078
4268
|
// src/orchestrator.ts
|
|
3079
|
-
var
|
|
3080
|
-
var
|
|
3081
|
-
var
|
|
4269
|
+
var import_node_child_process7 = require("node:child_process");
|
|
4270
|
+
var import_node_fs8 = require("node:fs");
|
|
4271
|
+
var import_node_path10 = require("node:path");
|
|
3082
4272
|
var import_node_url = require("node:url");
|
|
3083
|
-
var
|
|
4273
|
+
var import_shared4 = require("@locusai/shared");
|
|
3084
4274
|
var import_events4 = require("events");
|
|
4275
|
+
var MAX_AGENTS = 5;
|
|
4276
|
+
|
|
3085
4277
|
class AgentOrchestrator extends import_events4.EventEmitter {
|
|
3086
4278
|
client;
|
|
3087
4279
|
config;
|
|
@@ -3089,6 +4281,8 @@ class AgentOrchestrator extends import_events4.EventEmitter {
|
|
|
3089
4281
|
isRunning = false;
|
|
3090
4282
|
processedTasks = new Set;
|
|
3091
4283
|
resolvedSprintId = null;
|
|
4284
|
+
worktreeManager = null;
|
|
4285
|
+
heartbeatInterval = null;
|
|
3092
4286
|
constructor(config) {
|
|
3093
4287
|
super();
|
|
3094
4288
|
this.config = config;
|
|
@@ -3097,6 +4291,15 @@ class AgentOrchestrator extends import_events4.EventEmitter {
|
|
|
3097
4291
|
token: config.apiKey
|
|
3098
4292
|
});
|
|
3099
4293
|
}
|
|
4294
|
+
get agentCount() {
|
|
4295
|
+
return Math.min(Math.max(this.config.agentCount ?? 1, 1), MAX_AGENTS);
|
|
4296
|
+
}
|
|
4297
|
+
get useWorktrees() {
|
|
4298
|
+
return this.config.useWorktrees ?? true;
|
|
4299
|
+
}
|
|
4300
|
+
get worktreeCleanupPolicy() {
|
|
4301
|
+
return this.config.worktreeCleanupPolicy ?? "retain-on-failure";
|
|
4302
|
+
}
|
|
3100
4303
|
async resolveSprintId() {
|
|
3101
4304
|
if (this.config.sprintId) {
|
|
3102
4305
|
return this.config.sprintId;
|
|
@@ -3140,6 +4343,12 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
|
|
|
3140
4343
|
if (this.resolvedSprintId) {
|
|
3141
4344
|
console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
|
|
3142
4345
|
}
|
|
4346
|
+
console.log(`${c.bold("Agents:")} ${this.agentCount}`);
|
|
4347
|
+
console.log(`${c.bold("Worktrees:")} ${this.useWorktrees ? "enabled" : "disabled"}`);
|
|
4348
|
+
if (this.useWorktrees) {
|
|
4349
|
+
console.log(`${c.bold("Cleanup policy:")} ${this.worktreeCleanupPolicy}`);
|
|
4350
|
+
console.log(`${c.bold("Auto-push:")} ${this.config.autoPush ? "enabled" : "disabled"}`);
|
|
4351
|
+
}
|
|
3143
4352
|
console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
|
|
3144
4353
|
console.log(c.dim(`----------------------------------------------
|
|
3145
4354
|
`));
|
|
@@ -3148,9 +4357,30 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
|
|
|
3148
4357
|
console.log(c.dim("ℹ No available tasks found in the backlog."));
|
|
3149
4358
|
return;
|
|
3150
4359
|
}
|
|
3151
|
-
|
|
4360
|
+
if (tasks2.length > 0 && this.useWorktrees && !isGitAvailable()) {
|
|
4361
|
+
console.log(c.error("git is not installed. Worktree isolation requires git. Install from https://git-scm.com/"));
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
if (tasks2.length > 0 && this.config.autoPush && !isGhAvailable(this.config.projectPath)) {
|
|
4365
|
+
console.log(c.warning("GitHub CLI (gh) not available or not authenticated. Branch push can continue, but automatic PR creation may fail until gh is configured. Install from https://cli.github.com/"));
|
|
4366
|
+
}
|
|
4367
|
+
if (tasks2.length > 0 && this.useWorktrees) {
|
|
4368
|
+
this.worktreeManager = new WorktreeManager(this.config.projectPath, {
|
|
4369
|
+
cleanupPolicy: this.worktreeCleanupPolicy
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
this.startHeartbeatMonitor();
|
|
4373
|
+
const agentsToSpawn = Math.min(this.agentCount, tasks2.length);
|
|
4374
|
+
const SPAWN_DELAY_MS = 5000;
|
|
4375
|
+
const spawnPromises = [];
|
|
4376
|
+
for (let i = 0;i < agentsToSpawn; i++) {
|
|
4377
|
+
if (i > 0) {
|
|
4378
|
+
await this.sleep(SPAWN_DELAY_MS);
|
|
4379
|
+
}
|
|
4380
|
+
spawnPromises.push(this.spawnAgent(i));
|
|
4381
|
+
}
|
|
4382
|
+
await Promise.all(spawnPromises);
|
|
3152
4383
|
while (this.agents.size > 0 && this.isRunning) {
|
|
3153
|
-
await this.reapAgents();
|
|
3154
4384
|
if (this.agents.size === 0) {
|
|
3155
4385
|
break;
|
|
3156
4386
|
}
|
|
@@ -3159,8 +4389,8 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
|
|
|
3159
4389
|
console.log(`
|
|
3160
4390
|
${c.success("✅ Orchestrator finished")}`);
|
|
3161
4391
|
}
|
|
3162
|
-
async spawnAgent() {
|
|
3163
|
-
const agentId = `agent-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
4392
|
+
async spawnAgent(index) {
|
|
4393
|
+
const agentId = `agent-${index}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
3164
4394
|
const agentState = {
|
|
3165
4395
|
id: agentId,
|
|
3166
4396
|
status: "IDLE",
|
|
@@ -3172,13 +4402,9 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
3172
4402
|
this.agents.set(agentId, agentState);
|
|
3173
4403
|
console.log(`${c.primary("\uD83D\uDE80 Agent started:")} ${c.bold(agentId)}
|
|
3174
4404
|
`);
|
|
3175
|
-
const
|
|
3176
|
-
const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator.ts");
|
|
3177
|
-
const currentModuleDir = import_node_path9.dirname(currentModulePath);
|
|
3178
|
-
potentialPaths.push(import_node_path9.join(currentModuleDir, "agent", "worker.js"), import_node_path9.join(currentModuleDir, "worker.js"), import_node_path9.join(currentModuleDir, "agent", "worker.ts"));
|
|
3179
|
-
const workerPath = potentialPaths.find((p) => import_node_fs7.existsSync(p));
|
|
4405
|
+
const workerPath = this.resolveWorkerPath();
|
|
3180
4406
|
if (!workerPath) {
|
|
3181
|
-
throw new Error(
|
|
4407
|
+
throw new Error("Worker file not found. Make sure the SDK is properly built and installed.");
|
|
3182
4408
|
}
|
|
3183
4409
|
const workerArgs = [
|
|
3184
4410
|
"--agent-id",
|
|
@@ -3201,8 +4427,15 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
3201
4427
|
if (this.resolvedSprintId) {
|
|
3202
4428
|
workerArgs.push("--sprint-id", this.resolvedSprintId);
|
|
3203
4429
|
}
|
|
3204
|
-
|
|
4430
|
+
if (this.useWorktrees) {
|
|
4431
|
+
workerArgs.push("--use-worktrees");
|
|
4432
|
+
}
|
|
4433
|
+
if (this.config.autoPush) {
|
|
4434
|
+
workerArgs.push("--auto-push");
|
|
4435
|
+
}
|
|
4436
|
+
const agentProcess = import_node_child_process7.spawn(process.execPath, [workerPath, ...workerArgs], {
|
|
3205
4437
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4438
|
+
detached: true,
|
|
3206
4439
|
env: {
|
|
3207
4440
|
...process.env,
|
|
3208
4441
|
FORCE_COLOR: "1",
|
|
@@ -3217,6 +4450,9 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
3217
4450
|
agentState.tasksCompleted = msg.tasksCompleted || 0;
|
|
3218
4451
|
agentState.tasksFailed = msg.tasksFailed || 0;
|
|
3219
4452
|
}
|
|
4453
|
+
if (msg.type === "heartbeat") {
|
|
4454
|
+
agentState.lastHeartbeat = new Date;
|
|
4455
|
+
}
|
|
3220
4456
|
});
|
|
3221
4457
|
agentProcess.stdout?.on("data", (data) => {
|
|
3222
4458
|
process.stdout.write(data.toString());
|
|
@@ -3241,7 +4477,30 @@ ${agentId} finished (exit code: ${code})`);
|
|
|
3241
4477
|
});
|
|
3242
4478
|
this.emit("agent:spawned", { agentId });
|
|
3243
4479
|
}
|
|
3244
|
-
|
|
4480
|
+
resolveWorkerPath() {
|
|
4481
|
+
const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator.ts");
|
|
4482
|
+
const currentModuleDir = import_node_path10.dirname(currentModulePath);
|
|
4483
|
+
const potentialPaths = [
|
|
4484
|
+
import_node_path10.join(currentModuleDir, "agent", "worker.js"),
|
|
4485
|
+
import_node_path10.join(currentModuleDir, "worker.js"),
|
|
4486
|
+
import_node_path10.join(currentModuleDir, "agent", "worker.ts")
|
|
4487
|
+
];
|
|
4488
|
+
return potentialPaths.find((p) => import_node_fs8.existsSync(p));
|
|
4489
|
+
}
|
|
4490
|
+
startHeartbeatMonitor() {
|
|
4491
|
+
this.heartbeatInterval = setInterval(() => {
|
|
4492
|
+
const now = Date.now();
|
|
4493
|
+
for (const [agentId, agent] of this.agents.entries()) {
|
|
4494
|
+
if (agent.status === "WORKING" && now - agent.lastHeartbeat.getTime() > import_shared4.STALE_AGENT_TIMEOUT_MS) {
|
|
4495
|
+
console.log(c.error(`Agent ${agentId} is stale (no heartbeat for 10 minutes). Killing.`));
|
|
4496
|
+
if (agent.process && !agent.process.killed) {
|
|
4497
|
+
this.killProcessTree(agent.process);
|
|
4498
|
+
}
|
|
4499
|
+
this.emit("agent:stale", { agentId });
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
}, 60000);
|
|
4503
|
+
}
|
|
3245
4504
|
async getAvailableTasks() {
|
|
3246
4505
|
try {
|
|
3247
4506
|
const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
|
|
@@ -3258,10 +4517,10 @@ ${agentId} finished (exit code: ${code})`);
|
|
|
3258
4517
|
try {
|
|
3259
4518
|
const tasks2 = await this.getAvailableTasks();
|
|
3260
4519
|
const priorityOrder = [
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
4520
|
+
import_shared4.TaskPriority.CRITICAL,
|
|
4521
|
+
import_shared4.TaskPriority.HIGH,
|
|
4522
|
+
import_shared4.TaskPriority.MEDIUM,
|
|
4523
|
+
import_shared4.TaskPriority.LOW
|
|
3265
4524
|
];
|
|
3266
4525
|
let task = tasks2.sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority))[0];
|
|
3267
4526
|
if (!task && tasks2.length > 0) {
|
|
@@ -3285,7 +4544,7 @@ ${agentId} finished (exit code: ${code})`);
|
|
|
3285
4544
|
async completeTask(taskId, agentId, summary) {
|
|
3286
4545
|
try {
|
|
3287
4546
|
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
3288
|
-
status:
|
|
4547
|
+
status: import_shared4.TaskStatus.IN_REVIEW
|
|
3289
4548
|
});
|
|
3290
4549
|
if (summary) {
|
|
3291
4550
|
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
@@ -3310,7 +4569,7 @@ ${summary}`
|
|
|
3310
4569
|
async failTask(taskId, agentId, error) {
|
|
3311
4570
|
try {
|
|
3312
4571
|
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
3313
|
-
status:
|
|
4572
|
+
status: import_shared4.TaskStatus.BACKLOG,
|
|
3314
4573
|
assignedTo: null
|
|
3315
4574
|
});
|
|
3316
4575
|
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
@@ -3333,11 +4592,52 @@ ${summary}`
|
|
|
3333
4592
|
await this.cleanup();
|
|
3334
4593
|
this.emit("stopped", { timestamp: new Date });
|
|
3335
4594
|
}
|
|
4595
|
+
stopAgent(agentId) {
|
|
4596
|
+
const agent = this.agents.get(agentId);
|
|
4597
|
+
if (!agent)
|
|
4598
|
+
return false;
|
|
4599
|
+
if (agent.process && !agent.process.killed) {
|
|
4600
|
+
this.killProcessTree(agent.process);
|
|
4601
|
+
}
|
|
4602
|
+
return true;
|
|
4603
|
+
}
|
|
4604
|
+
killProcessTree(proc) {
|
|
4605
|
+
if (!proc.pid || proc.killed)
|
|
4606
|
+
return;
|
|
4607
|
+
try {
|
|
4608
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
4609
|
+
} catch {
|
|
4610
|
+
try {
|
|
4611
|
+
proc.kill("SIGTERM");
|
|
4612
|
+
} catch {}
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
3336
4615
|
async cleanup() {
|
|
4616
|
+
if (this.heartbeatInterval) {
|
|
4617
|
+
clearInterval(this.heartbeatInterval);
|
|
4618
|
+
this.heartbeatInterval = null;
|
|
4619
|
+
}
|
|
3337
4620
|
for (const [agentId, agent] of this.agents.entries()) {
|
|
3338
4621
|
if (agent.process && !agent.process.killed) {
|
|
3339
4622
|
console.log(`Killing agent: ${agentId}`);
|
|
3340
|
-
agent.process
|
|
4623
|
+
this.killProcessTree(agent.process);
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
if (this.worktreeManager) {
|
|
4627
|
+
try {
|
|
4628
|
+
if (this.worktreeCleanupPolicy === "auto") {
|
|
4629
|
+
const removed = this.worktreeManager.removeAll();
|
|
4630
|
+
if (removed > 0) {
|
|
4631
|
+
console.log(c.dim(`Cleaned up ${removed} worktree(s)`));
|
|
4632
|
+
}
|
|
4633
|
+
} else if (this.worktreeCleanupPolicy === "retain-on-failure") {
|
|
4634
|
+
this.worktreeManager.prune();
|
|
4635
|
+
console.log(c.dim("Retaining worktrees for failure analysis (cleanup policy: retain-on-failure)"));
|
|
4636
|
+
} else {
|
|
4637
|
+
console.log(c.dim("Skipping worktree cleanup (cleanup policy: manual)"));
|
|
4638
|
+
}
|
|
4639
|
+
} catch {
|
|
4640
|
+
console.log(c.dim("Could not clean up some worktrees"));
|
|
3341
4641
|
}
|
|
3342
4642
|
}
|
|
3343
4643
|
this.agents.clear();
|
|
@@ -3345,12 +4645,543 @@ ${summary}`
|
|
|
3345
4645
|
getStats() {
|
|
3346
4646
|
return {
|
|
3347
4647
|
activeAgents: this.agents.size,
|
|
4648
|
+
agentCount: this.agentCount,
|
|
4649
|
+
useWorktrees: this.useWorktrees,
|
|
3348
4650
|
processedTasks: this.processedTasks.size,
|
|
3349
4651
|
totalTasksCompleted: Array.from(this.agents.values()).reduce((sum, agent) => sum + agent.tasksCompleted, 0),
|
|
3350
4652
|
totalTasksFailed: Array.from(this.agents.values()).reduce((sum, agent) => sum + agent.tasksFailed, 0)
|
|
3351
4653
|
};
|
|
3352
4654
|
}
|
|
4655
|
+
getAgentStates() {
|
|
4656
|
+
return Array.from(this.agents.values());
|
|
4657
|
+
}
|
|
3353
4658
|
sleep(ms) {
|
|
3354
|
-
return new Promise((
|
|
4659
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
// src/planning/plan-manager.ts
|
|
4663
|
+
var import_node_fs9 = require("node:fs");
|
|
4664
|
+
var import_node_path11 = require("node:path");
|
|
4665
|
+
|
|
4666
|
+
// src/planning/sprint-plan.ts
|
|
4667
|
+
var import_shared5 = require("@locusai/shared");
|
|
4668
|
+
function sprintPlanToMarkdown(plan) {
|
|
4669
|
+
const lines = [];
|
|
4670
|
+
lines.push(`# Sprint Plan: ${plan.name}`);
|
|
4671
|
+
lines.push("");
|
|
4672
|
+
lines.push(`**Status:** ${plan.status.toUpperCase()}`);
|
|
4673
|
+
lines.push(`**Created:** ${plan.createdAt}`);
|
|
4674
|
+
lines.push(`**Estimated Duration:** ${plan.estimatedDays} day(s)`);
|
|
4675
|
+
lines.push("");
|
|
4676
|
+
lines.push(`## Goal`);
|
|
4677
|
+
lines.push(plan.goal);
|
|
4678
|
+
lines.push("");
|
|
4679
|
+
lines.push(`## CEO Directive`);
|
|
4680
|
+
lines.push(`> ${plan.directive}`);
|
|
4681
|
+
lines.push("");
|
|
4682
|
+
if (plan.feedback) {
|
|
4683
|
+
lines.push(`## CEO Feedback`);
|
|
4684
|
+
lines.push(`> ${plan.feedback}`);
|
|
4685
|
+
lines.push("");
|
|
4686
|
+
}
|
|
4687
|
+
lines.push(`## Tasks (${plan.tasks.length})`);
|
|
4688
|
+
lines.push("");
|
|
4689
|
+
for (const task of plan.tasks) {
|
|
4690
|
+
lines.push(`### ${task.index}. ${task.title}`);
|
|
4691
|
+
lines.push(`- **Role:** ${task.assigneeRole}`);
|
|
4692
|
+
lines.push(`- **Priority:** ${task.priority}`);
|
|
4693
|
+
lines.push(`- **Complexity:** ${"█".repeat(task.complexity)}${"░".repeat(5 - task.complexity)} (${task.complexity}/5)`);
|
|
4694
|
+
if (task.labels.length > 0) {
|
|
4695
|
+
lines.push(`- **Labels:** ${task.labels.join(", ")}`);
|
|
4696
|
+
}
|
|
4697
|
+
lines.push("");
|
|
4698
|
+
lines.push(task.description);
|
|
4699
|
+
lines.push("");
|
|
4700
|
+
if (task.acceptanceCriteria.length > 0) {
|
|
4701
|
+
lines.push(`**Acceptance Criteria:**`);
|
|
4702
|
+
for (const ac of task.acceptanceCriteria) {
|
|
4703
|
+
lines.push(`- [ ] ${ac}`);
|
|
4704
|
+
}
|
|
4705
|
+
lines.push("");
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
if (plan.risks.length > 0) {
|
|
4709
|
+
lines.push(`## Risks`);
|
|
4710
|
+
lines.push("");
|
|
4711
|
+
for (const risk of plan.risks) {
|
|
4712
|
+
lines.push(`- **[${risk.severity.toUpperCase()}]** ${risk.description}`);
|
|
4713
|
+
lines.push(` - Mitigation: ${risk.mitigation}`);
|
|
4714
|
+
}
|
|
4715
|
+
lines.push("");
|
|
4716
|
+
}
|
|
4717
|
+
lines.push(`---`);
|
|
4718
|
+
lines.push(`*Plan ID: ${plan.id}*`);
|
|
4719
|
+
return lines.join(`
|
|
4720
|
+
`);
|
|
4721
|
+
}
|
|
4722
|
+
function plannedTasksToCreatePayloads(plan, sprintId) {
|
|
4723
|
+
return plan.tasks.map((task) => ({
|
|
4724
|
+
title: task.title,
|
|
4725
|
+
description: task.description,
|
|
4726
|
+
status: import_shared5.TaskStatus.BACKLOG,
|
|
4727
|
+
assigneeRole: task.assigneeRole,
|
|
4728
|
+
priority: task.priority,
|
|
4729
|
+
labels: task.labels,
|
|
4730
|
+
sprintId,
|
|
4731
|
+
order: task.index * 10,
|
|
4732
|
+
acceptanceChecklist: task.acceptanceCriteria.map((text, i) => ({
|
|
4733
|
+
id: `ac-${i + 1}`,
|
|
4734
|
+
text,
|
|
4735
|
+
done: false
|
|
4736
|
+
}))
|
|
4737
|
+
}));
|
|
4738
|
+
}
|
|
4739
|
+
function parseSprintPlanFromAI(raw, directive) {
|
|
4740
|
+
let jsonStr = raw.trim();
|
|
4741
|
+
const jsonMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
4742
|
+
if (jsonMatch) {
|
|
4743
|
+
jsonStr = jsonMatch[1]?.trim() || "";
|
|
4744
|
+
}
|
|
4745
|
+
const parsed = JSON.parse(jsonStr);
|
|
4746
|
+
const now = new Date().toISOString();
|
|
4747
|
+
const id = `plan-${Date.now()}`;
|
|
4748
|
+
const tasks2 = (parsed.tasks || []).map((t, i) => ({
|
|
4749
|
+
index: i + 1,
|
|
4750
|
+
title: t.title || `Task ${i + 1}`,
|
|
4751
|
+
description: t.description || "",
|
|
4752
|
+
assigneeRole: t.assigneeRole || "BACKEND",
|
|
4753
|
+
priority: t.priority || "MEDIUM",
|
|
4754
|
+
complexity: t.complexity || 3,
|
|
4755
|
+
acceptanceCriteria: t.acceptanceCriteria || [],
|
|
4756
|
+
labels: t.labels || []
|
|
4757
|
+
}));
|
|
4758
|
+
return {
|
|
4759
|
+
id,
|
|
4760
|
+
name: parsed.name || "Unnamed Sprint",
|
|
4761
|
+
goal: parsed.goal || directive,
|
|
4762
|
+
directive,
|
|
4763
|
+
tasks: tasks2,
|
|
4764
|
+
risks: (parsed.risks || []).map((r) => ({
|
|
4765
|
+
description: r.description || "",
|
|
4766
|
+
mitigation: r.mitigation || "",
|
|
4767
|
+
severity: r.severity || "medium"
|
|
4768
|
+
})),
|
|
4769
|
+
estimatedDays: parsed.estimatedDays || 1,
|
|
4770
|
+
status: "pending",
|
|
4771
|
+
createdAt: now,
|
|
4772
|
+
updatedAt: now
|
|
4773
|
+
};
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
// src/planning/plan-manager.ts
|
|
4777
|
+
class PlanManager {
|
|
4778
|
+
projectPath;
|
|
4779
|
+
plansDir;
|
|
4780
|
+
constructor(projectPath) {
|
|
4781
|
+
this.projectPath = projectPath;
|
|
4782
|
+
this.plansDir = getLocusPath(projectPath, "plansDir");
|
|
4783
|
+
}
|
|
4784
|
+
save(plan) {
|
|
4785
|
+
this.ensurePlansDir();
|
|
4786
|
+
const slug = this.slugify(plan.name);
|
|
4787
|
+
const jsonPath = import_node_path11.join(this.plansDir, `${slug}.json`);
|
|
4788
|
+
const mdPath = import_node_path11.join(this.plansDir, `sprint-${slug}.md`);
|
|
4789
|
+
import_node_fs9.writeFileSync(jsonPath, JSON.stringify(plan, null, 2), "utf-8");
|
|
4790
|
+
import_node_fs9.writeFileSync(mdPath, sprintPlanToMarkdown(plan), "utf-8");
|
|
4791
|
+
return plan.id;
|
|
4792
|
+
}
|
|
4793
|
+
load(idOrSlug) {
|
|
4794
|
+
this.ensurePlansDir();
|
|
4795
|
+
const files = import_node_fs9.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
|
|
4796
|
+
for (const file of files) {
|
|
4797
|
+
const filePath = import_node_path11.join(this.plansDir, file);
|
|
4798
|
+
try {
|
|
4799
|
+
const plan = JSON.parse(import_node_fs9.readFileSync(filePath, "utf-8"));
|
|
4800
|
+
if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
|
|
4801
|
+
return plan;
|
|
4802
|
+
}
|
|
4803
|
+
} catch {}
|
|
4804
|
+
}
|
|
4805
|
+
return null;
|
|
4806
|
+
}
|
|
4807
|
+
list(status) {
|
|
4808
|
+
this.ensurePlansDir();
|
|
4809
|
+
const files = import_node_fs9.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
|
|
4810
|
+
const plans = [];
|
|
4811
|
+
for (const file of files) {
|
|
4812
|
+
try {
|
|
4813
|
+
const plan = JSON.parse(import_node_fs9.readFileSync(import_node_path11.join(this.plansDir, file), "utf-8"));
|
|
4814
|
+
if (!status || plan.status === status) {
|
|
4815
|
+
plans.push(plan);
|
|
4816
|
+
}
|
|
4817
|
+
} catch {}
|
|
4818
|
+
}
|
|
4819
|
+
plans.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
4820
|
+
return plans;
|
|
4821
|
+
}
|
|
4822
|
+
async approve(idOrSlug, client, workspaceId) {
|
|
4823
|
+
const plan = this.load(idOrSlug);
|
|
4824
|
+
if (!plan) {
|
|
4825
|
+
throw new Error(`Plan not found: ${idOrSlug}`);
|
|
4826
|
+
}
|
|
4827
|
+
if (plan.status !== "pending") {
|
|
4828
|
+
throw new Error(`Plan "${plan.name}" is ${plan.status}, can only approve pending plans`);
|
|
4829
|
+
}
|
|
4830
|
+
const sprint = await client.sprints.create(workspaceId, {
|
|
4831
|
+
name: plan.name
|
|
4832
|
+
});
|
|
4833
|
+
const payloads = plannedTasksToCreatePayloads(plan, sprint.id);
|
|
4834
|
+
const tasks2 = [];
|
|
4835
|
+
for (const payload of payloads) {
|
|
4836
|
+
const task = await client.tasks.create(workspaceId, payload);
|
|
4837
|
+
tasks2.push(task);
|
|
4838
|
+
}
|
|
4839
|
+
await client.sprints.start(sprint.id, workspaceId);
|
|
4840
|
+
plan.status = "approved";
|
|
4841
|
+
plan.updatedAt = new Date().toISOString();
|
|
4842
|
+
this.save(plan);
|
|
4843
|
+
const kb = new KnowledgeBase(this.projectPath);
|
|
4844
|
+
kb.updateProgress({
|
|
4845
|
+
type: "sprint_started",
|
|
4846
|
+
title: plan.name,
|
|
4847
|
+
details: `${tasks2.length} tasks created from planning meeting. Sprint goal: ${plan.goal}`
|
|
4848
|
+
});
|
|
4849
|
+
return { sprint, tasks: tasks2 };
|
|
4850
|
+
}
|
|
4851
|
+
reject(idOrSlug, feedback) {
|
|
4852
|
+
const plan = this.load(idOrSlug);
|
|
4853
|
+
if (!plan) {
|
|
4854
|
+
throw new Error(`Plan not found: ${idOrSlug}`);
|
|
4855
|
+
}
|
|
4856
|
+
if (plan.status !== "pending") {
|
|
4857
|
+
throw new Error(`Plan "${plan.name}" is ${plan.status}, can only reject pending plans`);
|
|
4858
|
+
}
|
|
4859
|
+
plan.status = "rejected";
|
|
4860
|
+
plan.feedback = feedback;
|
|
4861
|
+
plan.updatedAt = new Date().toISOString();
|
|
4862
|
+
this.save(plan);
|
|
4863
|
+
return plan;
|
|
4864
|
+
}
|
|
4865
|
+
cancel(idOrSlug) {
|
|
4866
|
+
const plan = this.load(idOrSlug);
|
|
4867
|
+
if (!plan) {
|
|
4868
|
+
throw new Error(`Plan not found: ${idOrSlug}`);
|
|
4869
|
+
}
|
|
4870
|
+
plan.status = "cancelled";
|
|
4871
|
+
plan.updatedAt = new Date().toISOString();
|
|
4872
|
+
this.save(plan);
|
|
4873
|
+
}
|
|
4874
|
+
delete(idOrSlug) {
|
|
4875
|
+
this.ensurePlansDir();
|
|
4876
|
+
const files = import_node_fs9.readdirSync(this.plansDir);
|
|
4877
|
+
for (const file of files) {
|
|
4878
|
+
const filePath = import_node_path11.join(this.plansDir, file);
|
|
4879
|
+
if (!file.endsWith(".json"))
|
|
4880
|
+
continue;
|
|
4881
|
+
try {
|
|
4882
|
+
const plan = JSON.parse(import_node_fs9.readFileSync(filePath, "utf-8"));
|
|
4883
|
+
if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
|
|
4884
|
+
import_node_fs9.unlinkSync(filePath);
|
|
4885
|
+
const mdPath = import_node_path11.join(this.plansDir, `sprint-${this.slugify(plan.name)}.md`);
|
|
4886
|
+
if (import_node_fs9.existsSync(mdPath)) {
|
|
4887
|
+
import_node_fs9.unlinkSync(mdPath);
|
|
4888
|
+
}
|
|
4889
|
+
return;
|
|
4890
|
+
}
|
|
4891
|
+
} catch {}
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
getMarkdown(idOrSlug) {
|
|
4895
|
+
const plan = this.load(idOrSlug);
|
|
4896
|
+
if (!plan)
|
|
4897
|
+
return null;
|
|
4898
|
+
return sprintPlanToMarkdown(plan);
|
|
4899
|
+
}
|
|
4900
|
+
ensurePlansDir() {
|
|
4901
|
+
if (!import_node_fs9.existsSync(this.plansDir)) {
|
|
4902
|
+
import_node_fs9.mkdirSync(this.plansDir, { recursive: true });
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
slugify(name) {
|
|
4906
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
// src/planning/planning-meeting.ts
|
|
4910
|
+
var import_node_fs10 = require("node:fs");
|
|
4911
|
+
|
|
4912
|
+
// src/planning/agents/architect.ts
|
|
4913
|
+
function buildArchitectPrompt(input) {
|
|
4914
|
+
let prompt = `# Role: Software Architect
|
|
4915
|
+
|
|
4916
|
+
You are a Software Architect participating in an async sprint planning meeting. The Tech Lead has produced an initial task breakdown. Your job is to refine it.
|
|
4917
|
+
|
|
4918
|
+
## CEO Directive
|
|
4919
|
+
> ${input.directive}
|
|
4920
|
+
`;
|
|
4921
|
+
if (input.feedback) {
|
|
4922
|
+
prompt += `
|
|
4923
|
+
## CEO Feedback on Previous Plan
|
|
4924
|
+
> ${input.feedback}
|
|
4925
|
+
|
|
4926
|
+
IMPORTANT: Ensure the refined plan addresses this feedback.
|
|
4927
|
+
`;
|
|
4928
|
+
}
|
|
4929
|
+
prompt += `
|
|
4930
|
+
## Project Context
|
|
4931
|
+
${input.projectContext || "No project context available."}
|
|
4932
|
+
|
|
4933
|
+
## Tech Lead's Task Breakdown
|
|
4934
|
+
${input.techLeadOutput}
|
|
4935
|
+
|
|
4936
|
+
## Your Task
|
|
4937
|
+
|
|
4938
|
+
Review and refine the Tech Lead's breakdown:
|
|
4939
|
+
|
|
4940
|
+
1. **Ordering** — Order tasks so that foundational work comes first. Tasks that produce outputs consumed by later tasks must appear earlier in the list. Foundation tasks (schemas, config, shared code) must be listed before tasks that build on them. The array index IS the execution order.
|
|
4941
|
+
2. **Risk Assessment** — Flag tasks that are risky, underestimated, or have unknowns.
|
|
4942
|
+
3. **Task Splitting** — If a task is too large (would take more than a day), split it.
|
|
4943
|
+
4. **Task Merging** — If two tasks are trivially small and related, merge them.
|
|
4944
|
+
5. **Complexity Scoring** — Rate each task 1-5 (1=trivial, 5=very complex).
|
|
4945
|
+
6. **Missing Tasks** — Add any tasks the Tech Lead missed (database migrations, configuration, testing, etc.).
|
|
4946
|
+
|
|
4947
|
+
## Output Format
|
|
4948
|
+
|
|
4949
|
+
Respond with ONLY a JSON object (no markdown code blocks, no explanation):
|
|
4950
|
+
|
|
4951
|
+
{
|
|
4952
|
+
"tasks": [
|
|
4953
|
+
{
|
|
4954
|
+
"title": "string",
|
|
4955
|
+
"description": "string (2-4 sentences)",
|
|
4956
|
+
"assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
|
|
4957
|
+
"priority": "HIGH | MEDIUM | LOW | CRITICAL",
|
|
4958
|
+
"labels": ["string"],
|
|
4959
|
+
"acceptanceCriteria": ["string"],
|
|
4960
|
+
"complexity": 3
|
|
4961
|
+
}
|
|
4962
|
+
],
|
|
4963
|
+
"risks": [
|
|
4964
|
+
{
|
|
4965
|
+
"description": "string",
|
|
4966
|
+
"mitigation": "string",
|
|
4967
|
+
"severity": "low | medium | high"
|
|
4968
|
+
}
|
|
4969
|
+
],
|
|
4970
|
+
"architectureNotes": "string (notes for the Sprint Organizer about parallelism opportunities and constraints)"
|
|
4971
|
+
}`;
|
|
4972
|
+
return prompt;
|
|
4973
|
+
}
|
|
4974
|
+
|
|
4975
|
+
// src/planning/agents/sprint-organizer.ts
|
|
4976
|
+
function buildSprintOrganizerPrompt(input) {
|
|
4977
|
+
let prompt = `# Role: Sprint Organizer
|
|
4978
|
+
|
|
4979
|
+
You are a Sprint Organizer finalizing the sprint plan from a planning meeting. The Architect has refined the task breakdown. Your job is to produce the final sprint plan document.
|
|
4980
|
+
|
|
4981
|
+
## CEO Directive
|
|
4982
|
+
> ${input.directive}
|
|
4983
|
+
`;
|
|
4984
|
+
if (input.feedback) {
|
|
4985
|
+
prompt += `
|
|
4986
|
+
## CEO Feedback on Previous Plan
|
|
4987
|
+
> ${input.feedback}
|
|
4988
|
+
|
|
4989
|
+
IMPORTANT: The final plan must address this feedback.
|
|
4990
|
+
`;
|
|
4991
|
+
}
|
|
4992
|
+
prompt += `
|
|
4993
|
+
## Architect's Refined Task Breakdown
|
|
4994
|
+
${input.architectOutput}
|
|
4995
|
+
|
|
4996
|
+
## Your Task
|
|
4997
|
+
|
|
4998
|
+
Produce the final sprint plan:
|
|
4999
|
+
|
|
5000
|
+
1. **Sprint Name** — A concise, memorable name for this sprint (e.g., "User Authentication", "Payment Integration")
|
|
5001
|
+
2. **Sprint Goal** — One paragraph describing what this sprint delivers
|
|
5002
|
+
3. **Task Ordering** — Final ordering so that foundational work comes first. The position in the array IS the execution order — task at index 0 runs first, index 1 runs second, etc.
|
|
5003
|
+
4. **Duration Estimate** — How many days this sprint will take with 2-3 agents working in parallel
|
|
5004
|
+
5. **Final Task List** — Each task with all fields filled in, ordered by execution priority
|
|
5005
|
+
|
|
5006
|
+
Guidelines:
|
|
5007
|
+
- The order of tasks in the array determines execution order. Tasks are dispatched sequentially from first to last.
|
|
5008
|
+
- Foundation tasks (schemas, config, shared code) must appear before tasks that build on them
|
|
5009
|
+
- Group related tasks together when possible
|
|
5010
|
+
- Ensure acceptance criteria are specific and testable
|
|
5011
|
+
- Keep the sprint focused — if it's too large (>12 tasks), consider reducing scope
|
|
5012
|
+
|
|
5013
|
+
## Output Format
|
|
5014
|
+
|
|
5015
|
+
Respond with ONLY a JSON object (no markdown code blocks, no explanation):
|
|
5016
|
+
|
|
5017
|
+
{
|
|
5018
|
+
"name": "string (2-4 words)",
|
|
5019
|
+
"goal": "string (1 paragraph)",
|
|
5020
|
+
"estimatedDays": 3,
|
|
5021
|
+
"tasks": [
|
|
5022
|
+
{
|
|
5023
|
+
"title": "string",
|
|
5024
|
+
"description": "string",
|
|
5025
|
+
"assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
|
|
5026
|
+
"priority": "CRITICAL | HIGH | MEDIUM | LOW",
|
|
5027
|
+
"labels": ["string"],
|
|
5028
|
+
"acceptanceCriteria": ["string"],
|
|
5029
|
+
"complexity": 3
|
|
5030
|
+
}
|
|
5031
|
+
],
|
|
5032
|
+
"risks": [
|
|
5033
|
+
{
|
|
5034
|
+
"description": "string",
|
|
5035
|
+
"mitigation": "string",
|
|
5036
|
+
"severity": "low | medium | high"
|
|
5037
|
+
}
|
|
5038
|
+
]
|
|
5039
|
+
}`;
|
|
5040
|
+
return prompt;
|
|
5041
|
+
}
|
|
5042
|
+
|
|
5043
|
+
// src/planning/agents/tech-lead.ts
|
|
5044
|
+
function buildTechLeadPrompt(input) {
|
|
5045
|
+
let prompt = `# Role: Senior Tech Lead
|
|
5046
|
+
|
|
5047
|
+
You are a Senior Tech Lead participating in an async sprint planning meeting. Your job is to take the CEO's directive and produce an initial task breakdown.
|
|
5048
|
+
|
|
5049
|
+
## CEO Directive
|
|
5050
|
+
> ${input.directive}
|
|
5051
|
+
`;
|
|
5052
|
+
if (input.feedback) {
|
|
5053
|
+
prompt += `
|
|
5054
|
+
## CEO Feedback on Previous Plan
|
|
5055
|
+
> ${input.feedback}
|
|
5056
|
+
|
|
5057
|
+
IMPORTANT: Incorporate this feedback into your task breakdown. The CEO has reviewed a previous plan and wants changes.
|
|
5058
|
+
`;
|
|
5059
|
+
}
|
|
5060
|
+
prompt += `
|
|
5061
|
+
## Project Context
|
|
5062
|
+
${input.projectContext || "No project context available."}
|
|
5063
|
+
|
|
5064
|
+
## Codebase Structure
|
|
5065
|
+
${input.codebaseIndex || "No codebase index available."}
|
|
5066
|
+
|
|
5067
|
+
## Your Task
|
|
5068
|
+
|
|
5069
|
+
Analyze the CEO's directive and produce a detailed task breakdown. For each task:
|
|
5070
|
+
|
|
5071
|
+
1. **Title** — Clear, action-oriented (e.g., "Implement user registration API endpoint")
|
|
5072
|
+
2. **Description** — What needs to be done technically, which files/modules are involved
|
|
5073
|
+
3. **Assignee Role** — Who should work on this: BACKEND, FRONTEND, QA, PM, or DESIGN
|
|
5074
|
+
4. **Priority** — HIGH, MEDIUM, or LOW based on business impact
|
|
5075
|
+
5. **Labels** — Relevant tags (e.g., "api", "database", "ui", "auth")
|
|
5076
|
+
6. **Acceptance Criteria** — Specific, testable conditions for completion
|
|
5077
|
+
|
|
5078
|
+
Think about:
|
|
5079
|
+
- What existing code can be reused or extended
|
|
5080
|
+
- Which tasks are independent vs. dependent
|
|
5081
|
+
- What the right granularity is (not too big, not too small)
|
|
5082
|
+
- What risks or unknowns exist
|
|
5083
|
+
|
|
5084
|
+
## Output Format
|
|
5085
|
+
|
|
5086
|
+
Respond with ONLY a JSON object (no markdown code blocks, no explanation):
|
|
5087
|
+
|
|
5088
|
+
{
|
|
5089
|
+
"tasks": [
|
|
5090
|
+
{
|
|
5091
|
+
"title": "string",
|
|
5092
|
+
"description": "string (2-4 sentences)",
|
|
5093
|
+
"assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
|
|
5094
|
+
"priority": "HIGH | MEDIUM | LOW",
|
|
5095
|
+
"labels": ["string"],
|
|
5096
|
+
"acceptanceCriteria": ["string"]
|
|
5097
|
+
}
|
|
5098
|
+
],
|
|
5099
|
+
"technicalNotes": "string (brief notes on architecture decisions, risks, or considerations for the Architect phase)"
|
|
5100
|
+
}`;
|
|
5101
|
+
return prompt;
|
|
5102
|
+
}
|
|
5103
|
+
|
|
5104
|
+
// src/planning/planning-meeting.ts
|
|
5105
|
+
class PlanningMeeting {
|
|
5106
|
+
projectPath;
|
|
5107
|
+
aiRunner;
|
|
5108
|
+
log;
|
|
5109
|
+
constructor(config) {
|
|
5110
|
+
this.projectPath = config.projectPath;
|
|
5111
|
+
this.aiRunner = config.aiRunner;
|
|
5112
|
+
this.log = config.log ?? ((_msg) => {
|
|
5113
|
+
return;
|
|
5114
|
+
});
|
|
5115
|
+
}
|
|
5116
|
+
async run(directive, feedback) {
|
|
5117
|
+
const projectContext = this.getProjectContext();
|
|
5118
|
+
const codebaseIndex = this.getCodebaseIndex();
|
|
5119
|
+
this.log("Phase 1/3: Tech Lead analyzing directive...", "info");
|
|
5120
|
+
const techLeadPrompt = buildTechLeadPrompt({
|
|
5121
|
+
directive,
|
|
5122
|
+
projectContext,
|
|
5123
|
+
codebaseIndex,
|
|
5124
|
+
feedback
|
|
5125
|
+
});
|
|
5126
|
+
const techLeadOutput = await this.aiRunner.run(techLeadPrompt);
|
|
5127
|
+
this.log("Tech Lead phase complete.", "success");
|
|
5128
|
+
this.log("Phase 2/3: Architect refining task breakdown...", "info");
|
|
5129
|
+
const architectPrompt = buildArchitectPrompt({
|
|
5130
|
+
directive,
|
|
5131
|
+
projectContext,
|
|
5132
|
+
techLeadOutput,
|
|
5133
|
+
feedback
|
|
5134
|
+
});
|
|
5135
|
+
const architectOutput = await this.aiRunner.run(architectPrompt);
|
|
5136
|
+
this.log("Architect phase complete.", "success");
|
|
5137
|
+
this.log("Phase 3/3: Sprint Organizer finalizing plan...", "info");
|
|
5138
|
+
const sprintOrganizerPrompt = buildSprintOrganizerPrompt({
|
|
5139
|
+
directive,
|
|
5140
|
+
architectOutput,
|
|
5141
|
+
feedback
|
|
5142
|
+
});
|
|
5143
|
+
const sprintOrganizerOutput = await this.aiRunner.run(sprintOrganizerPrompt);
|
|
5144
|
+
this.log("Sprint Organizer phase complete.", "success");
|
|
5145
|
+
const plan = parseSprintPlanFromAI(sprintOrganizerOutput, directive);
|
|
5146
|
+
if (feedback) {
|
|
5147
|
+
plan.feedback = feedback;
|
|
5148
|
+
}
|
|
5149
|
+
return {
|
|
5150
|
+
plan,
|
|
5151
|
+
phaseOutputs: {
|
|
5152
|
+
techLead: techLeadOutput,
|
|
5153
|
+
architect: architectOutput,
|
|
5154
|
+
sprintOrganizer: sprintOrganizerOutput
|
|
5155
|
+
}
|
|
5156
|
+
};
|
|
5157
|
+
}
|
|
5158
|
+
getProjectContext() {
|
|
5159
|
+
const kb = new KnowledgeBase(this.projectPath);
|
|
5160
|
+
return kb.getFullContext();
|
|
5161
|
+
}
|
|
5162
|
+
getCodebaseIndex() {
|
|
5163
|
+
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
5164
|
+
if (!import_node_fs10.existsSync(indexPath)) {
|
|
5165
|
+
return "";
|
|
5166
|
+
}
|
|
5167
|
+
try {
|
|
5168
|
+
const raw = import_node_fs10.readFileSync(indexPath, "utf-8");
|
|
5169
|
+
const index = JSON.parse(raw);
|
|
5170
|
+
const parts = [];
|
|
5171
|
+
if (index.responsibilities) {
|
|
5172
|
+
parts.push("### File Responsibilities");
|
|
5173
|
+
const entries = Object.entries(index.responsibilities);
|
|
5174
|
+
for (const [file, summary] of entries.slice(0, 50)) {
|
|
5175
|
+
parts.push(`- \`${file}\`: ${summary}`);
|
|
5176
|
+
}
|
|
5177
|
+
if (entries.length > 50) {
|
|
5178
|
+
parts.push(`... and ${entries.length - 50} more files`);
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
return parts.join(`
|
|
5182
|
+
`);
|
|
5183
|
+
} catch {
|
|
5184
|
+
return "";
|
|
5185
|
+
}
|
|
3355
5186
|
}
|
|
3356
5187
|
}
|