@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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1781 (2026-05-07)
4
+
5
+ ### Fixes
6
+ - plug Property-1 holes + consolidate isPathInside helper
7
+
3
8
  ## 0.1.1780 (2026-05-07)
4
9
 
5
10
  ### Features
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 "mirror dirty file" leaks (W-cc-doc-chat-continuity).
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
- const root = project.localPath ? path.resolve(project.localPath) : null;
378
- if (!root || !fs.existsSync(root)) continue;
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 (path.resolve(wt) === root) continue; // main worktree expected
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
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-07T23:26:31.525Z"
4
+ "cachedAt": "2026-05-07T23:33:01.432Z"
5
5
  }
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(root, resolved)) return null;
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(realRoot, realPath)) {
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,
@@ -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 (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.
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 the same as or nested within `parentPath`. Uses
2147
- * `path.relative` so it's cross-platform and resilient to mixed separators
2148
- * (Windows worktree paths often arrive with forward slashes from git output).
2149
- * Returns false when paths refer to different roots/drives or `childPath`
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 isPathInsideOrEqual(childPath, parentPath) {
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 true;
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.1780",
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"