@yemi33/minions 0.1.16 → 0.1.17

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.17 (2026-03-28)
4
+
5
+ ### Engine
6
+ - engine.js
7
+
8
+ ### Dashboard
9
+ - dashboard-build.js
10
+ - dashboard.js
11
+ - dashboard/js/refresh.js
12
+ - dashboard/js/render-inbox.js
13
+ - dashboard/js/render-kb.js
14
+ - dashboard/js/render-pinned.js
15
+ - dashboard/js/render-work-items.js
16
+ - dashboard/pages/home.html
17
+ - dashboard/pages/inbox.html
18
+
3
19
  ## 0.1.16 (2026-03-28)
4
20
 
5
21
  ### Dashboard
@@ -38,6 +38,7 @@ async function refresh() {
38
38
  renderSkills(data.skills || []);
39
39
  renderMcpServers(data.mcpServers || []);
40
40
  renderSchedules(data.schedules || []);
41
+ renderPinned(data.pinned || []);
41
42
  // Update sidebar counts
42
43
  const swi = document.getElementById('sidebar-wi');
43
44
  if (swi) swi.textContent = (data.workItems || []).length || '';
@@ -145,6 +145,36 @@ async function openInboxInExplorer(name) {
145
145
  } catch {}
146
146
  }
147
147
 
