@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.
@@ -31,6 +31,7 @@ const fs = require('fs');
31
31
  const https = require('https');
32
32
  const path = require('path');
33
33
  const { execSync } = require('child_process');
34
+ const { FAILURE_CLASS, safeWrite, ts } = require('../shared');
34
35
 
35
36
  const ENGINE_DIR = __dirname.replace(/[\\/]runtimes$/, '');
36
37
  const isWin = process.platform === 'win32';
@@ -254,6 +255,88 @@ function buildArgs(opts = {}) {
254
255
  return args;
255
256
  }
256
257
 
258
+ function buildSpawnFlags(opts = {}) {
259
+ const flags = ['--runtime', 'copilot'];
260
+ if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
261
+ if (opts.model) flags.push('--model', String(opts.model));
262
+ if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
263
+ if (module.exports.capabilities.effortLevels && opts.effort) flags.push('--effort', String(opts.effort));
264
+ if (module.exports.capabilities.sessionResume && opts.sessionId) flags.push('--resume', String(opts.sessionId));
265
+ if (module.exports.capabilities.budgetCap && opts.maxBudget != null) flags.push('--max-budget-usd', String(opts.maxBudget));
266
+ if (module.exports.capabilities.bareMode && opts.bare === true) flags.push('--bare');
267
+ if (module.exports.capabilities.fallbackModel && opts.fallbackModel) flags.push('--fallback-model', String(opts.fallbackModel));
268
+ if (opts.stream != null && opts.stream !== '') flags.push('--stream', String(opts.stream));
269
+ if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
270
+ if (opts.suppressAgentsMd === true) flags.push('--no-custom-instructions');
271
+ if (opts.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
272
+ return flags;
273
+ }
274
+
275
+ function getResumeSessionId({ agentId, branchName, agentsDir, maxAgeMs = 2 * 60 * 60 * 1000, logger = console } = {}) {
276
+ if (!agentId || agentId.startsWith('temp-') || !agentsDir) return null;
277
+ try {
278
+ const sessionPath = path.join(agentsDir, agentId, 'session.json');
279
+ const sessionFile = _safeJson(sessionPath);
280
+ if (!sessionFile?.sessionId || !sessionFile.savedAt) return null;
281
+ const sessionAge = Date.now() - new Date(sessionFile.savedAt).getTime();
282
+ const sameBranch = branchName && sessionFile.branch && sessionFile.branch === branchName;
283
+ if (sessionAge < maxAgeMs && sameBranch) {
284
+ if (logger && typeof logger.info === 'function') {
285
+ logger.info(`Resuming session ${sessionFile.sessionId} for ${agentId} on branch ${branchName} (age: ${Math.round(sessionAge / 60000)}min)`);
286
+ }
287
+ return sessionFile.sessionId;
288
+ }
289
+ } catch (e) {
290
+ if (logger && typeof logger.warn === 'function') logger.warn('session resume lookup: ' + e.message);
291
+ }
292
+ return null;
293
+ }
294
+
295
+ function saveSession({ agentId, dispatchId, branch, sessionId, agentsDir, now = ts, writeJson = safeWrite, logger = console } = {}) {
296
+ if (!sessionId || !agentId || agentId.startsWith('temp-') || !agentsDir) return false;
297
+ try {
298
+ writeJson(path.join(agentsDir, agentId, 'session.json'), {
299
+ sessionId,
300
+ dispatchId,
301
+ savedAt: typeof now === 'function' ? now() : new Date().toISOString(),
302
+ branch: branch || null,
303
+ });
304
+ return true;
305
+ } catch (err) {
306
+ if (logger && typeof logger.warn === 'function') logger.warn(`Session save: ${err.message}`);
307
+ return false;
308
+ }
309
+ }
310
+
311
+ function detectPermissionGate() {
312
+ return false;
313
+ }
314
+
315
+ function getPromptDeliveryMode() {
316
+ return 'stdin';
317
+ }
318
+
319
+ function usesSystemPromptFile() {
320
+ return false;
321
+ }
322
+
323
+ function _runtimeFailureClass(code) {
324
+ if (code === 'auth-failure' || code === 'budget-exceeded') return FAILURE_CLASS.PERMISSION_BLOCKED;
325
+ if (code === 'unknown-model') return FAILURE_CLASS.CONFIG_ERROR;
326
+ if (code === 'rate-limit') return FAILURE_CLASS.NETWORK_ERROR;
327
+ if (code === 'crash') return FAILURE_CLASS.SPAWN_ERROR;
328
+ return null;
329
+ }
330
+
331
+ function classifyFailure({ code, stdout = '', stderr = '', fallback } = {}) {
332
+ if (code === 78) return { failureClass: FAILURE_CLASS.CONFIG_ERROR, retryable: false, message: 'Copilot configuration error' };
333
+ const parsed = parseError(`${stdout || ''}\n${stderr || ''}`);
334
+ const runtimeClass = parsed.code ? _runtimeFailureClass(parsed.code) : null;
335
+ if (runtimeClass) return { failureClass: runtimeClass, retryable: parsed.retriable !== false, message: parsed.message || '' };
336
+ const fallbackClass = typeof fallback === 'function' ? fallback(code, stdout, stderr) : FAILURE_CLASS.UNKNOWN;
337
+ return { failureClass: fallbackClass, retryable: parsed.retriable !== false, message: parsed.message || '' };
338
+ }
339
+
257
340
  // ── Prompt Construction ─────────────────────────────────────────────────────
258
341
  //
259
342
  // Copilot has no --system-prompt-file flag, so we deliver the system prompt
@@ -681,8 +764,15 @@ module.exports = {
681
764
  // Use the same wrapper as Claude — spawn-agent.js is runtime-agnostic per P-9c4f2d6a
682
765
  spawnScript: path.join(ENGINE_DIR, 'spawn-agent.js'),
683
766
  installHint: INSTALL_HINT,
767
+ buildSpawnFlags,
684
768
  buildArgs,
685
769
  buildPrompt,
770
+ getResumeSessionId,
771
+ saveSession,
772
+ detectPermissionGate,
773
+ getPromptDeliveryMode,
774
+ usesSystemPromptFile,
775
+ classifyFailure,
686
776
  resolveModel,
687
777
  parseOutput,
688
778
  parseStreamChunk,
package/engine/shared.js CHANGED
@@ -230,6 +230,12 @@ function dispatchPromptSidecarPath(dispatchId) {
230
230
  return path.join(_promptContextsDir(), `${safeId}.md`);
231
231
  }
232
232
 
233
+ function dispatchCompletionReportPath(dispatchId) {
234
+ if (!dispatchId) return null;
235
+ const safeId = String(dispatchId).replace(/[^a-zA-Z0-9._-]/g, '-');
236
+ return path.join(MINIONS_DIR, 'engine', 'completions', `${safeId}.json`);
237
+ }
238
+
233
239
  /**
234
240
  * If the dispatch item's prompt exceeds thresholdBytes, write the full prompt
235
241
  * to engine/contexts/<id>.md and replace `item.prompt` with a short stub
@@ -716,7 +722,7 @@ const ENGINE_DEFAULTS = {
716
722
  autoFixBuilds: true, // auto-dispatch fix agents when a PR build fails
717
723
  meetingRoundTimeout: 900000, // 15min per meeting round before auto-advance
718
724
  evalLoop: true, // enable review→fix loop after implementation completes
719
- evalMaxIterations: 3, // max review→fix cycles before escalating to human
725
+ evalMaxIterations: 3, // legacy UI/config field; engine discovery no longer enforces review→fix cycle caps
720
726
  evalMaxCost: null, // USD ceiling per work item across all eval iterations; null = no limit (gather baseline data first)
721
727
  maxRetries: 3, // max dispatch retries before marking work item as failed
722
728
  minRetryGapMs: 120000, // 2min — minimum gap between retry dispatches for the same work item; prevents tight retry loops when an idempotent agent (e.g. review bailing out on a duplicate) cannot produce the expected output (#1770)
@@ -727,7 +733,7 @@ const ENGINE_DEFAULTS = {
727
733
  logBufferSize: 50, // flush immediately when buffer exceeds this many entries
728
734
  lockRetries: 0, // no retries — single 5s timeout window with 25ms polling (200 attempts) is sufficient; stale lock recovery at 60s handles crashes
729
735
  lockRetryBackoffMs: 500, // base backoff between lock retries (doubles each attempt: 500ms, 1s, 2s, ...)
730
- maxBuildFixAttempts: 3, // max consecutive auto-fix dispatch cycles per PR before escalation to human
736
+ maxBuildFixAttempts: 3, // legacy UI/config field; engine discovery no longer enforces build-fix attempt caps
731
737
  buildFixGracePeriod: 600000, // 10min — wait for CI to run after build fix before re-dispatching
732
738
  adoPollEnabled: true, // poll ADO PR status, comments, and reconciliation on each tick cycle
733
739
  ghPollEnabled: true, // poll GitHub PR status, comments, and reconciliation on each tick cycle
@@ -1171,7 +1177,7 @@ const ESCALATION_POLICY = {
1171
1177
  };
1172
1178
 
1173
1179
  // Structured completion protocol — fields agents must produce in ```completion blocks
1174
- const COMPLETION_FIELDS = ['status', 'files_changed', 'tests', 'pr', 'pending', 'failure_class'];
1180
+ const COMPLETION_FIELDS = ['status', 'summary', 'files_changed', 'tests', 'pr', 'pending', 'failure_class', 'retryable', 'needs_rerun', 'verdict'];
1175
1181
 
1176
1182
  const DEFAULT_AGENT_METRICS = {
1177
1183
  tasksCompleted: 0, tasksErrored: 0,
@@ -1778,6 +1784,39 @@ function findPrRecord(prs, prRef, project = null) {
1778
1784
  return numberMatches.length === 1 ? numberMatches[0] : null;
1779
1785
  }
1780
1786
 
1787
+ function snapshotPrRecord(pr) {
1788
+ if (pr === undefined) return undefined;
1789
+ return JSON.parse(JSON.stringify(pr));
1790
+ }
1791
+
1792
+ function _jsonEqual(a, b) {
1793
+ return JSON.stringify(a) === JSON.stringify(b);
1794
+ }
1795
+
1796
+ function _isPlainObject(value) {
1797
+ return !!value && typeof value === 'object' && !Array.isArray(value);
1798
+ }
1799
+
1800
+ function applyPrFieldDelta(target, before, after) {
1801
+ if (!target || typeof target !== 'object' || !after || typeof after !== 'object') return target;
1802
+ before = before && typeof before === 'object' ? before : {};
1803
+ const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
1804
+ for (const key of keys) {
1805
+ const beforeValue = before[key];
1806
+ const afterHas = Object.prototype.hasOwnProperty.call(after, key);
1807
+ const afterValue = after[key];
1808
+ if (_jsonEqual(beforeValue, afterValue)) continue;
1809
+ if (!afterHas) {
1810
+ delete target[key];
1811
+ } else if (_isPlainObject(beforeValue) && _isPlainObject(afterValue) && _isPlainObject(target[key])) {
1812
+ applyPrFieldDelta(target[key], beforeValue, afterValue);
1813
+ } else {
1814
+ target[key] = snapshotPrRecord(afterValue);
1815
+ }
1816
+ }
1817
+ return target;
1818
+ }
1819
+
1781
1820
  function normalizePrRecord(pr, project = null) {
1782
1821
  if (!pr || typeof pr !== 'object') return false;
1783
1822
  let changed = false;
@@ -2271,6 +2310,7 @@ module.exports = {
2271
2310
  safeUnlink,
2272
2311
  PROMPT_CONTEXTS_DIR,
2273
2312
  dispatchPromptSidecarPath,
2313
+ dispatchCompletionReportPath,
2274
2314
  sidecarDispatchPrompt,
2275
2315
  resolveDispatchPrompt,
2276
2316
  deleteDispatchPromptSidecar,
@@ -2325,6 +2365,8 @@ module.exports = {
2325
2365
  isPrCompatibleWithProject,
2326
2366
  getCanonicalPrId,
2327
2367
  findPrRecord,
2368
+ snapshotPrRecord,
2369
+ applyPrFieldDelta,
2328
2370
  normalizePrRecord,
2329
2371
  normalizePrRecords,
2330
2372
  upsertPullRequestRecord,
@@ -94,7 +94,7 @@ function parseSpawnArgs(argv) {
94
94
  * Returns:
95
95
  * {
96
96
  * bin, leadingArgs, args, // → spawn(execPath OR bin, [bin?, ...leadingArgs, ...args])
97
- * deliveryMode, // 'stdin' | 'arg' (per runtime.capabilities.promptViaArg)
97
+ * deliveryMode, // 'stdin' | 'arg' (runtime adapter decision)
98
98
  * finalPrompt, // adapter-built prompt text (may include sysprompt for some runtimes)
99
99
  * usingNodeShim, // true → runtime returned a non-native binary (Claude cli.js)
100
100
  * }
@@ -103,9 +103,10 @@ function buildSpawnInvocation({ runtime, resolved, promptText, sysPromptText, op
103
103
  const finalPrompt = runtime.buildPrompt(promptText, sysPromptText);
104
104
  const adapterOpts = { ...opts };
105
105
  if (Array.isArray(addDirs) && addDirs.length) adapterOpts.addDirs = addDirs;
106
- // When the adapter delivers the prompt via argv, hand it the prompt so it
107
- // can splice `--prompt <text>` into its args.
108
- if (runtime.capabilities && runtime.capabilities.promptViaArg) {
106
+ const deliveryMode = typeof runtime.getPromptDeliveryMode === 'function'
107
+ ? runtime.getPromptDeliveryMode(adapterOpts)
108
+ : (runtime.capabilities && runtime.capabilities.promptViaArg ? 'arg' : 'stdin');
109
+ if (deliveryMode === 'arg') {
109
110
  adapterOpts.prompt = finalPrompt;
110
111
  }
111
112
  const adapterArgs = runtime.buildArgs(adapterOpts);
@@ -115,7 +116,7 @@ function buildSpawnInvocation({ runtime, resolved, promptText, sysPromptText, op
115
116
  native,
116
117
  leadingArgs,
117
118
  args: [...adapterArgs, ...(passthrough || [])],
118
- deliveryMode: runtime.capabilities && runtime.capabilities.promptViaArg ? 'arg' : 'stdin',
119
+ deliveryMode,
119
120
  finalPrompt,
120
121
  usingNodeShim: !native,
121
122
  };
@@ -159,7 +160,9 @@ function main() {
159
160
  // already merged inside `runtime.buildPrompt(prompt, sys)`.
160
161
  const isResume = opts.sessionId != null;
161
162
  const sysTmpPath = sysPromptFile + '.tmp';
162
- const wantsSystemPromptFile = !isResume && runtime.capabilities && runtime.capabilities.systemPromptFile;
163
+ const wantsSystemPromptFile = typeof runtime.usesSystemPromptFile === 'function'
164
+ ? runtime.usesSystemPromptFile({ isResume, opts })
165
+ : (!isResume && runtime.capabilities && runtime.capabilities.systemPromptFile);
163
166
  if (wantsSystemPromptFile) {
164
167
  fs.writeFileSync(sysTmpPath, sysPromptText);
165
168
  opts.sysPromptFile = sysTmpPath;
package/engine/timeout.js CHANGED
@@ -147,7 +147,7 @@ function checkTimeouts(config) {
147
147
  const engineRestartGraceUntil = engine().engineRestartGraceUntil;
148
148
  const engineRestartGraceExempt = engine().engineRestartGraceExempt;
149
149
  const { completeDispatch } = dispatch();
150
- const { runPostCompletionHooks } = require('./lifecycle');
150
+ const { runPostCompletionHooks, parseAgentOutput, parseStructuredCompletion, detectNonTerminalResultSummary } = require('./lifecycle');
151
151
 
152
152
  const timeout = config.engine?.agentTimeout || ENGINE_DEFAULTS.agentTimeout;
153
153
  const defaultStaleOrphanTimeout = config.engine?.heartbeatTimeout || ENGINE_DEFAULTS.heartbeatTimeout;
@@ -257,12 +257,25 @@ function checkTimeouts(config) {
257
257
  safeWrite(outputLogPath, `# Output for dispatch ${item.id}\n# Exit code: ${processExitCode}\n# Completed: ${ts()}\n# Detected via output scan\n\n## Result\n${text || '(no text)'}\n`);
258
258
  } catch (e) { log('warn', 'parse output result: ' + e.message); }
259
259
 
260
- completeDispatch(item.id, isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR,
261
- isSuccess ? 'Completed (detected from output)' : `Exited with code ${processExitCode} (detected from output)`);
260
+ const fullLogForHooks = safeRead(liveLogPath) || liveLogTail;
261
+ let completionDetection = null;
262
+ let outputResultSummary = '';
263
+ try {
264
+ const runtimeName = item.meta?.runtimeName || item.runtimeName || 'claude';
265
+ outputResultSummary = parseAgentOutput(fullLogForHooks, runtimeName).resultSummary || '';
266
+ const gateSummary = outputResultSummary || (!fullLogForHooks.includes('"type":') ? fullLogForHooks : '');
267
+ completionDetection = isSuccess
268
+ ? detectNonTerminalResultSummary(gateSummary, parseStructuredCompletion(fullLogForHooks, runtimeName))
269
+ : null;
270
+ } catch (e) { log('warn', 'completion summary gate: ' + e.message); }
271
+
272
+ completeDispatch(item.id, completionDetection ? DISPATCH_RESULT.ERROR : (isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR),
273
+ completionDetection ? completionDetection.reason : (isSuccess ? 'Completed (detected from output)' : `Exited with code ${processExitCode} (detected from output)`),
274
+ outputResultSummary,
275
+ completionDetection ? { processWorkItemFailure: false } : {});
262
276
 
263
277
  // Run post-completion hooks via shared helper (async — fire and forget in timeout context).
264
278
  // Pass the actual exit code so autoRecovery (PR-created-but-failed) still works correctly.
265
- const fullLogForHooks = safeRead(liveLogPath) || liveLogTail;
266
279
  runPostCompletionHooks(item, item.agent, processExitCode, fullLogForHooks, config).catch(e => log('warn', 'post-completion hooks: ' + e.message));
267
280
 
268
281
  if (hasProcess) {