@yemi33/minions 0.1.1002 → 0.1.1004
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 -2
- package/dashboard.js +1 -1
- package/engine/ado.js +12 -13
- package/engine/github.js +2 -12
- package/engine/lifecycle.js +83 -44
- package/engine/queries.js +6 -5
- package/engine/shared.js +35 -8
- package/engine.js +34 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1004 (2026-04-15)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
6
|
- fix stopAfter=0 watch expiry — run forever for all conditions (#1117)
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
- gate auto-fix conflict dispatch behind autoFixConflicts flag
|
|
26
26
|
|
|
27
27
|
### Fixes
|
|
28
|
+
- only gate reviews/fixes when no free agent slots remain
|
|
28
29
|
- extend auto-link fallback to ADO projects
|
|
29
30
|
- auto-link existing GitHub PR when agent completes without creating one
|
|
30
31
|
- preserve ordered lists across blank lines in markdown renderer
|
|
@@ -44,9 +45,9 @@
|
|
|
44
45
|
- move review verdict check before updateWorkItemStatus(DONE)
|
|
45
46
|
- address review feedback — move writeToInbox outside lock, add absolute condition auto-expire
|
|
46
47
|
- fix watches feature gaps — human notifications, branch stub, status-change init, unique keys
|
|
47
|
-
- skip isAlreadyDispatched in needsReReview to allow re-review within 1hr
|
|
48
48
|
|
|
49
49
|
### Other
|
|
50
|
+
- refactor: migrate PR links to array format and consolidate shared helpers
|
|
50
51
|
- docs: update CLAUDE.md with recent Minions architecture changes
|
|
51
52
|
- chore: stop tracking runtime pipeline state
|
|
52
53
|
- chore: strengthen prompts and routing
|
package/dashboard.js
CHANGED
|
@@ -2533,7 +2533,7 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2533
2533
|
const prLinks = shared.getPrLinks();
|
|
2534
2534
|
const implContext = (plan.missing_features || []).map(f => {
|
|
2535
2535
|
const wi = planWis.find(w => w.id === f.id);
|
|
2536
|
-
const pr = allPrs.find(p => prLinks[p.id]
|
|
2536
|
+
const pr = allPrs.find(p => (prLinks[p.id] || []).includes(f.id) || (p.prdItems || []).includes(f.id));
|
|
2537
2537
|
return `- **${f.id}**: ${f.name} [status: ${wi?.status || f.status}]${pr ? ` (PR: ${pr.id}, branch: \`${pr.branch}\`)` : ''}`;
|
|
2538
2538
|
}).join('\n');
|
|
2539
2539
|
|
package/engine/ado.js
CHANGED
|
@@ -17,6 +17,11 @@ function engine() {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const stripRefsHeads = s => (s || '').replace('refs/heads/', '');
|
|
20
|
+
const getAdoPrUrl = (project, prNumber) => {
|
|
21
|
+
if (project.prUrlBase) return `${project.prUrlBase}${prNumber}`;
|
|
22
|
+
const repoPath = encodeURIComponent(project.repoName || project.repositoryId || '');
|
|
23
|
+
return `https://dev.azure.com/${project.adoOrg}/${project.adoProject}/_git/${repoPath}/pullrequest/${prNumber}`;
|
|
24
|
+
};
|
|
20
25
|
|
|
21
26
|
// ── Build/Review Status Helpers ───────────────────────────────────────────────
|
|
22
27
|
|
|
@@ -748,16 +753,7 @@ async function reconcilePrs(config) {
|
|
|
748
753
|
}
|
|
749
754
|
|
|
750
755
|
// Backfill prdItems from pr-links for any PR with empty array
|
|
751
|
-
const
|
|
752
|
-
let backfilled = 0;
|
|
753
|
-
for (const pr of existingPrs) {
|
|
754
|
-
const linked = prLinks[pr.id];
|
|
755
|
-
if (linked && !(pr.prdItems || []).includes(linked)) {
|
|
756
|
-
pr.prdItems = Array.isArray(pr.prdItems) ? pr.prdItems : [];
|
|
757
|
-
pr.prdItems.push(linked);
|
|
758
|
-
backfilled++;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
756
|
+
const backfilled = shared.backfillPrPrdItems(existingPrs, shared.getPrLinks());
|
|
761
757
|
|
|
762
758
|
if (projectAdded > 0 || projectUpdated > 0 || backfilled > 0) {
|
|
763
759
|
mutateJsonFileLocked(prPath, (currentPrs) => {
|
|
@@ -865,7 +861,7 @@ async function fetchSinglePrBuildStatus(project, prNumber) {
|
|
|
865
861
|
}
|
|
866
862
|
|
|
867
863
|
const votes = (prData.reviewers || []).map(r => r.vote);
|
|
868
|
-
const prUrl =
|
|
864
|
+
const prUrl = getAdoPrUrl(project, prNumber);
|
|
869
865
|
|
|
870
866
|
return {
|
|
871
867
|
prNumber,
|
|
@@ -900,6 +896,10 @@ const getAdoThrottleState = () => _adoThrottle.getState();
|
|
|
900
896
|
*/
|
|
901
897
|
async function findOpenPrOnBranch(project, branch) {
|
|
902
898
|
if (!project.adoOrg || !project.adoProject || !project.repositoryId || !branch) return null;
|
|
899
|
+
if (isAdoThrottled()) {
|
|
900
|
+
log('debug', `[ado] Skipping branch PR lookup for ${project.name || project.repoName || 'unknown project'}:${branch} — throttled`);
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
903
|
const token = await getAdoToken();
|
|
904
904
|
if (!token) return null;
|
|
905
905
|
const orgBase = shared.getAdoOrgBase(project);
|
|
@@ -909,7 +909,7 @@ async function findOpenPrOnBranch(project, branch) {
|
|
|
909
909
|
const pr = (data.value || [])[0];
|
|
910
910
|
if (!pr) return null;
|
|
911
911
|
const prNumber = pr.pullRequestId;
|
|
912
|
-
const prUrl = project
|
|
912
|
+
const prUrl = getAdoPrUrl(project, prNumber);
|
|
913
913
|
return { prNumber, url: prUrl };
|
|
914
914
|
}
|
|
915
915
|
|
|
@@ -940,4 +940,3 @@ module.exports = {
|
|
|
940
940
|
_resetAdoThrottle, // exported for testing
|
|
941
941
|
_setAdoThrottleForTest, // exported for testing
|
|
942
942
|
};
|
|
943
|
-
|
package/engine/github.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const shared = require('./shared');
|
|
8
|
-
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, mutateJsonFileLocked, MINIONS_DIR, addPrLink, getPrLinks, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES, createThrottleTracker } = shared;
|
|
8
|
+
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, mutateJsonFileLocked, MINIONS_DIR, addPrLink, getPrLinks, backfillPrPrdItems, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES, createThrottleTracker } = shared;
|
|
9
9
|
const { getPrs } = require('./queries');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -691,16 +691,7 @@ async function reconcilePrs(config) {
|
|
|
691
691
|
}
|
|
692
692
|
|
|
693
693
|
// Backfill prdItems from pr-links for any PR with empty array
|
|
694
|
-
const
|
|
695
|
-
let backfilled = 0;
|
|
696
|
-
for (const pr of currentPrs) {
|
|
697
|
-
const linked = prLinks[pr.id];
|
|
698
|
-
if (linked && !(pr.prdItems || []).includes(linked)) {
|
|
699
|
-
pr.prdItems = Array.isArray(pr.prdItems) ? pr.prdItems : [];
|
|
700
|
-
pr.prdItems.push(linked);
|
|
701
|
-
backfilled++;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
694
|
+
const backfilled = backfillPrPrdItems(currentPrs, getPrLinks());
|
|
704
695
|
|
|
705
696
|
if (projectAdded > 0 || backfilled > 0) {
|
|
706
697
|
mutateJsonFileLocked(prPath, (lockedPrs) => {
|
|
@@ -761,4 +752,3 @@ module.exports = {
|
|
|
761
752
|
_ghPollBackoff,
|
|
762
753
|
_ghThrottle, // exported for testing
|
|
763
754
|
};
|
|
764
|
-
|
package/engine/lifecycle.js
CHANGED
|
@@ -111,8 +111,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
111
111
|
const prs = safeJson(prPath) || [];
|
|
112
112
|
const prLinks = getPrLinks();
|
|
113
113
|
for (const pr of prs) {
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
114
|
+
const linkedItemIds = prLinks[pr.id] || [];
|
|
115
|
+
if (linkedItemIds.some(itemId => doneItems.find(w => w.id === itemId))) {
|
|
116
116
|
prsCreated.push(pr);
|
|
117
117
|
}
|
|
118
118
|
}
|
|
@@ -199,8 +199,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
199
199
|
const prLinks = getPrLinks();
|
|
200
200
|
const prs = (safeJson(shared.projectPrPath(p)) || [])
|
|
201
201
|
.filter(pr => {
|
|
202
|
-
const
|
|
203
|
-
return pr.status === PR_STATUS.ACTIVE &&
|
|
202
|
+
const linkedIds = prLinks[pr.id] || [];
|
|
203
|
+
return pr.status === PR_STATUS.ACTIVE && linkedIds.some(itemId => doneItems.find(w => w.id === itemId));
|
|
204
204
|
});
|
|
205
205
|
if (prs.length > 0) {
|
|
206
206
|
projectPrs[p.name] = { project: p, prs, mainBranch: shared.resolveMainBranch(p.localPath, p.mainBranch) };
|
|
@@ -413,8 +413,8 @@ function cleanupPlanWorktrees(planFile, plan, projects, config) {
|
|
|
413
413
|
const prs = safeJson(shared.projectPrPath(p)) || [];
|
|
414
414
|
const prLinks = getPrLinks();
|
|
415
415
|
for (const pr of prs) {
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
416
|
+
const linkedIds = prLinks[pr.id] || [];
|
|
417
|
+
if (linkedIds.some(itemId => planItems.find(w => w.id === itemId)) && pr.branch) {
|
|
418
418
|
branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
|
|
419
419
|
}
|
|
420
420
|
}
|
|
@@ -1109,13 +1109,14 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
1109
1109
|
if (newStatus !== PR_STATUS.MERGED) return;
|
|
1110
1110
|
|
|
1111
1111
|
// Resolve linked work item from pr-links or PR branch name
|
|
1112
|
-
|
|
1113
|
-
if (
|
|
1112
|
+
const mergedItemIds = [...(getPrLinks()[pr.id] || [])];
|
|
1113
|
+
if (mergedItemIds.length === 0 && pr.branch) {
|
|
1114
1114
|
const branchMatch = pr.branch.match(/(P-[a-z0-9]{6,})/i) || pr.branch.match(/(W-[a-z0-9]{6,})/i) || pr.branch.match(/(PL-[a-z0-9]{6,})/i);
|
|
1115
|
-
if (branchMatch)
|
|
1115
|
+
if (branchMatch) mergedItemIds.push(branchMatch[1]);
|
|
1116
1116
|
}
|
|
1117
1117
|
|
|
1118
|
-
if (
|
|
1118
|
+
if (mergedItemIds.length > 0) {
|
|
1119
|
+
const mergedItemSet = new Set(mergedItemIds);
|
|
1119
1120
|
// Mark PRD feature as implemented
|
|
1120
1121
|
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
1121
1122
|
try {
|
|
@@ -1124,49 +1125,62 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
1124
1125
|
for (const pf of planFiles) {
|
|
1125
1126
|
const plan = safeJson(path.join(prdDir, pf));
|
|
1126
1127
|
if (!plan?.missing_features) continue;
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
feature.status
|
|
1128
|
+
let changed = false;
|
|
1129
|
+
for (const feature of plan.missing_features) {
|
|
1130
|
+
if (mergedItemSet.has(feature.id) && feature.status !== WI_STATUS.DONE) {
|
|
1131
|
+
feature.status = WI_STATUS.DONE;
|
|
1132
|
+
changed = true;
|
|
1133
|
+
updated++;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (changed) {
|
|
1130
1137
|
shared.safeWrite(path.join(prdDir, pf), plan);
|
|
1131
|
-
updated++;
|
|
1132
1138
|
}
|
|
1133
1139
|
}
|
|
1134
|
-
if (updated > 0) log('info', `Post-merge: marked ${
|
|
1140
|
+
if (updated > 0) log('info', `Post-merge: marked ${mergedItemIds.join(', ')} as done for ${pr.id}`);
|
|
1135
1141
|
} catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
|
|
1136
1142
|
|
|
1137
1143
|
// Mark work item as done
|
|
1138
1144
|
const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
|
|
1139
1145
|
for (const p of shared.getProjects(config)) wiPaths.push(shared.projectWorkItemsPath(p));
|
|
1146
|
+
const remainingMergedIds = new Set(mergedItemIds);
|
|
1140
1147
|
for (const wiPath of wiPaths) {
|
|
1141
1148
|
try {
|
|
1142
|
-
let found = false;
|
|
1143
1149
|
mutateWorkItems(wiPath, items => {
|
|
1144
|
-
const item
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1150
|
+
for (const item of items) {
|
|
1151
|
+
if (!remainingMergedIds.has(item.id)) continue;
|
|
1152
|
+
if (item.status !== WI_STATUS.DONE) {
|
|
1153
|
+
log('info', `Post-merge: marking work item ${item.id} as done (was ${item.status}) for ${pr.id}`);
|
|
1154
|
+
item.status = WI_STATUS.DONE;
|
|
1155
|
+
item.completedAt = ts();
|
|
1156
|
+
item._mergedVia = pr.id;
|
|
1157
|
+
}
|
|
1158
|
+
remainingMergedIds.delete(item.id);
|
|
1151
1159
|
}
|
|
1152
1160
|
});
|
|
1153
|
-
if (
|
|
1161
|
+
if (remainingMergedIds.size === 0) break;
|
|
1154
1162
|
} catch (err) { log('warn', `Post-merge work item update: ${err.message}`); }
|
|
1155
1163
|
}
|
|
1156
1164
|
|
|
1157
1165
|
// Rebase dependent PRs onto main now that this dependency is merged
|
|
1158
1166
|
try {
|
|
1159
|
-
const
|
|
1160
|
-
for (const
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
continue;
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1167
|
+
const rebasedPrs = new Set();
|
|
1168
|
+
for (const mergedItemId of mergedItemIds) {
|
|
1169
|
+
const dependentPrs = findDependentActivePrs(mergedItemId, config);
|
|
1170
|
+
for (const { pr: depPr, project: depProject } of dependentPrs) {
|
|
1171
|
+
const rebaseKey = `${depProject.name}:${depPr.id}`;
|
|
1172
|
+
if (rebasedPrs.has(rebaseKey)) continue;
|
|
1173
|
+
rebasedPrs.add(rebaseKey);
|
|
1174
|
+
if (isBranchActive(depPr.branch)) {
|
|
1175
|
+
queuePendingRebase(depPr, depProject, mergedItemId);
|
|
1176
|
+
log('info', `Post-merge rebase deferred: ${depPr.branch} locked by active agent`);
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
const result = await rebaseBranchOntoMain(depPr, depProject, config);
|
|
1180
|
+
if (!result.success) {
|
|
1181
|
+
shared.writeToInbox('engine', `rebase-fail-${depPr.id}`,
|
|
1182
|
+
`# 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.`);
|
|
1183
|
+
}
|
|
1170
1184
|
}
|
|
1171
1185
|
}
|
|
1172
1186
|
} catch (err) { log('warn', `Post-merge rebase phase error: ${err.message}`); }
|
|
@@ -1780,7 +1794,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1780
1794
|
// Detect implement tasks that completed without creating a PR
|
|
1781
1795
|
if (effectiveSuccess && (type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || type === WORK_TYPE.FIX) && prsCreatedCount === 0 && meta?.item?.id && !meta?.item?.skipPr && meta?.project?.localPath) {
|
|
1782
1796
|
// Check if a PR already exists linked to this work item (from a previous attempt)
|
|
1783
|
-
let existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
|
1797
|
+
let existingPrFound = Object.values(getPrLinks()).some(linkedIds => (linkedIds || []).includes(meta.item.id));
|
|
1784
1798
|
// Also check pull-requests.json for PRs with matching prdItems or branch
|
|
1785
1799
|
if (!existingPrFound) {
|
|
1786
1800
|
const allProjects = shared.getProjects(config);
|
|
@@ -1800,22 +1814,49 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1800
1814
|
if (projectObj) {
|
|
1801
1815
|
try {
|
|
1802
1816
|
let found = null;
|
|
1803
|
-
|
|
1817
|
+
const host = projectObj.repoHost || 'ado';
|
|
1818
|
+
if (host === 'github') {
|
|
1804
1819
|
const ghSlug = projectObj.prUrlBase?.match(/github\.com\/([^/]+\/[^/]+)\/pull/)?.[1];
|
|
1805
1820
|
if (ghSlug) {
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1821
|
+
// Retry up to 3 times — newly created PRs can take a few seconds to appear in the API
|
|
1822
|
+
for (let attempt = 0; attempt < 3 && !found; attempt++) {
|
|
1823
|
+
if (attempt > 0) await new Promise(r => setTimeout(r, 3000));
|
|
1824
|
+
let raw = '';
|
|
1825
|
+
try {
|
|
1826
|
+
raw = await execAsync(`gh pr list --head "${meta.branch}" --repo ${ghSlug} --json number,url,state --limit 1`, { timeout: 15000, windowsHide: true });
|
|
1827
|
+
const parsed = JSON.parse(raw || '[]');
|
|
1828
|
+
const hits = Array.isArray(parsed) ? parsed : [];
|
|
1829
|
+
if (hits.length > 0 && hits[0].state === 'OPEN') {
|
|
1830
|
+
found = { prNumber: hits[0].number, url: hits[0].url };
|
|
1831
|
+
} else if (attempt === 2) {
|
|
1832
|
+
log('warn', `Auto-link fallback: no open PR found on branch ${meta.branch} after 3 attempts (raw: ${(raw || '').slice(0, 200)})`);
|
|
1833
|
+
}
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
if (attempt === 2) {
|
|
1836
|
+
const rawSuffix = raw ? ` (raw: ${raw.slice(0, 200)})` : '';
|
|
1837
|
+
log('warn', `Auto-link fallback: gh pr list lookup failed on branch ${meta.branch} after 3 attempts: ${err.message}${rawSuffix}`);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1809
1841
|
}
|
|
1810
|
-
} else {
|
|
1842
|
+
} else if (host === 'ado') {
|
|
1811
1843
|
found = await require('./ado').findOpenPrOnBranch(projectObj, meta.branch);
|
|
1844
|
+
} else {
|
|
1845
|
+
log('debug', `Skipping branch PR lookup for unsupported repo host "${host}" on ${projectObj.name}`);
|
|
1812
1846
|
}
|
|
1813
1847
|
if (found) {
|
|
1814
1848
|
const fullId = `PR-${found.prNumber}`;
|
|
1815
1849
|
const prPath = shared.projectPrPath(projectObj);
|
|
1816
1850
|
mutateJsonFileLocked(prPath, prs => {
|
|
1817
1851
|
if (!Array.isArray(prs)) prs = [];
|
|
1818
|
-
|
|
1852
|
+
const existingPr = prs.find(p => p.id === fullId);
|
|
1853
|
+
if (existingPr) {
|
|
1854
|
+
if (meta.item?.id) {
|
|
1855
|
+
if (!Array.isArray(existingPr.prdItems)) existingPr.prdItems = [];
|
|
1856
|
+
if (!existingPr.prdItems.includes(meta.item.id)) existingPr.prdItems.push(meta.item.id);
|
|
1857
|
+
}
|
|
1858
|
+
return prs;
|
|
1859
|
+
}
|
|
1819
1860
|
prs.push({
|
|
1820
1861
|
id: fullId, prNumber: found.prNumber, title: meta.item?.title || '',
|
|
1821
1862
|
agent: agentId, branch: meta.branch, reviewStatus: 'pending',
|
|
@@ -1825,7 +1866,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1825
1866
|
});
|
|
1826
1867
|
return prs;
|
|
1827
1868
|
});
|
|
1828
|
-
if (meta.item?.id) addPrLink(fullId, meta.item.id);
|
|
1829
1869
|
log('info', `Auto-linked existing PR ${fullId} on branch ${meta.branch} for ${meta.item?.id}`);
|
|
1830
1870
|
existingPrFound = true;
|
|
1831
1871
|
}
|
|
@@ -2069,4 +2109,3 @@ module.exports = {
|
|
|
2069
2109
|
processPendingRebases,
|
|
2070
2110
|
findDependentActivePrs,
|
|
2071
2111
|
};
|
|
2072
|
-
|
package/engine/queries.js
CHANGED
|
@@ -949,13 +949,15 @@ function getPrdInfo(config) {
|
|
|
949
949
|
for (const pr of allPrs) prById[pr.id] = pr;
|
|
950
950
|
|
|
951
951
|
const prdToPr = {};
|
|
952
|
-
const prLinks = shared.getPrLinks(); // { "PR-xxxx": "P-xxxx" }
|
|
953
|
-
for (const [prId,
|
|
952
|
+
const prLinks = shared.getPrLinks(); // { "PR-xxxx": ["P-xxxx", "P-yyyy"] }
|
|
953
|
+
for (const [prId, itemIds] of Object.entries(prLinks)) {
|
|
954
954
|
const pr = prById[prId];
|
|
955
955
|
const project = projects.find(p => p.name === pr?._project) || projects[0] || null;
|
|
956
956
|
const url = pr?.url || (project?.prUrlBase ? project.prUrlBase + prId.replace('PR-', '') : '');
|
|
957
|
-
|
|
958
|
-
|
|
957
|
+
for (const itemId of (itemIds || [])) {
|
|
958
|
+
if (!prdToPr[itemId]) prdToPr[itemId] = [];
|
|
959
|
+
prdToPr[itemId].push({ id: prId, url, title: pr?.title || '', status: pr?.status || 'active', _project: pr?._project || '' });
|
|
960
|
+
}
|
|
959
961
|
}
|
|
960
962
|
// Fallback: work item _pr field for anything still missing
|
|
961
963
|
for (const wi of Object.values(wiById)) {
|
|
@@ -1091,4 +1093,3 @@ module.exports = {
|
|
|
1091
1093
|
// Work items & PRD
|
|
1092
1094
|
getWorkItems, getPrdInfo,
|
|
1093
1095
|
};
|
|
1094
|
-
|
package/engine/shared.js
CHANGED
|
@@ -859,6 +859,17 @@ function parseSkillFrontmatter(content, filename) {
|
|
|
859
859
|
// Stable single-writer file: maps PR IDs → PRD item IDs.
|
|
860
860
|
// Never touched by polling loops — only written when a PR is first linked to a PRD item.
|
|
861
861
|
|
|
862
|
+
function normalizePrLinkItems(value) {
|
|
863
|
+
const items = Array.isArray(value) ? value : [value];
|
|
864
|
+
return [...new Set(items.filter(Boolean))];
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function mergePrLinkItems(links, prId, itemIds) {
|
|
868
|
+
if (!prId) return;
|
|
869
|
+
const merged = new Set([...(links[prId] || []), ...normalizePrLinkItems(itemIds)]);
|
|
870
|
+
if (merged.size > 0) links[prId] = [...merged];
|
|
871
|
+
}
|
|
872
|
+
|
|
862
873
|
function getPrLinks() {
|
|
863
874
|
const links = {};
|
|
864
875
|
// Primary source: derive from all projects/*/pull-requests.json prdItems
|
|
@@ -870,9 +881,7 @@ function getPrLinks() {
|
|
|
870
881
|
const prs = JSON.parse(fs.readFileSync(path.join(projectsDir, d.name, 'pull-requests.json'), 'utf8'));
|
|
871
882
|
for (const pr of prs) {
|
|
872
883
|
if (!pr.id) continue;
|
|
873
|
-
|
|
874
|
-
if (itemId) links[pr.id] = itemId;
|
|
875
|
-
}
|
|
884
|
+
mergePrLinkItems(links, pr.id, pr.prdItems || []);
|
|
876
885
|
}
|
|
877
886
|
} catch { /* missing or invalid */ }
|
|
878
887
|
}
|
|
@@ -881,17 +890,35 @@ function getPrLinks() {
|
|
|
881
890
|
try {
|
|
882
891
|
const static_ = JSON.parse(fs.readFileSync(PR_LINKS_PATH, 'utf8'));
|
|
883
892
|
for (const [k, v] of Object.entries(static_)) {
|
|
884
|
-
if (!links[k]) links
|
|
893
|
+
if (!links[k]) mergePrLinkItems(links, k, v);
|
|
885
894
|
}
|
|
886
895
|
} catch { /* missing */ }
|
|
887
896
|
return links;
|
|
888
897
|
}
|
|
889
898
|
|
|
899
|
+
function backfillPrPrdItems(prs, prLinks) {
|
|
900
|
+
let backfilled = 0;
|
|
901
|
+
for (const pr of prs) {
|
|
902
|
+
const linkedItems = prLinks[pr.id] || [];
|
|
903
|
+
if (linkedItems.length > 0) {
|
|
904
|
+
pr.prdItems = Array.isArray(pr.prdItems) ? pr.prdItems : [];
|
|
905
|
+
for (const linked of linkedItems) {
|
|
906
|
+
if (!pr.prdItems.includes(linked)) {
|
|
907
|
+
pr.prdItems.push(linked);
|
|
908
|
+
backfilled++;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return backfilled;
|
|
914
|
+
}
|
|
915
|
+
|
|
890
916
|
function addPrLink(prId, itemId) {
|
|
891
917
|
if (!prId || !itemId) return;
|
|
892
|
-
const links =
|
|
893
|
-
|
|
894
|
-
|
|
918
|
+
const links = safeJson(PR_LINKS_PATH) || {};
|
|
919
|
+
const current = normalizePrLinkItems(links[prId]);
|
|
920
|
+
if (current.includes(itemId)) return; // already correct, no write needed
|
|
921
|
+
links[prId] = [...current, itemId];
|
|
895
922
|
safeWrite(PR_LINKS_PATH, links);
|
|
896
923
|
}
|
|
897
924
|
|
|
@@ -1184,5 +1211,5 @@ module.exports = {
|
|
|
1184
1211
|
formatTranscriptEntry,
|
|
1185
1212
|
_logBuffer, // exported for testing
|
|
1186
1213
|
createThrottleTracker,
|
|
1214
|
+
backfillPrPrdItems,
|
|
1187
1215
|
};
|
|
1188
|
-
|
package/engine.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
const fs = require('fs');
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const shared = require('./engine/shared');
|
|
27
|
-
const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS
|
|
27
|
+
const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS,
|
|
28
28
|
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, DISPATCH_RESULT, AGENT_STATUS,
|
|
29
29
|
FAILURE_CLASS } = shared;
|
|
30
30
|
const queries = require('./engine/queries');
|
|
@@ -241,8 +241,8 @@ function _maxTurnsForType(type, engineConfig) {
|
|
|
241
241
|
// Priority: per-type config override → global config override → built-in per-type default → global default
|
|
242
242
|
const perType = engineConfig.maxTurnsByType || {};
|
|
243
243
|
if (perType[type]) return perType[type];
|
|
244
|
-
const globalOverride = engineConfig.maxTurns && engineConfig.maxTurns !==
|
|
245
|
-
return globalOverride || _MAX_TURNS_BY_TYPE[type] ||
|
|
244
|
+
const globalOverride = engineConfig.maxTurns && engineConfig.maxTurns !== ENGINE_DEFAULTS.maxTurns ? engineConfig.maxTurns : null;
|
|
245
|
+
return globalOverride || _MAX_TURNS_BY_TYPE[type] || ENGINE_DEFAULTS.maxTurns;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
// Resolve dependency plan item IDs to their PR branches
|
|
@@ -373,8 +373,8 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
373
373
|
let cwd = rootDir;
|
|
374
374
|
let worktreePath = null;
|
|
375
375
|
let branchName = meta?.branch ? sanitizeBranch(meta.branch) : null;
|
|
376
|
-
const worktreeCreateTimeout = Math.max(60000, Number(engineConfig.worktreeCreateTimeout) ||
|
|
377
|
-
const worktreeCreateRetries = Math.max(0, Math.min(3, Number(engineConfig.worktreeCreateRetries) ||
|
|
376
|
+
const worktreeCreateTimeout = Math.max(60000, Number(engineConfig.worktreeCreateTimeout) || ENGINE_DEFAULTS.worktreeCreateTimeout);
|
|
377
|
+
const worktreeCreateRetries = Math.max(0, Math.min(3, Number(engineConfig.worktreeCreateRetries) || ENGINE_DEFAULTS.worktreeCreateRetries));
|
|
378
378
|
const _gitOpts = { stdio: 'pipe', timeout: 30000, windowsHide: true, env: shared.gitEnv() };
|
|
379
379
|
const _worktreeGitOpts = { ..._gitOpts, timeout: worktreeCreateTimeout };
|
|
380
380
|
|
|
@@ -1021,7 +1021,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1021
1021
|
// Build resume args
|
|
1022
1022
|
const resumeArgs = [
|
|
1023
1023
|
'--output-format', claudeConfig?.outputFormat || 'stream-json',
|
|
1024
|
-
'--max-turns', String(engineConfig?.maxTurns ||
|
|
1024
|
+
'--max-turns', String(engineConfig?.maxTurns || ENGINE_DEFAULTS.maxTurns),
|
|
1025
1025
|
'--verbose',
|
|
1026
1026
|
'--permission-mode', claudeConfig?.permissionMode || 'bypassPermissions',
|
|
1027
1027
|
'--resume', steerSessionId,
|
|
@@ -1427,7 +1427,7 @@ function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
|
|
|
1427
1427
|
|
|
1428
1428
|
let exactPr = allPrs.find(pr => (pr.prdItems || []).includes(wi.id));
|
|
1429
1429
|
if (!exactPr) {
|
|
1430
|
-
const linkedPrId = Object.keys(prLinks).find(prId => prLinks[prId]
|
|
1430
|
+
const linkedPrId = Object.keys(prLinks).find(prId => (prLinks[prId] || []).includes(wi.id));
|
|
1431
1431
|
if (linkedPrId) exactPr = allPrs.find(pr => pr.id === linkedPrId) || { id: linkedPrId };
|
|
1432
1432
|
}
|
|
1433
1433
|
// Branch-based matching: PR branch contains the work item ID (e.g. work/P-k7m2v9a1)
|
|
@@ -1901,8 +1901,8 @@ async function discoverFromPrs(config, project) {
|
|
|
1901
1901
|
// Resolve poll-enabled per project — stale reviewStatus is untrustworthy without poller
|
|
1902
1902
|
const isAdoProject = project?.repoHost !== 'github';
|
|
1903
1903
|
const pollEnabled = isAdoProject
|
|
1904
|
-
? (config.engine?.adoPollEnabled ??
|
|
1905
|
-
: (config.engine?.ghPollEnabled ??
|
|
1904
|
+
? (config.engine?.adoPollEnabled ?? ENGINE_DEFAULTS.adoPollEnabled)
|
|
1905
|
+
: (config.engine?.ghPollEnabled ?? ENGINE_DEFAULTS.ghPollEnabled);
|
|
1906
1906
|
const evalLoopEnabled = config.engine?.evalLoop !== false;
|
|
1907
1907
|
|
|
1908
1908
|
// Collect active PR dispatches to prevent simultaneous review+fix on same PR
|
|
@@ -1935,7 +1935,7 @@ async function discoverFromPrs(config, project) {
|
|
|
1935
1935
|
const awaitingReReview = reviewStatus === 'waiting' && !!pr.minionsReview?.fixedAt;
|
|
1936
1936
|
|
|
1937
1937
|
// Review→fix cycle cap — stop review/fix dispatch after N iterations, but allow build fixes and conflict fixes
|
|
1938
|
-
const evalMax = config.engine?.evalMaxIterations ??
|
|
1938
|
+
const evalMax = config.engine?.evalMaxIterations ?? ENGINE_DEFAULTS.evalMaxIterations;
|
|
1939
1939
|
const evalCycles = pr._reviewFixCycles || 0;
|
|
1940
1940
|
const evalEscalated = evalCycles >= evalMax;
|
|
1941
1941
|
if (evalEscalated && !pr._evalEscalated) {
|
|
@@ -2099,12 +2099,12 @@ async function discoverFromPrs(config, project) {
|
|
|
2099
2099
|
// Grace period: after a build fix push, wait for CI to run before re-dispatching
|
|
2100
2100
|
// Skip if build hasn't transitioned since last fix (still showing the old failure)
|
|
2101
2101
|
if (pr._buildFixPushedAt && pr.buildStatus === 'failing') {
|
|
2102
|
-
const gracePeriodMs = config.engine?.buildFixGracePeriod ??
|
|
2102
|
+
const gracePeriodMs = config.engine?.buildFixGracePeriod ?? ENGINE_DEFAULTS.buildFixGracePeriod;
|
|
2103
2103
|
if (Date.now() - new Date(pr._buildFixPushedAt).getTime() < gracePeriodMs) continue;
|
|
2104
2104
|
}
|
|
2105
|
-
const autoFixBuilds = config.engine?.autoFixBuilds ??
|
|
2105
|
+
const autoFixBuilds = config.engine?.autoFixBuilds ?? ENGINE_DEFAULTS.autoFixBuilds;
|
|
2106
2106
|
if (autoFixBuilds && pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing') {
|
|
2107
|
-
const maxBuildFix = config.engine?.maxBuildFixAttempts ??
|
|
2107
|
+
const maxBuildFix = config.engine?.maxBuildFixAttempts ?? ENGINE_DEFAULTS.maxBuildFixAttempts;
|
|
2108
2108
|
|
|
2109
2109
|
// Check if max retry cap reached — escalate to human instead of dispatching another fix
|
|
2110
2110
|
if ((pr.buildFixAttempts || 0) >= maxBuildFix) {
|
|
@@ -2182,7 +2182,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2182
2182
|
}
|
|
2183
2183
|
|
|
2184
2184
|
// PRs with merge conflicts — dispatch fix to resolve (gated by autoFixConflicts)
|
|
2185
|
-
const autoFixConflicts = config.engine?.autoFixConflicts ??
|
|
2185
|
+
const autoFixConflicts = config.engine?.autoFixConflicts ?? ENGINE_DEFAULTS.autoFixConflicts;
|
|
2186
2186
|
if (autoFixConflicts && pr.status === PR_STATUS.ACTIVE && pr._mergeConflict && !fixDispatched) {
|
|
2187
2187
|
const key = `conflict-fix-${project?.name || 'default'}-${pr.id}`;
|
|
2188
2188
|
// Suppress re-dispatch for 10 min after last attempt — ADO/GitHub recomputes
|
|
@@ -2808,7 +2808,7 @@ function discoverCentralWorkItems(config) {
|
|
|
2808
2808
|
meta: {
|
|
2809
2809
|
dispatchKey: fanKey, source: 'central-work-item-fanout', item, parentKey: key,
|
|
2810
2810
|
branch: fanBranch,
|
|
2811
|
-
deadline: item.timeout ? Date.now() + item.timeout : Date.now() + (config.engine?.fanOutTimeout || config.engine?.agentTimeout ||
|
|
2811
|
+
deadline: item.timeout ? Date.now() + item.timeout : Date.now() + (config.engine?.fanOutTimeout || config.engine?.agentTimeout || ENGINE_DEFAULTS.agentTimeout)
|
|
2812
2812
|
}
|
|
2813
2813
|
});
|
|
2814
2814
|
}
|
|
@@ -3081,18 +3081,24 @@ async function discoverWork(config) {
|
|
|
3081
3081
|
} catch (e) { log('warn', 'plan completion sweep: ' + e.message); }
|
|
3082
3082
|
}
|
|
3083
3083
|
|
|
3084
|
-
// Gate reviews and fixes:
|
|
3084
|
+
// Gate reviews and fixes: only when at max concurrency — idle agents should pick up reviews
|
|
3085
|
+
// even if implement items are in progress (implements get priority via sort order, not by blocking)
|
|
3085
3086
|
const hasIncompleteImplements = queries.getWorkItems(config).some(i =>
|
|
3086
3087
|
['queued', 'pending', 'dispatched'].includes(i.status) && (i.type || '').startsWith('implement')
|
|
3087
3088
|
);
|
|
3088
3089
|
if (hasIncompleteImplements) {
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3090
|
+
const activeCount = (getDispatch().active || []).length;
|
|
3091
|
+
const maxConcurrent = config.engine?.maxConcurrent ?? ENGINE_DEFAULTS.maxConcurrent;
|
|
3092
|
+
const freeSlots = Math.max(0, maxConcurrent - activeCount);
|
|
3093
|
+
if (freeSlots === 0) {
|
|
3094
|
+
if (allReviews.length > 0) {
|
|
3095
|
+
log('info', `Gating ${allReviews.length} reviews — implement items in progress and no free slots`);
|
|
3096
|
+
allReviews = [];
|
|
3097
|
+
}
|
|
3098
|
+
if (allFixes.length > 0) {
|
|
3099
|
+
log('info', `Gating ${allFixes.length} fixes — implement items in progress and no free slots`);
|
|
3100
|
+
allFixes = [];
|
|
3101
|
+
}
|
|
3096
3102
|
}
|
|
3097
3103
|
}
|
|
3098
3104
|
|
|
@@ -3204,10 +3210,10 @@ async function tickInner() {
|
|
|
3204
3210
|
});
|
|
3205
3211
|
}
|
|
3206
3212
|
|
|
3207
|
-
const adoPollEnabled = config.engine?.adoPollEnabled ??
|
|
3208
|
-
const ghPollEnabled = config.engine?.ghPollEnabled ??
|
|
3209
|
-
const adoPollStatusEvery = Math.max(1, Number(config.engine?.adoPollStatusEvery) ||
|
|
3210
|
-
const adoPollCommentsEvery = Math.max(1, Number(config.engine?.adoPollCommentsEvery) ||
|
|
3213
|
+
const adoPollEnabled = config.engine?.adoPollEnabled ?? ENGINE_DEFAULTS.adoPollEnabled;
|
|
3214
|
+
const ghPollEnabled = config.engine?.ghPollEnabled ?? ENGINE_DEFAULTS.ghPollEnabled;
|
|
3215
|
+
const adoPollStatusEvery = Math.max(1, Number(config.engine?.adoPollStatusEvery) || ENGINE_DEFAULTS.adoPollStatusEvery);
|
|
3216
|
+
const adoPollCommentsEvery = Math.max(1, Number(config.engine?.adoPollCommentsEvery) || ENGINE_DEFAULTS.adoPollCommentsEvery);
|
|
3211
3217
|
|
|
3212
3218
|
// 2.6. Poll PR status: build, review, merge (every adoPollStatusEvery ticks, default ~6 minutes)
|
|
3213
3219
|
// Awaited so PR state is consistent before discoverWork reads it
|
|
@@ -3410,7 +3416,7 @@ async function tickInner() {
|
|
|
3410
3416
|
if (busyAgents.has(item.agent)) {
|
|
3411
3417
|
// Agent busy reassignment: if item has been waiting on a busy agent past the threshold,
|
|
3412
3418
|
// try to find an alternative agent via routing. Skip explicitly assigned items.
|
|
3413
|
-
const reassignMs = config.engine?.agentBusyReassignMs ??
|
|
3419
|
+
const reassignMs = config.engine?.agentBusyReassignMs ?? ENGINE_DEFAULTS.agentBusyReassignMs;
|
|
3414
3420
|
const isExplicitReassign = !!item.meta?.item?.agent;
|
|
3415
3421
|
if (!isExplicitReassign && reassignMs > 0 && item._agentBusySince) {
|
|
3416
3422
|
const busySinceMs = new Date(item._agentBusySince).getTime();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1004",
|
|
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"
|