@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.
Files changed (105) hide show
  1. package/dist/agent/artifact-syncer.d.ts +17 -0
  2. package/dist/agent/artifact-syncer.d.ts.map +1 -0
  3. package/dist/agent/artifact-syncer.js +77 -0
  4. package/dist/agent/codebase-indexer-service.d.ts +18 -0
  5. package/dist/agent/codebase-indexer-service.d.ts.map +1 -0
  6. package/dist/agent/codebase-indexer-service.js +55 -0
  7. package/dist/agent/index.d.ts +6 -0
  8. package/dist/agent/index.d.ts.map +1 -0
  9. package/dist/agent/index.js +5 -0
  10. package/dist/agent/sprint-planner.d.ts +17 -0
  11. package/dist/agent/sprint-planner.d.ts.map +1 -0
  12. package/dist/agent/sprint-planner.js +62 -0
  13. package/dist/agent/task-executor.d.ts +24 -0
  14. package/dist/agent/task-executor.d.ts.map +1 -0
  15. package/dist/agent/task-executor.js +56 -0
  16. package/dist/agent/worker.d.ts +37 -0
  17. package/dist/agent/worker.d.ts.map +1 -0
  18. package/dist/agent/worker.js +232 -0
  19. package/dist/ai/anthropic-client.d.ts +33 -0
  20. package/dist/ai/anthropic-client.d.ts.map +1 -0
  21. package/dist/ai/anthropic-client.js +70 -0
  22. package/dist/ai/claude-runner.d.ts +7 -0
  23. package/dist/ai/claude-runner.d.ts.map +1 -0
  24. package/dist/ai/claude-runner.js +43 -0
  25. package/dist/ai/index.d.ts +3 -0
  26. package/dist/ai/index.d.ts.map +1 -0
  27. package/dist/ai/index.js +2 -0
  28. package/dist/core/config.d.ts +10 -0
  29. package/dist/core/config.d.ts.map +1 -0
  30. package/dist/core/config.js +15 -0
  31. package/dist/core/index.d.ts +4 -0
  32. package/dist/core/index.d.ts.map +1 -0
  33. package/dist/core/index.js +3 -0
  34. package/dist/core/indexer.d.ts +18 -0
  35. package/dist/core/indexer.d.ts.map +1 -0
  36. package/dist/core/indexer.js +73 -0
  37. package/dist/core/prompt-builder.d.ts +8 -0
  38. package/dist/core/prompt-builder.d.ts.map +1 -0
  39. package/dist/core/prompt-builder.js +87 -0
  40. package/dist/events.d.ts +20 -0
  41. package/dist/events.d.ts.map +1 -0
  42. package/dist/events.js +15 -0
  43. package/dist/index-node.d.ts +14 -0
  44. package/dist/index-node.d.ts.map +1 -0
  45. package/dist/index-node.js +18 -0
  46. package/dist/index.d.ts +34 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +101 -0
  49. package/dist/modules/auth.d.ts +14 -0
  50. package/dist/modules/auth.d.ts.map +1 -0
  51. package/dist/modules/auth.js +23 -0
  52. package/dist/modules/base.d.ts +8 -0
  53. package/dist/modules/base.d.ts.map +1 -0
  54. package/dist/modules/base.js +8 -0
  55. package/dist/modules/ci.d.ts +8 -0
  56. package/dist/modules/ci.d.ts.map +1 -0
  57. package/dist/modules/ci.js +7 -0
  58. package/dist/modules/docs.d.ts +14 -0
  59. package/dist/modules/docs.d.ts.map +1 -0
  60. package/dist/modules/docs.js +38 -0
  61. package/dist/modules/invitations.d.ts +10 -0
  62. package/dist/modules/invitations.d.ts.map +1 -0
  63. package/dist/modules/invitations.js +22 -0
  64. package/dist/modules/organizations.d.ts +24 -0
  65. package/dist/modules/organizations.d.ts.map +1 -0
  66. package/dist/modules/organizations.js +39 -0
  67. package/dist/modules/sprints.d.ts +13 -0
  68. package/dist/modules/sprints.d.ts.map +1 -0
  69. package/dist/modules/sprints.js +34 -0
  70. package/dist/modules/tasks.d.ts +24 -0
  71. package/dist/modules/tasks.d.ts.map +1 -0
  72. package/dist/modules/tasks.js +56 -0
  73. package/dist/modules/workspaces.d.ts +21 -0
  74. package/dist/modules/workspaces.d.ts.map +1 -0
  75. package/dist/modules/workspaces.js +49 -0
  76. package/dist/orchestrator.d.ts +90 -0
  77. package/dist/orchestrator.d.ts.map +1 -0
  78. package/dist/orchestrator.js +326 -0
  79. package/package.json +53 -0
  80. package/src/agent/artifact-syncer.ts +111 -0
  81. package/src/agent/codebase-indexer-service.ts +71 -0
  82. package/src/agent/index.ts +5 -0
  83. package/src/agent/sprint-planner.ts +78 -0
  84. package/src/agent/task-executor.ts +77 -0
  85. package/src/agent/worker.ts +299 -0
  86. package/src/ai/anthropic-client.ts +93 -0
  87. package/src/ai/claude-runner.ts +49 -0
  88. package/src/ai/index.ts +2 -0
  89. package/src/core/config.ts +21 -0
  90. package/src/core/index.ts +3 -0
  91. package/src/core/indexer.ts +91 -0
  92. package/src/core/prompt-builder.ts +100 -0
  93. package/src/events.ts +32 -0
  94. package/src/index-node.ts +20 -0
  95. package/src/index.ts +119 -0
  96. package/src/modules/auth.ts +48 -0
  97. package/src/modules/base.ts +9 -0
  98. package/src/modules/ci.ts +12 -0
  99. package/src/modules/docs.ts +84 -0
  100. package/src/modules/invitations.ts +45 -0
  101. package/src/modules/organizations.ts +90 -0
  102. package/src/modules/sprints.ts +69 -0
  103. package/src/modules/tasks.ts +110 -0
  104. package/src/modules/workspaces.ts +94 -0
  105. package/src/orchestrator.ts +430 -0
@@ -0,0 +1,110 @@
1
+ import {
2
+ AddComment,
3
+ Comment,
4
+ CommentResponse,
5
+ CreateTask,
6
+ Task,
7
+ TaskResponse,
8
+ TaskStatus,
9
+ TasksResponse,
10
+ UpdateTask,
11
+ } from "@locusai/shared";
12
+ import { BaseModule } from "./base";
13
+
14
+ export interface TaskListOptions {
15
+ sprintId?: string;
16
+ status?: TaskStatus | TaskStatus[];
17
+ }
18
+
19
+ export class TasksModule extends BaseModule {
20
+ /**
21
+ * List all tasks in a workspace, optionally filtered
22
+ */
23
+ async list(workspaceId: string, options?: TaskListOptions): Promise<Task[]> {
24
+ const { data } = await this.api.get<TasksResponse>(
25
+ `/workspaces/${workspaceId}/tasks`
26
+ );
27
+
28
+ let tasks = data.tasks;
29
+
30
+ // Client-side filtering (API doesn't support query params yet)
31
+ if (options?.sprintId) {
32
+ tasks = tasks.filter((t) => t.sprintId === options.sprintId);
33
+ }
34
+
35
+ if (options?.status) {
36
+ const statuses = Array.isArray(options.status)
37
+ ? options.status
38
+ : [options.status];
39
+ tasks = tasks.filter((t) => statuses.includes(t.status as TaskStatus));
40
+ }
41
+
42
+ return tasks;
43
+ }
44
+
45
+ /**
46
+ * Get available tasks for an agent to work on.
47
+ * Returns tasks in BACKLOG or IN_PROGRESS (unassigned) status.
48
+ */
49
+ async getAvailable(workspaceId: string, sprintId?: string): Promise<Task[]> {
50
+ const tasks = await this.list(workspaceId, {
51
+ sprintId,
52
+ });
53
+
54
+ return tasks.filter(
55
+ (t) =>
56
+ t.status === TaskStatus.BACKLOG ||
57
+ (t.status === TaskStatus.IN_PROGRESS && !t.assignedTo)
58
+ );
59
+ }
60
+
61
+ async getById(id: string, workspaceId: string): Promise<Task> {
62
+ const { data } = await this.api.get<TaskResponse>(
63
+ `/workspaces/${workspaceId}/tasks/${id}`
64
+ );
65
+ return data.task;
66
+ }
67
+
68
+ async create(workspaceId: string, body: CreateTask): Promise<Task> {
69
+ const { data } = await this.api.post<TaskResponse>(
70
+ `/workspaces/${workspaceId}/tasks`,
71
+ body
72
+ );
73
+ return data.task;
74
+ }
75
+
76
+ async update(
77
+ id: string,
78
+ workspaceId: string,
79
+ body: UpdateTask
80
+ ): Promise<Task> {
81
+ const { data } = await this.api.patch<TaskResponse>(
82
+ `/workspaces/${workspaceId}/tasks/${id}`,
83
+ body
84
+ );
85
+ return data.task;
86
+ }
87
+
88
+ async delete(id: string, workspaceId: string): Promise<void> {
89
+ await this.api.delete(`/workspaces/${workspaceId}/tasks/${id}`);
90
+ }
91
+
92
+ async getBacklog(workspaceId: string): Promise<Task[]> {
93
+ const { data } = await this.api.get<TasksResponse>(
94
+ `/workspaces/${workspaceId}/tasks/backlog`
95
+ );
96
+ return data.tasks;
97
+ }
98
+
99
+ async addComment(
100
+ id: string,
101
+ workspaceId: string,
102
+ body: AddComment
103
+ ): Promise<Comment> {
104
+ const { data } = await this.api.post<CommentResponse>(
105
+ `/workspaces/${workspaceId}/tasks/${id}/comment`,
106
+ body
107
+ );
108
+ return data.comment;
109
+ }
110
+ }
@@ -0,0 +1,94 @@
1
+ import {
2
+ ActivityResponse,
3
+ CreateWorkspace,
4
+ Event,
5
+ Task,
6
+ TaskResponse,
7
+ UpdateWorkspace,
8
+ Workspace,
9
+ WorkspaceResponse,
10
+ WorkspaceStats,
11
+ WorkspacesResponse,
12
+ } from "@locusai/shared";
13
+ import { BaseModule } from "./base";
14
+
15
+ export class WorkspacesModule extends BaseModule {
16
+ async listAll(): Promise<Workspace[]> {
17
+ const { data } = await this.api.get<WorkspacesResponse>("/workspaces");
18
+ return data.workspaces;
19
+ }
20
+
21
+ async listByOrg(orgId: string): Promise<Workspace[]> {
22
+ const { data } = await this.api.get<WorkspacesResponse>(
23
+ `/workspaces/org/${orgId}`
24
+ );
25
+ return data.workspaces;
26
+ }
27
+
28
+ async create(body: CreateWorkspace & { orgId: string }): Promise<Workspace> {
29
+ const { orgId, ...bodyWithoutOrgId } = body;
30
+ const { data } = await this.api.post<WorkspaceResponse>(
31
+ `/workspaces/org/${orgId}`,
32
+ bodyWithoutOrgId
33
+ );
34
+ return data.workspace;
35
+ }
36
+
37
+ async createWithAutoOrg(body: CreateWorkspace): Promise<Workspace> {
38
+ const { data } = await this.api.post<WorkspaceResponse>(
39
+ "/workspaces",
40
+ body
41
+ );
42
+ return data.workspace;
43
+ }
44
+
45
+ async getById(id: string): Promise<Workspace> {
46
+ const { data } = await this.api.get<WorkspaceResponse>(`/workspaces/${id}`);
47
+ return data.workspace;
48
+ }
49
+
50
+ async update(id: string, body: UpdateWorkspace): Promise<Workspace> {
51
+ const { data } = await this.api.put<WorkspaceResponse>(
52
+ `/workspaces/${id}`,
53
+ body
54
+ );
55
+ return data.workspace;
56
+ }
57
+
58
+ async delete(id: string): Promise<void> {
59
+ await this.api.delete(`/workspaces/${id}`);
60
+ }
61
+
62
+ async getStats(id: string): Promise<WorkspaceStats> {
63
+ const { data } = await this.api.get<WorkspaceStats>(
64
+ `/workspaces/${id}/stats`
65
+ );
66
+ return data;
67
+ }
68
+
69
+ async getActivity(id: string, limit?: number): Promise<Event[]> {
70
+ const { data } = await this.api.get<ActivityResponse>(
71
+ `/workspaces/${id}/activity`,
72
+ {
73
+ params: { limit },
74
+ }
75
+ );
76
+ return data.activity;
77
+ }
78
+
79
+ /**
80
+ * Dispatch a task from the workspace backlog to an agent.
81
+ * Atomically moves a task from BACKLOG to IN_PROGRESS and assigns it.
82
+ */
83
+ async dispatch(
84
+ id: string,
85
+ workerId: string,
86
+ sprintId?: string
87
+ ): Promise<Task> {
88
+ const { data } = await this.api.post<TaskResponse>(
89
+ `/workspaces/${id}/dispatch`,
90
+ { workerId, sprintId }
91
+ );
92
+ return data.task;
93
+ }
94
+ }
@@ -0,0 +1,430 @@
1
+ import { ChildProcess, spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { Task, TaskPriority, TaskStatus } from "@locusai/shared";
5
+ import { EventEmitter } from "events";
6
+ import { LocusClient } from "./index";
7
+
8
+ export interface AgentConfig {
9
+ id: string;
10
+ maxConcurrentTasks: number;
11
+ }
12
+
13
+ export interface AgentState {
14
+ id: string;
15
+ status: "IDLE" | "WORKING" | "COMPLETED" | "FAILED";
16
+ currentTaskId: string | null;
17
+ tasksCompleted: number;
18
+ tasksFailed: number;
19
+ lastHeartbeat: Date;
20
+ process?: ChildProcess;
21
+ }
22
+
23
+ export interface OrchestratorConfig {
24
+ workspaceId: string;
25
+ sprintId: string;
26
+ apiBase: string;
27
+ maxIterations: number;
28
+ projectPath: string;
29
+ apiKey: string;
30
+ anthropicApiKey?: string;
31
+ model?: string;
32
+ }
33
+
34
+ export class AgentOrchestrator extends EventEmitter {
35
+ private client: LocusClient;
36
+ private config: OrchestratorConfig;
37
+ private agents: Map<string, AgentState> = new Map();
38
+ private isRunning = false;
39
+ private processedTasks: Set<string> = new Set();
40
+ private resolvedSprintId: string | null = null;
41
+
42
+ constructor(config: OrchestratorConfig) {
43
+ super();
44
+ this.config = config;
45
+ this.client = new LocusClient({
46
+ baseUrl: config.apiBase,
47
+ token: config.apiKey,
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Resolve the sprint ID - use provided or find active sprint
53
+ */
54
+ private async resolveSprintId(): Promise<string> {
55
+ if (this.config.sprintId) {
56
+ return this.config.sprintId;
57
+ }
58
+
59
+ // Try to find active sprint in workspace
60
+ try {
61
+ const sprint = await this.client.sprints.getActive(
62
+ this.config.workspaceId
63
+ );
64
+ if (sprint?.id) {
65
+ console.log(`📋 Using active sprint: ${sprint.name}`);
66
+ return sprint.id;
67
+ }
68
+ } catch {
69
+ // No active sprint found, will work with all tasks
70
+ }
71
+
72
+ console.log("ℹ No sprint specified, working with all workspace tasks");
73
+ return "";
74
+ }
75
+
76
+ /**
77
+ * Start the orchestrator with N agents
78
+ */
79
+ async start(): Promise<void> {
80
+ if (this.isRunning) {
81
+ throw new Error("Orchestrator is already running");
82
+ }
83
+
84
+ this.isRunning = true;
85
+ this.processedTasks.clear();
86
+
87
+ try {
88
+ await this.orchestrationLoop();
89
+ } catch (error) {
90
+ this.emit("error", error);
91
+ throw error;
92
+ } finally {
93
+ await this.cleanup();
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Main orchestration loop - runs 1 agent continuously
99
+ */
100
+ private async orchestrationLoop(): Promise<void> {
101
+ // Resolve sprint ID first
102
+ this.resolvedSprintId = await this.resolveSprintId();
103
+
104
+ this.emit("started", {
105
+ timestamp: new Date(),
106
+ config: this.config,
107
+ sprintId: this.resolvedSprintId,
108
+ });
109
+
110
+ console.log("\n🤖 Locus Agent Orchestrator");
111
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
112
+ console.log(`Workspace: ${this.config.workspaceId}`);
113
+ if (this.resolvedSprintId) {
114
+ console.log(`Sprint: ${this.resolvedSprintId}`);
115
+ }
116
+ console.log(`API Base: ${this.config.apiBase}`);
117
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
118
+
119
+ // Check if there are tasks to work on before spawning
120
+ const tasks = await this.getAvailableTasks();
121
+
122
+ if (tasks.length === 0) {
123
+ console.log("ℹ No available tasks found in the backlog.");
124
+ return;
125
+ }
126
+
127
+ // Spawn single agent
128
+ await this.spawnAgent();
129
+
130
+ // Wait for agent to complete
131
+ while (this.agents.size > 0 && this.isRunning) {
132
+ await this.reapAgents();
133
+
134
+ if (this.agents.size === 0) {
135
+ break;
136
+ }
137
+
138
+ await this.sleep(2000);
139
+ }
140
+
141
+ console.log("\n✅ Orchestrator finished");
142
+ }
143
+
144
+ /**
145
+ * Spawn a single agent process
146
+ */
147
+ private async spawnAgent(): Promise<void> {
148
+ const agentId = `agent-${Date.now()}-${Math.random()
149
+ .toString(36)
150
+ .slice(2, 9)}`;
151
+
152
+ const agentState: AgentState = {
153
+ id: agentId,
154
+ status: "IDLE",
155
+ currentTaskId: null,
156
+ tasksCompleted: 0,
157
+ tasksFailed: 0,
158
+ lastHeartbeat: new Date(),
159
+ };
160
+
161
+ this.agents.set(agentId, agentState);
162
+
163
+ console.log(`🚀 Agent started: ${agentId}\n`);
164
+
165
+ // Build arguments for agent worker
166
+ // Resolve path relative to this file's location (works in both dev and production)
167
+ const workerPath = join(__dirname, "agent", "worker.js");
168
+
169
+ // Verify worker file exists
170
+ if (!existsSync(workerPath)) {
171
+ throw new Error(
172
+ `Worker file not found at ${workerPath}. ` +
173
+ `Make sure the SDK is properly built. __dirname: ${__dirname}`
174
+ );
175
+ }
176
+
177
+ const workerArgs = [
178
+ "--agent-id",
179
+ agentId,
180
+ "--workspace-id",
181
+ this.config.workspaceId,
182
+ "--api-base",
183
+ this.config.apiBase,
184
+ "--api-key",
185
+ this.config.apiKey,
186
+ "--project-path",
187
+ this.config.projectPath,
188
+ ];
189
+
190
+ // Add anthropic API key if provided
191
+ if (this.config.anthropicApiKey) {
192
+ workerArgs.push("--anthropic-api-key", this.config.anthropicApiKey);
193
+ }
194
+
195
+ // Add model if specified
196
+ if (this.config.model) {
197
+ workerArgs.push("--model", this.config.model);
198
+ }
199
+
200
+ // Add sprint ID if resolved
201
+ if (this.resolvedSprintId) {
202
+ workerArgs.push("--sprint-id", this.resolvedSprintId);
203
+ }
204
+
205
+ // Use bun to run TypeScript files directly
206
+ const workerTsPath = workerPath.replace(/\.js$/, ".ts");
207
+ const agentProcess = spawn("bun", ["run", workerTsPath, ...workerArgs]);
208
+
209
+ agentState.process = agentProcess;
210
+
211
+ agentProcess.on("message", (msg: Record<string, unknown>) => {
212
+ if (msg.type === "stats") {
213
+ agentState.tasksCompleted = (msg.tasksCompleted as number) || 0;
214
+ agentState.tasksFailed = (msg.tasksFailed as number) || 0;
215
+ }
216
+ });
217
+
218
+ agentProcess.stdout?.on("data", (data) => {
219
+ process.stdout.write(data.toString());
220
+ });
221
+
222
+ agentProcess.stderr?.on("data", (data) => {
223
+ process.stderr.write(`[${agentId}] ERR: ${data.toString()}`);
224
+ });
225
+
226
+ agentProcess.on("exit", (code) => {
227
+ console.log(`\n${agentId} finished (exit code: ${code})`);
228
+ const agent = this.agents.get(agentId);
229
+ if (agent) {
230
+ agent.status = code === 0 ? "COMPLETED" : "FAILED";
231
+
232
+ // Ensure CLI gets the absolute latest stats
233
+ this.emit("agent:completed", {
234
+ agentId,
235
+ status: agent.status,
236
+ tasksCompleted: agent.tasksCompleted,
237
+ tasksFailed: agent.tasksFailed,
238
+ });
239
+
240
+ // Remove from active tracking after emitting
241
+ this.agents.delete(agentId);
242
+ }
243
+ });
244
+
245
+ this.emit("agent:spawned", { agentId });
246
+ }
247
+
248
+ /**
249
+ * Reap completed agents
250
+ */
251
+ private async reapAgents(): Promise<void> {
252
+ // No-op: agents now remove themselves in the 'exit' listener
253
+ // to ensure events are emitted with correct stats before deletion.
254
+ }
255
+
256
+ /**
257
+ * Get available tasks in sprint
258
+ */
259
+ private async getAvailableTasks(): Promise<Task[]> {
260
+ try {
261
+ const tasks = await this.client.tasks.getAvailable(
262
+ this.config.workspaceId,
263
+ this.resolvedSprintId || undefined
264
+ );
265
+
266
+ return tasks.filter((task) => !this.processedTasks.has(task.id));
267
+ } catch (error) {
268
+ this.emit("error", error);
269
+ return [];
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Assign task to agent
275
+ */
276
+ async assignTaskToAgent(agentId: string): Promise<Task | null> {
277
+ const agent = this.agents.get(agentId);
278
+ if (!agent) return null;
279
+
280
+ try {
281
+ const tasks = await this.getAvailableTasks();
282
+
283
+ const priorityOrder = [
284
+ TaskPriority.CRITICAL,
285
+ TaskPriority.HIGH,
286
+ TaskPriority.MEDIUM,
287
+ TaskPriority.LOW,
288
+ ];
289
+
290
+ // Find task with highest priority
291
+ let task = tasks.sort(
292
+ (a, b) =>
293
+ priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority)
294
+ )[0];
295
+
296
+ // Fallback: any available task
297
+ if (!task && tasks.length > 0) {
298
+ task = tasks[0];
299
+ }
300
+
301
+ if (!task) return null;
302
+
303
+ agent.currentTaskId = task.id;
304
+ agent.status = "WORKING";
305
+
306
+ this.emit("task:assigned", {
307
+ agentId,
308
+ taskId: task.id,
309
+ title: task.title,
310
+ });
311
+
312
+ return task;
313
+ } catch (error) {
314
+ this.emit("error", error);
315
+ return null;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Mark task as completed by agent
321
+ */
322
+ async completeTask(
323
+ taskId: string,
324
+ agentId: string,
325
+ summary?: string
326
+ ): Promise<void> {
327
+ try {
328
+ await this.client.tasks.update(taskId, this.config.workspaceId, {
329
+ status: TaskStatus.VERIFICATION,
330
+ });
331
+
332
+ if (summary) {
333
+ await this.client.tasks.addComment(taskId, this.config.workspaceId, {
334
+ author: agentId,
335
+ text: `✅ Task completed\n\n${summary}`,
336
+ });
337
+ }
338
+
339
+ this.processedTasks.add(taskId);
340
+
341
+ const agent = this.agents.get(agentId);
342
+ if (agent) {
343
+ agent.tasksCompleted += 1;
344
+ agent.currentTaskId = null;
345
+ agent.status = "IDLE";
346
+ }
347
+
348
+ this.emit("task:completed", { agentId, taskId });
349
+ } catch (error) {
350
+ this.emit("error", error);
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Mark task as failed
356
+ */
357
+ async failTask(
358
+ taskId: string,
359
+ agentId: string,
360
+ error: string
361
+ ): Promise<void> {
362
+ try {
363
+ await this.client.tasks.update(taskId, this.config.workspaceId, {
364
+ status: TaskStatus.BACKLOG,
365
+ assignedTo: null,
366
+ });
367
+
368
+ await this.client.tasks.addComment(taskId, this.config.workspaceId, {
369
+ author: agentId,
370
+ text: `❌ Agent failed: ${error}`,
371
+ });
372
+
373
+ const agent = this.agents.get(agentId);
374
+ if (agent) {
375
+ agent.tasksFailed += 1;
376
+ agent.currentTaskId = null;
377
+ agent.status = "IDLE";
378
+ }
379
+
380
+ this.emit("task:failed", { agentId, taskId, error });
381
+ } catch (error) {
382
+ this.emit("error", error);
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Stop orchestrator
388
+ */
389
+ async stop(): Promise<void> {
390
+ this.isRunning = false;
391
+ await this.cleanup();
392
+ this.emit("stopped", { timestamp: new Date() });
393
+ }
394
+
395
+ /**
396
+ * Cleanup - kill all agent processes
397
+ */
398
+ private async cleanup(): Promise<void> {
399
+ for (const [agentId, agent] of this.agents.entries()) {
400
+ if (agent.process && !agent.process.killed) {
401
+ console.log(`Killing agent: ${agentId}`);
402
+ agent.process.kill();
403
+ }
404
+ }
405
+
406
+ this.agents.clear();
407
+ }
408
+
409
+ /**
410
+ * Get orchestrator stats
411
+ */
412
+ getStats() {
413
+ return {
414
+ activeAgents: this.agents.size,
415
+ processedTasks: this.processedTasks.size,
416
+ totalTasksCompleted: Array.from(this.agents.values()).reduce(
417
+ (sum, agent) => sum + agent.tasksCompleted,
418
+ 0
419
+ ),
420
+ totalTasksFailed: Array.from(this.agents.values()).reduce(
421
+ (sum, agent) => sum + agent.tasksFailed,
422
+ 0
423
+ ),
424
+ };
425
+ }
426
+
427
+ private sleep(ms: number): Promise<void> {
428
+ return new Promise((resolve) => setTimeout(resolve, ms));
429
+ }
430
+ }