nexus-prime 7.9.13 → 7.9.15

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.
Files changed (32) hide show
  1. package/dist/agents/adapters/mcp/stdio-buffer.d.ts +6 -0
  2. package/dist/agents/adapters/mcp/stdio-buffer.js +45 -0
  3. package/dist/agents/adapters/mcp.d.ts +2 -0
  4. package/dist/agents/adapters/mcp.js +60 -7
  5. package/dist/cli/install-wizard.js +19 -0
  6. package/dist/cli.js +31 -1
  7. package/dist/dashboard/app/index.html +8 -0
  8. package/dist/dashboard/app/main.js +7 -0
  9. package/dist/dashboard/app/state.js +5 -0
  10. package/dist/dashboard/app/styles/board.css +163 -2
  11. package/dist/dashboard/app/styles/context-log.css +167 -0
  12. package/dist/dashboard/app/styles/memory.css +63 -0
  13. package/dist/dashboard/app/styles/workforce.css +21 -0
  14. package/dist/dashboard/app/views/board.js +145 -7
  15. package/dist/dashboard/app/views/context-log.js +158 -0
  16. package/dist/dashboard/app/views/memory.js +87 -3
  17. package/dist/dashboard/app/views/workforce.js +22 -6
  18. package/dist/dashboard/routes/events.js +80 -3
  19. package/dist/dashboard/stream/sse-broker.js +25 -13
  20. package/dist/engines/client-bootstrap.js +66 -20
  21. package/dist/engines/code-review-graph-client.d.ts +11 -3
  22. package/dist/engines/code-review-graph-client.js +151 -24
  23. package/dist/engines/instruction-gateway.js +6 -0
  24. package/dist/engines/mcp-entrypoint.js +3 -1
  25. package/dist/engines/orchestrator/decision-spine.d.ts +170 -0
  26. package/dist/engines/orchestrator/decision-spine.js +424 -0
  27. package/dist/engines/orchestrator/selection-policy.d.ts +39 -0
  28. package/dist/engines/orchestrator/selection-policy.js +32 -0
  29. package/dist/engines/orchestrator.js +19 -33
  30. package/dist/phantom/runtime.d.ts +16 -0
  31. package/dist/phantom/runtime.js +158 -20
  32. package/package.json +2 -2
