@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 +5 -1
- package/dashboard/js/settings.js +10 -5
- package/dashboard.js +156 -17
- package/docs/deprecated.json +8 -0
- package/engine/queries.js +43 -1
- package/engine/shared.js +2 -2
- package/engine.js +12 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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
|
package/dashboard/js/settings.js
CHANGED
|
@@ -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-
|
|
60
|
-
settingsField('PR Comments Poll Frequency', 'set-
|
|
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
|
-
|
|
262
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
693
|
-
const
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/docs/deprecated.json
CHANGED
|
@@ -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
|
-
|
|
677
|
-
|
|
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
|
|
3230
|
-
|
|
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
|
|
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 %
|
|
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
|
|
3269
|
-
if (tickCount %
|
|
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.
|
|
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"
|