@yemi33/minions 0.1.98 → 0.1.99

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,16 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.98 (2026-04-01)
3
+ ## 0.1.99 (2026-04-01)
4
4
 
5
5
  ### Engine
6
6
  - engine/ado.js
7
7
  - engine/github.js
8
+ - engine/lifecycle.js
9
+ - engine/preflight.js
10
+ - engine/shared.js
11
+
12
+ ### Other
13
+ - test/unit.test.js
8
14
 
9
15
  ## 0.1.97 (2026-04-01)
10
16
 
@@ -393,7 +393,12 @@ function chainPlanToPrd(dispatchItem, meta, config) {
393
393
  log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
394
394
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
395
395
  let items = [];
396
- try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch {}
396
+ try {
397
+ items = JSON.parse(fs.readFileSync(wiPath, 'utf8'));
398
+ } catch (err) {
399
+ log('warn', `Plan chaining: failed to parse ${wiPath}, falling back to empty list: ${err.message}`);
400
+ try { fs.copyFileSync(wiPath, wiPath + '.bak'); } catch (_) { /* backup best-effort */ }
401
+ }
397
402
  items.push({
398
403
  id: 'W-' + shared.uid(),
399
404
  title: `Convert plan to PRD: ${meta?.item?.title || planFile.name}`,
@@ -580,7 +585,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
580
585
  dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
581
586
  }
582
587
  const entry = dirtyTargets.get(targetName);
583
- if (entry.prs.some(p => p.id === fullId || String(p.id).includes(prId))) continue;
588
+ if (entry.prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
584
589
 
585
590
  let title = meta?.item?.title || '';
586
591
  const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
@@ -651,7 +656,7 @@ function updatePrAfterReview(agentId, pr, project) {
651
656
  }
652
657
 
653
658
  shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
654
- log('info', `Updated ${pr.id} → minions review: ${minionsVerdict} by ${reviewerName}`);
659
+ log('info', `Updated ${pr.id} → minions review: ${target.reviewStatus} by ${reviewerName}`);
655
660
  createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
656
661
  }
657
662
 
@@ -25,10 +25,11 @@ 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 wrapper = execSync(`bash -c "cat '${which}'"`, { encoding: 'utf8', windowsHide: true, timeout: 5000 });
28
+ const whichNative = which.replace(/^\/c\//, 'C:/').replace(/\//g, path.sep);
29
+ const wrapper = fs.readFileSync(whichNative, 'utf8');
29
30
  const m = wrapper.match(/node_modules\/@anthropic-ai\/claude-code\/cli\.js/);
30
31
  if (m) {
31
- const basedir = path.dirname(which.replace(/^\/c\//, 'C:/').replace(/\//g, path.sep));
32
+ const basedir = path.dirname(whichNative);
32
33
  const resolved = path.join(basedir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
33
34
  if (fs.existsSync(resolved)) return resolved;
34
35
  }
package/engine/shared.js CHANGED
@@ -43,11 +43,13 @@ function safeJson(p) {
43
43
  try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
44
44
  }
45
45
 
46
+ let _tmpCounter = 0;
47
+
46
48
  function safeWrite(p, data) {
47
49
  const dir = path.dirname(p);
48
50
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
49
51
  const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
50
- const tmp = p + '.tmp.' + process.pid;
52
+ const tmp = p + '.tmp.' + process.pid + '.' + (++_tmpCounter);
51
53
  try {
52
54
  fs.writeFileSync(tmp, content);
53
55
  // Atomic rename — retry on Windows EPERM (file locking)
@@ -58,7 +60,7 @@ function safeWrite(p, data) {
58
60
  } catch (e) {
59
61
  if (e.code === 'EPERM' && attempt < 4) {
60
62
  const delay = 50 * (attempt + 1); // 50, 100, 150, 200ms
61
- try { const ab = new SharedArrayBuffer(4); Atomics.wait(new Int32Array(ab), 0, 0, delay); } catch { /* fallback busy-wait */ const start = Date.now(); while (Date.now() - start < delay) {} }
63
+ sleepMs(delay);
62
64
  continue;
63
65
  }
64
66
  // Final attempt failed — throw to let caller retry
@@ -85,11 +87,13 @@ function sleepMs(ms) {
85
87
  const ab = new SharedArrayBuffer(4);
86
88
  Atomics.wait(new Int32Array(ab), 0, 0, ms);
87
89
  } catch {
88
- const start = Date.now();
89
- while (Date.now() - start < ms) {}
90
+ // Fallback: synchronous sleep via child process — avoids busy-wait blocking the event loop
91
+ _spawnSync(process.execPath, ['-e', `setTimeout(()=>{},${Math.max(0, Math.floor(ms))})`], { windowsHide: true });
90
92
  }
91
93
  }
92
94
 
95
+ const LOCK_STALE_MS = 60000; // 60 seconds — force-remove locks older than this
96
+
93
97
  function withFileLock(lockPath, fn, {
94
98
  timeoutMs = 5000,
95
99
  retryDelayMs = 25
@@ -104,6 +108,14 @@ function withFileLock(lockPath, fn, {
104
108
  break;
105
109
  } catch (err) {
106
110
  if (err.code !== 'EEXIST') throw err;
111
+ // Check for stale lock — if lock file is older than LOCK_STALE_MS, force-remove it
112
+ try {
113
+ const stat = fs.statSync(lockPath);
114
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
115
+ try { fs.unlinkSync(lockPath); } catch { /* race: another process removed it */ }
116
+ continue; // retry immediately after removing stale lock
117
+ }
118
+ } catch { /* lock file disappeared between EEXIST and stat — retry will succeed */ }
107
119
  sleepMs(retryDelayMs);
108
120
  }
109
121
  }
@@ -372,6 +384,23 @@ function getAdoOrgBase(project) {
372
384
  : `https://dev.azure.com/${project.adoOrg}`;
373
385
  }
374
386
 
387
+ // ── Path Sanitization ───────────────────────────────────────────────────────
388
+
389
+ /**
390
+ * Resolve a user-supplied path relative to a base directory and verify the
391
+ * result stays within the base. Throws if the resolved path escapes baseDir.
392
+ * Use to prevent path-traversal attacks on any user-facing file endpoint.
393
+ */
394
+ function sanitizePath(baseDir, userInput) {
395
+ const resolvedBase = path.resolve(baseDir);
396
+ const resolvedFull = path.resolve(resolvedBase, userInput);
397
+ // Append path.sep so "/foo" doesn't match "/foobar"
398
+ if (!resolvedFull.startsWith(resolvedBase + path.sep) && resolvedFull !== resolvedBase) {
399
+ throw new Error(`Path traversal blocked: ${userInput} resolves outside ${baseDir}`);
400
+ }
401
+ return resolvedFull;
402
+ }
403
+
375
404
  // ── Branch Sanitization ──────────────────────────────────────────────────────
376
405
 
377
406
  function sanitizeBranch(name) {
@@ -452,7 +481,10 @@ module.exports = {
452
481
  addPrLink,
453
482
  nextWorkItemId,
454
483
  getAdoOrgBase,
484
+ sanitizePath,
455
485
  sanitizeBranch,
456
486
  parseSkillFrontmatter,
487
+ sleepMs,
488
+ LOCK_STALE_MS,
457
489
  };
458
490
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.98",
3
+ "version": "0.1.99",
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"