@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 +6 -1
- package/dashboard/js/command-center.js +9 -3
- package/dashboard.js +97 -1
- package/engine/dispatch.js +19 -2
- package/engine.js +6 -1
- package/package.json +1 -1
- package/prompts/cc-system.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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
|
|
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 = '✓ 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 = '✓ 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;
|
package/engine/dispatch.js
CHANGED
|
@@ -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: ${
|
|
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
|
-
|
|
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.
|
|
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"
|
package/prompts/cc-system.md
CHANGED
|
@@ -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)
|