@yemi33/minions 0.1.2424 → 0.1.2426

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.
Files changed (185) hide show
  1. package/bin/install-internal-minions.js +1209 -0
  2. package/bin/minions.js +75 -9
  3. package/dashboard/docs/typography.md +27 -12
  4. package/dashboard/js/command-center.js +13 -11
  5. package/dashboard/js/live-stream.js +1 -1
  6. package/dashboard/js/memory-search.js +388 -43
  7. package/dashboard/js/modal-qa.js +5 -5
  8. package/dashboard/js/qa.js +39 -26
  9. package/dashboard/js/refresh.js +37 -18
  10. package/dashboard/js/render-agents.js +14 -18
  11. package/dashboard/js/render-dispatch.js +14 -74
  12. package/dashboard/js/render-inbox.js +3 -3
  13. package/dashboard/js/render-meetings.js +8 -8
  14. package/dashboard/js/render-other.js +45 -20
  15. package/dashboard/js/render-pipelines.js +6 -6
  16. package/dashboard/js/render-plans.js +14 -14
  17. package/dashboard/js/render-prd.js +46 -46
  18. package/dashboard/js/render-prs.js +9 -65
  19. package/dashboard/js/render-schedules.js +5 -5
  20. package/dashboard/js/render-skills.js +6 -6
  21. package/dashboard/js/render-utils.js +4 -4
  22. package/dashboard/js/render-watches.js +4 -16
  23. package/dashboard/js/render-work-items.js +95 -107
  24. package/dashboard/js/settings.js +175 -74
  25. package/dashboard/layout.html +3 -2
  26. package/dashboard/pages/inbox.html +1 -1
  27. package/dashboard/pages/work.html +1 -1
  28. package/dashboard/shared/model-display.js +9 -0
  29. package/dashboard/shared/watches-source.js +43 -0
  30. package/dashboard/shared/welcome-popup.js +259 -0
  31. package/dashboard/slim/body.html +6 -6
  32. package/dashboard/slim/js/chat.js +4 -2
  33. package/dashboard/slim/js/history.js +10 -3
  34. package/dashboard/slim/js/modals-tiles.js +85 -16
  35. package/dashboard/slim/js/status.js +12 -16
  36. package/dashboard/slim/styles.css +24 -4
  37. package/dashboard/styles.css +62 -27
  38. package/dashboard-build.js +8 -2
  39. package/dashboard.js +3854 -2514
  40. package/docs/README.md +7 -5
  41. package/docs/api-errors.md +144 -0
  42. package/docs/auto-discovery.md +83 -61
  43. package/docs/capture-demos.js +173 -30
  44. package/docs/command-center.md +5 -1
  45. package/docs/completion-reports.md +127 -11
  46. package/docs/constants.md +15 -3
  47. package/docs/constellation-style-telemetry.md +2 -2
  48. package/docs/copilot-cli-schema.md +32 -22
  49. package/docs/cross-repo-plans.md +30 -30
  50. package/docs/demo/memory-system.html +1 -1
  51. package/docs/deprecated.json +132 -18
  52. package/docs/diagnostics-memory.md +8 -4
  53. package/docs/human-vs-automated.md +2 -2
  54. package/docs/index.html +9 -2
  55. package/docs/internal-install.md +212 -0
  56. package/docs/kb-pr3223-cascade-archiving.md +16 -0
  57. package/docs/kb-pr696-merge-conflict-docs.md +23 -0
  58. package/docs/kb-sweep.md +2 -2
  59. package/docs/keep-processes.md +8 -1
  60. package/docs/live-checkout-mode.md +1 -1
  61. package/docs/managed-spawn.md +21 -3
  62. package/docs/named-agents.md +3 -2
  63. package/docs/onboarding.md +26 -0
  64. package/docs/plan-lifecycle.md +32 -32
  65. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  66. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  67. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  68. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  69. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  70. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  71. package/docs/qa-runbook-lifecycle.md +67 -51
  72. package/docs/qa-runbooks.md +2 -2
  73. package/docs/runtime-adapters.md +101 -50
  74. package/docs/security.md +28 -0
  75. package/docs/self-improvement.md +47 -13
  76. package/docs/skills.md +29 -0
  77. package/docs/slim-ux/concepts.md +2 -2
  78. package/docs/specs/agent-configurability.md +43 -41
  79. package/docs/specs/agent-rename.md +13 -13
  80. package/docs/team-memory.md +283 -14
  81. package/docs/tutorials/01-install-and-connect.md +23 -0
  82. package/docs/tutorials/03-plan-driven-feature.md +3 -3
  83. package/docs/tutorials/04-runtimes-and-harness.md +18 -21
  84. package/docs/tutorials/08-operations-and-recovery.md +3 -2
  85. package/docs/tutorials/README.md +4 -3
  86. package/docs/visual-evidence-ci.md +103 -0
  87. package/docs/watches.md +35 -30
  88. package/docs/worktree-lifecycle.md +151 -13
  89. package/engine/acp-transport.js +1 -0
  90. package/engine/ado.js +80 -33
  91. package/engine/agent-api-validation.js +571 -0
  92. package/engine/agent-worker-pool.js +7 -6
  93. package/engine/api-contracts/agent-content.js +761 -0
  94. package/engine/api-contracts/cc-ops.js +219 -0
  95. package/engine/api-contracts/config-runtime.js +419 -0
  96. package/engine/api-contracts/core.js +138 -0
  97. package/engine/api-contracts/index.js +506 -0
  98. package/engine/api-contracts/orchestration.js +602 -0
  99. package/engine/api-contracts/pull-requests.js +323 -0
  100. package/engine/api-contracts/qa-process.js +764 -0
  101. package/engine/api-contracts/work-plan-prd.js +494 -0
  102. package/engine/api-validation.js +543 -0
  103. package/engine/cc-api-validation.js +586 -0
  104. package/engine/cc-worker-pool.js +9 -1
  105. package/engine/cleanup.js +393 -271
  106. package/engine/cli.js +65 -7
  107. package/engine/comment-classifier.js +70 -5
  108. package/engine/consolidation.js +123 -5
  109. package/engine/content-api-validation.js +606 -0
  110. package/engine/create-pr-worktree.js +22 -1
  111. package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
  112. package/engine/dispatch.js +33 -0
  113. package/engine/distribution.js +189 -0
  114. package/engine/execution-model.js +59 -0
  115. package/engine/features.js +12 -0
  116. package/engine/github.js +67 -25
  117. package/engine/harness-context.js +313 -0
  118. package/engine/inbox-store.js +1 -1
  119. package/engine/kb-sweep.js +161 -38
  120. package/engine/keep-process-sweep.js +43 -3
  121. package/engine/lifecycle.js +1066 -165
  122. package/engine/llm.js +4 -0
  123. package/engine/managed-spawn.js +463 -48
  124. package/engine/meeting.js +197 -34
  125. package/engine/memory-retrieval.js +280 -0
  126. package/engine/memory-store.js +821 -38
  127. package/engine/model-discovery.js +17 -3
  128. package/engine/pipeline.js +802 -14
  129. package/engine/plan-prd-validation.js +745 -0
  130. package/engine/playbook.js +206 -17
  131. package/engine/pooled-agent-process.js +6 -1
  132. package/engine/pr-action.js +1 -1
  133. package/engine/pr-issue-validation.js +653 -0
  134. package/engine/pr-resolve.js +75 -13
  135. package/engine/prd-store.js +39 -6
  136. package/engine/preflight.js +7 -2
  137. package/engine/process-utils.js +79 -31
  138. package/engine/projects.js +198 -12
  139. package/engine/promotion.js +371 -0
  140. package/engine/qa-from-prd.js +3 -0
  141. package/engine/qa-process-validation.js +572 -0
  142. package/engine/qa-runbooks.js +41 -37
  143. package/engine/qa-runners/maestro.js +11 -7
  144. package/engine/qa-runners/playwright.js +37 -14
  145. package/engine/qa-runners.js +9 -10
  146. package/engine/qa-runs.js +124 -11
  147. package/engine/qa-sessions.js +139 -40
  148. package/engine/quarantine-refs.js +81 -11
  149. package/engine/queries.js +163 -9
  150. package/engine/review-learning-backfill.js +482 -0
  151. package/engine/review-learning.js +1236 -0
  152. package/engine/runtimes/claude.js +60 -2
  153. package/engine/runtimes/codex.js +57 -16
  154. package/engine/runtimes/contract.js +21 -0
  155. package/engine/runtimes/copilot.js +51 -3
  156. package/engine/runtimes/index.js +1 -0
  157. package/engine/scheduler.js +201 -32
  158. package/engine/settings-validation.js +909 -0
  159. package/engine/shared.js +705 -208
  160. package/engine/spawn-agent.js +48 -21
  161. package/engine/steering-constraints.js +31 -0
  162. package/engine/steering-store.js +10 -2
  163. package/engine/steering.js +14 -2
  164. package/engine/timeout.js +404 -111
  165. package/engine/untrusted-fence.js +24 -10
  166. package/engine/watch-actions.js +7 -2
  167. package/engine/watches.js +353 -51
  168. package/engine/work-item-validation.js +561 -0
  169. package/engine/work-items-store.js +61 -1
  170. package/engine/worktree-gc.js +656 -37
  171. package/engine/worktree-preflight.js +154 -0
  172. package/engine.js +2068 -342
  173. package/package.json +1 -1
  174. package/playbooks/_pr-description-audit.md +88 -41
  175. package/playbooks/build-fix-complex.md +3 -3
  176. package/playbooks/docs.md +2 -2
  177. package/playbooks/fix.md +18 -18
  178. package/playbooks/implement.md +18 -18
  179. package/playbooks/qa-session-draft.md +10 -12
  180. package/playbooks/qa-session-execute.md +16 -12
  181. package/playbooks/qa-validate.md +8 -7
  182. package/playbooks/shared-rules.md +45 -0
  183. package/playbooks/test.md +4 -0
  184. package/prompts/cc-system.md +1 -1
  185. package/skills/check-self-authored-review-comment/SKILL.md +2 -2
