@yemi33/minions 0.1.379 → 0.1.381
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 +6 -1
- package/dashboard.js +22 -22
- package/engine/ado.js +4 -3
- package/engine/github.js +4 -4
- package/engine/lifecycle.js +45 -46
- package/engine/timeout.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.381 (2026-04-06)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- clear failReason on all retry-to-pending paths
|
|
7
|
+
- clear failReason and noPr on no-PR retry reset
|
|
8
|
+
- PR duplicate race condition — convert safeWrite to mutateJsonFileLocked (#240)
|
|
4
9
|
|
|
5
10
|
### Other
|
|
6
11
|
- [E2E] Fix review re-dispatch loop, dashboard write races, crash bugs, and failing tests (#232)
|
package/dashboard.js
CHANGED
|
@@ -3482,34 +3482,34 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3482
3482
|
const projects = shared.getProjects(CONFIG);
|
|
3483
3483
|
const targetProject = projectName ? projects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) : projects[0];
|
|
3484
3484
|
const prPath = targetProject ? shared.projectPrPath(targetProject) : path.join(MINIONS_DIR, 'pull-requests.json');
|
|
3485
|
-
const prs = JSON.parse(safeRead(prPath) || '[]');
|
|
3486
3485
|
|
|
3487
3486
|
// Extract PR number from URL
|
|
3488
3487
|
const prNumMatch = url.match(/\/pull\/(\d+)|pullrequest\/(\d+)/);
|
|
3489
3488
|
const prNum = prNumMatch ? (prNumMatch[1] || prNumMatch[2]) : Date.now().toString().slice(-6);
|
|
3490
3489
|
const prId = 'PR-' + prNum;
|
|
3491
3490
|
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3491
|
+
// Atomic check-and-insert to prevent duplicates and races with polling loops
|
|
3492
|
+
let duplicate = false;
|
|
3493
|
+
mutateJsonFileLocked(prPath, (prs) => {
|
|
3494
|
+
if (!Array.isArray(prs)) prs = [];
|
|
3495
|
+
if (prs.some(p => p.id === prId || p.url === url)) { duplicate = true; return prs; }
|
|
3496
|
+
prs.push({
|
|
3497
|
+
id: prId,
|
|
3498
|
+
title: (title || 'PR #' + prNum + ' (polling...)').slice(0, 120),
|
|
3499
|
+
agent: 'human',
|
|
3500
|
+
branch: '',
|
|
3501
|
+
reviewStatus: autoObserve ? 'pending' : 'none',
|
|
3502
|
+
status: autoObserve ? 'active' : 'linked',
|
|
3503
|
+
created: new Date().toISOString(),
|
|
3504
|
+
url,
|
|
3505
|
+
prdItems: [],
|
|
3506
|
+
_manual: true,
|
|
3507
|
+
_autoObserve: !!autoObserve,
|
|
3508
|
+
_context: context || '',
|
|
3509
|
+
});
|
|
3510
|
+
return prs;
|
|
3511
|
+
}, { defaultValue: [] });
|
|
3512
|
+
if (duplicate) return jsonReply(res, 400, { error: 'PR already tracked' });
|
|
3513
3513
|
invalidateStatusCache();
|
|
3514
3514
|
return jsonReply(res, 200, { ok: true, id: prId });
|
|
3515
3515
|
}},
|
package/engine/ado.js
CHANGED
|
@@ -101,11 +101,12 @@ async function forEachActivePr(config, token, callback) {
|
|
|
101
101
|
|
|
102
102
|
if (projectUpdated > 0) {
|
|
103
103
|
mutateJsonFileLocked(shared.projectPrPath(project), (currentPrs) => {
|
|
104
|
-
//
|
|
105
|
-
|
|
104
|
+
// Only merge back PRs that the callback actually modified — not the entire
|
|
105
|
+
// stale snapshot, which would overwrite concurrent writes from other code paths
|
|
106
|
+
for (const updatedPr of activePrs) {
|
|
106
107
|
const idx = currentPrs.findIndex(p => p.id === updatedPr.id);
|
|
107
108
|
if (idx >= 0) currentPrs[idx] = updatedPr;
|
|
108
|
-
|
|
109
|
+
// Don't push if not found — it was deleted by another writer, respect that
|
|
109
110
|
}
|
|
110
111
|
return currentPrs;
|
|
111
112
|
}, { defaultValue: [] });
|
package/engine/github.js
CHANGED
|
@@ -72,10 +72,10 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
72
72
|
|
|
73
73
|
if (projectUpdated > 0) {
|
|
74
74
|
mutateJsonFileLocked(projectPrPath(project), (currentPrs) => {
|
|
75
|
-
|
|
75
|
+
// Only merge back PRs that the callback actually modified
|
|
76
|
+
for (const updatedPr of activePrs) {
|
|
76
77
|
const idx = currentPrs.findIndex(p => p.id === updatedPr.id);
|
|
77
78
|
if (idx >= 0) currentPrs[idx] = updatedPr;
|
|
78
|
-
else currentPrs.push(updatedPr);
|
|
79
79
|
}
|
|
80
80
|
return currentPrs;
|
|
81
81
|
}, { defaultValue: [] });
|
|
@@ -113,10 +113,10 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
113
113
|
}
|
|
114
114
|
if (centralUpdated > 0) {
|
|
115
115
|
mutateJsonFileLocked(centralPath, (currentPrs) => {
|
|
116
|
-
|
|
116
|
+
// Only merge back central PRs that the callback actually modified
|
|
117
|
+
for (const updatedPr of activeCentral) {
|
|
117
118
|
const idx = currentPrs.findIndex(p => p.id === updatedPr.id);
|
|
118
119
|
if (idx >= 0) currentPrs[idx] = updatedPr;
|
|
119
|
-
else currentPrs.push(updatedPr);
|
|
120
120
|
}
|
|
121
121
|
return currentPrs;
|
|
122
122
|
}, { defaultValue: [] });
|
package/engine/lifecycle.js
CHANGED
|
@@ -714,66 +714,63 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
714
714
|
function updatePrAfterReview(agentId, pr, project) {
|
|
715
715
|
|
|
716
716
|
if (!pr?.id) return;
|
|
717
|
-
const prs = getPrs(project);
|
|
718
|
-
const target = prs.find(p => p.id === pr.id);
|
|
719
|
-
if (!target) return;
|
|
720
717
|
|
|
721
718
|
const config = getConfig();
|
|
722
719
|
const reviewerName = config.agents[agentId]?.name || agentId;
|
|
723
|
-
// Record the reviewer — actual verdict comes from ADO/GitHub votes via pollPrStatus.
|
|
724
|
-
// Set to 'waiting' so pollPrStatus updates it with the real vote on next cycle.
|
|
725
720
|
const dispatch = getDispatch();
|
|
726
721
|
const completedEntry = (dispatch.completed || []).find(d => d.agent === agentId && d.type === 'review');
|
|
727
722
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
723
|
+
const prPath = project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json');
|
|
724
|
+
let updatedTarget = null;
|
|
725
|
+
shared.mutateJsonFileLocked(prPath, (prs) => {
|
|
726
|
+
if (!Array.isArray(prs)) return prs;
|
|
727
|
+
const target = prs.find(p => p.id === pr.id);
|
|
728
|
+
if (!target) return prs;
|
|
729
|
+
target.reviewStatus = 'waiting';
|
|
730
|
+
target.lastReviewedAt = ts();
|
|
731
|
+
target.minionsReview = {
|
|
732
|
+
reviewer: reviewerName,
|
|
733
|
+
reviewedAt: ts(),
|
|
734
|
+
note: completedEntry?.task || ''
|
|
735
|
+
};
|
|
736
|
+
updatedTarget = { ...pr, ...target };
|
|
737
|
+
return prs;
|
|
738
|
+
}, { defaultValue: [] });
|
|
739
|
+
|
|
740
|
+
// Track reviewer for metrics purposes (separate file, separate lock)
|
|
743
741
|
const authorAgentId = (pr.agent || '').toLowerCase();
|
|
744
742
|
if (authorAgentId && config.agents?.[authorAgentId]) {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
743
|
+
shared.mutateJsonFileLocked(path.join(ENGINE_DIR, 'metrics.json'), (metrics) => {
|
|
744
|
+
if (!metrics[authorAgentId]) metrics[authorAgentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
|
|
745
|
+
if (!metrics[agentId]) metrics[agentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
|
|
746
|
+
metrics[agentId].reviewsDone = (metrics[agentId].reviewsDone || 0) + 1;
|
|
747
|
+
return metrics;
|
|
748
|
+
}, { defaultValue: {} });
|
|
751
749
|
}
|
|
752
750
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
|
|
751
|
+
log('info', `Updated ${pr.id} → minions review: waiting by ${reviewerName}`);
|
|
752
|
+
if (updatedTarget) createReviewFeedbackForAuthor(agentId, updatedTarget, config);
|
|
756
753
|
}
|
|
757
754
|
|
|
758
755
|
function updatePrAfterFix(pr, project, source) {
|
|
759
756
|
|
|
760
757
|
if (!pr?.id) return;
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
758
|
+
const prPath = project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json');
|
|
759
|
+
shared.mutateJsonFileLocked(prPath, (prs) => {
|
|
760
|
+
if (!Array.isArray(prs)) return prs;
|
|
761
|
+
const target = prs.find(p => p.id === pr.id);
|
|
762
|
+
if (!target) return prs;
|
|
763
|
+
target.reviewStatus = 'waiting';
|
|
764
|
+
if (source === 'pr-human-feedback') {
|
|
765
|
+
if (target.humanFeedback) target.humanFeedback.pendingFix = false;
|
|
766
|
+
target.minionsReview = { ...target.minionsReview, note: 'Fixed human feedback, awaiting re-review', fixedAt: ts() };
|
|
767
|
+
log('info', `Updated ${pr.id} → cleared humanFeedback.pendingFix, reset to waiting for re-review`);
|
|
768
|
+
} else {
|
|
769
|
+
target.minionsReview = { ...target.minionsReview, note: 'Fixed, awaiting re-review', fixedAt: ts() };
|
|
770
|
+
log('info', `Updated ${pr.id} → reviewStatus: waiting (fix pushed)`);
|
|
771
|
+
}
|
|
772
|
+
return prs;
|
|
773
|
+
}, { defaultValue: [] });
|
|
777
774
|
}
|
|
778
775
|
|
|
779
776
|
// ─── Post-Merge / Post-Close Hooks ───────────────────────────────────────────
|
|
@@ -1196,7 +1193,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1196
1193
|
if (wi.status === WI_STATUS.DONE || wi.completedAt) {
|
|
1197
1194
|
log('info', `Skip retry for ${meta.item.id} — already completed`);
|
|
1198
1195
|
} else {
|
|
1199
|
-
wi._retryCount = retries + 1; wi.status = WI_STATUS.PENDING; delete wi.dispatched_at; delete wi.dispatched_to;
|
|
1196
|
+
wi._retryCount = retries + 1; wi.status = WI_STATUS.PENDING; delete wi.dispatched_at; delete wi.dispatched_to; delete wi.failReason;
|
|
1200
1197
|
if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing;
|
|
1201
1198
|
shared.safeWrite(wiPath, items);
|
|
1202
1199
|
}
|
|
@@ -1317,6 +1314,8 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1317
1314
|
wi._retryCount = retries + 1;
|
|
1318
1315
|
delete wi.dispatched_at;
|
|
1319
1316
|
delete wi.dispatched_to;
|
|
1317
|
+
delete wi.failReason;
|
|
1318
|
+
delete wi.noPr;
|
|
1320
1319
|
log('info', `Auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries} for ${meta.item.id} (no output, no PR)`);
|
|
1321
1320
|
} else if (hasOutput) {
|
|
1322
1321
|
// Agent ran successfully but chose not to create a PR — mark done
|
package/engine/timeout.js
CHANGED
|
@@ -254,6 +254,7 @@ function checkTimeouts(config) {
|
|
|
254
254
|
delete item.dispatched_at;
|
|
255
255
|
delete item.dispatched_to;
|
|
256
256
|
delete item._pendingReason;
|
|
257
|
+
delete item.failReason;
|
|
257
258
|
} else {
|
|
258
259
|
log('warn', `Reconcile: work item ${item.id} failed after ${retries} retries — marking as failed`);
|
|
259
260
|
item.status = WI_STATUS.FAILED;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.381",
|
|
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"
|