@yemi33/minions 0.1.2157 → 0.1.2159

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.
@@ -118,7 +118,7 @@ function renderDetailContent(detail, tab) {
118
118
  el.innerHTML =
119
119
  '<div style="display:flex;gap:6px;margin-bottom:8px">' +
120
120
  '<button class="pr-pager-btn" id="charter-edit-btn" style="font-size:var(--text-sm);padding:2px 10px" onclick="_toggleCharterEdit()">Edit</button>' +
121
- '<button class="pr-pager-btn" id="charter-save-btn" style="font-size:var(--text-sm);padding:2px 10px;color:var(--green);border-color:var(--green);display:none" onclick="_saveCharter()">Save</button>' +
121
+ '<button class="modal-copy is-success" id="charter-save-btn" style="display:none" onclick="_saveCharter()">Save</button>' +
122
122
  '<button class="pr-pager-btn" id="charter-cancel-btn" style="font-size:var(--text-sm);padding:2px 10px;display:none" onclick="_cancelCharterEdit()">Cancel</button>' +
123
123
  '</div>' +
124
124
  '<div id="charter-view" class="section">' + renderMd(charterContent || 'No charter found. Click Edit to create one.') + '</div>' +
@@ -120,17 +120,10 @@ function renderFre(statusOrProjects) {
120
120
  'flex-shrink:0',
121
121
  ].join(';');
122
122
 
123
- const btnPrimary = [
124
- 'padding:6px 14px',
125
- 'background:var(--blue)',
126
- 'color:#fff',
127
- 'border:none',
128
- 'border-radius:var(--radius-sm)',
129
- 'cursor:pointer',
130
- 'font-size:var(--text-md)',
131
- 'font-weight:600',
132
- 'margin-top:8px',
133
- ].join(';');
123
+ // W-mq57qetc0009ed48: blue-primary CTA inline blob removed; "+ Add Project"
124
+ // now uses the shared .btn-primary-solid primitive. Only the layout-only
125
+ // margin-top:8px stays inline.
126
+ const btnPrimaryMarginTop = 'margin-top:8px';
134
127
 
135
128
  const btnGhost = [
136
129
  'padding:6px 14px',
@@ -187,7 +180,7 @@ function renderFre(statusOrProjects) {
187
180
  '<div style="color:var(--muted);font-size:var(--text-md);line-height:1.5">' +
188
181
  'Projects map a local git worktree to a remote repo. Without a project, agents have nowhere to run.' +
189
182
  '</div>' +
190
- '<button onclick="addProject()" style="' + btnPrimary + '">+ Add Project</button>' +
183
+ '<button onclick="addProject()" class="btn-primary-solid" style="' + btnPrimaryMarginTop + '">+ Add Project</button>' +
191
184
  '</div>' +
192
185
  '</div>' +
193
186
 
@@ -229,8 +229,8 @@ async function loadQaRunbooks() {
229
229
  '</div>' +
230
230
  '</div>' +
231
231
  '<div class="qa-runbook-actions" style="display:flex;gap:var(--space-2)">' +
232
- '<button class="qa-btn-primary qa-runbook-run-btn" data-runbook-id="' + escHtml(id) + '" onclick="qaRunRunbook(this.dataset.runbookId)">Run</button>' +
233
- '<button class="qa-btn-ghost qa-btn-danger qa-runbook-delete-btn" data-runbook-id="' + escHtml(id) + '" onclick="qaDeleteRunbook(this.dataset.runbookId)">Delete</button>' +
232
+ '<button class="btn-primary-solid qa-runbook-run-btn" data-runbook-id="' + escHtml(id) + '" onclick="qaRunRunbook(this.dataset.runbookId)">Run</button>' +
233
+ '<button class="btn-ghost qa-btn-danger qa-runbook-delete-btn" data-runbook-id="' + escHtml(id) + '" onclick="qaDeleteRunbook(this.dataset.runbookId)">Delete</button>' +
234
234
  '</div>' +
235
235
  '</div>';
236
236
  }).join('');
@@ -350,7 +350,7 @@ function _qaRenderRunRow(run) {
350
350
  // to click.
351
351
  let deleteBtn = '';
352
352
  if (_qaIsTerminalStatus(status)) {
353
- deleteBtn = '<button class="qa-btn-ghost qa-run-delete-btn" ' +
353
+ deleteBtn = '<button class="btn-ghost qa-run-delete-btn" ' +
354
354
  'data-run-id="' + escHtml(id) + '" ' +
355
355
  'onclick="qaDeleteRun(this.dataset.runId)">Delete</button>';
356
356
  }
@@ -731,14 +731,14 @@ function _qaRenderSessionCard(s) {
731
731
  // - terminal → no actions
732
732
  let actions = '';
733
733
  if (state === 'awaiting-approval') {
734
- actions += '<button class="qa-btn-ghost" onclick="qaSessionReviewDraft(\'' + escHtml(id) + '\')">Review draft</button>';
735
- actions += '<button class="qa-btn-primary" onclick="qaSessionApprove(\'' + escHtml(id) + '\')">Approve &amp; run</button>';
736
- actions += '<button class="qa-btn-ghost" onclick="qaSessionEditPrompt(\'' + escHtml(id) + '\')">Edit</button>';
737
- actions += '<button class="qa-btn-ghost" onclick="qaSessionCancel(\'' + escHtml(id) + '\')">Cancel</button>';
734
+ actions += '<button class="btn-ghost" onclick="qaSessionReviewDraft(\'' + escHtml(id) + '\')">Review draft</button>';
735
+ actions += '<button class="btn-primary-solid" onclick="qaSessionApprove(\'' + escHtml(id) + '\')">Approve &amp; run</button>';
736
+ actions += '<button class="btn-ghost" onclick="qaSessionEditPrompt(\'' + escHtml(id) + '\')">Edit</button>';
737
+ actions += '<button class="btn-ghost" onclick="qaSessionCancel(\'' + escHtml(id) + '\')">Cancel</button>';
738
738
  }
739
739
  if (!isTerminal) {
740
- actions += '<button class="qa-btn-ghost" onclick="qaSessionDismiss(\'' + escHtml(id) + '\')">Dismiss</button>';
741
- actions += '<button class="qa-btn-ghost qa-btn-danger" onclick="qaSessionKill(\'' + escHtml(id) + '\')">Kill spawn</button>';
740
+ actions += '<button class="btn-ghost" onclick="qaSessionDismiss(\'' + escHtml(id) + '\')">Dismiss</button>';
741
+ actions += '<button class="btn-ghost qa-btn-danger" onclick="qaSessionKill(\'' + escHtml(id) + '\')">Kill spawn</button>';
742
742
  }
743
743
 
744
744
  let meta = '';
@@ -976,7 +976,7 @@ function _qaBuildModalShell(sessionId) {
976
976
  header.appendChild(title);
977
977
  const closeBtn = document.createElement('button');
978
978
  closeBtn.type = 'button';
979
- closeBtn.className = 'qa-btn-ghost qa-modal-close';
979
+ closeBtn.className = 'btn-ghost qa-modal-close';
980
980
  closeBtn.setAttribute('aria-label', 'Close');
981
981
  closeBtn.textContent = '×';
982
982
  closeBtn.addEventListener('click', _qaModalClose);
@@ -1080,7 +1080,7 @@ function _qaModalRenderBody(payload) {
1080
1080
  if (!payload.truncated) {
1081
1081
  const approve = document.createElement('button');
1082
1082
  approve.type = 'button';
1083
- approve.className = 'qa-btn-primary';
1083
+ approve.className = 'btn-primary-solid';
1084
1084
  approve.textContent = 'Approve & run';
1085
1085
  approve.addEventListener('click', () => {
1086
1086
  _qaModalClose();
@@ -1090,7 +1090,7 @@ function _qaModalRenderBody(payload) {
1090
1090
  }
1091
1091
  const editBtn = document.createElement('button');
1092
1092
  editBtn.type = 'button';
1093
- editBtn.className = 'qa-btn-ghost';
1093
+ editBtn.className = 'btn-ghost';
1094
1094
  editBtn.textContent = 'Request edit';
1095
1095
  editBtn.addEventListener('click', () => {
1096
1096
  _qaModalClose();
@@ -1099,7 +1099,7 @@ function _qaModalRenderBody(payload) {
1099
1099
  footer.appendChild(editBtn);
1100
1100
  const closeBtn = document.createElement('button');
1101
1101
  closeBtn.type = 'button';
1102
- closeBtn.className = 'qa-btn-ghost';
1102
+ closeBtn.className = 'btn-ghost';
1103
1103
  closeBtn.textContent = 'Close';
1104
1104
  closeBtn.addEventListener('click', _qaModalClose);
1105
1105
  footer.appendChild(closeBtn);
@@ -143,7 +143,7 @@ async function openAgentDetail(id) {
143
143
  document.getElementById('detail-content').innerHTML =
144
144
  '<div style="padding:24px;text-align:center">' +
145
145
  '<div style="color:var(--red);margin-bottom:12px">Error loading agent detail: ' + escapeHtml(e.message) + '</div>' +
146
- '<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:var(--text-md)">Retry</button>' +
146
+ '<button data-agent-id="' + escapeHtml(id) + '" onclick="openAgentDetail(this.dataset.agentId)" class="btn-primary-lg">Retry</button>' +
147
147
  ' <button onclick="closeDetail()" style="padding:6px 16px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;font-size:var(--text-md);color:var(--text)">Close</button>' +
148
148
  '</div>';
149
149
  }
@@ -270,7 +270,7 @@ function openQuickNoteModal() {
270
270
  '<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>' +
271
271
  '<div style="display:flex;justify-content:flex-end;gap:8px">' +
272
272
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
273
- '<button onclick="submitQuickNote()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Save Note</button>' +
273
+ '<button onclick="submitQuickNote()" class="btn-primary-lg">Save Note</button>' +
274
274
  '</div>' +
275
275
  '</div>';
276
276
  document.getElementById('modal').classList.add('open');
@@ -222,7 +222,7 @@ function openCreateKbModal() {
222
222
  '<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>' +
223
223
  '<div style="display:flex;justify-content:flex-end;gap:8px">' +
224
224
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
225
- '<button onclick="submitKbEntry(event)" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Save</button>' +
225
+ '<button onclick="submitKbEntry(event)" class="btn-primary-lg">Save</button>' +
226
226
  '</div>' +
227
227
  '</div>';
228
228
  document.getElementById('modal').classList.add('open');
@@ -123,7 +123,7 @@ function renderMeetings(meetings, opts) {
123
123
  (m.status === 'archived'
124
124
  ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px" onclick="event.stopPropagation();_unarchiveMeeting(\'' + escHtml(m.id) + '\')">Unarchive</button>'
125
125
  : (m.status === 'completed' ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px" onclick="event.stopPropagation();_archiveMeeting(\'' + escHtml(m.id) + '\')">Archive</button>' : '')) +
126
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
126
+ '<button class="btn-destructive" onclick="event.stopPropagation();_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
127
127
  '</div>' +
128
128
  '</div>' +
129
129
  (timeStr ? '<div style="margin-top:4px;font-size:var(--text-sm);color:var(--muted)">' + escHtml(timeStr) + '</div>' : '') +
@@ -246,17 +246,17 @@ function _renderMeetingDetail(m) {
246
246
  if (m.status === 'archived') {
247
247
  html += '<div style="display:flex;gap:8px;border-top:1px solid var(--border);padding-top:8px">' +
248
248
  '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" onclick="_unarchiveMeeting(\'' + escHtml(m.id) + '\')">Unarchive</button>' +
249
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
249
+ '<button class="btn-destructive btn-destructive-md" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
250
250
  '</div>';
251
251
  } else if (m.status === 'completed') {
252
252
  const linkedPlan = _findLinkedPlan(m);
253
253
  html += '<div style="display:flex;gap:8px;flex-wrap:wrap;border-top:1px solid var(--border);padding-top:8px">' +
254
254
  (linkedPlan
255
255
  ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--blue);border-color:var(--blue)" onclick="_viewPlanWithBack(\'' + escHtml(linkedPlan.file) + '\',\'' + escHtml(m.id) + '\')">View Plan</button>' +
256
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--green);border-color:var(--green)" onclick="_createPlanFromMeeting(\'' + escHtml(m.id) + '\',this)">New Plan</button>'
257
- : '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--green);border-color:var(--green)" onclick="_createPlanFromMeeting(\'' + escHtml(m.id) + '\',this)">Create Plan from Meeting</button>') +
256
+ '<button class="btn-action btn-action-md" onclick="_createPlanFromMeeting(\'' + escHtml(m.id) + '\',this)">New Plan</button>'
257
+ : '<button class="btn-action btn-action-md" onclick="_createPlanFromMeeting(\'' + escHtml(m.id) + '\',this)">Create Plan from Meeting</button>') +
258
258
  '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" onclick="_archiveMeeting(\'' + escHtml(m.id) + '\')">Archive</button>' +
259
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
259
+ '<button class="btn-destructive btn-destructive-md" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
260
260
  '</div>' +
261
261
  '<div id="meeting-plan-toast" class="cmd-toast cmd-toast-inline" style="margin-top:6px"></div>' +
262
262
  '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:4px">Use the Q&amp;A below to discuss action items' + (linkedPlan ? '' : ', then create a plan to execute them') + '.</div>';
@@ -267,8 +267,8 @@ function _renderMeetingDetail(m) {
267
267
  '</div>' +
268
268
  '<div style="display:flex;gap:8px;margin-top:4px">' +
269
269
  '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--yellow);border-color:var(--yellow)" onclick="_advanceMeeting(\'' + escHtml(m.id) + '\',this)">Skip to Next Round</button>' +
270
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_endMeeting(\'' + escHtml(m.id) + '\',this)">End Meeting</button>' +
271
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
270
+ '<button class="btn-destructive btn-destructive-md" onclick="_endMeeting(\'' + escHtml(m.id) + '\',this)">End Meeting</button>' +
271
+ '<button class="btn-destructive btn-destructive-md" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
272
272
  '</div>';
273
273
  }
274
274
 
@@ -347,7 +347,7 @@ function openCreateMeetingModal() {
347
347
  '<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>' +
348
348
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
349
349
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
350
- '<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>' +
350
+ '<button onclick="_submitCreateMeeting(event)" class="btn-primary-lg">Start Meeting</button>' +
351
351
  '</div>' +
352
352
  '</div>';
353
353
  document.getElementById('modal').classList.add('open');
@@ -5,8 +5,8 @@ function renderProjects(projects) {
5
5
  const visible = (projects || []).filter(p => typeof isDeleted === 'function' ? !isDeleted('project:' + p.name) : true);
6
6
  if (!visible.length) {
7
7
  list.innerHTML = '<span style="color:var(--muted);font-style:italic">No projects linked.</span>' +
8
- '<span onclick="addProject()" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--green);font-weight:500;cursor:pointer;border-style:dashed;margin-left:8px">+ Add Project</span>' +
9
- '<span onclick="openScanProjectsModal()" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--blue);font-weight:500;cursor:pointer;border-style:dashed;font-size:var(--text-sm)">Scan</span>';
8
+ '<span onclick="addProject()" class="empty-state-add" style="margin-left:8px">+ Add Project</span>' +
9
+ '<span onclick="openScanProjectsModal()" class="empty-state-add empty-state-add-blue">Scan</span>';
10
10
  return;
11
11
  }
12
12
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: project name, path, branch metadata)
@@ -17,8 +17,8 @@ function renderProjects(projects) {
17
17
  '<span onclick="event.stopPropagation();projectChipRemove(\'' + escHtml(p.name) + '\')" title="Remove project (cancels pending work, archives data dir)" style="color:var(--muted);font-weight:600;cursor:pointer;padding:0 2px;line-height:1" onmouseover="this.style.color=\'var(--red)\'" onmouseout="this.style.color=\'var(--muted)\'">&times;</span>' +
18
18
  '</span>'
19
19
  ).join('') +
20
- '<span onclick="addProject()" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--muted);font-weight:500;cursor:pointer;border-style:dashed">+ Add</span>' +
21
- '<span onclick="openScanProjectsModal()" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--blue);font-weight:500;cursor:pointer;border-style:dashed;font-size:var(--text-sm)">Scan</span>';
20
+ '<span onclick="addProject()" class="empty-state-add empty-state-add-muted">+ Add</span>' +
21
+ '<span onclick="openScanProjectsModal()" class="empty-state-add empty-state-add-blue">Scan</span>';
22
22
 
23
23
  }
24
24
 
@@ -422,7 +422,7 @@ async function openScanProjectsModal() {
422
422
  '<label style="width:60px;color:var(--text);font-size:var(--text-md)">Depth' +
423
423
  '<input id="scan-depth" type="number" value="3" min="1" max="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);font-size:var(--text-md)">' +
424
424
  '</label>' +
425
- '<button onclick="_runProjectScan()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap">Scan</button>' +
425
+ '<button onclick="_runProjectScan()" class="btn-primary-lg" style="white-space:nowrap">Scan</button>' +
426
426
  '</div>' +
427
427
  '<div id="scan-results" style="color:var(--muted);font-size:var(--text-md)">Click Scan to find git repos in the directory.</div>' +
428
428
  '<div class="cmd-toast" id="scan-toast" style="margin-top:0"></div>' +
@@ -17,7 +17,7 @@ function renderPinned(entries) {
17
17
  ';border-radius:4px;cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openPinnedView(' + i + ')">' +
18
18
  '<div style="display:flex;justify-content:space-between;align-items:center">' +
19
19
  '<strong style="font-size:var(--text-md)">' + escHtml(e.title) + '</strong>' +
20
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--red);border-color:var(--red)" data-pin-title="' + escHtml(e.title) + '" onclick="event.stopPropagation();removePinnedNote(this.dataset.pinTitle)">Unpin</button>' +
20
+ '<button class="btn-destructive" data-pin-title="' + escHtml(e.title) + '" onclick="event.stopPropagation();removePinnedNote(this.dataset.pinTitle)">Unpin</button>' +
21
21
  '</div>' +
22
22
  '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:4px">' + renderMd(e.content.slice(0, 200)) + (e.content.length > 200 ? '...' : '') + '</div>' +
23
23
  '</div>'
@@ -44,7 +44,7 @@ function openPinNoteModal() {
44
44
  '<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>' +
45
45
  '<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>' +
46
46
  '<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>' +
47
- '<div style="display:flex;justify-content:flex-end;gap:8px"><button onclick="closeModal()" class="pr-pager-btn">Cancel</button><button onclick="submitPinnedNote(event)" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Pin</button></div>' +
47
+ '<div style="display:flex;justify-content:flex-end;gap:8px"><button onclick="closeModal()" class="pr-pager-btn">Cancel</button><button onclick="submitPinnedNote(event)" class="btn-primary-lg">Pin</button></div>' +
48
48
  '</div>';
49
49
  document.getElementById('modal').classList.add('open');
50
50
  }
@@ -194,7 +194,7 @@ function openPinnedEdit(idx) {
194
194
  cancelBtn.textContent = 'Cancel';
195
195
  cancelBtn.onclick = function() { openPinnedView(idx); };
196
196
  const saveBtn = document.createElement('button');
197
- saveBtn.style.cssText = 'padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer';
197
+ saveBtn.className = 'btn-primary-lg';
198
198
  saveBtn.textContent = 'Save';
199
199
  saveBtn.onclick = function(ev) { submitPinnedEdit(originalTitle, ev); };
200
200
  actions.appendChild(cancelBtn);
@@ -399,12 +399,12 @@ function openPipelineDetail(id) {
399
399
  '<span style="font-size:var(--text-sm);color:var(--muted)">' + _renderPipelineTriggerLabel(p.trigger?.cron) + ' · ' + escHtml(_getPipelineStageLabel(p)) + '</span>' +
400
400
  '<div style="display:flex;gap:6px">' +
401
401
  (activeRun
402
- ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_abortPipeline(\'' + escHtml(id) + '\',this)">Abort</button>' +
402
+ ? '<button class="btn-destructive btn-destructive-md" onclick="_abortPipeline(\'' + escHtml(id) + '\',this)">Abort</button>' +
403
403
  '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--yellow);border-color:var(--yellow)" onclick="_retriggerPipeline(\'' + escHtml(id) + '\',this)">Retrigger</button>'
404
- : '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--green);border-color:var(--green)" onclick="_triggerPipeline(\'' + escHtml(id) + '\',this)">Run Now</button>') +
404
+ : '<button class="btn-action btn-action-md" onclick="_triggerPipeline(\'' + escHtml(id) + '\',this)">Run Now</button>') +
405
405
  '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--blue);border-color:var(--blue)" onclick="openEditPipelineModal(\'' + escHtml(id) + '\')">Edit</button>' +
406
406
  '<button class="pr-pager-btn' + (_pipelineToggleInFlight.has(id) ? ' disabled' : '') + '" style="font-size:var(--text-xs);padding:2px 8px" onclick="_togglePipelineEnabled(\'' + escHtml(id) + '\',' + !p.enabled + ',this)">' + (p.enabled !== false ? 'Disable' : 'Enable') + '</button>' +
407
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_deletePipelineConfirm(\'' + escHtml(id) + '\')">Delete</button>' +
407
+ '<button class="btn-destructive btn-destructive-md" onclick="_deletePipelineConfirm(\'' + escHtml(id) + '\')">Delete</button>' +
408
408
  '</div>' +
409
409
  '</div>';
410
410
 
@@ -454,7 +454,7 @@ function openPipelineDetail(id) {
454
454
  _renderMonitoredResources(s.monitoredResources || []) +
455
455
  _renderArtifactLinks(stageRun.artifacts, id) +
456
456
  (stageRun.output ? '<div class="pipeline-stage-output" style="margin-top:6px;font-size:var(--text-base);max-height:150px;overflow-y:auto">' + renderMd(stageRun.output.slice(0, 500)) + '</div>' : '') +
457
- (stageStatus === 'waiting-human' ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--green);border-color:var(--green);margin-top:6px" onclick="_continuePipeline(\'' + escHtml(id) + '\',\'' + escHtml(s.id) + '\',this)">Continue</button>' : '') +
457
+ (stageStatus === 'waiting-human' ? '<button class="btn-action btn-action-md" style="margin-top:6px" onclick="_continuePipeline(\'' + escHtml(id) + '\',\'' + escHtml(s.id) + '\',this)">Continue</button>' : '') +
458
458
  '</div>';
459
459
  });
460
460
 
@@ -726,7 +726,7 @@ function openCreatePipelineModal() {
726
726
  '<label style="color:var(--text);font-size:var(--text-md)">Stages (JSON array)<textarea id="pl-stages" rows="10" style="' + inputStyle + ';resize:vertical;font-family:Consolas,monospace" placeholder=\'[{"id":"audit","type":"task","title":"Audit codebase","taskType":"explore"},{"id":"discuss","type":"meeting","title":"Discuss findings","dependsOn":["audit"],"participants":["all"]}]\'></textarea></label>' +
727
727
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
728
728
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
729
- '<button onclick="_submitCreatePipeline()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Create Pipeline</button>' +
729
+ '<button onclick="_submitCreatePipeline()" class="btn-primary-lg">Create Pipeline</button>' +
730
730
  '</div>' +
731
731
  '</div>';
732
732
  document.getElementById('modal').classList.add('open');
@@ -828,7 +828,7 @@ function openEditPipelineModal(id) {
828
828
  '<label style="color:var(--text);font-size:var(--text-md)">Stages (JSON array)<textarea id="pl-stages" rows="10" style="' + inputStyle + ';resize:vertical;font-family:Consolas,monospace">' + escHtml(JSON.stringify(p.stages || [], null, 2)) + '</textarea></label>' +
829
829
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
830
830
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
831
- '<button onclick="_submitEditPipeline()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Save</button>' +
831
+ '<button onclick="_submitEditPipeline()" class="btn-primary-lg">Save</button>' +
832
832
  '</div>' +
833
833
  '</div>';
834
834
  document.getElementById('modal-body').style.whiteSpace = 'normal';
@@ -22,7 +22,7 @@ function openCreatePlanModal() {
22
22
  '<div style="font-size:var(--text-base);color:var(--muted)">After creating, click Execute on the plan card to have an agent convert it into a PRD with work items.</div>' +
23
23
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
24
24
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
25
- '<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
+ '<button onclick="_submitCreatePlan(event)" class="btn-primary-lg">Create Plan</button>' +
26
26
  '</div>' +
27
27
  '</div>';
28
28
  document.getElementById('modal').classList.add('open');
@@ -468,7 +468,7 @@ function planReexecuteModal(file, project) {
468
468
  '<div style="font-size:var(--text-base);color:var(--muted)">Leave blank to re-run without steering. The agent will read the existing PRD and rewrite it.</div>' +
469
469
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
470
470
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
471
- '<button onclick="planReexecuteSubmit(\'' + escapeHtml(file) + '\',\'' + escapeHtml(project || '') + '\')" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Re-execute</button>' +
471
+ '<button onclick="planReexecuteSubmit(\'' + escapeHtml(file) + '\',\'' + escapeHtml(project || '') + '\')" class="btn-primary-lg">Re-execute</button>' +
472
472
  '</div>' +
473
473
  '</div>';
474
474
  document.getElementById('modal').classList.add('open');
@@ -105,15 +105,15 @@ function renderPrd(prd, prog) {
105
105
  let actions = '';
106
106
  if (prdFile && !headerStale) {
107
107
  if (effectiveStatus === 'awaiting-approval') {
108
- actions = ' <button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:4px" onclick="planApprove(\'' + escHtml(prdFile) + '\',this)">Approve</button>';
108
+ actions = ' <button class="btn-action" style="margin-left:4px" onclick="planApprove(\'' + escHtml(prdFile) + '\',this)">Approve</button>';
109
109
  } else if (effectiveStatus === 'completed') {
110
110
  const hasVerifyWi = allWi.some(w => w.itemType === 'verify' && w.sourcePlan === prdFile);
111
- actions = (hasVerifyWi ? '' : ' <button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:4px" onclick="triggerVerify(\'' + escHtml(prdFile) + '\',this)">Verify</button>') +
111
+ actions = (hasVerifyWi ? '' : ' <button class="btn-action" style="margin-left:4px" onclick="triggerVerify(\'' + escHtml(prdFile) + '\',this)">Verify</button>') +
112
112
  ' <button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;margin-left:4px" onclick="planArchive(\'' + escHtml(prdFile) + '\',this)">Archive</button>';
113
113
  } else if (effectiveStatus === 'dispatched') {
114
114
  actions = ' <button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="planPause(\'' + escHtml(prdFile) + '\',this)">Pause</button>';
115
115
  } else if (effectiveStatus === 'paused') {
116
- actions = ' <button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:4px" onclick="planApprove(\'' + escHtml(prdFile) + '\',this)">Resume</button>' +
116
+ actions = ' <button class="btn-action" style="margin-left:4px" onclick="planApprove(\'' + escHtml(prdFile) + '\',this)">Resume</button>' +
117
117
  ' <button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;margin-left:4px" onclick="planArchive(\'' + escHtml(prdFile) + '\',this)">Archive</button>';
118
118
  }
119
119
  }
@@ -823,7 +823,7 @@ async function prdItemEdit(source, itemId) {
823
823
  '<label style="font-size:var(--text-base);color:var(--muted)">Description</label>' +
824
824
  '<div style="display:flex;gap:6px">' +
825
825
  '<button id="prd-desc-edit-btn" type="button" class="pr-pager-btn" aria-label="Edit description" style="font-size:var(--text-sm);padding:2px 10px" onclick="_togglePrdDescEdit()">Edit</button>' +
826
- '<button id="prd-desc-save-btn" type="button" class="pr-pager-btn" aria-label="Save description" style="font-size:var(--text-sm);padding:2px 10px;color:var(--green);border-color:var(--green);display:none" onclick="_savePrdDesc(\'' + escHtml(source) + '\',\'' + escHtml(itemId) + '\')">Save</button>' +
826
+ '<button id="prd-desc-save-btn" type="button" class="btn-action btn-action-lg" aria-label="Save description" style="display:none" onclick="_savePrdDesc(\'' + escHtml(source) + '\',\'' + escHtml(itemId) + '\')">Save</button>' +
827
827
  '<button id="prd-desc-cancel-btn" type="button" class="pr-pager-btn" aria-label="Cancel description edit" style="font-size:var(--text-sm);padding:2px 10px;display:none" onclick="_cancelPrdDescEdit()">Cancel</button>' +
828
828
  '</div>' +
829
829
  '</div>' +
@@ -204,7 +204,7 @@ function prRow(pr) {
204
204
  '<td><span class="pr-badge ' + statusClass + '" title="' + escapeHtml(statusLabel) + '">' + escapeHtml(statusLabel) + '</span></td>' +
205
205
  '<td>' + (observeBtn || '<span style="color:var(--muted);font-size:var(--text-base)">—</span>') + '</td>' +
206
206
  '<td><span class="pr-date" title="' + escapeHtml(createdLabel) + '">' + escapeHtml(createdLabel) + '</span></td>' +
207
- '<td><button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 5px;color:var(--red);border-color:var(--red)" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();unlinkPr(this.dataset.prId)" title="Remove from tracking">x</button></td>' +
207
+ '<td><button class="btn-destructive" style="padding:1px 5px" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();unlinkPr(this.dataset.prId)" title="Remove from tracking">x</button></td>' +
208
208
  '</tr>';
209
209
  }
210
210
 
@@ -350,7 +350,7 @@ function openAddPrModal() {
350
350
  '<div style="font-size:var(--text-base);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>' +
351
351
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
352
352
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
353
- '<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>' +
353
+ '<button onclick="_submitLinkPr(event)" class="btn-primary-lg">Link PR</button>' +
354
354
  '</div>' +
355
355
  '</div>';
356
356
  document.getElementById('modal').classList.add('open');
@@ -352,10 +352,10 @@ function renderSchedules(schedules, opts) {
352
352
  '<td>' + enabledBadge + '</td>' +
353
353
  '<td><span class="pr-date">' + escHtml(lastRun) + '</span></td>' +
354
354
  '<td style="white-space:nowrap">' +
355
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();runScheduleNow(\'' + escHtml(s.id) + '\',this)" title="Run now">Run now</button>' +
355
+ '<button class="btn-action" style="margin-right:4px" onclick="event.stopPropagation();runScheduleNow(\'' + escHtml(s.id) + '\',this)" title="Run now">Run now</button>' +
356
356
  '<button class="pr-pager-btn' + (_schedToggleInFlight.has(s.id) ? ' disabled' : '') + '" style="font-size:var(--text-xs);padding:1px 6px;color:' + (s.enabled ? 'var(--yellow)' : 'var(--green)') + ';border-color:' + (s.enabled ? 'var(--yellow)' : 'var(--green)') + ';margin-right:4px" onclick="event.stopPropagation();toggleScheduleEnabled(\'' + escHtml(s.id) + '\',' + !s.enabled + ')" title="' + (s.enabled ? 'Disable' : 'Enable') + '">' + (s.enabled ? '&#x23F8;' : '&#x25B6;') + '</button>' +
357
357
  '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();openEditScheduleModal(\'' + escHtml(s.id) + '\')" title="Edit">&#x270E;</button>' +
358
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteSchedule(\'' + escHtml(s.id) + '\')" title="Delete">&#x2715;</button>' +
358
+ '<button class="btn-destructive" onclick="event.stopPropagation();deleteSchedule(\'' + escHtml(s.id) + '\')" title="Delete">&#x2715;</button>' +
359
359
  '</td>' +
360
360
  '</tr>';
361
361
  }
@@ -515,7 +515,7 @@ function _scheduleFormHtml(sched, isEdit) {
515
515
  '</label>' +
516
516
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
517
517
  '<button onclick="closeModal()" class="pr-pager-btn" style="padding:6px 16px;font-size:var(--text-md)">Cancel</button>' +
518
- '<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>' +
518
+ '<button onclick="submitSchedule(' + isEdit + ',event)" class="btn-primary-lg">' + (isEdit ? 'Save' : 'Create') + '</button>' +
519
519
  '</div>' +
520
520
  '</div>';
521
521
  }
@@ -216,11 +216,11 @@ function renderWatches(watchesData, opts) {
216
216
  if (w.status === 'active') {
217
217
  html += '<button class="pr-pager-btn' + pauseBusyCls + '" style="font-size:var(--text-xs);padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-right:4px" onclick="event.stopPropagation();toggleWatchPause(\'' + escHtml(w.id) + '\',true)" title="Pause">&#x23F8;</button>';
218
218
  } else if (w.status === 'paused') {
219
- html += '<button class="pr-pager-btn' + pauseBusyCls + '" style="font-size:var(--text-xs);padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();toggleWatchPause(\'' + escHtml(w.id) + '\',false)" title="Resume">&#x25B6;</button>';
219
+ html += '<button class="btn-action' + pauseBusyCls + '" style="margin-right:4px" onclick="event.stopPropagation();toggleWatchPause(\'' + escHtml(w.id) + '\',false)" title="Resume">&#x25B6;</button>';
220
220
  }
221
221
 
222
222
  // Delete button
223
- html += '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteWatch(\'' + escHtml(w.id) + '\')" title="Delete">&#x2715;</button>';
223
+ html += '<button class="btn-destructive" onclick="event.stopPropagation();deleteWatch(\'' + escHtml(w.id) + '\')" title="Delete">&#x2715;</button>';
224
224
  html += '</td></tr>';
225
225
  }
226
226
 
@@ -548,7 +548,7 @@ function _addWatchStepRow(initial) {
548
548
  row.innerHTML =
549
549
  '<div style="display:flex;align-items:center;justify-content:space-between;gap:8px">' +
550
550
  '<select id="watch-step-' + idx + '-type" style="' + inputStyle + ';margin-top:0;flex:1" onchange="_updateWatchStepParamsHint(' + idx + ')">' + actionOpts + '</select>' +
551
- '<button type="button" class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_removeWatchStepRow(' + idx + ')">Remove</button>' +
551
+ '<button type="button" class="btn-destructive btn-destructive-lg" onclick="_removeWatchStepRow(' + idx + ')">Remove</button>' +
552
552
  '</div>' +
553
553
  '<label style="color:var(--text);font-size:var(--text-base)">Params (JSON) <span style="font-size:var(--text-sm);color:var(--muted)" id="watch-step-' + idx + '-params-hint"></span>' +
554
554
  '<textarea id="watch-step-' + idx + '-params" rows="3" placeholder=\'{"title": "Re-check {{target}} after merge"}\' style="' + inputStyle + ';font-family:monospace"></textarea>' +
@@ -612,7 +612,7 @@ function _addRequireRow(initial) {
612
612
  '<label style="color:var(--text);font-size:var(--text-base);margin:0">Target<input id="watch-require-' + idx + '-target" placeholder="e.g. PR-123, pipeline-X" style="' + inputStyle + '"></label>' +
613
613
  '<label style="color:var(--text);font-size:var(--text-base);margin:0">Target Type<select id="watch-require-' + idx + '-target-type" style="' + inputStyle + '" onchange="_updateRequireConditionOptions(' + idx + ')">' + ttOpts + '</select></label>' +
614
614
  '<label style="color:var(--text);font-size:var(--text-base);margin:0">Condition<select id="watch-require-' + idx + '-condition" style="' + inputStyle + '"></select></label>' +
615
- '<button type="button" class="pr-pager-btn" style="font-size:var(--text-sm);padding:6px 8px;color:var(--red);border-color:var(--red)" onclick="_removeRequireRow(' + idx + ')">Remove</button>';
615
+ '<button type="button" class="btn-destructive btn-destructive-lg" style="padding:6px 8px" onclick="_removeRequireRow(' + idx + ')">Remove</button>';
616
616
  container.appendChild(row);
617
617
  if (initTarget) document.getElementById('watch-require-' + idx + '-target').value = initTarget;
618
618
  if (initTT) document.getElementById('watch-require-' + idx + '-target-type').value = initTT;
@@ -655,7 +655,7 @@ function _updateWatchConditionOptions() {
655
655
 
656
656
  function _renderCreateWatchModal() {
657
657
  document.getElementById('modal-title').innerHTML = 'Create Watch' +
658
- ' <button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 12px;color:var(--green);border-color:var(--green);margin-left:8px" onclick="submitWatch()">Create</button>';
658
+ ' <button class="btn-action btn-action-lg" style="padding:2px 12px;margin-left:8px" onclick="submitWatch()">Create</button>';
659
659
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() by _watchFormHtml() (fields: target/action registries, project names, agent ids/names)
660
660
  document.getElementById('modal-body').innerHTML = _watchFormHtml();
661
661
  document.getElementById('modal-body').style.whiteSpace = 'normal';
@@ -246,9 +246,9 @@ function wiRow(item) {
246
246
  '<td style="white-space:nowrap">' +
247
247
  ((item.status === 'pending' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Edit work item">&#x270E;</button>' : '') +
248
248
  ((item.status === 'done' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Archive work item">&#x1F4E6;</button>' : '') +
249
- ((item.status === 'done' || item.status === 'failed') && !item._humanFeedback ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Give feedback">&#x1F44D;&#x1F44E;</button>' : (item._humanFeedback ? '<span style="font-size:var(--text-xs)" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '&#x1F44D;' : '&#x1F44E;') + '</span> ' : '')) +
249
+ ((item.status === 'done' || item.status === 'failed') && !item._humanFeedback ? '<button class="btn-action" style="margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Give feedback">&#x1F44D;&#x1F44E;</button>' : (item._humanFeedback ? '<span style="font-size:var(--text-xs)" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '&#x1F44D;' : '&#x1F44E;') + '</span> ' : '')) +
250
250
  ((item.status === 'pending' || item.status === 'dispatched' || item.status === 'queued' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--orange);border-color:var(--orange);margin-right:4px" onclick="event.stopPropagation();cancelWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Cancel work item and kill agent">&#x1F6AB;</button>' : '') +
251
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Delete work item and kill agent">&#x2715;</button>' +
251
+ '<button class="btn-destructive" onclick="event.stopPropagation();deleteWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Delete work item and kill agent">&#x2715;</button>' +
252
252
  '</td>' +
253
253
  '</tr>';
254
254
  }
@@ -380,7 +380,7 @@ async function editWorkItem(id, source) {
380
380
  '</label>' +
381
381
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
382
382
  '<button onclick="closeModal()" class="pr-pager-btn" style="padding:6px 16px;font-size:var(--text-md)">Cancel</button>' +
383
- '<button onclick="submitWorkItemEdit(\'' + escapeHtml(id) + '\',\'' + escapeHtml(source || '') + '\',event)" 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>' +
383
+ '<button onclick="submitWorkItemEdit(\'' + escapeHtml(id) + '\',\'' + escapeHtml(source || '') + '\',event)" class="btn-primary-lg">Save</button>' +
384
384
  '</div>' +
385
385
  '</div>';
386
386
  document.getElementById('modal').classList.add('open');
@@ -601,7 +601,7 @@ function openCreateWorkItemModal() {
601
601
  '<label id="wi-new-skippr-row" style="color:var(--text);font-size:var(--text-md);display:flex;gap:8px;align-items:center;cursor:pointer"><input type="checkbox" id="wi-new-skippr"> Skip PR creation (push branch only)</label>' +
602
602
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
603
603
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
604
- '<button onclick="_submitCreateWorkItem(event)" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Create</button>' +
604
+ '<button onclick="_submitCreateWorkItem(event)" class="btn-primary-lg">Create</button>' +
605
605
  '</div>' +
606
606
  '</div>';
607
607
  document.getElementById('modal').classList.add('open');
@@ -488,8 +488,7 @@ async function openSettings() {
488
488
  if (!document.getElementById('modal-settings-reset')) {
489
489
  const resetBtn = document.createElement('button');
490
490
  resetBtn.id = 'modal-settings-reset';
491
- resetBtn.className = 'modal-copy';
492
- resetBtn.style.cssText = 'color:var(--red);border-color:var(--red)';
491
+ resetBtn.className = 'modal-copy is-danger';
493
492
  resetBtn.textContent = 'Reset';
494
493
  resetBtn.onclick = resetSettingsToDefaults;
495
494
  actions.insertBefore(resetBtn, actions.lastElementChild);
@@ -497,8 +496,7 @@ async function openSettings() {
497
496
  if (!document.getElementById('modal-settings-save')) {
498
497
  const saveBtn = document.createElement('button');
499
498
  saveBtn.id = 'modal-settings-save';
500
- saveBtn.className = 'modal-copy';
501
- saveBtn.style.cssText = 'color:var(--green);border-color:var(--green)';
499
+ saveBtn.className = 'modal-copy is-success';
502
500
  saveBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg> Save';
503
501
  saveBtn.onclick = saveSettings;
504
502
  actions.insertBefore(saveBtn, actions.lastElementChild);
@@ -122,7 +122,7 @@
122
122
  <h3 id="modal-title">—</h3>
123
123
  <div class="modal-header-actions">
124
124
  <button class="modal-copy" id="modal-edit-btn" onclick="modalToggleEdit()" title="Edit" style="display:none"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61zM12.9 2.97L4.288 11.58l-.537 1.878 1.878-.537L14.242 4.31 12.9 2.97z"/></svg> Edit</button>
125
- <button class="modal-copy" id="modal-save-btn" onclick="modalSaveEdit()" title="Save" style="display:none;color:var(--green);border-color:var(--green)"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg> Save</button>
125
+ <button class="modal-copy is-success" id="modal-save-btn" onclick="modalSaveEdit()" title="Save" style="display:none"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg> Save</button>
126
126
  <button class="modal-copy" id="modal-cancel-edit-btn" onclick="modalCancelEdit()" title="Cancel edit" style="display:none">Cancel</button>
127
127
  <button class="modal-copy pin-btn" id="modal-pin-btn" onclick="toggleModalPin()" title="Pin to top" style="display:none">Pin</button>
128
128
  <button class="modal-copy" id="modal-copy-btn" onclick="copyModalContent()" title="Copy to clipboard"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25z"/></svg> Copy</button>
@@ -15,7 +15,7 @@
15
15
  </ul>
16
16
  </div>
17
17
  <div style="display:flex;gap:var(--space-3);align-items:center;margin-top:var(--space-2)">
18
- <button type="button" id="qa-cc-pitch-open" class="qa-btn-primary" onclick="qaFocusCommandCenter()">Open Command Center →</button>
18
+ <button type="button" id="qa-cc-pitch-open" class="btn-primary-solid" onclick="qaFocusCommandCenter()">Open Command Center →</button>
19
19
  <a href="/state/docs/qa-runbook-lifecycle.md" target="_blank" rel="noopener" class="qa-cc-pitch-docs" style="color:var(--blue);font-size:var(--text-base)">Docs →</a>
20
20
  </div>
21
21
  </div>
@@ -488,6 +488,170 @@
488
488
  .btn-add.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
489
489
  .btn-add.btn-add-lg { font-size: var(--text-sm); padding: 2px 8px; }
490
490
 
491
+ /* W-mq57qetc0009ed48 — green-bordered action button family WITHOUT a
492
+ leading "+" glyph (Approve / Verify / Resume / Save / Run Now /
493
+ Continue / Create / New Plan / Trigger Verify / etc). Sibling to
494
+ .btn-add — same green/surface2 visual identity, but semantically a
495
+ verb-action rather than a creator. Three padding+size variants cover
496
+ every callsite that was previously inline `style="font-size:…;
497
+ padding:…;color:var(--green);border-color:var(--green)"` on top of
498
+ .pr-pager-btn:
499
+ .btn-action — compact text-xs / 1px 6px (inline PRD
500
+ chips, work-item feedback)
501
+ .btn-action.btn-action-md — text-xs / 2px 8px (meetings, pipelines,
502
+ watches resume, schedule run-now chip)
503
+ .btn-action.btn-action-lg — text-sm / 2px 10px (modal Save, watch
504
+ Create, charter Save, schedule modal Run
505
+ now)
506
+ Positional/layout-only inline styles (e.g. margin-left:8px, margin-
507
+ right:4px, margin-top:6px) stay at the callsite. Do NOT re-create the
508
+ green inline blob — extend this primitive instead. */
509
+ .btn-action {
510
+ background: var(--surface2);
511
+ border: 1px solid var(--green);
512
+ color: var(--green);
513
+ font-size: var(--text-xs);
514
+ padding: 1px 6px;
515
+ border-radius: var(--radius-sm);
516
+ cursor: pointer;
517
+ transition: background var(--transition-base);
518
+ }
519
+ .btn-action:hover { background: rgba(63,185,80,0.1); }
520
+ .btn-action:focus-visible { outline: 2px solid var(--green); outline-offset: 1px; }
521
+ .btn-action:disabled,
522
+ .btn-action.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
523
+ .btn-action.btn-action-md { padding: 2px 8px; }
524
+ .btn-action.btn-action-lg { font-size: var(--text-sm); padding: 2px 10px; }
525
+
526
+ /* W-mq57qetc0009ed48 — red-bordered destructive button family (Delete /
527
+ Abort / Remove / Unpin / unlink-x). Visual sibling to .btn-action —
528
+ same surface2 background, same padding scale, same hover-tint pattern,
529
+ just swapped to var(--red). Three size variants:
530
+ .btn-destructive — compact text-xs / 1px 6px (work-item
531
+ delete, watch delete, schedule delete,
532
+ unpin, unlink-x in PR row)
533
+ .btn-destructive.btn-destructive-md — text-xs / 2px 8px (pipelines
534
+ Abort + Delete)
535
+ .btn-destructive.btn-destructive-lg — text-sm / 2px 10px (watch
536
+ builder Remove step / Remove
537
+ requirement)
538
+ Positional / outlier inline overrides (padding:1px 5px on the PR
539
+ unlink-x chip, padding:6px 8px on the requirement-row Remove) stay
540
+ at the callsite. Do NOT re-create the red inline blob — extend this
541
+ primitive instead. */
542
+ .btn-destructive {
543
+ background: var(--surface2);
544
+ border: 1px solid var(--red);
545
+ color: var(--red);
546
+ font-size: var(--text-xs);
547
+ padding: 1px 6px;
548
+ border-radius: var(--radius-sm);
549
+ cursor: pointer;
550
+ transition: background var(--transition-base);
551
+ }
552
+ .btn-destructive:hover { background: rgba(248,81,73,0.1); }
553
+ .btn-destructive:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
554
+ .btn-destructive:disabled,
555
+ .btn-destructive.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
556
+ .btn-destructive.btn-destructive-md { padding: 2px 8px; }
557
+ .btn-destructive.btn-destructive-lg { font-size: var(--text-sm); padding: 2px 10px; }
558
+
559
+ /* W-mq57qetc0009ed48 — solid-blue primary CTA. Single source of truth
560
+ for the filled-blue rectangle that gates submission flows (QA "Start
561
+ QA Session", "+ New runbook", "Save" in the QA runbook editor, FRE
562
+ "+ Add Project"). Replaces the standalone .qa-btn-primary (now
563
+ removed — all callsites migrated) and the ad-hoc
564
+ `background:var(--blue);color:#fff;border:none;…` style blob in
565
+ fre.js. */
566
+ .btn-primary-solid {
567
+ background: var(--blue);
568
+ color: #fff;
569
+ border: none;
570
+ border-radius: var(--radius-sm);
571
+ padding: var(--space-3) var(--space-7);
572
+ font-size: var(--text-md);
573
+ font-weight: 600;
574
+ cursor: pointer;
575
+ transition: opacity var(--transition-base);
576
+ }
577
+ .btn-primary-solid:hover { opacity: 0.9; }
578
+ .btn-primary-solid:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
579
+ .btn-primary-solid:disabled,
580
+ .btn-primary-solid.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
581
+
582
+ /* W-mq57qetc0009ed48 — solid-blue modal CTA. Lighter-weight sibling to
583
+ .btn-primary-solid: same filled-blue rectangle, padding:6px 16px, but
584
+ without the font-weight:600 used by qa/fre primary CTAs. Used for the
585
+ "Save" / "Create Plan" / "Re-execute" / "Save Note" buttons inside
586
+ dashboard modals (render-inbox, render-kb, render-plans) and the
587
+ "Retry" button in the agent-detail error state. Replaces the ad-hoc
588
+ `background:var(--blue);color:#fff;border:none;border-radius:sm;
589
+ padding:6px 16px` blob that was duplicated across five callsites. */
590
+ .btn-primary-lg {
591
+ background: var(--blue);
592
+ color: #fff;
593
+ border: none;
594
+ border-radius: var(--radius-sm);
595
+ padding: 6px 16px;
596
+ font-size: var(--text-md);
597
+ cursor: pointer;
598
+ transition: opacity var(--transition-base);
599
+ }
600
+ .btn-primary-lg:hover { opacity: 0.9; }
601
+ .btn-primary-lg:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
602
+ .btn-primary-lg:disabled,
603
+ .btn-primary-lg.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
604
+
605
+ /* W-mq57qetc0009ed48 — muted ghost button. Transparent background,
606
+ bordered, muted text. Used for secondary actions next to a primary
607
+ CTA (QA Cancel / Edit / Dismiss / "− remove artifact row" / "+ Add
608
+ artifact"). Replaces the standalone .qa-btn-ghost (now removed —
609
+ all callsites migrated). */
610
+ .btn-ghost {
611
+ background: transparent;
612
+ color: var(--muted);
613
+ border: 1px solid var(--border);
614
+ border-radius: var(--radius-sm);
615
+ padding: var(--space-2) var(--space-5);
616
+ font-size: var(--text-base);
617
+ cursor: pointer;
618
+ transition: color var(--transition-base), background var(--transition-base);
619
+ }
620
+ .btn-ghost:hover { color: var(--text); background: var(--surface); }
621
+ .btn-ghost:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
622
+ .btn-ghost:disabled,
623
+ .btn-ghost.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
624
+
625
+ /* W-mq57qetc0009ed48 — dashed empty-state chip ("+ Add Project" in
626
+ the projects empty-state). Same chip footprint as the project chips
627
+ in render-other.js, but with a dashed border + green text to read as
628
+ a placeholder/call-to-action. */
629
+ .empty-state-add {
630
+ display: inline-block;
631
+ background: var(--surface2);
632
+ border: 1px dashed var(--border);
633
+ color: var(--green);
634
+ font-weight: 500;
635
+ padding: 3px 10px;
636
+ border-radius: var(--radius-sm);
637
+ cursor: pointer;
638
+ transition: background var(--transition-base);
639
+ }
640
+ .empty-state-add:hover { background: rgba(63,185,80,0.1); }
641
+ .empty-state-add:focus-visible { outline: 2px solid var(--green); outline-offset: 1px; }
642
+ .empty-state-add:disabled,
643
+ .empty-state-add.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
644
+ /* Muted variant used when the chip sits next to existing project chips
645
+ (so it reads as a secondary "add another" affordance rather than the
646
+ primary empty-state CTA). */
647
+ .empty-state-add.empty-state-add-muted { color: var(--muted); }
648
+ .empty-state-add.empty-state-add-muted:hover { background: var(--surface); }
649
+ /* Blue variant used for the "Scan" affordance that sits next to "+ Add
650
+ Project" in the projects empty-state — same dashed chip, but text-sm and
651
+ blue text instead of green. */
652
+ .empty-state-add.empty-state-add-blue { color: var(--blue); font-size: var(--text-sm); }
653
+ .empty-state-add.empty-state-add-blue:hover { background: rgba(88,166,255,0.1); }
654
+
491
655
  /* Badge — unified badge/pill base */
492
656
  .badge {
493
657
  font-size: var(--text-sm); font-weight: 600;
@@ -796,6 +960,8 @@
796
960
  .modal-copy { background: var(--surface2); border: 1px solid var(--border); color: var(--muted); font-size: var(--text-base); cursor: pointer; padding: var(--space-2) var(--space-5); border-radius: var(--radius-sm); display: flex; align-items: center; gap: var(--space-2); transition: all var(--transition-base); }
797
961
  .modal-copy:hover { color: var(--text); border-color: var(--text); }
798
962
  .modal-copy.copied { color: var(--green); border-color: var(--green); }
963
+ .modal-copy.is-success { color: var(--green); border-color: var(--green); }
964
+ .modal-copy.is-danger { color: var(--red); border-color: var(--red); }
799
965
  .modal-close { background: none; border: none; color: var(--muted); font-size: var(--text-2xl); cursor: pointer; padding: var(--space-2) var(--space-4); }
800
966
  .modal-close:hover { color: var(--text); }
801
967
 
@@ -1019,20 +1185,10 @@
1019
1185
  .qa-add-artifact-btn { align-self: flex-start; margin-top: var(--space-2); }
1020
1186
  .qa-form-actions { flex-direction: row; align-items: center; gap: var(--space-4); }
1021
1187
  .qa-form-msg { font-size: var(--text-base); margin-left: var(--space-3); }
1022
- .qa-btn-primary {
1023
- background: var(--blue); color: #fff; border: none;
1024
- border-radius: var(--radius-sm); padding: var(--space-3) var(--space-7);
1025
- font-size: var(--text-md); font-weight: 600; cursor: pointer;
1026
- }
1027
- .qa-btn-primary:hover { opacity: 0.9; }
1028
- .qa-btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
1029
- .qa-btn-ghost {
1030
- background: transparent; color: var(--muted);
1031
- border: 1px solid var(--border); border-radius: var(--radius-sm);
1032
- padding: var(--space-2) var(--space-5); font-size: var(--text-base);
1033
- cursor: pointer;
1034
- }
1035
- .qa-btn-ghost:hover { color: var(--text); background: var(--surface); }
1188
+ /* .qa-btn-primary and .qa-btn-ghost were removed in W-mq57qetc0009ed48
1189
+ (PR #80 follow-up). All callsites migrated to the shared .btn-primary-solid
1190
+ and .btn-ghost primitives defined near .btn-add. .qa-btn-danger is kept
1191
+ as a color-only modifier composable with .btn-ghost. */
1036
1192
 
1037
1193
  .qa-runbooks-list { display: flex; flex-direction: column; gap: var(--space-3); }
1038
1194
  .qa-runbook-row {
@@ -86,6 +86,7 @@ Do **not** invent, regenerate, or share the nonce across dispatches — each spa
86
86
  | `followups` | array | Optional. PR-comment follow-up work items the agent dispatched via `POST /api/work-items` with `meta.pr_followup` set. Each entry: `{wi_id, title, reason, parent_comment_id}`. See [PR-comment follow-ups](#pr-comment-follow-ups). |
87
87
  | `meta.review` | object | Optional, review tasks only. Records project-local review-skill outcome — see [Review skill outcomes](#review-skill-outcomes). Aliased by the generalized `meta.skill` (W-mq1cczi90006b21f). |
88
88
  | `meta.skill` | object | Optional. Records project-local skill outcome for ANY playbook type that surfaces a `## Project skills` block (implement / fix / plan / review / etc.). See [Project skill outcomes](#project-skill-outcomes). |
89
+ | `meta.descriptionAudit` | object | Optional, `fix` / `implement` dispatches that push commits. Records the PR description audit + screenshot-refresh outcome — see [PR description audit](#pr-description-audit). |
89
90
 
90
91
  ## Project skill outcomes
91
92
 
@@ -150,6 +151,44 @@ All `meta.review` fields are optional and backward-compatible — older agents t
150
151
 
151
152
  Dispatchers can suppress the block entirely by setting `meta.skipProjectReviewSkills: true` on the review work item — the playbook then renders identically to the pre-W-mq16xtdx 8-step contract. Use this for meta-reviews of the review skill itself; the engine still accepts `meta.review.skillSkipped` in the report regardless. (W-mq1cczi90006b21f generalized this to `meta.skipProjectSkills`, which suppresses the block on every dispatch type — both flags are honored.)
152
153
 
154
+ ## PR description audit
155
+
156
+ W-mq5l3f2u000i9045. The `fix` and `implement` playbooks inline a `## PR description audit` section that the agent runs **after** pushing commits to the PR's source branch and **before** marking the work item done. The audit re-reads the PR description, compares it against the diff of the newly-pushed commits, and patches stale sections (feature lists, config tables, file/line counts, ASCII diagrams, embedded screenshots) so reviewers don't end up reading outdated prose.
157
+
158
+ The audit is best-effort:
159
+
160
+ - Screenshot recapture requires a runnable dev server (detected via `package.json` scripts named `dev`, `start`, or `serve`) and Playwright MCP. When either is missing or fails, the agent skips the recapture without failing the work item.
161
+ - GitHub does not expose a documented REST endpoint for PR-comment image uploads; the agent skips the recapture on GitHub with `result: "screenshots-refreshed-skipped (gh-attachment-unavailable)"` instead of failing.
162
+ - Raw PNGs are saved under `agents/<id>/screenshots/` only. **Screenshots NEVER land in the repo** — adding any code path that `git add`s a PNG is a release-blocker bug.
163
+ - The audit never introduces screenshots to a PR that didn't already have any (refresh only), never modifies the PR title, never toggles draft state, and never posts a separate PR comment narrating the description change.
164
+
165
+ Record the outcome under `meta.descriptionAudit`. All fields are optional and backward-compatible — older agents that never set them stay valid.
166
+
167
+ ```json
168
+ {
169
+ "status": "success",
170
+ "meta": {
171
+ "descriptionAudit": {
172
+ "ran": true,
173
+ "result": "description-patched",
174
+ "oldScreenshots": [],
175
+ "newScreenshots": [],
176
+ "patchedSections": ["feature-bullets", "config-table"]
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ | Field | Type | Notes |
183
+ |---|---|---|
184
+ | `meta.descriptionAudit.ran` | boolean | `true` if the agent executed the audit (whether or not it edited the description). `false` when the audit was suppressed (no commits pushed, no-op completion, `meta.skipDescriptionAudit` set, or the playbook skipped the audit for any reason). Omit `meta.descriptionAudit` entirely when the audit was not even considered. |
185
+ | `meta.descriptionAudit.result` | string | One of: `"no-changes-needed"`, `"description-patched"`, `"screenshots-refreshed"`, `"description-patched+screenshots-refreshed"`, or `"skipped:<reason>"` (e.g. `"skipped:meta-flag"`, `"skipped:no-commits"`, `"skipped:noop"`). The `"screenshots-refreshed-skipped (gh-attachment-unavailable)"` shape is also valid when GitHub's attachment limitation forces a partial-skip. |
186
+ | `meta.descriptionAudit.oldScreenshots` | string[] | URLs of image refs the agent replaced. Omit or `[]` when no screenshots changed. |
187
+ | `meta.descriptionAudit.newScreenshots` | string[] | URLs of newly uploaded image attachments. Omit or `[]` when no screenshots changed. |
188
+ | `meta.descriptionAudit.patchedSections` | string[] | Human-readable identifiers of the description sections that changed (e.g. `"feature-bullets"`, `"config-table"`, `"file-list"`, `"line-counts"`, `"ascii-diagram"`, `"screenshots"`). Omit or `[]` when no sections changed. |
189
+
190
+ Dispatchers can suppress the audit entirely by setting `meta.skipDescriptionAudit: true` on the work item — the playbook then renders an explicit "audit suppressed" notice instead of the audit steps. Use this for skill-meta updates, doc-only fixes whose PR description text won't be affected by the diff, and follow-up dispatches that explicitly own the description themselves.
191
+
153
192
  ## `failure_class` enum
154
193
 
155
194
  Defined in `engine/shared.js` as `FAILURE_CLASS`. Use the canonical hyphenated string.
@@ -307,6 +307,14 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
307
307
  // projects legitimately resolve it to ''.
308
308
  'project_review_skills_block',
309
309
  'skip_project_review_skills',
310
+ // W-mq5l3f2u000i9045 — opt-out flag for the PR description audit + screenshot
311
+ // refresh fragment inlined into fix.md / implement.md. Truthy (set when
312
+ // item.meta.skipDescriptionAudit is true) renders the "audit suppressed"
313
+ // notice; falsy/empty leaves the notice block out and the agent runs the
314
+ // standard audit. Optional because (a) every other playbook ignores the var
315
+ // and (b) the default state is the empty string, which the playbook's
316
+ // conditional block treats as "not skipped".
317
+ 'skip_description_audit',
310
318
  // P-e6b3c2d8 — QA Session template vars. session_id / target_kind /
311
319
  // flows_raw / managed_spawn_name are required (declared in
312
320
  // PLAYBOOK_REQUIRED_VARS['qa-session-setup']); these target_* sub-fields
@@ -435,7 +443,12 @@ function isSafePlaybookType(playbookType) {
435
443
  !name.includes('..') &&
436
444
  !/[\\/]/.test(name) &&
437
445
  !path.isAbsolute(name) &&
438
- !/^[a-zA-Z]:/.test(name);
446
+ !/^[a-zA-Z]:/.test(name) &&
447
+ // W-mq5l3f2u000i9045 — files named `_*.md` are shared fragments inlined
448
+ // into dispatching playbooks (e.g. `_pr-description-audit.md`). They are
449
+ // not standalone playbooks; reject `_…` types so a dispatch can never
450
+ // resolve to a fragment file.
451
+ !name.startsWith('_');
439
452
  }
440
453
 
441
454
  function resolvePlaybookPath(projectName, playbookType) {
@@ -1171,7 +1184,14 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
1171
1184
  // (skipProjectReviewSkills is the PR-82 alias). Default OFF: review/fix
1172
1185
  // dispatches surface the project-local skills block.
1173
1186
  skip_project_review_skills: !!(meta && meta.skipProjectReviewSkills),
1187
+ // W-mq1cczi90006b21f — generalized project-skills suppression flag.
1188
+ // Honors either the new `meta.skipProjectSkills` or the PR-82
1189
+ // `meta.skipProjectReviewSkills` alias so older work items keep working.
1174
1190
  skip_project_skills: !!(meta && (meta.skipProjectSkills || meta.skipProjectReviewSkills)),
1191
+ // W-mq5l3f2u000i9045 — opt-out for the inlined PR description audit
1192
+ // section in fix.md. Truthy renders the "audit suppressed" notice; falsy
1193
+ // (the default) leaves the standard audit instructions in place.
1194
+ skip_description_audit: !!(meta && meta.skipDescriptionAudit),
1175
1195
  ...extraVars,
1176
1196
  task_id: dispatchId,
1177
1197
  };
package/engine.js CHANGED
@@ -6121,6 +6121,14 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
6121
6121
  managed_spawn_ttl_minutes: item.meta && Number.isFinite(Number(item.meta.managed_spawn_ttl_minutes))
6122
6122
  ? Math.floor(Number(item.meta.managed_spawn_ttl_minutes))
6123
6123
  : '',
6124
+ // W-mq5l3f2u000i9045 — opt-out flag for the inlined PR description audit
6125
+ // section in fix.md / implement.md. When the dispatcher sets
6126
+ // item.meta.skipDescriptionAudit (skill-meta updates, doc-only fixes,
6127
+ // follow-up dispatches that explicitly own the description), the playbook
6128
+ // renders the "audit suppressed" notice instead of the standard audit
6129
+ // steps. Default off — every fix/implement dispatch that pushes commits
6130
+ // audits the PR description by default.
6131
+ skip_description_audit: !!(item.meta && item.meta.skipDescriptionAudit),
6124
6132
  // W-mpeiwz6k0005bf34-c — opt-in qa-validate context. The dispatch handler
6125
6133
  // POST /api/qa/runbooks/run stamps meta.qaRunId + meta.qaRunbook (full
6126
6134
  // spec) + meta.qaTarget (managed-process snapshot) on the work item;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2157",
3
+ "version": "0.1.2159",
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"
@@ -0,0 +1,142 @@
1
+ # PR description audit + screenshot refresh (shared fragment)
2
+
3
+ > **Naming convention.** Files named `_*.md` under `playbooks/` are **shared
4
+ > fragments**. The engine renderer (`engine/playbook.js`) does not (yet)
5
+ > implement an `<include>` directive, so each consuming playbook inlines this
6
+ > fragment verbatim and the two copies are kept in sync by hand. If you change
7
+ > this file, also update the inlined copies in `playbooks/fix.md` and
8
+ > `playbooks/implement.md` (and any other playbook that pushes commits to a
9
+ > tracked PR). The matching test in `test/unit/config-and-playbooks.test.js`
10
+ > ("PR description audit fragment is inlined consistently") fails fast when the
11
+ > copies drift.
12
+
13
+ ## When to run
14
+
15
+ Run this audit **after** you push new commits to the PR's source branch and
16
+ **before** you mark the work item done.
17
+
18
+ Skip the audit entirely when:
19
+
20
+ - This dispatch was invoked with `meta.skipDescriptionAudit: true` (the
21
+ engine renders an explicit "audit suppressed" notice when the flag is set).
22
+ - You did not push any new commits this dispatch (no behavior change → no
23
+ description drift).
24
+ - The dispatch produced a no-op completion (`noop: true`).
25
+
26
+ When you skip, still report it: set `meta.descriptionAudit.ran = false` and
27
+ `meta.descriptionAudit.result = "skipped:<reason>"` in the completion JSON.
28
+
29
+ ## Audit steps
30
+
31
+ 1. **Fetch the current PR description.**
32
+ - GitHub: `gh pr view <num> --repo <owner>/<repo> --json body --jq .body`
33
+ - Azure DevOps: `az repos pr show --id <num> --query description -o tsv`
34
+ 2. **Compute the diff for the commits you just pushed.**
35
+ - `git log --stat <baseline>..HEAD` where `<baseline>` is `origin/main` for
36
+ the first push or `origin/<branch>@{1}` (pre-push tip) for follow-up
37
+ pushes. `git log --oneline <baseline>..HEAD` if you only need the SHA list.
38
+ 3. **Compare description sections against the diff.** For each section,
39
+ evaluate whether the new commits change anything it claims. Focus
40
+ specifically on:
41
+ - Feature lists / "what changed" bullet points
42
+ - Config / option / matrix tables
43
+ - File or path lists
44
+ - Line counts, before/after metrics, perf numbers, table cell values
45
+ - ASCII diagrams that depict layout the diff touched
46
+ - Embedded markdown image refs (`![alt](url)`) — see the screenshot
47
+ subsection below
48
+ 4. **Decide the result.**
49
+ - Description is consistent with the diff → no-op. Record
50
+ `meta.descriptionAudit = { ran: true, result: "no-changes-needed" }`
51
+ in the completion report and stop here.
52
+ - Description needs targeted edits → build the patched body. **Preserve prose voice**. Edit only the stale parts. Do **not** rewrite. Do **not**
53
+ add new sections that weren't there before. Do **not** change the PR
54
+ title. Do **not** flip draft state, close/reopen, or post a separate PR
55
+ comment narrating the description edit.
56
+ 5. **Push the patched description.**
57
+ - GitHub: write the new body to a temp file, then
58
+ `gh pr edit <num> --repo <owner>/<repo> --body-file <file>`
59
+ - Azure DevOps:
60
+ `az repos pr update --id <num> --description "$(Get-Content -Raw <file>)"`
61
+ (PowerShell) or the POSIX equivalent. ADO's description field also
62
+ truncates around ~4 KB on the `--description` flag — if you hit that,
63
+ fall back to `az rest --method patch` against
64
+ `…/pullRequests/<id>?api-version=7.1` with `{ "description": "<body>" }`.
65
+ 6. **GET-verify the description post-patch** by re-running the fetch from
66
+ step 1 and confirming the body matches what you sent.
67
+
68
+ ## Screenshot handling
69
+
70
+ This applies only when the **existing** description embeds markdown image
71
+ refs. **Never introduce screenshots to a PR that didn't already have any** —
72
+ this audit is a refresh, not an introduction.
73
+
74
+ 1. Extract every `![alt](url)` from the current description.
75
+ 2. For each image, decide whether it depicts a view this dispatch's diff
76
+ touched. Heuristics (any one is enough):
77
+ - `alt` text or filename mentions a component / route / view name that
78
+ appears in the changed files.
79
+ - The image URL path contains an issue/PR id matching this PR.
80
+ - The description text immediately around the image references files
81
+ present in the diff.
82
+ 3. If the view was touched **and** the project has a runnable dev server
83
+ (detect via `package.json` scripts named `dev`, `start`, or `serve`)
84
+ **and** Playwright MCP is available, recapture:
85
+ 1. Spin up the dev server with a detached handoff (see
86
+ `shared-rules.md` → "Long-Running Commands"). Capture PID + log path +
87
+ URL + stop command.
88
+ 2. Drive Playwright MCP to the relevant route and screenshot the same
89
+ view. Save raw PNGs to `agents/<id>/screenshots/` only. **NEVER `git add` a PNG** — adding any code path that commits a screenshot is a
90
+ release-blocker bug.
91
+ 3. Upload as a PR attachment:
92
+ - **ADO**: `PUT /pullRequests/<id>/attachments/<filename>` with the
93
+ PNG bytes, then reference the returned URL in the description.
94
+ - **GitHub**: there is no documented REST endpoint for PR-comment
95
+ image uploads. The browser drag-drop path uses
96
+ `github.com/upload/asset`, which `gh` does not wrap. If you cannot
97
+ find a reliable scripted upload path, **skip** this screenshot,
98
+ leave the old image URL in place, and record
99
+ `meta.descriptionAudit.result = "screenshots-refreshed-skipped (gh-attachment-unavailable)"`.
100
+ 4. Patch the description to swap old image URLs for new.
101
+ 5. Stop the dev server using the recorded stop command.
102
+ 4. If the dev server fails to start, the route 404s, Playwright fails, or
103
+ the host's attachment API is unreachable → **skip that screenshot**
104
+ without failing the work item. Record the skip and the reason in
105
+ `meta.descriptionAudit`.
106
+
107
+ ## Out-of-scope guardrails
108
+
109
+ - Don't add screenshots that weren't already there.
110
+ - Don't rewrite description prose beyond the targeted stale edits.
111
+ - Don't modify the PR title.
112
+ - Don't toggle draft state, close/reopen the PR, or post a separate PR
113
+ comment narrating the description change.
114
+ - Don't fail the work item on a screenshot or attachment failure — degrade
115
+ gracefully and record the skip.
116
+ - Screenshots **never** land in the repo. Save raw PNGs only under
117
+ `agents/<id>/screenshots/`. No `git add` of any PNG, ever.
118
+
119
+ ## Completion-report shape
120
+
121
+ When the audit ran (whether it edited the description or no-op'd), include
122
+ this block in the JSON completion report (see
123
+ `docs/completion-reports.md` → "PR description audit"):
124
+
125
+ ```json
126
+ {
127
+ "meta": {
128
+ "descriptionAudit": {
129
+ "ran": true,
130
+ "result": "description-patched",
131
+ "oldScreenshots": [],
132
+ "newScreenshots": [],
133
+ "patchedSections": ["feature-bullets", "config-table"]
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ Omit `meta.descriptionAudit` entirely when the audit did not run (no
140
+ commits pushed, no-op completion, etc.). When `meta.skipDescriptionAudit`
141
+ suppressed the audit, set `ran: false` and
142
+ `result: "skipped:meta-flag"` so the audit history stays auditable.
package/playbooks/fix.md CHANGED
@@ -99,6 +99,77 @@ Do NOT remove the worktree — the engine handles cleanup automatically.
99
99
  - content: Explain what was fixed, reference each review finding, include build/test status
100
100
  - Sign: `Fixed by Minions ({{agent_name}} — {{agent_role}} · {{agent_model}})`
101
101
 
102
+ ## PR description audit (mandatory unless `meta.skipDescriptionAudit`)
103
+
104
+ <!-- Inlined from playbooks/_pr-description-audit.md — keep in sync (enforced by test/unit/config-and-playbooks.test.js). -->
105
+
106
+ {{#skip_description_audit}}
107
+ > **Audit suppressed.** This dispatch was invoked with `meta.skipDescriptionAudit: true`.
108
+ > Record `meta.descriptionAudit = { ran: false, result: "skipped:meta-flag" }` in your completion report
109
+ > and skip the audit steps below entirely.
110
+ {{/skip_description_audit}}
111
+
112
+ After you push commits to the PR's source branch and BEFORE you mark the work item done, audit the PR description and refresh it to match the new diff. Skip the audit when you pushed no commits this dispatch or when the dispatch is a no-op completion; record the skip in `meta.descriptionAudit`.
113
+
114
+ **Audit steps**
115
+
116
+ 1. Fetch the current PR description.
117
+ - GitHub: `gh pr view <num> --repo <owner>/<repo> --json body --jq .body`
118
+ - Azure DevOps: `az repos pr show --id <num> --query description -o tsv`
119
+ 2. Compute the diff for the commits you just pushed: `git log --stat <baseline>..HEAD` where `<baseline>` is `origin/<branch>@{1}` for follow-up pushes (or `origin/main` for the very first push).
120
+ 3. For each section of the description, evaluate whether the new diff invalidates anything it claims. Focus on: feature lists / "what changed" bullets, config/option tables, file or path lists, line counts, before/after metrics, ASCII diagrams, and embedded markdown image refs.
121
+ 4. **No drift** → record `meta.descriptionAudit = { ran: true, result: "no-changes-needed" }` and stop.
122
+ 5. **Drift detected** → build a patched body. **Preserve prose voice.** Edit only the stale parts. Do NOT rewrite. Do NOT add new sections. Do NOT change the PR title. Do NOT toggle draft state. Do NOT post a separate PR comment narrating the description change.
123
+ 6. Push the patched description.
124
+ - GitHub: write the new body to a temp file, then `gh pr edit <num> --repo <owner>/<repo> --body-file <file>`.
125
+ - Azure DevOps: `az repos pr update --id <num> --description "$(Get-Content -Raw <file>)"`. If the body exceeds ~4 KB, fall back to `az rest --method patch` against `…/pullRequests/<id>?api-version=7.1` with `{ "description": "<body>" }`.
126
+ 7. GET-verify the description post-patch by re-running step 1 and confirming the body matches what you sent.
127
+
128
+ **Screenshot handling (refresh-only — never introduce screenshots)**
129
+
130
+ This applies only when the **existing** description embeds markdown image refs (`![alt](url)`). NEVER add screenshots to a PR that didn't already have any.
131
+
132
+ 1. Extract every `![alt](url)` from the current description.
133
+ 2. Decide which images depict views this dispatch's diff touched (heuristic: alt text / filename / surrounding prose mentions a component / route / view name present in the changed files).
134
+ 3. If the view was touched AND the project has a runnable dev server (detect via `package.json` scripts named `dev`, `start`, or `serve`) AND Playwright MCP is available, recapture:
135
+ - Spin up the dev server with a detached handoff per `shared-rules.md` → "Long-Running Commands". Record PID + log path + URL + stop command.
136
+ - Drive Playwright MCP to the relevant route and screenshot the view. Save raw PNGs to `agents/<id>/screenshots/` ONLY. **NEVER `git add` a PNG** — committing a screenshot is a release-blocker bug.
137
+ - Upload as a PR attachment:
138
+ - **ADO**: `PUT /pullRequests/<id>/attachments/<filename>` with the PNG bytes, then reference the returned URL in the description.
139
+ - **GitHub**: no documented REST endpoint exists for PR-comment image uploads (the browser drag-drop uses `github.com/upload/asset`, which `gh` does not wrap). If you cannot find a reliable scripted upload path, **skip** the recapture, leave the old image URL in place, and record `meta.descriptionAudit.result = "screenshots-refreshed-skipped (gh-attachment-unavailable)"`.
140
+ - Patch the description to swap old image URLs for new.
141
+ - Stop the dev server using the recorded stop command.
142
+ 4. If the dev server fails to start, the route 404s, Playwright fails, or the host's attachment API is unreachable → **skip that screenshot** without failing the work item. Record the skip and the reason in `meta.descriptionAudit`.
143
+
144
+ **Out-of-scope guardrails**
145
+
146
+ - Don't add screenshots that weren't already there. Refresh only.
147
+ - Don't rewrite description prose beyond the targeted stale edits.
148
+ - Don't modify the PR title; don't toggle draft state, close/reopen the PR.
149
+ - Don't post a separate PR comment summarizing the description change.
150
+ - Don't fail the work item on a screenshot or attachment failure — degrade gracefully and record the skip.
151
+ - Screenshots NEVER land in the repo. `agents/<id>/screenshots/` only; never `git add` a PNG.
152
+
153
+ **Completion-report shape**
154
+
155
+ When the audit ran (whether it edited the description or no-op'd), include this in the completion JSON (full schema: `docs/completion-reports.md` → "PR description audit"):
156
+
157
+ ```json
158
+ {
159
+ "meta": {
160
+ "descriptionAudit": {
161
+ "ran": true,
162
+ "result": "description-patched",
163
+ "oldScreenshots": [],
164
+ "newScreenshots": [],
165
+ "patchedSections": ["feature-bullets", "config-table"]
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ Omit `meta.descriptionAudit` entirely when the audit did not run (no commits pushed, no-op completion). When `meta.skipDescriptionAudit` suppressed the audit, set `ran: false` and `result: "skipped:meta-flag"`.
172
+
102
173
  ## Resolve Review Comments
103
174
 
104
175
  After pushing, respond to each review comment/thread. **Every disposition requires a reply before you resolve, close, or mark-fixed the thread — no exceptions.** Resolving a thread silently is a process violation (see "Resolving Review Threads — No Silent Closures" in `shared-rules.md`).
@@ -101,6 +101,77 @@ Create the PR for implement tasks — the engine tracks review and completion fr
101
101
 
102
102
  Include build/test status and run instructions in the PR description. If the project has a runnable app, include the localhost URL.
103
103
 
104
+ ## PR description audit (mandatory unless `meta.skipDescriptionAudit`)
105
+
106
+ <!-- Inlined from playbooks/_pr-description-audit.md — keep in sync (enforced by test/unit/config-and-playbooks.test.js). -->
107
+
108
+ {{#skip_description_audit}}
109
+ > **Audit suppressed.** This dispatch was invoked with `meta.skipDescriptionAudit: true`.
110
+ > Record `meta.descriptionAudit = { ran: false, result: "skipped:meta-flag" }` in your completion report
111
+ > and skip the audit steps below entirely.
112
+ {{/skip_description_audit}}
113
+
114
+ After you push commits to the PR's source branch and BEFORE you mark the work item done, audit the PR description and refresh it to match the new diff. Skip the audit when you pushed no commits this dispatch or when the dispatch is a no-op completion; record the skip in `meta.descriptionAudit`.
115
+
116
+ **Audit steps**
117
+
118
+ 1. Fetch the current PR description.
119
+ - GitHub: `gh pr view <num> --repo <owner>/<repo> --json body --jq .body`
120
+ - Azure DevOps: `az repos pr show --id <num> --query description -o tsv`
121
+ 2. Compute the diff for the commits you just pushed: `git log --stat <baseline>..HEAD` where `<baseline>` is `origin/<branch>@{1}` for follow-up pushes (or `origin/main` for the very first push).
122
+ 3. For each section of the description, evaluate whether the new diff invalidates anything it claims. Focus on: feature lists / "what changed" bullets, config/option tables, file or path lists, line counts, before/after metrics, ASCII diagrams, and embedded markdown image refs.
123
+ 4. **No drift** → record `meta.descriptionAudit = { ran: true, result: "no-changes-needed" }` and stop.
124
+ 5. **Drift detected** → build a patched body. **Preserve prose voice.** Edit only the stale parts. Do NOT rewrite. Do NOT add new sections. Do NOT change the PR title. Do NOT toggle draft state. Do NOT post a separate PR comment narrating the description change.
125
+ 6. Push the patched description.
126
+ - GitHub: write the new body to a temp file, then `gh pr edit <num> --repo <owner>/<repo> --body-file <file>`.
127
+ - Azure DevOps: `az repos pr update --id <num> --description "$(Get-Content -Raw <file>)"`. If the body exceeds ~4 KB, fall back to `az rest --method patch` against `…/pullRequests/<id>?api-version=7.1` with `{ "description": "<body>" }`.
128
+ 7. GET-verify the description post-patch by re-running step 1 and confirming the body matches what you sent.
129
+
130
+ **Screenshot handling (refresh-only — never introduce screenshots)**
131
+
132
+ This applies only when the **existing** description embeds markdown image refs (`![alt](url)`). NEVER add screenshots to a PR that didn't already have any.
133
+
134
+ 1. Extract every `![alt](url)` from the current description.
135
+ 2. Decide which images depict views this dispatch's diff touched (heuristic: alt text / filename / surrounding prose mentions a component / route / view name present in the changed files).
136
+ 3. If the view was touched AND the project has a runnable dev server (detect via `package.json` scripts named `dev`, `start`, or `serve`) AND Playwright MCP is available, recapture:
137
+ - Spin up the dev server with a detached handoff per `shared-rules.md` → "Long-Running Commands". Record PID + log path + URL + stop command.
138
+ - Drive Playwright MCP to the relevant route and screenshot the view. Save raw PNGs to `agents/<id>/screenshots/` ONLY. **NEVER `git add` a PNG** — committing a screenshot is a release-blocker bug.
139
+ - Upload as a PR attachment:
140
+ - **ADO**: `PUT /pullRequests/<id>/attachments/<filename>` with the PNG bytes, then reference the returned URL in the description.
141
+ - **GitHub**: no documented REST endpoint exists for PR-comment image uploads (the browser drag-drop uses `github.com/upload/asset`, which `gh` does not wrap). If you cannot find a reliable scripted upload path, **skip** the recapture, leave the old image URL in place, and record `meta.descriptionAudit.result = "screenshots-refreshed-skipped (gh-attachment-unavailable)"`.
142
+ - Patch the description to swap old image URLs for new.
143
+ - Stop the dev server using the recorded stop command.
144
+ 4. If the dev server fails to start, the route 404s, Playwright fails, or the host's attachment API is unreachable → **skip that screenshot** without failing the work item. Record the skip and the reason in `meta.descriptionAudit`.
145
+
146
+ **Out-of-scope guardrails**
147
+
148
+ - Don't add screenshots that weren't already there. Refresh only.
149
+ - Don't rewrite description prose beyond the targeted stale edits.
150
+ - Don't modify the PR title; don't toggle draft state, close/reopen the PR.
151
+ - Don't post a separate PR comment summarizing the description change.
152
+ - Don't fail the work item on a screenshot or attachment failure — degrade gracefully and record the skip.
153
+ - Screenshots NEVER land in the repo. `agents/<id>/screenshots/` only; never `git add` a PNG.
154
+
155
+ **Completion-report shape**
156
+
157
+ When the audit ran (whether it edited the description or no-op'd), include this in the completion JSON (full schema: `docs/completion-reports.md` → "PR description audit"):
158
+
159
+ ```json
160
+ {
161
+ "meta": {
162
+ "descriptionAudit": {
163
+ "ran": true,
164
+ "result": "description-patched",
165
+ "oldScreenshots": [],
166
+ "newScreenshots": [],
167
+ "patchedSections": ["feature-bullets", "config-table"]
168
+ }
169
+ }
170
+ }
171
+ ```
172
+
173
+ Omit `meta.descriptionAudit` entirely when the audit did not run (no commits pushed, no-op completion). When `meta.skipDescriptionAudit` suppressed the audit, set `ran: false` and `result: "skipped:meta-flag"`.
174
+
104
175
  ## When to Stop
105
176
 
106
177
  Your task is complete when the requested implementation is delivered, the validation story is truthful and sufficient for review, the branch is pushed, and the PR exists. Include the PR URL in your final message so the engine can track it.