@locusai/sdk 0.8.0 → 0.9.0
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 +2 -0
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/review-service.d.ts +21 -0
- package/dist/agent/review-service.d.ts.map +1 -0
- 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 +2 -2
- package/dist/agent/task-executor.d.ts.map +1 -1
- package/dist/agent/worker.d.ts +47 -5
- package/dist/agent/worker.d.ts.map +1 -1
- package/dist/agent/worker.js +1127 -470
- package/dist/ai/claude-runner.d.ts +6 -1
- 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 +6 -1
- package/dist/ai/runner.d.ts.map +1 -1
- package/dist/core/config.d.ts +12 -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 +2461 -568
- 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,6 +480,7 @@ __export(exports_worker, {
|
|
|
512
480
|
AgentWorker: () => AgentWorker
|
|
513
481
|
});
|
|
514
482
|
module.exports = __toCommonJS(exports_worker);
|
|
483
|
+
var import_shared3 = require("@locusai/shared");
|
|
515
484
|
|
|
516
485
|
// src/core/config.ts
|
|
517
486
|
var import_node_path = require("node:path");
|
|
@@ -521,28 +490,50 @@ var PROVIDER = {
|
|
|
521
490
|
};
|
|
522
491
|
var DEFAULT_MODEL = {
|
|
523
492
|
[PROVIDER.CLAUDE]: "opus",
|
|
524
|
-
[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`
|
|
525
499
|
};
|
|
526
500
|
var LOCUS_CONFIG = {
|
|
527
501
|
dir: ".locus",
|
|
528
502
|
configFile: "config.json",
|
|
503
|
+
settingsFile: "settings.json",
|
|
529
504
|
indexFile: "codebase-index.json",
|
|
530
|
-
contextFile: "
|
|
505
|
+
contextFile: "LOCUS.md",
|
|
531
506
|
artifactsDir: "artifacts",
|
|
532
507
|
documentsDir: "documents",
|
|
533
|
-
|
|
534
|
-
|
|
508
|
+
sessionsDir: "sessions",
|
|
509
|
+
reviewsDir: "reviews",
|
|
510
|
+
plansDir: "plans",
|
|
511
|
+
projectDir: "project",
|
|
512
|
+
projectContextFile: "context.md",
|
|
513
|
+
projectProgressFile: "progress.md"
|
|
535
514
|
};
|
|
536
515
|
var LOCUS_GITIGNORE_PATTERNS = [
|
|
537
516
|
"# Locus AI - Session data (user-specific, can grow large)",
|
|
538
517
|
".locus/sessions/",
|
|
539
518
|
"",
|
|
540
519
|
"# Locus AI - Artifacts (local-only, user-specific)",
|
|
541
|
-
".locus/artifacts/"
|
|
520
|
+
".locus/artifacts/",
|
|
521
|
+
"",
|
|
522
|
+
"# Locus AI - Review reports (generated per sprint)",
|
|
523
|
+
".locus/reviews/",
|
|
524
|
+
"",
|
|
525
|
+
"# Locus AI - Plans (generated per task)",
|
|
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"
|
|
542
533
|
];
|
|
543
534
|
function getLocusPath(projectPath, fileName) {
|
|
544
|
-
if (fileName === "
|
|
545
|
-
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]);
|
|
546
537
|
}
|
|
547
538
|
return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG[fileName]);
|
|
548
539
|
}
|
|
@@ -618,6 +609,14 @@ var c = {
|
|
|
618
609
|
};
|
|
619
610
|
|
|
620
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
|
+
|
|
621
620
|
class ClaudeRunner {
|
|
622
621
|
model;
|
|
623
622
|
log;
|
|
@@ -625,6 +624,7 @@ class ClaudeRunner {
|
|
|
625
624
|
eventEmitter;
|
|
626
625
|
currentToolName;
|
|
627
626
|
activeTools = new Map;
|
|
627
|
+
activeProcess = null;
|
|
628
628
|
constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log) {
|
|
629
629
|
this.model = model;
|
|
630
630
|
this.log = log;
|
|
@@ -633,7 +633,13 @@ class ClaudeRunner {
|
|
|
633
633
|
setEventEmitter(emitter) {
|
|
634
634
|
this.eventEmitter = emitter;
|
|
635
635
|
}
|
|
636
|
-
|
|
636
|
+
abort() {
|
|
637
|
+
if (this.activeProcess && !this.activeProcess.killed) {
|
|
638
|
+
this.activeProcess.kill("SIGTERM");
|
|
639
|
+
this.activeProcess = null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async run(prompt) {
|
|
637
643
|
const maxRetries = 3;
|
|
638
644
|
let lastError = null;
|
|
639
645
|
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
@@ -661,7 +667,9 @@ class ClaudeRunner {
|
|
|
661
667
|
"stream-json",
|
|
662
668
|
"--include-partial-messages",
|
|
663
669
|
"--model",
|
|
664
|
-
this.model
|
|
670
|
+
this.model,
|
|
671
|
+
"--settings",
|
|
672
|
+
SANDBOX_SETTINGS
|
|
665
673
|
];
|
|
666
674
|
const env = {
|
|
667
675
|
...process.env,
|
|
@@ -678,6 +686,7 @@ class ClaudeRunner {
|
|
|
678
686
|
stdio: ["pipe", "pipe", "pipe"],
|
|
679
687
|
env
|
|
680
688
|
});
|
|
689
|
+
this.activeProcess = claude;
|
|
681
690
|
let buffer = "";
|
|
682
691
|
let stderrBuffer = "";
|
|
683
692
|
let resolveChunk = null;
|
|
@@ -745,6 +754,7 @@ class ClaudeRunner {
|
|
|
745
754
|
signalEnd();
|
|
746
755
|
});
|
|
747
756
|
claude.on("close", (code) => {
|
|
757
|
+
this.activeProcess = null;
|
|
748
758
|
if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
|
|
749
759
|
process.stderr.write(`${stderrBuffer}
|
|
750
760
|
`);
|
|
@@ -902,7 +912,9 @@ class ClaudeRunner {
|
|
|
902
912
|
"stream-json",
|
|
903
913
|
"--include-partial-messages",
|
|
904
914
|
"--model",
|
|
905
|
-
this.model
|
|
915
|
+
this.model,
|
|
916
|
+
"--settings",
|
|
917
|
+
SANDBOX_SETTINGS
|
|
906
918
|
];
|
|
907
919
|
const env = {
|
|
908
920
|
...process.env,
|
|
@@ -914,6 +926,7 @@ class ClaudeRunner {
|
|
|
914
926
|
stdio: ["pipe", "pipe", "pipe"],
|
|
915
927
|
env
|
|
916
928
|
});
|
|
929
|
+
this.activeProcess = claude;
|
|
917
930
|
let finalResult = "";
|
|
918
931
|
let errorOutput = "";
|
|
919
932
|
let buffer = "";
|
|
@@ -947,6 +960,7 @@ class ClaudeRunner {
|
|
|
947
960
|
reject(new Error(`Failed to start Claude CLI: ${err.message}. Please ensure the 'claude' command is available in your PATH.`));
|
|
948
961
|
});
|
|
949
962
|
claude.on("close", (code) => {
|
|
963
|
+
this.activeProcess = null;
|
|
950
964
|
if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
|
|
951
965
|
process.stderr.write(`${stderrBuffer}
|
|
952
966
|
`);
|
|
@@ -1013,11 +1027,18 @@ class CodexRunner {
|
|
|
1013
1027
|
projectPath;
|
|
1014
1028
|
model;
|
|
1015
1029
|
log;
|
|
1030
|
+
activeProcess = null;
|
|
1016
1031
|
constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log) {
|
|
1017
1032
|
this.projectPath = projectPath;
|
|
1018
1033
|
this.model = model;
|
|
1019
1034
|
this.log = log;
|
|
1020
1035
|
}
|
|
1036
|
+
abort() {
|
|
1037
|
+
if (this.activeProcess && !this.activeProcess.killed) {
|
|
1038
|
+
this.activeProcess.kill("SIGTERM");
|
|
1039
|
+
this.activeProcess = null;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1021
1042
|
async run(prompt) {
|
|
1022
1043
|
const maxRetries = 3;
|
|
1023
1044
|
let lastError = null;
|
|
@@ -1044,6 +1065,7 @@ class CodexRunner {
|
|
|
1044
1065
|
env: process.env,
|
|
1045
1066
|
shell: false
|
|
1046
1067
|
});
|
|
1068
|
+
this.activeProcess = codex;
|
|
1047
1069
|
let resolveChunk = null;
|
|
1048
1070
|
const chunkQueue = [];
|
|
1049
1071
|
let processEnded = false;
|
|
@@ -1093,6 +1115,7 @@ class CodexRunner {
|
|
|
1093
1115
|
signalEnd();
|
|
1094
1116
|
});
|
|
1095
1117
|
codex.on("close", (code) => {
|
|
1118
|
+
this.activeProcess = null;
|
|
1096
1119
|
this.cleanupTempFile(outputPath);
|
|
1097
1120
|
if (code === 0) {
|
|
1098
1121
|
const result = this.readOutput(outputPath, finalOutput);
|
|
@@ -1138,6 +1161,7 @@ class CodexRunner {
|
|
|
1138
1161
|
env: process.env,
|
|
1139
1162
|
shell: false
|
|
1140
1163
|
});
|
|
1164
|
+
this.activeProcess = codex;
|
|
1141
1165
|
let output = "";
|
|
1142
1166
|
let errorOutput = "";
|
|
1143
1167
|
const handleOutput = (data) => {
|
|
@@ -1155,6 +1179,7 @@ class CodexRunner {
|
|
|
1155
1179
|
reject(new Error(`Failed to start Codex CLI: ${err.message}. ` + `Ensure 'codex' is installed and available in PATH.`));
|
|
1156
1180
|
});
|
|
1157
1181
|
codex.on("close", (code) => {
|
|
1182
|
+
this.activeProcess = null;
|
|
1158
1183
|
this.cleanupTempFile(outputPath);
|
|
1159
1184
|
if (code === 0) {
|
|
1160
1185
|
resolve2(this.readOutput(outputPath, output));
|
|
@@ -1169,7 +1194,8 @@ class CodexRunner {
|
|
|
1169
1194
|
buildArgs(outputPath) {
|
|
1170
1195
|
const args = [
|
|
1171
1196
|
"exec",
|
|
1172
|
-
"--
|
|
1197
|
+
"--sandbox",
|
|
1198
|
+
"workspace-write",
|
|
1173
1199
|
"--skip-git-repo-check",
|
|
1174
1200
|
"--output-last-message",
|
|
1175
1201
|
outputPath
|
|
@@ -1236,315 +1262,745 @@ function createAiRunner(provider, config) {
|
|
|
1236
1262
|
}
|
|
1237
1263
|
}
|
|
1238
1264
|
|
|
1239
|
-
// src/
|
|
1240
|
-
var
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
+
}
|
|
1244
1357
|
|
|
1245
|
-
|
|
1358
|
+
// src/git/pr-service.ts
|
|
1359
|
+
var import_node_child_process4 = require("node:child_process");
|
|
1360
|
+
class PrService {
|
|
1246
1361
|
projectPath;
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
constructor(projectPath) {
|
|
1362
|
+
log;
|
|
1363
|
+
constructor(projectPath, log) {
|
|
1250
1364
|
this.projectPath = projectPath;
|
|
1251
|
-
this.
|
|
1365
|
+
this.log = log;
|
|
1252
1366
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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})`);
|
|
1256
1378
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
const treeString = currentFiles.join(`
|
|
1260
|
-
`);
|
|
1261
|
-
const newTreeHash = this.hashTree(treeString);
|
|
1262
|
-
const existingIndex = this.loadIndex();
|
|
1263
|
-
const currentHashes = this.computeFileHashes(currentFiles);
|
|
1264
|
-
const existingHashes = existingIndex?.fileHashes;
|
|
1265
|
-
const hasExistingContent = existingIndex && (Object.keys(existingIndex.symbols).length > 0 || Object.keys(existingIndex.responsibilities).length > 0);
|
|
1266
|
-
const canIncremental = !force && existingIndex && existingHashes && hasExistingContent;
|
|
1267
|
-
if (canIncremental) {
|
|
1268
|
-
onProgress?.("Performing incremental update");
|
|
1269
|
-
const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
|
|
1270
|
-
const changedFiles = [...added, ...modified];
|
|
1271
|
-
const totalChanges = changedFiles.length + deleted.length;
|
|
1272
|
-
const existingFileCount = Object.keys(existingHashes).length;
|
|
1273
|
-
onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
|
|
1274
|
-
if (existingFileCount > 0) {
|
|
1275
|
-
const changeRatio = totalChanges / existingFileCount;
|
|
1276
|
-
if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
|
|
1277
|
-
onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
|
|
1278
|
-
const incrementalIndex = await treeSummarizer(changedFiles.join(`
|
|
1279
|
-
`));
|
|
1280
|
-
const updatedIndex = this.cloneIndex(existingIndex);
|
|
1281
|
-
this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
|
|
1282
|
-
return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
|
|
1283
|
-
}
|
|
1284
|
-
if (changedFiles.length === 0 && deleted.length > 0) {
|
|
1285
|
-
onProgress?.(`Removing ${deleted.length} deleted files from index`);
|
|
1286
|
-
const updatedIndex = this.cloneIndex(existingIndex);
|
|
1287
|
-
this.removeFilesFromIndex(updatedIndex, deleted);
|
|
1288
|
-
return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
|
|
1289
|
-
}
|
|
1290
|
-
if (changedFiles.length === 0 && deleted.length === 0) {
|
|
1291
|
-
onProgress?.("No actual file changes, updating hashes only");
|
|
1292
|
-
const updatedIndex = this.cloneIndex(existingIndex);
|
|
1293
|
-
return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
|
|
1294
|
-
}
|
|
1295
|
-
onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
|
|
1296
|
-
}
|
|
1379
|
+
if (!isGhAvailable(this.projectPath)) {
|
|
1380
|
+
throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
|
|
1297
1381
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
}
|
|
1303
|
-
|
|
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.`);
|
|
1304
1411
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
submoduleIgnores.push(`**/${path}/**`);
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
} catch {}
|
|
1412
|
+
if (!this.hasRemoteBranch(headBranch)) {
|
|
1413
|
+
throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
|
|
1414
|
+
}
|
|
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.`);
|
|
1419
|
+
}
|
|
1420
|
+
if (!headRef) {
|
|
1421
|
+
throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
|
|
1422
|
+
}
|
|
1423
|
+
const commitsAhead = this.countCommitsAhead(baseRef, headRef);
|
|
1424
|
+
if (commitsAhead <= 0) {
|
|
1425
|
+
throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
|
|
1323
1426
|
}
|
|
1324
|
-
return import_globby.globby(["**/*"], {
|
|
1325
|
-
cwd: this.projectPath,
|
|
1326
|
-
gitignore: true,
|
|
1327
|
-
ignore: [
|
|
1328
|
-
...submoduleIgnores,
|
|
1329
|
-
"**/node_modules/**",
|
|
1330
|
-
"**/dist/**",
|
|
1331
|
-
"**/build/**",
|
|
1332
|
-
"**/target/**",
|
|
1333
|
-
"**/bin/**",
|
|
1334
|
-
"**/obj/**",
|
|
1335
|
-
"**/.next/**",
|
|
1336
|
-
"**/.svelte-kit/**",
|
|
1337
|
-
"**/.nuxt/**",
|
|
1338
|
-
"**/.cache/**",
|
|
1339
|
-
"**/out/**",
|
|
1340
|
-
"**/__tests__/**",
|
|
1341
|
-
"**/coverage/**",
|
|
1342
|
-
"**/*.test.*",
|
|
1343
|
-
"**/*.spec.*",
|
|
1344
|
-
"**/*.d.ts",
|
|
1345
|
-
"**/tsconfig.tsbuildinfo",
|
|
1346
|
-
"**/.locus/*.json",
|
|
1347
|
-
"**/.locus/*.md",
|
|
1348
|
-
"**/.locus/!(artifacts)/**",
|
|
1349
|
-
"**/.git/**",
|
|
1350
|
-
"**/.svn/**",
|
|
1351
|
-
"**/.hg/**",
|
|
1352
|
-
"**/.vscode/**",
|
|
1353
|
-
"**/.idea/**",
|
|
1354
|
-
"**/.DS_Store",
|
|
1355
|
-
"**/bun.lock",
|
|
1356
|
-
"**/package-lock.json",
|
|
1357
|
-
"**/yarn.lock",
|
|
1358
|
-
"**/pnpm-lock.yaml",
|
|
1359
|
-
"**/Cargo.lock",
|
|
1360
|
-
"**/go.sum",
|
|
1361
|
-
"**/poetry.lock",
|
|
1362
|
-
"**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
|
|
1363
|
-
]
|
|
1364
|
-
});
|
|
1365
1427
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1428
|
+
countCommitsAhead(baseRef, headRef) {
|
|
1429
|
+
const output = import_node_child_process4.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
|
|
1430
|
+
cwd: this.projectPath,
|
|
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;
|
|
1436
|
+
}
|
|
1437
|
+
resolveBranchRef(branch) {
|
|
1438
|
+
if (this.hasLocalBranch(branch)) {
|
|
1439
|
+
return branch;
|
|
1440
|
+
}
|
|
1441
|
+
if (this.hasRemoteTrackingBranch(branch)) {
|
|
1442
|
+
return `origin/${branch}`;
|
|
1373
1443
|
}
|
|
1374
1444
|
return null;
|
|
1375
1445
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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;
|
|
1380
1455
|
}
|
|
1381
|
-
import_node_fs2.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
|
|
1382
1456
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
+
}
|
|
1385
1467
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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
|
+
}
|
|
1391
1478
|
}
|
|
1392
|
-
|
|
1393
|
-
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
|
+
});
|
|
1394
1486
|
}
|
|
1395
|
-
|
|
1487
|
+
submitReview(prIdentifier, body, event) {
|
|
1396
1488
|
try {
|
|
1397
|
-
|
|
1398
|
-
|
|
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
|
+
}));
|
|
1399
1537
|
} catch {
|
|
1400
|
-
|
|
1538
|
+
this.log("Failed to list Locus PRs", "warn");
|
|
1539
|
+
return [];
|
|
1401
1540
|
}
|
|
1402
1541
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
}
|
|
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;
|
|
1410
1553
|
}
|
|
1411
|
-
return hashes;
|
|
1412
1554
|
}
|
|
1413
|
-
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
const existingSet = new Set(existingFiles);
|
|
1417
|
-
const currentSet = new Set(currentFiles);
|
|
1418
|
-
const added = currentFiles.filter((f) => !existingSet.has(f));
|
|
1419
|
-
const deleted = existingFiles.filter((f) => !currentSet.has(f));
|
|
1420
|
-
const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
|
|
1421
|
-
return { added, deleted, modified };
|
|
1555
|
+
listUnreviewedLocusPrs() {
|
|
1556
|
+
const allPrs = this.listLocusPrs();
|
|
1557
|
+
return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
|
|
1422
1558
|
}
|
|
1423
|
-
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
|
|
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("");
|
|
1427
1566
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1567
|
+
if (task.acceptanceChecklist?.length > 0) {
|
|
1568
|
+
sections.push("## Acceptance Criteria");
|
|
1569
|
+
for (const item of task.acceptanceChecklist) {
|
|
1570
|
+
sections.push(`- [ ] ${item.text}`);
|
|
1432
1571
|
}
|
|
1572
|
+
sections.push("");
|
|
1433
1573
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
if (mergedSymbols[symbol]) {
|
|
1439
|
-
mergedSymbols[symbol] = [
|
|
1440
|
-
...new Set([...mergedSymbols[symbol], ...paths])
|
|
1441
|
-
];
|
|
1442
|
-
} else {
|
|
1443
|
-
mergedSymbols[symbol] = paths;
|
|
1444
|
-
}
|
|
1574
|
+
if (summary) {
|
|
1575
|
+
sections.push("## Agent Summary");
|
|
1576
|
+
sections.push(summary);
|
|
1577
|
+
sections.push("");
|
|
1445
1578
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
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;
|
|
1455
1587
|
}
|
|
1456
1588
|
}
|
|
1457
1589
|
|
|
1458
|
-
// src/
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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");
|
|
1465
1599
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1600
|
+
readContext() {
|
|
1601
|
+
if (!import_node_fs2.existsSync(this.contextPath)) {
|
|
1602
|
+
return "";
|
|
1603
|
+
}
|
|
1604
|
+
return import_node_fs2.readFileSync(this.contextPath, "utf-8");
|
|
1605
|
+
}
|
|
1606
|
+
readProgress() {
|
|
1607
|
+
if (!import_node_fs2.existsSync(this.progressPath)) {
|
|
1608
|
+
return "";
|
|
1609
|
+
}
|
|
1610
|
+
return import_node_fs2.readFileSync(this.progressPath, "utf-8");
|
|
1611
|
+
}
|
|
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;
|
|
1651
|
+
}
|
|
1652
|
+
if (event.details) {
|
|
1653
|
+
entry += `
|
|
1654
|
+
${event.details}`;
|
|
1655
|
+
}
|
|
1656
|
+
const updated = existing ? `${existing}
|
|
1657
|
+
${entry}` : `# Project Progress
|
|
1473
1658
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1659
|
+
${entry}`;
|
|
1660
|
+
import_node_fs2.writeFileSync(this.progressPath, updated);
|
|
1661
|
+
}
|
|
1662
|
+
getFullContext() {
|
|
1663
|
+
const context = this.readContext();
|
|
1664
|
+
const progress = this.readProgress();
|
|
1665
|
+
const parts = [];
|
|
1666
|
+
if (context.trim()) {
|
|
1667
|
+
parts.push(context.trim());
|
|
1668
|
+
}
|
|
1669
|
+
if (progress.trim()) {
|
|
1670
|
+
parts.push(progress.trim());
|
|
1671
|
+
}
|
|
1672
|
+
return parts.join(`
|
|
1477
1673
|
|
|
1478
|
-
|
|
1479
|
-
${tree}
|
|
1674
|
+
---
|
|
1480
1675
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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}
|
|
1684
|
+
|
|
1685
|
+
## Mission
|
|
1686
|
+
${info.mission}
|
|
1687
|
+
|
|
1688
|
+
## Tech Stack
|
|
1689
|
+
${techStackList}
|
|
1690
|
+
|
|
1691
|
+
## Architecture
|
|
1692
|
+
<!-- Describe your high-level architecture here -->
|
|
1693
|
+
|
|
1694
|
+
## Key Decisions
|
|
1695
|
+
<!-- Document important technical decisions and their rationale -->
|
|
1696
|
+
|
|
1697
|
+
## Feature Areas
|
|
1698
|
+
<!-- List your main feature areas and their status -->
|
|
1699
|
+
`;
|
|
1700
|
+
const progressContent = `# Project Progress
|
|
1701
|
+
|
|
1702
|
+
No sprints started yet.
|
|
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 });
|
|
1497
1714
|
}
|
|
1498
1715
|
}
|
|
1499
1716
|
}
|
|
1500
1717
|
|
|
1501
|
-
// src/
|
|
1718
|
+
// src/worktree/worktree-manager.ts
|
|
1719
|
+
var import_node_child_process5 = require("node:child_process");
|
|
1502
1720
|
var import_node_fs3 = require("node:fs");
|
|
1503
1721
|
var import_node_path5 = require("node:path");
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1722
|
+
|
|
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
|
+
});
|
|
1508
1743
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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");
|
|
1760
|
+
try {
|
|
1761
|
+
this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
|
|
1762
|
+
} catch {
|
|
1763
|
+
import_node_fs3.rmSync(worktreePath, { recursive: true, force: true });
|
|
1764
|
+
this.git("worktree prune", this.projectPath);
|
|
1765
|
+
}
|
|
1513
1766
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
const
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
if (doc.groupId === artifactsGroupId) {
|
|
1522
|
-
continue;
|
|
1523
|
-
}
|
|
1524
|
-
const groupName = groupMap.get(doc.groupId || "") || "General";
|
|
1525
|
-
const groupDir = import_node_path5.join(documentsDir, groupName);
|
|
1526
|
-
if (!import_node_fs3.existsSync(groupDir)) {
|
|
1527
|
-
import_node_fs3.mkdirSync(groupDir, { recursive: true });
|
|
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.`);
|
|
1528
1774
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1775
|
+
this.log(`Removing existing worktree for branch: ${branch} (${worktreePath2})`, "warn");
|
|
1776
|
+
this.remove(worktreePath2, false);
|
|
1777
|
+
}
|
|
1778
|
+
try {
|
|
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(`
|
|
1804
|
+
|
|
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)";
|
|
1534
1829
|
}
|
|
1535
1830
|
}
|
|
1536
|
-
if (
|
|
1537
|
-
|
|
1831
|
+
if (import_node_path5.resolve(path) === this.projectPath) {
|
|
1832
|
+
isMain = true;
|
|
1833
|
+
}
|
|
1834
|
+
if (path) {
|
|
1835
|
+
worktrees.push({ path, branch, head, isMain, isPrunable });
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
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");
|
|
1538
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;
|
|
1539
1917
|
} catch (error) {
|
|
1540
|
-
this.
|
|
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 {}
|
|
1541
1980
|
}
|
|
1542
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
|
+
}
|
|
1543
2000
|
}
|
|
1544
2001
|
|
|
1545
2002
|
// src/core/prompt-builder.ts
|
|
1546
2003
|
var import_node_fs4 = require("node:fs");
|
|
1547
|
-
var import_node_os2 = require("node:os");
|
|
1548
2004
|
var import_node_path6 = require("node:path");
|
|
1549
2005
|
var import_shared2 = require("@locusai/shared");
|
|
1550
2006
|
class PromptBuilder {
|
|
@@ -1613,11 +2069,12 @@ ${fallback}
|
|
|
1613
2069
|
if (serverContext) {
|
|
1614
2070
|
prompt += `## Project Context (Server)
|
|
1615
2071
|
`;
|
|
1616
|
-
|
|
1617
|
-
|
|
2072
|
+
const project = serverContext.project;
|
|
2073
|
+
if (project) {
|
|
2074
|
+
prompt += `- Project: ${project.name || "Unknown"}
|
|
1618
2075
|
`;
|
|
1619
|
-
if (!hasLocalContext &&
|
|
1620
|
-
prompt += `- Tech Stack: ${
|
|
2076
|
+
if (!hasLocalContext && project.techStack?.length) {
|
|
2077
|
+
prompt += `- Tech Stack: ${project.techStack.join(", ")}
|
|
1621
2078
|
`;
|
|
1622
2079
|
}
|
|
1623
2080
|
}
|
|
@@ -1630,12 +2087,11 @@ ${serverContext.context}
|
|
|
1630
2087
|
`;
|
|
1631
2088
|
}
|
|
1632
2089
|
prompt += this.getProjectStructure();
|
|
1633
|
-
prompt += this.getSkillsInfo();
|
|
1634
2090
|
prompt += `## Project Knowledge Base
|
|
1635
2091
|
`;
|
|
1636
2092
|
prompt += `You have access to the following documentation directories for context:
|
|
1637
2093
|
`;
|
|
1638
|
-
prompt += `- Artifacts: \`.locus/artifacts\`
|
|
2094
|
+
prompt += `- Artifacts: \`.locus/artifacts\`
|
|
1639
2095
|
`;
|
|
1640
2096
|
prompt += `- Documents: \`.locus/documents\`
|
|
1641
2097
|
`;
|
|
@@ -1693,11 +2149,9 @@ ${comment.text}
|
|
|
1693
2149
|
}
|
|
1694
2150
|
}
|
|
1695
2151
|
prompt += `## Instructions
|
|
1696
|
-
1. Complete this task.
|
|
2152
|
+
1. Complete this task.
|
|
1697
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.
|
|
1698
|
-
3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...)
|
|
1699
|
-
4. When finished successfully, output: <promise>COMPLETE</promise>
|
|
1700
|
-
`;
|
|
2154
|
+
3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
|
|
1701
2155
|
return prompt;
|
|
1702
2156
|
}
|
|
1703
2157
|
async buildGenericPrompt(query) {
|
|
@@ -1744,7 +2198,6 @@ ${fallback}
|
|
|
1744
2198
|
}
|
|
1745
2199
|
}
|
|
1746
2200
|
prompt += this.getProjectStructure();
|
|
1747
|
-
prompt += this.getSkillsInfo();
|
|
1748
2201
|
prompt += `## Project Knowledge Base
|
|
1749
2202
|
`;
|
|
1750
2203
|
prompt += `You have access to the following documentation directories for context:
|
|
@@ -1765,9 +2218,7 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
1765
2218
|
}
|
|
1766
2219
|
prompt += `## Instructions
|
|
1767
2220
|
1. Execute the prompt based on the provided project context.
|
|
1768
|
-
2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...)
|
|
1769
|
-
3. When finished successfully, output: <promise>COMPLETE</promise>
|
|
1770
|
-
`;
|
|
2221
|
+
2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
|
|
1771
2222
|
return prompt;
|
|
1772
2223
|
}
|
|
1773
2224
|
getProjectConfig() {
|
|
@@ -1823,55 +2274,6 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
1823
2274
|
return "";
|
|
1824
2275
|
}
|
|
1825
2276
|
}
|
|
1826
|
-
getSkillsInfo() {
|
|
1827
|
-
const projectSkillsDirs = [
|
|
1828
|
-
LOCUS_CONFIG.agentSkillsDir,
|
|
1829
|
-
".cursor/skills",
|
|
1830
|
-
".claude/skills",
|
|
1831
|
-
".codex/skills",
|
|
1832
|
-
".gemini/skills"
|
|
1833
|
-
];
|
|
1834
|
-
const globalHome = import_node_os2.homedir();
|
|
1835
|
-
const globalSkillsDirs = [
|
|
1836
|
-
import_node_path6.join(globalHome, ".cursor/skills"),
|
|
1837
|
-
import_node_path6.join(globalHome, ".claude/skills"),
|
|
1838
|
-
import_node_path6.join(globalHome, ".codex/skills"),
|
|
1839
|
-
import_node_path6.join(globalHome, ".gemini/skills")
|
|
1840
|
-
];
|
|
1841
|
-
const allSkillNames = new Set;
|
|
1842
|
-
for (const relativePath of projectSkillsDirs) {
|
|
1843
|
-
const fullPath = import_node_path6.join(this.projectPath, relativePath);
|
|
1844
|
-
this.scanSkillsInDirectory(fullPath, allSkillNames);
|
|
1845
|
-
}
|
|
1846
|
-
for (const fullPath of globalSkillsDirs) {
|
|
1847
|
-
this.scanSkillsInDirectory(fullPath, allSkillNames);
|
|
1848
|
-
}
|
|
1849
|
-
const uniqueSkills = Array.from(allSkillNames).sort();
|
|
1850
|
-
if (uniqueSkills.length === 0)
|
|
1851
|
-
return "";
|
|
1852
|
-
return `## Available Agent Skills
|
|
1853
|
-
` + `The project has the following specialized skills available (from project or global locations):
|
|
1854
|
-
` + uniqueSkills.map((s) => `- ${s}`).join(`
|
|
1855
|
-
`) + `
|
|
1856
|
-
|
|
1857
|
-
`;
|
|
1858
|
-
}
|
|
1859
|
-
scanSkillsInDirectory(dirPath, skillSet) {
|
|
1860
|
-
if (!import_node_fs4.existsSync(dirPath))
|
|
1861
|
-
return;
|
|
1862
|
-
try {
|
|
1863
|
-
const entries = import_node_fs4.readdirSync(dirPath).filter((name) => {
|
|
1864
|
-
try {
|
|
1865
|
-
return import_node_fs4.statSync(import_node_path6.join(dirPath, name)).isDirectory();
|
|
1866
|
-
} catch {
|
|
1867
|
-
return false;
|
|
1868
|
-
}
|
|
1869
|
-
});
|
|
1870
|
-
for (const entry of entries) {
|
|
1871
|
-
skillSet.add(entry);
|
|
1872
|
-
}
|
|
1873
|
-
} catch {}
|
|
1874
|
-
}
|
|
1875
2277
|
roleToText(role) {
|
|
1876
2278
|
if (!role) {
|
|
1877
2279
|
return null;
|
|
@@ -1901,42 +2303,15 @@ class TaskExecutor {
|
|
|
1901
2303
|
this.deps = deps;
|
|
1902
2304
|
this.promptBuilder = new PromptBuilder(deps.projectPath);
|
|
1903
2305
|
}
|
|
1904
|
-
async execute(task
|
|
2306
|
+
async execute(task) {
|
|
1905
2307
|
this.deps.log(`Executing: ${task.title}`, "info");
|
|
1906
|
-
const basePrompt = await this.promptBuilder.build(task
|
|
1907
|
-
taskContext: context
|
|
1908
|
-
});
|
|
2308
|
+
const basePrompt = await this.promptBuilder.build(task);
|
|
1909
2309
|
try {
|
|
1910
|
-
let plan = null;
|
|
1911
|
-
this.deps.log("Phase 1: Planning (CLI)...", "info");
|
|
1912
|
-
const planningPrompt = `${basePrompt}
|
|
1913
|
-
|
|
1914
|
-
## Phase 1: Planning
|
|
1915
|
-
Analyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`;
|
|
1916
|
-
plan = await this.deps.aiRunner.run(planningPrompt, true);
|
|
1917
2310
|
this.deps.log("Starting Execution...", "info");
|
|
1918
|
-
|
|
1919
|
-
if (plan != null) {
|
|
1920
|
-
executionPrompt += `
|
|
1921
|
-
|
|
1922
|
-
## Phase 2: Execution
|
|
1923
|
-
Based on the plan, execute the task:
|
|
1924
|
-
|
|
1925
|
-
${plan}`;
|
|
1926
|
-
} else {
|
|
1927
|
-
executionPrompt += `
|
|
1928
|
-
|
|
1929
|
-
## Execution
|
|
1930
|
-
Execute the task directly.`;
|
|
1931
|
-
}
|
|
1932
|
-
executionPrompt += `
|
|
1933
|
-
|
|
1934
|
-
When finished, output: <promise>COMPLETE</promise>`;
|
|
1935
|
-
const output = await this.deps.aiRunner.run(executionPrompt);
|
|
1936
|
-
const success = output.includes("<promise>COMPLETE</promise>");
|
|
2311
|
+
await this.deps.aiRunner.run(basePrompt);
|
|
1937
2312
|
return {
|
|
1938
|
-
success,
|
|
1939
|
-
summary:
|
|
2313
|
+
success: true,
|
|
2314
|
+
summary: "Task completed by the agent"
|
|
1940
2315
|
};
|
|
1941
2316
|
} catch (error) {
|
|
1942
2317
|
return { success: false, summary: `Error: ${error}` };
|
|
@@ -1960,14 +2335,17 @@ class AgentWorker {
|
|
|
1960
2335
|
config;
|
|
1961
2336
|
client;
|
|
1962
2337
|
aiRunner;
|
|
1963
|
-
indexerService;
|
|
1964
|
-
documentFetcher;
|
|
1965
2338
|
taskExecutor;
|
|
1966
|
-
|
|
1967
|
-
|
|
2339
|
+
knowledgeBase;
|
|
2340
|
+
worktreeManager = null;
|
|
2341
|
+
prService = null;
|
|
1968
2342
|
maxTasks = 50;
|
|
1969
2343
|
tasksCompleted = 0;
|
|
1970
|
-
|
|
2344
|
+
heartbeatInterval = null;
|
|
2345
|
+
currentTaskId = null;
|
|
2346
|
+
currentWorktreePath = null;
|
|
2347
|
+
postCleanupDelayMs = 5000;
|
|
2348
|
+
ghUsername = null;
|
|
1971
2349
|
constructor(config) {
|
|
1972
2350
|
this.config = config;
|
|
1973
2351
|
const projectPath = config.projectPath || process.cwd();
|
|
@@ -1982,30 +2360,47 @@ class AgentWorker {
|
|
|
1982
2360
|
}
|
|
1983
2361
|
});
|
|
1984
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
|
+
}
|
|
1985
2376
|
const provider = config.provider ?? PROVIDER.CLAUDE;
|
|
1986
2377
|
this.aiRunner = createAiRunner(provider, {
|
|
1987
2378
|
projectPath,
|
|
1988
2379
|
model: config.model,
|
|
1989
2380
|
log
|
|
1990
2381
|
});
|
|
1991
|
-
this.
|
|
1992
|
-
aiRunner: this.aiRunner,
|
|
1993
|
-
projectPath,
|
|
1994
|
-
log
|
|
1995
|
-
});
|
|
1996
|
-
this.documentFetcher = new DocumentFetcher({
|
|
1997
|
-
client: this.client,
|
|
1998
|
-
workspaceId: config.workspaceId,
|
|
1999
|
-
projectPath,
|
|
2000
|
-
log
|
|
2001
|
-
});
|
|
2002
|
-
this.taskExecutor = new TaskExecutor({
|
|
2382
|
+
this.taskExecutor = new TaskExecutor({
|
|
2003
2383
|
aiRunner: this.aiRunner,
|
|
2004
2384
|
projectPath,
|
|
2005
2385
|
log
|
|
2006
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
|
+
}
|
|
2007
2396
|
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
2008
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
|
+
}
|
|
2009
2404
|
}
|
|
2010
2405
|
log(message, level = "info") {
|
|
2011
2406
|
const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
@@ -2029,87 +2424,984 @@ class AgentWorker {
|
|
|
2029
2424
|
}
|
|
2030
2425
|
}
|
|
2031
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
|
+
}
|
|
2032
2488
|
try {
|
|
2033
|
-
const
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
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");
|
|
2038
2574
|
}
|
|
2575
|
+
this.currentWorktreePath = null;
|
|
2039
2576
|
}
|
|
2040
2577
|
async executeTask(task) {
|
|
2041
2578
|
const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
|
|
2042
|
-
|
|
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) {
|
|
2043
2631
|
try {
|
|
2044
|
-
|
|
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
|
+
}
|
|
2045
2640
|
} catch (err) {
|
|
2046
|
-
this.log(`Failed to
|
|
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;
|
|
2047
2654
|
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
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));
|
|
2051
2666
|
}
|
|
2052
2667
|
async run() {
|
|
2053
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();
|
|
2054
2679
|
const sprint = await this.getActiveSprint();
|
|
2055
2680
|
if (sprint) {
|
|
2056
|
-
this.log(`Active sprint found: ${sprint.name}
|
|
2057
|
-
try {
|
|
2058
|
-
await this.client.sprints.triggerAIPlanning(sprint.id, this.config.workspaceId);
|
|
2059
|
-
this.log(`Sprint plan sync checked on server.`, "success");
|
|
2060
|
-
} catch (err) {
|
|
2061
|
-
this.log(`Sprint planning sync failed (non-critical): ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
2062
|
-
}
|
|
2681
|
+
this.log(`Active sprint found: ${sprint.name}`, "info");
|
|
2063
2682
|
} else {
|
|
2064
|
-
this.log("No active sprint found
|
|
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
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
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
|
+
}
|
|
3070
|
+
}
|
|
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
|
+
}
|
|
3082
|
+
|
|
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);
|
|
3090
|
+
}
|
|
3091
|
+
async reindex(force = false) {
|
|
3092
|
+
try {
|
|
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]);
|
|
3111
|
+
}
|
|
3112
|
+
return { symbols: {}, responsibilities: {}, lastIndexed: "" };
|
|
3113
|
+
}, force);
|
|
3114
|
+
if (index === null) {
|
|
3115
|
+
this.deps.log("No changes detected, skipping reindex", "info");
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
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
|
+
}
|
|
3123
|
+
}
|
|
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 });
|
|
3137
|
+
}
|
|
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;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
// src/agent/review-service.ts
|
|
3170
|
+
var import_node_child_process6 = require("node:child_process");
|
|
3171
|
+
|
|
3172
|
+
class ReviewService {
|
|
3173
|
+
deps;
|
|
3174
|
+
constructor(deps) {
|
|
3175
|
+
this.deps = deps;
|
|
3176
|
+
}
|
|
3177
|
+
async reviewStagedChanges(sprint) {
|
|
3178
|
+
const { projectPath, log } = this.deps;
|
|
3179
|
+
try {
|
|
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;
|
|
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;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
// src/agent/reviewer-worker.ts
|
|
3228
|
+
function resolveProvider2(value) {
|
|
3229
|
+
if (!value || value.startsWith("--"))
|
|
3230
|
+
return PROVIDER.CLAUDE;
|
|
3231
|
+
if (value === PROVIDER.CLAUDE || value === PROVIDER.CODEX)
|
|
3232
|
+
return value;
|
|
3233
|
+
return PROVIDER.CLAUDE;
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
class ReviewerWorker {
|
|
3237
|
+
config;
|
|
3238
|
+
client;
|
|
3239
|
+
aiRunner;
|
|
3240
|
+
prService;
|
|
3241
|
+
knowledgeBase;
|
|
3242
|
+
heartbeatInterval = null;
|
|
3243
|
+
currentTaskId = null;
|
|
3244
|
+
maxReviews = 50;
|
|
3245
|
+
reviewsCompleted = 0;
|
|
3246
|
+
constructor(config) {
|
|
3247
|
+
this.config = config;
|
|
3248
|
+
const projectPath = config.projectPath || process.cwd();
|
|
3249
|
+
this.client = new LocusClient({
|
|
3250
|
+
baseUrl: config.apiBase,
|
|
3251
|
+
token: config.apiKey,
|
|
3252
|
+
retryOptions: {
|
|
3253
|
+
maxRetries: 3,
|
|
3254
|
+
initialDelay: 1000,
|
|
3255
|
+
maxDelay: 5000,
|
|
3256
|
+
factor: 2
|
|
3257
|
+
}
|
|
3258
|
+
});
|
|
3259
|
+
const log = this.log.bind(this);
|
|
3260
|
+
const provider = config.provider ?? PROVIDER.CLAUDE;
|
|
3261
|
+
this.aiRunner = createAiRunner(provider, {
|
|
3262
|
+
projectPath,
|
|
3263
|
+
model: config.model,
|
|
3264
|
+
log
|
|
3265
|
+
});
|
|
3266
|
+
this.prService = new PrService(projectPath, log);
|
|
3267
|
+
this.knowledgeBase = new KnowledgeBase(projectPath);
|
|
3268
|
+
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
3269
|
+
this.log(`Reviewer agent using ${providerLabel} CLI`, "info");
|
|
3270
|
+
}
|
|
3271
|
+
log(message, level = "info") {
|
|
3272
|
+
const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
3273
|
+
const colorFn = {
|
|
3274
|
+
info: c.cyan,
|
|
3275
|
+
success: c.green,
|
|
3276
|
+
warn: c.yellow,
|
|
3277
|
+
error: c.red
|
|
3278
|
+
}[level];
|
|
3279
|
+
const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
|
|
3280
|
+
console.log(`${c.dim(`[${timestamp}]`)} ${c.bold(`[R:${this.config.agentId.slice(-8)}]`)} ${colorFn(`${prefix} ${message}`)}`);
|
|
3281
|
+
}
|
|
3282
|
+
getNextUnreviewedPr() {
|
|
3283
|
+
const prs = this.prService.listUnreviewedLocusPrs();
|
|
3284
|
+
return prs.length > 0 ? prs[0] : null;
|
|
3285
|
+
}
|
|
3286
|
+
async reviewPr(pr) {
|
|
3287
|
+
const prNumber = String(pr.number);
|
|
3288
|
+
this.log(`Reviewing PR #${prNumber}: ${pr.title}`, "info");
|
|
3289
|
+
let diff;
|
|
3290
|
+
try {
|
|
3291
|
+
diff = this.prService.getPrDiff(prNumber);
|
|
3292
|
+
} catch (err) {
|
|
3293
|
+
return {
|
|
3294
|
+
reviewed: false,
|
|
3295
|
+
approved: false,
|
|
3296
|
+
summary: `Failed to get PR diff: ${err instanceof Error ? err.message : String(err)}`
|
|
3297
|
+
};
|
|
3298
|
+
}
|
|
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();
|
|
3330
|
+
try {
|
|
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");
|
|
3337
|
+
} catch (err) {
|
|
3338
|
+
this.log(`Failed to post PR review: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
3339
|
+
}
|
|
3340
|
+
return { reviewed: true, approved, summary };
|
|
3341
|
+
}
|
|
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;
|
|
3350
|
+
}
|
|
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);
|
|
2065
3362
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
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));
|
|
2076
3377
|
continue;
|
|
2077
3378
|
}
|
|
2078
|
-
this.
|
|
2079
|
-
this.
|
|
2080
|
-
const result = await this.
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
2092
|
-
author: this.config.agentId,
|
|
2093
|
-
text: `✅ ${result.summary}`
|
|
2094
|
-
});
|
|
2095
|
-
this.tasksCompleted++;
|
|
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++;
|
|
2096
3392
|
} else {
|
|
2097
|
-
this.log(`
|
|
2098
|
-
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
2099
|
-
status: "BACKLOG",
|
|
2100
|
-
assignedTo: null
|
|
2101
|
-
});
|
|
2102
|
-
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
2103
|
-
author: this.config.agentId,
|
|
2104
|
-
text: `❌ ${result.summary}`
|
|
2105
|
-
});
|
|
3393
|
+
this.log(`Review skipped: ${result.summary}`, "warn");
|
|
2106
3394
|
}
|
|
3395
|
+
this.currentTaskId = null;
|
|
2107
3396
|
}
|
|
3397
|
+
this.stopHeartbeat();
|
|
3398
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
|
|
2108
3399
|
process.exit(0);
|
|
2109
3400
|
}
|
|
2110
3401
|
}
|
|
2111
|
-
|
|
2112
|
-
|
|
3402
|
+
var reviewerEntrypoint = process.argv[1]?.split(/[\\/]/).pop();
|
|
3403
|
+
if (reviewerEntrypoint === "reviewer-worker.js" || reviewerEntrypoint === "reviewer-worker.ts") {
|
|
3404
|
+
process.title = "locus-reviewer";
|
|
2113
3405
|
const args = process.argv.slice(2);
|
|
2114
3406
|
const config = {};
|
|
2115
3407
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -2132,59 +3424,19 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
|
|
|
2132
3424
|
const value = args[i + 1];
|
|
2133
3425
|
if (value && !value.startsWith("--"))
|
|
2134
3426
|
i++;
|
|
2135
|
-
config.provider =
|
|
3427
|
+
config.provider = resolveProvider2(value);
|
|
2136
3428
|
}
|
|
2137
3429
|
}
|
|
2138
3430
|
if (!config.agentId || !config.workspaceId || !config.apiBase || !config.apiKey || !config.projectPath) {
|
|
2139
3431
|
console.error("Missing required arguments");
|
|
2140
3432
|
process.exit(1);
|
|
2141
3433
|
}
|
|
2142
|
-
const worker = new
|
|
3434
|
+
const worker = new ReviewerWorker(config);
|
|
2143
3435
|
worker.run().catch((err) => {
|
|
2144
|
-
console.error("Fatal
|
|
3436
|
+
console.error("Fatal reviewer error:", err);
|
|
2145
3437
|
process.exit(1);
|
|
2146
3438
|
});
|
|
2147
3439
|
}
|
|
2148
|
-
|
|
2149
|
-
// src/index-node.ts
|
|
2150
|
-
var exports_index_node = {};
|
|
2151
|
-
__export(exports_index_node, {
|
|
2152
|
-
getLocusPath: () => getLocusPath,
|
|
2153
|
-
getAgentArtifactsPath: () => getAgentArtifactsPath,
|
|
2154
|
-
createAiRunner: () => createAiRunner,
|
|
2155
|
-
c: () => c,
|
|
2156
|
-
WorkspacesModule: () => WorkspacesModule,
|
|
2157
|
-
TasksModule: () => TasksModule,
|
|
2158
|
-
TaskExecutor: () => TaskExecutor,
|
|
2159
|
-
SprintsModule: () => SprintsModule,
|
|
2160
|
-
PromptBuilder: () => PromptBuilder,
|
|
2161
|
-
PROVIDER: () => PROVIDER,
|
|
2162
|
-
OrganizationsModule: () => OrganizationsModule,
|
|
2163
|
-
LocusEvent: () => LocusEvent,
|
|
2164
|
-
LocusEmitter: () => LocusEmitter,
|
|
2165
|
-
LocusClient: () => LocusClient,
|
|
2166
|
-
LOCUS_GITIGNORE_PATTERNS: () => LOCUS_GITIGNORE_PATTERNS,
|
|
2167
|
-
LOCUS_CONFIG: () => LOCUS_CONFIG,
|
|
2168
|
-
InvitationsModule: () => InvitationsModule,
|
|
2169
|
-
HistoryManager: () => HistoryManager,
|
|
2170
|
-
ExecSession: () => ExecSession,
|
|
2171
|
-
ExecEventType: () => ExecEventType,
|
|
2172
|
-
ExecEventEmitter: () => ExecEventEmitter,
|
|
2173
|
-
DocumentFetcher: () => DocumentFetcher,
|
|
2174
|
-
DocsModule: () => DocsModule,
|
|
2175
|
-
DEFAULT_MODEL: () => DEFAULT_MODEL,
|
|
2176
|
-
ContextTracker: () => ContextTracker,
|
|
2177
|
-
CodexRunner: () => CodexRunner,
|
|
2178
|
-
CodebaseIndexerService: () => CodebaseIndexerService,
|
|
2179
|
-
CodebaseIndexer: () => CodebaseIndexer,
|
|
2180
|
-
ClaudeRunner: () => ClaudeRunner,
|
|
2181
|
-
CiModule: () => CiModule,
|
|
2182
|
-
AuthModule: () => AuthModule,
|
|
2183
|
-
AgentWorker: () => AgentWorker,
|
|
2184
|
-
AgentOrchestrator: () => AgentOrchestrator,
|
|
2185
|
-
AIModule: () => AIModule
|
|
2186
|
-
});
|
|
2187
|
-
module.exports = __toCommonJS(exports_index_node);
|
|
2188
3440
|
// src/exec/context-tracker.ts
|
|
2189
3441
|
var REFERENCE_ALIASES = {
|
|
2190
3442
|
plan: ["the plan", "sprint plan", "project plan", "implementation plan"],
|
|
@@ -2598,8 +3850,8 @@ class ExecEventEmitter {
|
|
|
2598
3850
|
}
|
|
2599
3851
|
}
|
|
2600
3852
|
// src/exec/history-manager.ts
|
|
2601
|
-
var
|
|
2602
|
-
var
|
|
3853
|
+
var import_node_fs7 = require("node:fs");
|
|
3854
|
+
var import_node_path9 = require("node:path");
|
|
2603
3855
|
var DEFAULT_MAX_SESSIONS = 30;
|
|
2604
3856
|
function generateSessionId2() {
|
|
2605
3857
|
const timestamp = Date.now().toString(36);
|
|
@@ -2611,30 +3863,30 @@ class HistoryManager {
|
|
|
2611
3863
|
historyDir;
|
|
2612
3864
|
maxSessions;
|
|
2613
3865
|
constructor(projectPath, options) {
|
|
2614
|
-
this.historyDir = options?.historyDir ??
|
|
3866
|
+
this.historyDir = options?.historyDir ?? import_node_path9.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
|
|
2615
3867
|
this.maxSessions = options?.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
2616
3868
|
this.ensureHistoryDir();
|
|
2617
3869
|
}
|
|
2618
3870
|
ensureHistoryDir() {
|
|
2619
|
-
if (!
|
|
2620
|
-
|
|
3871
|
+
if (!import_node_fs7.existsSync(this.historyDir)) {
|
|
3872
|
+
import_node_fs7.mkdirSync(this.historyDir, { recursive: true });
|
|
2621
3873
|
}
|
|
2622
3874
|
}
|
|
2623
3875
|
getSessionPath(sessionId) {
|
|
2624
|
-
return
|
|
3876
|
+
return import_node_path9.join(this.historyDir, `${sessionId}.json`);
|
|
2625
3877
|
}
|
|
2626
3878
|
saveSession(session) {
|
|
2627
3879
|
const filePath = this.getSessionPath(session.id);
|
|
2628
3880
|
session.updatedAt = Date.now();
|
|
2629
|
-
|
|
3881
|
+
import_node_fs7.writeFileSync(filePath, JSON.stringify(session, null, 2), "utf-8");
|
|
2630
3882
|
}
|
|
2631
3883
|
loadSession(sessionId) {
|
|
2632
3884
|
const filePath = this.getSessionPath(sessionId);
|
|
2633
|
-
if (!
|
|
3885
|
+
if (!import_node_fs7.existsSync(filePath)) {
|
|
2634
3886
|
return null;
|
|
2635
3887
|
}
|
|
2636
3888
|
try {
|
|
2637
|
-
const content =
|
|
3889
|
+
const content = import_node_fs7.readFileSync(filePath, "utf-8");
|
|
2638
3890
|
return JSON.parse(content);
|
|
2639
3891
|
} catch {
|
|
2640
3892
|
return null;
|
|
@@ -2642,18 +3894,18 @@ class HistoryManager {
|
|
|
2642
3894
|
}
|
|
2643
3895
|
deleteSession(sessionId) {
|
|
2644
3896
|
const filePath = this.getSessionPath(sessionId);
|
|
2645
|
-
if (!
|
|
3897
|
+
if (!import_node_fs7.existsSync(filePath)) {
|
|
2646
3898
|
return false;
|
|
2647
3899
|
}
|
|
2648
3900
|
try {
|
|
2649
|
-
|
|
3901
|
+
import_node_fs7.rmSync(filePath);
|
|
2650
3902
|
return true;
|
|
2651
3903
|
} catch {
|
|
2652
3904
|
return false;
|
|
2653
3905
|
}
|
|
2654
3906
|
}
|
|
2655
3907
|
listSessions(options) {
|
|
2656
|
-
const files =
|
|
3908
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
2657
3909
|
let sessions = [];
|
|
2658
3910
|
for (const file of files) {
|
|
2659
3911
|
if (file.endsWith(".json")) {
|
|
@@ -2726,11 +3978,11 @@ class HistoryManager {
|
|
|
2726
3978
|
return deleted;
|
|
2727
3979
|
}
|
|
2728
3980
|
getSessionCount() {
|
|
2729
|
-
const files =
|
|
3981
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
2730
3982
|
return files.filter((f) => f.endsWith(".json")).length;
|
|
2731
3983
|
}
|
|
2732
3984
|
sessionExists(sessionId) {
|
|
2733
|
-
return
|
|
3985
|
+
return import_node_fs7.existsSync(this.getSessionPath(sessionId));
|
|
2734
3986
|
}
|
|
2735
3987
|
findSessionByPartialId(partialId) {
|
|
2736
3988
|
const sessions = this.listSessions();
|
|
@@ -2744,12 +3996,12 @@ class HistoryManager {
|
|
|
2744
3996
|
return this.historyDir;
|
|
2745
3997
|
}
|
|
2746
3998
|
clearAllSessions() {
|
|
2747
|
-
const files =
|
|
3999
|
+
const files = import_node_fs7.readdirSync(this.historyDir);
|
|
2748
4000
|
let deleted = 0;
|
|
2749
4001
|
for (const file of files) {
|
|
2750
4002
|
if (file.endsWith(".json")) {
|
|
2751
4003
|
try {
|
|
2752
|
-
|
|
4004
|
+
import_node_fs7.rmSync(import_node_path9.join(this.historyDir, file));
|
|
2753
4005
|
deleted++;
|
|
2754
4006
|
} catch {}
|
|
2755
4007
|
}
|
|
@@ -3014,12 +4266,14 @@ ${currentPrompt}`);
|
|
|
3014
4266
|
}
|
|
3015
4267
|
}
|
|
3016
4268
|
// src/orchestrator.ts
|
|
3017
|
-
var
|
|
3018
|
-
var
|
|
3019
|
-
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");
|
|
3020
4272
|
var import_node_url = require("node:url");
|
|
3021
|
-
var
|
|
4273
|
+
var import_shared4 = require("@locusai/shared");
|
|
3022
4274
|
var import_events4 = require("events");
|
|
4275
|
+
var MAX_AGENTS = 5;
|
|
4276
|
+
|
|
3023
4277
|
class AgentOrchestrator extends import_events4.EventEmitter {
|
|
3024
4278
|
client;
|
|
3025
4279
|
config;
|
|
@@ -3027,6 +4281,8 @@ class AgentOrchestrator extends import_events4.EventEmitter {
|
|
|
3027
4281
|
isRunning = false;
|
|
3028
4282
|
processedTasks = new Set;
|
|
3029
4283
|
resolvedSprintId = null;
|
|
4284
|
+
worktreeManager = null;
|
|
4285
|
+
heartbeatInterval = null;
|
|
3030
4286
|
constructor(config) {
|
|
3031
4287
|
super();
|
|
3032
4288
|
this.config = config;
|
|
@@ -3035,6 +4291,15 @@ class AgentOrchestrator extends import_events4.EventEmitter {
|
|
|
3035
4291
|
token: config.apiKey
|
|
3036
4292
|
});
|
|
3037
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
|
+
}
|
|
3038
4303
|
async resolveSprintId() {
|
|
3039
4304
|
if (this.config.sprintId) {
|
|
3040
4305
|
return this.config.sprintId;
|
|
@@ -3078,6 +4343,12 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
|
|
|
3078
4343
|
if (this.resolvedSprintId) {
|
|
3079
4344
|
console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
|
|
3080
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
|
+
}
|
|
3081
4352
|
console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
|
|
3082
4353
|
console.log(c.dim(`----------------------------------------------
|
|
3083
4354
|
`));
|
|
@@ -3086,9 +4357,30 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
|
|
|
3086
4357
|
console.log(c.dim("ℹ No available tasks found in the backlog."));
|
|
3087
4358
|
return;
|
|
3088
4359
|
}
|
|
3089
|
-
|
|
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);
|
|
3090
4383
|
while (this.agents.size > 0 && this.isRunning) {
|
|
3091
|
-
await this.reapAgents();
|
|
3092
4384
|
if (this.agents.size === 0) {
|
|
3093
4385
|
break;
|
|
3094
4386
|
}
|
|
@@ -3097,8 +4389,8 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
|
|
|
3097
4389
|
console.log(`
|
|
3098
4390
|
${c.success("✅ Orchestrator finished")}`);
|
|
3099
4391
|
}
|
|
3100
|
-
async spawnAgent() {
|
|
3101
|
-
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)}`;
|
|
3102
4394
|
const agentState = {
|
|
3103
4395
|
id: agentId,
|
|
3104
4396
|
status: "IDLE",
|
|
@@ -3110,13 +4402,9 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
3110
4402
|
this.agents.set(agentId, agentState);
|
|
3111
4403
|
console.log(`${c.primary("\uD83D\uDE80 Agent started:")} ${c.bold(agentId)}
|
|
3112
4404
|
`);
|
|
3113
|
-
const
|
|
3114
|
-
const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator.ts");
|
|
3115
|
-
const currentModuleDir = import_node_path8.dirname(currentModulePath);
|
|
3116
|
-
potentialPaths.push(import_node_path8.join(currentModuleDir, "agent", "worker.js"), import_node_path8.join(currentModuleDir, "worker.js"), import_node_path8.join(currentModuleDir, "agent", "worker.ts"));
|
|
3117
|
-
const workerPath = potentialPaths.find((p) => import_node_fs6.existsSync(p));
|
|
4405
|
+
const workerPath = this.resolveWorkerPath();
|
|
3118
4406
|
if (!workerPath) {
|
|
3119
|
-
throw new Error(
|
|
4407
|
+
throw new Error("Worker file not found. Make sure the SDK is properly built and installed.");
|
|
3120
4408
|
}
|
|
3121
4409
|
const workerArgs = [
|
|
3122
4410
|
"--agent-id",
|
|
@@ -3139,8 +4427,15 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
3139
4427
|
if (this.resolvedSprintId) {
|
|
3140
4428
|
workerArgs.push("--sprint-id", this.resolvedSprintId);
|
|
3141
4429
|
}
|
|
3142
|
-
|
|
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], {
|
|
3143
4437
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4438
|
+
detached: true,
|
|
3144
4439
|
env: {
|
|
3145
4440
|
...process.env,
|
|
3146
4441
|
FORCE_COLOR: "1",
|
|
@@ -3155,6 +4450,9 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
3155
4450
|
agentState.tasksCompleted = msg.tasksCompleted || 0;
|
|
3156
4451
|
agentState.tasksFailed = msg.tasksFailed || 0;
|
|
3157
4452
|
}
|
|
4453
|
+
if (msg.type === "heartbeat") {
|
|
4454
|
+
agentState.lastHeartbeat = new Date;
|
|
4455
|
+
}
|
|
3158
4456
|
});
|
|
3159
4457
|
agentProcess.stdout?.on("data", (data) => {
|
|
3160
4458
|
process.stdout.write(data.toString());
|
|
@@ -3179,7 +4477,30 @@ ${agentId} finished (exit code: ${code})`);
|
|
|
3179
4477
|
});
|
|
3180
4478
|
this.emit("agent:spawned", { agentId });
|
|
3181
4479
|
}
|
|
3182
|
-
|
|
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
|
+
}
|
|
3183
4504
|
async getAvailableTasks() {
|
|
3184
4505
|
try {
|
|
3185
4506
|
const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
|
|
@@ -3196,10 +4517,10 @@ ${agentId} finished (exit code: ${code})`);
|
|
|
3196
4517
|
try {
|
|
3197
4518
|
const tasks2 = await this.getAvailableTasks();
|
|
3198
4519
|
const priorityOrder = [
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
4520
|
+
import_shared4.TaskPriority.CRITICAL,
|
|
4521
|
+
import_shared4.TaskPriority.HIGH,
|
|
4522
|
+
import_shared4.TaskPriority.MEDIUM,
|
|
4523
|
+
import_shared4.TaskPriority.LOW
|
|
3203
4524
|
];
|
|
3204
4525
|
let task = tasks2.sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority))[0];
|
|
3205
4526
|
if (!task && tasks2.length > 0) {
|
|
@@ -3223,7 +4544,7 @@ ${agentId} finished (exit code: ${code})`);
|
|
|
3223
4544
|
async completeTask(taskId, agentId, summary) {
|
|
3224
4545
|
try {
|
|
3225
4546
|
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
3226
|
-
status:
|
|
4547
|
+
status: import_shared4.TaskStatus.IN_REVIEW
|
|
3227
4548
|
});
|
|
3228
4549
|
if (summary) {
|
|
3229
4550
|
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
@@ -3248,7 +4569,7 @@ ${summary}`
|
|
|
3248
4569
|
async failTask(taskId, agentId, error) {
|
|
3249
4570
|
try {
|
|
3250
4571
|
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
3251
|
-
status:
|
|
4572
|
+
status: import_shared4.TaskStatus.BACKLOG,
|
|
3252
4573
|
assignedTo: null
|
|
3253
4574
|
});
|
|
3254
4575
|
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
@@ -3271,11 +4592,52 @@ ${summary}`
|
|
|
3271
4592
|
await this.cleanup();
|
|
3272
4593
|
this.emit("stopped", { timestamp: new Date });
|
|
3273
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
|
+
}
|
|
3274
4615
|
async cleanup() {
|
|
4616
|
+
if (this.heartbeatInterval) {
|
|
4617
|
+
clearInterval(this.heartbeatInterval);
|
|
4618
|
+
this.heartbeatInterval = null;
|
|
4619
|
+
}
|
|
3275
4620
|
for (const [agentId, agent] of this.agents.entries()) {
|
|
3276
4621
|
if (agent.process && !agent.process.killed) {
|
|
3277
4622
|
console.log(`Killing agent: ${agentId}`);
|
|
3278
|
-
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"));
|
|
3279
4641
|
}
|
|
3280
4642
|
}
|
|
3281
4643
|
this.agents.clear();
|
|
@@ -3283,12 +4645,543 @@ ${summary}`
|
|
|
3283
4645
|
getStats() {
|
|
3284
4646
|
return {
|
|
3285
4647
|
activeAgents: this.agents.size,
|
|
4648
|
+
agentCount: this.agentCount,
|
|
4649
|
+
useWorktrees: this.useWorktrees,
|
|
3286
4650
|
processedTasks: this.processedTasks.size,
|
|
3287
4651
|
totalTasksCompleted: Array.from(this.agents.values()).reduce((sum, agent) => sum + agent.tasksCompleted, 0),
|
|
3288
4652
|
totalTasksFailed: Array.from(this.agents.values()).reduce((sum, agent) => sum + agent.tasksFailed, 0)
|
|
3289
4653
|
};
|
|
3290
4654
|
}
|
|
4655
|
+
getAgentStates() {
|
|
4656
|
+
return Array.from(this.agents.values());
|
|
4657
|
+
}
|
|
3291
4658
|
sleep(ms) {
|
|
3292
|
-
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
|
+
}
|
|
3293
5186
|
}
|
|
3294
5187
|
}
|