@yemi33/minions 0.1.2042 → 0.1.2043
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/dashboard/js/refresh.js +4 -3
- package/dashboard/js/render-dispatch.js +29 -34
- package/dashboard/js/render-inbox.js +7 -9
- package/dashboard/js/render-kb.js +7 -9
- package/dashboard/js/render-utils.js +53 -1
- package/engine/dispatch.js +12 -11
- package/engine/lifecycle.js +9 -9
- package/engine.js +27 -46
- package/package.json +1 -1
package/dashboard/js/refresh.js
CHANGED
|
@@ -67,7 +67,7 @@ const RENDER_VERSIONS = {
|
|
|
67
67
|
agents: 1,
|
|
68
68
|
prdProgress: 1,
|
|
69
69
|
prdPrs: 1,
|
|
70
|
-
inbox:
|
|
70
|
+
inbox: 2,
|
|
71
71
|
projects: 1,
|
|
72
72
|
notes: 1,
|
|
73
73
|
prd: 1,
|
|
@@ -77,8 +77,8 @@ const RENDER_VERSIONS = {
|
|
|
77
77
|
version: 1,
|
|
78
78
|
adoThrottle: 1,
|
|
79
79
|
ghThrottle: 1,
|
|
80
|
-
dispatch:
|
|
81
|
-
engineLog:
|
|
80
|
+
dispatch: 2,
|
|
81
|
+
engineLog: 2,
|
|
82
82
|
metrics: 1,
|
|
83
83
|
workItems: 1,
|
|
84
84
|
skills: 1,
|
|
@@ -88,6 +88,7 @@ const RENDER_VERSIONS = {
|
|
|
88
88
|
meetings: 1,
|
|
89
89
|
pipelines: 1,
|
|
90
90
|
pinned: 1,
|
|
91
|
+
kbPayload: 2,
|
|
91
92
|
};
|
|
92
93
|
const _sectionCache = {};
|
|
93
94
|
const _lastValueByKey = {};
|
|
@@ -191,19 +191,24 @@ function renderDispatch(dispatch) {
|
|
|
191
191
|
'<div class="dispatch-stat"><div class="dispatch-stat-num blue">' + (dispatch.pending || []).length + '</div><div class="dispatch-stat-label">Pending</div></div>' +
|
|
192
192
|
'<div class="dispatch-stat"><div class="dispatch-stat-num green">' + (dispatch.completedTotal || (dispatch.completed || []).length) + '</div><div class="dispatch-stat-label">Completed</div></div>';
|
|
193
193
|
|
|
194
|
+
// Shared row markup for the Active and Pending lists. The two differ only in
|
|
195
|
+
// the trailing chip: Active shows started_at, Pending shows skipReason.
|
|
196
|
+
const dispatchItemHtml = (d, trailing) =>
|
|
197
|
+
'<div class="dispatch-item">' +
|
|
198
|
+
'<span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span>' +
|
|
199
|
+
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
200
|
+
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
201
|
+
renderStuckChip(d) +
|
|
202
|
+
trailing +
|
|
203
|
+
'</div>';
|
|
204
|
+
|
|
194
205
|
// Active
|
|
195
206
|
const activeEl = document.getElementById('dispatch-active');
|
|
196
207
|
if ((dispatch.active || []).length > 0) {
|
|
197
208
|
activeEl.innerHTML = '<div style="font-size:11px;color:var(--green);margin-bottom:6px;font-weight:600">ACTIVE</div><div class="dispatch-list">' +
|
|
198
|
-
dispatch.active.map(d =>
|
|
199
|
-
'<
|
|
200
|
-
|
|
201
|
-
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
202
|
-
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
203
|
-
renderStuckChip(d) +
|
|
204
|
-
'<span class="dispatch-time">' + shortTime(d.started_at) + '</span>' +
|
|
205
|
-
'</div>'
|
|
206
|
-
).join('') + '</div>';
|
|
209
|
+
dispatch.active.map(d => dispatchItemHtml(d,
|
|
210
|
+
'<span class="dispatch-time">' + shortTime(d.started_at) + '</span>'
|
|
211
|
+
)).join('') + '</div>';
|
|
207
212
|
} else {
|
|
208
213
|
activeEl.innerHTML = '<div style="color:var(--muted);font-size:11px;margin-bottom:8px">No active dispatches</div>';
|
|
209
214
|
}
|
|
@@ -212,15 +217,9 @@ function renderDispatch(dispatch) {
|
|
|
212
217
|
const pendingEl = document.getElementById('dispatch-pending');
|
|
213
218
|
if ((dispatch.pending || []).length > 0) {
|
|
214
219
|
pendingEl.innerHTML = '<div style="font-size:11px;color:var(--yellow);margin:8px 0 6px;font-weight:600">PENDING</div><div class="dispatch-list">' +
|
|
215
|
-
dispatch.pending.map(d =>
|
|
216
|
-
'<
|
|
217
|
-
|
|
218
|
-
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
219
|
-
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
220
|
-
renderStuckChip(d) +
|
|
221
|
-
(d.skipReason ? '<span style="font-size:9px;color:var(--muted);margin-left:6px" title="' + escHtml(d.skipReason) + '">' + escHtml(d.skipReason.replace(/_/g, ' ')) + '</span>' : '') +
|
|
222
|
-
'</div>'
|
|
223
|
-
).join('') + '</div>';
|
|
220
|
+
dispatch.pending.map(d => dispatchItemHtml(d,
|
|
221
|
+
d.skipReason ? '<span style="font-size:9px;color:var(--muted);margin-left:6px" title="' + escHtml(d.skipReason) + '">' + escHtml(d.skipReason.replace(/_/g, ' ')) + '</span>' : ''
|
|
222
|
+
)).join('') + '</div>';
|
|
224
223
|
} else {
|
|
225
224
|
pendingEl.innerHTML = '';
|
|
226
225
|
}
|
|
@@ -254,14 +253,12 @@ function renderDispatch(dispatch) {
|
|
|
254
253
|
'<td class="pr-date">' + shortTime(d.completed_at) + '</td>' +
|
|
255
254
|
'</tr>';
|
|
256
255
|
}).join('') + '</tbody></table>';
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
'</div></div>');
|
|
264
|
-
}
|
|
256
|
+
const compPager = renderPager({
|
|
257
|
+
start: compStart, perPage: COMPLETED_PER_PAGE, total: completed.length,
|
|
258
|
+
page: _completedPage, totalPages: totalCompPages,
|
|
259
|
+
onPrev: '_completedPrev()', onNext: '_completedNext()',
|
|
260
|
+
});
|
|
261
|
+
if (compPager) completedEl.insertAdjacentHTML('beforeend', compPager);
|
|
265
262
|
} else {
|
|
266
263
|
completedEl.innerHTML = '<p class="empty">No completed dispatches yet.</p>';
|
|
267
264
|
}
|
|
@@ -288,14 +285,12 @@ function renderEngineLog(log) {
|
|
|
288
285
|
escHtml(e.message || '') +
|
|
289
286
|
'</div>'
|
|
290
287
|
).join('');
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
'</div></div>');
|
|
298
|
-
}
|
|
288
|
+
const logPager = renderPager({
|
|
289
|
+
start: logStart, perPage: LOG_PER_PAGE, total: reversed.length,
|
|
290
|
+
page: _logPage, totalPages: totalLogPages,
|
|
291
|
+
onPrev: '_logPrev()', onNext: '_logNext()',
|
|
292
|
+
});
|
|
293
|
+
if (logPager) el.insertAdjacentHTML('beforeend', logPager);
|
|
299
294
|
}
|
|
300
295
|
|
|
301
296
|
function shortTime(t) {
|
|
@@ -35,21 +35,19 @@ function renderInbox(inbox) {
|
|
|
35
35
|
</div>
|
|
36
36
|
<div class="inbox-preview" onclick="openModal(${idx})" style="cursor:pointer">${escapeHtml(item.content.slice(0,200))}</div>
|
|
37
37
|
<div style="display:flex;gap:6px;margin-top:6px;align-items:center">
|
|
38
|
-
|
|
38
|
+
${pinButton(pk, pinned, 'inbox')}
|
|
39
39
|
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();promoteToKB(this.dataset.inboxName)">Add to Knowledge Base</button>
|
|
40
40
|
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();openInboxInExplorer(this.dataset.inboxName)">Open in Explorer</button>
|
|
41
41
|
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red)" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();deleteInboxItem(this.dataset.inboxName)">Delete</button>
|
|
42
42
|
</div>
|
|
43
43
|
</div>`;
|
|
44
44
|
}).join('');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'</div></div>');
|
|
52
|
-
}
|
|
45
|
+
const inboxPager = renderPager({
|
|
46
|
+
start: inboxStart, perPage: INBOX_PER_PAGE, total: inbox.length,
|
|
47
|
+
page: _inboxPage, totalPages: totalInboxPages,
|
|
48
|
+
onPrev: '_inboxPrev()', onNext: '_inboxNext()',
|
|
49
|
+
});
|
|
50
|
+
if (inboxPager) list.insertAdjacentHTML('beforeend', inboxPager);
|
|
53
51
|
restoreNotifBadges();
|
|
54
52
|
}
|
|
55
53
|
|
|
@@ -142,7 +142,7 @@ function renderKnowledgeBase() {
|
|
|
142
142
|
return '<div class="kb-item' + (pinned ? ' item-pinned' : '') + '" data-file="knowledge/' + escapeHtml(item.category) + '/' + escapeHtml(item.file) + '" onclick="kbOpenItem(\'' + escapeHtml(item.category) + '\', \'' + escapeHtml(item.file) + '\')">' +
|
|
143
143
|
'<div class="kb-item-body">' +
|
|
144
144
|
'<div class="kb-item-title">' + icon + ' ' + escapeHtml(item.title) +
|
|
145
|
-
'
|
|
145
|
+
' ' + pinButton(pinKey, pinned, 'kb', { extraStyle: 'padding:1px 6px;margin-left:6px;vertical-align:middle' }) +
|
|
146
146
|
'</div>' +
|
|
147
147
|
'<div class="kb-item-meta">' +
|
|
148
148
|
'<span>' + label + '</span>' +
|
|
@@ -154,14 +154,12 @@ function renderKnowledgeBase() {
|
|
|
154
154
|
'</div>' +
|
|
155
155
|
'</div>';
|
|
156
156
|
}).join('');
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
'</div></div>');
|
|
164
|
-
}
|
|
157
|
+
const kbPager = renderPager({
|
|
158
|
+
start: kbStart, perPage: KB_PER_PAGE, total: items.length,
|
|
159
|
+
page: _kbPage, totalPages: totalKbPages,
|
|
160
|
+
onPrev: '_kbPrev()', onNext: '_kbNext()',
|
|
161
|
+
});
|
|
162
|
+
if (kbPager) listEl.insertAdjacentHTML('beforeend', kbPager);
|
|
165
163
|
restoreNotifBadges();
|
|
166
164
|
}
|
|
167
165
|
|
|
@@ -352,4 +352,56 @@ function renderAgentOutput(text) {
|
|
|
352
352
|
return fragments.join('');
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Standard "Prev / Next" pager HTML for a paginated list. Used by inbox, KB,
|
|
357
|
+
* completed dispatches, and engine log. The caller is responsible for the
|
|
358
|
+
* `onPrev`/`onNext` JS expression strings — they're inlined as `onclick`
|
|
359
|
+
* attributes, so callers must pass safe identifier-only expressions
|
|
360
|
+
* (e.g. `'_inboxPrev()'`).
|
|
361
|
+
*
|
|
362
|
+
* @param {object} opts
|
|
363
|
+
* @param {number} opts.start - Zero-based start index of the current page.
|
|
364
|
+
* @param {number} opts.perPage - Items per page.
|
|
365
|
+
* @param {number} opts.total - Total items across all pages.
|
|
366
|
+
* @param {number} opts.page - Zero-based current page index.
|
|
367
|
+
* @param {number} opts.totalPages - Total page count.
|
|
368
|
+
* @param {string} opts.onPrev - JS expression for the Prev button's onclick.
|
|
369
|
+
* @param {string} opts.onNext - JS expression for the Next button's onclick.
|
|
370
|
+
* @returns {string} HTML string for the pager (empty when total <= perPage).
|
|
371
|
+
*/
|
|
372
|
+
function renderPager(opts) {
|
|
373
|
+
var start = opts.start, perPage = opts.perPage, total = opts.total;
|
|
374
|
+
var page = opts.page, totalPages = opts.totalPages;
|
|
375
|
+
if (total <= perPage) return '';
|
|
376
|
+
return '<div class="pr-pager">' +
|
|
377
|
+
'<span class="pr-page-info">Showing ' + (start + 1) + ' to ' + Math.min(start + perPage, total) + ' of ' + total + '</span>' +
|
|
378
|
+
'<div class="pr-pager-btns">' +
|
|
379
|
+
'<button class="pr-pager-btn ' + (page === 0 ? 'disabled' : '') + '" onclick="' + opts.onPrev + '">Prev</button>' +
|
|
380
|
+
'<button class="pr-pager-btn ' + (page >= totalPages - 1 ? 'disabled' : '') + '" onclick="' + opts.onNext + '">Next</button>' +
|
|
381
|
+
'</div></div>';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Renders the standard Pin/Unpin chip used in the inbox and KB lists. Both
|
|
386
|
+
* call _togglePinAndRefresh(pinKey, source) on click; the only thing that
|
|
387
|
+
* varies is the source name. The data-pin-key attribute is escaped via
|
|
388
|
+
* escHtml; callers must pass the already-computed pin key.
|
|
389
|
+
*
|
|
390
|
+
* @param {string} pinKey - The pin storage key (e.g. inboxPinKey(name)).
|
|
391
|
+
* @param {boolean} pinned - Current pinned state.
|
|
392
|
+
* @param {string} source - The source name passed to _togglePinAndRefresh
|
|
393
|
+
* (e.g. 'inbox', 'kb').
|
|
394
|
+
* @param {object} [opts]
|
|
395
|
+
* @param {string} [opts.extraStyle] - Inline CSS appended to the button.
|
|
396
|
+
* @returns {string} HTML for the pin button.
|
|
397
|
+
*/
|
|
398
|
+
function pinButton(pinKey, pinned, source, opts) {
|
|
399
|
+
var extraStyle = (opts && opts.extraStyle) || '';
|
|
400
|
+
return '<button class="pr-pager-btn pin-btn' + (pinned ? ' pinned' : '') +
|
|
401
|
+
'" style="font-size:9px;padding:2px 8px;' + extraStyle +
|
|
402
|
+
'" data-pin-key="' + escHtml(pinKey) +
|
|
403
|
+
'" onclick="event.stopPropagation();_togglePinAndRefresh(this.dataset.pinKey,\'' + source + '\')">' +
|
|
404
|
+
(pinned ? 'Unpin' : 'Pin') + '</button>';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
window.MinionsRenderUtils = { formatToolSummary, renderAgentOutput, renderPager, pinButton };
|
package/engine/dispatch.js
CHANGED
|
@@ -9,11 +9,11 @@ const shared = require('./shared');
|
|
|
9
9
|
const queries = require('./queries');
|
|
10
10
|
const { setCooldown, setCooldownFailure } = require('./cooldown');
|
|
11
11
|
|
|
12
|
-
const {
|
|
13
|
-
mutatePullRequests, getProjects,
|
|
12
|
+
const { safeJsonArr, mutateJsonFileLocked, mutateWorkItems,
|
|
13
|
+
mutatePullRequests, getProjects, projectPrPath, log, ts, dateStamp,
|
|
14
14
|
sidecarDispatchPrompt, deleteDispatchPromptSidecar,
|
|
15
15
|
WI_STATUS, WORK_TYPE, DISPATCH_RESULT, ENGINE_DEFAULTS, AGENT_STATUS, FAILURE_CLASS, PR_STATUS } = shared;
|
|
16
|
-
const { getConfig,
|
|
16
|
+
const { getConfig, DISPATCH_PATH, INBOX_DIR } = queries;
|
|
17
17
|
|
|
18
18
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
19
19
|
|
|
@@ -149,10 +149,13 @@ function addToDispatch(item) {
|
|
|
149
149
|
}
|
|
150
150
|
let added = false;
|
|
151
151
|
mutateDispatch((dispatch) => {
|
|
152
|
+
// Walked once per addToDispatch — all three dedup checks below find against the same set.
|
|
153
|
+
const queued = [...dispatch.pending, ...(dispatch.active || [])];
|
|
154
|
+
|
|
152
155
|
// Dedup: skip if same work item ID is already pending or active
|
|
153
156
|
const wiId = item.meta?.source === 'central-work-item-fanout' ? null : item.meta?.item?.id;
|
|
154
157
|
if (wiId) {
|
|
155
|
-
const existing =
|
|
158
|
+
const existing = queued.find(d => d.meta?.item?.id === wiId);
|
|
156
159
|
if (existing) {
|
|
157
160
|
log('info', `Dedup: skipping ${item.id} — work item ${wiId} already in ${existing.id}`);
|
|
158
161
|
return dispatch;
|
|
@@ -160,7 +163,7 @@ function addToDispatch(item) {
|
|
|
160
163
|
}
|
|
161
164
|
// Also dedup by dispatchKey
|
|
162
165
|
if (item.meta?.dispatchKey) {
|
|
163
|
-
const existing =
|
|
166
|
+
const existing = queued.find(d => d.meta?.dispatchKey === item.meta.dispatchKey);
|
|
164
167
|
if (existing) {
|
|
165
168
|
log('info', `Dedup: skipping ${item.id} — dispatchKey ${item.meta.dispatchKey} already in ${existing.id}`);
|
|
166
169
|
return dispatch;
|
|
@@ -168,8 +171,7 @@ function addToDispatch(item) {
|
|
|
168
171
|
}
|
|
169
172
|
const prDedupeKey = getPrDispatchDedupeKey(item);
|
|
170
173
|
if (prDedupeKey) {
|
|
171
|
-
const existing =
|
|
172
|
-
.find(d => getPrDispatchDedupeKey(d) === prDedupeKey);
|
|
174
|
+
const existing = queued.find(d => getPrDispatchDedupeKey(d) === prDedupeKey);
|
|
173
175
|
if (existing) {
|
|
174
176
|
log('info', `Dedup: skipping ${item.id} — PR dispatch ${prDedupeKey} already in ${existing.id}`);
|
|
175
177
|
return dispatch;
|
|
@@ -453,8 +455,8 @@ function readLiveWorkItem(meta) {
|
|
|
453
455
|
if (!itemId) return null;
|
|
454
456
|
const wiPath = lifecycle().resolveWorkItemPath(meta);
|
|
455
457
|
if (!wiPath) return null;
|
|
456
|
-
const items =
|
|
457
|
-
return
|
|
458
|
+
const items = safeJsonArr(wiPath);
|
|
459
|
+
return items.find(i => i.id === itemId) || null;
|
|
458
460
|
}
|
|
459
461
|
|
|
460
462
|
function writeFailedAgentReport(item, reason, resultSummary, failureClass) {
|
|
@@ -882,14 +884,13 @@ function cancelPendingDispatchesForPr(prId) {
|
|
|
882
884
|
* @returns {number} count of removed entries
|
|
883
885
|
*/
|
|
884
886
|
function cleanDispatchEntries(matchFn) {
|
|
885
|
-
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
886
887
|
const tmpDir = path.join(MINIONS_DIR, 'engine', 'tmp');
|
|
887
888
|
let removed = 0;
|
|
888
889
|
const pidsToKill = [];
|
|
889
890
|
const filesToDelete = [];
|
|
890
891
|
const dispatchDirsToRemove = [];
|
|
891
892
|
try {
|
|
892
|
-
mutateJsonFileLocked(
|
|
893
|
+
mutateJsonFileLocked(DISPATCH_PATH, (dispatch) => {
|
|
893
894
|
for (const queue of ['pending', 'active', 'completed']) {
|
|
894
895
|
dispatch[queue] = Array.isArray(dispatch[queue]) ? dispatch[queue] : [];
|
|
895
896
|
const before = dispatch[queue].length;
|
package/engine/lifecycle.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const shared = require('./shared');
|
|
10
|
-
const { safeRead, safeJson, safeJsonNoRestore, safeWrite, mutateJsonFileLocked, mutateWorkItems, execAsync, projectPrPath, getPrLinks,
|
|
10
|
+
const { safeRead, safeJson, safeJsonArr, safeJsonNoRestore, safeWrite, mutateJsonFileLocked, mutateWorkItems, execAsync, projectPrPath, getPrLinks,
|
|
11
11
|
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
12
12
|
ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS, FAILURE_CLASS } = shared;
|
|
13
13
|
const { trackEngineUsage } = require('./llm');
|
|
@@ -113,7 +113,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
113
113
|
for (const p of projects) {
|
|
114
114
|
try {
|
|
115
115
|
const prPath = shared.projectPrPath(p);
|
|
116
|
-
const prs =
|
|
116
|
+
const prs = safeJsonArr(prPath);
|
|
117
117
|
const prLinks = getPrLinks();
|
|
118
118
|
for (const pr of prs) {
|
|
119
119
|
const linkedItemIds = prLinks[pr.id] || [];
|
|
@@ -221,7 +221,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
221
221
|
const projectPrs = {};
|
|
222
222
|
const prLinks = getPrLinks();
|
|
223
223
|
for (const p of projects) {
|
|
224
|
-
const prs = (
|
|
224
|
+
const prs = safeJsonArr(shared.projectPrPath(p))
|
|
225
225
|
.filter(pr => {
|
|
226
226
|
const linkedIds = prLinks[pr.id] || [];
|
|
227
227
|
return pr.status === PR_STATUS.ACTIVE && linkedIds.some(itemId => doneItems.find(w => w.id === itemId));
|
|
@@ -498,7 +498,7 @@ function cleanupPlanWorktrees(planFile, plan, projects, config) {
|
|
|
498
498
|
|
|
499
499
|
for (const p of projects) {
|
|
500
500
|
try {
|
|
501
|
-
const prs =
|
|
501
|
+
const prs = safeJsonArr(shared.projectPrPath(p));
|
|
502
502
|
const prLinks = getPrLinks();
|
|
503
503
|
for (const pr of prs) {
|
|
504
504
|
const linkedIds = prLinks[pr.id] || [];
|
|
@@ -1612,7 +1612,7 @@ function resolveReviewPrContext(pr, project, config, structuredCompletion = null
|
|
|
1612
1612
|
|
|
1613
1613
|
for (const candidateProject of projectCandidates) {
|
|
1614
1614
|
const prPath = shared.projectPrPath(candidateProject);
|
|
1615
|
-
const prs =
|
|
1615
|
+
const prs = safeJsonArr(prPath);
|
|
1616
1616
|
for (const ref of refs) {
|
|
1617
1617
|
const refUrl = typeof ref === 'object' ? ref.url || '' : String(ref || '');
|
|
1618
1618
|
if (!shared.isPrCompatibleWithProject(candidateProject, ref, refUrl)) continue;
|
|
@@ -1622,7 +1622,7 @@ function resolveReviewPrContext(pr, project, config, structuredCompletion = null
|
|
|
1622
1622
|
}
|
|
1623
1623
|
|
|
1624
1624
|
const centralPath = centralPrPath();
|
|
1625
|
-
const centralPrs =
|
|
1625
|
+
const centralPrs = safeJsonArr(centralPath);
|
|
1626
1626
|
const centralRefs = reportedPr ? [reportedPr] : refs;
|
|
1627
1627
|
for (const ref of centralRefs) {
|
|
1628
1628
|
const target = shared.findPrRecord(centralPrs, ref, null);
|
|
@@ -2215,7 +2215,7 @@ function findDependentActivePrs(mergedItemId, config) {
|
|
|
2215
2215
|
|
|
2216
2216
|
const projects = shared.getProjects(config);
|
|
2217
2217
|
for (const p of projects) {
|
|
2218
|
-
const prs =
|
|
2218
|
+
const prs = safeJsonArr(projectPrPath(p));
|
|
2219
2219
|
for (const pr of prs) {
|
|
2220
2220
|
if (!pr.branch || pr.status !== PR_STATUS.ACTIVE) continue;
|
|
2221
2221
|
const linked = (pr.prdItems || []).some(id => dependentWis.some(wi => wi.id === id));
|
|
@@ -4505,12 +4505,12 @@ function syncPrdFromPrs(config) {
|
|
|
4505
4505
|
const allProjects = shared.getProjects(config);
|
|
4506
4506
|
|
|
4507
4507
|
// Exact prdItems match only — no fuzzy matching
|
|
4508
|
-
const allPrs = allProjects.flatMap(p =>
|
|
4508
|
+
const allPrs = allProjects.flatMap(p => safeJsonArr(shared.projectPrPath(p)));
|
|
4509
4509
|
|
|
4510
4510
|
let totalReconciled = 0;
|
|
4511
4511
|
for (const project of allProjects) {
|
|
4512
4512
|
const wiPath = shared.projectWorkItemsPath(project);
|
|
4513
|
-
const items =
|
|
4513
|
+
const items = safeJsonArr(wiPath);
|
|
4514
4514
|
const hasReconcilable = items.some(wi =>
|
|
4515
4515
|
(wi.status === WI_STATUS.PENDING && !wi._pr) || wi.status === WI_STATUS.FAILED);
|
|
4516
4516
|
if (!hasReconcilable) continue;
|
package/engine.js
CHANGED
|
@@ -94,6 +94,8 @@ const { getProjects, projectRoot, projectStateDir, projectWorkItemsPath, project
|
|
|
94
94
|
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
95
95
|
|
|
96
96
|
const safeJson = shared.safeJson;
|
|
97
|
+
const safeJsonArr = shared.safeJsonArr;
|
|
98
|
+
const safeJsonObj = shared.safeJsonObj;
|
|
97
99
|
const safeJsonNoRestore = shared.safeJsonNoRestore;
|
|
98
100
|
const safeRead = shared.safeRead;
|
|
99
101
|
const safeWrite = shared.safeWrite;
|
|
@@ -154,8 +156,6 @@ const { renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS,
|
|
|
154
156
|
// through to an interactive `gh auth login` device-code flow.
|
|
155
157
|
const ghToken = require('./engine/gh-token');
|
|
156
158
|
|
|
157
|
-
// sanitizeBranch imported from shared.js
|
|
158
|
-
|
|
159
159
|
// ─── Lifecycle (extracted to engine/lifecycle.js) ────────────────────────────
|
|
160
160
|
|
|
161
161
|
const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, reconcilePrdStatuses, handlePostMerge, checkPlanCompletion,
|
|
@@ -559,9 +559,9 @@ function resolveDependencyBranches(depIds, sourcePlan, project, config) {
|
|
|
559
559
|
// Find PR branches for each dependency work item
|
|
560
560
|
for (const p of projects) {
|
|
561
561
|
const prPath = shared.projectPrPath(p);
|
|
562
|
-
const prs =
|
|
562
|
+
const prs = safeJsonArr(prPath);
|
|
563
563
|
for (const pr of prs) {
|
|
564
|
-
if (!pr.branch || pr.status !==
|
|
564
|
+
if (!pr.branch || pr.status !== PR_STATUS.ACTIVE) continue;
|
|
565
565
|
const linked = (pr.prdItems || []).some(id =>
|
|
566
566
|
depWorkItems.find(w => w.id === id)
|
|
567
567
|
);
|
|
@@ -1463,10 +1463,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1463
1463
|
// Fetch all dependency branches in parallel (git fetches are independent)
|
|
1464
1464
|
const fetchable = depBranches.filter(d => !_failedRefCache.has(d.branch));
|
|
1465
1465
|
const unfetchable = depBranches.filter(d => _failedRefCache.has(d.branch));
|
|
1466
|
-
const allPrsForDeps = unfetchable.length > 0 ? shared.getProjects(config).reduce((acc, p) => acc.concat(
|
|
1466
|
+
const allPrsForDeps = unfetchable.length > 0 ? shared.getProjects(config).reduce((acc, p) => acc.concat(safeJsonArr(shared.projectPrPath(p))), []) : [];
|
|
1467
1467
|
for (const { branch: depBranch, prId } of unfetchable) {
|
|
1468
1468
|
const pr = allPrsForDeps.find(p => p.id === prId);
|
|
1469
|
-
if (pr && (pr.status ===
|
|
1469
|
+
if (pr && (pr.status === PR_STATUS.MERGED || pr.status === PR_STATUS.CLOSED)) {
|
|
1470
1470
|
log('info', `Dependency ${depBranch} (${prId}) already merged — skipping, changes already in main`);
|
|
1471
1471
|
continue;
|
|
1472
1472
|
}
|
|
@@ -1482,7 +1482,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1482
1482
|
)
|
|
1483
1483
|
);
|
|
1484
1484
|
const hasFetchFailures = fetchResults.some(r => r.status === 'rejected');
|
|
1485
|
-
const allPrsForFetch = hasFetchFailures ? shared.getProjects(config).reduce((acc, p) => acc.concat(
|
|
1485
|
+
const allPrsForFetch = hasFetchFailures ? shared.getProjects(config).reduce((acc, p) => acc.concat(safeJsonArr(shared.projectPrPath(p))), []) : [];
|
|
1486
1486
|
// Track branches recovered by local-only push so they can be merged
|
|
1487
1487
|
const recoveredBranches = new Set();
|
|
1488
1488
|
for (let i = 0; i < fetchResults.length; i++) {
|
|
@@ -1491,7 +1491,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1491
1491
|
const failedPrId = fetchable[i].prId;
|
|
1492
1492
|
const errMsg = fetchResults[i].reason?.message || '';
|
|
1493
1493
|
const pr = allPrsForFetch.find(p => p.id === failedPrId);
|
|
1494
|
-
if (pr && (pr.status ===
|
|
1494
|
+
if (pr && (pr.status === PR_STATUS.MERGED || pr.status === PR_STATUS.CLOSED)) {
|
|
1495
1495
|
log('info', `Dependency ${failedBranch} (${failedPrId}) already merged — skipping, changes already in main`);
|
|
1496
1496
|
continue;
|
|
1497
1497
|
}
|
|
@@ -2079,10 +2079,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2079
2079
|
// orphan detector's "logSize > stub-only" check can tell this apart from a
|
|
2080
2080
|
// hung process. Preserves the diagnostic the prior inline catch wrote.
|
|
2081
2081
|
try { fs.appendFileSync(liveOutputPath, `[${new Date().toISOString()}] spawn-failed: ${spawnErr.message}\n[process-exit] spawn-failed\n`); } catch { /* cleanup-only best effort */ }
|
|
2082
|
-
} else if (proc &&
|
|
2082
|
+
} else if (proc && proc.pid) {
|
|
2083
2083
|
// spawn() returned a handle but a later registration step threw —
|
|
2084
|
-
// kill the orphan child so it doesn't run unmonitored.
|
|
2085
|
-
|
|
2084
|
+
// kill the orphan child so it doesn't run unmonitored. shared.killImmediate
|
|
2085
|
+
// recurses into the process tree (footgun #4) — plain proc.kill('SIGKILL')
|
|
2086
|
+
// doesn't on Windows.
|
|
2087
|
+
try { shared.killImmediate(proc); } catch { /* already exited */ }
|
|
2086
2088
|
}
|
|
2087
2089
|
if (registeredInActiveProcesses) {
|
|
2088
2090
|
try { activeProcesses.delete(id); } catch { /* map.delete never throws but be defensive */ }
|
|
@@ -3271,10 +3273,6 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3271
3273
|
return proc;
|
|
3272
3274
|
}
|
|
3273
3275
|
|
|
3274
|
-
// addToDispatch, isRetryableFailureReason — now in engine/dispatch.js
|
|
3275
|
-
|
|
3276
|
-
// completeDispatch — now in engine/dispatch.js
|
|
3277
|
-
|
|
3278
3276
|
// ─── Dependency Gate ─────────────────────────────────────────────────────────
|
|
3279
3277
|
// Returns: true (deps met), false (deps pending), 'failed' (dep failed — propagate)
|
|
3280
3278
|
function areDependenciesMet(item, config) {
|
|
@@ -3368,9 +3366,6 @@ function detectDependencyCycles(items) {
|
|
|
3368
3366
|
return [...cycleIds];
|
|
3369
3367
|
}
|
|
3370
3368
|
|
|
3371
|
-
|
|
3372
|
-
// writeInboxAlert — now in engine/dispatch.js
|
|
3373
|
-
|
|
3374
3369
|
// Reconciles work items against known PRs.
|
|
3375
3370
|
// Primary linkage comes from prdItems in pull-requests.json; fallback linkage
|
|
3376
3371
|
// uses engine/pr-links.json so matching does not depend on branch/title parsing.
|
|
@@ -3462,10 +3457,6 @@ function updateSnapshot(config) {
|
|
|
3462
3457
|
safeWrite(path.join(IDENTITY_DIR, 'now.md'), snapshot);
|
|
3463
3458
|
}
|
|
3464
3459
|
|
|
3465
|
-
// checkIdleThreshold, checkSteering, checkTimeouts — now in engine/timeout.js
|
|
3466
|
-
|
|
3467
|
-
// runCleanup — now in engine/cleanup.js
|
|
3468
|
-
|
|
3469
3460
|
// ─── Cooldowns (extracted to engine/cooldown.js) ─────────────────────────────
|
|
3470
3461
|
|
|
3471
3462
|
const { COOLDOWN_PATH, dispatchCooldowns, loadCooldowns, saveCooldowns,
|
|
@@ -3473,16 +3464,6 @@ const { COOLDOWN_PATH, dispatchCooldowns, loadCooldowns, saveCooldowns,
|
|
|
3473
3464
|
setCooldownFailure, clearCooldown, getPrReviewCooldownKey, clearLegacyPrReviewCooldown,
|
|
3474
3465
|
isAlreadyDispatched, isBranchActive } = require('./engine/cooldown');
|
|
3475
3466
|
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
/**
|
|
3479
|
-
* Scan ~/.minions/plans/ for plan-generated PRD files → queue implement tasks.
|
|
3480
|
-
* Plans are project-scoped JSON files written by the plan-to-prd playbook.
|
|
3481
|
-
*/
|
|
3482
|
-
/**
|
|
3483
|
-
* Convert plan files into project work items (side-effect, like specs).
|
|
3484
|
-
* Plans write to the target project's work-items.json — picked up by discoverFromWorkItems next tick.
|
|
3485
|
-
*/
|
|
3486
3467
|
// Auto-clean pending/failed work items for a PRD so they re-materialize with updated plan data
|
|
3487
3468
|
function autoCleanPrdWorkItems(prdFile, config) {
|
|
3488
3469
|
const allProjects = getProjects(config);
|
|
@@ -3928,7 +3909,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
3928
3909
|
if (!alreadyExists) {
|
|
3929
3910
|
for (const p of allProjects) {
|
|
3930
3911
|
if (String(p.name || '').toLowerCase() === String(projName || '').toLowerCase()) continue;
|
|
3931
|
-
const otherItems =
|
|
3912
|
+
const otherItems = safeJsonArr(projectWorkItemsPath(p));
|
|
3932
3913
|
const otherWi = otherItems.find(w => w.id === item.id);
|
|
3933
3914
|
if (otherWi) {
|
|
3934
3915
|
if (DONE_STATUSES.has(otherWi.status) && shouldReopen) {
|
|
@@ -3969,7 +3950,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
3969
3950
|
|
|
3970
3951
|
if (created > 0) {
|
|
3971
3952
|
// Reconciliation: exact prdItems match only, scoped to newly created items
|
|
3972
|
-
const allPrsForReconcile = allProjects.flatMap(p =>
|
|
3953
|
+
const allPrsForReconcile = allProjects.flatMap(p => safeJsonArr(projectPrPath(p)));
|
|
3973
3954
|
const reconciled = reconcileItemsWithPrs(existingItems, allPrsForReconcile, { onlyIds: newlyCreatedIds });
|
|
3974
3955
|
if (reconciled > 0) log('info', `Plan reconciliation: marked ${reconciled} item(s) as done → ${projName}`);
|
|
3975
3956
|
|
|
@@ -4950,7 +4931,7 @@ function resolveWorkItemPrRecord(item, project) {
|
|
|
4950
4931
|
if (!project) return null;
|
|
4951
4932
|
const prRef = getWorkItemPrRef(item);
|
|
4952
4933
|
if (!prRef) return null;
|
|
4953
|
-
const prs =
|
|
4934
|
+
const prs = safeJsonArr(projectPrPath(project));
|
|
4954
4935
|
shared.normalizePrRecords(prs, project);
|
|
4955
4936
|
return shared.findPrRecord(prs, prRef, project);
|
|
4956
4937
|
}
|
|
@@ -5037,7 +5018,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
5037
5018
|
}
|
|
5038
5019
|
|
|
5039
5020
|
const root = project?.localPath ? path.resolve(project.localPath) : path.resolve(MINIONS_DIR, '..');
|
|
5040
|
-
const items =
|
|
5021
|
+
const items = safeJsonArr(projectWorkItemsPath(project));
|
|
5041
5022
|
const cooldownMs = (src.cooldownMinutes || 0) * 60 * 1000;
|
|
5042
5023
|
const newWork = [];
|
|
5043
5024
|
// PRD sync for dispatched status deferred to spawnAgent success (#480)
|
|
@@ -5587,7 +5568,7 @@ function extractSpecInfo(filePath, projectRoot_) {
|
|
|
5587
5568
|
*/
|
|
5588
5569
|
function discoverCentralWorkItems(config) {
|
|
5589
5570
|
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
5590
|
-
const items =
|
|
5571
|
+
const items = safeJsonArr(centralPath);
|
|
5591
5572
|
const projects = getProjects(config);
|
|
5592
5573
|
const dispatchProjects = getCentralDispatchProjects(projects);
|
|
5593
5574
|
const newWork = [];
|
|
@@ -6095,11 +6076,11 @@ async function discoverWork(config) {
|
|
|
6095
6076
|
// readdir and read (e.g. concurrent archive), do not resurrect it
|
|
6096
6077
|
// from a stale .backup sidecar (W-mouptdh1000h9f39).
|
|
6097
6078
|
const plan = safeJsonNoRestore(path.join(prdDir, f));
|
|
6098
|
-
if (!plan?.missing_features || plan.status ===
|
|
6099
|
-
if (plan?.status ===
|
|
6079
|
+
if (!plan?.missing_features || plan.status === PLAN_STATUS.COMPLETED) {
|
|
6080
|
+
if (plan?.status === PLAN_STATUS.COMPLETED) completedPlanCache.add(f);
|
|
6100
6081
|
continue;
|
|
6101
6082
|
}
|
|
6102
|
-
if (plan.status !==
|
|
6083
|
+
if (plan.status !== PLAN_STATUS.APPROVED && plan.status !== PLAN_STATUS.ACTIVE) continue;
|
|
6103
6084
|
// Simulate the meta object checkPlanCompletion expects
|
|
6104
6085
|
const completed = lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
|
|
6105
6086
|
if (completed) completedPlanCache.add(f);
|
|
@@ -6407,14 +6388,14 @@ async function tickInner() {
|
|
|
6407
6388
|
const projects = getProjects(config);
|
|
6408
6389
|
const pullRequests = projects.flatMap(p => {
|
|
6409
6390
|
const prPath = path.join(MINIONS_DIR, 'projects', p.name, 'pull-requests.json');
|
|
6410
|
-
return
|
|
6391
|
+
return safeJsonArr(prPath);
|
|
6411
6392
|
});
|
|
6412
6393
|
const workItems = projects.flatMap(p => {
|
|
6413
6394
|
const wiPath = path.join(MINIONS_DIR, 'projects', p.name, 'work-items.json');
|
|
6414
|
-
return
|
|
6395
|
+
return safeJsonArr(wiPath);
|
|
6415
6396
|
});
|
|
6416
6397
|
// Also include central work items
|
|
6417
|
-
const centralWi =
|
|
6398
|
+
const centralWi = safeJsonArr(path.join(MINIONS_DIR, 'work-items.json'));
|
|
6418
6399
|
|
|
6419
6400
|
// Gather state for the new generalized target types. Each block is
|
|
6420
6401
|
// best-effort — if a module/file is missing the watch evaluator will
|
|
@@ -6438,7 +6419,7 @@ async function tickInner() {
|
|
|
6438
6419
|
} catch { /* optional */ }
|
|
6439
6420
|
|
|
6440
6421
|
let scheduleRuns = {};
|
|
6441
|
-
try { scheduleRuns =
|
|
6422
|
+
try { scheduleRuns = safeJsonObj(path.join(MINIONS_DIR, 'engine', 'schedule-runs.json')); } catch { /* optional */ }
|
|
6442
6423
|
|
|
6443
6424
|
let pipelineRuns = {};
|
|
6444
6425
|
try { pipelineRuns = require('./engine/pipeline').getPipelineRuns(); } catch { /* optional */ }
|
|
@@ -6508,10 +6489,10 @@ async function tickInner() {
|
|
|
6508
6489
|
continue;
|
|
6509
6490
|
}
|
|
6510
6491
|
const plan = safeJson(path.join(PRD_DIR, file));
|
|
6511
|
-
if (plan && plan.missing_features && plan.status !==
|
|
6492
|
+
if (plan && plan.missing_features && plan.status !== PLAN_STATUS.COMPLETED) {
|
|
6512
6493
|
const completed = checkPlanCompletion({ item: { sourcePlan: file } }, config);
|
|
6513
6494
|
if (completed) completedPlanCache.add(file);
|
|
6514
|
-
} else if (plan?.status ===
|
|
6495
|
+
} else if (plan?.status === PLAN_STATUS.COMPLETED) {
|
|
6515
6496
|
completedPlanCache.add(file);
|
|
6516
6497
|
}
|
|
6517
6498
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2043",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|