@yemi33/minions 0.1.328 → 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 +5 -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 +69 -58
- 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
|
|
@@ -156,7 +160,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
156
160
|
id, title: `Create PR for plan: ${plan.plan_summary || planFile}`,
|
|
157
161
|
type: 'implement', priority: 'high',
|
|
158
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}`,
|
|
159
|
-
status:
|
|
163
|
+
status: WI_STATUS.PENDING, created: ts(), createdBy: 'engine:plan-completion',
|
|
160
164
|
sourcePlan: planFile, itemType: 'pr',
|
|
161
165
|
branch: featureBranch, branchStrategy: 'shared-branch', project: projectName,
|
|
162
166
|
});
|
|
@@ -177,7 +181,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
177
181
|
const prs = (safeJson(shared.projectPrPath(p)) || [])
|
|
178
182
|
.filter(pr => {
|
|
179
183
|
const linkedId = prLinks[pr.id];
|
|
180
|
-
return pr.status ===
|
|
184
|
+
return pr.status === PR_STATUS.ACTIVE && linkedId && doneItems.find(w => w.id === linkedId);
|
|
181
185
|
});
|
|
182
186
|
if (prs.length > 0) {
|
|
183
187
|
projectPrs[p.name] = { project: p, prs, mainBranch: p.mainBranch || 'main' };
|
|
@@ -190,7 +194,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
190
194
|
const branches = prs.map(pr => pr.branch).filter(Boolean);
|
|
191
195
|
const lines = [
|
|
192
196
|
`# ${name} — merge ${branches.length} PR branch(es) into one worktree`,
|
|
193
|
-
`cd "${p.localPath}"`,
|
|
197
|
+
`cd "${p.localPath.replace(/\\/g, '/')}"`,
|
|
194
198
|
`git fetch origin ${branches.map(b => `"${b}"`).join(' ')} "${mainBranch}"`,
|
|
195
199
|
`git worktree add "${wtPath}" "origin/${mainBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${mainBranch}" && git pull origin "${mainBranch}")`,
|
|
196
200
|
`cd "${wtPath}"`,
|
|
@@ -247,7 +251,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
247
251
|
type: 'verify',
|
|
248
252
|
priority: 'high',
|
|
249
253
|
description,
|
|
250
|
-
status:
|
|
254
|
+
status: WI_STATUS.PENDING,
|
|
251
255
|
created: ts(),
|
|
252
256
|
createdBy: 'engine:plan-verification',
|
|
253
257
|
sourcePlan: planFile,
|
|
@@ -400,7 +404,7 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
400
404
|
type: 'plan-to-prd',
|
|
401
405
|
priority: meta?.item?.priority || 'high',
|
|
402
406
|
description: `Plan file: plans/${planFile.name}\nChained from plan task ${dispatchItem.id}`,
|
|
403
|
-
status:
|
|
407
|
+
status: WI_STATUS.PENDING,
|
|
404
408
|
created: ts(),
|
|
405
409
|
createdBy: 'engine:chain',
|
|
406
410
|
project: targetProject.name,
|
|
@@ -410,10 +414,15 @@ function chainPlanToPrd(dispatchItem, meta, config) {
|
|
|
410
414
|
}
|
|
411
415
|
|
|
412
416
|
// ─── Work Item Status ────────────────────────────────────────────────────────
|
|
417
|
+
const _VALID_WI_STATUSES = new Set(Object.values(WI_STATUS));
|
|
413
418
|
function updateWorkItemStatus(meta, status, reason) {
|
|
414
419
|
|
|
415
420
|
const itemId = meta.item?.id;
|
|
416
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
|
+
}
|
|
417
426
|
|
|
418
427
|
let wiPath;
|
|
419
428
|
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
@@ -435,20 +444,20 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
435
444
|
target.agentResults[agent] = { status, completedAt: ts(), reason: reason || undefined };
|
|
436
445
|
|
|
437
446
|
const results = Object.values(target.agentResults);
|
|
438
|
-
const anySuccess = results.some(r => r.status ===
|
|
447
|
+
const anySuccess = results.some(r => r.status === WI_STATUS.DONE);
|
|
439
448
|
const allDone = Array.isArray(target.fanOutAgents) && target.fanOutAgents.length > 0 ? results.length >= target.fanOutAgents.length : false;
|
|
440
449
|
const dispatchAge = target.dispatched_at ? Date.now() - new Date(target.dispatched_at).getTime() : 0;
|
|
441
450
|
const timedOut = !allDone && dispatchAge > 6 * 60 * 60 * 1000 && results.length > 0;
|
|
442
451
|
|
|
443
452
|
if (anySuccess) {
|
|
444
|
-
target.status =
|
|
453
|
+
target.status = WI_STATUS.DONE;
|
|
445
454
|
delete target.failReason;
|
|
446
455
|
delete target.failedAt;
|
|
447
456
|
target.completedAgents = Object.entries(target.agentResults)
|
|
448
|
-
.filter(([, r]) => r.status ===
|
|
457
|
+
.filter(([, r]) => r.status === WI_STATUS.DONE)
|
|
449
458
|
.map(([a]) => a);
|
|
450
459
|
} else if (allDone || timedOut) {
|
|
451
|
-
target.status =
|
|
460
|
+
target.status = WI_STATUS.FAILED;
|
|
452
461
|
target.failReason = timedOut
|
|
453
462
|
? `Fan-out timed out: ${results.length}/${(target.fanOutAgents || []).length} agents reported (all failed)`
|
|
454
463
|
: 'All fan-out agents failed';
|
|
@@ -456,11 +465,11 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
456
465
|
}
|
|
457
466
|
} else {
|
|
458
467
|
target.status = status;
|
|
459
|
-
if (status ===
|
|
468
|
+
if (status === WI_STATUS.DONE) {
|
|
460
469
|
delete target.failReason;
|
|
461
470
|
delete target.failedAt;
|
|
462
471
|
target.completedAt = ts();
|
|
463
|
-
} else if (status ===
|
|
472
|
+
} else if (status === WI_STATUS.FAILED) {
|
|
464
473
|
if (reason) target.failReason = reason;
|
|
465
474
|
target.failedAt = ts();
|
|
466
475
|
}
|
|
@@ -474,8 +483,10 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
474
483
|
}
|
|
475
484
|
}
|
|
476
485
|
|
|
486
|
+
const _VALID_PRD_STATUSES = new Set([...Object.values(WI_STATUS), 'missing']);
|
|
477
487
|
function syncPrdItemStatus(itemId, status, sourcePlan) {
|
|
478
488
|
if (!itemId) return;
|
|
489
|
+
if (!_VALID_PRD_STATUSES.has(status)) return;
|
|
479
490
|
try {
|
|
480
491
|
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
481
492
|
const files = sourcePlan ? [sourcePlan] : require('fs').readdirSync(prdDir).filter(f => f.endsWith('.json'));
|
|
@@ -594,7 +605,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
594
605
|
agent: agentName,
|
|
595
606
|
branch: meta?.branch || '',
|
|
596
607
|
reviewStatus: 'pending',
|
|
597
|
-
status:
|
|
608
|
+
status: PR_STATUS.ACTIVE,
|
|
598
609
|
created: dateStamp(),
|
|
599
610
|
url: extractPrUrl(prId),
|
|
600
611
|
prdItems: meta?.item?.id ? [meta.item.id] : [],
|
|
@@ -703,7 +714,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
703
714
|
} catch (err) { log('warn', `Post-merge worktree cleanup: ${err.message}`); }
|
|
704
715
|
}
|
|
705
716
|
|
|
706
|
-
if (newStatus !==
|
|
717
|
+
if (newStatus !== PR_STATUS.MERGED) return;
|
|
707
718
|
|
|
708
719
|
// Resolve linked work item from pr-links or PR branch name
|
|
709
720
|
let mergedItemId = getPrLinks()[pr.id];
|
|
@@ -722,13 +733,13 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
722
733
|
const plan = safeJson(path.join(prdDir, pf));
|
|
723
734
|
if (!plan?.missing_features) continue;
|
|
724
735
|
const feature = plan.missing_features.find(f => f.id === mergedItemId);
|
|
725
|
-
if (feature && feature.status !==
|
|
726
|
-
feature.status =
|
|
736
|
+
if (feature && feature.status !== WI_STATUS.DONE) {
|
|
737
|
+
feature.status = WI_STATUS.DONE;
|
|
727
738
|
shared.safeWrite(path.join(prdDir, pf), plan);
|
|
728
739
|
updated++;
|
|
729
740
|
}
|
|
730
741
|
}
|
|
731
|
-
if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as
|
|
742
|
+
if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as done for ${pr.id}`);
|
|
732
743
|
} catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
|
|
733
744
|
|
|
734
745
|
// Mark work item as done
|
|
@@ -739,10 +750,10 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
739
750
|
const items = safeJson(wiPath);
|
|
740
751
|
if (!items) continue;
|
|
741
752
|
const item = items.find(i => i.id === mergedItemId);
|
|
742
|
-
if (item && item.status !==
|
|
753
|
+
if (item && item.status !== WI_STATUS.DONE) {
|
|
743
754
|
log('info', `Post-merge: marking work item ${mergedItemId} as done (was ${item.status}) for ${pr.id}`);
|
|
744
|
-
item.status =
|
|
745
|
-
item.completedAt =
|
|
755
|
+
item.status = WI_STATUS.DONE;
|
|
756
|
+
item.completedAt = ts();
|
|
746
757
|
item._mergedVia = pr.id;
|
|
747
758
|
shared.safeWrite(wiPath, items);
|
|
748
759
|
break;
|
|
@@ -827,19 +838,19 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
827
838
|
if (proj) {
|
|
828
839
|
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
829
840
|
const items = safeJson(centralPath) || [];
|
|
830
|
-
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);
|
|
831
842
|
if (!alreadyExists) {
|
|
832
843
|
const skillId = `SK${String(items.filter(i => i.id?.startsWith('SK')).length + 1).padStart(3, '0')}`;
|
|
833
844
|
items.push({ id: skillId, type: 'implement', title: `Add skill: ${name}`,
|
|
834
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\`\`\``,
|
|
835
|
-
priority: 'low', status:
|
|
846
|
+
priority: 'low', status: WI_STATUS.QUEUED, created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
|
|
836
847
|
shared.safeWrite(centralPath, items);
|
|
837
848
|
log('info', `Queued work item ${skillId} to PR project skill "${name}" into ${project}`);
|
|
838
849
|
}
|
|
839
850
|
}
|
|
840
851
|
} else {
|
|
841
852
|
// Write in Claude Code native format: ~/.claude/skills/<name>/SKILL.md
|
|
842
|
-
const claudeSkillsDir = path.join(
|
|
853
|
+
const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
843
854
|
const skillDir = path.join(claudeSkillsDir, name.replace(/[^a-z0-9-]/g, '-'));
|
|
844
855
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
845
856
|
if (!fs.existsSync(skillPath)) {
|
|
@@ -918,10 +929,10 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
918
929
|
m.lastTask = dispatchItem.task;
|
|
919
930
|
m.lastCompleted = ts();
|
|
920
931
|
if (model) m.model = model;
|
|
921
|
-
if (result ===
|
|
932
|
+
if (result === DISPATCH_RESULT.SUCCESS) {
|
|
922
933
|
m.tasksCompleted++;
|
|
923
934
|
if (prsCreatedCount > 0) m.prsCreated = (m.prsCreated || 0) + prsCreatedCount;
|
|
924
|
-
if (dispatchItem.type ===
|
|
935
|
+
if (dispatchItem.type === WORK_TYPE.REVIEW) m.reviewsDone++;
|
|
925
936
|
} else if (result === 'retry') {
|
|
926
937
|
// Auto-retry: count cost but not as a final outcome
|
|
927
938
|
m.tasksRetried = (m.tasksRetried || 0) + 1;
|
|
@@ -1003,7 +1014,7 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
1003
1014
|
if (!parent) continue;
|
|
1004
1015
|
|
|
1005
1016
|
// Mark parent as decomposed
|
|
1006
|
-
parent.status =
|
|
1017
|
+
parent.status = WI_STATUS.DECOMPOSED;
|
|
1007
1018
|
parent._decomposed = true;
|
|
1008
1019
|
delete parent._decomposing;
|
|
1009
1020
|
parent._subItemIds = subItems.map(s => s.id);
|
|
@@ -1017,7 +1028,7 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
1017
1028
|
type: (sub.estimated_complexity === 'large') ? 'implement:large' : 'implement',
|
|
1018
1029
|
priority: sub.priority || parent.priority || 'medium',
|
|
1019
1030
|
description: sub.description || '',
|
|
1020
|
-
status:
|
|
1031
|
+
status: WI_STATUS.PENDING,
|
|
1021
1032
|
complexity: sub.estimated_complexity || 'medium',
|
|
1022
1033
|
depends_on: sub.depends_on || [],
|
|
1023
1034
|
parent_id: parentId,
|
|
@@ -1042,7 +1053,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1042
1053
|
const type = dispatchItem.type;
|
|
1043
1054
|
const meta = dispatchItem.meta;
|
|
1044
1055
|
const isSuccess = code === 0;
|
|
1045
|
-
const result = isSuccess ?
|
|
1056
|
+
const result = isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR;
|
|
1046
1057
|
const { resultSummary, taskUsage, sessionId, model } = parseAgentOutput(stdout);
|
|
1047
1058
|
|
|
1048
1059
|
// Save session for potential resume on next dispatch
|
|
@@ -1057,13 +1068,13 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1057
1068
|
|
|
1058
1069
|
// Handle decomposition results — create sub-items from decompose agent output
|
|
1059
1070
|
let skipDoneStatus = false;
|
|
1060
|
-
if (type ===
|
|
1071
|
+
if (type === WORK_TYPE.DECOMPOSE && isSuccess && meta?.item?.id) {
|
|
1061
1072
|
const subCount = handleDecompositionResult(stdout, meta, config);
|
|
1062
1073
|
if (subCount > 0) skipDoneStatus = true; // parent already marked 'decomposed' by handler
|
|
1063
1074
|
// If decomposition produced nothing, fall through to mark parent as done
|
|
1064
1075
|
}
|
|
1065
1076
|
|
|
1066
|
-
if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta,
|
|
1077
|
+
if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, WI_STATUS.DONE, '');
|
|
1067
1078
|
if (!isSuccess && meta?.item?.id) {
|
|
1068
1079
|
// Auto-retry: read fresh _retryCount from file (not stale dispatch-time snapshot)
|
|
1069
1080
|
let retries = (meta.item._retryCount || 0);
|
|
@@ -1078,9 +1089,9 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1078
1089
|
}
|
|
1079
1090
|
} catch { /* optional */ }
|
|
1080
1091
|
|
|
1081
|
-
if (retries <
|
|
1082
|
-
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}
|
|
1083
|
-
updateWorkItemStatus(meta,
|
|
1092
|
+
if (retries < ENGINE_DEFAULTS.maxRetries) {
|
|
1093
|
+
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries}`);
|
|
1094
|
+
updateWorkItemStatus(meta, WI_STATUS.PENDING, '');
|
|
1084
1095
|
try {
|
|
1085
1096
|
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1086
1097
|
? path.join(MINIONS_DIR, 'work-items.json')
|
|
@@ -1089,17 +1100,17 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1089
1100
|
const items = safeJson(wiPath) || [];
|
|
1090
1101
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1091
1102
|
if (wi) {
|
|
1092
|
-
wi._retryCount = retries + 1; wi.status =
|
|
1093
|
-
if (type ===
|
|
1103
|
+
wi._retryCount = retries + 1; wi.status = WI_STATUS.PENDING; delete wi.dispatched_at; delete wi.dispatched_to;
|
|
1104
|
+
if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing; // clear so item can retry decomposition
|
|
1094
1105
|
shared.safeWrite(wiPath, items);
|
|
1095
1106
|
}
|
|
1096
1107
|
}
|
|
1097
1108
|
} catch (err) { log('warn', `Retry update: ${err.message}`); }
|
|
1098
1109
|
} else {
|
|
1099
|
-
updateWorkItemStatus(meta,
|
|
1110
|
+
updateWorkItemStatus(meta, WI_STATUS.FAILED, `Agent failed (${ENGINE_DEFAULTS.maxRetries} retries exhausted)`);
|
|
1100
1111
|
}
|
|
1101
1112
|
// Clear _decomposing flag on failure so item doesn't get permanently stuck
|
|
1102
|
-
if (type ===
|
|
1113
|
+
if (type === WORK_TYPE.DECOMPOSE) {
|
|
1103
1114
|
try {
|
|
1104
1115
|
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1105
1116
|
? path.join(MINIONS_DIR, 'work-items.json')
|
|
@@ -1113,7 +1124,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1113
1124
|
}
|
|
1114
1125
|
}
|
|
1115
1126
|
// Meeting post-completion: collect findings/debate/conclusion
|
|
1116
|
-
if (type ===
|
|
1127
|
+
if (type === WORK_TYPE.MEETING && meta?.meetingId) {
|
|
1117
1128
|
try {
|
|
1118
1129
|
const { collectMeetingFindings } = require('./meeting');
|
|
1119
1130
|
collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout);
|
|
@@ -1140,7 +1151,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1140
1151
|
return d.includes(branchSlug) && fs.statSync(path.join(worktreeRoot, d)).isDirectory();
|
|
1141
1152
|
});
|
|
1142
1153
|
// Only remove if no other active dispatch uses this branch
|
|
1143
|
-
const dispatch =
|
|
1154
|
+
const dispatch = getDispatch();
|
|
1144
1155
|
const otherActive = ((dispatch.active || []).concat(dispatch.pending || [])).some(d =>
|
|
1145
1156
|
d.id !== dispatchItem.id && d.meta?.branch && shared.sanitizeBranch && shared.sanitizeBranch(d.meta.branch) === branchSlug
|
|
1146
1157
|
);
|
|
@@ -1162,7 +1173,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1162
1173
|
}
|
|
1163
1174
|
|
|
1164
1175
|
// Detect implement tasks that completed without creating a PR
|
|
1165
|
-
if (isSuccess && (type ===
|
|
1176
|
+
if (isSuccess && (type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || type === WORK_TYPE.FIX) && prsCreatedCount === 0 && meta?.item?.id) {
|
|
1166
1177
|
// Check if a PR already exists linked to this work item (from a previous attempt)
|
|
1167
1178
|
const projects = shared.getProjects(config);
|
|
1168
1179
|
const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
|
@@ -1182,15 +1193,15 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1182
1193
|
wi.noPr = true;
|
|
1183
1194
|
wi.failReason = 'Completed without creating a pull request';
|
|
1184
1195
|
const retries = wi._retryCount || 0;
|
|
1185
|
-
if (retries <
|
|
1186
|
-
wi.status =
|
|
1196
|
+
if (retries < ENGINE_DEFAULTS.maxRetries) {
|
|
1197
|
+
wi.status = WI_STATUS.PENDING;
|
|
1187
1198
|
wi._retryCount = retries + 1;
|
|
1188
1199
|
delete wi.dispatched_at;
|
|
1189
1200
|
delete wi.dispatched_to;
|
|
1190
|
-
|
|
1201
|
+
log('info', `Auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries} for ${meta.item.id} (no PR created)`);
|
|
1191
1202
|
} else {
|
|
1192
|
-
wi.status =
|
|
1193
|
-
|
|
1203
|
+
wi.status = WI_STATUS.FAILED;
|
|
1204
|
+
log('warn', `${meta.item.id} failed after ${ENGINE_DEFAULTS.maxRetries} retries — no PR created`);
|
|
1194
1205
|
}
|
|
1195
1206
|
shared.safeWrite(wiPath, items);
|
|
1196
1207
|
}
|
|
@@ -1198,13 +1209,13 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1198
1209
|
}
|
|
1199
1210
|
}
|
|
1200
1211
|
|
|
1201
|
-
if (type ===
|
|
1202
|
-
if (type ===
|
|
1212
|
+
if (type === WORK_TYPE.REVIEW) updatePrAfterReview(agentId, meta?.pr, meta?.project);
|
|
1213
|
+
if (type === WORK_TYPE.FIX) updatePrAfterFix(meta?.pr, meta?.project, meta?.source);
|
|
1203
1214
|
checkForLearnings(agentId, config.agents[agentId], dispatchItem.task);
|
|
1204
1215
|
if (isSuccess) extractSkillsFromOutput(stdout, agentId, dispatchItem, config);
|
|
1205
1216
|
updateAgentHistory(agentId, dispatchItem, result);
|
|
1206
1217
|
// Don't count auto-retries as errors in metrics — only count final outcomes
|
|
1207
|
-
const isAutoRetry = !isSuccess && meta?.item?.id && (meta.item._retryCount || 0) <
|
|
1218
|
+
const isAutoRetry = !isSuccess && meta?.item?.id && (meta.item._retryCount || 0) < ENGINE_DEFAULTS.maxRetries;
|
|
1208
1219
|
const metricsResult = isAutoRetry ? 'retry' : result;
|
|
1209
1220
|
updateMetrics(agentId, dispatchItem, metricsResult, taskUsage, prsCreatedCount, model);
|
|
1210
1221
|
|
|
@@ -1229,14 +1240,14 @@ function syncPrdFromPrs(config) {
|
|
|
1229
1240
|
for (const project of allProjects) {
|
|
1230
1241
|
const wiPath = projectWorkItemsPath(project);
|
|
1231
1242
|
const items = safeJson(wiPath) || [];
|
|
1232
|
-
const hasPending = items.some(wi => wi.status ===
|
|
1243
|
+
const hasPending = items.some(wi => wi.status === WI_STATUS.PENDING && !wi._pr);
|
|
1233
1244
|
if (!hasPending) continue;
|
|
1234
1245
|
const reconciled = reconcileItemsWithPrs(items, allPrs);
|
|
1235
1246
|
if (reconciled > 0) {
|
|
1236
1247
|
safeWrite(wiPath, items);
|
|
1237
1248
|
// Sync done status to PRD JSON for each newly reconciled item
|
|
1238
1249
|
for (const wi of items) {
|
|
1239
|
-
if (wi.status ===
|
|
1250
|
+
if (wi.status === WI_STATUS.DONE) syncPrdItemStatus(wi.id, WI_STATUS.DONE, wi.sourcePlan);
|
|
1240
1251
|
}
|
|
1241
1252
|
totalReconciled += reconciled;
|
|
1242
1253
|
}
|
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.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"
|