@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 CHANGED
@@ -1,11 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1649 (2026-05-01)
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)
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-01T01:27:16.930Z"
4
+ "cachedAt": "2026-05-01T01:28:04.266Z"
5
5
  }
@@ -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, resultSummary, code);
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
- completeDispatch(item.id, isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR,
261
- isSuccess ? 'Completed (detected from output)' : `Exited with code ${processExitCode} (detected from output)`);
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.1649",
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"