@yemi33/minions 0.1.332 → 0.1.334

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.334 (2026-04-03)
4
+
5
+ ### Features
6
+ - Fix PR write races in ado.js and github.js
7
+
8
+ ### Fixes
9
+ - guarantee test cleanup via finally block in test harness
10
+
3
11
  ## 0.1.332 (2026-04-03)
4
12
 
5
13
  ### Fixes
package/engine/ado.js CHANGED
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const shared = require('./shared');
8
8
  const { exec, getAdoOrgBase, addPrLink, log, ts, dateStamp, PR_STATUS } = shared;
9
9
  const { getPrs } = require('./queries');
10
+ const { mutateJsonFileLocked } = shared;
10
11
 
11
12
  // Lazy require to avoid circular dependency — only needed for engine().handlePostMerge
12
13
  let _engine = null;
@@ -99,7 +100,15 @@ async function forEachActivePr(config, token, callback) {
99
100
  }
100
101
 
101
102
  if (projectUpdated > 0) {
102
- shared.safeWrite(shared.projectPrPath(project), prs);
103
+ mutateJsonFileLocked(shared.projectPrPath(project), (currentPrs) => {
104
+ // Merge updated PRs into the locked copy by ID
105
+ for (const updatedPr of prs) {
106
+ const idx = currentPrs.findIndex(p => p.id === updatedPr.id);
107
+ if (idx >= 0) currentPrs[idx] = updatedPr;
108
+ else currentPrs.push(updatedPr);
109
+ }
110
+ return currentPrs;
111
+ }, { defaultValue: [] });
103
112
  totalUpdated += projectUpdated;
104
113
  }
105
114
  }
@@ -174,11 +183,11 @@ async function pollPrStatus(config) {
174
183
  if (authorId) {
175
184
  try {
176
185
  const metricsPath = path.join(__dirname, 'metrics.json');
177
- const metrics = shared.safeJson(metricsPath) || {};
178
- if (!metrics[authorId]) metrics[authorId] = {};
179
- if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
180
- else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
181
- shared.safeWrite(metricsPath, metrics);
186
+ mutateJsonFileLocked(metricsPath, (metrics) => {
187
+ if (!metrics[authorId]) metrics[authorId] = {};
188
+ if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
189
+ else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
190
+ });
182
191
  } catch (err) { log('warn', `Metrics update: ${err.message}`); }
183
192
  }
184
193
  }
@@ -196,8 +205,8 @@ async function pollPrStatus(config) {
196
205
 
197
206
  const buildStatuses = [...latest.values()].filter(s => {
198
207
  const ctx = ((s.context?.genre || '') + '/' + (s.context?.name || '')).toLowerCase();
199
- return ctx.includes('codecoverage') || ctx.includes('build') ||
200
- ctx.includes('deploy') || ctx.includes('ci/');
208
+ return /\bcodecoverage\b/.test(ctx) || /\bbuild\b/.test(ctx) ||
209
+ /\bdeploy\b/.test(ctx) || /(?:^|\/)ci(?:\/|$)/.test(ctx);
201
210
  });
202
211
 
203
212
  let buildStatus = 'none';
@@ -248,7 +257,8 @@ async function pollPrHumanComments(config) {
248
257
  const threadsData = await adoFetch(threadsUrl, token);
249
258
  const threads = threadsData.value || [];
250
259
 
251
- const cutoff = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
260
+ const cutoffStr = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
261
+ const cutoffMs = new Date(cutoffStr).getTime() || 0;
252
262
 
253
263
  // Collect ALL human comments on the PR for full context
254
264
  const allHumanComments = [];
@@ -269,7 +279,8 @@ async function pollPrHumanComments(config) {
269
279
  allHumanComments.push(entry);
270
280
 
271
281
  // Track which comments are new (for triggering — any new comment triggers a fix)
272
- if (comment.publishedDate && comment.publishedDate > cutoff) {
282
+ const commentMs = comment.publishedDate ? new Date(comment.publishedDate).getTime() : 0;
283
+ if (commentMs && commentMs > cutoffMs) {
273
284
  newHumanComments.push(entry);
274
285
  }
275
286
  }
@@ -285,7 +296,7 @@ async function pollPrHumanComments(config) {
285
296
  // Provide ALL comments as context — the agent needs full thread context to fix properly
286
297
  const feedbackContent = allHumanComments
287
298
  .map(c => {
288
- const isNew = c.date > cutoff;
299
+ const isNew = (new Date(c.date).getTime() || 0) > cutoffMs;
289
300
  return `${isNew ? '**[NEW]** ' : ''}**${c.author}** (${c.date}):\n${c.content.replace(/@minions\s*/gi, '').trim()}`;
290
301
  })
291
302
  .join('\n\n---\n\n');
@@ -412,7 +423,15 @@ async function reconcilePrs(config) {
412
423
  }
413
424
 
414
425
  if (projectAdded > 0 || projectUpdated > 0 || backfilled > 0) {
415
- shared.safeWrite(prPath, existingPrs);
426
+ mutateJsonFileLocked(prPath, (currentPrs) => {
427
+ // Merge reconciled PRs into the locked copy by ID
428
+ for (const pr of existingPrs) {
429
+ const idx = currentPrs.findIndex(p => p.id === pr.id);
430
+ if (idx >= 0) currentPrs[idx] = pr;
431
+ else currentPrs.push(pr);
432
+ }
433
+ return currentPrs;
434
+ }, { defaultValue: [] });
416
435
  totalAdded += projectAdded;
417
436
  if (projectUpdated > 0) log('info', `PR reconciliation: linked ${projectUpdated} existing PR(s) to PRD items in ${project.name}`);
418
437
  }
package/engine/github.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  const shared = require('./shared');
8
- const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR, addPrLink, getPrLinks, log, ts, dateStamp, PR_STATUS } = shared;
8
+ const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, mutateJsonFileLocked, MINIONS_DIR, addPrLink, getPrLinks, log, ts, dateStamp, PR_STATUS } = shared;
9
9
  const { getPrs } = require('./queries');
10
10
  const path = require('path');
11
11
 
@@ -71,7 +71,14 @@ async function forEachActiveGhPr(config, callback) {
71
71
  }
72
72
 
73
73
  if (projectUpdated > 0) {
74
- safeWrite(projectPrPath(project), prs);
74
+ mutateJsonFileLocked(projectPrPath(project), (currentPrs) => {
75
+ for (const updatedPr of prs) {
76
+ const idx = currentPrs.findIndex(p => p.id === updatedPr.id);
77
+ if (idx >= 0) currentPrs[idx] = updatedPr;
78
+ else currentPrs.push(updatedPr);
79
+ }
80
+ return currentPrs;
81
+ }, { defaultValue: [] });
75
82
  totalUpdated += projectUpdated;
76
83
  }
77
84
  }
@@ -105,7 +112,14 @@ async function forEachActiveGhPr(config, callback) {
105
112
  }
106
113
  }
