@yemi33/minions 0.1.2292 → 0.1.2294

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.
@@ -273,6 +273,18 @@ function _fileContentHash(filePath) {
273
273
 
274
274
  function _hydrateScopeFromJson(db, scope) {
275
275
  const jsonItems = _readJsonArrayFallback(scope);
276
+ // Preserve the SQL-only prd_item_id FK across the delete+reinsert. It is owned
277
+ // by the PRD dual-write (prd-store.js#_upsertPrd), NOT represented in the JSON
278
+ // mirror, so a naive rebuild-from-JSON silently zeroes it for the whole scope —
279
+ // regressing the WI↔PRD link every time the JSON diverges (the count would
280
+ // oscillate as the PRD mirror re-stamps and the next hydrate wipes it again).
281
+ // Snapshot before the DELETE, restore onto the rows that survive the rebuild.
282
+ const fkById = new Map();
283
+ try {
284
+ for (const r of db.prepare('SELECT id, prd_item_id FROM work_items WHERE scope = ? AND prd_item_id IS NOT NULL').all(scope)) {
285
+ fkById.set(r.id, r.prd_item_id);
286
+ }
287
+ } catch { /* column absent on a pre-v15 schema — nothing to preserve */ }
276
288
  // DELETE before re-insert: callers that wrote a smaller JSON file
277
289
  // (test cleanup() removing items) must end up with a smaller SQL state.
278
290
  db.prepare('DELETE FROM work_items WHERE scope = ?').run(scope);
@@ -298,6 +310,14 @@ function _hydrateScopeFromJson(db, scope) {
298
310
  now,
299
311
  );
300
312
  }
313
+ // Restore the FK for rows that still exist after the rebuild (a WI dropped
314
+ // from the JSON is gone, and its link goes with it — correct).
315
+ if (fkById.size) {
316
+ const restore = db.prepare('UPDATE work_items SET prd_item_id = ? WHERE scope = ? AND id = ?');
317
+ for (const [id, fk] of fkById) {
318
+ try { restore.run(fk, scope, id); } catch { /* best effort */ }
319
+ }
320
+ }
301
321
  }
302
322
 
303
323
  function applyWorkItemsMutation(scope, mutator) {
@@ -375,4 +395,5 @@ module.exports = {
375
395
  dropScope,
376
396
  _filePathForScope,
377
397
  _mirrorJsonFromSql,
398
+ _hydrateScopeFromJson, // exported for testing (prd_item_id-preservation)
378
399
  };
package/engine.js CHANGED
@@ -168,7 +168,7 @@ const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, reconci
168
168
  syncPrsFromOutput, updatePrAfterReview, updatePrAfterFix, checkForLearnings, extractSkillsFromOutput,
169
169
  updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs, persistVerifyPrsToPrd,
170
170
  isItemCompleted, classifyFailure: classifyFailureFallback, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemPath,
171
- mergeArtifactNotes, promoteCompletionArtifacts, pruneScopeMismatchDuplicatePrs } = require('./engine/lifecycle');
171
+ mergeArtifactNotes, promoteCompletionArtifacts, pruneScopeMismatchDuplicatePrs, collapseAllDuplicatePrRecords } = require('./engine/lifecycle');
172
172
 
173
173
  // ─── Diagnostics: memory + event-loop + GC sampler (P-a1b2c3d4 / P-b2c3d4e5) ─
174
174
 
@@ -196,9 +196,15 @@ function cleanupTempAgent(agentId) {
196
196
  try {
197
197
  const agentDir = path.join(AGENTS_DIR, agentId);
198
198
  // Keep output archive but remove temp agent directory (live-output.log etc.)
199
- fs.rmSync(agentDir, { recursive: true, force: true });
199
+ // Use _retryFsOp so Windows EBUSY/EPERM (AV / file indexer lock) is retried.
200
+ shared._retryFsOp(
201
+ () => fs.rmSync(agentDir, { recursive: true, force: true }),
202
+ `cleanupTempAgent(${agentId})`
203
+ );
200
204
  log('info', `Temp agent ${agentId} cleaned up`);
201
- } catch { /* cleanup */ }
205
+ } catch (err) {
206
+ log('warn', `cleanupTempAgent: fs.rmSync failed for ${agentId}: ${err && err.message}`);
207
+ }
202
208
  }
203
209
 
204
210
  // Per-tick cache of refs that failed to fetch — avoids repeating 30s ETIMEDOUT for same missing ref
