@yemi33/minions 0.1.1649 → 0.1.1650
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 +3 -1
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +130 -1
- package/engine/timeout.js +17 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1650 (2026-05-01)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- reject premature task_complete for nonterminal summaries
|
|
6
7
|
- ADO build poll repositoryId GUID handling
|
|
7
8
|
|
|
8
9
|
### Fixes
|
|
10
|
+
- yemi33/minions#1927
|
|
9
11
|
- yemi33/minions#1925
|
|
10
12
|
|
|
11
13
|
## 0.1.1648 (2026-05-01)
|
package/engine/lifecycle.js
CHANGED
|
@@ -1704,6 +1704,119 @@ function normalizeCompletionStatus(status) {
|
|
|
1704
1704
|
return String(status || '').trim().toLowerCase().replace(/[\s_]+/g, '-');
|
|
1705
1705
|
}
|
|
1706
1706
|
|
|
1707
|
+
function isTerminalPendingValue(value) {
|
|
1708
|
+
const text = String(value || '').trim().toLowerCase();
|
|
1709
|
+
if (!text) return true;
|
|
1710
|
+
return /^(?:none|n\/a|na|no|nothing|not-applicable|not applicable|-)$/.test(text)
|
|
1711
|
+
|| /^no\s+(?:pending|remaining|outstanding)\b/.test(text)
|
|
1712
|
+
|| /^(?:all\s+)?(?:pending|remaining|outstanding)\s+(?:work|items?|tasks?)?\s*(?:resolved|complete|completed|done|closed)$/.test(text);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function isTerminalPendingLine(line) {
|
|
1716
|
+
const text = String(line || '').trim().toLowerCase();
|
|
1717
|
+
return /\bno\s+pending\b/.test(text)
|
|
1718
|
+
|| /\bpending\s*[:=-]\s*(?:none|n\/a|na|no|nothing|not applicable|-)\b/.test(text)
|
|
1719
|
+
|| /\bpending\s+(?:work|items?|tasks?)?\s*(?:resolved|complete|completed|done|closed)\b/.test(text);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function detectNonTerminalResultSummary(resultSummary, structuredCompletion) {
|
|
1723
|
+
const completionStatus = normalizeCompletionStatus(structuredCompletion?.status);
|
|
1724
|
+
if (completionStatus) {
|
|
1725
|
+
if (/^(?:partial|partially-complete|in-progress|pending|deferred|blocked|incomplete|to-be-continued)/.test(completionStatus)) {
|
|
1726
|
+
return {
|
|
1727
|
+
phrase: `status:${structuredCompletion.status}`,
|
|
1728
|
+
reason: `Nonterminal completion summary: structured status is '${structuredCompletion.status}'`,
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
if (/^(?:fail|failed|failure|error)/.test(completionStatus)) {
|
|
1732
|
+
return {
|
|
1733
|
+
phrase: `status:${structuredCompletion.status}`,
|
|
1734
|
+
reason: `Nonterminal completion summary: structured status is '${structuredCompletion.status}', not a successful terminal state`,
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
if (structuredCompletion?.pending && !isTerminalPendingValue(structuredCompletion.pending)) {
|
|
1740
|
+
return {
|
|
1741
|
+
phrase: 'pending',
|
|
1742
|
+
reason: `Nonterminal completion summary: pending work remains (${String(structuredCompletion.pending).slice(0, 160)})`,
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
const text = String(resultSummary || '').replace(/\r/g, '').trim();
|
|
1747
|
+
if (!text) return null;
|
|
1748
|
+
|
|
1749
|
+
const patterns = [
|
|
1750
|
+
{ phrase: 'still running', re: /\b(?:still|currently|continues?\s+to\s+be)\s+(?:running|ongoing|in\s+progress)\b/i },
|
|
1751
|
+
{ phrase: 'will check later', re: /\b(?:i(?:'|’)ll|i\s+will|we(?:'|’)ll|we\s+will|will)\s+(?:check|verify|review|follow\s+up|revisit)\s+(?:again\s+)?(?:later|soon|in\b|after\b|when\b)/i },
|
|
1752
|
+
{ phrase: 'wake up', re: /\bwake(?:\s|-)?up\b|\bwake\b.*\b(?:check|verify|review)\b/i },
|
|
1753
|
+
{ phrase: 'not yet complete', re: /\b(?:not\s+yet|isn(?:'|’)t|not|incomplete|not\s+fully|not\s+completely)\s+(?:complete|completed|done|finished|validated|verified)\b/i },
|
|
1754
|
+
{ phrase: 'partial', re: /\bpartial(?:ly)?\b/i },
|
|
1755
|
+
{ phrase: 'to be continued', re: /\bto\s+be\s+continued\b|\btbc\b/i },
|
|
1756
|
+
{ phrase: 'in progress', re: /\bin\s+progress\b|\bongoing\b|\bincomplete\b/i },
|
|
1757
|
+
];
|
|
1758
|
+
for (const { phrase, re } of patterns) {
|
|
1759
|
+
if (re.test(text)) {
|
|
1760
|
+
return { phrase, reason: `Nonterminal completion summary: matched '${phrase}'` };
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const pendingLines = text.split('\n').filter(line => /\bpending\b/i.test(line));
|
|
1765
|
+
for (const line of pendingLines) {
|
|
1766
|
+
if (!isTerminalPendingLine(line)) {
|
|
1767
|
+
return { phrase: 'pending', reason: `Nonterminal completion summary: matched 'pending'` };
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
return null;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
function deferNonTerminalCompletion(meta, detection) {
|
|
1775
|
+
const itemId = meta?.item?.id;
|
|
1776
|
+
const reason = detection?.reason || 'Nonterminal completion summary';
|
|
1777
|
+
if (!itemId) return reason;
|
|
1778
|
+
const wiPath = resolveWorkItemPath(meta);
|
|
1779
|
+
if (!wiPath) return reason;
|
|
1780
|
+
|
|
1781
|
+
let finalStatus = WI_STATUS.PENDING;
|
|
1782
|
+
try {
|
|
1783
|
+
mutateJsonFileLocked(wiPath, data => {
|
|
1784
|
+
if (!Array.isArray(data)) return data;
|
|
1785
|
+
const w = data.find(i => i.id === itemId);
|
|
1786
|
+
if (!w) return data;
|
|
1787
|
+
const retries = w._retryCount || 0;
|
|
1788
|
+
if (retries < ENGINE_DEFAULTS.maxRetries) {
|
|
1789
|
+
w.status = WI_STATUS.PENDING;
|
|
1790
|
+
w._retryCount = retries + 1;
|
|
1791
|
+
w._lastRetryAt = ts();
|
|
1792
|
+
w._lastRetryReason = reason;
|
|
1793
|
+
w._pendingReason = 'nonterminal_completion';
|
|
1794
|
+
delete w.completedAt;
|
|
1795
|
+
delete w.dispatched_at;
|
|
1796
|
+
delete w.dispatched_to;
|
|
1797
|
+
delete w.failedAt;
|
|
1798
|
+
finalStatus = WI_STATUS.PENDING;
|
|
1799
|
+
log('warn', `Work item ${itemId} reported nonterminal success — retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries}: ${reason}`);
|
|
1800
|
+
} else {
|
|
1801
|
+
w.status = WI_STATUS.FAILED;
|
|
1802
|
+
w.failReason = `${reason} after ${ENGINE_DEFAULTS.maxRetries} attempts`;
|
|
1803
|
+
w.failedAt = ts();
|
|
1804
|
+
delete w.completedAt;
|
|
1805
|
+
delete w.dispatched_at;
|
|
1806
|
+
delete w.dispatched_to;
|
|
1807
|
+
delete w._pendingReason;
|
|
1808
|
+
finalStatus = WI_STATUS.FAILED;
|
|
1809
|
+
log('warn', `Work item ${itemId} failed — repeated nonterminal completion summaries after ${ENGINE_DEFAULTS.maxRetries} attempts`);
|
|
1810
|
+
}
|
|
1811
|
+
return data;
|
|
1812
|
+
}, { defaultValue: [], skipWriteIfUnchanged: true });
|
|
1813
|
+
syncPrdItemStatus(itemId, finalStatus, meta.item?.sourcePlan);
|
|
1814
|
+
} catch (err) {
|
|
1815
|
+
log('warn', `nonterminal completion gate: ${err.message}`);
|
|
1816
|
+
}
|
|
1817
|
+
return reason;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1707
1820
|
function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredCompletion, resultSummary, exitCode) {
|
|
1708
1821
|
if (!dispatchItem?.id || !outcome) {
|
|
1709
1822
|
log('warn', 'Cannot write non-clean agent report without dispatch id and outcome');
|
|
@@ -1840,6 +1953,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1840
1953
|
// (P-2a6d9c4f, P-9c4f2d6a) populate dispatchItem.meta.runtimeName at spawn time.
|
|
1841
1954
|
const runtimeName = dispatchItem.meta?.runtimeName || dispatchItem.runtimeName || 'claude';
|
|
1842
1955
|
const { resultSummary, taskUsage, sessionId, model } = parseAgentOutput(stdout, runtimeName);
|
|
1956
|
+
const completionGateSummary = resultSummary || (typeof stdout === 'string' && !stdout.includes('"type":') ? stdout : '');
|
|
1843
1957
|
|
|
1844
1958
|
// Try structured completion protocol first (```completion block from agent output)
|
|
1845
1959
|
const structuredCompletion = parseStructuredCompletion(stdout, runtimeName);
|
|
@@ -1878,9 +1992,11 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1878
1992
|
const effectiveSuccess = isSuccess || autoRecovered;
|
|
1879
1993
|
|
|
1880
1994
|
const completionStatus = normalizeCompletionStatus(structuredCompletion?.status);
|
|
1995
|
+
let nonCleanReportWritten = false;
|
|
1881
1996
|
if (completionStatus.startsWith('partial') || autoRecovered || (completionStatus.startsWith('fail') && isSuccess)) {
|
|
1882
1997
|
const outcome = completionStatus.startsWith('fail') ? 'failure' : 'partial';
|
|
1883
|
-
writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredCompletion,
|
|
1998
|
+
writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredCompletion, completionGateSummary, code);
|
|
1999
|
+
nonCleanReportWritten = true;
|
|
1884
2000
|
}
|
|
1885
2001
|
|
|
1886
2002
|
// Handle decomposition results — create sub-items from decompose agent output
|
|
@@ -1987,6 +2103,18 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1987
2103
|
}
|
|
1988
2104
|
|
|
1989
2105
|
let completionContractFailure = null;
|
|
2106
|
+
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus) {
|
|
2107
|
+
const nonTerminalCompletion = detectNonTerminalResultSummary(completionGateSummary, structuredCompletion);
|
|
2108
|
+
if (nonTerminalCompletion) {
|
|
2109
|
+
skipDoneStatus = true;
|
|
2110
|
+
const reason = deferNonTerminalCompletion(meta, nonTerminalCompletion);
|
|
2111
|
+
completionContractFailure = { reason, itemId: meta.item.id, nonTerminal: true };
|
|
2112
|
+
if (!nonCleanReportWritten) {
|
|
2113
|
+
writeNonCleanAgentReport(dispatchItem, agentId, 'partial', structuredCompletion, completionGateSummary, code);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
1990
2118
|
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus) {
|
|
1991
2119
|
completionContractFailure = await enforcePrAttachmentContract(type, meta, agentId, config, resultSummary);
|
|
1992
2120
|
if (completionContractFailure) skipDoneStatus = true;
|
|
@@ -2322,6 +2450,7 @@ module.exports = {
|
|
|
2322
2450
|
parseReviewVerdict,
|
|
2323
2451
|
isReviewBailout,
|
|
2324
2452
|
parseStructuredCompletion,
|
|
2453
|
+
detectNonTerminalResultSummary,
|
|
2325
2454
|
runPostCompletionHooks,
|
|
2326
2455
|
syncPrdFromPrs,
|
|
2327
2456
|
resolveWorkItemPath,
|
package/engine/timeout.js
CHANGED
|
@@ -147,7 +147,7 @@ function checkTimeouts(config) {
|
|
|
147
147
|
const engineRestartGraceUntil = engine().engineRestartGraceUntil;
|
|
148
148
|
const engineRestartGraceExempt = engine().engineRestartGraceExempt;
|
|
149
149
|
const { completeDispatch } = dispatch();
|
|
150
|
-
const { runPostCompletionHooks } = require('./lifecycle');
|
|
150
|
+
const { runPostCompletionHooks, parseAgentOutput, parseStructuredCompletion, detectNonTerminalResultSummary } = require('./lifecycle');
|
|
151
151
|
|
|
152
152
|
const timeout = config.engine?.agentTimeout || ENGINE_DEFAULTS.agentTimeout;
|
|
153
153
|
const defaultStaleOrphanTimeout = config.engine?.heartbeatTimeout || ENGINE_DEFAULTS.heartbeatTimeout;
|
|
@@ -257,12 +257,25 @@ function checkTimeouts(config) {
|
|
|
257
257
|
safeWrite(outputLogPath, `# Output for dispatch ${item.id}\n# Exit code: ${processExitCode}\n# Completed: ${ts()}\n# Detected via output scan\n\n## Result\n${text || '(no text)'}\n`);
|
|
258
258
|
} catch (e) { log('warn', 'parse output result: ' + e.message); }
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
const fullLogForHooks = safeRead(liveLogPath) || liveLogTail;
|
|
261
|
+
let completionDetection = null;
|
|
262
|
+
let outputResultSummary = '';
|
|
263
|
+
try {
|
|
264
|
+
const runtimeName = item.meta?.runtimeName || item.runtimeName || 'claude';
|
|
265
|
+
outputResultSummary = parseAgentOutput(fullLogForHooks, runtimeName).resultSummary || '';
|
|
266
|
+
const gateSummary = outputResultSummary || (!fullLogForHooks.includes('"type":') ? fullLogForHooks : '');
|
|
267
|
+
completionDetection = isSuccess
|
|
268
|
+
? detectNonTerminalResultSummary(gateSummary, parseStructuredCompletion(fullLogForHooks, runtimeName))
|
|
269
|
+
: null;
|
|
270
|
+
} catch (e) { log('warn', 'completion summary gate: ' + e.message); }
|
|
271
|
+
|
|
272
|
+
completeDispatch(item.id, completionDetection ? DISPATCH_RESULT.ERROR : (isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR),
|
|
273
|
+
completionDetection ? completionDetection.reason : (isSuccess ? 'Completed (detected from output)' : `Exited with code ${processExitCode} (detected from output)`),
|
|
274
|
+
outputResultSummary,
|
|
275
|
+
completionDetection ? { processWorkItemFailure: false } : {});
|
|
262
276
|
|
|
263
277
|
// Run post-completion hooks via shared helper (async — fire and forget in timeout context).
|
|
264
278
|
// Pass the actual exit code so autoRecovery (PR-created-but-failed) still works correctly.
|
|
265
|
-
const fullLogForHooks = safeRead(liveLogPath) || liveLogTail;
|
|
266
279
|
runPostCompletionHooks(item, item.agent, processExitCode, fullLogForHooks, config).catch(e => log('warn', 'post-completion hooks: ' + e.message));
|
|
267
280
|
|
|
268
281
|
if (hasProcess) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1650",
|
|
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"
|