@@ -350,11 +350,11 @@ function renderPipelines(pipelines, opts) {
350
350
  '</div>' +
351
351
  '<div class="pipeline-card-badges">' +
352
352
  '<span style="color:' + statusColor + ';font-size:var(--text-base);font-weight:600">' + escHtml(statusLabel) + '</span>' +
353
- (p.stopWhen ? '<span style="font-size:var(--text-xs);color:var(--yellow)" title="Auto-stops when condition met: ' + escHtml(typeof p.stopWhen === 'string' ? p.stopWhen : (p.stopWhen.check || 'condition')) + '">STOP-WHEN</span>' : '') +
354
- (p.enabled === false ? '<span style="font-size:var(--text-xs);color:var(--red)"' + (p._stopReason ? ' title="' + escHtml(p._stopReason) + '"' : '') + '>' + (p._stoppedBy ? 'AUTO-STOPPED' : 'DISABLED') + '</span>' : '') +
353
+ (p.stopWhen ? '<span style="font-size:var(--text-sm);color:var(--yellow)" title="Auto-stops when condition met: ' + escHtml(typeof p.stopWhen === 'string' ? p.stopWhen : (p.stopWhen.check || 'condition')) + '">STOP-WHEN</span>' : '') +
354
+ (p.enabled === false ? '<span style="font-size:var(--text-sm);color:var(--red)"' + (p._stopReason ? ' title="' + escHtml(p._stopReason) + '"' : '') + '>' + (p._stoppedBy ? 'AUTO-STOPPED' : 'DISABLED') + '</span>' : '') +
355
355
  (p.enabled !== false
356
356
  ? (activeRun
357
- ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--yellow);border-color:var(--yellow)" onclick="event.stopPropagation();_retriggerPipeline(\'' + escHtml(p.id) + '\',this)">Retrigger</button>'
357
+ ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--yellow);border-color:var(--yellow)" onclick="event.stopPropagation();_retriggerPipeline(\'' + escHtml(p.id) + '\',this)">Retrigger</button>'
358
358
  : '<button class="btn-action btn-action-md" onclick="event.stopPropagation();_triggerPipeline(\'' + escHtml(p.id) + '\',this)">Run Now</button>')
359
359
  : '') +
360
360
  '</div>' +
@@ -411,11 +411,11 @@ function openPipelineDetail(id) {
411
411
  (activeRun ? '<button class="btn-destructive btn-destructive-md" onclick="_abortPipeline(\'' + escHtml(id) + '\',this)">Abort</button>' : '') +
412
412
  (p.enabled !== false
413
413
  ? (activeRun
414
- ? '<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>'
414
+ ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--yellow);border-color:var(--yellow)" onclick="_retriggerPipeline(\'' + escHtml(id) + '\',this)">Retrigger</button>'
415
415
  : '<button class="btn-action btn-action-md" onclick="_triggerPipeline(\'' + escHtml(id) + '\',this)">Run Now</button>')
416
416
  : '') +
417
- '<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>' +
418
- '<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>' +
417
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--blue);border-color:var(--blue)" onclick="openEditPipelineModal(\'' + escHtml(id) + '\')">Edit</button>' +
418
+ '<button class="pr-pager-btn' + (_pipelineToggleInFlight.has(id) ? ' disabled' : '') + '" style="font-size:var(--text-sm);padding:2px 8px" onclick="_togglePipelineEnabled(\'' + escHtml(id) + '\',' + !p.enabled + ',this)">' + (p.enabled !== false ? 'Disable' : 'Enable') + '</button>' +
419
419
  '<button class="btn-destructive btn-destructive-md" onclick="_deletePipelineConfirm(\'' + escHtml(id) + '\')">Delete</button>' +
420
420
  '</div>' +
421
421
  '</div>';
@@ -343,7 +343,7 @@ function renderPlans(plans) {
343
343
  actions = '<div class="plan-card-meta" style="margin-top:6px;color:var(--purple,#a855f7)">Revision in progress: ' + escapeHtml((p.revisionFeedback || '').slice(0, 100)) + '</div>';
344
344
  }
345
345
 
346
- const executeBtn = isDraft && (effectiveStatus === 'active' || effectiveStatus === 'draft') && !isArchived && !prdFile ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--green);font-weight:600" ' +
346
+ const executeBtn = isDraft && (effectiveStatus === 'active' || effectiveStatus === 'draft') && !isArchived && !prdFile ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--green);font-weight:600" ' +
347
347
  'onclick="event.stopPropagation();planExecute(\'' + escapeHtml(p.file) + '\',\'' + escapeHtml(p.project) + '\',this)">Execute</button>' : '';
348
348
  const showPause = effectiveStatus === 'dispatched' && prdFile && !isArchived;
349
349
  // Resume pill not needed — paused state is handled by the actions block above
@@ -351,26 +351,26 @@ function renderPlans(plans) {
351
351
  const verifyWis = allWi.filter(w => w.itemType === 'verify' && w.sourcePlan === prdFile);
352
352
  const hasVerifyWi = verifyWis.length > 0;
353
353
  const showVerify = effectiveStatus === 'completed' && prdFile && !isArchived && !hasVerifyWi;
354
- const pauseBtn = showPause ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--yellow)" ' +
354
+ const pauseBtn = showPause ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--yellow)" ' +
355
355
  'onclick="event.stopPropagation();planPause(\'' + escapeHtml(prdFile) + '\',this)">Pause</button>' : '';
356
356
  const resumeBtn = showResume
357
- ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--green)" ' +
357
+ ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--green)" ' +
358
358
  'onclick="event.stopPropagation();planApprove(\'' + escapeHtml(prdFile) + '\',this)">Resume</button>'
359
359
  : '';
