@yemi33/minions 0.1.1775 → 0.1.1777
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 +11 -5
- package/engine/cleanup.js +27 -2
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +11 -0
- package/engine/preflight.js +32 -1
- package/engine/shared.js +45 -0
- package/engine.js +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1777 (2026-05-07)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- tighten worktreeRoot validation everywhere worktrees are created
|
|
7
|
+
|
|
8
|
+
## 0.1.1776 (2026-05-07)
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
- refuse worktree paths nested in any project root
|
|
12
|
+
|
|
13
|
+
## 0.1.1774 (2026-05-07)
|
|
4
14
|
|
|
5
15
|
### Features
|
|
6
|
-
- Improve dashboard API route metadata
|
|
7
16
|
- suppress stale doc-chat model errors
|
|
8
17
|
|
|
9
18
|
### Fixes
|
|
10
19
|
- yemi33/minions#2168
|
|
11
|
-
- honor central plan work item project
|
|
12
|
-
- restore canonical-home shared helpers
|
|
13
|
-
- yemi33/minions#2170
|
|
14
20
|
|
|
15
21
|
## 0.1.1772 (2026-05-07)
|
|
16
22
|
|
package/engine/cleanup.js
CHANGED
|
@@ -366,6 +366,31 @@ async function runCleanup(config, verbose = false) {
|
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
// 2b. Detect git worktrees registered inside any linked project's working tree.
|
|
370
|
+
// Nested worktrees cause glob/grep tools running with cwd=projectRoot to match
|
|
371
|
+
// BOTH copies of every file; a single Edit/MultiEdit then writes the same
|
|
372
|
+
// change to both locations, producing "mirror dirty file" leaks (W-cc-doc-chat-continuity).
|
|
373
|
+
// We only WARN here — removing someone else's worktree without consent could
|
|
374
|
+
// destroy in-flight work. The operator runs `git worktree remove <path>`.
|
|
375
|
+
cleaned.nestedWorktrees = 0;
|
|
376
|
+
for (const project of projects) {
|
|
377
|
+
const root = project.localPath ? path.resolve(project.localPath) : null;
|
|
378
|
+
if (!root || !fs.existsSync(root)) continue;
|
|
379
|
+
let raw;
|
|
380
|
+
try {
|
|
381
|
+
raw = String(shared.execSilent('git worktree list --porcelain', { cwd: root, timeout: 10000, windowsHide: true }) || '');
|
|
382
|
+
} catch (e) { log('warn', `nested-worktree scan for ${project.name || root}: ${e.message}`); continue; }
|
|
383
|
+
for (const line of raw.split('\n')) {
|
|
384
|
+
if (!line.startsWith('worktree ')) continue;
|
|
385
|
+
const wt = line.slice('worktree '.length).trim();
|
|
386
|
+
if (!wt) continue;
|
|
387
|
+
if (path.resolve(wt) === root) continue; // main worktree — expected
|
|
388
|
+
if (!shared.isPathInsideOrEqual(wt, root)) continue;
|
|
389
|
+
cleaned.nestedWorktrees++;
|
|
390
|
+
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
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
369
394
|
// 3. Clean git worktrees for merged/abandoned PRs
|
|
370
395
|
const _attemptedWorktreePaths = new Set(); // dedup across projects sharing a worktreeRoot
|
|
371
396
|
for (const project of projects) {
|
|
@@ -664,8 +689,8 @@ async function runCleanup(config, verbose = false) {
|
|
|
664
689
|
}
|
|
665
690
|
} catch (e) { log('warn', 'prune orphaned dispatches: ' + e.message); }
|
|
666
691
|
|
|
667
|
-
if (cleaned.tempFiles + cleaned.liveOutputs + cleaned.worktrees + cleaned.zombies + (cleaned.files || 0) + cleaned.orphanedDispatches > 0) {
|
|
668
|
-
log('info', `Cleanup: ${cleaned.tempFiles} temp, ${cleaned.liveOutputs} live outputs, ${cleaned.worktrees} worktrees, ${cleaned.zombies} zombies, ${cleaned.files || 0} archives, ${cleaned.orphanedDispatches} orphaned dispatches`);
|
|
692
|
+
if (cleaned.tempFiles + cleaned.liveOutputs + cleaned.worktrees + cleaned.zombies + (cleaned.files || 0) + cleaned.orphanedDispatches + (cleaned.nestedWorktrees || 0) > 0) {
|
|
693
|
+
log('info', `Cleanup: ${cleaned.tempFiles} temp, ${cleaned.liveOutputs} live outputs, ${cleaned.worktrees} worktrees, ${cleaned.zombies} zombies, ${cleaned.files || 0} archives, ${cleaned.orphanedDispatches} orphaned dispatches, ${cleaned.nestedWorktrees || 0} nested worktrees flagged`);
|
|
669
694
|
}
|
|
670
695
|
|
|
671
696
|
// 8. Clean swept KB files older than 7 days
|
package/engine/lifecycle.js
CHANGED
|
@@ -1745,6 +1745,17 @@ async function rebaseBranchOntoMain(pr, project, config) {
|
|
|
1745
1745
|
const tmpWt = path.join(wtRoot, `rebase-${shared.sanitizeBranch(branch)}-${Date.now()}`).replace(/\\/g, '/');
|
|
1746
1746
|
const _gitOpts = { cwd: root, timeout: 30000, windowsHide: true };
|
|
1747
1747
|
|
|
1748
|
+
// Refuse to create a worktree nested in the project root — would cause
|
|
1749
|
+
// glob/grep tools running with cwd=root to match both copies of every file
|
|
1750
|
+
// and produce mirror writes. Misconfigured engine.worktreeRoot is the only
|
|
1751
|
+
// way this lands inside root; the assert throws so the caller can recover.
|
|
1752
|
+
try {
|
|
1753
|
+
shared.assertWorktreeOutsideProject(tmpWt, root);
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
log('warn', `Post-merge rebase: refusing nested worktree path — ${err.message}`);
|
|
1756
|
+
return { success: false, error: err.message };
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1748
1759
|
try {
|
|
1749
1760
|
await execAsync(`git fetch origin "${mainBranch}" "${branch}"`, _gitOpts);
|
|
1750
1761
|
try {
|
package/engine/preflight.js
CHANGED
|
@@ -238,8 +238,13 @@ function runPreflight(opts = {}) {
|
|
|
238
238
|
// us the config. checkOrExit() / cli start() / doctor() pass it; legacy
|
|
239
239
|
// callers don't, in which case we skip silently.
|
|
240
240
|
if (opts && opts.config && typeof opts.config === 'object') {
|
|
241
|
+
// Hoisted: `shared` is referenced by every check block below, including
|
|
242
|
+
// workSources warnings and the worktreeRoot check. Previously declared
|
|
243
|
+
// inside the first inner try, which left later blocks reading an
|
|
244
|
+
// undefined identifier (ReferenceError silently caught by the wrapping
|
|
245
|
+
// try/catch).
|
|
246
|
+
const shared = require('./shared');
|
|
241
247
|
try {
|
|
242
|
-
const shared = require('./shared');
|
|
243
248
|
let runtimeNames = [];
|
|
244
249
|
try { runtimeNames = require('./runtimes').listRuntimes(); }
|
|
245
250
|
catch { /* registry may be missing during partial installs */ }
|
|
@@ -266,6 +271,32 @@ function runPreflight(opts = {}) {
|
|
|
266
271
|
results.push({ name: `Project config (${w.id})`, ok: 'warn', message: w.message });
|
|
267
272
|
}
|
|
268
273
|
} catch { /* defensive */ }
|
|
274
|
+
|
|
275
|
+
// 5. worktreeRoot config check — for every linked project, verify that the
|
|
276
|
+
// configured engine.worktreeRoot resolves OUTSIDE the project's
|
|
277
|
+
// localPath. A nested worktreeRoot causes glob/grep to match both
|
|
278
|
+
// copies of every file, producing silent mirror writes (the W-cc-doc-
|
|
279
|
+
// chat-continuity leak class). Hard-fail at preflight so the operator
|
|
280
|
+
// sees it before any agent dispatch — the runtime guard in spawnAgent
|
|
281
|
+
// is the second line of defense.
|
|
282
|
+
try {
|
|
283
|
+
const path = require('path');
|
|
284
|
+
const projects = shared.getProjects(opts.config) || [];
|
|
285
|
+
const wtRoot = opts.config?.engine?.worktreeRoot || shared.ENGINE_DEFAULTS.worktreeRoot;
|
|
286
|
+
for (const project of projects) {
|
|
287
|
+
if (!project || !project.localPath) continue;
|
|
288
|
+
const root = path.resolve(project.localPath);
|
|
289
|
+
const resolved = path.resolve(root, wtRoot);
|
|
290
|
+
if (shared.isPathInsideOrEqual(resolved, root)) {
|
|
291
|
+
results.push({
|
|
292
|
+
name: `worktreeRoot (${project.name || root})`,
|
|
293
|
+
ok: false,
|
|
294
|
+
message: `engine.worktreeRoot "${wtRoot}" resolves to "${resolved}" — INSIDE project root "${root}". This causes glob/grep to match duplicate files and produces mirror writes. Set engine.worktreeRoot to a sibling path (default: "../worktrees").`,
|
|
295
|
+
});
|
|
296
|
+
allOk = false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} catch { /* defensive — preflight must never throw */ }
|
|
269
300
|
}
|
|
270
301
|
|
|
271
302
|
return { passed: allOk, results };
|
package/engine/shared.js
CHANGED
|
@@ -2121,6 +2121,49 @@ function buildWorktreeDirName({
|
|
|
2121
2121
|
return `${projectSlug}-${sanitizeBranch(branchName || 'worktree')}-${suffix}`;
|
|
2122
2122
|
}
|
|
2123
2123
|
|
|
2124
|
+
/**
|
|
2125
|
+
* True when `childPath` is the same as or nested within `parentPath`. Uses
|
|
2126
|
+
* `path.relative` so it's cross-platform and resilient to mixed separators
|
|
2127
|
+
* (Windows worktree paths often arrive with forward slashes from git output).
|
|
2128
|
+
* Returns false when paths refer to different roots/drives or `childPath`
|
|
2129
|
+
* escapes via `..`.
|
|
2130
|
+
*
|
|
2131
|
+
* Why this helper exists: a git worktree placed inside the parent repo's
|
|
2132
|
+
* working tree causes glob/grep tools running with `cwd = projectRoot` to
|
|
2133
|
+
* match BOTH copies of every file. A single Edit/MultiEdit then writes the
|
|
2134
|
+
* same change to both locations, producing the "mirror dirty file" pattern.
|
|
2135
|
+
* Worktrees must always be siblings/cousins of the project root, never
|
|
2136
|
+
* descendants.
|
|
2137
|
+
*/
|
|
2138
|
+
function isPathInsideOrEqual(childPath, parentPath) {
|
|
2139
|
+
if (!childPath || !parentPath) return false;
|
|
2140
|
+
const childAbs = path.resolve(String(childPath));
|
|
2141
|
+
const parentAbs = path.resolve(String(parentPath));
|
|
2142
|
+
const rel = path.relative(parentAbs, childAbs);
|
|
2143
|
+
if (rel === '') return true;
|
|
2144
|
+
if (rel.startsWith('..')) return false;
|
|
2145
|
+
if (path.isAbsolute(rel)) return false;
|
|
2146
|
+
return true;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/**
|
|
2150
|
+
* Throws when `worktreePath` would land inside (or equal) `projectRoot`.
|
|
2151
|
+
* Called by the engine spawn path before `git worktree add`, and by the
|
|
2152
|
+
* cleanup sweep that audits already-registered worktrees per linked project.
|
|
2153
|
+
* The thrown Error has `code: 'WORKTREE_NESTED_IN_PROJECT'` so callers can
|
|
2154
|
+
* branch on it without parsing the message.
|
|
2155
|
+
*/
|
|
2156
|
+
function assertWorktreeOutsideProject(worktreePath, projectRoot) {
|
|
2157
|
+
if (!isPathInsideOrEqual(worktreePath, projectRoot)) return;
|
|
2158
|
+
const err = new Error(
|
|
2159
|
+
`Refusing to use worktree path "${worktreePath}" — it is inside project root "${projectRoot}". ` +
|
|
2160
|
+
`A worktree nested in its parent project causes glob/grep tools to match both copies and ` +
|
|
2161
|
+
`produces "mirror" dirty files. Place worktrees outside the project (engine.worktreeRoot default: "../worktrees").`
|
|
2162
|
+
);
|
|
2163
|
+
err.code = 'WORKTREE_NESTED_IN_PROJECT';
|
|
2164
|
+
throw err;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2124
2167
|
// ── HTTP Origin Allowlist & Security Headers ─────────────────────────────────
|
|
2125
2168
|
// Pure helpers used by dashboard.js to gate mutating requests against an
|
|
2126
2169
|
// explicit allowlist of local origins and to attach uniform security response
|
|
@@ -3192,6 +3235,8 @@ module.exports = {
|
|
|
3192
3235
|
sanitizePath,
|
|
3193
3236
|
sanitizeBranch,
|
|
3194
3237
|
buildWorktreeDirName, // exported for testing
|
|
3238
|
+
isPathInsideOrEqual,
|
|
3239
|
+
assertWorktreeOutsideProject,
|
|
3195
3240
|
isLiveCommandCenterPath,
|
|
3196
3241
|
describeCcProtectedPaths,
|
|
3197
3242
|
renderCcSystemPrompt,
|
package/engine.js
CHANGED
|
@@ -625,10 +625,18 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
625
625
|
branchName,
|
|
626
626
|
});
|
|
627
627
|
worktreePath = path.resolve(rootDir, engineConfig.worktreeRoot || '../worktrees', wtDirName);
|
|
628
|
+
// Refuse to spawn into a worktree path that's inside the project root —
|
|
629
|
+
// nested worktrees cause glob/grep to match both copies (mirror writes).
|
|
630
|
+
// Throws on violation; caught by the outer try/catch which fails dispatch.
|
|
631
|
+
shared.assertWorktreeOutsideProject(worktreePath, rootDir);
|
|
628
632
|
|
|
629
633
|
// If branch is already checked out in an existing worktree, reuse it
|
|
630
634
|
const existingWt = await findExistingWorktree(rootDir, branchName);
|
|
631
635
|
if (existingWt) {
|
|
636
|
+
// Same guard for reuse — a previously-created bad worktree must not
|
|
637
|
+
// be silently reused either; the cleanup sweep flags these so the
|
|
638
|
+
// operator can remove them.
|
|
639
|
+
shared.assertWorktreeOutsideProject(existingWt, rootDir);
|
|
632
640
|
worktreePath = existingWt;
|
|
633
641
|
log('info', `Reusing existing worktree for ${branchName}: ${existingWt}`);
|
|
634
642
|
// Probe origin first — locally-created branches that were never pushed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1777",
|
|
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"
|