@yemi33/minions 0.1.128 → 0.1.130

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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.130 (2026-04-01)
4
+
5
+ ### Engine
6
+ - engine/lifecycle.js
7
+ - engine/shared.js
8
+ - engine/timeout.js
9
+
10
+ ### Dashboard
11
+ - dashboard.js
12
+
13
+ ## 0.1.129 (2026-04-01)
14
+
15
+ ### Engine
16
+ - engine/github.js
17
+ - engine/preflight.js
18
+ - engine/spawn-agent.js
19
+
20
+ ### Dashboard
21
+ - dashboard/js/modal.js
22
+ - dashboard/js/render-agents.js
23
+
3
24
  ## 0.1.128 (2026-04-01)
4
25
 
5
26
  ### Engine
@@ -13,7 +13,7 @@ function closeModal() {
13
13
  if (_qaSessionKey && (_qaHistory.length > 0 || _qaQueue.length > 0)) {
14
14
  _qaSessions.set(_qaSessionKey, {
15
15
  history: _qaHistory,
16
- threadHtml: document.getElementById('modal-qa-thread').innerHTML,
16
+ threadHtml: (document.getElementById('modal-qa-thread') || {}).innerHTML || '',
17
17
  docContext: { ..._modalDocContext },
18
18
  filePath: _modalFilePath,
19
19
  });
@@ -17,10 +17,10 @@ function renderAgents(agents) {
17
17
  }
18
18
 
19
19
  async function openAgentDetail(id) {
20
- currentAgentId = id;
21
20
  const agent = agentData.find(a => a.id === id);
22
- currentTab = (agent?.status === 'working') ? 'live' : 'thought-process';
23
21
  if (!agent) return;
22
+ currentAgentId = id;
23
+ currentTab = (agent.status === 'working') ? 'live' : 'thought-process';
24
24
 
25
25
  document.getElementById('detail-agent-name').innerHTML =
26
26
  '<span style="font-size:22px">' + agent.emoji + '</span> ' + agent.name + ' — ' + agent.role;
package/dashboard.js CHANGED
@@ -1662,7 +1662,7 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
1662
1662
  const body = await readBody(req);
1663
1663
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
1664
1664
  const file = body.file;
1665
- if (file.includes('..') || file.includes('\0')) return jsonReply(res, 400, { error: 'invalid' });
1665
+ if (file.includes('..') || file.includes('\0') || file.includes('/') || file.includes('\\')) return jsonReply(res, 400, { error: 'invalid' });
1666
1666
 
1667
1667
  const isJson = file.endsWith('.json');
1668
1668
  const sourceDir = isJson ? PRD_DIR : PLANS_DIR;
@@ -1701,7 +1701,7 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
1701
1701
  const body = await readBody(req);
1702
1702
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
1703
1703
  const file = body.file;
1704
- if (file.includes('..') || file.includes('\0')) return jsonReply(res, 400, { error: 'invalid' });
1704
+ if (file.includes('..') || file.includes('\0') || file.includes('/') || file.includes('\\')) return jsonReply(res, 400, { error: 'invalid' });
1705
1705
 
1706
1706
  const isJson = file.endsWith('.json');
1707
1707
  const targetDir = isJson ? PRD_DIR : PLANS_DIR;
@@ -2660,7 +2660,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2660
2660
  const body = await readBody(req);
2661
2661
  const { name, category } = body;
2662
2662
  if (!name) return jsonReply(res, 400, { error: 'name required' });
2663
- if (name.includes('..') || name.includes('\0')) return jsonReply(res, 400, { error: 'Invalid file name' });
2663
+ if (name.includes('..') || name.includes('\0') || name.includes('/') || name.includes('\\')) return jsonReply(res, 400, { error: 'Invalid file name' });
2664
2664
  if (!category || !shared.KB_CATEGORIES.includes(category)) {
2665
2665
  return jsonReply(res, 400, { error: 'category required: ' + shared.KB_CATEGORIES.join(', ') });
2666
2666
  }
@@ -2737,13 +2737,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2737
2737
  const params = new URL(req.url, 'http://localhost').searchParams;
2738
2738
  const file = params.get('file');
2739
2739
  const dir = params.get('dir');
2740
- if (!file || file.includes('..') || file.includes('\0')) { res.statusCode = 400; res.end('Invalid file'); return; }
2740
+ if (!file || file.includes('..') || file.includes('\0') || file.includes('/') || file.includes('\\')) { res.statusCode = 400; res.end('Invalid file'); return; }
2741
2741
 
2742
2742
  let content = '';
2743
2743
  if (dir) {
2744
- // Direct path from collectSkillFiles
2745
- const fullPath = path.join(dir.replace(/\//g, path.sep), file);
2746
- if (!fullPath.includes('..')) content = safeRead(fullPath) || '';
2744
+ // Direct path from collectSkillFiles — validate resolved path stays within expected dir
2745
+ const resolvedDir = path.resolve(dir.replace(/\//g, path.sep));
2746
+ const fullPath = path.join(resolvedDir, file);
2747
+ if (fullPath.startsWith(resolvedDir)) content = safeRead(fullPath) || '';
2747
2748
  }
2748
2749
  if (!content) {
2749
2750
  // Fallback: search Claude Code skills, then project skills
package/engine/github.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  const shared = require('./shared');
8
- const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR, addPrLink } = shared;
8
+ const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR, addPrLink, getPrLinks } = shared;
9
9
  const { getPrs } = require('./queries');
10
10
  const path = require('path');
11
11
 
@@ -500,14 +500,18 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
500
500
  const files = sourcePlan ? [sourcePlan] : require('fs').readdirSync(prdDir).filter(f => f.endsWith('.json'));
501
501
  for (const pf of files) {
502
502
  const fpath = path.join(prdDir, pf);
503
+ mutateJsonFileLocked(fpath, (plan) => {
504
+ if (!plan?.missing_features) return plan;
505
+ const feature = plan.missing_features.find(f => f.id === itemId);
506
+ if (feature && feature.status !== status) {
507
+ feature.status = status;
508
+ }
509
+ return plan;
510
+ });
511
+ // Check if we found it (read back to verify)
503
512
  const plan = safeJson(fpath);
504
- if (!plan?.missing_features) continue;
505
- const feature = plan.missing_features.find(f => f.id === itemId);
506
- if (feature && feature.status !== status) {
507
- feature.status = status;
508
- shared.safeWrite(fpath, plan);
509
- return;
510
- }
513
+ const feature = plan?.missing_features?.find(f => f.id === itemId);
514
+ if (feature && feature.status === status) return;
511
515
  }
512
516
  } catch (err) { log('warn', `PRD status sync: ${err.message}`); }
513
517
  }
@@ -25,7 +25,7 @@ function findClaudeBinary() {
25
25
  // Fallback: parse the shell wrapper
26
26
  try {
27
27
  const which = execSync('bash -c "which claude"', { encoding: 'utf8', windowsHide: true, timeout: 5000 }).trim();
28
- const whichNative = which.replace(/^\/c\//, 'C:/').replace(/\//g, path.sep);
28
+ const whichNative = which.replace(/^\/([a-zA-Z])\//, (_, d) => d.toUpperCase() + ':/').replace(/\//g, path.sep);
29
29
  const wrapper = fs.readFileSync(whichNative, 'utf8');
30
30
  const m = wrapper.match(/node_modules\/@anthropic-ai\/claude-code\/cli\.js/);
31
31
  if (m) {
package/engine/shared.js CHANGED
@@ -22,11 +22,14 @@ function log(level, msg, meta = {}) {
22
22
  const entry = { timestamp: ts(), level, message: msg, ...meta };
23
23
  console.log(`[${logTs()}] [${level}] ${msg}`);
24
24
 
25
- let logData = safeJson(LOG_PATH) || [];
26
- if (!Array.isArray(logData)) logData = logData.entries || [];
27
- logData.push(entry);
28
- if (logData.length >= 2500) logData.splice(0, logData.length - 2000);
29
- safeWrite(LOG_PATH, logData);
25
+ try {
26
+ mutateJsonFileLocked(LOG_PATH, (logData) => {
27
+ if (!Array.isArray(logData)) logData = logData?.entries || [];
28
+ logData.push(entry);
29
+ if (logData.length >= 2500) logData.splice(0, logData.length - 2000);
30
+ return logData;
31
+ }, { defaultValue: [] });
32
+ } catch { /* logging should never crash the caller */ }
30
33
  }
31
34
 
32
35
  // ── File I/O ─────────────────────────────────────────────────────────────────
@@ -39,10 +39,11 @@ for (const p of searchPaths) {
39
39
  if (!claudeBin) {
40
40
  try {
41
41
  const which = exec('bash -c "which claude"', { encoding: 'utf8', env }).trim();
42
- const wrapper = exec(`bash -c "cat '${which}'"`, { encoding: 'utf8', env });
42
+ const whichNative = which.replace(/^\/([a-zA-Z])\//, (_, d) => d.toUpperCase() + ':/').replace(/\//g, path.sep);
43
+ const wrapper = fs.readFileSync(whichNative, 'utf8');
43
44
  const m = wrapper.match(/node_modules\/@anthropic-ai\/claude-code\/cli\.js/);
44
45
  if (m) {
45
- const basedir = path.dirname(which.replace(/^\/c\//, 'C:/').replace(/\//g, path.sep));
46
+ const basedir = path.dirname(whichNative);
46
47
  claudeBin = path.join(basedir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
47
48
  }
48
49
  } catch { /* optional */ }
package/engine/timeout.js CHANGED
@@ -248,6 +248,8 @@ function checkTimeouts(config) {
248
248
  const isActive = possibleKeys.some(k => activeKeys.has(k)) ||
249
249
  (dispatchData.active || []).some(d => d.meta?.item?.id === item.id);
250
250
  if (!isActive) {
251
+ // Don't revive items that were explicitly failed for non-retryable reasons
252
+ if (item.status === 'failed' && item.failReason && !item.failReason.includes('Agent died')) continue;
251
253
  const retries = (item._retryCount || 0);
252
254
  if (retries < 3) {
253
255
  log('info', `Reconcile: work item ${item.id} agent died — auto-retry ${retries + 1}/3`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.128",
3
+ "version": "0.1.130",
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"