360
- const verifyBtn = showVerify ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--green)" ' +
360
+ const verifyBtn = showVerify ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--green)" ' +
361
361
  'onclick="event.stopPropagation();triggerVerify(\'' + escapeHtml(prdFile) + '\',this)">Verify</button>' : '';
362
362
  const showArchive = !isArchived;
363
363
  const archiveFile = prdFile || p.file;
364
364
  const archiveReady = p.archiveReady && !isArchived;
365
- const archiveBtn = showArchive ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px' +
365
+ const archiveBtn = showArchive ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px' +
366
366
  (archiveReady ? ';color:var(--green);font-weight:600;border:1px solid var(--green)' : '') + '" ' +
367
367
  'onclick="event.stopPropagation();planArchive(\'' + escapeHtml(archiveFile) + '\',this)">' +
368
368
  (archiveReady ? '✓ Archive' : 'Archive') + '</button>' : '';
369
- const archiveReadyBadge = archiveReady ? '<span style="font-size:var(--text-xs);font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(63,185,80,0.15);color:var(--green);vertical-align:middle" title="Verification passed — ready to archive">Ready to archive</span>' : '';
370
- const deleteBtn = !isArchived ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--red)" ' +
369
+ const archiveReadyBadge = archiveReady ? '<span style="font-size:var(--text-sm);font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(63,185,80,0.15);color:var(--green);vertical-align:middle" title="Verification passed — ready to archive">Ready to archive</span>' : '';
370
+ const deleteBtn = !isArchived ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--red)" ' +
371
371
  'onclick="event.stopPropagation();planDelete(\'' + escapeHtml(p.file) + '\')">Delete</button>' : '';
372
372
 
373
- const versionBadge = p.version ? ' <span style="font-size:var(--text-xs);font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue);vertical-align:middle">v' + p.version + '</span>' : '';
373
+ const versionBadge = p.version ? ' <span style="font-size:var(--text-sm);font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue);vertical-align:middle">v' + p.version + '</span>' : '';
374
374
  const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'converting': 'var(--yellow)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--purple,#a855f7)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)', 'draft': 'var(--muted)' };
375
375
  const cardClass = effectiveStatus === 'dispatched' || effectiveStatus === 'converting' ? 'working' : effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused' ? 'awaiting' : effectiveStatus;
376
376
  // P-e8d49105 — cross-repo plans surface every touched project as its
@@ -399,7 +399,7 @@ function renderPlans(plans) {
399
399
  const color = isComplete ? 'var(--green)' : 'var(--muted)';
400
400
  return '<span title="' + escapeHtml(pn) + ': ' + complete + '/' + total +
401
401
  (isComplete ? ' complete' : ' in progress') + '" ' +
402
- 'style="font-size:var(--text-xs);font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(110,118,129,0.15);color:' + color + '">' +
402
+ 'style="font-size:var(--text-sm);font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(110,118,129,0.15);color:' + color + '">' +
403
403
  escapeHtml(pn) + ' ' + complete + '/' + total + tail + '</span>';
404
404
  }).join(' ')
405
405
  : '';
