@yemi33/minions 0.1.1988 → 0.1.1989
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/README.md +1 -1
- package/dashboard/js/command-center.js +32 -2
- package/dashboard/js/settings.js +2 -0
- package/dashboard.js +172 -16
- package/docs/completion-reports.md +1 -2
- package/docs/deprecated.json +9 -38
- package/docs/rfc-completion-json.md +2 -2
- package/engine/cc-worker-pool.js +72 -2
- package/engine/lifecycle.js +3 -2
- package/engine/shared.js +37 -1
- package/package.json +1 -1
- package/engine/recovery.js +0 -130
package/README.md
CHANGED
|
@@ -657,7 +657,7 @@ To move to a new machine: `npm install -g @yemi33/minions && minions init --forc
|
|
|
657
657
|
# Core orchestration
|
|
658
658
|
shared.js queries.js cli.js
|
|
659
659
|
lifecycle.js dispatch.js cooldown.js
|
|
660
|
-
timeout.js steering.js
|
|
660
|
+
timeout.js steering.js
|
|
661
661
|
pre-dispatch-eval.js
|
|
662
662
|
# Discovery, routing, playbooks
|
|
663
663
|
routing.js playbook.js cleanup.js
|
|
@@ -1013,7 +1013,27 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
1013
1013
|
for (var li = 0; li < lines.length; li++) {
|
|
1014
1014
|
var line = lines[li];
|
|
1015
1015
|
if (!line.startsWith('data: ')) continue;
|
|
1016
|
-
|
|
1016
|
+
// W-mpdavudb000v8446 — these used to swallow ALL errors via `catch {}`,
|
|
1017
|
+
// hiding JSON.parse failures AND any DOM/render exception thrown by
|
|
1018
|
+
// _handleEvent. When chunks=2 outcome=done server-side but the user
|
|
1019
|
+
// saw thinking dots forever, this was the most likely observability
|
|
1020
|
+
// hole: a render error in updateStreamDiv / addMsg / renderMd would
|
|
1021
|
+
// disappear silently and the loop would keep reading. Log with enough
|
|
1022
|
+
// context (event type, tab, error message) to triage from the browser
|
|
1023
|
+
// console without dumping raw event payloads (which may include user
|
|
1024
|
+
// content). Failure is still non-fatal — we keep reading the stream
|
|
1025
|
+
// so the `done` event still has a chance to flip terminalEventSeen.
|
|
1026
|
+
var rawJson = line.slice(6);
|
|
1027
|
+
var evt;
|
|
1028
|
+
try { evt = JSON.parse(rawJson); }
|
|
1029
|
+
catch (parseErr) {
|
|
1030
|
+
try { console.error('[cc-sse] parse-failed', { tab: activeTabId, len: rawJson.length, error: String(parseErr && parseErr.message || parseErr) }); } catch (_e) {}
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
try { await _handleEvent(evt); }
|
|
1034
|
+
catch (handleErr) {
|
|
1035
|
+
try { console.error('[cc-sse] handle-failed', { tab: activeTabId, type: evt && evt.type, error: String(handleErr && handleErr.message || handleErr), stack: handleErr && handleErr.stack }); } catch (_e) {}
|
|
1036
|
+
}
|
|
1017
1037
|
}
|
|
1018
1038
|
}
|
|
1019
1039
|
if (buf.trim()) {
|
|
@@ -1021,7 +1041,17 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
1021
1041
|
for (var ri = 0; ri < remainingLines.length; ri++) {
|
|
1022
1042
|
var rline = remainingLines[ri];
|
|
1023
1043
|
if (!rline.startsWith('data: ')) continue;
|
|
1024
|
-
|
|
1044
|
+
var trailRaw = rline.slice(6);
|
|
1045
|
+
var trailEvt;
|
|
1046
|
+
try { trailEvt = JSON.parse(trailRaw); }
|
|
1047
|
+
catch (parseErr) {
|
|
1048
|
+
try { console.error('[cc-sse] parse-failed-trailing', { tab: activeTabId, len: trailRaw.length, error: String(parseErr && parseErr.message || parseErr) }); } catch (_e) {}
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
try { await _handleEvent(trailEvt); }
|
|
1052
|
+
catch (handleErr) {
|
|
1053
|
+
try { console.error('[cc-sse] handle-failed-trailing', { tab: activeTabId, type: trailEvt && trailEvt.type, error: String(handleErr && handleErr.message || handleErr), stack: handleErr && handleErr.stack }); } catch (_e) {}
|
|
1054
|
+
}
|
|
1025
1055
|
}
|
|
1026
1056
|
}
|
|
1027
1057
|
return { interrupted: !terminalEventSeen, reconnectable: true };
|
package/dashboard/js/settings.js
CHANGED
|
@@ -106,6 +106,7 @@ async function openSettings() {
|
|
|
106
106
|
settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Keeps GitHub PR build results, votes, and comments fresh each tick; GitHub PR dispatch gates are inert when this is off') +
|
|
107
107
|
'</div>' +
|
|
108
108
|
'<div style="margin-top:10px;padding-top:10px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:4px">' +
|
|
109
|
+
settingsToggle('Auto-apply review vote to PR', 'set-autoApplyReviewVote', !!e.autoApplyReviewVote, 'When ON, Minions review verdicts (APPROVE / REQUEST_CHANGES) automatically flip the platform vote on ADO/GitHub. When OFF (default), verdicts are informational only and the human casts the final vote.') +
|
|
109
110
|
settingsToggle('Auto-fix Builds', 'set-autoFixBuilds', e.autoFixBuilds !== false, 'Shared dispatch gate: auto-fix agent when a PR build fails; also requires that PR provider polling is enabled') +
|
|
110
111
|
settingsToggle('Auto-fix Conflicts', 'set-autoFixConflicts', e.autoFixConflicts !== false, 'Shared dispatch gate: auto-fix agent when a PR merge conflict is detected; also requires that PR provider polling is enabled') +
|
|
111
112
|
settingsToggle('Auto-review PRs', 'set-autoReviewPrs', e.autoReviewPrs !== false, 'Shared dispatch gate: review agent for newly opened agent PRs; also requires that PR provider polling is enabled') +
|
|
@@ -568,6 +569,7 @@ async function saveSettings() {
|
|
|
568
569
|
autoDecompose: document.getElementById('set-autoDecompose').checked,
|
|
569
570
|
allowTempAgents: document.getElementById('set-allowTempAgents').checked,
|
|
570
571
|
autoArchive: document.getElementById('set-autoArchive').checked,
|
|
572
|
+
autoApplyReviewVote: document.getElementById('set-autoApplyReviewVote').checked,
|
|
571
573
|
autoFixBuilds: document.getElementById('set-autoFixBuilds').checked,
|
|
572
574
|
autoFixConflicts: document.getElementById('set-autoFixConflicts').checked,
|
|
573
575
|
autoReviewPrs: document.getElementById('set-autoReviewPrs').checked,
|
package/dashboard.js
CHANGED
|
@@ -462,6 +462,38 @@ function resolveManualPrLinkProject(url, projectName, projects = PROJECTS) {
|
|
|
462
462
|
};
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
// Lenient ADO match: repo segment may be a GUID that matches repositoryId
|
|
466
|
+
if (matches.length === 0 && prScope.startsWith('ado:')) {
|
|
467
|
+
const lenientMatches = projects.filter(p => shared.isAdoPrScopeCompatible(prScope, p));
|
|
468
|
+
if (lenientMatches.length === 1) {
|
|
469
|
+
const targetProject = lenientMatches[0];
|
|
470
|
+
return {
|
|
471
|
+
project: targetProject,
|
|
472
|
+
resolution: {
|
|
473
|
+
reason: 'inferred',
|
|
474
|
+
scope: prScope,
|
|
475
|
+
project: targetProject.name || '',
|
|
476
|
+
storage: 'project',
|
|
477
|
+
message: `Inferred project "${targetProject.name}" from PR scope ${prScope} (ADO repository ID match).`,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (lenientMatches.length > 1) {
|
|
482
|
+
const names = lenientMatches.map(p => p.name).filter(Boolean);
|
|
483
|
+
return {
|
|
484
|
+
project: null,
|
|
485
|
+
resolution: {
|
|
486
|
+
reason: 'ambiguous',
|
|
487
|
+
scope: prScope,
|
|
488
|
+
project: 'central',
|
|
489
|
+
storage: 'central',
|
|
490
|
+
matches: names,
|
|
491
|
+
message: `PR scope ${prScope} matches multiple configured projects (${names.join(', ')}); linked in central PR tracking. Select a project to attach it.`,
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
465
497
|
if (matches.length > 1) {
|
|
466
498
|
const names = matches.map(p => p.name).filter(Boolean);
|
|
467
499
|
return {
|
|
@@ -2287,6 +2319,54 @@ For all state files, look under \`${MINIONS_DIR}\`.${indexSection}`;
|
|
|
2287
2319
|
return result;
|
|
2288
2320
|
}
|
|
2289
2321
|
|
|
2322
|
+
// ── Compact state refresh for resumed CC sessions (W-mpeap2ug0016e69c) ──
|
|
2323
|
+
// Injected every turn on resume so new projects, MCPs, and agent changes
|
|
2324
|
+
// are visible without opening a new session. Smaller than the full preamble
|
|
2325
|
+
// (skips API/CLI indexes) and uses a shorter cache TTL.
|
|
2326
|
+
|
|
2327
|
+
let _refreshCache = null;
|
|
2328
|
+
let _refreshCacheTs = 0;
|
|
2329
|
+
const REFRESH_TTL = 10000; // 10s — short TTL so state propagates quickly on resume
|
|
2330
|
+
|
|
2331
|
+
function _resetRefreshCache() {
|
|
2332
|
+
_refreshCache = null;
|
|
2333
|
+
_refreshCacheTs = 0;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
function buildCCStateRefresh() {
|
|
2337
|
+
const now = Date.now();
|
|
2338
|
+
if (_refreshCache && now - _refreshCacheTs < REFRESH_TTL) return _refreshCache;
|
|
2339
|
+
|
|
2340
|
+
const ts = new Date().toISOString().slice(0, 16);
|
|
2341
|
+
const agents = getAgents().map(a => `- ${a.name}: ${a.status}`).join('\n');
|
|
2342
|
+
const projects = PROJECTS.map(p => `- ${p.name} (${p.repo || p.localPath})`).join('\n');
|
|
2343
|
+
|
|
2344
|
+
// MCP servers — just names + source for orientation
|
|
2345
|
+
let mcpLine = '(none discovered)';
|
|
2346
|
+
try {
|
|
2347
|
+
const mcps = getMcpServers();
|
|
2348
|
+
if (mcps && mcps.length > 0) {
|
|
2349
|
+
const maxShow = 15;
|
|
2350
|
+
const shown = mcps.slice(0, maxShow).map(m => m.name).join(', ');
|
|
2351
|
+
mcpLine = mcps.length > maxShow ? `${shown} …and ${mcps.length - maxShow} more` : shown;
|
|
2352
|
+
}
|
|
2353
|
+
} catch { /* optional */ }
|
|
2354
|
+
|
|
2355
|
+
const result = `### State Refresh (${ts})
|
|
2356
|
+
|
|
2357
|
+
**Projects:** ${PROJECTS.length}
|
|
2358
|
+
${projects || '(none)'}
|
|
2359
|
+
|
|
2360
|
+
**MCP Tools:** ${mcpLine}
|
|
2361
|
+
|
|
2362
|
+
**Agents:**
|
|
2363
|
+
${agents || '(none)'}`;
|
|
2364
|
+
|
|
2365
|
+
_refreshCache = result;
|
|
2366
|
+
_refreshCacheTs = now;
|
|
2367
|
+
return result;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2290
2370
|
// The ===ACTIONS=== delimiter parser tiers (findCCActionsHeader,
|
|
2291
2371
|
// findCCActionsPartialDelimiter, stripCCActionsForStream/Display) and the
|
|
2292
2372
|
// _extractActionsJson Copilot fence-stripper were retired with the move to
|
|
@@ -2735,8 +2815,14 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2735
2815
|
const resumeHasOutOfBandCarryover = !!sessionId && _transcriptHasCarryoverContext(transcript, { outOfBandOnly: true, currentMessage: message });
|
|
2736
2816
|
const freshNeedsCarryover = _transcriptHasCarryoverContext(transcript, { currentMessage: message });
|
|
2737
2817
|
|
|
2738
|
-
function buildPrompt({ includePreamble = true, includeCarryover = false, includeResumeGuard = false, outOfBandOnly = false } = {}) {
|
|
2739
|
-
|
|
2818
|
+
function buildPrompt({ includePreamble = true, includeRefresh = false, includeCarryover = false, includeResumeGuard = false, outOfBandOnly = false } = {}) {
|
|
2819
|
+
let preamblePart = null;
|
|
2820
|
+
if (!skipStatePreamble && includePreamble) {
|
|
2821
|
+
preamblePart = `## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`;
|
|
2822
|
+
} else if (!skipStatePreamble && includeRefresh) {
|
|
2823
|
+
preamblePart = buildCCStateRefresh();
|
|
2824
|
+
}
|
|
2825
|
+
const parts = preamblePart ? [preamblePart] : [];
|
|
2740
2826
|
if (extraContext) parts.push(extraContext);
|
|
2741
2827
|
if (includeResumeGuard) parts.push(CC_RESUME_BOOKKEEPING_GUARD);
|
|
2742
2828
|
if (includeCarryover) {
|
|
@@ -2755,6 +2841,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2755
2841
|
if (sessionId && maxTurns > 1) {
|
|
2756
2842
|
const p1 = llm.callLLM(buildPrompt({
|
|
2757
2843
|
includePreamble: false,
|
|
2844
|
+
includeRefresh: true,
|
|
2758
2845
|
includeResumeGuard: resumeNeedsBookkeepingGuard,
|
|
2759
2846
|
includeCarryover: resumeNeedsCarryover || resumeHasOutOfBandCarryover,
|
|
2760
2847
|
outOfBandOnly: !resumeNeedsCarryover,
|
|
@@ -2877,8 +2964,14 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
|
|
|
2877
2964
|
const resumeHasOutOfBandCarryover = !!sessionId && _transcriptHasCarryoverContext(transcript, { outOfBandOnly: true, currentMessage: message });
|
|
2878
2965
|
const freshNeedsCarryover = _transcriptHasCarryoverContext(transcript, { currentMessage: message });
|
|
2879
2966
|
|
|
2880
|
-
function buildPrompt({ includePreamble = true, includeCarryover = false, includeResumeGuard = false, outOfBandOnly = false } = {}) {
|
|
2881
|
-
|
|
2967
|
+
function buildPrompt({ includePreamble = true, includeRefresh = false, includeCarryover = false, includeResumeGuard = false, outOfBandOnly = false } = {}) {
|
|
2968
|
+
let preamblePart = null;
|
|
2969
|
+
if (!skipStatePreamble && includePreamble) {
|
|
2970
|
+
preamblePart = `## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`;
|
|
2971
|
+
} else if (!skipStatePreamble && includeRefresh) {
|
|
2972
|
+
preamblePart = buildCCStateRefresh();
|
|
2973
|
+
}
|
|
2974
|
+
const parts = preamblePart ? [preamblePart] : [];
|
|
2882
2975
|
if (extraContext) parts.push(extraContext);
|
|
2883
2976
|
if (includeResumeGuard) parts.push(CC_RESUME_BOOKKEEPING_GUARD);
|
|
2884
2977
|
if (includeCarryover) {
|
|
@@ -2896,6 +2989,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
|
|
|
2896
2989
|
if (sessionId && maxTurns > 1) {
|
|
2897
2990
|
const p1 = llm.callLLMStreaming(buildPrompt({
|
|
2898
2991
|
includePreamble: false,
|
|
2992
|
+
includeRefresh: true,
|
|
2899
2993
|
includeResumeGuard: resumeNeedsBookkeepingGuard,
|
|
2900
2994
|
includeCarryover: resumeNeedsCarryover || resumeHasOutOfBandCarryover,
|
|
2901
2995
|
outOfBandOnly: !resumeNeedsCarryover,
|
|
@@ -4245,6 +4339,22 @@ const server = http.createServer(async (req, res) => {
|
|
|
4245
4339
|
if (!Array.isArray(body.depends_on)) return jsonReply(res, 400, { error: 'depends_on must be an array of strings' });
|
|
4246
4340
|
if (!body.depends_on.every(s => typeof s === 'string')) return jsonReply(res, 400, { error: 'depends_on entries must be strings' });
|
|
4247
4341
|
}
|
|
4342
|
+
// Validate agent/agents against config.agents (W-mpeanskq001311cf)
|
|
4343
|
+
const knownAgents = CONFIG.agents && typeof CONFIG.agents === 'object' ? Object.keys(CONFIG.agents) : [];
|
|
4344
|
+
if (knownAgents.length > 0) {
|
|
4345
|
+
const allowTemp = !!CONFIG.engine?.allowTempAgents;
|
|
4346
|
+
const isValidAgent = (name) => knownAgents.includes(name) || (allowTemp && /^temp-/.test(name));
|
|
4347
|
+
if (body.agent && typeof body.agent === 'string' && body.agent.trim()) {
|
|
4348
|
+
if (!isValidAgent(body.agent.trim())) {
|
|
4349
|
+
return jsonReply(res, 400, { error: `Unknown agent "${body.agent}". Valid agents: ${knownAgents.join(', ')}`, validAgents: knownAgents });
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
const agentsArr = Array.isArray(body.agents) ? body.agents.filter(Boolean) : [];
|
|
4353
|
+
const invalidAgents = agentsArr.filter(a => typeof a === 'string' && !isValidAgent(a.trim()));
|
|
4354
|
+
if (invalidAgents.length > 0) {
|
|
4355
|
+
return jsonReply(res, 400, { error: `Unknown agent(s) in agents array: ${invalidAgents.join(', ')}. Valid agents: ${knownAgents.join(', ')}`, validAgents: knownAgents });
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4248
4358
|
// Worktree-requiring types must own a project so the engine's spawnAgent
|
|
4249
4359
|
// can resolve a per-project rootDir. With no project (and no single
|
|
4250
4360
|
// auto-target via defaultWhenSingle), spawn falls back to MINIONS_DIR's
|
|
@@ -6784,20 +6894,64 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6784
6894
|
let _ccStreamEnded = false;
|
|
6785
6895
|
let _ccHeartbeatTimer = null;
|
|
6786
6896
|
let _ccLastHeartbeatAt = Date.now();
|
|
6897
|
+
// W-mpdavudb000v8446 — SSE delivery telemetry. Previously writeCcEvent
|
|
6898
|
+
// swallowed all write failures (res.destroyed / res.write returning false
|
|
6899
|
+
// for backpressure / sync throw), and the [cc-timing] log only proved
|
|
6900
|
+
// onChunk/onDone fired — NOT that bytes left the kernel. When chunks=2
|
|
6901
|
+
// outcome=done but the user sees thinking dots forever, the gap was here.
|
|
6902
|
+
// Now writeCcEvent inspects res state and logs a structured [cc-sse-fail]
|
|
6903
|
+
// line whenever it cannot actually deliver a chunk/done frame to the wire.
|
|
6787
6904
|
const writeCcEvent = (payload) => {
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
if (
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6905
|
+
const type = payload && payload.type;
|
|
6906
|
+
const isUserFacing = type === 'chunk' || type === 'done' || type === 'tool' || type === 'tool-update' || type === 'error';
|
|
6907
|
+
const _logFail = (reason, extra) => {
|
|
6908
|
+
if (!isUserFacing) return;
|
|
6909
|
+
try {
|
|
6910
|
+
const meta = {
|
|
6911
|
+
tab: tabId || _ccTelemetry.tabId || 'unknown',
|
|
6912
|
+
type,
|
|
6913
|
+
reason,
|
|
6914
|
+
destroyed: !!res.destroyed,
|
|
6915
|
+
writableEnded: !!res.writableEnded,
|
|
6916
|
+
writableFinished: !!res.writableFinished,
|
|
6917
|
+
streamEnded: _ccStreamEnded,
|
|
6918
|
+
...(extra || {}),
|
|
6919
|
+
};
|
|
6920
|
+
shared.log('warn', `[cc-sse-fail] ${JSON.stringify(meta)}`);
|
|
6921
|
+
} catch { /* telemetry is best-effort */ }
|
|
6922
|
+
};
|
|
6923
|
+
if (res.destroyed || res.writableEnded) {
|
|
6924
|
+
_logFail(res.destroyed ? 'res-destroyed' : 'res-writable-ended');
|
|
6925
|
+
return false;
|
|
6926
|
+
}
|
|
6927
|
+
let wire;
|
|
6928
|
+
try { wire = 'data: ' + JSON.stringify(payload) + '\n\n'; }
|
|
6929
|
+
catch (err) {
|
|
6930
|
+
_logFail('json-serialize-failed', { error: String((err && err.message) || err).slice(0, 200) });
|
|
6799
6931
|
return false;
|
|
6800
6932
|
}
|
|
6933
|
+
let writeOk;
|
|
6934
|
+
try { writeOk = res.write(wire); }
|
|
6935
|
+
catch (err) {
|
|
6936
|
+
_logFail('res-write-threw', { error: String((err && err.message) || err).slice(0, 200), bytes: wire.length });
|
|
6937
|
+
return false;
|
|
6938
|
+
}
|
|
6939
|
+
if (writeOk === false) {
|
|
6940
|
+
// Backpressure — Node's writable buffer is over its highWaterMark.
|
|
6941
|
+
// The write IS still queued, so don't treat this as a failure, but
|
|
6942
|
+
// surface it so a slow consumer is visible in telemetry. Most CC
|
|
6943
|
+
// chunks are small enough that we never hit this in practice.
|
|
6944
|
+
try {
|
|
6945
|
+
shared.log('warn', `[cc-sse-backpressure] tab=${tabId || _ccTelemetry.tabId || 'unknown'} type=${type} bytes=${wire.length}`);
|
|
6946
|
+
} catch { /* telemetry is best-effort */ }
|
|
6947
|
+
}
|
|
6948
|
+
if (payload && payload.type === 'chunk') {
|
|
6949
|
+
_ccTelemetry.chunks++;
|
|
6950
|
+
_ccTelemetry.bytes += Buffer.byteLength(String(payload.text || ''), 'utf8');
|
|
6951
|
+
} else if (payload && payload.type === 'tool') {
|
|
6952
|
+
_ccTelemetry.tools++;
|
|
6953
|
+
}
|
|
6954
|
+
return true;
|
|
6801
6955
|
};
|
|
6802
6956
|
const stopCcHeartbeat = () => {
|
|
6803
6957
|
if (_ccHeartbeatTimer) {
|
|
@@ -6970,7 +7124,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6970
7124
|
const resumeNeedsCarryover = wasResume && _ccRuntimeNeedsResumeCarryover(currentRuntime);
|
|
6971
7125
|
const resumeNeedsBookkeepingGuard = wasResume && _ccRuntimeNeedsResumeBookkeepingGuard(currentRuntime);
|
|
6972
7126
|
const resumeHasOutOfBandCarryover = wasResume && _transcriptHasCarryoverContext(body.transcript, { outOfBandOnly: true, currentMessage: body.message });
|
|
6973
|
-
const preamble = wasResume ?
|
|
7127
|
+
const preamble = wasResume ? buildCCStateRefresh() : buildCCStatePreamble();
|
|
6974
7128
|
const includeFullCarryover = sessionReset || resumeNeedsCarryover;
|
|
6975
7129
|
const resumeGuard = resumeNeedsBookkeepingGuard ? CC_RESUME_BOOKKEEPING_GUARD : '';
|
|
6976
7130
|
const carryover = (includeFullCarryover || resumeHasOutOfBandCarryover)
|
|
@@ -9138,6 +9292,8 @@ module.exports = {
|
|
|
9138
9292
|
_resolveScheduleProjectValue: resolveScheduleProjectValue,
|
|
9139
9293
|
_collectArchivedWorkItems: collectArchivedWorkItems,
|
|
9140
9294
|
buildCCStatePreamble,
|
|
9295
|
+
buildCCStateRefresh,
|
|
9296
|
+
_resetRefreshCache,
|
|
9141
9297
|
_routesAsMeta,
|
|
9142
9298
|
_server: server,
|
|
9143
9299
|
_buildTranscriptCarryover,
|
|
@@ -68,7 +68,7 @@ Do **not** invent, regenerate, or share the nonce across dispatches — each spa
|
|
|
68
68
|
| `summary` | string | Short prose describing what changed and how it was validated. Truncated to 500 chars in dashboard surfaces (`engine/queries.js`). Do not summarize validation as "tests passed" — name the commands that ran. |
|
|
69
69
|
| `verdict` | string \| null | Required for review tasks: `approved` or `changes-requested`. `null` for non-review tasks. Aliases: `approve`, `request_changes`, `changes_requested`. |
|
|
70
70
|
| `pr` | string | PR URL, `PR-<number>`, or `N/A`. The engine uses this to attach the PR to the work item; missing-PR detection treats anything other than a recognizable URL/PR id as missing unless `noop: true` is set. |
|
|
71
|
-
| `failure_class` | string | One of the `failure_class` enum values below, or `N/A`. Drives retry policy in `engine/dispatch.js
|
|
71
|
+
| `failure_class` | string | One of the `failure_class` enum values below, or `N/A`. Drives retry policy in `engine/dispatch.js`. |
|
|
72
72
|
| `retryable` | boolean | `true` if the engine should auto-retry the dispatch on failure. Overrides the default per-class retry policy when present. |
|
|
73
73
|
| `needs_rerun` | boolean | `true` if the same work needs to be re-dispatched (vs. retried). Used by build-fix and review-fix loops. |
|
|
74
74
|
| `artifacts` | array | Durable artifacts the agent created or updated; surfaces in the dashboard work-item detail modal. See [Artifacts](#artifacts). |
|
|
@@ -235,6 +235,5 @@ If the JSON report exists and is well-formed, the engine ignores the fenced bloc
|
|
|
235
235
|
- `engine/shared.js` — `FAILURE_CLASS`, `COMPLETION_FIELDS`, `dispatchCompletionReportPath()`
|
|
236
236
|
- `engine/lifecycle.js` — `parseCompletionReportFile()`, `parseCompletionNoop()`, `enforcePrAttachmentContract()`
|
|
237
237
|
- `engine/dispatch.js` — `isRetryableFailureReason()`, `writeFailedAgentReport()`
|
|
238
|
-
- `engine/recovery.js` — per-`failure_class` recovery recipes
|
|
239
238
|
- `docs/rfc-completion-json.md` — original RFC describing the protocol's design
|
|
240
239
|
- `playbooks/shared-rules.md` — the per-task "Completion Reports" instruction every playbook inherits
|
package/docs/deprecated.json
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
1
|
[
|
|
2
|
-
{
|
|
3
|
-
"id": "managed-spawn-env-allowlist",
|
|
4
|
-
"removedAt": "2026-05-18",
|
|
5
|
-
"reason": "ENGINE_DEFAULTS.managedSpawn.envKeyAllowlist + envKeyAllowlistPrefixes removed; replaced by envKeyDenyPatterns + envKeyDenyOverrides. The allowlist shape required an engine PR for every new framework/project env prefix (W-mpbpa09c000rd513 tried per-project allowlist extension; user steered away — 'make sure that we are not hardcoding any env variables or being so rigid about it'). The denylist shape matches the actual credential-leakage threat model and lets plain project vars like CONSTELLATION_SERVER, DATABASE_URL, REDIS_HOST work with zero engine config while still blocking credential-shaped keys (AWS_*, *_TOKEN, *_SECRET, etc.). Per-project tightening is supported via project.managedSpawnExtraDenyPatterns (additive only, no per-project override list).",
|
|
6
|
-
"removedLocations": [
|
|
7
|
-
"engine/shared.js ENGINE_DEFAULTS.managedSpawn.envKeyAllowlist (15 keys)",
|
|
8
|
-
"engine/shared.js ENGINE_DEFAULTS.managedSpawn.envKeyAllowlistPrefixes (8 prefixes)",
|
|
9
|
-
"engine/managed-spawn.js _envKeyAllowed (rewritten to deny+override+shape model)",
|
|
10
|
-
"engine/managed-spawn.js buildManagedSpawnHint (env-key guidance rewritten)",
|
|
11
|
-
"PR #2624 (closed, superseded — added per-project allowlist union; replaced here by per-project deny tightening)",
|
|
12
|
-
"test/unit/managed-spawn-validator.test.js 4e/4f/11a (rewritten for denylist semantics)"
|
|
13
|
-
],
|
|
14
|
-
"notes": "Already removed in this PR; entry exists to track the breaking shape change in the deprecation log. Delete entry after 3 days per /cleanup-deprecated."
|
|
15
|
-
},
|
|
16
2
|
{
|
|
17
3
|
"id": "config-poll-key-migration",
|
|
18
4
|
"location": "engine/queries.js:123-162",
|
|
@@ -23,35 +9,20 @@
|
|
|
23
9
|
},
|
|
24
10
|
{
|
|
25
11
|
"id": "legacy-done-aliases",
|
|
26
|
-
"location": "engine/cleanup.js:
|
|
12
|
+
"location": "engine/cleanup.js:970-972",
|
|
27
13
|
"constants": ["LEGACY_DONE_ALIASES", "LEGACY_NEEDS_REVIEW_STATUS"],
|
|
28
14
|
"reason": "Read-side tolerance: cleanup sweep auto-migrates four obsolete work-item / PRD status strings ('in-pr', 'implemented', 'complete', 'needs-human-review') to the canonical 'done' / 'failed' values. The aliases are no longer written anywhere in the engine; the constants exist only to repair stale on-disk values from old engine versions.",
|
|
29
15
|
"targetRemovalDate": null,
|
|
30
|
-
"notes": "Keep indefinitely until telemetry / a sweep log shows zero migrations performed for 30 consecutive days across all known projects (work-items.json + prd/*.json). At that point the constants and both _migrateLegacyItem branches in engine/cleanup.js (definitions at :
|
|
16
|
+
"notes": "Keep indefinitely until telemetry / a sweep log shows zero migrations performed for 30 consecutive days across all known projects (work-items.json + prd/*.json). At that point the constants and both _migrateLegacyItem branches in engine/cleanup.js (definitions at :970-972; usage at :973-1000 for work items and :1051-1057 for PRD missing_features) can be deleted. Total cost on disk today: 4 strings."
|
|
31
17
|
},
|
|
32
18
|
{
|
|
33
|
-
"id": "
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"engine/teams-inbox.json (runtime state, generated by the deleted /api/bot handler)",
|
|
41
|
-
"dashboard.js: POST /api/bot route + handleTeamsBot, TEAMS_INBOX_PATH constant, CC mirror hooks (teamsPostCCResponse), plan-approval/rejection Teams notifications, settings GET/POST teams block",
|
|
42
|
-
"dashboard/js/settings.js: Teams Integration settings UI + teamsPayload submit",
|
|
43
|
-
"engine/lifecycle.js: teamsNotifyCompletion, teamsNotifyPlanEvent (verify-created + plan-completed), teamsNotifyPrEvent (post-merge)",
|
|
44
|
-
"engine/github.js + engine/ado.js: teamsNotifyPrEvent on pr-approved and build-failed",
|
|
45
|
-
"engine/preflight.js: Teams integration doctor check",
|
|
46
|
-
"engine/cli.js: teamsInboxTimer + clearInterval on shutdown",
|
|
47
|
-
"engine/shared.js: ENGINE_DEFAULTS.teams block",
|
|
48
|
-
"package.json: botbuilder dependency (4.23.3)",
|
|
49
|
-
"docs/teams-setup.md, docs/teams-production.md",
|
|
50
|
-
"test/unit/auto-recovery.test.js: ~58 Teams test cases",
|
|
51
|
-
"test/unit/preflight-behavioral.test.js: 4 doctor Teams checks + teams field in the docs-link coverage scenario",
|
|
52
|
-
"README.md / CLAUDE.md / docs/README.md / TODO.md / docs/rfc-completion-json.md: prose references"
|
|
53
|
-
],
|
|
54
|
-
"notes": "The Teams MCP (teams-* tools) lives outside this repo in the CC client config and is NOT affected. If Teams-style notifications are needed again, route them through the MCP layer or an external webhook watch action — do not re-introduce the Bot Framework SDK in-process."
|
|
19
|
+
"id": "completion-fallback-parsers",
|
|
20
|
+
"description": "parseStructuredCompletion and parseCompletionFieldSummary in engine/lifecycle.js",
|
|
21
|
+
"file": "engine/lifecycle.js",
|
|
22
|
+
"lines": "2747, 2848",
|
|
23
|
+
"telemetryGate": "_engine.completionFallbacks must read 0 across sweep window",
|
|
24
|
+
"enforcingTest": "test/unit/completion-fallback-telemetry.test.js:217-234",
|
|
25
|
+
"notes": "Do NOT set removedAt until telemetry confirms zero usage"
|
|
55
26
|
}
|
|
56
27
|
]
|
|
57
28
|
|
|
@@ -184,7 +184,7 @@ The agent must not write the file in pieces. Empty, truncated, or malformed JSON
|
|
|
184
184
|
| Value | When | Engine action |
|
|
185
185
|
|-------|------|---------------|
|
|
186
186
|
| `done` | Work complete; PR pushed (if applicable) | Mark WI `done`, sync PRD |
|
|
187
|
-
| `partial` | Some progress; agent ran out of turns or hit a known stop point | Auto-retry per
|
|
187
|
+
| `partial` | Some progress; agent ran out of turns or hit a known stop point | Auto-retry per runtime/agent retryability |
|
|
188
188
|
| `failed` | Hard failure; no recovery attempted by agent | Use `failure.class` to pick recipe |
|
|
189
189
|
| `noop` | Idempotent bail (review already posted, plan already shipped, etc.) | Mark WI `done` without retry, no failure metric |
|
|
190
190
|
| `needs-review` | Agent could not classify; flag for human | Set WI `failed` with an explicit `failReason` |
|
|
@@ -257,7 +257,7 @@ When `completion.json` is absent or invalid: full fallback to stdout regex on ev
|
|
|
257
257
|
|-------|--------|----------|----------------|
|
|
258
258
|
| **0. Preparation** (no flag) | Day 0 | Engine writes `MINIONS_COMPLETION_PATH` env var. Engine reads completion.json *opportunistically* (uses it when present, falls back to regex when absent). Playbooks updated to write the file. `parseStructuredCompletion`'s ` ```completion ` block continues to be parsed and merged with `completion.json` during this phase only — agents who upgrade slowly still work. | — |
|
|
259
259
|
| **1. Dual-mode** | Day 0 → Day 7 | Same as Phase 0, plus new metric `_engine.completionFile.{parsed,fallback,invalid}` per agent in `metrics.json`. Daily KB sweep posts a digest of fallback rates. | ≥95% of dispatches in the last 24h produce a parseable completion.json |
|
|
260
|
-
| **2. Strict** (gated by `engine.requireCompletionFile = false` → `true`) | Day 7 → Day 10 | When the flag is `true`, missing/invalid completion.json marks the dispatch `failed` with `failure.class = 'config-error'` (no retry
|
|
260
|
+
| **2. Strict** (gated by `engine.requireCompletionFile = false` → `true`) | Day 7 → Day 10 | When the flag is `true`, missing/invalid completion.json marks the dispatch `failed` with `failure.class = 'config-error'` (no retry). Default still `false`. | All permanent agents observed clean for 3 consecutive days |
|
|
261
261
|
| **3. Default flip** | Day 10 | `engine.requireCompletionFile` default becomes `true`. Stdout regex parsers (`syncPrsFromOutput`, `parseReviewVerdict`, etc.) become deprecated shims, registered in `docs/deprecated.json` with a `cleanup` date 3 days out (per the existing `/cleanup-deprecated` skill convention). | — |
|
|
262
262
|
| **4. Removal** | Day 13 | Stdout regex parsers deleted; ` ```completion ` block support removed. Only `completion.json` is read. | — |
|
|
263
263
|
|
package/engine/cc-worker-pool.js
CHANGED
|
@@ -96,6 +96,19 @@ const _internals = {
|
|
|
96
96
|
const _tabs = new Map();
|
|
97
97
|
let _reaperTimer = null;
|
|
98
98
|
|
|
99
|
+
// CC_POOL_TRACE-gated structured trace logger. Off by default; enable via
|
|
100
|
+
// `CC_POOL_TRACE=1 minions restart` to dump every getSession lifecycle
|
|
101
|
+
// transition, stream sessionId capture, and session/update notification
|
|
102
|
+
// match/mismatch to stderr. Added for W-mpdavudb000v8446 follow-up so the
|
|
103
|
+
// next investigation cycle can correlate engine state with the user-perceived
|
|
104
|
+
// first-message hang. NO PII — only tabId (caller-supplied), sessionIds
|
|
105
|
+
// (opaque ACP ids), and protocol flags. Safe to leave on in dev/staging.
|
|
106
|
+
function _trace(...parts) {
|
|
107
|
+
if (!process.env.CC_POOL_TRACE) return;
|
|
108
|
+
try { process.stderr.write('[cc-pool] ' + parts.join(' ') + '\n'); }
|
|
109
|
+
catch { /* swallow telemetry errors */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
99
112
|
function _hashMcpServers(mcpServers) {
|
|
100
113
|
// Stable hash via JSON.stringify; mcpServers is an array of plain objects
|
|
101
114
|
// in practice (name/command/env) so the natural key order is fine.
|
|
@@ -141,6 +154,19 @@ class Worker {
|
|
|
141
154
|
// settles. Racing getSession() callers await this to avoid the
|
|
142
155
|
// "warm-reuse path returns sessionId=null while init is still pending"
|
|
143
156
|
// hang on first message of a freshly-warmed tab (W-mpd45blx00072f04).
|
|
157
|
+
//
|
|
158
|
+
// Follow-up investigation (W-mpdavudb000v8446) verified the post-ab141995
|
|
159
|
+
// engine path holds the necessary invariants (see
|
|
160
|
+
// test/unit/cc-worker-pool-fresh-tab-race.test.js):
|
|
161
|
+
// * after `await worker.initPromise`, worker.sessionId is the real id
|
|
162
|
+
// * Worker.stream sets inflight.sessionId to that same real id
|
|
163
|
+
// * session/prompt is written with sessionId === inflight.sessionId
|
|
164
|
+
// When the symptom recurs (intermittent first-message hang despite the
|
|
165
|
+
// fix), it's almost certainly downstream of the pool — SSE delivery,
|
|
166
|
+
// browser-side render, or telemetry overstating delivery. Set
|
|
167
|
+
// `CC_POOL_TRACE=1` to dump every state transition + sessionId snapshot
|
|
168
|
+
// through the pool to stderr so the next investigation can correlate
|
|
169
|
+
// engine state with the user-perceived hang.
|
|
144
170
|
this.initPromise = null;
|
|
145
171
|
}
|
|
146
172
|
|
|
@@ -253,8 +279,24 @@ class Worker {
|
|
|
253
279
|
return;
|
|
254
280
|
}
|
|
255
281
|
// Notification (no id) — only `session/update` matters for streaming.
|
|
256
|
-
if (obj.method === 'session/update' && obj.params
|
|
257
|
-
|
|
282
|
+
if (obj.method === 'session/update' && obj.params) {
|
|
283
|
+
// Trace EVERY session/update notification, including drops — this is
|
|
284
|
+
// exactly where the W-mpd45blx00072f04 hang manifested (chunks dropped
|
|
285
|
+
// because inflight.sessionId was null). Logging both the notification
|
|
286
|
+
// sid and the inflight sid lets the next investigation cycle prove
|
|
287
|
+
// whether the engine still drops chunks. (W-mpdavudb000v8446)
|
|
288
|
+
const notifSid = obj.params.sessionId;
|
|
289
|
+
const inflightSid = this.inflight ? this.inflight.sessionId : null;
|
|
290
|
+
const updKind = obj.params.update && obj.params.update.sessionUpdate;
|
|
291
|
+
if (!this.inflight) {
|
|
292
|
+
_trace(`tab=${this.tabId} session/update dropped: no inflight (notifSid=${notifSid} kind=${updKind})`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (notifSid !== inflightSid) {
|
|
296
|
+
_trace(`tab=${this.tabId} session/update dropped: sid mismatch (notifSid=${notifSid} inflightSid=${inflightSid} kind=${updKind})`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
_trace(`tab=${this.tabId} session/update delivered: sid=${notifSid} kind=${updKind}`);
|
|
258
300
|
const update = obj.params.update;
|
|
259
301
|
if (!update) return;
|
|
260
302
|
if (update.sessionUpdate === 'agent_message_chunk') {
|
|
@@ -338,6 +380,14 @@ class Worker {
|
|
|
338
380
|
settled: false,
|
|
339
381
|
};
|
|
340
382
|
this.inflight = inflight;
|
|
383
|
+
// W-mpdavudb000v8446 — trace the sessionId captured by inflight at the
|
|
384
|
+
// exact moment Worker.stream commits to a write. inflight.sessionId is
|
|
385
|
+
// the value session/update notifications must match against in
|
|
386
|
+
// _handleMessage; if it's ever null, every chunk for this turn is silently
|
|
387
|
+
// dropped (the ab141995 hang signature). Pair with the [cc-pool] dispatch
|
|
388
|
+
// log on the dashboard side to correlate engine state with user-perceived
|
|
389
|
+
// delivery.
|
|
390
|
+
_trace(`tab=${this.tabId} stream begin: worker.sessionId=${this.sessionId} inflight.sessionId=${inflight.sessionId} reqId=${id}`);
|
|
341
391
|
|
|
342
392
|
if (signal && typeof signal.addEventListener === 'function') {
|
|
343
393
|
inflight.signalHandler = () => this.cancel();
|
|
@@ -504,6 +554,7 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
|
|
|
504
554
|
// 'new-session' — proc reused, fresh session/new (sysprompt hash changed)
|
|
505
555
|
// 'cold-spawn' — fresh proc + initialize + session/new
|
|
506
556
|
let lifecycle = 'warm-reuse';
|
|
557
|
+
_trace(`tab=${tabId} getSession entry: worker.exists=${!!worker} worker.sessionId=${worker?.sessionId ?? 'null'} worker.initPromise=${worker?.initPromise ? 'pending' : 'null'}`);
|
|
507
558
|
|
|
508
559
|
if (worker) {
|
|
509
560
|
// W-mpd45blx00072f04: if the existing worker is still mid-init (warm
|
|
@@ -516,6 +567,7 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
|
|
|
516
567
|
// freshly-warmed CC tab hangs (no chunks streamed, eventual onDone
|
|
517
568
|
// with empty text).
|
|
518
569
|
if (worker.initPromise) {
|
|
570
|
+
_trace(`tab=${tabId} getSession await-init: joining in-flight initPromise`);
|
|
519
571
|
try {
|
|
520
572
|
await worker.initPromise;
|
|
521
573
|
} catch (err) {
|
|
@@ -523,10 +575,12 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
|
|
|
523
575
|
// (or is about to) delete _tabs[tabId] and close the worker in its
|
|
524
576
|
// own catch handler. Surface the same error to this caller so the
|
|
525
577
|
// dashboard's spawn-failed path runs instead of hanging.
|
|
578
|
+
_trace(`tab=${tabId} getSession await-init failed: ${err.message}`);
|
|
526
579
|
throw err;
|
|
527
580
|
}
|
|
528
581
|
// Re-read in case the failing initPromise's cleanup already ran.
|
|
529
582
|
worker = _tabs.get(tabId) || null;
|
|
583
|
+
_trace(`tab=${tabId} getSession await-init done: worker.exists=${!!worker} worker.sessionId=${worker?.sessionId ?? 'null'}`);
|
|
530
584
|
}
|
|
531
585
|
}
|
|
532
586
|
|
|
@@ -592,6 +646,22 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
|
|
|
592
646
|
|
|
593
647
|
_ensureReaper();
|
|
594
648
|
|
|
649
|
+
// W-mpdavudb000v8446 — trace the handle being returned. If lifecycle is
|
|
650
|
+
// 'warm-reuse' but sessionId is null/empty, the engine has hit a state the
|
|
651
|
+
// ab141995 fix was supposed to prevent — surface it loudly. The empty-id
|
|
652
|
+
// case is also caught defensively below so callers can react instead of
|
|
653
|
+
// wedging on a null-sid session/prompt frame.
|
|
654
|
+
_trace(`tab=${tabId} getSession return: lifecycle=${lifecycle} sessionId=${worker.sessionId ?? 'null'}`);
|
|
655
|
+
if (!worker.sessionId) {
|
|
656
|
+
// This is the bug class the ab141995 fix closed; if it ever recurs the
|
|
657
|
+
// engine should fail loudly rather than hand back a half-initialized
|
|
658
|
+
// handle. Throwing here lets the dashboard surface spawn-failed instead
|
|
659
|
+
// of the silent thinking-dots-forever symptom.
|
|
660
|
+
throw new Error(
|
|
661
|
+
`cc-worker-pool: getSession returning handle with null sessionId (tab=${tabId} lifecycle=${lifecycle}) — engine race regression, see W-mpd45blx00072f04 / W-mpdavudb000v8446`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
595
665
|
return {
|
|
596
666
|
sessionId: worker.sessionId,
|
|
597
667
|
lifecycle,
|
package/engine/lifecycle.js
CHANGED
|
@@ -1684,7 +1684,8 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1684
1684
|
const prevReviewStatus = reviewPr?.reviewStatus || '';
|
|
1685
1685
|
const wasNegative = prevReviewStatus === 'changes-requested' || prevReviewStatus === 'waiting'
|
|
1686
1686
|
|| liveStatus === 'changes-requested' || liveStatus === 'waiting';
|
|
1687
|
-
|
|
1687
|
+
const autoApplyVote = config?.engine?.autoApplyReviewVote ?? ENGINE_DEFAULTS.autoApplyReviewVote;
|
|
1688
|
+
if (autoApplyVote && verdictRaw === 'approved' && !isSelfReview && wasNegative && projectObjForChecks) {
|
|
1688
1689
|
try {
|
|
1689
1690
|
const reconcileFn = hostForChecks === 'github'
|
|
1690
1691
|
? require('./github').dismissPriorViewerChangesRequestedReviews
|
|
@@ -1707,7 +1708,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1707
1708
|
}
|
|
1708
1709
|
}
|
|
1709
1710
|
|
|
1710
|
-
if (liveStatus && liveStatus !== 'pending') postReviewStatus = liveStatus;
|
|
1711
|
+
if (autoApplyVote && liveStatus && liveStatus !== 'pending') postReviewStatus = liveStatus;
|
|
1711
1712
|
|
|
1712
1713
|
// Fallback: if live check returned pending (e.g., GitHub self-approval blocked), use the agent's completion report.
|
|
1713
1714
|
if (!postReviewStatus) {
|
package/engine/shared.js
CHANGED
|
@@ -1676,6 +1676,7 @@ const ENGINE_DEFAULTS = {
|
|
|
1676
1676
|
autoReviewPrs: true, // auto-dispatch review agents for newly opened agent PRs
|
|
1677
1677
|
autoReReviewPrs: true, // auto-dispatch review agents after a PR fix is pushed
|
|
1678
1678
|
autoFixReviewFeedback: true, // auto-dispatch fix agents for minions review changes-requested verdicts
|
|
1679
|
+
autoApplyReviewVote: false, // when true, review verdicts (APPROVE / REQUEST_CHANGES) automatically flip the platform vote; when false (default), verdicts are informational only
|
|
1679
1680
|
autoFixHumanComments: true, // auto-dispatch fix agents for actionable human PR comments
|
|
1680
1681
|
prNoOpFixPauseAttempts: 2, // pause one PR automation cause after repeated no-op fixes for unchanged evidence
|
|
1681
1682
|
completionReportRetentionDays: 90, // retain completion report sidecars beyond capped dispatch history
|
|
@@ -1735,6 +1736,7 @@ const ENGINE_DEFAULTS = {
|
|
|
1735
1736
|
ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
|
|
1736
1737
|
enablePreDispatchEval: true, // P-d2a9f6e5: cheap LLM gate before queueing — on by default. See engine/pre-dispatch-eval.js (Ripley §3 recommendation, 2026-05-11 architecture review). Validates from acceptance_criteria when present, falls back to description when criteria are absent but description is rich (≥80 chars). Fail-open on any validator error.
|
|
1737
1738
|
completionNonceRequired: false, // P-d2a8f6c1 (agent trust boundary F8): when true, a missing `nonce` field in the completion JSON hard-fails the dispatch with failure_class:'completion-nonce-mismatch'. Default false for one release so older agents/runtime caches that haven't picked up the prompt change degrade with a warning instead of breaking. Mismatched nonces hard-fail regardless of this flag. See docs/completion-reports.md → "Trust boundary".
|
|
1739
|
+
autoApplyReviewVote: false, // W-mpea9fyb0010febf: when true, review verdict flips the platform vote (ADO resetReviewerNegativeVote / GitHub dismissPriorViewerChangesRequestedReviews). When false (default), the verdict is recorded in pull-requests.json reviewStatus only — informational, no platform side-effect.
|
|
1738
1740
|
|
|
1739
1741
|
// ── Runtime fleet (P-3b8e5f1d) ──────────────────────────────────────────────
|
|
1740
1742
|
// Single source of truth for which CLI runtime + model every spawn uses.
|
|
@@ -3213,7 +3215,15 @@ function buildWorktreeDirName({
|
|
|
3213
3215
|
const suffix = _worktreeNameSuffix(dispatchId, projectName, branchName);
|
|
3214
3216
|
if (platform === 'win32') return `W-${suffix}`;
|
|
3215
3217
|
const projectSlug = String(projectName || 'default').replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
3216
|
-
|
|
3218
|
+
// `sanitizeBranch` preserves `/` (legit in git ref names) but on POSIX that
|
|
3219
|
+
// turns the FS dir name into a nested path. Flatten by replacing `/` → `-`
|
|
3220
|
+
// so the dir name is a single basename. Without this, `path.join(parent,
|
|
3221
|
+
// dirName)` creates `parent/work/W-…/` on Linux for a `work/W-…` branch,
|
|
3222
|
+
// and `readdirSync(parent)` returns `work` (not the full name) — breaking
|
|
3223
|
+
// engine/worktree-gc.js's globalLiveDirNames lookup and evicting live
|
|
3224
|
+
// worktrees on boot.
|
|
3225
|
+
const branchSlug = sanitizeBranch(branchName || 'worktree').replace(/\//g, '-');
|
|
3226
|
+
return `${projectSlug}-${branchSlug}-${suffix}`;
|
|
3217
3227
|
}
|
|
3218
3228
|
|
|
3219
3229
|
/**
|
|
@@ -3677,6 +3687,31 @@ function isPrCompatibleWithProject(project, prRef, url = '') {
|
|
|
3677
3687
|
return !getPrProjectScopeMismatch(project, prRef, url);
|
|
3678
3688
|
}
|
|
3679
3689
|
|
|
3690
|
+
/**
|
|
3691
|
+
* Check if a parsed ADO PR scope is compatible with a project config,
|
|
3692
|
+
* considering that the repo segment in the URL might be either the friendly
|
|
3693
|
+
* repoName or the repositoryId (GUID). Case-insensitive comparison.
|
|
3694
|
+
*/
|
|
3695
|
+
function isAdoPrScopeCompatible(parsedScope, project) {
|
|
3696
|
+
if (!parsedScope || !project) return false;
|
|
3697
|
+
const colonIdx = String(parsedScope).indexOf(':');
|
|
3698
|
+
if (colonIdx < 0) return false;
|
|
3699
|
+
const host = String(parsedScope).slice(0, colonIdx);
|
|
3700
|
+
if (host !== 'ado') return false;
|
|
3701
|
+
const rest = String(parsedScope).slice(colonIdx + 1);
|
|
3702
|
+
const parts = rest.split('/');
|
|
3703
|
+
if (parts.length !== 3) return false;
|
|
3704
|
+
const [scopeOrg, scopeProject, scopeRepo] = parts;
|
|
3705
|
+
const projOrg = normalizePrScopeSegment(project.adoOrg);
|
|
3706
|
+
const projAdoProject = normalizePrScopeSegment(project.adoProject);
|
|
3707
|
+
if (!projOrg || !projAdoProject) return false;
|
|
3708
|
+
if (scopeOrg !== projOrg || scopeProject !== projAdoProject) return false;
|
|
3709
|
+
const projRepoName = normalizePrScopeSegment(project.repoName);
|
|
3710
|
+
const projRepositoryId = normalizePrScopeSegment(project.repositoryId);
|
|
3711
|
+
if (!projRepoName && !projRepositoryId) return false;
|
|
3712
|
+
return scopeRepo === projRepoName || scopeRepo === projRepositoryId;
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3680
3715
|
/**
|
|
3681
3716
|
* Build a canonical, repository-scoped PR identifier.
|
|
3682
3717
|
*
|
|
@@ -4752,6 +4787,7 @@ module.exports = {
|
|
|
4752
4787
|
getPrScopeInfo,
|
|
4753
4788
|
getPrProjectScopeMismatch,
|
|
4754
4789
|
isPrCompatibleWithProject,
|
|
4790
|
+
isAdoPrScopeCompatible,
|
|
4755
4791
|
getCanonicalPrId,
|
|
4756
4792
|
findPrRecord,
|
|
4757
4793
|
snapshotPrRecord,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1989",
|
|
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"
|
package/engine/recovery.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* engine/recovery.js — Recovery recipes for classified agent failures.
|
|
3
|
-
* Maps FAILURE_CLASS values to per-class retry limits and escalation policies.
|
|
4
|
-
* Zero external dependencies — uses only Node.js built-ins and imports from shared.js.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { FAILURE_CLASS, ESCALATION_POLICY, ENGINE_DEFAULTS } = require('./shared');
|
|
8
|
-
|
|
9
|
-
// ─── Recovery Recipes ───────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Each recipe defines:
|
|
13
|
-
* maxAttempts — max retries for this failure class (0 = never retry)
|
|
14
|
-
* escalation — ESCALATION_POLICY value
|
|
15
|
-
* freshSession — whether to clear session.json before retry
|
|
16
|
-
* description — human-readable explanation for logs/dashboard
|
|
17
|
-
*/
|
|
18
|
-
const RECOVERY_RECIPES = new Map([
|
|
19
|
-
[FAILURE_CLASS.CONFIG_ERROR, {
|
|
20
|
-
maxAttempts: 0,
|
|
21
|
-
escalation: ESCALATION_POLICY.NO_RETRY,
|
|
22
|
-
freshSession: false,
|
|
23
|
-
description: 'Configuration error — fix config before retrying',
|
|
24
|
-
}],
|
|
25
|
-
[FAILURE_CLASS.PERMISSION_BLOCKED, {
|
|
26
|
-
maxAttempts: 0,
|
|
27
|
-
escalation: ESCALATION_POLICY.NO_RETRY,
|
|
28
|
-
freshSession: false,
|
|
29
|
-
description: 'Permission/trust gate blocked — requires human intervention',
|
|
30
|
-
}],
|
|
31
|
-
[FAILURE_CLASS.AUTH, {
|
|
32
|
-
maxAttempts: 0,
|
|
33
|
-
escalation: ESCALATION_POLICY.NO_RETRY,
|
|
34
|
-
freshSession: false,
|
|
35
|
-
description: 'Git/network authentication failed (missing az login, expired token, GCM prompt) — requires human credential fix before retry',
|
|
36
|
-
}],
|
|
37
|
-
[FAILURE_CLASS.MERGE_CONFLICT, {
|
|
38
|
-
maxAttempts: 2,
|
|
39
|
-
escalation: ESCALATION_POLICY.RETRY_SAME,
|
|
40
|
-
freshSession: false,
|
|
41
|
-
description: 'Merge conflict — retry may succeed after dependency updates',
|
|
42
|
-
}],
|
|
43
|
-
[FAILURE_CLASS.BUILD_FAILURE, {
|
|
44
|
-
maxAttempts: 2,
|
|
45
|
-
escalation: ESCALATION_POLICY.RETRY_SAME,
|
|
46
|
-
freshSession: false,
|
|
47
|
-
description: 'Build/test failure — retry with same context for iterative fix',
|
|
48
|
-
}],
|
|
49
|
-
[FAILURE_CLASS.TIMEOUT, {
|
|
50
|
-
maxAttempts: 1,
|
|
51
|
-
escalation: ESCALATION_POLICY.RETRY_FRESH,
|
|
52
|
-
freshSession: true,
|
|
53
|
-
description: 'Timeout — retry with fresh session to avoid stuck state',
|
|
54
|
-
}],
|
|
55
|
-
[FAILURE_CLASS.EMPTY_OUTPUT, {
|
|
56
|
-
maxAttempts: 1,
|
|
57
|
-
escalation: ESCALATION_POLICY.HUMAN_REVIEW,
|
|
58
|
-
freshSession: true,
|
|
59
|
-
description: 'Empty output — agent produced nothing useful, flag for review',
|
|
60
|
-
}],
|
|
61
|
-
[FAILURE_CLASS.SPAWN_ERROR, {
|
|
62
|
-
maxAttempts: 2,
|
|
63
|
-
escalation: ESCALATION_POLICY.RETRY_FRESH,
|
|
64
|
-
freshSession: true,
|
|
65
|
-
description: 'Spawn error — retry with fresh session after transient failure',
|
|
66
|
-
}],
|
|
67
|
-
[FAILURE_CLASS.NETWORK_ERROR, {
|
|
68
|
-
maxAttempts: 3,
|
|
69
|
-
escalation: ESCALATION_POLICY.AUTO,
|
|
70
|
-
freshSession: false,
|
|
71
|
-
description: 'Network/API error — retry with exponential backoff',
|
|
72
|
-
}],
|
|
73
|
-
[FAILURE_CLASS.MAX_TURNS, {
|
|
74
|
-
maxAttempts: 3,
|
|
75
|
-
escalation: ESCALATION_POLICY.RETRY_SAME,
|
|
76
|
-
freshSession: false,
|
|
77
|
-
description: 'Max turns reached — work in progress, retry same agent to continue',
|
|
78
|
-
}],
|
|
79
|
-
[FAILURE_CLASS.OUT_OF_CONTEXT, {
|
|
80
|
-
maxAttempts: 1,
|
|
81
|
-
escalation: ESCALATION_POLICY.HUMAN_REVIEW,
|
|
82
|
-
freshSession: true,
|
|
83
|
-
description: 'Context exhausted — retry with fresh session, flag if repeated',
|
|
84
|
-
}],
|
|
85
|
-
[FAILURE_CLASS.WORKTREE_PREFLIGHT, {
|
|
86
|
-
maxAttempts: 0,
|
|
87
|
-
escalation: ESCALATION_POLICY.NO_RETRY,
|
|
88
|
-
freshSession: false,
|
|
89
|
-
description: 'Worktree preflight rejected — same inputs will recompute to the same rejection (drive-root rootDir, nested-in-project worktree). Fix the dispatch (attach a project, move MINIONS_DIR, or override engine.worktreeRoot) before retrying.',
|
|
90
|
-
}],
|
|
91
|
-
[FAILURE_CLASS.UNKNOWN, {
|
|
92
|
-
maxAttempts: null, // null = fall back to ENGINE_DEFAULTS.maxRetries
|
|
93
|
-
escalation: ESCALATION_POLICY.AUTO,
|
|
94
|
-
freshSession: false,
|
|
95
|
-
description: 'Unclassified failure — use default retry behavior',
|
|
96
|
-
}],
|
|
97
|
-
]);
|
|
98
|
-
|
|
99
|
-
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Get the recovery recipe for a failure class.
|
|
103
|
-
* @param {string} failureClass — one of FAILURE_CLASS values
|
|
104
|
-
* @returns {object} recipe with maxAttempts, escalation, freshSession, description
|
|
105
|
-
*/
|
|
106
|
-
function getRecoveryRecipe(failureClass) {
|
|
107
|
-
return RECOVERY_RECIPES.get(failureClass) || RECOVERY_RECIPES.get(FAILURE_CLASS.UNKNOWN);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Determine whether a failed dispatch should be retried based on its failure class
|
|
112
|
-
* and current attempt count.
|
|
113
|
-
* @param {string} failureClass — one of FAILURE_CLASS values (or empty for unclassified)
|
|
114
|
-
* @param {number} attemptCount — how many times this item has already been retried
|
|
115
|
-
* @returns {boolean} true if another retry is allowed
|
|
116
|
-
*/
|
|
117
|
-
function shouldRetry(failureClass, attemptCount = 0) {
|
|
118
|
-
const recipe = getRecoveryRecipe(failureClass || FAILURE_CLASS.UNKNOWN);
|
|
119
|
-
// null maxAttempts = fall back to global ENGINE_DEFAULTS.maxRetries
|
|
120
|
-
const limit = recipe.maxAttempts !== null ? recipe.maxAttempts : ENGINE_DEFAULTS.maxRetries;
|
|
121
|
-
return attemptCount < limit;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ─── Exports ────────────────────────────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
module.exports = {
|
|
127
|
-
RECOVERY_RECIPES,
|
|
128
|
-
getRecoveryRecipe,
|
|
129
|
-
shouldRetry,
|
|
130
|
-
};
|