@yemi33/minions 0.1.7 → 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 CHANGED
@@ -1,5 +1,24 @@
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
+
3
22
  ## 0.1.7 (2026-03-24)
4
23
 
5
24
  ### Documentation
package/README.md CHANGED
@@ -473,7 +473,7 @@ Engine behavior is controlled via `config.json`. Key settings:
473
473
  {
474
474
  "engine": {
475
475
  "tickInterval": 60000,
476
- "maxConcurrent": 3,
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` | 3 | Max agents running simultaneously |
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\n');
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 start Start engine daemon
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)) {
@@ -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
 
@@ -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 (3) - activeCount
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": 3, // max agents running at once
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 { process.kill(control.pid, 0); alive = true; } catch {}
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 || 3}`);
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(`State: ${control.state}`);
343
- console.log(`PID: ${control.pid || 'N/A'}`);
344
- console.log(`Projects: ${projects.map(p => p.name || 'unnamed').join(', ')}`);
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: 3,
234
+ maxConcurrent: 5,
235
235
  inboxConsolidateThreshold: 5,
236
236
  agentTimeout: 18000000, // 5h
237
237
  heartbeatTimeout: 300000, // 5min
@@ -67,8 +67,10 @@ if (isResume) {
67
67
  }
68
68
 
69
69
  if (!claudeBin) {
70
- fs.appendFileSync(debugPath, 'FATAL: Cannot find claude-code cli.js\n');
71
- process.exit(1);
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 (depItem.status !== 'done' && depItem.status !== 'in-pr') return false; // Pending, dispatched, or retrying — wait (in-pr accepted for backward compat)
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), 2);
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), 2)}x`);
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 || 3;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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"