@yemi33/minions 0.1.1671 → 0.1.1673

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/.claudeignore ADDED
@@ -0,0 +1,5 @@
1
+ agents/
2
+ engine/tmp/
3
+ engine/contexts/
4
+ prd/*.backup
5
+ plans/archive/
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1673 (2026-05-02)
4
+
5
+ ### Fixes
6
+ - cap Signed Off By column width and wrap multi-reviewer overflow
7
+
8
+ ## 0.1.1672 (2026-05-02)
9
+
10
+ ### Features
11
+ - fleet model validation + Copilot alias map + simplify pass
12
+
3
13
  ## 0.1.1671 (2026-05-02)
4
14
 
5
15
  ### Features
@@ -34,7 +34,7 @@ function prRow(pr) {
34
34
  '<td><span class="pr-agent">' + escapeHtml(pr.agent || '—') + '</span></td>' +
35
35
  '<td><span class="' + branchClass + '"' + (branchError ? ' title="' + escapeHtml(branchError) + '"' : '') + '>' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
36
36
  '<td><span class="pr-badge ' + reviewClass + '"' + (reviewTitle ? ' title="' + escapeHtml(reviewTitle) + '"' : '') + '>' + escapeHtml(reviewLabel) + '</span></td>' +
37
- '<td>' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escapeHtml(sq.note || '') + '">' + escapeHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>' : pr.reviewedBy && pr.reviewedBy.length ? '<span class="pr-agent">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
37
+ '<td class="pr-col-signoff">' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escapeHtml(sq.note || '') + '">' + escapeHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>' : pr.reviewedBy && pr.reviewedBy.length ? '<span class="pr-agent">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
38
38
  '<td><span class="pr-badge ' + buildClass + '"' + (buildTitle ? ' title="' + escapeHtml(buildTitle) + '"' : '') + '>' + escapeHtml(buildLabel) + '</span></td>' +
39
39
  '<td><span class="pr-badge ' + statusClass + '">' + escapeHtml(statusLabel) + '</span></td>' +
40
40
  '<td><span class="pr-date">' + escapeHtml((pr.created || '—').slice(0, 16).replace('T', ' ')) + '</span></td>' +
@@ -44,7 +44,7 @@ function prRow(pr) {
44
44
 
45
45
  function prTableHtml(rows) {
46
46
  return '<div class="pr-table-wrap"><table class="pr-table"><thead><tr>' +
47
- '<th>PR</th><th>Title</th><th>Agent</th><th>Branch</th><th>Review</th><th>Signed Off By</th><th>Build</th><th>Status</th><th>Created</th><th></th>' +
47
+ '<th>PR</th><th>Title</th><th>Agent</th><th>Branch</th><th>Review</th><th class="pr-col-signoff">Signed Off By</th><th>Build</th><th>Status</th><th>Created</th><th></th>' +
48
48
  '</tr></thead><tbody>' + rows + '</tbody></table></div>';
49
49
  }
50
50
 
@@ -213,7 +213,6 @@
213
213
  .prd-item-row.st-in-progress { border-left-color: var(--yellow); animation: prdWipPulse 2s infinite; }
214
214
  @keyframes prdWipPulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(210,153,34,0); } 50% { box-shadow: 0 0 0 4px rgba(210,153,34,0.2); } }
215
215
  .prd-item-row.st-failed { border-left-color: var(--red); }
216
- .prd-item-row.st-needs-human-review { border-left-color: var(--orange); }
217
216
  .prd-item-row.st-updated { border-left-color: var(--purple); }
218
217
  .prd-item-row.st-paused { border-left-color: var(--muted); opacity: 0.5; }
219
218
  .prd-item-id { font-family: Consolas, monospace; color: var(--muted); min-width: 36px; font-size: 0.9em; }
@@ -252,6 +251,7 @@
252
251
  .pr-table-wrap { overflow-x: auto; }
253
252
  .pr-table { width: 100%; border-collapse: collapse; font-size: var(--text-md); table-layout: auto; }
254
253
  .pr-table th:last-child, .pr-table td:last-child { width: 36px; min-width: 36px; text-align: center; }
254
+ .pr-table th.pr-col-signoff, .pr-table td.pr-col-signoff { width: 140px; max-width: 140px; white-space: normal; word-break: break-word; }
255
255
  .pr-table th { text-align: left; color: var(--muted); font-weight: 500; font-size: var(--text-base); text-transform: uppercase; letter-spacing: 0.5px; padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border); }
256
256
  .pr-table td { padding: var(--space-5); border-bottom: 1px solid var(--border); vertical-align: middle; white-space: nowrap; }
257
257
  .pr-table tr:last-child td { border-bottom: none; }
@@ -268,7 +268,6 @@
268
268
  .pr-badge.active { background: rgba(88,166,255,0.15); color: var(--blue); border: 1px solid var(--blue); }
269
269
  .pr-badge.approved { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
270
270
  .pr-badge.rejected { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
271
- .pr-badge.needs-review { background: rgba(227,179,65,0.15); color: var(--orange); border: 1px solid var(--orange); }
272
271
  .pr-badge.review-escalated { background: rgba(227,179,65,0.22); color: var(--orange); border: 1px dashed var(--orange); font-weight: 700; }
273
272
  .pr-badge.merged { background: rgba(188,140,255,0.15); color: var(--purple); border: 1px solid var(--purple); }
274
273
  .pr-badge.building { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
package/dashboard.js CHANGED
@@ -3357,7 +3357,7 @@ const server = http.createServer(async (req, res) => {
3357
3357
  async function _runKbSweepBackground(body, sweepToken) {
3358
3358
  try {
3359
3359
  const { runKbSweep } = require('./engine/kb-sweep');
3360
- const result = await runKbSweep({ pinnedKeys: body.pinnedKeys });
3360
+ const result = await runKbSweep({ pinnedKeys: body.pinnedKeys, engineConfig: CONFIG.engine });
3361
3361
  global._kbSweepLastResult = result;
3362
3362
  global._kbSweepLastCompletedAt = Date.now();
3363
3363
  } catch (e) {
@@ -5286,7 +5286,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5286
5286
 
5287
5287
  const prompt = `Convert this schedule description to a 3-field cron expression (minute hour dayOfWeek, where dayOfWeek is 0=Sun..6=Sat or ranges like 1-5). Return JSON only: {"cron": "...", "description": "..."}. Input: ${text.trim()}`;
5288
5288
  try {
5289
- const result = await llm.callLLM(prompt, '', { model: 'haiku', maxTurns: 1, timeout: 30000, label: 'schedule-parse', direct: true });
5289
+ const result = await llm.callLLM(prompt, '', {
5290
+ model: 'haiku', maxTurns: 1, timeout: 30000, label: 'schedule-parse', direct: true,
5291
+ engineConfig: CONFIG.engine,
5292
+ });
5290
5293
  const parsed = JSON.parse(result.text.trim());
5291
5294
  if (!parsed.cron) return jsonReply(res, 422, { error: 'Could not parse schedule' });
5292
5295
  return jsonReply(res, 200, { cron: parsed.cron, description: parsed.description || '' });
@@ -5379,6 +5382,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5379
5382
  if (!config.agents) config.agents = {};
5380
5383
 
5381
5384
  const _clamped = [];
5385
+ const _engineModelDiscovery = require('./engine/model-discovery');
5386
+ const _engineRuntimes = require('./engine/runtimes');
5387
+ function _resolveModelForRuntime(modelStr, runtimeName) {
5388
+ try {
5389
+ const adapter = _engineRuntimes.resolveRuntime(runtimeName);
5390
+ if (adapter && typeof adapter.resolveModel === 'function') {
5391
+ return adapter.resolveModel(modelStr) || modelStr;
5392
+ }
5393
+ } catch { /* unknown/free-text runtime */ }
5394
+ return modelStr;
5395
+ }
5382
5396
  if (body.engine) {
5383
5397
  const e = body.engine;
5384
5398
  const D = shared.ENGINE_DEFAULTS;
@@ -5440,10 +5454,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5440
5454
  // (where gpt-5.5 doesn't actually exist) cascaded into every agent
5441
5455
  // that didn't pin its own model. Reject when the model is known to
5442
5456
  // belong to a different runtime than the one it'll spawn against.
5443
- const _engineModelDiscovery = require('./engine/model-discovery');
5444
- const _engineRuntimes = require('./engine/runtimes');
5445
5457
  async function _validateFleetModel(modelStr, resolvedRuntime) {
5446
5458
  if (!modelStr) return null;
5459
+ const runtimeModelStr = _resolveModelForRuntime(modelStr, resolvedRuntime);
5447
5460
  let knownForResolved = null;
5448
5461
  try {
5449
5462
  const list = await _engineModelDiscovery.getRuntimeModels(resolvedRuntime, { config });
@@ -5451,11 +5464,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5451
5464
  knownForResolved = new Set(list.models.map(m => m.id || m.name).filter(Boolean));
5452
5465
  }
5453
5466
  } catch { /* unknown runtime */ }
5454
- if (knownForResolved && !knownForResolved.has(modelStr)) {
5467
+ if (knownForResolved && !knownForResolved.has(runtimeModelStr)) {
5455
5468
  return `not a valid model for runtime "${resolvedRuntime}" (known: ${[...knownForResolved].slice(0, 4).join(', ')}${knownForResolved.size > 4 ? '…' : ''})`;
5456
5469
  }
5457
5470
  if (!knownForResolved) {
5458
- // Free-text runtime (Claude). Reject only if model belongs to a different runtime's published list.
5471
+ // Free-text runtime (Claude). Reject only if the raw model belongs to a different runtime's published list.
5459
5472
  for (const rt of _engineRuntimes.listRuntimes()) {
5460
5473
  if (rt === resolvedRuntime) continue;
5461
5474
  try {
@@ -5548,13 +5561,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5548
5561
  // claude+gpt-* / copilot+claude-* combinations before they crash a
5549
5562
  // dispatch (see #model-validation: a stray engine.defaultModel='gpt-5.5'
5550
5563
  // pinned every Claude agent into a 404 spawn loop).
5551
- const _modelDiscovery = require('./engine/model-discovery');
5552
5564
  const _runtimeModelsCache = new Map(); // runtimeName → Set<modelId> (or null when unknown / Claude)
5553
5565
  async function _modelsFor(runtimeName) {
5554
5566
  if (_runtimeModelsCache.has(runtimeName)) return _runtimeModelsCache.get(runtimeName);
5555
5567
  let set = null;
5556
5568
  try {
5557
- const list = await _modelDiscovery.getRuntimeModels(runtimeName, { config });
5569
+ const list = await _engineModelDiscovery.getRuntimeModels(runtimeName, { config });
5558
5570
  if (Array.isArray(list?.models) && list.models.length > 0) {
5559
5571
  set = new Set(list.models.map(m => m.id || m.name).filter(Boolean));
5560
5572
  }
@@ -5562,11 +5574,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5562
5574
  _runtimeModelsCache.set(runtimeName, set);
5563
5575
  return set;
5564
5576
  }
5565
- // Returns the runtime that "owns" this model, or null if no other
5577
+ // Returns the runtime that "owns" this raw model, or null if no other
5566
5578
  // runtime claims it. Catches "claude + gpt-5.5" by spotting that
5567
5579
  // gpt-5.5 belongs to copilot's list.
5568
5580
  async function _ownerOfModel(modelId) {
5569
- for (const rt of require('./engine/runtimes').listRuntimes()) {
5581
+ for (const rt of _engineRuntimes.listRuntimes()) {
5570
5582
  const set = await _modelsFor(rt);
5571
5583
  if (set && set.has(modelId)) return rt;
5572
5584
  }
@@ -5594,6 +5606,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5594
5606
  else {
5595
5607
  const candidate = String(updates.model);
5596
5608
  const resolvedCli = config.agents[id].cli || config.engine.defaultCli || 'claude';
5609
+ const runtimeModelStr = _resolveModelForRuntime(candidate, resolvedCli);
5597
5610
  const knownModels = await _modelsFor(resolvedCli);
5598
5611
  // Two validation paths:
5599
5612
  // 1. If the runtime publishes a model list, enforce membership.
@@ -5601,7 +5614,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5601
5614
  // model belongs to a DIFFERENT runtime's list — that's how
5602
5615
  // we catch claude+gpt-5.5 (gpt-5.5 is in Copilot's list).
5603
5616
  let rejection = null;
5604
- if (knownModels && !knownModels.has(candidate)) {
5617
+ if (knownModels && !knownModels.has(runtimeModelStr)) {
5605
5618
  rejection = `not a valid model for runtime "${resolvedCli}" (known: ${[...knownModels].slice(0, 4).join(', ')}${knownModels.size > 4 ? '…' : ''})`;
5606
5619
  } else if (!knownModels) {
5607
5620
  const owner = await _ownerOfModel(candidate);
@@ -20,7 +20,7 @@
20
20
  | `capabilities.systemPromptFile` | **`false`** | No `--system-prompt-file` flag exists. Inject system prompt via a `<system>` block prepended to stdin. |
21
21
  | `capabilities.effortLevels` | **`true`** | `--effort` accepts `low|medium|high|xhigh` (no `max`). Adapter must map `'max' → 'xhigh'`. |
22
22
  | `capabilities.costTracking` | **`false`** | `result.usage` contains `premiumRequests` (count, not USD), no token counts, no cost. |
23
- | `capabilities.modelShorthands` | **`false`** | Models are full IDs (`claude-sonnet-4.5`, `gpt-5.4`). No `sonnet`/`opus`/`haiku` aliasing on the Copilot side. |
23
+ | `capabilities.modelShorthands` | **`false`** | The Copilot CLI requires full model IDs (`claude-sonnet-4.5`, `gpt-5.4`). Minions may accept internal aliases (`haiku`, `sonnet`, `opus`), but the adapter translates them to Copilot model IDs before invoking the CLI. |
24
24
  | `capabilities.budgetCap` | **`false`** | No `--max-budget-usd` flag. |
25
25
  | `capabilities.bareMode` | **`false`** | No `--bare`. Closest equivalent is `--no-custom-instructions` (suppresses AGENTS.md only, not all auto-discovery). |
26
26
  | `capabilities.fallbackModel` | **`false`** | No `--fallback-model` flag. |
@@ -594,9 +594,11 @@ When implementing `engine/runtimes/copilot.js`:
594
594
  **Never** emit `--verbose`.
595
595
  4. `buildPrompt()` injects `<system>...</system>\n\n` block when sysprompt is
596
596
  non-empty; passthrough otherwise (§2).
597
- 5. `resolveModel()` is verbatim passthrough; emit a one-time `console.warn`
598
- when input is `'sonnet' | 'opus' | 'haiku'` (Claude shorthand the user
599
- probably meant to set on the Claude adapter).
597
+ 5. `resolveModel()` translates Minions internal aliases before the CLI boundary:
598
+ `'haiku'` `'claude-haiku-4.5'`, `'sonnet'` `'claude-sonnet-4.5'`, and
599
+ `'opus'` `'claude-opus-4.5'`. All other model IDs pass through unchanged.
600
+ Keep `capabilities.modelShorthands` false because aliases are never passed
601
+ to the Copilot CLI.
600
602
  6. `_mapEffort()` private helper does `'max' → 'xhigh'`; pass through otherwise.
601
603
  7. `parseOutput(raw)` produces:
602
604
  - `text`: concatenation of all `assistant.message.data.content` (multi-turn
package/engine/cleanup.js CHANGED
@@ -39,6 +39,30 @@ function worktreeDirMatchesBranch(dirLower, branch) {
39
39
  return dirLower === branchSlug || dirLower.includes(branchSlug + '-') || dirLower.endsWith('-' + branchSlug);
40
40
  }
41
41
 
42
+ let _orphanPidProcessNamesCache = null;
43
+ function _orphanPidProcessNames() {
44
+ if (_orphanPidProcessNamesCache) return _orphanPidProcessNamesCache;
45
+ const names = new Set(['node']);
46
+ try {
47
+ for (const name of require('./runtimes').listRuntimes()) names.add(String(name).toLowerCase());
48
+ // Copilot can run through the GitHub CLI fallback (`gh copilot`), so allow
49
+ // gh only when the copilot runtime is registered.
50
+ if (names.has('copilot')) names.add('gh');
51
+ } catch {
52
+ names.add('claude');
53
+ }
54
+ _orphanPidProcessNamesCache = names;
55
+ return names;
56
+ }
57
+
58
+ function _processNameAllowedForOrphanKill(processText) {
59
+ const firstLine = String(processText || '').trim().split(/\r?\n/).find(Boolean) || '';
60
+ const imageName = path.basename(firstLine.trim().split(/\s+/)[0] || '').toLowerCase().replace(/\.exe$/, '');
61
+ if (!imageName) return false;
62
+ return _orphanPidProcessNames().has(imageName);
63
+ }
64
+
65
+
42
66
  /**
43
67
  * Kill orphaned processes whose dispatch ID appears in the worktree dir name.
44
68
  * Only kills processes NOT in the active dispatch queue — never kills live agents.
@@ -68,18 +92,18 @@ function _killProcessInWorktree(dir, activeProcesses, activeIds) {
68
92
  if (isActive) continue; // still active — do not kill
69
93
  const pid = parseInt(fs.readFileSync(path.join(tmpDir, f), 'utf8').trim(), 10);
70
94
  if (pid > 0) {
71
- // Verify the process is actually a node/claude process before killing
95
+ // Verify the PID still belongs to a Minions runtime process before killing
72
96
  try {
73
97
  if (process.platform === 'win32') {
74
98
  const taskInfo = exec(`tasklist /FI "PID eq ${pid}" /NH`, { encoding: 'utf8', timeout: 3000, windowsHide: true });
75
99
  const taskLower = taskInfo.toLowerCase();
76
- if (!taskLower.includes('node') && !taskLower.includes('claude')) continue;
100
+ if (!_processNameAllowedForOrphanKill(taskLower)) continue;
77
101
  exec(`taskkill /F /T /PID ${pid}`, { stdio: 'pipe', timeout: 5000, windowsHide: true });
78
102
  } else {
79
- // Verify it's a node process before killing (prevent recycled PID kill)
103
+ // Verify the process name before killing (prevent recycled PID kill)
80
104
  try {
81
105
  const psOut = exec(`ps -p ${pid} -o comm=`, { encoding: 'utf8', timeout: 3000 }).trim();
82
- if (!psOut.includes('node') && !psOut.includes('claude')) continue;
106
+ if (!_processNameAllowedForOrphanKill(psOut)) continue;
83
107
  } catch { continue; } // process dead or ps failed
84
108
  try { process.kill(-pid, 'SIGKILL'); } catch { process.kill(pid, 'SIGKILL'); }
85
109
  }
@@ -331,7 +355,7 @@ function runCleanup(config, verbose = false) {
331
355
  } // end worktreeRoots loop
332
356
  }
333
357
 
334
- // 4. Kill zombie claude processes not tracked by the engine
358
+ // 4. Kill zombie agent processes not tracked by the engine
335
359
  // List all node processes, check if any are running spawn-agent.js for our minions
336
360
  try {
337
361
  const dispatch = getDispatch();
package/engine/cli.js CHANGED
@@ -157,8 +157,8 @@ function _modelLooksIncompatible(runtime, model) {
157
157
  return true; // gpt-*, o3-*, codex, etc. — wrong CLI for these
158
158
  }
159
159
  if (runtime === 'copilot') {
160
- // Copilot accepts the full catalog by ID; only Claude shorthands are wrong.
161
- return m === 'sonnet' || m === 'opus' || m === 'haiku';
160
+ // Copilot adapter maps Minions' family aliases before spawning.
161
+ return false;
162
162
  }
163
163
  return false;
164
164
  }
@@ -8,11 +8,11 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const crypto = require('crypto');
10
10
  const shared = require('./shared');
11
- const { safeRead, safeWrite, safeUnlink, runFile, cleanChildEnv,
12
- parseStreamJsonOutput, classifyInboxItem, KB_CATEGORIES, log, ts, dateStamp } = shared;
13
- const { trackEngineUsage } = require('./llm');
11
+ const { safeRead, safeWrite,
12
+ classifyInboxItem, KB_CATEGORIES, log, ts, dateStamp } = shared;
13
+ const { callLLM, trackEngineUsage } = require('./llm');
14
14
  const queries = require('./queries');
15
- const { getInboxFiles, getNotes, INBOX_DIR, ENGINE_DIR, MINIONS_DIR,
15
+ const { getInboxFiles, getNotes, INBOX_DIR, ENGINE_DIR,
16
16
  NOTES_PATH, KNOWLEDGE_DIR, ARCHIVE_DIR } = queries;
17
17
 
18
18
  // Track in-flight LLM consolidation to prevent concurrent runs
@@ -150,51 +150,18 @@ function consolidateWithLLM(items, existingNotes, files, config) {
150
150
  });
151
151
 
152
152
  const prompt = buildConsolidationPrompt(items, existingNotes, kbPaths);
153
-
154
- const tmpDir = path.join(ENGINE_DIR, 'tmp');
155
- if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
156
- const promptPath = path.join(tmpDir, 'consolidate-prompt.md');
157
- safeWrite(promptPath, prompt);
158
-
159
153
  const sysPrompt = 'You are a concise knowledge manager. Output only markdown. No preamble. No code fences around your output.';
160
- const sysPromptPath = path.join(tmpDir, 'consolidate-sysprompt.md');
161
- safeWrite(sysPromptPath, sysPrompt);
162
-
163
- const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
164
- const args = [
165
- '--output-format', 'stream-json',
166
- '--max-turns', '1',
167
- '--model', 'haiku',
168
- '--permission-mode', 'bypassPermissions',
169
- '--verbose',
170
- ];
171
-
172
- log('info', 'Spawning Haiku for LLM consolidation...');
173
-
174
- const proc = runFile(process.execPath, [spawnScript, promptPath, sysPromptPath, ...args], {
175
- cwd: MINIONS_DIR,
176
- stdio: ['pipe', 'pipe', 'pipe'],
177
- env: cleanChildEnv()
178
- });
179
154
 
180
- let stdout = '';
181
- let stderr = '';
182
- proc.stdout.on('data', d => { stdout += d.toString(); if (stdout.length > 100000) stdout = stdout.slice(-50000); });
183
- proc.stderr.on('data', d => { stderr += d.toString(); if (stderr.length > 50000) stderr = stderr.slice(-25000); });
155
+ log('info', 'Starting LLM consolidation...');
184
156
 
185
- const timeout = setTimeout(() => {
186
- log('warn', 'LLM consolidation timed out after 3m — killing and falling back to regex');
187
- shared.killGracefully(proc, 10000);
188
- _forceResetTimeout = setTimeout(() => {
189
- if (!_cleared) log('warn', 'Consolidation flag force-reset after SIGKILL');
190
- _clearProcessingState();
191
- }, 10000);
192
- }, 180000);
157
+ let _cleared = false; // idempotency guard — timeout and promise resolution can race
158
+ let timeoutHandle = null;
159
+ let fallbackDone = false;
193
160
 
194
- let _cleared = false; // idempotency guard — both 'error' and 'close' can fire for the same process
195
161
  function _clearProcessingState() {
196
162
  if (_cleared) return;
197
163
  _cleared = true;
164
+ clearTimeout(timeoutHandle);
198
165
  clearTimeout(_forceResetTimeout);
199
166
  _forceResetTimeout = null;
200
167
  for (const f of files) _processingFiles.delete(f);
@@ -202,17 +169,43 @@ function consolidateWithLLM(items, existingNotes, files, config) {
202
169
  _consolidationStartedAt = 0;
203
170
  }
204
171
 
205
- proc.on('close', (code) => {
206
- clearTimeout(timeout);
207
- try { safeUnlink(promptPath); } catch (err) { log('warn', `Temp file cleanup failed: ${promptPath} — ${err.message}`); }
208
- try { safeUnlink(sysPromptPath); } catch (err) { log('warn', `Temp file cleanup failed: ${sysPromptPath} — ${err.message}`); }
172
+ function _fallback(message, err) {
173
+ if (_cleared || fallbackDone) return;
174
+ fallbackDone = true;
175
+ if (message) log('warn', message);
176
+ if (err?.message) log('debug', `LLM error: ${err.message}`);
177
+ consolidateWithRegex(items, files);
178
+ }
209
179
 
210
- const parsed = parseStreamJsonOutput(stdout);
211
- const extractedText = parsed.text;
212
- trackEngineUsage('consolidation', parsed.usage);
180
+ const llmCall = callLLM(prompt, sysPrompt, {
181
+ timeout: 180000,
182
+ label: 'consolidation',
183
+ model: 'haiku',
184
+ maxTurns: 1,
185
+ direct: true,
186
+ engineConfig: config.engine,
187
+ });
213
188
 
214
- if (code === 0 && (extractedText || stdout).trim().length > 50) {
215
- let digest = (extractedText || stdout).trim();
189
+ timeoutHandle = setTimeout(() => {
190
+ if (_cleared) return;
191
+ log('warn', 'LLM consolidation timed out after 3m — aborting and falling back to regex');
192
+ try { if (llmCall && typeof llmCall.abort === 'function') llmCall.abort(); } catch { /* ignore */ }
193
+ _forceResetTimeout = setTimeout(() => {
194
+ if (!_cleared) log('warn', 'Consolidation flag force-reset after timeout');
195
+ _fallback();
196
+ _clearProcessingState();
197
+ }, 10000);
198
+ }, 180000);
199
+
200
+ llmCall.then((result) => {
201
+ if (_cleared) return;
202
+ clearTimeout(timeoutHandle);
203
+ trackEngineUsage('consolidation', result.usage);
204
+
205
+ const extractedText = result.text || '';
206
+ const rawText = result.raw || '';
207
+ if (result.code === 0 && (extractedText || rawText).trim().length > 50) {
208
+ let digest = (extractedText || rawText).trim();
216
209
  digest = digest.replace(/^\`\`\`\w*\n?/gm, '').replace(/\n?\`\`\`$/gm, '').trim();
217
210
 
218
211
  if (!digest.startsWith('### ')) {
@@ -220,9 +213,7 @@ function consolidateWithLLM(items, existingNotes, files, config) {
220
213
  if (sectionIdx >= 0) {
221
214
  digest = digest.slice(sectionIdx);
222
215
  } else {
223
- log('warn', 'LLM consolidation output missing expected format — falling back to regex');
224
- consolidateWithRegex(items, files);
225
- _clearProcessingState();
216
+ _fallback('LLM consolidation output missing expected format — falling back to regex');
226
217
  return;
227
218
  }
228
219
  }
@@ -257,21 +248,15 @@ function consolidateWithLLM(items, existingNotes, files, config) {
257
248
  });
258
249
  classifyToKnowledgeBase(items);
259
250
  archiveInboxFiles(files);
260
- log('info', `LLM consolidation complete: ${files.length} notes processed by Haiku`);
251
+ log('info', `LLM consolidation complete: ${files.length} notes processed`);
261
252
  } else {
262
- log('warn', `LLM consolidation failed (code=${code}) — falling back to regex`);
263
- if (stderr) log('debug', `LLM stderr: ${stderr.slice(0, 500)}`);
264
- consolidateWithRegex(items, files);
253
+ _fallback(`LLM consolidation failed (code=${result.code}) — falling back to regex`, result.stderr ? { message: result.stderr.slice(0, 500) } : null);
265
254
  }
266
- _clearProcessingState();
267
- });
268
-
269
- proc.on('error', (err) => {
270
- clearTimeout(timeout);
271
- log('warn', `LLM consolidation spawn error: ${err.message} — falling back to regex`);
272
- try { safeUnlink(promptPath); } catch (unlinkErr) { log('warn', `Temp file cleanup failed: ${promptPath} — ${unlinkErr.message}`); }
273
- try { safeUnlink(sysPromptPath); } catch (unlinkErr) { log('warn', `Temp file cleanup failed: ${sysPromptPath} — ${unlinkErr.message}`); }
274
- consolidateWithRegex(items, files);
255
+ }).catch((err) => {
256
+ if (_cleared) return;
257
+ clearTimeout(timeoutHandle);
258
+ _fallback(`LLM consolidation error: ${err.message} falling back to regex`, err);
259
+ }).finally(() => {
275
260
  _clearProcessingState();
276
261
  });
277
262
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-02T02:37:22.632Z"
4
+ "cachedAt": "2026-05-02T04:13:14.794Z"
5
5
  }
@@ -104,7 +104,7 @@ function _hashDedup(manifest, opts = {}) {
104
104
  }
105
105
 
106
106
  /** Batched LLM sweep — finds within-batch dupes, reclassifies, removes stale. */
107
- async function _llmBatchSweep(manifest, callLLM, trackEngineUsage) {
107
+ async function _llmBatchSweep(manifest, callLLM, trackEngineUsage, opts = {}) {
108
108
  const plan = { duplicates: [], reclassify: [], remove: [] };
109
109
  const batches = [];
110
110
  for (let i = 0; i < manifest.length; i += LLM_BATCH_SIZE) {
@@ -130,7 +130,10 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
130
130
 
131
131
  let result;
132
132
  try {
133
- result = await callLLM(prompt, 'Output only JSON.', { timeout: 120000, label: 'kb-sweep', model: 'haiku', maxTurns: 1, direct: true });
133
+ result = await callLLM(prompt, 'Output only JSON.', {
134
+ timeout: 120000, label: 'kb-sweep', model: 'haiku', maxTurns: 1, direct: true,
135
+ engineConfig: opts.engineConfig,
136
+ });
134
137
  trackEngineUsage('kb-sweep', result.usage);
135
138
  } catch (e) { log('warn', `[kb-sweep] batch ${b + 1} LLM error: ${e.message}`); continue; }
136
139
 
@@ -206,6 +209,7 @@ ${body}`;
206
209
  try {
207
210
  const result = await callLLM(REWRITE_PROMPT(c.entry, c.body), 'Output ONLY the template body.', {
208
211
  timeout: 120000, label: 'kb-rewrite', model: 'haiku', maxTurns: 1, direct: true,
212
+ engineConfig: opts.engineConfig,
209
213
  });
210
214
  trackEngineUsage('kb-sweep', result.usage);
211
215
  let newBody = (result.text || '').trim();
@@ -326,7 +330,7 @@ async function runKbSweep(opts = {}) {
326
330
  // 2. LLM batch sweep — within-batch dupes + reclassify + remove stale
327
331
  // Only runs against survivors, but we need indices that match the LIST sent to the LLM
328
332
  const llmManifest = afterHash;
329
- const plan = await _llmBatchSweep(llmManifest, callLLM, trackEngineUsage);
333
+ const plan = await _llmBatchSweep(llmManifest, callLLM, trackEngineUsage, opts);
330
334
  const llmActions = _applyLlmPlan(plan, llmManifest, opts);
331
335
  summary.llmDuplicatesArchived = llmActions.merged;
332
336
  summary.staleRemoved = llmActions.removed;
package/engine/llm.js CHANGED
@@ -403,6 +403,12 @@ function _resolveModelFor(callOpts) {
403
403
  return undefined;
404
404
  }
405
405
 
406
+ function _resolveModelForRuntime(runtime, callOpts) {
407
+ const selected = _resolveModelFor(callOpts || {});
408
+ if (!runtime || typeof runtime.resolveModel !== 'function') return selected;
409
+ return runtime.resolveModel(selected);
410
+ }
411
+
406
412
  function _resolveRuntimeFeatureOpts({
407
413
  stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries, engineConfig,
408
414
  } = {}) {
@@ -431,7 +437,7 @@ function callLLM(promptText, sysPromptText, opts = {}) {
431
437
  } = opts;
432
438
 
433
439
  const runtime = _resolveRuntimeFor({ cli: cliOverride, engineConfig });
434
- const model = _resolveModelFor({ model: modelOverride, engineConfig });
440
+ const model = _resolveModelForRuntime(runtime, { model: modelOverride, engineConfig });
435
441
  const runtimeFeatureOpts = _resolveRuntimeFeatureOpts({
436
442
  stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries, engineConfig,
437
443
  });
@@ -528,7 +534,7 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
528
534
  } = opts;
529
535
 
530
536
  const runtime = _resolveRuntimeFor({ cli: cliOverride, engineConfig });
531
- const model = _resolveModelFor({ model: modelOverride, engineConfig });
537
+ const model = _resolveModelForRuntime(runtime, { model: modelOverride, engineConfig });
532
538
  const runtimeFeatureOpts = _resolveRuntimeFeatureOpts({
533
539
  stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries, engineConfig,
534
540
  });
@@ -619,5 +625,6 @@ module.exports = {
619
625
  _resetBinCache,
620
626
  _resolveRuntimeFor,
621
627
  _resolveModelFor,
628
+ _resolveModelForRuntime,
622
629
  _resolveRuntimeFeatureOpts,
623
630
  };
@@ -486,6 +486,7 @@ async function executePlanStage(stage, stageState, run, config) {
486
486
  const fullPrompt = meetingContext + '\n\n---\n\n' + planPrompt;
487
487
  const result = await llm.callLLM(fullPrompt, '', {
488
488
  timeout: 120000, label: 'pipeline-plan', model: 'sonnet', maxTurns: 1,
489
+ engineConfig: config.engine,
489
490
  });
490
491
  if (result.text) {
491
492
  content = result.text;
@@ -15,10 +15,10 @@
15
15
  * buildPrompt() prepends a <system> block.
16
16
  * - costTracking: false — result.usage has premiumRequests count
17
17
  * and durations only; no USD or per-token.
18
- * - modelShorthands: false — full model IDs like "claude-sonnet-4.5",
19
- * "gpt-5.4". Bare "sonnet" / "opus" / "haiku"
20
- * is a Claude-ism log a one-time warning
21
- * when seen so the user notices the mistake.
18
+ * - modelShorthands: false — Copilot itself expects full model IDs like
19
+ * "claude-sonnet-4.5" or "gpt-5.4". The
20
+ * adapter translates Minions' internal family
21
+ * aliases before argv construction.
22
22
  * - modelDiscovery: true — GET https://api.githubcopilot.com/models
23
23
  * with `gh auth token` Bearer returns the
24
24
  * catalog (24 models on the test account).
@@ -152,26 +152,23 @@ function resolveBinary({ env = process.env } = {}) {
152
152
 
153
153
  // ── Model Resolution ────────────────────────────────────────────────────────
154
154
  //
155
- // Copilot models are full IDs (`claude-sonnet-4.5`, `gpt-5.4`, ...). The
156
- // adapter passes them through verbatim. When we see a Claude shorthand
157
- // ('sonnet', 'opus', 'haiku') we log ONCE a stronger signal than silently
158
- // passing it to Copilot, which would respond with an unknown-model error.
159
-
160
- const _CLAUDE_SHORTHANDS = new Set(['sonnet', 'opus', 'haiku']);
161
- let _shorthandWarningLogged = false;
162
-
163
- function _resetShorthandWarning() { _shorthandWarningLogged = false; }
155
+ // Copilot models are full IDs (`claude-sonnet-4.5`, `gpt-5.4`, ...). Minions
156
+ // still uses the family aliases `haiku` / `sonnet` / `opus` internally, so this
157
+ // adapter maps those aliases to Copilot model IDs while passing all other input
158
+ // through verbatim. The capability remains false: Copilot CLI does not accept
159
+ // these aliases directly.
160
+
161
+ const _MINIONS_MODEL_ALIASES = {
162
+ haiku: 'claude-haiku-4.5',
163
+ sonnet: 'claude-sonnet-4.5',
164
+ opus: 'claude-opus-4.5',
165
+ };
164
166
 
165
- function resolveModel(input, { logger = console } = {}) {
167
+ function resolveModel(input) {
166
168
  if (input == null || input === '') return undefined;
167
169
  const s = String(input);
168
- if (_CLAUDE_SHORTHANDS.has(s.toLowerCase()) && !_shorthandWarningLogged) {
169
- _shorthandWarningLogged = true;
170
- try {
171
- const warn = (logger && typeof logger.warn === 'function') ? logger.warn.bind(logger) : null;
172
- if (warn) warn(`[copilot] "${s}" is a Claude family shorthand; Copilot expects a full model id (e.g. claude-sonnet-4.5). Passing through verbatim — Copilot will likely reject it.`);
173
- } catch { /* logger may be unwired during tests */ }
174
- }
170
+ const mapped = _MINIONS_MODEL_ALIASES[s.toLowerCase()];
171
+ if (mapped) return mapped;
175
172
  return s;
176
173
  }
177
174
 
@@ -783,8 +780,7 @@ module.exports = {
783
780
  createStreamConsumer,
784
781
  // Exposed for unit tests — engine code MUST go through resolveRuntime + the
785
782
  // adapter contract; never reach into these helpers directly.
786
- _CLAUDE_SHORTHANDS,
787
- _resetShorthandWarning,
783
+ _MINIONS_MODEL_ALIASES,
788
784
  _mapEffort,
789
785
  _copilotAssistantMessageHasTools,
790
786
  KNOWN_EVENT_TYPES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1671",
3
+ "version": "0.1.1673",
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"