@yemi33/minions 0.1.1044 → 0.1.1046

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,12 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1044 (2026-04-16)
3
+ ## 0.1.1046 (2026-04-16)
4
+
5
+ ### Fixes
6
+ - harden settings save and migrate pr poll config
4
7
 
5
8
  ### Other
9
+ - Harden loop watch conversion
6
10
  - Add watches sidebar activity badge
7
11
  - test(cli): add unit tests for handleCommand, start, stop, kill, spawn (#1191)
8
12
  - chore: untrack pipeline files — local config only
@@ -1,14 +1,18 @@
1
1
  // settings.js — Settings panel functions extracted from dashboard.html
2
2
 
3
+ let _settingsData = null;
4
+
3
5
  async function openSettings() {
4
6
  document.getElementById('modal-title').textContent = 'Settings';
5
7
  document.getElementById('modal-body').innerHTML = '<p style="color:var(--muted)">Loading...</p>';
6
8
  document.getElementById('modal').classList.add('open');
7
9
 
10
+ _settingsData = null;
8
11
  let data;
9
12
  try {
10
13
  const res = await fetch('/api/settings');
11
14
  data = await res.json();
15
+ _settingsData = data;
12
16
  } catch (e) { showToast('cmd-toast', 'Failed to load settings: ' + e.message, false); return; }
13
17
 
14
18
  const e = data.engine || {};
@@ -56,8 +60,8 @@ async function openSettings() {
56
60
  settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Poll GitHub PR status and comments each tick (reconciliation always runs)') +
57
61
  '</div>' +
58
62
  '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
59
- settingsField('PR Status Poll Frequency', 'set-adoPollStatusEvery', e.adoPollStatusEvery || 6, 'ticks', 'Poll PR build/review/merge status every N ticks for both ADO and GitHub (~6 min at default tick rate)') +
60
- settingsField('PR Comments Poll Frequency', 'set-adoPollCommentsEvery', e.adoPollCommentsEvery || 12, 'ticks', 'Poll PR human comments every N ticks for both ADO and GitHub (~12 min at default tick rate)') +
63
+ settingsField('PR Status Poll Frequency', 'set-prPollStatusEvery', e.prPollStatusEvery ?? e.adoPollStatusEvery ?? 12, 'ticks', 'Poll PR build/review/merge status every N ticks for both ADO and GitHub (~12 min at default tick rate)') +
64
+ settingsField('PR Comments Poll Frequency', 'set-prPollCommentsEvery', e.prPollCommentsEvery ?? e.adoPollCommentsEvery ?? 12, 'ticks', 'Poll PR human comments every N ticks for both ADO and GitHub (~12 min at default tick rate)') +
61
65
  '</div>' +
62
66
  '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
63
67
  settingsField('Eval Max Iterations', 'set-evalMaxIterations', e.evalMaxIterations || 3, '', 'Max review→fix cycles before escalating (1-10)') +
@@ -258,8 +262,8 @@ async function saveSettings() {
258
262
  autoCompletePrs: document.getElementById('set-autoCompletePrs').checked,
259
263
  adoPollEnabled: document.getElementById('set-adoPollEnabled').checked,
260
264
  ghPollEnabled: document.getElementById('set-ghPollEnabled').checked,
261
- adoPollStatusEvery: document.getElementById('set-adoPollStatusEvery').value,
262
- adoPollCommentsEvery: document.getElementById('set-adoPollCommentsEvery').value,
265
+ prPollStatusEvery: document.getElementById('set-prPollStatusEvery').value,
266
+ prPollCommentsEvery: document.getElementById('set-prPollCommentsEvery').value,
263
267
  evalMaxIterations: document.getElementById('set-evalMaxIterations').value,
264
268
  evalMaxCost: document.getElementById('set-evalMaxCost').value || null,
265
269
  maxBuildFixAttempts: document.getElementById('set-maxBuildFixAttempts').value,
@@ -305,7 +309,8 @@ async function saveSettings() {
305
309
  agentsPayload[id][field] = el.value;
306
310
  });
307
311
 
308
- const projectsPayload = (data.projects || []).map(function(p) {
312
+ const currentProjects = (_settingsData && Array.isArray(_settingsData.projects)) ? _settingsData.projects : [];
313
+ const projectsPayload = currentProjects.map(function(p) {
309
314
  return {
310
315
  name: p.name,
311
316
  workSources: {
package/dashboard.js CHANGED
@@ -672,11 +672,110 @@ function parseCCActions(text) {
672
672
  // This pure function detects /loop invocation in CC response text and synthesizes
673
673
  // a create-watch action as a fallback. Returns null if no conversion needed.
674
674
 
675
- function _detectLoopInvocation(text, actions) {
676
- if (!text) return null;
675
+ function _detectLoopInvocation(text, actions, toolUses) {
676
+ const observedToolUses = Array.isArray(toolUses) ? toolUses : [];
677
+ if (!text && observedToolUses.length === 0) return null;
677
678
  // If a create-watch action was already emitted, no fallback needed
678
679
  if (actions && actions.some(a => a.type === 'create-watch')) return null;
679
680
 
681
+ function _extractTargetFromValue(value, keyHint) {
682
+ if (value == null) return null;
683
+ const hint = String(keyHint || '').toLowerCase();
684
+ if (Array.isArray(value)) {
685
+ for (const item of value) {
686
+ const nested = _extractTargetFromValue(item, hint);
687
+ if (nested) return nested;
688
+ }
689
+ return null;
690
+ }
691
+ if (typeof value === 'object') {
692
+ for (const [k, v] of Object.entries(value)) {
693
+ const nested = _extractTargetFromValue(v, k);
694
+ if (nested) return nested;
695
+ }
696
+ return null;
697
+ }
698
+ const str = String(value).trim();
699
+ if (!str) return null;
700
+ const prUrlMatch = str.match(/\/pull\/(\d+)\b/i) || str.match(/\/pullrequest\/(\d+)\b/i);
701
+ if (prUrlMatch) return { target: prUrlMatch[1], targetType: 'pr' };
702
+ const prMatch = str.match(/\bPR[- #:]?(\d+)\b/i) || str.match(/\bpull[- ]request[- #:]?(\d+)\b/i);
703
+ if (prMatch) return { target: prMatch[1], targetType: 'pr' };
704
+ const wiMatch = str.match(/\bW-([a-z0-9]+)\b/i);
705
+ if (wiMatch) return { target: 'W-' + wiMatch[1], targetType: 'work-item' };
706
+ if ((hint.includes('pr') || hint.includes('pull')) && /^\d+$/.test(str)) return { target: str, targetType: 'pr' };
707
+ if ((hint.includes('work') || hint.includes('item') || hint === 'id') && /^W-[a-z0-9]+$/i.test(str)) return { target: str.toUpperCase().startsWith('W-') ? 'W-' + str.slice(2) : str, targetType: 'work-item' };
708
+ if (hint.includes('target')) {
709
+ if (/^\d+$/.test(str)) return { target: str, targetType: 'pr' };
710
+ if (/^W-[a-z0-9]+$/i.test(str)) return { target: 'W-' + str.slice(2), targetType: 'work-item' };
711
+ }
712
+ return null;
713
+ }
714
+
715
+ function _extractIntervalFromValue(value, keyHint) {
716
+ if (value == null) return null;
717
+ const hint = String(keyHint || '').toLowerCase();
718
+ if (Array.isArray(value)) {
719
+ for (const item of value) {
720
+ const nested = _extractIntervalFromValue(item, hint);
721
+ if (nested) return nested;
722
+ }
723
+ return null;
724
+ }
725
+ if (typeof value === 'object') {
726
+ for (const [k, v] of Object.entries(value)) {
727
+ const nested = _extractIntervalFromValue(v, k);
728
+ if (nested) return nested;
729
+ }
730
+ return null;
731
+ }
732
+ if (!(hint.includes('interval') || hint.includes('every') || hint.includes('frequency'))) return null;
733
+ const str = String(value).trim().toLowerCase();
734
+ if (!str) return null;
735
+ if (/^\d+$/.test(str)) return str;
736
+ const match = str.match(/^(\d+(?:\.\d+)?)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?)$/i);
737
+ if (!match) return null;
738
+ return match[1] + match[2][0].toLowerCase();
739
+ }
740
+
741
+ function _extractConditionFromValue(value, keyHint) {
742
+ if (value == null) return null;
743
+ const hint = String(keyHint || '').toLowerCase();
744
+ if (Array.isArray(value)) {
745
+ for (const item of value) {
746
+ const nested = _extractConditionFromValue(item, hint);
747
+ if (nested) return nested;
748
+ }
749
+ return null;
750
+ }
751
+ if (typeof value === 'object') {
752
+ for (const [k, v] of Object.entries(value)) {
753
+ const nested = _extractConditionFromValue(v, k);
754
+ if (nested) return nested;
755
+ }
756
+ return null;
757
+ }
758
+ if (!(hint.includes('condition') || hint.includes('until') || hint.includes('goal') || hint.includes('status'))) return null;
759
+ const str = String(value).trim().toLowerCase();
760
+ if (['merged', 'build-pass', 'build-fail', 'completed', 'failed', 'status-change', 'any', 'new-comments', 'vote-change'].includes(str)) return str;
761
+ if (/\b(?:pass(?:es|ing|ed)?|green|succeed(?:s|ed)?|success)\b/i.test(str)) return 'build-pass';
762
+ if (/\b(?:fail(?:s|ing|ed)?|red|broken|broke)\b/i.test(str)) return 'build-fail';
763
+ if (/\bmerge(?:d)?\b/i.test(str)) return 'merged';
764
+ if (/\bcomplete(?:d)?\b/i.test(str)) return 'completed';
765
+ if (/\bfail(?:ed)?\b/i.test(str)) return 'failed';
766
+ if (/\bcomment/i.test(str)) return 'new-comments';
767
+ if (/\bvote|review/i.test(str)) return 'vote-change';
768
+ if (/\bstatus/i.test(str)) return 'any';
769
+ return null;
770
+ }
771
+
772
+ const loopToolSeen = observedToolUses.some(t => /\bloop\b/i.test(String(t?.name || '')));
773
+ const toolText = observedToolUses.map(t => {
774
+ try { return [String(t?.name || ''), JSON.stringify(t?.input || {})].filter(Boolean).join(' '); }
775
+ catch { return String(t?.name || ''); }
776
+ }).join('\n');
777
+ const combinedText = [text || '', toolText].filter(Boolean).join('\n');
778
+
680
779
  // Check for /loop invocation patterns in CC response
681
780
  const loopPatterns = [
682
781
  /\/loop\b/i,
@@ -686,14 +785,21 @@ function _detectLoopInvocation(text, actions) {
686
785
  /\bmonitoring.*\bloop\b/i,
687
786
  /\binvok(?:e|ed|ing).*\bloop\b/i,
688
787
  ];
689
- if (!loopPatterns.some(p => p.test(text))) return null;
788
+ if (!loopToolSeen && !loopPatterns.some(p => p.test(combinedText))) return null;
690
789
 
691
790
  // Extract target — PR number or work item ID
692
- const prMatch = text.match(/\bPR[- #]?(\d+)\b/i) || text.match(/\bpull[- ]request[- #]?(\d+)/i);
693
- const wiMatch = text.match(/\bW-([a-z0-9]+)\b/);
791
+ const directTarget = observedToolUses.map(t => _extractTargetFromValue(t && t.input, t && t.name)).find(Boolean);
792
+ const prMatch = combinedText.match(/\/pull\/(\d+)\b/i) ||
793
+ combinedText.match(/\/pullrequest\/(\d+)\b/i) ||
794
+ combinedText.match(/\bPR[- #:]?(\d+)\b/i) ||
795
+ combinedText.match(/\bpull[- ]request[- #:]?(\d+)/i);
796
+ const wiMatch = combinedText.match(/\bW-([a-z0-9]+)\b/i);
694
797
 
695
798
  let target = null, targetType = 'pr';
696
- if (prMatch) {
799
+ if (directTarget) {
800
+ target = directTarget.target;
801
+ targetType = directTarget.targetType;
802
+ } else if (prMatch) {
697
803
  target = prMatch[1];
698
804
  targetType = 'pr';
699
805
  } else if (wiMatch) {
@@ -703,16 +809,20 @@ function _detectLoopInvocation(text, actions) {
703
809
  if (!target) return null; // Can't synthesize without a target
704
810
 
705
811
  // Extract interval (e.g. "every 15 minutes", "every 5m")
706
- const intervalMatch = text.match(/every\s+(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?)\b/i);
812
+ const directInterval = observedToolUses.map(t => _extractIntervalFromValue(t && t.input, t && t.name)).find(Boolean);
813
+ const intervalMatch = combinedText.match(/every\s+(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?)\b/i);
707
814
  let interval = '5m';
708
- if (intervalMatch) interval = intervalMatch[1] + intervalMatch[2][0];
815
+ if (directInterval) interval = directInterval;
816
+ else if (intervalMatch) interval = intervalMatch[1] + intervalMatch[2][0];
709
817
 
710
818
  // Infer condition from keywords
711
- let condition = 'any';
712
- if (/\bbuild\b/i.test(text) && /\b(?:pass(?:es|ing|ed)?|green|succeed(?:s|ed)?|success)\b/i.test(text)) condition = 'build-pass';
713
- else if (/\bbuild\b/i.test(text) && /\b(?:fail(?:s|ing|ed)?|red|broken|broke)\b/i.test(text)) condition = 'build-fail';
714
- else if (/\bmerge[d]?\b/i.test(text)) condition = 'merged';
715
- else if (/\bcomplete[d]?\b/i.test(text)) condition = 'completed';
819
+ let condition = observedToolUses.map(t => _extractConditionFromValue(t && t.input, t && t.name)).find(Boolean) || 'any';
820
+ if (condition === 'any') {
821
+ if (/\bbuild\b/i.test(combinedText) && /\b(?:pass(?:es|ing|ed)?|green|succeed(?:s|ed)?|success)\b/i.test(combinedText)) condition = 'build-pass';
822
+ else if (/\bbuild\b/i.test(combinedText) && /\b(?:fail(?:s|ing|ed)?|red|broken|broke)\b/i.test(combinedText)) condition = 'build-fail';
823
+ else if (/\bmerge[d]?\b/i.test(combinedText)) condition = 'merged';
824
+ else if (/\bcomplete[d]?\b/i.test(combinedText)) condition = 'completed';
825
+ }
716
826
 
717
827
  return {
718
828
  type: 'create-watch',
@@ -726,6 +836,23 @@ function _detectLoopInvocation(text, actions) {
726
836
  };
727
837
  }
728
838
 
839
+ function _extractToolUsesFromRaw(raw) {
840
+ const toolUses = [];
841
+ if (!raw) return toolUses;
842
+ for (const line of String(raw).split('\n')) {
843
+ const trimmed = line.trim();
844
+ if (!trimmed || !trimmed.startsWith('{')) continue;
845
+ try {
846
+ const obj = JSON.parse(trimmed);
847
+ if (obj.type !== 'assistant' || !Array.isArray(obj.message?.content)) continue;
848
+ for (const block of obj.message.content) {
849
+ if (block?.type === 'tool_use' && block.name) toolUses.push({ name: block.name, input: block.input || {} });
850
+ }
851
+ } catch {}
852
+ }
853
+ return toolUses;
854
+ }
855
+
729
856
  // ── Server-side CC action execution ──────────────────────────────────────────
730
857
  // Actions are executed server-side so all clients (frontend, curl, Teams) get the same behavior.
731
858
  // The frontend still shows status toasts but no longer needs to fire the API calls.
@@ -3758,8 +3885,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3758
3885
  }
3759
3886
 
3760
3887
  const parsed = parseCCActions(result.text);
3888
+ const toolUses = _extractToolUsesFromRaw(result.raw);
3761
3889
  // Safety net: detect /loop invocation and convert to create-watch
3762
- const _loopWatch = _detectLoopInvocation(parsed.text, parsed.actions);
3890
+ const _loopWatch = _detectLoopInvocation(parsed.text, parsed.actions, toolUses);
3763
3891
  if (_loopWatch) {
3764
3892
  parsed.actions.push(_loopWatch);
3765
3893
  console.warn('[CC] /loop invocation detected — converted to create-watch');
@@ -3869,6 +3997,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3869
3997
  const streamModel = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
3870
3998
  const streamEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
3871
3999
  const ccMaxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
4000
+ let toolUses = [];
3872
4001
  const llmPromise = callLLMStreaming(prompt, CC_STATIC_SYSTEM_PROMPT, {
3873
4002
  timeout: 900000, label: 'command-center', model: streamModel, maxTurns: ccMaxTurns,
3874
4003
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
@@ -3879,6 +4008,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3879
4008
  writeCcEvent({ type: 'chunk', text: display });
3880
4009
  },
3881
4010
  onToolUse: (name, input) => {
4011
+ toolUses.push({ name, input: input || {} });
3882
4012
  writeCcEvent({ type: 'tool', name, input: _lightToolInput(input) });
3883
4013
  }
3884
4014
  });
@@ -3893,6 +4023,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3893
4023
  console.log(`[CC-stream] Resume failed (code=${result.code}) — retrying fresh`);
3894
4024
  const freshPreamble = buildCCStatePreamble();
3895
4025
  const freshPrompt = (freshPreamble ? freshPreamble + '\n\n---\n\n' : '') + body.message;
4026
+ toolUses = []; // discard stale metadata from the failed resume attempt
3896
4027
  const retryPromise = callLLMStreaming(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
3897
4028
  timeout: 900000, label: 'command-center', model: streamModel, maxTurns: ccMaxTurns,
3898
4029
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
@@ -3903,6 +4034,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3903
4034
  writeCcEvent({ type: 'chunk', text: display });
3904
4035
  },
3905
4036
  onToolUse: (name, input) => {
4037
+ toolUses.push({ name, input: input || {} });
3906
4038
  writeCcEvent({ type: 'tool', name, input: _lightToolInput(input) });
3907
4039
  }
3908
4040
  });
@@ -3952,7 +4084,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3952
4084
  // Send final result with actions — execute server-side first
3953
4085
  const { text: displayText, actions } = parseCCActions(result.text);
3954
4086
  // Safety net: detect /loop invocation and convert to create-watch
3955
- const _loopWatch = _detectLoopInvocation(displayText, actions);
4087
+ const _loopWatch = _detectLoopInvocation(displayText, actions, toolUses);
3956
4088
  if (_loopWatch) {
3957
4089
  actions.push(_loopWatch);
3958
4090
  console.warn('[CC] /loop invocation detected — converted to create-watch');
@@ -4142,8 +4274,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4142
4274
  try {
4143
4275
  const config = queries.getConfig();
4144
4276
  const routing = safeRead(path.join(MINIONS_DIR, 'routing.md')) || '';
4277
+ const engine = { ...shared.ENGINE_DEFAULTS, ...(config.engine || {}) };
4278
+ if (engine.prPollStatusEvery === undefined && engine.adoPollStatusEvery !== undefined) engine.prPollStatusEvery = engine.adoPollStatusEvery;
4279
+ if (engine.prPollCommentsEvery === undefined && engine.adoPollCommentsEvery !== undefined) engine.prPollCommentsEvery = engine.adoPollCommentsEvery;
4145
4280
  return jsonReply(res, 200, {
4146
- engine: { ...shared.ENGINE_DEFAULTS, ...(config.engine || {}) },
4281
+ engine,
4147
4282
  claude: { ...shared.DEFAULT_CLAUDE, ...(config.claude || {}) },
4148
4283
  agents: config.agents || {},
4149
4284
  teams: { ...shared.ENGINE_DEFAULTS.teams, ...(config.teams || {}) },
@@ -4172,6 +4307,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4172
4307
  if (body.engine) {
4173
4308
  const e = body.engine;
4174
4309
  const D = shared.ENGINE_DEFAULTS;
4310
+ if (e.prPollStatusEvery === undefined && e.adoPollStatusEvery !== undefined) e.prPollStatusEvery = e.adoPollStatusEvery;
4311
+ if (e.prPollCommentsEvery === undefined && e.adoPollCommentsEvery !== undefined) e.prPollCommentsEvery = e.adoPollCommentsEvery;
4175
4312
  // Numeric fields: { key: [min, max?] }
4176
4313
  const numericFields = {
4177
4314
  tickInterval: [10000], maxConcurrent: [1, 50], inboxConsolidateThreshold: [1],
@@ -4181,7 +4318,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4181
4318
  meetingRoundTimeout: [60000],
4182
4319
  versionCheckInterval: [60000],
4183
4320
  maxBuildFixAttempts: [1, 10],
4184
- adoPollStatusEvery: [1], adoPollCommentsEvery: [1],
4321
+ prPollStatusEvery: [1], prPollCommentsEvery: [1],
4185
4322
  agentBusyReassignMs: [0],
4186
4323
  };
4187
4324
  for (const [key, [min, max]] of Object.entries(numericFields)) {
@@ -4194,6 +4331,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4194
4331
  config.engine[key] = val;
4195
4332
  }
4196
4333
  }
4334
+ delete config.engine.adoPollStatusEvery;
4335
+ delete config.engine.adoPollCommentsEvery;
4197
4336
  // String fields
4198
4337
  if (e.worktreeRoot !== undefined) config.engine.worktreeRoot = String(e.worktreeRoot || D.worktreeRoot);
4199
4338
  // CC model/effort
@@ -14,5 +14,13 @@
14
14
  "reason": "Redundant with evalLoop; evalLoop is the single gate for the review+fix cycle",
15
15
  "locations": ["engine/shared.js ENGINE_DEFAULTS.autoReview", "engine.js discoverFromPrs autoReview variable"],
16
16
  "cleanup": "Removed from ENGINE_DEFAULTS, removed autoReview variable from engine.js, replaced with reviewEnabled = evalLoopEnabled && pollEnabled"
17
+ },
18
+ {
19
+ "id": "ado-poll-frequency-keys",
20
+ "summary": "adoPollStatusEvery and adoPollCommentsEvery renamed to prPollStatusEvery and prPollCommentsEvery",
21
+ "deprecated": "2026-04-16",
22
+ "reason": "These cadence settings gate both ADO and GitHub PR polling, so the ADO-prefixed names are misleading.",
23
+ "locations": ["engine.js read-side fallback from config.engine.adoPollStatusEvery/adoPollCommentsEvery", "dashboard.js handleSettingsRead/handleSettingsUpdate alias fallback for old keys"],
24
+ "cleanup": "Remove the adoPoll* alias fallback reads after existing configs have been migrated to prPollStatusEvery/prPollCommentsEvery."
17
25
  }
18
26
  ]
package/engine/queries.js CHANGED
@@ -9,7 +9,7 @@ const path = require('path');
9
9
  const os = require('os');
10
10
  const shared = require('./shared');
11
11
 
12
- const { safeRead, safeReadDir, safeJson, safeWrite, getProjects,
12
+ const { safeRead, safeReadDir, safeJson, safeWrite, getProjects, mutateJsonFileLocked,
13
13
  projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES,
14
14
  WI_STATUS, DONE_STATUSES, PRD_ITEM_STATUS, ENGINE_DEFAULTS } = shared;
15
15
 
@@ -99,7 +99,49 @@ function timeSince(ms) {
99
99
 
100
100
  // ── Core State Readers ──────────────────────────────────────────────────────
101
101
 
102
+ let _configPollKeyMigrationChecked = false;
103
+
104
+ function migrateDeprecatedConfigPollKeysOnce() {
105
+ if (_configPollKeyMigrationChecked) return;
106
+ const initial = safeJson(CONFIG_PATH);
107
+ if (!initial || typeof initial !== 'object' || Array.isArray(initial)) {
108
+ _configPollKeyMigrationChecked = true;
109
+ return;
110
+ }
111
+ const engine = initial.engine;
112
+ if (!engine || typeof engine !== 'object' || Array.isArray(engine)) {
113
+ _configPollKeyMigrationChecked = true;
114
+ return;
115
+ }
116
+ const hasOldStatus = engine.adoPollStatusEvery !== undefined;
117
+ const hasOldComments = engine.adoPollCommentsEvery !== undefined;
118
+ if (!hasOldStatus && !hasOldComments) {
119
+ _configPollKeyMigrationChecked = true;
120
+ return;
121
+ }
122
+ try {
123
+ mutateJsonFileLocked(CONFIG_PATH, (config) => {
124
+ if (!config || typeof config !== 'object' || Array.isArray(config)) return config;
125
+ const nextEngine = config.engine;
126
+ if (!nextEngine || typeof nextEngine !== 'object' || Array.isArray(nextEngine)) return config;
127
+ if (nextEngine.prPollStatusEvery === undefined && nextEngine.adoPollStatusEvery !== undefined) {
128
+ nextEngine.prPollStatusEvery = nextEngine.adoPollStatusEvery;
129
+ }
130
+ if (nextEngine.prPollCommentsEvery === undefined && nextEngine.adoPollCommentsEvery !== undefined) {
131
+ nextEngine.prPollCommentsEvery = nextEngine.adoPollCommentsEvery;
132
+ }
133
+ delete nextEngine.adoPollStatusEvery;
134
+ delete nextEngine.adoPollCommentsEvery;
135
+ return config;
136
+ });
137
+ _configPollKeyMigrationChecked = true;
138
+ } catch (e) {
139
+ console.warn('[config] one-time prPoll migration failed:', e.message);
140
+ }
141
+ }
142
+
102
143
  function getConfig() {
144
+ migrateDeprecatedConfigPollKeysOnce();
103
145
  return safeJson(CONFIG_PATH) || {};
104
146
  }
105
147
 
package/engine/shared.js CHANGED
@@ -673,8 +673,8 @@ const ENGINE_DEFAULTS = {
673
673
  buildFixGracePeriod: 600000, // 10min — wait for CI to run after build fix before re-dispatching
674
674
  adoPollEnabled: true, // poll ADO PR status, comments, and reconciliation on each tick cycle
675
675
  ghPollEnabled: true, // poll GitHub PR status, comments, and reconciliation on each tick cycle
676
- adoPollStatusEvery: 6, // poll ADO PR build/review/merge status every N ticks (~6 min at default interval)
677
- adoPollCommentsEvery: 12, // poll ADO PR human comments every N ticks (~12 min at default interval)
676
+ prPollStatusEvery: 12, // poll PR build/review/merge status every N ticks for both ADO and GitHub (~12 min at default interval)
677
+ prPollCommentsEvery: 12, // poll PR human comments every N ticks for both ADO and GitHub (~12 min at default interval)
678
678
  autoCompletePrs: false, // auto-merge PRs when builds green + review approved (opt-in)
679
679
  prMergeMethod: 'squash', // merge method: squash, merge, rebase
680
680
  ignoredCommentAuthors: [], // comments from these authors are auto-closed and never trigger fixes
package/engine.js CHANGED
@@ -3226,13 +3226,19 @@ async function tickInner() {
3226
3226
 
3227
3227
  const adoPollEnabled = config.engine?.adoPollEnabled ?? ENGINE_DEFAULTS.adoPollEnabled;
3228
3228
  const ghPollEnabled = config.engine?.ghPollEnabled ?? ENGINE_DEFAULTS.ghPollEnabled;
3229
- const adoPollStatusEvery = Math.max(1, Number(config.engine?.adoPollStatusEvery) || ENGINE_DEFAULTS.adoPollStatusEvery);
3230
- const adoPollCommentsEvery = Math.max(1, Number(config.engine?.adoPollCommentsEvery) || ENGINE_DEFAULTS.adoPollCommentsEvery);
3229
+ const prPollStatusEvery = Math.max(
3230
+ 1,
3231
+ Number(config.engine?.prPollStatusEvery ?? config.engine?.adoPollStatusEvery) || ENGINE_DEFAULTS.prPollStatusEvery
3232
+ );
3233
+ const prPollCommentsEvery = Math.max(
3234
+ 1,
3235
+ Number(config.engine?.prPollCommentsEvery ?? config.engine?.adoPollCommentsEvery) || ENGINE_DEFAULTS.prPollCommentsEvery
3236
+ );
3231
3237
 
3232
- // 2.6. Poll PR status: build, review, merge (every adoPollStatusEvery ticks, default ~6 minutes)
3238
+ // 2.6. Poll PR status: build, review, merge (every prPollStatusEvery ticks, default ~12 minutes)
3233
3239
  // Awaited so PR state is consistent before discoverWork reads it
3234
3240
  // Also re-polls early if previous tick had ADO auth failures (stale build status recovery)
3235
- if (tickCount % adoPollStatusEvery === 0 || needsAdoPollRetry()) {
3241
+ if (tickCount % prPollStatusEvery === 0 || needsAdoPollRetry()) {
3236
3242
  // Build promise array — enabled+unthrottled polls run concurrently via Promise.allSettled
3237
3243
  const statusPolls = [];
3238
3244
  if (adoPollEnabled && !isAdoThrottled()) {
@@ -3265,8 +3271,8 @@ async function tickInner() {
3265
3271
  } catch (err) { log('warn', `Plan completion check error: ${err?.message || err}`); }
3266
3272
  }
3267
3273
 
3268
- // 2.7. Poll PR threads for human comments (every adoPollCommentsEvery ticks, default ~12 minutes)
3269
- if (tickCount % adoPollCommentsEvery === 0) {
3274
+ // 2.7. Poll PR threads for human comments (every prPollCommentsEvery ticks, default ~12 minutes)
3275
+ if (tickCount % prPollCommentsEvery === 0) {
3270
3276
  // Build promise array — enabled+unthrottled comment polls run concurrently via Promise.allSettled
3271
3277
  const commentPolls = [];
3272
3278
  if (adoPollEnabled && !isAdoThrottled()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1044",
3
+ "version": "0.1.1046",
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"