@yemi33/squad 0.1.14 → 0.1.15

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/README.md CHANGED
@@ -128,7 +128,6 @@ You can also run scripts directly: `node ~/.squad/engine.js start`, `node ~/.squ
128
128
  │ engine.js ← tick 60s │
129
129
  │ dashboard.js ← :7331 │
130
130
  │ config.json ← projects │
131
- │ mcp-servers.json ← auto-sync │
132
131
  │ agents/ ← 5 agents │
133
132
  │ playbooks/ ← templates │
134
133
  │ prd.json ← squad PRD │
@@ -231,7 +230,7 @@ When dispatching agents, the engine reads each project's `CLAUDE.md` and injects
231
230
 
232
231
  ## MCP Server Integration
233
232
 
234
- Agents need MCP tools to interact with your repo host (create PRs, post review comments, etc.). On engine start, MCP servers are auto-synced from `~/.claude.json` to `mcp-servers.json`.
233
+ Agents need MCP tools to interact with your repo host (create PRs, post review comments, etc.). Agents inherit MCP servers directly from `~/.claude.json` as Claude Code processes — add servers there and they're immediately available to all agents on next spawn.
235
234
 
236
235
  **Example:** If you use Azure DevOps, configure the `azure-ado` MCP server in your Claude Code settings. If you use GitHub, configure the `github` MCP server. Agents will discover and use whichever tools are available.
237
236
 
@@ -314,7 +313,7 @@ No bash or shell involved — Node spawns Node directly. Prompts with special ch
314
313
  - **System prompt** — identity, charter, history, project context, critical rules, skill index, team notes
315
314
  - **Task prompt** — rendered playbook with `{{variables}}` filled from config
316
315
  - **Working directory** — project root (agent creates worktrees as needed)
317
- - **MCP servers** — all servers from `~/.claude.json` via `--mcp-config`
316
+ - **MCP servers** — inherited from `~/.claude.json` (no extra config needed)
318
317
  - **Full tool access** — all built-in tools plus all MCP tools
319
318
  - **Permission mode** — `bypassPermissions` (no interactive prompts)
320
319
  - **Output format** — `stream-json` (real-time streaming for live dashboard + heartbeat)
@@ -446,7 +445,6 @@ squad start
446
445
 
447
446
  **Machine-specific (reconfigure per machine):**
448
447
  - `config.json` — contains absolute paths to project directories. Re-link via `squad add <dir>`.
449
- - `mcp-servers.json` — auto-synced from `~/.claude.json` on engine start.
450
448
 
451
449
  To move to a new machine: `npm install -g @yemi33/squad && squad init --force`, then re-run `squad add` for each project.
452
450
 
@@ -471,7 +469,6 @@ To move to a new machine: `npm install -g @yemi33/squad && squad init --force`,
471
469
  prd.json <- Squad-level PRD (multi-project items)
472
470
  config.template.json <- Template for new installs
473
471
  package.json <- npm package definition
474
- mcp-servers.json <- MCP servers (auto-synced, gitignored)
475
472
  routing.md <- Dispatch rules table (editable)
476
473
  team.md <- Team roster
477
474
  notes.md <- Team rules + consolidated learnings (runtime)
package/dashboard.html CHANGED
@@ -58,6 +58,11 @@
58
58
  .modal-qa-loading .dot-pulse span:nth-child(2) { animation-delay: 0.2s; }
59
59
  .modal-qa-loading .dot-pulse span:nth-child(3) { animation-delay: 0.4s; }
60
60
  @keyframes dotPulse { 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); } 40% { opacity: 1; transform: scale(1); } }
61
+ .modal-qa-selection-pill { display: flex; align-items: center; gap: 6px; padding: 4px 8px; margin-bottom: 6px; background: rgba(88,166,255,0.08); border: 1px solid rgba(88,166,255,0.25); border-radius: 4px; font-size: 11px; }
62
+ .modal-qa-selection-pill .pill-label { color: var(--blue); font-weight: 600; white-space: nowrap; }
63
+ .modal-qa-selection-pill .pill-text { color: var(--muted); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-style: italic; }
64
+ .modal-qa-selection-pill .pill-clear { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 14px; padding: 0 2px; line-height: 1; }
65
+ .modal-qa-selection-pill .pill-clear:hover { color: var(--red); }
61
66
  .modal-qa-input-wrap { display: flex; gap: 6px; }
62
67
  .modal-qa-input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 6px 10px; font-size: 12px; color: var(--text); font-family: inherit; }
63
68
  .modal-qa-input:focus { border-color: var(--blue); outline: none; }
@@ -239,7 +244,19 @@
239
244
  transition: border-color 0.2s, box-shadow 0.2s; padding: 0;
240
245
  }
241
246
  .cmd-input-wrap:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(88,166,255,0.12); }
247
+ .cmd-highlight-layer {
248
+ position: absolute; top: 0; left: 0; right: 0; bottom: 0;
249
+ padding: 14px 16px; font-size: 14px; font-family: inherit; line-height: 1.5;
250
+ white-space: pre-wrap; word-wrap: break-word; overflow: hidden;
251
+ pointer-events: none; color: transparent; border-radius: 10px;
252
+ }
253
+ .cmd-highlight-layer .hl-cmd { color: transparent; background: rgba(88,166,255,0.18); border-radius: 3px; }
254
+ .cmd-highlight-layer .hl-mention { color: transparent; background: rgba(63,185,80,0.18); border-radius: 3px; }
255
+ .cmd-highlight-layer .hl-priority { color: transparent; background: rgba(210,153,34,0.18); border-radius: 3px; }
256
+ .cmd-highlight-layer .hl-project { color: transparent; background: rgba(188,140,255,0.18); border-radius: 3px; }
257
+ .cmd-highlight-layer .hl-flag { color: transparent; background: rgba(248,81,73,0.18); border-radius: 3px; }
242
258
  .cmd-input-wrap textarea {
259
+ position: relative; z-index: 1;
243
260
  flex: 1; background: transparent; border: none; color: var(--text);
244
261
  padding: 14px 16px; font-size: 14px; font-family: inherit; resize: none;
245
262
  outline: none; line-height: 1.5; min-height: 48px; max-height: 200px;
@@ -358,11 +375,15 @@
358
375
  .dispatch-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; font-size: 12px; }
359
376
  .dispatch-type { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 8px; text-transform: uppercase; white-space: nowrap; }
360
377
  .dispatch-type.implement { background: rgba(88,166,255,0.15); color: var(--blue); }
378
+ .dispatch-type.implement\:large { background: rgba(88,166,255,0.25); color: var(--blue); }
361
379
  .dispatch-type.review { background: rgba(188,140,255,0.15); color: var(--purple); }
362
380
  .dispatch-type.fix { background: rgba(210,153,34,0.15); color: var(--yellow); }
363
381
  .dispatch-type.analyze { background: rgba(63,185,80,0.15); color: var(--green); }
364
382
  .dispatch-type.explore { background: rgba(139,148,158,0.15); color: var(--muted); }
365
383
  .dispatch-type.test { background: rgba(227,179,65,0.15); color: var(--orange); }
384
+ .dispatch-type.plan { background: rgba(168,85,247,0.15); color: #a855f7; }
385
+ .dispatch-type.plan-to-prd { background: rgba(168,85,247,0.1); color: #a855f7; }
386
+ .dispatch-type.ask { background: rgba(63,185,80,0.15); color: var(--green); }
366
387
  .dispatch-type.manual { background: rgba(139,148,158,0.15); color: var(--muted); }
367
388
  .dispatch-agent { font-weight: 600; color: var(--text); }
368
389
  .dispatch-task { flex: 1; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -478,8 +499,9 @@
478
499
  <section class="cmd-center">
479
500
  <h2>Command Center</h2>
480
501
  <div class="cmd-input-wrap" id="cmd-input-wrap">
502
+ <div class="cmd-highlight-layer" id="cmd-highlight" aria-hidden="true"></div>
481
503
  <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"'
482
- oninput="cmdInputChanged()" onkeydown="cmdKeyDown(event)"></textarea>
504
+ oninput="cmdInputChanged()" onkeydown="cmdKeyDown(event)" onscroll="syncHighlightScroll()"></textarea>
483
505
  <button class="cmd-send-btn" id="cmd-send-btn" onclick="cmdSubmit()">Send <kbd>Ctrl+Enter</kbd></button>
484
506
  </div>
485
507
  <div class="cmd-mention-popup" id="cmd-mention-popup"></div>
@@ -526,7 +548,7 @@
526
548
  </section>
527
549
 
528
550
  <section>
529
- <h2>Notes Inbox <span class="count" id="inbox-count">0</span></h2>
551
+ <h2>Notes Inbox <span class="count" id="inbox-count">0</span> <span style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">auto-consolidates at 3 notes</span></h2>
530
552
  <div class="inbox-list" id="inbox-list">Loading...</div>
531
553
  </section>
532
554
 
@@ -546,6 +568,11 @@
546
568
  <div id="skills-list"><p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p></div>
547
569
  </section>
548
570
 
571
+ <section>
572
+ <h2>MCP Servers <span class="count" id="mcp-count">0</span></h2>
573
+ <div id="mcp-list"><p class="empty">No MCP servers synced.</p></div>
574
+ </section>
575
+
549
576
  <section>
550
577
  <h2>Dispatch Queue</h2>
551
578
  <div class="dispatch-stats" id="dispatch-stats"></div>
@@ -601,6 +628,11 @@
601
628
  <div class="modal-body" id="modal-body"></div>
602
629
  <div class="modal-qa" id="modal-qa">
603
630
  <div class="modal-qa-thread" id="modal-qa-thread"></div>
631
+ <div class="modal-qa-selection-pill" id="modal-qa-pill" style="display:none">
632
+ <span class="pill-label">Selection:</span>
633
+ <span class="pill-text" id="modal-qa-pill-text"></span>
634
+ <button class="pill-clear" onclick="clearQaSelection()" title="Clear selection">&times;</button>
635
+ </div>
604
636
  <div class="modal-qa-input-wrap">
605
637
  <input type="text" class="modal-qa-input" id="modal-qa-input" placeholder="Ask about this document (or select text first)..." onkeydown="if(event.key==='Enter')modalAskSubmit()">
606
638
  <button class="modal-qa-btn" id="modal-qa-btn" onclick="modalAskSubmit()">Ask</button>
@@ -850,23 +882,45 @@ function renderInbox(inbox) {
850
882
  <span>${escHtml(item.name)}</span><span>${item.age}</span>
851
883
  </div>
852
884
  <div class="inbox-preview" onclick="openModal(${i})" style="cursor:pointer">${escHtml(item.content.slice(0,200))}</div>
853
- <div style="display:flex;gap:6px;margin-top:6px">
854
- <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();persistInboxItem('${escHtml(item.name)}')">Persist as Note</button>
885
+ <div style="display:flex;gap:6px;margin-top:6px;align-items:center">
886
+ <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();promoteToKB('${escHtml(item.name)}')">Add to Knowledge Base</button>
855
887
  <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();openInboxInExplorer('${escHtml(item.name)}')">Open in Explorer</button>
856
888
  </div>
857
889
  </div>
858
890
  `).join('');
