nexus-prime 7.9.25 → 7.9.26

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.
@@ -479,6 +479,38 @@
479
479
  color: var(--text-main); margin-bottom: 4px;
480
480
  }
481
481
  .frh-sub { font-size: var(--text-sm); color: var(--text-dim); }
482
+ .frh-steps {
483
+ display: grid;
484
+ grid-template-columns: repeat(3, minmax(0, 1fr));
485
+ gap: 8px;
486
+ margin-bottom: 14px;
487
+ }
488
+ .frh-step {
489
+ display: grid;
490
+ grid-template-columns: auto 1fr;
491
+ grid-template-rows: auto auto;
492
+ gap: 1px 8px;
493
+ align-items: center;
494
+ padding: 9px 10px;
495
+ border: 1px solid color-mix(in oklch, var(--accent) 24%, var(--border));
496
+ border-radius: var(--radius);
497
+ background: color-mix(in oklch, var(--accent) 4%, var(--bg-panel));
498
+ min-width: 0;
499
+ }
500
+ .frh-step span {
501
+ grid-row: 1 / span 2;
502
+ display: inline-grid;
503
+ place-items: center;
504
+ width: 22px;
505
+ height: 22px;
506
+ border-radius: 50%;
507
+ background: color-mix(in oklch, var(--accent) 18%, transparent);
508
+ color: var(--accent);
509
+ font-family: var(--font-mono);
510
+ font-size: 0.72rem;
511
+ }
512
+ .frh-step strong { font-size: 0.78rem; color: var(--text-main); min-width: 0; }
513
+ .frh-step small { font-size: 0.72rem; color: var(--text-dim); min-width: 0; }
482
514
  .frh-command {
483
515
  display: grid;
484
516
  grid-template-columns: minmax(220px, 1fr) auto auto;
@@ -492,6 +524,7 @@
492
524
  transition: border-color 0.15s;
493
525
  }
494
526
  .frh-pick:hover { border-color: var(--accent); }
527
+ .frh-pick-loading { border-style: dashed; }
495
528
  .frh-pick-name { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text-main); }
496
529
  .frh-pick-desc { font-size: 0.78rem; color: var(--text-dim); flex: 1; }
497
530
  .frh-pick-cost { font-family: var(--font-mono); font-size: 0.75rem; color: var(--accent); margin-top: 2px; }
@@ -507,6 +540,7 @@
507
540
  }
508
541
  @media (max-width: 768px) {
509
542
  #kanban-board { grid-template-columns: repeat(2, 1fr); }
543
+ .frh-steps,
510
544
  .frh-command,
511
545
  .frh-picks { grid-template-columns: 1fr; }
512
546
  }
