@yemi33/minions 0.1.981 → 0.1.983

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,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.981 (2026-04-15)
3
+ ## 0.1.983 (2026-04-15)
4
4
 
5
5
  ### Features
6
6
  - gate build failure auto-fix behind autoFixBuilds flag
@@ -25,6 +25,9 @@
25
25
  - add /api/work-items/reopen endpoint and reopen-work-item CC action (#982)
26
26
 
27
27
  ### Fixes
28
+ - Remove autoReview flag — consolidate into evalLoop (#1093)
29
+ - link scheduled task notes back to originating item (#1090)
30
+ - restore rereview and human-fix dispatch
28
31
  - build fix sets fixDispatched to block same-tick conflict fix
29
32
  - ENGINE_DEFAULTS undefined in tick + playbook empty-var false positives
30
33
  - defer review setCooldown to post-gating in discoverWork
@@ -42,9 +45,6 @@
42
45
  - restore queue flush after abort — queued messages are user intent
43
46
  - CC stale lock auto-release + queue drain on abort
44
47
  - CC streaming 'tabId' TDZ error on new tab first message
45
- - fix tabId scope and close-event lock race in CC handlers
46
- - defer setCooldown to post-gating in discoverWork
47
- - fix CC queued message 'already processing' and thinking UX stacking
48
48
 
49
49
  ### Other
50
50
  - refactor(watches): rename maxTriggers→stopAfter, add onNotMet per-poll action
package/dashboard.js CHANGED
@@ -395,7 +395,13 @@ function getStatus() {
395
395
  schedules: (() => {
396
396
  const scheds = CONFIG.schedules || [];
397
397
  const runs = shared.safeJson(path.join(MINIONS_DIR, 'engine', 'schedule-runs.json')) || {};
398
- return scheds.map(s => ({ ...s, _lastRun: runs[s.id] || null }));
398
+ return scheds.map(s => {
399
+ const runEntry = runs[s.id];
400
+ // Backward compat: runEntry can be a string (old format) or object (new format with back-references)
401
+ const _lastRun = typeof runEntry === 'string' ? runEntry : (runEntry?.lastRun || runEntry?.lastCompletedAt || null);
402
+ const extra = typeof runEntry === 'object' && runEntry ? { _lastWorkItemId: runEntry.lastWorkItemId, _lastResult: runEntry.lastResult, _lastCompletedAt: runEntry.lastCompletedAt } : {};
403
+ return { ...s, _lastRun, ...extra };
404
+ });
399
405
  })(),
400
406
  watches: watchesMod.getWatches(),
401
407
  meetings: (() => { try { return require('./engine/meeting').getMeetings(); } catch { return []; } })(),
@@ -3683,7 +3689,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3683
3689
  reloadConfig();
3684
3690
  const schedules = CONFIG.schedules || [];
3685
3691
  const runs = shared.safeJson(path.join(MINIONS_DIR, 'engine', 'schedule-runs.json')) || {};
3686
- const result = schedules.map(s => ({ ...s, _lastRun: runs[s.id] || null }));
3692
+ const result = schedules.map(s => {
3693
+ const runEntry = runs[s.id];
3694
+ // Backward compat: runEntry can be a string (old format) or object (new format with back-references)
3695
+ const _lastRun = typeof runEntry === 'string' ? runEntry : (runEntry?.lastRun || runEntry?.lastCompletedAt || null);
3696
+ const extra = typeof runEntry === 'object' && runEntry ? { _lastWorkItemId: runEntry.lastWorkItemId, _lastResult: runEntry.lastResult, _lastCompletedAt: runEntry.lastCompletedAt } : {};
3697
+ return { ...s, _lastRun, ...extra };
3698
+ });
3687
3699
  return jsonReply(res, 200, { schedules: result });
3688
3700
  }
3689
3701
 
@@ -6,5 +6,13 @@
6
6
  "reason": "Consolidated into review + evalLoop config. No separate evaluate dispatch.",
7
7
  "locations": ["routing.md: evaluate row removed", "CLAUDE.md: evaluate references removed"],
8
8
  "cleanup": "Already cleaned — routing.md and CLAUDE.md updated 2026-04-02"
9
+ },
10
+ {
11
+ "id": "autoReview-flag",
12
+ "summary": "autoReview ENGINE_DEFAULT consolidated into evalLoop",
13
+ "deprecated": "2026-04-15",
14
+ "reason": "Redundant with evalLoop; evalLoop is the single gate for the review+fix cycle",
15
+ "locations": ["engine/shared.js ENGINE_DEFAULTS.autoReview", "engine.js discoverFromPrs autoReview variable"],
16
+ "cleanup": "Removed from ENGINE_DEFAULTS, removed autoReview variable from engine.js, replaced with reviewEnabled = evalLoopEnabled && pollEnabled"
9
17
  }
10
18
  ]
@@ -90,6 +90,13 @@ function setCooldownFailure(key) {
90
90
  saveCooldowns();
91
91
  }
92
92
 
93
+ function clearCooldown(key) {
94
+ if (!dispatchCooldowns.has(key)) return false;
95
+ dispatchCooldowns.delete(key);
96
+ saveCooldowns();
97
+ return true;
98
+ }
99
+
93
100
  function isAlreadyDispatched(key) {
94
101
  const dispatch = queries.getDispatch();
95
102
  // Check pending and active
@@ -130,6 +137,7 @@ module.exports = {
130
137
  setCooldownWithContext,
131
138
  getCoalescedContexts,
132
139
  setCooldownFailure,
140
+ clearCooldown,
133
141
  isAlreadyDispatched,
134
142
  isBranchActive,
135
143
  };
@@ -1712,6 +1712,37 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
1712
1712
 
1713
1713
  // Archive is manual — user archives plans from the dashboard when ready
1714
1714
 
1715
+ // Scheduled task back-reference: update schedule-runs.json and write linked inbox note
1716
+ if (meta?.item?._scheduleId) {
1717
+ try {
1718
+ const scheduleId = meta.item._scheduleId;
1719
+ const itemId = meta.item.id;
1720
+ const schedRunsPath = path.join(ENGINE_DIR, 'schedule-runs.json');
1721
+ mutateJsonFileLocked(schedRunsPath, (runs) => {
1722
+ runs[scheduleId] = {
1723
+ lastRun: typeof runs[scheduleId] === 'string' ? runs[scheduleId] : (runs[scheduleId]?.lastRun || ts()),
1724
+ lastWorkItemId: itemId,
1725
+ lastResult: effectiveSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR,
1726
+ lastCompletedAt: ts(),
1727
+ };
1728
+ return runs;
1729
+ }, { defaultValue: {} });
1730
+ // Write a completion note to inbox with back-references
1731
+ const noteSlug = `sched-completion-${scheduleId}`;
1732
+ const status = effectiveSuccess ? 'succeeded' : 'failed';
1733
+ const noteContent = `# Scheduled Task ${status}: ${meta.item.title || scheduleId}\n\n` +
1734
+ `**Schedule:** \`${scheduleId}\`\n` +
1735
+ `**Work Item:** \`${itemId}\`\n` +
1736
+ `**Result:** ${status}\n` +
1737
+ (resultSummary ? `\n## Summary\n${resultSummary}\n` : '');
1738
+ shared.writeToInbox('engine', noteSlug, noteContent, null, {
1739
+ sourceItem: itemId,
1740
+ scheduleId,
1741
+ });
1742
+ log('info', `Scheduled task ${scheduleId} (${itemId}) → ${status}, back-reference written`);
1743
+ } catch (err) { log('warn', `Scheduled task back-reference: ${err.message}`); }
1744
+ }
1745
+
1715
1746
  // Clean up worktree for non-shared-branch tasks after completion
1716
1747
  if (meta?.branch && meta?.branchStrategy !== 'shared-branch') {
1717
1748
  try {
@@ -116,7 +116,9 @@ function discoverScheduledWork(config) {
116
116
  if (!sched.id || !sched.cron || !sched.title) continue;
117
117
  if (!sched.enabled) continue; // truthy check — matches dashboard UI badge behavior
118
118
 
119
- const lastRun = runs[sched.id] || null;
119
+ // Backward compat: runs[sched.id] can be a string (old format) or object (new format)
120
+ const runEntry = runs[sched.id] || null;
121
+ const lastRun = typeof runEntry === 'string' ? runEntry : (runEntry?.lastRun || null);
120
122
  if (!shouldRunNow(sched, lastRun)) continue;
121
123
 
122
124
  work.push({
@@ -133,8 +135,9 @@ function discoverScheduledWork(config) {
133
135
  _scheduleId: sched.id,
134
136
  });
135
137
 
136
- // Record run time inside the lock
137
- runs[sched.id] = ts();
138
+ // Record run time inside the lock — preserve existing fields (lastWorkItemId, lastResult, etc.)
139
+ const existing = typeof runs[sched.id] === 'object' && runs[sched.id] ? runs[sched.id] : {};
140
+ runs[sched.id] = { ...existing, lastRun: ts() };
138
141
  }
139
142
  }, { defaultValue: {} });
140
143
 
package/engine/shared.js CHANGED
@@ -295,18 +295,22 @@ function parseNoteId(content) {
295
295
  return m ? m[1] : null;
296
296
  }
297
297
 
298
- function writeToInbox(agentId, slug, content, _inboxDir) {
298
+ function writeToInbox(agentId, slug, content, _inboxDir, metadata) {
299
299
  try {
300
300
  const inboxDir = _inboxDir || path.join(MINIONS_DIR, 'notes', 'inbox');
301
301
  const prefix = `${agentId}-${slug}-${dateStamp()}`;
302
302
  const existing = safeReadDir(inboxDir).find(f => f.startsWith(prefix));
303
303
  if (existing) return false;
304
304
  const noteId = `NOTE-${uid()}`;
305
+ // Build optional metadata lines for frontmatter injection
306
+ const metaLines = (metadata && typeof metadata === 'object')
307
+ ? Object.entries(metadata).filter(([, v]) => v != null).map(([k, v]) => `${k}: ${v}`).join('\n')
308
+ : '';
305
309
  // Inject structured ID as YAML frontmatter if content doesn't already have it
306
310
  const hasFrontmatter = /^\s*---[\r\n]/.test(content);
307
311
  const tagged = hasFrontmatter
308
- ? content.replace(/^\s*---[\r\n]+/, `---\nid: ${noteId}\n`)
309
- : `---\nid: ${noteId}\nagent: ${agentId}\ndate: ${dateStamp()}\n---\n\n${content}`;
312
+ ? content.replace(/^\s*---[\r\n]+/, `---\nid: ${noteId}\n${metaLines ? metaLines + '\n' : ''}`)
313
+ : `---\nid: ${noteId}\nagent: ${agentId}\ndate: ${dateStamp()}\n${metaLines ? metaLines + '\n' : ''}---\n\n${content}`;
310
314
  const filePath = path.join(inboxDir, `${prefix}.md`);
311
315
  safeWrite(filePath, tagged);
312
316
  return noteId;
@@ -532,7 +536,6 @@ const ENGINE_DEFAULTS = {
532
536
  autoDecompose: true, // auto-decompose implement:large items into sub-tasks
533
537
  autoApprovePlans: false, // auto-approve PRDs without waiting for human approval
534
538
  autoArchive: false, // opt-in: auto-archive plans after verify completes (false = mark ready, user archives manually)
535
- autoReview: true, // auto-dispatch review agents for new PRs (disable for manual review workflow)
536
539
  autoFixConflicts: true, // auto-dispatch fix agents when a PR has merge conflicts
537
540
  autoFixBuilds: true, // auto-dispatch fix agents when a PR build fails
538
541
  meetingRoundTimeout: 600000, // 10min per meeting round before auto-advance
@@ -618,6 +621,12 @@ const PR_POLLABLE_STATUSES = new Set([PR_STATUS.ACTIVE, PR_STATUS.LINKED]);
618
621
  const WATCH_STATUS = { ACTIVE: 'active', PAUSED: 'paused', TRIGGERED: 'triggered', EXPIRED: 'expired' };
619
622
  const WATCH_TARGET_TYPE = { PR: 'pr', WORK_ITEM: 'work-item' };
620
623
  const WATCH_CONDITION = { MERGED: 'merged', BUILD_FAIL: 'build-fail', BUILD_PASS: 'build-pass', COMPLETED: 'completed', FAILED: 'failed', STATUS_CHANGE: 'status-change', ANY: 'any' };
624
+ // Absolute conditions auto-expire on first trigger when stopAfter=0 (fire-once semantics).
625
+ // Change-based conditions (status-change, any) run forever when stopAfter=0.
626
+ const WATCH_ABSOLUTE_CONDITIONS = new Set([
627
+ WATCH_CONDITION.MERGED, WATCH_CONDITION.BUILD_FAIL, WATCH_CONDITION.BUILD_PASS,
628
+ WATCH_CONDITION.COMPLETED, WATCH_CONDITION.FAILED,
629
+ ]);
621
630
 
622
631
  /** Update per-agent review metrics (prsApproved/prsRejected). Only writes for configured agents. */
623
632
  function trackReviewMetric(pr, newReviewStatus, config) {
@@ -1144,7 +1153,7 @@ module.exports = {
1144
1153
  classifyInboxItem,
1145
1154
  ENGINE_DEFAULTS,
1146
1155
  WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, DISPATCH_RESULT, trackReviewMetric, queuePlanToPrd,
1147
- WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION,
1156
+ WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS,
1148
1157
  PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
1149
1158
  FAILURE_CLASS, ESCALATION_POLICY, COMPLETION_FIELDS,
1150
1159
  DEFAULT_AGENT_METRICS,
package/engine/watches.js CHANGED
@@ -237,15 +237,10 @@ function checkWatches(config, state) {
237
237
  }
238
238
  log('info', `Watch triggered: ${watch.id} — ${result.message}`);
239
239
 
240
- // Expire: absolute conditions auto-expire when stopAfter is 0 (fire-once semantics),
241
- // change-based conditions (status-change, any) respect stopAfter literally (0 = run forever).
242
- const isAbsolute = WATCH_ABSOLUTE_CONDITIONS.has(watch.condition);
240
+ // Expire when stopAfter limit is reached. stopAfter=0 means run forever (no limit).
243
241
  if (watch.stopAfter > 0 && watch.triggerCount >= watch.stopAfter) {
244
242
  watch.status = WATCH_STATUS.EXPIRED;
245
243
  log('info', `Watch expired (stopAfter limit reached): ${watch.id}`);
246
- } else if (isAbsolute && watch.stopAfter === 0) {
247
- watch.status = WATCH_STATUS.EXPIRED;
248
- log('info', `Watch expired (absolute condition auto-expire): ${watch.id}`);
249
244
  }
250
245
  } else if (watch.onNotMet === 'notify' && watch.owner) {
251
246
  // Queue per-poll notification when condition is not yet met — unique key per poll
package/engine.js CHANGED
@@ -1500,7 +1500,7 @@ function updateSnapshot(config) {
1500
1500
 
1501
1501
  const { COOLDOWN_PATH, dispatchCooldowns, loadCooldowns, saveCooldowns,
1502
1502
  isOnCooldown, setCooldown, setCooldownWithContext, getCoalescedContexts,
1503
- setCooldownFailure, isAlreadyDispatched, isBranchActive } = require('./engine/cooldown');
1503
+ setCooldownFailure, clearCooldown, isAlreadyDispatched, isBranchActive } = require('./engine/cooldown');
1504
1504
 
1505
1505
 
1506
1506
 
@@ -1898,6 +1898,13 @@ async function discoverFromPrs(config, project) {
1898
1898
 
1899
1899
  const projMeta = { name: project?.name, localPath: project?.localPath };
1900
1900
 
1901
+ // Resolve poll-enabled per project — stale reviewStatus is untrustworthy without poller
1902
+ const isAdoProject = project?.repoHost !== 'github';
1903
+ const pollEnabled = isAdoProject
1904
+ ? (config.engine?.adoPollEnabled ?? DEFAULTS.adoPollEnabled)
1905
+ : (config.engine?.ghPollEnabled ?? DEFAULTS.ghPollEnabled);
1906
+ const evalLoopEnabled = config.engine?.evalLoop !== false;
1907
+
1901
1908
  // Collect active PR dispatches to prevent simultaneous review+fix on same PR
1902
1909
  const dispatch = getDispatch();
1903
1910
  const activePrIds = new Set(
@@ -1945,10 +1952,10 @@ async function discoverFromPrs(config, project) {
1945
1952
  log('warn', `PR ${pr.id}: review→fix escalated after ${evalCycles} cycles — suspending auto-dispatch`);
1946
1953
  }
1947
1954
 
1948
- // PRs needing review: pending review status and not already reviewed without new commits
1949
- const autoReview = config.engine?.autoReview !== false;
1955
+ // PRs needing review: evalLoop gates the entire review+fix cycle; pollEnabled ensures reviewStatus is fresh
1956
+ const reviewEnabled = evalLoopEnabled && pollEnabled;
1950
1957
  const alreadyReviewed = pr.lastReviewedAt && (!pr.lastPushedAt || pr.lastPushedAt <= pr.lastReviewedAt);
1951
- const needsReview = autoReview && reviewStatus === 'pending' && !alreadyReviewed && !evalEscalated;
1958
+ const needsReview = reviewEnabled && reviewStatus === 'pending' && !alreadyReviewed && !evalEscalated;
1952
1959
  if (needsReview) {
1953
1960
  const key = `review-${project?.name || 'default'}-${pr.id}`;
1954
1961
  if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
@@ -1972,6 +1979,47 @@ async function discoverFromPrs(config, project) {
1972
1979
  } catch {}
1973
1980
  continue;
1974
1981
  }
1982
+ } catch (e) { log('warn', `Pre-dispatch vote check for ${pr.id}: ${e.message} — skipping dispatch`); continue; }
1983
+
1984
+ const agentId = resolveAgent('review', config);
1985
+ if (!agentId) continue;
1986
+
1987
+ const item = buildPrDispatch(agentId, config, project, pr, 'review', {
1988
+ pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: pr.branch || '',
1989
+ pr_author: pr.agent || '', pr_url: pr.url || '',
1990
+ }, `Review ${pr.id}: ${pr.title}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
1991
+ if (item) { newWork.push(item); }
1992
+ }
1993
+
1994
+ // Re-review after fix: trigger when a fix was pushed after the last minions review,
1995
+ // or when no minions review has completed yet (e.g. human-feedback-only fix path).
1996
+ const fixedAfterReview = !!(pr.minionsReview?.fixedAt &&
1997
+ (!pr.lastReviewedAt || pr.minionsReview.fixedAt > pr.lastReviewedAt));
1998
+ const needsReReview = autoReview && reviewStatus === 'waiting' &&
1999
+ fixedAfterReview && !evalEscalated;
2000
+ if (needsReReview) {
2001
+ const key = `rereview-${project?.name || 'default'}-${pr.id}`;
2002
+ // Skip isAlreadyDispatched — fixedAfterReview/lastReviewedAt already dedupe; the 1hr
2003
+ // completed-dispatch window would block legitimate re-reviews within the hour after a fix
2004
+ if (isOnCooldown(key, cooldownMs)) continue;
2005
+
2006
+ // Pre-dispatch live vote check — cached 'waiting' may be stale if reviewer already acted
2007
+ try {
2008
+ const checkFn = project.repoHost === 'github' ? ghCheckLiveReview : adoCheckLiveReview;
2009
+ const liveStatus = await checkFn(pr, project);
2010
+ if (liveStatus && liveStatus !== 'waiting') {
2011
+ log('info', `Pre-dispatch vote check: ${pr.id} is ${liveStatus} (cached was waiting) — skipping re-review`);
2012
+ if (pr.reviewStatus !== 'approved') pr.reviewStatus = liveStatus;
2013
+ try {
2014
+ mutateJsonFileLocked(projectPrPath(project), data => {
2015
+ if (!Array.isArray(data)) return data;
2016
+ const target = data.find(p => p.id === pr.id);
2017
+ if (target && target.reviewStatus !== 'approved') target.reviewStatus = liveStatus;
2018
+ return data;
2019
+ });
2020
+ } catch {}
2021
+ continue;
2022
+ }
1975
2023
  } catch (e) { log('warn', `Pre-dispatch vote check for ${pr.id}: ${e.message}`); }
1976
2024
 
1977
2025
  const agentId = resolveAgent('review', config);
@@ -2013,7 +2061,15 @@ async function discoverFromPrs(config, project) {
2013
2061
  const hasCoalescedFeedback = (dispatchCooldowns.get(humanFixKey)?.pendingContexts || []).length > 0;
2014
2062
  if ((pr.humanFeedback?.pendingFix || hasCoalescedFeedback) && !awaitingReReview && !fixDispatched) {
2015
2063
  const key = humanFixKey;
2016
- if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) {
2064
+ let staleCoalesced = [];
2065
+ const alreadyDispatched = isAlreadyDispatched(key);
2066
+ const blockedByCooldown = isOnCooldown(key, cooldownMs);
2067
+ if (blockedByCooldown && !alreadyDispatched) {
2068
+ staleCoalesced = getCoalescedContexts(key);
2069
+ clearCooldown(key);
2070
+ log('info', `Cleared stale cooldown for ${key} — no matching dispatch history`);
2071
+ }
2072
+ if (alreadyDispatched || isOnCooldown(key, cooldownMs)) {
2017
2073
  // Coalesce: save feedback for next dispatch
2018
2074
  if (pr.humanFeedback?.feedbackContent) {
2019
2075
  setCooldownWithContext(key, { feedbackContent: pr.humanFeedback.feedbackContent, timestamp: ts() });
@@ -2023,7 +2079,7 @@ async function discoverFromPrs(config, project) {
2023
2079
  const agentId = resolveAgent('fix', config, pr.agent);
2024
2080
  if (!agentId) continue;
2025
2081
 
2026
- const coalesced = getCoalescedContexts(key);
2082
+ const coalesced = [...staleCoalesced, ...getCoalescedContexts(key)];
2027
2083
  let reviewNote = pr.humanFeedback.feedbackContent || 'See PR thread comments';
2028
2084
  if (coalesced.length > 0) {
2029
2085
  const earlier = coalesced.map(c => c.feedbackContent).filter(Boolean).join('\n\n---\n\n');
@@ -2035,7 +2091,7 @@ async function discoverFromPrs(config, project) {
2035
2091
  reviewer: 'Human Reviewer',
2036
2092
  review_note: reviewNote,
2037
2093
  }, `Fix ${pr.id}: ${pr.title || ''} — human feedback`, { dispatchKey: key, source: 'pr-human-feedback', pr, branch: pr.branch, project: projMeta });
2038
- if (item) { newWork.push(item); setCooldown(key); fixDispatched = true; }
2094
+ if (item) { newWork.push(item); fixDispatched = true; }
2039
2095
  }
2040
2096
 
2041
2097
  // PRs with build failures — route to author (has session context from implementing)
@@ -3561,4 +3617,3 @@ if (require.main === module) {
3561
3617
  const [cmd, ...args] = process.argv.slice(2);
3562
3618
  handleCommand(cmd, args);
3563
3619
  }
3564
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.981",
3
+ "version": "0.1.983",
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"