@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.
Files changed (54) hide show
  1. package/bin/minions.js +42 -2
  2. package/dashboard/js/refresh.js +8 -2
  3. package/dashboard/js/render-other.js +38 -0
  4. package/dashboard/js/render-work-items.js +61 -20
  5. package/dashboard/js/settings.js +7 -8
  6. package/dashboard/pages/engine.html +6 -0
  7. package/dashboard.js +110 -27
  8. package/docs/README.md +1 -0
  9. package/docs/claude-md-propagation.md +118 -0
  10. package/docs/completion-reports.md +20 -1
  11. package/docs/engine-restart.md +10 -0
  12. package/docs/runtime-adapters.md +54 -22
  13. package/docs/skills.md +2 -0
  14. package/docs/workspace-manifests.md +1 -1
  15. package/docs/worktree-lifecycle.md +23 -0
  16. package/engine/acp-transport.js +63 -35
  17. package/engine/ado-comment.js +3 -1
  18. package/engine/agent-worker-pool.js +11 -4
  19. package/engine/cc-worker-pool.js +4 -2
  20. package/engine/claude-md-context.js +195 -0
  21. package/engine/cli.js +9 -1
  22. package/engine/comment-format.js +51 -14
  23. package/engine/consolidation.js +47 -3
  24. package/engine/db/migrations/026-pre-dispatch-rejections.js +55 -0
  25. package/engine/dispatch.js +80 -23
  26. package/engine/execution-model.js +68 -0
  27. package/engine/gh-comment.js +6 -3
  28. package/engine/github.js +6 -7
  29. package/engine/lifecycle.js +195 -56
  30. package/engine/llm.js +59 -83
  31. package/engine/memory-store.js +3 -1
  32. package/engine/pipeline.js +147 -20
  33. package/engine/playbook.js +39 -6
  34. package/engine/pooled-agent-process.js +28 -26
  35. package/engine/prd-store.js +14 -6
  36. package/engine/quarantine-refs.js +33 -0
  37. package/engine/queries.js +8 -6
  38. package/engine/restart-health.js +69 -4
  39. package/engine/runtimes/claude.js +100 -25
  40. package/engine/runtimes/codex.js +19 -0
  41. package/engine/runtimes/contract.js +239 -0
  42. package/engine/runtimes/copilot.js +85 -8
  43. package/engine/runtimes/index.js +37 -9
  44. package/engine/shared.js +157 -64
  45. package/engine/spawn-agent.js +79 -113
  46. package/engine/spawn-phase-watchdog.js +8 -37
  47. package/engine/supervisor.js +72 -16
  48. package/engine/timeout.js +87 -16
  49. package/engine/untrusted-fence.js +2 -1
  50. package/engine.js +434 -125
  51. package/package.json +1 -1
  52. package/playbooks/fix.md +20 -0
  53. package/playbooks/review.md +2 -0
  54. package/skills/check-self-authored-review-comment/SKILL.md +34 -0
@@ -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 { stuck: false };
310
+ if (!itemId) return { exhausted: false };
311
311
  let wiScope;
312
312
  try { wiScope = lifecycle().resolveWorkItemScope(meta); } catch { wiScope = null; }
313
- if (!wiScope) return { stuck: false };
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 stuck = false;
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: ts(),
342
+ evaluatedAt,
336
343
  rejectCount,
337
344
  description: currentDescription,
345
+ history,
338
346
  };
339
347
  if (rejectCount >= maxRejections) {
340
- stuck = true;
341
- wi.status = WI_STATUS.FAILED;
342
- wi._failureClass = FAILURE_CLASS.PRE_DISPATCH_EVAL_STUCK;
343
- wi.failReason = `pre-dispatch-eval rejected this work item ${rejectCount} times in a row ` +
344
- `with an unchanged description — last reason: ${evaluation.reason || 'criteria not actionable'}`;
345
- wi.failedAt = ts();
346
- wi._pendingReason = 'pre_dispatch_eval_stuck';
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 (stuck) {
354
- log('warn', `pre-dispatch-eval: ${itemId} rejected ${maxRejections}+ times with unchanged description — flipped to failed (PRE_DISPATCH_EVAL_STUCK) instead of re-evaluating forever`);
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) return addToDispatch(item);
533
+ if (!evaluation || evaluation.valid !== false) {
534
+ _clearPreDispatchRejection(item, evaluation);
535
+ return addToDispatch(item);
536
+ }
481
537
 
482
- const { stuck } = _persistInvalidWorkItem(item, evaluation);
483
- // W-mrcrh5hj0017d22f: once the WI has been flipped to failed for
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 { processWorkItemFailure = true, processWorkItemSuccess = false, failureClass } = opts;
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
+ };
@@ -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. The downstream
286
- // `check-self-authored-review-comment` skill no-ops the dispatch, so this
287
- // is a slot/token tax rather than state corruption — but the frequency
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 (the downstream
1245
- // `check-self-authored-review-comment` skill no-ops them, so this is
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) {