@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 +228 -1
- package/dashboard.js +50 -1
- package/engine.js +174 -7
- package/package.json +1 -1
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 ||
|
|
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