@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 +3 -1
- package/engine/ado.js +9 -3
- package/engine/github.js +13 -5
- package/engine/lifecycle.js +134 -1
- package/engine.js +4 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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
|
-
|
|
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
|
-
//
|
|
466
|
-
if (
|
|
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
|
|
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
|
-
|
|
448
|
-
//
|
|
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;
|
|
462
|
+
if (/!\[.*\]\(https?:\/\/.*badge/i.test(body)) return false;
|
|
455
463
|
return true;
|
|
456
464
|
});
|
|
457
465
|
|
package/engine/lifecycle.js
CHANGED
|
@@ -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 ??
|
|
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 ??
|
|
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.
|
|
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"
|