@@ -974,7 +980,17 @@ async function runWorktreeAdd(rootDir, worktreePath, addArgs, gitOpts, worktreeC
974
980
  await shared.shellSafeGit(['worktree', 'remove', '--force', worktreePath], { ...gitOpts, cwd: rootDir, timeout: 30000 });
975
981
  } catch (rmErr) {
976
982
  log('warn', `runWorktreeAdd: worktree remove after partial checkout failed (${rmErr.message}) — falling back to fs-level cleanup`);
977
- try { fs.rmSync(worktreePath, { recursive: true, force: true }); } catch {}
983
+ try {
984
+ // W-mqw3g5be000s934f: use _retryFsOp so transient Windows EBUSY/EPERM from
985
+ // antivirus or GVFS virtual-filesystem handles do not silently abandon cleanup.
986
+ shared._retryFsOp(
987
+ () => fs.rmSync(worktreePath, { recursive: true, force: true }),
988
+ `runWorktreeAdd: fs-level cleanup ${worktreePath}`,
989
+ { attempts: 3, baseMs: 100 },
990
+ );
991
+ } catch (fsErr) {
992
+ log('warn', `runWorktreeAdd: fs-level cleanup failed after retries (${fsErr.message}) — worktree directory may be leaked: ${worktreePath}`);
993
+ }
978
994
  try { await shared.shellSafeGit(['worktree', 'prune'], { ...gitOpts, cwd: rootDir, timeout: 15000 }); } catch {}
979
995
  }
980
996
  const incompleteErr = new Error(`GVFS incomplete checkout at ${worktreePath}: worktree add exited cleanly but no source files were checked out (only .git was written). Retriable.`);
@@ -2933,10 +2949,10 @@ async function spawnAgent(dispatchItem, config) {
2933
2949
  try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 15000 }); } catch { /* optional */ }
2934
2950
  removeStaleIndexLock(rootDir);
2935
2951
  // Clean up partial worktree directory from the failed -b
2936
- // attempt. Keep this a RAW fs.rmSync: this husk pre-dates
2937
- // `git worktree add`, so git does not yet own it and
2938
- // shared.removeWorktree's `git worktree remove --force` would
2939
- // no-op on it (P-b3d9a162). Two guards before the delete:
2952
+ // attempt. This husk pre-dates `git worktree add`, so git
2953
+ // does not yet own it and shared.removeWorktree's
2954
+ // `git worktree remove --force` would no-op on it
2955
+ // (P-b3d9a162). Two guards before the delete:
2940
2956
  // 1. isWorktreePathLive (fail-open): skip if another
2941
2957
  // dispatch raced onto this path — it returns true on a
2942
2958
  // SQL outage, so we leak the husk rather than nuke a
@@ -2945,6 +2961,7 @@ async function spawnAgent(dispatchItem, config) {
2945
2961
  // carries its own `.git` DIRECTORY (a mis-pointed
2946
2962
  // worktreePath onto a real repo) — a linked worktree's
2947
2963
  // `.git` is a FILE, so this never blocks a real husk.
2964
+ // W-mqw3i9vp000tcfaf: use shared._retryFsOp so Windows EBUSY/EPERM doesn't brick the branch.
2948
2965
  try {
2949
2966
  const _huskGit = path.join(worktreePath, '.git');
2950
2967
  let _huskIsRealRepo = false;
@@ -2952,9 +2969,14 @@ async function spawnAgent(dispatchItem, config) {
2952
2969
  if (fs.existsSync(worktreePath)
2953
2970
  && !_huskIsRealRepo
2954
2971
  && !shared.isWorktreePathLive(worktreePath, { excludeDispatchId: id })) {
2955
- fs.rmSync(worktreePath, { recursive: true, force: true });
2972
+ shared._retryFsOp(
2973
+ () => fs.rmSync(worktreePath, { recursive: true, force: true }),
2974
+ `husk cleanup before -b retry ${branchName}`
2975
+ );
2956
2976
  }
2957
- } catch { /* optional */ }
2977
+ } catch (rmErr) {
2978
+ log('warn', `spawnAgent: husk cleanup failed before -b retry for ${branchName}: ${rmErr.message?.split('\n')[0]}`);
2979
+ }
2958
2980
  try {
2959
2981
  await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, _freshCreateBase], _worktreeGitOpts, 0);
2960
2982
  } catch (e1b) {
@@ -5863,54 +5885,6 @@ function materializePlansAsWorkItems(config) {
5863
5885
  return mutator(current) || current;
5864
5886
  }, { defaultValue: fallback || {}, ...options });
