@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
|
@@ -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, //
|
|
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, //
|
|
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,
|
package/engine/spawn-agent.js
CHANGED
|
@@ -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' (
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
261
|
-
|
|
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) {
|