@yemi33/minions 0.1.816 → 0.1.818
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 +11 -4
- package/dashboard/js/command-center.js +10 -8
- package/dashboard/js/modal-qa.js +72 -19
- package/dashboard/layout.html +3 -0
- package/dashboard.js +1 -0
- package/engine/lifecycle.js +19 -1
- package/engine.js +119 -23
- package/package.json +1 -1
- package/playbooks/plan-to-prd.md +14 -0
- package/playbooks/shared-rules.md +8 -8
- package/playbooks/verify.md +25 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.818 (2026-04-11)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- verify workflow handles shared-branch plans and existing E2E PRs
|
|
7
|
+
- doc-chat drag-to-resize, queued message UX, insertAdjacentHTML
|
|
8
|
+
- re-open done work items when PRD item set to updated/missing
|
|
9
|
+
- diff-aware plan-to-prd dispatch for completed/approved PRDs
|
|
6
10
|
- Bidirectional Teams integration for Command Center (#764)
|
|
7
11
|
- show branch strategy on PRD view (#762)
|
|
8
12
|
- Restructure decompose.md sub-task output format (#655)
|
|
@@ -13,6 +17,9 @@
|
|
|
13
17
|
- cap review→fix cycles at evalMaxIterations (default 3)
|
|
14
18
|
|
|
15
19
|
### Fixes
|
|
20
|
+
- strip ===ACTIONS=== from CC streamed text during rendering
|
|
21
|
+
- ccExecuteAction routes action status to correct tab via targetTabId
|
|
22
|
+
- clear _completionNotified on plan resume for re-completion
|
|
16
23
|
- handle local-only dependency branches in dep merge (closes #782) (#825)
|
|
17
24
|
- reset PRD item status when work item is deleted (closes #779) (#823)
|
|
18
25
|
- add smoke test for checkTimeouts ReferenceError regression (closes #775) (#822)
|
|
@@ -30,9 +37,9 @@
|
|
|
30
37
|
- approved is permanent — no path can ever downgrade it
|
|
31
38
|
- allow fix dispatch on approved PRs (only guard is in updatePrAfterFix)
|
|
32
39
|
- bot comment on approved PR can no longer reset vote
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
40
|
+
|
|
41
|
+
### Other
|
|
42
|
+
- docs: diff-aware PRD update playbook + prioritize team memory in shared rules
|
|
36
43
|
|
|
37
44
|
## 0.1.782 (2026-04-10)
|
|
38
45
|
|
|
@@ -149,7 +149,10 @@ function ccSwitchTab(id) {
|
|
|
149
149
|
html += '</div>';
|
|
150
150
|
}
|
|
151
151
|
var text = tab._streamedText || '';
|
|
152
|
-
if (text)
|
|
152
|
+
if (text) {
|
|
153
|
+
var displayRestore = text.indexOf('===ACTIONS===') >= 0 ? text.slice(0, text.indexOf('===ACTIONS===')).trim() : text;
|
|
154
|
+
html += renderMd(displayRestore);
|
|
155
|
+
}
|
|
153
156
|
var ms = Date.now() - restoreStart;
|
|
154
157
|
var label = 'Thinking...';
|
|
155
158
|
for (var pi = phases.length - 1; pi >= 0; pi--) { if (ms >= phases[pi][0]) { label = phases[pi][1]; break; } }
|
|
@@ -510,7 +513,8 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
510
513
|
html += '</div>';
|
|
511
514
|
}
|
|
512
515
|
if (streamedText) {
|
|
513
|
-
|
|
516
|
+
var displayText = streamedText.indexOf('===ACTIONS===') >= 0 ? streamedText.slice(0, streamedText.indexOf('===ACTIONS===')).trim() : streamedText;
|
|
517
|
+
html += renderMd(displayText);
|
|
514
518
|
}
|
|
515
519
|
html += '<div style="margin-top:' + (streamedText ? '6px' : '0') + '">' + _getThinkingHtml() + '</div>';
|
|
516
520
|
streamDiv.innerHTML = html;
|
|
@@ -579,7 +583,7 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
579
583
|
ccSaveState(); ccUpdateSessionIndicator();
|
|
580
584
|
}
|
|
581
585
|
if (evt.actions && evt.actions.length > 0) {
|
|
582
|
-
for (var ai = 0; ai < evt.actions.length; ai++) { await ccExecuteAction(evt.actions[ai]); }
|
|
586
|
+
for (var ai = 0; ai < evt.actions.length; ai++) { await ccExecuteAction(evt.actions[ai], activeTabId); }
|
|
583
587
|
}
|
|
584
588
|
} else if (evt.type === 'error') {
|
|
585
589
|
_cleanupStreamDiv();
|
|
@@ -609,7 +613,7 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
609
613
|
ccSaveState(); ccUpdateSessionIndicator();
|
|
610
614
|
}
|
|
611
615
|
if (revt.actions && revt.actions.length > 0) {
|
|
612
|
-
for (var ai2 = 0; ai2 < revt.actions.length; ai2++) { await ccExecuteAction(revt.actions[ai2]); }
|
|
616
|
+
for (var ai2 = 0; ai2 < revt.actions.length; ai2++) { await ccExecuteAction(revt.actions[ai2], activeTabId); }
|
|
613
617
|
}
|
|
614
618
|
} else if (revt.type === 'chunk') {
|
|
615
619
|
streamedText = revt.text;
|
|
@@ -688,8 +692,7 @@ async function _ccFetch(url, body) {
|
|
|
688
692
|
return res;
|
|
689
693
|
}
|
|
690
694
|
|
|
691
|
-
async function ccExecuteAction(action) {
|
|
692
|
-
var msgs = document.getElementById('cc-messages');
|
|
695
|
+
async function ccExecuteAction(action, targetTabId) {
|
|
693
696
|
var status = document.createElement('div');
|
|
694
697
|
status.style.cssText = 'padding:4px 10px;border-radius:4px;font-size:10px;align-self:flex-start;border:1px dashed var(--border);color:var(--muted)';
|
|
695
698
|
|
|
@@ -1052,8 +1055,7 @@ async function ccExecuteAction(action) {
|
|
|
1052
1055
|
status.style.color = 'var(--red)';
|
|
1053
1056
|
}
|
|
1054
1057
|
|
|
1055
|
-
|
|
1056
|
-
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
1058
|
+
ccAddMessage('assistant', status.outerHTML, false, targetTabId);
|
|
1057
1059
|
refresh();
|
|
1058
1060
|
}
|
|
1059
1061
|
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -28,6 +28,17 @@ let _qaAbortController = null;
|
|
|
28
28
|
let _qaQueue = []; // queued messages while processing
|
|
29
29
|
const QA_QUEUE_CAP = 10; // max queued messages
|
|
30
30
|
let _qaSessionKey = ''; // key for current conversation (title or filePath)
|
|
31
|
+
|
|
32
|
+
function _renderQaUserMessage(thread, message, selection) {
|
|
33
|
+
let qHtml = '<div class="modal-qa-q">' + escHtml(message);
|
|
34
|
+
if (selection) {
|
|
35
|
+
qHtml += '<span class="selection-ref">Re: "' + escHtml(selection.slice(0, 100)) + ((selection.length > 100) ? '...' : '') + '"</span>';
|
|
36
|
+
}
|
|
37
|
+
qHtml += '</div>';
|
|
38
|
+
thread.insertAdjacentHTML('beforeend', qHtml);
|
|
39
|
+
thread.scrollTop = thread.scrollHeight;
|
|
40
|
+
_showThreadWrap();
|
|
41
|
+
}
|
|
31
42
|
const _qaSessions = new Map(); // persist conversations across modal open/close {key → {history, threadHtml}}
|
|
32
43
|
// Restore from localStorage
|
|
33
44
|
try {
|
|
@@ -100,6 +111,11 @@ function _initQaSession() {
|
|
|
100
111
|
}
|
|
101
112
|
if (prior.filePath) _modalFilePath = prior.filePath;
|
|
102
113
|
_showThreadWrap();
|
|
114
|
+
// Defer scroll — container just transitioned from display:none, layout not yet computed
|
|
115
|
+
requestAnimationFrame(function() {
|
|
116
|
+
var thread = document.getElementById('modal-qa-thread');
|
|
117
|
+
if (thread) thread.scrollTop = thread.scrollHeight;
|
|
118
|
+
});
|
|
103
119
|
} else {
|
|
104
120
|
_qaHistory = [];
|
|
105
121
|
document.getElementById('modal-qa-thread').innerHTML = '';
|
|
@@ -145,16 +161,6 @@ function modalSend() {
|
|
|
145
161
|
var thread = document.getElementById('modal-qa-thread');
|
|
146
162
|
const selection = _modalDocContext.selection || '';
|
|
147
163
|
|
|
148
|
-
// Show message in thread immediately
|
|
149
|
-
let qHtml = '<div class="modal-qa-q">' + escHtml(message);
|
|
150
|
-
if (selection) {
|
|
151
|
-
qHtml += '<span class="selection-ref">Re: "' + escHtml(selection.slice(0, 100)) + ((selection.length > 100) ? '...' : '') + '"</span>';
|
|
152
|
-
}
|
|
153
|
-
qHtml += '</div>';
|
|
154
|
-
thread.innerHTML += qHtml;
|
|
155
|
-
thread.scrollTop = thread.scrollHeight;
|
|
156
|
-
_showThreadWrap();
|
|
157
|
-
|
|
158
164
|
// Clear input immediately so user can type next message
|
|
159
165
|
input.value = '';
|
|
160
166
|
_modalDocContext.selection = '';
|
|
@@ -165,14 +171,18 @@ function modalSend() {
|
|
|
165
171
|
showToast('cmd-toast', 'Queue full — wait for current response', false);
|
|
166
172
|
return;
|
|
167
173
|
}
|
|
168
|
-
// Queue the message — show
|
|
174
|
+
// Queue the message — show only grey queued indicator (blue bubble shows when processing starts)
|
|
169
175
|
_qaQueue.push({ message, selection });
|
|
170
176
|
const preview = escHtml(message.length > 60 ? message.slice(0, 57) + '...' : message);
|
|
171
|
-
thread.
|
|
177
|
+
thread.insertAdjacentHTML('beforeend', '<div class="qa-queued-item" style="color:var(--muted);font-size:10px;padding:4px 8px">Queued: "' + preview + '"</div>');
|
|
172
178
|
thread.scrollTop = thread.scrollHeight;
|
|
179
|
+
_showThreadWrap();
|
|
173
180
|
return;
|
|
174
181
|
}
|
|
175
182
|
|
|
183
|
+
// Show message in thread when processing starts (not when queued)
|
|
184
|
+
_renderQaUserMessage(thread, message, selection);
|
|
185
|
+
|
|
176
186
|
_processQaMessage(message, selection);
|
|
177
187
|
}
|
|
178
188
|
|
|
@@ -192,12 +202,12 @@ async function _processQaMessage(message, selection) {
|
|
|
192
202
|
const loadingId = 'chat-loading-' + Date.now();
|
|
193
203
|
const qaQueueBadge = _qaQueue.length > 0 ? ' <span style="font-size:9px;color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:8px;border:1px solid var(--border)">+' + _qaQueue.length + ' queued</span>' : '';
|
|
194
204
|
_qaAbortController = new AbortController();
|
|
195
|
-
thread.
|
|
205
|
+
thread.insertAdjacentHTML('beforeend', '<div class="modal-qa-loading" id="' + loadingId + '">' +
|
|
196
206
|
'<div class="dot-pulse"><span></span><span></span><span></span></div> ' +
|
|
197
207
|
'<span id="' + loadingId + '-text">Thinking...</span> ' +
|
|
198
208
|
'<span id="' + loadingId + '-time" style="font-size:10px;color:var(--muted)"></span>' +
|
|
199
209
|
' <button onclick="qaAbort()" style="font-size:9px;padding:2px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--red);cursor:pointer">Stop</button>' +
|
|
200
|
-
qaQueueBadge + '</div>';
|
|
210
|
+
qaQueueBadge + '</div>');
|
|
201
211
|
thread.scrollTop = thread.scrollHeight;
|
|
202
212
|
|
|
203
213
|
const isPlanEdit = _modalFilePath && _modalFilePath.match(/^plans\/.*\.md$/);
|
|
@@ -237,7 +247,7 @@ async function _processQaMessage(message, selection) {
|
|
|
237
247
|
const suffix = data.edited ? '\n\n\u2713 Document saved.' : '';
|
|
238
248
|
const qaElapsed = Math.round((Date.now() - qaStartTime) / 1000);
|
|
239
249
|
const qaTimeLabel = '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right;padding-right:24px">' + qaElapsed + 's</div>';
|
|
240
|
-
thread.
|
|
250
|
+
thread.insertAdjacentHTML('beforeend', '<div class="modal-qa-a" style="border-left-color:' + borderColor + '">' + llmCopyBtn() + renderMd(data.answer + suffix) + qaTimeLabel + '</div>');
|
|
241
251
|
|
|
242
252
|
// Track conversation history
|
|
243
253
|
_qaHistory.push({ role: 'user', text: message });
|
|
@@ -281,7 +291,7 @@ async function _processQaMessage(message, selection) {
|
|
|
281
291
|
}
|
|
282
292
|
} else {
|
|
283
293
|
const qaElapsedErr = Math.round((Date.now() - qaStartTime) / 1000);
|
|
284
|
-
thread.
|
|
294
|
+
thread.insertAdjacentHTML('beforeend', '<div class="modal-qa-a" style="color:var(--red)">Error: ' + escHtml(data.error || 'Failed') + '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right">' + qaElapsedErr + 's</div></div>');
|
|
285
295
|
}
|
|
286
296
|
} catch (e) {
|
|
287
297
|
clearInterval(qaTimer);
|
|
@@ -289,9 +299,9 @@ async function _processQaMessage(message, selection) {
|
|
|
289
299
|
if (loadingEl) loadingEl.remove();
|
|
290
300
|
const qaElapsedExc = Math.round((Date.now() - qaStartTime) / 1000);
|
|
291
301
|
if (e.name === 'AbortError') {
|
|
292
|
-
thread.
|
|
302
|
+
thread.insertAdjacentHTML('beforeend', '<div class="modal-qa-a" style="color:var(--muted)">Stopped<div style="font-size:9px;margin-top:4px;text-align:right">' + qaElapsedExc + 's</div></div>');
|
|
293
303
|
} else {
|
|
294
|
-
thread.
|
|
304
|
+
thread.insertAdjacentHTML('beforeend', '<div class="modal-qa-a" style="color:var(--red)">Error: ' + escHtml(e.message) + '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right">' + qaElapsedExc + 's</div></div>');
|
|
295
305
|
}
|
|
296
306
|
}
|
|
297
307
|
|
|
@@ -329,9 +339,10 @@ async function _processQaMessage(message, selection) {
|
|
|
329
339
|
// Process next queued message
|
|
330
340
|
if (_qaQueue.length > 0) {
|
|
331
341
|
const next = _qaQueue.shift();
|
|
332
|
-
// Remove the
|
|
342
|
+
// Remove the queued indicator and show the blue user bubble now that it's processing
|
|
333
343
|
const queuedEl = thread.querySelector('.qa-queued-item');
|
|
334
344
|
if (queuedEl) queuedEl.remove();
|
|
345
|
+
_renderQaUserMessage(thread, next.message, next.selection);
|
|
335
346
|
_processQaMessage(next.message, next.selection);
|
|
336
347
|
} else if (modalIsOpen) {
|
|
337
348
|
document.getElementById('modal-qa-input')?.focus();
|
|
@@ -361,4 +372,46 @@ function _showThreadWrap() {
|
|
|
361
372
|
if (expandBar) expandBar.style.display = 'none';
|
|
362
373
|
}
|
|
363
374
|
|
|
375
|
+
// ── Drag-to-resize doc chat thread ──────────────────────────────────────────
|
|
376
|
+
(function() {
|
|
377
|
+
var _dragging = false, _startY = 0, _startH = 0, _thread = null;
|
|
378
|
+
var COLLAPSE_THRESHOLD = 40;
|
|
379
|
+
var MIN_HEIGHT = 60;
|
|
380
|
+
var MAX_HEIGHT = 500;
|
|
381
|
+
|
|
382
|
+
document.addEventListener('pointerdown', function(e) {
|
|
383
|
+
var handle = e.target.closest('#qa-resize-handle');
|
|
384
|
+
if (!handle) return;
|
|
385
|
+
_thread = document.getElementById('modal-qa-thread');
|
|
386
|
+
if (!_thread) return;
|
|
387
|
+
_dragging = true;
|
|
388
|
+
_startY = e.clientY;
|
|
389
|
+
_startH = _thread.offsetHeight || 200;
|
|
390
|
+
handle.setPointerCapture(e.pointerId);
|
|
391
|
+
document.body.style.userSelect = 'none';
|
|
392
|
+
e.preventDefault();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
document.addEventListener('pointermove', function(e) {
|
|
396
|
+
if (!_dragging || !_thread) return;
|
|
397
|
+
var delta = _startY - e.clientY;
|
|
398
|
+
var newH = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, _startH + delta));
|
|
399
|
+
_thread.style.maxHeight = newH + 'px';
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
document.addEventListener('pointerup', function(e) {
|
|
403
|
+
if (!_dragging) return;
|
|
404
|
+
_dragging = false;
|
|
405
|
+
document.body.style.userSelect = '';
|
|
406
|
+
if (!_thread) return;
|
|
407
|
+
var delta = _startY - e.clientY;
|
|
408
|
+
var finalH = _startH + delta;
|
|
409
|
+
if (finalH < COLLAPSE_THRESHOLD) {
|
|
410
|
+
_thread.style.maxHeight = '';
|
|
411
|
+
toggleDocChat();
|
|
412
|
+
}
|
|
413
|
+
_thread = null;
|
|
414
|
+
});
|
|
415
|
+
})();
|
|
416
|
+
|
|
364
417
|
window.MinionsQA = { showModalQa, modalAskAboutSelection, clearQaSelection, clearQaConversation, modalSend, qaAbort, toggleDocChat, _showThreadWrap };
|
package/dashboard/layout.html
CHANGED
|
@@ -113,6 +113,9 @@
|
|
|
113
113
|
<button style="background:none;border:none;color:var(--muted);font-size:10px;cursor:pointer;padding:2px 6px" onclick="toggleDocChat()" title="Expand thread">▲ Expand chat</button>
|
|
114
114
|
</div>
|
|
115
115
|
<div id="modal-qa-thread-wrap" style="display:none">
|
|
116
|
+
<div id="qa-resize-handle" style="height:6px;cursor:ns-resize;display:flex;align-items:center;justify-content:center;user-select:none;touch-action:none" title="Drag to resize">
|
|
117
|
+
<div style="width:32px;height:3px;border-radius:2px;background:var(--border)"></div>
|
|
118
|
+
</div>
|
|
116
119
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
|
117
120
|
<button id="qa-clear-btn" style="background:none;border:none;color:var(--muted);font-size:10px;cursor:pointer;padding:2px 6px" onclick="clearQaConversation()" title="Clear conversation">Clear</button>
|
|
118
121
|
<button style="background:none;border:none;color:var(--muted);font-size:10px;cursor:pointer;padding:2px 6px" onclick="toggleDocChat()" id="qa-toggle-btn" title="Collapse thread">▼ Collapse</button>
|
package/dashboard.js
CHANGED
|
@@ -2186,6 +2186,7 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2186
2186
|
plan.approvedAt = new Date().toISOString();
|
|
2187
2187
|
plan.approvedBy = body.approvedBy || os.userInfo().username;
|
|
2188
2188
|
delete plan.pausedAt;
|
|
2189
|
+
delete plan._completionNotified; // Allow re-completion after new work finishes
|
|
2189
2190
|
safeWrite(planPath, plan);
|
|
2190
2191
|
|
|
2191
2192
|
// Resume paused work items across all projects
|
package/engine/lifecycle.js
CHANGED
|
@@ -204,8 +204,22 @@ function checkPlanCompletion(meta, config) {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
// Shared-branch plans already have all changes on a single feature branch — no merge needed
|
|
208
|
+
const isSharedBranch = plan.branch_strategy === 'shared-branch' && plan.feature_branch;
|
|
209
|
+
|
|
207
210
|
// Build per-project checkout commands: one worktree, merge all PR branches into it
|
|
208
211
|
const checkoutBlocks = Object.entries(projectPrs).map(([name, { project: p, prs, mainBranch }]) => {
|
|
212
|
+
if (isSharedBranch) {
|
|
213
|
+
const featureBranch = plan.feature_branch;
|
|
214
|
+
const wtPath = `${p.localPath}/../worktrees/verify-${name}-${planSlug}`;
|
|
215
|
+
const lines = [
|
|
216
|
+
`# ${name} — shared-branch: use existing feature branch directly`,
|
|
217
|
+
`cd "${p.localPath.replace(/\\/g, '/')}"`,
|
|
218
|
+
`git fetch origin "${featureBranch}"`,
|
|
219
|
+
`git worktree add "${wtPath}" "origin/${featureBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${featureBranch}" && git pull origin "${featureBranch}")`,
|
|
220
|
+
];
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}
|
|
209
223
|
const wtPath = `${p.localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
|
|
210
224
|
const branches = prs.map(pr => pr.branch).filter(Boolean);
|
|
211
225
|
const lines = [
|
|
@@ -235,9 +249,13 @@ function checkPlanCompletion(meta, config) {
|
|
|
235
249
|
`- **${name}**: \`${p.localPath}/../worktrees/verify-${planSlug}\``
|
|
236
250
|
).join('\n');
|
|
237
251
|
|
|
252
|
+
const sharedBranchNote = isSharedBranch
|
|
253
|
+
? `\n**Shared-branch plan** — all changes are already on branch \`${plan.feature_branch}\`. Use this branch directly for the E2E PR instead of creating a new \`e2e/\` branch. Check if a PR already exists for this branch before creating one.\n`
|
|
254
|
+
: '';
|
|
255
|
+
|
|
238
256
|
const description = [
|
|
239
257
|
`Verification task for completed plan \`${planFile}\`.`,
|
|
240
|
-
|
|
258
|
+
sharedBranchNote,
|
|
241
259
|
`## Projects & Worktrees`,
|
|
242
260
|
``,
|
|
243
261
|
`Each project gets ONE worktree with all PR branches merged in:`,
|
package/engine.js
CHANGED
|
@@ -1227,6 +1227,12 @@ function autoCleanPrdWorkItems(prdFile, config) {
|
|
|
1227
1227
|
}
|
|
1228
1228
|
}
|
|
1229
1229
|
|
|
1230
|
+
function buildWiDescription(item, planFile) {
|
|
1231
|
+
const criteria = (item.acceptance_criteria || []).map(c => `- ${c}`).join('\n');
|
|
1232
|
+
const complexity = item.estimated_complexity || 'medium';
|
|
1233
|
+
return `${item.description || ''}\n\n**Plan:** ${planFile}\n**Plan Item:** ${item.id}\n**Complexity:** ${complexity}${criteria ? '\n\n**Acceptance Criteria:**\n' + criteria : ''}`;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1230
1236
|
function materializePlansAsWorkItems(config) {
|
|
1231
1237
|
if (!fs.existsSync(PRD_DIR)) { try { fs.mkdirSync(PRD_DIR, { recursive: true }); } catch (e) { log('warn', 'create PRD directory: ' + e.message); } }
|
|
1232
1238
|
|
|
@@ -1325,19 +1331,72 @@ function materializePlansAsWorkItems(config) {
|
|
|
1325
1331
|
// Handle PRD based on current status
|
|
1326
1332
|
const prdStatus = plan.status || (plan.requires_approval ? 'awaiting-approval' : null);
|
|
1327
1333
|
|
|
1328
|
-
// Approved/
|
|
1329
|
-
if (prdStatus
|
|
1334
|
+
// Approved/completed PRDs with existing work: auto-dispatch diff-aware plan-to-prd update
|
|
1335
|
+
if (prdStatus === PLAN_STATUS.APPROVED || prdStatus === PLAN_STATUS.COMPLETED) {
|
|
1336
|
+
const allWorkItems = queries.getWorkItems(config);
|
|
1337
|
+
const planWis = allWorkItems.filter(w => w.sourcePlan === file && w.itemType !== 'pr' && w.itemType !== 'verify');
|
|
1338
|
+
const doneWis = planWis.filter(w => DONE_STATUSES.has(w.status));
|
|
1339
|
+
if (doneWis.length > 0) {
|
|
1340
|
+
const allPrs = getProjects(config).flatMap(p => safeJson(projectPrPath(p)) || []);
|
|
1341
|
+
const prLinks = shared.getPrLinks();
|
|
1342
|
+
const implContext = (plan.missing_features || []).map(f => {
|
|
1343
|
+
const wi = planWis.find(w => w.id === f.id);
|
|
1344
|
+
const pr = allPrs.find(p => prLinks[p.id] === f.id || (p.prdItems || []).includes(f.id));
|
|
1345
|
+
return `- **${f.id}**: ${f.name} [status: ${wi?.status || f.status}]${pr ? ` (PR: ${pr.id}, branch: \`${pr.branch}\`)` : ''}`;
|
|
1346
|
+
}).join('\n');
|
|
1347
|
+
|
|
1348
|
+
const planContent = safeRead(path.join(PLANS_DIR, plan.source_plan));
|
|
1349
|
+
if (planContent) {
|
|
1350
|
+
const projectName = plan.project || file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
1351
|
+
const allProjects = getProjects(config);
|
|
1352
|
+
const targetProject = allProjects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) || allProjects[0];
|
|
1353
|
+
if (targetProject) {
|
|
1354
|
+
const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
1355
|
+
let queued = false;
|
|
1356
|
+
mutateJsonFileLocked(centralWiPath, items => {
|
|
1357
|
+
if (!Array.isArray(items)) items = [];
|
|
1358
|
+
if (items.some(w => w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === plan.source_plan && (w.status === WI_STATUS.PENDING || w.status === WI_STATUS.DISPATCHED))) return items;
|
|
1359
|
+
items.push({
|
|
1360
|
+
id: 'W-' + shared.uid(),
|
|
1361
|
+
title: `Update PRD from revised plan: ${plan.source_plan}`,
|
|
1362
|
+
type: 'plan-to-prd',
|
|
1363
|
+
priority: 'high',
|
|
1364
|
+
description: `Plan file: plans/${plan.source_plan}\nPRD file: prd/${file}\n\n` +
|
|
1365
|
+
`Source plan was revised. Compare the updated plan against existing implementation and update the PRD.\n\n` +
|
|
1366
|
+
`**Existing implementation state:**\n${implContext}\n\n` +
|
|
1367
|
+
`**Rules for updating:**\n` +
|
|
1368
|
+
`- Items that are done and unchanged in the plan → keep status "done" (preserve their ID)\n` +
|
|
1369
|
+
`- Items that are done but modified in the plan (new requirements) → set status "updated" (engine will re-open)\n` +
|
|
1370
|
+
`- New items in the plan → set status "missing" (engine will materialize)\n` +
|
|
1371
|
+
`- Items removed from the plan → drop from PRD (engine will cancel pending WIs)\n` +
|
|
1372
|
+
`- Preserve all existing item IDs for unchanged/modified items — do NOT generate new IDs for them`,
|
|
1373
|
+
status: 'pending',
|
|
1374
|
+
created: ts(),
|
|
1375
|
+
createdBy: 'engine:plan-revision',
|
|
1376
|
+
project: targetProject.name,
|
|
1377
|
+
planFile: plan.source_plan,
|
|
1378
|
+
_existingPrdFile: file,
|
|
1379
|
+
});
|
|
1380
|
+
queued = true;
|
|
1381
|
+
return items;
|
|
1382
|
+
}, { defaultValue: [] });
|
|
1383
|
+
if (queued) log('info', `Queued diff-aware plan-to-prd update for ${plan.source_plan} (${doneWis.length} done, ${planWis.length} total items)`);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
} else {
|
|
1387
|
+
plan.planStale = true;
|
|
1388
|
+
log('info', `PRD ${file} flagged as stale (plan revised while ${prdStatus}, no done items) — user can re-execute from dashboard`);
|
|
1389
|
+
}
|
|
1390
|
+
} else if (prdStatus === PLAN_STATUS.PAUSED) {
|
|
1330
1391
|
plan.planStale = true;
|
|
1331
|
-
log('info', `PRD ${file} flagged as stale (plan revised while
|
|
1332
|
-
// Falls through to safeWrite below (writes planStale + sourcePlanModifiedAt together)
|
|
1392
|
+
log('info', `PRD ${file} flagged as stale (plan revised while paused) — user can re-execute from dashboard`);
|
|
1333
1393
|
}
|
|
1334
1394
|
|
|
1335
1395
|
// Awaiting-approval PRDs: auto-regenerate (no work started yet, safe to replace)
|
|
1336
1396
|
if (prdStatus === 'awaiting-approval') {
|
|
1337
1397
|
log('info', `PRD ${file} invalidated (was awaiting-approval) — queuing regeneration from revised plan`);
|
|
1338
1398
|
|
|
1339
|
-
|
|
1340
|
-
const completedStatuses = new Set(['done', 'in-pr', 'implemented']); // in-pr kept for backward compat
|
|
1399
|
+
const completedStatuses = new Set(['done', 'in-pr', 'implemented']);
|
|
1341
1400
|
const completedItems = (plan.missing_features || [])
|
|
1342
1401
|
.filter(f => completedStatuses.has(f.status))
|
|
1343
1402
|
.map(f => ({ id: f.id, name: f.name, status: f.status }));
|
|
@@ -1346,10 +1405,8 @@ function materializePlansAsWorkItems(config) {
|
|
|
1346
1405
|
? `\nPreviously completed items (preserve their status in the new PRD):\n${completedItems.map(i => `- ${i.id}: ${i.name} [${i.status}]`).join('\n')}`
|
|
1347
1406
|
: '';
|
|
1348
1407
|
|
|
1349
|
-
// Delete old PRD — agent will write replacement at same path
|
|
1350
1408
|
try { fs.unlinkSync(path.join(PRD_DIR, file)); } catch { /* cleanup */ }
|
|
1351
1409
|
|
|
1352
|
-
// Queue plan-to-prd regeneration
|
|
1353
1410
|
const planContent = safeRead(path.join(PLANS_DIR, plan.source_plan));
|
|
1354
1411
|
if (planContent) {
|
|
1355
1412
|
const projectName = plan.project || file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
@@ -1357,12 +1414,11 @@ function materializePlansAsWorkItems(config) {
|
|
|
1357
1414
|
const targetProject = allProjects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) || allProjects[0];
|
|
1358
1415
|
if (targetProject) {
|
|
1359
1416
|
const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
centralItems.push({
|
|
1417
|
+
let queued = false;
|
|
1418
|
+
mutateJsonFileLocked(centralWiPath, items => {
|
|
1419
|
+
if (!Array.isArray(items)) items = [];
|
|
1420
|
+
if (items.some(w => w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === plan.source_plan && (w.status === WI_STATUS.PENDING || w.status === WI_STATUS.DISPATCHED))) return items;
|
|
1421
|
+
items.push({
|
|
1366
1422
|
id: 'W-' + shared.uid(),
|
|
1367
1423
|
title: `Regenerate PRD from revised plan: ${plan.source_plan}`,
|
|
1368
1424
|
type: 'plan-to-prd',
|
|
@@ -1374,9 +1430,10 @@ function materializePlansAsWorkItems(config) {
|
|
|
1374
1430
|
project: targetProject.name,
|
|
1375
1431
|
planFile: plan.source_plan,
|
|
1376
1432
|
});
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1433
|
+
queued = true;
|
|
1434
|
+
return items;
|
|
1435
|
+
}, { defaultValue: [] });
|
|
1436
|
+
if (queued) log('info', `Queued plan-to-prd regeneration for revised plan ${plan.source_plan} (${completedItems.length} completed items to carry over)`);
|
|
1380
1437
|
}
|
|
1381
1438
|
}
|
|
1382
1439
|
continue; // Old PRD deleted — skip safeWrite below
|
|
@@ -1415,7 +1472,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
1415
1472
|
// No project found — use central work-items.json (engine works without projects)
|
|
1416
1473
|
const useCentral = !defaultProject;
|
|
1417
1474
|
|
|
1418
|
-
const statusFilter = ['missing', 'planned'];
|
|
1475
|
+
const statusFilter = ['missing', 'planned', 'updated'];
|
|
1419
1476
|
// Also materialize in-pr/done items that never got a work item (race with PR status sync)
|
|
1420
1477
|
const allExistingWiIds = new Set();
|
|
1421
1478
|
for (const w of queries.getWorkItems()) {
|
|
@@ -1466,32 +1523,53 @@ function materializePlansAsWorkItems(config) {
|
|
|
1466
1523
|
const wiPath = project ? projectWorkItemsPath(project) : path.join(MINIONS_DIR, 'work-items.json');
|
|
1467
1524
|
let created = 0;
|
|
1468
1525
|
const newlyCreatedIds = new Set(); // tracks IDs created in this pass for reconciliation scoping
|
|
1526
|
+
const deferredReopens = []; // cross-project re-opens executed after this lock releases
|
|
1469
1527
|
|
|
1470
1528
|
mutateWorkItems(wiPath, existingItems => {
|
|
1471
1529
|
for (const item of projItems) {
|
|
1530
|
+
// Re-open: PRD item set back to missing/planned/updated but work item is done → reset to pending
|
|
1531
|
+
const existingWi = existingItems.find(w => w.id === item.id);
|
|
1532
|
+
const shouldReopen = item.status === 'missing' || item.status === 'planned' || item.status === 'updated';
|
|
1533
|
+
if (existingWi && DONE_STATUSES.has(existingWi.status) && shouldReopen) {
|
|
1534
|
+
existingWi.status = WI_STATUS.PENDING;
|
|
1535
|
+
existingWi._reopened = true;
|
|
1536
|
+
const criteria = (item.acceptance_criteria || []).map(c => `- ${c}`).join('\n');
|
|
1537
|
+
existingWi.description = buildWiDescription(item, file);
|
|
1538
|
+
existingWi.title = `Implement: ${item.name}`;
|
|
1539
|
+
created++;
|
|
1540
|
+
log('info', `Re-opened work item ${item.id} (PRD item set back to ${item.status}) — will dispatch to existing branch`);
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1472
1544
|
// Skip if already materialized — work item ID = PRD item ID, check all projects
|
|
1473
|
-
let alreadyExists =
|
|
1545
|
+
let alreadyExists = !!existingWi;
|
|
1474
1546
|
if (!alreadyExists) {
|
|
1475
1547
|
for (const p of allProjects) {
|
|
1476
1548
|
if (p.name === projName) continue;
|
|
1477
1549
|
const otherItems = safeJson(projectWorkItemsPath(p)) || [];
|
|
1478
|
-
|
|
1550
|
+
const otherWi = otherItems.find(w => w.id === item.id);
|
|
1551
|
+
if (otherWi) {
|
|
1552
|
+
if (DONE_STATUSES.has(otherWi.status) && shouldReopen) {
|
|
1553
|
+
deferredReopens.push({ itemId: item.id, projectName: p.name, item });
|
|
1554
|
+
created++;
|
|
1555
|
+
}
|
|
1556
|
+
alreadyExists = true; break;
|
|
1557
|
+
}
|
|
1479
1558
|
}
|
|
1480
1559
|
}
|
|
1481
1560
|
if (alreadyExists) continue;
|
|
1482
1561
|
// Skip items involved in dependency cycles
|
|
1483
1562
|
if (cycleSet.has(item.id)) continue;
|
|
1484
1563
|
|
|
1485
|
-
const id = item.id;
|
|
1564
|
+
const id = item.id;
|
|
1486
1565
|
const complexity = item.estimated_complexity || 'medium';
|
|
1487
|
-
const criteria = (item.acceptance_criteria || []).map(c => `- ${c}`).join('\n');
|
|
1488
1566
|
|
|
1489
1567
|
const newItem = {
|
|
1490
1568
|
id,
|
|
1491
1569
|
title: `Implement: ${item.name}`,
|
|
1492
1570
|
type: complexity === 'large' ? 'implement:large' : 'implement',
|
|
1493
1571
|
priority: item.priority || 'medium',
|
|
1494
|
-
description:
|
|
1572
|
+
description: buildWiDescription(item, file),
|
|
1495
1573
|
status: 'pending',
|
|
1496
1574
|
created: ts(),
|
|
1497
1575
|
createdBy: 'engine:plan-discovery',
|
|
@@ -1530,6 +1608,24 @@ function materializePlansAsWorkItems(config) {
|
|
|
1530
1608
|
log('info', `Plan discovery: created ${created} work item(s) from ${file} → ${projName}`);
|
|
1531
1609
|
}
|
|
1532
1610
|
});
|
|
1611
|
+
|
|
1612
|
+
// Process cross-project re-opens outside the lock (no nested locks)
|
|
1613
|
+
for (const { itemId, projectName: rProjName, item: rItem } of deferredReopens) {
|
|
1614
|
+
const rProject = allProjects.find(p => p.name === rProjName);
|
|
1615
|
+
if (!rProject) continue;
|
|
1616
|
+
const rPath = projectWorkItemsPath(rProject);
|
|
1617
|
+
mutateWorkItems(rPath, items => {
|
|
1618
|
+
const target = items.find(w => w.id === itemId);
|
|
1619
|
+
if (target && DONE_STATUSES.has(target.status)) {
|
|
1620
|
+
target.status = WI_STATUS.PENDING;
|
|
1621
|
+
target._reopened = true;
|
|
1622
|
+
target.description = buildWiDescription(rItem, file);
|
|
1623
|
+
target.title = `Implement: ${rItem.name}`;
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
log('info', `Re-opened work item ${itemId} in ${rProjName} (cross-project, PRD item set to ${rItem.status})`);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1533
1629
|
totalCreated += created;
|
|
1534
1630
|
}
|
|
1535
1631
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.818",
|
|
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/plan-to-prd.md
CHANGED
|
@@ -91,6 +91,20 @@ Rules for items:
|
|
|
91
91
|
- Include `acceptance_criteria` so reviewers know when it's done
|
|
92
92
|
- Aim for 5-25 items depending on plan scope. If more than 25, group related work
|
|
93
93
|
|
|
94
|
+
## Updating an Existing PRD
|
|
95
|
+
|
|
96
|
+
If the task description mentions **"existing implementation state"**, you are updating an existing PRD, not creating one from scratch. The plan was revised and you need to produce an updated PRD that reflects the changes while preserving existing work.
|
|
97
|
+
|
|
98
|
+
**Rules for diff-aware updates:**
|
|
99
|
+
- **Read the existing PRD file first** — it's at `{{team_root}}/prd/{{prd_filename}}`
|
|
100
|
+
- **Preserve item IDs** — do NOT generate new IDs for items that already exist. The engine maps work items by ID.
|
|
101
|
+
- **Done + unchanged** → keep `"status": "done"` and the same ID (no work dispatched)
|
|
102
|
+
- **Done + modified** (plan added requirements/scope) → set `"status": "updated"` with same ID (engine re-opens the work item and dispatches to existing branch)
|
|
103
|
+
- **New items** in the updated plan → generate new `P-<uuid>` IDs, set `"status": "missing"`
|
|
104
|
+
- **Removed items** (in old PRD but not in updated plan) → drop from the PRD entirely (engine cancels pending work items)
|
|
105
|
+
- **Pending/failed items** → reset to `"missing"` with updated description
|
|
106
|
+
- Preserve `branch_strategy`, `feature_branch`, `project`, and other plan-level fields from the existing PRD unless the plan explicitly changes them
|
|
107
|
+
|
|
94
108
|
## Important
|
|
95
109
|
|
|
96
110
|
- Write ONLY the single `.json` PRD file to `{{team_root}}/prd/` — do NOT write any `.md` files there
|
|
@@ -10,14 +10,14 @@ Your context window may be compacted or summarized mid-task by Claude's automati
|
|
|
10
10
|
- Do NOT remove worktrees — the engine handles cleanup automatically.
|
|
11
11
|
- Do NOT checkout branches in the main working tree — use worktrees or `git diff`/`git show`.
|
|
12
12
|
- Read `notes.md` for team rules and decisions before starting.
|
|
13
|
-
- **Check
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
This avoids duplicating research another agent already completed.
|
|
13
|
+
- **Check team memory first, then look outside.** Before researching from scratch, check what the team already knows — in this order:
|
|
14
|
+
1. `pinned.md` — critical context flagged by the human teammate (READ FIRST)
|
|
15
|
+
2. `knowledge/` — categorized KB entries (architecture, conventions, build-reports, reviews)
|
|
16
|
+
3. `notes.md` — consolidated team knowledge, decisions, and context
|
|
17
|
+
4. `notes/inbox/` — recent agent findings not yet consolidated
|
|
18
|
+
5. Previous agent output in `agents/*/live-output.log` for related tasks
|
|
19
|
+
6. Work item descriptions and `resultSummary` for prior completed work on the same topic
|
|
20
|
+
Only after exhausting team memory should you look outside (web search, codebase exploration, external docs). This avoids duplicating research another agent already completed and ensures team decisions are respected.
|
|
21
21
|
- If you discover a repeatable workflow, output it as a fenced skill block. The engine auto-extracts it to `~/.claude/skills/<name>/SKILL.md`. Required format:
|
|
22
22
|
````
|
|
23
23
|
```skill
|
package/playbooks/verify.md
CHANGED
|
@@ -92,30 +92,39 @@ For each project that has changes, create a single **aggregate PR** that combine
|
|
|
92
92
|
|
|
93
93
|
For each project worktree:
|
|
94
94
|
|
|
95
|
-
1.
|
|
95
|
+
1. **Check for an existing E2E branch/PR first** — a prior verify run may have already created one:
|
|
96
96
|
```bash
|
|
97
97
|
cd <worktree-path>
|
|
98
|
-
git
|
|
99
|
-
|
|
98
|
+
git fetch origin
|
|
99
|
+
# Check if the E2E branch already exists on remote
|
|
100
|
+
git ls-remote --heads origin e2e/{{plan_slug}}
|
|
100
101
|
```
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
- **
|
|
112
|
-
|
|
113
|
-
|
|
102
|
+
- If the branch **already exists**: check out the existing branch (`git checkout e2e/{{plan_slug}}`), reset it to your current worktree state, and force-push to update it. Then check if a PR already exists for this branch — if so, **update that PR** instead of creating a new one.
|
|
103
|
+
- If the branch **does not exist**: create it fresh:
|
|
104
|
+
```bash
|
|
105
|
+
git checkout -b e2e/{{plan_slug}}
|
|
106
|
+
git push origin e2e/{{plan_slug}}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
2. **Check for an existing E2E PR** before creating a new one:
|
|
110
|
+
- For GitHub: `gh pr list --head e2e/{{plan_slug}} --state open`
|
|
111
|
+
- For ADO: search for PRs with source branch `e2e/{{plan_slug}}`
|
|
112
|
+
- If found, **update the existing PR** description with latest build/test results. Do NOT create a duplicate.
|
|
113
|
+
- If not found, create a new PR targeting the project's main branch:
|
|
114
|
+
- **Title:** `[E2E] <plan summary>`
|
|
115
|
+
- **Description:** Include the plan summary, list of all individual PRs merged, build/test status from Step 3, and link to the testing guide
|
|
116
|
+
- **Target branch:** the project's main branch (e.g., `main` or `master`)
|
|
117
|
+
- **Do NOT auto-complete** — this is for review only
|
|
118
|
+
- **Mark as draft** if the option is available
|
|
119
|
+
|
|
120
|
+
3. Add the E2E PR to the project's `.minions/pull-requests.json` (skip if it's already tracked):
|
|
114
121
|
```bash
|
|
115
122
|
node -e "
|
|
116
123
|
const fs = require('fs');
|
|
117
124
|
const p = '<project-path>/.minions/pull-requests.json';
|
|
118
125
|
const prs = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
126
|
+
// Skip if already tracked
|
|
127
|
+
if (prs.some(pr => pr.branch === 'e2e/{{plan_slug}}')) process.exit(0);
|
|
119
128
|
prs.push({
|
|
120
129
|
id: 'PR-<number>',
|
|
121
130
|
title: '[E2E] <plan summary>',
|