@yemi33/minions 0.1.94 → 0.1.96
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 +28 -0
- package/dashboard/js/detail-panel.js +4 -4
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/modal-qa.js +10 -2
- package/dashboard/js/render-agents.js +2 -2
- package/dashboard/js/render-inbox.js +1 -1
- package/dashboard/js/render-meetings.js +106 -3
- package/dashboard/js/render-pinned.js +1 -1
- package/dashboard/js/render-plans.js +59 -12
- package/dashboard/js/render-work-items.js +13 -7
- package/dashboard/js/utils.js +115 -1
- package/dashboard.js +46 -1
- package/engine/ado.js +3 -3
- package/engine/github.js +2 -2
- package/engine/queries.js +10 -0
- package/engine.js +55 -37
- package/package.json +1 -1
- package/playbooks/meeting-conclude.md +42 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.96 (2026-04-01)
|
|
4
|
+
|
|
5
|
+
### Engine
|
|
6
|
+
- engine.js
|
|
7
|
+
- engine/ado.js
|
|
8
|
+
- engine/github.js
|
|
9
|
+
- engine/queries.js
|
|
10
|
+
|
|
11
|
+
### Dashboard
|
|
12
|
+
- dashboard.js
|
|
13
|
+
- dashboard/js/detail-panel.js
|
|
14
|
+
- dashboard/js/live-stream.js
|
|
15
|
+
- dashboard/js/modal-qa.js
|
|
16
|
+
- dashboard/js/render-agents.js
|
|
17
|
+
- dashboard/js/render-inbox.js
|
|
18
|
+
- dashboard/js/render-meetings.js
|
|
19
|
+
- dashboard/js/render-pinned.js
|
|
20
|
+
- dashboard/js/render-work-items.js
|
|
21
|
+
- dashboard/js/utils.js
|
|
22
|
+
|
|
23
|
+
### Playbooks
|
|
24
|
+
- meeting-conclude.md
|
|
25
|
+
|
|
26
|
+
## 0.1.95 (2026-04-01)
|
|
27
|
+
|
|
28
|
+
### Dashboard
|
|
29
|
+
- dashboard/js/render-plans.js
|
|
30
|
+
|
|
3
31
|
## 0.1.94 (2026-04-01)
|
|
4
32
|
|
|
5
33
|
### Dashboard
|
|
@@ -44,21 +44,21 @@ function renderDetailContent(detail, tab) {
|
|
|
44
44
|
if (detail.statusData.completed_at) html += 'Completed: ' + detail.statusData.completed_at + '\n';
|
|
45
45
|
html += '</div>';
|
|
46
46
|
if (detail.statusData.resultSummary) {
|
|
47
|
-
html += '<h4>Last Result</h4><div class="section" style="border-left:3px solid var(--green);padding-left:12px">' +
|
|
47
|
+
html += '<h4>Last Result</h4><div class="section" style="border-left:3px solid var(--green);padding-left:12px">' + renderMd(detail.statusData.resultSummary) + '</div>';
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
if (detail.inboxContents && detail.inboxContents.length > 0) {
|
|
52
52
|
html += '<h4>Notes & Findings (' + detail.inboxContents.length + ')</h4>';
|
|
53
53
|
detail.inboxContents.forEach(item => {
|
|
54
|
-
html += '<div class="section"><strong style="color:var(--purple)">' + escHtml(item.name) + '</strong
|
|
54
|
+
html += '<div class="section"><strong style="color:var(--purple)">' + escHtml(item.name) + '</strong><div style="margin-top:4px">' + renderMd(item.content) + '</div></div>';
|
|
55
55
|
});
|
|
56
56
|
} else {
|
|
57
57
|
html += '<h4>Notes & Findings</h4><div class="section" style="color:var(--muted);font-style:italic">No notes or findings written yet.</div>';
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
if (detail.outputLog) {
|
|
61
|
-
html += '<h4>Latest Output</h4><div class="section">' +
|
|
61
|
+
html += '<h4>Latest Output</h4><div class="section">' + renderMd(detail.outputLog) + '</div>';
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
el.innerHTML = html;
|
|
@@ -79,7 +79,7 @@ function renderDetailContent(detail, tab) {
|
|
|
79
79
|
'</div>';
|
|
80
80
|
startLiveStream(currentAgentId);
|
|
81
81
|
} else if (tab === 'charter') {
|
|
82
|
-
el.innerHTML = '<div class="section">' +
|
|
82
|
+
el.innerHTML = '<div class="section">' + renderMd(detail.charter || 'No charter found.') + '</div>';
|
|
83
83
|
} else if (tab === 'history') {
|
|
84
84
|
let html = '';
|
|
85
85
|
// Recent dispatch results
|
|
@@ -14,7 +14,7 @@ function renderLiveChatMessage(raw) {
|
|
|
14
14
|
el.innerHTML += '<div style="font-size:10px;color:var(--muted);padding:2px 8px;font-style:italic">\u{1F4AD} Thinking...</div>';
|
|
15
15
|
}
|
|
16
16
|
if (block.type === 'text' && block.text) {
|
|
17
|
-
el.innerHTML += '<div style="background:var(--surface2);padding:8px 12px;border-radius:12px 12px 12px 2px;max-width:90%;margin:4px 0;font-size:12px;
|
|
17
|
+
el.innerHTML += '<div style="background:var(--surface2);padding:8px 12px;border-radius:12px 12px 12px 2px;max-width:90%;margin:4px 0;font-size:12px;word-break:break-word">' + renderMd(block.text) + '</div>';
|
|
18
18
|
}
|
|
19
19
|
if (block.type === 'tool_use') {
|
|
20
20
|
el.innerHTML += '<div style="background:var(--surface);border:1px solid var(--border);padding:4px 8px;border-radius:4px;margin:2px 0;font-size:10px;color:var(--muted);cursor:pointer" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display===\'none\'?\'block\':\'none\'">' +
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -201,7 +201,7 @@ async function _processQaMessage(message, selection) {
|
|
|
201
201
|
const suffix = data.edited ? '\n\n\u2713 Document saved.' : '';
|
|
202
202
|
const qaElapsed = Math.round((Date.now() - qaStartTime) / 1000);
|
|
203
203
|
const qaTimeLabel = '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right">' + qaElapsed + 's</div>';
|
|
204
|
-
thread.innerHTML += '<div class="modal-qa-a" style="border-left-color:' + borderColor + '">' + llmCopyBtn() +
|
|
204
|
+
thread.innerHTML += '<div class="modal-qa-a" style="border-left-color:' + borderColor + '">' + llmCopyBtn() + renderMd(data.answer + suffix) + qaTimeLabel + '</div>';
|
|
205
205
|
|
|
206
206
|
// Track conversation history
|
|
207
207
|
_qaHistory.push({ role: 'user', text: message });
|
|
@@ -215,7 +215,15 @@ async function _processQaMessage(message, selection) {
|
|
|
215
215
|
// Refresh modal body if document was edited
|
|
216
216
|
if (data.edited && data.content) {
|
|
217
217
|
const display = data.content.replace(/^---[\s\S]*?---\n*/m, '');
|
|
218
|
-
|
|
218
|
+
const isJson = capturedFilePath && capturedFilePath.endsWith('.json');
|
|
219
|
+
const body = document.getElementById('modal-body');
|
|
220
|
+
if (isJson) {
|
|
221
|
+
body.textContent = display;
|
|
222
|
+
} else {
|
|
223
|
+
body.innerHTML = '<div style="font-size:12px;line-height:1.6">' + renderMd(display) + '</div>';
|
|
224
|
+
body.style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
225
|
+
body.style.whiteSpace = 'normal';
|
|
226
|
+
}
|
|
219
227
|
_modalDocContext.content = display;
|
|
220
228
|
}
|
|
221
229
|
|
|
@@ -11,7 +11,7 @@ function renderAgents(agents) {
|
|
|
11
11
|
</div>
|
|
12
12
|
<div class="agent-role">${a.role}</div>
|
|
13
13
|
<div class="agent-action" title="${escHtml(a.lastAction)}">${escHtml(a.lastAction)}</div>
|
|
14
|
-
${a.resultSummary ? `<div class="agent-result" title="${escHtml(a.resultSummary)}">${
|
|
14
|
+
${a.resultSummary ? `<div class="agent-result" title="${escHtml(a.resultSummary)}">${renderMd(a.resultSummary.slice(0, 200))}${a.resultSummary.length > 200 ? '...' : ''}</div>` : ''}
|
|
15
15
|
</div>
|
|
16
16
|
`).join('');
|
|
17
17
|
}
|
|
@@ -29,7 +29,7 @@ async function openAgentDetail(id) {
|
|
|
29
29
|
document.getElementById('detail-status-line').innerHTML =
|
|
30
30
|
'<span class="status-badge ' + badgeClass + '">' + agent.status.toUpperCase() + '</span> ' +
|
|
31
31
|
'<span style="color:var(--muted)">' + escHtml(agent.lastAction) + '</span>' +
|
|
32
|
-
(agent.resultSummary ? '<div style="margin-top:4px;font-size:11px;color:var(--text);line-height:1.4">' +
|
|
32
|
+
(agent.resultSummary ? '<div style="margin-top:4px;font-size:11px;color:var(--text);line-height:1.4">' + renderMd(agent.resultSummary.slice(0, 300)) + '</div>' : '');
|
|
33
33
|
|
|
34
34
|
try {
|
|
35
35
|
const detail = await fetch('/api/agent/' + id).then(r => r.json());
|
|
@@ -55,7 +55,7 @@ function renderNotes(notes) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
if (!content || !content.trim()) { el.innerHTML = '<p class="empty">No team notes yet.</p>'; return; }
|
|
58
|
-
el.innerHTML = '<div class="notes-preview" onclick="openNotesModal()" title="Click to expand">' +
|
|
58
|
+
el.innerHTML = '<div class="notes-preview" onclick="openNotesModal()" title="Click to expand">' + renderMd(content) + '</div>';
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function openNotesModal() {
|
|
@@ -136,10 +136,16 @@ function openMeetingDetail(id) {
|
|
|
136
136
|
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
|
|
137
137
|
'</div>';
|
|
138
138
|
} else if (m.status === 'completed') {
|
|
139
|
-
|
|
139
|
+
const linkedPlan = _findLinkedPlan(m);
|
|
140
|
+
html += '<div style="display:flex;gap:8px;flex-wrap:wrap;border-top:1px solid var(--border);padding-top:8px">' +
|
|
141
|
+
(linkedPlan
|
|
142
|
+
? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--blue);border-color:var(--blue)" onclick="_viewPlanWithBack(\'' + escHtml(linkedPlan.file) + '\',\'' + escHtml(m.id) + '\')">View Plan</button>' +
|
|
143
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);border-color:var(--green)" onclick="_createPlanFromMeeting(\'' + escHtml(m.id) + '\',this)">New Plan</button>'
|
|
144
|
+
: '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);border-color:var(--green)" onclick="_createPlanFromMeeting(\'' + escHtml(m.id) + '\',this)">Create Plan from Meeting</button>') +
|
|
140
145
|
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="_archiveMeeting(\'' + escHtml(m.id) + '\')">Archive</button>' +
|
|
141
146
|
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
|
|
142
|
-
'</div>'
|
|
147
|
+
'</div>' +
|
|
148
|
+
'<div style="font-size:9px;color:var(--muted);margin-top:4px">Use the Q&A below to discuss action items' + (linkedPlan ? '' : ', then create a plan to execute them') + '.</div>';
|
|
143
149
|
} else {
|
|
144
150
|
html += '<div style="display:flex;gap:8px;border-top:1px solid var(--border);padding-top:8px">' +
|
|
145
151
|
'<input id="meeting-note-input" type="text" placeholder="Add context for all agents..." style="flex:1;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:12px" onkeydown="if(event.key===\'Enter\')_submitMeetingNote(\'' + escHtml(m.id) + '\')">' +
|
|
@@ -165,7 +171,8 @@ function openMeetingDetail(id) {
|
|
|
165
171
|
).join('\n\n---\n\n');
|
|
166
172
|
const meetingDoc = '# Meeting: ' + m.title + '\n\n**Agenda:** ' + m.agenda + '\n\n' + transcript;
|
|
167
173
|
_modalDocContext = { title: 'Meeting: ' + m.title, content: meetingDoc, selection: '' };
|
|
168
|
-
|
|
174
|
+
// Completed/archived meetings: read-only Q&A (no file editing to avoid corrupting JSON)
|
|
175
|
+
_modalFilePath = (m.status === 'completed' || m.status === 'archived') ? null : 'meetings/' + m.id + '.json';
|
|
169
176
|
try { showModalQa(); } catch { /* expected if QA not loaded */ }
|
|
170
177
|
|
|
171
178
|
document.getElementById('modal').classList.add('open');
|
|
@@ -277,6 +284,102 @@ async function _unarchiveMeeting(id) {
|
|
|
277
284
|
} catch (e) { alert('Error: ' + e.message); }
|
|
278
285
|
}
|
|
279
286
|
|
|
287
|
+
function _viewPlanWithBack(file, meetingId) {
|
|
288
|
+
planView(file);
|
|
289
|
+
// After modal opens, prepend a back button to return to meeting
|
|
290
|
+
setTimeout(function() {
|
|
291
|
+
const title = document.getElementById('modal-title');
|
|
292
|
+
if (title && !title.querySelector('.mtg-back-btn')) {
|
|
293
|
+
const back = document.createElement('button');
|
|
294
|
+
back.className = 'pr-pager-btn mtg-back-btn';
|
|
295
|
+
back.style.cssText = 'font-size:9px;padding:2px 8px;margin-right:8px;vertical-align:middle';
|
|
296
|
+
back.textContent = '\u2190 Back to Meeting';
|
|
297
|
+
back.onclick = function() { openMeetingDetail(meetingId); };
|
|
298
|
+
title.prepend(back);
|
|
299
|
+
}
|
|
300
|
+
}, 100);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function _findLinkedPlan(meeting) {
|
|
304
|
+
if (!meeting?.conclusion?.content) return null;
|
|
305
|
+
const match = meeting.conclusion.content.match(/plans\/([\w-]+\.md)/);
|
|
306
|
+
if (!match) return null;
|
|
307
|
+
const file = match[1];
|
|
308
|
+
const plans = window._lastStatus?.plans || [];
|
|
309
|
+
return plans.find(function(p) { return p.file === file; }) || { file, summary: file };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function _createPlanFromMeeting(id, btn) {
|
|
313
|
+
if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Checking...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
|
|
314
|
+
function resetBtn() { if (btn) { btn.textContent = btn.dataset.origText || 'Create Plan'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } }
|
|
315
|
+
try {
|
|
316
|
+
const res = await fetch('/api/meetings/' + encodeURIComponent(id));
|
|
317
|
+
const data = await res.json();
|
|
318
|
+
if (!data.meeting) { resetBtn(); alert('Meeting not found'); return; }
|
|
319
|
+
const m = data.meeting;
|
|
320
|
+
|
|
321
|
+
// Check if a plan already exists for this meeting
|
|
322
|
+
const existing = _findLinkedPlan(m);
|
|
323
|
+
if (existing) {
|
|
324
|
+
resetBtn();
|
|
325
|
+
if (!confirm('A plan already exists: "' + existing.summary + '"\n\nCreate a new one anyway?')) return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (btn) btn.textContent = 'Generating plan...';
|
|
329
|
+
|
|
330
|
+
// Use doc-chat to generate a structured plan from the meeting
|
|
331
|
+
const transcript = (m.transcript || []).map(function(t) {
|
|
332
|
+
return '### ' + t.agent + ' (' + t.type + ', Round ' + t.round + ')\n\n' + (t.content || '');
|
|
333
|
+
}).join('\n\n---\n\n');
|
|
334
|
+
const meetingDoc = '# Meeting: ' + m.title + '\n\n**Agenda:** ' + m.agenda + '\n\n' + transcript;
|
|
335
|
+
|
|
336
|
+
// Include Q&A thread if present
|
|
337
|
+
let humanContext = '';
|
|
338
|
+
const qaThread = document.getElementById('modal-qa-thread');
|
|
339
|
+
if (qaThread) {
|
|
340
|
+
const qaText = qaThread.innerText.trim();
|
|
341
|
+
if (qaText.length > 20) humanContext = '\n\n## Human Discussion\n\n' + qaText.slice(0, 3000);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const genRes = await fetch('/api/doc-chat', {
|
|
345
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
346
|
+
body: JSON.stringify({
|
|
347
|
+
message: 'Create an actionable implementation plan from this meeting. Extract concrete action items from the conclusion and debates. For each item include: what to do, which files/areas to change, priority (high/medium/low), and estimated complexity (small/medium/large). Structure it as a plan ready for execution. Do NOT include preamble — start with the plan title.' + humanContext,
|
|
348
|
+
document: meetingDoc,
|
|
349
|
+
title: 'Meeting: ' + m.title,
|
|
350
|
+
})
|
|
351
|
+
});
|
|
352
|
+
const genData = await genRes.json();
|
|
353
|
+
if (!genRes.ok || !genData.ok) { resetBtn(); alert('Failed to generate plan: ' + (genData.error || 'unknown')); return; }
|
|
354
|
+
|
|
355
|
+
const planContent = genData.answer || '';
|
|
356
|
+
const title = 'Meeting follow-up: ' + (m.title || id);
|
|
357
|
+
const planRes = await fetch('/api/plans/create', {
|
|
358
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
359
|
+
body: JSON.stringify({ title, content: planContent })
|
|
360
|
+
});
|
|
361
|
+
const planData = await planRes.json();
|
|
362
|
+
if (planRes.ok && planData.ok) {
|
|
363
|
+
showToast('cmd-toast', 'Plan created: ' + planData.file, true);
|
|
364
|
+
if (btn) {
|
|
365
|
+
btn.textContent = 'Plan created';
|
|
366
|
+
btn.style.color = 'var(--green)';
|
|
367
|
+
btn.style.borderColor = 'var(--green)';
|
|
368
|
+
btn.style.opacity = '1';
|
|
369
|
+
const viewLink = document.createElement('button');
|
|
370
|
+
viewLink.className = 'pr-pager-btn';
|
|
371
|
+
viewLink.style.cssText = 'font-size:9px;padding:2px 8px;color:var(--blue);border-color:var(--blue);pointer-events:auto';
|
|
372
|
+
viewLink.textContent = 'View Plan';
|
|
373
|
+
viewLink.onclick = function() { _viewPlanWithBack(planData.file, id); };
|
|
374
|
+
btn.parentElement.insertBefore(viewLink, btn.nextSibling);
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
resetBtn();
|
|
378
|
+
alert('Failed: ' + (planData.error || 'unknown'));
|
|
379
|
+
}
|
|
380
|
+
} catch (e) { resetBtn(); alert('Error: ' + e.message); }
|
|
381
|
+
}
|
|
382
|
+
|
|
280
383
|
async function _deleteMeeting(id) {
|
|
281
384
|
if (!confirm('Delete this meeting? This cannot be undone.')) return;
|
|
282
385
|
try {
|
|
@@ -15,7 +15,7 @@ function renderPinned(entries) {
|
|
|
15
15
|
'<strong style="font-size:var(--text-md)">' + escHtml(e.title) + '</strong>' +
|
|
16
16
|
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="removePinnedNote(\'' + escHtml(e.title) + '\')">Unpin</button>' +
|
|
17
17
|
'</div>' +
|
|
18
|
-
'<div style="font-size:var(--text-sm);color:var(--muted);margin-top:4px">' +
|
|
18
|
+
'<div style="font-size:var(--text-sm);color:var(--muted);margin-top:4px">' + renderMd(e.content.slice(0, 200)) + '</div>' +
|
|
19
19
|
'</div>'
|
|
20
20
|
).join('');
|
|
21
21
|
}
|
|
@@ -80,9 +80,14 @@ function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems) {
|
|
|
80
80
|
if (allDone && !hasActiveWork) return 'completed';
|
|
81
81
|
if (hasActiveWork || hasPendingPrd) return 'in-progress';
|
|
82
82
|
if (hasFailed && !hasActiveWork) return 'has-failures';
|
|
83
|
+
|
|
83
84
|
if (prdJsonStatus === 'awaiting-approval' && implementWi.length === 0) return 'awaiting-approval';
|
|
84
85
|
if (prdJsonStatus === 'approved' && implementWi.length === 0) return 'approved';
|
|
85
86
|
|
|
87
|
+
// plan-to-prd conversion done but no implement items and PRD is gone — truly completed
|
|
88
|
+
const prdConversionDone = wi.some(w => w.type === 'plan-to-prd' && w.status === 'done');
|
|
89
|
+
if (prdConversionDone && implementWi.length === 0) return 'completed';
|
|
90
|
+
|
|
86
91
|
return prdJsonStatus || 'active';
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -172,7 +177,14 @@ function renderPlans(plans) {
|
|
|
172
177
|
if (prdFile && p.format !== 'prd') {
|
|
173
178
|
const linkedPrd = plans.find(pp => pp.file === prdFile && pp.format === 'prd');
|
|
174
179
|
if (linkedPrd) prdJsonStatus = linkedPrd.status || prdJsonStatus;
|
|
180
|
+
// If the linked PRD was archived, treat the .md plan as completed
|
|
181
|
+
else if (!linkedPrd) {
|
|
182
|
+
const archivedPrd = archivedPlans.find(pp => pp.file === prdFile && pp.format === 'prd');
|
|
183
|
+
if (archivedPrd) prdJsonStatus = 'completed';
|
|
184
|
+
}
|
|
175
185
|
}
|
|
186
|
+
// 'converted' means plan-to-PRD succeeded — treat as 'approved' for status derivation
|
|
187
|
+
if (prdJsonStatus === 'converted') prdJsonStatus = prdFile ? 'approved' : 'completed';
|
|
176
188
|
|
|
177
189
|
// Single source of truth: derive status from work items
|
|
178
190
|
const effectiveStatus = isArchived ? 'completed' : derivePlanStatus(prdFile, p.file, prdJsonStatus, allWi);
|
|
@@ -180,7 +192,8 @@ function renderPlans(plans) {
|
|
|
180
192
|
const statusLabelsMap = {
|
|
181
193
|
'completed': 'Completed', 'in-progress': 'In Progress', 'paused': 'Paused',
|
|
182
194
|
'awaiting-approval': 'Awaiting Approval', 'approved': 'Approved', 'rejected': 'Rejected',
|
|
183
|
-
'revision-requested': 'Revision Requested', 'has-failures': 'Has Failures', 'active': 'Active'
|
|
195
|
+
'revision-requested': 'Revision Requested', 'has-failures': 'Has Failures', 'active': 'Active',
|
|
196
|
+
'converted': 'Converted to PRD', 'draft': 'Draft'
|
|
184
197
|
};
|
|
185
198
|
const label = statusLabelsMap[effectiveStatus] || effectiveStatus;
|
|
186
199
|
const needsAction = (effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused') && !isArchived;
|
|
@@ -222,6 +235,10 @@ function renderPlans(plans) {
|
|
|
222
235
|
: '';
|
|
223
236
|
const verifyBtn = showVerify ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green)" ' +
|
|
224
237
|
'onclick="event.stopPropagation();triggerVerify(\'' + escHtml(prdFile) + '\',this)">Verify</button>' : '';
|
|
238
|
+
const showArchive = !isArchived;
|
|
239
|
+
const archiveFile = prdFile || p.file;
|
|
240
|
+
const archiveBtn = showArchive ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" ' +
|
|
241
|
+
'onclick="event.stopPropagation();planArchive(\'' + escHtml(archiveFile) + '\',this)">Archive</button>' : '';
|
|
225
242
|
const deleteBtn = !isArchived ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red)" ' +
|
|
226
243
|
'onclick="event.stopPropagation();planDelete(\'' + escHtml(p.file) + '\')">Delete</button>' : '';
|
|
227
244
|
|
|
@@ -238,7 +255,7 @@ function renderPlans(plans) {
|
|
|
238
255
|
(p.updatedAt ? '<span title="Last updated: ' + p.updatedAt + '">Updated ' + timeAgo(p.updatedAt) + '</span>' : '') +
|
|
239
256
|
(p.completedAt ? '<span>' + p.completedAt.slice(0, 10) + '</span>' : '') +
|
|
240
257
|
(p.generatedBy ? '<span>by ' + escHtml(p.generatedBy) + '</span>' : '') +
|
|
241
|
-
executeBtn + pauseBtn + resumeBtn + verifyBtn + deleteBtn +
|
|
258
|
+
executeBtn + pauseBtn + resumeBtn + verifyBtn + archiveBtn + deleteBtn +
|
|
242
259
|
'</div>' +
|
|
243
260
|
'</div>' +
|
|
244
261
|
'</div>' +
|
|
@@ -432,11 +449,12 @@ async function planView(file) {
|
|
|
432
449
|
const isPaused = planStatus === 'awaiting-approval' || planStatus === 'paused';
|
|
433
450
|
// Check if work is in progress for this plan
|
|
434
451
|
const wi = window._lastWorkItems || [];
|
|
452
|
+
// Find the linked PRD for this .md plan (if any)
|
|
453
|
+
const linkedPrdFile = isMdPlan ? (window._lastStatus?.plans || []).find(p => p.sourcePlan === normalizedFile && p.format === 'prd')?.file : null;
|
|
435
454
|
const hasActiveWork = wi.some(w =>
|
|
436
455
|
(w.status === 'pending' || w.status === 'dispatched') &&
|
|
437
456
|
(w.planFile === normalizedFile || w.sourcePlan === normalizedFile ||
|
|
438
|
-
|
|
439
|
-
(isMdPlan && w.sourcePlan && w.sourcePlan.endsWith('.json')))
|
|
457
|
+
(linkedPrdFile && w.sourcePlan === linkedPrdFile))
|
|
440
458
|
);
|
|
441
459
|
const prdCompleted = wi.some(w => w.type === 'plan-to-prd' && w.status === 'done' && w.planFile === normalizedFile);
|
|
442
460
|
// Check if a PRD already exists for this plan (via plans list sourcePlan linkage)
|
|
@@ -454,9 +472,12 @@ async function planView(file) {
|
|
|
454
472
|
const modalVerifyBtn = isModalCompleted ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green)" ' +
|
|
455
473
|
'onclick="triggerVerify(\'' + escHtml(normalizedFile) + '\',this)">Verify</button>' : '';
|
|
456
474
|
|
|
475
|
+
const modalArchiveBtn = '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--muted)" ' +
|
|
476
|
+
'onclick="planArchive(\'' + escHtml(normalizedFile) + '\')">Archive</button>';
|
|
457
477
|
const lastModLabel = lastMod ? '<div style="font-size:10px;color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' + new Date(lastMod).toLocaleString() + '</div>' : '';
|
|
458
478
|
const actionBtns = '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' +
|
|
459
479
|
(modalCompletedLabel || '') + (modalInProgressLabel || '') + (modalExecuteBtn || '') + (modalPauseBtn || '') + (modalResumeBtn || '') + (modalVerifyBtn || '') +
|
|
480
|
+
' ' + modalArchiveBtn +
|
|
460
481
|
' <button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--red)" ' +
|
|
461
482
|
'onclick="planDelete(\'' + escHtml(normalizedFile) + '\')">Delete</button>' +
|
|
462
483
|
'</div>';
|
|
@@ -516,23 +537,27 @@ async function planDelete(file) {
|
|
|
516
537
|
} catch (e) { alert('Error: ' + e.message); }
|
|
517
538
|
}
|
|
518
539
|
|
|
519
|
-
async function planArchive(file) {
|
|
520
|
-
if (
|
|
540
|
+
async function planArchive(file, btn) {
|
|
541
|
+
if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Archiving...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
|
|
542
|
+
function resetBtn() { if (btn) { btn.textContent = btn.dataset.origText || 'Archive'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } }
|
|
521
543
|
try {
|
|
522
544
|
const res = await fetch('/api/plans/archive', {
|
|
523
545
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
524
546
|
body: JSON.stringify({ file })
|
|
525
547
|
});
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
548
|
+
const ct = res.headers.get('content-type') || '';
|
|
549
|
+
if (!ct.includes('json')) { resetBtn(); alert('Archive failed — dashboard may need a restart'); return; }
|
|
550
|
+
const d = await res.json().catch(() => ({}));
|
|
551
|
+
if (res.ok && d.ok) {
|
|
552
|
+
try { closeModal(); } catch { /* may not be open */ }
|
|
553
|
+
showToast('cmd-toast', 'Archived' + (file.endsWith('.json') ? ' PRD and source plan' : ''), true);
|
|
529
554
|
refreshPlans();
|
|
530
555
|
refresh();
|
|
531
556
|
} else {
|
|
532
|
-
|
|
557
|
+
resetBtn();
|
|
533
558
|
alert('Archive failed: ' + (d.error || 'unknown'));
|
|
534
559
|
}
|
|
535
|
-
} catch (e) { alert('Error: ' + e.message); }
|
|
560
|
+
} catch (e) { resetBtn(); alert('Error: ' + e.message); }
|
|
536
561
|
}
|
|
537
562
|
|
|
538
563
|
async function planPause(file, btn) {
|
|
@@ -674,4 +699,26 @@ async function triggerVerify(file, btn) {
|
|
|
674
699
|
} catch (e) { if (btn) { btn.textContent = btn.dataset.origText || 'Verify'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } alert('Error: ' + e.message); }
|
|
675
700
|
}
|
|
676
701
|
|
|
677
|
-
|
|
702
|
+
async function planUnarchive(file, btn) {
|
|
703
|
+
if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Restoring...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
|
|
704
|
+
function resetBtn() { if (btn) { btn.textContent = btn.dataset.origText || 'Unarchive'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } }
|
|
705
|
+
try {
|
|
706
|
+
const res = await fetch('/api/plans/unarchive', {
|
|
707
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
708
|
+
body: JSON.stringify({ file })
|
|
709
|
+
});
|
|
710
|
+
const ct = res.headers.get('content-type') || '';
|
|
711
|
+
if (!ct.includes('json')) { resetBtn(); alert('Unarchive failed — dashboard may need a restart'); return; }
|
|
712
|
+
const d = await res.json().catch(() => ({}));
|
|
713
|
+
if (res.ok && d.ok) {
|
|
714
|
+
showToast('cmd-toast', 'Restored from archive', true);
|
|
715
|
+
refreshPlans();
|
|
716
|
+
refresh();
|
|
717
|
+
} else {
|
|
718
|
+
resetBtn();
|
|
719
|
+
alert('Unarchive failed: ' + (d.error || 'unknown'));
|
|
720
|
+
}
|
|
721
|
+
} catch (e) { resetBtn(); alert('Error: ' + e.message); }
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, renderPlans, openArchivedPlansModal, qaDisablePrdButtons, showPlanVersionActions, qaJustSave, planExecute, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planArchive, planUnarchive, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
let allWorkItems = [];
|
|
4
4
|
let wiPage = 0;
|
|
5
|
-
const WI_PER_PAGE =
|
|
5
|
+
const WI_PER_PAGE = 20;
|
|
6
6
|
|
|
7
7
|
function wiRow(item) {
|
|
8
8
|
const statusBadge = (s) => {
|
|
@@ -23,7 +23,7 @@ function wiRow(item) {
|
|
|
23
23
|
'<td>' + priBadge(item.priority) + '</td>' +
|
|
24
24
|
'<td>' + statusBadge(item.status || 'pending') +
|
|
25
25
|
(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' ? ' <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>' : '') +
|
|
26
|
+
(item.status === 'failed' ? ' <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 || '') + '\',this)">Retry</button>' : '') +
|
|
27
27
|
'</td>' +
|
|
28
28
|
'<td>' +
|
|
29
29
|
(item.completedAgents && item.completedAgents.length > 0
|
|
@@ -210,17 +210,23 @@ async function toggleWorkItemArchive() {
|
|
|
210
210
|
} catch (e) { el.innerHTML = '<p class="empty">Failed to load archive.</p>'; }
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
async function retryWorkItem(id, source) {
|
|
213
|
+
async function retryWorkItem(id, source, btn) {
|
|
214
|
+
if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Retrying...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
|
|
214
215
|
try {
|
|
215
216
|
const res = await fetch('/api/work-items/retry', {
|
|
216
217
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
217
218
|
body: JSON.stringify({ id, source: source || undefined })
|
|
218
219
|
});
|
|
219
|
-
if (res.ok) {
|
|
220
|
-
|
|
220
|
+
if (res.ok) {
|
|
221
|
+
showToast('cmd-toast', 'Work item ' + id + ' reset to pending', true);
|
|
222
|
+
wakeEngine();
|
|
223
|
+
refresh();
|
|
224
|
+
} else {
|
|
225
|
+
if (btn) { btn.textContent = btn.dataset.origText || 'Retry'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
|
|
226
|
+
const d = await res.json().catch(() => ({}));
|
|
221
227
|
alert('Retry failed: ' + (d.error || 'unknown'));
|
|
222
228
|
}
|
|
223
|
-
} catch (e) { alert('Retry error: ' + e.message); }
|
|
229
|
+
} catch (e) { if (btn) { btn.textContent = btn.dataset.origText || 'Retry'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } alert('Retry error: ' + e.message); }
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
function wiPrev() { if (wiPage > 0) { wiPage--; renderWorkItems(allWorkItems); } }
|
|
@@ -365,7 +371,7 @@ function openWorkItemDetail(id) {
|
|
|
365
371
|
'<span class="dispatch-type ' + (item.type || 'implement') + '">' + escHtml(item.type || 'implement') + '</span>' +
|
|
366
372
|
'<span class="prd-item-priority ' + (item.priority || '') + '">' + escHtml(item.priority || 'medium') + '</span>' +
|
|
367
373
|
'</div>';
|
|
368
|
-
html += field('Description', '<div style="
|
|
374
|
+
html += field('Description', '<div style="font-size:12px">' + renderMd(item.description || item.title || '—') + '</div>');
|
|
369
375
|
html += field('Agent', escHtml(item.dispatched_to || item.agent || 'Auto'));
|
|
370
376
|
html += field('Source', escHtml(item._source || 'central'));
|
|
371
377
|
if (item.created) html += field('Created', escHtml(new Date(item.created).toLocaleString()));
|
package/dashboard/js/utils.js
CHANGED
|
@@ -41,4 +41,118 @@ function copyLlmText(btn) {
|
|
|
41
41
|
setTimeout(() => { btn.innerHTML = '⎘'; }, 1500);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Lightweight markdown → HTML renderer. XSS-safe (escapes first, then transforms).
|
|
46
|
+
* Handles: headings, bold, italic, inline code, code blocks, links, blockquotes,
|
|
47
|
+
* horizontal rules, ordered/unordered/checkbox lists, and tables.
|
|
48
|
+
*/
|
|
49
|
+
function renderMd(s) {
|
|
50
|
+
if (!s) return '';
|
|
51
|
+
let html = escHtml(s);
|
|
52
|
+
|
|
53
|
+
// 1. Extract code blocks and inline code into placeholders (protect from other transforms)
|
|
54
|
+
const codeSlots = [];
|
|
55
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function(_, lang, code) {
|
|
56
|
+
codeSlots.push('<pre style="background:var(--bg);padding:8px;border-radius:4px;overflow-x:auto;font-size:11px;margin:4px 0"><code>' + code + '</code></pre>');
|
|
57
|
+
return '\x00CB' + (codeSlots.length - 1) + '\x00';
|
|
58
|
+
});
|
|
59
|
+
html = html.replace(/`([^`\n]+)`/g, function(_, code) {
|
|
60
|
+
codeSlots.push('<code style="background:var(--bg);padding:1px 4px;border-radius:3px;font-size:0.9em">' + code + '</code>');
|
|
61
|
+
return '\x00CB' + (codeSlots.length - 1) + '\x00';
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 2. Inline transforms (before block processing so they work inside list items etc.)
|
|
65
|
+
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
66
|
+
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
|
|
67
|
+
html = html.replace(/(?<!\w)\*([^*\n]+)\*(?!\w)/g, '<em>$1</em>');
|
|
68
|
+
html = html.replace(/(?<!\w)_([^_\n]+)_(?!\w)/g, '<em>$1</em>');
|
|
69
|
+
html = html.replace(/~~(.+?)~~/g, '<s>$1</s>');
|
|
70
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--blue)">$1</a>');
|
|
71
|
+
|
|
72
|
+
// 3. Block-level processing (line by line)
|
|
73
|
+
var lines = html.split('\n');
|
|
74
|
+
var out = [];
|
|
75
|
+
var inList = false;
|
|
76
|
+
var listType = '';
|
|
77
|
+
|
|
78
|
+
function closeList() { if (inList) { out.push(listType === 'ol' ? '</ol>' : '</ul>'); inList = false; } }
|
|
79
|
+
function openList(type) {
|
|
80
|
+
if (inList && listType !== type) closeList();
|
|
81
|
+
if (!inList) {
|
|
82
|
+
var style = type === 'ol' ? 'margin:2px 0 2px 20px;padding:0' : type === 'cb' ? 'margin:2px 0 2px 16px;padding:0;list-style:none' : 'margin:2px 0 2px 16px;padding:0';
|
|
83
|
+
out.push('<' + (type === 'ol' ? 'ol' : 'ul') + ' style="' + style + '">');
|
|
84
|
+
inList = true; listType = type === 'cb' ? 'ul' : type;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (var i = 0; i < lines.length; i++) {
|
|
89
|
+
var line = lines[i];
|
|
90
|
+
|
|
91
|
+
// Code block placeholder — pass through as-is
|
|
92
|
+
if (line.match(/^\x00CB\d+\x00$/)) { closeList(); out.push(line); continue; }
|
|
93
|
+
|
|
94
|
+
// Headings
|
|
95
|
+
var headMatch = line.match(/^(#{1,4})\s+(.+)/);
|
|
96
|
+
if (headMatch) {
|
|
97
|
+
closeList();
|
|
98
|
+
var sizes = { 1: '16px', 2: '14px', 3: '13px', 4: '12px' };
|
|
99
|
+
out.push('<div style="font-weight:600;font-size:' + sizes[headMatch[1].length] + ';margin:8px 0 4px">' + headMatch[2] + '</div>');
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Horizontal rule (only bare ---, ***, ___ lines)
|
|
104
|
+
if (/^[-*_]{3,}\s*$/.test(line) && !/\S/.test(line.replace(/[-*_]/g, ''))) {
|
|
105
|
+
closeList();
|
|
106
|
+
out.push('<hr style="border:none;border-top:1px solid var(--border);margin:8px 0">');
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Blockquote
|
|
111
|
+
if (line.match(/^>\s?/)) {
|
|
112
|
+
closeList();
|
|
113
|
+
out.push('<div style="border-left:3px solid var(--border);padding-left:8px;color:var(--muted);margin:2px 0">' + line.replace(/^(>\s?)+/, '') + '</div>');
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Checkbox list (must come before UL — both start with - )
|
|
118
|
+
var cbMatch = line.match(/^(\s*)[-*]\s\[([ xX])\]\s(.+)/);
|
|
119
|
+
if (cbMatch) {
|
|
120
|
+
openList('cb');
|
|
121
|
+
out.push('<li>' + (cbMatch[2] !== ' ' ? '\u2611' : '\u2610') + ' ' + cbMatch[3] + '</li>');
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Unordered list (- or * followed by space and content, not bare --- or ***)
|
|
126
|
+
var ulMatch = line.match(/^(\s*)[-*]\s+(.+)/);
|
|
127
|
+
if (ulMatch) {
|
|
128
|
+
openList('ul');
|
|
129
|
+
out.push('<li>' + ulMatch[2] + '</li>');
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Ordered list
|
|
134
|
+
var olMatch = line.match(/^(\s*)\d+\.\s+(.+)/);
|
|
135
|
+
if (olMatch) {
|
|
136
|
+
openList('ol');
|
|
137
|
+
out.push('<li>' + olMatch[2] + '</li>');
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Non-list line — close any open list
|
|
142
|
+
closeList();
|
|
143
|
+
|
|
144
|
+
// Blank line → spacer
|
|
145
|
+
if (!line.trim()) { out.push('<div style="height:4px"></div>'); continue; }
|
|
146
|
+
|
|
147
|
+
out.push('<div>' + line + '</div>');
|
|
148
|
+
}
|
|
149
|
+
closeList();
|
|
150
|
+
html = out.join('\n');
|
|
151
|
+
|
|
152
|
+
// 4. Restore code placeholders
|
|
153
|
+
html = html.replace(/\x00CB(\d+)\x00/g, function(_, idx) { return codeSlots[idx]; });
|
|
154
|
+
|
|
155
|
+
return html;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
window.MinionsUtils = { wakeEngine, escHtml, renderMd, normalizePlanFile, timeAgo, statusColor, llmCopyBtn, copyLlmText };
|
package/dashboard.js
CHANGED
|
@@ -2082,17 +2082,61 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2082
2082
|
fs.renameSync(planPath, archivePath);
|
|
2083
2083
|
|
|
2084
2084
|
// Mark archived in JSON if PRD
|
|
2085
|
+
let archivedSource = null;
|
|
2085
2086
|
if (body.file.endsWith('.json')) {
|
|
2086
2087
|
try {
|
|
2087
2088
|
const prd = JSON.parse(safeRead(archivePath) || '{}');
|
|
2088
2089
|
prd.status = 'archived';
|
|
2089
2090
|
prd.archivedAt = new Date().toISOString();
|
|
2090
2091
|
safeWrite(archivePath, prd);
|
|
2092
|
+
// Also archive linked source plan
|
|
2093
|
+
if (prd.source_plan) {
|
|
2094
|
+
const mdPath = path.join(PLANS_DIR, prd.source_plan);
|
|
2095
|
+
if (fs.existsSync(mdPath)) {
|
|
2096
|
+
const planArchive = path.join(PLANS_DIR, 'archive');
|
|
2097
|
+
if (!fs.existsSync(planArchive)) fs.mkdirSync(planArchive, { recursive: true });
|
|
2098
|
+
fs.renameSync(mdPath, path.join(planArchive, prd.source_plan));
|
|
2099
|
+
archivedSource = prd.source_plan;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
} catch { /* optional */ }
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
invalidateStatusCache();
|
|
2106
|
+
return jsonReply(res, 200, { ok: true, archived: body.file, archivedSource });
|
|
2107
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
async function handlePlansUnarchive(req, res) {
|
|
2111
|
+
try {
|
|
2112
|
+
const body = await readBody(req);
|
|
2113
|
+
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
2114
|
+
if (body.file.includes('..') || body.file.includes('\0') || body.file.includes('/') || body.file.includes('\\')) {
|
|
2115
|
+
return jsonReply(res, 400, { error: 'invalid filename' });
|
|
2116
|
+
}
|
|
2117
|
+
const isJson = body.file.endsWith('.json');
|
|
2118
|
+
const targetDir = isJson ? PRD_DIR : PLANS_DIR;
|
|
2119
|
+
const archivePath = path.join(targetDir, 'archive', body.file);
|
|
2120
|
+
if (!fs.existsSync(archivePath)) return jsonReply(res, 404, { error: 'File not found in archive' });
|
|
2121
|
+
fs.renameSync(archivePath, path.join(targetDir, body.file));
|
|
2122
|
+
|
|
2123
|
+
// Also unarchive linked source plan
|
|
2124
|
+
let unarchivedSource = null;
|
|
2125
|
+
if (isJson) {
|
|
2126
|
+
try {
|
|
2127
|
+
const prd = safeJson(path.join(targetDir, body.file));
|
|
2128
|
+
if (prd?.source_plan) {
|
|
2129
|
+
const mdArchivePath = path.join(PLANS_DIR, 'archive', prd.source_plan);
|
|
2130
|
+
if (fs.existsSync(mdArchivePath)) {
|
|
2131
|
+
fs.renameSync(mdArchivePath, path.join(PLANS_DIR, prd.source_plan));
|
|
2132
|
+
unarchivedSource = prd.source_plan;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2091
2135
|
} catch { /* optional */ }
|
|
2092
2136
|
}
|
|
2093
2137
|
|
|
2094
2138
|
invalidateStatusCache();
|
|
2095
|
-
return jsonReply(res, 200, { ok: true,
|
|
2139
|
+
return jsonReply(res, 200, { ok: true, unarchivedSource });
|
|
2096
2140
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
2097
2141
|
}
|
|
2098
2142
|
|
|
@@ -3115,6 +3159,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3115
3159
|
{ method: 'POST', path: '/api/plans/regenerate', desc: 'Reset pending/failed work items for a plan so they re-materialize', params: 'source', handler: handlePlansRegenerate },
|
|
3116
3160
|
{ method: 'POST', path: '/api/plans/delete', desc: 'Delete a plan file and clean up work items', params: 'file', handler: handlePlansDelete },
|
|
3117
3161
|
{ method: 'POST', path: '/api/plans/archive', desc: 'Move a plan/PRD to archive (preserves work items)', params: 'file', handler: handlePlansArchive },
|
|
3162
|
+
{ method: 'POST', path: '/api/plans/unarchive', desc: 'Restore a plan/PRD from archive', params: 'file', handler: handlePlansUnarchive },
|
|
3118
3163
|
{ method: 'POST', path: '/api/plans/revise', desc: 'Request revision with feedback, dispatches agent to revise', params: 'file, feedback, requestedBy?', handler: handlePlansRevise },
|
|
3119
3164
|
{ method: 'POST', path: '/api/plans/discuss', desc: 'Generate a plan discussion session script for Claude CLI', params: 'file', handler: handlePlansDiscuss },
|
|
3120
3165
|
{ method: 'GET', path: /^\/api\/plans\/archive\/([^?]+)$/, desc: 'Read an archived plan file', handler: handlePlansArchiveRead },
|
package/engine/ado.js
CHANGED
|
@@ -344,8 +344,8 @@ async function reconcilePrs(config) {
|
|
|
344
344
|
const branch = (adoPr.sourceRefName || '').replace('refs/heads/', '');
|
|
345
345
|
const title = adoPr.title || '';
|
|
346
346
|
// Extract item ID from branch name or PR title (e.g., feat(P-2cafdc2a): ...)
|
|
347
|
-
const branchMatch = branch.match(/(P-[a-
|
|
348
|
-
const titleMatch = title.match(/\((P-[a-
|
|
347
|
+
const branchMatch = branch.match(/(P-[a-z0-9]{6,})/i) || branch.match(/(PL-W\d+)/i);
|
|
348
|
+
const titleMatch = title.match(/\((P-[a-z0-9]{6,})\)/) || title.match(/\((PL-W\d+)\)/);
|
|
349
349
|
const linkedItemId = branchMatch?.[1] || titleMatch?.[1] || null;
|
|
350
350
|
const linkedItem = linkedItemId ? allItems.find(i => i.id === linkedItemId) : null;
|
|
351
351
|
const confirmedItemId = linkedItem ? linkedItemId : null;
|
|
@@ -368,7 +368,7 @@ async function reconcilePrs(config) {
|
|
|
368
368
|
existingPrs.push({
|
|
369
369
|
id: prId,
|
|
370
370
|
title: (adoPr.title || `PR #${adoPr.pullRequestId}`).slice(0, 120),
|
|
371
|
-
agent: (adoPr.createdBy?.displayName || 'unknown').toLowerCase(),
|
|
371
|
+
agent: (linkedItem?.dispatched_to || adoPr.createdBy?.displayName || 'unknown').toLowerCase(),
|
|
372
372
|
branch,
|
|
373
373
|
reviewStatus: 'pending',
|
|
374
374
|
status: 'active',
|
package/engine/github.js
CHANGED
|
@@ -334,7 +334,7 @@ async function reconcilePrs(config) {
|
|
|
334
334
|
for (const ghPr of ghPrs) {
|
|
335
335
|
const prId = `PR-${ghPr.number}`;
|
|
336
336
|
const branch = ghPr.head?.ref || '';
|
|
337
|
-
const wiMatch = branch.match(/(P-[a-
|
|
337
|
+
const wiMatch = branch.match(/(P-[a-z0-9]{6,})/i) || branch.match(/(PL-W\d+)/i);
|
|
338
338
|
const linkedItemId = wiMatch ? wiMatch[1] : null;
|
|
339
339
|
const linkedItem = linkedItemId ? allItems.find(i => i.id === linkedItemId) : null;
|
|
340
340
|
const confirmedItemId = linkedItem ? linkedItemId : null;
|
|
@@ -356,7 +356,7 @@ async function reconcilePrs(config) {
|
|
|
356
356
|
existingPrs.push({
|
|
357
357
|
id: prId,
|
|
358
358
|
title: (ghPr.title || `PR #${ghPr.number}`).slice(0, 120),
|
|
359
|
-
agent: (ghPr.user?.login || 'unknown').toLowerCase(),
|
|
359
|
+
agent: (linkedItem?.dispatched_to || ghPr.user?.login || 'unknown').toLowerCase(),
|
|
360
360
|
branch,
|
|
361
361
|
reviewStatus: 'pending',
|
|
362
362
|
status: 'active',
|
package/engine/queries.js
CHANGED
|
@@ -187,6 +187,16 @@ function getAgents(config) {
|
|
|
187
187
|
? config.agents
|
|
188
188
|
: shared.DEFAULT_AGENTS;
|
|
189
189
|
const roster = Object.entries(agents).map(([id, info]) => ({ id, ...info }));
|
|
190
|
+
|
|
191
|
+
// Include temp agents that are currently active so they show up in agent tiles
|
|
192
|
+
const dispatch = getDispatch();
|
|
193
|
+
const seen = new Set(roster.map(a => a.id));
|
|
194
|
+
for (const d of (dispatch.active || [])) {
|
|
195
|
+
if (d.agent && d.agent.startsWith('temp-') && !seen.has(d.agent)) {
|
|
196
|
+
roster.push({ id: d.agent, name: d.agentName || d.agent, role: d.agentRole || 'Temp Agent', emoji: '\u{1F4A8}', skills: [], _temp: true });
|
|
197
|
+
seen.add(d.agent);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
190
200
|
const allInboxFiles = safeReadDir(INBOX_DIR);
|
|
191
201
|
|
|
192
202
|
return roster.map(a => {
|
package/engine.js
CHANGED
|
@@ -262,7 +262,10 @@ function spawnAgent(dispatchItem, config) {
|
|
|
262
262
|
const startedAt = ts();
|
|
263
263
|
|
|
264
264
|
// Resolve project context for this dispatch
|
|
265
|
-
|
|
265
|
+
// meta.project has {name, localPath} — enrich with full config (mainBranch, repoHost, etc.)
|
|
266
|
+
const metaProject = meta?.project || {};
|
|
267
|
+
const fullProject = getProjects(config).find(p => p.name === metaProject.name || p.localPath === metaProject.localPath) || getProjects(config)[0] || {};
|
|
268
|
+
const project = { ...fullProject, ...metaProject };
|
|
266
269
|
const rootDir = project.localPath ? path.resolve(project.localPath) : path.resolve(MINIONS_DIR, '..');
|
|
267
270
|
|
|
268
271
|
// Determine working directory
|
|
@@ -318,47 +321,61 @@ function spawnAgent(dispatchItem, config) {
|
|
|
318
321
|
}
|
|
319
322
|
} else {
|
|
320
323
|
log('info', `Creating worktree: ${worktreePath} on branch ${branchName}`);
|
|
324
|
+
const mainRef = sanitizeBranch(project.mainBranch || 'main');
|
|
321
325
|
try {
|
|
322
|
-
runWorktreeAdd(rootDir, worktreePath, `-b "${branchName}" ${
|
|
326
|
+
runWorktreeAdd(rootDir, worktreePath, `-b "${branchName}" ${mainRef}`, _worktreeGitOpts, worktreeCreateRetries);
|
|
323
327
|
} catch (e1) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
log('info', `
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
328
|
+
const branchExists = e1.message?.includes('already exists');
|
|
329
|
+
log('warn', `Worktree -b failed for ${branchName}: ${e1.message?.split('\n')[0]}`);
|
|
330
|
+
if (!branchExists) {
|
|
331
|
+
// Transient error (lock, timeout) — prune, clean, and retry -b once more
|
|
332
|
+
log('info', `Retrying -b create after prune for ${branchName}`);
|
|
333
|
+
try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 15000 }); } catch { /* optional */ }
|
|
334
|
+
removeStaleIndexLock(rootDir);
|
|
335
|
+
// Clean up partial worktree directory from failed attempt
|
|
336
|
+
try { if (fs.existsSync(worktreePath)) fs.rmSync(worktreePath, { recursive: true, force: true }); } catch { /* optional */ }
|
|
337
|
+
try {
|
|
338
|
+
runWorktreeAdd(rootDir, worktreePath, `-b "${branchName}" ${mainRef}`, _worktreeGitOpts, 0);
|
|
339
|
+
} catch (e1b) {
|
|
340
|
+
log('error', `Worktree -b retry also failed for ${branchName}: ${e1b.message?.split('\n')[0]}`);
|
|
341
|
+
throw e1b;
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
// Branch already exists — try checkout without -b
|
|
345
|
+
try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
346
|
+
try {
|
|
347
|
+
runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
|
|
348
|
+
log('info', `Reusing existing branch: ${branchName}`);
|
|
349
|
+
} catch (e2) {
|
|
350
|
+
// "already checked out" or "already used by worktree" — find and reuse or recover
|
|
351
|
+
const alreadyUsed = e2.message?.includes('already checked out') || e2.message?.includes('already used by worktree')
|
|
352
|
+
|| e1.message?.includes('already checked out') || e1.message?.includes('already used by worktree');
|
|
353
|
+
if (alreadyUsed) {
|
|
354
|
+
const existingWtPath = findExistingWorktree(rootDir, branchName);
|
|
355
|
+
if (existingWtPath && fs.existsSync(existingWtPath)) {
|
|
356
|
+
const dispatch = safeJson(DISPATCH_PATH) || {};
|
|
357
|
+
const activelyUsed = (dispatch.active || []).some(d => {
|
|
358
|
+
const dBranch = d.meta?.branch ? sanitizeBranch(d.meta.branch) : '';
|
|
359
|
+
return dBranch === branchName && d.id !== id;
|
|
360
|
+
});
|
|
361
|
+
if (activelyUsed) {
|
|
362
|
+
log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
|
|
363
|
+
throw e2;
|
|
364
|
+
}
|
|
365
|
+
log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
366
|
+
worktreePath = existingWtPath;
|
|
367
|
+
} else if (existingWtPath && !fs.existsSync(existingWtPath)) {
|
|
368
|
+
log('warn', `Branch ${branchName} tracked in missing dir ${existingWtPath} — pruning and recreating`);
|
|
369
|
+
try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
370
|
+
runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
|
|
371
|
+
log('info', `Recovered worktree for ${branchName} after stale entry prune`);
|
|
372
|
+
} else {
|
|
373
|
+
try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
374
|
+
runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
|
|
345
375
|
}
|
|
346
|
-
// Reuse the existing worktree — update path so the rest of spawnAgent uses it
|
|
347
|
-
log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
348
|
-
worktreePath = existingWtPath;
|
|
349
|
-
} else if (existingWtPath && !fs.existsSync(existingWtPath)) {
|
|
350
|
-
// Directory gone but git still tracks it — prune and recreate
|
|
351
|
-
log('warn', `Branch ${branchName} tracked in missing dir ${existingWtPath} — pruning and recreating`);
|
|
352
|
-
try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
353
|
-
runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
|
|
354
|
-
log('info', `Recovered worktree for ${branchName} after stale entry prune`);
|
|
355
376
|
} else {
|
|
356
|
-
|
|
357
|
-
try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
358
|
-
runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
|
|
377
|
+
throw e2;
|
|
359
378
|
}
|
|
360
|
-
} else {
|
|
361
|
-
throw e2;
|
|
362
379
|
}
|
|
363
380
|
}
|
|
364
381
|
}
|
|
@@ -1378,6 +1395,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
1378
1395
|
safeWrite(projectWorkItemsPath(project), items);
|
|
1379
1396
|
}
|
|
1380
1397
|
if (isAlreadyDispatched(key)) {
|
|
1398
|
+
if (item.status === 'pending') { item.status = 'dispatched'; needsWrite = true; }
|
|
1381
1399
|
if (item._pendingReason !== 'already_dispatched') { item._pendingReason = 'already_dispatched'; needsWrite = true; }
|
|
1382
1400
|
skipped.gated++; continue;
|
|
1383
1401
|
}
|
package/package.json
CHANGED
|
@@ -24,12 +24,52 @@ You are {{agent_name}} ({{agent_role}}), synthesizing the team meeting results.
|
|
|
24
24
|
|
|
25
25
|
## Your Task
|
|
26
26
|
|
|
27
|
-
Write a clear meeting conclusion:
|
|
27
|
+
Write a clear meeting conclusion covering:
|
|
28
28
|
|
|
29
29
|
1. **Areas of consensus** — what does the team agree on?
|
|
30
30
|
2. **Unresolved disagreements** — where do positions still differ?
|
|
31
31
|
3. **Recommended decision** — what should we do?
|
|
32
|
-
4. **Action items** — specific next steps with owners
|
|
32
|
+
4. **Action items** — specific next steps with owners (leave empty if none)
|
|
33
33
|
5. **Open questions** — what still needs human input?
|
|
34
34
|
|
|
35
35
|
Be decisive. If there's a clear best option, say so.
|
|
36
|
+
|
|
37
|
+
## Plan Creation (conditional)
|
|
38
|
+
|
|
39
|
+
After writing the conclusion, decide: **are there concrete action items that require implementation work?**
|
|
40
|
+
|
|
41
|
+
- **If YES** — write a plan file to `plans/<slugified-meeting-title>-<YYYY-MM-DD>.md` using the Minions plan format below. Only create this file if there are real, actionable implementation tasks — not just discussion points or observations.
|
|
42
|
+
|
|
43
|
+
- **If NO** — do not create a plan file. A conclusion with only open questions, observations, or decisions that require no code/implementation changes does not need a plan.
|
|
44
|
+
|
|
45
|
+
### Plan format (only if action items exist)
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
# <Plan Title>
|
|
49
|
+
|
|
50
|
+
> Source: Meeting {{meeting_title}} — <date>
|
|
51
|
+
|
|
52
|
+
## Background
|
|
53
|
+
|
|
54
|
+
<1-2 sentence summary of why this plan exists, from the meeting conclusion>
|
|
55
|
+
|
|
56
|
+
## Tasks
|
|
57
|
+
|
|
58
|
+
### 1. <Task title>
|
|
59
|
+
- **What**: <what needs to be done>
|
|
60
|
+
- **Why**: <from the meeting — what problem does this fix>
|
|
61
|
+
- **Owner**: <agent role best suited: Engineer / Architect / Analyst>
|
|
62
|
+
- **Files**: <relevant files if known>
|
|
63
|
+
|
|
64
|
+
### 2. <Task title>
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
## Open Questions
|
|
68
|
+
|
|
69
|
+
<Any unresolved items that need human input before work begins>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Do NOT create a plan for:
|
|
73
|
+
- Informational findings with no follow-up work
|
|
74
|
+
- Decisions that have already been made and require no changes
|
|
75
|
+
- Items that are purely "monitor and observe"
|