@yemi33/minions 0.1.1665 → 0.1.1667
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 +11 -6
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +235 -20
- package/engine.js +91 -57
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1667 (2026-05-01)
|
|
4
|
+
|
|
5
|
+
### Other
|
|
6
|
+
- Harden PR attachment completion handling
|
|
7
|
+
|
|
8
|
+
## 0.1.1666 (2026-05-01)
|
|
9
|
+
|
|
10
|
+
### Other
|
|
11
|
+
- Fix soft routing for fix dispatches
|
|
12
|
+
|
|
13
|
+
## 0.1.1664 (2026-05-01)
|
|
4
14
|
|
|
5
15
|
### Features
|
|
6
|
-
- treat failed task_complete as dispatch failure (#1954)
|
|
7
16
|
- prevent duplicate PR fix dispatch (#1953)
|
|
8
17
|
|
|
9
|
-
### Fixes
|
|
10
|
-
- pre-dispatch agent starvation when only one agent is idle (closes #1940) (#1956)
|
|
11
|
-
- Engine clean-exit agent runs incorrectly marked Orphaned/silent (#1955)
|
|
12
|
-
|
|
13
18
|
## 0.1.1662 (2026-05-01)
|
|
14
19
|
|
|
15
20
|
### Other
|
package/engine/lifecycle.js
CHANGED
|
@@ -943,7 +943,8 @@ function isPrAttachmentRequired(type, item, meta = {}) {
|
|
|
943
943
|
|| item.prRequired === true
|
|
944
944
|
|| item.requiresPullRequest === true
|
|
945
945
|
|| item.itemType === 'pr';
|
|
946
|
-
|
|
946
|
+
const branchStrategy = meta.branchStrategy || item.branchStrategy;
|
|
947
|
+
if (branchStrategy === 'shared-branch' && item.itemType !== 'pr' && !explicit) return false;
|
|
947
948
|
|
|
948
949
|
// Fix/test work items dispatched against an existing PR don't produce a new
|
|
949
950
|
// PR — the agent updates meta.pr in place. Only require fresh PR attachment
|
|
@@ -960,21 +961,144 @@ function isPrAttachmentRequired(type, item, meta = {}) {
|
|
|
960
961
|
|| type === WORK_TYPE.TEST;
|
|
961
962
|
}
|
|
962
963
|
|
|
964
|
+
function readOptionalJsonStrict(filePath, label, validate) {
|
|
965
|
+
if (!filePath) return null;
|
|
966
|
+
let raw;
|
|
967
|
+
try {
|
|
968
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
969
|
+
} catch (err) {
|
|
970
|
+
if (err.code === 'ENOENT') return null;
|
|
971
|
+
throw new Error(`Cannot read ${label} JSON at ${filePath}: ${err.message}`);
|
|
972
|
+
}
|
|
973
|
+
let parsed;
|
|
974
|
+
try {
|
|
975
|
+
parsed = JSON.parse(raw);
|
|
976
|
+
} catch (err) {
|
|
977
|
+
throw new Error(`Corrupt ${label} JSON at ${filePath}: ${err.message}`);
|
|
978
|
+
}
|
|
979
|
+
if (validate && !validate(parsed)) {
|
|
980
|
+
throw new Error(`Invalid ${label} JSON shape at ${filePath}`);
|
|
981
|
+
}
|
|
982
|
+
return parsed;
|
|
983
|
+
}
|
|
984
|
+
|
|
963
985
|
function hasCanonicalPrAttachment(itemId, config) {
|
|
964
986
|
if (!itemId) return false;
|
|
987
|
+
// Cheapest probe first — getPrLinks() is in-process cached and merges canonical IDs.
|
|
965
988
|
if (Object.values(getPrLinks()).some(linkedIds => (linkedIds || []).includes(itemId))) return true;
|
|
966
989
|
const projects = shared.getProjects(config);
|
|
967
990
|
for (const p of projects) {
|
|
968
|
-
const prs =
|
|
991
|
+
const prs = readOptionalJsonStrict(shared.projectPrPath(p), 'project pull-requests', Array.isArray) || [];
|
|
969
992
|
if (prs.some(pr => (pr.prdItems || []).includes(itemId))) return true;
|
|
970
993
|
}
|
|
971
|
-
const centralPrs =
|
|
994
|
+
const centralPrs = readOptionalJsonStrict(path.join(MINIONS_DIR, 'pull-requests.json'), 'central pull-requests', Array.isArray) || [];
|
|
972
995
|
return centralPrs.some(pr => (pr.prdItems || []).includes(itemId));
|
|
973
996
|
}
|
|
974
997
|
|
|
998
|
+
function resolvePrFallbackProject(meta, config) {
|
|
999
|
+
const projects = shared.getProjects(config);
|
|
1000
|
+
if (meta?.project?.name) {
|
|
1001
|
+
const match = projects.find(p => p.name === meta.project.name);
|
|
1002
|
+
if (match) return match;
|
|
1003
|
+
}
|
|
1004
|
+
if (meta?.project?.localPath) {
|
|
1005
|
+
const metaPath = path.resolve(meta.project.localPath);
|
|
1006
|
+
const match = projects.find(p => p.localPath && path.resolve(p.localPath) === metaPath);
|
|
1007
|
+
if (match) return match;
|
|
1008
|
+
}
|
|
1009
|
+
if (meta?.item?.project) {
|
|
1010
|
+
const match = projects.find(p => p.name === meta.item.project);
|
|
1011
|
+
if (match) return match;
|
|
1012
|
+
}
|
|
1013
|
+
return projects.length === 1 ? projects[0] : null;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function runFileCapture(file, args, opts = {}) {
|
|
1017
|
+
const { timeout = 30000, ...spawnOpts } = opts;
|
|
1018
|
+
const MAX_BUFFER = 4 * 1024 * 1024; // 4MB — generous for gh/git output
|
|
1019
|
+
return new Promise((resolve, reject) => {
|
|
1020
|
+
let child;
|
|
1021
|
+
let settled = false;
|
|
1022
|
+
let timedOut = false;
|
|
1023
|
+
let killedForBuffer = false;
|
|
1024
|
+
let stdout = '';
|
|
1025
|
+
let stderr = '';
|
|
1026
|
+
let hardKillTimer = null;
|
|
1027
|
+
const finish = (fn, value) => {
|
|
1028
|
+
if (settled) return;
|
|
1029
|
+
settled = true;
|
|
1030
|
+
if (timer) clearTimeout(timer);
|
|
1031
|
+
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
1032
|
+
fn(value);
|
|
1033
|
+
};
|
|
1034
|
+
const timer = timeout ? setTimeout(() => {
|
|
1035
|
+
timedOut = true;
|
|
1036
|
+
if (!child) return;
|
|
1037
|
+
shared.killGracefully(child, 1000);
|
|
1038
|
+
// Hard-kill fallback if the child ignores SIGTERM. Cleared on close.
|
|
1039
|
+
hardKillTimer = setTimeout(() => {
|
|
1040
|
+
try { shared.killImmediate(child); } catch {}
|
|
1041
|
+
}, 2500);
|
|
1042
|
+
}, timeout) : null;
|
|
1043
|
+
try {
|
|
1044
|
+
child = shared.runFile(file, args, { ...spawnOpts, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
finish(reject, err);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
child.stdout?.setEncoding('utf8');
|
|
1050
|
+
child.stderr?.setEncoding('utf8');
|
|
1051
|
+
child.stdout?.on('data', chunk => {
|
|
1052
|
+
if (stdout.length + chunk.length > MAX_BUFFER) {
|
|
1053
|
+
if (!killedForBuffer) { killedForBuffer = true; try { shared.killImmediate(child); } catch {} }
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
stdout += chunk;
|
|
1057
|
+
});
|
|
1058
|
+
child.stderr?.on('data', chunk => {
|
|
1059
|
+
if (stderr.length + chunk.length > MAX_BUFFER) {
|
|
1060
|
+
if (!killedForBuffer) { killedForBuffer = true; try { shared.killImmediate(child); } catch {} }
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
stderr += chunk;
|
|
1064
|
+
});
|
|
1065
|
+
child.on('error', err => {
|
|
1066
|
+
err.stdout = stdout;
|
|
1067
|
+
err.stderr = stderr;
|
|
1068
|
+
finish(reject, err);
|
|
1069
|
+
});
|
|
1070
|
+
child.once('close', () => {
|
|
1071
|
+
if (hardKillTimer) { clearTimeout(hardKillTimer); hardKillTimer = null; }
|
|
1072
|
+
});
|
|
1073
|
+
child.on('close', code => {
|
|
1074
|
+
if (code === 0 && !timedOut && !killedForBuffer) {
|
|
1075
|
+
finish(resolve, stdout);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
let message;
|
|
1079
|
+
let errCode;
|
|
1080
|
+
if (timedOut) {
|
|
1081
|
+
message = `${file} timed out after ${timeout}ms`;
|
|
1082
|
+
errCode = 'ETIMEDOUT';
|
|
1083
|
+
} else if (killedForBuffer) {
|
|
1084
|
+
message = `${file} exceeded max buffer of ${MAX_BUFFER} bytes`;
|
|
1085
|
+
errCode = 'ERR_OUT_OF_RANGE';
|
|
1086
|
+
} else {
|
|
1087
|
+
message = `${file} exited with code ${code}`;
|
|
1088
|
+
errCode = code;
|
|
1089
|
+
}
|
|
1090
|
+
const err = new Error(message);
|
|
1091
|
+
err.code = errCode;
|
|
1092
|
+
err.stdout = stdout;
|
|
1093
|
+
err.stderr = stderr;
|
|
1094
|
+
finish(reject, err);
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
|
|
975
1099
|
async function findOpenPrForBranch(meta, config) {
|
|
976
1100
|
if (!meta?.branch) return null;
|
|
977
|
-
const projectObj =
|
|
1101
|
+
const projectObj = resolvePrFallbackProject(meta, config);
|
|
978
1102
|
if (!projectObj) return null;
|
|
979
1103
|
const host = projectObj.repoHost || 'ado';
|
|
980
1104
|
if (host === 'github') {
|
|
@@ -984,7 +1108,7 @@ async function findOpenPrForBranch(meta, config) {
|
|
|
984
1108
|
if (attempt > 0) await new Promise(r => setTimeout(r, 3000));
|
|
985
1109
|
let raw = '';
|
|
986
1110
|
try {
|
|
987
|
-
raw = await
|
|
1111
|
+
raw = await runFileCapture('gh', ['pr', 'list', '--head', String(meta.branch), '--repo', ghSlug, '--json', 'number,url,state', '--limit', '1'], { timeout: 15000 });
|
|
988
1112
|
const parsed = JSON.parse(raw || '[]');
|
|
989
1113
|
const hits = Array.isArray(parsed) ? parsed : [];
|
|
990
1114
|
if (hits.length > 0 && hits[0].state === 'OPEN') {
|
|
@@ -1024,6 +1148,7 @@ function _outputContainsPrUrl(output) {
|
|
|
1024
1148
|
function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity) {
|
|
1025
1149
|
const noPrWiPath = resolveWorkItemPath(meta);
|
|
1026
1150
|
const isHard = severity !== 'soft';
|
|
1151
|
+
let syncNeedsReviewToPrd = false;
|
|
1027
1152
|
if (noPrWiPath) {
|
|
1028
1153
|
mutateJsonFileLocked(noPrWiPath, data => {
|
|
1029
1154
|
if (!Array.isArray(data)) return data;
|
|
@@ -1034,6 +1159,7 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity)
|
|
|
1034
1159
|
w._missingPrAttachment = true;
|
|
1035
1160
|
w.failReason = reason;
|
|
1036
1161
|
w._lastReviewReason = reason;
|
|
1162
|
+
syncNeedsReviewToPrd = !!meta.item?.sourcePlan;
|
|
1037
1163
|
delete w.completedAt;
|
|
1038
1164
|
delete w._noPr;
|
|
1039
1165
|
delete w._noPrReason;
|
|
@@ -1047,6 +1173,9 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity)
|
|
|
1047
1173
|
return data;
|
|
1048
1174
|
}, { skipWriteIfUnchanged: true });
|
|
1049
1175
|
}
|
|
1176
|
+
if (isHard && syncNeedsReviewToPrd) {
|
|
1177
|
+
syncPrdItemStatus(meta.item.id, WI_STATUS.NEEDS_REVIEW, meta.item.sourcePlan);
|
|
1178
|
+
}
|
|
1050
1179
|
if (isHard) {
|
|
1051
1180
|
shared.writeToInbox('engine', `missing-pr-attachment-${meta.item.id}`,
|
|
1052
1181
|
`# PR attachment missing for ${meta.item.id}\n\n` +
|
|
@@ -1075,9 +1204,50 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity)
|
|
|
1075
1204
|
}
|
|
1076
1205
|
}
|
|
1077
1206
|
|
|
1207
|
+
function markPrAttachmentVerificationError(meta, agentId, reason, resultSummary) {
|
|
1208
|
+
const wiPath = resolveWorkItemPath(meta);
|
|
1209
|
+
let syncNeedsReviewToPrd = false;
|
|
1210
|
+
if (wiPath) {
|
|
1211
|
+
mutateJsonFileLocked(wiPath, data => {
|
|
1212
|
+
if (!Array.isArray(data)) return data;
|
|
1213
|
+
const w = data.find(i => i.id === meta.item.id);
|
|
1214
|
+
if (!w) return data;
|
|
1215
|
+
w.status = WI_STATUS.NEEDS_REVIEW;
|
|
1216
|
+
w._prAttachmentStateError = true;
|
|
1217
|
+
w.failReason = reason;
|
|
1218
|
+
w._lastReviewReason = reason;
|
|
1219
|
+
syncNeedsReviewToPrd = !!meta.item?.sourcePlan;
|
|
1220
|
+
delete w.completedAt;
|
|
1221
|
+
delete w._missingPrAttachment;
|
|
1222
|
+
delete w._unverifiedPrAttachment;
|
|
1223
|
+
return data;
|
|
1224
|
+
}, { skipWriteIfUnchanged: true });
|
|
1225
|
+
}
|
|
1226
|
+
if (syncNeedsReviewToPrd) {
|
|
1227
|
+
syncPrdItemStatus(meta.item.id, WI_STATUS.NEEDS_REVIEW, meta.item.sourcePlan);
|
|
1228
|
+
}
|
|
1229
|
+
shared.writeToInbox('engine', `pr-attachment-state-error-${meta.item.id}`,
|
|
1230
|
+
`# PR attachment verification blocked for ${meta.item.id}\n\n` +
|
|
1231
|
+
`**Agent:** ${agentId}\n` +
|
|
1232
|
+
`**Work item:** \`${meta.item.id}\` — ${meta.item.title || ''}\n` +
|
|
1233
|
+
`**Type:** ${meta.item.type || 'unknown'}\n` +
|
|
1234
|
+
`**Branch:** ${meta.branch || '(none)'}\n\n` +
|
|
1235
|
+
`${reason}\n` +
|
|
1236
|
+
(resultSummary ? `\n## Agent summary\n${resultSummary}\n` : ''),
|
|
1237
|
+
null,
|
|
1238
|
+
{ sourceItem: meta.item.id, reason: 'pr-attachment-state-error' });
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1078
1241
|
async function enforcePrAttachmentContract(type, meta, agentId, config, resultSummary, output) {
|
|
1079
1242
|
if (!isPrAttachmentRequired(type, meta?.item, meta)) return null;
|
|
1080
|
-
|
|
1243
|
+
try {
|
|
1244
|
+
if (hasCanonicalPrAttachment(meta.item.id, config)) return null;
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
const reason = `${meta.item.id} completed but PR attachment verification could not read PR tracking state: ${err.message}`;
|
|
1247
|
+
markPrAttachmentVerificationError(meta, agentId, reason, resultSummary);
|
|
1248
|
+
log('warn', reason);
|
|
1249
|
+
return { reason, itemId: meta.item.id, severity: 'hard', stateError: true };
|
|
1250
|
+
}
|
|
1081
1251
|
|
|
1082
1252
|
const found = await findOpenPrForBranch(meta, config);
|
|
1083
1253
|
if (found) {
|
|
@@ -1100,7 +1270,14 @@ async function enforcePrAttachmentContract(type, meta, agentId, config, resultSu
|
|
|
1100
1270
|
itemId: meta.item.id,
|
|
1101
1271
|
});
|
|
1102
1272
|
log('info', `Auto-linked existing PR ${entry.id} on branch ${meta.branch} for ${meta.item.id}`);
|
|
1103
|
-
|
|
1273
|
+
try {
|
|
1274
|
+
if (hasCanonicalPrAttachment(meta.item.id, config)) return null;
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
const reason = `${meta.item.id} auto-linked a PR but PR attachment verification could not read PR tracking state: ${err.message}`;
|
|
1277
|
+
markPrAttachmentVerificationError(meta, agentId, reason, resultSummary);
|
|
1278
|
+
log('warn', reason);
|
|
1279
|
+
return { reason, itemId: meta.item.id, severity: 'hard', stateError: true };
|
|
1280
|
+
}
|
|
1104
1281
|
}
|
|
1105
1282
|
|
|
1106
1283
|
// Distinguish "agent never claimed a PR" (hard — silent failure the contract
|
|
@@ -1737,13 +1914,7 @@ function parseStructuredCompletion(stdout, runtimeName) {
|
|
|
1737
1914
|
if (!stdout || typeof stdout !== 'string') return null;
|
|
1738
1915
|
|
|
1739
1916
|
// Extract text from stream-json output if needed
|
|
1740
|
-
|
|
1741
|
-
if (stdout.includes('"type":')) {
|
|
1742
|
-
try {
|
|
1743
|
-
const parsed = shared.parseStreamJsonOutput(stdout, runtimeName);
|
|
1744
|
-
if (parsed.text) text = parsed.text;
|
|
1745
|
-
} catch {}
|
|
1746
|
-
}
|
|
1917
|
+
const text = extractCompletionText(stdout, runtimeName);
|
|
1747
1918
|
|
|
1748
1919
|
// Find all ```completion blocks, take the last one
|
|
1749
1920
|
const blockPattern = /```completion\s*\n([\s\S]*?)```/g;
|
|
@@ -1760,6 +1931,22 @@ function parseStructuredCompletion(stdout, runtimeName) {
|
|
|
1760
1931
|
return parseCompletionKeyValues(lastMatch);
|
|
1761
1932
|
}
|
|
1762
1933
|
|
|
1934
|
+
function extractCompletionText(stdout, runtimeName) {
|
|
1935
|
+
let text = stdout;
|
|
1936
|
+
if (typeof stdout === 'string' && stdout.includes('"type":')) {
|
|
1937
|
+
try {
|
|
1938
|
+
const parsed = shared.parseStreamJsonOutput(stdout, runtimeName);
|
|
1939
|
+
if (parsed.text) text = parsed.text;
|
|
1940
|
+
} catch {}
|
|
1941
|
+
}
|
|
1942
|
+
return text;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
function hasCompletionFence(stdout, runtimeName) {
|
|
1946
|
+
const text = extractCompletionText(stdout, runtimeName);
|
|
1947
|
+
return /```completion\s*\n[\s\S]*?```/.test(text);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1763
1950
|
function extractTaskCompleteSummary(stdout) {
|
|
1764
1951
|
if (!stdout || typeof stdout !== 'string') return '';
|
|
1765
1952
|
let summary = '';
|
|
@@ -1817,6 +2004,29 @@ function parseCompletionKeyValues(text) {
|
|
|
1817
2004
|
return result;
|
|
1818
2005
|
}
|
|
1819
2006
|
|
|
2007
|
+
function parseCompletionFieldSummary(text) {
|
|
2008
|
+
if (!text || typeof text !== 'string') return null;
|
|
2009
|
+
|
|
2010
|
+
const allowedFields = new Set(shared.COMPLETION_FIELDS || []);
|
|
2011
|
+
const result = {};
|
|
2012
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
2013
|
+
const line = rawLine.trim().replace(/^[-*]\s+/, '');
|
|
2014
|
+
const colonIdx = line.indexOf(':');
|
|
2015
|
+
if (colonIdx < 1) continue;
|
|
2016
|
+
const key = line.slice(0, colonIdx).trim().toLowerCase().replace(/[\s-]+/g, '_');
|
|
2017
|
+
if (!allowedFields.has(key)) continue;
|
|
2018
|
+
const value = line.slice(colonIdx + 1).trim().replace(/^["'`]+|["'`]+$/g, '');
|
|
2019
|
+
if (value) result[key] = value;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
if (!result.status) return null;
|
|
2023
|
+
const fieldCount = Object.keys(result).length;
|
|
2024
|
+
const status = normalizeCompletionStatus(result.status);
|
|
2025
|
+
const explicitlyFailed = status.startsWith('fail') || status === 'error';
|
|
2026
|
+
if (fieldCount < 2 && !explicitlyFailed) return null;
|
|
2027
|
+
return result;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1820
2030
|
function parseCompletionReportFile(dispatchItem, opts = {}) {
|
|
1821
2031
|
const reportPath = dispatchItem?.meta?.completionReportPath || shared.dispatchCompletionReportPath(dispatchItem?.id);
|
|
1822
2032
|
if (!reportPath || !fs.existsSync(reportPath)) {
|
|
@@ -1875,10 +2085,11 @@ function normalizeCompletionStatus(status) {
|
|
|
1875
2085
|
// and burned 3-9 minutes of agent time per false-positive retry.
|
|
1876
2086
|
//
|
|
1877
2087
|
// Both structured signals (the JSON completion report at MINIONS_COMPLETION_REPORT
|
|
1878
|
-
// and the fenced ```completion block in stdout) carry a `status` field.
|
|
1879
|
-
//
|
|
1880
|
-
//
|
|
1881
|
-
//
|
|
2088
|
+
// and the fenced ```completion block in stdout) carry a `status` field. A plain
|
|
2089
|
+
// task_complete summary made only of completion fields is accepted as a narrow
|
|
2090
|
+
// compatibility fallback. If the agent explicitly says they're not done, honor
|
|
2091
|
+
// it; otherwise accept the dispatch. The PR attachment contract still catches
|
|
2092
|
+
// silent-failure cases for PR-producing work.
|
|
1882
2093
|
const NON_TERMINAL_COMPLETION_STATUSES = new Set([
|
|
1883
2094
|
'partial', 'partially-complete', 'in-progress', 'pending', 'deferred',
|
|
1884
2095
|
'blocked', 'incomplete', 'to-be-continued',
|
|
@@ -2110,8 +2321,11 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
2110
2321
|
|
|
2111
2322
|
// Prefer the sidecar completion report; keep fenced output as a compatibility fallback.
|
|
2112
2323
|
const reportCompletion = parseCompletionReportFile(dispatchItem, { warnIfMissing: true });
|
|
2113
|
-
const
|
|
2114
|
-
const
|
|
2324
|
+
const fencedCompletion = reportCompletion ? null : parseStructuredCompletion(stdout, runtimeName);
|
|
2325
|
+
const summaryCompletion = reportCompletion || fencedCompletion ? null : parseCompletionFieldSummary(resultSummary);
|
|
2326
|
+
const fallbackCompletion = fencedCompletion || summaryCompletion;
|
|
2327
|
+
const fallbackSource = fencedCompletion && hasCompletionFence(stdout, runtimeName) ? 'fenced-completion' : 'summary-completion';
|
|
2328
|
+
const structuredCompletion = reportCompletion || persistCompletionReport(dispatchItem, fallbackCompletion, fallbackSource);
|
|
2115
2329
|
if (structuredCompletion) {
|
|
2116
2330
|
if (structuredCompletion.summary) resultSummary = String(structuredCompletion.summary);
|
|
2117
2331
|
log('info', `Structured completion from ${agentId}: status=${structuredCompletion.status}, pr=${structuredCompletion.pr || 'N/A'}${structuredCompletion._source ? ` (${structuredCompletion._source})` : ''}`);
|
|
@@ -2625,6 +2839,7 @@ module.exports = {
|
|
|
2625
2839
|
parseReviewVerdict,
|
|
2626
2840
|
isReviewBailout,
|
|
2627
2841
|
parseStructuredCompletion,
|
|
2842
|
+
parseCompletionFieldSummary,
|
|
2628
2843
|
detectNonTerminalResultSummary,
|
|
2629
2844
|
parseCompletionReportFile,
|
|
2630
2845
|
persistCompletionReport,
|
package/engine.js
CHANGED
|
@@ -1365,13 +1365,15 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1365
1365
|
let errorReason = '';
|
|
1366
1366
|
if (hardContractFail) {
|
|
1367
1367
|
errorReason = completionContractFailure.reason || 'PR attachment contract failed';
|
|
1368
|
-
} else if (agentReportedFailure
|
|
1369
|
-
errorReason =
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1368
|
+
} else if (agentReportedFailure) {
|
|
1369
|
+
errorReason = structuredCompletion
|
|
1370
|
+
? String(
|
|
1371
|
+
structuredCompletion.summary
|
|
1372
|
+
|| structuredCompletion.pending
|
|
1373
|
+
|| structuredCompletion.failure_class
|
|
1374
|
+
|| `Agent reported ${structuredCompletion.status || 'failure'}`
|
|
1375
|
+
).slice(0, 300)
|
|
1376
|
+
: 'Agent reported failure';
|
|
1375
1377
|
} else if (effectiveResult === DISPATCH_RESULT.ERROR) {
|
|
1376
1378
|
errorReason = stderr.split('\n').filter(l => l.trim()).slice(-5).join(' | ').trim().slice(0, 300);
|
|
1377
1379
|
// W-mo3zul9pirjb — when claude CLI exits in <3s with code 1 and no output (the
|
|
@@ -2753,7 +2755,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
2753
2755
|
return b && b > 0 && getMonthlySpend(id) >= b && isAgentIdle(id);
|
|
2754
2756
|
});
|
|
2755
2757
|
if (!agentId) {
|
|
2756
|
-
if (!budgetBlocked && !hardPinRequested) {
|
|
2758
|
+
if (!budgetBlocked && !hardPinRequested && workType !== WORK_TYPE.FIX) {
|
|
2757
2759
|
reservedAgentId = resolveAgentReservation(workType, config, { agentHints });
|
|
2758
2760
|
agentId = reservedAgentId && !hasAgentHints ? routing.ANY_AGENT : reservedAgentId;
|
|
2759
2761
|
}
|
|
@@ -3253,7 +3255,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3253
3255
|
const hardPinRequested = routing.isAgentHardPinned(item);
|
|
3254
3256
|
const agentId = routing.getHardPinnedAgent(item, config.agents || {})
|
|
3255
3257
|
|| (!hardPinRequested ? resolveAgent(workType, config, { agentHints }) : null)
|
|
3256
|
-
|| (!hardPinRequested ? resolveAgentReservation(workType, config, { agentHints }) : null);
|
|
3258
|
+
|| (!hardPinRequested && workType !== WORK_TYPE.FIX ? resolveAgentReservation(workType, config, { agentHints }) : null);
|
|
3257
3259
|
if (!agentId) continue;
|
|
3258
3260
|
|
|
3259
3261
|
const agentName = config.agents[agentId]?.name || agentId;
|
|
@@ -3561,6 +3563,62 @@ async function discoverWork(config) {
|
|
|
3561
3563
|
return allWork.length;
|
|
3562
3564
|
}
|
|
3563
3565
|
|
|
3566
|
+
function getPendingDispatchRoutingOpts(item) {
|
|
3567
|
+
const opts = { agentHints: routing.extractAgentHints(item?.meta?.item) };
|
|
3568
|
+
const authorAgent = item?.meta?.pr?.agent;
|
|
3569
|
+
if (authorAgent) opts.authorAgent = authorAgent;
|
|
3570
|
+
return opts;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
function resolvePendingDispatchAgent(item, config) {
|
|
3574
|
+
return resolveAgent(
|
|
3575
|
+
routing.normalizeWorkType(item?.type, WORK_TYPE.IMPLEMENT),
|
|
3576
|
+
config,
|
|
3577
|
+
getPendingDispatchRoutingOpts(item)
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
function assignPendingDispatchAgent(item, agentId, config) {
|
|
3582
|
+
const agents = config.agents || {};
|
|
3583
|
+
item.agent = agentId;
|
|
3584
|
+
item.agentName = agents[agentId]?.name || tempAgents.get(agentId)?.name || agentId;
|
|
3585
|
+
item.agentRole = agents[agentId]?.role || tempAgents.get(agentId)?.role || 'Agent';
|
|
3586
|
+
delete item._agentBusySince;
|
|
3587
|
+
delete item.skipReason;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
function clearPendingDispatchAgent(item) {
|
|
3591
|
+
delete item.agent;
|
|
3592
|
+
delete item.agentName;
|
|
3593
|
+
delete item.agentRole;
|
|
3594
|
+
delete item._agentBusySince;
|
|
3595
|
+
delete item.skipReason;
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
function persistPendingDispatchAgent(item) {
|
|
3599
|
+
mutateDispatch((dp) => {
|
|
3600
|
+
const p = (dp.pending || []).find(d => d.id === item.id);
|
|
3601
|
+
if (p) {
|
|
3602
|
+
if (item.agent) {
|
|
3603
|
+
p.agent = item.agent;
|
|
3604
|
+
p.agentName = item.agentName;
|
|
3605
|
+
p.agentRole = item.agentRole;
|
|
3606
|
+
} else {
|
|
3607
|
+
delete p.agent;
|
|
3608
|
+
delete p.agentName;
|
|
3609
|
+
delete p.agentRole;
|
|
3610
|
+
}
|
|
3611
|
+
delete p._agentBusySince;
|
|
3612
|
+
delete p.skipReason;
|
|
3613
|
+
}
|
|
3614
|
+
return dp;
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
function isSoftFixDispatch(item) {
|
|
3619
|
+
return item?.type === WORK_TYPE.FIX && !routing.isAgentHardPinned(item.meta?.item);
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3564
3622
|
// ─── Main Tick ──────────────────────────────────────────────────────────────
|
|
3565
3623
|
|
|
3566
3624
|
let tickCount = 0;
|
|
@@ -3955,27 +4013,17 @@ async function tickInner() {
|
|
|
3955
4013
|
// be of type string. Received undefined` and re-queues — every tick. Try to
|
|
3956
4014
|
// resolve a fallback via routing; if none is available, skip this tick.
|
|
3957
4015
|
if (!item.agent || typeof item.agent !== 'string') {
|
|
3958
|
-
const fallback =
|
|
4016
|
+
const fallback = resolvePendingDispatchAgent(item, config);
|
|
3959
4017
|
if (!fallback) {
|
|
3960
4018
|
log('warn', `Pending dispatch ${item.id} has no agent and routing returned no fallback — skipping`);
|
|
3961
4019
|
continue;
|
|
3962
4020
|
}
|
|
3963
4021
|
log('info', `Pending dispatch ${item.id} missing agent; routed → ${fallback} (#1206 guard)`);
|
|
3964
|
-
item
|
|
3965
|
-
item.agentName = config.agents[fallback]?.name || tempAgents.get(fallback)?.name || fallback;
|
|
3966
|
-
item.agentRole = config.agents[fallback]?.role || tempAgents.get(fallback)?.role || 'Agent';
|
|
4022
|
+
assignPendingDispatchAgent(item, fallback, config);
|
|
3967
4023
|
// Persist so the fix survives across ticks even if this dispatch is skipped
|
|
3968
4024
|
// later in the loop (branch lock, concurrency cap, agent busy, etc.).
|
|
3969
4025
|
try {
|
|
3970
|
-
|
|
3971
|
-
const p = (dp.pending || []).find(d => d.id === item.id);
|
|
3972
|
-
if (p) {
|
|
3973
|
-
p.agent = item.agent;
|
|
3974
|
-
p.agentName = item.agentName;
|
|
3975
|
-
p.agentRole = item.agentRole;
|
|
3976
|
-
}
|
|
3977
|
-
return dp;
|
|
3978
|
-
});
|
|
4026
|
+
persistPendingDispatchAgent(item);
|
|
3979
4027
|
} catch (e) { log('warn', `Persist agent resolution for ${item.id} failed: ${e.message}`); }
|
|
3980
4028
|
}
|
|
3981
4029
|
// #1204: Pre-assigned unspawned temp agents never unblock naturally.
|
|
@@ -3986,27 +4034,13 @@ async function tickInner() {
|
|
|
3986
4034
|
// them eagerly before the busy check so an idle named agent can pick up.
|
|
3987
4035
|
const isUnspawnedTemp = item.agent?.startsWith('temp-') && !busyAgents.has(item.agent);
|
|
3988
4036
|
if (isUnspawnedTemp) {
|
|
3989
|
-
const altAgent =
|
|
4037
|
+
const altAgent = resolvePendingDispatchAgent(item, config);
|
|
3990
4038
|
if (altAgent && altAgent !== item.agent) {
|
|
3991
4039
|
const prevAgent = item.agent;
|
|
3992
|
-
item
|
|
3993
|
-
item.agentName = config.agents[altAgent]?.name || tempAgents.get(altAgent)?.name || altAgent;
|
|
3994
|
-
item.agentRole = config.agents[altAgent]?.role || tempAgents.get(altAgent)?.role || 'Agent';
|
|
3995
|
-
delete item._agentBusySince;
|
|
3996
|
-
delete item.skipReason;
|
|
4040
|
+
assignPendingDispatchAgent(item, altAgent, config);
|
|
3997
4041
|
log('info', `Reassigning ${item.id} from unspawned temp ${prevAgent} to ${altAgent} — temp agent never spawned`);
|
|
3998
4042
|
// Persist reassignment to dispatch.json so it survives restarts/ticks
|
|
3999
|
-
|
|
4000
|
-
const p = (dp.pending || []).find(d => d.id === item.id);
|
|
4001
|
-
if (p) {
|
|
4002
|
-
p.agent = altAgent;
|
|
4003
|
-
p.agentName = item.agentName;
|
|
4004
|
-
p.agentRole = item.agentRole;
|
|
4005
|
-
delete p._agentBusySince;
|
|
4006
|
-
delete p.skipReason;
|
|
4007
|
-
}
|
|
4008
|
-
return dp;
|
|
4009
|
-
});
|
|
4043
|
+
persistPendingDispatchAgent(item);
|
|
4010
4044
|
}
|
|
4011
4045
|
}
|
|
4012
4046
|
if (busyAgents.has(item.agent)) {
|
|
@@ -4014,30 +4048,30 @@ async function tickInner() {
|
|
|
4014
4048
|
// try to find an alternative agent via routing. Skip explicitly assigned items.
|
|
4015
4049
|
const reassignMs = config.engine?.agentBusyReassignMs ?? ENGINE_DEFAULTS.agentBusyReassignMs;
|
|
4016
4050
|
const isHardPinned = routing.isAgentHardPinned(item.meta?.item);
|
|
4017
|
-
if (
|
|
4051
|
+
if (isSoftFixDispatch(item)) {
|
|
4052
|
+
const originalAgent = item.agent;
|
|
4053
|
+
const altAgent = resolvePendingDispatchAgent(item, config);
|
|
4054
|
+
if (altAgent && altAgent !== originalAgent && !busyAgents.has(altAgent)) {
|
|
4055
|
+
log('info', `Reassigning ${item.id} from ${originalAgent} to ${altAgent} — soft fix suggestion unavailable`);
|
|
4056
|
+
assignPendingDispatchAgent(item, altAgent, config);
|
|
4057
|
+
persistPendingDispatchAgent(item);
|
|
4058
|
+
// Fall through to branch mutex / concurrency checks below.
|
|
4059
|
+
} else {
|
|
4060
|
+
log('info', `Clearing busy soft fix agent on ${item.id} (${originalAgent}) — waiting for any available agent`);
|
|
4061
|
+
clearPendingDispatchAgent(item);
|
|
4062
|
+
persistPendingDispatchAgent(item);
|
|
4063
|
+
continue;
|
|
4064
|
+
}
|
|
4065
|
+
} else if (!isHardPinned && reassignMs > 0 && item._agentBusySince) {
|
|
4018
4066
|
const busySinceMs = new Date(item._agentBusySince).getTime();
|
|
4019
4067
|
if (Date.now() - busySinceMs > reassignMs) {
|
|
4020
4068
|
const originalAgent = item.agent;
|
|
4021
|
-
const altAgent =
|
|
4069
|
+
const altAgent = resolvePendingDispatchAgent(item, config);
|
|
4022
4070
|
if (altAgent && altAgent !== originalAgent && !busyAgents.has(altAgent)) {
|
|
4023
4071
|
log('info', `Reassigning ${item.id} from ${originalAgent} to ${altAgent} — agent busy > ${reassignMs}ms`);
|
|
4024
|
-
item
|
|
4025
|
-
item.agentName = config.agents[altAgent]?.name || tempAgents.get(altAgent)?.name || altAgent;
|
|
4026
|
-
item.agentRole = config.agents[altAgent]?.role || tempAgents.get(altAgent)?.role || 'Agent';
|
|
4027
|
-
delete item._agentBusySince;
|
|
4028
|
-
delete item.skipReason;
|
|
4072
|
+
assignPendingDispatchAgent(item, altAgent, config);
|
|
4029
4073
|
// Persist reassignment to dispatch.json
|
|
4030
|
-
|
|
4031
|
-
const p = (dp.pending || []).find(d => d.id === item.id);
|
|
4032
|
-
if (p) {
|
|
4033
|
-
p.agent = altAgent;
|
|
4034
|
-
p.agentName = item.agentName;
|
|
4035
|
-
p.agentRole = item.agentRole;
|
|
4036
|
-
delete p._agentBusySince;
|
|
4037
|
-
delete p.skipReason;
|
|
4038
|
-
}
|
|
4039
|
-
return dp;
|
|
4040
|
-
});
|
|
4074
|
+
persistPendingDispatchAgent(item);
|
|
4041
4075
|
// Fall through to branch mutex / concurrency checks below
|
|
4042
4076
|
} else {
|
|
4043
4077
|
continue; // No alternative agent available — keep waiting
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1667",
|
|
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"
|