@yemi33/minions 0.1.152 → 0.1.154
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 +28 -0
- package/dashboard/js/command-center.js +3 -5
- package/dashboard/js/modal-qa.js +3 -45
- package/dashboard/js/modal.js +0 -2
- package/dashboard/js/refresh.js +44 -0
- package/dashboard/js/render-inbox.js +0 -1
- package/dashboard/js/render-kb.js +0 -1
- package/dashboard/js/render-plans.js +2 -44
- package/dashboard/js/render-prd.js +10 -12
- package/dashboard/js/render-work-items.js +42 -8
- package/dashboard/styles.css +4 -2
- package/dashboard.html +8 -103
- package/dashboard.js +2 -144
- package/docs/deprecated.json +1 -83
- package/engine/lifecycle.js +72 -57
- package/engine/playbook.js +1 -1
- package/engine/queries.js +2 -4
- package/engine.js +5 -5
- package/package.json +1 -1
package/engine/lifecycle.js
CHANGED
|
@@ -58,7 +58,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
58
58
|
const unmaterialized = [...planFeatureIds].filter(id => {
|
|
59
59
|
if (workItemById[id]) return false;
|
|
60
60
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
61
|
-
return !(prdItem &&
|
|
61
|
+
return !(prdItem && prdItem.status === 'done');
|
|
62
62
|
});
|
|
63
63
|
if (unmaterialized.length > 0) {
|
|
64
64
|
log('info', `Plan ${planFile}: ${unmaterialized.length}/${planFeatureIds.size} feature(s) not yet materialized as work items: ${unmaterialized.join(', ')}`);
|
|
@@ -68,16 +68,16 @@ function checkPlanCompletion(meta, config) {
|
|
|
68
68
|
// Check 2: every feature's work item must be done (or PRD item marked done externally)
|
|
69
69
|
const notDone = [...planFeatureIds].filter(id => {
|
|
70
70
|
const w = workItemById[id];
|
|
71
|
-
if (w &&
|
|
71
|
+
if (w && w.status === 'done') return false;
|
|
72
72
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
73
|
-
return !(prdItem &&
|
|
73
|
+
return !(prdItem && prdItem.status === 'done');
|
|
74
74
|
});
|
|
75
75
|
if (notDone.length > 0) {
|
|
76
76
|
log('info', `Plan ${planFile}: waiting for done on ${notDone.length}/${planFeatureIds.size} item(s): ${notDone.join(', ')}`);
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const doneItems = planItems.filter(w => w.status === 'done'
|
|
80
|
+
const doneItems = planItems.filter(w => w.status === 'done');
|
|
81
81
|
const failedItems = planItems.filter(w => w.status === 'failed');
|
|
82
82
|
|
|
83
83
|
// 1. Mark plan as completed
|
|
@@ -603,7 +603,14 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
603
603
|
dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
|
|
604
604
|
}
|
|
605
605
|
const entry = dirtyTargets.get(targetName);
|
|
606
|
-
|
|
606
|
+
const existing = entry.prs.find(p => p.id === fullId || String(p.id) === String(prId));
|
|
607
|
+
if (existing) {
|
|
608
|
+
// Backfill prdItems if the entry was added by the poller before syncPrsFromOutput ran
|
|
609
|
+
if (meta?.item?.id && !existing.prdItems?.includes(meta.item.id)) {
|
|
610
|
+
existing.prdItems = [...(existing.prdItems || []), meta.item.id];
|
|
611
|
+
}
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
607
614
|
|
|
608
615
|
let title = meta?.item?.title || '';
|
|
609
616
|
const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
|
|
@@ -637,10 +644,26 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
637
644
|
|
|
638
645
|
// ─── Post-Completion Hooks ──────────────────────────────────────────────────
|
|
639
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Resolve which project's pull-requests.json contains a given PR ID.
|
|
649
|
+
* Returns the project object, or null if not found in any project file.
|
|
650
|
+
*/
|
|
651
|
+
function resolveProjectForPr(prId) {
|
|
652
|
+
const config = getConfig();
|
|
653
|
+
for (const p of shared.getProjects(config)) {
|
|
654
|
+
const prs = safeJson(projectPrPath(p)) || [];
|
|
655
|
+
if (prs.some(pr => pr.id === prId)) return p;
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
|
|
640
660
|
function updatePrAfterReview(agentId, pr, project) {
|
|
641
661
|
|
|
642
662
|
if (!pr?.id) return;
|
|
643
|
-
|
|
663
|
+
// Resolve actual project if not provided — avoids writing merged array to wrong path
|
|
664
|
+
const resolvedProject = project || resolveProjectForPr(pr.id);
|
|
665
|
+
if (!resolvedProject) { log('warn', `updatePrAfterReview: cannot resolve project for ${pr.id}`); return; }
|
|
666
|
+
const prs = getPrs(resolvedProject);
|
|
644
667
|
const target = prs.find(p => p.id === pr.id);
|
|
645
668
|
if (!target) return;
|
|
646
669
|
|
|
@@ -674,7 +697,7 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
674
697
|
shared.safeWrite(metricsPath, metrics);
|
|
675
698
|
}
|
|
676
699
|
|
|
677
|
-
shared.safeWrite(
|
|
700
|
+
shared.safeWrite(shared.projectPrPath(resolvedProject), prs);
|
|
678
701
|
log('info', `Updated ${pr.id} → minions review: ${target.reviewStatus} by ${reviewerName}`);
|
|
679
702
|
createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
|
|
680
703
|
}
|
|
@@ -682,7 +705,10 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
682
705
|
function updatePrAfterFix(pr, project, source) {
|
|
683
706
|
|
|
684
707
|
if (!pr?.id) return;
|
|
685
|
-
|
|
708
|
+
// Resolve actual project if not provided — avoids writing merged array to wrong path
|
|
709
|
+
const resolvedProject = project || resolveProjectForPr(pr.id);
|
|
710
|
+
if (!resolvedProject) { log('warn', `updatePrAfterFix: cannot resolve project for ${pr.id}`); return; }
|
|
711
|
+
const prs = getPrs(resolvedProject);
|
|
686
712
|
const target = prs.find(p => p.id === pr.id);
|
|
687
713
|
if (!target) return;
|
|
688
714
|
|
|
@@ -697,7 +723,7 @@ function updatePrAfterFix(pr, project, source) {
|
|
|
697
723
|
log('info', `Updated ${pr.id} → reviewStatus: waiting (fix pushed)`);
|
|
698
724
|
}
|
|
699
725
|
|
|
700
|
-
shared.safeWrite(
|
|
726
|
+
shared.safeWrite(shared.projectPrPath(resolvedProject), prs);
|
|
701
727
|
}
|
|
702
728
|
|
|
703
729
|
// ─── Post-Merge / Post-Close Hooks ───────────────────────────────────────────
|
|
@@ -740,7 +766,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
740
766
|
}
|
|
741
767
|
|
|
742
768
|
if (mergedItemId) {
|
|
743
|
-
// Mark PRD feature as
|
|
769
|
+
// Mark PRD feature as done
|
|
744
770
|
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
745
771
|
try {
|
|
746
772
|
const planFiles = fs.readdirSync(prdDir).filter(f => f.endsWith('.json'));
|
|
@@ -749,13 +775,13 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
749
775
|
const plan = safeJson(path.join(prdDir, pf));
|
|
750
776
|
if (!plan?.missing_features) continue;
|
|
751
777
|
const feature = plan.missing_features.find(f => f.id === mergedItemId);
|
|
752
|
-
if (feature && feature.status !== '
|
|
753
|
-
feature.status = '
|
|
778
|
+
if (feature && feature.status !== 'done') {
|
|
779
|
+
feature.status = 'done';
|
|
754
780
|
shared.safeWrite(path.join(prdDir, pf), plan);
|
|
755
781
|
updated++;
|
|
756
782
|
}
|
|
757
783
|
}
|
|
758
|
-
if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as
|
|
784
|
+
if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as done for ${pr.id}`);
|
|
759
785
|
} catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
|
|
760
786
|
|
|
761
787
|
// Mark work item as done
|
|
@@ -1011,7 +1037,8 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
1011
1037
|
if (taskUsage.numTurns > cp.maxTurns) cp.maxTurns = taskUsage.numTurns;
|
|
1012
1038
|
// Check if this dispatch hit the turn limit
|
|
1013
1039
|
const engineConfig = require('./queries').getConfig()?.engine || {};
|
|
1014
|
-
|
|
1040
|
+
// maxTurns default defined in ENGINE_DEFAULTS (shared.js) — avoid hardcoded fallback
|
|
1041
|
+
const turnLimit = engineConfig.maxTurns || shared.ENGINE_DEFAULTS.maxTurns;
|
|
1015
1042
|
if (taskUsage.numTurns >= turnLimit) cp.turnLimitHits++;
|
|
1016
1043
|
}
|
|
1017
1044
|
|
|
@@ -1206,7 +1233,8 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1206
1233
|
if (meta?.item?.id) {
|
|
1207
1234
|
try {
|
|
1208
1235
|
const engineConfig = (config.engine || {});
|
|
1209
|
-
|
|
1236
|
+
// maxTurns resolved from config, fallback to ENGINE_DEFAULTS (shared.js)
|
|
1237
|
+
const turnLimit = engineConfig.maxTurns || shared.ENGINE_DEFAULTS.maxTurns;
|
|
1210
1238
|
const turnCount = taskUsage?.numTurns || 0;
|
|
1211
1239
|
const hitTurnLimit = turnCount >= turnLimit;
|
|
1212
1240
|
let outputLogSizeBytes = 0;
|
|
@@ -1321,51 +1349,37 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1321
1349
|
}
|
|
1322
1350
|
|
|
1323
1351
|
if (!isSuccess && meta?.item?.id) {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1328
|
-
? path.join(MINIONS_DIR, 'work-items.json')
|
|
1329
|
-
: meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
|
|
1330
|
-
if (wiPath) {
|
|
1331
|
-
const items = safeJson(wiPath) || [];
|
|
1332
|
-
const wi = items.find(i => i.id === meta.item.id);
|
|
1333
|
-
if (wi) retries = (wi._retryCount || 0); // Use fresh value from file
|
|
1334
|
-
}
|
|
1335
|
-
} catch { /* optional */ }
|
|
1336
|
-
|
|
1337
|
-
if (retries < 3) {
|
|
1338
|
-
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
|
|
1339
|
-
updateWorkItemStatus(meta, 'pending', '');
|
|
1352
|
+
const wiPath = resolveWiPath(meta);
|
|
1353
|
+
if (wiPath) {
|
|
1354
|
+
let finalStatus = null;
|
|
1340
1355
|
try {
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
: meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
|
|
1344
|
-
if (wiPath) {
|
|
1345
|
-
const items = safeJson(wiPath) || [];
|
|
1356
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
1357
|
+
if (!Array.isArray(items)) return items;
|
|
1346
1358
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1347
|
-
if (wi)
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1359
|
+
if (!wi) return items;
|
|
1360
|
+
|
|
1361
|
+
const retries = wi._retryCount || 0;
|
|
1362
|
+
if (retries < 3) {
|
|
1363
|
+
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
|
|
1364
|
+
wi._retryCount = retries + 1;
|
|
1365
|
+
wi.status = 'pending';
|
|
1366
|
+
delete wi.dispatched_at;
|
|
1367
|
+
delete wi.dispatched_to;
|
|
1368
|
+
finalStatus = 'pending';
|
|
1369
|
+
} else {
|
|
1370
|
+
wi.status = 'failed';
|
|
1371
|
+
wi.failReason = 'Agent failed (3 retries exhausted)';
|
|
1372
|
+
wi.failedAt = ts();
|
|
1373
|
+
finalStatus = 'failed';
|
|
1351
1374
|
}
|
|
1352
|
-
|
|
1375
|
+
if (type === 'decompose') delete wi._decomposing;
|
|
1376
|
+
return items;
|
|
1377
|
+
});
|
|
1353
1378
|
} catch (err) { log('warn', `Retry update: ${err.message}`); }
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
if (type === 'decompose') {
|
|
1359
|
-
try {
|
|
1360
|
-
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1361
|
-
? path.join(MINIONS_DIR, 'work-items.json')
|
|
1362
|
-
: meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
|
|
1363
|
-
if (wiPath) {
|
|
1364
|
-
const items = safeJson(wiPath) || [];
|
|
1365
|
-
const wi = items.find(i => i.id === meta.item.id);
|
|
1366
|
-
if (wi) { delete wi._decomposing; shared.safeWrite(wiPath, items); }
|
|
1367
|
-
}
|
|
1368
|
-
} catch (err) { log('warn', `Decompose cleanup: ${err.message}`); }
|
|
1379
|
+
// Sync status to PRD outside the work-items lock
|
|
1380
|
+
if (finalStatus) {
|
|
1381
|
+
syncPrdItemStatus(meta.item.id, finalStatus, meta.item?.sourcePlan);
|
|
1382
|
+
}
|
|
1369
1383
|
}
|
|
1370
1384
|
}
|
|
1371
1385
|
// Meeting post-completion: collect findings/debate/conclusion
|
|
@@ -1418,7 +1432,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1418
1432
|
}
|
|
1419
1433
|
|
|
1420
1434
|
// Detect implement tasks that completed without creating a PR
|
|
1421
|
-
if (isSuccess && (type === 'implement' || type === 'implement:large' || type === 'fix') && prsCreatedCount === 0 && meta?.item?.id) {
|
|
1435
|
+
if (isSuccess && (type === 'implement' || type === 'implement:large' || type === 'fix') && prsCreatedCount === 0 && meta?.item?.id && !meta?.item?.skipPr) {
|
|
1422
1436
|
// Check if a PR already exists linked to this work item (from a previous attempt)
|
|
1423
1437
|
const projects = shared.getProjects(config);
|
|
1424
1438
|
const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
|
@@ -1511,6 +1525,7 @@ module.exports = {
|
|
|
1511
1525
|
updateWorkItemStatus,
|
|
1512
1526
|
syncPrdItemStatus,
|
|
1513
1527
|
syncPrsFromOutput,
|
|
1528
|
+
resolveProjectForPr,
|
|
1514
1529
|
updatePrAfterReview,
|
|
1515
1530
|
updatePrAfterFix,
|
|
1516
1531
|
handlePostMerge,
|
package/engine/playbook.js
CHANGED
|
@@ -478,7 +478,7 @@ function selectPlaybook(workType, item) {
|
|
|
478
478
|
if (workType === 'review' && !item?._pr && !item?.pr_id) {
|
|
479
479
|
return 'work-item';
|
|
480
480
|
}
|
|
481
|
-
const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose', 'meeting-investigate', 'meeting-debate', 'meeting-conclude'];
|
|
481
|
+
const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'evaluate', 'decompose', 'meeting-investigate', 'meeting-debate', 'meeting-conclude'];
|
|
482
482
|
return typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
|
|
483
483
|
}
|
|
484
484
|
|
package/engine/queries.js
CHANGED
|
@@ -506,9 +506,7 @@ function getWorkItems(config) {
|
|
|
506
506
|
pending: 0,
|
|
507
507
|
queued: 0,
|
|
508
508
|
dispatched: 1,
|
|
509
|
-
'in-pr': 3, // backward compat — treated as done
|
|
510
509
|
done: 3,
|
|
511
|
-
implemented: 3,
|
|
512
510
|
failed: 4,
|
|
513
511
|
paused: 5,
|
|
514
512
|
};
|
|
@@ -627,7 +625,7 @@ function getPrdInfo(config) {
|
|
|
627
625
|
|
|
628
626
|
const byStatus = {};
|
|
629
627
|
items.forEach(item => { const s = item.status || 'missing'; byStatus[s] = byStatus[s] || []; byStatus[s].push(item); });
|
|
630
|
-
const complete = (byStatus['done'] || []).length
|
|
628
|
+
const complete = (byStatus['done'] || []).length;
|
|
631
629
|
const inProgress = (byStatus['in-progress'] || []).length;
|
|
632
630
|
const paused = (byStatus['paused'] || []).length;
|
|
633
631
|
const missing = (byStatus['missing'] || []).length;
|
|
@@ -641,7 +639,7 @@ function getPrdInfo(config) {
|
|
|
641
639
|
const t = planTimings[wi.sourcePlan];
|
|
642
640
|
if (wi.dispatched_at) { const d = new Date(wi.dispatched_at).getTime(); if (!t.firstDispatched || d < t.firstDispatched) t.firstDispatched = d; }
|
|
643
641
|
if (wi.completedAt) { const c = new Date(wi.completedAt).getTime(); if (!t.lastCompleted || c > t.lastCompleted) t.lastCompleted = c; }
|
|
644
|
-
if (wi.status !== 'done'
|
|
642
|
+
if (wi.status !== 'done') t.allDone = false;
|
|
645
643
|
}
|
|
646
644
|
|
|
647
645
|
const progress = {
|
package/engine.js
CHANGED
|
@@ -743,7 +743,7 @@ function areDependenciesMet(item, config) {
|
|
|
743
743
|
} catch (e) { log('warn', 'read project work items for deps: ' + e.message); }
|
|
744
744
|
}
|
|
745
745
|
// PRD item statuses that count as "done" for dep resolution
|
|
746
|
-
const PRD_MET_STATUSES = new Set(['done'
|
|
746
|
+
const PRD_MET_STATUSES = new Set(['done']);
|
|
747
747
|
|
|
748
748
|
for (const depId of deps) {
|
|
749
749
|
const depItem = allWorkItems.find(w => w.id === depId);
|
|
@@ -977,7 +977,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
977
977
|
log('info', `PRD ${file} invalidated (was awaiting-approval) — queuing regeneration from revised plan`);
|
|
978
978
|
|
|
979
979
|
// Collect completed items to carry over to new PRD
|
|
980
|
-
const completedStatuses = new Set(['done'
|
|
980
|
+
const completedStatuses = new Set(['done']);
|
|
981
981
|
const completedItems = (plan.missing_features || [])
|
|
982
982
|
.filter(f => completedStatuses.has(f.status))
|
|
983
983
|
.map(f => ({ id: f.id, name: f.name, status: f.status }));
|
|
@@ -1057,7 +1057,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
1057
1057
|
const useCentral = !defaultProject;
|
|
1058
1058
|
|
|
1059
1059
|
const statusFilter = ['missing', 'planned'];
|
|
1060
|
-
// Also materialize
|
|
1060
|
+
// Also materialize done items that never got a work item (race with PR status sync)
|
|
1061
1061
|
const allExistingWiIds = new Set();
|
|
1062
1062
|
for (const p of allProjects) {
|
|
1063
1063
|
for (const w of (safeJson(projectWorkItemsPath(p)) || [])) {
|
|
@@ -1070,7 +1070,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
1070
1070
|
}
|
|
1071
1071
|
const items = plan.missing_features.filter(f =>
|
|
1072
1072
|
statusFilter.includes(f.status) ||
|
|
1073
|
-
(
|
|
1073
|
+
(f.status === 'done' && f.id && !allExistingWiIds.has(f.id))
|
|
1074
1074
|
);
|
|
1075
1075
|
|
|
1076
1076
|
// Group items by target project (per-item project field overrides plan-level project)
|
|
@@ -2203,7 +2203,7 @@ async function tickInner() {
|
|
|
2203
2203
|
try { await ghPollPrStatus(config); } catch (err) { log('warn', `GitHub PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
|
|
2204
2204
|
// Sync PR status back to PRD items (missing → done when active PR exists)
|
|
2205
2205
|
try { syncPrdFromPrs(config); } catch (err) { log('warn', `PRD sync error: ${err?.message || err}`); }
|
|
2206
|
-
// Check if any plans can be marked completed (all features done
|
|
2206
|
+
// Check if any plans can be marked completed (all features done)
|
|
2207
2207
|
try {
|
|
2208
2208
|
const prdFiles = safeReadDir(PRD_DIR).filter(f => f.endsWith('.json'));
|
|
2209
2209
|
for (const file of prdFiles) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.154",
|
|
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"
|