@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
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* worker exists and the pool is already at its configured size.
|
|
19
19
|
* WorkerHandle: {
|
|
20
20
|
* dispatchId,
|
|
21
|
+
* currentModel, // ACP-selected session model
|
|
21
22
|
* stream(promptText, opts) → Promise, // ACP session/prompt turn
|
|
22
23
|
* cancel(), // ACP session/cancel
|
|
23
24
|
* release(), // return the worker to the
|
|
@@ -133,7 +134,8 @@ function _pump() {
|
|
|
133
134
|
const req = _queue[0];
|
|
134
135
|
|
|
135
136
|
const matchIdx = _free.findIndex((w) =>
|
|
136
|
-
w.
|
|
137
|
+
w.runtime === req.runtime
|
|
138
|
+
&& w.mcpServersHash === req.mcpServersHash
|
|
137
139
|
&& w.processContextHash === req.processContextHash
|
|
138
140
|
);
|
|
139
141
|
if (matchIdx >= 0) {
|
|
@@ -230,6 +232,7 @@ async function _spawnFreshWorker(req) {
|
|
|
230
232
|
// queued request can spawn its replacement.
|
|
231
233
|
async function _bootWorker(req) {
|
|
232
234
|
const worker = new Worker({
|
|
235
|
+
runtime: req.runtime,
|
|
233
236
|
id: `agent-pool-${_nextWorkerId++}`,
|
|
234
237
|
model: req.model,
|
|
235
238
|
effort: req.effort,
|
|
@@ -312,7 +315,7 @@ function _clearLeaseContext(dispatchId) {
|
|
|
312
315
|
}
|
|
313
316
|
|
|
314
317
|
async function acquireWorker({
|
|
315
|
-
dispatchId, cwd, model, effort, mcpServers, hermeticDirs, processEnv, sessionContext,
|
|
318
|
+
runtime, dispatchId, cwd, model, effort, mcpServers, hermeticDirs, processEnv, sessionContext,
|
|
316
319
|
} = {}) {
|
|
317
320
|
if (!dispatchId) throw new Error('agent-worker-pool.acquireWorker: dispatchId is required');
|
|
318
321
|
if (_draining) throw new Error('agent-worker-pool: draining');
|
|
@@ -324,6 +327,7 @@ async function acquireWorker({
|
|
|
324
327
|
}
|
|
325
328
|
|
|
326
329
|
const mcpServersHash = hashMcpServers(mcpServers);
|
|
330
|
+
const runtimeAdapter = transport.resolveWorkerRuntime(runtime);
|
|
327
331
|
const workerProcessEnv = processEnv && typeof processEnv === 'object'
|
|
328
332
|
? Object.freeze({ ...processEnv })
|
|
329
333
|
: undefined;
|
|
@@ -332,6 +336,7 @@ async function acquireWorker({
|
|
|
332
336
|
? { ...sessionContext }
|
|
333
337
|
: {};
|
|
334
338
|
const req = {
|
|
339
|
+
runtime: runtimeAdapter,
|
|
335
340
|
dispatchId, cwd, model, effort, mcpServers, mcpServersHash,
|
|
336
341
|
processEnv: workerProcessEnv, processContextHash,
|
|
337
342
|
};
|
|
@@ -365,12 +370,14 @@ async function acquireWorker({
|
|
|
365
370
|
dispatchId,
|
|
366
371
|
// P-1d8f0b93 — exposed so engine.js's PooledAgentProcess facade (the
|
|
367
372
|
// spawnAgent integration) can write the same PID-file/log identity a
|
|
368
|
-
// cold-spawned child_process would,
|
|
369
|
-
// synthesized `result` event
|
|
373
|
+
// cold-spawned child_process would, stamp the ACP sessionId into the
|
|
374
|
+
// synthesized `result` event, and expose the model selected by session/new.
|
|
375
|
+
// All are read-time
|
|
370
376
|
// snapshots of the underlying worker's current values (a getter, not a
|
|
371
377
|
// captured copy), since sessionId can rotate under a reused worker.
|
|
372
378
|
get pid() { return (worker.proc && worker.proc.pid) || null; },
|
|
373
379
|
get sessionId() { return worker.sessionId || null; },
|
|
380
|
+
get currentModel() { return worker.currentModel || null; },
|
|
374
381
|
stream: async (promptText, opts = {}) => {
|
|
375
382
|
try {
|
|
376
383
|
return await worker.stream(promptText, {
|
package/engine/cc-worker-pool.js
CHANGED
|
@@ -151,8 +151,9 @@ function _trace(...parts) {
|
|
|
151
151
|
|
|
152
152
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
153
153
|
|
|
154
|
-
async function getSession({ tabId, model, effort, mcpServers, systemPromptHash, cwd } = {}) {
|
|
154
|
+
async function getSession({ runtime, tabId, model, effort, mcpServers, systemPromptHash, cwd } = {}) {
|
|
155
155
|
if (!tabId) throw new Error('cc-worker-pool.getSession: tabId is required');
|
|
156
|
+
const runtimeAdapter = transport.resolveWorkerRuntime(runtime);
|
|
156
157
|
const mcpServersHash = _hashMcpServers(mcpServers);
|
|
157
158
|
let worker = _tabs.get(tabId);
|
|
158
159
|
// Track which lifecycle path we took so the dashboard's [cc-timing] log can
|
|
@@ -195,7 +196,7 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
|
|
|
195
196
|
if (worker.killed) {
|
|
196
197
|
_tabs.delete(tabId);
|
|
197
198
|
worker = null;
|
|
198
|
-
} else if (worker.mcpServersHash !== mcpServersHash) {
|
|
199
|
+
} else if (worker.runtime !== runtimeAdapter || worker.mcpServersHash !== mcpServersHash) {
|
|
199
200
|
// mcpServers shape changed → must respawn the proc; the daemon
|
|
200
201
|
// resolves MCP server config at process boot, not per session.
|
|
201
202
|
_tabs.delete(tabId);
|
|
@@ -222,6 +223,7 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
|
|
|
222
223
|
|
|
223
224
|
if (!worker) {
|
|
224
225
|
worker = new Worker({
|
|
226
|
+
runtime: runtimeAdapter,
|
|
225
227
|
id: tabId, model, effort, mcpServers, mcpServersHash, systemPromptHash, cwd,
|
|
226
228
|
internals: _internals, trace: _trace,
|
|
227
229
|
});
|
package/engine/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ const shared = require('./shared');
|
|
|
9
9
|
const { safeRead, safeJson, safeJsonArr, safeWrite, readWorkItems, readPullRequests, mutateControl, mutateWorkItems, ts, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, REVIEW_STATUS, DISPATCH_RESULT, FAILURE_CLASS } = shared;
|
|
10
10
|
const queries = require('./queries');
|
|
11
11
|
const agentWorkerPool = require('./agent-worker-pool');
|
|
12
|
+
const { resolveCommentExecutionModel } = require('./execution-model');
|
|
12
13
|
const { getConfig, getControl, getDispatch, getAgentStatus,
|
|
13
14
|
MINIONS_DIR, ENGINE_DIR, AGENTS_DIR, PLANS_DIR, PRD_DIR, CONTROL_PATH } = queries;
|
|
14
15
|
|
|
@@ -2076,6 +2077,11 @@ const commands = {
|
|
|
2076
2077
|
console.error('error: must supply either --body-file <path> or --body <text>');
|
|
2077
2078
|
process.exit(2);
|
|
2078
2079
|
}
|
|
2080
|
+
const executionModel = resolveCommentExecutionModel({
|
|
2081
|
+
dispatch: getDispatch(),
|
|
2082
|
+
agentId,
|
|
2083
|
+
workItemId,
|
|
2084
|
+
});
|
|
2079
2085
|
|
|
2080
2086
|
// ── Azure DevOps: <prNumber> --host ado --ado-org --ado-project --repo-id ──
|
|
2081
2087
|
if (isAdo) {
|
|
@@ -2097,6 +2103,7 @@ const commands = {
|
|
|
2097
2103
|
const adoComment = require('./ado-comment');
|
|
2098
2104
|
adoComment.postAdoPrComment({
|
|
2099
2105
|
orgBase, project, repositoryId, prNumber, body, agentId, kind, workItemId,
|
|
2106
|
+
model: executionModel.model,
|
|
2100
2107
|
resolved: flags.resolved === true,
|
|
2101
2108
|
}).then((result) => {
|
|
2102
2109
|
if (result && result.threadId) {
|
|
@@ -2135,7 +2142,7 @@ const commands = {
|
|
|
2135
2142
|
|
|
2136
2143
|
try {
|
|
2137
2144
|
const result = ghComment.postPrComment({
|
|
2138
|
-
repo, prNumber, body, agentId, kind, workItemId,
|
|
2145
|
+
repo, prNumber, body, agentId, kind, workItemId, model: executionModel.model,
|
|
2139
2146
|
});
|
|
2140
2147
|
if (result.output) console.log(result.output);
|
|
2141
2148
|
} catch (e) {
|
|
@@ -2274,6 +2281,7 @@ module.exports = {
|
|
|
2274
2281
|
_readDispatchPid: readDispatchPid,
|
|
2275
2282
|
_normalizeSessionBranch: normalizeSessionBranch,
|
|
2276
2283
|
_dispatchSessionBranch: dispatchSessionBranch,
|
|
2284
|
+
_resolveCommentExecutionModel: resolveCommentExecutionModel, // exported for testing
|
|
2277
2285
|
// W-mpcyvff6000pf828 (#2653) — heartbeat writer + factory exported for tests
|
|
2278
2286
|
_writeHeartbeatNow: writeHeartbeatNow,
|
|
2279
2287
|
_createHeartbeatInterval: createHeartbeatInterval,
|
package/engine/comment-format.js
CHANGED
|
@@ -48,14 +48,17 @@ function linkifyBrandTrailer(text) {
|
|
|
48
48
|
// org/project/repoId).
|
|
49
49
|
//
|
|
50
50
|
// Marker format (single line, ASCII):
|
|
51
|
-
// <!-- minions:agent=<agentId> kind=<kind> wi=<workItemId> -->
|
|
51
|
+
// <!-- minions:agent=<agentId> kind=<kind> wi=<workItemId> model=<modelId> -->
|
|
52
52
|
//
|
|
53
53
|
// Validation (enforced before the marker is built):
|
|
54
54
|
// - agentId /^[a-z][a-z0-9-]{0,30}$/
|
|
55
55
|
// - kind /^[a-z][a-z0-9-]{0,30}$/
|
|
56
56
|
// - workItemId /^[A-Z]-[a-z0-9]+$/ (optional)
|
|
57
|
+
// - model concrete runtime execution model (optional)
|
|
57
58
|
// - no field may contain `--` (would close the HTML comment early)
|
|
58
59
|
|
|
60
|
+
const { normalizeExecutionModel } = require('./execution-model');
|
|
61
|
+
|
|
59
62
|
const AGENT_ID_RE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
60
63
|
const KIND_RE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
61
64
|
const WORK_ITEM_ID_RE = /^[A-Z]-[a-z0-9]+$/;
|
|
@@ -63,7 +66,7 @@ const WORK_ITEM_ID_RE = /^[A-Z]-[a-z0-9]+$/;
|
|
|
63
66
|
// Marker line (single-line HTML comment). Multiline flag so it matches at the
|
|
64
67
|
// start of any line — required for round-trip detection of the builder output.
|
|
65
68
|
const MINIONS_COMMENT_MARKER_RE =
|
|
66
|
-
/^<!--\s*minions:agent=([^\s]+)\s+kind=([^\s]+)(?:\s+wi=([^\s]+))?\s*-->/m;
|
|
69
|
+
/^<!--\s*minions:agent=([^\s]+)\s+kind=([^\s]+)(?:\s+wi=([^\s]+))?(?:\s+model=([^\s]+))?\s*-->/m;
|
|
67
70
|
|
|
68
71
|
// Canonical-format sample marker for tests/fixtures that need a marked body but
|
|
69
72
|
// don't care about the specific fields.
|
|
@@ -85,18 +88,44 @@ function _validateField(name, value, re) {
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
function _validateMarkerInputs({ agentId, kind, workItemId }) {
|
|
91
|
+
function _validateMarkerInputs({ agentId, kind, workItemId, model }) {
|
|
89
92
|
_validateField('agentId', agentId, AGENT_ID_RE);
|
|
90
93
|
_validateField('kind', kind, KIND_RE);
|
|
91
94
|
if (workItemId !== undefined && workItemId !== null) {
|
|
92
95
|
_validateField('workItemId', workItemId, WORK_ITEM_ID_RE);
|
|
93
96
|
}
|
|
97
|
+
if (model !== undefined && model !== null && !normalizeExecutionModel(model)) {
|
|
98
|
+
throw new Error(`invalid model: ${JSON.stringify(model)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function _encodeMarkerModel(model) {
|
|
103
|
+
const encoded = encodeURIComponent(model);
|
|
104
|
+
return encoded.includes('--') ? encoded.replace(/-/g, '%2D') : encoded;
|
|
94
105
|
}
|
|
95
106
|
|
|
96
|
-
function _buildMarker({ agentId, kind, workItemId }) {
|
|
97
|
-
_validateMarkerInputs({ agentId, kind, workItemId });
|
|
107
|
+
function _buildMarker({ agentId, kind, workItemId, model }) {
|
|
108
|
+
_validateMarkerInputs({ agentId, kind, workItemId, model });
|
|
98
109
|
const wi = (workItemId !== undefined && workItemId !== null) ? ` wi=${workItemId}` : '';
|
|
99
|
-
|
|
110
|
+
const modelPart = model ? ` model=${_encodeMarkerModel(model)}` : '';
|
|
111
|
+
return `<!-- minions:agent=${agentId} kind=${kind}${wi}${modelPart} -->`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const MODEL_SIGNOFF_RE =
|
|
115
|
+
/(\b(?:Review|Reviewed|Fixed|Verified|Rebased) by (?:Minions|\[Minions\]\([^)]+\)) \([^\r\n)]*? · )[^)\r\n]+(\))/g;
|
|
116
|
+
|
|
117
|
+
function stampExecutionModel(text, model) {
|
|
118
|
+
const normalized = normalizeExecutionModel(model);
|
|
119
|
+
if (!normalized || typeof text !== 'string') return text;
|
|
120
|
+
return text.replace(MODEL_SIGNOFF_RE, (_match, prefix, suffix) => `${prefix}${normalized}${suffix}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function _stampLeadingMarkerModel(text, model) {
|
|
124
|
+
if (!model || typeof text !== 'string') return text;
|
|
125
|
+
return text.replace(MINIONS_COMMENT_MARKER_RE, (marker, agentId, kind, workItemId) => {
|
|
126
|
+
if (marker !== text.slice(0, marker.length)) return marker;
|
|
127
|
+
return _buildMarker({ agentId, kind, workItemId, model });
|
|
128
|
+
});
|
|
100
129
|
}
|
|
101
130
|
|
|
102
131
|
/**
|
|
@@ -106,41 +135,49 @@ function _buildMarker({ agentId, kind, workItemId }) {
|
|
|
106
135
|
* Idempotency: if `body` already starts with a minions marker it is returned
|
|
107
136
|
* with the brand transform applied but no second marker prepended.
|
|
108
137
|
*
|
|
109
|
-
* @param {{agentId:string, kind:string, workItemId?:string, body?:string}} args
|
|
138
|
+
* @param {{agentId:string, kind:string, workItemId?:string, model?:string, body?:string}} args
|
|
110
139
|
* @returns {string} the final comment body
|
|
111
140
|
*/
|
|
112
|
-
function buildMinionsCommentBody({ agentId, kind, workItemId, body }) {
|
|
141
|
+
function buildMinionsCommentBody({ agentId, kind, workItemId, model, body }) {
|
|
113
142
|
// Validate the inputs even when the body is pre-marked, so callers can't
|
|
114
143
|
// silently bypass validation by pre-marking their body.
|
|
115
|
-
_validateMarkerInputs({ agentId, kind, workItemId });
|
|
144
|
+
_validateMarkerInputs({ agentId, kind, workItemId, model });
|
|
116
145
|
let safeBody = body == null ? '' : String(body);
|
|
117
146
|
|
|
147
|
+
safeBody = stampExecutionModel(safeBody, model);
|
|
148
|
+
|
|
118
149
|
// Brand-link safety net: deterministically hyperlink the first bare
|
|
119
150
|
// "… by Minions" signature trailer (no-op when the agent already linked it).
|
|
120
151
|
safeBody = linkifyBrandTrailer(safeBody);
|
|
121
152
|
|
|
122
|
-
if (_LEADING_MARKER_RE.test(safeBody)) return safeBody;
|
|
123
|
-
const marker = _buildMarker({ agentId, kind, workItemId });
|
|
153
|
+
if (_LEADING_MARKER_RE.test(safeBody)) return _stampLeadingMarkerModel(safeBody, model);
|
|
154
|
+
const marker = _buildMarker({ agentId, kind, workItemId, model });
|
|
124
155
|
return `${marker}\n\n${safeBody}`;
|
|
125
156
|
}
|
|
126
157
|
|
|
127
158
|
/**
|
|
128
|
-
* Inverse of the marker builder: parse `{agentId, kind, workItemId}`
|
|
129
|
-
* marked body, or null when no marker is present.
|
|
159
|
+
* Inverse of the marker builder: parse `{agentId, kind, workItemId, model?}`
|
|
160
|
+
* out of a marked body, or null when no marker is present.
|
|
130
161
|
*/
|
|
131
162
|
function parseMinionsMarker(body) {
|
|
132
163
|
if (typeof body !== 'string' || body.length === 0) return null;
|
|
133
164
|
const m = body.match(MINIONS_COMMENT_MARKER_RE);
|
|
134
165
|
if (!m) return null;
|
|
135
|
-
|
|
166
|
+
const parsed = {
|
|
136
167
|
agentId: m[1],
|
|
137
168
|
kind: m[2],
|
|
138
169
|
workItemId: m[3] === undefined ? undefined : m[3],
|
|
139
170
|
};
|
|
171
|
+
if (m[4] !== undefined) {
|
|
172
|
+
try { parsed.model = decodeURIComponent(m[4]); }
|
|
173
|
+
catch { return null; }
|
|
174
|
+
}
|
|
175
|
+
return parsed;
|
|
140
176
|
}
|
|
141
177
|
|
|
142
178
|
module.exports = {
|
|
143
179
|
linkifyBrandTrailer,
|
|
180
|
+
stampExecutionModel,
|
|
144
181
|
MINIONS_BRAND_URL,
|
|
145
182
|
// Neutral marker + body builder (consumed by gh-comment.js and ado-comment.js).
|
|
146
183
|
buildMinionsCommentBody,
|
package/engine/dispatch.js
CHANGED
|
@@ -978,7 +978,12 @@ function markCompletedSourceWorkItem(item) {
|
|
|
978
978
|
// ─── Complete Dispatch ───────────────────────────────────────────────────────
|
|
979
979
|
|
|
980
980
|
function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', resultSummary = '', opts = {}) {
|
|
981
|
-
const {
|
|
981
|
+
const {
|
|
982
|
+
processWorkItemFailure = true,
|
|
983
|
+
processWorkItemSuccess = false,
|
|
984
|
+
countAgentRetryFailure = true,
|
|
985
|
+
failureClass,
|
|
986
|
+
} = opts;
|
|
982
987
|
const agentRetryable = normalizeRetryableDecision(opts.agentRetryable ?? opts.retryable);
|
|
983
988
|
let item = null;
|
|
984
989
|
let completedSourceWorkItem = false;
|
|
@@ -1185,7 +1190,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
1185
1190
|
// agent hits the threshold. Skip when no agent is resolvable
|
|
1186
1191
|
// (anonymous failures shouldn't corrupt the map shape).
|
|
1187
1192
|
const failedAgent = item.agent || wi.dispatched_to;
|
|
1188
|
-
if (failedAgent) shared.bumpAgentRetryCount(wi, failedAgent);
|
|
1193
|
+
if (failedAgent && countAgentRetryFailure) shared.bumpAgentRetryCount(wi, failedAgent);
|
|
1189
1194
|
delete wi.failReason;
|
|
1190
1195
|
delete wi.failedAt;
|
|
1191
1196
|
delete wi.dispatched_at;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-model evidence shared by dispatch, PR-comment, and review persistence.
|
|
3
|
+
*
|
|
4
|
+
* Priority is deliberate: runtime completion output is authoritative, an
|
|
5
|
+
* earlier stream/session capture is next, and the requested model is only a
|
|
6
|
+
* fallback when Minions explicitly sent one to the runtime. No catalog-based
|
|
7
|
+
* default inference belongs here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const UNKNOWN_MODEL = 'unknown';
|
|
11
|
+
const MODEL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
|
12
|
+
|
|
13
|
+
function normalizeExecutionModel(value) {
|
|
14
|
+
if (typeof value !== 'string') return null;
|
|
15
|
+
const model = value.trim();
|
|
16
|
+
return MODEL_ID_RE.test(model) ? model : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveExecutionModel({
|
|
20
|
+
reportedModel,
|
|
21
|
+
capturedModel,
|
|
22
|
+
requestedModel,
|
|
23
|
+
} = {}) {
|
|
24
|
+
for (const [source, value] of [
|
|
25
|
+
['reported', reportedModel],
|
|
26
|
+
['captured', capturedModel],
|
|
27
|
+
['requested', requestedModel],
|
|
28
|
+
]) {
|
|
29
|
+
const model = normalizeExecutionModel(value);
|
|
30
|
+
if (model) return { model, source };
|
|
31
|
+
}
|
|
32
|
+
return { model: UNKNOWN_MODEL, source: 'unknown' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function _dispatchWorkItemId(item) {
|
|
36
|
+
return item?.meta?.item?.id
|
|
37
|
+
|| item?.meta?.workItemId
|
|
38
|
+
|| item?.workItemId
|
|
39
|
+
|| null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveCommentExecutionModel({
|
|
43
|
+
dispatch,
|
|
44
|
+
agentId,
|
|
45
|
+
workItemId,
|
|
46
|
+
} = {}) {
|
|
47
|
+
const active = Array.isArray(dispatch?.active) ? dispatch.active : [];
|
|
48
|
+
const candidates = active.filter((item) => {
|
|
49
|
+
if (String(item?.agent || '').toLowerCase() !== String(agentId || '').toLowerCase()) return false;
|
|
50
|
+
if (!workItemId) return true;
|
|
51
|
+
return item.id === workItemId || _dispatchWorkItemId(item) === workItemId;
|
|
52
|
+
});
|
|
53
|
+
candidates.sort((a, b) => String(b.started_at || '').localeCompare(String(a.started_at || '')));
|
|
54
|
+
const item = candidates[0] || null;
|
|
55
|
+
if (!item) return { model: null, source: 'none' };
|
|
56
|
+
return resolveExecutionModel({
|
|
57
|
+
capturedModel: item?.executionModel,
|
|
58
|
+
requestedModel: item?.requestedModel,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
UNKNOWN_MODEL,
|
|
64
|
+
MODEL_ID_RE,
|
|
65
|
+
normalizeExecutionModel,
|
|
66
|
+
resolveExecutionModel,
|
|
67
|
+
resolveCommentExecutionModel,
|
|
68
|
+
};
|
package/engine/gh-comment.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* minions-authored PR comments by structure rather than by body shape.
|
|
5
5
|
*
|
|
6
6
|
* Marker format (single line, ASCII):
|
|
7
|
-
* <!-- minions:agent=<agentId> kind=<kind> wi=<workItemId> -->
|
|
7
|
+
* <!-- minions:agent=<agentId> kind=<kind> wi=<workItemId> model=<modelId> -->
|
|
8
8
|
*
|
|
9
9
|
* Followed by `\n\n` and then the caller-provided body. `wi=` is omitted when
|
|
10
10
|
* no workItemId is supplied. The marker is intentionally minimal so it round-
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* - agentId /^[a-z][a-z0-9-]{0,30}$/ (lowercase, hyphenated, ≤31 chars)
|
|
15
15
|
* - kind /^[a-z][a-z0-9-]{0,30}$/ (same shape — categorical tag)
|
|
16
16
|
* - workItemId /^[A-Z]-[a-z0-9]+$/ (e.g. W-mp3bp0ha000997ab, P-d5a8c9b6)
|
|
17
|
+
* - model concrete runtime model, when active-dispatch evidence exists
|
|
17
18
|
* - no field may contain `--` (would close the HTML comment early)
|
|
18
19
|
* - no field may contain `=` `<` `>` `\n` `"` `'` `` ` `` ` ` (whitespace)
|
|
19
20
|
*
|
|
@@ -153,13 +154,14 @@ function postPrComment({
|
|
|
153
154
|
agentId,
|
|
154
155
|
kind,
|
|
155
156
|
workItemId,
|
|
157
|
+
model,
|
|
156
158
|
timeoutMs = 30000,
|
|
157
159
|
execFileSync = _execFileSync,
|
|
158
160
|
resolveTokenForSlug,
|
|
159
161
|
} = {}) {
|
|
160
162
|
_validateRepo(repo);
|
|
161
163
|
_validatePrNumber(prNumber);
|
|
162
|
-
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body });
|
|
164
|
+
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, model, body });
|
|
163
165
|
const file = _writeTempBodyFile(finalBody);
|
|
164
166
|
const env = _resolveTokenEnvForRepo(repo, resolveTokenForSlug);
|
|
165
167
|
try {
|
|
@@ -182,13 +184,14 @@ function postPrReviewComment({
|
|
|
182
184
|
agentId,
|
|
183
185
|
kind,
|
|
184
186
|
workItemId,
|
|
187
|
+
model,
|
|
185
188
|
timeoutMs = 30000,
|
|
186
189
|
execFileSync = _execFileSync,
|
|
187
190
|
resolveTokenForSlug,
|
|
188
191
|
} = {}) {
|
|
189
192
|
_validateRepo(repo);
|
|
190
193
|
_validatePrNumber(prNumber);
|
|
191
|
-
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body });
|
|
194
|
+
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, model, body });
|
|
192
195
|
const file = _writeTempBodyFile(finalBody);
|
|
193
196
|
const env = _resolveTokenEnvForRepo(repo, resolveTokenForSlug);
|
|
194
197
|
try {
|
package/engine/github.js
CHANGED
|
@@ -282,10 +282,10 @@ async function _resolveViewerLogin(slug) {
|
|
|
282
282
|
// `_isMinionsAuthoredComment` cannot tell minions posts apart from human
|
|
283
283
|
// posts (the `viewerDidAuthor` gate stays false for everything), so the
|
|
284
284
|
// comment classifier reclassifies every minions comment as human and
|
|
285
|
-
// queues a spurious fix dispatch on every poll cycle.
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
// data is worth tracking so future audits can prioritize a deeper fix
|
|
285
|
+
// queues a spurious fix dispatch on every poll cycle. Review-fix completion
|
|
286
|
+
// now rejects shared-identity-only no-ops, so failing closed here prevents
|
|
287
|
+
// both duplicate traffic and false completion retries. The frequency
|
|
288
|
+
// data is still worth tracking so future audits can prioritize a deeper fix
|
|
289
289
|
// (e.g. a credentialed re-probe of the gh CLI). Global counter under
|
|
290
290
|
// `_engine.viewerLoginResolutionFailures`; surfaced through the existing
|
|
291
291
|
// engine metrics aggregation (`getMetrics()` in `engine/queries.js`).
|
|
@@ -1241,9 +1241,8 @@ async function pollPrHumanComments(config) {
|
|
|
1241
1241
|
// `c.viewerDidAuthor === true`, and `_backfillViewerDidAuthor` is a
|
|
1242
1242
|
// no-op when viewerLogin is null. Running the classifier without a
|
|
1243
1243
|
// login mass-misclassifies every minions comment as human and queues
|
|
1244
|
-
// a redundant fix dispatch on every poll cycle
|
|
1245
|
-
//
|
|
1246
|
-
// a slot/token tax — see audit OQ-Y, HIGH not CRITICAL). Skip the
|
|
1244
|
+
// a redundant fix dispatch on every poll cycle. Shared identity is not
|
|
1245
|
+
// valid review-fix no-op evidence, so skip the
|
|
1247
1246
|
// entire round instead. One log + one counter increment per affected
|
|
1248
1247
|
// PR per cycle (no per-comment fanout).
|
|
1249
1248
|
if (!viewerLogin) {
|