@yemi33/minions 0.1.1676 → 0.1.1678

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1678 (2026-05-02)
4
+
5
+ ### Fixes
6
+ - dashboard screen audit — bug fixes across 11 renderers
7
+
8
+ ## 0.1.1677 (2026-05-02)
9
+
10
+ ### Fixes
11
+ - process bot PR comments as actionable
12
+
3
13
  ## 0.1.1676 (2026-05-02)
4
14
 
5
15
  ### Other
@@ -39,7 +39,7 @@ function renderAgents(agents) {
39
39
  agentData = agents;
40
40
  const grid = document.getElementById('agents-grid');
41
41
  grid.innerHTML = agents.map(a => `
42
- <div class="agent-card ${statusColor(a.status)}" onclick="if(shouldIgnoreSelectionClick(event))return;openAgentDetail('${escapeHtml(a.id)}')">
42
+ <div class="agent-card ${statusColor(a.status)}" data-agent-id="${escapeHtml(a.id)}" onclick="if(shouldIgnoreSelectionClick(event))return;openAgentDetail(this.dataset.agentId)">
43
43
  <div class="agent-card-header">
44
44
  <span class="agent-name"><span class="agent-emoji">${escapeHtml(a.emoji)}</span>${escapeHtml(a.name)}${_runtimeTagHtml(a.runtime)}</span>
45
45
  <span class="status-badge ${escapeHtml(a.status)}">${escapeHtml(a.status)}</span>
@@ -116,7 +116,7 @@ async function openAgentDetail(id) {
116
116
  document.getElementById('detail-content').innerHTML =
117
117
  '<div style="padding:24px;text-align:center">' +
118
118
  '<div style="color:var(--red);margin-bottom:12px">Error loading agent detail: ' + escapeHtml(e.message) + '</div>' +
119
- '<button onclick="openAgentDetail(\'' + escapeHtml(id) + '\')" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-size:12px">Retry</button>' +
119
+ '<button data-agent-id="' + escapeHtml(id) + '" onclick="openAgentDetail(this.dataset.agentId)" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-size:12px">Retry</button>' +
120
120
  ' <button onclick="closeDetail()" style="padding:6px 16px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;font-size:12px;color:var(--text)">Close</button>' +
121
121
  '</div>';
122
122
  }
@@ -18,12 +18,7 @@ function renderEngineStatus(engine) {
18
18
  // Detect stale engine — says running but heartbeat is old (>2 min)
19
19
  if (state === 'running' && engine?.heartbeat) {
20
20
  staleMs = Date.now() - engine.heartbeat;
21
- if (staleMs > 120000) {
22
- state = 'stale';
23
- }
24
- } else if (state === 'running' && !engine?.heartbeat) {
25
- // Running but no heartbeat yet — engine just started or old version
26
- state = 'running';
21
+ if (staleMs > 120000) state = 'stale';
27
22
  }
28
23
 
29
24
  badge.className = 'engine-badge ' + (state === 'stale' ? 'stopped' : state);
@@ -172,12 +167,12 @@ function renderDispatch(dispatch) {
172
167
  '</tr>';
173
168
  }).join('') + '</tbody></table>';
174
169
  if (completed.length > COMPLETED_PER_PAGE) {
175
- completedEl.innerHTML += '<div class="pr-pager">' +
170
+ completedEl.insertAdjacentHTML('beforeend', '<div class="pr-pager">' +
176
171
  '<span class="pr-page-info">Showing ' + (compStart + 1) + ' to ' + Math.min(compStart + COMPLETED_PER_PAGE, completed.length) + ' of ' + completed.length + '</span>' +
177
172
  '<div class="pr-pager-btns">' +
178
173
  '<button class="pr-pager-btn ' + (_completedPage === 0 ? 'disabled' : '') + '" onclick="_completedPrev()">Prev</button>' +
179
174
  '<button class="pr-pager-btn ' + (_completedPage >= totalCompPages - 1 ? 'disabled' : '') + '" onclick="_completedNext()">Next</button>' +
180
- '</div></div>';
175
+ '</div></div>');
181
176
  }
182
177
  } else {
183
178
  completedEl.innerHTML = '<p class="empty">No completed dispatches yet.</p>';
@@ -206,12 +201,12 @@ function renderEngineLog(log) {
206
201
  '</div>'
207
202
  ).join('');
208
203
  if (reversed.length > LOG_PER_PAGE) {
209
- el.innerHTML += '<div class="pr-pager">' +
204
+ el.insertAdjacentHTML('beforeend', '<div class="pr-pager">' +
210
205
  '<span class="pr-page-info">Showing ' + (logStart + 1) + ' to ' + Math.min(logStart + LOG_PER_PAGE, reversed.length) + ' of ' + reversed.length + '</span>' +
211
206
  '<div class="pr-pager-btns">' +
212
207
  '<button class="pr-pager-btn ' + (_logPage === 0 ? 'disabled' : '') + '" onclick="_logPrev()">Prev</button>' +
213
208
  '<button class="pr-pager-btn ' + (_logPage >= totalLogPages - 1 ? 'disabled' : '') + '" onclick="_logNext()">Next</button>' +
214
- '</div></div>';
209
+ '</div></div>');
215
210
  }
216
211
  }
217
212
 
@@ -228,7 +223,7 @@ async function showErrorDetails(agentId, reason, task) {
228
223
  document.getElementById('modal').classList.add('open');
229
224
 
230
225
  try {
231
- const output = await fetch('/api/agent/' + agentId + '/output').then(r => r.text());
226
+ const output = await safeFetch('/api/agent/' + agentId + '/output').then(r => r.text());
232
227
  const lines = output.split('\n');
233
228
  const stderrIdx = lines.findIndex(l => l.startsWith('## stderr'));
234
229
  let summary = '';
@@ -43,12 +43,12 @@ function renderInbox(inbox) {
43
43
  </div>`;
44
44
  }).join('');
