@yemi33/minions 0.1.774 → 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 +2 -1
- package/engine/lifecycle.js +134 -1
- package/engine.js +4 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
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.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"
|