@yemi33/minions 0.1.2230 → 0.1.2231

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.
@@ -33,6 +33,87 @@ function switchTab(tabId) {
33
33
  renderDetailContent(detail, tabId);
34
34
  }
35
35
 
36
+ // W-mqptc3n3 — Normalize a completion-report `pr` value (canonical id or URL)
37
+ // into the `github:owner/repo#N` / `ado:org/proj/repo#N` form openArtifact
38
+ // expects. Returns null when the value isn't a recognizable PR pointer so the
39
+ // caller can render no chip rather than a broken one.
40
+ function _detailNormalizePrId(value) {
41
+ if (!value || typeof value !== 'string') return null;
42
+ var v = value.trim();
43
+ if (!v || v === 'N/A') return null;
44
+ if (/^(github|ado):.+#\d+$/i.test(v)) return v;
45
+ if (typeof _wiDeriveCanonicalPrIdFromUrl === 'function') {
46
+ var derived = _wiDeriveCanonicalPrIdFromUrl(v);
47
+ if (derived) return derived;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ // W-mqptc3n3 — Pick the completion report of the most recent SUCCESSFUL
53
+ // (result !== 'error') run for this agent. statusData reflects the latest run
54
+ // inside the 5-min window; when that run is done with an available report it IS
55
+ // the last successful run. Otherwise (idle >5min, latest run errored, or no
56
+ // fresh report) walk recentDispatches (most-recent-first) for the newest
57
+ // non-error run with an available report. Returns null when none qualifies.
58
+ function _lastSuccessfulCompletionReport(detail) {
59
+ if (!detail || typeof detail !== 'object') return null;
60
+ var sd = detail.statusData;
61
+ if (sd && sd.status === 'done' && sd.completionReport && sd.completionReport.available) {
62
+ return sd.completionReport;
63
+ }
64
+ var rds = Array.isArray(detail.recentDispatches) ? detail.recentDispatches : [];
65
+ for (var i = 0; i < rds.length; i++) {
66
+ var rd = rds[i];
67
+ if (rd && rd.result !== 'error' && rd.completionReport && rd.completionReport.available) {
68
+ return rd.completionReport;
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+
74
+ // W-mqptc3n3 — Render artifact chips (PR / note / KB) for a successful run's
75
+ // completion report. All chips route through renderArtifactLink (which escapes
76
+ // every field) so click → openArtifact() → modal-stack push comes for free.
77
+ // Returns '' when there is no linkable artifact (no empty header, no broken
78
+ // chip).
79
+ function _renderLastResultArtifactChips(report) {
80
+ if (!report || typeof report !== 'object') return '';
81
+ var chips = '';
82
+ var seen = {};
83
+ var addPr = function(value, title) {
84
+ var prId = _detailNormalizePrId(value);
85
+ if (!prId || seen['pr:' + prId]) return;
86
+ seen['pr:' + prId] = true;
87
+ chips += renderArtifactLink({ type: 'pr', id: prId, label: prId, title: title || ('Pull request ' + prId) }) + ' ';
88
+ };
89
+ addPr(report.pr, '');
90
+ var arts = Array.isArray(report.artifacts) ? report.artifacts : [];
91
+ arts.forEach(function(a) {
92
+ if (!a || typeof a !== 'object') return;
93
+ var t = String(a.type || '');
94
+ var p = String(a.path || '');
95
+ if (!p) return;
96
+ if (t === 'pr') {
97
+ addPr(p, a.title || '');
98
+ } else if (t === 'note') {
99
+ var base = p.replace(/^.*[\\/]/, '');
100
+ if (!base || seen['note:' + base]) return;
101
+ seen['note:' + base] = true;
102
+ var noteLabel = base.replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-/, '').slice(0, 30) || base;
103
+ chips += renderArtifactLink({ type: 'note', id: base, label: noteLabel, title: a.title || ('Note: ' + base) }) + ' ';
104
+ } else if (t === 'kb') {
105
+ var kbId = p.replace(/^kb:/, '').replace(/^knowledge\//, '');
106
+ if (!kbId || kbId.indexOf('/') <= 0 || seen['kb:' + kbId]) return;
107
+ seen['kb:' + kbId] = true;
108
+ var kbFile = kbId.slice(kbId.indexOf('/') + 1);
109
+ var kbLabel = kbFile.replace(/\.md$/, '').slice(0, 30) || kbFile;
110
+ chips += renderArtifactLink({ type: 'kb', id: kbId, label: kbLabel, title: a.title || ('KB: ' + kbFile) }) + ' ';
111
+ }
112
+ });
113
+ if (!chips) return '';
114
+ return '<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:8px">' + chips + '</div>';
115
+ }
116
+
36
117
  function renderDetailContent(detail, tab) {
37
118
  const el = document.getElementById('detail-content');
38
119
 
@@ -72,8 +153,15 @@ function renderDetailContent(detail, tab) {
72
153
  if (last.result) html += 'Result: <span style="color:var(--' + (last.result === 'error' ? 'red' : 'green') + ')">' + escHtml(last.result) + '</span>\n';
73
154
  }
74
155
  html += '</div>';
156
+ // W-mqptc3n3 — Surface artifact links (PR / note / KB) from the last
157
+ // SUCCESSFUL run under 'Last Result'. The chips render even when the agent
158
+ // is idle (>5min, so resultSummary is absent) by sourcing the report from
159
+ // recentDispatches.
160
+ var lastResultArtifacts = _renderLastResultArtifactChips(_lastSuccessfulCompletionReport(detail));
75
161
  if (detail.statusData.resultSummary) {
76
- html += '<h4>Last Result</h4><div class="section" style="border-left:3px solid var(--green);padding-left:12px">' + renderMd(detail.statusData.resultSummary) + '</div>';
162
+ html += '<h4>Last Result</h4><div class="section" style="border-left:3px solid var(--green);padding-left:12px">' + renderMd(detail.statusData.resultSummary) + lastResultArtifacts + '</div>';
163
+ } else if (lastResultArtifacts) {
164
+ html += '<h4>Last Result</h4><div class="section" style="border-left:3px solid var(--green);padding-left:12px">' + lastResultArtifacts + '</div>';
77
165
  }
78
166
  }
79
167
 
@@ -90,7 +178,7 @@ function renderDetailContent(detail, tab) {
90
178
  html += '<h4>Latest Output</h4><div class="section">' + renderMd(detail.outputLog) + '</div>';
91
179
  }
92
180
 
93
- // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml()/renderMd() (fields: status task, dispatch task/result/reason, inbox name/content, output log, result summary)
181
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml()/renderMd()/renderArtifactLink() (fields: status task, dispatch task/result/reason, inbox name/content, output log, result summary, last-result artifact chips (pr/note/kb via renderArtifactLink))
94
182
  el.innerHTML = html;
95
183
  } else if (tab === 'live') {
96
184
  var startedAt = detail.statusData?.started_at;
@@ -131,7 +131,7 @@ function _detectPageChanges(data) {
131
131
  // reload path below — RENDER_VERSIONS handles the within-process case.
132
132
  const RENDER_VERSIONS = {
133
133
  agents: 2,
134
- prdProgress: 1,
134
+ prdProgress: 2,
135
135
  prdPrs: 1,
136
136
  inbox: 2,
137
137
  projects: 3,
@@ -606,6 +606,10 @@ function renderPrdProgress(prog) {
606
606
  if (!e2eByPlan[planKey]) e2eByPlan[planKey] = [];
607
607
  e2eByPlan[planKey].push(pr);
608
608
  }
609
+ // W-mqps9jlb — PRD-persisted verify/E2E PR refs (prd.verifyPrs[], surfaced by
610
+ // queries.getPrdInfo as progress.verifyPrsByPlan). Merged into renderE2eSection
611
+ // so a merged aggregate PR survives removal of its live tracker record.
612
+ const persistedVerifyByPlan = (prog && prog.verifyPrsByPlan) || {};
609
613
 
610
614
  // Find testing guides in prd/ (verify-*.md files). Issue #2949 —
611
615
  // verifyGuides moved off /api/status to /api/verify-guides; refresh.js
@@ -618,13 +622,24 @@ function renderPrdProgress(prog) {
618
622
  }
619
623
 
620
624
  function renderE2eSection(planFile) {
621
- // Keep abandoned/closed E2E PRs visible (rendered de-emphasized via the
622
- // muted status badge) so a closed verify PR still shows on its PRD as
623
- // closed rather than silently vanishing. Sort terminal-abandoned ones last
624
- // so live/merged aggregates stay on top.
625
- const prs = (e2eByPlan[planFile] || [])
626
- .slice()
627
- .sort((a, b) => (a.status === 'abandoned' ? 1 : 0) - (b.status === 'abandoned' ? 1 : 0));
625
+ // Merge the LIVE tracker E2E PRs (e2eByPlan[planFile]) with the PRD-persisted
626
+ // verify PR refs (persistedVerifyByPlan[planFile], stamped onto prd.verifyPrs
627
+ // by lifecycle.persistVerifyPrsToPrd). Dedup by pr.id with the LIVE record
628
+ // winning, so a merged aggregate PR keeps rendering on its PRD even after its
629
+ // live tracker record has been swept (e.g. daily-remove-merged-prs) — W-mqps9jlb.
630
+ const livePrs = e2eByPlan[planFile] || [];
631
+ const persistedPrs = persistedVerifyByPlan[planFile] || [];
632
+ const mergedById = new Map();
633
+ for (const p of persistedPrs) { if (p && p.id != null) mergedById.set(p.id, { ...p, _project: p.project || p._project || '' }); }
634
+ const liveNoId = [];
635
+ for (const p of livePrs) { if (p && p.id != null) { mergedById.set(p.id, p); } else if (p) { liveNoId.push(p); } }
636
+ // Keep abandoned/closed/merged E2E PRs visible (rendered de-emphasized via a
637
+ // muted status badge + reduced opacity) so a terminal verify PR still shows on
638
+ // its PRD rather than silently vanishing. Sort terminal rows after active ones;
639
+ // abandoned last of all.
640
+ const termRank = (s) => (s === 'abandoned' ? 2 : (s && s !== 'active' ? 1 : 0));
641
+ const prs = [...mergedById.values(), ...liveNoId]
642
+ .sort((a, b) => (termRank(a.status) - termRank(b.status)) || ((a.status === 'abandoned' ? 1 : 0) - (b.status === 'abandoned' ? 1 : 0)));
628
643
  const guide = guideByPlan[planFile];
629
644
  if (prs.length === 0 && !guide) return '';
630
645
  let html = '<div style="margin:6px 0 10px;padding:6px 10px;background:rgba(56,139,253,0.08);border:1px solid rgba(56,139,253,0.25);border-radius:4px">';
@@ -632,11 +647,13 @@ function renderPrdProgress(prog) {
632
647
  html += '<div style="font-size:var(--text-sm);font-weight:600;color:var(--blue);margin-bottom:4px">E2E Aggregate PRs</div>';
633
648
  html += prs.map(pr => {
634
649
  const statusColor = _prStatusColor(pr.status);
650
+ // Terminal (non-active) aggregates render de-emphasized like the abandoned case.
651
+ const rowOpacity = (pr.status && pr.status !== 'active') ? '0.65' : '1';
635
652
  // P-79b47b0c — render PR id as in-stack chip (was raw <a target="_blank">).
636
653
  const prChip = (typeof renderArtifactLink === 'function')
637
654
  ? renderArtifactLink({ type: 'pr', id: pr.id, label: pr.id, title: pr.title || pr.id })
638
655
  : '<code>' + escHtml(pr.id) + '</code>';
639
- return '<div style="display:flex;align-items:center;gap:6px;padding:2px 0;font-size:var(--text-base)">' +
656
+ return '<div style="display:flex;align-items:center;gap:6px;padding:2px 0;font-size:var(--text-base);opacity:' + rowOpacity + '">' +
640
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>' +
641
658
  prChip +
642
659
  '<span style="color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(pr.title || '') + '</span>' +
@@ -5557,6 +5557,123 @@ function syncPrdFromPrs(config) {
5557
5557
  }
5558
5558
  }
5559
5559
 
5560
+ // ─── Persist verify/E2E aggregate PRs onto the PRD JSON (W-mqps9jlb) ──────────
5561
+ // The PRD view's "E2E Aggregate PRs" section (dashboard/js/render-prd.js#
5562
+ // renderE2eSection) sources its PRs from the LIVE tracker (pull-requests.json).
5563
+ // A merged aggregate PR therefore VANISHES from the PRD page once its record is
5564
+ // swept — by the daily-remove-merged-prs schedule, a manual delete, or a
5565
+ // merge-back/reconcile flow. To make the PRD view survive removal of the live
5566
+ // record we durably stamp a compact reference array (`prd.verifyPrs`) onto the
5567
+ // owning PRD JSON.
5568
+ //
5569
+ // This runs per-tick (right after syncPrdFromPrs). It reads the NORMALIZED
5570
+ // pull-requests.json schema, so a single sweep covers BOTH the GitHub and ADO
5571
+ // pollers at once (engine/github.js + engine/ado.js are intentional mirrors
5572
+ // writing the same schema) — the durable single-source approach the task
5573
+ // allows in lieu of hooking each poller's merged transition separately. Because
5574
+ // it re-stamps every tick, the persisted copy is KEPT CURRENT through the
5575
+ // merged transition: the last poll before the sweep records status:'merged' +
5576
+ // mergedAt. Append-only-ish — a previously-recorded verify PR is never dropped
5577
+ // just because it left the live tracker.
5578
+
5579
+ /** Match the dashboard's renderE2eSection predicate (render-prd.js ~line 603). */
5580
+ function _isVerifyAggregatePr(pr) {
5581
+ if (!pr || typeof pr !== 'object') return false;
5582
+ // Literal 'verify'/'pr' itemType strings to mirror render-prd.js exactly —
5583
+ // note there is no WORK_TYPE.PR constant; the PR aggregate itemType is 'pr'.
5584
+ return pr.itemType === WORK_TYPE.VERIFY
5585
+ || pr.itemType === 'pr'
5586
+ || pr.e2e === true
5587
+ || (typeof pr.title === 'string' && pr.title.startsWith('[E2E]'));
5588
+ }
5589
+
5590
+ /** Compact, render-ready reference persisted onto prd.verifyPrs[]. */
5591
+ function _verifyPrRef(pr, projectName) {
5592
+ const ref = {
5593
+ id: pr.id,
5594
+ url: pr.url || '',
5595
+ title: pr.title || '',
5596
+ status: pr.status || PR_STATUS.ACTIVE,
5597
+ project: pr._project || projectName || '',
5598
+ };
5599
+ if (pr.mergedAt) ref.mergedAt = pr.mergedAt;
5600
+ return ref;
5601
+ }
5602
+
5603
+ /** True when persisting `refMap` would change the existing prd.verifyPrs[]. */
5604
+ function _verifyPrsNeedUpdate(existing, refMap) {
5605
+ const byId = new Map();
5606
+ for (const e of (existing || [])) { if (e && e.id) byId.set(e.id, e); }
5607
+ for (const [id, ref] of refMap) {
5608
+ const cur = byId.get(id);
5609
+ if (!cur) return true;
5610
+ if ((cur.status || '') !== (ref.status || '')) return true;
5611
+ if ((cur.url || '') !== (ref.url || '')) return true;
5612
+ if ((cur.title || '') !== (ref.title || '')) return true;
5613
+ if ((cur.project || '') !== (ref.project || '')) return true;
5614
+ if ((cur.mergedAt || '') !== (ref.mergedAt || '')) return true;
5615
+ }
5616
+ return false;
5617
+ }
5618
+
5619
+ function persistVerifyPrsToPrd(config) {
5620
+ try {
5621
+ config = config || queries.getConfig();
5622
+ if (!fs.existsSync(PRD_DIR)) return;
5623
+ const projects = shared.getProjects(config);
5624
+ if (!projects.length) return;
5625
+
5626
+ // Group verify/E2E PR refs by sourcePlan (PRD filename) across all projects.
5627
+ const refsByPlan = new Map(); // planFile -> Map(id -> ref)
5628
+ for (const project of projects) {
5629
+ let prs;
5630
+ try { prs = safeJsonArr(shared.projectPrPath(project)); } catch { continue; }
5631
+ for (const pr of (prs || [])) {
5632
+ if (!pr || !pr.id) continue;
5633
+ if (!_isVerifyAggregatePr(pr)) continue;
5634
+ const planFile = typeof pr.sourcePlan === 'string' ? pr.sourcePlan.trim() : '';
5635
+ if (!planFile) continue; // can't attribute to a PRD without a sourcePlan
5636
+ if (!refsByPlan.has(planFile)) refsByPlan.set(planFile, new Map());
5637
+ // A canonical PR id is unique to one project, so last-writer-wins here
5638
+ // only ever collapses byte-identical duplicates within a tick.
5639
+ refsByPlan.get(planFile).set(pr.id, _verifyPrRef(pr, project.name));
5640
+ }
5641
+ }
5642
+ if (refsByPlan.size === 0) return;
5643
+
5644
+ for (const [planFile, refMap] of refsByPlan) {
5645
+ // Resolve the PRD file (active or archived). sourcePlan is a bare filename.
5646
+ const candidates = [path.join(PRD_DIR, planFile), path.join(PRD_DIR, 'archive', planFile)];
5647
+ const fpath = candidates.find(p => { try { return fs.existsSync(p); } catch { return false; } });
5648
+ if (!fpath) continue;
5649
+
5650
+ // Lock-free peek (mirrors syncPrdItemStatus): only take the write lock
5651
+ // when the persisted refs would actually change. safeJsonNoRestore so an
5652
+ // archived PRD's .backup sidecar can't resurrect stale state.
5653
+ let plan;
5654
+ try { plan = safeJsonNoRestore(fpath); } catch { continue; }
5655
+ if (!plan || typeof plan !== 'object') continue;
5656
+ if (!_verifyPrsNeedUpdate(plan.verifyPrs, refMap)) continue;
5657
+
5658
+ mutateJsonFileLocked(fpath, (fresh) => {
5659
+ if (!fresh || typeof fresh !== 'object') return fresh;
5660
+ const byId = new Map();
5661
+ for (const e of (Array.isArray(fresh.verifyPrs) ? fresh.verifyPrs : [])) {
5662
+ if (e && e.id) byId.set(e.id, e);
5663
+ }
5664
+ // Upsert live refs (kept current); never drop a previously-recorded one.
5665
+ for (const [id, ref] of refMap) {
5666
+ byId.set(id, { ...(byId.get(id) || {}), ...ref });
5667
+ }
5668
+ fresh.verifyPrs = [...byId.values()];
5669
+ return fresh;
5670
+ }, { skipWriteIfUnchanged: true });
5671
+ }
5672
+ } catch (err) {
5673
+ try { log('warn', `persistVerifyPrsToPrd error: ${err?.message || err}`); } catch { /* engine not available */ }
5674
+ }
5675
+ }
5676
+
5560
5677
  // ─── Failure Classification ─────────────────────────────────────────────────
5561
5678
 
5562
5679
  /**
@@ -5794,6 +5911,7 @@ module.exports = {
5794
5911
  promoteCompletionArtifacts,
5795
5912
  runPostCompletionHooks,
5796
5913
  syncPrdFromPrs,
5914
+ persistVerifyPrsToPrd,
5797
5915
  resolveWorkItemPath,
5798
5916
  isItemCompleted,
5799
5917
  classifyFailure,
package/engine/queries.js CHANGED
@@ -1971,6 +1971,11 @@ function getPrdInfo(config) {
1971
1971
 
1972
1972
  let allPrdItems = [];
1973
1973
  const existingPrds = [];
1974
+ // W-mqps9jlb — verify/E2E aggregate PR refs persisted onto each PRD JSON
1975
+ // (prd.verifyPrs[], stamped by lifecycle.persistVerifyPrsToPrd). Keyed by PRD
1976
+ // filename so render-prd.js#renderE2eSection can merge them with the live
1977
+ // tracker and keep rendering a merged aggregate after its live record is swept.
1978
+ const verifyPrsByPlan = {};
1974
1979
  let latestStat = null;
1975
1980
 
1976
1981
  // Check if directory listings need refresh
@@ -2019,6 +2024,13 @@ function getPrdInfo(config) {
2019
2024
  completedAt: plan.completedAt || '',
2020
2025
  _archived: archived,
2021
2026
  });
2027
+ if (Array.isArray(plan.verifyPrs) && plan.verifyPrs.length > 0) {
2028
+ verifyPrsByPlan[pf] = plan.verifyPrs.map(r => ({
2029
+ id: r.id, url: r.url || '', title: r.title || '',
2030
+ status: r.status || 'active', project: r.project || '',
2031
+ ...(r.mergedAt ? { mergedAt: r.mergedAt } : {}),
2032
+ }));
2033
+ }
2022
2034
  for (const f of plan.missing_features) {
2023
2035
  allPrdItems.push({
2024
2036
  ...f, _source: pf, _planStatus: plan.status || 'active',
@@ -2205,6 +2217,7 @@ function getPrdInfo(config) {
2205
2217
 
2206
2218
  const progress = {
2207
2219
  total, complete, inProgress, paused, missing, donePercent, planTimings,
2220
+ verifyPrsByPlan,
2208
2221
  items: items.map(i => ({
2209
2222
  id: i.id, name: i.name || i.title, priority: i.priority,
2210
2223
  complexity: i.estimated_complexity || i.size, status: i.status || 'missing',
package/engine.js CHANGED
@@ -166,7 +166,7 @@ const ghToken = require('./engine/gh-token');
166
166
 
167
167
  const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, reconcilePrdStatuses, handlePostMerge, checkPlanCompletion,
168
168
  syncPrsFromOutput, updatePrAfterReview, updatePrAfterFix, checkForLearnings, extractSkillsFromOutput,
169
- updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs,
169
+ updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs, persistVerifyPrsToPrd,
170
170
  isItemCompleted, classifyFailure: classifyFailureFallback, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemPath,
171
171
  mergeArtifactNotes, promoteCompletionArtifacts, pruneScopeMismatchDuplicatePrs } = require('./engine/lifecycle');
172
172
 
@@ -9281,6 +9281,7 @@ async function tickInner() {
9281
9281
  if (_isTickStale(myGeneration)) return;
9282
9282
  // Sync PR status back to PRD items (missing → done when active PR exists)
9283
9283
  try { syncPrdFromPrs(config); } catch (err) { log('warn', `PRD sync error: ${err?.message || err}`); }
9284
+ try { persistVerifyPrsToPrd(config); } catch (err) { log('warn', `PRD verify-PR persist error: ${err?.message || err}`); }
9284
9285
  // Check if any plans can be marked completed (all features done/in-pr)
9285
9286
  try {
9286
9287
  const prdFiles = safeReadDir(PRD_DIR).filter(f => f.endsWith('.json'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2230",
3
+ "version": "0.1.2231",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"