@yemi33/minions 0.1.415 → 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,8 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.415 (2026-04-06)
3
+ ## 0.1.416 (2026-04-06)
4
4
 
5
5
  ### Features
6
+ - Fix notes.md race condition and null guards in consolidation.js
7
+ - Replace 5 raw status strings with WI_STATUS constants
6
8
  - Fix null crashes in lifecycle.js syncPrsFromOutput and createReviewFeedbackForAuthor
7
9
 
8
10
  ## 0.1.414 (2026-04-06)
@@ -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();
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.415",
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"