nexus-prime 6.0.2 → 6.2.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/dist/agents/adapters/mcp/dispatch.js +22 -7
- package/dist/agents/adapters/mcp/handlers/orchestration.js +64 -29
- package/dist/agents/adapters/mcp/tool-health.d.ts +17 -0
- package/dist/agents/adapters/mcp/tool-health.js +57 -0
- package/dist/agents/adapters/mcp/util/walk.d.ts +9 -2
- package/dist/agents/adapters/mcp/util/walk.js +27 -5
- package/dist/agents/adapters/mcp.js +6 -1
- package/dist/architects/bootstrap.js +52 -28
- package/dist/cli/hook.js +34 -2
- package/dist/cli/install-wizard.js +5 -1
- package/dist/dashboard/app/index.html +11 -0
- package/dist/dashboard/app/main.js +4 -0
- package/dist/dashboard/app/styles/workforce.css +4 -0
- package/dist/dashboard/app/views/board.js +37 -0
- package/dist/dashboard/app/views/workforce.js +69 -22
- package/dist/dashboard/routes/governance.js +15 -5
- package/dist/dashboard/routes/health.js +5 -0
- package/dist/dashboard/server.js +11 -0
- package/dist/dashboard/stream/sse-broker.js +2 -0
- package/dist/engines/event-bus.d.ts +13 -1
- package/dist/synapse/bootstrap.js +1 -1
- package/dist/synapse/mandate/pipeline.js +11 -1
- package/package.json +2 -1
|
@@ -13,6 +13,8 @@ import { handleGovernanceGroup } from './handlers/governance.js';
|
|
|
13
13
|
import { handleRuntimeGroup } from './handlers/runtime.js';
|
|
14
14
|
import { handleDiscoveryGroup } from './handlers/discovery.js';
|
|
15
15
|
import { handleMiscGroup } from './handlers/misc.js';
|
|
16
|
+
import { nexusEventBus } from '../../../engines/event-bus.js';
|
|
17
|
+
import { recordToolInvocation } from './tool-health.js';
|
|
16
18
|
/**
|
|
17
19
|
* Route a tool call to the appropriate handler group.
|
|
18
20
|
*
|
|
@@ -22,11 +24,24 @@ import { handleMiscGroup } from './handlers/misc.js';
|
|
|
22
24
|
*/
|
|
23
25
|
export async function dispatchMcpToolCall(hctx, request, args, ctx) {
|
|
24
26
|
const toolName = String(request.params?.name ?? '');
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
await
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
const startedAt = Date.now();
|
|
28
|
+
try {
|
|
29
|
+
// Try each group in priority order. Groups return undefined for unknown tools.
|
|
30
|
+
const result = (await handleOrchestrationGroup(toolName, hctx, request, args, ctx) ??
|
|
31
|
+
await handleMemoryGroup(toolName, hctx, request, args, ctx) ??
|
|
32
|
+
await handleGovernanceGroup(toolName, hctx, request, args, ctx) ??
|
|
33
|
+
await handleRuntimeGroup(toolName, hctx, request, args, ctx) ??
|
|
34
|
+
await handleDiscoveryGroup(toolName, hctx, request, args, ctx) ??
|
|
35
|
+
await handleMiscGroup(toolName, hctx, request, args, ctx));
|
|
36
|
+
const durationMs = Date.now() - startedAt;
|
|
37
|
+
recordToolInvocation(toolName, durationMs, 'ok');
|
|
38
|
+
nexusEventBus.emit('tool.invocation', { toolName, durationMs, status: 'ok' });
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const durationMs = Date.now() - startedAt;
|
|
43
|
+
recordToolInvocation(toolName, durationMs, 'error');
|
|
44
|
+
nexusEventBus.emit('tool.invocation', { toolName, durationMs, status: 'error' });
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
32
47
|
}
|
|
@@ -253,28 +253,11 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
253
253
|
formatBullets([
|
|
254
254
|
`Workspace: ${workspace.repoName} (${workspace.workspaceSource})`,
|
|
255
255
|
`Client: ${bootstrap.client?.displayName || bootstrap.client?.clientId || 'unknown'} (${bootstrap.client?.state || 'unknown'})`,
|
|
256
|
-
`Bootstrap depth: ${bootstrap.depth || bootstrapDepth}`,
|
|
257
256
|
`Memory recall: ${bootstrap.memoryRecall?.count ?? 0} result(s)`,
|
|
258
257
|
`Memory stats: prefrontal ${bootstrap.memoryStats?.prefrontal ?? 0} · hippocampus ${bootstrap.memoryStats?.hippocampus ?? 0} · cortex ${bootstrap.memoryStats?.cortex ?? 0}`,
|
|
259
258
|
`Recommended next step: ${bootstrap.recommendedNextStep || 'nexus_orchestrate'}`,
|
|
260
|
-
`Execution mode: ${bootstrap.recommendedExecutionMode || 'autonomous'}`,
|
|
261
259
|
`Token optimization: ${bootstrap.tokenOptimization?.autoApplied ? `auto-applied — saved ${Number(bootstrap.tokenOptimization?.planMetrics?.savings ?? 0).toLocaleString()} tokens (${bootstrap.tokenOptimization?.planMetrics?.pct ?? 0}% reduction)` : (bootstrap.tokenOptimization?.required ? 'required before broad reading' : 'not required yet')}`,
|
|
262
260
|
`Catalog health: ${bootstrap.catalogHealth?.overall || 'unknown'} · selected ${bootstrap.artifactSelectionAudit?.selected?.length || 0}`,
|
|
263
|
-
`Shortlist: ${bootstrap.shortlist?.skills?.slice(0, 3).join(', ') || 'none'} (skills), ${bootstrap.shortlist?.specialists?.slice(0, 3).join(', ') || 'none'} (specialists)`,
|
|
264
|
-
`Knowledge fabric: ${bootstrap.sourceMixRecommendation?.dominantSource || bootstrap.knowledgeFabric?.dominantSource || 'awaiting source mix'}`,
|
|
265
|
-
`RAG: ${bootstrap.ragCandidateStatus?.attachedCollections || 0} attached · ${bootstrap.ragCandidateStatus?.retrievedChunks || 0} retrieved`,
|
|
266
|
-
`Task graph: ${bootstrap.taskGraphPreview?.phases?.length || 0} phases · ${bootstrap.taskGraphPreview?.independentBranches || 0} branches`,
|
|
267
|
-
`Worker plan: ${bootstrap.workerPlanPreview?.totalWorkers || 0} workers planned`,
|
|
268
|
-
bootstrap.needsDeepBootstrap ? 'Deep preparation: still recommended before risky or cross-file work' : 'Deep preparation: not currently required',
|
|
269
|
-
`Payload ref: ${workspace.stateKey} · ${detailLevel}`,
|
|
270
|
-
`Session summary: ${bootstrap.sessionSummaryBootstrap?.savedTokens ? `reused ${Number(bootstrap.sessionSummaryBootstrap.savedTokens || 0).toLocaleString()} tokens from the previous visit` : 'none available yet'}`,
|
|
271
|
-
bootstrap.projectMemoryBootstrap?.count
|
|
272
|
-
? `Project memory: recovered ${bootstrap.projectMemoryBootstrap.count} prior project memories`
|
|
273
|
-
: 'Project memory: no prior project memories recovered',
|
|
274
|
-
bootstrap.autoGhostPass?.applied
|
|
275
|
-
? `Auto ghost-pass: ${bootstrap.autoGhostPass.riskAreas.length} risk area(s) · ${bootstrap.autoGhostPass.workerApproaches} approach(es)`
|
|
276
|
-
: `Auto ghost-pass: skipped`,
|
|
277
|
-
`Bootstrap status: ${bootstrap.clientBootstrapStatus?.clients?.length || 0} client manifests tracked`,
|
|
278
261
|
(() => {
|
|
279
262
|
try {
|
|
280
263
|
const _lic = getSharedLicenseManager().getStatus();
|
|
@@ -287,11 +270,30 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
287
270
|
return '';
|
|
288
271
|
}
|
|
289
272
|
})(),
|
|
273
|
+
...(detailLevel === 'debug' ? [
|
|
274
|
+
`Bootstrap depth: ${bootstrap.depth || bootstrapDepth}`,
|
|
275
|
+
`Execution mode: ${bootstrap.recommendedExecutionMode || 'autonomous'}`,
|
|
276
|
+
`Shortlist: ${bootstrap.shortlist?.skills?.slice(0, 3).join(', ') || 'none'} (skills), ${bootstrap.shortlist?.specialists?.slice(0, 3).join(', ') || 'none'} (specialists)`,
|
|
277
|
+
`Knowledge fabric: ${bootstrap.sourceMixRecommendation?.dominantSource || bootstrap.knowledgeFabric?.dominantSource || 'awaiting source mix'}`,
|
|
278
|
+
`RAG: ${bootstrap.ragCandidateStatus?.attachedCollections || 0} attached · ${bootstrap.ragCandidateStatus?.retrievedChunks || 0} retrieved`,
|
|
279
|
+
`Task graph: ${bootstrap.taskGraphPreview?.phases?.length || 0} phases · ${bootstrap.taskGraphPreview?.independentBranches || 0} branches`,
|
|
280
|
+
`Worker plan: ${bootstrap.workerPlanPreview?.totalWorkers || 0} workers planned`,
|
|
281
|
+
bootstrap.needsDeepBootstrap ? 'Deep preparation: still recommended before risky or cross-file work' : 'Deep preparation: not currently required',
|
|
282
|
+
`Payload ref: ${workspace.stateKey} · ${detailLevel}`,
|
|
283
|
+
`Session summary: ${bootstrap.sessionSummaryBootstrap?.savedTokens ? `reused ${Number(bootstrap.sessionSummaryBootstrap.savedTokens || 0).toLocaleString()} tokens from the previous visit` : 'none available yet'}`,
|
|
284
|
+
bootstrap.projectMemoryBootstrap?.count
|
|
285
|
+
? `Project memory: recovered ${bootstrap.projectMemoryBootstrap.count} prior project memories`
|
|
286
|
+
: 'Project memory: no prior project memories recovered',
|
|
287
|
+
bootstrap.autoGhostPass?.applied
|
|
288
|
+
? `Auto ghost-pass: ${bootstrap.autoGhostPass.riskAreas.length} risk area(s) · ${bootstrap.autoGhostPass.workerApproaches} approach(es)`
|
|
289
|
+
: `Auto ghost-pass: skipped`,
|
|
290
|
+
`Bootstrap status: ${bootstrap.clientBootstrapStatus?.clients?.length || 0} client manifests tracked`,
|
|
291
|
+
] : []),
|
|
290
292
|
]),
|
|
291
293
|
detailLevel === 'debug' && bootstrap.tokenOptimization?.autoApplied && bootstrap.tokenOptimization?.plan
|
|
292
294
|
? `Auto token plan\n\`\`\`txt\n${bootstrap.tokenOptimization.plan}\n\`\`\``
|
|
293
295
|
: '',
|
|
294
|
-
formatJsonDetails('Structured details', payload),
|
|
296
|
+
detailLevel === 'debug' ? formatJsonDetails('Structured details', payload) : '',
|
|
295
297
|
hctx.formatProtocolChecklist(),
|
|
296
298
|
].filter(Boolean).join('\n\n'),
|
|
297
299
|
}],
|
|
@@ -331,6 +333,36 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
331
333
|
const executionPreset = request.params.arguments?.executionPreset
|
|
332
334
|
? String(request.params.arguments.executionPreset)
|
|
333
335
|
: undefined;
|
|
336
|
+
// Auto-apply token optimization when explicit files are provided.
|
|
337
|
+
// Gives agents an upfront reading plan before orchestrate runs.
|
|
338
|
+
// No-files path relies on knowledge fabric (handled inside orchestrate).
|
|
339
|
+
let upfrontTokenNote = '';
|
|
340
|
+
if (files && files.length > 3) {
|
|
341
|
+
try {
|
|
342
|
+
const statLimit = pLimit(8);
|
|
343
|
+
const fileRefs = await Promise.all(files.map((p) => statLimit(async () => {
|
|
344
|
+
const resolved = hctx.resolveToolPath(p, request.params.arguments ?? {});
|
|
345
|
+
try {
|
|
346
|
+
const stat = await fsPromises.stat(resolved);
|
|
347
|
+
return { path: resolved, sizeBytes: stat.size, lastModified: stat.mtimeMs };
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return { path: resolved, sizeBytes: 0 };
|
|
351
|
+
}
|
|
352
|
+
})));
|
|
353
|
+
const upfrontPlan = await getTokenEngine().plan(prompt, fileRefs);
|
|
354
|
+
if (upfrontPlan.savings > 0) {
|
|
355
|
+
const pct = upfrontPlan.totalEstimatedTokens > 0
|
|
356
|
+
? Math.round(upfrontPlan.savings / (upfrontPlan.totalEstimatedTokens + upfrontPlan.savings) * 100)
|
|
357
|
+
: 0;
|
|
358
|
+
upfrontTokenNote = `Token plan: ${upfrontPlan.files.filter(a => a.action === 'full').length} full reads, ${upfrontPlan.files.filter(a => a.action === 'outline').length} outlines, ${upfrontPlan.files.filter(a => a.action === 'skip').length} skipped. ~${upfrontPlan.savings.toLocaleString()} tokens saved (${pct}%). `;
|
|
359
|
+
nexusEventBus.emit('tokens.optimized', { savings: upfrontPlan.savings, pct, source: 'orchestrate-preflight' });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// Non-fatal: proceed without upfront plan
|
|
364
|
+
}
|
|
365
|
+
}
|
|
334
366
|
try {
|
|
335
367
|
const preset = resolveExecutionPreset(executionPreset);
|
|
336
368
|
const execution = await hctx.nexusRef.orchestrate(prompt, applyExecutionPreset({
|
|
@@ -457,27 +489,30 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
457
489
|
content: [{
|
|
458
490
|
type: 'text',
|
|
459
491
|
text: [
|
|
492
|
+
upfrontTokenNote ? `[Token plan] ${upfrontTokenNote}` : '',
|
|
460
493
|
`Orchestrated run ${execution.state}.`,
|
|
461
494
|
formatBullets([
|
|
462
495
|
`Workspace: ${workspace.repoName} (${workspace.workspaceSource})`,
|
|
463
496
|
`Run ID: ${execution.runId}`,
|
|
464
497
|
`Summary: ${summarizeExecution(execution)}`,
|
|
465
498
|
`Crew: ${execution.plannerState?.selectedCrew?.name || 'baseline path'}`,
|
|
466
|
-
`Specialists: ${execution.plannerState?.selectedSpecialists.map((specialist) => specialist.name).slice(0, 4).join(', ') || 'none selected'}`,
|
|
467
|
-
`Assets: ${(execution.activeSkills || []).length} skills · ${(execution.activeWorkflows || []).length} workflows · ${(runtimeUsage.artifactSelectionAudit?.selected?.length || 0)} audited selections`,
|
|
468
|
-
`Task graph: ${runtimeUsage.taskGraph?.phases?.length || execution.taskGraph?.phases?.length || 0} phases · ${runtimeUsage.workerPlan?.totalWorkers || execution.workerPlan?.totalWorkers || 0} workers`,
|
|
469
|
-
`Catalog health: ${runtimeUsage.catalogHealth?.overall || 'unknown'}`,
|
|
470
|
-
preset ? `Preset: ${preset.name} (${preset.id})` : null,
|
|
471
499
|
`Verification: ${verifiedWorkers}/${execution.workerResults.length} worker(s) verified`,
|
|
472
|
-
`
|
|
473
|
-
`Tokens: saved ${Number(execution.tokenTelemetry?.savedTokens || 0).toLocaleString()} · compression ${Number(execution.tokenTelemetry?.compressionPct || 0)}% · dominant ${runtimeUsage.sourceAwareTokenBudget?.dominantSource || execution.knowledgeFabric?.sourceMix?.dominantSource || 'repo'}`,
|
|
500
|
+
`Tokens: saved ${Number(execution.tokenTelemetry?.savedTokens || 0).toLocaleString()} · compression ${Number(execution.tokenTelemetry?.compressionPct || 0)}%`,
|
|
474
501
|
autoTokenApplyNote || null,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
502
|
+
...(detailLevel === 'debug' ? [
|
|
503
|
+
`Specialists: ${execution.plannerState?.selectedSpecialists.map((specialist) => specialist.name).slice(0, 4).join(', ') || 'none selected'}`,
|
|
504
|
+
`Assets: ${(execution.activeSkills || []).length} skills · ${(execution.activeWorkflows || []).length} workflows · ${(runtimeUsage.artifactSelectionAudit?.selected?.length || 0)} audited selections`,
|
|
505
|
+
`Task graph: ${runtimeUsage.taskGraph?.phases?.length || execution.taskGraph?.phases?.length || 0} phases · ${runtimeUsage.workerPlan?.totalWorkers || execution.workerPlan?.totalWorkers || 0} workers`,
|
|
506
|
+
`Catalog health: ${runtimeUsage.catalogHealth?.overall || 'unknown'}`,
|
|
507
|
+
preset ? `Preset: ${preset.name} (${preset.id})` : null,
|
|
508
|
+
`Worktrees: ${runtimeUsage.worktreeHealth?.overall || 'unknown'} · repaired ${runtimeUsage.worktreeHealth?.repairedEntries || 0} · broken ${runtimeUsage.worktreeHealth?.brokenEntries || 0}`,
|
|
509
|
+
`RAG: ${(runtimeUsage.ragUsageSummary?.attachedCollections || execution.knowledgeFabric?.rag.attachedCollections.length || 0)} attached · ${(runtimeUsage.ragUsageSummary?.retrievedChunks || execution.knowledgeFabric?.rag.hits.length || 0)} retrieved`,
|
|
510
|
+
`Memory scopes: ${Object.entries(runtimeUsage.memoryScopeUsage?.byScope || execution.memoryScopeUsage?.byScope || {}).map(([scope, count]) => `${scope}:${count}`).join(' · ') || 'awaiting shared/session reads'}`,
|
|
511
|
+
`Payload ref: ${workspace.stateKey} · ${detailLevel}`,
|
|
512
|
+
] : []),
|
|
478
513
|
]),
|
|
479
514
|
detailLevel === 'debug' && execution.result ? `Result\n\`\`\`\n${execution.result}\n\`\`\`` : `Result preview\n\`\`\`\n${truncateText(execution.result || summarizeExecution(execution), 900)}\n\`\`\``,
|
|
480
|
-
formatJsonDetails('Structured details', payload),
|
|
515
|
+
detailLevel === 'debug' ? formatJsonDetails('Structured details', payload) : '',
|
|
481
516
|
hctx.formatRemainingProtocolSteps(),
|
|
482
517
|
].filter(Boolean).join('\n\n'),
|
|
483
518
|
}],
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tool invocation ring buffer and health aggregator.
|
|
3
|
+
*
|
|
4
|
+
* Stores the last RING_SIZE durations per tool (ok + error separately).
|
|
5
|
+
* Used by dispatch.ts to record each call; exposed via /api/runtime/tool-health.
|
|
6
|
+
*/
|
|
7
|
+
export declare function recordToolInvocation(toolName: string, durationMs: number, status: 'ok' | 'error'): void;
|
|
8
|
+
export interface ToolHealthEntry {
|
|
9
|
+
toolName: string;
|
|
10
|
+
p50Ms: number;
|
|
11
|
+
p95Ms: number;
|
|
12
|
+
errorRate: number;
|
|
13
|
+
recentCount: number;
|
|
14
|
+
lastSeenMs: number;
|
|
15
|
+
status: 'green' | 'amber' | 'red';
|
|
16
|
+
}
|
|
17
|
+
export declare function getToolHealthSummary(): ToolHealthEntry[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tool invocation ring buffer and health aggregator.
|
|
3
|
+
*
|
|
4
|
+
* Stores the last RING_SIZE durations per tool (ok + error separately).
|
|
5
|
+
* Used by dispatch.ts to record each call; exposed via /api/runtime/tool-health.
|
|
6
|
+
*/
|
|
7
|
+
const RING_SIZE = 100;
|
|
8
|
+
const ERROR_RATE_WINDOW_MS = 5 * 60 * 1000; // 5-minute sliding window for error rate
|
|
9
|
+
const _rings = new Map();
|
|
10
|
+
export function recordToolInvocation(toolName, durationMs, status) {
|
|
11
|
+
let ring = _rings.get(toolName);
|
|
12
|
+
if (!ring) {
|
|
13
|
+
ring = [];
|
|
14
|
+
_rings.set(toolName, ring);
|
|
15
|
+
}
|
|
16
|
+
ring.push({ ts: Date.now(), durationMs, status });
|
|
17
|
+
if (ring.length > RING_SIZE)
|
|
18
|
+
ring.shift();
|
|
19
|
+
}
|
|
20
|
+
function percentile(sorted, p) {
|
|
21
|
+
if (!sorted.length)
|
|
22
|
+
return 0;
|
|
23
|
+
const idx = Math.ceil((p / 100) * sorted.length) - 1;
|
|
24
|
+
return sorted[Math.max(0, idx)];
|
|
25
|
+
}
|
|
26
|
+
export function getToolHealthSummary() {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const result = [];
|
|
29
|
+
for (const [toolName, ring] of _rings) {
|
|
30
|
+
if (!ring.length)
|
|
31
|
+
continue;
|
|
32
|
+
const windowStart = now - ERROR_RATE_WINDOW_MS;
|
|
33
|
+
const recent = ring.filter(r => r.ts >= windowStart);
|
|
34
|
+
const allDurations = ring.map(r => r.durationMs).sort((a, b) => a - b);
|
|
35
|
+
const errors = recent.filter(r => r.status === 'error').length;
|
|
36
|
+
const errorRate = recent.length > 0 ? errors / recent.length : 0;
|
|
37
|
+
const p50Ms = percentile(allDurations, 50);
|
|
38
|
+
const p95Ms = percentile(allDurations, 95);
|
|
39
|
+
const lastSeenMs = ring[ring.length - 1].ts;
|
|
40
|
+
// Red: error rate > 5% OR p95 > 5s in last 5 min
|
|
41
|
+
// Amber: error rate > 1% OR p95 > 2s
|
|
42
|
+
let status;
|
|
43
|
+
if (errorRate > 0.05 || p95Ms > 5_000)
|
|
44
|
+
status = 'red';
|
|
45
|
+
else if (errorRate > 0.01 || p95Ms > 2_000)
|
|
46
|
+
status = 'amber';
|
|
47
|
+
else
|
|
48
|
+
status = 'green';
|
|
49
|
+
result.push({ toolName, p50Ms, p95Ms, errorRate, recentCount: recent.length, lastSeenMs, status });
|
|
50
|
+
}
|
|
51
|
+
// Sort: red first, then amber, then green; within tier sort by p95 desc
|
|
52
|
+
return result.sort((a, b) => {
|
|
53
|
+
const tier = { red: 0, amber: 1, green: 2 };
|
|
54
|
+
const td = tier[a.status] - tier[b.status];
|
|
55
|
+
return td !== 0 ? td : b.p95Ms - a.p95Ms;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from mcp.ts (Phase 3 split) to keep each file under 1500 LOC.
|
|
5
5
|
* Uses BFS with pLimit(8) concurrency — no sync FS calls, no per-entry stat.
|
|
6
|
+
*
|
|
7
|
+
* Fast path: `git ls-files` for repos (< 300 ms even on large codebases).
|
|
8
|
+
* Fall back to walkDir only when not inside a git repository.
|
|
6
9
|
*/
|
|
7
10
|
/**
|
|
8
11
|
* Recursively walk `root`, returning all file paths.
|
|
@@ -10,8 +13,12 @@
|
|
|
10
13
|
*/
|
|
11
14
|
export declare function walkDir(root: string): Promise<string[]>;
|
|
12
15
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
16
|
+
* Enumerate TypeScript source files via `git ls-files` (fast path) or
|
|
17
|
+
* recursive walkDir (fallback for non-git directories).
|
|
18
|
+
*
|
|
19
|
+
* `git ls-files` completes in < 300 ms even on large repos; walkDir on a
|
|
20
|
+
* repo with thousands of files can exceed the 25 s MCP handler timeout.
|
|
21
|
+
* Cache keyed on cwd + top-level directory mtime so repeated calls are free.
|
|
15
22
|
*/
|
|
16
23
|
export declare function scanSourceFiles(cwd: string, scanCache: Map<string, {
|
|
17
24
|
mtimeMs: number;
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from mcp.ts (Phase 3 split) to keep each file under 1500 LOC.
|
|
5
5
|
* Uses BFS with pLimit(8) concurrency — no sync FS calls, no per-entry stat.
|
|
6
|
+
*
|
|
7
|
+
* Fast path: `git ls-files` for repos (< 300 ms even on large codebases).
|
|
8
|
+
* Fall back to walkDir only when not inside a git repository.
|
|
6
9
|
*/
|
|
7
10
|
import { promises as fsPromises } from 'fs';
|
|
8
11
|
import * as path from 'path';
|
|
9
12
|
import { pLimit } from '../../../../engines/util/p-limit.js';
|
|
13
|
+
import { execAsync } from '../../../../utils/exec-async.js';
|
|
10
14
|
const SKIP = new Set([
|
|
11
15
|
'node_modules', 'dist', '.git', '.next', '.turbo', '.cache',
|
|
12
16
|
'coverage', '.nexus-prime', '.claude', '.vercel', 'build', 'out',
|
|
@@ -47,8 +51,12 @@ export async function walkDir(root) {
|
|
|
47
51
|
return results;
|
|
48
52
|
}
|
|
49
53
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
54
|
+
* Enumerate TypeScript source files via `git ls-files` (fast path) or
|
|
55
|
+
* recursive walkDir (fallback for non-git directories).
|
|
56
|
+
*
|
|
57
|
+
* `git ls-files` completes in < 300 ms even on large repos; walkDir on a
|
|
58
|
+
* repo with thousands of files can exceed the 25 s MCP handler timeout.
|
|
59
|
+
* Cache keyed on cwd + top-level directory mtime so repeated calls are free.
|
|
52
60
|
*/
|
|
53
61
|
export async function scanSourceFiles(cwd, scanCache) {
|
|
54
62
|
let topMtime = 0;
|
|
@@ -57,14 +65,28 @@ export async function scanSourceFiles(cwd, scanCache) {
|
|
|
57
65
|
topMtime = stat.mtimeMs;
|
|
58
66
|
}
|
|
59
67
|
catch {
|
|
60
|
-
// missing dir
|
|
68
|
+
// missing dir — fall through and return []
|
|
61
69
|
}
|
|
62
70
|
const cached = scanCache.get(cwd);
|
|
63
71
|
if (cached && cached.mtimeMs === topMtime) {
|
|
64
72
|
return cached.files;
|
|
65
73
|
}
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
let filtered;
|
|
75
|
+
try {
|
|
76
|
+
// Fast path: git ls-files lists only tracked + untracked (unignored) files.
|
|
77
|
+
// -z uses NUL separators so paths with spaces work correctly.
|
|
78
|
+
const { stdout } = await execAsync('git ls-files --cached --others --exclude-standard -z -- "*.ts"', { cwd, timeout: 8_000 });
|
|
79
|
+
filtered = stdout
|
|
80
|
+
.split('\0')
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.map((f) => path.isAbsolute(f) ? f : path.join(cwd, f))
|
|
83
|
+
.filter((f) => !f.endsWith('.d.ts'));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Not a git repo or git unavailable — fall back to directory walk.
|
|
87
|
+
const files = await walkDir(cwd);
|
|
88
|
+
filtered = files.filter((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'));
|
|
89
|
+
}
|
|
68
90
|
scanCache.set(cwd, { mtimeMs: topMtime, files: filtered });
|
|
69
91
|
return filtered;
|
|
70
92
|
}
|
|
@@ -913,9 +913,14 @@ export class MCPAdapter {
|
|
|
913
913
|
const swarm = await this.getOrchestrator().induce(goal);
|
|
914
914
|
nexusEventBus.emit('nexusnet.sync', { newItemsCount: swarm.length });
|
|
915
915
|
}
|
|
916
|
-
// v6: Lazy-init Synapse/Architects on first tool call if not yet initialized
|
|
916
|
+
// v6: Lazy-init Synapse/Architects on first tool call if not yet initialized.
|
|
917
|
+
// Mandate + hire tools need Architects live so coordination bridge is set before
|
|
918
|
+
// executeMandatePipeline runs (otherwise worklistId is null, orphaned teams).
|
|
917
919
|
if (toolName.startsWith('nexus_synapse_')) {
|
|
918
920
|
this.nexusRef.ensureSynapseInit();
|
|
921
|
+
if (toolName === 'nexus_synapse_mandate' || toolName === 'nexus_synapse_assign_mission' || toolName === 'nexus_synapse_hire') {
|
|
922
|
+
this.nexusRef.ensureArchitectsInit();
|
|
923
|
+
}
|
|
919
924
|
}
|
|
920
925
|
const synapseResponse = await handleSynapseToolCall(toolName, args, this.nexusRef.getSynapse?.() ?? null);
|
|
921
926
|
if (synapseResponse) {
|
|
@@ -185,41 +185,65 @@ export function initArchitects(options) {
|
|
|
185
185
|
if (hasOperative)
|
|
186
186
|
ward.start();
|
|
187
187
|
const onOperativeHired = ({ operativeId, strikeTeamId }) => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
try {
|
|
189
|
+
knownOperatives.add(operativeId);
|
|
190
|
+
operativeActivity.set(operativeId, { strikeTeamId, lastSortieAt: null });
|
|
191
|
+
ensureImplicitWorklist(strikeTeamId);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
console.error('[Architects] onOperativeHired failed', operativeId, err);
|
|
195
|
+
nexusEventBus.emit('architects.event.failed', { event: 'operative.hired', operativeId, error: String(err) });
|
|
196
|
+
}
|
|
191
197
|
};
|
|
192
198
|
const onMissionAssigned = ({ operativeId, missionId, title }) => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
199
|
+
try {
|
|
200
|
+
const activity = operativeActivity.get(operativeId);
|
|
201
|
+
const strikeTeamId = activity?.strikeTeamId ?? null;
|
|
202
|
+
if (!strikeTeamId)
|
|
203
|
+
return;
|
|
204
|
+
const worklistId = ensureImplicitWorklist(strikeTeamId);
|
|
205
|
+
upsertWorkItem(db, {
|
|
206
|
+
id: missionId,
|
|
207
|
+
worklistId,
|
|
208
|
+
title,
|
|
209
|
+
status: 'todo',
|
|
210
|
+
assignedOperativeId: operativeId,
|
|
211
|
+
constructionLockId: null,
|
|
212
|
+
dependsOn: [],
|
|
213
|
+
branch: null,
|
|
214
|
+
mergedAt: null,
|
|
215
|
+
createdAt: new Date().toISOString(),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
console.error('[Architects] onMissionAssigned failed', missionId, err);
|
|
220
|
+
nexusEventBus.emit('architects.event.failed', { event: 'mission.assigned', operativeId, missionId, error: String(err) });
|
|
221
|
+
}
|
|
210
222
|
};
|
|
211
223
|
const onSortieCompleted = ({ operativeId, missionId, status }) => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
try {
|
|
225
|
+
const activity = operativeActivity.get(operativeId);
|
|
226
|
+
if (activity) {
|
|
227
|
+
activity.lastSortieAt = new Date().toISOString();
|
|
228
|
+
operativeActivity.set(operativeId, activity);
|
|
229
|
+
}
|
|
230
|
+
if (!missionId)
|
|
231
|
+
return;
|
|
232
|
+
updateWorkItemStatus(db, missionId, status === 'completed' ? 'done' : status === 'failed' ? 'failed' : 'blocked');
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
console.error('[Architects] onSortieCompleted failed', missionId, err);
|
|
236
|
+
nexusEventBus.emit('architects.event.failed', { event: 'sortie.completed', operativeId, missionId: missionId ?? undefined, error: String(err) });
|
|
216
237
|
}
|
|
217
|
-
if (!missionId)
|
|
218
|
-
return;
|
|
219
|
-
updateWorkItemStatus(db, missionId, status === 'completed' ? 'done' : status === 'failed' ? 'failed' : 'blocked');
|
|
220
238
|
};
|
|
221
239
|
const onStriketeamDeployed = ({ strikeTeamId }) => {
|
|
222
|
-
|
|
240
|
+
try {
|
|
241
|
+
ensureImplicitWorklist(strikeTeamId);
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
console.error('[Architects] onStriketeamDeployed failed', strikeTeamId, err);
|
|
245
|
+
nexusEventBus.emit('architects.event.failed', { event: 'striketeam.deployed', strikeTeamId, error: String(err) });
|
|
246
|
+
}
|
|
223
247
|
};
|
|
224
248
|
const onStanddown = () => setConvergencePaused(true);
|
|
225
249
|
const onResumed = () => setConvergencePaused(false);
|
package/dist/cli/hook.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import fs from 'fs';
|
|
18
18
|
import path from 'path';
|
|
19
19
|
import os from 'os';
|
|
20
|
+
import { spawn } from 'child_process';
|
|
20
21
|
function findDaemonLock(cwd) {
|
|
21
22
|
const nexusPrimeDir = path.join(os.homedir(), '.nexus-prime');
|
|
22
23
|
if (!fs.existsSync(nexusPrimeDir))
|
|
@@ -103,6 +104,31 @@ async function readStdinJson() {
|
|
|
103
104
|
// --------------------------------------------------------------------------
|
|
104
105
|
// Hook subcommands
|
|
105
106
|
// --------------------------------------------------------------------------
|
|
107
|
+
function spawnDaemon(cwd) {
|
|
108
|
+
const entrypoint = process.argv[1];
|
|
109
|
+
if (!entrypoint)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
const child = spawn(process.execPath, [entrypoint, 'daemon', '__serve'], {
|
|
113
|
+
cwd,
|
|
114
|
+
env: { ...process.env, NEXUS_WORKSPACE_ROOT: cwd },
|
|
115
|
+
detached: true,
|
|
116
|
+
stdio: 'ignore',
|
|
117
|
+
});
|
|
118
|
+
child.unref();
|
|
119
|
+
}
|
|
120
|
+
catch { /* never block the hook */ }
|
|
121
|
+
}
|
|
122
|
+
async function waitForDaemonLock(cwd, timeoutMs) {
|
|
123
|
+
const deadline = Date.now() + timeoutMs;
|
|
124
|
+
while (Date.now() < deadline) {
|
|
125
|
+
const lock = findDaemonLock(cwd);
|
|
126
|
+
if (lock)
|
|
127
|
+
return lock;
|
|
128
|
+
await new Promise(r => setTimeout(r, 100).unref());
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
106
132
|
/** UserPromptSubmit — call nexus_session_bootstrap so the model never has to. */
|
|
107
133
|
export async function runHookBootstrap() {
|
|
108
134
|
const data = await readStdinJson();
|
|
@@ -110,9 +136,15 @@ export async function runHookBootstrap() {
|
|
|
110
136
|
const goal = String(data.user_message ?? data.prompt ?? '').trim();
|
|
111
137
|
if (!goal)
|
|
112
138
|
return;
|
|
113
|
-
|
|
139
|
+
let lock = findDaemonLock(cwd);
|
|
140
|
+
if (!lock) {
|
|
141
|
+
// Daemon not running — spawn it and wait up to 1.5 s for the lock to appear.
|
|
142
|
+
// If it doesn't come up in time, fall back silently (never block the model).
|
|
143
|
+
spawnDaemon(cwd);
|
|
144
|
+
lock = await waitForDaemonLock(cwd, 1_500);
|
|
145
|
+
}
|
|
114
146
|
if (!lock)
|
|
115
|
-
return;
|
|
147
|
+
return;
|
|
116
148
|
await callDaemonTool(lock, 'nexus_session_bootstrap', {
|
|
117
149
|
goal: goal.slice(0, 500),
|
|
118
150
|
}, 10_000);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { homedir } from 'os';
|
|
8
9
|
import { fileURLToPath } from 'url';
|
|
9
10
|
import { dirname, join, resolve } from 'path';
|
|
10
11
|
import { detectInstalledIDEs, getMcpConfigForIDE } from '../agents/adapters/ide-compat.js';
|
|
@@ -93,9 +94,12 @@ export async function configureIDE(ide, opts = {}) {
|
|
|
93
94
|
silent: opts.verbose === false,
|
|
94
95
|
workspaceRoot,
|
|
95
96
|
});
|
|
96
|
-
// Write Claude Code hooks alongside the MCP entry (idempotent merge)
|
|
97
|
+
// Write Claude Code hooks alongside the MCP entry (idempotent merge).
|
|
98
|
+
// Write to both workspace and global user settings so desktop-mode
|
|
99
|
+
// Claude Code (which reads ~/.claude/settings.json) also auto-bootstraps.
|
|
97
100
|
if (ide === 'claude-code') {
|
|
98
101
|
_writeClaudeCodeHooks(workspaceRoot);
|
|
102
|
+
_writeClaudeCodeHooks(homedir());
|
|
99
103
|
}
|
|
100
104
|
// Write workspace-local configs for other IDEs that may be present
|
|
101
105
|
_writeWorkspaceLocalConfigs(ide, workspaceRoot);
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
<script type="module" src="./main.js"></script>
|
|
24
24
|
</head>
|
|
25
25
|
<body>
|
|
26
|
+
<script>
|
|
27
|
+
if(location.protocol==='file:'){
|
|
28
|
+
document.write('<div style="position:fixed;top:0;left:0;right:0;z-index:9999;background:#d97706;color:#fff;text-align:center;padding:6px 12px;font:13px/1.4 system-ui,sans-serif">Preview mode \u2014 API unavailable. Open <a href="http://localhost:3377/" style="color:#fff;text-decoration:underline">http://localhost:3377/</a> for the live dashboard.</div><div style="height:34px"></div>');
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
26
31
|
<div class="noise" aria-hidden="true"></div>
|
|
27
32
|
<div id="toast-stack" aria-live="polite" aria-atomic="true"></div>
|
|
28
33
|
|
|
@@ -108,6 +113,12 @@
|
|
|
108
113
|
<div><div class="kb-col-hd"><span class="kb-col-title">Done</span><span class="kb-count" id="kc-done">0</span></div><div class="kb-body" id="kb-done"></div></div>
|
|
109
114
|
</div>
|
|
110
115
|
|
|
116
|
+
<!-- Tool health strip -->
|
|
117
|
+
<div id="tool-health-card" class="card" style="margin-bottom:16px;display:none">
|
|
118
|
+
<div class="shd" style="margin-bottom:8px">Tool Health <span id="tool-health-stamp" style="font-size:11px;opacity:.5;font-weight:400"></span></div>
|
|
119
|
+
<div id="tool-health-list" style="display:flex;flex-wrap:wrap;gap:8px"></div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
111
122
|
<!-- Events feed + system pulse -->
|
|
112
123
|
<div class="cockpit-bottom">
|
|
113
124
|
<div class="card">
|
|
@@ -47,6 +47,10 @@ setOnEvent(evt => {
|
|
|
47
47
|
Board.render();
|
|
48
48
|
_renderTicker();
|
|
49
49
|
}
|
|
50
|
+
// Refresh tool health tile on any tool invocation event (board always refreshes)
|
|
51
|
+
if (String(evt.type||'') === 'tool.invocation' && tab === 'board') {
|
|
52
|
+
Board.loadToolHealth();
|
|
53
|
+
}
|
|
50
54
|
if (tab === 'workforce' && ['synapse','operative','mission'].includes(evt.category)) {
|
|
51
55
|
Workforce.render();
|
|
52
56
|
}
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
.org-box.root-box { border-color: rgba(0,255,136,0.4); color: var(--accent); }
|
|
19
19
|
.org-box.team-box { border-color: rgba(0,212,255,0.3); color: var(--secondary); }
|
|
20
20
|
.org-vline { width: 1px; height: 14px; background: var(--border); margin: 0 auto; }
|
|
21
|
+
.org-row { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; align-items: flex-start; }
|
|
22
|
+
.org-branch { display: flex; flex-direction: column; align-items: center; }
|
|
23
|
+
/* Team tint: subtree root gets a faint background based on team color slot */
|
|
24
|
+
.org-branch[data-team] > div > .org-box { border-color: rgba(0,212,255,0.25); }
|
|
21
25
|
.op-status-dot {
|
|
22
26
|
display: inline-block; width: 5px; height: 5px; border-radius: 50%;
|
|
23
27
|
margin-right: 5px; background: var(--text-dim); vertical-align: middle;
|
|
@@ -72,6 +72,8 @@ export async function load() {
|
|
|
72
72
|
api('/api/dashboard/surface/operate', 5000),
|
|
73
73
|
api('/api/synapse/health', 5000),
|
|
74
74
|
]);
|
|
75
|
+
// Non-blocking: tool health data from ring buffer
|
|
76
|
+
loadToolHealth();
|
|
75
77
|
S.tokensSummary = tok;
|
|
76
78
|
S.tokensLifetime = life;
|
|
77
79
|
S.operateSurface = op;
|
|
@@ -94,6 +96,7 @@ export function render() {
|
|
|
94
96
|
renderKanban();
|
|
95
97
|
renderEvents();
|
|
96
98
|
renderPulse();
|
|
99
|
+
renderToolHealth();
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
/* ── Memory pyramid (above kanban) ── */
|
|
@@ -295,6 +298,40 @@ function renderPulse() {
|
|
|
295
298
|
el.innerHTML=rows.map(([k,v])=>`<div class="row"><span class="row-k">${esc(k)}</span><span class="row-v">${esc(v)}</span></div>`).join('');
|
|
296
299
|
}
|
|
297
300
|
|
|
301
|
+
/* ── Tool health strip ── */
|
|
302
|
+
let _toolHealth = [];
|
|
303
|
+
|
|
304
|
+
export async function loadToolHealth() {
|
|
305
|
+
const data = await api('/api/runtime/tool-health', 10_000);
|
|
306
|
+
if (Array.isArray(data)) {
|
|
307
|
+
_toolHealth = data;
|
|
308
|
+
renderToolHealth();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function renderToolHealth() {
|
|
313
|
+
const card = $('tool-health-card');
|
|
314
|
+
const list = $('tool-health-list');
|
|
315
|
+
const stamp = $('tool-health-stamp');
|
|
316
|
+
if (!card || !list) return;
|
|
317
|
+
if (!_toolHealth.length) { card.style.display = 'none'; return; }
|
|
318
|
+
card.style.display = '';
|
|
319
|
+
if (stamp) stamp.textContent = 'updated ' + new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
320
|
+
|
|
321
|
+
const colorMap = { green: 'var(--accent)', amber: '#ffd14d', red: '#ff5f57' };
|
|
322
|
+
list.innerHTML = _toolHealth.map(t => {
|
|
323
|
+
const col = colorMap[t.status] || 'var(--muted)';
|
|
324
|
+
const errPct = Math.round((t.errorRate || 0) * 100);
|
|
325
|
+
const p95 = t.p95Ms >= 1000 ? (t.p95Ms / 1000).toFixed(1) + 's' : t.p95Ms + 'ms';
|
|
326
|
+
const pill = errPct > 0 ? `<span style="background:#ff5f5722;color:#ff5f57;border-radius:4px;padding:1px 5px;font-size:10px">${errPct}% err</span>` : '';
|
|
327
|
+
return `<div style="display:flex;align-items:center;gap:6px;padding:4px 8px;border-radius:6px;background:var(--surface2);font-size:12px">
|
|
328
|
+
<span style="width:8px;height:8px;border-radius:50%;background:${col};flex-shrink:0"></span>
|
|
329
|
+
<span style="font-family:var(--font-mono);color:var(--fg1)">${esc(t.toolName.replace('nexus_',''))}</span>
|
|
330
|
+
<span style="color:var(--muted)">${p95}</span>${pill}
|
|
331
|
+
</div>`;
|
|
332
|
+
}).join('');
|
|
333
|
+
}
|
|
334
|
+
|
|
298
335
|
/* ── Run drawer helper ── */
|
|
299
336
|
function _openRunDrawer(runId) {
|
|
300
337
|
const run = S.runs.find(r=>r.id===runId||r.runId===runId);
|
|
@@ -168,6 +168,28 @@ export function render() {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/* ── Org chart ── */
|
|
171
|
+
function _renderOpNode(op) {
|
|
172
|
+
const sc=op.status==='ACTIVE'||op.status==='active'?'active':op.status==='ZOMBIE'||op.status==='dead'?'dead':'';
|
|
173
|
+
const interval=op.sortieIntervalMs||60000, lastAt=op.lastSortieAt||op.heartbeatAt||Date.now();
|
|
174
|
+
const pillId=`op-pill-${esc(op.id||op.operativeId||'')}`;
|
|
175
|
+
return `<div class="org-box" id="${pillId}" data-opid="${esc(op.id||op.operativeId)}" data-opname="${esc(op.role||op.name||'agent')}" data-interval="${interval}" data-lastat="${lastAt}">
|
|
176
|
+
<span class="op-status-dot ${esc(sc)}"></span>${esc(op.role||op.name||'agent')}
|
|
177
|
+
</div>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _renderOrgBranch(op, childrenOf, depth) {
|
|
181
|
+
const kids = childrenOf.get(op.id||op.operativeId) || [];
|
|
182
|
+
const kidsHtml = (depth < 3 && kids.length)
|
|
183
|
+
? `<div class="org-vline"></div><div class="org-row">${kids.map(k => _renderOrgBranch(k, childrenOf, depth+1)).join('')}</div>`
|
|
184
|
+
: '';
|
|
185
|
+
const teamTint = op.team ? `data-team="${esc(op.team)}"` : '';
|
|
186
|
+
return `<div class="org-branch" ${teamTint}>
|
|
187
|
+
<div style="display:flex;flex-direction:column;align-items:center">
|
|
188
|
+
${_renderOpNode(op)}${kidsHtml}
|
|
189
|
+
</div>
|
|
190
|
+
</div>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
171
193
|
function renderOrgChart() {
|
|
172
194
|
const el=$('org-container'); if (!el) return;
|
|
173
195
|
const ops=S.synapseHealth;
|
|
@@ -175,30 +197,29 @@ function renderOrgChart() {
|
|
|
175
197
|
el.innerHTML=`<div class="empty"><div class="empty-title">${V.empty.workforce}</div><div class="empty-sub">Hire a specialist from the panel below.</div></div>`;
|
|
176
198
|
return;
|
|
177
199
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}).join('');
|
|
200
|
+
|
|
201
|
+
// Build parent→children map keyed by operative id (null = root).
|
|
202
|
+
const childrenOf = new Map();
|
|
203
|
+
for (const op of ops) {
|
|
204
|
+
const parent = op.reportsToOperativeId || null;
|
|
205
|
+
if (!childrenOf.has(parent)) childrenOf.set(parent, []);
|
|
206
|
+
childrenOf.get(parent).push(op);
|
|
207
|
+
}
|
|
208
|
+
// Operatives with no parent whose parent id doesn't match any known op are also roots.
|
|
209
|
+
const knownIds = new Set(ops.map(o => o.id||o.operativeId));
|
|
210
|
+
for (const op of ops) {
|
|
211
|
+
if (op.reportsToOperativeId && !knownIds.has(op.reportsToOperativeId)) {
|
|
212
|
+
if (!childrenOf.has(null)) childrenOf.set(null, []);
|
|
213
|
+
childrenOf.get(null).push(op);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const roots = childrenOf.get(null) || ops; // fallback: all ops as roots if no hierarchy
|
|
217
|
+
|
|
218
|
+
const tHtml = roots.map(op => _renderOrgBranch(op, childrenOf, 0)).join('');
|
|
198
219
|
el.innerHTML=`<div style="display:flex;flex-direction:column;align-items:center">
|
|
199
220
|
<div class="org-box root-box">Orchestrator</div>
|
|
200
221
|
<div class="org-vline"></div>
|
|
201
|
-
<div
|
|
222
|
+
<div class="org-row">${tHtml}</div>
|
|
202
223
|
</div>`;
|
|
203
224
|
|
|
204
225
|
// Attach click + heartbeat
|
|
@@ -266,11 +287,30 @@ function renderSpecialistGrid() {
|
|
|
266
287
|
}
|
|
267
288
|
|
|
268
289
|
/* ── Hire sheet ── */
|
|
290
|
+
function _buildHireSelectors() {
|
|
291
|
+
const ops = (S.synapseHealth || []);
|
|
292
|
+
const teams = [...new Set(ops.map(o => o.team).filter(Boolean))];
|
|
293
|
+
const opOptions = [`<option value="">— none (team lead) —</option>`,
|
|
294
|
+
...ops.map(o => `<option value="${esc(o.id||o.operativeId)}">${esc(o.role||o.name||o.id)}</option>`)
|
|
295
|
+
].join('');
|
|
296
|
+
const teamOptions = [`<option value="">— solo —</option>`,
|
|
297
|
+
...teams.map(t => `<option value="${esc(t)}">${esc(t)}</option>`)
|
|
298
|
+
].join('');
|
|
299
|
+
const sel = (id, label, opts) =>
|
|
300
|
+
`<label style="display:block;margin-bottom:4px;font-size:var(--text-sm);color:var(--text-muted)">${label}</label>
|
|
301
|
+
<select id="${id}" style="width:100%;background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:6px 10px;font-size:var(--text-sm);color:var(--text-main);margin-bottom:var(--space-4)">${opts}</select>`;
|
|
302
|
+
return { opOptions: sel('hire-reports-to', 'Reports to (optional)', opOptions),
|
|
303
|
+
teamOptions: sel('hire-team', 'Strike team (optional)', teamOptions) };
|
|
304
|
+
}
|
|
305
|
+
|
|
269
306
|
function _showHireSheet(specialistId, name) {
|
|
307
|
+
const { opOptions, teamOptions } = _buildHireSelectors();
|
|
270
308
|
openDrawer({ title: `Hire ${name}`,
|
|
271
309
|
body: `<div class="dsec">
|
|
272
310
|
<div class="dsec-title">Specialist</div>
|
|
273
311
|
<div style="font-size:var(--text-sm);color:var(--text-muted);margin-bottom:var(--space-4)">${esc(name)}<br><span style="opacity:.6">${esc(specialistId)}</span></div>
|
|
312
|
+
${opOptions}
|
|
313
|
+
${teamOptions}
|
|
274
314
|
<label style="display:block;margin-bottom:var(--space-2);font-size:var(--text-sm);color:var(--text-muted)">Budget cap (USD)</label>
|
|
275
315
|
<input type="number" id="hire-budget" value="2.00" step="0.50" min="0.50" max="50"
|
|
276
316
|
style="width:100%;background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:6px 10px;font-size:var(--text-sm);color:var(--text-main);margin-bottom:var(--space-4)">
|
|
@@ -281,12 +321,19 @@ function _showHireSheet(specialistId, name) {
|
|
|
281
321
|
const specId = btn.target.dataset.specid;
|
|
282
322
|
const specName = btn.target.dataset.specname;
|
|
283
323
|
const budget = parseFloat(document.getElementById('hire-budget')?.value||'2');
|
|
324
|
+
const reportsToVal = (document.getElementById('hire-reports-to')?.value||'').trim()||null;
|
|
325
|
+
const teamVal = (document.getElementById('hire-team')?.value||'').trim()||null;
|
|
284
326
|
btn.target.disabled = true;
|
|
285
327
|
btn.target.textContent = 'Hiring…';
|
|
286
328
|
|
|
287
329
|
// Optimistic hire: open the success drawer immediately (<5ms), then reconcile.
|
|
288
330
|
const optimisticRecord = { operative: { specialistId: specId, _pending: true }, pricing: null };
|
|
289
|
-
const result = await post('/api/synapse/hire', {
|
|
331
|
+
const result = await post('/api/synapse/hire', {
|
|
332
|
+
specialistId: specId,
|
|
333
|
+
budgetCapUsd: budget,
|
|
334
|
+
reportsToOperativeId: reportsToVal,
|
|
335
|
+
strikeTeamId: teamVal,
|
|
336
|
+
}, { optimistic: optimisticRecord });
|
|
290
337
|
|
|
291
338
|
// Immediately open drawer with pending state.
|
|
292
339
|
openDrawer({ title: `${specName} hired`,
|
|
@@ -88,8 +88,8 @@ export const handleGovernanceRoutes = async (ctx, req, res, url) => {
|
|
|
88
88
|
const operativeId = result?.id ?? result?.operativeId ?? null;
|
|
89
89
|
const baseUrl = ctx.getAddress() ?? 'http://localhost:3377';
|
|
90
90
|
// Optionally fire a warm-up first sortie (body.fireFirstSortie === true).
|
|
91
|
-
// Non-blocking —
|
|
92
|
-
|
|
91
|
+
// Non-blocking — dispatch runs in the background; runId is emitted via SSE
|
|
92
|
+
// (synapse.first-sortie.dispatched) when ready so the client doesn't race.
|
|
93
93
|
if (body.fireFirstSortie === true && operativeId && result?.specialistId) {
|
|
94
94
|
const repoRoot = ctx.repoRoot ?? process.cwd();
|
|
95
95
|
scheduleFirstSortie({
|
|
@@ -97,14 +97,20 @@ export const handleGovernanceRoutes = async (ctx, req, res, url) => {
|
|
|
97
97
|
repoRoot,
|
|
98
98
|
repoName: path.basename(repoRoot),
|
|
99
99
|
budgetCapUsd: result?.budgetCapUsd ?? 0.50,
|
|
100
|
-
}).then(r => {
|
|
101
|
-
|
|
100
|
+
}).then(r => {
|
|
101
|
+
if (r?.runId) {
|
|
102
|
+
nexusEventBus.emit('dashboard.action', {
|
|
103
|
+
action: 'synapse.first-sortie.dispatched',
|
|
104
|
+
status: 'ok',
|
|
105
|
+
target: r.runId,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}).catch(() => { });
|
|
102
109
|
}
|
|
103
110
|
ctx.respondJson(res, {
|
|
104
111
|
operative: result,
|
|
105
112
|
pricing,
|
|
106
113
|
firstSortieEstimate: pricing,
|
|
107
|
-
firstDispatchRunId,
|
|
108
114
|
dashboardUrl: operativeId ? `${baseUrl}/#workforce/${operativeId}` : `${baseUrl}/#workforce`,
|
|
109
115
|
}, 201);
|
|
110
116
|
}
|
|
@@ -114,6 +120,10 @@ export const handleGovernanceRoutes = async (ctx, req, res, url) => {
|
|
|
114
120
|
return true;
|
|
115
121
|
}
|
|
116
122
|
if (req.method === 'POST' && url.pathname === '/api/synapse/deploy-crew') {
|
|
123
|
+
// Ensure Architects is live before mandate so coordination bridge is wired.
|
|
124
|
+
// getSynapse() triggers ensureSynapseInit(); getArchitects() triggers ensureArchitectsInit()
|
|
125
|
+
// + reconnectSynapseCoordination() so worklistId is non-null in the pipeline.
|
|
126
|
+
ctx.getArchitects();
|
|
117
127
|
const synapse = ctx.getSynapse();
|
|
118
128
|
if (!synapse) {
|
|
119
129
|
const initErr = ctx.getSynapseInitError();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getToolHealthSummary } from '../../agents/adapters/mcp/tool-health.js';
|
|
1
2
|
export const handleHealthRoutes = async (ctx, req, res, url) => {
|
|
2
3
|
if (req.method === 'GET' && url.pathname === '/api/license') {
|
|
3
4
|
const { getSharedLicenseManager } = await import('../../licensing/index.js');
|
|
@@ -22,5 +23,9 @@ export const handleHealthRoutes = async (ctx, req, res, url) => {
|
|
|
22
23
|
await ctx.respondCachedJson(res, 'health', 15_000, () => ctx.collectHealth());
|
|
23
24
|
return true;
|
|
24
25
|
}
|
|
26
|
+
if (req.method === 'GET' && url.pathname === '/api/runtime/tool-health') {
|
|
27
|
+
ctx.respondJson(res, getToolHealthSummary());
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
25
30
|
return false;
|
|
26
31
|
};
|
package/dist/dashboard/server.js
CHANGED
|
@@ -281,6 +281,16 @@ export class DashboardServer {
|
|
|
281
281
|
this.serveAppAsset(res, url.pathname);
|
|
282
282
|
return;
|
|
283
283
|
}
|
|
284
|
+
// Root-level aliases so relative paths in index.html work when served at /
|
|
285
|
+
// (e.g. ./styles/tokens.css → /styles/tokens.css → here).
|
|
286
|
+
// /app/* kept for backward compat (bookmarks, devtools, any hardcoded refs).
|
|
287
|
+
const ROOT_APP_PREFIXES = ['/styles/', '/views/', '/widgets/'];
|
|
288
|
+
const ROOT_APP_FILES = new Set(['/main.js', '/api.js', '/router.js', '/sse.js', '/state.js', '/vocabulary.js']);
|
|
289
|
+
if (req.method === 'GET' && (ROOT_APP_PREFIXES.some(p => url.pathname.startsWith(p)) ||
|
|
290
|
+
ROOT_APP_FILES.has(url.pathname))) {
|
|
291
|
+
this.serveAppAsset(res, '/app' + url.pathname);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
284
294
|
if (req.method === 'GET' && url.pathname === '/welcome') {
|
|
285
295
|
this.serveStaticHtml(res, path.join(__dirname, 'welcome.html'));
|
|
286
296
|
return;
|
|
@@ -415,6 +425,7 @@ export class DashboardServer {
|
|
|
415
425
|
serveAppIndex(res) {
|
|
416
426
|
const strictHeaders = {
|
|
417
427
|
'Content-Type': 'text/html',
|
|
428
|
+
'Cache-Control': 'no-cache, must-revalidate',
|
|
418
429
|
'Content-Security-Policy': [
|
|
419
430
|
"default-src 'self'",
|
|
420
431
|
"style-src 'self'",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type NexusEventType = 'system.boot' | 'planner.stage' | 'memory.store' | 'memory.dedup' | 'memory.recall' | 'memory.flushed' | 'memory.health.tick' | 'memory.sqlite.retry' | 'pod.signal' | 'tokens.optimized' | 'tokens.searchSaved' | 'session.summaryBootstrap' | 'phantom.worker.start' | 'phantom.worker.complete' | 'phantom.merge.complete' | 'phantom.merge' | 'guardrail.check' | 'ghost.pass' | 'graph.query' | 'graph.sync.failed' | 'graph.coverage.low' | 'graph.cr.build.start' | 'graph.cr.build.complete' | 'graph.cr.build.failed' | 'darwin.cycle' | 'darwin.cycle.complete' | 'session.dna' | 'skill.register' | 'skill.deploy' | 'skill.revoke' | 'hook.deploy' | 'hook.revoke' | 'hook.fire' | 'hook.error' | 'workflow.deploy' | 'workflow.run' | 'automation.deploy' | 'automation.revoke' | 'automation.run' | 'shield.decision' | 'memory.audit' | 'federation.heartbeat' | 'client.heartbeat' | 'client.inferred' | 'client.status' | 'dashboard.action' | 'nexus.shutdown' | 'orchestrator.disposed' | 'orchestrator.funnel.stage' | 'nexusnet.publish' | 'nexusnet.sync' | 'mcp.call.start' | 'mcp.call.stream' | 'mcp.call.complete' | 'mcp.handler.complete' | 'mcp.handler.failed' | 'mcp.handler.retry' | 'mcp.handler.best-effort-failed' | 'memory.tier.promoted' | 'memory.tier.promoted.failed' | 'memory.pre-compaction' | 'memory.flush-requested' | 'entanglement.create' | 'entanglement.collapse' | 'entanglement.correlate' | 'cas.encode' | 'cas.decode' | 'cas.pattern_learned' | 'kv.merge' | 'kv.adapt' | 'kv.consensus' | 'byzantine.vote.started' | 'byzantine.vote.result' | 'synapse.ready' | 'synapse.operative.hired' | 'synapse.operative.retired' | 'synapse.operative.health.changed' | 'synapse.striketeam.deployed' | 'synapse.striketeam.completed' | 'synapse.mission.assigned' | 'synapse.mission.completed' | 'synapse.sortie.started' | 'synapse.sortie.completed' | 'synapse.sortie.failed' | 'synapse.fieldreport.submitted' | 'synapse.echo.fired' | 'synapse.budget.warning' | 'synapse.budget.exceeded' | 'synapse.approval.requested' | 'synapse.approval.resolved' | 'synapse.compaction.standdown' | 'synapse.compaction.resumed' | 'synapse.watchdog.stall' | 'synapse.watchdog.zombie' | 'architects.ready' | 'architects.blueprint.instantiated' | 'architects.worklist.created' | 'architects.workitem.claimed' | 'architects.workitem.completed' | 'architects.workitem.blocked' | 'architects.constructionlock.acquired' | 'architects.constructionlock.renewed' | 'architects.constructionlock.released' | 'architects.constructionlock.contested' | 'architects.relay.sent' | 'architects.relay.read' | 'architects.sentinel.patrol' | 'architects.sentinel.stall' | 'architects.sentinel.zombie' | 'architects.ward.patrol' | 'architects.ward.escalation' | 'architects.convergence.started' | 'architects.convergence.merged' | 'architects.convergence.failed' | 'architects.dispatch.go' | 'architects.dispatch.queued' | 'architects.relay.failed' | 'ledger.duplicate-prevented' | 'nexus.circuit-open' | 'nexus.circuit-tripped' | 'license.activated' | 'license.deactivated' | 'license.expired' | 'license.cap.warning' | 'license.cap.reached' | 'dispatch.started' | 'dispatch.event' | 'dispatch.token-usage' | 'dispatch.complete' | 'dispatch.failed' | 'dispatch.cancelled' | 'dispatch.budget-exceeded' | 'mcp.auto-memory.failed' | 'daemon.self-heal.db-corrupt' | 'feature.time_to_first_bootstrap' | 'feature.time_to_first_memory' | 'feature.time_to_first_interaction' | 'feature.time_to_first_mission_complete';
|
|
1
|
+
export type NexusEventType = 'system.boot' | 'planner.stage' | 'memory.store' | 'memory.dedup' | 'memory.recall' | 'memory.flushed' | 'memory.health.tick' | 'memory.sqlite.retry' | 'pod.signal' | 'tokens.optimized' | 'tokens.searchSaved' | 'session.summaryBootstrap' | 'phantom.worker.start' | 'phantom.worker.complete' | 'phantom.merge.complete' | 'phantom.merge' | 'guardrail.check' | 'ghost.pass' | 'graph.query' | 'graph.sync.failed' | 'graph.coverage.low' | 'graph.cr.build.start' | 'graph.cr.build.complete' | 'graph.cr.build.failed' | 'darwin.cycle' | 'darwin.cycle.complete' | 'session.dna' | 'skill.register' | 'skill.deploy' | 'skill.revoke' | 'hook.deploy' | 'hook.revoke' | 'hook.fire' | 'hook.error' | 'workflow.deploy' | 'workflow.run' | 'automation.deploy' | 'automation.revoke' | 'automation.run' | 'shield.decision' | 'memory.audit' | 'federation.heartbeat' | 'client.heartbeat' | 'client.inferred' | 'client.status' | 'dashboard.action' | 'nexus.shutdown' | 'orchestrator.disposed' | 'orchestrator.funnel.stage' | 'nexusnet.publish' | 'nexusnet.sync' | 'mcp.call.start' | 'mcp.call.stream' | 'mcp.call.complete' | 'mcp.handler.complete' | 'mcp.handler.failed' | 'mcp.handler.retry' | 'mcp.handler.best-effort-failed' | 'tool.invocation' | 'memory.tier.promoted' | 'memory.tier.promoted.failed' | 'memory.pre-compaction' | 'memory.flush-requested' | 'entanglement.create' | 'entanglement.collapse' | 'entanglement.correlate' | 'cas.encode' | 'cas.decode' | 'cas.pattern_learned' | 'kv.merge' | 'kv.adapt' | 'kv.consensus' | 'byzantine.vote.started' | 'byzantine.vote.result' | 'synapse.ready' | 'synapse.operative.hired' | 'synapse.operative.retired' | 'synapse.operative.health.changed' | 'synapse.striketeam.deployed' | 'synapse.striketeam.completed' | 'synapse.mission.assigned' | 'synapse.mission.completed' | 'synapse.sortie.started' | 'synapse.sortie.completed' | 'synapse.sortie.failed' | 'synapse.fieldreport.submitted' | 'synapse.echo.fired' | 'synapse.budget.warning' | 'synapse.budget.exceeded' | 'synapse.approval.requested' | 'synapse.approval.resolved' | 'synapse.compaction.standdown' | 'synapse.compaction.resumed' | 'synapse.watchdog.stall' | 'synapse.watchdog.zombie' | 'architects.ready' | 'architects.blueprint.instantiated' | 'architects.worklist.created' | 'architects.workitem.claimed' | 'architects.workitem.completed' | 'architects.workitem.blocked' | 'architects.constructionlock.acquired' | 'architects.constructionlock.renewed' | 'architects.constructionlock.released' | 'architects.constructionlock.contested' | 'architects.relay.sent' | 'architects.relay.read' | 'architects.sentinel.patrol' | 'architects.sentinel.stall' | 'architects.sentinel.zombie' | 'architects.ward.patrol' | 'architects.ward.escalation' | 'architects.convergence.started' | 'architects.convergence.merged' | 'architects.convergence.failed' | 'architects.dispatch.go' | 'architects.dispatch.queued' | 'architects.relay.failed' | 'architects.event.failed' | 'ledger.duplicate-prevented' | 'nexus.circuit-open' | 'nexus.circuit-tripped' | 'license.activated' | 'license.deactivated' | 'license.expired' | 'license.cap.warning' | 'license.cap.reached' | 'dispatch.started' | 'dispatch.event' | 'dispatch.token-usage' | 'dispatch.complete' | 'dispatch.failed' | 'dispatch.cancelled' | 'dispatch.budget-exceeded' | 'mcp.auto-memory.failed' | 'daemon.self-heal.db-corrupt' | 'feature.time_to_first_bootstrap' | 'feature.time_to_first_memory' | 'feature.time_to_first_interaction' | 'feature.time_to_first_mission_complete';
|
|
2
2
|
export interface NexusEventPayloads {
|
|
3
3
|
'system.boot': {
|
|
4
4
|
version: string;
|
|
@@ -302,6 +302,11 @@ export interface NexusEventPayloads {
|
|
|
302
302
|
stage: string;
|
|
303
303
|
error: string;
|
|
304
304
|
};
|
|
305
|
+
'tool.invocation': {
|
|
306
|
+
toolName: string;
|
|
307
|
+
durationMs: number;
|
|
308
|
+
status: 'ok' | 'error';
|
|
309
|
+
};
|
|
305
310
|
'memory.tier.promoted': {
|
|
306
311
|
memoryId: string;
|
|
307
312
|
from: string;
|
|
@@ -620,6 +625,13 @@ export interface NexusEventPayloads {
|
|
|
620
625
|
subject: string;
|
|
621
626
|
error: string;
|
|
622
627
|
};
|
|
628
|
+
'architects.event.failed': {
|
|
629
|
+
event: string;
|
|
630
|
+
operativeId?: string;
|
|
631
|
+
missionId?: string;
|
|
632
|
+
strikeTeamId?: string;
|
|
633
|
+
error: string;
|
|
634
|
+
};
|
|
623
635
|
'ledger.duplicate-prevented': {
|
|
624
636
|
fingerprint: string;
|
|
625
637
|
existingId: string;
|
|
@@ -144,7 +144,7 @@ export function initSynapse(options) {
|
|
|
144
144
|
if (payload.operativeId) {
|
|
145
145
|
db.prepare('UPDATE synapse_operatives SET health_state=\'BLOCKED\', state=\'SUSPENDED\', suspend_reason=\'manual\' WHERE id=?').run(payload.operativeId);
|
|
146
146
|
nexusEventBus.emit('synapse.operative.health.changed', { operativeId: payload.operativeId, healthState: 'BLOCKED' });
|
|
147
|
-
providers.memory.store(`[Architects:Blocked] ${payload.operativeId} ${payload.reason ?? ''}`.trim(), 0.82, ['#synapse', '#blocked']);
|
|
147
|
+
providers.memory.store(`[Architects:Blocked] ${payload.operativeId} ${payload.reason ?? ''}`.trim(), 0.82, ['#synapse', '#blocked'], undefined, 0, { provenance: { agentId: payload.operativeId, source: 'system' } });
|
|
148
148
|
}
|
|
149
149
|
};
|
|
150
150
|
const stallListener = ({ operativeId }) => {
|
|
@@ -69,6 +69,7 @@ export async function executeMandatePipeline(db, mandateText, providers, opts =
|
|
|
69
69
|
// Ensure Architects creates a blueprint + worklist for this team (auto-wiring)
|
|
70
70
|
const worklistId = providers.coordination?.getWorklistId(teamId) ?? null;
|
|
71
71
|
const blueprintId = worklistId ? `implicit-blueprint:${teamId}` : null;
|
|
72
|
+
let leadOperativeId = null;
|
|
72
73
|
const operativeIds = Array.from({ length: maxOps }, (_, index) => {
|
|
73
74
|
const skill = matched.skills[index] ?? matched.skills[0];
|
|
74
75
|
const specialist = matched.specialists[index] ?? matched.specialists[0] ?? null;
|
|
@@ -82,7 +83,11 @@ export async function executeMandatePipeline(db, mandateText, providers, opts =
|
|
|
82
83
|
strikeTeamId: teamId,
|
|
83
84
|
sortieIntervalMs: SynapseConfig.sortieIntervalMs,
|
|
84
85
|
origin: 'mandate',
|
|
86
|
+
// First operative is the team lead; subsequent members report to it.
|
|
87
|
+
reportsToOperativeId: index === 0 ? null : leadOperativeId,
|
|
85
88
|
});
|
|
89
|
+
if (index === 0)
|
|
90
|
+
leadOperativeId = operative.id;
|
|
86
91
|
nexusEventBus.emit('synapse.operative.hired', {
|
|
87
92
|
operativeId: operative.id,
|
|
88
93
|
name: operative.name,
|
|
@@ -152,7 +157,12 @@ export async function executeMandatePipeline(db, mandateText, providers, opts =
|
|
|
152
157
|
correlationId: teamId,
|
|
153
158
|
status: 'deployed',
|
|
154
159
|
});
|
|
155
|
-
providers.memory.store(`[Synapse:Mandate] ${mandateText}`, 0.85, ['#synapse', '#mandate', `#team:${teamId}`]
|
|
160
|
+
providers.memory.store(`[Synapse:Mandate] ${mandateText}`, 0.85, ['#synapse', '#mandate', `#team:${teamId}`], undefined, 0, {
|
|
161
|
+
provenance: {
|
|
162
|
+
source: 'runtime',
|
|
163
|
+
containerTags: [`#team:${teamId}`, ...(worklistId ? [`#worklist:${worklistId}`] : [])],
|
|
164
|
+
},
|
|
165
|
+
});
|
|
156
166
|
return getStrikeTeam(db, teamId);
|
|
157
167
|
})();
|
|
158
168
|
return team;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-prime",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Local-first MCP control plane for coding agents with bootstrap-orchestrate execution, memory fabric, token budgeting, and worktree-backed swarms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"nexus-prime": "dist/cli.js"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
|
+
"prepare": "git config core.hooksPath .hooks || true",
|
|
28
29
|
"nexus:dev": "node dev-orchestrator.mjs",
|
|
29
30
|
"generate:competitive-landscape": "tsx scripts/generate-competitive-landscape.ts",
|
|
30
31
|
"generate:readme-catalog": "tsx scripts/generate-readme-runtime-catalog.ts",
|