@yemi33/minions 0.1.575 → 0.1.577

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.577 (2026-04-08)
4
+
5
+ ### Other
6
+ - perf: parallelize worktree/prompt, direct CLI spawn, parallel dep fetch
7
+ - chore: remove orphaned comment, fix PID write test assertion
8
+
3
9
  ## 0.1.575 (2026-04-08)
4
10
 
5
11
  ### Fixes
package/dashboard.js CHANGED
@@ -788,7 +788,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
788
788
  // Attempt 1: resume existing session — skip preamble (session already has context)
789
789
  if (sessionId && maxTurns > 1) {
790
790
  result = await llm.callLLM(buildPrompt({ includePreamble: false }), '', {
791
- timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort,
791
+ timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
792
792
  });
793
793
  llm.trackEngineUsage(label, result.usage);
794
794
 
@@ -823,7 +823,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
823
823
  // Attempt 2: fresh session (include preamble for full context)
824
824
  const freshPrompt = buildPrompt();
825
825
  result = await llm.callLLM(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
826
- timeout, label, model, maxTurns, allowedTools, effort: ccEffort,
826
+ timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
827
827
  });
828
828
  llm.trackEngineUsage(label, result.usage);
829
829
 
@@ -837,7 +837,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
837
837
  console.log(`[${label}] Fresh call also failed (code=${result.code}, empty=${!result.text}), retrying once more...`);
838
838
  await new Promise(r => setTimeout(r, 2000));
839
839
  result = await llm.callLLM(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
840
- timeout, label, model, maxTurns, allowedTools, effort: ccEffort,
840
+ timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
841
841
  });
842
842
  llm.trackEngineUsage(label, result.usage);
843
843
 
@@ -3350,7 +3350,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3350
3350
  const llmPromise = callLLMStreaming(prompt, CC_STATIC_SYSTEM_PROMPT, {
3351
3351
  timeout: 900000, label: 'command-center', model: streamModel, maxTurns: 50,
3352
3352
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
3353
- sessionId, effort: streamEffort,
3353
+ sessionId, effort: streamEffort, direct: true,
3354
3354
  onChunk: (text) => {
3355
3355
  try { res.write('data: ' + JSON.stringify({ type: 'chunk', text }) + '\n\n'); } catch {}
3356
3356
  },
@@ -4221,7 +4221,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4221
4221
  { method: 'POST', path: '/api/settings/reset', desc: 'Reset engine + claude + agent settings to defaults', handler: handleSettingsReset },
4222
4222
  ];
4223
4223
 
4224
- // Expose routes to CC preamble builder (once, on first request)
4225
4224
  // ── Route Dispatcher ────────────────────────────────────────────────────────
4226
4225
 
4227
4226
  const pathname = req.url.split('?')[0];
package/engine/llm.js CHANGED
@@ -43,32 +43,79 @@ function trackEngineUsage(category, usage) {
43
43
  } catch (e) { console.error('metrics update:', e.message); }
44
44
  }
45
45
 
46
+ // ── Claude Binary Resolution (cached by spawn-agent.js) ─────────────────────
47
+
48
+ let _claudeBinCache = null;
49
+ function _resolveClaudeBin() {
50
+ if (_claudeBinCache) return _claudeBinCache;
51
+ const caps = shared.safeJson(path.join(ENGINE_DIR, 'claude-caps.json'));
52
+ if (caps?.claudeBin && require('fs').existsSync(caps.claudeBin)) {
53
+ _claudeBinCache = { bin: caps.claudeBin, native: !!caps.claudeIsNative };
54
+ return _claudeBinCache;
55
+ }
56
+ return null;
57
+ }
58
+
59
+ // ── Spawn Helpers ───────────────────────────────────────────────────────────
60
+
61
+ function _buildCliArgs({ model, maxTurns, allowedTools, effort, sessionId, sysPromptFile }) {
62
+ const args = ['-p', '--output-format', 'stream-json', '--max-turns', String(maxTurns), '--model', model, '--verbose'];
63
+ if (sysPromptFile) args.push('--system-prompt-file', sysPromptFile);
64
+ if (allowedTools) args.push('--allowedTools', allowedTools);
65
+ if (effort) args.push('--effort', effort);
66
+ args.push('--permission-mode', 'bypassPermissions');
67
+ if (sessionId) args.push('--resume', sessionId);
68
+ return args;
69
+ }
70
+
46
71
  // ── Core LLM Call ───────────────────────────────────────────────────────────
