@yemi33/squad 0.1.11 → 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 CHANGED
@@ -47,6 +47,35 @@
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%; }
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); }
50
79
  .agent-card { min-width: 0; }
51
80
  .agent-emoji { font-size: 20px; margin-right: 4px; }
52
81
  .click-hint { font-size: 10px; color: var(--border); margin-top: 6px; }
@@ -462,6 +491,12 @@
462
491
  <div id="notes-list">Loading...</div>
463
492
  </section>
464
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
+
465
500
  <section>
466
501
  <h2>Squad Skills <span class="count" id="skills-count">0</span></h2>
467
502
  <div id="skills-list"><p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p></div>
@@ -484,6 +519,11 @@
484
519
  <div id="metrics-content"><p class="empty">No metrics yet. Metrics appear after agents complete tasks.</p></div>
485
520
  </section>
486
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
+
487
527
  <section class="pr-panel" id="completed-section">
488
528
  <h2>Recent Completions <span class="count" id="completed-count">0</span></h2>
489
529
  <div id="completed-content"><p class="empty">No completed dispatches yet.</p></div>
@@ -671,6 +711,7 @@ function renderAgents(agents) {
671
711
  </div>
672
712
  <div class="agent-role">${a.role}</div>
673
713
  <div class="agent-action" title="${escHtml(a.lastAction)}">${escHtml(a.lastAction)}</div>
714
+ ${a.resultSummary ? `<div class="agent-result" title="${escHtml(a.resultSummary)}">${escHtml(a.resultSummary.slice(0, 200))}${a.resultSummary.length > 200 ? '...' : ''}</div>` : ''}
674
715
  </div>
675
716
  `).join('');
676
717
  }
@@ -1132,6 +1173,9 @@ async function refresh() {
1132
1173
  renderMetrics(data.metrics || {});
1133
1174
  renderWorkItems(data.workItems || []);
1134
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();
1135
1179
  } catch(e) { console.error('refresh error', e); }
1136
1180
  }
1137
1181
 
@@ -1355,9 +1399,10 @@ function openAllWorkItems() {
1355
1399
  // -- Metrics --
1356
1400
  function renderMetrics(metrics) {
1357
1401
  const el = document.getElementById('metrics-content');
1358
- const agents = Object.entries(metrics);
1402
+ const agents = Object.entries(metrics).filter(([k]) => !k.startsWith('_'));
1359
1403
  if (agents.length === 0) {
1360
1404
  el.innerHTML = '<p class="empty">No metrics yet. Metrics appear after agents complete tasks.</p>';
1405
+ renderTokenUsage(metrics);
1361
1406
  return;
1362
1407
  }
1363
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>';
@@ -1377,6 +1422,85 @@ function renderMetrics(metrics) {
1377
1422
  }
1378
1423
  html += '</tbody></table>';
1379
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;
1380
1504
  }
1381
1505
 
1382
1506
  // -- Command Center (Unified Input) --
@@ -1844,6 +1968,109 @@ async function cmdSubmitPrd(parsed) {
1844
1968
  const projLabel = (parsed.projects || []).length > 0 ? ' (' + parsed.projects.join(', ') + ')' : '';
1845
1969
  showToast('cmd-toast', 'PRD item ' + (data.id || id) + ' added' + projLabel, true);
1846
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
+
1847
2074
  // ─── Command History ──────────────────────────────────────────────────────────
1848
2075
  const CMD_HISTORY_KEY = 'squad-cmd-history';
1849
2076
  const CMD_HISTORY_MAX = 50;
package/dashboard.js CHANGED
@@ -99,6 +99,7 @@ function getAgents() {
99
99
  let status = 'idle';
100
100
  let lastAction = 'Waiting for assignment';
101
101
  let currentTask = '';
102
+ let resultSummary = '';
102
103
 
103
104
  const statusFile = safeRead(path.join(SQUAD_DIR, 'agents', a.id, 'status.json'));
104
105
  if (statusFile) {
@@ -106,6 +107,7 @@ function getAgents() {
106
107
  const sj = JSON.parse(statusFile);
107
108
  status = sj.status || 'idle';
108
109
  currentTask = sj.task || '';
110
+ resultSummary = sj.resultSummary || '';
109
111
  if (sj.status === 'working') {
110
112
  lastAction = `Working: ${sj.task}`;
111
113
  } else if (sj.status === 'done') {
@@ -126,7 +128,7 @@ function getAgents() {
126
128
  const chartered = fs.existsSync(path.join(SQUAD_DIR, 'agents', a.id, 'charter.md'));
127
129
  // Truncate lastAction to prevent UI overflow from corrupted data
128
130
  if (lastAction.length > 120) lastAction = lastAction.slice(0, 120) + '...';
129
- return { ...a, status, lastAction, currentTask: (currentTask || '').slice(0, 200), chartered, inboxCount: inboxFiles.length };
131
+ return { ...a, status, lastAction, currentTask: (currentTask || '').slice(0, 200), resultSummary: (resultSummary || '').slice(0, 500), chartered, inboxCount: inboxFiles.length };
130
132
  });
131
133
  }
132
134
 
@@ -809,6 +811,53 @@ const server = http.createServer(async (req, res) => {
809
811
  return;
810
812
  }
811
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
+
812
861
  // POST /api/inbox/persist — promote an inbox item to team notes
813
862
  if (req.method === 'POST' && req.url === '/api/inbox/persist') {
814
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,6 +739,35 @@ function spawnAgent(dispatchItem, config) {
738
739
  safeWrite(archivePath, outputContent);
739
740
  safeWrite(latestPath, outputContent); // overwrite latest for dashboard compat
740
741
 
742
+ // Extract agent's final result text + token usage from stream-json output
743
+ let resultSummary = '';
744
+ let taskUsage = null;
745
+ try {
746
+ const lines = stdout.split('\n');
747
+ for (let i = lines.length - 1; i >= 0; i--) {
748
+ const line = lines[i].trim();
749
+ if (!line || !line.startsWith('{')) continue;
750
+ try {
751
+ const obj = JSON.parse(line);
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
+ }
765
+ break;
766
+ }
767
+ } catch {}
768
+ }
769
+ } catch {}
770
+
741
771
  // Update agent status
742
772
  setAgentStatus(agentId, {
743
773
  status: code === 0 ? 'done' : 'error',
@@ -747,7 +777,8 @@ function spawnAgent(dispatchItem, config) {
747
777
  branch: branchName,
748
778
  exit_code: code,
749
779
  started_at: startedAt,
750
- completed_at: ts()
780
+ completed_at: ts(),
781
+ resultSummary: resultSummary || undefined,
751
782
  });
752
783
 
753
784
  // Move from active to completed in dispatch
@@ -789,7 +820,7 @@ function spawnAgent(dispatchItem, config) {
789
820
  updateAgentHistory(agentId, dispatchItem, code === 0 ? 'success' : 'error');
790
821
 
791
822
  // Update quality metrics
792
- updateMetrics(agentId, dispatchItem, code === 0 ? 'success' : 'error');
823
+ updateMetrics(agentId, dispatchItem, code === 0 ? 'success' : 'error', taskUsage);
793
824
 
794
825
  // Cleanup temp files
795
826
  try { fs.unlinkSync(sysPromptPath); } catch {}
@@ -1974,7 +2005,7 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
1974
2005
  log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
1975
2006
  }
1976
2007
 
1977
- function updateMetrics(agentId, dispatchItem, result) {
2008
+ function updateMetrics(agentId, dispatchItem, result, taskUsage) {
1978
2009
  const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
1979
2010
  const metrics = safeJson(metricsPath) || {};
1980
2011
 
@@ -1987,7 +2018,11 @@ function updateMetrics(agentId, dispatchItem, result) {
1987
2018
  prsRejected: 0,
1988
2019
  reviewsDone: 0,
1989
2020
  lastTask: null,
1990
- lastCompleted: null
2021
+ lastCompleted: null,
2022
+ totalCostUsd: 0,
2023
+ totalInputTokens: 0,
2024
+ totalOutputTokens: 0,
2025
+ totalCacheRead: 0,
1991
2026
  };
1992
2027
  }
1993
2028
 
@@ -2003,6 +2038,35 @@ function updateMetrics(agentId, dispatchItem, result) {
2003
2038
  m.tasksErrored++;
2004
2039
  }
2005
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
+
2006
2070
  safeWrite(metricsPath, metrics);
2007
2071
  }
2008
2072
 
@@ -2012,7 +2076,7 @@ function updateMetrics(agentId, dispatchItem, result) {
2012
2076
  let _consolidationInFlight = false;
2013
2077
 
2014
2078
  function consolidateInbox(config) {
2015
- const threshold = config.engine?.inboxConsolidateThreshold || 1;
2079
+ const threshold = config.engine?.inboxConsolidateThreshold || 3;
2016
2080
  const files = getInboxFiles();
2017
2081
  if (files.length < threshold) return;
2018
2082
  if (_consolidationInFlight) return;
@@ -2035,6 +2099,25 @@ function consolidateInbox(config) {
2035
2099
  function consolidateWithLLM(items, existingNotes, files, config) {
2036
2100
  _consolidationInFlight = true;
2037
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
+
2038
2121
  // Build the prompt with all inbox notes
2039
2122
  const notesBlock = items.map(item =>
2040
2123
  `<note file="${item.name}">\n${(item.content || '').slice(0, 8000)}\n</note>`
@@ -2079,6 +2162,10 @@ Read every inbox note carefully. Produce a consolidated digest following these r
2079
2162
 
2080
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".
2081
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
+
2082
2169
  ## Output Format
2083
2170
 
2084
2171
  Respond with ONLY the markdown below — no preamble, no explanation, no code fences:
@@ -2088,6 +2175,7 @@ Respond with ONLY the markdown below — no preamble, no explanation, no code fe
2088
2175
 
2089
2176
  #### Category Name
2090
2177
  - **Bold key**: insight text _(agent)_
2178
+ → see \`knowledge/category/filename.md\`
2091
2179
 
2092
2180
  _Processed N notes, M insights extracted, K duplicates removed._
2093
2181
 
@@ -2176,6 +2264,7 @@ Use today's date: ${dateStamp()}`;
2176
2264
  }
