@yemi33/minions 0.1.1985 → 0.1.1986

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/engine.js CHANGED
@@ -586,7 +586,16 @@ async function syncReusedWorktree(rootDir, worktreePath, branchName, gitOpts = {
586
586
  async function findExistingWorktree(repoDir, branchName) {
587
587
  try {
588
588
  const out = await shared.shellSafeGit(['worktree', 'list', '--porcelain'], { cwd: repoDir, timeout: 10000 });
589
- const found = shared.parseWorktreePorcelain(out).find(w => w.branch === branchName);
589
+ // Skip worktrees at or inside `repoDir` itself: the main checkout (always
590
+ // first in the porcelain output) sits AT repoDir, and nested worktrees
591
+ // under it would trip assertWorktreeOutsideProject downstream anyway.
592
+ // Returning either as "reusable" creates an unresolvable preflight
593
+ // rejection loop when the branch happens to be checked out at the
594
+ // project root (common when an operator works on the minions repo
595
+ // locally while the engine tries to review that same branch).
596
+ const found = shared.parseWorktreePorcelain(out).find(w =>
597
+ w.branch === branchName && !shared.isPathInsideOrEqual(w.path, repoDir)
598
+ );
590
599
  if (found && fs.existsSync(found.path)) return found.path;
591
600
  } catch (e) { log('warn', 'git: ' + e.message); }
592
601
  return null;
@@ -926,14 +935,34 @@ async function spawnAgent(dispatchItem, config) {
926
935
  : taskPromptWithReport;
927
936
  };
928
937
  let fullTaskPrompt = buildFullTaskPrompt(taskPrompt);
929
- const tmpDir = path.join(ENGINE_DIR, 'tmp');
930
- if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
931
938
  const safeId = id.replace(/[:\\/*?"<>|]/g, '-');
932
- const promptPath = path.join(tmpDir, `prompt-${safeId}.md`);
939
+ // P-f6-tmp-toctou: per-dispatch unique tmp dir closes the symlink-plant
940
+ // window on engine/tmp/prompt-<id>.md (mkdtempSync + chmod 0o700). Filename
941
+ // pattern inside the dir is unchanged so spawn-agent.js's prompt→pid regex
942
+ // derivation still works.
943
+ const dispatchTmpDir = shared.createDispatchTmpDir(id);
944
+ const promptPath = path.join(dispatchTmpDir, `prompt-${safeId}.md`);
933
945
  safeWrite(promptPath, fullTaskPrompt);
934
- const sysPromptPath = path.join(tmpDir, `sysprompt-${safeId}.md`);
946
+ const sysPromptPath = path.join(dispatchTmpDir, `sysprompt-${safeId}.md`);
935
947
  safeWrite(sysPromptPath, systemPrompt);
936
- const _cleanupPromptFiles = () => { safeUnlink(promptPath); safeUnlink(sysPromptPath); };
948
+ // Stamp the tmpDir on the dispatch entry so cleanup/orphan-reap/kill paths
949
+ // (dispatch.js, meeting.js, cli.js, cleanup.js, timeout.js) can find the
950
+ // dir via shared.dispatchPidCandidates / findDispatchPidFile. Best-effort —
951
+ // backward-compat fallbacks still scan dispatch-<safeId>-* dirs by id.
952
+ try {
953
+ dispatchItem.tmpDir = dispatchTmpDir;
954
+ const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
955
+ mutateJsonFileLocked(dispatchPath, (dispatch) => {
956
+ for (const queue of ['pending', 'active', 'completed']) {
957
+ const arr = Array.isArray(dispatch?.[queue]) ? dispatch[queue] : null;
958
+ if (!arr) continue;
959
+ const found = arr.find(d => d && d.id === id);
960
+ if (found) found.tmpDir = dispatchTmpDir;
961
+ }
962
+ return dispatch;
963
+ }, { defaultValue: { pending: [], active: [], completed: [] } });
964
+ } catch (e) { log('warn', `spawnAgent: failed to persist tmpDir for ${id}: ${e.message}`); }
965
+ const _cleanupPromptFiles = () => { shared.removeDispatchTmpDir(dispatchTmpDir); };
937
966
  // Convert a WORKTREE_NESTED_IN_PROJECT throw into a fail-fast non-retryable
938
967
  // dispatch failure (W-mp62taw2000ubcc3). The error's `.code` is set by
939
968
  // shared.assertWorktreeOutsideProject so we don't have to parse the message.
@@ -1834,6 +1863,9 @@ async function spawnAgent(dispatchItem, config) {
1834
1863
  if (pidFilePath) {
1835
1864
  try { safeUnlink(pidFilePath); } catch { /* may not exist yet */ }
1836
1865
  }
1866
+ // P-f6-tmp-toctou: rm the per-dispatch tmp dir too so prompt/sysprompt
1867
+ // sidecars don't leak when runFile throws before onAgentClose can run.
1868
+ try { _cleanupPromptFiles(); } catch { /* cleanup is best-effort */ }
1837
1869
  cleanupTempAgent(agentId);
1838
1870
  throw spawnErr;
1839
1871
  }
@@ -1974,7 +2006,7 @@ async function spawnAgent(dispatchItem, config) {
1974
2006
  const pendingForResume = steering.buildPendingSteeringPrompt(agentId);
1975
2007
  const steerPromptBody = pendingForResume.prompt || steerMsg;
1976
2008
  const steerPrompt = `Message from your human teammate:\n\n${steerPromptBody}\n\nRespond to this, then continue working on your current task.`;
1977
- const steerPromptPath = path.join(ENGINE_DIR, 'tmp', `prompt-steer-${safeId}.md`);
2009
+ const steerPromptPath = path.join(dispatchTmpDir, `prompt-steer-${safeId}.md`);
1978
2010
  try { safeWrite(steerPromptPath, steerPrompt); } catch (e) {
1979
2011
  log('warn', `Steering: failed to write prompt for ${agentId}: ${e.message}`);
1980
2012
  try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Could not write prompt. Message was: ${steerMsg}\n`); } catch {}
@@ -2832,10 +2864,11 @@ async function spawnAgent(dispatchItem, config) {
2832
2864
  } catch (e) { log('warn', `keep-processes acceptance: failed to set _pendingReason: ${e.message}`); }
2833
2865
  }
2834
2866
 
2835
- // Cleanup temp files (including PID file now that dispatch is complete)
2836
- try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
2837
- try { fs.unlinkSync(promptPath); } catch { /* cleanup */ }
2838
- try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch { /* cleanup */ }
2867
+ // Cleanup temp files (whole per-dispatch dir, including PID/sysprompt
2868
+ // tmp/prompt — P-f6-tmp-toctou). removeDispatchTmpDir validates the path
2869
+ // resolves under engine/tmp/dispatch-* before rmSync, so a corrupted
2870
+ // dispatch.json field can't redirect this to an arbitrary path.
2871
+ try { shared.removeDispatchTmpDir(dispatchTmpDir); } catch { /* cleanup */ }
2839
2872
 
2840
2873
  log('info', `Agent ${agentId} completed. Output saved to ${archivePath}`);
2841
2874
 
@@ -5028,10 +5061,19 @@ function materializeSpecsAsWorkItems(config, project) {
5028
5061
  let recentSpecs = [];
5029
5062
  for (const pattern of filePatterns) {
5030
5063
  try {
5031
- const result = exec(
5032
- `git log --diff-filter=AM --name-only --pretty=format:"COMMIT:%H|%s" --since="${sinceDate}" -- "${pattern}"`,
5033
- { cwd: root, encoding: 'utf8', timeout: 10000 }
5034
- ).trim();
5064
+ // P-f7-git-log: argv form via execFileSync (shell:false) so file patterns
5065
+ // from operator config.json reach git as literal pathspec args, never as
5066
+ // shell tokens. Spread the pattern after the '--' pathspec separator.
5067
+ const args = [
5068
+ 'log',
5069
+ '--diff-filter=AM',
5070
+ '--name-only',
5071
+ '--pretty=format:COMMIT:%H|%s',
5072
+ `--since=${sinceDate}`,
5073
+ '--',
5074
+ pattern,
5075
+ ];
5076
+ const result = shared.shellSafeGitSync(args, { cwd: root, timeout: 10000 }).trim();
5035
5077
  if (!result) continue;
5036
5078
 
5037
5079
  let currentCommit = null;
@@ -6539,6 +6581,7 @@ module.exports = {
6539
6581
  // Discovery
6540
6582
  discoverWork, discoverFromPrs, discoverFromWorkItems, discoverCentralWorkItems,
6541
6583
  materializePlansAsWorkItems,
6584
+ materializeSpecsAsWorkItems, // exported for testing (P-f7-git-log)
6542
6585
  reservePrdFilename, // exported for testing (P-9b7e5d3c)
6543
6586
  sweepStaleArchivedPrdBackups, // exported for testing
6544
6587
 
@@ -6548,6 +6591,7 @@ module.exports = {
6548
6591
  buildDepConflictFixItem, // exported for testing (W-mpcwojgr000a0244)
6549
6592
  isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, // exported for testing
6550
6593
  pruneStaleWorktreeForBranch, // exported for testing
6594
+ findExistingWorktree, // exported for testing
6551
6595
  _maxTurnsForType, buildProjectContext, normalizeAc, _buildAgentSpawnFlags, _classifyAgentFailure, // exported for testing
6552
6596
  promoteCheckpointSteeringForClose, // exported for testing
6553
6597
  normalizePrBranch, resolvePrBranch, prCausePart, getPrCauseHead, getPrCauseBase, getPrAutomationCauseKey, getPrAutomationDispatchKey, // exported for testing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1985",
3
+ "version": "0.1.1986",
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"