nexus-prime 7.9.18 → 7.9.20

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.
@@ -976,11 +976,10 @@ export function buildMcpToolDefinitions() {
976
976
  required: ['taskId', 'goal', 'findings'],
977
977
  },
978
978
  },
979
- // When NEXUS_DISABLE_WORKFORCE=1 (the daemon default since v7.6.1),
979
+ // When NEXUS_DISABLE_WORKFORCE=1 (explicit break-glass mode),
980
980
  // strip the 24 nexus_synapse_* / nexus_architects_* tools from the
981
981
  // catalog so the model doesn't see surface area for engines that
982
- // aren't initialized at runtime. Set NEXUS_DISABLE_WORKFORCE=0 to
983
- // re-enable both engines + their tools.
982
+ // aren't initialized at runtime.
984
983
  ...(process.env.NEXUS_DISABLE_WORKFORCE === '1' ? [] : synapseToolDefinitions),
985
984
  ...(process.env.NEXUS_DISABLE_WORKFORCE === '1' ? [] : architectsToolDefinitions),
986
985
  // ── Workforce (unified worker+job layer) ──────────────────────────
@@ -173,7 +173,7 @@ export async function handleGovernanceGroup(toolName, hctx, request, args, ctx)
173
173
  const manifest = ensureBootstrap({
174
174
  packageRoot: PROJECT_ROOT,
175
175
  workspaceRoot: hctx.nexusRef.getWorkspaceContext().workspaceRoot,
176
- phase: 'runtime',
176
+ phase: 'install',
177
177
  silent: true,
178
178
  });
179
179
  const memoryMaintenance = hctx.nexusRef.maintainMemory();
@@ -12,10 +12,9 @@ import { resolveRunsBudget, resolveWorktreeBudget } from './cleanup.js';
12
12
  import { dirBytes, formatBytes } from '../install/fs-purge.js';
13
13
  import { enumerateNgramArchives, enumerateStatePaths, getNexusStateDir, getRuntimeTmpRoots, getWorktreeRoots, } from '../install/state-locator.js';
14
14
  import { INSTALL_ARCH_GENERATION, loadManifest, } from '../install/manifest.js';
15
- import { getNgramFootprintBytes } from '../engines/ngram-index.js';
15
+ import { getNgramFootprintBytes, NGRAM_DEFAULT_FOOTPRINT_BYTES } from '../engines/ngram-index.js';
16
16
  import { getSharedLicenseManager } from '../licensing/license-manager.js';
17
17
  const NGRAM_DEFAULT_WAL_LIMIT_BYTES = 64 * 1024 * 1024;
