@yemi33/minions 0.1.411 → 0.1.413
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 -0
- package/dashboard.js +5 -4
- package/engine/lifecycle.js +175 -156
- package/engine/meeting.js +4 -5
- package/engine/queries.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard.js
CHANGED
|
@@ -179,12 +179,13 @@ async function checkNpmVersion() {
|
|
|
179
179
|
const now = Date.now();
|
|
180
180
|
if (_npmVersionCache && (now - _npmVersionCacheTs) < NPM_CHECK_INTERVAL) return _npmVersionCache;
|
|
181
181
|
try {
|
|
182
|
-
// Use npm view — respects user's .npmrc proxy/registry config
|
|
182
|
+
// Use npm view — respects user's .npmrc proxy/registry config
|
|
183
|
+
// Resolve npm path from Node binary location — detached processes may not have PATH
|
|
183
184
|
const { execFile } = require('child_process');
|
|
184
|
-
|
|
185
|
-
const
|
|
185
|
+
const nodeDir = require('path').dirname(process.execPath);
|
|
186
|
+
const npmPath = require('path').join(nodeDir, process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
186
187
|
const version = await new Promise((resolve, reject) => {
|
|
187
|
-
execFile(
|
|
188
|
+
execFile(npmPath, ['view', PKG_NAME, 'version'], { timeout: 15000, windowsHide: true }, (err, stdout) => {
|
|
188
189
|
if (err) reject(err); else resolve((stdout || '').trim());
|
|
189
190
|
});
|
|
190
191
|
});
|
package/engine/lifecycle.js
CHANGED
|
@@ -923,14 +923,17 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
923
923
|
const proj = shared.getProjects(config).find(p => p.name === project);
|
|
924
924
|
if (proj) {
|
|
925
925
|
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
926
|
+
let skillId = null;
|
|
927
|
+
mutateJsonFileLocked(centralPath, data => {
|
|
928
|
+
data = data || [];
|
|
929
|
+
if (data.some(i => i.title === `Add skill: ${name}` && i.status !== WI_STATUS.FAILED)) return data;
|
|
930
|
+
skillId = `SK${String(data.filter(i => i.id?.startsWith('SK')).length + 1).padStart(3, '0')}`;
|
|
931
|
+
data.push({ id: skillId, type: 'implement', title: `Add skill: ${name}`,
|
|
931
932
|
description: `Create project-level skill \`${filename}\` in ${project}.\n\nWrite this file to \`${proj.localPath}/.claude/skills/${filename}\` via a PR.\n\n## Skill Content\n\n\`\`\`\n${enrichedBlock}\n\`\`\``,
|
|
932
933
|
priority: 'low', status: WI_STATUS.QUEUED, created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
|
|
933
|
-
|
|
934
|
+
return data;
|
|
935
|
+
});
|
|
936
|
+
if (skillId) {
|
|
934
937
|
log('info', `Queued work item ${skillId} to PR project skill "${name}" into ${project}`);
|
|
935
938
|
}
|
|
936
939
|
}
|
|
@@ -1006,49 +1009,50 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
|
|
|
1006
1009
|
function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount, model) {
|
|
1007
1010
|
|
|
1008
1011
|
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
metrics[agentId]
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1012
|
+
mutateJsonFileLocked(metricsPath, metrics => {
|
|
1013
|
+
metrics = metrics || {};
|
|
1014
|
+
if (!metrics[agentId]) {
|
|
1015
|
+
metrics[agentId] = { tasksCompleted: 0, tasksErrored: 0, prsCreated: 0, prsApproved: 0, prsRejected: 0,
|
|
1016
|
+
reviewsDone: 0, lastTask: null, lastCompleted: null, totalCostUsd: 0, totalInputTokens: 0, totalOutputTokens: 0, totalCacheRead: 0 };
|
|
1017
|
+
}
|
|
1018
|
+
const m = metrics[agentId];
|
|
1019
|
+
m.lastTask = dispatchItem.task;
|
|
1020
|
+
m.lastCompleted = ts();
|
|
1021
|
+
if (model) m.model = model;
|
|
1022
|
+
if (result === DISPATCH_RESULT.SUCCESS) {
|
|
1023
|
+
m.tasksCompleted++;
|
|
1024
|
+
if (prsCreatedCount > 0) m.prsCreated = (m.prsCreated || 0) + prsCreatedCount;
|
|
1025
|
+
if (dispatchItem.type === WORK_TYPE.REVIEW) m.reviewsDone++;
|
|
1026
|
+
} else if (result === 'retry') {
|
|
1027
|
+
m.tasksRetried = (m.tasksRetried || 0) + 1;
|
|
1028
|
+
} else {
|
|
1029
|
+
m.tasksErrored++;
|
|
1030
|
+
}
|
|
1031
|
+
if (taskUsage) {
|
|
1032
|
+
m.totalCostUsd = (m.totalCostUsd || 0) + (taskUsage.costUsd || 0);
|
|
1033
|
+
m.totalInputTokens = (m.totalInputTokens || 0) + (taskUsage.inputTokens || 0);
|
|
1034
|
+
m.totalOutputTokens = (m.totalOutputTokens || 0) + (taskUsage.outputTokens || 0);
|
|
1035
|
+
m.totalCacheRead = (m.totalCacheRead || 0) + (taskUsage.cacheRead || 0);
|
|
1036
|
+
}
|
|
1037
|
+
const today = dateStamp();
|
|
1038
|
+
if (!metrics._daily) metrics._daily = {};
|
|
1039
|
+
if (!metrics._daily[today]) metrics._daily[today] = { costUsd: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, tasks: 0 };
|
|
1040
|
+
const daily = metrics._daily[today];
|
|
1041
|
+
daily.tasks++;
|
|
1042
|
+
if (taskUsage) {
|
|
1043
|
+
daily.costUsd += taskUsage.costUsd || 0;
|
|
1044
|
+
daily.inputTokens += taskUsage.inputTokens || 0;
|
|
1045
|
+
daily.outputTokens += taskUsage.outputTokens || 0;
|
|
1046
|
+
daily.cacheRead += taskUsage.cacheRead || 0;
|
|
1047
|
+
}
|
|
1048
|
+
const cutoff = new Date();
|
|
1049
|
+
cutoff.setDate(cutoff.getDate() - 30);
|
|
1050
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
1051
|
+
for (const day of Object.keys(metrics._daily)) {
|
|
1052
|
+
if (day < cutoffStr) delete metrics._daily[day];
|
|
1053
|
+
}
|
|
1054
|
+
return metrics;
|
|
1055
|
+
});
|
|
1052
1056
|
}
|
|
1053
1057
|
|
|
1054
1058
|
// ─── Agent Output Parsing ────────────────────────────────────────────────────
|
|
@@ -1095,38 +1099,42 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
1095
1099
|
for (const p of projects) allPaths.push(shared.projectWorkItemsPath(p));
|
|
1096
1100
|
|
|
1097
1101
|
for (const wiPath of allPaths) {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
items
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1102
|
+
let found = false;
|
|
1103
|
+
mutateJsonFileLocked(wiPath, data => {
|
|
1104
|
+
if (!Array.isArray(data)) return data;
|
|
1105
|
+
const p = data.find(i => i.id === parentId);
|
|
1106
|
+
if (!p) return data;
|
|
1107
|
+
found = true;
|
|
1108
|
+
|
|
1109
|
+
// Mark parent as decomposed
|
|
1110
|
+
p.status = WI_STATUS.DECOMPOSED;
|
|
1111
|
+
p._decomposed = true;
|
|
1112
|
+
delete p._decomposing;
|
|
1113
|
+
p._subItemIds = subItems.map(s => s.id);
|
|
1114
|
+
|
|
1115
|
+
// Create child work items
|
|
1116
|
+
for (const sub of subItems) {
|
|
1117
|
+
if (data.some(i => i.id === sub.id)) continue; // dedupe
|
|
1118
|
+
data.push({
|
|
1119
|
+
id: sub.id,
|
|
1120
|
+
title: sub.name || sub.title || `Sub-task of ${parentId}`,
|
|
1121
|
+
type: (sub.estimated_complexity === 'large') ? 'implement:large' : 'implement',
|
|
1122
|
+
priority: sub.priority || p.priority || 'medium',
|
|
1123
|
+
description: sub.description || '',
|
|
1124
|
+
status: WI_STATUS.PENDING,
|
|
1125
|
+
complexity: sub.estimated_complexity || 'medium',
|
|
1126
|
+
depends_on: sub.depends_on || [],
|
|
1127
|
+
parent_id: parentId,
|
|
1128
|
+
sourcePlan: p.sourcePlan,
|
|
1129
|
+
branchStrategy: p.branchStrategy,
|
|
1130
|
+
featureBranch: p.featureBranch,
|
|
1131
|
+
created: new Date().toISOString(),
|
|
1132
|
+
createdBy: 'decomposition',
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
return data;
|
|
1136
|
+
});
|
|
1137
|
+
if (!found) continue;
|
|
1130
1138
|
log('info', `Decomposition: ${parentId} → ${subItems.length} sub-items: ${subItems.map(s => s.id).join(', ')}`);
|
|
1131
1139
|
return subItems.length;
|
|
1132
1140
|
}
|
|
@@ -1152,25 +1160,37 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1152
1160
|
} catch (err) { log('warn', `Session save: ${err.message}`); }
|
|
1153
1161
|
}
|
|
1154
1162
|
|
|
1163
|
+
// Always attempt PR sync — even failed/timed-out agents may have created PRs before dying
|
|
1164
|
+
let prsCreatedCount = 0;
|
|
1165
|
+
try {
|
|
1166
|
+
prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
|
|
1167
|
+
} catch (err) { log('warn', `PR sync from output: ${err.message}`); }
|
|
1168
|
+
|
|
1169
|
+
// Auto-recover: if a failed implement/fix agent created PRs, it likely succeeded before being killed (e.g. heartbeat timeout)
|
|
1170
|
+
const prCreatingType = type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || type === WORK_TYPE.FIX;
|
|
1171
|
+
const autoRecovered = !isSuccess && prsCreatedCount > 0 && prCreatingType && !!meta?.item?.id;
|
|
1172
|
+
if (autoRecovered) {
|
|
1173
|
+
log('info', `Auto-recovery: agent failed but created ${prsCreatedCount} PR(s) — upgrading ${meta.item.id} to done`);
|
|
1174
|
+
}
|
|
1175
|
+
const effectiveSuccess = isSuccess || autoRecovered;
|
|
1176
|
+
|
|
1155
1177
|
// Handle decomposition results — create sub-items from decompose agent output
|
|
1156
1178
|
let skipDoneStatus = false;
|
|
1157
|
-
if (type === WORK_TYPE.DECOMPOSE &&
|
|
1179
|
+
if (type === WORK_TYPE.DECOMPOSE && effectiveSuccess && meta?.item?.id) {
|
|
1158
1180
|
const subCount = handleDecompositionResult(stdout, meta, config);
|
|
1159
1181
|
if (subCount > 0) skipDoneStatus = true; // parent already marked 'decomposed' by handler
|
|
1160
1182
|
// If decomposition produced nothing, fall through to mark parent as done
|
|
1161
1183
|
}
|
|
1162
1184
|
|
|
1163
|
-
if (
|
|
1185
|
+
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus) {
|
|
1164
1186
|
meta._agentId = agentId;
|
|
1165
1187
|
updateWorkItemStatus(meta, WI_STATUS.DONE, '');
|
|
1166
1188
|
}
|
|
1167
|
-
if (!
|
|
1189
|
+
if (!effectiveSuccess && meta?.item?.id) {
|
|
1168
1190
|
// Auto-retry: read fresh _retryCount from file (not stale dispatch-time snapshot)
|
|
1169
1191
|
let retries = (meta.item._retryCount || 0);
|
|
1192
|
+
const wiPath = resolveWorkItemPath(meta);
|
|
1170
1193
|
try {
|
|
1171
|
-
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1172
|
-
? path.join(MINIONS_DIR, 'work-items.json')
|
|
1173
|
-
: meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
|
|
1174
1194
|
if (wiPath) {
|
|
1175
1195
|
const items = safeJson(wiPath) || [];
|
|
1176
1196
|
const wi = items.find(i => i.id === meta.item.id);
|
|
@@ -1182,22 +1202,20 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1182
1202
|
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries}`);
|
|
1183
1203
|
updateWorkItemStatus(meta, WI_STATUS.PENDING, '');
|
|
1184
1204
|
try {
|
|
1185
|
-
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1186
|
-
? path.join(MINIONS_DIR, 'work-items.json')
|
|
1187
|
-
: meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
|
|
1188
1205
|
if (wiPath) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1206
|
+
mutateJsonFileLocked(wiPath, data => {
|
|
1207
|
+
if (!Array.isArray(data)) return data;
|
|
1208
|
+
const wi = data.find(i => i.id === meta.item.id);
|
|
1209
|
+
if (!wi) return data;
|
|
1192
1210
|
// Don't revert if already completed by another code path
|
|
1193
1211
|
if (wi.status === WI_STATUS.DONE || wi.completedAt) {
|
|
1194
1212
|
log('info', `Skip retry for ${meta.item.id} — already completed`);
|
|
1195
|
-
|
|
1196
|
-
wi._retryCount = retries + 1; wi.status = WI_STATUS.PENDING; delete wi.dispatched_at; delete wi.dispatched_to; delete wi.failReason;
|
|
1197
|
-
if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing;
|
|
1198
|
-
shared.safeWrite(wiPath, items);
|
|
1213
|
+
return data;
|
|
1199
1214
|
}
|
|
1200
|
-
|
|
1215
|
+
wi._retryCount = retries + 1; wi.status = WI_STATUS.PENDING; delete wi.dispatched_at; delete wi.dispatched_to; delete wi.failReason;
|
|
1216
|
+
if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing;
|
|
1217
|
+
return data;
|
|
1218
|
+
});
|
|
1201
1219
|
}
|
|
1202
1220
|
} catch (err) { log('warn', `Retry update: ${err.message}`); }
|
|
1203
1221
|
} else {
|
|
@@ -1206,13 +1224,13 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1206
1224
|
// Clear _decomposing flag on failure so item doesn't get permanently stuck
|
|
1207
1225
|
if (type === WORK_TYPE.DECOMPOSE) {
|
|
1208
1226
|
try {
|
|
1209
|
-
const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
|
|
1210
|
-
? path.join(MINIONS_DIR, 'work-items.json')
|
|
1211
|
-
: meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
|
|
1212
1227
|
if (wiPath) {
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1228
|
+
mutateJsonFileLocked(wiPath, data => {
|
|
1229
|
+
if (!Array.isArray(data)) return data;
|
|
1230
|
+
const wi = data.find(i => i.id === meta.item.id);
|
|
1231
|
+
if (wi) delete wi._decomposing;
|
|
1232
|
+
return data;
|
|
1233
|
+
});
|
|
1216
1234
|
}
|
|
1217
1235
|
} catch (err) { log('warn', `Decompose cleanup: ${err.message}`); }
|
|
1218
1236
|
}
|
|
@@ -1226,13 +1244,10 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1226
1244
|
}
|
|
1227
1245
|
|
|
1228
1246
|
// Plan chaining removed — user must explicitly execute plan-to-prd after reviewing the plan
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1231
|
-
let prsCreatedCount = 0;
|
|
1232
|
-
if (isSuccess) prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
|
|
1247
|
+
if (effectiveSuccess && meta?.item?.sourcePlan) checkPlanCompletion(meta, config);
|
|
1233
1248
|
|
|
1234
1249
|
// After verify completes, archive the plan
|
|
1235
|
-
if (
|
|
1250
|
+
if (effectiveSuccess && meta?.item?.itemType === 'verify' && meta?.item?.sourcePlan) {
|
|
1236
1251
|
try {
|
|
1237
1252
|
const vPlanFile = meta.item.sourcePlan;
|
|
1238
1253
|
const vPlanPath = path.join(PRD_DIR, vPlanFile);
|
|
@@ -1280,7 +1295,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1280
1295
|
}
|
|
1281
1296
|
|
|
1282
1297
|
// Detect implement tasks that completed without creating a PR
|
|
1283
|
-
if (
|
|
1298
|
+
if (effectiveSuccess && (type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || type === WORK_TYPE.FIX) && prsCreatedCount === 0 && meta?.item?.id && !meta?.item?.skipPr && meta?.project?.localPath) {
|
|
1284
1299
|
// Check if a PR already exists linked to this work item (from a previous attempt)
|
|
1285
1300
|
let existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
|
1286
1301
|
// Also check pull-requests.json for PRs with matching prdItems or branch
|
|
@@ -1295,44 +1310,44 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1295
1310
|
}
|
|
1296
1311
|
}
|
|
1297
1312
|
if (!existingPrFound) {
|
|
1298
|
-
|
|
1299
|
-
if (
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
if (wi) {
|
|
1308
|
-
const retries = wi._retryCount || 0;
|
|
1309
|
-
// Check if agent produced meaningful output (not just MCP timeout)
|
|
1310
|
-
const hasOutput = stdout && stdout.length > 500;
|
|
1313
|
+
const noPrWiPath = resolveWorkItemPath(meta);
|
|
1314
|
+
if (noPrWiPath) {
|
|
1315
|
+
const hasOutput = stdout && stdout.length > 500;
|
|
1316
|
+
let action = null;
|
|
1317
|
+
mutateJsonFileLocked(noPrWiPath, data => {
|
|
1318
|
+
if (!Array.isArray(data)) return data;
|
|
1319
|
+
const w = data.find(i => i.id === meta.item.id);
|
|
1320
|
+
if (!w) return data;
|
|
1321
|
+
const retries = w._retryCount || 0;
|
|
1311
1322
|
if (!hasOutput && retries < ENGINE_DEFAULTS.maxRetries) {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
delete
|
|
1316
|
-
delete
|
|
1317
|
-
delete
|
|
1318
|
-
|
|
1319
|
-
log('info', `Auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries} for ${meta.item.id} (no output, no PR)`);
|
|
1323
|
+
w.status = WI_STATUS.PENDING;
|
|
1324
|
+
w._retryCount = retries + 1;
|
|
1325
|
+
delete w.dispatched_at;
|
|
1326
|
+
delete w.dispatched_to;
|
|
1327
|
+
delete w.failReason;
|
|
1328
|
+
delete w.noPr;
|
|
1329
|
+
action = { type: 'retry', retries: retries + 1 };
|
|
1320
1330
|
} else if (hasOutput) {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
log('info', `${meta.item.id} completed without PR — marking done (agent produced output)`);
|
|
1331
|
+
w.status = WI_STATUS.DONE;
|
|
1332
|
+
w.completedAt = ts();
|
|
1333
|
+
w._noPr = true;
|
|
1334
|
+
w._noPrReason = 'Agent completed without creating a PR (changes may already exist or not be needed)';
|
|
1335
|
+
delete w.failReason;
|
|
1336
|
+
action = { type: 'done' };
|
|
1328
1337
|
} else {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
log('warn', `${meta.item.id} needs review — no output after ${ENGINE_DEFAULTS.maxRetries} retries`);
|
|
1338
|
+
w.status = WI_STATUS.NEEDS_REVIEW;
|
|
1339
|
+
w._noPr = true;
|
|
1340
|
+
w.failReason = 'Completed without output or PR after ' + ENGINE_DEFAULTS.maxRetries + ' attempts';
|
|
1341
|
+
action = { type: 'needs-review' };
|
|
1334
1342
|
}
|
|
1335
|
-
|
|
1343
|
+
return data;
|
|
1344
|
+
});
|
|
1345
|
+
if (action?.type === 'retry') {
|
|
1346
|
+
log('info', `Auto-retry ${action.retries}/${ENGINE_DEFAULTS.maxRetries} for ${meta.item.id} (no output, no PR)`);
|
|
1347
|
+
} else if (action?.type === 'done') {
|
|
1348
|
+
log('info', `${meta.item.id} completed without PR — marking done (agent produced output)`);
|
|
1349
|
+
} else if (action?.type === 'needs-review') {
|
|
1350
|
+
log('warn', `${meta.item.id} needs review — no output after ${ENGINE_DEFAULTS.maxRetries} retries`);
|
|
1336
1351
|
}
|
|
1337
1352
|
}
|
|
1338
1353
|
}
|
|
@@ -1341,14 +1356,15 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1341
1356
|
if (type === WORK_TYPE.REVIEW) updatePrAfterReview(agentId, meta?.pr, meta?.project);
|
|
1342
1357
|
if (type === WORK_TYPE.FIX) updatePrAfterFix(meta?.pr, meta?.project, meta?.source);
|
|
1343
1358
|
checkForLearnings(agentId, config.agents[agentId], dispatchItem.task);
|
|
1344
|
-
if (
|
|
1345
|
-
|
|
1359
|
+
if (effectiveSuccess) extractSkillsFromOutput(stdout, agentId, dispatchItem, config);
|
|
1360
|
+
const finalResult = effectiveSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR;
|
|
1361
|
+
updateAgentHistory(agentId, dispatchItem, finalResult);
|
|
1346
1362
|
// Don't count auto-retries as errors in metrics — only count final outcomes
|
|
1347
|
-
const isAutoRetry = !
|
|
1348
|
-
const metricsResult = isAutoRetry ? 'retry' :
|
|
1363
|
+
const isAutoRetry = !effectiveSuccess && meta?.item?.id && (meta.item._retryCount || 0) < ENGINE_DEFAULTS.maxRetries;
|
|
1364
|
+
const metricsResult = isAutoRetry ? 'retry' : finalResult;
|
|
1349
1365
|
updateMetrics(agentId, dispatchItem, metricsResult, taskUsage, prsCreatedCount, model);
|
|
1350
1366
|
|
|
1351
|
-
return { resultSummary, taskUsage };
|
|
1367
|
+
return { resultSummary, taskUsage, autoRecovered };
|
|
1352
1368
|
}
|
|
1353
1369
|
|
|
1354
1370
|
// ─── PR → PRD Status Sync ─────────────────────────────────────────────────────
|
|
@@ -1357,25 +1373,28 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1357
1373
|
// (e.g., manually raised PRs, cross-plan PRs, or PRs created while engine was paused).
|
|
1358
1374
|
function syncPrdFromPrs(config) {
|
|
1359
1375
|
try {
|
|
1360
|
-
const { getProjects, projectWorkItemsPath, projectPrPath, safeJson, safeWrite } = require('./shared');
|
|
1361
1376
|
const { reconcileItemsWithPrs } = require('../engine');
|
|
1362
1377
|
config = config || queries.getConfig();
|
|
1363
|
-
const allProjects = getProjects(config);
|
|
1378
|
+
const allProjects = shared.getProjects(config);
|
|
1364
1379
|
|
|
1365
1380
|
// Exact prdItems match only — no fuzzy matching
|
|
1366
|
-
const allPrs = allProjects.flatMap(p => safeJson(projectPrPath(p)) || []);
|
|
1381
|
+
const allPrs = allProjects.flatMap(p => safeJson(shared.projectPrPath(p)) || []);
|
|
1367
1382
|
|
|
1368
1383
|
let totalReconciled = 0;
|
|
1369
1384
|
for (const project of allProjects) {
|
|
1370
|
-
const wiPath = projectWorkItemsPath(project);
|
|
1385
|
+
const wiPath = shared.projectWorkItemsPath(project);
|
|
1371
1386
|
const items = safeJson(wiPath) || [];
|
|
1372
1387
|
const hasPending = items.some(wi => wi.status === WI_STATUS.PENDING && !wi._pr);
|
|
1373
1388
|
if (!hasPending) continue;
|
|
1374
|
-
|
|
1389
|
+
let reconciled = 0;
|
|
1390
|
+
const reconciledItems = mutateJsonFileLocked(wiPath, data => {
|
|
1391
|
+
if (!Array.isArray(data)) return data;
|
|
1392
|
+
reconciled = reconcileItemsWithPrs(data, allPrs);
|
|
1393
|
+
return data;
|
|
1394
|
+
});
|
|
1375
1395
|
if (reconciled > 0) {
|
|
1376
|
-
safeWrite(wiPath, items);
|
|
1377
1396
|
// Sync done status to PRD JSON for each newly reconciled item
|
|
1378
|
-
for (const wi of
|
|
1397
|
+
for (const wi of (reconciledItems || [])) {
|
|
1379
1398
|
if (wi.status === WI_STATUS.DONE) syncPrdItemStatus(wi.id, WI_STATUS.DONE, wi.sourcePlan);
|
|
1380
1399
|
}
|
|
1381
1400
|
totalReconciled += reconciled;
|
package/engine/meeting.js
CHANGED
|
@@ -234,11 +234,10 @@ function collectMeetingFindings(meetingId, agentId, roundName, output) {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
// Check if all participants have submitted for this round
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
});
|
|
237
|
+
const participantCount = meeting.participants.length;
|
|
238
|
+
const allSubmitted =
|
|
239
|
+
(meeting.status === 'investigating' && Object.keys(meeting.findings || {}).length >= participantCount) ||
|
|
240
|
+
(meeting.status === 'debating' && Object.keys(meeting.debate || {}).length >= participantCount);
|
|
242
241
|
|
|
243
242
|
if (allSubmitted) {
|
|
244
243
|
// Advance to next round
|
package/engine/queries.js
CHANGED
|
@@ -648,13 +648,13 @@ function getPrdInfo(config) {
|
|
|
648
648
|
for (const project of projects) {
|
|
649
649
|
try {
|
|
650
650
|
const workItems = safeJson(projectWorkItemsPath(project)) || [];
|
|
651
|
-
for (const wi of workItems) { if (!wi
|
|
651
|
+
for (const wi of workItems) { if (!wi?.id) { console.warn(`[queries] Skipping work item without id in ${project.name}:`, JSON.stringify(wi).slice(0, 120)); continue; } if (wi.sourcePlan) wiById[wi.id] = wi; }
|
|
652
652
|
} catch { /* optional */ }
|
|
653
653
|
}
|
|
654
654
|
// Also check central work-items.json
|
|
655
655
|
try {
|
|
656
656
|
const centralWi = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
657
|
-
for (const wi of centralWi) { if (!wi
|
|
657
|
+
for (const wi of centralWi) { if (!wi?.id) { console.warn('[queries] Skipping central work item without id:', JSON.stringify(wi).slice(0, 120)); continue; } if (wi.sourcePlan && !wiById[wi.id]) wiById[wi.id] = wi; }
|
|
658
658
|
} catch { /* optional */ }
|
|
659
659
|
|
|
660
660
|
// PR-to-PRD linking — derived from PR.prdItems (single source of truth)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.413",
|
|
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"
|