@@ -0,0 +1,167 @@
1
+ .context-log-shell {
2
+ display: grid;
3
+ grid-template-columns: 260px minmax(0, 1fr);
4
+ gap: 14px;
5
+ align-items: start;
6
+ }
7
+
8
+ .context-log-rail,
9
+ .context-log-summary,
10
+ .context-log-row,
11
+ .context-log-decision {
12
+ background: var(--bg-elevated);
13
+ border: 1px solid var(--border);
14
+ border-radius: var(--radius);
15
+ }
16
+
17
+ .context-log-rail {
18
+ padding: 10px;
19
+ max-height: calc(100vh - 150px);
20
+ overflow-y: auto;
21
+ }
22
+
23
+ .context-log-rail-head {
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: space-between;
27
+ gap: 8px;
28
+ margin-bottom: 8px;
29
+ color: var(--text-muted);
30
+ font-family: var(--font-mono);
31
+ font-size: 0.78rem;
32
+ }
33
+
34
+ .context-log-run {
35
+ width: 100%;
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 3px;
39
+ padding: 8px 9px;
40
+ margin-bottom: 6px;
41
+ border: 1px solid var(--border);
42
+ border-radius: var(--radius);
43
+ background: var(--bg-panel);
44
+ color: var(--text-muted);
45
+ text-align: left;
46
+ cursor: pointer;
47
+ }
48
+
49
+ .context-log-run.active {
50
+ border-color: rgba(0,255,136,0.38);
51
+ background: rgba(0,255,136,0.06);
52
+ color: var(--text-main);
53
+ }
54
+
55
+ .context-log-run span,
56
+ .context-log-row-head span {
57
+ font-family: var(--font-mono);
58
+ font-size: 0.78rem;
59
+ }
60
+
61
+ .context-log-run small {
62
+ color: var(--text-dim);
63
+ font-size: 0.72rem;
64
+ }
65
+
66
+ .context-log-main {
67
+ min-width: 0;
68
+ }
69
+
70
+ .context-log-summary {
71
+ display: grid;
72
+ grid-template-columns: repeat(5, minmax(90px, 1fr));
73
+ gap: 10px;
74
+ padding: 14px;
75
+ margin-bottom: 14px;
76
+ }
77
+
78
+ .context-log-kpi span,
79
+ .context-log-selection span {
80
+ display: block;
81
+ margin-bottom: 4px;
82
+ color: var(--text-dim);
83
+ font-family: var(--font-mono);
84
+ font-size: 0.68rem;
85
+ text-transform: uppercase;
86
+ letter-spacing: 0.05em;
87
+ }
88
+
89
+ .context-log-kpi strong {
90
+ color: var(--text-main);
91
+ font-size: 1rem;
92
+ }
93
+
94
+ .context-log-selection {
95
+ grid-column: 1 / -1;
96
+ display: grid;
97
+ grid-template-columns: repeat(3, minmax(0, 1fr));
98
+ gap: 10px;
99
+ }
100
+
101
+ .context-log-selection > div,
102
+ .context-log-refs {
103
+ display: flex;
104
+ flex-wrap: wrap;
105
+ gap: 4px;
106
+ min-width: 0;
107
+ }
108
+
109
+ .context-log-grid {
110
+ display: grid;
111
+ grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
112
+ gap: 14px;
113
+ }
114
+
115
+ .context-log-row,
116
+ .context-log-decision {
117
+ padding: 10px 12px;
118
+ margin-bottom: 8px;
119
+ }
120
+
121
+ .context-log-row-head {
122
+ display: grid;
123
+ grid-template-columns: auto auto minmax(0, 1fr);
124
+ gap: 8px;
125
+ align-items: center;
126
+ margin-bottom: 7px;
127
+ color: var(--accent);
128
+ }
129
+
130
+ .context-log-row-head time {
131
+ justify-self: end;
132
+ color: var(--text-dim);
133
+ font-family: var(--font-mono);
134
+ font-size: 0.7rem;
135
+ }
136
+
137
+ .context-log-row p,
138
+ .context-log-decision p {
139
+ margin: 0 0 8px;
140
+ color: var(--text-muted);
141
+ font-size: 0.78rem;
142
+ line-height: 1.45;
143
+ }
144
+
145
+ .context-log-decision strong {
146
+ display: block;
147
+ margin-bottom: 6px;
148
+ color: var(--text-main);
149
+ font-size: 0.82rem;
150
+ line-height: 1.35;
151
+ }
152
+
153
+ .context-log-empty {
154
+ color: var(--text-dim);
155
+ font-size: 0.72rem;
156
+ }
157
+
158
+ @media (max-width: 980px) {
159
+ .context-log-shell,
160
+ .context-log-grid,
161
+ .context-log-selection {
162
+ grid-template-columns: 1fr;
163
+ }
164
+ .context-log-summary {
165
+ grid-template-columns: repeat(2, minmax(0, 1fr));
166
+ }
167
+ }
@@ -58,6 +58,51 @@
58
58
  }
59
59
  .mem-search-row input { flex: 1; border: none; outline: none; font-size: 0.78rem; color: var(--text-main); min-width: 0; }
60
60
  .mem-search-row input::placeholder { color: var(--text-dim); }
61
+ .memory-selection-panel {
62
+ border-bottom: 1px solid var(--border);
63
+ background: var(--bg-panel);
64
+ }
65
+ .memory-selection-empty {
66
+ padding: 8px 12px;
67
+ color: var(--text-dim);
68
+ font-size: 0.78rem;
69
+ }
70
+ .memory-selection-head {
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: space-between;
74
+ gap: 8px;
75
+ padding: 8px 12px 6px;
76
+ color: var(--text-muted);
77
+ font-size: 0.78rem;
78
+ }
79
+ .memory-selection-head strong {
80
+ color: var(--accent);
81
+ font-family: var(--font-mono);
82
+ }
83
+ .memory-selection-actions,
84
+ .memory-selection-chips {
85
+ display: flex;
86
+ flex-wrap: wrap;
87
+ gap: 6px;
88
+ }
89
+ .memory-selection-chips {
90
+ padding: 0 12px 8px;
91
+ }
92
+ .memory-code-block {
93
+ margin: 0 12px 10px;
94
+ max-height: 180px;
95
+ overflow: auto;
96
+ padding: 9px 10px;
97
+ border: 1px solid var(--border);
98
+ border-radius: var(--radius);
99
+ background: rgba(0,0,0,0.32);
100
+ color: var(--text-muted);
101
+ font-family: var(--font-mono);
102
+ font-size: 0.72rem;
103
+ line-height: 1.45;
104
+ white-space: pre-wrap;
105
+ }
61
106
  #mem-list { max-height: 290px; overflow-y: auto; }