5865
5887
  };
5866
- const declaredProjectPrdFilename = (fileName, projectName) => {
5867
- const match = String(fileName || '').match(/-(\d{4}-\d{2}-\d{2}(?:-\d+)?)\.json$/);
5868
- if (!match) return null;
5869
- return safePrdFilenameForProject(projectName, match[1]);
5870
- };
5871
- const migratePrdFilenameReferences = (oldFileName, newFileName) => {
5872
- if (!oldFileName || !newFileName || oldFileName === newFileName) return 0;
5873
- let migrated = 0;
5874
- const wiPaths = new Set([path.join(MINIONS_DIR, 'work-items.json')]);
5875
- for (const project of getProjects(config)) wiPaths.add(projectWorkItemsPath(project));
5876
- for (const wiPath of wiPaths) {
5877
- if (!fs.existsSync(wiPath)) continue;
5878
- mutateWorkItems(wiPath, (items) => {
5879
- for (const wi of items) {
5880
- if (!wi || typeof wi !== 'object') continue;
5881
- if (wi.sourcePlan === oldFileName) {
5882
- wi.sourcePlan = newFileName;
5883
- migrated++;
5884
- }
5885
- if (wi._artifacts?.sourcePlan === oldFileName) {
5886
- wi._artifacts.sourcePlan = newFileName;
5887
- migrated++;
5888
- }
5889
- }
5890
- return items;
5891
- });
5892
- }
5893
- if (fs.existsSync(DISPATCH_PATH)) {
5894
- mutateDispatch((dispatch) => {
5895
- for (const queue of ['pending', 'active', 'completed']) {
5896
- for (const entry of dispatch[queue] || []) {
5897
- const metaItem = entry?.meta?.item;
5898
- if (!metaItem || typeof metaItem !== 'object') continue;
5899
- if (metaItem.sourcePlan === oldFileName) {
5900
- metaItem.sourcePlan = newFileName;
5901
- migrated++;
5902
- }
5903
- if (metaItem._prdFilename === oldFileName) {
5904
- metaItem._prdFilename = newFileName;
5905
- migrated++;
5906
- }
5907
- }
5908
- }
5909
- return dispatch;
5910
- });
5911
- }
5912
- return migrated;
5913
- };
5914
5888
  const enforceDeclaredPlanProject = (fileName, currentPlan) => {
5915
5889
  if (!currentPlan?.source_plan) return { fileName, plan: currentPlan };
5916
5890
  let declaredProject = '';
@@ -5938,95 +5912,18 @@ function materializePlansAsWorkItems(config) {
5938
5912
  return planData;
5939
5913
  }, { skipWriteIfUnchanged: true });
5940
5914
 
