@yemi33/minions 0.1.1045 → 0.1.1047
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/docs/deprecated.json +8 -0
- package/engine/queries.js +43 -1
- package/engine/shared.js +2 -2
- package/engine/timeout.js +13 -2
- package/engine.js +50 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
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/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/timeout.js
CHANGED
|
@@ -308,16 +308,27 @@ function checkTimeouts(config) {
|
|
|
308
308
|
const procInfo = activeProcesses.get(item.id);
|
|
309
309
|
if (procInfo?._steeringAt && Date.now() - procInfo._steeringAt < 60000) continue;
|
|
310
310
|
|
|
311
|
+
// Capture live-output.log file state for orphan/hung diagnostics (#W-mo248lkjwgsu).
|
|
312
|
+
// Three distinguishable failure modes:
|
|
313
|
+
// logExists=false → spawn call itself threw, no log ever written
|
|
314
|
+
// logExists=true, size~stub → process died at startup (stub-only content)
|
|
315
|
+
// logExists=true, size>stub → genuine hang (process alive, wrote output, then stopped)
|
|
316
|
+
let _logState = 'logExists=false logSize=0';
|
|
317
|
+
try {
|
|
318
|
+
const lst = fs.statSync(liveLogPath);
|
|
319
|
+
_logState = `logExists=true logSize=${lst.size}`;
|
|
320
|
+
} catch { /* ENOENT — keep default */ }
|
|
321
|
+
|
|
311
322
|
if (!hasProcess && silentMs > effectiveTimeout && (Date.now() > engineRestartGraceUntil || engineRestartGraceExempt?.has(item.id))) {
|
|
312
323
|
// No tracked process AND no recent output past effective timeout AND (grace period expired OR confirmed-dead at restart) → orphaned
|
|
313
|
-
log('warn', `Orphan detected: ${item.agent} (${item.id}) — no process tracked, silent for ${silentSec}s${isBlocking ? ' (blocking timeout exceeded)' : ''}`);
|
|
324
|
+
log('warn', `Orphan detected: ${item.agent} (${item.id}) — no process tracked, silent for ${silentSec}s${isBlocking ? ' (blocking timeout exceeded)' : ''} [${_logState}]`);
|
|
314
325
|
dispatch().updateAgentStatus(item.id, AGENT_STATUS.TIMED_OUT, `Orphaned — no process, silent for ${silentSec}s`);
|
|
315
326
|
// Clear session so retry starts fresh
|
|
316
327
|
try { shared.safeUnlink(path.join(AGENTS_DIR, item.agent, 'session.json')); } catch {}
|
|
317
328
|
deadItems.push({ item, reason: `Orphaned — no process, silent for ${silentSec}s` });
|
|
318
329
|
} else if (hasProcess && silentMs > effectiveTimeout) {
|
|
319
330
|
// Has process but no output past effective timeout → hung
|
|
320
|
-
log('warn', `Hung agent: ${item.agent} (${item.id}) — process exists but no output for ${silentSec}s${isBlocking ? ' (blocking timeout exceeded)' : ''}`);
|
|
331
|
+
log('warn', `Hung agent: ${item.agent} (${item.id}) — process exists but no output for ${silentSec}s${isBlocking ? ' (blocking timeout exceeded)' : ''} [${_logState}]`);
|
|
321
332
|
dispatch().updateAgentStatus(item.id, AGENT_STATUS.TIMED_OUT, `Hung — no output for ${silentSec}s`);
|
|
322
333
|
const procInfo = activeProcesses.get(item.id);
|
|
323
334
|
if (procInfo) {
|
package/engine.js
CHANGED
|
@@ -871,25 +871,16 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
871
871
|
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
872
872
|
const spawnArgs = [spawnScript, promptPath, sysPromptPath, ...args];
|
|
873
873
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
const MAX_OUTPUT = 1024 * 1024; // 1MB
|
|
881
|
-
let stdout = '';
|
|
882
|
-
let stderr = '';
|
|
883
|
-
let lastOutputAt = Date.now();
|
|
884
|
-
let heartbeatTimer = null;
|
|
885
|
-
let _trustCheckDone = false;
|
|
886
|
-
const _spawnTime = Date.now();
|
|
887
|
-
|
|
888
|
-
// Live output file — written as data arrives so dashboard can tail it
|
|
874
|
+
// Live output file — stamped BEFORE child process is spawned (#W-mo248lkjwgsu).
|
|
875
|
+
// Writing the stub pre-spawn lets the orphan detector distinguish three failure modes
|
|
876
|
+
// that otherwise look identical:
|
|
877
|
+
// 1. No log file → spawn call itself threw before reaching this line
|
|
878
|
+
// 2. Log has stub only → process started but died before its first write
|
|
879
|
+
// 3. Log has stub + ... → process alive but hung (the only case that warrants orphan kill+retry)
|
|
889
880
|
const liveOutputPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
|
|
890
881
|
|
|
891
882
|
// Rotate previous live output to preserve session history (fixes #543: orphan recovery overwrites)
|
|
892
|
-
// Only rotate if the existing file has meaningful content (beyond just the header)
|
|
883
|
+
// Only rotate if the existing file has meaningful content (beyond just the header stub)
|
|
893
884
|
const LIVE_OUTPUT_SPARSE_THRESHOLD = 500; // bytes — header + init JSON is typically < 500
|
|
894
885
|
try {
|
|
895
886
|
if (fs.existsSync(liveOutputPath)) {
|
|
@@ -901,7 +892,37 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
901
892
|
}
|
|
902
893
|
} catch { /* rotation is best-effort — overwrite still happens below */ }
|
|
903
894
|
|
|
904
|
-
|
|
895
|
+
// Stamp the log synchronously before spawn. If the synchronous write throws,
|
|
896
|
+
// we still attempt to spawn (log-file stamp failure must not block the agent),
|
|
897
|
+
// but we note it so the diagnostic trail isn't silent either.
|
|
898
|
+
try {
|
|
899
|
+
safeWrite(liveOutputPath, `# Live output for ${agentId} — ${id}\n# Started: ${startedAt}\n# Task: ${dispatchItem.task}\n[${new Date().toISOString()}] spawn: agent=${agentId} item=${id}\n\n`);
|
|
900
|
+
} catch (stubErr) {
|
|
901
|
+
log('warn', `Failed to stamp live-output stub for ${agentId} (${id}): ${stubErr.message}`);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
let proc;
|
|
905
|
+
try {
|
|
906
|
+
proc = runFile(process.execPath, spawnArgs, {
|
|
907
|
+
cwd,
|
|
908
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
909
|
+
env: childEnv,
|
|
910
|
+
});
|
|
911
|
+
} catch (spawnErr) {
|
|
912
|
+
// Synchronous spawn failure — record it to the (already-stamped) log so the
|
|
913
|
+
// orphan detector's "logSize > stub-only" check can tell this apart from a
|
|
914
|
+
// hung process. Then rethrow so the dispatch loop handles it normally.
|
|
915
|
+
try { fs.appendFileSync(liveOutputPath, `[${new Date().toISOString()}] spawn-failed: ${spawnErr.message}\n[process-exit] spawn-failed\n`); } catch { /* cleanup-only best effort */ }
|
|
916
|
+
throw spawnErr;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const MAX_OUTPUT = 1024 * 1024; // 1MB
|
|
920
|
+
let stdout = '';
|
|
921
|
+
let stderr = '';
|
|
922
|
+
let lastOutputAt = Date.now();
|
|
923
|
+
let heartbeatTimer = null;
|
|
924
|
+
let _trustCheckDone = false;
|
|
925
|
+
const _spawnTime = Date.now();
|
|
905
926
|
|
|
906
927
|
// Keep live log active even when the agent produces no stdout/stderr for long stretches.
|
|
907
928
|
// This makes "silent but running" states visible in the dashboard tail view.
|
|
@@ -3226,13 +3247,19 @@ async function tickInner() {
|
|
|
3226
3247
|
|
|
3227
3248
|
const adoPollEnabled = config.engine?.adoPollEnabled ?? ENGINE_DEFAULTS.adoPollEnabled;
|
|
3228
3249
|
const ghPollEnabled = config.engine?.ghPollEnabled ?? ENGINE_DEFAULTS.ghPollEnabled;
|
|
3229
|
-
const
|
|
3230
|
-
|
|
3250
|
+
const prPollStatusEvery = Math.max(
|
|
3251
|
+
1,
|
|
3252
|
+
Number(config.engine?.prPollStatusEvery ?? config.engine?.adoPollStatusEvery) || ENGINE_DEFAULTS.prPollStatusEvery
|
|
3253
|
+
);
|
|
3254
|
+
const prPollCommentsEvery = Math.max(
|
|
3255
|
+
1,
|
|
3256
|
+
Number(config.engine?.prPollCommentsEvery ?? config.engine?.adoPollCommentsEvery) || ENGINE_DEFAULTS.prPollCommentsEvery
|
|
3257
|
+
);
|
|
3231
3258
|
|
|
3232
|
-
// 2.6. Poll PR status: build, review, merge (every
|
|
3259
|
+
// 2.6. Poll PR status: build, review, merge (every prPollStatusEvery ticks, default ~12 minutes)
|
|
3233
3260
|
// Awaited so PR state is consistent before discoverWork reads it
|
|
3234
3261
|
// Also re-polls early if previous tick had ADO auth failures (stale build status recovery)
|
|
3235
|
-
if (tickCount %
|
|
3262
|
+
if (tickCount % prPollStatusEvery === 0 || needsAdoPollRetry()) {
|
|
3236
3263
|
// Build promise array — enabled+unthrottled polls run concurrently via Promise.allSettled
|
|
3237
3264
|
const statusPolls = [];
|
|
3238
3265
|
if (adoPollEnabled && !isAdoThrottled()) {
|
|
@@ -3265,8 +3292,8 @@ async function tickInner() {
|
|
|
3265
3292
|
} catch (err) { log('warn', `Plan completion check error: ${err?.message || err}`); }
|
|
3266
3293
|
}
|
|
3267
3294
|
|
|
3268
|
-
// 2.7. Poll PR threads for human comments (every
|
|
3269
|
-
if (tickCount %
|
|
3295
|
+
// 2.7. Poll PR threads for human comments (every prPollCommentsEvery ticks, default ~12 minutes)
|
|
3296
|
+
if (tickCount % prPollCommentsEvery === 0) {
|
|
3270
3297
|
// Build promise array — enabled+unthrottled comment polls run concurrently via Promise.allSettled
|
|
3271
3298
|
const commentPolls = [];
|
|
3272
3299
|
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.1047",
|
|
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"
|