@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/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,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;
|
|
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 };
|
|
1252
1407
|
}
|
|
1253
|
-
|
|
1254
|
-
if (!
|
|
1255
|
-
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.`);
|
|
1256
1411
|
}
|
|
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
|
-
}
|
|
1412
|
+
if (!this.hasRemoteBranch(headBranch)) {
|
|
1413
|
+
throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
|
|
1297
1414
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
} catch (error) {
|
|
1303
|
-
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.`);
|
|
1304
1419
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const
|
|
1309
|
-
if (
|
|
1310
|
-
|
|
1311
|
-
const content = import_node_fs2.readFileSync(gitmodulesPath, "utf-8");
|
|
1312
|
-
const lines = content.split(`
|
|
1313
|
-
`);
|
|
1314
|
-
for (const line of lines) {
|
|
1315
|
-
const match = line.match(/^\s*path\s*=\s*(.*)$/);
|
|
1316
|
-
const path = match?.[1]?.trim();
|
|
1317
|
-
if (path) {
|
|
1318
|
-
submoduleIgnores.push(`${path}/**`);
|
|
1319
|
-
submoduleIgnores.push(`**/${path}/**`);
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
} catch {}
|
|
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
|
-
|
|
1427
|
+
}
|
|
1428
|
+
countCommitsAhead(baseRef, headRef) {
|
|
1429
|
+
const output = import_node_child_process4.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
|
|
1325
1430
|
cwd: this.projectPath,
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
-
});
|
|
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;
|
|
1365
1436
|
}
|
|
1366
|
-
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
}
|
|
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
|
+
});
|
|
1486
|
+
}
|
|
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
|
+
}
|
|
1394
1513
|
}
|
|
1395
|
-
|
|
1514
|
+
listLocusPrs() {
|
|
1396
1515
|
try {
|
|
1397
|
-
const
|
|
1398
|
-
|
|
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;
|
|
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.`);
|
|
1523
1774
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
}
|
|
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");
|
|
1538
1863
|
}
|
|
1864
|
+
}
|
|
1865
|
+
this.log("Worktree removed", "success");
|
|
1866
|
+
}
|
|
1867
|
+
prune() {
|
|
1868
|
+
const before = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
1869
|
+
this.git("worktree prune", this.projectPath);
|
|
1870
|
+
const after = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
|
|
1871
|
+
const pruned = before - after;
|
|
1872
|
+
if (pruned > 0) {
|
|
1873
|
+
this.log(`Pruned ${pruned} stale worktree(s)`, "success");
|
|
1874
|
+
}
|
|
1875
|
+
return pruned;
|
|
1876
|
+
}
|
|
1877
|
+
removeAll() {
|
|
1878
|
+
const agentWorktrees = this.listAgentWorktrees();
|
|
1879
|
+
let removed = 0;
|
|
1880
|
+
for (const wt of agentWorktrees) {
|
|
1881
|
+
try {
|
|
1882
|
+
this.remove(wt.path, true);
|
|
1883
|
+
removed++;
|
|
1884
|
+
} catch {
|
|
1885
|
+
this.log(`Failed to remove worktree: ${wt.path}`, "warn");
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
if (import_node_fs3.existsSync(this.rootPath)) {
|
|
1889
|
+
try {
|
|
1890
|
+
import_node_fs3.rmSync(this.rootPath, { recursive: true, force: true });
|
|
1891
|
+
} catch {}
|
|
1892
|
+
}
|
|
1893
|
+
return removed;
|
|
1894
|
+
}
|
|
1895
|
+
hasChanges(worktreePath) {
|
|
1896
|
+
const status = this.git("status --porcelain", worktreePath).trim();
|
|
1897
|
+
return status.length > 0;
|
|
1898
|
+
}
|
|
1899
|
+
commitChanges(worktreePath, message) {
|
|
1900
|
+
if (!this.hasChanges(worktreePath)) {
|
|
1901
|
+
this.log("No changes to commit", "info");
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
this.git("add -A", worktreePath);
|
|
1905
|
+
this.gitExec(["commit", "-m", message], worktreePath);
|
|
1906
|
+
const hash = this.git("rev-parse HEAD", worktreePath).trim();
|
|
1907
|
+
this.log(`Committed: ${hash.slice(0, 8)}`, "success");
|
|
1908
|
+
return hash;
|
|
1909
|
+
}
|
|
1910
|
+
pushBranch(worktreePath, remote = "origin") {
|
|
1911
|
+
const branch = this.getBranch(worktreePath);
|
|
1912
|
+
this.log(`Pushing branch ${branch} to ${remote}`, "info");
|
|
1913
|
+
try {
|
|
1914
|
+
this.gitExec(["push", "-u", remote, branch], worktreePath);
|
|
1915
|
+
this.log(`Pushed ${branch} to ${remote}`, "success");
|
|
1916
|
+
return branch;
|
|
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;
|
|
1541
1942
|
}
|
|
1542
1943
|
}
|
|
1944
|
+
getCurrentBranch() {
|
|
1945
|
+
return this.git("rev-parse --abbrev-ref HEAD", this.projectPath).trim();
|
|
1946
|
+
}
|
|
1947
|
+
isManagedWorktreePath(worktreePath) {
|
|
1948
|
+
const rootPath = import_node_path5.resolve(this.rootPath);
|
|
1949
|
+
const candidate = import_node_path5.resolve(worktreePath);
|
|
1950
|
+
const rootWithSep = rootPath.endsWith(import_node_path5.sep) ? rootPath : `${rootPath}${import_node_path5.sep}`;
|
|
1951
|
+
return candidate.startsWith(rootWithSep);
|
|
1952
|
+
}
|
|
1953
|
+
ensureDirectory(dirPath, label) {
|
|
1954
|
+
if (import_node_fs3.existsSync(dirPath)) {
|
|
1955
|
+
if (!import_node_fs3.statSync(dirPath).isDirectory()) {
|
|
1956
|
+
throw new Error(`${label} exists but is not a directory: ${dirPath}`);
|
|
1957
|
+
}
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
import_node_fs3.mkdirSync(dirPath, { recursive: true });
|
|
1961
|
+
}
|
|
1962
|
+
isMissingDirectoryError(error) {
|
|
1963
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1964
|
+
return message.includes("cannot create directory") || message.includes("No such file or directory");
|
|
1965
|
+
}
|
|
1966
|
+
cleanupFailedWorktree(worktreePath, branch) {
|
|
1967
|
+
try {
|
|
1968
|
+
this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
|
|
1969
|
+
} catch {}
|
|
1970
|
+
if (import_node_fs3.existsSync(worktreePath)) {
|
|
1971
|
+
import_node_fs3.rmSync(worktreePath, { recursive: true, force: true });
|
|
1972
|
+
}
|
|
1973
|
+
try {
|
|
1974
|
+
this.git("worktree prune", this.projectPath);
|
|
1975
|
+
} catch {}
|
|
1976
|
+
if (this.branchExists(branch)) {
|
|
1977
|
+
try {
|
|
1978
|
+
this.git(`branch -D "${branch}"`, this.projectPath);
|
|
1979
|
+
} catch {}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
isNonFastForwardPushError(error) {
|
|
1983
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1984
|
+
return message.includes("non-fast-forward") || message.includes("[rejected]") || message.includes("fetch first");
|
|
1985
|
+
}
|
|
1986
|
+
git(args, cwd) {
|
|
1987
|
+
return import_node_child_process5.execSync(`git ${args}`, {
|
|
1988
|
+
cwd,
|
|
1989
|
+
encoding: "utf-8",
|
|
1990
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
gitExec(args, cwd) {
|
|
1994
|
+
return import_node_child_process5.execFileSync("git", args, {
|
|
1995
|
+
cwd,
|
|
1996
|
+
encoding: "utf-8",
|
|
1997
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
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.indexerService = new CodebaseIndexerService({
|
|
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
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,74 +2424,323 @@ 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 };
|
|
2038
2564
|
}
|
|
2039
2565
|
}
|
|
2566
|
+
cleanupTaskWorktree(worktreePath, keepBranch) {
|
|
2567
|
+
if (!this.worktreeManager || !worktreePath)
|
|
2568
|
+
return;
|
|
2569
|
+
try {
|
|
2570
|
+
this.worktreeManager.remove(worktreePath, !keepBranch);
|
|
2571
|
+
this.log(keepBranch ? "Worktree cleaned up (branch preserved)" : "Worktree cleaned up", "info");
|
|
2572
|
+
} catch {
|
|
2573
|
+
this.log(`Could not clean up worktree: ${worktreePath}`, "warn");
|
|
2574
|
+
}
|
|
2575
|
+
this.currentWorktreePath = null;
|
|
2576
|
+
}
|
|
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");
|
|
2047
2642
|
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2643
|
+
}
|
|
2644
|
+
startHeartbeat() {
|
|
2645
|
+
this.sendHeartbeat();
|
|
2646
|
+
this.heartbeatInterval = setInterval(() => {
|
|
2647
|
+
this.sendHeartbeat();
|
|
2648
|
+
}, 60000);
|
|
2649
|
+
}
|
|
2650
|
+
stopHeartbeat() {
|
|
2651
|
+
if (this.heartbeatInterval) {
|
|
2652
|
+
clearInterval(this.heartbeatInterval);
|
|
2653
|
+
this.heartbeatInterval = null;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
sendHeartbeat() {
|
|
2657
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, this.currentTaskId, this.currentTaskId ? "WORKING" : "IDLE").catch((err) => {
|
|
2658
|
+
this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
async delayAfterCleanup() {
|
|
2662
|
+
if (!this.config.useWorktrees || this.postCleanupDelayMs <= 0)
|
|
2663
|
+
return;
|
|
2664
|
+
this.log(`Waiting ${Math.floor(this.postCleanupDelayMs / 1000)}s after worktree cleanup before next dispatch`, "info");
|
|
2665
|
+
await new Promise((resolve3) => setTimeout(resolve3, this.postCleanupDelayMs));
|
|
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");
|
|
2065
2684
|
}
|
|
2066
|
-
while (this.tasksCompleted < this.maxTasks
|
|
2685
|
+
while (this.tasksCompleted < this.maxTasks) {
|
|
2067
2686
|
const task = await this.getNextTask();
|
|
2068
2687
|
if (!task) {
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
}
|
|
2072
|
-
this.consecutiveEmpty++;
|
|
2073
|
-
if (this.consecutiveEmpty >= this.maxEmpty)
|
|
2074
|
-
break;
|
|
2075
|
-
await new Promise((r) => setTimeout(r, this.pollInterval));
|
|
2076
|
-
continue;
|
|
2688
|
+
this.log("No more tasks to process. Exiting.", "info");
|
|
2689
|
+
break;
|
|
2077
2690
|
}
|
|
2078
|
-
this.consecutiveEmpty = 0;
|
|
2079
2691
|
this.log(`Claimed: ${task.title}`, "success");
|
|
2692
|
+
this.currentTaskId = task.id;
|
|
2693
|
+
this.sendHeartbeat();
|
|
2080
2694
|
const result = await this.executeTask(task);
|
|
2081
|
-
try {
|
|
2082
|
-
await this.documentFetcher.fetch();
|
|
2083
|
-
} catch (err) {
|
|
2084
|
-
this.log(`Document fetch failed: ${err}`, "error");
|
|
2085
|
-
}
|
|
2086
2695
|
if (result.success) {
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
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
|
+
}
|
|
2096
2740
|
} else {
|
|
2097
2741
|
this.log(`Failed: ${task.title} - ${result.summary}`, "error");
|
|
2098
2742
|
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
2099
|
-
status:
|
|
2743
|
+
status: import_shared3.TaskStatus.BACKLOG,
|
|
2100
2744
|
assignedTo: null
|
|
2101
2745
|
});
|
|
2102
2746
|
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
@@ -2104,11 +2748,18 @@ class AgentWorker {
|
|
|
2104
2748
|
text: `❌ ${result.summary}`
|
|
2105
2749
|
});
|
|
2106
2750
|
}
|
|
2751
|
+
this.currentTaskId = null;
|
|
2752
|
+
this.sendHeartbeat();
|
|
2753
|
+
await this.delayAfterCleanup();
|
|
2107
2754
|
}
|
|
2755
|
+
this.currentTaskId = null;
|
|
2756
|
+
this.stopHeartbeat();
|
|
2757
|
+
this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
|
|
2108
2758
|
process.exit(0);
|
|
2109
2759
|
}
|
|
2110
2760
|
}
|
|
2111
|
-
|
|
2761
|
+
var workerEntrypoint = process.argv[1]?.split(/[\\/]/).pop();
|
|
2762
|
+
if (workerEntrypoint === "worker.js" || workerEntrypoint === "worker.ts") {
|
|
2112
2763
|
process.title = "locus-worker";
|
|
2113
2764
|
const args = process.argv.slice(2);
|
|
2114
2765
|
const config = {};
|
|
@@ -2126,8 +2777,14 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
|
|
|
2126
2777
|
config.apiKey = args[++i];
|
|
2127
2778
|
else if (arg === "--project-path")
|
|
2128
2779
|
config.projectPath = args[++i];
|
|
2780
|
+
else if (arg === "--main-project-path")
|
|
2781
|
+
config.mainProjectPath = args[++i];
|
|
2129
2782
|
else if (arg === "--model")
|
|
2130
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;
|
|
2131
2788
|
else if (arg === "--provider") {
|
|
2132
2789
|
const value = args[i + 1];
|
|
2133
2790
|
if (value && !value.startsWith("--"))
|