nexus-prime 6.2.0 → 6.3.0

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.
@@ -4,18 +4,18 @@
4
4
  * Renders a d3 force-layout combining:
5
5
  * - File nodes (source / test / config / other) from /api/knowledge-topology
6
6
  * - Memory nodes linked to those files
7
- * - Edges: memory→file references + memory co-tags
7
+ * - Deterministic structural edges plus separate memory overlay edges
8
8
  *
9
9
  * Interactions:
10
10
  * - Hover node → highlight 1-hop neighbours, dim others
11
11
  * - Click node → open drawer with detail
12
12
  * - Search input → highlight matching nodes by label
13
- * - Build button → POST /api/dispatch (no-op if unavailable) or trigger ensureCrGraphBuilt
13
+ * - Build button → POST /api/knowledge-topology/build with live SSE status
14
14
  */
15
15
 
16
- import { S } from '../state.js';
17
- import { api, post } from '../api.js';
18
- import { openDrawer } from '../widgets/drawer.js';
16
+ import { S } from '../state.js';
17
+ import { api, post, bustCache } from '../api.js';
18
+ import { openDrawer } from '../widgets/drawer.js';
19
19
 
20
20
  const $ = id => document.getElementById(id);
21
21
  const esc = s => s == null ? '' : String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
@@ -23,11 +23,15 @@ const esc = s => s == null ? '' : String(s).replace(/&/g,'&amp;').replace(/</g,'
23
23
  let _sim = null;
24
24
  let _topo = null;
25
25
  let _searchTerm = '';
26
+ let _buildState = { status: 'idle', message: '', at: 0 };
26
27
 
27
28
  /* ── Data loader ── */
28
29
  export async function load() {
29
30
  _topo = await api('/api/knowledge-topology', 60_000);
30
31
  if (_topo?.error) _topo = null;
32
+ if (_topo?.nodes?.length && !['queued', 'building'].includes(_buildState.status)) {
33
+ _setBuildState('ready', 'Graph ready.');
34
+ }
31
35
  _renderMeta();
32
36
  _renderGraph();
33
37
  _bindControls();
@@ -38,34 +42,84 @@ export function render() {
38
42
  _renderGraph();
39
43
  }
40
44
 
45
+ export function handleBuildEvent(evt) {
46
+ const repoRoot = evt?.payload?.repoRoot;
47
+ const selectedRoot = S.workspace?.repoRoot;
48
+ if (repoRoot && selectedRoot && repoRoot !== selectedRoot) return;
49
+
50
+ if (evt?.type === 'graph.cr.build.start') {
51
+ _setBuildState('building', 'Graph build in progress…');
52
+ return;
53
+ }
54
+ if (evt?.type === 'graph.cr.build.complete') {
55
+ _setBuildState('ready', 'Graph ready.');
56
+ bustCache('/api/knowledge-topology');
57
+ void load();
58
+ return;
59
+ }
60
+ if (evt?.type === 'graph.cr.build.failed') {
61
+ _setBuildState('failed', 'Graph build failed. Check backend logs and retry.');
62
+ }
63
+ }
64
+
41
65
  /* ── Meta header ── */
42
66
  function _renderMeta() {
43
67
  const el = $('repo-graph-meta'); if (!el) return;
44
- if (!_topo) { el.textContent = 'No topology data'; return; }
45
- const { meta, repoName, generatedAt } = _topo;
46
- const ts = generatedAt ? new Date(generatedAt).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}) : '—';
47
- el.textContent = `${repoName || '—'} · ${meta?.fileCount ?? 0} files · ${meta?.memoryCount ?? 0} memories · ${meta?.edgeCount ?? 0} edges · ${ts}`;
68
+ if (!_topo) {
69
+ el.textContent = _buildState.message || 'No topology data';
70
+ _syncBuildControls();
71
+ return;
72
+ }
73
+ const meta = _topo.meta || {};
74
+ const provenance = _topo.provenance || {};
75
+ const generatedAt = _topo.generatedAt
76
+ ? new Date(_topo.generatedAt).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' })
77
+ : '—';
78
+ const buildLabel = _buildState.status === 'idle'
79
+ ? 'idle'
80
+ : `${_buildState.status}${_buildState.message ? ` · ${_buildState.message}` : ''}`;
81
+ el.innerHTML = [
82
+ `<span>${esc(_topo.repoName || 'workspace')}</span>`,
83
+ `<span>${fmtNum(meta.fileCount ?? 0)} files</span>`,
84
+ `<span>${fmtNum(meta.edgeCount ?? 0)} edges</span>`,
85
+ `<span>${esc(provenance.graphSource || meta.graphSource || 'unknown-source')}</span>`,
86
+ `<span>${esc(provenance.freshness || meta.freshness || 'n/a')}</span>`,
87
+ `<span>${esc(buildLabel)}</span>`,
88
+ `<span>${esc(generatedAt)}</span>`,
89
+ ].join(' · ');
90
+ _syncBuildControls();
48
91
  }
