@yemi33/minions 0.1.1780 → 0.1.1782

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1782 (2026-05-07)
4
+
5
+ ### Other
6
+ - refactor(worktree): consolidate porcelain parsers behind shared helper
7
+
8
+ ## 0.1.1781 (2026-05-07)
9
+
10
+ ### Fixes
11
+ - plug Property-1 holes + consolidate isPathInside helper
12
+
3
13
  ## 0.1.1780 (2026-05-07)
4
14
 
5
15
  ### Features
package/engine/cleanup.js CHANGED
@@ -68,11 +68,13 @@ function isProtectedLocalBranch(branch, project = {}) {
68
68
 
69
69
  function localBranchWorktreeInUse(root, branch) {
70
70
  try {
71
- const out = String(shared.execSilent('git worktree list --porcelain', {
71
+ const raw = String(shared.execSilent('git worktree list --porcelain', {
72
72
  cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 10000, windowsHide: true,
73
73
  }) || '');
74
- return out.split(/\r?\n/).some(line => line.trim() === `branch refs/heads/${branch}`);
74
+ return shared.parseWorktreePorcelain(raw).some(w => w.branch === branch);
75
75
  } catch {
76
+ // Defensive: assume "in use" so the caller doesn't proceed to delete a
77
+ // branch when worktree state can't be enumerated.
76
78
  return true;
77
79
  }
78
80
  }
@@ -369,25 +371,33 @@ async function runCleanup(config, verbose = false) {
369
371
  // 2b. Detect git worktrees registered inside any linked project's working tree.
370
372
  // Nested worktrees cause glob/grep tools running with cwd=projectRoot to match
371
373
  // 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).
374
+ // change to both locations, producing mirror-write leaks.
373
375
  // We only WARN here — removing someone else's worktree without consent could
374
376
  // destroy in-flight work. The operator runs `git worktree remove <path>`.
375
377
  cleaned.nestedWorktrees = 0;
378
+ const _scannedRoots = new Set(); // dedup projects sharing localPath
376
379
  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
+ if (!project.localPath) continue;
381
+ const root = path.resolve(project.localPath);
382
+ if (_scannedRoots.has(root)) continue;
383
+ _scannedRoots.add(root);
384
+ if (!fs.existsSync(root)) {
385
+ // Configured project whose checkout has been moved or deleted — surface
386
+ // it so the operator knows their config is out of sync. A missing root
387
+ // means we cannot scan it, leaving nested worktrees there undetectable.
388
+ log('warn', `Project "${project.name || root}" has localPath "${root}" which does not exist on disk — skipping worktree scan`);
389
+ continue;
390
+ }
391
+ let trees;
380
392
  try {
381
- raw = String(shared.execSilent('git worktree list --porcelain', { cwd: root, timeout: 10000, windowsHide: true }) || '');
393
+ const raw = String(shared.execSilent('git worktree list --porcelain', { cwd: root, timeout: 10000, windowsHide: true }) || '');
394
+ trees = shared.parseWorktreePorcelain(raw);
382
395
  } 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;
396
+ for (const wt of trees) {
397
+ // Strict — main worktree (equal path) is expected, descendants are the leak.
398
+ if (!shared.isPathInside(wt.path, root)) continue;
389
399
  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}"`);
400
+ log('warn', `Nested worktree in project "${project.name || root}": "${wt.path}" is inside "${root}". This causes glob tools to match both copies and produces mirror writes. Run: git worktree remove "${wt.path}"`);
391
401
  }
392
402
  }
393
403
 
@@ -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:40:59.915Z"
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,76 @@ 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
+
2180
+ /**
2181
+ * Parse `git worktree list --porcelain` output. Pure — callers run the
2182
+ * subprocess (sync `execSilent` or async `execAsync`) and feed stdout in.
2183
+ *
2184
+ * Returns `[{path, head, branch, bare, detached, locked, prunable}]`.
2185
+ * `branch` strips the `refs/heads/` prefix; flag fields default to false.
2186
+ *
2187
+ * Porcelain format: each worktree is a block of `key value\n` lines
2188
+ * terminated by a blank line. First line of every block is `worktree <path>`
2189
+ * (always present). Optional follow-ups: `HEAD <sha>`, `branch refs/heads/<name>`,
2190
+ * and the flag lines `bare`, `detached`, `locked [<reason>]`, `prunable [<reason>]`.
2191
+ */
2192
+ function parseWorktreePorcelain(raw) {
2193
+ const trees = [];
2194
+ if (!raw) return trees;
2195
+ let current = null;
2196
+ const flush = () => { if (current) { trees.push(current); current = null; } };
2197
+ for (const line of String(raw).split(/\r?\n/)) {
2198
+ if (line === '') { flush(); continue; }
2199
+ if (line.startsWith('worktree ')) {
2200
+ flush();
2201
+ current = { path: line.slice('worktree '.length).trim(), head: '', branch: '', bare: false, detached: false, locked: false, prunable: false };
2202
+ continue;
2203
+ }
2204
+ if (!current) continue;
2205
+ if (line.startsWith('HEAD ')) current.head = line.slice('HEAD '.length).trim();
2206
+ else if (line.startsWith('branch ')) current.branch = line.slice('branch '.length).trim().replace(/^refs\/heads\//, '');
2207
+ else if (line === 'bare') current.bare = true;
2208
+ else if (line === 'detached') current.detached = true;
2209
+ else if (line === 'locked' || line.startsWith('locked ')) current.locked = true;
2210
+ else if (line === 'prunable' || line.startsWith('prunable ')) current.prunable = true;
2211
+ }
2212
+ flush();
2213
+ return trees;
2214
+ }
2215
+
2170
2216
  /**
2171
2217
  * Throws when `worktreePath` would land inside (or equal) `projectRoot`.
2172
2218
  * Called by the engine spawn path before `git worktree add`, and by the
@@ -3257,7 +3303,9 @@ module.exports = {
3257
3303
  sanitizePath,
3258
3304
  sanitizeBranch,
3259
3305
  buildWorktreeDirName, // exported for testing
3306
+ isPathInside,
3260
3307
  isPathInsideOrEqual,
3308
+ parseWorktreePorcelain,
3261
3309
  assertWorktreeOutsideProject,
3262
3310
  isLiveCommandCenterPath,
3263
3311
  describeCcProtectedPaths,
package/engine.js CHANGED
@@ -474,20 +474,8 @@ async function syncReusedWorktree(rootDir, worktreePath, branchName, gitOpts = {
474
474
  async function findExistingWorktree(repoDir, branchName) {
475
475
  try {
476
476
  const out = await execAsync(`git worktree list --porcelain`, { cwd: repoDir, timeout: 10000 });
477
- const branchRef = `branch refs/heads/${branchName}`;
478
- const lines = out.split('\n');
479
- for (let i = 0; i < lines.length; i++) {
480
- if (lines[i].trim() === branchRef) {
481
- // Walk back to find the worktree path
482
- for (let j = i - 1; j >= 0; j--) {
483
- if (lines[j].startsWith('worktree ')) {
484
- const wtPath = lines[j].slice('worktree '.length).trim();
485
- if (fs.existsSync(wtPath)) return wtPath;
486
- break;
487
- }
488
- }
489
- }
490
- }
477
+ const found = shared.parseWorktreePorcelain(out).find(w => w.branch === branchName);
478
+ if (found && fs.existsSync(found.path)) return found.path;
491
479
  } catch (e) { log('warn', 'git: ' + e.message); }
492
480
  return null;
493
481
  }
@@ -666,6 +654,7 @@ async function spawnAgent(dispatchItem, config) {
666
654
  if (eShared.message?.includes('already used by worktree') || eShared.message?.includes('already checked out')) {
667
655
  const existingWtPath = await findExistingWorktree(rootDir, branchName);
668
656
  if (existingWtPath && fs.existsSync(existingWtPath)) {
657
+ shared.assertWorktreeOutsideProject(existingWtPath, rootDir);
669
658
  log('info', `Shared branch ${branchName} already checked out at ${existingWtPath} — reusing`);
670
659
  worktreePath = existingWtPath;
671
660
  } else { throw eShared; }
@@ -723,6 +712,7 @@ async function spawnAgent(dispatchItem, config) {
723
712
  log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
724
713
  throw e2;
725
714
  }
715
+ shared.assertWorktreeOutsideProject(existingWtPath, rootDir);
726
716
  log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
727
717
  worktreePath = existingWtPath;
728
718
  } 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.1782",
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"