18
- const NGRAM_DEFAULT_FOOTPRINT_BYTES = 512 * 1024 * 1024;
19
18
  function readEnvBytesPositive(name, fallback) {
20
19
  const raw = process.env[name];
21
20
  if (!raw)
package/dist/cli.js CHANGED
@@ -30,7 +30,7 @@ import { cliSetup, configureIDE, computeFileHash, readSetupMarker, writeSetupMar
30
30
  import { isNewUser, promptLicenseKey, printReturningUserBanner } from './cli/interactive-setup.js';
31
31
  import { runHookBootstrap, runHookMemory, runHookMindkit, runHookGhostPass, runHookSessionDna } from './cli/hook.js';
32
32
  import { resolveWorkspaceContext } from './engines/workspace-resolver.js';
33
- import { ensureDaemonReady, getDaemonStatus, stopDaemon } from './daemon/client.js';
33
+ import { ensureDaemonReady, getDaemonStatus, pingDaemonHealth, stopDaemon } from './daemon/client.js';
34
34
  import { NexusDaemonServer } from './daemon/server.js';
35
35
  import { DaemonSupervisor } from './daemon/supervisor.js';
36
36
  import { startDaemonBackedMcpProxy } from './daemon/proxy.js';
@@ -720,8 +720,10 @@ program
720
720
  try {
721
721
  const record = await ensureDaemonManaged({ force: options.force });
722
722
  const dashboardPort = process.env.NEXUS_DASHBOARD_PORT ?? '3377';
723
+ const health = await pingDaemonHealth(record).catch(() => null);
724
+ const dashboardUrl = health?.dashboardUrl ?? `http://localhost:${dashboardPort}`;
723
725
  console.log(`Nexus Prime daemon running (pid ${record.pid}, ${formatDaemonAddress(record)})`);
724
- console.log(`Dashboard: http://localhost:${dashboardPort}`);
726
+ console.log(`Dashboard: ${dashboardUrl}`);
725
727
  }
726
728
  catch (err) {
727
729
  console.error(`Failed to start daemon: ${err.message}`);
@@ -820,15 +822,14 @@ program
820
822
  if (process.env.NEXUS_DAEMON_FAST_START === undefined) {
821
823
  process.env.NEXUS_DAEMON_FAST_START = '1';
822
824
  }
823
- // Default the daemon to "workforce-disabled" mode: Synapse + Architects
824
- // engines stay in the source tree (tests still run, code is preserved)
825
- // but they don't open their SQLite DBs, run schema migrations, or
826
- // surface their 24 MCP tools to clients. Saves ~700ms boot, shrinks
827
- // the model-visible tool catalog, and removes the empty Workforce
828
- // surface from the dashboard. Re-enable with NEXUS_DISABLE_WORKFORCE=0
829
- // for users who actually hire operatives + dispatch missions.
830
- if (process.env.NEXUS_DISABLE_WORKFORCE === undefined) {
831
- process.env.NEXUS_DISABLE_WORKFORCE = '1';
825
+ // Keep daemon startup fast, but do not disable workforce. Synapse and
826
+ // Architects can lazily warm on first hire/tool call unless the operator
827
+ // explicitly sets the NEXUS_DISABLE_WORKFORCE escape hatch.
828
+ if (process.env.NEXUS_SYNAPSE_LAZY === undefined) {
829
+ process.env.NEXUS_SYNAPSE_LAZY = '1';
830
+ }
831
+ if (process.env.NEXUS_ARCHITECTS_LAZY === undefined) {
832
+ process.env.NEXUS_ARCHITECTS_LAZY = '1';
832
833
  }
833
834
  const workspaceContext = resolveWorkspaceContext({ workspaceRoot: getWorkspaceRoot() });
834
835
  const daemon = new NexusDaemonServer(workspaceContext);
@@ -9,6 +9,7 @@ export interface DaemonHealthResponse {
9
9
  stateKey: string;
10
10
  workspaceRoot: string;
11
11
  repoRoot: string;
12
+ dashboardUrl?: string | null;
12
13
  startedAt: number;
13
14
  }
14
15
  export interface DaemonStatus {
@@ -390,6 +390,7 @@ export class NexusDaemonServer {
390
390
  stateKey: this.workspace.stateKey,
391
391
  workspaceRoot: this.workspace.workspaceRoot,
392
392
  repoRoot: this.workspace.repoRoot,
393
+ dashboardUrl: this.nexus?.getDashboardAddress?.() ?? null,
393
394
  startedAt: this.lockRecord?.startedAt ?? Date.now(),
394
395
  });
395
396
  return;
@@ -395,6 +395,12 @@
395
395
  color: var(--text-main); margin-bottom: 4px;
396
396
  }
397
397
  .frh-sub { font-size: var(--text-sm); color: var(--text-dim); }
398
+ .frh-command {
399
+ display: grid;
400
+ grid-template-columns: minmax(220px, 1fr) auto auto;
401
+ gap: 8px;
402
+ margin-bottom: 14px;
403
+ }
398
404
  .frh-picks { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
399
405
  .frh-pick {
400
406
  background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius);
@@ -417,6 +423,8 @@
417
423
  }
418
424
  @media (max-width: 768px) {
419
425
  #kanban-board { grid-template-columns: repeat(2, 1fr); }
426
+ .frh-command,
427
+ .frh-picks { grid-template-columns: 1fr; }
420
428
  }
421
429
  @media (max-width: 480px) {
422
430
  #kanban-board { grid-template-columns: 1fr; }
@@ -76,12 +76,14 @@ function normalizeOperative(op) {
76
76
 
77
77
  /* ── Data loader ── */
78
78
  export async function load() {
79
- const [tok, life, op, sh, health] = await Promise.all([
79
+ const [tok, life, op, sh, health, memHealth, runs] = await Promise.all([
80
80
  api('/api/tokens/summary'),
81
81
  api('/api/tokens/lifetime', 15000),
82
82
  api('/api/dashboard/surface/operate', 5000),
83
83
  api('/api/synapse/health', 5000),
84
84
  api('/api/health', 15000),
85
+ api('/api/memory/health', 15000),
86
+ api('/api/runs?limit=12', 3000),
85
87
  ]);
86
88
  // Non-blocking: tool health data from ring buffer
87
89
  loadToolHealth();
@@ -91,6 +93,8 @@ export async function load() {
91
93
  S.synapseHealthRaw = sh;
92
94
  S.synapseHealth = (Array.isArray(sh) ? sh : (sh?.operatives||[])).map(normalizeOperative);
93
95
  S.healthData = health;
96
+ S.memHealth = memHealth;
97
+ S.runs = Array.isArray(runs) ? runs : [];
94
98
  notifyNotReady([sh]);
95
99
  // Prefetch curated specialists for first-run hero (non-blocking)
96
100
  if (!S.synapseHealth.length && !S.curatedSpecialists) {
@@ -354,13 +358,14 @@ function renderFirstRunHero() {
354
358
  if (!parent) return;
355
359
 
356
360
  const hasOps = S.synapseHealth.length > 0;
361
+ const hasRuns = (S.runs || []).length > 0;
357
362
  const alreadySeen = (() => { try { return !!localStorage.getItem(FIRST_RUN_KEY); } catch { return false; } })();
358
363
 
359
364
  // Remove any existing hero card
360
365
  const existing = $('first-run-hero');
361
366
  if (existing) existing.remove();
362
367
 
363
- if (hasOps || alreadySeen) return;
368
+ if (hasOps || (alreadySeen && hasRuns)) return;
364
369
 
365
370
  const specs = (S.curatedSpecialists || []).slice(0, 3);
366
371
  if (!specs.length) return; // Still loading — will re-render when prefetch resolves
@@ -376,10 +381,15 @@ function renderFirstRunHero() {
376
381
  card.className = 'first-run-hero card';
377
382
  card.innerHTML = `
378
383
  <div class="frh-header">
379
- <div class="frh-title">Your next hire</div>
380
- <div class="frh-sub">Hire a specialist to start running tasks autonomously.</div>
384
+ <div class="frh-title">${hasRuns ? 'Your next hire' : 'Start the first real run'}</div>
385
+ <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>
381
386
  </div>
382
387
  ${noticesHtml}
388
+ <div class="frh-command">
389
+ <input id="frh-goal-input" class="form-input" type="text" placeholder="Inspect this repo and suggest the next fix" autocomplete="off">
390
+ <button class="btn btn-primary btn-sm" id="frh-run-btn">Run goal</button>
391
+ <button class="btn btn-sm" id="frh-context-btn">Open context</button>
392
+ </div>
383
393
  <div class="frh-picks">
384
394
  ${specs.map(s => `
385
395
  <div class="frh-pick" data-specid="${esc(s.specialistId)}" data-specname="${esc(s.name)}">
@@ -393,6 +403,33 @@ function renderFirstRunHero() {
393
403
  <button class="btn btn-ghost btn-sm frh-dismiss" style="margin-top:var(--space-3)">Dismiss</button>`;
394
404
 
395
405
  // Wire buttons before inserting
406
+ card.querySelector('#frh-run-btn')?.addEventListener('click', async () => {
407
+ const input = card.querySelector('#frh-goal-input');
408
+ const button = card.querySelector('#frh-run-btn');
409
+ const goal = (input?.value || `Inspect ${S.workspace?.repoName || 'this repo'} and report the next best action`).trim();
410
+ if (!goal) return;
411
+ if (button) {
412
+ button.disabled = true;
413
+ button.textContent = 'Queueing…';
414
+ }
415
+ setFirstRunStatus('Queueing dashboard run…');
416
+ const result = await post('/api/orchestrate', { goal, source: 'dashboard-onboarding' });
417
+ if (result.ok) {
418
+ setFirstRunStatus('Run queued. Board and Context Log will update as Nexus writes artifacts.');
419
+ bustCache('/api/runs?limit=12');
420
+ bustCache('/api/events');
421
+ setTimeout(load, 900);
422
+ } else {
423
+ setFirstRunStatus(result.error || 'Run failed to queue.', 'bad');
424
+ if (button) {
425
+ button.disabled = false;
426
+ button.textContent = 'Run goal';
427
+ }
428
+ }
429
+ });
430
+ card.querySelector('#frh-context-btn')?.addEventListener('click', () => {
431
+ window.location.hash = '#context-log';
432
+ });
396
433
  card.querySelectorAll('.frh-hire-btn').forEach(btn => {
397
434
  btn.addEventListener('click', async e => {
398
435
  e.stopPropagation();
@@ -452,18 +489,41 @@ function renderAgentsLiveStrip() {
452
489
  /* ── Kanban ── */
453
490
  function buildKanbanCols() {
454
491
  const cols={planning:[],hiring:[],running:[],ghostpass:[],done:[]};
455
- const op=S.operateSurface; if (!op) return cols;
456
- const pc=op.orchestration?.planningContext||op.planningContext;
457
- if (pc?.goal) cols.planning.push({id:'ctx',goal:pc.goal,status:'planning',tokens:null,time:pc.startedAt,role:null});
458
- const ws=op.orchestration?.workerPlan?.workers||op.workerPlan?.workers||[];
459
- for (const w of ws) {
460
- const st=String(w.status||'').toLowerCase();
461
- const sg=st==='active'||st==='running'?'running':st==='hiring'?'hiring':st==='complete'||st==='done'?'done':st==='reviewing'||st.includes('ghost')?'ghostpass':null;
462
- 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});
492
+ const op=S.operateSurface;
493
+ if (op) {
494
+ const pc=op.orchestration?.planningContext||op.planningContext;
495
+ if (pc?.goal) cols.planning.push({id:'ctx',goal:pc.goal,status:'planning',tokens:null,time:pc.startedAt,role:null});
496
+ const ws=op.orchestration?.workerPlan?.workers||op.workerPlan?.workers||[];
497
+ for (const w of ws) {
498
+ const st=String(w.status||'').toLowerCase();
499
+ const sg=st==='active'||st==='running'?'running':st==='hiring'?'hiring':st==='complete'||st==='done'?'done':st==='reviewing'||st.includes('ghost')?'ghostpass':null;
500
+ 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});
501
+ }
463
502
  }
464
- for (const r of (S.runs||[]).slice(0,6)) {
465
- if ((r.status==='complete'||r.status==='done')&&!cols.done.some(c=>c.id===r.id))
466
- cols.done.push({id:r.id,goal:r.goal||r.mandate||'(run)',status:'done',tokens:r.tokensUsed||r.tokenCount,time:r.completedAt||r.createdAt,role:null});
503
+ for (const r of (S.runs||[]).slice(0,8)) {
504
+ const runId = r.runId || r.id;
505
+ if (!runId) continue;
506
+ const status = String(r.status || r.state || '').toLowerCase();
507
+ const stage = String(r.stage || '').toLowerCase();
508
+ const lane = status.includes('complete') || status === 'done' || status === 'failed'
509
+ ? 'done'
510
+ : stage.includes('hire')
511
+ ? 'hiring'
512
+ : stage.includes('ghost') || stage.includes('review')
513
+ ? 'ghostpass'
514
+ : status === 'running' || stage.includes('orchestrat')
515
+ ? 'running'
516
+ : 'planning';
517
+ if (!cols[lane].some(c=>c.id===runId)) {
518
+ cols[lane].push({
519
+ id: runId,
520
+ goal: r.goal||r.mandate||'(run)',
521
+ status: status || stage || 'queued',
522
+ tokens:r.tokensUsed||r.tokenCount,
523
+ time:r.completedAt||r.updatedAt||r.createdAt||r.startedAt,
524
+ role:null,
525
+ });
526
+ }
467
527
  }
468
528
  return cols;
469
529
  }
@@ -89,17 +89,42 @@ function decisionRows(entries) {
89
89
  }
90
90
 
91
91
  function renderRunRail(runs) {
92
- if (!runs.length) return '<div class="empty"><div class="empty-title">No runs recorded</div></div>';
92
+ if (!runs.length) return `<div class="empty">
93
+ <div class="empty-title">No runs recorded</div>
94
+ <div class="empty-sub">Run a goal from the command bar and its context spine will appear here.</div>
95
+ </div>`;
93
96
  return runs.map(run => {
94
97
  const id = runIdOf(run);
95
98
  const active = id === S.contextLogSelectedRunId;
96
99
  return `<button class="context-log-run ${active ? 'active' : ''}" data-context-run="${esc(id)}">
97
100
  <span>${esc(String(id).slice(-10) || 'run')}</span>
98
- <small>${esc(run.state || run.status || 'queued')} · ${esc(fmtTs(run.createdAt))}</small>
101
+ <small>${esc(run.stage || run.state || run.status || 'queued')} · ${esc(fmtTs(run.createdAt || run.startedAt || run.updatedAt))}</small>
99
102
  </button>`;
100
103
  }).join('');
101
104
  }
102
105
 
106
+ function emptyContextMain() {
107
+ return `<div class="context-log-summary context-log-empty-state">
108
+ <div class="context-log-kpi"><span>Runs</span><strong>0</strong></div>
109
+ <div class="context-log-kpi"><span>Context</span><strong>—</strong></div>
110
+ <div class="context-log-kpi"><span>Decision</span><strong>—</strong></div>
111
+ <div class="context-log-selection">
112
+ <div>
113
+ <span>Start</span>
114
+ <button class="btn btn-sm" id="context-log-run-goal-btn">Run a goal</button>
115
+ </div>
116
+ <div>
117
+ <span>Expected here</span>
118
+ ${chips(['request brief', 'selection plan', 'context events', 'decisions'])}
119
+ </div>
120
+ <div>
121
+ <span>History</span>
122
+ <span class="context-log-empty">Previous runs persist after the dashboard restarts.</span>
123
+ </div>
124
+ </div>
125
+ </div>`;
126
+ }
127
+
103
128
  export async function load() {
104
129
  const runs = await api('/api/runs?limit=30', 3000);
105
130
  S.contextLogRuns = Array.isArray(runs) ? runs : [];
@@ -130,7 +155,7 @@ export function render() {
130
155
  ${renderRunRail(S.contextLogRuns || [])}
131
156
  </aside>
132
157
  <section class="context-log-main">
133
- ${spine ? summaryCard(spine) : '<div class="empty"><div class="empty-title">Select a run</div></div>'}
158
+ ${spine ? summaryCard(spine) : emptyContextMain()}
134
159
  <div class="context-log-grid">
135
160
  <div>
136
161
  <div class="shd">Context events</div>
@@ -150,6 +175,10 @@ export function render() {
150
175
  await load();
151
176
  });
152
177
  });
178
+ $('context-log-run-goal-btn')?.addEventListener('click', () => {
179
+ window.location.hash = '#board';
180
+ setTimeout(() => $('cmd-input')?.focus(), 100);
181
+ });
153
182
  $('context-log-refresh-btn')?.addEventListener('click', async () => {
154
183
  bustCache('/api/runs?limit=30');
155
184
  if (S.contextLogSelectedRunId) bustCache(spineUrl(S.contextLogSelectedRunId));
@@ -17,6 +17,20 @@ function fmtNum(n) {
17
17
  return String(Math.round(v));
18
18
  }
19
19
 
20
+ function collectionIdOf(collection) {
21
+ return collection?.collectionId || collection?.id || '';
22
+ }
23
+
24
+ function collectionNameOf(collection) {
25
+ return collection?.name || collectionIdOf(collection) || 'collection';
26
+ }
27
+
28
+ function collectionCounts(collection) {
29
+ const sources = collection?.sourceCount ?? collection?.documentCount ?? collection?.count ?? 0;
30
+ const chunks = collection?.chunkCount ?? 0;
31
+ return `${fmtNum(sources)} sources · ${fmtNum(chunks)} chunks`;
32
+ }
33
+
20
34
  /* ── Data loader ── */
21
35
  export async function load() {
22
36
  const [rags, topo] = await Promise.all([
@@ -46,24 +60,38 @@ function renderRagCollections() {
46
60
  $('rag-create-btn')?.addEventListener('click', _showCreateSheet);
47
61
  return;
48
62
  }
49
- el.innerHTML=`<div style="margin-bottom:8px;display:flex;align-items:center;gap:8px;justify-content:flex-end">
63
+ el.innerHTML=`<div style="margin-bottom:8px;display:flex;align-items:center;gap:8px;justify-content:space-between;flex-wrap:wrap">
64
+ <span style="font-size:var(--text-sm);color:var(--text-muted)">Attach collections to feed the orchestrator and dashboard chat.</span>
50
65
  <button class="btn btn-sm" id="rag-create-btn">New collection</button>
51
66
  </div>
52
- <div>${cols.map(c=>`<div class="mission-row">
67
+ <div>${cols.map(c=> {
68
+ const id = collectionIdOf(c);
69
+ const name = collectionNameOf(c);
70
+ const attached = (c.attachedRuntimeIds || []).length || (c.attachedSessionIds || []).length;
71
+ return `<div class="mission-row">
53
72
  <div class="mission-body">
54
- <div class="mission-title">${esc(c.name||c.id)}</div>
73
+ <div class="mission-title">${esc(name)}</div>
55
74
  <div class="mission-meta" style="font-family:var(--font-mono);font-size:var(--text-sm);color:var(--text-dim)">
56
- ${fmtNum(c.documentCount||c.count||0)} docs · ${fmtNum(c.tokenCount||0)} tokens
75
+ ${collectionCounts(c)} · ${attached ? 'attached' : 'detached'}
57
76
  </div>
58
77
  </div>
59
- <button class="btn btn-sm" data-ingest="${esc(c.id)}" data-ingestname="${esc(c.name||c.id)}">Ingest</button>
60
- <button class="btn btn-sm" data-drop-target="${esc(c.id)}" data-drop-name="${esc(c.name||c.id)}" title="Drop files to upload">↑ Upload</button>
61
- </div>`).join('')}</div>
78
+ <button class="btn btn-sm" data-ingest="${esc(id)}" data-ingestname="${esc(name)}">Add source</button>
79
+ <button class="btn btn-sm" data-ingest-repo="${esc(id)}" data-ingestname="${esc(name)}">Ingest repo</button>
80
+ <button class="btn btn-sm" data-attach="${esc(id)}" data-attached="${attached ? '1' : '0'}">${attached ? 'Detach' : 'Attach'}</button>
81
+ <button class="btn btn-sm" data-drop-target="${esc(id)}" data-drop-name="${esc(name)}" title="Drop files to upload">Upload</button>
82
+ </div>`;
83
+ }).join('')}</div>
62
84
  ${_renderDropZone(cols)}`;
63
85
 
64
86
  $('rag-create-btn')?.addEventListener('click', _showCreateSheet);
65
87
  el.querySelectorAll('[data-ingest]').forEach(btn => {
66
- btn.addEventListener('click', () => _ingestCollection(btn.dataset.ingest, btn.dataset.ingestname, btn));
88
+ btn.addEventListener('click', () => _showIngestSheet(btn.dataset.ingest, btn.dataset.ingestname));
89
+ });
90
+ el.querySelectorAll('[data-ingest-repo]').forEach(btn => {
91
+ btn.addEventListener('click', () => _ingestRepoCollection(btn.dataset.ingestRepo, btn.dataset.ingestname, btn));
92
+ });
93
+ el.querySelectorAll('[data-attach]').forEach(btn => {
94
+ btn.addEventListener('click', () => _toggleCollectionAttach(btn.dataset.attach, btn.dataset.attached === '1', btn));
67
95
  });
68
96
  el.querySelectorAll('[data-drop-target]').forEach(btn => {
69
97
  btn.addEventListener('click', () => _focusDropZone(btn.dataset.dropTarget, btn.dataset.dropName));
@@ -73,7 +101,7 @@ function renderRagCollections() {
73
101
 
74
102
  /** Render the drag-and-drop upload zone below the collection list. */
75
103
  function _renderDropZone(cols) {
76
- const opts = cols.map(c=>`<option value="${esc(c.id)}">${esc(c.name||c.id)}</option>`).join('');
104
+ const opts = cols.map(c=>`<option value="${esc(collectionIdOf(c))}">${esc(collectionNameOf(c))}</option>`).join('');
77
105
  return `
78
106
  <div id="rag-drop-zone" class="rag-drop-zone" aria-label="Drop files to ingest" role="region">
79
107
  <div class="rag-drop-inner" id="rag-drop-inner">
@@ -157,7 +185,7 @@ async function _uploadFiles(files) {
157
185
  const r = await post(`/api/rag/collections/${encodeURIComponent(collectionId)}/ingest`, { inputs }).catch(() => null);
158
186
 
159
187
  if (progress) progress.textContent = '';
160
- if (r && !r.error) {
188
+ if (r?.ok) {
161
189
  _toast(`Ingested ${inputs.length} file(s) into collection.`, 'good');
162
190
  bustCache('/api/rag/collections');
163
191
  setTimeout(load, 1200);
@@ -166,12 +194,13 @@ async function _uploadFiles(files) {
166
194
  }
167
195
  }
168
196
 
169
- async function _ingestCollection(id, name, btn) {
197
+ async function _ingestRepoCollection(id, name, btn) {
170
198
  btn.disabled=true; btn.textContent='Ingesting…';
171
199
  const r=await post('/api/rag/ingest', { collectionId: id });
172
- btn.disabled=false; btn.textContent='Ingest';
200
+ btn.disabled=false; btn.textContent='Ingest repo';
173
201
  if (r.ok) {
174
- _toast(`Collection "${name}" ingestion started.`, 'good');
202
+ const added = r.data?.sourcesAdded ?? r.data?.chunksAdded ?? 0;
203
+ _toast(`Collection "${name}" ingested ${added ? fmtNum(added) : 'repo'} source(s).`, 'good');
175
204
  bustCache('/api/rag/collections');
176
205
  setTimeout(load, 1500);
177
206
  } else {
@@ -179,6 +208,68 @@ async function _ingestCollection(id, name, btn) {
179
208
  }
180
209
  }
181
210
 
211
+ async function _toggleCollectionAttach(id, attached, btn) {
212
+ btn.disabled = true;
213
+ btn.textContent = attached ? 'Detaching…' : 'Attaching…';
214
+ const r = await post(`/api/rag/collections/${encodeURIComponent(id)}/${attached ? 'detach' : 'attach'}`, {});
215
+ if (r.ok) {
216
+ _toast(attached ? 'Collection detached.' : 'Collection attached.', 'good');
217
+ bustCache('/api/rag/collections');
218
+ setTimeout(load, 700);
219
+ } else {
220
+ btn.disabled = false;
221
+ btn.textContent = attached ? 'Detach' : 'Attach';
222
+ _toast(`Collection update failed: ${r.error}`, 'bad');
223
+ }
224
+ }
225
+
226
+ function _showIngestSheet(collectionId, collectionName) {
227
+ openDrawer({
228
+ title: `Add source to ${collectionName}`,
229
+ body: `<div class="dsec">
230
+ <div class="dsec-title">Ingest source</div>
231
+ <label class="form-label" for="rag-ingest-url">URL</label>
232
+ <input id="rag-ingest-url" class="form-input" type="url" placeholder="https://docs.example.com/page">
233
+ <label class="form-label" for="rag-ingest-file" style="margin-top:10px">File or folder path</label>
234
+ <input id="rag-ingest-file" class="form-input" type="text" placeholder="${esc(S.workspace?.repoRoot || '/path/to/repo')}">
235
+ <label class="form-label" for="rag-ingest-text" style="margin-top:10px">Text note</label>
236
+ <textarea id="rag-ingest-text" class="form-input" rows="5" placeholder="Paste notes, API docs, or decision context"></textarea>
237
+ <label class="form-label" for="rag-ingest-tags" style="margin-top:10px">Tags</label>
238
+ <input id="rag-ingest-tags" class="form-input" type="text" placeholder="repo,dashboard">
239
+ <button class="btn btn-primary" id="rag-ingest-confirm" style="margin-top:var(--space-4)">Ingest source</button>
240
+ <div id="rag-ingest-status" style="margin-top:var(--space-3);font-size:var(--text-sm);color:var(--text-muted)"></div>
241
+ </div>`,
242
+ });
243
+ $('rag-ingest-confirm')?.addEventListener('click', () => _submitIngest(collectionId));
244
+ }
245
+
246
+ async function _submitIngest(collectionId) {
247
+ const status = $('rag-ingest-status');
248
+ const url = ($('rag-ingest-url')?.value || '').trim();
249
+ const fileOrFolder = ($('rag-ingest-file')?.value || '').trim();
250
+ const text = ($('rag-ingest-text')?.value || '').trim();
251
+ const tags = ($('rag-ingest-tags')?.value || '').split(',').map(s => s.trim()).filter(Boolean);
252
+ const inputs = [];
253
+ if (url) inputs.push({ url, tags });
254
+ if (fileOrFolder) inputs.push({ folderPath: fileOrFolder, filePath: fileOrFolder, tags });
255
+ if (text) inputs.push({ text, label: 'dashboard-note', tags });
256
+ if (!inputs.length) {
257
+ if (status) { status.textContent = 'Add a URL, file/folder path, or text note.'; status.style.color = 'var(--bad)'; }
258
+ return;
259
+ }
260
+ if (status) { status.textContent = 'Ingesting…'; status.style.color = 'var(--text-muted)'; }
261
+ const r = await post(`/api/rag/collections/${encodeURIComponent(collectionId)}/ingest`, { inputs }).catch(() => null);
262
+ if (r?.ok) {
263
+ if (status) { status.textContent = `Added ${r.data?.sourcesAdded ?? inputs.length} source(s).`; status.style.color = 'var(--ok)'; }
264
+ _toast('RAG source ingested.', 'good');
265
+ bustCache('/api/rag/collections');
266
+ setTimeout(load, 900);
267
+ } else if (status) {
268
+ status.textContent = `Ingest failed: ${r?.error ?? 'unknown error'}`;
269
+ status.style.color = 'var(--bad)';
270
+ }
271
+ }
272
+
182
273
  function _showCreateSheet() {
183
274
  openDrawer({
184
275
  title: 'Create collection',
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { CONTEXT_LOG_FILE, DECISION_LOG_FILE, REQUEST_BRIEF_FILE, SELECTION_PLAN_FILE, } from '../../engines/orchestrator/decision-spine.js';
4
4
  import { getSharedNgramIndex } from '../../engines/ngram-index.js';
5
+ import { getRun as getStoredRun, getSpans } from '../../engines/orchestrator/store.js';
5
6
  import { getSharedTelemetry } from '../../engines/telemetry-remote.js';
6
7
  import { getDashboardEventCards } from '../stream/sse-broker.js';
7
8
  async function readJsonArtifact(run, filename, fallback) {
@@ -29,6 +30,90 @@ async function readJsonlArtifact(run, filename, fallback) {
29
30
  return fallback;
30
31
  }
31
32
  }
33
+ function storedRunDecisionSpine(run) {
34
+ const spans = getSpans(run.runId);
35
+ const contextLog = spans.length
36
+ ? spans.map((span) => ({
37
+ phase: span.name,
38
+ actor: 'dashboard',
39
+ reason: String(span.attrs?.summary ?? span.attrs?.state ?? span.name),
40
+ createdAt: span.startedAt,
41
+ contextRefs: [span.spanId],
42
+ evidenceRefs: [
43
+ span.attrs?.actualRunId ? `actual:${span.attrs.actualRunId}` : null,
44
+ span.status === 'error' && span.attrs?.error ? `error:${span.attrs.error}` : null,
45
+ ].filter(Boolean),
46
+ }))
47
+ : [{
48
+ phase: run.stage ?? 'intake',
49
+ actor: run.client ?? 'dashboard',
50
+ reason: `${run.status ?? 'queued'} run: ${run.goal ?? run.runId}`,
51
+ createdAt: run.updatedAt ?? run.startedAt ?? Date.now(),
52
+ evidenceRefs: [run.runId],
53
+ }];
54
+ const decisionLog = [{
55
+ verb: run.status === 'failed' ? 'blocked' : run.status === 'completed' ? 'completed' : 'queued',
56
+ surface: 'dashboard',
57
+ decision: run.status === 'failed'
58
+ ? (run.error ?? 'Run failed')
59
+ : run.status === 'completed'
60
+ ? (run.resultSummary ?? 'Run completed')
61
+ : `Run is ${run.stage ?? run.status ?? 'queued'}`,
62
+ reason: run.goal ?? '',
63
+ createdAt: run.completedAt ?? run.updatedAt ?? run.startedAt ?? Date.now(),
64
+ beforeRef: run.runId,
65
+ afterRef: run.status === 'completed' ? 'orchestration-store' : null,
66
+ }];
67
+ const requestBrief = {
68
+ runId: run.runId,
69
+ rawPrompt: run.goal,
70
+ intent: 'orchestrate',
71
+ risk: run.status === 'failed' ? 'needs-attention' : 'standard',
72
+ source: run.client ?? 'dashboard',
73
+ createdAt: run.startedAt,
74
+ };
75
+ const selectionPlan = {
76
+ runId: run.runId,
77
+ intent: 'orchestrate',
78
+ selected: {
79
+ files: [],
80
+ skills: [],
81
+ specialists: [],
82
+ crews: [],
83
+ },
84
+ executionPolicy: {
85
+ route: run.client ?? 'dashboard',
86
+ stage: run.stage,
87
+ progress: run.progress,
88
+ },
89
+ };
90
+ return {
91
+ runId: run.runId,
92
+ artifactsPath: null,
93
+ files: {
94
+ requestBrief: REQUEST_BRIEF_FILE,
95
+ selectionPlan: SELECTION_PLAN_FILE,
96
+ contextLog: CONTEXT_LOG_FILE,
97
+ decisionLog: DECISION_LOG_FILE,
98
+ },
99
+ artifacts: {
100
+ requestBrief,
101
+ selectionPlan,
102
+ contextLog,
103
+ decisionLog,
104
+ },
105
+ summary: {
106
+ intent: requestBrief.intent,
107
+ risk: requestBrief.risk,
108
+ modelRoute: null,
109
+ selected: selectionPlan.selected,
110
+ contextEvents: contextLog.length,
111
+ decisions: decisionLog.length,
112
+ latestDecision: decisionLog[decisionLog.length - 1],
113
+ source: 'orchestration-store',
114
+ },
115
+ };
116
+ }
32
117
  export const handleEventRoutes = async (ctx, req, res, url) => {
33
118
  if (req.method === 'GET' && url.pathname === '/stream') {
34
119
  ctx.serveSse(req, res);
@@ -56,7 +141,12 @@ export const handleEventRoutes = async (ctx, req, res, url) => {
56
141
  const runId = decodeURIComponent(decisionSpineMatch[1]);
57
142
  const run = await ctx.getRuntime()?.getRun?.(runId);
58
143
  if (!run) {
59
- ctx.respondJson(res, { error: 'decision-spine-not-found', runId }, 404);
144
+ const stored = getStoredRun(runId);
145
+ if (!stored) {
146
+ ctx.respondJson(res, { error: 'decision-spine-not-found', runId }, 404);
147
+ return true;
148
+ }
149
+ ctx.respondJson(res, storedRunDecisionSpine(stored));
60
150
  return true;
61
151
  }
62
152
  const [requestBrief, selectionPlan, contextLog, decisionLog] = await Promise.all([
@@ -1,5 +1,50 @@
1
+ import fs from 'fs/promises';
1
2
  import path from 'path';
2
3
  import { resolveWorkspaceContext } from '../../engines/workspace-resolver.js';
4
+ function safeWorkspacePath(value, allowedRoots) {
5
+ if (!value)
6
+ return undefined;
7
+ const abs = path.resolve(String(value));
8
+ return allowedRoots.some((root) => abs === root || abs.startsWith(root + path.sep)) ? abs : undefined;
9
+ }
10
+ async function isDirectory(value) {
11
+ try {
12
+ return (await fs.stat(value)).isDirectory();
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ async function normalizeRagInputs(body, allowedRoots) {
19
+ const rawInputs = Array.isArray(body?.inputs) ? body.inputs : [body ?? {}];
20
+ const normalized = await Promise.all(rawInputs
21
+ .map(async (entry) => {
22
+ let filePath = safeWorkspacePath(entry?.filePath, allowedRoots);
23
+ let folderPath = safeWorkspacePath(entry?.folderPath, allowedRoots);
24
+ if (filePath && !folderPath && await isDirectory(filePath)) {
25
+ folderPath = filePath;
26
+ filePath = undefined;
27
+ }
28
+ if (filePath && folderPath && filePath === folderPath) {
29
+ if (await isDirectory(filePath)) {
30
+ filePath = undefined;
31
+ }
32
+ else {
33
+ folderPath = undefined;
34
+ }
35
+ }
36
+ return {
37
+ filePath,
38
+ folderPath,
39
+ folderGlob: entry?.folderGlob ? String(entry.folderGlob) : undefined,
40
+ url: entry?.url ? String(entry.url) : undefined,
41
+ text: entry?.text ? String(entry.text) : undefined,
42
+ label: entry?.label ? String(entry.label) : undefined,
43
+ tags: Array.isArray(entry?.tags) ? entry.tags.map(String) : [],
44
+ };
45
+ }));
46
+ return normalized.filter((entry) => entry.filePath || entry.folderPath || entry.url || entry.text);
47
+ }
3
48
  export const handleMemoryRoutes = async (ctx, req, res, url) => {
4
49
  if (req.method === 'GET' && url.pathname === '/api/knowledge-fabric/session') {
5
50
  const snapshot = ctx.resolveRuntimeSnapshot(url);
@@ -222,22 +267,32 @@ export const handleMemoryRoutes = async (ctx, req, res, url) => {
222
267
  ctx.respondJson(res, { error: 'orchestrator-unavailable' }, 503);
223
268
  return true;
224
269
  }
225
- const inputs = Array.isArray(body.inputs)
226
- ? body.inputs.map((entry) => ({
227
- filePath: (() => {
228
- if (!entry?.filePath)
229
- return undefined;
230
- const abs = path.resolve(String(entry.filePath));
231
- return allowedRoots.some((root) => abs === root || abs.startsWith(root + path.sep))
232
- ? String(entry.filePath)
233
- : undefined;
234
- })(),
235
- url: entry?.url ? String(entry.url) : undefined,
236
- text: entry?.text ? String(entry.text) : undefined,
237
- label: entry?.label ? String(entry.label) : undefined,
238
- tags: Array.isArray(entry?.tags) ? entry.tags.map(String) : [],
239
- }))
240
- : [];
270
+ const inputs = await normalizeRagInputs(body, allowedRoots);
271
+ ctx.respondJson(res, await orchestrator.ingestRagCollection(collectionId, inputs));
272
+ return true;
273
+ }
274
+ if (req.method === 'POST' && url.pathname === '/api/rag/ingest') {
275
+ const body = await ctx.readJsonBody(req);
276
+ const collectionId = String(body?.collectionId ?? body?.id ?? '').trim();
277
+ const orchestrator = ctx.getOrchestrator();
278
+ const workspace = ctx.workspaceContextProvider?.() ?? resolveWorkspaceContext({ workspaceRoot: ctx.repoRoot });
279
+ const allowedRoots = [workspace.workspaceRoot, workspace.repoRoot].map((root) => path.resolve(root));
280
+ if (!orchestrator) {
281
+ ctx.respondJson(res, { error: 'orchestrator-unavailable' }, 503);
282
+ return true;
283
+ }
284
+ if (!collectionId) {
285
+ ctx.respondJson(res, { error: 'collectionId-required' }, 400);
286
+ return true;
287
+ }
288
+ let inputs = await normalizeRagInputs(body, allowedRoots);
289
+ if (!inputs.length && !body?.inputs && !body?.filePath && !body?.folderPath && !body?.url && !body?.text) {
290
+ inputs = [{
291
+ folderPath: workspace.repoRoot,
292
+ label: workspace.repoName || path.basename(workspace.repoRoot),
293
+ tags: ['repo', 'dashboard'],
294
+ }];
295
+ }
241
296
  ctx.respondJson(res, await orchestrator.ingestRagCollection(collectionId, inputs));
242
297
  return true;
243
298
  }
@@ -5,6 +5,7 @@ import { execAsync } from '../../utils/exec-async.js';
5
5
  import { recordFirstInteraction } from '../../engines/telemetry.js';
6
6
  import { pushDispatch, cancelDispatch, listActiveDispatches } from '../../engines/dispatch/push-dispatch.js';
7
7
  import { invokerRegistry } from '../../invokers/registry.js';
8
+ import { completeRun, createRun, failRun, getRun as getStoredRun, listRecentRuns, startSpan, endSpan, updateStage, } from '../../engines/orchestrator/store.js';
8
9
  function normalizeLifetimeTokenPayload(record) {
9
10
  const savedTokens = Number(record?.totalSavedTokens ?? 0);
10
11
  const grossInputTokens = Number(record?.totalGrossInputTokens ?? 0);
@@ -48,6 +49,72 @@ function normalizeTokenSummaryPayload(record) {
48
49
  net: compressedTokens,
49
50
  };
50
51
  }
52
+ function mergeDashboardRuns(liveRuns, storedRuns, limit) {
53
+ const seen = new Set();
54
+ return [...(liveRuns ?? []), ...(storedRuns ?? [])]
55
+ .map((run) => {
56
+ const runId = run?.runId ?? run?.id;
57
+ return runId ? { ...run, runId, id: run.id ?? runId } : null;
58
+ })
59
+ .filter((run) => {
60
+ if (!run?.runId || seen.has(run.runId))
61
+ return false;
62
+ seen.add(run.runId);
63
+ return true;
64
+ })
65
+ .sort((left, right) => {
66
+ const leftTs = Number(left.updatedAt ?? left.completedAt ?? left.createdAt ?? left.startedAt ?? 0);
67
+ const rightTs = Number(right.updatedAt ?? right.completedAt ?? right.createdAt ?? right.startedAt ?? 0);
68
+ return rightTs - leftTs;
69
+ })
70
+ .slice(0, Math.max(1, limit));
71
+ }
72
+ function summarizeDashboardRun(run, fallbackGoal) {
73
+ const state = run?.state ?? run?.status ?? 'completed';
74
+ const summary = run?.summary ?? run?.resultSummary ?? run?.message ?? '';
75
+ return String(summary || `${state}: ${fallbackGoal}`).slice(0, 1024);
76
+ }
77
+ async function tokenLedgerFallback(ctx) {
78
+ const memory = ctx.getMemory?.();
79
+ if (!memory)
80
+ return null;
81
+ try {
82
+ const { TokenAnalyticsEngine } = await import('../../engines/token-analytics.js');
83
+ const report = new TokenAnalyticsEngine(memory).getLifetimeReport();
84
+ const optimized = Number(report.totalTokensOptimized ?? 0);
85
+ const saved = Number(report.totalTokensSaved ?? 0);
86
+ const forwarded = Number(report.totalTokensForwarded ?? Math.max(0, optimized - saved));
87
+ if (optimized <= 0 && saved <= 0 && forwarded <= 0)
88
+ return null;
89
+ return {
90
+ totalSavedTokens: saved,
91
+ totalGrossInputTokens: optimized,
92
+ totalCompressedTokens: forwarded,
93
+ totalRuns: Number(report.totalSessions ?? 0),
94
+ lastUpdatedAt: Date.now(),
95
+ tokenLedger: report,
96
+ };
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ function mergeLifetimeTokenRecords(fileRecord, ledgerRecord) {
103
+ if (!ledgerRecord)
104
+ return fileRecord;
105
+ return {
106
+ ...fileRecord,
107
+ totalSavedTokens: Math.max(Number(fileRecord?.totalSavedTokens ?? 0), Number(ledgerRecord.totalSavedTokens ?? 0)),
108
+ totalGrossInputTokens: Math.max(Number(fileRecord?.totalGrossInputTokens ?? 0), Number(ledgerRecord.totalGrossInputTokens ?? 0)),
109
+ totalCompressedTokens: Math.max(Number(fileRecord?.totalCompressedTokens ?? 0), Number(ledgerRecord.totalCompressedTokens ?? 0)),
110
+ totalRuns: Math.max(Number(fileRecord?.totalRuns ?? 0), Number(ledgerRecord.totalRuns ?? 0)),
111
+ lastUpdatedAt: Math.max(Number(fileRecord?.lastUpdatedAt ?? 0), Number(ledgerRecord.lastUpdatedAt ?? 0)),
112
+ sources: {
113
+ lifetimeFile: fileRecord,
114
+ tokenLedger: ledgerRecord.tokenLedger,
115
+ },
116
+ };
117
+ }
51
118
  async function readGitBranch(repoRoot) {
52
119
  try {
53
120
  const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
@@ -64,7 +131,9 @@ async function readGitBranch(repoRoot) {
64
131
  export const handleRuntimeRoutes = async (ctx, req, res, url) => {
65
132
  if (req.method === 'GET' && url.pathname === '/api/runs') {
66
133
  const limit = parseInt(url.searchParams.get('limit') || '20', 10);
67
- ctx.respondJson(res, ctx.getRuntime()?.listRuns(limit) ?? []);
134
+ const liveRuns = ctx.getRuntime()?.listRuns(limit) ?? [];
135
+ const storedRuns = listRecentRuns(limit);
136
+ ctx.respondJson(res, mergeDashboardRuns(liveRuns, storedRuns, limit));
68
137
  return true;
69
138
  }
70
139
  if (req.method === 'GET' && url.pathname === '/api/usage') {
@@ -140,7 +209,8 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
140
209
  if (req.method === 'GET' && url.pathname === '/api/tokens/lifetime') {
141
210
  try {
142
211
  const { readLifetimeTokens } = await import('../../engines/lifetime-tokens.js');
143
- ctx.respondJson(res, { ok: true, data: normalizeLifetimeTokenPayload(readLifetimeTokens()) });
212
+ const merged = mergeLifetimeTokenRecords(readLifetimeTokens(), await tokenLedgerFallback(ctx));
213
+ ctx.respondJson(res, { ok: true, data: normalizeLifetimeTokenPayload(merged) });
144
214
  }
145
215
  catch (err) {
146
216
  ctx.respondJson(res, { ok: false, error: err?.message ?? 'Failed to read lifetime tokens' });
@@ -167,7 +237,7 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
167
237
  }
168
238
  if (req.method === 'GET' && url.pathname.startsWith('/api/runs/')) {
169
239
  const runId = decodeURIComponent(url.pathname.replace('/api/runs/', ''));
170
- const run = await ctx.getRuntime()?.getRun?.(runId);
240
+ const run = await ctx.getRuntime()?.getRun?.(runId) ?? getStoredRun(runId);
171
241
  ctx.respondJson(res, run ?? { error: 'run-not-found', runId }, run ? 200 : 404);
172
242
  return true;
173
243
  }
@@ -215,13 +285,39 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
215
285
  ctx.respondJson(res, { error: 'goal-required' }, 400);
216
286
  return true;
217
287
  }
218
- const run = await orchestrator.orchestrate(goal, { ...body, goal });
219
- nexusEventBus.emit('dashboard.action', {
220
- action: 'runtime.execute',
221
- status: run.state,
222
- target: run.runId,
223
- });
224
- ctx.respondJson(res, run, 201);
288
+ const dashboardRunId = `dash_${Date.now()}`;
289
+ createRun(dashboardRunId, goal, 'dashboard-chat');
290
+ updateStage(dashboardRunId, 'orchestrating', 35);
291
+ const spanId = startSpan(dashboardRunId, 'dashboard.runtime.execute', undefined, { source: 'dashboard-chat' });
292
+ try {
293
+ const run = await orchestrator.orchestrate(goal, { ...body, goal, source: body.source ?? 'dashboard-chat' });
294
+ const actualRunId = run?.runId ?? dashboardRunId;
295
+ if (actualRunId !== dashboardRunId) {
296
+ createRun(actualRunId, goal, 'dashboard-chat');
297
+ updateStage(actualRunId, 'orchestrating', 80);
298
+ }
299
+ completeRun(dashboardRunId, summarizeDashboardRun(run, goal));
300
+ if (actualRunId !== dashboardRunId)
301
+ completeRun(actualRunId, summarizeDashboardRun(run, goal));
302
+ endSpan(spanId, run?.state === 'failed' ? 'error' : 'ok', { actualRunId, state: run?.state ?? 'completed' });
303
+ nexusEventBus.emit('dashboard.action', {
304
+ action: 'runtime.execute',
305
+ status: run.state,
306
+ target: actualRunId,
307
+ });
308
+ ctx.respondJson(res, run, 201);
309
+ }
310
+ catch (err) {
311
+ const message = err?.message ?? String(err);
312
+ failRun(dashboardRunId, message);
313
+ endSpan(spanId, 'error', { error: message });
314
+ nexusEventBus.emit('dashboard.action', {
315
+ action: 'runtime.execute.failed',
316
+ status: 'fail',
317
+ target: dashboardRunId,
318
+ });
319
+ ctx.respondJson(res, { error: message, runId: dashboardRunId }, 500);
320
+ }
225
321
  return true;
226
322
  }
227
323
  if (req.method === 'POST' && url.pathname === '/api/runtime/plan') {
@@ -315,22 +411,38 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
315
411
  return true;
316
412
  }
317
413
  const runId = `run_${Date.now()}`;
414
+ createRun(runId, goal, 'dashboard');
415
+ updateStage(runId, 'queued', 10);
416
+ const spanId = startSpan(runId, 'dashboard.orchestrate', undefined, { source: body.source ?? 'dashboard' });
318
417
  // TTV: first goal submission from the dashboard counts as first interaction.
319
418
  void recordFirstInteraction().catch(() => { });
320
419
  // Fire-and-forget: orchestration runs in the background; caller polls /api/runs/:id.
321
- orchestrator.orchestrate(goal, { source: 'dashboard' })
420
+ orchestrator.orchestrate(goal, { source: body.source ?? 'dashboard', dashboardRunId: runId })
322
421
  .then((run) => {
422
+ const actualRunId = run?.runId ?? runId;
423
+ if (actualRunId !== runId) {
424
+ createRun(actualRunId, goal, 'dashboard');
425
+ updateStage(actualRunId, 'orchestrating', 80);
426
+ }
427
+ const summary = summarizeDashboardRun(run, goal);
428
+ completeRun(runId, summary);
429
+ if (actualRunId !== runId)
430
+ completeRun(actualRunId, summary);
431
+ endSpan(spanId, run?.state === 'failed' ? 'error' : 'ok', { actualRunId, state: run?.state ?? 'completed' });
323
432
  nexusEventBus.emit('dashboard.action', {
324
433
  action: 'orchestrate.complete',
325
434
  status: run?.state === 'failed' ? 'fail' : 'ok',
326
- target: run?.runId ?? runId,
435
+ target: actualRunId,
327
436
  });
328
437
  })
329
438
  .catch((err) => {
439
+ const message = err instanceof Error ? err.message : String(err);
440
+ failRun(runId, message);
441
+ endSpan(spanId, 'error', { error: message });
330
442
  nexusEventBus.emit('dashboard.action', {
331
443
  action: 'orchestrate.failed',
332
444
  status: 'fail',
333
- target: err instanceof Error ? err.message : String(err),
445
+ target: runId,
334
446
  });
335
447
  });
336
448
  nexusEventBus.emit('dashboard.action', { action: 'orchestrate.enqueue', status: 'queued', target: runId });
@@ -1126,9 +1126,26 @@ export class DashboardServer {
1126
1126
  const capabilities = this.buildAdvertisedCapabilities();
1127
1127
  const compatibility = this.buildCompatibilityStatus(capabilities);
1128
1128
  const runtimeEnvelope = this.buildProbeRuntimeEnvelope();
1129
+ const workspace = this.resolveCanonicalWorkspaceContext();
1129
1130
  return {
1130
1131
  dashboardApiVersion: DASHBOARD_API_VERSION,
1131
- projectName: path.basename(this.repoRoot) || 'workspace',
1132
+ projectName: workspace.repoName,
1133
+ repoIdentity: {
1134
+ repoName: workspace.repoName,
1135
+ repoRoot: workspace.repoRoot,
1136
+ workspaceRoot: workspace.workspaceRoot,
1137
+ workspaceSource: workspace.workspaceSource,
1138
+ workspaceStateKey: workspace.stateKey,
1139
+ remoteUrl: workspace.remoteUrl ?? null,
1140
+ currentRepoId: null,
1141
+ },
1142
+ scanContext: {
1143
+ repoRoot: workspace.repoRoot,
1144
+ workspaceRoot: workspace.workspaceRoot,
1145
+ workspaceSource: workspace.workspaceSource,
1146
+ workspaceStateKey: workspace.stateKey,
1147
+ notes: ['Probe snapshot uses the lightweight compatibility contract.'],
1148
+ },
1132
1149
  capabilities,
1133
1150
  compatibility,
1134
1151
  dashboardUrl: this.getAddress(),
@@ -1513,6 +1530,10 @@ export class DashboardServer {
1513
1530
  if (payload.compatibility?.status !== 'compatible') {
1514
1531
  return false;
1515
1532
  }
1533
+ const stateRoot = payload.runtimeEnvelope?.workspace?.stateRoot;
1534
+ if (!stateRoot || path.resolve(stateRoot) !== path.resolve(resolveNexusStateDir())) {
1535
+ return false;
1536
+ }
1516
1537
  const required = payload.compatibility?.requiredCapabilities?.length
1517
1538
  ? payload.compatibility.requiredCapabilities
1518
1539
  : [...DASHBOARD_COMPATIBILITY_CAPABILITIES];
@@ -312,14 +312,14 @@
312
312
 
313
313
  <!-- Step 3: Ready -->
314
314
  <div class="panel" id="panel-3" style="display:none">
315
- <div class="panel-title" style="color:var(--ok)">✓ You're ready</div>
316
- <div class="panel-sub">Your control plane is online. Here's what you can do next.</div>
315
+ <div class="panel-title" style="color:var(--ok)">✓ Control plane ready</div>
316
+ <div class="panel-sub">Your dashboard can now run goals, hire operatives, ingest knowledge, and show context history.</div>
317
317
 
318
318
  <div class="ready-grid">
319
319
  <div class="ready-tile">
320
320
  <div class="ready-tile-icon">🧠</div>
321
- <div class="ready-tile-label">Orchestrate</div>
322
- <div class="ready-tile-desc">Give an agent a goal and watch it plan, remember, and execute.</div>
321
+ <div class="ready-tile-label">Run a goal</div>
322
+ <div class="ready-tile-desc">Queue a dashboard run and watch Board plus Context Log fill with real artifacts.</div>
323
323
  </div>
324
324
  <div class="ready-tile">
325
325
  <div class="ready-tile-icon">👥</div>
@@ -328,13 +328,13 @@
328
328
  </div>
329
329
  <div class="ready-tile">
330
330
  <div class="ready-tile-icon">📡</div>
331
- <div class="ready-tile-label">Live Dashboard</div>
332
- <div class="ready-tile-desc">Watch every tool call, token saved, and memory formed in real time.</div>
331
+ <div class="ready-tile-label">Context history</div>
332
+ <div class="ready-tile-desc">Open prior runs, selected files, decisions, memory events, and model routing.</div>
333
333
  </div>
334
334
  <div class="ready-tile">
335
335
  <div class="ready-tile-icon">🔑</div>
336
- <div class="ready-tile-label">MCP Connected</div>
337
- <div class="ready-tile-desc">Nexus Prime is wired into your IDE agents via the control plane.</div>
336
+ <div class="ready-tile-label">Knowledge ready</div>
337
+ <div class="ready-tile-desc">Create RAG collections, attach them, and ingest repo files or pasted notes.</div>
338
338
  </div>
339
339
  </div>
340
340
 
@@ -348,9 +348,9 @@
348
348
  </div>
349
349
 
350
350
  <div class="actions" style="margin-top:24px">
351
- <a class="btn primary" href="/">Open Board →</a>
351
+ <a class="btn primary" href="/#board">Open Board →</a>
352
352
  <a class="btn" href="/#workforce">Workforce</a>
353
- <a class="btn" href="/#memory">Memory</a>
353
+ <a class="btn" href="/#context-log">Context Log</a>
354
354
  </div>
355
355
  </div>
356
356
 
@@ -561,8 +561,8 @@
561
561
 
562
562
  // ── Step 3: Ready ─────────────────────────────────────────────────────────
563
563
  function loadReadyStep() {
564
- document.getElementById('page-title').textContent = 'You\'re all set 🚀';
565
- document.getElementById('page-sub').textContent = 'Control plane online. Start orchestrating.';
564
+ document.getElementById('page-title').textContent = 'Nexus Prime is ready';
565
+ document.getElementById('page-sub').textContent = 'Open Board, run the first goal, or hire an operative. The dashboard will keep the run history visible.';
566
566
  }
567
567
 
568
568
  async function hireOperative() {
@@ -575,7 +575,7 @@
575
575
  const res = await fetch('/api/synapse/hire', {
576
576
  method: 'POST',
577
577
  headers: { 'Content-Type': 'application/json' },
578
- body: JSON.stringify({ specialistId: 'engineering.rapid-prototyper', budgetCapUsd: 2, name: 'Rapid Prototyper' }),
578
+ body: JSON.stringify({ specialistId: 'engineering.rapid-prototyper', budgetCapUsd: 2, name: 'Rapid Prototyper', fireFirstSortie: true }),
579
579
  });
580
580
  if (res.ok) {
581
581
  const d = await res.json();
@@ -1,7 +1,7 @@
1
1
  import { MemoryEngine } from './memory.js';
2
2
  import { PatternRegistry, type PatternCard, type PatternSearchResult } from './pattern-registry.js';
3
3
  import { RuntimeRegistry, type RuntimeRegistrySnapshot } from './runtime-registry.js';
4
- import { RagCollectionStore, type RagCollectionSummary, type RagRetrievalHit } from './rag-collections.js';
4
+ import { RagCollectionStore, type RagCollectionSummary, type RagIngestInput, type RagRetrievalHit } from './rag-collections.js';
5
5
  import { TokenSupremacyEngine, type ReadingPlan } from './token-supremacy.js';
6
6
  export type KnowledgeSourceClass = 'repo' | 'memory' | 'rag' | 'patterns' | 'runtime';
7
7
  export type ModelTier = 'low' | 'high';
@@ -171,13 +171,7 @@ export declare class KnowledgeFabricEngine {
171
171
  tags?: string[];
172
172
  scope?: 'session' | 'project';
173
173
  }): import("./rag-collections.js").RagCollection;
174
- ingestCollection(collectionId: string, inputs: Array<{
175
- filePath?: string;
176
- url?: string;
177
- text?: string;
178
- label?: string;
179
- tags?: string[];
180
- }>): Promise<{
174
+ ingestCollection(collectionId: string, inputs: RagIngestInput[]): Promise<{
181
175
  collection: import("./rag-collections.js").RagCollection;
182
176
  sourcesAdded: number;
183
177
  chunksAdded: number;
@@ -11,6 +11,9 @@
11
11
  *
12
12
  * Persistence: SQLite (same pattern as memory.db / graph.db)
13
13
  */
14
+ export declare const NGRAM_WARMUP_MAX_DB_BYTES: number;
15
+ export declare const NGRAM_DEFAULT_ROTATE_BYTES: number;
16
+ export declare const NGRAM_DEFAULT_FOOTPRINT_BYTES: number;
14
17
  export declare function getNgramWalPath(dbPath: string): string;
15
18
  export declare function getNgramShmPath(dbPath: string): string;
16
19
  export declare function getNgramFootprintBytes(dbPath: string): number;
@@ -147,7 +150,7 @@ export declare class NgramIndex {
147
150
  optimizeStorage(force?: boolean): void;
148
151
  /**
149
152
  * Operator-focused maintenance for the on-disk ngram DB.
150
- * - Bounds runaway DB growth via rotation (default >= 1GB), counting the
153
+ * - Bounds runaway DB growth via rotation, counting the
151
154
  * full SQLite footprint (db + wal + shm) so a runaway WAL triggers it.
152
155
  * - Vacuums only when safe (<= vacuumMaxBytes) and either forced or dirty.
153
156
  */
@@ -45,7 +45,9 @@ function safeStatSize(filePath) {
45
45
  // wal/shm misses the bug that turned ngram-index.db-wal into 84GB on disk.
46
46
  // ─────────────────────────────────────────────────────────────────────────────
47
47
  const NGRAM_DEFAULT_WAL_LIMIT_BYTES = 64 * 1024 * 1024; // 64 MB
48
- const NGRAM_DEFAULT_FOOTPRINT_BYTES = 512 * 1024 * 1024; // 512 MB
48
+ export const NGRAM_WARMUP_MAX_DB_BYTES = 200 * 1024 * 1024; // 200 MB
49
+ export const NGRAM_DEFAULT_ROTATE_BYTES = NGRAM_WARMUP_MAX_DB_BYTES;
50
+ export const NGRAM_DEFAULT_FOOTPRINT_BYTES = NGRAM_WARMUP_MAX_DB_BYTES;
49
51
  const NGRAM_DEFAULT_CHECKPOINT_INTERVAL_MS = 30_000;
50
52
  const NGRAM_DEFAULT_CHECKPOINT_DOC_COUNT = 200;
51
53
  export function getNgramWalPath(dbPath) {
@@ -199,13 +201,13 @@ export class NgramIndex {
199
201
  this.warmHashSet();
200
202
  try {
201
203
  const sizeBytes = safeStatSize(this.dbPath);
202
- // Only VACUUM on medium-size DBs. Large DBs (>200MB) take too long
204
+ // Only VACUUM on medium-size DBs. Large DBs take too long
203
205
  // synchronously — VACUUM rewrites the entire file. Skip here; let
204
206
  // periodic maintenance handle it.
205
- if (sizeBytes > 32 * 1024 * 1024 && sizeBytes <= 200 * 1024 * 1024) {
207
+ if (sizeBytes > 32 * 1024 * 1024 && sizeBytes <= NGRAM_WARMUP_MAX_DB_BYTES) {
206
208
  this.optimizeStorage(true);
207
209
  }
208
- else if (sizeBytes > 200 * 1024 * 1024) {
210
+ else if (sizeBytes > NGRAM_WARMUP_MAX_DB_BYTES) {
209
211
  logNgramNoticeOnce(`ngram:vacuum-skip:${this.dbPath}`, `[NgramIndex] skipping VACUUM on large DB (${Math.round(sizeBytes / 1024 / 1024)}MB) db=${this.dbPath}`);
210
212
  }
211
213
  }
@@ -217,17 +219,17 @@ export class NgramIndex {
217
219
  // Count the full SQLite footprint (db + wal + shm). The 84GB regression
218
220
  // happened because a 32MB db file had a 84GB -wal sibling that this
219
221
  // routine never inspected.
220
- const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES', 1024 * 1024 * 1024); // 1GB default
222
+ const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES', NGRAM_DEFAULT_ROTATE_BYTES);
221
223
  const dbBytes = safeStatSize(this.dbPath);
222
224
  const footprint = getNgramFootprintBytes(this.dbPath);
223
225
  if (footprint <= 0 || footprint < rotateBytes)
224
226
  return;
225
- // Keep only the most recent oversize archive to bound disk usage. When the
226
- // operator opts out of archiving (ARCHIVE_OVERSIZE=0), drop the leftovers
227
- // outright instead of growing a backlog of multi-GB carcasses.
227
+ // The n-gram DB is a rebuildable cache, so oversized rotation drops it by
228
+ // default. Operators can opt into one retained archive for forensics with
229
+ // NEXUS_NGRAM_ARCHIVE_OVERSIZE=1.
228
230
  const dir = path.dirname(this.dbPath);
229
231
  const base = path.basename(this.dbPath);
230
- const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE !== '0';
232
+ const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE === '1';
231
233
  const existing = fs.existsSync(dir)
232
234
  ? fs.readdirSync(dir).filter((entry) => entry.startsWith(`${base}.oversize.`)).sort().reverse()
233
235
  : [];
@@ -324,8 +326,8 @@ export class NgramIndex {
324
326
  // Size guard: SELECT DISTINCT on very large DBs blocks startup under swap pressure.
325
327
  try {
326
328
  const sizeBytes = fs.statSync(this.dbPath).size;
327
- if (sizeBytes > 200 * 1024 * 1024) {
328
- logNgramNoticeOnce(`ngram:warmup-skip:${this.dbPath}`, `[NgramIndex] warmup skipped — DB too large (${Math.round(sizeBytes / 1024 / 1024)}MB > 200MB) db=${this.dbPath}`);
329
+ if (sizeBytes > NGRAM_WARMUP_MAX_DB_BYTES) {
330
+ logNgramNoticeOnce(`ngram:warmup-skip:${this.dbPath}`, `[NgramIndex] warmup skipped — DB too large (${Math.round(sizeBytes / 1024 / 1024)}MB > ${Math.round(NGRAM_WARMUP_MAX_DB_BYTES / 1024 / 1024)}MB) db=${this.dbPath}`);
329
331
  return;
330
332
  }
331
333
  }
@@ -667,17 +669,17 @@ export class NgramIndex {
667
669
  }
668
670
  /**
669
671
  * Operator-focused maintenance for the on-disk ngram DB.
670
- * - Bounds runaway DB growth via rotation (default >= 1GB), counting the
672
+ * - Bounds runaway DB growth via rotation, counting the
671
673
  * full SQLite footprint (db + wal + shm) so a runaway WAL triggers it.
672
674
  * - Vacuums only when safe (<= vacuumMaxBytes) and either forced or dirty.
673
675
  */
674
676
  maintainBounded(options = {}) {
675
677
  const dbBytes = safeStatSize(this.dbPath);
676
678
  const footprint = this.getSqliteFootprintBytes();
677
- const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES', 1024 * 1024 * 1024);
679
+ const rotateBytes = readEnvBytes('NEXUS_NGRAM_ROTATE_BYTES', NGRAM_DEFAULT_ROTATE_BYTES);
678
680
  const vacuumMaxBytes = readEnvBytes('NEXUS_NGRAM_VACUUM_MAX_BYTES', 256 * 1024 * 1024);
679
681
  if (footprint >= rotateBytes && footprint > 0) {
680
- const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE !== '0';
682
+ const archiveEnabled = process.env.NEXUS_NGRAM_ARCHIVE_OVERSIZE === '1';
681
683
  const rotatedPath = `${this.dbPath}.oversize.${Date.now()}`;
682
684
  const removeSibling = (suffix) => {
683
685
  try {
@@ -7,6 +7,7 @@
7
7
  import { MemoryEngine } from './memory.js';
8
8
  import { SessionDNAManager } from './session-dna.js';
9
9
  import type { ClientRegistry } from './client-registry.js';
10
+ import type { RagIngestInput } from './rag-collections.js';
10
11
  import { type ExecutionRun, type ExecutionTask, type SubAgentRuntime } from '../phantom/index.js';
11
12
  export * from './orchestrator/types.js';
12
13
  import { type Agent, type AutonomyIntent, type BootstrapRequestOptions, type SessionAutonomyState, type SessionBootstrapResult, type OrchestratorOptions } from './orchestrator/types.js';
@@ -96,13 +97,7 @@ export declare class OrchestratorEngine {
96
97
  tags?: string[];
97
98
  scope?: 'session' | 'project';
98
99
  }): import("./rag-collections.js").RagCollection;
99
- ingestRagCollection(collectionId: string, inputs: Array<{
100
- filePath?: string;
101
- url?: string;
102
- text?: string;
103
- label?: string;
104
- tags?: string[];
105
- }>): Promise<{
100
+ ingestRagCollection(collectionId: string, inputs: RagIngestInput[]): Promise<{
106
101
  collection: import("./rag-collections.js").RagCollection;
107
102
  sourcesAdded: number;
108
103
  chunksAdded: number;
package/dist/index.d.ts CHANGED
@@ -213,6 +213,7 @@ export declare class NexusPrime {
213
213
  awaitReady(): Promise<void>;
214
214
  getClientRegistry(): ClientRegistry;
215
215
  getRuntimeHotAt(): number | null;
216
+ getDashboardAddress(): string | null;
216
217
  getSynapse(): SynapseRuntime | null;
217
218
  getArchitects(): ArchitectsRuntime | null;
218
219
  /**
package/dist/index.js CHANGED
@@ -847,6 +847,9 @@ export class NexusPrime {
847
847
  getRuntimeHotAt() {
848
848
  return this.runtimeHotAt;
849
849
  }
850
+ getDashboardAddress() {
851
+ return this.dashboardServer.getAddress();
852
+ }
850
853
  getSynapse() {
851
854
  return this.synapse;
852
855
  }
@@ -1070,7 +1073,7 @@ export class NexusPrime {
1070
1073
  const bootstrapManifest = ensureBootstrap({
1071
1074
  packageRoot: PACKAGE_ROOT,
1072
1075
  workspaceRoot: this.getWorkspaceContext().workspaceRoot,
1073
- phase: 'runtime',
1076
+ phase: 'install',
1074
1077
  silent: true,
1075
1078
  });
1076
1079
  this.runtime.recordBootstrapManifestStatus?.(bootstrapManifest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.9.18",
3
+ "version": "7.9.20",
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/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",
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/dashboard-runtime-adapters.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",