@yemi33/minions 0.1.1780 → 0.1.1781
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 +5 -0
- package/engine/cleanup.js +14 -5
- package/engine/copilot-models.json +1 -1
- package/engine/meeting.js +2 -8
- package/engine/preflight.js +3 -4
- package/engine/shared.js +25 -14
- package/engine.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/engine/cleanup.js
CHANGED
|
@@ -369,13 +369,23 @@ async function runCleanup(config, verbose = false) {
|
|
|
369
369
|
// 2b. Detect git worktrees registered inside any linked project's working tree.
|
|
370
370
|
// Nested worktrees cause glob/grep tools running with cwd=projectRoot to match
|
|
371
371
|
// BOTH copies of every file; a single Edit/MultiEdit then writes the same
|
|
372
|
-
// change to both locations, producing
|
|
372
|
+
// change to both locations, producing mirror-write leaks.
|
|
373
373
|
// We only WARN here — removing someone else's worktree without consent could
|
|
374
374
|
// destroy in-flight work. The operator runs `git worktree remove <path>`.
|
|
375
375
|
cleaned.nestedWorktrees = 0;
|
|
376
|
+
const _scannedRoots = new Set(); // dedup projects sharing localPath
|
|
376
377
|
for (const project of projects) {
|
|
377
|
-
|
|
378
|
-
|
|
378
|
+
if (!project.localPath) continue;
|
|
379
|
+
const root = path.resolve(project.localPath);
|
|
380
|
+
if (_scannedRoots.has(root)) continue;
|
|
381
|
+
_scannedRoots.add(root);
|
|
382
|
+
if (!fs.existsSync(root)) {
|
|
383
|
+
// Configured project whose checkout has been moved or deleted — surface
|
|
384
|
+
// it so the operator knows their config is out of sync. A missing root
|
|
385
|
+
// means we cannot scan it, leaving nested worktrees there undetectable.
|
|
386
|
+
log('warn', `Project "${project.name || root}" has localPath "${root}" which does not exist on disk — skipping worktree scan`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
379
389
|
let raw;
|
|
380
390
|
try {
|
|
381
391
|
raw = String(shared.execSilent('git worktree list --porcelain', { cwd: root, timeout: 10000, windowsHide: true }) || '');
|
|
@@ -384,8 +394,7 @@ async function runCleanup(config, verbose = false) {
|
|
|
384
394
|
if (!line.startsWith('worktree ')) continue;
|
|
385
395
|
const wt = line.slice('worktree '.length).trim();
|
|
386
396
|
if (!wt) continue;
|
|
387
|
-
if (
|
|
388
|
-
if (!shared.isPathInsideOrEqual(wt, root)) continue;
|
|
397
|
+
if (!shared.isPathInside(wt, root)) continue; // strict — main worktree (equal path) is expected, descendants are the leak
|
|
389
398
|
cleaned.nestedWorktrees++;
|
|
390
399
|
log('warn', `Nested worktree in project "${project.name || root}": "${wt}" is inside "${root}". This causes glob tools to match both copies and produces mirror writes. Run: git worktree remove "${wt}"`);
|
|
391
400
|
}
|
package/engine/meeting.js
CHANGED
|
@@ -155,17 +155,12 @@ function getStructuredNoteArtifacts(structuredCompletion) {
|
|
|
155
155
|
);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function isPathInside(parent, child) {
|
|
159
|
-
const rel = path.relative(parent, child);
|
|
160
|
-
return Boolean(rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
158
|
function resolveMeetingNoteArtifactPath(artifactPath) {
|
|
164
159
|
const raw = String(artifactPath || '').trim();
|
|
165
160
|
if (!raw || raw.includes('\0')) return null;
|
|
166
161
|
const resolved = path.resolve(path.isAbsolute(raw) ? raw : path.join(shared.MINIONS_DIR, raw));
|
|
167
162
|
const root = path.resolve(MEETING_NOTE_ARTIFACT_ROOT);
|
|
168
|
-
if (!isPathInside(
|
|
163
|
+
if (!shared.isPathInside(resolved, root)) return null;
|
|
169
164
|
if (path.extname(resolved).toLowerCase() !== '.md') return null;
|
|
170
165
|
return resolved;
|
|
171
166
|
}
|
|
@@ -179,7 +174,7 @@ function readMeetingNoteArtifact(artifactPath) {
|
|
|
179
174
|
try {
|
|
180
175
|
const realRoot = fs.realpathSync(MEETING_NOTE_ARTIFACT_ROOT);
|
|
181
176
|
const realPath = fs.realpathSync(resolved);
|
|
182
|
-
if (!isPathInside(
|
|
177
|
+
if (!shared.isPathInside(realPath, realRoot)) {
|
|
183
178
|
log('warn', `Ignoring meeting note artifact outside notes/inbox: ${artifactPath}`);
|
|
184
179
|
return '';
|
|
185
180
|
}
|
|
@@ -914,7 +909,6 @@ module.exports = {
|
|
|
914
909
|
// exported for testing — engine code MUST go through
|
|
915
910
|
// getMeetings/discoverMeetingWork/collectMeetingFindings/checkMeetingTimeouts,
|
|
916
911
|
// never these helpers directly.
|
|
917
|
-
isPathInside,
|
|
918
912
|
resolveMeetingNoteArtifactPath,
|
|
919
913
|
cleanMeetingSummaryText,
|
|
920
914
|
splitMeetingSummaryFragments,
|
package/engine/preflight.js
CHANGED
|
@@ -275,10 +275,9 @@ function runPreflight(opts = {}) {
|
|
|
275
275
|
// 5. worktreeRoot config check — for every linked project, verify that the
|
|
276
276
|
// configured engine.worktreeRoot resolves OUTSIDE the project's
|
|
277
277
|
// localPath. A nested worktreeRoot causes glob/grep to match both
|
|
278
|
-
// copies of every file, producing silent mirror writes
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
// is the second line of defense.
|
|
278
|
+
// copies of every file, producing silent mirror writes. Hard-fail at
|
|
279
|
+
// preflight so the operator sees it before any agent dispatch — the
|
|
280
|
+
// runtime guard in spawnAgent is the second line of defense.
|
|
282
281
|
try {
|
|
283
282
|
const path = require('path');
|
|
284
283
|
const projects = shared.getProjects(opts.config) || [];
|
package/engine/shared.js
CHANGED
|
@@ -2143,30 +2143,40 @@ function buildWorktreeDirName({
|
|
|
2143
2143
|
}
|
|
2144
2144
|
|
|
2145
2145
|
/**
|
|
2146
|
-
* True when `childPath` is
|
|
2147
|
-
*
|
|
2148
|
-
*
|
|
2149
|
-
*
|
|
2150
|
-
* escapes via `..`.
|
|
2151
|
-
*
|
|
2152
|
-
* Why this helper exists: a git worktree placed inside the parent repo's
|
|
2153
|
-
* working tree causes glob/grep tools running with `cwd = projectRoot` to
|
|
2154
|
-
* match BOTH copies of every file. A single Edit/MultiEdit then writes the
|
|
2155
|
-
* same change to both locations, producing the "mirror dirty file" pattern.
|
|
2156
|
-
* Worktrees must always be siblings/cousins of the project root, never
|
|
2157
|
-
* descendants.
|
|
2146
|
+
* True when `childPath` is strictly nested within `parentPath` (descendant,
|
|
2147
|
+
* NOT the same path). Cross-platform via `path.relative`; resilient to mixed
|
|
2148
|
+
* separators. Returns false for equal paths, different roots/drives, or
|
|
2149
|
+
* `childPath` escapes via `..`.
|
|
2158
2150
|
*/
|
|
2159
|
-
function
|
|
2151
|
+
function isPathInside(childPath, parentPath) {
|
|
2160
2152
|
if (!childPath || !parentPath) return false;
|
|
2161
2153
|
const childAbs = path.resolve(String(childPath));
|
|
2162
2154
|
const parentAbs = path.resolve(String(parentPath));
|
|
2163
2155
|
const rel = path.relative(parentAbs, childAbs);
|
|
2164
|
-
if (rel === '') return
|
|
2156
|
+
if (rel === '') return false;
|
|
2165
2157
|
if (rel.startsWith('..')) return false;
|
|
2166
2158
|
if (path.isAbsolute(rel)) return false;
|
|
2167
2159
|
return true;
|
|
2168
2160
|
}
|
|
2169
2161
|
|
|
2162
|
+
/**
|
|
2163
|
+
* Same as `isPathInside` but ALSO returns true when paths are equal.
|
|
2164
|
+
*
|
|
2165
|
+
* Why this helper exists: a git worktree placed at — or inside — the parent
|
|
2166
|
+
* repo's working tree causes glob/grep tools running with `cwd = projectRoot`
|
|
2167
|
+
* to match BOTH copies of every file. A single Edit/MultiEdit then writes
|
|
2168
|
+
* the same change to both locations, producing the "mirror dirty file"
|
|
2169
|
+
* pattern. Worktrees must always be siblings/cousins of the project root,
|
|
2170
|
+
* never the root itself nor a descendant.
|
|
2171
|
+
*/
|
|
2172
|
+
function isPathInsideOrEqual(childPath, parentPath) {
|
|
2173
|
+
if (!childPath || !parentPath) return false;
|
|
2174
|
+
const childAbs = path.resolve(String(childPath));
|
|
2175
|
+
const parentAbs = path.resolve(String(parentPath));
|
|
2176
|
+
if (childAbs === parentAbs) return true;
|
|
2177
|
+
return isPathInside(childAbs, parentAbs);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2170
2180
|
/**
|
|
2171
2181
|
* Throws when `worktreePath` would land inside (or equal) `projectRoot`.
|
|
2172
2182
|
* Called by the engine spawn path before `git worktree add`, and by the
|
|
@@ -3257,6 +3267,7 @@ module.exports = {
|
|
|
3257
3267
|
sanitizePath,
|
|
3258
3268
|
sanitizeBranch,
|
|
3259
3269
|
buildWorktreeDirName, // exported for testing
|
|
3270
|
+
isPathInside,
|
|
3260
3271
|
isPathInsideOrEqual,
|
|
3261
3272
|
assertWorktreeOutsideProject,
|
|
3262
3273
|
isLiveCommandCenterPath,
|
package/engine.js
CHANGED
|
@@ -666,6 +666,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
666
666
|
if (eShared.message?.includes('already used by worktree') || eShared.message?.includes('already checked out')) {
|
|
667
667
|
const existingWtPath = await findExistingWorktree(rootDir, branchName);
|
|
668
668
|
if (existingWtPath && fs.existsSync(existingWtPath)) {
|
|
669
|
+
shared.assertWorktreeOutsideProject(existingWtPath, rootDir);
|
|
669
670
|
log('info', `Shared branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
670
671
|
worktreePath = existingWtPath;
|
|
671
672
|
} else { throw eShared; }
|
|
@@ -723,6 +724,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
723
724
|
log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
|
|
724
725
|
throw e2;
|
|
725
726
|
}
|
|
727
|
+
shared.assertWorktreeOutsideProject(existingWtPath, rootDir);
|
|
726
728
|
log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
727
729
|
worktreePath = existingWtPath;
|
|
728
730
|
} else if (existingWtPath && !fs.existsSync(existingWtPath)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1781",
|
|
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"
|