@yemi33/minions 0.1.2423 → 0.1.2425

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 (184) 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 +6 -4
  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/internal-install.md +190 -0
  55. package/docs/kb-pr3223-cascade-archiving.md +16 -0
  56. package/docs/kb-pr696-merge-conflict-docs.md +23 -0
  57. package/docs/kb-sweep.md +2 -2
  58. package/docs/keep-processes.md +8 -1
  59. package/docs/live-checkout-mode.md +12 -7
  60. package/docs/managed-spawn.md +21 -3
  61. package/docs/named-agents.md +3 -2
  62. package/docs/plan-lifecycle.md +32 -32
  63. package/docs/pr-auto-fix-dispatch.md +3 -3
  64. package/docs/pr-review-fix-loop.md +6 -6
  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/03-plan-driven-feature.md +3 -3
  82. package/docs/tutorials/04-runtimes-and-harness.md +18 -21
  83. package/docs/tutorials/08-operations-and-recovery.md +3 -2
  84. package/docs/tutorials/README.md +4 -3
  85. package/docs/visual-evidence-ci.md +103 -0
  86. package/docs/watches.md +35 -30
  87. package/docs/worktree-lifecycle.md +151 -13
  88. package/engine/acp-transport.js +1 -0
  89. package/engine/ado.js +80 -33
  90. package/engine/agent-api-validation.js +571 -0
  91. package/engine/agent-worker-pool.js +7 -6
  92. package/engine/api-contracts/agent-content.js +761 -0
  93. package/engine/api-contracts/cc-ops.js +219 -0
  94. package/engine/api-contracts/config-runtime.js +419 -0
  95. package/engine/api-contracts/core.js +138 -0
  96. package/engine/api-contracts/index.js +506 -0
  97. package/engine/api-contracts/orchestration.js +602 -0
  98. package/engine/api-contracts/pull-requests.js +323 -0
  99. package/engine/api-contracts/qa-process.js +764 -0
  100. package/engine/api-contracts/work-plan-prd.js +494 -0
  101. package/engine/api-validation.js +543 -0
  102. package/engine/cc-api-validation.js +586 -0
  103. package/engine/cc-worker-pool.js +9 -1
  104. package/engine/cleanup.js +393 -271
  105. package/engine/cli.js +65 -7
  106. package/engine/comment-classifier.js +70 -5
  107. package/engine/consolidation.js +123 -5
  108. package/engine/content-api-validation.js +606 -0
  109. package/engine/create-pr-worktree.js +22 -1
  110. package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
  111. package/engine/dispatch.js +33 -0
  112. package/engine/distribution.js +189 -0
  113. package/engine/execution-model.js +59 -0
  114. package/engine/features.js +12 -0
  115. package/engine/github.js +67 -25
  116. package/engine/harness-context.js +313 -0
  117. package/engine/inbox-store.js +1 -1
  118. package/engine/kb-sweep.js +161 -38
  119. package/engine/keep-process-sweep.js +43 -3
  120. package/engine/lifecycle.js +1066 -165
  121. package/engine/llm.js +4 -0
  122. package/engine/managed-spawn.js +463 -48
  123. package/engine/meeting.js +197 -34
  124. package/engine/memory-retrieval.js +280 -0
  125. package/engine/memory-store.js +821 -38
  126. package/engine/model-discovery.js +17 -3
  127. package/engine/pipeline.js +802 -14
  128. package/engine/plan-prd-validation.js +745 -0
  129. package/engine/playbook.js +206 -17
  130. package/engine/pooled-agent-process.js +6 -1
  131. package/engine/pr-action.js +1 -1
  132. package/engine/pr-issue-validation.js +653 -0
  133. package/engine/pr-resolve.js +75 -13
  134. package/engine/prd-store.js +39 -6
  135. package/engine/preflight.js +7 -2
  136. package/engine/process-utils.js +79 -31
  137. package/engine/projects.js +198 -12
  138. package/engine/promotion.js +371 -0
  139. package/engine/qa-from-prd.js +3 -0
  140. package/engine/qa-process-validation.js +572 -0
  141. package/engine/qa-runbooks.js +41 -37
  142. package/engine/qa-runners/maestro.js +11 -7
  143. package/engine/qa-runners/playwright.js +37 -14
  144. package/engine/qa-runners.js +9 -10
  145. package/engine/qa-runs.js +124 -11
  146. package/engine/qa-sessions.js +139 -40
  147. package/engine/quarantine-refs.js +81 -11
  148. package/engine/queries.js +163 -9
  149. package/engine/review-learning-backfill.js +482 -0
  150. package/engine/review-learning.js +1236 -0
  151. package/engine/runtimes/claude.js +60 -2
  152. package/engine/runtimes/codex.js +57 -16
  153. package/engine/runtimes/contract.js +21 -0
  154. package/engine/runtimes/copilot.js +51 -3
  155. package/engine/runtimes/index.js +1 -0
  156. package/engine/scheduler.js +201 -32
  157. package/engine/settings-validation.js +909 -0
  158. package/engine/shared.js +705 -208
  159. package/engine/spawn-agent.js +48 -21
  160. package/engine/steering-constraints.js +31 -0
  161. package/engine/steering-store.js +10 -2
  162. package/engine/steering.js +14 -2
  163. package/engine/timeout.js +404 -111
  164. package/engine/untrusted-fence.js +24 -10
  165. package/engine/watch-actions.js +7 -2
  166. package/engine/watches.js +353 -51
  167. package/engine/work-item-validation.js +561 -0
  168. package/engine/work-items-store.js +61 -1
  169. package/engine/worktree-gc.js +656 -37
  170. package/engine/worktree-preflight.js +154 -0
  171. package/engine.js +2068 -342
  172. package/package.json +1 -1
  173. package/playbooks/_pr-description-audit.md +88 -41
  174. package/playbooks/build-fix-complex.md +3 -3
  175. package/playbooks/docs.md +2 -2
  176. package/playbooks/fix.md +18 -18
  177. package/playbooks/implement.md +18 -18
  178. package/playbooks/qa-session-draft.md +10 -12
  179. package/playbooks/qa-session-execute.md +16 -12
  180. package/playbooks/qa-validate.md +8 -7
  181. package/playbooks/shared-rules.md +45 -0
  182. package/playbooks/test.md +4 -0
  183. package/prompts/cc-system.md +1 -1
  184. package/skills/check-self-authored-review-comment/SKILL.md +2 -2
