@yemi33/minions 0.1.347 → 0.1.348
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 +4 -1
- package/dashboard.js +22 -16
- package/engine/dispatch.js +2 -6
- package/engine/lifecycle.js +29 -16
- package/engine/timeout.js +36 -36
- package/engine.js +6 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard.js
CHANGED
|
@@ -14,7 +14,7 @@ const shared = require('./engine/shared');
|
|
|
14
14
|
const queries = require('./engine/queries');
|
|
15
15
|
const os = require('os');
|
|
16
16
|
|
|
17
|
-
const { safeRead, safeReadDir, safeWrite, safeJson, safeUnlink, mutateJsonFileLocked, getProjects: _getProjects } = shared;
|
|
17
|
+
const { safeRead, safeReadDir, safeWrite, safeJson, safeUnlink, mutateJsonFileLocked, getProjects: _getProjects, DONE_STATUSES } = shared;
|
|
18
18
|
const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
|
|
19
19
|
getSkills, getInbox, getNotesWithMeta, getPullRequests,
|
|
20
20
|
getEngineLog, getMetrics, getKnowledgeBaseEntries, timeSince,
|
|
@@ -895,6 +895,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
895
895
|
if (!Array.isArray(items)) items = [];
|
|
896
896
|
const item = items.find(i => i.id === id);
|
|
897
897
|
if (!item) return items;
|
|
898
|
+
// Don't reset completed items unless explicitly forced
|
|
899
|
+
if ((item.status === 'done' || item.completedAt) && !body.force) {
|
|
900
|
+
found = 'already_done';
|
|
901
|
+
return items;
|
|
902
|
+
}
|
|
898
903
|
found = true;
|
|
899
904
|
item.status = 'pending';
|
|
900
905
|
item._retryCount = 0; // Reset retry counter on manual retry
|
|
@@ -902,9 +907,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
902
907
|
delete item.dispatched_to;
|
|
903
908
|
delete item.failReason;
|
|
904
909
|
delete item.failedAt;
|
|
910
|
+
delete item.completedAt;
|
|
905
911
|
delete item.fanOutAgents;
|
|
906
912
|
return items;
|
|
907
913
|
});
|
|
914
|
+
if (found === 'already_done') return jsonReply(res, 409, { error: 'item already completed — use force:true to retry' });
|
|
908
915
|
if (!found) return jsonReply(res, 404, { error: 'item not found' });
|
|
909
916
|
|
|
910
917
|
// Clear completed dispatch entries so the engine doesn't dedup this item
|
|
@@ -1800,20 +1807,19 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1800
1807
|
}
|
|
1801
1808
|
for (const wiPath of wiPaths) {
|
|
1802
1809
|
try {
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
changed = true;
|
|
1810
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
1811
|
+
if (!Array.isArray(items)) return items;
|
|
1812
|
+
for (const w of items) {
|
|
1813
|
+
if (w.sourcePlan === body.file && w.status === 'paused' && w._pausedBy === 'prd-pause') {
|
|
1814
|
+
w.status = 'pending';
|
|
1815
|
+
delete w._pausedBy;
|
|
1816
|
+
w._resumedAt = new Date().toISOString();
|
|
1817
|
+
resumedItemIds.push(w.id);
|
|
1818
|
+
resumed++;
|
|
1819
|
+
}
|
|
1814
1820
|
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1821
|
+
return items;
|
|
1822
|
+
}, { defaultValue: [] });
|
|
1817
1823
|
} catch (e) { console.error('resume work items:', e.message); }
|
|
1818
1824
|
}
|
|
1819
1825
|
|
|
@@ -1864,7 +1870,7 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1864
1870
|
for (const w of items) {
|
|
1865
1871
|
if (w.sourcePlan !== body.file) continue;
|
|
1866
1872
|
// Keep completed items as-is, reset everything else to pending.
|
|
1867
|
-
if (w.
|
|
1873
|
+
if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
|
|
1868
1874
|
|
|
1869
1875
|
if (w.status === 'dispatched') {
|
|
1870
1876
|
// Kill the agent working on this item, if any.
|
|
@@ -2574,7 +2580,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2574
2580
|
let changed = false;
|
|
2575
2581
|
for (const w of items) {
|
|
2576
2582
|
if (w.sourcePlan !== f) continue;
|
|
2577
|
-
if (w.
|
|
2583
|
+
if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
|
|
2578
2584
|
if (w.status === 'dispatched') {
|
|
2579
2585
|
const activeEntry = (dispatch.active || []).find(d => d.meta?.item?.id === w.id || d.meta?.dispatchKey?.includes(w.id));
|
|
2580
2586
|
if (activeEntry) {
|
package/engine/dispatch.js
CHANGED
|
@@ -104,9 +104,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
104
104
|
if (processWorkItemFailure && result === DISPATCH_RESULT.ERROR && item.meta?.item?.id) {
|
|
105
105
|
let retries = (item.meta.item._retryCount || 0);
|
|
106
106
|
try {
|
|
107
|
-
const wiPath =
|
|
108
|
-
? path.join(MINIONS_DIR, 'work-items.json')
|
|
109
|
-
: item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
|
|
107
|
+
const wiPath = lifecycle().resolveWorkItemPath(item.meta);
|
|
110
108
|
if (wiPath) {
|
|
111
109
|
const items = safeJson(wiPath);
|
|
112
110
|
if (items && Array.isArray(items)) {
|
|
@@ -130,9 +128,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
130
128
|
}
|
|
131
129
|
// Increment retry counter on the source work item
|
|
132
130
|
try {
|
|
133
|
-
const wiPath =
|
|
134
|
-
? path.join(MINIONS_DIR, 'work-items.json')
|
|
135
|
-
: item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
|
|
131
|
+
const wiPath = lifecycle().resolveWorkItemPath(item.meta);
|
|
136
132
|
if (wiPath) {
|
|
137
133
|
const items = safeJson(wiPath);
|
|
138
134
|
if (!items || !Array.isArray(items)) throw new Error('work items unreadable');
|
package/engine/lifecycle.js
CHANGED
|
@@ -477,6 +477,24 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
477
477
|
shared.safeWrite(wiPath, items);
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
+
// ─── Work Item Path Resolution ───────────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
/** Resolve the work-items.json path from dispatch meta. Reused by retry paths. */
|
|
483
|
+
function resolveWorkItemPath(meta) {
|
|
484
|
+
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
485
|
+
return path.join(MINIONS_DIR, 'work-items.json');
|
|
486
|
+
}
|
|
487
|
+
if (meta.source === 'work-item' && meta.project?.name) {
|
|
488
|
+
return path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json');
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Check if a work item is in a terminal completed state. */
|
|
494
|
+
function isItemCompleted(item) {
|
|
495
|
+
return item.status === WI_STATUS.DONE || !!item.completedAt;
|
|
496
|
+
}
|
|
497
|
+
|
|
480
498
|
// ─── Work Item Status ────────────────────────────────────────────────────────
|
|
481
499
|
const _VALID_WI_STATUSES = new Set(Object.values(WI_STATUS));
|
|
482
500
|
function updateWorkItemStatus(meta, status, reason) {
|
|
@@ -488,19 +506,14 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
488
506
|
return;
|
|
489
507
|
}
|
|
490
508
|
|
|
491
|
-
|
|
492
|
-
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
493
|
-
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
494
|
-
} else if (meta.source === 'work-item' && meta.project?.name) {
|
|
495
|
-
wiPath = path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json');
|
|
496
|
-
}
|
|
509
|
+
const wiPath = resolveWorkItemPath(meta);
|
|
497
510
|
if (!wiPath) return;
|
|
498
511
|
|
|
499
|
-
|
|
500
|
-
|
|
512
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
513
|
+
if (!items || !Array.isArray(items)) return items;
|
|
514
|
+
const target = items.find(i => i.id === itemId);
|
|
515
|
+
if (!target) return items;
|
|
501
516
|
|
|
502
|
-
const target = items.find(i => i.id === itemId);
|
|
503
|
-
if (target) {
|
|
504
517
|
if (meta.source === 'central-work-item-fanout') {
|
|
505
518
|
if (!target.agentResults) target.agentResults = {};
|
|
506
519
|
const parts = (meta.dispatchKey || '').split('-');
|
|
@@ -538,13 +551,11 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
538
551
|
target.failedAt = ts();
|
|
539
552
|
}
|
|
540
553
|
}
|
|
554
|
+
return items;
|
|
555
|
+
}, { defaultValue: [] });
|
|
541
556
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
// Sync status to PRD JSON so the two share the same value (work item is source of truth)
|
|
546
|
-
syncPrdItemStatus(itemId, status, meta.item?.sourcePlan);
|
|
547
|
-
}
|
|
557
|
+
log('info', `Work item ${itemId} → ${status}${reason ? ': ' + reason : ''}`);
|
|
558
|
+
syncPrdItemStatus(itemId, status, meta.item?.sourcePlan);
|
|
548
559
|
}
|
|
549
560
|
|
|
550
561
|
const _VALID_PRD_STATUSES = new Set([...Object.values(WI_STATUS), 'missing']);
|
|
@@ -1371,5 +1382,7 @@ module.exports = {
|
|
|
1371
1382
|
parseAgentOutput,
|
|
1372
1383
|
runPostCompletionHooks,
|
|
1373
1384
|
syncPrdFromPrs,
|
|
1385
|
+
resolveWorkItemPath,
|
|
1386
|
+
isItemCompleted,
|
|
1374
1387
|
};
|
|
1375
1388
|
|
package/engine/timeout.js
CHANGED
|
@@ -8,7 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const shared = require('./shared');
|
|
9
9
|
const queries = require('./queries');
|
|
10
10
|
|
|
11
|
-
const { safeRead, safeWrite, safeJson, getProjects, projectWorkItemsPath, log, ts,
|
|
11
|
+
const { safeRead, safeWrite, safeJson, mutateJsonFileLocked, getProjects, projectWorkItemsPath, log, ts,
|
|
12
12
|
ENGINE_DEFAULTS: DEFAULTS, WI_STATUS, DISPATCH_RESULT } = shared;
|
|
13
13
|
const { getDispatch, getAgentStatus } = queries;
|
|
14
14
|
const AGENTS_DIR = queries.AGENTS_DIR;
|
|
@@ -229,43 +229,43 @@ function checkTimeouts(config) {
|
|
|
229
229
|
allWiPaths.push(projectWorkItemsPath(project));
|
|
230
230
|
}
|
|
231
231
|
for (const wiPath of allWiPaths) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
232
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
233
|
+
if (!items || !Array.isArray(items)) return items;
|
|
234
|
+
let changed = false;
|
|
235
|
+
for (const item of items) {
|
|
236
|
+
if (item.status !== WI_STATUS.DISPATCHED) continue;
|
|
237
|
+
// Never revert completed items
|
|
238
|
+
if (item.completedAt || item.status === WI_STATUS.DONE) continue;
|
|
239
|
+
// Check if any active dispatch references this item
|
|
240
|
+
const projectNames = getProjects(config).map(p => p.name);
|
|
241
|
+
const possibleKeys = [
|
|
242
|
+
`central-work-${item.id}`,
|
|
243
|
+
...projectNames.map(p => `work-${p}-${item.id}`),
|
|
244
|
+
];
|
|
245
|
+
const isActive = possibleKeys.some(k => activeKeys.has(k)) ||
|
|
246
|
+
(dispatchData.active || []).some(d => d.meta?.item?.id === item.id);
|
|
247
|
+
if (!isActive) {
|
|
248
|
+
const retries = (item._retryCount || 0);
|
|
249
|
+
const maxRetries = DEFAULTS.maxRetries;
|
|
250
|
+
if (retries < maxRetries) {
|
|
251
|
+
log('info', `Reconcile: work item ${item.id} agent died — auto-retry ${retries + 1}/${maxRetries}`);
|
|
252
|
+
item.status = WI_STATUS.PENDING;
|
|
253
|
+
item._retryCount = retries + 1;
|
|
254
|
+
delete item.dispatched_at;
|
|
255
|
+
delete item.dispatched_to;
|
|
256
|
+
delete item._pendingReason;
|
|
257
|
+
} else {
|
|
258
|
+
log('warn', `Reconcile: work item ${item.id} failed after ${retries} retries — marking as failed`);
|
|
259
|
+
item.status = WI_STATUS.FAILED;
|
|
260
|
+
item.failReason = `Agent died or was killed (${maxRetries} retries exhausted)`;
|
|
261
|
+
item.failedAt = ts();
|
|
262
|
+
delete item._pendingReason;
|
|
263
|
+
}
|
|
264
|
+
changed = true;
|
|
264
265
|
}
|
|
265
|
-
changed = true;
|
|
266
266
|
}
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
return items;
|
|
268
|
+
}, { defaultValue: [] });
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
package/engine.js
CHANGED
|
@@ -132,7 +132,8 @@ const { renderPlaybook, buildSystemPrompt, buildAgentContext, selectPlaybook,
|
|
|
132
132
|
|
|
133
133
|
const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, handlePostMerge, checkPlanCompletion,
|
|
134
134
|
syncPrsFromOutput, updatePrAfterReview, updatePrAfterFix, checkForLearnings, extractSkillsFromOutput,
|
|
135
|
-
updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs
|
|
135
|
+
updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs,
|
|
136
|
+
isItemCompleted } = require('./engine/lifecycle');
|
|
136
137
|
|
|
137
138
|
// ─── Agent Spawner ──────────────────────────────────────────────────────────
|
|
138
139
|
|
|
@@ -1381,7 +1382,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
1381
1382
|
for (const item of items) {
|
|
1382
1383
|
try {
|
|
1383
1384
|
// Re-evaluate failed items: if deps have recovered, reset to pending
|
|
1384
|
-
if (item.status === WI_STATUS.FAILED && item.failReason === 'Dependency failed — cannot proceed') {
|
|
1385
|
+
if (item.status === WI_STATUS.FAILED && !isItemCompleted(item) && item.failReason === 'Dependency failed — cannot proceed') {
|
|
1385
1386
|
const depStatus = areDependenciesMet(item, config);
|
|
1386
1387
|
if (depStatus === true) {
|
|
1387
1388
|
item.status = WI_STATUS.PENDING;
|
|
@@ -1396,7 +1397,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
1396
1397
|
// Dependency gate: skip items whose depends_on are not yet met; propagate failure
|
|
1397
1398
|
if (item.depends_on && item.depends_on.length > 0) {
|
|
1398
1399
|
const depStatus = areDependenciesMet(item, config);
|
|
1399
|
-
if (depStatus === 'failed') {
|
|
1400
|
+
if (depStatus === 'failed' && !isItemCompleted(item)) {
|
|
1400
1401
|
item.status = WI_STATUS.FAILED;
|
|
1401
1402
|
item.failReason = 'Dependency failed — cannot proceed';
|
|
1402
1403
|
delete item._pendingReason;
|
|
@@ -2272,7 +2273,7 @@ async function tickInner() {
|
|
|
2272
2273
|
if (pendingWithBlockedDeps.length > 0) {
|
|
2273
2274
|
// Auto-retry failed items that are blocking others (transient errors)
|
|
2274
2275
|
for (const item of items) {
|
|
2275
|
-
if (item.status !== 'failed') continue;
|
|
2276
|
+
if (item.status !== 'failed' || isItemCompleted(item)) continue;
|
|
2276
2277
|
// Only retry if something depends on this item
|
|
2277
2278
|
const isBlocking = items.some(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(item.id));
|
|
2278
2279
|
if (!isBlocking) continue;
|
|
@@ -2310,7 +2311,7 @@ async function tickInner() {
|
|
|
2310
2311
|
if (changed) {
|
|
2311
2312
|
const retriedIds = new Set(items.filter(w => w.status === WI_STATUS.PENDING && w._retryCount === 0).map(w => w.id));
|
|
2312
2313
|
for (const dep of items) {
|
|
2313
|
-
if (dep.status === WI_STATUS.FAILED && dep.failReason === 'Dependency failed — cannot proceed') {
|
|
2314
|
+
if (dep.status === WI_STATUS.FAILED && !isItemCompleted(dep) && dep.failReason === 'Dependency failed — cannot proceed') {
|
|
2314
2315
|
const blockers = (dep.depends_on || []).filter(d => retriedIds.has(d));
|
|
2315
2316
|
if (blockers.length > 0) {
|
|
2316
2317
|
log('info', `Stall recovery: un-failing ${dep.id} (blocker ${blockers.join(',')} retried)`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.348",
|
|
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"
|