@yemi33/minions 0.1.920 → 0.1.922
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 +5 -1
- package/dashboard/js/command-center.js +7 -0
- package/dashboard.js +97 -1
- package/engine/dispatch.js +19 -2
- package/engine/lifecycle.js +8 -6
- 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.922 (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
|
+
- show actionable failure context instead of Unknown error (#1003)
|
|
23
|
+
- prioritize error_max_turns over permission-blocked in classifyFailure (#1001)
|
|
21
24
|
- optimistic archive hides both PRD and linked source plan simultaneously
|
|
22
25
|
- align CC tab unread dot to same position as working dot
|
|
23
26
|
- pipeline artifact links use pushModalBack instead of setTimeout race (#993)
|
|
@@ -35,6 +38,7 @@
|
|
|
35
38
|
- prohibit agents from self-merging their own PRs
|
|
36
39
|
|
|
37
40
|
### Other
|
|
41
|
+
- test(meeting): add unit tests for key functions (#957)
|
|
38
42
|
- [E2E] Architecture meeting Tier 1+2: 6 correctness bugs + 2 nested lock violations + log buffering (#972)
|
|
39
43
|
|
|
40
44
|
## 0.1.901 (2026-04-12)
|
|
@@ -1122,6 +1122,13 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1122
1122
|
status.style.color = 'var(--green)';
|
|
1123
1123
|
break;
|
|
1124
1124
|
}
|
|
1125
|
+
case 'reopen-work-item': {
|
|
1126
|
+
await _ccFetch('/api/work-items/reopen', { id: action.id, project: action.project || action.source || '', description: action.description });
|
|
1127
|
+
status.innerHTML = '✓ Work item reopened: <strong>' + escHtml(action.id) + '</strong>';
|
|
1128
|
+
status.style.color = 'var(--green)';
|
|
1129
|
+
wakeEngine();
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1125
1132
|
case 'reopen-prd-item': {
|
|
1126
1133
|
await _ccFetch('/api/prd-items/update', { source: action.file, itemId: action.id, status: 'updated' });
|
|
1127
1134
|
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/lifecycle.js
CHANGED
|
@@ -1910,6 +1910,14 @@ function classifyFailure(code, stdout = '', stderr = '') {
|
|
|
1910
1910
|
// Exit code 78 — configuration error (Claude CLI not found, bad setup)
|
|
1911
1911
|
if (code === 78) return FAILURE_CLASS.CONFIG_ERROR;
|
|
1912
1912
|
|
|
1913
|
+
// Max turns exhausted (error_max_turns) — definitive stop reason, retryable
|
|
1914
|
+
// Must be checked FIRST — hook startup failures (e.g. curl exit code 28) can inject
|
|
1915
|
+
// permission/auth text into stderr, but if the agent ran to turn exhaustion that's the
|
|
1916
|
+
// real cause. Checked before PERMISSION_BLOCKED and OUT_OF_CONTEXT.
|
|
1917
|
+
if (/error_max_turns|"subtype"\s*:\s*"error_max_turns"|terminal_reason.*max_turns|max.*turns.*reached/i.test(combined)) {
|
|
1918
|
+
return FAILURE_CLASS.MAX_TURNS;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1913
1921
|
// Permission / trust / auth failures
|
|
1914
1922
|
if (/permission denied|access denied|unauthorized|403 forbidden|trust.*blocked|auth.*fail/i.test(combined)) {
|
|
1915
1923
|
return FAILURE_CLASS.PERMISSION_BLOCKED;
|
|
@@ -1920,12 +1928,6 @@ function classifyFailure(code, stdout = '', stderr = '') {
|
|
|
1920
1928
|
return FAILURE_CLASS.MERGE_CONFLICT;
|
|
1921
1929
|
}
|
|
1922
1930
|
|
|
1923
|
-
// Max turns exhausted (error_max_turns) — work in progress, retryable
|
|
1924
|
-
// Must be checked BEFORE OUT_OF_CONTEXT to avoid misclassification as non-retryable
|
|
1925
|
-
if (/error_max_turns|"subtype"\s*:\s*"error_max_turns"|terminal_reason.*max_turns|max.*turns.*reached/i.test(combined)) {
|
|
1926
|
-
return FAILURE_CLASS.MAX_TURNS;
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
1931
|
// Context window exhausted (token limit, context length — NOT max turns)
|
|
1930
1932
|
if (/context window|token limit|conversation.*too long|context.*length.*exceeded/i.test(combined)) {
|
|
1931
1933
|
return FAILURE_CLASS.OUT_OF_CONTEXT;
|
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.922",
|
|
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)
|