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.
- package/dist/agents/adapters/mcp/stdio-buffer.d.ts +6 -0
- package/dist/agents/adapters/mcp/stdio-buffer.js +45 -0
- package/dist/agents/adapters/mcp.d.ts +2 -0
- package/dist/agents/adapters/mcp.js +60 -7
- package/dist/cli/install-wizard.js +19 -0
- package/dist/cli.js +31 -1
- package/dist/dashboard/app/index.html +8 -0
- package/dist/dashboard/app/main.js +7 -0
- package/dist/dashboard/app/state.js +5 -0
- package/dist/dashboard/app/styles/board.css +163 -2
- package/dist/dashboard/app/styles/context-log.css +167 -0
- package/dist/dashboard/app/styles/memory.css +63 -0
- package/dist/dashboard/app/styles/workforce.css +21 -0
- package/dist/dashboard/app/views/board.js +145 -7
- package/dist/dashboard/app/views/context-log.js +158 -0
- package/dist/dashboard/app/views/memory.js +87 -3
- package/dist/dashboard/app/views/workforce.js +22 -6
- package/dist/dashboard/routes/events.js +80 -3
- package/dist/dashboard/stream/sse-broker.js +25 -13
- package/dist/engines/client-bootstrap.js +66 -20
- package/dist/engines/code-review-graph-client.d.ts +11 -3
- package/dist/engines/code-review-graph-client.js +151 -24
- package/dist/engines/instruction-gateway.js +6 -0
- package/dist/engines/mcp-entrypoint.js +3 -1
- package/dist/engines/orchestrator/decision-spine.d.ts +170 -0
- package/dist/engines/orchestrator/decision-spine.js +424 -0
- package/dist/engines/orchestrator/selection-policy.d.ts +39 -0
- package/dist/engines/orchestrator/selection-policy.js +32 -0
- package/dist/engines/orchestrator.js +19 -33
- package/dist/phantom/runtime.d.ts +16 -0
- package/dist/phantom/runtime.js +158 -20
- 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
|
-
|
|
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=>{
|
|
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
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
+
}
|