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.
@@ -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 src/' },
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 filePaths = rawFiles
589
- ?? await hctx.scanSourceFiles(hctx.getWorkspace(request.params.arguments ?? {}).repoRoot);
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', text: JSON.stringify({
691
- passed: result.passed,
692
- score: Math.round(result.score * 100),
693
- violations: result.violations,
694
- warnings: result.warnings,
695
- summary: getGuardrailEngine().format(result)
696
- }, null, 2) + nudge
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:3377`);
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
- // Fire warm-up dispatch so the new operative can introduce itself immediately
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
- fetch('/api/dispatch', {
567
- method: 'POST',
568
- headers: { 'Content-Type': 'application/json' },
569
- body: JSON.stringify({
570
- goal: 'Introduce yourself to the codebase. Read the README and 2 source files of your choice, store what you learn as memories.',
571
- operativeId,
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.respondJson(res, { dispatch, worklist: [], activeLocks: [] });
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.getSynapse();
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: 'Check stderr for `synapse-init-failed reason=...`. Common causes: better-sqlite3 native binding mismatch, corrupt state dir, missing coordination bridge.',
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 dispatch runs in the background; runId is emitted via SSE
92
- // (synapse.first-sortie.dispatched) when ready so the client doesn't race.
93
- if (body.fireFirstSortie === true && operativeId && result?.specialistId) {
94
- const repoRoot = ctx.repoRoot ?? process.cwd();
95
- scheduleFirstSortie({
96
- operative: result,
97
- repoRoot,
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
- if (r?.runId) {
102
- nexusEventBus.emit('dashboard.action', {
103
- action: 'synapse.first-sortie.dispatched',
104
- status: 'ok',
105
- target: r.runId,
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
- }).catch(() => { });
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.getSynapse();
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
- ctx.respondJson(res, ctx.getRuntime()?.getNetworkStatus?.() ?? {});
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' }).catch(() => { });
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 {
@@ -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: Number(tokens?.savedTokens || 0),
981
- forwardedTokens: Number(tokens?.forwardedTokens || 0),
982
- grossInputTokens: Number(tokens?.grossInputTokens || 0),
983
- compressionPct: Number(tokens?.compressionPct || 0),
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 : [],
@@ -828,6 +828,8 @@ export interface NexusEventPayloads {
828
828
  'orchestrator.run.complete': {
829
829
  goal: string;
830
830
  sessionId?: string;
831
+ runId?: string;
832
+ state?: string;
831
833
  durationMs?: number;
832
834
  };
833
835
  }
@@ -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
@@ -890,6 +890,7 @@ export class NexusPrime {
890
890
  });
891
891
  if (this.synapse) {
892
892
  this.synapseHealth = this.synapse.getStatus();
893
+ this.synapseInitError = null;
893
894
  console.error('[NexusPrime] Synapse lazy-initialized successfully.');
894
895
  }
895
896
  }
@@ -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.17",
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",