45
45
  if (inbox.length > INBOX_PER_PAGE) {
46
- list.innerHTML += '<div class="pr-pager">' +
46
+ list.insertAdjacentHTML('beforeend', '<div class="pr-pager">' +
47
47
  '<span class="pr-page-info">Showing ' + (inboxStart + 1) + ' to ' + Math.min(inboxStart + INBOX_PER_PAGE, inbox.length) + ' of ' + inbox.length + '</span>' +
48
48
  '<div class="pr-pager-btns">' +
49
49
  '<button class="pr-pager-btn ' + (_inboxPage === 0 ? 'disabled' : '') + '" onclick="_inboxPrev()">Prev</button>' +
50
50
  '<button class="pr-pager-btn ' + (_inboxPage >= totalInboxPages - 1 ? 'disabled' : '') + '" onclick="_inboxNext()">Next</button>' +
51
- '</div></div>';
51
+ '</div></div>');
52
52
  }
53
53
  restoreNotifBadges();
54
54
  }
@@ -134,12 +134,12 @@ function renderKnowledgeBase() {
134
134
  '</div>';
135
135
  }).join('');
136
136
  if (items.length > KB_PER_PAGE) {
137
- listEl.innerHTML += '<div class="pr-pager">' +
137
+ listEl.insertAdjacentHTML('beforeend', '<div class="pr-pager">' +
138
138
  '<span class="pr-page-info">Showing ' + (kbStart + 1) + ' to ' + Math.min(kbStart + KB_PER_PAGE, items.length) + ' of ' + items.length + '</span>' +
139
139
  '<div class="pr-pager-btns">' +
140
140
  '<button class="pr-pager-btn ' + (_kbPage === 0 ? 'disabled' : '') + '" onclick="_kbPrev()">Prev</button>' +
141
141
  '<button class="pr-pager-btn ' + (_kbPage >= totalKbPages - 1 ? 'disabled' : '') + '" onclick="_kbNext()">Next</button>' +
142
- '</div></div>';
142
+ '</div></div>');
143
143
  }
144
144
  restoreNotifBadges();
145
145
  }
@@ -242,18 +242,18 @@ function openCreateKbModal() {
242
242
  '<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>' +
243
243
  '<div style="display:flex;justify-content:flex-end;gap:8px">' +
244
244
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
245
- '<button onclick="submitKbEntry()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Save</button>' +
245
+ '<button onclick="submitKbEntry(event)" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Save</button>' +
246
246
  '</div>' +
247
247
  '</div>';
248
248
  document.getElementById('modal').classList.add('open');
249
249
  }
