@yemi33/minions 0.1.1649 → 0.1.1651
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 -0
- package/engine/ado.js +17 -30
- package/engine/copilot-models.json +1 -1
- package/engine/dispatch.js +14 -6
- package/engine/github.js +24 -22
- package/engine/lifecycle.js +276 -48
- package/engine/runtimes/claude.js +90 -0
- package/engine/runtimes/copilot.js +90 -0
- package/engine/shared.js +45 -3
- package/engine/spawn-agent.js +9 -6
- package/engine/timeout.js +17 -4
- package/engine.js +108 -139
- package/package.json +1 -1
- package/playbooks/fix.md +2 -2
- package/playbooks/implement-shared.md +2 -2
- package/playbooks/review.md +2 -3
- package/playbooks/shared-rules.md +12 -2
package/engine.js
CHANGED
|
@@ -141,7 +141,7 @@ const { renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS,
|
|
|
141
141
|
const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, reconcilePrdStatuses, handlePostMerge, checkPlanCompletion,
|
|
142
142
|
syncPrsFromOutput, updatePrAfterReview, updatePrAfterFix, checkForLearnings, extractSkillsFromOutput,
|
|
143
143
|
updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs,
|
|
144
|
-
isItemCompleted, classifyFailure, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemPath } = require('./engine/lifecycle');
|
|
144
|
+
isItemCompleted, classifyFailure: classifyFailureFallback, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemPath } = require('./engine/lifecycle');
|
|
145
145
|
|
|
146
146
|
// ─── Agent Spawner ──────────────────────────────────────────────────────────
|
|
147
147
|
|
|
@@ -232,62 +232,27 @@ async function preflightMergeSimulation(deps, mainRef, gitOpts, cwd) {
|
|
|
232
232
|
return { ok: true };
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
[WORK_TYPE.EXPLORE]: 30, [WORK_TYPE.ASK]: 20, [WORK_TYPE.REVIEW]: 30,
|
|
238
|
-
[WORK_TYPE.DECOMPOSE]: 15, [WORK_TYPE.PLAN]: 30, [WORK_TYPE.PLAN_TO_PRD]: 35,
|
|
239
|
-
[WORK_TYPE.MEETING]: 30,
|
|
240
|
-
[WORK_TYPE.IMPLEMENT]: 75, [WORK_TYPE.IMPLEMENT_LARGE]: 75, [WORK_TYPE.FIX]: 75,
|
|
241
|
-
[WORK_TYPE.TEST]: 50, [WORK_TYPE.VERIFY]: 100, [WORK_TYPE.DOCS]: 30,
|
|
242
|
-
};
|
|
243
|
-
function _maxTurnsForType(type, engineConfig) {
|
|
244
|
-
// Priority: per-type config override → global config override → built-in per-type default → global default
|
|
235
|
+
function _maxTurnsForType(type, engineConfig = {}) {
|
|
236
|
+
// Engine keeps only explicit operator caps; runtime/playbook owns task-level pacing.
|
|
245
237
|
const perType = engineConfig.maxTurnsByType || {};
|
|
246
238
|
if (perType[type]) return perType[type];
|
|
247
|
-
|
|
248
|
-
return globalOverride || _MAX_TURNS_BY_TYPE[type] || ENGINE_DEFAULTS.maxTurns;
|
|
239
|
+
return engineConfig.maxTurns || ENGINE_DEFAULTS.maxTurns;
|
|
249
240
|
}
|
|
250
241
|
|
|
251
242
|
// ─── Runtime adapter integration (P-2a6d9c4f) ────────────────────────────────
|
|
252
|
-
//
|
|
253
|
-
// _buildAgentSpawnFlags translates a resolved opts bag into the named CLI
|
|
254
|
-
// flags consumed by `engine/spawn-agent.js`. spawn-agent.js parses these back
|
|
255
|
-
// into an opts object and calls `runtime.buildArgs(opts)` once — keeping the
|
|
256
|
-
// adapter as the single source of truth for CLI-level flag formatting and
|
|
257
|
-
// avoiding double-emission.
|
|
258
|
-
//
|
|
259
|
-
// Capability gating happens HERE (in engine.js), not in the adapter:
|
|
260
|
-
// - Runtimes that don't support a feature (Copilot has no budgetCap, no
|
|
261
|
-
// bareMode, no fallbackModel) never see the flag. The adapter doesn't
|
|
262
|
-
// have to silently drop opts it can't honor.
|
|
263
|
-
// - Truly cross-runtime opts (model, maxTurns, allowedTools, effort,
|
|
264
|
-
// sessionId) are emitted whenever set; capability flags filter the
|
|
265
|
-
// ones that are runtime-specific.
|
|
266
|
-
// - Copilot-specific opts (`stream`, `disableBuiltinMcps`,
|
|
267
|
-
// `suppressAgentsMd`, `reasoningSummaries`) are emitted unconditionally;
|
|
268
|
-
// the Claude adapter ignores them via the "tolerate unknown opts" rule.
|
|
269
243
|
|
|
270
244
|
function _buildAgentSpawnFlags(runtime, opts = {}) {
|
|
245
|
+
if (runtime && typeof runtime.buildSpawnFlags === 'function') return runtime.buildSpawnFlags(opts);
|
|
271
246
|
const caps = (runtime && runtime.capabilities) || {};
|
|
272
247
|
const flags = ['--runtime', String(runtime?.name || 'claude')];
|
|
273
|
-
|
|
274
|
-
// Always-applicable: every runtime understands these.
|
|
275
248
|
if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
|
|
276
249
|
if (opts.model) flags.push('--model', String(opts.model));
|
|
277
250
|
if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
|
|
278
|
-
|
|
279
|
-
// Capability-gated. The first three (effort/resume) gate on whether the
|
|
280
|
-
// runtime exposes the feature at all; the next three gate on whether the
|
|
281
|
-
// runtime's CLI surface actually accepts the flag.
|
|
282
251
|
if (caps.effortLevels && opts.effort) flags.push('--effort', String(opts.effort));
|
|
283
252
|
if (caps.sessionResume && opts.sessionId) flags.push('--resume', String(opts.sessionId));
|
|
284
253
|
if (caps.budgetCap && opts.maxBudget != null) flags.push('--max-budget-usd', String(opts.maxBudget));
|
|
285
254
|
if (caps.bareMode && opts.bare === true) flags.push('--bare');
|
|
286
255
|
if (caps.fallbackModel && opts.fallbackModel) flags.push('--fallback-model', String(opts.fallbackModel));
|
|
287
|
-
|
|
288
|
-
// Copilot-specific opts. Always emitted when set; the Claude adapter
|
|
289
|
-
// silently ignores them (per the "tolerate unknown opts" rule from
|
|
290
|
-
// P-7e3a8b1c). Engine code never branches on `runtime.name`.
|
|
291
256
|
if (opts.stream != null && opts.stream !== '') flags.push('--stream', String(opts.stream));
|
|
292
257
|
if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
|
|
293
258
|
if (opts.suppressAgentsMd === true) flags.push('--no-custom-instructions');
|
|
@@ -296,6 +261,26 @@ function _buildAgentSpawnFlags(runtime, opts = {}) {
|
|
|
296
261
|
return flags;
|
|
297
262
|
}
|
|
298
263
|
|
|
264
|
+
function _runtimeLogger() {
|
|
265
|
+
return {
|
|
266
|
+
info: (msg) => log('info', msg),
|
|
267
|
+
warn: (msg) => log('warn', msg),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function _classifyAgentFailure(runtime, code, stdout, stderr) {
|
|
272
|
+
if (runtime && typeof runtime.classifyFailure === 'function') {
|
|
273
|
+
const classified = runtime.classifyFailure({
|
|
274
|
+
code,
|
|
275
|
+
stdout,
|
|
276
|
+
stderr,
|
|
277
|
+
fallback: classifyFailureFallback,
|
|
278
|
+
});
|
|
279
|
+
if (classified && classified.failureClass) return classified;
|
|
280
|
+
}
|
|
281
|
+
return { failureClass: classifyFailureFallback(code, stdout, stderr) };
|
|
282
|
+
}
|
|
283
|
+
|
|
299
284
|
function ackPendingSteeringFiles(agentId, procInfo, rawOutput, observedAtMs = Date.now()) {
|
|
300
285
|
if (!procInfo?._pendingSteeringFiles?.length || !rawOutput) return;
|
|
301
286
|
const acked = steering.ackProcessedSteeringMessages(agentId, procInfo._pendingSteeringFiles, rawOutput, { observedAtMs });
|
|
@@ -449,12 +434,31 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
449
434
|
const systemPrompt = buildSystemPrompt(agentId, config, project);
|
|
450
435
|
const agentContext = buildAgentContext(agentId, config, project);
|
|
451
436
|
const pendingSteering = steering.buildPendingSteeringPrompt(agentId);
|
|
437
|
+
const completionReportPath = shared.dispatchCompletionReportPath(id);
|
|
438
|
+
if (completionReportPath) {
|
|
439
|
+
try {
|
|
440
|
+
fs.mkdirSync(path.dirname(completionReportPath), { recursive: true });
|
|
441
|
+
safeUnlink(completionReportPath);
|
|
442
|
+
} catch (e) { log('warn', `completion report setup: ${e.message}`); }
|
|
443
|
+
}
|
|
444
|
+
const completionReportInstruction = completionReportPath ? [
|
|
445
|
+
'## Completion Report',
|
|
446
|
+
'',
|
|
447
|
+
`Before exiting, write a JSON completion report to: \`${completionReportPath}\``,
|
|
448
|
+
'',
|
|
449
|
+
'Use this shape: {"status":"success|partial|failed","summary":"...","verdict":"approved|changes-requested|null","pr":"PR URL or id if relevant","failure_class":"...","retryable":true|false,"needs_rerun":true|false}.',
|
|
450
|
+
'This report is the primary completion signal; fenced completion blocks are only a fallback.',
|
|
451
|
+
'',
|
|
452
|
+
].join('\n') : '';
|
|
452
453
|
const taskPromptWithSteering = pendingSteering.prompt
|
|
453
454
|
? `${pendingSteering.prompt}\n\n---\n\n${taskPrompt}`
|
|
454
455
|
: taskPrompt;
|
|
455
|
-
const
|
|
456
|
-
?
|
|
456
|
+
const taskPromptWithReport = completionReportInstruction
|
|
457
|
+
? `${taskPromptWithSteering}\n\n---\n\n${completionReportInstruction}`
|
|
457
458
|
: taskPromptWithSteering;
|
|
459
|
+
const fullTaskPrompt = agentContext
|
|
460
|
+
? `## Agent Context\n\n${agentContext}\n---\n\n## Your Task\n\n${taskPromptWithReport}`
|
|
461
|
+
: taskPromptWithReport;
|
|
458
462
|
const tmpDir = path.join(ENGINE_DIR, 'tmp');
|
|
459
463
|
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
460
464
|
const safeId = id.replace(/[:\\/*?"<>|]/g, '-');
|
|
@@ -890,32 +894,18 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
890
894
|
const resolvedMaxBudget = shared.resolveAgentMaxBudget(agentConfig, engineConfig);
|
|
891
895
|
const resolvedBare = shared.resolveAgentBareMode(agentConfig, engineConfig);
|
|
892
896
|
|
|
893
|
-
|
|
894
|
-
// The capability gate happens inside _buildAgentSpawnFlags — runtimes
|
|
895
|
-
// without effortLevels never see the flag.
|
|
896
|
-
const requestedEffort = engineConfig.agentEffort || (_FAST_WORK_TYPES.has(type) ? 'low' : null);
|
|
897
|
+
const requestedEffort = engineConfig.agentEffort || null;
|
|
897
898
|
|
|
898
|
-
// Session resume: gated on `runtime.capabilities.sessionResume`. Same branch
|
|
899
|
-
// means the agent is continuing work on the same PR/feature (e.g., author
|
|
900
|
-
// fixing their own build failure).
|
|
901
899
|
let cachedSessionId = null;
|
|
902
|
-
if (runtime
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
cachedSessionId = sessionFile.sessionId;
|
|
910
|
-
log('info', `Resuming session ${sessionFile.sessionId} for ${agentId} on branch ${branchName} (age: ${Math.round(sessionAge / 60000)}min)`);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
} catch (e) { log('warn', 'session resume lookup: ' + e.message); }
|
|
900
|
+
if (runtime && typeof runtime.getResumeSessionId === 'function') {
|
|
901
|
+
cachedSessionId = runtime.getResumeSessionId({
|
|
902
|
+
agentId,
|
|
903
|
+
branchName,
|
|
904
|
+
agentsDir: AGENTS_DIR,
|
|
905
|
+
logger: _runtimeLogger(),
|
|
906
|
+
});
|
|
914
907
|
}
|
|
915
908
|
|
|
916
|
-
// Build the spawn-agent.js flag bag. spawn-agent parses the named flags
|
|
917
|
-
// back into an opts object and calls runtime.buildArgs(opts) — so the
|
|
918
|
-
// adapter is the single source of truth for the actual CLI args.
|
|
919
909
|
const args = _buildAgentSpawnFlags(runtime, {
|
|
920
910
|
model: resolvedModel,
|
|
921
911
|
maxTurns: _maxTurnsForType(type, engineConfig),
|
|
@@ -941,6 +931,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
941
931
|
|
|
942
932
|
// Spawn the claude process
|
|
943
933
|
const childEnv = shared.cleanChildEnv();
|
|
934
|
+
if (completionReportPath) childEnv.MINIONS_COMPLETION_REPORT = completionReportPath;
|
|
944
935
|
|
|
945
936
|
// Inject cached ADO token so agents skip re-authentication (#998)
|
|
946
937
|
// getAdoToken() returns cached token (30-min TTL) or null — never blocks on browser auth
|
|
@@ -1025,8 +1016,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1025
1016
|
|
|
1026
1017
|
// Trust gate detection: check first 30s of output for trust/permission prompts
|
|
1027
1018
|
if (!_trustCheckDone && (Date.now() - _spawnTime) <= 30000) {
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1019
|
+
const blockedOnPermission = runtime && typeof runtime.detectPermissionGate === 'function'
|
|
1020
|
+
? runtime.detectPermissionGate(chunk)
|
|
1021
|
+
: false;
|
|
1022
|
+
if (blockedOnPermission) {
|
|
1030
1023
|
_trustCheckDone = true;
|
|
1031
1024
|
updateAgentStatus(id, AGENT_STATUS.TRUST_BLOCKED, 'Agent appears to be waiting for trust approval');
|
|
1032
1025
|
log('warn', `Trust gate detected for ${agentId} (${id}) — agent may be blocked on a permission prompt`);
|
|
@@ -1044,9 +1037,16 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1044
1037
|
const obj = JSON.parse(line);
|
|
1045
1038
|
if (obj.session_id) {
|
|
1046
1039
|
procInfo.sessionId = obj.session_id;
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1040
|
+
if (runtime && typeof runtime.saveSession === 'function') {
|
|
1041
|
+
runtime.saveSession({
|
|
1042
|
+
agentId,
|
|
1043
|
+
dispatchId: id,
|
|
1044
|
+
branch: branchName,
|
|
1045
|
+
sessionId: obj.session_id,
|
|
1046
|
+
agentsDir: AGENTS_DIR,
|
|
1047
|
+
logger: _runtimeLogger(),
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
1050
|
break;
|
|
1051
1051
|
}
|
|
1052
1052
|
}
|
|
@@ -1123,19 +1123,6 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1123
1123
|
return;
|
|
1124
1124
|
}
|
|
1125
1125
|
|
|
1126
|
-
// Steering resume — gated on `runtime.capabilities.sessionResume`. If the
|
|
1127
|
-
// runtime can't resume sessions, fail fast: nothing to spawn here.
|
|
1128
|
-
if (!runtime.capabilities.sessionResume) {
|
|
1129
|
-
log('warn', `Steering: runtime ${runtime.name} does not support session resume — skipping for ${agentId}`);
|
|
1130
|
-
try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Runtime ${runtime.name} does not support session resume. Message was: ${steerMsg}\n`); } catch {}
|
|
1131
|
-
try { fs.unlinkSync(steerPromptPath); } catch {}
|
|
1132
|
-
activeProcesses.delete(id);
|
|
1133
|
-
completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering not supported by runtime', '', { processWorkItemFailure: false });
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// Reuse the same flag-builder used by the main spawn so capability gates
|
|
1138
|
-
// and runtime-specific opts stay consistent across the two paths.
|
|
1139
1126
|
const resumeArgs = _buildAgentSpawnFlags(runtime, {
|
|
1140
1127
|
model: resolvedModel,
|
|
1141
1128
|
maxTurns: engineConfig?.maxTurns || ENGINE_DEFAULTS.maxTurns,
|
|
@@ -1149,9 +1136,18 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1149
1136
|
suppressAgentsMd: engineConfig?.copilotSuppressAgentsMd,
|
|
1150
1137
|
reasoningSummaries: engineConfig?.copilotReasoningSummaries,
|
|
1151
1138
|
});
|
|
1139
|
+
if (!resumeArgs.includes('--resume')) {
|
|
1140
|
+
log('warn', `Steering: runtime ${runtime.name} did not accept session resume — skipping for ${agentId}`);
|
|
1141
|
+
try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Runtime ${runtime.name} does not support session resume. Message was: ${steerMsg}\n`); } catch {}
|
|
1142
|
+
try { fs.unlinkSync(steerPromptPath); } catch {}
|
|
1143
|
+
activeProcesses.delete(id);
|
|
1144
|
+
completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering not supported by runtime', '', { processWorkItemFailure: false });
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1152
1147
|
|
|
1153
1148
|
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
1154
1149
|
const childEnv = shared.cleanChildEnv();
|
|
1150
|
+
if (completionReportPath) childEnv.MINIONS_COMPLETION_REPORT = completionReportPath;
|
|
1155
1151
|
// Inject cached ADO token for steering session too (#998)
|
|
1156
1152
|
try {
|
|
1157
1153
|
const adoToken = await getAdoToken();
|
|
@@ -1284,11 +1280,13 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1284
1280
|
safeWrite(latestPath, outputContent); // overwrite latest for dashboard compat
|
|
1285
1281
|
|
|
1286
1282
|
// Classify failure for non-zero exits
|
|
1287
|
-
const
|
|
1283
|
+
const failureInfo = code !== 0 ? _classifyAgentFailure(runtime, code, stdout, stderr) : {};
|
|
1284
|
+
const failureClass = failureInfo.failureClass;
|
|
1288
1285
|
|
|
1289
|
-
// Detect configuration errors (e.g.
|
|
1286
|
+
// Detect configuration errors (e.g. missing runtime CLI) — fail immediately with clear message
|
|
1290
1287
|
if (code === 78) {
|
|
1291
|
-
const
|
|
1288
|
+
const runtimeHint = runtime.installHint ? ` ${runtime.installHint}` : '';
|
|
1289
|
+
const errMsg = stderr.trim() || `Configuration error — ${runtimeName} runtime unavailable.${runtimeHint}`;
|
|
1292
1290
|
log('error', `Agent ${agentId} (${id}) failed: ${errMsg} [${failureClass}]`);
|
|
1293
1291
|
completeDispatch(id, DISPATCH_RESULT.ERROR, errMsg, '', { failureClass });
|
|
1294
1292
|
try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
|
|
@@ -1298,18 +1296,27 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1298
1296
|
}
|
|
1299
1297
|
|
|
1300
1298
|
// Parse output and run all post-completion hooks
|
|
1301
|
-
const { resultSummary, autoRecovered, completionContractFailure } = await runPostCompletionHooks(dispatchItem, agentId, code, stdout, config);
|
|
1299
|
+
const { resultSummary, autoRecovered, completionContractFailure, structuredCompletion, agentReportedFailure, agentRetryable } = await runPostCompletionHooks(dispatchItem, agentId, code, stdout, config);
|
|
1300
|
+
const retryableDecision = typeof agentRetryable === 'boolean' ? agentRetryable : failureInfo.retryable;
|
|
1302
1301
|
|
|
1303
1302
|
// Move from active to completed in dispatch (single source of truth for agent status)
|
|
1304
1303
|
// autoRecovered: agent failed after creating PRs — treat as success
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1304
|
+
const hardContractFail = completionContractFailure?.severity === 'hard'
|
|
1305
|
+
|| completionContractFailure?.nonTerminal === true;
|
|
1306
|
+
const effectiveResult = hardContractFail ? DISPATCH_RESULT.ERROR : (((code === 0 && !agentReportedFailure) || autoRecovered) ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
|
|
1307
|
+
const completeOpts = hardContractFail
|
|
1307
1308
|
? { processWorkItemFailure: false }
|
|
1308
|
-
: (effectiveResult === DISPATCH_RESULT.ERROR
|
|
1309
|
+
: (effectiveResult === DISPATCH_RESULT.ERROR ? {
|
|
1310
|
+
...(failureClass ? { failureClass } : {}),
|
|
1311
|
+
...(typeof retryableDecision === 'boolean' ? { agentRetryable: retryableDecision } : {}),
|
|
1312
|
+
...(structuredCompletion?.failure_class ? { failureClass: structuredCompletion.failure_class } : {}),
|
|
1313
|
+
} : {});
|
|
1309
1314
|
// Extract last 5 non-empty stderr lines as error context when exit code is non-zero
|
|
1310
1315
|
let errorReason = '';
|
|
1311
|
-
if (
|
|
1316
|
+
if (hardContractFail) {
|
|
1312
1317
|
errorReason = completionContractFailure.reason || 'PR attachment contract failed';
|
|
1318
|
+
} else if (agentReportedFailure && structuredCompletion?.summary) {
|
|
1319
|
+
errorReason = String(structuredCompletion.summary).slice(0, 300);
|
|
1313
1320
|
} else if (effectiveResult === DISPATCH_RESULT.ERROR) {
|
|
1314
1321
|
errorReason = stderr.split('\n').filter(l => l.trim()).slice(-5).join(' | ').trim().slice(0, 300);
|
|
1315
1322
|
// W-mo3zul9pirjb — when claude CLI exits in <3s with code 1 and no output (the
|
|
@@ -2177,24 +2184,10 @@ async function discoverFromPrs(config, project) {
|
|
|
2177
2184
|
// The poller holds reviewStatus at 'waiting' until the reviewer acts on the new code.
|
|
2178
2185
|
const awaitingReReview = reviewStatus === 'waiting' && !!pr.minionsReview?.fixedAt;
|
|
2179
2186
|
|
|
2180
|
-
// Review→fix cycle cap — stop review/fix dispatch after N iterations, but allow build fixes and conflict fixes
|
|
2181
|
-
const evalMax = config.engine?.evalMaxIterations ?? ENGINE_DEFAULTS.evalMaxIterations;
|
|
2182
|
-
const evalCycles = pr._reviewFixCycles || 0;
|
|
2183
|
-
const evalEscalated = evalCycles >= evalMax;
|
|
2184
|
-
if (evalEscalated && !pr._evalEscalated) {
|
|
2185
|
-
try {
|
|
2186
|
-
mutatePullRequests(projectPrPath(project), prs => {
|
|
2187
|
-
const target = shared.findPrRecord(prs, pr, project);
|
|
2188
|
-
if (target) target._evalEscalated = true;
|
|
2189
|
-
});
|
|
2190
|
-
} catch (e) { log('warn', 'mark eval escalated: ' + e.message); }
|
|
2191
|
-
log('warn', `PR ${pr.id}: review→fix escalated after ${evalCycles} cycles — suspending review/re-review and review-fix dispatch; build/conflict fixes may continue`);
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
2187
|
// PRs needing review: evalLoop gates the entire review+fix cycle; pollEnabled ensures reviewStatus is fresh
|
|
2195
2188
|
const reviewEnabled = evalLoopEnabled && pollEnabled;
|
|
2196
2189
|
const alreadyReviewed = pr.lastReviewedAt && (!pr.lastPushedAt || pr.lastPushedAt <= pr.lastReviewedAt);
|
|
2197
|
-
const needsReview = reviewEnabled && reviewStatus === 'pending' && !alreadyReviewed
|
|
2190
|
+
const needsReview = reviewEnabled && reviewStatus === 'pending' && !alreadyReviewed;
|
|
2198
2191
|
if (needsReview) {
|
|
2199
2192
|
const key = `review-${project?.name || 'default'}-${prDisplayId}`;
|
|
2200
2193
|
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
@@ -2266,6 +2259,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2266
2259
|
const earlier = coalesced.map(c => c.feedbackContent).filter(Boolean).join('\n\n---\n\n');
|
|
2267
2260
|
if (earlier) reviewNote = earlier + '\n\n---\n\n' + reviewNote;
|
|
2268
2261
|
}
|
|
2262
|
+
reviewNote = `New PR comments were observed. Read the full PR thread, decide whether the comments require code/documentation/test changes, make only necessary changes, and push if action is needed.\n\n${reviewNote}`;
|
|
2269
2263
|
|
|
2270
2264
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2271
2265
|
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
|
|
@@ -2280,7 +2274,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2280
2274
|
const fixedAfterReview = !!(pr.minionsReview?.fixedAt &&
|
|
2281
2275
|
(!pr.lastReviewedAt || pr.minionsReview.fixedAt > pr.lastReviewedAt));
|
|
2282
2276
|
const needsReReview = reviewEnabled && reviewStatus === 'waiting' &&
|
|
2283
|
-
fixedAfterReview && !
|
|
2277
|
+
fixedAfterReview && !fixDispatched;
|
|
2284
2278
|
if (needsReReview) {
|
|
2285
2279
|
const key = `rereview-${project?.name || 'default'}-${prDisplayId}`;
|
|
2286
2280
|
// Skip isAlreadyDispatched — fixedAfterReview/lastReviewedAt already dedupe; the 1hr
|
|
@@ -2320,7 +2314,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2320
2314
|
|
|
2321
2315
|
// PRs with changes requested → route back to author for fix
|
|
2322
2316
|
// Gate on evalLoopEnabled — the review→fix cycle is the eval loop
|
|
2323
|
-
if (evalLoopEnabled && reviewStatus === 'changes-requested' && !awaitingReReview && !
|
|
2317
|
+
if (evalLoopEnabled && reviewStatus === 'changes-requested' && !awaitingReReview && !fixDispatched) {
|
|
2324
2318
|
const key = `fix-${project?.name || 'default'}-${prDisplayId}`;
|
|
2325
2319
|
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2326
2320
|
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
@@ -2334,13 +2328,6 @@ async function discoverFromPrs(config, project) {
|
|
|
2334
2328
|
}, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2335
2329
|
if (item) {
|
|
2336
2330
|
newWork.push(item); setCooldown(key); fixDispatched = true;
|
|
2337
|
-
// Increment review→fix cycle counter
|
|
2338
|
-
try {
|
|
2339
|
-
mutatePullRequests(projectPrPath(project), prs => {
|
|
2340
|
-
const target = shared.findPrRecord(prs, pr, project);
|
|
2341
|
-
if (target) target._reviewFixCycles = (target._reviewFixCycles || 0) + 1;
|
|
2342
|
-
});
|
|
2343
|
-
} catch (e) { log('warn', 'increment review-fix cycles: ' + e.message); }
|
|
2344
2331
|
}
|
|
2345
2332
|
}
|
|
2346
2333
|
|
|
@@ -2354,23 +2341,6 @@ async function discoverFromPrs(config, project) {
|
|
|
2354
2341
|
const autoFixBuilds = config.engine?.autoFixBuilds ?? ENGINE_DEFAULTS.autoFixBuilds;
|
|
2355
2342
|
const fixThrottled = isAdoProject ? isAdoThrottled() : isGhThrottled();
|
|
2356
2343
|
if (autoFixBuilds && pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing') {
|
|
2357
|
-
const maxBuildFix = config.engine?.maxBuildFixAttempts ?? ENGINE_DEFAULTS.maxBuildFixAttempts;
|
|
2358
|
-
|
|
2359
|
-
// Check if max retry cap reached — escalate to human instead of dispatching another fix
|
|
2360
|
-
if ((pr.buildFixAttempts || 0) >= maxBuildFix) {
|
|
2361
|
-
if (!pr.buildFixEscalated) {
|
|
2362
|
-
try {
|
|
2363
|
-
const prPath = projectPrPath(project);
|
|
2364
|
-
mutatePullRequests(prPath, prs => {
|
|
2365
|
-
const target = shared.findPrRecord(prs, pr, project);
|
|
2366
|
-
if (target) target.buildFixEscalated = true;
|
|
2367
|
-
});
|
|
2368
|
-
} catch (e) { log('warn', 'mark build fix escalated: ' + e.message); }
|
|
2369
|
-
log('warn', `PR ${pr.id}: build fix escalated after ${pr.buildFixAttempts} attempts — suspending auto-dispatch`);
|
|
2370
|
-
}
|
|
2371
|
-
continue;
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
2344
|
const key = `build-fix-${project?.name || 'default'}-${prDisplayId}`;
|
|
2375
2345
|
if (fixThrottled || isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2376
2346
|
|
|
@@ -2423,10 +2393,11 @@ async function discoverFromPrs(config, project) {
|
|
|
2423
2393
|
const prBranch = ensurePrBranchForDispatch(project, pr, 'build-fix');
|
|
2424
2394
|
if (!prBranch) continue;
|
|
2425
2395
|
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2396
|
+
const reviewNote = [
|
|
2397
|
+
`Build is failing: ${pr.buildFailReason || 'check CI pipeline for details'}.`,
|
|
2398
|
+
'Inspect the live PR checks/build logs yourself, decide the root cause, fix it, run the relevant local validation, and push.',
|
|
2399
|
+
pr.url ? `PR URL: ${pr.url}` : '',
|
|
2400
|
+
].filter(Boolean).join('\n');
|
|
2430
2401
|
|
|
2431
2402
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2432
2403
|
pr_id: pr.id, pr_branch: prBranch,
|
|
@@ -2434,17 +2405,15 @@ async function discoverFromPrs(config, project) {
|
|
|
2434
2405
|
}, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2435
2406
|
if (item) {
|
|
2436
2407
|
newWork.push(item); setCooldown(key); fixDispatched = true;
|
|
2437
|
-
// Increment build fix attempts counter
|
|
2438
2408
|
try {
|
|
2439
2409
|
const prPath = projectPrPath(project);
|
|
2440
2410
|
mutatePullRequests(prPath, prs => {
|
|
2441
2411
|
const target = shared.findPrRecord(prs, pr, project);
|
|
2442
2412
|
if (target) {
|
|
2443
|
-
target.buildFixAttempts = (target.buildFixAttempts || 0) + 1;
|
|
2444
2413
|
target._buildFixPushedAt = ts();
|
|
2445
2414
|
}
|
|
2446
2415
|
});
|
|
2447
|
-
} catch (e) { log('warn', '
|
|
2416
|
+
} catch (e) { log('warn', 'mark build fix dispatched: ' + e.message); }
|
|
2448
2417
|
}
|
|
2449
2418
|
|
|
2450
2419
|
if (pr.agent && !pr._buildFailNotified) {
|
|
@@ -2501,7 +2470,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2501
2470
|
if (!prBranch) continue;
|
|
2502
2471
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2503
2472
|
pr_id: pr.id, pr_branch: prBranch,
|
|
2504
|
-
review_note: `This PR has merge conflicts with the target branch.
|
|
2473
|
+
review_note: `This PR has merge conflicts with the target branch. Inspect the live PR and repository history, choose the safest merge/rebase/update strategy, resolve all conflicts, validate the result, and push the branch.`,
|
|
2505
2474
|
}, `Fix merge conflicts on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2506
2475
|
if (item) {
|
|
2507
2476
|
newWork.push(item);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1651",
|
|
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/playbooks/fix.md
CHANGED
|
@@ -80,7 +80,7 @@ Your task is complete when each review finding has either been fixed or answered
|
|
|
80
80
|
|
|
81
81
|
## Completion
|
|
82
82
|
|
|
83
|
-
After finishing,
|
|
83
|
+
After finishing, write the JSON completion report described in the shared rules. Also output this structured completion block as a compatibility fallback:
|
|
84
84
|
|
|
85
85
|
```completion
|
|
86
86
|
status: done | partial | failed
|
|
@@ -91,4 +91,4 @@ failure_class: N/A
|
|
|
91
91
|
pending: <any remaining work, or none>
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
Replace the values with your actual results.
|
|
94
|
+
Replace the values with your actual results.
|
|
@@ -84,7 +84,7 @@ Your task is complete when the requested implementation is delivered, the valida
|
|
|
84
84
|
|
|
85
85
|
## Completion
|
|
86
86
|
|
|
87
|
-
After finishing,
|
|
87
|
+
After finishing, write the JSON completion report described in the shared rules. Also output this structured completion block as a compatibility fallback:
|
|
88
88
|
|
|
89
89
|
```completion
|
|
90
90
|
status: done | partial | failed
|
|
@@ -95,4 +95,4 @@ failure_class: N/A
|
|
|
95
95
|
pending: <any remaining work, or none>
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
Replace the values with your actual results.
|
|
98
|
+
Replace the values with your actual results.
|
package/playbooks/review.md
CHANGED
|
@@ -42,7 +42,7 @@ Use subagents only for genuinely parallel, independent tasks (e.g., reviewing un
|
|
|
42
42
|
|
|
43
43
|
## Post Review — Submit your verdict
|
|
44
44
|
|
|
45
|
-
You MUST post a review comment with a clear verdict
|
|
45
|
+
You MUST post a review comment with a clear verdict and write the completion report described in the shared rules. The verdict in the report is the primary machine-readable signal; the verdict line in the PR comment is for humans and backward compatibility.
|
|
46
46
|
|
|
47
47
|
### Post your review with verdict
|
|
48
48
|
|
|
@@ -65,7 +65,6 @@ If you encounter merge conflicts (e.g., the PR shows conflicts):
|
|
|
65
65
|
|
|
66
66
|
## When to Stop
|
|
67
67
|
|
|
68
|
-
Your task is complete when your review comment (with `VERDICT: APPROVE` or `VERDICT: REQUEST_CHANGES` on the first line) has been posted successfully
|
|
68
|
+
Your task is complete when your review comment (with `VERDICT: APPROVE` or `VERDICT: REQUEST_CHANGES` on the first line) has been posted successfully and the completion report has `verdict: "approved"` or `verdict: "changes-requested"`.
|
|
69
69
|
|
|
70
70
|
Do NOT stop before posting the review. Do NOT continue reading unrelated files after posting.
|
|
71
|
-
|
|
@@ -14,7 +14,7 @@ Treat a Minions assignment like the user typed the same task directly into a cap
|
|
|
14
14
|
- Use judgment to choose the smallest reliable workflow that fully satisfies the task.
|
|
15
15
|
- Read only the context needed to make correct decisions; do not perform broad archaeology unless the task requires it.
|
|
16
16
|
- Validate with the repo's own documented commands and acceptance criteria. If full validation is impossible or pre-existing failures block it, explain that precisely instead of inventing a green result.
|
|
17
|
-
- Prefer direct work over ceremony. Branches, PRs, inbox notes, completion blocks, and status comments exist for traceability; they should not change what "done" means for the user.
|
|
17
|
+
- Prefer direct work over ceremony. Branches, PRs, inbox notes, completion reports/blocks, and status comments exist for traceability; they should not change what "done" means for the user.
|
|
18
18
|
- Safety and observability rules still win: stay in the engine-created worktree, do not self-merge, do not edit engine-managed status files, do not hide failures, and leave enough evidence for the human and engine to track the result.
|
|
19
19
|
|
|
20
20
|
## Engine Rules (apply to all tasks)
|
|
@@ -50,6 +50,16 @@ Treat a Minions assignment like the user typed the same task directly into a cap
|
|
|
50
50
|
Do **not** create a skill for one-off bug fixes, isolated command outputs, obvious repo facts, or anything already covered by existing docs/playbooks/skills.
|
|
51
51
|
- Do TDD where it makes sense — write failing tests first, then implement, then verify tests pass. Especially for bug fixes (write a test that reproduces the bug) and new utility functions.
|
|
52
52
|
|
|
53
|
+
## Completion Reports
|
|
54
|
+
|
|
55
|
+
The engine provides a completion report path in the prompt and in `MINIONS_COMPLETION_REPORT`. Before exiting, write JSON there with the actual outcome:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{"status":"success","summary":"what changed and how it was validated","verdict":null,"pr":"PR id/url or N/A","failure_class":"N/A","retryable":false,"needs_rerun":false}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use `status: "failed"` plus an accurate `failure_class`, `retryable`, and `needs_rerun` when the task could not be completed. For PR reviews, set `verdict` to `approved` or `changes-requested`. Fenced `completion` blocks are still accepted as a fallback, but the JSON report is the primary signal.
|
|
62
|
+
|
|
53
63
|
## Long-Running Commands
|
|
54
64
|
|
|
55
65
|
Builds, dependency installs, tests, and local servers can be quiet for long periods. Run the repo's normal CLI commands and let them finish; do not add artificial progress output, heartbeat loops, or command-specific workarounds just to keep Minions active.
|
|
@@ -61,7 +71,7 @@ When asked to check build status, CI results, or review state for a PR:
|
|
|
61
71
|
**Preferred — read cached state (refreshed every `prPollStatusEvery` ticks, default ~12 min when engine is running):**
|
|
62
72
|
Find the PR in `projects/<project-name>/pull-requests.json` by `prNumber`. Key fields:
|
|
63
73
|
- `buildStatus` — `passing` | `failing` | `running` | `none`
|
|
64
|
-
- `
|
|
74
|
+
- `buildFailReason` — failing check/pipeline name when `buildStatus` is `failing`; inspect live CI logs yourself for details
|
|
65
75
|
- `reviewStatus` — `approved` | `changes-requested` | `waiting` | `pending`
|
|
66
76
|
- `status` — `active` | `merged` | `abandoned`
|
|
67
77
|
- `url` — link to the PR in ADO
|