@yemi33/minions 0.1.526 → 0.1.527

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,9 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.527 (2026-04-07)
4
+
5
+ ### Features
6
+ - Replace direct safeJson reads of work-items.json with getWorkItems() (#424)
7
+
3
8
  ## 0.1.526 (2026-04-07)
4
9
 
5
10
  ### Features
6
- - Replace new Date().toISOString() with ts() from shared.js (#411)
7
11
  - Add Concurrency & Lock Ordering section to CLAUDE.md (#409)
8
12
  - 9 test isolation verification tests — run last to detect pollution
9
13
 
@@ -175,8 +175,8 @@ async function submitWorkItemEdit(id, source, e) {
175
175
  const refsRaw = document.getElementById('wi-edit-refs')?.value || '';
176
176
  const references = refsRaw.split('\n').filter(function(l) { return l.trim(); }).map(function(l) {
177
177
  var parts = l.split('|').map(function(s) { return s.trim(); });
178
- return { url: parts[0], title: parts[1] || parts[0], type: parts[2] || 'link' };
179
- });
178
+ return { url: parts[0] || '', title: parts[1] || parts[0] || '', type: parts[2] || 'link' };
179
+ }).filter(function(r) { return r.url; });
180
180
  const acRaw = document.getElementById('wi-edit-ac')?.value || '';
181
181
  const acceptanceCriteria = acRaw.split('\n').filter(function(l) { return l.trim(); });
182
182
  if (!title) { if (btn) { btn.disabled = false; btn.textContent = 'Save'; } alert('Title is required'); return; }
@@ -387,8 +387,8 @@ async function _submitCreateWorkItem(e) {
387
387
  const refsRaw = document.getElementById('wi-new-refs')?.value || '';
388
388
  const references = refsRaw.split('\n').filter(l => l.trim()).map(l => {
389
389
  const parts = l.split('|').map(s => s.trim());
390
- return { url: parts[0], title: parts[1] || parts[0], type: parts[2] || 'link' };
391
- });
390
+ return { url: parts[0] || '', title: parts[1] || parts[0] || '', type: parts[2] || 'link' };
391
+ }).filter(r => r.url);
392
392
 
393
393
  try {
394
394
  const body = { title, description: desc, type, priority };
package/dashboard.js CHANGED
@@ -192,8 +192,8 @@ async function checkNpmVersion() {
192
192
  _npmVersionCache = { latest: version || null, checkedAt: new Date().toISOString() };
193
193
  _npmVersionCacheTs = now;
194
194
  } catch (e) {
195
- console.error('[version-check] npm view failed:', e.message?.split('\n')[0]);
196
- _npmVersionCache = _npmVersionCache || { latest: null, checkedAt: null, error: e.message?.split('\n')[0] || 'check failed' };
195
+ console.error('[version-check] npm view failed:', e.message?.split('\n')?.[0]);
196
+ _npmVersionCache = _npmVersionCache || { latest: null, checkedAt: null, error: e.message?.split('\n')?.[0] || 'check failed' };
197
197
  }
198
198
  return _npmVersionCache;
199
199
  }
@@ -3816,7 +3816,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3816
3816
  // Determine project
3817
3817
  reloadConfig();
3818
3818
  const projects = shared.getProjects(CONFIG);
3819
- const targetProject = projectName ? projects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) : projects[0];
3819
+ const targetProject = projectName ? projects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) : (projects[0] || null);
3820
3820
  const prPath = targetProject ? shared.projectPrPath(targetProject) : path.join(MINIONS_DIR, 'pull-requests.json');
3821
3821
 
3822
3822
  // Extract PR number from URL
package/engine/cleanup.js CHANGED
@@ -308,15 +308,9 @@ function runCleanup(config, verbose = false) {
308
308
  // Collect all work item IDs across all sources
309
309
  const allWiIds = new Set();
310
310
  try {
311
- const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
312
- central.forEach(w => allWiIds.add(w.id));
313
- } catch (e) { log('warn', 'read central work items for orphan check: ' + e.message); }
314
- for (const project of projects) {
315
- try {
316
- const projItems = safeJson(projectWorkItemsPath(project)) || [];
317
- projItems.forEach(w => allWiIds.add(w.id));
318
- } catch (e) { log('warn', 'read project work items for orphan check: ' + e.message); }
319
- }
311
+ const allItems = queries.getWorkItems();
312
+ allItems.forEach(w => allWiIds.add(w.id));
313
+ } catch (e) { log('warn', 'read work items for orphan check: ' + e.message); }
320
314
 
321
315
  let changed = false;
