@yemi33/minions 0.1.808 → 0.1.809
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 +2 -1
- package/dashboard/js/command-center.js +2 -2
- package/dashboard/js/modal-qa.js +19 -13
- package/dashboard/js/render-other.js +4 -2
- package/dashboard/layout.html +3 -0
- package/dashboard.js +19 -9
- package/engine/ado.js +37 -22
- package/engine/cleanup.js +93 -0
- package/engine/github.js +11 -19
- package/engine/shared.js +19 -2
- package/engine/timeout.js +6 -0
- package/engine.js +4 -3
- package/package.json +1 -1
- package/prompts/cc-system.md +24 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.809 (2026-04-10)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- CC race condition fixes, doc-chat expand, settings save fix, metric rate
|
|
6
7
|
- configurable ignored comment authors — auto-filtered, never trigger fixes
|
|
7
8
|
- add open-source community sources to agentic research pipeline
|
|
8
9
|
- broaden daily pipeline research from Anthropic-only to full agentic space
|
|
@@ -379,10 +379,10 @@ async function ccSend() {
|
|
|
379
379
|
var wasAborted = await _ccDoSend(message);
|
|
380
380
|
|
|
381
381
|
// Flush queued messages — combine into single request
|
|
382
|
-
if (tab._queue && tab._queue.length > 0
|
|
382
|
+
if (tab._queue && tab._queue.length > 0) {
|
|
383
383
|
var combined = tab._queue.splice(0).join('\n\n');
|
|
384
384
|
_renderQueueIndicator();
|
|
385
|
-
|
|
385
|
+
await _ccDoSend(combined);
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -26,6 +26,7 @@ let _qaHistory = []; // multi-turn conversation history [{role:'user',text:''},{
|
|
|
26
26
|
let _qaProcessing = false; // true while waiting for response
|
|
27
27
|
let _qaAbortController = null;
|
|
28
28
|
let _qaQueue = []; // queued messages while processing
|
|
29
|
+
const QA_QUEUE_CAP = 10; // max queued messages
|
|
29
30
|
let _qaSessionKey = ''; // key for current conversation (title or filePath)
|
|
30
31
|
const _qaSessions = new Map(); // persist conversations across modal open/close {key → {history, threadHtml}}
|
|
31
32
|
// Restore from localStorage
|
|
@@ -103,7 +104,9 @@ function _initQaSession() {
|
|
|
103
104
|
_qaHistory = [];
|
|
104
105
|
document.getElementById('modal-qa-thread').innerHTML = '';
|
|
105
106
|
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
107
|
+
var expandBar = document.getElementById('qa-expand-bar');
|
|
106
108
|
if (wrap) wrap.style.display = 'none';
|
|
109
|
+
if (expandBar) expandBar.style.display = 'none';
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -113,7 +116,9 @@ function clearQaConversation() {
|
|
|
113
116
|
_qaProcessing = false;
|
|
114
117
|
document.getElementById('modal-qa-thread').innerHTML = '';
|
|
115
118
|
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
119
|
+
var expandBar = document.getElementById('qa-expand-bar');
|
|
116
120
|
if (wrap) wrap.style.display = 'none';
|
|
121
|
+
if (expandBar) expandBar.style.display = 'none';
|
|
117
122
|
if (_qaSessionKey) _qaSessions.delete(_qaSessionKey);
|
|
118
123
|
}
|
|
119
124
|
|
|
@@ -156,10 +161,14 @@ function modalSend() {
|
|
|
156
161
|
document.getElementById('modal-qa-pill').style.display = 'none';
|
|
157
162
|
|
|
158
163
|
if (_qaProcessing) {
|
|
164
|
+
if (_qaQueue.length >= QA_QUEUE_CAP) {
|
|
165
|
+
showToast('cmd-toast', 'Queue full — wait for current response', false);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
159
168
|
// Queue the message — show it as "queued" in the thread
|
|
160
169
|
_qaQueue.push({ message, selection });
|
|
161
|
-
const preview = message.
|
|
162
|
-
thread.innerHTML += '<div class="
|
|
170
|
+
const preview = escHtml(message.length > 60 ? message.slice(0, 57) + '...' : message);
|
|
171
|
+
thread.innerHTML += '<div class="qa-queued-item" style="color:var(--muted);font-size:10px;padding:4px 8px">Queued: "' + preview + '"</div>';
|
|
163
172
|
thread.scrollTop = thread.scrollHeight;
|
|
164
173
|
return;
|
|
165
174
|
}
|
|
@@ -320,10 +329,9 @@ async function _processQaMessage(message, selection) {
|
|
|
320
329
|
// Process next queued message
|
|
321
330
|
if (_qaQueue.length > 0) {
|
|
322
331
|
const next = _qaQueue.shift();
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
332
|
+
// Remove the first queued indicator (matches the message being sent now)
|
|
333
|
+
const queuedEl = thread.querySelector('.qa-queued-item');
|
|
334
|
+
if (queuedEl) queuedEl.remove();
|
|
327
335
|
_processQaMessage(next.message, next.selection);
|
|
328
336
|
} else if (modalIsOpen) {
|
|
329
337
|
document.getElementById('modal-qa-input')?.focus();
|
|
@@ -339,20 +347,18 @@ function qaAbort() {
|
|
|
339
347
|
|
|
340
348
|
function toggleDocChat() {
|
|
341
349
|
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
350
|
+
var expandBar = document.getElementById('qa-expand-bar');
|
|
342
351
|
if (!wrap) return;
|
|
343
352
|
var visible = wrap.style.display !== 'none';
|
|
344
353
|
wrap.style.display = visible ? 'none' : '';
|
|
345
|
-
|
|
346
|
-
if (btn) btn.innerHTML = visible ? '▲ Expand' : '▼ Collapse';
|
|
354
|
+
if (expandBar) expandBar.style.display = visible ? '' : 'none';
|
|
347
355
|
}
|
|
348
356
|
|
|
349
357
|
function _showThreadWrap() {
|
|
350
358
|
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (btn) btn.innerHTML = '▼ Collapse';
|
|
355
|
-
}
|
|
359
|
+
var expandBar = document.getElementById('qa-expand-bar');
|
|
360
|
+
if (wrap) wrap.style.display = '';
|
|
361
|
+
if (expandBar) expandBar.style.display = 'none';
|
|
356
362
|
}
|
|
357
363
|
|
|
358
364
|
window.MinionsQA = { showModalQa, modalAskAboutSelection, clearQaSelection, clearQaConversation, modalSend, qaAbort, toggleDocChat, _showThreadWrap };
|
|
@@ -73,8 +73,10 @@ function renderMetrics(metrics) {
|
|
|
73
73
|
|
|
74
74
|
let html = '<table class="pr-table"><thead><tr><th>Agent</th><th>Done</th><th>Errors</th><th>PRs</th><th>Approved</th><th>Rejected</th><th>Rate</th><th>Reviews</th><th>Avg Runtime</th></tr></thead><tbody>';
|
|
75
75
|
for (const [id, m] of rows) {
|
|
76
|
-
const
|
|
77
|
-
const
|
|
76
|
+
const totalTasks = (m.tasksCompleted || 0) + (m.tasksErrored || 0);
|
|
77
|
+
const ratio = totalTasks > 0 ? m.tasksCompleted / totalTasks : 0;
|
|
78
|
+
const rate = totalTasks > 0 ? Math.round(ratio * 100) + '%' : '-';
|
|
79
|
+
const rateColor = totalTasks > 0 ? (ratio >= 0.7 ? 'var(--green)' : 'var(--red)') : 'var(--muted)';
|
|
78
80
|
html += '<tr>' +
|
|
79
81
|
'<td style="font-weight:600">' + escHtml(id) + '</td>' +
|
|
80
82
|
'<td style="color:var(--green)">' + (m.tasksCompleted || 0) + '</td>' +
|
package/dashboard/layout.html
CHANGED
|
@@ -109,6 +109,9 @@
|
|
|
109
109
|
</div>
|
|
110
110
|
<div class="modal-body" id="modal-body"></div>
|
|
111
111
|
<div class="modal-qa" id="modal-qa" style="display:none">
|
|
112
|
+
<div id="qa-expand-bar" style="display:none;text-align:center;padding:2px 0">
|
|
113
|
+
<button style="background:none;border:none;color:var(--muted);font-size:10px;cursor:pointer;padding:2px 6px" onclick="toggleDocChat()" title="Expand thread">▲ Expand chat</button>
|
|
114
|
+
</div>
|
|
112
115
|
<div id="modal-qa-thread-wrap" style="display:none">
|
|
113
116
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
|
114
117
|
<button id="qa-clear-btn" style="background:none;border:none;color:var(--muted);font-size:10px;cursor:pointer;padding:2px 6px" onclick="clearQaConversation()" title="Clear conversation">Clear</button>
|
package/dashboard.js
CHANGED
|
@@ -746,7 +746,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
746
746
|
store: 'doc', sessionKey,
|
|
747
747
|
extraContext: docContext, label: 'doc-chat',
|
|
748
748
|
allowedTools: canEdit ? 'Read,Write,Edit,Glob,Grep' : 'Read,Glob,Grep',
|
|
749
|
-
maxTurns: canEdit ?
|
|
749
|
+
maxTurns: canEdit ? 25 : 10,
|
|
750
750
|
skipStatePreamble: true,
|
|
751
751
|
...(model ? { model } : {}),
|
|
752
752
|
});
|
|
@@ -2868,12 +2868,21 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2868
2868
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
2869
2869
|
}
|
|
2870
2870
|
|
|
2871
|
+
const docChatInFlight = new Set(); // per-document concurrency guard
|
|
2871
2872
|
async function handleDocChat(req, res) {
|
|
2872
2873
|
try {
|
|
2873
2874
|
const body = await readBody(req);
|
|
2874
2875
|
if (!body.message) return jsonReply(res, 400, { error: 'message required' });
|
|
2875
2876
|
if (!body.document) return jsonReply(res, 400, { error: 'document required' });
|
|
2876
2877
|
|
|
2878
|
+
// Per-document concurrency guard — prevent parallel writes to same file
|
|
2879
|
+
const docKey = body.filePath || body.title || 'default';
|
|
2880
|
+
if (docChatInFlight.has(docKey)) {
|
|
2881
|
+
return jsonReply(res, 429, { error: 'This document is already being processed — wait for the current response.' });
|
|
2882
|
+
}
|
|
2883
|
+
docChatInFlight.add(docKey);
|
|
2884
|
+
|
|
2885
|
+
try {
|
|
2877
2886
|
const canEdit = !!body.filePath;
|
|
2878
2887
|
const isJson = body.filePath?.endsWith('.json');
|
|
2879
2888
|
let currentContent = body.document;
|
|
@@ -2999,6 +3008,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2999
3008
|
return jsonReply(res, 200, { ok: true, answer, edited: true, content, actions, pausedPrd });
|
|
3000
3009
|
}
|
|
3001
3010
|
return jsonReply(res, 200, { ok: true, answer: answer + '\n\n(Read-only — changes not saved)', edited: false, actions });
|
|
3011
|
+
} finally { docChatInFlight.delete(docKey); }
|
|
3002
3012
|
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
3003
3013
|
}
|
|
3004
3014
|
|
|
@@ -3682,6 +3692,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3682
3692
|
if (!config.claude) config.claude = {};
|
|
3683
3693
|
if (!config.agents) config.agents = {};
|
|
3684
3694
|
|
|
3695
|
+
const _clamped = [];
|
|
3685
3696
|
if (body.engine) {
|
|
3686
3697
|
const e = body.engine;
|
|
3687
3698
|
const D = shared.ENGINE_DEFAULTS;
|
|
@@ -3695,14 +3706,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3695
3706
|
versionCheckInterval: [60000],
|
|
3696
3707
|
maxBuildFixAttempts: [1, 10],
|
|
3697
3708
|
};
|
|
3698
|
-
const clamped = [];
|
|
3699
3709
|
for (const [key, [min, max]] of Object.entries(numericFields)) {
|
|
3700
3710
|
if (e[key] !== undefined) {
|
|
3701
3711
|
let val = Number(e[key]) || D[key];
|
|
3702
3712
|
const raw = val;
|
|
3703
3713
|
val = Math.max(min, val);
|
|
3704
3714
|
if (max !== undefined) val = Math.min(max, val);
|
|
3705
|
-
if (val !== raw)
|
|
3715
|
+
if (val !== raw) _clamped.push(`${key}: ${raw} → ${val} (range: ${min}–${max || '∞'})`);
|
|
3706
3716
|
config.engine[key] = val;
|
|
3707
3717
|
}
|
|
3708
3718
|
}
|
|
@@ -3766,10 +3776,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3766
3776
|
reloadConfig();
|
|
3767
3777
|
invalidateStatusCache();
|
|
3768
3778
|
console.log('[settings] Saved config.json — engine keys:', Object.keys(config.engine || {}));
|
|
3769
|
-
const msg =
|
|
3770
|
-
? 'Settings saved. Some values were adjusted: ' +
|
|
3779
|
+
const msg = (_clamped.length > 0)
|
|
3780
|
+
? 'Settings saved. Some values were adjusted: ' + _clamped.join('; ')
|
|
3771
3781
|
: 'Settings saved. Engine picks up changes on next tick.';
|
|
3772
|
-
return jsonReply(res, 200, { ok: true, message: msg, clamped });
|
|
3782
|
+
return jsonReply(res, 200, { ok: true, message: msg, clamped: _clamped });
|
|
3773
3783
|
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
3774
3784
|
}
|
|
3775
3785
|
|
|
@@ -4040,13 +4050,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4040
4050
|
description: '',
|
|
4041
4051
|
agent: 'human',
|
|
4042
4052
|
branch: '',
|
|
4043
|
-
reviewStatus:
|
|
4044
|
-
status:
|
|
4053
|
+
reviewStatus: 'pending',
|
|
4054
|
+
status: 'active',
|
|
4045
4055
|
created: new Date().toISOString(),
|
|
4046
4056
|
url,
|
|
4047
4057
|
prdItems: [],
|
|
4048
4058
|
_manual: true,
|
|
4049
|
-
|
|
4059
|
+
_contextOnly: !autoObserve,
|
|
4050
4060
|
_context: context || '',
|
|
4051
4061
|
});
|
|
4052
4062
|
return prs;
|
package/engine/ado.js
CHANGED
|
@@ -189,7 +189,7 @@ async function forEachActivePr(config, token, callback) {
|
|
|
189
189
|
if (!project.adoOrg || !project.adoProject || !project.repositoryId) continue;
|
|
190
190
|
|
|
191
191
|
const prs = getPrs(project);
|
|
192
|
-
const activePrs = prs.filter(pr => pr.status
|
|
192
|
+
const activePrs = prs.filter(pr => shared.PR_POLLABLE_STATUSES.has(pr.status));
|
|
193
193
|
if (activePrs.length === 0) continue;
|
|
194
194
|
|
|
195
195
|
let projectUpdated = 0;
|
|
@@ -212,11 +212,18 @@ async function forEachActivePr(config, token, callback) {
|
|
|
212
212
|
|
|
213
213
|
if (projectUpdated > 0) {
|
|
214
214
|
mutateJsonFileLocked(shared.projectPrPath(project), (currentPrs) => {
|
|
215
|
-
//
|
|
216
|
-
//
|
|
215
|
+
// Merge back updated PRs — preserve disk values that may have been changed
|
|
216
|
+
// by other writers between poll start and this write
|
|
217
217
|
for (const updatedPr of activePrs) {
|
|
218
218
|
const idx = currentPrs.findIndex(p => p.id === updatedPr.id);
|
|
219
|
-
if (idx >= 0)
|
|
219
|
+
if (idx >= 0) {
|
|
220
|
+
// Never downgrade reviewStatus from 'approved' — it's a permanent terminal state
|
|
221
|
+
// The disk version may have been set to 'approved' by another writer after we read
|
|
222
|
+
if (currentPrs[idx].reviewStatus === 'approved' && updatedPr.reviewStatus !== 'approved') {
|
|
223
|
+
updatedPr.reviewStatus = 'approved';
|
|
224
|
+
}
|
|
225
|
+
currentPrs[idx] = updatedPr;
|
|
226
|
+
}
|
|
220
227
|
// Don't push if not found — it was deleted by another writer, respect that
|
|
221
228
|
}
|
|
222
229
|
// Remove duplicates — prefer merged/abandoned over active
|
|
@@ -297,6 +304,12 @@ async function pollPrStatus(config) {
|
|
|
297
304
|
pr._adoHeadCommit = headCommit;
|
|
298
305
|
updated = true;
|
|
299
306
|
}
|
|
307
|
+
// Track source commit separately — detects actual author pushes vs target branch updates
|
|
308
|
+
const sourceCommit = prData.lastMergeSourceCommit?.commitId || '';
|
|
309
|
+
if (sourceCommit && pr._adoSourceCommit !== sourceCommit) {
|
|
310
|
+
pr._adoSourceCommit = sourceCommit;
|
|
311
|
+
updated = true;
|
|
312
|
+
}
|
|
300
313
|
|
|
301
314
|
const reviewers = prData.reviewers || [];
|
|
302
315
|
const votes = reviewers.map(r => r.vote).filter(v => v !== undefined);
|
|
@@ -304,6 +317,20 @@ async function pollPrStatus(config) {
|
|
|
304
317
|
// Once approved, it stays approved permanently
|
|
305
318
|
if (pr.reviewStatus === 'approved') {
|
|
306
319
|
newReviewStatus = 'approved';
|
|
320
|
+
// Re-approve: ADO resets votes when target branch (master) advances, even though
|
|
321
|
+
// the source branch is unchanged. Re-apply the approval vote via API.
|
|
322
|
+
if (!votes.some(v => v >= 5) && sourceCommit && pr._adoSourceCommit === sourceCommit) {
|
|
323
|
+
try {
|
|
324
|
+
const identityData = await adoFetch(`${orgBase}/_apis/connectionData?api-version=7.1`, token).catch(() => null);
|
|
325
|
+
const myId = identityData?.authenticatedUser?.id;
|
|
326
|
+
if (myId) {
|
|
327
|
+
await adoFetch(`${repoBase}/reviewers/${myId}?api-version=7.1`, token, {
|
|
328
|
+
method: 'PUT', body: JSON.stringify({ vote: 10 })
|
|
329
|
+
});
|
|
330
|
+
log('info', `PR ${pr.id}: re-applied approval vote (ADO reset due to target branch update)`);
|
|
331
|
+
}
|
|
332
|
+
} catch (e) { log('warn', `PR ${pr.id}: failed to re-apply approval: ${e.message}`); }
|
|
333
|
+
}
|
|
307
334
|
} else if (votes.length > 0) {
|
|
308
335
|
if (votes.some(v => v === -10)) {
|
|
309
336
|
if (pr.reviewStatus === 'waiting' && pr.minionsReview?.fixedAt && (!pr.lastPushedAt || pr.lastPushedAt <= pr.minionsReview.fixedAt)) {
|
|
@@ -334,24 +361,10 @@ async function pollPrStatus(config) {
|
|
|
334
361
|
log('info', `PR ${pr.id} reviewStatus: ${pr.reviewStatus} → ${newReviewStatus}`);
|
|
335
362
|
pr.reviewStatus = newReviewStatus;
|
|
336
363
|
updated = true;
|
|
337
|
-
|
|
338
|
-
if (newReviewStatus === 'approved'
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
const metricsPath = path.join(__dirname, 'metrics.json');
|
|
343
|
-
mutateJsonFileLocked(metricsPath, (metrics) => {
|
|
344
|
-
if (!metrics[authorId]) metrics[authorId] = {};
|
|
345
|
-
if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
|
|
346
|
-
else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
|
|
347
|
-
return metrics;
|
|
348
|
-
});
|
|
349
|
-
} catch (err) { log('warn', `Metrics update: ${err.message}`); }
|
|
350
|
-
}
|
|
351
|
-
if (newReviewStatus === 'approved') {
|
|
352
|
-
delete pr._reviewFixCycles;
|
|
353
|
-
delete pr._evalEscalated;
|
|
354
|
-
}
|
|
364
|
+
shared.trackReviewMetric(pr, newReviewStatus, config);
|
|
365
|
+
if (newReviewStatus === 'approved') {
|
|
366
|
+
delete pr._reviewFixCycles;
|
|
367
|
+
delete pr._evalEscalated;
|
|
355
368
|
}
|
|
356
369
|
}
|
|
357
370
|
|
|
@@ -507,6 +520,8 @@ async function pollPrHumanComments(config) {
|
|
|
507
520
|
const ignoredAuthors = (config.engine?.ignoredCommentAuthors || []).map(a => a.toLowerCase());
|
|
508
521
|
|
|
509
522
|
for (const thread of threads) {
|
|
523
|
+
// Skip resolved/closed threads — only process active (1) and pending (6)
|
|
524
|
+
if (thread.status && thread.status !== 'active' && thread.status !== 1 && thread.status !== 6) continue;
|
|
510
525
|
for (const comment of (thread.comments || [])) {
|
|
511
526
|
if (!comment.content || comment.commentType === 'system') continue;
|
|
512
527
|
const content = comment.content;
|
package/engine/cleanup.js
CHANGED
|
@@ -582,6 +582,99 @@ function runCleanup(config, verbose = false) {
|
|
|
582
582
|
}
|
|
583
583
|
} catch (e) { log('warn', 'migrate PRD legacy statuses: ' + e.message); }
|
|
584
584
|
|
|
585
|
+
// 10. Prune CC tab sessions — cap at 50 entries, remove oldest beyond cap
|
|
586
|
+
cleaned.ccSessions = 0;
|
|
587
|
+
try {
|
|
588
|
+
const ccSessionsPath = path.join(ENGINE_DIR, 'cc-sessions.json');
|
|
589
|
+
const sessions = shared.safeJsonArr(ccSessionsPath);
|
|
590
|
+
const CC_SESSIONS_CAP = 50;
|
|
591
|
+
if (sessions.length > CC_SESSIONS_CAP) {
|
|
592
|
+
// Sort by lastActiveAt descending, keep newest
|
|
593
|
+
sessions.sort((a, b) => new Date(b.lastActiveAt || 0) - new Date(a.lastActiveAt || 0));
|
|
594
|
+
const pruned = sessions.slice(0, CC_SESSIONS_CAP);
|
|
595
|
+
cleaned.ccSessions = sessions.length - pruned.length;
|
|
596
|
+
safeWrite(ccSessionsPath, pruned);
|
|
597
|
+
}
|
|
598
|
+
} catch (e) { log('warn', 'prune cc-sessions: ' + e.message); }
|
|
599
|
+
|
|
600
|
+
// 10b. Prune doc-chat sessions — cap at 100 entries, remove oldest beyond cap
|
|
601
|
+
cleaned.docSessions = 0;
|
|
602
|
+
try {
|
|
603
|
+
const docSessionsPath = path.join(ENGINE_DIR, 'doc-sessions.json');
|
|
604
|
+
const docSessions = safeJson(docSessionsPath);
|
|
605
|
+
if (docSessions && typeof docSessions === 'object') {
|
|
606
|
+
const entries = Object.entries(docSessions);
|
|
607
|
+
const DOC_SESSIONS_CAP = 100;
|
|
608
|
+
if (entries.length > DOC_SESSIONS_CAP) {
|
|
609
|
+
entries.sort((a, b) => new Date(b[1].lastActiveAt || 0) - new Date(a[1].lastActiveAt || 0));
|
|
610
|
+
const keep = Object.fromEntries(entries.slice(0, DOC_SESSIONS_CAP));
|
|
611
|
+
cleaned.docSessions = entries.length - DOC_SESSIONS_CAP;
|
|
612
|
+
safeWrite(docSessionsPath, keep);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
} catch (e) { log('warn', 'prune doc-sessions: ' + e.message); }
|
|
616
|
+
|
|
617
|
+
// 11. Cap cooldowns.json — keep at most 500 entries (on top of 24h TTL in cooldown.js)
|
|
618
|
+
cleaned.cooldowns = 0;
|
|
619
|
+
try {
|
|
620
|
+
const cooldownPath = path.join(ENGINE_DIR, 'cooldowns.json');
|
|
621
|
+
const cooldowns = safeJson(cooldownPath);
|
|
622
|
+
if (cooldowns && typeof cooldowns === 'object') {
|
|
623
|
+
const entries = Object.entries(cooldowns);
|
|
624
|
+
const COOLDOWN_CAP = 500;
|
|
625
|
+
if (entries.length > COOLDOWN_CAP) {
|
|
626
|
+
// Keep most recent by timestamp
|
|
627
|
+
entries.sort((a, b) => (b[1].timestamp || 0) - (a[1].timestamp || 0));
|
|
628
|
+
const keep = Object.fromEntries(entries.slice(0, COOLDOWN_CAP));
|
|
629
|
+
cleaned.cooldowns = entries.length - COOLDOWN_CAP;
|
|
630
|
+
safeWrite(cooldownPath, keep);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} catch (e) { log('warn', 'cap cooldowns: ' + e.message); }
|
|
634
|
+
|
|
635
|
+
// 12. Clean stale PID files — remove PID files whose process is no longer running
|
|
636
|
+
cleaned.pidFiles = 0;
|
|
637
|
+
try {
|
|
638
|
+
const tmpDir = path.join(ENGINE_DIR, 'tmp');
|
|
639
|
+
if (fs.existsSync(tmpDir)) {
|
|
640
|
+
let pidDirEntries;
|
|
641
|
+
try { pidDirEntries = fs.readdirSync(tmpDir); } catch { pidDirEntries = []; }
|
|
642
|
+
const activePids = new Set();
|
|
643
|
+
for (const [, info] of activeProcesses) {
|
|
644
|
+
if (info.proc?.pid) activePids.add(String(info.proc.pid));
|
|
645
|
+
}
|
|
646
|
+
for (const f of pidDirEntries) {
|
|
647
|
+
if (!f.startsWith('pid-') || !f.endsWith('.pid')) continue;
|
|
648
|
+
const fp = path.join(tmpDir, f);
|
|
649
|
+
try {
|
|
650
|
+
const pidStr = fs.readFileSync(fp, 'utf8').trim();
|
|
651
|
+
// Skip if actively tracked
|
|
652
|
+
if (activePids.has(pidStr)) continue;
|
|
653
|
+
// Check if file is stale (>1 hour old)
|
|
654
|
+
const stat = fs.statSync(fp);
|
|
655
|
+
if (stat.mtimeMs < oneHourAgo) {
|
|
656
|
+
fs.unlinkSync(fp);
|
|
657
|
+
cleaned.pidFiles++;
|
|
658
|
+
}
|
|
659
|
+
} catch { /* cleanup */ }
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
} catch (e) { log('warn', 'clean stale PID files: ' + e.message); }
|
|
663
|
+
|
|
664
|
+
// 13. Prune test-results.json — keep last 200 entries
|
|
665
|
+
try {
|
|
666
|
+
const testResultsPath = path.join(ENGINE_DIR, 'test-results.json');
|
|
667
|
+
const results = shared.safeJsonArr(testResultsPath);
|
|
668
|
+
const TEST_RESULTS_CAP = 200;
|
|
669
|
+
if (results.length > TEST_RESULTS_CAP) {
|
|
670
|
+
safeWrite(testResultsPath, results.slice(-TEST_RESULTS_CAP));
|
|
671
|
+
}
|
|
672
|
+
} catch { /* optional — file may not exist */ }
|
|
673
|
+
|
|
674
|
+
if (cleaned.ccSessions + cleaned.docSessions + cleaned.cooldowns + cleaned.pidFiles > 0) {
|
|
675
|
+
log('info', `Cleanup (resources): ${cleaned.ccSessions} cc-sessions, ${cleaned.docSessions} doc-sessions, ${cleaned.cooldowns} cooldowns, ${cleaned.pidFiles} PID files`);
|
|
676
|
+
}
|
|
677
|
+
|
|
585
678
|
return cleaned;
|
|
586
679
|
}
|
|
587
680
|
|
package/engine/github.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const shared = require('./shared');
|
|
8
|
-
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, mutateJsonFileLocked, MINIONS_DIR, addPrLink, getPrLinks, log, ts, dateStamp, PR_STATUS } = shared;
|
|
8
|
+
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, mutateJsonFileLocked, MINIONS_DIR, addPrLink, getPrLinks, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES } = shared;
|
|
9
9
|
const { getPrs } = require('./queries');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -164,7 +164,7 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
164
164
|
if (isSlugInBackoff(slug)) continue;
|
|
165
165
|
|
|
166
166
|
const prs = getPrs(project);
|
|
167
|
-
const activePrs = prs.filter(pr => pr.status
|
|
167
|
+
const activePrs = prs.filter(pr => PR_POLLABLE_STATUSES.has(pr.status));
|
|
168
168
|
if (activePrs.length === 0) continue;
|
|
169
169
|
|
|
170
170
|
// Probe repo accessibility before iterating PRs — avoids N warnings per inaccessible repo
|
|
@@ -194,7 +194,13 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
194
194
|
// Merge back updated PRs and deduplicate
|
|
195
195
|
for (const updatedPr of activePrs) {
|
|
196
196
|
const idx = currentPrs.findIndex(p => p.id === updatedPr.id);
|
|
197
|
-
if (idx >= 0)
|
|
197
|
+
if (idx >= 0) {
|
|
198
|
+
// Never downgrade reviewStatus from 'approved' — it's a permanent terminal state
|
|
199
|
+
if (currentPrs[idx].reviewStatus === 'approved' && updatedPr.reviewStatus !== 'approved') {
|
|
200
|
+
updatedPr.reviewStatus = 'approved';
|
|
201
|
+
}
|
|
202
|
+
currentPrs[idx] = updatedPr;
|
|
203
|
+
}
|
|
198
204
|
}
|
|
199
205
|
// Remove duplicates — prefer merged/abandoned over active
|
|
200
206
|
const bestById = new Map();
|
|
@@ -214,7 +220,7 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
214
220
|
// Also poll manually-linked PRs from central pull-requests.json (extract slug from URL)
|
|
215
221
|
const centralPath = path.join(MINIONS_DIR, 'pull-requests.json');
|
|
216
222
|
const centralPrs = safeJson(centralPath) || [];
|
|
217
|
-
const activeCentral = centralPrs.filter(pr => pr.status
|
|
223
|
+
const activeCentral = centralPrs.filter(pr => PR_POLLABLE_STATUSES.has(pr.status) && pr.url);
|
|
218
224
|
let centralUpdated = 0;
|
|
219
225
|
for (const pr of activeCentral) {
|
|
220
226
|
const ghMatch = pr.url.match(/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
|
|
@@ -348,21 +354,7 @@ async function pollPrStatus(config) {
|
|
|
348
354
|
log('info', `PR ${pr.id} reviewStatus: ${pr.reviewStatus} → ${newReviewStatus}`);
|
|
349
355
|
pr.reviewStatus = newReviewStatus;
|
|
350
356
|
updated = true;
|
|
351
|
-
|
|
352
|
-
if (newReviewStatus === 'approved' || newReviewStatus === 'changes-requested') {
|
|
353
|
-
const authorId = (pr.agent || '').toLowerCase();
|
|
354
|
-
if (authorId) {
|
|
355
|
-
try {
|
|
356
|
-
const metricsPath = path.join(__dirname, 'metrics.json');
|
|
357
|
-
mutateJsonFileLocked(metricsPath, (metrics) => {
|
|
358
|
-
if (!metrics[authorId]) metrics[authorId] = {};
|
|
359
|
-
if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
|
|
360
|
-
else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
|
|
361
|
-
return metrics;
|
|
362
|
-
});
|
|
363
|
-
} catch (err) { log('warn', `Metrics update: ${err.message}`); }
|
|
364
|
-
}
|
|
365
|
-
}
|
|
357
|
+
shared.trackReviewMetric(pr, newReviewStatus, config);
|
|
366
358
|
// Reset review→fix cycle counter on approval (loop succeeded)
|
|
367
359
|
if (newReviewStatus === 'approved') {
|
|
368
360
|
delete pr._reviewFixCycles;
|
package/engine/shared.js
CHANGED
|
@@ -578,7 +578,24 @@ const PLAN_STATUS = {
|
|
|
578
578
|
PAUSED: 'paused', REJECTED: 'rejected', COMPLETED: 'completed',
|
|
579
579
|
REVISION_REQUESTED: 'revision-requested',
|
|
580
580
|
};
|
|
581
|
-
const PR_STATUS = { ACTIVE: 'active', MERGED: 'merged', ABANDONED: 'abandoned', CLOSED: 'closed' };
|
|
581
|
+
const PR_STATUS = { ACTIVE: 'active', MERGED: 'merged', ABANDONED: 'abandoned', CLOSED: 'closed', LINKED: 'linked' };
|
|
582
|
+
// PRs eligible for polling (status/build/comment checks) — excludes terminal statuses
|
|
583
|
+
const PR_POLLABLE_STATUSES = new Set([PR_STATUS.ACTIVE, PR_STATUS.LINKED]);
|
|
584
|
+
|
|
585
|
+
/** Update per-agent review metrics (prsApproved/prsRejected). Only writes for configured agents. */
|
|
586
|
+
function trackReviewMetric(pr, newReviewStatus, config) {
|
|
587
|
+
if (newReviewStatus !== 'approved' && newReviewStatus !== 'changes-requested') return;
|
|
588
|
+
const authorId = (pr.agent || '').toLowerCase();
|
|
589
|
+
if (!authorId || !config?.agents?.[authorId]) return;
|
|
590
|
+
try {
|
|
591
|
+
mutateJsonFileLocked(path.join(__dirname, 'metrics.json'), (metrics) => {
|
|
592
|
+
if (!metrics[authorId]) metrics[authorId] = {};
|
|
593
|
+
if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
|
|
594
|
+
else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
|
|
595
|
+
return metrics;
|
|
596
|
+
});
|
|
597
|
+
} catch (err) { log('warn', `Metrics update: ${err.message}`); }
|
|
598
|
+
}
|
|
582
599
|
const DISPATCH_RESULT = { SUCCESS: 'success', ERROR: 'error', TIMEOUT: 'timeout' };
|
|
583
600
|
const PIPELINE_STATUS = {
|
|
584
601
|
PENDING: 'pending', RUNNING: 'running', COMPLETED: 'completed',
|
|
@@ -995,7 +1012,7 @@ module.exports = {
|
|
|
995
1012
|
KB_CATEGORIES,
|
|
996
1013
|
classifyInboxItem,
|
|
997
1014
|
ENGINE_DEFAULTS,
|
|
998
|
-
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
1015
|
+
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, PR_POLLABLE_STATUSES, DISPATCH_RESULT, trackReviewMetric,
|
|
999
1016
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
1000
1017
|
FAILURE_CLASS, ESCALATION_POLICY, COMPLETION_FIELDS,
|
|
1001
1018
|
DEFAULT_AGENT_METRICS,
|
package/engine/timeout.js
CHANGED
|
@@ -316,6 +316,12 @@ function checkTimeouts(config) {
|
|
|
316
316
|
const procInfo = activeProcesses.get(item.id);
|
|
317
317
|
if (procInfo) {
|
|
318
318
|
shared.killGracefully(procInfo.proc, 5000);
|
|
319
|
+
// On Unix, also kill child process tree (killGracefully only hits parent PID)
|
|
320
|
+
if (process.platform !== 'win32' && procInfo.proc?.pid) {
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
try { shared.exec(`pkill -KILL -P ${procInfo.proc.pid}`, { timeout: 3000 }); } catch { /* children may already be dead */ }
|
|
323
|
+
}, 6000); // after grace period
|
|
324
|
+
}
|
|
319
325
|
activeProcesses.delete(item.id);
|
|
320
326
|
}
|
|
321
327
|
// Clear session so retry starts fresh instead of resuming the killed session
|
package/engine.js
CHANGED
|
@@ -1576,7 +1576,7 @@ async function discoverFromPrs(config, project) {
|
|
|
1576
1576
|
|
|
1577
1577
|
const knownAgents = new Set(Object.keys(config.agents || {}));
|
|
1578
1578
|
for (const pr of prs) {
|
|
1579
|
-
if (pr.status !==
|
|
1579
|
+
if (pr.status !== PR_STATUS.ACTIVE || pr._contextOnly) continue;
|
|
1580
1580
|
if (activePrIds.has(pr.id)) continue; // Skip PRs with active dispatch (prevent race)
|
|
1581
1581
|
// Branch mutex: skip if PR branch is locked by any active dispatch (cross-type collision)
|
|
1582
1582
|
if (pr.branch && isBranchActive(pr.branch)) {
|
|
@@ -1629,13 +1629,14 @@ async function discoverFromPrs(config, project) {
|
|
|
1629
1629
|
const liveStatus = await checkFn(pr, project);
|
|
1630
1630
|
if (liveStatus && liveStatus !== 'pending') {
|
|
1631
1631
|
log('info', `Pre-dispatch vote check: ${pr.id} is ${liveStatus} (cached was pending) — skipping review`);
|
|
1632
|
-
|
|
1632
|
+
// Never downgrade from approved
|
|
1633
|
+
if (pr.reviewStatus !== 'approved') pr.reviewStatus = liveStatus;
|
|
1633
1634
|
// Persist so next tick doesn't re-check
|
|
1634
1635
|
try {
|
|
1635
1636
|
mutateJsonFileLocked(projectPrPath(project), data => {
|
|
1636
1637
|
if (!Array.isArray(data)) return data;
|
|
1637
1638
|
const target = data.find(p => p.id === pr.id);
|
|
1638
|
-
if (target) target.reviewStatus = liveStatus;
|
|
1639
|
+
if (target && target.reviewStatus !== 'approved') target.reviewStatus = liveStatus;
|
|
1639
1640
|
return data;
|
|
1640
1641
|
});
|
|
1641
1642
|
} catch {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.809",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|
package/prompts/cc-system.md
CHANGED
|
@@ -10,9 +10,29 @@ Minions state lives in `{{minions_dir}}/`. Key paths: `config.json` (config), `r
|
|
|
10
10
|
|
|
11
11
|
## Role: Orchestrator
|
|
12
12
|
Default: **delegate to agents**. Agents have full Claude Code + worktrees + MCP tools.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
|
|
14
|
+
### When to delegate (ALWAYS)
|
|
15
|
+
- Code changes, fixes, refactors, new features → `implement` or `fix`
|
|
16
|
+
- Exploration, investigation, research, audits → `explore`
|
|
17
|
+
- Code reviews → `review`
|
|
18
|
+
- Testing → `test`
|
|
19
|
+
- Architecture analysis → `explore`
|
|
20
|
+
- Any task that would require **more than 3 tool calls** or touching **more than 2 files**
|
|
21
|
+
|
|
22
|
+
### When to do it yourself (ONLY these)
|
|
23
|
+
- Quick status lookups (reading 1-2 state files)
|
|
24
|
+
- Notes, plan edits, KB entries, routing updates
|
|
25
|
+
- Git ops the user explicitly asked CC to do
|
|
26
|
+
- Simple config changes (`set-config`)
|
|
27
|
+
- Answering questions from context you already have
|
|
28
|
+
|
|
29
|
+
### Size estimation rule
|
|
30
|
+
Before responding, estimate the task size:
|
|
31
|
+
- **Small** (≤3 tool calls, 1-2 files): do it yourself
|
|
32
|
+
- **Medium** (4-10 tool calls, 3+ files): DELEGATE to an agent
|
|
33
|
+
- **Large** (10+ tool calls, cross-cutting): DELEGATE, consider a plan with decomposition
|
|
34
|
+
|
|
35
|
+
When in doubt, delegate. You are the dispatcher, not the worker. Agents have isolated worktrees, full tool access, and no turn limits — they are better suited for real work.
|
|
16
36
|
|
|
17
37
|
## Actions
|
|
18
38
|
Append actions at the END of your response. Write your response first, then `===ACTIONS===` on its own line, then a JSON array. No text after the JSON. Omit entirely if no actions needed.
|
|
@@ -57,4 +77,4 @@ Terms like schedules, pipelines, agents, inbox, work items, plans, PRD, PRs, dis
|
|
|
57
77
|
1. Answer from the state preamble and context first. Only use tools for specific file lookups the user asked about — not to explore or investigate.
|
|
58
78
|
2. Be specific — cite IDs, names, filenames, line numbers.
|
|
59
79
|
3. Never modify engine source. Never push to git without user confirmation.
|
|
60
|
-
4. Delegate
|
|
80
|
+
4. Delegate, don't do. If a task involves code changes, multi-file reads, debugging, or any real engineering work — dispatch an agent. Your tools are for quick lookups, not for doing the work.
|