@yemi33/minions 0.1.2382 → 0.1.2383
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/bin/minions.js +1 -0
- package/dashboard.js +26 -12
- package/docs/completion-reports.md +20 -1
- package/docs/runtime-adapters.md +54 -22
- package/docs/skills.md +2 -0
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +14 -0
- package/engine/acp-transport.js +63 -35
- package/engine/ado-comment.js +3 -1
- package/engine/agent-worker-pool.js +11 -4
- package/engine/cc-worker-pool.js +4 -2
- package/engine/cli.js +9 -1
- package/engine/comment-format.js +51 -14
- package/engine/dispatch.js +7 -2
- package/engine/execution-model.js +68 -0
- package/engine/gh-comment.js +6 -3
- package/engine/github.js +6 -7
- package/engine/lifecycle.js +125 -44
- package/engine/llm.js +59 -83
- package/engine/memory-store.js +3 -1
- package/engine/playbook.js +4 -6
- package/engine/pooled-agent-process.js +28 -26
- package/engine/runtimes/claude.js +94 -25
- package/engine/runtimes/codex.js +1 -0
- package/engine/runtimes/contract.js +239 -0
- package/engine/runtimes/copilot.js +78 -8
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +95 -21
- package/engine/spawn-agent.js +79 -113
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine.js +183 -109
- package/package.json +1 -1
- package/playbooks/fix.md +20 -0
- package/playbooks/review.md +2 -0
- package/skills/check-self-authored-review-comment/SKILL.md +34 -0
package/engine/lifecycle.js
CHANGED
|
@@ -17,6 +17,7 @@ const harness = require('./harness');
|
|
|
17
17
|
const smallStateStore = require('./small-state-store');
|
|
18
18
|
const prdStore = require('./prd-store');
|
|
19
19
|
const { isBranchActive } = require('./cooldown');
|
|
20
|
+
const { resolveExecutionModel } = require('./execution-model');
|
|
20
21
|
const { worktreeMatchesBranch, getWorktreeBranch, cleanupMergedPrLocalBranch } = require('./cleanup');
|
|
21
22
|
const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
|
|
22
23
|
MINIONS_DIR, ENGINE_DIR, PLANS_DIR, INBOX_DIR, AGENTS_DIR } = queries;
|
|
@@ -2319,7 +2320,7 @@ function resolveReviewPrContext(pr, project, config, structuredCompletion = null
|
|
|
2319
2320
|
: null;
|
|
2320
2321
|
}
|
|
2321
2322
|
|
|
2322
|
-
async function updatePrAfterReview(agentId, pr, project, config, resultSummary, structuredCompletion = null, dispatchItem = null) {
|
|
2323
|
+
async function updatePrAfterReview(agentId, pr, project, config, resultSummary, structuredCompletion = null, dispatchItem = null, executionMetadata = null) {
|
|
2323
2324
|
|
|
2324
2325
|
if (!config) config = getConfig();
|
|
2325
2326
|
const completionStatus = normalizeCompletionStatus(structuredCompletion?.status);
|
|
@@ -2338,6 +2339,11 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2338
2339
|
const reviewProject = reviewContext.project;
|
|
2339
2340
|
const prScope = reviewContext.scope;
|
|
2340
2341
|
const reviewerName = config.agents?.[agentId]?.name || agentId;
|
|
2342
|
+
const reviewExecutionModel = resolveExecutionModel({
|
|
2343
|
+
reportedModel: executionMetadata?.reportedModel,
|
|
2344
|
+
capturedModel: dispatchItem?.executionModel,
|
|
2345
|
+
requestedModel: dispatchItem?.requestedModel,
|
|
2346
|
+
});
|
|
2341
2347
|
|
|
2342
2348
|
// Check actual review status from the platform (agent may have approved or requested changes)
|
|
2343
2349
|
// If platform hasn't propagated the vote yet (returns 'pending'), keep current status unchanged.
|
|
@@ -2549,6 +2555,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2549
2555
|
reviewer: reviewerName,
|
|
2550
2556
|
reviewedAt: ts(),
|
|
2551
2557
|
note: reviewNote,
|
|
2558
|
+
model: reviewExecutionModel.model,
|
|
2552
2559
|
dispatchId: dispatchItem?.id || structuredCompletion?.dispatchId || null,
|
|
2553
2560
|
sourceItem: dispatchItem?.meta?.item?.id || null,
|
|
2554
2561
|
...(reviewThreads.length > 0 ? { threads: reviewThreads } : {}),
|
|
@@ -3134,7 +3141,6 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
3134
3141
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
3135
3142
|
options = { automationCauseKey: options, dispatchId: legacyDispatchId };
|
|
3136
3143
|
}
|
|
3137
|
-
const explicitlyChangedBranch = options.branchChanged !== false;
|
|
3138
3144
|
const prScope = project || 'central';
|
|
3139
3145
|
const automationCauseKey = options.automationCauseKey || options.dispatchItem?.meta?.automationCauseKey || '';
|
|
3140
3146
|
const fixDispatchId = options.dispatchItem?.id || options.dispatchId || legacyDispatchId || '';
|
|
@@ -3143,6 +3149,15 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
3143
3149
|
source,
|
|
3144
3150
|
task: options.dispatchItem?.task,
|
|
3145
3151
|
});
|
|
3152
|
+
const evidenceOnlyReviewResolution = cause === shared.PR_FIX_CAUSE.REVIEW_FEEDBACK
|
|
3153
|
+
&& options.reviewFindingResolution;
|
|
3154
|
+
// Review-fix completion is governed by the live branch probe. Preserve the
|
|
3155
|
+
// legacy files_changed behavior for unrelated fix causes.
|
|
3156
|
+
let explicitlyChangedBranch = options.branchChanged !== false;
|
|
3157
|
+
if (cause === shared.PR_FIX_CAUSE.REVIEW_FEEDBACK && options.branchChange) {
|
|
3158
|
+
explicitlyChangedBranch = true;
|
|
3159
|
+
}
|
|
3160
|
+
if (evidenceOnlyReviewResolution) explicitlyChangedBranch = false;
|
|
3146
3161
|
let result = null;
|
|
3147
3162
|
shared.mutatePullRequests(prScope, (prs) => {
|
|
3148
3163
|
if (!Array.isArray(prs)) return prs;
|
|
@@ -4569,8 +4584,8 @@ function parseCompletionBoolean(value) {
|
|
|
4569
4584
|
}
|
|
4570
4585
|
|
|
4571
4586
|
// Detect a deliberate no-op completion — the agent correctly declined to make
|
|
4572
|
-
// changes (work was already shipped, dispatch premise was wrong,
|
|
4573
|
-
//
|
|
4587
|
+
// changes (work was already shipped, dispatch premise was wrong, or a finding
|
|
4588
|
+
// was proven resolved/invalid with current-code evidence) and should NOT be flagged as a
|
|
4574
4589
|
// silent failure for missing a PR. Honored signals:
|
|
4575
4590
|
// - completion.noop === true (canonical, primary)
|
|
4576
4591
|
// - completion.result === 'noop' OR completion.result.type === 'noop'
|
|
@@ -4651,6 +4666,39 @@ function classifyBenignZeroTargetOutcome(dispatchItem, completion, resultSummary
|
|
|
4651
4666
|
return noMutation ? evidence : null;
|
|
4652
4667
|
}
|
|
4653
4668
|
|
|
4669
|
+
function validateReviewFixCompletion({ type, meta, branchChange, structuredCompletion } = {}) {
|
|
4670
|
+
if (!shared.isFixLikeWorkType(type)) return { valid: true };
|
|
4671
|
+
const cause = shared.getPrFixAutomationCause({
|
|
4672
|
+
dispatchKey: meta?.dispatchKey,
|
|
4673
|
+
source: meta?.source,
|
|
4674
|
+
task: meta?.item?.title,
|
|
4675
|
+
});
|
|
4676
|
+
if (cause !== shared.PR_FIX_CAUSE.REVIEW_FEEDBACK
|
|
4677
|
+
|| (branchChange?.changed === true && branchChange.evidence === 'remote-head')) {
|
|
4678
|
+
return { valid: true };
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
const findingId = shared.prReviewFindingIdentity(meta?.pr);
|
|
4682
|
+
const resolution = structuredCompletion?.reviewFindingResolution;
|
|
4683
|
+
const disposition = String(resolution?.disposition || '').trim().toLowerCase();
|
|
4684
|
+
const reportedFindingId = String(resolution?.findingId || '').trim();
|
|
4685
|
+
const currentCodeEvidence = String(resolution?.currentCodeEvidence || '').trim();
|
|
4686
|
+
const hasCurrentCodeReference = /(?:[a-z]:[\\/])?[\w./\\-]+\.[a-z0-9]+:\d+\b/i.test(currentCodeEvidence)
|
|
4687
|
+
|| /\bcommit\s+[0-9a-f]{7,40}\b/i.test(currentCodeEvidence);
|
|
4688
|
+
const validDisposition = disposition === 'already-resolved' || disposition === 'invalid';
|
|
4689
|
+
const explicitNoop = !!parseCompletionNoop(structuredCompletion);
|
|
4690
|
+
|
|
4691
|
+
if (explicitNoop && validDisposition && reportedFindingId === findingId && hasCurrentCodeReference) {
|
|
4692
|
+
return { valid: true, findingId, resolution };
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4695
|
+
return {
|
|
4696
|
+
valid: false,
|
|
4697
|
+
findingId,
|
|
4698
|
+
reason: `Review-fix completion did not advance the PR branch and must explicitly prove the exact finding is already resolved or invalid with current-code evidence. Expected reviewFindingResolution.findingId="${findingId}", disposition "already-resolved" or "invalid", and a file:line or commit reference. Shared platform ownership, viewerDidAuthor, or reviewer/fixer agent equality is not evidence.`,
|
|
4699
|
+
};
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4654
4702
|
function normalizeReviewVerdict(verdict) {
|
|
4655
4703
|
const value = String(verdict || '').trim().toLowerCase().replace(/[\s-]+/g, '_');
|
|
4656
4704
|
if (value === 'approve' || value === 'approved') return REVIEW_STATUS.APPROVED;
|
|
@@ -5807,13 +5855,70 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5807
5855
|
}
|
|
5808
5856
|
|
|
5809
5857
|
// No-op signal: agent declared the work was correctly NOT done (already
|
|
5810
|
-
// shipped, dispatch premise wrong,
|
|
5858
|
+
// shipped, dispatch premise wrong, evidence-backed resolved finding, etc.). Skip the PR
|
|
5811
5859
|
// attachment contract — a missing PR is intentional, not a silent failure.
|
|
5812
|
-
|
|
5860
|
+
let prFixBranchChange = null;
|
|
5861
|
+
let prFixBuildStillFailing = null;
|
|
5862
|
+
if (shared.isFixLikeWorkType(type) && effectiveSuccess && meta?.pr?.id) {
|
|
5863
|
+
try {
|
|
5864
|
+
prFixBranchChange = await detectPrFixBranchChange(meta, config);
|
|
5865
|
+
} catch (err) {
|
|
5866
|
+
log('warn', `PR fix no-op detection for ${meta.pr.id}: ${err.message}`);
|
|
5867
|
+
prFixBranchChange = { changed: null, reason: err.message };
|
|
5868
|
+
}
|
|
5869
|
+
if (prFixBranchChange?.changed === false) {
|
|
5870
|
+
const fixCause = shared.getPrFixAutomationCause({
|
|
5871
|
+
dispatchKey: dispatchItem?.meta?.dispatchKey,
|
|
5872
|
+
source: meta?.source,
|
|
5873
|
+
task: dispatchItem?.task,
|
|
5874
|
+
});
|
|
5875
|
+
if (fixCause === shared.PR_FIX_CAUSE.BUILD_FAILURE) {
|
|
5876
|
+
try {
|
|
5877
|
+
const liveStatus = await checkBuildStillFailingLive(meta.pr, meta.project);
|
|
5878
|
+
if (liveStatus === shared.BUILD_STATUS.FAILING) {
|
|
5879
|
+
prFixBuildStillFailing = liveStatus;
|
|
5880
|
+
}
|
|
5881
|
+
} catch (err) {
|
|
5882
|
+
log('warn', `Live build re-check for ${meta.pr.id}: ${err.message}`);
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
let noopRationale = (effectiveSuccess && !skipDoneStatus)
|
|
5813
5888
|
? (parseCompletionNoop(structuredCompletion) || benignNoop)
|
|
5814
5889
|
: null;
|
|
5890
|
+
let reviewFixResolution = null;
|
|
5891
|
+
if (effectiveSuccess && !skipDoneStatus) {
|
|
5892
|
+
const reviewFixValidation = validateReviewFixCompletion({
|
|
5893
|
+
type,
|
|
5894
|
+
meta,
|
|
5895
|
+
branchChange: prFixBranchChange,
|
|
5896
|
+
structuredCompletion,
|
|
5897
|
+
});
|
|
5898
|
+
if (!reviewFixValidation.valid) {
|
|
5899
|
+
skipDoneStatus = true;
|
|
5900
|
+
meta._agentId = agentId;
|
|
5901
|
+
const detection = {
|
|
5902
|
+
phrase: 'review-fix-resolution-evidence-missing',
|
|
5903
|
+
reason: reviewFixValidation.reason,
|
|
5904
|
+
};
|
|
5905
|
+
const reason = meta?.item?.id
|
|
5906
|
+
? deferNonTerminalCompletion(meta, detection)
|
|
5907
|
+
: detection.reason;
|
|
5908
|
+
completionContractFailure = {
|
|
5909
|
+
reason,
|
|
5910
|
+
itemId: meta?.item?.id || dispatchItem.id,
|
|
5911
|
+
nonTerminal: true,
|
|
5912
|
+
processWorkItemFailure: false,
|
|
5913
|
+
};
|
|
5914
|
+
noopRationale = null;
|
|
5915
|
+
log('warn', `Review-fix completion rejected for ${meta?.item?.id || dispatchItem.id}: ${reviewFixValidation.reason}`);
|
|
5916
|
+
} else {
|
|
5917
|
+
reviewFixResolution = reviewFixValidation.resolution || null;
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5815
5920
|
if (noopRationale) {
|
|
5816
|
-
log('info', `No-op completion for ${meta
|
|
5921
|
+
log('info', `No-op completion for ${meta?.item?.id || dispatchItem.id}: ${noopRationale.slice(0, 200)}`);
|
|
5817
5922
|
}
|
|
5818
5923
|
|
|
5819
5924
|
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus && !noopRationale) {
|
|
@@ -6051,41 +6156,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6051
6156
|
|
|
6052
6157
|
// Archive is manual — user archives plans from the dashboard when ready
|
|
6053
6158
|
|
|
6054
|
-
let prFixBranchChange = null;
|
|
6055
|
-
let prFixBuildStillFailing = null;
|
|
6056
|
-
if (shared.isFixLikeWorkType(type) && effectiveSuccess && meta?.pr?.id) {
|
|
6057
|
-
try {
|
|
6058
|
-
prFixBranchChange = await detectPrFixBranchChange(meta, config);
|
|
6059
|
-
} catch (err) {
|
|
6060
|
-
log('warn', `PR fix no-op detection for ${meta.pr.id}: ${err.message}`);
|
|
6061
|
-
prFixBranchChange = { changed: null, reason: err.message };
|
|
6062
|
-
}
|
|
6063
|
-
// #639 — a BUILD_FAILURE fix that didn't change the branch is not
|
|
6064
|
-
// necessarily a genuine no-op: the agent may have found its own (or a
|
|
6065
|
-
// prior dispatch's) fix commit already present and concluded "nothing to
|
|
6066
|
-
// change" without checking whether CI actually turned green on that
|
|
6067
|
-
// commit. Re-poll live CI for the head before letting updatePrAfterFix
|
|
6068
|
-
// accept the no-op — if it's still failing, flag it so the no-op path
|
|
6069
|
-
// records a distinct fixIneffective signal instead of a clean no-op.
|
|
6070
|
-
if (prFixBranchChange?.changed === false) {
|
|
6071
|
-
const fixCause = shared.getPrFixAutomationCause({
|
|
6072
|
-
dispatchKey: dispatchItem?.meta?.dispatchKey,
|
|
6073
|
-
source: meta?.source,
|
|
6074
|
-
task: dispatchItem?.task,
|
|
6075
|
-
});
|
|
6076
|
-
if (fixCause === shared.PR_FIX_CAUSE.BUILD_FAILURE) {
|
|
6077
|
-
try {
|
|
6078
|
-
const liveStatus = await checkBuildStillFailingLive(meta.pr, meta.project);
|
|
6079
|
-
if (liveStatus === shared.BUILD_STATUS.FAILING) {
|
|
6080
|
-
prFixBuildStillFailing = liveStatus;
|
|
6081
|
-
}
|
|
6082
|
-
} catch (err) {
|
|
6083
|
-
log('warn', `Live build re-check for ${meta.pr.id}: ${err.message}`);
|
|
6084
|
-
}
|
|
6085
|
-
}
|
|
6086
|
-
}
|
|
6087
|
-
}
|
|
6088
|
-
|
|
6089
6159
|
// Scheduled task back-reference: update SQL state and write a linked inbox note.
|
|
6090
6160
|
if (meta?.item?._scheduleId) {
|
|
6091
6161
|
try {
|
|
@@ -6191,11 +6261,20 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6191
6261
|
|| completionContractFailure?.nonTerminal === true;
|
|
6192
6262
|
const finalResult = hardContractFail ? DISPATCH_RESULT.ERROR : (effectiveSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
|
|
6193
6263
|
if (type === WORK_TYPE.REVIEW && finalResult === DISPATCH_RESULT.SUCCESS && !skipDoneStatus && !benignNoop) {
|
|
6194
|
-
await updatePrAfterReview(
|
|
6264
|
+
await updatePrAfterReview(
|
|
6265
|
+
agentId,
|
|
6266
|
+
meta?.pr,
|
|
6267
|
+
meta?.project,
|
|
6268
|
+
config,
|
|
6269
|
+
resultSummary,
|
|
6270
|
+
structuredCompletion,
|
|
6271
|
+
dispatchItem,
|
|
6272
|
+
{ reportedModel: model },
|
|
6273
|
+
);
|
|
6195
6274
|
} else if (type === WORK_TYPE.REVIEW) {
|
|
6196
6275
|
log('warn', `Skipping PR review metadata update for ${meta?.pr?.id || meta?.pr?.url || '(unknown PR)'} because review dispatch ${dispatchItem.id} did not complete cleanly`);
|
|
6197
6276
|
}
|
|
6198
|
-
if (shared.isFixLikeWorkType(type) && effectiveSuccess) {
|
|
6277
|
+
if (shared.isFixLikeWorkType(type) && effectiveSuccess && !skipDoneStatus) {
|
|
6199
6278
|
updatePrAfterFix(meta?.pr, meta?.project, meta?.source, {
|
|
6200
6279
|
branchChanged: fixCompletionChangedBranch(structuredCompletion),
|
|
6201
6280
|
automationCauseKey: meta?.automationCauseKey,
|
|
@@ -6204,6 +6283,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6204
6283
|
buildStillFailing: prFixBuildStillFailing,
|
|
6205
6284
|
config,
|
|
6206
6285
|
noopReason: noopRationale || meta?._noopReason || '',
|
|
6286
|
+
reviewFindingResolution: reviewFixResolution,
|
|
6207
6287
|
});
|
|
6208
6288
|
// W-mpg58wv3 — closure-loop dispatch. When the completed fix WI was spawned
|
|
6209
6289
|
// to address a minion REQUEST_CHANGES (its meta carries
|
|
@@ -7044,6 +7124,7 @@ module.exports = {
|
|
|
7044
7124
|
isReviewBailout,
|
|
7045
7125
|
parseCompletionNoop,
|
|
7046
7126
|
classifyBenignZeroTargetOutcome, // exported for testing
|
|
7127
|
+
validateReviewFixCompletion,
|
|
7047
7128
|
detectNonTerminalResultSummary,
|
|
7048
7129
|
deferNonTerminalCompletion,
|
|
7049
7130
|
deferPhantomCompletion,
|
package/engine/llm.js
CHANGED
|
@@ -18,7 +18,15 @@ const {
|
|
|
18
18
|
ENGINE_DEFAULTS,
|
|
19
19
|
resolveCcCli, resolveCcModel,
|
|
20
20
|
} = shared;
|
|
21
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
resolveRuntime,
|
|
23
|
+
resolveRuntimeByCapability,
|
|
24
|
+
listRuntimes,
|
|
25
|
+
RUNTIME_IMAGES_FILE_OPTION,
|
|
26
|
+
sanitizeInvocationOptions,
|
|
27
|
+
resolveInvocationOptions,
|
|
28
|
+
buildSpawnFlags,
|
|
29
|
+
} = require('./runtimes');
|
|
22
30
|
|
|
23
31
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
24
32
|
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
@@ -217,15 +225,17 @@ function _runtimeInstallHint(runtimeName, runtime) {
|
|
|
217
225
|
function _missingRuntimeMessage(runtimeName, runtime, reason) {
|
|
218
226
|
const selected = runtime?.name || runtimeName || 'configured';
|
|
219
227
|
if (!runtime) {
|
|
228
|
+
const installLines = listRuntimes().map((name) => {
|
|
229
|
+
const adapter = resolveRuntime(name);
|
|
230
|
+
return `- ${name}: ${_runtimeInstallHint(name, adapter)}`;
|
|
231
|
+
});
|
|
220
232
|
return [
|
|
221
233
|
`Minions can't run the configured runtime "${selected}".`,
|
|
222
234
|
'',
|
|
223
235
|
reason || 'The configured runtime is not registered.',
|
|
224
236
|
'',
|
|
225
237
|
'Choose a supported runtime in Settings -> Engine -> defaultCli/ccCli, then install its CLI:',
|
|
226
|
-
|
|
227
|
-
'- OpenAI Codex CLI: npm install -g @openai/codex or brew install --cask codex',
|
|
228
|
-
'- GitHub Copilot CLI: install via GitHub CLI/gh-copilot or download from https://github.com/github/copilot-cli/releases',
|
|
238
|
+
...installLines,
|
|
229
239
|
'',
|
|
230
240
|
'After installing, restart Minions so the dashboard and engine inherit the updated PATH.',
|
|
231
241
|
].join('\n');
|
|
@@ -239,10 +249,9 @@ function _missingRuntimeMessage(runtimeName, runtime, reason) {
|
|
|
239
249
|
'',
|
|
240
250
|
'After installing, restart Minions so the dashboard and engine inherit the updated PATH.',
|
|
241
251
|
];
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
lines.push('Or switch Settings -> Engine -> defaultCli/ccCli back to "claude" after installing Claude Code.');
|
|
252
|
+
const alternatives = listRuntimes().filter(name => name !== selected);
|
|
253
|
+
if (alternatives.length) {
|
|
254
|
+
lines.push(`Or switch Settings -> Engine -> defaultCli/ccCli to another installed runtime: ${alternatives.join(', ')}.`);
|
|
246
255
|
}
|
|
247
256
|
return lines.join('\n');
|
|
248
257
|
}
|
|
@@ -337,37 +346,12 @@ function _runtimeUnavailableResult(callOpts = {}) {
|
|
|
337
346
|
// ─── Spawn Helpers ───────────────────────────────────────────────────────────
|
|
338
347
|
|
|
339
348
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* the single source of truth and avoiding double-flag emission.
|
|
344
|
-
*
|
|
345
|
-
* Capability gating (matches engine.js _buildAgentSpawnFlags from P-2a6d9c4f):
|
|
346
|
-
* - effort/sessionId/maxBudget/bare/fallbackModel are dropped when the
|
|
347
|
-
* runtime's matching capability is false.
|
|
348
|
-
* - Copilot-specific opts (stream, disableBuiltinMcps,
|
|
349
|
-
* reasoningSummaries) are emitted unconditionally; the Claude adapter
|
|
350
|
-
* ignores them via the "tolerate unknown opts" rule.
|
|
349
|
+
* Encode the semantic invocation options through the stable runtime facade.
|
|
350
|
+
* Legacy named wrapper flags remain for compatibility; the opaque options bag
|
|
351
|
+
* is authoritative for adapter fields the wrapper does not know yet.
|
|
351
352
|
*/
|
|
352
353
|
function _buildSpawnAgentFlags(runtime, opts = {}) {
|
|
353
|
-
|
|
354
|
-
const flags = ['--runtime', String(runtime?.name || 'claude')];
|
|
355
|
-
|
|
356
|
-
if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
|
|
357
|
-
if (opts.model) flags.push('--model', String(opts.model));
|
|
358
|
-
if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
|
|
359
|
-
|
|
360
|
-
if (caps.effortLevels && opts.effort) flags.push('--effort', String(opts.effort));
|
|
361
|
-
if (caps.sessionResume && opts.sessionId) flags.push('--resume', String(opts.sessionId));
|
|
362
|
-
if (caps.budgetCap && opts.maxBudget != null) flags.push('--max-budget-usd', String(opts.maxBudget));
|
|
363
|
-
if (caps.bareMode && opts.bare === true) flags.push('--bare');
|
|
364
|
-
if (caps.fallbackModel && opts.fallbackModel) flags.push('--fallback-model', String(opts.fallbackModel));
|
|
365
|
-
|
|
366
|
-
if (opts.stream === 'on' || opts.stream === 'off') flags.push('--stream', opts.stream);
|
|
367
|
-
if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
|
|
368
|
-
if (opts.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
|
|
369
|
-
|
|
370
|
-
return flags;
|
|
354
|
+
return buildSpawnFlags(runtime, opts);
|
|
371
355
|
}
|
|
372
356
|
|
|
373
357
|
/**
|
|
@@ -379,15 +363,12 @@ function _buildSpawnAgentFlags(runtime, opts = {}) {
|
|
|
379
363
|
*
|
|
380
364
|
* Indirect path: uses engine/spawn-agent.js — mostly a fallback when the
|
|
381
365
|
* direct path can't resolve the binary cache. spawn-agent.js handles
|
|
382
|
-
* adapter resolution itself; we
|
|
383
|
-
*
|
|
366
|
+
* adapter resolution itself; we hand it the runtime plus an opaque options
|
|
367
|
+
* bag, so adding a harness flag does not require wrapper changes.
|
|
384
368
|
*/
|
|
385
369
|
function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
386
370
|
const {
|
|
387
|
-
direct, label, runtime,
|
|
388
|
-
maxBudget, bare, fallbackModel,
|
|
389
|
-
stream, disableBuiltinMcps, reasoningSummaries,
|
|
390
|
-
images,
|
|
371
|
+
direct, label, runtime, adapterOptions = {},
|
|
391
372
|
} = callOpts;
|
|
392
373
|
|
|
393
374
|
const id = uid();
|
|
@@ -399,24 +380,10 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
399
380
|
const cleanupFiles = [];
|
|
400
381
|
const cleanupDirs = [llmTmpDir];
|
|
401
382
|
const caps = (runtime && runtime.capabilities) || {};
|
|
402
|
-
const adapterOpts = {
|
|
403
|
-
|
|
404
|
-
maxBudget, bare, fallbackModel,
|
|
405
|
-
stream, disableBuiltinMcps, reasoningSummaries,
|
|
406
|
-
images,
|
|
383
|
+
const adapterOpts = sanitizeInvocationOptions(runtime, {
|
|
384
|
+
...adapterOptions,
|
|
407
385
|
tmpDir: llmTmpDir,
|
|
408
|
-
};
|
|
409
|
-
// Capability-gate per-flag opts before prompt construction so adapters can
|
|
410
|
-
// make resume-aware prompt decisions from the same opts used for argv.
|
|
411
|
-
if (!caps.effortLevels) adapterOpts.effort = undefined;
|
|
412
|
-
if (!caps.sessionResume) adapterOpts.sessionId = undefined;
|
|
413
|
-
if (!caps.budgetCap) adapterOpts.maxBudget = undefined;
|
|
414
|
-
if (!caps.bareMode) adapterOpts.bare = undefined;
|
|
415
|
-
if (!caps.fallbackModel) adapterOpts.fallbackModel = undefined;
|
|
416
|
-
// P-7b2e4d01 — defense in depth: never forward images to a runtime that
|
|
417
|
-
// can't read them (callLLM/callLLMStreaming already gate + surface a typed
|
|
418
|
-
// error upstream; this protects any other _spawnProcess caller).
|
|
419
|
-
if (!caps.imageInput) adapterOpts.images = undefined;
|
|
386
|
+
});
|
|
420
387
|
const finalPrompt = runtime.buildPrompt(promptText, sysPromptText, adapterOpts);
|
|
421
388
|
|
|
422
389
|
// ── Direct path ──
|
|
@@ -426,7 +393,7 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
426
393
|
// Only write a sys-prompt tmp file when the runtime actually consumes one
|
|
427
394
|
// via --system-prompt-file (Claude) AND we're not resuming (resumed sessions
|
|
428
395
|
// already have the sys prompt baked in).
|
|
429
|
-
if (!sessionId && sysPromptText && caps.systemPromptFile) {
|
|
396
|
+
if (!adapterOpts.sessionId && sysPromptText && caps.systemPromptFile) {
|
|
430
397
|
sysTmpPath = path.join(llmTmpDir, `direct-sys-${id}.md`);
|
|
431
398
|
fs.writeFileSync(sysTmpPath, sysPromptText);
|
|
432
399
|
cleanupFiles.push(sysTmpPath);
|
|
@@ -472,12 +439,16 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
472
439
|
const pidPath = promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid');
|
|
473
440
|
cleanupFiles.push(promptPath, sysPath, pidPath);
|
|
474
441
|
|
|
475
|
-
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
442
|
+
const spawnScript = runtime.spawnScript || path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
443
|
+
const indirectAdapterOpts = { ...adapterOpts };
|
|
444
|
+
if (Array.isArray(indirectAdapterOpts.images) && indirectAdapterOpts.images.length) {
|
|
445
|
+
const imagesPath = path.join(llmTmpDir, `runtime-images-${id}.json`);
|
|
446
|
+
safeWrite(imagesPath, JSON.stringify(indirectAdapterOpts.images));
|
|
447
|
+
cleanupFiles.push(imagesPath);
|
|
448
|
+
indirectAdapterOpts[RUNTIME_IMAGES_FILE_OPTION] = imagesPath;
|
|
449
|
+
delete indirectAdapterOpts.images;
|
|
450
|
+
}
|
|
451
|
+
const adapterFlags = _buildSpawnAgentFlags(runtime, indirectAdapterOpts);
|
|
481
452
|
const args = [spawnScript, promptPath, sysPath, ...adapterFlags];
|
|
482
453
|
|
|
483
454
|
const proc = runFile(process.execPath, args, {
|
|
@@ -692,11 +663,16 @@ function _resolveModelForRuntime(runtime, callOpts) {
|
|
|
692
663
|
function _resolveRuntimeFeatureOpts({
|
|
693
664
|
stream, disableBuiltinMcps, reasoningSummaries, engineConfig,
|
|
694
665
|
} = {}) {
|
|
695
|
-
const
|
|
666
|
+
const runtime = resolveRuntimeByCapability('acpWorkerPool');
|
|
667
|
+
const resolved = resolveInvocationOptions(runtime, {
|
|
668
|
+
stream,
|
|
669
|
+
disableBuiltinMcps,
|
|
670
|
+
reasoningSummaries,
|
|
671
|
+
}, { engineConfig: engineConfig || {} });
|
|
696
672
|
return {
|
|
697
|
-
stream: stream
|
|
698
|
-
disableBuiltinMcps: disableBuiltinMcps
|
|
699
|
-
reasoningSummaries: reasoningSummaries
|
|
673
|
+
stream: resolved.stream,
|
|
674
|
+
disableBuiltinMcps: resolved.disableBuiltinMcps,
|
|
675
|
+
reasoningSummaries: resolved.reasoningSummaries,
|
|
700
676
|
};
|
|
701
677
|
}
|
|
702
678
|
|
|
@@ -770,17 +746,17 @@ function callLLM(promptText, sysPromptText, opts = {}) {
|
|
|
770
746
|
const model = _resolveModelForRuntime(runtime, { model: modelOverride, engineConfig });
|
|
771
747
|
const imageGate = _resolveImageOpts(images, runtime && runtime.capabilities);
|
|
772
748
|
if (imageGate.error) return _resolvedCallResult(_imageUnsupportedResult(runtime, imageGate.error));
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
749
|
+
const runtimeOptions = resolveInvocationOptions(runtime, {
|
|
750
|
+
model, maxTurns, allowedTools, effort, sessionId,
|
|
751
|
+
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
752
|
+
stream, disableBuiltinMcps, reasoningSummaries,
|
|
753
|
+
}, { engineConfig: engineConfig || {} });
|
|
776
754
|
|
|
777
755
|
let _abort = null;
|
|
778
756
|
const promise = new Promise((resolve) => {
|
|
779
757
|
const _startMs = Date.now();
|
|
780
758
|
const { proc, cleanupFiles, cleanupDirs } = _spawnProcess(promptText, sysPromptText, {
|
|
781
|
-
direct, label, runtime,
|
|
782
|
-
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
783
|
-
...runtimeFeatureOpts,
|
|
759
|
+
direct, label, runtime, adapterOptions: runtimeOptions,
|
|
784
760
|
});
|
|
785
761
|
let resolved = false;
|
|
786
762
|
let exitFallbackTimer = null;
|
|
@@ -895,17 +871,17 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
895
871
|
const model = _resolveModelForRuntime(runtime, { model: modelOverride, engineConfig });
|
|
896
872
|
const imageGate = _resolveImageOpts(images, runtime && runtime.capabilities);
|
|
897
873
|
if (imageGate.error) return _resolvedCallResult(_imageUnsupportedResult(runtime, imageGate.error));
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
874
|
+
const runtimeOptions = resolveInvocationOptions(runtime, {
|
|
875
|
+
model, maxTurns, allowedTools, effort, sessionId,
|
|
876
|
+
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
877
|
+
stream, disableBuiltinMcps, reasoningSummaries,
|
|
878
|
+
}, { engineConfig: engineConfig || {} });
|
|
901
879
|
|
|
902
880
|
let _abort = null;
|
|
903
881
|
const promise = new Promise((resolve) => {
|
|
904
882
|
const _startMs = Date.now();
|
|
905
883
|
const { proc, cleanupFiles, cleanupDirs } = _spawnProcess(promptText, sysPromptText, {
|
|
906
|
-
direct, label, runtime,
|
|
907
|
-
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
908
|
-
...runtimeFeatureOpts,
|
|
884
|
+
direct, label, runtime, adapterOptions: runtimeOptions,
|
|
909
885
|
});
|
|
910
886
|
let resolved = false;
|
|
911
887
|
let exitFallbackTimer = null;
|
package/engine/memory-store.js
CHANGED
|
@@ -269,10 +269,12 @@ function searchMemoryRecords(query, options = {}) {
|
|
|
269
269
|
}
|
|
270
270
|
const limit = Math.max(1, Math.min(200, Number(options.limit) || 50));
|
|
271
271
|
args.push(limit);
|
|
272
|
+
// CROSS JOIN pins FTS as the outer loop; Node 22's SQLite otherwise prefers
|
|
273
|
+
// the scope index and evaluates MATCH once for every scoped record.
|
|
272
274
|
const sql = `
|
|
273
275
|
SELECT m.*, bm25(memory_records_fts, 5.0, 1.0, 2.0, 3.0) AS fts_rank
|
|
274
276
|
FROM memory_records_fts
|
|
275
|
-
JOIN memory_records m ON m.rowid=memory_records_fts.rowid
|
|
277
|
+
CROSS JOIN memory_records m ON m.rowid=memory_records_fts.rowid
|
|
276
278
|
WHERE ${where.join(' AND ')}
|
|
277
279
|
ORDER BY fts_rank
|
|
278
280
|
LIMIT ?
|
package/engine/playbook.js
CHANGED
|
@@ -1214,12 +1214,10 @@ function formatPrContextSuffix(pr) {
|
|
|
1214
1214
|
// ─── Work Discovery Helpers ──────────────────────────────────────────────────
|
|
1215
1215
|
|
|
1216
1216
|
function buildBaseVars(agentId, config, project) {
|
|
1217
|
-
//
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1221
|
-
// field when neither agent.model nor engine.defaultModel resolves —
|
|
1222
|
-
// omitting would leave a dangling `· ` in the rendered sign-off.
|
|
1217
|
+
// This is the provisional requested model used in prompt examples. The
|
|
1218
|
+
// `minions pr comment` path replaces it with runtime/session evidence from
|
|
1219
|
+
// the active dispatch before posting; `unknown` survives only when neither
|
|
1220
|
+
// runtime nor requested-model evidence exists.
|
|
1223
1221
|
const resolvedModel = shared.resolveAgentModel(
|
|
1224
1222
|
config?.agents?.[agentId] || null,
|
|
1225
1223
|
config?.engine || null
|
|
@@ -15,30 +15,23 @@
|
|
|
15
15
|
* completely unmodified whether `proc` is a real child_process or a pooled
|
|
16
16
|
* ACP lease.
|
|
17
17
|
*
|
|
18
|
-
* Output translation (parity, not full fidelity)
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `copilot` CLI stdout stream — `{"type":"assistant.message_delta","data":
|
|
22
|
-
* {"deltaContent":"..."}}` per chunk, plus a terminal `{"type":"result",
|
|
23
|
-
* "sessionId":...,"usage":{...}}` line once the turn completes. This is the
|
|
24
|
-
* minimum subset `parseOutput` needs to recover an equivalent `{text,
|
|
25
|
-
* sessionId, usage}` to the cold-spawn path (deltas alone populate its
|
|
26
|
-
* `pendingDeltaContent` accumulator; no `assistant.message`/`session.
|
|
27
|
-
* tools_updated`/`tool.execution_*` events are required for that recovery).
|
|
28
|
-
* Tool-call and model-announcement events are intentionally NOT synthesized
|
|
29
|
-
* — a deliberate, documented scope limit, not an oversight: the live-output
|
|
30
|
-
* log still gets the real text in real time either way, and `usage`'s
|
|
31
|
-
* cost/token fields are already null-for-copilot on the cold path (see
|
|
32
|
-
* `runtimes/copilot.js#parseOutput`), so nothing measurable is lost.
|
|
18
|
+
* Output translation (parity, not full fidelity) is adapter-owned through
|
|
19
|
+
* `encodePooledOutput()`. The facade only forwards semantic chunk/result
|
|
20
|
+
* events, so a harness can change its JSONL schema without changing this file.
|
|
33
21
|
*/
|
|
34
22
|
|
|
35
23
|
const { EventEmitter } = require('events');
|
|
36
24
|
const { PassThrough } = require('stream');
|
|
25
|
+
const {
|
|
26
|
+
resolveRuntimeByCapability,
|
|
27
|
+
encodePooledOutput,
|
|
28
|
+
} = require('./runtimes');
|
|
37
29
|
|
|
38
30
|
class PooledAgentProcess extends EventEmitter {
|
|
39
31
|
/**
|
|
40
32
|
* @param {object} opts
|
|
41
33
|
* @param {object} opts.pool - engine/agent-worker-pool.js module (or a test double)
|
|
34
|
+
* @param {object} opts.runtime - resolved runtime adapter
|
|
42
35
|
* @param {string} opts.dispatchId
|
|
43
36
|
* @param {string} opts.cwd
|
|
44
37
|
* @param {string} [opts.model]
|
|
@@ -51,7 +44,7 @@ class PooledAgentProcess extends EventEmitter {
|
|
|
51
44
|
* @param {string} opts.promptText - task/user prompt only
|
|
52
45
|
*/
|
|
53
46
|
constructor({
|
|
54
|
-
pool, dispatchId, cwd, model, effort, mcpServers, hermeticDirs,
|
|
47
|
+
pool, runtime, dispatchId, cwd, model, effort, mcpServers, hermeticDirs,
|
|
55
48
|
processEnv, sessionContext, systemPromptText, promptText,
|
|
56
49
|
}) {
|
|
57
50
|
super();
|
|
@@ -61,10 +54,12 @@ class PooledAgentProcess extends EventEmitter {
|
|
|
61
54
|
this.stdout = new PassThrough();
|
|
62
55
|
this.stderr = new PassThrough();
|
|
63
56
|
this._pool = pool;
|
|
57
|
+
this._runtime = runtime || resolveRuntimeByCapability('acpWorkerPool');
|
|
64
58
|
this._dispatchId = dispatchId;
|
|
65
59
|
this._handle = null;
|
|
66
60
|
this._closed = false;
|
|
67
61
|
this._startedAt = Date.now();
|
|
62
|
+
this._runtimeOutputError = null;
|
|
68
63
|
|
|
69
64
|
// Fire-and-forget, mirroring child_process.spawn()'s async nature — the
|
|
70
65
|
// facade is usable (event listeners can be attached) immediately, before
|
|
@@ -82,6 +77,7 @@ class PooledAgentProcess extends EventEmitter {
|
|
|
82
77
|
let handle;
|
|
83
78
|
try {
|
|
84
79
|
handle = await this._pool.acquireWorker({
|
|
80
|
+
runtime: this._runtime,
|
|
85
81
|
dispatchId: this._dispatchId, cwd, model, effort, mcpServers, hermeticDirs,
|
|
86
82
|
processEnv, sessionContext,
|
|
87
83
|
});
|
|
@@ -102,6 +98,9 @@ class PooledAgentProcess extends EventEmitter {
|
|
|
102
98
|
|
|
103
99
|
this._handle = handle;
|
|
104
100
|
this.pid = typeof handle.pid === 'number' ? handle.pid : null;
|
|
101
|
+
if (handle.currentModel) {
|
|
102
|
+
this._writeRuntimeEvent({ kind: 'model', model: handle.currentModel });
|
|
103
|
+
}
|
|
105
104
|
// Lets engine.js write the PID-file equivalent (spawn-agent.js normally
|
|
106
105
|
// does this itself, from inside the child process) as soon as a real OS
|
|
107
106
|
// PID is known — see engine.js's pooled branch in spawnAgent().
|
|
@@ -113,16 +112,14 @@ class PooledAgentProcess extends EventEmitter {
|
|
|
113
112
|
systemPromptText,
|
|
114
113
|
onChunk: (text) => {
|
|
115
114
|
if (!text) return;
|
|
116
|
-
this.
|
|
115
|
+
this._writeRuntimeEvent({ kind: 'chunk', text });
|
|
117
116
|
},
|
|
118
117
|
onDone: (result) => {
|
|
119
|
-
this.
|
|
120
|
-
|
|
118
|
+
this._writeRuntimeEvent({
|
|
119
|
+
kind: 'result',
|
|
121
120
|
sessionId: handle.sessionId || null,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
stopReason: (result && result.stopReason) || null,
|
|
125
|
-
},
|
|
121
|
+
durationMs: Date.now() - this._startedAt,
|
|
122
|
+
stopReason: (result && result.stopReason) || null,
|
|
126
123
|
});
|
|
127
124
|
},
|
|
128
125
|
onError: (err) => {
|
|
@@ -137,11 +134,16 @@ class PooledAgentProcess extends EventEmitter {
|
|
|
137
134
|
try { handle.release(); } catch { /* best-effort */ }
|
|
138
135
|
this._handle = null;
|
|
139
136
|
}
|
|
140
|
-
this._finish(sawError ? 1 : 0);
|
|
137
|
+
this._finish(sawError || this._runtimeOutputError ? 1 : 0);
|
|
141
138
|
}
|
|
142
139
|
|
|
143
|
-
|
|
144
|
-
try {
|
|
140
|
+
_writeRuntimeEvent(event) {
|
|
141
|
+
try {
|
|
142
|
+
this.stdout.write(encodePooledOutput(this._runtime, event));
|
|
143
|
+
} catch (err) {
|
|
144
|
+
this._runtimeOutputError = err;
|
|
145
|
+
try { this.stderr.write(`Runtime output encoding failed: ${err.message}\n`); } catch {}
|
|
146
|
+
}
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
_finish(code) {
|