@yemi33/minions 0.1.60 → 0.1.62
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 +39 -0
- package/dashboard/js/command-center.js +2 -0
- package/dashboard/js/command-history.js +2 -0
- package/dashboard/js/command-input.js +2 -0
- package/dashboard/js/command-parser.js +2 -0
- package/dashboard/js/detail-panel.js +2 -0
- package/dashboard/js/live-stream.js +2 -0
- package/dashboard/js/modal-qa.js +2 -0
- package/dashboard/js/modal.js +2 -0
- package/dashboard/js/refresh.js +2 -0
- package/dashboard/js/render-agents.js +2 -0
- package/dashboard/js/render-dispatch.js +2 -0
- package/dashboard/js/render-inbox.js +2 -0
- package/dashboard/js/render-kb.js +2 -0
- package/dashboard/js/render-other.js +2 -0
- package/dashboard/js/render-pinned.js +2 -0
- package/dashboard/js/render-plans.js +2 -0
- package/dashboard/js/render-prd.js +2 -0
- package/dashboard/js/render-prs.js +2 -0
- package/dashboard/js/render-schedules.js +2 -0
- package/dashboard/js/render-skills.js +2 -0
- package/dashboard/js/render-work-items.js +2 -0
- package/dashboard/js/settings.js +2 -0
- package/dashboard/js/state.js +2 -0
- package/dashboard/js/utils.js +2 -0
- package/engine/cleanup.js +393 -0
- package/engine/dispatch.js +207 -0
- package/engine/timeout.js +280 -0
- package/engine.js +27 -757
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.62 (2026-03-30)
|
|
4
|
+
|
|
5
|
+
### Dashboard
|
|
6
|
+
- dashboard/js/command-center.js
|
|
7
|
+
- dashboard/js/command-history.js
|
|
8
|
+
- dashboard/js/command-input.js
|
|
9
|
+
- dashboard/js/command-parser.js
|
|
10
|
+
- dashboard/js/detail-panel.js
|
|
11
|
+
- dashboard/js/live-stream.js
|
|
12
|
+
- dashboard/js/modal-qa.js
|
|
13
|
+
- dashboard/js/modal.js
|
|
14
|
+
- dashboard/js/refresh.js
|
|
15
|
+
- dashboard/js/render-agents.js
|
|
16
|
+
- dashboard/js/render-dispatch.js
|
|
17
|
+
- dashboard/js/render-inbox.js
|
|
18
|
+
- dashboard/js/render-kb.js
|
|
19
|
+
- dashboard/js/render-other.js
|
|
20
|
+
- dashboard/js/render-pinned.js
|
|
21
|
+
- dashboard/js/render-plans.js
|
|
22
|
+
- dashboard/js/render-prd.js
|
|
23
|
+
- dashboard/js/render-prs.js
|
|
24
|
+
- dashboard/js/render-schedules.js
|
|
25
|
+
- dashboard/js/render-skills.js
|
|
26
|
+
- dashboard/js/render-work-items.js
|
|
27
|
+
- dashboard/js/settings.js
|
|
28
|
+
- dashboard/js/state.js
|
|
29
|
+
- dashboard/js/utils.js
|
|
30
|
+
|
|
31
|
+
## 0.1.61 (2026-03-30)
|
|
32
|
+
|
|
33
|
+
### Engine
|
|
34
|
+
- engine.js
|
|
35
|
+
- engine/cleanup.js
|
|
36
|
+
- engine/dispatch.js
|
|
37
|
+
- engine/timeout.js
|
|
38
|
+
|
|
39
|
+
### Other
|
|
40
|
+
- test/unit.test.js
|
|
41
|
+
|
|
3
42
|
## 0.1.60 (2026-03-30)
|
|
4
43
|
|
|
5
44
|
### Engine
|
|
@@ -378,3 +378,5 @@ async function ccExecuteAction(action) {
|
|
|
378
378
|
msgs.scrollTop = msgs.scrollHeight;
|
|
379
379
|
refresh();
|
|
380
380
|
}
|
|
381
|
+
|
|
382
|
+
window.MinionsCC = { toggleCommandCenter, ccNewSession, ccRestoreMessages, ccSaveState, ccUpdateSessionIndicator, ccAddMessage, ccSend, ccExecuteAction };
|
|
@@ -266,3 +266,5 @@ function cmdInsertPopupItem(id) {
|
|
|
266
266
|
cmdHidePopup();
|
|
267
267
|
cmdRenderMeta();
|
|
268
268
|
}
|
|
269
|
+
|
|
270
|
+
window.MinionsCmdInput = { cmdAutoResize, cmdUpdateHighlight, syncHighlightScroll, cmdInputChanged, cmdKeyDown, cmdSubmit, cmdRenderMeta, cmdShowMentions, cmdShowProjects, cmdHidePopup, cmdInsertPopupItem };
|
|
@@ -105,3 +105,5 @@ function renderDetailContent(detail, tab) {
|
|
|
105
105
|
el.innerHTML = '<div class="section">' + escHtml(detail.outputLog || 'No output log. The coordinator will save agent output here when tasks complete.') + '</div>';
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
window.MinionsDetail = { closeDetail, renderDetailTabs, switchTab, renderDetailContent };
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -307,3 +307,5 @@ async function qaReplacePrd(planFile) {
|
|
|
307
307
|
const btn = document.getElementById('qa-generate-prd-btn');
|
|
308
308
|
if (btn) btn.innerHTML = '<span style="color:var(--orange);font-size:12px">Replacing PRD — old items paused, agent regenerating from revised plan.</span>';
|
|
309
309
|
}
|
|
310
|
+
|
|
311
|
+
window.MinionsQA = { showModalQa, modalAskAboutSelection, clearQaSelection, clearQaConversation, modalSend, qaNewPrd, qaReplacePrd };
|
package/dashboard/js/modal.js
CHANGED
|
@@ -129,3 +129,5 @@ function renderArchiveButtons(archives) {
|
|
|
129
129
|
'<button class="archive-btn" onclick="openArchive(' + i + ')">Archived: ' + escHtml(a.version) + ' (' + a.total + ' items)</button>'
|
|
130
130
|
).join(' ');
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
window.MinionsModal = { closeModal, copyModalContent, showNotifBadge, clearNotifBadge, restoreNotifBadges, findCardForFile, renderArchiveButtons };
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -169,3 +169,5 @@ async function showErrorDetails(agentId, reason, task) {
|
|
|
169
169
|
document.getElementById('modal-body').textContent = 'Reason: ' + reason + '\n\n(Could not load agent output)';
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
+
|
|
173
|
+
window.MinionsDispatch = { renderEngineStatus, renderEngineAlert, renderDispatch, renderEngineLog, shortTime, showErrorDetails };
|
|
@@ -198,3 +198,5 @@ async function doPromoteToKB(name, category) {
|
|
|
198
198
|
}
|
|
199
199
|
} catch (e) { alert('Error: ' + e.message); }
|
|
200
200
|
}
|
|
201
|
+
|
|
202
|
+
window.MinionsInbox = { renderInbox, promoteToKB, renderNotes, openNotesModal, modalToggleEdit, modalSaveEdit, modalCancelEdit, deleteInboxItem, openInboxInExplorer, openQuickNoteModal, submitQuickNote, doPromoteToKB };
|
|
@@ -629,3 +629,5 @@ async function triggerVerify(file) {
|
|
|
629
629
|
}
|
|
630
630
|
} catch (e) { alert('Error: ' + e.message); }
|
|
631
631
|
}
|
|
632
|
+
|
|
633
|
+
window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, renderPlans, openArchivedPlansModal, qaDisablePrdButtons, showPlanVersionActions, qaJustSave, planExecute, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
|
|
@@ -716,3 +716,5 @@ function openArchive(i) {
|
|
|
716
716
|
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
717
717
|
document.getElementById('modal').classList.add('open');
|
|
718
718
|
}
|
|
719
|
+
|
|
720
|
+
window.MinionsPrd = { renderPrd, renderPrdProgress, openArchivedPrdModal, showArchivedPrdDetail, prdItemEdit, prdItemSave, prdItemRemove, prdItemRequeue, prdRegenerate, openArchive };
|
|
@@ -396,3 +396,5 @@ function openAllWorkItems() {
|
|
|
396
396
|
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
397
397
|
document.getElementById('modal').classList.add('open');
|
|
398
398
|
}
|
|
399
|
+
|
|
400
|
+
window.MinionsWork = { wiRow, renderWorkItems, editWorkItem, submitWorkItemEdit, deleteWorkItem, archiveWorkItem, toggleWorkItemArchive, retryWorkItem, wiPrev, wiNext, feedbackWorkItem, submitFeedback, openCreateWorkItemModal, openWorkItemDetail, openAllWorkItems };
|
package/dashboard/js/settings.js
CHANGED
package/dashboard/js/state.js
CHANGED
|
@@ -82,3 +82,5 @@ function rerenderPrdFromCache() {
|
|
|
82
82
|
if (!window._lastStatus || !window._lastStatus.prdProgress) return;
|
|
83
83
|
renderPrd(window._lastStatus.prd, window._lastStatus.prdProgress);
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
window.MinionsState = { getPageFromUrl, switchPage, getPrdRequeueState, setPrdRequeueState, clearPrdRequeueState, prunePrdRequeueState, rerenderPrdFromCache };
|
package/dashboard/js/utils.js
CHANGED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/cleanup.js — Periodic cleanup: temp files, worktrees, zombies, migrations.
|
|
3
|
+
* Extracted from engine.js for modularity. No logic changes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const shared = require('./shared');
|
|
9
|
+
const queries = require('./queries');
|
|
10
|
+
|
|
11
|
+
const { exec, execSilent } = shared;
|
|
12
|
+
const { safeJson, safeWrite, safeReadDir, getProjects, projectWorkItemsPath, projectPrPath,
|
|
13
|
+
sanitizeBranch, KB_CATEGORIES } = shared;
|
|
14
|
+
const { getDispatch, getAgentStatus } = queries;
|
|
15
|
+
|
|
16
|
+
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
17
|
+
const AGENTS_DIR = queries.AGENTS_DIR;
|
|
18
|
+
const ENGINE_DIR = queries.ENGINE_DIR;
|
|
19
|
+
const PRD_DIR = queries.PRD_DIR;
|
|
20
|
+
const PLANS_DIR = queries.PLANS_DIR;
|
|
21
|
+
|
|
22
|
+
// Lazy require to break circular dependency with engine.js
|
|
23
|
+
let _engine = null;
|
|
24
|
+
function engine() { if (!_engine) _engine = require('../engine'); return _engine; }
|
|
25
|
+
function log(level, msg, meta) { return engine().log(level, msg, meta); }
|
|
26
|
+
function ts() { return engine().ts(); }
|
|
27
|
+
|
|
28
|
+
// Lazy require for dispatch module
|
|
29
|
+
let _dispatch = null;
|
|
30
|
+
function dispatchModule() { if (!_dispatch) _dispatch = require('./dispatch'); return _dispatch; }
|
|
31
|
+
|
|
32
|
+
// ─── Cleanup Orchestrator ────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function runCleanup(config, verbose = false) {
|
|
35
|
+
const activeProcesses = engine().activeProcesses;
|
|
36
|
+
const projects = getProjects(config);
|
|
37
|
+
let cleaned = { tempFiles: 0, liveOutputs: 0, worktrees: 0, zombies: 0 };
|
|
38
|
+
|
|
39
|
+
// 1. Clean stale temp prompt/sysprompt files (older than 1 hour)
|
|
40
|
+
const oneHourAgo = Date.now() - 3600000;
|
|
41
|
+
try {
|
|
42
|
+
const tmpDir = path.join(ENGINE_DIR, 'tmp');
|
|
43
|
+
const scanDirs = [ENGINE_DIR, ...(fs.existsSync(tmpDir) ? [tmpDir] : [])];
|
|
44
|
+
for (const dir of scanDirs) {
|
|
45
|
+
for (const f of fs.readdirSync(dir)) {
|
|
46
|
+
if (f.startsWith('prompt-') || f.startsWith('sysprompt-') || f.startsWith('tmp-sysprompt-')) {
|
|
47
|
+
const fp = path.join(dir, f);
|
|
48
|
+
try {
|
|
49
|
+
const stat = fs.statSync(fp);
|
|
50
|
+
if (stat.mtimeMs < oneHourAgo) {
|
|
51
|
+
fs.unlinkSync(fp);
|
|
52
|
+
cleaned.tempFiles++;
|
|
53
|
+
}
|
|
54
|
+
} catch { /* cleanup */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (e) { log('warn', 'cleanup temp files: ' + e.message); }
|
|
59
|
+
|
|
60
|
+
// 2. Clean live-output.log for idle agents (not currently working)
|
|
61
|
+
for (const [agentId] of Object.entries(config.agents || {})) {
|
|
62
|
+
const status = getAgentStatus(agentId);
|
|
63
|
+
if (status.status !== 'working') {
|
|
64
|
+
const livePath = path.join(AGENTS_DIR, agentId, 'live-output.log');
|
|
65
|
+
if (fs.existsSync(livePath)) {
|
|
66
|
+
try {
|
|
67
|
+
const stat = fs.statSync(livePath);
|
|
68
|
+
if (stat.mtimeMs < oneHourAgo) {
|
|
69
|
+
fs.unlinkSync(livePath);
|
|
70
|
+
cleaned.liveOutputs++;
|
|
71
|
+
}
|
|
72
|
+
} catch { /* cleanup */ }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Clean git worktrees for merged/abandoned PRs
|
|
78
|
+
for (const project of projects) {
|
|
79
|
+
const root = project.localPath ? path.resolve(project.localPath) : null;
|
|
80
|
+
if (!root || !fs.existsSync(root)) continue;
|
|
81
|
+
|
|
82
|
+
const worktreeRoot = path.resolve(root, config.engine?.worktreeRoot || '../worktrees');
|
|
83
|
+
if (!fs.existsSync(worktreeRoot)) continue;
|
|
84
|
+
|
|
85
|
+
// Get PRs for this project
|
|
86
|
+
const prs = safeJson(projectPrPath(project)) || [];
|
|
87
|
+
const mergedBranches = new Set();
|
|
88
|
+
for (const pr of prs) {
|
|
89
|
+
if (pr.status === 'merged' || pr.status === 'abandoned' || pr.status === 'completed') {
|
|
90
|
+
if (pr.branch) mergedBranches.add(pr.branch);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// List worktrees — collect info for age-based + cap-based cleanup
|
|
95
|
+
const MAX_WORKTREES = 10;
|
|
96
|
+
try {
|
|
97
|
+
const dirs = fs.readdirSync(worktreeRoot);
|
|
98
|
+
const wtEntries = []; // { dir, wtPath, mtime, shouldClean, isProtected }
|
|
99
|
+
const dispatch = getDispatch();
|
|
100
|
+
|
|
101
|
+
for (const dir of dirs) {
|
|
102
|
+
const wtPath = path.join(worktreeRoot, dir);
|
|
103
|
+
try { if (!fs.statSync(wtPath).isDirectory()) continue; } catch { continue; }
|
|
104
|
+
|
|
105
|
+
let shouldClean = false;
|
|
106
|
+
let isProtected = false;
|
|
107
|
+
|
|
108
|
+
// Check if this worktree's branch is merged/abandoned
|
|
109
|
+
// Use sanitized exact match on the branch portion of the dir name (format: {slug}-{branch}-{suffix})
|
|
110
|
+
const dirLower = dir.toLowerCase();
|
|
111
|
+
for (const branch of mergedBranches) {
|
|
112
|
+
const branchSlug = sanitizeBranch(branch).toLowerCase();
|
|
113
|
+
if (dirLower === branchSlug || dirLower.includes(branchSlug + '-') || dirLower.endsWith('-' + branchSlug)) {
|
|
114
|
+
shouldClean = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if referenced by active/pending dispatch (use sanitized branch comparison)
|
|
120
|
+
const isReferenced = [...dispatch.pending, ...(dispatch.active || [])].some(d => {
|
|
121
|
+
if (!d.meta?.branch) return false;
|
|
122
|
+
const dispBranch = sanitizeBranch(d.meta.branch).toLowerCase();
|
|
123
|
+
return dirLower.includes(dispBranch);
|
|
124
|
+
});
|
|
125
|
+
if (isReferenced) isProtected = true;
|
|
126
|
+
|
|
127
|
+
// Also clean worktrees older than 2 hours with no active dispatch referencing them
|
|
128
|
+
let mtime = Date.now();
|
|
129
|
+
if (!shouldClean) {
|
|
130
|
+
try {
|
|
131
|
+
const stat = fs.statSync(wtPath);
|
|
132
|
+
mtime = stat.mtimeMs;
|
|
133
|
+
const ageMs = Date.now() - mtime;
|
|
134
|
+
if (ageMs > 7200000 && !isReferenced) { // 2 hours
|
|
135
|
+
shouldClean = true;
|
|
136
|
+
}
|
|
137
|
+
} catch { /* optional */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Skip worktrees for active shared-branch plans (check both prd/ and plans/ for .json PRDs)
|
|
141
|
+
if (shouldClean || !isProtected) {
|
|
142
|
+
try {
|
|
143
|
+
for (const checkDir of [PRD_DIR, path.join(MINIONS_DIR, 'plans')]) {
|
|
144
|
+
if (!fs.existsSync(checkDir)) continue;
|
|
145
|
+
for (const pf of fs.readdirSync(checkDir).filter(f => f.endsWith('.json'))) {
|
|
146
|
+
const plan = safeJson(path.join(checkDir, pf));
|
|
147
|
+
if (plan?.branch_strategy === 'shared-branch' && plan?.feature_branch && plan?.status !== 'completed') {
|
|
148
|
+
const planBranch = sanitizeBranch(plan.feature_branch).toLowerCase();
|
|
149
|
+
if (dirLower.includes(planBranch)) {
|
|
150
|
+
isProtected = true;
|
|
151
|
+
if (shouldClean) {
|
|
152
|
+
shouldClean = false;
|
|
153
|
+
if (verbose) console.log(` Skipping worktree ${dir}: active shared-branch plan`);
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (isProtected) break;
|
|
160
|
+
}
|
|
161
|
+
} catch (e) { log('warn', 'check shared-branch protection: ' + e.message); }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
wtEntries.push({ dir, wtPath, mtime, shouldClean, isProtected });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Enforce max worktree cap — if over limit, mark oldest unprotected for cleanup
|
|
168
|
+
const surviving = wtEntries.filter(e => !e.shouldClean && !e.isProtected);
|
|
169
|
+
if (surviving.length + wtEntries.filter(e => e.isProtected).length > MAX_WORKTREES) {
|
|
170
|
+
// Sort oldest first
|
|
171
|
+
surviving.sort((a, b) => a.mtime - b.mtime);
|
|
172
|
+
const excess = surviving.length + wtEntries.filter(e => e.isProtected).length - MAX_WORKTREES;
|
|
173
|
+
for (let i = 0; i < Math.min(excess, surviving.length); i++) {
|
|
174
|
+
surviving[i].shouldClean = true;
|
|
175
|
+
if (verbose) console.log(` Marking worktree ${surviving[i].dir} for cap cleanup (${MAX_WORKTREES} max)`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Remove all marked worktrees
|
|
180
|
+
for (const entry of wtEntries) {
|
|
181
|
+
if (entry.shouldClean) {
|
|
182
|
+
try {
|
|
183
|
+
exec(`git worktree remove "${entry.wtPath}" --force`, { cwd: root, stdio: 'pipe' });
|
|
184
|
+
cleaned.worktrees++;
|
|
185
|
+
if (verbose) console.log(` Removed worktree: ${entry.wtPath}`);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
if (verbose) console.log(` Failed to remove worktree ${entry.wtPath}: ${e.message}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch (e) { log('warn', 'cleanup worktrees: ' + e.message); }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 4. Kill zombie claude processes not tracked by the engine
|
|
195
|
+
// List all node processes, check if any are running spawn-agent.js for our minions
|
|
196
|
+
try {
|
|
197
|
+
const dispatch = getDispatch();
|
|
198
|
+
const activePids = new Set();
|
|
199
|
+
for (const [, info] of activeProcesses.entries()) {
|
|
200
|
+
if (info.proc?.pid) activePids.add(info.proc.pid);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Clean individual orphaned processes — no matching active dispatch
|
|
204
|
+
const activeIds = new Set((dispatch.active || []).map(d => d.id));
|
|
205
|
+
for (const [id, info] of activeProcesses.entries()) {
|
|
206
|
+
if (!activeIds.has(id)) {
|
|
207
|
+
try { if (info.proc) info.proc.kill('SIGTERM'); } catch { /* process may be dead */ }
|
|
208
|
+
activeProcesses.delete(id);
|
|
209
|
+
cleaned.zombies++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (e) { log('warn', 'cleanup zombie processes: ' + e.message); }
|
|
213
|
+
|
|
214
|
+
// 5. Clean spawn-debug.log
|
|
215
|
+
try { fs.unlinkSync(path.join(ENGINE_DIR, 'spawn-debug.log')); } catch { /* cleanup */ }
|
|
216
|
+
|
|
217
|
+
// 6. Prune old output archive files (keep last 30 per agent)
|
|
218
|
+
for (const agentId of Object.keys(config.agents || {})) {
|
|
219
|
+
const agentDir = path.join(MINIONS_DIR, 'agents', agentId);
|
|
220
|
+
if (!fs.existsSync(agentDir)) continue;
|
|
221
|
+
try {
|
|
222
|
+
const outputFiles = fs.readdirSync(agentDir)
|
|
223
|
+
.filter(f => f.startsWith('output-') && f.endsWith('.log') && f !== 'output.log')
|
|
224
|
+
.map(f => ({ name: f, mtime: fs.statSync(path.join(agentDir, f)).mtimeMs }))
|
|
225
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
226
|
+
for (const old of outputFiles.slice(30)) {
|
|
227
|
+
try { fs.unlinkSync(path.join(agentDir, old.name)); cleaned.files++; } catch { /* cleanup */ }
|
|
228
|
+
}
|
|
229
|
+
} catch (e) { log('warn', 'prune output archives: ' + e.message); }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 7. Prune orphaned dispatch entries — items whose source work item no longer exists
|
|
233
|
+
cleaned.orphanedDispatches = 0;
|
|
234
|
+
try {
|
|
235
|
+
const dispatch = getDispatch();
|
|
236
|
+
// Collect all work item IDs across all sources
|
|
237
|
+
const allWiIds = new Set();
|
|
238
|
+
try {
|
|
239
|
+
const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
240
|
+
central.forEach(w => allWiIds.add(w.id));
|
|
241
|
+
} catch (e) { log('warn', 'read central work items for orphan check: ' + e.message); }
|
|
242
|
+
for (const project of projects) {
|
|
243
|
+
try {
|
|
244
|
+
const projItems = safeJson(projectWorkItemsPath(project)) || [];
|
|
245
|
+
projItems.forEach(w => allWiIds.add(w.id));
|
|
246
|
+
} catch (e) { log('warn', 'read project work items for orphan check: ' + e.message); }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let changed = false;
|
|
250
|
+
for (const queue of ['pending', 'active']) {
|
|
251
|
+
if (!dispatch[queue]) continue;
|
|
252
|
+
const before = dispatch[queue].length;
|
|
253
|
+
dispatch[queue] = dispatch[queue].filter(d => {
|
|
254
|
+
const itemId = d.meta?.item?.id;
|
|
255
|
+
if (!itemId) return true; // keep entries without item tracking
|
|
256
|
+
return allWiIds.has(itemId);
|
|
257
|
+
});
|
|
258
|
+
const removed = before - dispatch[queue].length;
|
|
259
|
+
if (removed > 0) {
|
|
260
|
+
cleaned.orphanedDispatches += removed;
|
|
261
|
+
changed = true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (changed) {
|
|
265
|
+
const { mutateDispatch } = dispatchModule();
|
|
266
|
+
mutateDispatch((dp) => {
|
|
267
|
+
for (const queue of ['pending', 'active']) {
|
|
268
|
+
if (!dp[queue]) continue;
|
|
269
|
+
dp[queue] = dp[queue].filter(d => {
|
|
270
|
+
const itemId = d.meta?.item?.id;
|
|
271
|
+
if (!itemId) return true;
|
|
272
|
+
return allWiIds.has(itemId);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
} catch (e) { log('warn', 'prune orphaned dispatches: ' + e.message); }
|
|
278
|
+
|
|
279
|
+
if (cleaned.tempFiles + cleaned.liveOutputs + cleaned.worktrees + cleaned.zombies + (cleaned.files || 0) + cleaned.orphanedDispatches > 0) {
|
|
280
|
+
log('info', `Cleanup: ${cleaned.tempFiles} temp, ${cleaned.liveOutputs} live outputs, ${cleaned.worktrees} worktrees, ${cleaned.zombies} zombies, ${cleaned.files || 0} archives, ${cleaned.orphanedDispatches} orphaned dispatches`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 8. Clean swept KB files older than 7 days
|
|
284
|
+
try {
|
|
285
|
+
const sweptDir = path.join(MINIONS_DIR, 'knowledge', '_swept');
|
|
286
|
+
if (fs.existsSync(sweptDir)) {
|
|
287
|
+
const sevenDaysAgo = Date.now() - 7 * 86400000;
|
|
288
|
+
for (const f of fs.readdirSync(sweptDir)) {
|
|
289
|
+
try {
|
|
290
|
+
const fp = path.join(sweptDir, f);
|
|
291
|
+
if (fs.statSync(fp).mtimeMs < sevenDaysAgo) {
|
|
292
|
+
fs.unlinkSync(fp);
|
|
293
|
+
if (!cleaned.sweptKb) cleaned.sweptKb = 0;
|
|
294
|
+
cleaned.sweptKb++;
|
|
295
|
+
}
|
|
296
|
+
} catch { /* cleanup */ }
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} catch (e) { log('warn', 'cleanup swept KB files: ' + e.message); }
|
|
300
|
+
|
|
301
|
+
// 9. KB watchdog — restore deleted KB files from git if count dropped vs checkpoint
|
|
302
|
+
try {
|
|
303
|
+
const checkpoint = safeJson(path.join(ENGINE_DIR, 'kb-checkpoint.json'));
|
|
304
|
+
if (checkpoint && checkpoint.count > 0) {
|
|
305
|
+
const cats = KB_CATEGORIES;
|
|
306
|
+
const knowledgeDir = path.join(MINIONS_DIR, 'knowledge');
|
|
307
|
+
let current = 0;
|
|
308
|
+
for (const cat of cats) {
|
|
309
|
+
const d = path.join(knowledgeDir, cat);
|
|
310
|
+
if (fs.existsSync(d)) current += fs.readdirSync(d).length;
|
|
311
|
+
}
|
|
312
|
+
if (current < checkpoint.count) {
|
|
313
|
+
log('warn', `KB watchdog: file count dropped ${checkpoint.count} → ${current}, restoring from git`);
|
|
314
|
+
try {
|
|
315
|
+
const trackedCheck = execSilent('git ls-tree --name-only HEAD -- knowledge', { cwd: MINIONS_DIR }).toString().trim();
|
|
316
|
+
if (!trackedCheck) {
|
|
317
|
+
log('warn', 'KB watchdog: knowledge/ is not tracked in git HEAD — skipping restore');
|
|
318
|
+
} else {
|
|
319
|
+
execSilent('git checkout HEAD -- knowledge', { cwd: MINIONS_DIR });
|
|
320
|
+
log('info', 'KB watchdog: restored knowledge/ from git HEAD');
|
|
321
|
+
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
log('error', `KB watchdog: git restore failed — ${err.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} catch (e) { log('warn', 'KB watchdog check: ' + e.message); }
|
|
328
|
+
|
|
329
|
+
// 6. Migrate legacy work-item statuses to canonical values
|
|
330
|
+
// in-pr, implemented, complete → done (one-time correction per item)
|
|
331
|
+
const LEGACY_DONE_STATUSES = new Set(['in-pr', 'implemented', 'complete']);
|
|
332
|
+
for (const project of projects) {
|
|
333
|
+
try {
|
|
334
|
+
const wiPath = projectWorkItemsPath(project);
|
|
335
|
+
const items = safeJson(wiPath) || [];
|
|
336
|
+
let migrated = 0;
|
|
337
|
+
for (const item of items) {
|
|
338
|
+
if (LEGACY_DONE_STATUSES.has(item.status)) {
|
|
339
|
+
item.status = 'done';
|
|
340
|
+
migrated++;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (migrated > 0) {
|
|
344
|
+
safeWrite(wiPath, items);
|
|
345
|
+
log('info', `Migrated ${migrated} legacy status(es) → done in ${project.name} work items`);
|
|
346
|
+
}
|
|
347
|
+
} catch (e) { log('warn', 'migrate legacy statuses: ' + e.message); }
|
|
348
|
+
}
|
|
349
|
+
// Central work items
|
|
350
|
+
try {
|
|
351
|
+
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
352
|
+
const centralItems = safeJson(centralPath) || [];
|
|
353
|
+
let migrated = 0;
|
|
354
|
+
for (const item of centralItems) {
|
|
355
|
+
if (LEGACY_DONE_STATUSES.has(item.status)) {
|
|
356
|
+
item.status = 'done';
|
|
357
|
+
migrated++;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (migrated > 0) {
|
|
361
|
+
safeWrite(centralPath, centralItems);
|
|
362
|
+
log('info', `Migrated ${migrated} legacy status(es) → done in central work items`);
|
|
363
|
+
}
|
|
364
|
+
} catch (e) { log('warn', 'migrate central legacy statuses: ' + e.message); }
|
|
365
|
+
// PRD items (missing_features[].status)
|
|
366
|
+
try {
|
|
367
|
+
const prdFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json'));
|
|
368
|
+
for (const pf of prdFiles) {
|
|
369
|
+
const prdPath = path.join(PRD_DIR, pf);
|
|
370
|
+
const prd = safeJson(prdPath);
|
|
371
|
+
if (!prd?.missing_features) continue;
|
|
372
|
+
let migrated = 0;
|
|
373
|
+
for (const feat of prd.missing_features) {
|
|
374
|
+
if (LEGACY_DONE_STATUSES.has(feat.status)) {
|
|
375
|
+
feat.status = 'done';
|
|
376
|
+
migrated++;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (migrated > 0) {
|
|
380
|
+
safeWrite(prdPath, prd);
|
|
381
|
+
log('info', `Migrated ${migrated} legacy PRD item status(es) → done in ${pf}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch (e) { log('warn', 'migrate PRD legacy statuses: ' + e.message); }
|
|
385
|
+
|
|
386
|
+
return cleaned;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ─── Exports ─────────────────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
module.exports = {
|
|
392
|
+
runCleanup,
|
|
393
|
+
};
|