@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 CHANGED
@@ -1,16 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1775 (2026-05-07)
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
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-07T21:35:37.002Z"
4
+ "cachedAt": "2026-05-07T23:06:07.639Z"
5
5
  }
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.1775",
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"