@yemi33/minions 0.1.2381 → 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 +42 -2
- package/dashboard/js/refresh.js +8 -2
- package/dashboard/js/render-other.js +38 -0
- package/dashboard/js/render-work-items.js +61 -20
- package/dashboard/js/settings.js +7 -8
- package/dashboard/pages/engine.html +6 -0
- package/dashboard.js +110 -27
- package/docs/README.md +1 -0
- package/docs/claude-md-propagation.md +118 -0
- package/docs/completion-reports.md +20 -1
- package/docs/engine-restart.md +10 -0
- 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 +23 -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/claude-md-context.js +195 -0
- package/engine/cli.js +9 -1
- package/engine/comment-format.js +51 -14
- package/engine/consolidation.js +47 -3
- package/engine/db/migrations/026-pre-dispatch-rejections.js +55 -0
- package/engine/dispatch.js +80 -23
- package/engine/execution-model.js +68 -0
- package/engine/gh-comment.js +6 -3
- package/engine/github.js +6 -7
- package/engine/lifecycle.js +195 -56
- package/engine/llm.js +59 -83
- package/engine/memory-store.js +3 -1
- package/engine/pipeline.js +147 -20
- package/engine/playbook.js +39 -6
- package/engine/pooled-agent-process.js +28 -26
- package/engine/prd-store.js +14 -6
- package/engine/quarantine-refs.js +33 -0
- package/engine/queries.js +8 -6
- package/engine/restart-health.js +69 -4
- package/engine/runtimes/claude.js +100 -25
- package/engine/runtimes/codex.js +19 -0
- package/engine/runtimes/contract.js +239 -0
- package/engine/runtimes/copilot.js +85 -8
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +157 -64
- package/engine/spawn-agent.js +79 -113
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine/supervisor.js +72 -16
- package/engine/timeout.js +87 -16
- package/engine/untrusted-fence.js +2 -1
- package/engine.js +434 -125
- 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/dispatch.js
CHANGED
|
@@ -307,10 +307,10 @@ function addToDispatch(item) {
|
|
|
307
307
|
function _persistInvalidWorkItem(item, evaluation) {
|
|
308
308
|
const meta = item?.meta;
|
|
309
309
|
const itemId = meta?.item?.id;
|
|
310
|
-
if (!itemId) return {
|
|
310
|
+
if (!itemId) return { exhausted: false };
|
|
311
311
|
let wiScope;
|
|
312
312
|
try { wiScope = lifecycle().resolveWorkItemScope(meta); } catch { wiScope = null; }
|
|
313
|
-
if (!wiScope) return {
|
|
313
|
+
if (!wiScope) return { exhausted: false };
|
|
314
314
|
// W-mrcrh5hj0017d22f — repeat-rejection cap. Compare against the
|
|
315
315
|
// description snapshot from the LAST rejection so an operator edit to the
|
|
316
316
|
// WI resets the counter (it deserves a fresh evaluation), while an
|
|
@@ -319,7 +319,7 @@ function _persistInvalidWorkItem(item, evaluation) {
|
|
|
319
319
|
const maxRejections = queries.getConfig()?.engine?.preDispatchEvalMaxRejections
|
|
320
320
|
?? ENGINE_DEFAULTS.preDispatchEvalMaxRejections;
|
|
321
321
|
const currentDescription = String((meta.item && meta.item.description) || '');
|
|
322
|
-
let
|
|
322
|
+
let exhausted = false;
|
|
323
323
|
try {
|
|
324
324
|
mutateWorkItems(wiScope, (items) => {
|
|
325
325
|
if (!Array.isArray(items)) return items;
|
|
@@ -329,31 +329,79 @@ function _persistInvalidWorkItem(item, evaluation) {
|
|
|
329
329
|
const prior = wi._preDispatchEval;
|
|
330
330
|
const sameDescription = prior && prior.description === currentDescription;
|
|
331
331
|
const rejectCount = (sameDescription ? (prior.rejectCount || 0) : 0) + 1;
|
|
332
|
+
const history = Array.isArray(prior?.history) ? prior.history.slice(-19) : [];
|
|
333
|
+
const evaluatedAt = ts();
|
|
334
|
+
history.push({
|
|
335
|
+
reason: evaluation.reason || '',
|
|
336
|
+
evaluatedAt,
|
|
337
|
+
description: currentDescription,
|
|
338
|
+
});
|
|
332
339
|
wi._preDispatchEval = {
|
|
333
340
|
valid: false,
|
|
334
341
|
reason: evaluation.reason || '',
|
|
335
|
-
evaluatedAt
|
|
342
|
+
evaluatedAt,
|
|
336
343
|
rejectCount,
|
|
337
344
|
description: currentDescription,
|
|
345
|
+
history,
|
|
338
346
|
};
|
|
339
347
|
if (rejectCount >= maxRejections) {
|
|
340
|
-
|
|
341
|
-
wi.status = WI_STATUS.
|
|
342
|
-
wi.
|
|
343
|
-
wi.
|
|
344
|
-
|
|
345
|
-
wi.
|
|
346
|
-
wi.
|
|
348
|
+
exhausted = true;
|
|
349
|
+
wi.status = WI_STATUS.PENDING;
|
|
350
|
+
wi._preDispatchEval.exhausted = true;
|
|
351
|
+
wi._preDispatchEval.exhaustedAt = evaluatedAt;
|
|
352
|
+
wi._pendingReason = 'pre_dispatch_eval_rejected';
|
|
353
|
+
delete wi._failureClass;
|
|
354
|
+
delete wi.failReason;
|
|
355
|
+
delete wi.failedAt;
|
|
347
356
|
}
|
|
348
357
|
return items;
|
|
349
358
|
}, { skipWriteIfUnchanged: true });
|
|
350
359
|
} catch (e) {
|
|
351
360
|
log('warn', `pre-dispatch-eval: failed to persist reason on ${itemId}: ${e.message}`);
|
|
352
361
|
}
|
|
353
|
-
if (
|
|
354
|
-
log('warn', `pre-dispatch-eval: ${itemId} rejected ${maxRejections}+ times with unchanged description —
|
|
362
|
+
if (exhausted) {
|
|
363
|
+
log('warn', `pre-dispatch-eval: ${itemId} rejected ${maxRejections}+ times with unchanged description — paused pending operator edit`);
|
|
364
|
+
}
|
|
365
|
+
return { exhausted };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function _clearPreDispatchRejection(item, evaluation) {
|
|
369
|
+
const meta = item?.meta;
|
|
370
|
+
const itemId = meta?.item?.id;
|
|
371
|
+
if (!itemId) return;
|
|
372
|
+
let wiScope;
|
|
373
|
+
try { wiScope = lifecycle().resolveWorkItemScope(meta); } catch { wiScope = null; }
|
|
374
|
+
if (!wiScope) return;
|
|
375
|
+
try {
|
|
376
|
+
mutateWorkItems(wiScope, (items) => {
|
|
377
|
+
if (!Array.isArray(items)) return items;
|
|
378
|
+
const wi = items.find(w => w && w.id === itemId);
|
|
379
|
+
if (!wi || !wi._preDispatchEval) return items;
|
|
380
|
+
wi._preDispatchEval = {
|
|
381
|
+
...wi._preDispatchEval,
|
|
382
|
+
valid: true,
|
|
383
|
+
reason: evaluation?.reason || '',
|
|
384
|
+
evaluatedAt: ts(),
|
|
385
|
+
rejectCount: 0,
|
|
386
|
+
description: String(meta.item?.description || ''),
|
|
387
|
+
};
|
|
388
|
+
delete wi._preDispatchEval.exhausted;
|
|
389
|
+
delete wi._preDispatchEval.exhaustedAt;
|
|
390
|
+
if (wi._pendingReason === 'pre_dispatch_eval_rejected') delete wi._pendingReason;
|
|
391
|
+
return items;
|
|
392
|
+
}, { skipWriteIfUnchanged: true });
|
|
393
|
+
} catch (e) {
|
|
394
|
+
log('warn', `pre-dispatch-eval: failed to clear rejection on ${itemId}: ${e.message}`);
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
mutateDispatch((dispatch) => {
|
|
398
|
+
dispatch.review = (Array.isArray(dispatch.review) ? dispatch.review : [])
|
|
399
|
+
.filter(entry => entry?.meta?.item?.id !== itemId);
|
|
400
|
+
return dispatch;
|
|
401
|
+
});
|
|
402
|
+
} catch (e) {
|
|
403
|
+
log('warn', `pre-dispatch-eval: failed to clear review entry for ${itemId}: ${e.message}`);
|
|
355
404
|
}
|
|
356
|
-
return { stuck };
|
|
357
405
|
}
|
|
358
406
|
|
|
359
407
|
function _routeToReviewQueue(item, evaluation) {
|
|
@@ -438,6 +486,11 @@ async function addToDispatchWithValidation(item, opts = {}) {
|
|
|
438
486
|
if (!hasCriteria && !hasUsableDescription) {
|
|
439
487
|
return addToDispatch(item);
|
|
440
488
|
}
|
|
489
|
+
const priorEvaluation = wi?._preDispatchEval;
|
|
490
|
+
if (priorEvaluation?.exhausted === true
|
|
491
|
+
&& priorEvaluation.description === String(wi?.description || '')) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
441
494
|
|
|
442
495
|
// W-mq9acoo800177bcb — PRD-sourced bypass. plan-to-prd already LLM-vets
|
|
443
496
|
// every item it lands in a PRD, so re-validating each materialized item
|
|
@@ -477,14 +530,13 @@ async function addToDispatchWithValidation(item, opts = {}) {
|
|
|
477
530
|
return addToDispatch(item);
|
|
478
531
|
}
|
|
479
532
|
|
|
480
|
-
if (!evaluation || evaluation.valid !== false)
|
|
533
|
+
if (!evaluation || evaluation.valid !== false) {
|
|
534
|
+
_clearPreDispatchRejection(item, evaluation);
|
|
535
|
+
return addToDispatch(item);
|
|
536
|
+
}
|
|
481
537
|
|
|
482
|
-
const {
|
|
483
|
-
|
|
484
|
-
// repeat-rejection, don't also push it into the review queue — the
|
|
485
|
-
// failed status already stops future discovery ticks from re-evaluating
|
|
486
|
-
// it, and a review-queue entry would just be a stale duplicate signal.
|
|
487
|
-
if (!stuck) _routeToReviewQueue(item, evaluation);
|
|
538
|
+
const { exhausted } = _persistInvalidWorkItem(item, evaluation);
|
|
539
|
+
if (!exhausted) _routeToReviewQueue(item, evaluation);
|
|
488
540
|
log('warn', `pre-dispatch-eval: blocked work item ${wi.id} — ${evaluation.reason || 'criteria not actionable'}`);
|
|
489
541
|
return null;
|
|
490
542
|
}
|
|
@@ -926,7 +978,12 @@ function markCompletedSourceWorkItem(item) {
|
|
|
926
978
|
// ─── Complete Dispatch ───────────────────────────────────────────────────────
|
|
927
979
|
|
|
928
980
|
function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', resultSummary = '', opts = {}) {
|
|
929
|
-
const {
|
|
981
|
+
const {
|
|
982
|
+
processWorkItemFailure = true,
|
|
983
|
+
processWorkItemSuccess = false,
|
|
984
|
+
countAgentRetryFailure = true,
|
|
985
|
+
failureClass,
|
|
986
|
+
} = opts;
|
|
930
987
|
const agentRetryable = normalizeRetryableDecision(opts.agentRetryable ?? opts.retryable);
|
|
931
988
|
let item = null;
|
|
932
989
|
let completedSourceWorkItem = false;
|
|
@@ -1133,7 +1190,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
1133
1190
|
// agent hits the threshold. Skip when no agent is resolvable
|
|
1134
1191
|
// (anonymous failures shouldn't corrupt the map shape).
|
|
1135
1192
|
const failedAgent = item.agent || wi.dispatched_to;
|
|
1136
|
-
if (failedAgent) shared.bumpAgentRetryCount(wi, failedAgent);
|
|
1193
|
+
if (failedAgent && countAgentRetryFailure) shared.bumpAgentRetryCount(wi, failedAgent);
|
|
1137
1194
|
delete wi.failReason;
|
|
1138
1195
|
delete wi.failedAt;
|
|
1139
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) {
|