62
107
  .mem-item {
63
108
  display: flex; align-items: flex-start; gap: 9px; padding: 9px 12px;
@@ -65,6 +110,24 @@
65
110
  }
66
111
  .mem-item:last-child { border-bottom: none; }
67
112
  .mem-item:hover { background: var(--bg-panel); }
113
+ .mem-item.selected { background: rgba(0,255,136,0.05); }
114
+ .mem-select-btn {
115
+ flex-shrink: 0;
116
+ min-width: 62px;
117
+ padding: 3px 6px;
118
+ border: 1px solid var(--border);
119
+ border-radius: var(--radius);
120
+ background: var(--bg-elevated);
121
+ color: var(--text-dim);
122
+ font-family: var(--font-mono);
123
+ font-size: 0.68rem;
124
+ cursor: pointer;
125
+ }
126
+ .mem-select-btn.active {
127
+ color: var(--accent);
128
+ border-color: rgba(0,255,136,0.32);
129
+ background: rgba(0,255,136,0.08);
130
+ }
68
131
  .tier-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; margin-top: 4px; }
69
132
  .t-working { background: var(--accent); }
70
133
  .t-episodic { background: var(--secondary); }
@@ -145,6 +145,27 @@
145
145
  .kc-worker { font-size: 0.65rem; color: var(--text-dim); font-family: var(--font-mono); margin-top: 3px; display: block; }
146
146
  .kanban-overflow { font-size: 0.7rem; color: var(--text-dim); text-align: center; padding: 4px; }
147
147
  .kanban-empty { color: var(--text-dim); font-size: 0.8rem; padding: 20px; text-align: center; }
148
+ .kanban-empty-panel {
149
+ min-height: 110px;
150
+ border: 1px dashed var(--border);
151
+ border-radius: var(--radius);
152
+ display: flex;
153
+ flex-direction: column;
154
+ align-items: center;
155
+ justify-content: center;
156
+ gap: 6px;
157
+ }
158
+ .kanban-empty-title {
159
+ font-family: var(--font-mono);
160
+ font-size: 0.8rem;
161
+ color: var(--text-dim);
162
+ }
163
+ .kanban-empty-sub {
164
+ max-width: 360px;
165
+ font-size: 0.74rem;
166
+ line-height: 1.45;
167
+ opacity: 0.75;
168
+ }
148
169
 
149
170
  /* ── Sortie timeline ── */
150
171
  .sortie-scroll { display: flex; flex-direction: column; gap: 8px; padding: 10px; }
