@yemi33/minions 0.1.773 → 0.1.775

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,13 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.773 (2026-04-10)
3
+ ## 0.1.775 (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
9
10
  - filter CI bot noise from PR human comment detection
10
11
 
12
+ ### Other
13
+ - refactor: shared _renderPrLink for list and graph PR status display
14
+
11
15
  ## 0.1.771 (2026-04-10)
12
16
 
13
17
  ### Fixes
@@ -66,6 +66,13 @@ function renderPrd(prd, prog) {
66
66
  section.innerHTML = '';
67
67
  }
68
68
 
69
+ function _renderPrLink(pr, opts) {
70
+ var size = (opts && opts.size) || '10px';
71
+ var statusColor = pr.status === 'merged' ? 'var(--green)' : pr.status === 'abandoned' ? 'var(--red)' : 'var(--blue)';
72
+ var statusIcon = pr.status === 'merged' ? '✓' : pr.status === 'abandoned' ? '✗' : '○';
73
+ return '<a href="' + escHtml(pr.url || '#') + '" target="_blank" rel="noopener" onclick="event.stopPropagation()" style="font-size:' + size + ';color:' + statusColor + ';text-decoration:underline;cursor:pointer;margin-left:4px" title="' + escHtml((pr.title || '') + ' (' + (pr.status || 'active') + ')') + '">' + statusIcon + ' ' + escHtml(pr.id) + '</a>';
74
+ }
75
+
69
76
  function renderPrdProgress(prog) {
70
77
  const el = document.getElementById('prd-progress-content');
71
78
  const countEl = document.getElementById('prd-progress-count');
@@ -149,11 +156,7 @@ function renderPrdProgress(prog) {
149
156
  }
150
157
 
151
158
  const renderItem = (i) => {
152
- const prLinks = (i.prs || []).map(function(pr) {
153
- var statusColor = pr.status === 'merged' ? 'var(--green)' : pr.status === 'abandoned' ? 'var(--red)' : 'var(--blue)';
154
- var statusIcon = pr.status === 'merged' ? '✓' : pr.status === 'abandoned' ? '✗' : '○';
155
- return '<a class="pr-title" href="' + escHtml(pr.url || '#') + '" target="_blank" style="font-size:10px;margin-left:4px;color:' + statusColor + '" title="' + escHtml((pr.title || '') + ' (' + (pr.status || 'active') + ')') + '">' + statusIcon + ' ' + escHtml(pr.id) + '</a>';
156
- }).join(' ');
159
+ const prLinks = (i.prs || []).map(function(pr) { return _renderPrLink(pr); }).join(' ');
157
160
  const projBadges = (i.projects || []).map(p =>
158
161
  '<span class="prd-project-badge">' + escHtml(p) + '</span>'
159
162
  ).join(' ');
@@ -394,9 +397,7 @@ function renderPrdProgress(prog) {
394
397
  })() +
395
398
  (deps ? '<span style="font-size:8px;color:var(--muted)" title="Depends on: ' + escHtml(deps) + '">deps: ' + escHtml(deps) + '</span>' : '') +
396
399
  '</div>' +
397
- ((i.prs || []).length ? '<div style="margin-top:3px">' + (i.prs || []).map(function(pr) {
398
- return '<a href="' + escHtml(pr.url || '#') + '" target="_blank" rel="noopener" onclick="event.stopPropagation()" style="font-size:9px;color:var(--green);text-decoration:underline;cursor:pointer" title="' + escHtml(pr.title || '') + '">' + escHtml(pr.id) + '</a>';
399
- }).join(' ') + '</div>' : '') +
400
+ ((i.prs || []).length ? '<div style="margin-top:3px">' + (i.prs || []).map(function(pr) { return _renderPrLink(pr, { size: '9px' }); }).join(' ') + '</div>' : '') +
400
401
  (i.status === 'decomposed' ? (function() {
401
402
  var children = (window._lastWorkItems || []).filter(function(w) { return w.parent_id === i.id; });
402
403
  if (!children.length) return '';
@@ -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.773",
3
+ "version": "0.1.775",
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"