@yemi33/minions 0.1.315 → 0.1.317
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 +4 -1
- package/engine/cleanup.js +1 -1
- package/engine/dispatch.js +13 -11
- package/engine/lifecycle.js +31 -25
- package/engine/shared.js +22 -0
- package/engine/timeout.js +12 -10
- package/engine.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.317 (2026-04-03)
|
|
4
4
|
|
|
5
5
|
### Fixes
|
|
6
|
+
- deduplicate PRs in pull-requests.json on write
|
|
6
7
|
- show reviewer names in dashboard Signed Off By column
|
|
7
8
|
|
|
8
9
|
### Other
|
|
10
|
+
- refactor: use constants in lifecycle.js and timeout.js
|
|
11
|
+
- refactor: extract status/type/result constants to shared.js
|
|
9
12
|
- cleanup: remove evaluate.md (re-created by agents), fix stale references
|
|
10
13
|
- perf: CC message handling — debounce localStorage, cap array, batch scroll
|
|
11
14
|
|
package/engine/cleanup.js
CHANGED
|
@@ -433,7 +433,7 @@ function runCleanup(config, verbose = false) {
|
|
|
433
433
|
|
|
434
434
|
// 6. Migrate legacy work-item statuses to canonical values
|
|
435
435
|
// in-pr, implemented, complete → done (one-time correction per item)
|
|
436
|
-
const LEGACY_DONE_STATUSES =
|
|
436
|
+
const LEGACY_DONE_STATUSES = shared.DONE_STATUSES;
|
|
437
437
|
for (const project of projects) {
|
|
438
438
|
try {
|
|
439
439
|
const wiPath = projectWorkItemsPath(project);
|
package/engine/dispatch.js
CHANGED
|
@@ -10,7 +10,8 @@ const queries = require('./queries');
|
|
|
10
10
|
const { setCooldownFailure } = require('./cooldown');
|
|
11
11
|
|
|
12
12
|
const { safeJson, safeWrite, safeReadDir, mutateJsonFileLocked,
|
|
13
|
-
getProjects, projectWorkItemsPath, log, ts, dateStamp
|
|
13
|
+
getProjects, projectWorkItemsPath, log, ts, dateStamp,
|
|
14
|
+
WI_STATUS, DISPATCH_RESULT, ENGINE_DEFAULTS } = shared;
|
|
14
15
|
const { getConfig, getDispatch, DISPATCH_PATH, INBOX_DIR } = queries;
|
|
15
16
|
|
|
16
17
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
@@ -65,7 +66,7 @@ function isRetryableFailureReason(reason = '') {
|
|
|
65
66
|
|
|
66
67
|
// ─── Complete Dispatch ───────────────────────────────────────────────────────
|
|
67
68
|
|
|
68
|
-
function completeDispatch(id, result =
|
|
69
|
+
function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', resultSummary = '', opts = {}) {
|
|
69
70
|
const { processWorkItemFailure = true } = opts;
|
|
70
71
|
let item = null;
|
|
71
72
|
|
|
@@ -98,9 +99,9 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
|
|
|
98
99
|
|
|
99
100
|
// Update source work item status on failure + auto-retry with backoff
|
|
100
101
|
const retryableFailure = isRetryableFailureReason(reason);
|
|
101
|
-
if (result ===
|
|
102
|
+
if (result === DISPATCH_RESULT.ERROR && item.meta?.dispatchKey && retryableFailure) setCooldownFailure(item.meta.dispatchKey);
|
|
102
103
|
|
|
103
|
-
if (processWorkItemFailure && result ===
|
|
104
|
+
if (processWorkItemFailure && result === DISPATCH_RESULT.ERROR && item.meta?.item?.id) {
|
|
104
105
|
let retries = (item.meta.item._retryCount || 0);
|
|
105
106
|
try {
|
|
106
107
|
const wiPath = item.meta.source === 'central-work-item' || item.meta.source === 'central-work-item-fanout'
|
|
@@ -112,8 +113,9 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
|
|
|
112
113
|
if (wi) retries = wi._retryCount || 0;
|
|
113
114
|
}
|
|
114
115
|
} catch (e) { log('warn', 'read retry count: ' + e.message); }
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
const maxRetries = ENGINE_DEFAULTS.maxRetries;
|
|
117
|
+
if (retryableFailure && retries < maxRetries) {
|
|
118
|
+
log('info', `Dispatch error for ${item.meta.item.id} — auto-retry ${retries + 1}/${maxRetries}`);
|
|
117
119
|
lifecycle().updateWorkItemStatus(item.meta, 'pending', '');
|
|
118
120
|
// Remove this dispatch key from completed so dedupe doesn't block immediate redispatch.
|
|
119
121
|
if (item.meta?.dispatchKey) {
|
|
@@ -132,9 +134,9 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
|
|
|
132
134
|
if (wiPath) {
|
|
133
135
|
const items = safeJson(wiPath) || [];
|
|
134
136
|
const wi = items.find(i => i.id === item.meta.item.id);
|
|
135
|
-
if (wi && wi.status !==
|
|
137
|
+
if (wi && wi.status !== WI_STATUS.PAUSED) {
|
|
136
138
|
wi._retryCount = retries + 1;
|
|
137
|
-
wi.status =
|
|
139
|
+
wi.status = WI_STATUS.PENDING;
|
|
138
140
|
wi._lastRetryReason = reason || '';
|
|
139
141
|
wi._lastRetryAt = ts();
|
|
140
142
|
delete wi.failReason;
|
|
@@ -148,7 +150,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
|
|
|
148
150
|
} else {
|
|
149
151
|
const finalReason = !retryableFailure
|
|
150
152
|
? `Non-retryable failure: ${reason || 'Unknown error'}`
|
|
151
|
-
: (reason ||
|
|
153
|
+
: (reason || `Failed after ${maxRetries} retries`);
|
|
152
154
|
lifecycle().updateWorkItemStatus(item.meta, 'failed', finalReason);
|
|
153
155
|
// Alert: find items blocked by this failure and write inbox note
|
|
154
156
|
try {
|
|
@@ -157,11 +159,11 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
|
|
|
157
159
|
const blockedItems = [];
|
|
158
160
|
for (const p of getProjects(config)) {
|
|
159
161
|
const items = safeJson(projectWorkItemsPath(p)) || [];
|
|
160
|
-
items.filter(w => w.status ===
|
|
162
|
+
items.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
|
|
161
163
|
.forEach(w => blockedItems.push(`- \`${w.id}\` — ${w.title}`));
|
|
162
164
|
}
|
|
163
165
|
const centralItems = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
164
|
-
centralItems.filter(w => w.status ===
|
|
166
|
+
centralItems.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
|
|
165
167
|
.forEach(w => blockedItems.push(`- \`${w.id}\` — ${w.title}`));
|
|
166
168
|
|
|
167
169
|
writeInboxAlert(`failed-${failedId}`,
|
package/engine/lifecycle.js
CHANGED
|
@@ -8,7 +8,8 @@ const path = require('path');
|
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const shared = require('./shared');
|
|
10
10
|
const { safeRead, safeJson, safeWrite, mutateJsonFileLocked, execSilent, projectPrPath, getPrLinks, addPrLink,
|
|
11
|
-
log, ts, dateStamp
|
|
11
|
+
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
12
|
+
ENGINE_DEFAULTS } = shared;
|
|
12
13
|
const { trackEngineUsage } = require('./llm');
|
|
13
14
|
const queries = require('./queries');
|
|
14
15
|
const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
|
|
@@ -57,7 +58,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
57
58
|
const unmaterialized = [...planFeatureIds].filter(id => {
|
|
58
59
|
if (workItemById[id]) return false;
|
|
59
60
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
60
|
-
return !(prdItem && (prdItem.status
|
|
61
|
+
return !(prdItem && DONE_STATUSES.has(prdItem.status));
|
|
61
62
|
});
|
|
62
63
|
if (unmaterialized.length > 0) {
|
|
63
64
|
log('info', `Plan ${planFile}: ${unmaterialized.length}/${planFeatureIds.size} feature(s) not yet materialized as work items: ${unmaterialized.join(', ')}`);
|
|
@@ -67,17 +68,17 @@ function checkPlanCompletion(meta, config) {
|
|
|
67
68
|
// Check 2: every feature's work item must be done (or PRD item marked done externally)
|
|
68
69
|
const notDone = [...planFeatureIds].filter(id => {
|
|
69
70
|
const w = workItemById[id];
|
|
70
|
-
if (w && (w.status
|
|
71
|
+
if (w && DONE_STATUSES.has(w.status)) return false;
|
|
71
72
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
72
|
-
return !(prdItem && (prdItem.status
|
|
73
|
+
return !(prdItem && DONE_STATUSES.has(prdItem.status));
|
|
73
74
|
});
|
|
74
75
|
if (notDone.length > 0) {
|
|
75
76
|
log('info', `Plan ${planFile}: waiting for done on ${notDone.length}/${planFeatureIds.size} item(s): ${notDone.join(', ')}`);
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
const doneItems = planItems.filter(w =>
|
|
80
|
-
const failedItems = planItems.filter(w => w.status ===
|
|
80
|
+
const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
|
|
81
|
+
const failedItems = planItems.filter(w => w.status === WI_STATUS.FAILED);
|
|
81
82
|
|
|
82
83
|
// 1. Mark plan as completed
|
|
83
84
|
plan.status = 'completed';
|
|
@@ -490,20 +491,20 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
490
491
|
target.agentResults[agent] = { status, completedAt: ts(), reason: reason || undefined };
|
|
491
492
|
|
|
492
493
|
const results = Object.values(target.agentResults);
|
|
493
|
-
const anySuccess = results.some(r => r.status ===
|
|
494
|
+
const anySuccess = results.some(r => r.status === WI_STATUS.DONE);
|
|
494
495
|
const allDone = Array.isArray(target.fanOutAgents) && target.fanOutAgents.length > 0 ? results.length >= target.fanOutAgents.length : false;
|
|
495
496
|
const dispatchAge = target.dispatched_at ? Date.now() - new Date(target.dispatched_at).getTime() : 0;
|
|
496
497
|
const timedOut = !allDone && dispatchAge > 6 * 60 * 60 * 1000 && results.length > 0;
|
|
497
498
|
|
|
498
499
|
if (anySuccess) {
|
|
499
|
-
target.status =
|
|
500
|
+
target.status = WI_STATUS.DONE;
|
|
500
501
|
delete target.failReason;
|
|
501
502
|
delete target.failedAt;
|
|
502
503
|
target.completedAgents = Object.entries(target.agentResults)
|
|
503
|
-
.filter(([, r]) => r.status ===
|
|
504
|
+
.filter(([, r]) => r.status === WI_STATUS.DONE)
|
|
504
505
|
.map(([a]) => a);
|
|
505
506
|
} else if (allDone || timedOut) {
|
|
506
|
-
target.status =
|
|
507
|
+
target.status = WI_STATUS.FAILED;
|
|
507
508
|
target.failReason = timedOut
|
|
508
509
|
? `Fan-out timed out: ${results.length}/${(target.fanOutAgents || []).length} agents reported (all failed)`
|
|
509
510
|
: 'All fan-out agents failed';
|
|
@@ -511,11 +512,11 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
511
512
|
}
|
|
512
513
|
} else {
|
|
513
514
|
target.status = status;
|
|
514
|
-
if (status ===
|
|
515
|
+
if (status === WI_STATUS.DONE) {
|
|
515
516
|
delete target.failReason;
|
|
516
517
|
delete target.failedAt;
|
|
517
518
|
target.completedAt = ts();
|
|
518
|
-
} else if (status ===
|
|
519
|
+
} else if (status === WI_STATUS.FAILED) {
|
|
519
520
|
if (reason) target.failReason = reason;
|
|
520
521
|
target.failedAt = ts();
|
|
521
522
|
}
|
|
@@ -643,6 +644,9 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
643
644
|
for (const [name, { prPath, prIds }] of targetPrIds) {
|
|
644
645
|
mutateJsonFileLocked(prPath, (prs) => {
|
|
645
646
|
if (!Array.isArray(prs)) prs = [];
|
|
647
|
+
// Deduplicate any existing entries with same id (case-insensitive agent name race)
|
|
648
|
+
const seen = new Set();
|
|
649
|
+
prs = prs.filter(p => { const k = String(p.id); if (seen.has(k)) return false; seen.add(k); return true; });
|
|
646
650
|
for (const { prId, fullId } of prIds) {
|
|
647
651
|
if (prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
|
|
648
652
|
|
|
@@ -981,7 +985,7 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
981
985
|
m.lastTask = dispatchItem.task;
|
|
982
986
|
m.lastCompleted = ts();
|
|
983
987
|
if (model) m.model = model;
|
|
984
|
-
if (result ===
|
|
988
|
+
if (result === DISPATCH_RESULT.SUCCESS) {
|
|
985
989
|
m.tasksCompleted++;
|
|
986
990
|
if (prsCreatedCount > 0) m.prsCreated = (m.prsCreated || 0) + prsCreatedCount;
|
|
987
991
|
if (dispatchItem.type === 'review') m.reviewsDone++;
|
|
@@ -1105,7 +1109,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1105
1109
|
const type = dispatchItem.type;
|
|
1106
1110
|
const meta = dispatchItem.meta;
|
|
1107
1111
|
const isSuccess = code === 0;
|
|
1108
|
-
const result = isSuccess ?
|
|
1112
|
+
const result = isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR;
|
|
1109
1113
|
const { resultSummary, taskUsage, sessionId, model } = parseAgentOutput(stdout);
|
|
1110
1114
|
|
|
1111
1115
|
// Save session for potential resume on next dispatch
|
|
@@ -1141,9 +1145,10 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1141
1145
|
}
|
|
1142
1146
|
} catch { /* optional */ }
|
|
1143
1147
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1148
|
+
const maxRetries = ENGINE_DEFAULTS.maxRetries;
|
|
1149
|
+
if (retries < maxRetries) {
|
|
1150
|
+
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/${maxRetries}`);
|
|
1151
|
+
updateWorkItemStatus(meta, WI_STATUS.PENDING, '');
|
|
1147
1152
|
try {
|
|
1148
1153
|
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1149
1154
|
? path.join(MINIONS_DIR, 'work-items.json')
|
|
@@ -1152,14 +1157,14 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1152
1157
|
const items = safeJson(wiPath) || [];
|
|
1153
1158
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1154
1159
|
if (wi) {
|
|
1155
|
-
wi._retryCount = retries + 1; wi.status =
|
|
1156
|
-
if (type ===
|
|
1160
|
+
wi._retryCount = retries + 1; wi.status = WI_STATUS.PENDING; delete wi.dispatched_at; delete wi.dispatched_to;
|
|
1161
|
+
if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing;
|
|
1157
1162
|
shared.safeWrite(wiPath, items);
|
|
1158
1163
|
}
|
|
1159
1164
|
}
|
|
1160
1165
|
} catch (err) { log('warn', `Retry update: ${err.message}`); }
|
|
1161
1166
|
} else {
|
|
1162
|
-
updateWorkItemStatus(meta,
|
|
1167
|
+
updateWorkItemStatus(meta, WI_STATUS.FAILED, `Agent failed (${maxRetries} retries exhausted)`);
|
|
1163
1168
|
}
|
|
1164
1169
|
// Clear _decomposing flag on failure so item doesn't get permanently stuck
|
|
1165
1170
|
if (type === 'decompose') {
|
|
@@ -1258,15 +1263,16 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1258
1263
|
wi.noPr = true;
|
|
1259
1264
|
wi.failReason = 'Completed without creating a pull request';
|
|
1260
1265
|
const retries = wi._retryCount || 0;
|
|
1261
|
-
|
|
1262
|
-
|
|
1266
|
+
const maxR = ENGINE_DEFAULTS.maxRetries;
|
|
1267
|
+
if (retries < maxR) {
|
|
1268
|
+
wi.status = WI_STATUS.PENDING;
|
|
1263
1269
|
wi._retryCount = retries + 1;
|
|
1264
1270
|
delete wi.dispatched_at;
|
|
1265
1271
|
delete wi.dispatched_to;
|
|
1266
|
-
e.log('info', `Auto-retry ${retries + 1}
|
|
1272
|
+
e.log('info', `Auto-retry ${retries + 1}/${maxR} for ${meta.item.id} (no PR created)`);
|
|
1267
1273
|
} else {
|
|
1268
|
-
wi.status =
|
|
1269
|
-
e.log('warn', `${meta.item.id} failed after
|
|
1274
|
+
wi.status = WI_STATUS.FAILED;
|
|
1275
|
+
e.log('warn', `${meta.item.id} failed after ${maxR} retries — no PR created`);
|
|
1270
1276
|
}
|
|
1271
1277
|
shared.safeWrite(wiPath, items);
|
|
1272
1278
|
}
|
package/engine/shared.js
CHANGED
|
@@ -386,8 +386,29 @@ const ENGINE_DEFAULTS = {
|
|
|
386
386
|
evalLoop: true, // enable review→fix loop after implementation completes
|
|
387
387
|
evalMaxIterations: 3, // max review→fix cycles before escalating to human
|
|
388
388
|
evalMaxCost: null, // USD ceiling per work item across all eval iterations; null = no limit (gather baseline data first)
|
|
389
|
+
maxRetries: 3, // max dispatch retries before marking work item as failed
|
|
389
390
|
};
|
|
390
391
|
|
|
392
|
+
// ─── Status & Type Constants ─────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
const WI_STATUS = {
|
|
395
|
+
PENDING: 'pending', DISPATCHED: 'dispatched', DONE: 'done', FAILED: 'failed',
|
|
396
|
+
PAUSED: 'paused', QUEUED: 'queued', NEEDS_REVIEW: 'needs-human-review', DECOMPOSED: 'decomposed',
|
|
397
|
+
};
|
|
398
|
+
const DONE_STATUSES = new Set([WI_STATUS.DONE, 'in-pr', 'implemented', 'complete']); // includes legacy aliases
|
|
399
|
+
const WORK_TYPE = {
|
|
400
|
+
IMPLEMENT: 'implement', IMPLEMENT_LARGE: 'implement:large', FIX: 'fix', REVIEW: 'review',
|
|
401
|
+
VERIFY: 'verify', PLAN: 'plan', PLAN_TO_PRD: 'plan-to-prd', DECOMPOSE: 'decompose',
|
|
402
|
+
MEETING: 'meeting', EXPLORE: 'explore', ASK: 'ask', TEST: 'test', DOCS: 'docs',
|
|
403
|
+
};
|
|
404
|
+
const PLAN_STATUS = {
|
|
405
|
+
ACTIVE: 'active', AWAITING_APPROVAL: 'awaiting-approval', APPROVED: 'approved',
|
|
406
|
+
PAUSED: 'paused', REJECTED: 'rejected', COMPLETED: 'completed',
|
|
407
|
+
REVISION_REQUESTED: 'revision-requested',
|
|
408
|
+
};
|
|
409
|
+
const PR_STATUS = { ACTIVE: 'active', MERGED: 'merged', ABANDONED: 'abandoned', CLOSED: 'closed' };
|
|
410
|
+
const DISPATCH_RESULT = { SUCCESS: 'success', ERROR: 'error', TIMEOUT: 'timeout' };
|
|
411
|
+
|
|
391
412
|
const DEFAULT_AGENTS = {
|
|
392
413
|
ripley: { name: 'Ripley', emoji: '\u{1F3D7}\uFE0F', role: 'Lead / Explorer', skills: ['architecture', 'codebase-exploration', 'design-review'] },
|
|
393
414
|
dallas: { name: 'Dallas', emoji: '\u{1F527}', role: 'Engineer', skills: ['implementation', 'typescript', 'docker', 'testing'] },
|
|
@@ -617,6 +638,7 @@ module.exports = {
|
|
|
617
638
|
KB_CATEGORIES,
|
|
618
639
|
classifyInboxItem,
|
|
619
640
|
ENGINE_DEFAULTS,
|
|
641
|
+
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
620
642
|
DEFAULT_AGENTS,
|
|
621
643
|
DEFAULT_CLAUDE,
|
|
622
644
|
getProjects,
|
package/engine/timeout.js
CHANGED
|
@@ -8,7 +8,8 @@ const path = require('path');
|
|
|
8
8
|
const shared = require('./shared');
|
|
9
9
|
const queries = require('./queries');
|
|
10
10
|
|
|
11
|
-
const { safeRead, safeWrite, safeJson, getProjects, projectWorkItemsPath, log, ts,
|
|
11
|
+
const { safeRead, safeWrite, safeJson, getProjects, projectWorkItemsPath, log, ts,
|
|
12
|
+
ENGINE_DEFAULTS: DEFAULTS, WI_STATUS, DISPATCH_RESULT } = shared;
|
|
12
13
|
const { getDispatch, getAgentStatus } = queries;
|
|
13
14
|
const AGENTS_DIR = queries.AGENTS_DIR;
|
|
14
15
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
@@ -140,7 +141,7 @@ function checkTimeouts(config) {
|
|
|
140
141
|
safeWrite(outputLogPath, `# Output for dispatch ${item.id}\n# Exit code: ${isSuccess ? 0 : 1}\n# Completed: ${ts()}\n# Detected via output scan\n\n## Result\n${text || '(no text)'}\n`);
|
|
141
142
|
} catch (e) { log('warn', 'parse output result: ' + e.message); }
|
|
142
143
|
|
|
143
|
-
completeDispatch(item.id, isSuccess ?
|
|
144
|
+
completeDispatch(item.id, isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR, 'Completed (detected from output)');
|
|
144
145
|
|
|
145
146
|
// Run post-completion hooks via shared helper
|
|
146
147
|
runPostCompletionHooks(item, item.agent, isSuccess ? 0 : 1, liveLog, config);
|
|
@@ -215,7 +216,7 @@ function checkTimeouts(config) {
|
|
|
215
216
|
|
|
216
217
|
// Clean up dead items
|
|
217
218
|
for (const { item, reason } of deadItems) {
|
|
218
|
-
completeDispatch(item.id,
|
|
219
|
+
completeDispatch(item.id, DISPATCH_RESULT.ERROR, reason);
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
// Agent status is now derived from dispatch.json at read time (getAgentStatus).
|
|
@@ -232,7 +233,7 @@ function checkTimeouts(config) {
|
|
|
232
233
|
if (!items || !Array.isArray(items)) continue;
|
|
233
234
|
let changed = false;
|
|
234
235
|
for (const item of items) {
|
|
235
|
-
if (item.status !==
|
|
236
|
+
if (item.status !== WI_STATUS.DISPATCHED) continue;
|
|
236
237
|
// Check if any active dispatch references this item
|
|
237
238
|
// Dispatch keys include project name: work-{project}-{id} or central-work-{id}
|
|
238
239
|
const projectNames = getProjects(config).map(p => p.name);
|
|
@@ -244,19 +245,20 @@ function checkTimeouts(config) {
|
|
|
244
245
|
(dispatchData.active || []).some(d => d.meta?.item?.id === item.id);
|
|
245
246
|
if (!isActive) {
|
|
246
247
|
// Don't revive items that were explicitly failed for non-retryable reasons
|
|
247
|
-
if (item.status ===
|
|
248
|
+
if (item.status === WI_STATUS.FAILED && item.failReason && !item.failReason.includes('Agent died')) continue;
|
|
248
249
|
const retries = (item._retryCount || 0);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
item.
|
|
250
|
+
const maxRetries = DEFAULTS.maxRetries;
|
|
251
|
+
if (retries < maxRetries) {
|
|
252
|
+
log('info', `Reconcile: work item ${item.id} agent died — auto-retry ${retries + 1}/${maxRetries}`);
|
|
253
|
+
item.status = WI_STATUS.PENDING;
|
|
252
254
|
item._retryCount = retries + 1;
|
|
253
255
|
delete item.dispatched_at;
|
|
254
256
|
delete item.dispatched_to;
|
|
255
257
|
delete item._pendingReason;
|
|
256
258
|
} else {
|
|
257
259
|
log('warn', `Reconcile: work item ${item.id} failed after ${retries} retries — marking as failed`);
|
|
258
|
-
item.status =
|
|
259
|
-
item.failReason =
|
|
260
|
+
item.status = WI_STATUS.FAILED;
|
|
261
|
+
item.failReason = `Agent died or was killed (${maxRetries} retries exhausted)`;
|
|
260
262
|
item.failedAt = ts();
|
|
261
263
|
delete item._pendingReason;
|
|
262
264
|
}
|
package/engine.js
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
const fs = require('fs');
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const shared = require('./engine/shared');
|
|
27
|
-
const { exec, execSilent, runFile, ENGINE_DEFAULTS: DEFAULTS
|
|
27
|
+
const { exec, execSilent, runFile, ENGINE_DEFAULTS: DEFAULTS,
|
|
28
|
+
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, DISPATCH_RESULT } = shared;
|
|
28
29
|
const queries = require('./engine/queries');
|
|
29
30
|
|
|
30
31
|
// ─── Paths ──────────────────────────────────────────────────────────────────
|
|
@@ -754,7 +755,7 @@ function areDependenciesMet(item, config) {
|
|
|
754
755
|
} catch (e) { log('warn', 'read project work items for deps: ' + e.message); }
|
|
755
756
|
}
|
|
756
757
|
// PRD item statuses that count as "done" for dep resolution
|
|
757
|
-
const PRD_MET_STATUSES =
|
|
758
|
+
const PRD_MET_STATUSES = DONE_STATUSES;
|
|
758
759
|
|
|
759
760
|
for (const depId of deps) {
|
|
760
761
|
const depItem = allWorkItems.find(w => w.id === depId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.317",
|
|
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"
|