@yemi33/minions 0.1.418 → 0.1.420

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,10 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.418 (2026-04-06)
3
+ ## 0.1.420 (2026-04-06)
4
4
 
5
5
  ### Features
6
+ - Low-priority cleanup: dedupe regex, consolidate streaming parse, add path validation alignment
7
+ - Fix 6 medium bugs: dispatch pruning, null guards, skill regex, meeting advancement, CLI PID check, pipeline retry
6
8
  - Convert remaining lifecycle.js safeWrite calls to mutateJsonFileLocked
7
9
 
10
+ ### Fixes
11
+ - address review feedback — pipeline.js socket leak and magic numbers
12
+
8
13
  ## 0.1.417 (2026-04-06)
9
14
 
10
15
  ### Features
package/dashboard.js CHANGED
@@ -2329,9 +2329,8 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2329
2329
  try {
2330
2330
  const body = await readBody(req);
2331
2331
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
2332
- if (body.file.includes('..') || body.file.includes('\0') || body.file.includes('/') || body.file.includes('\\')) {
2333
- return jsonReply(res, 400, { error: 'invalid filename' });
2334
- }
2332
+ try { shared.sanitizePath(body.file, body.file.endsWith('.json') ? PRD_DIR : PLANS_DIR); }
2333
+ catch { return jsonReply(res, 400, { error: 'invalid filename' }); }
2335
2334
  const isJson = body.file.endsWith('.json');
2336
2335
  const targetDir = isJson ? PRD_DIR : PLANS_DIR;
2337
2336
  const archivePath = path.join(targetDir, 'archive', body.file);
package/engine/ado.js CHANGED
@@ -381,7 +381,7 @@ async function reconcilePrs(config) {
381
381
  const title = adoPr.title || '';
382
382
  // Extract item ID from branch name or PR title (e.g., feat(P-2cafdc2a): ...)
383
383
  const branchMatch = branch.match(/(P-[a-z0-9]{6,})/i) || branch.match(/(W-[a-z0-9]{6,})/i) || branch.match(/(PL-[a-z0-9]{6,})/i);
384
- const titleMatch = title.match(/\((P-[a-z0-9]{6,})\)/) || title.match(/\((W-[a-z0-9]{6,})\)/) || title.match(/\((W-[a-z0-9]{6,})\)/) || title.match(/\((PL-[a-z0-9]{6,})\)/);
384
+ const titleMatch = title.match(/\((P-[a-z0-9]{6,})\)/) || title.match(/\((W-[a-z0-9]{6,})\)/) || title.match(/\((PL-[a-z0-9]{6,})\)/);
385
385
  const linkedItemId = branchMatch?.[1] || titleMatch?.[1] || null;
386
386
  const linkedItem = linkedItemId ? allItems.find(i => i.id === linkedItemId) : null;
387
387
  const confirmedItemId = linkedItem ? linkedItemId : null;
@@ -87,7 +87,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
87
87
  if (reason) item.reason = reason;
88
88
  if (resultSummary) item.resultSummary = resultSummary;
89
89
  delete item.prompt;
90
- if (dispatch.completed.length > 100) {
90
+ if (dispatch.completed.length >= 100) {
91
91
  dispatch.completed = dispatch.completed.slice(-100);
92
92
  }
93
93
  dispatch.completed.push(item);
@@ -901,6 +901,7 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
901
901
  if (!fullText) fullText = output;
902
902
  const skillBlocks = [];
903
903
  const skillRegex = /```skill\s*\n([\s\S]*?)```/g;
904
+ skillRegex.lastIndex = 0;
904
905
  let match;
905
906
  while ((match = skillRegex.exec(fullText)) !== null) {
906
907
  skillBlocks.push(match[1].trim());
package/engine/llm.js CHANGED
@@ -160,25 +160,12 @@ function callLLMStreaming(promptText, sysPromptText, { timeout = 120000, label =
160
160
  if (block.type === 'text' && block.text && block.text !== lastTextSent) {
161
161
  lastTextSent = block.text;
162
162
  onChunk(block.text);
163
- }
164
- }
165
- }
166
- } catch { /* incomplete JSON or non-JSON line */ }
167
- }
168
- // Also emit tool_use events so the frontend can show "Using tool: Read..."
169
- for (const line of lines) {
170
- const trimmed = line.trim();
171
- if (!trimmed || !trimmed.startsWith('{')) continue;
172
- try {
173
- const obj = JSON.parse(trimmed);
174
- if (obj.type === 'assistant' && obj.message?.content) {
175
- for (const block of obj.message.content) {
176
- if (block.type === 'tool_use' && block.name && onToolUse) {
163
+ } else if (block.type === 'tool_use' && block.name && onToolUse) {
177
164
  onToolUse(block.name, block.input);
178
165
  }
179
166
  }
180
167
  }
181
- } catch {}
168
+ } catch { /* incomplete JSON or non-JSON line */ }
182
169
  }
183
170
  });