5941
- let nextFileName = fileName;
5942
- const desiredFileName = declaredProjectPrdFilename(fileName, declaredProject);
5943
- if (desiredFileName && desiredFileName.toLowerCase() !== String(fileName).toLowerCase()) {
5944
- const fromPath = path.join(PRD_DIR, fileName);
5945
- const desiredPath = shared.sanitizePath(desiredFileName, PRD_DIR);
5946
- // W-mq8qdai6 refuse to rename into a name owned by an archived PRD.
5947
- // `shared.uniquePath` only scans the live `prd/` directory; without this
5948
- // guard, an archived PRD with the canonical "<project>-<date>.json" name
5949
- // would silently collide with the rename target. The dashboard joins
5950
- // work items to PRDs by basename (sourcePlan === <prd filename>), so a
5951
- // live↔archive name collision bleeds the archived PRD's done items,
5952
- // verify task, and PRs into the live PRD's view (the bug-fix-plan vs
5953
- // minions-opg-2026-06-10 incident).
5954
- const archiveBasenames = new Set(
5955
- safeReadDir(path.join(PRD_DIR, 'archive'))
5956
- .filter(f => f.endsWith('.json'))
5957
- .map(f => f.toLowerCase())
5958
- );
5959
- if (archiveBasenames.has(desiredFileName.toLowerCase())) {
5960
- log('warn', `Plan project enforcement: skipping rename of ${fileName} to ${desiredFileName} — would collide with archived PRD`);
5961
- } else {
5962
- const toPath = shared.uniquePath(desiredPath);
5963
- const toBasename = path.basename(toPath);
5964
- if (archiveBasenames.has(toBasename.toLowerCase())) {
5965
- // uniquePath bumped past a live conflict (e.g. <name>-2.json) but
5966
- // the bumped name itself is owned by an archived PRD — same risk.
5967
- log('warn', `Plan project enforcement: skipping rename of ${fileName} to ${toBasename} — would collide with archived PRD`);
5968
- } else {
5969
- // W-mqfevwrm — Serialize the rename behind file locks on BOTH the
5970
- // source and destination paths so a concurrent `mutateJsonFileLocked`
5971
- // on either name cannot interleave (creating a divergent ghost PRD
5972
- // or losing data). Move the `.backup` sidecar along with the `.json`
5973
- // so the canonical name retains its restore-from-backup safety net
5974
- // — without this, deleting the renamed `.json` (concurrent sweep,
5975
- // plan-completion purge, etc.) leaves no `.backup` under the new
5976
- // name and the PRD is permanently lost (live incident 2026-06-15:
5977
- // `minions-opg-2026-06-10.json` gone, only the OLD-name `.backup`
5978
- // survived, all 9 WIs orphaned). The OLD-name `.backup` is removed
5979
- // so it cannot resurrect a ghost PRD on a future read of the OLD
5980
- // path (W-mouptdh1000h9f39-style landmine).
5981
- try {
5982
- withFileLock(`${fromPath}.lock`, () => {
5983
- withFileLock(`${toPath}.lock`, () => {
5984
- // Re-verify under both locks — another tick may have raced
5985
- // ahead and either deleted the source or claimed the dest.
5986
- if (!fs.existsSync(fromPath)) {
5987
- log('warn', `Plan project enforcement: source ${fileName} disappeared before rename (raced)`);
5988
- return;
5989
- }
5990
- if (fs.existsSync(toPath)) {
5991
- log('warn', `Plan project enforcement: destination ${toBasename} appeared before rename (raced) — skipping`);
5992
- return;
5993
- }
5994
- fs.renameSync(fromPath, toPath);
5995
- // Move the `.backup` sidecar along with the primary `.json`
5996
- // so safeJson's restore-from-backup path stays viable under
5997
- // the new canonical name. Both ops are best-effort: missing
5998
- // `.backup` is normal (nothing to move) and an unlink failure
5999
- // on the OLD-name `.backup` is logged but non-fatal.
6000
- const fromBackup = `${fromPath}.backup`;
6001
- const toBackup = `${toPath}.backup`;
6002
- if (fs.existsSync(fromBackup)) {
6003
- try {
6004
- fs.renameSync(fromBackup, toBackup);
6005
- } catch (be) {
6006
- // Fall back to copy + unlink if rename across the same dir
6007
- // surfaces a Windows EPERM (AV/Search Indexer hold).
6008
- try {
6009
- fs.copyFileSync(fromBackup, toBackup);
6010
- try { fs.unlinkSync(fromBackup); } catch { /* best-effort */ }
6011
- } catch (ce) {
6012
- log('warn', `Plan project enforcement: could not move .backup sidecar ${fileName}.backup → ${toBasename}.backup: ${be.message} / ${ce.message}`);
6013
- }
6014
- }
6015
- }
6016
- nextFileName = toBasename;
6017
- const migrated = migratePrdFilenameReferences(fileName, nextFileName);
6018
- if (migrated > 0) log('info', `Plan project enforcement: migrated ${migrated} PRD reference(s) from ${fileName} to ${nextFileName}`);
6019
- changed = true;
6020
- });
6021
- });
6022
- } catch (e) {
6023
- log('warn', `Plan project enforcement: could not rename ${fileName} to ${toBasename}: ${e.message}`);
6024
- }
6025
- }
6026
- }
6027
- }
6028
- if (changed) log('info', `Plan project enforcement: preserved declared project "${declaredProject}" for ${nextFileName}`);
6029
- return { fileName: nextFileName, plan: normalizedPlan };
5915
+ // Phase 10 step 4.3 — the PRD filename is NO LONGER renamed to the canonical
5916
+ // "<project>-<date>.json". That rename existed only to keep the basename
5917
+ // identity (sourcePlan === <prd filename>) and the live↔archive collision
5918
+ // guard (footgun #7) working. With the surrogate-key model — archive is an
5919
+ // in-place flag (no move) and the WI↔PRD-item join is the stable SQL FK
5920
+ // (work_items.prd_item_id), not the basename the rename is dead weight and
5921
+ // its own machinery (the `.backup` sidecar move, migratePrdFilenameReferences,
5922
+ // the rename-race window that prd-rename-race.test.js guarded) is retired.
5923
+ // The SEMANTIC enforcement above (declared project on the PRD + its features)
5924
+ // still runs; only the file-move is gone.
5925
+ if (changed) log('info', `Plan project enforcement: preserved declared project "${declaredProject}" for ${fileName}`);
5926
+ return { fileName, plan: normalizedPlan };
6030
5927
  };