2177
2265
 
2178
2266
  safeWrite(NOTES_PATH, newContent);
2267
+ classifyToKnowledgeBase(items);
2179
2268
  archiveInboxFiles(files);
2180
2269
  log('info', `LLM consolidation complete: ${files.length} notes processed by Haiku`);
2181
2270
  } else {
@@ -2295,10 +2384,78 @@ function consolidateWithRegex(items, files) {
2295
2384
  if (sections.length > 10) { newContent = sections[0] + '\n---\n\n### ' + sections.slice(-8).join('\n---\n\n### '); }
2296
2385
  }
2297
2386
  safeWrite(NOTES_PATH, newContent);
2387
+ classifyToKnowledgeBase(items);
2298
2388
  archiveInboxFiles(files);
2299
2389
  log('info', `Regex fallback: consolidated ${files.length} notes → ${deduped.length} insights into notes.md`);
2300
2390
  }
2301
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
+
2302
2459
  function archiveInboxFiles(files) {
2303
2460
  if (!fs.existsSync(ARCHIVE_DIR)) fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
2304
2461
  for (const f of files) {
@@ -3301,6 +3458,9 @@ function discoverFromWorkItems(config, project) {
3301
3458
  item_name: item.title || item.id,
3302
3459
  item_priority: item.priority || 'medium',
3303
3460
  item_description: item.description || '',
3461
+ item_complexity: item.complexity || item.estimated_complexity || 'medium',
3462
+ task_description: item.title + (item.description ? '\n\n' + item.description : ''),
3463
+ task_id: item.id,
3304
3464
  work_type: workType,
3305
3465
  additional_context: item.prompt ? `## Additional Context\n\n${item.prompt}` : '',
3306
3466
  scope_section: `## Scope: Project — ${project?.name || 'default'}\n\nThis task is scoped to a single project.`,
@@ -3315,8 +3475,10 @@ function discoverFromWorkItems(config, project) {
3315
3475
  ado_org: project?.adoOrg || 'Unknown',
3316
3476
  ado_project: project?.adoProject || 'Unknown',
3317
3477
  repo_name: project?.repoName || 'Unknown',
3318
- date: dateStamp()
3478
+ date: dateStamp(),
3479
+ notes_content: '',
3319
3480
  };
3481
+ try { vars.notes_content = fs.readFileSync(path.join(SQUAD_DIR, 'notes.md'), 'utf8'); } catch {}
3320
3482
 
3321
3483
  // Inject ask-specific variables for the ask playbook
3322
3484
  if (workType === 'ask') {
@@ -3661,6 +3823,9 @@ function discoverCentralWorkItems(config) {
3661
3823
  item_name: item.title || item.id,
3662
3824
  item_priority: item.priority || 'medium',
3663
3825
  item_description: item.description || '',
3826
+ item_complexity: item.complexity || item.estimated_complexity || 'medium',
3827
+ task_description: item.title + (item.description ? '\n\n' + item.description : ''),
3828
+ task_id: item.id,
3664
3829
  work_type: workType,
3665
3830
  additional_context: item.prompt ? `## Additional Context\n\n${item.prompt}` : '',
3666
3831
  scope_section: buildProjectContext(projects, null, false, agentName, agentRole),
@@ -3672,8 +3837,10 @@ function discoverCentralWorkItems(config) {
3672
3837
  ado_project: firstProject?.adoProject || 'Unknown',
3673
3838
  repo_name: firstProject?.repoName || 'Unknown',
3674
3839
  team_root: SQUAD_DIR,
3675
- date: dateStamp()
3840
+ date: dateStamp(),
3841
+ notes_content: '',
3676
3842
  };
3843
+ try { vars.notes_content = fs.readFileSync(path.join(SQUAD_DIR, 'notes.md'), 'utf8'); } catch {}
3677
3844
 
3678
3845
  // Inject plan-specific variables for the plan playbook
3679
3846
  if (workType === 'plan') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/squad",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.squad/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "squad": "bin/squad.js"