@locusai/sdk 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/artifact-syncer.d.ts +17 -0
- package/dist/agent/artifact-syncer.d.ts.map +1 -0
- package/dist/agent/artifact-syncer.js +77 -0
- package/dist/agent/codebase-indexer-service.d.ts +18 -0
- package/dist/agent/codebase-indexer-service.d.ts.map +1 -0
- package/dist/agent/codebase-indexer-service.js +55 -0
- package/dist/agent/index.d.ts +6 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +5 -0
- package/dist/agent/sprint-planner.d.ts +17 -0
- package/dist/agent/sprint-planner.d.ts.map +1 -0
- package/dist/agent/sprint-planner.js +62 -0
- package/dist/agent/task-executor.d.ts +24 -0
- package/dist/agent/task-executor.d.ts.map +1 -0
- package/dist/agent/task-executor.js +56 -0
- package/dist/agent/worker.d.ts +37 -0
- package/dist/agent/worker.d.ts.map +1 -0
- package/dist/agent/worker.js +232 -0
- package/dist/ai/anthropic-client.d.ts +33 -0
- package/dist/ai/anthropic-client.d.ts.map +1 -0
- package/dist/ai/anthropic-client.js +70 -0
- package/dist/ai/claude-runner.d.ts +7 -0
- package/dist/ai/claude-runner.d.ts.map +1 -0
- package/dist/ai/claude-runner.js +43 -0
- package/dist/ai/index.d.ts +3 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +2 -0
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +15 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/indexer.d.ts +18 -0
- package/dist/core/indexer.d.ts.map +1 -0
- package/dist/core/indexer.js +73 -0
- package/dist/core/prompt-builder.d.ts +8 -0
- package/dist/core/prompt-builder.d.ts.map +1 -0
- package/dist/core/prompt-builder.js +87 -0
- package/dist/events.d.ts +20 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +15 -0
- package/dist/index-node.d.ts +14 -0
- package/dist/index-node.d.ts.map +1 -0
- package/dist/index-node.js +18 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/modules/auth.d.ts +14 -0
- package/dist/modules/auth.d.ts.map +1 -0
- package/dist/modules/auth.js +23 -0
- package/dist/modules/base.d.ts +8 -0
- package/dist/modules/base.d.ts.map +1 -0
- package/dist/modules/base.js +8 -0
- package/dist/modules/ci.d.ts +8 -0
- package/dist/modules/ci.d.ts.map +1 -0
- package/dist/modules/ci.js +7 -0
- package/dist/modules/docs.d.ts +14 -0
- package/dist/modules/docs.d.ts.map +1 -0
- package/dist/modules/docs.js +38 -0
- package/dist/modules/invitations.d.ts +10 -0
- package/dist/modules/invitations.d.ts.map +1 -0
- package/dist/modules/invitations.js +22 -0
- package/dist/modules/organizations.d.ts +24 -0
- package/dist/modules/organizations.d.ts.map +1 -0
- package/dist/modules/organizations.js +39 -0
- package/dist/modules/sprints.d.ts +13 -0
- package/dist/modules/sprints.d.ts.map +1 -0
- package/dist/modules/sprints.js +34 -0
- package/dist/modules/tasks.d.ts +24 -0
- package/dist/modules/tasks.d.ts.map +1 -0
- package/dist/modules/tasks.js +56 -0
- package/dist/modules/workspaces.d.ts +21 -0
- package/dist/modules/workspaces.d.ts.map +1 -0
- package/dist/modules/workspaces.js +49 -0
- package/dist/orchestrator.d.ts +90 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +326 -0
- package/package.json +53 -0
- package/src/agent/artifact-syncer.ts +111 -0
- package/src/agent/codebase-indexer-service.ts +71 -0
- package/src/agent/index.ts +5 -0
- package/src/agent/sprint-planner.ts +78 -0
- package/src/agent/task-executor.ts +77 -0
- package/src/agent/worker.ts +299 -0
- package/src/ai/anthropic-client.ts +93 -0
- package/src/ai/claude-runner.ts +49 -0
- package/src/ai/index.ts +2 -0
- package/src/core/config.ts +21 -0
- package/src/core/index.ts +3 -0
- package/src/core/indexer.ts +91 -0
- package/src/core/prompt-builder.ts +100 -0
- package/src/events.ts +32 -0
- package/src/index-node.ts +20 -0
- package/src/index.ts +119 -0
- package/src/modules/auth.ts +48 -0
- package/src/modules/base.ts +9 -0
- package/src/modules/ci.ts +12 -0
- package/src/modules/docs.ts +84 -0
- package/src/modules/invitations.ts +45 -0
- package/src/modules/organizations.ts +90 -0
- package/src/modules/sprints.ts +69 -0
- package/src/modules/tasks.ts +110 -0
- package/src/modules/workspaces.ts +94 -0
- package/src/orchestrator.ts +430 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { TaskPriority, TaskStatus } from "@locusai/shared";
|
|
5
|
+
import { EventEmitter } from "events";
|
|
6
|
+
import { LocusClient } from "./index";
|
|
7
|
+
export class AgentOrchestrator extends EventEmitter {
|
|
8
|
+
client;
|
|
9
|
+
config;
|
|
10
|
+
agents = new Map();
|
|
11
|
+
isRunning = false;
|
|
12
|
+
processedTasks = new Set();
|
|
13
|
+
resolvedSprintId = null;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
super();
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.client = new LocusClient({
|
|
18
|
+
baseUrl: config.apiBase,
|
|
19
|
+
token: config.apiKey,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the sprint ID - use provided or find active sprint
|
|
24
|
+
*/
|
|
25
|
+
async resolveSprintId() {
|
|
26
|
+
if (this.config.sprintId) {
|
|
27
|
+
return this.config.sprintId;
|
|
28
|
+
}
|
|
29
|
+
// Try to find active sprint in workspace
|
|
30
|
+
try {
|
|
31
|
+
const sprint = await this.client.sprints.getActive(this.config.workspaceId);
|
|
32
|
+
if (sprint?.id) {
|
|
33
|
+
console.log(`📋 Using active sprint: ${sprint.name}`);
|
|
34
|
+
return sprint.id;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// No active sprint found, will work with all tasks
|
|
39
|
+
}
|
|
40
|
+
console.log("ℹ No sprint specified, working with all workspace tasks");
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Start the orchestrator with N agents
|
|
45
|
+
*/
|
|
46
|
+
async start() {
|
|
47
|
+
if (this.isRunning) {
|
|
48
|
+
throw new Error("Orchestrator is already running");
|
|
49
|
+
}
|
|
50
|
+
this.isRunning = true;
|
|
51
|
+
this.processedTasks.clear();
|
|
52
|
+
try {
|
|
53
|
+
await this.orchestrationLoop();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
this.emit("error", error);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await this.cleanup();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Main orchestration loop - runs 1 agent continuously
|
|
65
|
+
*/
|
|
66
|
+
async orchestrationLoop() {
|
|
67
|
+
// Resolve sprint ID first
|
|
68
|
+
this.resolvedSprintId = await this.resolveSprintId();
|
|
69
|
+
this.emit("started", {
|
|
70
|
+
timestamp: new Date(),
|
|
71
|
+
config: this.config,
|
|
72
|
+
sprintId: this.resolvedSprintId,
|
|
73
|
+
});
|
|
74
|
+
console.log("\n🤖 Locus Agent Orchestrator");
|
|
75
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
76
|
+
console.log(`Workspace: ${this.config.workspaceId}`);
|
|
77
|
+
if (this.resolvedSprintId) {
|
|
78
|
+
console.log(`Sprint: ${this.resolvedSprintId}`);
|
|
79
|
+
}
|
|
80
|
+
console.log(`API Base: ${this.config.apiBase}`);
|
|
81
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
|
82
|
+
// Check if there are tasks to work on before spawning
|
|
83
|
+
const tasks = await this.getAvailableTasks();
|
|
84
|
+
if (tasks.length === 0) {
|
|
85
|
+
console.log("ℹ No available tasks found in the backlog.");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Spawn single agent
|
|
89
|
+
await this.spawnAgent();
|
|
90
|
+
// Wait for agent to complete
|
|
91
|
+
while (this.agents.size > 0 && this.isRunning) {
|
|
92
|
+
await this.reapAgents();
|
|
93
|
+
if (this.agents.size === 0) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
await this.sleep(2000);
|
|
97
|
+
}
|
|
98
|
+
console.log("\n✅ Orchestrator finished");
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Spawn a single agent process
|
|
102
|
+
*/
|
|
103
|
+
async spawnAgent() {
|
|
104
|
+
const agentId = `agent-${Date.now()}-${Math.random()
|
|
105
|
+
.toString(36)
|
|
106
|
+
.slice(2, 9)}`;
|
|
107
|
+
const agentState = {
|
|
108
|
+
id: agentId,
|
|
109
|
+
status: "IDLE",
|
|
110
|
+
currentTaskId: null,
|
|
111
|
+
tasksCompleted: 0,
|
|
112
|
+
tasksFailed: 0,
|
|
113
|
+
lastHeartbeat: new Date(),
|
|
114
|
+
};
|
|
115
|
+
this.agents.set(agentId, agentState);
|
|
116
|
+
console.log(`🚀 Agent started: ${agentId}\n`);
|
|
117
|
+
// Build arguments for agent worker
|
|
118
|
+
// Resolve path relative to this file's location (works in both dev and production)
|
|
119
|
+
const workerPath = join(__dirname, "agent", "worker.js");
|
|
120
|
+
// Verify worker file exists
|
|
121
|
+
if (!existsSync(workerPath)) {
|
|
122
|
+
throw new Error(`Worker file not found at ${workerPath}. ` +
|
|
123
|
+
`Make sure the SDK is properly built. __dirname: ${__dirname}`);
|
|
124
|
+
}
|
|
125
|
+
const workerArgs = [
|
|
126
|
+
"--agent-id",
|
|
127
|
+
agentId,
|
|
128
|
+
"--workspace-id",
|
|
129
|
+
this.config.workspaceId,
|
|
130
|
+
"--api-base",
|
|
131
|
+
this.config.apiBase,
|
|
132
|
+
"--api-key",
|
|
133
|
+
this.config.apiKey,
|
|
134
|
+
"--project-path",
|
|
135
|
+
this.config.projectPath,
|
|
136
|
+
];
|
|
137
|
+
// Add anthropic API key if provided
|
|
138
|
+
if (this.config.anthropicApiKey) {
|
|
139
|
+
workerArgs.push("--anthropic-api-key", this.config.anthropicApiKey);
|
|
140
|
+
}
|
|
141
|
+
// Add model if specified
|
|
142
|
+
if (this.config.model) {
|
|
143
|
+
workerArgs.push("--model", this.config.model);
|
|
144
|
+
}
|
|
145
|
+
// Add sprint ID if resolved
|
|
146
|
+
if (this.resolvedSprintId) {
|
|
147
|
+
workerArgs.push("--sprint-id", this.resolvedSprintId);
|
|
148
|
+
}
|
|
149
|
+
// Use bun to run TypeScript files directly
|
|
150
|
+
const workerTsPath = workerPath.replace(/\.js$/, ".ts");
|
|
151
|
+
const agentProcess = spawn("bun", ["run", workerTsPath, ...workerArgs]);
|
|
152
|
+
agentState.process = agentProcess;
|
|
153
|
+
agentProcess.on("message", (msg) => {
|
|
154
|
+
if (msg.type === "stats") {
|
|
155
|
+
agentState.tasksCompleted = msg.tasksCompleted || 0;
|
|
156
|
+
agentState.tasksFailed = msg.tasksFailed || 0;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
agentProcess.stdout?.on("data", (data) => {
|
|
160
|
+
process.stdout.write(data.toString());
|
|
161
|
+
});
|
|
162
|
+
agentProcess.stderr?.on("data", (data) => {
|
|
163
|
+
process.stderr.write(`[${agentId}] ERR: ${data.toString()}`);
|
|
164
|
+
});
|
|
165
|
+
agentProcess.on("exit", (code) => {
|
|
166
|
+
console.log(`\n${agentId} finished (exit code: ${code})`);
|
|
167
|
+
const agent = this.agents.get(agentId);
|
|
168
|
+
if (agent) {
|
|
169
|
+
agent.status = code === 0 ? "COMPLETED" : "FAILED";
|
|
170
|
+
// Ensure CLI gets the absolute latest stats
|
|
171
|
+
this.emit("agent:completed", {
|
|
172
|
+
agentId,
|
|
173
|
+
status: agent.status,
|
|
174
|
+
tasksCompleted: agent.tasksCompleted,
|
|
175
|
+
tasksFailed: agent.tasksFailed,
|
|
176
|
+
});
|
|
177
|
+
// Remove from active tracking after emitting
|
|
178
|
+
this.agents.delete(agentId);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
this.emit("agent:spawned", { agentId });
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Reap completed agents
|
|
185
|
+
*/
|
|
186
|
+
async reapAgents() {
|
|
187
|
+
// No-op: agents now remove themselves in the 'exit' listener
|
|
188
|
+
// to ensure events are emitted with correct stats before deletion.
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get available tasks in sprint
|
|
192
|
+
*/
|
|
193
|
+
async getAvailableTasks() {
|
|
194
|
+
try {
|
|
195
|
+
const tasks = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
|
|
196
|
+
return tasks.filter((task) => !this.processedTasks.has(task.id));
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
this.emit("error", error);
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Assign task to agent
|
|
205
|
+
*/
|
|
206
|
+
async assignTaskToAgent(agentId) {
|
|
207
|
+
const agent = this.agents.get(agentId);
|
|
208
|
+
if (!agent)
|
|
209
|
+
return null;
|
|
210
|
+
try {
|
|
211
|
+
const tasks = await this.getAvailableTasks();
|
|
212
|
+
const priorityOrder = [
|
|
213
|
+
TaskPriority.CRITICAL,
|
|
214
|
+
TaskPriority.HIGH,
|
|
215
|
+
TaskPriority.MEDIUM,
|
|
216
|
+
TaskPriority.LOW,
|
|
217
|
+
];
|
|
218
|
+
// Find task with highest priority
|
|
219
|
+
let task = tasks.sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority))[0];
|
|
220
|
+
// Fallback: any available task
|
|
221
|
+
if (!task && tasks.length > 0) {
|
|
222
|
+
task = tasks[0];
|
|
223
|
+
}
|
|
224
|
+
if (!task)
|
|
225
|
+
return null;
|
|
226
|
+
agent.currentTaskId = task.id;
|
|
227
|
+
agent.status = "WORKING";
|
|
228
|
+
this.emit("task:assigned", {
|
|
229
|
+
agentId,
|
|
230
|
+
taskId: task.id,
|
|
231
|
+
title: task.title,
|
|
232
|
+
});
|
|
233
|
+
return task;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
this.emit("error", error);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Mark task as completed by agent
|
|
242
|
+
*/
|
|
243
|
+
async completeTask(taskId, agentId, summary) {
|
|
244
|
+
try {
|
|
245
|
+
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
246
|
+
status: TaskStatus.VERIFICATION,
|
|
247
|
+
});
|
|
248
|
+
if (summary) {
|
|
249
|
+
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
250
|
+
author: agentId,
|
|
251
|
+
text: `✅ Task completed\n\n${summary}`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
this.processedTasks.add(taskId);
|
|
255
|
+
const agent = this.agents.get(agentId);
|
|
256
|
+
if (agent) {
|
|
257
|
+
agent.tasksCompleted += 1;
|
|
258
|
+
agent.currentTaskId = null;
|
|
259
|
+
agent.status = "IDLE";
|
|
260
|
+
}
|
|
261
|
+
this.emit("task:completed", { agentId, taskId });
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
this.emit("error", error);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Mark task as failed
|
|
269
|
+
*/
|
|
270
|
+
async failTask(taskId, agentId, error) {
|
|
271
|
+
try {
|
|
272
|
+
await this.client.tasks.update(taskId, this.config.workspaceId, {
|
|
273
|
+
status: TaskStatus.BACKLOG,
|
|
274
|
+
assignedTo: null,
|
|
275
|
+
});
|
|
276
|
+
await this.client.tasks.addComment(taskId, this.config.workspaceId, {
|
|
277
|
+
author: agentId,
|
|
278
|
+
text: `❌ Agent failed: ${error}`,
|
|
279
|
+
});
|
|
280
|
+
const agent = this.agents.get(agentId);
|
|
281
|
+
if (agent) {
|
|
282
|
+
agent.tasksFailed += 1;
|
|
283
|
+
agent.currentTaskId = null;
|
|
284
|
+
agent.status = "IDLE";
|
|
285
|
+
}
|
|
286
|
+
this.emit("task:failed", { agentId, taskId, error });
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
this.emit("error", error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Stop orchestrator
|
|
294
|
+
*/
|
|
295
|
+
async stop() {
|
|
296
|
+
this.isRunning = false;
|
|
297
|
+
await this.cleanup();
|
|
298
|
+
this.emit("stopped", { timestamp: new Date() });
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Cleanup - kill all agent processes
|
|
302
|
+
*/
|
|
303
|
+
async cleanup() {
|
|
304
|
+
for (const [agentId, agent] of this.agents.entries()) {
|
|
305
|
+
if (agent.process && !agent.process.killed) {
|
|
306
|
+
console.log(`Killing agent: ${agentId}`);
|
|
307
|
+
agent.process.kill();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
this.agents.clear();
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get orchestrator stats
|
|
314
|
+
*/
|
|
315
|
+
getStats() {
|
|
316
|
+
return {
|
|
317
|
+
activeAgents: this.agents.size,
|
|
318
|
+
processedTasks: this.processedTasks.size,
|
|
319
|
+
totalTasksCompleted: Array.from(this.agents.values()).reduce((sum, agent) => sum + agent.tasksCompleted, 0),
|
|
320
|
+
totalTasksFailed: Array.from(this.agents.values()).reduce((sum, agent) => sum + agent.tasksFailed, 0),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
sleep(ms) {
|
|
324
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
325
|
+
}
|
|
326
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@locusai/sdk",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"default": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"./node": {
|
|
13
|
+
"types": "./src/index-node.ts",
|
|
14
|
+
"default": "./src/index-node.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./node": {
|
|
31
|
+
"types": "./dist/index-node.d.ts",
|
|
32
|
+
"default": "./dist/index-node.js"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"dev": "tsc --watch",
|
|
39
|
+
"lint": "biome lint .",
|
|
40
|
+
"typecheck": "tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@anthropic-ai/sdk": "^0.71.2",
|
|
44
|
+
"@locusai/shared": "workspace:*",
|
|
45
|
+
"axios": "^1.13.2",
|
|
46
|
+
"events": "^3.3.0",
|
|
47
|
+
"globby": "^14.0.2"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"typescript": "5.8.3",
|
|
51
|
+
"@types/node": "^22.10.7"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
statSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { getLocusPath } from "../core/config";
|
|
10
|
+
import type { LocusClient } from "../index";
|
|
11
|
+
|
|
12
|
+
export interface ArtifactSyncerDeps {
|
|
13
|
+
client: LocusClient;
|
|
14
|
+
workspaceId: string;
|
|
15
|
+
projectPath: string;
|
|
16
|
+
log: (message: string, level?: "info" | "success" | "warn" | "error") => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handles syncing local artifacts to the platform
|
|
21
|
+
*/
|
|
22
|
+
export class ArtifactSyncer {
|
|
23
|
+
constructor(private deps: ArtifactSyncerDeps) {}
|
|
24
|
+
|
|
25
|
+
private async getOrCreateArtifactsGroup(): Promise<string> {
|
|
26
|
+
try {
|
|
27
|
+
const groups = await this.deps.client.docs.listGroups(
|
|
28
|
+
this.deps.workspaceId
|
|
29
|
+
);
|
|
30
|
+
const artifactsGroup = groups.find((g) => g.name === "Artifacts");
|
|
31
|
+
|
|
32
|
+
if (artifactsGroup) {
|
|
33
|
+
return artifactsGroup.id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create the Artifacts group
|
|
37
|
+
const newGroup = await this.deps.client.docs.createGroup(
|
|
38
|
+
this.deps.workspaceId,
|
|
39
|
+
{
|
|
40
|
+
name: "Artifacts",
|
|
41
|
+
order: 999, // Place at the end
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
this.deps.log(
|
|
45
|
+
"Created 'Artifacts' group for agent-generated docs",
|
|
46
|
+
"info"
|
|
47
|
+
);
|
|
48
|
+
return newGroup.id;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
this.deps.log(`Failed to get/create Artifacts group: ${error}`, "error");
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async sync(): Promise<void> {
|
|
56
|
+
const artifactsDir = getLocusPath(this.deps.projectPath, "artifactsDir");
|
|
57
|
+
if (!existsSync(artifactsDir)) {
|
|
58
|
+
mkdirSync(artifactsDir, { recursive: true });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const files = readdirSync(artifactsDir);
|
|
64
|
+
if (files.length === 0) return;
|
|
65
|
+
|
|
66
|
+
this.deps.log(`Syncing ${files.length} artifacts to server...`, "info");
|
|
67
|
+
|
|
68
|
+
// Get or create the Artifacts group
|
|
69
|
+
const artifactsGroupId = await this.getOrCreateArtifactsGroup();
|
|
70
|
+
|
|
71
|
+
// Get existing docs to check for updates
|
|
72
|
+
const existingDocs = await this.deps.client.docs.list(
|
|
73
|
+
this.deps.workspaceId
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const filePath = join(artifactsDir, file);
|
|
78
|
+
if (statSync(filePath).isFile()) {
|
|
79
|
+
const content = readFileSync(filePath, "utf-8");
|
|
80
|
+
const title = file.replace(/\.md$/, "").trim();
|
|
81
|
+
if (!title) continue;
|
|
82
|
+
|
|
83
|
+
const existing = existingDocs.find((d) => d.title === title);
|
|
84
|
+
|
|
85
|
+
if (existing) {
|
|
86
|
+
if (
|
|
87
|
+
existing.content !== content ||
|
|
88
|
+
existing.groupId !== artifactsGroupId
|
|
89
|
+
) {
|
|
90
|
+
await this.deps.client.docs.update(
|
|
91
|
+
existing.id,
|
|
92
|
+
this.deps.workspaceId,
|
|
93
|
+
{ content, groupId: artifactsGroupId }
|
|
94
|
+
);
|
|
95
|
+
this.deps.log(`Updated artifact: ${file}`, "success");
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
await this.deps.client.docs.create(this.deps.workspaceId, {
|
|
99
|
+
title,
|
|
100
|
+
content,
|
|
101
|
+
groupId: artifactsGroupId,
|
|
102
|
+
});
|
|
103
|
+
this.deps.log(`Created artifact: ${file}`, "success");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
this.deps.log(`Failed to sync artifacts: ${error}`, "error");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { AnthropicClient } from "../ai/anthropic-client";
|
|
2
|
+
import type { ClaudeRunner } from "../ai/claude-runner";
|
|
3
|
+
import { CodebaseIndexer } from "../core/indexer";
|
|
4
|
+
|
|
5
|
+
export interface CodebaseIndexerServiceDeps {
|
|
6
|
+
anthropicClient: AnthropicClient | null;
|
|
7
|
+
claudeRunner: ClaudeRunner;
|
|
8
|
+
projectPath: string;
|
|
9
|
+
log: (message: string, level?: "info" | "success" | "warn" | "error") => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handles codebase indexing with AI analysis
|
|
14
|
+
*/
|
|
15
|
+
export class CodebaseIndexerService {
|
|
16
|
+
private indexer: CodebaseIndexer;
|
|
17
|
+
|
|
18
|
+
constructor(private deps: CodebaseIndexerServiceDeps) {
|
|
19
|
+
this.indexer = new CodebaseIndexer(deps.projectPath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async reindex(): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
this.deps.log("Reindexing codebase...", "info");
|
|
25
|
+
|
|
26
|
+
const index = await this.indexer.index(
|
|
27
|
+
(msg) => this.deps.log(msg, "info"),
|
|
28
|
+
async (tree: string) => {
|
|
29
|
+
const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
|
|
30
|
+
1. Key symbols (classes, functions, types) and their locations
|
|
31
|
+
2. Responsibilities of each directory/file
|
|
32
|
+
3. Overall project structure
|
|
33
|
+
|
|
34
|
+
Analyze this file tree and provide a JSON response with:
|
|
35
|
+
- "symbols": object mapping symbol names to file paths (array)
|
|
36
|
+
- "responsibilities": object mapping paths to brief descriptions
|
|
37
|
+
|
|
38
|
+
File tree:
|
|
39
|
+
${tree}
|
|
40
|
+
|
|
41
|
+
Return ONLY valid JSON, no markdown formatting.`;
|
|
42
|
+
|
|
43
|
+
let response: string;
|
|
44
|
+
|
|
45
|
+
if (this.deps.anthropicClient) {
|
|
46
|
+
// Use Anthropic SDK with caching for faster indexing
|
|
47
|
+
response = await this.deps.anthropicClient.run({
|
|
48
|
+
systemPrompt:
|
|
49
|
+
"You are a codebase analysis expert specialized in extracting structure and symbols from file trees.",
|
|
50
|
+
userPrompt: prompt,
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
// Fallback to Claude CLI
|
|
54
|
+
response = await this.deps.claudeRunner.run(prompt, true);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Extract JSON from response (handle markdown code blocks)
|
|
58
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
59
|
+
if (jsonMatch) {
|
|
60
|
+
return JSON.parse(jsonMatch[0]);
|
|
61
|
+
}
|
|
62
|
+
return { symbols: {}, responsibilities: {}, lastIndexed: "" };
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
this.indexer.saveIndex(index);
|
|
66
|
+
this.deps.log("Codebase reindexed successfully", "success");
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.deps.log(`Failed to reindex codebase: ${error}`, "error");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ArtifactSyncer } from "./artifact-syncer";
|
|
2
|
+
export { CodebaseIndexerService } from "./codebase-indexer-service";
|
|
3
|
+
export { SprintPlanner } from "./sprint-planner";
|
|
4
|
+
export { TaskExecutor } from "./task-executor";
|
|
5
|
+
export { AgentWorker, type WorkerConfig } from "./worker";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Sprint, Task } from "@locusai/shared";
|
|
2
|
+
import type { AnthropicClient } from "../ai/anthropic-client";
|
|
3
|
+
import type { ClaudeRunner } from "../ai/claude-runner";
|
|
4
|
+
|
|
5
|
+
export interface SprintPlannerDeps {
|
|
6
|
+
anthropicClient: AnthropicClient | null;
|
|
7
|
+
claudeRunner: ClaudeRunner;
|
|
8
|
+
log: (message: string, level?: "info" | "success" | "warn" | "error") => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handles sprint planning and mindmap generation
|
|
13
|
+
*/
|
|
14
|
+
export class SprintPlanner {
|
|
15
|
+
constructor(private deps: SprintPlannerDeps) {}
|
|
16
|
+
|
|
17
|
+
async planSprint(sprint: Sprint, tasks: Task[]): Promise<string> {
|
|
18
|
+
this.deps.log(`Planning sprint: ${sprint.name}`, "info");
|
|
19
|
+
|
|
20
|
+
const taskList = tasks
|
|
21
|
+
.map(
|
|
22
|
+
(t) => `- [${t.id}] ${t.title}: ${t.description || "No description"}`
|
|
23
|
+
)
|
|
24
|
+
.join("\n");
|
|
25
|
+
|
|
26
|
+
let plan: string;
|
|
27
|
+
|
|
28
|
+
if (this.deps.anthropicClient) {
|
|
29
|
+
// Use Anthropic SDK with caching for faster planning
|
|
30
|
+
const systemPrompt = `You are an expert project manager and lead engineer specialized in sprint planning and task prioritization.`;
|
|
31
|
+
|
|
32
|
+
const userPrompt = `# Sprint Planning: ${sprint.name}
|
|
33
|
+
|
|
34
|
+
## Tasks
|
|
35
|
+
${taskList}
|
|
36
|
+
|
|
37
|
+
## Instructions
|
|
38
|
+
1. Analyze dependencies between these tasks.
|
|
39
|
+
2. Prioritize them for the most efficient execution.
|
|
40
|
+
3. Create a mindmap (in Markdown or Mermaid format) that visualizes the sprint structure.
|
|
41
|
+
4. Output your final plan. The plan should clearly state the order of execution.
|
|
42
|
+
|
|
43
|
+
**IMPORTANT**:
|
|
44
|
+
- Do NOT create any files on the filesystem during this planning phase.
|
|
45
|
+
- Avoid using absolute local paths (e.g., /Users/...) in your output. Use relative paths starting from the project root if necessary.
|
|
46
|
+
- Your output will be saved as the official sprint mindmap on the server.`;
|
|
47
|
+
|
|
48
|
+
plan = await this.deps.anthropicClient.run({
|
|
49
|
+
systemPrompt,
|
|
50
|
+
userPrompt,
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
// Fallback to Claude CLI
|
|
54
|
+
const planningPrompt = `# Sprint Planning: ${sprint.name}
|
|
55
|
+
|
|
56
|
+
You are an expert project manager and lead engineer. You need to create a mindmap and execution plan for the following tasks in this sprint.
|
|
57
|
+
|
|
58
|
+
## Tasks
|
|
59
|
+
${taskList}
|
|
60
|
+
|
|
61
|
+
## Instructions
|
|
62
|
+
1. Analyze dependencies between these tasks.
|
|
63
|
+
2. Prioritize them for the most efficient execution.
|
|
64
|
+
3. Create a mindmap (in Markdown or Mermaid format) that visualizes the sprint structure.
|
|
65
|
+
4. Output your final plan. The plan should clearly state the order of execution.
|
|
66
|
+
|
|
67
|
+
**IMPORTANT**:
|
|
68
|
+
- Do NOT create any files on the filesystem during this planning phase.
|
|
69
|
+
- Avoid using absolute local paths (e.g., /Users/...) in your output. Use relative paths starting from the project root if necessary.
|
|
70
|
+
- Your output will be saved as the official sprint mindmap on the server.`;
|
|
71
|
+
|
|
72
|
+
plan = await this.deps.claudeRunner.run(planningPrompt, true);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.deps.log("Sprint mindmap generated and posted to server.", "success");
|
|
76
|
+
return plan;
|
|
77
|
+
}
|
|
78
|
+
}
|