@yemi33/minions 0.1.327 → 0.1.329
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 +10 -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 +104 -176
- package/engine/playbook.js +3 -3
- package/engine/queries.js +4 -4
- package/engine.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.329 (2026-04-03)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- remove orphan statuses, add validation, replace in-progress with dispatched
|
|
7
|
+
|
|
8
|
+
## 0.1.328 (2026-04-03)
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
- import PR_STATUS in engine.js — was causing discoverWork to fail
|
|
12
|
+
|
|
3
13
|
## 0.1.327 (2026-04-03)
|
|
4
14
|
|
|
5
15
|
### Fixes
|
|
@@ -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
|
@@ -23,9 +23,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
23
23
|
const planPath = path.join(PRD_DIR, planFile);
|
|
24
24
|
const plan = safeJson(planPath);
|
|
25
25
|
if (!plan?.missing_features) return;
|
|
26
|
-
if (plan.status ===
|
|
26
|
+
if (plan.status === PLAN_STATUS.COMPLETED) {
|
|
27
27
|
if (plan._completionNotified) return;
|
|
28
|
-
// Crash recovery: status=completed but _completionNotified not set — fall through
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
const projects = shared.getProjects(config);
|
|
@@ -81,7 +80,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
81
80
|
const failedItems = planItems.filter(w => w.status === WI_STATUS.FAILED);
|
|
82
81
|
|
|
83
82
|
// 1. Mark plan as completed
|
|
84
|
-
plan.status =
|
|
83
|
+
plan.status = PLAN_STATUS.COMPLETED;
|
|
85
84
|
plan.completedAt = ts();
|
|
86
85
|
|
|
87
86
|
// Compute timing
|
|
@@ -137,30 +136,15 @@ function checkPlanCompletion(meta, config) {
|
|
|
137
136
|
...uniquePrs.map(pr => `- ${pr.id}: ${pr.title || ''} ${pr.url || ''}`),
|
|
138
137
|
].filter(Boolean).join('\n');
|
|
139
138
|
|
|
140
|
-
// Write summary to notes/inbox
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Persist status and _completionNotified atomically BEFORE creating work items
|
|
146
|
-
// NOTE: Do NOT set plan._completionNotified in-memory before persist —
|
|
147
|
-
// if persist fails, the in-memory flag would prevent retry on next tick.
|
|
148
|
-
mutateJsonFileLocked(planPath, (data) => {
|
|
149
|
-
data.status = 'completed';
|
|
150
|
-
data.completedAt = plan.completedAt;
|
|
151
|
-
data._completionNotified = true;
|
|
152
|
-
if (plan._timing) data._timing = plan._timing;
|
|
153
|
-
return data;
|
|
154
|
-
});
|
|
139
|
+
// Write summary to notes/inbox
|
|
140
|
+
const summaryFile = `prd-completion-${planFile.replace('.json', '')}-${ts().slice(0, 10)}.md`;
|
|
141
|
+
shared.safeWrite(shared.uniquePath(path.join(MINIONS_DIR, 'notes', 'inbox', summaryFile)), summary);
|
|
142
|
+
log('info', `PRD completion summary written to notes/inbox/${summaryFile}`);
|
|
155
143
|
|
|
156
144
|
// Resolve the primary project for writing new work items (PR, verify)
|
|
157
145
|
const projectName = plan.project;
|
|
158
146
|
const primaryProject = projectName
|
|
159
|
-
? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) :
|
|
160
|
-
if (!primaryProject) {
|
|
161
|
-
log('warn', `checkPlanCompletion: no project available (projects array ${projects.length === 0 ? 'empty' : 'no match for ' + projectName}) — skipping PR/verify creation for ${planFile}`);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
147
|
+
? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) : projects[0];
|
|
164
148
|
const wiPath = primaryProject ? shared.projectWorkItemsPath(primaryProject) : null;
|
|
165
149
|
const workItems = wiPath ? (safeJson(wiPath) || []) : [];
|
|
166
150
|
|
|
@@ -176,7 +160,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
176
160
|
id, title: `Create PR for plan: ${plan.plan_summary || planFile}`,
|
|
177
161
|
type: 'implement', priority: 'high',
|
|
178
162
|
description: `All plan items from \`${planFile}\` are complete on branch \`${featureBranch}\`.\n\n**Branch:** \`${featureBranch}\`\n**Target:** \`${mainBranch}\`\n\n## Completed Items\n${itemSummary}`,
|
|
179
|
-
status:
|
|
163
|
+
status: WI_STATUS.PENDING, created: ts(), createdBy: 'engine:plan-completion',
|
|
180
164
|
sourcePlan: planFile, itemType: 'pr',
|
|
181
165
|
branch: featureBranch, branchStrategy: 'shared-branch', project: projectName,
|
|
182
166
|
});
|
|
@@ -206,12 +190,11 @@ function checkPlanCompletion(meta, config) {
|
|
|
206
190
|
|
|
207
191
|
// Build per-project checkout commands: one worktree, merge all PR branches into it
|
|
208
192
|
const checkoutBlocks = Object.entries(projectPrs).map(([name, { project: p, prs, mainBranch }]) => {
|
|
209
|
-
const
|
|
210
|
-
const wtPath = `${localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
|
|
193
|
+
const wtPath = `${p.localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
|
|
211
194
|
const branches = prs.map(pr => pr.branch).filter(Boolean);
|
|
212
195
|
const lines = [
|
|
213
196
|
`# ${name} — merge ${branches.length} PR branch(es) into one worktree`,
|
|
214
|
-
`cd "${localPath}"`,
|
|
197
|
+
`cd "${p.localPath.replace(/\\/g, '/')}"`,
|
|
215
198
|
`git fetch origin ${branches.map(b => `"${b}"`).join(' ')} "${mainBranch}"`,
|
|
216
199
|
`git worktree add "${wtPath}" "origin/${mainBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${mainBranch}" && git pull origin "${mainBranch}")`,
|
|
217
200
|
`cd "${wtPath}"`,
|
|
@@ -232,10 +215,9 @@ function checkPlanCompletion(meta, config) {
|
|
|
232
215
|
).join('\n');
|
|
233
216
|
|
|
234
217
|
// List projects and their worktree paths for the agent
|
|
235
|
-
const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) =>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}).join('\n');
|
|
218
|
+
const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) =>
|
|
219
|
+
`- **${name}**: \`${p.localPath}/../worktrees/verify-${planSlug}\``
|
|
220
|
+
).join('\n');
|
|
239
221
|
|
|
240
222
|
const description = [
|
|
241
223
|
`Verification task for completed plan \`${planFile}\`.`,
|
|
@@ -269,7 +251,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
269
251
|
type: 'verify',
|
|
270
252
|
priority: 'high',
|
|
271
253
|
description,
|
|
272
|
-
status:
|
|
254
|
+
status: WI_STATUS.PENDING,
|
|
273
255
|
created: ts(),
|
|
274
256
|
createdBy: 'engine:plan-verification',
|
|
275
257
|
sourcePlan: planFile,
|
|
@@ -280,75 +262,46 @@ function checkPlanCompletion(meta, config) {
|
|
|
280
262
|
log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
|
|
281
263
|
}
|
|
282
264
|
|
|
283
|
-
// 5. Archive
|
|
284
|
-
// Plan stays active until verification finishes so artifacts are visible.
|
|
285
|
-
|
|
286
|
-
log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ─── Plan Archiving (called after verify completes) ─────────────────────────
|
|
290
|
-
|
|
291
|
-
function archivePlan(planFile, plan, projects, config) {
|
|
292
|
-
const planPath = path.join(PRD_DIR, planFile);
|
|
293
|
-
const projectName = plan.project || '';
|
|
294
|
-
|
|
295
|
-
// Archive PRD .json to prd/archive/
|
|
265
|
+
// 5. Archive: move PRD .json to prd/archive/ and source .md plan to plans/archive/
|
|
296
266
|
const prdArchiveDir = path.join(PRD_DIR, 'archive');
|
|
297
267
|
if (!fs.existsSync(prdArchiveDir)) fs.mkdirSync(prdArchiveDir, { recursive: true });
|
|
298
|
-
|
|
268
|
+
shared.safeWrite(planPath, plan); // save completed status first
|
|
269
|
+
try {
|
|
270
|
+
fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
|
|
271
|
+
log('info', `Archived completed PRD: prd/archive/${planFile}`);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
|
|
299
274
|
shared.safeWrite(planPath, plan);
|
|
300
|
-
try {
|
|
301
|
-
fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
|
|
302
|
-
log('info', `Archived completed PRD: prd/archive/${planFile}`);
|
|
303
|
-
} catch (err) { log('warn', `Failed to archive PRD ${planFile}: ${err.message}`); }
|
|
304
275
|
}
|
|
305
276
|
|
|
306
|
-
//
|
|
277
|
+
// Also archive the source .md plan if it exists
|
|
307
278
|
const planArchiveDir = path.join(PLANS_DIR, 'archive');
|
|
308
279
|
if (!fs.existsSync(planArchiveDir)) fs.mkdirSync(planArchiveDir, { recursive: true });
|
|
309
|
-
if (plan.source_plan) {
|
|
310
|
-
const mdPath = path.join(PLANS_DIR, plan.source_plan);
|
|
311
|
-
if (fs.existsSync(mdPath)) {
|
|
312
|
-
try {
|
|
313
|
-
fs.renameSync(mdPath, path.join(planArchiveDir, plan.source_plan));
|
|
314
|
-
log('info', `Archived source plan: plans/archive/${plan.source_plan}`);
|
|
315
|
-
} catch (err) { log('warn', `Failed to archive source plan ${plan.source_plan}: ${err.message}`); }
|
|
316
|
-
}
|
|
317
|
-
} else {
|
|
318
|
-
try {
|
|
319
|
-
const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
|
|
320
|
-
for (const md of mdFiles) {
|
|
321
|
-
const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
|
|
322
|
-
if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
|
|
323
|
-
try {
|
|
324
|
-
fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
|
|
325
|
-
log('info', `Archived source plan: plans/archive/${md}`);
|
|
326
|
-
} catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
} catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
|
|
334
280
|
try {
|
|
335
|
-
|
|
336
|
-
for (const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
281
|
+
const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
|
|
282
|
+
for (const md of mdFiles) {
|
|
283
|
+
const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
|
|
284
|
+
// Match by project name or plan summary appearing in the .md content
|
|
285
|
+
if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
|
|
286
|
+
try {
|
|
287
|
+
fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
|
|
288
|
+
log('info', `Archived source plan: plans/archive/${md}`);
|
|
289
|
+
} catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
343
292
|
}
|
|
293
|
+
} catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
|
|
344
294
|
|
|
295
|
+
// 6. Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
|
|
296
|
+
try {
|
|
297
|
+
// Collect all branch slugs: shared-branch + per-item branches + item IDs
|
|
345
298
|
const branchSlugs = new Set();
|
|
346
299
|
if (plan.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
|
|
347
|
-
for (const w of
|
|
300
|
+
for (const w of doneItems) {
|
|
348
301
|
if (w.branch) branchSlugs.add(shared.sanitizeBranch(w.branch).toLowerCase());
|
|
349
302
|
if (w.id) branchSlugs.add(w.id.toLowerCase());
|
|
350
303
|
}
|
|
351
|
-
for (const pr of
|
|
304
|
+
for (const pr of uniquePrs) {
|
|
352
305
|
if (pr.branch) branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
|
|
353
306
|
}
|
|
354
307
|
|
|
@@ -370,8 +323,10 @@ function archivePlan(planFile, plan, projects, config) {
|
|
|
370
323
|
}
|
|
371
324
|
}
|
|
372
325
|
}
|
|
373
|
-
if (cleanedWt > 0) log('info', `Plan
|
|
326
|
+
if (cleanedWt > 0) log('info', `Plan completion: cleaned ${cleanedWt} worktree(s)`);
|
|
374
327
|
} catch (err) { log('warn', `Worktree cleanup: ${err.message}`); }
|
|
328
|
+
|
|
329
|
+
log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
|
|
375
330
|
}
|
|
376
331
|
|
|
377
332
|
// ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
|
|
@@ -430,10 +385,6 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
430
385
|
|
|
431
386
|
const projectName = meta?.item?.project || meta?.project?.name;
|
|
432
387
|
const projects = shared.getProjects(config);
|
|
433
|
-
if (projects.length === 0) {
|
|
434
|
-
log('error', 'Plan chaining: no projects configured — cannot chain plan to PRD');
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
388
|
const targetProject = projectName
|
|
438
389
|
? projects.find(p => p.name === projectName) || projects[0]
|
|
439
390
|
: projects[0];
|
|
@@ -446,17 +397,14 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
446
397
|
log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
|
|
447
398
|
const wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
448
399
|
let items = [];
|
|
449
|
-
try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch
|
|
450
|
-
log('warn', `Failed to parse ${wiPath}: ${err.message} — creating .bak and starting fresh`);
|
|
451
|
-
try { fs.copyFileSync(wiPath, wiPath + '.bak'); } catch {}
|
|
452
|
-
}
|
|
400
|
+
try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch {}
|
|
453
401
|
items.push({
|
|
454
402
|
id: 'W-' + shared.uid(),
|
|
455
403
|
title: `Convert plan to PRD: ${meta?.item?.title || planFile.name}`,
|
|
456
404
|
type: 'plan-to-prd',
|
|
457
405
|
priority: meta?.item?.priority || 'high',
|
|
458
406
|
description: `Plan file: plans/${planFile.name}\nChained from plan task ${dispatchItem.id}`,
|
|
459
|
-
status:
|
|
407
|
+
status: WI_STATUS.PENDING,
|
|
460
408
|
created: ts(),
|
|
461
409
|
createdBy: 'engine:chain',
|
|
462
410
|
project: targetProject.name,
|
|
@@ -466,10 +414,15 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
466
414
|
}
|
|
467
415
|
|
|
468
416
|
// ─── Work Item Status ────────────────────────────────────────────────────────
|
|
417
|
+
const _VALID_WI_STATUSES = new Set(Object.values(WI_STATUS));
|
|
469
418
|
function updateWorkItemStatus(meta, status, reason) {
|
|
470
419
|
|
|
471
420
|
const itemId = meta.item?.id;
|
|
472
421
|
if (!itemId) return;
|
|
422
|
+
if (!_VALID_WI_STATUSES.has(status)) {
|
|
423
|
+
log('warn', `Invalid work item status '${status}' for ${itemId} — ignoring`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
473
426
|
|
|
474
427
|
let wiPath;
|
|
475
428
|
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
@@ -530,8 +483,10 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
530
483
|
}
|
|
531
484
|
}
|
|
532
485
|
|
|
486
|
+
const _VALID_PRD_STATUSES = new Set([...Object.values(WI_STATUS), 'missing']);
|
|
533
487
|
function syncPrdItemStatus(itemId, status, sourcePlan) {
|
|
534
488
|
if (!itemId) return;
|
|
489
|
+
if (!_VALID_PRD_STATUSES.has(status)) return;
|
|
535
490
|
try {
|
|
536
491
|
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
537
492
|
const files = sourcePlan ? [sourcePlan] : require('fs').readdirSync(prdDir).filter(f => f.endsWith('.json'));
|
|
@@ -594,11 +549,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
594
549
|
if (prMatches.size === 0) return 0;
|
|
595
550
|
|
|
596
551
|
const projects = shared.getProjects(config);
|
|
597
|
-
|
|
598
|
-
log('warn', `syncPrsFromOutput: no projects configured and no project in meta — cannot sync PRs`);
|
|
599
|
-
return 0;
|
|
600
|
-
}
|
|
601
|
-
const defaultProject = (meta?.project?.name && projects.find(p => p.name === meta.project.name)) || (projects[0] || null);
|
|
552
|
+
const defaultProject = (meta?.project?.name && projects.find(p => p.name === meta.project.name)) || projects[0];
|
|
602
553
|
const useCentral = !defaultProject;
|
|
603
554
|
|
|
604
555
|
// Match each PR to its correct project by finding which repo URL appears near the PR number in output
|
|
@@ -626,54 +577,47 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
626
577
|
const agentName = config.agents?.[agentId]?.name || agentId;
|
|
627
578
|
let added = 0;
|
|
628
579
|
const centralPrPath = path.join(MINIONS_DIR, 'pull-requests.json');
|
|
580
|
+
// Track which PR files need writing — keyed by target name
|
|
581
|
+
const dirtyTargets = new Map(); // name -> { prs, prPath }
|
|
629
582
|
|
|
630
|
-
// Group PR matches by target file so we take one lock per target
|
|
631
|
-
const targetPrIds = new Map(); // targetName -> { prPath, prIds: [{ prId, fullId }] }
|
|
632
583
|
for (const prId of prMatches) {
|
|
633
584
|
const fullId = `PR-${prId}`;
|
|
634
585
|
const targetProject = useCentral ? null : resolveProjectForPr(prId);
|
|
635
586
|
const targetName = targetProject ? targetProject.name : '_central';
|
|
636
587
|
const prPath = targetProject ? shared.projectPrPath(targetProject) : centralPrPath;
|
|
637
|
-
|
|
638
|
-
|
|
588
|
+
|
|
589
|
+
// Load PRs for this target (cache per target)
|
|
590
|
+
if (!dirtyTargets.has(targetName)) {
|
|
591
|
+
dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
|
|
639
592
|
}
|
|
640
|
-
|
|
593
|
+
const entry = dirtyTargets.get(targetName);
|
|
594
|
+
if (entry.prs.some(p => p.id === fullId || String(p.id).includes(prId))) continue;
|
|
595
|
+
|
|
596
|
+
let title = meta?.item?.title || '';
|
|
597
|
+
const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
|
|
598
|
+
if (titleMatch) title = titleMatch[1].trim();
|
|
599
|
+
if (title.includes('session_id') || title.includes('is_error') || title.includes('uuid') || title.length > 120) {
|
|
600
|
+
title = meta?.item?.title || '';
|
|
601
|
+
}
|
|
602
|
+
entry.prs.push({
|
|
603
|
+
id: fullId,
|
|
604
|
+
title: (title || `PR created by ${agentName}`).slice(0, 120),
|
|
605
|
+
agent: agentName,
|
|
606
|
+
branch: meta?.branch || '',
|
|
607
|
+
reviewStatus: 'pending',
|
|
608
|
+
status: PR_STATUS.ACTIVE,
|
|
609
|
+
created: dateStamp(),
|
|
610
|
+
url: extractPrUrl(prId),
|
|
611
|
+
prdItems: meta?.item?.id ? [meta.item.id] : [],
|
|
612
|
+
sourcePlan: meta?.item?.sourcePlan || '',
|
|
613
|
+
itemType: meta?.item?.itemType || ''
|
|
614
|
+
});
|
|
615
|
+
if (meta?.item?.id) addPrLink(fullId, meta.item.id);
|
|
616
|
+
added++;
|
|
641
617
|
}
|
|
642
618
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
mutateJsonFileLocked(prPath, (prs) => {
|
|
646
|
-
if (!Array.isArray(prs)) prs = [];
|
|
647
|
-
// Deduplicate any existing entries with same id (case-insensitive agent name race)
|
|
648
|
-
const seen = new Set();
|
|
649
|
-
prs = prs.filter(p => { const k = String(p.id); if (seen.has(k)) return false; seen.add(k); return true; });
|
|
650
|
-
for (const { prId, fullId } of prIds) {
|
|
651
|
-
if (prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
|
|
652
|
-
|
|
653
|
-
let title = meta?.item?.title || '';
|
|
654
|
-
const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
|
|
655
|
-
if (titleMatch) title = titleMatch[1].trim();
|
|
656
|
-
if (title.includes('session_id') || title.includes('is_error') || title.includes('uuid') || title.length > 120) {
|
|
657
|
-
title = meta?.item?.title || '';
|
|
658
|
-
}
|
|
659
|
-
prs.push({
|
|
660
|
-
id: fullId,
|
|
661
|
-
title: (title || `PR created by ${agentName}`).slice(0, 120),
|
|
662
|
-
agent: agentName,
|
|
663
|
-
branch: meta?.branch || '',
|
|
664
|
-
reviewStatus: 'pending',
|
|
665
|
-
status: 'active',
|
|
666
|
-
created: dateStamp(),
|
|
667
|
-
url: extractPrUrl(prId),
|
|
668
|
-
prdItems: meta?.item?.id ? [meta.item.id] : [],
|
|
669
|
-
sourcePlan: meta?.item?.sourcePlan || '',
|
|
670
|
-
itemType: meta?.item?.itemType || ''
|
|
671
|
-
});
|
|
672
|
-
if (meta?.item?.id) addPrLink(fullId, meta.item.id);
|
|
673
|
-
added++;
|
|
674
|
-
}
|
|
675
|
-
return prs;
|
|
676
|
-
}, { defaultValue: [] });
|
|
619
|
+
for (const [name, entry] of dirtyTargets) {
|
|
620
|
+
shared.safeWrite(entry.prPath, entry.prs);
|
|
677
621
|
log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
|
|
678
622
|
}
|
|
679
623
|
return added;
|
|
@@ -693,7 +637,7 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
693
637
|
// Record the reviewer — actual verdict comes from ADO/GitHub votes via pollPrStatus.
|
|
694
638
|
// Set to 'waiting' so pollPrStatus updates it with the real vote on next cycle.
|
|
695
639
|
const dispatch = getDispatch();
|
|
696
|
-
const completedEntry = (dispatch.completed || []).find(d => d.agent === agentId && d.type ===
|
|
640
|
+
const completedEntry = (dispatch.completed || []).find(d => d.agent === agentId && d.type === 'review');
|
|
697
641
|
|
|
698
642
|
// Set reviewStatus to 'waiting' (single source of truth — synced from ADO/GitHub votes on next poll)
|
|
699
643
|
target.reviewStatus = 'waiting';
|
|
@@ -718,7 +662,7 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
718
662
|
}
|
|
719
663
|
|
|
720
664
|
shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
|
|
721
|
-
log('info', `Updated ${pr.id} → minions review: ${
|
|
665
|
+
log('info', `Updated ${pr.id} → minions review: ${minionsVerdict} by ${reviewerName}`);
|
|
722
666
|
createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
|
|
723
667
|
}
|
|
724
668
|
|
|
@@ -770,7 +714,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
770
714
|
} catch (err) { log('warn', `Post-merge worktree cleanup: ${err.message}`); }
|
|
771
715
|
}
|
|
772
716
|
|
|
773
|
-
if (newStatus !==
|
|
717
|
+
if (newStatus !== PR_STATUS.MERGED) return;
|
|
774
718
|
|
|
775
719
|
// Resolve linked work item from pr-links or PR branch name
|
|
776
720
|
let mergedItemId = getPrLinks()[pr.id];
|
|
@@ -894,12 +838,12 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
894
838
|
if (proj) {
|
|
895
839
|
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
896
840
|
const items = safeJson(centralPath) || [];
|
|
897
|
-
const alreadyExists = items.some(i => i.title === `Add skill: ${name}` && i.status !==
|
|
841
|
+
const alreadyExists = items.some(i => i.title === `Add skill: ${name}` && i.status !== WI_STATUS.FAILED);
|
|
898
842
|
if (!alreadyExists) {
|
|
899
843
|
const skillId = `SK${String(items.filter(i => i.id?.startsWith('SK')).length + 1).padStart(3, '0')}`;
|
|
900
844
|
items.push({ id: skillId, type: 'implement', title: `Add skill: ${name}`,
|
|
901
845
|
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\`\`\``,
|
|
902
|
-
priority: 'low', status:
|
|
846
|
+
priority: 'low', status: WI_STATUS.QUEUED, created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
|
|
903
847
|
shared.safeWrite(centralPath, items);
|
|
904
848
|
log('info', `Queued work item ${skillId} to PR project skill "${name}" into ${project}`);
|
|
905
849
|
}
|
|
@@ -1084,7 +1028,7 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
1084
1028
|
type: (sub.estimated_complexity === 'large') ? 'implement:large' : 'implement',
|
|
1085
1029
|
priority: sub.priority || parent.priority || 'medium',
|
|
1086
1030
|
description: sub.description || '',
|
|
1087
|
-
status:
|
|
1031
|
+
status: WI_STATUS.PENDING,
|
|
1088
1032
|
complexity: sub.estimated_complexity || 'medium',
|
|
1089
1033
|
depends_on: sub.depends_on || [],
|
|
1090
1034
|
parent_id: parentId,
|
|
@@ -1124,13 +1068,13 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1124
1068
|
|
|
1125
1069
|
// Handle decomposition results — create sub-items from decompose agent output
|
|
1126
1070
|
let skipDoneStatus = false;
|
|
1127
|
-
if (type ===
|
|
1071
|
+
if (type === WORK_TYPE.DECOMPOSE && isSuccess && meta?.item?.id) {
|
|
1128
1072
|
const subCount = handleDecompositionResult(stdout, meta, config);
|
|
1129
1073
|
if (subCount > 0) skipDoneStatus = true; // parent already marked 'decomposed' by handler
|
|
1130
1074
|
// If decomposition produced nothing, fall through to mark parent as done
|
|
1131
1075
|
}
|
|
1132
1076
|
|
|
1133
|
-
if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta,
|
|
1077
|
+
if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, WI_STATUS.DONE, '');
|
|
1134
1078
|
if (!isSuccess && meta?.item?.id) {
|
|
1135
1079
|
// Auto-retry: read fresh _retryCount from file (not stale dispatch-time snapshot)
|
|
1136
1080
|
let retries = (meta.item._retryCount || 0);
|
|
@@ -1145,9 +1089,8 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1145
1089
|
}
|
|
1146
1090
|
} catch { /* optional */ }
|
|
1147
1091
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/${maxRetries}`);
|
|
1092
|
+
if (retries < ENGINE_DEFAULTS.maxRetries) {
|
|
1093
|
+
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries}`);
|
|
1151
1094
|
updateWorkItemStatus(meta, WI_STATUS.PENDING, '');
|
|
1152
1095
|
try {
|
|
1153
1096
|
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
@@ -1158,16 +1101,16 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1158
1101
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1159
1102
|
if (wi) {
|
|
1160
1103
|
wi._retryCount = retries + 1; wi.status = WI_STATUS.PENDING; delete wi.dispatched_at; delete wi.dispatched_to;
|
|
1161
|
-
if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing;
|
|
1104
|
+
if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing; // clear so item can retry decomposition
|
|
1162
1105
|
shared.safeWrite(wiPath, items);
|
|
1163
1106
|
}
|
|
1164
1107
|
}
|
|
1165
1108
|
} catch (err) { log('warn', `Retry update: ${err.message}`); }
|
|
1166
1109
|
} else {
|
|
1167
|
-
updateWorkItemStatus(meta, WI_STATUS.FAILED, `Agent failed (${maxRetries} retries exhausted)`);
|
|
1110
|
+
updateWorkItemStatus(meta, WI_STATUS.FAILED, `Agent failed (${ENGINE_DEFAULTS.maxRetries} retries exhausted)`);
|
|
1168
1111
|
}
|
|
1169
1112
|
// Clear _decomposing flag on failure so item doesn't get permanently stuck
|
|
1170
|
-
if (type ===
|
|
1113
|
+
if (type === WORK_TYPE.DECOMPOSE) {
|
|
1171
1114
|
try {
|
|
1172
1115
|
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1173
1116
|
? path.join(MINIONS_DIR, 'work-items.json')
|
|
@@ -1181,7 +1124,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1181
1124
|
}
|
|
1182
1125
|
}
|
|
1183
1126
|
// Meeting post-completion: collect findings/debate/conclusion
|
|
1184
|
-
if (type ===
|
|
1127
|
+
if (type === WORK_TYPE.MEETING && meta?.meetingId) {
|
|
1185
1128
|
try {
|
|
1186
1129
|
const { collectMeetingFindings } = require('./meeting');
|
|
1187
1130
|
collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout);
|
|
@@ -1194,19 +1137,6 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1194
1137
|
let prsCreatedCount = 0;
|
|
1195
1138
|
if (isSuccess) prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
|
|
1196
1139
|
|
|
1197
|
-
// Archive plan after verify task completes (AFTER PR sync so E2E PR is linked)
|
|
1198
|
-
if (meta?.item?.itemType === 'verify' && meta?.item?.sourcePlan) {
|
|
1199
|
-
try {
|
|
1200
|
-
const vPlanFile = meta.item.sourcePlan;
|
|
1201
|
-
const vPlanPath = path.join(PRD_DIR, vPlanFile);
|
|
1202
|
-
const vPlan = safeJson(vPlanPath);
|
|
1203
|
-
if (vPlan) {
|
|
1204
|
-
const vProjects = shared.getProjects(config);
|
|
1205
|
-
archivePlan(vPlanFile, vPlan, vProjects, config);
|
|
1206
|
-
}
|
|
1207
|
-
} catch (err) { log('warn', `Verify archive: ${err.message}`); }
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
1140
|
// Clean up worktree for non-shared-branch tasks after completion
|
|
1211
1141
|
if (meta?.branch && meta?.branchStrategy !== 'shared-branch') {
|
|
1212
1142
|
try {
|
|
@@ -1229,7 +1159,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1229
1159
|
for (const dir of dirs) {
|
|
1230
1160
|
const wtPath = path.join(worktreeRoot, dir);
|
|
1231
1161
|
try {
|
|
1232
|
-
|
|
1162
|
+
shared.exec(`git worktree remove "${wtPath}" --force`, { cwd: rootDir, stdio: 'pipe', timeout: 15000, windowsHide: true });
|
|
1233
1163
|
log('info', `Post-completion: removed worktree ${dir}`);
|
|
1234
1164
|
} catch (err) {
|
|
1235
1165
|
log('warn', `Post-completion: failed to remove worktree ${dir}: ${err.message}`);
|
|
@@ -1263,16 +1193,15 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1263
1193
|
wi.noPr = true;
|
|
1264
1194
|
wi.failReason = 'Completed without creating a pull request';
|
|
1265
1195
|
const retries = wi._retryCount || 0;
|
|
1266
|
-
|
|
1267
|
-
if (retries < maxR) {
|
|
1196
|
+
if (retries < ENGINE_DEFAULTS.maxRetries) {
|
|
1268
1197
|
wi.status = WI_STATUS.PENDING;
|
|
1269
1198
|
wi._retryCount = retries + 1;
|
|
1270
1199
|
delete wi.dispatched_at;
|
|
1271
1200
|
delete wi.dispatched_to;
|
|
1272
|
-
log('info', `Auto-retry ${retries + 1}/${
|
|
1201
|
+
log('info', `Auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries} for ${meta.item.id} (no PR created)`);
|
|
1273
1202
|
} else {
|
|
1274
1203
|
wi.status = WI_STATUS.FAILED;
|
|
1275
|
-
log('warn', `${meta.item.id} failed after ${
|
|
1204
|
+
log('warn', `${meta.item.id} failed after ${ENGINE_DEFAULTS.maxRetries} retries — no PR created`);
|
|
1276
1205
|
}
|
|
1277
1206
|
shared.safeWrite(wiPath, items);
|
|
1278
1207
|
}
|
|
@@ -1286,7 +1215,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1286
1215
|
if (isSuccess) extractSkillsFromOutput(stdout, agentId, dispatchItem, config);
|
|
1287
1216
|
updateAgentHistory(agentId, dispatchItem, result);
|
|
1288
1217
|
// Don't count auto-retries as errors in metrics — only count final outcomes
|
|
1289
|
-
const isAutoRetry = !isSuccess && meta?.item?.id && (meta.item._retryCount || 0) <
|
|
1218
|
+
const isAutoRetry = !isSuccess && meta?.item?.id && (meta.item._retryCount || 0) < ENGINE_DEFAULTS.maxRetries;
|
|
1290
1219
|
const metricsResult = isAutoRetry ? 'retry' : result;
|
|
1291
1220
|
updateMetrics(agentId, dispatchItem, metricsResult, taskUsage, prsCreatedCount, model);
|
|
1292
1221
|
|
|
@@ -1318,7 +1247,7 @@ function syncPrdFromPrs(config) {
|
|
|
1318
1247
|
safeWrite(wiPath, items);
|
|
1319
1248
|
// Sync done status to PRD JSON for each newly reconciled item
|
|
1320
1249
|
for (const wi of items) {
|
|
1321
|
-
if (wi.status === WI_STATUS.DONE) syncPrdItemStatus(wi.id,
|
|
1250
|
+
if (wi.status === WI_STATUS.DONE) syncPrdItemStatus(wi.id, WI_STATUS.DONE, wi.sourcePlan);
|
|
1322
1251
|
}
|
|
1323
1252
|
totalReconciled += reconciled;
|
|
1324
1253
|
}
|
|
@@ -1334,7 +1263,6 @@ function syncPrdFromPrs(config) {
|
|
|
1334
1263
|
|
|
1335
1264
|
module.exports = {
|
|
1336
1265
|
checkPlanCompletion,
|
|
1337
|
-
archivePlan,
|
|
1338
1266
|
updateWorkItemStatus,
|
|
1339
1267
|
syncPrdItemStatus,
|
|
1340
1268
|
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/engine.js
CHANGED
|
@@ -25,7 +25,7 @@ const fs = require('fs');
|
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const shared = require('./engine/shared');
|
|
27
27
|
const { exec, execSilent, runFile, ENGINE_DEFAULTS: DEFAULTS,
|
|
28
|
-
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, DISPATCH_RESULT } = shared;
|
|
28
|
+
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
29
29
|
const queries = require('./engine/queries');
|
|
30
30
|
|
|
31
31
|
// ─── Paths ──────────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.329",
|
|
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"
|