@yemi33/minions 0.1.295 → 0.1.296

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.296 (2026-04-03)
4
+
5
+ ### Features
6
+ - fix 7 bugs across cooldown, spawn-agent, playbook, scheduler, consolidation
7
+
3
8
  ## 0.1.295 (2026-04-03)
4
9
 
5
10
  ### Features
@@ -224,12 +224,22 @@ function consolidateWithLLM(items, existingNotes, files, config) {
224
224
  let newContent = current + entry;
225
225
 
226
226
  if (newContent.length > 50000) {
227
- const sections = newContent.split('\n---\n\n### ');
228
- if (sections.length > 10) {
229
- const header = sections[0];
230
- const recent = sections.slice(-8);
231
- newContent = header + '\n---\n\n### ' + recent.join('\n---\n\n### ');
232
- log('info', `Pruned notes.md: removed ${sections.length - 9} old sections`);
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`);
242
+ }
233
243
  }
234
244
  }
235
245
 
@@ -312,8 +322,9 @@ function consolidateWithRegex(items, files) {
312
322
  const seen = new Map();
313
323
  const deduped = [];
314
324
  for (const insight of allInsights) {
315
- const fpWords = insight.fingerprint.split(' ').filter(w => w.length > 4).slice(0, 5);
325
+ const fpWords = insight.fingerprint.split(' ').filter(w => w.length > 4 && w.length <= 200).slice(0, 5);
316
326
  // Use word-boundary regex to avoid substring false positives (e.g. 'fix' matching 'prefix')
327
+ // Cap word length at 200 chars to prevent ReDoS on pathological input
317
328
  if (fpWords.length >= 3 && fpWords.every(w => new RegExp(`\\b${w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(existingNotes))) continue;
318
329
  const existing = seen.get(insight.fingerprint);
319
330
  if (existing) { if (!existing.sources.includes(insight.agent)) existing.sources.push(insight.agent); continue; }
@@ -355,8 +366,14 @@ function consolidateWithRegex(items, files) {
355
366
  const current = getNotes();
356
367
  let newContent = current + entry;
357
368
  if (newContent.length > 50000) {
358
- const sections = newContent.split('\n---\n\n### ');
359
- if (sections.length > 10) { newContent = sections[0] + '\n---\n\n### ' + sections.slice(-8).join('\n---\n\n### '); }
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### '); }
376
+ }
360
377
  }
361
378
  safeWrite(NOTES_PATH, newContent);
362
379
  classifyToKnowledgeBase(items);
@@ -38,7 +38,11 @@ function saveCooldowns() {
38
38
  if (now - v.timestamp > 24 * 60 * 60 * 1000) dispatchCooldowns.delete(k);
39
39
  }
40
40
  const obj = Object.fromEntries(dispatchCooldowns);
41
- safeWrite(COOLDOWN_PATH, obj);
41
+ try {
42
+ safeWrite(COOLDOWN_PATH, obj);
43
+ } catch (err) {
44
+ log('warn', `saveCooldowns failed writing ${COOLDOWN_PATH}: ${err.message}`);
45
+ }
42
46
  }, 1000); // debounce — write at most once per second
43
47
  }
44
48
 
@@ -285,6 +285,14 @@ function renderPlaybook(type, vars) {
285
285
  content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(val));
286
286
  }
287
287
 
288
+ // Warn when a substituted value itself contains {{...}} patterns (potential self-reference)
289
+ const selfRefVars = Object.entries(allVars)
290
+ .filter(([, val]) => /\{\{\w+\}\}/.test(String(val)))
291
+ .map(([key]) => key);
292
+ if (selfRefVars.length > 0) {
293
+ log('warn', `Playbook "${type}": substituted values contain unresolved {{...}} patterns (potential self-reference): ${selfRefVars.join(', ')}`);
294
+ }
295
+
288
296
  // Warn on variables that resolved to empty string
289
297
  const emptyVars = Object.entries(allVars)
290
298
  .filter(([, val]) => String(val) === '')
@@ -114,7 +114,7 @@ function discoverScheduledWork(config) {
114
114
  mutateJsonFileLocked(SCHEDULE_RUNS_PATH, (runs) => {
115
115
  for (const sched of schedules) {
116
116
  if (!sched.id || !sched.cron || !sched.title) continue;
117
- if (sched.enabled === false) continue;
117
+ if (sched.enabled !== true) continue; // explicit true required — undefined/null/false all skip
118
118
 
119
119
  const lastRun = runs[sched.id] || null;
120
120
  if (!shouldRunNow(sched, lastRun)) continue;
@@ -154,17 +154,31 @@ const pidFile = promptFile.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid');
154
154
  fs.writeFileSync(pidFile, String(proc.pid || ''));
155
155
 
156
156
  // Send prompt via stdin — if system prompt was truncated, prepend the full context
157
- if (!isResume && Buffer.byteLength(sysPrompt) >= 30000) {
158
- // System prompt was too large for CLI — prepend full context to user prompt
159
- proc.stdin.write(`## Full Agent Context\n\n${sysPrompt}\n\n---\n\n## Your Task\n\n${prompt}`);
160
- } else {
161
- proc.stdin.write(prompt);
157
+ try {
158
+ if (!isResume && Buffer.byteLength(sysPrompt) >= 30000) {
159
+ // System prompt was too large for CLI — prepend full context to user prompt
160
+ proc.stdin.write(`## Full Agent Context\n\n${sysPrompt}\n\n---\n\n## Your Task\n\n${prompt}`);
161
+ } else {
162
+ proc.stdin.write(prompt);
163
+ }
164
+ proc.stdin.end();
165
+ } catch (err) {
166
+ console.error(`FATAL: stdin write failed (broken pipe): ${err.message}`);
167
+ fs.appendFileSync(debugPath, `STDIN ERROR: ${err.message}\n`);
168
+ try { proc.kill('SIGTERM'); } catch { /* process may already be dead */ }
169
+ process.exit(1);
162
170
  }
163
- proc.stdin.end();
164
171
 
165
172
  // Clean up temp file (only created for non-resume sessions)
166
173
  if (!isResume) setTimeout(() => { try { fs.unlinkSync(sysTmpPath); } catch { /* cleanup */ } }, 5000);
167
174
 
175
+ // Register exit handler to clean up orphaned temp files (system prompt tmp)
176
+ function _cleanupSpawnTempFiles() {
177
+ try { fs.unlinkSync(sysTmpPath); } catch { /* may already be cleaned */ }
178
+ }
179
+ process.on('exit', _cleanupSpawnTempFiles);
180
+ process.on('SIGTERM', () => { _cleanupSpawnTempFiles(); process.exit(143); });
181
+
168
182
  // Capture stderr separately for debugging
169
183
  let stderrBuf = '';
170
184
  proc.stderr.on('data', (chunk) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.295",
3
+ "version": "0.1.296",
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"