@yemi33/minions 0.1.534 → 0.1.535
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/engine.js +196 -199
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/engine.js
CHANGED
|
@@ -92,6 +92,8 @@ const safeJson = shared.safeJson;
|
|
|
92
92
|
const safeRead = shared.safeRead;
|
|
93
93
|
const safeWrite = shared.safeWrite;
|
|
94
94
|
const mutateJsonFileLocked = shared.mutateJsonFileLocked;
|
|
95
|
+
const mutateWorkItems = shared.mutateWorkItems;
|
|
96
|
+
const mutatePullRequests = shared.mutatePullRequests;
|
|
95
97
|
const withFileLock = shared.withFileLock;
|
|
96
98
|
|
|
97
99
|
// ─── Dispatch Management (extracted to engine/dispatch.js) ───────────────────
|
|
@@ -1005,15 +1007,15 @@ function autoCleanPrdWorkItems(prdFile, config) {
|
|
|
1005
1007
|
const deletedIds = [];
|
|
1006
1008
|
for (const wiPath of wiPaths) {
|
|
1007
1009
|
try {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1010
|
+
mutateWorkItems(wiPath, items => {
|
|
1011
|
+
const filtered = items.filter(w => {
|
|
1012
|
+
if (w.sourcePlan === prdFile && (w.status === WI_STATUS.PENDING || w.status === WI_STATUS.FAILED)) {
|
|
1013
|
+
deletedIds.push(w.id); return false;
|
|
1014
|
+
}
|
|
1015
|
+
return true;
|
|
1016
|
+
});
|
|
1017
|
+
if (filtered.length < items.length) return filtered;
|
|
1015
1018
|
});
|
|
1016
|
-
if (filtered.length < items.length) safeWrite(wiPath, filtered);
|
|
1017
1019
|
} catch (e) { log('warn', 'auto-clean PRD work items: ' + e.message); }
|
|
1018
1020
|
}
|
|
1019
1021
|
if (deletedIds.length > 0) {
|
|
@@ -1237,71 +1239,71 @@ function materializePlansAsWorkItems(config) {
|
|
|
1237
1239
|
let totalCreated = 0;
|
|
1238
1240
|
for (const [projName, { project, items: projItems }] of itemsByProject) {
|
|
1239
1241
|
const wiPath = project ? projectWorkItemsPath(project) : path.join(MINIONS_DIR, 'work-items.json');
|
|
1240
|
-
const existingItems = safeJson(wiPath) || [];
|
|
1241
1242
|
let created = 0;
|
|
1242
1243
|
const newlyCreatedIds = new Set(); // tracks IDs created in this pass for reconciliation scoping
|
|
1243
1244
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1245
|
+
mutateWorkItems(wiPath, existingItems => {
|
|
1246
|
+
for (const item of projItems) {
|
|
1247
|
+
// Skip if already materialized — work item ID = PRD item ID, check all projects
|
|
1248
|
+
let alreadyExists = existingItems.some(w => w.id === item.id);
|
|
1249
|
+
if (!alreadyExists) {
|
|
1250
|
+
for (const p of allProjects) {
|
|
1251
|
+
if (p.name === projName) continue;
|
|
1252
|
+
const otherItems = safeJson(projectWorkItemsPath(p)) || [];
|
|
1253
|
+
if (otherItems.some(w => w.id === item.id)) { alreadyExists = true; break; }
|
|
1254
|
+
}
|
|
1252
1255
|
}
|
|
1256
|
+
if (alreadyExists) continue;
|
|
1257
|
+
// Skip items involved in dependency cycles
|
|
1258
|
+
if (cycleSet.has(item.id)) continue;
|
|
1259
|
+
|
|
1260
|
+
const id = item.id; // Work item ID = PRD item ID — no indirection
|
|
1261
|
+
const complexity = item.estimated_complexity || 'medium';
|
|
1262
|
+
const criteria = (item.acceptance_criteria || []).map(c => `- ${c}`).join('\n');
|
|
1263
|
+
|
|
1264
|
+
const newItem = {
|
|
1265
|
+
id,
|
|
1266
|
+
title: `Implement: ${item.name}`,
|
|
1267
|
+
type: complexity === 'large' ? 'implement:large' : 'implement',
|
|
1268
|
+
priority: item.priority || 'medium',
|
|
1269
|
+
description: `${item.description || ''}\n\n**Plan:** ${file}\n**Plan Item:** ${item.id}\n**Complexity:** ${complexity}${criteria ? '\n\n**Acceptance Criteria:**\n' + criteria : ''}`,
|
|
1270
|
+
status: 'pending',
|
|
1271
|
+
created: ts(),
|
|
1272
|
+
createdBy: 'engine:plan-discovery',
|
|
1273
|
+
sourcePlan: file,
|
|
1274
|
+
depends_on: item.depends_on || [],
|
|
1275
|
+
branchStrategy: plan.branch_strategy || 'parallel',
|
|
1276
|
+
featureBranch: plan.feature_branch || null,
|
|
1277
|
+
project: item.project || plan.project || null,
|
|
1278
|
+
};
|
|
1279
|
+
existingItems.push(newItem);
|
|
1280
|
+
newlyCreatedIds.add(id);
|
|
1281
|
+
created++;
|
|
1253
1282
|
}
|
|
1254
|
-
if (alreadyExists) continue;
|
|
1255
|
-
// Skip items involved in dependency cycles
|
|
1256
|
-
if (cycleSet.has(item.id)) continue;
|
|
1257
|
-
|
|
1258
|
-
const id = item.id; // Work item ID = PRD item ID — no indirection
|
|
1259
|
-
const complexity = item.estimated_complexity || 'medium';
|
|
1260
|
-
const criteria = (item.acceptance_criteria || []).map(c => `- ${c}`).join('\n');
|
|
1261
|
-
|
|
1262
|
-
const newItem = {
|
|
1263
|
-
id,
|
|
1264
|
-
title: `Implement: ${item.name}`,
|
|
1265
|
-
type: complexity === 'large' ? 'implement:large' : 'implement',
|
|
1266
|
-
priority: item.priority || 'medium',
|
|
1267
|
-
description: `${item.description || ''}\n\n**Plan:** ${file}\n**Plan Item:** ${item.id}\n**Complexity:** ${complexity}${criteria ? '\n\n**Acceptance Criteria:**\n' + criteria : ''}`,
|
|
1268
|
-
status: 'pending',
|
|
1269
|
-
created: ts(),
|
|
1270
|
-
createdBy: 'engine:plan-discovery',
|
|
1271
|
-
sourcePlan: file,
|
|
1272
|
-
depends_on: item.depends_on || [],
|
|
1273
|
-
branchStrategy: plan.branch_strategy || 'parallel',
|
|
1274
|
-
featureBranch: plan.feature_branch || null,
|
|
1275
|
-
project: item.project || plan.project || null,
|
|
1276
|
-
};
|
|
1277
|
-
existingItems.push(newItem);
|
|
1278
|
-
newlyCreatedIds.add(id);
|
|
1279
|
-
created++;
|
|
1280
|
-
}
|
|
1281
1283
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1284
|
+
if (created > 0) {
|
|
1285
|
+
// Reconciliation: exact prdItems match only, scoped to newly created items
|
|
1286
|
+
const allPrsForReconcile = allProjects.flatMap(p => safeJson(projectPrPath(p)) || []);
|
|
1287
|
+
const reconciled = reconcileItemsWithPrs(existingItems, allPrsForReconcile, { onlyIds: newlyCreatedIds });
|
|
1288
|
+
if (reconciled > 0) log('info', `Plan reconciliation: marked ${reconciled} item(s) as done → ${projName}`);
|
|
1289
|
+
|
|
1290
|
+
// PRD removal sync: cancel pending work items whose PRD item was removed from the plan
|
|
1291
|
+
const currentPrdIds = new Set(plan.missing_features.map(f => f.id));
|
|
1292
|
+
let cancelled = 0;
|
|
1293
|
+
for (const wi of existingItems) {
|
|
1294
|
+
if (wi.status !== WI_STATUS.PENDING || wi.sourcePlan !== file) continue;
|
|
1295
|
+
if (!currentPrdIds.has(wi.id)) {
|
|
1296
|
+
wi.status = WI_STATUS.CANCELLED;
|
|
1297
|
+
wi.cancelledAt = ts();
|
|
1298
|
+
wi.cancelReason = `PRD item removed from ${file}`;
|
|
1299
|
+
cancelled++;
|
|
1300
|
+
}
|
|
1298
1301
|
}
|
|
1299
|
-
|
|
1300
|
-
if (cancelled > 0) log('info', `Plan sync: cancelled ${cancelled} item(s) removed from ${file} → ${projName}`);
|
|
1302
|
+
if (cancelled > 0) log('info', `Plan sync: cancelled ${cancelled} item(s) removed from ${file} → ${projName}`);
|
|
1301
1303
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
}
|
|
1304
|
+
log('info', `Plan discovery: created ${created} work item(s) from ${file} → ${projName}`);
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1305
1307
|
totalCreated += created;
|
|
1306
1308
|
}
|
|
1307
1309
|
|
|
@@ -1336,11 +1338,11 @@ function clearPendingHumanFeedbackFlag(projectMeta, prId) {
|
|
|
1336
1338
|
if (!prId) return;
|
|
1337
1339
|
try {
|
|
1338
1340
|
const prsPath = projectPrPath(projectMeta);
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1341
|
+
mutatePullRequests(prsPath, prs => {
|
|
1342
|
+
const target = prs.find(p => p.id === prId);
|
|
1343
|
+
if (!target?.humanFeedback?.pendingFix) return;
|
|
1344
|
+
target.humanFeedback.pendingFix = false;
|
|
1345
|
+
});
|
|
1344
1346
|
} catch (e) { log('warn', 'clear pending human feedback flag: ' + e.message); }
|
|
1345
1347
|
}
|
|
1346
1348
|
|
|
@@ -1488,12 +1490,12 @@ function discoverFromPrs(config, project) {
|
|
|
1488
1490
|
// Mark notified to prevent duplicate alerts
|
|
1489
1491
|
try {
|
|
1490
1492
|
const prPath = projectPrPath(project);
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
}
|
|
1493
|
+
mutatePullRequests(prPath, prs => {
|
|
1494
|
+
const target = prs.find(p => p.id === pr.id);
|
|
1495
|
+
if (target) {
|
|
1496
|
+
target._buildFailNotified = true;
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1497
1499
|
} catch (e) { log('warn', 'mark build fail notified: ' + e.message); }
|
|
1498
1500
|
}
|
|
1499
1501
|
}
|
|
@@ -1738,14 +1740,13 @@ function discoverFromWorkItems(config, project) {
|
|
|
1738
1740
|
}
|
|
1739
1741
|
|
|
1740
1742
|
// Write back updated statuses (always, since we mark items dispatched before newWork check)
|
|
1741
|
-
if (newWork.length > 0) {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1743
|
+
if (newWork.length > 0 || needsWrite) {
|
|
1744
|
+
mutateWorkItems(projectWorkItemsPath(project), () => items);
|
|
1745
|
+
if (newWork.length > 0) {
|
|
1746
|
+
for (const s of prdSyncQueue) syncPrdItemStatus(s.id, 'dispatched', s.sourcePlan);
|
|
1747
|
+
}
|
|
1745
1748
|
}
|
|
1746
1749
|
|
|
1747
|
-
if (needsWrite) safeWrite(projectWorkItemsPath(project), items);
|
|
1748
|
-
|
|
1749
1750
|
const skipTotal = skipped.gated + skipped.noAgent;
|
|
1750
1751
|
if (skipTotal > 0) {
|
|
1751
1752
|
log('debug', `Work item discovery (${project?.name}): skipped ${skipTotal} items (${skipped.gated} gated, ${skipped.noAgent} no agent)`);
|
|
@@ -1839,53 +1840,50 @@ function materializeSpecsAsWorkItems(config, project) {
|
|
|
1839
1840
|
if (recentSpecs.length === 0) return;
|
|
1840
1841
|
|
|
1841
1842
|
const wiPath = projectWorkItemsPath(project);
|
|
1842
|
-
const existingItems = safeJson(wiPath) || [];
|
|
1843
1843
|
let created = 0;
|
|
1844
1844
|
|
|
1845
|
-
|
|
1846
|
-
const
|
|
1847
|
-
|
|
1848
|
-
const
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
if (matchedSpecs.length === 0) {
|
|
1854
|
-
tracker.processedPrs[pr.id] = { processedAt: ts(), matched: false };
|
|
1855
|
-
continue;
|
|
1856
|
-
}
|
|
1845
|
+
mutateWorkItems(wiPath, existingItems => {
|
|
1846
|
+
for (const pr of mergedPrs) {
|
|
1847
|
+
const prBranch = (pr.branch || '').toLowerCase();
|
|
1848
|
+
const matchedSpecs = recentSpecs.filter(doc => {
|
|
1849
|
+
const msg = doc.message.toLowerCase();
|
|
1850
|
+
// Match any doc whose commit message references this PR's branch
|
|
1851
|
+
return prBranch && msg.includes(prBranch.split('/').pop());
|
|
1852
|
+
});
|
|
1857
1853
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1854
|
+
if (matchedSpecs.length === 0) {
|
|
1855
|
+
tracker.processedPrs[pr.id] = { processedAt: ts(), matched: false };
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1860
1858
|
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1859
|
+
for (const doc of matchedSpecs) {
|
|
1860
|
+
if (existingItems.some(i => i.sourceSpec === doc.file)) continue;
|
|
1863
1861
|
|
|
1864
|
-
|
|
1862
|
+
const info = extractSpecInfo(doc.file, root);
|
|
1863
|
+
if (!info) continue;
|
|
1865
1864
|
|
|
1866
|
-
|
|
1867
|
-
id: newId,
|
|
1868
|
-
type: 'implement',
|
|
1869
|
-
title: `Implement: ${info.title}`,
|
|
1870
|
-
description: `Implementation work from merged spec.\n\n**Spec:** \`${doc.file}\`\n**Source PR:** ${pr.id} — ${pr.title || ''}\n**PR URL:** ${pr.url || 'N/A'}\n\n## Summary\n\n${info.summary}\n\nRead the full spec at \`${doc.file}\` before starting.`,
|
|
1871
|
-
priority: info.priority,
|
|
1872
|
-
status: 'queued',
|
|
1873
|
-
created: ts(),
|
|
1874
|
-
createdBy: 'engine:spec-discovery',
|
|
1875
|
-
sourceSpec: doc.file,
|
|
1876
|
-
sourcePr: pr.id
|
|
1877
|
-
});
|
|
1878
|
-
created++;
|
|
1879
|
-
log('info', `Spec discovery: created ${newId} "${info.title}" from PR ${pr.id} in ${project.name}`);
|
|
1880
|
-
}
|
|
1865
|
+
const newId = 'SP-' + shared.uid();
|
|
1881
1866
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1867
|
+
existingItems.push({
|
|
1868
|
+
id: newId,
|
|
1869
|
+
type: 'implement',
|
|
1870
|
+
title: `Implement: ${info.title}`,
|
|
1871
|
+
description: `Implementation work from merged spec.\n\n**Spec:** \`${doc.file}\`\n**Source PR:** ${pr.id} — ${pr.title || ''}\n**PR URL:** ${pr.url || 'N/A'}\n\n## Summary\n\n${info.summary}\n\nRead the full spec at \`${doc.file}\` before starting.`,
|
|
1872
|
+
priority: info.priority,
|
|
1873
|
+
status: 'queued',
|
|
1874
|
+
created: ts(),
|
|
1875
|
+
createdBy: 'engine:spec-discovery',
|
|
1876
|
+
sourceSpec: doc.file,
|
|
1877
|
+
sourcePr: pr.id
|
|
1878
|
+
});
|
|
1879
|
+
created++;
|
|
1880
|
+
log('info', `Spec discovery: created ${newId} "${info.title}" from PR ${pr.id} in ${project.name}`);
|
|
1881
|
+
}
|
|
1884
1882
|
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1883
|
+
tracker.processedPrs[pr.id] = { processedAt: ts(), matched: true, specs: matchedSpecs.map(d => d.file) };
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
mutateJsonFileLocked(trackerPath, () => tracker, { defaultValue: {} });
|
|
1889
1887
|
}
|
|
1890
1888
|
|
|
1891
1889
|
/**
|
|
@@ -2472,78 +2470,77 @@ async function tickInner() {
|
|
|
2472
2470
|
for (const project of projects) {
|
|
2473
2471
|
try {
|
|
2474
2472
|
const wiPath = projectWorkItemsPath(project);
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
mutateDispatch((dp) => {
|
|
2503
|
-
dp.completed = dp.completed.filter(d => d.meta?.dispatchKey !== key);
|
|
2504
|
-
return dp;
|
|
2505
|
-
});
|
|
2506
|
-
} catch (e) { log('warn', 'stall recovery clear dispatch: ' + e.message); }
|
|
2507
|
-
|
|
2508
|
-
// Clear cooldown so item isn't blocked by exponential backoff
|
|
2509
|
-
try {
|
|
2510
|
-
const key = `work-${project.name}-${item.id}`;
|
|
2511
|
-
if (dispatchCooldowns.has(key)) {
|
|
2512
|
-
dispatchCooldowns.delete(key);
|
|
2513
|
-
saveCooldowns();
|
|
2514
|
-
}
|
|
2515
|
-
} catch (e) { log('warn', 'stall recovery clear cooldown: ' + e.message); }
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
// Un-fail dependent items that were cascade-failed
|
|
2520
|
-
if (changed) {
|
|
2521
|
-
const retriedIds = new Set(items.filter(w => w.status === WI_STATUS.PENDING && w._retryCount === 0).map(w => w.id));
|
|
2522
|
-
for (const dep of items) {
|
|
2523
|
-
if (dep.status === WI_STATUS.FAILED && !isItemCompleted(dep) && dep.failReason === 'Dependency failed — cannot proceed') {
|
|
2524
|
-
const blockers = (dep.depends_on || []).filter(d => retriedIds.has(d));
|
|
2525
|
-
if (blockers.length > 0) {
|
|
2526
|
-
log('info', `Stall recovery: un-failing ${dep.id} (blocker ${blockers.join(',')} retried)`);
|
|
2527
|
-
dep.status = WI_STATUS.PENDING;
|
|
2528
|
-
dep._retryCount = 0;
|
|
2529
|
-
delete dep.failReason;
|
|
2530
|
-
delete dep.failedAt;
|
|
2531
|
-
delete dep.dispatched_at;
|
|
2532
|
-
delete dep.dispatched_to;
|
|
2533
|
-
// Clear dispatch entries for this dependent too
|
|
2534
|
-
try {
|
|
2535
|
-
const key = `work-${project.name}-${dep.id}`;
|
|
2473
|
+
mutateWorkItems(wiPath, items => {
|
|
2474
|
+
let changed = false;
|
|
2475
|
+
const failedIds = new Set(items.filter(w => w.status === WI_STATUS.FAILED).map(w => w.id));
|
|
2476
|
+
const pendingWithBlockedDeps = items.filter(w =>
|
|
2477
|
+
w.status === WI_STATUS.PENDING && (w.depends_on || []).some(d => failedIds.has(d))
|
|
2478
|
+
);
|
|
2479
|
+
|
|
2480
|
+
if (pendingWithBlockedDeps.length > 0) {
|
|
2481
|
+
// Auto-retry failed items that are blocking others (transient errors)
|
|
2482
|
+
for (const item of items) {
|
|
2483
|
+
if (item.status !== WI_STATUS.FAILED || isItemCompleted(item)) continue;
|
|
2484
|
+
// Only retry if something depends on this item
|
|
2485
|
+
const isBlocking = items.some(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(item.id));
|
|
2486
|
+
if (!isBlocking) continue;
|
|
2487
|
+
|
|
2488
|
+
log('info', `Stall recovery: auto-retrying ${item.id} (blocking ${pendingWithBlockedDeps.filter(w => (w.depends_on || []).includes(item.id)).length} items)`);
|
|
2489
|
+
item.status = WI_STATUS.PENDING;
|
|
2490
|
+
item._retryCount = 0;
|
|
2491
|
+
delete item.failReason;
|
|
2492
|
+
delete item.failedAt;
|
|
2493
|
+
delete item.dispatched_at;
|
|
2494
|
+
delete item.dispatched_to;
|
|
2495
|
+
changed = true;
|
|
2496
|
+
|
|
2497
|
+
// Clear completed dispatch entries so isAlreadyDispatched doesn't block re-dispatch
|
|
2498
|
+
try {
|
|
2499
|
+
const key = `work-${project.name}-${item.id}`;
|
|
2536
2500
|
mutateDispatch((dp) => {
|
|
2537
2501
|
dp.completed = dp.completed.filter(d => d.meta?.dispatchKey !== key);
|
|
2538
2502
|
return dp;
|
|
2539
2503
|
});
|
|
2540
|
-
} catch (e) { log('warn', 'stall recovery clear
|
|
2541
|
-
|
|
2504
|
+
} catch (e) { log('warn', 'stall recovery clear dispatch: ' + e.message); }
|
|
2505
|
+
|
|
2506
|
+
// Clear cooldown so item isn't blocked by exponential backoff
|
|
2507
|
+
try {
|
|
2508
|
+
const key = `work-${project.name}-${item.id}`;
|
|
2509
|
+
if (dispatchCooldowns.has(key)) {
|
|
2510
|
+
dispatchCooldowns.delete(key);
|
|
2511
|
+
saveCooldowns();
|
|
2512
|
+
}
|
|
2513
|
+
} catch (e) { log('warn', 'stall recovery clear cooldown: ' + e.message); }
|
|
2542
2514
|
}
|
|
2543
2515
|
}
|
|
2544
|
-
}
|
|
2545
2516
|
|
|
2546
|
-
|
|
2517
|
+
// Un-fail dependent items that were cascade-failed
|
|
2518
|
+
if (changed) {
|
|
2519
|
+
const retriedIds = new Set(items.filter(w => w.status === WI_STATUS.PENDING && w._retryCount === 0).map(w => w.id));
|
|
2520
|
+
for (const dep of items) {
|
|
2521
|
+
if (dep.status === WI_STATUS.FAILED && !isItemCompleted(dep) && dep.failReason === 'Dependency failed — cannot proceed') {
|
|
2522
|
+
const blockers = (dep.depends_on || []).filter(d => retriedIds.has(d));
|
|
2523
|
+
if (blockers.length > 0) {
|
|
2524
|
+
log('info', `Stall recovery: un-failing ${dep.id} (blocker ${blockers.join(',')} retried)`);
|
|
2525
|
+
dep.status = WI_STATUS.PENDING;
|
|
2526
|
+
dep._retryCount = 0;
|
|
2527
|
+
delete dep.failReason;
|
|
2528
|
+
delete dep.failedAt;
|
|
2529
|
+
delete dep.dispatched_at;
|
|
2530
|
+
delete dep.dispatched_to;
|
|
2531
|
+
// Clear dispatch entries for this dependent too
|
|
2532
|
+
try {
|
|
2533
|
+
const key = `work-${project.name}-${dep.id}`;
|
|
2534
|
+
mutateDispatch((dp) => {
|
|
2535
|
+
dp.completed = dp.completed.filter(d => d.meta?.dispatchKey !== key);
|
|
2536
|
+
return dp;
|
|
2537
|
+
});
|
|
2538
|
+
} catch (e) { log('warn', 'stall recovery clear dependent dispatch: ' + e.message); }
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2547
2544
|
} catch (e) { log('warn', 'stall recovery process project: ' + e.message); }
|
|
2548
2545
|
}
|
|
2549
2546
|
}
|
|
@@ -2628,19 +2625,19 @@ async function tickInner() {
|
|
|
2628
2625
|
? path.join(ENGINE_DIR, '..', 'work-items.json')
|
|
2629
2626
|
: item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
|
|
2630
2627
|
if (wiPath) {
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
}
|
|
2628
|
+
mutateWorkItems(wiPath, items => {
|
|
2629
|
+
const wi = items.find(i => i.id === item.meta.item.id);
|
|
2630
|
+
if (wi && wi.status === WI_STATUS.DISPATCHED) {
|
|
2631
|
+
// completeDispatch didn't update the work item — re-queue manually
|
|
2632
|
+
wi.status = WI_STATUS.PENDING;
|
|
2633
|
+
wi._retryCount = (wi._retryCount || 0) + 1;
|
|
2634
|
+
wi._lastRetryReason = 'spawnAgent returned null';
|
|
2635
|
+
wi._lastRetryAt = ts();
|
|
2636
|
+
delete wi.dispatched_at;
|
|
2637
|
+
delete wi.dispatched_to;
|
|
2638
|
+
log('info', `Re-queued ${item.meta.item.id} as pending (retry ${wi._retryCount})`);
|
|
2639
|
+
}
|
|
2640
|
+
});
|
|
2644
2641
|
}
|
|
2645
2642
|
} catch (e) { log('warn', `Failed to re-queue work item after spawn failure: ${e.message}`); }
|
|
2646
2643
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.535",
|
|
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"
|