@yemi33/squad 0.1.5 → 0.1.6

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
@@ -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: 2px; font-size: 10px; color: var(--muted);
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><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">&#x2715;</button></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">&#x1F4E6;</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">&#x2715;</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) result.project = proj.name;
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.project) {
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
- showToast('cmd-toast', 'PRD item ' + (data.id || id) + ' added', true);
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
- // Aggregate PRD across all projects
111
- const firstProject = PROJECTS[0];
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/prd-items
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 firstProject = PROJECTS[0];
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 });
@@ -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. Quality Metrics
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