@yemi33/minions 0.1.921 → 0.1.923

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,8 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.921 (2026-04-13)
3
+ ## 0.1.923 (2026-04-13)
4
4
 
5
5
  ### Features
6
+ - add /api/work-items/reopen endpoint and reopen-work-item CC action (#982)
6
7
  - CC tab unread dot + reopened badge on work items
7
8
  - fix dep re-merge failure on retry with existing commits (#977)
8
9
  - audit and harden log buffering implementation (#971)
@@ -18,6 +19,8 @@
18
19
  - add red dot notification on CC tab when response completes (#934) (#946)
19
20
 
20
21
  ### Fixes
22
+ - 429 retry on abort race applies to all sends, not just queued
23
+ - show actionable failure context instead of Unknown error (#1003)
21
24
  - prioritize error_max_turns over permission-blocked in classifyFailure (#1001)
22
25
  - optimistic archive hides both PRD and linked source plan simultaneously
23
26
  - align CC tab unread dot to same position as working dot
@@ -36,6 +39,8 @@
36
39
  - prohibit agents from self-merging their own PRs
37
40
 
38
41
  ### Other
42
+ - test(preflight): add unit tests for findClaudeBinary, runPreflight, printPreflight, checkOrExit (#953)
43
+ - test(meeting): add unit tests for key functions (#957)
39
44
  - [E2E] Architecture meeting Tier 1+2: 6 correctness bugs + 2 nested lock violations + log buffering (#972)
40
45
 
41
46
  ## 0.1.901 (2026-04-12)
@@ -538,12 +538,11 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
538
538
  });
539
539
 
540
540
  if (!res.ok) {
541
- // 429 = server still processing previous request (abort race) — retry silently up to 3 times
541
+ // 429 = server still releasing previous request (abort race) — retry silently up to 3 times
542
542
  if (res.status === 429 && (!activeTab._429retries || activeTab._429retries < 3)) {
543
543
  activeTab._429retries = (activeTab._429retries || 0) + 1;
544
- _cleanupStreamDiv();
545
544
  await new Promise(function(r) { setTimeout(r, 1500); });
546
- return await _ccDoSend(message, true);
545
+ return await _ccDoSend(message, true); // retry with skipUserMsg=true (already displayed)
547
546
  }
548
547
  activeTab._429retries = 0;
549
548
  _cleanupStreamDiv();
@@ -1122,6 +1121,13 @@ async function ccExecuteAction(action, targetTabId) {
1122
1121
  status.style.color = 'var(--green)';
1123
1122
  break;
1124
1123
  }
1124
+ case 'reopen-work-item': {
1125
+ await _ccFetch('/api/work-items/reopen', { id: action.id, project: action.project || action.source || '', description: action.description });
1126
+ status.innerHTML = '&#10003; Work item reopened: <strong>' + escHtml(action.id) + '</strong>';
1127
+ status.style.color = 'var(--green)';
1128
+ wakeEngine();
1129
+ break;
1130
+ }
1125
1131
  case 'reopen-prd-item': {
1126
1132
  await _ccFetch('/api/prd-items/update', { source: action.file, itemId: action.id, status: 'updated' });
1127
1133
  status.innerHTML = '&#10003; PRD item reopened: <strong>' + escHtml(action.id) + '</strong>';
package/dashboard.js CHANGED
@@ -23,7 +23,7 @@ const queries = require('./engine/queries');
23
23
  const teams = require('./engine/teams');
24
24
  const os = require('os');
25
25
 
26
- const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS } = shared;
26
+ const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, reopenWorkItem } = shared;
27
27
  const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
28
28
  getSkills, getInbox, getNotesWithMeta, getPullRequests,
29
29
  getEngineLog, getMetrics, getKnowledgeBaseEntries, timeSince,
@@ -616,6 +616,41 @@ async function executeCCActions(actions) {
616
616
  results.push({ type: 'knowledge', ok: true });
617
617
  break;
618
618
  }
619
+ case 'reopen-work-item': {
620
+ const project = action.project || '';
621
+ const targetProject = project ? PROJECTS.find(p => p.name?.toLowerCase() === project.toLowerCase()) : PROJECTS[0];
622
+ const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : path.join(MINIONS_DIR, 'work-items.json');
623
+ let reopenResult = null;
624
+ mutateJsonFileLocked(wiPath, items => {
625
+ if (!Array.isArray(items)) items = [];
626
+ const item = items.find(i => i.id === action.id);
627
+ if (!item) { reopenResult = { error: 'item not found' }; return items; }
628
+ if (item.status !== WI_STATUS.DONE && item.status !== WI_STATUS.FAILED && !DONE_STATUSES.has(item.status)) {
629
+ reopenResult = { error: 'can only reopen done or failed items' }; return items;
630
+ }
631
+ reopenWorkItem(item);
632
+ if (action.description) item.description = action.description;
633
+ reopenResult = { ok: true };
634
+ return items;
635
+ }, { defaultValue: [] });
636
+ if (reopenResult?.ok) {
637
+ // Clear dispatch history outside lock
638
+ const sourcePrefix = targetProject ? `work-${targetProject.name}-` : 'central-work-';
639
+ const dispatchKey = sourcePrefix + action.id;
640
+ try {
641
+ const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
642
+ mutateJsonFileLocked(dispatchPath, dispatch => {
643
+ dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
644
+ dispatch.completed = dispatch.completed.filter(d => d.meta?.dispatchKey !== dispatchKey);
645
+ dispatch.completed = dispatch.completed.filter(d => !d.meta?.parentKey || d.meta.parentKey !== dispatchKey);
646
+ return dispatch;
647
+ }, { defaultValue: { pending: [], active: [], completed: [] } });
648
+ } catch { /* best effort */ }
649
+ invalidateStatusCache();
650
+ }
651
+ results.push({ type: 'reopen-work-item', id: action.id, ...(reopenResult || { error: 'unexpected' }) });
652
+ break;
653
+ }
619
654
  default:
620
655
  // Server didn't handle — frontend must execute
621
656
  results.push({ type: action.type });
@@ -1358,6 +1393,66 @@ const server = http.createServer(async (req, res) => {
1358
1393
  } catch (e) { console.error('Archive fetch error:', e.message); return jsonReply(res, 500, { error: e.message }); }
1359
1394
  }
1360
1395
 
1396
+ async function handleWorkItemsReopen(req, res) {
1397
+ try {
1398
+ const body = await readBody(req);
1399
+ const { id, description } = body;
1400
+ const project = body.project || body.source;
1401
+ if (!id) return jsonReply(res, 400, { error: 'id required' });
1402
+
1403
+ // Find the right work-items file
1404
+ let wiPath;
1405
+ if (!project || project === 'central') {
1406
+ wiPath = path.join(MINIONS_DIR, 'work-items.json');
1407
+ } else {
1408
+ const proj = PROJECTS.find(p => p.name === project);
1409
+ if (proj) wiPath = shared.projectWorkItemsPath(proj);
1410
+ }
1411
+ if (!wiPath) return jsonReply(res, 404, { error: 'project not found' });
1412
+
1413
+ let result = null;
1414
+ mutateJsonFileLocked(wiPath, (items) => {
1415
+ if (!Array.isArray(items)) items = [];
1416
+ const item = items.find(i => i.id === id);
1417
+ if (!item) { result = { code: 404, body: { error: 'item not found' } }; return items; }
1418
+ if (item.status !== WI_STATUS.DONE && item.status !== WI_STATUS.FAILED && !DONE_STATUSES.has(item.status)) {
1419
+ result = { code: 400, body: { error: 'can only reopen done or failed items (current: ' + item.status + ')' } };
1420
+ return items;
1421
+ }
1422
+ reopenWorkItem(item);
1423
+ if (description !== undefined) item.description = description;
1424
+ result = { code: 200, body: { ok: true, item } };
1425
+ return items;
1426
+ });
1427
+ if (!result) return jsonReply(res, 500, { error: 'unexpected state' });
1428
+ if (result.code !== 200) return jsonReply(res, result.code, result.body);
1429
+
1430
+ // Clear dispatch history and cooldowns outside lock
1431
+ const sourcePrefix = (!project || project === 'central') ? 'central-work-' : `work-${project}-`;
1432
+ const dispatchKey = sourcePrefix + id;
1433
+ try {
1434
+ const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
1435
+ mutateJsonFileLocked(dispatchPath, (dispatch) => {
1436
+ dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
1437
+ dispatch.completed = dispatch.completed.filter(d => d.meta?.dispatchKey !== dispatchKey);
1438
+ dispatch.completed = dispatch.completed.filter(d => !d.meta?.parentKey || d.meta.parentKey !== dispatchKey);
1439
+ return dispatch;
1440
+ }, { defaultValue: { pending: [], active: [], completed: [] } });
1441
+ } catch (e) { console.error('dispatch cleanup on reopen:', e.message); }
1442
+ try {
1443
+ const cooldownPath = path.join(MINIONS_DIR, 'engine', 'cooldowns.json');
1444
+ const cooldowns = safeJsonObj(cooldownPath);
1445
+ if (cooldowns[dispatchKey]) {
1446
+ delete cooldowns[dispatchKey];
1447
+ safeWrite(cooldownPath, cooldowns);
1448
+ }
1449
+ } catch (e) { console.error('cooldown cleanup on reopen:', e.message); }
1450
+
1451
+ invalidateStatusCache();
1452
+ return jsonReply(res, result.code, result.body);
1453
+ } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1454
+ }
1455
+
1361
1456
  async function handleWorkItemsCreate(req, res) {
1362
1457
  try {
1363
1458
  const body = await readBody(req);
@@ -3895,6 +3990,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3895
3990
  { method: 'POST', path: '/api/work-items/delete', desc: 'Remove a work item, kill agent, clear dispatch', params: 'id, source?', handler: handleWorkItemsDelete },
3896
3991
  { method: 'POST', path: '/api/work-items/archive', desc: 'Move a completed/failed work item to archive', params: 'id, source?', handler: handleWorkItemsArchive },
3897
3992
  { method: 'GET', path: '/api/work-items/archive', desc: 'List archived work items', handler: handleWorkItemsArchiveList },
3993
+ { method: 'POST', path: '/api/work-items/reopen', desc: 'Reopen a done/failed work item back to pending', params: 'id, project?, description?', handler: handleWorkItemsReopen },
3898
3994
  { method: 'POST', path: '/api/work-items/feedback', desc: 'Add human feedback on completed work', params: 'id, rating, comment?', handler: async (req, res) => {
3899
3995
  const body = await readBody(req);
3900
3996
  const { id, source, rating, comment } = body;
@@ -172,9 +172,26 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
172
172
  }
173
173
  } catch (e) { log('warn', 'increment retry counter: ' + e.message); }
174
174
  } else {
175
+ // Human-readable labels for each failure class — used as fallback when reason is empty
176
+ const CLASS_LABELS = {
177
+ [FAILURE_CLASS.EMPTY_OUTPUT]: 'agent produced no output \u2014 likely crashed on startup',
178
+ [FAILURE_CLASS.BUILD_FAILURE]: 'build/test/lint failure in output',
179
+ [FAILURE_CLASS.MERGE_CONFLICT]: 'merge conflict',
180
+ [FAILURE_CLASS.MAX_TURNS]: 'reached max turn limit',
181
+ [FAILURE_CLASS.TIMEOUT]: 'timed out waiting for agent',
182
+ [FAILURE_CLASS.SPAWN_ERROR]: 'agent process failed to start',
183
+ [FAILURE_CLASS.NETWORK_ERROR]: 'network or API error',
184
+ [FAILURE_CLASS.OUT_OF_CONTEXT]: 'context window exhausted',
185
+ [FAILURE_CLASS.CONFIG_ERROR]: 'configuration error',
186
+ [FAILURE_CLASS.PERMISSION_BLOCKED]: 'permission or auth failure',
187
+ [FAILURE_CLASS.UNKNOWN]: 'unknown error',
188
+ };
189
+ const classLabel = failureClass ? (CLASS_LABELS[failureClass] || failureClass) : '';
190
+ const effectiveReason = reason || classLabel || 'Unknown error';
191
+ const classSuffix = failureClass ? ` [${failureClass.toUpperCase().replace(/-/g, '_')}]` : '';
175
192
  const finalReason = !retryableFailure
176
- ? `Non-retryable failure: ${reason || 'Unknown error'}`
177
- : (reason || `Failed after ${maxRetries} retries`);
193
+ ? `Non-retryable failure: ${effectiveReason}${classSuffix}`
194
+ : (reason || `Failed after ${maxRetries} retries${classSuffix}`);
178
195
  lifecycle().updateWorkItemStatus(item.meta, WI_STATUS.FAILED, finalReason);
179
196
  // Alert: find items blocked by this failure and write inbox note
180
197
  try {
package/engine.js CHANGED
@@ -1141,7 +1141,12 @@ async function spawnAgent(dispatchItem, config) {
1141
1141
  // autoRecovered: agent failed (e.g. heartbeat timeout) but created PRs — treat as success
1142
1142
  const effectiveResult = (code === 0 || autoRecovered) ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR;
1143
1143
  const completeOpts = effectiveResult === DISPATCH_RESULT.ERROR && failureClass ? { failureClass } : {};
1144
- completeDispatch(id, effectiveResult, '', resultSummary, completeOpts);
1144
+ // Extract last 5 non-empty stderr lines as error context when exit code is non-zero
1145
+ let errorReason = '';
1146
+ if (effectiveResult === DISPATCH_RESULT.ERROR) {
1147
+ errorReason = stderr.split('\n').filter(l => l.trim()).slice(-5).join(' | ').trim().slice(0, 300);
1148
+ }
1149
+ completeDispatch(id, effectiveResult, errorReason, resultSummary, completeOpts);
1145
1150
 
1146
1151
  // Cleanup temp files (including PID file now that dispatch is complete)
1147
1152
  try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.921",
3
+ "version": "0.1.923",
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"
@@ -76,7 +76,7 @@ Additional actions (all take `id` or `file` as primary key):
76
76
  - Schedules: schedule (id, title, cron, workType, project, agent, description, priority, enabled), delete-schedule (id)
77
77
  - Pipelines: create-pipeline (id, title, stages[], trigger, stopWhen, monitoredResources), edit-pipeline (id, title, stages, trigger), delete-pipeline (id), trigger-pipeline (id), abort-pipeline (id), retrigger-pipeline (id)
78
78
  - Meetings: add-meeting-note (id, note), advance-meeting (id), end-meeting (id), archive-meeting (id), unarchive-meeting (id), delete-meeting (id)
79
- - Work item ops: delete-work-item (id, source), archive-work-item (id), work-item-feedback (id, rating: up/down, comment)
79
+ - Work item ops: delete-work-item (id, source), archive-work-item (id), work-item-feedback (id, rating: up/down, comment), reopen-work-item (id, project[, description] — reopen a done/failed item back to pending)
80
80
  - PRD ops: edit-prd-item, remove-prd-item, reopen-prd-item (id, file — re-dispatches on existing branch)
81
81
  - KB/Inbox: promote-to-kb (file, category), kb-sweep, toggle-kb-pin (key)
82
82
  - Plan lifecycle: revise-plan (file, feedback — dispatches agent to revise)