@yemi33/minions 0.1.2286 → 0.1.2288

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.
@@ -75,6 +75,31 @@ async function fetchNotesFromDisk() {
75
75
  }
76
76
  }
77
77
 
78
+ // Promote a human-readable title to the inbox card heading. Prefers an
79
+ // explicit `title` field from the API, then a `title:` YAML frontmatter key,
80
+ // then the first markdown heading, then the first non-empty body line. Mirrors
81
+ // the engine's own title derivation (engine/consolidation.js — first `# ` ATX
82
+ // heading). Returns '' when nothing readable is extractable so the caller can
83
+ // fall back to the raw filename.
84
+ function _extractInboxTitle(item) {
85
+ if (item && item.title && String(item.title).trim()) return String(item.title).trim();
86
+ const content = (item && item.content) || '';
87
+ let body = content;
88
+ const fm = content.match(/^\s*---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/);
89
+ if (fm) {
90
+ const titleField = fm[1].match(/^title:[ \t]*(.+)$/m);
91
+ if (titleField && titleField[1].trim()) {
92
+ return titleField[1].trim().replace(/^["']|["']$/g, '').trim();
93
+ }
94
+ body = content.slice(fm[0].length);
95
+ }
96
+ const heading = body.match(/^#{1,6}[ \t]+(.+)$/m);
97
+ if (heading && heading[1].trim()) return heading[1].trim();
98
+ const firstLine = body.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
99
+ if (firstLine) return firstLine.replace(/^#+[ \t]*/, '').trim();
100
+ return '';
101
+ }
102
+
78
103
  function _inboxPrev() { if (_inboxPage > 0) { _inboxPage--; renderInbox(inboxData, { preserveScroll: false }); } }
79
104
  function _inboxNext() { _inboxPage++; renderInbox(inboxData, { preserveScroll: false }); }
80
105
 
@@ -99,15 +124,19 @@ function renderInbox(inbox, opts) {
99
124
  const inboxStart = _inboxPage * INBOX_PER_PAGE;
100
125
  const pageInbox = inbox.slice(inboxStart, inboxStart + INBOX_PER_PAGE);
101
126
 
102
- // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: inbox name, age, content, pin key)
127
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: inbox name, derived title/heading, age, content, pin key)
103
128
  list.innerHTML = pageInbox.map((item, i) => {
104
129
  const idx = inboxStart + i;
105
130
  const pk = inboxPinKey(item.name);
106
131
  const pinned = isPinned(pk);
132
+ const title = _extractInboxTitle(item);
133
+ const hasTitle = !!title;
134
+ const heading = hasTitle ? title : item.name;
107
135
  return `<div class="inbox-item${pinned ? ' item-pinned' : ''}" data-file="notes/inbox/${escapeHtml(item.name)}">
108
- <div class="inbox-name" onclick="openModal(${idx})" style="cursor:pointer">
109
- <span>${escapeHtml(item.name)}</span><span>${escapeHtml(item.age || '')}</span>
136
+ <div class="inbox-name${hasTitle ? '' : ' inbox-name-fallback'}" onclick="openModal(${idx})" style="cursor:pointer">
137
+ <span>${escapeHtml(heading)}</span><span>${escapeHtml(item.age || '')}</span>
110
138
  </div>
139
+ ${hasTitle ? `<div class="inbox-filename">${escapeHtml(item.name)}</div>` : ''}
111
140
  <div class="inbox-preview" onclick="openModal(${idx})" style="cursor:pointer">${escapeHtml(item.content.slice(0,200))}</div>
112
141
  <div style="display:flex;gap:6px;margin-top:6px;align-items:center">
113
142
  ${pinButton(pk, pinned, 'inbox')}
@@ -265,7 +265,7 @@ function renderLlmPerf(metrics) {
265
265
  return '<tr><td style="font-weight:600">' + escHtml(type) + '</td>' +
266
266
  '<td>' + calls + '</td>' +
267
267
  '<td style="color:var(--muted)">' + (totalMs ? fmtTotal : '-') + '</td>' +
268
- '<td style="color:var(--blue)">' + (avgMs ? fmtAvg : '-') + '</td>' +
268
+ '<td style="color:var(--green)">' + (avgMs ? fmtAvg : '-') + '</td>' +
269
269
  '<td style="color:var(--muted)">' + cost + '</td></tr>';
270
270
  }
271
271
  let html = '<table class="pr-table"><thead><tr><th>Call Type</th><th>Calls</th><th>Total Time</th><th>Avg Time</th><th>Cost</th></tr></thead><tbody>';
@@ -306,7 +306,7 @@ function renderPlans(plans) {
306
306
  'completed': 'Completed', 'dispatched': 'In Progress', 'converting': 'Converting to PRD',
307
307
  'paused': 'Paused', 'awaiting-approval': 'Awaiting Approval', 'approved': 'Approved',
308
308
  'rejected': 'Rejected', 'revision-requested': 'Revision Requested',
309
- 'has-failures': 'Has Failures', 'active': 'Active'
309
+ 'has-failures': 'Has Failures', 'active': 'Active', 'draft': 'Draft'
310
310
  };
311
311
  const label = statusLabelsMap[effectiveStatus] || effectiveStatus;
312
312
  const needsAction = (effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused') && !isArchived;
@@ -366,7 +366,7 @@ function renderPlans(plans) {
366
366
  'onclick="event.stopPropagation();planDelete(\'' + escapeHtml(p.file) + '\')">Delete</button>' : '';
367
367
 
368
368
  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>' : '';
369
- const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'converting': 'var(--yellow)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--green)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)' };
369
+ const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'converting': 'var(--yellow)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--green)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)', 'draft': 'var(--muted)' };
370
370
  const cardClass = effectiveStatus === 'dispatched' || effectiveStatus === 'converting' ? 'working' : effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused' ? 'awaiting' : effectiveStatus;
371
371
  // P-e8d49105 — cross-repo plans surface every touched project as its
372
372
  // own badge. Single-project plans (and old PRDs without _projects)
@@ -402,7 +402,7 @@ function renderPlans(plans) {
402
402
  '<div class="plan-card-header">' +
403
403
  '<div><div class="plan-card-title">' + escapeHtml(p.summary || p.file) + versionBadge + '</div>' +
404
404
  '<div class="plan-card-meta">' +
405
- '<span style="font-weight:600;color:' + (statusColors[effectiveStatus] || 'var(--muted)') + '">' + label + '</span>' +
405
+ '<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>' +
406
406
  projectMeta +
407
407
  '<span>' + p.itemCount + ' items</span>' +
408
408
  perProjectPills +
@@ -93,6 +93,44 @@ function _parseCanonicalPrId(id) {
93
93
  return { host: m[1], slug: m[2], number: parseInt(m[3], 10) };
94
94
  }
95
95
 
96
+ // Polish (W-mqv5ccn7): collapse the canonical PR id (`<host>:<slug>#<number>`)
97
+ // into a short, readable label for the narrow PR column — `gh #481`,
98
+ // `ado #5383607` — instead of the full slug, which ellipsis-clipped mid-org
99
+ // (`ado:of…`, `github…`) and looked like an error. The full canonical id stays
100
+ // on the cell's title attr for hover. Unrecognized ids pass through unchanged.
101
+ function prShortId(id) {
102
+ if (!id || typeof id !== 'string') return id || '—';
103
+ var m = id.match(/^([^:]+):.+#(\d+)$/);
104
+ if (!m) return id;
105
+ var host = m[1].toLowerCase();
106
+ var label = host === 'github' ? 'gh' : host === 'ado' ? 'ado' : host;
107
+ return label + ' #' + m[2];
108
+ }
109
+
110
+ // Polish (W-mqv5ccn7): strip common Markdown syntax so the PR body preview
111
+ // shows plain text instead of raw source (`## Summary`, `**bold**`, backticks,
112
+ // links, …). This is a display-only flatten — not a renderer — so headings,
113
+ // emphasis, code spans, list markers, blockquotes, links and images all reduce
114
+ // to their visible text on a single collapsed line.
115
+ function stripMarkdown(text) {
116
+ if (!text || typeof text !== 'string') return '';
117
+ return text
118
+ .replace(/```[\s\S]*?```/g, ' ') // fenced code blocks
119
+ .replace(/`([^`]+)`/g, '$1') // inline code
120
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, ' ') // images
121
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') // links -> link text
122
+ .replace(/^\s{0,3}#{1,6}\s+/gm, '') // ATX headings
123
+ .replace(/^\s{0,3}>\s?/gm, '') // blockquotes
124
+ .replace(/^\s*([-*_]\s*){3,}$/gm, ' ') // horizontal rules
125
+ .replace(/^\s*[-*+]\s+/gm, '') // bullet list markers
126
+ .replace(/^\s*\d+\.\s+/gm, '') // ordered list markers
127
+ .replace(/(\*\*|__)(.*?)\1/g, '$2') // bold
128
+ .replace(/(\*|_)(.*?)\1/g, '$2') // italic
129
+ .replace(/~~(.*?)~~/g, '$1') // strikethrough
130
+ .replace(/\s+/g, ' ') // collapse whitespace/newlines
131
+ .trim();
132
+ }
133
+
96
134
  function prRow(pr) {
97
135
  // Minions review (agent) state — separate from ADO human review
98
136
  const sq = pr.minionsReview || {};
@@ -150,6 +188,9 @@ function prRow(pr) {
150
188
  }).join('');
151
189
  }
152
190
  const titleText = pr.title || 'Untitled';
191
+ // Polish (W-mqv5ccn7): flatten Markdown in the body so the preview shows
192
+ // plain text instead of raw `##` / `**…**` / backtick source.
193
+ const descPreview = stripMarkdown(pr.description || '');
153
194
  const agentText = pr.agent || '—';
154
195
  const reviewerCell = sq.reviewer && sq.status !== 'waiting'
155
196
  ? '<span class="pr-agent" title="' + escapeHtml(sq.note || sq.reviewer) + '">' + escapeHtml(sq.reviewer) + '</span>'
@@ -200,8 +241,8 @@ function prRow(pr) {
200
241
  // the header-to-cell count assertion in test/unit.test.js continues to
201
242
  // balance.
202
243
  return '<tr>' +
203
- '<td><span class="pr-id" title="' + escapeHtml(String(prId)) + '">' + escapeHtml(String(prId)) + '</span></td>' +
204
- '<td><a class="pr-title" title="' + escapeHtml(titleText) + '" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(titleText) + '</a>' + followupChip + pausedChips + (pr.description ? '<div class="pr-desc" title="' + escapeHtml(pr.description) + '">' + escapeHtml(pr.description.length > 120 ? pr.description.slice(0, 120) + '...' : pr.description) + '</div>' : '') + '</td>' +
244
+ '<td><span class="pr-id" title="' + escapeHtml(String(prId)) + '">' + escapeHtml(prShortId(String(prId))) + '</span></td>' +
245
+ '<td><a class="pr-title" title="' + escapeHtml(titleText) + '" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(titleText) + '</a>' + followupChip + pausedChips + (descPreview ? '<div class="pr-desc" title="' + escapeHtml(descPreview) + '">' + escapeHtml(descPreview.length > 120 ? descPreview.slice(0, 120) + '...' : descPreview) + '</div>' : '') + '</td>' +
205
246
  '<td><span class="pr-agent" title="' + escapeHtml(agentText) + '">' + escapeHtml(agentText) + '</span></td>' +
206
247
  '<td><span class="' + branchClass + '" title="' + escapeHtml(branchError || branchLabel) + '">' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
207
248
  '<td><span class="pr-badge ' + reviewClass + '" title="' + escapeHtml(reviewTitle || reviewLabel) + '">' + escapeHtml(reviewLabel) + '</span></td>' +
@@ -216,12 +257,12 @@ function prRow(pr) {
216
257
  }
217
258
 
218
259
  // Explicit per-column widths keep the PR table from ballooning when titles or
219
- // branches are long. Total ≈1420px → table grows past viewport on narrow
260
+ // branches are long. Total ≈1455px → table grows past viewport on narrow
220
261
  // windows and the .pr-table-wrap--prs container scrolls horizontally inside
221
262
  // the viewport (sticky scrollbar — see styles.css).
222
263
  const PRS_COLGROUP =
223
264
  '<colgroup>' +
224
- '<col style="width:75px">' + // PR id
265
+ '<col style="width:110px">' + // PR id (short label: `gh #481` / `ado #5383607`)
225
266
  '<col style="width:320px">' + // Title
226
267
  '<col style="width:140px">' + // Agent
227
268
  '<col style="width:200px">' + // Branch
@@ -340,12 +340,12 @@ function renderSchedules(schedules, opts) {
340
340
  ? '<span class="pr-badge approved">enabled</span>'
341
341
  : '<span class="pr-badge rejected">disabled</span>';
342
342
  const lastRun = s._lastRun ? timeAgo(s._lastRun) : 'never';
343
- const typeBadge = '<span class="dispatch-type ' + escHtml(s.type || 'implement') + '">' + escHtml(s.type || 'implement') + '</span>';
343
+ const typeBadge = '<span class="pr-badge draft">' + escHtml(s.type || 'implement') + '</span>';
344
344
  const humanCron = _cronToHuman(s.cron || '');
345
345
  html += '<tr data-sched-id="' + escHtml(s.id || '') + '" style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openScheduleDetail(\'' + escHtml(s.id) + '\')">' +
346
346
  '<td><span class="pr-id">' + escHtml(s.id || '') + '</span></td>' +
347
347
  '<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(s.title || '') + '">' + escHtml(s.title || '') + '</td>' +
348
- '<td><span title="' + escHtml(s.cron || '') + '" style="font-size:var(--text-base);color:var(--blue)">' + escHtml(humanCron) + '</span></td>' +
348
+ '<td><span title="' + escHtml(s.cron || '') + '" style="font-size:var(--text-base);color:var(--muted)">' + escHtml(humanCron) + '</span></td>' +
349
349
  '<td>' + typeBadge + '</td>' +
350
350
  '<td><span style="font-size:var(--text-sm);color:var(--muted)">' + escHtml(s.project || '') + '</span></td>' +
351
351
  '<td><span class="pr-agent">' + escHtml(s.agent || 'auto') + '</span></td>' +
@@ -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);color:var(--muted);margin-bottom:8px;line-height:1.4">' +
76
+ html += '<div style="font-size:var(--text-xs);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.' +
@@ -92,10 +92,10 @@ function renderSkills(skills) {
92
92
  const meta = _skillMetaOf(r.source);
93
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>' : '';
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
- '<div class="inbox-name"><span style="color:var(--green);font-weight:600">' + meta.icon + ' ' + escHtml(r.name) + '</span>' + autoTag +
95
+ '<div class="inbox-name"><span style="color:var(--green);font-weight:700">' + meta.icon + ' ' + escHtml(r.name) + '</span>' + autoTag +
96
96
  '<span style="font-size:var(--text-xs);color:var(--muted);margin-left:auto">' + escHtml(meta.label) + '</span>' +
97
97
  '</div>' +
98
- (r.description ? '<div class="inbox-preview" style="color:var(--text)">' + escHtml(r.description) + '</div>' : '') +
98
+ (r.description ? '<div class="inbox-preview" style="font-size:var(--text-sm);color:var(--text)">' + escHtml(r.description) + '</div>' : '') +
99
99
  '</div>';
100
100
  }
101
101
  html += '</div>';
@@ -242,9 +242,9 @@ function renderWatches(watchesData, opts) {
242
242
 
243
243
  html += '<tr style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openWatchDetail(\'' + escHtml(w.id) + '\')">' +
244
244
  '<td><span class="pr-id">' + escHtml(w.id) + '</span></td>' +
245
- '<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(_watchTitleFallback(w)) + '">' + escHtml(_formatWatchTarget(w.target, w.targetType) || _watchTitleFallback(w)) + '</td>' +
245
+ '<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(_watchTitleFallback(w)) + '"><span class="pr-id">' + escHtml(_formatWatchTarget(w.target, w.targetType) || _watchTitleFallback(w)) + '</span></td>' +
246
246
  '<td><span class="dispatch-type explore">' + escHtml(targetLabel) + '</span></td>' +
247
- '<td><span style="font-size:var(--text-base);color:var(--blue)">' + escHtml(condLabel) + '</span></td>' +
247
+ '<td><span class="pr-badge" style="background:rgba(139,148,158,0.15);color:var(--muted);border-color:var(--muted)">' + escHtml(condLabel) + '</span></td>' +
248
248
  '<td><span style="font-size:var(--text-sm)">' + actionLabel + '</span></td>' +
249
249
  '<td><span style="font-size:var(--text-sm);color:var(--muted)">' + escHtml(_intervalToHuman(w.interval)) + '</span></td>' +
250
250
  '<td><span class="pr-agent">' + escHtml(w.owner || 'human') + '</span></td>' +
@@ -175,7 +175,9 @@ function wiRow(item) {
175
175
  // the PR column (not the Agent column). The engine prefixes failReason with
176
176
  // "Non-retryable failure:" and stamps item._failureClass on non-retryable
177
177
  // demotion (engine/dispatch.js writeDispatchResult / force-demote path).
178
- var isNonRetryableFail = !!item._failureClass || /^Non-retryable failure:/i.test(item.failReason || '');
178
+ var isNonRetryableFail = !!item._failureClass
179
+ || /^Non-retryable failure:/i.test(item.failReason || '')
180
+ || /^Project "[^"]+" not found\./i.test(item.failReason || '');
179
181
  var failSnippet = item.failReason
180
182
  ? '<span style="display:block;font-size:var(--text-xs);color:var(--red)" title="' + escapeHtml(item.failReason) + '">' + escapeHtml(item.failReason.slice(0, 30)) + '</span>'
181
183
  : '';
@@ -289,6 +289,32 @@ async function openSettings() {
289
289
  '⚠ Live mode: dispatches run directly in this repo\'s checkout. Only one mutating dispatch runs at a time. Dirty working trees block dispatch — commit or stash before running.' +
290
290
  '</div>' +
291
291
  '</div>';
292
+ // M006 — liveValidation section (per-project hybrid mode config).
293
+ // Controls: type (work item type that stays serialized in live checkout)
294
+ // and autoDispatch (auto-create validation WI after coding WI completes).
295
+ // Section is rendered with reduced opacity + pointer-events:none when
296
+ // checkoutMode is not 'live' — it only applies in hybrid mode.
297
+ var lvType = (p.liveValidation && p.liveValidation.type) ? p.liveValidation.type : '';
298
+ var lvAutoDispatch = !!(p.liveValidation && p.liveValidation.autoDispatch);
299
+ var lvDisabled = (currentWtMode !== 'live');
300
+ var lvSectionStyle = lvDisabled
301
+ ? 'opacity:0.45;pointer-events:none;margin-bottom:6px'
302
+ : 'margin-bottom:6px';
303
+ var liveValidationBlock =
304
+ '<div data-live-validation-section="' + escHtml(p.name) + '" data-search="live validation deferred build test auto dispatch worktree" style="' + lvSectionStyle + '">' +
305
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Live validation (deferred build/test)' +
306
+ (lvDisabled ? ' <span style="font-size:var(--text-xs);opacity:0.7">(requires Live checkout)</span>' : '') +
307
+ '</label>' +
308
+ '<div style="font-size:var(--text-xs);color:var(--muted);margin-bottom:4px;line-height:1.4">' +
309
+ 'For checkoutMode: live projects — coding agents run in worktrees; a separate dispatch validates in live checkout.' +
310
+ '</div>' +
311
+ '<input id="set-liveValidationType-' + escHtml(p.name) + '" value="' + escHtml(lvType) + '" placeholder="e.g. build-and-test" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md);margin-bottom:4px">' +
312
+ '<div style="font-size:var(--text-xs);color:var(--muted);margin-bottom:4px">Validation work item type (e.g. <code>build-and-test</code>, <code>test</code>, <code>verify</code>). Leave blank to disable.</div>' +
313
+ '<div style="display:flex;align-items:center;gap:8px;padding:2px 0">' +
314
+ '<input type="checkbox" id="set-liveValidationAutoDispatch-' + escHtml(p.name) + '"' + (lvAutoDispatch ? ' checked' : '') + ' style="accent-color:var(--blue);width:16px;height:16px;cursor:pointer">' +
315
+ '<label for="set-liveValidationAutoDispatch-' + escHtml(p.name) + '" style="font-size:var(--text-md);color:var(--text);cursor:pointer">Auto-dispatch validation WI after coding WI completes</label>' +
316
+ '</div>' +
317
+ '</div>';
292
318
  return '<div data-settings-project="' + escHtml(p.name) + '" data-search="project ' + escHtml(p.name.toLowerCase()) + '" style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:12px">' +
293
319
  '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
294
320
  '<div style="font-size:var(--text-md);font-weight:600">' + escHtml(p.name) + '</div>' +
@@ -297,6 +323,7 @@ async function openSettings() {
297
323
  pathRow +
298
324
  branchGrid +
299
325
  worktreeModeBlock +
326
+ liveValidationBlock +
300
327
  driftNote +
301
328
  '<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">' +
302
329
  settingsToggle('Discover from PRs', 'set-ws-prs-' + p.name, p.workSources.pullRequests.enabled, 'Discovery gate: scan repo for open PRs and surface them as review tasks. Independent of ADO/GitHub polling — does not affect already-tracked PRs.') +
@@ -700,6 +727,14 @@ async function openSettings() {
700
727
  const chip = document.querySelector('[data-checkout-mode-chip="' + (window.CSS && CSS.escape ? CSS.escape(projName) : projName) + '"]');
701
728
  if (!chip) return;
702
729
  chip.style.display = (sel.value === 'live') ? '' : 'none';
730
+ // M006 — also toggle the liveValidation section opacity/pointer-events
731
+ // reactively: disabled when checkoutMode is not 'live'.
732
+ const lvSection = document.querySelector('[data-live-validation-section="' + (window.CSS && CSS.escape ? CSS.escape(projName) : projName) + '"]');
733
+ if (lvSection) {
734
+ const isLive = (sel.value === 'live');
735
+ lvSection.style.opacity = isLive ? '' : '0.45';
736
+ lvSection.style.pointerEvents = isLive ? '' : 'none';
737
+ }
703
738
  });
704
739
  });
705
740
  }
@@ -1143,10 +1178,20 @@ async function saveSettings() {
1143
1178
  // values.
1144
1179
  const wtModeInput = document.getElementById('set-checkoutMode-' + p.name);
1145
1180
  const wtModeValue = (wtModeInput && wtModeInput.value === 'live') ? 'live' : 'worktree';
1181
+ // M006 — liveValidation: read type text input and autoDispatch checkbox.
1182
+ // Empty type → send null so the server clears the field.
1183
+ const lvTypeInput = document.getElementById('set-liveValidationType-' + p.name);
1184
+ const lvAutoDispatchInput = document.getElementById('set-liveValidationAutoDispatch-' + p.name);
1185
+ const lvTypeValue = lvTypeInput ? lvTypeInput.value.trim() : '';
1186
+ const lvAutoDispatchValue = lvAutoDispatchInput ? !!lvAutoDispatchInput.checked : false;
1187
+ const liveValidationValue = lvTypeValue
1188
+ ? { type: lvTypeValue, autoDispatch: lvAutoDispatchValue }
1189
+ : null;
1146
1190
  return {
1147
1191
  name: p.name,
1148
1192
  mainBranch: mainBranchValue || null,
1149
1193
  checkoutMode: wtModeValue,
1194
+ liveValidation: liveValidationValue,
1150
1195
  workSources: {
1151
1196
  pullRequests: { enabled: document.getElementById('set-ws-prs-' + p.name)?.checked ?? true },
1152
1197
  workItems: { enabled: document.getElementById('set-ws-wi-' + p.name)?.checked ?? true }
@@ -5,7 +5,7 @@
5
5
  </section>
6
6
  <!-- __ENGINE_MEMORY_PANEL__ -->
7
7
  <section>
8
- <h2>Engine Log <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">tick-by-tick audit trail of engine operations</span></h2>
8
+ <h2>Engine Log <span class="qa-section-subtitle">tick-by-tick audit trail of engine operations</span></h2>
9
9
  <div class="log-list" id="engine-log">No log entries yet.</div>
10
10
  </section>
11
11
  <section>
@@ -22,13 +22,13 @@
22
22
  </section>
23
23
  <section id="keep-processes-section">
24
24
  <h2>Keep-Processes <span class="count" id="keep-processes-count">0</span>
25
- <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">processes left running by agents</span>
25
+ <span class="qa-section-subtitle">processes left running by agents</span>
26
26
  </h2>
27
27
  <div id="keep-processes-content"><p class="empty">No agents have left processes running. Set <code>meta.keep_processes: true</code> on a work item to enable.</p></div>
28
28
  </section>
29
29
  <section id="managed-processes-section">
30
30
  <h2>Managed Processes <span class="count" id="managed-processes-count">0</span>
31
- <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">engine-managed long-running services</span>
31
+ <span class="qa-section-subtitle">engine-managed long-running services</span>
32
32
  </h2>
33
33
  <div id="managed-processes-content"><p class="empty">No managed processes. Agents declare them via <code>agents/&lt;id&gt;/managed-spawn.json</code>.</p></div>
34
34
  </section>
@@ -1,7 +1,7 @@
1
1
  <section>
2
- <h2>Team Meetings <span class="count" id="meetings-count">0</span>
2
+ <h2>Meetings <span class="count" id="meetings-count">0</span>
3
3
  <button class="btn-add" style="margin-left:8px" onclick="openCreateMeetingModal()">+ New Meeting</button>
4
- <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">multi-round agent discussions — investigate, debate, conclude</span>
4
+ <span class="qa-section-subtitle">multi-round agent discussions — investigate, debate, conclude</span>
5
5
  </h2>
6
6
  <div id="meetings-content"><p class="empty">No meetings yet. Start one to have agents investigate, debate, and conclude on a topic.</p></div>
7
7
  </section>
@@ -1,7 +1,7 @@
1
1
  <section>
2
2
  <h2>Pipelines <span class="count" id="pipelines-count">0</span>
3
3
  <button class="btn-add" style="margin-left:8px" onclick="openCreatePipelineModal()">+ New Pipeline</button>
4
- <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">multi-stage workflows with dependencies — chain meetings, plans, tasks, merges in any order, on a schedule or manual</span>
4
+ <span class="qa-section-subtitle">multi-stage workflows with dependencies — chain meetings, plans, tasks, merges in any order, on a schedule or manual</span>
5
5
  </h2>
6
6
  <div id="pipelines-content"><p class="empty">No pipelines yet. Create one to chain stages like audit → meeting → plan → merge.</p></div>
7
7
  </section>
@@ -1,9 +1,9 @@
1
1
  <section>
2
- <h2>Minions Skills <span class="count" id="skills-count">0</span> <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">discovered from runtime native dirs, plugin installs, and configured project repos</span></h2>
2
+ <h2>Minions Skills <span class="count" id="skills-count">0</span> <span class="qa-section-subtitle">discovered from runtime native dirs, plugin installs, and configured project repos</span></h2>
3
3
  <div id="skills-list"><p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p></div>
4
4
  </section>
5
5
  <section>
6
- <h2>Slash Commands <span class="count" id="commands-count">0</span> <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">discovered from runtime native command dirs, plugin installs, and configured project repos</span></h2>
6
+ <h2>Slash Commands <span class="count" id="commands-count">0</span> <span class="qa-section-subtitle">discovered from runtime native command dirs, plugin installs, and configured project repos</span></h2>
7
7
  <div id="commands-list"><p class="empty">No slash commands discovered.</p></div>
8
8
  </section>
9
9
  <section>
@@ -11,6 +11,6 @@
11
11
  <div id="mcp-list"><p class="empty">No MCP servers synced.</p></div>
12
12
  </section>
13
13
  <section>
14
- <h2>Harness Propagation <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">what each runtime sees, what's attached via --add-dir, and which assets won't propagate to a fresh worktree</span></h2>
14
+ <h2>Harness Propagation <span class="qa-section-subtitle">what each runtime sees, what's attached via --add-dir, and which assets won't propagate to a fresh worktree</span></h2>
15
15
  <div id="harness-diag"><p class="empty">Loading harness diagnostics…</p></div>
16
16
  </section>
@@ -1,7 +1,7 @@
1
1
  <section id="watches-section">
2
2
  <h2>Watches <span class="count" id="watches-count">0</span>
3
3
  <button class="btn-add" style="margin-left:8px" onclick="openCreateWatchModal()">+ New</button>
4
- <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">persistent watches that monitor PRs, work items, and branches for changes</span>
4
+ <span class="qa-section-subtitle">persistent watches that monitor PRs, work items, and branches for changes</span>
5
5
  </h2>
6
6
  <div id="watches-content"><p class="empty">No active watches. Create one to monitor PRs, work items, or branches.</p></div>
7
7
  </section>
@@ -86,21 +86,35 @@
86
86
 
87
87
  // ── Engine tile ────────────────────────────────────────────
88
88
  var engine = data.engine || {};
89
- // /api/status exposes `state` ('running'|'paused'|'stopping'|'stopped')
90
- // and `started_at`. Trust `state` only codeVersion/started_at are static
91
- // fields that persist when the engine is paused/stopped, so they'd mislabel
92
- // a down engine as running. Absent state (shouldn't happen) reads as down.
93
- var engineState = engine.state || '';
94
- var startedAt = engine.started_at;
95
- var engineRunning = !!engineState && engineState !== 'stopped';
96
- if (!engineState) engineState = 'stopped';
97
- var engineLit = engineState === 'running' ? 'green' : (engineState === 'paused' ? 'amber' : 'red');
98
- updateTile(
99
- 'engine',
100
- engineRunning ? engineState : 'down',
101
- startedAt ? 'up since ' + relTime(startedAt) : '—',
102
- engineLit
103
- );
89
+ // Use lastTickAt (stamped by engine.js#tickInner on every successful tick)
90
+ // to show recency instead of the static started_at. This catches a frozen
91
+ // engine that is still "running" per state but hasn't ticked in minutes.
92
+ //
93
+ // Stale detection mirrors classic dashboard render-dispatch.js#renderEngineStatus:
94
+ // when the server flags heartbeatStale (BOTH heartbeat AND lastTickAt have
95
+ // aged past their thresholds) we override the display state to 'stale' and
96
+ // light the tile red — the engine claims running but may have crashed.
97
+ var rawState = engine.state || 'stopped';
98
+ var lastTickAt = engine.lastTickAt;
99
+ var heartbeatStale = !!engine.heartbeatStale;
100
+
101
+ var engineState = rawState;
102
+ if ((rawState === 'running' || rawState === 'degraded') && heartbeatStale) {
103
+ engineState = 'stale';
104
+ }
105
+
106
+ var engineLit = engineState === 'running' ? 'green'
107
+ : engineState === 'paused' ? 'amber'
108
+ : 'red'; // stopped / stale / degraded
109
+
110
+ var engineValue = engineState === 'stopped' ? 'down' : engineState;
111
+
112
+ // Detail: time since the last successful tick. When lastTickAt is absent
113
+ // (engine never ticked or very fresh start) fall back to '—'.
114
+ var engineDetail = lastTickAt ? 'last tick ' + relTime(lastTickAt) : '—';
115
+ if (engineState === 'stale') engineDetail += ' · may be down';
116
+
117
+ updateTile('engine', engineValue, engineDetail, engineLit);
104
118
 
105
119
  // ── Dispatches ─────────────────────────────────────────────
106
120
  // The per-minion "working" view moved to the Team cards (renderMembers);
@@ -274,6 +274,10 @@
274
274
  .pipeline-card-meta { margin-top: var(--space-2); display: flex; flex-wrap: wrap; gap: var(--space-4); font-size: var(--text-sm); color: var(--muted); }
275
275
  .pipeline-card-badges { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: var(--space-4); flex-shrink: 0; }
276
276
  .pipeline-empty-runs { border: 1px dashed var(--border); border-radius: var(--radius-md); padding: var(--space-6); color: var(--muted); font-size: var(--text-md); background: rgba(139,148,158,0.04); }
277
+ /* Separate the empty-state line from the muted section subtitle so the two
278
+ * muted lines don't read as one block (W-mqv5dvl8000odd79). Scoped to the
279
+ * pipelines list only; the global .empty stays untouched for other screens. */
280
+ #pipelines-content > .empty { margin-top: 2rem; }
277
281
  .prd-items-list { display: flex; flex-direction: column; gap: 3px; max-height: 400px; overflow-y: auto; padding: 0 8px; }
278
282
  .prd-item-row { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: var(--radius-sm); font-size: var(--text-base); background: var(--surface2); border: 1px solid var(--border); border-left: 3px solid var(--border); }
279
283
  .prd-item-row.st-done { border-left-color: var(--green); }
@@ -285,7 +289,7 @@
285
289
  .prd-item-row.st-paused { border-left-color: var(--muted); opacity: 0.5; }
286
290
  .prd-item-id { font-family: Consolas, monospace; color: var(--muted); min-width: 36px; font-size: var(--text-code); }
287
291
  .prd-item-name { flex: 1; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
288
- .prd-item-priority { font-size: var(--text-sm); padding: var(--space-1) var(--space-3); border-radius: var(--radius-lg); }
292
+ .prd-item-priority { font-size: var(--text-sm); padding: var(--space-1) var(--space-3); border-radius: var(--radius-lg); text-transform: uppercase; }
289
293
  .prd-item-priority.high { background: rgba(248,81,73,0.15); color: var(--red); }
290
294
  .prd-item-priority.medium { background: rgba(210,153,34,0.15); color: var(--yellow); }
291
295
  .prd-item-priority.low { background: rgba(139,148,158,0.15); color: var(--muted); }
@@ -301,6 +305,8 @@
301
305
  .pin-btn { color: var(--muted); border-color: var(--border); }
302
306
  .pin-btn.pinned { color: var(--yellow); border-color: var(--yellow); }
303
307
  .inbox-name { font-weight: 500; font-size: var(--text-md); color: var(--purple); margin-bottom: var(--space-2); display: flex; justify-content: space-between; }
308
+ .inbox-name-fallback { font-weight: 400; color: var(--muted); }
309
+ .inbox-filename { font-size: var(--text-sm); color: var(--muted); margin-bottom: var(--space-2); word-break: break-all; line-height: 1.4; }
304
310
  .inbox-preview { font-size: var(--text-base); color: var(--muted); line-height: 1.5; max-height: 60px; overflow: hidden; }
305
311
 
306
312
  .prd-panel, .pr-panel { border-bottom: 1px solid var(--border); overflow: visible; min-width: 0; }
@@ -324,8 +330,12 @@
324
330
  ellipsis overflow, and the horizontal scrollbar is pinned inside the
325
331
  viewport via a bounded-height container on the standalone /prs page so
326
332
  it stays reachable without scrolling to the bottom of a tall table. */
327
- .pr-table--prs { table-layout: fixed; width: 100%; min-width: 1420px; }
333
+ .pr-table--prs { table-layout: fixed; width: 100%; min-width: 1455px; }
328
334
  .pr-table--prs th, .pr-table--prs td { overflow: hidden; text-overflow: ellipsis; }
335
+ /* W-mqv5ccn7 — the Status pill (e.g. ABANDONED) must never ellipsis-truncate
336
+ its own label; let the Status cell (8th column) render its pill in full
337
+ rather than inheriting the shared cell-level clip above. */
338
+ .pr-table--prs td:nth-child(8) { overflow: visible; }
329
339
  .pr-table--prs th:last-child, .pr-table--prs td:last-child { width: auto; min-width: 0; }
330
340
  .pr-table--prs .pr-title { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
331
341
  .pr-table--prs .pr-agent { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; white-space: nowrap; }
@@ -1286,8 +1296,16 @@
1286
1296
  font-size: var(--text-sm); color: var(--muted);
1287
1297
  font-weight: 400; text-transform: none; letter-spacing: 0;
1288
1298
  }
1289
- .qa-engine-link { color: var(--blue); text-decoration: none; }
1290
- .qa-engine-link:hover { text-decoration: underline; }
1299
+ .qa-engine-link { color: var(--muted); text-decoration: underline; }
1300
+ .qa-engine-link:hover { color: var(--text); }
1301
+ /* Inline text link rendered from a <button> — strip the button chrome so it
1302
+ * reads as a colored underlined link mid-sentence, not a boxed control. */
1303
+ .qa-inline-link {
1304
+ border: none; background: none; padding: 0;
1305
+ display: inline; font: inherit;
1306
+ color: var(--blue); text-decoration: underline; cursor: pointer;
1307
+ }
1308
+ .qa-inline-link:hover { text-decoration: none; }
1291
1309
 
1292
1310
  /* Targets list — slim row per dedup'd target. */
1293
1311
  .qa-targets-list {