@yemi33/minions 0.1.95 → 0.1.97
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 +34 -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-prd.js +2 -3
- package/dashboard/js/render-prs.js +4 -2
- package/dashboard/js/render-work-items.js +13 -7
- package/dashboard/js/utils.js +115 -1
- package/dashboard.js +122 -1
- package/engine/ado.js +7 -3
- package/engine/github.js +7 -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,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.97 (2026-04-01)
|
|
4
|
+
|
|
5
|
+
### Engine
|
|
6
|
+
- engine/ado.js
|
|
7
|
+
- engine/github.js
|
|
8
|
+
|
|
9
|
+
### Dashboard
|
|
10
|
+
- dashboard.js
|
|
11
|
+
- dashboard/js/render-prd.js
|
|
12
|
+
- dashboard/js/render-prs.js
|
|
13
|
+
|
|
14
|
+
## 0.1.96 (2026-04-01)
|
|
15
|
+
|
|
16
|
+
### Engine
|
|
17
|
+
- engine.js
|
|
18
|
+
- engine/ado.js
|
|
19
|
+
- engine/github.js
|
|
20
|
+
- engine/queries.js
|
|
21
|
+
|
|
22
|
+
### Dashboard
|
|
23
|
+
- dashboard.js
|
|
24
|
+
- dashboard/js/detail-panel.js
|
|
25
|
+
- dashboard/js/live-stream.js
|
|
26
|
+
- dashboard/js/modal-qa.js
|
|
27
|
+
- dashboard/js/render-agents.js
|
|
28
|
+
- dashboard/js/render-inbox.js
|
|
29
|
+
- dashboard/js/render-meetings.js
|
|
30
|
+
- dashboard/js/render-pinned.js
|
|
31
|
+
- dashboard/js/render-work-items.js
|
|
32
|
+
- dashboard/js/utils.js
|
|
33
|
+
|
|
34
|
+
### Playbooks
|
|
35
|
+
- meeting-conclude.md
|
|
36
|
+
|
|
3
37
|
## 0.1.95 (2026-04-01)
|
|
4
38
|
|
|
5
39
|
### 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
|
}
|
|
@@ -238,9 +238,7 @@ function renderPrdProgress(prog) {
|
|
|
238
238
|
: isCompleted
|
|
239
239
|
? '<span onclick="event.stopPropagation();triggerVerify(\'' + escHtml(g.file) + '\',this)" style="color:var(--green);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">Verify</span>'
|
|
240
240
|
: '<span onclick="event.stopPropagation();planPause(\'' + escHtml(g.file) + '\',this)" style="color:var(--yellow);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.3);border-radius:3px">Pause</span>';
|
|
241
|
-
const archiveBtn = isCompleted
|
|
242
|
-
? '<span onclick="event.stopPropagation();planArchive(\'' + escHtml(g.file) + '\',this)" style="color:var(--muted);cursor:pointer;font-size:9px;padding:1px 6px;background:var(--surface);border:1px solid var(--border);border-radius:3px">Archive</span>'
|
|
243
|
-
: '';
|
|
241
|
+
const archiveBtn = (isCompleted || isPaused) ? '<span onclick="event.stopPropagation();planArchive(\'' + escHtml(g.file) + '\',this)" style="color:var(--muted);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(139,148,158,0.1);border:1px solid rgba(139,148,158,0.3);border-radius:3px">Archive</span>' : '';
|
|
244
242
|
const deleteBtn = '<span onclick="event.stopPropagation();planDelete(\'' + escHtml(g.file) + '\')" style="color:var(--red);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.3);border-radius:3px">Delete</span>';
|
|
245
243
|
const sourcePlanLink = g.sourcePlan
|
|
246
244
|
? '<span onclick="event.stopPropagation();planView(\'' + escHtml(g.sourcePlan) + '\')" style="color:var(--blue);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:3px" title="View source plan">📄 Plan</span>'
|
|
@@ -509,6 +507,7 @@ function showArchivedPrdDetail(idx) {
|
|
|
509
507
|
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'graph\';showArchivedPrdDetail(' + idx + ')">Graph</button>' +
|
|
510
508
|
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (!isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'list\';showArchivedPrdDetail(' + idx + ')">List</button>' +
|
|
511
509
|
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);margin-left:auto" onclick="triggerVerify(\'' + escHtml(g.file) + '\')">Trigger Verify</button>' +
|
|
510
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="planUnarchive(\'' + escHtml(g.file) + '\',this)">Unarchive</button>' +
|
|
512
511
|
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="openArchivedPrdModal()">Back</button>' +
|
|
513
512
|
'</div>';
|
|
514
513
|
|
|
@@ -7,9 +7,11 @@ const PR_PER_PAGE = 25;
|
|
|
7
7
|
function prRow(pr) {
|
|
8
8
|
// Minions review (agent) state — separate from ADO human review
|
|
9
9
|
const sq = pr.minionsReview || {};
|
|
10
|
-
|
|
10
|
+
// If PR is merged/abandoned, treat 'waiting' review as resolved
|
|
11
|
+
const effectiveReviewStatus = (pr.status === 'merged' || pr.status === 'abandoned') && pr.reviewStatus === 'waiting' ? (pr.status === 'merged' ? 'approved' : 'pending') : pr.reviewStatus;
|
|
12
|
+
const reviewSource = sq.status || effectiveReviewStatus || 'pending';
|
|
11
13
|
const reviewClass = reviewSource === 'approved' ? 'approved' : (reviewSource === 'changes-requested' || reviewSource === 'rejected') ? 'rejected' : reviewSource === 'waiting' ? 'building' : 'draft';
|
|
12
|
-
const reviewLabel = sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (
|
|
14
|
+
const reviewLabel = sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (effectiveReviewStatus || 'pending');
|
|
13
15
|
const buildClass = pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
|
|
14
16
|
const buildLabel = pr.buildStatus || 'none';
|
|
15
17
|
const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
|
|
@@ -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
|
@@ -1647,6 +1647,80 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1647
1647
|
return jsonReply(res, 200, plans);
|
|
1648
1648
|
}
|
|
1649
1649
|
|
|
1650
|
+
async function handlePlansArchiveMove(req, res) {
|
|
1651
|
+
try {
|
|
1652
|
+
const body = await readBody(req);
|
|
1653
|
+
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
1654
|
+
const file = body.file;
|
|
1655
|
+
if (file.includes('..') || file.includes('\0')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1656
|
+
|
|
1657
|
+
const isJson = file.endsWith('.json');
|
|
1658
|
+
const sourceDir = isJson ? PRD_DIR : PLANS_DIR;
|
|
1659
|
+
const archiveDir = path.join(sourceDir, 'archive');
|
|
1660
|
+
const sourcePath = path.join(sourceDir, file);
|
|
1661
|
+
|
|
1662
|
+
if (!fs.existsSync(sourcePath)) return jsonReply(res, 404, { error: 'File not found' });
|
|
1663
|
+
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
|
|
1664
|
+
|
|
1665
|
+
fs.renameSync(sourcePath, path.join(archiveDir, file));
|
|
1666
|
+
|
|
1667
|
+
// If archiving a PRD .json, also archive its source .md plan
|
|
1668
|
+
let archivedSource = null;
|
|
1669
|
+
if (isJson) {
|
|
1670
|
+
try {
|
|
1671
|
+
const prd = safeJson(path.join(archiveDir, file));
|
|
1672
|
+
if (prd?.source_plan) {
|
|
1673
|
+
const mdPath = path.join(PLANS_DIR, prd.source_plan);
|
|
1674
|
+
if (fs.existsSync(mdPath)) {
|
|
1675
|
+
const planArchive = path.join(PLANS_DIR, 'archive');
|
|
1676
|
+
if (!fs.existsSync(planArchive)) fs.mkdirSync(planArchive, { recursive: true });
|
|
1677
|
+
fs.renameSync(mdPath, path.join(planArchive, prd.source_plan));
|
|
1678
|
+
archivedSource = prd.source_plan;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
} catch { /* optional — source plan may not exist */ }
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
invalidateStatusCache();
|
|
1685
|
+
return jsonReply(res, 200, { ok: true, archivedSource });
|
|
1686
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
async function handlePlansUnarchive(req, res) {
|
|
1690
|
+
try {
|
|
1691
|
+
const body = await readBody(req);
|
|
1692
|
+
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
1693
|
+
const file = body.file;
|
|
1694
|
+
if (file.includes('..') || file.includes('\0')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1695
|
+
|
|
1696
|
+
const isJson = file.endsWith('.json');
|
|
1697
|
+
const targetDir = isJson ? PRD_DIR : PLANS_DIR;
|
|
1698
|
+
const archiveDir = path.join(targetDir, 'archive');
|
|
1699
|
+
const archivePath = path.join(archiveDir, file);
|
|
1700
|
+
|
|
1701
|
+
if (!fs.existsSync(archivePath)) return jsonReply(res, 404, { error: 'File not found in archive' });
|
|
1702
|
+
fs.renameSync(archivePath, path.join(targetDir, file));
|
|
1703
|
+
|
|
1704
|
+
// If unarchiving a PRD .json, also unarchive its source .md plan
|
|
1705
|
+
let unarchivedSource = null;
|
|
1706
|
+
if (isJson) {
|
|
1707
|
+
try {
|
|
1708
|
+
const prd = safeJson(path.join(targetDir, file));
|
|
1709
|
+
if (prd?.source_plan) {
|
|
1710
|
+
const mdArchivePath = path.join(PLANS_DIR, 'archive', prd.source_plan);
|
|
1711
|
+
if (fs.existsSync(mdArchivePath)) {
|
|
1712
|
+
fs.renameSync(mdArchivePath, path.join(PLANS_DIR, prd.source_plan));
|
|
1713
|
+
unarchivedSource = prd.source_plan;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
} catch { /* optional */ }
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
invalidateStatusCache();
|
|
1720
|
+
return jsonReply(res, 200, { ok: true, unarchivedSource });
|
|
1721
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1650
1724
|
async function handlePlansArchiveRead(req, res, match) {
|
|
1651
1725
|
const file = decodeURIComponent(match[1]);
|
|
1652
1726
|
if (file.includes('..') || file.includes('\0')) return jsonReply(res, 400, { error: 'invalid' });
|
|
@@ -2082,17 +2156,61 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2082
2156
|
fs.renameSync(planPath, archivePath);
|
|
2083
2157
|
|
|
2084
2158
|
// Mark archived in JSON if PRD
|
|
2159
|
+
let archivedSource = null;
|
|
2085
2160
|
if (body.file.endsWith('.json')) {
|
|
2086
2161
|
try {
|
|
2087
2162
|
const prd = JSON.parse(safeRead(archivePath) || '{}');
|
|
2088
2163
|
prd.status = 'archived';
|
|
2089
2164
|
prd.archivedAt = new Date().toISOString();
|
|
2090
2165
|
safeWrite(archivePath, prd);
|
|
2166
|
+
// Also archive linked source plan
|
|
2167
|
+
if (prd.source_plan) {
|
|
2168
|
+
const mdPath = path.join(PLANS_DIR, prd.source_plan);
|
|
2169
|
+
if (fs.existsSync(mdPath)) {
|
|
2170
|
+
const planArchive = path.join(PLANS_DIR, 'archive');
|
|
2171
|
+
if (!fs.existsSync(planArchive)) fs.mkdirSync(planArchive, { recursive: true });
|
|
2172
|
+
fs.renameSync(mdPath, path.join(planArchive, prd.source_plan));
|
|
2173
|
+
archivedSource = prd.source_plan;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
} catch { /* optional */ }
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
invalidateStatusCache();
|
|
2180
|
+
return jsonReply(res, 200, { ok: true, archived: body.file, archivedSource });
|
|
2181
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
async function handlePlansUnarchive(req, res) {
|
|
2185
|
+
try {
|
|
2186
|
+
const body = await readBody(req);
|
|
2187
|
+
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
2188
|
+
if (body.file.includes('..') || body.file.includes('\0') || body.file.includes('/') || body.file.includes('\\')) {
|
|
2189
|
+
return jsonReply(res, 400, { error: 'invalid filename' });
|
|
2190
|
+
}
|
|
2191
|
+
const isJson = body.file.endsWith('.json');
|
|
2192
|
+
const targetDir = isJson ? PRD_DIR : PLANS_DIR;
|
|
2193
|
+
const archivePath = path.join(targetDir, 'archive', body.file);
|
|
2194
|
+
if (!fs.existsSync(archivePath)) return jsonReply(res, 404, { error: 'File not found in archive' });
|
|
2195
|
+
fs.renameSync(archivePath, path.join(targetDir, body.file));
|
|
2196
|
+
|
|
2197
|
+
// Also unarchive linked source plan
|
|
2198
|
+
let unarchivedSource = null;
|
|
2199
|
+
if (isJson) {
|
|
2200
|
+
try {
|
|
2201
|
+
const prd = safeJson(path.join(targetDir, body.file));
|
|
2202
|
+
if (prd?.source_plan) {
|
|
2203
|
+
const mdArchivePath = path.join(PLANS_DIR, 'archive', prd.source_plan);
|
|
2204
|
+
if (fs.existsSync(mdArchivePath)) {
|
|
2205
|
+
fs.renameSync(mdArchivePath, path.join(PLANS_DIR, prd.source_plan));
|
|
2206
|
+
unarchivedSource = prd.source_plan;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2091
2209
|
} catch { /* optional */ }
|
|
2092
2210
|
}
|
|
2093
2211
|
|
|
2094
2212
|
invalidateStatusCache();
|
|
2095
|
-
return jsonReply(res, 200, { ok: true,
|
|
2213
|
+
return jsonReply(res, 200, { ok: true, unarchivedSource });
|
|
2096
2214
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
2097
2215
|
}
|
|
2098
2216
|
|
|
@@ -3115,8 +3233,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3115
3233
|
{ method: 'POST', path: '/api/plans/regenerate', desc: 'Reset pending/failed work items for a plan so they re-materialize', params: 'source', handler: handlePlansRegenerate },
|
|
3116
3234
|
{ method: 'POST', path: '/api/plans/delete', desc: 'Delete a plan file and clean up work items', params: 'file', handler: handlePlansDelete },
|
|
3117
3235
|
{ method: 'POST', path: '/api/plans/archive', desc: 'Move a plan/PRD to archive (preserves work items)', params: 'file', handler: handlePlansArchive },
|
|
3236
|
+
{ method: 'POST', path: '/api/plans/unarchive', desc: 'Restore a plan/PRD from archive', params: 'file', handler: handlePlansUnarchive },
|
|
3118
3237
|
{ method: 'POST', path: '/api/plans/revise', desc: 'Request revision with feedback, dispatches agent to revise', params: 'file, feedback, requestedBy?', handler: handlePlansRevise },
|
|
3119
3238
|
{ method: 'POST', path: '/api/plans/discuss', desc: 'Generate a plan discussion session script for Claude CLI', params: 'file', handler: handlePlansDiscuss },
|
|
3239
|
+
{ method: 'POST', path: '/api/plans/archive', desc: 'Archive a plan/PRD (move to archive folder)', params: 'file', handler: handlePlansArchiveMove },
|
|
3240
|
+
{ method: 'POST', path: '/api/plans/unarchive', desc: 'Unarchive a plan/PRD (restore from archive folder)', params: 'file', handler: handlePlansUnarchive },
|
|
3120
3241
|
{ method: 'GET', path: /^\/api\/plans\/archive\/([^?]+)$/, desc: 'Read an archived plan file', handler: handlePlansArchiveRead },
|
|
3121
3242
|
{ method: 'GET', path: /^\/api\/plans\/([^?]+)$/, desc: 'Read a full plan (JSON from prd/ or markdown from plans/)', handler: handlePlansRead },
|
|
3122
3243
|
|
package/engine/ado.js
CHANGED
|
@@ -131,6 +131,10 @@ async function pollPrStatus(config) {
|
|
|
131
131
|
updated = true;
|
|
132
132
|
|
|
133
133
|
if (newStatus === 'merged' || newStatus === 'abandoned') {
|
|
134
|
+
if (pr.reviewStatus === 'waiting') {
|
|
135
|
+
pr.reviewStatus = newStatus === 'merged' ? 'approved' : 'pending';
|
|
136
|
+
e.log('info', `PR ${pr.id} reviewStatus: waiting → ${pr.reviewStatus} (${newStatus})`);
|
|
137
|
+
}
|
|
134
138
|
await engine().handlePostMerge(pr, project, config, newStatus);
|
|
135
139
|
}
|
|
136
140
|
}
|
|
@@ -344,8 +348,8 @@ async function reconcilePrs(config) {
|
|
|
344
348
|
const branch = (adoPr.sourceRefName || '').replace('refs/heads/', '');
|
|
345
349
|
const title = adoPr.title || '';
|
|
346
350
|
// 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-
|
|
351
|
+
const branchMatch = branch.match(/(P-[a-z0-9]{6,})/i) || branch.match(/(PL-W\d+)/i);
|
|
352
|
+
const titleMatch = title.match(/\((P-[a-z0-9]{6,})\)/) || title.match(/\((PL-W\d+)\)/);
|
|
349
353
|
const linkedItemId = branchMatch?.[1] || titleMatch?.[1] || null;
|
|
350
354
|
const linkedItem = linkedItemId ? allItems.find(i => i.id === linkedItemId) : null;
|
|
351
355
|
const confirmedItemId = linkedItem ? linkedItemId : null;
|
|
@@ -368,7 +372,7 @@ async function reconcilePrs(config) {
|
|
|
368
372
|
existingPrs.push({
|
|
369
373
|
id: prId,
|
|
370
374
|
title: (adoPr.title || `PR #${adoPr.pullRequestId}`).slice(0, 120),
|
|
371
|
-
agent: (adoPr.createdBy?.displayName || 'unknown').toLowerCase(),
|
|
375
|
+
agent: (linkedItem?.dispatched_to || adoPr.createdBy?.displayName || 'unknown').toLowerCase(),
|
|
372
376
|
branch,
|
|
373
377
|
reviewStatus: 'pending',
|
|
374
378
|
status: 'active',
|
package/engine/github.js
CHANGED
|
@@ -135,6 +135,11 @@ async function pollPrStatus(config) {
|
|
|
135
135
|
updated = true;
|
|
136
136
|
|
|
137
137
|
if (newStatus === 'merged' || newStatus === 'abandoned') {
|
|
138
|
+
// Resolve stale 'waiting' review status — won't be polled again after this
|
|
139
|
+
if (pr.reviewStatus === 'waiting') {
|
|
140
|
+
pr.reviewStatus = newStatus === 'merged' ? 'approved' : 'pending';
|
|
141
|
+
e.log('info', `PR ${pr.id} reviewStatus: waiting → ${pr.reviewStatus} (${newStatus})`);
|
|
142
|
+
}
|
|
138
143
|
await engine().handlePostMerge(pr, project, config, newStatus);
|
|
139
144
|
}
|
|
140
145
|
}
|
|
@@ -334,7 +339,7 @@ async function reconcilePrs(config) {
|
|
|
334
339
|
for (const ghPr of ghPrs) {
|
|
335
340
|
const prId = `PR-${ghPr.number}`;
|
|
336
341
|
const branch = ghPr.head?.ref || '';
|
|
337
|
-
const wiMatch = branch.match(/(P-[a-
|
|
342
|
+
const wiMatch = branch.match(/(P-[a-z0-9]{6,})/i) || branch.match(/(PL-W\d+)/i);
|
|
338
343
|
const linkedItemId = wiMatch ? wiMatch[1] : null;
|
|
339
344
|
const linkedItem = linkedItemId ? allItems.find(i => i.id === linkedItemId) : null;
|
|
340
345
|
const confirmedItemId = linkedItem ? linkedItemId : null;
|
|
@@ -356,7 +361,7 @@ async function reconcilePrs(config) {
|
|
|
356
361
|
existingPrs.push({
|
|
357
362
|
id: prId,
|
|
358
363
|
title: (ghPr.title || `PR #${ghPr.number}`).slice(0, 120),
|
|
359
|
-
agent: (ghPr.user?.login || 'unknown').toLowerCase(),
|
|
364
|
+
agent: (linkedItem?.dispatched_to || ghPr.user?.login || 'unknown').toLowerCase(),
|
|
360
365
|
branch,
|
|
361
366
|
reviewStatus: 'pending',
|
|
362
367
|
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"
|