322
316
  for (const queue of ['pending', 'active']) {
@@ -126,14 +126,8 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
126
126
  if (processWorkItemFailure && result === DISPATCH_RESULT.ERROR && item.meta?.item?.id) {
127
127
  let retries = (item.meta.item._retryCount || 0);
128
128
  try {
129
- const wiPath = lifecycle().resolveWorkItemPath(item.meta);
130
- if (wiPath) {
131
- const items = safeJson(wiPath);
132
- if (items && Array.isArray(items)) {
133
- const wi = items.find(i => i.id === item.meta.item.id);
134
- if (wi) retries = wi._retryCount || 0;
135
- }
136
- }
129
+ const wi = queries.getWorkItems().find(i => i.id === item.meta.item.id);
130
+ if (wi) retries = wi._retryCount || 0;
137
131
  } catch (e) { log('warn', 'read retry count: ' + e.message); }
138
132
  const maxRetries = ENGINE_DEFAULTS.maxRetries;
139
133
  if (retryableFailure && retries < maxRetries) {
@@ -178,13 +172,8 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
178
172
  const config = getConfig();
179
173
  const failedId = item.meta.item.id;
180
174
  const blockedItems = [];
181
- for (const p of getProjects(config)) {
182
- const items = safeJson(projectWorkItemsPath(p)) || [];
183
- items.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
184
- .forEach(w => blockedItems.push(`- \`${w.id}\` — ${w.title}`));
185
- }
186
- const centralItems = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
187
- centralItems.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
175
+ const allItems = queries.getWorkItems(config);
176
+ allItems.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
188
177
  .forEach(w => blockedItems.push(`- \`${w.id}\` — ${w.title}`));
189
178
 
190
179
  writeInboxAlert(`failed-${failedId}`,
@@ -30,20 +30,7 @@ function checkPlanCompletion(meta, config) {
30
30
  const projects = shared.getProjects(config);
31
31
 
32
32
  // Collect work items from ALL projects + central (PRD items can be in either)
33
- let allWorkItems = [];
34
- for (const p of projects) {
35
- try {
36
- const wi = safeJson(shared.projectWorkItemsPath(p)) || [];
37
- allWorkItems = allWorkItems.concat(wi);
38
- } catch { /* optional */ }
39
- }
40
- // Also check central work-items.json (for no-project setups)
41
- try {
42
- const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
43
- for (const w of central) {
44
- if (!allWorkItems.some(existing => existing.id === w.id)) allWorkItems.push(w);
45
- }
46
- } catch { /* optional */ }
33
+ const allWorkItems = queries.getWorkItems(config);
47
34
  const planItems = allWorkItems.filter(w => w.sourcePlan === planFile && w.itemType !== 'pr' && w.itemType !== 'verify');
48
35
  if (planItems.length === 0) return;
49
36
 
@@ -330,19 +317,7 @@ function archivePlan(planFile, plan, projects, config) {
330
317
  if (plan.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
331
318
 
332
319
  // Collect work items for this plan
333
- let allWorkItems = [];
334
- for (const p of projects) {
335
- try {
336
- const wi = safeJson(shared.projectWorkItemsPath(p)) || [];
337
- allWorkItems = allWorkItems.concat(wi);
338
- } catch { /* optional */ }
339
- }
340
- try {
341
- const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
342
- for (const w of central) {
343
- if (!allWorkItems.some(existing => existing.id === w.id)) allWorkItems.push(w);
344
- }
345
- } catch { /* optional */ }
320
+ const allWorkItems = queries.getWorkItems(config);
346
321
  const planItems = allWorkItems.filter(w => w.sourcePlan === planFile);
347
322
  const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
348
323
 
@@ -133,7 +133,7 @@ function resolveTaskContext(item, config) {
133
133
  try {
134
134
  const plans = fs.readdirSync(path.join(MINIONS_DIR, 'plans')).filter(f => f.endsWith('.md') || f.endsWith('.json'));
135
135
  // Check work-items to find which plan file this agent created
136
- const workItems = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
136
+ const workItems = queries.getWorkItems();
137
137
  const agentPlanItems = workItems.filter(w =>
138
138
  w.type === WORK_TYPE.PLAN && w.dispatched_to === agent.id && w.status === WI_STATUS.DONE && w._planFileName
139
139
  ).sort((a, b) => (b.completedAt || '').localeCompare(a.completedAt || ''));
package/engine/shared.js CHANGED
@@ -738,6 +738,34 @@ function killImmediate(proc) {
738
738
  }
739
739
  }
740
740
 
741
+ // ─── Work Items & Pull Requests Mutation Helpers ────────────────────────────
742
+
743
+ /**
744
+ * Atomic read-modify-write for work-items JSON files.
745
+ * Wraps mutateJsonFileLocked with defaultValue of [].
746
+ * @param {string} filePath - Path to the work-items JSON file
747
+ * @param {Function} mutator - Receives the array, mutates in place or returns new value
748
+ */
749
+ function mutateWorkItems(filePath, mutator) {
750
+ return mutateJsonFileLocked(filePath, (data) => {
751
+ if (!Array.isArray(data)) data = [];
752
+ return mutator(data) || data;
753
+ }, { defaultValue: [] });
754
+ }
755
+
756
+ /**
757
+ * Atomic read-modify-write for pull-requests JSON files.
758
+ * Wraps mutateJsonFileLocked with defaultValue of [].
759
+ * @param {string} filePath - Path to the pull-requests JSON file
760
+ * @param {Function} mutator - Receives the array, mutates in place or returns new value
761
+ */
762
+ function mutatePullRequests(filePath, mutator) {
763
+ return mutateJsonFileLocked(filePath, (data) => {
764
+ if (!Array.isArray(data)) data = [];
765
+ return mutator(data) || data;
766
+ }, { defaultValue: [] });
767
+ }
768
+
741
769
  module.exports = {
742
770
  MINIONS_DIR,
743
771
  PR_LINKS_PATH,
@@ -753,6 +781,8 @@ module.exports = {
753
781
  safeUnlink,
754
782
  withFileLock,
755
783
  mutateJsonFileLocked,
784
+ mutateWorkItems,
785
+ mutatePullRequests,
756
786
  uid,
757
787
  uniquePath,
758
788
  writeToInbox,
package/engine.js CHANGED
@@ -149,16 +149,8 @@ function resolveDependencyBranches(depIds, sourcePlan, project, config) {
149
149
  const projects = shared.getProjects(config);
150
150
 
151
151
  // Find work items for each dependency plan item
152
- const depWorkItems = [];
153
- for (const p of projects) {
154
- const wiPath = shared.projectWorkItemsPath(p);
155
- const items = safeJson(wiPath) || [];
156
- for (const wi of items) {
157
- if (depIds.includes(wi.id)) {
158
- depWorkItems.push(wi);
159
- }
160
- }
161
- }
152
+ const allItems = queries.getWorkItems(config);
153
+ const depWorkItems = allItems.filter(wi => depIds.includes(wi.id));
162
154
 
163
155
  // Find PR branches for each dependency work item
164
156
  for (const p of projects) {
@@ -837,13 +829,7 @@ function areDependenciesMet(item, config) {
837
829
  const projects = getProjects(config);
838
830
 
839
831
  // Collect work items from ALL projects (dependencies can be cross-project)
840
- let allWorkItems = [];
841
- for (const p of projects) {
842
- try {
843
- const wi = safeJson(projectWorkItemsPath(p)) || [];
844
- allWorkItems = allWorkItems.concat(wi);
845
- } catch (e) { log('warn', 'read project work items for deps: ' + e.message); }
846
- }
832
+ const allWorkItems = queries.getWorkItems(config);
847
833
  // PRD item statuses that count as "done" for dep resolution
848
834
  const PRD_MET_STATUSES = DONE_STATUSES;
849
835
 
@@ -1195,13 +1181,7 @@ function materializePlansAsWorkItems(config) {
1195
1181
  const statusFilter = ['missing', 'planned'];
1196
1182
  // Also materialize in-pr/done items that never got a work item (race with PR status sync)
1197
1183
  const allExistingWiIds = new Set();
1198
- for (const p of allProjects) {
1199
- for (const w of (safeJson(projectWorkItemsPath(p)) || [])) {
1200
- if (w.id) allExistingWiIds.add(w.id);
1201
- }
1202
- }
1203
- // Also check central work-items.json
1204
- for (const w of (safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [])) {
1184
+ for (const w of queries.getWorkItems()) {
1205
1185
  if (w.id) allExistingWiIds.add(w.id);
1206
1186
  }
1207
1187
  const items = plan.missing_features.filter(f =>
@@ -2307,10 +2287,9 @@ function discoverWork(config) {
2307
2287
  }
2308
2288
 
2309
2289
  // Gate reviews and fixes: do not dispatch until all implement items are complete
2310
- const hasIncompleteImplements = projects.some(project => {
2311
- const items = safeJson(projectWorkItemsPath(project)) || [];
2312
- return items.some(i => ['queued', 'pending', 'dispatched'].includes(i.status) && (i.type || '').startsWith('implement'));
2313
- });
2290
+ const hasIncompleteImplements = queries.getWorkItems(config).some(i =>
2291
+ ['queued', 'pending', 'dispatched'].includes(i.status) && (i.type || '').startsWith('implement')
2292
+ );
2314
2293
  if (hasIncompleteImplements) {
2315
2294
  if (allReviews.length > 0) {
2316
2295
  log('info', `Gating ${allReviews.length} reviews — implement items still in progress`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.526",
3
+ "version": "0.1.527",
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"