nexus-prime 7.9.17 → 7.9.18
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/definitions.js +3 -2
- package/dist/agents/adapters/mcp/handlers/orchestration.js +85 -13
- package/dist/agents/adapters/mcp/types.js +7 -0
- package/dist/agents/adapters/mcp.js +2 -0
- package/dist/cli/hook.js +3 -2
- package/dist/cli.js +2 -2
- package/dist/dashboard/app/views/board.js +9 -3
- package/dist/dashboard/app/views/workforce.js +56 -18
- package/dist/dashboard/routes/architects.js +9 -1
- package/dist/dashboard/routes/governance.js +69 -19
- package/dist/dashboard/routes/runtime.js +36 -2
- package/dist/dashboard/routes/workforce.d.ts +34 -1
- package/dist/dashboard/routes/workforce.js +171 -0
- package/dist/dashboard/server.js +13 -4
- package/dist/engines/event-bus.d.ts +2 -0
- package/dist/engines/orchestrator.js +11 -0
- package/dist/index.js +1 -0
- package/dist/utils/ascii-art.js +2 -0
- package/package.json +2 -2
|
@@ -292,8 +292,9 @@ export function buildMcpToolDefinitions() {
|
|
|
292
292
|
properties: {
|
|
293
293
|
goal: { type: 'string', description: 'What you are trying to accomplish' },
|
|
294
294
|
task: { type: 'string', description: 'Alias for goal (deprecated, use goal)' },
|
|
295
|
-
files: { type: 'array', items: { type: 'string' }, description: 'File paths to analyze. If omitted, auto-scans
|
|
296
|
-
budget: { type: 'number', description: 'Token budget override' }
|
|
295
|
+
files: { type: 'array', items: { type: 'string' }, description: 'File paths to analyze. If omitted, auto-scans a capped source shortlist instead of the whole repo.' },
|
|
296
|
+
budget: { type: 'number', description: 'Token budget override' },
|
|
297
|
+
maxFiles: { type: 'number', description: 'Maximum files to include when files are omitted (default 80, max 200)' }
|
|
297
298
|
},
|
|
298
299
|
required: [],
|
|
299
300
|
},
|
|
@@ -14,12 +14,28 @@ import { GhostPass, summarizeExecution } from '../../../../phantom/index.js';
|
|
|
14
14
|
import { applyExecutionPreset, resolveExecutionPreset } from '../../../../engines/execution-presets.js';
|
|
15
15
|
import { pLimit } from '../../../../engines/util/p-limit.js';
|
|
16
16
|
import { promises as fsPromises } from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
17
18
|
import { getSharedLicenseManager } from '../../../../licensing/index.js';
|
|
18
19
|
import { SessionDNAManager } from '../../../../engines/session-dna.js';
|
|
19
20
|
import { getSharedTelemetry } from '../../../../engines/telemetry-remote.js';
|
|
20
21
|
import { requireRuntime } from '../util/require-runtime.js';
|
|
21
22
|
import { ensureCrGraphBuilt } from '../../../../engines/code-review-graph-client.js';
|
|
22
23
|
import { recordFirstBootstrap } from '../../../../engines/telemetry.js';
|
|
24
|
+
function escapeMarkdownTableCell(value) {
|
|
25
|
+
return String(value ?? '')
|
|
26
|
+
.replace(/\r?\n/g, ' ')
|
|
27
|
+
.replace(/\|/g, '\\|')
|
|
28
|
+
.trim();
|
|
29
|
+
}
|
|
30
|
+
function markdownTable(headers, rows) {
|
|
31
|
+
if (rows.length === 0)
|
|
32
|
+
return '';
|
|
33
|
+
return [
|
|
34
|
+
`| ${headers.map(escapeMarkdownTableCell).join(' | ')} |`,
|
|
35
|
+
`| ${headers.map(() => '---').join(' | ')} |`,
|
|
36
|
+
...rows.map(row => `| ${row.map(escapeMarkdownTableCell).join(' | ')} |`),
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
23
39
|
export function extractSkillSelectorsFromPrompt(prompt) {
|
|
24
40
|
const selectors = new Set();
|
|
25
41
|
const add = (value) => {
|
|
@@ -282,6 +298,9 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
282
298
|
`Memory stats: prefrontal ${bootstrap.memoryStats?.prefrontal ?? 0} · hippocampus ${bootstrap.memoryStats?.hippocampus ?? 0} · cortex ${bootstrap.memoryStats?.cortex ?? 0}`,
|
|
283
299
|
`Recommended next step: ${bootstrap.recommendedNextStep || 'nexus_orchestrate'}`,
|
|
284
300
|
`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')}`,
|
|
301
|
+
bootstrap.tokenOptimization?.planMetrics
|
|
302
|
+
? `Token budget: original ${Number(bootstrap.tokenOptimization.planMetrics.originalTokens ?? 0).toLocaleString()} → planned ${Number(bootstrap.tokenOptimization.planMetrics.compressedTokens ?? 0).toLocaleString()} across ${Number(bootstrap.tokenOptimization.planMetrics.files ?? 0).toLocaleString()} file action(s)`
|
|
303
|
+
: '',
|
|
285
304
|
`Catalog health: ${bootstrap.catalogHealth?.overall || 'unknown'} · selected ${bootstrap.artifactSelectionAudit?.selected?.length || 0}`,
|
|
286
305
|
(() => {
|
|
287
306
|
try {
|
|
@@ -585,8 +604,13 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
585
604
|
const rawFiles = Array.isArray(request.params.arguments?.files)
|
|
586
605
|
? request.params.arguments.files.map(String)
|
|
587
606
|
: null;
|
|
588
|
-
const
|
|
589
|
-
|
|
607
|
+
const workspaceRoot = hctx.getWorkspace(request.params.arguments ?? {}).repoRoot;
|
|
608
|
+
const scannedFilePaths = rawFiles
|
|
609
|
+
?? await hctx.scanSourceFiles(workspaceRoot);
|
|
610
|
+
const requestedMaxFiles = Number(request.params.arguments?.maxFiles ?? request.params.arguments?.limit ?? 80);
|
|
611
|
+
const maxFiles = Math.max(1, Math.min(Number.isFinite(requestedMaxFiles) ? Math.floor(requestedMaxFiles) : 80, 200));
|
|
612
|
+
const filePaths = rawFiles ? scannedFilePaths : scannedFilePaths.slice(0, maxFiles);
|
|
613
|
+
const omittedFiles = rawFiles ? 0 : Math.max(0, scannedFilePaths.length - filePaths.length);
|
|
590
614
|
const statLimit = pLimit(8);
|
|
591
615
|
const files = await Promise.all(filePaths.map((p) => statLimit(async () => {
|
|
592
616
|
const resolved = hctx.resolveToolPath(p, request.params.arguments ?? {});
|
|
@@ -660,10 +684,14 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
660
684
|
const dedupLine = dedupTokens > 0
|
|
661
685
|
? `\n ${dedupTokens.toLocaleString()} tok deduped (${delta.unchanged.length} unchanged files)`
|
|
662
686
|
: '';
|
|
687
|
+
const scanLine = rawFiles
|
|
688
|
+
? `\n explicit file set: ${filePaths.length.toLocaleString()} file(s)`
|
|
689
|
+
: `\n auto-scan capped: ${filePaths.length.toLocaleString()}/${scannedFilePaths.length.toLocaleString()} file(s)${omittedFiles ? ` (${omittedFiles.toLocaleString()} omitted; pass files or maxFiles to override)` : ''}`;
|
|
663
690
|
const receipt = [
|
|
664
691
|
'',
|
|
665
692
|
`▸ Receipt ${plan.totalEstimatedTokens.toLocaleString()} tok in`,
|
|
666
693
|
` ${plan.savings.toLocaleString()} tok saved (${pct}% vs full read)${dedupLine}`,
|
|
694
|
+
scanLine.trimStart(),
|
|
667
695
|
` ~$${estimatedCostUsd.toFixed(4)} estimated · ~$${savedCostUsd.toFixed(4)} saved`,
|
|
668
696
|
].join('\n');
|
|
669
697
|
return { content: [{ type: 'text', text: formatted + receipt + notification + nudge }] };
|
|
@@ -673,7 +701,9 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
673
701
|
const ctx = {
|
|
674
702
|
action: String(args?.action ?? ''),
|
|
675
703
|
tokenCount: args?.tokenCount,
|
|
676
|
-
filesToModify: args?.filesToModify
|
|
704
|
+
filesToModify: Array.isArray(args?.filesToModify)
|
|
705
|
+
? args.filesToModify.map(String)
|
|
706
|
+
: (Array.isArray(args?.files) ? args.files.map(String) : undefined),
|
|
677
707
|
isDestructive: args?.isDestructive,
|
|
678
708
|
};
|
|
679
709
|
const result = getGuardrailEngine().check(ctx);
|
|
@@ -685,15 +715,47 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
685
715
|
const nudge = result.passed
|
|
686
716
|
? hctx.telemetry.planningNudge('high_call_count', {})
|
|
687
717
|
: hctx.telemetry.planningNudge('mindkit_fail', {});
|
|
718
|
+
const verdict = result.passed ? 'PASS' : 'FAIL';
|
|
719
|
+
const violationRows = result.violations.map((v) => {
|
|
720
|
+
const item = v;
|
|
721
|
+
return [
|
|
722
|
+
item.id ?? 'violation',
|
|
723
|
+
item.severity ?? 'high',
|
|
724
|
+
item.message ?? item.description ?? 'blocked by guardrail',
|
|
725
|
+
];
|
|
726
|
+
});
|
|
727
|
+
const warningRows = result.warnings.map((w) => {
|
|
728
|
+
const item = w;
|
|
729
|
+
return [
|
|
730
|
+
item.id ?? 'warning',
|
|
731
|
+
item.severity ?? 'warn',
|
|
732
|
+
item.message ?? item.description ?? 'review recommended',
|
|
733
|
+
];
|
|
734
|
+
});
|
|
735
|
+
const detailJson = JSON.stringify({
|
|
736
|
+
passed: result.passed,
|
|
737
|
+
score: Math.round(result.score * 100),
|
|
738
|
+
filesToModify: ctx.filesToModify ?? [],
|
|
739
|
+
violations: result.violations,
|
|
740
|
+
warnings: result.warnings,
|
|
741
|
+
summary: getGuardrailEngine().format(result)
|
|
742
|
+
}, null, 2);
|
|
688
743
|
return {
|
|
689
744
|
content: [{
|
|
690
|
-
type: 'text',
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
745
|
+
type: 'text',
|
|
746
|
+
text: [
|
|
747
|
+
`Mindkit check: ${verdict}`,
|
|
748
|
+
formatBullets([
|
|
749
|
+
`Score: ${Math.round(result.score * 100)}/100`,
|
|
750
|
+
`Files: ${ctx.filesToModify?.length ? ctx.filesToModify.join(', ') : 'none declared'}`,
|
|
751
|
+
`Decision: ${result.passed ? 'proceed' : 'stop and resolve guardrail violations'}`,
|
|
752
|
+
`Summary: ${getGuardrailEngine().format(result)}`,
|
|
753
|
+
]),
|
|
754
|
+
violationRows.length ? `Violations\n${markdownTable(['Rule', 'Severity', 'Message'], violationRows)}` : '',
|
|
755
|
+
warningRows.length ? `Warnings\n${markdownTable(['Rule', 'Severity', 'Message'], warningRows)}` : '',
|
|
756
|
+
`Details\n\`\`\`json\n${detailJson}\n\`\`\``,
|
|
757
|
+
nudge,
|
|
758
|
+
].filter(Boolean).join('\n\n')
|
|
697
759
|
}]
|
|
698
760
|
};
|
|
699
761
|
}
|
|
@@ -717,6 +779,17 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
717
779
|
hctx.nexusRef.storeMemory(`Ghost pass for "${goal.slice(0, 80)}": ${report.riskAreas.length} risks identified.`, 0.6, ['#ghost-pass']);
|
|
718
780
|
nexusEventBus.emit('ghost.pass', { task: goal, risks: report.riskAreas.length, workers: report.workerAssignments.length });
|
|
719
781
|
const ghostNudge = hctx.telemetry.planningNudge('ghost_pass', { risks: report.riskAreas.length });
|
|
782
|
+
const workerTable = markdownTable(['Worker', 'Approach', 'Token Budget', 'Files'], report.workerAssignments.map((worker, index) => [
|
|
783
|
+
`Worker ${index + 1}`,
|
|
784
|
+
worker.approach,
|
|
785
|
+
worker.tokenBudget.toLocaleString(),
|
|
786
|
+
worker.files.map(file => path.basename(file.path)).slice(0, 4).join(', ') || 'n/a',
|
|
787
|
+
]));
|
|
788
|
+
const riskTable = markdownTable(['Risk', 'Severity', 'Mitigation'], report.riskAreas.map((risk) => [
|
|
789
|
+
risk,
|
|
790
|
+
'review',
|
|
791
|
+
'Use the reading plan and isolate edits before mutation',
|
|
792
|
+
]));
|
|
720
793
|
// Console ASCII UI
|
|
721
794
|
const rCount = report.riskAreas.length;
|
|
722
795
|
hctx.box('👻 GHOST PASS PRE-FLIGHT', [
|
|
@@ -735,10 +808,9 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
735
808
|
`Risks: ${report.riskAreas.length ? report.riskAreas.join(' | ') : 'none detected'}`,
|
|
736
809
|
`Worker approaches: ${report.workerAssignments.length}`,
|
|
737
810
|
]),
|
|
811
|
+
riskTable ? `Risk table\n${riskTable}` : 'Risk table\nNo risks detected.',
|
|
812
|
+
workerTable ? `Worker table\n${workerTable}` : '',
|
|
738
813
|
'Reading plan\n```txt\n' + formatReadingPlan(report.readingPlan) + '\n```',
|
|
739
|
-
report.workerAssignments.length
|
|
740
|
-
? formatBullets(report.workerAssignments.map((worker, index) => `Worker ${index + 1}: ${worker.approach} · budget ${worker.tokenBudget.toLocaleString()} tokens`))
|
|
741
|
-
: '',
|
|
742
814
|
ghostNudge,
|
|
743
815
|
].filter(Boolean).join('\n\n'),
|
|
744
816
|
}],
|
|
@@ -120,6 +120,13 @@ export class SessionTelemetry {
|
|
|
120
120
|
this.noteFileIntent(args.files);
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
|
+
if (toolName === 'nexus_spawn_workers') {
|
|
124
|
+
if (this.lifecyclePhase === 'bootstrapped' || this.lifecyclePhase === 'orchestrated') {
|
|
125
|
+
this.advancePhase('working');
|
|
126
|
+
}
|
|
127
|
+
this.noteFileIntent(args.files);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
123
130
|
if (toolName === 'nexus_store_memory') {
|
|
124
131
|
if (this.isPostOrchestratePhase()) {
|
|
125
132
|
this.storeMemoryCalledPostOrchestrate = true;
|
|
@@ -136,6 +136,7 @@ const PRE_ORCHESTRATE_ALLOWED_TOOLS = new Set([
|
|
|
136
136
|
'nexus_optimize_tokens',
|
|
137
137
|
'nexus_mindkit_check',
|
|
138
138
|
'nexus_ghost_pass',
|
|
139
|
+
'nexus_spawn_workers',
|
|
139
140
|
'nexus_token_report',
|
|
140
141
|
'nexus_session_dna',
|
|
141
142
|
'nexus_run_status',
|
|
@@ -545,6 +546,7 @@ export class MCPAdapter {
|
|
|
545
546
|
code: 'orchestration-required',
|
|
546
547
|
reason: 'nexus_orchestrate has not been called for this session',
|
|
547
548
|
action: 'Call nexus_orchestrate(prompt="<your task>") before invoking mutation, worker, kernel, hook, automation, or low-level execution tools.',
|
|
549
|
+
nextTool: 'nexus_orchestrate',
|
|
548
550
|
hint: 'Nexus Prime must own planning, token budgeting, memory recall, hooks, and review gates before work starts.',
|
|
549
551
|
}, null, 2),
|
|
550
552
|
}],
|
package/dist/cli/hook.js
CHANGED
|
@@ -209,8 +209,9 @@ export async function runHookMindkit() {
|
|
|
209
209
|
if (!lock)
|
|
210
210
|
return; // no daemon — skip rather than block
|
|
211
211
|
const resp = await callDaemonTool(lock, 'nexus_mindkit_check', {
|
|
212
|
-
action: toolName,
|
|
213
|
-
files,
|
|
212
|
+
action: `${toolName}: ${files.join(', ')}`,
|
|
213
|
+
filesToModify: files,
|
|
214
|
+
isDestructive: ['Write', 'Edit', 'MultiEdit'].includes(toolName),
|
|
214
215
|
}, 6_000);
|
|
215
216
|
// Parse the tool result text to check for a block verdict
|
|
216
217
|
const text = extractResultText(resp);
|
package/dist/cli.js
CHANGED
|
@@ -717,11 +717,11 @@ program
|
|
|
717
717
|
.description('Start Nexus Prime daemon')
|
|
718
718
|
.option('--force', 'Force restart if already running')
|
|
719
719
|
.action(async (options) => {
|
|
720
|
-
const workspaceContext = resolveWorkspaceContext({ workspaceRoot: getWorkspaceRoot() });
|
|
721
720
|
try {
|
|
722
721
|
const record = await ensureDaemonManaged({ force: options.force });
|
|
722
|
+
const dashboardPort = process.env.NEXUS_DASHBOARD_PORT ?? '3377';
|
|
723
723
|
console.log(`Nexus Prime daemon running (pid ${record.pid}, ${formatDaemonAddress(record)})`);
|
|
724
|
-
console.log(`Dashboard: http://localhost
|
|
724
|
+
console.log(`Dashboard: http://localhost:${dashboardPort}`);
|
|
725
725
|
}
|
|
726
726
|
catch (err) {
|
|
727
727
|
console.error(`Failed to start daemon: ${err.message}`);
|
|
@@ -274,6 +274,7 @@ function renderHero() {
|
|
|
274
274
|
?? lt?.totalSaved
|
|
275
275
|
?? lt?.lifetime?.saved
|
|
276
276
|
?? op?.tokenOptimization?.savedTokens
|
|
277
|
+
?? op?.tokenOptimization?.estimatedSavings
|
|
277
278
|
?? t?.savedTokens
|
|
278
279
|
?? t?.saved
|
|
279
280
|
?? 0,
|
|
@@ -317,14 +318,18 @@ function getHireReadiness() {
|
|
|
317
318
|
const synapseState = S.healthData?.runtimeEnvelope?.engines?.synapse ?? {};
|
|
318
319
|
const memoryStorage = S.healthData?.memory?.storage ?? {};
|
|
319
320
|
const rawReason = S.synapseHealthRaw?.reason;
|
|
321
|
+
const deferred = synapseState.deferred === true || /deferred\s*\(lazy mode\)/i.test(`${rawReason || ''} ${synapseState.reason || ''}`);
|
|
320
322
|
const unavailable = Boolean(
|
|
321
|
-
S.synapseHealthRaw?.notReady
|
|
322
|
-
|| synapseState.ready === false
|
|
323
|
-
|| synapseState.available === false,
|
|
323
|
+
(S.synapseHealthRaw?.notReady && !deferred)
|
|
324
|
+
|| (synapseState.ready === false && !deferred)
|
|
325
|
+
|| (synapseState.available === false && !deferred),
|
|
324
326
|
);
|
|
325
327
|
if (unavailable) {
|
|
326
328
|
notes.push({ tone: 'bad', text: rawReason || synapseState.reason || 'Synapse is not ready for hires in this dashboard session.' });
|
|
327
329
|
}
|
|
330
|
+
if (deferred && !unavailable) {
|
|
331
|
+
notes.push({ tone: 'warn', text: 'Synapse is in lazy mode and will warm up on the first hire.' });
|
|
332
|
+
}
|
|
328
333
|
if (synapseState.fallbackApplied) {
|
|
329
334
|
notes.push({ tone: 'warn', text: synapseState.reason || 'Synapse is running in fallback storage mode.' });
|
|
330
335
|
}
|
|
@@ -401,6 +406,7 @@ function renderFirstRunHero() {
|
|
|
401
406
|
specialistId: btn.dataset.specid,
|
|
402
407
|
name: btn.dataset.specname,
|
|
403
408
|
budgetCapUsd: 2,
|
|
409
|
+
fireFirstSortie: true,
|
|
404
410
|
});
|
|
405
411
|
if (result.ok) {
|
|
406
412
|
setFirstRunStatus(readiness.notes.some(note => note.tone === 'warn')
|
|
@@ -514,6 +514,7 @@ function _showHireSheet(specialistId, name) {
|
|
|
514
514
|
budgetCapUsd: budget,
|
|
515
515
|
reportsToOperativeId: reportsToVal,
|
|
516
516
|
strikeTeamId: teamVal,
|
|
517
|
+
fireFirstSortie: true,
|
|
517
518
|
}, { optimistic: optimisticRecord });
|
|
518
519
|
|
|
519
520
|
// Immediately open drawer with pending state.
|
|
@@ -547,10 +548,10 @@ function _showHireSheet(specialistId, name) {
|
|
|
547
548
|
}
|
|
548
549
|
bustCache('/api/synapse/health');
|
|
549
550
|
setTimeout(load, 800);
|
|
550
|
-
//
|
|
551
|
+
// The backend materializes this hire as a Workforce agent and
|
|
552
|
+
// starts the first sortie when fireFirstSortie is true.
|
|
551
553
|
const operativeId = real.data?.operative?.id || real.data?.operative?.operativeId;
|
|
552
554
|
if (operativeId) {
|
|
553
|
-
// Insert dispatch-strip placeholder into the already-open drawer
|
|
554
555
|
const drawerBody = document.getElementById('drawer-body');
|
|
555
556
|
if (drawerBody) {
|
|
556
557
|
let stripDiv = drawerBody.querySelector('[data-dispatch-strip]');
|
|
@@ -563,22 +564,12 @@ function _showHireSheet(specialistId, name) {
|
|
|
563
564
|
_dispatches.set('__warmup__', warmupRun);
|
|
564
565
|
stripDiv.innerHTML = _buildDispatchStrip(warmupRun);
|
|
565
566
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
specialistId: specId,
|
|
573
|
-
budgetCapUsd: 0.5,
|
|
574
|
-
}),
|
|
575
|
-
}).then(r => r.json()).then(dr => {
|
|
576
|
-
_dispatches.delete('__warmup__');
|
|
577
|
-
if (dr?.runId) {
|
|
578
|
-
_dispatches.set(dr.runId, { runId: dr.runId, operativeId, status: 'queued', tokens: 0, costUsd: 0, messages: [], filesChanged: [] });
|
|
579
|
-
}
|
|
580
|
-
_refreshDrawerForOp(operativeId);
|
|
581
|
-
}).catch(() => { _dispatches.delete('__warmup__'); _refreshDrawerForOp(operativeId); });
|
|
567
|
+
const fd = real.data?.firstDispatch;
|
|
568
|
+
_dispatches.delete('__warmup__');
|
|
569
|
+
if (fd?.runId) {
|
|
570
|
+
_dispatches.set(fd.runId, { runId: fd.runId, operativeId, status: 'queued', tokens: 0, costUsd: 0, messages: [], filesChanged: [] });
|
|
571
|
+
}
|
|
572
|
+
_refreshDrawerForOp(operativeId);
|
|
582
573
|
}
|
|
583
574
|
} else {
|
|
584
575
|
if (msg) {
|
|
@@ -609,6 +600,15 @@ function _openOpDrawer(id, name) {
|
|
|
609
600
|
['Budget alloc',op.budget!=null?fmtNum(op.budget):'—'],
|
|
610
601
|
['Team',op.team||op.strikeName||'—']
|
|
611
602
|
])}</div>
|
|
603
|
+
<div class="dsec">
|
|
604
|
+
<div class="dsec-title">Agent control</div>
|
|
605
|
+
<textarea id="agent-control-input" rows="4" placeholder="Ask this agent to inspect, plan, or run a focused task"
|
|
606
|
+
style="width:100%;resize:vertical;background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:8px 10px;color:var(--text-main);font:var(--text-sm) var(--font-sans);margin-bottom:var(--space-3)"></textarea>
|
|
607
|
+
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
|
608
|
+
<button class="btn btn-primary btn-sm" id="agent-control-send">Send to agent</button>
|
|
609
|
+
<span id="agent-control-status" style="font-size:var(--text-sm);color:var(--text-muted)"></span>
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
612
|
<div data-dispatch-strip>${stripInner}</div>` });
|
|
613
613
|
// Attach stop button handlers for any already-rendered strips
|
|
614
614
|
const drawerBody = document.getElementById('drawer-body');
|
|
@@ -621,6 +621,44 @@ function _openOpDrawer(id, name) {
|
|
|
621
621
|
});
|
|
622
622
|
});
|
|
623
623
|
}
|
|
624
|
+
const sendBtn = document.getElementById('agent-control-send');
|
|
625
|
+
const input = document.getElementById('agent-control-input');
|
|
626
|
+
const status = document.getElementById('agent-control-status');
|
|
627
|
+
sendBtn?.addEventListener('click', async () => {
|
|
628
|
+
const goal = input?.value?.trim();
|
|
629
|
+
if (!goal) {
|
|
630
|
+
if (status) status.textContent = 'Enter a task first.';
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
sendBtn.disabled = true;
|
|
634
|
+
sendBtn.textContent = 'Sending…';
|
|
635
|
+
if (status) status.textContent = 'Dispatching to agent…';
|
|
636
|
+
const response = await fetch(`/api/workforce/agents/${encodeURIComponent(opId)}/chat`, {
|
|
637
|
+
method: 'POST',
|
|
638
|
+
headers: { 'Content-Type': 'application/json' },
|
|
639
|
+
body: JSON.stringify({
|
|
640
|
+
goal,
|
|
641
|
+
message: goal,
|
|
642
|
+
specialistId: op.specialistId || op.role || undefined,
|
|
643
|
+
budgetCapUsd: op.budget || 2,
|
|
644
|
+
}),
|
|
645
|
+
}).then(r => r.json().then(data => ({ ok: r.ok, data }))).catch(error => ({ ok: false, data: { error: error?.message || String(error) } }));
|
|
646
|
+
if (response.ok) {
|
|
647
|
+
if (input) input.value = '';
|
|
648
|
+
const runId = response.data?.runId;
|
|
649
|
+
if (runId) _dispatches.set(runId, { runId, operativeId: opId, status: 'queued', tokens: 0, costUsd: 0, messages: [response.data?.message || 'Agent command queued.'], filesChanged: [] });
|
|
650
|
+
if (status) status.textContent = response.data?.mode === 'orchestrator-fallback'
|
|
651
|
+
? 'Queued through Nexus orchestrator.'
|
|
652
|
+
: 'Queued to agent runtime.';
|
|
653
|
+
_refreshDrawerForOp(opId);
|
|
654
|
+
setTimeout(load, 800);
|
|
655
|
+
} else if (status) {
|
|
656
|
+
status.textContent = response.data?.hint || response.data?.error || 'Agent command failed.';
|
|
657
|
+
status.style.color = 'var(--bad)';
|
|
658
|
+
}
|
|
659
|
+
sendBtn.disabled = false;
|
|
660
|
+
sendBtn.textContent = 'Send to agent';
|
|
661
|
+
});
|
|
624
662
|
}
|
|
625
663
|
|
|
626
664
|
function _openMissionDrawer(id) {
|
|
@@ -27,7 +27,15 @@ export const handleArchitectsRoutes = async (ctx, req, res, url) => {
|
|
|
27
27
|
return true;
|
|
28
28
|
}
|
|
29
29
|
const dispatch = architects.getDispatchStatus?.() ?? null;
|
|
30
|
-
ctx.
|
|
30
|
+
const preferred = ctx.getPreferredArchitectsWorklist(url, url.searchParams.get('worklistId'), url.searchParams.get('strikeTeamId'));
|
|
31
|
+
ctx.respondJson(res, {
|
|
32
|
+
dispatch,
|
|
33
|
+
worklist: Array.isArray(preferred?.items) ? preferred.items : [],
|
|
34
|
+
worklistId: preferred?.worklistId ?? preferred?.worklist?.id ?? null,
|
|
35
|
+
activeLocks: [],
|
|
36
|
+
idle: Boolean(preferred?.idle),
|
|
37
|
+
reason: preferred?.reason ?? '',
|
|
38
|
+
});
|
|
31
39
|
return true;
|
|
32
40
|
}
|
|
33
41
|
/* ── POST /api/architects/wards/:wardId/resolve ──────────────────────── */
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { nexusEventBus } from '../../engines/event-bus.js';
|
|
2
|
+
import { getDefaultFederation } from '../../engines/federation.js';
|
|
2
3
|
import { podNetwork } from '../../engines/pod-network.js';
|
|
3
4
|
import { getSpecialist } from '../../engines/specialist-roster.js';
|
|
4
5
|
import { estimateSpecialistSortieCost } from '../../engines/specialist-cost-estimator.js';
|
|
5
6
|
import { scheduleFirstSortie } from '../../synapse/bootstrap.js';
|
|
6
7
|
import { DarwinLoop } from '../../engines/darwin-loop.js';
|
|
7
8
|
import { DarwinJournal } from '../../engines/darwin-journal.js';
|
|
9
|
+
import { materializeWorkforceAgent, runDashboardAgentControl } from './workforce.js';
|
|
8
10
|
import path from 'node:path';
|
|
9
11
|
// Surface "Pillar 2 disconnected" with a tagged empty array. The marker keeps
|
|
10
12
|
// existing `.map()`/`.length` iteration code working while letting the frontend
|
|
@@ -27,6 +29,22 @@ function notInitializedObject(pillar) {
|
|
|
27
29
|
items: [],
|
|
28
30
|
};
|
|
29
31
|
}
|
|
32
|
+
function getSynapseForMutation(ctx) {
|
|
33
|
+
let synapse = ctx.getSynapse();
|
|
34
|
+
if (synapse)
|
|
35
|
+
return synapse;
|
|
36
|
+
const reinit = ctx.getReinitSynapse?.();
|
|
37
|
+
if (reinit) {
|
|
38
|
+
try {
|
|
39
|
+
reinit();
|
|
40
|
+
synapse = ctx.getSynapse();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
synapse = undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return synapse;
|
|
47
|
+
}
|
|
30
48
|
export const handleGovernanceRoutes = async (ctx, req, res, url) => {
|
|
31
49
|
if (req.method === 'GET' && url.pathname === '/api/synapse/teams') {
|
|
32
50
|
const synapse = ctx.getSynapse();
|
|
@@ -44,13 +62,13 @@ export const handleGovernanceRoutes = async (ctx, req, res, url) => {
|
|
|
44
62
|
return true;
|
|
45
63
|
}
|
|
46
64
|
if (req.method === 'POST' && url.pathname === '/api/synapse/hire') {
|
|
47
|
-
const synapse = ctx
|
|
65
|
+
const synapse = getSynapseForMutation(ctx);
|
|
48
66
|
if (!synapse) {
|
|
49
67
|
const initErr = ctx.getSynapseInitError();
|
|
50
68
|
ctx.respondJson(res, {
|
|
51
69
|
error: initErr?.message ?? 'synapse-unavailable',
|
|
52
70
|
code: 'synapse-init-failed',
|
|
53
|
-
hint: '
|
|
71
|
+
hint: 'Synapse could not warm for this hire request. Run `nexus-prime doctor`, then retry from the dashboard.',
|
|
54
72
|
at: initErr?.at ?? null,
|
|
55
73
|
}, 503);
|
|
56
74
|
return true;
|
|
@@ -87,30 +105,54 @@ export const handleGovernanceRoutes = async (ctx, req, res, url) => {
|
|
|
87
105
|
const pricing = specialistProfile ? estimateSpecialistSortieCost(specialistProfile) : null;
|
|
88
106
|
const operativeId = result?.id ?? result?.operativeId ?? null;
|
|
89
107
|
const baseUrl = ctx.getAddress() ?? 'http://localhost:3377';
|
|
108
|
+
const workforceAgent = operativeId ? materializeWorkforceAgent(ctx, {
|
|
109
|
+
operativeId,
|
|
110
|
+
name: result?.name ?? name ?? specialistProfile?.name ?? specialistId ?? 'Dashboard agent',
|
|
111
|
+
role: result?.roleTitle ?? specialistProfile?.name ?? null,
|
|
112
|
+
specialistId: result?.specialistId ?? specialistId ?? null,
|
|
113
|
+
budgetCapUsd: result?.budgetCapUsd ?? body.budgetCapUsd ?? null,
|
|
114
|
+
}) : null;
|
|
115
|
+
let firstDispatch = null;
|
|
90
116
|
// Optionally fire a warm-up first sortie (body.fireFirstSortie === true).
|
|
91
|
-
// Non-blocking
|
|
92
|
-
//
|
|
93
|
-
if (body.fireFirstSortie === true && operativeId
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
repoName: path.basename(repoRoot),
|
|
117
|
+
// Non-blocking: the dashboard gets an immediate hire response while the
|
|
118
|
+
// Workforce job and run lifecycle continue through SSE and kanban state.
|
|
119
|
+
if (body.fireFirstSortie === true && operativeId) {
|
|
120
|
+
firstDispatch = { queued: true, operativeId, mode: 'pending' };
|
|
121
|
+
runDashboardAgentControl(ctx, new URL(`${baseUrl}/api/workforce/agents/${encodeURIComponent(operativeId)}/dispatch`), {
|
|
122
|
+
operativeId,
|
|
123
|
+
specialistId: result?.specialistId ?? specialistId ?? null,
|
|
99
124
|
budgetCapUsd: result?.budgetCapUsd ?? 0.50,
|
|
125
|
+
title: `First sortie: ${result?.name ?? specialistProfile?.name ?? operativeId}`,
|
|
126
|
+
goal: 'Introduce yourself to the codebase. Read the README and two source files, then report the repo shape and next useful task.',
|
|
100
127
|
}).then(r => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
nexusEventBus.emit('dashboard.action', {
|
|
129
|
+
action: 'synapse.first-sortie.dispatched',
|
|
130
|
+
status: 'ok',
|
|
131
|
+
target: r.runId ?? r.job?.id ?? operativeId,
|
|
132
|
+
});
|
|
133
|
+
}).catch((error) => {
|
|
134
|
+
nexusEventBus.emit('dashboard.action', {
|
|
135
|
+
action: 'synapse.first-sortie.failed',
|
|
136
|
+
status: 'fail',
|
|
137
|
+
target: error?.message ?? String(error),
|
|
138
|
+
});
|
|
139
|
+
if (result?.specialistId) {
|
|
140
|
+
const repoRoot = ctx.repoRoot ?? process.cwd();
|
|
141
|
+
scheduleFirstSortie({
|
|
142
|
+
operative: result,
|
|
143
|
+
repoRoot,
|
|
144
|
+
repoName: path.basename(repoRoot),
|
|
145
|
+
budgetCapUsd: result?.budgetCapUsd ?? 0.50,
|
|
146
|
+
}).catch(() => { });
|
|
107
147
|
}
|
|
108
|
-
})
|
|
148
|
+
});
|
|
109
149
|
}
|
|
110
150
|
ctx.respondJson(res, {
|
|
111
151
|
operative: result,
|
|
152
|
+
agent: workforceAgent,
|
|
112
153
|
pricing,
|
|
113
154
|
firstSortieEstimate: pricing,
|
|
155
|
+
firstDispatch,
|
|
114
156
|
dashboardUrl: operativeId ? `${baseUrl}/#workforce/${operativeId}` : `${baseUrl}/#workforce`,
|
|
115
157
|
}, 201);
|
|
116
158
|
}
|
|
@@ -124,7 +166,7 @@ export const handleGovernanceRoutes = async (ctx, req, res, url) => {
|
|
|
124
166
|
// getSynapse() triggers ensureSynapseInit(); getArchitects() triggers ensureArchitectsInit()
|
|
125
167
|
// + reconnectSynapseCoordination() so worklistId is non-null in the pipeline.
|
|
126
168
|
ctx.getArchitects();
|
|
127
|
-
const synapse = ctx
|
|
169
|
+
const synapse = getSynapseForMutation(ctx);
|
|
128
170
|
if (!synapse) {
|
|
129
171
|
const initErr = ctx.getSynapseInitError();
|
|
130
172
|
ctx.respondJson(res, {
|
|
@@ -234,7 +276,15 @@ export const handleGovernanceRoutes = async (ctx, req, res, url) => {
|
|
|
234
276
|
return true;
|
|
235
277
|
}
|
|
236
278
|
if (req.method === 'GET' && url.pathname === '/api/federation') {
|
|
237
|
-
|
|
279
|
+
const runtimeStatus = ctx.getRuntime()?.getNetworkStatus?.();
|
|
280
|
+
const snapshot = runtimeStatus && Object.keys(runtimeStatus).length > 0
|
|
281
|
+
? runtimeStatus
|
|
282
|
+
: getDefaultFederation().getSnapshot();
|
|
283
|
+
ctx.respondJson(res, {
|
|
284
|
+
relayMode: snapshot?.relay?.connected ? 'active' : 'local-only',
|
|
285
|
+
peers: snapshot?.knownPeers ?? [],
|
|
286
|
+
...snapshot,
|
|
287
|
+
});
|
|
238
288
|
return true;
|
|
239
289
|
}
|
|
240
290
|
if (req.method === 'POST' && url.pathname === '/api/synapse/reinit') {
|
|
@@ -28,6 +28,26 @@ function normalizeLifetimeTokenPayload(record) {
|
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
function normalizeTokenSummaryPayload(record) {
|
|
32
|
+
const savedTokens = Number(record?.savedTokens ?? record?.totalSavedTokens ?? record?.saved ?? 0);
|
|
33
|
+
const grossInputTokens = Number(record?.grossInputTokens ?? record?.totalGrossInputTokens ?? record?.gross ?? 0);
|
|
34
|
+
const compressedTokens = Number(record?.compressedTokens ?? record?.totalCompressedTokens ?? record?.net ?? 0);
|
|
35
|
+
const forwardedTokens = Number(record?.forwardedTokens ?? record?.tokensForwarded ?? compressedTokens);
|
|
36
|
+
const compressionPct = Number.isFinite(Number(record?.compressionPct))
|
|
37
|
+
? Number(record.compressionPct)
|
|
38
|
+
: (grossInputTokens > 0 ? Math.round((savedTokens / grossInputTokens) * 100) : 0);
|
|
39
|
+
return {
|
|
40
|
+
...record,
|
|
41
|
+
savedTokens,
|
|
42
|
+
grossInputTokens,
|
|
43
|
+
compressedTokens,
|
|
44
|
+
forwardedTokens,
|
|
45
|
+
compressionPct,
|
|
46
|
+
saved: savedTokens,
|
|
47
|
+
gross: grossInputTokens,
|
|
48
|
+
net: compressedTokens,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
31
51
|
async function readGitBranch(repoRoot) {
|
|
32
52
|
try {
|
|
33
53
|
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
|
@@ -97,7 +117,7 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
|
|
|
97
117
|
if (req.method === 'GET' && url.pathname === '/api/tokens/summary') {
|
|
98
118
|
await ctx.respondCachedJson(res, `tokens-summary:${url.search}`, 2_000, async () => {
|
|
99
119
|
const snapshot = ctx.resolveRuntimeSnapshot(url);
|
|
100
|
-
return snapshot?.tokens ?? ctx.getRuntime()?.getTokenTelemetrySummary?.() ?? {};
|
|
120
|
+
return normalizeTokenSummaryPayload(snapshot?.tokens ?? ctx.getRuntime()?.getTokenTelemetrySummary?.() ?? {});
|
|
101
121
|
});
|
|
102
122
|
return true;
|
|
103
123
|
}
|
|
@@ -298,7 +318,21 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
|
|
|
298
318
|
// TTV: first goal submission from the dashboard counts as first interaction.
|
|
299
319
|
void recordFirstInteraction().catch(() => { });
|
|
300
320
|
// Fire-and-forget: orchestration runs in the background; caller polls /api/runs/:id.
|
|
301
|
-
orchestrator.orchestrate(goal, { source: 'dashboard' })
|
|
321
|
+
orchestrator.orchestrate(goal, { source: 'dashboard' })
|
|
322
|
+
.then((run) => {
|
|
323
|
+
nexusEventBus.emit('dashboard.action', {
|
|
324
|
+
action: 'orchestrate.complete',
|
|
325
|
+
status: run?.state === 'failed' ? 'fail' : 'ok',
|
|
326
|
+
target: run?.runId ?? runId,
|
|
327
|
+
});
|
|
328
|
+
})
|
|
329
|
+
.catch((err) => {
|
|
330
|
+
nexusEventBus.emit('dashboard.action', {
|
|
331
|
+
action: 'orchestrate.failed',
|
|
332
|
+
status: 'fail',
|
|
333
|
+
target: err instanceof Error ? err.message : String(err),
|
|
334
|
+
});
|
|
335
|
+
});
|
|
302
336
|
nexusEventBus.emit('dashboard.action', { action: 'orchestrate.enqueue', status: 'queued', target: runId });
|
|
303
337
|
ctx.respondJson(res, { queued: true, runId }, 202);
|
|
304
338
|
return true;
|
|
@@ -5,5 +5,38 @@
|
|
|
5
5
|
* GET /api/workforce/workers — worker list
|
|
6
6
|
* GET /api/workforce/jobs — job list with optional ?status= and ?client= filters
|
|
7
7
|
*/
|
|
8
|
-
import type { DashboardRouteHandler } from '../types.js';
|
|
8
|
+
import type { DashboardRouteContext, DashboardRouteHandler } from '../types.js';
|
|
9
|
+
export declare function materializeWorkforceAgent(ctx: DashboardRouteContext, input: {
|
|
10
|
+
operativeId: string;
|
|
11
|
+
name?: string | null;
|
|
12
|
+
role?: string | null;
|
|
13
|
+
specialistId?: string | null;
|
|
14
|
+
budgetCapUsd?: number | null;
|
|
15
|
+
}): import("../../workforce/types.js").Worker;
|
|
16
|
+
export declare function runDashboardAgentControl(ctx: DashboardRouteContext, url: URL, input: {
|
|
17
|
+
operativeId: string;
|
|
18
|
+
goal: string;
|
|
19
|
+
specialistId?: string | null;
|
|
20
|
+
budgetCapUsd?: number | null;
|
|
21
|
+
preferredRuntime?: string | null;
|
|
22
|
+
title?: string | null;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
ok: boolean;
|
|
25
|
+
mode: string;
|
|
26
|
+
worker: import("../../workforce/types.js").Worker;
|
|
27
|
+
job: import("../../workforce/types.js").Job;
|
|
28
|
+
runId: string;
|
|
29
|
+
invoker: string;
|
|
30
|
+
state?: undefined;
|
|
31
|
+
message?: undefined;
|
|
32
|
+
} | {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
mode: string;
|
|
35
|
+
worker: import("../../workforce/types.js").Worker;
|
|
36
|
+
job: import("../../workforce/types.js").Job;
|
|
37
|
+
runId: string;
|
|
38
|
+
state: import("../../phantom/runtime.js").ExecutionState;
|
|
39
|
+
message: string;
|
|
40
|
+
invoker?: undefined;
|
|
41
|
+
}>;
|
|
9
42
|
export declare const handleWorkforceRoutes: DashboardRouteHandler;
|
|
@@ -7,12 +7,151 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { getWorkforce } from '../../workforce/index.js';
|
|
9
9
|
import { getSpans, listRecentRuns } from '../../engines/orchestrator/store.js';
|
|
10
|
+
import { pushDispatch } from '../../engines/dispatch/push-dispatch.js';
|
|
11
|
+
import { nexusEventBus } from '../../engines/event-bus.js';
|
|
12
|
+
import { resolveWorkspaceContext } from '../../engines/workspace-resolver.js';
|
|
13
|
+
import path from 'path';
|
|
10
14
|
const VALID_WORKER_STATUSES = new Set(['idle', 'active', 'stale', 'retired']);
|
|
11
15
|
const VALID_JOB_STATUSES = new Set(['backlog', 'ready', 'claimed', 'wip', 'blocked', 'review', 'done', 'failed', 'cancelled']);
|
|
12
16
|
function safeLimit(raw, def) {
|
|
13
17
|
const n = Number(raw);
|
|
14
18
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : def;
|
|
15
19
|
}
|
|
20
|
+
function trimString(value) {
|
|
21
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
22
|
+
}
|
|
23
|
+
function getRepoIdentity(ctx, url) {
|
|
24
|
+
const selected = url ? ctx.getSelectedRepoIdentity(url) : null;
|
|
25
|
+
const repoRoot = trimString(selected?.repoRoot) ?? ctx.repoRoot ?? process.cwd();
|
|
26
|
+
try {
|
|
27
|
+
const workspace = resolveWorkspaceContext({ workspaceRoot: repoRoot });
|
|
28
|
+
return {
|
|
29
|
+
repoRoot: workspace.repoRoot,
|
|
30
|
+
repoName: trimString(selected?.repoName) ?? workspace.repoName ?? path.basename(workspace.repoRoot),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { repoRoot, repoName: trimString(selected?.repoName) ?? (path.basename(repoRoot) || 'workspace') };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function getOperativeName(ctx, operativeId) {
|
|
38
|
+
const operatives = ctx.getSynapse()?.getOperativeHealth?.() ?? [];
|
|
39
|
+
const found = Array.isArray(operatives)
|
|
40
|
+
? operatives.find((op) => op?.id === operativeId || op?.operativeId === operativeId)
|
|
41
|
+
: null;
|
|
42
|
+
return trimString(found?.name) ?? trimString(found?.roleTitle) ?? trimString(found?.role) ?? `Agent ${operativeId.slice(0, 8)}`;
|
|
43
|
+
}
|
|
44
|
+
export function materializeWorkforceAgent(ctx, input) {
|
|
45
|
+
const wf = getWorkforce(ctx.repoRoot ?? process.cwd());
|
|
46
|
+
const worker = wf.registerWorker({
|
|
47
|
+
id: input.operativeId,
|
|
48
|
+
name: input.name ?? getOperativeName(ctx, input.operativeId),
|
|
49
|
+
role: input.role ?? input.specialistId ?? 'synapse-operative',
|
|
50
|
+
client: 'synapse',
|
|
51
|
+
budgetCapUsd: Number(input.budgetCapUsd ?? 2),
|
|
52
|
+
});
|
|
53
|
+
nexusEventBus.emit('dashboard.action', {
|
|
54
|
+
action: 'workforce.agent.materialized',
|
|
55
|
+
status: 'ok',
|
|
56
|
+
target: worker.id,
|
|
57
|
+
});
|
|
58
|
+
return worker;
|
|
59
|
+
}
|
|
60
|
+
export async function runDashboardAgentControl(ctx, url, input) {
|
|
61
|
+
const operativeId = input.operativeId;
|
|
62
|
+
const goal = input.goal.trim();
|
|
63
|
+
const wf = getWorkforce(ctx.repoRoot ?? process.cwd());
|
|
64
|
+
const worker = wf.getWorker(operativeId) ?? materializeWorkforceAgent(ctx, {
|
|
65
|
+
operativeId,
|
|
66
|
+
specialistId: input.specialistId ?? null,
|
|
67
|
+
budgetCapUsd: input.budgetCapUsd ?? 2,
|
|
68
|
+
});
|
|
69
|
+
const job = wf.enqueueJob({
|
|
70
|
+
title: input.title ?? (goal.slice(0, 80) || 'Agent command'),
|
|
71
|
+
tier: 'tactical',
|
|
72
|
+
client: 'dashboard-agent',
|
|
73
|
+
priority: 8,
|
|
74
|
+
budgetCapUsd: input.budgetCapUsd ?? 2,
|
|
75
|
+
payload: {
|
|
76
|
+
goal,
|
|
77
|
+
operativeId,
|
|
78
|
+
specialistId: input.specialistId ?? worker.role,
|
|
79
|
+
source: 'dashboard-agent-control',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const claim = wf.claimJob(job.id, worker.id, 15 * 60_000);
|
|
83
|
+
if (claim)
|
|
84
|
+
wf.advanceToWip(job.id, worker.id);
|
|
85
|
+
const { repoRoot, repoName } = getRepoIdentity(ctx, url);
|
|
86
|
+
const orchestrator = ctx.getOrchestrator();
|
|
87
|
+
const memory = ctx.getMemory();
|
|
88
|
+
const memories = await memory?.recall?.(goal, 6).then((items) => (items.map((content, index) => ({ id: `recall-${index + 1}`, content, tier: 'recall', priority: 0.6 })))).catch(() => []) ?? [];
|
|
89
|
+
const files = await orchestrator?.listTopologyFiles?.(20).catch(() => []) ?? [];
|
|
90
|
+
const graphLines = files.slice(0, 12).map((file) => path.relative(repoRoot, file) || file);
|
|
91
|
+
const crGraphContext = graphLines.length
|
|
92
|
+
? `Repo graph shortlist for this command:\n${graphLines.map((file) => `- ${file}`).join('\n')}`
|
|
93
|
+
: null;
|
|
94
|
+
try {
|
|
95
|
+
const dispatch = await pushDispatch({
|
|
96
|
+
goal,
|
|
97
|
+
specialistId: input.specialistId ?? worker.role ?? 'engineering.rapid-prototyper',
|
|
98
|
+
operativeId,
|
|
99
|
+
budgetCapUsd: Number(input.budgetCapUsd ?? worker.budgetCapUsd ?? 2),
|
|
100
|
+
workingDir: repoRoot,
|
|
101
|
+
repoName,
|
|
102
|
+
preferredRuntime: input.preferredRuntime ?? undefined,
|
|
103
|
+
memories,
|
|
104
|
+
files,
|
|
105
|
+
crGraphContext,
|
|
106
|
+
storeMemory: async (content, priority, tags) => { memory?.store?.(content, priority, tags); },
|
|
107
|
+
});
|
|
108
|
+
const cleanup = [
|
|
109
|
+
nexusEventBus.on('dispatch.complete', (payload) => {
|
|
110
|
+
if (payload.runId !== dispatch.runId)
|
|
111
|
+
return;
|
|
112
|
+
wf.recordTokens(job.id, worker.id, Number(payload.tokensUsed ?? 0));
|
|
113
|
+
if (payload.success === false)
|
|
114
|
+
wf.failJob(job.id, worker.id);
|
|
115
|
+
else
|
|
116
|
+
wf.completeJob(job.id, worker.id);
|
|
117
|
+
cleanup.forEach((unsubscribe) => unsubscribe());
|
|
118
|
+
}),
|
|
119
|
+
nexusEventBus.on('dispatch.failed', (payload) => {
|
|
120
|
+
if (payload.runId !== dispatch.runId)
|
|
121
|
+
return;
|
|
122
|
+
wf.failJob(job.id, worker.id);
|
|
123
|
+
cleanup.forEach((unsubscribe) => unsubscribe());
|
|
124
|
+
}),
|
|
125
|
+
];
|
|
126
|
+
return { ok: true, mode: 'dispatch', worker, job: wf.getJob(job.id) ?? job, runId: dispatch.runId, invoker: dispatch.invoker };
|
|
127
|
+
}
|
|
128
|
+
catch (dispatchError) {
|
|
129
|
+
if (!orchestrator) {
|
|
130
|
+
wf.failJob(job.id, worker.id);
|
|
131
|
+
throw dispatchError;
|
|
132
|
+
}
|
|
133
|
+
const run = await orchestrator.orchestrate(goal, {
|
|
134
|
+
source: 'dashboard-agent-control',
|
|
135
|
+
workers: 1,
|
|
136
|
+
files,
|
|
137
|
+
specialistId: input.specialistId ?? worker.role,
|
|
138
|
+
operativeId,
|
|
139
|
+
});
|
|
140
|
+
if (run.state === 'failed')
|
|
141
|
+
wf.failJob(job.id, worker.id);
|
|
142
|
+
else
|
|
143
|
+
wf.completeJob(job.id, worker.id);
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
mode: 'orchestrator-fallback',
|
|
147
|
+
worker,
|
|
148
|
+
job: wf.getJob(job.id) ?? job,
|
|
149
|
+
runId: run.runId,
|
|
150
|
+
state: run.state,
|
|
151
|
+
message: dispatchError?.message ? `Push dispatch unavailable; ran through Nexus orchestrator instead. ${dispatchError.message}` : 'Ran through Nexus orchestrator.',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
16
155
|
export const handleWorkforceRoutes = async (ctx, req, res, url) => {
|
|
17
156
|
if (!url.pathname.startsWith('/api/workforce/'))
|
|
18
157
|
return false;
|
|
@@ -48,6 +187,38 @@ export const handleWorkforceRoutes = async (ctx, req, res, url) => {
|
|
|
48
187
|
ctx.respondJson(res, { jobs });
|
|
49
188
|
return true;
|
|
50
189
|
}
|
|
190
|
+
const agentControlMatch = req.method === 'POST'
|
|
191
|
+
? /^\/api\/workforce\/agents\/([^/]+)\/(?:chat|dispatch)$/.exec(url.pathname)
|
|
192
|
+
: null;
|
|
193
|
+
if (agentControlMatch) {
|
|
194
|
+
const operativeId = decodeURIComponent(agentControlMatch[1]);
|
|
195
|
+
const body = await ctx.readJsonBody(req);
|
|
196
|
+
const goal = trimString(body.goal) ?? trimString(body.message) ?? trimString(body.prompt);
|
|
197
|
+
if (!goal) {
|
|
198
|
+
ctx.respondJson(res, { error: 'goal-required' }, 400);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const result = await runDashboardAgentControl(ctx, url, {
|
|
203
|
+
operativeId,
|
|
204
|
+
goal,
|
|
205
|
+
specialistId: trimString(body.specialistId),
|
|
206
|
+
budgetCapUsd: typeof body.budgetCapUsd === 'number' ? body.budgetCapUsd : undefined,
|
|
207
|
+
preferredRuntime: trimString(body.preferredRuntime),
|
|
208
|
+
title: trimString(body.title),
|
|
209
|
+
});
|
|
210
|
+
ctx.respondJson(res, result, 202);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
ctx.respondJson(res, {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: error?.message ?? String(error),
|
|
216
|
+
code: 'agent-control-failed',
|
|
217
|
+
hint: 'No headless runtime or local orchestrator could accept this agent command.',
|
|
218
|
+
}, 503);
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
51
222
|
if (url.pathname === '/api/workforce/runs') {
|
|
52
223
|
const limit = safeLimit(url.searchParams.get('limit'), 20);
|
|
53
224
|
try {
|
package/dist/dashboard/server.js
CHANGED
|
@@ -974,13 +974,22 @@ export class DashboardServer {
|
|
|
974
974
|
const tokens = snapshot?.tokens ?? this.getRuntime()?.getTokenTelemetrySummary?.() ?? {};
|
|
975
975
|
const budget = usage?.sourceAwareTokenBudget ?? {};
|
|
976
976
|
const autoApplied = Boolean(usage?.tokenAutoApplied);
|
|
977
|
+
const savedTokens = Number(tokens?.savedTokens ?? tokens?.totalSavedTokens ?? tokens?.saved ?? 0);
|
|
978
|
+
const forwardedTokens = Number(tokens?.forwardedTokens ?? tokens?.tokensForwarded ?? tokens?.compressedTokens ?? 0);
|
|
979
|
+
const grossInputTokens = Number(tokens?.grossInputTokens ?? tokens?.totalGrossInputTokens ?? tokens?.gross ?? 0);
|
|
980
|
+
const compressionPct = Number.isFinite(Number(tokens?.compressionPct))
|
|
981
|
+
? Number(tokens.compressionPct)
|
|
982
|
+
: (grossInputTokens > 0 ? Math.round((savedTokens / grossInputTokens) * 100) : 0);
|
|
977
983
|
return {
|
|
978
984
|
applied: Boolean(usage?.tokenOptimizationApplied || budget?.applied || autoApplied),
|
|
979
985
|
autoApplied,
|
|
980
|
-
savedTokens
|
|
981
|
-
forwardedTokens
|
|
982
|
-
grossInputTokens
|
|
983
|
-
compressionPct
|
|
986
|
+
savedTokens,
|
|
987
|
+
forwardedTokens,
|
|
988
|
+
grossInputTokens,
|
|
989
|
+
compressionPct,
|
|
990
|
+
estimatedSavings: Number(budget?.estimatedSavings ?? savedTokens ?? 0),
|
|
991
|
+
candidateFiles: Array.isArray(budget?.candidateFiles) ? budget.candidateFiles : [],
|
|
992
|
+
selectedFiles: Array.isArray(budget?.selectedFiles) ? budget.selectedFiles : [],
|
|
984
993
|
reason: budget?.reason || 'Token optimization has not reported a source-aware budget yet.',
|
|
985
994
|
dominantSource: budget?.dominantSource || null,
|
|
986
995
|
dropped: Array.isArray(budget?.dropped) ? budget.dropped : [],
|
|
@@ -995,6 +995,7 @@ export class OrchestratorEngine {
|
|
|
995
995
|
this.memoryBackgroundWorker?.start();
|
|
996
996
|
}
|
|
997
997
|
async orchestrate(task, options = {}) {
|
|
998
|
+
const orchestrateStartedAt = Date.now();
|
|
998
999
|
this.checkCircuit();
|
|
999
1000
|
try {
|
|
1000
1001
|
nexusEventBus.emit('orchestrator.run.start', {
|
|
@@ -1425,6 +1426,16 @@ export class OrchestratorEngine {
|
|
|
1425
1426
|
executionLedger: ledger,
|
|
1426
1427
|
knowledgeFabric,
|
|
1427
1428
|
});
|
|
1429
|
+
try {
|
|
1430
|
+
nexusEventBus.emit('orchestrator.run.complete', {
|
|
1431
|
+
goal: task.slice(0, 200),
|
|
1432
|
+
sessionId: this.sessionState.sessionId,
|
|
1433
|
+
runId: run.runId,
|
|
1434
|
+
state: run.state,
|
|
1435
|
+
durationMs: Date.now() - orchestrateStartedAt,
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
catch { /* non-fatal */ }
|
|
1428
1439
|
return run;
|
|
1429
1440
|
}
|
|
1430
1441
|
/**
|
package/dist/index.js
CHANGED
package/dist/utils/ascii-art.js
CHANGED
|
@@ -52,6 +52,8 @@ export const ASCII_ART = {
|
|
|
52
52
|
║ 🔄 Orchestration engines live ║
|
|
53
53
|
║ 📡 MCP control plane active ║
|
|
54
54
|
║ ⚙️ Worker swarm standing by ║
|
|
55
|
+
║ Dashboard: http://localhost:3377 ║
|
|
56
|
+
║ Open later: nexus-prime start ║
|
|
55
57
|
║ ║
|
|
56
58
|
║ Build: ${buildDate.slice(0, 19).padEnd(47, ' ')}║
|
|
57
59
|
║ Node : ${nodeVersion.padEnd(47, ' ')}║
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-prime",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.18",
|
|
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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"test:public": "tsx test/public-surface.test.ts",
|
|
50
50
|
"test:synapse": "tsx src/synapse/__tests__/run.ts",
|
|
51
51
|
"test:architects": "tsx src/architects/__tests__/run.ts",
|
|
52
|
-
"test": "npm run build && tsx test/basic.test.ts && tsx test/memory.test.ts && tsx test/memory-regressions.test.ts && tsx test/memory-bridge.test.ts && tsx test/automation-runtime.test.ts && tsx test/session-dna-search.test.ts && tsx test/channel-gateway.test.ts && tsx test/semantic-ranking.test.ts && tsx test/storage-maintenance.test.ts && tsx test/ngram-index.test.ts && tsx test/security-shield.test.ts && tsx test/compaction-sentinel.test.ts && tsx test/context-compressor.test.ts && tsx test/embedder.test.ts && tsx test/skill-learner.test.ts && tsx test/skill-distribution.test.ts && tsx test/darwin-integration.test.ts && tsx test/github-bridge.test.ts && tsx test/telemetry-remote.test.ts && tsx test/orchestrator-engine.test.ts && tsx test/phase9.test.ts && tsx test/work-ledger.test.ts && tsx src/verify-token-scoring.ts && tsx test/phantom.test.ts && tsx test/rag-collections.test.ts && tsx test/dashboard.test.ts && tsx test/docs.test.ts && tsx test/competitive-landscape.test.ts && tsx test/runtime-upgrade-path.test.ts && tsx test/runtime-setup.test.ts && tsx test/control-plane-integration.test.ts && tsx test/runtime-timeout.test.ts && tsx test/mcp-dashboard-contract.test.ts && tsx test/dashboard-surfaces.test.ts && tsx test/startup-and-runtime-regressions.test.ts && tsx test/mcp-readiness-truth.test.ts && tsx test/mcp-stdio-session.test.ts && tsx test/kernel-context.test.ts && tsx test/kernel-execution.test.ts && tsx test/kernel-runtime.test.ts && tsx test/adapter-boundary.test.ts && tsx test/mcp-deprecation.test.ts && tsx test/dashboard-mutations.test.ts && tsx test/hooks-adapter.test.ts && tsx test/admin-adapter.test.ts && tsx test/pkg-subexport.test.ts && tsx test/dashboard-sse-only.test.ts && tsx test/license-sync-offline.test.ts && tsx test/mcp-killlist-v2.test.ts && tsx test/uninstall.test.ts && tsx test/uninstall-lifecycle.test.ts && tsx test/install-arch-upgrade.test.ts && tsx test/unregister-configs.test.ts && tsx test/cleanup-storage.test.ts && tsx test/dashboard-memory-truthfulness.test.ts && tsx test/runtime-lifecycle.test.ts && tsx test/orchestrate-pipeline.test.ts && tsx test/daemon-supervisor.test.ts && tsx test/auto-optimize-tokens.test.ts && npm run test:synapse && npm run test:architects && npm run test:public",
|
|
52
|
+
"test": "npm run build && tsx test/basic.test.ts && tsx test/memory.test.ts && tsx test/memory-regressions.test.ts && tsx test/memory-bridge.test.ts && tsx test/automation-runtime.test.ts && tsx test/session-dna-search.test.ts && tsx test/channel-gateway.test.ts && tsx test/semantic-ranking.test.ts && tsx test/storage-maintenance.test.ts && tsx test/ngram-index.test.ts && tsx test/security-shield.test.ts && tsx test/compaction-sentinel.test.ts && tsx test/context-compressor.test.ts && tsx test/embedder.test.ts && tsx test/skill-learner.test.ts && tsx test/skill-distribution.test.ts && tsx test/darwin-integration.test.ts && tsx test/github-bridge.test.ts && tsx test/telemetry-remote.test.ts && tsx test/orchestrator-engine.test.ts && tsx test/phase9.test.ts && tsx test/work-ledger.test.ts && tsx src/verify-token-scoring.ts && tsx test/phantom.test.ts && tsx test/rag-collections.test.ts && tsx test/dashboard.test.ts && tsx test/docs.test.ts && tsx test/competitive-landscape.test.ts && tsx test/runtime-upgrade-path.test.ts && tsx test/runtime-setup.test.ts && tsx test/control-plane-integration.test.ts && tsx test/runtime-timeout.test.ts && tsx test/mcp-dashboard-contract.test.ts && tsx test/dashboard-surfaces.test.ts && tsx test/startup-and-runtime-regressions.test.ts && tsx test/mcp-readiness-truth.test.ts && tsx test/mcp-stdio-session.test.ts && tsx test/kernel-context.test.ts && tsx test/kernel-execution.test.ts && tsx test/kernel-runtime.test.ts && tsx test/adapter-boundary.test.ts && tsx test/mcp-deprecation.test.ts && tsx test/dashboard-mutations.test.ts && tsx test/dashboard-agent-control.test.ts && tsx test/hooks-adapter.test.ts && tsx test/admin-adapter.test.ts && tsx test/pkg-subexport.test.ts && tsx test/dashboard-sse-only.test.ts && tsx test/license-sync-offline.test.ts && tsx test/mcp-killlist-v2.test.ts && tsx test/uninstall.test.ts && tsx test/uninstall-lifecycle.test.ts && tsx test/install-arch-upgrade.test.ts && tsx test/unregister-configs.test.ts && tsx test/cleanup-storage.test.ts && tsx test/dashboard-memory-truthfulness.test.ts && tsx test/runtime-lifecycle.test.ts && tsx test/orchestrate-pipeline.test.ts && tsx test/daemon-supervisor.test.ts && tsx test/auto-optimize-tokens.test.ts && npm run test:synapse && npm run test:architects && npm run test:public",
|
|
53
53
|
"lint": "eslint src --ext .ts",
|
|
54
54
|
"audit:prod": "npm audit --omit=dev",
|
|
55
55
|
"smoke:release": "tsx scripts/release-smoke.ts",
|