148
+ function openQuickNoteModal() {
149
+ document.getElementById('modal-title').textContent = 'Quick Note';
150
+ document.getElementById('modal-body').innerHTML =
151
+ '<div style="display:flex;flex-direction:column;gap:12px">' +
152
+ '<label style="color:var(--text);font-size:var(--text-md)">Title' +
153
+ '<input id="note-title" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text)" placeholder="Decision, observation, or context..."></label>' +
154
+ '<label style="color:var(--text);font-size:var(--text-md)">Content' +
155
+ '<textarea id="note-content" rows="6" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);resize:vertical;font-family:inherit" placeholder="Write your note... Agents will see this after consolidation."></textarea></label>' +
156
+ '<div style="display:flex;justify-content:flex-end;gap:8px">' +
157
+ '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
158
+ '<button onclick="submitQuickNote()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Save Note</button>' +
159
+ '</div>' +
160
+ '</div>';
161
+ document.getElementById('modal').classList.add('open');
162
+ }
163
+
164
+ async function submitQuickNote() {
165
+ const title = document.getElementById('note-title').value;
166
+ const content = document.getElementById('note-content').value;
167
+ if (!title && !content) { alert('Title or content required'); return; }
168
+ try {
169
+ const res = await fetch('/api/notes', {
170
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
171
+ body: JSON.stringify({ title: title || 'Quick note', content: content || title })
172
+ });
173
+ if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Note saved to inbox', true); }
174
+ else { const d = await res.json(); alert('Error: ' + (d.error || 'unknown')); }
175
+ } catch (e) { alert('Error: ' + e.message); }
176
+ }
177
+
148
178
  async function doPromoteToKB(name, category) {
149
179
  try {
150
180
  const res = await fetch('/api/inbox/promote-kb', {
@@ -106,6 +106,45 @@ async function kbSweep() {
106
106
  setTimeout(() => { btn.textContent = origText; btn.style.color = 'var(--muted)'; btn.disabled = false; }, 3000);
107
107
  }
108
108
 
109
+ function openCreateKbModal() {
110
+ document.getElementById('modal-title').textContent = 'New Knowledge Base Entry';
111
+ document.getElementById('modal-body').innerHTML =
112
+ '<div style="display:flex;flex-direction:column;gap:12px">' +
113
+ '<label style="color:var(--text);font-size:var(--text-md)">Category' +
114
+ '<select id="kb-new-category" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text)">' +
115
+ '<option value="architecture">Architecture</option>' +
116
+ '<option value="conventions">Conventions</option>' +
117
+ '<option value="project-notes">Project Notes</option>' +
118
+ '<option value="build-reports">Build Reports</option>' +
119
+ '<option value="reviews">Reviews</option>' +
120
+ '</select></label>' +
121
+ '<label style="color:var(--text);font-size:var(--text-md)">Title' +
122
+ '<input id="kb-new-title" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text)" placeholder="Entry title"></label>' +
123
+ '<label style="color:var(--text);font-size:var(--text-md)">Content' +
124
+ '<textarea id="kb-new-content" rows="8" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);resize:vertical;font-family:inherit" placeholder="Write your knowledge entry..."></textarea></label>' +
125
+ '<div style="display:flex;justify-content:flex-end;gap:8px">' +
126
+ '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
127
+ '<button onclick="submitKbEntry()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Save</button>' +
128
+ '</div>' +
129
+ '</div>';
130
+ document.getElementById('modal').classList.add('open');
131
+ }
132
+
133
+ async function submitKbEntry() {
134
+ const category = document.getElementById('kb-new-category').value;
135
+ const title = document.getElementById('kb-new-title').value;
136
+ const content = document.getElementById('kb-new-content').value;
137
+ if (!title || !content) { alert('Title and content are required'); return; }
138
+ try {
139
+ const res = await fetch('/api/knowledge', {
140
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
141
+ body: JSON.stringify({ category, title, content })
142
+ });
143
+ if (res.ok) { closeModal(); refreshKnowledgeBase(); showToast('cmd-toast', 'KB entry created', true); }
144
+ else { const d = await res.json(); alert('Error: ' + (d.error || 'unknown')); }
145
+ } catch (e) { alert('Error: ' + e.message); }
146
+ }
147
+
109
148
  async function kbOpenItem(category, file) {
110
149
  try {
111
150
  const content = await fetch('/api/knowledge/' + category + '/' + encodeURIComponent(file)).then(r => r.text());
@@ -0,0 +1,53 @@
1
+ // dashboard/js/render-pinned.js — Pinned context notes rendering and management
2
+
3
+ function renderPinned(entries) {
4
+ const el = document.getElementById('pinned-content');
5
+ if (!el) return;
6
+ if (!entries || entries.length === 0) {
7
+ el.innerHTML = '<p class="empty">No pinned notes. Pin important context that all agents should see.</p>';
8
+ return;
9
+ }
10
+ el.innerHTML = entries.map(e =>
11
+ '<div style="padding:8px 12px;margin-bottom:6px;background:var(--surface2);border-left:3px solid ' +
12
+ (e.level === 'critical' ? 'var(--red)' : e.level === 'warning' ? 'var(--yellow)' : 'var(--blue)') +
13
+ ';border-radius:4px">' +
14
+ '<div style="display:flex;justify-content:space-between;align-items:center">' +
15
+ '<strong style="font-size:var(--text-md)">' + escHtml(e.title) + '</strong>' +
16
+ '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="removePinnedNote(\'' + escHtml(e.title) + '\')">Unpin</button>' +
17
+ '</div>' +
18
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:4px">' + escHtml(e.content.slice(0, 200)) + '</div>' +
19
+ '</div>'
20
+ ).join('');
21
+ }
22
+
23
+ function openPinNoteModal() {
24
+ document.getElementById('modal-title').textContent = 'Pin a Note';
25
+ document.getElementById('modal-body').innerHTML =
26
+ '<div style="display:flex;flex-direction:column;gap:12px">' +
27
+ '<label style="color:var(--text);font-size:var(--text-md)">Title<input id="pin-title" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text)" placeholder="e.g. API freeze until Friday"></label>' +
28
+ '<label style="color:var(--text);font-size:var(--text-md)">Content<textarea id="pin-content" rows="4" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);resize:vertical;font-family:inherit" placeholder="Context all agents should see..."></textarea></label>' +
29
+ '<label style="color:var(--text);font-size:var(--text-md)">Level<select id="pin-level" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text)"><option value="info">Info</option><option value="warning">Warning</option><option value="critical">Critical</option></select></label>' +
30
+ '<div style="display:flex;justify-content:flex-end;gap:8px"><button onclick="closeModal()" class="pr-pager-btn">Cancel</button><button onclick="submitPinnedNote()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Pin</button></div>' +
31
+ '</div>';
32
+ document.getElementById('modal').classList.add('open');
33
+ }
34
+
35
+ async function submitPinnedNote() {
36
+ const title = document.getElementById('pin-title').value;
37
+ const content = document.getElementById('pin-content').value;
38
+ const level = document.getElementById('pin-level').value;
39
+ if (!title || !content) { alert('Title and content required'); return; }
40
+ try {
41
+ const res = await fetch('/api/pinned', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, content, level }) });
42
+ if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Note pinned', true); }
43
+ else { const d = await res.json(); alert('Error: ' + (d.error || 'unknown')); }
44
+ } catch (e) { alert('Error: ' + e.message); }
45
+ }
46
+
47
+ async function removePinnedNote(title) {
48
+ if (!confirm('Unpin "' + title + '"?')) return;
49
+ try {
50
+ await fetch('/api/pinned/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) });
51
+ refresh();
52
+ } catch (e) { alert('Error: ' + e.message); }
53
+ }
@@ -33,9 +33,14 @@ function wiRow(item) {
33
33
  '</td>' +
34
34
  '<td>' + prLink + '</td>' +
35
35
  '<td><span class="pr-date">' + shortTime(item.created) + '</span></td>' +
36
+ '<td style="white-space:nowrap;font-size:9px;color:var(--muted)">' +
37
+ (item.references && item.references.length ? '<span title="' + item.references.length + ' reference(s)" style="margin-right:4px">&#x1F517;' + item.references.length + '</span>' : '') +
38
+ (item.acceptanceCriteria && item.acceptanceCriteria.length ? '<span title="' + item.acceptanceCriteria.length + ' acceptance criteria">&#x2611;' + item.acceptanceCriteria.length + '</span>' : '') +
39
+ '</td>' +
36
40
  '<td style="white-space:nowrap">' +
37
41
  ((item.status === 'pending' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Edit work item">&#x270E;</button>' : '') +
38
42
  ((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>' : '') +
43
+ ((item.status === 'done' || item.status === 'failed') && !item._humanFeedback ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Give feedback">&#x1F44D;&#x1F44E;</button>' : (item._humanFeedback ? '<span style="font-size:9px" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '&#x1F44D;' : '&#x1F44E;') + '</span> ' : '')) +
39
44
  '<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>' +
40
45
  '</td>' +
41
46
  '</tr>';
@@ -65,7 +70,7 @@ function renderWorkItems(items) {
65
70
  const start = wiPage * WI_PER_PAGE;
66
71
  const pageItems = items.slice(start, start + WI_PER_PAGE);
67
72
 
68
- let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Source</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th></tr></thead><tbody>';
73
+ let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Source</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th><th></th></tr></thead><tbody>';
69
74
  html += pageItems.map(wiRow).join('');
70
75
  html += '</tbody></table></div>';
71
76
 
@@ -113,6 +118,12 @@ function editWorkItem(id, source) {
113
118
  '<select id="wi-edit-agent" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md)"><option value="">Auto</option>' + agentOpts + '</select>' +
114
119
  '</label>' +
115
120
  '</div>' +
121
+ '<label style="color:var(--text);font-size:var(--text-md)">References (one per line: url | title | type)' +
122
+ '<textarea id="wi-edit-refs" rows="3" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit;resize:vertical">' + escHtml((item.references || []).map(function(r) { return r.url + ' | ' + (r.title || '') + ' | ' + (r.type || 'link'); }).join('\n')) + '</textarea>' +
123
+ '</label>' +
124
+ '<label style="color:var(--text);font-size:var(--text-md)">Acceptance Criteria (one per line)' +
125
+ '<textarea id="wi-edit-ac" rows="3" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit;resize:vertical">' + escHtml((item.acceptanceCriteria || []).join('\n')) + '</textarea>' +
126
+ '</label>' +
116
127
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
117
128
  '<button onclick="closeModal()" class="pr-pager-btn" style="padding:6px 16px;font-size:var(--text-md)">Cancel</button>' +
118
129
  '<button onclick="submitWorkItemEdit(\'' + escHtml(id) + '\',\'' + escHtml(source || '') + '\')" style="padding:6px 16px;font-size:var(--text-md);background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Save</button>' +
@@ -127,11 +138,18 @@ async function submitWorkItemEdit(id, source) {
127
138
  const type = document.getElementById('wi-edit-type').value;
128
139
  const priority = document.getElementById('wi-edit-priority').value;
129
140
  const agent = document.getElementById('wi-edit-agent').value;
141
+ const refsRaw = document.getElementById('wi-edit-refs')?.value || '';
142
+ const references = refsRaw.split('\n').filter(function(l) { return l.trim(); }).map(function(l) {
143
+ var parts = l.split('|').map(function(s) { return s.trim(); });
144
+ return { url: parts[0], title: parts[1] || parts[0], type: parts[2] || 'link' };
145
+ });
146
+ const acRaw = document.getElementById('wi-edit-ac')?.value || '';
147
+ const acceptanceCriteria = acRaw.split('\n').filter(function(l) { return l.trim(); });
130
148
  if (!title) { alert('Title is required'); return; }
131
149
  try {
132
150
  const res = await fetch('/api/work-items/update', {
133
151
  method: 'POST', headers: { 'Content-Type': 'application/json' },
134
- body: JSON.stringify({ id, source: source || undefined, title, description, type, priority, agent })
152
+ body: JSON.stringify({ id, source: source || undefined, title, description, type, priority, agent, references, acceptanceCriteria })
135
153
  });
136
154
  if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Work item updated', true); } else {
137
155
  const d = await res.json();
@@ -208,9 +226,34 @@ async function retryWorkItem(id, source) {
208
226
  function wiPrev() { if (wiPage > 0) { wiPage--; renderWorkItems(allWorkItems); } }
209
227
  function wiNext() { const tp = Math.ceil(allWorkItems.length / WI_PER_PAGE); if (wiPage < tp-1) { wiPage++; renderWorkItems(allWorkItems); } }
210
228
 
229
+ function feedbackWorkItem(id, source) {
230
+ document.getElementById('modal-title').textContent = 'Feedback on ' + id;
231
+ document.getElementById('modal-body').innerHTML =
232
+ '<div style="display:flex;flex-direction:column;gap:16px;align-items:center">' +
233
+ '<div style="display:flex;gap:24px">' +
234
+ '<button onclick="submitFeedback(\'' + escHtml(id) + '\',\'' + escHtml(source) + '\',\'up\')" style="font-size:40px;background:none;border:2px solid var(--border);border-radius:12px;padding:16px 24px;cursor:pointer;transition:all 0.2s" onmouseover="this.style.borderColor=\'var(--green)\'" onmouseout="this.style.borderColor=\'var(--border)\'">&#x1F44D;</button>' +
235
+ '<button onclick="submitFeedback(\'' + escHtml(id) + '\',\'' + escHtml(source) + '\',\'down\')" style="font-size:40px;background:none;border:2px solid var(--border);border-radius:12px;padding:16px 24px;cursor:pointer;transition:all 0.2s" onmouseover="this.style.borderColor=\'var(--red)\'" onmouseout="this.style.borderColor=\'var(--border)\'">&#x1F44E;</button>' +
236
+ '</div>' +
237
+ '<textarea id="feedback-comment" rows="3" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-family:inherit;resize:vertical" placeholder="Optional: what was good or needs improvement?"></textarea>' +
238
+ '</div>';
239
+ document.getElementById('modal').classList.add('open');
240
+ }
241
+
242
+ async function submitFeedback(id, source, rating) {
243
+ const comment = document.getElementById('feedback-comment')?.value || '';
244
+ try {
245
+ const res = await fetch('/api/work-items/feedback', {
246
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
247
+ body: JSON.stringify({ id, source, rating, comment })
248
+ });
249
+ if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Feedback saved — agents will learn from it', true); }
250
+ else { const d = await res.json(); alert('Error: ' + (d.error || 'unknown')); }
251
+ } catch (e) { alert('Error: ' + e.message); }
252
+ }
253
+
211
254
  function openAllWorkItems() {
212
255
  document.getElementById('modal-title').textContent = 'All Work Items (' + allWorkItems.length + ')';
213
- const html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Source</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th></tr></thead><tbody>' +
256
+ const html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Source</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th><th></th></tr></thead><tbody>' +
214
257
  allWorkItems.map(wiRow).join('') + '</tbody></table></div>';
215
258
  document.getElementById('modal-body').innerHTML = html;
216
259
  document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
@@ -12,6 +12,7 @@
12
12
  <span style="color:var(--blue);font-weight:600">Command Center</span>
13
13
  <span>Ask anything, dispatch work, manage plans — powered by Sonnet</span>
14
14
  <button class="cmd-history-btn" onclick="cmdShowHistory()">Past Commands</button>
15
+ <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);border-color:var(--green)" onclick="openQuickNoteModal()">+ Note</button>
15
16
  </div>
16
17
  <div class="cmd-toast" id="cmd-toast"></div>
17
18
  </section>
@@ -19,6 +20,10 @@
19
20
  <h2>Minions Members <span style="font-size:10px;color:var(--border);font-weight:400;text-transform:none;letter-spacing:0">click for details</span></h2>
20
21
  <div class="agents" id="agents-grid">Loading...</div>
21
22
  </section>
23
+ <section>
24
+ <h2>Pinned Context <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:8px" onclick="openPinNoteModal()">+ Pin</button></h2>
25
+ <div id="pinned-content"><p class="empty">No pinned notes.</p></div>
26
+ </section>
22
27
  <section>
23
28
  <h2>Dispatch Queue</h2>
24
29
  <div class="dispatch-stats" id="dispatch-stats"></div>
@@ -1,5 +1,5 @@
1
1
  <section>
2
- <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>
2
+ <h2>Notes Inbox <span class="count" id="inbox-count">0</span> <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:8px" onclick="openQuickNoteModal()">+ Note</button> <span style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">auto-consolidates at 3 notes</span></h2>
3
3
  <div class="inbox-list" id="inbox-list">Loading...</div>
4
4
  </section>
5
5
  <section>
@@ -7,7 +7,7 @@
7
7
  <div id="notes-list">Loading...</div>
8
8
  </section>
9
9
  <section>
10
- <h2>Knowledge Base <span class="count" id="kb-count">0</span> <button id="kb-sweep-btn" onclick="kbSweep()" style="font-size:9px;padding:2px 8px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);border-radius:4px;cursor:pointer;margin-left:8px;vertical-align:middle">sweep</button><span id="kb-swept-time" style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0;margin-left:8px"></span></h2>
10
+ <h2>Knowledge Base <span class="count" id="kb-count">0</span> <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:8px" onclick="openCreateKbModal()">+ New</button><button id="kb-sweep-btn" onclick="kbSweep()" style="font-size:9px;padding:2px 8px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);border-radius:4px;cursor:pointer;margin-left:8px;vertical-align:middle">sweep</button><span id="kb-swept-time" style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0;margin-left:8px"></span></h2>
11
11
  <div class="kb-tabs" id="kb-tabs"></div>
12
12
  <div class="kb-list" id="kb-list"><p class="empty">No knowledge entries yet. Notes are classified here after consolidation.</p></div>
13
13
  </section>
@@ -32,7 +32,7 @@ function buildDashboardHtml() {
32
32
  'utils', 'state', 'detail-panel', 'live-stream',
33
33
  'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
34
34
  'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
35
- 'render-other', 'render-schedules',
35
+ 'render-other', 'render-schedules', 'render-pinned',
36
36
  'command-parser', 'command-input', 'command-center', 'command-history',
37
37
  'modal', 'modal-qa', 'settings', 'refresh'
38
38
  ];
package/dashboard.js CHANGED
@@ -76,7 +76,7 @@ function buildDashboardHtml() {
76
76
  'utils', 'state', 'detail-panel', 'live-stream',
77
77
  'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
78
78
  'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
79
- 'render-other', 'render-schedules',
79
+ 'render-other', 'render-schedules', 'render-pinned',
80
80
  'command-parser', 'command-input', 'command-center', 'command-history',
81
81
  'modal', 'modal-qa', 'settings', 'refresh'
82
82
  ];
@@ -132,6 +132,17 @@ function getMcpServers() {
132
132
  } catch { return []; }
133
133
  }
134
134
 
135
+ function parsePinnedEntries(content) {
136
+ if (!content) return [];
137
+ const entries = [];
138
+ const regex = /###\s*(🔴\s*|🟡\s*)?(.+)\n\n([\s\S]*?)(?=\n\n###|\n\n\*Pinned|$)/g;
139
+ let m;
140
+ while ((m = regex.exec(content)) !== null) {
141
+ entries.push({ level: m[1]?.includes('🔴') ? 'critical' : m[1]?.includes('🟡') ? 'warning' : 'info', title: m[2].trim(), content: m[3].trim() });
142
+ }
143
+ return entries;
144
+ }
145
+
135
146
  let _statusCache = null;
136
147
  let _statusCacheTs = 0;
137
148
  const STATUS_CACHE_TTL = 10000; // 10s — reduces expensive aggregation frequency; mutations call invalidateStatusCache()
@@ -166,6 +177,7 @@ function getStatus() {
166
177
  const runs = shared.safeJson(path.join(MINIONS_DIR, 'engine', 'schedule-runs.json')) || {};
167
178
  return scheds.map(s => ({ ...s, _lastRun: runs[s.id] || null }));
168
179
  })(),
180
+ pinned: (() => { try { return parsePinnedEntries(safeRead(path.join(MINIONS_DIR, 'pinned.md'))); } catch { return []; } })(),
169
181
  projects: PROJECTS.map(p => ({ name: p.name, path: p.localPath, description: p.description || '' })),
170
182
  initialized: !!(CONFIG.agents && Object.keys(CONFIG.agents).length > 0),
171
183
  installId: safeRead(path.join(MINIONS_DIR, '.install-id')).trim() || null,
@@ -900,6 +912,8 @@ const server = http.createServer(async (req, res) => {
900
912
  if (body.scope) item.scope = body.scope;
901
913
  if (body.agent) item.agent = body.agent;
902
914
  if (body.agents) item.agents = body.agents;
915
+ if (body.references) item.references = body.references;
916
+ if (body.acceptanceCriteria) item.acceptanceCriteria = body.acceptanceCriteria;
903
917
  items.push(item);
904
918
  safeWrite(wiPath, items);
905
919
  return jsonReply(res, 200, { ok: true, id });
@@ -936,6 +950,8 @@ const server = http.createServer(async (req, res) => {
936
950
  if (type !== undefined) item.type = type;
937
951
  if (priority !== undefined) item.priority = priority;
938
952
  if (agent !== undefined) item.agent = agent || null;
953
+ if (body.references !== undefined) item.references = body.references;
954
+ if (body.acceptanceCriteria !== undefined) item.acceptanceCriteria = body.acceptanceCriteria;
939
955
  item.updatedAt = new Date().toISOString();
940
956
 
941
957
  safeWrite(wiPath, items);
@@ -2738,12 +2754,69 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2738
2754
  { method: 'GET', path: '/api/health', desc: 'Lightweight health check for monitoring', handler: handleHealth },
2739
2755
 
2740
2756
  // Work items
2741
- { method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: 'title, type?, description?, priority?, project?, agent?, agents?, scope?', handler: handleWorkItemsCreate },
2742
- { method: 'POST', path: '/api/work-items/update', desc: 'Edit a pending/failed work item', params: 'id, source?, title?, description?, type?, priority?, agent?', handler: handleWorkItemsUpdate },
2757
+ { method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: 'title, type?, description?, priority?, project?, agent?, agents?, scope?, references?, acceptanceCriteria?', handler: handleWorkItemsCreate },
2758
+ { method: 'POST', path: '/api/work-items/update', desc: 'Edit a pending/failed work item', params: 'id, source?, title?, description?, type?, priority?, agent?, references?, acceptanceCriteria?', handler: handleWorkItemsUpdate },
2743
2759
  { method: 'POST', path: '/api/work-items/retry', desc: 'Reset a failed/dispatched item to pending', params: 'id, source?', handler: handleWorkItemsRetry },
2744
2760
  { method: 'POST', path: '/api/work-items/delete', desc: 'Remove a work item, kill agent, clear dispatch', params: 'id, source?', handler: handleWorkItemsDelete },
2745
2761
  { method: 'POST', path: '/api/work-items/archive', desc: 'Move a completed/failed work item to archive', params: 'id, source?', handler: handleWorkItemsArchive },
2746
2762
  { method: 'GET', path: '/api/work-items/archive', desc: 'List archived work items', handler: handleWorkItemsArchiveList },
2763
+ { method: 'POST', path: '/api/work-items/feedback', desc: 'Add human feedback on completed work', params: 'id, rating, comment?', handler: async (req, res) => {
2764
+ const body = await readBody(req);
2765
+ const { id, source, rating, comment } = body;
2766
+ if (!id || !rating) return jsonReply(res, 400, { error: 'id and rating required' });
2767
+ const projects = shared.getProjects(CONFIG);
2768
+ const paths = [path.join(MINIONS_DIR, 'work-items.json')];
2769
+ for (const p of projects) paths.push(shared.projectWorkItemsPath(p));
2770
+ for (const wiPath of paths) {
2771
+ const items = JSON.parse(safeRead(wiPath) || '[]');
2772
+ const item = items.find(i => i.id === id);
2773
+ if (!item) continue;
2774
+ item._humanFeedback = { rating, comment: comment || '', at: new Date().toISOString() };
2775
+ safeWrite(wiPath, items);
2776
+ const agent = item.dispatched_to || item.agent || 'unknown';
2777
+ const feedbackNote = '# Human Feedback on ' + id + '\n\n' +
2778
+ '**Rating:** ' + (rating === 'up' ? '👍 Good' : '👎 Needs improvement') + '\n' +
2779
+ '**Item:** ' + (item.title || id) + '\n' +
2780
+ '**Agent:** ' + agent + '\n' +
2781
+ (comment ? '**Feedback:** ' + comment + '\n' : '');
2782
+ const inboxPath = path.join(MINIONS_DIR, 'notes', 'inbox', agent + '-feedback-' + new Date().toISOString().slice(0, 10) + '-' + shared.uid().slice(0, 4) + '.md');
2783
+ safeWrite(inboxPath, feedbackNote);
2784
+ invalidateStatusCache();
2785
+ return jsonReply(res, 200, { ok: true });
2786
+ }
2787
+ return jsonReply(res, 404, { error: 'Work item not found' });
2788
+ }},
2789
+
2790
+ // Pinned notes
2791
+ { method: 'GET', path: '/api/pinned', desc: 'Get pinned notes', handler: async (req, res) => {
2792
+ const content = safeRead(path.join(MINIONS_DIR, 'pinned.md'));
2793
+ return jsonReply(res, 200, { content, entries: parsePinnedEntries(content) });
2794
+ }},
2795
+ { method: 'POST', path: '/api/pinned', desc: 'Add a pinned note', params: 'title, content, level?', handler: async (req, res) => {
2796
+ const body = await readBody(req);
2797
+ const { title, content, level } = body;
2798
+ if (!title || !content) return jsonReply(res, 400, { error: 'title and content required' });
2799
+ const pinnedPath = path.join(MINIONS_DIR, 'pinned.md');
2800
+ const existing = safeRead(pinnedPath);
2801
+ const levelTag = level === 'critical' ? '🔴 ' : level === 'warning' ? '🟡 ' : '';
2802
+ const entry = '\n\n### ' + levelTag + title + '\n\n' + content + '\n\n*Pinned by human on ' + new Date().toISOString().slice(0, 10) + '*';
2803
+ safeWrite(pinnedPath, (existing || '# Pinned Context\n\nCritical notes visible to all agents.') + entry);
2804
+ invalidateStatusCache();
2805
+ return jsonReply(res, 200, { ok: true });
2806
+ }},
2807
+ { method: 'POST', path: '/api/pinned/remove', desc: 'Remove a pinned note by title', params: 'title', handler: async (req, res) => {
2808
+ const body = await readBody(req);
2809
+ const { title } = body;
2810
+ if (!title) return jsonReply(res, 400, { error: 'title required' });
2811
+ const pinnedPath = path.join(MINIONS_DIR, 'pinned.md');
2812
+ let content = safeRead(pinnedPath);
2813
+ if (!content) return jsonReply(res, 404, { error: 'No pinned notes' });
2814
+ const regex = new RegExp('\\n\\n###\\s*(?:🔴\\s*|🟡\\s*)?' + title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\n[\\s\\S]*?(?=\\n\\n###|$)', 'i');
2815
+ content = content.replace(regex, '');
2816
+ safeWrite(pinnedPath, content);
2817
+ invalidateStatusCache();
2818
+ return jsonReply(res, 200, { ok: true });
2819
+ }},
2747
2820
 
2748
2821
  // Notes
2749
2822
  { method: 'POST', path: '/api/notes', desc: 'Write a note to inbox for consolidation', params: 'title, what, why?, author?', handler: handleNotesCreate },
@@ -2780,6 +2853,21 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2780
2853
 
2781
2854
  // Knowledge base
2782
2855
  { method: 'GET', path: '/api/knowledge', desc: 'List all knowledge base entries grouped by category', handler: handleKnowledgeList },
2856
+ { method: 'POST', path: '/api/knowledge', desc: 'Create a knowledge base entry', params: 'category, title, content', handler: async (req, res) => {
2857
+ const body = await readBody(req);
2858
+ const { category, title, content } = body;
2859
+ if (!category || !title || !content) return jsonReply(res, 400, { error: 'category, title, and content required' });
2860
+ const validCategories = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
2861
+ if (!validCategories.includes(category)) return jsonReply(res, 400, { error: 'Invalid category. Must be: ' + validCategories.join(', ') });
2862
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60);
2863
+ const filePath = path.join(MINIONS_DIR, 'knowledge', category, slug + '.md');
2864
+ const dir = path.dirname(filePath);
2865
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
2866
+ const header = '# ' + title + '\n\n*Created by human teammate on ' + new Date().toISOString().slice(0, 10) + '*\n\n';
2867
+ safeWrite(filePath, header + content);
2868
+ invalidateStatusCache();
2869
+ return jsonReply(res, 200, { ok: true, path: filePath });
2870
+ }},
2783
2871
  { method: 'POST', path: '/api/knowledge/sweep', desc: 'Deduplicate, consolidate, and reorganize knowledge base', handler: handleKnowledgeSweep },
2784
2872
  { method: 'GET', path: /^\/api\/knowledge\/([^/]+)\/([^?]+)/, desc: 'Read a specific knowledge base entry', handler: handleKnowledgeRead },
2785
2873
 
package/engine.js CHANGED
@@ -357,6 +357,13 @@ function renderPlaybook(type, vars) {
357
357
  return null;
358
358
  }
359
359
 
360
+ // Inject pinned context (always visible to agents)
361
+ let pinnedContent = '';
362
+ try { pinnedContent = fs.readFileSync(path.join(MINIONS_DIR, 'pinned.md'), 'utf8'); } catch {}
363
+ if (pinnedContent) {
364
+ content += '\n\n---\n\n## Pinned Context (CRITICAL — READ FIRST)\n\n' + pinnedContent;
365
+ }
366
+
360
367
  // Inject team notes context
361
368
  const notes = getNotes();
362
369
  if (notes) {
@@ -2675,6 +2682,14 @@ function discoverFromWorkItems(config, project) {
2675
2682
  };
2676
2683
  try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
2677
2684
 
2685
+ // Inject references and acceptance criteria
2686
+ const refs = (item.references || []).map(r =>
2687
+ '- [' + (r.title || r.url) + '](' + r.url + ')' + (r.type ? ' (' + r.type + ')' : '')
2688
+ ).join('\n');
2689
+ vars.references = refs ? '## References\n\n' + refs : '';
2690
+ const ac = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
2691
+ vars.acceptance_criteria = ac ? '## Acceptance Criteria\n\n' + ac : '';
2692
+
2678
2693
  // Inject ask-specific variables for the ask playbook
2679
2694
  if (workType === 'ask') {
2680
2695
  vars.question = item.title + (item.description ? '\n\n' + item.description : '');
@@ -2962,6 +2977,14 @@ function discoverCentralWorkItems(config) {
2962
2977
  project_path: ap?.localPath || '',
2963
2978
  };
2964
2979
 
2980
+ // Inject references and acceptance criteria
2981
+ const fanRefs = (item.references || []).map(r =>
2982
+ '- [' + (r.title || r.url) + '](' + r.url + ')' + (r.type ? ' (' + r.type + ')' : '')
2983
+ ).join('\n');
2984
+ vars.references = fanRefs ? '## References\n\n' + fanRefs : '';
2985
+ const fanAc = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
2986
+ vars.acceptance_criteria = fanAc ? '## Acceptance Criteria\n\n' + fanAc : '';
2987
+
2965
2988
  if (workType === 'ask') {
2966
2989
  vars.question = item.title + (item.description ? '\n\n' + item.description : '');
2967
2990
  vars.task_id = item.id;
@@ -3029,6 +3052,14 @@ function discoverCentralWorkItems(config) {
3029
3052
  };
3030
3053
  try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
3031
3054
 
3055
+ // Inject references and acceptance criteria
3056
+ const normRefs = (item.references || []).map(r =>
3057
+ '- [' + (r.title || r.url) + '](' + r.url + ')' + (r.type ? ' (' + r.type + ')' : '')
3058
+ ).join('\n');
3059
+ vars.references = normRefs ? '## References\n\n' + normRefs : '';
3060
+ const normAc = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
3061
+ vars.acceptance_criteria = normAc ? '## Acceptance Criteria\n\n' + normAc : '';
3062
+
3032
3063
  // Inject plan-specific variables for the plan playbook
3033
3064
  if (workType === 'plan') {
3034
3065
  // Ensure plans directory exists before agent tries to write
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"