@yemi33/minions 0.1.1627 → 0.1.1629

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1629 (2026-04-29)
4
+
5
+ ### Other
6
+ - test(cli): add unit tests for runtime-flag helpers
7
+
3
8
  ## 0.1.1627 (2026-04-29)
4
9
 
5
10
  ### Fixes
package/engine/cli.js CHANGED
@@ -23,9 +23,9 @@ function dispatchModule() { if (!_dispatchModule) _dispatchModule = require('./d
23
23
 
24
24
  function handleCommand(cmd, args) {
25
25
  if (!cmd) {
26
- commands.start();
26
+ return commands.start();
27
27
  } else if (commands[cmd]) {
28
- commands[cmd](...args);
28
+ return commands[cmd](...args);
29
29
  } else {
30
30
  console.log(`Unknown command: ${cmd}`);
31
31
  console.log('Commands:');
@@ -50,7 +50,7 @@ function handleCommand(cmd, args) {
50
50
  }
51
51
  }
52
52
 
53
- // ─── Runtime fleet flags (--cli / --model) ───────────────────────────────────
53
+ // ─── Runtime fleet flags (--cli / --model / --effort) ────────────────────────
54
54
  //
55
55
  // Shared by `start`, `restart`, and `config set-cli`. Single source of truth
56
56
  // for: flag parsing, runtime validation, incompatibility heuristics, and the
@@ -58,27 +58,52 @@ function handleCommand(cmd, args) {
58
58
  // config.json" — the helper below is the only caller that mutates fleet keys.
59
59
 
60
60
  /**
61
- * Strip `--cli <name>` / `--model <value>` from `args` (in-place). Returns
62
- * `{ cli, model, modelExplicit, errors }`. `modelExplicit` distinguishes
61
+ * Strip `--cli <name>` / `--model <value>` / `--effort <level>` from `args`
62
+ * (in-place), including `--flag=value` forms. Returns
63
+ * `{ cli, model, effort, modelExplicit, errors }`. `modelExplicit` distinguishes
63
64
  * "user passed --model with empty string" (clear) from "no flag" (no-op).
64
65
  *
65
66
  * Errors (e.g. `--cli` with no follow-up token) are collected for the caller
66
67
  * to print + exit-non-zero, instead of throwing — matches existing CLI flow.
67
68
  */
68
69
  function _parseRuntimeFlags(args) {
69
- const out = { cli: undefined, model: undefined, modelExplicit: false, errors: [] };
70
+ const out = { cli: undefined, model: undefined, effort: undefined, modelExplicit: false, errors: [] };
70
71
  let i = 0;
71
72
  while (i < args.length) {
72
73
  const a = args[i];
73
- if (a === '--cli') {
74
- if (i + 1 >= args.length) { out.errors.push('--cli requires a value'); args.splice(i, 1); break; }
75
- out.cli = String(args[i + 1]);
74
+ const eq = typeof a === 'string' ? a.indexOf('=') : -1;
75
+ const flag = eq > 0 ? a.slice(0, eq) : a;
76
+ const inlineValue = eq > 0 ? a.slice(eq + 1) : undefined;
77
+ const readValue = (name, { allowEmpty = false, hint = '' } = {}) => {
78
+ if (inlineValue !== undefined) {
79
+ if (!allowEmpty && inlineValue === '') {
80
+ out.errors.push(`${name} requires a value${hint}`);
81
+ args.splice(i, 1);
82
+ return { ok: false };
83
+ }
84
+ args.splice(i, 1);
85
+ return { ok: true, value: inlineValue };
86
+ }
87
+ if (i + 1 >= args.length || String(args[i + 1]).startsWith('--')) {
88
+ out.errors.push(`${name} requires a value${hint}`);
89
+ args.splice(i, 1);
90
+ return { ok: false };
91
+ }
92
+ const value = String(args[i + 1]);
76
93
  args.splice(i, 2);
77
- } else if (a === '--model') {
78
- if (i + 1 >= args.length) { out.errors.push('--model requires a value (use --model "" to clear)'); args.splice(i, 1); break; }
79
- out.model = String(args[i + 1]);
94
+ return { ok: true, value };
95
+ };
96
+ if (flag === '--cli') {
97
+ const parsed = readValue('--cli');
98
+ if (parsed.ok) out.cli = parsed.value;
99
+ } else if (flag === '--model') {
100
+ const parsed = readValue('--model', { allowEmpty: true, hint: ' (use --model "" to clear)' });
101
+ if (!parsed.ok) continue;
102
+ out.model = parsed.value;
80
103
  out.modelExplicit = true;
81
- args.splice(i, 2);
104
+ } else if (flag === '--effort') {
105
+ const parsed = readValue('--effort');
106
+ if (parsed.ok) out.effort = parsed.value;
82
107
  } else {
83
108
  i++;
84
109
  }
@@ -1222,7 +1247,7 @@ const commands = {
1222
1247
 
1223
1248
  doctor() {
1224
1249
  const { doctor } = require('./preflight');
1225
- doctor(MINIONS_DIR).then(ok => {
1250
+ return doctor(MINIONS_DIR).then(ok => {
1226
1251
  if (!ok) process.exit(1);
1227
1252
  });
1228
1253
  },
@@ -1298,5 +1323,10 @@ const commands = {
1298
1323
  }
1299
1324
  };
1300
1325
 
1301
- module.exports = { handleCommand };
1302
-
1326
+ module.exports = {
1327
+ handleCommand,
1328
+ // exported for testing
1329
+ _parseRuntimeFlags,
1330
+ _modelLooksIncompatible,
1331
+ _applyRuntimeFlags,
1332
+ };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-04-29T18:05:40.260Z"
4
+ "cachedAt": "2026-04-29T18:09:36.782Z"
5
5
  }
@@ -12,7 +12,7 @@ const { setCooldownFailure } = require('./cooldown');
12
12
  const { safeJson, safeWrite, safeReadDir, mutateJsonFileLocked, mutateWorkItems,
13
13
  mutatePullRequests, getProjects, projectWorkItemsPath, projectPrPath, log, ts, dateStamp,
14
14
  sidecarDispatchPrompt, deleteDispatchPromptSidecar,
15
- WI_STATUS, DISPATCH_RESULT, ENGINE_DEFAULTS, AGENT_STATUS, FAILURE_CLASS } = shared;
15
+ WI_STATUS, DISPATCH_RESULT, ENGINE_DEFAULTS, AGENT_STATUS, FAILURE_CLASS, PR_STATUS } = shared;
16
16
  const { getConfig, getDispatch, DISPATCH_PATH, INBOX_DIR } = queries;
17
17
 
18
18
  const MINIONS_DIR = shared.MINIONS_DIR;
@@ -91,6 +91,64 @@ function addToDispatch(item) {
91
91
  return item.id;
92
92
  }
93
93
 
94
+ function _resolveDispatchProject(projectRef, config) {
95
+ if (!projectRef) return null;
96
+ const projects = getProjects(config);
97
+ if (projectRef.name) {
98
+ const byName = projects.find(p => p.name === projectRef.name);
99
+ if (byName) return byName;
100
+ }
101
+ if (projectRef.localPath) {
102
+ const refPath = path.resolve(projectRef.localPath);
103
+ const byPath = projects.find(p => p.localPath && path.resolve(p.localPath) === refPath);
104
+ if (byPath) return byPath;
105
+ }
106
+ return projectRef;
107
+ }
108
+
109
+ function _isPrBackedDispatch(entry) {
110
+ return !!(entry?.meta?.pr && entry.meta?.project);
111
+ }
112
+
113
+ function getStalePrDispatchReason(entry, config) {
114
+ if (!_isPrBackedDispatch(entry)) return '';
115
+ const project = _resolveDispatchProject(entry.meta.project, config);
116
+ if (!project) return 'missing project metadata';
117
+
118
+ const tracked = shared.findPrRecord(queries.getPrs(project), entry.meta.pr, project);
119
+ const prLabel = entry.meta.pr?.id || entry.meta.pr?.url || entry.id;
120
+ if (!tracked) return `PR ${prLabel} is no longer tracked`;
121
+ if (tracked.status !== PR_STATUS.ACTIVE) return `PR ${tracked.id || prLabel} is ${tracked.status || 'missing status'}`;
122
+ if (tracked._contextOnly) return `PR ${tracked.id || prLabel} is context-only`;
123
+
124
+ const queuedBranch = entry.meta.branch || entry.meta.pr?.branch || '';
125
+ const trackedBranch = tracked.branch || '';
126
+ if (queuedBranch && trackedBranch && shared.sanitizeBranch(queuedBranch) !== shared.sanitizeBranch(trackedBranch)) {
127
+ return `PR ${tracked.id || prLabel} branch changed from ${queuedBranch} to ${trackedBranch}`;
128
+ }
129
+
130
+ return '';
131
+ }
132
+
133
+ function pruneStalePrDispatches(config = queries.getConfig()) {
134
+ const removed = [];
135
+ mutateDispatch((dispatch) => {
136
+ dispatch.pending = (dispatch.pending || []).filter(entry => {
137
+ const reason = getStalePrDispatchReason(entry, config);
138
+ if (!reason) return true;
139
+ removed.push({ entry, reason });
140
+ return false;
141
+ });
142
+ return dispatch;
143
+ });
144
+
145
+ for (const { entry, reason } of removed) {
146
+ try { deleteDispatchPromptSidecar(entry); } catch { /* cleanup best-effort */ }
147
+ log('info', `Dropped stale PR dispatch ${entry.id}: ${reason}`);
148
+ }
149
+ return removed.length;
150
+ }
151
+
94
152
  // ─── Retryable Failure Classification ────────────────────────────────────────
95
153
 
96
154
  function isRetryableFailureReason(reason = '', failureClass = '') {
@@ -253,11 +311,16 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
253
311
  if (prId && project) {
254
312
  try {
255
313
  const prsPath = projectPrPath(project);
314
+ let restored = false;
256
315
  mutatePullRequests(prsPath, prs => {
257
316
  const target = shared.findPrRecord(prs, { id: prId }, project);
258
- if (target?.humanFeedback) target.humanFeedback.pendingFix = true;
317
+ if (target?.humanFeedback) {
318
+ target.humanFeedback.pendingFix = true;
319
+ restored = true;
320
+ }
259
321
  });
260
- log('info', `Restored pendingFix=true on ${prId} after failed human-feedback fix`);
322
+ if (restored) log('info', `Restored pendingFix=true on ${prId} after failed human-feedback fix`);
323
+ else log('info', `Skipped pendingFix restore for ${prId} — PR is no longer tracked`);
261
324
  } catch (e) { log('warn', `restore pendingFix: ${e.message}`); }
262
325
  }
263
326
  // Clear completed dispatch entry so dedup doesn't block re-dispatch
@@ -424,6 +487,8 @@ module.exports = {
424
487
  completeDispatch,
425
488
  writeInboxAlert,
426
489
  updateAgentStatus,
490
+ getStalePrDispatchReason,
491
+ pruneStalePrDispatches,
427
492
  cancelPendingDispatchesForPr,
428
493
  cleanDispatchEntries,
429
494
  cancelPendingWorkItems,
package/engine.js CHANGED
@@ -102,7 +102,7 @@ const withFileLock = shared.withFileLock;
102
102
  // ─── Dispatch Management (extracted to engine/dispatch.js) ───────────────────
103
103
 
104
104
  const { mutateDispatch, addToDispatch, isRetryableFailureReason, completeDispatch,
105
- writeInboxAlert, updateAgentStatus } = require('./engine/dispatch');
105
+ writeInboxAlert, updateAgentStatus, pruneStalePrDispatches } = require('./engine/dispatch');
106
106
 
107
107
  // ─── Timeout / Steering / Idle (extracted to engine/timeout.js) ──────────────
108
108
 
@@ -3639,6 +3639,7 @@ async function tickInner() {
3639
3639
  const maxC = config.engine?.maxConcurrent ?? ENGINE_DEFAULTS.maxConcurrent;
3640
3640
  setTempBudget(Math.max(0, maxC - activeCountPre));
3641
3641
  }
3642
+ try { pruneStalePrDispatches(config); } catch (e) { log('warn', 'prune stale PR dispatches: ' + e.message); }
3642
3643
  let discoveryOk = true;
3643
3644
  try { await discoverWork(config); } catch (e) { log('warn', 'discoverWork: ' + e.message); discoveryOk = false; }
3644
3645
 
@@ -3915,7 +3916,7 @@ module.exports = {
3915
3916
  validateConfig,
3916
3917
 
3917
3918
  // Dispatch management (re-exported from engine/dispatch.js)
3918
- mutateDispatch, addToDispatch, isRetryableFailureReason, completeDispatch, writeInboxAlert, updateAgentStatus,
3919
+ mutateDispatch, addToDispatch, isRetryableFailureReason, completeDispatch, writeInboxAlert, updateAgentStatus, pruneStalePrDispatches,
3919
3920
  activeProcesses, realActivityMap, engineRestartGraceExempt,
3920
3921
  get engineRestartGraceUntil() { return engineRestartGraceUntil; },
3921
3922
  set engineRestartGraceUntil(v) { engineRestartGraceUntil = v; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1627",
3
+ "version": "0.1.1629",
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"