@@ -407,7 +407,7 @@ function renderPlans(plans) {
407
407
  '<div class="plan-card-header">' +
408
408
  '<div><div class="plan-card-title">' + escapeHtml(p.summary || p.file) + versionBadge + '</div>' +
409
409
  '<div class="plan-card-meta">' +
410
- '<span style="font-size:var(--text-xs);font-weight:600;padding:1px 8px;border-radius:10px;background:color-mix(in srgb, ' + (statusColors[effectiveStatus] || 'var(--muted)') + ' 15%, transparent);color:' + (statusColors[effectiveStatus] || 'var(--muted)') + ';border:1px solid color-mix(in srgb, ' + (statusColors[effectiveStatus] || 'var(--muted)') + ' 40%, transparent)">' + escapeHtml(label) + '</span>' +
410
+ '<span style="font-size:var(--text-sm);font-weight:600;padding:1px 8px;border-radius:10px;background:color-mix(in srgb, ' + (statusColors[effectiveStatus] || 'var(--muted)') + ' 15%, transparent);color:' + (statusColors[effectiveStatus] || 'var(--muted)') + ';border:1px solid color-mix(in srgb, ' + (statusColors[effectiveStatus] || 'var(--muted)') + ' 40%, transparent)">' + escapeHtml(label) + '</span>' +
411
411
  projectMeta +
412
412
  '<span>' + p.itemCount + ' items</span>' +
413
413
  perProjectPills +
@@ -1074,13 +1074,13 @@ function _renderVerifyBadge(verifyWi, opts) {
1074
1074
  const planFile = verifyWi.sourcePlan || '';
1075
1075
  const planSlug = planFile.replace('.json', '');
1076
1076
  const verifyPr = allPrs.find(pr => (pr.prdItems || []).includes(verifyWi.id) || (pr.branch && pr.branch.includes(planSlug) && (pr.title || '').includes('[E2E]')));
1077
- const prLink = verifyPr?.url ? ' <a href="' + escapeHtml(verifyPr.url) + '" target="_blank" onclick="event.stopPropagation()" style="color:var(--blue);text-decoration:underline;font-size:var(--text-xs)">' + escapeHtml(verifyPr.id || 'E2E PR') + '</a>' : '';
1077
+ const prLink = verifyPr?.url ? ' <a href="' + escapeHtml(verifyPr.url) + '" target="_blank" onclick="event.stopPropagation()" style="color:var(--blue);text-decoration:underline;font-size:var(--text-sm)">' + escapeHtml(verifyPr.id || 'E2E PR') + '</a>' : '';
1078
1078
  // Testing guide. verifyGuides moved to /api/verify-guides
1079
1079
  // (window._lastVerifyGuides set by refresh.js).
1080
1080
  const guides = window._lastVerifyGuides || [];
1081
1081
  const guide = guides.find(g => g.planFile === planFile);
1082
- const guideLink = guide ? ' <span onclick="event.stopPropagation();openVerifyGuide(\'' + escapeHtml(guide.file) + '\')" style="color:var(--green);cursor:pointer;text-decoration:underline;font-size:var(--text-xs)">Testing Guide</span>' : '';
1083
- return '<span style="font-size:var(--text-xs);font-weight:600;color:' + color + ';padding:0 4px">' + label + '</span>' + prLink + guideLink;
1082
+ const guideLink = guide ? ' <span onclick="event.stopPropagation();openVerifyGuide(\'' + escapeHtml(guide.file) + '\')" style="color:var(--green);cursor:pointer;text-decoration:underline;font-size:var(--text-sm)">Testing Guide</span>' : '';
1083
+ return '<span style="font-size:var(--text-sm);font-weight:600;color:' + color + ';padding:0 4px">' + label + '</span>' + prLink + guideLink;
1084
1084
  }
1085
1085
 
1086
1086
  async function openVerifyGuide(file) {
@@ -1088,7 +1088,7 @@ async function openVerifyGuide(file) {
1088
1088
  const normalizedFile = normalizePlanFile(file);
1089
1089
  const content = await fetch('/api/plans/' + encodeURIComponent(normalizedFile)).then(r => r.text());
1090
1090
  document.getElementById('modal-title').innerHTML = 'Manual Testing Guide' +
1091
- ' <button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;margin-left:8px;vertical-align:middle" onclick="openArchivedPrdModal()">Back</button>';
1091
+ ' <button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;margin-left:8px;vertical-align:middle" onclick="openArchivedPrdModal()">Back</button>';
1092
1092
  // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
1093
1093
  document.getElementById('modal-body').innerHTML = renderMd(content);
1094
1094
  _modalDocContext = { title: 'Manual Testing Guide', content, selection: '' };
@@ -110,12 +110,12 @@ function renderPrd(prd, prog) {
110
110
  const hasVerifyWi = allWi.some(w => w.itemType === 'verify' && w.sourcePlan === prdFile);
111
111
  actions = (hasVerifyWi ? '' : ' <button class="btn-action" style="margin-left:4px" onclick="triggerVerify(\'' + escHtml(prdFile) + '\',this)">Verify</button>') +
112
112
  ' <button class="btn-action" style="margin-left:4px" onclick="qaThisPrd(\'' + escHtml(prdFile) + '\',this)">QA this PRD</button>' +
113
- ' <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
+ ' <button class="pr-pager-btn" style="font-size:var(--text-sm);padding:1px 6px;margin-left:4px" onclick="planArchive(\'' + escHtml(prdFile) + '\',this)">Archive</button>';
114
114
  } else if (effectiveStatus === 'dispatched') {
115
- 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
+ actions = ' <button class="pr-pager-btn" style="font-size:var(--text-sm);padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="planPause(\'' + escHtml(prdFile) + '\',this)">Pause</button>';
116
116
  } else if (effectiveStatus === 'paused') {
117
117
  actions = ' <button class="btn-action" style="margin-left:4px" onclick="planApprove(\'' + escHtml(prdFile) + '\',this)">Resume</button>' +
118
- ' <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
+ ' <button class="pr-pager-btn" style="font-size:var(--text-sm);padding:1px 6px;margin-left:4px" onclick="planArchive(\'' + escHtml(prdFile) + '\',this)">Archive</button>';
119
119
  }
120
120
  }
121
121
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: PRD file; status label/color and age text are derived UI state)
@@ -242,7 +242,7 @@ function renderPrdProgress(prog) {
242
242
  const labels = { 'done': 'DONE', 'in-progress': 'WIP', 'failed': 'FAIL', 'paused': 'PAUSED', 'missing': '\u2014', 'updated': 'REDO' };
243
243
  const style = styles[s] || 'background:var(--surface);color:var(--muted)';
244
244
  const label = labels[s] || '—';
245
- return '<span style="font-size:var(--text-xs);font-weight:700;padding:2px 6px;border-radius:3px;letter-spacing:0.5px;white-space:nowrap;' + style + '">' + label + '</span>';
245
+ return '<span style="font-size:var(--text-sm);font-weight:700;padding:2px 6px;border-radius:3px;letter-spacing:0.5px;white-space:nowrap;' + style + '">' + label + '</span>';
246
246
  };
247
247
 
248
248
  // Build work item lookup by actual work item ID.
@@ -276,13 +276,13 @@ function renderPrdProgress(prog) {
276
276
  const wi = wiById[workItemId] || wiById[i.id];
277
277
  const wiAgent = wi?.dispatched_to ? (agentData.find(a => a.id === wi.dispatched_to) || {}) : null;
278
278
  const wiLabel = _renderPrdWorkItemIdBadge(wi ? wi.id : workItemId, i.id);
279
- const agentLabel = wiAgent ? '<span style="font-size:var(--text-xs);color:var(--muted)" title="' + escHtml(wiAgent.name || wi.dispatched_to) + '">' +
279
+ const agentLabel = wiAgent ? '<span style="font-size:var(--text-sm);color:var(--muted)" title="' + escHtml(wiAgent.name || wi.dispatched_to) + '">' +
280
280
  (wiAgent.emoji || '') + ' ' + escHtml(wiAgent.name || wi.dispatched_to) + '</span>' : '';
281
281
 
282
282
  // Branch label — show the target branch for this item
283
283
  const wiBranch = wi ? (wi.branch || wi.featureBranch || (wi._artifacts && wi._artifacts.branch) || '') : '';
284
284
  const branchLabel = wiBranch
285
- ? '<span style="font-size:var(--text-xs);color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:3px;border:1px solid var(--border);font-family:monospace;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;vertical-align:middle" title="Branch: ' + escHtml(wiBranch) + '">&#x1F33F; ' + escHtml(wiBranch) + '</span>'
285
+ ? '<span style="font-size:var(--text-sm);color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:3px;border:1px solid var(--border);font-family:monospace;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;vertical-align:middle" title="Branch: ' + escHtml(wiBranch) + '">&#x1F33F; ' + escHtml(wiBranch) + '</span>'
286
286
  : '';
287
287
 
288
288
  // Requeue button for failed items or PRD items with no work item (orphaned/deleted)
@@ -291,19 +291,19 @@ function renderPrdProgress(prog) {
291
291
  const requeueState = getPrdRequeueState(wi ? wi.id : i.id);
292
292
  let requeueBtn = '';
293
293
  if (requeueState && requeueState.status === 'pending') {
294
- requeueBtn = '<span style="color:var(--yellow);cursor:wait;font-size:var(--text-xs);padding:1px 5px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.35);border-radius:3px" title="Retry request in progress">requeuing…</span>';
294
+ requeueBtn = '<span style="color:var(--yellow);cursor:wait;font-size:var(--text-sm);padding:1px 5px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.35);border-radius:3px" title="Retry request in progress">requeuing…</span>';
295
295
  } else if (requeueState && requeueState.status === 'queued') {
296
- requeueBtn = '<span style="color:var(--green);cursor:default;font-size:var(--text-xs);padding:1px 5px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.35);border-radius:3px" title="Successfully requeued">requeued</span>';
296
+ requeueBtn = '<span style="color:var(--green);cursor:default;font-size:var(--text-sm);padding:1px 5px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.35);border-radius:3px" title="Successfully requeued">requeued</span>';
297
297
  } else if (requeueState && requeueState.status === 'error') {
298
- requeueBtn = '<span style="color:var(--red);cursor:default;font-size:var(--text-xs);padding:1px 5px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.35);border-radius:3px" title="' + escHtml(requeueState.message || 'Retry failed') + '">retry failed</span>';
298
+ requeueBtn = '<span style="color:var(--red);cursor:default;font-size:var(--text-sm);padding:1px 5px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.35);border-radius:3px" title="' + escHtml(requeueState.message || 'Retry failed') + '">retry failed</span>';
299
299
  } else if (canRequeue) {
300
- requeueBtn = '<span onclick="event.stopPropagation();prdItemRequeue(\'' + escHtml(wi ? wi.id : i.id) + '\',\'' + escHtml(wi ? (wi._source || '') : '') + '\',\'' + escHtml(i.source || '') + '\')" style="color:var(--green);cursor:pointer;font-size:var(--text-xs);padding:1px 5px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px" title="Requeue this work item">retry</span>';
300
+ requeueBtn = '<span onclick="event.stopPropagation();prdItemRequeue(\'' + escHtml(wi ? wi.id : i.id) + '\',\'' + escHtml(wi ? (wi._source || '') : '') + '\',\'' + escHtml(i.source || '') + '\')" style="color:var(--green);cursor:pointer;font-size:var(--text-sm);padding:1px 5px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px" title="Requeue this work item">retry</span>';
301
301
  }
302
302
 
303
303
  // Re-open button for done items — sets PRD item to "updated" so engine re-dispatches
304
304
  const isDone = i.status === 'done' || (wi && wi.status === 'done');
305
305
  const reopenBtn = isDone
306
- ? '<span onclick="event.stopPropagation();prdItemReopen(\'' + escHtml(i.source || '') + '\',\'' + escHtml(i.id) + '\')" style="color:var(--blue);cursor:pointer;font-size:var(--text-xs);padding:1px 5px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:3px" title="Re-open: set to updated so engine re-dispatches on existing branch">re-open</span>'
306
+ ? '<span onclick="event.stopPropagation();prdItemReopen(\'' + escHtml(i.source || '') + '\',\'' + escHtml(i.id) + '\')" style="color:var(--blue);cursor:pointer;font-size:var(--text-sm);padding:1px 5px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:3px" title="Re-open: set to updated so engine re-dispatches on existing branch">re-open</span>'
307
307
  : '';
308
308
 
309
309
  // P-79b47b0c — replace the bare prd-item-id span with a renderArtifactLink
@@ -339,9 +339,9 @@ function renderPrdProgress(prog) {
339
339
  const childAgent = c.dispatched_to ? (agentData.find(a => a.id === c.dispatched_to) || {}) : null;
340
340
  return '<div style="font-size:var(--text-sm);display:flex;align-items:center;gap:6px;color:var(--text);padding:2px 6px;background:var(--surface);border-radius:4px;border:1px solid var(--border)">' +
341
341
  statusBadge(c.status) +
342
- '<span style="color:var(--muted);font-size:var(--text-xs)">' + escHtml(c.id) + '</span>' +
342
+ '<span style="color:var(--muted);font-size:var(--text-sm)">' + escHtml(c.id) + '</span>' +
343
343
  '<span style="flex:1">' + escHtml((c.title || '').replace('Implement: ', '').slice(0, 60)) + '</span>' +
344
- (childAgent ? '<span style="font-size:var(--text-xs);color:var(--muted)">' + (childAgent.emoji || '') + ' ' + escHtml(childAgent.name || c.dispatched_to) + '</span>' : '') +
344
+ (childAgent ? '<span style="font-size:var(--text-sm);color:var(--muted)">' + (childAgent.emoji || '') + ' ' + escHtml(childAgent.name || c.dispatched_to) + '</span>' : '') +
345
345
  '</div>';
346
346
  }).join('') +
347
347
  '</div>';
@@ -404,24 +404,24 @@ function renderPrdProgress(prog) {
404
404
  // Hide regular action buttons when stale banner is showing — stale banner has its own actions
405
405
  const isStale = showStale;
406
406
  const pauseResumeBtn = isStale ? '' : isAwaitingApproval
407
- ? '<span onclick="event.stopPropagation();planApprove(\'' + escHtml(g.file) + '\',this)" style="color:var(--green);cursor:pointer;font-size:var(--text-xs);padding:1px 6px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">Approve</span>'
407
+ ? '<span onclick="event.stopPropagation();planApprove(\'' + escHtml(g.file) + '\',this)" style="color:var(--green);cursor:pointer;font-size:var(--text-sm);padding:1px 6px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">Approve</span>'
408
408
  : isPaused
409
- ? '<span onclick="event.stopPropagation();planApprove(\'' + escHtml(g.file) + '\',this)" style="color:var(--green);cursor:pointer;font-size:var(--text-xs);padding:1px 6px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">Resume</span>'
409
+ ? '<span onclick="event.stopPropagation();planApprove(\'' + escHtml(g.file) + '\',this)" style="color:var(--green);cursor:pointer;font-size:var(--text-sm);padding:1px 6px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">Resume</span>'
410
410
  : isCompleted && !(window._lastWorkItems || []).some(w => w.itemType === 'verify' && w.sourcePlan === g.file)
411
- ? '<span onclick="event.stopPropagation();triggerVerify(\'' + escHtml(g.file) + '\',this)" style="color:var(--green);cursor:pointer;font-size:var(--text-xs);padding:1px 6px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">Verify</span>'
412
- : isCompleted ? '' : '<span onclick="event.stopPropagation();planPause(\'' + escHtml(g.file) + '\',this)" style="color:var(--yellow);cursor:pointer;font-size:var(--text-xs);padding:1px 6px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.3);border-radius:3px">Pause</span>';
413
- const archiveBtn = (!isStale && (isCompleted || isPaused)) ? '<span onclick="event.stopPropagation();planArchive(\'' + escHtml(g.file) + '\',this)" style="color:var(--muted);cursor:pointer;font-size:var(--text-xs);padding:1px 6px;background:rgba(139,148,158,0.1);border:1px solid rgba(139,148,158,0.3);border-radius:3px">Archive</span>' : '';
414
- const deleteBtn = '<span onclick="event.stopPropagation();planDelete(\'' + escHtml(g.file) + '\')" style="color:var(--red);cursor:pointer;font-size:var(--text-xs);padding:1px 6px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.3);border-radius:3px">Delete</span>';
411
+ ? '<span onclick="event.stopPropagation();triggerVerify(\'' + escHtml(g.file) + '\',this)" style="color:var(--green);cursor:pointer;font-size:var(--text-sm);padding:1px 6px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">Verify</span>'
412
+ : isCompleted ? '' : '<span onclick="event.stopPropagation();planPause(\'' + escHtml(g.file) + '\',this)" style="color:var(--yellow);cursor:pointer;font-size:var(--text-sm);padding:1px 6px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.3);border-radius:3px">Pause</span>';
413
+ const archiveBtn = (!isStale && (isCompleted || isPaused)) ? '<span onclick="event.stopPropagation();planArchive(\'' + escHtml(g.file) + '\',this)" style="color:var(--muted);cursor:pointer;font-size:var(--text-sm);padding:1px 6px;background:rgba(139,148,158,0.1);border:1px solid rgba(139,148,158,0.3);border-radius:3px">Archive</span>' : '';
414
+ const deleteBtn = '<span onclick="event.stopPropagation();planDelete(\'' + escHtml(g.file) + '\')" style="color:var(--red);cursor:pointer;font-size:var(--text-sm);padding:1px 6px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.3);border-radius:3px">Delete</span>';
415
415
  const sourcePlanLink = g.sourcePlan
416
- ? '<span onclick="event.stopPropagation();planView(\'' + escHtml(g.sourcePlan) + '\')" style="color:var(--blue);cursor:pointer;font-size:var(--text-xs);padding:1px 6px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:3px" title="View source plan">&#x1F4C4; Plan</span>'
416
+ ? '<span onclick="event.stopPropagation();planView(\'' + escHtml(g.sourcePlan) + '\')" style="color:var(--blue);cursor:pointer;font-size:var(--text-sm);padding:1px 6px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:3px" title="View source plan">&#x1F4C4; Plan</span>'
417
417
  : '';
418
418
 
419
419
  return '<div style="font-size:var(--text-base);font-weight:600;color:var(--blue);margin-bottom:4px;padding:6px 8px;background:var(--surface2);border-radius:4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
420
420
  (g._projects.length > 0 ? g._projects.map(function(p) { return '<span class="prd-project-badge">' + escHtml(p) + '</span>'; }).join(' ') : '') +
421
421
  '<span style="color:var(--text)">' + escHtml(summary || g.file) + '</span>' +
422
422
  (g.branchStrategy === 'shared-branch'
423
- ? '<span style="font-size:var(--text-xs);padding:1px 5px;border-radius:3px;background:rgba(210,153,34,0.15);color:var(--yellow);font-weight:400" title="All items commit to a single shared feature branch">&#x1F333; shared branch</span>'
424
- : '<span style="font-size:var(--text-xs);padding:1px 5px;border-radius:3px;background:rgba(56,139,253,0.12);color:var(--blue);font-weight:400" title="Each item gets its own branch (work/P-xxx) and PR">&#x1F500; parallel branches</span>') +
423
+ ? '<span style="font-size:var(--text-sm);padding:1px 5px;border-radius:3px;background:rgba(210,153,34,0.15);color:var(--yellow);font-weight:400" title="All items commit to a single shared feature branch">&#x1F333; shared branch</span>'
424
+ : '<span style="font-size:var(--text-sm);padding:1px 5px;border-radius:3px;background:rgba(56,139,253,0.12);color:var(--blue);font-weight:400" title="Each item gets its own branch (work/P-xxx) and PR">&#x1F500; parallel branches</span>') +
425
425
  pausedLabel +
426
426
  staleLabel +
427
427
  '<span style="font-weight:700;font-size:var(--text-base);color:' + (done === g.items.length && g.items.length > 0 ? 'var(--green)' : 'var(--text)') + '">' + (g.items.length > 0 ? Math.round((done / g.items.length) * 100) : 0) + '%</span>' +
@@ -498,7 +498,7 @@ function renderPrdProgress(prog) {
498
498
  const col = columns[d] || [];
499
499
  const colLabel = d === 0 ? 'Root' : 'Wave ' + d;
500
500
  html += '<div style="flex:1;min-width:0">' +
501
- '<div style="font-size:var(--text-xs);color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;text-align:center">' + colLabel + '</div>';
501
+ '<div style="font-size:var(--text-sm);color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;text-align:center">' + colLabel + '</div>';
502
502
  for (const i of col) {
503
503
  const borderColor = statusColor(i.status, i.id);
504
504
  const src = escHtml(i.source || '');
@@ -513,7 +513,7 @@ function renderPrdProgress(prog) {
513
513
  const wiIdBadge = _renderPrdWorkItemIdBadge(wiForCard ? wiForCard.id : workItemId, i.id, '8px', '1px 4px');
514
514
  const isDoneCard = i.status === 'done' || (wiForCard && wiForCard.status === 'done');
515
515
  const reopenBtnGraph = isDoneCard
516
- ? '<span onclick="event.stopPropagation();prdItemReopen(\'' + src + '\',\'' + iid + '\')" style="font-size:var(--text-xs);color:var(--blue);cursor:pointer;padding:1px 4px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:3px" title="Re-open: set to updated so engine re-dispatches on existing branch">re-open</span>'
516
+ ? '<span onclick="event.stopPropagation();prdItemReopen(\'' + src + '\',\'' + iid + '\')" style="font-size:var(--text-sm);color:var(--blue);cursor:pointer;padding:1px 4px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:3px" title="Re-open: set to updated so engine re-dispatches on existing branch">re-open</span>'
517
517
  : '';
518
518
  const cardTitle = i.description ? escHtml(i.name + ' — ' + i.description) : escHtml(i.name || '');
519
519
  html += '<div onclick="prdItemEdit(\'' + src + '\',\'' + iid + '\')" ' +
@@ -528,34 +528,34 @@ function renderPrdProgress(prog) {
528
528
  '</div>' +
529
529
  '<div style="color:var(--text);font-size:var(--text-base);line-height:1.3;margin-bottom:3px;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical">' + escHtml(i.name) + '</div>' +
530
530
  '<div style="display:flex;gap:4px;flex-wrap:wrap;align-items:center">' +
531
- '<span class="prd-item-priority ' + (i.priority || '') + '" style="font-size:var(--text-xs);padding:1px 4px">' + escHtml(i.priority || '') + '</span>' +
532
- (agentDisplay ? '<span style="font-size:var(--text-xs);color:var(--muted)">' + agentDisplay + '</span>' : '') +
531
+ '<span class="prd-item-priority ' + (i.priority || '') + '" style="font-size:var(--text-sm);padding:1px 4px">' + escHtml(i.priority || '') + '</span>' +
532
+ (agentDisplay ? '<span style="font-size:var(--text-sm);color:var(--muted)">' + agentDisplay + '</span>' : '') +
533
533
  (function() {
534
534
  const w = wi[i.id];
535
535
  const rqId = w ? w.id : i.id;
536
536
  const rq = getPrdRequeueState(rqId);
537
537
  if (rq && rq.status === 'pending') {
538
- return '<span style="font-size:var(--text-xs);color:var(--yellow);cursor:wait;padding:1px 4px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.35);border-radius:3px">requeuing…</span>';
538
+ return '<span style="font-size:var(--text-sm);color:var(--yellow);cursor:wait;padding:1px 4px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.35);border-radius:3px">requeuing…</span>';
539
539
  }
540
540
  if (rq && rq.status === 'queued') {
541
- return '<span style="font-size:var(--text-xs);color:var(--green);cursor:default;padding:1px 4px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.35);border-radius:3px">requeued</span>';
541
+ return '<span style="font-size:var(--text-sm);color:var(--green);cursor:default;padding:1px 4px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.35);border-radius:3px">requeued</span>';
542
542
  }
543
543
  if (rq && rq.status === 'error') {
544
- return '<span style="font-size:var(--text-xs);color:var(--red);cursor:default;padding:1px 4px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.35);border-radius:3px" title="' + escHtml(rq.message || 'Retry failed') + '">failed</span>';
544
+ return '<span style="font-size:var(--text-sm);color:var(--red);cursor:default;padding:1px 4px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.35);border-radius:3px" title="' + escHtml(rq.message || 'Retry failed') + '">failed</span>';
545
545
  }
546
546
  // Show retry for failed items, or PRD items with no work item (orphaned/deleted)
547
547
  const canRetry = (w && i.status === 'failed') ||
548
548
  (!w && i.status && i.status !== 'missing' && i.status !== 'done' && i.status !== 'planned');
549
549
  if (!canRetry) return '';
550
- return '<span onclick="event.stopPropagation();prdItemRequeue(\'' + escHtml(rqId) + '\',\'' + escHtml(w ? (w._source || '') : '') + '\',\'' + escHtml(i.source || '') + '\')" style="font-size:var(--text-xs);color:var(--green);cursor:pointer;padding:1px 4px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">retry</span>';
550
+ return '<span onclick="event.stopPropagation();prdItemRequeue(\'' + escHtml(rqId) + '\',\'' + escHtml(w ? (w._source || '') : '') + '\',\'' + escHtml(i.source || '') + '\')" style="font-size:var(--text-sm);color:var(--green);cursor:pointer;padding:1px 4px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">retry</span>';
551
551
  })() +
552
552
  reopenBtnGraph +
553
- (deps ? '<span style="font-size:var(--text-xs);color:var(--muted)" title="Depends on: ' + escHtml(deps) + '">deps: ' + escHtml(deps) + '</span>' : '') +
553
+ (deps ? '<span style="font-size:var(--text-sm);color:var(--muted)" title="Depends on: ' + escHtml(deps) + '">deps: ' + escHtml(deps) + '</span>' : '') +
554
554
  (function() {
555
555
  var w = wi[i.id];
556
556
  var b = w ? (w.branch || w.featureBranch || (w._artifacts && w._artifacts.branch) || '') : '';
557
557
  if (!b) return '';
558
- return '<span style="font-size:var(--text-xs);color:var(--muted);font-family:monospace;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;vertical-align:middle" title="Branch: ' + escHtml(b) + '">&#x1F33F; ' + escHtml(b) + '</span>';
558
+ return '<span style="font-size:var(--text-sm);color:var(--muted);font-family:monospace;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;vertical-align:middle" title="Branch: ' + escHtml(b) + '">&#x1F33F; ' + escHtml(b) + '</span>';
559
559
  })() +
560
560
  '</div>' +
561
561
  ((i.prs || []).length ? '<div style="margin-top:3px">' + (i.prs || []).map(function(pr) { return _renderPrLink(pr, { size: '9px' }); }).join(' ') + '</div>' : '') +
@@ -565,10 +565,10 @@ function renderPrdProgress(prog) {
565
565
  return '<div style="margin-top:4px;border-top:1px solid var(--border);padding-top:4px">' +
566
566
  children.map(function(c) {
567
567
  var cAgent = c.dispatched_to ? (agentData.find(function(a) { return a.id === c.dispatched_to; }) || {}) : null;
568
- return '<div style="font-size:var(--text-xs);display:flex;align-items:center;gap:4px;padding:1px 0">' +
568
+ return '<div style="font-size:var(--text-sm);display:flex;align-items:center;gap:4px;padding:1px 0">' +
569
569
  statusBadge(c.status) +
570
570
  '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml((c.title || '').replace('Implement: ', '').slice(0, 40)) + '</span>' +
571
- (cAgent ? '<span style="font-size:var(--text-xs);color:var(--muted)">' + (cAgent.emoji || '') + '</span>' : '') +
571
+ (cAgent ? '<span style="font-size:var(--text-sm);color:var(--muted)">' + (cAgent.emoji || '') + '</span>' : '') +
572
572
  '</div>';
573
573
  }).join('') +
574
574
  '</div>';
@@ -588,8 +588,8 @@ function renderPrdProgress(prog) {
588
588
  const renderViewToggle = () => {
589
589
  const isGraph = window._prdViewMode === 'graph';
590
590
  return '<div style="display:flex;gap:4px;margin-bottom:8px;padding:0 8px">' +
591
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;' + (isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._prdViewMode=\'graph\';rerenderPrdFromCache()">Graph</button>' +
592
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;' + (!isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._prdViewMode=\'list\';rerenderPrdFromCache()">List</button>' +
591
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;' + (isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._prdViewMode=\'graph\';rerenderPrdFromCache()">Graph</button>' +
592
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;' + (!isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._prdViewMode=\'list\';rerenderPrdFromCache()">List</button>' +
593
593
  '</div>';
594
594
  };
595
595
 
@@ -654,18 +654,18 @@ function renderPrdProgress(prog) {
654
654
  ? renderArtifactLink({ type: 'pr', id: pr.id, label: pr.id, title: pr.title || pr.id })
655
655
  : '<code>' + escHtml(pr.id) + '</code>';
656
656
  return '<div style="display:flex;align-items:center;gap:6px;padding:2px 0;font-size:var(--text-base);opacity:' + rowOpacity + '">' +
657
- '<span style="color:' + statusColor + ';font-size:var(--text-xs);font-weight:600;padding:1px 4px;border:1px solid;border-radius:3px">' + escHtml(pr.status || 'active') + '</span>' +
657
+ '<span style="color:' + statusColor + ';font-size:var(--text-sm);font-weight:600;padding:1px 4px;border:1px solid;border-radius:3px">' + escHtml(pr.status || 'active') + '</span>' +
658
658
  prChip +
659
659
  '<span style="color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(pr.title || '') + '</span>' +
660
- '<span style="color:var(--muted);font-size:var(--text-xs)">' + escHtml(pr._project || '') + '</span>' +
660
+ '<span style="color:var(--muted);font-size:var(--text-sm)">' + escHtml(pr._project || '') + '</span>' +
661
661
  '</div>';
662
662
  }).join('');
663
663
  }
664
664
  if (guide) {
665
665
  html += '<div style="display:flex;align-items:center;gap:6px;padding:' + (prs.length ? '4px' : '0') + ' 0 0;font-size:var(--text-base)">' +
666
- '<span style="font-size:var(--text-xs);color:var(--green);font-weight:600;padding:1px 4px;border:1px solid var(--green);border-radius:3px">GUIDE</span>' +
666
+ '<span style="font-size:var(--text-sm);color:var(--green);font-weight:600;padding:1px 4px;border:1px solid var(--green);border-radius:3px">GUIDE</span>' +
667
667
  '<span onclick="openVerifyGuide(\'' + escHtml(guide.file) + '\')" style="color:var(--blue);cursor:pointer;text-decoration:underline;font-weight:500">Manual Testing Guide</span>' +
668
- '<span style="color:var(--muted);font-size:var(--text-xs)">Build instructions, test steps, known issues</span>' +
668
+ '<span style="color:var(--muted);font-size:var(--text-sm)">Build instructions, test steps, known issues</span>' +
669
669
  '</div>';
670
670
  }
671
671
  html += '</div>';
@@ -766,11 +766,11 @@ function showArchivedPrdDetail(idxOrFile) {
766
766
  // View mode toggle for archived
767
767
  const isGraph = window._archivedPrdViewMode !== 'list';
768
768
  const toggleHtml = '<div style="display:flex;gap:4px;margin-bottom:8px">' +
769
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;' + (isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'graph\';showArchivedPrdDetail(\'' + escHtml(g.file) + '\')">Graph</button>' +
770
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;' + (!isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'list\';showArchivedPrdDetail(\'' + escHtml(g.file) + '\')">List</button>' +
771
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--green);margin-left:auto" onclick="triggerVerify(\'' + escHtml(g.file) + '\')">Trigger Verify</button>' +
772
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" onclick="planUnarchive(\'' + escHtml(g.file) + '\',this)">Unarchive</button>' +
773
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" onclick="openArchivedPrdModal()">Back</button>' +
769
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;' + (isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'graph\';showArchivedPrdDetail(\'' + escHtml(g.file) + '\')">Graph</button>' +
770
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;' + (!isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'list\';showArchivedPrdDetail(\'' + escHtml(g.file) + '\')">List</button>' +
771
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--green);margin-left:auto" onclick="triggerVerify(\'' + escHtml(g.file) + '\')">Trigger Verify</button>' +
772
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px" onclick="planUnarchive(\'' + escHtml(g.file) + '\',this)">Unarchive</button>' +
773
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px" onclick="openArchivedPrdModal()">Back</button>' +
774
774
  '</div>';
775
775
 
776
776
  // Render directly from the archived group data (not from stale renderGroup closure)
@@ -1072,7 +1072,7 @@ function openArchive(i) {
1072
1072
  html += '<div class="archive-feature">' +
1073
1073
  '<span class="feat-id">' + escHtml(f.id) + '</span> ' +
1074
1074
  '<span class="prd-item-priority ' + pClass + '">' + escHtml(f.priority || '') + '</span>' +
1075
- (f.status ? ' <span class="pr-badge ' + (f.status === 'done' ? 'approved' : 'draft') + '" style="font-size:var(--text-xs)">' + escHtml(f.status) + '</span>' : '') +
1075
+ (f.status ? ' <span class="pr-badge ' + (f.status === 'done' ? 'approved' : 'draft') + '" style="font-size:var(--text-sm)">' + escHtml(f.status) + '</span>' : '') +
1076
1076
  '<div class="feat-name">' + escHtml(f.name) + '</div>' +
1077
1077
  '<div class="feat-desc">' + escHtml(f.description || '') + '</div>' +
1078
1078
  (f.rationale ? '<div class="feat-desc" style="margin-top:4px;color:var(--yellow)">Rationale: ' + escHtml(f.rationale) + '</div>' : '') +
@@ -20,66 +20,10 @@ const PR_FILTER_ALL_LABELS = {
20
20
  build: 'All build statuses',
21
21
  };
22
22
 
23
- // Fetch each project's pull-requests.json straight off disk through the
24
- // /state/projects/<name>/pull-requests.json static-file passthrough. This
25
- // is the issue #2949 escape hatch the engine writes PR status / review
26
- // state / build state directly into pull-requests.json, and the mtime-ETag
27
- // on /state/ guarantees a fresh read on the next 4 s poll regardless of
28
- // whether the /api/status outer cache has busted.
29
- //
30
- // Engine-side enrichment is minimal: stamping `_project` from the project
31
- // name (the filename context) and back-filling `url` from `prUrlBase` when
32
- // missing. Both are applied here to keep parity with the legacy slice.
33
- // Other engine-derived fields (build status from CI poll, reviewStatus,
34
- // _buildStatusStale) are PERSISTED to disk by the engine's PR pollers, so
35
- // they survive the round-trip naturally.
36
- async function fetchPullRequestsFromDisk(projects) {
37
- if (!Array.isArray(projects) || !projects.length) return null;
38
- const results = await Promise.all(projects.map(async (p) => {
39
- const name = p && (p.name || p.path);
40
- if (!name) return [];
41
- const urlBase = (p && p.prUrlBase) || '';
42
- try {
43
- const r = await fetch('/state/projects/' + encodeURIComponent(name) + '/pull-requests.json');
44
- if (!r.ok) return [];
45
- const arr = await r.json();
46
- if (!Array.isArray(arr)) return [];
47
- return arr.map((pr) => {
48
- if (!pr || typeof pr !== 'object' || !pr.id) return null;
49
- const stamped = Object.assign({ _project: name }, pr);
50
- if (!stamped.url && urlBase && typeof stamped.number === 'number') {
51
- stamped.url = urlBase + stamped.number;
52
- }
53
- return stamped;
54
- }).filter(Boolean);
55
- } catch {
56
- return [];
57
- }
58
- }));
59
- const out = [];
60
- const seen = new Set();
61
- for (const arr of results) {
62
- for (const pr of arr) {
63
- if (seen.has(pr.id)) continue;
64
- seen.add(pr.id);
65
- out.push(pr);
66
- }
67
- }
68
- // Same sort the engine applies (engine/queries.js#getPullRequests): group
69
- // terminal (merged/abandoned/closed) PRs to the bottom, then most recent
70
- // created first within each group, falling back to id.
71
- const _isTerminalPr = (pr) => pr.status === 'merged' || pr.status === 'abandoned' || pr.status === 'closed';
72
- out.sort((a, b) => {
73
- const at = _isTerminalPr(a) ? 1 : 0;
74
- const bt = _isTerminalPr(b) ? 1 : 0;
75
- if (at !== bt) return at - bt;
76
- const ac = a.created || '';
77
- const bc = b.created || '';
78
- if (ac && bc) return bc.localeCompare(ac);
79
- return (b.id || '').localeCompare(a.id || '');
80
- });
81
- return out;
82
- }
23
+ // Pull requests are served fresh by GET /api/pull-requests (see refresh.js),
24
+ // which runs queries.getPullRequests() server-side. The per-project
25
+ // pull-requests.json mirrors it used to read were retired by the SQL-only
26
+ // cutover.
83
27
 
84
28
  // Terminal statuses — follow-up WIs in these states are historical, not active.
85
29
  var _WI_TERMINAL = new Set(['done', 'cancelled', 'failed', 'decomposed', 'in-pr', 'implemented', 'complete']);
@@ -185,11 +129,11 @@ function prRow(pr) {
185
129
  const prId = pr.id || '—';
186
130
  const pendingReason = pr._pendingReason ? String(pr._pendingReason) : '';
187
131
  const pendingReasonHtml = pendingReason
188
- ? '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:2px" title="Pending reason: ' + escapeHtml(pendingReason) + '">' + escapeHtml(pendingReason.replace(/_/g, ' ')) + '</div>'
132
+ ? '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:2px" title="Pending reason: ' + escapeHtml(pendingReason) + '">' + escapeHtml(pendingReason.replace(/_/g, ' ')) + '</div>'
189
133
  : '';
190
134
  var followupCount = _countPrFollowups(pr);
191
135
  var followupChip = followupCount > 0
192
- ? ' <span class="pr-badge draft" style="font-size:var(--text-xs)" title="' + followupCount + ' active follow-up work item(s) in progress from comments on this PR">+' + followupCount + ' follow-up' + (followupCount === 1 ? '' : 's') + '</span>'
136
+ ? ' <span class="pr-badge draft" style="font-size:var(--text-sm)" title="' + followupCount + ' active follow-up work item(s) in progress from comments on this PR">+' + followupCount + ' follow-up' + (followupCount === 1 ? '' : 's') + '</span>'
193
137
  : '';
194
138
  // Issue #2969 — paused-cause chip(s). Prefer the API-enriched _pausedCauses
195
139
  // field; fall back to deriving from _noOpFixes for the /state/ disk-fetch
@@ -210,7 +154,7 @@ function prRow(pr) {
210
154
  + (rec.reason || 'unknown') + '. Click to resume (clears _noOpFixes['
211
155
  + cause + '] so the next discovery pass can re-dispatch).';
212
156
  return ' <button class="pr-badge rejected pr-paused-chip" '
213
- + 'style="font-size:var(--text-xs);cursor:pointer;border:none" '
157
+ + 'style="font-size:var(--text-sm);cursor:pointer;border:none" '
214
158
  + 'title="' + escapeHtml(titleAttr) + '" '
215
159
  + 'data-pr-id="' + escapeHtml(String(pr.id || '')) + '" '
216
160
  + 'data-pr-cause="' + escapeHtml(cause) + '" '
@@ -243,7 +187,7 @@ function prRow(pr) {
243
187
  + (buildFixIneffective.reason || 'unknown')
244
188
  + '. A new push to this branch will reset this pause automatically, or click to resume now.';
245
189
  pausedChips += ' <button class="pr-badge rejected pr-build-fix-ineffective-chip" '
246
- + 'style="font-size:var(--text-xs);cursor:pointer;border:none" '
190
+ + 'style="font-size:var(--text-sm);cursor:pointer;border:none" '
247
191
  + 'title="' + escapeHtml(bfiTitle) + '" '
248
192
  + 'data-pr-id="' + escapeHtml(String(pr.id || '')) + '" '
249
193
  + 'data-pr-head-sha="' + escapeHtml(String(buildFixIneffective.headSha || '')) + '" '
@@ -264,7 +208,7 @@ function prRow(pr) {
264
208
  var whpTitle = (worktreeHeldPaused.reason || 'Branch is held externally; PR automation paused')
265
209
  + '. Auto-clears once the holder releases the branch, or click to resume now.';
266
210
  pausedChips += ' <button class="pr-badge rejected pr-worktree-held-chip" '
267
- + 'style="font-size:var(--text-xs);cursor:pointer;border:none" '
211
+ + 'style="font-size:var(--text-sm);cursor:pointer;border:none" '
268
212
  + 'title="' + escapeHtml(whpTitle) + '" '
269
213
  + 'data-pr-id="' + escapeHtml(String(pr.id || '')) + '" '
270
214
  + 'onclick="event.stopPropagation();resumeWorktreeHeldPause(this)">'
@@ -240,8 +240,8 @@ const _SLOT_COLORS = {
240
240
  function _renderViewToggle() {
241
241
  const isList = _schedViewMode === 'list';
242
242
  return '<div style="display:flex;gap:4px;margin-bottom:8px">' +
243
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;' + (isList ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="_schedSetView(\'list\')">List</button>' +
244
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;' + (!isList ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="_schedSetView(\'calendar\')">Calendar</button>' +
243
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;' + (isList ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="_schedSetView(\'list\')">List</button>' +
244
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;' + (!isList ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="_schedSetView(\'calendar\')">Calendar</button>' +
245
245
  '</div>';
246
246
  }
247
247
 
@@ -299,7 +299,7 @@ function _renderScheduleCalendar(schedules) {
299
299
  html += '<div class="sched-cal-slot" style="background:' + colors.bg + ';border-left-color:' + colors.border + ';color:' + colors.text + ';opacity:' + opacity + '" ' +
300
300
  'onclick="if(shouldIgnoreSelectionClick(event))return;openScheduleDetail(\'' + escHtml(s.id) + '\')" title="' + escHtml(s.title + ' — ' + timeLabel + ' — ' + (s.type || 'implement')) + '">' +
301
301
  '<span style="font-weight:600;' + strikeStyle + '">' + escHtml((s.title || s.id).slice(0, 25)) + '</span>' +
302
- '<span style="font-size:var(--text-xs);opacity:0.7"> ' + timeLabel + '</span>' +
302
+ '<span style="font-size:var(--text-sm);opacity:0.7"> ' + timeLabel + '</span>' +
303
303
  '</div>';
304
304
  }
305
305
  if (cellSlots.length === 0) html += '&nbsp;';
@@ -353,8 +353,8 @@ function renderSchedules(schedules, opts) {
353
353
  '<td><span class="pr-date">' + escHtml(lastRun) + '</span></td>' +
354
354
  '<td style="white-space:nowrap">' +
355
355
  '<button class="btn-action" style="margin-right:4px" onclick="event.stopPropagation();runScheduleNow(\'' + escHtml(s.id) + '\',this)" title="Run now">Run now</button>' +
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
- '<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>' +
356
+ '<button class="pr-pager-btn' + (_schedToggleInFlight.has(s.id) ? ' disabled' : '') + '" style="font-size:var(--text-sm);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
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);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
358
  '<button class="btn-destructive" onclick="event.stopPropagation();deleteSchedule(\'' + escHtml(s.id) + '\')" title="Delete">&#x2715;</button>' +
359
359
  '</td>' +
360
360
  '</tr>';