@yemi33/minions 0.1.414 → 0.1.416

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.416 (2026-04-06)
4
+
5
+ ### Features
6
+ - Fix notes.md race condition and null guards in consolidation.js
7
+ - Replace 5 raw status strings with WI_STATUS constants
8
+ - Fix null crashes in lifecycle.js syncPrsFromOutput and createReviewFeedbackForAuthor
9
+
3
10
  ## 0.1.414 (2026-04-06)
4
11
 
5
12
  ### Fixes
@@ -38,7 +38,7 @@ function consolidateInbox(config) {
38
38
 
39
39
  const items = files.map(f => ({
40
40
  name: f,
41
- content: safeRead(path.join(INBOX_DIR, f))
41
+ content: safeRead(path.join(INBOX_DIR, f)) || ''
42
42
  }));
43
43
 
44
44
  const existingNotes = getNotes() || '';
@@ -220,30 +220,33 @@ function consolidateWithLLM(items, existingNotes, files, config) {
220
220
  }
221
221
 
222
222
  const entry = '\n\n---\n\n' + digest;
223
- const current = getNotes();
224
- let newContent = current + entry;
225
-
226
- if (newContent.length > 50000) {
227
- // Truncate on section boundary — scan backward for last \n# before byte limit
228
- // Never cut mid-section to preserve readability
229
- const limit = 50000;
230
- const lastSectionBoundary = newContent.lastIndexOf('\n---\n\n### ', limit);
231
- if (lastSectionBoundary > 0) {
232
- newContent = newContent.slice(0, lastSectionBoundary);
233
- log('info', `Pruned notes.md at section boundary (pos ${lastSectionBoundary}) to stay under ${limit} bytes`);
234
- } else {
235
- // Fallback: use the old section-count approach
236
- const sections = newContent.split('\n---\n\n### ');
237
- if (sections.length > 10) {
238
- const header = sections[0];
239
- const recent = sections.slice(-8);
240
- newContent = header + '\n---\n\n### ' + recent.join('\n---\n\n### ');
241
- log('info', `Pruned notes.md: removed ${sections.length - 9} old sections`);
223
+ // Wrap read-modify-write in file lock to prevent race with concurrent consolidation or manual edits
224
+ shared.withFileLock(NOTES_PATH + '.lock', () => {
225
+ const current = getNotes() || '';
226
+ let newContent = current + entry;
227
+
228
+ if (newContent.length > 50000) {
229
+ // Truncate on section boundary — scan backward for last \n# before byte limit
230
+ // Never cut mid-section to preserve readability
231
+ const limit = 50000;
232
+ const lastSectionBoundary = newContent.lastIndexOf('\n---\n\n### ', limit);
233
+ if (lastSectionBoundary > 0) {
234
+ newContent = newContent.slice(0, lastSectionBoundary);
235
+ log('info', `Pruned notes.md at section boundary (pos ${lastSectionBoundary}) to stay under ${limit} bytes`);
236
+ } else {
237
+ // Fallback: use the old section-count approach
238
+ const sections = newContent.split('\n---\n\n### ');
239
+ if (sections.length > 10) {
240
+ const header = sections[0];
241
+ const recent = sections.slice(-8);
242
+ newContent = header + '\n---\n\n### ' + recent.join('\n---\n\n### ');
243
+ log('info', `Pruned notes.md: removed ${sections.length - 9} old sections`);
244
+ }
242
245
  }
243
246
  }
244
- }
245
247
 
246
- safeWrite(NOTES_PATH, newContent);
248
+ safeWrite(NOTES_PATH, newContent);
249
+ });
247
250
  classifyToKnowledgeBase(items);
248
251
  archiveInboxFiles(files);
249
252
  log('info', `LLM consolidation complete: ${files.length} notes processed by Haiku`);
@@ -297,10 +300,10 @@ function consolidateWithRegex(items, files) {
297
300
  if (!trimmed || sectionPattern.test(trimmed)) continue;
298
301
  let insight = null;
299
302
  const numMatch = trimmed.match(numberedPattern);
300
- if (numMatch) insight = `**${numMatch[1].trim()}**: ${numMatch[2].trim()}`;
303
+ if (numMatch && numMatch[1] && numMatch[2]) insight = `**${numMatch[1].trim()}**: ${numMatch[2].trim()}`;
301
304
  if (!insight) {
302
305
  const bulMatch = trimmed.match(bulletPattern);
303
- if (bulMatch) insight = `**${bulMatch[1].trim()}**: ${bulMatch[2].trim()}`;
306
+ if (bulMatch && bulMatch[1] && bulMatch[2]) insight = `**${bulMatch[1].trim()}**: ${bulMatch[2].trim()}`;
304
307
  }
305
308
  if (!insight && importantKeywords.test(trimmed) && !trimmed.startsWith('#') && trimmed.length > 30 && trimmed.length < 500) {
306
309
  insight = trimmed;
@@ -363,19 +366,22 @@ function consolidateWithRegex(items, files) {
363
366
  const dupCount = allInsights.length - deduped.length;
364
367
  if (dupCount > 0) entry += `_Deduplication: ${dupCount} duplicate(s) removed._\n`;
365
368
 
366
- const current = getNotes();
367
- let newContent = current + entry;
368
- if (newContent.length > 50000) {
369
- const limit = 50000;
370
- const lastBoundary = newContent.lastIndexOf('\n---\n\n### ', limit);
371
- if (lastBoundary > 0) {
372
- newContent = newContent.slice(0, lastBoundary);
373
- } else {
374
- const sections = newContent.split('\n---\n\n### ');
375
- if (sections.length > 10) { newContent = sections[0] + '\n---\n\n### ' + sections.slice(-8).join('\n---\n\n### '); }
369
+ // Wrap read-modify-write in file lock to prevent race with concurrent consolidation or manual edits
370
+ shared.withFileLock(NOTES_PATH + '.lock', () => {
371
+ const current = getNotes() || '';
372
+ let newContent = current + entry;
373
+ if (newContent.length > 50000) {
374
+ const limit = 50000;
375
+ const lastBoundary = newContent.lastIndexOf('\n---\n\n### ', limit);
376
+ if (lastBoundary > 0) {
377
+ newContent = newContent.slice(0, lastBoundary);
378
+ } else {
379
+ const sections = newContent.split('\n---\n\n### ');
380
+ if (sections.length > 10) { newContent = sections[0] + '\n---\n\n### ' + sections.slice(-8).join('\n---\n\n### '); }
381
+ }
376
382
  }
377
- }
378
- safeWrite(NOTES_PATH, newContent);
383
+ safeWrite(NOTES_PATH, newContent);
384
+ });
379
385
  classifyToKnowledgeBase(items);
380
386
  archiveInboxFiles(files);
381
387
  log('info', `Regex fallback: consolidated ${files.length} notes \u2192 ${deduped.length} insights into notes.md`);
@@ -116,7 +116,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
116
116
  const maxRetries = ENGINE_DEFAULTS.maxRetries;
117
117
  if (retryableFailure && retries < maxRetries) {
118
118
  log('info', `Dispatch error for ${item.meta.item.id} — auto-retry ${retries + 1}/${maxRetries}`);
119
- lifecycle().updateWorkItemStatus(item.meta, 'pending', '');
119
+ lifecycle().updateWorkItemStatus(item.meta, WI_STATUS.PENDING, '');
120
120
  // Remove this dispatch key from completed so dedupe doesn't block immediate redispatch.
121
121
  if (item.meta?.dispatchKey) {
122
122
  try {
@@ -150,7 +150,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
150
150
  const finalReason = !retryableFailure
151
151
  ? `Non-retryable failure: ${reason || 'Unknown error'}`
152
152
  : (reason || `Failed after ${maxRetries} retries`);
153
- lifecycle().updateWorkItemStatus(item.meta, 'failed', finalReason);
153
+ lifecycle().updateWorkItemStatus(item.meta, WI_STATUS.FAILED, finalReason);
154
154
  // Alert: find items blocked by this failure and write inbox note
155
155
  try {
156
156
  const config = getConfig();
@@ -617,6 +617,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
617
617
  const inboxFiles = getInboxFiles().filter(f => f.includes(agentId) && f.includes(today));
618
618
  for (const f of inboxFiles) {
619
619
  const content = safeRead(path.join(INBOX_DIR, f));
620
+ if (!content) continue;
620
621
  const prHeaderPattern = /\*\*PR[:\*]*\*?\s*[#-]*\s*(?:(?:visualstudio\.com|dev\.azure\.com)[^\s"]*?pullrequest\/(\d+)|github\.com\/[^\s"]*?\/pull\/(\d+))/gi;
621
622
  while ((match = prHeaderPattern.exec(content)) !== null) prMatches.add(match[1] || match[2]);
622
623
  }
@@ -991,7 +992,7 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
991
992
  const inboxFiles = getInboxFiles();
992
993
  const reviewFiles = inboxFiles.filter(f => f.includes(reviewerAgentId) && f.includes(today));
993
994
  if (reviewFiles.length === 0) return;
994
- const reviewContent = reviewFiles.map(f => safeRead(path.join(INBOX_DIR, f))).join('\n\n');
995
+ const reviewContent = reviewFiles.map(f => safeRead(path.join(INBOX_DIR, f))).filter(Boolean).join('\n\n');
995
996
  const feedbackFile = `feedback-${authorAgentId}-from-${reviewerAgentId}-${pr.id}-${today}.md`;
996
997
  const feedbackPath = shared.uniquePath(path.join(INBOX_DIR, feedbackFile));
997
998
  const content = `# Review Feedback for ${config.agents[authorAgentId]?.name || authorAgentId}\n\n` +
package/engine/queries.js CHANGED
@@ -10,7 +10,8 @@ const os = require('os');
10
10
  const shared = require('./shared');
11
11
 
12
12
  const { safeRead, safeReadDir, safeJson, safeWrite, getProjects,
13
- projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES } = shared;
13
+ projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES,
14
+ WI_STATUS } = shared;
14
15
 
15
16
  // ── Paths ───────────────────────────────────────────────────────────────────
16
17
 
@@ -179,7 +180,7 @@ function getAgentStatus(agentId) {
179
180
  const latestInFlight = allItems
180
181
  .filter(w =>
181
182
  (w.dispatched_to || '').toLowerCase() === String(agentId).toLowerCase() &&
182
- w.status === 'dispatched'
183
+ w.status === WI_STATUS.DISPATCHED
183
184
  )
184
185
  .sort((a, b) => (b.dispatched_at || '').localeCompare(a.dispatched_at || ''))[0];
185
186
  if (latestInFlight) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.414",
3
+ "version": "0.1.416",
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"