@@ -49,6 +49,16 @@ function animCounter(id, target) {
49
49
  function chipHtml(text, mod='') {
50
50
  return `<span class="chip ${esc(mod)}">${esc(String(text))}</span>`;
51
51
  }
52
+ function miniListHtml(items, limit = 5) {
53
+ const arr = Array.isArray(items) ? items.filter(Boolean) : [];
54
+ if (!arr.length) return '<span class="muted">—</span>';
55
+ const shown = arr.slice(0, limit).map(item => chipHtml(String(item).replace(/^.*\//, ''))).join('');
56
+ const more = arr.length > limit ? chipHtml('+' + (arr.length - limit), 'muted') : '';
57
+ return `<div class="decision-spine-list">${shown}${more}</div>`;
58
+ }
59
+ function decisionSpineUrl(runId) {
60
+ return '/api/runs/' + encodeURIComponent(runId) + '/decision-spine';
61
+ }
52
62
  function catColor(cat) {
53
63
  const m = {memory:'#00d4ff',tokens:'#00ff88',runtime:'#ffd14d',pod:'#b05cff',
54
64
  shield:'#ff5f57',mcp:'#52525b',clients:'#a1a1aa',system:'#a1a1aa',
@@ -467,16 +477,27 @@ function kcardHtml(c, stage) {
467
477
  <div class="kcard-meta">${role}${opBadge}${tok}</div>${tm}</div>`;
468
478
  }
469
479
 
480
+ function kanbanEmptyHtml(stage) {
481
+ const copy = {
482
+ planning: ['No work in flight', 'Run a goal to see decomposition here.'],
483
+ hiring: ['No hires pending', 'Worker selection appears when a run needs operatives.'],
484
+ running: ['No active workers', 'Dispatched work appears here while it executes.'],
485
+ ghostpass: ['No review pending', 'Ghost-pass waits here only when a patch exists.'],
486
+ done: ['No completed runs yet', 'Finished runs land here with their final state.'],
487
+ }[stage] || ['No work here', 'This lane will populate when the run reaches it.'];
488
+ return `<div class="kb-empty">
489
+ <span class="kb-empty-title">${esc(copy[0])}</span>
490
+ <span class="kb-empty-sub">${esc(copy[1])}</span>
491
+ </div>`;
492
+ }
493
+
470
494
  function renderKanban() {
471
495
  const cols = buildKanbanCols();
472
496
  for (const s of ['planning','hiring','running','ghostpass','done']) {
473
497
  const body=$('kb-'+s), cnt=$('kc-'+s); if (!body) continue;
474
498
  const cards=cols[s]||[];
475
499
  if (cnt) cnt.textContent=String(cards.length);
476
- const emptyHtml = s==='planning'
477
- ? `<div class="kb-empty"><span class="kb-empty-txt">No work in flight</span></div>`
478
- : `<div class="kb-empty"><span class="kb-empty-txt">empty</span></div>`;
479
- body.innerHTML = cards.length ? cards.map(c=>kcardHtml(c,s)).join('') : emptyHtml;
500
+ body.innerHTML = cards.length ? cards.map(c=>kcardHtml(c,s)).join('') : kanbanEmptyHtml(s);
480
501
  }
481
502
  // Click delegation
482
503
  document.querySelectorAll('.kcard[data-runid]').forEach(el => {
@@ -543,14 +564,34 @@ function renderToolHealth() {
543
564
  }
544
565
 
545
566
  /* ── Run drawer helper ── */
567
+ function loadDecisionSpine(runId) {
568
+ if (!runId) return Promise.resolve(null);
569
+ return api(decisionSpineUrl(runId), 0).then(spine => {
570
+ if (spine?.artifacts) {
571
+ S.lastDecisionSpine = spine;
572
+ renderOrchestrationPipeline();
573
+ return spine;
574
+ }
575
+ return null;
576
+ });
577
+ }
578
+
546
579
  function _openRunDrawer(runId) {
547
580
  const run = S.runs.find(r=>r.id===runId||r.runId===runId);
548
581
  if (!run) {
549
582
  openDrawer({ title: 'Run '+runId, body: '<div class="empty"><div class="empty-title">Loading…</div></div>' });
550
- api('/api/runs/'+encodeURIComponent(runId),0).then(d=>{ if(d){ const b=document.getElementById('drawer-body'); if(b) b.innerHTML=_buildRunHtml(d); } });
583
+ api('/api/runs/'+encodeURIComponent(runId),0).then(d=>{
584
+ const b=document.getElementById('drawer-body');
585
+ if(d && b) b.innerHTML=_buildRunHtml(d);
586
+ loadDecisionSpine(runId).then(spine => { if (d && spine && b) b.innerHTML=_buildRunHtml({...d, decisionSpine: spine}); });
587
+ });
551
588
  return;
552
589
  }
553
590
  openDrawer({ title: 'Run '+String(runId).slice(-6), body: _buildRunHtml(run) });
591
+ loadDecisionSpine(runId).then(spine => {
592
+ const b=document.getElementById('drawer-body');
593
+ if (spine && b) b.innerHTML=_buildRunHtml({...run, decisionSpine: spine});
594
+ });
554
595
  }
555
596
 
556
597
  function _buildRunHtml(r) {
@@ -558,8 +599,100 @@ function _buildRunHtml(r) {
558
599
  const op = S.synapseHealth.find(o => o.currentRunId===(r.id||r.runId)||(r.goal&&(o.goal===r.goal||(o.role&&r.goal?.toLowerCase().includes(o.role?.toLowerCase())))));
559
600
  const opSection = op ? `<div class="dsec"><div class="dsec-title">Operative</div>${drows([['Name',op.name||op.role||op.id],['Status',op.status||'—'],['Budget used',op.budgetUsed!=null?fmtNum(op.budgetUsed)+'t':'—'],['Team',op.team||op.strikeName||'—']])}</div>` : '';
560
601
  const tokDisplay = r.tokensUsed ? `<span style="color:var(--accent);font-family:var(--font-mono);font-size:var(--text-lg);font-weight:var(--weight-semibold)">${fmtNum(r.tokensUsed)}t</span>` : '—';
602
+ const spineSection = r.decisionSpine ? _buildDecisionSpineHtml(r.decisionSpine) : '';
561
603
  return `<div class="dsec"><div class="dsec-title">Run details</div>${drows([['ID',(r.id||r.runId||'').slice(-12)],['Status',r.status],['Goal',r.goal||r.mandate],['Created',r.createdAt?new Date(r.createdAt).toLocaleString():'—'],['Completed',r.completedAt?new Date(r.completedAt).toLocaleString():'—']])}</div>
562
- <div class="dsec"><div class="dsec-title">Token cost</div><div style="padding:var(--space-2) 0">${tokDisplay}</div></div>${opSection}`;
604
+ <div class="dsec"><div class="dsec-title">Token cost</div><div style="padding:var(--space-2) 0">${tokDisplay}</div></div>${spineSection}${opSection}`;
605
+ }
606
+
607
+ function _buildDecisionSpineHtml(spine) {
608
+ const artifacts = spine?.artifacts || {};
609
+ const brief = artifacts.requestBrief || {};
610
+ const plan = artifacts.selectionPlan || {};
611
+ const contextLog = Array.isArray(artifacts.contextLog) ? artifacts.contextLog : [];
612
+ const decisionLog = Array.isArray(artifacts.decisionLog) ? artifacts.decisionLog : [];
613
+ const selected = spine?.summary?.selected || plan.selected || {};
614
+ const model = spine?.summary?.modelRoute || plan.modelRoute || brief.modelPolicy || {};
615
+ const latestDecision = spine?.summary?.latestDecision || decisionLog[decisionLog.length - 1] || null;
616
+ const prompt = brief.rewrittenPrompt || brief.rawPrompt || '';
617
+ return `<div class="dsec decision-spine">
618
+ <div class="dsec-title">Decision spine <a class="decision-spine-full-link" href="#context-log">Open full log</a></div>
619
+ ${prompt ? `<div class="decision-spine-prompt">${esc(prompt)}</div>` : ''}
620
+ <div class="decision-spine-grid">
621
+ <div class="decision-spine-card">
622
+ <div class="decision-spine-k">Intent</div>
623
+ <div class="decision-spine-v">${esc(brief.intent || plan.intent || spine?.summary?.intent || '—')}</div>
624
+ <div class="decision-spine-sub">${esc(brief.risk || spine?.summary?.risk || 'risk: —')}</div>
625
+ </div>
626
+ <div class="decision-spine-card">
627
+ <div class="decision-spine-k">Model route</div>
628
+ <div class="decision-spine-v">${esc(model.workerTier || '—')}</div>
629
+ <div class="decision-spine-sub">${esc(model.reviewerTier ? 'review ' + model.reviewerTier : model.reason || '—')}</div>
630
+ </div>
631
+ <div class="decision-spine-card">
632
+ <div class="decision-spine-k">Context</div>
633
+ <div class="decision-spine-v">${fmtNum(contextLog.length)} events</div>
634
+ <div class="decision-spine-sub">${fmtNum(decisionLog.length)} decisions</div>
635
+ </div>
636
+ </div>
637
+ <div class="decision-spine-block"><span>Files</span>${miniListHtml(selected.files || plan.candidates?.files || [])}</div>
638
+ <div class="decision-spine-block"><span>Skills</span>${miniListHtml(selected.skills || plan.candidates?.skills || [])}</div>
639
+ <div class="decision-spine-block"><span>Crew</span>${miniListHtml([...(selected.crews || []), ...(selected.specialists || [])], 6)}</div>
640
+ ${latestDecision ? `<div class="decision-spine-latest">${esc(latestDecision.verb || 'decision')} · ${esc(latestDecision.decision || '')}</div>` : ''}
641
+ ${_buildDecisionSpineLogBrowser(contextLog, decisionLog)}
642
+ </div>`;
643
+ }
644
+
645
+ function _buildDecisionSpineLogBrowser(contextLog, decisionLog) {
646
+ const contextRows = contextLog.slice(0, 6).map(entry => `
647
+ <div class="decision-spine-log-row">
648
+ <div class="decision-spine-log-head">
649
+ <span>${esc(entry.phase || 'context')}</span>
650
+ <span>${esc(entry.actor || 'system')}</span>
651
+ </div>
652
+ <div class="decision-spine-log-reason">${esc(entry.reason || '—')}</div>
653
+ ${miniListHtml([...(entry.contextRefs || []), ...(entry.fileRefs || []), ...(entry.evidenceRefs || [])], 7)}
654
+ </div>`).join('');
655
+ const decisionRows = decisionLog.slice(0, 6).map(entry => `
656
+ <div class="decision-spine-log-row">
657
+ <div class="decision-spine-log-head">
658
+ <span>${esc(entry.verb || 'decision')}</span>
659
+ <span>${esc(entry.surface || entry.actor || 'runtime')}</span>
660
+ </div>
661
+ <div class="decision-spine-log-reason">${esc(entry.decision || '—')}</div>
662
+ <div class="decision-spine-log-ref">${esc(entry.reason || '')}</div>
663
+ </div>`).join('');
664
+ if (!contextRows && !decisionRows) return '';
665
+ return `<div class="decision-spine-browser">
666
+ <div class="decision-spine-browser-col">
667
+ <div class="decision-spine-browser-title">Context log</div>
668
+ ${contextRows || '<div class="decision-spine-empty">—</div>'}
669
+ </div>
670
+ <div class="decision-spine-browser-col">
671
+ <div class="decision-spine-browser-title">Decision log</div>
672
+ ${decisionRows || '<div class="decision-spine-empty">—</div>'}
673
+ </div>
674
+ </div>`;
675
+ }
676
+
677
+ function _buildDecisionSpineMiniHtml(spine) {
678
+ const artifacts = spine?.artifacts || {};
679
+ const brief = artifacts.requestBrief || {};
680
+ const plan = artifacts.selectionPlan || {};
681
+ const selected = spine?.summary?.selected || plan.selected || {};
682
+ const model = spine?.summary?.modelRoute || plan.modelRoute || brief.modelPolicy || {};
683
+ return `<div class="decision-spine-mini">
684
+ <div class="decision-spine-mini-head">Decision Spine · run ${esc((spine.runId || '').slice(-8))}</div>
685
+ <div class="decision-spine-mini-row">
686
+ <span>intent ${esc(brief.intent || plan.intent || spine?.summary?.intent || '—')}</span>
687
+ <span>model ${esc(model.workerTier || '—')}</span>
688
+ <span>ctx ${fmtNum(spine?.summary?.contextEvents || artifacts.contextLog?.length || 0)}</span>
689
+ <span>decisions ${fmtNum(spine?.summary?.decisions || artifacts.decisionLog?.length || 0)}</span>
690
+ </div>
691
+ <div class="decision-spine-mini-lists">
692
+ ${miniListHtml(selected.skills || plan.candidates?.skills || [], 4)}
693
+ ${miniListHtml(selected.files || plan.candidates?.files || [], 4)}
694
+ </div>
695
+ </div>`;
563
696
  }
564
697
 
565
698
  /* ─── Orchestration pipeline (decomposition + completion live signal) ─── */
@@ -574,8 +707,10 @@ export function handleOrchestrationEvent(evt) {
574
707
  const payload = evt.payload || evt;
575
708
  if (evt.type === 'orchestration.decomposed') {
576
709
  S.lastDecomposition = { ...payload, ts: evt.time || Date.now() };
710
+ loadDecisionSpine(payload.runId || payload.id);
577
711
  } else if (evt.type === 'orchestration.completed') {
578
712
  S.lastCompletion = { ...payload, ts: evt.time || Date.now() };
713
+ loadDecisionSpine(payload.runId || payload.id);
579
714
  }
580
715
  renderOrchestrationPipeline();
581
716
  }
@@ -585,7 +720,8 @@ function renderOrchestrationPipeline() {
585
720
  if (!host) return;
586
721
  const dec = S.lastDecomposition;
587
722
  const cmp = S.lastCompletion;
588
- if (!dec && !cmp) {
723
+ const spine = S.lastDecisionSpine;
724
+ if (!dec && !cmp && !spine) {
589
725
  host.style.display = 'none';
590
726
  return;
591
727
  }
@@ -618,11 +754,13 @@ function renderOrchestrationPipeline() {
618
754
  <div style="font-size:12px;color:var(--muted);margin-bottom:6px">${esc(cmp.result || '')}</div>
619
755
  <div>${chip('verified', `${cmp.verifiedWorkers ?? 0}/${cmp.totalWorkers ?? 0}`)}${chip('saved', `${fmtNum(cmp.savedTokens ?? 0)} t`)}${chip('compression', `${cmp.compressionPct ?? 0}%`)}${chip('duration', `${Math.round((cmp.durationMs ?? 0) / 100) / 10}s`)}</div>
620
756
  </div>` : '';
757
+ const spineBlock = spine ? _buildDecisionSpineMiniHtml(spine) : '';
621
758
  const headerNote = same ? '' : (dec && cmp ? '<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Showing latest decomposition + most recent completion (different runs)</div>' : '');
622
759
  host.innerHTML = `
623
760
  <div class="shd" style="margin-bottom:8px">Orchestration Pipeline</div>
624
761
  ${headerNote}
625
762
  ${decBlock}
626
763
  ${cmpBlock}
764
+ ${spineBlock}
627
765
  `;
628
766
  }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * context-log.js — Standalone decision-spine context and decision browser.
3
+ */
4
+
5
+ import { S } from '../state.js';
6
+ import { api, bustCache } from '../api.js';
7
+
8
+ const $ = id => document.getElementById(id);
9
+ const esc = s => s == null ? '' : String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
10
+
11
+ function fmtTs(ts) {
12
+ if (!ts) return '—';
13
+ try { return new Date(ts).toLocaleString(); } catch { return '—'; }
14
+ }
15
+
16
+ function runIdOf(run) {
17
+ return run?.runId || run?.id || '';
18
+ }
19
+
20
+ function spineUrl(runId) {
21
+ return '/api/runs/' + encodeURIComponent(runId) + '/decision-spine';
22
+ }
23
+
24
+ function chips(items, limit = 12) {
25
+ const values = (items || []).filter(Boolean).slice(0, limit);
26
+ if (!values.length) return '<span class="context-log-empty">—</span>';
27
+ return values.map(value => `<span class="chip chip-muted">${esc(String(value).replace(/^.*\//, ''))}</span>`).join('');
28
+ }
29
+
30
+ function summaryCard(spine) {
31
+ const artifacts = spine?.artifacts || {};
32
+ const brief = artifacts.requestBrief || {};
33
+ const plan = artifacts.selectionPlan || {};
34
+ const model = spine?.summary?.modelRoute || plan.modelRoute || brief.modelPolicy || {};
35
+ const selected = spine?.summary?.selected || plan.selected || {};
36
+ return `<div class="context-log-summary">
37
+ <div class="context-log-kpi"><span>Intent</span><strong>${esc(brief.intent || plan.intent || spine?.summary?.intent || '—')}</strong></div>
38
+ <div class="context-log-kpi"><span>Risk</span><strong>${esc(brief.risk || '—')}</strong></div>
39
+ <div class="context-log-kpi"><span>Model</span><strong>${esc(model.workerTier || '—')}${model.reviewerTier ? ` / ${esc(model.reviewerTier)}` : ''}</strong></div>
40
+ <div class="context-log-kpi"><span>Events</span><strong>${esc(spine?.summary?.contextEvents ?? artifacts.contextLog?.length ?? 0)}</strong></div>
41
+ <div class="context-log-kpi"><span>Decisions</span><strong>${esc(spine?.summary?.decisions ?? artifacts.decisionLog?.length ?? 0)}</strong></div>
42
+ <div class="context-log-selection">
43
+ <div><span>Files</span>${chips(selected.files, 18)}</div>
44
+ <div><span>Skills</span>${chips(selected.skills, 18)}</div>
45
+ <div><span>Specialists</span>${chips(selected.specialists, 18)}</div>
46
+ </div>
47
+ </div>`;
48
+ }
49
+
50
+ function contextRows(entries) {
51
+ if (!entries?.length) {
52
+ return '<div class="empty"><div class="empty-title">No context events for this run</div></div>';
53
+ }
54
+ return entries.map(entry => {
55
+ const refs = [
56
+ ...(entry.contextRefs || []),
57
+ ...(entry.memoryRefs || []),
58
+ ...(entry.fileRefs || []),
59
+ ...(entry.evidenceRefs || []),
60
+ ];
61
+ return `<article class="context-log-row">
62
+ <div class="context-log-row-head">
63
+ <span>${esc(entry.phase || 'context')}</span>
64
+ <span>${esc(entry.actor || 'system')}</span>
65
+ <time>${esc(fmtTs(entry.createdAt))}</time>
66
+ </div>
67
+ <p>${esc(entry.reason || '')}</p>
68
+ <div class="context-log-refs">${chips(refs, 24)}</div>
69
+ </article>`;
70
+ }).join('');
71
+ }
72
+
73
+ function decisionRows(entries) {
74
+ if (!entries?.length) {
75
+ return '<div class="empty"><div class="empty-title">No decisions for this run</div></div>';
76
+ }
77
+ return `<div class="context-log-decision-list">${entries.map(entry => `
78
+ <article class="context-log-decision">
79
+ <div class="context-log-row-head">
80
+ <span>${esc(entry.verb || 'decision')}</span>
81
+ <span>${esc(entry.surface || entry.actor || 'runtime')}</span>
82
+ <time>${esc(fmtTs(entry.createdAt))}</time>
83
+ </div>
84
+ <strong>${esc(entry.decision || '')}</strong>
85
+ <p>${esc(entry.reason || '')}</p>
86
+ <div class="context-log-refs">${chips([entry.beforeRef, entry.afterRef].filter(Boolean), 6)}</div>
87
+ </article>
88
+ `).join('')}</div>`;
89
+ }
90
+
91
+ function renderRunRail(runs) {
92
+ if (!runs.length) return '<div class="empty"><div class="empty-title">No runs recorded</div></div>';
93
+ return runs.map(run => {
94
+ const id = runIdOf(run);
95
+ const active = id === S.contextLogSelectedRunId;
96
+ return `<button class="context-log-run ${active ? 'active' : ''}" data-context-run="${esc(id)}">
97
+ <span>${esc(String(id).slice(-10) || 'run')}</span>
98
+ <small>${esc(run.state || run.status || 'queued')} · ${esc(fmtTs(run.createdAt))}</small>
99
+ </button>`;
100
+ }).join('');
101
+ }
102
+
103
+ export async function load() {
104
+ const runs = await api('/api/runs?limit=30', 3000);
105
+ S.contextLogRuns = Array.isArray(runs) ? runs : [];
106
+ if (!S.contextLogSelectedRunId) {
107
+ S.contextLogSelectedRunId = runIdOf(S.contextLogRuns[0]) || null;
108
+ }
109
+ if (S.contextLogSelectedRunId) {
110
+ S.contextLogSpine = await api(spineUrl(S.contextLogSelectedRunId), 0);
111
+ } else {
112
+ S.contextLogSpine = null;
113
+ }
114
+ render();
115
+ }
116
+
117
+ export function render() {
118
+ const host = $('context-log-view');
119
+ if (!host) return;
120
+ const spine = S.contextLogSpine;
121
+ const artifacts = spine?.artifacts || {};
122
+ const contextLog = artifacts.contextLog || [];
123
+ const decisionLog = artifacts.decisionLog || [];
124
+ host.innerHTML = `<div class="context-log-shell">
125
+ <aside class="context-log-rail">
126
+ <div class="context-log-rail-head">
127
+ <span>Runs</span>
128
+ <button class="btn btn-sm" id="context-log-refresh-btn">Refresh</button>
129
+ </div>
130
+ ${renderRunRail(S.contextLogRuns || [])}
131
+ </aside>
132
+ <section class="context-log-main">
133
+ ${spine ? summaryCard(spine) : '<div class="empty"><div class="empty-title">Select a run</div></div>'}
134
+ <div class="context-log-grid">
135
+ <div>
136
+ <div class="shd">Context events</div>
137
+ ${contextRows(contextLog)}
138
+ </div>
139
+ <div>
140
+ <div class="shd">Decisions</div>
141
+ ${decisionRows(decisionLog)}
142
+ </div>
143
+ </div>
144
+ </section>
145
+ </div>`;
146
+ host.querySelectorAll('[data-context-run]').forEach(button => {
147
+ button.addEventListener('click', async () => {
148
+ S.contextLogSelectedRunId = button.dataset.contextRun || null;
149
+ if (S.contextLogSelectedRunId) bustCache(spineUrl(S.contextLogSelectedRunId));
150
+ await load();
151
+ });
152
+ });
153
+ $('context-log-refresh-btn')?.addEventListener('click', async () => {
154
+ bustCache('/api/runs?limit=30');
155
+ if (S.contextLogSelectedRunId) bustCache(spineUrl(S.contextLogSelectedRunId));
156
+ await load();
157
+ });
158
+ }