@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 +7 -1
- package/engine/lifecycle.js +8 -3
- package/engine/preflight.js +3 -2
- package/engine/shared.js +36 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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
|
|
package/engine/lifecycle.js
CHANGED
|
@@ -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 {
|
|
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)
|
|
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: ${
|
|
659
|
+
log('info', `Updated ${pr.id} → minions review: ${target.reviewStatus} by ${reviewerName}`);
|
|
655
660
|
createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
|
|
656
661
|
}
|
|
657
662
|
|
package/engine/preflight.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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