@yemi33/minions 0.1.977 → 0.1.978

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.977 (2026-04-14)
3
+ ## 0.1.978 (2026-04-15)
4
4
 
5
5
  ### Features
6
6
  - free-form interval input + CC create-watch action
@@ -25,6 +25,7 @@
25
25
  - CC tab unread dot + reopened badge on work items
26
26
 
27
27
  ### Fixes
28
+ - ENGINE_DEFAULTS undefined in tick + playbook empty-var false positives
28
29
  - defer review setCooldown to post-gating in discoverWork
29
30
  - move review verdict check before updateWorkItemStatus(DONE)
30
31
  - address review feedback — move writeToInbox outside lock, add absolute condition auto-expire
@@ -44,7 +45,6 @@
44
45
  - defer setCooldown to post-gating in discoverWork
45
46
  - fix CC queued message 'already processing' and thinking UX stacking
46
47
  - enforce --timeout 1 on all azureauth calls to prevent agent orphans (#1063)
47
- - cancel steering kill watcher on resume spawn (#1052) (#1062)
48
48
 
49
49
  ### Other
50
50
  - refactor(watches): rename maxTriggers→stopAfter, add onNotMet per-poll action
@@ -388,15 +388,14 @@ async function ccSend() {
388
388
  }
389
389
  var wasAborted = await _ccDoSend(message, false, originTabId);
390
390
 
391
- // Flush all queued messages at once combine into a single request.
392
- // Loop in case user queues more messages while the combined flush is processing.
391
+ // Flush queued messages to the ORIGINAL tab, even if user switched tabs
393
392
  while (tab._queue && tab._queue.length > 0) {
394
393
  if (wasAborted) {
395
394
  await new Promise(function(r) { setTimeout(r, 1500); });
396
395
  }
397
- var combined = tab._queue.splice(0);
396
+ var next = tab._queue.shift();
398
397
  _renderQueueIndicator();
399
- wasAborted = await _ccDoSend(combined.join('\n\n'), false, originTabId);
398
+ wasAborted = await _ccDoSend(next, false, originTabId);
400
399
  }
401
400
  }
402
401
 
@@ -268,10 +268,32 @@ function validatePlaybookVars(playbookName, vars) {
268
268
  return { valid: missing.length === 0, missing };
269
269
  }
270
270
 
271
+ // ─── Playbook Path Resolution ───────────────────────────────────────────────
272
+
273
+ /**
274
+ * Resolve playbook file path, checking project-local override first.
275
+ * If projects/<projectName>/playbooks/<playbookType>.md exists, use it.
276
+ * Otherwise fall back to the global playbooks/<playbookType>.md.
277
+ * @param {string|null|undefined} projectName — project name (directory under projects/)
278
+ * @param {string} playbookType — playbook type name (e.g. 'implement', 'review')
279
+ * @returns {string} absolute path to the playbook file
280
+ */
281
+ function resolvePlaybookPath(projectName, playbookType) {
282
+ if (projectName) {
283
+ const localPath = path.join(MINIONS_DIR, 'projects', projectName, 'playbooks', `${playbookType}.md`);
284
+ try {
285
+ fs.accessSync(localPath, fs.constants.R_OK);
286
+ return localPath;
287
+ } catch { /* no local override — fall through to global */ }
288
+ }
289
+ return path.join(PLAYBOOKS_DIR, `${playbookType}.md`);
290
+ }
291
+
271
292
  // ─── Playbook Renderer ──────────────────────────────────────────────────────
272
293
 
273
294
  function renderPlaybook(type, vars) {
274
- const pbPath = path.join(PLAYBOOKS_DIR, `${type}.md`);
295
+ const projectName = vars.project_name || null;
296
+ const pbPath = resolvePlaybookPath(projectName, type);
275
297
  let content;
276
298
  try { content = fs.readFileSync(pbPath, 'utf8'); } catch {
277
299
  log('warn', `Playbook not found: ${type}`);
@@ -361,6 +383,9 @@ function renderPlaybook(type, vars) {
361
383
  return null;
362
384
  }
363
385
 
386
+ // Capture which vars are actually referenced in the template before substitution
387
+ const referencedVars = new Set((content.match(/\{\{(\w+)\}\}/g) || []).map(m => m.slice(2, -2)));
388
+
364
389
  // Process conditional blocks: {{#key}}...{{/key}} — include block only if key is truthy
365
390
  content = content.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, block) => {
366
391
  const val = allVars[key];
@@ -380,9 +405,9 @@ function renderPlaybook(type, vars) {
380
405
  log('warn', `Playbook "${type}": substituted values contain unresolved {{...}} patterns (potential self-reference): ${selfRefVars.join(', ')}`);
381
406
  }
382
407
 
383
- // Warn on variables that resolved to empty string (skip known-optional vars)
408
+ // Warn on variables that resolved to empty string only for vars actually used in the template
384
409
  const emptyVars = Object.entries(allVars)
385
- .filter(([key, val]) => String(val) === '' && !PLAYBOOK_OPTIONAL_VARS.has(key))
410
+ .filter(([key, val]) => String(val) === '' && referencedVars.has(key) && !PLAYBOOK_OPTIONAL_VARS.has(key))
386
411
  .map(([key]) => key);
387
412
  if (emptyVars.length > 0) {
388
413
  log('warn', `Playbook "${type}": template variables resolved to empty string: ${emptyVars.join(', ')}`);
@@ -565,6 +590,7 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
565
590
 
566
591
  module.exports = {
567
592
  renderPlaybook,
593
+ resolvePlaybookPath,
568
594
  validatePlaybookVars,
569
595
  PLAYBOOK_REQUIRED_VARS,
570
596
  PLAYBOOK_OPTIONAL_VARS,
package/engine/shared.js CHANGED
@@ -617,8 +617,6 @@ const PR_POLLABLE_STATUSES = new Set([PR_STATUS.ACTIVE, PR_STATUS.LINKED]);
617
617
  const WATCH_STATUS = { ACTIVE: 'active', PAUSED: 'paused', TRIGGERED: 'triggered', EXPIRED: 'expired' };
618
618
  const WATCH_TARGET_TYPE = { PR: 'pr', WORK_ITEM: 'work-item' };
619
619
  const WATCH_CONDITION = { MERGED: 'merged', BUILD_FAIL: 'build-fail', BUILD_PASS: 'build-pass', COMPLETED: 'completed', FAILED: 'failed', STATUS_CHANGE: 'status-change', ANY: 'any' };
620
- // Absolute conditions check point-in-time state, not transitions — auto-expire after first trigger when stopAfter is 0
621
- const WATCH_ABSOLUTE_CONDITIONS = new Set([WATCH_CONDITION.MERGED, WATCH_CONDITION.BUILD_FAIL, WATCH_CONDITION.BUILD_PASS, WATCH_CONDITION.COMPLETED, WATCH_CONDITION.FAILED]);
622
620
 
623
621
  /** Update per-agent review metrics (prsApproved/prsRejected). Only writes for configured agents. */
624
622
  function trackReviewMetric(pr, newReviewStatus, config) {
@@ -1145,7 +1143,7 @@ module.exports = {
1145
1143
  classifyInboxItem,
1146
1144
  ENGINE_DEFAULTS,
1147
1145
  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,
1148
- WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS,
1146
+ WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION,
1149
1147
  PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
1150
1148
  FAILURE_CLASS, ESCALATION_POLICY, COMPLETION_FIELDS,
1151
1149
  DEFAULT_AGENT_METRICS,
package/engine.js CHANGED
@@ -1984,46 +1984,6 @@ async function discoverFromPrs(config, project) {
1984
1984
  if (item) { newWork.push(item); }
1985
1985
  }
1986
1986
 
1987
- // Re-review after fix: only trigger when fixedAt > lastReviewedAt (not a broad !alreadyReviewed
1988
- // fallback — that caused infinite re-review loops on GitHub where self-approval is blocked)
1989
- const fixedAfterReview = !!(pr.minionsReview?.fixedAt &&
1990
- pr.lastReviewedAt && pr.minionsReview.fixedAt > pr.lastReviewedAt);
1991
- const needsReReview = autoReview && reviewStatus === 'waiting' &&
1992
- fixedAfterReview && !evalEscalated;
1993
- if (needsReReview) {
1994
- const key = `review-${project?.name || 'default'}-${pr.id}`;
1995
- // Skip isAlreadyDispatched — fixedAfterReview/alreadyReviewed already dedup; the 1hr
1996
- // completed-dispatch window would block legitimate re-reviews within the hour after a fix
1997
- if (isOnCooldown(key, cooldownMs)) continue;
1998
-
1999
- // Pre-dispatch live vote check — cached 'waiting' may be stale if reviewer already acted
2000
- try {
2001
- const checkFn = project.repoHost === 'github' ? ghCheckLiveReview : adoCheckLiveReview;
2002
- const liveStatus = await checkFn(pr, project);
2003
- if (liveStatus && liveStatus !== 'waiting') {
2004
- log('info', `Pre-dispatch vote check: ${pr.id} is ${liveStatus} (cached was waiting) — skipping re-review`);
2005
- if (pr.reviewStatus !== 'approved') pr.reviewStatus = liveStatus;
2006
- try {
2007
- mutateJsonFileLocked(projectPrPath(project), data => {
2008
- if (!Array.isArray(data)) return data;
2009
- const target = data.find(p => p.id === pr.id);
2010
- if (target && target.reviewStatus !== 'approved') target.reviewStatus = liveStatus;
2011
- return data;
2012
- });
2013
- } catch {}
2014
- continue;
2015
- }
2016
- } catch (e) { log('warn', `Pre-dispatch vote check for ${pr.id}: ${e.message}`); }
2017
-
2018
- const agentId = resolveAgent('review', config);
2019
- if (!agentId) continue;
2020
- const item = buildPrDispatch(agentId, config, project, pr, 'review', {
2021
- pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: pr.branch || '',
2022
- pr_author: pr.agent || '', pr_url: pr.url || '',
2023
- }, `Review ${pr.id}: ${pr.title}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
2024
- if (item) { newWork.push(item); }
2025
- }
2026
-
2027
1987
  // PRs with changes requested → route back to author for fix
2028
1988
  let fixDispatched = false;
2029
1989
  if (reviewStatus === 'changes-requested' && !awaitingReReview && !evalEscalated) {
@@ -2037,7 +1997,7 @@ async function discoverFromPrs(config, project) {
2037
1997
  review_note: pr.minionsReview?.note || pr.reviewNote || 'See PR thread comments',
2038
1998
  }, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
2039
1999
  if (item) {
2040
- newWork.push(item); fixDispatched = true;
2000
+ newWork.push(item); setCooldown(key); fixDispatched = true;
2041
2001
  // Increment review→fix cycle counter
2042
2002
  try {
2043
2003
  mutatePullRequests(projectPrPath(project), prs => {
@@ -2075,7 +2035,7 @@ async function discoverFromPrs(config, project) {
2075
2035
  reviewer: 'Human Reviewer',
2076
2036
  review_note: reviewNote,
2077
2037
  }, `Fix ${pr.id}: ${pr.title || ''} — human feedback`, { dispatchKey: key, source: 'pr-human-feedback', pr, branch: pr.branch, project: projMeta });
2078
- if (item) { newWork.push(item); fixDispatched = true; }
2038
+ if (item) { newWork.push(item); setCooldown(key); fixDispatched = true; }
2079
2039
  }
2080
2040
 
2081
2041
  // PRs with build failures — route to author (has session context from implementing)
@@ -2124,7 +2084,7 @@ async function discoverFromPrs(config, project) {
2124
2084
  review_note: reviewNote,
2125
2085
  }, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
2126
2086
  if (item) {
2127
- newWork.push(item);
2087
+ newWork.push(item); setCooldown(key);
2128
2088
  // Increment build fix attempts counter
2129
2089
  try {
2130
2090
  const prPath = projectPrPath(project);
@@ -2181,6 +2141,7 @@ async function discoverFromPrs(config, project) {
2181
2141
  }, `Fix merge conflicts on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
2182
2142
  if (item) {
2183
2143
  newWork.push(item);
2144
+ setCooldown(key);
2184
2145
  // Record dispatch timestamp so re-dispatch is suppressed during ADO lag window
2185
2146
  try {
2186
2147
  mutatePullRequests(projectPrPath(project), prs => {
@@ -3390,7 +3351,7 @@ async function tickInner() {
3390
3351
  if (busyAgents.has(item.agent)) {
3391
3352
  // Agent busy reassignment: if item has been waiting on a busy agent past the threshold,
3392
3353
  // try to find an alternative agent via routing. Skip explicitly assigned items.
3393
- const reassignMs = config.engine?.agentBusyReassignMs ?? ENGINE_DEFAULTS.agentBusyReassignMs;
3354
+ const reassignMs = config.engine?.agentBusyReassignMs ?? DEFAULTS.agentBusyReassignMs;
3394
3355
  const isExplicitReassign = !!item.meta?.item?.agent;
3395
3356
  if (!isExplicitReassign && reassignMs > 0 && item._agentBusySince) {
3396
3357
  const busySinceMs = new Date(item._agentBusySince).getTime();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.977",
3
+ "version": "0.1.978",
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"