@yemi33/minions 0.1.774 → 0.1.776

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,11 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.774 (2026-04-10)
3
+ ## 0.1.776 (2026-04-10)
4
4
 
5
5
  ### Features
6
+ - auto-rebase dependent PRs after dependency merge
6
7
  - show PR merged/active status on PRD item links
7
8
 
8
9
  ### Fixes
10
+ - better agent comment filtering — detect by patterns, not just signature
9
11
  - filter CI bot noise from PR human comment detection
10
12
 
11
13
  ### Other
package/engine/ado.js CHANGED
@@ -458,12 +458,18 @@ async function pollPrHumanComments(config) {
458
458
  for (const thread of threads) {
459
459
  for (const comment of (thread.comments || [])) {
460
460
  if (!comment.content || comment.commentType === 'system') continue;
461
- if (/\bMinions\s*\(/i.test(comment.content)) continue; // skip minions's own comments
461
+ const content = comment.content;
462
+ // Minions agent signatures
463
+ if (/\bMinions\s*\(/i.test(content)) continue;
464
+ if (/\b(Review|Fixed|Explored|Verified|Tested)\s+by\s+Minions\b/i.test(content)) continue;
465
+ if (/\[minions\]/i.test(content)) continue;
462
466
  // Skip bot/service account comments
463
467
  const authorName = (comment.author?.displayName || '').toLowerCase();
464
468
  if (/\b(bot|service|build|pipeline|codecov|sonar)\b/i.test(authorName)) continue;
465
- // Skip automated status comments (coverage, build reports)
466
- if (/^#{1,3}\s*(Coverage|Build|Test|Deploy|Pipeline)\s*(Report|Status|Result|Summary)/i.test(comment.content)) continue;
469
+ // Short comments from same author that look agent-generated
470
+ if (content.length < 500 && /\b(fixed|addressed|resolved|pushed|updated|rebased)\b/i.test(content) && /\b(review|feedback|comment|finding)\b/i.test(content)) continue;
471
+ // Automated status comments (coverage, build reports)
472
+ if (/^#{1,3}\s*(Coverage|Build|Test|Deploy|Pipeline)\s*(Report|Status|Result|Summary)/i.test(content)) continue;
467
473
 
468
474
  const entry = {
469
475
  threadId: thread.id,
package/engine/github.js CHANGED
@@ -441,17 +441,25 @@ async function pollPrHumanComments(config) {
441
441
  ...(Array.isArray(reviewComments) ? reviewComments : []).map(c => ({ ...c, _type: 'review' }))
442
442
  ];
443
443
 
444
- // Filter out bot comments, minions's own comments, and CI noise
444
+ // Filter out bot comments, minions agent comments, and CI noise
445
+ // Agents post as the PAT owner — detect by signature patterns, not account type
446
+ const prAuthorLogin = (prData?.user?.login || '').toLowerCase();
445
447
  const humanComments = allComments.filter(c => {
446
448
  if (c.user?.type === 'Bot') return false;
447
- if (/\bMinions\s*\(/i.test(c.body || '')) return false;
448
- // Common CI bot usernames
449
+ const body = c.body || '';
450
+ // Minions agent signatures (review, fix, explore, etc.)
451
+ if (/\bMinions\s*\(/i.test(body)) return false;
452
+ if (/\b(Review|Fixed|Explored|Verified|Tested)\s+by\s+Minions\b/i.test(body)) return false;
453
+ if (/\[minions\]/i.test(body)) return false;
454
+ // Comments from the PR author account are likely agent-generated (agents use same PAT)
455
+ // Only skip if the comment looks automated (short, or has agent patterns)
449
456
  const login = (c.user?.login || '').toLowerCase();
457
+ if (login === prAuthorLogin && body.length < 500 && /\b(fixed|addressed|resolved|pushed|updated|rebased)\b/i.test(body)) return false;
458
+ // Common CI bot usernames
450
459
  if (/\b(bot|codecov|sonar|dependabot|renovate|github-actions|azure-pipelines)\b/i.test(login)) return false;
451
460
  // Automated status comments (coverage reports, build badges, etc.)
452
- const body = c.body || '';
453
461
  if (/^#{1,3}\s*(Coverage|Build|Test|Deploy|Pipeline)\s*(Report|Status|Result|Summary)/i.test(body)) return false;
454
- if (/!\[.*\]\(https?:\/\/.*badge/i.test(body)) return false; // badge images
462
+ if (/!\[.*\]\(https?:\/\/.*badge/i.test(body)) return false;
455
463
  return true;
456
464
  });
457
465
 
@@ -7,11 +7,12 @@ const fs = require('fs');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
9
  const shared = require('./shared');
10
- const { safeRead, safeJson, safeWrite, mutateJsonFileLocked, mutateWorkItems, execSilent, projectPrPath, getPrLinks, addPrLink,
10
+ const { safeRead, safeJson, safeWrite, mutateJsonFileLocked, mutateWorkItems, execSilent, execAsync, projectPrPath, getPrLinks, addPrLink,
11
11
  log, ts, dateStamp, WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
12
12
  ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS, FAILURE_CLASS } = shared;
13
13
  const { trackEngineUsage } = require('./llm');
14
14
  const queries = require('./queries');
15
+ const { isBranchActive } = require('./cooldown');
15
16
  const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
16
17
  MINIONS_DIR, ENGINE_DIR, PLANS_DIR, PRD_DIR, INBOX_DIR, AGENTS_DIR } = queries;
17
18
 
@@ -797,6 +798,119 @@ function updatePrAfterFix(pr, project, source) {
797
798
  }, { defaultValue: [] });
798
799
  }
799
800
 
801
+ // ─── Post-Merge Rebase ──────────────────────────────────────────────────────
802
+
803
+ /**
804
+ * Find active PRs whose work items depend on the just-merged item.
805
+ * Excludes shared-branch items (those use git pull instead).
806
+ */
807
+ function findDependentActivePrs(mergedItemId, config) {
808
+ const results = [];
809
+ const allWi = queries.getWorkItems(config);
810
+ const dependentWis = allWi.filter(wi =>
811
+ wi.depends_on?.includes(mergedItemId) &&
812
+ !DONE_STATUSES.has(wi.status) &&
813
+ wi.branchStrategy !== 'shared-branch'
814
+ );
815
+ if (dependentWis.length === 0) return results;
816
+
817
+ const projects = shared.getProjects(config);
818
+ for (const p of projects) {
819
+ const prs = safeJson(projectPrPath(p)) || [];
820
+ for (const pr of prs) {
821
+ if (!pr.branch || pr.status !== 'active') continue;
822
+ const linked = (pr.prdItems || []).some(id => dependentWis.some(wi => wi.id === id));
823
+ if (linked && !results.some(r => r.pr.id === pr.id)) {
824
+ const wi = dependentWis.find(w => (pr.prdItems || []).includes(w.id));
825
+ results.push({ pr, project: p, workItem: wi });
826
+ }
827
+ }
828
+ }
829
+ return results;
830
+ }
831
+
832
+ /**
833
+ * Rebase a PR branch onto main in a temporary worktree.
834
+ * Returns { success: true } or { success: false, error: string }.
835
+ */
836
+ async function rebaseBranchOntoMain(pr, project, config) {
837
+ const root = path.resolve(project.localPath);
838
+ const mainBranch = shared.resolveMainBranch(root, project.mainBranch);
839
+ const branch = pr.branch;
840
+ const wtRoot = path.resolve(root, config.engine?.worktreeRoot || ENGINE_DEFAULTS.worktreeRoot);
841
+ const tmpWt = path.join(wtRoot, `rebase-${shared.sanitizeBranch(branch)}-${Date.now()}`).replace(/\\/g, '/');
842
+ const _gitOpts = { cwd: root, timeout: 30000, windowsHide: true };
843
+
844
+ try {
845
+ await execAsync(`git fetch origin "${mainBranch}" "${branch}"`, _gitOpts);
846
+ try {
847
+ await execAsync(`git worktree add "${tmpWt}" "${branch}"`, { ..._gitOpts, timeout: 60000 });
848
+ } catch (wtErr) {
849
+ // Branch may already be checked out in a stale worktree — prune and retry once
850
+ if (String(wtErr.message || wtErr).includes('already checked out')) {
851
+ await execAsync(`git worktree prune`, _gitOpts);
852
+ await execAsync(`git worktree add "${tmpWt}" "${branch}"`, { ..._gitOpts, timeout: 60000 });
853
+ } else { throw wtErr; }
854
+ }
855
+ } catch (err) {
856
+ log('warn', `Post-merge rebase: setup failed for ${branch}: ${err.message}`);
857
+ try { await execAsync(`git worktree remove "${tmpWt}" --force`, _gitOpts); } catch {}
858
+ return { success: false, error: err.message };
859
+ }
860
+
861
+ try {
862
+ await execAsync(`git rebase "origin/${mainBranch}"`, { cwd: tmpWt, timeout: 120000, windowsHide: true });
863
+ await execAsync(`git push --force-with-lease origin "${branch}"`, { cwd: tmpWt, timeout: 30000, windowsHide: true });
864
+ log('info', `Post-merge rebase: rebased ${branch} onto ${mainBranch} and force-pushed`);
865
+ return { success: true };
866
+ } catch (err) {
867
+ try { await execAsync(`git rebase --abort`, { cwd: tmpWt, timeout: 10000, windowsHide: true }); } catch {}
868
+ log('warn', `Post-merge rebase failed for ${branch}: ${err.message}`);
869
+ return { success: false, error: err.message };
870
+ } finally {
871
+ try { shared.removeWorktree(tmpWt, root, wtRoot); } catch {}
872
+ try { await execAsync(`git worktree prune`, _gitOpts); } catch {}
873
+ }
874
+ }
875
+
876
+ const PENDING_REBASES_PATH = path.join(ENGINE_DIR, 'pending-rebases.json');
877
+
878
+ function queuePendingRebase(pr, project, mergedItemId) {
879
+ const pending = safeJson(PENDING_REBASES_PATH) || [];
880
+ if (pending.some(e => e.prId === pr.id)) return; // already queued
881
+ pending.push({ prId: pr.id, branch: pr.branch, projectName: project.name, mergedItemId, queuedAt: ts(), attempts: 0 });
882
+ safeWrite(PENDING_REBASES_PATH, pending);
883
+ }
884
+
885
+ async function processPendingRebases(config) {
886
+ const pending = safeJson(PENDING_REBASES_PATH) || [];
887
+ if (pending.length === 0) return;
888
+
889
+ const remaining = [];
890
+ for (const entry of pending) {
891
+ if (isBranchActive(entry.branch)) { remaining.push(entry); continue; }
892
+
893
+ const project = shared.getProjects(config).find(p => p.name === entry.projectName);
894
+ if (!project) continue;
895
+
896
+ const prs = safeJson(projectPrPath(project)) || [];
897
+ const pr = prs.find(p => p.id === entry.prId && p.status === 'active');
898
+ if (!pr) continue; // PR closed/merged since queuing
899
+
900
+ const result = await rebaseBranchOntoMain(pr, project, config);
901
+ if (!result.success) {
902
+ entry.attempts = (entry.attempts || 0) + 1;
903
+ if (entry.attempts < 3) {
904
+ remaining.push(entry);
905
+ } else {
906
+ shared.writeToInbox('engine', `rebase-fail-${pr.id}`,
907
+ `# Rebase Failed: ${pr.id}\n\nBranch \`${pr.branch}\` could not be rebased onto main after dependency ${entry.mergedItemId} merged.\n\nError: ${result.error}\n\nManual rebase may be needed.`);
908
+ }
909
+ }
910
+ }
911
+ safeWrite(PENDING_REBASES_PATH, remaining);
912
+ }
913
+
800
914
  // ─── Post-Merge / Post-Close Hooks ───────────────────────────────────────────
801
915
 
802
916
  async function handlePostMerge(pr, project, config, newStatus) {
@@ -871,6 +985,23 @@ async function handlePostMerge(pr, project, config, newStatus) {
871
985
  if (found) break;
872
986
  } catch (err) { log('warn', `Post-merge work item update: ${err.message}`); }
873
987
  }
988
+
989
+ // Rebase dependent PRs onto main now that this dependency is merged
990
+ try {
991
+ const dependentPrs = findDependentActivePrs(mergedItemId, config);
992
+ for (const { pr: depPr, project: depProject } of dependentPrs) {
993
+ if (isBranchActive(depPr.branch)) {
994
+ queuePendingRebase(depPr, depProject, mergedItemId);
995
+ log('info', `Post-merge rebase deferred: ${depPr.branch} locked by active agent`);
996
+ continue;
997
+ }
998
+ const result = await rebaseBranchOntoMain(depPr, depProject, config);
999
+ if (!result.success) {
1000
+ shared.writeToInbox('engine', `rebase-fail-${depPr.id}`,
1001
+ `# Rebase Failed: ${depPr.id}\n\nBranch \`${depPr.branch}\` could not be rebased onto main after dependency ${mergedItemId} merged.\n\nError: ${result.error}\n\nManual rebase may be needed.`);
1002
+ }
1003
+ }
1004
+ } catch (err) { log('warn', `Post-merge rebase phase error: ${err.message}`); }
874
1005
  }
875
1006
 
876
1007
  const agentId = (pr.agent || '').toLowerCase();
@@ -1615,5 +1746,7 @@ module.exports = {
1615
1746
  resolveWorkItemPath,
1616
1747
  isItemCompleted,
1617
1748
  classifyFailure,
1749
+ processPendingRebases,
1750
+ findDependentActivePrs,
1618
1751
  };
1619
1752
 
package/engine.js CHANGED
@@ -137,7 +137,7 @@ const { renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS,
137
137
  const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, handlePostMerge, checkPlanCompletion,
138
138
  syncPrsFromOutput, updatePrAfterReview, updatePrAfterFix, checkForLearnings, extractSkillsFromOutput,
139
139
  updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs,
140
- isItemCompleted, classifyFailure } = require('./engine/lifecycle');
140
+ isItemCompleted, classifyFailure, processPendingRebases } = require('./engine/lifecycle');
141
141
 
142
142
  // ─── Agent Spawner ──────────────────────────────────────────────────────────
143
143
 
@@ -1664,11 +1664,11 @@ async function discoverFromPrs(config, project) {
1664
1664
  // Grace period: after a build fix push, wait for CI to run before re-dispatching
1665
1665
  // Skip if build hasn't transitioned since last fix (still showing the old failure)
1666
1666
  if (pr._buildFixPushedAt && pr.buildStatus === 'failing') {
1667
- const gracePeriodMs = config.engine?.buildFixGracePeriod ?? ENGINE_DEFAULTS.buildFixGracePeriod;
1667
+ const gracePeriodMs = config.engine?.buildFixGracePeriod ?? DEFAULTS.buildFixGracePeriod;
1668
1668
  if (Date.now() - new Date(pr._buildFixPushedAt).getTime() < gracePeriodMs) continue;
1669
1669
  }
1670
1670
  if (pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing') {
1671
- const maxBuildFix = config.engine?.maxBuildFixAttempts ?? ENGINE_DEFAULTS.maxBuildFixAttempts;
1671
+ const maxBuildFix = config.engine?.maxBuildFixAttempts ?? DEFAULTS.maxBuildFixAttempts;
1672
1672
 
1673
1673
  // Check if max retry cap reached — escalate to human instead of dispatching another fix
1674
1674
  if ((pr.buildFixAttempts || 0) >= maxBuildFix) {
@@ -2694,6 +2694,7 @@ async function tickInner() {
2694
2694
  if (tickCount % 6 === 0 || needsAdoPollRetry()) {
2695
2695
  try { await pollPrStatus(config); } catch (err) { log('warn', `ADO PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
2696
2696
  try { await ghPollPrStatus(config); } catch (err) { log('warn', `GitHub PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
2697
+ try { await processPendingRebases(config); } catch (err) { log('warn', `Pending rebase processing error: ${err?.message || err}`); }
2697
2698
  // Sync PR status back to PRD items (missing → done when active PR exists)
2698
2699
  try { syncPrdFromPrs(config); } catch (err) { log('warn', `PRD sync error: ${err?.message || err}`); }
2699
2700
  // Check if any plans can be marked completed (all features done/in-pr)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.774",
3
+ "version": "0.1.776",
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"