@@ -376,7 +376,6 @@ function renderFirstRunHero() {
376
376
  if (hasOps || (alreadySeen && hasRuns)) return;
377
377
 
378
378
  const specs = (S.curatedSpecialists || []).slice(0, 3);
379
- if (!specs.length) return; // Still loading — will re-render when prefetch resolves
380
379
  const readiness = getHireReadiness();
381
380
  const noticesHtml = readiness.notes.length
382
381
  ? `<div class="frh-notices" style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px">
@@ -389,23 +388,33 @@ function renderFirstRunHero() {
389
388
  card.className = 'first-run-hero card';
390
389
  card.innerHTML = `
391
390
  <div class="frh-header">
392
- <div class="frh-title">${hasRuns ? 'Your next hire' : 'Start the first real run'}</div>
393
- <div class="frh-sub">${hasRuns ? 'Hire a specialist to start running tasks autonomously.' : 'Run a goal from the dashboard or hire a specialist. The run will appear in Board and Context Log.'}</div>
391
+ <div class="frh-title">${hasRuns ? 'Choose the next worker' : 'Start the first real run'}</div>
392
+ <div class="frh-sub">${hasRuns ? 'Hire a specialist or queue a goal. Nexus will show the route, budget, workers, and verification here.' : 'Queue one goal from the dashboard. Nexus will route it, show who gets hired, and write the run trail into Board and Context Log.'}</div>
394
393
  </div>
395
394
  ${noticesHtml}
395
+ <div class="frh-steps" aria-label="Onboarding steps">
396
+ <div class="frh-step"><span>1</span><strong>Describe goal</strong><small>Run or hire from here.</small></div>
397
+ <div class="frh-step"><span>2</span><strong>Watch route</strong><small>Board shows workers and budget.</small></div>
398
+ <div class="frh-step"><span>3</span><strong>Verify proof</strong><small>Context Log keeps artifacts.</small></div>
399
+ </div>
396
400
  <div class="frh-command">
397
401
  <input id="frh-goal-input" class="form-input" type="text" placeholder="Inspect this repo and suggest the next fix" autocomplete="off">
398
- <button class="btn btn-primary btn-sm" id="frh-run-btn">Run goal</button>
402
+ <button class="btn btn-primary btn-sm" id="frh-run-btn">Run first goal</button>
399
403
  <button class="btn btn-sm" id="frh-context-btn">Open context</button>
400
404
  </div>
401
405
  <div class="frh-picks">
402
- ${specs.map(s => `
406
+ ${specs.length ? specs.map(s => `
403
407
  <div class="frh-pick" data-specid="${esc(s.specialistId)}" data-specname="${esc(s.name)}">
404
408
  <div class="frh-pick-name">${esc(s.name)}</div>
405
409
  <div class="frh-pick-desc">${esc((s.description||'').slice(0, 72))}${(s.description||'').length > 72 ? '…' : ''}</div>
406
410
  <div class="frh-pick-cost">~$${esc(String(s.pricing?.typical ?? '?'))}/sortie</div>
407
411
  <button class="btn btn-primary btn-sm frh-hire-btn" data-specid="${esc(s.specialistId)}" data-specname="${esc(s.name)}" ${readiness.unavailable ? 'disabled title="Synapse is not ready"' : ''}>Hire</button>
408
- </div>`).join('')}
412
+ </div>`).join('') : `
413
+ <div class="frh-pick frh-pick-loading">
414
+ <div class="frh-pick-name">Specialists loading</div>
415
+ <div class="frh-pick-desc">You can run a goal immediately. Hiring picks will appear when the catalog responds.</div>
416
+ <div class="frh-pick-cost">route first, hire second</div>
417
+ </div>`}
409
418
  </div>
410
419
  <div id="frh-status" style="display:none;margin-top:12px;font-size:var(--text-sm)"></div>
411
420
  <button class="btn btn-ghost btn-sm frh-dismiss" style="margin-top:var(--space-3)">Dismiss</button>`;
@@ -424,6 +433,7 @@ function renderFirstRunHero() {
424
433
  const result = await post('/api/orchestrate', { goal, source: 'dashboard-onboarding' });
425
434
  if (result.ok) {
426
435
  setFirstRunStatus('Run queued. Board and Context Log will update as Nexus writes artifacts.');
436
+ try { localStorage.setItem(FIRST_RUN_KEY, '1'); } catch { /* ignore */ }
427
437
  bustCache('/api/runs?limit=12');
428
438
  bustCache('/api/events');
429
439
  setTimeout(load, 900);
@@ -431,7 +441,7 @@ function renderFirstRunHero() {
431
441
  setFirstRunStatus(result.error || 'Run failed to queue.', 'bad');
432
442
  if (button) {
433
443
  button.disabled = false;
434
- button.textContent = 'Run goal';
444
+ button.textContent = 'Run first goal';
435
445
  }
436
446
  }
437
447
  });
@@ -508,6 +518,21 @@ function buildKanbanCols() {
508
518
  if (sg) cols[sg].push({id:w.id||w.workerId||w.goal,goal:w.goal||w.task||w.approach||'(worker)',status:st,tokens:w.tokensUsed||w.budget,time:w.startedAt||w.createdAt,role:w.role});
509
519
  }
510
520
  }
521
+ const ghost = S.lastDecomposition?.autoGhostPass || S.lastCompletion?.autoGhostPass || op?.orchestration?.autoGhostPass || op?.autoGhostPass;
522
+ if (ghost && (ghost.applied || ghost.policy?.reason)) {
523
+ const risks = Array.isArray(ghost.riskAreas) ? ghost.riskAreas.length : 0;
524
+ const reason = ghost.policy?.reason ? ` · ${ghost.policy.reason}` : '';
525
+ cols.ghostpass.push({
526
+ id: 'auto-ghostpass',
527
+ goal: ghost.applied
528
+ ? `Auto Ghost Pass: ${risks} risk area${risks === 1 ? '' : 's'}, ${ghost.workerApproaches || 0} approach${ghost.workerApproaches === 1 ? '' : 'es'}`
529
+ : `Auto Ghost Pass skipped${reason}`,
530
+ status: ghost.applied ? 'reviewing' : 'skipped',
531
+ tokens: ghost.estimatedTokens,
532
+ time: S.lastDecomposition?.ts || S.lastCompletion?.ts,
533
+ role: 'ghost-pass',
534
+ });
535
+ }
511
536
  for (const r of (S.runs||[]).slice(0,8)) {
512
537
  const runId = r.runId || r.id;
513
538
  if (!runId) continue;
@@ -874,11 +899,16 @@ function renderOrchestrationPipeline() {
874
899
  const more = arr.length > 6 ? ` <span style="color:var(--muted)">+${arr.length - 6}</span>` : '';
875
900
  return `<div style="margin:4px 0"><span style="color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:0.5px">${esc(label)}</span> <span style="font-family:var(--font-mono);font-size:12px">${head}${more}</span></div>`;
876
901
  };
902
+ const ghostChip = (ghost) => ghost
903
+ ? chip('ghost-pass', ghost.applied
904
+ ? `${ghost.riskAreas?.length ?? 0} risks · ${ghost.workerApproaches ?? 0} approaches`
905
+ : `skipped${ghost.policy?.reason ? ` · ${ghost.policy.reason}` : ''}`)
906
+ : '';
877
907
  const decBlock = dec ? `
878
908
  <div style="border-left:3px solid var(--accent);padding:8px 12px;margin-bottom:8px">
879
909
  <div style="font-size:13px;font-weight:600;margin-bottom:4px">Decomposition · run ${esc((dec.runId || '').slice(-8))}</div>
880
910
  <div style="font-size:12px;color:var(--muted);margin-bottom:6px">${esc(dec.goal || '')}</div>
881
- <div>${chip('intent', dec.intent || 'auto')}${chip('crew', dec.crew || 'baseline')}${chip('workers', dec.workers ?? 0)}${chip('phases', dec.phases ?? 0)}</div>
911
+ <div>${chip('intent', dec.intent || 'auto')}${chip('crew', dec.crew || 'baseline')}${chip('workers', dec.workers ?? 0)}${chip('phases', dec.phases ?? 0)}${ghostChip(dec.autoGhostPass)}</div>
882
912
  ${chipList('specialists', dec.specialists)}
883
913
  ${chipList('skills', dec.skills)}
884
914
  ${chipList('files', dec.files)}
@@ -888,7 +918,7 @@ function renderOrchestrationPipeline() {
888
918
  <div style="border-left:3px solid ${stateColor(cmpState)};padding:8px 12px">
889
919
  <div style="font-size:13px;font-weight:600;margin-bottom:4px">Completion · run ${esc((cmp.runId || '').slice(-8))} · <span style="color:${stateColor(cmpState)}">${esc(cmpState || '')}</span></div>
890
920
  <div style="font-size:12px;color:var(--muted);margin-bottom:6px">${esc(cmp.result || '')}</div>
891
- <div>${chip('verified', `${cmp.verifiedWorkers ?? 0}/${cmp.totalWorkers ?? 0}`)}${chip('saved', `${fmtNum(cmp.savedTokens ?? 0)} t`)}${chip('compression', `${cmp.compressionPct ?? 0}%`)}${chip('duration', `${Math.round((cmp.durationMs ?? 0) / 100) / 10}s`)}</div>
921
+ <div>${chip('verified', `${cmp.verifiedWorkers ?? 0}/${cmp.totalWorkers ?? 0}`)}${chip('saved', `${fmtNum(cmp.savedTokens ?? 0)} t`)}${chip('compression', `${cmp.compressionPct ?? 0}%`)}${chip('duration', `${Math.round((cmp.durationMs ?? 0) / 100) / 10}s`)}${ghostChip(cmp.autoGhostPass)}</div>
892
922
  </div>` : '';
893
923
  const spineBlock = spine ? _buildDecisionSpineMiniHtml(spine) : '';
894
924
  const headerNote = same ? '' : (dec && cmp ? '<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Showing latest decomposition + most recent completion (different runs)</div>' : '');
@@ -93,6 +93,16 @@ export interface NexusEventPayloads {
93
93
  files: string[];
94
94
  workers: number;
95
95
  phases: number;
96
+ autoGhostPass?: {
97
+ applied: boolean;
98
+ riskAreas: string[];
99
+ workerApproaches: number;
100
+ estimatedTokens: number;
101
+ policy?: {
102
+ enabled?: boolean;
103
+ reason?: string;
104
+ };
105
+ };
96
106
  };
97
107
  'orchestration.completed': {
98
108
  runId: string;
@@ -104,6 +114,12 @@ export interface NexusEventPayloads {
104
114
  compressionPct: number;
105
115
  durationMs: number;
106
116
  result: string;
117
+ autoGhostPass?: {
118
+ applied: boolean;
119
+ riskAreas: string[];
120
+ workerApproaches: number;
121
+ estimatedTokens: number;
122
+ };
107
123
  };
108
124
  'session.summaryBootstrap': {
109
125
  originalTokens: number;
@@ -1036,7 +1036,7 @@ export class OrchestratorEngine {
1036
1036
  // Non-fatal: bootstrap receipt is a safety net, not a hard dependency
1037
1037
  }
1038
1038
  const [army, prepared] = await this.runParallelPhases(this.induce(task), this._prepareExecution(task, options));
1039
- const { intent, phases, primaryClient, bootstrapManifest, latestDNA, memoryMatches, memoryStats, candidateFiles, knowledgeFabric, plannedFiles, planner, selections, catalogHealth, tokenBudget, workerCount, mode, taskGraph, workerPlan, autoGhostPass, crSignals, } = prepared;
1039
+ const { intent, phases, primaryClient, bootstrapManifest, latestDNA, memoryMatches, memoryStats, candidateFiles, knowledgeFabric, plannedFiles, planner, selections, catalogHealth, tokenBudget, workerCount, mode, taskGraph, workerPlan, autoGhostPass, autoGhostPassDecision, crSignals, } = prepared;
1040
1040
  this.runtime.recordClientToolCall('nexus_orchestrate', {
1041
1041
  orchestrateCalled: true,
1042
1042
  plannerCalled: true,
@@ -1314,6 +1314,8 @@ export class OrchestratorEngine {
1314
1314
  instructionPacket,
1315
1315
  executionLedger: ledger,
1316
1316
  knowledgeFabric,
1317
+ autoGhostPass,
1318
+ autoGhostPassDecision,
1317
1319
  });
1318
1320
  this.executionDedupeStore.set(fingerprint, {
1319
1321
  id: ledger.runId,
@@ -24,6 +24,10 @@ export function getNexusHookSpec() {
24
24
  matcher: 'Edit|Write|MultiEdit',
25
25
  hooks: [{ type: 'command', command: 'nexus-prime hook mindkit', timeout: 10 }],
26
26
  },
27
+ {
28
+ matcher: 'MultiEdit',
29
+ hooks: [{ type: 'command', command: 'nexus-prime hook ghost-pass', timeout: 15 }],
30
+ },
27
31
  ],
28
32
  PostToolUse: [
29
33
  {
@@ -101,6 +101,12 @@ export interface ExecutionTask {
101
101
  instructionPacket?: InstructionPacket;
102
102
  executionLedger?: ExecutionLedger;
103
103
  knowledgeFabric?: KnowledgeFabricBundle;
104
+ autoGhostPass?: GhostReport;
105
+ autoGhostPassDecision?: {
106
+ enabled: boolean;
107
+ reason: string;
108
+ contextHash?: string;
109
+ };
104
110
  }
105
111
  export interface WorkerSkillOverlay {
106
112
  base: string[];
@@ -225,6 +225,16 @@ export class SubAgentRuntime {
225
225
  files: (task.files ?? []).slice(0, 24),
226
226
  workers: planner.plannerState.selectedSpecialists.length || 1,
227
227
  phases: planner.plannerState.ledger.length,
228
+ autoGhostPass: {
229
+ applied: Boolean(task.autoGhostPass),
230
+ riskAreas: task.autoGhostPass?.riskAreas ?? [],
231
+ workerApproaches: task.autoGhostPass?.workerAssignments?.length ?? 0,
232
+ estimatedTokens: task.autoGhostPass?.totalEstimatedTokens ?? 0,
233
+ policy: task.autoGhostPassDecision ? {
234
+ enabled: task.autoGhostPassDecision.enabled,
235
+ reason: task.autoGhostPassDecision.reason,
236
+ } : undefined,
237
+ },
228
238
  });
229
239
  }
230
240
  catch { /* best-effort */ }
@@ -765,6 +775,12 @@ export class SubAgentRuntime {
765
775
  compressionPct: Number(tt?.compressionPct ?? 0),
766
776
  durationMs: Date.now() - runStartedAt,
767
777
  result: String(applied.summary ?? '').slice(0, 280),
778
+ autoGhostPass: {
779
+ applied: Boolean(task.autoGhostPass),
780
+ riskAreas: task.autoGhostPass?.riskAreas ?? [],
781
+ workerApproaches: task.autoGhostPass?.workerAssignments?.length ?? 0,
782
+ estimatedTokens: task.autoGhostPass?.totalEstimatedTokens ?? 0,
783
+ },
768
784
  });
769
785
  }
770
786
  catch { /* best-effort */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.9.25",
3
+ "version": "7.9.26",
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",