@yemi33/minions 0.1.6 → 0.1.8
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 +24 -0
- package/README.md +5 -5
- package/bin/minions.js +51 -2
- package/config.template.json +1 -25
- package/docs/auto-discovery.md +2 -2
- package/engine/cli.js +77 -5
- package/engine/preflight.js +239 -0
- package/engine/shared.js +1 -1
- package/engine/spawn-agent.js +4 -2
- package/engine.js +15 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.8 (2026-03-25)
|
|
4
|
+
|
|
5
|
+
### Engine
|
|
6
|
+
- engine.js
|
|
7
|
+
- engine/cli.js
|
|
8
|
+
- engine/preflight.js
|
|
9
|
+
- engine/shared.js
|
|
10
|
+
- engine/spawn-agent.js
|
|
11
|
+
|
|
12
|
+
### Documentation
|
|
13
|
+
- README.md
|
|
14
|
+
- auto-discovery.md
|
|
15
|
+
|
|
16
|
+
### Other
|
|
17
|
+
- CLAUDE.md
|
|
18
|
+
- bin/minions.js
|
|
19
|
+
- config.template.json
|
|
20
|
+
- test/unit.test.js
|
|
21
|
+
|
|
22
|
+
## 0.1.7 (2026-03-24)
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
- README.md
|
|
26
|
+
|
|
3
27
|
## 0.1.6 (2026-03-24)
|
|
4
28
|
|
|
5
29
|
### Other
|
package/README.md
CHANGED
|
@@ -59,15 +59,15 @@ Upgrades now skip the interactive repo scan automatically. If you want to re-run
|
|
|
59
59
|
|
|
60
60
|
**What's shown:** A summary of files updated, added, and preserved, plus a pointer to the changelog.
|
|
61
61
|
|
|
62
|
-
### Migrating from legacy
|
|
62
|
+
### Migrating from legacy installs
|
|
63
63
|
|
|
64
|
-
If you previously used
|
|
64
|
+
If you previously used an older install layout, run:
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
67
|
minions init --force
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
`minions` will auto-detect legacy
|
|
70
|
+
`minions` will auto-detect legacy runtime locations and markers, migrate state into `~/.minions`, normalize runtime marker names, and record the action in `~/.minions/migration.log`.
|
|
71
71
|
|
|
72
72
|
## Quick Start
|
|
73
73
|
|
|
@@ -473,7 +473,7 @@ Engine behavior is controlled via `config.json`. Key settings:
|
|
|
473
473
|
{
|
|
474
474
|
"engine": {
|
|
475
475
|
"tickInterval": 60000,
|
|
476
|
-
"maxConcurrent":
|
|
476
|
+
"maxConcurrent": 5,
|
|
477
477
|
"agentTimeout": 18000000,
|
|
478
478
|
"heartbeatTimeout": 300000,
|
|
479
479
|
"maxTurns": 100,
|
|
@@ -487,7 +487,7 @@ Engine behavior is controlled via `config.json`. Key settings:
|
|
|
487
487
|
| Setting | Default | Description |
|
|
488
488
|
|---------|---------|-------------|
|
|
489
489
|
| `tickInterval` | 60000 (1min) | Milliseconds between engine ticks |
|
|
490
|
-
| `maxConcurrent` |
|
|
490
|
+
| `maxConcurrent` | 5 | Max agents running simultaneously |
|
|
491
491
|
| `agentTimeout` | 18000000 (5h) | Max total agent runtime |
|
|
492
492
|
| `heartbeatTimeout` | 300000 (5min) | Kill agents silent longer than this |
|
|
493
493
|
| `maxTurns` | 100 | Max Claude CLI turns per agent session |
|
package/bin/minions.js
CHANGED
|
@@ -297,6 +297,13 @@ function init() {
|
|
|
297
297
|
showChangelog(installedVersion);
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
// Run preflight checks (warn only — don't block init)
|
|
301
|
+
try {
|
|
302
|
+
const { runPreflight, printPreflight } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
|
|
303
|
+
const { results } = runPreflight();
|
|
304
|
+
printPreflight(results, { label: 'Preflight checks' });
|
|
305
|
+
} catch {}
|
|
306
|
+
|
|
300
307
|
// Auto-start on fresh install; force-upgrade restarts automatically.
|
|
301
308
|
if (isUpgrade) {
|
|
302
309
|
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME }); } catch {}
|
|
@@ -315,7 +322,17 @@ function init() {
|
|
|
315
322
|
});
|
|
316
323
|
dashProc.unref();
|
|
317
324
|
console.log(` Dashboard started (PID: ${dashProc.pid})`);
|
|
318
|
-
console.log(' Dashboard: http://localhost:7331
|
|
325
|
+
console.log(' Dashboard: http://localhost:7331');
|
|
326
|
+
|
|
327
|
+
// Next steps guidance
|
|
328
|
+
console.log(`
|
|
329
|
+
Next steps:
|
|
330
|
+
minions work "Explore the codebase" Give your first task
|
|
331
|
+
minions status Check engine state
|
|
332
|
+
http://localhost:7331 Open the dashboard
|
|
333
|
+
minions doctor Verify everything is working
|
|
334
|
+
minions --help See all commands
|
|
335
|
+
`);
|
|
319
336
|
}
|
|
320
337
|
|
|
321
338
|
function copyDir(src, dest, excludeTop, alwaysUpdate, neverOverwrite, isUpgrade, actions, relPath = '') {
|
|
@@ -391,6 +408,16 @@ function showVersion() {
|
|
|
391
408
|
} else {
|
|
392
409
|
console.log(' Not installed yet. Run: minions init');
|
|
393
410
|
}
|
|
411
|
+
|
|
412
|
+
// Check npm registry for latest version (best-effort, non-blocking)
|
|
413
|
+
try {
|
|
414
|
+
const latest = execSync('npm view @yemi33/minions version', { encoding: 'utf8', timeout: 5000, windowsHide: true }).trim();
|
|
415
|
+
if (latest && latest !== pkg) {
|
|
416
|
+
console.log(`\n Latest on npm: ${latest}`);
|
|
417
|
+
console.log(' To update: npm update -g @yemi33/minions && minions init --force');
|
|
418
|
+
}
|
|
419
|
+
} catch {} // offline or npm not available — skip silently
|
|
420
|
+
|
|
394
421
|
console.log('');
|
|
395
422
|
}
|
|
396
423
|
|
|
@@ -428,12 +455,14 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
428
455
|
minions init [--skip-scan] Bootstrap ~/.minions/ (first time)
|
|
429
456
|
minions init --force Upgrade engine code + add new files (auto-skip scan)
|
|
430
457
|
minions version Show installed vs package version
|
|
458
|
+
minions doctor Check prerequisites and runtime health
|
|
431
459
|
minions add <project-dir> Link a project (interactive)
|
|
432
460
|
minions remove <project-dir> Unlink a project
|
|
433
461
|
minions list List linked projects
|
|
434
462
|
|
|
435
463
|
Engine:
|
|
436
|
-
minions
|
|
464
|
+
minions up Start engine + dashboard (use after reboot)
|
|
465
|
+
minions start Start engine daemon only
|
|
437
466
|
minions stop Stop the engine
|
|
438
467
|
minions status Show agents, projects, queue
|
|
439
468
|
minions pause / resume Pause/resume dispatching
|
|
@@ -455,6 +484,26 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
455
484
|
showVersion();
|
|
456
485
|
} else if (cmd === 'add' || cmd === 'remove' || cmd === 'list') {
|
|
457
486
|
delegate('minions.js', [cmd, ...rest]);
|
|
487
|
+
} else if (cmd === 'up' || cmd === 'restart') {
|
|
488
|
+
// Start both engine and dashboard — the go-to command after a reboot
|
|
489
|
+
ensureInstalled();
|
|
490
|
+
// Stop engine if running (ignore errors)
|
|
491
|
+
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME }); } catch {}
|
|
492
|
+
const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start'], {
|
|
493
|
+
cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
|
|
494
|
+
});
|
|
495
|
+
engineProc.unref();
|
|
496
|
+
console.log(`\n Engine started (PID: ${engineProc.pid})`);
|
|
497
|
+
const dashProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'dashboard.js')], {
|
|
498
|
+
cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
|
|
499
|
+
});
|
|
500
|
+
dashProc.unref();
|
|
501
|
+
console.log(` Dashboard started (PID: ${dashProc.pid})`);
|
|
502
|
+
console.log(' Dashboard: http://localhost:7331\n');
|
|
503
|
+
} else if (cmd === 'doctor') {
|
|
504
|
+
ensureInstalled();
|
|
505
|
+
const { doctor } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
|
|
506
|
+
doctor(MINIONS_HOME).then(ok => process.exit(ok ? 0 : 1));
|
|
458
507
|
} else if (cmd === 'dash' || cmd === 'dashboard') {
|
|
459
508
|
delegate('dashboard.js', rest);
|
|
460
509
|
} else if (engineCmds.has(cmd)) {
|
package/config.template.json
CHANGED
|
@@ -1,28 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"projects": [
|
|
3
|
-
{
|
|
4
|
-
"name": "YOUR_PROJECT_NAME",
|
|
5
|
-
"localPath": "/path/to/your/project",
|
|
6
|
-
"repoHost": "ado",
|
|
7
|
-
"repositoryId": "YOUR_REPO_ID",
|
|
8
|
-
"adoOrg": "YOUR_ORG",
|
|
9
|
-
"adoProject": "YOUR_PROJECT",
|
|
10
|
-
"repoName": "YOUR_REPO_NAME",
|
|
11
|
-
"mainBranch": "main",
|
|
12
|
-
"prUrlBase": "",
|
|
13
|
-
"workSources": {
|
|
14
|
-
"pullRequests": {
|
|
15
|
-
"enabled": true,
|
|
16
|
-
"path": ".minions/pull-requests.json",
|
|
17
|
-
"cooldownMinutes": 30
|
|
18
|
-
},
|
|
19
|
-
"workItems": {
|
|
20
|
-
"enabled": true,
|
|
21
|
-
"path": ".minions/work-items.json",
|
|
22
|
-
"cooldownMinutes": 0
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
]
|
|
2
|
+
"projects": []
|
|
27
3
|
}
|
|
28
4
|
|
package/docs/auto-discovery.md
CHANGED
|
@@ -196,7 +196,7 @@ Discovered items land in `engine/dispatch.json`:
|
|
|
196
196
|
Each tick, the engine checks available slots:
|
|
197
197
|
|
|
198
198
|
```
|
|
199
|
-
slotsAvailable = maxConcurrent (
|
|
199
|
+
slotsAvailable = maxConcurrent (5) - activeCount
|
|
200
200
|
```
|
|
201
201
|
|
|
202
202
|
It takes up to `slotsAvailable` items from pending and spawns them. Items are processed in discovery-priority order (fixes first, then reviews, then implements, then work-items).
|
|
@@ -378,7 +378,7 @@ All discovery behavior is controlled via `config.json`:
|
|
|
378
378
|
{
|
|
379
379
|
"engine": {
|
|
380
380
|
"tickInterval": 60000, // ms between ticks
|
|
381
|
-
"maxConcurrent":
|
|
381
|
+
"maxConcurrent": 5, // max agents running at once
|
|
382
382
|
"agentTimeout": 18000000, // 5 hours — kill hung processes
|
|
383
383
|
"heartbeatTimeout": 300000, // 5min — kill stale/silent agents
|
|
384
384
|
"maxTurns": 100, // max claude CLI turns per agent
|
package/engine/cli.js
CHANGED
|
@@ -40,18 +40,41 @@ function handleCommand(cmd, args) {
|
|
|
40
40
|
console.log(' complete <id> Mark dispatch as done');
|
|
41
41
|
console.log(' cleanup Clean temp files, worktrees, zombies');
|
|
42
42
|
console.log(' mcp-sync Sync MCP servers from ~/.claude.json');
|
|
43
|
+
console.log(' doctor Check prerequisites and runtime health');
|
|
43
44
|
process.exit(1);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const commands = {
|
|
48
49
|
start() {
|
|
50
|
+
// Run preflight checks (warn but don't block — engine may still be useful)
|
|
51
|
+
try {
|
|
52
|
+
const { runPreflight, printPreflight } = require('./preflight');
|
|
53
|
+
const { results } = runPreflight();
|
|
54
|
+
const hasFatal = results.some(r => r.ok === false);
|
|
55
|
+
if (hasFatal) {
|
|
56
|
+
printPreflight(results, { label: 'Preflight checks' });
|
|
57
|
+
console.log(' Some checks failed — agents may not work. Run `minions doctor` for details.\n');
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
|
|
49
61
|
const e = engine();
|
|
50
62
|
const control = getControl();
|
|
51
63
|
if (control.state === 'running') {
|
|
52
64
|
let alive = false;
|
|
53
65
|
if (control.pid) {
|
|
54
|
-
try {
|
|
66
|
+
try {
|
|
67
|
+
if (process.platform === 'win32') {
|
|
68
|
+
// On Windows, process.kill(pid, 0) can false-positive if the PID was recycled.
|
|
69
|
+
// Use tasklist and verify the process is actually node.
|
|
70
|
+
const { execSync } = require('child_process');
|
|
71
|
+
const out = execSync(`tasklist /FI "PID eq ${control.pid}" /NH`, { encoding: 'utf8', windowsHide: true, timeout: 3000 });
|
|
72
|
+
alive = out.includes(String(control.pid)) && out.toLowerCase().includes('node');
|
|
73
|
+
} else {
|
|
74
|
+
process.kill(control.pid, 0);
|
|
75
|
+
alive = true;
|
|
76
|
+
}
|
|
77
|
+
} catch {}
|
|
55
78
|
}
|
|
56
79
|
if (alive) {
|
|
57
80
|
console.log(`Engine is already running (PID ${control.pid}).`);
|
|
@@ -283,7 +306,7 @@ const commands = {
|
|
|
283
306
|
|
|
284
307
|
// Start tick loop
|
|
285
308
|
setInterval(() => e.tick(), interval);
|
|
286
|
-
console.log(`Tick interval: ${interval / 1000}s | Max concurrent: ${config.engine?.maxConcurrent ||
|
|
309
|
+
console.log(`Tick interval: ${interval / 1000}s | Max concurrent: ${config.engine?.maxConcurrent || 5}`);
|
|
287
310
|
console.log('Press Ctrl+C to stop');
|
|
288
311
|
},
|
|
289
312
|
|
|
@@ -338,10 +361,52 @@ const commands = {
|
|
|
338
361
|
const { getProjects } = require('./shared');
|
|
339
362
|
const projects = getProjects(config);
|
|
340
363
|
|
|
364
|
+
// Version info
|
|
365
|
+
let version = '?';
|
|
366
|
+
try {
|
|
367
|
+
const vFile = path.join(MINIONS_DIR, '.minions-version');
|
|
368
|
+
version = fs.readFileSync(vFile, 'utf8').trim();
|
|
369
|
+
} catch {}
|
|
370
|
+
|
|
341
371
|
console.log('\n=== Minions Engine ===\n');
|
|
342
|
-
console.log(`
|
|
343
|
-
|
|
344
|
-
|
|
372
|
+
console.log(`Version: ${version}`);
|
|
373
|
+
|
|
374
|
+
// Engine state with liveness check
|
|
375
|
+
let engineAlive = false;
|
|
376
|
+
if (control.state === 'running' && control.pid) {
|
|
377
|
+
try {
|
|
378
|
+
if (process.platform === 'win32') {
|
|
379
|
+
const { execSync } = require('child_process');
|
|
380
|
+
const out = execSync(`tasklist /FI "PID eq ${control.pid}" /NH`, { encoding: 'utf8', windowsHide: true, timeout: 3000 });
|
|
381
|
+
engineAlive = out.includes(String(control.pid)) && out.toLowerCase().includes('node');
|
|
382
|
+
} else {
|
|
383
|
+
process.kill(control.pid, 0);
|
|
384
|
+
engineAlive = true;
|
|
385
|
+
}
|
|
386
|
+
} catch {}
|
|
387
|
+
}
|
|
388
|
+
if (control.state === 'running' && !engineAlive) {
|
|
389
|
+
console.log(`Engine: stale (PID ${control.pid} is dead) — run: minions start`);
|
|
390
|
+
} else {
|
|
391
|
+
console.log(`Engine: ${control.state} (PID ${control.pid || 'N/A'})`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Dashboard check
|
|
395
|
+
const http = require('http');
|
|
396
|
+
const dashCheck = new Promise(resolve => {
|
|
397
|
+
const req = http.get('http://localhost:7331/api/health', { timeout: 2000 }, () => resolve(true));
|
|
398
|
+
req.on('error', () => resolve(false));
|
|
399
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
400
|
+
});
|
|
401
|
+
dashCheck.then(dashUp => {
|
|
402
|
+
if (dashUp) console.log('Dashboard: running (http://localhost:7331)');
|
|
403
|
+
else console.log('Dashboard: not running — start with: minions dash');
|
|
404
|
+
}).catch(() => {});
|
|
405
|
+
|
|
406
|
+
// Projects with health
|
|
407
|
+
const healthyProjects = projects.filter(p => p.localPath && fs.existsSync(path.resolve(p.localPath)));
|
|
408
|
+
const missingProjects = projects.filter(p => !p.localPath || !fs.existsSync(path.resolve(p.localPath)));
|
|
409
|
+
console.log(`Projects: ${healthyProjects.length} linked${missingProjects.length ? ` (${missingProjects.length} path missing)` : ''}`);
|
|
345
410
|
console.log('');
|
|
346
411
|
|
|
347
412
|
console.log('Agents:');
|
|
@@ -727,6 +792,13 @@ const commands = {
|
|
|
727
792
|
console.log('MCP servers are read directly from ~/.claude.json — no sync needed.');
|
|
728
793
|
},
|
|
729
794
|
|
|
795
|
+
doctor() {
|
|
796
|
+
const { doctor } = require('./preflight');
|
|
797
|
+
doctor(MINIONS_DIR).then(ok => {
|
|
798
|
+
if (!ok) process.exit(1);
|
|
799
|
+
});
|
|
800
|
+
},
|
|
801
|
+
|
|
730
802
|
discover() {
|
|
731
803
|
const e = engine();
|
|
732
804
|
const config = getConfig();
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/preflight.js — Prerequisite and health checks for Minions.
|
|
3
|
+
* Used by `minions init`, `minions start`, and `minions doctor`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the Claude Code CLI binary path.
|
|
12
|
+
* Returns the path if found, null otherwise.
|
|
13
|
+
* Reuses the same search logic as spawn-agent.js.
|
|
14
|
+
*/
|
|
15
|
+
function findClaudeBinary() {
|
|
16
|
+
const searchPaths = [
|
|
17
|
+
path.join(process.env.npm_config_prefix || '', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
18
|
+
path.join(process.env.APPDATA || '', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
19
|
+
'/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js',
|
|
20
|
+
'/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js',
|
|
21
|
+
];
|
|
22
|
+
for (const p of searchPaths) {
|
|
23
|
+
if (p && fs.existsSync(p)) return p;
|
|
24
|
+
}
|
|
25
|
+
// Fallback: parse the shell wrapper
|
|
26
|
+
try {
|
|
27
|
+
const which = execSync('bash -c "which claude"', { encoding: 'utf8', windowsHide: true, timeout: 5000 }).trim();
|
|
28
|
+
const wrapper = execSync(`bash -c "cat '${which}'"`, { encoding: 'utf8', windowsHide: true, timeout: 5000 });
|
|
29
|
+
const m = wrapper.match(/node_modules\/@anthropic-ai\/claude-code\/cli\.js/);
|
|
30
|
+
if (m) {
|
|
31
|
+
const basedir = path.dirname(which.replace(/^\/c\//, 'C:/').replace(/\//g, path.sep));
|
|
32
|
+
const resolved = path.join(basedir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
|
|
33
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
34
|
+
}
|
|
35
|
+
} catch {}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Run prerequisite checks. Returns { passed, results } where results is an
|
|
41
|
+
* array of { name, ok, message } objects.
|
|
42
|
+
*
|
|
43
|
+
* Options:
|
|
44
|
+
* - warnOnly: if true, missing items don't cause passed=false (for init)
|
|
45
|
+
* - verbose: include extra detail in messages
|
|
46
|
+
*/
|
|
47
|
+
function runPreflight(opts = {}) {
|
|
48
|
+
const results = [];
|
|
49
|
+
let allOk = true;
|
|
50
|
+
|
|
51
|
+
// 1. Node.js version >= 18
|
|
52
|
+
const nodeVersion = process.versions.node;
|
|
53
|
+
const major = parseInt(nodeVersion.split('.')[0], 10);
|
|
54
|
+
if (major >= 18) {
|
|
55
|
+
results.push({ name: 'Node.js', ok: true, message: `v${nodeVersion}` });
|
|
56
|
+
} else {
|
|
57
|
+
results.push({ name: 'Node.js', ok: false, message: `v${nodeVersion} — requires >= 18. Upgrade at https://nodejs.org` });
|
|
58
|
+
allOk = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Git available
|
|
62
|
+
try {
|
|
63
|
+
const gitVersion = execSync('git --version', { encoding: 'utf8', windowsHide: true, timeout: 5000 }).trim();
|
|
64
|
+
results.push({ name: 'Git', ok: true, message: gitVersion.replace('git version ', 'v') });
|
|
65
|
+
} catch {
|
|
66
|
+
results.push({ name: 'Git', ok: false, message: 'not found — install from https://git-scm.com' });
|
|
67
|
+
allOk = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 3. Claude Code CLI
|
|
71
|
+
const claudeBin = findClaudeBinary();
|
|
72
|
+
if (claudeBin) {
|
|
73
|
+
results.push({ name: 'Claude Code CLI', ok: true, message: path.basename(path.dirname(path.dirname(claudeBin))) });
|
|
74
|
+
} else {
|
|
75
|
+
results.push({ name: 'Claude Code CLI', ok: false, message: 'not found — install with: npm install -g @anthropic-ai/claude-code' });
|
|
76
|
+
allOk = false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. Anthropic API key or Claude Max (best-effort warning)
|
|
80
|
+
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
81
|
+
if (hasApiKey) {
|
|
82
|
+
results.push({ name: 'Anthropic auth', ok: true, message: 'ANTHROPIC_API_KEY set' });
|
|
83
|
+
} else {
|
|
84
|
+
// Not fatal — user may have Claude Max subscription
|
|
85
|
+
results.push({ name: 'Anthropic auth', ok: 'warn', message: 'ANTHROPIC_API_KEY not set — agents need an API key or Claude Max subscription' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { passed: allOk, results };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Print preflight results to console. Returns true if all critical checks passed.
|
|
93
|
+
*/
|
|
94
|
+
function printPreflight(results, { label = 'Preflight checks' } = {}) {
|
|
95
|
+
console.log(`\n ${label}:\n`);
|
|
96
|
+
let allOk = true;
|
|
97
|
+
for (const r of results) {
|
|
98
|
+
const icon = r.ok === true ? '\u2713' : r.ok === 'warn' ? '!' : '\u2717';
|
|
99
|
+
const prefix = r.ok === true ? ' ' : r.ok === 'warn' ? ' ' : ' ';
|
|
100
|
+
console.log(`${prefix} ${icon} ${r.name}: ${r.message}`);
|
|
101
|
+
if (r.ok === false) allOk = false;
|
|
102
|
+
}
|
|
103
|
+
console.log('');
|
|
104
|
+
return allOk;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Run preflight and print results. Exits with code 1 if fatal checks fail
|
|
109
|
+
* and exitOnFail is true.
|
|
110
|
+
*/
|
|
111
|
+
function checkOrExit({ exitOnFail = false, label = 'Preflight checks' } = {}) {
|
|
112
|
+
const { passed, results } = runPreflight();
|
|
113
|
+
const ok = printPreflight(results, { label });
|
|
114
|
+
if (!ok && exitOnFail) {
|
|
115
|
+
console.error(' Fix the issues above before continuing.\n');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
return ok;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Run extended doctor checks (preflight + runtime health).
|
|
123
|
+
* Requires minionsHome path for runtime checks.
|
|
124
|
+
*/
|
|
125
|
+
function doctor(minionsHome) {
|
|
126
|
+
const { passed, results } = runPreflight();
|
|
127
|
+
|
|
128
|
+
// Runtime checks
|
|
129
|
+
const runtimeResults = [];
|
|
130
|
+
|
|
131
|
+
// Check if minions is installed
|
|
132
|
+
const engineJs = path.join(minionsHome, 'engine.js');
|
|
133
|
+
if (fs.existsSync(engineJs)) {
|
|
134
|
+
runtimeResults.push({ name: 'Minions installed', ok: true, message: minionsHome });
|
|
135
|
+
} else {
|
|
136
|
+
runtimeResults.push({ name: 'Minions installed', ok: false, message: `not found at ${minionsHome} — run: minions init` });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check config.json
|
|
140
|
+
const configPath = path.join(minionsHome, 'config.json');
|
|
141
|
+
try {
|
|
142
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
143
|
+
const projects = config.projects || [];
|
|
144
|
+
const real = projects.filter(p => p.name && !p.name.startsWith('YOUR_'));
|
|
145
|
+
if (real.length > 0) {
|
|
146
|
+
runtimeResults.push({ name: 'Projects configured', ok: true, message: `${real.length} project(s)` });
|
|
147
|
+
} else {
|
|
148
|
+
runtimeResults.push({ name: 'Projects configured', ok: false, message: 'no projects — run: minions add <dir>' });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check project paths exist
|
|
152
|
+
for (const p of real) {
|
|
153
|
+
if (p.localPath && !fs.existsSync(p.localPath)) {
|
|
154
|
+
runtimeResults.push({ name: `Project "${p.name}"`, ok: false, message: `path not found: ${p.localPath}` });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check agents
|
|
159
|
+
const agents = config.agents || {};
|
|
160
|
+
if (Object.keys(agents).length > 0) {
|
|
161
|
+
runtimeResults.push({ name: 'Agents configured', ok: true, message: `${Object.keys(agents).length} agent(s)` });
|
|
162
|
+
} else {
|
|
163
|
+
runtimeResults.push({ name: 'Agents configured', ok: false, message: 'no agents in config.json' });
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
runtimeResults.push({ name: 'Config', ok: false, message: `missing or invalid — run: minions init` });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check engine status
|
|
170
|
+
const controlPath = path.join(minionsHome, 'engine', 'control.json');
|
|
171
|
+
try {
|
|
172
|
+
const control = JSON.parse(fs.readFileSync(controlPath, 'utf8'));
|
|
173
|
+
if (control.state === 'running' && control.pid) {
|
|
174
|
+
let alive = false;
|
|
175
|
+
try {
|
|
176
|
+
if (process.platform === 'win32') {
|
|
177
|
+
const out = execSync(`tasklist /FI "PID eq ${control.pid}" /NH`, { encoding: 'utf8', windowsHide: true, timeout: 3000 });
|
|
178
|
+
alive = out.includes(String(control.pid)) && out.toLowerCase().includes('node');
|
|
179
|
+
} else {
|
|
180
|
+
process.kill(control.pid, 0);
|
|
181
|
+
alive = true;
|
|
182
|
+
}
|
|
183
|
+
} catch {}
|
|
184
|
+
runtimeResults.push({ name: 'Engine', ok: alive, message: alive ? `running (PID ${control.pid})` : `stale PID ${control.pid} — run: minions start` });
|
|
185
|
+
} else {
|
|
186
|
+
runtimeResults.push({ name: 'Engine', ok: 'warn', message: `${control.state || 'stopped'} — run: minions start` });
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
runtimeResults.push({ name: 'Engine', ok: 'warn', message: 'not started — run: minions start' });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check dashboard (try HTTP)
|
|
193
|
+
const http = require('http');
|
|
194
|
+
const dashCheck = new Promise(resolve => {
|
|
195
|
+
const req = http.get('http://localhost:7331/api/health', { timeout: 2000 }, res => {
|
|
196
|
+
resolve({ name: 'Dashboard', ok: true, message: 'running on http://localhost:7331' });
|
|
197
|
+
});
|
|
198
|
+
req.on('error', () => resolve({ name: 'Dashboard', ok: 'warn', message: 'not reachable on :7331 — run: minions dash' }));
|
|
199
|
+
req.on('timeout', () => { req.destroy(); resolve({ name: 'Dashboard', ok: 'warn', message: 'not reachable on :7331 — run: minions dash' }); });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return dashCheck.then(dashResult => {
|
|
203
|
+
runtimeResults.push(dashResult);
|
|
204
|
+
|
|
205
|
+
// Check playbooks
|
|
206
|
+
const playbooksDir = path.join(minionsHome, 'playbooks');
|
|
207
|
+
const required = ['implement.md', 'review.md', 'fix.md'];
|
|
208
|
+
const missing = required.filter(f => !fs.existsSync(path.join(playbooksDir, f)));
|
|
209
|
+
if (missing.length === 0) {
|
|
210
|
+
runtimeResults.push({ name: 'Playbooks', ok: true, message: `${required.length} required playbooks present` });
|
|
211
|
+
} else {
|
|
212
|
+
runtimeResults.push({ name: 'Playbooks', ok: false, message: `missing: ${missing.join(', ')} — run: minions init --force` });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check port 7331 availability (only if dashboard isn't running)
|
|
216
|
+
if (dashResult.ok !== true) {
|
|
217
|
+
// Dashboard isn't running, port should be free
|
|
218
|
+
runtimeResults.push({ name: 'Port 7331', ok: 'warn', message: 'dashboard not running — port status unknown' });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Print all
|
|
222
|
+
const allResults = [...results, ...runtimeResults];
|
|
223
|
+
const ok = printPreflight(allResults, { label: 'Minions Doctor' });
|
|
224
|
+
|
|
225
|
+
const criticalFails = allResults.filter(r => r.ok === false).length;
|
|
226
|
+
const warnings = allResults.filter(r => r.ok === 'warn').length;
|
|
227
|
+
if (criticalFails === 0 && warnings === 0) {
|
|
228
|
+
console.log(' All checks passed.\n');
|
|
229
|
+
} else if (criticalFails === 0) {
|
|
230
|
+
console.log(` ${warnings} warning(s), no critical issues.\n`);
|
|
231
|
+
} else {
|
|
232
|
+
console.log(` ${criticalFails} issue(s) to fix, ${warnings} warning(s).\n`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return ok;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = { findClaudeBinary, runPreflight, printPreflight, checkOrExit, doctor };
|
package/engine/shared.js
CHANGED
|
@@ -231,7 +231,7 @@ function classifyInboxItem(name, content) {
|
|
|
231
231
|
|
|
232
232
|
const ENGINE_DEFAULTS = {
|
|
233
233
|
tickInterval: 60000,
|
|
234
|
-
maxConcurrent:
|
|
234
|
+
maxConcurrent: 5,
|
|
235
235
|
inboxConsolidateThreshold: 5,
|
|
236
236
|
agentTimeout: 18000000, // 5h
|
|
237
237
|
heartbeatTimeout: 300000, // 5min
|
package/engine/spawn-agent.js
CHANGED
|
@@ -67,8 +67,10 @@ if (isResume) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
if (!claudeBin) {
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const msg = 'FATAL: Cannot find claude-code cli.js — install with: npm install -g @anthropic-ai/claude-code';
|
|
71
|
+
fs.appendFileSync(debugPath, msg + '\n');
|
|
72
|
+
console.error(msg);
|
|
73
|
+
process.exit(78); // 78 = configuration error (distinct from runtime failures)
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
// Check if --system-prompt-file is supported (cached to avoid spawning claude --help every call)
|
package/engine.js
CHANGED
|
@@ -942,6 +942,17 @@ function spawnAgent(dispatchItem, config) {
|
|
|
942
942
|
safeWrite(archivePath, outputContent);
|
|
943
943
|
safeWrite(latestPath, outputContent); // overwrite latest for dashboard compat
|
|
944
944
|
|
|
945
|
+
// Detect configuration errors (e.g. Claude CLI not found) — fail immediately with clear message
|
|
946
|
+
if (code === 78) {
|
|
947
|
+
const errMsg = stderr.includes('claude-code') ? stderr.trim() : 'Configuration error — Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code';
|
|
948
|
+
log('error', `Agent ${agentId} (${id}) failed: ${errMsg}`);
|
|
949
|
+
completeDispatch(id, 'error', errMsg, '');
|
|
950
|
+
try { fs.unlinkSync(sysPromptPath); } catch {}
|
|
951
|
+
try { fs.unlinkSync(promptPath); } catch {}
|
|
952
|
+
try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch {}
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
945
956
|
// Parse output and run all post-completion hooks
|
|
946
957
|
const { resultSummary } = runPostCompletionHooks(dispatchItem, agentId, code, stdout, config);
|
|
947
958
|
|
|
@@ -1186,7 +1197,7 @@ function areDependenciesMet(item, config) {
|
|
|
1186
1197
|
return false;
|
|
1187
1198
|
}
|
|
1188
1199
|
if (depItem.status === 'failed') return 'failed';
|
|
1189
|
-
if (
|
|
1200
|
+
if (!PRD_MET_STATUSES.has(depItem.status)) return false; // Pending, dispatched, or retrying — wait (legacy aliases accepted)
|
|
1190
1201
|
}
|
|
1191
1202
|
return true;
|
|
1192
1203
|
}
|
|
@@ -1893,7 +1904,7 @@ function saveCooldowns() {
|
|
|
1893
1904
|
function isOnCooldown(key, cooldownMs) {
|
|
1894
1905
|
const entry = dispatchCooldowns.get(key);
|
|
1895
1906
|
if (!entry) return false;
|
|
1896
|
-
const backoff = Math.min(Math.pow(2, entry.failures || 0),
|
|
1907
|
+
const backoff = Math.min(Math.pow(2, entry.failures || 0), 8);
|
|
1897
1908
|
return (Date.now() - entry.timestamp) < (cooldownMs * backoff);
|
|
1898
1909
|
}
|
|
1899
1910
|
|
|
@@ -1908,7 +1919,7 @@ function setCooldownFailure(key) {
|
|
|
1908
1919
|
const failures = (existing?.failures || 0) + 1;
|
|
1909
1920
|
dispatchCooldowns.set(key, { timestamp: Date.now(), failures });
|
|
1910
1921
|
if (failures >= 3) {
|
|
1911
|
-
log('warn', `${key} has failed ${failures} times — cooldown is now ${Math.min(Math.pow(2, failures),
|
|
1922
|
+
log('warn', `${key} has failed ${failures} times — cooldown is now ${Math.min(Math.pow(2, failures), 8)}x`);
|
|
1912
1923
|
}
|
|
1913
1924
|
saveCooldowns();
|
|
1914
1925
|
}
|
|
@@ -3158,7 +3169,7 @@ async function tickInner() {
|
|
|
3158
3169
|
// 5. Process pending dispatches — auto-spawn agents
|
|
3159
3170
|
const dispatch = getDispatch();
|
|
3160
3171
|
const activeCount = (dispatch.active || []).length;
|
|
3161
|
-
const maxConcurrent = config.engine?.maxConcurrent ||
|
|
3172
|
+
const maxConcurrent = config.engine?.maxConcurrent || 5;
|
|
3162
3173
|
|
|
3163
3174
|
if (activeCount >= maxConcurrent) {
|
|
3164
3175
|
log('info', `At max concurrency (${activeCount}/${maxConcurrent}) — skipping dispatch`);
|
package/package.json
CHANGED