@cleocode/core 2026.3.73 → 2026.3.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cleo.d.ts.map +1 -1
- package/dist/index.js +485 -176
- package/dist/index.js.map +4 -4
- package/dist/nexus/index.d.ts +2 -0
- package/dist/nexus/index.d.ts.map +1 -1
- package/dist/nexus/workspace.d.ts +128 -0
- package/dist/nexus/workspace.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/cleo.ts +14 -0
- package/src/nexus/index.ts +15 -0
- package/src/nexus/workspace.ts +508 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NEXUS Workspace — Cross-project orchestration operations.
|
|
3
|
+
*
|
|
4
|
+
* Implements ORCH-PLAN Phase B:
|
|
5
|
+
* - B.2: nexus.route(directiveEvent) — dispatch Conduit directives to the correct project
|
|
6
|
+
* - B.3: nexus.workspace.status() — aggregated cross-project task view
|
|
7
|
+
* - B.4: nexus.workspace.agents() — cross-project agent registry view
|
|
8
|
+
*
|
|
9
|
+
* Security: project-level ACL enforced on all routing operations (HIGH-02).
|
|
10
|
+
* Rate limiting: per-agent throttling on route operations (MEDIUM-05).
|
|
11
|
+
* Audit: all routing operations logged (LOW-06).
|
|
12
|
+
*
|
|
13
|
+
* @module nexus/workspace
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ConduitMessage } from '@cleocode/contracts';
|
|
17
|
+
import { ExitCode } from '@cleocode/contracts';
|
|
18
|
+
import { CleoError } from '../errors.js';
|
|
19
|
+
import type { DataAccessor } from '../store/data-accessor.js';
|
|
20
|
+
import { getAccessor } from '../store/data-accessor.js';
|
|
21
|
+
import { type NexusProject, nexusList } from './registry.js';
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Types
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/** Parsed directive from a Conduit message. */
|
|
28
|
+
export interface ParsedDirective {
|
|
29
|
+
/** The directive verb (claim, done, blocked, action, etc.). */
|
|
30
|
+
verb: string;
|
|
31
|
+
/** Task references extracted from the message (e.g., T042, T1234). */
|
|
32
|
+
taskRefs: string[];
|
|
33
|
+
/** The agent ID that sent the directive. */
|
|
34
|
+
agentId: string;
|
|
35
|
+
/** Original message ID for audit trail. */
|
|
36
|
+
messageId: string;
|
|
37
|
+
/** Timestamp of the directive. */
|
|
38
|
+
timestamp: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Result of routing a directive to a project. */
|
|
42
|
+
export interface RouteResult {
|
|
43
|
+
/** Whether the routing succeeded. */
|
|
44
|
+
success: boolean;
|
|
45
|
+
/** The project that was routed to. */
|
|
46
|
+
project: string;
|
|
47
|
+
/** The project's filesystem path. */
|
|
48
|
+
projectPath: string;
|
|
49
|
+
/** The task that was affected. */
|
|
50
|
+
taskId: string;
|
|
51
|
+
/** What operation was performed. */
|
|
52
|
+
operation: string;
|
|
53
|
+
/** Error message if routing failed. */
|
|
54
|
+
error?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Aggregated task status across all projects. */
|
|
58
|
+
export interface WorkspaceStatus {
|
|
59
|
+
/** Total projects in the workspace. */
|
|
60
|
+
projectCount: number;
|
|
61
|
+
/** Per-project task summaries. */
|
|
62
|
+
projects: WorkspaceProjectSummary[];
|
|
63
|
+
/** Aggregated totals. */
|
|
64
|
+
totals: {
|
|
65
|
+
pending: number;
|
|
66
|
+
active: number;
|
|
67
|
+
done: number;
|
|
68
|
+
total: number;
|
|
69
|
+
};
|
|
70
|
+
/** When the status was computed. */
|
|
71
|
+
computedAt: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Task summary for a single project. */
|
|
75
|
+
export interface WorkspaceProjectSummary {
|
|
76
|
+
/** Project name. */
|
|
77
|
+
name: string;
|
|
78
|
+
/** Project path. */
|
|
79
|
+
path: string;
|
|
80
|
+
/** Task counts by status. */
|
|
81
|
+
counts: {
|
|
82
|
+
pending: number;
|
|
83
|
+
active: number;
|
|
84
|
+
done: number;
|
|
85
|
+
total: number;
|
|
86
|
+
};
|
|
87
|
+
/** Health status from the Nexus registry. */
|
|
88
|
+
health: string;
|
|
89
|
+
/** Last sync time. */
|
|
90
|
+
lastSync: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Agent info aggregated across projects. */
|
|
94
|
+
export interface WorkspaceAgent {
|
|
95
|
+
/** Agent instance ID. */
|
|
96
|
+
agentId: string;
|
|
97
|
+
/** Agent type. */
|
|
98
|
+
agentType: string;
|
|
99
|
+
/** Current status. */
|
|
100
|
+
status: string;
|
|
101
|
+
/** Which project this agent is registered in. */
|
|
102
|
+
project: string;
|
|
103
|
+
/** Current task (if any). */
|
|
104
|
+
taskId: string | null;
|
|
105
|
+
/** Last heartbeat. */
|
|
106
|
+
lastHeartbeat: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Project-level ACL entry. */
|
|
110
|
+
export interface ProjectACL {
|
|
111
|
+
/** Agent IDs with write access. */
|
|
112
|
+
authorizedAgents: string[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Rate limiting (MEDIUM-05)
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
const RATE_LIMIT_WINDOW_MS = 60_000;
|
|
120
|
+
const RATE_LIMIT_MAX_OPS = 100;
|
|
121
|
+
|
|
122
|
+
/** Per-agent operation counters for rate limiting. */
|
|
123
|
+
const rateLimitCounters = new Map<string, { count: number; windowStart: number }>();
|
|
124
|
+
|
|
125
|
+
/** Check if an agent is rate-limited. Throws if exceeded. */
|
|
126
|
+
function checkRateLimit(agentId: string): void {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
const entry = rateLimitCounters.get(agentId);
|
|
129
|
+
|
|
130
|
+
if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
|
|
131
|
+
rateLimitCounters.set(agentId, { count: 1, windowStart: now });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
entry.count++;
|
|
136
|
+
if (entry.count > RATE_LIMIT_MAX_OPS) {
|
|
137
|
+
throw new CleoError(
|
|
138
|
+
ExitCode.GENERAL_ERROR,
|
|
139
|
+
`Agent '${agentId}' exceeded rate limit: ${RATE_LIMIT_MAX_OPS} routing ops per ${RATE_LIMIT_WINDOW_MS / 1000}s`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// ACL (HIGH-02)
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
/** Default ACL: all agents have access (open by default, restrict as needed). */
|
|
149
|
+
const DEFAULT_ACL: ProjectACL = { authorizedAgents: ['*'] };
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Load ACL for a project. Reads from .cleo/config.json authorizedAgents field.
|
|
153
|
+
* Falls back to open access if not configured.
|
|
154
|
+
*/
|
|
155
|
+
async function loadProjectACL(projectPath: string): Promise<ProjectACL> {
|
|
156
|
+
try {
|
|
157
|
+
const { loadConfig } = await import('../config.js');
|
|
158
|
+
const config = await loadConfig(projectPath);
|
|
159
|
+
const agents = (config as unknown as Record<string, unknown>)?.authorizedAgents;
|
|
160
|
+
if (Array.isArray(agents) && agents.length > 0) {
|
|
161
|
+
return { authorizedAgents: agents as string[] };
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// Config load failure = open access
|
|
165
|
+
}
|
|
166
|
+
return DEFAULT_ACL;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Check if an agent is authorized to mutate a project. */
|
|
170
|
+
function isAuthorized(acl: ProjectACL, agentId: string): boolean {
|
|
171
|
+
if (acl.authorizedAgents.includes('*')) return true;
|
|
172
|
+
return acl.authorizedAgents.includes(agentId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// B.2: nexus.route() — Directive routing
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/** Task reference pattern: T followed by digits (T001, T42, T1234). */
|
|
180
|
+
const TASK_REF_PATTERN = /\bT(\d+)\b/g;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Parse a Conduit message into a structured directive.
|
|
184
|
+
*
|
|
185
|
+
* Extracts directive verbs (/claim, /done, /blocked) and task references
|
|
186
|
+
* from the message content and metadata.
|
|
187
|
+
*/
|
|
188
|
+
export function parseDirective(message: ConduitMessage): ParsedDirective | null {
|
|
189
|
+
const content = message.content;
|
|
190
|
+
|
|
191
|
+
// Extract directive verb (first /word in content)
|
|
192
|
+
const verbMatch = content.match(/^\/(\w+)/);
|
|
193
|
+
if (!verbMatch) return null;
|
|
194
|
+
|
|
195
|
+
const verb = verbMatch[1];
|
|
196
|
+
|
|
197
|
+
// Extract task refs from content
|
|
198
|
+
const taskRefs: string[] = [];
|
|
199
|
+
const pattern = new RegExp(TASK_REF_PATTERN.source, 'g');
|
|
200
|
+
for (const m of content.matchAll(pattern)) {
|
|
201
|
+
taskRefs.push(`T${m[1]}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Also check metadata.taskRefs from SignalDock extraction (Phase A.5)
|
|
205
|
+
const metaRefs = (message.metadata as Record<string, unknown>)?.taskRefs;
|
|
206
|
+
if (Array.isArray(metaRefs)) {
|
|
207
|
+
for (const ref of metaRefs) {
|
|
208
|
+
if (typeof ref === 'string' && !taskRefs.includes(ref)) {
|
|
209
|
+
taskRefs.push(ref);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (taskRefs.length === 0) return null;
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
verb,
|
|
218
|
+
taskRefs,
|
|
219
|
+
agentId: message.from,
|
|
220
|
+
messageId: message.id,
|
|
221
|
+
timestamp: message.timestamp,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Map directive verbs to CLEO task operations. */
|
|
226
|
+
const VERB_TO_OPERATION: Record<string, string> = {
|
|
227
|
+
claim: 'tasks.start',
|
|
228
|
+
done: 'tasks.complete',
|
|
229
|
+
complete: 'tasks.complete',
|
|
230
|
+
blocked: 'tasks.update', // Update status to blocked
|
|
231
|
+
start: 'tasks.start',
|
|
232
|
+
stop: 'tasks.stop',
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Route a Conduit directive to the correct project's CLEO instance.
|
|
237
|
+
*
|
|
238
|
+
* Resolves which project owns the referenced task, checks ACL,
|
|
239
|
+
* and dispatches the appropriate CLEO operation.
|
|
240
|
+
*
|
|
241
|
+
* @param directive - Parsed directive from a Conduit message
|
|
242
|
+
* @returns Array of route results (one per task reference)
|
|
243
|
+
*/
|
|
244
|
+
export async function routeDirective(directive: ParsedDirective): Promise<RouteResult[]> {
|
|
245
|
+
// Rate limit check (MEDIUM-05)
|
|
246
|
+
checkRateLimit(directive.agentId);
|
|
247
|
+
|
|
248
|
+
const results: RouteResult[] = [];
|
|
249
|
+
const operation = VERB_TO_OPERATION[directive.verb];
|
|
250
|
+
|
|
251
|
+
if (!operation) {
|
|
252
|
+
// Unknown verb — not routable, just skip
|
|
253
|
+
return results;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const projects = await nexusList();
|
|
257
|
+
|
|
258
|
+
for (const taskRef of directive.taskRefs) {
|
|
259
|
+
const result = await routeSingleTask(taskRef, directive, operation, projects);
|
|
260
|
+
results.push(result);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return results;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Route a single task reference to its project and execute the operation. */
|
|
267
|
+
async function routeSingleTask(
|
|
268
|
+
taskId: string,
|
|
269
|
+
directive: ParsedDirective,
|
|
270
|
+
operation: string,
|
|
271
|
+
projects: NexusProject[],
|
|
272
|
+
): Promise<RouteResult> {
|
|
273
|
+
// Find which project owns this task
|
|
274
|
+
let targetProject: NexusProject | null = null;
|
|
275
|
+
let targetAccessor: DataAccessor | null = null;
|
|
276
|
+
|
|
277
|
+
for (const project of projects) {
|
|
278
|
+
try {
|
|
279
|
+
const acc = await getAccessor(project.path);
|
|
280
|
+
const { tasks } = await acc.queryTasks({});
|
|
281
|
+
const task = tasks.find((t) => t.id === taskId);
|
|
282
|
+
if (task) {
|
|
283
|
+
targetProject = project;
|
|
284
|
+
targetAccessor = acc;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
} catch {}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!targetProject || !targetAccessor) {
|
|
291
|
+
return {
|
|
292
|
+
success: false,
|
|
293
|
+
project: 'unknown',
|
|
294
|
+
projectPath: '',
|
|
295
|
+
taskId,
|
|
296
|
+
operation,
|
|
297
|
+
error: `Task ${taskId} not found in any registered project`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ACL check (HIGH-02)
|
|
302
|
+
const acl = await loadProjectACL(targetProject.path);
|
|
303
|
+
if (!isAuthorized(acl, directive.agentId)) {
|
|
304
|
+
return {
|
|
305
|
+
success: false,
|
|
306
|
+
project: targetProject.name,
|
|
307
|
+
projectPath: targetProject.path,
|
|
308
|
+
taskId,
|
|
309
|
+
operation,
|
|
310
|
+
error: `Agent '${directive.agentId}' not authorized to mutate project '${targetProject.name}'`,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Execute the operation
|
|
315
|
+
try {
|
|
316
|
+
await executeOperation(operation, taskId, targetProject.path, targetAccessor, directive);
|
|
317
|
+
|
|
318
|
+
// Audit log (LOW-06)
|
|
319
|
+
await logRouteAudit(directive, targetProject.name, taskId, operation, true);
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
success: true,
|
|
323
|
+
project: targetProject.name,
|
|
324
|
+
projectPath: targetProject.path,
|
|
325
|
+
taskId,
|
|
326
|
+
operation,
|
|
327
|
+
};
|
|
328
|
+
} catch (err) {
|
|
329
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
330
|
+
|
|
331
|
+
// Audit log for failures too
|
|
332
|
+
await logRouteAudit(directive, targetProject.name, taskId, operation, false, errorMsg);
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
success: false,
|
|
336
|
+
project: targetProject.name,
|
|
337
|
+
projectPath: targetProject.path,
|
|
338
|
+
taskId,
|
|
339
|
+
operation,
|
|
340
|
+
error: errorMsg,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Execute a CLEO operation on a project's task. */
|
|
346
|
+
async function executeOperation(
|
|
347
|
+
operation: string,
|
|
348
|
+
taskId: string,
|
|
349
|
+
projectPath: string,
|
|
350
|
+
accessor: DataAccessor,
|
|
351
|
+
directive: ParsedDirective,
|
|
352
|
+
): Promise<void> {
|
|
353
|
+
switch (operation) {
|
|
354
|
+
case 'tasks.start': {
|
|
355
|
+
const { startTask } = await import('../task-work/index.js');
|
|
356
|
+
await startTask(taskId, projectPath, accessor);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case 'tasks.complete': {
|
|
360
|
+
const { completeTask } = await import('../tasks/complete.js');
|
|
361
|
+
await completeTask(
|
|
362
|
+
{ taskId, notes: `Completed via Conduit directive from ${directive.agentId}` },
|
|
363
|
+
projectPath,
|
|
364
|
+
accessor,
|
|
365
|
+
);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
case 'tasks.stop': {
|
|
369
|
+
const { stopTask } = await import('../task-work/index.js');
|
|
370
|
+
await stopTask(projectPath, accessor);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case 'tasks.update': {
|
|
374
|
+
const { updateTask } = await import('../tasks/update.js');
|
|
375
|
+
await updateTask(
|
|
376
|
+
{ taskId, notes: `Marked blocked via Conduit directive from ${directive.agentId}` },
|
|
377
|
+
projectPath,
|
|
378
|
+
accessor,
|
|
379
|
+
);
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Log a routing operation to the audit trail. */
|
|
386
|
+
async function logRouteAudit(
|
|
387
|
+
directive: ParsedDirective,
|
|
388
|
+
projectName: string,
|
|
389
|
+
taskId: string,
|
|
390
|
+
operation: string,
|
|
391
|
+
success: boolean,
|
|
392
|
+
error?: string,
|
|
393
|
+
): Promise<void> {
|
|
394
|
+
try {
|
|
395
|
+
const { getLogger } = await import('../logger.js');
|
|
396
|
+
const log = getLogger('nexus.route');
|
|
397
|
+
const level = success ? 'info' : 'warn';
|
|
398
|
+
log[level](
|
|
399
|
+
{
|
|
400
|
+
directive: directive.verb,
|
|
401
|
+
agentId: directive.agentId,
|
|
402
|
+
messageId: directive.messageId,
|
|
403
|
+
project: projectName,
|
|
404
|
+
taskId,
|
|
405
|
+
operation,
|
|
406
|
+
success,
|
|
407
|
+
error,
|
|
408
|
+
},
|
|
409
|
+
`Conduit directive routed: ${directive.verb} ${taskId} → ${projectName} (${success ? 'OK' : 'FAILED'})`,
|
|
410
|
+
);
|
|
411
|
+
} catch {
|
|
412
|
+
// Audit logging is best-effort
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ============================================================================
|
|
417
|
+
// B.3: nexus.workspace.status() — Aggregated view
|
|
418
|
+
// ============================================================================
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get aggregated task status across all registered projects.
|
|
422
|
+
*
|
|
423
|
+
* Returns per-project task counts and workspace-wide totals.
|
|
424
|
+
* Respects project permissions — only includes readable projects.
|
|
425
|
+
*/
|
|
426
|
+
export async function workspaceStatus(): Promise<WorkspaceStatus> {
|
|
427
|
+
const projects = await nexusList();
|
|
428
|
+
const summaries: WorkspaceProjectSummary[] = [];
|
|
429
|
+
const totals = { pending: 0, active: 0, done: 0, total: 0 };
|
|
430
|
+
|
|
431
|
+
for (const project of projects) {
|
|
432
|
+
try {
|
|
433
|
+
const acc = await getAccessor(project.path);
|
|
434
|
+
const { tasks } = await acc.queryTasks({});
|
|
435
|
+
|
|
436
|
+
const counts = {
|
|
437
|
+
pending: tasks.filter((t) => t.status === 'pending').length,
|
|
438
|
+
active: tasks.filter((t) => t.status === 'active').length,
|
|
439
|
+
done: tasks.filter((t) => t.status === 'done').length,
|
|
440
|
+
total: tasks.length,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
summaries.push({
|
|
444
|
+
name: project.name,
|
|
445
|
+
path: project.path,
|
|
446
|
+
counts,
|
|
447
|
+
health: project.healthStatus,
|
|
448
|
+
lastSync: project.lastSync,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
totals.pending += counts.pending;
|
|
452
|
+
totals.active += counts.active;
|
|
453
|
+
totals.done += counts.done;
|
|
454
|
+
totals.total += counts.total;
|
|
455
|
+
} catch {
|
|
456
|
+
// Skip unreachable projects
|
|
457
|
+
summaries.push({
|
|
458
|
+
name: project.name,
|
|
459
|
+
path: project.path,
|
|
460
|
+
counts: { pending: 0, active: 0, done: 0, total: 0 },
|
|
461
|
+
health: 'unreachable',
|
|
462
|
+
lastSync: project.lastSync,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
projectCount: projects.length,
|
|
469
|
+
projects: summaries,
|
|
470
|
+
totals,
|
|
471
|
+
computedAt: new Date().toISOString(),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ============================================================================
|
|
476
|
+
// B.4: nexus.workspace.agents() — Cross-project agent view
|
|
477
|
+
// ============================================================================
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Get all agents registered across all projects.
|
|
481
|
+
*
|
|
482
|
+
* Queries each project's agent_instances table and returns a unified list.
|
|
483
|
+
*/
|
|
484
|
+
export async function workspaceAgents(): Promise<WorkspaceAgent[]> {
|
|
485
|
+
const projects = await nexusList();
|
|
486
|
+
const agents: WorkspaceAgent[] = [];
|
|
487
|
+
|
|
488
|
+
for (const project of projects) {
|
|
489
|
+
try {
|
|
490
|
+
const { listAgentInstances } = await import('../agents/registry.js');
|
|
491
|
+
const instances = await listAgentInstances(undefined, project.path);
|
|
492
|
+
for (const inst of instances) {
|
|
493
|
+
agents.push({
|
|
494
|
+
agentId: inst.id,
|
|
495
|
+
agentType: inst.agentType,
|
|
496
|
+
status: inst.status,
|
|
497
|
+
project: project.name,
|
|
498
|
+
taskId: inst.taskId ?? null,
|
|
499
|
+
lastHeartbeat: inst.lastHeartbeat,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
} catch {
|
|
503
|
+
// Skip projects without agent support
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return agents;
|
|
508
|
+
}
|