@yemi33/minions 0.1.328 → 0.1.330
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 -0
- package/dashboard/js/render-plans.js +5 -5
- package/dashboard/js/render-prd.js +15 -15
- package/dashboard/js/render-work-items.js +1 -1
- package/dashboard/styles.css +3 -3
- package/engine/cli.js +2 -2
- package/engine/lifecycle.js +208 -112
- package/engine/playbook.js +3 -3
- package/engine/queries.js +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -83,7 +83,7 @@ function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems) {
|
|
|
83
83
|
|
|
84
84
|
// Derive from work item progress
|
|
85
85
|
if (allDone && !hasActiveWork) return 'completed';
|
|
86
|
-
if (hasActiveWork || hasPendingPrd) return '
|
|
86
|
+
if (hasActiveWork || hasPendingPrd) return 'dispatched';
|
|
87
87
|
if (hasFailed && !hasActiveWork) return 'has-failures';
|
|
88
88
|
|
|
89
89
|
if (prdJsonStatus === 'awaiting-approval' && implementWi.length === 0) return 'awaiting-approval';
|
|
@@ -192,7 +192,7 @@ function renderPlans(plans) {
|
|
|
192
192
|
const effectiveStatus = isArchived ? 'completed' : derivePlanStatus(prdFile, p.file, prdJsonStatus, allWi);
|
|
193
193
|
|
|
194
194
|
const statusLabelsMap = {
|
|
195
|
-
'completed': 'Completed', '
|
|
195
|
+
'completed': 'Completed', 'dispatched': 'In Progress', 'paused': 'Paused',
|
|
196
196
|
'awaiting-approval': 'Awaiting Approval', 'approved': 'Approved', 'rejected': 'Rejected',
|
|
197
197
|
'revision-requested': 'Revision Requested', 'has-failures': 'Has Failures', 'active': 'Active'
|
|
198
198
|
};
|
|
@@ -225,7 +225,7 @@ function renderPlans(plans) {
|
|
|
225
225
|
|
|
226
226
|
const executeBtn = isDraft && (effectiveStatus === 'active' || effectiveStatus === 'draft') && !isArchived && !prdFile ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);font-weight:600" ' +
|
|
227
227
|
'onclick="event.stopPropagation();planExecute(\'' + escHtml(p.file) + '\',\'' + escHtml(p.project) + '\',this)">Execute</button>' : '';
|
|
228
|
-
const showPause = effectiveStatus === '
|
|
228
|
+
const showPause = effectiveStatus === 'dispatched' && prdFile && !isArchived;
|
|
229
229
|
const showResume = (effectiveStatus === 'paused' || effectiveStatus === 'awaiting-approval') && prdFile && !isArchived;
|
|
230
230
|
const verifyWi = allWi.find(w => w.itemType === 'verify' && w.sourcePlan === prdFile);
|
|
231
231
|
const hasVerifyWi = !!verifyWi;
|
|
@@ -246,8 +246,8 @@ function renderPlans(plans) {
|
|
|
246
246
|
'onclick="event.stopPropagation();planDelete(\'' + escHtml(p.file) + '\')">Delete</button>' : '';
|
|
247
247
|
|
|
248
248
|
const versionBadge = p.version ? ' <span style="font-size:9px;font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue);vertical-align:middle">v' + p.version + '</span>' : '';
|
|
249
|
-
const statusColors = { 'completed': 'var(--green)', '
|
|
250
|
-
const cardClass = effectiveStatus === '
|
|
249
|
+
const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--green)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)' };
|
|
250
|
+
const cardClass = effectiveStatus === 'dispatched' ? 'working' : effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused' ? 'awaiting' : effectiveStatus;
|
|
251
251
|
return '<div class="plan-card ' + cardClass + '" data-file="plans/' + escHtml(p.file) + '" style="cursor:pointer' + (isArchived ? ';opacity:0.7' : '') + '" onclick="planView(\'' + escHtml(p.file) + '\')">' +
|
|
252
252
|
'<div class="plan-card-header">' +
|
|
253
253
|
'<div><div class="plan-card-title">' + escHtml(p.summary || p.file) + versionBadge + '</div>' +
|
|
@@ -13,8 +13,8 @@ function renderPrd(prd, prog) {
|
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const statusColors = { 'completed': 'var(--green)', '
|
|
17
|
-
const statusLabels = { 'completed': 'Completed', '
|
|
16
|
+
const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'awaiting-approval': 'var(--yellow)', 'paused': 'var(--muted)', 'approved': 'var(--green)' };
|
|
17
|
+
const statusLabels = { 'completed': 'Completed', 'dispatched': 'In Progress', 'awaiting-approval': 'Awaiting Approval', 'paused': 'Paused', 'approved': 'Approved' };
|
|
18
18
|
|
|
19
19
|
// Show per-PRD status summary in header when multiple PRDs exist
|
|
20
20
|
const existing = prd.existing || [];
|
|
@@ -28,7 +28,7 @@ function renderPrd(prd, prog) {
|
|
|
28
28
|
const hasActive = implementItems.some(w => w.status === 'pending' || w.status === 'dispatched');
|
|
29
29
|
const prdFile = existing[0]?.file || '';
|
|
30
30
|
const prdStatus = existing[0]?.status || '';
|
|
31
|
-
const effectiveStatus = allDone && !hasActive ? 'completed' : hasActive ? '
|
|
31
|
+
const effectiveStatus = allDone && !hasActive ? 'completed' : hasActive ? 'dispatched' : prdStatus || 'active';
|
|
32
32
|
|
|
33
33
|
let actions = '';
|
|
34
34
|
if (prdFile) {
|
|
@@ -37,7 +37,7 @@ function renderPrd(prd, prog) {
|
|
|
37
37
|
} else if (effectiveStatus === 'completed') {
|
|
38
38
|
actions = ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:4px" onclick="triggerVerify(\'' + escHtml(prdFile) + '\',this)">Verify</button>' +
|
|
39
39
|
' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;margin-left:4px" onclick="planArchive(\'' + escHtml(prdFile) + '\',this)">Archive</button>';
|
|
40
|
-
} else if (effectiveStatus === '
|
|
40
|
+
} else if (effectiveStatus === 'dispatched') {
|
|
41
41
|
actions = ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="planPause(\'' + escHtml(prdFile) + '\',this)">Pause</button>';
|
|
42
42
|
} else if (effectiveStatus === 'paused') {
|
|
43
43
|
actions = ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:4px" onclick="planApprove(\'' + escHtml(prdFile) + '\',this)">Resume</button>' +
|
|
@@ -48,13 +48,13 @@ function renderPrd(prd, prog) {
|
|
|
48
48
|
' <span style="color:var(--muted);font-size:10px">' + (prd.age || '') + '</span>' + actions;
|
|
49
49
|
} else {
|
|
50
50
|
// Multiple PRDs — show count summary, per-PRD details are in renderPrdProgress groups
|
|
51
|
-
const counts = { completed: 0, '
|
|
51
|
+
const counts = { completed: 0, 'dispatched': 0, 'awaiting-approval': 0, paused: 0 };
|
|
52
52
|
for (const p of existing) {
|
|
53
53
|
const items = prdItems.filter(i => i.source === p.file);
|
|
54
54
|
const wiForPrd = allWi.filter(w => items.some(pi => pi.id === w.id));
|
|
55
55
|
const allDone = wiForPrd.length > 0 && wiForPrd.every(w => w.status === 'done');
|
|
56
56
|
const hasActive = wiForPrd.some(w => w.status === 'pending' || w.status === 'dispatched');
|
|
57
|
-
const s = allDone && !hasActive ? 'completed' : hasActive ? '
|
|
57
|
+
const s = allDone && !hasActive ? 'completed' : hasActive ? 'dispatched' : p.status || 'active';
|
|
58
58
|
counts[s] = (counts[s] || 0) + 1;
|
|
59
59
|
}
|
|
60
60
|
const parts = Object.entries(counts).filter(([, n]) => n > 0).map(([s, n]) =>
|
|
@@ -83,7 +83,7 @@ function renderPrdProgress(prog) {
|
|
|
83
83
|
const total = items.length;
|
|
84
84
|
if (total === 0) return '';
|
|
85
85
|
const done = items.filter(i => i.status === 'done').length;
|
|
86
|
-
const inProgress = items.filter(i => i.status === '
|
|
86
|
+
const inProgress = items.filter(i => i.status === 'dispatched').length;
|
|
87
87
|
const failed = items.filter(i => i.status === 'failed').length;
|
|
88
88
|
const paused = items.filter(i => i.status === 'paused').length;
|
|
89
89
|
const missing = items.filter(i => i.status === 'missing' || !i.status).length;
|
|
@@ -100,7 +100,7 @@ function renderPrdProgress(prog) {
|
|
|
100
100
|
|
|
101
101
|
const bar = '<div class="prd-progress-bar">' +
|
|
102
102
|
'<div class="seg complete" style="width:' + pct(done) + '%"></div>' +
|
|
103
|
-
'<div class="seg
|
|
103
|
+
'<div class="seg dispatched" style="width:' + pct(inProgress) + '%"></div>' +
|
|
104
104
|
'<div class="seg paused" style="width:' + pct(paused) + '%"></div>' +
|
|
105
105
|
'<div class="seg missing" style="width:' + pct(missing) + '%"></div>' +
|
|
106
106
|
'</div>';
|
|
@@ -108,15 +108,15 @@ function renderPrdProgress(prog) {
|
|
|
108
108
|
return '<div style="margin:6px 0 8px 0;padding:0 8px">' + stats + '<div style="margin-top:8px">' + bar + '</div></div>';
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
// PRD item statuses: missing →
|
|
111
|
+
// PRD item statuses: missing → dispatched → done
|
|
112
112
|
const statusBadge = (s) => {
|
|
113
113
|
const styles = {
|
|
114
114
|
'done': 'background:rgba(63,185,80,0.15);color:var(--green)',
|
|
115
|
-
'
|
|
115
|
+
'dispatched': 'background:rgba(210,153,34,0.15);color:var(--yellow);animation:wipPulse 1.5s infinite',
|
|
116
116
|
'failed': 'background:rgba(248,81,73,0.15);color:var(--red)',
|
|
117
117
|
'paused': 'background:rgba(139,148,158,0.15);color:var(--muted)',
|
|
118
118
|
};
|
|
119
|
-
const labels = { 'done': 'DONE', '
|
|
119
|
+
const labels = { 'done': 'DONE', 'dispatched': 'WIP', 'failed': 'FAIL', 'paused': 'PAUSED', 'missing': '\u2014' };
|
|
120
120
|
const style = styles[s] || 'background:var(--surface);color:var(--muted)';
|
|
121
121
|
const label = labels[s] || '—';
|
|
122
122
|
return '<span style="font-size:9px;font-weight:700;padding:2px 6px;border-radius:3px;letter-spacing:0.5px;white-space:nowrap;' + style + '">' + label + '</span>';
|
|
@@ -196,7 +196,7 @@ function renderPrdProgress(prog) {
|
|
|
196
196
|
|
|
197
197
|
const renderGroupHeader = (g) => {
|
|
198
198
|
const done = g.items.filter(i => i.status === 'done').length;
|
|
199
|
-
const wip = g.items.filter(i => i.status === '
|
|
199
|
+
const wip = g.items.filter(i => i.status === 'dispatched' || i.status === 'dispatched').length;
|
|
200
200
|
const summary = (g.summary || '').replace(/^Convert plan to PRD:\s*/i, '').slice(0, 80);
|
|
201
201
|
const isAwaitingApproval = g.planStatus === 'awaiting-approval';
|
|
202
202
|
const isPaused = g.planStatus === 'paused';
|
|
@@ -303,7 +303,7 @@ function renderPrdProgress(prog) {
|
|
|
303
303
|
|
|
304
304
|
const statusColor = (s) => {
|
|
305
305
|
if (s === 'done') return 'var(--green)';
|
|
306
|
-
if (s === '
|
|
306
|
+
if (s === 'dispatched') return 'var(--yellow)';
|
|
307
307
|
if (s === 'failed') return 'var(--red)';
|
|
308
308
|
if (s === 'paused') return 'var(--muted)';
|
|
309
309
|
return 'var(--border)';
|
|
@@ -322,7 +322,7 @@ function renderPrdProgress(prog) {
|
|
|
322
322
|
const iid = escHtml(i.id || '');
|
|
323
323
|
const agent = wi[i.id]?.dispatched_to || '';
|
|
324
324
|
const deps = (i.depends_on || []).join(', ');
|
|
325
|
-
const wipAnim = i.status === '
|
|
325
|
+
const wipAnim = i.status === 'dispatched' ? 'animation:prdWipPulse 2s infinite;' : '';
|
|
326
326
|
html += '<div onclick="prdItemEdit(\'' + src + '\',\'' + iid + '\')" ' +
|
|
327
327
|
'style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid ' + borderColor + ';' + wipAnim +
|
|
328
328
|
'border-radius:4px;padding:6px 8px;margin-bottom:6px;cursor:pointer;font-size:11px">' +
|
|
@@ -557,7 +557,7 @@ async function prdItemEdit(source, itemId) {
|
|
|
557
557
|
let completionHtml = '';
|
|
558
558
|
const isDone = item.status === 'done';
|
|
559
559
|
const isFailed = item.status === 'failed';
|
|
560
|
-
const isActive = item.status === '
|
|
560
|
+
const isActive = item.status === 'dispatched' || item.status === 'dispatched';
|
|
561
561
|
|
|
562
562
|
if (isDone || isFailed || isActive) {
|
|
563
563
|
const agent = wi?.dispatched_to || completedEntry?.agent || '';
|
|
@@ -30,7 +30,7 @@ function wiRetryBtn(item) {
|
|
|
30
30
|
|
|
31
31
|
function wiRow(item) {
|
|
32
32
|
const statusBadge = (s) => {
|
|
33
|
-
const cls = s === 'failed' ? 'rejected' : s === 'needs-human-review' ? 'needs-review' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : 'draft';
|
|
33
|
+
const cls = s === 'failed' ? 'rejected' : s === 'needs-human-review' ? 'needs-review' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : s === 'decomposed' ? 'building' : 'draft';
|
|
34
34
|
return '<span class="pr-badge ' + cls + '">' + escHtml(s) + '</span>';
|
|
35
35
|
};
|
|
36
36
|
const typeBadge = (t) => '<span class="dispatch-type ' + (t || 'implement') + '">' + escHtml(t || 'implement') + '</span>';
|
package/dashboard/styles.css
CHANGED
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
.prd-progress-bar { width: 100%; height: 20px; background: var(--bg); border-radius: var(--radius-xl); overflow: hidden; display: flex; margin-bottom: var(--space-6); border: 1px solid var(--border); }
|
|
169
169
|
.prd-progress-bar .seg { height: 100%; transition: width 0.5s ease; }
|
|
170
170
|
.prd-progress-bar .seg.complete { background: var(--green); }
|
|
171
|
-
.prd-progress-bar .seg.
|
|
171
|
+
.prd-progress-bar .seg.dispatched { background: var(--yellow); }
|
|
172
172
|
.prd-progress-bar .seg.paused { background: var(--muted); opacity: 0.5; }
|
|
173
173
|
.prd-progress-bar .seg.missing { background: var(--border); }
|
|
174
174
|
.prd-progress-pct { font-size: 22px; font-weight: 700; color: var(--green); margin-bottom: var(--space-4); }
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
.prd-legend-item { display: flex; align-items: center; gap: 5px; font-size: var(--text-base); color: var(--muted); }
|
|
177
177
|
.prd-legend-dot { width: 10px; height: 10px; border-radius: 2px; }
|
|
178
178
|
.prd-legend-dot.complete { background: var(--green); }
|
|
179
|
-
.prd-legend-dot.
|
|
179
|
+
.prd-legend-dot.dispatched { background: var(--yellow); }
|
|
180
180
|
.prd-legend-dot.missing { background: var(--border); }
|
|
181
181
|
|
|
182
182
|
/* Pipeline Step-Progress */
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
.prd-items-list { display: flex; flex-direction: column; gap: 3px; max-height: 400px; overflow-y: auto; padding: 0 8px; }
|
|
194
194
|
.prd-item-row { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: var(--radius-sm); font-size: var(--text-base); background: var(--surface2); border: 1px solid var(--border); border-left: 3px solid var(--border); }
|
|
195
195
|
.prd-item-row.st-done { border-left-color: var(--green); }
|
|
196
|
-
.prd-item-row.st-
|
|
196
|
+
.prd-item-row.st-dispatched { border-left-color: var(--yellow); animation: prdWipPulse 2s infinite; }
|
|
197
197
|
@keyframes prdWipPulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(210,153,34,0); } 50% { box-shadow: 0 0 0 4px rgba(210,153,34,0.2); } }
|
|
198
198
|
.prd-item-row.st-failed { border-left-color: var(--red); }
|
|
199
199
|
.prd-item-row.st-needs-human-review { border-left-color: var(--orange); }
|
package/engine/cli.js
CHANGED
|
@@ -809,8 +809,8 @@ const commands = {
|
|
|
809
809
|
if (exists && name === 'prd') {
|
|
810
810
|
const prd = safeJson(filePath);
|
|
811
811
|
if (prd) {
|
|
812
|
-
const missing = (prd.missing_features || []).filter(f =>
|
|
813
|
-
console.log(` Items: ${missing.length} missing
|
|
812
|
+
const missing = (prd.missing_features || []).filter(f => f.status === 'missing' || !f.status);
|
|
813
|
+
console.log(` Items: ${missing.length} missing features`);
|
|
814
814
|
}
|
|
815
815
|
}
|
|
816
816
|
if (exists && name === 'pullRequests') {
|
package/engine/lifecycle.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
8
9
|
const shared = require('./shared');
|
|
9
|
-
const { safeRead, safeJson, safeWrite, execSilent, projectPrPath, getPrLinks, addPrLink,
|
|
10
|
-
log, ts, dateStamp
|
|
10
|
+
const { safeRead, safeJson, safeWrite, mutateJsonFileLocked, execSilent, projectPrPath, getPrLinks, addPrLink,
|
|
11
|
+
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
12
|
+
ENGINE_DEFAULTS } = shared;
|
|
11
13
|
const { trackEngineUsage } = require('./llm');
|
|
12
14
|
const queries = require('./queries');
|
|
13
15
|
const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
|
|
@@ -21,7 +23,9 @@ function checkPlanCompletion(meta, config) {
|
|
|
21
23
|
const planPath = path.join(PRD_DIR, planFile);
|
|
22
24
|
const plan = safeJson(planPath);
|
|
23
25
|
if (!plan?.missing_features) return;
|
|
24
|
-
if (plan.status ===
|
|
26
|
+
if (plan.status === PLAN_STATUS.COMPLETED) {
|
|
27
|
+
if (plan._completionNotified) return;
|
|
28
|
+
}
|
|
25
29
|
|
|
26
30
|
const projects = shared.getProjects(config);
|
|
27
31
|
|
|
@@ -53,7 +57,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
53
57
|
const unmaterialized = [...planFeatureIds].filter(id => {
|
|
54
58
|
if (workItemById[id]) return false;
|
|
55
59
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
56
|
-
return !(prdItem && (prdItem.status
|
|
60
|
+
return !(prdItem && DONE_STATUSES.has(prdItem.status));
|
|
57
61
|
});
|
|
58
62
|
if (unmaterialized.length > 0) {
|
|
59
63
|
log('info', `Plan ${planFile}: ${unmaterialized.length}/${planFeatureIds.size} feature(s) not yet materialized as work items: ${unmaterialized.join(', ')}`);
|
|
@@ -63,20 +67,20 @@ function checkPlanCompletion(meta, config) {
|
|
|
63
67
|
// Check 2: every feature's work item must be done (or PRD item marked done externally)
|
|
64
68
|
const notDone = [...planFeatureIds].filter(id => {
|
|
65
69
|
const w = workItemById[id];
|
|
66
|
-
if (w && (w.status
|
|
70
|
+
if (w && DONE_STATUSES.has(w.status)) return false;
|
|
67
71
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
68
|
-
return !(prdItem && (prdItem.status
|
|
72
|
+
return !(prdItem && DONE_STATUSES.has(prdItem.status));
|
|
69
73
|
});
|
|
70
74
|
if (notDone.length > 0) {
|
|
71
75
|
log('info', `Plan ${planFile}: waiting for done on ${notDone.length}/${planFeatureIds.size} item(s): ${notDone.join(', ')}`);
|
|
72
76
|
return;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
const doneItems = planItems.filter(w =>
|
|
76
|
-
const failedItems = planItems.filter(w => w.status ===
|
|
79
|
+
const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
|
|
80
|
+
const failedItems = planItems.filter(w => w.status === WI_STATUS.FAILED);
|
|
77
81
|
|
|
78
82
|
// 1. Mark plan as completed
|
|
79
|
-
plan.status =
|
|
83
|
+
plan.status = PLAN_STATUS.COMPLETED;
|
|
80
84
|
plan.completedAt = ts();
|
|
81
85
|
|
|
82
86
|
// Compute timing
|
|
@@ -133,16 +137,28 @@ function checkPlanCompletion(meta, config) {
|
|
|
133
137
|
].filter(Boolean).join('\n');
|
|
134
138
|
|
|
135
139
|
// Write summary to notes/inbox
|
|
136
|
-
const
|
|
137
|
-
shared.
|
|
138
|
-
log('info', `PRD completion summary written to notes/inbox/${
|
|
140
|
+
const summarySlug = `prd-completion-${planFile.replace('.json', '')}`;
|
|
141
|
+
shared.writeToInbox('engine', summarySlug, summary);
|
|
142
|
+
log('info', `PRD completion summary written to notes/inbox/${summarySlug}`);
|
|
143
|
+
|
|
144
|
+
// Persist completed status + _completionNotified via file lock
|
|
145
|
+
mutateJsonFileLocked(planPath, (data) => {
|
|
146
|
+
data.status = PLAN_STATUS.COMPLETED;
|
|
147
|
+
data.completedAt = plan.completedAt;
|
|
148
|
+
data._completionNotified = true;
|
|
149
|
+
return data;
|
|
150
|
+
});
|
|
139
151
|
|
|
140
152
|
// Resolve the primary project for writing new work items (PR, verify)
|
|
141
153
|
const projectName = plan.project;
|
|
142
154
|
const primaryProject = projectName
|
|
143
155
|
? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) : projects[0];
|
|
144
|
-
|
|
145
|
-
|
|
156
|
+
if (!primaryProject) {
|
|
157
|
+
log('warn', `Plan ${planFile}: no primary project found — skipping PR/verify creation`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const wiPath = shared.projectWorkItemsPath(primaryProject);
|
|
161
|
+
const workItems = safeJson(wiPath) || [];
|
|
146
162
|
|
|
147
163
|
// 3. For shared-branch plans, create PR work item
|
|
148
164
|
if (plan.branch_strategy === 'shared-branch' && plan.feature_branch && wiPath) {
|
|
@@ -156,7 +172,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
156
172
|
id, title: `Create PR for plan: ${plan.plan_summary || planFile}`,
|
|
157
173
|
type: 'implement', priority: 'high',
|
|
158
174
|
description: `All plan items from \`${planFile}\` are complete on branch \`${featureBranch}\`.\n\n**Branch:** \`${featureBranch}\`\n**Target:** \`${mainBranch}\`\n\n## Completed Items\n${itemSummary}`,
|
|
159
|
-
status:
|
|
175
|
+
status: WI_STATUS.PENDING, created: ts(), createdBy: 'engine:plan-completion',
|
|
160
176
|
sourcePlan: planFile, itemType: 'pr',
|
|
161
177
|
branch: featureBranch, branchStrategy: 'shared-branch', project: projectName,
|
|
162
178
|
});
|
|
@@ -177,7 +193,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
177
193
|
const prs = (safeJson(shared.projectPrPath(p)) || [])
|
|
178
194
|
.filter(pr => {
|
|
179
195
|
const linkedId = prLinks[pr.id];
|
|
180
|
-
return pr.status ===
|
|
196
|
+
return pr.status === PR_STATUS.ACTIVE && linkedId && doneItems.find(w => w.id === linkedId);
|
|
181
197
|
});
|
|
182
198
|
if (prs.length > 0) {
|
|
183
199
|
projectPrs[p.name] = { project: p, prs, mainBranch: p.mainBranch || 'main' };
|
|
@@ -190,7 +206,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
190
206
|
const branches = prs.map(pr => pr.branch).filter(Boolean);
|
|
191
207
|
const lines = [
|
|
192
208
|
`# ${name} — merge ${branches.length} PR branch(es) into one worktree`,
|
|
193
|
-
`cd "${p.localPath}"`,
|
|
209
|
+
`cd "${p.localPath.replace(/\\/g, '/')}"`,
|
|
194
210
|
`git fetch origin ${branches.map(b => `"${b}"`).join(' ')} "${mainBranch}"`,
|
|
195
211
|
`git worktree add "${wtPath}" "origin/${mainBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${mainBranch}" && git pull origin "${mainBranch}")`,
|
|
196
212
|
`cd "${wtPath}"`,
|
|
@@ -247,7 +263,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
247
263
|
type: 'verify',
|
|
248
264
|
priority: 'high',
|
|
249
265
|
description,
|
|
250
|
-
status:
|
|
266
|
+
status: WI_STATUS.PENDING,
|
|
251
267
|
created: ts(),
|
|
252
268
|
createdBy: 'engine:plan-verification',
|
|
253
269
|
sourcePlan: planFile,
|
|
@@ -258,47 +274,94 @@ function checkPlanCompletion(meta, config) {
|
|
|
258
274
|
log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
|
|
259
275
|
}
|
|
260
276
|
|
|
261
|
-
//
|
|
277
|
+
// Archive deferred until verify completes
|
|
278
|
+
|
|
279
|
+
log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─── Archive Plan ───────────────────────────────────────────────────────────
|
|
283
|
+
function archivePlan(planFile, plan, projects, config) {
|
|
284
|
+
const planPath = path.join(PRD_DIR, planFile);
|
|
285
|
+
|
|
286
|
+
// Archive PRD .json to prd/archive/
|
|
262
287
|
const prdArchiveDir = path.join(PRD_DIR, 'archive');
|
|
263
288
|
if (!fs.existsSync(prdArchiveDir)) fs.mkdirSync(prdArchiveDir, { recursive: true });
|
|
264
|
-
shared.safeWrite(planPath, plan); // save completed status first
|
|
265
289
|
try {
|
|
266
|
-
fs.
|
|
267
|
-
|
|
290
|
+
if (fs.existsSync(planPath)) {
|
|
291
|
+
fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
|
|
292
|
+
log('info', `Archived completed PRD: prd/archive/${planFile}`);
|
|
293
|
+
}
|
|
268
294
|
} catch (err) {
|
|
269
295
|
log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
|
|
270
|
-
shared.safeWrite(planPath, plan);
|
|
271
296
|
}
|
|
272
297
|
|
|
273
|
-
//
|
|
298
|
+
// Archive the source .md plan if it exists
|
|
299
|
+
const projectName = plan.project;
|
|
274
300
|
const planArchiveDir = path.join(PLANS_DIR, 'archive');
|
|
275
301
|
if (!fs.existsSync(planArchiveDir)) fs.mkdirSync(planArchiveDir, { recursive: true });
|
|
276
302
|
try {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
303
|
+
// Direct match by source_plan field or planFile-derived name
|
|
304
|
+
const sourcePlanName = plan.source_plan || planFile.replace(/\.json$/, '.md');
|
|
305
|
+
if (sourcePlanName && fs.existsSync(path.join(PLANS_DIR, sourcePlanName))) {
|
|
306
|
+
try {
|
|
307
|
+
fs.renameSync(path.join(PLANS_DIR, sourcePlanName), path.join(planArchiveDir, sourcePlanName));
|
|
308
|
+
log('info', `Archived source plan: plans/archive/${sourcePlanName}`);
|
|
309
|
+
} catch (err) { log('warn', `Failed to archive plan ${sourcePlanName}: ${err.message}`); }
|
|
310
|
+
} else {
|
|
311
|
+
// Fallback: match by content
|
|
312
|
+
const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
|
|
313
|
+
for (const md of mdFiles) {
|
|
314
|
+
const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
|
|
315
|
+
if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
|
|
316
|
+
try {
|
|
317
|
+
fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
|
|
318
|
+
log('info', `Archived source plan: plans/archive/${md}`);
|
|
319
|
+
} catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
287
322
|
}
|
|
288
323
|
}
|
|
289
324
|
} catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
|
|
290
325
|
|
|
291
|
-
//
|
|
326
|
+
// Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
|
|
292
327
|
try {
|
|
293
|
-
// Collect all branch slugs: shared-branch + per-item branches + item IDs
|
|
294
328
|
const branchSlugs = new Set();
|
|
295
329
|
if (plan.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
|
|
330
|
+
|
|
331
|
+
// Collect work items for this plan
|
|
332
|
+
let allWorkItems = [];
|
|
333
|
+
for (const p of projects) {
|
|
334
|
+
try {
|
|
335
|
+
const wi = safeJson(shared.projectWorkItemsPath(p)) || [];
|
|
336
|
+
allWorkItems = allWorkItems.concat(wi);
|
|
337
|
+
} catch { /* optional */ }
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
341
|
+
for (const w of central) {
|
|
342
|
+
if (!allWorkItems.some(existing => existing.id === w.id)) allWorkItems.push(w);
|
|
343
|
+
}
|
|
344
|
+
} catch { /* optional */ }
|
|
345
|
+
const planItems = allWorkItems.filter(w => w.sourcePlan === planFile);
|
|
346
|
+
const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
|
|
347
|
+
|
|
296
348
|
for (const w of doneItems) {
|
|
297
349
|
if (w.branch) branchSlugs.add(shared.sanitizeBranch(w.branch).toLowerCase());
|
|
298
350
|
if (w.id) branchSlugs.add(w.id.toLowerCase());
|
|
299
351
|
}
|
|
300
|
-
|
|
301
|
-
|
|
352
|
+
|
|
353
|
+
// Collect PR branches
|
|
354
|
+
for (const p of projects) {
|
|
355
|
+
try {
|
|
356
|
+
const prs = safeJson(shared.projectPrPath(p)) || [];
|
|
357
|
+
const prLinks = getPrLinks();
|
|
358
|
+
for (const pr of prs) {
|
|
359
|
+
const linkedId = prLinks[pr.id];
|
|
360
|
+
if (linkedId && doneItems.find(w => w.id === linkedId) && pr.branch) {
|
|
361
|
+
branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} catch { /* optional */ }
|
|
302
365
|
}
|
|
303
366
|
|
|
304
367
|
let cleanedWt = 0;
|
|
@@ -319,10 +382,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
319
382
|
}
|
|
320
383
|
}
|
|
321
384
|
}
|
|
322
|
-
if (cleanedWt > 0) log('info', `
|
|
385
|
+
if (cleanedWt > 0) log('info', `Archive: cleaned ${cleanedWt} worktree(s)`);
|
|
323
386
|
} catch (err) { log('warn', `Worktree cleanup: ${err.message}`); }
|
|
324
|
-
|
|
325
|
-
log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
|
|
326
387
|
}
|
|
327
388
|
|
|
328
389
|
// ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
|
|
@@ -381,6 +442,10 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
381
442
|
|
|
382
443
|
const projectName = meta?.item?.project || meta?.project?.name;
|
|
383
444
|
const projects = shared.getProjects(config);
|
|
445
|
+
if (projects.length === 0) {
|
|
446
|
+
log('error', 'Plan chaining: no projects configured');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
384
449
|
const targetProject = projectName
|
|
385
450
|
? projects.find(p => p.name === projectName) || projects[0]
|
|
386
451
|
: projects[0];
|
|
@@ -393,14 +458,17 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
393
458
|
log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
|
|
394
459
|
const wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
395
460
|
let items = [];
|
|
396
|
-
try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch {
|
|
461
|
+
try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch (err) {
|
|
462
|
+
log('warn', `Failed to parse ${wiPath}: ${err.message}`);
|
|
463
|
+
try { fs.copyFileSync(wiPath, wiPath + '.bak'); } catch {}
|
|
464
|
+
}
|
|
397
465
|
items.push({
|
|
398
466
|
id: 'W-' + shared.uid(),
|
|
399
467
|
title: `Convert plan to PRD: ${meta?.item?.title || planFile.name}`,
|
|
400
468
|
type: 'plan-to-prd',
|
|
401
469
|
priority: meta?.item?.priority || 'high',
|
|
402
470
|
description: `Plan file: plans/${planFile.name}\nChained from plan task ${dispatchItem.id}`,
|
|
403
|
-
status:
|
|
471
|
+
status: WI_STATUS.PENDING,
|
|
404
472
|
created: ts(),
|
|
405
473
|
createdBy: 'engine:chain',
|
|
406
474
|
project: targetProject.name,
|
|
@@ -410,10 +478,15 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
410
478
|
}
|
|
411
479
|
|
|
412
480
|
// ─── Work Item Status ────────────────────────────────────────────────────────
|
|
481
|
+
const _VALID_WI_STATUSES = new Set(Object.values(WI_STATUS));
|
|
413
482
|
function updateWorkItemStatus(meta, status, reason) {
|
|
414
483
|
|
|
415
484
|
const itemId = meta.item?.id;
|
|
416
485
|
if (!itemId) return;
|
|
486
|
+
if (!_VALID_WI_STATUSES.has(status)) {
|
|
487
|
+
log('warn', `Invalid work item status '${status}' for ${itemId} — ignoring`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
417
490
|
|
|
418
491
|
let wiPath;
|
|
419
492
|
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
@@ -435,20 +508,20 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
435
508
|
target.agentResults[agent] = { status, completedAt: ts(), reason: reason || undefined };
|
|
436
509
|
|
|
437
510
|
const results = Object.values(target.agentResults);
|
|
438
|
-
const anySuccess = results.some(r => r.status ===
|
|
511
|
+
const anySuccess = results.some(r => r.status === WI_STATUS.DONE);
|
|
439
512
|
const allDone = Array.isArray(target.fanOutAgents) && target.fanOutAgents.length > 0 ? results.length >= target.fanOutAgents.length : false;
|
|
440
513
|
const dispatchAge = target.dispatched_at ? Date.now() - new Date(target.dispatched_at).getTime() : 0;
|
|
441
514
|
const timedOut = !allDone && dispatchAge > 6 * 60 * 60 * 1000 && results.length > 0;
|
|
442
515
|
|
|
443
516
|
if (anySuccess) {
|
|
444
|
-
target.status =
|
|
517
|
+
target.status = WI_STATUS.DONE;
|
|
445
518
|
delete target.failReason;
|
|
446
519
|
delete target.failedAt;
|
|
447
520
|
target.completedAgents = Object.entries(target.agentResults)
|
|
448
|
-
.filter(([, r]) => r.status ===
|
|
521
|
+
.filter(([, r]) => r.status === WI_STATUS.DONE)
|
|
449
522
|
.map(([a]) => a);
|
|
450
523
|
} else if (allDone || timedOut) {
|
|
451
|
-
target.status =
|
|
524
|
+
target.status = WI_STATUS.FAILED;
|
|
452
525
|
target.failReason = timedOut
|
|
453
526
|
? `Fan-out timed out: ${results.length}/${(target.fanOutAgents || []).length} agents reported (all failed)`
|
|
454
527
|
: 'All fan-out agents failed';
|
|
@@ -456,11 +529,11 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
456
529
|
}
|
|
457
530
|
} else {
|
|
458
531
|
target.status = status;
|
|
459
|
-
if (status ===
|
|
532
|
+
if (status === WI_STATUS.DONE) {
|
|
460
533
|
delete target.failReason;
|
|
461
534
|
delete target.failedAt;
|
|
462
535
|
target.completedAt = ts();
|
|
463
|
-
} else if (status ===
|
|
536
|
+
} else if (status === WI_STATUS.FAILED) {
|
|
464
537
|
if (reason) target.failReason = reason;
|
|
465
538
|
target.failedAt = ts();
|
|
466
539
|
}
|
|
@@ -474,8 +547,10 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
474
547
|
}
|
|
475
548
|
}
|
|
476
549
|
|
|
550
|
+
const _VALID_PRD_STATUSES = new Set([...Object.values(WI_STATUS), 'missing']);
|
|
477
551
|
function syncPrdItemStatus(itemId, status, sourcePlan) {
|
|
478
552
|
if (!itemId) return;
|
|
553
|
+
if (!_VALID_PRD_STATUSES.has(status)) return;
|
|
479
554
|
try {
|
|
480
555
|
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
481
556
|
const files = sourcePlan ? [sourcePlan] : require('fs').readdirSync(prdDir).filter(f => f.endsWith('.json'));
|
|
@@ -538,6 +613,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
538
613
|
if (prMatches.size === 0) return 0;
|
|
539
614
|
|
|
540
615
|
const projects = shared.getProjects(config);
|
|
616
|
+
if (projects.length === 0 && !meta?.project?.name) return 0;
|
|
541
617
|
const defaultProject = (meta?.project?.name && projects.find(p => p.name === meta.project.name)) || projects[0];
|
|
542
618
|
const useCentral = !defaultProject;
|
|
543
619
|
|
|
@@ -566,8 +642,9 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
566
642
|
const agentName = config.agents?.[agentId]?.name || agentId;
|
|
567
643
|
let added = 0;
|
|
568
644
|
const centralPrPath = path.join(MINIONS_DIR, 'pull-requests.json');
|
|
569
|
-
|
|
570
|
-
|
|
645
|
+
|
|
646
|
+
// Group new PRs by target file path
|
|
647
|
+
const newPrsByPath = new Map(); // prPath -> [{ prId, newEntry }]
|
|
571
648
|
|
|
572
649
|
for (const prId of prMatches) {
|
|
573
650
|
const fullId = `PR-${prId}`;
|
|
@@ -575,38 +652,43 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
575
652
|
const targetName = targetProject ? targetProject.name : '_central';
|
|
576
653
|
const prPath = targetProject ? shared.projectPrPath(targetProject) : centralPrPath;
|
|
577
654
|
|
|
578
|
-
// Load PRs for this target (cache per target)
|
|
579
|
-
if (!dirtyTargets.has(targetName)) {
|
|
580
|
-
dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
|
|
581
|
-
}
|
|
582
|
-
const entry = dirtyTargets.get(targetName);
|
|
583
|
-
if (entry.prs.some(p => p.id === fullId || String(p.id).includes(prId))) continue;
|
|
584
|
-
|
|
585
655
|
let title = meta?.item?.title || '';
|
|
586
656
|
const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
|
|
587
657
|
if (titleMatch) title = titleMatch[1].trim();
|
|
588
658
|
if (title.includes('session_id') || title.includes('is_error') || title.includes('uuid') || title.length > 120) {
|
|
589
659
|
title = meta?.item?.title || '';
|
|
590
660
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
661
|
+
|
|
662
|
+
if (!newPrsByPath.has(prPath)) newPrsByPath.set(prPath, { name: targetName, entries: [] });
|
|
663
|
+
newPrsByPath.get(prPath).entries.push({
|
|
664
|
+
prId, fullId,
|
|
665
|
+
entry: {
|
|
666
|
+
id: fullId,
|
|
667
|
+
title: (title || `PR created by ${agentName}`).slice(0, 120),
|
|
668
|
+
agent: agentName,
|
|
669
|
+
branch: meta?.branch || '',
|
|
670
|
+
reviewStatus: 'pending',
|
|
671
|
+
status: PR_STATUS.ACTIVE,
|
|
672
|
+
created: dateStamp(),
|
|
673
|
+
url: extractPrUrl(prId),
|
|
674
|
+
prdItems: meta?.item?.id ? [meta.item.id] : [],
|
|
675
|
+
sourcePlan: meta?.item?.sourcePlan || '',
|
|
676
|
+
itemType: meta?.item?.itemType || ''
|
|
677
|
+
}
|
|
603
678
|
});
|
|
604
|
-
if (meta?.item?.id) addPrLink(fullId, meta.item.id);
|
|
605
|
-
added++;
|
|
606
679
|
}
|
|
607
680
|
|
|
608
|
-
for (const [name,
|
|
609
|
-
|
|
681
|
+
for (const [prPath, { name, entries }] of newPrsByPath) {
|
|
682
|
+
mutateJsonFileLocked(prPath, (data) => {
|
|
683
|
+
const prs = Array.isArray(data) ? data : [];
|
|
684
|
+
for (const { prId, fullId, entry } of entries) {
|
|
685
|
+
if (prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
|
|
686
|
+
prs.push(entry);
|
|
687
|
+
if (meta?.item?.id) addPrLink(fullId, meta.item.id);
|
|
688
|
+
added++;
|
|
689
|
+
}
|
|
690
|
+
return prs;
|
|
691
|
+
});
|
|
610
692
|
log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
|
|
611
693
|
}
|
|
612
694
|
return added;
|
|
@@ -651,7 +733,7 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
651
733
|
}
|
|
652
734
|
|
|
653
735
|
shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
|
|
654
|
-
log('info', `Updated ${pr.id} → minions review: ${
|
|
736
|
+
log('info', `Updated ${pr.id} → minions review: ${target.reviewStatus} by ${reviewerName}`);
|
|
655
737
|
createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
|
|
656
738
|
}
|
|
657
739
|
|
|
@@ -703,7 +785,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
703
785
|
} catch (err) { log('warn', `Post-merge worktree cleanup: ${err.message}`); }
|
|
704
786
|
}
|
|
705
787
|
|
|
706
|
-
if (newStatus !==
|
|
788
|
+
if (newStatus !== PR_STATUS.MERGED) return;
|
|
707
789
|
|
|
708
790
|
// Resolve linked work item from pr-links or PR branch name
|
|
709
791
|
let mergedItemId = getPrLinks()[pr.id];
|
|
@@ -722,13 +804,13 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
722
804
|
const plan = safeJson(path.join(prdDir, pf));
|
|
723
805
|
if (!plan?.missing_features) continue;
|
|
724
806
|
const feature = plan.missing_features.find(f => f.id === mergedItemId);
|
|
725
|
-
if (feature && feature.status !==
|
|
726
|
-
feature.status =
|
|
807
|
+
if (feature && feature.status !== WI_STATUS.DONE) {
|
|
808
|
+
feature.status = WI_STATUS.DONE;
|
|
727
809
|
shared.safeWrite(path.join(prdDir, pf), plan);
|
|
728
810
|
updated++;
|
|
729
811
|
}
|
|
730
812
|
}
|
|
731
|
-
if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as
|
|
813
|
+
if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as done for ${pr.id}`);
|
|
732
814
|
} catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
|
|
733
815
|
|
|
734
816
|
// Mark work item as done
|
|
@@ -739,10 +821,10 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
739
821
|
const items = safeJson(wiPath);
|
|
740
822
|
if (!items) continue;
|
|
741
823
|
const item = items.find(i => i.id === mergedItemId);
|
|
742
|
-
if (item && item.status !==
|
|
824
|
+
if (item && item.status !== WI_STATUS.DONE) {
|
|
743
825
|
log('info', `Post-merge: marking work item ${mergedItemId} as done (was ${item.status}) for ${pr.id}`);
|
|
744
|
-
item.status =
|
|
745
|
-
item.completedAt =
|
|
826
|
+
item.status = WI_STATUS.DONE;
|
|
827
|
+
item.completedAt = ts();
|
|
746
828
|
item._mergedVia = pr.id;
|
|
747
829
|
shared.safeWrite(wiPath, items);
|
|
748
830
|
break;
|
|
@@ -827,19 +909,19 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
827
909
|
if (proj) {
|
|
828
910
|
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
829
911
|
const items = safeJson(centralPath) || [];
|
|
830
|
-
const alreadyExists = items.some(i => i.title === `Add skill: ${name}` && i.status !==
|
|
912
|
+
const alreadyExists = items.some(i => i.title === `Add skill: ${name}` && i.status !== WI_STATUS.FAILED);
|
|
831
913
|
if (!alreadyExists) {
|
|
832
914
|
const skillId = `SK${String(items.filter(i => i.id?.startsWith('SK')).length + 1).padStart(3, '0')}`;
|
|
833
915
|
items.push({ id: skillId, type: 'implement', title: `Add skill: ${name}`,
|
|
834
916
|
description: `Create project-level skill \`${filename}\` in ${project}.\n\nWrite this file to \`${proj.localPath}/.claude/skills/${filename}\` via a PR.\n\n## Skill Content\n\n\`\`\`\n${enrichedBlock}\n\`\`\``,
|
|
835
|
-
priority: 'low', status:
|
|
917
|
+
priority: 'low', status: WI_STATUS.QUEUED, created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
|
|
836
918
|
shared.safeWrite(centralPath, items);
|
|
837
919
|
log('info', `Queued work item ${skillId} to PR project skill "${name}" into ${project}`);
|
|
838
920
|
}
|
|
839
921
|
}
|
|
840
922
|
} else {
|
|
841
923
|
// Write in Claude Code native format: ~/.claude/skills/<name>/SKILL.md
|
|
842
|
-
const claudeSkillsDir = path.join(
|
|
924
|
+
const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
843
925
|
const skillDir = path.join(claudeSkillsDir, name.replace(/[^a-z0-9-]/g, '-'));
|
|
844
926
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
845
927
|
if (!fs.existsSync(skillPath)) {
|
|
@@ -918,10 +1000,10 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
918
1000
|
m.lastTask = dispatchItem.task;
|
|
919
1001
|
m.lastCompleted = ts();
|
|
920
1002
|
if (model) m.model = model;
|
|
921
|
-
if (result ===
|
|
1003
|
+
if (result === DISPATCH_RESULT.SUCCESS) {
|
|
922
1004
|
m.tasksCompleted++;
|
|
923
1005
|
if (prsCreatedCount > 0) m.prsCreated = (m.prsCreated || 0) + prsCreatedCount;
|
|
924
|
-
if (dispatchItem.type ===
|
|
1006
|
+
if (dispatchItem.type === WORK_TYPE.REVIEW) m.reviewsDone++;
|
|
925
1007
|
} else if (result === 'retry') {
|
|
926
1008
|
// Auto-retry: count cost but not as a final outcome
|
|
927
1009
|
m.tasksRetried = (m.tasksRetried || 0) + 1;
|
|
@@ -1003,7 +1085,7 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
1003
1085
|
if (!parent) continue;
|
|
1004
1086
|
|
|
1005
1087
|
// Mark parent as decomposed
|
|
1006
|
-
parent.status =
|
|
1088
|
+
parent.status = WI_STATUS.DECOMPOSED;
|
|
1007
1089
|
parent._decomposed = true;
|
|
1008
1090
|
delete parent._decomposing;
|
|
1009
1091
|
parent._subItemIds = subItems.map(s => s.id);
|
|
@@ -1017,7 +1099,7 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
1017
1099
|
type: (sub.estimated_complexity === 'large') ? 'implement:large' : 'implement',
|
|
1018
1100
|
priority: sub.priority || parent.priority || 'medium',
|
|
1019
1101
|
description: sub.description || '',
|
|
1020
|
-
status:
|
|
1102
|
+
status: WI_STATUS.PENDING,
|
|
1021
1103
|
complexity: sub.estimated_complexity || 'medium',
|
|
1022
1104
|
depends_on: sub.depends_on || [],
|
|
1023
1105
|
parent_id: parentId,
|
|
@@ -1042,7 +1124,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1042
1124
|
const type = dispatchItem.type;
|
|
1043
1125
|
const meta = dispatchItem.meta;
|
|
1044
1126
|
const isSuccess = code === 0;
|
|
1045
|
-
const result = isSuccess ?
|
|
1127
|
+
const result = isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR;
|
|
1046
1128
|
const { resultSummary, taskUsage, sessionId, model } = parseAgentOutput(stdout);
|
|
1047
1129
|
|
|
1048
1130
|
// Save session for potential resume on next dispatch
|
|
@@ -1057,13 +1139,13 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1057
1139
|
|
|
1058
1140
|
// Handle decomposition results — create sub-items from decompose agent output
|
|
1059
1141
|
let skipDoneStatus = false;
|
|
1060
|
-
if (type ===
|
|
1142
|
+
if (type === WORK_TYPE.DECOMPOSE && isSuccess && meta?.item?.id) {
|
|
1061
1143
|
const subCount = handleDecompositionResult(stdout, meta, config);
|
|
1062
1144
|
if (subCount > 0) skipDoneStatus = true; // parent already marked 'decomposed' by handler
|
|
1063
1145
|
// If decomposition produced nothing, fall through to mark parent as done
|
|
1064
1146
|
}
|
|
1065
1147
|
|
|
1066
|
-
if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta,
|
|
1148
|
+
if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, WI_STATUS.DONE, '');
|
|
1067
1149
|
if (!isSuccess && meta?.item?.id) {
|
|
1068
1150
|
// Auto-retry: read fresh _retryCount from file (not stale dispatch-time snapshot)
|
|
1069
1151
|
let retries = (meta.item._retryCount || 0);
|
|
@@ -1078,9 +1160,9 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1078
1160
|
}
|
|
1079
1161
|
} catch { /* optional */ }
|
|
1080
1162
|
|
|
1081
|
-
if (retries <
|
|
1082
|
-
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}
|
|
1083
|
-
updateWorkItemStatus(meta,
|
|
1163
|
+
if (retries < ENGINE_DEFAULTS.maxRetries) {
|
|
1164
|
+
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries}`);
|
|
1165
|
+
updateWorkItemStatus(meta, WI_STATUS.PENDING, '');
|
|
1084
1166
|
try {
|
|
1085
1167
|
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1086
1168
|
? path.join(MINIONS_DIR, 'work-items.json')
|
|
@@ -1089,17 +1171,17 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1089
1171
|
const items = safeJson(wiPath) || [];
|
|
1090
1172
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1091
1173
|
if (wi) {
|
|
1092
|
-
wi._retryCount = retries + 1; wi.status =
|
|
1093
|
-
if (type ===
|
|
1174
|
+
wi._retryCount = retries + 1; wi.status = WI_STATUS.PENDING; delete wi.dispatched_at; delete wi.dispatched_to;
|
|
1175
|
+
if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing; // clear so item can retry decomposition
|
|
1094
1176
|
shared.safeWrite(wiPath, items);
|
|
1095
1177
|
}
|
|
1096
1178
|
}
|
|
1097
1179
|
} catch (err) { log('warn', `Retry update: ${err.message}`); }
|
|
1098
1180
|
} else {
|
|
1099
|
-
updateWorkItemStatus(meta,
|
|
1181
|
+
updateWorkItemStatus(meta, WI_STATUS.FAILED, `Agent failed (${ENGINE_DEFAULTS.maxRetries} retries exhausted)`);
|
|
1100
1182
|
}
|
|
1101
1183
|
// Clear _decomposing flag on failure so item doesn't get permanently stuck
|
|
1102
|
-
if (type ===
|
|
1184
|
+
if (type === WORK_TYPE.DECOMPOSE) {
|
|
1103
1185
|
try {
|
|
1104
1186
|
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1105
1187
|
? path.join(MINIONS_DIR, 'work-items.json')
|
|
@@ -1113,7 +1195,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1113
1195
|
}
|
|
1114
1196
|
}
|
|
1115
1197
|
// Meeting post-completion: collect findings/debate/conclusion
|
|
1116
|
-
if (type ===
|
|
1198
|
+
if (type === WORK_TYPE.MEETING && meta?.meetingId) {
|
|
1117
1199
|
try {
|
|
1118
1200
|
const { collectMeetingFindings } = require('./meeting');
|
|
1119
1201
|
collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout);
|
|
@@ -1126,6 +1208,19 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1126
1208
|
let prsCreatedCount = 0;
|
|
1127
1209
|
if (isSuccess) prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
|
|
1128
1210
|
|
|
1211
|
+
// After verify completes, archive the plan
|
|
1212
|
+
if (isSuccess && meta?.item?.itemType === 'verify' && meta?.item?.sourcePlan) {
|
|
1213
|
+
try {
|
|
1214
|
+
const vPlanFile = meta.item.sourcePlan;
|
|
1215
|
+
const vPlanPath = path.join(PRD_DIR, vPlanFile);
|
|
1216
|
+
const vPlan = safeJson(vPlanPath);
|
|
1217
|
+
if (vPlan) {
|
|
1218
|
+
const vProjects = shared.getProjects(config);
|
|
1219
|
+
archivePlan(vPlanFile, vPlan, vProjects, config);
|
|
1220
|
+
}
|
|
1221
|
+
} catch (err) { log('warn', `Verify archive: ${err.message}`); }
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1129
1224
|
// Clean up worktree for non-shared-branch tasks after completion
|
|
1130
1225
|
if (meta?.branch && meta?.branchStrategy !== 'shared-branch') {
|
|
1131
1226
|
try {
|
|
@@ -1140,7 +1235,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1140
1235
|
return d.includes(branchSlug) && fs.statSync(path.join(worktreeRoot, d)).isDirectory();
|
|
1141
1236
|
});
|
|
1142
1237
|
// Only remove if no other active dispatch uses this branch
|
|
1143
|
-
const dispatch =
|
|
1238
|
+
const dispatch = getDispatch();
|
|
1144
1239
|
const otherActive = ((dispatch.active || []).concat(dispatch.pending || [])).some(d =>
|
|
1145
1240
|
d.id !== dispatchItem.id && d.meta?.branch && shared.sanitizeBranch && shared.sanitizeBranch(d.meta.branch) === branchSlug
|
|
1146
1241
|
);
|
|
@@ -1162,7 +1257,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1162
1257
|
}
|
|
1163
1258
|
|
|
1164
1259
|
// Detect implement tasks that completed without creating a PR
|
|
1165
|
-
if (isSuccess && (type ===
|
|
1260
|
+
if (isSuccess && (type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || type === WORK_TYPE.FIX) && prsCreatedCount === 0 && meta?.item?.id) {
|
|
1166
1261
|
// Check if a PR already exists linked to this work item (from a previous attempt)
|
|
1167
1262
|
const projects = shared.getProjects(config);
|
|
1168
1263
|
const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
|
@@ -1182,15 +1277,15 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1182
1277
|
wi.noPr = true;
|
|
1183
1278
|
wi.failReason = 'Completed without creating a pull request';
|
|
1184
1279
|
const retries = wi._retryCount || 0;
|
|
1185
|
-
if (retries <
|
|
1186
|
-
wi.status =
|
|
1280
|
+
if (retries < ENGINE_DEFAULTS.maxRetries) {
|
|
1281
|
+
wi.status = WI_STATUS.PENDING;
|
|
1187
1282
|
wi._retryCount = retries + 1;
|
|
1188
1283
|
delete wi.dispatched_at;
|
|
1189
1284
|
delete wi.dispatched_to;
|
|
1190
|
-
|
|
1285
|
+
log('info', `Auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries} for ${meta.item.id} (no PR created)`);
|
|
1191
1286
|
} else {
|
|
1192
|
-
wi.status =
|
|
1193
|
-
|
|
1287
|
+
wi.status = WI_STATUS.FAILED;
|
|
1288
|
+
log('warn', `${meta.item.id} failed after ${ENGINE_DEFAULTS.maxRetries} retries — no PR created`);
|
|
1194
1289
|
}
|
|
1195
1290
|
shared.safeWrite(wiPath, items);
|
|
1196
1291
|
}
|
|
@@ -1198,13 +1293,13 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1198
1293
|
}
|
|
1199
1294
|
}
|
|
1200
1295
|
|
|
1201
|
-
if (type ===
|
|
1202
|
-
if (type ===
|
|
1296
|
+
if (type === WORK_TYPE.REVIEW) updatePrAfterReview(agentId, meta?.pr, meta?.project);
|
|
1297
|
+
if (type === WORK_TYPE.FIX) updatePrAfterFix(meta?.pr, meta?.project, meta?.source);
|
|
1203
1298
|
checkForLearnings(agentId, config.agents[agentId], dispatchItem.task);
|
|
1204
1299
|
if (isSuccess) extractSkillsFromOutput(stdout, agentId, dispatchItem, config);
|
|
1205
1300
|
updateAgentHistory(agentId, dispatchItem, result);
|
|
1206
1301
|
// Don't count auto-retries as errors in metrics — only count final outcomes
|
|
1207
|
-
const isAutoRetry = !isSuccess && meta?.item?.id && (meta.item._retryCount || 0) <
|
|
1302
|
+
const isAutoRetry = !isSuccess && meta?.item?.id && (meta.item._retryCount || 0) < ENGINE_DEFAULTS.maxRetries;
|
|
1208
1303
|
const metricsResult = isAutoRetry ? 'retry' : result;
|
|
1209
1304
|
updateMetrics(agentId, dispatchItem, metricsResult, taskUsage, prsCreatedCount, model);
|
|
1210
1305
|
|
|
@@ -1229,14 +1324,14 @@ function syncPrdFromPrs(config) {
|
|
|
1229
1324
|
for (const project of allProjects) {
|
|
1230
1325
|
const wiPath = projectWorkItemsPath(project);
|
|
1231
1326
|
const items = safeJson(wiPath) || [];
|
|
1232
|
-
const hasPending = items.some(wi => wi.status ===
|
|
1327
|
+
const hasPending = items.some(wi => wi.status === WI_STATUS.PENDING && !wi._pr);
|
|
1233
1328
|
if (!hasPending) continue;
|
|
1234
1329
|
const reconciled = reconcileItemsWithPrs(items, allPrs);
|
|
1235
1330
|
if (reconciled > 0) {
|
|
1236
1331
|
safeWrite(wiPath, items);
|
|
1237
1332
|
// Sync done status to PRD JSON for each newly reconciled item
|
|
1238
1333
|
for (const wi of items) {
|
|
1239
|
-
if (wi.status ===
|
|
1334
|
+
if (wi.status === WI_STATUS.DONE) syncPrdItemStatus(wi.id, WI_STATUS.DONE, wi.sourcePlan);
|
|
1240
1335
|
}
|
|
1241
1336
|
totalReconciled += reconciled;
|
|
1242
1337
|
}
|
|
@@ -1252,6 +1347,7 @@ function syncPrdFromPrs(config) {
|
|
|
1252
1347
|
|
|
1253
1348
|
module.exports = {
|
|
1254
1349
|
checkPlanCompletion,
|
|
1350
|
+
archivePlan,
|
|
1255
1351
|
updateWorkItemStatus,
|
|
1256
1352
|
syncPrdItemStatus,
|
|
1257
1353
|
syncPrsFromOutput,
|
package/engine/playbook.js
CHANGED
|
@@ -400,19 +400,19 @@ function buildAgentContext(agentId, config, project) {
|
|
|
400
400
|
const projects = getProjects(config);
|
|
401
401
|
const allPrs = [];
|
|
402
402
|
for (const p of projects) {
|
|
403
|
-
const prs = getPrs(p).filter(pr => pr.status === PR_STATUS.ACTIVE
|
|
403
|
+
const prs = getPrs(p).filter(pr => pr.status === PR_STATUS.ACTIVE);
|
|
404
404
|
for (const pr of prs) allPrs.push({ ...pr, _project: p.name });
|
|
405
405
|
}
|
|
406
406
|
// Also check central pull-requests.json
|
|
407
407
|
try {
|
|
408
408
|
const centralPrs = safeJson(path.join(MINIONS_DIR, 'pull-requests.json')) || [];
|
|
409
|
-
for (const pr of centralPrs.filter(pr => pr.status === PR_STATUS.ACTIVE
|
|
409
|
+
for (const pr of centralPrs.filter(pr => pr.status === PR_STATUS.ACTIVE)) {
|
|
410
410
|
if (!allPrs.some(p => p.id === pr.id)) allPrs.push({ ...pr, _project: 'central' });
|
|
411
411
|
}
|
|
412
412
|
} catch (e) { log('warn', 'read central pull-requests: ' + e.message); }
|
|
413
413
|
if (allPrs.length > 0) {
|
|
414
414
|
const prLines = allPrs.map(pr =>
|
|
415
|
-
`- **${pr.id}** (${pr._project}): ${(pr.title || '').slice(0, 80)} [${
|
|
415
|
+
`- **${pr.id}** (${pr._project}): ${(pr.title || '').slice(0, 80)} [${(pr.reviewStatus || 'pending')}${pr.buildStatus === 'failing' ? ', BUILD FAILING' : ''}]${pr.branch ? ' branch: `' + pr.branch + '`' : ''}${pr._context ? ' — ' + pr._context.slice(0, 100) : ''}`
|
|
416
416
|
);
|
|
417
417
|
context += `## Active Pull Requests\n\n${prLines.join('\n')}\n\n`;
|
|
418
418
|
}
|
package/engine/queries.js
CHANGED
|
@@ -177,7 +177,7 @@ function getAgentStatus(agentId) {
|
|
|
177
177
|
const latestInFlight = allItems
|
|
178
178
|
.filter(w =>
|
|
179
179
|
(w.dispatched_to || '').toLowerCase() === String(agentId).toLowerCase() &&
|
|
180
|
-
|
|
180
|
+
w.status === 'dispatched'
|
|
181
181
|
)
|
|
182
182
|
.sort((a, b) => (b.dispatched_at || '').localeCompare(a.dispatched_at || ''))[0];
|
|
183
183
|
if (latestInFlight) {
|
|
@@ -679,9 +679,9 @@ function getPrdInfo(config) {
|
|
|
679
679
|
}
|
|
680
680
|
|
|
681
681
|
// PRD JSON status is the source of truth — kept in sync with work item by syncPrdItemStatus.
|
|
682
|
-
// Map from PRD JSON values to display values (
|
|
682
|
+
// Map from PRD JSON values to display values (pending → missing for undispatched items)
|
|
683
683
|
// Augment each item with execution metadata from the work item.
|
|
684
|
-
const statusDisplay = {
|
|
684
|
+
const statusDisplay = { pending: 'missing' };
|
|
685
685
|
for (const item of items) {
|
|
686
686
|
const wi = wiById[item.id];
|
|
687
687
|
// Work item status is source of truth when available (PRD JSON may lag behind)
|
|
@@ -697,7 +697,7 @@ function getPrdInfo(config) {
|
|
|
697
697
|
const byStatus = {};
|
|
698
698
|
items.forEach(item => { const s = item.status || 'missing'; byStatus[s] = byStatus[s] || []; byStatus[s].push(item); });
|
|
699
699
|
const complete = (byStatus['done'] || []).length;
|
|
700
|
-
const inProgress = (byStatus['
|
|
700
|
+
const inProgress = (byStatus['dispatched'] || []).length;
|
|
701
701
|
const paused = (byStatus['paused'] || []).length;
|
|
702
702
|
const missing = (byStatus['missing'] || []).length;
|
|
703
703
|
const donePercent = total > 0 ? Math.round((complete / total) * 100) : 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.330",
|
|
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"
|