107
114
  if (centralUpdated > 0) {
108
- safeWrite(centralPath, centralPrs);
115
+ mutateJsonFileLocked(centralPath, (currentPrs) => {
116
+ for (const updatedPr of centralPrs) {
117
+ const idx = currentPrs.findIndex(p => p.id === updatedPr.id);
118
+ if (idx >= 0) currentPrs[idx] = updatedPr;
119
+ else currentPrs.push(updatedPr);
120
+ }
121
+ return currentPrs;
122
+ }, { defaultValue: [] });
109
123
  totalUpdated += centralUpdated;
110
124
  }
111
125
 
@@ -182,11 +196,11 @@ async function pollPrStatus(config) {
182
196
  if (authorId) {
183
197
  try {
184
198
  const metricsPath = path.join(__dirname, 'metrics.json');
185
- const metrics = shared.safeJson(metricsPath) || {};
186
- if (!metrics[authorId]) metrics[authorId] = {};
187
- if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
188
- else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
189
- shared.safeWrite(metricsPath, metrics);
199
+ mutateJsonFileLocked(metricsPath, (metrics) => {
200
+ if (!metrics[authorId]) metrics[authorId] = {};
201
+ if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
202
+ else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
203
+ });
190
204
  } catch (err) { log('warn', `Metrics update: ${err.message}`); }
191
205
  }
192
206
  }
@@ -258,7 +272,8 @@ async function pollPrHumanComments(config) {
258
272
  return true;
259
273
  });
260
274
 
261
- const cutoff = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
275
+ const cutoffStr = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
276
+ const cutoffMs = new Date(cutoffStr).getTime() || 0;
262
277
 
263
278
  // Collect ALL human comments for full context, track new ones for triggering
264
279
  const allCommentEntries = [];
@@ -275,7 +290,8 @@ async function pollPrHumanComments(config) {
275
290
  allCommentEntries.push(entry);
276
291
 
277
292
  // Any new comment triggers a fix — no @minions filter needed
278
- if (date > cutoff) {
293
+ const dateMs = date ? new Date(date).getTime() : 0;
294
+ if (dateMs && dateMs > cutoffMs) {
279
295
  newComments.push(entry);
280
296
  }
281
297
  }
@@ -290,7 +306,7 @@ async function pollPrHumanComments(config) {
290
306
  // Provide ALL comments as context — the agent needs full thread context to fix properly
291
307
  const feedbackContent = allCommentEntries
292
308
  .map(c => {
293
- const isNew = c.date > cutoff;
309
+ const isNew = (new Date(c.date).getTime() || 0) > cutoffMs;
294
310
  return `${isNew ? '**[NEW]** ' : ''}**${c.author}** (${c.date}):\n${c.content.replace(/@minions\s*/gi, '').trim()}`;
295
311
  })
296
312
  .join('\n\n---\n\n');
@@ -397,7 +413,14 @@ async function reconcilePrs(config) {
397
413
  }
398
414
 
399
415
  if (projectAdded > 0 || backfilled > 0) {
400
- safeWrite(prPath, existingPrs);
416
+ mutateJsonFileLocked(prPath, (currentPrs) => {
417
+ for (const pr of existingPrs) {
418
+ const idx = currentPrs.findIndex(p => p.id === pr.id);
419
+ if (idx >= 0) currentPrs[idx] = pr;
420
+ else currentPrs.push(pr);
421
+ }
422
+ return currentPrs;
423
+ }, { defaultValue: [] });
401
424
  totalAdded += projectAdded;
402
425
  }
403
426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.332",
3
+ "version": "0.1.334",
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"