nexus-prime 7.9.18 → 7.9.19

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.
@@ -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 });
@@ -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;
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.9.18",
3
+ "version": "7.9.19",
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",