nexus-prime 6.0.2 → 6.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/dist/agents/adapters/mcp/dispatch.js +22 -7
- package/dist/agents/adapters/mcp/handlers/orchestration.js +33 -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/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 +11 -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 +6 -1
- package/dist/synapse/mandate/pipeline.js +5 -0
- package/package.json +1 -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
|
}],
|
|
@@ -463,21 +465,23 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
463
465
|
`Run ID: ${execution.runId}`,
|
|
464
466
|
`Summary: ${summarizeExecution(execution)}`,
|
|
465
467
|
`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
468
|
`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'}`,
|
|
469
|
+
`Tokens: saved ${Number(execution.tokenTelemetry?.savedTokens || 0).toLocaleString()} · compression ${Number(execution.tokenTelemetry?.compressionPct || 0)}%`,
|
|
474
470
|
autoTokenApplyNote || null,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
471
|
+
...(detailLevel === 'debug' ? [
|
|
472
|
+
`Specialists: ${execution.plannerState?.selectedSpecialists.map((specialist) => specialist.name).slice(0, 4).join(', ') || 'none selected'}`,
|
|
473
|
+
`Assets: ${(execution.activeSkills || []).length} skills · ${(execution.activeWorkflows || []).length} workflows · ${(runtimeUsage.artifactSelectionAudit?.selected?.length || 0)} audited selections`,
|
|
474
|
+
`Task graph: ${runtimeUsage.taskGraph?.phases?.length || execution.taskGraph?.phases?.length || 0} phases · ${runtimeUsage.workerPlan?.totalWorkers || execution.workerPlan?.totalWorkers || 0} workers`,
|
|
475
|
+
`Catalog health: ${runtimeUsage.catalogHealth?.overall || 'unknown'}`,
|
|
476
|
+
preset ? `Preset: ${preset.name} (${preset.id})` : null,
|
|
477
|
+
`Worktrees: ${runtimeUsage.worktreeHealth?.overall || 'unknown'} · repaired ${runtimeUsage.worktreeHealth?.repairedEntries || 0} · broken ${runtimeUsage.worktreeHealth?.brokenEntries || 0}`,
|
|
478
|
+
`RAG: ${(runtimeUsage.ragUsageSummary?.attachedCollections || execution.knowledgeFabric?.rag.attachedCollections.length || 0)} attached · ${(runtimeUsage.ragUsageSummary?.retrievedChunks || execution.knowledgeFabric?.rag.hits.length || 0)} retrieved`,
|
|
479
|
+
`Memory scopes: ${Object.entries(runtimeUsage.memoryScopeUsage?.byScope || execution.memoryScopeUsage?.byScope || {}).map(([scope, count]) => `${scope}:${count}`).join(' · ') || 'awaiting shared/session reads'}`,
|
|
480
|
+
`Payload ref: ${workspace.stateKey} · ${detailLevel}`,
|
|
481
|
+
] : []),
|
|
478
482
|
]),
|
|
479
483
|
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),
|
|
484
|
+
detailLevel === 'debug' ? formatJsonDetails('Structured details', payload) : '',
|
|
481
485
|
hctx.formatRemainingProtocolSteps(),
|
|
482
486
|
].filter(Boolean).join('\n\n'),
|
|
483
487
|
}],
|
|
@@ -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
|
}
|
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
|
}
|
|
@@ -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' | '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;
|
|
@@ -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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-prime",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.1.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",
|