@yemi33/minions 0.1.1677 → 0.1.1679

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.1679 (2026-05-02)
4
+
5
+ ### Fixes
6
+ - preserve PR context for linked fix work items
7
+
8
+ ## 0.1.1678 (2026-05-02)
9
+
10
+ ### Fixes
11
+ - dashboard screen audit — bug fixes across 11 renderers
12
+
3
13
  ## 0.1.1677 (2026-05-02)
4
14
 
5
15
  ### Fixes
@@ -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/dashboard.js CHANGED
@@ -110,6 +110,22 @@ function normalizePrMetadata(metadata) {
110
110
  };
111
111
  }
112
112
 
113
+ function getWorkItemPrRef(input) {
114
+ if (!input || typeof input !== 'object') return null;
115
+ return input.targetPr || input.pr || input.prId || input.prNumber || input.pullRequest || input.sourcePr || input.prUrl || null;
116
+ }
117
+
118
+ function copyWorkItemPrFields(item, input, pr = null) {
119
+ const prRef = getWorkItemPrRef(input);
120
+ if (!prRef && !pr) return;
121
+ const prNumber = pr ? shared.getPrNumber(pr) : shared.getPrNumber(prRef);
122
+ item.targetPr = pr?.id || prRef;
123
+ item.pr_id = pr?.id || (typeof prRef === 'string' ? prRef : '');
124
+ if (prNumber != null) item.prNumber = prNumber;
125
+ if (pr?.branch || input.prBranch) item.prBranch = pr?.branch || input.prBranch;
126
+ if (pr?.title || input.prTitle) item.prTitle = pr?.title || input.prTitle;
127
+ if (pr?.url || input.prUrl) item.prUrl = pr?.url || input.prUrl;
128
+ }
113
129
  function linkPullRequestForTracking({ url, title, project: projectName, autoObserve, context, workItemId }, config = CONFIG, options = {}) {
114
130
  if (!url) {
115
131
  const err = new Error('url required');
@@ -1395,6 +1411,8 @@ async function executeCCActions(actions) {
1395
1411
  const workType = routing.normalizeWorkType(action.workType || (action.type !== 'dispatch' ? action.type : WORK_TYPE.IMPLEMENT), WORK_TYPE.IMPLEMENT);
1396
1412
  const id = 'W-' + shared.uid();
1397
1413
  const project = action.project || '';
1414
+ const prRef = getWorkItemPrRef(action);
1415
+ let linkedPr = null;
1398
1416
 
1399
1417
  // Strict project resolution. Silent fallback to PROJECTS[0] when the model named an unknown
1400
1418
  // project caused work items to land in the wrong repo. Now: unknown name → error; ambiguous
@@ -1408,6 +1426,18 @@ async function executeCCActions(actions) {
1408
1426
  results.push({ type: action.type, error: `Project "${project}" not found. Known projects: ${known}` });
1409
1427
  break;
1410
1428
  }
1429
+ } else if (prRef) {
1430
+ const allPrs = getPullRequests().filter(p => !p._ghost);
1431
+ linkedPr = shared.findPrRecord(allPrs, prRef) || null;
1432
+ if (linkedPr?._project && linkedPr._project !== 'central') {
1433
+ targetProject = PROJECTS.find(p => p.name?.toLowerCase() === String(linkedPr._project).toLowerCase()) || null;
1434
+ }
1435
+ if (!targetProject && PROJECTS.length > 1) {
1436
+ results.push({ type: action.type, error: `project field is required when ${PROJECTS.length} projects are configured: ${PROJECTS.map(p => p.name).join(', ')}` });
1437
+ break;
1438
+ } else if (!targetProject && PROJECTS.length === 1) {
1439
+ targetProject = PROJECTS[0];
1440
+ }
1411
1441
  } else if (PROJECTS.length > 1) {
1412
1442
  results.push({ type: action.type, error: `project field is required when ${PROJECTS.length} projects are configured: ${PROJECTS.map(p => p.name).join(', ')}` });
1413
1443
  break;
@@ -1416,6 +1446,16 @@ async function executeCCActions(actions) {
1416
1446
  }
1417
1447
  // PROJECTS.length === 0 → targetProject stays null, falls back to root work-items.json (existing behavior).
1418
1448
 
1449
+ if (prRef && !linkedPr && targetProject) {
1450
+ const projectPrs = shared.safeJson(shared.projectPrPath(targetProject)) || [];
1451
+ shared.normalizePrRecords(projectPrs, targetProject);
1452
+ linkedPr = shared.findPrRecord(projectPrs, prRef, targetProject) || null;
1453
+ }
1454
+ if (prRef && (workType === WORK_TYPE.FIX || workType === WORK_TYPE.REVIEW || workType === WORK_TYPE.TEST) && !linkedPr) {
1455
+ results.push({ type: action.type, error: `PR not found: ${prRef}` });
1456
+ break;
1457
+ }
1458
+
1419
1459
  const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : path.join(MINIONS_DIR, 'work-items.json');
1420
1460
 
1421
1461
  // Promote `agent` (singular) → `agents` (array). Models emit either shape and the prior code
@@ -1438,14 +1478,16 @@ async function executeCCActions(actions) {
1438
1478
  const isOneShot = action.oneShot === true || (action.oneShot !== false && ccOneShotTypes.has(workType));
1439
1479
  shared.mutateJsonFileLocked(wiPath, items => {
1440
1480
  if (!Array.isArray(items)) items = [];
1441
- items.push({
1481
+ const item = {
1442
1482
  id, title: action.title, type: workType,
1443
1483
  priority: action.priority || 'medium', description: action.description || '',
1444
1484
  status: WI_STATUS.PENDING, created: new Date().toISOString(),
1445
- createdBy: 'command-center', project,
1485
+ createdBy: 'command-center', project: targetProject?.name || project,
1446
1486
  ...(agentHints.length ? { preferred_agent: agentHints[0], agents: agentHints } : {}),
1447
1487
  ...(isOneShot ? { oneShot: true } : {}),
1448
- });
1488
+ };
1489
+ copyWorkItemPrFields(item, action, linkedPr);
1490
+ items.push(item);
1449
1491
  return items;
1450
1492
  }, { defaultValue: [] });
1451
1493
  results.push({ type: action.type, id, ok: true });
@@ -2740,6 +2782,7 @@ const server = http.createServer(async (req, res) => {
2740
2782
  if (body.acceptanceCriteria) item.acceptanceCriteria = body.acceptanceCriteria;
2741
2783
  if (body.skipPr) item.skipPr = true;
2742
2784
  if (body.oneShot) item.oneShot = true;
2785
+ copyWorkItemPrFields(item, body);
2743
2786
  let dupId = null;
2744
2787
  mutateJsonFileLocked(wiPath, (items) => {
2745
2788
  if (!Array.isArray(items)) items = [];
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-02T05:19:51.665Z"
4
+ "cachedAt": "2026-05-02T05:46:43.671Z"
5
5
  }
@@ -628,9 +628,13 @@ function selectPlaybook(workType, item) {
628
628
  if (workType === WORK_TYPE.IMPLEMENT || workType === WORK_TYPE.IMPLEMENT_LARGE) {
629
629
  return 'implement';
630
630
  }
631
- if (workType === WORK_TYPE.REVIEW && !item?._pr && !item?.pr_id) {
631
+ const hasPrContext = !!(item?._pr || item?.pr_id || item?.targetPr || item?.sourcePr || item?.pr);
632
+ if (workType === WORK_TYPE.REVIEW && !hasPrContext) {
632
633
  return 'work-item';
633
634
  }
635
+ if (workType === WORK_TYPE.FIX && hasPrContext) {
636
+ return 'fix';
637
+ }
634
638
  const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose', 'docs', 'meeting-investigate', 'meeting-debate', 'meeting-conclude'];
635
639
  return typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
636
640
  }
package/engine.js CHANGED
@@ -2592,6 +2592,14 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
2592
2592
  worktree_path: path.resolve(root, config.engine?.worktreeRoot || '../worktrees', `${branchName}`),
2593
2593
  commit_message: item.commitMessage || `feat: ${item.title || item.id}`,
2594
2594
  notes_content: '',
2595
+ pr_id: item.pr_id || item._pr || item.targetPr || item.sourcePr || item.pr || '',
2596
+ pr_number: item.prNumber || item.pr_number || '',
2597
+ pr_title: item.pr_title || item.prTitle || '',
2598
+ pr_branch: item.pr_branch || item.prBranch || '',
2599
+ pr_author: item.pr_author || item.prAuthor || '',
2600
+ pr_url: item.pr_url || item.prUrl || '',
2601
+ reviewer: item.reviewer || 'Reviewer',
2602
+ review_note: item.review_note || item.reviewNote || item.description || item.title || 'See PR thread comments',
2595
2603
  };
2596
2604
  const cpResult = buildWorkItemDispatchVars(item, vars, config, {
2597
2605
  worktreePath: vars.worktree_path || root,
@@ -2612,6 +2620,38 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
2612
2620
  };
2613
2621
  }
2614
2622
 
2623
+ function getWorkItemPrRef(item) {
2624
+ if (!item || typeof item !== 'object') return null;
2625
+ return item.targetPr || item.pr || item.pr_id || item.sourcePr || item.pullRequest || item.prUrl || item.prNumber || null;
2626
+ }
2627
+
2628
+ function resolveWorkItemPrRecord(item, project) {
2629
+ if (!project) return null;
2630
+ const prRef = getWorkItemPrRef(item);
2631
+ if (!prRef) return null;
2632
+ const prs = safeJson(projectPrPath(project)) || [];
2633
+ shared.normalizePrRecords(prs, project);
2634
+ return shared.findPrRecord(prs, prRef, project);
2635
+ }
2636
+
2637
+ function withWorkItemPrContext(item, pr) {
2638
+ if (!pr) return item;
2639
+ const prNumber = shared.getPrNumber(pr);
2640
+ return {
2641
+ ...item,
2642
+ _pr: pr.id,
2643
+ pr_id: pr.id,
2644
+ targetPr: item.targetPr || pr.id,
2645
+ prNumber: prNumber ?? item.prNumber,
2646
+ pr_number: prNumber ?? item.pr_number,
2647
+ pr_title: pr.title || item.pr_title || item.prTitle || '',
2648
+ pr_branch: pr.branch || item.pr_branch || item.prBranch || '',
2649
+ pr_author: pr.agent || item.pr_author || item.prAuthor || '',
2650
+ pr_url: pr.url || item.pr_url || item.prUrl || '',
2651
+ reviewer: item.reviewer || 'Reviewer',
2652
+ review_note: item.review_note || item.reviewNote || item.description || item.title || 'See PR thread comments',
2653
+ };
2654
+ }
2615
2655
  function projectFromDispatchMeta(metaProject, config) {
2616
2656
  if (!metaProject) return null;
2617
2657
  const projects = getProjects(config);
@@ -2775,8 +2815,17 @@ function discoverFromWorkItems(config, project) {
2775
2815
  skipped.noAgent++; continue;
2776
2816
  }
2777
2817
 
2818
+ const linkedPr = resolveWorkItemPrRecord(item, project);
2819
+ const promptItem = linkedPr ? withWorkItemPrContext(item, linkedPr) : item;
2820
+ const prBranch = linkedPr?.branch || '';
2821
+ const isPrTargeted = !!(linkedPr && (workType === WORK_TYPE.FIX || workType === WORK_TYPE.REVIEW || workType === WORK_TYPE.TEST));
2822
+ if (!linkedPr && getWorkItemPrRef(item) && (workType === WORK_TYPE.FIX || workType === WORK_TYPE.REVIEW || workType === WORK_TYPE.TEST)) {
2823
+ if (item._pendingReason !== 'pr_not_found') { item._pendingReason = 'pr_not_found'; needsWrite = true; }
2824
+ log('warn', `Work item ${item.id} references PR ${getWorkItemPrRef(item)} but no tracked PR record was found`);
2825
+ continue;
2826
+ }
2778
2827
  const isShared = item.branchStrategy === 'shared-branch' && item.featureBranch;
2779
- const branchName = isShared ? item.featureBranch : (item.branch || `work/${item.id}`);
2828
+ const branchName = isPrTargeted && prBranch ? prBranch : (isShared ? item.featureBranch : (item.branch || `work/${item.id}`));
2780
2829
  const deferredAgentResolution = agentId === routing.ANY_AGENT;
2781
2830
 
2782
2831
  // Branch mutex: skip if target branch is locked by an active dispatch
@@ -2789,7 +2838,7 @@ function discoverFromWorkItems(config, project) {
2789
2838
  }
2790
2839
 
2791
2840
  const promptAgentId = deferredAgentResolution ? reservedAgentId : agentId;
2792
- const promptResult = renderProjectWorkItemPromptForAgent(item, workType, promptAgentId, config, project, root, branchName);
2841
+ const promptResult = renderProjectWorkItemPromptForAgent(promptItem, workType, promptAgentId, config, project, root, branchName);
2793
2842
  if (promptResult.needsReview) {
2794
2843
  log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as failed for manual intervention`);
2795
2844
  item.status = WI_STATUS.FAILED;
@@ -2827,7 +2876,7 @@ function discoverFromWorkItems(config, project) {
2827
2876
  agentRole: config.agents[agentId]?.role || tempAgents.get(agentId)?.role || 'Agent',
2828
2877
  task: `[${project?.name || 'project'}] ${item.title || item.description?.slice(0, 80) || item.id}`,
2829
2878
  prompt,
2830
- meta: { dispatchKey: key, source: 'work-item', branch: branchName, branchStrategy: item.branchStrategy || 'parallel', useExistingBranch: !!(item.branchStrategy === 'shared-branch' && item.featureBranch), item, project: { name: project?.name, localPath: project?.localPath }, deferAgentResolution: deferredAgentResolution }
2879
+ meta: { dispatchKey: key, source: 'work-item', branch: branchName, branchStrategy: item.branchStrategy || 'parallel', useExistingBranch: !!(isPrTargeted || (item.branchStrategy === 'shared-branch' && item.featureBranch)), item: promptItem, project: { name: project?.name, localPath: project?.localPath }, deferAgentResolution: deferredAgentResolution, ...(linkedPr ? { pr: linkedPr } : {}) }
2831
2880
  });
2832
2881
 
2833
2882
  setCooldown(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1677",
3
+ "version": "0.1.1679",
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"
@@ -80,8 +80,8 @@ I'll dispatch dallas to fix that bug.
80
80
  - `knowledge`: `title`, `content`, and `category` REQUIRED. Valid categories: architecture, conventions, project-notes, build-reports, reviews.
81
81
 
82
82
  Core action types:
83
- - **dispatch**: title (REQUIRED), workType, priority (low/medium/high), agents[] or agent (optional — both shapes accepted), project (REQUIRED when multi-project), description
84
- workTypes: `explore` (research/report only, NO PR), `ask` (answer/report, NO PR), `implement` (new code, PR REQUIRED), `fix` (bug fix, PR REQUIRED), `review` (code review, NO PR), `test` (tests, PR if new), `verify` (merge/build/maintenance, NO PR)
83
+ - **dispatch**: title (REQUIRED), workType, priority (low/medium/high), agents[] or agent (optional — both shapes accepted), project (REQUIRED when multi-project unless `pr` resolves to a tracked PR), description, pr (optional PR number/id/url for work that targets an existing PR)
84
+ workTypes: `explore` (research/report only, NO PR), `ask` (answer/report, NO PR), `implement` (new code, PR REQUIRED), `fix` (standalone bug fix creates a PR; include `pr` when fixing review comments/build failures on an existing PR), `review` (code review, NO PR), `test` (tests, PR if new), `verify` (merge/build/maintenance, NO PR)
85
85
  If the user wants a design/architecture artifact committed through a PR, dispatch `implement` or `docs` rather than `explore`.
86
86
  When the user names a specific agent ("assign this to lambert"), put exactly that one name in `agents` (e.g. `"agents": ["lambert"]`). A single-agent assignment is hard-pinned by the server — it will queue for that agent only and skip the routing table. Use multi-agent arrays only when the user names multiple agents or asks for fan-out.
87
87
  - **build-and-test**: pr, project (optional), agent (optional) — Run the build-and-test playbook against a PR. The agent will checkout the PR branch, run the project's build/test commands, and report results. Use when the user asks to "run tests on PR X" or "build PR X" or after a fix to verify nothing regressed.