@@ -63,7 +63,7 @@ function renderSkills(skills) {
63
63
  let html = '<div style="display:flex;gap:4px;margin-bottom:8px;flex-wrap:wrap">';
64
64
  for (const t of tabs) {
65
65
  const active = _skillsTab === t.key;
66
- html += '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;' +
66
+ html += '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;' +
67
67
  (active ? 'background:var(--green);color:#fff;border-color:var(--green)' : '') +
68
68
  '" onclick="_skillsSelectTab(' + _jsArg(t.key) + ')">' + escHtml(t.label) + '</button>';
69
69
  }
@@ -73,7 +73,7 @@ function renderSkills(skills) {
73
73
  // native locations (~/.claude/skills, ~/.copilot/skills, plugin skills) are
74
74
  // only visible to that runtime. The "agent" tab (~/.agents/skills) is the
75
75
  // cross-runtime portable bucket and IS visible to every runtime.
76
- html += '<div style="font-size:var(--text-xs);font-style:italic;color:var(--muted);margin-bottom:8px;line-height:1.4">' +
76
+ html += '<div style="font-size:var(--text-sm);font-style:italic;color:var(--muted);margin-bottom:8px;line-height:1.4">' +
77
77
  'Skills are reference docs agents read on demand — they are not injected wholesale into prompts. ' +
78
78
  'Each tab reflects what the matching runtime would see; runtime-native skills are NOT cross-visible. ' +
79
79
  'The agent tab (~/.agents/skills) is the cross-runtime portable bucket — visible to every runtime.' +
@@ -90,10 +90,10 @@ function renderSkills(skills) {
90
90
  html += '<div style="display:flex;flex-direction:column;gap:6px">';
91
91
  for (const r of page) {
92
92
  const meta = _skillMetaOf(r.source);
93
- const autoTag = r.autoGenerated ? '<span style="font-size:var(--text-xs);background:rgba(63,185,80,0.15);color:var(--green);padding:1px 4px;border-radius:3px;margin-left:4px">auto</span>' : '';
93
+ const autoTag = r.autoGenerated ? '<span style="font-size:var(--text-sm);background:rgba(63,185,80,0.15);color:var(--green);padding:1px 4px;border-radius:3px;margin-left:4px">auto</span>' : '';
94
94
  html += '<div class="inbox-item" onclick="openSkill(' + _jsArg(r.file) + ',' + _jsArg(r.source || 'claude-code') + ',' + _jsArg(r.dir || '') + ')" style="border-left-color:var(--green)">' +
95
95
  '<div class="inbox-name"><span style="color:var(--green);font-weight:700">' + meta.icon + ' ' + escHtml(r.name) + '</span>' + autoTag +
96
- '<span style="font-size:var(--text-xs);color:var(--muted);margin-left:auto">' + escHtml(meta.label) + '</span>' +
96
+ '<span style="font-size:var(--text-sm);color:var(--muted);margin-left:auto">' + escHtml(meta.label) + '</span>' +
97
97
  '</div>' +
98
98
  (r.description ? '<div class="inbox-preview" style="font-size:var(--text-sm);color:var(--text)">' + escHtml(r.description) + '</div>' : '') +
99
99
  '</div>';
@@ -103,11 +103,11 @@ function renderSkills(skills) {
103
103
  // Pagination controls
104
104
  if (totalPages > 1) {
105
105
  html += '<div style="display:flex;align-items:center;gap:8px;margin-top:6px;justify-content:center">';
106
- html += '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" ' +
106
+ html += '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px" ' +
107
107
  (_skillsPage === 0 ? 'disabled' : '') +
108
108
  ' onclick="_skillsPage--;renderSkills(window._lastSkills)">&laquo;</button>';
109
109
  html += '<span style="font-size:var(--text-sm);color:var(--muted)">' + (_skillsPage + 1) + ' / ' + totalPages + '</span>';
110
- html += '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" ' +
110
+ html += '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px" ' +
111
111
  (_skillsPage >= totalPages - 1 ? 'disabled' : '') +
112
112
  ' onclick="_skillsPage++;renderSkills(window._lastSkills)">&raquo;</button>';
113
113
  html += '</div>';
@@ -260,7 +260,7 @@ function _renderJsonObj(obj, state) {
260
260
  if (!text || text.length <= 10) return '';
261
261
  var truncated = text.length > 3000;
262
262
  var displayText = truncated ? text.slice(0, 3000) + '...' : text;
263
- return '<div style="background:var(--surface);border-left:2px solid var(--border);padding:2px 8px;margin:0 0 2px 16px;font-size:var(--text-xs);font-family:monospace;color:var(--muted);max-height:160px;overflow-y:auto;white-space:pre-wrap;cursor:pointer" onclick="this.style.maxHeight=this.style.maxHeight===\'160px\'?\'none\':\'160px\'">' + escHtml(displayText) + '</div>';
263
+ return '<div style="background:var(--surface);border-left:2px solid var(--border);padding:2px 8px;margin:0 0 2px 16px;font-size:var(--text-sm);font-family:monospace;color:var(--muted);max-height:160px;overflow-y:auto;white-space:pre-wrap;cursor:pointer" onclick="this.style.maxHeight=this.style.maxHeight===\'160px\'?\'none\':\'160px\'">' + escHtml(displayText) + '</div>';
264
264
  }
265
265
  function toolKey(name, input) {
266
266
  try { return String(name || 'tool') + '|' + JSON.stringify(input || {}); }
@@ -406,7 +406,7 @@ function renderAgentOutput(text) {
406
406
  var msg = trimmed.replace('[human-steering] ', '');
407
407
  fragments.push('<div style="align-self:flex-end;background:var(--blue);color:#fff;padding:6px 12px;border-radius:12px 12px 2px 12px;max-width:80%;margin:4px 0;font-size:var(--text-md)">' +
408
408
  '<span style="margin-right:4px">❯</span>' + escHtml(msg) +
409
- '<div style="font-size:var(--text-xs);opacity:0.7;margin-top:2px">\u2713 Queued</div></div>');
409
+ '<div style="font-size:var(--text-sm);opacity:0.7;margin-top:2px">\u2713 Queued</div></div>');
410
410
  continue;
411
411
  }
412
412
 
@@ -444,7 +444,7 @@ function renderAgentOutput(text) {
444
444
 
445
445
  // Stderr
446
446
  if (trimmed.startsWith('[stderr]')) {
447
- fragments.push('<div style="font-size:var(--text-xs);color:var(--red);font-family:monospace;padding:1px 4px">' + escHtml(trimmed) + '</div>');
447
+ fragments.push('<div style="font-size:var(--text-sm);color:var(--red);font-family:monospace;padding:1px 4px">' + escHtml(trimmed) + '</div>');
448
448
  } else {
449
449
  // Plain text fallback
450
450
  fragments.push('<div style="font-size:var(--text-sm);color:var(--muted);font-family:monospace;padding:1px 4px">' + escHtml(trimmed) + '</div>');
@@ -550,7 +550,7 @@ function renderPager(opts) {
550
550
  function pinButton(pinKey, pinned, source, opts) {
551
551
  var extraStyle = (opts && opts.extraStyle) || '';
552
552
  return '<button class="pr-pager-btn pin-btn' + (pinned ? ' pinned' : '') +
553
- '" style="font-size:var(--text-xs);padding:2px 8px;' + extraStyle +
553
+ '" style="font-size:var(--text-sm);padding:2px 8px;' + extraStyle +
554
554
  '" data-pin-key="' + escHtml(pinKey) +
555
555
  '" onclick="event.stopPropagation();_togglePinAndRefresh(this.dataset.pinKey,\'' + source + '\')">' +
556
556
  (pinned ? 'Unpin' : 'Pin') + '</button>';
@@ -1,20 +1,8 @@
1
1
  // render-watches.js — Watch rendering and CRUD functions for the dashboard
2
2
 
3
- // Pull watches.json straight off disk through /state/engine/watches.json
4
- // instead of waiting on the /api/status outer cache (issue #2949). The
5
- // engine writes watch status / _history transitions directly into
6
- // watches.json, so mtime+size ETag surfaces them on the next 4 s poll.
7
- // Falls back to data.watches when /state/ returns null.
8
- async function fetchWatchesFromDisk() {
9
- try {
10
- const r = await fetch('/state/engine/watches.json');
11
- if (!r.ok) return null;
12
- const arr = await r.json();
13
- return Array.isArray(arr) ? arr : null;
14
- } catch {
15
- return null;
16
- }
17
- }
3
+ // Watch loading lives in dashboard/shared/watches-source.js
4
+ // (MinionsWatchesSource) so the classic and Slim surfaces resolve watches from
5
+ // the same SQL-backed endpoint. See refresh.js for the polling call site.
18
6
 
19
7
  // ─── Helpers ────────────────────────────────────────────────────────────────
20
8
 
@@ -256,7 +244,7 @@ function renderWatches(watchesData, opts) {
256
244
  // Pause/Resume button (F7: marks as `disabled` while POST in flight)
257
245
  var pauseBusyCls = _watchToggleInFlight.has(w.id) ? ' disabled' : '';
258
246
  if (w.status === 'active') {
259
- 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>';
247
+ html += '<button class="pr-pager-btn' + pauseBusyCls + '" style="font-size:var(--text-sm);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>';
260
248
  } else if (w.status === 'paused') {
261
249
  html += '<button class="btn-action' + pauseBusyCls + '" style="margin-right:4px" onclick="event.stopPropagation();toggleWatchPause(\'' + escHtml(w.id) + '\',false)" title="Resume">&#x25B6;</button>';
262
250
  }
@@ -4,28 +4,6 @@ let allWorkItems = [];
4
4
  let wiPage = 0;
5
5
  const WI_PER_PAGE = 20;
6
6
 
7
- // Engine-enrichment field names that live on the /api/status workItems slice
8
- // but are NOT on disk in projects/<name>/work-items.json. The /state/ fetch
9
- // recovers raw disk state — these get overlaid from the most recent
10
- // /api/status snapshot (window._lastWorkItems) so the table still renders the
11
- // PR chip, pending-reason badge, blockedBy hint, etc. The disk fields win
12
- // for everything else, so a status flip from `pending` → `done` lands within
13
- // one /state poll regardless of how stale /api/status got.
14
- const _WI_ENRICHMENT_FIELDS = [
15
- '_pr', '_prUrl', '_pendingReason', '_skipReason', '_blockedBy',
16
- '_humanFeedback', '_reopened', '_managedSpawnPartial', '_securityFlag',
17
- '_artifacts', 'referencesCount', 'acceptanceCriteriaCount',
18
- // W-mq5xg5e9000nec0e — slim shape marker so the detail/edit modals know
19
- // to hydrate the full description via GET /api/work-items/<id> rather than
20
- // pre-filling textareas with the truncated snippet.
21
- '_descriptionTruncated',
22
- // W-mq08kuog001110a6 — _preDispatchEval IS persisted to disk by
23
- // engine/dispatch.js#_persistInvalidWorkItem, but the slim /api/status
24
- // overlay may drop it on later passes. Carry it across the overlay so
25
- // the Needs-Attention badge survives /state polls.
26
- '_preDispatchEval',
27
- ];
28
-
29
7
  // W-mq08kuog001110a6 — Needs-Attention badge for stuck pending work items.
30
8
  //
31
9
  // A pending WI is "needs attention" when the engine is intentionally refusing
@@ -40,10 +18,21 @@ const _WI_ENRICHMENT_FIELDS = [
40
18
  // Does NOT trigger for transient reasons that auto-clear:
41
19
  // * cooldown / retry_cooldown — wait it out
42
20
  // * already_dispatched — engine reconciles on next tick
21
+ // * agents_busy — an eligible agent exists but every one is currently busy;
22
+ // the engine auto-dispatches on a later tick when a slot frees. This is
23
+ // the transient sibling of no_agent (W-mrzlzgju0002e6f0): the engine
24
+ // classifies the two at the source via resolveAgentReservation, so the
25
+ // UI never has to guess whether a "no agent" pending state is actionable.
43
26
  // * branch_locked — wait for the holding dispatch
27
+ // * worktree_branch_held — configured checkout is re-checked each tick
44
28
  // * dependency_unmet — auto-clears when upstream dep finishes (engine discovery loop + DEPENDENCY_MET watch)
45
29
  // * live_checkout_busy — one live-mode dispatch at a time; clears when running dispatch finishes
46
30
  //
31
+ // no_agent (kept as a trigger) now means the engine found NO eligible agent
32
+ // that could EVER take this work (empty/over-budget agent roster or an
33
+ // unresolvable route) — a genuine operator-actionable state. Transient
34
+ // capacity exhaustion is emitted as agents_busy instead.
35
+ //
47
36
  // Returns { kind, short, full } when the item needs attention, else null.
48
37
  // Pure helper (no DOM / escapeHtml deps) so it can be unit-tested in Node.
49
38
  function needsAttentionInfo(item) {
@@ -75,7 +64,8 @@ function needsAttentionInfo(item) {
75
64
  return { kind: 'pr_not_found', short: prMsg, full: prMsg };
76
65
  }
77
66
  if (reason === 'no_agent') {
78
- var noAgentMsg = 'No agent assigned and routing.md has no match for type=' + (item.type || 'unknown') + '.';
67
+ var noAgentMsg = 'No eligible agent can take this work (type=' + (item.type || 'unknown') +
68
+ '). The agent roster is empty, over budget, or routing.md has no resolvable match — assign or configure an agent.';
79
69
  return { kind: 'no_agent', short: noAgentMsg, full: noAgentMsg };
80
70
  }
81
71
  if (reason === 'budget_exceeded') {
@@ -86,63 +76,27 @@ function needsAttentionInfo(item) {
86
76
  return null;
87
77
  }
88
78
 
89
- // Pull each project's work-items.json straight off disk through
90
- // /state/projects/<name>/work-items.json. The endpoint is a static-file
91
- // passthrough with mtime+size ETag — there is no server-side cache in the
92
- // way, so an engine write to `status: 'done'` is visible to the next poll
93
- // (issue #2949 root fix). Enrichment fields are overlaid from a cached
94
- // id→item map sourced from window._lastWorkItems (the last /api/status
95
- // snapshot). All fetches run in parallel; a single project's failure does
96
- // not block the others.
97
- async function fetchWorkItemsFromDisk(projects, cachedById) {
98
- if (!Array.isArray(projects) || !projects.length) return null;
99
- const results = await Promise.all(projects.map(async (p) => {
100
- const name = p && (p.name || p.path);
101
- if (!name) return [];
102
- try {
103
- const r = await fetch('/state/projects/' + encodeURIComponent(name) + '/work-items.json');
104
- if (!r.ok) return [];
105
- const arr = await r.json();
106
- if (!Array.isArray(arr)) return [];
107
- return arr.map((item) => {
108
- if (!item || typeof item !== 'object') return null;
109
- const stamped = Object.assign({}, item, { _source: name });
110
- const cached = cachedById && cachedById.get(item.id);
111
- if (cached) {
112
- for (const k of _WI_ENRICHMENT_FIELDS) {
113
- if (stamped[k] === undefined && cached[k] !== undefined) {
114
- stamped[k] = cached[k];
115
- }
116
- }
117
- }
118
- // referencesCount / acceptanceCriteriaCount don't live on disk;
119
- // derive from the raw arrays if present so the table icons render
120
- // even before the cached snapshot catches up.
121
- if (stamped.referencesCount == null && Array.isArray(stamped.references)) {
122
- stamped.referencesCount = stamped.references.length;
123
- }
124
- if (stamped.acceptanceCriteriaCount == null && Array.isArray(stamped.acceptanceCriteria)) {
125
- stamped.acceptanceCriteriaCount = stamped.acceptanceCriteria.length;
126
- }
127
- return stamped;
128
- }).filter(Boolean);
129
- } catch {
130
- return [];
131
- }
132
- }));
133
- // Flatten + dedupe by id (a project rename mid-flight could double-list).
134
- const out = [];
135
- const seen = new Set();
136
- for (const arr of results) {
137
- for (const item of arr) {
138
- if (!item.id || seen.has(item.id)) continue;
139
- seen.add(item.id);
140
- out.push(item);
141
- }
142
- }
143
- return out;
79
+ function formatPendingReason(item, detailed) {
80
+ var reason = item && item._pendingReason ? String(item._pendingReason) : '';
81
+ if (!reason) return '';
82
+ if (reason === 'already_dispatched') return 'Queued waiting for available agent slot';
83
+ if (reason === 'agents_busy') return 'Waiting for an available agent — all eligible agents are busy';
84
+ if (reason !== 'worktree_branch_held') return reason.replace(/_/g, ' ');
85
+ if (!detailed) return 'operator checkout holds branch';
86
+
87
+ var diagnostic = item._worktreeBranchHeld || {};
88
+ var branch = diagnostic.branch || item.branch || '(unknown branch)';
89
+ var holderPath = diagnostic.holderPath || '(unknown checkout path)';
90
+ return 'Branch ' + branch + ' is checked out by configured operator checkout ' + holderPath +
91
+ '. The work item remains pending and will dispatch automatically once that checkout releases the branch.';
144
92
  }
145
93
 
94
+ // Work items are served fresh by GET /api/work-items (see refresh.js), which
95
+ // re-runs queries.getWorkItems() server-side so _pr/_prUrl/_pendingReason/
96
+ // _preDispatchEval/_descriptionTruncated are always current. The per-project
97
+ // work-items.json mirrors this file used to read were retired by the SQL-only
98
+ // cutover, and the client-side enrichment overlay they fed is gone with them.
99
+
146
100
  // Track retry state per work item so loading/success/error survives re-renders
147
101
  const _wiRetryState = {}; // { [id]: { status: 'pending'|'done'|'error', message?, until? } }
148
102
  function setWiRetryState(id, state) { _wiRetryState[id] = state; }
@@ -156,15 +110,15 @@ function getWiRetryState(id) {
156
110
  function wiRetryBtn(item) {
157
111
  const rs = getWiRetryState(item.id);
158
112
  if (rs && rs.status === 'pending') {
159
- return '<span style="font-size:var(--text-xs);padding:1px 6px;color:var(--yellow);border:1px solid rgba(210,153,34,0.35);background:rgba(210,153,34,0.1);border-radius:3px;cursor:wait;margin-left:4px">Retrying\u2026</span>';
113
+ return '<span style="font-size:var(--text-sm);padding:1px 6px;color:var(--yellow);border:1px solid rgba(210,153,34,0.35);background:rgba(210,153,34,0.1);border-radius:3px;cursor:wait;margin-left:4px">Retrying\u2026</span>';
160
114
  }
161
115
  if (rs && rs.status === 'done') {
162
- return '<span style="font-size:var(--text-xs);padding:1px 6px;color:var(--green);border:1px solid rgba(63,185,80,0.35);background:rgba(63,185,80,0.1);border-radius:3px;margin-left:4px">Requeued</span>';
116
+ return '<span style="font-size:var(--text-sm);padding:1px 6px;color:var(--green);border:1px solid rgba(63,185,80,0.35);background:rgba(63,185,80,0.1);border-radius:3px;margin-left:4px">Requeued</span>';
163
117
  }
164
118
  if (rs && rs.status === 'error') {
165
- return '<span style="font-size:var(--text-xs);padding:1px 6px;color:var(--red);border:1px solid rgba(248,81,73,0.35);background:rgba(248,81,73,0.1);border-radius:3px;margin-left:4px;cursor:pointer" title="' + escapeHtml(rs.message || 'Retry failed') + ' — click to try again" onclick="event.stopPropagation();retryWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')">Retry failed</span>';
119
+ return '<span style="font-size:var(--text-sm);padding:1px 6px;color:var(--red);border:1px solid rgba(248,81,73,0.35);background:rgba(248,81,73,0.1);border-radius:3px;margin-left:4px;cursor:pointer" title="' + escapeHtml(rs.message || 'Retry failed') + ' — click to try again" onclick="event.stopPropagation();retryWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')">Retry failed</span>';
166
120
  }
167
- return '<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="event.stopPropagation();retryWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')">Retry</button>';
121
+ return '<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="event.stopPropagation();retryWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')">Retry</button>';
168
122
  }
169
123
 
170
124
  function wiRow(item) {
@@ -177,13 +131,13 @@ function wiRow(item) {
177
131
  const prLink = item._pr
178
132
  ? '<a class="pr-title" href="' + escapeHtml(item._prUrl || '#') + '" target="_blank" style="font-size:var(--text-sm)">' + escapeHtml(item._pr) + '</a>'
179
133
  : (item.branchStrategy === 'shared-branch' && item.status === 'done')
180
- ? '<span style="font-size:var(--text-xs);color:var(--muted)" title="Part of shared branch — aggregate PR created at verify stage">shared branch</span>'
181
- : '<span style="color:var(--muted)">—</span>';
134
+ ? '<span style="font-size:var(--text-sm);color:var(--muted)" title="Part of shared branch — aggregate PR created at verify stage">shared branch</span>'
135
+ : (item.failReason ? '' : '<span style="color:var(--muted)">—</span>');
182
136
  // W-mqrdn6qq / W-mrkqc0pl — ALL failure/error snippets (retryable AND
183
137
  // non-retryable) surface their red failReason snippet in the PR column,
184
138
  // never the Agent column.
185
139
  var failSnippet = item.failReason
186
- ? '<span style="display:block;font-size:var(--text-xs);color:var(--red)" title="' + escapeHtml(item.failReason) + '">' + escapeHtml(item.failReason.slice(0, 30)) + '</span>'
140
+ ? '<span style="display:block;font-size:var(--text-sm);color:var(--red)" title="' + escapeHtml(item.failReason) + '">' + escapeHtml(item.failReason.slice(0, 30)) + '</span>'
187
141
  : '';
188
142
  // PR follow-up chip (W-mpej3cox00099466) — surfaced when the WI was spun
189
143
  // off from a PR comment via meta.pr_followup. Links to the parent PR.
@@ -193,16 +147,16 @@ function wiRow(item) {
193
147
  var prRef = prFollowup.parent_pr_id || prFollowup.parent_pr_url;
194
148
  var prNumMatch = String(prRef).match(/(\d+)(?!.*\d)/);
195
149
  var prLabel = prNumMatch ? ('PR #' + prNumMatch[1]) : 'parent PR';
196
- followupChip = ' <a class="pr-badge draft" style="font-size:var(--text-xs);text-decoration:none" target="_blank" rel="noopener" href="' + escapeHtml(prFollowup.parent_pr_url) + '" title="Follow-up dispatched from ' + escapeHtml(prRef) + (prFollowup.parent_comment_author ? ' by ' + escapeHtml(prFollowup.parent_comment_author) : '') + '" onclick="event.stopPropagation()">&#8617; from ' + escapeHtml(prLabel) + '</a>';
150
+ followupChip = ' <a class="pr-badge draft" style="font-size:var(--text-sm);text-decoration:none" target="_blank" rel="noopener" href="' + escapeHtml(prFollowup.parent_pr_url) + '" title="Follow-up dispatched from ' + escapeHtml(prRef) + (prFollowup.parent_comment_author ? ' by ' + escapeHtml(prFollowup.parent_comment_author) : '') + '" onclick="event.stopPropagation()">&#8617; from ' + escapeHtml(prLabel) + '</a>';
197
151
  }
198
152
  return '<tr data-wi-id="' + escapeHtml(item.id) + '" style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openWorkItemDetail(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')">' +
199
153
  '<td style="min-width:280px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml((item.title || '').slice(0, 200)) + '">' + escapeHtml(item.title || '') + followupChip + '</td>' +
200
154
  '<td><span style="font-size:var(--text-sm);color:var(--muted)">' + escapeHtml(item._source || '') + '</span>' +
201
- (item.scope === 'fan-out' ? ' <span class="pr-badge ' + (item.status === 'done' || item.status === 'failed' ? 'draft' : 'building') + '" style="font-size:var(--text-xs)">fan-out</span>' : '') + '</td>' +
155
+ (item.scope === 'fan-out' ? ' <span class="pr-badge ' + (item.status === 'done' || item.status === 'failed' ? 'draft' : 'building') + '" style="font-size:var(--text-sm)">fan-out</span>' : '') + '</td>' +
202
156
  '<td>' + typeBadge(item.type) + '</td>' +
203
157
  '<td>' + priBadge(item.priority) + '</td>' +
204
158
  '<td>' + statusBadge(item.status || 'pending') +
205
- (item._reopened ? ' <span style="font-size:var(--text-xs);color:var(--purple);margin-left:4px" title="This item was reopened from a previously completed state">reopened</span>' : '') +
159
+ (item._reopened ? ' <span style="font-size:var(--text-sm);color:var(--purple);margin-left:4px" title="This item was reopened from a previously completed state">reopened</span>' : '') +
206
160
  (function() {
207
161
  // W-mq08kuog001110a6 — Needs-Attention badge for stuck pending WIs.
208
162
  // Click anywhere on the row to open the modal where the full reason
@@ -210,12 +164,12 @@ function wiRow(item) {
210
164
  var info = needsAttentionInfo(item);
211
165
  if (!info) return '';
212
166
  var tooltip = (info.short || '').replace(/\s+/g, ' ').slice(0, 120);
213
- return ' <span class="pr-badge needs-attention" style="font-size:var(--text-xs);margin-left:4px" title="' + escapeHtml(tooltip) + ' — click row for full reason">&#x26A0; Needs attention</span>';
167
+ return ' <span class="pr-badge needs-attention" style="font-size:var(--text-sm);margin-left:4px" title="' + escapeHtml(tooltip) + ' — click row for full reason">&#x26A0; Needs attention</span>';
214
168
  })() +
215
- (item._pendingReason && item.status === 'pending' && item._pendingReason !== 'already_dispatched' ? ' <span style="font-size:var(--text-xs);color:var(--muted);margin-left:4px" title="Pending reason: ' + escapeHtml(item._pendingReason) + '">' + escapeHtml(item._pendingReason.replace(/_/g, ' ')) + '</span>' : '') +
216
- (item._pendingReason === 'already_dispatched' && item.status === 'pending' ? ' <span style="font-size:var(--text-xs);color:var(--blue);margin-left:4px" title="In dispatch queue, waiting to be assigned">queued</span>' : '') +
169
+ (item._pendingReason && item.status === 'pending' && item._pendingReason !== 'already_dispatched' ? ' <span style="font-size:var(--text-sm);color:var(--muted);margin-left:4px" title="Pending reason: ' + escapeHtml(formatPendingReason(item, true)) + '">' + escapeHtml(formatPendingReason(item, false)) + '</span>' : '') +
170
+ (item._pendingReason === 'already_dispatched' && item.status === 'pending' ? ' <span style="font-size:var(--text-sm);color:var(--blue);margin-left:4px" title="In dispatch queue, waiting to be assigned">queued</span>' : '') +
217
171
  (item._managedSpawnPartial && Array.isArray(item._managedSpawnPartial.failed) && item._managedSpawnPartial.failed.length
218
- ? ' <span style="font-size:var(--text-xs);color:var(--yellow);margin-left:4px;border:1px solid var(--yellow);padding:0 4px;border-radius:6px" title="managed-spawn: '
172
+ ? ' <span style="font-size:var(--text-sm);color:var(--yellow);margin-left:4px;border:1px solid var(--yellow);padding:0 4px;border-radius:6px" title="managed-spawn: '
219
173
  + escapeHtml(((item._managedSpawnPartial.healthy || []).length) + '/' + (((item._managedSpawnPartial.healthy || []).length) + item._managedSpawnPartial.failed.length))
220
174
  + ' healthy — failed: '
221
175
  + escapeHtml(item._managedSpawnPartial.failed.map(function(f){ return (f && f.name) || '?'; }).join(', '))
@@ -223,7 +177,7 @@ function wiRow(item) {
223
177
  + escapeHtml(((item._managedSpawnPartial.healthy || []).length) + '/' + (((item._managedSpawnPartial.healthy || []).length) + item._managedSpawnPartial.failed.length))
224
178
  + ' healthy</span>'
225
179
  : '') +
226
- (item._skipReason && item.status === 'pending' && item._skipReason !== 'live_checkout_busy' ? ' <span style="font-size:var(--text-xs);color:var(--yellow);margin-left:4px" title="Dispatch blocked: ' + escapeHtml(item._skipReason) + (item._blockedBy ? ' (by ' + escapeHtml(item._blockedBy) + ')' : '') + '">' + escapeHtml(item._skipReason.replace(/_/g, ' ')) + (item._blockedBy ? ' <span style="color:var(--muted)">(' + escapeHtml(item._blockedBy) + ')</span>' : '') + '</span>' : '') +
180
+ (item._skipReason && item.status === 'pending' && item._skipReason !== 'live_checkout_busy' ? ' <span style="font-size:var(--text-sm);color:var(--yellow);margin-left:4px" title="Dispatch blocked: ' + escapeHtml(item._skipReason) + (item._blockedBy ? ' (by ' + escapeHtml(item._blockedBy) + ')' : '') + '">' + escapeHtml(item._skipReason.replace(/_/g, ' ')) + (item._blockedBy ? ' <span style="color:var(--muted)">(' + escapeHtml(item._blockedBy) + ')</span>' : '') + '</span>' : '') +
227
181
  (item.status === 'failed' ? ' ' + wiRetryBtn(item) : '') +
228
182
  '</td>' +
229
183
  '<td>' +
@@ -233,7 +187,7 @@ function wiRow(item) {
233
187
  '</td>' +
234
188
  '<td>' + prLink + (failSnippet ? failSnippet : '') + '</td>' +
235
189
  '<td><span class="pr-date">' + escapeHtml((item.created || '').slice(0, 16).replace('T', ' ')) + '</span></td>' +
236
- '<td style="white-space:nowrap;font-size:var(--text-xs);color:var(--muted)">' +
190
+ '<td style="white-space:nowrap;font-size:var(--text-sm);color:var(--muted)">' +
237
191
  (function() {
238
192
  // /api/status slims the full arrays into integer counts
239
193
  // (dashboard.js:1796-1797: referencesCount / acceptanceCriteriaCount).
@@ -249,15 +203,15 @@ function wiRow(item) {
249
203
  (acCount ? '<span title="' + acCount + ' acceptance criteria">&#x2611;' + acCount + '</span>' : '');
250
204
  })() +
251
205
  '</td>' +
252
- '<td style="white-space:nowrap">' +
206
+ '<td style="white-space:nowrap;text-align:right">' +
253
207
  // W-mqqa76pi — "View live output" opens the agent slide-in panel (live-output
254
208
  // tab auto-selected when the agent is working). Only for genuinely active
255
209
  // dispatches with a resolvable assigned agent id.
256
- ((item.status === 'dispatched' && (item.dispatched_to || item.agent)) ? '<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();openArtifact(\'agent\',\'' + escapeHtml(item.dispatched_to || item.agent) + '\')" title="View live output">&#x1F4FA;</button>' : '') +
257
- ((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>' : '') +
258
- ((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>' : '') +
259
- ((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> ' : '')) +
260
- ((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>' : '') +
210
+ ((item.status === 'dispatched' && (item.dispatched_to || item.agent)) ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();openArtifact(\'agent\',\'' + escapeHtml(item.dispatched_to || item.agent) + '\')" title="View live output">&#x1F4FA;</button>' : '') +
211
+ ((item.status === 'pending' || item.status === 'failed') ? '<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();editWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Edit work item">&#x270E;</button>' : '') +
212
+ ((item.status === 'done' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:1px 6px;color:var(--muted);border-color:var(--muted);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Archive work item">&#x1F4E6;</button>' : '') +
213
+ ((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-sm)" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '&#x1F44D;' : '&#x1F44E;') + '</span> ' : '')) +
214
+ ((item.status === 'pending' || item.status === 'dispatched' || item.status === 'queued' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);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>' : '') +
261
215
  '<button class="btn-destructive" onclick="event.stopPropagation();deleteWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Delete work item and kill agent">&#x2715;</button>' +
262
216
  '</td>' +
263
217
  '</tr>';
@@ -705,6 +659,24 @@ function _wiDeriveCanonicalPrIdFromUrl(url) {
705
659
  return null;
706
660
  }
707
661
 
662
+ // W-ms0ytfdd000d5a26 — provenance hint for the WI detail modal's Model field.
663
+ // Explains WHERE the shown model came from so an operator can tell a recorded
664
+ // fact from a projection. Mirrors the `source` values produced by
665
+ // engine/execution-model.js#resolveWorkItemModel.
666
+ function _wiModelSourceHint(m) {
667
+ var runtime = m.runtime || 'runtime';
668
+ var hint;
669
+ if (m.source === 'runtime-reported') hint = 'reported by ' + runtime;
670
+ else if (m.source === 'requested') hint = 'sent to ' + runtime + ' at dispatch';
671
+ else if (m.source === 'runtime-default') hint = 'no model pinned';
672
+ else if (m.source === 'configured') hint = 'pinned in current config';
673
+ else hint = 'no runtime recorded';
674
+ // Nothing was persisted for this item, so the value is what the NEXT dispatch
675
+ // would use — not a record of a past run.
676
+ if (!m.persisted) hint += ' · projected (no dispatch record)';
677
+ return hint;
678
+ }
679
+
708
680
  function _wiRenderDetail(item) {
709
681
  const field = (label, value) => value ? '<div style="margin-bottom:8px"><span style="color:var(--muted);font-size:var(--text-sm);text-transform:uppercase;letter-spacing:0.5px">' + label + '</span><div style="margin-top:2px">' + value + '</div></div>' : '';
710
682
  const badge = (cls, text) => '<span class="pr-badge ' + cls + '">' + escapeHtml(text) + '</span>';
@@ -713,14 +685,14 @@ function _wiRenderDetail(item) {
713
685
  let html = '<div style="display:flex;flex-direction:column;gap:4px;font-size:var(--text-lg)">';
714
686
  html += '<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px">' +
715
687
  badge(statusCls, item.status || 'pending') +
716
- (item._reopened ? ' <span style="font-size:var(--text-xs);color:var(--purple);margin-left:4px" title="This item was reopened from a previously completed state">reopened</span>' : '') + ' ' +
688
+ (item._reopened ? ' <span style="font-size:var(--text-sm);color:var(--purple);margin-left:4px" title="This item was reopened from a previously completed state">reopened</span>' : '') + ' ' +
717
689
  '<span class="dispatch-type ' + (item.type || 'implement') + '">' + escapeHtml(item.type || 'implement') + '</span>' +
718
690
  '<span class="prd-item-priority ' + (item.priority || '') + '">' + escapeHtml(item.priority || 'medium') + '</span>' +
719
691
  (function() {
720
692
  // W-mq08kuog001110a6 — Needs-Attention chip in the detail modal header.
721
693
  var info = needsAttentionInfo(item);
722
694
  if (!info) return '';
723
- return ' <span class="pr-badge needs-attention" style="font-size:var(--text-xs);margin-left:4px" title="See &quot;Why this is blocked&quot; below">&#x26A0; Needs attention</span>';
695
+ return ' <span class="pr-badge needs-attention" style="font-size:var(--text-sm);margin-left:4px" title="See &quot;Why this is blocked&quot; below">&#x26A0; Needs attention</span>';
724
696
  })() +
725
697
  '</div>';
726
698
  // W-mqqa76pi — Prominent "View live output" button for an actively-dispatched
@@ -764,6 +736,22 @@ function _wiRenderDetail(item) {
764
736
  }
765
737
  html += field('Description', '<div id="wi-detail-desc" style="font-size:var(--text-md);max-height:320px;overflow-y:auto;padding:8px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm)">' + _descHtml + '</div>');
766
738
  html += field('Agent', escapeHtml(item.dispatched_to || item.agent || 'Auto'));
739
+ // W-ms0ytfdd000d5a26 — the model this work item actually ran on. Resolved
740
+ // server-side (engine/queries.js#resolveWorkItemModelInfo) so persisted
741
+ // dispatch evidence — what the runtime reported, else what Minions requested
742
+ // — always beats current config. Items with no dispatch record show the
743
+ // projection from current config, marked as such. When nothing concrete is
744
+ // known we render "<runtime> default" rather than blanking or guessing.
745
+ (function() {
746
+ var m = item._model;
747
+ if (!m || typeof m !== 'object') return;
748
+ var label = m.label || m.model;
749
+ if (!label) return;
750
+ var hint = _wiModelSourceHint(m);
751
+ html += field('Model',
752
+ '<code style="font-size:var(--text-sm);background:var(--surface2);padding:2px 6px;border-radius:var(--radius-sm)">' + escapeHtml(String(label)) + '</code>' +
753
+ (hint ? ' <span style="color:var(--muted);font-size:var(--text-sm)">' + escapeHtml(hint) + '</span>' : ''));
754
+ })();
767
755
  html += field('Project', escapeHtml(item._source || 'central'));
768
756
  // P-714ef144 — surface meta.workdir in the detail modal so operators can
769
757
  // confirm the agent's actual cwd. Renders nothing when unset (which is
@@ -791,7 +779,7 @@ function _wiRenderDetail(item) {
791
779
  (item.cancelledAt ? '<div style="margin-top:2px;color:var(--muted);font-size:var(--text-sm)">' + escapeHtml(formatLocalDateTime(item.cancelledAt)) + '</div>' : '');
792
780
  html += field('Cancellation Reason', _cancelBody);
793
781
  }
794
- if (item._pendingReason && item.status === 'pending') html += field('Pending Reason', item._pendingReason === 'already_dispatched' ? 'Queued — waiting for available agent slot' : escapeHtml(item._pendingReason.replace(/_/g, ' ')));
782
+ if (item._pendingReason && item.status === 'pending') html += field('Pending Reason', escapeHtml(formatPendingReason(item, true)));
795
783
  if (item._managedSpawnPartial && Array.isArray(item._managedSpawnPartial.failed) && item._managedSpawnPartial.failed.length) {
796
784
  var _msp = item._managedSpawnPartial;
797
785
  var _mspHealthy = Array.isArray(_msp.healthy) ? _msp.healthy : [];
@@ -899,8 +887,8 @@ function _wiRenderDetail(item) {
899
887
  // Artifacts — branch (non-navigation) + navigation chips routed through
900
888
  // renderArtifactLink so chip click → openArtifact() → modal-stack push +
901
889
  // URL hash + browser-Back integration come for free (P-e265cd31).
902
- // NOTE (issue #603): item._source is the WI's *project name* (stamped by
903
- // fetchWorkItemsFromDisk above / used by every action button to route
890
+ // NOTE (issue #603): item._source is the WI's *project name* (stamped
891
+ // server-side by queries.getWorkItems / used by every action button to route
904
892
  // retry/cancel/delete/etc.), NOT a plan/PRD file id. A prior "Source" chip
905
893
  // here treated item._source as if it held a genuine source-plan/PRD
906
894
  // filename, which rendered a bogus chip labeled with the project name on
@@ -1189,9 +1177,9 @@ function _wiInjectSourceChipFromFrontmatter(content, containerId) {
1189
1177
  }
1190
1178
 
1191
1179
  if (typeof window !== 'undefined') {
1192
- window.MinionsWork = { wiRow, renderWorkItems, editWorkItem, submitWorkItemEdit, deleteWorkItem, archiveWorkItem, toggleWorkItemArchive, retryWorkItem, wiPrev, wiNext, feedbackWorkItem, submitFeedback, openCreateWorkItemModal, openWorkItemDetail, openAllWorkItems, viewAgentOutput, openInboxNote, needsAttentionInfo };
1180
+ window.MinionsWork = { wiRow, renderWorkItems, editWorkItem, submitWorkItemEdit, deleteWorkItem, archiveWorkItem, toggleWorkItemArchive, retryWorkItem, wiPrev, wiNext, feedbackWorkItem, openCreateWorkItemModal, openWorkItemDetail, openAllWorkItems, viewAgentOutput, openInboxNote, needsAttentionInfo };
1193
1181
  }
1194
1182
  // exported for testing — pure helpers safe to require under Node (no DOM deps).
1195
1183
  if (typeof module !== 'undefined' && module.exports) {
1196
- module.exports = { needsAttentionInfo };
1184
+ module.exports = { needsAttentionInfo, formatPendingReason };
1197
1185
  }