@maestroai/core 0.1.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/README.md +46 -0
- package/dist/index.d.ts +1007 -0
- package/dist/index.js +3929 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3929 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/task/parser.ts
|
|
12
|
+
function normalizePriority(raw) {
|
|
13
|
+
return raw.toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
function normalizeStatus(raw) {
|
|
16
|
+
const trimmed = raw.trim();
|
|
17
|
+
const doneMatch = trimmed.match(/^done:(\S+)$/);
|
|
18
|
+
if (doneMatch) {
|
|
19
|
+
return { status: "done", completedAt: doneMatch[1] };
|
|
20
|
+
}
|
|
21
|
+
const statusCandidate = trimmed.toLowerCase();
|
|
22
|
+
if (VALID_STATUSES.includes(statusCandidate)) {
|
|
23
|
+
return {
|
|
24
|
+
status: statusCandidate,
|
|
25
|
+
completedAt: statusCandidate === "done" ? (/* @__PURE__ */ new Date()).toISOString() : null
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { status: "pending", completedAt: null };
|
|
29
|
+
}
|
|
30
|
+
var TASK_LINE_REGEX, PRIORITY_HEADER_REGEX, PROJECT_HEADER_REGEX, NOTE_REGEX, VALID_STATUSES, TaskParser;
|
|
31
|
+
var init_parser = __esm({
|
|
32
|
+
"src/task/parser.ts"() {
|
|
33
|
+
"use strict";
|
|
34
|
+
TASK_LINE_REGEX = /^- \[(x| )\]\s+(\S+)\s+\|\s+(.+?)\s+\|\s+(@\S+|unassigned)\s+\|\s+deps:\s*(.+?)\s+\|\s+(.+)$/;
|
|
35
|
+
PRIORITY_HEADER_REGEX = /^##\s+(Critical|High|Medium|Low)\s*$/i;
|
|
36
|
+
PROJECT_HEADER_REGEX = /^#\s+Project:\s+(.+)$/;
|
|
37
|
+
NOTE_REGEX = /^\s+>\s?(.*)$/;
|
|
38
|
+
VALID_STATUSES = ["pending", "in-progress", "done", "blocked", "cancelled"];
|
|
39
|
+
TaskParser = class {
|
|
40
|
+
parse(content) {
|
|
41
|
+
const lines = content.split("\n");
|
|
42
|
+
let projectName = "";
|
|
43
|
+
let currentPriority = "medium";
|
|
44
|
+
const tasks = [];
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
const line = lines[i];
|
|
47
|
+
const projectMatch = line.match(PROJECT_HEADER_REGEX);
|
|
48
|
+
if (projectMatch) {
|
|
49
|
+
projectName = projectMatch[1].trim();
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const priorityMatch = line.match(PRIORITY_HEADER_REGEX);
|
|
53
|
+
if (priorityMatch) {
|
|
54
|
+
currentPriority = normalizePriority(priorityMatch[1]);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const task = this.parseLine(line, currentPriority);
|
|
58
|
+
if (task) {
|
|
59
|
+
while (i + 1 < lines.length) {
|
|
60
|
+
const noteMatch = lines[i + 1].match(NOTE_REGEX);
|
|
61
|
+
if (noteMatch) {
|
|
62
|
+
task.notes.push(noteMatch[1]);
|
|
63
|
+
i++;
|
|
64
|
+
} else {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
tasks.push(task);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { projectName, tasks };
|
|
72
|
+
}
|
|
73
|
+
parseLine(line, currentPriority) {
|
|
74
|
+
const match = line.match(TASK_LINE_REGEX);
|
|
75
|
+
if (!match) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const [, checkbox, id, title, assigneeRaw, depsRaw, statusRaw] = match;
|
|
79
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
80
|
+
const assignee = assigneeRaw === "unassigned" ? null : assigneeRaw.slice(1);
|
|
81
|
+
const deps = depsRaw.trim().toLowerCase() === "none" ? [] : depsRaw.split(",").map((d) => d.trim()).filter(Boolean);
|
|
82
|
+
let status;
|
|
83
|
+
let completedAt = null;
|
|
84
|
+
if (checkbox === "x") {
|
|
85
|
+
status = "done";
|
|
86
|
+
const doneTimestamp = statusRaw.trim().match(/^done:(\S+)$/);
|
|
87
|
+
completedAt = doneTimestamp ? doneTimestamp[1] : now;
|
|
88
|
+
} else {
|
|
89
|
+
const parsed = normalizeStatus(statusRaw);
|
|
90
|
+
status = parsed.status;
|
|
91
|
+
completedAt = parsed.completedAt;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
id,
|
|
95
|
+
title: title.trim(),
|
|
96
|
+
description: title.trim(),
|
|
97
|
+
status,
|
|
98
|
+
priority: currentPriority,
|
|
99
|
+
assignee,
|
|
100
|
+
dependencies: deps,
|
|
101
|
+
tags: [],
|
|
102
|
+
createdAt: now,
|
|
103
|
+
updatedAt: now,
|
|
104
|
+
completedAt,
|
|
105
|
+
notes: []
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// src/task/writer.ts
|
|
113
|
+
var PRIORITY_ORDER, PRIORITY_LABELS, TaskWriter;
|
|
114
|
+
var init_writer = __esm({
|
|
115
|
+
"src/task/writer.ts"() {
|
|
116
|
+
"use strict";
|
|
117
|
+
PRIORITY_ORDER = ["critical", "high", "medium", "low"];
|
|
118
|
+
PRIORITY_LABELS = {
|
|
119
|
+
critical: "Critical",
|
|
120
|
+
high: "High",
|
|
121
|
+
medium: "Medium",
|
|
122
|
+
low: "Low"
|
|
123
|
+
};
|
|
124
|
+
TaskWriter = class {
|
|
125
|
+
write(projectName, tasks) {
|
|
126
|
+
const lines = [];
|
|
127
|
+
lines.push(`# Project: ${projectName}`);
|
|
128
|
+
lines.push(`# Generated by Maestro | Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
129
|
+
lines.push("");
|
|
130
|
+
for (const priority of PRIORITY_ORDER) {
|
|
131
|
+
const group = tasks.filter((t) => t.priority === priority);
|
|
132
|
+
if (group.length === 0) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
lines.push(`## ${PRIORITY_LABELS[priority]}`);
|
|
136
|
+
for (const task of group) {
|
|
137
|
+
lines.push(this.formatTask(task));
|
|
138
|
+
}
|
|
139
|
+
lines.push("");
|
|
140
|
+
}
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
formatTask(task) {
|
|
144
|
+
const checkbox = task.status === "done" ? "[x]" : "[ ]";
|
|
145
|
+
const assignee = task.assignee ? `@${task.assignee}` : "unassigned";
|
|
146
|
+
const deps = task.dependencies.length > 0 ? task.dependencies.join(",") : "none";
|
|
147
|
+
let statusField;
|
|
148
|
+
if (task.status === "done" && task.completedAt) {
|
|
149
|
+
statusField = `done:${task.completedAt}`;
|
|
150
|
+
} else {
|
|
151
|
+
statusField = task.status;
|
|
152
|
+
}
|
|
153
|
+
const line = `- ${checkbox} ${task.id} | ${task.title} | ${assignee} | deps: ${deps} | ${statusField}`;
|
|
154
|
+
const noteLines = task.notes.map((note) => ` > ${note}`);
|
|
155
|
+
return [line, ...noteLines].join("\n");
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// src/utils/id.ts
|
|
162
|
+
import { nanoid } from "nanoid";
|
|
163
|
+
function generateId() {
|
|
164
|
+
return nanoid(12);
|
|
165
|
+
}
|
|
166
|
+
function generateTaskId() {
|
|
167
|
+
taskCounter++;
|
|
168
|
+
return `T-${String(taskCounter).padStart(3, "0")}`;
|
|
169
|
+
}
|
|
170
|
+
function resetTaskCounter(startFrom = 0) {
|
|
171
|
+
taskCounter = startFrom;
|
|
172
|
+
}
|
|
173
|
+
function setTaskCounter(value) {
|
|
174
|
+
taskCounter = value;
|
|
175
|
+
}
|
|
176
|
+
var taskCounter;
|
|
177
|
+
var init_id = __esm({
|
|
178
|
+
"src/utils/id.ts"() {
|
|
179
|
+
"use strict";
|
|
180
|
+
taskCounter = 0;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// src/task/lock.ts
|
|
185
|
+
import lockfile from "proper-lockfile";
|
|
186
|
+
import { access, writeFile } from "fs/promises";
|
|
187
|
+
async function ensureFileExists(filePath) {
|
|
188
|
+
try {
|
|
189
|
+
await access(filePath);
|
|
190
|
+
} catch {
|
|
191
|
+
await writeFile(filePath, "", "utf-8");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function acquireLock(filePath) {
|
|
195
|
+
await ensureFileExists(filePath);
|
|
196
|
+
const release = await lockfile.lock(filePath, {
|
|
197
|
+
stale: 1e4,
|
|
198
|
+
retries: {
|
|
199
|
+
retries: 5,
|
|
200
|
+
minTimeout: 100,
|
|
201
|
+
maxTimeout: 1e3
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
return release;
|
|
205
|
+
}
|
|
206
|
+
async function withLock(filePath, fn) {
|
|
207
|
+
const release = await acquireLock(filePath);
|
|
208
|
+
try {
|
|
209
|
+
return await fn();
|
|
210
|
+
} finally {
|
|
211
|
+
await release();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
var init_lock = __esm({
|
|
215
|
+
"src/task/lock.ts"() {
|
|
216
|
+
"use strict";
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// src/task/manager.ts
|
|
221
|
+
var manager_exports = {};
|
|
222
|
+
__export(manager_exports, {
|
|
223
|
+
TaskManager: () => TaskManager
|
|
224
|
+
});
|
|
225
|
+
import { readFile, writeFile as writeFile2, access as access2 } from "fs/promises";
|
|
226
|
+
import EventEmitter from "eventemitter3";
|
|
227
|
+
var TaskManager;
|
|
228
|
+
var init_manager = __esm({
|
|
229
|
+
"src/task/manager.ts"() {
|
|
230
|
+
"use strict";
|
|
231
|
+
init_id();
|
|
232
|
+
init_parser();
|
|
233
|
+
init_writer();
|
|
234
|
+
init_lock();
|
|
235
|
+
TaskManager = class extends EventEmitter {
|
|
236
|
+
filePath;
|
|
237
|
+
projectName;
|
|
238
|
+
parser;
|
|
239
|
+
writer;
|
|
240
|
+
constructor(options) {
|
|
241
|
+
super();
|
|
242
|
+
this.filePath = options.filePath;
|
|
243
|
+
this.projectName = options.projectName;
|
|
244
|
+
this.parser = new TaskParser();
|
|
245
|
+
this.writer = new TaskWriter();
|
|
246
|
+
}
|
|
247
|
+
async ensureFile() {
|
|
248
|
+
try {
|
|
249
|
+
await access2(this.filePath);
|
|
250
|
+
} catch {
|
|
251
|
+
await writeFile2(this.filePath, "", "utf-8");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async load() {
|
|
255
|
+
await this.ensureFile();
|
|
256
|
+
const content = await readFile(this.filePath, "utf-8");
|
|
257
|
+
const { tasks } = this.parser.parse(content);
|
|
258
|
+
return tasks;
|
|
259
|
+
}
|
|
260
|
+
async save(tasks) {
|
|
261
|
+
await withLock(this.filePath, async () => {
|
|
262
|
+
const content = this.writer.write(this.projectName, tasks);
|
|
263
|
+
await writeFile2(this.filePath, content, "utf-8");
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async getTask(id) {
|
|
267
|
+
const tasks = await this.load();
|
|
268
|
+
return tasks.find((t) => t.id === id);
|
|
269
|
+
}
|
|
270
|
+
async getTasks(filter) {
|
|
271
|
+
const tasks = await this.load();
|
|
272
|
+
if (!filter) {
|
|
273
|
+
return tasks;
|
|
274
|
+
}
|
|
275
|
+
return tasks.filter((task) => {
|
|
276
|
+
if (filter.status && filter.status.length > 0) {
|
|
277
|
+
if (!filter.status.includes(task.status)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (filter.assignee !== void 0) {
|
|
282
|
+
if (task.assignee !== filter.assignee) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (filter.priority && filter.priority.length > 0) {
|
|
287
|
+
if (!filter.priority.includes(task.priority)) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
292
|
+
if (!filter.tags.some((tag) => task.tags.includes(tag))) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async createTask(data) {
|
|
300
|
+
const tasks = await this.load();
|
|
301
|
+
const maxId = this.extractMaxTaskNumber(tasks);
|
|
302
|
+
setTaskCounter(maxId);
|
|
303
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
304
|
+
const task = {
|
|
305
|
+
id: generateTaskId(),
|
|
306
|
+
title: data.title,
|
|
307
|
+
description: data.description ?? data.title,
|
|
308
|
+
status: "pending",
|
|
309
|
+
priority: data.priority,
|
|
310
|
+
assignee: null,
|
|
311
|
+
dependencies: data.dependencies ?? [],
|
|
312
|
+
tags: data.tags ?? [],
|
|
313
|
+
createdAt: now,
|
|
314
|
+
updatedAt: now,
|
|
315
|
+
completedAt: null,
|
|
316
|
+
notes: []
|
|
317
|
+
};
|
|
318
|
+
tasks.push(task);
|
|
319
|
+
await this.save(tasks);
|
|
320
|
+
this.emit("task:created", task);
|
|
321
|
+
return task;
|
|
322
|
+
}
|
|
323
|
+
async updateTask(id, update) {
|
|
324
|
+
const tasks = await this.load();
|
|
325
|
+
const index = tasks.findIndex((t) => t.id === id);
|
|
326
|
+
if (index === -1) {
|
|
327
|
+
throw new Error(`Task not found: ${id}`);
|
|
328
|
+
}
|
|
329
|
+
const task = tasks[index];
|
|
330
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
331
|
+
if (update.status !== void 0) {
|
|
332
|
+
task.status = update.status;
|
|
333
|
+
if (update.status === "done") {
|
|
334
|
+
task.completedAt = now;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (update.assignee !== void 0) {
|
|
338
|
+
task.assignee = update.assignee;
|
|
339
|
+
}
|
|
340
|
+
if (update.priority !== void 0) {
|
|
341
|
+
task.priority = update.priority;
|
|
342
|
+
}
|
|
343
|
+
if (update.notes !== void 0) {
|
|
344
|
+
task.notes = update.notes;
|
|
345
|
+
}
|
|
346
|
+
task.updatedAt = now;
|
|
347
|
+
tasks[index] = task;
|
|
348
|
+
await this.save(tasks);
|
|
349
|
+
this.emit("task:updated", task);
|
|
350
|
+
return task;
|
|
351
|
+
}
|
|
352
|
+
async assignTask(id, agentId) {
|
|
353
|
+
const tasks = await this.load();
|
|
354
|
+
const index = tasks.findIndex((t) => t.id === id);
|
|
355
|
+
if (index === -1) {
|
|
356
|
+
throw new Error(`Task not found: ${id}`);
|
|
357
|
+
}
|
|
358
|
+
const task = tasks[index];
|
|
359
|
+
task.assignee = agentId;
|
|
360
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
361
|
+
tasks[index] = task;
|
|
362
|
+
await this.save(tasks);
|
|
363
|
+
this.emit("task:assigned", task);
|
|
364
|
+
return task;
|
|
365
|
+
}
|
|
366
|
+
async completeTask(id) {
|
|
367
|
+
const tasks = await this.load();
|
|
368
|
+
const index = tasks.findIndex((t) => t.id === id);
|
|
369
|
+
if (index === -1) {
|
|
370
|
+
throw new Error(`Task not found: ${id}`);
|
|
371
|
+
}
|
|
372
|
+
const task = tasks[index];
|
|
373
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
374
|
+
task.status = "done";
|
|
375
|
+
task.completedAt = now;
|
|
376
|
+
task.updatedAt = now;
|
|
377
|
+
tasks[index] = task;
|
|
378
|
+
await this.save(tasks);
|
|
379
|
+
this.emit("task:completed", task);
|
|
380
|
+
return task;
|
|
381
|
+
}
|
|
382
|
+
extractMaxTaskNumber(tasks) {
|
|
383
|
+
let max = 0;
|
|
384
|
+
for (const task of tasks) {
|
|
385
|
+
const match = task.id.match(/^T-(\d+)$/);
|
|
386
|
+
if (match) {
|
|
387
|
+
const num = parseInt(match[1], 10);
|
|
388
|
+
if (num > max) {
|
|
389
|
+
max = num;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return max;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// src/utils/logger.ts
|
|
400
|
+
import pino from "pino";
|
|
401
|
+
import { openSync, closeSync, writeSync, mkdirSync } from "fs";
|
|
402
|
+
import { dirname } from "path";
|
|
403
|
+
function createLogger(name, logFile) {
|
|
404
|
+
const stream = logFile ? new SyncFileStream(logFile) : null;
|
|
405
|
+
return pino(
|
|
406
|
+
{
|
|
407
|
+
name,
|
|
408
|
+
level: "trace"
|
|
409
|
+
},
|
|
410
|
+
stream ?? { write: () => {
|
|
411
|
+
} }
|
|
412
|
+
// No-op stream if no log file
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
var SyncFileStream;
|
|
416
|
+
var init_logger = __esm({
|
|
417
|
+
"src/utils/logger.ts"() {
|
|
418
|
+
"use strict";
|
|
419
|
+
SyncFileStream = class {
|
|
420
|
+
constructor(destination) {
|
|
421
|
+
this.destination = destination;
|
|
422
|
+
try {
|
|
423
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
424
|
+
this.fd = openSync(destination, "a");
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
fd = null;
|
|
429
|
+
write(chunk) {
|
|
430
|
+
if (this.fd !== null) {
|
|
431
|
+
try {
|
|
432
|
+
writeSync(this.fd, chunk);
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
end() {
|
|
438
|
+
if (this.fd !== null) {
|
|
439
|
+
closeSync(this.fd);
|
|
440
|
+
this.fd = null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// src/agent/lifecycle.ts
|
|
448
|
+
import { execa } from "execa";
|
|
449
|
+
import treeKill from "tree-kill";
|
|
450
|
+
import { EventEmitter as EventEmitter2 } from "eventemitter3";
|
|
451
|
+
function killProcessTree(pid, signal) {
|
|
452
|
+
return new Promise((resolve, reject) => {
|
|
453
|
+
treeKill(pid, signal, (err) => {
|
|
454
|
+
if (err) {
|
|
455
|
+
reject(err);
|
|
456
|
+
} else {
|
|
457
|
+
resolve();
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
var logger, AgentProcess;
|
|
463
|
+
var init_lifecycle = __esm({
|
|
464
|
+
"src/agent/lifecycle.ts"() {
|
|
465
|
+
"use strict";
|
|
466
|
+
init_logger();
|
|
467
|
+
logger = createLogger("agent-process");
|
|
468
|
+
AgentProcess = class extends EventEmitter2 {
|
|
469
|
+
constructor(config) {
|
|
470
|
+
super();
|
|
471
|
+
this.config = config;
|
|
472
|
+
this.state = {
|
|
473
|
+
config,
|
|
474
|
+
status: "idle",
|
|
475
|
+
pid: null,
|
|
476
|
+
sessionId: null,
|
|
477
|
+
currentTask: null,
|
|
478
|
+
startedAt: null,
|
|
479
|
+
lastActivityAt: null,
|
|
480
|
+
totalCostUsd: 0,
|
|
481
|
+
turnCount: 0,
|
|
482
|
+
totalInputTokens: 0,
|
|
483
|
+
totalOutputTokens: 0,
|
|
484
|
+
totalCacheReadTokens: 0,
|
|
485
|
+
totalCacheCreationTokens: 0,
|
|
486
|
+
error: null,
|
|
487
|
+
resultError: null,
|
|
488
|
+
rateLimited: false,
|
|
489
|
+
rateLimitResetAt: null
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
subprocess = null;
|
|
493
|
+
state;
|
|
494
|
+
streamBuffer = "";
|
|
495
|
+
get id() {
|
|
496
|
+
return this.config.id;
|
|
497
|
+
}
|
|
498
|
+
get currentState() {
|
|
499
|
+
return { ...this.state };
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Set the current task this agent is working on (task ID + title).
|
|
503
|
+
*/
|
|
504
|
+
setCurrentTask(taskInfo) {
|
|
505
|
+
this.state.currentTask = taskInfo;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Spawn a claude CLI process with the given prompt.
|
|
509
|
+
*/
|
|
510
|
+
async spawn(prompt) {
|
|
511
|
+
if (this.subprocess) {
|
|
512
|
+
throw new Error(`Agent ${this.id} already has a running process`);
|
|
513
|
+
}
|
|
514
|
+
const args = this.buildArgs();
|
|
515
|
+
this.setStatus("starting");
|
|
516
|
+
const cmd = this.config.claudeCommand ?? "claude";
|
|
517
|
+
logger.info(
|
|
518
|
+
{ agentId: this.id, cmd, argCount: args.length, promptLength: prompt.length },
|
|
519
|
+
"Spawning agent process"
|
|
520
|
+
);
|
|
521
|
+
this.subprocess = execa(cmd, args, {
|
|
522
|
+
input: prompt,
|
|
523
|
+
cwd: this.config.workingDirectory,
|
|
524
|
+
env: this.config.claudeEnv ? { ...process.env, ...this.config.claudeEnv } : void 0,
|
|
525
|
+
stdout: "pipe",
|
|
526
|
+
stderr: "pipe",
|
|
527
|
+
reject: false
|
|
528
|
+
});
|
|
529
|
+
this.state.pid = this.subprocess.pid ?? null;
|
|
530
|
+
this.state.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
531
|
+
this.state.error = null;
|
|
532
|
+
this.state.resultError = null;
|
|
533
|
+
this.state.rateLimited = false;
|
|
534
|
+
this.state.rateLimitResetAt = null;
|
|
535
|
+
logger.info({ agentId: this.id, pid: this.state.pid }, "Agent process started");
|
|
536
|
+
this.setStatus("running");
|
|
537
|
+
this.emitOutput("system", { message: `Process started (PID: ${this.state.pid ?? "unknown"})`, cmd, argCount: args.length });
|
|
538
|
+
this.processStream().catch((err) => {
|
|
539
|
+
this.state.error = err instanceof Error ? err.message : String(err);
|
|
540
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
541
|
+
});
|
|
542
|
+
this.processStderr().catch(() => {
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Resume a previous session with a new prompt.
|
|
547
|
+
*/
|
|
548
|
+
async resume(prompt) {
|
|
549
|
+
if (!this.state.sessionId) {
|
|
550
|
+
throw new Error(`Agent ${this.id} has no session to resume`);
|
|
551
|
+
}
|
|
552
|
+
if (this.subprocess) {
|
|
553
|
+
throw new Error(`Agent ${this.id} already has a running process`);
|
|
554
|
+
}
|
|
555
|
+
const args = this.buildArgs();
|
|
556
|
+
args.push("--resume", this.state.sessionId);
|
|
557
|
+
this.setStatus("starting");
|
|
558
|
+
const cmd = this.config.claudeCommand ?? "claude";
|
|
559
|
+
logger.info({ agentId: this.id, cmd, sessionId: this.state.sessionId }, "Resuming agent process");
|
|
560
|
+
this.subprocess = execa(cmd, args, {
|
|
561
|
+
input: prompt,
|
|
562
|
+
cwd: this.config.workingDirectory,
|
|
563
|
+
env: this.config.claudeEnv ? { ...process.env, ...this.config.claudeEnv } : void 0,
|
|
564
|
+
stdout: "pipe",
|
|
565
|
+
stderr: "pipe",
|
|
566
|
+
reject: false
|
|
567
|
+
});
|
|
568
|
+
this.state.pid = this.subprocess.pid ?? null;
|
|
569
|
+
this.state.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
570
|
+
this.state.error = null;
|
|
571
|
+
this.state.resultError = null;
|
|
572
|
+
this.state.rateLimited = false;
|
|
573
|
+
this.state.rateLimitResetAt = null;
|
|
574
|
+
logger.info({ agentId: this.id, pid: this.state.pid }, "Agent process resumed");
|
|
575
|
+
this.setStatus("running");
|
|
576
|
+
this.emitOutput("system", { message: `Process resumed (PID: ${this.state.pid ?? "unknown"})` });
|
|
577
|
+
this.processStream().catch((err) => {
|
|
578
|
+
this.state.error = err instanceof Error ? err.message : String(err);
|
|
579
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
580
|
+
});
|
|
581
|
+
this.processStderr().catch(() => {
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Graceful stop: SIGTERM, wait, then force kill.
|
|
586
|
+
*/
|
|
587
|
+
async stop(timeoutMs = 1e4) {
|
|
588
|
+
if (!this.subprocess || !this.state.pid) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this.setStatus("stopping");
|
|
592
|
+
try {
|
|
593
|
+
this.subprocess.kill("SIGTERM");
|
|
594
|
+
const timeoutPromise = new Promise(
|
|
595
|
+
(resolve) => setTimeout(() => resolve("timeout"), timeoutMs)
|
|
596
|
+
);
|
|
597
|
+
const exitPromise = this.subprocess.then(() => "exited");
|
|
598
|
+
const result = await Promise.race([exitPromise, timeoutPromise]);
|
|
599
|
+
if (result === "timeout" && this.state.pid) {
|
|
600
|
+
await killProcessTree(this.state.pid, "SIGKILL");
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
if (this.state.pid) {
|
|
604
|
+
try {
|
|
605
|
+
await killProcessTree(this.state.pid, "SIGKILL");
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} finally {
|
|
610
|
+
this.subprocess = null;
|
|
611
|
+
this.setStatus("stopped");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Force kill immediately.
|
|
616
|
+
*/
|
|
617
|
+
async kill() {
|
|
618
|
+
if (!this.subprocess || !this.state.pid) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
await killProcessTree(this.state.pid, "SIGKILL");
|
|
623
|
+
} catch {
|
|
624
|
+
} finally {
|
|
625
|
+
this.subprocess = null;
|
|
626
|
+
this.setStatus("stopped");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Build CLI arguments for the claude command.
|
|
631
|
+
* The prompt is piped via stdin, not passed as a positional argument.
|
|
632
|
+
*/
|
|
633
|
+
buildArgs() {
|
|
634
|
+
const args = [
|
|
635
|
+
"--print",
|
|
636
|
+
"--output-format",
|
|
637
|
+
"stream-json",
|
|
638
|
+
"--verbose",
|
|
639
|
+
"--model",
|
|
640
|
+
this.config.model ?? "sonnet"
|
|
641
|
+
];
|
|
642
|
+
if (this.config.permissionMode && this.config.permissionMode !== "default") {
|
|
643
|
+
args.push("--permission-mode", this.config.permissionMode);
|
|
644
|
+
}
|
|
645
|
+
if (this.config.systemPrompt) {
|
|
646
|
+
args.push("--system-prompt", this.config.systemPrompt);
|
|
647
|
+
}
|
|
648
|
+
if (this.config.allowedTools && this.config.allowedTools.length > 0) {
|
|
649
|
+
args.push("--allowedTools", this.config.allowedTools.join(","));
|
|
650
|
+
}
|
|
651
|
+
if (this.config.maxBudgetUsd !== void 0) {
|
|
652
|
+
args.push("--max-budget-usd", String(this.config.maxBudgetUsd));
|
|
653
|
+
}
|
|
654
|
+
if (this.config.claudeArgs && this.config.claudeArgs.length > 0) {
|
|
655
|
+
args.push(...this.config.claudeArgs);
|
|
656
|
+
}
|
|
657
|
+
return args;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Parse NDJSON stream from claude stdout.
|
|
661
|
+
* Reads stdout line by line and emits typed events.
|
|
662
|
+
*/
|
|
663
|
+
async processStream() {
|
|
664
|
+
if (!this.subprocess || !this.subprocess.stdout) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const stdout = this.subprocess.stdout;
|
|
668
|
+
for await (const chunk of stdout) {
|
|
669
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
670
|
+
this.streamBuffer += text;
|
|
671
|
+
let newlineIndex;
|
|
672
|
+
while ((newlineIndex = this.streamBuffer.indexOf("\n")) !== -1) {
|
|
673
|
+
const line = this.streamBuffer.slice(0, newlineIndex).trim();
|
|
674
|
+
this.streamBuffer = this.streamBuffer.slice(newlineIndex + 1);
|
|
675
|
+
if (!line) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const parsed = JSON.parse(line);
|
|
680
|
+
const eventType = parsed.type ?? "unknown";
|
|
681
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
682
|
+
const streamEvent = {
|
|
683
|
+
type: eventType,
|
|
684
|
+
agentId: this.id,
|
|
685
|
+
timestamp: now,
|
|
686
|
+
data: parsed
|
|
687
|
+
};
|
|
688
|
+
if (eventType === "system" && typeof parsed.session_id === "string") {
|
|
689
|
+
this.state.sessionId = parsed.session_id;
|
|
690
|
+
}
|
|
691
|
+
if (eventType === "result") {
|
|
692
|
+
if (typeof parsed.cost_usd === "number") {
|
|
693
|
+
this.state.totalCostUsd = parsed.cost_usd;
|
|
694
|
+
}
|
|
695
|
+
if (typeof parsed.num_turns === "number") {
|
|
696
|
+
this.state.turnCount = parsed.num_turns;
|
|
697
|
+
}
|
|
698
|
+
const usage = parsed.usage;
|
|
699
|
+
if (usage && typeof usage === "object") {
|
|
700
|
+
if (typeof usage.input_tokens === "number") {
|
|
701
|
+
this.state.totalInputTokens = usage.input_tokens;
|
|
702
|
+
}
|
|
703
|
+
if (typeof usage.output_tokens === "number") {
|
|
704
|
+
this.state.totalOutputTokens = usage.output_tokens;
|
|
705
|
+
}
|
|
706
|
+
if (typeof usage.cache_read_input_tokens === "number") {
|
|
707
|
+
this.state.totalCacheReadTokens = usage.cache_read_input_tokens;
|
|
708
|
+
}
|
|
709
|
+
if (typeof usage.cache_creation_input_tokens === "number") {
|
|
710
|
+
this.state.totalCacheCreationTokens = usage.cache_creation_input_tokens;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (parsed.is_error === true) {
|
|
714
|
+
const resultText = typeof parsed.result === "string" ? parsed.result : "";
|
|
715
|
+
this.state.resultError = resultText || "Unknown API error";
|
|
716
|
+
if (resultText.includes("429") || resultText.includes("Usage limit reached") || resultText.includes("rate limit")) {
|
|
717
|
+
this.state.rateLimited = true;
|
|
718
|
+
const resetMatch = resultText.match(/will reset at ([0-9T :.-]+)/);
|
|
719
|
+
if (resetMatch) {
|
|
720
|
+
this.state.rateLimitResetAt = resetMatch[1].trim();
|
|
721
|
+
}
|
|
722
|
+
logger.warn(
|
|
723
|
+
{ agentId: this.id, resetAt: this.state.rateLimitResetAt },
|
|
724
|
+
"Agent hit API rate limit (429)"
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
this.state.lastActivityAt = now;
|
|
730
|
+
this.emit("output", streamEvent);
|
|
731
|
+
} catch {
|
|
732
|
+
this.emitOutput("system", { message: `[stdout] ${line}` });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const result = await this.subprocess;
|
|
737
|
+
const exitCode = result?.exitCode ?? null;
|
|
738
|
+
this.subprocess = null;
|
|
739
|
+
if (this.state.resultError) {
|
|
740
|
+
this.state.error = this.state.resultError;
|
|
741
|
+
this.emitOutput("error", {
|
|
742
|
+
error: this.state.resultError,
|
|
743
|
+
rateLimited: this.state.rateLimited,
|
|
744
|
+
rateLimitResetAt: this.state.rateLimitResetAt
|
|
745
|
+
});
|
|
746
|
+
this.setStatus("error");
|
|
747
|
+
} else if (exitCode === 0 || exitCode === null) {
|
|
748
|
+
this.setStatus("stopped");
|
|
749
|
+
} else {
|
|
750
|
+
this.state.error = `Process exited with code ${exitCode}`;
|
|
751
|
+
this.emitOutput("error", { error: `Process exited with code ${exitCode}`, stderr: result?.stderr?.slice(0, 500) });
|
|
752
|
+
this.setStatus("error");
|
|
753
|
+
}
|
|
754
|
+
this.emit("stopped", exitCode);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Read stderr and emit lines as error events so they're visible.
|
|
758
|
+
*/
|
|
759
|
+
async processStderr() {
|
|
760
|
+
if (!this.subprocess || !this.subprocess.stderr) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
let stderrBuffer = "";
|
|
764
|
+
const stderr = this.subprocess.stderr;
|
|
765
|
+
for await (const chunk of stderr) {
|
|
766
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
767
|
+
stderrBuffer += text;
|
|
768
|
+
let newlineIndex;
|
|
769
|
+
while ((newlineIndex = stderrBuffer.indexOf("\n")) !== -1) {
|
|
770
|
+
const line = stderrBuffer.slice(0, newlineIndex).trim();
|
|
771
|
+
stderrBuffer = stderrBuffer.slice(newlineIndex + 1);
|
|
772
|
+
if (line) {
|
|
773
|
+
logger.warn({ agentId: this.id, stderr: line }, "Agent stderr output");
|
|
774
|
+
this.emitOutput("error", { error: line, source: "stderr" });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const remaining = stderrBuffer.trim();
|
|
779
|
+
if (remaining) {
|
|
780
|
+
logger.warn({ agentId: this.id, stderr: remaining }, "Agent stderr output");
|
|
781
|
+
this.emitOutput("error", { error: remaining, source: "stderr" });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Emit a synthetic output event.
|
|
786
|
+
*/
|
|
787
|
+
emitOutput(type, data) {
|
|
788
|
+
this.emit("output", {
|
|
789
|
+
type,
|
|
790
|
+
agentId: this.id,
|
|
791
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
792
|
+
data
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Update internal state and emit status change events.
|
|
797
|
+
*/
|
|
798
|
+
setStatus(status) {
|
|
799
|
+
const oldStatus = this.state.status;
|
|
800
|
+
if (oldStatus === status) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
this.state.status = status;
|
|
804
|
+
this.emit("status-changed", oldStatus, status);
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// src/agent/pool.ts
|
|
811
|
+
var pool_exports = {};
|
|
812
|
+
__export(pool_exports, {
|
|
813
|
+
AgentPool: () => AgentPool
|
|
814
|
+
});
|
|
815
|
+
import { EventEmitter as EventEmitter3 } from "eventemitter3";
|
|
816
|
+
var AgentPool;
|
|
817
|
+
var init_pool = __esm({
|
|
818
|
+
"src/agent/pool.ts"() {
|
|
819
|
+
"use strict";
|
|
820
|
+
init_lifecycle();
|
|
821
|
+
AgentPool = class extends EventEmitter3 {
|
|
822
|
+
agents = /* @__PURE__ */ new Map();
|
|
823
|
+
/**
|
|
824
|
+
* Spawn a new agent process with the given config and initial prompt.
|
|
825
|
+
*/
|
|
826
|
+
async spawnAgent(config, prompt) {
|
|
827
|
+
if (this.agents.has(config.id)) {
|
|
828
|
+
throw new Error(`Agent with id "${config.id}" already exists in the pool`);
|
|
829
|
+
}
|
|
830
|
+
const agent = new AgentProcess(config);
|
|
831
|
+
this.agents.set(config.id, agent);
|
|
832
|
+
this.attachListeners(agent);
|
|
833
|
+
await agent.spawn(prompt);
|
|
834
|
+
this.emit("agent:spawned", agent.id, agent);
|
|
835
|
+
return agent;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Stop a specific agent by ID with an optional timeout.
|
|
839
|
+
*/
|
|
840
|
+
async stopAgent(id, timeoutMs) {
|
|
841
|
+
const agent = this.agents.get(id);
|
|
842
|
+
if (!agent) {
|
|
843
|
+
throw new Error(`Agent "${id}" not found in pool`);
|
|
844
|
+
}
|
|
845
|
+
await agent.stop(timeoutMs);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Stop all agents concurrently. Uses Promise.allSettled for graceful handling.
|
|
849
|
+
*/
|
|
850
|
+
async stopAll(timeoutMs) {
|
|
851
|
+
const stopPromises = Array.from(this.agents.values()).map(
|
|
852
|
+
(agent) => agent.stop(timeoutMs)
|
|
853
|
+
);
|
|
854
|
+
await Promise.allSettled(stopPromises);
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Remove a stopped agent from the pool so it can be re-spawned.
|
|
858
|
+
*/
|
|
859
|
+
removeAgent(id) {
|
|
860
|
+
const agent = this.agents.get(id);
|
|
861
|
+
if (!agent) return;
|
|
862
|
+
const status = agent.currentState.status;
|
|
863
|
+
if (status !== "stopped" && status !== "error") {
|
|
864
|
+
throw new Error(`Cannot remove agent "${id}" with status "${status}" \u2014 must be stopped or error`);
|
|
865
|
+
}
|
|
866
|
+
this.agents.delete(id);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Get an agent process by ID.
|
|
870
|
+
*/
|
|
871
|
+
getAgent(id) {
|
|
872
|
+
return this.agents.get(id);
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Get all agents with a specific role.
|
|
876
|
+
*/
|
|
877
|
+
getAgentsByRole(role) {
|
|
878
|
+
return Array.from(this.agents.values()).filter(
|
|
879
|
+
(agent) => agent.currentState.config.role === role
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Get all agents with a specific status.
|
|
884
|
+
*/
|
|
885
|
+
getAgentsByStatus(status) {
|
|
886
|
+
return Array.from(this.agents.values()).filter(
|
|
887
|
+
(agent) => agent.currentState.status === status
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Get all agents in the pool.
|
|
892
|
+
*/
|
|
893
|
+
getAllAgents() {
|
|
894
|
+
return Array.from(this.agents.values());
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Get aggregate statistics across all agents.
|
|
898
|
+
*/
|
|
899
|
+
getStats() {
|
|
900
|
+
let running = 0;
|
|
901
|
+
let idle = 0;
|
|
902
|
+
let stopped = 0;
|
|
903
|
+
let totalCostUsd = 0;
|
|
904
|
+
for (const agent of this.agents.values()) {
|
|
905
|
+
const state = agent.currentState;
|
|
906
|
+
switch (state.status) {
|
|
907
|
+
case "running":
|
|
908
|
+
case "starting":
|
|
909
|
+
running++;
|
|
910
|
+
break;
|
|
911
|
+
case "idle":
|
|
912
|
+
idle++;
|
|
913
|
+
break;
|
|
914
|
+
case "stopped":
|
|
915
|
+
case "stopping":
|
|
916
|
+
case "error":
|
|
917
|
+
stopped++;
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
totalCostUsd += state.totalCostUsd;
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
total: this.agents.size,
|
|
924
|
+
running,
|
|
925
|
+
idle,
|
|
926
|
+
stopped,
|
|
927
|
+
totalCostUsd
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Attach event forwarding listeners from an agent process to the pool.
|
|
932
|
+
*/
|
|
933
|
+
attachListeners(agent) {
|
|
934
|
+
agent.on("output", (event) => {
|
|
935
|
+
this.emit("agent:output", event);
|
|
936
|
+
});
|
|
937
|
+
agent.on("status-changed", (oldStatus, newStatus) => {
|
|
938
|
+
this.emit("agent:status-changed", agent.id, oldStatus, newStatus);
|
|
939
|
+
});
|
|
940
|
+
agent.on("error", (error) => {
|
|
941
|
+
this.emit("agent:error", agent.id, error);
|
|
942
|
+
});
|
|
943
|
+
agent.on("stopped", (exitCode) => {
|
|
944
|
+
this.emit("agent:stopped", agent.id, exitCode);
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// src/agent/roles.ts
|
|
952
|
+
function buildTechStackSection(ts) {
|
|
953
|
+
if (!ts) return "";
|
|
954
|
+
const lines = ["# Tech Stack"];
|
|
955
|
+
if (ts.frontend) lines.push(`- **Frontend**: ${ts.frontend}`);
|
|
956
|
+
if (ts.uiLibrary) lines.push(`- **UI Library**: ${ts.uiLibrary}`);
|
|
957
|
+
if (ts.backend) lines.push(`- **Backend**: ${ts.backend}`);
|
|
958
|
+
if (ts.database) lines.push(`- **Database**: ${ts.database}`);
|
|
959
|
+
if (ts.other) lines.push(`- **Other**: ${ts.other}`);
|
|
960
|
+
if (lines.length === 1) return "";
|
|
961
|
+
lines.push("", "Use these technologies in your work. Follow their conventions and best practices.");
|
|
962
|
+
return lines.join("\n");
|
|
963
|
+
}
|
|
964
|
+
function getDefaultSystemPrompt(params) {
|
|
965
|
+
const {
|
|
966
|
+
role,
|
|
967
|
+
agentId,
|
|
968
|
+
projectName,
|
|
969
|
+
todoFilePath,
|
|
970
|
+
inboxPath,
|
|
971
|
+
outboxPath,
|
|
972
|
+
workingDirectory,
|
|
973
|
+
techStack
|
|
974
|
+
} = params;
|
|
975
|
+
const techStackSection = buildTechStackSection(techStack);
|
|
976
|
+
return `
|
|
977
|
+
# Agent Identity
|
|
978
|
+
|
|
979
|
+
You are **${agentId}**, a ${role} agent working on the **${projectName}** project.
|
|
980
|
+
|
|
981
|
+
${ROLE_DESCRIPTIONS[role]}
|
|
982
|
+
|
|
983
|
+
# Working Environment
|
|
984
|
+
|
|
985
|
+
- **Working Directory**: ${workingDirectory}
|
|
986
|
+
- **Todo File**: ${todoFilePath}
|
|
987
|
+
- **Inbox Directory**: ${inboxPath}
|
|
988
|
+
- **Outbox Directory**: ${outboxPath}
|
|
989
|
+
|
|
990
|
+
You should regularly check the todo file for task updates and your inbox for new messages.
|
|
991
|
+
When you need to communicate with other agents, write message files to your outbox.
|
|
992
|
+
|
|
993
|
+
${techStackSection ? techStackSection + "\n\n" : ""}${TODO_FORMAT_INSTRUCTIONS}
|
|
994
|
+
|
|
995
|
+
${MESSAGE_FORMAT_INSTRUCTIONS}
|
|
996
|
+
|
|
997
|
+
${ROLE_SPECIFIC_INSTRUCTIONS[role]}
|
|
998
|
+
|
|
999
|
+
${PROCESS_SAFETY_RULES}
|
|
1000
|
+
|
|
1001
|
+
## Architecture Compliance
|
|
1002
|
+
|
|
1003
|
+
If an \`ARCHITECTURE.md\` file exists in the working directory, you MUST read it before starting any task. It is the source of truth for project structure, component boundaries, data models, API contracts, and technical decisions.
|
|
1004
|
+
|
|
1005
|
+
- **Follow the architecture.** Do not contradict decisions documented in ARCHITECTURE.md.
|
|
1006
|
+
- **Check before creating.** Before adding new files, directories, or modules, verify the architecture allows it and place them where it specifies.
|
|
1007
|
+
- **Flag conflicts.** If a task asks you to do something that conflicts with ARCHITECTURE.md, do NOT silently proceed. Send a message to the orchestrator or architect describing the conflict and wait for guidance.
|
|
1008
|
+
- **Keep it updated.** If you are the architect or technical-writer and your work changes the architecture, update ARCHITECTURE.md to reflect the new state.
|
|
1009
|
+
|
|
1010
|
+
## Asking the User Questions
|
|
1011
|
+
|
|
1012
|
+
To ask the user a question, send a message with \`to: "user"\` and \`type: "question"\` to your outbox. Only ask when you genuinely cannot proceed without user input.
|
|
1013
|
+
|
|
1014
|
+
# General Guidelines
|
|
1015
|
+
|
|
1016
|
+
- Identify yourself as "${agentId}" in messages
|
|
1017
|
+
- Check your inbox at the start of each task cycle
|
|
1018
|
+
- Update the todo file when you start or complete work
|
|
1019
|
+
- Send task-blocked messages immediately when you hit a blocker
|
|
1020
|
+
- Focus on your role; do not overstep into other agents' responsibilities
|
|
1021
|
+
`.trim();
|
|
1022
|
+
}
|
|
1023
|
+
var ROLE_DESCRIPTIONS, TODO_FORMAT_INSTRUCTIONS, MESSAGE_FORMAT_INSTRUCTIONS, PROCESS_SAFETY_RULES, ROLE_SPECIFIC_INSTRUCTIONS;
|
|
1024
|
+
var init_roles = __esm({
|
|
1025
|
+
"src/agent/roles.ts"() {
|
|
1026
|
+
"use strict";
|
|
1027
|
+
ROLE_DESCRIPTIONS = {
|
|
1028
|
+
orchestrator: "The orchestrator is the top-level coordinator. It breaks down high-level goals into tasks, assigns work to other agents, monitors progress, resolves conflicts, and ensures the overall project stays on track. It communicates directives and reviews results from all other agents.",
|
|
1029
|
+
"project-manager": "The project manager tracks priorities, manages dependencies between tasks, updates timelines, and ensures work is progressing according to plan. It identifies blockers, re-prioritizes when needed, and keeps the task board organized and up to date.",
|
|
1030
|
+
architect: "The architect designs the system architecture, makes key technical decisions, defines API contracts, reviews design proposals, and ensures the codebase maintains structural integrity. It provides guidance on patterns, technology choices, and code organization.",
|
|
1031
|
+
developer: "The developer implements features, writes production code, fixes bugs, writes tests, and ensures code quality. It follows the architecture guidelines, writes clean and well-tested code, and reports progress on assigned tasks.",
|
|
1032
|
+
designer: "The designer handles UI/UX design decisions, creates component structures, defines styling and theming systems, ensures accessibility standards, and maintains visual consistency across the application.",
|
|
1033
|
+
"qa-engineer": "The QA engineer ensures software quality through comprehensive testing strategies. It writes and maintains test suites (unit, integration, e2e), identifies edge cases, validates requirements coverage, and reports bugs with clear reproduction steps.",
|
|
1034
|
+
devops: "The DevOps engineer handles infrastructure, CI/CD pipelines, deployment configurations, containerization (Docker), environment setup, monitoring, and build tooling. It ensures the project can be reliably built, tested, and deployed.",
|
|
1035
|
+
"technical-writer": "The technical writer creates and maintains project documentation including README files, API documentation, architecture decision records, user guides, setup instructions, and inline code documentation. It ensures docs stay accurate and accessible.",
|
|
1036
|
+
"code-reviewer": "The code reviewer examines completed work for correctness, security, performance, and adherence to project standards. It provides constructive feedback, identifies potential issues, and verifies that changes align with the architecture."
|
|
1037
|
+
};
|
|
1038
|
+
TODO_FORMAT_INSTRUCTIONS = `
|
|
1039
|
+
## Todo File Format
|
|
1040
|
+
|
|
1041
|
+
The shared todo file uses markdown: sections for Pending, In Progress, Done, Blocked.
|
|
1042
|
+
|
|
1043
|
+
Each task line: \`- [ ] T-XXX: Title [priority:high] [assignee:agent-id]\` with a description on the indented line below. Mark done tasks with \`[x]\`. Use \`[blocked-by:T-XXX]\` for dependencies.
|
|
1044
|
+
`.trim();
|
|
1045
|
+
MESSAGE_FORMAT_INSTRUCTIONS = `
|
|
1046
|
+
## Messaging System
|
|
1047
|
+
|
|
1048
|
+
**Inbox**: Check your inbox directory for JSON message files. Read all pending messages regularly.
|
|
1049
|
+
**Outbox**: To send a message, write a JSON file to your outbox directory.
|
|
1050
|
+
|
|
1051
|
+
Message format: \`{ "message": { "id": "unique-id", "type": "<type>", "from": "<your-id>", "to": "<recipient-id>", "subject": "...", "body": "...", "priority": "normal|urgent", "timestamp": "ISO date" }, "status": "pending" }\`
|
|
1052
|
+
|
|
1053
|
+
Types: task-assignment, task-update, task-complete, task-blocked, question, answer, review-request, review-result, system, directive
|
|
1054
|
+
`.trim();
|
|
1055
|
+
PROCESS_SAFETY_RULES = `
|
|
1056
|
+
## Process Safety Rules
|
|
1057
|
+
|
|
1058
|
+
You run in a shared multi-agent environment. Resource-intensive processes starve other agents and destabilize the system. The following are **strictly forbidden**:
|
|
1059
|
+
|
|
1060
|
+
**No Docker**: \`docker build\`, \`docker run\`, \`docker compose up\`, \`docker pull/push/exec\`, \`podman build/run\`. You MAY write Dockerfiles and compose configs.
|
|
1061
|
+
|
|
1062
|
+
**No servers**: \`npm run dev\`, \`npm start\`, \`next dev\`, \`vite\`, \`flask run\`, \`uvicorn\`, \`rails server\`, \`nodemon\`, or any process that listens on a port.
|
|
1063
|
+
|
|
1064
|
+
**Instead**: Write code and config files. Run only targeted tests (single file). Use lightweight commands (lint, type-check single files). If you need a build or server, message the user.
|
|
1065
|
+
`.trim();
|
|
1066
|
+
ROLE_SPECIFIC_INSTRUCTIONS = {
|
|
1067
|
+
orchestrator: `
|
|
1068
|
+
## Orchestrator-Specific Instructions
|
|
1069
|
+
|
|
1070
|
+
You are the central coordinator for this project. Your responsibilities:
|
|
1071
|
+
|
|
1072
|
+
1. **Goal Decomposition**: Break down high-level project goals into concrete, actionable tasks.
|
|
1073
|
+
Write these tasks to the shared todo file with clear descriptions and priorities.
|
|
1074
|
+
|
|
1075
|
+
2. **Task Assignment**: Assign tasks to the most appropriate agents based on their roles:
|
|
1076
|
+
- Architecture and design decisions -> architect
|
|
1077
|
+
- Implementation and coding -> developer
|
|
1078
|
+
- UI/UX, styling, components -> designer
|
|
1079
|
+
- Testing, QA, test suites -> qa-engineer
|
|
1080
|
+
- CI/CD, Docker, deployment, infrastructure -> devops
|
|
1081
|
+
- Documentation, README, API docs -> technical-writer
|
|
1082
|
+
- Code review, security review -> code-reviewer
|
|
1083
|
+
- Scheduling and tracking -> project-manager
|
|
1084
|
+
|
|
1085
|
+
3. **Progress Monitoring**: Regularly check the todo file and inbox for updates. Track which
|
|
1086
|
+
tasks are in progress, completed, or blocked.
|
|
1087
|
+
|
|
1088
|
+
4. **Coordination**: When tasks have dependencies, ensure they are executed in the right order.
|
|
1089
|
+
Resolve conflicts between agents. Re-assign work if an agent is overloaded. When QA
|
|
1090
|
+
returns a task, ensure the project-manager reassigns it to a developer promptly \u2014 blocked
|
|
1091
|
+
tasks stall all their dependents and can halt the entire project.
|
|
1092
|
+
|
|
1093
|
+
5. **Quality Control**: Review completed work by examining task completion messages.
|
|
1094
|
+
Request revisions if needed by sending review-request messages.
|
|
1095
|
+
|
|
1096
|
+
6. **Decision Making**: Make final calls on technical disputes, prioritization conflicts,
|
|
1097
|
+
and scope questions. Send directives when needed.
|
|
1098
|
+
|
|
1099
|
+
Do NOT implement code yourself. Your job is to coordinate, not to code.
|
|
1100
|
+
`.trim(),
|
|
1101
|
+
"project-manager": `
|
|
1102
|
+
## Project Manager-Specific Instructions
|
|
1103
|
+
|
|
1104
|
+
You manage the project timeline, priorities, and dependencies. Your responsibilities:
|
|
1105
|
+
|
|
1106
|
+
1. **Priority Management**: Keep tasks properly prioritized. Escalate critical items.
|
|
1107
|
+
Re-order work based on changing requirements or blockers.
|
|
1108
|
+
|
|
1109
|
+
2. **Dependency Tracking**: Identify and document task dependencies using blocked-by tags.
|
|
1110
|
+
Alert the orchestrator when circular dependencies or bottlenecks are detected.
|
|
1111
|
+
|
|
1112
|
+
3. **Timeline Updates**: Track progress rates and estimate remaining work. Update task
|
|
1113
|
+
descriptions with timeline notes when appropriate.
|
|
1114
|
+
|
|
1115
|
+
4. **Blocker Resolution**: When a task is blocked, investigate the cause by checking
|
|
1116
|
+
dependent tasks and sending questions to the relevant agents.
|
|
1117
|
+
|
|
1118
|
+
5. **Status Reporting**: Periodically summarize project status for the orchestrator,
|
|
1119
|
+
including completed tasks, active work, blockers, and risks.
|
|
1120
|
+
|
|
1121
|
+
6. **Todo Organization**: Keep the todo file clean and well-organized. Move completed
|
|
1122
|
+
tasks to the Done section. Archive old items. Ensure consistent formatting.
|
|
1123
|
+
|
|
1124
|
+
7. **Task Reassignment & QA Returns**: You have the authority to reassign tasks between
|
|
1125
|
+
agents. This is critical for unblocking the pipeline. When QA reports a blocker on a
|
|
1126
|
+
task (missing files, broken implementation, failing tests), you MUST:
|
|
1127
|
+
a. Move the task from Blocked back to In Progress in the todo file.
|
|
1128
|
+
b. Change the assignee from the qa-engineer to an available developer.
|
|
1129
|
+
c. Add a note: \`> [returned] Returned from QA: <reason>\`
|
|
1130
|
+
d. Set the priority to high (QA returns are rework and block downstream tasks).
|
|
1131
|
+
e. Send a task-assignment message to the developer with the QA findings.
|
|
1132
|
+
f. Send a task-update message to the orchestrator about the reassignment.
|
|
1133
|
+
|
|
1134
|
+
Do NOT leave QA-blocked tasks sitting. Every blocked task potentially stalls all its
|
|
1135
|
+
dependents. Act on QA returns immediately \u2014 they are your highest-priority pipeline work.
|
|
1136
|
+
|
|
1137
|
+
Focus on process and organization. Do NOT make technical decisions or write code.
|
|
1138
|
+
`.trim(),
|
|
1139
|
+
architect: `
|
|
1140
|
+
## Architect-Specific Instructions
|
|
1141
|
+
|
|
1142
|
+
You design the system architecture and make technical decisions. Your responsibilities:
|
|
1143
|
+
|
|
1144
|
+
1. **System Design**: Create and maintain the technical architecture. Document key
|
|
1145
|
+
design decisions, API contracts, data models, and component boundaries.
|
|
1146
|
+
|
|
1147
|
+
2. **Technology Choices**: Evaluate and select appropriate technologies, libraries,
|
|
1148
|
+
and patterns. Justify decisions with clear reasoning.
|
|
1149
|
+
|
|
1150
|
+
3. **Code Organization**: Define the project structure, module boundaries, naming
|
|
1151
|
+
conventions, and file organization patterns.
|
|
1152
|
+
|
|
1153
|
+
4. **Design Reviews**: When developers send review-request messages, evaluate the
|
|
1154
|
+
approach against the architecture. Provide specific, actionable feedback.
|
|
1155
|
+
|
|
1156
|
+
5. **Pattern Guidance**: Establish coding patterns, error handling strategies,
|
|
1157
|
+
and best practices. Document these for the development team.
|
|
1158
|
+
|
|
1159
|
+
6. **Technical Debt**: Identify areas of technical debt and propose refactoring
|
|
1160
|
+
plans when appropriate.
|
|
1161
|
+
|
|
1162
|
+
7. **ARCHITECTURE.md Ownership**: You own ARCHITECTURE.md. Keep it current whenever you
|
|
1163
|
+
make design decisions. All other agents rely on it as the source of truth \u2014 if it is
|
|
1164
|
+
outdated or missing, they will make incorrect assumptions.
|
|
1165
|
+
|
|
1166
|
+
You may write architectural documentation, design specs, and configuration files,
|
|
1167
|
+
but delegate implementation work to developers.
|
|
1168
|
+
`.trim(),
|
|
1169
|
+
developer: `
|
|
1170
|
+
## Developer-Specific Instructions
|
|
1171
|
+
|
|
1172
|
+
You implement features, write code, and fix bugs. Your responsibilities:
|
|
1173
|
+
|
|
1174
|
+
1. **Implementation**: Read ARCHITECTURE.md before writing any code. Follow its structure,
|
|
1175
|
+
patterns, and conventions. Implement features assigned to you in the todo file.
|
|
1176
|
+
|
|
1177
|
+
2. **Testing**: Write unit tests, integration tests, and any other tests needed to
|
|
1178
|
+
verify your work. Aim for good test coverage on critical paths.
|
|
1179
|
+
|
|
1180
|
+
3. **Bug Fixes**: When assigned bugs, investigate the root cause, implement a fix,
|
|
1181
|
+
and verify with tests.
|
|
1182
|
+
|
|
1183
|
+
4. **Code Quality**: Follow established patterns and conventions. Write clear comments
|
|
1184
|
+
where logic is complex. Keep functions focused and composable.
|
|
1185
|
+
|
|
1186
|
+
5. **Progress Updates**: When you start, complete, or get blocked on a task, update
|
|
1187
|
+
the todo file and send appropriate messages to the orchestrator.
|
|
1188
|
+
|
|
1189
|
+
6. **Review & QA Response**: When you receive review-result or task-assignment messages
|
|
1190
|
+
with QA feedback, treat them as high priority. QA returns mean a task was sent back
|
|
1191
|
+
because implementation was missing or broken \u2014 read the \`[BLOCKER]\` and \`[returned]\`
|
|
1192
|
+
notes in the todo file, fix the root cause, then mark the task done so QA can re-verify.
|
|
1193
|
+
|
|
1194
|
+
7. **Process Boundaries**: Your job is to write code and tests, NOT to build, compile, or
|
|
1195
|
+
run the project. Do not start servers or run full test suites. If you need to verify a specific
|
|
1196
|
+
piece of logic, run a single targeted test file. Write the code; let the CI pipeline
|
|
1197
|
+
and the user handle builds and deployments.
|
|
1198
|
+
|
|
1199
|
+
Focus on writing excellent code. Ask the architect (via messages) if you are unsure
|
|
1200
|
+
about design decisions. Report blockers promptly.
|
|
1201
|
+
`.trim(),
|
|
1202
|
+
designer: `
|
|
1203
|
+
## Designer-Specific Instructions
|
|
1204
|
+
|
|
1205
|
+
You handle UI/UX design and visual implementation. Your responsibilities:
|
|
1206
|
+
|
|
1207
|
+
1. **Component Design**: Design and implement UI components with clear, reusable
|
|
1208
|
+
structures. Define component APIs (props, events, slots).
|
|
1209
|
+
|
|
1210
|
+
2. **Styling System**: Create and maintain the design system: colors, typography,
|
|
1211
|
+
spacing, breakpoints, and theme configuration.
|
|
1212
|
+
|
|
1213
|
+
3. **Layout Architecture**: Design page layouts, navigation structures, and
|
|
1214
|
+
responsive behavior. Ensure consistency across views.
|
|
1215
|
+
|
|
1216
|
+
4. **Accessibility**: Ensure all UI components meet accessibility standards (WCAG).
|
|
1217
|
+
Include proper ARIA attributes, keyboard navigation, and screen reader support.
|
|
1218
|
+
|
|
1219
|
+
5. **Visual Consistency**: Maintain visual coherence across the application.
|
|
1220
|
+
Review UI-related code changes for design consistency.
|
|
1221
|
+
|
|
1222
|
+
6. **Design Documentation**: Document design tokens, component usage patterns,
|
|
1223
|
+
and visual guidelines for the development team.
|
|
1224
|
+
|
|
1225
|
+
You write UI code (components, styles, layouts). Coordinate with developers on
|
|
1226
|
+
integration and with the architect on component architecture.
|
|
1227
|
+
`.trim(),
|
|
1228
|
+
"qa-engineer": `
|
|
1229
|
+
## QA Engineer-Specific Instructions
|
|
1230
|
+
|
|
1231
|
+
You ensure software quality through testing and validation. Your responsibilities:
|
|
1232
|
+
|
|
1233
|
+
1. **Test Strategy**: Design comprehensive testing strategies covering unit tests,
|
|
1234
|
+
integration tests, and end-to-end tests. Identify critical paths that need coverage.
|
|
1235
|
+
|
|
1236
|
+
2. **Test Implementation**: Write automated tests using the project's testing framework.
|
|
1237
|
+
Cover happy paths, edge cases, error conditions, and boundary values.
|
|
1238
|
+
|
|
1239
|
+
3. **Bug Reporting & QA Returns**: When you find bugs or missing implementation, you must
|
|
1240
|
+
act immediately \u2014 do not just mark a task blocked and wait. Document the issue clearly
|
|
1241
|
+
(reproduction steps, expected vs actual behavior, missing files), then:
|
|
1242
|
+
a. Add a \`> [BLOCKER]\` note to the task in the todo file with specifics.
|
|
1243
|
+
b. Send a task-blocked message to the **project-manager** (not just the orchestrator)
|
|
1244
|
+
so they can reassign the task back to a developer.
|
|
1245
|
+
c. Add a \`> [pipeline] Returned from QA to develop - <reason>\` note.
|
|
1246
|
+
The project manager will handle reassignment. Do not keep working on a task that has
|
|
1247
|
+
missing or fundamentally broken implementation \u2014 return it.
|
|
1248
|
+
|
|
1249
|
+
4. **Requirements Validation**: Verify that implementations match the task descriptions
|
|
1250
|
+
and acceptance criteria. Flag gaps between requirements and implementation.
|
|
1251
|
+
|
|
1252
|
+
5. **Regression Testing**: Ensure new changes don't break existing functionality.
|
|
1253
|
+
Maintain test suites that catch regressions early.
|
|
1254
|
+
|
|
1255
|
+
6. **Test Documentation**: Document test coverage, testing patterns, and any manual
|
|
1256
|
+
test procedures that cannot be automated.
|
|
1257
|
+
|
|
1258
|
+
7. **Process Boundaries**: Write tests, but be cautious about executing them. Run only
|
|
1259
|
+
targeted, specific test files \u2014 never a full test suite (e.g. \`npm test\` or \`pytest\`
|
|
1260
|
+
with no arguments). A single test file that takes more than 60 seconds likely indicates
|
|
1261
|
+
an integration test that depends on external services; skip it and flag it for manual
|
|
1262
|
+
execution. Do not start servers, databases, or other services as part of testing.
|
|
1263
|
+
|
|
1264
|
+
Focus on finding issues before they reach production. Coordinate with developers on
|
|
1265
|
+
bug fixes and with the architect on testability of the design.
|
|
1266
|
+
`.trim(),
|
|
1267
|
+
devops: `
|
|
1268
|
+
## DevOps-Specific Instructions
|
|
1269
|
+
|
|
1270
|
+
You handle infrastructure, deployment, and build tooling. Your responsibilities:
|
|
1271
|
+
|
|
1272
|
+
1. **CI/CD Pipeline**: Set up and maintain continuous integration and deployment pipelines.
|
|
1273
|
+
Configure build, test, lint, and deploy stages.
|
|
1274
|
+
|
|
1275
|
+
2. **Containerization**: Create and maintain Dockerfiles, docker-compose configurations,
|
|
1276
|
+
and container orchestration setups as needed.
|
|
1277
|
+
|
|
1278
|
+
3. **Environment Configuration**: Set up development, staging, and production environment
|
|
1279
|
+
configurations. Manage environment variables, secrets handling, and config files.
|
|
1280
|
+
|
|
1281
|
+
4. **Build Tooling**: Configure and optimize build tools, bundlers, and compilation
|
|
1282
|
+
pipelines. Ensure fast, reliable builds.
|
|
1283
|
+
|
|
1284
|
+
5. **Monitoring & Logging**: Set up application monitoring, error tracking, health checks,
|
|
1285
|
+
and structured logging where applicable.
|
|
1286
|
+
|
|
1287
|
+
6. **Infrastructure as Code**: Define infrastructure using code (Terraform, CloudFormation,
|
|
1288
|
+
etc.) when applicable. Document deployment procedures.
|
|
1289
|
+
|
|
1290
|
+
7. **Process Boundaries**: You write configuration, you do NOT execute it. Do not run
|
|
1291
|
+
\`docker build\`, \`docker run\`, \`docker compose up\`, or any Docker commands. Do not
|
|
1292
|
+
run build pipelines, start servers, or execute deployment scripts. Write the Dockerfiles,
|
|
1293
|
+
CI configs, and shell scripts \u2014 the actual execution happens in CI/CD or by the user.
|
|
1294
|
+
|
|
1295
|
+
Write configuration files, scripts, and infrastructure code. Do NOT execute Docker
|
|
1296
|
+
commands, build pipelines, or deployment scripts in this environment. Coordinate with
|
|
1297
|
+
developers on build requirements and with the architect on deployment architecture.
|
|
1298
|
+
`.trim(),
|
|
1299
|
+
"technical-writer": `
|
|
1300
|
+
## Technical Writer-Specific Instructions
|
|
1301
|
+
|
|
1302
|
+
You create and maintain project documentation. Your responsibilities:
|
|
1303
|
+
|
|
1304
|
+
1. **README & Setup Guides**: Write clear README files with project overview, setup
|
|
1305
|
+
instructions, prerequisites, and quickstart guides.
|
|
1306
|
+
|
|
1307
|
+
2. **API Documentation**: Document public APIs, endpoints, request/response formats,
|
|
1308
|
+
authentication, and error codes. Keep API docs in sync with implementation.
|
|
1309
|
+
|
|
1310
|
+
3. **Architecture Documentation**: Work with the architect to document system design,
|
|
1311
|
+
data flow diagrams, component relationships, and design decisions (ADRs).
|
|
1312
|
+
|
|
1313
|
+
4. **User Guides**: Write end-user documentation, tutorials, and how-to guides that
|
|
1314
|
+
explain features and workflows in accessible language.
|
|
1315
|
+
|
|
1316
|
+
5. **Code Documentation**: Review code for missing or outdated inline documentation.
|
|
1317
|
+
Add JSDoc/docstrings to public interfaces, complex functions, and non-obvious logic.
|
|
1318
|
+
|
|
1319
|
+
6. **Changelog & Release Notes**: Maintain changelogs and write release notes that
|
|
1320
|
+
clearly communicate changes, fixes, and breaking changes.
|
|
1321
|
+
|
|
1322
|
+
Write documentation files (markdown, JSDoc, etc.). Coordinate with all team members
|
|
1323
|
+
to ensure docs accurately reflect the current state of the project.
|
|
1324
|
+
`.trim(),
|
|
1325
|
+
"code-reviewer": `
|
|
1326
|
+
## Code Reviewer-Specific Instructions
|
|
1327
|
+
|
|
1328
|
+
You review completed work for quality and correctness. Your responsibilities:
|
|
1329
|
+
|
|
1330
|
+
1. **Code Review**: Examine implementation code for correctness, readability, and
|
|
1331
|
+
adherence to project conventions. Check for logic errors, off-by-one errors,
|
|
1332
|
+
and edge case handling.
|
|
1333
|
+
|
|
1334
|
+
2. **Security Review**: Identify potential security vulnerabilities: injection attacks,
|
|
1335
|
+
authentication/authorization issues, data exposure, and insecure defaults.
|
|
1336
|
+
|
|
1337
|
+
3. **Performance Review**: Flag performance anti-patterns: N+1 queries, unnecessary
|
|
1338
|
+
re-renders, memory leaks, missing indexes, and inefficient algorithms.
|
|
1339
|
+
|
|
1340
|
+
4. **Standards Compliance**: Verify code follows ARCHITECTURE.md, naming conventions,
|
|
1341
|
+
file organization patterns, and coding standards. Flag deviations.
|
|
1342
|
+
|
|
1343
|
+
5. **Feedback Delivery**: Provide specific, constructive feedback. Distinguish between
|
|
1344
|
+
blocking issues and suggestions. Include code examples for recommended changes.
|
|
1345
|
+
|
|
1346
|
+
6. **Verification**: After developers address review feedback, verify the changes
|
|
1347
|
+
resolve the identified issues without introducing new problems.
|
|
1348
|
+
|
|
1349
|
+
Do NOT implement changes yourself. Send review-result messages with specific feedback.
|
|
1350
|
+
Coordinate with the architect on design-level concerns and developers on implementation.
|
|
1351
|
+
`.trim()
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
// src/agent/config.ts
|
|
1357
|
+
var config_exports = {};
|
|
1358
|
+
__export(config_exports, {
|
|
1359
|
+
loadAgentConfigs: () => loadAgentConfigs
|
|
1360
|
+
});
|
|
1361
|
+
import { readFileSync, existsSync } from "fs";
|
|
1362
|
+
import path from "path";
|
|
1363
|
+
function loadAgentConfigs(projectConfig) {
|
|
1364
|
+
const configs = [];
|
|
1365
|
+
const { settings } = projectConfig;
|
|
1366
|
+
for (const roleConfig of projectConfig.agents) {
|
|
1367
|
+
const count = roleConfig.count ?? 1;
|
|
1368
|
+
for (let i = 1; i <= count; i++) {
|
|
1369
|
+
const agentId = count > 1 ? `${roleConfig.role}-${i}` : roleConfig.role;
|
|
1370
|
+
const systemPrompt = resolveSystemPrompt({
|
|
1371
|
+
roleConfig,
|
|
1372
|
+
agentId,
|
|
1373
|
+
role: roleConfig.role,
|
|
1374
|
+
projectConfig
|
|
1375
|
+
});
|
|
1376
|
+
const mergedArgs = [
|
|
1377
|
+
...settings.claudeArgs ?? [],
|
|
1378
|
+
...roleConfig.claudeArgs ?? []
|
|
1379
|
+
];
|
|
1380
|
+
const mergedEnv = {
|
|
1381
|
+
...settings.claudeEnv ?? {},
|
|
1382
|
+
...roleConfig.claudeEnv ?? {}
|
|
1383
|
+
};
|
|
1384
|
+
const config = {
|
|
1385
|
+
id: agentId,
|
|
1386
|
+
role: roleConfig.role,
|
|
1387
|
+
name: formatAgentName(roleConfig.role, count > 1 ? i : void 0),
|
|
1388
|
+
model: roleConfig.model ?? settings.defaultModel,
|
|
1389
|
+
systemPrompt,
|
|
1390
|
+
allowedTools: roleConfig.allowedTools,
|
|
1391
|
+
maxBudgetUsd: roleConfig.maxBudgetUsd,
|
|
1392
|
+
workingDirectory: settings.workingDirectory,
|
|
1393
|
+
permissionMode: roleConfig.permissionMode ?? "default",
|
|
1394
|
+
claudeCommand: settings.claudeCommand,
|
|
1395
|
+
claudeArgs: mergedArgs.length > 0 ? mergedArgs : void 0,
|
|
1396
|
+
claudeEnv: Object.keys(mergedEnv).length > 0 ? mergedEnv : void 0,
|
|
1397
|
+
maxTurns: roleConfig.maxTurns
|
|
1398
|
+
};
|
|
1399
|
+
configs.push(config);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return configs;
|
|
1403
|
+
}
|
|
1404
|
+
function resolveSystemPrompt(params) {
|
|
1405
|
+
const { roleConfig, agentId, role, projectConfig } = params;
|
|
1406
|
+
const { settings } = projectConfig;
|
|
1407
|
+
if (roleConfig.systemPromptOverride) {
|
|
1408
|
+
return roleConfig.systemPromptOverride;
|
|
1409
|
+
}
|
|
1410
|
+
if (roleConfig.systemPromptFile) {
|
|
1411
|
+
const promptPath = path.isAbsolute(roleConfig.systemPromptFile) ? roleConfig.systemPromptFile : path.resolve(settings.workingDirectory, roleConfig.systemPromptFile);
|
|
1412
|
+
return readFileSync(promptPath, "utf-8");
|
|
1413
|
+
}
|
|
1414
|
+
const localAgentsDir = path.resolve(settings.workingDirectory, ".agents");
|
|
1415
|
+
const localPromptPath = path.join(localAgentsDir, `${role}.md`);
|
|
1416
|
+
if (existsSync(localPromptPath)) {
|
|
1417
|
+
try {
|
|
1418
|
+
const content = readFileSync(localPromptPath, "utf-8").trim();
|
|
1419
|
+
if (content) {
|
|
1420
|
+
const todoFilePath2 = path.resolve(settings.workingDirectory, settings.todoFile);
|
|
1421
|
+
const messagesDir2 = path.resolve(settings.workingDirectory, settings.messagesDirectory);
|
|
1422
|
+
return content.replace(/\{\{agentId\}\}/g, agentId).replace(/\{\{projectName\}\}/g, projectConfig.name).replace(/\{\{todoFilePath\}\}/g, todoFilePath2).replace(/\{\{inboxPath\}\}/g, path.join(messagesDir2, agentId, "inbox")).replace(/\{\{outboxPath\}\}/g, path.join(messagesDir2, agentId, "outbox")).replace(/\{\{workingDirectory\}\}/g, settings.workingDirectory);
|
|
1423
|
+
}
|
|
1424
|
+
} catch {
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
const todoFilePath = path.resolve(settings.workingDirectory, settings.todoFile);
|
|
1428
|
+
const messagesDir = path.resolve(settings.workingDirectory, settings.messagesDirectory);
|
|
1429
|
+
return getDefaultSystemPrompt({
|
|
1430
|
+
role,
|
|
1431
|
+
agentId,
|
|
1432
|
+
projectName: projectConfig.name,
|
|
1433
|
+
todoFilePath,
|
|
1434
|
+
inboxPath: path.join(messagesDir, agentId, "inbox"),
|
|
1435
|
+
outboxPath: path.join(messagesDir, agentId, "outbox"),
|
|
1436
|
+
workingDirectory: settings.workingDirectory,
|
|
1437
|
+
techStack: projectConfig.techStack
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
function formatAgentName(role, index) {
|
|
1441
|
+
const roleName = role.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1442
|
+
return index !== void 0 ? `${roleName} ${index}` : roleName;
|
|
1443
|
+
}
|
|
1444
|
+
var init_config = __esm({
|
|
1445
|
+
"src/agent/config.ts"() {
|
|
1446
|
+
"use strict";
|
|
1447
|
+
init_roles();
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
// src/types/orchestrator-activity.ts
|
|
1452
|
+
var PIPELINE_STAGE_ORDER = ["plan", "develop", "qa", "review"];
|
|
1453
|
+
var PIPELINE_STAGE_ROLES = {
|
|
1454
|
+
plan: ["project-manager", "architect"],
|
|
1455
|
+
develop: ["developer"],
|
|
1456
|
+
qa: ["qa-engineer"],
|
|
1457
|
+
review: ["project-manager"]
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
// src/index.ts
|
|
1461
|
+
init_parser();
|
|
1462
|
+
init_writer();
|
|
1463
|
+
init_manager();
|
|
1464
|
+
init_lifecycle();
|
|
1465
|
+
init_pool();
|
|
1466
|
+
init_roles();
|
|
1467
|
+
init_config();
|
|
1468
|
+
|
|
1469
|
+
// src/messaging/broker.ts
|
|
1470
|
+
import { EventEmitter as EventEmitter5 } from "eventemitter3";
|
|
1471
|
+
import { mkdir as mkdir2, writeFile as writeFile3, readFile as readFile3, readdir, rename as rename2 } from "fs/promises";
|
|
1472
|
+
import { join as join2 } from "path";
|
|
1473
|
+
|
|
1474
|
+
// src/messaging/inbox.ts
|
|
1475
|
+
init_logger();
|
|
1476
|
+
import { watch } from "chokidar";
|
|
1477
|
+
import { EventEmitter as EventEmitter4 } from "eventemitter3";
|
|
1478
|
+
import { readFile as readFile2, rename, mkdir } from "fs/promises";
|
|
1479
|
+
import { join, basename } from "path";
|
|
1480
|
+
var logger2 = createLogger("inbox-watcher");
|
|
1481
|
+
var InboxWatcher = class extends EventEmitter4 {
|
|
1482
|
+
watcher = null;
|
|
1483
|
+
dir;
|
|
1484
|
+
processedDir;
|
|
1485
|
+
constructor(inboxDir, processedDir) {
|
|
1486
|
+
super();
|
|
1487
|
+
this.dir = inboxDir;
|
|
1488
|
+
this.processedDir = processedDir;
|
|
1489
|
+
}
|
|
1490
|
+
async start() {
|
|
1491
|
+
await mkdir(this.dir, { recursive: true });
|
|
1492
|
+
await mkdir(this.processedDir, { recursive: true });
|
|
1493
|
+
this.watcher = watch(this.dir, {
|
|
1494
|
+
ignoreInitial: false,
|
|
1495
|
+
awaitWriteFinish: { stabilityThreshold: 200 }
|
|
1496
|
+
});
|
|
1497
|
+
this.watcher.on("add", (filePath) => {
|
|
1498
|
+
if (filePath.endsWith(".json")) {
|
|
1499
|
+
this.processFile(filePath).catch((err) => {
|
|
1500
|
+
logger2.error({ err, filePath }, "Failed to process inbox file");
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
this.watcher.on("error", (err) => {
|
|
1505
|
+
logger2.error({ err }, "Inbox watcher error");
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
async stop() {
|
|
1509
|
+
if (this.watcher) {
|
|
1510
|
+
await this.watcher.close();
|
|
1511
|
+
this.watcher = null;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
async processFile(filePath) {
|
|
1515
|
+
try {
|
|
1516
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1517
|
+
let envelope;
|
|
1518
|
+
try {
|
|
1519
|
+
envelope = JSON.parse(content);
|
|
1520
|
+
} catch {
|
|
1521
|
+
logger2.warn({ filePath }, "Malformed JSON in inbox file, skipping");
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
envelope.deliveredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1525
|
+
envelope.status = "delivered";
|
|
1526
|
+
this.emit("message", envelope);
|
|
1527
|
+
const destPath = join(this.processedDir, basename(filePath));
|
|
1528
|
+
await rename(filePath, destPath);
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
if (err.code === "ENOENT") {
|
|
1531
|
+
logger2.debug({ filePath }, "Inbox file already removed");
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
throw err;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
// src/messaging/broker.ts
|
|
1540
|
+
init_logger();
|
|
1541
|
+
var logger3 = createLogger("message-broker");
|
|
1542
|
+
function messageFileName(message) {
|
|
1543
|
+
const prefix = message.taskId ?? "general";
|
|
1544
|
+
return `${prefix}_${message.id}.json`;
|
|
1545
|
+
}
|
|
1546
|
+
var MessageBroker = class extends EventEmitter5 {
|
|
1547
|
+
baseDir;
|
|
1548
|
+
watchers = /* @__PURE__ */ new Map();
|
|
1549
|
+
registeredAgents = /* @__PURE__ */ new Set();
|
|
1550
|
+
constructor(baseDir) {
|
|
1551
|
+
super();
|
|
1552
|
+
this.baseDir = baseDir;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Initialize directory structure for an agent.
|
|
1556
|
+
* Creates inbox/, outbox/, and processed/ directories.
|
|
1557
|
+
*/
|
|
1558
|
+
async registerAgent(agentId) {
|
|
1559
|
+
const agentDir = join2(this.baseDir, agentId);
|
|
1560
|
+
await mkdir2(join2(agentDir, "inbox"), { recursive: true });
|
|
1561
|
+
await mkdir2(join2(agentDir, "outbox"), { recursive: true });
|
|
1562
|
+
await mkdir2(join2(agentDir, "processed"), { recursive: true });
|
|
1563
|
+
this.registeredAgents.add(agentId);
|
|
1564
|
+
logger3.debug({ agentId }, "Registered agent");
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Initialize directories for all agents and start watching.
|
|
1568
|
+
* Watches each agent's outbox directory so that messages agents write
|
|
1569
|
+
* there are automatically picked up and routed.
|
|
1570
|
+
*/
|
|
1571
|
+
async start(agentIds) {
|
|
1572
|
+
for (const agentId of agentIds) {
|
|
1573
|
+
await this.registerAgent(agentId);
|
|
1574
|
+
}
|
|
1575
|
+
for (const agentId of agentIds) {
|
|
1576
|
+
const outboxDir = join2(this.baseDir, agentId, "outbox");
|
|
1577
|
+
const processedDir = join2(this.baseDir, agentId, "processed");
|
|
1578
|
+
const watcher = new InboxWatcher(outboxDir, processedDir);
|
|
1579
|
+
watcher.on("message", (envelope) => {
|
|
1580
|
+
logger3.info(
|
|
1581
|
+
{ from: envelope.message.from, to: envelope.message.to, type: envelope.message.type },
|
|
1582
|
+
"Message received from agent outbox"
|
|
1583
|
+
);
|
|
1584
|
+
this.emit("message:received", envelope.message);
|
|
1585
|
+
if (envelope.message.to === "*") {
|
|
1586
|
+
this.broadcast(envelope.message).catch((err) => {
|
|
1587
|
+
logger3.error({ err }, "Failed to broadcast message");
|
|
1588
|
+
});
|
|
1589
|
+
} else {
|
|
1590
|
+
this.sendMessage(envelope.message).catch((err) => {
|
|
1591
|
+
logger3.error({ err }, "Failed to route message");
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
await watcher.start();
|
|
1596
|
+
this.watchers.set(agentId, watcher);
|
|
1597
|
+
}
|
|
1598
|
+
logger3.info({ agentCount: agentIds.length }, "Message broker started");
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Send a message to an agent's inbox.
|
|
1602
|
+
* If message.to is "*", broadcasts to all registered agents instead.
|
|
1603
|
+
*/
|
|
1604
|
+
async sendMessage(message) {
|
|
1605
|
+
if (message.to === "*") {
|
|
1606
|
+
await this.broadcast(message);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
if (message.to === "user") {
|
|
1610
|
+
logger3.info({ from: message.from, type: message.type }, "User-directed message intercepted");
|
|
1611
|
+
this.emit("message:user-directed", message);
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
if (!this.registeredAgents.has(message.to)) {
|
|
1615
|
+
logger3.warn({ to: message.to }, "Attempted to send message to unregistered agent");
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const envelope = {
|
|
1619
|
+
message,
|
|
1620
|
+
deliveredAt: null,
|
|
1621
|
+
readAt: null,
|
|
1622
|
+
status: "pending"
|
|
1623
|
+
};
|
|
1624
|
+
const filePath = join2(this.baseDir, message.to, "inbox", messageFileName(message));
|
|
1625
|
+
await writeFile3(filePath, JSON.stringify(envelope, null, 2), "utf-8");
|
|
1626
|
+
logger3.debug(
|
|
1627
|
+
{ id: message.id, from: message.from, to: message.to, type: message.type },
|
|
1628
|
+
"Message sent"
|
|
1629
|
+
);
|
|
1630
|
+
this.emit("message:sent", message);
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Send a message to all registered agents' inboxes.
|
|
1634
|
+
*/
|
|
1635
|
+
async broadcast(message) {
|
|
1636
|
+
const promises = [];
|
|
1637
|
+
for (const agentId of this.registeredAgents) {
|
|
1638
|
+
if (agentId === message.from) {
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
const envelope = {
|
|
1642
|
+
message: { ...message, to: agentId },
|
|
1643
|
+
deliveredAt: null,
|
|
1644
|
+
readAt: null,
|
|
1645
|
+
status: "pending"
|
|
1646
|
+
};
|
|
1647
|
+
const broadcastMsg = { ...message, to: agentId };
|
|
1648
|
+
const filePath = join2(this.baseDir, agentId, "inbox", messageFileName(broadcastMsg));
|
|
1649
|
+
promises.push(
|
|
1650
|
+
writeFile3(filePath, JSON.stringify(envelope, null, 2), "utf-8").then(() => {
|
|
1651
|
+
this.emit("message:sent", broadcastMsg);
|
|
1652
|
+
})
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
await Promise.all(promises);
|
|
1656
|
+
logger3.debug(
|
|
1657
|
+
{ id: message.id, from: message.from, agentCount: this.registeredAgents.size },
|
|
1658
|
+
"Broadcast sent"
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Mark an inbox message as processed and move it to the processed directory.
|
|
1663
|
+
* Called when a task completes so the inbox reflects the current state.
|
|
1664
|
+
*/
|
|
1665
|
+
async markInboxProcessed(agentId, taskId) {
|
|
1666
|
+
const inboxDir = join2(this.baseDir, agentId, "inbox");
|
|
1667
|
+
const processedDir = join2(this.baseDir, agentId, "processed");
|
|
1668
|
+
try {
|
|
1669
|
+
const files = await readdir(inboxDir);
|
|
1670
|
+
for (const file of files) {
|
|
1671
|
+
if (!file.endsWith(".json")) continue;
|
|
1672
|
+
const filePath = join2(inboxDir, file);
|
|
1673
|
+
try {
|
|
1674
|
+
const content = await readFile3(filePath, "utf-8");
|
|
1675
|
+
const envelope = JSON.parse(content);
|
|
1676
|
+
if (envelope.message.taskId === taskId) {
|
|
1677
|
+
envelope.status = "processed";
|
|
1678
|
+
envelope.readAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1679
|
+
await writeFile3(filePath, JSON.stringify(envelope, null, 2), "utf-8");
|
|
1680
|
+
const destPath = join2(processedDir, file);
|
|
1681
|
+
await rename2(filePath, destPath);
|
|
1682
|
+
logger3.debug({ agentId, taskId, messageId: envelope.message.id }, "Inbox message marked as processed");
|
|
1683
|
+
}
|
|
1684
|
+
} catch {
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Clean up stale inbox messages for a task (e.g., after restart when
|
|
1692
|
+
* in-progress tasks are reset). Moves matching messages to processed/.
|
|
1693
|
+
*/
|
|
1694
|
+
async cleanInboxForTask(agentId, taskId) {
|
|
1695
|
+
await this.markInboxProcessed(agentId, taskId);
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Read processed messages for a specific task across all agents.
|
|
1699
|
+
* Used to display task completion details in the TUI.
|
|
1700
|
+
*/
|
|
1701
|
+
async getProcessedMessagesForTask(taskId) {
|
|
1702
|
+
const results = [];
|
|
1703
|
+
let agentDirs = [];
|
|
1704
|
+
try {
|
|
1705
|
+
const entries = await readdir(this.baseDir, { withFileTypes: true });
|
|
1706
|
+
agentDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1707
|
+
} catch {
|
|
1708
|
+
return results;
|
|
1709
|
+
}
|
|
1710
|
+
for (const agentId of agentDirs) {
|
|
1711
|
+
for (const subdir of ["inbox", "processed"]) {
|
|
1712
|
+
const dir = join2(this.baseDir, agentId, subdir);
|
|
1713
|
+
try {
|
|
1714
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1715
|
+
for (const entry of entries) {
|
|
1716
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
1717
|
+
try {
|
|
1718
|
+
const content = await readFile3(join2(dir, entry.name), "utf-8");
|
|
1719
|
+
const envelope = JSON.parse(content);
|
|
1720
|
+
if (envelope.message?.taskId === taskId) {
|
|
1721
|
+
results.push(envelope);
|
|
1722
|
+
}
|
|
1723
|
+
} catch {
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
} catch {
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1731
|
+
const deduped = results.filter((env) => {
|
|
1732
|
+
if (seen.has(env.message.id)) return false;
|
|
1733
|
+
seen.add(env.message.id);
|
|
1734
|
+
return true;
|
|
1735
|
+
});
|
|
1736
|
+
deduped.sort((a, b) => a.message.timestamp.localeCompare(b.message.timestamp));
|
|
1737
|
+
return deduped;
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Move ALL inbox messages to processed/ for every agent directory.
|
|
1741
|
+
* Called on startup to ensure a clean slate — agents should not pick up
|
|
1742
|
+
* stale task-assignment messages from a previous session.
|
|
1743
|
+
*/
|
|
1744
|
+
async cleanAllInboxes() {
|
|
1745
|
+
let agentDirs = [];
|
|
1746
|
+
try {
|
|
1747
|
+
const entries = await readdir(this.baseDir, { withFileTypes: true });
|
|
1748
|
+
agentDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1749
|
+
} catch {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
let movedCount = 0;
|
|
1753
|
+
for (const agentId of agentDirs) {
|
|
1754
|
+
const inboxDir = join2(this.baseDir, agentId, "inbox");
|
|
1755
|
+
const processedDir = join2(this.baseDir, agentId, "processed");
|
|
1756
|
+
try {
|
|
1757
|
+
await mkdir2(processedDir, { recursive: true });
|
|
1758
|
+
const files = await readdir(inboxDir);
|
|
1759
|
+
for (const file of files) {
|
|
1760
|
+
if (!file.endsWith(".json")) continue;
|
|
1761
|
+
try {
|
|
1762
|
+
await rename2(join2(inboxDir, file), join2(processedDir, file));
|
|
1763
|
+
movedCount++;
|
|
1764
|
+
} catch {
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
} catch {
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (movedCount > 0) {
|
|
1771
|
+
logger3.info({ movedCount }, "Cleaned stale inbox messages from previous session");
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
get messagesBaseDir() {
|
|
1775
|
+
return this.baseDir;
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Stop all watchers and clean up.
|
|
1779
|
+
*/
|
|
1780
|
+
async stop() {
|
|
1781
|
+
const stopPromises = [];
|
|
1782
|
+
for (const [agentId, watcher] of this.watchers) {
|
|
1783
|
+
logger3.debug({ agentId }, "Stopping watcher");
|
|
1784
|
+
stopPromises.push(watcher.stop());
|
|
1785
|
+
}
|
|
1786
|
+
await Promise.all(stopPromises);
|
|
1787
|
+
this.watchers.clear();
|
|
1788
|
+
logger3.info("Message broker stopped");
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
// src/orchestrator/orchestrator.ts
|
|
1793
|
+
import { EventEmitter as EventEmitter7 } from "eventemitter3";
|
|
1794
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
1795
|
+
|
|
1796
|
+
// src/feedback/feedback-loop.ts
|
|
1797
|
+
import { EventEmitter as EventEmitter6 } from "eventemitter3";
|
|
1798
|
+
|
|
1799
|
+
// src/feedback/progress-tracker.ts
|
|
1800
|
+
init_id();
|
|
1801
|
+
var PHASE_ORDER = ["planning", "architecture", "design", "implementation", "testing", "documentation"];
|
|
1802
|
+
var ProgressTracker = class {
|
|
1803
|
+
generateReport(tasks, stats) {
|
|
1804
|
+
const completedTasks = tasks.filter((t) => t.status === "done");
|
|
1805
|
+
const pendingTasks = tasks.filter((t) => t.status === "pending");
|
|
1806
|
+
const blockedTasks = tasks.filter((t) => t.status === "blocked");
|
|
1807
|
+
return {
|
|
1808
|
+
id: generateId(),
|
|
1809
|
+
phase: this.detectPhase(tasks),
|
|
1810
|
+
completedTasks: completedTasks.length,
|
|
1811
|
+
totalTasks: tasks.length,
|
|
1812
|
+
percentComplete: this.getCompletionPercentage(tasks),
|
|
1813
|
+
recentCompletions: completedTasks.map((t) => t.title),
|
|
1814
|
+
upcomingWork: pendingTasks.map((t) => t.title),
|
|
1815
|
+
blockers: blockedTasks.map((t) => t.title),
|
|
1816
|
+
totalCostUsd: stats.totalCostUsd,
|
|
1817
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
getCompletionPercentage(tasks) {
|
|
1821
|
+
if (tasks.length === 0) return 0;
|
|
1822
|
+
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
1823
|
+
return Math.round(doneCount / tasks.length * 100);
|
|
1824
|
+
}
|
|
1825
|
+
detectPhase(tasks) {
|
|
1826
|
+
const inProgressTasks = tasks.filter((t) => t.status === "in-progress");
|
|
1827
|
+
if (inProgressTasks.length === 0) {
|
|
1828
|
+
return "implementation";
|
|
1829
|
+
}
|
|
1830
|
+
const tagCounts = /* @__PURE__ */ new Map();
|
|
1831
|
+
for (const task of inProgressTasks) {
|
|
1832
|
+
for (const tag of task.tags) {
|
|
1833
|
+
const normalizedTag = tag.toLowerCase();
|
|
1834
|
+
if (PHASE_ORDER.includes(normalizedTag)) {
|
|
1835
|
+
tagCounts.set(normalizedTag, (tagCounts.get(normalizedTag) ?? 0) + 1);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
if (tagCounts.size === 0) {
|
|
1840
|
+
return "implementation";
|
|
1841
|
+
}
|
|
1842
|
+
let maxCount = 0;
|
|
1843
|
+
let mostCommonPhase = "implementation";
|
|
1844
|
+
for (const [tag, count] of tagCounts) {
|
|
1845
|
+
if (count > maxCount) {
|
|
1846
|
+
maxCount = count;
|
|
1847
|
+
mostCommonPhase = tag;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return mostCommonPhase;
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1854
|
+
// src/feedback/feedback-loop.ts
|
|
1855
|
+
init_id();
|
|
1856
|
+
init_logger();
|
|
1857
|
+
var logger4 = createLogger("feedback-loop");
|
|
1858
|
+
var DEFAULT_SETTINGS = {
|
|
1859
|
+
interactionMode: "supervised",
|
|
1860
|
+
progressReportIntervalMs: 6e4,
|
|
1861
|
+
milestonePercentages: [25, 50, 75, 100],
|
|
1862
|
+
questionTimeoutMs: 3e5,
|
|
1863
|
+
requirePlanApproval: true
|
|
1864
|
+
};
|
|
1865
|
+
var FeedbackLoop = class extends EventEmitter6 {
|
|
1866
|
+
mode;
|
|
1867
|
+
settings;
|
|
1868
|
+
pendingPlan = null;
|
|
1869
|
+
planResolver = null;
|
|
1870
|
+
pendingQuestions = /* @__PURE__ */ new Map();
|
|
1871
|
+
lastProgressReport = 0;
|
|
1872
|
+
progressTracker;
|
|
1873
|
+
reachedMilestones = /* @__PURE__ */ new Set();
|
|
1874
|
+
pendingMilestone = null;
|
|
1875
|
+
constructor(settings) {
|
|
1876
|
+
super();
|
|
1877
|
+
this.settings = { ...DEFAULT_SETTINGS, ...settings };
|
|
1878
|
+
this.mode = this.settings.interactionMode;
|
|
1879
|
+
this.progressTracker = new ProgressTracker();
|
|
1880
|
+
}
|
|
1881
|
+
get interactionMode() {
|
|
1882
|
+
return this.mode;
|
|
1883
|
+
}
|
|
1884
|
+
setMode(mode) {
|
|
1885
|
+
this.mode = mode;
|
|
1886
|
+
this.emit("interaction-mode:changed", mode);
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Propose a plan for approval.
|
|
1890
|
+
* In unattended mode: auto-approves immediately.
|
|
1891
|
+
* In supervised/interactive: emits plan:proposed and waits for decidePlan() call.
|
|
1892
|
+
*/
|
|
1893
|
+
async proposePlan(tasks, projectGoal) {
|
|
1894
|
+
if (this.mode === "unattended" && !this.settings.requirePlanApproval) {
|
|
1895
|
+
return "approved";
|
|
1896
|
+
}
|
|
1897
|
+
if (this.mode === "unattended") {
|
|
1898
|
+
return "approved";
|
|
1899
|
+
}
|
|
1900
|
+
const proposal = {
|
|
1901
|
+
id: generateId(),
|
|
1902
|
+
tasks,
|
|
1903
|
+
totalEstimatedTasks: tasks.length,
|
|
1904
|
+
projectGoal,
|
|
1905
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1906
|
+
status: "pending"
|
|
1907
|
+
};
|
|
1908
|
+
this.pendingPlan = proposal;
|
|
1909
|
+
return new Promise((resolve) => {
|
|
1910
|
+
this.planResolver = resolve;
|
|
1911
|
+
this.emit("plan:proposed", proposal);
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
decidePlan(proposalId, decision, modifications) {
|
|
1915
|
+
if (!this.pendingPlan || this.pendingPlan.id !== proposalId) {
|
|
1916
|
+
logger4.warn({ proposalId }, "No pending plan with this ID");
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
this.pendingPlan.status = decision === "approved" ? "approved" : "rejected";
|
|
1920
|
+
this.emit("plan:decided", proposalId, decision, modifications);
|
|
1921
|
+
if (this.planResolver) {
|
|
1922
|
+
this.planResolver(decision);
|
|
1923
|
+
this.planResolver = null;
|
|
1924
|
+
}
|
|
1925
|
+
this.pendingPlan = null;
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Route a question from an agent to the user.
|
|
1929
|
+
* In unattended mode: returns null immediately (agent should handle itself).
|
|
1930
|
+
* In interactive: emits question:asked and waits for answerQuestion() call with timeout.
|
|
1931
|
+
* In supervised: same as interactive but question is lower priority.
|
|
1932
|
+
*/
|
|
1933
|
+
async routeQuestion(fromAgent, question, opts) {
|
|
1934
|
+
if (this.mode === "unattended") {
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
const userQuestion = {
|
|
1938
|
+
id: generateId(),
|
|
1939
|
+
fromAgent,
|
|
1940
|
+
taskId: opts?.taskId,
|
|
1941
|
+
question,
|
|
1942
|
+
context: opts?.context,
|
|
1943
|
+
options: opts?.options,
|
|
1944
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1945
|
+
status: "pending"
|
|
1946
|
+
};
|
|
1947
|
+
this.emit("question:asked", userQuestion);
|
|
1948
|
+
return new Promise((resolve) => {
|
|
1949
|
+
const timer = setTimeout(() => {
|
|
1950
|
+
userQuestion.status = "timed-out";
|
|
1951
|
+
this.pendingQuestions.delete(userQuestion.id);
|
|
1952
|
+
resolve(null);
|
|
1953
|
+
}, this.settings.questionTimeoutMs);
|
|
1954
|
+
this.pendingQuestions.set(userQuestion.id, { question: userQuestion, resolve, timer });
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
answerQuestion(questionId, answer) {
|
|
1958
|
+
const entry = this.pendingQuestions.get(questionId);
|
|
1959
|
+
if (!entry) {
|
|
1960
|
+
logger4.warn({ questionId }, "No pending question with this ID");
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
entry.question.status = "answered";
|
|
1964
|
+
entry.question.answer = answer;
|
|
1965
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
1966
|
+
this.pendingQuestions.delete(questionId);
|
|
1967
|
+
this.emit("question:answered", questionId, answer);
|
|
1968
|
+
entry.resolve(answer);
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Called each orchestrator cycle to check progress and emit reports/milestones.
|
|
1972
|
+
*/
|
|
1973
|
+
checkProgress(tasks, stats) {
|
|
1974
|
+
const now = Date.now();
|
|
1975
|
+
if (now - this.lastProgressReport >= this.settings.progressReportIntervalMs) {
|
|
1976
|
+
const report = this.progressTracker.generateReport(tasks, stats);
|
|
1977
|
+
this.emit("progress:report", report);
|
|
1978
|
+
this.lastProgressReport = now;
|
|
1979
|
+
}
|
|
1980
|
+
const percent = this.progressTracker.getCompletionPercentage(tasks);
|
|
1981
|
+
for (const threshold of this.settings.milestonePercentages) {
|
|
1982
|
+
if (percent >= threshold && !this.reachedMilestones.has(threshold)) {
|
|
1983
|
+
this.reachedMilestones.add(threshold);
|
|
1984
|
+
const milestone = {
|
|
1985
|
+
id: generateId(),
|
|
1986
|
+
name: `${threshold}% Complete`,
|
|
1987
|
+
percentComplete: threshold,
|
|
1988
|
+
message: `Project has reached ${threshold}% completion (${stats.completedTasks}/${stats.totalTasks} tasks done)`,
|
|
1989
|
+
requiresAck: this.mode === "supervised" || this.mode === "interactive",
|
|
1990
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1991
|
+
};
|
|
1992
|
+
this.emit("milestone:reached", milestone);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
acknowledgeMilestone(milestoneId) {
|
|
1997
|
+
this.emit("milestone:acknowledged", milestoneId);
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Clean up all pending timers and resolvers.
|
|
2001
|
+
* Call this during shutdown to prevent leaked timers from blocking process exit.
|
|
2002
|
+
*/
|
|
2003
|
+
cleanup() {
|
|
2004
|
+
for (const [id, entry] of this.pendingQuestions) {
|
|
2005
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
2006
|
+
entry.resolve(null);
|
|
2007
|
+
}
|
|
2008
|
+
this.pendingQuestions.clear();
|
|
2009
|
+
if (this.planResolver) {
|
|
2010
|
+
this.planResolver("rejected");
|
|
2011
|
+
this.planResolver = null;
|
|
2012
|
+
}
|
|
2013
|
+
this.pendingPlan = null;
|
|
2014
|
+
this.removeAllListeners();
|
|
2015
|
+
logger4.debug("FeedbackLoop cleaned up");
|
|
2016
|
+
}
|
|
2017
|
+
getPendingPlan() {
|
|
2018
|
+
return this.pendingPlan;
|
|
2019
|
+
}
|
|
2020
|
+
getPendingQuestions() {
|
|
2021
|
+
return Array.from(this.pendingQuestions.values()).map((e) => e.question);
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
|
|
2025
|
+
// src/orchestrator/planner.ts
|
|
2026
|
+
init_logger();
|
|
2027
|
+
import { execa as execa2 } from "execa";
|
|
2028
|
+
import { readdirSync, statSync } from "fs";
|
|
2029
|
+
import { join as join3 } from "path";
|
|
2030
|
+
var logger5 = createLogger("planner");
|
|
2031
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
2032
|
+
".git",
|
|
2033
|
+
"node_modules",
|
|
2034
|
+
".maestro",
|
|
2035
|
+
"dist",
|
|
2036
|
+
"build",
|
|
2037
|
+
"out",
|
|
2038
|
+
".next",
|
|
2039
|
+
".nuxt",
|
|
2040
|
+
".output",
|
|
2041
|
+
".cache",
|
|
2042
|
+
".turbo",
|
|
2043
|
+
"coverage",
|
|
2044
|
+
"__pycache__",
|
|
2045
|
+
".venv",
|
|
2046
|
+
"venv",
|
|
2047
|
+
".tox",
|
|
2048
|
+
"target"
|
|
2049
|
+
]);
|
|
2050
|
+
var IGNORE_FILES = /* @__PURE__ */ new Set([
|
|
2051
|
+
".DS_Store",
|
|
2052
|
+
"Thumbs.db",
|
|
2053
|
+
".env",
|
|
2054
|
+
".env.local"
|
|
2055
|
+
]);
|
|
2056
|
+
var DECOMPOSE_PROMPT = `You are a project planning assistant. Break down the following project goal into a set of discrete, actionable tasks.
|
|
2057
|
+
|
|
2058
|
+
Return ONLY a JSON array (no markdown, no explanation) where each element has:
|
|
2059
|
+
- "title": short task title
|
|
2060
|
+
- "priority": one of "critical", "high", "medium", "low"
|
|
2061
|
+
- "description": detailed description of what needs to be done
|
|
2062
|
+
- "dependencies": array of titles of tasks this depends on (empty array if none)
|
|
2063
|
+
- "tags": array of relevant tags from: "architecture", "design-system", "ui", "ux", "design", "planning", "priority", "frontend", "backend", "testing", "qa", "e2e", "documentation", "docs", "api-docs", "devops", "ci-cd", "deployment", "docker", "infrastructure", "security", "review", "code-review"
|
|
2064
|
+
|
|
2065
|
+
Order tasks so that dependencies come before the tasks that depend on them.
|
|
2066
|
+
|
|
2067
|
+
IMPORTANT \u2014 You MUST include tasks for ALL of the following categories. A complete project requires work from every team role:
|
|
2068
|
+
|
|
2069
|
+
1. **Architecture** (tag: "architecture"): At least one task for creating ARCHITECTURE.md with system design, component structure, data models, and key technical decisions. This should have no dependencies and be listed as a dependency for implementation tasks.
|
|
2070
|
+
|
|
2071
|
+
2. **Design** (tags: "design", "ui", "ux", "design-system"): Tasks for UI/UX design, component design, design system setup, styling, and visual layouts.
|
|
2072
|
+
|
|
2073
|
+
3. **Frontend/Backend development** (tags: "frontend", "backend"): Core implementation tasks for building features.
|
|
2074
|
+
|
|
2075
|
+
4. **Testing & QA** (tags: "testing", "qa", "e2e"): Tasks for writing unit tests, integration tests, and end-to-end tests. These should depend on the implementation tasks they test. Include at least one task for setting up the test framework and one for writing tests for each major feature.
|
|
2076
|
+
|
|
2077
|
+
5. **Documentation** (tags: "documentation", "docs", "api-docs"): Tasks for writing README, API documentation, user guides, and inline documentation. These should depend on the features being documented.
|
|
2078
|
+
|
|
2079
|
+
6. **DevOps & CI/CD** (tags: "devops", "ci-cd", "deployment", "docker", "infrastructure"): Tasks for setting up CI/CD pipelines, Docker configuration, deployment scripts, and infrastructure. Include at least one devops task.
|
|
2080
|
+
|
|
2081
|
+
7. **Code Review** (tags: "code-review", "review", "security"): Tasks for reviewing code quality, security audit, and ensuring best practices. These should depend on implementation tasks and run toward the end.
|
|
2082
|
+
|
|
2083
|
+
Each category must have at least one task. This ensures all team members (architect, designer, developers, QA engineer, technical writer, DevOps engineer, code reviewer) have meaningful work assigned to them.
|
|
2084
|
+
`;
|
|
2085
|
+
var Planner = class {
|
|
2086
|
+
model;
|
|
2087
|
+
workingDirectory;
|
|
2088
|
+
claudeCommand;
|
|
2089
|
+
claudeEnv;
|
|
2090
|
+
techStack;
|
|
2091
|
+
constructor(opts) {
|
|
2092
|
+
this.model = opts.model ?? "sonnet";
|
|
2093
|
+
this.workingDirectory = opts.workingDirectory;
|
|
2094
|
+
this.claudeCommand = opts.claudeCommand ?? "claude";
|
|
2095
|
+
this.claudeEnv = opts.claudeEnv;
|
|
2096
|
+
this.techStack = opts.techStack;
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Use Claude CLI to decompose a goal into a list of planned tasks.
|
|
2100
|
+
*/
|
|
2101
|
+
async decompose(goal) {
|
|
2102
|
+
const tree = this.scanDirectory(this.workingDirectory, 4);
|
|
2103
|
+
const hasFiles = tree.length > 0;
|
|
2104
|
+
let prompt = DECOMPOSE_PROMPT;
|
|
2105
|
+
if (hasFiles) {
|
|
2106
|
+
prompt += `
|
|
2107
|
+
Project working directory: ${this.workingDirectory}
|
|
2108
|
+
`;
|
|
2109
|
+
prompt += `
|
|
2110
|
+
Existing project structure:
|
|
2111
|
+
\`\`\`
|
|
2112
|
+
${tree.join("\n")}
|
|
2113
|
+
\`\`\`
|
|
2114
|
+
`;
|
|
2115
|
+
prompt += `
|
|
2116
|
+
Consider the existing files and structure when planning tasks. `;
|
|
2117
|
+
prompt += `Build on what already exists rather than recreating from scratch.
|
|
2118
|
+
`;
|
|
2119
|
+
}
|
|
2120
|
+
if (this.techStack) {
|
|
2121
|
+
const parts = [];
|
|
2122
|
+
if (this.techStack.frontend) parts.push(`- Frontend: ${this.techStack.frontend}`);
|
|
2123
|
+
if (this.techStack.uiLibrary) parts.push(`- UI Library: ${this.techStack.uiLibrary}`);
|
|
2124
|
+
if (this.techStack.backend) parts.push(`- Backend: ${this.techStack.backend}`);
|
|
2125
|
+
if (this.techStack.database) parts.push(`- Database: ${this.techStack.database}`);
|
|
2126
|
+
if (this.techStack.other) parts.push(`- Other: ${this.techStack.other}`);
|
|
2127
|
+
if (parts.length > 0) {
|
|
2128
|
+
prompt += `
|
|
2129
|
+
Tech Stack:
|
|
2130
|
+
${parts.join("\n")}
|
|
2131
|
+
`;
|
|
2132
|
+
prompt += `
|
|
2133
|
+
Use the specified technologies in your task planning. `;
|
|
2134
|
+
prompt += `Include setup and configuration tasks for these technologies as needed.
|
|
2135
|
+
`;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
prompt += `
|
|
2139
|
+
Goal:
|
|
2140
|
+
${goal}`;
|
|
2141
|
+
logger5.info({ model: this.model, cwd: this.workingDirectory, fileCount: tree.length }, "Decomposing goal into tasks");
|
|
2142
|
+
try {
|
|
2143
|
+
const result = await execa2(this.claudeCommand, [
|
|
2144
|
+
"--print",
|
|
2145
|
+
"--output-format",
|
|
2146
|
+
"json",
|
|
2147
|
+
"--model",
|
|
2148
|
+
this.model
|
|
2149
|
+
], {
|
|
2150
|
+
input: prompt,
|
|
2151
|
+
cwd: this.workingDirectory,
|
|
2152
|
+
env: this.claudeEnv ? { ...process.env, ...this.claudeEnv } : void 0,
|
|
2153
|
+
timeout: 12e4
|
|
2154
|
+
});
|
|
2155
|
+
const output = result.stdout.trim();
|
|
2156
|
+
return this.parseResponse(output);
|
|
2157
|
+
} catch (err) {
|
|
2158
|
+
logger5.error({ err }, "Failed to decompose goal via Claude CLI");
|
|
2159
|
+
return [{
|
|
2160
|
+
title: "Implement project goal",
|
|
2161
|
+
priority: "high",
|
|
2162
|
+
description: goal,
|
|
2163
|
+
dependencies: [],
|
|
2164
|
+
tags: []
|
|
2165
|
+
}];
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Scan a directory recursively and return an indented tree listing.
|
|
2170
|
+
*/
|
|
2171
|
+
scanDirectory(dir, maxDepth, depth = 0, prefix = "") {
|
|
2172
|
+
if (depth >= maxDepth) return [];
|
|
2173
|
+
const lines = [];
|
|
2174
|
+
let entries;
|
|
2175
|
+
try {
|
|
2176
|
+
entries = readdirSync(dir).sort();
|
|
2177
|
+
} catch {
|
|
2178
|
+
return lines;
|
|
2179
|
+
}
|
|
2180
|
+
const dirs = [];
|
|
2181
|
+
const files = [];
|
|
2182
|
+
for (const entry of entries) {
|
|
2183
|
+
if (IGNORE_DIRS.has(entry) || IGNORE_FILES.has(entry) || entry.startsWith(".")) continue;
|
|
2184
|
+
const fullPath = join3(dir, entry);
|
|
2185
|
+
try {
|
|
2186
|
+
const stat = statSync(fullPath);
|
|
2187
|
+
if (stat.isDirectory()) {
|
|
2188
|
+
dirs.push(entry);
|
|
2189
|
+
} else {
|
|
2190
|
+
files.push(entry);
|
|
2191
|
+
}
|
|
2192
|
+
} catch {
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
for (const file of files) {
|
|
2196
|
+
lines.push(`${prefix}${file}`);
|
|
2197
|
+
}
|
|
2198
|
+
for (const d of dirs) {
|
|
2199
|
+
lines.push(`${prefix}${d}/`);
|
|
2200
|
+
const subLines = this.scanDirectory(join3(dir, d), maxDepth, depth + 1, prefix + " ");
|
|
2201
|
+
lines.push(...subLines);
|
|
2202
|
+
}
|
|
2203
|
+
return lines;
|
|
2204
|
+
}
|
|
2205
|
+
parseResponse(output) {
|
|
2206
|
+
try {
|
|
2207
|
+
let text = output;
|
|
2208
|
+
try {
|
|
2209
|
+
const parsed = JSON.parse(output);
|
|
2210
|
+
if (parsed && typeof parsed === "object" && "result" in parsed) {
|
|
2211
|
+
text = parsed.result;
|
|
2212
|
+
}
|
|
2213
|
+
} catch {
|
|
2214
|
+
}
|
|
2215
|
+
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
2216
|
+
if (!jsonMatch) {
|
|
2217
|
+
throw new Error("No JSON array found in response");
|
|
2218
|
+
}
|
|
2219
|
+
const tasks = JSON.parse(jsonMatch[0]);
|
|
2220
|
+
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
2221
|
+
throw new Error("Parsed result is not a non-empty array");
|
|
2222
|
+
}
|
|
2223
|
+
for (const task of tasks) {
|
|
2224
|
+
if (!task.title || !task.description) {
|
|
2225
|
+
throw new Error(`Task missing required fields: ${JSON.stringify(task)}`);
|
|
2226
|
+
}
|
|
2227
|
+
task.priority = task.priority ?? "medium";
|
|
2228
|
+
task.dependencies = task.dependencies ?? [];
|
|
2229
|
+
task.tags = task.tags ?? [];
|
|
2230
|
+
}
|
|
2231
|
+
logger5.info({ taskCount: tasks.length }, "Goal decomposed into tasks");
|
|
2232
|
+
return tasks;
|
|
2233
|
+
} catch (err) {
|
|
2234
|
+
logger5.warn({ err }, "Failed to parse planner response, returning single task");
|
|
2235
|
+
return [{
|
|
2236
|
+
title: "Implement project goal",
|
|
2237
|
+
priority: "high",
|
|
2238
|
+
description: output,
|
|
2239
|
+
dependencies: [],
|
|
2240
|
+
tags: []
|
|
2241
|
+
}];
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
|
|
2246
|
+
// src/orchestrator/scheduler.ts
|
|
2247
|
+
init_logger();
|
|
2248
|
+
var logger6 = createLogger("scheduler");
|
|
2249
|
+
var TAG_ROLE_MAP = {
|
|
2250
|
+
"architecture": "architect",
|
|
2251
|
+
"design-system": "architect",
|
|
2252
|
+
"ui": "designer",
|
|
2253
|
+
"ux": "designer",
|
|
2254
|
+
"design": "designer",
|
|
2255
|
+
"planning": "project-manager",
|
|
2256
|
+
"priority": "project-manager",
|
|
2257
|
+
"testing": "qa-engineer",
|
|
2258
|
+
"qa": "qa-engineer",
|
|
2259
|
+
"e2e": "qa-engineer",
|
|
2260
|
+
"devops": "devops",
|
|
2261
|
+
"ci-cd": "devops",
|
|
2262
|
+
"deployment": "devops",
|
|
2263
|
+
"infrastructure": "devops",
|
|
2264
|
+
"docker": "devops",
|
|
2265
|
+
"documentation": "technical-writer",
|
|
2266
|
+
"docs": "technical-writer",
|
|
2267
|
+
"api-docs": "technical-writer",
|
|
2268
|
+
"review": "code-reviewer",
|
|
2269
|
+
"code-review": "code-reviewer",
|
|
2270
|
+
"security": "code-reviewer"
|
|
2271
|
+
};
|
|
2272
|
+
var ROLE_FALLBACK = {
|
|
2273
|
+
"designer": ["developer"],
|
|
2274
|
+
"architect": ["developer"],
|
|
2275
|
+
"project-manager": [],
|
|
2276
|
+
"qa-engineer": ["developer"],
|
|
2277
|
+
"devops": ["developer"],
|
|
2278
|
+
"technical-writer": ["developer"],
|
|
2279
|
+
"code-reviewer": ["architect", "developer"]
|
|
2280
|
+
};
|
|
2281
|
+
var PRIORITY_ORDER2 = {
|
|
2282
|
+
critical: 0,
|
|
2283
|
+
high: 1,
|
|
2284
|
+
medium: 2,
|
|
2285
|
+
low: 3
|
|
2286
|
+
};
|
|
2287
|
+
var Scheduler = class {
|
|
2288
|
+
/** Optional pipeline reference for pipeline-aware scheduling. */
|
|
2289
|
+
pipeline = null;
|
|
2290
|
+
/**
|
|
2291
|
+
* Set a pipeline reference so the scheduler can use pipeline stage
|
|
2292
|
+
* information when determining which role should handle a task.
|
|
2293
|
+
*/
|
|
2294
|
+
setPipeline(pipeline) {
|
|
2295
|
+
this.pipeline = pipeline;
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Determine which pending tasks should be assigned to which idle agents.
|
|
2299
|
+
*
|
|
2300
|
+
* Rules:
|
|
2301
|
+
* 1. Only consider tasks whose status is 'pending' and whose dependencies are all 'done'.
|
|
2302
|
+
* 2. Only consider agents whose status is 'idle'.
|
|
2303
|
+
* 3. Match tasks to agents by role:
|
|
2304
|
+
* - If a task has a pipeline state, use the pipeline stage to determine role
|
|
2305
|
+
* - Otherwise, fall back to tag-based role determination
|
|
2306
|
+
* 4. Assign higher-priority tasks first.
|
|
2307
|
+
* 5. Each agent receives at most one assignment per scheduling cycle.
|
|
2308
|
+
*/
|
|
2309
|
+
schedule(tasks, agents) {
|
|
2310
|
+
const taskStatusMap = /* @__PURE__ */ new Map();
|
|
2311
|
+
for (const task of tasks) {
|
|
2312
|
+
taskStatusMap.set(task.id, task);
|
|
2313
|
+
}
|
|
2314
|
+
const readyTasks = tasks.filter((task) => {
|
|
2315
|
+
if (task.status !== "pending") return false;
|
|
2316
|
+
return task.dependencies.every((depId) => {
|
|
2317
|
+
const dep = taskStatusMap.get(depId);
|
|
2318
|
+
return !dep || dep.status === "done";
|
|
2319
|
+
});
|
|
2320
|
+
});
|
|
2321
|
+
readyTasks.sort((a, b) => PRIORITY_ORDER2[a.priority] - PRIORITY_ORDER2[b.priority]);
|
|
2322
|
+
const idleAgentsByRole = /* @__PURE__ */ new Map();
|
|
2323
|
+
for (const agent of agents) {
|
|
2324
|
+
if (agent.status !== "idle") continue;
|
|
2325
|
+
const role = agent.config.role;
|
|
2326
|
+
if (!idleAgentsByRole.has(role)) {
|
|
2327
|
+
idleAgentsByRole.set(role, []);
|
|
2328
|
+
}
|
|
2329
|
+
idleAgentsByRole.get(role).push(agent);
|
|
2330
|
+
}
|
|
2331
|
+
const assignments = [];
|
|
2332
|
+
const assignedAgentIds = /* @__PURE__ */ new Set();
|
|
2333
|
+
for (const task of readyTasks) {
|
|
2334
|
+
const neededRole = this.determineRole(task);
|
|
2335
|
+
const rolesToTry = [neededRole, ...ROLE_FALLBACK[neededRole] ?? []];
|
|
2336
|
+
let assigned = false;
|
|
2337
|
+
for (const role of rolesToTry) {
|
|
2338
|
+
const candidates = idleAgentsByRole.get(role);
|
|
2339
|
+
if (!candidates || candidates.length === 0) {
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
const agent = candidates.find((a) => !assignedAgentIds.has(a.config.id));
|
|
2343
|
+
if (!agent) {
|
|
2344
|
+
continue;
|
|
2345
|
+
}
|
|
2346
|
+
assignedAgentIds.add(agent.config.id);
|
|
2347
|
+
assignments.push({
|
|
2348
|
+
taskId: task.id,
|
|
2349
|
+
agentId: agent.config.id
|
|
2350
|
+
});
|
|
2351
|
+
logger6.debug(
|
|
2352
|
+
{ taskId: task.id, agentId: agent.config.id, neededRole, actualRole: role },
|
|
2353
|
+
role === neededRole ? "Task assigned to agent" : "Task assigned to fallback agent"
|
|
2354
|
+
);
|
|
2355
|
+
assigned = true;
|
|
2356
|
+
break;
|
|
2357
|
+
}
|
|
2358
|
+
if (!assigned) {
|
|
2359
|
+
logger6.trace(
|
|
2360
|
+
{ taskId: task.id, neededRole },
|
|
2361
|
+
"No idle agent available for role, will retry next cycle"
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return assignments;
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Determine the best role for a task.
|
|
2369
|
+
*
|
|
2370
|
+
* If the task has an active pipeline state, the pipeline stage takes
|
|
2371
|
+
* priority over tag-based role determination. This enables the
|
|
2372
|
+
* plan -> develop -> qa -> review handoff sequence.
|
|
2373
|
+
*
|
|
2374
|
+
* Falls back to tag-based determination, then defaults to 'developer'.
|
|
2375
|
+
*/
|
|
2376
|
+
determineRole(task) {
|
|
2377
|
+
if (this.pipeline) {
|
|
2378
|
+
const pipelineState = this.pipeline.getState(task.id);
|
|
2379
|
+
if (pipelineState) {
|
|
2380
|
+
const stageRoles = PIPELINE_STAGE_ROLES[pipelineState.currentStage];
|
|
2381
|
+
if (stageRoles && stageRoles.length > 0) {
|
|
2382
|
+
return stageRoles[0];
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
for (const tag of task.tags) {
|
|
2387
|
+
const role = TAG_ROLE_MAP[tag];
|
|
2388
|
+
if (role) {
|
|
2389
|
+
return role;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
return "developer";
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
|
|
2396
|
+
// src/orchestrator/pipeline.ts
|
|
2397
|
+
init_logger();
|
|
2398
|
+
var logger7 = createLogger("pipeline");
|
|
2399
|
+
var TaskPipeline = class {
|
|
2400
|
+
pipelines = /* @__PURE__ */ new Map();
|
|
2401
|
+
/**
|
|
2402
|
+
* Register a task in the pipeline at a given starting stage.
|
|
2403
|
+
* Tasks tagged with 'architecture' or 'planning' start at 'plan'.
|
|
2404
|
+
* Tasks tagged with 'testing' or 'qa' start at 'qa'.
|
|
2405
|
+
* All others start at 'develop' (they'll skip the plan stage
|
|
2406
|
+
* if they were already created from the planner).
|
|
2407
|
+
*/
|
|
2408
|
+
initTask(task, startStage) {
|
|
2409
|
+
const stage = startStage ?? this.inferStartStage(task);
|
|
2410
|
+
const state = {
|
|
2411
|
+
taskId: task.id,
|
|
2412
|
+
currentStage: stage,
|
|
2413
|
+
stageHistory: [],
|
|
2414
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2415
|
+
};
|
|
2416
|
+
this.pipelines.set(task.id, state);
|
|
2417
|
+
logger7.debug({ taskId: task.id, stage }, "Task registered in pipeline");
|
|
2418
|
+
return state;
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Get the pipeline state for a task.
|
|
2422
|
+
*/
|
|
2423
|
+
getState(taskId) {
|
|
2424
|
+
return this.pipelines.get(taskId);
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* Record that an agent has started working on the current stage.
|
|
2428
|
+
*/
|
|
2429
|
+
startStage(taskId, agentId) {
|
|
2430
|
+
const state = this.pipelines.get(taskId);
|
|
2431
|
+
if (!state) return;
|
|
2432
|
+
state.stageHistory.push({
|
|
2433
|
+
stage: state.currentStage,
|
|
2434
|
+
agentId,
|
|
2435
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2436
|
+
completedAt: null,
|
|
2437
|
+
status: "in-progress"
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Complete the current stage and return the next stage (if any).
|
|
2442
|
+
* Returns null if the pipeline is finished.
|
|
2443
|
+
*/
|
|
2444
|
+
completeStage(taskId) {
|
|
2445
|
+
const state = this.pipelines.get(taskId);
|
|
2446
|
+
if (!state) return null;
|
|
2447
|
+
const currentEntry = state.stageHistory.find(
|
|
2448
|
+
(h) => h.stage === state.currentStage && h.status === "in-progress"
|
|
2449
|
+
);
|
|
2450
|
+
if (currentEntry) {
|
|
2451
|
+
currentEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2452
|
+
currentEntry.status = "completed";
|
|
2453
|
+
}
|
|
2454
|
+
const currentIndex = PIPELINE_STAGE_ORDER.indexOf(state.currentStage);
|
|
2455
|
+
if (currentIndex < 0 || currentIndex >= PIPELINE_STAGE_ORDER.length - 1) {
|
|
2456
|
+
logger7.info({ taskId }, "Task pipeline completed");
|
|
2457
|
+
return {
|
|
2458
|
+
nextStage: null,
|
|
2459
|
+
requiredRoles: [],
|
|
2460
|
+
handoffMessage: `Pipeline complete for task ${taskId}`
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
const nextStage = PIPELINE_STAGE_ORDER[currentIndex + 1];
|
|
2464
|
+
state.currentStage = nextStage;
|
|
2465
|
+
const requiredRoles = PIPELINE_STAGE_ROLES[nextStage];
|
|
2466
|
+
const stageLabels = {
|
|
2467
|
+
plan: "Planning & Architecture",
|
|
2468
|
+
develop: "Development",
|
|
2469
|
+
qa: "QA & Testing",
|
|
2470
|
+
review: "Project Manager Review"
|
|
2471
|
+
};
|
|
2472
|
+
logger7.info({ taskId, from: PIPELINE_STAGE_ORDER[currentIndex], to: nextStage }, "Pipeline handoff");
|
|
2473
|
+
return {
|
|
2474
|
+
nextStage,
|
|
2475
|
+
requiredRoles,
|
|
2476
|
+
handoffMessage: `Handoff: ${stageLabels[PIPELINE_STAGE_ORDER[currentIndex]]} -> ${stageLabels[nextStage]}`
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Mark a stage as failed.
|
|
2481
|
+
*/
|
|
2482
|
+
failStage(taskId) {
|
|
2483
|
+
const state = this.pipelines.get(taskId);
|
|
2484
|
+
if (!state) return;
|
|
2485
|
+
const currentEntry = state.stageHistory.find(
|
|
2486
|
+
(h) => h.stage === state.currentStage && h.status === "in-progress"
|
|
2487
|
+
);
|
|
2488
|
+
if (currentEntry) {
|
|
2489
|
+
currentEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2490
|
+
currentEntry.status = "failed";
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Get the role(s) needed for the current pipeline stage of a task.
|
|
2495
|
+
*/
|
|
2496
|
+
getRequiredRoles(taskId) {
|
|
2497
|
+
const state = this.pipelines.get(taskId);
|
|
2498
|
+
if (!state) return ["developer"];
|
|
2499
|
+
return PIPELINE_STAGE_ROLES[state.currentStage];
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Check if a task has a pipeline registered.
|
|
2503
|
+
*/
|
|
2504
|
+
has(taskId) {
|
|
2505
|
+
return this.pipelines.has(taskId);
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Infer the starting pipeline stage from task tags.
|
|
2509
|
+
*/
|
|
2510
|
+
inferStartStage(task) {
|
|
2511
|
+
const tags = task.tags.map((t) => t.toLowerCase());
|
|
2512
|
+
if (tags.includes("architecture") || tags.includes("planning") || tags.includes("design-system")) {
|
|
2513
|
+
return "plan";
|
|
2514
|
+
}
|
|
2515
|
+
if (tags.includes("testing") || tags.includes("qa") || tags.includes("e2e")) {
|
|
2516
|
+
return "qa";
|
|
2517
|
+
}
|
|
2518
|
+
return "develop";
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2521
|
+
|
|
2522
|
+
// src/orchestrator/pm-scanner.ts
|
|
2523
|
+
init_id();
|
|
2524
|
+
init_logger();
|
|
2525
|
+
var logger8 = createLogger("pm-scanner");
|
|
2526
|
+
var DEFAULT_SCAN_INTERVAL_MS = 3e4;
|
|
2527
|
+
var STALE_TASK_THRESHOLD_MS = 5 * 6e4;
|
|
2528
|
+
var ProjectManagerScanner = class {
|
|
2529
|
+
intervalId = null;
|
|
2530
|
+
scanIntervalMs;
|
|
2531
|
+
lastScanTasks = /* @__PURE__ */ new Map();
|
|
2532
|
+
cycleCount = 0;
|
|
2533
|
+
/** Track tasks that have already received a PM message to avoid duplicates. */
|
|
2534
|
+
messagedTasks = /* @__PURE__ */ new Set();
|
|
2535
|
+
constructor(scanIntervalMs) {
|
|
2536
|
+
this.scanIntervalMs = scanIntervalMs ?? DEFAULT_SCAN_INTERVAL_MS;
|
|
2537
|
+
}
|
|
2538
|
+
/**
|
|
2539
|
+
* Start the periodic scan loop.
|
|
2540
|
+
* @param scanFn — called each interval; the orchestrator provides context.
|
|
2541
|
+
*/
|
|
2542
|
+
start(scanFn) {
|
|
2543
|
+
if (this.intervalId) return;
|
|
2544
|
+
this.intervalId = setInterval(scanFn, this.scanIntervalMs);
|
|
2545
|
+
logger8.info({ intervalMs: this.scanIntervalMs }, "PM scanner started");
|
|
2546
|
+
}
|
|
2547
|
+
stop() {
|
|
2548
|
+
if (this.intervalId) {
|
|
2549
|
+
clearInterval(this.intervalId);
|
|
2550
|
+
this.intervalId = null;
|
|
2551
|
+
}
|
|
2552
|
+
logger8.info("PM scanner stopped");
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Run a single scan pass.
|
|
2556
|
+
*
|
|
2557
|
+
* @param tasks - Current task list
|
|
2558
|
+
* @param agents - Current agent states (from pool + virtual)
|
|
2559
|
+
* @param phase - Current orchestrator phase
|
|
2560
|
+
*/
|
|
2561
|
+
scan(tasks, agents, phase) {
|
|
2562
|
+
this.cycleCount++;
|
|
2563
|
+
const now = Date.now();
|
|
2564
|
+
const findings = [];
|
|
2565
|
+
const blockedTasks = tasks.filter((t) => t.status === "blocked");
|
|
2566
|
+
for (const task of blockedTasks) {
|
|
2567
|
+
const blockerIds = task.dependencies.filter((depId) => {
|
|
2568
|
+
const dep = tasks.find((t) => t.id === depId);
|
|
2569
|
+
return dep && dep.status !== "done";
|
|
2570
|
+
});
|
|
2571
|
+
findings.push({
|
|
2572
|
+
kind: "blocked-task",
|
|
2573
|
+
severity: task.priority === "critical" ? "critical" : "warning",
|
|
2574
|
+
message: `Task "${task.title}" (${task.id}) is blocked by: ${blockerIds.join(", ")}`,
|
|
2575
|
+
taskId: task.id
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
const highPriorityPending = tasks.filter(
|
|
2579
|
+
(t) => t.status === "pending" && (t.priority === "critical" || t.priority === "high")
|
|
2580
|
+
);
|
|
2581
|
+
for (const task of highPriorityPending) {
|
|
2582
|
+
findings.push({
|
|
2583
|
+
kind: "high-priority-waiting",
|
|
2584
|
+
severity: task.priority === "critical" ? "critical" : "warning",
|
|
2585
|
+
message: `High-priority task "${task.title}" (${task.id}) is pending and unassigned`,
|
|
2586
|
+
taskId: task.id
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
const inProgressTasks = tasks.filter((t) => t.status === "in-progress");
|
|
2590
|
+
for (const task of inProgressTasks) {
|
|
2591
|
+
const prev = this.lastScanTasks.get(task.id);
|
|
2592
|
+
if (prev && prev.status === "in-progress") {
|
|
2593
|
+
const elapsed = now - prev.at;
|
|
2594
|
+
if (elapsed > STALE_TASK_THRESHOLD_MS) {
|
|
2595
|
+
findings.push({
|
|
2596
|
+
kind: "stale-task",
|
|
2597
|
+
severity: "warning",
|
|
2598
|
+
message: `Task "${task.title}" (${task.id}) has been in-progress for ${Math.round(elapsed / 6e4)}min without completion`,
|
|
2599
|
+
taskId: task.id,
|
|
2600
|
+
agentId: task.assignee ?? void 0
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
} else {
|
|
2604
|
+
this.lastScanTasks.set(task.id, { status: "in-progress", at: now });
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
for (const task of tasks) {
|
|
2608
|
+
if (task.status === "done" || task.status === "cancelled") {
|
|
2609
|
+
this.lastScanTasks.delete(task.id);
|
|
2610
|
+
this.clearTrackedTask(task.id);
|
|
2611
|
+
} else if (task.status !== "in-progress") {
|
|
2612
|
+
const prev = this.lastScanTasks.get(task.id);
|
|
2613
|
+
if (prev && prev.status === "in-progress") {
|
|
2614
|
+
this.lastScanTasks.delete(task.id);
|
|
2615
|
+
this.clearTrackedTask(task.id);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
const taskStatusMap = new Map(tasks.map((t) => [t.id, t]));
|
|
2620
|
+
const readyTasks = tasks.filter((t) => {
|
|
2621
|
+
if (t.status !== "pending") return false;
|
|
2622
|
+
return t.dependencies.every((depId) => {
|
|
2623
|
+
const dep = taskStatusMap.get(depId);
|
|
2624
|
+
return dep && dep.status === "done";
|
|
2625
|
+
});
|
|
2626
|
+
});
|
|
2627
|
+
const idleAgents = agents.filter((a) => a.status === "idle");
|
|
2628
|
+
if (readyTasks.length > 0 && idleAgents.length === 0) {
|
|
2629
|
+
findings.push({
|
|
2630
|
+
kind: "unassigned-ready-task",
|
|
2631
|
+
severity: "warning",
|
|
2632
|
+
message: `${readyTasks.length} task(s) ready but no idle agents available`
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
const erroredAgents = agents.filter((a) => a.status === "error");
|
|
2636
|
+
for (const agent of erroredAgents) {
|
|
2637
|
+
findings.push({
|
|
2638
|
+
kind: "agent-errored",
|
|
2639
|
+
severity: "warning",
|
|
2640
|
+
message: `Agent ${agent.config.name} (${agent.config.id}) is in error state: ${agent.error ?? "unknown"}`,
|
|
2641
|
+
agentId: agent.config.id
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
if (idleAgents.length > 0 && readyTasks.length > 0) {
|
|
2645
|
+
findings.push({
|
|
2646
|
+
kind: "agent-idle",
|
|
2647
|
+
severity: "info",
|
|
2648
|
+
message: `${idleAgents.length} idle agent(s) with ${readyTasks.length} ready task(s) \u2014 scheduling may pick these up next cycle`
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
const blockingCounts = /* @__PURE__ */ new Map();
|
|
2652
|
+
for (const task of tasks) {
|
|
2653
|
+
if (task.status === "done" || task.status === "cancelled") continue;
|
|
2654
|
+
for (const depId of task.dependencies) {
|
|
2655
|
+
const dep = taskStatusMap.get(depId);
|
|
2656
|
+
if (dep && dep.status !== "done") {
|
|
2657
|
+
blockingCounts.set(depId, (blockingCounts.get(depId) ?? 0) + 1);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
for (const [taskId, count] of blockingCounts) {
|
|
2662
|
+
if (count >= 3) {
|
|
2663
|
+
const task = taskStatusMap.get(taskId);
|
|
2664
|
+
findings.push({
|
|
2665
|
+
kind: "dependency-bottleneck",
|
|
2666
|
+
severity: "critical",
|
|
2667
|
+
message: `Task "${task?.title ?? taskId}" is blocking ${count} other tasks \u2014 should be prioritized`,
|
|
2668
|
+
taskId
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
2673
|
+
const inProgressCount = inProgressTasks.length;
|
|
2674
|
+
const pendingCount = tasks.filter((t) => t.status === "pending").length;
|
|
2675
|
+
if (findings.length === 0) {
|
|
2676
|
+
findings.push({
|
|
2677
|
+
kind: "all-clear",
|
|
2678
|
+
severity: "info",
|
|
2679
|
+
message: "All systems nominal \u2014 no gaps or blockers detected"
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
const criticalCount = findings.filter((f) => f.severity === "critical").length;
|
|
2683
|
+
const warningCount = findings.filter((f) => f.severity === "warning").length;
|
|
2684
|
+
let summary;
|
|
2685
|
+
if (criticalCount > 0) {
|
|
2686
|
+
summary = `PM Scan: ${criticalCount} critical, ${warningCount} warning(s) | ${doneCount}/${tasks.length} done | ${inProgressCount} active | ${pendingCount} pending`;
|
|
2687
|
+
} else if (warningCount > 0) {
|
|
2688
|
+
summary = `PM Scan: ${warningCount} warning(s) | ${doneCount}/${tasks.length} done | ${inProgressCount} active | ${pendingCount} pending`;
|
|
2689
|
+
} else {
|
|
2690
|
+
summary = `PM Scan: All clear | ${doneCount}/${tasks.length} done | ${inProgressCount} active | ${pendingCount} pending`;
|
|
2691
|
+
}
|
|
2692
|
+
return {
|
|
2693
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2694
|
+
findings,
|
|
2695
|
+
summary
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
/**
|
|
2699
|
+
* Build messages from PM findings that should be sent to agents.
|
|
2700
|
+
* Only produces messages for actionable findings.
|
|
2701
|
+
* Tracks which tasks have already been messaged to avoid flooding inboxes.
|
|
2702
|
+
*/
|
|
2703
|
+
buildMessages(result) {
|
|
2704
|
+
const messages = [];
|
|
2705
|
+
for (const finding of result.findings) {
|
|
2706
|
+
if (finding.kind === "dependency-bottleneck" && finding.severity === "critical" && finding.taskId) {
|
|
2707
|
+
const key = `bottleneck:${finding.taskId}`;
|
|
2708
|
+
if (this.messagedTasks.has(key)) continue;
|
|
2709
|
+
this.messagedTasks.add(key);
|
|
2710
|
+
messages.push({
|
|
2711
|
+
id: generateId(),
|
|
2712
|
+
type: "directive",
|
|
2713
|
+
from: "project-manager",
|
|
2714
|
+
to: "orchestrator",
|
|
2715
|
+
subject: `Bottleneck: ${finding.message}`,
|
|
2716
|
+
body: `The project manager has identified a critical dependency bottleneck.
|
|
2717
|
+
|
|
2718
|
+
${finding.message}
|
|
2719
|
+
|
|
2720
|
+
Recommendation: Prioritize this task immediately to unblock downstream work.`,
|
|
2721
|
+
taskId: finding.taskId,
|
|
2722
|
+
priority: "urgent",
|
|
2723
|
+
timestamp: result.timestamp
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
if (finding.kind === "stale-task" && finding.agentId && finding.taskId) {
|
|
2727
|
+
const key = `stale:${finding.taskId}`;
|
|
2728
|
+
if (this.messagedTasks.has(key)) continue;
|
|
2729
|
+
this.messagedTasks.add(key);
|
|
2730
|
+
messages.push({
|
|
2731
|
+
id: generateId(),
|
|
2732
|
+
type: "system",
|
|
2733
|
+
from: "project-manager",
|
|
2734
|
+
to: finding.agentId,
|
|
2735
|
+
subject: `Status check: ${finding.taskId}`,
|
|
2736
|
+
body: `The project manager is checking on progress.
|
|
2737
|
+
|
|
2738
|
+
${finding.message}
|
|
2739
|
+
|
|
2740
|
+
Please provide a status update or flag any blockers you're experiencing.`,
|
|
2741
|
+
taskId: finding.taskId,
|
|
2742
|
+
priority: "normal",
|
|
2743
|
+
timestamp: result.timestamp
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
return messages;
|
|
2748
|
+
}
|
|
2749
|
+
/**
|
|
2750
|
+
* Clear tracked messages for a task (e.g., when it moves from in-progress
|
|
2751
|
+
* back to pending, allowing a new message if it stalls again later).
|
|
2752
|
+
*/
|
|
2753
|
+
clearTrackedTask(taskId) {
|
|
2754
|
+
for (const key of this.messagedTasks) {
|
|
2755
|
+
if (key.endsWith(`:${taskId}`)) {
|
|
2756
|
+
this.messagedTasks.delete(key);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
};
|
|
2761
|
+
|
|
2762
|
+
// src/orchestrator/orchestrator.ts
|
|
2763
|
+
init_id();
|
|
2764
|
+
init_logger();
|
|
2765
|
+
var logger9 = createLogger("orchestrator");
|
|
2766
|
+
var Orchestrator = class extends EventEmitter7 {
|
|
2767
|
+
config;
|
|
2768
|
+
running = false;
|
|
2769
|
+
paused = false;
|
|
2770
|
+
intervalId = null;
|
|
2771
|
+
startedAt = null;
|
|
2772
|
+
broker = null;
|
|
2773
|
+
scheduler = null;
|
|
2774
|
+
planner = null;
|
|
2775
|
+
pipeline;
|
|
2776
|
+
pmScanner;
|
|
2777
|
+
// These will hold references to modules that manage tasks and agents.
|
|
2778
|
+
// They are typed loosely here because TaskManager, AgentPool, and
|
|
2779
|
+
// WorkspaceManager are sibling modules that may not yet be implemented.
|
|
2780
|
+
// The orchestrator interfaces with them via their public APIs.
|
|
2781
|
+
taskManager = null;
|
|
2782
|
+
agentPool = null;
|
|
2783
|
+
agentConfigs = [];
|
|
2784
|
+
cachedTasks = [];
|
|
2785
|
+
feedbackLoop = null;
|
|
2786
|
+
planApprovalPending = false;
|
|
2787
|
+
phase = "planning";
|
|
2788
|
+
architectureApprovalPending = false;
|
|
2789
|
+
constructor(config) {
|
|
2790
|
+
super();
|
|
2791
|
+
this.config = config;
|
|
2792
|
+
this.pipeline = new TaskPipeline();
|
|
2793
|
+
this.pmScanner = new ProjectManagerScanner();
|
|
2794
|
+
}
|
|
2795
|
+
get isActive() {
|
|
2796
|
+
return this.running;
|
|
2797
|
+
}
|
|
2798
|
+
get isPaused() {
|
|
2799
|
+
return this.paused;
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Emit a structured orchestrator activity event.
|
|
2803
|
+
* These feed the persistent "Orchestrator" panel in the TUI.
|
|
2804
|
+
*/
|
|
2805
|
+
emitActivity(kind, message, details) {
|
|
2806
|
+
const activity = {
|
|
2807
|
+
id: generateId(),
|
|
2808
|
+
kind,
|
|
2809
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2810
|
+
message,
|
|
2811
|
+
details
|
|
2812
|
+
};
|
|
2813
|
+
this.emit("orchestrator:activity", activity);
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* Start the orchestrator:
|
|
2817
|
+
* 1. Ensure workspace directories exist
|
|
2818
|
+
* 2. Initialize TaskManager, AgentPool, MessageBroker, Scheduler, Planner
|
|
2819
|
+
* 3. Load agent configs and register them with the broker
|
|
2820
|
+
* 4. If no tasks exist, use Planner to decompose the project description
|
|
2821
|
+
* 5. Start the polling loop
|
|
2822
|
+
*/
|
|
2823
|
+
async start() {
|
|
2824
|
+
if (this.running) {
|
|
2825
|
+
logger9.warn("Orchestrator already running");
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
const settings = this.config.settings;
|
|
2829
|
+
await mkdir3(settings.messagesDirectory, { recursive: true });
|
|
2830
|
+
await mkdir3(settings.logsDirectory, { recursive: true });
|
|
2831
|
+
const [{ TaskManager: TaskManager2 }, { AgentPool: AgentPool2 }, { loadAgentConfigs: loadAgentConfigs2 }] = await Promise.all([
|
|
2832
|
+
Promise.resolve().then(() => (init_manager(), manager_exports)),
|
|
2833
|
+
Promise.resolve().then(() => (init_pool(), pool_exports)),
|
|
2834
|
+
Promise.resolve().then(() => (init_config(), config_exports))
|
|
2835
|
+
]);
|
|
2836
|
+
this.taskManager = new TaskManager2({ filePath: settings.todoFile, projectName: this.config.name });
|
|
2837
|
+
this.agentPool = new AgentPool2();
|
|
2838
|
+
this.broker = new MessageBroker(settings.messagesDirectory);
|
|
2839
|
+
this.scheduler = new Scheduler();
|
|
2840
|
+
this.scheduler.setPipeline(this.pipeline);
|
|
2841
|
+
this.planner = new Planner({
|
|
2842
|
+
model: settings.defaultModel,
|
|
2843
|
+
workingDirectory: settings.workingDirectory,
|
|
2844
|
+
claudeCommand: settings.claudeCommand,
|
|
2845
|
+
claudeEnv: settings.claudeEnv,
|
|
2846
|
+
techStack: this.config.techStack
|
|
2847
|
+
});
|
|
2848
|
+
this.feedbackLoop = new FeedbackLoop(settings.feedback);
|
|
2849
|
+
this.feedbackLoop.on("plan:proposed", (p) => this.emit("plan:proposed", p));
|
|
2850
|
+
this.feedbackLoop.on("plan:decided", (...args) => this.emit("plan:decided", ...args));
|
|
2851
|
+
this.feedbackLoop.on("question:asked", (q) => this.emit("question:asked", q));
|
|
2852
|
+
this.feedbackLoop.on("question:answered", (...args) => this.emit("question:answered", ...args));
|
|
2853
|
+
this.feedbackLoop.on("progress:report", (r) => this.emit("progress:report", r));
|
|
2854
|
+
this.feedbackLoop.on("milestone:reached", (m) => this.emit("milestone:reached", m));
|
|
2855
|
+
this.feedbackLoop.on("milestone:acknowledged", (id) => this.emit("milestone:acknowledged", id));
|
|
2856
|
+
this.feedbackLoop.on("interaction-mode:changed", (m) => this.emit("interaction-mode:changed", m));
|
|
2857
|
+
const agentConfigs = loadAgentConfigs2(this.config);
|
|
2858
|
+
const agentIds = agentConfigs.map((c) => c.id);
|
|
2859
|
+
this.agentConfigs = agentConfigs;
|
|
2860
|
+
this.emitActivity("info", `Orchestrator starting with ${agentConfigs.length} agent(s) configured`);
|
|
2861
|
+
await this.broker.start(agentIds);
|
|
2862
|
+
this.broker.on("message:received", (message) => {
|
|
2863
|
+
this.emit("message:received", message);
|
|
2864
|
+
});
|
|
2865
|
+
this.broker.on("message:sent", (message) => {
|
|
2866
|
+
this.emit("message:sent", message);
|
|
2867
|
+
});
|
|
2868
|
+
this.agentPool.on("agent:output", (event) => {
|
|
2869
|
+
this.emit("agent:output", event);
|
|
2870
|
+
});
|
|
2871
|
+
this.agentPool.on("agent:status-changed", (agentId, oldStatus, newStatus) => {
|
|
2872
|
+
this.emit("agent:status-changed", agentId, oldStatus, newStatus);
|
|
2873
|
+
});
|
|
2874
|
+
this.agentPool.on("agent:error", (agentId, error) => {
|
|
2875
|
+
this.emit("agent:error", agentId, error);
|
|
2876
|
+
this.emitActivity("agent-error", `Agent ${agentId} encountered an error: ${error.message}`, {
|
|
2877
|
+
agentId
|
|
2878
|
+
});
|
|
2879
|
+
});
|
|
2880
|
+
this.agentPool.on("agent:stopped", (agentId, exitCode) => {
|
|
2881
|
+
this.emit("agent:stopped", agentId, exitCode);
|
|
2882
|
+
this.emitActivity("agent-stopped", `Agent ${agentId} stopped (exit code: ${exitCode})`, {
|
|
2883
|
+
agentId
|
|
2884
|
+
});
|
|
2885
|
+
});
|
|
2886
|
+
this.agentPool.on("agent:spawned", (agentId) => {
|
|
2887
|
+
const agent = this.agentPool.getAgent(agentId);
|
|
2888
|
+
if (agent) {
|
|
2889
|
+
this.emit("agent:spawned", agent.currentState);
|
|
2890
|
+
this.emitActivity("agent-spawned", `Spawned agent ${agent.currentState.config.name} (${agent.currentState.config.role})`, {
|
|
2891
|
+
agentId,
|
|
2892
|
+
agentRole: agent.currentState.config.role
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
});
|
|
2896
|
+
const existingTasks = await this.taskManager.load();
|
|
2897
|
+
if (existingTasks.length === 0) {
|
|
2898
|
+
logger9.info("No existing tasks found, decomposing project goal");
|
|
2899
|
+
this.emitActivity("info", "No existing tasks found, decomposing project goal...");
|
|
2900
|
+
const plannedTasks = await this.planner.decompose(this.config.description);
|
|
2901
|
+
const summaries = plannedTasks.map((t) => ({
|
|
2902
|
+
title: t.title,
|
|
2903
|
+
description: t.description,
|
|
2904
|
+
priority: t.priority,
|
|
2905
|
+
assignedRole: "",
|
|
2906
|
+
// Will be determined by scheduler
|
|
2907
|
+
tags: t.tags
|
|
2908
|
+
}));
|
|
2909
|
+
this.emitActivity("plan-created", `Created plan with ${plannedTasks.length} task(s) from project goal`);
|
|
2910
|
+
this.planApprovalPending = true;
|
|
2911
|
+
this.feedbackLoop.proposePlan(summaries, this.config.description).then(async (decision) => {
|
|
2912
|
+
this.planApprovalPending = false;
|
|
2913
|
+
if (decision === "rejected") {
|
|
2914
|
+
logger9.info("Plan rejected by user, stopping");
|
|
2915
|
+
this.emitActivity("plan-rejected", "Plan rejected by user, stopping orchestrator");
|
|
2916
|
+
await this.stop();
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
this.emitActivity("plan-approved", "Plan approved, creating tasks...");
|
|
2920
|
+
const titleToId = /* @__PURE__ */ new Map();
|
|
2921
|
+
for (const planned of plannedTasks) {
|
|
2922
|
+
const resolvedDeps = planned.dependencies.map((depTitle) => titleToId.get(depTitle)).filter((id) => id != null);
|
|
2923
|
+
const task = await this.taskManager.createTask({
|
|
2924
|
+
title: planned.title,
|
|
2925
|
+
description: planned.description,
|
|
2926
|
+
priority: planned.priority,
|
|
2927
|
+
dependencies: resolvedDeps,
|
|
2928
|
+
tags: planned.tags
|
|
2929
|
+
});
|
|
2930
|
+
titleToId.set(planned.title, task.id);
|
|
2931
|
+
this.emit("task:created", task);
|
|
2932
|
+
this.pipeline.initTask(task);
|
|
2933
|
+
}
|
|
2934
|
+
const allNewTasks = await this.taskManager.load();
|
|
2935
|
+
this.emit("tasks:loaded", allNewTasks);
|
|
2936
|
+
const hasArchTasks = plannedTasks.some((t) => t.tags.includes("architecture"));
|
|
2937
|
+
this.phase = hasArchTasks ? "architecture" : "development";
|
|
2938
|
+
this.emit("phase:changed", this.phase);
|
|
2939
|
+
this.emitActivity("phase-changed", `Phase set to: ${this.phase}`, { toPhase: this.phase });
|
|
2940
|
+
logger9.info({ count: plannedTasks.length, phase: this.phase }, "Initial tasks created from project goal");
|
|
2941
|
+
}).catch((err) => {
|
|
2942
|
+
this.planApprovalPending = false;
|
|
2943
|
+
logger9.error({ err }, "Plan approval flow failed");
|
|
2944
|
+
});
|
|
2945
|
+
} else {
|
|
2946
|
+
if (this.broker) {
|
|
2947
|
+
await this.broker.cleanAllInboxes();
|
|
2948
|
+
}
|
|
2949
|
+
const staleTasks = existingTasks.filter((t) => t.status === "in-progress");
|
|
2950
|
+
for (const task of staleTasks) {
|
|
2951
|
+
await this.taskManager.updateTask(task.id, {
|
|
2952
|
+
status: "pending",
|
|
2953
|
+
assignee: null
|
|
2954
|
+
});
|
|
2955
|
+
logger9.info(
|
|
2956
|
+
{ taskId: task.id, previousAssignee: task.assignee },
|
|
2957
|
+
"Reset stale in-progress task to pending (previous session ended)"
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2960
|
+
const refreshedTasks = staleTasks.length > 0 ? await this.taskManager.load() : existingTasks;
|
|
2961
|
+
const archTasks = refreshedTasks.filter((t) => t.tags.includes("architecture"));
|
|
2962
|
+
const archPending = archTasks.some((t) => t.status !== "done");
|
|
2963
|
+
if (archTasks.length > 0 && archPending) {
|
|
2964
|
+
this.phase = "architecture";
|
|
2965
|
+
} else {
|
|
2966
|
+
this.phase = "development";
|
|
2967
|
+
}
|
|
2968
|
+
this.emit("phase:changed", this.phase);
|
|
2969
|
+
this.emitActivity("phase-changed", `Resuming in phase: ${this.phase} (${existingTasks.length} existing tasks)`, {
|
|
2970
|
+
toPhase: this.phase
|
|
2971
|
+
});
|
|
2972
|
+
for (const task of existingTasks) {
|
|
2973
|
+
if (!this.pipeline.has(task.id)) {
|
|
2974
|
+
this.pipeline.initTask(task);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
this.emit("tasks:loaded", refreshedTasks);
|
|
2978
|
+
logger9.info({
|
|
2979
|
+
phase: this.phase,
|
|
2980
|
+
existingTaskCount: existingTasks.length,
|
|
2981
|
+
staleTasksReset: staleTasks.length
|
|
2982
|
+
}, "Resuming with existing tasks");
|
|
2983
|
+
}
|
|
2984
|
+
this.running = true;
|
|
2985
|
+
this.startedAt = Date.now();
|
|
2986
|
+
this.intervalId = setInterval(() => {
|
|
2987
|
+
this.cycle().catch((err) => {
|
|
2988
|
+
logger9.error({ err }, "Orchestrator cycle failed");
|
|
2989
|
+
});
|
|
2990
|
+
}, settings.pollIntervalMs);
|
|
2991
|
+
await this.cycle();
|
|
2992
|
+
this.pmScanner.start(() => {
|
|
2993
|
+
this.runPMScan().catch((err) => {
|
|
2994
|
+
logger9.error({ err }, "PM scan failed");
|
|
2995
|
+
});
|
|
2996
|
+
});
|
|
2997
|
+
this.emitActivity("info", "Orchestrator started and running (PM scanner active)");
|
|
2998
|
+
logger9.info("Orchestrator started");
|
|
2999
|
+
}
|
|
3000
|
+
/**
|
|
3001
|
+
* Stop the orchestrator gracefully.
|
|
3002
|
+
*/
|
|
3003
|
+
async stop() {
|
|
3004
|
+
if (!this.running) {
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
logger9.info("Stopping orchestrator");
|
|
3008
|
+
this.emitActivity("info", "Orchestrator shutting down...");
|
|
3009
|
+
if (this.intervalId !== null) {
|
|
3010
|
+
clearInterval(this.intervalId);
|
|
3011
|
+
this.intervalId = null;
|
|
3012
|
+
}
|
|
3013
|
+
this.pmScanner.stop();
|
|
3014
|
+
if (this.agentPool) {
|
|
3015
|
+
await this.agentPool.stopAll();
|
|
3016
|
+
}
|
|
3017
|
+
if (this.broker) {
|
|
3018
|
+
await this.broker.stop();
|
|
3019
|
+
}
|
|
3020
|
+
if (this.feedbackLoop) {
|
|
3021
|
+
this.feedbackLoop.cleanup();
|
|
3022
|
+
}
|
|
3023
|
+
this.running = false;
|
|
3024
|
+
this.paused = false;
|
|
3025
|
+
logger9.info("Orchestrator stopped");
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Pause the orchestrator: stop the polling cycle and all running agents.
|
|
3029
|
+
* Tasks and state are preserved so work can be resumed later.
|
|
3030
|
+
*/
|
|
3031
|
+
async pause() {
|
|
3032
|
+
if (!this.running || this.paused) return;
|
|
3033
|
+
this.paused = true;
|
|
3034
|
+
logger9.info("Pausing orchestrator");
|
|
3035
|
+
this.emitActivity("info", "Orchestrator pausing \u2014 stopping agents...");
|
|
3036
|
+
if (this.intervalId !== null) {
|
|
3037
|
+
clearInterval(this.intervalId);
|
|
3038
|
+
this.intervalId = null;
|
|
3039
|
+
}
|
|
3040
|
+
this.pmScanner.stop();
|
|
3041
|
+
if (this.agentPool) {
|
|
3042
|
+
await this.agentPool.stopAll();
|
|
3043
|
+
}
|
|
3044
|
+
if (this.taskManager) {
|
|
3045
|
+
const tasks = await this.taskManager.load();
|
|
3046
|
+
for (const task of tasks) {
|
|
3047
|
+
if (task.status === "in-progress") {
|
|
3048
|
+
await this.taskManager.updateTask(task.id, {
|
|
3049
|
+
status: "pending",
|
|
3050
|
+
assignee: null
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
const refreshed = await this.taskManager.load();
|
|
3055
|
+
this.emit("tasks:loaded", refreshed);
|
|
3056
|
+
}
|
|
3057
|
+
this.emitActivity("info", "Orchestrator paused");
|
|
3058
|
+
this.emit("orchestrator:paused");
|
|
3059
|
+
logger9.info("Orchestrator paused");
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Resume the orchestrator after a pause. Restarts the polling cycle
|
|
3063
|
+
* and PM scanner so agents can be re-assigned to pending tasks.
|
|
3064
|
+
*/
|
|
3065
|
+
async resume() {
|
|
3066
|
+
if (!this.running || !this.paused) return;
|
|
3067
|
+
this.paused = false;
|
|
3068
|
+
logger9.info("Resuming orchestrator");
|
|
3069
|
+
this.emitActivity("info", "Orchestrator resuming...");
|
|
3070
|
+
if (this.broker) {
|
|
3071
|
+
await this.broker.cleanAllInboxes();
|
|
3072
|
+
}
|
|
3073
|
+
const settings = this.config.settings;
|
|
3074
|
+
this.intervalId = setInterval(() => {
|
|
3075
|
+
this.cycle().catch((err) => {
|
|
3076
|
+
logger9.error({ err }, "Orchestrator cycle failed");
|
|
3077
|
+
});
|
|
3078
|
+
}, settings.pollIntervalMs);
|
|
3079
|
+
await this.cycle();
|
|
3080
|
+
this.pmScanner.start(() => {
|
|
3081
|
+
this.runPMScan().catch((err) => {
|
|
3082
|
+
logger9.error({ err }, "PM scan failed");
|
|
3083
|
+
});
|
|
3084
|
+
});
|
|
3085
|
+
this.emitActivity("info", "Orchestrator resumed");
|
|
3086
|
+
this.emit("orchestrator:resumed");
|
|
3087
|
+
logger9.info("Orchestrator resumed");
|
|
3088
|
+
}
|
|
3089
|
+
/**
|
|
3090
|
+
* Add new instructions: decompose them into tasks and append to the
|
|
3091
|
+
* existing todo.md without deleting any existing tasks.
|
|
3092
|
+
*/
|
|
3093
|
+
async addInstructions(instructions) {
|
|
3094
|
+
if (!this.planner || !this.taskManager) {
|
|
3095
|
+
throw new Error("Orchestrator must be started before adding instructions");
|
|
3096
|
+
}
|
|
3097
|
+
this.emitActivity("info", "Decomposing new instructions into tasks...");
|
|
3098
|
+
this.emit("instructions:decomposing", instructions);
|
|
3099
|
+
const plannedTasks = await this.planner.decompose(instructions);
|
|
3100
|
+
const existingTasks = await this.taskManager.load();
|
|
3101
|
+
const titleToId = /* @__PURE__ */ new Map();
|
|
3102
|
+
for (const t of existingTasks) {
|
|
3103
|
+
titleToId.set(t.title, t.id);
|
|
3104
|
+
}
|
|
3105
|
+
const createdTasks = [];
|
|
3106
|
+
for (const planned of plannedTasks) {
|
|
3107
|
+
const resolvedDeps = planned.dependencies.map((depTitle) => titleToId.get(depTitle)).filter((id) => id != null);
|
|
3108
|
+
const task = await this.taskManager.createTask({
|
|
3109
|
+
title: planned.title,
|
|
3110
|
+
description: planned.description,
|
|
3111
|
+
priority: planned.priority,
|
|
3112
|
+
dependencies: resolvedDeps,
|
|
3113
|
+
tags: planned.tags
|
|
3114
|
+
});
|
|
3115
|
+
titleToId.set(planned.title, task.id);
|
|
3116
|
+
this.emit("task:created", task);
|
|
3117
|
+
this.pipeline.initTask(task);
|
|
3118
|
+
createdTasks.push(task);
|
|
3119
|
+
}
|
|
3120
|
+
const allTasks = await this.taskManager.load();
|
|
3121
|
+
this.emit("tasks:loaded", allTasks);
|
|
3122
|
+
this.emitActivity("info", `Added ${createdTasks.length} new task(s) from instructions`);
|
|
3123
|
+
this.emit("instructions:added", createdTasks);
|
|
3124
|
+
logger9.info({ count: createdTasks.length }, "New tasks added from instructions");
|
|
3125
|
+
return createdTasks;
|
|
3126
|
+
}
|
|
3127
|
+
/**
|
|
3128
|
+
* A single orchestration cycle:
|
|
3129
|
+
* 1. Load current tasks
|
|
3130
|
+
* 2. Run scheduler to determine assignments
|
|
3131
|
+
* 3. For each assignment: update task, send message, spawn agent
|
|
3132
|
+
* 4. Handle pipeline handoffs for completed tasks
|
|
3133
|
+
* 5. Check budget limits
|
|
3134
|
+
* 6. Emit cycle stats
|
|
3135
|
+
*/
|
|
3136
|
+
async cycle() {
|
|
3137
|
+
if (!this.taskManager || !this.scheduler || !this.broker || !this.agentPool) {
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
if (this.paused || this.planApprovalPending) {
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
let tasks = await this.taskManager.load();
|
|
3144
|
+
const poolAgents = this.agentPool.getAllAgents().map((a) => a.currentState);
|
|
3145
|
+
let tasksUpdated = false;
|
|
3146
|
+
for (const agentState of poolAgents) {
|
|
3147
|
+
if (agentState.status === "stopped" || agentState.status === "error") {
|
|
3148
|
+
const agentProcess = this.agentPool.getAgent(agentState.config.id);
|
|
3149
|
+
if (agentProcess) {
|
|
3150
|
+
agentProcess.setCurrentTask(null);
|
|
3151
|
+
}
|
|
3152
|
+
const assignedTask = tasks.find(
|
|
3153
|
+
(t) => t.status === "in-progress" && t.assignee === agentState.config.id
|
|
3154
|
+
);
|
|
3155
|
+
if (assignedTask) {
|
|
3156
|
+
if (agentState.status === "stopped") {
|
|
3157
|
+
const handoff = this.pipeline.completeStage(assignedTask.id);
|
|
3158
|
+
if (handoff && handoff.nextStage) {
|
|
3159
|
+
this.emitActivity("pipeline-handoff", handoff.handoffMessage, {
|
|
3160
|
+
taskId: assignedTask.id,
|
|
3161
|
+
taskTitle: assignedTask.title,
|
|
3162
|
+
agentId: agentState.config.id,
|
|
3163
|
+
agentRole: agentState.config.role,
|
|
3164
|
+
pipelineStage: handoff.nextStage
|
|
3165
|
+
});
|
|
3166
|
+
await this.taskManager.updateTask(assignedTask.id, {
|
|
3167
|
+
status: "pending",
|
|
3168
|
+
assignee: null,
|
|
3169
|
+
notes: [
|
|
3170
|
+
...assignedTask.notes,
|
|
3171
|
+
`[pipeline] Handed off to ${handoff.nextStage} stage`
|
|
3172
|
+
]
|
|
3173
|
+
});
|
|
3174
|
+
const handoffMsg = {
|
|
3175
|
+
id: generateId(),
|
|
3176
|
+
type: "task-update",
|
|
3177
|
+
from: "orchestrator",
|
|
3178
|
+
to: "*",
|
|
3179
|
+
subject: `Pipeline Handoff: ${assignedTask.title}`,
|
|
3180
|
+
body: `${handoff.handoffMessage}
|
|
3181
|
+
|
|
3182
|
+
Task "${assignedTask.title}" (${assignedTask.id}) is now in the ${handoff.nextStage} stage. Required roles: ${handoff.requiredRoles.join(", ")}`,
|
|
3183
|
+
taskId: assignedTask.id,
|
|
3184
|
+
priority: assignedTask.priority === "critical" ? "urgent" : "normal",
|
|
3185
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3186
|
+
metadata: {
|
|
3187
|
+
pipelineStage: handoff.nextStage,
|
|
3188
|
+
previousAgent: agentState.config.id
|
|
3189
|
+
}
|
|
3190
|
+
};
|
|
3191
|
+
await this.broker.sendMessage(handoffMsg);
|
|
3192
|
+
} else {
|
|
3193
|
+
await this.taskManager.updateTask(assignedTask.id, { status: "done" });
|
|
3194
|
+
this.emit("task:completed", assignedTask);
|
|
3195
|
+
this.emitActivity("task-completed", `Task completed: ${assignedTask.title}`, {
|
|
3196
|
+
taskId: assignedTask.id,
|
|
3197
|
+
taskTitle: assignedTask.title,
|
|
3198
|
+
agentId: agentState.config.id,
|
|
3199
|
+
agentRole: agentState.config.role
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
logger9.info(
|
|
3203
|
+
{ taskId: assignedTask.id, agentId: agentState.config.id },
|
|
3204
|
+
"Task stage completed (agent process stopped)"
|
|
3205
|
+
);
|
|
3206
|
+
} else if (agentState.rateLimited) {
|
|
3207
|
+
this.pipeline.failStage(assignedTask.id);
|
|
3208
|
+
await this.taskManager.updateTask(assignedTask.id, {
|
|
3209
|
+
status: "pending",
|
|
3210
|
+
assignee: null
|
|
3211
|
+
});
|
|
3212
|
+
const stats2 = this.getStats();
|
|
3213
|
+
const totalTokens = stats2.totalInputTokens + stats2.totalOutputTokens + stats2.totalCacheReadTokens + stats2.totalCacheCreationTokens;
|
|
3214
|
+
const resetInfo = agentState.rateLimitResetAt ? ` Resets at ${agentState.rateLimitResetAt}.` : "";
|
|
3215
|
+
this.emitActivity(
|
|
3216
|
+
"rate-limit",
|
|
3217
|
+
`RATE LIMIT: Agent ${agentState.config.name} (${agentState.config.role}) hit API usage limit.${resetInfo} Total tokens used: ${this.formatTokenCount(totalTokens)} (in:${this.formatTokenCount(stats2.totalInputTokens)} out:${this.formatTokenCount(stats2.totalOutputTokens)} cache-r:${this.formatTokenCount(stats2.totalCacheReadTokens)} cache-w:${this.formatTokenCount(stats2.totalCacheCreationTokens)}) | $${stats2.totalCostUsd.toFixed(4)}`,
|
|
3218
|
+
{
|
|
3219
|
+
taskId: assignedTask.id,
|
|
3220
|
+
taskTitle: assignedTask.title,
|
|
3221
|
+
agentId: agentState.config.id,
|
|
3222
|
+
agentRole: agentState.config.role,
|
|
3223
|
+
rateLimitResetAt: agentState.rateLimitResetAt,
|
|
3224
|
+
totalTokens,
|
|
3225
|
+
cost: stats2.totalCostUsd
|
|
3226
|
+
}
|
|
3227
|
+
);
|
|
3228
|
+
this.emit("rate-limit", agentState.config.id, agentState.rateLimitResetAt);
|
|
3229
|
+
await this.broker.cleanInboxForTask(agentState.config.id, assignedTask.id);
|
|
3230
|
+
logger9.warn(
|
|
3231
|
+
{ taskId: assignedTask.id, agentId: agentState.config.id, resetAt: agentState.rateLimitResetAt },
|
|
3232
|
+
"Agent hit rate limit \u2014 task reset to pending"
|
|
3233
|
+
);
|
|
3234
|
+
} else {
|
|
3235
|
+
this.pipeline.failStage(assignedTask.id);
|
|
3236
|
+
await this.taskManager.updateTask(assignedTask.id, {
|
|
3237
|
+
status: "pending",
|
|
3238
|
+
assignee: null
|
|
3239
|
+
});
|
|
3240
|
+
this.emitActivity("task-failed", `Task failed and reset: ${assignedTask.title} (agent ${agentState.config.id} errored)`, {
|
|
3241
|
+
taskId: assignedTask.id,
|
|
3242
|
+
taskTitle: assignedTask.title,
|
|
3243
|
+
agentId: agentState.config.id
|
|
3244
|
+
});
|
|
3245
|
+
await this.broker.cleanInboxForTask(agentState.config.id, assignedTask.id);
|
|
3246
|
+
logger9.warn(
|
|
3247
|
+
{ taskId: assignedTask.id, agentId: agentState.config.id },
|
|
3248
|
+
"Task reset to pending (agent process errored)"
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3251
|
+
tasksUpdated = true;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
const activeAgentIds = new Set(
|
|
3256
|
+
poolAgents.filter((a) => a.status === "running" || a.status === "starting").map((a) => a.config.id)
|
|
3257
|
+
);
|
|
3258
|
+
for (const task of tasks) {
|
|
3259
|
+
if (task.status !== "in-progress") continue;
|
|
3260
|
+
if (!task.assignee) {
|
|
3261
|
+
await this.taskManager.updateTask(task.id, { status: "pending", assignee: null });
|
|
3262
|
+
this.emitActivity("info", `Reset orphaned task "${task.title}" (no assignee)`, { taskId: task.id });
|
|
3263
|
+
tasksUpdated = true;
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
3266
|
+
if (!activeAgentIds.has(task.assignee)) {
|
|
3267
|
+
const inPool = poolAgents.find((a) => a.config.id === task.assignee);
|
|
3268
|
+
if (!inPool || inPool.status === "idle" || inPool.status === "stopped" || inPool.status === "error") {
|
|
3269
|
+
await this.taskManager.updateTask(task.id, { status: "pending", assignee: null });
|
|
3270
|
+
this.emitActivity("info", `Reset orphaned task "${task.title}" (agent ${task.assignee} ${inPool ? inPool.status : "not in pool"})`, {
|
|
3271
|
+
taskId: task.id,
|
|
3272
|
+
agentId: task.assignee
|
|
3273
|
+
});
|
|
3274
|
+
logger9.info({ taskId: task.id, assignee: task.assignee, agentStatus: inPool?.status }, "Reset orphaned in-progress task");
|
|
3275
|
+
tasksUpdated = true;
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
for (const task of tasks) {
|
|
3280
|
+
if (task.status !== "blocked") continue;
|
|
3281
|
+
if (task.dependencies.length === 0) {
|
|
3282
|
+
await this.taskManager.updateTask(task.id, { status: "pending" });
|
|
3283
|
+
this.emitActivity("info", `Unblocked task "${task.title}" (no dependencies)`, { taskId: task.id });
|
|
3284
|
+
tasksUpdated = true;
|
|
3285
|
+
continue;
|
|
3286
|
+
}
|
|
3287
|
+
const allDepsResolved = task.dependencies.every((depId) => {
|
|
3288
|
+
const dep = tasks.find((t) => t.id === depId);
|
|
3289
|
+
return !dep || dep.status === "done" || dep.status === "cancelled";
|
|
3290
|
+
});
|
|
3291
|
+
if (allDepsResolved) {
|
|
3292
|
+
await this.taskManager.updateTask(task.id, { status: "pending" });
|
|
3293
|
+
this.emitActivity("info", `Unblocked task "${task.title}" (all dependencies resolved)`, { taskId: task.id });
|
|
3294
|
+
tasksUpdated = true;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
if (tasksUpdated) {
|
|
3298
|
+
tasks = await this.taskManager.load();
|
|
3299
|
+
}
|
|
3300
|
+
this.cachedTasks = tasks;
|
|
3301
|
+
if (this.feedbackLoop) {
|
|
3302
|
+
this.feedbackLoop.checkProgress(tasks, this.getStats());
|
|
3303
|
+
}
|
|
3304
|
+
if (tasks.length > 0) {
|
|
3305
|
+
const allDone = tasks.every((t) => t.status === "done" || t.status === "cancelled");
|
|
3306
|
+
if (allDone) {
|
|
3307
|
+
const summary = this.buildCompletionSummary(tasks);
|
|
3308
|
+
this.emit("project:completed", summary);
|
|
3309
|
+
this.emitActivity("project-completed", `All ${summary.totalTasks} tasks completed! Total cost: $${summary.totalCostUsd.toFixed(4)}`);
|
|
3310
|
+
logger9.info({ totalTasks: summary.totalTasks, totalCost: summary.totalCostUsd }, "All tasks completed");
|
|
3311
|
+
await this.stop();
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
if (this.phase === "architecture") {
|
|
3316
|
+
if (this.architectureApprovalPending) {
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
3319
|
+
const archTasks = tasks.filter((t) => t.tags.includes("architecture"));
|
|
3320
|
+
const allArchDone = archTasks.length > 0 && archTasks.every((t) => t.status === "done");
|
|
3321
|
+
if (allArchDone) {
|
|
3322
|
+
const mode = this.feedbackLoop?.interactionMode ?? "unattended";
|
|
3323
|
+
if (mode === "supervised" || mode === "interactive") {
|
|
3324
|
+
this.architectureApprovalPending = true;
|
|
3325
|
+
this.emit("architecture:ready", archTasks);
|
|
3326
|
+
this.emitActivity("phase-changed", "Architecture phase complete, waiting for user review", {
|
|
3327
|
+
fromPhase: "architecture"
|
|
3328
|
+
});
|
|
3329
|
+
logger9.info("Architecture phase complete, waiting for user review");
|
|
3330
|
+
return;
|
|
3331
|
+
} else {
|
|
3332
|
+
this.phase = "development";
|
|
3333
|
+
this.emit("phase:changed", "development");
|
|
3334
|
+
this.emitActivity("phase-changed", "Architecture complete, advancing to development", {
|
|
3335
|
+
fromPhase: "architecture",
|
|
3336
|
+
toPhase: "development"
|
|
3337
|
+
});
|
|
3338
|
+
logger9.info("Architecture phase complete, advancing to development");
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
const poolAgentIds = new Set(poolAgents.map((a) => a.config.id));
|
|
3343
|
+
const virtualAgentStates = poolAgents.map((a) => {
|
|
3344
|
+
if (a.status === "stopped" || a.status === "error") {
|
|
3345
|
+
return { ...a, status: "idle" };
|
|
3346
|
+
}
|
|
3347
|
+
return a;
|
|
3348
|
+
});
|
|
3349
|
+
for (const config of this.agentConfigs) {
|
|
3350
|
+
if (!poolAgentIds.has(config.id)) {
|
|
3351
|
+
virtualAgentStates.push({
|
|
3352
|
+
config,
|
|
3353
|
+
status: "idle",
|
|
3354
|
+
pid: null,
|
|
3355
|
+
sessionId: null,
|
|
3356
|
+
currentTask: null,
|
|
3357
|
+
startedAt: null,
|
|
3358
|
+
lastActivityAt: null,
|
|
3359
|
+
totalCostUsd: 0,
|
|
3360
|
+
turnCount: 0,
|
|
3361
|
+
totalInputTokens: 0,
|
|
3362
|
+
totalOutputTokens: 0,
|
|
3363
|
+
totalCacheReadTokens: 0,
|
|
3364
|
+
totalCacheCreationTokens: 0,
|
|
3365
|
+
error: null,
|
|
3366
|
+
resultError: null,
|
|
3367
|
+
rateLimited: false,
|
|
3368
|
+
rateLimitResetAt: null
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
let schedulerTasks = tasks;
|
|
3373
|
+
let schedulerAgents = virtualAgentStates;
|
|
3374
|
+
if (this.phase === "architecture") {
|
|
3375
|
+
schedulerTasks = tasks.filter((t) => t.tags.includes("architecture"));
|
|
3376
|
+
schedulerAgents = virtualAgentStates.filter((a) => a.config.role === "architect");
|
|
3377
|
+
}
|
|
3378
|
+
const assignments = this.scheduler.schedule(schedulerTasks, schedulerAgents);
|
|
3379
|
+
for (const assignment of assignments) {
|
|
3380
|
+
const task = tasks.find((t) => t.id === assignment.taskId);
|
|
3381
|
+
if (!task) continue;
|
|
3382
|
+
if (!this.pipeline.has(task.id)) {
|
|
3383
|
+
this.pipeline.initTask(task);
|
|
3384
|
+
}
|
|
3385
|
+
this.pipeline.startStage(task.id, assignment.agentId);
|
|
3386
|
+
const pipelineState = this.pipeline.getState(task.id);
|
|
3387
|
+
const currentStage = pipelineState?.currentStage ?? "develop";
|
|
3388
|
+
await this.taskManager.updateTask(assignment.taskId, {
|
|
3389
|
+
status: "in-progress",
|
|
3390
|
+
assignee: assignment.agentId
|
|
3391
|
+
});
|
|
3392
|
+
this.emit("task:assigned", task, assignment.agentId);
|
|
3393
|
+
const agentConfig = this.agentConfigs.find((c) => c.id === assignment.agentId);
|
|
3394
|
+
const agentRole = agentConfig?.role ?? "unknown";
|
|
3395
|
+
this.emitActivity("task-delegated", `Delegated "${task.title}" to ${agentConfig?.name ?? assignment.agentId} (${agentRole}) [stage: ${currentStage}]`, {
|
|
3396
|
+
taskId: task.id,
|
|
3397
|
+
taskTitle: task.title,
|
|
3398
|
+
agentId: assignment.agentId,
|
|
3399
|
+
agentRole,
|
|
3400
|
+
pipelineStage: currentStage
|
|
3401
|
+
});
|
|
3402
|
+
const pipelineContext = pipelineState ? `
|
|
3403
|
+
|
|
3404
|
+
**Pipeline Stage:** ${currentStage}
|
|
3405
|
+
**Pipeline History:** ${pipelineState.stageHistory.map((h) => `${h.stage}(${h.status})`).join(" -> ") || "none"}` : "";
|
|
3406
|
+
const message = {
|
|
3407
|
+
id: generateId(),
|
|
3408
|
+
type: "task-assignment",
|
|
3409
|
+
from: "orchestrator",
|
|
3410
|
+
to: assignment.agentId,
|
|
3411
|
+
subject: `Task Assignment: ${task.title}`,
|
|
3412
|
+
body: this.buildTaskPrompt(task) + pipelineContext,
|
|
3413
|
+
taskId: task.id,
|
|
3414
|
+
priority: task.priority === "critical" ? "urgent" : "normal",
|
|
3415
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3416
|
+
metadata: {
|
|
3417
|
+
pipelineStage: currentStage
|
|
3418
|
+
}
|
|
3419
|
+
};
|
|
3420
|
+
await this.broker.sendMessage(message);
|
|
3421
|
+
const taskLabel = `${task.id}: ${task.title}`;
|
|
3422
|
+
try {
|
|
3423
|
+
const existingAgent = this.agentPool.getAgent(assignment.agentId);
|
|
3424
|
+
if (existingAgent) {
|
|
3425
|
+
existingAgent.setCurrentTask(taskLabel);
|
|
3426
|
+
const agentStatus = existingAgent.currentState.status;
|
|
3427
|
+
if (agentStatus === "stopped" || agentStatus === "error") {
|
|
3428
|
+
if (existingAgent.currentState.sessionId && agentStatus === "stopped") {
|
|
3429
|
+
await existingAgent.resume(message.body);
|
|
3430
|
+
} else {
|
|
3431
|
+
this.agentPool.removeAgent(assignment.agentId);
|
|
3432
|
+
if (agentConfig) {
|
|
3433
|
+
const newAgent = await this.agentPool.spawnAgent(agentConfig, message.body);
|
|
3434
|
+
newAgent.setCurrentTask(taskLabel);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
} else {
|
|
3439
|
+
if (agentConfig) {
|
|
3440
|
+
const newAgent = await this.agentPool.spawnAgent(agentConfig, message.body);
|
|
3441
|
+
newAgent.setCurrentTask(taskLabel);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
} catch (err) {
|
|
3445
|
+
logger9.error(
|
|
3446
|
+
{ err, agentId: assignment.agentId, taskId: assignment.taskId },
|
|
3447
|
+
"Failed to spawn agent for task"
|
|
3448
|
+
);
|
|
3449
|
+
this.emitActivity("agent-error", `Failed to spawn agent ${assignment.agentId} for task ${task.title}`, {
|
|
3450
|
+
agentId: assignment.agentId,
|
|
3451
|
+
taskId: task.id,
|
|
3452
|
+
taskTitle: task.title
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
this.checkBudget();
|
|
3457
|
+
const stats = this.getStats();
|
|
3458
|
+
this.emit("orchestrator:cycle", stats);
|
|
3459
|
+
const pendingCount = tasks.filter((t) => t.status === "pending").length;
|
|
3460
|
+
const inProgressCount = tasks.filter((t) => t.status === "in-progress").length;
|
|
3461
|
+
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
3462
|
+
const activeCount = stats.activeAgents;
|
|
3463
|
+
if (tasks.length > 0) {
|
|
3464
|
+
const totalTokens = stats.totalInputTokens + stats.totalOutputTokens + stats.totalCacheReadTokens + stats.totalCacheCreationTokens;
|
|
3465
|
+
const tokenStr = totalTokens > 0 ? ` | ${this.formatTokenCount(totalTokens)} tokens (in:${this.formatTokenCount(stats.totalInputTokens)} out:${this.formatTokenCount(stats.totalOutputTokens)} cache-r:${this.formatTokenCount(stats.totalCacheReadTokens)} cache-w:${this.formatTokenCount(stats.totalCacheCreationTokens)})` : "";
|
|
3466
|
+
this.emitActivity(
|
|
3467
|
+
"cycle-summary",
|
|
3468
|
+
`Cycle: ${doneCount}/${tasks.length} done | ${inProgressCount} in-progress | ${pendingCount} pending | ${activeCount} agent(s) active | $${stats.totalCostUsd.toFixed(4)}${tokenStr}`
|
|
3469
|
+
);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Run a single PM scan pass — assess project health, identify gaps and
|
|
3474
|
+
* blockers, and surface findings in the orchestrator activity log.
|
|
3475
|
+
*/
|
|
3476
|
+
async runPMScan() {
|
|
3477
|
+
if (!this.running || !this.taskManager || !this.agentPool) return;
|
|
3478
|
+
const tasks = await this.taskManager.load();
|
|
3479
|
+
if (tasks.length === 0) return;
|
|
3480
|
+
const poolAgents = this.agentPool.getAllAgents().map((a) => a.currentState);
|
|
3481
|
+
const poolAgentIds = new Set(poolAgents.map((a) => a.config.id));
|
|
3482
|
+
const allAgentStates = [...poolAgents];
|
|
3483
|
+
for (const config of this.agentConfigs) {
|
|
3484
|
+
if (!poolAgentIds.has(config.id)) {
|
|
3485
|
+
allAgentStates.push({
|
|
3486
|
+
config,
|
|
3487
|
+
status: "idle",
|
|
3488
|
+
pid: null,
|
|
3489
|
+
sessionId: null,
|
|
3490
|
+
currentTask: null,
|
|
3491
|
+
startedAt: null,
|
|
3492
|
+
lastActivityAt: null,
|
|
3493
|
+
totalCostUsd: 0,
|
|
3494
|
+
turnCount: 0,
|
|
3495
|
+
totalInputTokens: 0,
|
|
3496
|
+
totalOutputTokens: 0,
|
|
3497
|
+
totalCacheReadTokens: 0,
|
|
3498
|
+
totalCacheCreationTokens: 0,
|
|
3499
|
+
error: null,
|
|
3500
|
+
resultError: null,
|
|
3501
|
+
rateLimited: false,
|
|
3502
|
+
rateLimitResetAt: null
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
const result = this.pmScanner.scan(tasks, allAgentStates, this.phase);
|
|
3507
|
+
for (const finding of result.findings) {
|
|
3508
|
+
if (finding.kind === "all-clear") {
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
const activityKind = finding.severity === "critical" ? "gap-detected" : "info";
|
|
3512
|
+
this.emitActivity(activityKind, `[PM] ${finding.message}`, {
|
|
3513
|
+
taskId: finding.taskId,
|
|
3514
|
+
agentId: finding.agentId
|
|
3515
|
+
});
|
|
3516
|
+
}
|
|
3517
|
+
this.emitActivity("pm-scan", result.summary);
|
|
3518
|
+
if (this.broker) {
|
|
3519
|
+
const messages = this.pmScanner.buildMessages(result);
|
|
3520
|
+
for (const msg of messages) {
|
|
3521
|
+
try {
|
|
3522
|
+
await this.broker.sendMessage(msg);
|
|
3523
|
+
} catch (err) {
|
|
3524
|
+
logger9.error({ err, msgId: msg.id }, "Failed to send PM scan message");
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
logger9.debug({ findingCount: result.findings.length }, "PM scan complete");
|
|
3529
|
+
}
|
|
3530
|
+
/**
|
|
3531
|
+
* Build a task prompt for an agent.
|
|
3532
|
+
*/
|
|
3533
|
+
buildTaskPrompt(task) {
|
|
3534
|
+
const parts = [
|
|
3535
|
+
`# Task: ${task.title}`,
|
|
3536
|
+
`**ID:** ${task.id}`,
|
|
3537
|
+
`**Priority:** ${task.priority}`,
|
|
3538
|
+
"",
|
|
3539
|
+
task.description
|
|
3540
|
+
];
|
|
3541
|
+
if (task.tags.length > 0) {
|
|
3542
|
+
parts.push("", `**Tags:** ${task.tags.join(", ")}`);
|
|
3543
|
+
}
|
|
3544
|
+
if (task.notes.length > 0) {
|
|
3545
|
+
parts.push("", "**Notes:**");
|
|
3546
|
+
for (const note of task.notes) {
|
|
3547
|
+
parts.push(`- ${note}`);
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
return parts.join("\n");
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3553
|
+
* Check whether the total cost across all agents exceeds the budget.
|
|
3554
|
+
*/
|
|
3555
|
+
checkBudget() {
|
|
3556
|
+
const maxBudget = this.config.settings.maxTotalBudgetUsd;
|
|
3557
|
+
if (maxBudget == null) return;
|
|
3558
|
+
const agentStates = this.agentPool.getAllAgents().map((a) => a.currentState);
|
|
3559
|
+
const totalCost = agentStates.reduce(
|
|
3560
|
+
(sum, a) => sum + a.totalCostUsd,
|
|
3561
|
+
0
|
|
3562
|
+
);
|
|
3563
|
+
const warningThreshold = maxBudget * 0.8;
|
|
3564
|
+
if (totalCost >= maxBudget) {
|
|
3565
|
+
this.emit("budget:exceeded", totalCost, maxBudget);
|
|
3566
|
+
this.emitActivity("budget-warning", `Budget exceeded: $${totalCost.toFixed(4)} / $${maxBudget.toFixed(2)}`, {
|
|
3567
|
+
cost: totalCost
|
|
3568
|
+
});
|
|
3569
|
+
logger9.warn({ totalCost, maxBudget }, "Budget exceeded, stopping orchestrator");
|
|
3570
|
+
this.stop().catch((err) => {
|
|
3571
|
+
logger9.error({ err }, "Failed to stop orchestrator after budget exceeded");
|
|
3572
|
+
});
|
|
3573
|
+
} else if (totalCost >= warningThreshold) {
|
|
3574
|
+
this.emit("budget:warning", totalCost, maxBudget);
|
|
3575
|
+
this.emitActivity("budget-warning", `Approaching budget limit: $${totalCost.toFixed(4)} / $${maxBudget.toFixed(2)} (${(totalCost / maxBudget * 100).toFixed(0)}%)`, {
|
|
3576
|
+
cost: totalCost
|
|
3577
|
+
});
|
|
3578
|
+
logger9.warn({ totalCost, maxBudget }, "Approaching budget limit");
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* Get all current tasks.
|
|
3583
|
+
*/
|
|
3584
|
+
async getTasks() {
|
|
3585
|
+
if (!this.taskManager) return [];
|
|
3586
|
+
return this.taskManager.load();
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Get all agent states.
|
|
3590
|
+
*/
|
|
3591
|
+
getAgentStates() {
|
|
3592
|
+
if (!this.agentPool) return [];
|
|
3593
|
+
return this.agentPool.getAllAgents().map((a) => a.currentState);
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* Get the pipeline state for a specific task.
|
|
3597
|
+
*/
|
|
3598
|
+
getTaskPipelineState(taskId) {
|
|
3599
|
+
return this.pipeline.getState(taskId);
|
|
3600
|
+
}
|
|
3601
|
+
/**
|
|
3602
|
+
* Get current orchestrator statistics.
|
|
3603
|
+
*/
|
|
3604
|
+
getStats() {
|
|
3605
|
+
const tasks = this.cachedTasks;
|
|
3606
|
+
const agentStates = this.agentPool ? this.agentPool.getAllAgents().map((a) => a.currentState) : [];
|
|
3607
|
+
const spawnedIds = new Set(agentStates.map((a) => a.config.id));
|
|
3608
|
+
const completedTasks = tasks.filter((t) => t.status === "done").length;
|
|
3609
|
+
const activeAgents = agentStates.filter(
|
|
3610
|
+
(a) => a.status === "running" || a.status === "starting"
|
|
3611
|
+
).length;
|
|
3612
|
+
const totalCostUsd = agentStates.reduce(
|
|
3613
|
+
(sum, a) => sum + a.totalCostUsd,
|
|
3614
|
+
0
|
|
3615
|
+
);
|
|
3616
|
+
const totalInputTokens = agentStates.reduce(
|
|
3617
|
+
(sum, a) => sum + a.totalInputTokens,
|
|
3618
|
+
0
|
|
3619
|
+
);
|
|
3620
|
+
const totalOutputTokens = agentStates.reduce(
|
|
3621
|
+
(sum, a) => sum + a.totalOutputTokens,
|
|
3622
|
+
0
|
|
3623
|
+
);
|
|
3624
|
+
const totalCacheReadTokens = agentStates.reduce(
|
|
3625
|
+
(sum, a) => sum + a.totalCacheReadTokens,
|
|
3626
|
+
0
|
|
3627
|
+
);
|
|
3628
|
+
const totalCacheCreationTokens = agentStates.reduce(
|
|
3629
|
+
(sum, a) => sum + a.totalCacheCreationTokens,
|
|
3630
|
+
0
|
|
3631
|
+
);
|
|
3632
|
+
const uptimeMs = this.startedAt ? Date.now() - this.startedAt : 0;
|
|
3633
|
+
const agents = agentStates.map((a) => ({
|
|
3634
|
+
id: a.config.id,
|
|
3635
|
+
name: a.config.name,
|
|
3636
|
+
role: a.config.role,
|
|
3637
|
+
status: a.status,
|
|
3638
|
+
spawned: true,
|
|
3639
|
+
currentTask: a.currentTask
|
|
3640
|
+
}));
|
|
3641
|
+
for (const config of this.agentConfigs) {
|
|
3642
|
+
if (!spawnedIds.has(config.id)) {
|
|
3643
|
+
agents.push({
|
|
3644
|
+
id: config.id,
|
|
3645
|
+
name: config.name,
|
|
3646
|
+
role: config.role,
|
|
3647
|
+
status: "idle",
|
|
3648
|
+
spawned: false,
|
|
3649
|
+
currentTask: null
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
return {
|
|
3654
|
+
totalTasks: tasks.length,
|
|
3655
|
+
completedTasks,
|
|
3656
|
+
activeAgents,
|
|
3657
|
+
totalCostUsd,
|
|
3658
|
+
totalInputTokens,
|
|
3659
|
+
totalOutputTokens,
|
|
3660
|
+
totalCacheReadTokens,
|
|
3661
|
+
totalCacheCreationTokens,
|
|
3662
|
+
uptimeMs,
|
|
3663
|
+
agents
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* Format a token count for display (e.g. 1234567 → "1.23M", 45678 → "45.7k").
|
|
3668
|
+
*/
|
|
3669
|
+
formatTokenCount(tokens) {
|
|
3670
|
+
if (tokens >= 1e6) {
|
|
3671
|
+
return `${(tokens / 1e6).toFixed(2)}M`;
|
|
3672
|
+
}
|
|
3673
|
+
if (tokens >= 1e3) {
|
|
3674
|
+
return `${(tokens / 1e3).toFixed(1)}k`;
|
|
3675
|
+
}
|
|
3676
|
+
return String(tokens);
|
|
3677
|
+
}
|
|
3678
|
+
/**
|
|
3679
|
+
* Build a summary of the completed project for reporting.
|
|
3680
|
+
*/
|
|
3681
|
+
buildCompletionSummary(tasks) {
|
|
3682
|
+
const agentStates = this.agentPool ? this.agentPool.getAllAgents().map((a) => a.currentState) : [];
|
|
3683
|
+
const agentTaskCounts = /* @__PURE__ */ new Map();
|
|
3684
|
+
for (const task of tasks) {
|
|
3685
|
+
if (task.assignee && task.status === "done") {
|
|
3686
|
+
agentTaskCounts.set(task.assignee, (agentTaskCounts.get(task.assignee) ?? 0) + 1);
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
const agentSummaries = agentStates.map((a) => ({
|
|
3690
|
+
agentId: a.config.id,
|
|
3691
|
+
name: a.config.name,
|
|
3692
|
+
role: a.config.role,
|
|
3693
|
+
tasksCompleted: agentTaskCounts.get(a.config.id) ?? 0,
|
|
3694
|
+
costUsd: a.totalCostUsd,
|
|
3695
|
+
inputTokens: a.totalInputTokens ?? 0,
|
|
3696
|
+
outputTokens: a.totalOutputTokens ?? 0
|
|
3697
|
+
}));
|
|
3698
|
+
const doneTasks = tasks.filter((t) => t.status === "done").length;
|
|
3699
|
+
const cancelledTasks = tasks.filter((t) => t.status === "cancelled").length;
|
|
3700
|
+
return {
|
|
3701
|
+
totalTasks: tasks.length,
|
|
3702
|
+
doneTasks,
|
|
3703
|
+
cancelledTasks,
|
|
3704
|
+
totalCostUsd: agentStates.reduce((sum, a) => sum + a.totalCostUsd, 0),
|
|
3705
|
+
totalInputTokens: agentStates.reduce((sum, a) => sum + (a.totalInputTokens ?? 0), 0),
|
|
3706
|
+
totalOutputTokens: agentStates.reduce((sum, a) => sum + (a.totalOutputTokens ?? 0), 0),
|
|
3707
|
+
totalCacheReadTokens: agentStates.reduce((sum, a) => sum + (a.totalCacheReadTokens ?? 0), 0),
|
|
3708
|
+
totalCacheCreationTokens: agentStates.reduce((sum, a) => sum + (a.totalCacheCreationTokens ?? 0), 0),
|
|
3709
|
+
uptimeMs: this.startedAt ? Date.now() - this.startedAt : 0,
|
|
3710
|
+
agentSummaries,
|
|
3711
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
/**
|
|
3715
|
+
* Approve the architecture and advance to development phase.
|
|
3716
|
+
*/
|
|
3717
|
+
approveArchitecture() {
|
|
3718
|
+
if (this.architectureApprovalPending) {
|
|
3719
|
+
this.architectureApprovalPending = false;
|
|
3720
|
+
this.phase = "development";
|
|
3721
|
+
this.emit("phase:changed", "development");
|
|
3722
|
+
this.emitActivity("phase-changed", "Architecture approved, advancing to development phase", {
|
|
3723
|
+
fromPhase: "architecture",
|
|
3724
|
+
toPhase: "development"
|
|
3725
|
+
});
|
|
3726
|
+
logger9.info("Architecture approved, advancing to development phase");
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
/**
|
|
3730
|
+
* Get the current orchestration phase.
|
|
3731
|
+
*/
|
|
3732
|
+
getPhase() {
|
|
3733
|
+
return this.phase;
|
|
3734
|
+
}
|
|
3735
|
+
decidePlan(proposalId, decision, modifications) {
|
|
3736
|
+
this.feedbackLoop?.decidePlan(proposalId, decision, modifications);
|
|
3737
|
+
}
|
|
3738
|
+
answerQuestion(questionId, answer) {
|
|
3739
|
+
this.feedbackLoop?.answerQuestion(questionId, answer);
|
|
3740
|
+
}
|
|
3741
|
+
acknowledgeMilestone(milestoneId) {
|
|
3742
|
+
this.feedbackLoop?.acknowledgeMilestone(milestoneId);
|
|
3743
|
+
}
|
|
3744
|
+
setInteractionMode(mode) {
|
|
3745
|
+
this.feedbackLoop?.setMode(mode);
|
|
3746
|
+
}
|
|
3747
|
+
getInteractionMode() {
|
|
3748
|
+
return this.feedbackLoop?.interactionMode ?? "supervised";
|
|
3749
|
+
}
|
|
3750
|
+
getPendingPlan() {
|
|
3751
|
+
return this.feedbackLoop?.getPendingPlan() ?? null;
|
|
3752
|
+
}
|
|
3753
|
+
getPendingQuestions() {
|
|
3754
|
+
return this.feedbackLoop?.getPendingQuestions() ?? [];
|
|
3755
|
+
}
|
|
3756
|
+
/**
|
|
3757
|
+
* Get processed messages for a specific task (for task detail view).
|
|
3758
|
+
*/
|
|
3759
|
+
async getTaskMessages(taskId) {
|
|
3760
|
+
if (!this.broker) return [];
|
|
3761
|
+
return this.broker.getProcessedMessagesForTask(taskId);
|
|
3762
|
+
}
|
|
3763
|
+
};
|
|
3764
|
+
|
|
3765
|
+
// src/project/config.ts
|
|
3766
|
+
import { z } from "zod";
|
|
3767
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
3768
|
+
import { parse as parseYaml } from "yaml";
|
|
3769
|
+
var permissionModeSchema = z.enum(["default", "acceptEdits", "bypassPermissions"]);
|
|
3770
|
+
var agentRoleConfigSchema = z.object({
|
|
3771
|
+
role: z.enum(["orchestrator", "project-manager", "architect", "developer", "designer", "qa-engineer", "devops", "technical-writer", "code-reviewer"]),
|
|
3772
|
+
count: z.number().int().min(1),
|
|
3773
|
+
model: z.string().optional(),
|
|
3774
|
+
systemPromptFile: z.string().optional(),
|
|
3775
|
+
systemPromptOverride: z.string().optional(),
|
|
3776
|
+
allowedTools: z.array(z.string()).optional(),
|
|
3777
|
+
maxBudgetUsd: z.number().positive().optional(),
|
|
3778
|
+
permissionMode: permissionModeSchema.optional(),
|
|
3779
|
+
claudeArgs: z.array(z.string()).optional(),
|
|
3780
|
+
claudeEnv: z.record(z.string()).optional(),
|
|
3781
|
+
maxTurns: z.number().int().positive().optional()
|
|
3782
|
+
});
|
|
3783
|
+
var projectSettingsSchema = z.object({
|
|
3784
|
+
workingDirectory: z.string().default("."),
|
|
3785
|
+
todoFile: z.string().default("todo.md"),
|
|
3786
|
+
messagesDirectory: z.string().default(".maestro/messages"),
|
|
3787
|
+
logsDirectory: z.string().default(".maestro/logs"),
|
|
3788
|
+
defaultModel: z.string().default("sonnet"),
|
|
3789
|
+
maxConcurrentAgents: z.number().int().min(1).default(5),
|
|
3790
|
+
pollIntervalMs: z.number().int().min(500).default(5e3),
|
|
3791
|
+
maxTotalBudgetUsd: z.number().positive().optional(),
|
|
3792
|
+
claudeCommand: z.string().optional(),
|
|
3793
|
+
claudeArgs: z.array(z.string()).optional(),
|
|
3794
|
+
claudeEnv: z.record(z.string()).optional(),
|
|
3795
|
+
feedback: z.object({
|
|
3796
|
+
interactionMode: z.enum(["unattended", "supervised", "interactive"]).default("supervised"),
|
|
3797
|
+
progressReportIntervalMs: z.number().int().min(1e3).default(6e4),
|
|
3798
|
+
milestonePercentages: z.array(z.number().min(0).max(100)).default([25, 50, 75, 100]),
|
|
3799
|
+
questionTimeoutMs: z.number().int().min(1e3).default(3e5),
|
|
3800
|
+
requirePlanApproval: z.boolean().default(true)
|
|
3801
|
+
}).optional()
|
|
3802
|
+
});
|
|
3803
|
+
var techStackSchema = z.object({
|
|
3804
|
+
frontend: z.string().optional(),
|
|
3805
|
+
uiLibrary: z.string().optional(),
|
|
3806
|
+
backend: z.string().optional(),
|
|
3807
|
+
database: z.string().optional(),
|
|
3808
|
+
other: z.string().optional()
|
|
3809
|
+
}).optional();
|
|
3810
|
+
var projectConfigSchema = z.object({
|
|
3811
|
+
name: z.string().min(1),
|
|
3812
|
+
description: z.string(),
|
|
3813
|
+
version: z.string(),
|
|
3814
|
+
agents: z.array(agentRoleConfigSchema).min(1),
|
|
3815
|
+
settings: projectSettingsSchema.default({}),
|
|
3816
|
+
techStack: techStackSchema
|
|
3817
|
+
});
|
|
3818
|
+
async function loadProjectConfig(filePath) {
|
|
3819
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
3820
|
+
const parsed = parseYaml(raw);
|
|
3821
|
+
return validateProjectConfig(parsed);
|
|
3822
|
+
}
|
|
3823
|
+
function validateProjectConfig(config) {
|
|
3824
|
+
return projectConfigSchema.parse(config);
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
// src/project/workspace.ts
|
|
3828
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
3829
|
+
import path2 from "path";
|
|
3830
|
+
var WorkspaceManager = class {
|
|
3831
|
+
baseDir;
|
|
3832
|
+
config = null;
|
|
3833
|
+
constructor(baseDir) {
|
|
3834
|
+
this.baseDir = path2.resolve(baseDir);
|
|
3835
|
+
}
|
|
3836
|
+
/**
|
|
3837
|
+
* Create all runtime directories required by the project.
|
|
3838
|
+
*
|
|
3839
|
+
* This creates the top-level .maestro directory, the messages directory,
|
|
3840
|
+
* the logs directory, and per-agent inbox/outbox directories for every
|
|
3841
|
+
* agent defined in the project config.
|
|
3842
|
+
*/
|
|
3843
|
+
async initialize(config) {
|
|
3844
|
+
this.config = config;
|
|
3845
|
+
const messagesDir = this.getMessagesDir();
|
|
3846
|
+
const logsDir = this.getLogsDir();
|
|
3847
|
+
await mkdir4(messagesDir, { recursive: true });
|
|
3848
|
+
await mkdir4(logsDir, { recursive: true });
|
|
3849
|
+
for (const agentRoleConfig of config.agents) {
|
|
3850
|
+
for (let i = 0; i < agentRoleConfig.count; i++) {
|
|
3851
|
+
const agentId = agentRoleConfig.count === 1 ? agentRoleConfig.role : `${agentRoleConfig.role}-${i + 1}`;
|
|
3852
|
+
await mkdir4(this.getAgentInboxPath(agentId), { recursive: true });
|
|
3853
|
+
await mkdir4(this.getAgentOutboxPath(agentId), { recursive: true });
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
/**
|
|
3858
|
+
* Get the absolute path to the project's todo file.
|
|
3859
|
+
*/
|
|
3860
|
+
getTodoFilePath() {
|
|
3861
|
+
const todoFile = this.config?.settings.todoFile ?? "todo.md";
|
|
3862
|
+
return path2.resolve(this.baseDir, todoFile);
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Get the absolute path to the messages directory.
|
|
3866
|
+
*/
|
|
3867
|
+
getMessagesDir() {
|
|
3868
|
+
const messagesDir = this.config?.settings.messagesDirectory ?? ".maestro/messages";
|
|
3869
|
+
return path2.resolve(this.baseDir, messagesDir);
|
|
3870
|
+
}
|
|
3871
|
+
/**
|
|
3872
|
+
* Get the absolute path to the logs directory.
|
|
3873
|
+
*/
|
|
3874
|
+
getLogsDir() {
|
|
3875
|
+
const logsDir = this.config?.settings.logsDirectory ?? ".maestro/logs";
|
|
3876
|
+
return path2.resolve(this.baseDir, logsDir);
|
|
3877
|
+
}
|
|
3878
|
+
/**
|
|
3879
|
+
* Get the absolute path to a specific agent's log file.
|
|
3880
|
+
*/
|
|
3881
|
+
getAgentLogPath(agentId) {
|
|
3882
|
+
return path2.resolve(this.getLogsDir(), `${agentId}.log`);
|
|
3883
|
+
}
|
|
3884
|
+
/**
|
|
3885
|
+
* Get the absolute path to a specific agent's inbox directory.
|
|
3886
|
+
*/
|
|
3887
|
+
getAgentInboxPath(agentId) {
|
|
3888
|
+
return path2.resolve(this.getMessagesDir(), agentId, "inbox");
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Get the absolute path to a specific agent's outbox directory.
|
|
3892
|
+
*/
|
|
3893
|
+
getAgentOutboxPath(agentId) {
|
|
3894
|
+
return path2.resolve(this.getMessagesDir(), agentId, "outbox");
|
|
3895
|
+
}
|
|
3896
|
+
};
|
|
3897
|
+
|
|
3898
|
+
// src/index.ts
|
|
3899
|
+
init_logger();
|
|
3900
|
+
init_id();
|
|
3901
|
+
export {
|
|
3902
|
+
AgentPool,
|
|
3903
|
+
AgentProcess,
|
|
3904
|
+
FeedbackLoop,
|
|
3905
|
+
InboxWatcher,
|
|
3906
|
+
MessageBroker,
|
|
3907
|
+
Orchestrator,
|
|
3908
|
+
PIPELINE_STAGE_ORDER,
|
|
3909
|
+
PIPELINE_STAGE_ROLES,
|
|
3910
|
+
Planner,
|
|
3911
|
+
ProgressTracker,
|
|
3912
|
+
ProjectManagerScanner,
|
|
3913
|
+
ROLE_DESCRIPTIONS,
|
|
3914
|
+
Scheduler,
|
|
3915
|
+
TaskManager,
|
|
3916
|
+
TaskParser,
|
|
3917
|
+
TaskPipeline,
|
|
3918
|
+
TaskWriter,
|
|
3919
|
+
WorkspaceManager,
|
|
3920
|
+
createLogger,
|
|
3921
|
+
generateId,
|
|
3922
|
+
generateTaskId,
|
|
3923
|
+
getDefaultSystemPrompt,
|
|
3924
|
+
loadAgentConfigs,
|
|
3925
|
+
loadProjectConfig,
|
|
3926
|
+
resetTaskCounter,
|
|
3927
|
+
validateProjectConfig
|
|
3928
|
+
};
|
|
3929
|
+
//# sourceMappingURL=index.js.map
|