@yemi33/minions 0.1.415 → 0.1.417
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 +6 -1
- package/bin/minions.js +42 -45
- package/engine/consolidation.js +42 -36
- package/engine/dispatch.js +2 -2
- package/engine/queries.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.417 (2026-04-06)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- Fix notes.md race condition and null guards in consolidation.js
|
|
7
|
+
- Replace 5 raw status strings with WI_STATUS constants
|
|
6
8
|
- Fix null crashes in lifecycle.js syncPrsFromOutput and createReviewFeedbackForAuthor
|
|
7
9
|
|
|
10
|
+
### Fixes
|
|
11
|
+
- restart kills zombie dashboard via port-based detection
|
|
12
|
+
|
|
8
13
|
## 0.1.414 (2026-04-06)
|
|
9
14
|
|
|
10
15
|
### Fixes
|
package/bin/minions.js
CHANGED
|
@@ -28,6 +28,42 @@ const os = require('os');
|
|
|
28
28
|
const { spawn, execSync } = require('child_process');
|
|
29
29
|
|
|
30
30
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
31
|
+
|
|
32
|
+
/** Kill process(es) listening on a given port. Works cross-platform. */
|
|
33
|
+
function killByPort(port) {
|
|
34
|
+
try {
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
const out = execSync(`netstat -ano | findstr ":${port} " | findstr LISTENING`, { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
37
|
+
const pids = new Set();
|
|
38
|
+
for (const line of out.split('\n')) {
|
|
39
|
+
const pid = line.trim().split(/\s+/).pop();
|
|
40
|
+
if (pid && /^\d+$/.test(pid) && pid !== '0' && pid !== String(process.pid)) pids.add(pid);
|
|
41
|
+
}
|
|
42
|
+
for (const pid of pids) try { process.kill(parseInt(pid)); } catch {}
|
|
43
|
+
} else {
|
|
44
|
+
execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null`, { timeout: 5000 });
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Kill minions processes by command-line pattern matching (wmic on Windows, pkill on Unix). */
|
|
50
|
+
function killMinionsProcesses(patterns) {
|
|
51
|
+
try {
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
const out = execSync('wmic process where "name=\'node.exe\'" get processid,commandline /format:csv', { encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
54
|
+
for (const line of out.split('\n')) {
|
|
55
|
+
if (patterns.some(p => line.includes(p))) {
|
|
56
|
+
const pid = line.split(',').pop()?.trim();
|
|
57
|
+
if (pid && /^\d+$/.test(pid) && pid !== String(process.pid)) try { process.kill(parseInt(pid)); } catch {}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
for (const p of patterns) {
|
|
62
|
+
try { execSync(`pkill -f "${p}" 2>/dev/null`, { timeout: 5000 }); } catch {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
31
67
|
const DEFAULT_MINIONS_HOME = path.join(os.homedir(), '.minions');
|
|
32
68
|
const ROOT_POINTER_PATH = path.join(os.homedir(), '.minions-root');
|
|
33
69
|
const LEGACY_DEFAULT_SQUAD_HOME = path.join(os.homedir(), '.squad');
|
|
@@ -495,20 +531,8 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
495
531
|
ensureInstalled();
|
|
496
532
|
// Stop engine if running
|
|
497
533
|
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME }); } catch {}
|
|
498
|
-
// Kill existing dashboard
|
|
499
|
-
|
|
500
|
-
if (process.platform === 'win32') {
|
|
501
|
-
const out = execSync('wmic process where "name=\'node.exe\'" get processid,commandline /format:csv', { encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
502
|
-
for (const line of out.split('\n')) {
|
|
503
|
-
if (line.includes('dashboard.js') && line.includes('minions')) {
|
|
504
|
-
const pid = line.split(',').pop()?.trim();
|
|
505
|
-
if (pid && pid !== String(process.pid)) try { process.kill(parseInt(pid)); } catch {}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
} else {
|
|
509
|
-
try { execSync('lsof -ti:7331 | xargs kill -9 2>/dev/null', { timeout: 5000 }); } catch {}
|
|
510
|
-
}
|
|
511
|
-
} catch {}
|
|
534
|
+
// Kill existing dashboard — port-based is reliable across all setups
|
|
535
|
+
killByPort(7331);
|
|
512
536
|
const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start'], {
|
|
513
537
|
cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
|
|
514
538
|
});
|
|
@@ -550,22 +574,8 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
550
574
|
|
|
551
575
|
// 1. Kill all processes
|
|
552
576
|
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME }); } catch {}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (process.platform === 'win32') {
|
|
556
|
-
const out = execSync('wmic process where "name=\'node.exe\'" get processid,commandline /format:csv', { encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
557
|
-
for (const line of out.split('\n')) {
|
|
558
|
-
if (line.includes('minions') && (line.includes('engine.js') || line.includes('dashboard.js') || line.includes('spawn-agent.js'))) {
|
|
559
|
-
const pid = line.split(',').pop()?.trim();
|
|
560
|
-
if (pid && pid !== String(process.pid)) {
|
|
561
|
-
try { process.kill(parseInt(pid)); } catch {}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
} else {
|
|
566
|
-
try { execSync('lsof -ti:7331 | xargs kill -9 2>/dev/null', { timeout: 5000 }); } catch {}
|
|
567
|
-
}
|
|
568
|
-
} catch {}
|
|
577
|
+
killByPort(7331);
|
|
578
|
+
killMinionsProcesses(['engine.js', 'dashboard.js', 'spawn-agent.js']);
|
|
569
579
|
console.log(' Killed all processes');
|
|
570
580
|
|
|
571
581
|
// 2. Delete runtime state
|
|
@@ -650,21 +660,8 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
650
660
|
|
|
651
661
|
// 1. Kill all processes
|
|
652
662
|
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000 }); } catch {}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const out = execSync('wmic process where "name=\'node.exe\'" get processid,commandline /format:csv', { encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
656
|
-
for (const line of out.split('\n')) {
|
|
657
|
-
if (line.includes('minions') && (line.includes('engine.js') || line.includes('dashboard.js') || line.includes('spawn-agent.js'))) {
|
|
658
|
-
const pid = line.split(',').pop()?.trim();
|
|
659
|
-
if (pid && pid !== String(process.pid)) try { process.kill(parseInt(pid)); } catch {}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
} else {
|
|
663
|
-
try { execSync('pkill -f "minions.*engine.js" 2>/dev/null', { timeout: 5000 }); } catch {}
|
|
664
|
-
try { execSync('pkill -f "minions.*dashboard.js" 2>/dev/null', { timeout: 5000 }); } catch {}
|
|
665
|
-
try { execSync('lsof -ti:7331 | xargs kill -9 2>/dev/null', { timeout: 5000 }); } catch {}
|
|
666
|
-
}
|
|
667
|
-
} catch {}
|
|
663
|
+
killByPort(7331);
|
|
664
|
+
killMinionsProcesses(['engine.js', 'dashboard.js', 'spawn-agent.js']);
|
|
668
665
|
console.log(' Killed all processes');
|
|
669
666
|
|
|
670
667
|
// 2. Remove minions-authored skills from ~/.claude/skills/
|
package/engine/consolidation.js
CHANGED
|
@@ -38,7 +38,7 @@ function consolidateInbox(config) {
|
|
|
38
38
|
|
|
39
39
|
const items = files.map(f => ({
|
|
40
40
|
name: f,
|
|
41
|
-
content: safeRead(path.join(INBOX_DIR, f))
|
|
41
|
+
content: safeRead(path.join(INBOX_DIR, f)) || ''
|
|
42
42
|
}));
|
|
43
43
|
|
|
44
44
|
const existingNotes = getNotes() || '';
|
|
@@ -220,30 +220,33 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
const entry = '\n\n---\n\n' + digest;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
223
|
+
// Wrap read-modify-write in file lock to prevent race with concurrent consolidation or manual edits
|
|
224
|
+
shared.withFileLock(NOTES_PATH + '.lock', () => {
|
|
225
|
+
const current = getNotes() || '';
|
|
226
|
+
let newContent = current + entry;
|
|
227
|
+
|
|
228
|
+
if (newContent.length > 50000) {
|
|
229
|
+
// Truncate on section boundary — scan backward for last \n# before byte limit
|
|
230
|
+
// Never cut mid-section to preserve readability
|
|
231
|
+
const limit = 50000;
|
|
232
|
+
const lastSectionBoundary = newContent.lastIndexOf('\n---\n\n### ', limit);
|
|
233
|
+
if (lastSectionBoundary > 0) {
|
|
234
|
+
newContent = newContent.slice(0, lastSectionBoundary);
|
|
235
|
+
log('info', `Pruned notes.md at section boundary (pos ${lastSectionBoundary}) to stay under ${limit} bytes`);
|
|
236
|
+
} else {
|
|
237
|
+
// Fallback: use the old section-count approach
|
|
238
|
+
const sections = newContent.split('\n---\n\n### ');
|
|
239
|
+
if (sections.length > 10) {
|
|
240
|
+
const header = sections[0];
|
|
241
|
+
const recent = sections.slice(-8);
|
|
242
|
+
newContent = header + '\n---\n\n### ' + recent.join('\n---\n\n### ');
|
|
243
|
+
log('info', `Pruned notes.md: removed ${sections.length - 9} old sections`);
|
|
244
|
+
}
|
|
242
245
|
}
|
|
243
246
|
}
|
|
244
|
-
}
|
|
245
247
|
|
|
246
|
-
|
|
248
|
+
safeWrite(NOTES_PATH, newContent);
|
|
249
|
+
});
|
|
247
250
|
classifyToKnowledgeBase(items);
|
|
248
251
|
archiveInboxFiles(files);
|
|
249
252
|
log('info', `LLM consolidation complete: ${files.length} notes processed by Haiku`);
|
|
@@ -297,10 +300,10 @@ function consolidateWithRegex(items, files) {
|
|
|
297
300
|
if (!trimmed || sectionPattern.test(trimmed)) continue;
|
|
298
301
|
let insight = null;
|
|
299
302
|
const numMatch = trimmed.match(numberedPattern);
|
|
300
|
-
if (numMatch) insight = `**${numMatch[1].trim()}**: ${numMatch[2].trim()}`;
|
|
303
|
+
if (numMatch && numMatch[1] && numMatch[2]) insight = `**${numMatch[1].trim()}**: ${numMatch[2].trim()}`;
|
|
301
304
|
if (!insight) {
|
|
302
305
|
const bulMatch = trimmed.match(bulletPattern);
|
|
303
|
-
if (bulMatch) insight = `**${bulMatch[1].trim()}**: ${bulMatch[2].trim()}`;
|
|
306
|
+
if (bulMatch && bulMatch[1] && bulMatch[2]) insight = `**${bulMatch[1].trim()}**: ${bulMatch[2].trim()}`;
|
|
304
307
|
}
|
|
305
308
|
if (!insight && importantKeywords.test(trimmed) && !trimmed.startsWith('#') && trimmed.length > 30 && trimmed.length < 500) {
|
|
306
309
|
insight = trimmed;
|
|
@@ -363,19 +366,22 @@ function consolidateWithRegex(items, files) {
|
|
|
363
366
|
const dupCount = allInsights.length - deduped.length;
|
|
364
367
|
if (dupCount > 0) entry += `_Deduplication: ${dupCount} duplicate(s) removed._\n`;
|
|
365
368
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
369
|
+
// Wrap read-modify-write in file lock to prevent race with concurrent consolidation or manual edits
|
|
370
|
+
shared.withFileLock(NOTES_PATH + '.lock', () => {
|
|
371
|
+
const current = getNotes() || '';
|
|
372
|
+
let newContent = current + entry;
|
|
373
|
+
if (newContent.length > 50000) {
|
|
374
|
+
const limit = 50000;
|
|
375
|
+
const lastBoundary = newContent.lastIndexOf('\n---\n\n### ', limit);
|
|
376
|
+
if (lastBoundary > 0) {
|
|
377
|
+
newContent = newContent.slice(0, lastBoundary);
|
|
378
|
+
} else {
|
|
379
|
+
const sections = newContent.split('\n---\n\n### ');
|
|
380
|
+
if (sections.length > 10) { newContent = sections[0] + '\n---\n\n### ' + sections.slice(-8).join('\n---\n\n### '); }
|
|
381
|
+
}
|
|
376
382
|
}
|
|
377
|
-
|
|
378
|
-
|
|
383
|
+
safeWrite(NOTES_PATH, newContent);
|
|
384
|
+
});
|
|
379
385
|
classifyToKnowledgeBase(items);
|
|
380
386
|
archiveInboxFiles(files);
|
|
381
387
|
log('info', `Regex fallback: consolidated ${files.length} notes \u2192 ${deduped.length} insights into notes.md`);
|
package/engine/dispatch.js
CHANGED
|
@@ -116,7 +116,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
116
116
|
const maxRetries = ENGINE_DEFAULTS.maxRetries;
|
|
117
117
|
if (retryableFailure && retries < maxRetries) {
|
|
118
118
|
log('info', `Dispatch error for ${item.meta.item.id} — auto-retry ${retries + 1}/${maxRetries}`);
|
|
119
|
-
lifecycle().updateWorkItemStatus(item.meta,
|
|
119
|
+
lifecycle().updateWorkItemStatus(item.meta, WI_STATUS.PENDING, '');
|
|
120
120
|
// Remove this dispatch key from completed so dedupe doesn't block immediate redispatch.
|
|
121
121
|
if (item.meta?.dispatchKey) {
|
|
122
122
|
try {
|
|
@@ -150,7 +150,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
150
150
|
const finalReason = !retryableFailure
|
|
151
151
|
? `Non-retryable failure: ${reason || 'Unknown error'}`
|
|
152
152
|
: (reason || `Failed after ${maxRetries} retries`);
|
|
153
|
-
lifecycle().updateWorkItemStatus(item.meta,
|
|
153
|
+
lifecycle().updateWorkItemStatus(item.meta, WI_STATUS.FAILED, finalReason);
|
|
154
154
|
// Alert: find items blocked by this failure and write inbox note
|
|
155
155
|
try {
|
|
156
156
|
const config = getConfig();
|
package/engine/queries.js
CHANGED
|
@@ -10,7 +10,8 @@ const os = require('os');
|
|
|
10
10
|
const shared = require('./shared');
|
|
11
11
|
|
|
12
12
|
const { safeRead, safeReadDir, safeJson, safeWrite, getProjects,
|
|
13
|
-
projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES
|
|
13
|
+
projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES,
|
|
14
|
+
WI_STATUS } = shared;
|
|
14
15
|
|
|
15
16
|
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
16
17
|
|
|
@@ -179,7 +180,7 @@ function getAgentStatus(agentId) {
|
|
|
179
180
|
const latestInFlight = allItems
|
|
180
181
|
.filter(w =>
|
|
181
182
|
(w.dispatched_to || '').toLowerCase() === String(agentId).toLowerCase() &&
|
|
182
|
-
w.status ===
|
|
183
|
+
w.status === WI_STATUS.DISPATCHED
|
|
183
184
|
)
|
|
184
185
|
.sort((a, b) => (b.dispatched_at || '').localeCompare(a.dispatched_at || ''))[0];
|
|
185
186
|
if (latestInFlight) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.417",
|
|
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"
|