@yemi33/minions 0.1.2393 → 0.1.2395

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.
@@ -141,7 +141,7 @@ const RENDER_VERSIONS = {
141
141
  projects: 9,
142
142
  notes: 1,
143
143
  prd: 3,
144
- prs: 3,
144
+ prs: 4,
145
145
  archivedPrds: 1,
146
146
  engine: 4,
147
147
  version: 1,
@@ -137,6 +137,12 @@ function stripMarkdown(text) {
137
137
  .trim();
138
138
  }
139
139
 
140
+ function _renderPrMergeConflictBadge(pr) {
141
+ if (!isPrMergeConflicted(pr)) return '';
142
+ return '<span class="pr-badge rejected merge-conflict" title="' + escapeHtml(PR_MERGE_CONFLICT_LABEL) +
143
+ '" aria-label="' + escapeHtml(PR_MERGE_CONFLICT_LABEL) + '">conflict</span>';
144
+ }
145
+
140
146
  function prRow(pr) {
141
147
  // Minions review (agent) state — separate from ADO human review
142
148
  const sq = pr.minionsReview || {};
@@ -151,6 +157,9 @@ function prRow(pr) {
151
157
  const buildTitle = pr.buildStaleMergeCommit
152
158
  ? 'ADO has builds on this PR\'s merge ref, but none target the current merge commit (typically after a rebase or target-branch advance). Engine classified from the pre-rebase build set and queued a fresh build (rate limited 30m). See Issue #2747.'
153
159
  : (pr._buildStatusDetail || '');
160
+ const mergeConflictBadge = _renderPrMergeConflictBadge(pr);
161
+ const mergeStateHtml = mergeConflictBadge ||
162
+ '<span style="color:var(--muted);font-size:var(--text-base)" aria-label="No merge conflict reported">—</span>';
154
163
  const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
155
164
  const statusLabel = pr.status || 'active';
156
165
  const branchError = pr._branchResolutionError?.reason || '';
@@ -312,6 +321,7 @@ function prRow(pr) {
312
321
  '<td><span class="pr-badge ' + statusClass + '" title="' + escapeHtml(statusLabel) + '">' + escapeHtml(statusLabel) + '</span></td>' +
313
322
  '<td><span class="pr-badge ' + reviewClass + '" title="' + escapeHtml(reviewTitle || reviewLabel) + '">' + escapeHtml(reviewLabel) + '</span></td>' +
314
323
  '<td><span class="pr-badge ' + buildClass + '" title="' + escapeHtml(buildTitle || buildLabel) + '">' + escapeHtml(buildLabel) + '</span></td>' +
324
+ '<td>' + mergeStateHtml + '</td>' +
315
325
  '<td><span class="' + branchClass + '" title="' + escapeHtml(branchError || branchLabel) + '">' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
316
326
  '<td><span class="pr-agent" title="' + escapeHtml(agentText) + '">' + escapeHtml(agentText) + '</span></td>' +
317
327
  '<td>' + reviewerCell + '</td>' +
@@ -323,7 +333,7 @@ function prRow(pr) {
323
333
  }
324
334
 
325
335
  // Explicit per-column widths keep the PR table from ballooning when titles or
326
- // branches are long. Total ≈1455px table grows past viewport on narrow
336
+ // branches are long. Total 1600px -> table grows past viewport on narrow
327
337
  // windows and the .pr-table-wrap--prs container scrolls horizontally inside
328
338
  // the viewport (sticky scrollbar — see styles.css).
329
339
  const PRS_COLGROUP =
@@ -333,6 +343,7 @@ const PRS_COLGROUP =
333
343
  '<col style="width:110px">' + // Status
334
344
  '<col style="width:130px">' + // Review
335
345
  '<col style="width:130px">' + // Build
346
+ '<col style="width:120px">' + // Merge conflict
336
347
  '<col style="width:200px">' + // Branch
337
348
  '<col style="width:140px">' + // Agent
338
349
  '<col style="width:140px">' + // Signed Off By
@@ -345,7 +356,7 @@ function prTableHtml(rows) {
345
356
  return '<div class="pr-table-wrap pr-table-wrap--prs"><table class="pr-table pr-table--prs">' +
346
357
  PRS_COLGROUP +
347
358
  '<thead><tr>' +
348
- '<th>PR</th><th>Title</th><th>Status</th><th>Review</th><th>Build</th><th>Branch</th><th>Agent</th><th>Signed Off By</th><th>Observe</th><th>Created</th><th></th>' +
359
+ '<th>PR</th><th>Title</th><th>Status</th><th>Review</th><th>Build</th><th>Merge</th><th>Branch</th><th>Agent</th><th>Signed Off By</th><th>Observe</th><th>Created</th><th></th>' +
349
360
  '</tr></thead><tbody>' + rows + '</tbody></table></div>';
350
361
  }
351
362
 
@@ -474,11 +485,13 @@ function _renderPrDetail(pr) {
474
485
  const statusClass = statusLabel === 'merged' ? 'pr-merged' :
475
486
  statusLabel === 'abandoned' ? 'pr-abandoned' :
476
487
  statusLabel === 'draft' ? 'draft' : 'pr-active';
488
+ const mergeConflictBadge = _renderPrMergeConflictBadge(pr);
477
489
 
478
490
  const badges =
479
491
  '<span class="pr-badge ' + statusClass + '" title="status">' + escapeHtml(String(statusLabel)) + '</span> ' +
480
492
  '<span class="pr-badge ' + reviewClass + '" title="review">' + escapeHtml(String(reviewLabel)) + '</span> ' +
481
- '<span class="pr-badge ' + buildClass + '" title="build">' + escapeHtml(String(buildLabel)) + '</span>';
493
+ '<span class="pr-badge ' + buildClass + '" title="build">' + escapeHtml(String(buildLabel)) + '</span>' +
494
+ (mergeConflictBadge ? ' ' + mergeConflictBadge : '');
482
495
 
483
496
  // Linked WIs — join via item._pr === pr.id (stamped by engine/queries.js).
484
497
  const wis = (window._lastWorkItems) || [];
@@ -845,4 +858,20 @@ async function resumeWorktreeHeldPause(btn) {
845
858
  }
846
859
  }
847
860
 
848
- window.MinionsPrs = { prRow, prTableHtml, renderPrs, prPrev, prNext, openAllPrs, openModal, openAddPrModal, openPrDetail, unlinkPr, togglePrObserve, resumePausedCause, resumeBuildFixIneffective, resumeWorktreeHeldPause };
861
+ window.MinionsPrs = {
862
+ prRow,
863
+ prTableHtml,
864
+ renderPrs,
865
+ renderPrDetail: _renderPrDetail, // exported for testing
866
+ prPrev,
867
+ prNext,
868
+ openAllPrs,
869
+ openModal,
870
+ openAddPrModal,
871
+ openPrDetail,
872
+ unlinkPr,
873
+ togglePrObserve,
874
+ resumePausedCause,
875
+ resumeBuildFixIneffective,
876
+ resumeWorktreeHeldPause
877
+ };
@@ -0,0 +1,18 @@
1
+ // Shared by the classic PR table/detail and the slim activity-card renderer.
2
+ // `_mergeConflict` is the provider-normalized signal persisted by both pollers;
3
+ // mergeable must still be explicitly false so null/unknown polls never show a
4
+ // stale conflict and a later mergeable poll removes the badge immediately.
5
+ const PR_MERGE_CONFLICT_LABEL = 'Merge conflict: update the source branch or resolve conflicts before this pull request can merge.';
6
+
7
+ function isPrMergeConflicted(pr) {
8
+ if (!pr || (pr.status !== 'active' && pr.status !== 'linked')) return false;
9
+ if (pr.mergeable !== false) return false;
10
+
11
+ var mergeStateStatus = typeof pr.mergeStateStatus === 'string'
12
+ ? pr.mergeStateStatus.trim().toLowerCase()
13
+ : '';
14
+ if (mergeStateStatus === 'blocked') return false;
15
+ if (mergeStateStatus === 'dirty') return true;
16
+
17
+ return pr._mergeConflict === true;
18
+ }
@@ -382,14 +382,23 @@
382
382
  titleEl.textContent = title;
383
383
  bodyTop.appendChild(titleEl);
384
384
 
385
- // Status row — grid col 2, row 2 (aligns with rail-bottom). A single
386
- // tile-chip, the same pill component (and color classes) as the PR list.
385
+ // Status row — grid col 2, row 2 (aligns with rail-bottom). Uses the same
386
+ // pill component for lifecycle status and the independently-derived
387
+ // merge-conflict state.
387
388
  var statusRow = document.createElement('div');
388
389
  statusRow.className = 'completions-card-pr-status';
389
390
  var chip = document.createElement('span');
390
391
  chip.className = 'tile-chip ' + cls;
391
392
  chip.textContent = p.status || '—';
392
393
  statusRow.appendChild(chip);
394
+ if (isPrMergeConflicted(p)) {
395
+ var conflictChip = document.createElement('span');
396
+ conflictChip.className = 'tile-chip red merge-conflict';
397
+ conflictChip.textContent = 'conflict';
398
+ conflictChip.title = PR_MERGE_CONFLICT_LABEL;
399
+ conflictChip.setAttribute('aria-label', conflictChip.title);
400
+ statusRow.appendChild(conflictChip);
401
+ }
393
402
 
394
403
  card.appendChild(rail);
395
404
  card.appendChild(bodyTop);
@@ -1602,8 +1602,8 @@
1602
1602
  }
1603
1603
  .completions-card-chip-pr:hover { filter: brightness(1.2); }
1604
1604
  /* PR card status row — occupies the same grid cell as the lineage row
1605
- (col 2, row 2) so the rail-bottom time aligns with it. Holds a single
1606
- left-aligned .tile-chip reusing the PR-list pill styling. */
1605
+ (col 2, row 2) so the rail-bottom time aligns with it. Holds lifecycle
1606
+ and optional conflict chips using the PR-list pill styling. */
1607
1607
  .completions-card-pr-status {
1608
1608
  grid-column: 2;
1609
1609
  display: flex;
@@ -330,12 +330,12 @@
330
330
  ellipsis overflow, and the horizontal scrollbar is pinned inside the
331
331
  viewport via a bounded-height container on the standalone /prs page so
332
332
  it stays reachable without scrolling to the bottom of a tall table. */
333
- .pr-table--prs { table-layout: fixed; width: 100%; min-width: 1455px; }
333
+ .pr-table--prs { table-layout: fixed; width: 100%; min-width: 1600px; }
334
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
335
+ /* W-mqv5ccn7 — the Signed Off By value must never ellipsis-truncate
336
+ its own label; let the reviewer cell (9th column) render in full
337
337
  rather than inheriting the shared cell-level clip above. */
338
- .pr-table--prs td:nth-child(8) { overflow: visible; }
338
+ .pr-table--prs td:nth-child(9) { overflow: visible; }
339
339
  .pr-table--prs th:last-child, .pr-table--prs td:last-child { width: auto; min-width: 0; }
340
340
  .pr-table--prs .pr-title { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
341
341
  .pr-table--prs .pr-agent { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; white-space: nowrap; }
@@ -8,6 +8,7 @@ const shared = require('./engine/shared');
8
8
  const { safeRead } = shared;
9
9
 
10
10
  const MINIONS_DIR = __dirname;
11
+ const DASHBOARD_SHARED_JS = ['pr-merge-state'];
11
12
 
12
13
  function buildDashboardHtml() {
13
14
  const dashDir = path.join(MINIONS_DIR, 'dashboard');
@@ -52,6 +53,10 @@ function buildDashboardHtml() {
52
53
  'confirm-dialog', 'modal', 'modal-qa', 'settings', 'qa', 'fre', 'refresh'
53
54
  ];
54
55
  let jsHtml = '';
56
+ for (const f of DASHBOARD_SHARED_JS) {
57
+ const content = safeRead(path.join(dashDir, 'shared', f + '.js'));
58
+ jsHtml += `\n// ─── shared/${f}.js ────────────────────────────────────────\n${content}\n`;
59
+ }
55
60
  for (const f of jsFiles) {
56
61
  const content = safeRead(path.join(dashDir, 'js', f + '.js'));
57
62
  jsHtml += `\n// ─── ${f}.js ────────────────────────────────────────\n${content}\n`;
@@ -87,6 +92,7 @@ function _slimSourcePaths(slimDir) {
87
92
  path.join(slimDir, 'layout.html'),
88
93
  path.join(slimDir, 'styles.css'),
89
94
  path.join(slimDir, 'body.html'),
95
+ ...DASHBOARD_SHARED_JS.map(f => path.join(MINIONS_DIR, 'dashboard', 'shared', f + '.js')),
90
96
  ...SLIM_JS_ORDER.map(f => path.join(slimDir, 'js', f + '.js')),
91
97
  ];
92
98
  }
package/dashboard.js CHANGED
@@ -1796,6 +1796,10 @@ function buildDashboardHtml() {
1796
1796
  'confirm-dialog', 'modal', 'modal-qa', 'settings', 'qa', 'fre', 'refresh'
1797
1797
  ];
1798
1798
  let jsHtml = '';
1799
+ for (const f of ['pr-merge-state']) {
1800
+ const content = safeRead(path.join(dashDir, 'shared', f + '.js'));
1801
+ jsHtml += `\n// ─── shared/${f}.js ────────────────────────────────────────\n${content}\n`;
1802
+ }
1799
1803
  for (const f of jsFiles) {
1800
1804
  const content = safeRead(path.join(dashDir, 'js', f + '.js'));
1801
1805
  jsHtml += `\n// ─── ${f}.js ────────────────────────────────────────\n${content}\n`;
@@ -2629,7 +2629,7 @@ function normalizePrFixBranchName(branch) {
2629
2629
  }
2630
2630
 
2631
2631
  function getPrFixBaselineHead(pr) {
2632
- return String(pr?.headSha || pr?._adoSourceCommit || pr?._adoHeadCommit || '').trim();
2632
+ return String(pr?.headRefOid || pr?.headSha || pr?._adoSourceCommit || pr?._adoHeadCommit || '').trim();
2633
2633
  }
2634
2634
 
2635
2635
  function findPrFixWorktree(meta, project, config) {
@@ -5962,7 +5962,14 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5962
5962
  }
5963
5963
  if (effectiveSuccess && !skipDoneStatus) {
5964
5964
  // Coding WIs and direct PR-sourced fix dispatches share this follow-up path.
5965
- try { autoDispatchLiveValidationWi(meta, config, dispatchItem); } catch (err) { log('warn', `autoDispatchLiveValidationWi: ${err.message}`); }
5965
+ const verifiedHeadSha = prFixBranchChange?.evidence === 'remote-head'
5966
+ ? prFixBranchChange.afterHead
5967
+ : '';
5968
+ try {
5969
+ autoDispatchLiveValidationWi(meta, config, dispatchItem, { verifiedHeadSha });
5970
+ } catch (err) {
5971
+ log('warn', `autoDispatchLiveValidationWi: ${err.message}`);
5972
+ }
5966
5973
  }
5967
5974
  // Failure retry is handled by completeDispatch in dispatch.js — not duplicated here.
5968
5975
  // Only clear _decomposing flag on failure so decompose items don't get permanently stuck.
@@ -6943,9 +6950,9 @@ function collapseAllDuplicatePrRecords(config) {
6943
6950
  // always a canonical `test` WI with the PR-focused `build-and-test` playbook;
6944
6951
  // liveValidation.type controls routing, not the semantic type of the task.
6945
6952
  //
6946
- // Deduplicates per codingWiId: any non-terminal WI with
6947
- // meta.liveValidationFor === codingWiId blocks a second dispatch, so only one
6948
- // validation task is ever in flight per coding completion.
6953
+ // Deduplicates both per codingWiId and per canonical PR source revision. This
6954
+ // keeps concurrent/repeated fixes for one unchanged PR head from queueing
6955
+ // duplicate validations while allowing a fresh validation after the head moves.
6949
6956
  //
6950
6957
  // W-mr9u39az000db5c2 — guard against a self-referential recursive cascade.
6951
6958
  // When liveValidation.type overlaps with a coding WI's own type set (e.g.
@@ -6970,7 +6977,7 @@ const ELIGIBLE_LIVE_VALIDATION_TYPES = new Set([
6970
6977
  WORK_TYPE.BUILD_FIX_COMPLEX,
6971
6978
  ]);
6972
6979
 
6973
- function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
6980
+ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null, options = {}) {
6974
6981
  const item = meta?.item || null;
6975
6982
  const sourceType = item?.type || dispatchItem?.type || null;
6976
6983
  const sourceKey = item?.id || dispatchItem?.id || null;
@@ -7033,27 +7040,48 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
7033
7040
  const prTarget = prUrl || (prId != null ? String(prId) : null);
7034
7041
  if (!prTarget) return;
7035
7042
 
7043
+ const prIdentityRef = {
7044
+ id: prId != null ? String(prId) : prTarget,
7045
+ url: prUrl || '',
7046
+ };
7047
+ let currentPr = typeof prRef === 'object' ? prRef : null;
7048
+ try {
7049
+ currentPr = shared.findPrRecord(shared.readPullRequests(project), prIdentityRef, project) || currentPr;
7050
+ } catch (err) {
7051
+ log('warn', `liveValidation: could not resolve tracked PR ${prTarget}: ${err.message}`);
7052
+ }
7053
+ const liveValidationPrId = shared.getPrIdentityKey(currentPr || prIdentityRef, project);
7054
+ if (!liveValidationPrId) return;
7055
+ const liveValidationHeadSha = String(options?.verifiedHeadSha || '').trim()
7056
+ || getPrFixBaselineHead(currentPr);
7057
+
7036
7058
  const codingWiId = sourceKey;
7037
7059
  const wiScope = item ? resolveWorkItemScope(meta) : projectName;
7038
7060
  if (!wiScope) return;
7039
7061
 
7040
7062
  try {
7041
- mutateWorkItems(wiScope, items => {
7063
+ mutateWorkItems(wiScope, (items, comparisonItems) => {
7042
7064
  if (!Array.isArray(items)) return items;
7043
7065
 
7044
- // Dedup: only one validation WI in flight per coding WI. Any non-terminal
7045
- // WI already tracking this coding WI (meta.liveValidationFor) blocks a
7046
- // second dispatch, regardless of its validation type. (Back-compat: WIs
7047
- // created before meta.liveValidationType existed are matched too, since
7048
- // we key solely on liveValidationFor.)
7049
- const existing = items.find(i =>
7050
- i &&
7051
- i.meta &&
7052
- i.meta.liveValidationFor === codingWiId &&
7053
- !PLAN_TERMINAL_STATUSES.has(i.status)
7054
- );
7066
+ // Keep the source-WI guard and revision guard in this same mutation as the
7067
+ // insert, and compare both supported scopes while the SQLite write
7068
+ // transaction is held. Legacy live-validation WIs predate the persisted
7069
+ // revision fields, so derive their identity from structured PR fields and
7070
+ // conservatively treat a missing head as matching until terminal.
7071
+ const existing = comparisonItems.find(i => {
7072
+ if (!i?.meta?.liveValidationFor || PLAN_TERMINAL_STATUSES.has(i.status)) return false;
7073
+ if (i.meta.liveValidationFor === codingWiId) return true;
7074
+
7075
+ const existingPrRef = i.meta.liveValidationPrId || shared.extractStructuredWorkItemPrRef(i);
7076
+ if (!existingPrRef) return false;
7077
+ const existingPrId = shared.getPrIdentityKey(existingPrRef, project);
7078
+ if (existingPrId !== liveValidationPrId) return false;
7079
+
7080
+ const existingHeadSha = String(i.meta.liveValidationHeadSha || '').trim();
7081
+ return !existingHeadSha || !liveValidationHeadSha || existingHeadSha === liveValidationHeadSha;
7082
+ });
7055
7083
  if (existing) {
7056
- log('info', `liveValidation: dedup — non-terminal validation WI ${existing.id} already exists for ${codingWiId}`);
7084
+ log('info', `liveValidation: dedup — non-terminal validation WI ${existing.id} blocks another validation for ${codingWiId} (${liveValidationPrId}@${liveValidationHeadSha || 'unknown'})`);
7057
7085
  return items;
7058
7086
  }
7059
7087
 
@@ -7081,6 +7109,8 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
7081
7109
  meta: {
7082
7110
  liveValidationFor: codingWiId,
7083
7111
  liveValidationType: validationType,
7112
+ liveValidationPrId,
7113
+ ...(liveValidationHeadSha ? { liveValidationHeadSha } : {}),
7084
7114
  playbook: 'build-and-test',
7085
7115
  ...(dispatchItem?.id ? { sourceDispatchId: dispatchItem.id } : {}),
7086
7116
  },
@@ -7093,7 +7123,7 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
7093
7123
  items.push(validationWi);
7094
7124
  log('info', `liveValidation: auto-dispatched validation WI ${validationWi.id} (type=${validationType}) for coding WI ${codingWiId}`);
7095
7125
  return items;
7096
- });
7126
+ }, { readScopes: ['central', projectName] });
7097
7127
  } catch (err) {
7098
7128
  log('warn', `autoDispatchLiveValidationWi for ${codingWiId}: ${err.message}`);
7099
7129
  }
package/engine/shared.js CHANGED
@@ -7702,12 +7702,17 @@ function readWorkItems(source = null) {
7702
7702
  return require('./work-items-store').readWorkItemsForScope(stateScope(source));
7703
7703
  }
7704
7704
 
7705
- function mutateWorkItems(source, mutator) {
7705
+ function mutateWorkItems(source, mutator, opts = {}) {
7706
7706
  const store = require('./work-items-store');
7707
- const { wrote, result } = store.applyWorkItemsMutation(stateScope(source), (items) => {
7707
+ const scope = stateScope(source);
7708
+ const normalizedOpts = { ...opts };
7709
+ if (Array.isArray(opts.readScopes)) {
7710
+ normalizedOpts.readScopes = opts.readScopes.map(stateScope);
7711
+ }
7712
+ const { wrote, result } = store.applyWorkItemsMutation(scope, (items, comparisonItems) => {
7708
7713
  if (!Array.isArray(items)) items = [];
7709
- return mutator(items) || items;
7710
- });
7714
+ return mutator(items, comparisonItems) || items;
7715
+ }, normalizedOpts);
7711
7716
  if (wrote) {
7712
7717
  try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
7713
7718
  try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
@@ -128,13 +128,14 @@ function _applyWorkItemsDiff(db, diff) {
128
128
 
129
129
  // Apply a mutation to the work-items array for a given scope. Wrapped in
130
130
  // a single transaction so the diff and apply can't race against a
131
- // concurrent reader/writer.
131
+ // concurrent reader/writer. Optional readScopes are exposed to the mutator
132
+ // as a combined comparison set; only target-scope mutations are persisted.
132
133
  //
133
134
  // Returns { wrote, result }:
134
135
  // wrote — true iff at least one INSERT/UPDATE/DELETE landed
135
136
  // result — the post-mutation array (legacy return shape of
136
137
  // mutateJsonFileLocked)
137
- function applyWorkItemsMutation(scope, mutator, opts) {
138
+ function applyWorkItemsMutation(scope, mutator, opts = {}) {
138
139
  const { getDb, withTransaction } = require('./db');
139
140
  const db = getDb();
140
141
 
@@ -144,8 +145,14 @@ function applyWorkItemsMutation(scope, mutator, opts) {
144
145
  // added by readAllWorkItems only and would round-trip as garbage into
145
146
  // the data column otherwise.
146
147
  for (const wi of before) { if (wi && wi._source) delete wi._source; }
148
+ const comparisonItems = [...before];
149
+ const readScopes = Array.isArray(opts.readScopes) ? opts.readScopes : [];
150
+ for (const readScope of new Set(readScopes)) {
151
+ if (readScope === scope) continue;
152
+ comparisonItems.push(...readWorkItemsForScope(readScope));
153
+ }
147
154
  const beforeSnapshot = JSON.parse(JSON.stringify(before));
148
- const next = mutator(before);
155
+ const next = mutator(before, comparisonItems);
149
156
  const after = (next === undefined || next === null)
150
157
  ? before
151
158
  : (Array.isArray(next) ? next : before);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2393",
3
+ "version": "0.1.2395",
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"