859
891
  }
860
892
 
861
- async function persistInboxItem(name) {
893
+ function promoteToKB(name) {
894
+ const categories = [
895
+ { id: 'architecture', label: 'Architecture' },
896
+ { id: 'conventions', label: 'Conventions' },
897
+ { id: 'project-notes', label: 'Project Notes' },
898
+ { id: 'build-reports', label: 'Build Reports' },
899
+ { id: 'reviews', label: 'Reviews' },
900
+ ];
901
+ const picker = '<div style="padding:16px 20px">' +
902
+ '<p style="font-size:13px;color:var(--text);margin-bottom:12px">Choose a category for <strong>' + escHtml(name) + '</strong>:</p>' +
903
+ '<div style="display:flex;flex-direction:column;gap:8px">' +
904
+ categories.map(c =>
905
+ '<button class="pr-pager-btn" style="font-size:12px;padding:8px 16px;text-align:left" onclick="doPromoteToKB(\'' + escHtml(name) + '\',\'' + c.id + '\')">' + c.label + '</button>'
906
+ ).join('') +
907
+ '</div></div>';
908
+ document.getElementById('modal-title').textContent = 'Add to Knowledge Base';
909
+ document.getElementById('modal-body').innerHTML = picker;
910
+ document.getElementById('modal').classList.add('open');
911
+ }
912
+
913
+ async function doPromoteToKB(name, category) {
862
914
  try {
863
- const res = await fetch('/api/inbox/persist', {
915
+ const res = await fetch('/api/inbox/promote-kb', {
864
916
  method: 'POST', headers: { 'Content-Type': 'application/json' },
865
- body: JSON.stringify({ name })
917
+ body: JSON.stringify({ name, category })
866
918
  });
867
919
  const data = await res.json();
868
920
  if (res.ok) {
921
+ closeModal();
869
922
  refresh();
923
+ refreshKnowledgeBase();
870
924
  } else {
871
925
  alert('Failed: ' + (data.error || 'unknown'));
872
926
  }
@@ -1002,7 +1056,9 @@ function openModal(i) {
1002
1056
  const item = inboxData[i];
1003
1057
  if (!item) return;
1004
1058
  document.getElementById('modal-title').textContent = item.name;
1005
- document.getElementById('modal-body').textContent = item.content;
1059
+ document.getElementById('modal-body').innerHTML =
1060
+ '<div style="margin-bottom:12px"><button class="pr-pager-btn" style="font-size:10px;padding:3px 10px" onclick="promoteToKB(\'' + escHtml(item.name) + '\')">Add to Knowledge Base</button></div>' +
1061
+ '<pre style="white-space:pre-wrap;word-wrap:break-word;margin:0;font-family:Consolas,monospace;font-size:12px;line-height:1.7;color:var(--muted)">' + escHtml(item.content) + '</pre>';
1006
1062
  document.getElementById('modal').classList.add('open');
1007
1063
  }
1008
1064
  function closeModal() {
@@ -1012,6 +1068,7 @@ function closeModal() {
1012
1068
  document.getElementById('modal-qa-thread').innerHTML = '';
1013
1069
  document.getElementById('modal-qa-input').value = '';
1014
1070
  document.getElementById('modal-qa-input').placeholder = 'Ask about this document (or select text first)...';
1071
+ document.getElementById('modal-qa-pill').style.display = 'none';
1015
1072
  document.getElementById('ask-selection-btn').style.display = 'none';
1016
1073
  }
1017
1074
 
@@ -1254,6 +1311,7 @@ async function refresh() {
1254
1311
  renderMetrics(data.metrics || {});
1255
1312
  renderWorkItems(data.workItems || []);
1256
1313
  renderSkills(data.skills || []);
1314
+ renderMcpServers(data.mcpServers || []);
1257
1315
  // Refresh KB and plans less frequently (every 3rd cycle = ~12s)
1258
1316
  if (!window._kbRefreshCount) window._kbRefreshCount = 0;
1259
1317
  if (window._kbRefreshCount++ % 3 === 0) { refreshKnowledgeBase(); refreshPlans(); }
@@ -1282,6 +1340,25 @@ function renderProjects(projects) {
1282
1340
 
1283
1341
  }
1284
1342
 
1343
+ // -- MCP Servers --
1344
+ function renderMcpServers(servers) {
1345
+ const el = document.getElementById('mcp-list');
1346
+ const countEl = document.getElementById('mcp-count');
1347
+ countEl.textContent = servers.length;
1348
+ if (!servers.length) {
1349
+ el.innerHTML = '<p class="empty">No MCP servers found. Add them to <code>~/.claude.json</code> and they\'ll appear here automatically.</p>';
1350
+ return;
1351
+ }
1352
+ el.innerHTML = '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">' +
1353
+ servers.map(s =>
1354
+ '<div style="font-size:11px;padding:5px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text)" title="' + escHtml(s.args || s.command) + '">' +
1355
+ escHtml(s.name) +
1356
+ '</div>'
1357
+ ).join('') +
1358
+ '</div>' +
1359
+ '<p style="font-size:10px;color:var(--muted);margin:0">Synced from <code style="color:var(--blue)">~/.claude.json</code> — add MCP servers there to make them available to all agents.</p>';
1360
+ }
1361
+
1285
1362
  // -- Squad Skills --
1286
1363
  function renderSkills(skills) {
1287
1364
  const el = document.getElementById('skills-list');
@@ -1645,11 +1722,11 @@ function cmdParseInput(raw) {
1645
1722
  if (/--parallel\b/i.test(text)) {
1646
1723
  result.branchStrategy = 'parallel';
1647
1724
  text = text.replace(/--parallel\b/i, '').trim();
1648
- } else if (/--shared\b/i.test(text)) {
1725
+ } else if (/--stack\b/i.test(text)) {
1649
1726
  result.branchStrategy = 'shared-branch';
1650
- text = text.replace(/--shared\b/i, '').trim();
1727
+ text = text.replace(/--stack\b/i, '').trim();
1651
1728
  } else {
1652
- result.branchStrategy = 'shared-branch'; // default
1729
+ result.branchStrategy = 'parallel'; // default — items without depends_on get independent branches
1653
1730
  }
1654
1731
  } else if (/^\/prd\b/i.test(text)) {
1655
1732
  result.intent = 'prd';
@@ -1712,19 +1789,19 @@ function cmdRenderMeta() {
1712
1789
  let chips = [];
1713
1790
 
1714
1791
  // Intent chip
1715
- const intentLabels = { 'work-item': 'Work Item', 'note': 'Note', 'prd': 'PRD Item', 'plan': 'Plan → PRD → Dispatch' };
1716
- chips.push('<span class="cmd-chip intent">' + intentLabels[parsed.intent] + '</span>');
1792
+ if (parsed.intent === 'plan') {
1793
+ const strategy = parsed.branchStrategy || 'parallel';
1794
+ const stratLabel = strategy === 'parallel' ? 'parallel branches' : 'stacked';
1795
+ chips.push('<span class="cmd-chip" style="background:var(--purple,#a855f7);color:#fff">Plan → PRD → Dispatch (' + stratLabel + ')</span>');
1796
+ } else {
1797
+ const intentLabels = { 'work-item': 'Work Item', 'note': 'Note', 'prd': 'PRD Item' };
1798
+ chips.push('<span class="cmd-chip intent">' + intentLabels[parsed.intent] + '</span>');
1799
+ }
1717
1800
 
1718
1801
  // Type chip (only for work items)
1719
1802
  if (parsed.intent === 'work-item') {
1720
1803
  chips.push('<span class="cmd-chip">' + parsed.type + '</span>');
1721
1804
  }
1722
- // Pipeline chip for /plan
1723
- if (parsed.intent === 'plan') {
1724
- const strategy = parsed.branchStrategy || 'shared-branch';
1725
- const stratLabel = strategy === 'parallel' ? 'parallel branches' : 'shared branch';
1726
- chips.push('<span class="cmd-chip" style="background:var(--purple,#a855f7);color:#fff">plan → prd → agents (' + stratLabel + ')</span>');
1727
- }
1728
1805
 
1729
1806
  // Priority chip
1730
1807
  chips.push('<span class="cmd-chip priority-' + parsed.priority + '">' + parsed.priority + ' priority</span>');
@@ -1832,8 +1909,30 @@ function cmdAutoResize() {
1832
1909
  ta.style.height = Math.min(ta.scrollHeight, 200) + 'px';
1833
1910
  }
1834
1911
 
1912
+ function cmdUpdateHighlight() {
1913
+ const text = document.getElementById('cmd-input').value;
1914
+ const hl = document.getElementById('cmd-highlight');
1915
+ if (!text) { hl.innerHTML = ''; return; }
1916
+ // Escape HTML then wrap tokens with highlight spans
1917
+ let html = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1918
+ html = html.replace(/(\/(?:plan|prd|note|decide)\b)/gi, '<span class="hl-cmd">$1</span>');
1919
+ html = html.replace(/(@\w+)/g, '<span class="hl-mention">$1</span>');
1920
+ html = html.replace(/(![a-z]+\b)/gi, '<span class="hl-priority">$1</span>');
1921
+ html = html.replace(/(#\S+)/g, '<span class="hl-project">$1</span>');
1922
+ html = html.replace(/(--(?:stack|parallel)\b)/gi, '<span class="hl-flag">$1</span>');
1923
+ hl.innerHTML = html + '\n'; // trailing newline prevents layout shift
1924
+ }
1925
+
1926
+ function syncHighlightScroll() {
1927
+ const ta = document.getElementById('cmd-input');
1928
+ const hl = document.getElementById('cmd-highlight');
1929
+ hl.scrollTop = ta.scrollTop;
1930
+ hl.scrollLeft = ta.scrollLeft;
1931
+ }
1932
+
1835
1933
  function cmdInputChanged() {
1836
1934
  cmdAutoResize();
1935
+ cmdUpdateHighlight();
1837
1936
  cmdRenderMeta();
1838
1937
 
1839
1938
  // Check for @ mention or # project trigger
@@ -2014,7 +2113,7 @@ async function cmdSubmitPlan(parsed) {
2014
2113
  title: parsed.title,
2015
2114
  description: parsed.description || '',
2016
2115
  priority: parsed.priority,
2017
- branch_strategy: parsed.branchStrategy || 'shared-branch',
2116
+ branch_strategy: parsed.branchStrategy || 'parallel',
2018
2117
  };
2019
2118
  if (parsed.project) body.project = parsed.project;
2020
2119
  if (parsed.agents.length === 1) body.agent = parsed.agents[0];
@@ -2053,36 +2152,98 @@ async function cmdSubmitPrd(parsed) {
2053
2152
 
2054
2153
  let _modalDocContext = { title: '', content: '', selection: '' };
2055
2154
 
2056
- // Track text selection in modal body for the floating "Ask about this" button
2155
+ // Track text selection anywhere in content areas for the floating "Ask about this" button
2057
2156
  document.addEventListener('mouseup', function(e) {
2058
- const btn = document.getElementById('ask-selection-btn');
2059
- const modalBody = document.getElementById('modal-body');
2060
- const sel = window.getSelection();
2061
- const text = sel?.toString()?.trim();
2062
-
2063
- if (text && text.length > 5 && modalBody?.contains(sel?.anchorNode)) {
2064
- _modalDocContext.selection = text;
2065
- btn.style.display = 'block';
2066
- btn.style.left = e.pageX + 'px';
2067
- btn.style.top = (e.pageY - 35) + 'px';
2068
- } else {
2069
- btn.style.display = 'none';
2070
- }
2157
+ // Small delay to let the selection finalize
2158
+ setTimeout(() => {
2159
+ const btn = document.getElementById('ask-selection-btn');
2160
+ if (!btn) return;
2161
+ const sel = window.getSelection();
2162
+ const text = sel?.toString()?.trim();
2163
+
2164
+ if (!text || text.length < 3) {
2165
+ btn.style.display = 'none';
2166
+ return;
2167
+ }
2168
+
2169
+ // Check if selection is in any content area: modal body, detail panel, notes, kb
2170
+ const anchor = sel?.anchorNode;
2171
+ if (!anchor) { btn.style.display = 'none'; return; }
2172
+
2173
+ const modalBody = document.getElementById('modal-body');
2174
+ const detailContent = document.getElementById('detail-content');
2175
+ const isInModal = modalBody?.contains(anchor);
2176
+ const isInDetail = detailContent?.contains(anchor);
2177
+
2178
+ if (isInModal || isInDetail) {
2179
+ _modalDocContext.selection = text;
2180
+ // If we don't have document context yet (e.g. detail panel), capture it
2181
+ if (!_modalDocContext.content && isInDetail) {
2182
+ _modalDocContext.content = detailContent.textContent || '';
2183
+ _modalDocContext.title = document.getElementById('detail-agent-name')?.textContent || 'Agent Detail';
2184
+ }
2185
+ btn.style.display = 'block';
2186
+ // Position near the cursor but within viewport
2187
+ const x = Math.min(e.clientX, window.innerWidth - 140);
2188
+ const y = Math.max(e.clientY - 35, 10);
2189
+ btn.style.left = x + 'px';
2190
+ btn.style.top = y + 'px';
2191
+ btn.style.position = 'fixed';
2192
+ } else {
2193
+ btn.style.display = 'none';
2194
+ }
2195
+ }, 10);
2071
2196
  });
2072
2197
 
2073
2198
  function modalAskAboutSelection() {
2074
2199
  document.getElementById('ask-selection-btn').style.display = 'none';
2200
+
2201
+ // If the modal isn't open but we have a selection (from detail panel), open modal for Q&A
2202
+ const modal = document.getElementById('modal');
2203
+ if (!modal.classList.contains('open')) {
2204
+ document.getElementById('modal-title').textContent = 'Q&A: ' + (_modalDocContext.title || 'Document');
2205
+ document.getElementById('modal-body').textContent = _modalDocContext.content.slice(0, 3000) + (_modalDocContext.content.length > 3000 ? '\n\n...(truncated for display)' : '');
2206
+ modal.classList.add('open');
2207
+ }
2208
+
2209
+ // Show the selection pill
2210
+ const pill = document.getElementById('modal-qa-pill');
2211
+ const pillText = document.getElementById('modal-qa-pill-text');
2212
+ const sel = _modalDocContext.selection || '';
2213
+ if (sel) {
2214
+ pillText.textContent = sel.slice(0, 80) + (sel.length > 80 ? '...' : '');
2215
+ pill.style.display = 'flex';
2216
+ }
2217
+
2075
2218
  const input = document.getElementById('modal-qa-input');
2076
2219
  input.value = '';
2077
- input.placeholder = 'Ask about: "' + _modalDocContext.selection.slice(0, 60) + '..."';
2220
+ input.placeholder = 'What do you want to know about this?';
2078
2221
  input.focus();
2079
2222
  }
2080
2223
 
2224
+ function clearQaSelection() {
2225
+ _modalDocContext.selection = '';
2226
+ document.getElementById('modal-qa-pill').style.display = 'none';
2227
+ document.getElementById('modal-qa-input').placeholder = 'Ask about this document (or select text first)...';
2228
+ }
2229
+
2081
2230
  async function modalAskSubmit() {
2082
2231
  const input = document.getElementById('modal-qa-input');
2083
2232
  const question = input.value.trim();
2084
2233
  if (!question) return;
2085
- if (!_modalDocContext.content) return;
2234
+
2235
+ // Capture content from modal body if not already set
2236
+ if (!_modalDocContext.content) {
2237
+ const body = document.getElementById('modal-body');
2238
+ if (body) {
2239
+ _modalDocContext.content = body.textContent || body.innerText || '';
2240
+ _modalDocContext.title = document.getElementById('modal-title')?.textContent || '';
2241
+ }
2242
+ }
2243
+ if (!_modalDocContext.content) {
2244
+ showToast('cmd-toast', 'No document content to ask about', false);
2245
+ return;
2246
+ }
2086
2247
 
2087
2248
  const thread = document.getElementById('modal-qa-thread');
2088
2249
  const btn = document.getElementById('modal-qa-btn');
@@ -2131,6 +2292,8 @@ async function modalAskSubmit() {
2131
2292
  }
2132
2293
 
2133
2294
  _modalDocContext.selection = ''; // Clear selection after asking
2295
+ document.getElementById('modal-qa-pill').style.display = 'none';
2296
+ document.getElementById('modal-qa-input').placeholder = 'Ask another question...';
2134
2297
  btn.disabled = false;
2135
2298
  thread.scrollTop = thread.scrollHeight;
2136
2299
  input.focus();
package/dashboard.js CHANGED
@@ -442,6 +442,20 @@ function getWorkItems() {
442
442
  return allItems;
443
443
  }
444
444
 
445
+ function getMcpServers() {
446
+ try {
447
+ const home = process.env.USERPROFILE || process.env.HOME || '';
448
+ const claudeJsonPath = path.join(home, '.claude.json');
449
+ const data = JSON.parse(safeRead(claudeJsonPath) || '{}');
450
+ const servers = data.mcpServers || {};
451
+ return Object.entries(servers).map(([name, cfg]) => ({
452
+ name,
453
+ command: cfg.command || '',
454
+ args: (cfg.args || []).slice(-1)[0] || '',
455
+ }));
456
+ } catch { return []; }
457
+ }
458
+
445
459
  function getStatus() {
446
460
  const prdInfo = getPrdInfo();
447
461
  return {
@@ -458,6 +472,7 @@ function getStatus() {
458
472
  metrics: getMetrics(),
459
473
  workItems: getWorkItems(),
460
474
  skills: getSkills(),
475
+ mcpServers: getMcpServers(),
461
476
  projects: PROJECTS.map(p => ({ name: p.name, path: p.localPath, description: p.description || '' })),
462
477
  timestamp: new Date().toISOString(),
463
478
  };
@@ -758,7 +773,7 @@ const server = http.createServer(async (req, res) => {
758
773
  priority: body.priority || 'high', description: body.description || '',
759
774
  status: 'pending', created: new Date().toISOString(), createdBy: 'dashboard',
760
775
  chain: 'plan-to-prd',
761
- branchStrategy: body.branch_strategy || 'shared-branch',
776
+ branchStrategy: body.branch_strategy || 'parallel',
762
777
  };
763
778
  if (body.project) item.project = body.project;
764
779
  if (body.agent) item.agent = body.agent;
@@ -1095,7 +1110,12 @@ ${body.question}
1095
1110
 
1096
1111
  ## Instructions
1097
1112
 
1098
- Answer concisely and directly. Reference specific parts of the document. If the answer isn't in the document, say so. Use markdown formatting.`;
1113
+ Answer concisely and directly. Follow these rules:
1114
+ 1. **Cite sources**: When the document includes file paths, line numbers, PR URLs, or code references — include them in your answer. Format: \`(source: path/to/file.ts:42)\`
1115
+ 2. **Quote the document**: Reference the exact text that supports your answer.
1116
+ 3. **Flag missing sources**: If the document makes a claim without a source reference (no file path, no PR link, no line number), say: "Note: this claim has no source reference in the document — verify independently."
1117
+ 4. **Be honest**: If the document doesn't contain the answer, say so clearly. Don't speculate beyond what's written.
1118
+ 5. Use markdown formatting.`;
1099
1119
 
1100
1120
  const sysPrompt = 'You are a concise technical assistant. Answer based on the document provided. No preamble.';
1101
1121
 
@@ -1189,6 +1209,45 @@ Answer concisely and directly. Reference specific parts of the document. If the
1189
1209
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1190
1210
  }
1191
1211
 
1212
+ // POST /api/inbox/promote-kb — promote an inbox item to the knowledge base
1213
+ if (req.method === 'POST' && req.url === '/api/inbox/promote-kb') {
1214
+ try {
1215
+ const body = await readBody(req);
1216
+ const { name, category } = body;
1217
+ if (!name) return jsonReply(res, 400, { error: 'name required' });
1218
+ const validCategories = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
1219
+ if (!category || !validCategories.includes(category)) {
1220
+ return jsonReply(res, 400, { error: 'category required: ' + validCategories.join(', ') });
1221
+ }
1222
+
1223
+ const inboxPath = path.join(SQUAD_DIR, 'notes', 'inbox', name);
1224
+ const content = safeRead(inboxPath);
1225
+ if (!content) return jsonReply(res, 404, { error: 'inbox item not found' });
1226
+
1227
+ // Add frontmatter if not present
1228
+ const today = new Date().toISOString().slice(0, 10);
1229
+ let kbContent = content;
1230
+ if (!content.startsWith('---')) {
1231
+ const titleMatch = content.match(/^#+ (.+)$/m);
1232
+ const title = titleMatch ? titleMatch[1].trim() : name.replace('.md', '');
1233
+ kbContent = `---\ntitle: ${title}\ncategory: ${category}\ndate: ${today}\nsource: inbox/${name}\n---\n\n${content}`;
1234
+ }
1235
+
1236
+ // Write to knowledge base
1237
+ const kbDir = path.join(SQUAD_DIR, 'knowledge', category);
1238
+ if (!fs.existsSync(kbDir)) fs.mkdirSync(kbDir, { recursive: true });
1239
+ const kbFile = path.join(kbDir, name);
1240
+ safeWrite(kbFile, kbContent);
1241
+
1242
+ // Move inbox item to archive
1243
+ const archiveDir = path.join(SQUAD_DIR, 'notes', 'archive');
1244
+ if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
1245
+ try { fs.renameSync(inboxPath, path.join(archiveDir, `kb-${category}-${name}`)); } catch {}
1246
+
1247
+ return jsonReply(res, 200, { ok: true, category, file: name });
1248
+ } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1249
+ }
1250
+
1192
1251
  // POST /api/inbox/open — open inbox file in Windows explorer
1193
1252
  if (req.method === 'POST' && req.url === '/api/inbox/open') {
1194
1253
  try {
@@ -87,7 +87,7 @@ When the engine restarts, the in-memory `activeProcesses` Map is lost. Active di
87
87
 
88
88
  **Dispatch 1773292681199** — Dallas, central work item, auto-route:
89
89
 
90
- 1. Engine spawns `node spawn-agent.js prompt.md sysprompt.md --output-format stream-json --verbose --permission-mode bypassPermissions --mcp-config mcp-servers.json`
90
+ 1. Engine spawns `node spawn-agent.js prompt.md sysprompt.md --output-format stream-json --verbose --permission-mode bypassPermissions`
91
91
  2. spawn-agent.js resolves `cli.js`, spawns `node cli.js -p --system-prompt <content> ...`
92
92
  3. Prompt piped via stdin — no shell interpretation
93
93
  4. MCP servers connect (azure-ado, azure-kusto, mobile, DevBox)
package/engine.js CHANGED
@@ -212,39 +212,6 @@ function getInboxFiles() {
212
212
  try { return fs.readdirSync(INBOX_DIR).filter(f => f.endsWith('.md')); } catch { return []; }
213
213
  }
214
214
 
215
- // ─── MCP Server Sync ─────────────────────────────────────────────────────────
216
-
217
- const MCP_SERVERS_PATH = path.join(SQUAD_DIR, 'mcp-servers.json');
218
-
219
- function syncMcpServers() {
220
- // Sync MCP servers from ~/.claude.json into squad's mcp-servers.json
221
- const home = process.env.USERPROFILE || process.env.HOME || '';
222
- const claudeJsonPath = path.join(home, '.claude.json');
223
-
224
- if (!fs.existsSync(claudeJsonPath)) {
225
- console.log(' ~/.claude.json not found — skipping MCP sync');
226
- return false;
227
- }
228
-
229
- try {
230
- const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
231
- const servers = claudeJson.mcpServers;
232
- if (!servers || Object.keys(servers).length === 0) {
233
- console.log(' No MCP servers found in ~/.claude.json');
234
- return false;
235
- }
236
-
237
- safeWrite(MCP_SERVERS_PATH, { mcpServers: servers });
238
- const names = Object.keys(servers);
239
- console.log(` MCP servers synced (${names.length}): ${names.join(', ')}`);
240
- log('info', `Synced ${names.length} MCP servers from ~/.claude.json: ${names.join(', ')}`);
241
- return true;
242
- } catch (e) {
243
- console.log(` MCP sync failed: ${e.message}`);
244
- return false;
245
- }
246
- }
247
-
248
215
  // ─── Skills ──────────────────────────────────────────────────────────────────
249
216
 
250
217
  const SKILLS_DIR = path.join(SQUAD_DIR, 'skills');
@@ -313,6 +280,32 @@ function getSkillIndex() {
313
280
  } catch { return ''; }
314
281
  }
315
282
 
283
+ function getKnowledgeBaseIndex() {
284
+ try {
285
+ const kbDir = path.join(SQUAD_DIR, 'knowledge');
286
+ if (!fs.existsSync(kbDir)) return '';
287
+ const categories = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
288
+ let entries = [];
289
+ for (const cat of categories) {
290
+ const catDir = path.join(kbDir, cat);
291
+ const files = safeReadDir(catDir).filter(f => f.endsWith('.md'));
292
+ for (const f of files) {
293
+ const content = safeRead(path.join(catDir, f)) || '';
294
+ const titleMatch = content.match(/^#\s+(.+)/m);
295
+ const title = titleMatch ? titleMatch[1].trim() : f.replace(/\.md$/, '');
296
+ entries.push({ cat, file: f, title });
297
+ }
298
+ }
299
+ if (entries.length === 0) return '';
300
+ let index = '## Knowledge Base Reference\n\n';
301
+ index += 'Deep-reference docs from past work. Read the file if you need detail.\n\n';
302
+ for (const e of entries) {
303
+ index += `- \`knowledge/${e.cat}/${e.file}\` — ${e.title}\n`;
304
+ }
305
+ return index + '\n';
306
+ } catch { return ''; }
307
+ }
308
+
316
309
  function getPrs(project) {
317
310
  if (project) {
318
311
  const prPath = project.workSources?.pullRequests?.path;
@@ -412,7 +405,8 @@ function renderPlaybook(type, vars) {
412
405
  content += `- What you learned about the codebase\n`;
413
406
  content += `- Patterns you discovered or established\n`;
414
407
  content += `- Gotchas or warnings for future agents\n`;
415
- content += `- Conventions to follow\n\n`;
408
+ content += `- Conventions to follow\n`;
409
+ content += `- **SOURCE REFERENCES for every finding** — file paths with line numbers, PR URLs, API endpoints, config keys. Format: \`(source: path/to/file.ts:42)\` or \`(source: PR-12345)\`. Without references, findings cannot be verified.\n\n`;
416
410
  content += `### Skill Extraction (IMPORTANT)\n\n`;
417
411
  content += `If during this task you discovered a **repeatable workflow** — a multi-step procedure, workaround, build process, or pattern that other agents should follow in similar situations — output it as a fenced skill block. The engine will automatically extract it.\n\n`;
418
412
  content += `Format your skill as a fenced code block with the \`skill\` language tag:\n\n`;
@@ -574,6 +568,12 @@ function buildAgentContext(agentId, config, project) {
574
568
  context += skillIndex + '\n';
575
569
  }
576
570
 
571
+ // Knowledge base index (paths + titles only — agents can Read if needed)
572
+ const kbIndex = getKnowledgeBaseIndex();
573
+ if (kbIndex) {
574
+ context += kbIndex + '\n';
575
+ }
576
+
577
577
  // Team notes (the big one — can be 50KB)
578
578
  if (notes) {
579
579
  context += `## Team Notes (MUST READ)\n\n${notes}\n\n`;
@@ -669,11 +669,8 @@ function spawnAgent(dispatchItem, config) {
669
669
  args.push('--allowedTools', claudeConfig.allowedTools);
670
670
  }
671
671
 
672
- // MCP servers pass config file if it exists
673
- const mcpConfigPath = claudeConfig.mcpConfig || path.join(SQUAD_DIR, 'mcp-servers.json');
674
- if (fs.existsSync(mcpConfigPath)) {
675
- args.push('--mcp-config', mcpConfigPath);
676
- }
672
+ // MCP servers: agents inherit from ~/.claude.json directly as Claude Code processes.
673
+ // No --mcp-config needed avoids redundant config and ensures agents always have latest servers.
677
674
 
678
675
  log('info', `Spawning agent: ${agentId} (${id}) in ${cwd}`);
679
676
  log('info', `Task type: ${type} | Branch: ${branchName || 'none'}`);
@@ -3980,7 +3977,7 @@ async function tickInner() {
3980
3977
  // 2. Consolidate inbox
3981
3978
  consolidateInbox(config);
3982
3979
 
3983
- // 2.5. Periodic cleanup (every 10 ticks = ~5 minutes)
3980
+ // 2.5. Periodic cleanup + MCP sync (every 10 ticks = ~5 minutes)
3984
3981
  if (tickCount % 10 === 0) {
3985
3982
  runCleanup(config);
3986
3983
  }
@@ -4065,9 +4062,6 @@ const commands = {
4065
4062
  const config = getConfig();
4066
4063
  const interval = config.engine?.tickInterval || 60000;
4067
4064
 
4068
- // Sync MCP servers from Claude Code
4069
- syncMcpServers();
4070
-
4071
4065
  // Validate project paths
4072
4066
  const projects = getProjects(config);
4073
4067
  for (const p of projects) {
@@ -4542,7 +4536,7 @@ const commands = {
4542
4536
  },
4543
4537
 
4544
4538
  'mcp-sync'() {
4545
- syncMcpServers();
4539
+ console.log('MCP servers are read directly from ~/.claude.json — no sync needed.');
4546
4540
  },
4547
4541
 
4548
4542
  discover() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/squad",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.squad/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "squad": "bin/squad.js"
@@ -38,6 +38,7 @@ Write your findings to `{{team_root}}/notes/inbox/{{agent_id}}-explore-{{task_id
38
38
  - **Dependencies**: what depends on what
39
39
  - **Gaps**: anything missing, broken, or unclear
40
40
  - **Recommendations**: suggestions for the team
41
+ - **Source References**: for EVERY finding, include the source — file paths, line numbers, PR URLs, API endpoints, config keys. Format: `(source: path/to/file.ts:42)` or `(source: PR-12345)`. This is critical — other agents and humans need to verify your findings.
41
42
 
42
43
  ### 5. Create Deliverable (if the task asks for one)
43
44
  If the task asks you to write a design doc, architecture doc, or any durable artifact: