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