@yemi33/minions 0.1.1722 → 0.1.1724

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1724 (2026-05-05)
4
+
5
+ ### Features
6
+ - add per-cause PR automation dedupe (#2075)
7
+
8
+ ## 0.1.1723 (2026-05-05)
9
+
10
+ ### Other
11
+ - Extend command center session TTL
12
+
3
13
  ## 0.1.1722 (2026-05-05)
4
14
 
5
15
  ### Other
package/engine/ado.js CHANGED
@@ -708,6 +708,7 @@ async function pollPrStatus(config) {
708
708
  delete pr.buildStatus;
709
709
  delete pr.buildFailReason;
710
710
  delete pr.buildErrorLog;
711
+ delete pr.buildFailureSignature;
711
712
  delete pr._buildFailNotified;
712
713
  delete pr._buildStatusStale;
713
714
  delete pr._buildStatusDetail;
@@ -738,6 +739,11 @@ async function pollPrStatus(config) {
738
739
  pr._adoSourceCommit = sourceCommit;
739
740
  updated = true;
740
741
  }
742
+ const targetCommit = prData.lastMergeTargetCommit?.commitId || '';
743
+ if (targetCommit && pr._adoTargetCommit !== targetCommit) {
744
+ pr._adoTargetCommit = targetCommit;
745
+ updated = true;
746
+ }
741
747
 
742
748
  const reviewers = prData.reviewers || [];
743
749
  const votes = reviewers.map(r => r.vote).filter(v => v !== undefined);
@@ -811,6 +817,7 @@ async function pollPrStatus(config) {
811
817
  const mergeCommitId = prData.lastMergeCommit?.commitId;
812
818
  let buildStatus = pr.buildStatus || 'none';
813
819
  let buildFailReason = pr.buildFailReason || '';
820
+ let buildFailureSignature = pr.buildFailureSignature || '';
814
821
  let buildStatusResolved = true;
815
822
  let buildStatusStaleDetail = '';
816
823
 
@@ -834,6 +841,11 @@ async function pollPrStatus(config) {
834
841
  if (buildStatus === 'failing') {
835
842
  const failed = prBuilds.find(b => b.result === 'failed');
836
843
  buildFailReason = failed?.definition?.name || 'Build failed';
844
+ buildFailureSignature = shared.safeSlugComponent([
845
+ failed?.definition?.name,
846
+ failed?.result,
847
+ failed?.status,
848
+ ].filter(Boolean).join('\n') || buildFailReason, 80);
837
849
  }
838
850
  } else if (allBuilds.length > 0 && pr.buildStatus) {
839
851
  // Stale merge-commit fallback — ADO returned builds for this PR's merge ref
@@ -879,6 +891,8 @@ async function pollPrStatus(config) {
879
891
  pr.buildStatus = buildStatus;
880
892
  if (buildFailReason) pr.buildFailReason = buildFailReason;
881
893
  else delete pr.buildFailReason;
894
+ if (buildFailureSignature) pr.buildFailureSignature = buildFailureSignature;
895
+ else delete pr.buildFailureSignature;
882
896
  // Build transitioned — clear grace period and auto-complete flag
883
897
  delete pr._buildFixPushedAt;
884
898
  if (buildStatus === 'failing') delete pr._autoCompleted;
@@ -894,6 +908,7 @@ async function pollPrStatus(config) {
894
908
  // fix agents to be dispatched blind.
895
909
  if (buildStatus === 'passing') {
896
910
  delete pr.buildErrorLog;
911
+ delete pr.buildFailureSignature;
897
912
  // Reset build fix retry counter on recovery — allows fresh auto-fix cycles if build breaks again
898
913
  if (pr.buildFixAttempts) { delete pr.buildFixAttempts; delete pr.buildFixEscalated; }
899
914
  }
@@ -909,6 +924,16 @@ async function pollPrStatus(config) {
909
924
  } catch {}
910
925
  }
911
926
  }
927
+ if (buildStatus === 'failing') {
928
+ if (buildFailReason && pr.buildFailReason !== buildFailReason) {
929
+ pr.buildFailReason = buildFailReason;
930
+ updated = true;
931
+ }
932
+ if (buildFailureSignature && pr.buildFailureSignature !== buildFailureSignature) {
933
+ pr.buildFailureSignature = buildFailureSignature;
934
+ updated = true;
935
+ }
936
+ }
912
937
  }
913
938
 
914
939
  // Auto-complete: set auto-complete on PR when builds green + review approved
@@ -1041,6 +1066,8 @@ async function pollPrHumanComments(config) {
1041
1066
 
1042
1067
  pr.humanFeedback = {
1043
1068
  lastProcessedCommentDate: latestDate,
1069
+ lastProcessedCommentId: String(newHumanComments[newHumanComments.length - 1].commentId),
1070
+ lastProcessedCommentKey: `${newHumanComments[newHumanComments.length - 1].threadId}:${newHumanComments[newHumanComments.length - 1].commentId}`,
1044
1071
  pendingFix: true,
1045
1072
  feedbackContent
1046
1073
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-05T07:20:48.344Z"
4
+ "cachedAt": "2026-05-05T09:09:53.466Z"
5
5
  }
package/engine/github.js CHANGED
@@ -372,6 +372,10 @@ async function pollPrStatus(config) {
372
372
  pr.lastPushedAt = ts();
373
373
  updated = true;
374
374
  }
375
+ if (prData.base?.sha && pr.baseSha !== prData.base.sha) {
376
+ pr.baseSha = prData.base.sha;
377
+ updated = true;
378
+ }
375
379
 
376
380
  if (pr.status !== newStatus) {
377
381
  log('info', `PR ${pr.id} status: ${pr.status} → ${newStatus}`);
@@ -389,6 +393,7 @@ async function pollPrStatus(config) {
389
393
  delete pr.buildStatus;
390
394
  delete pr.buildFailReason;
391
395
  delete pr.buildErrorLog;
396
+ delete pr.buildFailureSignature;
392
397
  delete pr._buildFailNotified;
393
398
  delete pr.buildFixAttempts;
394
399
  delete pr.buildFixEscalated;
@@ -471,6 +476,7 @@ async function pollPrStatus(config) {
471
476
  const runs = checksData.check_runs;
472
477
  let buildStatus = 'none';
473
478
  let buildFailReason = '';
479
+ let buildFailureSignature = '';
474
480
 
475
481
  if (runs.length > 0) {
476
482
  const hasFailed = runs.some(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
@@ -481,6 +487,13 @@ async function pollPrStatus(config) {
481
487
  buildStatus = 'failing';
482
488
  const failed = runs.find(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
483
489
  buildFailReason = failed?.name || 'Check failed';
490
+ buildFailureSignature = shared.safeSlugComponent([
491
+ failed?.name,
492
+ failed?.conclusion,
493
+ failed?.output?.title,
494
+ failed?.output?.summary,
495
+ failed?.output?.text,
496
+ ].filter(Boolean).join('\n') || buildFailReason, 80);
484
497
  } else if (allDone && allPassed) {
485
498
  buildStatus = 'passing';
486
499
  } else {
@@ -493,6 +506,8 @@ async function pollPrStatus(config) {
493
506
  pr.buildStatus = buildStatus;
494
507
  if (buildFailReason) pr.buildFailReason = buildFailReason;
495
508
  else delete pr.buildFailReason;
509
+ if (buildFailureSignature) pr.buildFailureSignature = buildFailureSignature;
510
+ else delete pr.buildFailureSignature;
496
511
  // Build transitioned — clear grace period and auto-complete flag
497
512
  delete pr._buildFixPushedAt;
498
513
  if (buildStatus === 'failing') delete pr._autoCompleted; // allow re-merge after fix
@@ -504,6 +519,7 @@ async function pollPrStatus(config) {
504
519
  // while a queued build was still running.
505
520
  if (buildStatus === 'passing') {
506
521
  delete pr.buildErrorLog;
522
+ delete pr.buildFailureSignature;
507
523
  // Reset build fix retry counter on recovery — allows fresh auto-fix cycles if build breaks again
508
524
  if (pr.buildFixAttempts) { delete pr.buildFixAttempts; delete pr.buildFixEscalated; }
509
525
  }
@@ -519,6 +535,16 @@ async function pollPrStatus(config) {
519
535
  } catch {}
520
536
  }
521
537
  }
538
+ if (buildStatus === 'failing') {
539
+ if (buildFailReason && pr.buildFailReason !== buildFailReason) {
540
+ pr.buildFailReason = buildFailReason;
541
+ updated = true;
542
+ }
543
+ if (buildFailureSignature && pr.buildFailureSignature !== buildFailureSignature) {
544
+ pr.buildFailureSignature = buildFailureSignature;
545
+ updated = true;
546
+ }
547
+ }
522
548
  }
523
549
  }
524
550
 
@@ -636,6 +662,8 @@ async function pollPrHumanComments(config) {
636
662
 
637
663
  pr.humanFeedback = {
638
664
  lastProcessedCommentDate: latestDate,
665
+ lastProcessedCommentId: String(newComments[newComments.length - 1].commentId),
666
+ lastProcessedCommentKey: String(newComments[newComments.length - 1].commentId),
639
667
  pendingFix: true,
640
668
  feedbackContent
641
669
  };
@@ -1431,7 +1431,26 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
1431
1431
  }
1432
1432
  }
1433
1433
 
1434
- function updatePrAfterFix(pr, project, source) {
1434
+ function getHumanFeedbackAutomationCauseKey(pr) {
1435
+ const feedback = pr?.humanFeedback;
1436
+ if (!feedback || typeof feedback !== 'object') return '';
1437
+ const commentRef = feedback.lastProcessedCommentKey
1438
+ || feedback.lastProcessedCommentId
1439
+ || feedback.commentId
1440
+ || feedback.lastProcessedCommentDate
1441
+ || feedback.feedbackContent
1442
+ || '';
1443
+ return commentRef ? `human-comment:${shared.safeSlugComponent(commentRef, 80)}` : '';
1444
+ }
1445
+
1446
+ function shouldClearHumanFeedbackPendingFix(target, completedPr, automationCauseKey) {
1447
+ if (!target?.humanFeedback?.pendingFix) return true;
1448
+ const currentCauseKey = getHumanFeedbackAutomationCauseKey(target);
1449
+ const completedCauseKey = automationCauseKey || getHumanFeedbackAutomationCauseKey(completedPr);
1450
+ return !currentCauseKey || !completedCauseKey || currentCauseKey === completedCauseKey;
1451
+ }
1452
+
1453
+ function updatePrAfterFix(pr, project, source, automationCauseKey = '', dispatchId = '') {
1435
1454
 
1436
1455
  if (!pr?.id) return;
1437
1456
  const prPath = project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json');
@@ -1442,13 +1461,26 @@ function updatePrAfterFix(pr, project, source) {
1442
1461
  // Never downgrade from approved — fix was dispatched but PR is already approved
1443
1462
  if (target.reviewStatus !== 'approved') target.reviewStatus = 'waiting';
1444
1463
  if (source === 'pr-human-feedback') {
1445
- if (target.humanFeedback) target.humanFeedback.pendingFix = false;
1464
+ const clearPendingFix = shouldClearHumanFeedbackPendingFix(target, pr, automationCauseKey);
1465
+ if (target.humanFeedback && clearPendingFix) target.humanFeedback.pendingFix = false;
1446
1466
  target.minionsReview = { ...target.minionsReview, note: 'Fixed human feedback, awaiting re-review', fixedAt: ts() };
1447
- log('info', `Updated ${pr.id} → cleared humanFeedback.pendingFix, reset to waiting for re-review`);
1467
+ if (clearPendingFix) {
1468
+ log('info', `Updated ${pr.id} → cleared humanFeedback.pendingFix, reset to waiting for re-review`);
1469
+ } else {
1470
+ log('info', `Updated ${pr.id} → preserved newer humanFeedback.pendingFix, reset to waiting for re-review`);
1471
+ }
1448
1472
  } else {
1449
1473
  target.minionsReview = { ...target.minionsReview, note: 'Fixed, awaiting re-review', fixedAt: ts() };
1450
1474
  log('info', `Updated ${pr.id} → reviewStatus: waiting (fix pushed)`);
1451
1475
  }
1476
+ if (automationCauseKey) {
1477
+ shared.markPrAutomationCause(target, automationCauseKey, {
1478
+ source,
1479
+ dispatchId: dispatchId || null,
1480
+ status: 'handled',
1481
+ handledAt: ts(),
1482
+ });
1483
+ }
1452
1484
  return prs;
1453
1485
  }, { defaultValue: [] });
1454
1486
  }
@@ -2762,7 +2794,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
2762
2794
  log('warn', `Skipping PR review metadata update for ${meta?.pr?.id || meta?.pr?.url || '(unknown PR)'} because review dispatch ${dispatchItem.id} did not complete cleanly`);
2763
2795
  }
2764
2796
  if (type === WORK_TYPE.FIX && effectiveSuccess) {
2765
- updatePrAfterFix(meta?.pr, meta?.project, meta?.source);
2797
+ updatePrAfterFix(meta?.pr, meta?.project, meta?.source, meta?.automationCauseKey, dispatchItem?.id);
2766
2798
  // (#984) Sync PRD status for PR-linked features: fix work items have a different ID
2767
2799
  // than the original PRD feature, so syncPrdItemStatus(fixWiId, ...) finds nothing.
2768
2800
  // Use the PR's prdItems to propagate done status when the original work item is done.
package/engine/shared.js CHANGED
@@ -836,7 +836,7 @@ const ENGINE_DEFAULTS = {
836
836
  maxDispatchPromptBytes: 1024 * 1024, // 1 MB — dispatch items with prompts larger than this sidecar to engine/contexts/ to prevent dispatch.json OOM (#1167)
837
837
  maxStateFileBytes: 100 * 1024 * 1024, // 100 MB — fail startup with a clear error when dispatch.json / cooldowns.json exceed this, rather than silently OOMing on JSON.parse (#1167)
838
838
  ccMaxTurns: 50, // max tool-use turns for CC/doc-chat before CLI stops
839
- ccSessionTtlMs: 2 * 60 * 60 * 1000, // 2hexpire stale resumed CC sessions to cap context growth
839
+ ccSessionTtlMs: 7 * 24 * 60 * 60 * 1000, // 7dkeep chats resumable after breaks, still bounded by turn cap
840
840
  docSessionTtlMs: 7 * 24 * 60 * 60 * 1000, // 7d — longer-lived doc sessions, still bounded
841
841
  maxLlmRawBytes: 256 * 1024, // keep only a bounded stdout tail from direct Claude calls
842
842
  maxLlmStderrBytes: 64 * 1024, // keep only a bounded stderr tail from direct Claude calls
@@ -2532,6 +2532,40 @@ function safeSlugComponent(text, maxLen = 80) {
2532
2532
  return `${base}-${hash}`.slice(0, maxLen);
2533
2533
  }
2534
2534
 
2535
+ const PR_AUTOMATION_CAUSE_LIMIT = 50;
2536
+
2537
+ function getPrAutomationCauses(pr) {
2538
+ const causes = pr?._automationFixCauses;
2539
+ return causes && typeof causes === 'object' && !Array.isArray(causes) ? causes : {};
2540
+ }
2541
+
2542
+ function hasPrAutomationCause(pr, causeKey) {
2543
+ return !!(causeKey && getPrAutomationCauses(pr)[causeKey]);
2544
+ }
2545
+
2546
+ function markPrAutomationCause(pr, causeKey, details = {}) {
2547
+ if (!pr || !causeKey) return false;
2548
+ const now = ts();
2549
+ const causes = { ...getPrAutomationCauses(pr) };
2550
+ causes[causeKey] = {
2551
+ ...(causes[causeKey] || {}),
2552
+ ...details,
2553
+ status: details.status || causes[causeKey]?.status || 'handled',
2554
+ updatedAt: now,
2555
+ };
2556
+ if (!causes[causeKey].firstSeenAt) causes[causeKey].firstSeenAt = now;
2557
+
2558
+ const entries = Object.entries(causes);
2559
+ if (entries.length > PR_AUTOMATION_CAUSE_LIMIT) {
2560
+ entries
2561
+ .sort((a, b) => String(b[1]?.updatedAt || b[1]?.firstSeenAt || '').localeCompare(String(a[1]?.updatedAt || a[1]?.firstSeenAt || '')))
2562
+ .slice(PR_AUTOMATION_CAUSE_LIMIT)
2563
+ .forEach(([key]) => delete causes[key]);
2564
+ }
2565
+ pr._automationFixCauses = causes;
2566
+ return true;
2567
+ }
2568
+
2535
2569
  function formatTranscriptEntry(t) {
2536
2570
  return '### ' + (t.agent || 'agent') + ' (' + (t.type || '') + ', Round ' + (t.round || '?') + ')\n\n' + (t.content || '');
2537
2571
  }
@@ -2716,6 +2750,9 @@ module.exports = {
2716
2750
  redactSecrets,
2717
2751
  slugify,
2718
2752
  safeSlugComponent,
2753
+ getPrAutomationCauses,
2754
+ hasPrAutomationCause,
2755
+ markPrAutomationCause,
2719
2756
  formatTranscriptEntry,
2720
2757
  getPinnedItems,
2721
2758
  _logBuffer, // exported for testing
package/engine.js CHANGED
@@ -2308,6 +2308,93 @@ function ensurePrBranchForDispatch(project, pr, automationType) {
2308
2308
  return '';
2309
2309
  }
2310
2310
 
2311
+ function prCausePart(value, fallback = 'unknown') {
2312
+ const raw = String(value || '').trim();
2313
+ return shared.safeSlugComponent(raw || fallback, 80);
2314
+ }
2315
+
2316
+ function getPrCauseHead(pr) {
2317
+ return pr?.headSha
2318
+ || pr?.headSHA
2319
+ || pr?.head?.sha
2320
+ || pr?._adoSourceCommit
2321
+ || pr?._adoHeadCommit
2322
+ || pr?.lastMergeSourceCommit?.commitId
2323
+ || pr?.lastMergeCommit?.commitId
2324
+ || '';
2325
+ }
2326
+
2327
+ function getPrCauseBase(pr) {
2328
+ return pr?.baseSha
2329
+ || pr?.base?.sha
2330
+ || pr?._adoTargetCommit
2331
+ || pr?.lastMergeTargetCommit?.commitId
2332
+ || pr?.targetSha
2333
+ || pr?.targetRefSha
2334
+ || pr?.baseRefName
2335
+ || pr?.targetRefName
2336
+ || '';
2337
+ }
2338
+
2339
+ function getPrAutomationCauseKey(kind, pr) {
2340
+ if (kind === 'review-feedback') {
2341
+ const reviewRef = pr?.minionsReview?.dispatchId
2342
+ || pr?.minionsReview?.reviewedAt
2343
+ || pr?.lastReviewedAt
2344
+ || getPrCauseHead(pr)
2345
+ || pr?.reviewNote
2346
+ || pr?.minionsReview?.note
2347
+ || 'review';
2348
+ return `review-feedback:${prCausePart(reviewRef)}`;
2349
+ }
2350
+ if (kind === 'human-comment') {
2351
+ const commentRef = pr?.humanFeedback?.lastProcessedCommentKey
2352
+ || pr?.humanFeedback?.lastProcessedCommentId
2353
+ || pr?.humanFeedback?.commentId
2354
+ || pr?.humanFeedback?.lastProcessedCommentDate
2355
+ || pr?.humanFeedback?.feedbackContent
2356
+ || 'comment';
2357
+ return `human-comment:${prCausePart(commentRef)}`;
2358
+ }
2359
+ if (kind === 'build') {
2360
+ const checkName = pr?.buildFailReason || pr?._buildStatusDetail || 'check';
2361
+ const signature = pr?.buildFailureSignature
2362
+ || pr?.buildErrorLog
2363
+ || pr?._buildStatusDetail
2364
+ || pr?.buildStatusDetail
2365
+ || pr?.buildFailReason
2366
+ || 'failure';
2367
+ return `build:${prCausePart(checkName)}:${prCausePart(getPrCauseHead(pr), 'unknown-head')}:${prCausePart(signature)}`;
2368
+ }
2369
+ if (kind === 'merge-conflict') {
2370
+ return `merge-conflict:${prCausePart(getPrCauseBase(pr), 'unknown-base')}:${prCausePart(getPrCauseHead(pr), 'unknown-head')}`;
2371
+ }
2372
+ return `${kind}:${prCausePart(getPrCauseHead(pr) || pr?.id || 'pr')}`;
2373
+ }
2374
+
2375
+ function getPrAutomationDispatchKey(baseKey, causeKey) {
2376
+ return `${baseKey}-${shared.safeSlugComponent(causeKey, 96)}`;
2377
+ }
2378
+
2379
+ function isPrAutomationCausePending(project, pr, causeKey) {
2380
+ if (!causeKey) return false;
2381
+ const prCanonicalId = shared.getCanonicalPrId(project, pr, pr?.url || '');
2382
+ const dispatch = getDispatch();
2383
+ return [...(dispatch.pending || []), ...(dispatch.active || [])].some(d => {
2384
+ if (d.meta?.automationCauseKey !== causeKey) return false;
2385
+ if (!prCanonicalId) return true;
2386
+ const dispatchProject = d.meta?.project?.name
2387
+ ? (getProjects(getConfig()).find(p => p.name === d.meta.project.name) || d.meta.project)
2388
+ : (d.meta?.project || null);
2389
+ const dispatchPrId = shared.getCanonicalPrId(dispatchProject, d.meta?.pr, d.meta?.pr?.url || '');
2390
+ return !dispatchPrId || dispatchPrId === prCanonicalId;
2391
+ });
2392
+ }
2393
+
2394
+ function isPrAutomationCauseHandledOrPending(project, pr, causeKey) {
2395
+ return shared.hasPrAutomationCause(pr, causeKey) || isPrAutomationCausePending(project, pr, causeKey);
2396
+ }
2397
+
2311
2398
 
2312
2399
  // Tracks per-process which silent-discovery warnings have already been logged
2313
2400
  // so we don't spam the log every tick. Cleared on process exit (no need to
@@ -2448,10 +2535,13 @@ async function discoverFromPrs(config, project) {
2448
2535
 
2449
2536
  // Fresh reviewer comments are actionable fixes, even while the PR is otherwise
2450
2537
  // awaiting a stale-vote re-review or has build-fix retries escalated.
2451
- const humanFixKey = `human-fix-${project?.name || 'default'}-${prDisplayId}`;
2538
+ const humanFixBaseKey = `human-fix-${project?.name || 'default'}-${prDisplayId}`;
2539
+ const humanCauseKey = getPrAutomationCauseKey('human-comment', pr);
2540
+ const humanFixKey = getPrAutomationDispatchKey(humanFixBaseKey, humanCauseKey);
2452
2541
  const hasCoalescedFeedback = (dispatchCooldowns.get(humanFixKey)?.pendingContexts || []).length > 0;
2453
2542
  if (pollEnabled && autoFixHumanComments && (pr.humanFeedback?.pendingFix || hasCoalescedFeedback) && !fixDispatched) {
2454
2543
  const key = humanFixKey;
2544
+ if (isPrAutomationCauseHandledOrPending(project, pr, humanCauseKey)) continue;
2455
2545
  let staleCoalesced = [];
2456
2546
  const alreadyDispatched = isAlreadyDispatched(key);
2457
2547
  const blockedByCooldown = isOnCooldown(key, cooldownMs);
@@ -2492,7 +2582,7 @@ async function discoverFromPrs(config, project) {
2492
2582
  pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
2493
2583
  reviewer: 'Human Reviewer',
2494
2584
  review_note: reviewNote,
2495
- }, `Fix ${pr.id}: ${pr.title || ''} — human feedback`, { dispatchKey: key, source: 'pr-human-feedback', pr, branch: prBranch, project: projMeta });
2585
+ }, `Fix ${pr.id}: ${pr.title || ''} — human feedback`, { dispatchKey: key, automationCauseKey: humanCauseKey, source: 'pr-human-feedback', pr, branch: prBranch, project: projMeta });
2496
2586
  if (item) { newWork.push(item); fixDispatched = true; }
2497
2587
  }
2498
2588
 
@@ -2547,7 +2637,9 @@ async function discoverFromPrs(config, project) {
2547
2637
  // PRs with changes requested → route back to author for fix.
2548
2638
  // Gate on evalLoopEnabled and provider polling — the review→fix cycle depends on fresh vote state.
2549
2639
  if (evalLoopEnabled && pollEnabled && autoFixReviewFeedback && reviewStatus === 'changes-requested' && !awaitingReReview && !fixDispatched) {
2550
- const key = `fix-${project?.name || 'default'}-${prDisplayId}`;
2640
+ const reviewCauseKey = getPrAutomationCauseKey('review-feedback', pr);
2641
+ const key = getPrAutomationDispatchKey(`fix-${project?.name || 'default'}-${prDisplayId}`, reviewCauseKey);
2642
+ if (isPrAutomationCauseHandledOrPending(project, pr, reviewCauseKey)) continue;
2551
2643
  if (fixThrottled || isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
2552
2644
  const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
2553
2645
  if (!agentId) continue;
@@ -2557,7 +2649,7 @@ async function discoverFromPrs(config, project) {
2557
2649
  const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
2558
2650
  pr_id: pr.id, pr_branch: prBranch,
2559
2651
  review_note: pr.minionsReview?.note || pr.reviewNote || 'See PR thread comments',
2560
- }, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
2652
+ }, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, { dispatchKey: key, automationCauseKey: reviewCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta });
2561
2653
  if (item) {
2562
2654
  newWork.push(item); setCooldown(key); fixDispatched = true;
2563
2655
  }
@@ -2572,7 +2664,9 @@ async function discoverFromPrs(config, project) {
2572
2664
  }
2573
2665
  const autoFixBuilds = config.engine?.autoFixBuilds ?? ENGINE_DEFAULTS.autoFixBuilds;
2574
2666
  if (pollEnabled && autoFixBuilds && pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing') {
2575
- const key = `build-fix-${project?.name || 'default'}-${prDisplayId}`;
2667
+ const buildCauseKey = getPrAutomationCauseKey('build', pr);
2668
+ const key = getPrAutomationDispatchKey(`build-fix-${project?.name || 'default'}-${prDisplayId}`, buildCauseKey);
2669
+ if (isPrAutomationCauseHandledOrPending(project, pr, buildCauseKey)) continue;
2576
2670
  if (fixThrottled || isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
2577
2671
 
2578
2672
  // Pre-dispatch live build check — cached buildStatus may be stale: ADO can
@@ -2633,7 +2727,7 @@ async function discoverFromPrs(config, project) {
2633
2727
  const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
2634
2728
  pr_id: pr.id, pr_branch: prBranch,
2635
2729
  review_note: reviewNote,
2636
- }, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
2730
+ }, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, automationCauseKey: buildCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta });
2637
2731
  if (item) {
2638
2732
  newWork.push(item); setCooldown(key); fixDispatched = true;
2639
2733
  try {
@@ -2664,13 +2758,14 @@ async function discoverFromPrs(config, project) {
2664
2758
  // PRs with merge conflicts — dispatch fix to resolve (gated by provider polling + autoFixConflicts)
2665
2759
  const autoFixConflicts = config.engine?.autoFixConflicts ?? ENGINE_DEFAULTS.autoFixConflicts;
2666
2760
  if (pollEnabled && autoFixConflicts && pr.status === PR_STATUS.ACTIVE && pr._mergeConflict && !fixDispatched) {
2667
- const key = `conflict-fix-${project?.name || 'default'}-${prDisplayId}`;
2761
+ const conflictCauseKey = getPrAutomationCauseKey('merge-conflict', pr);
2762
+ const key = getPrAutomationDispatchKey(`conflict-fix-${project?.name || 'default'}-${prDisplayId}`, conflictCauseKey);
2668
2763
  // Suppress re-dispatch for 10 min after last attempt — ADO/GitHub recomputes
2669
2764
  // mergeStatus asynchronously (1–5 min lag), so the flag may stay set even after
2670
2765
  // a successful push. _conflictFixedAt is cleared when the poller confirms clean status.
2671
2766
  const conflictFixedAt = pr._conflictFixedAt;
2672
2767
  const withinLag = conflictFixedAt && Date.now() - new Date(conflictFixedAt).getTime() < 10 * 60 * 1000;
2673
- if (!withinLag && !fixThrottled && !isAlreadyDispatched(key) && !isOnCooldown(key, cooldownMs)) {
2768
+ if (!withinLag && !fixThrottled && !isPrAutomationCauseHandledOrPending(project, pr, conflictCauseKey) && !isAlreadyDispatched(key) && !isOnCooldown(key, cooldownMs)) {
2674
2769
  // Pre-dispatch live conflict check — cached `_mergeConflict` may be
2675
2770
  // stale: ADO/GitHub recompute mergeStatus asynchronously (1–5 min lag),
2676
2771
  // so a successful upstream merge can leave the flag set even after the
@@ -2702,7 +2797,7 @@ async function discoverFromPrs(config, project) {
2702
2797
  const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
2703
2798
  pr_id: pr.id, pr_branch: prBranch,
2704
2799
  review_note: `This PR has merge conflicts with the target branch. Inspect the live PR and repository history, choose the safest merge/rebase/update strategy, resolve all conflicts, validate the result, and push the branch.`,
2705
- }, `Fix merge conflicts on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
2800
+ }, `Fix merge conflicts on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, automationCauseKey: conflictCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta });
2706
2801
  if (item) {
2707
2802
  newWork.push(item);
2708
2803
  setCooldown(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1722",
3
+ "version": "0.1.1724",
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"