@locusai/sdk 0.8.1 → 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 +1 -0
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/review-service.d.ts.map +1 -1
- package/dist/agent/reviewer-worker.d.ts +42 -0
- package/dist/agent/reviewer-worker.d.ts.map +1 -0
- package/dist/agent/task-executor.d.ts +1 -1
- package/dist/agent/task-executor.d.ts.map +1 -1
- package/dist/agent/worker.d.ts +47 -4
- package/dist/agent/worker.d.ts.map +1 -1
- package/dist/agent/worker.js +1102 -506
- package/dist/ai/claude-runner.d.ts +5 -0
- package/dist/ai/claude-runner.d.ts.map +1 -1
- package/dist/ai/codex-runner.d.ts +5 -0
- package/dist/ai/codex-runner.d.ts.map +1 -1
- package/dist/ai/runner.d.ts +5 -0
- package/dist/ai/runner.d.ts.map +1 -1
- package/dist/core/config.d.ts +10 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/prompt-builder.d.ts +3 -6
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/git/git-utils.d.ts +31 -0
- package/dist/git/git-utils.d.ts.map +1 -0
- package/dist/git/index.d.ts +3 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/pr-service.d.ts +66 -0
- package/dist/git/pr-service.d.ts.map +1 -0
- package/dist/index-node.d.ts +5 -1
- package/dist/index-node.d.ts.map +1 -1
- package/dist/index-node.js +2560 -729
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -49
- package/dist/modules/auth.d.ts +3 -0
- package/dist/modules/auth.d.ts.map +1 -1
- package/dist/modules/tasks.d.ts +0 -5
- package/dist/modules/tasks.d.ts.map +1 -1
- package/dist/modules/workspaces.d.ts +10 -10
- package/dist/modules/workspaces.d.ts.map +1 -1
- package/dist/orchestrator.d.ts +38 -5
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/planning/agents/architect.d.ts +15 -0
- package/dist/planning/agents/architect.d.ts.map +1 -0
- package/dist/planning/agents/sprint-organizer.d.ts +14 -0
- package/dist/planning/agents/sprint-organizer.d.ts.map +1 -0
- package/dist/planning/agents/tech-lead.d.ts +15 -0
- package/dist/planning/agents/tech-lead.d.ts.map +1 -0
- package/dist/planning/index.d.ts +4 -0
- package/dist/planning/index.d.ts.map +1 -0
- package/dist/planning/plan-manager.d.ts +52 -0
- package/dist/planning/plan-manager.d.ts.map +1 -0
- package/dist/planning/planning-meeting.d.ts +36 -0
- package/dist/planning/planning-meeting.d.ts.map +1 -0
- package/dist/planning/sprint-plan.d.ts +47 -0
- package/dist/planning/sprint-plan.d.ts.map +1 -0
- package/dist/project/knowledge-base.d.ts +25 -0
- package/dist/project/knowledge-base.d.ts.map +1 -0
- package/dist/worktree/index.d.ts +3 -0
- package/dist/worktree/index.d.ts.map +1 -0
- package/dist/worktree/worktree-config.d.ts +58 -0
- package/dist/worktree/worktree-config.d.ts.map +1 -0
- package/dist/worktree/worktree-manager.d.ts +96 -0
- package/dist/worktree/worktree-manager.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/modules/ai.d.ts +0 -55
- package/dist/modules/ai.d.ts.map +0 -1
package/dist/agent/worker.js
CHANGED
|
@@ -52,8 +52,7 @@ __export(exports_src, {
|
|
|
52
52
|
InvitationsModule: () => InvitationsModule,
|
|
53
53
|
DocsModule: () => DocsModule,
|
|
54
54
|
CiModule: () => CiModule,
|
|
55
|
-
AuthModule: () => AuthModule
|
|
56
|
-
AIModule: () => AIModule
|
|
55
|
+
AuthModule: () => AuthModule
|
|
57
56
|
});
|
|
58
57
|
module.exports = __toCommonJS(exports_src);
|
|
59
58
|
var import_axios = __toESM(require("axios"));
|
|
@@ -86,43 +85,6 @@ class BaseModule {
|
|
|
86
85
|
}
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
// src/modules/ai.ts
|
|
90
|
-
class AIModule extends BaseModule {
|
|
91
|
-
async chat(workspaceId, body) {
|
|
92
|
-
const { data } = await this.api.post(`/ai/${workspaceId}/chat`, body, { timeout: 300000 });
|
|
93
|
-
return data;
|
|
94
|
-
}
|
|
95
|
-
async detectIntent(workspaceId, body) {
|
|
96
|
-
const { data } = await this.api.post(`/ai/${workspaceId}/chat/intent`, body, { timeout: 300000 });
|
|
97
|
-
return data;
|
|
98
|
-
}
|
|
99
|
-
async executeIntent(workspaceId, body) {
|
|
100
|
-
const { data } = await this.api.post(`/ai/${workspaceId}/chat/execute`, body, { timeout: 300000 });
|
|
101
|
-
return data;
|
|
102
|
-
}
|
|
103
|
-
async listSessions(workspaceId) {
|
|
104
|
-
const { data } = await this.api.get(`/ai/${workspaceId}/sessions`);
|
|
105
|
-
return data;
|
|
106
|
-
}
|
|
107
|
-
async getSession(workspaceId, sessionId) {
|
|
108
|
-
const { data } = await this.api.get(`/ai/${workspaceId}/session/${sessionId}`);
|
|
109
|
-
return data;
|
|
110
|
-
}
|
|
111
|
-
getChatStreamUrl(workspaceId, sessionId) {
|
|
112
|
-
return `${this.api.defaults.baseURL}/ai/${workspaceId}/chat/stream?sessionId=${sessionId}`;
|
|
113
|
-
}
|
|
114
|
-
async deleteSession(workspaceId, sessionId) {
|
|
115
|
-
await this.api.delete(`/ai/${workspaceId}/session/${sessionId}`);
|
|
116
|
-
}
|
|
117
|
-
async shareSession(workspaceId, sessionId, body) {
|
|
118
|
-
await this.api.post(`/ai/${workspaceId}/session/${sessionId}/share`, body);
|
|
119
|
-
}
|
|
120
|
-
async getSharedSession(sessionId) {
|
|
121
|
-
const { data } = await this.api.get(`/ai/shared/${sessionId}`);
|
|
122
|
-
return data;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
88
|
// src/modules/auth.ts
|
|
127
89
|
class AuthModule extends BaseModule {
|
|
128
90
|
async getProfile() {
|
|
@@ -149,6 +111,10 @@ class AuthModule extends BaseModule {
|
|
|
149
111
|
const { data } = await this.api.post("/auth/complete-registration", body);
|
|
150
112
|
return data;
|
|
151
113
|
}
|
|
114
|
+
async deleteAccount() {
|
|
115
|
+
const { data } = await this.api.delete("/auth/account");
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
152
118
|
}
|
|
153
119
|
|
|
154
120
|
// src/modules/ci.ts
|
|
@@ -346,10 +312,6 @@ class TasksModule extends BaseModule {
|
|
|
346
312
|
updates
|
|
347
313
|
});
|
|
348
314
|
}
|
|
349
|
-
async getContext(id, workspaceId) {
|
|
350
|
-
const { data } = await this.api.get(`/workspaces/${workspaceId}/tasks/${id}/context`);
|
|
351
|
-
return data;
|
|
352
|
-
}
|
|
353
315
|
}
|
|
354
316
|
|
|
355
317
|
// src/modules/workspaces.ts
|
|
@@ -386,10 +348,6 @@ class WorkspacesModule extends BaseModule {
|
|
|
386
348
|
const { data } = await this.api.get(`/workspaces/${id}/stats`);
|
|
387
349
|
return data;
|
|
388
350
|
}
|
|
389
|
-
async getManifestStatus(workspaceId) {
|
|
390
|
-
const { data } = await this.api.get(`/workspaces/${workspaceId}/manifest-status`);
|
|
391
|
-
return data;
|
|
392
|
-
}
|
|
393
351
|
async getActivity(id, limit) {
|
|
394
352
|
const { data } = await this.api.get(`/workspaces/${id}/activity`, {
|
|
395
353
|
params: { limit }
|
|
@@ -400,6 +358,18 @@ class WorkspacesModule extends BaseModule {
|
|
|
400
358
|
const { data } = await this.api.post(`/workspaces/${id}/dispatch`, { workerId, sprintId });
|
|
401
359
|
return data.task;
|
|
402
360
|
}
|
|
361
|
+
async heartbeat(workspaceId, agentId, currentTaskId, status) {
|
|
362
|
+
const { data } = await this.api.post(`/workspaces/${workspaceId}/agents/heartbeat`, {
|
|
363
|
+
agentId,
|
|
364
|
+
currentTaskId: currentTaskId ?? null,
|
|
365
|
+
status: status ?? "WORKING"
|
|
366
|
+
});
|
|
367
|
+
return data.agent;
|
|
368
|
+
}
|
|
369
|
+
async getAgents(workspaceId) {
|
|
370
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/agents`);
|
|
371
|
+
return data.agents;
|
|
372
|
+
}
|
|
403
373
|
async listApiKeys(workspaceId) {
|
|
404
374
|
const { data } = await this.api.get(`/workspaces/${workspaceId}/api-keys`);
|
|
405
375
|
return data.apiKeys;
|
|
@@ -418,7 +388,6 @@ class LocusClient {
|
|
|
418
388
|
api;
|
|
419
389
|
emitter;
|
|
420
390
|
auth;
|
|
421
|
-
ai;
|
|
422
391
|
tasks;
|
|
423
392
|
sprints;
|
|
424
393
|
workspaces;
|
|
@@ -438,7 +407,6 @@ class LocusClient {
|
|
|
438
407
|
});
|
|
439
408
|
this.setupInterceptors();
|
|
440
409
|
this.auth = new AuthModule(this.api, this.emitter);
|
|
441
|
-
this.ai = new AIModule(this.api, this.emitter);
|
|
442
410
|
this.tasks = new TasksModule(this.api, this.emitter);
|
|
443
411
|
this.sprints = new SprintsModule(this.api, this.emitter);
|
|
444
412
|
this.workspaces = new WorkspacesModule(this.api, this.emitter);
|
|
@@ -512,8 +480,7 @@ __export(exports_worker, {
|
|
|
512
480
|
AgentWorker: () => AgentWorker
|
|
513
481
|
});
|
|
514
482
|
module.exports = __toCommonJS(exports_worker);
|
|
515
|
-
var
|
|
516
|
-
var import_node_path7 = require("node:path");
|
|
483
|
+
var import_shared3 = require("@locusai/shared");
|
|
517
484
|
|
|
518
485
|
// src/core/config.ts
|
|
519
486
|
var import_node_path = require("node:path");
|
|
@@ -523,19 +490,27 @@ var PROVIDER = {
|
|
|
523
490
|
};
|
|
524
491
|
var DEFAULT_MODEL = {
|
|
525
492
|
[PROVIDER.CLAUDE]: "opus",
|
|
526
|
-
[PROVIDER.CODEX]: "gpt-5.
|
|
493
|
+
[PROVIDER.CODEX]: "gpt-5.3-codex"
|
|
494
|
+
};
|
|
495
|
+
var LOCUS_SCHEMA_BASE_URL = "https://locusai.dev/schemas";
|
|
496
|
+
var LOCUS_SCHEMAS = {
|
|
497
|
+
config: `${LOCUS_SCHEMA_BASE_URL}/config.schema.json`,
|
|
498
|
+
settings: `${LOCUS_SCHEMA_BASE_URL}/settings.schema.json`
|
|
527
499
|
};
|
|
528
500
|
var LOCUS_CONFIG = {
|
|
529
501
|
dir: ".locus",
|
|
530
502
|
configFile: "config.json",
|
|
503
|
+
settingsFile: "settings.json",
|
|
531
504
|
indexFile: "codebase-index.json",
|
|
532
|
-
contextFile: "
|
|
505
|
+
contextFile: "LOCUS.md",
|
|
533
506
|
artifactsDir: "artifacts",
|
|
534
507
|
documentsDir: "documents",
|
|
535
|
-
agentSkillsDir: ".agent/skills",
|
|
536
508
|
sessionsDir: "sessions",
|
|
537
509
|
reviewsDir: "reviews",
|
|
538
|
-
plansDir: "plans"
|
|
510
|
+
plansDir: "plans",
|
|
511
|
+
projectDir: "project",
|
|
512
|
+
projectContextFile: "context.md",
|
|
513
|
+
projectProgressFile: "progress.md"
|
|
539
514
|
};
|
|
540
515
|
var LOCUS_GITIGNORE_PATTERNS = [
|
|
541
516
|
"# Locus AI - Session data (user-specific, can grow large)",
|
|
@@ -548,11 +523,17 @@ var LOCUS_GITIGNORE_PATTERNS = [
|
|
|
548
523
|
".locus/reviews/",
|
|
549
524
|
"",
|
|
550
525
|
"# Locus AI - Plans (generated per task)",
|
|
551
|
-
".locus/plans/"
|
|
526
|
+
".locus/plans/",
|
|
527
|
+
"",
|
|
528
|
+
"# Locus AI - Agent worktrees (parallel execution)",
|
|
529
|
+
".locus-worktrees/",
|
|
530
|
+
"",
|
|
531
|
+
"# Locus AI - Settings (contains API key, telegram config, etc.)",
|
|
532
|
+
".locus/settings.json"
|
|
552
533
|
];
|
|
553
534
|
function getLocusPath(projectPath, fileName) {
|
|
554
|
-
if (fileName === "
|
|
555
|
-
return import_node_path.join(projectPath, LOCUS_CONFIG.
|
|
535
|
+
if (fileName === "projectContextFile" || fileName === "projectProgressFile") {
|
|
536
|
+
return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.projectDir, LOCUS_CONFIG[fileName]);
|
|
556
537
|
}
|
|
557
538
|
return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG[fileName]);
|
|
558
539
|
}
|
|
@@ -628,6 +609,14 @@ var c = {
|
|
|
628
609
|
};
|
|
629
610
|
|
|
630
611
|
// src/ai/claude-runner.ts
|
|
612
|
+
var SANDBOX_SETTINGS = JSON.stringify({
|
|
613
|
+
sandbox: {
|
|
614
|
+
enabled: true,
|
|
615
|
+
autoAllow: true,
|
|
616
|
+
allowUnsandboxedCommands: false
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
631
620
|
class ClaudeRunner {
|
|
632
621
|
model;
|
|
633
622
|
log;
|
|
@@ -635,6 +624,7 @@ class ClaudeRunner {
|
|
|
635
624
|
eventEmitter;
|
|
636
625
|
currentToolName;
|
|
637
626
|
activeTools = new Map;
|
|
627
|
+
activeProcess = null;
|
|
638
628
|
constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log) {
|
|
639
629
|
this.model = model;
|
|
640
630
|
this.log = log;
|
|
@@ -643,6 +633,12 @@ class ClaudeRunner {
|
|
|
643
633
|
setEventEmitter(emitter) {
|
|
644
634
|
this.eventEmitter = emitter;
|
|
645
635
|
}
|
|
636
|
+
abort() {
|
|
637
|
+
if (this.activeProcess && !this.activeProcess.killed) {
|
|
638
|
+
this.activeProcess.kill("SIGTERM");
|
|
639
|
+
this.activeProcess = null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
646
642
|
async run(prompt) {
|
|
647
643
|
const maxRetries = 3;
|
|
648
644
|
let lastError = null;
|
|
@@ -671,7 +667,9 @@ class ClaudeRunner {
|
|
|
671
667
|
"stream-json",
|
|
672
668
|
"--include-partial-messages",
|
|
673
669
|
"--model",
|
|
674
|
-
this.model
|
|
670
|
+
this.model,
|
|
671
|
+
"--settings",
|
|
672
|
+
SANDBOX_SETTINGS
|
|
675
673
|
];
|
|
676
674
|
const env = {
|
|
677
675
|
...process.env,
|
|
@@ -688,6 +686,7 @@ class ClaudeRunner {
|
|
|
688
686
|
stdio: ["pipe", "pipe", "pipe"],
|
|
689
687
|
env
|
|
690
688
|
});
|
|
689
|
+
this.activeProcess = claude;
|
|
691
690
|
let buffer = "";
|
|
692
691
|
let stderrBuffer = "";
|
|
693
692
|
let resolveChunk = null;
|
|
@@ -755,6 +754,7 @@ class ClaudeRunner {
|
|
|
755
754
|
signalEnd();
|
|
756
755
|
});
|
|
757
756
|
claude.on("close", (code) => {
|
|
757
|
+
this.activeProcess = null;
|
|
758
758
|
if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
|
|
759
759
|
process.stderr.write(`${stderrBuffer}
|
|
760
760
|
`);
|
|
@@ -912,7 +912,9 @@ class ClaudeRunner {
|
|
|
912
912
|
"stream-json",
|
|
913
913
|
"--include-partial-messages",
|
|
914
914
|
"--model",
|
|
915
|
-
this.model
|
|
915
|
+
this.model,
|
|
916
|
+
"--settings",
|
|
917
|
+
SANDBOX_SETTINGS
|
|
916
918
|
];
|
|
917
919
|
const env = {
|
|
918
920
|
...process.env,
|
|
@@ -924,6 +926,7 @@ class ClaudeRunner {
|
|
|
924
926
|
stdio: ["pipe", "pipe", "pipe"],
|
|
925
927
|
env
|
|
926
928
|
});
|
|
929
|
+
this.activeProcess = claude;
|
|
927
930
|
let finalResult = "";
|
|
928
931
|
let errorOutput = "";
|
|
929
932
|
let buffer = "";
|
|
@@ -957,6 +960,7 @@ class ClaudeRunner {
|
|
|
957
960
|
reject(new Error(`Failed to start Claude CLI: ${err.message}. Please ensure the 'claude' command is available in your PATH.`));
|
|
958
961
|
});
|
|
959
962
|
claude.on("close", (code) => {
|
|
963
|
+
this.activeProcess = null;
|
|
960
964
|
if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
|
|
961
965
|
process.stderr.write(`${stderrBuffer}
|
|
962
966
|
`);
|
|
@@ -1023,11 +1027,18 @@ class CodexRunner {
|
|
|
1023
1027
|
projectPath;
|
|
1024
1028
|
model;
|
|
1025
1029
|
log;
|
|
1030
|
+
activeProcess = null;
|
|
1026
1031
|
constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log) {
|
|
1027
1032
|
this.projectPath = projectPath;
|
|
1028
1033
|
this.model = model;
|
|
1029
1034
|
this.log = log;
|
|
1030
1035
|
}
|
|
1036
|
+
abort() {
|
|
1037
|
+
if (this.activeProcess && !this.activeProcess.killed) {
|
|
1038
|
+
this.activeProcess.kill("SIGTERM");
|
|
1039
|
+
this.activeProcess = null;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1031
1042
|
async run(prompt) {
|
|
1032
1043
|
const maxRetries = 3;
|
|
1033
1044
|
let lastError = null;
|
|
@@ -1054,6 +1065,7 @@ class CodexRunner {
|
|
|
1054
1065
|
env: process.env,
|
|
1055
1066
|
shell: false
|
|
1056
1067
|
});
|
|
1068
|
+
this.activeProcess = codex;
|
|
1057
1069
|
let resolveChunk = null;
|
|
1058
1070
|
const chunkQueue = [];
|
|
1059
1071
|
let processEnded = false;
|
|
@@ -1103,6 +1115,7 @@ class CodexRunner {
|
|
|
1103
1115
|
signalEnd();
|
|
1104
1116
|
});
|
|
1105
1117
|
codex.on("close", (code) => {
|
|
1118
|
+
this.activeProcess = null;
|
|
1106
1119
|
this.cleanupTempFile(outputPath);
|
|
1107
1120
|
if (code === 0) {
|
|
1108
1121
|
const result = this.readOutput(outputPath, finalOutput);
|
|
@@ -1148,6 +1161,7 @@ class CodexRunner {
|
|
|
1148
1161
|
env: process.env,
|
|
1149
1162
|
shell: false
|
|
1150
1163
|
});
|
|
1164
|
+
this.activeProcess = codex;
|
|
1151
1165
|
let output = "";
|
|
1152
1166
|
let errorOutput = "";
|
|
1153
1167
|
const handleOutput = (data) => {
|
|
@@ -1165,6 +1179,7 @@ class CodexRunner {
|
|
|
1165
1179
|
reject(new Error(`Failed to start Codex CLI: ${err.message}. ` + `Ensure 'codex' is installed and available in PATH.`));
|
|
1166
1180
|
});
|
|
1167
1181
|
codex.on("close", (code) => {
|
|
1182
|
+
this.activeProcess = null;
|
|
1168
1183
|
this.cleanupTempFile(outputPath);
|
|
1169
1184
|
if (code === 0) {
|
|
1170
1185
|
resolve2(this.readOutput(outputPath, output));
|
|
@@ -1179,7 +1194,8 @@ class CodexRunner {
|
|
|
1179
1194
|
buildArgs(outputPath) {
|
|
1180
1195
|
const args = [
|
|
1181
1196
|
"exec",
|
|
1182
|
-
"--
|
|
1197
|
+
"--sandbox",
|
|
1198
|
+
"workspace-write",
|
|
1183
1199
|
"--skip-git-repo-check",
|
|
1184
1200
|
"--output-last-message",
|
|
1185
1201
|
outputPath
|
|
@@ -1246,375 +1262,745 @@ function createAiRunner(provider, config) {
|
|
|
1246
1262
|
}
|
|
1247
1263
|
}
|
|
1248
1264
|
|
|
1249
|
-
// src/
|
|
1250
|
-
var
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1265
|
+
// src/git/git-utils.ts
|
|
1266
|
+
var import_node_child_process3 = require("node:child_process");
|
|
1267
|
+
function isGitAvailable() {
|
|
1268
|
+
try {
|
|
1269
|
+
import_node_child_process3.execFileSync("git", ["--version"], {
|
|
1270
|
+
encoding: "utf-8",
|
|
1271
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1272
|
+
});
|
|
1273
|
+
return true;
|
|
1274
|
+
} catch {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function isGhAvailable(projectPath) {
|
|
1279
|
+
try {
|
|
1280
|
+
import_node_child_process3.execFileSync("gh", ["auth", "status"], {
|
|
1281
|
+
cwd: projectPath,
|
|
1282
|
+
encoding: "utf-8",
|
|
1283
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1284
|
+
});
|
|
1285
|
+
return true;
|
|
1286
|
+
} catch {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function getGhUsername() {
|
|
1291
|
+
try {
|
|
1292
|
+
const output = import_node_child_process3.execFileSync("gh", ["api", "user", "--jq", ".login"], {
|
|
1293
|
+
encoding: "utf-8",
|
|
1294
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1295
|
+
}).trim();
|
|
1296
|
+
return output || null;
|
|
1297
|
+
} catch {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
function detectRemoteProvider(projectPath) {
|
|
1302
|
+
const url = getRemoteUrl(projectPath);
|
|
1303
|
+
if (!url)
|
|
1304
|
+
return "unknown";
|
|
1305
|
+
if (url.includes("github.com"))
|
|
1306
|
+
return "github";
|
|
1307
|
+
if (url.includes("gitlab.com") || url.includes("gitlab"))
|
|
1308
|
+
return "gitlab";
|
|
1309
|
+
if (url.includes("bitbucket.org"))
|
|
1310
|
+
return "bitbucket";
|
|
1311
|
+
return "unknown";
|
|
1312
|
+
}
|
|
1313
|
+
function getRemoteUrl(projectPath, remote = "origin") {
|
|
1314
|
+
try {
|
|
1315
|
+
return import_node_child_process3.execFileSync("git", ["remote", "get-url", remote], {
|
|
1316
|
+
cwd: projectPath,
|
|
1317
|
+
encoding: "utf-8",
|
|
1318
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1319
|
+
}).trim();
|
|
1320
|
+
} catch {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function getCurrentBranch(projectPath) {
|
|
1325
|
+
return import_node_child_process3.execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1326
|
+
cwd: projectPath,
|
|
1327
|
+
encoding: "utf-8",
|
|
1328
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1329
|
+
}).trim();
|
|
1330
|
+
}
|
|
1331
|
+
function getDefaultBranch(projectPath, remote = "origin") {
|
|
1332
|
+
try {
|
|
1333
|
+
const ref = import_node_child_process3.execFileSync("git", ["symbolic-ref", `refs/remotes/${remote}/HEAD`], {
|
|
1334
|
+
cwd: projectPath,
|
|
1335
|
+
encoding: "utf-8",
|
|
1336
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1337
|
+
}).trim();
|
|
1338
|
+
return ref.replace(`refs/remotes/${remote}/`, "");
|
|
1339
|
+
} catch {
|
|
1340
|
+
for (const candidate of ["main", "master"]) {
|
|
1341
|
+
try {
|
|
1342
|
+
import_node_child_process3.execFileSync("git", ["ls-remote", "--exit-code", "--heads", remote, candidate], {
|
|
1343
|
+
cwd: projectPath,
|
|
1344
|
+
encoding: "utf-8",
|
|
1345
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1346
|
+
});
|
|
1347
|
+
return candidate;
|
|
1348
|
+
} catch {}
|
|
1349
|
+
}
|
|
1350
|
+
try {
|
|
1351
|
+
return getCurrentBranch(projectPath);
|
|
1352
|
+
} catch {
|
|
1353
|
+
return "main";
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1254
1357
|
|
|
1255
|
-
|
|
1358
|
+
// src/git/pr-service.ts
|
|
1359
|
+
var import_node_child_process4 = require("node:child_process");
|
|
1360
|
+
class PrService {
|
|
1256
1361
|
projectPath;
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
constructor(projectPath) {
|
|
1362
|
+
log;
|
|
1363
|
+
constructor(projectPath, log) {
|
|
1260
1364
|
this.projectPath = projectPath;
|
|
1261
|
-
this.
|
|
1365
|
+
this.log = log;
|
|
1366
|
+
}
|
|
1367
|
+
createPr(options) {
|
|
1368
|
+
const {
|
|
1369
|
+
task,
|
|
1370
|
+
branch,
|
|
1371
|
+
baseBranch: requestedBaseBranch,
|
|
1372
|
+
agentId,
|
|
1373
|
+
summary
|
|
1374
|
+
} = options;
|
|
1375
|
+
const provider = detectRemoteProvider(this.projectPath);
|
|
1376
|
+
if (provider !== "github") {
|
|
1377
|
+
throw new Error(`PR creation is only supported for GitHub repositories (detected: ${provider})`);
|
|
1378
|
+
}
|
|
1379
|
+
if (!isGhAvailable(this.projectPath)) {
|
|
1380
|
+
throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
|
|
1381
|
+
}
|
|
1382
|
+
const title = `[Locus] ${task.title}`;
|
|
1383
|
+
const body = this.buildPrBody(task, agentId, summary);
|
|
1384
|
+
const baseBranch = requestedBaseBranch ?? getDefaultBranch(this.projectPath);
|
|
1385
|
+
this.validateCreatePrInputs(baseBranch, branch);
|
|
1386
|
+
this.log(`Creating PR: ${title} (${branch} → ${baseBranch})`, "info");
|
|
1387
|
+
const output = import_node_child_process4.execFileSync("gh", [
|
|
1388
|
+
"pr",
|
|
1389
|
+
"create",
|
|
1390
|
+
"--title",
|
|
1391
|
+
title,
|
|
1392
|
+
"--body",
|
|
1393
|
+
body,
|
|
1394
|
+
"--base",
|
|
1395
|
+
baseBranch,
|
|
1396
|
+
"--head",
|
|
1397
|
+
branch
|
|
1398
|
+
], {
|
|
1399
|
+
cwd: this.projectPath,
|
|
1400
|
+
encoding: "utf-8",
|
|
1401
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1402
|
+
}).trim();
|
|
1403
|
+
const url = output;
|
|
1404
|
+
const prNumber = this.extractPrNumber(url);
|
|
1405
|
+
this.log(`PR created: ${url}`, "success");
|
|
1406
|
+
return { url, number: prNumber };
|
|
1262
1407
|
}
|
|
1263
|
-
|
|
1264
|
-
if (!
|
|
1265
|
-
throw new Error("
|
|
1408
|
+
validateCreatePrInputs(baseBranch, headBranch) {
|
|
1409
|
+
if (!this.hasRemoteBranch(baseBranch)) {
|
|
1410
|
+
throw new Error(`Base branch "${baseBranch}" does not exist on origin. Push/fetch refs and retry.`);
|
|
1266
1411
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
const treeString = currentFiles.join(`
|
|
1270
|
-
`);
|
|
1271
|
-
const newTreeHash = this.hashTree(treeString);
|
|
1272
|
-
const existingIndex = this.loadIndex();
|
|
1273
|
-
const currentHashes = this.computeFileHashes(currentFiles);
|
|
1274
|
-
const existingHashes = existingIndex?.fileHashes;
|
|
1275
|
-
const hasExistingContent = existingIndex && (Object.keys(existingIndex.symbols).length > 0 || Object.keys(existingIndex.responsibilities).length > 0);
|
|
1276
|
-
const canIncremental = !force && existingIndex && existingHashes && hasExistingContent;
|
|
1277
|
-
if (canIncremental) {
|
|
1278
|
-
onProgress?.("Performing incremental update");
|
|
1279
|
-
const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
|
|
1280
|
-
const changedFiles = [...added, ...modified];
|
|
1281
|
-
const totalChanges = changedFiles.length + deleted.length;
|
|
1282
|
-
const existingFileCount = Object.keys(existingHashes).length;
|
|
1283
|
-
onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
|
|
1284
|
-
if (existingFileCount > 0) {
|
|
1285
|
-
const changeRatio = totalChanges / existingFileCount;
|
|
1286
|
-
if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
|
|
1287
|
-
onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
|
|
1288
|
-
const incrementalIndex = await treeSummarizer(changedFiles.join(`
|
|
1289
|
-
`));
|
|
1290
|
-
const updatedIndex = this.cloneIndex(existingIndex);
|
|
1291
|
-
this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
|
|
1292
|
-
return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
|
|
1293
|
-
}
|
|
1294
|
-
if (changedFiles.length === 0 && deleted.length > 0) {
|
|
1295
|
-
onProgress?.(`Removing ${deleted.length} deleted files from index`);
|
|
1296
|
-
const updatedIndex = this.cloneIndex(existingIndex);
|
|
1297
|
-
this.removeFilesFromIndex(updatedIndex, deleted);
|
|
1298
|
-
return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
|
|
1299
|
-
}
|
|
1300
|
-
if (changedFiles.length === 0 && deleted.length === 0) {
|
|
1301
|
-
onProgress?.("No actual file changes, updating hashes only");
|
|
1302
|
-
const updatedIndex = this.cloneIndex(existingIndex);
|
|
1303
|
-
return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
|
|
1304
|
-
}
|
|
1305
|
-
onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
|
|
1306
|
-
}
|
|
1412
|
+
if (!this.hasRemoteBranch(headBranch)) {
|
|
1413
|
+
throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
|
|
1307
1414
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
} catch (error) {
|
|
1313
|
-
throw new Error(`AI analysis failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1415
|
+
const baseRef = this.resolveBranchRef(baseBranch);
|
|
1416
|
+
const headRef = this.resolveBranchRef(headBranch);
|
|
1417
|
+
if (!baseRef) {
|
|
1418
|
+
throw new Error(`Could not resolve base branch "${baseBranch}" locally.`);
|
|
1314
1419
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
const gitmodulesPath = import_node_path4.join(this.projectPath, ".gitmodules");
|
|
1318
|
-
const submoduleIgnores = [];
|
|
1319
|
-
if (import_node_fs2.existsSync(gitmodulesPath)) {
|
|
1320
|
-
try {
|
|
1321
|
-
const content = import_node_fs2.readFileSync(gitmodulesPath, "utf-8");
|
|
1322
|
-
const lines = content.split(`
|
|
1323
|
-
`);
|
|
1324
|
-
for (const line of lines) {
|
|
1325
|
-
const match = line.match(/^\s*path\s*=\s*(.*)$/);
|
|
1326
|
-
const path = match?.[1]?.trim();
|
|
1327
|
-
if (path) {
|
|
1328
|
-
submoduleIgnores.push(`${path}/**`);
|
|
1329
|
-
submoduleIgnores.push(`**/${path}/**`);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
} catch {}
|
|
1420
|
+
if (!headRef) {
|
|
1421
|
+
throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
|
|
1333
1422
|
}
|
|
1334
|
-
|
|
1423
|
+
const commitsAhead = this.countCommitsAhead(baseRef, headRef);
|
|
1424
|
+
if (commitsAhead <= 0) {
|
|
1425
|
+
throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
countCommitsAhead(baseRef, headRef) {
|
|
1429
|
+
const output = import_node_child_process4.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
|
|
1335
1430
|
cwd: this.projectPath,
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
"**/build/**",
|
|
1342
|
-
"**/target/**",
|
|
1343
|
-
"**/bin/**",
|
|
1344
|
-
"**/obj/**",
|
|
1345
|
-
"**/.next/**",
|
|
1346
|
-
"**/.svelte-kit/**",
|
|
1347
|
-
"**/.nuxt/**",
|
|
1348
|
-
"**/.cache/**",
|
|
1349
|
-
"**/out/**",
|
|
1350
|
-
"**/__tests__/**",
|
|
1351
|
-
"**/coverage/**",
|
|
1352
|
-
"**/*.test.*",
|
|
1353
|
-
"**/*.spec.*",
|
|
1354
|
-
"**/*.d.ts",
|
|
1355
|
-
"**/tsconfig.tsbuildinfo",
|
|
1356
|
-
"**/.locus/*.json",
|
|
1357
|
-
"**/.locus/*.md",
|
|
1358
|
-
"**/.locus/!(artifacts)/**",
|
|
1359
|
-
"**/.git/**",
|
|
1360
|
-
"**/.svn/**",
|
|
1361
|
-
"**/.hg/**",
|
|
1362
|
-
"**/.vscode/**",
|
|
1363
|
-
"**/.idea/**",
|
|
1364
|
-
"**/.DS_Store",
|
|
1365
|
-
"**/bun.lock",
|
|
1366
|
-
"**/package-lock.json",
|
|
1367
|
-
"**/yarn.lock",
|
|
1368
|
-
"**/pnpm-lock.yaml",
|
|
1369
|
-
"**/Cargo.lock",
|
|
1370
|
-
"**/go.sum",
|
|
1371
|
-
"**/poetry.lock",
|
|
1372
|
-
"**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
|
|
1373
|
-
]
|
|
1374
|
-
});
|
|
1431
|
+
encoding: "utf-8",
|
|
1432
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1433
|
+
}).trim();
|
|
1434
|
+
const value = Number.parseInt(output || "0", 10);
|
|
1435
|
+
return Number.isNaN(value) ? 0 : value;
|
|
1375
1436
|
}
|
|
1376
|
-
|
|
1377
|
-
if (
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
}
|
|
1437
|
+
resolveBranchRef(branch) {
|
|
1438
|
+
if (this.hasLocalBranch(branch)) {
|
|
1439
|
+
return branch;
|
|
1440
|
+
}
|
|
1441
|
+
if (this.hasRemoteTrackingBranch(branch)) {
|
|
1442
|
+
return `origin/${branch}`;
|
|
1383
1443
|
}
|
|
1384
1444
|
return null;
|
|
1385
1445
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1446
|
+
hasLocalBranch(branch) {
|
|
1447
|
+
try {
|
|
1448
|
+
import_node_child_process4.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
|
|
1449
|
+
cwd: this.projectPath,
|
|
1450
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1451
|
+
});
|
|
1452
|
+
return true;
|
|
1453
|
+
} catch {
|
|
1454
|
+
return false;
|
|
1390
1455
|
}
|
|
1391
|
-
import_node_fs2.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
|
|
1392
1456
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1457
|
+
hasRemoteTrackingBranch(branch) {
|
|
1458
|
+
try {
|
|
1459
|
+
import_node_child_process4.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`], {
|
|
1460
|
+
cwd: this.projectPath,
|
|
1461
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1462
|
+
});
|
|
1463
|
+
return true;
|
|
1464
|
+
} catch {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1395
1467
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1468
|
+
hasRemoteBranch(branch) {
|
|
1469
|
+
try {
|
|
1470
|
+
import_node_child_process4.execFileSync("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], {
|
|
1471
|
+
cwd: this.projectPath,
|
|
1472
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1473
|
+
});
|
|
1474
|
+
return true;
|
|
1475
|
+
} catch {
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
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
|
+
});
|
|
1401
1486
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1487
|
+
submitReview(prIdentifier, body, event) {
|
|
1488
|
+
try {
|
|
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
|
+
}
|
|
1404
1513
|
}
|
|
1405
|
-
|
|
1514
|
+
listLocusPrs() {
|
|
1406
1515
|
try {
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1516
|
+
const output = import_node_child_process4.execFileSync("gh", [
|
|
1517
|
+
"pr",
|
|
1518
|
+
"list",
|
|
1519
|
+
"--search",
|
|
1520
|
+
"[Locus] in:title",
|
|
1521
|
+
"--state",
|
|
1522
|
+
"open",
|
|
1523
|
+
"--json",
|
|
1524
|
+
"number,title,url,headRefName"
|
|
1525
|
+
], {
|
|
1526
|
+
cwd: this.projectPath,
|
|
1527
|
+
encoding: "utf-8",
|
|
1528
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1529
|
+
}).trim();
|
|
1530
|
+
const prs = JSON.parse(output || "[]");
|
|
1531
|
+
return prs.map((pr) => ({
|
|
1532
|
+
number: pr.number,
|
|
1533
|
+
title: pr.title,
|
|
1534
|
+
url: pr.url,
|
|
1535
|
+
branch: pr.headRefName
|
|
1536
|
+
}));
|
|
1409
1537
|
} catch {
|
|
1410
|
-
|
|
1538
|
+
this.log("Failed to list Locus PRs", "warn");
|
|
1539
|
+
return [];
|
|
1411
1540
|
}
|
|
1412
1541
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
}
|
|
1542
|
+
hasLocusReview(prNumber) {
|
|
1543
|
+
try {
|
|
1544
|
+
const output = import_node_child_process4.execFileSync("gh", ["pr", "view", prNumber, "--json", "reviews"], {
|
|
1545
|
+
cwd: this.projectPath,
|
|
1546
|
+
encoding: "utf-8",
|
|
1547
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1548
|
+
}).trim();
|
|
1549
|
+
const data = JSON.parse(output || "{}");
|
|
1550
|
+
return data.reviews?.some((r) => r.body?.includes("## Locus Agent Review")) ?? false;
|
|
1551
|
+
} catch {
|
|
1552
|
+
return false;
|
|
1420
1553
|
}
|
|
1421
|
-
return hashes;
|
|
1422
1554
|
}
|
|
1423
|
-
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
const existingSet = new Set(existingFiles);
|
|
1427
|
-
const currentSet = new Set(currentFiles);
|
|
1428
|
-
const added = currentFiles.filter((f) => !existingSet.has(f));
|
|
1429
|
-
const deleted = existingFiles.filter((f) => !currentSet.has(f));
|
|
1430
|
-
const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
|
|
1431
|
-
return { added, deleted, modified };
|
|
1555
|
+
listUnreviewedLocusPrs() {
|
|
1556
|
+
const allPrs = this.listLocusPrs();
|
|
1557
|
+
return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
|
|
1432
1558
|
}
|
|
1433
|
-
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1559
|
+
buildPrBody(task, agentId, summary) {
|
|
1560
|
+
const sections = [];
|
|
1561
|
+
sections.push(`## Task: ${task.title}`);
|
|
1562
|
+
sections.push("");
|
|
1563
|
+
if (task.description) {
|
|
1564
|
+
sections.push(task.description);
|
|
1565
|
+
sections.push("");
|
|
1437
1566
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1567
|
+
if (task.acceptanceChecklist?.length > 0) {
|
|
1568
|
+
sections.push("## Acceptance Criteria");
|
|
1569
|
+
for (const item of task.acceptanceChecklist) {
|
|
1570
|
+
sections.push(`- [ ] ${item.text}`);
|
|
1442
1571
|
}
|
|
1572
|
+
sections.push("");
|
|
1443
1573
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
if (mergedSymbols[symbol]) {
|
|
1449
|
-
mergedSymbols[symbol] = [
|
|
1450
|
-
...new Set([...mergedSymbols[symbol], ...paths])
|
|
1451
|
-
];
|
|
1452
|
-
} else {
|
|
1453
|
-
mergedSymbols[symbol] = paths;
|
|
1454
|
-
}
|
|
1574
|
+
if (summary) {
|
|
1575
|
+
sections.push("## Agent Summary");
|
|
1576
|
+
sections.push(summary);
|
|
1577
|
+
sections.push("");
|
|
1455
1578
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
return this.applyIndexMetadata(merged, newHashes, newTreeHash);
|
|
1579
|
+
sections.push("---");
|
|
1580
|
+
sections.push(`*Created by Locus Agent \`${agentId.slice(-8)}\`* | Task ID: \`${task.id}\``);
|
|
1581
|
+
return sections.join(`
|
|
1582
|
+
`);
|
|
1583
|
+
}
|
|
1584
|
+
extractPrNumber(url) {
|
|
1585
|
+
const match = url.match(/\/pull\/(\d+)/);
|
|
1586
|
+
return match ? Number.parseInt(match[1], 10) : 0;
|
|
1465
1587
|
}
|
|
1466
1588
|
}
|
|
1467
1589
|
|
|
1468
|
-
// src/
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1590
|
+
// src/project/knowledge-base.ts
|
|
1591
|
+
var import_node_fs2 = require("node:fs");
|
|
1592
|
+
var import_node_path4 = require("node:path");
|
|
1593
|
+
class KnowledgeBase {
|
|
1594
|
+
contextPath;
|
|
1595
|
+
progressPath;
|
|
1596
|
+
constructor(projectPath) {
|
|
1597
|
+
this.contextPath = getLocusPath(projectPath, "projectContextFile");
|
|
1598
|
+
this.progressPath = getLocusPath(projectPath, "projectProgressFile");
|
|
1475
1599
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
|
1483
1658
|
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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(`
|
|
1487
1673
|
|
|
1488
|
-
|
|
1489
|
-
${tree}
|
|
1674
|
+
---
|
|
1490
1675
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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 });
|
|
1507
1714
|
}
|
|
1508
1715
|
}
|
|
1509
1716
|
}
|
|
1510
1717
|
|
|
1511
|
-
// src/
|
|
1718
|
+
// src/worktree/worktree-manager.ts
|
|
1719
|
+
var import_node_child_process5 = require("node:child_process");
|
|
1512
1720
|
var import_node_fs3 = require("node:fs");
|
|
1513
1721
|
var import_node_path5 = require("node:path");
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
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
|
+
});
|
|
1518
1743
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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
|
+
}
|
|
1523
1766
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
const
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
if (doc.groupId === artifactsGroupId) {
|
|
1532
|
-
continue;
|
|
1533
|
-
}
|
|
1534
|
-
const groupName = groupMap.get(doc.groupId || "") || "General";
|
|
1535
|
-
const groupDir = import_node_path5.join(documentsDir, groupName);
|
|
1536
|
-
if (!import_node_fs3.existsSync(groupDir)) {
|
|
1537
|
-
import_node_fs3.mkdirSync(groupDir, { recursive: true });
|
|
1538
|
-
}
|
|
1539
|
-
const fileName = `${doc.title}.md`;
|
|
1540
|
-
const filePath = import_node_path5.join(groupDir, fileName);
|
|
1541
|
-
if (!import_node_fs3.existsSync(filePath) || import_node_fs3.readFileSync(filePath, "utf-8") !== doc.content) {
|
|
1542
|
-
import_node_fs3.writeFileSync(filePath, doc.content || "");
|
|
1543
|
-
fetchedCount++;
|
|
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.`);
|
|
1544
1774
|
}
|
|
1775
|
+
this.log(`Removing existing worktree for branch: ${branch} (${worktreePath2})`, "warn");
|
|
1776
|
+
this.remove(worktreePath2, false);
|
|
1545
1777
|
}
|
|
1546
|
-
|
|
1547
|
-
this.
|
|
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);
|
|
1548
1783
|
}
|
|
1784
|
+
}
|
|
1785
|
+
const addWorktree = () => this.git(`worktree add "${worktreePath}" -b "${branch}" "${baseBranch}"`, this.projectPath);
|
|
1786
|
+
try {
|
|
1787
|
+
addWorktree();
|
|
1549
1788
|
} catch (error) {
|
|
1550
|
-
this.
|
|
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)";
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
if (import_node_path5.resolve(path) === this.projectPath) {
|
|
1832
|
+
isMain = true;
|
|
1833
|
+
}
|
|
1834
|
+
if (path) {
|
|
1835
|
+
worktrees.push({ path, branch, head, isMain, isPrunable });
|
|
1836
|
+
}
|
|
1551
1837
|
}
|
|
1838
|
+
return worktrees;
|
|
1552
1839
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
// src/agent/review-service.ts
|
|
1556
|
-
var import_node_child_process3 = require("node:child_process");
|
|
1557
|
-
|
|
1558
|
-
class ReviewService {
|
|
1559
|
-
deps;
|
|
1560
|
-
constructor(deps) {
|
|
1561
|
-
this.deps = deps;
|
|
1840
|
+
listAgentWorktrees() {
|
|
1841
|
+
return this.list().filter((wt) => !wt.isMain);
|
|
1562
1842
|
}
|
|
1563
|
-
|
|
1564
|
-
const
|
|
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");
|
|
1565
1849
|
try {
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1850
|
+
this.git(`worktree remove "${absolutePath}" --force`, this.projectPath);
|
|
1851
|
+
} catch {
|
|
1852
|
+
if (import_node_fs3.existsSync(absolutePath)) {
|
|
1853
|
+
import_node_fs3.rmSync(absolutePath, { recursive: true, force: true });
|
|
1854
|
+
}
|
|
1855
|
+
this.git("worktree prune", this.projectPath);
|
|
1856
|
+
}
|
|
1857
|
+
if (deleteBranch && branchToDelete && !branchToDelete.startsWith("(")) {
|
|
1858
|
+
try {
|
|
1859
|
+
this.git(`branch -D "${branchToDelete}"`, this.projectPath);
|
|
1860
|
+
this.log(`Deleted branch: ${branchToDelete}`, "success");
|
|
1861
|
+
} catch {
|
|
1862
|
+
this.log(`Could not delete branch: ${branchToDelete} (may already be deleted)`, "warn");
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
this.log("Worktree removed", "success");
|
|
1866
|
+
}
|
|
1867
|
+
prune() {
|
|
1868
|
+
const before = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
1869
|
+
this.git("worktree prune", this.projectPath);
|
|
1870
|
+
const after = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
1871
|
+
const pruned = before - after;
|
|
1872
|
+
if (pruned > 0) {
|
|
1873
|
+
this.log(`Pruned ${pruned} stale worktree(s)`, "success");
|
|
1874
|
+
}
|
|
1875
|
+
return pruned;
|
|
1876
|
+
}
|
|
1877
|
+
removeAll() {
|
|
1878
|
+
const agentWorktrees = this.listAgentWorktrees();
|
|
1879
|
+
let removed = 0;
|
|
1880
|
+
for (const wt of agentWorktrees) {
|
|
1881
|
+
try {
|
|
1882
|
+
this.remove(wt.path, true);
|
|
1883
|
+
removed++;
|
|
1884
|
+
} catch {
|
|
1885
|
+
this.log(`Failed to remove worktree: ${wt.path}`, "warn");
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
if (import_node_fs3.existsSync(this.rootPath)) {
|
|
1889
|
+
try {
|
|
1890
|
+
import_node_fs3.rmSync(this.rootPath, { recursive: true, force: true });
|
|
1891
|
+
} catch {}
|
|
1892
|
+
}
|
|
1893
|
+
return removed;
|
|
1894
|
+
}
|
|
1895
|
+
hasChanges(worktreePath) {
|
|
1896
|
+
const status = this.git("status --porcelain", worktreePath).trim();
|
|
1897
|
+
return status.length > 0;
|
|
1898
|
+
}
|
|
1899
|
+
commitChanges(worktreePath, message) {
|
|
1900
|
+
if (!this.hasChanges(worktreePath)) {
|
|
1901
|
+
this.log("No changes to commit", "info");
|
|
1570
1902
|
return null;
|
|
1571
1903
|
}
|
|
1572
|
-
|
|
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");
|
|
1573
1913
|
try {
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1914
|
+
this.gitExec(["push", "-u", remote, branch], worktreePath);
|
|
1915
|
+
this.log(`Pushed ${branch} to ${remote}`, "success");
|
|
1916
|
+
return branch;
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
if (!this.isNonFastForwardPushError(error)) {
|
|
1919
|
+
throw error;
|
|
1920
|
+
}
|
|
1921
|
+
this.log(`Push rejected for ${branch} (non-fast-forward). Retrying with --force-with-lease.`, "warn");
|
|
1922
|
+
try {
|
|
1923
|
+
this.gitExec(["fetch", remote, branch], worktreePath);
|
|
1924
|
+
} catch {}
|
|
1925
|
+
this.gitExec(["push", "--force-with-lease", "-u", remote, branch], worktreePath);
|
|
1926
|
+
this.log(`Pushed ${branch} to ${remote} with --force-with-lease`, "success");
|
|
1581
1927
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
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;
|
|
1584
1942
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1943
|
+
}
|
|
1944
|
+
getCurrentBranch() {
|
|
1945
|
+
return this.git("rev-parse --abbrev-ref HEAD", this.projectPath).trim();
|
|
1946
|
+
}
|
|
1947
|
+
isManagedWorktreePath(worktreePath) {
|
|
1948
|
+
const rootPath = import_node_path5.resolve(this.rootPath);
|
|
1949
|
+
const candidate = import_node_path5.resolve(worktreePath);
|
|
1950
|
+
const rootWithSep = rootPath.endsWith(import_node_path5.sep) ? rootPath : `${rootPath}${import_node_path5.sep}`;
|
|
1951
|
+
return candidate.startsWith(rootWithSep);
|
|
1952
|
+
}
|
|
1953
|
+
ensureDirectory(dirPath, label) {
|
|
1954
|
+
if (import_node_fs3.existsSync(dirPath)) {
|
|
1955
|
+
if (!import_node_fs3.statSync(dirPath).isDirectory()) {
|
|
1956
|
+
throw new Error(`${label} exists but is not a directory: ${dirPath}`);
|
|
1957
|
+
}
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
import_node_fs3.mkdirSync(dirPath, { recursive: true });
|
|
1961
|
+
}
|
|
1962
|
+
isMissingDirectoryError(error) {
|
|
1963
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1964
|
+
return message.includes("cannot create directory") || message.includes("No such file or directory");
|
|
1965
|
+
}
|
|
1966
|
+
cleanupFailedWorktree(worktreePath, branch) {
|
|
1967
|
+
try {
|
|
1968
|
+
this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
|
|
1969
|
+
} catch {}
|
|
1970
|
+
if (import_node_fs3.existsSync(worktreePath)) {
|
|
1971
|
+
import_node_fs3.rmSync(worktreePath, { recursive: true, force: true });
|
|
1972
|
+
}
|
|
1973
|
+
try {
|
|
1974
|
+
this.git("worktree prune", this.projectPath);
|
|
1975
|
+
} catch {}
|
|
1976
|
+
if (this.branchExists(branch)) {
|
|
1977
|
+
try {
|
|
1978
|
+
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
1979
|
+
} catch {}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
isNonFastForwardPushError(error) {
|
|
1983
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1984
|
+
return message.includes("non-fast-forward") || message.includes("[rejected]") || message.includes("fetch first");
|
|
1985
|
+
}
|
|
1986
|
+
git(args, cwd) {
|
|
1987
|
+
return import_node_child_process5.execSync(`git ${args}`, {
|
|
1988
|
+
cwd,
|
|
1989
|
+
encoding: "utf-8",
|
|
1990
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
gitExec(args, cwd) {
|
|
1994
|
+
return import_node_child_process5.execFileSync("git", args, {
|
|
1995
|
+
cwd,
|
|
1996
|
+
encoding: "utf-8",
|
|
1997
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1998
|
+
});
|
|
1612
1999
|
}
|
|
1613
2000
|
}
|
|
1614
2001
|
|
|
1615
2002
|
// src/core/prompt-builder.ts
|
|
1616
2003
|
var import_node_fs4 = require("node:fs");
|
|
1617
|
-
var import_node_os2 = require("node:os");
|
|
1618
2004
|
var import_node_path6 = require("node:path");
|
|
1619
2005
|
var import_shared2 = require("@locusai/shared");
|
|
1620
2006
|
class PromptBuilder {
|
|
@@ -1683,11 +2069,12 @@ ${fallback}
|
|
|
1683
2069
|
if (serverContext) {
|
|
1684
2070
|
prompt += `## Project Context (Server)
|
|
1685
2071
|
`;
|
|
1686
|
-
|
|
1687
|
-
|
|
2072
|
+
const project = serverContext.project;
|
|
2073
|
+
if (project) {
|
|
2074
|
+
prompt += `- Project: ${project.name || "Unknown"}
|
|
1688
2075
|
`;
|
|
1689
|
-
if (!hasLocalContext &&
|
|
1690
|
-
prompt += `- Tech Stack: ${
|
|
2076
|
+
if (!hasLocalContext && project.techStack?.length) {
|
|
2077
|
+
prompt += `- Tech Stack: ${project.techStack.join(", ")}
|
|
1691
2078
|
`;
|
|
1692
2079
|
}
|
|
1693
2080
|
}
|
|
@@ -1700,12 +2087,11 @@ ${serverContext.context}
|
|
|
1700
2087
|
`;
|
|
1701
2088
|
}
|
|
1702
2089
|
prompt += this.getProjectStructure();
|
|
1703
|
-
prompt += this.getSkillsInfo();
|
|
1704
2090
|
prompt += `## Project Knowledge Base
|
|
1705
2091
|
`;
|
|
1706
2092
|
prompt += `You have access to the following documentation directories for context:
|
|
1707
2093
|
`;
|
|
1708
|
-
prompt += `- Artifacts: \`.locus/artifacts\`
|
|
2094
|
+
prompt += `- Artifacts: \`.locus/artifacts\`
|
|
1709
2095
|
`;
|
|
1710
2096
|
prompt += `- Documents: \`.locus/documents\`
|
|
1711
2097
|
`;
|
|
@@ -1763,11 +2149,9 @@ ${comment.text}
|
|
|
1763
2149
|
}
|
|
1764
2150
|
}
|
|
1765
2151
|
prompt += `## Instructions
|
|
1766
|
-
1. Complete this task.
|
|
2152
|
+
1. Complete this task.
|
|
1767
2153
|
2. **Artifact Management**: If you create any high-level documentation (PRDs, technical drafts, architecture docs), you MUST save them in \`.locus/artifacts/\`. Do NOT create them in the root directory.
|
|
1768
|
-
3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...)
|
|
1769
|
-
4. When finished successfully, output: <promise>COMPLETE</promise>
|
|
1770
|
-
`;
|
|
2154
|
+
3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
|
|
1771
2155
|
return prompt;
|
|
1772
2156
|
}
|
|
1773
2157
|
async buildGenericPrompt(query) {
|
|
@@ -1814,7 +2198,6 @@ ${fallback}
|
|
|
1814
2198
|
}
|
|
1815
2199
|
}
|
|
1816
2200
|
prompt += this.getProjectStructure();
|
|
1817
|
-
prompt += this.getSkillsInfo();
|
|
1818
2201
|
prompt += `## Project Knowledge Base
|
|
1819
2202
|
`;
|
|
1820
2203
|
prompt += `You have access to the following documentation directories for context:
|
|
@@ -1835,9 +2218,7 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
1835
2218
|
}
|
|
1836
2219
|
prompt += `## Instructions
|
|
1837
2220
|
1. Execute the prompt based on the provided project context.
|
|
1838
|
-
2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...)
|
|
1839
|
-
3. When finished successfully, output: <promise>COMPLETE</promise>
|
|
1840
|
-
`;
|
|
2221
|
+
2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
|
|
1841
2222
|
return prompt;
|
|
1842
2223
|
}
|
|
1843
2224
|
getProjectConfig() {
|
|
@@ -1893,55 +2274,6 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
1893
2274
|
return "";
|
|
1894
2275
|
}
|
|
1895
2276
|
}
|
|
1896
|
-
getSkillsInfo() {
|
|
1897
|
-
const projectSkillsDirs = [
|
|
1898
|
-
LOCUS_CONFIG.agentSkillsDir,
|
|
1899
|
-
".cursor/skills",
|
|
1900
|
-
".claude/skills",
|
|
1901
|
-
".codex/skills",
|
|
1902
|
-
".gemini/skills"
|
|
1903
|
-
];
|
|
1904
|
-
const globalHome = import_node_os2.homedir();
|
|
1905
|
-
const globalSkillsDirs = [
|
|
1906
|
-
import_node_path6.join(globalHome, ".cursor/skills"),
|
|
1907
|
-
import_node_path6.join(globalHome, ".claude/skills"),
|
|
1908
|
-
import_node_path6.join(globalHome, ".codex/skills"),
|
|
1909
|
-
import_node_path6.join(globalHome, ".gemini/skills")
|
|
1910
|
-
];
|
|
1911
|
-
const allSkillNames = new Set;
|
|
1912
|
-
for (const relativePath of projectSkillsDirs) {
|
|
1913
|
-
const fullPath = import_node_path6.join(this.projectPath, relativePath);
|
|
1914
|
-
this.scanSkillsInDirectory(fullPath, allSkillNames);
|
|
1915
|
-
}
|
|
1916
|
-
for (const fullPath of globalSkillsDirs) {
|
|
1917
|
-
this.scanSkillsInDirectory(fullPath, allSkillNames);
|
|
1918
|
-
}
|
|
1919
|
-
const uniqueSkills = Array.from(allSkillNames).sort();
|
|
1920
|
-
if (uniqueSkills.length === 0)
|
|
1921
|
-
return "";
|
|
1922
|
-
return `## Available Agent Skills
|
|
1923
|
-
` + `The project has the following specialized skills available (from project or global locations):
|
|
1924
|
-
` + uniqueSkills.map((s) => `- ${s}`).join(`
|
|
1925
|
-
`) + `
|
|
1926
|
-
|
|
1927
|
-
`;
|
|
1928
|
-
}
|
|
1929
|
-
scanSkillsInDirectory(dirPath, skillSet) {
|
|
1930
|
-
if (!import_node_fs4.existsSync(dirPath))
|
|
1931
|
-
return;
|
|
1932
|
-
try {
|
|
1933
|
-
const entries = import_node_fs4.readdirSync(dirPath).filter((name) => {
|
|
1934
|
-
try {
|
|
1935
|
-
return import_node_fs4.statSync(import_node_path6.join(dirPath, name)).isDirectory();
|
|
1936
|
-
} catch {
|
|
1937
|
-
return false;
|
|
1938
|
-
}
|
|
1939
|
-
});
|
|
1940
|
-
for (const entry of entries) {
|
|
1941
|
-
skillSet.add(entry);
|
|
1942
|
-
}
|
|
1943
|
-
} catch {}
|
|
1944
|
-
}
|
|
1945
2277
|
roleToText(role) {
|
|
1946
2278
|
if (!role) {
|
|
1947
2279
|
return null;
|
|
@@ -1971,21 +2303,15 @@ class TaskExecutor {
|
|
|
1971
2303
|
this.deps = deps;
|
|
1972
2304
|
this.promptBuilder = new PromptBuilder(deps.projectPath);
|
|
1973
2305
|
}
|
|
1974
|
-
async execute(task
|
|
2306
|
+
async execute(task) {
|
|
1975
2307
|
this.deps.log(`Executing: ${task.title}`, "info");
|
|
1976
|
-
const basePrompt = await this.promptBuilder.build(task
|
|
1977
|
-
taskContext: context
|
|
1978
|
-
});
|
|
2308
|
+
const basePrompt = await this.promptBuilder.build(task);
|
|
1979
2309
|
try {
|
|
1980
2310
|
this.deps.log("Starting Execution...", "info");
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
When finished, output: <promise>COMPLETE</promise>`;
|
|
1984
|
-
const output = await this.deps.aiRunner.run(executionPrompt);
|
|
1985
|
-
const success = output.includes("<promise>COMPLETE</promise>");
|
|
2311
|
+
await this.deps.aiRunner.run(basePrompt);
|
|
1986
2312
|
return {
|
|
1987
|
-
success,
|
|
1988
|
-
summary:
|
|
2313
|
+
success: true,
|
|
2314
|
+
summary: "Task completed by the agent"
|
|
1989
2315
|
};
|
|
1990
2316
|
} catch (error) {
|
|
1991
2317
|
return { success: false, summary: `Error: ${error}` };
|
|
@@ -2009,12 +2335,17 @@ class AgentWorker {
|
|
|
2009
2335
|
config;
|
|
2010
2336
|
client;
|
|
2011
2337
|
aiRunner;
|
|
2012
|
-
indexerService;
|
|
2013
|
-
documentFetcher;
|
|
2014
2338
|
taskExecutor;
|
|
2015
|
-
|
|
2339
|
+
knowledgeBase;
|
|
2340
|
+
worktreeManager = null;
|
|
2341
|
+
prService = null;
|
|
2016
2342
|
maxTasks = 50;
|
|
2017
2343
|
tasksCompleted = 0;
|
|
2344
|
+
heartbeatInterval = null;
|
|
2345
|
+
currentTaskId = null;
|
|
2346
|
+
currentWorktreePath = null;
|
|
2347
|
+
postCleanupDelayMs = 5000;
|
|
2348
|
+
ghUsername = null;
|
|
2018
2349
|
constructor(config) {
|
|
2019
2350
|
this.config = config;
|
|
2020
2351
|
const projectPath = config.projectPath || process.cwd();
|
|
@@ -2029,35 +2360,47 @@ class AgentWorker {
|
|
|
2029
2360
|
}
|
|
2030
2361
|
});
|
|
2031
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
|
+
}
|
|
2032
2376
|
const provider = config.provider ?? PROVIDER.CLAUDE;
|
|
2033
2377
|
this.aiRunner = createAiRunner(provider, {
|
|
2034
2378
|
projectPath,
|
|
2035
2379
|
model: config.model,
|
|
2036
2380
|
log
|
|
2037
2381
|
});
|
|
2038
|
-
this.indexerService = new CodebaseIndexerService({
|
|
2039
|
-
aiRunner: this.aiRunner,
|
|
2040
|
-
projectPath,
|
|
2041
|
-
log
|
|
2042
|
-
});
|
|
2043
|
-
this.documentFetcher = new DocumentFetcher({
|
|
2044
|
-
client: this.client,
|
|
2045
|
-
workspaceId: config.workspaceId,
|
|
2046
|
-
projectPath,
|
|
2047
|
-
log
|
|
2048
|
-
});
|
|
2049
2382
|
this.taskExecutor = new TaskExecutor({
|
|
2050
2383
|
aiRunner: this.aiRunner,
|
|
2051
2384
|
projectPath,
|
|
2052
2385
|
log
|
|
2053
2386
|
});
|
|
2054
|
-
this.
|
|
2055
|
-
|
|
2056
|
-
projectPath,
|
|
2057
|
-
|
|
2058
|
-
|
|
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
|
+
}
|
|
2059
2396
|
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
2060
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
|
+
}
|
|
2061
2404
|
}
|
|
2062
2405
|
log(message, level = "info") {
|
|
2063
2406
|
const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
@@ -2081,49 +2424,258 @@ class AgentWorker {
|
|
|
2081
2424
|
}
|
|
2082
2425
|
}
|
|
2083
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
|
+
}
|
|
2084
2488
|
try {
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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 };
|
|
2090
2542
|
}
|
|
2091
2543
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
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");
|
|
2095
2551
|
try {
|
|
2096
|
-
|
|
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 };
|
|
2097
2560
|
} catch (err) {
|
|
2098
|
-
|
|
2561
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2562
|
+
this.log(`PR creation failed: ${errorMessage}`, "error");
|
|
2563
|
+
return { url: null, error: errorMessage };
|
|
2099
2564
|
}
|
|
2100
|
-
const result = await this.taskExecutor.execute(fullTask, context);
|
|
2101
|
-
await this.indexerService.reindex();
|
|
2102
|
-
return result;
|
|
2103
2565
|
}
|
|
2104
|
-
|
|
2566
|
+
cleanupTaskWorktree(worktreePath, keepBranch) {
|
|
2567
|
+
if (!this.worktreeManager || !worktreePath)
|
|
2568
|
+
return;
|
|
2105
2569
|
try {
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2570
|
+
this.worktreeManager.remove(worktreePath, !keepBranch);
|
|
2571
|
+
this.log(keepBranch ? "Worktree cleaned up (branch preserved)" : "Worktree cleaned up", "info");
|
|
2572
|
+
} catch {
|
|
2573
|
+
this.log(`Could not clean up worktree: ${worktreePath}`, "warn");
|
|
2574
|
+
}
|
|
2575
|
+
this.currentWorktreePath = null;
|
|
2576
|
+
}
|
|
2577
|
+
async executeTask(task) {
|
|
2578
|
+
const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
|
|
2579
|
+
const { worktreePath, baseBranch, executor } = this.createTaskWorktree(fullTask);
|
|
2580
|
+
this.currentWorktreePath = worktreePath;
|
|
2581
|
+
let branchPushed = false;
|
|
2582
|
+
let keepBranch = false;
|
|
2583
|
+
let preserveWorktree = false;
|
|
2584
|
+
try {
|
|
2585
|
+
const result = await executor.execute(fullTask);
|
|
2586
|
+
let taskBranch = null;
|
|
2587
|
+
let prUrl = null;
|
|
2588
|
+
let prError = null;
|
|
2589
|
+
let noChanges = false;
|
|
2590
|
+
if (result.success && worktreePath) {
|
|
2591
|
+
const commitResult = this.commitAndPushWorktree(worktreePath, fullTask);
|
|
2592
|
+
taskBranch = commitResult.branch;
|
|
2593
|
+
branchPushed = commitResult.pushed;
|
|
2594
|
+
keepBranch = taskBranch !== null;
|
|
2595
|
+
noChanges = Boolean(commitResult.noChanges);
|
|
2596
|
+
if (commitResult.pushFailed) {
|
|
2597
|
+
preserveWorktree = true;
|
|
2598
|
+
prError = commitResult.pushError ?? "Git push failed before PR creation. Please retry manually.";
|
|
2599
|
+
this.log(`Preserving worktree after push failure: ${worktreePath}`, "warn");
|
|
2600
|
+
}
|
|
2601
|
+
if (branchPushed && taskBranch) {
|
|
2602
|
+
const prResult = this.createPullRequest(fullTask, taskBranch, result.summary, baseBranch ?? undefined);
|
|
2603
|
+
prUrl = prResult.url;
|
|
2604
|
+
prError = prResult.error ?? null;
|
|
2605
|
+
if (!prUrl) {
|
|
2606
|
+
preserveWorktree = true;
|
|
2607
|
+
this.log(`Preserving worktree for manual follow-up: ${worktreePath}`, "warn");
|
|
2608
|
+
}
|
|
2609
|
+
} else if (commitResult.skipReason) {
|
|
2610
|
+
this.log(`Skipping PR creation: ${commitResult.skipReason}`, "info");
|
|
2111
2611
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
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;
|
|
2118
2625
|
} else {
|
|
2119
|
-
this.
|
|
2626
|
+
this.cleanupTaskWorktree(worktreePath, keepBranch);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
updateProgress(task, success) {
|
|
2631
|
+
try {
|
|
2632
|
+
if (success) {
|
|
2633
|
+
this.knowledgeBase.updateProgress({
|
|
2634
|
+
type: "task_completed",
|
|
2635
|
+
title: task.title,
|
|
2636
|
+
details: `Agent: ${this.config.agentId.slice(-8)}`
|
|
2637
|
+
});
|
|
2638
|
+
this.log(`Updated progress.md: ${task.title}`, "info");
|
|
2120
2639
|
}
|
|
2121
2640
|
} catch (err) {
|
|
2122
|
-
this.log(`
|
|
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;
|
|
2123
2654
|
}
|
|
2124
2655
|
}
|
|
2656
|
+
sendHeartbeat() {
|
|
2657
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, this.currentTaskId, this.currentTaskId ? "WORKING" : "IDLE").catch((err) => {
|
|
2658
|
+
this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
async delayAfterCleanup() {
|
|
2662
|
+
if (!this.config.useWorktrees || this.postCleanupDelayMs <= 0)
|
|
2663
|
+
return;
|
|
2664
|
+
this.log(`Waiting ${Math.floor(this.postCleanupDelayMs / 1000)}s after worktree cleanup before next dispatch`, "info");
|
|
2665
|
+
await new Promise((resolve3) => setTimeout(resolve3, this.postCleanupDelayMs));
|
|
2666
|
+
}
|
|
2125
2667
|
async run() {
|
|
2126
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();
|
|
2127
2679
|
const sprint = await this.getActiveSprint();
|
|
2128
2680
|
if (sprint) {
|
|
2129
2681
|
this.log(`Active sprint found: ${sprint.name}`, "info");
|
|
@@ -2133,31 +2685,62 @@ class AgentWorker {
|
|
|
2133
2685
|
while (this.tasksCompleted < this.maxTasks) {
|
|
2134
2686
|
const task = await this.getNextTask();
|
|
2135
2687
|
if (!task) {
|
|
2136
|
-
this.log("No tasks
|
|
2137
|
-
await this.runStagedChangesReview(sprint);
|
|
2688
|
+
this.log("No more tasks to process. Exiting.", "info");
|
|
2138
2689
|
break;
|
|
2139
2690
|
}
|
|
2140
2691
|
this.log(`Claimed: ${task.title}`, "success");
|
|
2692
|
+
this.currentTaskId = task.id;
|
|
2693
|
+
this.sendHeartbeat();
|
|
2141
2694
|
const result = await this.executeTask(task);
|
|
2142
|
-
try {
|
|
2143
|
-
await this.documentFetcher.fetch();
|
|
2144
|
-
} catch (err) {
|
|
2145
|
-
this.log(`Document fetch failed: ${err}`, "error");
|
|
2146
|
-
}
|
|
2147
2695
|
if (result.success) {
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
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
|
+
}
|
|
2157
2740
|
} else {
|
|
2158
2741
|
this.log(`Failed: ${task.title} - ${result.summary}`, "error");
|
|
2159
2742
|
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
2160
|
-
status:
|
|
2743
|
+
status: import_shared3.TaskStatus.BACKLOG,
|
|
2161
2744
|
assignedTo: null
|
|
2162
2745
|
});
|
|
2163
2746
|
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
@@ -2165,11 +2748,18 @@ class AgentWorker {
|
|
|
2165
2748
|
text: `❌ ${result.summary}`
|
|
2166
2749
|
});
|
|
2167
2750
|
}
|
|
2751
|
+
this.currentTaskId = null;
|
|
2752
|
+
this.sendHeartbeat();
|
|
2753
|
+
await this.delayAfterCleanup();
|
|
2168
2754
|
}
|
|
2755
|
+
this.currentTaskId = null;
|
|
2756
|
+
this.stopHeartbeat();
|
|
2757
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
|
|
2169
2758
|
process.exit(0);
|
|
2170
2759
|
}
|
|
2171
2760
|
}
|
|
2172
|
-
|
|
2761
|
+
var workerEntrypoint = process.argv[1]?.split(/[\\/]/).pop();
|
|
2762
|
+
if (workerEntrypoint === "worker.js" || workerEntrypoint === "worker.ts") {
|
|
2173
2763
|
process.title = "locus-worker";
|
|
2174
2764
|
const args = process.argv.slice(2);
|
|
2175
2765
|
const config = {};
|
|
@@ -2187,8 +2777,14 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
|
|
|
2187
2777
|
config.apiKey = args[++i];
|
|
2188
2778
|
else if (arg === "--project-path")
|
|
2189
2779
|
config.projectPath = args[++i];
|
|
2780
|
+
else if (arg === "--main-project-path")
|
|
2781
|
+
config.mainProjectPath = args[++i];
|
|
2190
2782
|
else if (arg === "--model")
|
|
2191
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;
|
|
2192
2788
|
else if (arg === "--provider") {
|
|
2193
2789
|
const value = args[i + 1];
|
|
2194
2790
|
if (value && !value.startsWith("--"))
|