49
92
 
50
93
  /* ── D3 force graph ── */
51
94
  function _renderGraph() {
52
95
  const svg = $('repo-graph-svg');
53
96
  const empty = $('repo-graph-empty');
54
- if (!svg || typeof d3 === 'undefined') return;
97
+ if (!svg || !empty) return;
98
+
99
+ if (typeof d3 === 'undefined') {
100
+ svg.innerHTML = '';
101
+ _showEmpty('Graph rendering unavailable', 'D3 did not load. Build state and metadata are still live.');
102
+ return;
103
+ }
55
104
 
56
105
  if (!_topo || !_topo.nodes?.length) {
57
- if (empty) { empty.style.display = 'flex'; }
106
+ svg.innerHTML = '';
107
+ const fallback = _buildState.status === 'failed'
108
+ ? ['Graph build failed', _buildState.message || 'Retry the build when the backend is healthy.']
109
+ : _buildState.status === 'queued' || _buildState.status === 'building'
110
+ ? ['Graph build in progress', _buildState.message || 'Waiting for backend build events…']
111
+ : ['Repo graph not built yet', 'Queue a graph build to render deterministic repo relationships.'];
112
+ _showEmpty(fallback[0], fallback[1]);
58
113
  return;
59
114
  }
60
- if (empty) empty.style.display = 'none';
115
+
116
+ empty.style.display = 'none';
61
117
 
62
118
  const container = $('repo-graph-container');
63
119
  const W = container?.clientWidth || 680;
64
120
  const H = container?.clientHeight || 480;
65
121
 
66
- // Build node and link arrays (mutable copies for simulation)
67
122
  const nodes = _topo.nodes.map(n => ({ ...n }));
68
- const nodeById = new Map(nodes.map(n => [n.id, n]));
69
123
  const links = (_topo.edges || []).map(e => ({ ...e }));
70
124
 
71
125
  const d3s = d3.select(svg);
@@ -75,38 +129,34 @@ function _renderGraph() {
75
129
  if (_sim) _sim.stop();
76
130
 
77
131
  _sim = d3.forceSimulation(nodes)
78
- .force('link', d3.forceLink(links).id(d => d.id).distance(d => d.type === 'co-tagged' ? 80 : 55).strength(0.2))
79
- .force('charge', d3.forceManyBody().strength(d => d.type === 'memory' ? -40 : -65))
132
+ .force('link', d3.forceLink(links).id(d => d.id).distance(d => _edgeDistance(d.type)).strength(0.22))
133
+ .force('charge', d3.forceManyBody().strength(d => d.type === 'memory' ? -40 : -70))
80
134
  .force('center', d3.forceCenter(W / 2, H / 2))
81
135
  .force('collide', d3.forceCollide(d => _nodeR(d) + 3));
82
136
 
83
- // ── Edge layer ──
84
137
  const linkLayer = d3s.append('g').attr('class', 'links');
85
138
  const linkEl = linkLayer.selectAll('line')
86
139
  .data(links).enter().append('line')
87
- .attr('stroke', d => d.type === 'co-tagged' ? '#3f3f46' : '#27272a')
88
- .attr('stroke-width', 1)
89
- .attr('stroke-opacity', 0.5)
90
- .attr('stroke-dasharray', d => d.type === 'co-tagged' ? '3,3' : null);
140
+ .attr('stroke', d => _edgeColor(d.type))
141
+ .attr('stroke-width', d => d.type === 'imports' ? 1.3 : 1)
142
+ .attr('stroke-opacity', d => d.type === 'memory-overlay' ? 0.35 : 0.5)
143
+ .attr('stroke-dasharray', d => ['memory-overlay', 'memory-association'].includes(d.type) ? '3,3' : null);
91
144
 
92
- // ── Node layer ──
93
145
  const nodeLayer = d3s.append('g').attr('class', 'nodes');
94
146
  const nodeEl = nodeLayer.selectAll('.repo-node')
95
147
  .data(nodes).enter().append(d => _makeNodeEl(d))
96
148
  .attr('class', 'repo-node')
97
149
  .style('cursor', 'pointer')
98
- .on('mouseover', (ev, d) => _onHover(d, nodeEl, linkEl, nodeById))
150
+ .on('mouseover', (ev, d) => _onHover(d, nodeEl, linkEl))
99
151
  .on('mouseout', () => _onHoverOut(nodeEl, linkEl))
100
152
  .on('click', (ev, d) => _onClick(d));
101
153
 
102
- // Drag behaviour
103
154
  const drag = d3.drag()
104
155
  .on('start', (ev, d) => { if (!ev.active) _sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
105
156
  .on('drag', (ev, d) => { d.fx = ev.x; d.fy = ev.y; })
106
157
  .on('end', (ev, d) => { if (!ev.active) _sim.alphaTarget(0); d.fx = null; d.fy = null; });
107
158
  nodeEl.call(drag);
108
159
 
109
- // Tick
110
160
  _sim.on('tick', () => {
111
161
  linkEl
112
162
  .attr('x1', d => d.source.x).attr('y1', d => d.source.y)
@@ -125,26 +175,25 @@ function _makeNodeEl(d) {
125
175
  c.setAttribute('r', String(r));
126
176
  c.setAttribute('fill', _memColor(d));
127
177
  c.setAttribute('fill-opacity', '0.85');
128
- c.setAttribute('stroke', '#000');
129
- c.setAttribute('stroke-width', '1');
130
- if (d.promoted) {
131
- c.classList.add('node-pulse');
132
- }
178
+ c.setAttribute('stroke', d.promoted ? '#a78bfa' : '#000');
179
+ c.setAttribute('stroke-width', d.promoted ? '1.5' : '1');
180
+ if (d.promoted) c.classList.add('node-pulse');
133
181
  doc.appendChild(c);
134
- } else {
135
- const half = r;
136
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
137
- rect.setAttribute('x', String(-half));
138
- rect.setAttribute('y', String(-half));
139
- rect.setAttribute('width', String(half * 2));
140
- rect.setAttribute('height', String(half * 2));
141
- rect.setAttribute('rx', '2');
142
- rect.setAttribute('fill', _fileColor(d));
143
- rect.setAttribute('fill-opacity', '0.8');
144
- rect.setAttribute('stroke', '#000');
145
- rect.setAttribute('stroke-width', '1');
146
- doc.appendChild(rect);
182
+ return doc;
147
183
  }
184
+
185
+ const half = r;
186
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
187
+ rect.setAttribute('x', String(-half));
188
+ rect.setAttribute('y', String(-half));
189
+ rect.setAttribute('width', String(half * 2));
190
+ rect.setAttribute('height', String(half * 2));
191
+ rect.setAttribute('rx', '2');
192
+ rect.setAttribute('fill', _fileColor(d));
193
+ rect.setAttribute('fill-opacity', '0.8');
194
+ rect.setAttribute('stroke', '#000');
195
+ rect.setAttribute('stroke-width', '1');
196
+ doc.appendChild(rect);
148
197
  return doc;
149
198
  }
150
199
 
@@ -153,11 +202,28 @@ function _nodeR(d) {
153
202
  return 5;
154
203
  }
155
204
 
205
+ function _edgeDistance(type) {
206
+ if (type === 'memory-association') return 85;
207
+ if (type === 'memory-overlay') return 70;
208
+ if (type === 'configures') return 80;
209
+ if (type === 'tests') return 60;
210
+ return 55;
211
+ }
212
+
213
+ function _edgeColor(type) {
214
+ if (type === 'imports') return '#27272a';
215
+ if (type === 'tests') return '#22c55e';
216
+ if (type === 'configures') return '#78716c';
217
+ if (type === 'memory-overlay') return '#00d4ff';
218
+ if (type === 'memory-association') return '#3f3f46';
219
+ return '#27272a';
220
+ }
221
+
156
222
  function _memColor(d) {
157
223
  const tier = d.tier || 'cortex';
158
224
  if (tier === 'prefrontal' || tier === 'working') return '#00d4ff';
159
225
  if (tier === 'hippocampus' || tier === 'episodic') return '#00ff88';
160
- return '#ffd14d'; // cortex / semantic
226
+ return '#ffd14d';
161
227
  }
162
228
 
163
229
  function _fileColor(d) {
@@ -169,7 +235,7 @@ function _fileColor(d) {
169
235
  }
170
236
 
171
237
  /* ── Hover: highlight 1-hop neighbours ── */
172
- function _onHover(d, nodeEl, linkEl, nodeById) {
238
+ function _onHover(d, nodeEl, linkEl) {
173
239
  const connected = new Set([d.id]);
174
240
  linkEl.each(function(l) {
175
241
  const s = typeof l.source === 'object' ? l.source.id : l.source;
@@ -188,41 +254,55 @@ function _onHover(d, nodeEl, linkEl, nodeById) {
188
254
 
189
255
  function _onHoverOut(nodeEl, linkEl) {
190
256
  nodeEl.attr('opacity', 1);
191
- linkEl.attr('stroke-opacity', 0.5);
257
+ linkEl.attr('stroke-opacity', l => l.type === 'memory-overlay' ? 0.35 : 0.5);
192
258
  _hideTooltip();
193
259
  }
194
260
 
195
261
  /* ── Click → drawer ── */
196
262
  function _onClick(d) {
197
- const lines = [];
263
+ const rows = [];
198
264
  if (d.type === 'file') {
199
- lines.push(`<div class="detail-row"><span class="detail-lbl">Path</span><code class="mono">${esc(d.path)}</code></div>`);
200
- lines.push(`<div class="detail-row"><span class="detail-lbl">Kind</span>${esc(d.kind || 'other')}</div>`);
265
+ rows.push(['Path', d.path]);
266
+ rows.push(['Kind', d.kind || 'other']);
201
267
  } else {
202
- lines.push(`<div class="detail-row"><span class="detail-lbl">Tier</span>${esc(d.tier)}</div>`);
203
- lines.push(`<div class="detail-row"><span class="detail-lbl">Priority</span>${(d.priority||0).toFixed(2)}</div>`);
204
- if (d.tags?.length) {
205
- lines.push(`<div class="detail-row"><span class="detail-lbl">Tags</span>${d.tags.map(t=>`<span class="tag">${esc(t)}</span>`).join(' ')}</div>`);
206
- }
207
- lines.push(`<div class="detail-row" style="margin-top:8px">${esc(d.label)}</div>`);
268
+ rows.push(['Tier', d.tier || 'cortex']);
269
+ rows.push(['Priority', (d.priority || 0).toFixed(2)]);
270
+ rows.push(['Tags', Array.isArray(d.tags) && d.tags.length ? d.tags.join(', ') : '—']);
208
271
  }
209
- openDrawer(d.label || d.id, `<div class="detail-block">${lines.join('')}</div>`);
272
+ rows.push(['Node type', d.type || 'unknown']);
273
+ openDrawer({
274
+ title: d.label || d.id,
275
+ body: `<div class="detail-block">
276
+ ${rows.map(([k, v]) => `<div class="detail-row"><span class="detail-lbl">${esc(k)}</span><span>${esc(String(v ?? '—'))}</span></div>`).join('')}
277
+ </div>`,
278
+ });
210
279
  }
211
280
 
212
281
  /* ── Search highlight ── */
213
282
  function _applySearch() {
214
- if (!_searchTerm) return;
283
+ const nodes = d3.selectAll('.repo-node');
284
+ if (!_searchTerm) {
285
+ nodes.attr('opacity', 1);
286
+ return;
287
+ }
215
288
  const q = _searchTerm.toLowerCase();
216
- d3.selectAll('.repo-node').attr('opacity', d =>
217
- (d.label || d.path || d.id || '').toLowerCase().includes(q) ? 1 : 0.15,
218
- );
289
+ nodes.attr('opacity', d => (d.label || d.path || d.id || '').toLowerCase().includes(q) ? 1 : 0.15);
219
290
  }
220
291
 
221
292
  /* ── Controls ── */
222
293
  function _bindControls() {
223
294
  const buildHandler = async () => {
224
- await post('/api/orchestrate', { goal: 'Build the code review graph for this repository.' }).catch(() => {});
295
+ _setBuildState('queued', 'Graph build queued…');
296
+ const result = await post('/api/knowledge-topology/build', {});
297
+ if (!result.ok) {
298
+ _setBuildState('failed', result.error || 'Graph build could not be queued.');
299
+ return;
300
+ }
301
+ bustCache('/api/knowledge-topology');
302
+ _renderMeta();
303
+ _renderGraph();
225
304
  };
305
+
226
306
  const b1 = $('repo-build-btn');
227
307
  const b2 = $('repo-build-btn2');
228
308
  if (b1) b1.onclick = buildHandler;
@@ -232,13 +312,58 @@ function _bindControls() {
232
312
  if (si) {
233
313
  si.oninput = () => {
234
314
  _searchTerm = si.value.trim();
235
- if (!_searchTerm) {
236
- d3.selectAll('.repo-node').attr('opacity', 1);
237
- } else {
238
- _applySearch();
239
- }
315
+ if (typeof d3 === 'undefined') return;
316
+ _applySearch();
240
317
  };
241
318
  }
319
+
320
+ _syncBuildControls();
321
+ }
322
+
323
+ function _syncBuildControls() {
324
+ const buttons = [$('repo-build-btn'), $('repo-build-btn2')].filter(Boolean);
325
+ const label = _buildState.status === 'building'
326
+ ? 'Building…'
327
+ : _buildState.status === 'queued'
328
+ ? 'Queued…'
329
+ : _buildState.status === 'failed'
330
+ ? 'Retry build'
331
+ : _buildState.status === 'ready'
332
+ ? 'Rebuild graph'
333
+ : 'Build graph';
334
+ for (const btn of buttons) {
335
+ btn.textContent = label;
336
+ btn.disabled = ['queued', 'building'].includes(_buildState.status);
337
+ btn.title = _buildState.message || '';
338
+ }
339
+ }
340
+
341
+ function _setBuildState(status, message='') {
342
+ _buildState = { status, message, at: Date.now() };
343
+ _syncBuildControls();
344
+ }
345
+
346
+ function _showEmpty(title, subtitle) {
347
+ const empty = $('repo-graph-empty');
348
+ if (!empty) return;
349
+ empty.style.display = 'flex';
350
+ empty.innerHTML = `
351
+ <span>${esc(title)}</span>
352
+ <span style="font-size:var(--caption);opacity:.7;max-width:360px;text-align:center">${esc(subtitle)}</span>
353
+ <button id="repo-build-btn2" class="btn btn-sm">${_buildState.status === 'failed' ? 'Retry build' : 'Build now'}</button>`;
354
+ $('repo-build-btn2')?.addEventListener('click', async () => {
355
+ _setBuildState('queued', 'Graph build queued…');
356
+ const result = await post('/api/knowledge-topology/build', {});
357
+ if (!result.ok) {
358
+ _setBuildState('failed', result.error || 'Graph build could not be queued.');
359
+ _renderMeta();
360
+ _renderGraph();
361
+ return;
362
+ }
363
+ bustCache('/api/knowledge-topology');
364
+ _renderMeta();
365
+ _renderGraph();
366
+ });
242
367
  }
243
368
 
244
369
  /* ── Tooltip ── */
@@ -253,10 +378,23 @@ function _showTooltip(txt) {
253
378
  _tip.style.display = 'block';
254
379
  document.addEventListener('mousemove', _moveTooltip, { passive: true });
255
380
  }
381
+
256
382
  function _moveTooltip(ev) {
257
- if (_tip) { _tip.style.left = (ev.clientX + 12) + 'px'; _tip.style.top = (ev.clientY - 8) + 'px'; }
383
+ if (_tip) {
384
+ _tip.style.left = (ev.clientX + 12) + 'px';
385
+ _tip.style.top = (ev.clientY - 8) + 'px';
386
+ }
258
387
  }
388
+
259
389
  function _hideTooltip() {
260
390
  if (_tip) _tip.style.display = 'none';
261
391
  document.removeEventListener('mousemove', _moveTooltip);
262
392
  }
393
+
394
+ function fmtNum(n) {
395
+ if (n == null || isNaN(n)) return '—';
396
+ const v = Number(n);
397
+ if (v >= 1e6) return (v / 1e6).toFixed(1) + 'M';
398
+ if (v >= 1000) return (v / 1000).toFixed(1) + 'K';
399
+ return String(Math.round(v));
400
+ }
@@ -134,19 +134,22 @@ function _buildDispatchStrip(run) {
134
134
 
135
135
  /* ── Data loader ── */
136
136
  export async function load() {
137
- const [teams, health, disp, appr, assets] = await Promise.all([
137
+ const [teams, health, disp, appr, assets, healthData] = await Promise.all([
138
138
  api('/api/synapse/teams', 5000),
139
139
  api('/api/synapse/health', 5000),
140
140
  api('/api/architects/dispatch', 5000),
141
141
  api('/api/synapse/approvals', 5000),
142
142
  api('/api/dashboard/surface/assets', 15000),
143
+ api('/api/health', 15000),
143
144
  ]);
144
145
  S.synapseTeams = teams;
146
+ S.synapseHealthRaw = health;
145
147
  S.synapseHealth = (Array.isArray(health) ? health : (health?.operatives||[])).map(_norm);
146
148
  S.archDispatch = disp;
147
149
  S.approvals = Array.isArray(appr) ? appr : (appr?.approvals||[]);
148
150
  S.assetsSurface = assets;
149
- notifyNotReady([teams, disp], _toast);
151
+ S.healthData = healthData;
152
+ notifyNotReady([teams, health, disp]);
150
153
  render();
151
154
  }
152
155
 
@@ -303,26 +306,73 @@ function _buildHireSelectors() {
303
306
  teamOptions: sel('hire-team', 'Strike team (optional)', teamOptions) };
304
307
  }
305
308
 
309
+ function _getHireReadiness() {
310
+ const synapseState = S.healthData?.runtimeEnvelope?.engines?.synapse ?? {};
311
+ const memoryStorage = S.healthData?.memory?.storage ?? {};
312
+ const notes = [];
313
+ const blocked = Boolean(
314
+ S.synapseHealthRaw?.notReady
315
+ || synapseState.ready === false
316
+ || synapseState.available === false,
317
+ );
318
+ if (blocked) {
319
+ notes.push({ tone: 'bad', text: S.synapseHealthRaw?.reason || synapseState.reason || 'Synapse is not ready for hires in this dashboard session.' });
320
+ }
321
+ if (synapseState.fallbackApplied) {
322
+ notes.push({ tone: 'warn', text: synapseState.reason || 'Synapse is running in fallback storage mode.' });
323
+ }
324
+ if (memoryStorage.fallbackApplied) {
325
+ notes.push({ tone: 'warn', text: memoryStorage.fallbackReason || 'Memory storage fallback is active.' });
326
+ }
327
+ return { blocked, notes };
328
+ }
329
+
330
+ function _formatHireFailure(result) {
331
+ const hint = result?.data?.hint;
332
+ const base = result?.error || 'Hire failed.';
333
+ return hint ? `${base} ${hint}` : base;
334
+ }
335
+
306
336
  function _showHireSheet(specialistId, name) {
307
337
  const { opOptions, teamOptions } = _buildHireSelectors();
338
+ const readiness = _getHireReadiness();
339
+ const noticesHtml = readiness.notes.length
340
+ ? `<div class="dsec" style="margin-bottom:var(--space-4)">
341
+ ${readiness.notes.map(note => `<div style="padding:8px 10px;border:1px solid ${note.tone === 'bad' ? '#ff5f5733' : '#ffd14d33'};background:${note.tone === 'bad' ? '#ff5f5712' : '#ffd14d12'};color:${note.tone === 'bad' ? 'var(--bad)' : '#ffd14d'};border-radius:8px;font-size:var(--text-sm);margin-bottom:8px">${esc(note.text)}</div>`).join('')}
342
+ </div>`
343
+ : '';
308
344
  openDrawer({ title: `Hire ${name}`,
309
345
  body: `<div class="dsec">
310
346
  <div class="dsec-title">Specialist</div>
311
347
  <div style="font-size:var(--text-sm);color:var(--text-muted);margin-bottom:var(--space-4)">${esc(name)}<br><span style="opacity:.6">${esc(specialistId)}</span></div>
348
+ ${noticesHtml}
312
349
  ${opOptions}
313
350
  ${teamOptions}
314
351
  <label style="display:block;margin-bottom:var(--space-2);font-size:var(--text-sm);color:var(--text-muted)">Budget cap (USD)</label>
315
352
  <input type="number" id="hire-budget" value="2.00" step="0.50" min="0.50" max="50"
316
353
  style="width:100%;background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:6px 10px;font-size:var(--text-sm);color:var(--text-main);margin-bottom:var(--space-4)">
317
- <button class="btn btn-primary" id="hire-confirm-btn" data-specid="${esc(specialistId)}" data-specname="${esc(name)}">Confirm Hire</button>
354
+ <button class="btn btn-primary" id="hire-confirm-btn" data-specid="${esc(specialistId)}" data-specname="${esc(name)}" ${readiness.blocked ? 'disabled title="Synapse is not ready"' : ''}>Confirm Hire</button>
355
+ <div id="hire-inline-status" style="margin-top:var(--space-3);font-size:var(--text-sm);color:${readiness.blocked ? 'var(--bad)' : 'var(--text-muted)'}">${readiness.blocked ? esc(readiness.notes[0]?.text || 'Synapse is not ready for hires.') : ''}</div>
318
356
  </div>` });
319
357
 
320
358
  document.getElementById('hire-confirm-btn')?.addEventListener('click', async btn => {
359
+ if (readiness.blocked) {
360
+ const status = document.getElementById('hire-inline-status');
361
+ if (status) status.textContent = readiness.notes[0]?.text || 'Synapse is not ready for hires.';
362
+ return;
363
+ }
321
364
  const specId = btn.target.dataset.specid;
322
365
  const specName = btn.target.dataset.specname;
323
366
  const budget = parseFloat(document.getElementById('hire-budget')?.value||'2');
324
367
  const reportsToVal = (document.getElementById('hire-reports-to')?.value||'').trim()||null;
325
368
  const teamVal = (document.getElementById('hire-team')?.value||'').trim()||null;
369
+ const inlineStatus = document.getElementById('hire-inline-status');
370
+ if (inlineStatus) {
371
+ inlineStatus.textContent = readiness.notes.some(note => note.tone === 'warn')
372
+ ? 'Submitting hire request. Fallback storage warning is active.'
373
+ : 'Submitting hire request…';
374
+ inlineStatus.style.color = readiness.notes.some(note => note.tone === 'warn') ? '#ffd14d' : 'var(--text-muted)';
375
+ }
326
376
  btn.target.disabled = true;
327
377
  btn.target.textContent = 'Hiring…';
328
378
 
@@ -341,13 +391,20 @@ function _showHireSheet(specialistId, name) {
341
391
  <div style="color:var(--accent);font-size:var(--text-sm);margin-bottom:var(--space-4)" id="hire-status-msg">Confirming with server…</div>
342
392
  <div class="drow" id="hire-id-row" style="display:none"><span class="drow-k">ID</span><span class="drow-v" id="hire-id-val">—</span></div>
343
393
  <div class="drow" id="hire-cost-row" style="display:none"><span class="drow-k">Est. per sortie</span><span class="drow-v" id="hire-cost-val">—</span></div>
394
+ <div class="drow"><span class="drow-k">Reports to</span><span class="drow-v">${esc(reportsToVal || 'team lead')}</span></div>
395
+ <div class="drow"><span class="drow-k">Strike team</span><span class="drow-v">${esc(teamVal || 'solo')}</span></div>
396
+ <div class="drow"><span class="drow-k">Budget cap</span><span class="drow-v">$${budget.toFixed(2)}</span></div>
344
397
  </div>` });
345
398
 
346
399
  // Reconcile when real response arrives.
347
400
  result.realResponse?.then(real => {
348
401
  const msg = document.getElementById('hire-status-msg');
349
402
  if (real.ok) {
350
- if (msg) msg.textContent = 'Hired successfully.';
403
+ if (msg) {
404
+ msg.textContent = readiness.notes.some(note => note.tone === 'warn')
405
+ ? 'Hired successfully. Fallback storage warning is still active.'
406
+ : 'Hired successfully.';
407
+ }
351
408
  const idRow = document.getElementById('hire-id-row');
352
409
  const idVal = document.getElementById('hire-id-val');
353
410
  if (idRow && idVal) { idVal.textContent = (real.data?.operative?.id||'').slice(-12)||'—'; idRow.style.display=''; }
@@ -393,7 +450,10 @@ function _showHireSheet(specialistId, name) {
393
450
  }).catch(() => { _dispatches.delete('__warmup__'); _refreshDrawerForOp(operativeId); });
394
451
  }
395
452
  } else {
396
- if (msg) { msg.textContent = `Hire failed: ${real.error||'server error'}`; msg.style.color = 'var(--bad)'; }
453
+ if (msg) {
454
+ msg.textContent = `Hire failed: ${_formatHireFailure(real)}`;
455
+ msg.style.color = 'var(--bad)';
456
+ }
397
457
  }
398
458
  }).catch(() => {
399
459
  const msg = document.getElementById('hire-status-msg');
@@ -1,13 +1,2 @@
1
- /**
2
- * graph.ts — /api/knowledge-topology
3
- *
4
- * Returns a combined node+edge graph for the repo view:
5
- * - File nodes from orchestrator.listTopologyFiles() (up to 120)
6
- * - Memory nodes from memory.listSnapshots() (up to 60)
7
- * - Edges: memory→file when a relative file path appears in a memory excerpt
8
- *
9
- * Cached 60s via respondCachedJson. No cr-graph connection at route time
10
- * (cr-graph build is non-blocking, done by ensureCrGraphBuilt in Cut 18).
11
- */
12
1
  import type { DashboardRouteHandler } from '../types.js';
13
2
  export declare const handleGraphRoutes: DashboardRouteHandler;