@yemi33/minions 0.1.1650 → 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/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
- const _FAST_WORK_TYPES = new Set([WORK_TYPE.EXPLORE, WORK_TYPE.ASK, WORK_TYPE.REVIEW]);
236
- const _MAX_TURNS_BY_TYPE = {
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
- const globalOverride = engineConfig.maxTurns && engineConfig.maxTurns !== ENGINE_DEFAULTS.maxTurns ? engineConfig.maxTurns : null;
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 fullTaskPrompt = agentContext
456
- ? `## Agent Context\n\n${agentContext}\n---\n\n## Your Task\n\n${taskPromptWithSteering}`
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
- // Effort: use 'low' for fast work types unless configured otherwise.
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.capabilities.sessionResume && !agentId.startsWith('temp-')) {
903
- try {
904
- const sessionFile = safeJson(path.join(AGENTS_DIR, agentId, 'session.json'));
905
- if (sessionFile?.sessionId && sessionFile.savedAt) {
906
- const sessionAge = Date.now() - new Date(sessionFile.savedAt).getTime();
907
- const sameBranch = branchName && sessionFile.branch && sessionFile.branch === branchName;
908
- if (sessionAge < 2 * 60 * 60 * 1000 && sameBranch) {
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 lower = chunk.toLowerCase();
1029
- if (/\b(trust this|do you trust|allow access|grant permission|approve tools?|permission prompt)\b/.test(lower)) {
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
- safeWrite(path.join(AGENTS_DIR, agentId, 'session.json'), {
1048
- sessionId: obj.session_id, dispatchId: id, savedAt: ts(), branch: branchName
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 failureClass = code !== 0 ? classifyFailure(code, stdout, stderr) : undefined;
1283
+ const failureInfo = code !== 0 ? _classifyAgentFailure(runtime, code, stdout, stderr) : {};
1284
+ const failureClass = failureInfo.failureClass;
1288
1285
 
1289
- // Detect configuration errors (e.g. Claude CLI not found) — fail immediately with clear message
1286
+ // Detect configuration errors (e.g. missing runtime CLI) — fail immediately with clear message
1290
1287
  if (code === 78) {
1291
- const errMsg = stderr.includes('claude-code') ? stderr.trim() : 'Configuration error — Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code';
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 effectiveResult = completionContractFailure ? DISPATCH_RESULT.ERROR : ((code === 0 || autoRecovered) ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
1306
- const completeOpts = completionContractFailure
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 && failureClass ? { failureClass } : {});
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 (completionContractFailure) {
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 && !evalEscalated;
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 && !evalEscalated && !fixDispatched;
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 && !evalEscalated && !fixDispatched) {
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
- let reviewNote = `Build is failing: ${pr.buildFailReason || 'Check CI pipeline for details'}. Fix the build errors and push.`;
2427
- if (pr.buildErrorLog) {
2428
- reviewNote += `\n\n## Build Error Log\n\n\`\`\`\n${pr.buildErrorLog}\n\`\`\``;
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', 'increment build fix attempts: ' + e.message); }
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. Resolve the conflicts:\n\n1. Pull latest from main/master\n2. Resolve all conflicts (prefer PR branch changes unless main has critical fixes)\n3. Build and test after resolving\n4. Push the resolved 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.1650",
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, output a structured completion block so the engine can parse your results:
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. This block MUST appear in your final output.
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, output a structured completion block so the engine can parse your results:
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. This block MUST appear in your final output.
98
+ Replace the values with your actual results.
@@ -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. The engine parses your verdict to update PR status **a review without a verdict line is incomplete and will be retried.**
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
- - `buildErrorLog` — compiler/pipeline errors when `buildStatus` is `failing`
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