@yemi33/squad 0.1.12 → 0.1.13
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/dashboard.html +226 -1
- package/dashboard.js +47 -0
- package/engine.js +145 -6
- package/package.json +1 -1
package/dashboard.html
CHANGED
|
@@ -47,6 +47,34 @@
|
|
|
47
47
|
.status-badge.working { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
|
|
48
48
|
.status-badge.done { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
|
|
49
49
|
.agent-action { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
50
|
+
.token-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; margin-bottom: 12px; }
|
|
51
|
+
.token-tile { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }
|
|
52
|
+
.token-tile-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
53
|
+
.token-tile-value { font-size: 20px; font-weight: 700; color: var(--text); margin-top: 2px; }
|
|
54
|
+
.token-tile-sub { font-size: 10px; color: var(--muted); margin-top: 2px; }
|
|
55
|
+
.token-chart { display: flex; align-items: flex-end; gap: 3px; height: 80px; margin: 8px 0; }
|
|
56
|
+
.token-bar { flex: 1; min-width: 8px; max-width: 24px; background: var(--blue); border-radius: 2px 2px 0 0; position: relative; cursor: default; transition: background 0.15s; }
|
|
57
|
+
.token-bar:hover { background: var(--green); }
|
|
58
|
+
.token-bar-tip { display: none; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: var(--surface); border: 1px solid var(--border); border-radius: 4px; padding: 3px 6px; font-size: 9px; white-space: nowrap; z-index: 10; color: var(--text); }
|
|
59
|
+
.token-bar:hover .token-bar-tip { display: block; }
|
|
60
|
+
.token-chart-labels { display: flex; gap: 3px; }
|
|
61
|
+
.token-chart-labels span { flex: 1; min-width: 8px; max-width: 24px; font-size: 8px; color: var(--muted); text-align: center; overflow: hidden; }
|
|
62
|
+
.token-agent-table { width: 100%; margin-top: 10px; }
|
|
63
|
+
.token-agent-table th { text-align: left; font-size: 10px; color: var(--muted); font-weight: 500; padding: 4px 8px; border-bottom: 1px solid var(--border); }
|
|
64
|
+
.token-agent-table td { font-size: 11px; padding: 4px 8px; }
|
|
65
|
+
.kb-tabs { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
66
|
+
.kb-tab { background: var(--surface2); border: 1px solid var(--border); color: var(--muted); font-size: 11px; cursor: pointer; padding: 3px 10px; border-radius: 4px; transition: all 0.2s; }
|
|
67
|
+
.kb-tab:hover { color: var(--text); border-color: var(--text); }
|
|
68
|
+
.kb-tab.active { color: var(--blue); border-color: var(--blue); background: rgba(88,166,255,0.08); }
|
|
69
|
+
.kb-tab .badge { background: var(--border); color: var(--text); font-size: 9px; padding: 0 5px; border-radius: 8px; margin-left: 4px; }
|
|
70
|
+
.kb-list { max-height: 400px; overflow-y: auto; }
|
|
71
|
+
.kb-item { display: flex; align-items: flex-start; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.1s; }
|
|
72
|
+
.kb-item:hover { background: var(--surface2); }
|
|
73
|
+
.kb-item:last-child { border-bottom: none; }
|
|
74
|
+
.kb-item-body { flex: 1; min-width: 0; }
|
|
75
|
+
.kb-item-title { font-size: 12px; color: var(--text); font-weight: 500; }
|
|
76
|
+
.kb-item-meta { font-size: 10px; color: var(--muted); margin-top: 2px; display: flex; gap: 8px; }
|
|
77
|
+
.kb-item-preview { font-size: 10px; color: var(--muted); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
50
78
|
.agent-result { font-size: 10px; color: var(--text); background: var(--surface2); padding: 6px 8px; border-radius: 4px; margin-top: 6px; white-space: pre-wrap; word-break: break-word; max-height: 80px; overflow-y: auto; line-height: 1.4; border-left: 2px solid var(--blue); }
|
|
51
79
|
.agent-card { min-width: 0; }
|
|
52
80
|
.agent-emoji { font-size: 20px; margin-right: 4px; }
|
|
@@ -463,6 +491,12 @@
|
|
|
463
491
|
<div id="notes-list">Loading...</div>
|
|
464
492
|
</section>
|
|
465
493
|
|
|
494
|
+
<section>
|
|
495
|
+
<h2>Knowledge Base <span class="count" id="kb-count">0</span></h2>
|
|
496
|
+
<div class="kb-tabs" id="kb-tabs"></div>
|
|
497
|
+
<div class="kb-list" id="kb-list"><p class="empty">No knowledge entries yet. Notes are classified here after consolidation.</p></div>
|
|
498
|
+
</section>
|
|
499
|
+
|
|
466
500
|
<section>
|
|
467
501
|
<h2>Squad Skills <span class="count" id="skills-count">0</span></h2>
|
|
468
502
|
<div id="skills-list"><p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p></div>
|
|
@@ -485,6 +519,11 @@
|
|
|
485
519
|
<div id="metrics-content"><p class="empty">No metrics yet. Metrics appear after agents complete tasks.</p></div>
|
|
486
520
|
</section>
|
|
487
521
|
|
|
522
|
+
<section>
|
|
523
|
+
<h2>Token Usage</h2>
|
|
524
|
+
<div id="token-usage-content"><p class="empty">No usage data yet.</p></div>
|
|
525
|
+
</section>
|
|
526
|
+
|
|
488
527
|
<section class="pr-panel" id="completed-section">
|
|
489
528
|
<h2>Recent Completions <span class="count" id="completed-count">0</span></h2>
|
|
490
529
|
<div id="completed-content"><p class="empty">No completed dispatches yet.</p></div>
|
|
@@ -1134,6 +1173,9 @@ async function refresh() {
|
|
|
1134
1173
|
renderMetrics(data.metrics || {});
|
|
1135
1174
|
renderWorkItems(data.workItems || []);
|
|
1136
1175
|
renderSkills(data.skills || []);
|
|
1176
|
+
// Refresh KB less frequently (every 3rd cycle = ~12s)
|
|
1177
|
+
if (!window._kbRefreshCount) window._kbRefreshCount = 0;
|
|
1178
|
+
if (window._kbRefreshCount++ % 3 === 0) refreshKnowledgeBase();
|
|
1137
1179
|
} catch(e) { console.error('refresh error', e); }
|
|
1138
1180
|
}
|
|
1139
1181
|
|
|
@@ -1357,9 +1399,10 @@ function openAllWorkItems() {
|
|
|
1357
1399
|
// -- Metrics --
|
|
1358
1400
|
function renderMetrics(metrics) {
|
|
1359
1401
|
const el = document.getElementById('metrics-content');
|
|
1360
|
-
const agents = Object.entries(metrics);
|
|
1402
|
+
const agents = Object.entries(metrics).filter(([k]) => !k.startsWith('_'));
|
|
1361
1403
|
if (agents.length === 0) {
|
|
1362
1404
|
el.innerHTML = '<p class="empty">No metrics yet. Metrics appear after agents complete tasks.</p>';
|
|
1405
|
+
renderTokenUsage(metrics);
|
|
1363
1406
|
return;
|
|
1364
1407
|
}
|
|
1365
1408
|
let html = '<table class="pr-table"><thead><tr><th>Agent</th><th>Done</th><th>Errors</th><th>PRs</th><th>Approved</th><th>Rejected</th><th>Rate</th><th>Reviews</th></tr></thead><tbody>';
|
|
@@ -1379,6 +1422,85 @@ function renderMetrics(metrics) {
|
|
|
1379
1422
|
}
|
|
1380
1423
|
html += '</tbody></table>';
|
|
1381
1424
|
el.innerHTML = html;
|
|
1425
|
+
renderTokenUsage(metrics);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function renderTokenUsage(metrics) {
|
|
1429
|
+
const el = document.getElementById('token-usage-content');
|
|
1430
|
+
const agents = Object.entries(metrics).filter(([k]) => !k.startsWith('_'));
|
|
1431
|
+
const daily = metrics._daily || {};
|
|
1432
|
+
|
|
1433
|
+
// Aggregate totals
|
|
1434
|
+
let totalCost = 0, totalInput = 0, totalOutput = 0, totalCache = 0;
|
|
1435
|
+
for (const [, m] of agents) {
|
|
1436
|
+
totalCost += m.totalCostUsd || 0;
|
|
1437
|
+
totalInput += m.totalInputTokens || 0;
|
|
1438
|
+
totalOutput += m.totalOutputTokens || 0;
|
|
1439
|
+
totalCache += m.totalCacheRead || 0;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (totalCost === 0 && Object.keys(daily).length === 0) {
|
|
1443
|
+
el.innerHTML = '<p class="empty">No usage data yet. Token tracking starts on next agent completion.</p>';
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const fmtTokens = (n) => n >= 1000000 ? (n / 1000000).toFixed(1) + 'M' : n >= 1000 ? (n / 1000).toFixed(0) + 'K' : String(n);
|
|
1448
|
+
const fmtCost = (n) => '$' + n.toFixed(2);
|
|
1449
|
+
|
|
1450
|
+
// Summary tiles
|
|
1451
|
+
let html = '<div class="token-tiles">';
|
|
1452
|
+
html += '<div class="token-tile"><div class="token-tile-label">Total Cost</div><div class="token-tile-value">' + fmtCost(totalCost) + '</div></div>';
|
|
1453
|
+
html += '<div class="token-tile"><div class="token-tile-label">Input Tokens</div><div class="token-tile-value">' + fmtTokens(totalInput) + '</div></div>';
|
|
1454
|
+
html += '<div class="token-tile"><div class="token-tile-label">Output Tokens</div><div class="token-tile-value">' + fmtTokens(totalOutput) + '</div></div>';
|
|
1455
|
+
html += '<div class="token-tile"><div class="token-tile-label">Cache Reads</div><div class="token-tile-value">' + fmtTokens(totalCache) + '</div></div>';
|
|
1456
|
+
|
|
1457
|
+
// Today's cost
|
|
1458
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1459
|
+
const todayData = daily[today];
|
|
1460
|
+
if (todayData) {
|
|
1461
|
+
html += '<div class="token-tile"><div class="token-tile-label">Today</div><div class="token-tile-value">' + fmtCost(todayData.costUsd) + '</div><div class="token-tile-sub">' + todayData.tasks + ' tasks</div></div>';
|
|
1462
|
+
}
|
|
1463
|
+
html += '</div>';
|
|
1464
|
+
|
|
1465
|
+
// Daily bar chart (last 14 days)
|
|
1466
|
+
const days = Object.keys(daily).sort().slice(-14);
|
|
1467
|
+
if (days.length > 1) {
|
|
1468
|
+
const maxCost = Math.max(...days.map(d => daily[d].costUsd || 0), 0.01);
|
|
1469
|
+
html += '<div style="font-size:10px;color:var(--muted);margin:8px 0 4px">Daily Cost (last ' + days.length + ' days)</div>';
|
|
1470
|
+
html += '<div class="token-chart">';
|
|
1471
|
+
for (const day of days) {
|
|
1472
|
+
const d = daily[day];
|
|
1473
|
+
const pct = Math.max(((d.costUsd || 0) / maxCost) * 100, 2);
|
|
1474
|
+
html += '<div class="token-bar" style="height:' + pct + '%"><div class="token-bar-tip">' + day.slice(5) + ': ' + fmtCost(d.costUsd) + ' / ' + d.tasks + ' tasks</div></div>';
|
|
1475
|
+
}
|
|
1476
|
+
html += '</div>';
|
|
1477
|
+
html += '<div class="token-chart-labels">';
|
|
1478
|
+
for (const day of days) {
|
|
1479
|
+
html += '<span>' + day.slice(8) + '</span>';
|
|
1480
|
+
}
|
|
1481
|
+
html += '</div>';
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// Per-agent token table
|
|
1485
|
+
const agentsWithUsage = agents.filter(([, m]) => (m.totalCostUsd || 0) > 0);
|
|
1486
|
+
if (agentsWithUsage.length > 0) {
|
|
1487
|
+
html += '<table class="token-agent-table"><thead><tr><th>Agent</th><th>Cost</th><th>Input</th><th>Output</th><th>Cache</th><th>$/task</th></tr></thead><tbody>';
|
|
1488
|
+
for (const [id, m] of agentsWithUsage.sort((a, b) => (b[1].totalCostUsd || 0) - (a[1].totalCostUsd || 0))) {
|
|
1489
|
+
const tasks = (m.tasksCompleted || 0) + (m.tasksErrored || 0);
|
|
1490
|
+
const perTask = tasks > 0 ? fmtCost((m.totalCostUsd || 0) / tasks) : '-';
|
|
1491
|
+
html += '<tr>' +
|
|
1492
|
+
'<td style="font-weight:600">' + escHtml(id) + '</td>' +
|
|
1493
|
+
'<td>' + fmtCost(m.totalCostUsd || 0) + '</td>' +
|
|
1494
|
+
'<td>' + fmtTokens(m.totalInputTokens || 0) + '</td>' +
|
|
1495
|
+
'<td>' + fmtTokens(m.totalOutputTokens || 0) + '</td>' +
|
|
1496
|
+
'<td>' + fmtTokens(m.totalCacheRead || 0) + '</td>' +
|
|
1497
|
+
'<td style="color:var(--muted)">' + perTask + '</td>' +
|
|
1498
|
+
'</tr>';
|
|
1499
|
+
}
|
|
1500
|
+
html += '</tbody></table>';
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
el.innerHTML = html;
|
|
1382
1504
|
}
|
|
1383
1505
|
|
|
1384
1506
|
// -- Command Center (Unified Input) --
|
|
@@ -1846,6 +1968,109 @@ async function cmdSubmitPrd(parsed) {
|
|
|
1846
1968
|
const projLabel = (parsed.projects || []).length > 0 ? ' (' + parsed.projects.join(', ') + ')' : '';
|
|
1847
1969
|
showToast('cmd-toast', 'PRD item ' + (data.id || id) + ' added' + projLabel, true);
|
|
1848
1970
|
}
|
|
1971
|
+
// ─── Knowledge Base ──────────────────────────────────────────────────────────
|
|
1972
|
+
let _kbData = {};
|
|
1973
|
+
let _kbActiveTab = 'all';
|
|
1974
|
+
|
|
1975
|
+
const KB_CAT_LABELS = {
|
|
1976
|
+
architecture: 'Architecture',
|
|
1977
|
+
conventions: 'Conventions',
|
|
1978
|
+
'project-notes': 'Project Notes',
|
|
1979
|
+
'build-reports': 'Build Reports',
|
|
1980
|
+
reviews: 'Reviews',
|
|
1981
|
+
};
|
|
1982
|
+
const KB_CAT_ICONS = {
|
|
1983
|
+
architecture: '\u{1F3D7}',
|
|
1984
|
+
conventions: '\u{1F4CB}',
|
|
1985
|
+
'project-notes': '\u{1F4DD}',
|
|
1986
|
+
'build-reports': '\u{2699}',
|
|
1987
|
+
reviews: '\u{1F50D}',
|
|
1988
|
+
};
|
|
1989
|
+
|
|
1990
|
+
async function refreshKnowledgeBase() {
|
|
1991
|
+
try {
|
|
1992
|
+
_kbData = await fetch('/api/knowledge').then(r => r.json());
|
|
1993
|
+
renderKnowledgeBase();
|
|
1994
|
+
} catch {}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function renderKnowledgeBase() {
|
|
1998
|
+
const tabsEl = document.getElementById('kb-tabs');
|
|
1999
|
+
const listEl = document.getElementById('kb-list');
|
|
2000
|
+
const countEl = document.getElementById('kb-count');
|
|
2001
|
+
|
|
2002
|
+
// Count total
|
|
2003
|
+
let total = 0;
|
|
2004
|
+
for (const items of Object.values(_kbData)) total += items.length;
|
|
2005
|
+
countEl.textContent = total;
|
|
2006
|
+
|
|
2007
|
+
if (total === 0) {
|
|
2008
|
+
tabsEl.innerHTML = '';
|
|
2009
|
+
listEl.innerHTML = '<p class="empty">No knowledge entries yet. Notes are classified here after consolidation.</p>';
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Render tabs
|
|
2014
|
+
let tabsHtml = '<button class="kb-tab ' + (_kbActiveTab === 'all' ? 'active' : '') + '" onclick="kbSetTab(\'all\')">All <span class="badge">' + total + '</span></button>';
|
|
2015
|
+
for (const [cat, items] of Object.entries(_kbData)) {
|
|
2016
|
+
if (items.length === 0) continue;
|
|
2017
|
+
const label = KB_CAT_LABELS[cat] || cat;
|
|
2018
|
+
tabsHtml += '<button class="kb-tab ' + (_kbActiveTab === cat ? 'active' : '') + '" onclick="kbSetTab(\'' + cat + '\')">' + label + ' <span class="badge">' + items.length + '</span></button>';
|
|
2019
|
+
}
|
|
2020
|
+
tabsEl.innerHTML = tabsHtml;
|
|
2021
|
+
|
|
2022
|
+
// Collect items for active tab
|
|
2023
|
+
let items = [];
|
|
2024
|
+
if (_kbActiveTab === 'all') {
|
|
2025
|
+
for (const [cat, catItems] of Object.entries(_kbData)) {
|
|
2026
|
+
for (const item of catItems) items.push({ ...item, category: cat });
|
|
2027
|
+
}
|
|
2028
|
+
items.sort((a, b) => b.date.localeCompare(a.date));
|
|
2029
|
+
} else {
|
|
2030
|
+
items = (_kbData[_kbActiveTab] || []).map(i => ({ ...i, category: _kbActiveTab }));
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
if (items.length === 0) {
|
|
2034
|
+
listEl.innerHTML = '<p class="empty">No entries in this category.</p>';
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
listEl.innerHTML = items.slice(0, 50).map(item => {
|
|
2039
|
+
const icon = KB_CAT_ICONS[item.category] || '\u{1F4C4}';
|
|
2040
|
+
const label = KB_CAT_LABELS[item.category] || item.category;
|
|
2041
|
+
return '<div class="kb-item" onclick="kbOpenItem(\'' + escHtml(item.category) + '\', \'' + escHtml(item.file) + '\')">' +
|
|
2042
|
+
'<div class="kb-item-body">' +
|
|
2043
|
+
'<div class="kb-item-title">' + icon + ' ' + escHtml(item.title) + '</div>' +
|
|
2044
|
+
'<div class="kb-item-meta">' +
|
|
2045
|
+
'<span>' + label + '</span>' +
|
|
2046
|
+
(item.agent ? '<span>' + item.agent + '</span>' : '') +
|
|
2047
|
+
'<span>' + (item.date || '') + '</span>' +
|
|
2048
|
+
'<span>' + Math.round(item.size / 1024) + 'KB</span>' +
|
|
2049
|
+
'</div>' +
|
|
2050
|
+
(item.preview ? '<div class="kb-item-preview">' + escHtml(item.preview) + '</div>' : '') +
|
|
2051
|
+
'</div>' +
|
|
2052
|
+
'</div>';
|
|
2053
|
+
}).join('');
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function kbSetTab(tab) {
|
|
2057
|
+
_kbActiveTab = tab;
|
|
2058
|
+
renderKnowledgeBase();
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
async function kbOpenItem(category, file) {
|
|
2062
|
+
try {
|
|
2063
|
+
const content = await fetch('/api/knowledge/' + category + '/' + encodeURIComponent(file)).then(r => r.text());
|
|
2064
|
+
// Strip frontmatter for display
|
|
2065
|
+
const display = content.replace(/^---[\s\S]*?---\n*/m, '');
|
|
2066
|
+
document.getElementById('modal-title').textContent = file;
|
|
2067
|
+
document.getElementById('modal-body').textContent = display;
|
|
2068
|
+
document.getElementById('modal').classList.add('open');
|
|
2069
|
+
} catch (e) {
|
|
2070
|
+
console.error('Failed to load KB item:', e);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
1849
2074
|
// ─── Command History ──────────────────────────────────────────────────────────
|
|
1850
2075
|
const CMD_HISTORY_KEY = 'squad-cmd-history';
|
|
1851
2076
|
const CMD_HISTORY_MAX = 50;
|
package/dashboard.js
CHANGED
|
@@ -811,6 +811,53 @@ const server = http.createServer(async (req, res) => {
|
|
|
811
811
|
return;
|
|
812
812
|
}
|
|
813
813
|
|
|
814
|
+
// GET /api/knowledge — list all knowledge base entries grouped by category
|
|
815
|
+
if (req.method === 'GET' && req.url === '/api/knowledge') {
|
|
816
|
+
const kbDir = path.join(SQUAD_DIR, 'knowledge');
|
|
817
|
+
const categories = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
|
|
818
|
+
const result = {};
|
|
819
|
+
for (const cat of categories) {
|
|
820
|
+
const catDir = path.join(kbDir, cat);
|
|
821
|
+
const files = safeReadDir(catDir).filter(f => f.endsWith('.md')).sort().reverse();
|
|
822
|
+
result[cat] = files.map(f => {
|
|
823
|
+
const content = safeRead(path.join(catDir, f)) || '';
|
|
824
|
+
// Extract title from first heading
|
|
825
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
826
|
+
const title = titleMatch ? titleMatch[1] : f.replace(/\.md$/, '');
|
|
827
|
+
// Extract agent and date from frontmatter
|
|
828
|
+
const agentMatch = content.match(/^agent:\s*(.+)/m);
|
|
829
|
+
const dateMatch = content.match(/^date:\s*(.+)/m);
|
|
830
|
+
return {
|
|
831
|
+
file: f,
|
|
832
|
+
category: cat,
|
|
833
|
+
title,
|
|
834
|
+
agent: agentMatch ? agentMatch[1].trim() : '',
|
|
835
|
+
date: dateMatch ? dateMatch[1].trim() : '',
|
|
836
|
+
size: content.length,
|
|
837
|
+
preview: content.replace(/^---[\s\S]*?---\n*/m, '').split('\n').filter(l => l.trim() && !l.startsWith('#')).slice(0, 3).join(' ').slice(0, 200),
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
return jsonReply(res, 200, result);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// GET /api/knowledge/:category/:file — read a specific knowledge base entry
|
|
845
|
+
const kbMatch = req.url.match(/^\/api\/knowledge\/([^/]+)\/([^?]+)/);
|
|
846
|
+
if (kbMatch && req.method === 'GET') {
|
|
847
|
+
const cat = kbMatch[1];
|
|
848
|
+
const file = decodeURIComponent(kbMatch[2]);
|
|
849
|
+
// Prevent path traversal
|
|
850
|
+
if (file.includes('..') || file.includes('/') || file.includes('\\')) {
|
|
851
|
+
return jsonReply(res, 400, { error: 'invalid file name' });
|
|
852
|
+
}
|
|
853
|
+
const content = safeRead(path.join(SQUAD_DIR, 'knowledge', cat, file));
|
|
854
|
+
if (content === null) return jsonReply(res, 404, { error: 'not found' });
|
|
855
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
856
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
857
|
+
res.end(content);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
814
861
|
// POST /api/inbox/persist — promote an inbox item to team notes
|
|
815
862
|
if (req.method === 'POST' && req.url === '/api/inbox/persist') {
|
|
816
863
|
try {
|
package/engine.js
CHANGED
|
@@ -38,6 +38,7 @@ const CONTROL_PATH = path.join(ENGINE_DIR, 'control.json');
|
|
|
38
38
|
const DISPATCH_PATH = path.join(ENGINE_DIR, 'dispatch.json');
|
|
39
39
|
const LOG_PATH = path.join(ENGINE_DIR, 'log.json');
|
|
40
40
|
const INBOX_DIR = path.join(SQUAD_DIR, 'notes', 'inbox');
|
|
41
|
+
const KNOWLEDGE_DIR = path.join(SQUAD_DIR, 'knowledge');
|
|
41
42
|
const ARCHIVE_DIR = path.join(SQUAD_DIR, 'notes', 'archive');
|
|
42
43
|
const PLANS_DIR = path.join(SQUAD_DIR, 'plans');
|
|
43
44
|
const IDENTITY_DIR = path.join(SQUAD_DIR, 'identity');
|
|
@@ -738,8 +739,9 @@ function spawnAgent(dispatchItem, config) {
|
|
|
738
739
|
safeWrite(archivePath, outputContent);
|
|
739
740
|
safeWrite(latestPath, outputContent); // overwrite latest for dashboard compat
|
|
740
741
|
|
|
741
|
-
// Extract agent's final result text from stream-json output
|
|
742
|
+
// Extract agent's final result text + token usage from stream-json output
|
|
742
743
|
let resultSummary = '';
|
|
744
|
+
let taskUsage = null;
|
|
743
745
|
try {
|
|
744
746
|
const lines = stdout.split('\n');
|
|
745
747
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -747,8 +749,19 @@ function spawnAgent(dispatchItem, config) {
|
|
|
747
749
|
if (!line || !line.startsWith('{')) continue;
|
|
748
750
|
try {
|
|
749
751
|
const obj = JSON.parse(line);
|
|
750
|
-
if (obj.type === 'result'
|
|
751
|
-
resultSummary = obj.result.slice(0, 500);
|
|
752
|
+
if (obj.type === 'result') {
|
|
753
|
+
if (obj.result) resultSummary = obj.result.slice(0, 500);
|
|
754
|
+
if (obj.total_cost_usd || obj.usage) {
|
|
755
|
+
taskUsage = {
|
|
756
|
+
costUsd: obj.total_cost_usd || 0,
|
|
757
|
+
inputTokens: obj.usage?.input_tokens || 0,
|
|
758
|
+
outputTokens: obj.usage?.output_tokens || 0,
|
|
759
|
+
cacheRead: obj.usage?.cache_read_input_tokens || obj.usage?.cacheReadInputTokens || 0,
|
|
760
|
+
cacheCreation: obj.usage?.cache_creation_input_tokens || obj.usage?.cacheCreationInputTokens || 0,
|
|
761
|
+
durationMs: obj.duration_ms || 0,
|
|
762
|
+
numTurns: obj.num_turns || 0,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
752
765
|
break;
|
|
753
766
|
}
|
|
754
767
|
} catch {}
|
|
@@ -807,7 +820,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
807
820
|
updateAgentHistory(agentId, dispatchItem, code === 0 ? 'success' : 'error');
|
|
808
821
|
|
|
809
822
|
// Update quality metrics
|
|
810
|
-
updateMetrics(agentId, dispatchItem, code === 0 ? 'success' : 'error');
|
|
823
|
+
updateMetrics(agentId, dispatchItem, code === 0 ? 'success' : 'error', taskUsage);
|
|
811
824
|
|
|
812
825
|
// Cleanup temp files
|
|
813
826
|
try { fs.unlinkSync(sysPromptPath); } catch {}
|
|
@@ -1992,7 +2005,7 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
|
|
|
1992
2005
|
log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
|
|
1993
2006
|
}
|
|
1994
2007
|
|
|
1995
|
-
function updateMetrics(agentId, dispatchItem, result) {
|
|
2008
|
+
function updateMetrics(agentId, dispatchItem, result, taskUsage) {
|
|
1996
2009
|
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
1997
2010
|
const metrics = safeJson(metricsPath) || {};
|
|
1998
2011
|
|
|
@@ -2005,7 +2018,11 @@ function updateMetrics(agentId, dispatchItem, result) {
|
|
|
2005
2018
|
prsRejected: 0,
|
|
2006
2019
|
reviewsDone: 0,
|
|
2007
2020
|
lastTask: null,
|
|
2008
|
-
lastCompleted: null
|
|
2021
|
+
lastCompleted: null,
|
|
2022
|
+
totalCostUsd: 0,
|
|
2023
|
+
totalInputTokens: 0,
|
|
2024
|
+
totalOutputTokens: 0,
|
|
2025
|
+
totalCacheRead: 0,
|
|
2009
2026
|
};
|
|
2010
2027
|
}
|
|
2011
2028
|
|
|
@@ -2021,6 +2038,35 @@ function updateMetrics(agentId, dispatchItem, result) {
|
|
|
2021
2038
|
m.tasksErrored++;
|
|
2022
2039
|
}
|
|
2023
2040
|
|
|
2041
|
+
// Track token usage per agent
|
|
2042
|
+
if (taskUsage) {
|
|
2043
|
+
m.totalCostUsd = (m.totalCostUsd || 0) + (taskUsage.costUsd || 0);
|
|
2044
|
+
m.totalInputTokens = (m.totalInputTokens || 0) + (taskUsage.inputTokens || 0);
|
|
2045
|
+
m.totalOutputTokens = (m.totalOutputTokens || 0) + (taskUsage.outputTokens || 0);
|
|
2046
|
+
m.totalCacheRead = (m.totalCacheRead || 0) + (taskUsage.cacheRead || 0);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// Track daily usage (all agents combined)
|
|
2050
|
+
const today = dateStamp();
|
|
2051
|
+
if (!metrics._daily) metrics._daily = {};
|
|
2052
|
+
if (!metrics._daily[today]) metrics._daily[today] = { costUsd: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, tasks: 0 };
|
|
2053
|
+
const daily = metrics._daily[today];
|
|
2054
|
+
daily.tasks++;
|
|
2055
|
+
if (taskUsage) {
|
|
2056
|
+
daily.costUsd += taskUsage.costUsd || 0;
|
|
2057
|
+
daily.inputTokens += taskUsage.inputTokens || 0;
|
|
2058
|
+
daily.outputTokens += taskUsage.outputTokens || 0;
|
|
2059
|
+
daily.cacheRead += taskUsage.cacheRead || 0;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// Prune daily entries older than 30 days
|
|
2063
|
+
const cutoff = new Date();
|
|
2064
|
+
cutoff.setDate(cutoff.getDate() - 30);
|
|
2065
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
2066
|
+
for (const day of Object.keys(metrics._daily)) {
|
|
2067
|
+
if (day < cutoffStr) delete metrics._daily[day];
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2024
2070
|
safeWrite(metricsPath, metrics);
|
|
2025
2071
|
}
|
|
2026
2072
|
|
|
@@ -2053,6 +2099,25 @@ function consolidateInbox(config) {
|
|
|
2053
2099
|
function consolidateWithLLM(items, existingNotes, files, config) {
|
|
2054
2100
|
_consolidationInFlight = true;
|
|
2055
2101
|
|
|
2102
|
+
// Pre-classify items to generate KB paths for Haiku to reference
|
|
2103
|
+
const kbPaths = items.map(item => {
|
|
2104
|
+
const content = item.content || '';
|
|
2105
|
+
const name = (item.name || '').toLowerCase();
|
|
2106
|
+
const contentLower = content.toLowerCase();
|
|
2107
|
+
let cat = 'project-notes';
|
|
2108
|
+
if (name.includes('review') || name.includes('pr-') || name.includes('pr4') || name.includes('feedback')) cat = 'reviews';
|
|
2109
|
+
else if (name.includes('build') || name.includes('bt-') || contentLower.includes('build pass') || contentLower.includes('build fail') || contentLower.includes('lint')) cat = 'build-reports';
|
|
2110
|
+
else if (contentLower.includes('architecture') || contentLower.includes('design doc') || contentLower.includes('system design')) cat = 'architecture';
|
|
2111
|
+
else if (contentLower.includes('convention') || contentLower.includes('pattern') || contentLower.includes('always use') || contentLower.includes('best practice')) cat = 'conventions';
|
|
2112
|
+
const agentMatch = item.name.match(/^(\w+)-/);
|
|
2113
|
+
const agent = agentMatch ? agentMatch[1] : 'unknown';
|
|
2114
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
2115
|
+
const titleSlug = titleMatch ? titleMatch[1].toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50) : item.name.replace(/\.md$/, '');
|
|
2116
|
+
return { file: item.name, category: cat, kbPath: `knowledge/${cat}/${dateStamp()}-${agent}-${titleSlug}.md` };
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
const kbRefBlock = kbPaths.map(p => `- \`${p.file}\` → \`${p.kbPath}\``).join('\n');
|
|
2120
|
+
|
|
2056
2121
|
// Build the prompt with all inbox notes
|
|
2057
2122
|
const notesBlock = items.map(item =>
|
|
2058
2123
|
`<note file="${item.name}">\n${(item.content || '').slice(0, 8000)}\n</note>`
|
|
@@ -2097,6 +2162,10 @@ Read every inbox note carefully. Produce a consolidated digest following these r
|
|
|
2097
2162
|
|
|
2098
2163
|
6. **Write a descriptive title**: First line must be a single-line title summarizing what was learned. Do NOT use generic text like "Consolidated from N items".
|
|
2099
2164
|
|
|
2165
|
+
7. **Reference the knowledge base**: Each note is being filed into the knowledge base at these paths. After each insight bullet, add a reference link so readers know where to find the full detail:
|
|
2166
|
+
${kbRefBlock}
|
|
2167
|
+
Format: \`→ see knowledge/category/filename.md\` on a new line after the insight, indented.
|
|
2168
|
+
|
|
2100
2169
|
## Output Format
|
|
2101
2170
|
|
|
2102
2171
|
Respond with ONLY the markdown below — no preamble, no explanation, no code fences:
|
|
@@ -2106,6 +2175,7 @@ Respond with ONLY the markdown below — no preamble, no explanation, no code fe
|
|
|
2106
2175
|
|
|
2107
2176
|
#### Category Name
|
|
2108
2177
|
- **Bold key**: insight text _(agent)_
|
|
2178
|
+
→ see \`knowledge/category/filename.md\`
|
|
2109
2179
|
|
|
2110
2180
|
_Processed N notes, M insights extracted, K duplicates removed._
|
|
2111
2181
|
|
|
@@ -2194,6 +2264,7 @@ Use today's date: ${dateStamp()}`;
|
|
|
2194
2264
|
}
|
|
2195
2265
|
|
|
2196
2266
|
safeWrite(NOTES_PATH, newContent);
|
|
2267
|
+
classifyToKnowledgeBase(items);
|
|
2197
2268
|
archiveInboxFiles(files);
|
|
2198
2269
|
log('info', `LLM consolidation complete: ${files.length} notes processed by Haiku`);
|
|
2199
2270
|
} else {
|
|
@@ -2313,10 +2384,78 @@ function consolidateWithRegex(items, files) {
|
|
|
2313
2384
|
if (sections.length > 10) { newContent = sections[0] + '\n---\n\n### ' + sections.slice(-8).join('\n---\n\n### '); }
|
|
2314
2385
|
}
|
|
2315
2386
|
safeWrite(NOTES_PATH, newContent);
|
|
2387
|
+
classifyToKnowledgeBase(items);
|
|
2316
2388
|
archiveInboxFiles(files);
|
|
2317
2389
|
log('info', `Regex fallback: consolidated ${files.length} notes → ${deduped.length} insights into notes.md`);
|
|
2318
2390
|
}
|
|
2319
2391
|
|
|
2392
|
+
// ─── Knowledge Base Classification ───────────────────────────────────────────
|
|
2393
|
+
// Classifies each inbox note into a knowledge/ subdirectory based on content.
|
|
2394
|
+
// Full original content is preserved (not summarized) for deep reference.
|
|
2395
|
+
function classifyToKnowledgeBase(items) {
|
|
2396
|
+
if (!fs.existsSync(KNOWLEDGE_DIR)) fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
|
|
2397
|
+
|
|
2398
|
+
const categoryDirs = {
|
|
2399
|
+
architecture: path.join(KNOWLEDGE_DIR, 'architecture'),
|
|
2400
|
+
conventions: path.join(KNOWLEDGE_DIR, 'conventions'),
|
|
2401
|
+
'project-notes': path.join(KNOWLEDGE_DIR, 'project-notes'),
|
|
2402
|
+
'build-reports': path.join(KNOWLEDGE_DIR, 'build-reports'),
|
|
2403
|
+
reviews: path.join(KNOWLEDGE_DIR, 'reviews'),
|
|
2404
|
+
};
|
|
2405
|
+
for (const dir of Object.values(categoryDirs)) {
|
|
2406
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
let classified = 0;
|
|
2410
|
+
for (const item of items) {
|
|
2411
|
+
const content = item.content || '';
|
|
2412
|
+
const name = (item.name || '').toLowerCase();
|
|
2413
|
+
const contentLower = content.toLowerCase();
|
|
2414
|
+
|
|
2415
|
+
// Classify by filename patterns + content keywords
|
|
2416
|
+
let category = 'project-notes'; // default
|
|
2417
|
+
if (name.includes('review') || name.includes('pr-') || name.includes('pr4') || name.includes('feedback')) {
|
|
2418
|
+
category = 'reviews';
|
|
2419
|
+
} else if (name.includes('build') || name.includes('bt-') || contentLower.includes('build pass') || contentLower.includes('build fail') || contentLower.includes('lint')) {
|
|
2420
|
+
category = 'build-reports';
|
|
2421
|
+
} else if (contentLower.includes('architecture') || contentLower.includes('design doc') || contentLower.includes('system design') || contentLower.includes('data flow') || contentLower.includes('how it works')) {
|
|
2422
|
+
category = 'architecture';
|
|
2423
|
+
} else if (contentLower.includes('convention') || contentLower.includes('pattern') || contentLower.includes('always use') || contentLower.includes('never use') || contentLower.includes('rule:') || contentLower.includes('best practice')) {
|
|
2424
|
+
category = 'conventions';
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// Write to knowledge base with clean filename
|
|
2428
|
+
const agentMatch = item.name.match(/^(\w+)-/);
|
|
2429
|
+
const agent = agentMatch ? agentMatch[1] : 'unknown';
|
|
2430
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
2431
|
+
const titleSlug = titleMatch
|
|
2432
|
+
? titleMatch[1].toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50)
|
|
2433
|
+
: item.name.replace(/\.md$/, '');
|
|
2434
|
+
const kbFilename = `${dateStamp()}-${agent}-${titleSlug}.md`;
|
|
2435
|
+
const kbPath = path.join(categoryDirs[category], kbFilename);
|
|
2436
|
+
|
|
2437
|
+
// Add frontmatter with metadata
|
|
2438
|
+
const frontmatter = `---
|
|
2439
|
+
source: ${item.name}
|
|
2440
|
+
agent: ${agent}
|
|
2441
|
+
category: ${category}
|
|
2442
|
+
date: ${dateStamp()}
|
|
2443
|
+
---
|
|
2444
|
+
|
|
2445
|
+
`;
|
|
2446
|
+
try {
|
|
2447
|
+
safeWrite(kbPath, frontmatter + content);
|
|
2448
|
+
classified++;
|
|
2449
|
+
} catch (e) {
|
|
2450
|
+
log('warn', `Failed to classify ${item.name} to knowledge base: ${e.message}`);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
if (classified > 0) {
|
|
2455
|
+
log('info', `Knowledge base: classified ${classified} note(s) into knowledge/`);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2320
2459
|
function archiveInboxFiles(files) {
|
|
2321
2460
|
if (!fs.existsSync(ARCHIVE_DIR)) fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
2322
2461
|
for (const f of files) {
|
package/package.json
CHANGED