@yemi33/minions 0.1.1775 → 0.1.1776
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 +6 -5
- package/engine/cleanup.js +27 -2
- package/engine/copilot-models.json +1 -1
- package/engine/shared.js +45 -0
- package/engine.js +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1776 (2026-05-07)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- refuse worktree paths nested in any project root
|
|
7
|
+
|
|
8
|
+
## 0.1.1774 (2026-05-07)
|
|
4
9
|
|
|
5
10
|
### Features
|
|
6
|
-
- Improve dashboard API route metadata
|
|
7
11
|
- suppress stale doc-chat model errors
|
|
8
12
|
|
|
9
13
|
### Fixes
|
|
10
14
|
- yemi33/minions#2168
|
|
11
|
-
- honor central plan work item project
|
|
12
|
-
- restore canonical-home shared helpers
|
|
13
|
-
- yemi33/minions#2170
|
|
14
15
|
|
|
15
16
|
## 0.1.1772 (2026-05-07)
|
|
16
17
|
|
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/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.1776",
|
|
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"
|