250
250
 
251
- async function submitKbEntry() {
252
- var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Creating...'; }
251
+ async function submitKbEntry(e) {
252
+ var btn = (e || window.event)?.target; if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
253
253
  const category = document.getElementById('kb-new-category').value;
254
254
  const title = document.getElementById('kb-new-title').value;
255
255
  const content = document.getElementById('kb-new-content').value;
256
- if (!title || !content) { if (btn) { btn.disabled = false; btn.textContent = 'Create'; } alert('Title and content are required'); return; }
256
+ if (!title || !content) { if (btn) { btn.disabled = false; btn.textContent = 'Save'; } alert('Title and content are required'); return; }
257
257
  try {
258
258
  showToast('cmd-toast', 'KB entry created', true);
259
259
  const res = await fetch('/api/knowledge', {
@@ -261,8 +261,8 @@ async function submitKbEntry() {
261
261
  body: JSON.stringify({ category, title, content })
262
262
  });
263
263
  if (res.ok) { closeModal(); refreshKnowledgeBase(); }
264
- else { if (btn) { btn.disabled = false; btn.textContent = 'Create'; } const d = await res.json().catch(() => ({})); showToast('cmd-toast', 'KB create failed: ' + (d.error || 'unknown'), false); }
265
- } catch (e) { if (btn) { btn.disabled = false; btn.textContent = 'Create'; } showToast('cmd-toast', 'Error: ' + e.message, false); }
264
+ else { if (btn) { btn.disabled = false; btn.textContent = 'Save'; } const d = await res.json().catch(() => ({})); showToast('cmd-toast', 'KB create failed: ' + (d.error || 'unknown'), false); }
265
+ } catch (err) { if (btn) { btn.disabled = false; btn.textContent = 'Save'; } showToast('cmd-toast', 'Error: ' + err.message, false); }
266
266
  }
267
267
 
268
268
  async function kbOpenItem(category, file) {
@@ -26,7 +26,7 @@ function renderMeetings(meetings) {
26
26
  const visible = _showArchived ? meetings : active;
27
27
  if (visible.length === 0) {
28
28
  el.innerHTML = '<p class="empty">No active meetings.</p>';
29
- if (archived.length) el.innerHTML += '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:10px" onclick="_toggleArchivedMeetings()">Show ' + archived.length + ' archived</button></div>';
29
+ if (archived.length) el.insertAdjacentHTML('beforeend', '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:10px" onclick="_toggleArchivedMeetings()">Show ' + archived.length + ' archived</button></div>');
30
30
  return;
31
31
  }
32
32
 
@@ -66,17 +66,17 @@ function renderMeetings(meetings) {
66
66
  }).join('');
67
67
 
68
68
  if (visible.length > MTG_PER_PAGE) {
69
- el.innerHTML += '<div class="pr-pager">' +
69
+ el.insertAdjacentHTML('beforeend', '<div class="pr-pager">' +
70
70
  '<span class="pr-page-info">Showing ' + (start + 1) + ' to ' + Math.min(start + MTG_PER_PAGE, visible.length) + ' of ' + visible.length + '</span>' +
71
71
  '<div class="pr-pager-btns">' +
72
72
  '<button class="pr-pager-btn ' + (_mtgPage === 0 ? 'disabled' : '') + '" onclick="_mtgPrev()">Prev</button>' +
73
73
  '<button class="pr-pager-btn ' + (_mtgPage >= totalPages - 1 ? 'disabled' : '') + '" onclick="_mtgNext()">Next</button>' +
74
- '</div></div>';
74
+ '</div></div>');
75
75
  }
76
76
 
77
77
  if (archived.length > 0) {
78
- el.innerHTML += '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:10px" onclick="_toggleArchivedMeetings()">' +
79
- (_showArchived ? 'Hide' : 'Show') + ' ' + archived.length + ' archived</button></div>';
78
+ el.insertAdjacentHTML('beforeend', '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:10px" onclick="_toggleArchivedMeetings()">' +
79
+ (_showArchived ? 'Hide' : 'Show') + ' ' + archived.length + ' archived</button></div>');
80
80
  }
81
81
  restoreNotifBadges();
82
82
  }
@@ -254,14 +254,14 @@ function openCreateMeetingModal() {
254
254
  '<div style="color:var(--text);font-size:var(--text-md)">Participants<div style="display:flex;flex-direction:column;gap:4px;margin-top:4px" id="mtg-participants">' + agentOpts + '</div></div>' +
255
255
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
256
256
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
257
- '<button onclick="_submitCreateMeeting()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Start Meeting</button>' +
257
+ '<button onclick="_submitCreateMeeting(event)" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Start Meeting</button>' +
258
258
  '</div>' +
259
259
  '</div>';
260
260
  document.getElementById('modal').classList.add('open');
261
261
  }
262
262
 
263
- async function _submitCreateMeeting() {
264
- var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Starting...'; }
263
+ async function _submitCreateMeeting(e) {
264
+ var btn = (e || window.event)?.target; if (btn) { btn.disabled = true; btn.textContent = 'Starting...'; }
265
265
  const title = document.getElementById('mtg-title')?.value?.trim();
266
266
  const agenda = document.getElementById('mtg-agenda')?.value?.trim();
267
267
  if (!title || !agenda) { if (btn) { btn.disabled = false; btn.textContent = 'Start Meeting'; } alert('Title and agenda required'); return; }
@@ -4,7 +4,7 @@ function renderPinned(entries) {
4
4
  entries = (entries || []).filter(function(e) { return !isDeleted('pin:' + e.title); });
5
5
  const el = document.getElementById('pinned-content');
6
6
  if (!el) return;
7
- if (!entries || entries.length === 0) {
7
+ if (entries.length === 0) {
8
8
  el.innerHTML = '<p class="empty">No pinned notes. Pin important context that all agents should see.</p>';
9
9
  return;
10
10
  }
@@ -40,7 +40,7 @@ async function submitPinnedNote(e) {
40
40
  const title = document.getElementById('pin-title').value;
41
41
  const content = document.getElementById('pin-content').value;
42
42
  const level = document.getElementById('pin-level').value;
43
- if (!title || !content) { if (btn) { btn.disabled = false; btn.textContent = 'Pin Note'; } alert('Title and content required'); return; }
43
+ if (!title || !content) { if (btn) { btn.disabled = false; btn.textContent = 'Pin'; } alert('Title and content required'); return; }
44
44
  try { closeModal(); } catch { /* may not be open */ }
45
45
 
46
46
  // Optimistic render: append the new entry to the pinned list and re-render immediately
@@ -21,15 +21,15 @@ function openCreatePlanModal() {
21
21
  '<div style="font-size:11px;color:var(--muted)">After creating, click Execute on the plan card to have an agent convert it into a PRD with work items.</div>' +
22
22
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
23
23
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
24
- '<button onclick="_submitCreatePlan()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Create Plan</button>' +
24
+ '<button onclick="_submitCreatePlan(event)" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Create Plan</button>' +
25
25
  '</div>' +
26
26
  '</div>';
27
27
  document.getElementById('modal').classList.add('open');
28
28
  setTimeout(() => document.getElementById('plan-new-title')?.focus(), 100);
29
29
  }
30
30
 
31
- async function _submitCreatePlan() {
32
- var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Creating...'; }
31
+ async function _submitCreatePlan(e) {
32
+ var btn = (e || window.event)?.target; if (btn) { btn.disabled = true; btn.textContent = 'Creating...'; }
33
33
  const title = document.getElementById('plan-new-title')?.value?.trim();
34
34
  const content = document.getElementById('plan-new-content')?.value?.trim();
35
35
  if (!title) { if (btn) { btn.disabled = false; btn.textContent = 'Create Plan'; } alert('Title is required'); return; }
@@ -144,16 +144,13 @@ function renderPlans(plans) {
144
144
  }
145
145
  }
146
146
 
147
- // Link .md plans to their PRD .json — if a PRD is being worked on, the source plan is too
148
- // Convention: plan-w025-2026-03-15.md → officeagent-2026-03-15.json (same date, different prefix)
147
+ // Link .md plans to their PRD .json — if a PRD is being worked on, the source plan is too.
148
+ // The .md → .json mapping is materialized in planToPrdFile below; here we only need to
149
+ // know whether any PRD is active to mark all candidate source .md plans as working.
149
150
  const workingJsons = new Set([...workingPlanFiles].filter(f => f.endsWith('.json')));
150
151
  if (workingJsons.size > 0) {
151
152
  for (const p of plans) {
152
- if (p.format === 'draft' && p.file.endsWith('.md')) {
153
- // A .md plan is "working" if any PRD .json has active dispatches
154
- // (since the .md is the source that generated those PRDs)
155
- if (workingJsons.size > 0) workingPlanFiles.add(p.file);
156
- }
153
+ if (p.format === 'draft' && p.file.endsWith('.md')) workingPlanFiles.add(p.file);
157
154
  }
158
155
  }
159
156
 
@@ -134,15 +134,15 @@ function openAddPrModal() {
134
134
  '<div style="font-size:11px;color:var(--muted);margin-top:-4px;padding-left:24px">Off = context only (e.g. teammate\'s PR). On = agents actively monitor and fix issues.</div>' +
135
135
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
136
136
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
137
- '<button onclick="_submitLinkPr()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Link PR</button>' +
137
+ '<button onclick="_submitLinkPr(event)" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Link PR</button>' +
138
138
  '</div>' +
139
139
  '</div>';
140
140
  document.getElementById('modal').classList.add('open');
141
141
  setTimeout(() => document.getElementById('pr-link-url')?.focus(), 100);
142
142
  }
143
143
 
144
- async function _submitLinkPr() {
145
- var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Linking...'; }
144
+ async function _submitLinkPr(e) {
145
+ var btn = (e || window.event)?.target; if (btn) { btn.disabled = true; btn.textContent = 'Linking...'; }
146
146
  const url = document.getElementById('pr-link-url')?.value?.trim();
147
147
  if (!url) { if (btn) { btn.disabled = false; btn.textContent = 'Link PR'; } alert('PR URL is required'); return; }
148
148
  const title = document.getElementById('pr-link-title')?.value?.trim() || '';
@@ -166,7 +166,8 @@ async function unlinkPr(id) {
166
166
  if (!confirm('Remove ' + id + ' from tracking?')) return;
167
167
  showToast('cmd-toast', id + ' removed', true);
168
168
  markDeleted('pr:' + id);
169
- const row = document.querySelector('[data-pr-id="' + id + '"]')?.closest('tr');
169
+ const escId = window.CSS && CSS.escape ? CSS.escape(id) : id;
170
+ const row = document.querySelector('[data-pr-id="' + escId + '"]')?.closest('tr');
170
171
  if (row) row.remove();
171
172
  try {
172
173
  const res = await fetch('/api/pull-requests/delete', {
@@ -292,7 +292,7 @@ function renderSchedules(schedules) {
292
292
  const lastRun = s._lastRun ? timeAgo(s._lastRun) : 'never';
293
293
  const typeBadge = '<span class="dispatch-type ' + escHtml(s.type || 'implement') + '">' + escHtml(s.type || 'implement') + '</span>';
294
294
  const humanCron = _cronToHuman(s.cron || '');
295
- html += '<tr style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openScheduleDetail(\'' + escHtml(s.id) + '\')">' +
295
+ html += '<tr data-sched-id="' + escHtml(s.id || '') + '" style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openScheduleDetail(\'' + escHtml(s.id) + '\')">' +
296
296
  '<td><span class="pr-id">' + escHtml(s.id || '') + '</span></td>' +
297
297
  '<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(s.title || '') + '">' + escHtml(s.title || '') + '</td>' +
298
298
  '<td><span title="' + escHtml(s.cron || '') + '" style="font-size:11px;color:var(--blue)">' + escHtml(humanCron) + '</span></td>' +
@@ -459,7 +459,7 @@ function _scheduleFormHtml(sched, isEdit) {
459
459
  '</label>' +
460
460
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
461
461
  '<button onclick="closeModal()" class="pr-pager-btn" style="padding:6px 16px;font-size:var(--text-md)">Cancel</button>' +
462
- '<button onclick="submitSchedule(' + isEdit + ')" style="padding:6px 16px;font-size:var(--text-md);background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">' + (isEdit ? 'Save' : 'Create') + '</button>' +
462
+ '<button onclick="submitSchedule(' + isEdit + ',event)" style="padding:6px 16px;font-size:var(--text-md);background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">' + (isEdit ? 'Save' : 'Create') + '</button>' +
463
463
  '</div>' +
464
464
  '</div>';
465
465
  }
@@ -487,8 +487,8 @@ function openEditScheduleModal(id) {
487
487
  _updateCronPreview();
488
488
  }
489
489
 
490
- async function submitSchedule(isEdit) {
491
- var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = isEdit ? 'Saving...' : 'Creating...'; }
490
+ async function submitSchedule(isEdit, e) {
491
+ var btn = (e || window.event)?.target; if (btn) { btn.disabled = true; btn.textContent = isEdit ? 'Saving...' : 'Creating...'; }
492
492
  _showScheduleError('');
493
493
  const title = document.getElementById('sched-edit-title').value.trim();
494
494
  const cron = window._schedComputedCron || '';
@@ -505,7 +505,7 @@ async function submitSchedule(isEdit) {
505
505
  id = _generateScheduleId(title);
506
506
  }
507
507
 
508
- function _resetSchedBtn() { if (btn) { btn.disabled = false; btn.textContent = isEdit ? 'Save Changes' : 'Create Schedule'; } }
508
+ function _resetSchedBtn() { if (btn) { btn.disabled = false; btn.textContent = isEdit ? 'Save' : 'Create'; } }
509
509
  if (!title) { _resetSchedBtn(); _showScheduleError('Title is required'); return; }
510
510
  if (!cron) { _resetSchedBtn(); _showScheduleError('Schedule is required \u2014 select days and time, or use natural language'); return; }
511
511
 
@@ -523,9 +523,15 @@ async function submitSchedule(isEdit) {
523
523
  } catch (e) { _resetSchedBtn(); _showScheduleError('Error: ' + e.message); }
524
524
  }
525
525
 
526
+ function _findSchedRow(id) {
527
+ var sel = 'tr[data-sched-id="' + (window.CSS && CSS.escape ? CSS.escape(id) : id) + '"]';
528
+ return document.querySelector(sel);
529
+ }
530
+
526
531
  async function toggleScheduleEnabled(id, enabled) {
527
532
  // Optimistic toggle — swap badge text immediately
528
- document.querySelectorAll('tr').forEach(function(r) { if (r.textContent.includes(id)) { var badge = r.querySelector('.status-badge'); if (badge) badge.textContent = enabled ? 'ENABLED' : 'DISABLED'; } });
533
+ var row = _findSchedRow(id);
534
+ if (row) { var badge = row.querySelector('.status-badge'); if (badge) badge.textContent = enabled ? 'ENABLED' : 'DISABLED'; }
529
535
  try {
530
536
  const res = await fetch('/api/schedules/update', {
531
537
  method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -542,7 +548,8 @@ async function deleteSchedule(id) {
542
548
  if (!confirm('Delete scheduled task "' + id + '"?')) return;
543
549
  showToast('cmd-toast', 'Schedule deleted', true);
544
550
  markDeleted('sched:' + id);
545
- document.querySelectorAll('tr').forEach(function(r) { if (r.textContent.includes(id)) r.remove(); });
551
+ var row = _findSchedRow(id);
552
+ if (row) row.remove();
546
553
  try {
547
554
  const res = await fetch('/api/schedules/delete', {
548
555
  method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -184,8 +184,10 @@ function toggleWatchPause(id, pause) {
184
184
  method: 'POST',
185
185
  headers: { 'Content-Type': 'application/json' },
186
186
  body: JSON.stringify({ id: id, status: newStatus })
187
- }).then(function(res) { return res.json(); }).then(function(data) {
188
- if (data.error) showToast('cmd-toast', 'Error: ' + data.error, false);
187
+ }).then(async function(res) {
188
+ var data = await res.json().catch(function() { return {}; });
189
+ if (!res.ok || data.error) showToast('cmd-toast', 'Error: ' + (data.error || ('HTTP ' + res.status)), false);
190
+ else if (typeof refresh === 'function') refresh();
189
191
  }).catch(function(err) {
190
192
  showToast('cmd-toast', 'Error: ' + err.message, false);
191
193
  });
@@ -297,12 +299,14 @@ function submitWatch() {
297
299
  stopAfter: stopAfter,
298
300
  onNotMet: onNotMet || null,
299
301
  })
300
- }).then(function(res) { return res.json(); }).then(function(data) {
301
- if (data.error) {
302
- showToast('cmd-toast', 'Error: ' + data.error, false);
302
+ }).then(async function(res) {
303
+ var data = await res.json().catch(function() { return {}; });
304
+ if (!res.ok || data.error) {
305
+ showToast('cmd-toast', 'Error: ' + (data.error || ('HTTP ' + res.status)), false);
303
306
  } else {
304
- showToast('cmd-toast', 'Watch created: ' + data.watch.id, true);
307
+ showToast('cmd-toast', 'Watch created: ' + (data.watch && data.watch.id || ''), true);
305
308
  closeModal();
309
+ if (typeof refresh === 'function') refresh();
306
310
  }
307
311
  }).catch(function(err) {
308
312
  showToast('cmd-toast', 'Error: ' + err.message, false);
@@ -40,7 +40,7 @@ function wiRow(item) {
40
40
  : (item.branchStrategy === 'shared-branch' && item.status === 'done')
41
41
  ? '<span style="font-size:9px;color:var(--muted)" title="Part of shared branch — aggregate PR created at verify stage">shared branch</span>'
42
42
  : '<span style="color:var(--muted)">—</span>';
43
- return '<tr style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openWorkItemDetail(\'' + escapeHtml(item.id) + '\')">' +
43
+ return '<tr data-wi-id="' + escapeHtml(item.id) + '" style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openWorkItemDetail(\'' + escapeHtml(item.id) + '\')">' +
44
44
  '<td><span class="pr-id">' + escapeHtml(item.id || '') + '</span></td>' +
45
45
  '<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml((item.title || '').slice(0, 200)) + '">' + escapeHtml(item.title || '') + '</td>' +
46
46
  '<td><span style="font-size:10px;color:var(--muted)">' + escapeHtml(item._source || '') + '</span>' +
@@ -195,12 +195,16 @@ async function submitWorkItemEdit(id, source, e) {
195
195
  } catch (e) { alert('Update error: ' + e.message); editWorkItem(id, source); }
196
196
  }
197
197
 
198
+ function _removeWiRow(id) {
199
+ var sel = 'tr[data-wi-id="' + (window.CSS && CSS.escape ? CSS.escape(id) : id) + '"]';
200
+ document.querySelectorAll(sel).forEach(function(r) { r.remove(); });
201
+ }
202
+
198
203
  async function deleteWorkItem(id, source) {
199
204
  if (!confirm('Delete work item ' + id + '? This will kill any running agent and remove all dispatch history.')) return;
200
205
  showToast('cmd-toast', 'Work item deleted', true);
201
206
  markDeleted('wi:' + id);
202
- var wiTable = document.getElementById('work-items-content');
203
- (wiTable || document).querySelectorAll('tr').forEach(function(r) { if (r.textContent.includes(id)) r.remove(); });
207
+ _removeWiRow(id);
204
208
  try {
205
209
  const res = await fetch('/api/work-items/delete', {
206
210
  method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -214,8 +218,7 @@ async function cancelWorkItem(id, source) {
214
218
  if (!confirm('Cancel work item ' + id + '? This will kill any running agent and mark it cancelled.')) return;
215
219
  showToast('cmd-toast', 'Work item cancelled', true);
216
220
  markDeleted('wi:' + id);
217
- var wiTable = document.getElementById('work-items-content');
218
- (wiTable || document).querySelectorAll('tr').forEach(function(r) { if (r.textContent.includes(id)) r.remove(); });
221
+ _removeWiRow(id);
219
222
  try {
220
223
  const res = await fetch('/api/work-items/cancel', {
221
224
  method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -228,8 +231,7 @@ async function cancelWorkItem(id, source) {
228
231
  async function archiveWorkItem(id, source) {
229
232
  showToast('cmd-toast', 'Archived ' + id, true);
230
233
  markDeleted('wi:' + id);
231
- var wiTable = document.getElementById('work-items-content');
232
- (wiTable || document).querySelectorAll('tr').forEach(function(r) { if (r.textContent.includes(id)) r.remove(); });
234
+ _removeWiRow(id);
233
235
  try {
234
236
  const res = await fetch('/api/work-items/archive', {
235
237
  method: 'POST', headers: { 'Content-Type': 'application/json' },
package/engine/ado.js CHANGED
@@ -795,10 +795,9 @@ async function pollPrHumanComments(config) {
795
795
  for (const comment of (thread.comments || [])) {
796
796
  if (!comment.content || comment.commentType === 'system') continue;
797
797
  const content = comment.content;
798
- // Skip bots, CI noise, and ignored authors
798
+ // Skip explicitly ignored authors and CI-report bodies, but do not ignore bot authors by default.
799
799
  const authorName = (comment.author?.displayName || '').toLowerCase();
800
800
  if (ignoredAuthors.some(a => authorName.includes(a))) continue;
801
- if (/\b(bot|service|build|pipeline|codecov|sonar)\b/i.test(authorName)) continue;
802
801
  if (/^#{1,3}\s*(Coverage|Build|Test|Deploy|Pipeline)\s*(Report|Status|Result|Summary)/i.test(content)) continue;
803
802
  // Detect agent comments (included in context, but don't trigger fix)
804
803
  const isAgent = /\bMinions\s*\(/i.test(content) || /\bby\s+Minions\b/i.test(content) || /\[minions\]/i.test(content);
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-02T04:37:01.667Z"
4
+ "cachedAt": "2026-05-02T05:22:33.108Z"
5
5
  }
package/engine/github.js CHANGED
@@ -561,14 +561,12 @@ async function pollPrHumanComments(config) {
561
561
  ...(Array.isArray(reviewComments) ? reviewComments : []).map(c => ({ ...c, _type: 'review' }))
562
562
  ];
563
563
 
564
- // Separate: agent comments (included in context, don't trigger fix) vs human comments (trigger fix)
565
- // All non-bot, non-CI comments go into context. Only non-agent comments trigger pendingFix.
564
+ // Separate: agent comments (included in context, don't trigger fix) vs actionable comments (trigger fix).
565
+ // Bot-authored comments are actionable unless explicitly ignored or clearly CI report noise.
566
566
  const ignoredAuthors = new Set((config.engine?.ignoredCommentAuthors || []).map(a => a.toLowerCase()));
567
- function _isBot(c) {
568
- if (c.user?.type === 'Bot') return true;
567
+ function _isIgnoredComment(c) {
569
568
  const login = (c.user?.login || '').toLowerCase();
570
569
  if (ignoredAuthors.has(login)) return true;
571
- if (/\b(bot|codecov|sonar|dependabot|renovate|github-actions|azure-pipelines)\b/i.test(login)) return true;
572
570
  const body = c.body || '';
573
571
  if (/^#{1,3}\s*(Coverage|Build|Test|Deploy|Pipeline)\s*(Report|Status|Result|Summary)/i.test(body)) return true;
574
572
  if (/!\[.*\]\(https?:\/\/.*badge/i.test(body)) return true;
@@ -581,16 +579,16 @@ async function pollPrHumanComments(config) {
581
579
  if (/\[minions\]/i.test(body)) return true;
582
580
  return false;
583
581
  }
584
- const humanComments = allComments.filter(c => !_isBot(c));
582
+ const actionableComments = allComments.filter(c => !_isIgnoredComment(c));
585
583
 
586
584
  const cutoffStr = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
587
585
  const cutoffMs = new Date(cutoffStr).getTime() || 0;
588
586
 
589
- // Collect ALL human comments for full context, track new ones for triggering
587
+ // Collect ALL actionable comments for full context, track new ones for triggering
590
588
  const allCommentEntries = [];
591
589
  const newComments = [];
592
590
 
593
- for (const c of humanComments) {
591
+ for (const c of actionableComments) {
594
592
  const date = c.created_at || c.updated_at || '';
595
593
  const isAgent = _isAgentComment(c);
596
594
  const entry = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1676",
3
+ "version": "0.1.1678",
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"