@yemi33/minions 0.1.2286 → 0.1.2287

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
@@ -849,6 +849,7 @@ async function findExistingWorktree(repoDir, branchName) {
849
849
  }
850
850
 
851
851
  function isWorktreeRetryableError(err) {
852
+ if (err?._incompleteCheckout) return true;
852
853
  const msg = String(err?.message || '');
853
854
  return msg.includes('ETIMEDOUT')
854
855
  || msg.includes('index.lock')
@@ -957,6 +958,29 @@ async function runWorktreeAdd(rootDir, worktreePath, addArgs, gitOpts, worktreeC
957
958
  log('warn', `Retrying git worktree add (attempt ${attempt + 1}/${retries + 1}) for ${path.basename(worktreePath)}`);
958
959
  }
959
960
  await shared.shellSafeGit(['worktree', 'add', worktreePath, ...addArgs], { ...gitOpts, cwd: rootDir });
961
+ // W-mqvaxv65000m76f2 (issue #430, Layer 1) — GVFS partial-checkout guard.
962
+ // On large GVFS repos (e.g. 1JS with 296K files), `git worktree add`
963
+ // exits cleanly but only writes a .git file — no source files are checked
964
+ // out. The engine would otherwise treat the empty worktree as valid for
965
+ // reuse; the subsequent `git status` preflight fails (nothing to stat) and
966
+ // the dispatch loop recurs. Detect the empty checkout and throw a retriable
967
+ // error so the retry loop re-creates the worktree on the next attempt.
968
+ let worktreeEntries;
969
+ try { worktreeEntries = fs.readdirSync(worktreePath).filter(e => e !== '.git'); }
970
+ catch { worktreeEntries = null; } // If the read fails, skip the check — don't block normal cases.
971
+ if (worktreeEntries !== null && worktreeEntries.length === 0) {
972
+ log('warn', `runWorktreeAdd: GVFS partial checkout detected at ${worktreePath} (only .git written) — removing and retrying`);
973
+ try {
974
+ await shared.shellSafeGit(['worktree', 'remove', '--force', worktreePath], { ...gitOpts, cwd: rootDir, timeout: 30000 });
975
+ } catch (rmErr) {
976
+ 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 {}
978
+ try { await shared.shellSafeGit(['worktree', 'prune'], { ...gitOpts, cwd: rootDir, timeout: 15000 }); } catch {}
979
+ }
980
+ 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.`);
981
+ incompleteErr._incompleteCheckout = true;
982
+ throw incompleteErr;
983
+ }
960
984
  // W-mqecdoot — stamp the engine ownership marker so the out-of-root
961
985
  // worktree GC can distinguish a worktree we created from one a human
962
986
  // developer made by hand. Best-effort; never blocks the spawn.
@@ -5992,8 +6016,10 @@ function materializePlansAsWorkItems(config) {
5992
6016
  let planFiles;
5993
6017
  try { planFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json')); } catch { return; }
5994
6018
 
5995
- // Regex for detecting sequential PRD item IDs (P-001, P-002) — hoisted outside loop
5996
- const SEQUENTIAL_ID_RE = /^P-?\d+$/;
6019
+ // Regex for detecting sequential PRD item IDs (P-001, M001, M-001, WI-001, …) — hoisted outside loop.
6020
+ // Covers all patterns where an alphabetic prefix is followed by an optional dash and pure digits,
6021
+ // e.g. P001, P-001, M001, M-001, WI-001. Does NOT match hex UUIDs like P-a3f9b2c1.
6022
+ const SEQUENTIAL_ID_RE = /^[A-Za-z]+-?\d+$/;
5997
6023
 
5998
6024
  for (let file of planFiles) {
5999
6025
  // safeJsonNoRestore — if a PRD was archived between readdir and this
@@ -6211,7 +6237,9 @@ function materializePlansAsWorkItems(config) {
6211
6237
  mutateWorkItems(wiPath, existingItems => {
6212
6238
  for (const item of projItems) {
6213
6239
  // Re-open: 'updated' or 'missing' re-opens a done work item (#906)
6214
- const existingWi = existingItems.find(w => w.id === item.id);
6240
+ // Use composite (sourcePlan, id) key so a WI from another PRD with the
6241
+ // same id is not treated as already-materialized for this PRD.
6242
+ const existingWi = existingItems.find(w => w.id === item.id && w.sourcePlan === file);
6215
6243
  const shouldReopen = item.status === PRD_ITEM_STATUS.UPDATED || item.status === PRD_ITEM_STATUS.MISSING;
6216
6244
  if (existingWi && DONE_STATUSES.has(existingWi.status) && shouldReopen) {
6217
6245
  shared.reopenWorkItem(existingWi);
@@ -6222,13 +6250,14 @@ function materializePlansAsWorkItems(config) {
6222
6250
  continue;
6223
6251
  }
6224
6252
 
6225
- // Skip if already materialized — work item ID = PRD item ID, check all projects
6253
+ // Skip if already materialized — use composite (sourcePlan, id) key to prevent
6254
+ // cross-PRD collision when two plans share the same item id (W-mqv6ql5w001w792f).
6226
6255
  let alreadyExists = !!existingWi;
6227
6256
  if (!alreadyExists) {
6228
6257
  for (const p of allProjects) {
6229
6258
  if (String(p.name || '').toLowerCase() === String(projName || '').toLowerCase()) continue;
6230
6259
  const otherItems = safeJsonArr(projectWorkItemsPath(p));
6231
- const otherWi = otherItems.find(w => w.id === item.id);
6260
+ const otherWi = otherItems.find(w => w.id === item.id && w.sourcePlan === file);
6232
6261
  if (otherWi) {
6233
6262
  if (DONE_STATUSES.has(otherWi.status) && shouldReopen) {
6234
6263
  deferredReopens.push({ itemId: item.id, projectName: p.name, item });
@@ -8510,7 +8539,7 @@ function discoverCentralWorkItems(config) {
8510
8539
  const itemProjectResolution = shared.resolveConfiguredProject(item.project, projects);
8511
8540
  if (itemProjectResolution.error) {
8512
8541
  const error = itemProjectResolution.error;
8513
- mutations.set(item.id, { status: WI_STATUS.FAILED, failReason: error, failedAt: ts() });
8542
+ mutations.set(item.id, { status: WI_STATUS.FAILED, failReason: error, failedAt: ts(), _failureClass: FAILURE_CLASS.PROJECT_NOT_FOUND });
8514
8543
  log('warn', `central work item ${item.id}: ${error}`);
8515
8544
  continue;
8516
8545
  }
@@ -8648,6 +8677,7 @@ function discoverCentralWorkItems(config) {
8648
8677
  status: WI_STATUS.FAILED,
8649
8678
  failReason: error,
8650
8679
  failedAt: ts(),
8680
+ _failureClass: FAILURE_CLASS.PROJECT_NOT_FOUND,
8651
8681
  ...(declaredPlanProject ? { _declaredPlanProject: declaredPlanProject, _declaredPlanProjectMissing: true } : {}),
8652
8682
  });
8653
8683
  log('warn', `central work item ${item.id}: ${error}`);
@@ -8661,6 +8691,7 @@ function discoverCentralWorkItems(config) {
8661
8691
  status: WI_STATUS.FAILED,
8662
8692
  failReason: crossRepoFallbackError,
8663
8693
  failedAt: ts(),
8694
+ _failureClass: FAILURE_CLASS.PROJECT_NOT_FOUND,
8664
8695
  _crossRepoTargetProjects: planTargetProjects.slice(),
8665
8696
  });
8666
8697
  log('warn', `central work item ${item.id} (cross-repo plan-to-prd): ${crossRepoFallbackError}`);
@@ -10288,12 +10319,15 @@ async function tickInner() {
10288
10319
  } else {
10289
10320
  // P-a3f9b205: surface the per-project live-mode gate. Order matters:
10290
10321
  // max_concurrency / agent_busy / branch_locked are all more specific
10291
- // and win when both apply.
10322
+ // and win when both apply. Also check the pending item's own resolved
10323
+ // checkout mode so worktree-mode items on hybrid projects are not
10324
+ // falsely annotated live_checkout_busy (mirrors seeding guard above).
10292
10325
  const itemProjName = item.project || item.meta?.project?.name || null;
10293
10326
  if (
10294
10327
  itemProjName
10295
10328
  && !READ_ONLY_ROOT_TASK_TYPES.has(item.type)
10296
10329
  && postLiveProjectsInUse.has(itemProjName)
10330
+ && shared.resolveCheckoutMode(shared.findProjectByName(shared.getProjects(config), itemProjName), item.type) === 'live'
10297
10331
  ) {
10298
10332
  reason = 'live_checkout_busy';
10299
10333
  }
@@ -10490,6 +10524,7 @@ module.exports = {
10490
10524
  resolveDependencyBranches, buildCrossRepoDepsSection, // exported for testing (P-faea3206)
10491
10525
  gitOutputToString, gitErrorOutput, classifyDepMergeFailureOutput, listUnmergedFiles, // exported for testing
10492
10526
  buildDepConflictFixItem, deriveConflictFixKey, // exported for testing (W-mpcwojgr000a0244)
10527
+ runWorktreeAdd, // exported for testing (W-mqvaxv65000m76f2 — GVFS partial-checkout behavioral test)
10493
10528
  isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
10494
10529
  _statusPorcelainCmd, _killGitDescendantsForWorktree, _bumpQuarantineOutcome, // exported for testing (W-mq5n1zx5)
10495
10530
  _reapWorktreeHolders, _findTerminalWorktreeOwners, // exported for testing (W-mqila0t5 — CWD-pinned holder reap)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2286",
3
+ "version": "0.1.2287",
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"