@yemi33/squad 0.1.5 → 0.1.7
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 +117 -11
- package/dashboard.js +95 -10
- package/docs/distribution.md +95 -0
- package/docs/self-improvement.md +25 -1
- package/engine.js +535 -43
- package/package.json +1 -1
- package/playbooks/ask.md +49 -0
- package/playbooks/implement-shared.md +68 -0
- package/playbooks/implement.md +13 -0
- package/playbooks/plan-to-prd.md +22 -1
- package/playbooks/plan.md +99 -0
- package/routing.md +2 -0
package/dashboard.html
CHANGED
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
.prd-item-priority.high { background: rgba(248,81,73,0.15); color: var(--red); }
|
|
79
79
|
.prd-item-priority.medium { background: rgba(210,153,34,0.15); color: var(--yellow); }
|
|
80
80
|
.prd-item-priority.low { background: rgba(139,148,158,0.15); color: var(--muted); }
|
|
81
|
+
.prd-project-badge { font-size: 9px; padding: 1px 5px; border-radius: 6px; background: rgba(56,139,253,0.12); color: var(--blue); border: 1px solid rgba(56,139,253,0.25); white-space: nowrap; }
|
|
81
82
|
|
|
82
83
|
.inbox-item { background: var(--surface2); border: 1px solid var(--border); border-left: 3px solid var(--purple); border-radius: 4px; padding: 10px 12px; cursor: pointer; }
|
|
83
84
|
.inbox-item:hover { border-color: var(--blue); border-left-color: var(--blue); }
|
|
@@ -232,7 +233,7 @@
|
|
|
232
233
|
|
|
233
234
|
/* Hints bar */
|
|
234
235
|
.cmd-hints {
|
|
235
|
-
display: flex; gap: 12px; margin-top:
|
|
236
|
+
display: flex; gap: 12px; margin-top: 4px; font-size: 10px; color: var(--muted);
|
|
236
237
|
letter-spacing: 0.2px; padding: 0 4px; overflow-x: auto; white-space: nowrap;
|
|
237
238
|
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
|
238
239
|
}
|
|
@@ -387,7 +388,7 @@
|
|
|
387
388
|
<section class="cmd-center">
|
|
388
389
|
<h2>Command Center</h2>
|
|
389
390
|
<div class="cmd-input-wrap" id="cmd-input-wrap">
|
|
390
|
-
<textarea id="cmd-input" rows="1" placeholder='What do you need? e.g. "Fix the auth bug @dallas" or "/note always use feature flags"'
|
|
391
|
+
<textarea id="cmd-input" rows="1" placeholder='What do you need? e.g. "Fix the auth bug @dallas", "explain the dispatch flow", or "/note always use feature flags"'
|
|
391
392
|
oninput="cmdInputChanged()" onkeydown="cmdKeyDown(event)"></textarea>
|
|
392
393
|
<button class="cmd-send-btn" id="cmd-send-btn" onclick="cmdSubmit()">Send <kbd>Ctrl+Enter</kbd></button>
|
|
393
394
|
</div>
|
|
@@ -410,8 +411,9 @@
|
|
|
410
411
|
</section>
|
|
411
412
|
|
|
412
413
|
<section id="work-items-section" style="overflow:visible">
|
|
413
|
-
<h2>Work Items <span class="count" id="wi-count">0</span></h2>
|
|
414
|
+
<h2>Work Items <span class="count" id="wi-count">0</span> <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;margin-left:8px" onclick="toggleWorkItemArchive()">See Archive</button></h2>
|
|
414
415
|
<div id="work-items-content"><p class="empty">No work items. Add tasks via Command Center above.</p></div>
|
|
416
|
+
<div id="work-items-archive" style="display:none;margin-top:12px"></div>
|
|
415
417
|
</section>
|
|
416
418
|
|
|
417
419
|
<section class="prd-panel" id="prd-section">
|
|
@@ -680,10 +682,14 @@ function renderPrdProgress(prog) {
|
|
|
680
682
|
const prLinks = (i.prs || []).map(pr =>
|
|
681
683
|
'<a class="pr-title" href="' + escHtml(pr.url || '#') + '" target="_blank" style="font-size:10px;margin-left:4px" title="' + escHtml(pr.title || '') + '">' + escHtml(pr.id) + '</a>'
|
|
682
684
|
).join(' ');
|
|
685
|
+
const projBadges = (i.projects || []).map(p =>
|
|
686
|
+
'<span class="prd-project-badge">' + escHtml(p) + '</span>'
|
|
687
|
+
).join(' ');
|
|
683
688
|
return '<div class="prd-item-row">' +
|
|
684
689
|
'<span>' + statusIcon(i.status) + '</span>' +
|
|
685
690
|
'<span class="prd-item-id">' + escHtml(i.id) + '</span>' +
|
|
686
691
|
'<span class="prd-item-name" title="' + escHtml(i.name) + '">' + escHtml(i.name) + '</span>' +
|
|
692
|
+
(projBadges ? '<span>' + projBadges + '</span>' : '') +
|
|
687
693
|
(prLinks ? '<span>' + prLinks + '</span>' : '') +
|
|
688
694
|
'<span class="prd-item-priority ' + (i.priority || '') + '">' + escHtml(i.priority || '') + '</span>' +
|
|
689
695
|
'</div>';
|
|
@@ -1197,11 +1203,23 @@ function wiRow(item) {
|
|
|
1197
1203
|
'</td>' +
|
|
1198
1204
|
'<td>' + prLink + '</td>' +
|
|
1199
1205
|
'<td><span class="pr-date">' + shortTime(item.created) + '</span></td>' +
|
|
1200
|
-
'<td
|
|
1206
|
+
'<td style="white-space:nowrap">' +
|
|
1207
|
+
((item.status === 'done' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Archive work item">📦</button>' : '') +
|
|
1208
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Delete work item and kill agent">✕</button>' +
|
|
1209
|
+
'</td>' +
|
|
1201
1210
|
'</tr>';
|
|
1202
1211
|
}
|
|
1203
1212
|
|
|
1204
1213
|
function renderWorkItems(items) {
|
|
1214
|
+
// Sort: active/dispatched first, then by most recent activity
|
|
1215
|
+
const statusOrder = { dispatched: 0, pending: 1, queued: 1, failed: 2, done: 3 };
|
|
1216
|
+
items.sort((a, b) => {
|
|
1217
|
+
const sa = statusOrder[a.status] ?? 2, sb = statusOrder[b.status] ?? 2;
|
|
1218
|
+
if (sa !== sb) return sa - sb;
|
|
1219
|
+
const ta = a.completedAt || a.dispatched_at || a.created || '';
|
|
1220
|
+
const tb = b.completedAt || b.dispatched_at || b.created || '';
|
|
1221
|
+
return tb.localeCompare(ta); // most recent first
|
|
1222
|
+
});
|
|
1205
1223
|
allWorkItems = items;
|
|
1206
1224
|
const el = document.getElementById('work-items-content');
|
|
1207
1225
|
const countEl = document.getElementById('wi-count');
|
|
@@ -1261,6 +1279,44 @@ async function deleteWorkItem(id, source) {
|
|
|
1261
1279
|
} catch (e) { alert('Delete error: ' + e.message); }
|
|
1262
1280
|
}
|
|
1263
1281
|
|
|
1282
|
+
async function archiveWorkItem(id, source) {
|
|
1283
|
+
try {
|
|
1284
|
+
const res = await fetch('/api/work-items/archive', {
|
|
1285
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1286
|
+
body: JSON.stringify({ id, source: source || undefined })
|
|
1287
|
+
});
|
|
1288
|
+
if (res.ok) { refresh(); } else {
|
|
1289
|
+
const d = await res.json();
|
|
1290
|
+
alert('Archive failed: ' + (d.error || 'unknown'));
|
|
1291
|
+
}
|
|
1292
|
+
} catch (e) { alert('Archive error: ' + e.message); }
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
let wiArchiveVisible = false;
|
|
1296
|
+
async function toggleWorkItemArchive() {
|
|
1297
|
+
const el = document.getElementById('work-items-archive');
|
|
1298
|
+
wiArchiveVisible = !wiArchiveVisible;
|
|
1299
|
+
if (!wiArchiveVisible) { el.style.display = 'none'; return; }
|
|
1300
|
+
el.style.display = 'block';
|
|
1301
|
+
el.innerHTML = '<p class="empty">Loading archive...</p>';
|
|
1302
|
+
try {
|
|
1303
|
+
const items = await fetch('/api/work-items/archive').then(r => r.json());
|
|
1304
|
+
if (!items.length) { el.innerHTML = '<p class="empty">No archived work items.</p>'; return; }
|
|
1305
|
+
el.innerHTML = '<div style="font-size:10px;color:var(--muted);margin-bottom:6px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Archived (' + items.length + ')</div>' +
|
|
1306
|
+
'<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Agent</th><th>Archived</th></tr></thead><tbody>' +
|
|
1307
|
+
items.map(function(i) {
|
|
1308
|
+
return '<tr style="opacity:0.6">' +
|
|
1309
|
+
'<td><span class="pr-id">' + escHtml(i.id || '') + '</span></td>' +
|
|
1310
|
+
'<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(i.title || '') + '</td>' +
|
|
1311
|
+
'<td><span class="dispatch-type ' + (i.type || '') + '">' + escHtml(i.type || '') + '</span></td>' +
|
|
1312
|
+
'<td style="color:' + (i.status === 'done' ? 'var(--green)' : 'var(--red)') + '">' + escHtml(i.status || '') + '</td>' +
|
|
1313
|
+
'<td>' + escHtml(i.dispatched_to || '—') + '</td>' +
|
|
1314
|
+
'<td class="pr-date">' + shortTime(i.archivedAt) + '</td>' +
|
|
1315
|
+
'</tr>';
|
|
1316
|
+
}).join('') + '</tbody></table></div>';
|
|
1317
|
+
} catch (e) { el.innerHTML = '<p class="empty">Failed to load archive.</p>'; }
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1264
1320
|
function wiPrev() { if (wiPage > 0) { wiPage--; renderWorkItems(allWorkItems); } }
|
|
1265
1321
|
function wiNext() { const tp = Math.ceil(allWorkItems.length / WI_PER_PAGE); if (wiPage < tp-1) { wiPage++; renderWorkItems(allWorkItems); } }
|
|
1266
1322
|
|
|
@@ -1323,6 +1379,7 @@ function showToast(id, msg, ok) {
|
|
|
1323
1379
|
function detectWorkItemType(text) {
|
|
1324
1380
|
const t = text.toLowerCase();
|
|
1325
1381
|
const patterns = [
|
|
1382
|
+
{ type: 'ask', words: ['explain', 'why does', 'why is', 'what does', 'how do i', 'how do you', 'what\'s the', 'tell me', 'can you explain', 'walk me through'] },
|
|
1326
1383
|
{ type: 'explore', words: ['explore', 'investigate', 'understand', 'analyze', 'audit', 'document', 'architecture', 'how does', 'what is', 'look into', 'research', 'survey', 'map out', 'codebase'] },
|
|
1327
1384
|
{ type: 'fix', words: ['fix', 'bug', 'broken', 'crash', 'error', 'issue', 'patch', 'repair', 'resolve', 'regression', 'failing', 'doesn\'t work', 'not working'] },
|
|
1328
1385
|
{ type: 'review', words: ['review', 'code review', 'check pr', 'look at pr', 'audit code', 'inspect'] },
|
|
@@ -1342,7 +1399,8 @@ function cmdParseInput(raw) {
|
|
|
1342
1399
|
agents: [], // assigned agent IDs
|
|
1343
1400
|
fanout: false,
|
|
1344
1401
|
priority: 'medium',
|
|
1345
|
-
project: '',
|
|
1402
|
+
project: '', // primary project (for work items, plans)
|
|
1403
|
+
projects: [], // multi-project list (for PRD items)
|
|
1346
1404
|
title: '',
|
|
1347
1405
|
description: '',
|
|
1348
1406
|
type: '', // work item type (auto-detected)
|
|
@@ -1352,6 +1410,19 @@ function cmdParseInput(raw) {
|
|
|
1352
1410
|
if (/^\/decide\b/i.test(text) || /^\/note\b/i.test(text)) {
|
|
1353
1411
|
result.intent = 'note';
|
|
1354
1412
|
text = text.replace(/^\/decide\s*/i, '');
|
|
1413
|
+
} else if (/^\/plan\b/i.test(text)) {
|
|
1414
|
+
result.intent = 'plan';
|
|
1415
|
+
text = text.replace(/^\/plan\s*/i, '');
|
|
1416
|
+
// Extract branch strategy flag
|
|
1417
|
+
if (/--parallel\b/i.test(text)) {
|
|
1418
|
+
result.branchStrategy = 'parallel';
|
|
1419
|
+
text = text.replace(/--parallel\b/i, '').trim();
|
|
1420
|
+
} else if (/--shared\b/i.test(text)) {
|
|
1421
|
+
result.branchStrategy = 'shared-branch';
|
|
1422
|
+
text = text.replace(/--shared\b/i, '').trim();
|
|
1423
|
+
} else {
|
|
1424
|
+
result.branchStrategy = 'shared-branch'; // default
|
|
1425
|
+
}
|
|
1355
1426
|
} else if (/^\/prd\b/i.test(text)) {
|
|
1356
1427
|
result.intent = 'prd';
|
|
1357
1428
|
text = text.replace(/^\/prd\s*/i, '');
|
|
@@ -1377,12 +1448,15 @@ function cmdParseInput(raw) {
|
|
|
1377
1448
|
else if (/!low\b/i.test(text)) { result.priority = 'low'; text = text.replace(/!low\b/i, '').trim(); }
|
|
1378
1449
|
else if (/!urgent\b/i.test(text)) { result.priority = 'high'; text = text.replace(/!urgent\b/i, '').trim(); }
|
|
1379
1450
|
|
|
1380
|
-
// Extract #project
|
|
1451
|
+
// Extract #project(s)
|
|
1381
1452
|
const projRe = /#(\S+)/g;
|
|
1382
1453
|
while ((m = projRe.exec(text)) !== null) {
|
|
1383
1454
|
const pname = m[1];
|
|
1384
1455
|
const proj = cmdProjects.find(p => p.name.toLowerCase() === pname.toLowerCase());
|
|
1385
|
-
if (proj)
|
|
1456
|
+
if (proj) {
|
|
1457
|
+
result.project = proj.name; // last match for backward compat (work items, plans)
|
|
1458
|
+
if (!result.projects.includes(proj.name)) result.projects.push(proj.name);
|
|
1459
|
+
}
|
|
1386
1460
|
}
|
|
1387
1461
|
text = text.replace(/#\S+/g, '').trim();
|
|
1388
1462
|
|
|
@@ -1410,13 +1484,19 @@ function cmdRenderMeta() {
|
|
|
1410
1484
|
let chips = [];
|
|
1411
1485
|
|
|
1412
1486
|
// Intent chip
|
|
1413
|
-
const intentLabels = { 'work-item': 'Work Item', 'note': 'Note', 'prd': 'PRD Item' };
|
|
1487
|
+
const intentLabels = { 'work-item': 'Work Item', 'note': 'Note', 'prd': 'PRD Item', 'plan': 'Plan → PRD → Dispatch' };
|
|
1414
1488
|
chips.push('<span class="cmd-chip intent">' + intentLabels[parsed.intent] + '</span>');
|
|
1415
1489
|
|
|
1416
1490
|
// Type chip (only for work items)
|
|
1417
1491
|
if (parsed.intent === 'work-item') {
|
|
1418
1492
|
chips.push('<span class="cmd-chip">' + parsed.type + '</span>');
|
|
1419
1493
|
}
|
|
1494
|
+
// Pipeline chip for /plan
|
|
1495
|
+
if (parsed.intent === 'plan') {
|
|
1496
|
+
const strategy = parsed.branchStrategy || 'shared-branch';
|
|
1497
|
+
const stratLabel = strategy === 'parallel' ? 'parallel branches' : 'shared branch';
|
|
1498
|
+
chips.push('<span class="cmd-chip" style="background:var(--purple,#a855f7);color:#fff">plan → prd → agents (' + stratLabel + ')</span>');
|
|
1499
|
+
}
|
|
1420
1500
|
|
|
1421
1501
|
// Priority chip
|
|
1422
1502
|
chips.push('<span class="cmd-chip priority-' + parsed.priority + '">' + parsed.priority + ' priority</span>');
|
|
@@ -1432,8 +1512,10 @@ function cmdRenderMeta() {
|
|
|
1432
1512
|
}
|
|
1433
1513
|
}
|
|
1434
1514
|
|
|
1435
|
-
// Project chip
|
|
1436
|
-
if (parsed.
|
|
1515
|
+
// Project chip(s)
|
|
1516
|
+
if (parsed.projects.length > 0) {
|
|
1517
|
+
parsed.projects.forEach(p => chips.push('<span class="cmd-chip project-chip">#' + escHtml(p) + '</span>'));
|
|
1518
|
+
} else if (parsed.project) {
|
|
1437
1519
|
chips.push('<span class="cmd-chip project-chip">#' + escHtml(parsed.project) + '</span>');
|
|
1438
1520
|
}
|
|
1439
1521
|
|
|
@@ -1600,6 +1682,8 @@ async function cmdSubmit() {
|
|
|
1600
1682
|
try {
|
|
1601
1683
|
if (parsed.intent === 'note') {
|
|
1602
1684
|
await cmdSubmitNote(parsed);
|
|
1685
|
+
} else if (parsed.intent === 'plan') {
|
|
1686
|
+
await cmdSubmitPlan(parsed);
|
|
1603
1687
|
} else if (parsed.intent === 'prd') {
|
|
1604
1688
|
await cmdSubmitPrd(parsed);
|
|
1605
1689
|
} else {
|
|
@@ -1657,6 +1741,26 @@ async function cmdSubmitNote(parsed) {
|
|
|
1657
1741
|
showToast('cmd-toast', 'Note added', true);
|
|
1658
1742
|
}
|
|
1659
1743
|
|
|
1744
|
+
async function cmdSubmitPlan(parsed) {
|
|
1745
|
+
const body = {
|
|
1746
|
+
title: parsed.title,
|
|
1747
|
+
description: parsed.description || '',
|
|
1748
|
+
priority: parsed.priority,
|
|
1749
|
+
branch_strategy: parsed.branchStrategy || 'shared-branch',
|
|
1750
|
+
};
|
|
1751
|
+
if (parsed.project) body.project = parsed.project;
|
|
1752
|
+
if (parsed.agents.length === 1) body.agent = parsed.agents[0];
|
|
1753
|
+
|
|
1754
|
+
const res = await fetch('/api/plan', {
|
|
1755
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1756
|
+
body: JSON.stringify(body),
|
|
1757
|
+
});
|
|
1758
|
+
const data = await res.json();
|
|
1759
|
+
if (!res.ok) throw new Error(data.error || 'Failed to create plan');
|
|
1760
|
+
const agentLabel = data.agent ? ' → ' + data.agent : '';
|
|
1761
|
+
showToast('cmd-toast', 'Plan ' + data.id + ' dispatched' + agentLabel + ' (will chain → PRD → dispatch)', true);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1660
1764
|
async function cmdSubmitPrd(parsed) {
|
|
1661
1765
|
// Auto-generate PRD ID
|
|
1662
1766
|
const id = 'M' + String(Date.now()).slice(-4);
|
|
@@ -1668,12 +1772,14 @@ async function cmdSubmitPrd(parsed) {
|
|
|
1668
1772
|
description: parsed.description || parsed.title,
|
|
1669
1773
|
priority: parsed.priority,
|
|
1670
1774
|
estimated_complexity: 'medium',
|
|
1775
|
+
projects: parsed.projects || [],
|
|
1671
1776
|
rationale: '',
|
|
1672
1777
|
}),
|
|
1673
1778
|
});
|
|
1674
1779
|
const data = await res.json();
|
|
1675
1780
|
if (!res.ok) throw new Error(data.error || 'Failed to add');
|
|
1676
|
-
|
|
1781
|
+
const projLabel = (parsed.projects || []).length > 0 ? ' (' + parsed.projects.join(', ') + ')' : '';
|
|
1782
|
+
showToast('cmd-toast', 'PRD item ' + (data.id || id) + ' added' + projLabel, true);
|
|
1677
1783
|
}
|
|
1678
1784
|
</script>
|
|
1679
1785
|
</body>
|
package/dashboard.js
CHANGED
|
@@ -107,11 +107,8 @@ function getAgents() {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function getPrdInfo() {
|
|
110
|
-
//
|
|
111
|
-
const
|
|
112
|
-
const root = path.resolve(firstProject.localPath || path.resolve(SQUAD_DIR, '..'));
|
|
113
|
-
const prdSrc = firstProject.workSources?.prd || CONFIG.workSources?.prd || {};
|
|
114
|
-
const prdPath = path.resolve(root, prdSrc.path || 'docs/prd-gaps.json');
|
|
110
|
+
// Squad-level PRD — single file at ~/.squad/prd.json
|
|
111
|
+
const prdPath = path.join(SQUAD_DIR, 'prd.json');
|
|
115
112
|
if (!fs.existsSync(prdPath)) return { progress: null, status: null };
|
|
116
113
|
|
|
117
114
|
try {
|
|
@@ -149,6 +146,7 @@ function getPrdInfo() {
|
|
|
149
146
|
items: items.map(i => ({
|
|
150
147
|
id: i.id, name: i.name || i.title, priority: i.priority,
|
|
151
148
|
complexity: i.estimated_complexity || i.size, status: i.status || 'missing',
|
|
149
|
+
projects: i.projects || [],
|
|
152
150
|
prs: prdToPr[i.id] || []
|
|
153
151
|
})),
|
|
154
152
|
};
|
|
@@ -580,6 +578,66 @@ const server = http.createServer(async (req, res) => {
|
|
|
580
578
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
581
579
|
}
|
|
582
580
|
|
|
581
|
+
// POST /api/work-items/archive — move a completed/failed work item to archive
|
|
582
|
+
if (req.method === 'POST' && req.url === '/api/work-items/archive') {
|
|
583
|
+
try {
|
|
584
|
+
const body = await readBody(req);
|
|
585
|
+
const { id, source } = body;
|
|
586
|
+
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
587
|
+
|
|
588
|
+
let wiPath;
|
|
589
|
+
if (!source || source === 'central') {
|
|
590
|
+
wiPath = path.join(SQUAD_DIR, 'work-items.json');
|
|
591
|
+
} else {
|
|
592
|
+
const proj = PROJECTS.find(p => p.name === source);
|
|
593
|
+
if (proj) {
|
|
594
|
+
const root = path.resolve(proj.localPath || path.resolve(SQUAD_DIR, '..'));
|
|
595
|
+
const wiSrc = proj.workSources?.workItems || CONFIG.workSources?.workItems || {};
|
|
596
|
+
wiPath = path.resolve(root, wiSrc.path || '.squad/work-items.json');
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
600
|
+
|
|
601
|
+
const items = JSON.parse(safeRead(wiPath) || '[]');
|
|
602
|
+
const idx = items.findIndex(i => i.id === id);
|
|
603
|
+
if (idx === -1) return jsonReply(res, 404, { error: 'item not found' });
|
|
604
|
+
|
|
605
|
+
const item = items.splice(idx, 1)[0];
|
|
606
|
+
item.archivedAt = new Date().toISOString();
|
|
607
|
+
|
|
608
|
+
// Append to archive file
|
|
609
|
+
const archivePath = wiPath.replace('.json', '-archive.json');
|
|
610
|
+
let archive = [];
|
|
611
|
+
const existing = safeRead(archivePath);
|
|
612
|
+
if (existing) { try { archive = JSON.parse(existing); } catch {} }
|
|
613
|
+
archive.push(item);
|
|
614
|
+
fs.writeFileSync(archivePath, JSON.stringify(archive, null, 2));
|
|
615
|
+
fs.writeFileSync(wiPath, JSON.stringify(items, null, 2));
|
|
616
|
+
|
|
617
|
+
return jsonReply(res, 200, { ok: true, id });
|
|
618
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// GET /api/work-items/archive — list archived work items
|
|
622
|
+
if (req.method === 'GET' && req.url === '/api/work-items/archive') {
|
|
623
|
+
try {
|
|
624
|
+
let allArchived = [];
|
|
625
|
+
// Central archive
|
|
626
|
+
const centralPath = path.join(SQUAD_DIR, 'work-items-archive.json');
|
|
627
|
+
const central = safeRead(centralPath);
|
|
628
|
+
if (central) { try { allArchived.push(...JSON.parse(central).map(i => ({ ...i, _source: 'central' }))); } catch {} }
|
|
629
|
+
// Project archives
|
|
630
|
+
for (const project of PROJECTS) {
|
|
631
|
+
const root = path.resolve(project.localPath || path.resolve(SQUAD_DIR, '..'));
|
|
632
|
+
const wiSrc = project.workSources?.workItems || CONFIG.workSources?.workItems || {};
|
|
633
|
+
const archPath = path.resolve(root, (wiSrc.path || '.squad/work-items.json').replace('.json', '-archive.json'));
|
|
634
|
+
const content = safeRead(archPath);
|
|
635
|
+
if (content) { try { allArchived.push(...JSON.parse(content).map(i => ({ ...i, _source: project.name }))); } catch {} }
|
|
636
|
+
}
|
|
637
|
+
return jsonReply(res, 200, allArchived);
|
|
638
|
+
} catch (e) { return jsonReply(res, 200, []); }
|
|
639
|
+
}
|
|
640
|
+
|
|
583
641
|
// POST /api/work-items
|
|
584
642
|
if (req.method === 'POST' && req.url === '/api/work-items') {
|
|
585
643
|
try {
|
|
@@ -641,14 +699,40 @@ const server = http.createServer(async (req, res) => {
|
|
|
641
699
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
642
700
|
}
|
|
643
701
|
|
|
644
|
-
// POST /api/
|
|
702
|
+
// POST /api/plan — create a plan work item that chains to PRD on completion
|
|
703
|
+
if (req.method === 'POST' && req.url === '/api/plan') {
|
|
704
|
+
try {
|
|
705
|
+
const body = await readBody(req);
|
|
706
|
+
// Write as a work item with type 'plan' — engine handles the chaining
|
|
707
|
+
const wiPath = path.join(SQUAD_DIR, 'work-items.json');
|
|
708
|
+
let items = [];
|
|
709
|
+
const existing = safeRead(wiPath);
|
|
710
|
+
if (existing) { try { items = JSON.parse(existing); } catch {} }
|
|
711
|
+
const maxNum = items.reduce(function(max, i) {
|
|
712
|
+
const m = (i.id || '').match(/(\d+)$/);
|
|
713
|
+
return m ? Math.max(max, parseInt(m[1])) : max;
|
|
714
|
+
}, 0);
|
|
715
|
+
const id = 'W' + String(maxNum + 1).padStart(3, '0');
|
|
716
|
+
const item = {
|
|
717
|
+
id, title: body.title, type: 'plan',
|
|
718
|
+
priority: body.priority || 'high', description: body.description || '',
|
|
719
|
+
status: 'pending', created: new Date().toISOString(), createdBy: 'dashboard',
|
|
720
|
+
chain: 'plan-to-prd',
|
|
721
|
+
branchStrategy: body.branch_strategy || 'shared-branch',
|
|
722
|
+
};
|
|
723
|
+
if (body.project) item.project = body.project;
|
|
724
|
+
if (body.agent) item.agent = body.agent;
|
|
725
|
+
items.push(item);
|
|
726
|
+
fs.writeFileSync(wiPath, JSON.stringify(items, null, 2));
|
|
727
|
+
return jsonReply(res, 200, { ok: true, id, agent: body.agent || '' });
|
|
728
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// POST /api/prd-items — squad-level PRD
|
|
645
732
|
if (req.method === 'POST' && req.url === '/api/prd-items') {
|
|
646
733
|
try {
|
|
647
734
|
const body = await readBody(req);
|
|
648
|
-
const
|
|
649
|
-
const root = path.resolve(firstProject.localPath || path.resolve(SQUAD_DIR, '..'));
|
|
650
|
-
const prdSrc = firstProject.workSources?.prd || CONFIG.workSources?.prd || {};
|
|
651
|
-
const prdPath = path.resolve(root, prdSrc.path || 'docs/prd-gaps.json');
|
|
735
|
+
const prdPath = path.join(SQUAD_DIR, 'prd.json');
|
|
652
736
|
let data = { missing_features: [], existing_features: [], open_questions: [] };
|
|
653
737
|
const existing = safeRead(prdPath);
|
|
654
738
|
if (existing) { try { data = JSON.parse(existing); } catch {} }
|
|
@@ -657,6 +741,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
657
741
|
id: body.id, name: body.name, description: body.description || '',
|
|
658
742
|
priority: body.priority || 'medium', estimated_complexity: body.estimated_complexity || 'medium',
|
|
659
743
|
rationale: body.rationale || '', status: 'missing', affected_areas: [],
|
|
744
|
+
projects: body.projects || [],
|
|
660
745
|
});
|
|
661
746
|
fs.writeFileSync(prdPath, JSON.stringify(data, null, 2));
|
|
662
747
|
return jsonReply(res, 200, { ok: true, id: body.id });
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Distribution & Publishing
|
|
2
|
+
|
|
3
|
+
Squad is distributed as an npm package (`@yemi33/squad`) from a sanitized copy of the main repo.
|
|
4
|
+
|
|
5
|
+
## Two-Repo Architecture
|
|
6
|
+
|
|
7
|
+
| Repo | Purpose | What's included |
|
|
8
|
+
|------|---------|----------------|
|
|
9
|
+
| **origin** (`yemishin_microsoft/squad`) | Full working repo with all session state | Everything — history, notes, decisions, work items, CLAUDE.md |
|
|
10
|
+
| **personal** (`yemi33/squad`) | Clean distribution for others | Engine, dashboard, playbooks, charters, skills, docs, npm package files |
|
|
11
|
+
|
|
12
|
+
## What Gets Stripped
|
|
13
|
+
|
|
14
|
+
These files are removed during sync to personal:
|
|
15
|
+
|
|
16
|
+
| Category | Pattern | Reason |
|
|
17
|
+
|----------|---------|--------|
|
|
18
|
+
| Agent history | `agents/*/history.md` | Session-specific task logs |
|
|
19
|
+
| Notes archive | `notes/archive/*` | Historical agent findings |
|
|
20
|
+
| Notes inbox | `notes/inbox/*` | Pending agent findings |
|
|
21
|
+
| Notes summary | `notes.md` | Consolidated knowledge (runtime) |
|
|
22
|
+
| Work items | `work-items.json` | Runtime dispatch tracking |
|
|
23
|
+
| Project instructions | `CLAUDE.md` | Org-specific context |
|
|
24
|
+
|
|
25
|
+
## npm Package
|
|
26
|
+
|
|
27
|
+
**Package:** `@yemi33/squad`
|
|
28
|
+
**Registry:** https://www.npmjs.com/package/@yemi33/squad
|
|
29
|
+
|
|
30
|
+
### What's in the package
|
|
31
|
+
|
|
32
|
+
Controlled by the `files` field in `package.json`:
|
|
33
|
+
- `bin/squad.js` — CLI entry point
|
|
34
|
+
- `engine.js`, `dashboard.js`, `dashboard.html`, `squad.js` — core scripts
|
|
35
|
+
- `engine/spawn-agent.js`, `engine/ado-mcp-wrapper.js` — engine helpers
|
|
36
|
+
- `agents/*/charter.md` — agent role definitions
|
|
37
|
+
- `playbooks/*.md` — task templates
|
|
38
|
+
- `config.template.json` — starter config
|
|
39
|
+
- `routing.md`, `team.md` — editable team config
|
|
40
|
+
- `skills/`, `docs/` — documentation and workflows
|
|
41
|
+
|
|
42
|
+
### How `squad init` works
|
|
43
|
+
|
|
44
|
+
1. Copies all package files from `node_modules/@yemi33/squad/` to `~/.squad/`
|
|
45
|
+
2. Creates `config.json` from `config.template.json` if it doesn't exist
|
|
46
|
+
3. Creates runtime directories (`engine/`, `notes/inbox/`, `notes/archive/`, etc.)
|
|
47
|
+
4. Runs `squad.js init` to populate config with default agents
|
|
48
|
+
5. On `--force`, overwrites `.js` and `.html` files but preserves user-modified `.md` files
|
|
49
|
+
|
|
50
|
+
### How updates work
|
|
51
|
+
|
|
52
|
+
- Users run `npm update -g @yemi33/squad` then `squad init --force` to update engine code
|
|
53
|
+
- `npx @yemi33/squad` always fetches the latest version
|
|
54
|
+
|
|
55
|
+
## Auto-Publishing
|
|
56
|
+
|
|
57
|
+
A GitHub Action on the personal repo auto-publishes to npm on every push to master.
|
|
58
|
+
|
|
59
|
+
### How it works
|
|
60
|
+
|
|
61
|
+
1. Push to `yemi33/squad` master triggers `.github/workflows/publish.yml`
|
|
62
|
+
2. Action queries npm for the current published version
|
|
63
|
+
3. Bumps patch version (e.g., `0.1.5` → `0.1.6`)
|
|
64
|
+
4. Publishes to npm with the new version
|
|
65
|
+
5. Commits the version bump back to the repo with `[skip ci]` to prevent loops
|
|
66
|
+
|
|
67
|
+
### Why version comes from npm, not the repo
|
|
68
|
+
|
|
69
|
+
The sync-to-personal workflow force-pushes, which overwrites any version bump commits from previous action runs. So the action reads the latest version from the npm registry and bumps from there.
|
|
70
|
+
|
|
71
|
+
### Setup requirements
|
|
72
|
+
|
|
73
|
+
- `NPM_TOKEN` secret on `yemi33/squad` — a granular access token with publish permissions and 2FA bypass enabled
|
|
74
|
+
- The workflow file (`.github/workflows/publish.yml`) is gitignored on the org repo and force-added during sync
|
|
75
|
+
|
|
76
|
+
## Sync Workflow
|
|
77
|
+
|
|
78
|
+
Run `/sync-to-personal` or manually:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# 1. Create dist branch, strip files, add workflow, force-push
|
|
82
|
+
git checkout -b dist-clean
|
|
83
|
+
git rm --cached agents/*/history.md notes.md work-items.json CLAUDE.md
|
|
84
|
+
git rm -r --cached notes/archive/ notes/inbox/ notes/
|
|
85
|
+
# ... add .gitkeep files, .gitignore entries, workflow file
|
|
86
|
+
git add -f .github/workflows/publish.yml
|
|
87
|
+
git commit -m "Strip for distribution"
|
|
88
|
+
git push personal dist-clean:master --force
|
|
89
|
+
|
|
90
|
+
# 2. Return to master
|
|
91
|
+
git checkout master
|
|
92
|
+
git branch -D dist-clean
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The full workflow is documented in `.claude/skills/sync-to-personal/SKILL.md`.
|
package/docs/self-improvement.md
CHANGED
|
@@ -117,7 +117,31 @@ in the future, avoid the patterns flagged here.
|
|
|
117
117
|
|
|
118
118
|
Without this, review findings only exist in the inbox file under the reviewer's name. The author never explicitly sees them unless they happen to read the consolidated notes.md. The feedback loop ensures the author gets a direct, targeted learning from every review.
|
|
119
119
|
|
|
120
|
-
## 4.
|
|
120
|
+
## 4. Human Feedback on PRs
|
|
121
|
+
|
|
122
|
+
Humans can leave comments on ADO PRs containing `@squad` to trigger fix tasks. The engine polls PR threads every ~6 minutes and dispatches fixes to the PR's author agent.
|
|
123
|
+
|
|
124
|
+
### Flow
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
Human comments on PR with "@squad fix the error handling here"
|
|
128
|
+
→ pollPrHumanComments() detects new @squad comment
|
|
129
|
+
→ sets pr.humanFeedback.pendingFix = true with comment text
|
|
130
|
+
→ discoverFromPrs() sees pendingFix flag
|
|
131
|
+
→ dispatches fix task to PR author agent
|
|
132
|
+
→ author agent fixes and pushes
|
|
133
|
+
→ PR goes back into normal review cycle
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### How it works
|
|
137
|
+
|
|
138
|
+
- **Trigger:** If you're the only human commenter, **any** comment triggers a fix. If multiple humans are commenting, `@squad` keyword is required to avoid noise
|
|
139
|
+
- **Agent detection:** Comments matching `/\bSquad\s*\(/i` are skipped (agent signature pattern)
|
|
140
|
+
- **Dedup:** Only comments newer than `pr.humanFeedback.lastProcessedCommentDate` are processed
|
|
141
|
+
- **Multiple comments:** All new `@squad` comments are concatenated into a single fix task
|
|
142
|
+
- **After fix:** `pendingFix` is cleared; PR re-enters normal review cycle
|
|
143
|
+
|
|
144
|
+
## 5. Quality Metrics
|
|
121
145
|
|
|
122
146
|
The engine tracks per-agent performance metrics in `engine/metrics.json`. Updated after every task completion and PR review.
|
|
123
147
|
|