@yemi33/minions 0.1.1673 → 0.1.1674

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1674 (2026-05-02)
4
+
5
+ ### Other
6
+ - refactor: collapse needs-human-review into failed + simplify migration paths
7
+
3
8
  ## 0.1.1673 (2026-05-02)
4
9
 
5
10
  ### Fixes
@@ -30,7 +30,7 @@ function wiRetryBtn(item) {
30
30
 
31
31
  function wiRow(item) {
32
32
  const statusBadge = (s) => {
33
- const cls = s === 'failed' ? 'rejected' : s === 'needs-human-review' ? 'needs-review' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : s === 'decomposed' ? 'approved' : 'draft';
33
+ const cls = s === 'failed' ? 'rejected' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : s === 'decomposed' ? 'approved' : 'draft';
34
34
  return '<span class="pr-badge ' + cls + '">' + escapeHtml(s) + '</span>';
35
35
  };
36
36
  const typeBadge = (t) => '<span class="dispatch-type ' + (t || 'implement') + '">' + escapeHtml(t || 'implement') + '</span>';
@@ -67,10 +67,10 @@ function wiRow(item) {
67
67
  (item.acceptanceCriteria && item.acceptanceCriteria.length ? '<span title="' + item.acceptanceCriteria.length + ' acceptance criteria">&#x2611;' + item.acceptanceCriteria.length + '</span>' : '') +
68
68
  '</td>' +
69
69
  '<td style="white-space:nowrap">' +
70
- ((item.status === 'pending' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Edit work item">&#x270E;</button>' : '') +
71
- ((item.status === 'done' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Archive work item">&#x1F4E6;</button>' : '') +
72
- ((item.status === 'done' || item.status === 'failed' || item.status === 'needs-human-review') && !item._humanFeedback ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Give feedback">&#x1F44D;&#x1F44E;</button>' : (item._humanFeedback ? '<span style="font-size:9px" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '&#x1F44D;' : '&#x1F44E;') + '</span> ' : '')) +
73
- ((item.status === 'pending' || item.status === 'dispatched' || item.status === 'queued' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--orange);border-color:var(--orange);margin-right:4px" onclick="event.stopPropagation();cancelWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Cancel work item and kill agent">&#x1F6AB;</button>' : '') +
70
+ ((item.status === 'pending' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Edit work item">&#x270E;</button>' : '') +
71
+ ((item.status === 'done' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Archive work item">&#x1F4E6;</button>' : '') +
72
+ ((item.status === 'done' || item.status === 'failed') && !item._humanFeedback ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Give feedback">&#x1F44D;&#x1F44E;</button>' : (item._humanFeedback ? '<span style="font-size:9px" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '&#x1F44D;' : '&#x1F44E;') + '</span> ' : '')) +
73
+ ((item.status === 'pending' || item.status === 'dispatched' || item.status === 'queued' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--orange);border-color:var(--orange);margin-right:4px" onclick="event.stopPropagation();cancelWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Cancel work item and kill agent">&#x1F6AB;</button>' : '') +
74
74
  '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Delete work item and kill agent">&#x2715;</button>' +
75
75
  '</td>' +
76
76
  '</tr>';
@@ -79,7 +79,7 @@ function wiRow(item) {
79
79
  function renderWorkItems(items) {
80
80
  items = items.filter(function(w) { return !isDeleted('wi:' + w.id); });
81
81
  // Sort: active/dispatched first, then by most recent activity
82
- const statusOrder = { dispatched: 0, pending: 1, queued: 1, 'needs-human-review': 2, failed: 2, done: 3 };
82
+ const statusOrder = { dispatched: 0, pending: 1, queued: 1, failed: 2, done: 3 };
83
83
  items.sort((a, b) => {
84
84
  const sa = statusOrder[a.status] ?? 2, sb = statusOrder[b.status] ?? 2;
85
85
  if (sa !== sb) return sa - sb;
@@ -187,7 +187,7 @@ The agent must not write the file in pieces. Empty, truncated, or malformed JSON
187
187
  | `partial` | Some progress; agent ran out of turns or hit a known stop point | Auto-retry per `RECOVERY_RECIPES` (`engine/recovery.js`) |
188
188
  | `failed` | Hard failure; no recovery attempted by agent | Use `failure.class` to pick recipe |
189
189
  | `noop` | Idempotent bail (review already posted, plan already shipped, etc.) | Mark WI `done` without retry, no failure metric |
190
- | `needs-review` | Agent could not classify; flag for human | Set WI `needs-human-review` |
190
+ | `needs-review` | Agent could not classify; flag for human | Set WI `failed` with an explicit `failReason` |
191
191
 
192
192
  `noop` collapses the current `isReviewBailout` (lifecycle.js:907), the `verify-plan-already-shipped` family of skills, and the "shared-branch redispatch" skill into a single explicit signal. Any agent that detects "the work is already done" returns `status: "noop"` and a one-line `summary` — the engine takes the success path without retry.
193
193
 
@@ -370,7 +370,7 @@ The current ` ```completion ` block in `playbooks/fix.md:85-93` and `playbooks/i
370
370
 
371
371
  ### 7.3 No-PR Tasks
372
372
 
373
- `explore`, `ask`, `test`, `docs`, `plan-to-prd`, and the read-only legs of `meeting-*` simply omit `prs[]`. They still write completion.json with `status` + `summary`. This makes "I had nothing to push" an explicit signal instead of inferred from "no PR URL found in stdout" (which today triggers the auto-retry-then-needs-review chain at `lifecycle.js:1943-1984`).
373
+ `explore`, `ask`, `test`, `docs`, `plan-to-prd`, and the read-only legs of `meeting-*` simply omit `prs[]`. They still write completion.json with `status` + `summary`. This makes "I had nothing to push" an explicit signal instead of inferred from "no PR URL found in stdout" (which today triggers the auto-retry-then-failed chain at `lifecycle.js:1943-1984`).
374
374
 
375
375
  ## 8. Validation & Testing
376
376
 
package/engine/cleanup.js CHANGED
@@ -507,48 +507,39 @@ function runCleanup(config, verbose = false) {
507
507
  } catch (e) { log('warn', 'reconcile failed-with-PR: ' + e.message); }
508
508
  }
509
509
 
510
- // 6b. Migrate legacy work-item statuses to canonical 'done'
511
- // in-pr, implemented, complete → done (one-time correction per item)
510
+ // 6b. Migrate legacy work-item statuses to canonical replacements
511
+ // in-pr, implemented, complete → done; needs-human-review failed
512
512
  const LEGACY_DONE_ALIASES = new Set(['in-pr', 'implemented', 'complete']);
513
- for (const project of projects) {
513
+ const LEGACY_NEEDS_REVIEW_STATUS = 'needs-human-review';
514
+ const LEGACY_NEEDS_REVIEW_FAIL_REASON = 'Manual intervention required (migrated from needs-human-review)';
515
+ function _migrateLegacyItem(item) {
516
+ if (LEGACY_DONE_ALIASES.has(item.status)) {
517
+ item.status = shared.WI_STATUS.DONE;
518
+ delete item._retryCount;
519
+ delete item._pendingReason;
520
+ if (!item.completedAt) item.completedAt = shared.ts();
521
+ return true;
522
+ }
523
+ if (item.status === LEGACY_NEEDS_REVIEW_STATUS) {
524
+ item.status = shared.WI_STATUS.FAILED;
525
+ if (!item.failReason) item.failReason = LEGACY_NEEDS_REVIEW_FAIL_REASON;
526
+ if (!item.failedAt) item.failedAt = shared.ts();
527
+ delete item.completedAt;
528
+ return true;
529
+ }
530
+ return false;
531
+ }
532
+ function _migrateLegacyItemsAt(wiPath, label) {
514
533
  try {
515
- const wiPath = projectWorkItemsPath(project);
516
534
  let migrated = 0;
517
- mutateWorkItems(wiPath, items => {
518
- for (const item of items) {
519
- if (LEGACY_DONE_ALIASES.has(item.status)) {
520
- item.status = shared.WI_STATUS.DONE;
521
- delete item._retryCount;
522
- delete item._pendingReason;
523
- if (!item.completedAt) item.completedAt = shared.ts();
524
- migrated++;
525
- }
526
- }
527
- });
528
- if (migrated > 0) {
529
- log('info', `Migrated ${migrated} legacy status(es) → done in ${project.name} work items`);
530
- }
531
- } catch (e) { log('warn', 'migrate legacy statuses: ' + e.message); }
535
+ mutateWorkItems(wiPath, items => {
536
+ for (const item of items) if (_migrateLegacyItem(item)) migrated++;
537
+ });
538
+ if (migrated > 0) log('info', `Migrated ${migrated} legacy status(es) in ${label}`);
539
+ } catch (e) { log('warn', `migrate legacy statuses (${label}): ${e.message}`); }
532
540
  }
533
- // Central work items
534
- try {
535
- const centralPath = path.join(MINIONS_DIR, 'work-items.json');
536
- let migrated = 0;
537
- mutateWorkItems(centralPath, items => {
538
- for (const item of items) {
539
- if (LEGACY_DONE_ALIASES.has(item.status)) {
540
- item.status = shared.WI_STATUS.DONE;
541
- delete item._retryCount;
542
- delete item._pendingReason;
543
- if (!item.completedAt) item.completedAt = shared.ts();
544
- migrated++;
545
- }
546
- }
547
- });
548
- if (migrated > 0) {
549
- log('info', `Migrated ${migrated} legacy status(es) → done in central work items`);
550
- }
551
- } catch (e) { log('warn', 'migrate central legacy statuses: ' + e.message); }
541
+ for (const project of projects) _migrateLegacyItemsAt(projectWorkItemsPath(project), `${project.name} work items`);
542
+ _migrateLegacyItemsAt(path.join(MINIONS_DIR, 'work-items.json'), 'central work items');
552
543
 
553
544
  // 6c. Strip stale retry metadata from completed work items
554
545
  cleaned.doneRetryCounts = 0;
@@ -591,18 +582,23 @@ function runCleanup(config, verbose = false) {
591
582
  const prdFiles = prdDirEntries.filter(f => f.endsWith('.json'));
592
583
  for (const pf of prdFiles) {
593
584
  const prdPath = path.join(PRD_DIR, pf);
594
- const prd = safeJson(prdPath);
595
- if (!prd?.missing_features) continue;
596
585
  let migrated = 0;
597
- for (const feat of prd.missing_features) {
598
- if (LEGACY_DONE_ALIASES.has(feat.status)) {
599
- feat.status = shared.WI_STATUS.DONE;
600
- migrated++;
586
+ shared.withFileLock(`${prdPath}.lock`, () => {
587
+ const prd = safeJson(prdPath);
588
+ if (!prd?.missing_features) return;
589
+ for (const feat of prd.missing_features) {
590
+ if (LEGACY_DONE_ALIASES.has(feat.status)) {
591
+ feat.status = shared.WI_STATUS.DONE;
592
+ migrated++;
593
+ } else if (feat.status === LEGACY_NEEDS_REVIEW_STATUS) {
594
+ feat.status = shared.WI_STATUS.FAILED;
595
+ migrated++;
596
+ }
601
597
  }
602
- }
598
+ if (migrated > 0) safeWrite(prdPath, prd);
599
+ });
603
600
  if (migrated > 0) {
604
- safeWrite(prdPath, prd);
605
- log('info', `Migrated ${migrated} legacy PRD item status(es) → done in ${pf}`);
601
+ log('info', `Migrated ${migrated} legacy PRD item status(es) in ${pf}`);
606
602
  }
607
603
  }
608
604
  } catch (e) { log('warn', 'migrate PRD legacy statuses: ' + e.message); }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-02T04:13:14.794Z"
4
+ "cachedAt": "2026-05-02T04:24:34.949Z"
5
5
  }
@@ -637,14 +637,21 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
637
637
  const files = sourcePlan ? [sourcePlan] : require('fs').readdirSync(prdDir).filter(f => f.endsWith('.json'));
638
638
  for (const pf of files) {
639
639
  const fpath = path.join(prdDir, pf);
640
+ // Lock-free peek: most PRDs won't contain the ID, so skip the lock cost.
640
641
  const plan = safeJson(fpath);
641
- if (!plan?.missing_features) continue;
642
- const feature = plan.missing_features.find(f => f.id === itemId);
643
- if (feature && feature.status !== status) {
644
- feature.status = status;
645
- shared.safeWrite(fpath, plan);
646
- return;
647
- }
642
+ const feature = plan?.missing_features?.find(f => f.id === itemId);
643
+ if (!feature || feature.status === status) continue;
644
+ let updated = false;
645
+ shared.withFileLock(`${fpath}.lock`, () => {
646
+ const fresh = safeJson(fpath);
647
+ const f = fresh?.missing_features?.find(x => x.id === itemId);
648
+ if (f && f.status !== status) {
649
+ f.status = status;
650
+ safeWrite(fpath, fresh);
651
+ updated = true;
652
+ }
653
+ });
654
+ if (updated) return;
648
655
  }
649
656
  } catch (err) { log('warn', `PRD status sync: ${err.message}`); }
650
657
  }
@@ -947,18 +954,19 @@ function isPrAttachmentRequired(type, item, meta = {}) {
947
954
  if (branchStrategy === 'shared-branch' && item.itemType !== 'pr' && !explicit) return false;
948
955
 
949
956
  // Fix/test work items dispatched against an existing PR don't produce a new
950
- // PR — the agent updates meta.pr in place. Only require fresh PR attachment
951
- // when there's NO existing PR to operate on (rare: standalone fix dispatched
952
- // from CC without a PR target). The meta.pr short-circuit beats the
953
- // explicit-flag fallthrough so a legacy requiresPr:true fix doesn't trigger
954
- // the contract when there's already a PR attached.
957
+ // PR — the agent updates meta.pr in place. The meta.pr short-circuit beats
958
+ // the explicit-flag fallthrough so a legacy requiresPr:true fix doesn't
959
+ // trigger the contract when there's already a PR attached.
955
960
  if ((type === WORK_TYPE.FIX || type === WORK_TYPE.TEST) && meta?.pr) return false;
956
961
 
962
+ // Standalone test work is usually pure build/run/verify. It should only be
963
+ // PR-required when the caller explicitly marks it as file-changing work.
964
+ if (type === WORK_TYPE.TEST && !explicit) return false;
965
+
957
966
  return explicit
958
967
  || type === WORK_TYPE.IMPLEMENT
959
968
  || type === WORK_TYPE.IMPLEMENT_LARGE
960
- || type === WORK_TYPE.FIX
961
- || type === WORK_TYPE.TEST;
969
+ || type === WORK_TYPE.FIX;
962
970
  }
963
971
 
964
972
  function readOptionalJsonStrict(filePath, label, validate) {
@@ -1148,18 +1156,19 @@ function _outputContainsPrUrl(output) {
1148
1156
  function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity) {
1149
1157
  const noPrWiPath = resolveWorkItemPath(meta);
1150
1158
  const isHard = severity !== 'soft';
1151
- let syncNeedsReviewToPrd = false;
1159
+ let syncFailedToPrd = false;
1152
1160
  if (noPrWiPath) {
1153
1161
  mutateJsonFileLocked(noPrWiPath, data => {
1154
1162
  if (!Array.isArray(data)) return data;
1155
1163
  const w = data.find(i => i.id === meta.item.id);
1156
1164
  if (!w) return data;
1157
1165
  if (isHard) {
1158
- w.status = WI_STATUS.NEEDS_REVIEW;
1166
+ w.status = WI_STATUS.FAILED;
1159
1167
  w._missingPrAttachment = true;
1160
1168
  w.failReason = reason;
1169
+ w.failedAt = ts();
1161
1170
  w._lastReviewReason = reason;
1162
- syncNeedsReviewToPrd = !!meta.item?.sourcePlan;
1171
+ syncFailedToPrd = !!meta.item?.sourcePlan;
1163
1172
  delete w.completedAt;
1164
1173
  delete w._noPr;
1165
1174
  delete w._noPrReason;
@@ -1173,8 +1182,8 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity)
1173
1182
  return data;
1174
1183
  }, { skipWriteIfUnchanged: true });
1175
1184
  }
1176
- if (isHard && syncNeedsReviewToPrd) {
1177
- syncPrdItemStatus(meta.item.id, WI_STATUS.NEEDS_REVIEW, meta.item.sourcePlan);
1185
+ if (isHard && syncFailedToPrd) {
1186
+ syncPrdItemStatus(meta.item.id, WI_STATUS.FAILED, meta.item.sourcePlan);
1178
1187
  }
1179
1188
  if (isHard) {
1180
1189
  shared.writeToInbox('engine', `missing-pr-attachment-${meta.item.id}`,
@@ -1206,25 +1215,26 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity)
1206
1215
 
1207
1216
  function markPrAttachmentVerificationError(meta, agentId, reason, resultSummary) {
1208
1217
  const wiPath = resolveWorkItemPath(meta);
1209
- let syncNeedsReviewToPrd = false;
1218
+ let syncFailedToPrd = false;
1210
1219
  if (wiPath) {
1211
1220
  mutateJsonFileLocked(wiPath, data => {
1212
1221
  if (!Array.isArray(data)) return data;
1213
1222
  const w = data.find(i => i.id === meta.item.id);
1214
1223
  if (!w) return data;
1215
- w.status = WI_STATUS.NEEDS_REVIEW;
1224
+ w.status = WI_STATUS.FAILED;
1216
1225
  w._prAttachmentStateError = true;
1217
1226
  w.failReason = reason;
1227
+ w.failedAt = ts();
1218
1228
  w._lastReviewReason = reason;
1219
- syncNeedsReviewToPrd = !!meta.item?.sourcePlan;
1229
+ syncFailedToPrd = !!meta.item?.sourcePlan;
1220
1230
  delete w.completedAt;
1221
1231
  delete w._missingPrAttachment;
1222
1232
  delete w._unverifiedPrAttachment;
1223
1233
  return data;
1224
1234
  }, { skipWriteIfUnchanged: true });
1225
1235
  }
1226
- if (syncNeedsReviewToPrd) {
1227
- syncPrdItemStatus(meta.item.id, WI_STATUS.NEEDS_REVIEW, meta.item.sourcePlan);
1236
+ if (syncFailedToPrd) {
1237
+ syncPrdItemStatus(meta.item.id, WI_STATUS.FAILED, meta.item.sourcePlan);
1228
1238
  }
1229
1239
  shared.writeToInbox('engine', `pr-attachment-state-error-${meta.item.id}`,
1230
1240
  `# PR attachment verification blocked for ${meta.item.id}\n\n` +
package/engine/shared.js CHANGED
@@ -1089,7 +1089,7 @@ function runtimeConfigWarnings(config, registeredRuntimes) {
1089
1089
 
1090
1090
  const WI_STATUS = {
1091
1091
  PENDING: 'pending', DISPATCHED: 'dispatched', DONE: 'done', FAILED: 'failed',
1092
- PAUSED: 'paused', QUEUED: 'queued', NEEDS_REVIEW: 'needs-human-review',
1092
+ PAUSED: 'paused', QUEUED: 'queued',
1093
1093
  DECOMPOSED: 'decomposed', CANCELLED: 'cancelled',
1094
1094
  };
1095
1095
  // Read-side: accept legacy aliases for backward compat with old data/clients.
package/engine.js CHANGED
@@ -100,6 +100,8 @@ const mutateWorkItems = shared.mutateWorkItems;
100
100
  const mutatePullRequests = shared.mutatePullRequests;
101
101
  const withFileLock = shared.withFileLock;
102
102
 
103
+ const CHECKPOINT_CAP_FAIL_REASON = 'Exceeded 3 checkpoint-resumes; manual intervention required';
104
+
103
105
  // ─── Dispatch Management (extracted to engine/dispatch.js) ───────────────────
104
106
 
105
107
  const { mutateDispatch, addToDispatch, isRetryableFailureReason, completeDispatch,
@@ -2789,10 +2791,16 @@ function discoverFromWorkItems(config, project) {
2789
2791
  const promptAgentId = deferredAgentResolution ? reservedAgentId : agentId;
2790
2792
  const promptResult = renderProjectWorkItemPromptForAgent(item, workType, promptAgentId, config, project, root, branchName);
2791
2793
  if (promptResult.needsReview) {
2792
- log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
2793
- item.status = WI_STATUS.NEEDS_REVIEW;
2794
+ log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as failed for manual intervention`);
2795
+ item.status = WI_STATUS.FAILED;
2796
+ item.failReason = CHECKPOINT_CAP_FAIL_REASON;
2797
+ item.failedAt = ts();
2794
2798
  item._checkpointCount = promptResult.checkpointCount;
2795
2799
  needsWrite = true;
2800
+ if (item.sourcePlan) {
2801
+ try { syncPrdItemStatus(item.id, WI_STATUS.FAILED, item.sourcePlan); }
2802
+ catch (e) { log('warn', `PRD status sync after checkpoint cap (${item.id}): ${e.message}`); }
2803
+ }
2796
2804
  continue;
2797
2805
  }
2798
2806
  if (promptResult.checkpointCount !== null) {
@@ -3305,8 +3313,17 @@ function discoverCentralWorkItems(config) {
3305
3313
  workType,
3306
3314
  });
3307
3315
  if (cpResult.needsReview) {
3308
- log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
3309
- mutations.set(item.id, { status: WI_STATUS.NEEDS_REVIEW, _checkpointCount: cpResult.checkpointCount });
3316
+ log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as failed for manual intervention`);
3317
+ mutations.set(item.id, {
3318
+ status: WI_STATUS.FAILED,
3319
+ failReason: CHECKPOINT_CAP_FAIL_REASON,
3320
+ failedAt: ts(),
3321
+ _checkpointCount: cpResult.checkpointCount,
3322
+ });
3323
+ if (item.sourcePlan) {
3324
+ try { syncPrdItemStatus(item.id, WI_STATUS.FAILED, item.sourcePlan); }
3325
+ catch (e) { log('warn', `PRD status sync after checkpoint cap (${item.id}): ${e.message}`); }
3326
+ }
3310
3327
  continue;
3311
3328
  }
3312
3329
  if (cpResult.checkpointCount !== null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1673",
3
+ "version": "0.1.1674",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"