@yemi33/minions 0.1.1570 → 0.1.1572

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.1572 (2026-04-27)
4
+
5
+ ### Fixes
6
+ - add skipWriteIfUnchanged to all watched-file mutators
7
+
8
+ ## 0.1.1571 (2026-04-27)
9
+
10
+ ### Fixes
11
+ - break work-items.json watcher tick storm
12
+
3
13
  ## 0.1.1570 (2026-04-27)
4
14
 
5
15
  ### Fixes
@@ -615,7 +615,7 @@ function updateWorkItemStatus(meta, status, reason) {
615
615
  }
616
616
  }
617
617
  return items;
618
- }, { defaultValue: [] });
618
+ }, { defaultValue: [], skipWriteIfUnchanged: true });
619
619
 
620
620
  log('info', `Work item ${itemId} → ${status}${reason ? ': ' + reason : ''}`);
621
621
  syncPrdItemStatus(itemId, status, meta.item?.sourcePlan);
@@ -1325,7 +1325,7 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
1325
1325
  description: `Create project-level skill \`${skillDirName}/SKILL.md\` in ${project}.\n\nWrite this file to \`${proj.localPath}/.claude/skills/${skillDirName}/SKILL.md\` via a PR.\n\n## Skill Content\n\n\`\`\`\n${enrichedBlock}\n\`\`\``,
1326
1326
  priority: 'low', status: WI_STATUS.QUEUED, created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
1327
1327
  return data;
1328
- });
1328
+ }, { skipWriteIfUnchanged: true });
1329
1329
  if (skillId) {
1330
1330
  log('info', `Queued work item ${skillId} to PR project skill "${name}" into ${project}`);
1331
1331
  }
@@ -1609,7 +1609,7 @@ function handleDecompositionResult(stdout, meta, config) {
1609
1609
  data.push(childItem);
1610
1610
  }
1611
1611
  return data;
1612
- });
1612
+ }, { skipWriteIfUnchanged: true });
1613
1613
  if (!found) continue;
1614
1614
  log('info', `Decomposition: ${parentId} → ${subItems.length} sub-items: ${subItems.map(s => s.id).join(', ')}`);
1615
1615
  return subItems.length;
@@ -1710,7 +1710,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
1710
1710
  log('warn', `Review ${meta.item.id} failed — no verdict after ${ENGINE_DEFAULTS.maxRetries} retries`);
1711
1711
  }
1712
1712
  return data;
1713
- });
1713
+ }, { skipWriteIfUnchanged: true });
1714
1714
  } catch (err) { log('warn', `review verdict check: ${err.message}`); }
1715
1715
  }
1716
1716
  }
@@ -1758,7 +1758,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
1758
1758
  log('warn', `plan-to-prd ${meta.item.id} failed — no PRD file after ${ENGINE_DEFAULTS.maxRetries} retries`);
1759
1759
  }
1760
1760
  return data;
1761
- });
1761
+ }, { skipWriteIfUnchanged: true });
1762
1762
  } catch (err) { log('warn', `plan-to-prd PRD check: ${err.message}`); }
1763
1763
  }
1764
1764
  }
@@ -1779,7 +1779,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
1779
1779
  const wi = data.find(i => i.id === meta.item.id);
1780
1780
  if (wi) delete wi._decomposing;
1781
1781
  return data;
1782
- });
1782
+ }, { skipWriteIfUnchanged: true });
1783
1783
  } catch (err) { log('warn', `Decompose cleanup: ${err.message}`); }
1784
1784
  }
1785
1785
  }
@@ -1972,7 +1972,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
1972
1972
  action = { type: 'needs-review' };
1973
1973
  }
1974
1974
  return data;
1975
- });
1975
+ }, { skipWriteIfUnchanged: true });
1976
1976
  if (action?.type === 'retry') {
1977
1977
  log('info', `Auto-retry ${action.retries}/${ENGINE_DEFAULTS.maxRetries} for ${meta.item.id} (no output, no PR)`);
1978
1978
  } else if (action?.type === 'done') {
@@ -2059,11 +2059,14 @@ function syncPrdFromPrs(config) {
2059
2059
  (wi.status === WI_STATUS.PENDING && !wi._pr) || wi.status === WI_STATUS.FAILED);
2060
2060
  if (!hasReconcilable) continue;
2061
2061
  let reconciled = 0;
2062
+ // skipWriteIfUnchanged: per-poll reconcile runs every prPollStatusEvery
2063
+ // ticks. Without the flag it'd unconditionally rewrite project work-items.json,
2064
+ // tripping the cli.js watcher into a tick storm even with zero reconciliation.
2062
2065
  const reconciledItems = mutateJsonFileLocked(wiPath, data => {
2063
2066
  if (!Array.isArray(data)) return data;
2064
2067
  reconciled = reconcileItemsWithPrs(data, allPrs);
2065
2068
  return data;
2066
- });
2069
+ }, { skipWriteIfUnchanged: true });
2067
2070
  if (reconciled > 0) {
2068
2071
  // Sync done status to PRD JSON for each newly reconciled item
2069
2072
  for (const wi of (reconciledItems || [])) {
package/engine/timeout.js CHANGED
@@ -398,6 +398,10 @@ function checkTimeouts(config) {
398
398
  allWiPaths.push(projectWorkItemsPath(project));
399
399
  }
400
400
  for (const wiPath of allWiPaths) {
401
+ // skipWriteIfUnchanged is critical — checkTimeouts runs every tick, and
402
+ // without this gate the file's mtime updates on every call (no real
403
+ // change), tripping the cli.js work-items.json watcher → triggering tick
404
+ // → calling checkTimeouts → ... a 5-6s loop of "File change detected".
401
405
  mutateJsonFileLocked(wiPath, (items) => {
402
406
  if (!items || !Array.isArray(items)) return items;
403
407
  let changed = false;
@@ -438,7 +442,7 @@ function checkTimeouts(config) {
438
442
  }
439
443
  }
440
444
  return items;
441
- }, { defaultValue: [] });
445
+ }, { defaultValue: [], skipWriteIfUnchanged: true });
442
446
  }
443
447
  }
444
448
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1570",
3
+ "version": "0.1.1572",
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"