@yemi33/minions 0.1.534 → 0.1.536
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 +9 -1
- package/engine.js +200 -210
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.536 (2026-04-07)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- plan regenerate/execute always queues fresh plan-to-prd (#449)
|
|
7
|
+
|
|
8
|
+
## 0.1.535 (2026-04-07)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- Convert engine.js safeWrite calls to mutateWorkItems/mutatePullRequests (#416)
|
|
4
12
|
|
|
5
13
|
### Fixes
|
|
6
14
|
- update pipeline work item status on dispatch (#443)
|
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) {
|
|
@@ -1096,18 +1098,11 @@ function materializePlansAsWorkItems(config) {
|
|
|
1096
1098
|
plan.sourcePlanModifiedAt = new Date(sourceMtime).toISOString();
|
|
1097
1099
|
plan.lastSyncedFromPlan = ts();
|
|
1098
1100
|
|
|
1099
|
-
// Handle PRD based on current status
|
|
1101
|
+
// Handle PRD based on current status — auto-regenerate for any state
|
|
1100
1102
|
const prdStatus = plan.status || (plan.requires_approval ? 'awaiting-approval' : null);
|
|
1101
1103
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
plan.planStale = true;
|
|
1105
|
-
log('info', `PRD ${file} flagged as stale (plan revised while ${prdStatus}) — user can regenerate from dashboard`);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// Awaiting-approval PRDs: invalidate, carry over completed items, delete old PRD, queue regeneration
|
|
1109
|
-
if (prdStatus === 'awaiting-approval') {
|
|
1110
|
-
log('info', `PRD ${file} invalidated (was awaiting-approval) — queuing regeneration from revised plan`);
|
|
1104
|
+
if (prdStatus) {
|
|
1105
|
+
log('info', `PRD ${file} invalidated (was ${prdStatus}) — queuing regeneration from revised plan`);
|
|
1111
1106
|
|
|
1112
1107
|
// Collect completed items to carry over to new PRD
|
|
1113
1108
|
const completedStatuses = new Set(['done', 'in-pr', 'implemented']); // in-pr kept for backward compat
|
|
@@ -1140,7 +1135,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
1140
1135
|
title: `Regenerate PRD from revised plan: ${plan.source_plan}`,
|
|
1141
1136
|
type: 'plan-to-prd',
|
|
1142
1137
|
priority: 'high',
|
|
1143
|
-
description: `Plan file: plans/${plan.source_plan}\nSource plan was revised
|
|
1138
|
+
description: `Plan file: plans/${plan.source_plan}\nSource plan was revised (PRD was ${prdStatus}) — regenerating.${completedContext}`,
|
|
1144
1139
|
status: 'pending',
|
|
1145
1140
|
created: ts(),
|
|
1146
1141
|
createdBy: 'engine:plan-revision',
|
|
@@ -1237,71 +1232,71 @@ function materializePlansAsWorkItems(config) {
|
|
|
1237
1232
|
let totalCreated = 0;
|
|
1238
1233
|
for (const [projName, { project, items: projItems }] of itemsByProject) {
|
|
1239
1234
|
const wiPath = project ? projectWorkItemsPath(project) : path.join(MINIONS_DIR, 'work-items.json');
|
|
1240
|
-
const existingItems = safeJson(wiPath) || [];
|
|
1241
1235
|
let created = 0;
|
|
1242
1236
|
const newlyCreatedIds = new Set(); // tracks IDs created in this pass for reconciliation scoping
|
|
1243
1237
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1238
|
+
mutateWorkItems(wiPath, existingItems => {
|
|
1239
|
+
for (const item of projItems) {
|
|
1240
|
+
// Skip if already materialized — work item ID = PRD item ID, check all projects
|
|
1241
|
+
let alreadyExists = existingItems.some(w => w.id === item.id);
|
|
1242
|
+
if (!alreadyExists) {
|
|
1243
|
+
for (const p of allProjects) {
|
|
1244
|
+
if (p.name === projName) continue;
|
|
1245
|
+
const otherItems = safeJson(projectWorkItemsPath(p)) || [];
|
|
1246
|
+
if (otherItems.some(w => w.id === item.id)) { alreadyExists = true; break; }
|
|
1247
|
+
}
|
|
1252
1248
|
}
|
|
1249
|
+
if (alreadyExists) continue;
|
|
1250
|
+
// Skip items involved in dependency cycles
|
|
1251
|
+
if (cycleSet.has(item.id)) continue;
|
|
1252
|
+
|
|
1253
|
+
const id = item.id; // Work item ID = PRD item ID — no indirection
|
|
1254
|
+
const complexity = item.estimated_complexity || 'medium';
|
|
1255
|
+
const criteria = (item.acceptance_criteria || []).map(c => `- ${c}`).join('\n');
|
|
1256
|
+
|
|
1257
|
+
const newItem = {
|
|
1258
|
+
id,
|
|
1259
|
+
title: `Implement: ${item.name}`,
|
|
1260
|
+
type: complexity === 'large' ? 'implement:large' : 'implement',
|
|
1261
|
+
priority: item.priority || 'medium',
|
|
1262
|
+
description: `${item.description || ''}\n\n**Plan:** ${file}\n**Plan Item:** ${item.id}\n**Complexity:** ${complexity}${criteria ? '\n\n**Acceptance Criteria:**\n' + criteria : ''}`,
|
|
1263
|
+
status: 'pending',
|
|
1264
|
+
created: ts(),
|
|
1265
|
+
createdBy: 'engine:plan-discovery',
|
|
1266
|
+
sourcePlan: file,
|
|
1267
|
+
depends_on: item.depends_on || [],
|
|
1268
|
+
branchStrategy: plan.branch_strategy || 'parallel',
|
|
1269
|
+
featureBranch: plan.feature_branch || null,
|
|
1270
|
+
project: item.project || plan.project || null,
|
|
1271
|
+
};
|
|
1272
|
+
existingItems.push(newItem);
|
|
1273
|
+
newlyCreatedIds.add(id);
|
|
1274
|
+
created++;
|
|
1253
1275
|
}
|
|
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
1276
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1277
|
+
if (created > 0) {
|
|
1278
|
+
// Reconciliation: exact prdItems match only, scoped to newly created items
|
|
1279
|
+
const allPrsForReconcile = allProjects.flatMap(p => safeJson(projectPrPath(p)) || []);
|
|
1280
|
+
const reconciled = reconcileItemsWithPrs(existingItems, allPrsForReconcile, { onlyIds: newlyCreatedIds });
|
|
1281
|
+
if (reconciled > 0) log('info', `Plan reconciliation: marked ${reconciled} item(s) as done → ${projName}`);
|
|
1282
|
+
|
|
1283
|
+
// PRD removal sync: cancel pending work items whose PRD item was removed from the plan
|
|
1284
|
+
const currentPrdIds = new Set(plan.missing_features.map(f => f.id));
|
|
1285
|
+
let cancelled = 0;
|
|
1286
|
+
for (const wi of existingItems) {
|
|
1287
|
+
if (wi.status !== WI_STATUS.PENDING || wi.sourcePlan !== file) continue;
|
|
1288
|
+
if (!currentPrdIds.has(wi.id)) {
|
|
1289
|
+
wi.status = WI_STATUS.CANCELLED;
|
|
1290
|
+
wi.cancelledAt = ts();
|
|
1291
|
+
wi.cancelReason = `PRD item removed from ${file}`;
|
|
1292
|
+
cancelled++;
|
|
1293
|
+
}
|
|
1298
1294
|
}
|
|
1299
|
-
|
|
1300
|
-
if (cancelled > 0) log('info', `Plan sync: cancelled ${cancelled} item(s) removed from ${file} → ${projName}`);
|
|
1295
|
+
if (cancelled > 0) log('info', `Plan sync: cancelled ${cancelled} item(s) removed from ${file} → ${projName}`);
|
|
1301
1296
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
}
|
|
1297
|
+
log('info', `Plan discovery: created ${created} work item(s) from ${file} → ${projName}`);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1305
1300
|
totalCreated += created;
|
|
1306
1301
|
}
|
|
1307
1302
|
|
|
@@ -1336,11 +1331,11 @@ function clearPendingHumanFeedbackFlag(projectMeta, prId) {
|
|
|
1336
1331
|
if (!prId) return;
|
|
1337
1332
|
try {
|
|
1338
1333
|
const prsPath = projectPrPath(projectMeta);
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1334
|
+
mutatePullRequests(prsPath, prs => {
|
|
1335
|
+
const target = prs.find(p => p.id === prId);
|
|
1336
|
+
if (!target?.humanFeedback?.pendingFix) return;
|
|
1337
|
+
target.humanFeedback.pendingFix = false;
|
|
1338
|
+
});
|
|
1344
1339
|
} catch (e) { log('warn', 'clear pending human feedback flag: ' + e.message); }
|
|
1345
1340
|
}
|
|
1346
1341
|
|
|
@@ -1488,12 +1483,12 @@ function discoverFromPrs(config, project) {
|
|
|
1488
1483
|
// Mark notified to prevent duplicate alerts
|
|
1489
1484
|
try {
|
|
1490
1485
|
const prPath = projectPrPath(project);
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
}
|
|
1486
|
+
mutatePullRequests(prPath, prs => {
|
|
1487
|
+
const target = prs.find(p => p.id === pr.id);
|
|
1488
|
+
if (target) {
|
|
1489
|
+
target._buildFailNotified = true;
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1497
1492
|
} catch (e) { log('warn', 'mark build fail notified: ' + e.message); }
|
|
1498
1493
|
}
|
|
1499
1494
|
}
|
|
@@ -1738,14 +1733,13 @@ function discoverFromWorkItems(config, project) {
|
|
|
1738
1733
|
}
|
|
1739
1734
|
|
|
1740
1735
|
// Write back updated statuses (always, since we mark items dispatched before newWork check)
|
|
1741
|
-
if (newWork.length > 0) {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1736
|
+
if (newWork.length > 0 || needsWrite) {
|
|
1737
|
+
mutateWorkItems(projectWorkItemsPath(project), () => items);
|
|
1738
|
+
if (newWork.length > 0) {
|
|
1739
|
+
for (const s of prdSyncQueue) syncPrdItemStatus(s.id, 'dispatched', s.sourcePlan);
|
|
1740
|
+
}
|
|
1745
1741
|
}
|
|
1746
1742
|
|
|
1747
|
-
if (needsWrite) safeWrite(projectWorkItemsPath(project), items);
|
|
1748
|
-
|
|
1749
1743
|
const skipTotal = skipped.gated + skipped.noAgent;
|
|
1750
1744
|
if (skipTotal > 0) {
|
|
1751
1745
|
log('debug', `Work item discovery (${project?.name}): skipped ${skipTotal} items (${skipped.gated} gated, ${skipped.noAgent} no agent)`);
|
|
@@ -1839,53 +1833,50 @@ function materializeSpecsAsWorkItems(config, project) {
|
|
|
1839
1833
|
if (recentSpecs.length === 0) return;
|
|
1840
1834
|
|
|
1841
1835
|
const wiPath = projectWorkItemsPath(project);
|
|
1842
|
-
const existingItems = safeJson(wiPath) || [];
|
|
1843
1836
|
let created = 0;
|
|
1844
1837
|
|
|
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
|
-
}
|
|
1838
|
+
mutateWorkItems(wiPath, existingItems => {
|
|
1839
|
+
for (const pr of mergedPrs) {
|
|
1840
|
+
const prBranch = (pr.branch || '').toLowerCase();
|
|
1841
|
+
const matchedSpecs = recentSpecs.filter(doc => {
|
|
1842
|
+
const msg = doc.message.toLowerCase();
|
|
1843
|
+
// Match any doc whose commit message references this PR's branch
|
|
1844
|
+
return prBranch && msg.includes(prBranch.split('/').pop());
|
|
1845
|
+
});
|
|
1857
1846
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1847
|
+
if (matchedSpecs.length === 0) {
|
|
1848
|
+
tracker.processedPrs[pr.id] = { processedAt: ts(), matched: false };
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1860
1851
|
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1852
|
+
for (const doc of matchedSpecs) {
|
|
1853
|
+
if (existingItems.some(i => i.sourceSpec === doc.file)) continue;
|
|
1863
1854
|
|
|
1864
|
-
|
|
1855
|
+
const info = extractSpecInfo(doc.file, root);
|
|
1856
|
+
if (!info) continue;
|
|
1865
1857
|
|
|
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
|
-
}
|
|
1858
|
+
const newId = 'SP-' + shared.uid();
|
|
1881
1859
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1860
|
+
existingItems.push({
|
|
1861
|
+
id: newId,
|
|
1862
|
+
type: 'implement',
|
|
1863
|
+
title: `Implement: ${info.title}`,
|
|
1864
|
+
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.`,
|
|
1865
|
+
priority: info.priority,
|
|
1866
|
+
status: 'queued',
|
|
1867
|
+
created: ts(),
|
|
1868
|
+
createdBy: 'engine:spec-discovery',
|
|
1869
|
+
sourceSpec: doc.file,
|
|
1870
|
+
sourcePr: pr.id
|
|
1871
|
+
});
|
|
1872
|
+
created++;
|
|
1873
|
+
log('info', `Spec discovery: created ${newId} "${info.title}" from PR ${pr.id} in ${project.name}`);
|
|
1874
|
+
}
|
|
1884
1875
|
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1876
|
+
tracker.processedPrs[pr.id] = { processedAt: ts(), matched: true, specs: matchedSpecs.map(d => d.file) };
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
mutateJsonFileLocked(trackerPath, () => tracker, { defaultValue: {} });
|
|
1889
1880
|
}
|
|
1890
1881
|
|
|
1891
1882
|
/**
|
|
@@ -2472,78 +2463,77 @@ async function tickInner() {
|
|
|
2472
2463
|
for (const project of projects) {
|
|
2473
2464
|
try {
|
|
2474
2465
|
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}`;
|
|
2466
|
+
mutateWorkItems(wiPath, items => {
|
|
2467
|
+
let changed = false;
|
|
2468
|
+
const failedIds = new Set(items.filter(w => w.status === WI_STATUS.FAILED).map(w => w.id));
|
|
2469
|
+
const pendingWithBlockedDeps = items.filter(w =>
|
|
2470
|
+
w.status === WI_STATUS.PENDING && (w.depends_on || []).some(d => failedIds.has(d))
|
|
2471
|
+
);
|
|
2472
|
+
|
|
2473
|
+
if (pendingWithBlockedDeps.length > 0) {
|
|
2474
|
+
// Auto-retry failed items that are blocking others (transient errors)
|
|
2475
|
+
for (const item of items) {
|
|
2476
|
+
if (item.status !== WI_STATUS.FAILED || isItemCompleted(item)) continue;
|
|
2477
|
+
// Only retry if something depends on this item
|
|
2478
|
+
const isBlocking = items.some(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(item.id));
|
|
2479
|
+
if (!isBlocking) continue;
|
|
2480
|
+
|
|
2481
|
+
log('info', `Stall recovery: auto-retrying ${item.id} (blocking ${pendingWithBlockedDeps.filter(w => (w.depends_on || []).includes(item.id)).length} items)`);
|
|
2482
|
+
item.status = WI_STATUS.PENDING;
|
|
2483
|
+
item._retryCount = 0;
|
|
2484
|
+
delete item.failReason;
|
|
2485
|
+
delete item.failedAt;
|
|
2486
|
+
delete item.dispatched_at;
|
|
2487
|
+
delete item.dispatched_to;
|
|
2488
|
+
changed = true;
|
|
2489
|
+
|
|
2490
|
+
// Clear completed dispatch entries so isAlreadyDispatched doesn't block re-dispatch
|
|
2491
|
+
try {
|
|
2492
|
+
const key = `work-${project.name}-${item.id}`;
|
|
2536
2493
|
mutateDispatch((dp) => {
|
|
2537
2494
|
dp.completed = dp.completed.filter(d => d.meta?.dispatchKey !== key);
|
|
2538
2495
|
return dp;
|
|
2539
2496
|
});
|
|
2540
|
-
} catch (e) { log('warn', 'stall recovery clear
|
|
2541
|
-
|
|
2497
|
+
} catch (e) { log('warn', 'stall recovery clear dispatch: ' + e.message); }
|
|
2498
|
+
|
|
2499
|
+
// Clear cooldown so item isn't blocked by exponential backoff
|
|
2500
|
+
try {
|
|
2501
|
+
const key = `work-${project.name}-${item.id}`;
|
|
2502
|
+
if (dispatchCooldowns.has(key)) {
|
|
2503
|
+
dispatchCooldowns.delete(key);
|
|
2504
|
+
saveCooldowns();
|
|
2505
|
+
}
|
|
2506
|
+
} catch (e) { log('warn', 'stall recovery clear cooldown: ' + e.message); }
|
|
2542
2507
|
}
|
|
2543
2508
|
}
|
|
2544
|
-
}
|
|
2545
2509
|
|
|
2546
|
-
|
|
2510
|
+
// Un-fail dependent items that were cascade-failed
|
|
2511
|
+
if (changed) {
|
|
2512
|
+
const retriedIds = new Set(items.filter(w => w.status === WI_STATUS.PENDING && w._retryCount === 0).map(w => w.id));
|
|
2513
|
+
for (const dep of items) {
|
|
2514
|
+
if (dep.status === WI_STATUS.FAILED && !isItemCompleted(dep) && dep.failReason === 'Dependency failed — cannot proceed') {
|
|
2515
|
+
const blockers = (dep.depends_on || []).filter(d => retriedIds.has(d));
|
|
2516
|
+
if (blockers.length > 0) {
|
|
2517
|
+
log('info', `Stall recovery: un-failing ${dep.id} (blocker ${blockers.join(',')} retried)`);
|
|
2518
|
+
dep.status = WI_STATUS.PENDING;
|
|
2519
|
+
dep._retryCount = 0;
|
|
2520
|
+
delete dep.failReason;
|
|
2521
|
+
delete dep.failedAt;
|
|
2522
|
+
delete dep.dispatched_at;
|
|
2523
|
+
delete dep.dispatched_to;
|
|
2524
|
+
// Clear dispatch entries for this dependent too
|
|
2525
|
+
try {
|
|
2526
|
+
const key = `work-${project.name}-${dep.id}`;
|
|
2527
|
+
mutateDispatch((dp) => {
|
|
2528
|
+
dp.completed = dp.completed.filter(d => d.meta?.dispatchKey !== key);
|
|
2529
|
+
return dp;
|
|
2530
|
+
});
|
|
2531
|
+
} catch (e) { log('warn', 'stall recovery clear dependent dispatch: ' + e.message); }
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
});
|
|
2547
2537
|
} catch (e) { log('warn', 'stall recovery process project: ' + e.message); }
|
|
2548
2538
|
}
|
|
2549
2539
|
}
|
|
@@ -2628,19 +2618,19 @@ async function tickInner() {
|
|
|
2628
2618
|
? path.join(ENGINE_DIR, '..', 'work-items.json')
|
|
2629
2619
|
: item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
|
|
2630
2620
|
if (wiPath) {
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
}
|
|
2621
|
+
mutateWorkItems(wiPath, items => {
|
|
2622
|
+
const wi = items.find(i => i.id === item.meta.item.id);
|
|
2623
|
+
if (wi && wi.status === WI_STATUS.DISPATCHED) {
|
|
2624
|
+
// completeDispatch didn't update the work item — re-queue manually
|
|
2625
|
+
wi.status = WI_STATUS.PENDING;
|
|
2626
|
+
wi._retryCount = (wi._retryCount || 0) + 1;
|
|
2627
|
+
wi._lastRetryReason = 'spawnAgent returned null';
|
|
2628
|
+
wi._lastRetryAt = ts();
|
|
2629
|
+
delete wi.dispatched_at;
|
|
2630
|
+
delete wi.dispatched_to;
|
|
2631
|
+
log('info', `Re-queued ${item.meta.item.id} as pending (retry ${wi._retryCount})`);
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2644
2634
|
}
|
|
2645
2635
|
} catch (e) { log('warn', `Failed to re-queue work item after spawn failure: ${e.message}`); }
|
|
2646
2636
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.536",
|
|
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"
|