184
171
  proc.stderr.on('data', d => { stderr += d.toString(); });
@@ -7,7 +7,8 @@
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const shared = require('./shared');
10
- const { safeJson, safeWrite, safeRead, safeReadDir, uid, log, ts, dateStamp, mutateJsonFileLocked, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS } = shared;
10
+ const { safeJson, safeWrite, safeRead, safeReadDir, uid, log, ts, dateStamp, mutateJsonFileLocked, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, ENGINE_DEFAULTS } = shared;
11
+ const http = require('http');
11
12
  const { parseCronExpr, shouldRunNow } = require('./scheduler');
12
13
 
13
14
  const PIPELINES_DIR = path.join(__dirname, '..', 'pipelines');
@@ -313,18 +314,34 @@ function executeApiStage(stage, stageState, run) {
313
314
  for (const call of calls) {
314
315
  const url = `http://localhost:${process.env.MINIONS_PORT || 7331}${call.endpoint}`;
315
316
  const body = typeof call.body === 'string' ? call.body : JSON.stringify(call.body || {});
316
- // Fire and forget — use Node's http module
317
- try {
318
- const http = require('http');
319
- const parsed = new URL(url);
320
- const req = http.request({
321
- hostname: parsed.hostname, port: parsed.port, path: parsed.pathname,
322
- method: call.method || 'POST',
323
- headers: { 'Content-Type': 'application/json' },
324
- });
325
- req.write(body);
326
- req.end();
327
- } catch (e) { log('warn', `Pipeline API call failed: ${e.message}`); }
317
+ const maxAttempts = ENGINE_DEFAULTS.pipelineApiRetries;
318
+ const retryDelay = ENGINE_DEFAULTS.pipelineApiRetryDelay;
319
+ const makeRequest = (attempt) => {
320
+ try {
321
+ const parsed = new URL(url);
322
+ const req = http.request({
323
+ hostname: parsed.hostname, port: parsed.port, path: parsed.pathname,
324
+ method: call.method || 'POST',
325
+ headers: { 'Content-Type': 'application/json' },
326
+ }, (res) => {
327
+ res.resume(); // drain body to free socket
328
+ if (res.statusCode >= 400) {
329
+ log('warn', `Pipeline API call to ${call.endpoint} returned ${res.statusCode} (attempt ${attempt})`);
330
+ if (attempt < maxAttempts) setTimeout(() => makeRequest(attempt + 1), retryDelay);
331
+ }
332
+ });
333
+ req.on('error', (err) => {
334
+ log('warn', `Pipeline API call to ${call.endpoint} failed: ${err.message} (attempt ${attempt})`);
335
+ if (attempt < maxAttempts) setTimeout(() => makeRequest(attempt + 1), retryDelay);
336
+ });
337
+ req.write(body);
338
+ req.end();
339
+ } catch (e) {
340
+ log('warn', `Pipeline API call to ${call.endpoint} threw: ${e.message} (attempt ${attempt})`);
341
+ if (attempt < maxAttempts) setTimeout(() => makeRequest(attempt + 1), retryDelay);
342
+ }
343
+ };
344
+ makeRequest(1);
328
345
  }
329
346
  return { status: PIPELINE_STATUS.COMPLETED, completedAt: ts() };
330
347
  }
@@ -27,7 +27,13 @@ function findClaudeBinary() {
27
27
  path.join(path.dirname(process.execPath), '..', 'lib', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
28
28
  // fnm / volta — sibling to the node binary
29
29
  path.join(path.dirname(process.execPath), 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
30
- ].filter(Boolean);
30
+ ].filter(p => {
31
+ if (!p) {
32
+ if (process.env.MINIONS_DEBUG) console.log('[preflight] Dropped empty CLI search path entry');
33
+ return false;
34
+ }
35
+ return true;
36
+ });
31
37
  for (const p of searchPaths) {
32
38
  try { if (fs.existsSync(p)) return p; } catch {}
33
39
  }
package/engine/shared.js CHANGED
@@ -438,6 +438,8 @@ const ENGINE_DEFAULTS = {
438
438
  evalMaxIterations: 3, // max review→fix cycles before escalating to human
439
439
  evalMaxCost: null, // USD ceiling per work item across all eval iterations; null = no limit (gather baseline data first)
440
440
  maxRetries: 3, // max dispatch retries before marking work item as failed
441
+ pipelineApiRetries: 2, // max attempts for pipeline API calls
442
+ pipelineApiRetryDelay: 2000, // ms delay between pipeline API retries
441
443
  };
442
444
 
443
445
  // ─── Status & Type Constants ─────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.418",
3
+ "version": "0.1.420",
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"