47
72
 
48
- function callLLM(promptText, sysPromptText, { timeout = 120000, label = 'llm', model = 'sonnet', maxTurns = 1, allowedTools = '', sessionId = null, effort = null } = {}) {
73
+ function callLLM(promptText, sysPromptText, { timeout = 120000, label = 'llm', model = 'sonnet', maxTurns = 1, allowedTools = '', sessionId = null, effort = null, direct = false } = {}) {
49
74
  return new Promise((resolve) => {
75
+ const _startMs = Date.now();
76
+ const fs = require('fs');
50
77
  const id = uid();
51
78
  const tmpDir = path.join(ENGINE_DIR, 'tmp');
52
- if (!require('fs').existsSync(tmpDir)) require('fs').mkdirSync(tmpDir, { recursive: true });
53
- const promptPath = path.join(tmpDir, `${label}-prompt-${id}.md`);
54
- const sysPath = path.join(tmpDir, `${label}-sys-${id}.md`);
55
- safeWrite(promptPath, promptText);
56
- safeWrite(sysPath, sysPromptText || '');
57
-
58
- const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
59
- const args = [
60
- spawnScript, promptPath, sysPath,
61
- '--output-format', 'stream-json', '--max-turns', String(maxTurns), '--model', model,
62
- '--verbose',
63
- ];
64
- if (allowedTools) args.push('--allowedTools', allowedTools);
65
- if (effort) args.push('--effort', effort);
66
- args.push('--permission-mode', 'bypassPermissions');
67
-
68
- if (sessionId) args.push('--resume', sessionId);
79
+ if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
69
80
 
70
- const _startMs = Date.now();
71
- const proc = runFile(process.execPath, args, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
81
+ let proc;
82
+ const cleanupFiles = [];
83
+ const resolved = direct ? _resolveClaudeBin() : null;
84
+
85
+ if (resolved) {
86
+ // Direct spawn: skip spawn-agent.js — fewer file syscalls, no extra process
87
+ let sysTmpPath = null;
88
+ if (!sessionId && sysPromptText) {
89
+ sysTmpPath = path.join(tmpDir, `direct-sys-${id}.md`);
90
+ fs.writeFileSync(sysTmpPath, sysPromptText);
91
+ cleanupFiles.push(sysTmpPath);
92
+ }
93
+ const cliArgs = _buildCliArgs({ model, maxTurns, allowedTools, effort, sessionId, sysPromptFile: sysTmpPath });
94
+ proc = resolved.native
95
+ ? runFile(resolved.bin, cliArgs, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() })
96
+ : runFile(process.execPath, [resolved.bin, ...cliArgs], { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
97
+ try { proc.stdin.write(promptText); proc.stdin.end(); } catch { /* broken pipe */ }
98
+ } else {
99
+ // Indirect: use spawn-agent.js (for agent dispatches or if binary not cached)
100
+ const promptPath = path.join(tmpDir, `${label}-prompt-${id}.md`);
101
+ const sysPath = path.join(tmpDir, `${label}-sys-${id}.md`);
102
+ safeWrite(promptPath, promptText);
103
+ safeWrite(sysPath, sysPromptText || '');
104
+ cleanupFiles.push(promptPath, sysPath);
105
+
106
+ const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
107
+ const args = [
108
+ spawnScript, promptPath, sysPath,
109
+ '--output-format', 'stream-json', '--max-turns', String(maxTurns), '--model', model,
110
+ '--verbose',
111
+ ];
112
+ if (allowedTools) args.push('--allowedTools', allowedTools);
113
+ if (effort) args.push('--effort', effort);
114
+ args.push('--permission-mode', 'bypassPermissions');
115
+ if (sessionId) args.push('--resume', sessionId);
116
+
117
+ proc = runFile(process.execPath, args, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
118
+ }
72
119
 
73
120
  let stdout = '';
74
121
  let stderr = '';
@@ -79,8 +126,7 @@ function callLLM(promptText, sysPromptText, { timeout = 120000, label = 'llm', m
79
126
 
80
127
  proc.on('close', (code) => {
81
128
  clearTimeout(timer);
82
- safeUnlink(promptPath);
83
- safeUnlink(sysPath);
129
+ for (const f of cleanupFiles) safeUnlink(f);
84
130
  const parsed = parseStreamJsonOutput(stdout);
85
131
  const durationMs = Date.now() - _startMs;
86
132
  const usage = parsed.usage ? { ...parsed.usage, durationMs } : { durationMs };
@@ -89,8 +135,7 @@ function callLLM(promptText, sysPromptText, { timeout = 120000, label = 'llm', m
89
135
 
90
136
  proc.on('error', (err) => {
91
137
  clearTimeout(timer);
92
- safeUnlink(promptPath);
93
- safeUnlink(sysPath);
138
+ for (const f of cleanupFiles) safeUnlink(f);
94
139
  resolve({ text: '', usage: null, sessionId: null, code: 1, stderr: err.message, raw: '' });
95
140
  });
96
141
  });
@@ -118,30 +163,51 @@ function isResumeSessionStillValid(result) {
118
163
  * Returns the same result object as callLLM when the process completes.
119
164
  * onChunk(text) is called for each assistant text block as it arrives.
120
165
  */
121
- function callLLMStreaming(promptText, sysPromptText, { timeout = 120000, label = 'llm', model = 'sonnet', maxTurns = 1, allowedTools = '', sessionId = null, onChunk = () => {}, onToolUse = null, effort = null } = {}) {
166
+ function callLLMStreaming(promptText, sysPromptText, { timeout = 120000, label = 'llm', model = 'sonnet', maxTurns = 1, allowedTools = '', sessionId = null, onChunk = () => {}, onToolUse = null, effort = null, direct = false } = {}) {
122
167
  let _abort = null;
123
168
  const promise = new Promise((resolve) => {
169
+ const _startMs = Date.now();
170
+ const fs = require('fs');
124
171
  const id = uid();
125
172
  const tmpDir = path.join(ENGINE_DIR, 'tmp');
126
- if (!require('fs').existsSync(tmpDir)) require('fs').mkdirSync(tmpDir, { recursive: true });
127
- const promptPath = path.join(tmpDir, `${label}-prompt-${id}.md`);
128
- const sysPath = path.join(tmpDir, `${label}-sys-${id}.md`);
129
- safeWrite(promptPath, promptText);
130
- safeWrite(sysPath, sysPromptText || '');
131
-
132
- const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
133
- const args = [
134
- spawnScript, promptPath, sysPath,
135
- '--output-format', 'stream-json', '--max-turns', String(maxTurns), '--model', model,
136
- '--verbose',
137
- ];
138
- if (allowedTools) args.push('--allowedTools', allowedTools);
139
- if (effort) args.push('--effort', effort);
140
- args.push('--permission-mode', 'bypassPermissions');
141
- if (sessionId) args.push('--resume', sessionId);
173
+ if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
142
174
 
143
- const _startMs = Date.now();
144
- const proc = runFile(process.execPath, args, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
175
+ let proc;
176
+ const cleanupFiles = [];
177
+ const resolved = direct ? _resolveClaudeBin() : null;
178
+
179
+ if (resolved) {
180
+ let sysTmpPath = null;
181
+ if (!sessionId && sysPromptText) {
182
+ sysTmpPath = path.join(tmpDir, `direct-sys-${id}.md`);
183
+ fs.writeFileSync(sysTmpPath, sysPromptText);
184
+ cleanupFiles.push(sysTmpPath);
185
+ }
186
+ const cliArgs = _buildCliArgs({ model, maxTurns, allowedTools, effort, sessionId, sysPromptFile: sysTmpPath });
187
+ proc = resolved.native
188
+ ? runFile(resolved.bin, cliArgs, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() })
189
+ : runFile(process.execPath, [resolved.bin, ...cliArgs], { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
190
+ try { proc.stdin.write(promptText); proc.stdin.end(); } catch { /* broken pipe */ }
191
+ } else {
192
+ const promptPath = path.join(tmpDir, `${label}-prompt-${id}.md`);
193
+ const sysPath = path.join(tmpDir, `${label}-sys-${id}.md`);
194
+ safeWrite(promptPath, promptText);
195
+ safeWrite(sysPath, sysPromptText || '');
196
+ cleanupFiles.push(promptPath, sysPath);
197
+
198
+ const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
199
+ const args = [
200
+ spawnScript, promptPath, sysPath,
201
+ '--output-format', 'stream-json', '--max-turns', String(maxTurns), '--model', model,
202
+ '--verbose',
203
+ ];
204
+ if (allowedTools) args.push('--allowedTools', allowedTools);
205
+ if (effort) args.push('--effort', effort);
206
+ args.push('--permission-mode', 'bypassPermissions');
207
+ if (sessionId) args.push('--resume', sessionId);
208
+
209
+ proc = runFile(process.execPath, args, { cwd: MINIONS_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: cleanChildEnv() });
210
+ }
145
211
 
146
212
  _abort = () => { shared.killImmediate(proc); };
147
213
 
@@ -181,8 +247,7 @@ function callLLMStreaming(promptText, sysPromptText, { timeout = 120000, label =
181
247
 
182
248
  proc.on('close', (code) => {
183
249
  clearTimeout(timer);
184
- safeUnlink(promptPath);
185
- safeUnlink(sysPath);
250
+ for (const f of cleanupFiles) safeUnlink(f);
186
251
  const parsed = parseStreamJsonOutput(stdout);
187
252
  const durationMs = Date.now() - _startMs;
188
253
  const usage = parsed.usage ? { ...parsed.usage, durationMs } : { durationMs };
@@ -191,8 +256,7 @@ function callLLMStreaming(promptText, sysPromptText, { timeout = 120000, label =
191
256
 
192
257
  proc.on('error', (err) => {
193
258
  clearTimeout(timer);
194
- safeUnlink(promptPath);
195
- safeUnlink(sysPath);
259
+ for (const f of cleanupFiles) safeUnlink(f);
196
260
  resolve({ text: '', usage: null, sessionId: null, code: 1, stderr: err.message, raw: '' });
197
261
  });
198
262
  });
package/engine.js CHANGED
@@ -278,6 +278,21 @@ async function spawnAgent(dispatchItem, config) {
278
278
  const _gitOpts = { stdio: 'pipe', timeout: 30000, windowsHide: true, env: shared.gitEnv() };
279
279
  const _worktreeGitOpts = { ..._gitOpts, timeout: worktreeCreateTimeout };
280
280
 
281
+ // Build prompt before worktree setup — prompt doesn't depend on worktree path
282
+ // and this avoids blocking 200ms of file reads behind 20-60s of git operations
283
+ const systemPrompt = buildSystemPrompt(agentId, config, project);
284
+ const agentContext = buildAgentContext(agentId, config, project);
285
+ const fullTaskPrompt = agentContext
286
+ ? `## Agent Context\n\n${agentContext}\n---\n\n## Your Task\n\n${taskPrompt}`
287
+ : taskPrompt;
288
+ const tmpDir = path.join(ENGINE_DIR, 'tmp');
289
+ if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
290
+ const safeId = id.replace(/[:\\/*?"<>|]/g, '-');
291
+ const promptPath = path.join(tmpDir, `prompt-${safeId}.md`);
292
+ safeWrite(promptPath, fullTaskPrompt);
293
+ const sysPromptPath = path.join(tmpDir, `sysprompt-${safeId}.md`);
294
+ safeWrite(sysPromptPath, systemPrompt);
295
+
281
296
  if (branchName) {
282
297
  const wtSuffix = id ? id.split('-').pop() : shared.uid();
283
298
  const projectSlug = (project.name || 'default').replace(/[^a-zA-Z0-9_-]/g, '-');
@@ -413,21 +428,37 @@ async function spawnAgent(dispatchItem, config) {
413
428
  try {
414
429
  const depBranches = resolveDependencyBranches(depIds, meta?.item?.sourcePlan, project, config);
415
430
  let depMergeFailed = false;
416
- for (const { branch: depBranch, prId } of depBranches) {
417
- // Skip refs already known to be missing this tick (avoids repeated 30s ETIMEDOUT)
418
- if (_failedRefCache.has(depBranch)) {
419
- log('warn', `Skipping dependency ${depBranch} already failed to fetch this tick`);
431
+ // Fetch all dependency branches in parallel (git fetches are independent)
432
+ const fetchable = depBranches.filter(d => !_failedRefCache.has(d.branch));
433
+ const unfetchable = depBranches.filter(d => _failedRefCache.has(d.branch));
434
+ for (const { branch: depBranch } of unfetchable) {
435
+ log('warn', `Skipping dependency ${depBranch} — already failed to fetch this tick`);
436
+ depMergeFailed = true;
437
+ }
438
+ const fetchResults = await Promise.allSettled(
439
+ fetchable.map(({ branch: depBranch }) =>
440
+ execAsync(`git fetch origin "${depBranch}"`, { ..._gitOpts, cwd: rootDir }).then(() => depBranch)
441
+ )
442
+ );
443
+ for (const r of fetchResults) {
444
+ if (r.status === 'rejected') {
445
+ const failedBranch = fetchable.find(d => r.reason?.message?.includes(d.branch))?.branch || 'unknown';
446
+ _failedRefCache.add(failedBranch);
447
+ log('warn', `Failed to fetch dependency ${failedBranch}: ${r.reason?.message}`);
420
448
  depMergeFailed = true;
421
- continue;
422
449
  }
423
- try {
424
- await execAsync(`git fetch origin "${depBranch}"`, { ..._gitOpts, cwd: rootDir });
425
- await execAsync(`git merge "origin/${depBranch}" --no-edit`, { ..._gitOpts, cwd: worktreePath });
426
- log('info', `Merged dependency branch ${depBranch} (${prId}) into worktree ${branchName}`);
427
- } catch (mergeErr) {
428
- _failedRefCache.add(depBranch);
429
- log('warn', `Failed to merge dependency ${depBranch} into ${branchName}: ${mergeErr.message}`);
430
- depMergeFailed = true;
450
+ }
451
+ // Merge fetched branches sequentially (merges modify the worktree)
452
+ if (!depMergeFailed) {
453
+ for (const { branch: depBranch, prId } of fetchable) {
454
+ try {
455
+ await execAsync(`git merge "origin/${depBranch}" --no-edit`, { ..._gitOpts, cwd: worktreePath });
456
+ log('info', `Merged dependency branch ${depBranch} (${prId}) into worktree ${branchName}`);
457
+ } catch (mergeErr) {
458
+ log('warn', `Failed to merge dependency ${depBranch} into ${branchName}: ${mergeErr.message}`);
459
+ depMergeFailed = true;
460
+ break;
461
+ }
431
462
  }
432
463
  }
433
464
  if (depMergeFailed) {
@@ -441,30 +472,11 @@ async function spawnAgent(dispatchItem, config) {
441
472
  }
442
473
  }
443
474
 
444
- // Build lean system prompt (identity + rules, ~2-4KB) and bulk context (history, notes, skills)
445
- const systemPrompt = buildSystemPrompt(agentId, config, project);
446
- const agentContext = buildAgentContext(agentId, config, project);
447
-
448
475
  // Safety check: warn if a write-capable task is running in the main repo without a worktree
449
476
  if (cwd === rootDir && ['implement', 'implement:large', 'fix', 'test', 'verify', 'plan-to-prd'].includes(type)) {
450
477
  log('warn', `Agent ${agentId} running ${type} task in main repo (no worktree) for ${id} — changes may land on master directly`);
451
478
  }
452
479
 
453
- // Prepend bulk context to task prompt — keeps system prompt small and stable
454
- const fullTaskPrompt = agentContext
455
- ? `## Agent Context\n\n${agentContext}\n---\n\n## Your Task\n\n${taskPrompt}`
456
- : taskPrompt;
457
-
458
- // Write prompt and system prompt to temp files (avoids shell escaping issues)
459
- const tmpDir = path.join(ENGINE_DIR, 'tmp');
460
- if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
461
- const safeId = id.replace(/[:\\/*?"<>|]/g, '-');
462
- const promptPath = path.join(tmpDir, `prompt-${safeId}.md`);
463
- safeWrite(promptPath, fullTaskPrompt);
464
-
465
- const sysPromptPath = path.join(tmpDir, `sysprompt-${safeId}.md`);
466
- safeWrite(sysPromptPath, systemPrompt);
467
-
468
480
  // Build claude CLI args
469
481
  const args = [
470
482
  '--output-format', claudeConfig.outputFormat || 'stream-json',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.575",
3
+ "version": "0.1.577",
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"