@yemi33/minions 0.1.1659 → 0.1.1661

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1661 (2026-05-01)
4
+
5
+ ### Other
6
+ - Harden Copilot steering resume flow
7
+
8
+ ## 0.1.1660 (2026-05-01)
9
+
10
+ ### Fixes
11
+ - less rigid agent orphan detection
12
+
3
13
  ## 0.1.1659 (2026-05-01)
4
14
 
5
15
  ### Other
package/engine/cli.js CHANGED
@@ -21,6 +21,17 @@ function engine() {
21
21
  let _dispatchModule = null;
22
22
  function dispatchModule() { if (!_dispatchModule) _dispatchModule = require('./dispatch'); return _dispatchModule; }
23
23
 
24
+ function normalizeSessionBranch(branch) {
25
+ if (!branch) return null;
26
+ return String(branch).replace(/^refs\/heads\//, '');
27
+ }
28
+
29
+ function dispatchSessionBranch(item) {
30
+ return normalizeSessionBranch(
31
+ item?.meta?.branch || item?.branch || item?.meta?.item?.branch || item?.meta?.item?.featureBranch
32
+ );
33
+ }
34
+
24
35
  function isEngineProcessAlive(control) {
25
36
  if (!control?.pid) return false;
26
37
  if (control.pid === process.pid) return true;
@@ -396,13 +407,21 @@ const commands = {
396
407
  }
397
408
 
398
409
  if (agentPid) {
399
- // Load sessionId from session.json for steering support
410
+ // Load sessionId from session.json for steering support, but only
411
+ // when its saved branch still matches the active dispatch branch.
400
412
  let sessionId = null;
401
413
  try {
402
414
  const sj = safeJson(path.join(AGENTS_DIR, agentId, 'session.json'));
403
- if (sj?.sessionId) sessionId = sj.sessionId;
415
+ const expectedBranch = dispatchSessionBranch(item);
416
+ const savedBranch = normalizeSessionBranch(sj?.branch);
417
+ if (sj?.sessionId && (!expectedBranch || savedBranch === expectedBranch)) {
418
+ sessionId = sj.sessionId;
419
+ } else if (sj?.sessionId && expectedBranch) {
420
+ shared.log('warn', `Reattach: ignoring session for ${agentId} on branch ${savedBranch || 'unknown'}; expected ${expectedBranch}`);
421
+ }
404
422
  } catch {}
405
- e.activeProcesses.set(item.id, { proc: { pid: agentPid > 0 ? agentPid : null }, agentId, startedAt: item.created_at, reattached: true, sessionId });
423
+ const runtimeName = item.runtimeName || item.runtime || item.meta?.runtimeName || item.meta?.runtime || null;
424
+ e.activeProcesses.set(item.id, { proc: { pid: agentPid > 0 ? agentPid : null }, agentId, startedAt: item.created_at, reattached: true, sessionId, runtimeName });
406
425
  // Sync work item status to dispatched — atomic write to avoid lifecycle lazy init issues
407
426
  if (item.meta?.item?.id && item.meta?.project?.localPath) {
408
427
  try {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-01T05:34:43.680Z"
4
+ "cachedAt": "2026-05-01T06:25:57.091Z"
5
5
  }
package/engine/llm.js CHANGED
@@ -163,7 +163,14 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
163
163
  maxBudget, bare, fallbackModel,
164
164
  stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries,
165
165
  };
166
- const finalPrompt = runtime.buildPrompt(promptText, sysPromptText);
166
+ // Capability-gate per-flag opts before prompt construction so adapters can
167
+ // make resume-aware prompt decisions from the same opts used for argv.
168
+ if (!caps.effortLevels) adapterOpts.effort = undefined;
169
+ if (!caps.sessionResume) adapterOpts.sessionId = undefined;
170
+ if (!caps.budgetCap) adapterOpts.maxBudget = undefined;
171
+ if (!caps.bareMode) adapterOpts.bare = undefined;
172
+ if (!caps.fallbackModel) adapterOpts.fallbackModel = undefined;
173
+ const finalPrompt = runtime.buildPrompt(promptText, sysPromptText, adapterOpts);
167
174
 
168
175
  // ── Direct path ──
169
176
  const resolved = direct ? _resolveBin(runtime) : null;
@@ -178,13 +185,6 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
178
185
  cleanupFiles.push(sysTmpPath);
179
186
  adapterOpts.sysPromptFile = sysTmpPath;
180
187
  }
181
- // Capability-gate per-flag opts so the Claude path keeps emitting its
182
- // historical flag set while Copilot only sees what it understands.
183
- if (!caps.effortLevels) adapterOpts.effort = undefined;
184
- if (!caps.sessionResume) adapterOpts.sessionId = undefined;
185
- if (!caps.budgetCap) adapterOpts.maxBudget = undefined;
186
- if (!caps.bareMode) adapterOpts.bare = undefined;
187
- if (!caps.fallbackModel) adapterOpts.fallbackModel = undefined;
188
188
  // promptViaArg=true: the adapter splices `--prompt <text>` into args itself.
189
189
  if (caps.promptViaArg) adapterOpts.prompt = finalPrompt;
190
190
 
@@ -581,6 +581,8 @@ const capabilities = {
581
581
  streaming: true,
582
582
  // `--resume <session-id>` resumes a previous turn
583
583
  sessionResume: true,
584
+ // Emits a resumable session ID before the terminal result event
585
+ midRunSessionId: true,
584
586
  // Accepts the system prompt via `--system-prompt-file`
585
587
  systemPromptFile: true,
586
588
  // Honours `--effort low|medium|high|xhigh`
@@ -344,8 +344,9 @@ function classifyFailure({ code, stdout = '', stderr = '', fallback } = {}) {
344
344
  // convention from Anthropic tool-use docs and is recognized as "system role"
345
345
  // content by every model in the Copilot catalog.
346
346
 
347
- function buildPrompt(promptText, sysPromptText) {
347
+ function buildPrompt(promptText, sysPromptText, opts = {}) {
348
348
  const user = promptText == null ? '' : String(promptText);
349
+ if (opts && opts.sessionId) return user;
349
350
  if (sysPromptText == null || sysPromptText === '') return user;
350
351
  return `<system>\n${String(sysPromptText)}\n</system>\n\n${user}`;
351
352
  }
@@ -722,6 +723,8 @@ const capabilities = {
722
723
  streaming: true,
723
724
  // --resume=<id> resumes a session
724
725
  sessionResume: true,
726
+ // Copilot only exposes sessionId on the terminal result event
727
+ midRunSessionId: false,
725
728
  // No --system-prompt-file flag — system prompt is merged into stdin
726
729
  systemPromptFile: false,
727
730
  // --effort low|medium|high|xhigh (no 'max' — adapter maps it)
package/engine/shared.js CHANGED
@@ -721,7 +721,19 @@ const ENGINE_DEFAULTS = {
721
721
  inboxConsolidateThreshold: 5,
722
722
  agentTimeout: 18000000, // 5h
723
723
  heartbeatTimeout: 300000, // 5min — stale-orphan grace after process tracking is lost
724
- heartbeatTimeouts: {}, // optional per-type stale-orphan overrides; merged at runtime (see timeout.js)
724
+ // Per-type stale-orphan overrides (merged with config.engine.heartbeatTimeouts at runtime see timeout.js).
725
+ // Heavy work types (multi-file edits, builds, test suites, full verify cycles) routinely go quiet for
726
+ // longer than the 5-min default when the engine has lost their tracked handle (e.g. across an engine
727
+ // restart). We give them headroom up to a typical build+tests cycle. Short-running types
728
+ // (decompose / meeting / etc.) keep the 5-min default by simply not appearing here.
729
+ heartbeatTimeouts: {
730
+ implement: 900000, // 15min — refactors, multi-file edits, builds
731
+ 'implement:large': 900000, // 15min — same class of work, larger scope
732
+ fix: 900000, // 15min — fix runs often include builds + retries
733
+ test: 900000, // 15min — build-and-test against existing PR
734
+ verify: 900000, // 15min — full project verification cycle
735
+ plan: 600000, // 10min — research-heavy
736
+ },
725
737
  maxTurns: 100,
726
738
  worktreeCreateTimeout: 300000, // 5min for git worktree add on large Windows repos
727
739
  worktreeCreateRetries: 1, // retry once on transient timeout/lock races
@@ -785,7 +797,6 @@ const ENGINE_DEFAULTS = {
785
797
  copilotReasoningSummaries: false, // Copilot --enable-reasoning-summaries (Anthropic-family models only)
786
798
  maxBudgetUsd: undefined, // fleet USD ceiling for --max-budget-usd (per-agent override: agents.<id>.maxBudgetUsd). Honors 0 via ?? so a literal cap of $0 works
787
799
  disableModelDiscovery: false, // skip runtime.listModels() REST calls fleet-wide (settings UI falls back to free-text)
788
- heartbeatTimeouts: {},
789
800
  maxPendingContexts: 20, // cap pendingContexts arrays in cooldowns.json to prevent unbounded growth
790
801
  maxPendingContextEntryBytes: 256 * 1024, // 256 KB — cap each pendingContexts entry to prevent huge PR comments from bloating cooldowns.json
791
802
  maxDispatchPromptBytes: 1024 * 1024, // 1 MB — dispatch items with prompts larger than this sidecar to engine/contexts/ to prevent dispatch.json OOM (#1167)
@@ -2083,27 +2094,60 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
2083
2094
 
2084
2095
  // ─── Cross-Platform Process Kill Helpers ─────────────────────────────────────
2085
2096
 
2097
+ function normalizeKillPid(proc) {
2098
+ const pid = Number(proc?.pid);
2099
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
2100
+ }
2101
+
2102
+ function unixChildPids(pid) {
2103
+ if (!Number.isInteger(pid) || pid <= 0) return [];
2104
+ try {
2105
+ return _execSync(`pgrep -P ${pid}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 })
2106
+ .split(/\r?\n/)
2107
+ .map(line => Number(line.trim()))
2108
+ .filter(childPid => Number.isInteger(childPid) && childPid > 0 && childPid !== pid);
2109
+ } catch {
2110
+ return [];
2111
+ }
2112
+ }
2113
+
2114
+ function killUnixProcessTree(pid, signal, seen = new Set()) {
2115
+ if (!Number.isInteger(pid) || pid <= 0 || seen.has(pid)) return;
2116
+ seen.add(pid);
2117
+ for (const childPid of unixChildPids(pid)) {
2118
+ killUnixProcessTree(childPid, signal, seen);
2119
+ }
2120
+ try { process.kill(pid, signal); } catch { /* process may be dead */ }
2121
+ }
2122
+
2123
+ function unrefTimer(timer) {
2124
+ if (timer && typeof timer.unref === 'function') timer.unref();
2125
+ return timer;
2126
+ }
2127
+
2086
2128
  function killGracefully(proc, graceMs = 5000) {
2087
- if (!proc || !proc.pid) return;
2129
+ const pid = normalizeKillPid(proc);
2130
+ if (!pid) return;
2088
2131
  if (process.platform === 'win32') {
2089
- try { _execSync(`taskkill /PID ${proc.pid} /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true }); } catch { /* process may be dead */ }
2090
- setTimeout(() => {
2091
- try { _execSync(`taskkill /PID ${proc.pid} /F /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true }); } catch { /* process may be dead */ }
2092
- }, graceMs);
2132
+ try { _execSync(`taskkill /PID ${pid} /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true }); } catch { /* process may be dead */ }
2133
+ unrefTimer(setTimeout(() => {
2134
+ try { _execSync(`taskkill /PID ${pid} /F /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true }); } catch { /* process may be dead */ }
2135
+ }, graceMs));
2093
2136
  } else {
2094
- try { proc.kill('SIGTERM'); } catch { /* process may be dead */ }
2095
- setTimeout(() => {
2096
- try { proc.kill('SIGKILL'); } catch { /* process may be dead */ }
2097
- }, graceMs);
2137
+ killUnixProcessTree(pid, 'SIGTERM');
2138
+ unrefTimer(setTimeout(() => {
2139
+ killUnixProcessTree(pid, 'SIGKILL');
2140
+ }, graceMs));
2098
2141
  }
2099
2142
  }
2100
2143
 
2101
2144
  function killImmediate(proc) {
2102
- if (!proc || !proc.pid) return;
2145
+ const pid = normalizeKillPid(proc);
2146
+ if (!pid) return;
2103
2147
  if (process.platform === 'win32') {
2104
- try { _execSync(`taskkill /PID ${proc.pid} /F /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true }); } catch { /* process may be dead */ }
2148
+ try { _execSync(`taskkill /PID ${pid} /F /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true }); } catch { /* process may be dead */ }
2105
2149
  } else {
2106
- try { proc.kill('SIGKILL'); } catch { /* process may be dead */ }
2150
+ killUnixProcessTree(pid, 'SIGKILL');
2107
2151
  }
2108
2152
  }
2109
2153
 
@@ -100,9 +100,9 @@ function parseSpawnArgs(argv) {
100
100
  * }
101
101
  */
102
102
  function buildSpawnInvocation({ runtime, resolved, promptText, sysPromptText, opts, passthrough, addDirs }) {
103
- const finalPrompt = runtime.buildPrompt(promptText, sysPromptText);
104
103
  const adapterOpts = { ...opts };
105
104
  if (Array.isArray(addDirs) && addDirs.length) adapterOpts.addDirs = addDirs;
105
+ const finalPrompt = runtime.buildPrompt(promptText, sysPromptText, adapterOpts);
106
106
  const deliveryMode = typeof runtime.getPromptDeliveryMode === 'function'
107
107
  ? runtime.getPromptDeliveryMode(adapterOpts)
108
108
  : (runtime.capabilities && runtime.capabilities.promptViaArg ? 'arg' : 'stdin');
package/engine/timeout.js CHANGED
@@ -9,7 +9,7 @@ const queries = require('./queries');
9
9
  const steering = require('./steering');
10
10
 
11
11
  const { safeRead, safeWrite, safeJson, mutateJsonFileLocked, getProjects, projectWorkItemsPath, log, ts,
12
- ENGINE_DEFAULTS, WI_STATUS, WORK_TYPE, DISPATCH_RESULT, AGENT_STATUS } = shared;
12
+ ENGINE_DEFAULTS, ENGINE_DIR, WI_STATUS, WORK_TYPE, DISPATCH_RESULT, AGENT_STATUS } = shared;
13
13
  const { getDispatch, getAgentStatus } = queries;
14
14
  const AGENTS_DIR = queries.AGENTS_DIR;
15
15
  const MINIONS_DIR = shared.MINIONS_DIR;
@@ -57,6 +57,27 @@ function checkIdleThreshold(config) {
57
57
  // How long to wait for a steered agent to exit before retrying the kill
58
58
  const STEERING_KILL_RETRY_MS = 30000;
59
59
 
60
+ function runtimeSupportsMidRunSessionId(info) {
61
+ if (typeof info?.midRunSessionId === 'boolean') return info.midRunSessionId;
62
+ if (typeof info?.runtime?.capabilities?.midRunSessionId === 'boolean') return info.runtime.capabilities.midRunSessionId;
63
+ if (info?.runtimeName) {
64
+ try {
65
+ const { resolveRuntime } = require('./runtimes');
66
+ const runtime = resolveRuntime(info.runtimeName);
67
+ if (typeof runtime.capabilities?.midRunSessionId === 'boolean') return runtime.capabilities.midRunSessionId;
68
+ } catch {
69
+ return true;
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+
75
+ function rememberDeferredSteering(info, steerEntry) {
76
+ const existing = new Set(Array.isArray(info._deferredSteeringFiles) ? info._deferredSteeringFiles : []);
77
+ if (steerEntry?.path) existing.add(steerEntry.path);
78
+ info._deferredSteeringFiles = Array.from(existing);
79
+ }
80
+
60
81
  function checkSteering(config) {
61
82
  const activeProcesses = engine().activeProcesses;
62
83
  for (const [id, info] of activeProcesses) {
@@ -79,7 +100,10 @@ function checkSteering(config) {
79
100
  // Skip if already being steered (prevents double-kill race)
80
101
  if (info._steeringMessage || info._steeringAt) continue;
81
102
 
82
- const alreadyPending = new Set((info._pendingSteeringFiles || []).map(entry => entry.path || entry));
103
+ const alreadyPending = new Set([
104
+ ...(info._pendingSteeringFiles || []).map(entry => entry.path || entry),
105
+ ...(info._deferredSteeringFiles || []),
106
+ ]);
83
107
  const unread = steering.listUnreadSteeringMessages(info.agentId);
84
108
  for (const empty of unread.filter(entry => !entry.message.trim())) {
85
109
  shared.safeUnlink(empty.path);
@@ -90,8 +114,19 @@ function checkSteering(config) {
90
114
 
91
115
  const sessionId = info.sessionId;
92
116
  if (!sessionId) {
93
- // No session to resume — kill agent and leave message unread in inbox for retry.
94
- // Previously this silently skipped for up to 5m then deleted the message (#627).
117
+ if (!runtimeSupportsMidRunSessionId(info)) {
118
+ log('info', `Steering: no mid-run sessionId for ${info.agentId} (${id}) queued until resumable checkpoint`);
119
+ rememberDeferredSteering(info, steerEntry);
120
+ try {
121
+ const liveLogPath = path.join(AGENTS_DIR, info.agentId, 'live-output.log');
122
+ fs.appendFileSync(liveLogPath, `\n[steering] Message received. This runtime has not emitted a resumable session yet, so the message is queued until the agent reaches a resumable checkpoint or the next dispatch.\n`);
123
+ } catch { /* optional */ }
124
+ continue;
125
+ }
126
+
127
+ // No session to resume for a runtime that should have emitted one — kill
128
+ // agent and leave message unread in inbox for retry. Previously this
129
+ // silently skipped for up to 5m then deleted the message (#627).
95
130
  log('info', `Steering: no sessionId for ${info.agentId} (${id}) — killing and keeping unread message in inbox`);
96
131
 
97
132
  // Append to live output so user sees confirmation in the dashboard
@@ -142,6 +177,23 @@ function isTrackedProcessAlive(procInfo) {
142
177
  }
143
178
  }
144
179
 
180
+ // Last-resort liveness check via the on-disk PID file (engine/tmp/pid-<safeId>.pid).
181
+ // Used by orphan detection to avoid false-positive kills when the engine has lost the
182
+ // tracked process handle (engine restart, never-tracked spawn, etc.) but the OS-level
183
+ // child process is still alive and healthy. The safeId here mirrors engine.js spawn
184
+ // (id.replace(/[:\\/*?"<>|]/g, '-')) — same pattern engine/cli.js uses to re-attach.
185
+ function isOsPidAliveForDispatch(itemId) {
186
+ const safeId = String(itemId || '').replace(/[:\\/*?"<>|]/g, '-');
187
+ const pidPath = path.join(ENGINE_DIR, 'tmp', `pid-${safeId}.pid`);
188
+ let raw;
189
+ try { raw = fs.readFileSync(pidPath, 'utf8'); }
190
+ catch { return false; }
191
+ const pid = parseInt(String(raw).trim(), 10);
192
+ if (!Number.isFinite(pid) || pid <= 0) return false;
193
+ try { process.kill(pid, 0); return true; }
194
+ catch { return false; }
195
+ }
196
+
145
197
  function checkTimeouts(config) {
146
198
  const activeProcesses = engine().activeProcesses;
147
199
  const engineRestartGraceUntil = engine().engineRestartGraceUntil;
@@ -335,6 +387,11 @@ function checkTimeouts(config) {
335
387
  } catch { /* ENOENT — keep default */ }
336
388
 
337
389
  if (!processAlive && silentMs > staleOrphanTimeout && (Date.now() > engineRestartGraceUntil || engineRestartGraceExempt?.has(item.id))) {
390
+ // Last-resort PID check: lost tracked handle but OS process may still be alive.
391
+ if (isOsPidAliveForDispatch(item.id)) {
392
+ log('info', `Orphan check: ${item.agent} (${item.id}) silent ${silentSec}s but OS PID is alive — keeping [${_logState}]`);
393
+ continue;
394
+ }
338
395
  // No tracked process AND no recent output past stale-orphan timeout AND (grace period expired OR confirmed-dead at restart) → orphaned
339
396
  log('warn', `Orphan detected: ${item.agent} (${item.id}) — no live process tracked, silent for ${silentSec}s [${_logState}]`);
340
397
  dispatch().updateAgentStatus(item.id, AGENT_STATUS.TIMED_OUT, `Orphaned — no process, silent for ${silentSec}s`);
@@ -424,4 +481,5 @@ module.exports = {
424
481
  checkTimeouts,
425
482
  checkSteering,
426
483
  checkIdleThreshold,
484
+ isOsPidAliveForDispatch,
427
485
  };
package/engine.js CHANGED
@@ -1043,6 +1043,16 @@ async function spawnAgent(dispatchItem, config) {
1043
1043
  fs.appendFileSync(liveOutputPath, `[${new Date().toISOString()}] pid: ${proc.pid ?? 'unknown'}\n`);
1044
1044
  } catch { /* log stamp is best-effort — don't block spawn on fs failure */ }
1045
1045
 
1046
+ const initialProcInfo = {
1047
+ proc,
1048
+ agentId,
1049
+ startedAt,
1050
+ runtimeName,
1051
+ sessionId: cachedSessionId,
1052
+ _pendingSteeringFiles: pendingSteering.entries,
1053
+ };
1054
+ activeProcesses.set(id, initialProcInfo);
1055
+
1046
1056
  const MAX_OUTPUT = 1024 * 1024; // 1MB
1047
1057
  let stdout = '';
1048
1058
  let stderr = '';
@@ -1204,6 +1214,7 @@ async function spawnAgent(dispatchItem, config) {
1204
1214
  proc: resumeProc,
1205
1215
  agentId,
1206
1216
  startedAt: procInfo.startedAt,
1217
+ runtimeName,
1207
1218
  sessionId: steerSessionId,
1208
1219
  lastRealOutputAt: Date.now(),
1209
1220
  _pendingSteeringFiles: mergePendingSteeringEntries(
@@ -1446,12 +1457,15 @@ async function spawnAgent(dispatchItem, config) {
1446
1457
  // realActivityMap was already seeded immediately after runFile() returned (#W-mo25loq8kjer);
1447
1458
  // don't re-seed here — the stdout/stderr handlers above can already have updated it with
1448
1459
  // a fresher timestamp, and overwriting would clobber the real "last activity" signal.
1460
+ const existingProcInfo = activeProcesses.get(id) || {};
1449
1461
  activeProcesses.set(id, {
1462
+ ...existingProcInfo,
1450
1463
  proc,
1451
1464
  agentId,
1452
1465
  startedAt,
1453
- sessionId: cachedSessionId,
1454
- _pendingSteeringFiles: pendingSteering.entries,
1466
+ runtimeName,
1467
+ sessionId: existingProcInfo.sessionId || cachedSessionId,
1468
+ _pendingSteeringFiles: mergePendingSteeringEntries(existingProcInfo._pendingSteeringFiles, pendingSteering.entries),
1455
1469
  });
1456
1470
 
1457
1471
  updateAgentStatus(id, AGENT_STATUS.RUNNING, `Process spawned for ${agentId}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1659",
3
+ "version": "0.1.1661",
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"