@yemi33/minions 0.1.798 → 0.1.800

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,13 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.798 (2026-04-10)
3
+ ## 0.1.800 (2026-04-10)
4
4
 
5
5
  ### Features
6
+ - configurable ignored comment authors — auto-filtered, never trigger fixes
6
7
  - add open-source community sources to agentic research pipeline
7
8
  - broaden daily pipeline research from Anthropic-only to full agentic space
8
9
  - cap review→fix cycles at evalMaxIterations (default 3)
9
10
 
10
11
  ### Fixes
12
+ - approved is permanent — no path can ever downgrade it
11
13
  - allow fix dispatch on approved PRs (only guard is in updatePrAfterFix)
12
14
  - bot comment on approved PR can no longer reset vote
13
15
  - remove redundant 'PR' prefix from dispatch labels (id already has it)
@@ -54,6 +54,7 @@ async function openSettings() {
54
54
  settingsField('Eval Max Cost', 'set-evalMaxCost', e.evalMaxCost === null || e.evalMaxCost === undefined ? '' : e.evalMaxCost, '$', 'USD ceiling per work item across all eval iterations (blank = no limit)') +
55
55
  settingsField('Max Build Fix Attempts', 'set-maxBuildFixAttempts', e.maxBuildFixAttempts || 3, '', 'Max auto-fix dispatches per PR before escalating to human (1-10)') +
56
56
  settingsField('Version Check Interval', 'set-versionCheckInterval', e.versionCheckInterval || 3600000, 'ms', 'How often to check npm for updates (default: 1 hour)') +
57
+ settingsField('Ignored Comment Authors', 'set-ignoredCommentAuthors', (e.ignoredCommentAuthors || []).join(', '), '', 'Comma-separated usernames — comments auto-closed, never trigger fixes') +
57
58
  '</div>' +
58
59
 
59
60
  '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Max Turns by Task Type</h3>' +
@@ -210,6 +211,7 @@ async function saveSettings() {
210
211
  evalMaxIterations: document.getElementById('set-evalMaxIterations').value,
211
212
  evalMaxCost: document.getElementById('set-evalMaxCost').value || null,
212
213
  maxBuildFixAttempts: document.getElementById('set-maxBuildFixAttempts').value,
214
+ ignoredCommentAuthors: document.getElementById('set-ignoredCommentAuthors').value,
213
215
  versionCheckInterval: document.getElementById('set-versionCheckInterval').value,
214
216
  ccModel: document.getElementById('set-ccModel').value,
215
217
  ccEffort: document.getElementById('set-ccEffort').value || null,
package/dashboard.js CHANGED
@@ -461,7 +461,8 @@ setInterval(() => {
461
461
 
462
462
  // ── Command Center: session state + helpers ─────────────────────────────────
463
463
 
464
- const CC_SESSION_EXPIRY_MS = 2 * 60 * 60 * 1000; // 2 hours
464
+ // Sessions never expire by time user controls lifecycle via tabs (new tab = new session, /clear = reset).
465
+ // Only invalidated by: system prompt change, explicit clear, or tab close.
465
466
  const CC_SESSION_MAX_TURNS = Infinity;
466
467
  let ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
467
468
  const ccInFlightTabs = new Set(); // per-tab in-flight tracking for parallel CC requests
@@ -477,17 +478,13 @@ function ccSessionValid() {
477
478
  ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
478
479
  return false;
479
480
  }
480
- const age = Date.now() - new Date(ccSession.lastActiveAt || 0).getTime();
481
- return age < CC_SESSION_EXPIRY_MS && ccSession.turnCount < CC_SESSION_MAX_TURNS;
481
+ return ccSession.turnCount < CC_SESSION_MAX_TURNS;
482
482
  }
483
483
 
484
484
  // Load persisted CC session on startup
485
485
  try {
486
486
  const saved = safeJson(path.join(ENGINE_DIR, 'cc-session.json'));
487
- if (saved && saved.sessionId) {
488
- const age = Date.now() - new Date(saved.lastActiveAt || 0).getTime();
489
- if (age < CC_SESSION_EXPIRY_MS) ccSession = saved;
490
- }
487
+ if (saved && saved.sessionId) ccSession = saved;
491
488
  } catch { /* optional */ }
492
489
 
493
490
  // Static system prompt — baked into session on creation, never changes
@@ -584,10 +581,8 @@ const docSessions = new Map(); // key → { sessionId, lastActiveAt, turnCount }
584
581
  try {
585
582
  const saved = safeJson(DOC_SESSIONS_PATH);
586
583
  if (saved && typeof saved === 'object') {
587
- const now = Date.now();
588
584
  for (const [key, s] of Object.entries(saved)) {
589
- const age = now - new Date(s.lastActiveAt || 0).getTime();
590
- if (age < CC_SESSION_EXPIRY_MS && s.turnCount < CC_SESSION_MAX_TURNS) {
585
+ if (s.turnCount < CC_SESSION_MAX_TURNS) {
591
586
  docSessions.set(key, s);
592
587
  }
593
588
  }
@@ -608,8 +603,7 @@ function resolveSession(store, key) {
608
603
  if (!key) return null;
609
604
  const s = docSessions.get(key);
610
605
  if (!s) return null;
611
- const age = Date.now() - new Date(s.lastActiveAt).getTime();
612
- if (age > CC_SESSION_EXPIRY_MS || s.turnCount >= CC_SESSION_MAX_TURNS) {
606
+ if (s.turnCount >= CC_SESSION_MAX_TURNS) {
613
607
  docSessions.delete(key);
614
608
  persistDocSessions();
615
609
  return null;
@@ -3342,9 +3336,15 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3342
3336
  ccInFlightTabs.add(tabId);
3343
3337
 
3344
3338
  try {
3339
+ let sessionReset = false;
3345
3340
  if (body.sessionId && body.sessionId !== ccSession.sessionId) {
3346
3341
  ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
3347
3342
  }
3343
+ // Detect prompt hash change — force fresh session
3344
+ if (body.sessionId && ccSession._promptHash && ccSession._promptHash !== _ccPromptHash) {
3345
+ ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
3346
+ sessionReset = true;
3347
+ }
3348
3348
  const wasResume = !!(body.sessionId && body.sessionId === ccSession.sessionId && ccSessionValid());
3349
3349
 
3350
3350
  const result = await ccCall(body.message, { store: 'cc' });
@@ -3365,7 +3365,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3365
3365
  });
3366
3366
  }
3367
3367
 
3368
- return jsonReply(res, 200, { ...parseCCActions(result.text), sessionId: ccSession.sessionId, newSession: !wasResume });
3368
+ const reply = { ...parseCCActions(result.text), sessionId: ccSession.sessionId, newSession: !wasResume };
3369
+ if (sessionReset) reply.sessionReset = true;
3370
+ return jsonReply(res, 200, reply);
3369
3371
  } finally {
3370
3372
  ccInFlightTabs.delete(tabId);
3371
3373
  }
@@ -3397,7 +3399,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3397
3399
 
3398
3400
  try {
3399
3401
  // Session management — per-tab: use sessionId from request, don't mutate global ccSession
3400
- const tabSessionId = body.sessionId || null;
3402
+ let tabSessionId = body.sessionId || null;
3403
+ let sessionReset = false;
3404
+ // If system prompt changed since this session was created, force a fresh session
3405
+ if (tabSessionId) {
3406
+ const sessions = shared.safeJsonArr(CC_SESSIONS_PATH);
3407
+ const tabEntry = sessions.find(s => s.id === (body.tabId || 'default'));
3408
+ if (tabEntry && tabEntry._promptHash && tabEntry._promptHash !== _ccPromptHash) {
3409
+ tabSessionId = null;
3410
+ sessionReset = true;
3411
+ }
3412
+ }
3401
3413
  const wasResume = !!tabSessionId;
3402
3414
  const sessionId = tabSessionId || null;
3403
3415
  const preamble = wasResume ? '' : buildCCStatePreamble();
@@ -3451,10 +3463,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3451
3463
  if (existing) {
3452
3464
  existing.sessionId = responseSessionId;
3453
3465
  existing.lastActiveAt = new Date(now).toISOString();
3454
- existing.turnCount = (existing.turnCount || 0) + 1;
3466
+ existing.turnCount = sessionReset ? 1 : (existing.turnCount || 0) + 1;
3455
3467
  existing.preview = preview;
3468
+ existing._promptHash = _ccPromptHash;
3456
3469
  } else {
3457
- sessions.push({ id: tabId, title: (body.message || 'New chat').slice(0, 40), sessionId: responseSessionId, createdAt: new Date(now).toISOString(), lastActiveAt: new Date(now).toISOString(), turnCount: 1, preview });
3470
+ sessions.push({ id: tabId, title: (body.message || 'New chat').slice(0, 40), sessionId: responseSessionId, createdAt: new Date(now).toISOString(), lastActiveAt: new Date(now).toISOString(), turnCount: 1, preview, _promptHash: _ccPromptHash });
3458
3471
  }
3459
3472
  safeWrite(CC_SESSIONS_PATH, sessions);
3460
3473
  } catch { /* non-critical */ }
@@ -3462,7 +3475,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3462
3475
 
3463
3476
  // Send final result with actions
3464
3477
  const { text: displayText, actions } = parseCCActions(result.text);
3465
- res.write('data: ' + JSON.stringify({ type: 'done', text: displayText, actions, sessionId: responseSessionId, newSession: !wasResume }) + '\n\n');
3478
+ const donePayload = { type: 'done', text: displayText, actions, sessionId: responseSessionId, newSession: !wasResume };
3479
+ if (sessionReset) donePayload.sessionReset = true;
3480
+ res.write('data: ' + JSON.stringify(donePayload) + '\n\n');
3466
3481
  _ccStreamEnded = true; res.end();
3467
3482
  } finally {
3468
3483
  ccInFlightTabs.delete(tabId);
@@ -3651,6 +3666,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3651
3666
  // Eval loop settings
3652
3667
  if (e.evalMaxIterations !== undefined) config.engine.evalMaxIterations = Math.max(1, Math.min(10, Number(e.evalMaxIterations) || D.evalMaxIterations));
3653
3668
  if (e.evalMaxCost !== undefined) config.engine.evalMaxCost = e.evalMaxCost === null || e.evalMaxCost === '' ? null : Math.max(0, Number(e.evalMaxCost) || 0);
3669
+ if (e.ignoredCommentAuthors !== undefined) {
3670
+ config.engine.ignoredCommentAuthors = String(e.ignoredCommentAuthors || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
3671
+ }
3654
3672
  }
3655
3673
 
3656
3674
  if (body.claude) {
package/engine/ado.js CHANGED
@@ -295,19 +295,18 @@ async function pollPrStatus(config) {
295
295
  const reviewers = prData.reviewers || [];
296
296
  const votes = reviewers.map(r => r.vote).filter(v => v !== undefined);
297
297
  let newReviewStatus = pr.reviewStatus || 'pending';
298
- if (votes.length > 0) {
298
+ // Once approved, it stays approved permanently
299
+ if (pr.reviewStatus === 'approved') {
300
+ newReviewStatus = 'approved';
301
+ } else if (votes.length > 0) {
299
302
  if (votes.some(v => v === -10)) {
300
- // Don't re-trigger 'changes-requested' if fix agent already responded (waiting state).
301
- // Only re-trigger if there's been a new push since the fix (reviewer re-reviewed).
302
303
  if (pr.reviewStatus === 'waiting' && pr.minionsReview?.fixedAt && (!pr.lastPushedAt || pr.lastPushedAt <= pr.minionsReview.fixedAt)) {
303
- newReviewStatus = 'waiting'; // fix was submitted, same vote still present — wait for re-review
304
+ newReviewStatus = 'waiting';
304
305
  } else {
305
306
  newReviewStatus = 'changes-requested';
306
307
  }
307
308
  }
308
309
  else if (votes.some(v => v >= 5)) newReviewStatus = 'approved';
309
- // Never downgrade from 'approved' — only -10 (reject) can override it
310
- else if (pr.reviewStatus === 'approved') newReviewStatus = 'approved';
311
310
  else if (votes.some(v => v === -5)) newReviewStatus = 'waiting';
312
311
  else newReviewStatus = 'pending';
313
312
  }
@@ -502,8 +501,10 @@ async function pollPrHumanComments(config) {
502
501
  for (const comment of (thread.comments || [])) {
503
502
  if (!comment.content || comment.commentType === 'system') continue;
504
503
  const content = comment.content;
505
- // Skip bots and CI noise
504
+ // Skip bots, CI noise, and ignored authors
506
505
  const authorName = (comment.author?.displayName || '').toLowerCase();
506
+ const ignoredAuthors = (config.engine?.ignoredCommentAuthors || []).map(a => a.toLowerCase());
507
+ if (ignoredAuthors.some(a => authorName.includes(a))) continue;
507
508
  if (/\b(bot|service|build|pipeline|codecov|sonar)\b/i.test(authorName)) continue;
508
509
  if (/^#{1,3}\s*(Coverage|Build|Test|Deploy|Pipeline)\s*(Report|Status|Result|Summary)/i.test(content)) continue;
509
510
  // Detect agent comments (included in context, but don't trigger fix)
package/engine/github.js CHANGED
@@ -330,8 +330,10 @@ async function pollPrStatus(config) {
330
330
  }
331
331
 
332
332
  let newReviewStatus = pr.reviewStatus || 'pending';
333
- if (states.some(s => s === 'CHANGES_REQUESTED')) {
334
- // Don't re-trigger if fix agent already responded — wait for re-review
333
+ // Once approved, it stays approved permanently
334
+ if (pr.reviewStatus === 'approved') {
335
+ newReviewStatus = 'approved';
336
+ } else if (states.some(s => s === 'CHANGES_REQUESTED')) {
335
337
  if (pr.reviewStatus === 'waiting' && pr.minionsReview?.fixedAt && (!pr.lastPushedAt || pr.lastPushedAt <= pr.minionsReview.fixedAt)) {
336
338
  newReviewStatus = 'waiting';
337
339
  } else {
@@ -339,8 +341,6 @@ async function pollPrStatus(config) {
339
341
  }
340
342
  }
341
343
  else if (states.some(s => s === 'APPROVED')) newReviewStatus = 'approved';
342
- // Never downgrade from 'approved' — only CHANGES_REQUESTED can override it
343
- else if (pr.reviewStatus === 'approved') newReviewStatus = 'approved';
344
344
  else if (states.length > 0) newReviewStatus = 'pending';
345
345
  else if (states.length === 0 && reviews.length > 0 && newReviewStatus === 'pending') newReviewStatus = 'waiting';
346
346
 
@@ -477,9 +477,11 @@ async function pollPrHumanComments(config) {
477
477
 
478
478
  // Separate: agent comments (included in context, don't trigger fix) vs human comments (trigger fix)
479
479
  // All non-bot, non-CI comments go into context. Only non-agent comments trigger pendingFix.
480
+ const ignoredAuthors = new Set((config.engine?.ignoredCommentAuthors || []).map(a => a.toLowerCase()));
480
481
  function _isBot(c) {
481
482
  if (c.user?.type === 'Bot') return true;
482
483
  const login = (c.user?.login || '').toLowerCase();
484
+ if (ignoredAuthors.has(login)) return true;
483
485
  if (/\b(bot|codecov|sonar|dependabot|renovate|github-actions|azure-pipelines)\b/i.test(login)) return true;
484
486
  const body = c.body || '';
485
487
  if (/^#{1,3}\s*(Coverage|Build|Test|Deploy|Pipeline)\s*(Report|Status|Result|Summary)/i.test(body)) return true;
@@ -751,8 +751,8 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary)
751
751
  if (!Array.isArray(prs)) return prs;
752
752
  const target = prs.find(p => p.id === pr.id);
753
753
  if (!target) return prs;
754
- // Only update if live check returned decisive result; never downgrade from 'approved' unless explicitly rejected
755
- if (postReviewStatus && !(target.reviewStatus === 'approved' && postReviewStatus !== 'changes-requested')) {
754
+ // Once approved, stays approved permanently no path can downgrade
755
+ if (postReviewStatus && target.reviewStatus !== 'approved') {
756
756
  target.reviewStatus = postReviewStatus;
757
757
  }
758
758
  target.lastReviewedAt = ts();
package/engine/shared.js CHANGED
@@ -549,6 +549,7 @@ const ENGINE_DEFAULTS = {
549
549
  buildFixGracePeriod: 600000, // 10min — wait for CI to run after build fix before re-dispatching
550
550
  autoCompletePrs: false, // auto-merge PRs when builds green + review approved (opt-in)
551
551
  prMergeMethod: 'squash', // merge method: squash, merge, rebase
552
+ ignoredCommentAuthors: [], // comments from these authors are auto-closed and never trigger fixes
552
553
  ccModel: 'sonnet', // model for Command Center and doc-chat (sonnet, haiku, opus)
553
554
  ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
554
555
  ccMaxTurns: 50, // max tool-use turns for CC/doc-chat before CLI stops
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.798",
3
+ "version": "0.1.800",
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"