@yemi33/squad 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dashboard.html CHANGED
@@ -47,6 +47,43 @@
47
47
  .status-badge.working { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
48
48
  .status-badge.done { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
49
49
  .agent-action { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
50
+ .modal-qa { border-top: 1px solid var(--border); padding: 10px 20px; }
51
+ .modal-qa-thread { max-height: 200px; overflow-y: auto; margin-bottom: 8px; }
52
+ .modal-qa-q { font-size: 12px; color: var(--blue); margin-bottom: 4px; font-weight: 600; }
53
+ .modal-qa-q .selection-ref { font-weight: 400; color: var(--muted); font-style: italic; display: block; font-size: 10px; margin-top: 2px; }
54
+ .modal-qa-a { font-size: 12px; color: var(--text); margin-bottom: 12px; padding: 8px 10px; background: var(--surface2); border-radius: 6px; border-left: 2px solid var(--blue); white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
55
+ .modal-qa-loading { font-size: 11px; color: var(--muted); padding: 8px 10px; display: flex; align-items: center; gap: 8px; }
56
+ .modal-qa-loading .dot-pulse { display: inline-flex; gap: 3px; }
57
+ .modal-qa-loading .dot-pulse span { width: 5px; height: 5px; background: var(--blue); border-radius: 50%; animation: dotPulse 1.2s infinite; }
58
+ .modal-qa-loading .dot-pulse span:nth-child(2) { animation-delay: 0.2s; }
59
+ .modal-qa-loading .dot-pulse span:nth-child(3) { animation-delay: 0.4s; }
60
+ @keyframes dotPulse { 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); } 40% { opacity: 1; transform: scale(1); } }
61
+ .modal-qa-input-wrap { display: flex; gap: 6px; }
62
+ .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
+ .modal-qa-input:focus { border-color: var(--blue); outline: none; }
64
+ .modal-qa-btn { background: var(--blue); color: #fff; border: none; border-radius: 4px; padding: 6px 14px; font-size: 12px; cursor: pointer; }
65
+ .modal-qa-btn:hover { opacity: 0.9; }
66
+ .modal-qa-btn:disabled { opacity: 0.4; cursor: not-allowed; }
67
+ .ask-selection-btn { display: none; position: fixed; z-index: 500; background: var(--blue); color: #fff; font-size: 11px; padding: 5px 12px; border-radius: 4px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.3); }
68
+ .ask-selection-btn:hover { opacity: 0.9; }
69
+ .plan-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 12px; margin-bottom: 8px; }
70
+ .plan-card.awaiting { border-left: 3px solid var(--yellow, #d29922); }
71
+ .plan-card.approved { border-left: 3px solid var(--green); }
72
+ .plan-card.rejected { border-left: 3px solid var(--red); opacity: 0.6; }
73
+ .plan-card.revision-requested { border-left: 3px solid var(--purple, #a855f7); }
74
+ .plan-card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
75
+ .plan-card-title { font-size: 13px; font-weight: 600; color: var(--text); }
76
+ .plan-card-meta { font-size: 10px; color: var(--muted); margin-top: 4px; display: flex; gap: 8px; flex-wrap: wrap; }
77
+ .plan-card-actions { display: flex; gap: 4px; margin-top: 8px; flex-wrap: wrap; }
78
+ .plan-btn { font-size: 11px; padding: 4px 10px; border-radius: 4px; cursor: pointer; border: 1px solid var(--border); background: var(--surface); color: var(--text); transition: all 0.15s; }
79
+ .plan-btn:hover { border-color: var(--text); }
80
+ .plan-btn.approve { color: var(--green); border-color: var(--green); }
81
+ .plan-btn.approve:hover { background: rgba(63,185,80,0.1); }
82
+ .plan-btn.revise { color: var(--yellow, #d29922); border-color: var(--yellow, #d29922); }
83
+ .plan-btn.revise:hover { background: rgba(210,153,34,0.1); }
84
+ .plan-btn.reject { color: var(--red); border-color: var(--red); }
85
+ .plan-btn.reject:hover { background: rgba(248,81,73,0.1); }
86
+ .plan-feedback-input { width: 100%; margin-top: 6px; padding: 6px 8px; font-size: 11px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text); font-family: inherit; resize: vertical; min-height: 50px; }
50
87
  .token-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; margin-bottom: 12px; }
51
88
  .token-tile { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }
52
89
  .token-tile-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
@@ -450,10 +487,12 @@
450
487
  <div class="cmd-hints">
451
488
  <span><code>@agent</code> assign</span>
452
489
  <span><code>@everyone</code> fan-out</span>
490
+ <span><code>#project</code> target</span>
453
491
  <span><code>!high</code> / <code>!low</code> priority</span>
454
- <span><code>/note</code> team note</span>
492
+ <span><code>/plan</code> feature plan</span>
455
493
  <span><code>/prd</code> PRD item</span>
456
- <span><code>#project</code> target project</span>
494
+ <span><code>/note</code> team note</span>
495
+ <span>or just type a task</span>
457
496
  <button class="cmd-history-btn" onclick="cmdShowHistory()">Past Commands</button>
458
497
  </div>
459
498
  <div class="cmd-toast" id="cmd-toast"></div>
@@ -481,6 +520,11 @@
481
520
  <div id="pr-content"><p class="pr-empty">No pull requests yet.</p></div>
482
521
  </section>
483
522
 
523
+ <section>
524
+ <h2>Plans <span class="count" id="plans-count">0</span></h2>
525
+ <div id="plans-list"><p class="empty">No plans yet. Use /plan in the command center to create one.</p></div>
526
+ </section>
527
+
484
528
  <section>
485
529
  <h2>Notes Inbox <span class="count" id="inbox-count">0</span></h2>
486
530
  <div class="inbox-list" id="inbox-list">Loading...</div>
@@ -555,9 +599,19 @@
555
599
  </div>
556
600
  </div>
557
601
  <div class="modal-body" id="modal-body"></div>
602
+ <div class="modal-qa" id="modal-qa">
603
+ <div class="modal-qa-thread" id="modal-qa-thread"></div>
604
+ <div class="modal-qa-input-wrap">
605
+ <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
+ <button class="modal-qa-btn" id="modal-qa-btn" onclick="modalAskSubmit()">Ask</button>
607
+ </div>
608
+ </div>
558
609
  </div>
559
610
  </div>
560
611
 
612
+ <!-- Floating "Ask about selection" button -->
613
+ <div class="ask-selection-btn" id="ask-selection-btn" onclick="modalAskAboutSelection()">Ask about this</div>
614
+
561
615
  <script>
562
616
  let inboxData = [];
563
617
  let agentData = [];
@@ -668,7 +722,26 @@ function renderDetailContent(detail, tab) {
668
722
  } else if (tab === 'charter') {
669
723
  el.innerHTML = '<div class="section">' + escHtml(detail.charter || 'No charter found.') + '</div>';
670
724
  } else if (tab === 'history') {
671
- el.innerHTML = '<div class="section">' + escHtml(detail.history || 'No history yet.') + '</div>';
725
+ let html = '';
726
+ // Recent dispatch results
727
+ if (detail.recentDispatches && detail.recentDispatches.length > 0) {
728
+ html += '<h4>Recent Dispatches</h4><table class="pr-table" style="margin-bottom:16px"><thead><tr><th>Task</th><th>Type</th><th>Result</th><th>Completed</th></tr></thead><tbody>';
729
+ detail.recentDispatches.forEach(d => {
730
+ const isError = d.result === 'error';
731
+ const color = isError ? 'var(--red)' : 'var(--green)';
732
+ const reason = d.reason ? ' title="' + escHtml(d.reason) + '"' : '';
733
+ html += '<tr>' +
734
+ '<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(d.task) + '">' + escHtml(d.task.slice(0, 80)) + '</td>' +
735
+ '<td><span class="dispatch-type ' + d.type + '">' + escHtml(d.type) + '</span></td>' +
736
+ '<td style="color:' + color + '"' + reason + '>' + escHtml(d.result) + (isError && d.reason ? ' <span style="font-size:10px;color:var(--muted)">(' + escHtml(d.reason.slice(0, 50)) + ')</span>' : '') + '</td>' +
737
+ '<td style="font-size:10px;color:var(--muted)">' + (d.completed_at ? new Date(d.completed_at).toLocaleString() : '') + '</td>' +
738
+ '</tr>';
739
+ });
740
+ html += '</tbody></table>';
741
+ }
742
+ // Raw history.md
743
+ html += '<h4>Task History</h4><div class="section">' + escHtml(detail.history || 'No history yet.') + '</div>';
744
+ el.innerHTML = html;
672
745
  } else if (tab === 'output') {
673
746
  el.innerHTML = '<div class="section">' + escHtml(detail.outputLog || 'No output log. The coordinator will save agent output here when tasks complete.') + '</div>';
674
747
  }
@@ -932,7 +1005,15 @@ function openModal(i) {
932
1005
  document.getElementById('modal-body').textContent = item.content;
933
1006
  document.getElementById('modal').classList.add('open');
934
1007
  }
935
- function closeModal() { document.getElementById('modal').classList.remove('open'); }
1008
+ function closeModal() {
1009
+ document.getElementById('modal').classList.remove('open');
1010
+ // Clear Q&A state
1011
+ _modalDocContext = { title: '', content: '', selection: '' };
1012
+ document.getElementById('modal-qa-thread').innerHTML = '';
1013
+ document.getElementById('modal-qa-input').value = '';
1014
+ document.getElementById('modal-qa-input').placeholder = 'Ask about this document (or select text first)...';
1015
+ document.getElementById('ask-selection-btn').style.display = 'none';
1016
+ }
936
1017
 
937
1018
  document.addEventListener('keydown', e => {
938
1019
  if (e.key === 'Escape') { closeDetail(); closeModal(); }
@@ -1173,9 +1254,9 @@ async function refresh() {
1173
1254
  renderMetrics(data.metrics || {});
1174
1255
  renderWorkItems(data.workItems || []);
1175
1256
  renderSkills(data.skills || []);
1176
- // Refresh KB less frequently (every 3rd cycle = ~12s)
1257
+ // Refresh KB and plans less frequently (every 3rd cycle = ~12s)
1177
1258
  if (!window._kbRefreshCount) window._kbRefreshCount = 0;
1178
- if (window._kbRefreshCount++ % 3 === 0) refreshKnowledgeBase();
1259
+ if (window._kbRefreshCount++ % 3 === 0) { refreshKnowledgeBase(); refreshPlans(); }
1179
1260
  } catch(e) { console.error('refresh error', e); }
1180
1261
  }
1181
1262
 
@@ -1526,7 +1607,7 @@ function detectWorkItemType(text) {
1526
1607
  const t = text.toLowerCase();
1527
1608
  const patterns = [
1528
1609
  { 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'] },
1529
- { type: 'explore', words: ['explore', 'investigate', 'understand', 'analyze', 'audit', 'document', 'architecture', 'how does', 'what is', 'look into', 'research', 'survey', 'map out', 'codebase'] },
1610
+ { type: 'explore', words: ['explore', 'investigate', 'understand', 'analyze', 'audit', 'document', 'architecture', 'how does', 'what is', 'look into', 'research', 'survey', 'map out', 'codebase', 'make a note of', 'find out'] },
1530
1611
  { type: 'fix', words: ['fix', 'bug', 'broken', 'crash', 'error', 'issue', 'patch', 'repair', 'resolve', 'regression', 'failing', 'doesn\'t work', 'not working'] },
1531
1612
  { type: 'review', words: ['review', 'code review', 'check pr', 'look at pr', 'audit code', 'inspect'] },
1532
1613
  { type: 'test', words: ['test', 'write tests', 'add tests', 'unit test', 'e2e test', 'coverage', 'testing', 'build', 'run locally', 'localhost', 'start the', 'spin up', 'verify', 'check if it works'] },
@@ -1557,9 +1638,9 @@ function cmdParseInput(raw) {
1557
1638
  if (/^\/decide\b/i.test(text) || /^\/note\b/i.test(text) || rememberPattern.test(text)) {
1558
1639
  result.intent = 'note';
1559
1640
  text = text.replace(/^\/decide\s*/i, '').replace(/^\/note\s*/i, '').replace(rememberPattern, '').trim();
1560
- } else if (/^\/plan\b/i.test(text)) {
1641
+ } else if (/^\/plan\b/i.test(text) || /^(make a plan|plan out|plan for|plan how|create a plan|design a plan|come up with a plan|draft a plan|write a plan)\b/i.test(text)) {
1561
1642
  result.intent = 'plan';
1562
- text = text.replace(/^\/plan\s*/i, '');
1643
+ text = text.replace(/^\/plan\s*/i, '').replace(/^(make a plan for|plan out how|plan for how|plan how|create a plan for|design a plan for|come up with a plan for|draft a plan for|write a plan for|make a plan|plan out|create a plan|design a plan|come up with a plan|draft a plan|write a plan)\s*/i, '').trim();
1563
1644
  // Extract branch strategy flag
1564
1645
  if (/--parallel\b/i.test(text)) {
1565
1646
  result.branchStrategy = 'parallel';
@@ -1968,6 +2049,249 @@ async function cmdSubmitPrd(parsed) {
1968
2049
  const projLabel = (parsed.projects || []).length > 0 ? ' (' + parsed.projects.join(', ') + ')' : '';
1969
2050
  showToast('cmd-toast', 'PRD item ' + (data.id || id) + ' added' + projLabel, true);
1970
2051
  }
2052
+ // ─── Modal Q&A (Ask about document) ──────────────────────────────────────────
2053
+
2054
+ let _modalDocContext = { title: '', content: '', selection: '' };
2055
+
2056
+ // Track text selection in modal body for the floating "Ask about this" button
2057
+ 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
+ }
2071
+ });
2072
+
2073
+ function modalAskAboutSelection() {
2074
+ document.getElementById('ask-selection-btn').style.display = 'none';
2075
+ const input = document.getElementById('modal-qa-input');
2076
+ input.value = '';
2077
+ input.placeholder = 'Ask about: "' + _modalDocContext.selection.slice(0, 60) + '..."';
2078
+ input.focus();
2079
+ }
2080
+
2081
+ async function modalAskSubmit() {
2082
+ const input = document.getElementById('modal-qa-input');
2083
+ const question = input.value.trim();
2084
+ if (!question) return;
2085
+ if (!_modalDocContext.content) return;
2086
+
2087
+ const thread = document.getElementById('modal-qa-thread');
2088
+ const btn = document.getElementById('modal-qa-btn');
2089
+
2090
+ // Show question
2091
+ let qHtml = '<div class="modal-qa-q">' + escHtml(question);
2092
+ if (_modalDocContext.selection) {
2093
+ qHtml += '<span class="selection-ref">Re: "' + escHtml(_modalDocContext.selection.slice(0, 100)) + ((_modalDocContext.selection.length > 100) ? '...' : '') + '"</span>';
2094
+ }
2095
+ qHtml += '</div>';
2096
+ thread.innerHTML += qHtml;
2097
+
2098
+ // Show loading
2099
+ const loadingId = 'qa-loading-' + Date.now();
2100
+ thread.innerHTML += '<div class="modal-qa-loading" id="' + loadingId + '"><div class="dot-pulse"><span></span><span></span><span></span></div> Thinking...</div>';
2101
+ thread.scrollTop = thread.scrollHeight;
2102
+
2103
+ input.value = '';
2104
+ input.placeholder = 'Ask another question...';
2105
+ btn.disabled = true;
2106
+
2107
+ try {
2108
+ const res = await fetch('/api/ask-about', {
2109
+ method: 'POST',
2110
+ headers: { 'Content-Type': 'application/json' },
2111
+ body: JSON.stringify({
2112
+ question,
2113
+ document: _modalDocContext.content,
2114
+ title: _modalDocContext.title,
2115
+ selection: _modalDocContext.selection || '',
2116
+ }),
2117
+ });
2118
+ const data = await res.json();
2119
+ const loadingEl = document.getElementById(loadingId);
2120
+ if (loadingEl) loadingEl.remove();
2121
+
2122
+ if (data.ok && data.answer) {
2123
+ thread.innerHTML += '<div class="modal-qa-a">' + escHtml(data.answer) + '</div>';
2124
+ } else {
2125
+ thread.innerHTML += '<div class="modal-qa-a" style="color:var(--red)">Error: ' + escHtml(data.error || 'No answer') + '</div>';
2126
+ }
2127
+ } catch (e) {
2128
+ const loadingEl = document.getElementById(loadingId);
2129
+ if (loadingEl) loadingEl.remove();
2130
+ thread.innerHTML += '<div class="modal-qa-a" style="color:var(--red)">Error: ' + escHtml(e.message) + '</div>';
2131
+ }
2132
+
2133
+ _modalDocContext.selection = ''; // Clear selection after asking
2134
+ btn.disabled = false;
2135
+ thread.scrollTop = thread.scrollHeight;
2136
+ input.focus();
2137
+ }
2138
+
2139
+ // Override closeModal to clear Q&A state
2140
+ const _origCloseModal = typeof closeModal === 'function' ? closeModal : null;
2141
+
2142
+ // ─── Plans (Approval Gate) ────────────────────────────────────────────────────
2143
+
2144
+ async function refreshPlans() {
2145
+ try {
2146
+ const plans = await fetch('/api/plans').then(r => r.json());
2147
+ renderPlans(plans);
2148
+ } catch {}
2149
+ }
2150
+
2151
+ function renderPlans(plans) {
2152
+ const el = document.getElementById('plans-list');
2153
+ const countEl = document.getElementById('plans-count');
2154
+ countEl.textContent = plans.length;
2155
+
2156
+ if (plans.length === 0) {
2157
+ el.innerHTML = '<p class="empty">No plans yet. Use /plan in the command center to create one.</p>';
2158
+ return;
2159
+ }
2160
+
2161
+ const statusLabels = { 'awaiting-approval': 'Awaiting Approval', 'approved': 'Approved', 'rejected': 'Rejected', 'revision-requested': 'Revision Requested', 'completed': 'Completed', 'active': 'Active' };
2162
+ const statusClass = (s) => s === 'awaiting-approval' ? 'awaiting' : s || '';
2163
+
2164
+ el.innerHTML = plans.map(p => {
2165
+ const status = p.status || 'active';
2166
+ const label = statusLabels[status] || status;
2167
+ const needsAction = status === 'awaiting-approval';
2168
+ const isRevision = status === 'revision-requested';
2169
+
2170
+ let actions = '';
2171
+ if (needsAction) {
2172
+ actions = '<div class="plan-card-actions">' +
2173
+ '<button class="plan-btn approve" onclick="planApprove(\'' + escHtml(p.file) + '\')">Approve</button>' +
2174
+ '<button class="plan-btn" style="color:var(--blue);border-color:var(--blue)" onclick="planDiscuss(\'' + escHtml(p.file) + '\')">Discuss &amp; Revise</button>' +
2175
+ '<button class="plan-btn reject" onclick="planReject(\'' + escHtml(p.file) + '\')">Reject</button>' +
2176
+ '<button class="plan-btn" onclick="planView(\'' + escHtml(p.file) + '\')">View Full Plan</button>' +
2177
+ '</div>' +
2178
+ '<div id="revise-input-' + escHtml(p.file).replace(/\./g, '-') + '" style="display:none">' +
2179
+ '<textarea class="plan-feedback-input" placeholder="What should be changed? Be specific..." id="revise-feedback-' + escHtml(p.file).replace(/\./g, '-') + '"></textarea>' +
2180
+ '<div class="plan-card-actions" style="margin-top:4px">' +
2181
+ '<button class="plan-btn revise" onclick="planSubmitRevise(\'' + escHtml(p.file) + '\')">Submit Revision Request</button>' +
2182
+ '<button class="plan-btn" onclick="planHideRevise(\'' + escHtml(p.file) + '\')">Cancel</button>' +
2183
+ '</div>' +
2184
+ '</div>';
2185
+ } else if (isRevision) {
2186
+ actions = '<div class="plan-card-meta" style="margin-top:6px;color:var(--purple,#a855f7)">Revision in progress: ' + escHtml((p.revisionFeedback || '').slice(0, 100)) + '</div>';
2187
+ } else if (status === 'approved' || status === 'active') {
2188
+ actions = '<div class="plan-card-actions"><button class="plan-btn" onclick="planView(\'' + escHtml(p.file) + '\')">View</button></div>';
2189
+ }
2190
+
2191
+ return '<div class="plan-card ' + statusClass(status) + '">' +
2192
+ '<div class="plan-card-header">' +
2193
+ '<div><div class="plan-card-title">' + escHtml(p.summary || p.file) + '</div>' +
2194
+ '<div class="plan-card-meta">' +
2195
+ '<span style="font-weight:600;color:' + (needsAction ? 'var(--yellow,#d29922)' : status === 'approved' ? 'var(--green)' : 'var(--muted)') + '">' + label + '</span>' +
2196
+ '<span>' + escHtml(p.project) + '</span>' +
2197
+ '<span>' + p.itemCount + ' items</span>' +
2198
+ '<span>' + escHtml(p.branchStrategy) + '</span>' +
2199
+ (p.generatedBy ? '<span>by ' + escHtml(p.generatedBy) + '</span>' : '') +
2200
+ (p.generatedAt ? '<span>' + p.generatedAt + '</span>' : '') +
2201
+ '</div>' +
2202
+ '</div>' +
2203
+ '</div>' +
2204
+ actions +
2205
+ '</div>';
2206
+ }).join('');
2207
+ }
2208
+
2209
+ async function planApprove(file) {
2210
+ try {
2211
+ await fetch('/api/plans/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
2212
+ showToast('cmd-toast', 'Plan approved — work will begin on next engine tick', true);
2213
+ refreshPlans();
2214
+ } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
2215
+ }
2216
+
2217
+ async function planReject(file) {
2218
+ if (!confirm('Reject this plan? It will not be executed.')) return;
2219
+ const reason = prompt('Reason for rejection (optional):') || '';
2220
+ try {
2221
+ await fetch('/api/plans/reject', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file, reason }) });
2222
+ showToast('cmd-toast', 'Plan rejected', true);
2223
+ refreshPlans();
2224
+ } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
2225
+ }
2226
+
2227
+ function planShowRevise(file) {
2228
+ const id = 'revise-input-' + file.replace(/\./g, '-');
2229
+ document.getElementById(id).style.display = 'block';
2230
+ }
2231
+
2232
+ function planHideRevise(file) {
2233
+ const id = 'revise-input-' + file.replace(/\./g, '-');
2234
+ document.getElementById(id).style.display = 'none';
2235
+ }
2236
+
2237
+ async function planSubmitRevise(file) {
2238
+ const id = 'revise-feedback-' + file.replace(/\./g, '-');
2239
+ const feedback = document.getElementById(id).value.trim();
2240
+ if (!feedback) { showToast('cmd-toast', 'Please enter feedback', false); return; }
2241
+ try {
2242
+ const res = await fetch('/api/plans/revise', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file, feedback }) });
2243
+ const data = await res.json();
2244
+ showToast('cmd-toast', 'Revision requested — agent will update the plan (' + data.workItemId + ')', true);
2245
+ planHideRevise(file);
2246
+ refreshPlans();
2247
+ } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
2248
+ }
2249
+
2250
+ async function planDiscuss(file) {
2251
+ try {
2252
+ const res = await fetch('/api/plans/discuss', {
2253
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
2254
+ body: JSON.stringify({ file })
2255
+ });
2256
+ const data = await res.json();
2257
+ if (!res.ok) throw new Error(data.error);
2258
+
2259
+ // Show the launch command in a modal
2260
+ const content = `To discuss and revise this plan interactively, run this command in a terminal:\n\n` +
2261
+ `━━━ Bash / Git Bash ━━━\n${data.command}\n\n` +
2262
+ `━━━ PowerShell ━━━\n${data.psCommand}\n\n` +
2263
+ `━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
2264
+ `This launches an interactive Claude session with the plan pre-loaded.\n` +
2265
+ `Chat naturally to review and refine. When you're satisfied, say "approve" and the session will write the approved plan back to disk.\n\n` +
2266
+ `The engine will pick it up on the next tick and start dispatching work.`;
2267
+
2268
+ document.getElementById('modal-title').textContent = 'Discuss Plan: ' + file;
2269
+ document.getElementById('modal-body').textContent = content;
2270
+ document.getElementById('modal').classList.add('open');
2271
+ } catch (e) {
2272
+ showToast('cmd-toast', 'Error: ' + e.message, false);
2273
+ }
2274
+ }
2275
+
2276
+ async function planView(file) {
2277
+ try {
2278
+ const plan = await fetch('/api/plans/' + encodeURIComponent(file)).then(r => r.json());
2279
+ const items = (plan.missing_features || []).map((f, i) =>
2280
+ (i + 1) + '. [' + f.id + '] ' + f.name + ' (' + f.estimated_complexity + ', ' + f.priority + ')' +
2281
+ (f.depends_on?.length ? ' → depends on: ' + f.depends_on.join(', ') : '') +
2282
+ '\n ' + (f.description || '').slice(0, 150)
2283
+ ).join('\n\n');
2284
+ const text = 'Project: ' + plan.project + '\nStrategy: ' + (plan.branch_strategy || 'parallel') +
2285
+ '\nBranch: ' + (plan.feature_branch || 'per-item') +
2286
+ '\nStatus: ' + (plan.status || 'active') +
2287
+ '\n\n--- Items ---\n\n' + items +
2288
+ (plan.open_questions?.length ? '\n\n--- Open Questions ---\n\n' + plan.open_questions.join('\n') : '');
2289
+ document.getElementById('modal-title').textContent = plan.plan_summary || file;
2290
+ document.getElementById('modal-body').textContent = text;
2291
+ document.getElementById('modal').classList.add('open');
2292
+ } catch (e) { console.error(e); }
2293
+ }
2294
+
1971
2295
  // ─── Knowledge Base ──────────────────────────────────────────────────────────
1972
2296
  let _kbData = {};
1973
2297
  let _kbActiveTab = 'all';
package/dashboard.js CHANGED
@@ -75,7 +75,7 @@ function getAgentDetail(id) {
75
75
  const agentDir = path.join(SQUAD_DIR, 'agents', id);
76
76
  const charter = safeRead(path.join(agentDir, 'charter.md')) || 'No charter found.';
77
77
  const history = safeRead(path.join(agentDir, 'history.md')) || 'No history yet.';
78
- const outputLog = safeRead(path.join(agentDir, 'output.md')) || '';
78
+ const outputLog = safeRead(path.join(agentDir, 'output.log')) || '';
79
79
 
80
80
  const statusJson = safeRead(path.join(agentDir, 'status.json'));
81
81
  let statusData = null;
@@ -86,7 +86,25 @@ function getAgentDetail(id) {
86
86
  .filter(f => f.includes(id))
87
87
  .map(f => ({ name: f, content: safeRead(path.join(inboxDir, f)) || '' }));
88
88
 
89
- return { charter, history, statusData, outputLog, inboxContents };
89
+ // Recent completed dispatches for this agent (last 10)
90
+ let recentDispatches = [];
91
+ try {
92
+ const dispatch = JSON.parse(safeRead(path.join(SQUAD_DIR, 'engine', 'dispatch.json')) || '{}');
93
+ recentDispatches = (dispatch.completed || [])
94
+ .filter(d => d.agent === id)
95
+ .slice(-10)
96
+ .reverse()
97
+ .map(d => ({
98
+ id: d.id,
99
+ task: d.task || '',
100
+ type: d.type || '',
101
+ result: d.result || '',
102
+ reason: d.reason || '',
103
+ completed_at: d.completed_at || '',
104
+ }));
105
+ } catch {}
106
+
107
+ return { charter, history, statusData, outputLog, inboxContents, recentDispatches };
90
108
  }
91
109
 
92
110
  function getAgents() {
@@ -858,6 +876,279 @@ const server = http.createServer(async (req, res) => {
858
876
  return;
859
877
  }
860
878
 
879
+ // GET /api/plans — list all plan files with status
880
+ if (req.method === 'GET' && req.url === '/api/plans') {
881
+ const plansDir = path.join(SQUAD_DIR, 'plans');
882
+ const files = safeReadDir(plansDir).filter(f => f.endsWith('.json'));
883
+ const plans = files.map(f => {
884
+ const plan = JSON.parse(safeRead(path.join(plansDir, f)) || '{}');
885
+ return {
886
+ file: f,
887
+ project: plan.project || '',
888
+ summary: plan.plan_summary || '',
889
+ status: plan.status || 'active',
890
+ branchStrategy: plan.branch_strategy || 'parallel',
891
+ featureBranch: plan.feature_branch || '',
892
+ itemCount: (plan.missing_features || []).length,
893
+ generatedBy: plan.generated_by || '',
894
+ generatedAt: plan.generated_at || '',
895
+ requiresApproval: plan.requires_approval || false,
896
+ revisionFeedback: plan.revision_feedback || null,
897
+ };
898
+ }).sort((a, b) => b.generatedAt.localeCompare(a.generatedAt));
899
+ return jsonReply(res, 200, plans);
900
+ }
901
+
902
+ // GET /api/plans/:file — read full plan JSON
903
+ const planFileMatch = req.url.match(/^\/api\/plans\/([^?]+)$/);
904
+ if (planFileMatch && req.method === 'GET') {
905
+ const file = decodeURIComponent(planFileMatch[1]);
906
+ if (file.includes('..') || file.includes('/') || file.includes('\\')) return jsonReply(res, 400, { error: 'invalid' });
907
+ const content = safeRead(path.join(SQUAD_DIR, 'plans', file));
908
+ if (!content) return jsonReply(res, 404, { error: 'not found' });
909
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
910
+ res.setHeader('Access-Control-Allow-Origin', '*');
911
+ res.end(content);
912
+ return;
913
+ }
914
+
915
+ // POST /api/plans/approve — approve a plan for execution
916
+ if (req.method === 'POST' && req.url === '/api/plans/approve') {
917
+ try {
918
+ const body = await readBody(req);
919
+ if (!body.file) return jsonReply(res, 400, { error: 'file required' });
920
+ const planPath = path.join(SQUAD_DIR, 'plans', body.file);
921
+ const plan = JSON.parse(safeRead(planPath) || '{}');
922
+ plan.status = 'approved';
923
+ plan.approvedAt = new Date().toISOString();
924
+ plan.approvedBy = body.approvedBy || os.userInfo().username;
925
+ safeWrite(planPath, plan);
926
+ return jsonReply(res, 200, { ok: true, status: 'approved' });
927
+ } catch (e) { return jsonReply(res, 400, { error: e.message }); }
928
+ }
929
+
930
+ // POST /api/plans/reject — reject a plan
931
+ if (req.method === 'POST' && req.url === '/api/plans/reject') {
932
+ try {
933
+ const body = await readBody(req);
934
+ if (!body.file) return jsonReply(res, 400, { error: 'file required' });
935
+ const planPath = path.join(SQUAD_DIR, 'plans', body.file);
936
+ const plan = JSON.parse(safeRead(planPath) || '{}');
937
+ plan.status = 'rejected';
938
+ plan.rejectedAt = new Date().toISOString();
939
+ plan.rejectedBy = body.rejectedBy || os.userInfo().username;
940
+ if (body.reason) plan.rejectionReason = body.reason;
941
+ safeWrite(planPath, plan);
942
+ return jsonReply(res, 200, { ok: true, status: 'rejected' });
943
+ } catch (e) { return jsonReply(res, 400, { error: e.message }); }
944
+ }
945
+
946
+ // POST /api/plans/revise — request revision with feedback, dispatches agent to revise
947
+ if (req.method === 'POST' && req.url === '/api/plans/revise') {
948
+ try {
949
+ const body = await readBody(req);
950
+ if (!body.file || !body.feedback) return jsonReply(res, 400, { error: 'file and feedback required' });
951
+ const planPath = path.join(SQUAD_DIR, 'plans', body.file);
952
+ const plan = JSON.parse(safeRead(planPath) || '{}');
953
+ plan.status = 'revision-requested';
954
+ plan.revision_feedback = body.feedback;
955
+ plan.revisionRequestedAt = new Date().toISOString();
956
+ plan.revisionRequestedBy = body.requestedBy || os.userInfo().username;
957
+ safeWrite(planPath, plan);
958
+
959
+ // Create a work item to revise the plan
960
+ const wiPath = path.join(SQUAD_DIR, 'work-items.json');
961
+ let items = [];
962
+ const existing = safeRead(wiPath);
963
+ if (existing) { try { items = JSON.parse(existing); } catch {} }
964
+ const maxNum = items.reduce(function(max, i) {
965
+ const m = (i.id || '').match(/(\d+)$/);
966
+ return m ? Math.max(max, parseInt(m[1])) : max;
967
+ }, 0);
968
+ const id = 'W' + String(maxNum + 1).padStart(3, '0');
969
+ items.push({
970
+ id, title: 'Revise plan: ' + (plan.plan_summary || body.file),
971
+ type: 'plan-to-prd', priority: 'high',
972
+ description: 'Revision requested on plan file: plans/' + body.file + '\n\nFeedback:\n' + body.feedback + '\n\nRevise the plan to address this feedback. Read the existing plan, apply the feedback, and overwrite the file with the updated version. Set status back to "awaiting-approval".',
973
+ status: 'pending', created: new Date().toISOString(), createdBy: 'dashboard:revision',
974
+ project: plan.project || '',
975
+ planFile: body.file,
976
+ });
977
+ safeWrite(wiPath, items);
978
+ return jsonReply(res, 200, { ok: true, status: 'revision-requested', workItemId: id });
979
+ } catch (e) { return jsonReply(res, 400, { error: e.message }); }
980
+ }
981
+
982
+ // POST /api/plans/discuss — generate a plan discussion session script
983
+ if (req.method === 'POST' && req.url === '/api/plans/discuss') {
984
+ try {
985
+ const body = await readBody(req);
986
+ if (!body.file) return jsonReply(res, 400, { error: 'file required' });
987
+ const planPath = path.join(SQUAD_DIR, 'plans', body.file);
988
+ const planContent = safeRead(planPath);
989
+ if (!planContent) return jsonReply(res, 404, { error: 'plan not found' });
990
+
991
+ const plan = JSON.parse(planContent);
992
+ const projectName = plan.project || 'Unknown';
993
+
994
+ // Build the session launch script
995
+ const sessionName = 'plan-review-' + body.file.replace(/\.json$/, '');
996
+ const sysPrompt = `You are a Plan Advisor helping a human review and refine a feature plan before it gets dispatched to an agent squad.
997
+
998
+ ## Your Role
999
+ - Help the user understand, question, and refine the plan
1000
+ - Accept feedback and update the plan accordingly
1001
+ - When the user is satisfied, write the approved plan back to disk
1002
+
1003
+ ## The Plan File
1004
+ Path: ${planPath}
1005
+ Project: ${projectName}
1006
+
1007
+ ## How This Works
1008
+ 1. The user will discuss the plan with you — answer questions, suggest changes
1009
+ 2. When they want changes, update the plan items (add/remove/reorder/modify)
1010
+ 3. When they say ANY of these (or similar intent):
1011
+ - "approve", "go", "ship it", "looks good", "lgtm"
1012
+ - "clear context and implement", "clear context and go"
1013
+ - "go build it", "start working", "dispatch", "execute"
1014
+ - "do it", "proceed", "let's go", "send it"
1015
+
1016
+ Then:
1017
+ a. Read the current plan file fresh from disk
1018
+ b. Update status to "approved", set approvedAt and approvedBy
1019
+ c. Write it back to ${planPath} using the Write tool
1020
+ d. Print exactly: "Plan approved and saved. The engine will dispatch work on the next tick. You can close this session."
1021
+ e. Then EXIT the session — use /exit or simply stop responding. The user does NOT need to interact further.
1022
+
1023
+ 4. If they say "reject" or "cancel":
1024
+ - Update status to "rejected"
1025
+ - Write it back
1026
+ - Confirm and exit.
1027
+
1028
+ ## Important
1029
+ - Always read the plan file fresh before writing (another process may have modified it)
1030
+ - Preserve all existing fields when writing back
1031
+ - Use the Write tool to save changes
1032
+ - You have full file access — you can also read the project codebase for context
1033
+ - When the user signals approval, ALWAYS write the file and exit. Do not ask for confirmation — their intent is clear.`;
1034
+
1035
+ const initialPrompt = `Here's the plan awaiting your review:
1036
+
1037
+ **${plan.plan_summary || body.file}**
1038
+ Project: ${projectName}
1039
+ Strategy: ${plan.branch_strategy || 'parallel'}
1040
+ Branch: ${plan.feature_branch || 'per-item'}
1041
+ Items: ${(plan.missing_features || []).length}
1042
+
1043
+ ${(plan.missing_features || []).map((f, i) =>
1044
+ `${i + 1}. **${f.id}: ${f.name}** (${f.estimated_complexity}, ${f.priority})${f.depends_on?.length ? ' → depends on: ' + f.depends_on.join(', ') : ''}
1045
+ ${f.description || ''}`
1046
+ ).join('\n\n')}
1047
+
1048
+ ${plan.open_questions?.length ? '\n**Open Questions:**\n' + plan.open_questions.map(q => '- ' + q).join('\n') : ''}
1049
+
1050
+ What would you like to discuss or change? When you're happy, say "approve" and I'll finalize it.`;
1051
+
1052
+ // Write session files
1053
+ const sessionDir = path.join(SQUAD_DIR, 'engine');
1054
+ const sysFile = path.join(sessionDir, `plan-discuss-sys-${Date.now()}.md`);
1055
+ const promptFile = path.join(sessionDir, `plan-discuss-prompt-${Date.now()}.md`);
1056
+ safeWrite(sysFile, sysPrompt);
1057
+ safeWrite(promptFile, initialPrompt);
1058
+
1059
+ // Generate the launch command
1060
+ const cmd = `claude --system-prompt "$(cat '${sysFile.replace(/\\/g, '/')}')" --name "${sessionName}" --add-dir "${SQUAD_DIR.replace(/\\/g, '/')}" < "${promptFile.replace(/\\/g, '/')}"`;
1061
+
1062
+ // Also generate a PowerShell-friendly version
1063
+ const psCmd = `Get-Content "${promptFile}" | claude --system-prompt (Get-Content "${sysFile}" -Raw) --name "${sessionName}" --add-dir "${SQUAD_DIR}"`;
1064
+
1065
+ return jsonReply(res, 200, {
1066
+ ok: true,
1067
+ sessionName,
1068
+ command: cmd,
1069
+ psCommand: psCmd,
1070
+ sysFile,
1071
+ promptFile,
1072
+ planFile: body.file,
1073
+ });
1074
+ } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1075
+ }
1076
+
1077
+ // POST /api/ask-about — ask a question about a document with context, answered by Haiku
1078
+ if (req.method === 'POST' && req.url === '/api/ask-about') {
1079
+ try {
1080
+ const body = await readBody(req);
1081
+ if (!body.question) return jsonReply(res, 400, { error: 'question required' });
1082
+ if (!body.document) return jsonReply(res, 400, { error: 'document required' });
1083
+
1084
+ const prompt = `You are answering a question about a document from a software engineering squad's knowledge base.
1085
+
1086
+ ## Document
1087
+ ${body.title ? '**Title:** ' + body.title + '\n' : ''}
1088
+ ${body.document.slice(0, 15000)}
1089
+
1090
+ ${body.selection ? '## Highlighted Selection\n\nThe user highlighted this specific part:\n> ' + body.selection.slice(0, 2000) + '\n' : ''}
1091
+
1092
+ ## Question
1093
+
1094
+ ${body.question}
1095
+
1096
+ ## Instructions
1097
+
1098
+ Answer concisely and directly. Reference specific parts of the document. If the answer isn't in the document, say so. Use markdown formatting.`;
1099
+
1100
+ const sysPrompt = 'You are a concise technical assistant. Answer based on the document provided. No preamble.';
1101
+
1102
+ // Write temp files
1103
+ const id = Date.now();
1104
+ const promptPath = path.join(SQUAD_DIR, 'engine', 'ask-prompt-' + id + '.md');
1105
+ const sysPath = path.join(SQUAD_DIR, 'engine', 'ask-sys-' + id + '.md');
1106
+ safeWrite(promptPath, prompt);
1107
+ safeWrite(sysPath, sysPrompt);
1108
+
1109
+ // Spawn Haiku
1110
+ const spawnScript = path.join(SQUAD_DIR, 'engine', 'spawn-agent.js');
1111
+ const childEnv = { ...process.env };
1112
+ for (const key of Object.keys(childEnv)) {
1113
+ if (key === 'CLAUDECODE' || key.startsWith('CLAUDE_CODE') || key.startsWith('CLAUDECODE_')) delete childEnv[key];
1114
+ }
1115
+
1116
+ const { spawn: cpSpawn } = require('child_process');
1117
+ const proc = cpSpawn(process.execPath, [
1118
+ spawnScript, promptPath, sysPath,
1119
+ '--output-format', 'text', '--max-turns', '1', '--model', 'haiku',
1120
+ '--permission-mode', 'bypassPermissions', '--verbose',
1121
+ ], { cwd: SQUAD_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: childEnv });
1122
+
1123
+ let stdout = '';
1124
+ let stderr = '';
1125
+ proc.stdout.on('data', d => { stdout += d.toString(); });
1126
+ proc.stderr.on('data', d => { stderr += d.toString(); });
1127
+
1128
+ // Timeout 60s
1129
+ const timeout = setTimeout(() => { try { proc.kill('SIGTERM'); } catch {} }, 60000);
1130
+
1131
+ proc.on('close', (code) => {
1132
+ clearTimeout(timeout);
1133
+ try { fs.unlinkSync(promptPath); } catch {}
1134
+ try { fs.unlinkSync(sysPath); } catch {}
1135
+ if (code === 0 && stdout.trim()) {
1136
+ return jsonReply(res, 200, { ok: true, answer: stdout.trim() });
1137
+ } else {
1138
+ return jsonReply(res, 500, { error: 'Failed to get answer', stderr: stderr.slice(0, 200) });
1139
+ }
1140
+ });
1141
+
1142
+ proc.on('error', (err) => {
1143
+ clearTimeout(timeout);
1144
+ try { fs.unlinkSync(promptPath); } catch {}
1145
+ try { fs.unlinkSync(sysPath); } catch {}
1146
+ return jsonReply(res, 500, { error: err.message });
1147
+ });
1148
+ return; // Don't fall through — response handled in callbacks
1149
+ } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1150
+ }
1151
+
861
1152
  // POST /api/inbox/persist — promote an inbox item to team notes
862
1153
  if (req.method === 'POST' && req.url === '/api/inbox/persist') {
863
1154
  try {
package/engine.js CHANGED
@@ -3083,6 +3083,13 @@ function materializePlansAsWorkItems(config) {
3083
3083
  const plan = safeJson(path.join(PLANS_DIR, file));
3084
3084
  if (!plan?.missing_features) continue;
3085
3085
 
3086
+ // Human approval gate: plans start as 'awaiting-approval' and must be approved before work begins
3087
+ // Plans without a status (legacy) or with status 'approved' are allowed through
3088
+ const planStatus = plan.status || (plan.requires_approval ? 'awaiting-approval' : null);
3089
+ if (planStatus === 'awaiting-approval' || planStatus === 'rejected' || planStatus === 'revision-requested') {
3090
+ continue; // Skip — waiting for human approval or revision
3091
+ }
3092
+
3086
3093
  const projectName = plan.project || file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
3087
3094
  const project = getProjects(config).find(p => p.name?.toLowerCase() === projectName.toLowerCase());
3088
3095
  if (!project) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/squad",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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"
@@ -33,6 +33,8 @@ This file is NOT checked into the repo. The engine reads it on every tick and di
33
33
  "generated_by": "{{agent_id}}",
34
34
  "generated_at": "{{date}}",
35
35
  "plan_summary": "{{plan_summary}}",
36
+ "status": "awaiting-approval",
37
+ "requires_approval": true,
36
38
  "branch_strategy": "shared-branch|parallel",
37
39
  "feature_branch": "feat/plan-short-name",
38
40
  "missing_features": [