@yemi33/minions 0.1.153 → 0.1.155
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 +8 -1
- package/dashboard/js/command-center.js +3 -5
- package/dashboard/js/refresh.js +44 -0
- package/dashboard/js/render-pipelines.js +138 -6
- package/dashboard/js/render-work-items.js +54 -8
- package/dashboard/styles.css +16 -0
- package/dashboard.js +5 -5
- package/engine/lifecycle.js +60 -47
- package/engine.js +22 -6
- package/package.json +1 -1
- package/playbooks/implement.md +1 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.155 (2026-04-02)
|
|
4
4
|
|
|
5
5
|
### Engine
|
|
6
6
|
- engine.js
|
|
@@ -11,14 +11,21 @@
|
|
|
11
11
|
### Dashboard
|
|
12
12
|
- dashboard.html
|
|
13
13
|
- dashboard.js
|
|
14
|
+
- dashboard/js/command-center.js
|
|
14
15
|
- dashboard/js/modal-qa.js
|
|
15
16
|
- dashboard/js/modal.js
|
|
17
|
+
- dashboard/js/refresh.js
|
|
16
18
|
- dashboard/js/render-inbox.js
|
|
17
19
|
- dashboard/js/render-kb.js
|
|
20
|
+
- dashboard/js/render-pipelines.js
|
|
18
21
|
- dashboard/js/render-plans.js
|
|
19
22
|
- dashboard/js/render-prd.js
|
|
23
|
+
- dashboard/js/render-work-items.js
|
|
20
24
|
- dashboard/styles.css
|
|
21
25
|
|
|
26
|
+
### Playbooks
|
|
27
|
+
- implement.md
|
|
28
|
+
|
|
22
29
|
### Documentation
|
|
23
30
|
- deprecated.json
|
|
24
31
|
|
|
@@ -88,7 +88,7 @@ function ccAddMessage(role, html, skipSave) {
|
|
|
88
88
|
const isUser = role === 'user';
|
|
89
89
|
const div = document.createElement('div');
|
|
90
90
|
const isAssistant = !isUser;
|
|
91
|
-
div.className = isAssistant ? 'cc-msg-assistant' : '';
|
|
91
|
+
div.className = isAssistant ? 'cc-msg-assistant md-content' : '';
|
|
92
92
|
div.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:12px;line-height:1.6;max-width:95%;' +
|
|
93
93
|
(isUser ? 'background:var(--blue);color:#fff;align-self:flex-end' : 'background:var(--surface2);color:var(--text);align-self:flex-start;border:1px solid var(--border);position:relative');
|
|
94
94
|
div.innerHTML = (isAssistant && !html.includes('color:var(--red)') && !html.includes('cc-queued-pill') ? llmCopyBtn() : '') + html;
|
|
@@ -205,11 +205,9 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
205
205
|
ccUpdateSessionIndicator();
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
// Render markdown
|
|
208
|
+
// Render markdown response
|
|
209
209
|
const ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
|
|
210
|
-
const rendered = (data.text || '')
|
|
211
|
-
.replace(/`([^`]+)`/g, '<code style="background:var(--surface);padding:1px 4px;border-radius:3px;font-size:11px">$1</code>')
|
|
212
|
-
.replace(/\n/g, '<br>');
|
|
210
|
+
const rendered = renderMd(data.text || '');
|
|
213
211
|
ccAddMessage('assistant', rendered + '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">' + ccElapsed + 's</div>');
|
|
214
212
|
|
|
215
213
|
// Execute actions
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// Sidebar activity indicators — detect changes between refreshes
|
|
4
4
|
let _prevCounts = {};
|
|
5
|
+
let _prevEngineAlert = false;
|
|
5
6
|
function _detectPageChanges(data) {
|
|
6
7
|
const counts = {
|
|
7
8
|
completions: (data.dispatch?.completed || []).length,
|
|
@@ -22,9 +23,52 @@ function _detectPageChanges(data) {
|
|
|
22
23
|
if (counts.meetingRounds > _prevCounts.meetingRounds) changes.meetings = true;
|
|
23
24
|
}
|
|
24
25
|
_prevCounts = counts;
|
|
26
|
+
|
|
27
|
+
// Engine page — only badge for genuine problems, not routine activity
|
|
28
|
+
const engineAlert = _isEngineAlertWorthy(data);
|
|
29
|
+
if (engineAlert && !_prevEngineAlert) changes.engine = true;
|
|
30
|
+
// Clear the engine badge when alert condition resolves
|
|
31
|
+
if (!engineAlert && _prevEngineAlert) {
|
|
32
|
+
const engineLink = document.querySelector('.sidebar-link[data-page="engine"]');
|
|
33
|
+
if (engineLink) clearNotifBadge(engineLink);
|
|
34
|
+
}
|
|
35
|
+
_prevEngineAlert = engineAlert;
|
|
36
|
+
|
|
25
37
|
return changes;
|
|
26
38
|
}
|
|
27
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Determine if the engine state warrants a notification dot.
|
|
42
|
+
* Returns true only for genuine problems:
|
|
43
|
+
* - Engine stopped, stale, or in error state
|
|
44
|
+
* - 3+ failed work items in the last hour
|
|
45
|
+
* - Agent timeout/crash detected (error results in recent completions)
|
|
46
|
+
*/
|
|
47
|
+
function _isEngineAlertWorthy(data) {
|
|
48
|
+
// 1. Engine not running (stopped, stale, or error)
|
|
49
|
+
const engineState = data.engine?.state || 'stopped';
|
|
50
|
+
if (engineState === 'stopped' || engineState === 'error') return true;
|
|
51
|
+
// Stale heartbeat (>2 min old while claiming running)
|
|
52
|
+
if (engineState === 'running' && data.engine?.heartbeat) {
|
|
53
|
+
if (Date.now() - data.engine.heartbeat > 120000) return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. 3+ failed work items in the last hour
|
|
57
|
+
const oneHourAgo = Date.now() - 3600000;
|
|
58
|
+
const recentFailures = (data.workItems || []).filter(w =>
|
|
59
|
+
w.status === 'failed' && w.updated_at && new Date(w.updated_at).getTime() > oneHourAgo
|
|
60
|
+
);
|
|
61
|
+
if (recentFailures.length >= 3) return true;
|
|
62
|
+
|
|
63
|
+
// 3. Agent timeout/crash — 3+ error results in recent completed dispatches
|
|
64
|
+
const recentErrors = (data.dispatch?.completed || []).filter(d =>
|
|
65
|
+
d.result === 'error' && d.completed_at && new Date(d.completed_at).getTime() > oneHourAgo
|
|
66
|
+
);
|
|
67
|
+
if (recentErrors.length >= 3) return true;
|
|
68
|
+
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
28
72
|
function _processStatusUpdate(data) {
|
|
29
73
|
// Detect fresh install — clear stale browser state if install ID changed
|
|
30
74
|
if (data.installId) {
|
|
@@ -2,6 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
let _pipelinesData = [];
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Render clickable artifact links for a pipeline stage.
|
|
7
|
+
* Each artifact type gets an icon and navigates to the relevant detail view.
|
|
8
|
+
*/
|
|
9
|
+
function _renderArtifactLinks(artifacts) {
|
|
10
|
+
if (!artifacts) return '';
|
|
11
|
+
var links = [];
|
|
12
|
+
var linkStyle = 'display:inline-flex;align-items:center;gap:2px;padding:1px 6px;border-radius:10px;font-size:10px;cursor:pointer;text-decoration:none;color:var(--blue);background:color-mix(in srgb, var(--blue) 10%, transparent);border:1px solid color-mix(in srgb, var(--blue) 20%, transparent)';
|
|
13
|
+
|
|
14
|
+
// Work items → navigate to work page & open detail
|
|
15
|
+
(artifacts.workItems || []).forEach(function(id) {
|
|
16
|
+
links.push('<span style="' + linkStyle + '" onclick="event.stopPropagation();closeModal();switchPage(\'work\');setTimeout(function(){openWorkItemDetail(\'' + escHtml(id) + '\')},200)" title="Open work item ' + escHtml(id) + '">⚙ ' + escHtml(id) + '</span>');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Meetings → navigate to meetings page & open detail
|
|
20
|
+
(artifacts.meetings || []).forEach(function(id) {
|
|
21
|
+
links.push('<span style="' + linkStyle + '" onclick="event.stopPropagation();closeModal();switchPage(\'meetings\');setTimeout(function(){openMeetingDetail(\'' + escHtml(id) + '\')},200)" title="Open meeting ' + escHtml(id) + '">💬 ' + escHtml(id) + '</span>');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Plans → navigate to plans page
|
|
25
|
+
(artifacts.plans || []).forEach(function(name) {
|
|
26
|
+
links.push('<span style="' + linkStyle + '" onclick="event.stopPropagation();closeModal();switchPage(\'plans\')" title="Plan: ' + escHtml(name) + '">📋 ' + escHtml(name.replace(/\.md$/, '').slice(0, 30)) + '</span>');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// PRDs → navigate to PRD page
|
|
30
|
+
(artifacts.prds || []).forEach(function(name) {
|
|
31
|
+
links.push('<span style="' + linkStyle + '" onclick="event.stopPropagation();closeModal();switchPage(\'prd\')" title="PRD: ' + escHtml(name) + '">📄 ' + escHtml(name.replace(/\.json$/, '').slice(0, 30)) + '</span>');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// PRs → navigate to PRs page
|
|
35
|
+
(artifacts.prs || []).forEach(function(id) {
|
|
36
|
+
links.push('<span style="' + linkStyle + '" onclick="event.stopPropagation();closeModal();switchPage(\'prs\')" title="Pull request ' + escHtml(id) + '">🔀 PR-' + escHtml(id) + '</span>');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Sub-stages (parallel) — just label them, no nav needed
|
|
40
|
+
(artifacts.subStages || []).forEach(function(id) {
|
|
41
|
+
links.push('<span style="' + linkStyle + ';cursor:default;color:var(--muted);background:color-mix(in srgb, var(--muted) 8%, transparent);border-color:color-mix(in srgb, var(--muted) 15%, transparent)" title="Sub-stage ' + escHtml(id) + '">⚓ ' + escHtml(id) + '</span>');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (links.length === 0) return '';
|
|
45
|
+
return '<div style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px">' + links.join('') + '</div>';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Collect and deduplicate artifacts from all stages in a run.
|
|
50
|
+
* Returns { merged: { workItems, meetings, plans, prds, prs, subStages }, total }.
|
|
51
|
+
*/
|
|
52
|
+
function _collectRunArtifacts(run) {
|
|
53
|
+
var merged = { workItems: [], meetings: [], plans: [], prds: [], prs: [], subStages: [] };
|
|
54
|
+
var stages = run.stages || {};
|
|
55
|
+
for (var stageId in stages) {
|
|
56
|
+
var a = stages[stageId].artifacts || {};
|
|
57
|
+
['workItems', 'meetings', 'plans', 'prds', 'prs', 'subStages'].forEach(function(key) {
|
|
58
|
+
(a[key] || []).forEach(function(v) {
|
|
59
|
+
if (merged[key].indexOf(v) === -1) merged[key].push(v);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
var total = merged.workItems.length + merged.meetings.length + merged.plans.length + merged.prds.length + merged.prs.length;
|
|
64
|
+
return { merged: merged, total: total };
|
|
65
|
+
}
|
|
66
|
+
|
|
5
67
|
function renderPipelines(pipelines) {
|
|
6
68
|
_pipelinesData = pipelines || [];
|
|
7
69
|
const el = document.getElementById('pipelines-content');
|
|
@@ -29,6 +91,45 @@ function renderPipelines(pipelines) {
|
|
|
29
91
|
return '<span style="color:' + color + ';font-size:11px" title="' + escHtml(s.id) + ': ' + escHtml(s.title || s.type) + ' (' + stageStatus + ')">' + icon + ' ' + escHtml(s.id) + '</span>';
|
|
30
92
|
}).join(' <span style="color:var(--border)">\u2192</span> ');
|
|
31
93
|
|
|
94
|
+
// Build step-progress indicator for pipelines with a run
|
|
95
|
+
var progressHtml = '';
|
|
96
|
+
var displayRun = activeRun || lastRun;
|
|
97
|
+
if (displayRun && (p.stages || []).length > 0) {
|
|
98
|
+
var totalStages = (p.stages || []).length;
|
|
99
|
+
var completedCount = 0;
|
|
100
|
+
var runningCount = 0;
|
|
101
|
+
var failedCount = 0;
|
|
102
|
+
(p.stages || []).forEach(function(s) {
|
|
103
|
+
var st = displayRun.stages?.[s.id]?.status;
|
|
104
|
+
if (st === 'completed') completedCount++;
|
|
105
|
+
else if (st === 'running') runningCount++;
|
|
106
|
+
else if (st === 'failed') failedCount++;
|
|
107
|
+
});
|
|
108
|
+
var pct = Math.round((completedCount / totalStages) * 100);
|
|
109
|
+
|
|
110
|
+
// Segmented progress bar — one segment per stage
|
|
111
|
+
var segments = (p.stages || []).map(function(s) {
|
|
112
|
+
var st = displayRun.stages?.[s.id]?.status || 'pending';
|
|
113
|
+
var cls = st === 'completed' ? 'complete' : st === 'running' ? 'running' : st === 'failed' ? 'failed' : st === 'waiting-human' ? 'waiting' : 'pending';
|
|
114
|
+
return '<div class="pl-prog-seg ' + cls + '" style="width:' + (100 / totalStages) + '%" title="' + escHtml(s.id) + ': ' + st + '"></div>';
|
|
115
|
+
}).join('');
|
|
116
|
+
|
|
117
|
+
var statusParts = [];
|
|
118
|
+
if (completedCount) statusParts.push(completedCount + ' done');
|
|
119
|
+
if (runningCount) statusParts.push(runningCount + ' running');
|
|
120
|
+
if (failedCount) statusParts.push(failedCount + ' failed');
|
|
121
|
+
var remaining = totalStages - completedCount - runningCount - failedCount;
|
|
122
|
+
if (remaining > 0) statusParts.push(remaining + ' pending');
|
|
123
|
+
|
|
124
|
+
progressHtml = '<div class="pl-progress-wrap">' +
|
|
125
|
+
'<div class="pl-progress-bar">' + segments + '</div>' +
|
|
126
|
+
'<div class="pl-progress-label">' +
|
|
127
|
+
'<span style="font-weight:600;color:' + (pct === 100 ? 'var(--green)' : failedCount ? 'var(--red)' : 'var(--blue)') + '">' + pct + '%</span>' +
|
|
128
|
+
'<span style="color:var(--muted)">' + statusParts.join(' \u00b7 ') + '</span>' +
|
|
129
|
+
'</div>' +
|
|
130
|
+
'</div>';
|
|
131
|
+
}
|
|
132
|
+
|
|
32
133
|
return '<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;cursor:pointer" onclick="openPipelineDetail(\'' + escHtml(p.id) + '\')">' +
|
|
33
134
|
'<div style="display:flex;justify-content:space-between;align-items:center">' +
|
|
34
135
|
'<strong style="font-size:13px">' + escHtml(p.title) + '</strong>' +
|
|
@@ -39,6 +140,7 @@ function renderPipelines(pipelines) {
|
|
|
39
140
|
'</div>' +
|
|
40
141
|
'</div>' +
|
|
41
142
|
'<div style="margin-top:6px;display:flex;gap:4px;align-items:center;flex-wrap:wrap">' + stageFlow + '</div>' +
|
|
143
|
+
progressHtml +
|
|
42
144
|
'</div>';
|
|
43
145
|
}).join('');
|
|
44
146
|
}
|
|
@@ -61,7 +163,28 @@ function openPipelineDetail(id) {
|
|
|
61
163
|
'</div>' +
|
|
62
164
|
'</div>';
|
|
63
165
|
|
|
64
|
-
// Stage detail
|
|
166
|
+
// Stage detail with progress bar
|
|
167
|
+
var detailRun = activeRun || (p.runs || []).slice(-1)[0];
|
|
168
|
+
if (detailRun && (p.stages || []).length > 0) {
|
|
169
|
+
var dtotal = (p.stages || []).length;
|
|
170
|
+
var ddone = 0, drun = 0, dfail = 0;
|
|
171
|
+
(p.stages || []).forEach(function(s) {
|
|
172
|
+
var st = detailRun.stages?.[s.id]?.status;
|
|
173
|
+
if (st === 'completed') ddone++;
|
|
174
|
+
else if (st === 'running') drun++;
|
|
175
|
+
else if (st === 'failed') dfail++;
|
|
176
|
+
});
|
|
177
|
+
var dpct = Math.round((ddone / dtotal) * 100);
|
|
178
|
+
var dsegs = (p.stages || []).map(function(s) {
|
|
179
|
+
var st = detailRun.stages?.[s.id]?.status || 'pending';
|
|
180
|
+
var cls = st === 'completed' ? 'complete' : st === 'running' ? 'running' : st === 'failed' ? 'failed' : st === 'waiting-human' ? 'waiting' : 'pending';
|
|
181
|
+
return '<div class="pl-prog-seg ' + cls + '" style="width:' + (100 / dtotal) + '%" title="' + escHtml(s.id) + ': ' + st + '"></div>';
|
|
182
|
+
}).join('');
|
|
183
|
+
html += '<div class="pl-progress-wrap">' +
|
|
184
|
+
'<div class="pl-progress-bar" style="height:8px">' + dsegs + '</div>' +
|
|
185
|
+
'<div class="pl-progress-label"><span style="font-weight:600;color:' + (dpct === 100 ? 'var(--green)' : dfail ? 'var(--red)' : 'var(--blue)') + '">' + dpct + '% complete</span> <span style="color:var(--muted)">(' + ddone + '/' + dtotal + ' stages)</span></div>' +
|
|
186
|
+
'</div>';
|
|
187
|
+
}
|
|
65
188
|
html += '<h4 style="font-size:12px;color:var(--blue);margin:0">Stages</h4>';
|
|
66
189
|
(p.stages || []).forEach(function(s, i) {
|
|
67
190
|
var stageRun = activeRun?.stages?.[s.id] || {};
|
|
@@ -75,6 +198,7 @@ function openPipelineDetail(id) {
|
|
|
75
198
|
'<span style="color:' + statusColor + ';font-size:10px;font-weight:600">' + stageStatus.toUpperCase() + '</span>' +
|
|
76
199
|
'</div>' +
|
|
77
200
|
'<div style="font-size:10px;color:var(--muted);margin-top:4px">Type: ' + escHtml(s.type) + ' | Depends on: ' + escHtml(deps) + (s.agent ? ' | Agent: ' + escHtml(s.agent) : '') + '</div>' +
|
|
201
|
+
_renderArtifactLinks(stageRun.artifacts) +
|
|
78
202
|
(stageRun.output ? '<div style="margin-top:6px;font-size:11px;max-height:150px;overflow-y:auto">' + renderMd(stageRun.output.slice(0, 500)) + '</div>' : '') +
|
|
79
203
|
(stageStatus === 'waiting-human' ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);border-color:var(--green);margin-top:6px" onclick="_continuePipeline(\'' + escHtml(id) + '\',\'' + escHtml(s.id) + '\',this)">Continue</button>' : '') +
|
|
80
204
|
'</div>';
|
|
@@ -84,12 +208,20 @@ function openPipelineDetail(id) {
|
|
|
84
208
|
var runs = (p.runs || []).slice(-5).reverse();
|
|
85
209
|
if (runs.length > 0) {
|
|
86
210
|
html += '<h4 style="font-size:12px;color:var(--blue);margin:0">Recent Runs</h4>';
|
|
87
|
-
runs.forEach(function(r) {
|
|
211
|
+
runs.forEach(function(r, ri) {
|
|
88
212
|
var color = r.status === 'completed' ? 'var(--green)' : r.status === 'failed' ? 'var(--red)' : r.status === 'running' ? 'var(--blue)' : 'var(--muted)';
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
213
|
+
// Collect all artifacts across stages for this run
|
|
214
|
+
var runArtifacts = _collectRunArtifacts(r);
|
|
215
|
+
var artifactCount = runArtifacts.total;
|
|
216
|
+
var toggleId = 'run-artifacts-' + ri;
|
|
217
|
+
html += '<div style="font-size:10px">' +
|
|
218
|
+
'<div style="display:flex;gap:8px;align-items:center">' +
|
|
219
|
+
'<span style="color:' + color + ';font-weight:600">' + r.status + '</span>' +
|
|
220
|
+
'<span style="color:var(--muted)">' + (r.startedAt ? new Date(r.startedAt).toLocaleString() : '') + '</span>' +
|
|
221
|
+
(r.completedAt ? '<span style="color:var(--muted)">\u2192 ' + new Date(r.completedAt).toLocaleString() + '</span>' : '') +
|
|
222
|
+
(artifactCount > 0 ? '<span style="color:var(--blue);cursor:pointer;user-select:none" onclick="var el=document.getElementById(\'' + toggleId + '\');el.style.display=el.style.display===\'none\'?\'flex\':\'none\'" title="Toggle artifacts">' + artifactCount + ' artifact' + (artifactCount !== 1 ? 's' : '') + ' ▾</span>' : '') +
|
|
223
|
+
'</div>' +
|
|
224
|
+
(artifactCount > 0 ? '<div id="' + toggleId + '" style="display:none;flex-wrap:wrap;gap:4px;margin-top:4px;margin-left:12px">' + _renderArtifactLinks(runArtifacts.merged) + '</div>' : '') +
|
|
93
225
|
'</div>';
|
|
94
226
|
});
|
|
95
227
|
}
|
|
@@ -4,6 +4,30 @@ let allWorkItems = [];
|
|
|
4
4
|
let wiPage = 0;
|
|
5
5
|
const WI_PER_PAGE = 20;
|
|
6
6
|
|
|
7
|
+
// Track retry state per work item so loading/success/error survives re-renders
|
|
8
|
+
const _wiRetryState = {}; // { [id]: { status: 'pending'|'done'|'error', message?, until? } }
|
|
9
|
+
function setWiRetryState(id, state) { _wiRetryState[id] = state; }
|
|
10
|
+
function getWiRetryState(id) {
|
|
11
|
+
const s = _wiRetryState[id];
|
|
12
|
+
if (!s) return null;
|
|
13
|
+
if (s.until && Date.now() > s.until) { delete _wiRetryState[id]; return null; }
|
|
14
|
+
return s;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function wiRetryBtn(item) {
|
|
18
|
+
const rs = getWiRetryState(item.id);
|
|
19
|
+
if (rs && rs.status === 'pending') {
|
|
20
|
+
return '<span style="font-size:9px;padding:1px 6px;color:var(--yellow);border:1px solid rgba(210,153,34,0.35);background:rgba(210,153,34,0.1);border-radius:3px;cursor:wait;margin-left:4px">Retrying\u2026</span>';
|
|
21
|
+
}
|
|
22
|
+
if (rs && rs.status === 'done') {
|
|
23
|
+
return '<span style="font-size:9px;padding:1px 6px;color:var(--green);border:1px solid rgba(63,185,80,0.35);background:rgba(63,185,80,0.1);border-radius:3px;margin-left:4px">Requeued</span>';
|
|
24
|
+
}
|
|
25
|
+
if (rs && rs.status === 'error') {
|
|
26
|
+
return '<span style="font-size:9px;padding:1px 6px;color:var(--red);border:1px solid rgba(248,81,73,0.35);background:rgba(248,81,73,0.1);border-radius:3px;margin-left:4px;cursor:pointer" title="' + escHtml(rs.message || 'Retry failed') + ' — click to try again" onclick="event.stopPropagation();retryWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')">Retry failed</span>';
|
|
27
|
+
}
|
|
28
|
+
return '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="event.stopPropagation();retryWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')">Retry</button>';
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
function wiRow(item) {
|
|
8
32
|
const statusBadge = (s) => {
|
|
9
33
|
const cls = s === 'failed' ? 'rejected' : s === 'needs-human-review' ? 'needs-review' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : 'draft';
|
|
@@ -23,7 +47,7 @@ function wiRow(item) {
|
|
|
23
47
|
'<td>' + priBadge(item.priority) + '</td>' +
|
|
24
48
|
'<td>' + statusBadge(item.status || 'pending') +
|
|
25
49
|
(item._pendingReason ? ' <span style="font-size:9px;color:var(--muted);margin-left:4px" title="Pending reason: ' + escHtml(item._pendingReason) + '">' + escHtml(item._pendingReason.replace(/_/g, ' ')) + '</span>' : '') +
|
|
26
|
-
(item.status === 'failed' ? '
|
|
50
|
+
(item.status === 'failed' ? ' ' + wiRetryBtn(item) : '') +
|
|
27
51
|
'</td>' +
|
|
28
52
|
'<td>' +
|
|
29
53
|
(item.completedAgents && item.completedAgents.length > 0
|
|
@@ -92,7 +116,7 @@ function renderWorkItems(items) {
|
|
|
92
116
|
function editWorkItem(id, source) {
|
|
93
117
|
const item = allWorkItems.find(i => i.id === id);
|
|
94
118
|
if (!item) return;
|
|
95
|
-
const types = ['implement', 'fix', 'review', 'plan', 'verify', 'investigate', 'refactor', 'test', 'docs'];
|
|
119
|
+
const types = ['implement', 'fix', 'review', 'plan', 'verify', 'evaluate', 'decompose', 'meeting', 'investigate', 'refactor', 'test', 'explore', 'ask', 'docs'];
|
|
96
120
|
const priorities = ['critical', 'high', 'medium', 'low'];
|
|
97
121
|
const agentOpts = (cmdAgents || []).map(a => '<option value="' + escHtml(a.id) + '"' + (item.agent === a.id ? ' selected' : '') + '>' + escHtml(a.name) + '</option>').join('');
|
|
98
122
|
const typeOpts = types.map(t => '<option value="' + t + '"' + ((item.type || 'implement') === t ? ' selected' : '') + '>' + t + '</option>').join('');
|
|
@@ -211,23 +235,33 @@ async function toggleWorkItemArchive() {
|
|
|
211
235
|
} catch (e) { el.innerHTML = '<p class="empty">Failed to load archive.</p>'; }
|
|
212
236
|
}
|
|
213
237
|
|
|
214
|
-
async function retryWorkItem(id, source
|
|
215
|
-
|
|
238
|
+
async function retryWorkItem(id, source) {
|
|
239
|
+
// Prevent double-click: if already retrying, ignore
|
|
240
|
+
const existing = getWiRetryState(id);
|
|
241
|
+
if (existing && existing.status === 'pending') return;
|
|
242
|
+
|
|
243
|
+
setWiRetryState(id, { status: 'pending' });
|
|
244
|
+
renderWorkItems(allWorkItems);
|
|
216
245
|
try {
|
|
217
246
|
const res = await fetch('/api/work-items/retry', {
|
|
218
247
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
219
248
|
body: JSON.stringify({ id, source: source || undefined })
|
|
220
249
|
});
|
|
221
250
|
if (res.ok) {
|
|
251
|
+
setWiRetryState(id, { status: 'done', until: Date.now() + 8000 });
|
|
222
252
|
showToast('cmd-toast', 'Work item ' + id + ' reset to pending', true);
|
|
223
253
|
wakeEngine();
|
|
224
254
|
refresh();
|
|
225
255
|
} else {
|
|
226
|
-
if (btn) { btn.textContent = btn.dataset.origText || 'Retry'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
|
|
227
256
|
const d = await res.json().catch(() => ({}));
|
|
228
|
-
|
|
257
|
+
const msg = d.error || 'unknown';
|
|
258
|
+
setWiRetryState(id, { status: 'error', message: msg, until: Date.now() + 10000 });
|
|
259
|
+
renderWorkItems(allWorkItems);
|
|
229
260
|
}
|
|
230
|
-
} catch (e) {
|
|
261
|
+
} catch (e) {
|
|
262
|
+
setWiRetryState(id, { status: 'error', message: e.message, until: Date.now() + 10000 });
|
|
263
|
+
renderWorkItems(allWorkItems);
|
|
264
|
+
}
|
|
231
265
|
}
|
|
232
266
|
|
|
233
267
|
function wiPrev() { if (wiPage > 0) { wiPage--; renderWorkItems(allWorkItems); } }
|
|
@@ -282,7 +316,7 @@ async function submitFeedback(id, source) {
|
|
|
282
316
|
}
|
|
283
317
|
|
|
284
318
|
function openCreateWorkItemModal() {
|
|
285
|
-
const typeOpts = ['implement', 'fix', 'explore', 'test', 'review', 'ask', 'plan'].map(t =>
|
|
319
|
+
const typeOpts = ['implement', 'fix', 'explore', 'test', 'review', 'ask', 'plan', 'verify', 'evaluate', 'decompose', 'meeting'].map(t =>
|
|
286
320
|
'<option value="' + t + '"' + (t === 'implement' ? ' selected' : '') + '>' + t + '</option>'
|
|
287
321
|
).join('');
|
|
288
322
|
const priOpts = ['high', 'medium', 'low'].map(p =>
|
|
@@ -311,12 +345,22 @@ function openCreateWorkItemModal() {
|
|
|
311
345
|
'</div>' +
|
|
312
346
|
'<label style="color:var(--text);font-size:var(--text-md)">Acceptance Criteria <textarea id="wi-new-ac" rows="2" style="' + inputStyle + ';resize:vertical" placeholder="One criterion per line (optional)"></textarea></label>' +
|
|
313
347
|
'<label style="color:var(--text);font-size:var(--text-md)">References <textarea id="wi-new-refs" rows="2" style="' + inputStyle + ';resize:vertical" placeholder="url | title | type — one per line (optional)"></textarea></label>' +
|
|
348
|
+
'<label id="wi-new-skippr-row" style="color:var(--text);font-size:var(--text-md);display:flex;gap:8px;align-items:center;cursor:pointer"><input type="checkbox" id="wi-new-skippr"> Skip PR creation (push branch only)</label>' +
|
|
314
349
|
'<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
|
|
315
350
|
'<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
|
|
316
351
|
'<button onclick="_submitCreateWorkItem()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Create</button>' +
|
|
317
352
|
'</div>' +
|
|
318
353
|
'</div>';
|
|
319
354
|
document.getElementById('modal').classList.add('open');
|
|
355
|
+
// Show skipPr checkbox only for implement/fix types
|
|
356
|
+
const typeSelect = document.getElementById('wi-new-type');
|
|
357
|
+
const skipPrRow = document.getElementById('wi-new-skippr-row');
|
|
358
|
+
function _toggleSkipPr() {
|
|
359
|
+
const v = typeSelect?.value || '';
|
|
360
|
+
if (skipPrRow) skipPrRow.style.display = (v === 'implement' || v === 'fix') ? 'flex' : 'none';
|
|
361
|
+
}
|
|
362
|
+
_toggleSkipPr();
|
|
363
|
+
if (typeSelect) typeSelect.addEventListener('change', _toggleSkipPr);
|
|
320
364
|
setTimeout(() => document.getElementById('wi-new-title')?.focus(), 100);
|
|
321
365
|
}
|
|
322
366
|
|
|
@@ -342,6 +386,8 @@ async function _submitCreateWorkItem() {
|
|
|
342
386
|
if (project) body.project = project;
|
|
343
387
|
if (acceptanceCriteria.length) body.acceptanceCriteria = acceptanceCriteria;
|
|
344
388
|
if (references.length && references[0].url) body.references = references;
|
|
389
|
+
const skipPr = document.getElementById('wi-new-skippr')?.checked || false;
|
|
390
|
+
if (skipPr) body.skipPr = true;
|
|
345
391
|
|
|
346
392
|
try { closeModal(); } catch { /* expected */ }
|
|
347
393
|
showToast('cmd-toast', 'Creating work item...', true);
|
package/dashboard/styles.css
CHANGED
|
@@ -178,6 +178,18 @@
|
|
|
178
178
|
.prd-legend-dot.complete { background: var(--green); }
|
|
179
179
|
.prd-legend-dot.in-progress { background: var(--yellow); }
|
|
180
180
|
.prd-legend-dot.missing { background: var(--border); }
|
|
181
|
+
|
|
182
|
+
/* Pipeline Step-Progress */
|
|
183
|
+
.pl-progress-wrap { margin-top: 8px; }
|
|
184
|
+
.pl-progress-bar { width: 100%; height: 6px; background: var(--bg); border-radius: 3px; overflow: hidden; display: flex; border: 1px solid var(--border); }
|
|
185
|
+
.pl-prog-seg { height: 100%; transition: background 0.3s ease; }
|
|
186
|
+
.pl-prog-seg.complete { background: var(--green); }
|
|
187
|
+
.pl-prog-seg.running { background: var(--blue); animation: plSegPulse 1.5s ease-in-out infinite; }
|
|
188
|
+
.pl-prog-seg.failed { background: var(--red); }
|
|
189
|
+
.pl-prog-seg.waiting { background: var(--yellow); }
|
|
190
|
+
.pl-prog-seg.pending { background: transparent; }
|
|
191
|
+
@keyframes plSegPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
192
|
+
.pl-progress-label { display: flex; gap: 8px; align-items: center; margin-top: 4px; font-size: 10px; }
|
|
181
193
|
.prd-items-list { display: flex; flex-direction: column; gap: 3px; max-height: 400px; overflow-y: auto; padding: 0 8px; }
|
|
182
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); }
|
|
183
195
|
.prd-item-row.st-done { border-left-color: var(--green); }
|
|
@@ -505,6 +517,10 @@
|
|
|
505
517
|
.dispatch-type.plan { background: rgba(168,85,247,0.15); color: #a855f7; }
|
|
506
518
|
.dispatch-type.plan-to-prd { background: rgba(168,85,247,0.1); color: #a855f7; }
|
|
507
519
|
.dispatch-type.ask { background: rgba(63,185,80,0.15); color: var(--green); }
|
|
520
|
+
.dispatch-type.evaluate { background: rgba(248,81,73,0.15); color: var(--red); }
|
|
521
|
+
.dispatch-type.verify { background: rgba(63,185,80,0.2); color: var(--green); }
|
|
522
|
+
.dispatch-type.meeting { background: rgba(88,166,255,0.2); color: var(--blue); }
|
|
523
|
+
.dispatch-type.decompose { background: rgba(188,140,255,0.2); color: var(--purple); }
|
|
508
524
|
.dispatch-type.manual { background: rgba(139,148,158,0.15); color: var(--muted); }
|
|
509
525
|
.dispatch-agent { font-weight: 600; color: var(--text); }
|
|
510
526
|
.dispatch-task { flex: 1; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
package/dashboard.js
CHANGED
|
@@ -1038,6 +1038,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1038
1038
|
if (body.agents) item.agents = body.agents;
|
|
1039
1039
|
if (body.references) item.references = body.references;
|
|
1040
1040
|
if (body.acceptanceCriteria) item.acceptanceCriteria = body.acceptanceCriteria;
|
|
1041
|
+
if (body.skipPr === true) item.skipPr = true;
|
|
1041
1042
|
items.push(item);
|
|
1042
1043
|
safeWrite(wiPath, items);
|
|
1043
1044
|
return jsonReply(res, 200, { ok: true, id });
|
|
@@ -1076,6 +1077,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1076
1077
|
if (agent !== undefined) item.agent = agent || null;
|
|
1077
1078
|
if (body.references !== undefined) item.references = body.references;
|
|
1078
1079
|
if (body.acceptanceCriteria !== undefined) item.acceptanceCriteria = body.acceptanceCriteria;
|
|
1080
|
+
if (body.skipPr !== undefined) item.skipPr = body.skipPr === true;
|
|
1079
1081
|
item.updatedAt = new Date().toISOString();
|
|
1080
1082
|
|
|
1081
1083
|
safeWrite(wiPath, items);
|
|
@@ -1643,7 +1645,7 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1643
1645
|
file: f, format: 'draft', archived,
|
|
1644
1646
|
project: projectMatch ? projectMatch[1].trim() : '',
|
|
1645
1647
|
summary: titleMatch ? titleMatch[1].trim() : f.replace('.md', ''),
|
|
1646
|
-
status: archived ? 'completed' : completedPrdFiles.has(f) ? '
|
|
1648
|
+
status: archived ? 'completed' : completedPrdFiles.has(f) ? 'approved' : 'active',
|
|
1647
1649
|
branchStrategy: '',
|
|
1648
1650
|
featureBranch: '',
|
|
1649
1651
|
itemCount: (content.match(/^\d+\.\s+\*\*/gm) || []).length,
|
|
@@ -3057,7 +3059,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3057
3059
|
}},
|
|
3058
3060
|
|
|
3059
3061
|
// Work items
|
|
3060
|
-
{ method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: 'title, type?, description?, priority?, project?, agent?, agents?, scope?, references?, acceptanceCriteria?', handler: handleWorkItemsCreate },
|
|
3062
|
+
{ method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: 'title, type?, description?, priority?, project?, agent?, agents?, scope?, references?, acceptanceCriteria?, skipPr?', handler: handleWorkItemsCreate },
|
|
3061
3063
|
{ method: 'POST', path: '/api/work-items/update', desc: 'Edit a pending/failed work item', params: 'id, source?, title?, description?, type?, priority?, agent?, references?, acceptanceCriteria?', handler: handleWorkItemsUpdate },
|
|
3062
3064
|
{ method: 'POST', path: '/api/work-items/retry', desc: 'Reset a failed/dispatched item to pending', params: 'id, source?', handler: handleWorkItemsRetry },
|
|
3063
3065
|
{ method: 'POST', path: '/api/work-items/delete', desc: 'Remove a work item, kill agent, clear dispatch', params: 'id, source?', handler: handleWorkItemsDelete },
|
|
@@ -3136,12 +3138,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3136
3138
|
{ method: 'POST', path: '/api/plans/reject', desc: 'Reject a plan', params: 'file, rejectedBy?, reason?', handler: handlePlansReject },
|
|
3137
3139
|
{ method: 'POST', path: '/api/plans/regenerate', desc: 'Reset pending/failed work items for a plan so they re-materialize', params: 'source', handler: handlePlansRegenerate },
|
|
3138
3140
|
{ method: 'POST', path: '/api/plans/delete', desc: 'Delete a plan file and clean up work items', params: 'file', handler: handlePlansDelete },
|
|
3139
|
-
{ method: 'POST', path: '/api/plans/archive', desc: '
|
|
3141
|
+
{ method: 'POST', path: '/api/plans/archive', desc: 'Archive a plan/PRD (move to archive folder, also archives source .md plan if PRD)', params: 'file', handler: handlePlansArchiveMove },
|
|
3140
3142
|
{ method: 'POST', path: '/api/plans/unarchive', desc: 'Restore a plan/PRD from archive', params: 'file', handler: handlePlansUnarchive },
|
|
3141
3143
|
{ method: 'POST', path: '/api/plans/revise', desc: 'Request revision with feedback, dispatches agent to revise', params: 'file, feedback, requestedBy?', handler: handlePlansRevise },
|
|
3142
3144
|
{ method: 'POST', path: '/api/plans/discuss', desc: 'Generate a plan discussion session script for Claude CLI', params: 'file', handler: handlePlansDiscuss },
|
|
3143
|
-
{ method: 'POST', path: '/api/plans/archive', desc: 'Archive a plan/PRD (move to archive folder)', params: 'file', handler: handlePlansArchiveMove },
|
|
3144
|
-
{ method: 'POST', path: '/api/plans/unarchive', desc: 'Unarchive a plan/PRD (restore from archive folder)', params: 'file', handler: handlePlansUnarchive },
|
|
3145
3145
|
{ method: 'GET', path: /^\/api\/plans\/archive\/([^?]+)$/, desc: 'Read an archived plan file', handler: handlePlansArchiveRead },
|
|
3146
3146
|
{ method: 'GET', path: /^\/api\/plans\/([^?]+)$/, desc: 'Read a full plan (JSON from prd/ or markdown from plans/)', handler: handlePlansRead },
|
|
3147
3147
|
|
package/engine/lifecycle.js
CHANGED
|
@@ -603,7 +603,14 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
603
603
|
dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
|
|
604
604
|
}
|
|
605
605
|
const entry = dirtyTargets.get(targetName);
|
|
606
|
-
|
|
606
|
+
const existing = entry.prs.find(p => p.id === fullId || String(p.id) === String(prId));
|
|
607
|
+
if (existing) {
|
|
608
|
+
// Backfill prdItems if the entry was added by the poller before syncPrsFromOutput ran
|
|
609
|
+
if (meta?.item?.id && !existing.prdItems?.includes(meta.item.id)) {
|
|
610
|
+
existing.prdItems = [...(existing.prdItems || []), meta.item.id];
|
|
611
|
+
}
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
607
614
|
|
|
608
615
|
let title = meta?.item?.title || '';
|
|
609
616
|
const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
|
|
@@ -637,10 +644,26 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
637
644
|
|
|
638
645
|
// ─── Post-Completion Hooks ──────────────────────────────────────────────────
|
|
639
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Resolve which project's pull-requests.json contains a given PR ID.
|
|
649
|
+
* Returns the project object, or null if not found in any project file.
|
|
650
|
+
*/
|
|
651
|
+
function resolveProjectForPr(prId) {
|
|
652
|
+
const config = getConfig();
|
|
653
|
+
for (const p of shared.getProjects(config)) {
|
|
654
|
+
const prs = safeJson(projectPrPath(p)) || [];
|
|
655
|
+
if (prs.some(pr => pr.id === prId)) return p;
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
|
|
640
660
|
function updatePrAfterReview(agentId, pr, project) {
|
|
641
661
|
|
|
642
662
|
if (!pr?.id) return;
|
|
643
|
-
|
|
663
|
+
// Resolve actual project if not provided — avoids writing merged array to wrong path
|
|
664
|
+
const resolvedProject = project || resolveProjectForPr(pr.id);
|
|
665
|
+
if (!resolvedProject) { log('warn', `updatePrAfterReview: cannot resolve project for ${pr.id}`); return; }
|
|
666
|
+
const prs = getPrs(resolvedProject);
|
|
644
667
|
const target = prs.find(p => p.id === pr.id);
|
|
645
668
|
if (!target) return;
|
|
646
669
|
|
|
@@ -674,7 +697,7 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
674
697
|
shared.safeWrite(metricsPath, metrics);
|
|
675
698
|
}
|
|
676
699
|
|
|
677
|
-
shared.safeWrite(
|
|
700
|
+
shared.safeWrite(shared.projectPrPath(resolvedProject), prs);
|
|
678
701
|
log('info', `Updated ${pr.id} → minions review: ${target.reviewStatus} by ${reviewerName}`);
|
|
679
702
|
createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
|
|
680
703
|
}
|
|
@@ -682,7 +705,10 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
682
705
|
function updatePrAfterFix(pr, project, source) {
|
|
683
706
|
|
|
684
707
|
if (!pr?.id) return;
|
|
685
|
-
|
|
708
|
+
// Resolve actual project if not provided — avoids writing merged array to wrong path
|
|
709
|
+
const resolvedProject = project || resolveProjectForPr(pr.id);
|
|
710
|
+
if (!resolvedProject) { log('warn', `updatePrAfterFix: cannot resolve project for ${pr.id}`); return; }
|
|
711
|
+
const prs = getPrs(resolvedProject);
|
|
686
712
|
const target = prs.find(p => p.id === pr.id);
|
|
687
713
|
if (!target) return;
|
|
688
714
|
|
|
@@ -697,7 +723,7 @@ function updatePrAfterFix(pr, project, source) {
|
|
|
697
723
|
log('info', `Updated ${pr.id} → reviewStatus: waiting (fix pushed)`);
|
|
698
724
|
}
|
|
699
725
|
|
|
700
|
-
shared.safeWrite(
|
|
726
|
+
shared.safeWrite(shared.projectPrPath(resolvedProject), prs);
|
|
701
727
|
}
|
|
702
728
|
|
|
703
729
|
// ─── Post-Merge / Post-Close Hooks ───────────────────────────────────────────
|
|
@@ -1323,51 +1349,37 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1323
1349
|
}
|
|
1324
1350
|
|
|
1325
1351
|
if (!isSuccess && meta?.item?.id) {
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1330
|
-
? path.join(MINIONS_DIR, 'work-items.json')
|
|
1331
|
-
: meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
|
|
1332
|
-
if (wiPath) {
|
|
1333
|
-
const items = safeJson(wiPath) || [];
|
|
1334
|
-
const wi = items.find(i => i.id === meta.item.id);
|
|
1335
|
-
if (wi) retries = (wi._retryCount || 0); // Use fresh value from file
|
|
1336
|
-
}
|
|
1337
|
-
} catch { /* optional */ }
|
|
1338
|
-
|
|
1339
|
-
if (retries < 3) {
|
|
1340
|
-
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
|
|
1341
|
-
updateWorkItemStatus(meta, 'pending', '');
|
|
1352
|
+
const wiPath = resolveWiPath(meta);
|
|
1353
|
+
if (wiPath) {
|
|
1354
|
+
let finalStatus = null;
|
|
1342
1355
|
try {
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
: meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
|
|
1346
|
-
if (wiPath) {
|
|
1347
|
-
const items = safeJson(wiPath) || [];
|
|
1356
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
1357
|
+
if (!Array.isArray(items)) return items;
|
|
1348
1358
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1349
|
-
if (wi)
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1359
|
+
if (!wi) return items;
|
|
1360
|
+
|
|
1361
|
+
const retries = wi._retryCount || 0;
|
|
1362
|
+
if (retries < 3) {
|
|
1363
|
+
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
|
|
1364
|
+
wi._retryCount = retries + 1;
|
|
1365
|
+
wi.status = 'pending';
|
|
1366
|
+
delete wi.dispatched_at;
|
|
1367
|
+
delete wi.dispatched_to;
|
|
1368
|
+
finalStatus = 'pending';
|
|
1369
|
+
} else {
|
|
1370
|
+
wi.status = 'failed';
|
|
1371
|
+
wi.failReason = 'Agent failed (3 retries exhausted)';
|
|
1372
|
+
wi.failedAt = ts();
|
|
1373
|
+
finalStatus = 'failed';
|
|
1353
1374
|
}
|
|
1354
|
-
|
|
1375
|
+
if (type === 'decompose') delete wi._decomposing;
|
|
1376
|
+
return items;
|
|
1377
|
+
});
|
|
1355
1378
|
} catch (err) { log('warn', `Retry update: ${err.message}`); }
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
if (type === 'decompose') {
|
|
1361
|
-
try {
|
|
1362
|
-
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1363
|
-
? path.join(MINIONS_DIR, 'work-items.json')
|
|
1364
|
-
: meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
|
|
1365
|
-
if (wiPath) {
|
|
1366
|
-
const items = safeJson(wiPath) || [];
|
|
1367
|
-
const wi = items.find(i => i.id === meta.item.id);
|
|
1368
|
-
if (wi) { delete wi._decomposing; shared.safeWrite(wiPath, items); }
|
|
1369
|
-
}
|
|
1370
|
-
} catch (err) { log('warn', `Decompose cleanup: ${err.message}`); }
|
|
1379
|
+
// Sync status to PRD outside the work-items lock
|
|
1380
|
+
if (finalStatus) {
|
|
1381
|
+
syncPrdItemStatus(meta.item.id, finalStatus, meta.item?.sourcePlan);
|
|
1382
|
+
}
|
|
1371
1383
|
}
|
|
1372
1384
|
}
|
|
1373
1385
|
// Meeting post-completion: collect findings/debate/conclusion
|
|
@@ -1420,7 +1432,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1420
1432
|
}
|
|
1421
1433
|
|
|
1422
1434
|
// Detect implement tasks that completed without creating a PR
|
|
1423
|
-
if (isSuccess && (type === 'implement' || type === 'implement:large' || type === 'fix') && prsCreatedCount === 0 && meta?.item?.id) {
|
|
1435
|
+
if (isSuccess && (type === 'implement' || type === 'implement:large' || type === 'fix') && prsCreatedCount === 0 && meta?.item?.id && !meta?.item?.skipPr) {
|
|
1424
1436
|
// Check if a PR already exists linked to this work item (from a previous attempt)
|
|
1425
1437
|
const projects = shared.getProjects(config);
|
|
1426
1438
|
const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
|
@@ -1513,6 +1525,7 @@ module.exports = {
|
|
|
1513
1525
|
updateWorkItemStatus,
|
|
1514
1526
|
syncPrdItemStatus,
|
|
1515
1527
|
syncPrsFromOutput,
|
|
1528
|
+
resolveProjectForPr,
|
|
1516
1529
|
updatePrAfterReview,
|
|
1517
1530
|
updatePrAfterFix,
|
|
1518
1531
|
handlePostMerge,
|
package/engine.js
CHANGED
|
@@ -1373,6 +1373,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
1373
1373
|
}
|
|
1374
1374
|
}
|
|
1375
1375
|
|
|
1376
|
+
if (item.status === 'needs-human-review') continue; // Explicit skip — flagged for human attention
|
|
1376
1377
|
if (item.status !== 'queued' && item.status !== 'pending') continue;
|
|
1377
1378
|
|
|
1378
1379
|
// Dependency gate: skip items whose depends_on are not yet met; propagate failure
|
|
@@ -1474,6 +1475,11 @@ function discoverFromWorkItems(config, project) {
|
|
|
1474
1475
|
const ac = (Array.isArray(item.acceptanceCriteria) ? item.acceptanceCriteria : []).map(c => '- [ ] ' + c).join('\n');
|
|
1475
1476
|
vars.acceptance_criteria = ac ? '## Acceptance Criteria\n\n' + ac : '';
|
|
1476
1477
|
|
|
1478
|
+
// Inject PR section — conditional based on skipPr flag
|
|
1479
|
+
vars.pr_section = item.skipPr
|
|
1480
|
+
? '## Push Branch\n\n**PR creation is skipped for this work item.** Push your branch and report the branch name.\n\n```bash\ngit push -u origin {{branch_name}}\n```\n\nInclude the branch name in your completion summary.'
|
|
1481
|
+
: '## Create PR (MANDATORY)\n\n**Your task is NOT complete until a pull request exists.** If PR creation fails, retry up to 3 times before reporting the error.\n\n{{pr_create_instructions}}\n- sourceRefName: `refs/heads/{{branch_name}}`\n- targetRefName: `refs/heads/{{main_branch}}`\n- title: `{{commit_message}}`\n- labels: `["minions:{{agent_id}}"]`\n\nInclude in the PR description:\n- What was built and why\n- Files changed\n- How to build and test, browser URL if applicable\n- Test plan\n\n## Post self-review on PR\n\n{{pr_comment_instructions}}\n- pullRequestId: `<from PR creation>`\n- Re-read your own diff critically before posting\n- Sign: `Built by Minions ({{agent_name}} — {{agent_role}})`';
|
|
1482
|
+
|
|
1477
1483
|
// Inject checkpoint context if agent left a checkpoint.json from a prior run
|
|
1478
1484
|
vars.checkpoint_context = '';
|
|
1479
1485
|
try {
|
|
@@ -1559,15 +1565,13 @@ function discoverFromWorkItems(config, project) {
|
|
|
1559
1565
|
setCooldown(key);
|
|
1560
1566
|
}
|
|
1561
1567
|
|
|
1562
|
-
// Write back updated statuses
|
|
1563
|
-
if (newWork.length > 0) {
|
|
1568
|
+
// Write back updated statuses — needsWrite covers mutation-only ticks, newWork covers dispatches
|
|
1569
|
+
if (needsWrite || newWork.length > 0) {
|
|
1564
1570
|
const workItemsPath = projectWorkItemsPath(project);
|
|
1565
1571
|
safeWrite(workItemsPath, items);
|
|
1566
1572
|
for (const s of prdSyncQueue) syncPrdItemStatus(s.id, 'dispatched', s.sourcePlan);
|
|
1567
1573
|
}
|
|
1568
1574
|
|
|
1569
|
-
if (needsWrite) safeWrite(projectWorkItemsPath(project), items);
|
|
1570
|
-
|
|
1571
1575
|
const skipTotal = skipped.gated + skipped.noAgent;
|
|
1572
1576
|
if (skipTotal > 0) {
|
|
1573
1577
|
log('debug', `Work item discovery (${project?.name}): skipped ${skipTotal} items (${skipped.gated} gated, ${skipped.noAgent} no agent)`);
|
|
@@ -1755,8 +1759,10 @@ function discoverCentralWorkItems(config) {
|
|
|
1755
1759
|
const items = safeJson(centralPath) || [];
|
|
1756
1760
|
const projects = getProjects(config);
|
|
1757
1761
|
const newWork = [];
|
|
1762
|
+
let needsWrite = false;
|
|
1758
1763
|
|
|
1759
1764
|
for (const item of items) {
|
|
1765
|
+
if (item.status === 'needs-human-review') continue; // Explicit skip — flagged for human attention
|
|
1760
1766
|
if (item.status !== 'queued' && item.status !== 'pending') continue;
|
|
1761
1767
|
|
|
1762
1768
|
const key = `central-work-${item.id}`;
|
|
@@ -1809,6 +1815,12 @@ function discoverCentralWorkItems(config) {
|
|
|
1809
1815
|
const fanAc = (Array.isArray(item.acceptanceCriteria) ? item.acceptanceCriteria : []).map(c => '- [ ] ' + c).join('\n');
|
|
1810
1816
|
vars.acceptance_criteria = fanAc ? '## Acceptance Criteria\n\n' + fanAc : '';
|
|
1811
1817
|
|
|
1818
|
+
// Inject PR section — conditional based on skipPr flag
|
|
1819
|
+
const fanBranch = '{{branch_name}}';
|
|
1820
|
+
vars.pr_section = item.skipPr
|
|
1821
|
+
? '## Push Branch\n\n**PR creation is skipped for this work item.** Push your branch and report the branch name.\n\n```bash\ngit push -u origin ' + fanBranch + '\n```\n\nInclude the branch name in your completion summary.'
|
|
1822
|
+
: '## Create PR (MANDATORY)\n\n**Your task is NOT complete until a pull request exists.** If PR creation fails, retry up to 3 times before reporting the error.\n\n{{pr_create_instructions}}\n- sourceRefName: `refs/heads/' + fanBranch + '`\n- targetRefName: `refs/heads/{{main_branch}}`\n- title: `{{commit_message}}`\n- labels: `["minions:{{agent_id}}"]`\n\nInclude in the PR description:\n- What was built and why\n- Files changed\n- How to build and test, browser URL if applicable\n- Test plan\n\n## Post self-review on PR\n\n{{pr_comment_instructions}}\n- pullRequestId: `<from PR creation>`\n- Re-read your own diff critically before posting\n- Sign: `Built by Minions ({{agent_name}} — {{agent_role}})`';
|
|
1823
|
+
|
|
1812
1824
|
if (workType === 'ask') {
|
|
1813
1825
|
vars.question = item.title + (item.description ? '\n\n' + item.description : '');
|
|
1814
1826
|
vars.task_id = item.id;
|
|
@@ -1828,7 +1840,7 @@ function discoverCentralWorkItems(config) {
|
|
|
1828
1840
|
if (!prompt) {
|
|
1829
1841
|
if (renderError) {
|
|
1830
1842
|
log('warn', `Fan-out: ${item.id} → ${agent.id}: ${renderError.message}`);
|
|
1831
|
-
if (item._pendingReason !== 'critical_vars_missing') { item._pendingReason = 'critical_vars_missing'; }
|
|
1843
|
+
if (item._pendingReason !== 'critical_vars_missing') { item._pendingReason = 'critical_vars_missing'; needsWrite = true; }
|
|
1832
1844
|
} else {
|
|
1833
1845
|
log('warn', `Fan-out: playbook '${playbookName}' failed to render for ${item.id} → ${agent.id}, skipping`);
|
|
1834
1846
|
}
|
|
@@ -1854,6 +1866,7 @@ function discoverCentralWorkItems(config) {
|
|
|
1854
1866
|
item.dispatched_to = idleAgents.map(a => a.id).join(', ');
|
|
1855
1867
|
item.scope = 'fan-out';
|
|
1856
1868
|
item.fanOutAgents = idleAgents.map(a => a.id);
|
|
1869
|
+
needsWrite = true;
|
|
1857
1870
|
setCooldown(key);
|
|
1858
1871
|
log('info', `Fan-out: ${item.id} dispatched to ${idleAgents.length} agents: ${idleAgents.map(a => a.name).join(', ')}`);
|
|
1859
1872
|
|
|
@@ -1981,10 +1994,12 @@ function discoverCentralWorkItems(config) {
|
|
|
1981
1994
|
if (renderError) {
|
|
1982
1995
|
log('warn', `Dispatch: ${item.id}: ${renderError.message}`);
|
|
1983
1996
|
item._pendingReason = 'critical_vars_missing';
|
|
1997
|
+
needsWrite = true;
|
|
1984
1998
|
} else {
|
|
1985
1999
|
log('warn', `Dispatch: playbook '${playbookName}' failed to render for ${item.id}, resetting to pending`);
|
|
1986
2000
|
}
|
|
1987
2001
|
item.status = 'pending';
|
|
2002
|
+
needsWrite = true;
|
|
1988
2003
|
continue;
|
|
1989
2004
|
}
|
|
1990
2005
|
|
|
@@ -2001,11 +2016,12 @@ function discoverCentralWorkItems(config) {
|
|
|
2001
2016
|
item.status = 'dispatched';
|
|
2002
2017
|
item.dispatched_at = ts();
|
|
2003
2018
|
item.dispatched_to = agentId;
|
|
2019
|
+
needsWrite = true;
|
|
2004
2020
|
setCooldown(key);
|
|
2005
2021
|
}
|
|
2006
2022
|
}
|
|
2007
2023
|
|
|
2008
|
-
if (newWork.length > 0) safeWrite(centralPath, items);
|
|
2024
|
+
if (needsWrite || newWork.length > 0) safeWrite(centralPath, items);
|
|
2009
2025
|
return newWork;
|
|
2010
2026
|
}
|
|
2011
2027
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.155",
|
|
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"
|
package/playbooks/implement.md
CHANGED
|
@@ -50,28 +50,7 @@ git push -u origin {{branch_name}}
|
|
|
50
50
|
|
|
51
51
|
Do NOT remove the worktree — the engine handles cleanup automatically.
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
**Your task is NOT complete until a pull request exists.** If PR creation fails, retry up to 3 times before reporting the error.
|
|
56
|
-
|
|
57
|
-
{{pr_create_instructions}}
|
|
58
|
-
- sourceRefName: `refs/heads/{{branch_name}}`
|
|
59
|
-
- targetRefName: `refs/heads/{{main_branch}}`
|
|
60
|
-
- title: `{{commit_message}}`
|
|
61
|
-
- labels: `["minions:{{agent_id}}"]`
|
|
62
|
-
|
|
63
|
-
Include in the PR description:
|
|
64
|
-
- What was built and why
|
|
65
|
-
- Files changed
|
|
66
|
-
- How to build and test, browser URL if applicable
|
|
67
|
-
- Test plan
|
|
68
|
-
|
|
69
|
-
## Post self-review on PR
|
|
70
|
-
|
|
71
|
-
{{pr_comment_instructions}}
|
|
72
|
-
- pullRequestId: `<from PR creation>`
|
|
73
|
-
- Re-read your own diff critically before posting
|
|
74
|
-
- Sign: `Built by Minions ({{agent_name}} — {{agent_role}})`
|
|
53
|
+
{{pr_section}}
|
|
75
54
|
|
|
76
55
|
## Signal Completion
|
|
77
56
|
|