nexus-prime 7.9.16 → 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
  }],
@@ -44,8 +44,10 @@ export async function handleRuntimeGroup(toolName, hctx, request, args, ctx) {
44
44
  ? `- **Trial remaining**: ${licStatus.trialDaysRemaining} day${licStatus.trialDaysRemaining === 1 ? '' : 's'}`
45
45
  : '',
46
46
  licStatus.activationRequired
47
- ? `- **Agent motion**: Stop paid-tier work and ask the user to request or activate a Nexus Prime license now.`
48
- : `- **Agent motion**: 3-day no-license grace is active; ask the user to request a license before day 4.`,
47
+ ? `- **Agent motion**: License activation is required for paid-tier work; keep license/status tools available.`
48
+ : licStatus.trialPhase === 'activation'
49
+ ? `- **Agent motion**: Trial remains active; ask the user to request or activate a license soon, then keep working.`
50
+ : `- **Agent motion**: No license is mandatory during the first 3 days; ask the user to request a license soon.`,
49
51
  ].filter((line) => Boolean(line)) : [];
50
52
  const lines = [
51
53
  `## License Status`,
@@ -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;
@@ -154,6 +161,8 @@ export class SessionTelemetry {
154
161
  needsOptimizeTokens(currentToolName) {
155
162
  if (currentToolName === 'nexus_optimize_tokens')
156
163
  return false;
164
+ if (currentToolName === 'nexus_mindkit_check')
165
+ return false;
157
166
  if (this.lifecyclePhase === 'pre-bootstrap')
158
167
  return false;
159
168
  if (this.tokenAutoApplied)
@@ -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);
@@ -39,6 +39,13 @@ const SETUP_CLIENT_BY_IDE = {
39
39
  cline: 'cline',
40
40
  codex: 'codex',
41
41
  };
42
+ function resolveSetupWorkspaceRoot(explicitWorkspaceRoot) {
43
+ return resolveWorkspaceContext({
44
+ workspaceRoot: explicitWorkspaceRoot,
45
+ cwd: process.cwd(),
46
+ env: process.env,
47
+ }).workspaceRoot;
48
+ }
42
49
  function isSetupMarker(value) {
43
50
  if (!value || typeof value !== 'object') {
44
51
  return false;
@@ -89,7 +96,7 @@ export function writeSetupMarker(marker, markerPath = SETUP_MARKER_PATH) {
89
96
  return nextMarker;
90
97
  }
91
98
  export async function configureIDE(ide, opts = {}) {
92
- const workspaceRoot = resolve(opts.workspaceRoot ?? process.cwd());
99
+ const workspaceRoot = resolveSetupWorkspaceRoot(opts.workspaceRoot);
93
100
  const mappedClient = SETUP_CLIENT_BY_IDE[ide];
94
101
  if (mappedClient) {
95
102
  const definition = getSetupDefinition(mappedClient, {
@@ -285,7 +292,7 @@ export function runArchitectureUpgrade(opts = {}) {
285
292
  }
286
293
  /** Run the install wizard: detect IDEs and write MCP configs. */
287
294
  export async function runInstallWizard(opts = {}) {
288
- const workspaceRoot = resolve(opts.workspaceRoot ?? process.cwd());
295
+ const workspaceRoot = resolveSetupWorkspaceRoot(opts.workspaceRoot);
289
296
  const verbose = opts.verbose ?? true;
290
297
  const dryRun = opts.dryRun ?? false;
291
298
  const setupMarker = readSetupMarker();
@@ -414,6 +421,7 @@ export async function cliSetup(opts = []) {
414
421
  const port = process.env.NEXUS_DASHBOARD_PORT ?? '3377';
415
422
  const dashUrl = `http://localhost:${port}`;
416
423
  const isNew = options.isNewUser ?? false;
424
+ const workspaceRoot = resolveSetupWorkspaceRoot();
417
425
  const ANSI = { cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m', dim: '\x1b[2m', reset: '\x1b[0m' };
418
426
  const isTTY = process.stdout.isTTY === true && !process.env.NO_COLOR;
419
427
  const c = (s, k) => isTTY ? `${ANSI[k]}${s}${ANSI.reset}` : s;
@@ -438,7 +446,7 @@ export async function cliSetup(opts = []) {
438
446
  }
439
447
  catch { /* non-fatal */ }
440
448
  };
441
- const result = await runInstallWizard({ dryRun, verbose: false });
449
+ const result = await runInstallWizard({ workspaceRoot, dryRun, verbose: false });
442
450
  const allIDEs = [...result.configured, ...result.skipped, ...result.errors.map(e => e.ide)];
443
451
  if (allIDEs.length > 0) {
444
452
  for (const ide of allIDEs) {
@@ -499,7 +507,7 @@ export async function cliSetup(opts = []) {
499
507
  let daemonReady = false;
500
508
  if (!dryRun) {
501
509
  try {
502
- const workspace = resolveWorkspaceContext({ workspaceRoot: process.cwd() });
510
+ const workspace = resolveWorkspaceContext({ workspaceRoot, cwd: process.cwd(), env: process.env });
503
511
  await withSpinner('Connecting to daemon', ensureDaemonReady(workspace, { timeoutMs: DEFAULT_DAEMON_READY_TIMEOUT_MS, entrypoint: process.argv[1] }));
504
512
  daemonReady = true;
505
513
  log(` ${c('✓', 'green')} Dashboard: ${c(dashUrl, 'cyan')}`);
package/dist/cli.js CHANGED
@@ -454,11 +454,13 @@ function resolveClaudeDesktopConfigPath() {
454
454
  return join(homedir(), '.claude', 'claude_desktop_config.json');
455
455
  }
456
456
  function getSetupDefinition(clientId) {
457
+ const workspaceRoot = getWorkspaceRoot();
457
458
  const instructionFiles = buildInstructionFiles(clientId);
458
459
  if (clientId === 'codex') {
459
460
  return {
460
461
  id: clientId,
461
462
  label: 'Codex',
463
+ workspaceRoot,
462
464
  configPath: resolveCodexConfigPath(),
463
465
  instructionFiles,
464
466
  };
@@ -467,6 +469,7 @@ function getSetupDefinition(clientId) {
467
469
  return {
468
470
  id: clientId,
469
471
  label: 'Cursor',
472
+ workspaceRoot,
470
473
  configPath: join(homedir(), '.cursor', 'mcp.json'),
471
474
  instructionFiles,
472
475
  };
@@ -475,7 +478,8 @@ function getSetupDefinition(clientId) {
475
478
  return {
476
479
  id: clientId,
477
480
  label: 'Claude Code',
478
- configPath: join(getWorkspaceRoot(), '.mcp.json'),
481
+ workspaceRoot,
482
+ configPath: join(workspaceRoot, '.mcp.json'),
479
483
  instructionFiles,
480
484
  };
481
485
  }
@@ -483,6 +487,7 @@ function getSetupDefinition(clientId) {
483
487
  return {
484
488
  id: clientId,
485
489
  label: 'Claude Desktop',
490
+ workspaceRoot,
486
491
  configPath: resolveClaudeDesktopConfigPath(),
487
492
  instructionFiles,
488
493
  };
@@ -491,6 +496,7 @@ function getSetupDefinition(clientId) {
491
496
  return {
492
497
  id: clientId,
493
498
  label: 'Opencode',
499
+ workspaceRoot,
494
500
  configPath: join(homedir(), '.config', 'opencode', 'opencode.json'),
495
501
  instructionFiles,
496
502
  };
@@ -499,6 +505,7 @@ function getSetupDefinition(clientId) {
499
505
  return {
500
506
  id: clientId,
501
507
  label: 'Windsurf',
508
+ workspaceRoot,
502
509
  configPath: join(homedir(), '.windsurf', 'mcp.json'),
503
510
  instructionFiles,
504
511
  };
@@ -507,6 +514,7 @@ function getSetupDefinition(clientId) {
507
514
  return {
508
515
  id: clientId,
509
516
  label: 'Antigravity / OpenClaw',
517
+ workspaceRoot,
510
518
  configPath: join(homedir(), '.antigravity', 'mcp.json'),
511
519
  instructionFiles,
512
520
  };
@@ -515,6 +523,7 @@ function getSetupDefinition(clientId) {
515
523
  return {
516
524
  id: clientId,
517
525
  label: 'Aider',
526
+ workspaceRoot,
518
527
  configPath: join(homedir(), '.aider', 'mcp.json'),
519
528
  instructionFiles,
520
529
  };
@@ -523,6 +532,7 @@ function getSetupDefinition(clientId) {
523
532
  return {
524
533
  id: clientId,
525
534
  label: 'Continue.dev',
535
+ workspaceRoot,
526
536
  configPath: join(homedir(), '.continue', 'config.json'),
527
537
  instructionFiles,
528
538
  };
@@ -531,6 +541,7 @@ function getSetupDefinition(clientId) {
531
541
  return {
532
542
  id: clientId,
533
543
  label: 'OpenClaw',
544
+ workspaceRoot,
534
545
  configPath: join(homedir(), '.openclaw', 'openclaw.json'),
535
546
  instructionFiles,
536
547
  };
@@ -538,6 +549,7 @@ function getSetupDefinition(clientId) {
538
549
  return {
539
550
  id: clientId,
540
551
  label: 'Cline',
552
+ workspaceRoot,
541
553
  configPath: join(homedir(), '.vscode', 'cline-mcp.json'),
542
554
  instructionFiles,
543
555
  };
@@ -548,10 +560,10 @@ function installSetup(definition) {
548
560
  writeCodexMcpConfig(definition.configPath);
549
561
  }
550
562
  else if (definition.id === 'opencode') {
551
- writeOpencodeConfig(definition.configPath, getWorkspaceRoot());
563
+ writeOpencodeConfig(definition.configPath, definition.workspaceRoot);
552
564
  }
553
565
  else {
554
- writeStandardMcpConfig(definition.configPath, getWorkspaceRoot());
566
+ writeStandardMcpConfig(definition.configPath, definition.workspaceRoot);
555
567
  }
556
568
  }
557
569
  for (const file of definition.instructionFiles) {
@@ -585,12 +597,12 @@ function printSetupPreview(definition) {
585
597
  console.log(JSON.stringify(definition.id === 'opencode'
586
598
  ? {
587
599
  mcp: {
588
- servers: [{ id: 'nexus-prime', ...buildStandardMcpServerConfig(getWorkspaceRoot()) }]
600
+ servers: [{ id: 'nexus-prime', ...buildStandardMcpServerConfig(definition.workspaceRoot) }]
589
601
  }
590
602
  }
591
603
  : {
592
604
  mcpServers: {
593
- 'nexus-prime': buildStandardMcpServerConfig(getWorkspaceRoot())
605
+ 'nexus-prime': buildStandardMcpServerConfig(definition.workspaceRoot)
594
606
  }
595
607
  }, null, 2));
596
608
  }
@@ -612,10 +624,10 @@ function hasExpectedConfig(definition) {
612
624
  return false;
613
625
  if (definition.id === 'opencode') {
614
626
  const server = parsed?.mcp?.['nexus-prime'];
615
- return Boolean(server && isStableNexusMcpServerConfig(server, 'environment'));
627
+ return Boolean(server && isStableNexusMcpServerConfig(server, 'environment', definition.workspaceRoot));
616
628
  }
617
629
  const server = parsed?.mcpServers?.['nexus-prime'];
618
- return Boolean(server && isStableNexusMcpServerConfig(server));
630
+ return Boolean(server && isStableNexusMcpServerConfig(server, 'env', definition.workspaceRoot));
619
631
  }
620
632
  catch {
621
633
  return false;
@@ -705,11 +717,11 @@ program
705
717
  .description('Start Nexus Prime daemon')
706
718
  .option('--force', 'Force restart if already running')
707
719
  .action(async (options) => {
708
- const workspaceContext = resolveWorkspaceContext({ workspaceRoot: getWorkspaceRoot() });
709
720
  try {
710
721
  const record = await ensureDaemonManaged({ force: options.force });
722
+ const dashboardPort = process.env.NEXUS_DASHBOARD_PORT ?? '3377';
711
723
  console.log(`Nexus Prime daemon running (pid ${record.pid}, ${formatDaemonAddress(record)})`);
712
- console.log(`Dashboard: http://localhost:3377`);
724
+ console.log(`Dashboard: http://localhost:${dashboardPort}`);
713
725
  }
714
726
  catch (err) {
715
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 ──────────────────────── */