6031
5928
 
6032
5929
  // Enforce: PRDs must be .json — auto-rename .md files that contain valid PRD JSON
@@ -6420,6 +6317,19 @@ function materializePlansAsWorkItems(config) {
6420
6317
  log('info', `Re-opened work item ${itemId} in ${rProjName} (cross-project, PRD item set to ${rItem.status})`);
6421
6318
  }
6422
6319
 
6320
+ // Stamp workItemId onto each newly created PRD item immediately after WI creation (P-b3c2d4e5).
6321
+ // Uses mutatePrdLocked so the write is protected by a file lock — not a bare safeWrite.
6322
+ if (newlyCreatedIds.size > 0) {
6323
+ try {
6324
+ mutatePrdLocked(file, plan, (current) => {
6325
+ for (const feature of (current.missing_features || [])) {
6326
+ if (newlyCreatedIds.has(feature.id)) feature.workItemId = feature.id;
6327
+ }
6328
+ return current;
6329
+ }, { skipWriteIfUnchanged: true });
6330
+ } catch (e) { log('warn', `workItemId stamp failed for ${file}: ${e.message}`); }
6331
+ }
6332
+
6423
6333
  totalCreated += created;
6424
6334
  }
6425
6335
 
@@ -8314,6 +8224,45 @@ function normalizeAc(ac) {
8314
8224
  * @param {string} [options.workType] - Work type (used for ASK-specific vars)
8315
8225
  * @returns {{ needsReview: boolean, checkpointCount: number|null }} checkpoint side-effect info
8316
8226
  */
8227
+ /**
8228
+ * SHERLOC (P-mqyp0008v022w3x4): detect prior completed explore WI that
8229
+ * references this fix WI via its `references` array. Returns the explore
8230
+ * WI's `resultSummary` string, or '' when none found.
8231
+ *
8232
+ * Lookup: scan all work items for type=explore, status=done, non-empty
8233
+ * resultSummary, and a reference whose URL includes the fix WI's ID.
8234
+ * The explore WI is expected to have been dispatched as a pre-localization
8235
+ * step and to carry a back-reference to the fix WI it was researching.
8236
+ *
8237
+ * @param {object} item - the fix work item being dispatched
8238
+ * @param {object} config - engine config (passed to getWorkItems)
8239
+ * @returns {string}
8240
+ */
8241
+ function resolvePriorExploreContext(item, config) {
8242
+ if (!item || !item.id) return '';
8243
+ try {
8244
+ const allItems = queries.getWorkItems(config, { enrich: false });
8245
+ const candidates = allItems.filter(wi =>
8246
+ wi.type === WORK_TYPE.EXPLORE &&
8247
+ DONE_STATUSES.has(wi.status) &&
8248
+ wi.resultSummary &&
8249
+ Array.isArray(wi.references) &&
8250
+ wi.references.some(r => r && typeof r.url === 'string' && r.url.includes(item.id))
8251
+ );
8252
+ if (candidates.length === 0) return '';
8253
+ // Prefer most recently completed; fall back to most recently created.
8254
+ candidates.sort((a, b) => {
8255
+ const ta = a.completedAt || a.created || '';
8256
+ const tb = b.completedAt || b.created || '';
8257
+ return tb.localeCompare(ta);
8258
+ });
8259
+ return candidates[0].resultSummary || '';
8260
+ } catch (e) {
8261
+ log('warn', `resolvePriorExploreContext for ${item.id}: ${e.message}`);
8262
+ return '';
8263
+ }
8264
+ }
8265
+
8317
8266
  function buildWorkItemDispatchVars(item, vars, config, options = {}) {
8318
8267
  const { worktreePath, includeNotes = true, workType } = options;
8319
8268
 
@@ -8370,6 +8319,13 @@ function buildWorkItemDispatchVars(item, vars, config, options = {}) {
8370
8319
  vars.notes_content = 'See the **Team Notes** section above for the latest shared team context.';
8371
8320
  }
8372
8321
 
8322
+ // SHERLOC (P-mqyp0008v022w3x4): inject prior explore context for fix dispatches.
8323
+ // When a completed explore WI has a reference to this fix WI, surface its
8324
+ // resultSummary so the fix agent starts with pre-localized fault context.
8325
+ if (workType === WORK_TYPE.FIX) {
8326
+ vars.prior_explore_context = resolvePriorExploreContext(item, config);
8327
+ }
8328
+
8373
8329
  // Resolve implicit context references (e.g., "ripley's plan", "the latest plan")
8374
8330
  const resolvedCtx = resolveTaskContext(item, config);
8375
8331
  if (resolvedCtx.additionalContext) {
@@ -9947,6 +9903,15 @@ async function tickInner() {
9947
9903
  log('warn', `[pull-requests] scope-mismatch sweep error: ${err?.message || err}`);
9948
9904
  }
9949
9905
 
9906
+ // P-e9f0a2b4 — Collapse prNumber duplicates (different canonical IDs, same
9907
+ // prNumber) that accumulated before upsert-time dedup was added. One mutation
9908
+ // per affected scope per reconcile tick; normally a no-op after first run.
9909
+ try {
9910
+ collapseAllDuplicatePrRecords(config);
9911
+ } catch (err) {
9912
+ log('warn', `[pull-requests] collapse-duplicates sweep error: ${err?.message || err}`);
9913
+ }
9914
+
9950
9915
  // P-6b3a9f78 — Shared-branch PR reconciler (recovery sweep). Backfills the
9951
9916
  // engine PR store for shared-branch plans whose aggregate remote PR exists
9952
9917
  // but is untracked / under-linked, so the dashboard renders the single
@@ -10403,15 +10368,12 @@ async function tickInner() {
10403
10368
  } else {
10404
10369
  // P-a3f9b205: surface the per-project live-mode gate. Order matters:
10405
10370
  // max_concurrency / agent_busy / branch_locked are all more specific
10406
- // and win when both apply. Also check the pending item's own resolved
10407
- // checkout mode so worktree-mode items on hybrid projects are not
10408
- // falsely annotated live_checkout_busy (mirrors seeding guard above).
10371
+ // and win when both apply.
10409
10372
  const itemProjName = item.project || item.meta?.project?.name || null;
10410
10373
  if (
10411
10374
  itemProjName
10412
10375
  && !READ_ONLY_ROOT_TASK_TYPES.has(item.type)
10413
10376
  && postLiveProjectsInUse.has(itemProjName)
10414
- && shared.resolveCheckoutMode(shared.findProjectByName(shared.getProjects(config), itemProjName), item.type) === 'live'
10415
10377
  ) {
10416
10378
  reason = 'live_checkout_busy';
10417
10379
  }
@@ -10607,6 +10569,8 @@ module.exports = {
10607
10569
 
10608
10570
  // Shared helpers (used by lifecycle.js and tests)
10609
10571
  reconcileItemsWithPrs, detectDependencyCycles,
10572
+ safePrdProjectSlug, safePrdFilenameForProject, // exported for testing (W-mqyn7joo0004079f)
10573
+ isSoftFixDispatch, // exported for testing (W-mqyn7joo0004079f)
10610
10574
  areDependenciesMet, // exported for testing (P-bf04-decompose-zero-children)
10611
10575
  parseConflictFiles, pruneAncestorDeps, preflightMergeSimulation, // exported for testing
10612
10576
  resolveDependencyBranches, buildCrossRepoDepsSection, // exported for testing (P-faea3206)
@@ -10627,6 +10591,7 @@ module.exports = {
10627
10591
 
10628
10592
  // Playbooks
10629
10593
  renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS, buildWorkItemDispatchVars,
10594
+ resolvePriorExploreContext, // exported for testing (P-mqyp0008v022w3x4 SHERLOC)
10630
10595
  renderProjectWorkItemPromptForAgent, // exported for testing
10631
10596
 
10632
10597
  // Timeout / Steering / Idle (re-exported from engine/timeout.js)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2292",
3
+ "version": "0.1.2294",
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"
package/playbooks/fix.md CHANGED
@@ -19,6 +19,15 @@ When `{{pr_id}}`/`{{pr_branch}}` identify an existing pull request, this is a fi
19
19
 
20
20
  {{checkpoint_context}}
21
21
 
22
+ {{#prior_explore_context}}
23
+ ## Exploration Context
24
+
25
+ A prior explore work item investigated this issue. Use this context as your starting point for fault localization:
26
+
27
+ {{prior_explore_context}}
28
+
29
+ {{/prior_explore_context}}
30
+
22
31
  ## Review Findings to Address
23
32
 
24
33
  {{review_note}}